numpy의 핵심 자료구조는 ndarray라고 하는 n차원의 배열 객체이다. 이는 파이썬에서 이용할 수 있는 대규모 데이터 집합을 담을 수 있는 빠르고 유연한 자료구조이다.
ndarray배열은 전체 데이터에 대한 수학적인 연산을 벡터화 하여 진행할 수 있도록 해준다.
벡터화(Vectorization)란?
벡터화란 반복문을 사용하지 않고 배열 연산을 수행하는 방식을 말한다. 이는 여러 데이터를 한 번에 처리하는 기법으로, 수학적으로 행렬 단위로 연산을 수행하는 것과 관련이 깊다. ndarray는 연산 과정을 벡터화시킴으로써 보다 고속으로 연산이 가능하고, 루프를 사용하지 않기에 코드가 더 간결해진다.
import numpy as np
data=np.random.randn(2,3) #임의의 값을 생성
data
#out:array([[ 1.57867535, 0.66476244, -0.01575973],[ 0.6936159 , -0.62238816, -0.21432507]])
data * 10
#out:array([[15.78675345, 6.6476244 , -0.15759728],[ 6.936159 , -6.22388164, -2.14325067]])
data+data
#out:array([[ 3.15735069, 1.32952488, -0.03151946],[ 1.3872318 , -1.24477633, -0.42865013]])
ndarray는 '같은 자료형'의 데이터만을 담을 수 있는 포괄적인 다차원 배열이다.
ndarray는 다른 배열과 같이, 각 차원의 '크기'를 알려주는 shape와, 배열에 저장된 자료형을 알려주는 dtype 객체를 가지고있다.
data.shape#데이터의 차원의 크기 출력
#out:(2, 3) -> 2행 3열의 크기를 가지는 2차원 배열
data.dtype#데이터의 원소의 자료형 출력
#out:dtype('float64')
배열을 생성하는 가장 쉬운 방법은 numpy의 array 함수를 이용하는 것이다. 순차적인 객체(이터레이터 이거나 이터레이터로 변환 가능한 객체)를 넘겨받고, 넘겨받은 데이터가 들어 있는 새로운 ndarray를 생성한다.
data1=[1,2,3,4,5]
type(data1)
#out:list
np_data1=np.array(data1)
type(np_data1)
#out:numpy.ndarray
'같은 길이를 가지는 배열들'을 다차원 배열로 변환 가능하다.
data2=[[1,2,3,4],[5,6,7,8]]
np_data2=np.array(data2)
np_data2
#out:array([[1, 2, 3, 4],[5, 6, 7, 8]])
np_data2.ndim
#out:2
ndim 메소드를 통해서 해당 데이터의 차원을 반환받을 수 있다. (shape는 크기를 반환)
np.zeros, ones 함수를 이용해서도 배열을 생성할 수 있다. 원하는 크기의 튜플을 넘기면 된다.
np.zeros(10)
#out:array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
np.zeros((3,6))
#out:array([[0., 0., 0., 0., 0., 0.],[0., 0., 0., 0., 0., 0.],[0., 0., 0., 0., 0., 0.]])
arange 함수는 numpy의 range함수라고 생각하면 편하다.
np.arange(15)
#out:array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
'ndarray'의 astype 메서드를 사용해서 배열의 dtype을 다른 자료형으로 명시적으로 변환(캐스팅) 가능하다.
arr=np.array([1,2,3,4,5])
arr.dtype
#out:dtype('int64')
float_arr=arr.astype(np.float64)
float_arr.dtype
#out:dtype('float64')
위 예제는 정수형을 부동소수점으로 캐스팅하였다. 만약 부동소수점수를 정수형 dtype으로 변환하면 소수점 아래는 비워진다.
arr2=np.array([1.2,3.4,-45.2])
arr2.dtype
#out:dtype('float64')
int_arr2=arr2.astype(np.int32)
int_arr2
#out:array([1,3,-45],dtype=int32)
'숫자 형식'의 문자열을 담고있는 배열이 있다면,
마찬가지로 astype을 사용하여 숫자 자료형으로 dtype을 변환할 수 있다.
ndarray 객체의 중요한 특징은 for 문을 작성하지 않고 데이터를 일괄 처리할 수 있다는 것이다. 이를 '벡터화'라고 하는데, 같은 크기의 배열 간의 산술 연산은 배열의 각 원소 단위로 적용된다.
ndarray의 벡터화 연산은 작성 코드에는 반복문을 사용하지 않는 것이 맞지만, numpy 내부적으로 살펴보면 c언어를 활용하여 반복문을 돈다.
스칼라 인자가 포함된 산술 연산의 경우 배열 내의 모든 원소에 스칼라 인자가 적용된다.
arr*arr
#out:array([[ 1., 4., 9.],[16., 25., 36.]])
arr-arr
#out:array([[0., 0., 0.],[0., 0., 0.]])
arr*0.5
#out:array([[0.5, 1. , 1.5],[2. , 2.5, 3. ]])
같은 크기를 가지는 배열 간의 비교 연산은 불리언 배열을 반환한다.
arr2=np.array([[0.,4.,1.],[7.,2.,12.]])
arr2>arr
#out:array([[False, True, False],[ True, False, True]])
ndarray의 색인은 다룰 주제가 많다. 데이터의 부분집합이나 개별 요소를 선택하기 위한 수많은 방법이 존재하기 때문이다. 비교적 1차원 배열은 단순한데, 표면적으로는 리스트와 유사하게 동작한다.
arr=np.arange(10)
arr
#out:array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr[5]
#out:5
arr[5:8]
#out:array([5,6,7])
arr[5:8]=12
#out:array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
위에서 볼 수 있듯이 arr[5:8]=12 처럼 배열 조각에 스칼라값을 대입하면 12가 선택영역 전체로 전파된다. 리스트와의 중요한 차이점은 배열 조각이 원본 배열의 '뷰'라는 점이다.
뷰 란
뷰는 기존 배열과 메모리를 공유하는 새로운 배열이다.
numpy는 대용량의 데이터 처리를 염두에 두고 설계되었기 때문에 만약 numpy가 데이터 복사를 남발한다면 성능과 메모리 문제에 마주치게될 것이다. 고로, numpy는 뷰(view)를 사용함으로써 이를 방지한다. 만약에 뷰 대신 슬라이스의 복사본을 얻고 싶다면, copy()를 사용하면 된다.
다차원 배열을 다룰 때는 더 많은 옵션이 있다. 2차원 배열에서 각 색인에 해당하는 요소는 스칼라값이 아니라 1차원 배열임을 기억하자.
arr2d=np.array([[1,2,3],[4,5,6],[7,8,9]])
arr2d[1]
#out:array([4,5,6])
따라서 개별 요소를 접근할 때는 재귀적으로 접근해야한다. 하지만 그렇게 하기는 다소 귀찮으므로 콤마로 구분된 색인 리스트를 넘겨도 된다. 그러므로 다음 두 표현은 동일하다.
arr2d[0][2]
arr2d[0,2]
다차원 배열에서 마지막 색인을 생략하면 반환되는 객체는 상위 차원의 데이터를 포함하고 있는 한 차원 낮은 ndarray가 된다.
예를들어, 2x2x3 크기의 3차원 배열 arr3d라는 객체가 있다면 arr3d[0]의 크기는 2x3인 2차원 배열이 된다.
arr3d=np.array([[[1,2,3],[4,5,6]],[[7,8,9],[10,11,12]]])
arr3d
#out:array([[[ 1, 2, 3],[ 4, 5, 6]],[[ 7, 8, 9],[10, 11, 12]]])
arr3d[0]
copy_arr3d=arr3d[0].copy()
arr3d[0]=64 #arr3d[0]에는 스칼라 값과 배열 모두 대입할 수 있다.
arr3d
#out:array([[[64, 64, 64],[64, 64, 64]],[[ 7, 8, 9],[10, 11, 12]]])
파이썬의 리스트 같은 1차원 객체처럼 ndarray는 익숙한 문법으로 슬라이싱할 수 있다.
arr
#out:array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
arr[1:6]
#out:array([ 1, 2, 3, 4, 64])
2차원 배열의 슬라이싱하는 방법은 조금 다르다.
arr2d
#out:array([[1, 2, 3],[4, 5, 6],[7, 8, 9]])
arr2d[:2]
#out:array([[1, 2, 3],[4, 5, 6]]) ->2차원 객체 반환
arr2d[:2,:1]
#out:array([[1],[4]]) -> 마찬가지로 2차원 객체
불리언 배열은 반드시 색인하려는 축의 길이와 동일한 길이를 가져야 한다. 불리언 배열 색인도 슬라이스나 요소를 선택하는 데 짜 맞출 수 있다.
불리언 배열과 색인하려는 축의 길이가 다르더라도 에러가 뜨거나 실패하진 않는다. 그렇기에 이 기능을 사용할 때 불리언 배열의 크기를 주의하여 살펴 보아야한다.
중복된 이름이 포함된 배열과 Numpy의 randn 함수를 사용해서 임의의 표준 정규 분포 데이터를 생성하자.
names=np.array(['Bob','Joe','Will','Bob','Will','Joe','Joe'])
data=np.random.randn(7,4)
data
#out: [[ 0.08516211, 0.86533149, -0.04682661, 1.57142815],
# [-0.21182578, 0.8965067 , 1.49844582, -0.41909834],
# [-0.06534977, -0.26753473, 0.33158171, 2.07495602],
# [ 0.05852164, -0.34931047, 1.24066775, -0.37347369],
# [ 0.50908895, 0.10597614, 0.11396517, -1.21356034],
# [-0.22541898, 1.14692604, -0.76156805, 1.73640102],
# [-0.65800707, -0.80421816, 2.05344066, 0.31559467]])
각각의 이름은 data 배열의 각 로우에 '대응' 한다고 가정하자. 만약에 'Bob'과 같은 이름을 선택하려면 배열에 대한 비교연산도 벡터화 되므로 names를 'Bob' 문자열과 비교하면 배열을 반환한다.
names=['Bob']
#out:array([ True, False, False, True, False, False, False])
#이 불리언 배열을 색인으로 사용할 수 있다.
data[names=="Bob"]
#out:array([[ 0.08516211, 0.86533149, -0.04682661, 1.57142815],
# [ 0.05852164, -0.34931047, 1.24066775, -0.37347369]])
#불리언 배열 색인도 슬라이스나 요소를 선택하는 데 짜 맞출 수 있다.
data[names=='Bob',:2]
#out: array([[ 0.5132709 , -0.64642041],[-0.96730205, 2.06318124]])
Bob이 아닌 요소들을 선택하려면 != 연산자를 사용하거나 ~를 사용해서 조건절을 부인하면 된다.
세 가지 이름 중에서 두 가지 이름을 선택하려면 &,| 같은 논리 연산자를 사용한 불리언 조건을 사용하면된다.
names!='Bob'
#~(names=='Bob') #이 코드는 위 코드와 출력값이 같다
#out:array([[-0.21182578, 0.8965067 , 1.49844582, -0.41909834],
# [-0.06534977, -0.26753473, 0.33158171, 2.07495602],
# [ 0.50908895, 0.10597614, 0.11396517, -1.21356034],
# [-0.22541898, 1.14692604, -0.76156805, 1.73640102],
# [-0.65800707, -0.80421816, 2.05344066, 0.31559467]])
mask=(names=='Bob')|(names=="Will") #Bob 이거나 Will인 경우
data[mask]
#out:array([[ 0.08516211, 0.86533149, -0.04682661, 1.57142815],
# [-0.06534977, -0.26753473, 0.33158171, 2.07495602],
# [ 0.05852164, -0.34931047, 1.24066775, -0.37347369],
# [ 0.50908895, 0.10597614, 0.11396517, -1.21356034]])
배열에 불리언 색인을 이용해서 데이터를 선택하면 반환대는 배열의 내용이 바뀌지 않더라도 항상 데이터 복사가 발생한다.
팬시 색인은 정수 배열을 사용한 색인을 설명하기 위해 numpy에서 차용한 단어다. 예를 위해,(8,4) 크기의 배열을 생성하자.
arr=np.zeros((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.]])
"""
특정한 순서로 로우를 선택하고 싶다면 그냥 원하는 순서가 명시된 ndarray나 리스트를 인자로 넘기면 된다.
arr[[4,3,0,6]]
#out:array([[4., 4., 4., 4.],
# [3., 3., 3., 3.],
# [0., 0., 0., 0.],
# [6., 6., 6., 6.]])
#색인으로 음수를 사용하면 끝에서부터 로우를 선택하게 된다
arr[[-3,-5,-7]]
#out:array([[5., 5., 5., 5.],
# [3., 3., 3., 3.],
# [1., 1., 1., 1.]])
다차원 색인 배열을 넘기면 각각의 색인 튜플에 대응하는 1차원 배열이 선택된다.
arr=np.arange(32).reshape((8,4)) #reshape 메서드는 이후에 다루겠다.
arr
#out: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, 30, 31]])
arr[[1,3,4,5],[0,1,3,2]]
#array([ 4, 13, 19, 22])
#만약 행렬의 행과 열에 대응하는 사각형 모양의 값이 선택될려면 아래처럼 하면된다.
arr[[1,3,4,5]][:,[0,1,3,2]]
#out:array([[ 4, 5, 7, 6],
# [12, 13, 15, 14],
# [16, 17, 19, 18],
# [20, 21, 23, 22]])
arr[[1,3,4,5],[0,1,3,2]]와 arr[[1,3,4,5]][:,[0,1,3,2]]의 차이를 명확히 하자.
ndarray는 transpose 메서드와 T라는 이름의 속성을 가지고있다. 이는 데이터를 복사하지 않고 데이터의 모양이 바뀐 뷰를 반환하는 특별한 기능이다
행렬 계산을 할 때 자주 사용하게 되는데, 예를 들어 행렬의 내적은 np.dot을 이용해서 구할 수 있다.
arr=np.random.randn(6,3)
arr
#out:array([[ 0.71144895, 0.81074102, 1.47309279],
# [-0.64599032, 1.55907519, -0.53721194],
# [-0.34174219, -0.38158762, 0.5747142 ],
# [-0.27342593, -0.09154743, -1.06507134],
# [-1.35391805, 1.3829889 , 1.05933088],
# [ 1.21034188, -1.15108771, 0.28477523]])
arr.T
#out:array([[ 0.71144895, -0.64599032, -0.34174219, -0.27342593, -1.35391805,
# 1.21034188],
# [ 0.81074102, 1.55907519, -0.38158762, -0.09154743, 1.3829889 ,
# -1.15108771],
# [ 1.47309279, -0.53721194, 0.5747142 , -1.06507134, 1.05933088,
# 0.28477523]])
np.dot(arr.T,arr) #arr.T와 arr의 내적
#np.dot(arr.T,arr)
#arr.T와 arr의 내적
np.dot(arr.T,arr)
#arr.T와 arr의 내적
#out:array([[ 4.41303413, -3.54057391, 0.40030625],
# [-3.54057391, 6.4796677 , 1.37218524],
# [ 0.40030625, 1.37218524, 5.12655128]])
다차원 배열의 경우 transpose 메서드는 튜플로 축 번호를 받아서 치환한다.
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()
#out:array([[[ 0, 8],[ 4, 12]],
# [[ 1, 9],[ 5, 13]],
# [[ 2, 10],[ 6, 14]],
# [[ 3, 11],[ 7, 15]]])
arr.transpose((1,0,2)) #튜플을 인자로 넘겨서, 축 순서를 지정 가능하다.
#out:array([[[ 0, 1, 2, 3],[ 8, 9, 10, 11]],
# [[ 4, 5, 6, 7],[12, 13, 14, 15]]])
이번 로그에서는 ndarray의 다양한 연산과 색인 등을 알아보았다. 다음 Log에서는 Numpy의 ufunc에 대해 알아보자