[NumPy] NumPy 개요

NumPy 소개

NumPy는 Numerical Python 의 약자로 Python 에서 대규모 다차원 배열을 다룰 수 있게 도와주는 고성능의 수치 계산을 위해 만들어진 라이브러리이다.

음압을 나타내는 1차원 배열, 흑백 이미지를 나타내는 2차원 배열, RGB 데이터를 담은 3차원 배열 등 데이터 대부분은 숫자 배열로 볼 수 있다. 이들은 물론 Python List 자료형으로도 나타낼 수 있지만, NumPy 는 파이썬 리스트보다 빠른 연산과 효율적 메모리 활용하도록 지원한다.

배열 만들기

파이썬에서 제공하는 list 자료형을 이용해서 Numpy Array 를 만들 수 있다.

import numpy as np

arr = np.array([1, 2, 3, 4, 5])
print(arr)

# [1 2 3 4 5]

이렇게 만들어진 배열의 타입을 확인해보면 아래와 같이 나타난다.

import numpy as np

arr = np.array([1, 2, 3, 4, 5])
print(type(arr))

# <class 'numpy.ndarray'>

생성된 ndarray 클래스 소스코드는 numpy/numpy/core/src/multiarray/arrayobject.c 에서 확인할 수 있다. 구현된 소스코드를 보다보면 API 들이 심심치 않게 등장하는 것을 볼 수 있다. NumPy c-api 에서 배열과 관련된 설명과 소스코드들을 볼 수 있다.

나중에 NumPy 를 이용해 데이터를 표현할 때, transpose, reshape, view 와 같은 메소드들이 언제 어떻게 쓰이는지 헷갈릴 때가 있다. 이럴 때, 여러 설명을 보기보다 직접 소스코드를 뜯어볼 때 오히려 이해가 빠른 경우가 있다. 물론 다른 사람이 설명한 글로 이해가 되면 BEST.

NumPy Array 를 만들 때, 타입과 모양을 결정해 만들 수도 있다.

import numpy as np

arr1 = np.array([1, 2, 3, 4, 5, 6])
print("arr1: ", arr1)

arr2 = np.array([3, 1.4, 4.2, 2, 5])
print("arr2: ",arr2)

arr3 = np.array([[1, 2, 3],
                 [4, 5, 6]])
print("arr3: ",arr3)

arr4 = np.array([1, 2, 3, 4], dtype='float')
print("arr4: ",arr4)

# arr1:  [1 2 3 4 5 6]
# arr2:  [3.  1.4 4.2 2.  5. ]
# arr3:  [[1 2 3]
#  [4 5 6]]
# arr4:  [1. 2. 3. 4.]

한 가지 알고가면 좋을 것은 Numerical 이지만 문자열 타입이 들어갈 수 있다는 것이다. 배열에 하나라도 문자열이나 문자 타입이 있다면 자동으로 모든 값이 해당 타입으로 변환된다.

# 데이터 연산 시 dtype 이 자동 변환되어 에러가 나는 경우도 있다.

import numpy as np

arr1 = np.array(['a', 'b', 'c', 'd', 'e'])
print("arr1.dtype:", arr1.dtype)

arr2 = np.array(['a', 2])
print("arr2.dtype:", arr2.dtype)

arr3 = np.array(['string', 'a'])
print("arr3.dtype:", arr3.dtype)

# arr1.dtype: <U1
# arr2.dtype: <U11
# arr3.dtype: <U6

다양한 배열 만들기

파이썬 리스트를 삽입하는 것 이외에 만드는 배열을 만드는 방법이 있다. 자주 사용되는 아래 메소드들을 살펴보자.

import numpy as np

print("np.zeros(10, dtype=int):\n", np.zeros(10, dtype=int))
print()

print("np.ones((3, 5), dtype=float):\n", np.ones((3, 5), dtype=float))
print()

print("np.eye(5):\n", np.eye(5))
print()

print("np.arange(0, 20, 2):\n", np.arange(0, 20, 2))
print()

print("np.linspace(0, 1, 5):\n", np.linspace(0, 1, 5))
print()

# np.zeros(10, dtype=int):
#  [0 0 0 0 0 0 0 0 0 0]

# np.ones((3, 5), dtype=float):
#  [[1. 1. 1. 1. 1.]
#  [1. 1. 1. 1. 1.]
#  [1. 1. 1. 1. 1.]]

# np.eye(5):
#  [[1. 0. 0. 0. 0.]
#  [0. 1. 0. 0. 0.]
#  [0. 0. 1. 0. 0.]
#  [0. 0. 0. 1. 0.]
#  [0. 0. 0. 0. 1.]]

# np.arange(0, 20, 2):
#  [ 0  2  4  6  8 10 12 14 16 18]

# np.linspace(0, 1, 5):
#  [0.   0.25 0.5  0.75 1.  ]

난수로 채워진 배열 만들기

직접 숫자를 입력하는 것은 번거롭다. 난수로 채워진 배열을 만들 때에는 아래와 같이 numpy 에서 지원해주는 method 를 사용하자.

import numpy as np

print("0~1 실수를 원소로 하는 shape이 (2, 2)인 배열: \n", np.random.random((2, 2)))
print()

print("평균이 0이고 표준편차가 1인 정규분포를 따르는 shape이 (2, 2)인 배열:\n", np.random.normal(0, 1, (2, 2)))
print()

print("원소가 [0, 10) 정수인 shape이 (2, 2)인 배열\n", np.random.randint(0, 10, (2, 2)))
print()

# 0~1 실수를 원소로 하는 shape이 (2, 2)인 배열: 
#  [[0.60523059 0.9114613 ]
#  [0.43288374 0.05701229]]

# 평균이 0이고 표준편차가 1인 정규분포를 따르는 shape이 (2, 2)인 배열:
#  [[-0.63903383  0.88088198]
#  [ 1.05652918  1.16487656]]

# 원소가 [0, 10) 정수인 shape이 (2, 2)인 배열
#  [[5 8]
#  [1 3]]

배열 클래스와 STRIDES

NumPy 배열은 단순한 메모리의 연속이 아니라 하나의 클래스이다. 그렇기 때문에 Attribute 들이 있는데, 각각은 처음 봐도 직관적으로 이해할 수 있다.

import numpy as np

x = np.random.randint(10, size=(2,4,3))
print(x)
print("=========================")

print("x.ndim:", x.ndim)
print("x.shape:", x.shape)
print("x.size:", x.size)
print("x.dtype:", x.dtype)
print("x.strides:", x.strides)

# [[[4 1 5]
#   [1 4 2]
#   [4 7 1]
#   [2 7 2]]

#  [[0 5 9]
#   [8 2 2]
#   [6 4 0]
#   [0 5 1]]]
# =========================
# x.ndim: 3
# x.shape: (2, 4, 3)
# x.size: 24
# x.dtype: int32
# x.strides: (48, 12, 4)

그치만 strides 는 조금 헷갈릴 수 있다. 자세히 알아보기 위해 아래 예제먼저 확인해보자. x 배열은 Integer 를 32bit (4byte) 으로, y 배열은 Integer 를 64bit (8byte) 으로 나타낸다.

import numpy as np

x = np.random.randint(10, size=(2, 4, 3), dtype='int32')
y = np.random.randint(10, size=(2, 4, 3), dtype='int64')

print("x strides: ", x.strides)
print("y strides: ", y.strides)

# x strides:  (48, 12, 4)
# y strides:  (96, 24, 8)

결론부터 말하면, STRIDE는 각 axis 방향으로 다음 원소로 접근하기 위해 넘어가야하는 Byte 수를 의미한다. NumPy 배열도 힘들게 이해하고 있는데, Axis 는 뭐지? 라고 생각할 수 있다. STRIDE 를 알아보기 전에 간단하게 Axis 에 대한 느낌만 살짝 가져가보자.

Axis는 복잡하고, 처음 접할 때에는 어렵게 느껴질 수 있다. 지금은 가장 앞쪽부터 0, 1, 2, ... 순서로 이름이 붙는다는 것만 알자. 제대로 이해했다면 Shape 이 (2, 4, 3) 인 배열의 axis 1 방향으로는 4개 칸이 있다 라는 말에 대충 고개를 끄덕거릴 수 있을 것이다.

이제 Array 를 읽는 방법인데, axis 0 부터 가장 바깥 쪽의 원소 개수를 뜻하는 것을 알면 임의 배열이 주어져도 Shape 을 추정할 수 있을 것이다. 자, 아래와 같은 배열이 있다.

[                   # axis 0
    [               # axis 1
        [4 1 0]     # axis 2
        [2 5 5]
    ]
    
    [
        [6 3 8]
        [8 9 6]
    ]
]

설명을 위해 색을 표시하면 아래와 같다.

먼저 노란색을 보자. 노란색 안으로 보라색 [ ] 이 몇 개가 있을까? 2개가 있다. 그럼 axis 0 은 2개 칸을 가진다고 생각하면 된다. 일단 주어진 배열의 모양은 (2, ?, ?, ...) 라고 생각할 수 있다.

그 다음 보라색 axis 1 과 대응되는 부분을 보자. 안으로 파란색 [ ] 칸이 몇 개가 있을까? 역시 2개가 있다. 그러면 (2, 2, ?, ...) 이라고 할 수 있다.

파란색 안으로 들어가보자. [ ] 은 아니지만 파란색 괄호 안에는 3개 칸이 있다. 그리고 끝이 나기 때문에 (2, 2, 3) 으로 나타낼 수 있겠다. 실제로 이 배열은 np.random.randint(10, size=(2, 2, 3), dtype='int32') 로 만든 배열이다.

아래 테스트로 만들어 본 배열의 Shape 을 맞춰보자. 정답은 (4, 1, 3) 이다.

[[[2 3 0]]

 [[9 7 3]]

 [[3 0 7]]

 [[0 4 5]]]

이제 axis 개념까지 이해했다면, STRIDE 정의를 다시 한 번 생각해보자. STRIDE 는 각 axis 방향으로 다음 원소로 접근하기 위해 넘어가야하는 Byte 수 라고 했다.

x = np.arange(24).reshape(3,2,4)
print(x)

# [[[ 0  1  2  3]
#   [ 4  5  6  7]]

#  [[ 8  9 10 11]
#   [12 13 14 15]]

#  [[16 17 18 19]
#   [20 21 22 23]]]

print(x.strides)
# (32, 16, 4)

위 배열에서 0 에서 axis 0 방향으로 다음 원소는 무엇일까? 8이다. 8까지는 8개 원소를 지나야하고, 4byte * 8 = 32 이다. 그럼 axis 1 방향으로 다음 원소는? 4이다. 똑같이 4개 원소를 지나야하니 4byte * 4 = 16 이 된다. axis 2 방향으로는? 간단하다. 4byte 이다.

간단하게 STRIDE 가 어떤 개념인지 확인했다. 배운 내용을 활용해 더 재밌게 이해해볼 수 있는 얘기가 있는데, 관심있다면 Difference between reshape and transpose operators 에서 확인할 수 있다.

Indexing / Slicing

Indexing 이란 인덱스 값으로 접근해 값을 찾아내는 것이다. 리스트나 배열과 마찬가지로, NumPy 배열도 Indexing 접근을 허용한다. 또한 Python 리스트의 편리한 기능 중 하나인 Slicing 까지 지원한다.

Concatenate / Split

또한, 이어 붙이거나 나누는 것도 가능하다. 이 때, 앞에서 배운 axis 개념이 활용된다. 방향을 지정해 작업을 수행할 수 있다.

import numpy as np

matrix = np.arange(4).reshape(2, 2)
print(matrix)

print("============================")
concatenated0 = np.concatenate([matrix, matrix], axis=0)
print("concatenated to axis-0\n", concatenated0)

print("============================")
concatenated1 = np.concatenate([matrix, matrix], axis=1)
print("concatenated to axis-1\n", concatenated1)

# [[0 1]
#  [2 3]]
# ============================
# concatenated to axis-0
#  [[0 1]
#  [2 3]
#  [0 1]
#  [2 3]]
# ============================
# concatenated to axis-1
#  [[0 1 0 1]
#  [2 3 2 3]]

집계 함수

NumPy 는 주어진 배열에 대해 데이터를 요약하고 통계낼 수 있는 간단한 함수들을 제공한다. 이러한 집계 함수는 axis 를 지정해 수행할 수도 있다.

import numpy as np

x = np.random.normal(0, 1, size=(3, 3))
print("array x\n",x)
print("======================")
print("np.sum(x)\n", np.sum(x))
print("======================")
print("np.sum(x, axis=1)\n", np.sum(x, axis=1))
print("======================")
print("np.min(x)\n", np.min(x))
print("======================")
print("np.max(x)\n", np.max(x))
print("======================")
print("np.mean(x)\n", np.mean(x))

# array x
#  [[-0.02581028  0.77451849 -0.11143006]
#  [-1.90484666 -0.51627231  2.04499199]
#  [ 1.82849608 -1.82324903 -0.58452629]]
# ======================
# np.sum(x)
#  -0.31812807805108556
# ======================
# np.sum(x, axis=1)
#  [ 0.63727814 -0.37612698 -0.57927924]
# ======================
# np.min(x)
#  -1.9048466600469687
# ======================
# np.max(x)
#  2.044991990437637
# ======================
# np.mean(x)
#  -0.03534756422789839

마스킹 연산

마스킹 연산은 True, False 배열을 통해 조건을 만족하는 값들을 뽑아내는 방법이다. 먼저 True, False 배열을 만드는 방법부터 알아보자. 간단하게 판단 조건을 명시하면 자동으로 대상과 같은 Shape 을 가지는 것을 배열을 만들어 반환한다.

import numpy as np

x = np.random.randint(0, 10, size=(2, 5))
t_or_f_arr = x > 3
print(t_or_f_arr)

# [[ True  True  True  True  True]
#  [ True  True  True False False]]

끝으로 해당 배열을 다시 Index 로 넣어주면, True 인 값들만 추출되어 나타나는 것을 확인할 수 있다.

import numpy as np

x = np.random.randint(0, 10, size=(2, 5))
t_or_f_arr = x > 3
print(x[t_or_f_arr])

# [4 4 4 7 7]

정리

NumPy 는 데이터 사이언스 업무를 수행할 때, 필연적으로 만나게 될 라이브러리이다. 본인만의 정리노트를 작성해두었다가, 필요할 때마다 찾아보고 내용을 추가해보자.

References

  • Elice 코딩 교육 - 데이터 사이언스 기초

Updated:

Leave a comment