15. 어레이 변형#

어레이의 모양을 변형해서 새로운 어레이를 생성하는 다양한 방식을 살펴본다.

주요 내용

  • 어레이 변형

  • 차원 추가와 삭제

  • 어레이 이어붙이기/쌓기

  • 브로드캐스팅

기본 설정

numpy 모듈과 시각화 도구 모듈인 matplotlib.pyplot에 대한 기본 설정을 지정한다.

# 넘파이
import numpy as np

# 램덤 시드
np.random.seed(12345)

# 어레이 사용되는 부동소수점들의 정확도 지정
np.set_printoptions(precision=4, suppress=True)
# 파이플롯
import matplotlib.pyplot as plt

# 도표 크기 지정
plt.rc('figure', figsize=(10, 6))

15.1. 차원 추가와 삭제#

reshape() 메서드와 함께 항목은 그대로 유지하면서 어레이의 모양만 변형시키는 방법으로 차원 추가와 삭제 기법이 활용된다.

15.1.1. 차원 추가#

어레이에 임의의 축을 추가하는 방식으로 차원이 하나 더 추가된 어레이를 생성할 수 있다. 어느 축을 추가하느냐에 따라 생성된 어레이의 모양은 달라진다.

예제

다음 길이가 3인 1차원 어레이를 이용하자.

arr_1d = np.random.normal(size=3)
arr_1d
array([-0.2047,  0.4789, -0.5194])

arr_1d는 원래 0번 축 하나만 갖는데, 아래 코드는 여기에 1번 축을 추가하여 2차원 어레이로 만든다.

arr_1d[:, np.newaxis]
array([[-0.2047],
       [ 0.4789],
       [-0.5194]])

reshape() 메소드로도 동일한 결과를 얻을 수 있다.

arr_1d.reshape((3, 1))
array([[-0.2047],
       [ 0.4789],
       [-0.5194]])

아래 코드는 기존의 0번 축을 1번 축으로 바꾼다.

arr_1d[np.newaxis, :]
array([[-0.2047,  0.4789, -0.5194]])

reshape() 메소드로도 동일한 결과를 얻을 수 있다.

arr_1d.reshape((1,3))
array([[-0.2047,  0.4789, -0.5194]])

예제

2차원 어레이에 축을 추가하면 3차원 어레이가 생성되며, 작동방식은 앞서와 동일하다.

arr = np.random.normal(size=(4, 3))
arr
array([[-0.5557,  1.9658,  1.3934],
       [ 0.0929,  0.2817,  0.769 ],
       [ 1.2464,  1.0072, -1.2962],
       [ 0.275 ,  0.2289,  1.3529]])
arr[:,:,np.newaxis].shape
(4, 3, 1)
arr[:,:,np.newaxis]
array([[[-0.5557],
        [ 1.9658],
        [ 1.3934]],

       [[ 0.0929],
        [ 0.2817],
        [ 0.769 ]],

       [[ 1.2464],
        [ 1.0072],
        [-1.2962]],

       [[ 0.275 ],
        [ 0.2289],
        [ 1.3529]]])
arr[:,np.newaxis,:].shape
(4, 1, 3)
arr[:,np.newaxis,:]
array([[[-0.5557,  1.9658,  1.3934]],

       [[ 0.0929,  0.2817,  0.769 ]],

       [[ 1.2464,  1.0072, -1.2962]],

       [[ 0.275 ,  0.2289,  1.3529]]])

15.1.2. 차원 삭제#

ravel() 메서드와 flatten() 메서드는 어레이를 1차원으로 변형한다. 즉, 차원을 모두 없앤다.

arr = np.arange(15).reshape((5, 3))
arr
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11],
       [12, 13, 14]])
arr1 = arr.ravel()
arr1
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])
arr2 = arr.flatten()
arr2
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

차이점은 ravel() 메서드는 뷰(view)를 사용하는 반면에 flatten() 메서드는 어레이를 새로 생성한다. 예를 들어, 아래처럼 arr1의 항목을 변경하면 arr의 항목도 함께 변경된다.

arr1[0] = -1
arr
array([[-1,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11],
       [12, 13, 14]])

arr2arr과 전혀 상관이 없다.

arr2[0] = -7
arr
array([[-1,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11],
       [12, 13, 14]])

15.2. 어레이 쪼개기#

np.split() 함수

어레이를 지정된 기준에 따라 여러 개의 어레이로 쪼갠다. 반환값은 쪼개진 어레이들의 리스트다.

아래 예제를 살펴보자.

arr = np.random.randn(6, 5)
arr
array([[ 0.8864, -2.0016, -0.3718,  1.669 , -0.4386],
       [-0.5397,  0.477 ,  3.2489, -1.0212, -0.5771],
       [ 0.1241,  0.3026,  0.5238,  0.0009,  1.3438],
       [-0.7135, -0.8312, -2.3702, -1.8608, -0.8608],
       [ 0.5601, -1.2659,  0.1198, -1.0635,  0.3329],
       [-2.3594, -0.1995, -1.542 , -0.9707, -1.307 ]])

np.split() 함수의 인자는 하나의 인덱스이거나 여러 인덱스들의 리스트가 사용된다. 먼저, 정수 리스트가 들어오면 축이 정한 방향으로 리스트에 포함된 정수를 이용하여 여러 개의 구간으로 쪼갠다.

아래 코드는 행을 기준으로 행의 인덱스를 0-1, 2, 3-5 세 개의 구간으로 쪼갠다. 따라서 결과는 네 개의 어레이로 이루어진 리스트가 되며, 각 어레의 모양은 다음과 같다.

(2, 5), (1, 5), (3, 5)
np.split(arr, [2, 3]) # np.split(arr, [2, 3],axis=0)
[array([[ 0.8864, -2.0016, -0.3718,  1.669 , -0.4386],
        [-0.5397,  0.477 ,  3.2489, -1.0212, -0.5771]]),
 array([[0.1241, 0.3026, 0.5238, 0.0009, 1.3438]]),
 array([[-0.7135, -0.8312, -2.3702, -1.8608, -0.8608],
        [ 0.5601, -1.2659,  0.1198, -1.0635,  0.3329],
        [-2.3594, -0.1995, -1.542 , -0.9707, -1.307 ]])]

반면에 열을 기준으로 0, 1-2, 3-4 세 개의 구간으로 쪼개면 다음과 같으며, 각 어레이의 모양은 다음과 같다.

(7, 1) (7, 2), (7, 2)
np.split(arr, [1, 3], axis=1)
[array([[ 0.8864],
        [-0.5397],
        [ 0.1241],
        [-0.7135],
        [ 0.5601],
        [-2.3594]]),
 array([[-2.0016, -0.3718],
        [ 0.477 ,  3.2489],
        [ 0.3026,  0.5238],
        [-0.8312, -2.3702],
        [-1.2659,  0.1198],
        [-0.1995, -1.542 ]]),
 array([[ 1.669 , -0.4386],
        [-1.0212, -0.5771],
        [ 0.0009,  1.3438],
        [-1.8608, -0.8608],
        [-1.0635,  0.3329],
        [-0.9707, -1.307 ]])]

둘째 인자로 하나의 정수를 사용하면 행 또는 열을 기준으로 등분한다. 아래 코드는 6개의 행을 3등분 한다.

np.split(arr, 3)
[array([[ 0.8864, -2.0016, -0.3718,  1.669 , -0.4386],
        [-0.5397,  0.477 ,  3.2489, -1.0212, -0.5771]]),
 array([[ 0.1241,  0.3026,  0.5238,  0.0009,  1.3438],
        [-0.7135, -0.8312, -2.3702, -1.8608, -0.8608]]),
 array([[ 0.5601, -1.2659,  0.1198, -1.0635,  0.3329],
        [-2.3594, -0.1995, -1.542 , -0.9707, -1.307 ]])]

등분을 위해서는 행 또는 열의 약수만 둘째 인자로 사용할 수 있다. 그렇지 않으면 오류가 발생한다. 예를 들어, 아래 코드처럼 열을 2등분 하려 하면 오류가 발생한다. 이유는 5개의 열을 2등분 할 수 없기 때문이다.

>>> np.split(arr, 2, axis=1)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [16], in <module>
----> 1 np.split(arr, 2, axis=1)

File <__array_function__ internals>:180, in split(*args, **kwargs)

File ~\miniconda3\envs\homl3\lib\site-packages\numpy\lib\shape_base.py:872, in split(ary, indices_or_sections, axis)
    870     N = ary.shape[axis]
    871     if N % sections:
--> 872         raise ValueError(
    873             'array split does not result in an equal division') from None
    874 return array_split(ary, indices_or_sections, axis)

ValueError: array split does not result in an equal division

np.vsplit()/np.hsplit() 함수

두 함수는 np.split() 함수에 축을 각각 0과 1로 지정한 함수이다.

  • np.vsplit(arr, z) := np.split(arr, z, axis=0)

np.vsplit(arr, [2, 3, 5])
[array([[ 0.8864, -2.0016, -0.3718,  1.669 , -0.4386],
        [-0.5397,  0.477 ,  3.2489, -1.0212, -0.5771]]),
 array([[0.1241, 0.3026, 0.5238, 0.0009, 1.3438]]),
 array([[-0.7135, -0.8312, -2.3702, -1.8608, -0.8608],
        [ 0.5601, -1.2659,  0.1198, -1.0635,  0.3329]]),
 array([[-2.3594, -0.1995, -1.542 , -0.9707, -1.307 ]])]
  • np.hsplit(arr, z) := np.split(arr, z, axis=1)

np.hsplit(arr, [1, 3])
[array([[ 0.8864],
        [-0.5397],
        [ 0.1241],
        [-0.7135],
        [ 0.5601],
        [-2.3594]]),
 array([[-2.0016, -0.3718],
        [ 0.477 ,  3.2489],
        [ 0.3026,  0.5238],
        [-0.8312, -2.3702],
        [-1.2659,  0.1198],
        [-0.1995, -1.542 ]]),
 array([[ 1.669 , -0.4386],
        [-1.0212, -0.5771],
        [ 0.0009,  1.3438],
        [-1.8608, -0.8608],
        [-1.0635,  0.3329],
        [-0.9707, -1.307 ]])]

15.3. 어레이 이어붙이기와 쌓기#

np.concatenate() 함수

두 개의 어레이를 이어붙인다. 지정되는 축에 따라 좌우로 또는 상하로 이어붙인다. 아래 세 어레이를 이용하여 사용법을 설명한다.

arr1 = np.array([[1, 2, 3], [4, 5, 6]])
arr1
array([[1, 2, 3],
       [4, 5, 6]])
arr2 = np.array([[7, 8, 9], [10, 11, 12]])
arr2
array([[ 7,  8,  9],
       [10, 11, 12]])
arr3 = np.array([[13, 14, 15], [16, 17, 18]])
arr3
array([[13, 14, 15],
       [16, 17, 18]])

위아래로 이어붙이려면 축을 0으로 정한다. 이어붙이 어레이로 이루어진 리스트 또는 튜플을 사용함에 주의한다.

np.concatenate([arr1, arr2, arr3], axis=0)
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12],
       [13, 14, 15],
       [16, 17, 18]])
np.concatenate((arr1, arr2, arr3), axis=0)
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12],
       [13, 14, 15],
       [16, 17, 18]])

좌우로 이어붙이려면 축을 1로 정한다.

np.concatenate([arr1, arr2, arr3], axis=1)
array([[ 1,  2,  3,  7,  8,  9, 13, 14, 15],
       [ 4,  5,  6, 10, 11, 12, 16, 17, 18]])
np.concatenate((arr1, arr2, arr3), axis=1)
array([[ 1,  2,  3,  7,  8,  9, 13, 14, 15],
       [ 4,  5,  6, 10, 11, 12, 16, 17, 18]])

np.vstack()/np.hstack() 함수

두 함수는 np.concatenate() 함수에 축을 각각 0과 1로 지정한 함수이다.

  • np.vstack((x, y, ...)) := np.concatenate((x, y, ...), axis=0)

np.vstack((arr1, arr2, arr3))
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12],
       [13, 14, 15],
       [16, 17, 18]])
  • np.hstack((x, y, ...)) := np.concatenate((x, y, ...) axis=1)

np.hstack((arr1, arr2, arr3))
array([[ 1,  2,  3,  7,  8,  9, 13, 14, 15],
       [ 4,  5,  6, 10, 11, 12, 16, 17, 18]])

np.r_[]/np.c_[] 객체

vstack()/hstack() 과 동일한 기능을 수행하는 특수한 객체들이다.

  • np.r_[x, y, ...] := np.vstack((x, y, ...))

np.r_[arr1, arr2, arr3]
array([[ 1,  2,  3],
       [ 4,  5,  6],
       [ 7,  8,  9],
       [10, 11, 12],
       [13, 14, 15],
       [16, 17, 18]])
  • np.c_[x, y, ...] := np.hstack((x, y, ...))

np.c_[arr1, arr2, arr3]
array([[ 1,  2,  3,  7,  8,  9, 13, 14, 15],
       [ 4,  5,  6, 10, 11, 12, 16, 17, 18]])

15.4. 브로드캐스팅#

브로드캐스팅broadcasting 모양이 서로 다른 두 어레이가 주어졌을 때 두 모양을 통일시킬 수 있다면 두 어레이의 연산이 가능하도록 도와주는 기능이다. 설명을 위해 하나의 어레이와 하나의 정수의 곱셈이 작동하는 과정을 살펴본다.

arr = np.arange(6).reshape((2,3))
arr
array([[0, 1, 2],
       [3, 4, 5]])

위 어레이에 4를 곱한 결과는 다음과 같다.

arr * 4
array([[ 0,  4,  8],
       [12, 16, 20]])

결과가 항목별로 곱해지는 이유는 arr * 4 가 아래 어레이의 곱셈과 동일하게 작동하기 때문이다. 즉, 정수 4로 채워진 동일한 모양의 어레이를 먼저 생성한 후에 항목별 곱셈을 진행한다.

이와 같이 어레이의 모양을 확장하여 항목별 연산이 가능해지도록 하는 기능은 두 어레이의 모야을 통일시킬 수 있는 경우 항상 작동한다.

15.4.1. 브로드캐스팅과 연산#

어레이 연산을 실행할 때 브로드캐스팅이 가능한 경우 자동 적용된다.

예제

아래 코드는 1차원 어레이를 2차원 어레이로 확장하여 다른 어레이와 모양을 맞춘 후 연산을 실행한 결과를 보여준다.

arr2 = np.arange(4).reshape((4,1)).repeat(3,axis=1)
arr2
array([[0, 0, 0],
       [1, 1, 1],
       [2, 2, 2],
       [3, 3, 3]])
arr3 = np.arange(1, 4)
arr3
array([1, 2, 3])
arr2 + arr3
array([[1, 2, 3],
       [2, 3, 4],
       [3, 4, 5],
       [4, 5, 6]])

아래 그림이 위 연산이 작동하는 이유를 설명한다.

동일한 이유로 다음 연산도 가능하다.

arr3_a = np.arange(1, 4)[np.newaxis, :]
arr3_a
array([[1, 2, 3]])
arr2 + arr3_a
array([[1, 2, 3],
       [2, 3, 4],
       [3, 4, 5],
       [4, 5, 6]])

예제

아래 예제는 2차원 어레이의 칸을 복제하여 모양을 맞춘 후 연산을 실행한다.

arr4 = np.arange(1, 5).reshape((4,1))
arr4
array([[1],
       [2],
       [3],
       [4]])
arr2 + arr4
array([[1, 1, 1],
       [3, 3, 3],
       [5, 5, 5],
       [7, 7, 7]])

그런데 브로드캐스팅이 가능하지 않으면 오류가 발생한다. 예를 아래 두 어레이의 덧셈은 불가능하다.

x = np.arange(0, 31, 10)
arr5 = np.c_[x, x, x]
arr5
array([[ 0,  0,  0],
       [10, 10, 10],
       [20, 20, 20],
       [30, 30, 30]])
arr4_a = arr4.flatten()
arr4_a
array([1, 2, 3, 4])
>>> arr5+ arr4_a
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [80], line 1
----> 1 arr5+ arr4_a

ValueError: operands could not be broadcast together with shapes (4,3) (4,)

아래 그림이 이유를 설명한다.

<그림 출처: NumPy: Broadcasting>

예제

아래 예제는 2차원 어레이를 3차원으로 확장한 후에 연산을 진행하는 것을 보여준다.

arr6 = np.arange(24).reshape((3, 4, 2))
arr6
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]]])
arr7 = np.arange(8).reshape((4, 2))
arr7
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7]])
arr6 + arr7
array([[[ 0,  2],
        [ 4,  6],
        [ 8, 10],
        [12, 14]],

       [[ 8, 10],
        [12, 14],
        [16, 18],
        [20, 22]],

       [[16, 18],
        [20, 22],
        [24, 26],
        [28, 30]]])

예제

아래 코드는 어레이의 열별 평균값이 0이 되도록 하려 한다.

arr = np.random.randn(4, 3)
arr
array([[ 0.2863,  0.378 , -0.7539],
       [ 0.3313,  1.3497,  0.0699],
       [ 0.2467, -0.0119,  1.0048],
       [ 1.3272, -0.9193, -1.5491]])

기존 어레이의 열별 평균값을 각각의 열에서 뺀다.

arr.mean(0) # arr.mean(axis=0)
array([ 0.5479,  0.1992, -0.3071])
demeaned = arr - arr.mean(0)
demeaned
array([[-0.2615,  0.1788, -0.4468],
       [-0.2166,  1.1506,  0.377 ],
       [-0.3012, -0.211 ,  1.3119],
       [ 0.7793, -1.1184, -1.242 ]])

이제 열별 평균값을 확인하면 0이 된다.

demeaned.mean(0)
array([-0.,  0.,  0.])

예제

아래 코드는 어레이의 행별 평균값이 0이 되도록 하려 한다.

arr
array([[ 0.2863,  0.378 , -0.7539],
       [ 0.3313,  1.3497,  0.0699],
       [ 0.2467, -0.0119,  1.0048],
       [ 1.3272, -0.9193, -1.5491]])
row_means = arr.mean(1)
row_means
array([-0.0299,  0.5836,  0.4132, -0.3804])
row_means.reshape((4, 1))
array([[-0.0299],
       [ 0.5836],
       [ 0.4132],
       [-0.3804]])
demeaned = arr - row_means.reshape((4, 1))
demeaned.mean(1)
array([ 0.,  0., -0.,  0.])

15.4.2. 브로드캐스팅과 항목 대체#

브로드캐스팅으로 어레이의 항목을 대체할 수 있다. 설명을 위해 아래 어레이를 사용한다.

arr = np.zeros((4, 3))
arr
array([[0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.]])

예제

모든 항목을 5로 대체한다.

arr[:] = 5
arr
array([[5., 5., 5.],
       [5., 5., 5.],
       [5., 5., 5.],
       [5., 5., 5.]])

예제

모든 열을 지정된 열로 대체한다.

col = np.array([1.28, -0.42, 0.44, 1.6])
col[:, np.newaxis]
array([[ 1.28],
       [-0.42],
       [ 0.44],
       [ 1.6 ]])
arr[:] = col[:, np.newaxis]
arr
array([[ 1.28,  1.28,  1.28],
       [-0.42, -0.42, -0.42],
       [ 0.44,  0.44,  0.44],
       [ 1.6 ,  1.6 ,  1.6 ]])

예제

0번, 1번 행을 특정 값으로 대체한다.

arr[:2] = [[-1.37], [0.509]]
arr
array([[-1.37 , -1.37 , -1.37 ],
       [ 0.509,  0.509,  0.509],
       [ 0.44 ,  0.44 ,  0.44 ],
       [ 1.6  ,  1.6  ,  1.6  ]])

15.5. 연습문제#

참고: (실습) 고급 넘파이