넘파이 어레이: 인덱싱과 슬라이싱

주요 내용

리스트의 경우처럼 인덱싱과 슬라이싱을 이용하여 어레이 항목들을 확인, 수정할 수 있으며, 새로운 어레이를 생성할 수 있다. 여기서는 어레이의 인덱싱과 슬라이싱의 다양한 활용을 배운다.

1차원 어레이의 인덱싱과 슬라이싱

1차원 어레이의 경우 리스트의 인덱싱, 슬라이싱과 동일하게 처리한다. 다만, 슬라이싱의 경우 리턴값 또한 1차원 어레이이다.


In [1]:
import numpy as np

인덱싱

인덱싱은 숫자 0부터 인덱스가 시작하며, -1은 마지막 항목을 의미한다.


In [2]:
a = np.arange(10)
a


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

In [3]:
a[0], a[2], a[-1]


Out[3]:
(0, 2, 9)

슬라이싱

두 개의 콜론(':')을 사용하여 슬라이싱 구간의 처음과 끝, 그리고 스텝 옵션을 지정할 수 있다. -1은 역순으로 나열하는 것을 의미한다.


In [4]:
a[::-1]


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

In [5]:
a[2:9:3] # [시작:끝:계단]


Out[5]:
array([2, 5, 8])

In [6]:
a[:4]


Out[6]:
array([0, 1, 2, 3])

In [7]:
a[1:3]


Out[7]:
array([1, 2])

In [8]:
a[::2]


Out[8]:
array([0, 2, 4, 6, 8])

In [9]:
a[3:]


Out[9]:
array([3, 4, 5, 6, 7, 8, 9])

다차원 어레이의 인덱싱과 슬라이싱

다차원 어레이에서 사용되는 인덱싱은 인덱스 숫자들의 튜플을 사용한다. 사용된 튜플의 길이만큼 인덱싱의 대상이 되는 항목들의 차원이 증가한다.

리스트의 인덱싱 되돌아보기

먼저 중첩 리스트의 경우 인덱싱을 어떻게 하는지 확인해 보자.


In [10]:
b = [[0, 1, 2], [3, 4, 5]]

기본적으로 b는 길이가 2인 리스트이다.


In [11]:
len(b)


Out[11]:
2

b에서 0을 뽑아내고자 할 경우 인덱싱을 두 번 연속해서 사용해야 하는데, 이는 0b의 첫째 항목의 첫째 항목이기 때문이다. 즉,


In [12]:
b[0][0]


Out[12]:
0

5의 경우 둘째 항목의 셋째 항목이다.


In [13]:
b[1][2]


Out[13]:
5

다차원 어레이 인덱싱

반면에 b를 이차원 어레이로 다룰 경우에는 다음과 같이 하면 된다.


In [14]:
b = np.array([[0, 1, 2], [3, 4, 5]])
b


Out[14]:
array([[0, 1, 2],
       [3, 4, 5]])

이제 0을 뽑아내고자 할 때 2차원 어레이에 대한 인덱스를 사용하면 된다. 즉, 0번 인덱스 값의 0번 인덱스 값이란 의미로 [0, 0]을 사용한다.


In [15]:
b[0, 0]


Out[15]:
0

5의 경우는 1번 인덱스 값의 2번 인덱스 값이므로, [1, 2]를 사용한다.


In [16]:
b[1,2]


Out[16]:
5

그런데 이렇게 몇 번 인덱스 값의 몇 번 인덱스 값 이란 표현은 사용하기가 힘들다. 대신에 행과 열의 개념을 사용하는 것이 편하다.

예제: 어레이 b의 경우 0은 0행 0열의 값이며, 5는 1행 2열의 값이다. 따라서 아래 식을 사용한다.

주의: 인덱스는 0부터 시작한다.


In [17]:
b[0,0]


Out[17]:
0

In [18]:
b[1,2]


Out[18]:
5

보다 다양한 예제들을 이용하여 어레이 인덱싱을 연습해보자.

아래 코드에서 정의된 d는 2차원 어레이이며, 대각선에 0, 1, 2를 담고 있다.


In [19]:
d = np.diag(np.arange(3))
d


Out[19]:
array([[0, 0, 0],
       [0, 1, 0],
       [0, 0, 2]])

1의 위치는 1행 1열이므로 인덱싱을 (1, 1)을 이용한다.


In [20]:
d[1, 1]


Out[20]:
1

어레이 수정

어레이는 가변자료형이다. 따라서 리스트의 경우와 마찬가지로 인덱싱을 활용하여 항목을 수정할 수 있다.

먼저 리스트에서 인덱싱을 이용하여 항목을 수정하는 것을 확인해보자.


In [21]:
e = range(3)
e


Out[21]:
[0, 1, 2]

리스트 e의 2번 항목인 3을 5로 변경하려면 아래와 같이 인덱싱을 사용하면 된다.


In [22]:
e[2] = 5
e


Out[22]:
[0, 1, 5]

동일한 방식으로 어레이의 특정 항목을 변경할 수 있다.

예제

어레이 d의 2행 1열 항목값인 0을 10으로 변경하고자 하면 다음과 같이 인덱싱을 이용하면 된다.


In [23]:
d[2, 1] = 10
d


Out[23]:
array([[ 0,  0,  0],
       [ 0,  1,  0],
       [ 0, 10,  2]])

주의:

그렇다면 d[2]는 무엇을 의미할까? d를 리스트라 생각하면 된다. 즉, d의 2번 항목인 [0, 10, 2] 어레이를 가리킨다.


In [24]:
d[2]


Out[24]:
array([ 0, 10,  2])

다차원 어레이 슬라이싱

다차원 어레이의 슬라이싱은 좀 더 생소하다. 복잡하거나 어렵지는 않지만 작동방식에 좀 익숙해져야 한다.

다차원 어레이 생성

먼저, 다음과 같은 모양의 2차원 어레이를 생성해보자. 단, 수동으로 항목들을 입력하면 안된다.

0 1 2 3 4 5
10 11 12 13 14 15
20 21 22 23 24 25
30 31 32 33 34 35
40 41 42 43 44 45
50 51 52 53 54 55
  • 여러 가지 방법이 있을 수 있으나 shape을 변경하는 방법을 활용하는 것이 가장 기초적이다. 먼저, np.arange 함수를 이용하여 길이가 36인 어레이를 생성한 다음에 shape 값을 변경하여 2차원 어레이를 생성한다.

In [25]:
a = np.arange(36)
a.shape = (6,6)
a


Out[25]:
array([[ 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]])

In [26]:
a = np.arange(36).reshape((6,6))
a


Out[26]:
array([[ 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]])

이제 각 행별로 4의 배수를 더하면 원하는 어레이가 얻어지는 성질을 이용한다. 어레이와 숫자의 연산은 각 항목별로 실행된다는 점을 이용한다.


In [27]:
a = np.arange(36).reshape((6,6))

for i in range(len(a)):
    a[i] = a[i] + 4*i

a


Out[27]:
array([[ 0,  1,  2,  3,  4,  5],
       [10, 11, 12, 13, 14, 15],
       [20, 21, 22, 23, 24, 25],
       [30, 31, 32, 33, 34, 35],
       [40, 41, 42, 43, 44, 45],
       [50, 51, 52, 53, 54, 55]])

위 코드를 보다 친숙한 방식으로 구현한다면 다음과 같다.


In [28]:
a = np.arange(36).reshape((6,6))

for i in range(len(a)):
        for j in range(6):
            a[i, j] = a[i, j] + 4*i
            
a


Out[28]:
array([[ 0,  1,  2,  3,  4,  5],
       [10, 11, 12, 13, 14, 15],
       [20, 21, 22, 23, 24, 25],
       [30, 31, 32, 33, 34, 35],
       [40, 41, 42, 43, 44, 45],
       [50, 51, 52, 53, 54, 55]])
  • 브로드캐스팅이란 고급 기술을 이용하면 훨씬 간단하게 원하는 어레이를 구할 수 있다. 브로드캐스팅은 다음 시간에 자세히 다룬다.

In [29]:
a = np.arange(6) + np.arange(0, 51, 10)[:, np.newaxis]
a


Out[29]:
array([[ 0,  1,  2,  3,  4,  5],
       [10, 11, 12, 13, 14, 15],
       [20, 21, 22, 23, 24, 25],
       [30, 31, 32, 33, 34, 35],
       [40, 41, 42, 43, 44, 45],
       [50, 51, 52, 53, 54, 55]])

연습

이제 위 어레이를 이용하여 슬라이싱을 연습하도록 해보자.

먼저 아래 그림에 나와 있는대로 슬라이싱을 따라해보면서 슬라이싱이 어떻게 작동하는지 확인해보자.

코드의 색깔과 어레이 그림에서 표시된 동일한 색을 가진 상자들 사이의 관계를 주시하면 된다.

a[0, 3:5] 확인하기

먼저 a[0]를 확인한다.


In [30]:
a[0]


Out[30]:
array([0, 1, 2, 3, 4, 5])

이제 a[0][3:5]를 확인한다.


In [31]:
a[0][3:5]


Out[31]:
array([3, 4])

따라서 a[0, 3:5] 결과는 아래와 같다.


In [32]:
a[0, 3:5]


Out[32]:
array([3, 4])

a[4:, 4:] 확인하기

이 경우를 앞서의 경우처럼 확인할 수는 없다. 왜냐하면 a[4:]가 길이가 2인 어레이이기에 a[4:][4:] 는 빈 어레이가 된다.


In [33]:
a[4:]


Out[33]:
array([[40, 41, 42, 43, 44, 45],
       [50, 51, 52, 53, 54, 55]])

In [34]:
a[4:][4:]


Out[34]:
array([], shape=(0, 6), dtype=int64)

따라서 1차원 값과 2차원 값을 함께 생각해야 한다.

  • 첫번 째 '4:'는 4번 인덱스 행부터 마지막 행까지 전체를 의미한다.
  • 두번 째 '4:'는 앞서 선택한 4번 인덱스 행부터 마지막 행 각각에서 4번 인덱스 열부터 전체를 나타낸다.

In [35]:
a[4:, 4:]


Out[35]:
array([[44, 45],
       [54, 55]])

위와 비슷한 방식으로 해석하면 아래 코드도 이해할 수 있다.

  • ':' 은 모든 행을 대상으로 삼는 것이고
  • '2' 는 각 행에서 2번 인덱스 값을 선택한다는 의미이다.

따라서 아래 값이 나온다.


In [36]:
a[:, 2]


Out[36]:
array([ 2, 12, 22, 32, 42, 52])

이제 아래 코드를 이해할 수 있어야 한다.


In [37]:
a[2::2, ::2]


Out[37]:
array([[20, 22, 24],
       [40, 42, 44]])

슬라이싱을 이용한 어레이 변경

슬라이싱을 적절히 이용하면 원하는 모양의 어레이를 쉽게 구할 수 있다.

예제


In [38]:
c = np.arange(10)

아래 코드는 5번 인덱스 값부터 모두 10으로 일괄적으로 변경한다.


In [39]:
c[5:] = 10
c


Out[39]:
array([ 0,  1,  2,  3,  4, 10, 10, 10, 10, 10])

예제


In [40]:
d = np.arange(5)

아래 코드는 5번 이상의 인덱스 값을 특정 어레이의 항목값들로 대체한다.


In [41]:
c[5:] = d[::-1]
c


Out[41]:
array([0, 1, 2, 3, 4, 4, 3, 2, 1, 0])

연습

슬라이싱을 이용하여 아래 기능을 수행하는 함수 odd_numberseven_numbers를 구현하라.

  • odd_numbers 함수는 양의 정수 n을 입력 받아 0부터 n까지의 정수 중에서 홀수를 역순으로 담은 1차원 어레이를 리턴한다.

      In [31]: odd_numbers(10)
      Out[31]: array([9, 7, 5, 3, 1])
  • even_numbers 함수는 양의 정수 n을 입력 받아 0부터 n까지의 정수 중에서 짝수를 담은 1차원 어레이를 리턴한다.

      In [32]: even_numbers(10)
      Out[32]: array([0, 2, 4, 6, 8, 10])

힌트: arange(), 함수와 슬라이싱을 적절히 활용한다.


In [42]:
def odd_numbers(n):
    odds = np.arange(1, n, 2)
    return odds[::-1]

odd_numbers(10)


Out[42]:
array([9, 7, 5, 3, 1])

In [43]:
def even_numbers(n):
    evens = np.arange(0, n+1, 2)
    return evens

even_numbers(10)


Out[43]:
array([ 0,  2,  4,  6,  8, 10])

복사와 뷰(copies and views)

슬라이싱을 처리할 때 리스트의 경우와 어레이의 경우가 다르다.

  • 리스트 슬라이싱: 새로운 리스트를 복사(copy)해서 사용한다.
  • 어레이 슬라이싱: 어레이를 복사하지 않고 기존의 어레이를 사용하며 관찰(view)한다.

리스트 슬라이싱: 복사 사용


In [44]:
a = [1, 2, 3]

In [45]:
b = a[0:2]
b


Out[45]:
[1, 2]

b의 값을 변경해도 a는 영향을 받지 않는다.


In [46]:
b[0] = 4
b


Out[46]:
[4, 2]

In [47]:
a


Out[47]:
[1, 2, 3]

어레이 슬라이싱: 뷰 사용


In [48]:
c = np.array([1,2, 3])

In [49]:
d = c[0:2]
d


Out[49]:
array([1, 2])

d의 값을 변경하면 c의 값도 변경된다.


In [50]:
d[0] = 4
d


Out[50]:
array([4, 2])

In [51]:
c


Out[51]:
array([4, 2, 3])

어레이를 슬라이싱 하면 어레이를 새로 생성하는 것이 아니라 기존의 어레이를 관찰하며 필요한 정보만 보여준다. 즉, 슬라이싱을 위해 메모리를 새롭게 사용하지 않는다. 복사보다 뷰 방식을 사용하는 이유는 처리속도 및 메모리 활용성을 높이기 위해서이다.

뷰 방식을 사용하지 않으려면 copy 메소드를 사용하여 복사해야 한다.


In [52]:
e = np.arange(10)
g = e[::2].copy()
g


Out[52]:
array([0, 2, 4, 6, 8])

In [53]:
g[0] = 12
g


Out[53]:
array([12,  2,  4,  6,  8])

복사를 사용하였기 때문에 e는 변하지 않는다.


In [54]:
e


Out[54]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

뷰 방식으로 작동하는 함수 정리

지금까지 살펴 본 어레이와 관련된 기능들 중에서 뷰 방식으로 작동하는 것들은 다음과 같다.

  • 전치행렬 구하기
  • reshape 함수
  • 슬라이싱

마스크(mask) 인덱싱

True, False의 불리언 값들로만 이루어진 어레이를 마스크(mask)라 부른다. 마스크를 이용하여 인덱싱을 실행할 수 있다.

주의: 마스크 인덱싱은 뷰 방식을 따르지 않는다.

예제: 무작위로 생성된 정수들 중에서 3의 배수만 추출하기

무작위로 선택된 정수들의 어레이를 생성하기 위해서는 np.random.randint 함수를 이용한다.

주의: 무작위성을 높이기 위해 앞서 np.random.rand의 경우처럼 seed 값을 이용하자.


In [55]:
np.random.seed(5)

이제 0과 20 사이에서 무작위로 15개의 정수를 3 x 5 행렬의 모양으로 생성하자.


In [56]:
a = np.random.randint(0, 20, size=(3, 5))
a


Out[56]:
array([[ 3, 14, 15,  6, 16],
       [ 9,  8,  4,  7, 16],
       [16,  7, 12, 15, 17]])

a의 항목들이 3의 배수 여부를 알려주는 어레이는 다음처럼 생성할 수 있다.


In [57]:
mask = (a % 3 == 0)
mask


Out[57]:
array([[ True, False,  True,  True, False],
       [ True, False, False, False, False],
       [False, False,  True,  True, False]], dtype=bool)

주의: 어레이의 연산은 항목별로 실행된다.


In [58]:
a % 3


Out[58]:
array([[0, 2, 0, 0, 1],
       [0, 2, 1, 1, 1],
       [1, 1, 0, 0, 2]])

또한 두 어레이의 모양이 다르면 브로드캐스팅 기능을 이용하여 두 어레이의 모양을 가능하면 통일시킨다. 물론, 모양의 통일이 불가능하면 오류가 발생한다. 브로드캐스팅에 대해서는 다음 시간에 자세히 배운다.

아래 코드에서 a % 33 x 5 모양의 2차원 어레이 인 반면에 숫자 0은 어레이가 전혀 아니다. 하지만 숫자 0으로 이루어진 3 x 5 모양의 어레이로 모양을 만들면 두 어레이가 동일한 모양을 갖게 되고, 따라서 항목별로 비교가 가능해진다.

사실은 a % 3 도 이와 같은 방식으로 이해할 수 있다. 즉, 먼저 3으로 이루어진 3 x 5 모양의 어레이로 만든 다음에 두 어레이 간의 나머지 연산을 실행한다.


In [59]:
a % 3 == 0


Out[59]:
array([[ True, False,  True,  True, False],
       [ True, False, False, False, False],
       [False, False,  True,  True, False]], dtype=bool)

이제 a의 항목들 중에서 mask의 항목들 중 True가 위치한 곳과 동일한 곳에 위치한 항목들만 뽑아서 1차원 어레이를 만들어 준다. 즉, a의 항목들 중에서 3의 배수인 숫자들만 뽑아서 어레이로 만들어서 리턴한다.


In [60]:
multiple_3 = a[mask]
multiple_3


Out[60]:
array([ 3, 15,  6,  9, 12, 15])

다음처럼 보다 간단한 구현이 가능하다.


In [61]:
a[a % 3 == 0]


Out[61]:
array([ 3, 15,  6,  9, 12, 15])

마스크 인덱싱 활용

마스크 인덱싱을 이용하여 특정 항목들의 값을 동시에 변경할 수 있다. 예를 들어, 생성된 정수들의 어레이어서 3배수값들만을 -1로 변경하고 할 때 마스크 인덱싱 기능을 활용할 수 있다.


In [62]:
a[a % 3 == 0] = -1
a


Out[62]:
array([[-1, 14, -1, -1, 16],
       [-1,  8,  4,  7, 16],
       [16,  7, -1, -1, 17]])