이 글은 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
만 남았다. 결과를 보면 다음과 같은 절차로 값 할당이 처리된 것으로 생각할 수 있다.
x[idx[0]] = 1
수행x[idx[1]] = 2
수행idx[0]
과idx[1]
의 값이1
로 같으므로 결론적으로는x[1]
의 값이2
가 된다.
합리적 접근이지만 이 절차로는 다음 경우에 문제가 생긴다.
idx = [0, 0, 1, 1, 1]
x[idx] += 1
실행 결과를 보기 전 위에서 가정한 절차로 예상해 보자.
x[0]
은10
이니 첫 번째, 두 번째 인덱스에 대한 결과로+= 1
이 두 번 호출 돼12
가 된다.x[1]
은2
이니 셋, 넷, 다섯 번째 인덱스에 대한 결과로+= 1
이 세 번 호출 돼5
가 된다.
하지만 결과는…
x
array([11, 3, 12, 75, 14, 85, 16, 17, 18, 19])
x[0]
과 x[1]
요소에 대해 각각 += 1
연산이 한 번씩만 적용된 것을 볼 수 있다.
이와 관련해 파이썬 데이터 사이언스 핸드북 | 위키북스 에는 다음과 같이 설명돼 있다.
개념적으로 이것은
x[i] += 1
이x[i] = x[i] + 1
의 축약형을 의미하기 때문이다.x[i] + 1
이 평가되고 나면 결과가x
의 인덱스에 할당된다. 이 점을 생각하면 그것은 여러 차례 일어나는 증가가 아니라 할당이므로 보기와는 다른 결과를 가져온다.
당연히 x[i] += 1
은 x[i] = x[i] + 1
이고 그래서 예상한 결과가 나와야 하는 것 아닌가?
답답한 마음에 우리말이 어려운 것은 아닌지 원서 (Python Data Science Handbook | O’REILLY (Jake VanderPlas))를 봐도 별반 내용이 다르지 않다.
Conceptually, this is because
x[i] += 1
is meant as a shorthand ofx[i] = x[i] + 1
.x[1] + 1
is evaluated, and then the result is assigned to the indices inx
. With this in mind, it is not the augmentation that happens multiple times, but the assignment, which leads to the rather nonintuitive results.
멀리 돌아서 결론은…
가정을 해 보자. 위 두 경우를 모두 설명할 수 있도록.
- 인덱스 배열의 인덱스를 키(key), 할당값을 값(value)으로 갖는 사전을 만든다.
- 중복키에 대해서는 자연히 마지막 값으로 갱신된다.
- 이 사전을 배열에 적용하면 중복 인덱스에 대해 마지막 한 번만 적용된다.
많이 어설프지만 두 경우 모두 설명이 가능해 보인다.
…
…
…
x[i] += 1
은 x[i] = x[i] + 1
을 축약한 것이다.
이 문장은 x[0] += 1
과 x[0] = x[0] + 1
이 같다는 것이 아니라, x[[0, 0, 1, 1, 1]] += 1
이 x[[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]]
에 할당한 것이다.