데이터 분석 목적
이산값(Discrete values)
중간에 소수점값이 존재할 수 없는 딱딱 끊어지는 숫자 (중간값이 존재하지 않음)
연속값 (continuous values)
어떤 구간 내에서 어떤 값이라도 가질 수 있는 숫자. 숫자와 숫자 사이에 무한한 세분화 가능 (중간값 존재)
-측정할때 사용 (measurable)
-정확한 도구가 있다면 소수점 아래로 무한히 정밀해질수 있음
-그래프로 그리면 끊기지 않는 선의 형태
-실수형태
e.g)
키와 몸무게 (162.5cm, 53kg..)
현재 온도
시간
얼마나 큰가/긴가 (how much)
시각화 -> 히스토그램, 선 그래프 (line chart)
중심경향측도(Central Tendency): 데이터의 대표적인 위치
어떤 데이터 집합 전체를 대표할 수 있는 '중심적인 위치'가 어디인지를 나타내는 통계적 수치. 쉽게 말해 "데이터들이 보통 어느 값 근처에 모여 있는가?"를 보여주는 지표
arr = np.array([10, 20, 30, 40])
print(np.mean(arr)) # 25.0
print(np.median(arr)) # 25.0
from scipy import stats
arr = np.array([10, 20, 20, 30, 40])
print(stats.mode(arr)) # Mode: 20
산포 측도(Measures of Dispersion):
데이터가 얼마나 넓게 퍼져 있는가 or 얼마나 중심에 밀집해 있는가를 나타내는 지표
print(np.max(arr) - np.min(arr)) # 30
# 범위 (range)
np.max(height) - np.min(height)
print(np.var(arr)) # 125.0
# 분산 (Variance)
# (값 - 평균)^2의 평균
var1 = np.mean(np.square(height - np.mean(height))) # ndarray - broadcasting 값
print(var1)
var2 = np.var(height)
print(var2)
print(np.std(arr)) # 11.1803
std1 = np.sqrt(var1)
print(std1)
std2 = np.std(height)
print(std2)
확률(probability) "얼마나 일어날까?"
:확률은 특정 사건이 일어날 가능성을 0(절대 안 일어남)에서 1(무조건 일어남) 사이의 숫자로 나타낸 것. 확률이 1에 가까울수록 해당 사건이 발생할 가능성이 크다.
확률분포(probability distribution) "전체적인 모양"
: 확률변수(Random Variable)가 어떤 값을 가질 확률이 어느정도인지 표나 그래프로 나타낸 지도. 데이터가 어디에 많이 모여있고 어디에 적게 퍼져있는지를 보여줌.
정규분포(Normal distribution)
: 종모양 (bell-curve) 의 분포.
모양: 가운데(평균)가 가장 높고 양옆으로 갈수록 낮아짐
관련 스케일러 - StandardScaler (데이터를 평균 0, 표준편차 1인 정규분포로 변환)
arr = np.random.randn(1000) # 평균 0, 표준편차 1인 정규분포 난수 1000개
print(np.mean(arr), np.std(arr))
# 정규분포
# np.random.randn(shape): (평균이 0, 표준편차가 1인) 표준 정규 분포를 따르는 난수를 shape에 맞게 생성
std_arr = np.random.randn(1000)
plt.hist(std_arr, bins=100)
plt.show()
# 정규분포
# np.random.normal(loc, scale, size): 평균이 loc, 표준편차가 scale인 정규 분포를 따르는 난수를 size에 맞게 생성
# - loc: 평균
# - scale: 표준편차
# - size: shape (갯수)
nor_arr = np.random.normal(10, 1, 1000)
plt.hist(nor_arr, bins=10)
plt.show()
표준정규분포(standard normal distribution)
:모든 정규분포를 서로 비교하거나 확률을 쉽게 계산하기 위해 평균을 0으로, 표준편차를 1로 고정시킨 것
표준정규분포의 확률 (68-95-99.7 법칙)
*[ 68-95-99.7 규칙 ]
정규 분포를 나타내는 규칙으로, 경험적인 규칙(empirical rule)이라고도 한다.
3시그마 규칙(three-sigma rule)이라고도 하는데 이 때는 평균에서 양쪽으로 3표준편차의 범위에 거의 모든 값들(99.7%)이 들어간다는 것을 나타낸다.
균등분포(uniform distribution)
: 모든 값이 동일한 확률로 발생하는 분포.
모양: 직사각형 모양 (평평함)
처음부터 꿑까지 높이가 일정함. 특정 값이 더 많이 나오지 않고 모든 구간에서 골고루 발생.
관련 스케일러 - MinMaxScaler (데이터를 0과 1 사이로 쫙 펼쳐서 균등하게 만듦)
arr = np.random.uniform(0, 1, 1000) # 0과 1 사이의 균등분포 난수 1000개
print(np.mean(arr), np.std(arr))
# 균등분포
# np.random.rand(shape): 0.0 이상 1.0 미만 수에서 균등분포를 따르는 난수를 shape에 맞게 생성
arr = np.random.rand(1000)
plt.hist(arr, bins=10)
plt.show()
# 균등분포
# np.random.randint(start, end, size): start 이상 end 미만의 균등분포를 따르는 난수를 size에 맞게 생성
# - start: 시작값(포함)
# - end: 끝값(미만/포함X)
# - size: shape (갯수)
np.random.seed(0) # 난수 계산 시작값 seed 고정 (재연 가능)
arr = np.random.randint(1, 100, 1000)
plt.hist(arr, bins=10)
plt.show()
np.sort()함수를 사용하여 수행. 이 함수는 배열의 복사본을 생성하여 정렬된 배열을 반환하며, 기본적으로 오름차순으로 정렬됨.(1) 1차원 배열 정렬 - 배열의 원소를 오름차순으로 정렬
# 1차원 배열 정렬
arr = np.random.randint(1, 11, 10) # 1이상 10미만의 균등분포
arr
# np.sort(arr): 정렬된 ndarray를 "반환" (원본 ndarray는 영향을 받지 않음)
print(np.sort(arr)) # 오름차순
print(arr)
np.sort(arr)[::-1] # 내림차순
sorted_arr_desc = np.sort(arr)[::-1]
print(sorted_arr_desc) # [50, 40, 30, 20, 10]
(2) 2차원 배열 정렬 - 2차원 배열은 각 행(row) 또는 열(column)을 기준으로 정렬할 수 있다. 기본적으로 각 행을 기준으로 정렬이 수행된다.
# 2차원 배열 정렬
arr = np.random.randint(10, size=(3, 4)) # 0 이상 10 미만
arr
print(np.sort(arr)) # 같은 행 안에서 열방향으로 정렬됨
# 기본값 axis=1
(3) 특정 축(axis)을 기준으로 정렬 - 축을 기준으로 정렬할 수 있으며, 축 0은 열(column) 을 기준으로, 축 1은 행(row) 을 기준으로 정렬
print(np.sort(arr, axis=0)) # 행 방향 정렬 (같은 열 안에서 정렬)
print(np.sort(arr, axis=1)) # 열 방향 정렬 (같은 행 안에서 정렬)
# axis=None: 축 무시 (행/열 구분없이 모든 값 정렬)
print(np.sort(arr, axis=None))
print(np.sort(arr, axis=None).shape)
np.argsort()# 1차원 배열에서 argsort 사용
arr = np.array([100, 400, 200, 300])
print(np.sort(arr)) # [100 200 300 400]
print(np.argsort(arr)) # [0 2 3 1]
names = np.array(['리사', '로제', '제니', '지수'])
album_sales = np.array([900, 870, 1000, 670])
#argsort() -> 앨번판매량이 많은 순으로 멤버 이름을 출력
sales_desc = np.argsort(album_sales)[::-1]
print(names[sales_desc])
# 2차원 배열에서 argsort -> 특정 축을 기준으로 인덱스 반환
**어떤 순서로 값을 배치해야 정렬이 되는가?**
arr = np.array([[2026, 1, 21],
[14, 43, 12]])
print(np.argsort(arr)) # 기본값 axis=1
[[1 2 0]
[2 0 1]]
print(np.argsort(arr, axis=0)) # 열(세로) 방향으로 정렬 인덱스 추출
[[1 0 1]
[0 1 0]]
print(np.argsort(arr, axis=1)) # 행(가로) 방향으로 정렬 인덱스 추출
[[1 2 0]
[2 0 1]]
배열병합: 여러 개의 배열을 연결하여 하나의 배열로 만드는 방법
NumPy에서 배열을 병합하는 방법: np.concatenate(), np.vstack(), np.hstack() 등의 함수를 사용
(1)np.insert()
요소를 추가한 새로운 배열을 반환
# 1차원 배열에서 insert()
arr = np.array([10, 20, 30])
arr1 = np.insert(arr, 1, 15) # insert(원본배열, 삽입할 위치 인덱스, 삽입할 요소)
arr2 = np.append(arr, 100) # append(원본배열, 삽입할 요소) /
※ numpy에서 append()는 병합 함수이므로, 아래 병합에서 다시 볼 것!
print(arr1) # [10 15 20 30]
print(arr2) # [ 10 20 30 100]
# is 연산자: 두 객체(인스턴스)가 동일한지 비교
print(arr1 is arr) # False
print(arr2 is arr) # False
# 2차원 배열에서 insert()
arr_2d = np.arange(1, 13).reshape(3, 4) # 1 이상부터 13 미만
print(arr_2d)
print(np.insert(arr_2d, 1, 100)) # axis=None 축 무시 (1차원 배열로 변환 후 삽입)
print(np.insert(arr_2d, 1, 100).shape)
# print(np.insert(arr_2d, 1, 999, axis=0)) # axis=0: 행 기준
print(np.insert(arr_2d, 1, [999], axis=0))
# print(np.insert(arr_2d, 1, [777, 888], axis=0)) # 브로드캐스팅 연산이 가능한 스칼라값 또는 배열만 insert 가능
# print(np.insert(arr_2d, 1, [777, 888, 999], axis=0)) # 브로드캐스팅 연산이 가능한 스칼라값 또는 배열만 insert 가능
print(np.insert(arr_2d, 1, [777, 888, 999, 1010], axis=0))
# print(np.insert(arr_2d, 1, 999, axis=1)) # axis=1: 열 기준
print(np.insert(arr_2d, 1, [999], axis=1))
# print(np.insert(arr_2d, 1, [777, 888], axis=1)) # 브로드캐스팅 연산이 가능한 스칼라값 또는 배열만 insert 가능
print(np.insert(arr_2d, 1, [777, 888, 999], axis=1))
(2)np.delete()
요소를 삭제한 새로운 배열 반환
1차원 배열에서 delete()
arr = np.arange(0, 15)
arr_del = np.delete(arr, 7) # delete(원본배열, 삭제할 위치 인덱스)
print(arr_del)
print(arr_del is arr)
2차원 배열에서 delete()
arr_2d = np.arange(1, 13).reshape(3, 4)
print(arr_2d)
print(np.delete(arr_2d, 1)) # axis=None: 축 무시 (1차원 배열로 변환 후 삭제)
print(np.delete(arr_2d, 1, axis=0)) # axis=0: 행 기준
print(np.delete(arr_2d, 1, axis=1)) # axis=1: 열 기준
np.append()np.vstack() & np.hstack()np.concatenate() - 축 기준 배열 병합arr1 = np.arange(1, 13).reshape(3, 4)
arr2 = np.arange(101, 113).reshape(3, 4)
arr1, arr2
print(np.append(arr1, arr2)) # axis=None
print(np.append(arr1, arr2).shape)
print(np.append(arr1, arr2, axis=0))
print(np.append(arr1, arr2, axis=1))
print(np.vstack((arr1, arr2))) # axis=0 병합
print(np.hstack([arr1, arr2])) # axis=1 병합
# append(), hstack(), vstack()과의 공통점/차이점
# -> axis 설정 기본값
print(np.concatenate((arr1, arr2))) # axis=0 기본값
# print(np.concatenate([arr1, arr2]))
print(np.concatenate((arr1, arr2), axis=0))
print(np.concatenate((arr1, arr2), axis=1))
print(np.concatenate((arr1, arr2), axis=None))
# 1차원 배열 병합 - 두 개 이상의 1차원 배열을 연결할 수 있다.
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
merged_arr = np.concatenate((arr1, arr2))
print(merged_arr) # [1, 2, 3, 4, 5, 6]
arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[5, 6], [7, 8]])
# 2차원 배열 병합 (축 0) - 행을 기준으로 배열을 병합할 수 있다. 이 경우 배열들의 열 개수가 같아야 한다.
merged_arr = np.concatenate((arr1, arr2), axis=0)
print(merged_arr)
# [[1, 2],
# [3, 4],
# [5, 6],
# [7, 8]]
# 2차원 배열 병합 (축 1) - 열을 기준으로 배열을 병합할 수 있다. 이 경우 배열들의 행 개수가 같아야 한다.
merged_arr = np.concatenate((arr1, arr2), axis=1)
print(merged_arr)
# [[1, 2, 5, 6],
# [3, 4, 7, 8]]
# 새로운 축을 따라 배열 병합
```np.stack()```: 새로운 축을 따라 배열 병합 -> 기존 배열에 차원을 추가한 후 그 축을 따라 배열을 쌓을 수 있다.
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
# 1. 0축을 따라 배열 병합
stacked_arr = np.stack((arr1, arr2), axis=0)
print(stacked_arr)
# [[1, 2, 3],
# [4, 5, 6]]
# 2. 1축을 따라 배열 병합
stacked_arr = np.stack((arr1, arr2), axis=1)
print(stacked_arr)
# [[1, 4],
# [2, 5],
# [3, 6]]
np.split()
arr = np.concatenate((arr1, arr2))
print(arr.shape)
# split (원본배열, 분할할 개수, axis) / 기본값 axis=0
# np.split(arr, 2)
np.split(arr, 2, axis=0)
np.split(arr, 2, axis=1)
#※ 약수를 사용해서 분할할 개수를 정해야 함
# arr의 shape: (6, 4)
n = 2
print(np.split(arr, n, axis=0)) # 행 기준(방향) -> 6행: 1, 2, 3, 6
print(np.split(arr, n, axis=1)) # 열 기준(방향) -> 4열: 1, 2, 4
arr = np.arange(12) # 0 이상 12 미만
print(arr.shape) # (12,)
arr = arr.reshape(3, 4) # 3행 4열 (3 * 4 = 12))
arr = arr.reshape(6, 2) # 6행 2열 (6 * 2 = 12)
arr = arr.reshape(1, 12) # 1행 12열 (1 * 12 = 12)
# arr = arr.reshape(7, 1)
# arr = arr.reshape(7, 2)
print(arr.shape) # (1, 12)
arr = arr.reshape(3, 2, 2) # 3면 2행 2열 (3 * 2 * 2 = 12)
print(arr.shape) # (3, 2, 2)
print(arr)
# [[[ 0 1]
# [ 2 3]]
# [[ 4 5]
# [ 6 7]]
# [[ 8 9]
# [10 11]]]
# 여기까지 arr의 shape = (3, 2, 2)
# reshape(-1) == 1차원 처리 (모든 숫자를 직접 계산하기 귀찮을 때 -1을 쓰면 NumPy가 알아서 계산)
arr = arr.reshape(-1)
print(arr)
print(arr.shape)
# 여기까지 arr의 shape = (12,)
# shape의 요소 중 하나만 -1 == -1 인 축은 자동 계산
arr = arr.reshape(4, -1)
arr = arr.reshape(-1, 6)
print(arr)
print(arr.shape)
np.ravel()arr = np.arange(50).reshape(5, 5, 2) # -> 0부터 49까지 50개의 숫자를 생성하여 (5, 5, 2) 모양으로 재배열
print(arr) # 5개의 층(page)이 있고, 각 층은 5행2열인 구조
print(np.ravel(arr)) # 1차원으로 평평하게 펴기 (1차원 배열로 돌아감)
np.expand.dims()arr_1d = np.arange(10) # (10,)
print(np.expand_dims(arr_1d, axis=0)) # (1, 10) 행(row)차원 추가 / 1행10열의 행 벡터
print(np.expand_dims(arr_1d, axis=1)) # (10, 1) 열(column)차원 추가 / 10행1열의 열 벡터
# print(np.expand_dims(arr_1d, axis=None)) # [[0 1 2 3 4 5 6 7 8 9]]
# print(np.expand_dims(arr_1d, axis=2))
[[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]]
arr_2d = np.array([[1, 2], [3, 4]]) #(2, 2) 차원을 추가
print(np.expand_dims(arr_2d, axis=0).shape) # (1, 2, 2)
print(np.expand_dims(arr_2d, axis=1).shape) # (2, 1, 2)
print(np.expand_dims(arr_2d, axis=2).shape) # (2, 2, 1)
np.squeeze()arr = np.array([[1, 2]])
print(arr.shape) #(1, 2)
arr_sq = np.squeeze(arr)
arr_sq = np.squeeze(arr, axis=0)
# arr_sq = np.squeeze(arr, axis=1)
print(arr_sq, arr_sq.shape) # [1 2] (2,)
arr = np.array([[1], [2]])
print(arr.shape) #(2, 1)
arr_sq = np.squeeze(arr)
# arr_sq = np.squeeze(arr, axis=0)
arr_sq = np.squeeze(arr, axis=1)
print(arr_sq, arr_sq.shape) # [1 2] (2,)
arr = np.array([[[[[[[[[[1, 2]]]]]]]]]])
print(arr.shape, arr.ndim) # (1, 1, 1, 1, 1, 1, 1, 1, 1, 2) 10
print(np.squeeze(arr)) # [1 2]
print(np.squeeze(arr).shape) # (2,)
[심화 파트]
NumPy는 내부적으로 C 언어로 구현되어 있어, Python의 기본 리스트보다 훨씬 더 빠르고 메모리 효율적이다.
Python 리스트의 성능 측정
import time
size = 1000000
python_list = list(range(size))
start_time = time.time()
python_list = [x * 2 for x in python_list]
print("Python 리스트 성능:", time.time() - start_time)
import numpy as np
np_array = np.arange(size)
start_time = time.time()
np_array = np_array * 2
print("NumPy 배열 성능:", time.time() - start_time)
⇒ 대용량 데이터에서 NumPy 배열이 Python 리스트에 비해 얼마나 빠른지 성능 차이를 확인할 수 있다.
반복문을 사용하지 않고 벡터화 연산으로 성능을 최적화
반복문을 사용하는 방식
size = 1000000
python_list = list(range(size))
result = []
start_time = time.time()
for x in python_list:
result.append(x * 2)
print("반복문 성능:", time.time() - start_time)
np_array = np.arange(size)
start_time = time.time()
np_array = np_array * 2 # 벡터화 연산
print("벡터화 성능:", time.time() - start_time)
⇒ 벡터화 연산은 루프를 사용하지 않고, 데이터를 한 번에 처리하므로 훨씬 더 빠른 성능을 제공한다.
arr = np.array([1, 2, 3], dtype='int64')
print(arr.nbytes) # 배열이 사용하는 메모리 크기 (bytes)
arr = np.array([1, 2, 3], dtype='int32')
print(arr.nbytes) # 더 작은 메모리 사용
결론: 큰 데이터셋을 처리할 때는 데이터 타입을 적절히 조정해 메모리 효율성을 높일 수 있다.
size = 100000000 # 1억 개의 요소
arr = np.random.rand(size) # 난수 배열 생성
start_time = time.time()
result = np.sum(arr) # 배열의 합 계산
print("대용량 데이터 합계 성능:", time.time() - start_time)
np.memmap()을 사용하면, 전체 데이터를 메모리에 올리지 않고 파일로부터 필요한 부분만 메모리에 로드해 작업을 수행할 수 있음# 1GB 크기의 파일을 생성 후 메모리 맵핑 방식으로 읽기
size = 10**9
arr = np.memmap('large_file.dat', dtype='float32', mode='w+', shape=(size,))
arr[:100] = np.random.rand(100) # 일부 데이터를 파일에 쓰기
# 파일을 메모리 맵핑하여 읽기
memmap_arr = np.memmap('large_file.dat', dtype='float32', mode='r', shape=(size,))
print(memmap_arr[:10]) # 메모리에 로드된 일부 데이터만 접근
⇒ 메모리 크기를 초과하는 대용량 데이터를 처리할 때 np.memmap()을 사용하여 메모리 부담을 줄일 수 있다.