작성자: admin 작성일시: 2016-09-22 14:03:58 조회수: 4116 다운로드: 137
카테고리: Python 태그목록:

NumPy 배열 사용하기

데이터는 수많은 숫자들로 이루어져 있다. 이 많은 숫자들을 효율적으로 계산하기 위해서는 관련된 데이터를 모두 하나의 변수에 넣고 처리해야 한다. 하나의 변수에 여러 개의 데이터를 넣는 방법으로 리스트를 사용할 수도 있지만 리스트는 속도가 느리고 메모리를 많이 차지하는 단점이 있다. 더 적은 메모리를 사용해서 빠르게 데이터를 처리하려면 배열(array)을 사용해야 한다.

배열은 같은 자료형의 데이터를 정해진 갯수만큼 모아놓은 것이다. 배열은 다음과 같은 점에서 리스트와 다르다.

  1. 모든 원소가 같은 자료형이어야 한다.
  2. 원소의 갯수를 바꿀 수 없다.

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

NumPy는 2005년에 Travis Oliphant가 발표한 수치해석용 Python 패키지이다. 다차원의 배열 자료구조 클래스인 ndarray 클래스를 지원하며 벡터와 행렬을 사용하는 선형대수 계산에 주로 사용된다. 내부적으로는 BLAS 라이브러리와 LAPACK 라이브러리에 기반하고 있어서 C로 구현된 CPython에서만 사용할 수 있다.

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

  • NumPy
    • 수치해석용 Python 라이브러리
    • ndarray 다차원 배열 자료 클래스 제공
    • BLAS/LAPACK 기반
    • CPython에서만 사용 가능
    • 내부 반복문 사용으로 빠른 배열 연산 가능
    • 배열 인덱싱(array indexing)을 사용한 질의(Query) 기능

NumPy 패키지 임포트

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

In:
import numpy as np

1차원 배열 만들기

NumPy의 array라는 함수에 리스트를 넣으면 배열로 변환해 준다. 따라서 1 차원 배열을 만드는 방법은 다음과 같다.

In:
a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
a
Out:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In:
type(a)
Out:
numpy.ndarray

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

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

벡터화 연산

배열 객체는 배열의 각 원소에 대한 연산을 한 번에 처리하는 벡터화 연산(vectorized operation)을 지원한다는 점이다. 예를 들어 다음과 같은 데이터를 모두 2배 해야 하는 경우를 생각하자.

In:
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

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

In:
b = []
for ai in a:
    b.append(ai * 2)
b    
Out:
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

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

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

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

In:
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]

벡터화 연산은 모든 종류의 수학 연산에 대해 적용된다.

In:
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
In:
2 * a + b
Out:
array([12, 24, 36])
In:
np.exp(a)
Out:
array([  2.71828183,   7.3890561 ,  20.08553692])
In:
np.log(b)
Out:
array([ 2.30258509,  2.99573227,  3.40119738])
In:
np.sin(a)
Out:
array([ 0.84147098,  0.90929743,  0.14112001])

2차원 배열 만들기

ndarray 는 N-dimensional Array의 약자이다. 이름 그대로 배열 객체는 단순 리스트와 유사한 1차원 배열 이외에도 2차원 배열, 3차원 배열 등의 다차원 배열 자료 구조를 지원한다.

2차원 배열은 행렬(matrix)이라고도 하는데 행렬에서는 가로줄을 (row)이라고 하고 세로줄을 (column)이라고 한다.

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

In:
b = np.array([[0, 1, 2], [3, 4, 5]])  # 2 x 3 array
b
Out:
array([[0, 1, 2],
       [3, 4, 5]])
In:
len(b)
Out:
2
In:
len(b[0])
Out:
3

연습 문제 1

다음과 같은 행렬을 만든다.

10 20 30 40
50 60 70 80

3차원 배열 만들기

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

In:
c = 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
c
Out:
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]]])
In:
len(c)
Out:
2
In:
len(c[0])
Out:
3
In:
len(c[0][0])
Out:
4

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

배열의 차원 및 크기는 ndim 속성과 shape 속성으로 알 수 있다.

In:
print(a.ndim)
print(a.shape)
1
(3,)
In:
print(b.ndim)
print(b.shape)
2
(2, 3)
In:
print(c.ndim)
print(c.shape)
3
(2, 3, 4)

배열의 인덱싱

배열 객체로 구현한 다차원 배열의 원소 하나 하나는 다음과 같이 콤마(comma ,)를 사용하여 접근할 수 있다. 콤마로 구분된 차원을 축(axis)이라고도 한다. 플롯의 x축과 y축을 떠올리면 될 것이다.

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

배열 슬라이싱

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

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

연습 문제 2

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

m = 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]] 을 슬라이싱한다.

배열 인덱싱

NumPy 배열 객체의 또다른 강력한 기능은 팬시 인덱싱(fancy indexing)이라고도 부르는 배열 인덱싱(array indexing) 방법이다. 인덱싱이라는 이름이 붙었지만 사실은 데이터베이스의 질의(Query) 기능을 수행한다.

배열 인덱싱에서는 대괄호(Bracket, [])안의 인덱스 정보로 숫자나 슬라이스가 아니라 위치 정보를 나타내는 또 다른 ndarray 배열을 받을 수 있다. 여기에서는 이 배열을 편의상 인덱스 배열이라고 부르겠다. 배열 인덱싱의 방식에은 불리안(Boolean) 배열 방식과 정수 배열 방식 두가지가 있다.

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

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

In:
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]
Out:
array([0, 2, 4, 6, 8])

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

In:
a[a % 2 == 0]
Out:
array([0, 2, 4, 6, 8])

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

In:
a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) * 10
idx = np.array([0, 2, 4, 6, 8])
a[idx]
Out:
array([ 0, 20, 40, 60, 80])

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

In:
a = np.array([0, 1, 2, 3]) * 10
idx = np.array([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2])
a[idx]
Out:
array([ 0,  0,  0,  0,  0,  0, 10, 10, 10, 10, 10, 20, 20, 20, 20, 20])

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

In:
a = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]]) 
a
Out:
array([[ 1,  2,  3,  4],
       [ 5,  6,  7,  8],
       [ 9, 10, 11, 12]])
In:
a[:, [True, False, False, True]]
Out:
array([[ 1,  4],
       [ 5,  8],
       [ 9, 12]])
In:
a[[2, 0, 1], :]
Out:
array([[ 9, 10, 11, 12],
       [ 1,  2,  3,  4],
       [ 5,  6,  7,  8]])
  • 배열 인덱싱

    • 불리안(Boolean) 방식 배열 인덱싱

      • True인 원소만 선택
      • 인덱스의 크기가 배열의 크기와 같아야 한다.
    • 위치 지정 방식 배열 인덱싱

      • 지정된 위치의 원소만 선택
      • 인덱스의 크기가 배열의 크기와 달라도 된다.

질문/덧글

차이점이 무엇일까요? tch2*** 2017년 2월 14일 3:47 오전

a = np.linspace(0, 9, 10) 라고 했을때,
1. a[ [a % 2 == 0] ]
2. a[ a % 2 == 0 ]

위 두 값이 동일하게 출력이 됩니다.

2번은 1번에 대한 줄임표현인가요?
아니면 2번과 1번은 서로 다른 연산인건가요?

답변: 차이점이 무엇일까요? 관리자 2017년 2월 17일 5:10 오후

값은 같지만 연산은 다릅니다.
원래는 ``a[ a % 2 == 0 ]`` 이 올바른 코드입니다.
다만 인덱스 값으로 같은 결과를 내는 다차원 배열 또는 리스트의 리스트가 들어간 것 뿐입니다.