본문 바로가기
ML&DATA/python for data analysis

numpy 기본 : 배열과 벡터 연산

by sun__ 2020. 7. 19.

 

4. 서론

과학 계산을 위한 대부분의 패키지는 numpy의 배열 객체를 데이터 교환을 위한 공통 언어처럼 사용한다.

 

대용량 데이터 배열을 효율적으로 다룰 수 있도록 설계됨. 내부적으로 c로 작성되어 오버헤드 없이 메모리 직접 조작 가능.

 

4.1 ndarray: 다차원 배열 객체

 

4.1.1 ndarray 생성

 

리스트로 생성

data = [[1,2,3],[4,5,6]]
arr = np.array(data)
arr
#out:
#arry([[1,2,3],
	[4,5,6]])

 

np.zeros(), np.ones(): 특정 값으로 채워진 ndarray 생성

 

np.empty() : zeros나 ones처럼 ndarray를 생성하지만 특정 값을 초기화하진 않는다. (출력해보면 0. 나옴)

 

np.random.randn() : 랜덤 값으로 채워진 ndarray 생성

 

np.arange() : 내장 range와 유사함. ndarray 반환

 

 

4.1.2 dtype

 

저수준 언어와 자료형을 맞추는데 용이함. astype메서드로 다른 형으로 명시적 형변환 가능.

숫자형식의 문자열을 담은 배열은 astype으로 숫자로 변환할 수도 있다.

arr = np.array([1.2, -1.3, 4.9, -8.9])
arr.dtype	#out: float64

arr.astype(np.int32)
arr	#out: array([1,-1,4,-8], dtype=int32)

 

4.1.3 numpy 배열의 산술 연산

 

for문 없이 데이터 일괄 처리 가능. 이를 벡터화라고 함.

arr = np.array([[1.,2.],[3.,4.]])
arr*arr	#out: array([[1.,4.],[9.,16.]])
arr-arr #out: array([[0.,0.],[0.,0.]])
1/arr	#out: array([[1.,0.5],[0.3333,0.25]])
arr**0.5	#out: array([[1.,1.4142],[1.7321,2.]])

 

크기가 다른 배열 간의 연산을 브로드캐스팅이라고 하는데 잘 쓰이진 않는다. (책 12장)

 

 

4.1.4 색인(index)와 슬라이싱 

 

배열 조각은 원본 배열의 view이다. 뷰에 대한 변경은 그대로 원본 배열에 반영된다.

예제와 같이 배열조각에 스칼라 값을 대입하면 스칼라 값이 영역 전체로 전파 또는 브로드캐스팅 된다고 한다.

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

 

numpy는 대용량 데이터 처리를 고려하여 설계했기 때문에 기본적으로 모든 대입연산은 참조라고 보면 된다. 데이터를 복사하고 싶다면 명시적으로 다음과 같이 사용해야 한다.

arr = data[2:3].copy()
arr2 = data.copy()

 

다차원 배열을 다룰 땐 arr[i][j][k]와 arr[i,j,k]를 같은 의미로 사용한다.

arr[ : , : , : ] 꼴로 사용할 수 있으므로 후자를 더 자주 사용하게 된다.

 

 

4.1.5 boolean배열로 인덱스 선택하기

 

boolean 배열을 배열의 인덱스로 사용할 수 있다. boolean배열은 인덱싱하려는 축의 길이와 동일한 길이여야 한다.

특이하게도 아래와 같이 boolean색인을 이용해서 선택된 data[names=='Bob']은 뷰가 아닌 copy이다.

처음엔 이질적일 수 있지만 꽤나 직관적인 사용인 걸로 보인다. pandas로 사용하면 더 편하다고 함.

names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data = np.random.randn(7, 4)
data
#out:
#array([[ 1.76212286,  0.70710198, -1.31641368,  2.08525075],
#       [ 1.4259997 , -0.49725741,  0.5385653 ,  1.70713419],
#       [-0.46515703,  0.41160394,  0.38795753,  0.48748894],
#       [-0.33431218, -0.04779873, -1.48188102,  0.39880071],
#       [-0.9212987 ,  0.38653538, -1.25058595,  0.17631704],
#       [-2.4606216 , -1.26660824, -0.5079846 , -0.15963173],
#       [-0.96030837,  0.95033339, -1.28352582,  0.40386911]])

names=='Bob'
#out:
#array([ True, False, False,  True, False, False, False])
#0번, 3번 인덱스가 true인 것을 볼 수 있음

data[names=='Bob']
#out:
#array([[ 1.76212286,  0.70710198, -1.31641368,  2.08525075],
#       [-0.33431218, -0.04779873, -1.48188102,  0.39880071]])
#data의 0번 3번 인덱스가 선택 돼 출력


 

ex) 위 data배열의 음수값을 모두 0으로 만드는 코드

data[data<0] = 0
data
#out:
#array([[1.76212286, 0.70710198, 0.        , 2.08525075],
#       [1.4259997 , 0.        , 0.5385653 , 1.70713419],
#       [0.        , 0.41160394, 0.38795753, 0.48748894],
#       [0.        , 0.        , 0.        , 0.39880071],
#       [0.        , 0.38653538, 0.        , 0.17631704],
#       [0.        , 0.        , 0.        , 0.        ],
#       [0.        , 0.95033339, 0.        , 0.40386911]])

 

4.1.6 정수배열로 인덱스 선택하기 (fancy indexing)

2차원 배열에서 특정 순서로 row를 선택하고 시파면 원하는 순서가 명시된 정수가 담긴 ndarray나 리스트를 넘기면 된다.

arr = np.empty((8,4))
for i in range(8):
    arr[i] = i

arr
#out:
#array([[0., 0., 0., 0.],
#       [1., 1., 1., 1.],
#       [2., 2., 2., 2.],
#       [3., 3., 3., 3.],
#       [4., 4., 4., 4.],
#       [5., 5., 5., 5.],
#       [6., 6., 6., 6.],
#       [7., 7., 7., 7.]])

arr[[4,3,0,6]]
#out:
#array([[4., 4., 4., 4.],
#       [3., 3., 3., 3.],
#       [0., 0., 0., 0.],
#       [6., 6., 6., 6.]])

 

다차원 정수배열을 넘기면 각각의 인덱스 튜플에 대응하는 1차원 배열이 선택된다. 항상 팬시 색인의 결과는 1차원이다.

arr[[1,5,7,2],[0,3,1,2]]
#out: array([1., 5., 7., 2.])
#(1,0), (5,3), (7,1), (2,2) 선택됨

 

 

4.1.7 배열 전치(transpose)와 축 바꾸기

 

배열 전치란 데이터를 복사하지 않고 데이터의 모양이 바뀐 뷰(전치행렬)를 반환하는 기능이다.

arr = np.arange(15).reshape((3,5))

arr
#out:
#array([[ 0,  1,  2,  3,  4],
#       [ 5,  6,  7,  8,  9],
#       [10, 11, 12, 13, 14]])
       
arr.T
#out:
#array([[ 0,  5, 10],
#       [ 1,  6, 11],
#       [ 2,  7, 12],
#       [ 3,  8, 13],
#       [ 4,  9, 14]])

np.dot(arr.T, arr) 	#행렬 내적
#out:
#array([[125, 140, 155, 170, 185],
#       [140, 158, 176, 194, 212],
#       [155, 176, 197, 218, 239],
#       [170, 194, 218, 242, 266],
#       [185, 212, 239, 266, 293]])

 

다차원 배열의 경우 transpose 메서드를 사용한다. 아래 예제에선 0,1번 축을 서로 바꿨다.

이를테면 arr[0,1,?]와 arr[1,0,?]의 위치를 바꿔주면 된다. 그리고 결과의 차원은 2,2,4가 될 것

arr = np.arange(16).reshape(2,2,4)
arr
#out:
#array([[[ 0,  1,  2,  3],
#        [ 4,  5,  6,  7]],
#
#       [[ 8,  9, 10, 11],
#        [12, 13, 14, 15]]])

arr.transpose((1,0,2))
#out:
#array([[[ 0,  1,  2,  3],
#        [ 8,  9, 10, 11]],
#
#       [[ 4,  5,  6,  7],
#        [12, 13, 14, 15]]])

 

swapaxes메서드로 두 개의 축 번호를 받아 배열을 바꾸는 방법도 있다.

arr.swapaxes(1,2)	#arr.transpose((0,2,1))과 같다
#out:
#array([[[ 0,  4],
#        [ 1,  5],
#        [ 2,  6],
#        [ 3,  7]],
#       [[ 8, 12],
#        [ 9, 13],
#        [10, 14],
#        [11, 15]]])

 

swapaxes와 transpose, T 모두 원래 데이터에 대한 복사가 아닌 뷰가 반환된다.

 

 

4.2 유니버셜 함수

배열의 각 원소를 빠르게 처리하는 함수

np.sqrt(x), np.exp(x), np.maximum(x,y), np.modf(x,y) 등 여럿 존재함

arr = np.arange(10)
arr	#out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

np.sqrt(arr) 
#out : 
#array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ,
#       2.23606798, 2.44948974, 2.64575131, 2.82842712, 3.        ])

np.exp(arr)
#array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
#       5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03,
#       2.98095799e+03, 8.10308393e+03])

 

 

4.3 배열지향 프로그래밍

 

반복문을 작성하지 않고 배열 연산을 하는 것을 벡터화라고하며 순수 파이썬 연산에 비해 최대 수백 배까지 빠르다. 부록에서 다루는 브로드캐스팅은 강력한 벡터연산의 예.

 

np.meshgrid 함수는 두 개의 1차원 배열을 받아서 가능한 모든 (x,y)쌍을 만들 수 있도록 2차원 배열 두 개를 반환한다. (행렬 곱으로 모든 쌍을 만들 수 있음)

 

 

4.3.1 배열 연산으로 조건절 표현하기

 

순수 파이썬 연산으로 조건절 표현하면 x if cond else y 구문을 활용하면 된다.

np.where을 사용하면 더 간결하게 작성할 수 있다. np.where의 인자는 배열이 아니라 스칼라여도 된다.

xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
cond = np.array([True, False, True, True, False])

#각 인덱스마다 cond가 true이면 xarr에서, False면 yarr에서 값을 받아 새로운 벡터를 만들고자 함
result = [ (x if b else y) for x,y,b in zip(xarr,yarr,cond) ]
result	#out: [1.1, 2.2, 1.3, 1.4, 2.5]

result = np.where(cond, xarr, yarr)
result	#out: array([1.1, 2.2, 1.3, 1.4, 2.5])

 

ex) 랜덤 데이터가 들어있는 4*4 2차원 배열에서 양수는 모두 2로 음수는 모두 -2로 바꾸는 코드

arr = np.random.randn(4,4) 
arr
#out:
#array([[ 0.41182367,  2.36700958, -0.99200417, -0.15730266],
#       [-0.31411873, -0.36643885, -1.93444125,  1.4454334 ],
#       [ 0.51930734,  0.99350324, -0.11905604,  0.00998165],
#       [-0.47522724, -0.90466411, -0.32780328, -0.1982803 ]])

arr>0
#out:
#array([[ True,  True, False, False],
#       [False, False, False,  True],
#       [ True,  True, False,  True],
#       [False, False, False, False]])

np.where(arr>0, 2, -2)
#out:
#array([[ 2,  2, -2, -2],
#       [-2, -2, -2,  2],
#       [ 2,  2, -2,  2],
#       [-2, -2, -2, -2]])

#또는 이렇게 할 수 있다. (데이터를 아예 바꿔버림)
arr[arr>0] = 2
arr[arr<0] = -2
arr
#out:
#array([[ 2.,  2., -2., -2.],
#       [-2., -2., -2.,  2.],
#       [ 2.,  2., -2.,  2.],
#       [-2., -2., -2., -2.]])

 

 

4.3.2 수학, 통계 메서드

 

mean, sum, cumsum, cumprod 등

arr = np.random.randn(5,4)
arr
#out:
#array([[-0.09698187, -0.20368439,  1.12802544,  0.50058135],
#       [-0.5101975 , -0.97026416, -0.32453478, -0.26215898],
#       [ 0.13382969,  1.6747899 , -1.80246094,  1.11054217],
#       [-0.11467877,  0.58433909, -0.59688885, -0.63397299],
#       [-1.1735514 ,  0.04316415, -0.64697411, -1.22957288]])

arr.mean()	#같은의미 np.mean(arr), arr 모든 요소의 평균
arr.sum()	#arr 모든 요소의 합

arr.mean(axis=1)	#1번 축에 대한 평균. 5x4행렬 이므로 5개의 결과값을 갖는 리스트
arr.sum(axis=0)		#0번 축에 대한 합. 4개의 결과값을 갖는 리스트

arr.cumsum(axis=0)
#out: 0번 축에 대한 누적 합
#array([[-0.09698187, -0.20368439,  1.12802544,  0.50058135],
#       [-0.60717937, -1.17394855,  0.80349066,  0.23842237],
#       [-0.47334968,  0.50084135, -0.99897028,  1.34896454],
#       [-0.58802846,  1.08518044, -1.59585913,  0.71499155],
#       [-1.76157986,  1.12834458, -2.24283324, -0.51458133]])

arr.cumprod(axis=1)
#out: 1번 축에 대한 누적 곱
#array([[-0.09698187,  0.01975369,  0.02228267,  0.01115429],
#       [-0.5101975 ,  0.49502635, -0.16065327,  0.0421167 ],
#       [ 0.13382969,  0.22413661, -0.40399748, -0.44865624],
#       [-0.11467877, -0.06701129,  0.03999829, -0.02535784],
#       [-1.1735514 , -0.05065534,  0.0327727 , -0.04029642]])

 

 

4.3.3 boolean 배열을 위한 메서드

 

ex) 1차원 배열에서 양수 원소의 개수

arr = np.random.randn(100)
(arr>0).sum()

 

any, all 메서드. 불리언 배열에 사용. arr.any(), arr.all() 

any : 하나 이상의 값이 True라면 True 반환

all : 모든 값이 True라면 True 반환

 

불리언 배열이 아니면 0이 아닌 원소는 모두 True로 간주함

 

 

4.3.4 정렬

 

arr.sort() : 정렬

arr.sort(axis=1) : 1번 축에 대해 정렬함. (row마다 정렬)

arr.sort(axis=0) : 0번 축에 대해 정렬함. (col마다 정렬)

 

np.sort(arr) : 실제로 정렬하진 않고, 정렬된 배열을 반환함

 

100 분위수 구하기 : n% 분위수를 구하는 코드

arr = np.random.randn(1000)
arr.sort()

n = input() #백분위수의 n분위수를 구하기 위한 입력
nper = arr[int(0.01 * n * len(arr))]

 

 

4.3.5 집합 관련 함수

 

unique, intersect1d, union1d, in1d, setdiff1d, setxor1d 등 다양한 함수 존재

 

np.unique(arr) : arr의 중복원소를 제거하고 남은 원소를 정렬된 형태로 반환. 실제로 값을 바꾸진 않음

arr = [3,3,4,4,5,1,2]
np.unique(arr)	#out : array([1, 2, 3, 4, 5])
arr		#out : [3, 3, 4, 4, 5, 1, 2]

#순수 파이썬 표현
sorted(set(arr)) #out : [1, 2, 3, 4, 5]

 

np.in1d(x, y) : x배열의 원소를 차례로 보며 두번째 배열의 원소중 하나인지 나타내는 불리언 배열

x = np.array([6,0,0,3,2,5,6])
np.in1d(x, [2,3,6])
#out : array([ True, False, False,  True,  True, False,  True])

 

 

4.4 배열 데이터의 파일 입출력

 

텍스트, 표, 바이너리 형식의 데이터를 불러오거나 저장할 수 있다. 이는 보통 pandas로 수행하므로 numpy의 내장 이진형식의 데이터만 알면 된다.

 

np.save, np.load 는 배열 데이터를 데스크에 저장하고 불러오기 위한 함수이다. 확장자는 바이너리 형식의 .npy이다.

arr = np.arange(10)
np.save('test.npy', arr)

np.load('test.npy')
#out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

 

여러 배열을 압축된 형식으로 저장할 땐 np.savez를 사용할 수 있다. 확장자는 .npz이고 이 파일을 로드할 땐 딕셔너리 형식의 객체에 정보를 받아올 수 있다.

np.savez('test2.npz', a=arr, b=arr)
arch = np.load('test2.npz')

arch['b']
#out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

 

savez대신 savez_compressed를 사용하면 용량이 줄어드는 것 같다. 확장자는 .npz이다.

 

 

 

4.5 선형대수

 

행렬 곱은 x.dot(y) 또는 np.dot(x,y) 또는 x@y를 사용

 

numpy.linalg는 행렬의 분할과 역행렬, 행렬식 등을 포함한다. 포트란 라이브러리 LAPACK으로 구현됨.

 

inv(X) : X의 역행렬 반환

ex) X가 정방행렬이라면 X.dot(inv(X)) => 단위행렬

 

qr(X) : X의 qr분해 행렬 반환

 

diag, dot, trace, det, eig, inv, pinv, qr, svd, solve 등의 함수가 있음

 

 

 

4.6 난수생성

 

np.random : 파이썬의 내장 random함수 보강

 

np.random.normal(size=(x,y)) : 표준정규분포로부터 x * y 크기의 행렬 생성

 

np.random.seed(x) : 시드값 고정

 

np.random.permutation : 임의의 순열을 반환

 

np.random.shuffle : 리스트나 배열의 순서 뒤섞는다.

 

np.random.randint : 주어진 최대/최소 범위 안에서 임의의 난수 추출

 

np.random.RandomState(x) : 시드값 x에 맞는 새로운 난수생성기

rng = np.random.RandomState(1234)

rng.randn(10) #np.random.randn과는 별개의 난수생성기
#out : 
#array([ 0.47143516, -1.19097569,  1.43270697, -0.3126519 , -0.72058873,
#        0.88716294,  0.85958841, -0.6365235 ,  0.01569637, -2.24268495])