{학습 목적}
03 파트에서는 numpy에 대해서 학습한다.
이 부분을 학습하는 이유는 대량 데이터 기반의 프로그램을 빠른 계산으로 처리하게 만들어주어 많은 알고리즘이 넘파이를 기반으로 작성되어있기 때문이다.
또한 넘파이가 배열을 이루는 기본 방식을 이해하는 것은 판다스를 이해하는 데도 많은 도움이 된다.
03. 넘파이
넘파이(Numpy): 파이썬에서 선형대수 기반의 프로그램을 쉽게 만들 수 있도록 지원하는 대표적인 패키지
루프를 사용하지 않고 대량 데이터의 배열 연산을 가능하게 하여 빠른 배열 연산 속도를 보장
넘파이의 데이터 핸들링 기능은 편리성이 떨어진다는 점이 있다.
하지만, 넘파이를 이해하는 것은 파이썬 기반의 머신러닝에서 매우 중요하다.
많은 머신러닝 알고리즘이 넘파이 기반으로 작성돼 있고, 이들 알고리즘의 입력 데이터와 출력 데이터를 넘파이 배열 타입으로 사용하기 때문이다.
[넘파이 ndarray 개요]
데이터의 차원의 차이를 이해하는 것은 매우 중요하다.
array1, array3와 같이 데이터값은 서로 동일하나 차원이 달라서 오류가 발생하기 때문이다.
이 경우 명확히 차원의 차수를 변환하는 방법을 알아야한다. --> reshape() 함수
print('array1: {:0}차원, array2:{:1}차원, array3:{:2}차원'.format(array1.ndim,
array2.ndim, array3.ndim))
[output]
array1: 1차원, array2:2차원, array3: 2차원
ndarray.ndim함수를 통해 각 array의 차원을 알 수 있다.
array()함수의 인자로는 파이썬 리스트 객체가 주로 사용된다.
리스트[]는 1차원이고, 리스트의 리스트[[]]는 2차원과 같은 형태로 배열의 차원과 크기를 쉽게 표현할 수 있기 때문이다.
[ndarray의 데이터 타입]
list1 = [1,2,3]
print(type(list1))
array1 = np.array(list1) # array()함수: list -> ndarray로 변환하는 기능
print(type(array1))
print(array1, array1.dtype) #dtype: 데이터값의 타입을 알려주는 메서드
#[output]
<class 'list'>
<class 'numpy.ndarray'>
[1 2 3] int32
서로 다른 데이터 타입을 가질 수 있는 리스트와는 다르게 ndarray 내의 데이터 타입은 그 연산 특성상 같은 데이터 타입만 가능하다.
만약 다른 데이터 유형이 섞여있는 리스트를 ndarrray로 변경하면 데이터 크기가 더 큰 데이터 타입으로 형 변환을 일괄적용한다.
list2 = [1,2,'test'] # int형과 string형이 섞여있는 리스트
array2 = np.array(list2) # list -> ndarray
print(array2, array2.dtype) #array2의 데이터 값 타입에 대해
list3 = [1,2,3.0] #int형과 float형이 섞여있는 리스트
array3 = np.array(list3)
print(array3, array3.dtype)
#[output]
['1' '2' 'test'] <U11 #숫자형 값 1,2 가 모두 문자열 값으로 변환
[1. 2. 3.] float64 #int 1,2 가 1.2.인 float64 형으로 변환
#astype() 메서드 : ndarray 내 데이터값 타입 변경
array_int = np.array([1,2,3]) #int32형 리스트 -> int32형 ndarray
array_float = array_int.astype('float64') # int32형 ndarray -> float64형 ndarray
print(array_float, array_float.dtype)
array_int1 = array_float.astype('int32') #float64형 ndarray -> int32형 ndarray
print(array_int1, array_int1.dtype)
array_float1 = np.array([1.1, 2.1, 3.1])
array_int2 = array_float1.astype('int32') #float64형 ndarray -> int32형 ndarray
print(array_int2, array_int2.dtype)
#[output]
[1. 2. 3.] float64
[1 2 3] int32
[1 2 3] int32
astype()는 데이터 타입을 작은 크기로 바꾸어 메모리를 절약해야 할 때 이용된다.
[ndarray를 편리하게 생성하기-arange, zeros, ones]
# arange, zeros, ones : 주로 테스트용으로 데이터를 만들거나 대규모의 데이터를 일괄적으로 초기화해야 할 경우에 사용
# arange() : 0부터 함수인자값-1 까지의 값을 순차적으로 ndarray의 데이터값으로 변환 --> 파이썬의 range()와 비슷
sequence_array = np.arange(10) #[0 1 2 3 4 5 6 7 8 9]
print(sequence_array)
print(sequence_array.dtype, sequence_array.shape) #int32, (10,)
#[output]
[0 1 2 3 4 5 6 7 8 9] #1차원 ndarray
int32 (10,)
#zeros() : 함수 인자로 튜플 형태의 shape(행,열) 값을 입력하면 모든 값을 0으로 채운 shape를 가진 ndarray 반환
zero_array = np.zeros((3,2), dtype = 'int32') #3행 2열의 행렬의 요소 값을 모두 int32형인 0으로 바꿈
print(zero_array) # [[0 0][0 0][0 0]]
print(zero_array.dtype, zero_array.shape) # int 32, (3,2)
#ones() : 함수 인자로 튜플 형태의 shape(행,열) 값을 입력하면 모든 값을 1로 채운 shape를 가진 ndarray 반환
one_array = np.ones((3,2)) # 함수 인자로 dtype값 정해주지 않으면 default로 float64형의 데이터로 ndarray 구성
print(one_array) # [[1.1.][1.1.][1.1.]]
print(one_array.dtype, one_array.shape) #float64, (3,2)
#[output]
[[0 0]
[0 0]
[0 0]]
int32 (3, 2)
[[1. 1.]
[1. 1.]
[1. 1.]]
float64 (3, 2)
[ndarray의 차원과 크기를 변경하는 reshape()]
#reshape(): ndarray를 특정 차원 및 크기로 변환한다.
array1 = np.arange(10)
print('array1:\n', array1)
array2 = array1.reshape(2,5) #2행 5열로 변환
print('array2:\n', array2)
array3 = array1.reshape(5,2) #5행 2열로 변환
print('array3:\n', array3)
#[output]
array1:
[0 1 2 3 4 5 6 7 8 9]
array2:
[[0 1 2 3 4]
[5 6 7 8 9]]
array3:
[[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
reshape()는 지정된 사이즈로 변경이 불가능하면 오류를 발생한다.
array1.reshape(4,3) #(10,)데이터 -> (4,3) reshape 불가능하다.
[output]
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-12-4e0cc8f56dc7> in <module>
----> 1 array1.reshape(4,3) #(10,)데이터 -> (4,3) reshape 불가능하다.
ValueError: cannot reshape array of size 10 into shape (4,3)
reshape()를 실전에서 더욱 효율적으로 사용할 때는 인자로 -1을 적용하는 경우이다.
-1을 인자로 사용하면 원래 ndarray와 호환되는 새로운 shape로 변환해준다.
array1 = np.arange(10)
print(array1) #[0 1 2 3 4 5 6 7 8 9]
array2 = array1.reshape(-1,5) #고정된 5개의 칼럼에 맞는 로우를 자동으로 새롭게 생성하여 변환, 1차원에서 2차원으로 변환
print('array2 shape:', array2.shape) #(2,5) 10개의 1차원 데이터와 호환될 수 있는 고정된 5개의 칼럼에 맞는 로우의 개수는 2
array3 = array1.reshape(5,-1) #고정된 5개의 로우에 맞는 칼럼은 2
print('array3 shape:', array3.shape) #(5,2)
[output]
[0 1 2 3 4 5 6 7 8 9]
array2 shape: (2, 5)
array3 shape: (5, 2)
물론 -1을 사용하더라도 호환될 수 없는 형태는 변환될 수 없다.
array1 = np.arange(10)
array4 = array1.reshape(-1,4) #고정된 4개의 칼럼에 맞는 로우의 개수는 없음
[output]
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-14-6ecf8311a948> in <module>
1 array1 = np.arange(10)
----> 2 array4 = array1.reshape(-1,4) #고정된 4개의 칼럼에 맞는 로우의 개수는 없음
ValueError: cannot reshape array of size 10 into shape (4)
-1인자는 reshape(-1,1)로 자주 쓰인다.
이는 원본 ndarrray가 어떤 형태라도 2차원이고, 여러개의 로우를 가지되 반드시 1개의 칼럼을 가진 ndarray로 변환됨을 보장한다.
array1 = np.arange(8)
array3d = array1.reshape((2,2,2))
print('array3d:\n', array3d.tolist()) #tolist(): ndarray를 list자료형으로 변환, 시각적으로 이해하기 더 쉬워짐
#3차원 ndarray를 2차원 ndarray로 변환
array5 = array3d.reshape(-1,1)
print('array5:\n', array5.tolist())
print('array5 shape:', array5.shape)
#1차원 ndarray를 2차원 ndarray로 변환
array6 = array1.reshape(-1,1)
print('array6:\n', array6.tolist())
print('array6 shape:', array6.shape)
[output]
array3d:
[[[0, 1], [2, 3]], [[4, 5], [6, 7]]]
array5:
[[0], [1], [2], [3], [4], [5], [6], [7]]
array5 shape: (8, 1)
array6:
[[0], [1], [2], [3], [4], [5], [6], [7]]
array6 shape: (8, 1)
[넘파이의 ndarray의 데이터 세트 선택하기 - 인덱싱(Indexing)]
1. 단일 값 추출
1개의 데이터값을 선택하려면 ndarray 객체에 해당하는 위치의 인덱스 값을 []안에 입력하면 된다.
#1부터 9까지 1차원 ndarray 생성
array1 = np.arange(start=1, stop=10)
print('arrray1:', array1)
#index는 0부터 시작하므로 array1[2]는 3번째 index 위치의 값을 의미
value = array1[2]
print('value:', value)
print(type(value)) #array1[2]타입은 ndarray 내의 데이터값을 의미
[output]
arrray1: [1 2 3 4 5 6 7 8 9]
value: 3
<class 'numpy.int32'>
# 인덱스에 마이너스 기호를 이용하면 맨 뒤에서 부터 데이터를 추출할 수 있다.
print('맨 뒤의 값:', array1[-1], '맨 뒤에서 두 번째 값:', array1[-2])
[output]
맨 뒤의 값: 9 맨 뒤에서 두 번째 값: 8
# 단일 인덱스를 이용하여 ndarray내의 데이터값도 간단히 수정 가능하다.
array1[0] = 9
array1[8] = 0
print('array1:', array1)
[output]
array1: [9 2 3 4 5 6 7 8 0]
다차원 ndarray에서의 단일 값을 추출해보자
array1d = np.arange(start = 1, stop = 10)
array2d = array1d.reshape(3,3) # 1차원 -> 2차원으로의 변환
print(array2d)
print('(row=0, col=0) index 가리키는 값:', array2d[0,0])
print('(row=0, col=1) index 가리키는 값:', array2d[0,1])
print('(row=1, col=0) index 가리키는 값:', array2d[1,0])
print('(row=2, col=2) index 가리키는 값:', array2d[2,2])
[output]
[[1 2 3]
[4 5 6]
[7 8 9]]
(row=0, col=0) index 가리키는 값: 1
(row=0, col=1) index 가리키는 값: 2
(row=1, col=0) index 가리키는 값: 4
(row=2, col=2) index 가리키는 값: 9
ndarray에서 주목해야 할 부분은 axis 0과 axis 1이다.
로우와 칼럼은 넘파이 ndarray에서는 사용되지 않는다.
즉, axis 0은 로우 방향의 축을 의미하고 axis 1은 칼럼 방향의 축을 의미한다.
그러므로 실제로 [row=0, col=1] 인덱싱은 [axis 0=0, axis 1= 1] 이 정확한 표현이다.
3차원 ndarray의 경우에는 axis0, axis 1, axis 2로 3개의 축을 가지게 된다. (행, 열, 높이)
축 기반의 연산에서 axis가 생략되면 axis 0을 의미한다.
2. 슬라이싱
':' 기호를 사용하여 연속한 데이터를 슬라이싱하여 추출할 수 있다.
':' 사이에 시작 인덱스와 종료 인덱스를 표시하면 시작 인덱스에서 종료 인덱스-1 위치에 있는 데이터의 ndarray를 반환한다.
# 슬라이싱
array1 = np.arange(start=1, stop =10)
array3 = array1[0:3]
print(array3) #[1,2,3]
print(type(array3)) #numpy.ndarray
#[output]
[1 2 3]
<class 'numpy.ndarray'>
슬라이싱 기호인 ':' 사이의 시작, 종료 인덱스는 생략 가능
# ':'기호 앞에 시작 인덱스를 생략하면 자동으로 맨 처음 인덱스인 0으로 간주
array1 = np.arange(start=1, stop=10)
array4 = array1[:3]
print(array4) #[1 2 3]
# ':' 기호 뒤에 종료 인덱스를 생략하면 자동으로 맨 마지막 인덱스로 간주
array5 = array1[3:]
print(array5) #[4 5 6 7 8 9]
#':' 기호 앞/뒤에 시작/종료 인덱스를 생략하면 자동으로 맨 처음/맨 마지막 인덱스로 간주한다.
array6 = array1[:]
print(array6) #[1 2 3 4 5 6 7 8 9]
#[output]
[1 2 3]
[4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]
2차원 ndarray에서의 슬라이싱도 1차원과 유사하지만 콤마(,)로 로우와 칼럼 인덱스를 지칭하는 부분만 다르다.
array1d = np.arange(start =1, stop =10)
array2d = array1d.reshape(3,3) #1차원 -> 2차원 3x3행렬
print('array2d:\n', array2d)
print('array2d[0:2, 0:2]\n', array2d[0:2, 0:2])
print('array2d[1:3, 0:3]\n', array2d[1:3, 0:3])
print('array2d[1:3, :]\n', array2d[1:3, :])
print('array2d[:, :]\n', array2d[:, :])
print('array2d[:2, 1:]\n', array2d[:2, 1:])
#로우나 칼럼 축 한쪽에만 슬라이싱을 적용하고, 다른 쪽 축에는 단일 값 인덱스를 적용해도 된다.
print('array2d[:2, 0]\n', array2d[:2, 0])
#[output]
array2d:
[[1 2 3]
[4 5 6]
[7 8 9]]
array2d[0:2, 0:2]
[[1 2]
[4 5]]
array2d[1:3, 0:3]
[[4 5 6]
[7 8 9]]
array2d[1:3, :]
[[4 5 6]
[7 8 9]]
array2d[:, :]
[[1 2 3]
[4 5 6]
[7 8 9]]
array2d[:2, 1:]
[[2 3]
[5 6]]
array2d[:2, 0]
[1 4]
# 2차원 ndarray에서 뒤에오는 인덱스를 없애면 1차원 ndarray를 반환
print(array2d[0])
print(array2d[1])
print('array2d[0] shape:', array2d[0].shape, 'array2d[1] shape:', array2d[1].shape)
#[output]
[1 2 3]
[4 5 6]
array2d[0] shape: (3,) array2d[1] shape: (3,)
3. 팬시 인덱싱
리스트나 ndarry로 인덱스 집합을 지정하면 해당 위치의 인덱스에 해당하는 ndarray를 반환하는 인덱싱 방식
array1d = np.arange(start=1, stop =10)
array2d = array1d.reshape(3,3)
array3 = array2d[[0,1],2] #로우에 팬시 인덱싱인 [0,1], 칼럼축에는 단일 값 인덱싱 2를 적용-> 인덱스 (0,2)(1,2)적용됨
print('array2d[[0,1],2] =>', array3. tolist())
array4 = array2d[[0,1], 0:2] #((0,0),(0,1)), ((1,0)(1,1)) 인덱싱 적용
print('array2d[[0,1],0:2] =>', array4.tolist())
array5 = array2d[[0,1]] #((0,:),(1,:)) 인덱싱적용
print('array[[0,1]] =>', array5.tolist())
#[output]
array2d[[0,1],2] => [3, 6]
array2d[[0,1],0:2] => [[1, 2], [4, 5]]
array[[0,1]] => [[1, 2, 3], [4, 5, 6]]
4.불린 인덱싱
조건 필터링과 검색을 동시에 할 수 있기 때문에 자주 사용되는 인덱싱 방식 -> for loop/if else문보다 훨씬 간단하게 구현 가능
array1d = np.arange(start=1, stop=10)
#[]안에 array1d>5 Boolean indexing을 적용
array3 = array1d[array1d>5]
print('array1d >5 불린 인덱싱 결과 값:', array3)
#[output]
array1d >5 불린 인덱싱 결과 값: [6 7 8 9]
어떻게 조건 필터링이 간단하게 될 수 있는지 알아보면,
array1d > 5
#[output]
array([False, False, False, False, False, True, True, True, True])
False가 있는 0~4인덱스는 무시하고 인덱스 [5,6,7,8] 이 만들어지게 되어 인덱스에 해당하는 데이터 세트 [6 7 8 9] 를 반환하게 된다.
위와 동일한 불린 ndarray를 만들고 이를 array1d[]내에 인덱스로 입력하면 동일한 데이터 세트가 반환됨을 알 수 있다.
boolean_indexes = np.array([False, False, False, False, False, True, True, True, True])
array3 = array1d[boolean_indexes]
print('불린 인덱스로 필터링 결과:', array3)
#[output]
불린 인덱스로 필터링 결과: [6 7 8 9]
즉, 다음과 같이 인덱스 집합을 만들어 대입한 것과 동일하다.
indexes = np.array([5,6,7,8])
array4 = array1d[indexes]
print('일반 인덱스로 필터링 결과:', array4)
#[output]
일반 인덱스로 필터링 결과: [6 7 8 9]
불린 인덱싱이 동작하는 단계를 순차적으로 도식화 해보자면,
- Step 1 : array1d> 5와 같이 ndarray의 필터링 조건을 []안에 기재
- Step 2 : False 값은 무시하고 True 값에 해당하는 인덱스값만 저장 (*주의: True 값 자체인 1을 저장하는 것이 아니라 True값을 가진 인덱스를 저장하는 것이다)
- Step 3 : 저장된 인덱스 데이터 세트로 ndarray 조회
[행렬의 정럴 - sort()와 argsort()]
1. 행렬 정렬
넘파이의 행렬 정렬은 (1) np.sort()와 같이 넘파이에서 sort()를 호출하는 방식과 (2)ndarray.sort()와 같이 행렬 자체에서 sort()를 호줄하는 방식이 있다.
두 방식의 차이는
np.sort(): 원 행렬은 그대로 유지한 채 원 행렬의 정렬된 행렬을 반환
ndarray.sort(): 원 행렬 자체를 정렬한 형태로 변환하며 반환 값은 None
org_array = np.array([3,1,9,5])
print('원본행렬:', org_array)
#np.sort()로 정렬
sort_array1 = np.sort(org_array)
print('np.sort()호출 후 반환된 정렬 행렬:', sort_array1)
print('np.sort()호출 후 원본 행렬:', org_array) #원본 행렬은 그대로 유지
#ndarray.sort()로 정렬
sort_array2 = org_array.sort()
print('org_array.sort()호출 후 반횐된 행렬:', sort_array2)#None
print('org_array.sort()호출 후 원본행렬:', org_array) #원본행렬도 변환
#[output]
원본행렬: [3 1 9 5]
np.sort()호출 후 반환된 정렬 행렬: [1 3 5 9]
np.sort()호출 후 원본 행렬: [3 1 9 5]
org_array.sort()호출 후 반횐된 행렬: None
org_array.sort()호출 후 원본행렬: [1 3 5 9]
내림차순으로 정렬하기 위해서는 [::-1]을 적용
sort_array1_desc = np.sort(org_array)[::-1]
print('내림차순으로 정렬:', sort_array1_desc)
#[output]
내림차순으로 정렬: [9 5 3 1]
행렬이 2차원 이상일 경우에 axis 축 값 설정을 통해 로우, 칼럼 방향으로 정렬을 수행할 수 있다
array2d = np.array([[8,12],
[7,1]])
sort_array2d_axis0 = np.sort(array2d, axis=0)
print('로우 방향으로 정렬:\n', sort_array2d_axis0)
sort_array2d_axis1 = np.sort(array2d, axis=1)
print('칼럼 방향으로 정렬:\n', sort_array2d_axis1)
#[output]
로우 방향으로 정렬:
[[ 7 1]
[ 8 12]]
칼럼 방향으로 정렬:
[[ 8 12]
[ 1 7]]
2. 정렬된 행렬의 인덱스를 반환하기
np.argsort() : 정렬행렬의 원본행렬 인덱스를 ndarray형으로 반환
org_array = np.array([3,1,9,5])
sort_indices = np.argsort(org_array) #[1 3 9 5]의 원본행렬의 변환된 인덱스를 반환
print(type(sort_indices)) #ndarray 형
print('행렬 정렬 시 원본 행렬의 인덱스:', sort_indices)
#[output]
<class 'numpy.ndarray'>
행렬 정렬 시 원본 행렬의 인덱스: [1 0 3 2]
내림차순 [::-1]이용
#내림차순
org_array = np.array([3,1,9,5])
sort_indices_desc = np.argsort(org_array)[::-1]
print('행렬 내림차순 정렬 시 원본 행렬의 인덱스:', sort_indices_desc)
#[output]
행렬 내림차순 정렬 시 원본 행렬의 인덱스: [2 3 0 1]
ndarray는 실제 값과 그 값이 뜻하는 메타 데이터를 별도의 ndarray로 가져야 한다.
예를 들어, 학생별 시험 성적 데이터를 시험 성적 순으로 학생 이름을 출력하는 경우를 보자
import numpy as np
name_array = np.array(['John', 'Mike', 'Sarah', 'Kate', 'Samuel'])
score_array = np.array([78, 95, 84, 98, 88])
sort_indices_asc = np.argsort(score_array) #argsort()로 인덱스 추출
print('성적 오름차순 정렬시 score_array의 인덱스:', sort_indices_asc) # 점수에 대한 원본행렬의 변환된 인덱스를 반환
print('성적 오름차순으로 name_array의 이름 출력:', name_array[sort_indices_asc]) # 팬시인덱스로 적용
#[output]
성적 오름차순 정렬시 score_array의 인덱스: [0 2 4 1 3]
성적 오름차순으로 name_array의 이름 출력: ['John' 'Sarah' 'Samuel' 'Mike' 'Kate']
이러한 방식은 넘파이의 데이터 추출에서 많이 사용된다.
[선형대수의 연산 - 행렬 내적과 전치 행렬 구하기]
1. 행렬 내적(행렬 곱)
np.dot() : 두 행렬의 내적을 구함
행렬 내적의 특성으로 한 행렬의 열 개수와 다른 행렬의 행 개수가 동일해야 내적 연산이 가능하다.
A = np.array([[1,2,3],
[4,5,6]])
B = np.array([[7,8],
[9,10],
[11,12]])
dot_product = np.dot(A,B)
print('행렬 내적 결과:\n', dot_product)
#[output]
행렬 내적 결과:
[[ 58 64]
[139 154]]
2. 전치 행렬
np.transpose() : 전치 행렬을 구하는 매서드
#전치행렬
A = np.array([[1,2],
[3,4]])
transpose_mat = np.transpose(A)
print('A의 전치행렬:\n', transpose_mat)
#[output]
A의 전치행렬:
[[1 3]
[2 4]]
Ref) 파이썬 머신러닝 완벽가이드