본문 바로가기

Python/Data Analysis

Data Analysis / NumPy / ndarray(2)

< reshape() 함수와 resize() 함수 >

# reshape()와 resize()

import numpy as np

# ndarray의 shape 조절 함수
# reshape() : 원하는 형태(shape)로 ndarray의 shape를 조절
# ravel() : 1차원 vector(1차원 ndarray)로 shape를 변형

# 정수형태의 난수를 이용해서 (3,4) shape을 가지는 ndarray 생성
np.random.seed(10)
arr = np.random.randint(0, 10, (3,4))

# result = arr.resize(2,6) # error : view가 생성되는 것이 아니라 원본이 변함

# reshape() 함수는 요소의 개수가 맞지 않으면 reshape가 되지 않음
# resize() 함수는 요소의 개수가 맞지 않아도 shape 변경 가능
# 요소수가 줄어들면 기존 데이터를 버리고, 늘어나면 0으로 설정
arr.resize(3,5) # 요소수가 늘어남
print(arr)
arr.resize(2,2)	# 요소수가 줄어듬
print(arr)

arr.resize(2,2)
arr.resize(3,5)

< indexing과 slicing >

# indexing & slicing

import numpy as np

arr = np.arange(10, 20, 1)

# ndarray의 각 요소를 출력하려면?
# for 문을 사용하는 방법
for tmp in arr:
    print(tmp)
    
# enumerate를 사용하면 (index, value)의 tuple 형태로 가져옴
for (idx, tmp) in enumberate(arr):
    print('인덱스 : {}, 데이터 : {}'.format(idx,tmp))
    
# indexing & slicing
print(arr[3])	# 13
print(arr[1:4])	# [11 12 13]
print(arr[:-1])	# [10 11 12 ... 18]
print(arr[1:-1:2)	# [11 13 15 17]

# 2차원 배열 indexing & slicing
arr = np.arange(1,17,1).reshape(4,4).copy()

print(arr[1,2])	# 7
print(arr[1])	# [5 6 7 8]
print(arr[1][2])	# 7
print(arr[2,:])	# [9 10 11 12]
print(arr[1:3,:])	
# [[ 5  6  7  8]
# [ 9 10 11 12]]
print(arr[1:3,:2])	
# [[ 5  6]
# [ 9 10]]

< Boolean indexing & Fancy indexing >

# Boolean indexing
# boolean indexing은 ndarray 각 요소의 선택여부를 나타냄
# True, False로 구성된 boolean mask를 이용하여 지정
# boolean mask의 True에 해당하는 index 만 조회하는 방식

import num as np

np.random.seed(1)
arr = np.random.randint(0, 10, (5,))
print(arr)	# [5 8 9 5 0]
print(arr % 2)	# [1 0 1 1 0]
print(arr % 2 == 0)	# [False True False False True] => boolean mask
# boolean indexing : boolean mask의 값이 True인 항목들만 저장
print(arr[arr % 2 == 0])	# [8 0] => boolean indexing

# Fancy indexing
# ndarray에 index 배열을 전달하여 배열요소를 참조하는 방식

arr = np.arange(0,12,1).reshape(3,4).copy()
#[[ 0  1  2  3]
# [ 4  5  6  7]
# [ 8  9 10 11]]
print(arr[2,2])	# indexing : 10
print(arr[1:2,2])	# slicing : [6] => slicing 이므로 배열로 나옴에 유의
print(arr[1:2,1:2])	# slicing : [[5]]
# index 배열을 이용한 indexing
# 배열의 값에 해당하는 항목한 추출
print(arr[[0,2],2]	# [2 10]
print(arr[[0,2],2:3] => slicing이 있으므로 2차원 배열이 return 됨
# [[ 2]
# [10]]

# 예제 : 다음의 2차원 ndarray를 추출해보자
# [[1 3]
# [9 11]]
# Fancy indexing을 배운대로 사용하면...
# print(arr[[0,2],[1,3]]) 으로 작성할텐데 결과는 [1 11]로 나옴
# 행과열에 동시에 Fancy indexing이 적용되지 않기 때문!!!

# 해결방법 1 - fancy indexing 두 번 적용
print(arr[[0,2]][:,[1,3]])

# 해결방법 2 - NumPy가 제공하는 'ix_' 라는 이름의 함수 이용
print(arr[np.ix_([0,2],[1,3])])

< ndarray의 사칙연산과 행렬곱 >

# ndarray의 사칙연산과 행렬곱
# +, -, *, /
# 행렬곱 => dot product

import numpy as np

arr1 = np.array([[1,2,3], [4,5,6]])	# 2 x 3 ndarray
arr2 = np.array([[7,8,9], [10,11,12]])	# 2 x 3 ndarray

# python의 list에서 + 연산자는 concatenation 수행
# ndarray에서 + 연산자는 vector, matrix 연산
# ndarray의 사칙연산 기본 전제는 shape이 같아야 연산 성립
# print(arr1 + arr2)	# 각각을 서로 더함
# [[ 8 10 12]
# [14 16 18]]

# 만약 연산하는 항목이 shape가 안맞는 경우, ndarray가 broadcasting을 수행
arr2 = 3

# arr1이 2 x 3 이므로 arr2도 이에 맞게 broadcasting
# [[3 3 3]
# [3 3 3]]
print(arr1 + arr2)
# [[4 5 6]
# [7 8 9]]

arr2 = np.array([[1,2,3])

# arr2를 broadcasting
# [[1 2 3]
# [1 2 3]]
print(arr1 + arr2)
# [[2 4 6]
# [5 7 9]]

arr2 = np.array([1,2])
# broadcasting은 해당 값을 복사하는 형태로 수행됨
# ex) [1 2 1 2 1 2 ...]
# 그러므로 위의 경우에는 broadcasting이 불가능
# print(arr1 + arr2)	# error : 2 x 3 형태로 broadcasting 불가

## 행렬곱연산
# 두 행렬간의 행렬곱은 np.dot(), np.matmul()로 수행 가능
# np.dot(A,B)에서 A행렬의 열 vector와 B행렬의 행 vector의 size가 같아야 한다.
# broadcasting은 사칙연산에만 적용되고 dot product에서는 일어나지 않는다!!!
# 만약 크기가 다르다면, reshape(), resize() 등을 이용해서 크기를 맞추어야 한다.

arr1 = np.array([[1,2,3], [4,5,6]])	# 2 x 3 ndarray
arr2 = np.array([[7,8], [9,10], [11,12]])	# 3 x 2 ndarray

print(arr1.shape)	# (2, 3)
print(arr2.shape)	# (3, 2)
print(np.dot(arr1, arr2))
# [[ 58  64]
# [139 154]]

# 행렬곱 연산을 알아야 하는 이유는?
# 만약 행렬곱 연산이 없다면, matrix 연산은 같은 크기로만 연산이 수행된다.
# 행렬곱 연산을 이용해서 행렬곱 조건만 만족시키면 다양한 크기의 행렬을
# 연속적으로 이용해서 특정 작업을 수행할 수 있다.
# ML, 이미지처리 분야에서 사용
# ex) 입력 : 32 x 32 matrix (이미지파일)
# 출력 : 32 x 10 matrix (다양한 처리가 적용된 이미지)
# 행렬곱 : (32 x 32) dot (32 x 128) dot (128 x 64) dot (64 x 10) = (32 x 10)
# 이와 같이 필터 처리를 여러번 수행해서 원하는 이미지로 만들 수 있다.

< Transpose(전치행렬) >

# Transpose
# 일반적으로 전치행렬이라고 불린다.
# 원본행렬의 행은 열로, 열은 행으로 바꾼 행렬
# 전치행렬의 표현은 윗첨자로 T를 이용

import numpy as np

arr = np.array([[1,2,3], [4,5,6]])	# 2 x 3 ndarray

# 전치행렬을 속성을 이용해서 간단하게 구할 수 있다.
t_array = arr.T	# 원본 데이터를 공유하는 구조
print(t_array)
# [[1 4]
# [2 5]
# [3 6]]

arr[0][0] = 100
print(t_array)	# view : 원본 데이터를 공유하고 있기 때문에 값이 변경됨

arr = np.array([1,2,3,4])	# vector (1차원 ndarray)
t_array = arr.T	# 1차원 vector를 transpose 해도 그대로이므로 의미가 없음

arr = np.array([[1,2,3,4]])	# 2차원 ndarray
t_array = arr.T
print(t_array)
# [[1]
#  [2]
#  [3]
#  [4]]

< NumPy iterator >

# NumPy iterator
# 목표 : iterator를 왜 써야 하는지 알고 동작방식을 익힌다.

import numpy as np

# 1차원 ndarray를 만들어서 각 요소를 출력해보자
arr = np.array([1,2,3,4,5])

# for 문을 이용하여 각 요소에 접근하는 방법
for tmp in arr:
    print(tmp, end=' ')
    
# iterator로 각 요소에 접근하는 방법
# 1차원 배열의 경우 flags는 c_index로 설정
it = np.nditer(arr, flags=['c_index'])

while not it.finished:	# iterator가 지정하는 위치가 끝이 아닐때 까지 반복
    idx = it.index	# iterator가 현재 가리키는 곳의 index 숫자를 가져옴
    print(arr[idx], end=' ')
    it.iternext()	# 다음 요소로 iterator를 이동

## 2차원 ndarray를 출력해보자
# for문을 사용하여 각 요소에 접근하는 방법
for row in range(arr.shape[0]):	# shape가 tuple이므로 indexing
    for col in range(arr.shape[1]):
        print(arr[row, col], end=' ')
        
# 만약 3차원 이면? => 3중 for문이 구성되어야 함
# 차원이 높아질수록 코드처리가 힘들어진다!!

# 다차원의 경우 flags 값은 multi_index
it = np.nditer(arr.flags=['multi_index']

while not it.finished:
    idx = it.multi_index	# index를 tuple 형태로 가져옴( ex. (0, 2) )
    print(arr[idx], end=' ')
    it.iternext()

< ndarray의 비교연산 >

# ndarray의 비교연산
# 사칙연산과 마찬가지로 비교연산도 같은 index 끼리 수행
np.random.seed(0)

arr1 = np.random.randint(0,10,(2,3))
arr2 = np.random.randint(0,10,(2,3))
print(arr1)
# [[5 0 3]
# [3 7 9]]
print(arr2)
# [[3 5 2]
# [4 7 6]]
print(arr1 == arr2)	# boolean mask
# [[False False False]
# [False  True False]]

# 만약 2개의 ndarray가 같은 데이터를 가지고 있는지 비교할 때는 어떻게?
arr1 = np.arange(10)
arr2 = np.arange(10)
# array_equal() 이라는 함수를 이용한다.
print(np.array_equal(arr1, arr2))	# True

< NumPy 집계함수 및 axis(축) >

# NumPy 집계함수 및 axis(축)

import numpy as np

arr = np.arange(1,7,1).reshape(2,3).copy()
# [[1 2 3]
# [4 5 6]]

# for문을 이용한 누적 집계
result = 0
for row in range(arr.shape[0]):
    for col in range(ar.shape[1]):
        result += arr[row, col]

# numpy array에 대해서 sum() 함수를 호출하면 모든 항목 누적 가능
print(arr.sum())
# 또는 아래의 방법도 사용 가능
print(np.sum(arr))

# 누적합을 구해서 1차원 벡터로 출력 - cumsum()
print(np.cumsum(arr))	# [1 3 6 10 15 21]
# 평균 - mean()
print(np.mean(arr))	# 3.5
# 최대값 - max()
print(np.max(arr))	# 6
# 최소값 - min()
print(np.min(arr))	# 1
# 최대값의 index()
print(np.argmax(arr))	# 5 (tuple 형태로 (1, 2)로 출력되는 것이 아님에 유의!!
# 최소값의 index()
print(np.argmin(arr))	# 0
# 표준편차 - std()
print(np.std(arr))	# 1.707
# 자연상수 - exp()
print(np.exp(arr))	# arr[index] 거듭제곱 한 것들을 배열에 저장

## Numpy의 모든 집계함수는 axs를 기준으로 계산됨
# 만약 axis를 지정하지 않으면, axis는 None으로 설정됨
# None인 경우 함수의 대상범위를 전체 dnarray로 지정함

# axis의 설정방법과 효과는?
arr = np.array([1,2,3,4,5])
# 1차원은 축이 1개, 2차원은 축이 2개, 3차원은 축이 3개...
# 축(axis)은 숫자로 표현
# 1차원인 경우, axis=0 열방향(가로방향) 밖에 사용 못함
print(arr.sum(axis=0))	# 15
print(arr.sum(axis=1))	# error : axis가 1이라는 것은 적어도 배열이 2차원 이상임을 의미

# 2차원 ndarray로 axis를 조절해보자
arr = np.array([[1,2,3], [4,5,6]]	# 2 x 3 ndarray
print(arr.sum())	# 21
# 2차원에서 axis=0은 행방향(세로방향)을 의미
print(arr, sum(axis=0))	# [5 7 9]
# 2차원에서 axis=1은 열방향(가로방향)을 의미
print(arr, sum(axis=1))	# [6 15]
print(arr.argmax(axis=1))	# [2 2]

# 3차원 ndarray 라면?
# axis = 0 => depth 방향
# axis = 1 => 행방향(세로방향)
# axis = 2 => 열방향(가로방향)

< NumPy 집계함수 vs for 구문 >

1. for 구문

%%time
# 맨위에 %%time을 적어주면, 현재 셀을 수행하는데 걸리는 시간을 보여준다.
# 단, 다른 내용(주석포함)을 아무것도 그 줄에 적으면 안된다.

arr = np.arange(100000, dtype=np.float64)
result = 0

for tmp in arr:
    result += tmp
    
print(result)	# 4999950000.0 , Wall time: 43 ms 

2. 집계함수

%%time

arr = np.arange(100000, dtype=np.float64)
print(arr.sum())	# 4999950000.0, Wall time: 1e+03 µs

- 결론 : 엄청난 시간차이가 발생하는 것을 확인할 수 있다.

- 나중에 ML 학습할 때, 이런 연산시간을 줄여두지 않으면 학습이 매우 오래 걸릴 수 있다.

## 연습문제

# ndarray 안에 10보다 큰 수가 몇개 있는지 알려면?

import numpy as np

arr = np.arange(1,17).reshape(4,4).copy()

# Boolean mask를 이용하며, True = 1, False = 0 임을 이용한다.
print((arr > 10).sum())

< ndarray의 정렬 >

# ndarray의 정렬

# NumPy array는 axis를 기준으로 정렬하는 sort() 함수를 제공
# 만약 axis를 지정하지 않으면, -1값으로 지정 => -1은 마지막 axis(열방향)을 의미
# np.sort(arr) : 원본은 변하지 않고, 정렬된 결과 ndarray를 return
# arr.sort() : 원본을 정렬, return 값은 None

import numpy as np

arr = np.arange(10)
np.random.shuffle(arr)
print(arr)	# [7 6 4 3 2 9 8 1 5 0]

# sort() 함수의 default는 오름차순 정렬
print(np.sort(arr))	# [0 1 2 3 4 5 6 7 8 9]
# slicing의 3번째 인자로 -1을 주면 역순으로 slicing을 수행
print(np.sort(arr)[::-1])	# [9 8 7 6 5 4 3 2 1 0]

# 2차원 ndarray의 정렬
arr = np.arange(1,17)
np.random.shuffle(arr)
arr = arr.reshape(4,4).copy()

print(arr)
# [[16  2 15 12]
# [10  5  1  7]
# [ 6  3  8  4]
# [11 13  9 14]]
print(np.sort(arr, axis=1))	# 2차원에서 axis=1이면 열방향(가로방향)
# [[ 2 12 15 16]
# [ 1  5  7 10]
# [ 3  4  6  8]
# [ 9 11 13 14]]

## 연습문제
# 표준정규분포에서 200개의 샘플을 추출한 후
# 내림차순으로 상위 5% 까지의 결과만 출력
arr = np.random.randn(200)
arr = np.sort(arr)[::-1]
end = int(200 * 0.05)
print(arr[0:end])