NumPy 팬시 인덱싱 값 할당 시 중복 인덱스 처리

인덱스 배열에 중복값이 포함된 경우 NumPy에서 처리하는 방법

이 글은 Jupyter Notebook에서 작성했습니다.

NumPy 팬시 인덱싱 (fancy indexing)은 배열 요소에 접근하기 위한 인덱스로 단일 스칼라 대신 배열을 전달하는 기능이다. 이를 통해 배열의 하위 집합에 빠르게 접근할 수 있다.

import numpy as np
x = np.arange(10, 20)
x
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])

위와 같은 배열이 있을 때 다음 코드로 1, 3, 5번째 요소에 접근할 수 있다.

sub_x = x[[0, 2, 4]]
sub_x
array([10, 12, 14])

팬시 인덱싱으로 배열 생성하기

위에서 본 것처럼 팬시 인덱싱을 활용하면 쉽게 원하는 요소들만 모아 다른 배열을 생성할 수 있다. 인덱스에 사용할 배열은 다차원일 수 있으며, 이 경우 생성된 배열은 인덱스 배열의 형상을 따른다.

idx = np.array([[1, 3], [6, 8]])
idx.shape
(2, 2)
sub_x_md = x[idx]
sub_x_md
array([[11, 13],
       [16, 18]])
sub_x_md.shape
(2, 2)

팬시 인덱싱으로 요소값 할당하기

인덱스 배열에 같은 값이 중복해 포함돼 있어도 하위 배열을 생성하고 이해하는데 전혀 문제가 발생하지 않는다.

x[[1, 1, 2, 2, 2]]
array([11, 11, 12, 12, 12])

하지만 값을 할당하는 기능으로 넘어가면 약간의 혼돈이 생긴다.

우선, 인덱스 배열에 중복이 없는 경우를 몇 가지 살펴보자.

idx = np.array([1, 3, 5])
x[idx] = 50
x
array([10, 50, 12, 50, 14, 50, 16, 17, 18, 19])

할당할 값도 배열로 지정할 수 있다.

x[idx] = [60, 70, 80]
x
array([10, 60, 12, 70, 14, 80, 16, 17, 18, 19])

연산도 가능하다.

x[idx] += 5
x
array([10, 65, 12, 75, 14, 85, 16, 17, 18, 19])

이제 예고한 상황을 살펴보자.

idx = np.array([1, 1])
x[idx] = [1, 2]
x
array([10,  2, 12, 75, 14, 85, 16, 17, 18, 19])

할당한 값 중 1은 없어지고 2만 남았다. 결과를 보면 다음과 같은 절차로 값 할당이 처리된 것으로 생각할 수 있다.

  1. x[idx[0]] = 1 수행
  2. x[idx[1]] = 2 수행
  3. idx[0]idx[1]의 값이 1로 같으므로 결론적으로는 x[1]의 값이 2가 된다.

합리적 접근이지만 이 절차로는 다음 경우에 문제가 생긴다.

idx = [0, 0, 1, 1, 1]
x[idx] += 1

실행 결과를 보기 전 위에서 가정한 절차로 예상해 보자.

  1. x[0]10이니 첫 번째, 두 번째 인덱스에 대한 결과로 += 1이 두 번 호출 돼 12가 된다.
  2. x[1]2이니 셋, 넷, 다섯 번째 인덱스에 대한 결과로 += 1이 세 번 호출 돼 5가 된다.

하지만 결과는…

x
array([11,  3, 12, 75, 14, 85, 16, 17, 18, 19])

x[0]x[1] 요소에 대해 각각 += 1 연산이 한 번씩만 적용된 것을 볼 수 있다.

이와 관련해 파이썬 데이터 사이언스 핸드북 | 위키북스 에는 다음과 같이 설명돼 있다.

개념적으로 이것은 x[i] += 1x[i] = x[i] + 1의 축약형을 의미하기 때문이다. x[i] + 1이 평가되고 나면 결과가 x의 인덱스에 할당된다. 이 점을 생각하면 그것은 여러 차례 일어나는 증가가 아니라 할당이므로 보기와는 다른 결과를 가져온다.

당연히 x[i] += 1x[i] = x[i] + 1이고 그래서 예상한 결과가 나와야 하는 것 아닌가?

답답한 마음에 우리말이 어려운 것은 아닌지 원서 (Python Data Science Handbook | O’REILLY (Jake VanderPlas))를 봐도 별반 내용이 다르지 않다.

Conceptually, this is because x[i] += 1 is meant as a shorthand of x[i] = x[i] + 1. x[1] + 1 is evaluated, and then the result is assigned to the indices in x. With this in mind, it is not the augmentation that happens multiple times, but the assignment, which leads to the rather nonintuitive results.

멀리 돌아서 결론은…

가정을 해 보자. 위 두 경우를 모두 설명할 수 있도록.

  1. 인덱스 배열의 인덱스를 키(key), 할당값을 값(value)으로 갖는 사전을 만든다.
  2. 중복키에 대해서는 자연히 마지막 값으로 갱신된다.
  3. 이 사전을 배열에 적용하면 중복 인덱스에 대해 마지막 한 번만 적용된다.

많이 어설프지만 두 경우 모두 설명이 가능해 보인다.

x[i] += 1x[i] = x[i] + 1을 축약한 것이다.

이 문장은 x[0] += 1x[0] = x[0] + 1이 같다는 것이 아니라, x[[0, 0, 1, 1, 1]] += 1x[[0, 0, 1, 1, 1]] = x[[0, 0, 1, 1, 1]] + 1과 같다는 의미였다.

x[[0, 0, 1, 1, 1]][10, 10, 2, 2, 2]이고 각각에 1을 더했으니 [11, 11, 3, 3, 3]이 된다. 이 배열을 다시 x[[0, 0, 1, 1, 1]]에 할당한 것이다.