본문 바로가기
머신러닝/Numpy

[Numpy] #7 indexing, slicing / 인덱싱, 슬라이싱 기초문법 공부하기 7

by doyou1 2021. 10. 4.
반응형

앞선 포스팅에서 브로드캐스팅을 정리해봤다.

이번 포스팅에서 다룬 주제는 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에 대해 알아보았다. 참 가독성 높고, 효율적인 방식인 듯하다

반응형

댓글