NumPy 란?
numpy 는 파이썬에서 과학계산을 위한 가장 기본적인 패키지 입니다. 다차원 배열(array) 객체, 여러가지 파생 객체(마스크 배열이나 행렬(matrix) 등), 수학/논리/변형/정렬/선택/IO/이산 푸리에 연산, 기본 선형대수, 기본 통계 연산, 무작위 시뮬레이션 등의 배열에 관한 연산을 위한 다양한 루틴 제공하는 파이썬 라이브러리입니다.
NumPy 패키지의 핵심은 ndarray 객체입니다. ndarray는 동일한 데이터 유형의 n 차원 배열을, 빠른 성능을 위해 미리 컴파일된 코드로 수행되는 많은 연산과 함께 캡슐화되어 있습니다. NumPy 배열과 표준적인 파이썬 시퀀스(sequence, list/dictionary 등)는 다음과 같은 중요한 차이가 존재합니다.
- Numpy 배열은 생성시 크기가 고정됩니다. (파이썬 list의 경우에는 동적으로 크기가 변경됩니다) ndarray의 크기를 변경시킬 경우, 기존 배열은 사라지고 새로운 배열이 생성됩니다.
- NumPy 배열의 요소는 모두 동일한 유형이어야 합니다. 따라서 메모리에서 동일한 크기를 차지합니다. 단 파이썬 객체(Numpy도 파이썬 객체임) 배열의 경우엔 예외로서, 이 경우 요소의 크기가 다른 배열이 허용됩니다.
- NumPy 배열은 대량의 숫자 데이터에 대한 고급 수학 연산 및 기타 연산을 쉽게 적용할 수 있습니다. 일반적으로 이러한 연산은 파이썬의 내장 시퀀스를 사용할 때보다 더 효율적으로 실행됩니다.
- NumPy 배열을 사용하는 과학적 수학적 패키지가 점점 더 늘어나고 있습니다. 이러한 패키지는 일반적으로 파이썬 시퀀스 입력을 지원하지만, 시퀀스 입력을 NumPy 배열로 변환하여 처리한 후 NumPy 배열로 출력하는 경우가 많습니다. 다시 말해서 오늘날의 과학/수학 패키지를 효율적으로 사용하려면 Python 내장 시퀀스 유형 사용법만으로는 충분하지 않고, NumPy에 대해서도 잘 알아야 합니다.
시퀀스의 크기와 속도의 관계는 과학 계산에서 매우 중요합니다. 간단한 예로서, 1차원 수열의 각 원소를, 동일한 크기의 다른 수열의 각 원소와 곱하는 경우를 생각해 보겠습니다. 데이터가 리스트 a, b에 각각 저장되어 있다면 아래와 같이 반복계산을 하여야 합니다.
c=[]
for i in range(len(a)):
c.append(a[i] * b[i])
답은 정확하지만, a 와 b가 수백만개의 숫자로 이루어졌다면, 파이썬에서 루프를 돌리는 비효율이 발생하게 됩니다. 동일한 작업을 C로 수행하면 훨씬 빠르게 처리됩니다(이해하기 쉽도록 변수 선언, 초기화, 메모리 할당 등은 생략했습니다).
for (i=-; i < rows; i++) {
c[i] = a[i]*b[i];
}
이렇게 하면 파이썬 코드를 한줄 한줄 해석(interpretation)해석하고 파이썬 객체를 다루는 데 따른 오버헤드를 절약할 수 있지만, 파이썬 코딩으로 얻을 수 있는 장점을 포기해야 합니다. 더군다나 데이터의 차원이 늘어나면 코딩 작업이 더 복잡해집니다. 예를 들어 2차원 배열의 경우 C 코느는 아래와 같이 늘어나게 됩니다.
for (i = 0; i < rows; i++) {
for (j = 0; j < columns; j++) {
c[i][j] = a[i][j]*b[i][j];
}
}
NumPy 를 사용하면 파이썬의 장점과 C의 장점을 모두 얻을 수 있습니다. ndarray배열의 경우 요소별 연산은 기본이어서, 미리 컴파일된 C 코드로 빠르게 수행됩니다. 아래는 NumPy 코드입니다.
c = a * b
이 코드는 위에서 수행했던 예제와 동일한 기능을, C 언어에 필적하는 속도로, 파이썬 코드의 간단함을 모두 얻으면서 수행합니다. 실제로 NumPy 관용구는 훨씬 간단합니다. 이 마지막 예시는 벡터화(vectorization)과 브로드캐스팅(bradcasting)이라는, 많은 NumPy 기능의 기반이 되는 두가지 기능을 보여준 것입니다.
NumPy의 속도가 빠른 이유는
벡터화(vectorization)란 코드에 명시적인 루프 또는 인덱스가 없는 것을 말하는데, 물론 이것은 보이지 않는 곳에서 사전 컴파일된 C 코드에서 이루어집니다. 벡터화된 코드는 다음과 같은 많은 장점이 있습니다.
- 벡터화된 코드는 간결하며 읽기 쉽습니다.
- 코드 길이가 짧아 버그가 줄어듭니다.
- 코드가 표준적인 수학 표현과 많이 닮았습니다(따라서 일반적인 수학적 구조를 쉽게 코딩할 수 있습니다).
- 벡터화하면 프로그램 코드가 좀더 파이썬 같은 코드가 됩니다. 벡터화가 없다면 우리 코드는 수많은 for 루프가 반복되는, 비효율적이고 읽기 힘든 코드가 될 것입니다.
브로드캐스팅(Broadcasting)이란 연산이 암시적으로 요소별로 동작하는 것을 설명하는데 사용하는 용어로서, 일반적으로 NumPy의 모든 연산은(산술 연산 뿐만 아니라 논리/비트/함수 등 연산도) 이러한 암시적 요소별 방식으로, 즉 브로드캐스팅 방식으로 동작합니다. 더우기 위의 예제에서 a 와 b 는 동일한 크기의 다차원 배열일 수도 있고, 스칼라(단일 값) 또는 단일 배열일 수도 있고, 크기가 다른 두개의 배열일 수도 있습니다. (크기가 다를 경우에는 결과 브로드캐스팅이 모호하지 않은 방식으로, 작은 배열이 큰 배열 형태로 "확장 가능"할 경우 가능합니다) 브로드캐스팅에 대한 더 자세한 내용은 여기를 읽어보세요.
누가 NumPy를 사용하나?
NumPy는 ndarray를 시작으로 객체지향접근법을 완벽하게 지원합니다. 예를 들어 ndarray는 수많은 메소드와 속성을 가지고 있는 클래스입니다. 대부분의 메소드는 가장 바깥쪽 NumPy 네임스페이스에서 함수로 미러링되므로, 프로그래머는 자신이 좋아하는 방식(객체지향이든 함수형이든)으로 프로그래밍할 수 있습니다. 이러한 유연성으로 인해 NumPy 배열과 NumPy ndarray 클래스는 파이썬에서 사용되는 다차원 데이터 교환의 사실상의 표준이 되었습니다.
NumPy 퀵스타트 |
선행 요건
파이썬에 대해 약간 알고 있어야 합니다. 복습하려면 파이썬 투토리얼을 참조하세요. 또한 예제를 시행하려면 NumPy외에도 matplotlib이 설치되어 있어야 합니다.
이 강좌는 NumPy의 배열에 대한 간략한 개요입니다. n차원(n>=2) 배열 표현 방법 및 처리 방법에 대해 설명합니다. 특히, for 루프를 사용하지 않고 n차원 배열에 일반적인 함수를 적용하는 방법과, n차원 배열의 축(axis) 및 형태(shape) 속성을 이해하고자 하는 경우 이 문서가 도움이 될 수 있습니다.
기초
NumPy의 주요 대상은 동질적인 다차원 배열입니다. 즉, 모든 요소의 유형이 동일한 테이블로서, 음수가 아닌 정수 튜플로 인덱싱 가능합니다. NumPy에서 차원은 축(axes)라고 합니다.
예를 들어, 3차원 공간상의 점 좌표에 대한 배열 (예 [1, 2, 1])은 축이 하나입니다. 이 측에는 3개의 요소가 있으므로, 길이가 3이라고 합니다. 아래의 예의 경우 축이 2개이며, 길이는 3인 배열입니다.
[[1., 0., 0.],
[0., 1., 2.]]
NumPy의 배열 클래스는 ndarray라고 합니다. 별명으로 array라고 하는 경우도 있습니다. 참고로 numpy.array는 표준 파이썬 라이브러리 클래스 array.array와 같지 않습니다. array.array의 경우 1차원 배열만 다루며 기능도 제한적입니다. 아래는 ndarray의 중요한 속성중 일부입니다.
ndarray.ndim : 배열의 축(차원)의 수.
ndarray.shape : 배열의 차원입니다. 정수 튜플로서 각 차원별 배열의 수를 나타냅니다. n개의 행과 m개의 열로 이루어진 행렬(matrix)의 경우 shape는 (n,m)이 됩니다. 따라서 shape의 길이는 축의 수 (ndim)이 됩니다.
ndarray.size : 배열의 총 요소수. 즉 shape 요소의 곱과 같습니다.
ndarray.dtype : 배열에 포함된 요소들의 유형을 설명하는 객체. 표준 Python 유형을 사용한 dtype을 생성하거나 명시할 수 있습니다. 그 외에도 NumPy는 자신만의 유형을 제공합니다. numpy.int32, numpy.int16, numpy.float64 등이 그 예입니다.
ndarray.itemsize : 배열의 각 요소의 크기(단위: byte). 예를 들어 유형이 float64인 요소의 배열은 itemsize가 8(64/8) 이며, complex32 유형의 경우 itemsize는 4(32/8)입니다. ndarray.itemsize 는 ndarray.dtype.itemsize와 동등합니다.
ndarray.data : 배열의 실제 요소를 포함하는 버퍼입니다. 일반적으로 이 속성은 사용할 필요가 없습니다. 대부분 배열의 요소는 인덱스를 사용하여 접근하기 때문입니다.
예제
>>> import numpy as np
>>> a = np.arange(15).reshape(3,5)
>>> a
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
>>> a.shape
(3, 5)
>>> a.ndim
2
>>> a.dtype.name
'int32'
>>> a.itemsize
4
>>> a.size
15
>>> type(a)
<class 'numpy.ndarray'>
>>> b = np.array([6, 7, 8])
>>> b
array([6, 7, 8])
>>> type(b)
<class 'numpy.ndarray'>
배열 생성
배열을 생성하는 방법은 여러가지가 있습니다. 예를 들어 파이쎤 list 또는 tuple로부터 배열을 생성할 경우 array 함수를 사용합니다. 생성되는 배열의 유형은 해당 시퀀스에 있는 요소의 유형에서 자동으로 결정됩니다.
>>> import numpy as np
>>> a = np.array([2,3,4])
>>> a
array([2, 3, 4])
>>> a.dtype
dtype('int32')
>>> b=np.array([1.2, 3.5, 5.1])
>>> b.dtype
dtype('float64')
array 함수를 사용할 때, 하나의 시퀀스를 매개변수를 사용하는 게 아니라, 여러개의 매개변수를 넣는 오류가 자주 발생합니다.
>>> a = np.array(1, 2, 3, 4) #ERROR
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: array() takes from 1 to 2 positional arguments but 4 were given
>>> a = np.array([1, 2, 3, 4]) #RIGHT
array는 시퀀스의 시퀀스를 2차원 배열로, 시퀀스의 시퀀스의 시퀀스를 3차원 배열로... 등으로 변환합니다.
>>> b = np. array([(1.5, 2, 3), (4, 5, 6)])
>>> b
array([[1.5, 2. , 3. ],
[4. , 5. , 6. ]])
# 아래도 동일하게 작동합니다.
>>> c = np.array([[1.5, 2, 3], [4., 5., 6.]])
>>> c
array([[1.5, 2. , 3. ],
[4. , 5. , 6. ]])
배열을 생성할 때 데이터 유형을 명시적으로 지정할 수도 있습니다.
>>> c = np.array([[1,2], [3,4]], dtype=complex)
>>> c
array([[1.+0.j, 2.+0.j],
[3.+0.j, 4.+0.j]])
가끔은 배열 요소자체는 몰라도 크기는 알고 있는 경우가 있습니다. 따라서 NumPy는 초기 자리표시(placeholder)를 사용해 배열을 생성하는 함수가 여러가지 존재합니다. 이를 사용하면 많은 비용이 소요되는 배열 확장을 줄일 수 있습니다.
zeros 함수는 0으로 채워진 배열, ones는 1로 채워진 배열, empty는 메모리 상태에 따라 초기 내용이 무작위로 배치되는 배열을 생성합니다. 생성된 배열의 dtype은 float64가 기본이지만, dtype 매개변수를 사용해 지정할 수도 있습니다.
>>> np.zeros((3,4))
array([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
>>> np.ones((2,3,4), dtype=np.int16)
array([[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]],
[[1, 1, 1, 1],
[1, 1, 1, 1],
[1, 1, 1, 1]]], dtype=int16)
>>> np.empty((2,3))
array([[3.73603959e-262, 6.02658058e-154, 6.55490914e-260], # may vary
[5.30498948e-313, 3.14673309e-307, 1.00000000e+000]])
연속 수열을 생성할 경우, NumPy의 arange 함수를 사용할 수 있습니다. arange는 파이썬 내장 range와 비슷하지만 배열을 반환합니다.
>>> np.arange(10,30,5)
array([10, 15, 20, 25])
>>> np.arange(0, 2, 0.3) #float 값도 지원합니다.
array([0. , 0.3, 0.6, 0.9, 1.2, 1.5, 1.8])
arange를 부동 소숫점 인수와 함께 사용하면, 일반적으로 얻어지는 요소의 수를 예측하기가 불가능합니다. 부동 소숫점 수의 정밀도 때문입니다. 이 때문에 단계수 대신에 원하는 요소의 수를 인수로 받는 linspace 함수를 사용하는 것이 더 좋습니다.
>>> from numpy import pi
>>> np.linspace(0,2,9) # 9 numbers from 0 to 2)
array([0. , 0.25, 0.5 , 0.75, 1. , 1.25, 1.5 , 1.75, 2. ])
>>> x = np.linspace(0, 2*pi, 100) #이런 식으로 사용하면 많은 점에서 평가할 때 유용합니다.
>>> f = np.sin(x)
자세한 내용은 아래를 읽어보세요. array, zeros, zeros_like, ones, ones_like, empty, empty_like, arange, linspace, numpy.random.Generator.rand, numpy.random.Generator.randn, fromfunction, fromfile
배열 프린트
배열을 프린트하면 중첩된 리스트와 비슷한 방식으로 표시해주지만, 다음과 같은 차이가 있습니다.
- 마지막 축은 왼쪽에서 오른쪽으로 프린트됨
- 마지막 바로 앞 축은 위에서 아래로 프린트됨
- 나머지 축은 위에서 아래로 프린트되며, 각각의 사이에는 빈줄이 들어갑니다.
일차원 배열은 행으로서 프린트되며, 2차원 배열은 행렬로, 3차원 배열은 배열의 목록으로 프린트됩니다.
>>> a = np.arange(6)
>>> print(a)
[0 1 2 3 4 5]
>>> b = np.arange(12).reshape(4,3)
>>> print(b)
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
>>> c = np.arange(24).reshape(2,3,4)
>>> print(c)
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
참고로, reshape에 대한 내용은 아래에 나옵니다.
배열이 너무 클 경우, NumPy는 자동적으로 중간 부분을 생락하고 모서리 부분만 프린트합니다.
>>> print(np.arange(10000))
[ 0 1 2 ... 9997 9998 9999]
>>> print(np.arange(10000).reshape(100,100))
[[ 0 1 2 ... 97 98 99]
[ 100 101 102 ... 197 198 199]
[ 200 201 202 ... 297 298 299]
...
[9700 9701 9702 ... 9797 9798 9799]
[9800 9801 9802 ... 9897 9898 9899]
[9900 9901 9902 ... 9997 9998 9999]]
이러한 방식을 비활성화하고, NumPy가 전체 배열을 프린트하도록 만들려면 set_printoptions를 사용하여 옵션을 변경해야 합니다.
np.set_printoptions(threshold=sys.maxsize) # sys module should be imported
기본 연산
배열에 대한 산술 연산은 각 요소별로 이루어집니다. 새로운 배열이 생성된 후 연산 결과가 새로운 배열에 들어갑니다.
>>> a = np.array([20, 30, 40, 50])
>>> b = np.arange(4)
>>> b
array([0, 1, 2, 3])
>>> c = a -b
>>> c
array([20, 29, 38, 47])
>>> b**2
array([0, 1, 4, 9])
>>> 10 * np.sin(a)
array([ 9.12945251, -9.88031624, 7.4511316 , -2.62374854])
>>> a < 35
array([ True, True, False, False])
일반적인 행렬 언어와는 달리 곱셈 연산자 * 는 요소별로 작용합니다. 행렬의 곱셈은 @연산자(파이썬 >=3.5) 또는 dot 함수를 사용하여 수행할 수 있습니다.
>>> A = np.array([[1,1], [0,1]])
>>> B = np.array([[2,0], [3,4]])
>>> A*B # 요소별 곱셈
array([[2, 0],
[0, 4]])
>>> A @ B # 행렬 곱셈
array([[5, 4],
[3, 4]])
>>> A.dot(B)
array([[5, 4],
[3, 4]])
+= 또는 *=과 같은 일부 연산의 경우, 새로운 배열을 생성하는 대신 기존 배열을 변경시킵니다.
>>> rg = np.random.default_rng(1) #기본 난수 생성기 인스턴스 생성
>>> a = np.ones((2,3), dtype = int)
>>> b = rg.random((2,3))
>>> a *= 3
>>> a
array([[3, 3, 3],
[3, 3, 3]])
>>> b += a
>>> b
array([[3.51182162, 3.9504637 , 3.14415961],
[3.94864945, 3.31183145, 3.42332645]])
>>> a += b # b 가 자동적으로 int 유형으로 변환되지 않아서 오류 발생
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
numpy.core._exceptions._UFuncOutputCastingError: Cannot cast ufunc 'add' output from dtype('float64') to dtype('int32') with casting rule 'same_kind'
유형이 다른 배결간에 연산이 일어날 경우, 결과 배열의 유형은 좀더 일반적이거나 더 정밀한 유형을 따르게 됩니다(이를 업캐스팅(upcasting)이라고 합니다).
>>> a = np.ones(3, dtype=np.int32)
>>> b = np.linspace(0, pi, 3)
>>> b.dtype.name
'float64'
>>> c = a + b
>>> c
array([1. , 2.57079633, 4.14159265])
>>> c.dtype.name
'float64'
>>> d = np.exp(c * 1j)
>>> d
array([ 0.54030231+0.84147098j, -0.84147098+0.54030231j,
-0.54030231-0.84147098j])
>>> d.dtype.name
'complex128'
배열내 모든 요소의 합과 같은 단항(unary) 연산자의 경우, ndarray 클래스의 메소드로서 구현됩니다.
>>> a = rg.random((2,3))
>>> a
array([[0.82770259, 0.40919914, 0.54959369],
[0.02755911, 0.75351311, 0.53814331]])
>>> a.sum()
3.1057109529998157
>>> a.min()
0.027559113243068367
>>> a.max()
0.8277025938204418
이들 연산자는 배열을 형태(shape)에 관계없이 숫자의 목록인 것처럼 적용되는 것이 기본입니다. 하지만, axis 매개변수를 명시하면 배열내 특정 축을 따라 연산을 적용시킬 수 있습니다.
>>> b = np.arange(12).reshape(3,4)
>>> b
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> b.sum(axis=0) # 열별 합계
array([12, 15, 18, 21])
>>> b.min(axis=1) # 행별 최소값
array([0, 4, 8])
>>> b.cumsum(axis=1) # 각 행을 따라 누적합
array([[ 0, 1, 3, 6],
[ 4, 9, 15, 22],
[ 8, 17, 27, 38]])
범용 함수 (universal functions)
NumPy는 sin, cos, exp과 같은 익숙한 수학 함수도 제공합니다. NumPy에서는 이들을 "범용함수"(ufunc)라고 합니다. NumPy에서 이들 함수는 배열내 각 요소별로 작용하여 새로운 결과 배열을 생성합니다.
>>> B = np.arange(3)
>>> B
array([0, 1, 2])
>>> np.exp(B)
array([1. , 2.71828183, 7.3890561 ])
>>> np.sqrt(B)
array([0. , 1. , 1.41421356])
>>> C = np.array([2., -1., 4.])
>>> np.add(B, C)
array([2., 0., 6.])
자세한 내용은 아래를 보세요 all, any, apply_along_axis, argmax, argmin, argsort, average, bincount, ceil, clip, conj, corrcoef, cov, cross, cumprod, cumsum, diff, dot, floor, inner, invert, lexsort, max, maximum, mean, median, min, minimum, nonzero, outer, prod, re, round, sort, std, sum, trace, transpose, var, vdot, vectorize, where
인덱싱, 자르기, 반복하기(iterating)
1 차원 배열은 마치 list나 다른 Python 시퀀스처럼 인덱싱, 자르기, 반복하기가 가능합니다.
>>> a = np.arange(10)**3
>>> a
array([ 0, 1, 8, 27, 64, 125, 216, 343, 512, 729], dtype=int32)
>>> a[2]
8
>>> a[2:5]
array([ 8, 27, 64], dtype=int32)
>>>
>>> # 아래 식은 a[0:6:2] = 1000과 동등합니다.
>>> a[:6:2] = 1000
>>> a
array([1000, 1, 1000, 27, 1000, 125, 216, 343, 512, 729],
dtype=int32)
>>> a[::-1] # reverse
array([ 729, 512, 343, 216, 125, 1000, 27, 1000, 1, 1000],
dtype=int32)
>>> for i in a:
... print(i**(1/3.))
...
9.999999999999998
1.0
9.999999999999998
3.0
9.999999999999998
5.0
5.999999999999999
6.999999999999999
7.999999999999999
8.999999999999998
다차원 배열의 경우 축하나당 하나의 인덱스를 가질 수 있습니다. 이들 인덱스는 쉼표로 구분된 튜플로 주어집니다.
>>> def f(x, y):
... return 10 * x + y
...
>>> b = np.fromfunction(f, (5,4), dtype=int) # 재미있는 함수네요.
>>> b
array([[ 0, 1, 2, 3],
[10, 11, 12, 13],
[20, 21, 22, 23],
[30, 31, 32, 33],
[40, 41, 42, 43]])
>>> b[2,3]
23
>>> b[0:5,1]
array([ 1, 11, 21, 31, 41])
>>> b[:, 1]
array([ 1, 11, 21, 31, 41])
>>> b[1:3, :]
array([[10, 11, 12, 13],
[20, 21, 22, 23]])
축의 수보다 인덱스가 적다면, 빠진 인덱스는 전체 슬라이스(:)라고 취급됩니다.
>>> b[-1] # 마지막 행
array([40, 41, 42, 43])
b[i] 는 i 다음에 나머지 축을 표현하는데 필요한 만큼 : 가 있는 걸로 취급됩니다. 또한 이것을 b[i, ,,,] 과 같이 표현할 수도 있습니다.
점(...)은 완전한 인덱싱 튜플을 생성하는데 필요한 만큼의 콜론(:)을 의미합니다. 예를 들어 x가 축이 5개인 배열일 경우,
- x[1, 2, ...] 는 x[1, 2, :, :, :]와 동등합니다.
- x[..., 3]은 x[:, :, :, :, 3]과 동등합니다.
- x[4, ..., 5, :]의 경우 x[4, :, :, 5, :]와 동등합니다.
>>> c = np.array([[[ 0, 1, 2], # 3D 배열
... [ 10, 12, 13]],
... [[100, 101, 102],
... [110, 112, 113]]])
>>> c.shape
(2, 2, 3)
>>> c[1, ...] # c[1, :, :] 또는 c[1] 과 동일함
array([[100, 101, 102],
[110, 112, 113]])
>>> c[..., 2] # c[:, :, 2]와 동일함
array([[ 2, 13],
[102, 113]])
다차원 배열에 대한 반복(iterating)은 첫번째 축에 대해 적용됩니다.
>>> for row in b:
... print(row)
...
[0 1 2 3]
[10 11 12 13]
[20 21 22 23]
[30 31 32 33]
[40 41 42 43]
하지만 배열내 각각의 요소에 대해 연산을 수행하고 싶을 경우, flat 속성을 사용하여 구현할 수 있습니다.
>>> for element in b.flat:
... print(element)
...
0
1
2
...
41
42
43
자세한 내용은 아래를 보세요.
Indexing on ndarrays, Indexing routines (reference), newaxis, ndenumerate, indices
형태 처리(Shape Manipulation)
배열 형태 변경
배열은 각 축을 따른 요소의 수에 따라 형태가 결정됩니다.
>>> a = np.floor(10 * rg.random((3,4)))
>>> a
array([[3., 7., 3., 4.],
[1., 4., 2., 2.],
[7., 2., 4., 9.]])
>>> a.shape
(3, 4)
배열의 형태는 여러가지 명령어를 사용해 변경할 수 있습니다. 참고로, 아래의 세가지 명령이는 변경된 배열을 반환하지만, 원래의 배열은 변경되지 않습니다.
>>> a.ravel()
array([3., 7., 3., 4., 1., 4., 2., 2., 7., 2., 4., 9.])
>>> a.reshape(3,4)
array([[3., 7., 3., 4.],
[1., 4., 2., 2.],
[7., 2., 4., 9.]])
>>> a.T
array([[3., 1., 7.],
[7., 4., 2.],
[3., 2., 4.],
[4., 2., 9.]])
>>> a.T.shape
(4, 3)
>>> a.shape
(3, 4)
ravel 메소드로 반환되는 배열의 요소의 순서는 일반적으로 "C 스타일"입니다. 즉, 제일 오른쪽 인덱스가 "가장 빨리 변합니다. 따라서 a[0,0] 다음에는 a[0,1]이 오게 됩니다. 배열이 다른 형태로 변경될 때에도 동일하게 처리됩니다. NumPy는 일반적으로 이 순서로 저장된 배열을 생성하므로, 일반적으로 ravel은 인수를 복사할 필요가 없지만, 다른 배열의 조각을 사용한 경우 혹은 특이한 옵션으로 만들어진 경우 복사가 필요할 수도 있습니다. ravel과 reshape 함수는 가장 왼쪽 인덱스가 가장 빠르게 변하는 FORTRAN 스타일의 배열을 사용하도록 인수로 지정할 수도 있습니다.
reshape 함수는 인수를 수정된 형태로 반환하지만, ndarray.resize 메소드는 배열 자체를 변경시킵니다.
>>> a
array([[3., 7., 3., 4.],
[1., 4., 2., 2.],
[7., 2., 4., 9.]])
>>> a.resize((2,6))
>>> a
array([[3., 7., 3., 4., 1., 4.],
[2., 2., 7., 2., 4., 9.]])
reshape 연산에서 차원을 -1로 지정하면, 해당 차원은 자동작으로 계산됩니다.
>>> a.reshape((3, -1))
array([[3., 7., 3., 4.],
[1., 4., 2., 2.],
[7., 2., 4., 9.]])
자세한 내용은 아래를 보세요.
ndarray.shape, reshape, resize, ravel
서로 다른 배열을 쌓아올리기(stacking)
다른 축을 따라 여러개의 배열을 쌓아 올릴 수 있습니다.
>>> a = np.floor(10 * rg.random((2,2)))
>>> a
array([[9., 7.],
[5., 2.]])
>>> b = np.floor(10 * rg.random((2,2)))
>>> b
array([[1., 9.],
[5., 1.]])
>>> np.vstack((a, b))
array([[9., 7.],
[5., 2.],
[1., 9.],
[5., 1.]])
>>> np.hstack((a, b))
array([[9., 7., 1., 9.],
[5., 2., 5., 1.]])
column_stack 함수는 1차원 배열을 열로서 쌓아서 2차원 배열로 만듧니다. 이는 2차원 배열의 경우에만 hstack과 동등하게 됩니다.
>>> from numpy import newaxis
>>> np.column_stack((a,b)) # 2차원의 경우
array([[9., 7., 1., 9.],
[5., 2., 5., 1.]])
>>> a = np.array([4., 2.])
>>> b = np.array([3., 8.])
>>> np.column_stack((a,b)) # 2차원 배열을 반환함
array([[4., 3.],
[2., 8.]])
>>> np.hstack((a, b)) # 결과가 다름
array([4., 2., 3., 8.])
>>> a[:, newaxis] # a 를 2차원 column vector로 보기
array([[4.],
[2.]])
>>> np.column_stack((a[:,newaxis], b[:,newaxis]))
array([[4., 3.],
[2., 8.]])
>>> np.hstack((a[:,newaxis], b[:,newaxis])) # 결과가 동일함
array([[4., 3.],
[2., 8.]])
반면, row_stack 함수는 항상 vstack 함수와 동등합니다. 사실 row_stack은 vstack의 별명(alias)입니다.
>>> np.column_stack is np.hstack
False
>>> np.row_stack is np.vstack
True
일반적으로 2차원 이상의 배열에 대해, hstack 함수는 2번째 축을 따라 쌓으며, vstack은 첫번째 축을 따라 쌓고, concatenate의 경우에는 어떤 축을 따라 붙일 것인지를 지정하는 추가 인수가 존재합니다.
참고
복잡한 경우, r_ 와 c_ 를 사용하면 한 축을 따라 숫자를 쌓아 배열을 만들 수 있습니다. 이들은 범위 리터럴 : 를 사용할 수 있습니다.
>>> np.r_[1:4, 0, 4]
array([1, 2, 3, 0, 4])
자세한 내용은 아래를 보세요.
hstack, vstack, column_stack, concatenate, c_, r_
배열을 여러개의 작은 배열로 분리(split)
hsplit을 사용하면 수평축을 따라 배열을 분리할 수 있습니다. 몇개(동일한 크기)로 나누고 싶은지를 지정하는 방법과, 몇번째 열에서 자를지를 지정하는 방법이 있습니다.
>>> a = np.floor(10* rg.random((2,12)))
>>> a
array([[6., 7., 6., 9., 0., 5., 4., 0., 6., 8., 5., 2.],
[8., 5., 5., 7., 1., 8., 6., 7., 1., 8., 1., 0.]])
>>> # Split 'a' into 3
>>> np.hsplit(a, 3)
[array([[6., 7., 6., 9.],
[8., 5., 5., 7.]]), array([[0., 5., 4., 0.],
[1., 8., 6., 7.]]), array([[6., 8., 5., 2.],
[1., 8., 1., 0.]])]
>>> # 3번째 및 4번째 컬럼 이후 'a'를 자르기
>>> np.hsplit(a, (3,4))
[array([[6., 7., 6.],
[8., 5., 5.]]), array([[9.],
[7.]]), array([[0., 5., 4., 0., 6., 8., 5., 2.],
[1., 8., 6., 7., 1., 8., 1., 0.]])]
vsplit은 수직축 방향을 따라 자르며, array_split는 어떤 축을 따라 자를지를 명시할 수 있습니다.
복사와 뷰(Copy and View)
배열을 조작하거나 연산할 때, 데이터가 새로운 데이터로 복사되기도 하고 아닐때도 있습니다. 이는 초보자들에게 혼선을 일으키기 쉽습니다.
복사가 안되는 경우
단순 할당은 객체도 데이터도 복사하지 않습니다.
>>> a = np.arange(12).reshape((3,4))
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> b = a
>>> b is a
True
파이썬은 변경가능한 객체를 참조로 전달하므로, 함수 호출은 복사본을 만들지 않습니다.
>>> def f(x):
... print(id(x))
...
>>> id(a) # id는 객체의 유일 식별자입니다.
2121616396656 # 이 값은 달라질 수 있습니다.
>>> f(a)
2121616396656
뷰(View) 또는 얕은 복사(Shallow Copy)
다른 배열 객체가 동일한 데이터를 공유할 수 있습니다. view 메소드를 사용하면 동일한 데이터에 대한 새로운 배열 객체를 생성합니다.
>>> c = a.view()
>>> c is a
False
>>> c.base is a # c 는 a가 소유한 데이터에 대한 view입ㄴ디ㅏ.
False
>>> c.flags.owndata
False
>>> c = c.reshape((2,6))
>>> a.shape # a 자체는 변경되지 않습니다.
(3, 4)
>>> c
array([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11]])
>>> c[0,4] = 1234 # c 를 변경시키면 a 도 변경됩니다.
>>> a
array([[ 0, 1, 2, 3],
[1234, 5, 6, 7],
[ 8, 9, 10, 11]])
배열을 자르면(slice) 뷰(view)를 반환합니다.
>>> s = a[:, 1:3]
>>> s[:] = 100 # s[:]는 s에 대한 뷰입니다. s=10과 s[:]=10은 다릅니다.
>>> a
array([[ 0, 100, 100, 3],
[1234, 100, 100, 7],
[ 8, 100, 100, 11]])
깊은 복사(Deep Copy)
copy 메소드를 사용하면 해당 배열 및 데이터를 완벽하게 복제합니다.
>>> d = a.copy()
>>> d is a
False
>>> d.base is a # d는 a와 아무것도 공유하지 않습니다.
False
>>> d[0,0]=9999
>>> a # a는 변경되지 않습니다.
array([[ 0, 100, 100, 3],
[1234, 100, 100, 7],
[ 8, 100, 100, 11]])
원래 배열이 필요하지 않는 경우, 자르기(slicing)후 copy를 호출하는 것이 좋습니다. 예를 들어 a가 거대한 중간 결과이고, b는 a 중의 일부인 최종 결과라고 할 경우, slicing으로 b를 생성할 때 deep copy를 수행합니다.
>>> a = np.arange(int(1e8))
>>> b = a[:100].copy()
>>> del a # 'a'배열 메모리를 해제합니다.
함수와 메소드 개요
아래는 범주에 따라 유용한 NumPy함수 및 메소드를 정렬한 것입니다.
배열 생성
arange, array, copy, empty, empty_like, eye, fromfile, fromfunction, identity, linspace, logspace, mgrid, ogrid, ones, ones_like, r_, zeros, zeros_like
변환
ndarray.astype, atleast_1d, atleast_2d, atleast_3d, mat
조작
array_split, column_stack, concatenate, diagonal, dsplit, dstack, hsplit, hstack, ndarray.item, newaxis, ravel, repeat, reshape, resize, squeeze, swapaxes, take, transpose, vsplit, vstack
질문
정렬
argmax, argmin, argsort, max, min, ptp, searchsorted, sort
연산
choose, compress, cumprod, cumsum, inner, ndarray.fill, imag, prod, put, putmask, real, sum
기본적 통계
기본 선형 대수
cross, dot, outer, linalg.svd, vdot
약간 고급
브로드캐스팅(Broadcasting) 규칙
브로드캐스팅을 사용하면 범용 함수(universal function)가 정확히 같은 모양이 아닌 입력도 의미있게 처리시킬 수 있습니다.
브로드캐스팅의 첫번째 규칙은, 입력된 배열의 차원수가 모두 같지 않은 경우, 모든 배열의 차원수가 같아질 때까지 작은 배열의 형태(shape)에 1을 반복해서 추가하는 것입니다.
두번째 규칙은 특정 차원을 따라 크기가 1인 배열이 있을 경우, 이 배열이 해당 차원을 따라 가장 큰 형태(shape)를 가진 것처럼 작동합니다. "브로드 캐스트" 배열의 경우, 배열요소의 값은 해당 차원을 따라 동일한 것으로 가정됩니다.
브로드캐스팅 규칙을 적용한 후, 배열의 크기는 모두 일치해야 합니다. 좀 더 자세한 내용은 Broadcasting을 참고하세요.
고급 인덱싱과 인덱스 트릭
NumPy는 일반 파이썬 시퀀스보다 더 많은 인덱싱 기능을 제공합니다. 정수에 의한 인덱싱, 슬라이싱에 의한 인덱싱과 같은 위에서 본 것 이외에 정수 배열 또는 불린 배열을 사용해서 인덱싱이 가능합니다.
인덱스 배열을 사용한 인덱싱
>>> a = np.arange(12)**2
>>> i = np.array([1, 1, 3, 8, 5]) # 인덱스 배열
>>> a[i] # i 배열의 위치에 있는 a 원소
array([ 1, 1, 9, 64, 25])
>>> j=np.array([[3,4], [9,7]]) # 2차원 인덱스 배열
>>> a[j]
array([[ 9, 16],
[81, 49]])
인덱싱되는 배열 a 가 다차원일 경우, 단일 인덱스 배열은 a의 첫번째 차원을 참조합니다. 다음 예제에서는 팔레트를 사용하여 레이블 이미지를 컬러 이미지로 변환하는 예로서 이 동작을 예시합니다.
>>> palette = np.array([[0, 0, 0], # black
... [255, 0, 0], # red
... [0, 255, 0], # green
... [0, 0, 266], # blue
... [255, 255, 255]]) # white
>>> image = np.array([[0, 1, 2, 0], # 각 값은 palette에 있는 색 번호)
... [0, 3, 4, 0]])
>>> palette[image]
array([[[ 0, 0, 0],
[255, 0, 0],
[ 0, 255, 0],
[ 0, 0, 0]],
[[ 0, 0, 0],
[ 0, 0, 266],
[255, 255, 255],
[ 0, 0, 0]]])
1차원 이상의 인덱싱도 가능합니다. 단, 인덱스 배열은 모두 동일한 형태(shape)이어야 합니다.
>>> a = np.arange(12).reshape(3,-1)
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> i = np.array([[0,1],
... [1,2]])
>>> j = np.array([[2,1],
... [3,3]])
>>> a[i,j]
array([[ 2, 5],
[ 7, 11]])
>>> a[i, 2]
array([[ 2, 6],
[ 6, 10]])
>>> a[:, j]
array([[[ 2, 1],
[ 3, 3]],
[[ 6, 5],
[ 7, 7]],
[[10, 9],
[11, 11]]])
파이썬에서 arr[i, j]는 arr[(i, j)] 와 동일합니다. 따라서 튜플에 i, j 를 넣은 뒤 인덱싱을 수행할 수 있습니다.
>>> l = (i, j)
>>> l
(array([[0, 1],
[1, 2]]), array([[2, 1],
[3, 3]]))
>>> a[l]
array([[ 2, 5],
[ 7, 11]])
하지만 i 와 j 를 배열로 만들면 이를 수행할 수 없습니다. 이 배열은 a의 1차원을 인덱싱하는 것으로 해석되기 때문입니다.
>>> s = np.array([i, j])
>>> s
array([[[0, 1],
[1, 2]],
[[2, 1],
[3, 3]]])
>>> a[s] # 이렇게는 불가능
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: index 3 is out of bounds for axis 0 with size 3
>>> a[tuple(s)]
array([[ 2, 5],
[ 7, 11]])
배열을 사용한 인덱싱의 또다른 일반적인 용도는 시간 종속 시계열의 최대값을 검색하는 것입니다.
>>> time = np.linspace(20, 145, 5)
>>> data = np.sin(np.arange(20)).reshape(5,4)
>>> time
array([ 20. , 51.25, 82.5 , 113.75, 145. ])
>>> data
array([[ 0. , 0.84147098, 0.90929743, 0.14112001],
[-0.7568025 , -0.95892427, -0.2794155 , 0.6569866 ],
[ 0.98935825, 0.41211849, -0.54402111, -0.99999021],
[-0.53657292, 0.42016704, 0.99060736, 0.65028784],
[-0.28790332, -0.96139749, -0.75098725, 0.14987721]])
>>> ind = data.argmax(axis=0)
>>> ind
array([2, 0, 3, 1], dtype=int64)
>>> time_max = time[ind]
>>> data_max = data[ind, range(data.shape[1])] # 아직 이해가 안됩니다ㅠㅠ
>>> time_max
array([ 82.5 , 20. , 113.75, 51.25])
>>> data_max
array([0.98935825, 0.84147098, 0.99060736, 0.6569866 ])
>>> np.all(data_max == data.max(axis=0))
True
또한 배열 인덱싱을 할당 대상으로 사용할 수도 있습니다.
>>> a = np.arange(5)
>>> a[[1,3,4]] = 0
>>> a
array([0, 0, 2, 0, 0])
그러나 인덱스 리스트에 반복이 있을 경우, 항당이 여러번 수행되어 맨 마지막 값만 남게 됩니다.
>>> a = np.arange(5)
>>> a[[0,0,2]]=[1,2,3]
>>> a
array([2, 1, 3, 3, 4])
이는 당연하지만, 만약 파이썬의 += 을 사용할 경우, 결과가 예상과 달라질 수 있습니다.
>>> a = np.arange(5)
>>> a[[0, 0, 2]] += 1
>>> a
array([1, 1, 3, 3, 4])
위에서 인덱스에 0이 두번 나왔음에도 0번째 요소는 단 한번만 증가됩니다 파이썬에서 a+=1 이란 a=a+1과 동등하기 때문입니다.
불린 배열을 사용한 인덱싱
(정수) 인덱스 배열을 사용하여 배열을 인덱싱할 경우, 선택하고자 하는 인덱스 목록을 제공합니다. 불린 인덳느는 접근법이 다릅니다. 즉, 어떤 아이템을 선택할 것인지 아닌지를 명시적으로 선택합니다.
불린 인덱싱에 대한 가장 자연스런 생각은, 원 배열과 동일한 형태(shape)의 불린 인덱스 배열을 사용하는 것입니다.
>>> a = np.arange(12).reshape(3,4)
>>> b = a>4
>>> b
array([[False, False, False, False],
[False, True, True, True],
[ True, True, True, True]])
>>> a[b]
array([ 5, 6, 7, 8, 9, 10, 11])
이러한 특징은 할당에 매우 유용합니다. 즉, 조건에 맞는 요소만 찾아서 값을 할당할 수 있습니다.
>>> a[b] = 0
>>> a
array([[0, 1, 2, 3],
[4, 0, 0, 0],
[0, 0, 0, 0]])
아래의 예에서는 불린 인덱싱을 사용하여 Mandelbrot set 의 미미지를 생성해보겠습니다.
import numpy as np
import matplotlib.pyplot as plt
def mandelbrot(h, w, maxit=20, r=2):
'''Returns an image of the Mandelbrot fractal of size (h,w).'''
x = np.linspace(-2.5, 1.5, 4*h+1)
y = np.linspace(-1.5, 1.5, 3*w+1)
A, B = np.meshgrid(x,y)
C = A + B*1j
z = np.zeros_like(C)
divtime = maxit + np.zeros(z.shape, dtype=int)
for i in range(maxit):
z = z**2 + C
diverge = abs(z) > r # who is diverging
div_now = diverge & (divtime ==maxit) # who is diverging now
divtime[div_now] = i # note when
z[diverge] = r # avoid diverging too much
return divtime
plt.clf()
plt.imshow(mandelbrot(400, 400))
plt.show()
불린 인덱싱을 사용하는 두번째 방법은 정수 인덱싱과 비슷합니다. 즉, 배열의 각 차원에 대해 원하는 슬라이스를 선택하는 1D 불린 배열을 제공하는 것입니다.
>>> a = np.arange(12).reshape(3,4)
>>> b1 = np.array([False, True, True])
>>> b2 = np.array([True, False, True, False])
>>>
>>> a[b1, :] # 행을 선택합니다.
array([[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[b1] # 위와 동일합니다.
array([[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[:, b2] # 열을 선택합니다.
array([[ 0, 2],
[ 4, 6],
[ 8, 10]])
>>> a[b1, b2] # 예상과 다르네요. ??
array([ 4, 10])
1D 불린 배열은 자르고자하는 차원(축)의 길이와 일치해야 합니다. 위의 예에서 b1의 길이은 행의 수와 동일한 3, b2의 길이는 열의 수와 동일한 4입니다.
ix_() 함수
ix_() 함수를 사용하면 서로다른 벡터를 결합하여, 각각의 n-uplet에 대한 결과를 얻을 수 있습니다. 예를 들어 각 벡터 a, b, c로부터 얻어진 삼중항에 대해 a+b*c를 모두 계산하려는 경우입니다.
>>> import numpy as np
>>> a = np.array([2,3,4,5])
>>> b = np.array([8,5,4])
>>> c = np.array([5,4,6,8,3])
>>> ax, bx, cx = np.ix_(a,b,c)
>>> ax
array([[[2]],
[[3]],
[[4]],
[[5]]])
>>> bx
array([[[8],
[5],
[4]]])
>>> cx
array([[[5, 4, 6, 8, 3]]])
>>> ax.shape, bx.shape, cx.shape
((4, 1, 1), (1, 3, 1), (1, 1, 5))
>>> result = ax + bx * cx
>>> result
array([[[42, 34, 50, 66, 26],
[27, 22, 32, 42, 17],
[22, 18, 26, 34, 14]],
[[43, 35, 51, 67, 27],
[28, 23, 33, 43, 18],
[23, 19, 27, 35, 15]],
[[44, 36, 52, 68, 28],
[29, 24, 34, 44, 19],
[24, 20, 28, 36, 16]],
[[45, 37, 53, 69, 29],
[30, 25, 35, 45, 20],
[25, 21, 29, 37, 17]]])
이 reduce? 를 아래와 같이 구현할 수 있습니다.
>>> def ufunc_reduce(ufct, *vectors):
... vs = np.ix_(*vectors)
... r = ufct.identity
... for v in vs:
... r = ufct(r, v)
... return r
...
>>> ufunc_reduce(np.add, a, b, c)
array([[[15, 14, 16, 18, 13],
[12, 11, 13, 15, 10],
[11, 10, 12, 14, 9]],
[[16, 15, 17, 19, 14],
[13, 12, 14, 16, 11],
[12, 11, 13, 15, 10]],
[[17, 16, 18, 20, 15],
[14, 13, 15, 17, 12],
[13, 12, 14, 16, 11]],
[[18, 17, 19, 21, 16],
[15, 14, 16, 18, 13],
[14, 13, 15, 17, 12]]])
이처럼 reduce 버전을 사용하면 일반 ufunc.reduce에 비해 브로드캐스팅 규칙을 활용할 수 있어서, 출력 크기와 벡터수를 곱한 크기의 인자 배열을 생성하지 않는다는 장점이 있습니다.
문자열 인덱싱
Structured arrays 를 보세요.
팁과 트릭
간단하지만 유용한 팁 몇가지를 소개합니다.
"자동" Reshaping
배열의 차원을 바꿀 때, 크기중 하나를 생략하면 자동적으로 결정됩니다.
>>> a = np.arange(30)
>>> b = a.reshape((2, -1, 3)) # -1을 넣으면 알아서 채우리는 뜻입니다.
>>> b.shape
(2, 5, 3)
>>> b
array([[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]],
[[15, 16, 17],
[18, 19, 20],
[21, 22, 23],
[24, 25, 26],
[27, 28, 29]]])
벡터 쌓기(Vector Stacking)
동일한 크기의 행 벡터 리스트에서 2D 배열을 어떻게 만들 수 있을까요? MATLAB에서는 상당히 쉽습니다. x, y가 같은 길이의 벡터라면 m =[x;y] 라고 하면 해결되죠. NumPy의 경우 어떤 방법으로 쌓느냐에 따라, colum_stack, dstack, hstack, vstack 함수를 사용해 이루어집니다. 예를 들면 아래와 같습니다.
>>> x = np.arange(0, 10, 2)
>>> y = np.arange(5)
>>> m = np.vstack([x, y])
>>> m
array([[0, 2, 4, 6, 8],
[0, 1, 2, 3, 4]])
>>> xy = np.hstack([x,y])
>>> xy
array([0, 2, 4, 6, 8, 0, 1, 2, 3, 4])
다만, 2차원 이상인 경우, 이 함수가 이상하게 작동할 수 있습니다.
히스토그램(Histogram)
NumPy의 histogram 함수를 배열에 적용하면 한 쌍의 벡터가 반환됩니다. 하나는 배열의 히스토그램, 다른 하나는 bin edges에 대한 벡터입니다. 주의: matplotlib 에도 히스토그램을 만드는 함수가 존재하는데(Matlab과 마찬가지로 hist라고 함) NumPy의 histogram 과는 다릅니다. pylab.hist는 히스토그램을 자동적으로 그리는데 반해, numpy.historgram은 데이터만 생성한다는 점이 주요 차이점입니다.
>>> import numpy as np
>>> rg = np.random.default_rng(1)
>>> import matplotlib.pyplot as plt
>>> mu, sigma = 2, 0.5 # 정규분포 10000개 샘플 생성
>>> v = rg.normal(mu, sigma, 10000)
>>> plt.hist(v, bins=50, density=True) # 50개 단위로 정규 히스토그램 그리기
(array([0.00128706, 0.00257412, 0.00257412, 0.00257412, 0.00772237,
0.00900943, 0.01415767, 0.03861183, 0.04247301, 0.03989889,
.....
3.96588894]), <BarContainer object of 50 artists>)
>>> (n, bins) = np.histogram(v, bins=50, density=True)
>>> plt.plot(.5 * (bins[1:] + bins[:-1]), n) # matplotlib 버전(plot)
[<matplotlib.lines.Line2D object at 0x000002E43A2AA410>] # NumPy 버전
>>> plt.show()
Matplotlib >=3.4 인 경우, plt.stairs(n, bins)를 사용해도 됩니다.
====
이 글은 아래 두 글을 번역한 글입니다.