파이썬 넘파이 Numpy 인덱싱과 슬라이싱 (불리언, 팬시, where)

지난 시간에는 파이썬 넘파이 배열끼리 사칙연산을 하고 벡터의 내적 (행렬곱) 하는 방법에 대해 알아보았다.

이전글 : 파이썬 넘파이 배열연산 : broadcast, 벡터의 내적, 행렬곱

이번에는 numpy array 에서 특정 위치를 추출하는 인덱싱과 슬라이싱 방법에 대해 공부한다.

1차원 배열 인덱싱과 슬라이싱

넘파이 어레이가 1차원 배열일 경우 인덱싱과 슬라이싱은 기본적으로는 리스트에서 했던것과 동일하다.

생각이 안난다면 리스트 총정리 글을 다시 복습

import numpy as np

a = np.arange(10, 100, 10)
a

array([10, 20, 30, 40, 50, 60, 70, 80, 90])


b = list(range(10,100,10))
b

[10, 20, 30, 40, 50, 60, 70, 80, 90]

🔺 arange를 이용해 10에서 100(미포함) 까지 10 단위의 1차원 어레이 (1d array)를 하나 생성하였다. 동일한 방법으로 range 함수를 이용해 같은 원소값을 가지는 리스트 b를 만들었다.

a[0], a[5]

(10, 60)


b[0], b[5]

(10, 60)

🔺 이렇게 변수명[인덱스번호] 를 지정하면 해당 인덱스에 있는 원소값만 불러올 수 있다. 리스트에서 했던 것과 같다. 결과도 동일하게 나오는 것을 볼 수 있다.

구간 지정

a[1:4]
array([20, 30, 40])

b[1:4]
[20, 30, 40]

a[:4]
array([10, 20, 30, 40])

b[:4]
[10, 20, 30, 40]

a[4:]
array([50, 60, 70, 80, 90])

b[4:]
[50, 60, 70, 80, 90]

🔺 [1:4] 라고 인덱스 번호 1번에서 3번까지로 슬라이싱 하는경우, [:4]로 맨 앞에서 3번까지 슬라이싱 하는경우, [4:]로 인덱스 번호 4번에서 끝까지 자르는 경우 모두 리스트에서 했던 것과 같은 결과를 얻는다.

a[:]
array([10, 20, 30, 40, 50, 60, 70, 80, 90])

b[:]
[10, 20, 30, 40, 50, 60, 70, 80, 90]

a[1:8:2]
array([20, 40, 60, 80])

b[1:8:2]
[20, 40, 60, 80]

🔺 [:] 전체 구간을 지정하는 것, [start : end : step] 으로 인덱스 시작과 끝 정해진 구간에서 몇개마다 추출할 것인지 지정하는 방법도 동일하다.

슬라이싱 수정시 원본도 변경

인덱싱과 슬라이싱 방법은 리스트와 동일한데 한가지 큰 차이점이 있다. ⚡️⚡️⚡️

슬라이싱해서 특정 요소를 변경시, 원본도 같이 바뀌어 버린다는 점이다. 무슨 말인지 예시를 통해 살펴보자.

a = np.arange(10, 100, 10)
a

array([10, 20, 30, 40, 50, 60, 70, 80, 90])

b = list(range(10,100,10))
b

[10, 20, 30, 40, 50, 60, 70, 80, 90]

🔺 방금 만든 array a와 list b를 다시 사용해보겠다.

a1 = a[:4]   # a를 슬라이싱해서 a1으로 지정
a1
array([10, 20, 30, 40])

a1[0] = 100  # a1의 0번 인덱스를 수정
a1
array([100,  20,  30,  40])

a
array([100,  20,  30,  40,  50,  60,  70,  80,  90]) # a[0]도 바뀜

🔺 1차원 배열 a에서 인덱스 0~3번까지만 슬라이싱해서 a1이라는 새로운 배열을 생성하고, a1의 0번째 인덱스를 10에서 100으로 변경해주었다. 이 경우 a1의 0번째 인덱스 뿐만 아니라, 원본인 a의 0번째 인덱스도 같이 수정된 것을 알 수 있다.

이말은 배열을 슬라이싱해서 새로운 변수명에 넣어줘도, 새로운 메모리 주소에 데이터를 복제한 것이 아니라 기존 데이터를 같이 가리키고 있다는 말이 된다. 이걸 모르고 복사했다고 생각하고 수정 작업을 하다가는 원본이 엉망이 되는 불상사가 발생할 수 있다.

b1 = b[:4]     # b를 슬라이싱해서 b1 리스트 생성
b1
[10, 20, 30, 40]

b1[0] = 100    # b1 리스트의 0번째 인덱스를 변경
b1
[100, 20, 30, 40]

b        
[10, 20, 30, 40, 50, 60, 70, 80, 90]   # 원본 b는 안바뀜

🔺 반면에 리스트에서는 이렇게 슬라이싱해서 만든 새로운 리스트를 바꿔주어도, 원본 리스트의 데이터는 바뀌지 않는다. 새로운 변수명으로 만든 순간 원본과 별개의 메모리 주소에 새로운 데이터를 가지는 각각 따로인 상태가 된 것이다.

.copy()

a1 = a[:4].copy()     # a를 슬라이싱해서 복사본 a1 생성
a1
array([10, 20, 30, 40])

a1[0] = 100    # a1 원소 변경
a1
array([100,  20,  30,  40])

a   
array([10, 20, 30, 40, 50, 60, 70, 80, 90])  # 원본 a는 안바뀜

🔺 원본이 변경되는 것을 원치 않는다면 이렇게 copy 메소드를 붙여서 생성해주면 리스트처럼 각각 따로인 사본이 만들어진다. 이 경우에는 사본의 특정 원소를 변경해도 원본 a는 수정되지 않았음을 알 수 있다.

2차원 배열 인덱싱과 슬라이싱

이제 numpy array가 2차원 배열인 경우 특정 위치를 인덱싱하고, 원하는 구간을 짤라서 슬라이싱하는 방법을 살펴본다.

import numpy as np

array_2d = np.arange(12).reshape(4,3)
array_2d

array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

🔺 (4,3) shape을 가지는 2차원 어레이를 array_2d 라는 이름으로 생성하였다.

참고로 numpy 예제 실습할 때 이렇게 arange로 개수를 만들고 그걸 바로 .reshape 해서 행열지정 해주면 쉽게 만들 수 있다. np.array([[1행내용], [2행내용], [3행내용]]) 이렇게 괄호로 구분하면서 일일이 다 써주지 않더라도 말이다.

array_2d[1]   # 1번 행만 출력
array([3, 4, 5])

array_2d[2][2]   # 2번 행에 2번째 열 원소를 출력
8

🔺 array 변수명 뒤에 [ ] 붙여서 바로 실행하면 행번호가 나온다. 2차원 배열의 인덱싱도 기본적으로 2차원 리스트에서 했던것과 비슷한데, 먼저 큰 괄호의 인덱싱을 하고 그 안에서 다시 인덱싱을 하는 방식이다.

[2][2] 라고 하면 첫번째 2는 0, 1, 2 즉 세번째 행을 고르게 되고

두번째 2는 거기에서 0, 1, 2 세번째 원소를 픽한다. 그래서 결과는 8

array_2d[2, 2]
8

🔺 어레이의 경우는 이렇게 [2][2]가 아니라 [2, 2]와 같이 괄호 안에 한번에 써도 된다. 이게 리스트에서는 안되는 것이 차이점이다. 

test1 = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]]
test1

test1[2][2]
8

test1[2,2]
TypeError: list indices must be integers or slices, not tuple

🔺 2차원 리스트를 하나 만든 다음 인덱싱 방법을 두가지로 해보면, [2, 2]로 지정할 시 타입에러가 발생한다.

array_2d
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])


array_2d[:,1]    # 행은 전체, 열은 1번째
array([ 1,  4,  7, 10])

array_2d[1, :]   # 행은 1번째, 열은 전체
array([3, 4, 5])

array_2d[1:3,1:3]   # 행 1~2번째, 열 1~2번째
array([[4, 5],
       [7, 8]])

array_2d[1:3, 1:]   # 행 1~2번째, 열은 1~전부
array([[4, 5],
       [7, 8]])

🔺 배열의 슬라이싱은 괄호 하나로 할 수 있어서 리스트보다 좀 더 간편하게 작성할 수 있다. [행범위 : 열범위] 식으로만 써주면 된다. 이 때 행이든 열이든 한 줄만 슬라이싱하면 위와 같이 괄호 하나에 1차원 배열로 바뀌어서 출력된다.

불리언 인덱싱

불리언 (Boolean) 이란 어떤 조건에 해당되면 True, 만족하지 않으면 False를 반환하는 것이다. 

a = np.array([10,20,30,40])
a

array([10, 20, 30, 40])

🔺 이렇게 1차원 배열 a라는 것을 하나 만들고,

select = [True, False, True, True]
a[select]

array([10, 30, 40])

🔺 True, False로 이루어진 리스트를 하나 생성한다. 그리고 그 불리언 리스트로 a를 인덱싱을 하면, True에 해당하는 위치의 원소들만 출력되게 된다. 일종의 필터링과도 같은 느낌이다.

a > 20
array([False, False,  True,  True])

a[a > 20]
array([30, 40])

🔺 a>20이라고 한다던지, a==10 이라고 한다던지 배열에 논리연산자를 적용하면 각 원소별로 True/False 결과를 데이터로 갖는 어레이가 하나 생긴다. 그 불리언 형태 어레이를 다시 원래의 배열에 인덱싱 조건으로 쓰면 필터링을 할 수 있다.

a[a > 20] 이렇게 한번에 하는 것과 b = a >20 이라고 새로운 변수명에 담아준 뒤 a[b] 이렇게 인덱싱 하는 것과 결과는 같다.

a = np.arange(12).reshape(4,3)
a

array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

🔺 다시 2차원 배열을 하나 만들어주고 불리언 인덱싱을 적용해보자.

a[[True, False, False, True], :]

array([[ 0,  1,  2],
       [ 9, 10, 11]])


a[[True, False, False, True]]
array([[ 0,  1,  2],
       [ 9, 10, 11]])

🔺 이렇게 하면 행 기준으로 불리언 인덱싱을 해서 선택/미선택/미선택/선택 적용하고, 열에는 : 넣어놨으니 전체를 선택한다. 첫번째 네번째 행 두개만 결과로 반환됨을 알 수 있다.

두번째 예시처럼 열 부분은 안써도 결과는 같다. 인덱싱 조건 한가지만 들어가 있다면 기본적으로 행에 적용된다.

a[:, [True, False, True]]

array([[ 0,  2],
       [ 3,  5],
       [ 6,  8],
       [ 9, 11]])

🔺 그래서 열 중에서 취사선택하려면 앞에 : , 이렇게 행부분 조건도 넣어주어야 한다. 안그러면 행을 불리언 인덱싱하는 것으로 인식하니.

select =  np.array([[True, False, False],
                   [False, True, False],
                   [True, False, False],
                   [False, True, True]])
a[select]

array([ 0,  4,  6, 10, 11])

🔺 아예 이렇게 shape이 똑같은 어레이에 불리언 True/False 값을 넣어주고 그걸로 인덱싱을 할 수도 있다. 이 때는 뽑힌 원소들이 1차원 배열로 반환된다.

a[a > 7]

array([ 8,  9, 10, 11])

🔺 2차원 배열에서도 a >7 이라는 연을 해주면 똑같은 shape의 불리언 배열이 하나 생기는데 그걸로 인덱싱을 시킬 수 있다. 이때도 결과는 일치하는 애들만 1차원 배열로 반환된다.

팬시 인덱싱

팬시 (Fancy) 인덱싱은 특정 위치들만 뽑아주는 방법이다. 위에 예시들처럼 특정 구간이나 조건같은 걸로 일괄적으로 뽑기가 어렵고 불규칙적인 인덱싱을 해야할 경우 사용한다.

a = np.array([10,20,30,40])
a

array([10, 20, 30, 40])


select = [0, 3]
a[select]

array([10, 40])

🔺 이렇게 특정 인덱스 위치를 지정하는 형태의 리스트를 만들어 준 뒤, 그것을 인덱싱 조건으로 적용하는 방식이다.

별거 아닌거 같은데 아까 2차원 어레이 기본 인덱싱 방법과 비교하면 괄호가 추가되는 차이점이 있으니 잘 알아두어야 한다.

b = np.arange(9).reshape(3,3)
b

array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])


b[0,2]     # 기본 행 열 인덱싱 방법
2

b[[0,2]]   # 특정 행들만 고르는 팬시 인덱싱
array([[0, 1, 2],
       [6, 7, 8]])

🔺 첫번째 예시는 [0, 2]를 인덱싱했는데 이렇게 하면 0행 2열의 원소를 추출한다는 뜻이 된다. 그런데 0행과 2행 두개의 행만 뽑아서 가져오려면 어떻게 해야할까?

밑에 예시처럼 [0, 2] 이렇게 추출할 행들을 하나의 괄호로 묶어주고 넣어야 한다. 그래서 괄호가 이중으로 되어있다. 이것은 열이 생략되어 있는 것이며 다시 쓰면 아래와 같다.

b[[0,2], : ]

array([[0, 1, 2],
       [6, 7, 8]])

🔺 행은 0번째 2번째 가져오고 열은 전체를 가져온다고 한것과 같은 의미이다.

어레이명[인덱싱 조건] >> 이런 형태로 지정해 줄 때

어레이명[행 , 열] >> 이렇게 하는 것이 기본적인 방법이고 확장하면

어레이명[행시작:행끝 , 열시작:열끝]  >> 이렇게 범위로 슬라이싱 할 수 있고

어레이명[[행1, 행2] , [열1, 열2]]  >> 이렇게 행 열을 각각 특정위치들을 지정하는 것이 팬시 인덱싱이다.

b = np.arange(9).reshape(3,3)
b

array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])


b[[0,2,2],[0,1,0]]

array([0, 7, 6])

🔺 이렇게 shape(3, 3) 2차원 배열에서 3개의 원소만 콕 찝어내고 싶을 때 각각의 행 인덱스와 열 인덱스를 입력해서 추출하는 것이 팬시 인덱싱이다.

조건검색 where

아까 배열에 연산자 붙여서 조건문을 만들어주면 바로 불리언 배열이 생성된다고 했는데,

where 함수를 이용하면 조건을 만족하는 원소들이 어디에 있는지 그 인덱스들만 뽑아준다.

a = np.array([10, 20, 30, 40])
a
array([10, 20, 30, 40])


np.where( a > 20)

(array([2, 3]),)

🔺 원소 4개를 가지는 1차원 배열에서 a > 20 을 만족하는 원소가 어디있는지 where 찾아라 라는 명령어이다. 그런데 결과값을 보면 괄호가 좀 많고 이상한 형태로 보이는데 원래 이런 식이어서 그렇다

where조건 만족하는

( array의 ([행 값들 팬시 인덱싱]) ,  array의 ([열 값들 팬시 인덱싱]) )

튜플 괄호안에 어레이 두 개가 들어가는 형태로 주는 것인데 위 예시가 1차원 배열이라서 열 값이 따로 없으니까 뒷부분이 비어있고 콤마만 있는 것이다.

연속된 범위가 아닌 특정 행이나 열 인덱스 값들을 나열해야 하기 때문에 앞에서 본 팬시 인덱싱 형태로 반환된다.

a[ np.where(a > 20)]

array([30, 40])

🔺 이렇게 where 함수로 만든 인덱스를 다시 원래 어레이 a에 씌워서 그 위치의 원소들만 가져오는 인덱싱을 할 수 있다.

b = np.arange(12).reshape(4,3)   # 2차원 배열 생성
b
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

np.where(b > 6)    # 6보다 큰 원소들의 위치 인덱싱
(array([2, 2, 3, 3, 3]), array([1, 2, 0, 1, 2]))

b[np.where(b > 6)]  # 인덱싱 배열로 원소 값들을 추출
array([ 7,  8,  9, 10, 11])

🔺 2차원에서 where로 인덱스 추출하면 위와 같은 행/열 어레이 두개짜리 결과가 반환되고 이것을 이용해서 다시 원래 함수에 인덱싱하여 조건을 만족하는 원소들만 뽑아낼 수 있다.

기타 : nonzero, all, any

몇 가지 numpy array 인덱싱/슬라이싱에 쓰이는 함수들을 추가로 살펴본다.

a = np.array([0,1,2,0,1,2])
np.nonzero(a)

(array([1, 2, 4, 5]),)

🔺 nonzero는 0이 아닌 요소들의 인덱스들을 튜플 어레이 형태로 반환해준다. (반환 형태는 where 함수와 동일)

np.nonzero(np.array([True, False, True, False]))
(array([0, 2]),)

np.where(np.array([True, False, True, False]))
(array([0, 2]),)

🔺 True=1 , False=0이기 때문에 불리언 배열에 nonzero를 이용해 True의 위치들만 찾아줄 수 있다. where을 써서 True의 위치들만 찾는 것과 동일한 결과를 얻는다.

b = np.array([10, 20, 30, 40])
b
array([10, 20, 30, 40])


b > 20
array([False, False,  True,  True])


np.nonzero(b > 20)    # 예시1
(array([2, 3]),)


np.where(b > 20)    # 예시2
(array([2, 3]),)

🔺 이 단계별 예시를 보면 보다 명확하다. b > 20 이렇게 배열에 연산기호로 조건만 써도 불리언 배열이 결과로 얻어진다고 하였다.

즉, 위에서 예시1은 불리언 배열로 nonzero 예시2도 불리언 배열로 where를 하는 것이니 True위치를 찾으라는 같은 소리인 셈이다.

np.any([True, False, False])
True

np.any([False, False, False])
False

🔺 any 함수는 어레이 내에 True가 한개라도 있으면 결과값으로 True를, 전부 False일 경우 False를 반환한다.

np.all([True, True, True])
True

np.all([True, False, True])
False

🔺 반대로 all 함수는 모두 True일 때에만 True를, 하나라도 False가 있으면 False를 반환한다.

여기까지 Numpy Array를 인덱싱하고 슬라이싱하는 여러가지 방법들에 대해 알아보았다. 다음 시간에는 배열끼리 합치고 분리하는 방법에 대해 공부해본다.

평점 0 / 5. 참여 : 0

첫번째 평가를 남겨주세요!