3.1 넘파이 배열

많은 숫자 데이터를 하나의 변수에 넣고 관리 할 때 리스트는 속도가 느리고 메모리를 많이 차지하는 단점이 있다. 배열(array)을 사용하면 적은 메모리로 많은 데이터를 빠르게 처리할 수 있다. 배열은 리스트와 비슷하지만 다음과 같은 점에서 다르다.

  1. 모든 원소가 같은 자료형이어야 한다.

  2. 원소의 갯수를 바꿀 수 없다.

파이썬은 자체적으로 배열 자료형을 제공하지 않는다. 따라서 배열을 구현한 다른 패키지를 임포트해야 한다. 파이썬에서 배열을 사용하기 위한 표준 패키지는 넘파이(NumPy)다.

넘파이는 수치해석용 파이썬 패키지이다. 다차원의 배열 자료구조 클래스인 ndarray 클래스를 지원하며 벡터와 행렬을 사용하는 선형대수 계산에 주로 사용된다. 내부적으로는 BLAS 라이브러리와 LAPACK 라이브러리를 사용하고 있으며 C로 구현된 CPython에서만 사용할 수 있다.

넘파이의 배열 연산은 C로 구현된 내부 반복문을 사용하기 때문에 파이썬 반복문에 비해 속도가 빠르며 벡터화 연산(vectorized operation)을 이용하여 간단한 코드로도 복잡한 선형 대수 연산을 수행할 수 있다. 또한 배열 인덱싱(array indexing)을 사용한 질의(Query) 기능을 이용하여 간단한 코드로도 복잡한 수식을 계산할 수 있다.

넘파이 패키지 임포트

배열을 사용하기 위해서는 우선 다음과 같이 넘파이 패키지를 임포트한다. 넘파이는 np라는 이름으로 임포트하는 것이 관례이다.

import numpy as np

1차원 배열 만들기

넘파이의 array 함수에 리스트를 넣으면 ndarray 클래스 객체 즉, 배열로 변환해 준다. 따라서 1 차원 배열을 만드는 방법은 다음과 같다.

ar = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
ar
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

리스트와 비슷해 보이지만 type 명령으로 자료형을 살펴보면 ndarray임을 알 수 있다.

type(ar)
numpy.ndarray

만들어진 ndarray 객체의 표현식(representation)을 보면 바깥쪽에 array()란 것이 붙어 있을 뿐 리스트와 동일한 구조처럼 보인다. 그러나 배열 객체와 리스트 객체는 많은 차이가 있다.

우선 리스트 클래스 객체는 각각의 원소가 다른 자료형이 될 수 있다. 그러나 배열 객체 객체는 C언어의 배열처럼 연속적인 메모리 배치를 가지기 때문에 모든 원소가 같은 자료형이어야 한다. 이러한 제약사항이 있는 대신 원소에 대한 접근과 반복문 실행이 빨라진다.

벡터화 연산

배열 객체는 배열의 각 원소에 대한 반복 연산을 하나의 명령어로 처리하는 벡터화 연산(vectorized operation)을 지원한다. 예를 들어 다음처럼 여러개의 데이터를 모두 2배 해야 하는 경우를 생각하자.

data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

for 반복문을 사용하면 다음과 같이 구현할 수 있다.

answer = []
for di in data:
    answer.append(2 * di)
answer
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

하지만 벡터화 연산을 사용하면 다음과 같이 for 반복문이 없이 한번의 연산으로 할 수 있다. 계산 속도도 반복문을 사용할 때 보다 훨씬 빠르다.

x = np.array(data)
x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
2 * x
array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])

참고로 일반적인 리스트 객체에 정수를 곱하면 객체의 크기가 정수배 만큼으로 증가한다.

L = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(2 * L)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

벡터화 연산은 비교 연산과 논리 연산을 포함한 모든 종류의 수학 연산에 대해 적용된다. 선형 대수에 적용되는 벡터화 연산에 대해서는 나중에 보다 자세히 설명한다.

a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
2 * a + b
array([12, 24, 36])
a == 2
array([False,  True, False])
b > 10
array([False,  True,  True])
(a == 2) & (b > 10)
array([False,  True, False])

2차원 배열 만들기

ndarray 는 N-dimensional Array의 약자이다. 이름 그대로 1차원 배열 이외에도 2차원 배열, 3차원 배열 등의 다차원 배열 자료 구조를 지원한다. 2차원 배열은 행렬(matrix)이라고 하는데 행렬에서는 가로줄을 행(row)이라고 하고 세로줄을 열(column)이라고 부른다.

다음과 같이 리스트의 리스트(list of list)를 이용하면 2차원 배열을 생성할 수 있다. 안쪽 리스트의 길이는 행렬의 열의 수 즉, 가로 크기가 되고 바깥쪽 리스트의 길이는 행렬의 행의 수, 즉 세로 크기가 된다. 예를 들어 2개의 행과 3개의 열을 가지는 2 x 3 배열은 다음과 같이 만든다.

c = np.array([[0, 1, 2], [3, 4, 5]])  # 2 x 3 array
c
array([[0, 1, 2],
       [3, 4, 5]])

2차원 배열의 행과 열의 갯수는 다음처럼 구한다.

# 행의 갯수
len(c)
2
# 열의 갯수
len(c[0])
3

연습 문제 1

넘파이를 사용하여 다음과 같은 행렬을 만든다.

10 20 30 40
50 60 70 80

3차원 배열 만들기

리스트의 리스트의 리스트를 이용하면 3차원 배열도 생성할 수 있다. 크기를 나타낼 때는 가장 바깥쪽 리스트의 길이부터 가장 안쪽 리스트 길이의 순서로 표시한다. 예를 들어 2 x 3 x 4 배열은 다음과 같이 만든다.

d = np.array([[[1, 2, 3, 4],
               [5, 6, 7, 8],
               [9, 10, 11, 12]],
              [[11, 12, 13, 14],
               [15, 16, 17, 18],
               [19, 20, 21, 22]]])   # 2 x 3 x 4 array

3차원 배열의 깊이, 행, 열은 다음과 같이 구할 수 있다.

len(d), len(d[0]), len(d[0][0])
(2, 3, 4)

배열의 차원과 크기 알아내기

배열의 차원 및 크기를 구하는 더 간단한 방법은 배열의 ndim 속성과 shape 속성을 이용하는 것이다. ndim 속성은 배열의 차원, shape 속성은 배열의 크기를 반환한다.

# a = np.array([1, 2, 3])
print(a.ndim)
print(a.shape)
1
(3,)
# c = np.array([[0, 1, 2], [3, 4, 5]])
print(c.ndim)
print(c.shape)
2
(2, 3)
print(d.ndim)
print(d.shape)
3
(2, 3, 4)

배열의 인덱싱

일차원 배열의 인덱싱은 리스트의 인덱싱과 같다.

a = np.array([0, 1, 2, 3, 4])
a[2]
2
a[-1]
4

다차원 배열일 때는 다음과 같이 콤마(comma ,)를 사용하여 접근할 수 있다. 콤마로 구분된 차원을 축(axis)이라고도 한다. 그래프의 x축과 y축을 떠올리면 될 것이다.

a = np.array([[0, 1, 2], [3, 4, 5]])
a
array([[0, 1, 2],
       [3, 4, 5]])
a[0, 0]  # 첫번째 행의 첫번째 열
0
a[0, 1]  # 첫번째 행의 두번째 열
1
a[-1, -1]  # 마지막 행의 마지막 열
5

배열 슬라이싱

배열 객체로 구현한 다차원 배열의 원소 중 복수 개를 접근하려면 일반적인 파이썬 슬라이싱(slicing)과 comma(,)를 함께 사용하면 된다.

a = np.array([[0, 1, 2, 3], [4, 5, 6, 7]])
a
array([[0, 1, 2, 3],
       [4, 5, 6, 7]])
a[0, :]  # 첫번째 행 전체
array([0, 1, 2, 3])
a[:, 1]  # 두번째 열 전체
array([1, 5])
a[1, 1:]  # 두번째 행의 두번째 열부터 끝열까지
array([5, 6, 7])
a[:2, :2]
array([[0, 1],
       [4, 5]])

연습 문제 2

다음 행렬과 같은 행렬이 있다.

m = np.array([[ 0,  1,  2,  3,  4],
            [ 5,  6,  7,  8,  9],
            [10, 11, 12, 13, 14]])
  1. 이 행렬에서 값 7 을 인덱싱한다.

  2. 이 행렬에서 값 14 을 인덱싱한다.

  3. 이 행렬에서 배열 [6, 7] 을 슬라이싱한다.

  4. 이 행렬에서 배열 [7, 12] 을 슬라이싱한다.

  5. 이 행렬에서 배열 [[3, 4], [8, 9]] 을 슬라이싱한다.

배열 인덱싱

넘파이 배열 객체의 또다른 강력한 기능은 팬시 인덱싱(fancy indexing)이라고도 부르는 배열 인덱싱(array indexing) 방법이다. 인덱싱이라는 이름이 붙었지만 사실은 데이터베이스의 질의(Query) 기능을 수행한다. 배열 인덱싱에서는 대괄호(Bracket, [])안의 인덱스 정보로 숫자나 슬라이스가 아니라 위치 정보를 나타내는 또 다른 ndarray 배열을 받을 수 있다. 여기에서는 이 배열을 편의상 인덱스 배열이라고 부르겠다. 배열 인덱싱의 방식에은 불리언(Boolean) 배열 방식과 정수 배열 방식 두가지가 있다.

먼저 불리안 배열 인덱싱 방식은 인덱스 배열의 원소가 True, False 두 값으로만 구성되며 인덱스 배열의 크기가 원래 ndarray 객체의 크기와 같아야 한다.

예를 들어 다음과 같은 1차원 ndarray에서 짝수인 원소만 골라내려면 짝수인 원소에 대응하는 인덱스 값이 True이고 홀수인 원소에 대응하는 인덱스 값이 False인 인덱스 배열을 넣으면 된다.

a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
idx = np.array([True, False, True, False, True,
               False, True, False, True, False])
a[idx]
array([0, 2, 4, 6, 8])

조건문 연산을 사용하면 다음과 같이 간단히 쓸 수 있다.

a % 2
array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1], dtype=int32)
a % 2 == 0
array([ True, False,  True, False,  True, False,  True, False,  True,
       False])
a[a % 2 == 0]
array([0, 2, 4, 6, 8])

정수 배열 인덱싱에서는 인덱스 배열의 원소 각각이 원래 ndarray 객체 원소 하나를 가리키는 인덱스 정수이여야 한다. 예를 들어 1차원 배열에서 홀수번째 원소만 골라내는 것은 다음과 같다

a = np.array([11, 22, 33, 44, 55, 66, 77, 88, 99])
idx = np.array([0, 2, 4, 6, 8])
a[idx]
array([11, 33, 55, 77, 99])

이 때는 배열 인덱스의 크기가 원래의 배열 크기와 달라도 상관없다. 같은 원소를 반복해서 가리키는 경우에는 배열 인덱스가 원래의 배열보다 더 커지기도 한다.

a = np.array([11, 22, 33, 44, 55, 66, 77, 88, 99])
idx = np.array([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2])
a[idx]
array([11, 11, 11, 11, 11, 11, 22, 22, 22, 22, 22, 33, 33, 33, 33, 33])

배열 인덱싱은 다차원 배열의 각 차원에 대해서도 할 수 있다.

a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
a
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])
a[:, [True, False, False, True]]
array([[ 1,  4],
       [ 5,  8],
       [ 9, 12]])
a[[2, 0, 1], :]
array([[ 9, 10, 11, 12],
       [ 1,  2,  3,  4],
       [ 5,  6,  7,  8]])

연습 문제 3

다음 행렬과 같은 배열이 있다.

x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
             11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
  1. 이 배열에서 3의 배수를 찾아라.

  2. 이 배열에서 4로 나누면 1이 남는 수를 찾아라.

  3. 이 배열에서 3으로 나누면 나누어지고 4로 나누면 1이 남는 수를 찾아라.