앞선 포스팅에서 브로드캐스팅을 정리해봤다.
이번 포스팅에서 다룬 주제는 indexing과 slicing이다. 브로드캐스팅을 하려고 하더라도 데이터가 적절하지 않으면, 원하는 결과값을 얻지 못한다. 그런 맥락에서 데이터 전처리 과정 중 indexing과 slicing은 중요한 역할을 한다.
1. Slicing의 종류
1.1 ndarray[:]
import numpy as np
a = np.arange(10)
print(f"a: {a}") # a: [0 1 2 3 4 5 6 7 8 9]
print(f"a[:]: {a[:]}, {type(a[:])}") # a[:]: [0 1 2 3 4 5 6 7 8 9], <class 'numpy.ndarray'>
print(f"a[0]: {a[0]}, {type(a[0])}") # a[0]: 0, <class 'numpy.int64'>
print(f"a[1:2]: {a[1:2], {type(a[1:2])}}") # a[1:2]: (array([1]), {<class 'numpy.ndarray'>})
print(f"a[2:]: {a[2:]}") # a[2:]: [2 3 4 5 6 7 8 9]
print(f"a[:3]: {a[:3]}") # a[:3]: [0 1 2]
print(f"a[-1:]: {a[-1:]}") # a[-1:]: [9]
print(f"a[-3:]: {a[-3:]}") # a[-3:]: [7 8 9]
print(f"a[:-1]: {a[:-1]}") # a[:-1]: [0 1 2 3 4 5 6 7 8]
print(f"a[-2:-5]: {a[-2:-5]}") # a[-2:-5]: []
print(f"a[-5:-2]: {a[-5:-2]}") # a[-5:-2]: [5 6 7]
print(f"a[2:7:2]: {a[2:7:2]}") # a[2:7:2]: [2 4 6]
print(f"a[::3]: {a[::3]}") # a[::3]: [0 3 6 9]
먼저 ndarray[:]이다.
- python의 list문법과 동일하여 이해하기 쉽다.
1.1.1
print(f"a[:]: {a[:]}, {type(a[:])}") # a[:]: [0 1 2 3 4 5 6 7 8 9], <class 'numpy.ndarray'>
ndarray[] 사이에 ":"만 넣게 되면, 이전의 copy 방식과 동일하게 원본 array값을 그대로 가져온다
1.1.2
print(f"a[0]: {a[0]}, {type(a[0])}") # a[0]: 0, <class 'numpy.int64'>
print(f"a[1:2]: {a[1:2], {type(a[1:2])}}") # a[1:2]: (array([1]), {<class 'numpy.ndarray'>})
첫 번째 줄의 코드와 같이 [] 안에 하나의 상수 넣어서 ndarray의 요소 하나를 가져올 수도 있고,
두 번째 줄의 코드와 같이 [n:m]을 넣어서 요소들을 가져올 수도 있다. 여기서 중요한 것은
ndarray[n:m]이 가져오는 값은 nparray[n] ~ [m-1]를 가져온다는 것이다.
numpy indexing을 처음으로 배우고 있다면, 왜 한번 더 꽈놓은 형태로 만들었나 싶겠지만
nparray[n] ~ [m-1] 개념만 이해하면 요소들의 개수를 파악하기도 쉽고 여러모로 유용하다
1.1.3
print(f"a[2:]: {a[2:]}") # a[2:]: [2 3 4 5 6 7 8 9]
print(f"a[:3]: {a[:3]}") # a[:3]: [0 1 2]
첫 번째 줄의 코드와 같이 nparray[n:]의 형태로 괄호 안의 콜론의 왼쪽에만 값을 넣은 경우,
nparray[n:]이 가져오는 값은 nparray[n] ~ 끝이다. 위의 주석에서 값을 확인할 수 있다.
두 번째 줄의 코드는 첫 번째 줄의 코드와 반대로 콜론의 오른쪽에만 값을 넣은 경우이다. 이 경우,
nparray[:m]이 가져오는 값은 nparray[0] ~ [m-1]이다. 위의 주석에서 값을 확인할 수 있다.
1.1.4
print(f"a[-1:]: {a[-1:]}") # a[-1:]: [9]
print(f"a[-3:]: {a[-3:]}") # a[-3:]: [7 8 9]
print(f"a[:-1]: {a[:-1]}") # a[:-1]: [0 1 2 3 4 5 6 7 8]
print(f"a[-2:-5]: {a[-2:-5]}") # a[-2:-5]: []
print(f"a[-5:-2]: {a[-5:-2]}") # a[-5:-2]: [5 6 7]
첫 번째, 두 번째 줄의 코드와 같이 ndarray[n:], ndarray[:m]의 형태에서 n, m인 음수인 경우이다.
이 경우는 python의 list index방식처럼 뒤에서부터 바라보는 방식이다
a = np.arange(9) # [0 1 2 3 4 5 6 7 8]
# a의 index
# 0 1 2 3 4 5 6 7 8
# a의 역index
# -8 -7 -6 -5 -4 -3 -2 -1
위의 코드를 기준으로 바라보면,
a[-1:] = a[8:]
a[-3:] = a[6:]
a[:-1] = a[:8]
a[-1:] = a[8:]
a[-2:-5] = a[7:3] <- 때문에 값이 없었던 것
a[-5:-2] = a[3:7]
으로 바꿔 바라볼 수 있다.
이러한 방식은 ndarray의 모양, 크기가 상황에 따라 다른 경우, 활용하기 좋은 방식이다.
1.1.5
print(f"a[2:7:2]: {a[2:7:2]}") # a[2:7:2]: [2 4 6]
print(f"a[::3]: {a[::3]}") # a[::3]: [0 3 6 9]
위 방식 역시 python list 방식과 동일하다. ndarray[n:m:k]의 경우 k는 간격을 의미한다.
위 코드를 기준으로 바라보면,
a[2:7:2] = [2, 2+2, 2+2+2, 2+2+2+2(over size)] = [2, 4, 6]
a[::3] = [0, 0+3, 0+3+3, 0+3+3+3, 0+3+3+3+3(over size)] = [0, 3, 6, 9]
으로 바꿔 바라볼 수 있다.
index over size를 마주하기 전까지 간격만큼의 거리를 두고 slicing을 하는 방식이다.
1.1.6 다차원 배열
import numpy as np
a_py = [[0,1,2,3],[4,5,6,7],[8,9,10,11]]
a_np = np.arange(12).reshape((3, 4))
print(a_py)
# [[0, 1, 2, 3],
# [4, 5, 6, 7],
# [8, 9, 10, 11]]
print(a_np)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(a_py[0]) # [0, 1, 2, 3]
print(a_np[0]) # [0 1 2 3]
print(a_py[1][2]) # 6
print(a_np[1][2]) # 6
print(a_py[:2])
# [[0, 1, 2, 3],
# [4, 5, 6, 7]]
print(a_np[:2])
# [[0 1 2 3]
# [4 5 6 7]]
print(a_py[0][:3]) # [0, 1, 2]
print(a_np[0, :3]) # [0 1 2]
print(a_py[:3][0]) # [0, 1, 2, 3]
print(a_np[:3, 0]) # [0 4 8]
print(a_py[1:3][2:]) # []
print(a_np[1:3, 2:])
# [[ 6 7]
# [10 11]]
위의 코드를 통해 python list와 ndarray를 비교해봤다.
ndarray는 다차원의 경우, 하나의 괄호 안의 콤마를 통해 차원을 구분하는 방식을 취하고 있다.
값을 확인하는 코드들은 제외하고 중간부터 보겠다.
print(a_py[0][:3]) # [0, 1, 2]
print(a_np[0, :3]) # [0 1 2]
print(a_py[:3][0]) # [0, 1, 2, 3]
print(a_np[:3, 0]) # [0 4 8]
pythonlist[n:m][i:j]와 ndarray[n:m, i:j]를 보여주는 예제이다.
첫 번째 단락 코드를 먼저 바라보면
[0][:3]의 의미는 "0번행"의 " ~ (3-1)열"이다.
두 객체 모두 0번행이 [0,1,2,3]이기에 0~2번열인 [0, 1, 2]로 slicing 된다.
두 번째 단락 코드부터 값이 달라진다. 우리는 첫 번째 코드를 통해 생각해보았듯
[:3][0]의 의미는 " ~ (3-1)번행"의 "0번열"이다.
그런데, python list의 경우는 [0, 1, 2, 3]로 "0번행"으로 slicing 됐다. 내부 작동방식이 우리가 생각했던 것과 다른 듯하다.
반면, ndarray의 경우에는 0~2번행에서 0번행인 [0 4 8]로 적절하게 slicing 된다.
print(a_py[1:3][2:]) # []
print(a_np[1:3, 2:])
# [[ 6 7]
# [10 11]]
위에서 보듯이 python list는 우리가 원하는 slicing을 얻을 수 없지만, ndarray의 경우에는 직관성 좋게 slicing을 수행한다.
1.1.7 ndarray[n, ...]
import numpy as np
a = np.arange(3*4).reshape((3, 4))
print(a)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(a[:, 1]) # [1 5 9]
print(a[..., 1]) # [1 5 9]
print(a[2, :]) # [ 8 9 10 11]
print(a[2, ...]) # [ 8 9 10 11]
a = np.arange(3*3*5).reshape((3,3,5))
print(a)
# [[[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]]
# [[15 16 17 18 19]
# [20 21 22 23 24]
# [25 26 27 28 29]]
# [[30 31 32 33 34]
# [35 36 37 38 39]
# [40 41 42 43 44]]]
print(a[:, :, 1])
# [[ 1 6 11]
# [16 21 26]
# [31 36 41]]
print(a[..., 1])
# [[ 1 6 11]
# [16 21 26]
# [31 36 41]]
print(a[2, :, 1]) # [31 36 41]
print(a[2, ..., 1]) # [31 36 41]
a = np.arange(3*4*5*6*7*8).reshape((3, 4, 5, 6, 7 ,8))
print(a[2, :, 1].shape) # (4, 6, 7, 8)
print(a[2, :, :, :,:, 1].shape) # (4, 5, 6, 7)
print(a[2, ..., 1].shape) # (4, 5, 6, 7)
ndarray[n, ...]에서 "..."는 python ellipsis 문법이다.
a = np.arange(3*4*5*6*7*8).reshape((3, 4, 5, 6, 7 ,8))
print(a[2, :, 1].shape) # (4, 6, 7, 8)
print(a[2, :, :, :,:, 1].shape) # (4, 5, 6, 7)
print(a[2, ..., 1].shape) # (4, 5, 6, 7)
위 코드 단락처럼 다차원일 경우 중간을 생략해 부정확한 외부자료를 전처리할 때도 활용될 수 있겠다.
1.2 ndarray[ndarray]
import numpy as np
a = np.arange(3*3).reshape((3, 3))
print(a)
# [[0 1 2]
# [3 4 5]
# [6 7 8]]
idx1_py, idx2_py = [0,1], [1,2]
# print(a[idx1_py][idx2_py]) # IndexError : out of bounds
for idx1, idx2 in zip(idx1_py, idx2_py):
print(f"idx1: {idx1}, idx2: {idx2} : {a[idx1][idx2]}")
# idx1: 0, idx2: 1 : 1
# idx1: 1, idx2: 2 : 5
idx1_np, idx2_np = np.array([0,1]), np.array([1,2])
print(a[idx1_np, idx2_np])
# [1 5]
위의 코드처럼 다차원 배열을 pair indices를 이용해 for문 없이 ndarray형태로 나타낼 수 있다.
2. ndarray[Bools]
import numpy as np
a = np.arange(3*3).reshape((3, 3))
### Python ###
print(a)
# [[0 1 2]
# [3 4 5]
# [6 7 8]]
a_morethan3_bool = []
a_morethan3 = []
for row in a:
for col in row:
if col > 3:
a_morethan3_bool.append(True)
a_morethan3.append(col)
else:
a_morethan3_bool.append(False)
a_morethan3_bool = np.array(a_morethan3_bool).reshape(a.shape)
print(a_morethan3_bool)
# [[False False False]
# [False True True]
# [ True True True]]
print(a_morethan3) # [4, 5, 6, 7, 8]
### Numpy ###
print(a)
# [[0 1 2]
# [3 4 5]
# [6 7 8]]
print(a>3)
# [[False False False]
# [False True True]
# [ True True True]]
print(a[a>3])
# [4 5 6 7 8]
ㅋㅋ Numpy의 효율성을 보이려고 좀 과하게 짜봤다. 위 코드를 보면 알 수 있듯이 Numpy는 for문 없이 array의 요소에 접근해 조건에 값을 확인한 후, 조건에 해당하는 요소들을 ndarray의 형태로 리턴한다.
보기 편하니 좋네!
3. make like_array
import numpy as np
a = np.random.randint(0, 2, (3, 3))
print(a)
# [[0 1 0]
# [1 0 1]
# [1 1 1]]
print(np.nonzero(a), "\n")
# (array([0, 1, 1, 2, 2, 2]),
# array([1, 0, 2, 0, 1, 2]))
print(np.where(a), "\n")
# (array([0, 1, 1, 2, 2, 2]),
# array([1, 0, 2, 0, 1, 2]))
print(np.where(a, 1, -1), "\n")
# [[-1 1 -1]
# [ 1 -1 1]
# [ 1 1 1]]
x, y = np.arange(9).reshape(a.shape), np.arange(0, -9, -1).reshape(a.shape)
print(x, y)
# x y
# [[0 1 2] [[ 0 -1 -2]
# [3 4 5] [-3 -4 -5]
# [6 7 8]] [-6 -7 -8]]
print(np.where(a, x, y))
# [ 0 -1 -2]
# [-3 -4 -5]
# [ 6 7 -8]]
a = np.arange(9).reshape(3,3)
print(a[np.where(a>3)])
# [4 5 6 7 8]
np.nonzero(), np.where()는 위에서 설명한 nparray[bools], nparray[condition]를 좀 더 일반화하여 사용할 수 있게 돕는다.
3.1 np.nonzero()
- 값이 0이 아닌 요소들의 pair indices를 리턴한다. pair indices를 리턴한다면, 위 1.2와 같이 곧바로 활용할 수 있겠다.
a = np.random.randint(0, 2, (3, 3))
print(a)
# [[0 1 0]
# [1 0 1]
# [1 1 1]]
print(np.nonzero(a), "\n")
# (array([0, 1, 1, 2, 2, 2]),
# array([1, 0, 2, 0, 1, 2]))
3.2 np.where(arr, x, y)
- default인 경우, np.nonzero()와 동일하게 동작
print(np.where(a), "\n")
# (array([0, 1, 1, 2, 2, 2]),
# array([1, 0, 2, 0, 1, 2]))
print(np.where(a, 1, -1), "\n")
# [[-1 1 -1]
# [ 1 -1 1]
# [ 1 1 1]]
- 조건(condition)을 확인한 후
arr와 동일한 shape이고,
True인 경우, 해당 index에 -> x,
False인 경우, 해당 index에 ->y를 넣은 ndarray를 리턴한다.
import numpy as np
a = np.random.randint(-5, 5, (3, 3))
print(a)
# [[-4 2 -2]
# [ 3 4 3]
# [ 0 0 -2]]
print(np.where(a), "\n")
# (array([0, 0, 0, 1, 1, 1, 2]),
# array([0, 1, 2, 0, 1, 2, 2]))
print(np.where(a > 2, 100, a), "\n")
# [[ -4 2 -2]
# [100 100 100]
# [ 0 0 -2]]
x, y = np.arange(9).reshape(a.shape), np.arange(0, -9, -1).reshape(a.shape)
print(x, y)
# x y
# [[0 1 2] [[ 0 -1 -2]
# [3 4 5] [-3 -4 -5]
# [6 7 8]] [-6 -7 -8]]
print(np.where(a, x, y))
# [[ 0 1 2]
# [ 3 4 5]
# [-6 -7 8]]
a = np.arange(9).reshape(3,3)
print(a[np.where(a>3)])
# [4 5 6 7 8]
그런 특징들을 중심으로 코드를 작성해봤다.
이상으로 ndarray의 indexing, slicing에 대해 알아보았다. 참 가독성 높고, 효율적인 방식인 듯하다
'머신러닝 > Numpy' 카테고리의 다른 글
[Numpy] #9 np.cumsum(), .prod(), .cumprod(), .diff(), .median() 등 기초문법 공부하기 9 (0) | 2021.10.05 |
---|---|
[Numpy] #8 axis, sum(), mean(), keepdims 기초문법 공부하기 8 (0) | 2021.10.04 |
[Numpy] #6 브로드캐스팅(Broadcasting) 심화 기초문법 공부하기 6 (0) | 2021.09.29 |
[Numpy] #5 요소별 연산, 브로드캐스팅(Broadcasting) ndarray 가지고놀기 기초문법 공부하기 5 (0) | 2021.09.28 |
[Numpy] #4 reshape(-1), flatten(), copy(), ndarray 가지고놀기 기초문법 공부하기 4 (0) | 2021.09.28 |
댓글