9 minute read

이 글에서는 numpy 라이브러리에 대한 기초 지식만 소개합니다. numpy 라이브러리를 이용하면 다음과 같은 작업을 수행할 수 있습니다.

  • 1~3차원의 배열(ndarray)을 생성할 수 있습니다.
  • 생성된 배열의 차원(dimension)을 확인하고 형태(shape)를 변경할 수 있습니다.
  • 1차원 배열(벡터) 및 2차원 배열(행렬) 연산을 수행할 수 있습니다.(선형대수)
    • 1차원 배열: 벡터의 덧셈과 뺄셈, 스칼라배, 벡터의 내적 연산 등
    • 2차원 배열: 행렬의 덧셈과 뺄셈, 스칼라배, 행렬의 곱셈, 판별식, 역행렬, 고윳값 분해 등

이 글에서는 선형대수와 관련된 내용은 생략합니다. 대신에 1~2차원 배열을 생성하고, 인덱싱과 슬라이싱하는 방법을 소개합니다. 먼저 관련 라이브러리를 호출하겠습니다.

import numpy as np

numpy에 포함된 모듈 및 함수를 사용하려면 모듈 및 함수명 앞에 ’numpy.’를 추가해야 하는데, numpy 라이브러리를 호출하면서 마지막에 as np를 추가함으로써 ’np.’로 변경할 수 있습니다. 많은 사용자와 이와 같이 코딩합니다만, 자신만의 스타일이 있다면 굳이 똑같이 따라하지 않아도 됩니다.

1차원 배열

1차원 배열은 선형대수에서의 벡터와 같습니다. 따라서 1차원 배열은 원소가 가로 방향으로 정렬되어 있는 자료구조입니다. 선형대수에서는 벡터에 속한 값을 성분(component)라고 하지만 이 글에서는 원소(element)라고 하겠습니다.

1차원 배열이 갖는 특징 중 리스트와 다른 점은 모든 원소의 자료형(data type)이 같아야 한다는 것입니다. 1차원 배열을 생성할 때 원소의 자료형을 지정할 수 있지만, 만약 자료형이 섞여 있는 원소로 배열을 생성할 때 자료형을 지정하지 않는다면 정수 < 실수 < 문자열 방향으로 원소의 자료형이 강제로 변환됩니다. 즉, 정수와 실수가 섞여 있으면 실수로 변환되고, 숫자와 문자열이 섞여 있으면 문자열로 변환됩니다. 이러한 이유로 배열을 생성했다면 원소의 자료형을 확인하는 것이 좋습니다.

1차원 배열 생성

1차원 배열을 생성하려면 np.array() 함수의 object 매개변수에 리스트 또는 튜플을 지정합니다. dtype 매개변수에는 원소의 자료형을 설정할 수 있는데, 생략하면 object에 지정된 리스트 또는 튜플의 원소에 의해 결정됩니다.

ar1 = np.array(object = [1, 2, 3], dtype = 'int')
ar1
## array([1, 2, 3])
type(ar1)
## <class 'numpy.ndarray'>

첫 번째 코드에서 object에 전달되는 인자는 리스트이며, 리스트의 원소는 모두 정수이므로 dtype='int'를 생략해도 같은 결과를 얻습니다.

두 번째 코드에서 ar1을 출력하면 맨 앞에 ’array’라는 문자열이 추가로 출력되므로 이런 점이 리스트와 구분됩니다.

세 번째 코드에서 ar1의 클래스를 확인해보면 ’numpy.ndarray’라고 출력됩니다.

1차원 배열 확인

1차원 배열 객체에는 다양한 속성과 방식이 포함되어 있지만, 이번 절에서는 차원수와 형태 및 자료형을 확인하는 방법을 소개합니다.

ar1.ndim
## 1

배열의 차원수(number of dimension)를 확인하려면 ndim 속성을 사용합니다. ar1은 원소가 가로 방향의 일렬로 입력되었으므로 차원수는 1이 출력됩니다.

ar1.shape
## (3,)

배열의 형태(shape)를 확인하려면 shape 속성을 사용합니다. ar1의 차원수가 1이므로, 형태를 출력하면 ar1 원소의 개수인 ’(3, )’이 출력됩니다. 이는 1차원 배열의 첫 번째(0번 인덱스) 차원은 가로 방향이라는 것을 의미합니다.

ar1.dtype
## dtype('int64')

배열 원소의 자료형을 확인하려면 dtype 속성을 사용합니다. ar1을 생성할 때 원소의 자료형을 지정했으므로, 이미 알고 있는 것처럼 ar1 원소의 자료형이 정수인 것을 확인했습니다.

1차원 배열의 형 변환

원소의 자료형이 섞여 있는 리스트로 배열을 생성하고, 자료형이 어떻게 변환되는지 확인해보겠습니다.

ar1 = np.array(object = [1, 2.0, '3'])
ar1
## array(['1', '2.0', '3'], dtype='<U32')

원소의 자료형이 섞여 있는 리스트로 1차원 배열을 생성했더니 ar1 원소의 자료형이 문자열로 변환되었습니다. ar1을 출력했을 때 마지막에 출력되는 ’<U32’는 문자열을 의미합니다.

ar1 원소의 자료형이 문자열이므로 실수나 정수로 변경해야 한다면 ar1에 속한 astype() 방식을 사용합니다.

ar1.astype('float')
## array([1., 2., 3.])

ar1 원소의 자료형을 실수로 변환합니다. 문자열이 실수로 변환되면 따옴표가 떨어져 나갑니다.

ar1.astype('int')
## ValueError: invalid literal for int() with base 10: '2.0'

ar1 원소의 자료형을 정수로 변환하려는데, 문자열 ‘2.0’ 때문에 에러가 발생합니다.

주의

소수점이 있는 실수를 따옴표로 감싼 문자열을 실수로 변환할 때 에러가 발생하지 않지만, 정수로 변환할 때는 에러가 발생합니다. 따라서 소수점이 있는 실수를 따옴표로 감싼 문자열을 정수로 변환할 때 먼저 실수로 변환한 다음, 정수로 변환해야 합니다.

ar1.astype('float').astype('int')
## array([1, 2, 3])

ar1 원소의 자료형을 실수, 정수 순서로 변환합니다. 실수가 정수로 변환되는 과정에서 소수점 이하가 절사됩니다.

원소의 간격이 일정한 배열 생성

np.arange() 함수는 간격이 일정한 정수 또는 실수를 원소로 갖는 배열을 생성합니다. 이 함수는 3개 매개변수가 정의되어 있지만, 모든 매개변수에 인자를 지정해야 하는 것은 아닙니다. 만약 숫자를 하나만 지정하면 0부터 지정된 숫자 앞까지 1 간격의 정수 또는 실수를 반환합니다. 0부터 시작하지 않으려면 start 매개변수에 시작하는 숫자, stop 매개변수에는 끝나는 숫자, 마지막으로 step 매개변수에 간격에 해당하는 숫자를 지정합니다. step 매개변수를 생략하면 기본값인 1이 지정됩니다. 그리고 반드시 기억해야 할 점은 stop에 지정된 숫자는 배열에 포함되지 않는다는 것입니다.

np.arange(5)
## array([0, 1, 2, 3, 4])

0부터 1 간격으로 5를 제외한 연속된 정수를 갖는 1차원 배열을 생성합니다.

np.arange(start = 1, stop = 6)
## array([1, 2, 3, 4, 5])

1부터 1 간격으로 6을 제외한 연속된 정수를 갖는 1차원 배열을 생성합니다.

np.arange(start = 1, stop = 11, step = 2)
## array([1, 3, 5, 7, 9])

1부터 2 간격으로 11을 제외한 연속된 정수를 갖는 1차원 배열을 생성합니다. 시작하는 숫자가 끝나는 숫자보다 크면 step에 음수를 지정할 수 있습니다.

np.arange(start = 0, stop = 1, step = 0.1)
## array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])

0부터 0.1 간격으로 1을 제외한 연속된 실수를 갖는 1차원 배열을 생성합니다. 따라서 0.9로 끝납니다. 만약 1을 포함시켜야 한다면 stop 매개변수에 1보다 조금 큰 수, 예를 들어 1.1 또는 1.01을 대신 지정해야 합니다.

참고

Python 기본함수인 range()는 정수만 생성하며, 실수가 포함되면 에러가 발생합니다.

연속된 숫자를 원소로 갖는 배열을 생성할 때 원소의 길이만 정해져 있다면 간격을 자동으로 계산해주는 np.linspace() 함수를 사용하는 것이 좋습니다. 이 함수의 장점은 stop 매개변수에 지정된 숫자를 포함한다는 것입니다.

np.linspace(start = 0, stop = 1, num = 11)
## array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])

원소의 길이를 11로 지정하면 0부터 1까지 0.1 간격의 실수를 원소로 갖는 1차원 배열을 생성합니다.

(학습의 재미를 위해) 만약 2022년 1월 1일 아침에 잰 체중이 80kg이고 1월 31일 밤까지 75kg으로 감량하고 싶을 때, 매일 밤 목표 체중을 같은 양으로 설정하려면?

np.linspace(start = 80, stop = 75, num = 32)
## array([80.        , 79.83870968, 79.67741935, 79.51612903, 79.35483871,
##        79.19354839, 79.03225806, 78.87096774, 78.70967742, 78.5483871 ,
##        78.38709677, 78.22580645, 78.06451613, 77.90322581, 77.74193548,
##        77.58064516, 77.41935484, 77.25806452, 77.09677419, 76.93548387,
##        76.77419355, 76.61290323, 76.4516129 , 76.29032258, 76.12903226,
##        75.96774194, 75.80645161, 75.64516129, 75.48387097, 75.32258065,
##        75.16129032, 75.        ])

1월은 31일까지 있고, 시작 몸무게를 포함해야 하므로 원소의 길이를 32로 정하면 됩니다. 매일 약 160g씩 감량하면 되는군요.

원소가 반복되는 배열 생성

배열 또는 리스트 전체를 원하는 횟수만큼 반복하려면 np.tile() 함수를 사용합니다. 이 함수의 A 매개변수에 배열 또는 리스트를 지정하고, reps 매개변수에 반복할 횟수를 정수로 지정합니다.

np.tile(A = ar1, reps = 2)
## array(['1', '2.0', '3', '1', '2.0', '3'], dtype='<U32')

만약 배열에 포함된 개별 원소별로 두 번씩 반복하려면 np.repeat() 함수를 대신 사용합니다. 이 함수의 a 매개변수에 배열 또는 리스트를 지정하고, repeats 매개변수에 반복할 횟수를 정수로 지정합니다.

np.repeat(a = ar1, repeats = 2)
## array(['1', '1', '2.0', '2.0', '3', '3'], dtype='<U32')

np.repeat() 함수는 배열의 원소별로 반복할 횟수를 다르게 설정할 수 있습니다. repeats 매개변수에 원소별로 반복할 횟수를 리스트로 지정합니다.

np.repeat(a = ar1, repeats = [1, 2, 3])
## array(['1', '2.0', '2.0', '3', '3', '3'], dtype='<U32')

ar1의 첫 번째 원소는 1번, 두 번째 원소는 2번, 세 번째 원소는 3번 반복되었습니다.

np.repeat(a = ar1, repeats = range(3, 0, -1))
## array(['1', '1', '1', '2.0', '2.0', '3'], dtype='<U32')

이번에는 ar1의 첫 번째 원소가 1번, 두 번째 원소는 2번, 세 번째 원소는 1번 반복되었습니다.

1차원 배열의 인덱싱

배열의 원소마다 위치번호인 인덱스(index)가 자동으로 설정되며, 배열의 인덱싱은 인덱스를 정수 스칼라로 지정하여 해당 인덱스 원소를 선택하는 것입니다.

1부터 11까지 홀수를 원소로 갖는 1차원 배열 ar1을 생성하고, 인덱싱을 실습해보겠습니다.

ar1 = np.arange(start = 1, stop = 12, step = 2)
ar1
## array([ 1,  3,  5,  7,  9, 11])

ar1의 0번 인덱스, 즉 첫 번째 원소를 선택합니다.

ar1[0]
## 1

ar1의 1번 인덱스, 즉 두 번째 원소를 선택합니다.

ar1[1]
## 3

ar1의 -1번 인덱스, 즉 마지막 원소를 선택합니다.

ar1[-1]
## 11

이와 같이 배열의 위치번호인 인덱스를 정수 스칼라로 지정하여 해당 위치의 원소를 선택하는 것을 인덱싱이라고 합니다. 인덱싱 결과로 정수 자료형인 배열의 원소를 반환합니다.

배열의 인덱싱에서 정수 스칼라 대신 정수를 원소로 갖는 리스트를 지정할 수 있습니다.

ar1[[3, 2, 1]]
## array([7, 5, 3])

이와 같이 2개 이상의 인덱스를 동시에 지정하기 위해 리스트를 사용하는 것을 팬시 인덱싱(fancy indexing)이라고 하며, 팬시 인덱싱 결과로 배열을 반환합니다.

ar1[[3, 3, 3]]
## array([7, 7, 7])

팬시 인덱싱을 할 때 리스트의 원소를 반복하면 같은 원소가 여러 번 선택됩니다.

참고

리스트에서는 팬시 인덱싱이 실행되지 않습니다!

1차원 배열의 슬라이싱

배열의 슬라이싱은 콜론을 이용하여 연속된 원소를 선택하는 것입니다.

ar1[:3]
## array([1, 3, 5])

ar1의 0~2번 인덱스 원소를 선택합니다. 콜론 앞을 생략하면 0번 인덱스부터 시작합니다.

ar1[3:]
## array([ 7,  9, 11])

ar1의 3번 인덱스부터 마지막 원소까지 선택합니다. 콜론 뒤를 생략하면 마지막 인덱스로 끝납니다.

ar1[:]
## array([ 1,  3,  5,  7,  9, 11])

ar1의 전체 원소를 선택합니다.

주의

배열의 슬라이싱을 할 때 콜론을 생략하면 에러가 발생합니다.

2차원 배열

2차원 배열은 선형대수에서의 행렬과 같습니다. 2차원 배열은 1차원 배열을 행(row)과 열(column)을 갖는 2차원으로 변환한 자료구조입니다. 2차원 배열을 생성하는 2가지 방법을 소개하겠습니다.

2차원 배열 생성

2차원 배열을 생성하는 첫 번째 방법은 np.array() 함수의 object 매개변수에 원소 개수가 같은 리스트를 원소로 갖는 리스트를 지정하는 것입니다.

ar2 = np.array(object = [[1, 2, 3], [4, 5, 6]])
ar2
## array([[1, 2, 3],
##        [4, 5, 6]])
type(ar2)
## <class 'numpy.ndarray'>

첫 번째 코드에서 object에 전달되는 인자는 리스트이며, 원소가 2개인데 모두 리스트입니다. 첫 번째 원소인 [1, 2, 3]은 첫 번째(0번 인덱스) 행이 되고, 두 번째 원소인 [4, 5, 6]은 두 번째(1번 인덱스) 행이 됩니다.

두 번째 코드에서 ar2를 출력하면 2행 3열의 배열이 출력됩니다.

세 번째 코드에서 ar2의 클래스를 확인해보면 ’numpy.ndarray’라고 출력됩니다.

2차원 배열 확인

1차원 배열과 실행하는 코드는 같지만 결과가 조금씩 다릅니다.

ar2.ndim
## 2

ar2는 2행 3열의 배열이므로 차원수는 2가 출력됩니다.

ar2.shape
## (2, 3)

ar2의 차원수가 2이므로, 2차원 배열의 형태를 출력하면 ‘(행 길이, 열 길이)’ 순서로 원소 개수가 각각 출력됩니다. 이는 2차원 배열의 첫 번째(0번 인덱스) 차원은 세로 방향이고, 두 번째(1번 인덱스) 차원은 가로 방향으로 변경된다는 것을 의미입니다. 따라서 위 코드를 실행하면 ar2 원소의 행과 열 개수인 ’(2, 3)’이 출력됩니다.

ar2.dtype
## dtype('int64')

ar2을 생성할 때 원소의 자료형을 지정하지 않았지만 원소가 모두 정수였으므로 ar2 원소의 자료형은 정수로 출력됩니다.

1차원 배열의 재구조화

2차원 배열을 생성하는 두 번째 방법은 1차원 배열의 재구조화를 통해 2차원 배열로 변환하는 것입니다.

0부터 11까지 12개의 원소를 갖는 1차원 배열 ar1을 생성하고, 재구조화를 2차원 배열로 변환해보겠습니다.

ar1 = np.arange(12)
ar1.reshape(3, 4)
## array([[ 0,  1,  2,  3],
##        [ 4,  5,  6,  7],
##        [ 8,  9, 10, 11]])

1차원 배열에는 reshape()이라는 방식이 있는데, 행과 열 길이를 차례로 지정하면 2차원 배열로 변환합니다. 이때, 주의할 사항은 행과 열 길이를 1차원 배열 원소 개수의 약수로 지정해야 한다는 것입니다. ar1의 원소 개수가 12이므로 12의 약수인 3과 4를 지정하여 3행 4열인 2차원 배열로 변환하였습니다.

2차원 배열로 변환된 결과를 살펴보면, 1차원 배열 원소가 가로 방향으로 입력되었다는 것을 알 수 있습니다. reshape() 방식에는 order 매개변수가 정의되어 있는데 전달인자의 기본값이 ’C’입니다. 만약 1차원 배열 원소를 세로 방향으로 입력하고 싶다면 order='F'를 지정합니다.

ar1.reshape(3, 4, order = 'F')
## array([[ 0,  3,  6,  9],
##        [ 1,  4,  7, 10],
##        [ 2,  5,  8, 11]])

reshape() 방식에 행과 열 길이를 지정할 때, 둘 중 하나만 지정하고 나머지를 -1로 지정하면 행 또는 열 길이가 자동으로 계산됩니다. 예를 들어, ar1은 원소 개수가 12이므로 행 길이를 4로 지정하고 열 길이에 -1을 지정하면 3이 지정된 것과 같은 결과를 반환합니다.

ar1.reshape(4, -1)
## array([[ 0,  1,  2],
##        [ 3,  4,  5],
##        [ 6,  7,  8],
##        [ 9, 10, 11]])

주의

행과 열 길이 모두 -1을 지정하면 당연하게도 에러가 발생합니다.

다음 절에서 2차원 배열의 인덱싱 및 슬라이싱 실습을 위해 2차원 배열 ar2를 생성합니다.

ar2 = ar1.reshape(4, -1)

1차원 배열로 변환

ar2는 2차원 배열인데, 1차원 배열로 변환하려면 flatten() 방식을 사용합니다.

ar2.flatten(order = 'C')
## array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

2차원 배열의 인덱싱 및 슬라이싱

ar2의 행과 열 인덱스를 추가하면 아래와 같은 그림이 됩니다.

2차원 배열을 인덱싱할 때 대괄호 안에 콤마를 추가하고 콤마 왼쪽은 행 인덱스, 콤마 오른쪽은 열 인덱스를 차례대로 지정합니다.

ar2[0, 0]
## 0

ar2의 1행 1열 원소를 선택합니다.

ar2[1, 1]
## 4

ar2의 2행 2열 원소를 선택합니다.

콜론을 이용한 슬라이싱을 실행합니다.

ar2[0:2, 0:2]
## array([[0, 1],
##        [3, 4]])

ar2의 1~2행 1~2열 원소를 선택합니다.

ar2[:, 1:3]
## array([[ 1,  2],
##        [ 4,  5],
##        [ 7,  8],
##        [10, 11]])

ar2의 전체 행 2~3열 원소를 선택합니다. 전체 행 또는 전체 열을 선택하려면 빈 콜론을 추가해야 합니다. 그렇지 않으면 에러가 발생합니다.

정수 스칼라나 콜론 대신 리스트를 사용한 팬시 인덱싱도 가능합니다.

ar2[:, [2, 1]]
## array([[ 2,  1],
##        [ 5,  4],
##        [ 8,  7],
##        [11, 10]])

ar2의 일부 열 순서를 변경합니다. 참고로 행과 열 순서를 동시에 변경하는 것은 안됩니다.

이번 장에서는 numpy 라이브러리를 이용하여 1~2차원 배열을 생성하고 인덱싱 및 슬라이싱 사용법을 소개했습니다. numpy의 아주 일부만 소개한 것이므로 벡터의 내적 및 행렬의 고윳값 분해 등 선형대수를 공부하실 분은 공식문서를 참고하시기 바랍니다.

Updated: