import numpy as np
rng = np.random.default_rng(seed=1701) # 재현성을 위한 시드 설정
x1 = rng.integers(10, size=6) # 1차원 배열
x2 = rng.integers(10, size=(3, 4)) # 2차원 배열
x3 = rng.integers(10, size=(3, 4, 5)) # 3차원 배열NumPy 배열의 기초
파이썬에서 데이터를 다루는 것은 곧 NumPy 배열을 다루는 것과 같습니다. Pandas(3부) 같은 최신 도구들도 모두 NumPy 배열을 기반으로 만들어졌습니다. 이번 섹션에서는 NumPy 배열을 사용해 데이터와 하위 배열에 접근하고, 배열을 나누고 재구성하며 합치는 다양한 작업 예시를 살펴보겠습니다. 여기서 다루는 내용이 조금 지루하고 기초적인 것처럼 보일 수 있지만, 책 전체에서 사용될 수많은 예제의 든든한 밑바탕이 됩니다. 그러니 이러한 기초 작업들을 확실히 익혀두시기 바랍니다.
여기서는 크게 다음 다섯 가지 범주의 배열 조작법을 다룹니다.
- 배열의 속성 확인: 배열의 크기, 모양, 메모리 소비량, 데이터 타입 결정
- 배열 인덱싱: 개별 배열 요소의 값을 가져오거나 설정
- 배열 슬라이싱: 큰 배열 안에서 작은 하위 배열을 가져오거나 설정
- 배열 재구성: 배열의 모양(shape) 변경
- 배열 합치기 및 나누기: 여러 배열을 하나로 합치거나 하나의 배열을 여러 개로 나누기
NumPy 배열 속성
먼저 배열의 유용한 속성들을 알아보겠습니다. 실습을 위해 1차원, 2차원, 3차원 난수 배열을 정의해 보죠. 이 코드를 실행할 때마다 항상 같은 배열이 생성되도록 NumPy의 난수 생성기에 특정 시드(seed) 값을 설정하겠습니다.
각 배열은 ndim(차원의 수), shape(각 차원의 크기), size(배열의 전체 요소 개수), dtype(각 요소의 데이터 타입) 속성을 가집니다.
print("x3 ndim: ", x3.ndim)
print("x3 shape:", x3.shape)
print("x3 size: ", x3.size)
print("dtype: ", x3.dtype)x3 ndim: 3
x3 shape: (3, 4, 5)
x3 size: 60
dtype: int64
데이터 타입에 대해 더 자세히 알고 싶다면 파이썬의 데이터 타입 이해하기 섹션을 참조하세요.
배열 인덱싱: 단일 요소에 접근하기
파이썬의 표준 리스트 인덱싱에 익숙하다면 NumPy의 인덱싱도 매우 친숙하게 느껴질 것입니다. 1차원 배열에서 \(i\)번째 값(0부터 시작)은 리스트와 마찬가지로 대괄호 안에 인덱스를 넣어 접근합니다.
x1array([9, 4, 0, 3, 8, 6])
x1[0]9
x1[4]8
배열의 끝에서부터 거꾸로 접근하려면 음수 인덱스를 사용하면 됩니다.
x1[-1]6
x1[-2]8
다차원 배열에서는 쉼표로 구분된 (행, 열) 튜플을 사용하여 각 항목에 접근합니다.
x2array([[3, 1, 3, 7],
[4, 0, 2, 3],
[0, 0, 6, 9]])
x2[0, 0]3
x2[2, 0]0
x2[2, -1]9
같은 인덱스 표기법을 사용하여 값을 수정할 수도 있습니다.
x2[0, 0] = 12
x2array([[12, 1, 3, 7],
[ 4, 0, 2, 3],
[ 0, 0, 6, 9]])
파이썬 리스트와 달리 NumPy 배열은 고정된 데이터 타입을 가집니다. 예를 들어 정수형 배열에 실수값을 넣으려고 하면, 소수점 이하가 자동으로 잘려 나갑니다. 이러한 동작을 미리 알고 주의하는 것이 좋습니다.
x1[0] = 3.14159 # 이 값은 정수로 잘려서 저장됩니다!
x1array([3, 4, 0, 3, 8, 6])
배열 슬라이싱: 하위 배열에 접근하기
대괄호를 사용해 개별 요소에 접근하듯, 콜론(:) 기호를 사용한 슬라이스(slice) 표기법으로 하위 배열에 접근합니다. NumPy 슬라이싱 구문은 표준 파이썬 리스트와 동일합니다. 배열 x의 일부를 가져오려면 다음 구문을 사용합니다.
`python x[start:stop:step] ```` 인자를 생략하면start=0,stop=<배열 크기>,step=1` 값이 적용됩니다. 1차원과 다차원 배열에서 하위 배열에 접근하는 몇 가지 예시를 살펴보겠습니다.
1차원 하위 배열
다음은 1차원 배열의 요소에 접근하는 여러 방법입니다.
x1array([3, 4, 0, 3, 8, 6])
x1[:3] # 처음 세 요소array([3, 4, 0])
x1[3:] # 인덱스 3 이후의 요소들array([3, 8, 6])
x1[1:4] # 중간 부분의 하위 배열array([4, 0, 3])
x1[::2] # 한 요소씩 건너뛰며 가져오기array([3, 0, 8])
x1[1::2] # 인덱스 1부터 시작해 한 요소씩 건너뛰기array([4, 3, 6])
조금 헷갈릴 수 있는 부분은 step 값이 음수인 경우입니다. 이때는 start와 stop의 기본값이 서로 뒤바뀝니다. 이 방식은 배열을 뒤집는 아주 편리한 방법이 되기도 합니다.
x1[::-1] # 모든 요소의 순서를 뒤집기array([6, 8, 3, 0, 4, 3])
x1[4::-2] # 인덱스 4부터 시작해 역순으로 한 요소씩 건너뛰기array([8, 0, 3])
다차원 하위 배열
다차원 슬라이싱도 같은 방식으로 작동하며, 각 차원을 쉼표로 구분해 줍니다. 예를 들어 보죠.
x2array([[12, 1, 3, 7],
[ 4, 0, 2, 3],
[ 0, 0, 6, 9]])
x2[:2, :3] # 처음 두 행과 처음 세 열array([[12, 1, 3],
[ 4, 0, 2]])
x2[:3, ::2] # 모든 행에서 한 열씩 건너뛰기array([[12, 3],
[ 4, 2],
[ 0, 6]])
x2[::-1, ::-1] # 모든 행과 열의 순서를 완전히 뒤집기array([[ 9, 6, 0, 0],
[ 3, 2, 0, 4],
[ 7, 3, 1, 12]])
배열의 행과 열에 접근하기
일반적으로 배열의 특정 행이나 열 하나를 통째로 가져와야 할 때가 많습니다. 이럴 때는 인덱싱과 슬라이싱을 조합해, 콜론(:)만 표시한 빈 슬라이스를 사용하면 됩니다.
x2[:, 0] # x2의 첫 번째 열array([12, 4, 0])
x2[0, :] # x2의 첫 번째 행array([12, 1, 3, 7])
행에 접근할 때는 더 간결하게 빈 슬라이스를 생략할 수도 있습니다.
x2[0] # x2[0, :]와 동일한 결과array([12, 1, 3, 7])
사본이 아닌 뷰(View)로서의 하위 배열
파이썬 리스트와 결정적으로 다른 점 하나는, NumPy 배열을 슬라이싱하면 데이터의 사본(copy)이 아니라 뷰(view)를 반환한다는 점입니다. 앞서 보았던 2차원 배열을 예로 들어보죠.
print(x2)[[12 1 3 7]
[ 4 0 2 3]
[ 0 0 6 9]]
여기서 \(2 \times 2\) 크기의 하위 배열을 추출해 보겠습니다.
x2_sub = x2[:2, :2]
print(x2_sub)[[12 1]
[ 4 0]]
이제 이 하위 배열의 값을 수정하면, 원래 배열의 값도 함께 변하는 것을 확인합니다!
x2_sub[0, 0] = 99
print(x2_sub)[[99 1]
[ 4 0]]
print(x2)[[99 1 3 7]
[ 4 0 2 3]
[ 0 0 6 9]]
이러한 동작이 처음에는 의외일 수 있지만, 상당히 큰 장점이기도 합니다. 대용량 데이터 세트를 다룰 때 데이터를 일일이 복사할 필요 없이 특정 부분만 효율적으로 접근해 처리할 수 있기 때문입니다.
배열의 사본 만들기
물론 뷰의 장점과는 별개로, 배열이나 하위 배열의 데이터를 명시적으로 복사해야 할 때도 있습니다. 이럴 때는 copy() 메서드를 사용하면 됩니다.
x2_sub_copy = x2[:2, :2].copy()
print(x2_sub_copy)[[99 1]
[ 4 0]]
이제 이 사본 배열을 수정해도 원본 배열에는 아무런 영향을 주지 않습니다.
x2_sub_copy[0, 0] = 42
print(x2_sub_copy)[[42 1]
[ 4 0]]
print(x2)[[99 1 3 7]
[ 4 0 2 3]
[ 0 0 6 9]]
배열 재구성하기
배열의 모양을 바꾸는 또 다른 중요한 기능은 reshape() 메서드입니다. 예를 들어 1부터 9까지의 숫자를 \(3 \times 3\) 격자 모양으로 배치하고 싶다면 다음과 같이 하면 됩니다.
grid = np.arange(1, 10).reshape(3, 3)
print(grid)[[1 2 3]
[4 5 6]
[7 8 9]]
이 작업이 가능하려면 원래 배열의 크기와 바꿀 배열의 크기가 일치해야 합니다. 대부분의 경우 reshape() 메서드는 원본 배열의 뷰(view)를 반환합니다.
자주 쓰이는 재구성 작업 중 하나는 1차원 배열을 2차원 행 또는 열 벡터로 변환하는 것입니다.
x = np.array([1, 2, 3])
x.reshape((1, 3)) # reshape을 이용한 행 벡터 변환array([[1, 2, 3]])
x.reshape((3, 1)) # reshape을 이용한 열 벡터 변환array([[1],
[2],
[3]])
이를 더 간편하게 표현하는 방법은 슬라이싱 구문에 np.newaxis를 사용하는 것입니다.
x[np.newaxis, :] # newaxis를 이용한 행 벡터 변환array([[1, 2, 3]])
x[:, np.newaxis] # newaxis를 이용한 열 벡터 변환array([[1],
[2],
[3]])
이는 앞으로 이 책에서 매우 자주 보게 될 유용한 패턴입니다.
배열 합치기 및 나누기
지금까지는 단일 배열 안에서 일어나는 작업들을 알아보았습니다. NumPy는 여러 배열을 하나로 합치거나, 반대로 하나의 배열을 여러 개로 나누는 도구도 제공합니다.
배열 합치기
NumPy에서 배열을 합칠 때는 주로 np.concatenate, np.vstack, np.hstack 함수를 사용합니다. np.concatenate는 아래 예시처럼 배열들이 담긴 튜플이나 리스트를 첫 번째 인자로 받습니다.
x = np.array([1, 2, 3])
y = np.array([3, 2, 1])
np.concatenate([x, y])array([1, 2, 3, 3, 2, 1])
한 번에 세 개 이상의 배열을 합치는 것도 가능합니다.
z = np.array([99, 99, 99])
print(np.concatenate([x, y, z]))[ 1 2 3 3 2 1 99 99 99]
이 방식은 2차원 배열에도 동일하게 적용됩니다.
grid = np.array([[1, 2, 3], [4, 5, 6]])# 첫 번째 축(행 방향)을 따라 합치기
np.concatenate([grid, grid])array([[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[4, 5, 6]])
# 두 번째 축(열 방향)을 따라 합치기
np.concatenate([grid, grid], axis=1)array([[1, 2, 3, 1, 2, 3],
[4, 5, 6, 4, 5, 6]])
서로 차원이 다른 배열을 합칠 때는 np.vstack(수직 스택)이나 np.hstack(수평 스택) 함수를 쓰는 것이 훨씬 직관적입니다.
# 배열을 세로로 쌓기
np.vstack([x, grid])array([[1, 2, 3],
[1, 2, 3],
[4, 5, 6]])
# 배열을 가로로 쌓기
y = np.array([[99], [99]])
np.hstack([grid, y])array([[ 1, 2, 3, 99],
[ 4, 5, 6, 99]])
참고로 3차원 이상의 배열에서는 np.dstack을 사용하여 세 번째 축(깊이 방향)을 따라 합칠 수 있습니다.
배열 나누기
합치기의 반대는 나누기이며, np.split, np.hsplit, np.vsplit 함수로 수행합니다. 각 함수에 나눌 지점을 인덱스 리스트 형태로 전달합니다.
x = [1, 2, 3, 99, 99, 3, 2, 1]
x1, x2, x3 = np.split(x, [3, 5])
print(x1, x2, x3)[1 2 3] [99 99] [3 2 1]
\(N\)개의 분할 지점은 \(N+1\)개의 하위 배열을 만들어냅니다. np.hsplit과 np.vsplit도 비슷한 방식으로 작동합니다.
grid = np.arange(16).reshape((4, 4))
gridarray([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
upper, lower = np.vsplit(grid, [2])
print(upper)
print(lower)[[0 1 2 3]
[4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
left, right = np.hsplit(grid, [2])
print(left)
print(right)[[ 0 1]
[ 4 5]
[ 8 9]
[12 13]]
[[ 2 3]
[ 6 7]
[10 11]
[14 15]]
마찬가지로 np.dsplit을 사용하면 세 번째 축을 따라 배열을 나눌 수 있습니다.