Python NumPy 튜토리얼: 과학 계산의 기초
NumPy 소개
NumPy(Numerical Python)는 Python에서 과학 계산을 위한 핵심 라이브러리입니다. 대규모 다차원 배열과 행렬 연산을 효율적으로 처리할 수 있으며, 이러한 배열을 다루기 위한 고수준의 수학 함수들을 제공합니다. NumPy는 C언어로 구현되어 있어 순수 Python보다 훨씬 빠른 연산 속도를 제공하며, 과학 계산, 데이터 분석, 머신러닝 등 다양한 분야에서 필수적으로 사용됩니다.
설치 방법
NumPy를 설치하는 가장 간단한 방법은 pip를 사용하는 것입니다:
pip install numpy
또는 Anaconda를 사용하는 경우:
conda install numpy
1. 배열 기초: ndarray 이해하기
NumPy의 핵심은 ndarray(N-dimensional array) 객체입니다. 이는 같은 타입의 요소들로 구성된 다차원 배열로, Python의 리스트보다 훨씬 효율적입니다.
import numpy as np
# NumPy 배열과 Python 리스트의 차이점
# Python 리스트
python_list = [1, 2, 3, 4, 5]
print("Python 리스트:", python_list)
print("타입:", type(python_list))
# NumPy 배열
numpy_array = np.array([1, 2, 3, 4, 5])
print("\nNumPy 배열:", numpy_array)
print("타입:", type(numpy_array))
# 연산 속도 비교
import time
# 큰 리스트/배열 생성
size = 1000000
python_list = list(range(size))
numpy_array = np.arange(size)
# Python 리스트 연산
start = time.time()
python_result = [x * 2 for x in python_list]
python_time = time.time() - start
# NumPy 배열 연산
start = time.time()
numpy_result = numpy_array * 2
numpy_time = time.time() - start
print(f"\nPython 리스트 연산 시간: {python_time:.4f}초")
print(f"NumPy 배열 연산 시간: {numpy_time:.4f}초")
print(f"NumPy가 {python_time/numpy_time:.1f}배 빠름")
2. 배열 생성 방법
NumPy는 다양한 방법으로 배열을 생성할 수 있는 함수들을 제공합니다.
# 1. 리스트로부터 배열 생성
arr1 = np.array([1, 2, 3, 4, 5])
print("1차원 배열:", arr1)
# 2차원 배열 (행렬)
arr2 = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print("\n2차원 배열:\n", arr2)
# 3차원 배열
arr3 = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
print("\n3차원 배열:\n", arr3)
# 2. 특정 값으로 초기화된 배열
print("\n=== 특정 값으로 초기화 ===")
# 모든 요소가 0인 배열
zeros = np.zeros((3, 4))
print("영행렬:\n", zeros)
# 모든 요소가 1인 배열
ones = np.ones((2, 3, 4))
print("\n1로 채워진 3차원 배열 shape:", ones.shape)
# 특정 값으로 채워진 배열
full = np.full((3, 3), 7)
print("\n7로 채워진 배열:\n", full)
# 단위 행렬 (Identity matrix)
identity = np.eye(4)
print("\n4x4 단위 행렬:\n", identity)
# 3. 수열 생성
print("\n=== 수열 생성 ===")
# arange: 일정 간격의 수열
seq1 = np.arange(0, 10, 2) # 시작, 끝(미포함), 간격
print("arange(0, 10, 2):", seq1)
# linspace: 구간을 균등 분할
seq2 = np.linspace(0, 1, 5) # 시작, 끝(포함), 개수
print("linspace(0, 1, 5):", seq2)
# logspace: 로그 스케일로 균등 분할
seq3 = np.logspace(0, 3, 4) # 10^0부터 10^3까지 4개
print("logspace(0, 3, 4):", seq3)
# 4. 배열 복사와 뷰
print("\n=== 배열 복사 ===")
original = np.array([1, 2, 3, 4, 5])
# 얕은 복사 (뷰)
view = original.view()
view[0] = 100
print("뷰 수정 후 원본:", original) # 원본도 변경됨
# 깊은 복사
copy = original.copy()
copy[0] = 200
print("복사본 수정 후 원본:", original) # 원본은 변경 안됨
3. 배열 속성과 정보 확인
NumPy 배열의 다양한 속성을 확인하여 배열의 구조와 특성을 파악할 수 있습니다.
# 다양한 형태의 배열 생성
arr_1d = np.array([1, 2, 3, 4, 5])
arr_2d = np.array([[1, 2, 3], [4, 5, 6]])
arr_3d = np.random.rand(2, 3, 4)
print("=== 배열 속성 확인 ===")
# shape: 배열의 차원과 크기
print(f"1차원 배열 shape: {arr_1d.shape}")
print(f"2차원 배열 shape: {arr_2d.shape}")
print(f"3차원 배열 shape: {arr_3d.shape}")
# ndim: 배열의 차원 수
print(f"\n차원 수 - 1D: {arr_1d.ndim}, 2D: {arr_2d.ndim}, 3D: {arr_3d.ndim}")
# size: 전체 요소 개수
print(f"\n전체 요소 개수 - 1D: {arr_1d.size}, 2D: {arr_2d.size}, 3D: {arr_3d.size}")
# dtype: 데이터 타입
print(f"\n데이터 타입:")
print(f"정수 배열: {arr_1d.dtype}")
print(f"실수 배열: {arr_3d.dtype}")
# 데이터 타입 지정
arr_int32 = np.array([1, 2, 3], dtype=np.int32)
arr_float64 = np.array([1, 2, 3], dtype=np.float64)
arr_complex = np.array([1+2j, 3+4j], dtype=np.complex128)
print(f"\nint32: {arr_int32.dtype}, float64: {arr_float64.dtype}, complex: {arr_complex.dtype}")
# itemsize: 각 요소의 바이트 크기
print(f"\n요소당 바이트 크기:")
print(f"int32: {arr_int32.itemsize} bytes")
print(f"float64: {arr_float64.itemsize} bytes")
# nbytes: 전체 배열의 바이트 크기
print(f"\n전체 배열 크기:")
print(f"2D 배열: {arr_2d.nbytes} bytes")
print(f"3D 배열: {arr_3d.nbytes} bytes")
# 배열 정보 요약
print("\n=== 배열 정보 요약 ===")
def array_info(arr, name):
print(f"\n{name}:")
print(f" Shape: {arr.shape}")
print(f" Dimensions: {arr.ndim}")
print(f" Size: {arr.size}")
print(f" Data type: {arr.dtype}")
print(f" Memory usage: {arr.nbytes} bytes")
array_info(arr_3d, "3D Random Array")
4. 인덱싱과 슬라이싱
NumPy 배열의 요소에 접근하고 부분 배열을 추출하는 다양한 방법을 제공합니다.
# 1차원 배열 인덱싱
print("=== 1차원 배열 인덱싱 ===")
arr_1d = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90])
print("원본 배열:", arr_1d)
# 기본 인덱싱
print(f"첫 번째 요소: {arr_1d[0]}")
print(f"마지막 요소: {arr_1d[-1]}")
print(f"뒤에서 세 번째: {arr_1d[-3]}")
# 슬라이싱
print(f"\n처음 3개: {arr_1d[:3]}")
print(f"3번째부터 6번째까지: {arr_1d[2:6]}")
print(f"2칸씩 건너뛰며: {arr_1d[::2]}")
print(f"역순: {arr_1d[::-1]}")
# 2차원 배열 인덱싱
print("\n=== 2차원 배열 인덱싱 ===")
arr_2d = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print("원본 배열:\n", arr_2d)
# 특정 요소 접근
print(f"\n(1,2) 위치의 요소: {arr_2d[1, 2]}")
print(f"두 번째 행: {arr_2d[1]}")
print(f"세 번째 열: {arr_2d[:, 2]}")
# 부분 배열 추출
print("\n부분 배열:")
print(arr_2d[0:2, 1:3])
# 팬시 인덱싱 (Fancy Indexing)
print("\n=== 팬시 인덱싱 ===")
arr = np.array([10, 20, 30, 40, 50])
# 인덱스 배열로 접근
indices = np.array([0, 2, 4])
print(f"인덱스 [0, 2, 4]의 요소들: {arr[indices]}")
# 2차원에서의 팬시 인덱싱
rows = np.array([0, 1, 2])
cols = np.array([1, 2, 3])
print(f"\n대각선 아래 요소들: {arr_2d[rows, cols]}")
# 불리언 인덱싱 (Boolean Indexing)
print("\n=== 불리언 인덱싱 ===")
arr = np.array([1, 5, 3, 8, 2, 7, 4, 6])
print("원본 배열:", arr)
# 조건을 만족하는 요소 선택
mask = arr > 4
print(f"4보다 큰 값들: {arr[mask]}")
# 복합 조건
mask = (arr > 2) & (arr < 7)
print(f"2보다 크고 7보다 작은 값들: {arr[mask]}")
# 조건을 만족하는 요소 수정
arr[arr % 2 == 0] = 0
print(f"짝수를 0으로 변경: {arr}")
5. 배열 연산
NumPy는 배열 간의 다양한 수학적 연산을 벡터화된 방식으로 효율적으로 수행합니다.
# 기본 산술 연산
print("=== 기본 산술 연산 ===")
a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])
print(f"a = {a}")
print(f"b = {b}")
print(f"a + b = {a + b}")
print(f"a - b = {a - b}")
print(f"a * b = {a * b}") # 요소별 곱셈
print(f"a / b = {a / b}")
print(f"a ** 2 = {a ** 2}")
# 스칼라 연산
print("\n=== 스칼라 연산 ===")
print(f"a * 10 = {a * 10}")
print(f"a + 100 = {a + 100}")
# 비교 연산
print("\n=== 비교 연산 ===")
print(f"a > 2 = {a > 2}")
print(f"a == b = {a == b}")
print(f"a < b = {a < b}")
# 행렬 연산
print("\n=== 행렬 연산 ===")
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print("행렬 A:\n", A)
print("\n행렬 B:\n", B)
# 요소별 곱셈
print("\n요소별 곱셈 (A * B):\n", A * B)
# 행렬 곱셈
print("\n행렬 곱셈 (A @ B):\n", A @ B)
print("\ndot 함수 사용:\n", np.dot(A, B))
# 전치 행렬
print("\nA의 전치 행렬:\n", A.T)
# 집계 함수
print("\n=== 집계 함수 ===")
arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print("배열:\n", arr)
print(f"\n전체 합: {arr.sum()}")
print(f"행 방향 합: {arr.sum(axis=1)}")
print(f"열 방향 합: {arr.sum(axis=0)}")
print(f"평균: {arr.mean()}")
print(f"표준편차: {arr.std():.2f}")
print(f"최댓값: {arr.max()}")
print(f"최솟값 위치: {arr.argmin()}")
6. 브로드캐스팅
브로드캐스팅은 서로 다른 크기의 배열 간 연산을 가능하게 하는 NumPy의 강력한 기능입니다.
# 브로드캐스팅 기본 개념
print("=== 브로드캐스팅 기본 ===")
# 스칼라와 배열
arr = np.array([1, 2, 3, 4])
print(f"배열: {arr}")
print(f"배열 + 10: {arr + 10}") # 10이 모든 요소에 브로드캐스트
# 1차원과 2차원 배열
print("\n=== 1D와 2D 배열 브로드캐스팅 ===")
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
vector = np.array([10, 20, 30])
print("행렬:\n", matrix)
print("벡터:", vector)
print("행렬 + 벡터:\n", matrix + vector)
# 열 벡터와의 브로드캐스팅
col_vector = np.array([[100], [200], [300]])
print("\n열 벡터:\n", col_vector)
print("행렬 + 열 벡터:\n", matrix + col_vector)
# 브로드캐스팅 규칙 시연
print("\n=== 브로드캐스팅 규칙 ===")
a = np.ones((3, 4))
b = np.arange(4)
c = np.arange(3).reshape(3, 1)
print(f"a shape: {a.shape}")
print(f"b shape: {b.shape}")
print(f"c shape: {c.shape}")
print("\na + b (3,4) + (4,) -> (3,4):")
print(a + b)
print("\na + c (3,4) + (3,1) -> (3,4):")
print(a + c)
# 실용적인 브로드캐스팅 예제
print("\n=== 실용적인 예제 ===")
# 이미지 정규화 시뮬레이션
image = np.random.randint(0, 255, size=(3, 4, 3)) # 3x4 RGB 이미지
mean = np.array([128, 128, 128]) # RGB 평균값
std = np.array([64, 64, 64]) # RGB 표준편차
normalized = (image - mean) / std
print("원본 이미지 shape:", image.shape)
print("정규화된 이미지 shape:", normalized.shape)
# 거리 행렬 계산
points = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
# 각 점 간의 유클리드 거리
diff = points[:, np.newaxis, :] - points[np.newaxis, :, :]
distances = np.sqrt((diff ** 2).sum(axis=2))
print("\n점들 간의 거리 행렬:\n", distances)
7. 배열 조작과 변형
NumPy는 배열의 형태를 변경하고 조작하는 다양한 메서드를 제공합니다.
# 배열 형태 변경
print("=== 배열 형태 변경 ===")
arr = np.arange(12)
print("원본 배열:", arr)
# reshape: 형태 변경
reshaped = arr.reshape(3, 4)
print("\n3x4로 변형:\n", reshaped)
# -1을 사용한 자동 크기 계산
auto_reshaped = arr.reshape(2, -1)
print("\n2x?로 자동 변형:\n", auto_reshaped)
# flatten과 ravel
print("\n=== 1차원으로 평탄화 ===")
matrix = np.array([[1, 2, 3], [4, 5, 6]])
print("원본 행렬:\n", matrix)
flattened = matrix.flatten() # 복사본 반환
raveled = matrix.ravel() # 가능하면 뷰 반환
print("flatten():", flattened)
print("ravel():", raveled)
# 배열 결합
print("\n=== 배열 결합 ===")
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
# 수직 결합 (행 방향)
v_stack = np.vstack((a, b))
print("수직 결합 (vstack):\n", v_stack)
# 수평 결합 (열 방향)
h_stack = np.hstack((a, b))
print("\n수평 결합 (hstack):\n", h_stack)
# concatenate 함수
concat_axis0 = np.concatenate((a, b), axis=0)
concat_axis1 = np.concatenate((a, b), axis=1)
print("\nconcatenate axis=0:\n", concat_axis0)
print("\nconcatenate axis=1:\n", concat_axis1)
# 배열 분할
print("\n=== 배열 분할 ===")
arr = np.arange(16).reshape(4, 4)
print("원본 배열:\n", arr)
# 수평 분할
h_parts = np.hsplit(arr, 2)
print("\n수평 분할 (2개로):")
for i, part in enumerate(h_parts):
print(f"Part {i+1}:\n{part}")
# 수직 분할
v_parts = np.vsplit(arr, 2)
print("\n수직 분할 (2개로):")
for i, part in enumerate(v_parts):
print(f"Part {i+1}:\n{part}")
# 차원 추가와 제거
print("\n=== 차원 추가/제거 ===")
arr_1d = np.array([1, 2, 3, 4])
print(f"원본 1D 배열: {arr_1d}, shape: {arr_1d.shape}")
# newaxis를 사용한 차원 추가
col_vector = arr_1d[:, np.newaxis]
row_vector = arr_1d[np.newaxis, :]
print(f"\n열 벡터 shape: {col_vector.shape}")
print(f"행 벡터 shape: {row_vector.shape}")
# squeeze: 크기가 1인 차원 제거
arr_3d = np.array([[[1, 2, 3]]])
print(f"\n원본 shape: {arr_3d.shape}")
squeezed = np.squeeze(arr_3d)
print(f"squeeze 후 shape: {squeezed.shape}")
8. 수학 함수와 통계
NumPy는 다양한 수학 함수와 통계 함수를 제공하여 배열 데이터를 분석할 수 있습니다.
# 기본 수학 함수
print("=== 기본 수학 함수 ===")
arr = np.array([0, 30, 45, 60, 90])
print(f"각도 배열: {arr}")
# 삼각함수 (라디안 단위)
rad = np.deg2rad(arr)
print(f"\n라디안: {rad}")
print(f"sin: {np.sin(rad).round(3)}")
print(f"cos: {np.cos(rad).round(3)}")
print(f"tan: {np.tan(rad).round(3)}")
# 지수와 로그
print("\n=== 지수와 로그 ===")
arr = np.array([1, 2, 3, 4, 5])
print(f"원본: {arr}")
print(f"exp(x): {np.exp(arr).round(2)}")
print(f"log(x): {np.log(arr).round(3)}")
print(f"log10(x): {np.log10(arr).round(3)}")
print(f"log2(x): {np.log2(arr).round(3)}")
# 반올림 함수
print("\n=== 반올림 함수 ===")
arr = np.array([1.23, 2.67, 3.5, 4.89, -1.23, -2.67])
print(f"원본: {arr}")
print(f"round: {np.round(arr, 1)}")
print(f"floor: {np.floor(arr)}")
print(f"ceil: {np.ceil(arr)}")
print(f"trunc: {np.trunc(arr)}")
# 통계 함수
print("\n=== 통계 함수 ===")
data = np.random.normal(100, 15, 1000) # 평균 100, 표준편차 15
print(f"데이터 개수: {len(data)}")
print(f"평균: {np.mean(data):.2f}")
print(f"중앙값: {np.median(data):.2f}")
print(f"표준편차: {np.std(data):.2f}")
print(f"분산: {np.var(data):.2f}")
print(f"최솟값: {np.min(data):.2f}")
print(f"최댓값: {np.max(data):.2f}")
# 백분위수
percentiles = [25, 50, 75]
print(f"\n백분위수 {percentiles}: {np.percentile(data, percentiles).round(2)}")
# 축별 통계
print("\n=== 축별 통계 ===")
matrix = np.random.randint(1, 10, size=(4, 5))
print("행렬:\n", matrix)
print(f"\n행별 합계: {matrix.sum(axis=1)}")
print(f"열별 평균: {matrix.mean(axis=0).round(2)}")
print(f"전체 표준편차: {matrix.std():.2f}")
# 누적 함수
print("\n=== 누적 함수 ===")
arr = np.array([1, 2, 3, 4, 5])
print(f"원본: {arr}")
print(f"누적 합: {np.cumsum(arr)}")
print(f"누적 곱: {np.cumprod(arr)}")
# 차분
print(f"\n차분: {np.diff(arr)}")
print(f"2차 차분: {np.diff(arr, n=2)}")
9. 선형대수 연산
NumPy는 선형대수 연산을 위한 강력한 기능을 제공합니다.
# 행렬 곱셈
print("=== 행렬 곱셈 ===")
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print("A:\n", A)
print("\nB:\n", B)
print("\nA @ B:\n", A @ B)
print("\nnp.matmul(A, B):\n", np.matmul(A, B))
# 내적과 외적
print("\n=== 벡터 연산 ===")
v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])
print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"내적 (dot product): {np.dot(v1, v2)}")
print(f"외적 (cross product): {np.cross(v1, v2)}")
print("외적 행렬:\n", np.outer(v1, v2))
# 행렬식과 역행렬
print("\n=== 행렬식과 역행렬 ===")
A = np.array([[4, 7], [2, 6]])
print("행렬 A:\n", A)
det = np.linalg.det(A)
print(f"\n행렬식: {det:.2f}")
if det != 0:
inv_A = np.linalg.inv(A)
print("\n역행렬:\n", inv_A)
print("\nA @ A^(-1):\n", A @ inv_A)
# 고유값과 고유벡터
print("\n=== 고유값과 고유벡터 ===")
A = np.array([[4, -2], [1, 1]])
eigenvalues, eigenvectors = np.linalg.eig(A)
print("행렬 A:\n", A)
print(f"\n고유값: {eigenvalues}")
print("\n고유벡터:\n", eigenvectors)
# 검증
for i in range(len(eigenvalues)):
v = eigenvectors[:, i]
print(f"\n고유값 {eigenvalues[i]:.2f}에 대한 검증:")
print(f"A @ v = {A @ v}")
print(f"λ * v = {eigenvalues[i] * v}")
# 선형 방정식 해
print("\n=== 선형 방정식 Ax = b 풀기 ===")
A = np.array([[3, 1], [1, 2]])
b = np.array([9, 8])
x = np.linalg.solve(A, b)
print("A:\n", A)
print(f"\nb: {b}")
print(f"\n해 x: {x}")
print(f"검증 A @ x: {A @ x}")
# 특이값 분해 (SVD)
print("\n=== 특이값 분해 (SVD) ===")
A = np.array([[1, 2, 3], [4, 5, 6]])
U, s, Vt = np.linalg.svd(A)
print("원본 행렬 A:\n", A)
print(f"\n특이값: {s}")
print(f"U shape: {U.shape}")
print(f"Vt shape: {Vt.shape}")
# 재구성
S = np.zeros((2, 3))
S[:2, :2] = np.diag(s)
A_reconstructed = U @ S @ Vt
print("\n재구성된 A:\n", A_reconstructed)
10. 난수 생성과 샘플링
NumPy는 다양한 확률 분포에서 난수를 생성할 수 있는 기능을 제공합니다.
# 난수 시드 설정
np.random.seed(42) # 재현 가능한 결과를 위해
# 균등 분포
print("=== 균등 분포 ===")
uniform = np.random.uniform(0, 1, 10)
print(f"0과 1 사이 균등 분포: {uniform.round(3)}")
rand_int = np.random.randint(1, 11, 10)
print(f"1-10 사이 정수: {rand_int}")
# 정규 분포
print("\n=== 정규 분포 ===")
normal = np.random.normal(0, 1, 1000) # 평균 0, 표준편차 1
print(f"표준 정규 분포 통계:")
print(f" 평균: {normal.mean():.3f}")
print(f" 표준편차: {normal.std():.3f}")
# 다양한 분포
print("\n=== 다양한 확률 분포 ===")
# 이항 분포
binomial = np.random.binomial(n=10, p=0.5, size=20)
print(f"이항 분포 (n=10, p=0.5): {binomial}")
# 포아송 분포
poisson = np.random.poisson(lam=3, size=20)
print(f"포아송 분포 (λ=3): {poisson}")
# 지수 분포
exponential = np.random.exponential(scale=2, size=10)
print(f"지수 분포 (scale=2): {exponential.round(2)}")
# 배열 셔플링과 샘플링
print("\n=== 배열 셔플링과 샘플링 ===")
arr = np.arange(10)
print(f"원본 배열: {arr}")
# 복사본 셔플
shuffled = arr.copy()
np.random.shuffle(shuffled)
print(f"셔플된 배열: {shuffled}")
# 무작위 샘플링
sample = np.random.choice(arr, size=5, replace=False)
print(f"무작위 샘플 (비복원): {sample}")
# 가중치를 사용한 샘플링
weights = np.array([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
weighted_sample = np.random.choice(arr, size=20, p=weights)
print(f"가중치 샘플링: {weighted_sample}")
# 다차원 난수 배열
print("\n=== 다차원 난수 배열 ===")
rand_2d = np.random.rand(3, 4) # 0-1 균등 분포
print("3x4 균등 분포:\n", rand_2d.round(3))
randn_3d = np.random.randn(2, 3, 4) # 표준 정규 분포
print(f"\n2x3x4 표준 정규 분포 shape: {randn_3d.shape}")
11. 성능 최적화 기법
NumPy를 효율적으로 사용하기 위한 성능 최적화 기법들을 살펴봅니다.
import time
# 벡터화 vs 반복문
print("=== 벡터화 vs 반복문 ===")
size = 1000000
arr = np.random.rand(size)
# 반복문 사용
start = time.time()
result_loop = np.empty(size)
for i in range(size):
result_loop[i] = arr[i] ** 2 + 2 * arr[i] + 1
loop_time = time.time() - start
# 벡터화 연산
start = time.time()
result_vector = arr ** 2 + 2 * arr + 1
vector_time = time.time() - start
print(f"반복문 시간: {loop_time:.4f}초")
print(f"벡터화 시간: {vector_time:.4f}초")
print(f"속도 향상: {loop_time/vector_time:.1f}배")
# 메모리 레이아웃
print("\n=== 메모리 레이아웃 ===")
# C-order (행 우선) vs F-order (열 우선)
c_array = np.ones((1000, 1000), order='C')
f_array = np.ones((1000, 1000), order='F')
# 행 방향 합계 (C-order가 유리)
start = time.time()
c_row_sum = c_array.sum(axis=1)
c_time = time.time() - start
start = time.time()
f_row_sum = f_array.sum(axis=1)
f_time = time.time() - start
print(f"행 합계 - C-order: {c_time:.6f}초")
print(f"행 합계 - F-order: {f_time:.6f}초")
# 뷰 vs 복사
print("\n=== 뷰 vs 복사 ===")
large_array = np.random.rand(10000000)
# 뷰 생성 (빠름)
start = time.time()
view = large_array[1000000:2000000]
view_time = time.time() - start
# 복사 생성 (느림)
start = time.time()
copy = large_array[1000000:2000000].copy()
copy_time = time.time() - start
print(f"뷰 생성 시간: {view_time:.6f}초")
print(f"복사 생성 시간: {copy_time:.6f}초")
# 사전 할당
print("\n=== 사전 할당 ===")
n = 10000
# 동적 추가 (느림)
start = time.time()
result = np.array([])
for i in range(n):
result = np.append(result, i)
append_time = time.time() - start
# 사전 할당 (빠름)
start = time.time()
result = np.empty(n)
for i in range(n):
result[i] = i
preallocate_time = time.time() - start
print(f"동적 추가: {append_time:.4f}초")
print(f"사전 할당: {preallocate_time:.4f}초")
print(f"속도 향상: {append_time/preallocate_time:.1f}배")
# 유용한 팁
print("\n=== 성능 최적화 팁 ===")
print("1. 가능한 벡터화 연산 사용")
print("2. 적절한 데이터 타입 선택 (float32 vs float64)")
print("3. 뷰 활용으로 불필요한 복사 방지")
print("4. 배열 크기를 미리 알 때는 사전 할당")
print("5. 연산 순서에 맞는 메모리 레이아웃 선택")
12. 실전 예제: 종합 활용
지금까지 배운 NumPy 기능들을 종합적으로 활용하는 실전 예제를 살펴보겠습니다.
# 실전 예제 1: 이미지 처리 시뮬레이션
print("=== 예제 1: 이미지 처리 ===")
# 가상의 그레이스케일 이미지 생성
image = np.random.randint(0, 256, size=(100, 100), dtype=np.uint8)
print(f"원본 이미지 shape: {image.shape}, dtype: {image.dtype}")
# 1. 밝기 조정
brightness_factor = 1.5
brightened = np.clip(image * brightness_factor, 0, 255).astype(np.uint8)
# 2. 가우시안 블러 (간단한 버전)
kernel = np.array([[1, 2, 1],
[2, 4, 2],
[1, 2, 1]]) / 16
def simple_conv2d(image, kernel):
"""간단한 2D 컨볼루션"""
h, w = image.shape
kh, kw = kernel.shape
pad_h, pad_w = kh // 2, kw // 2
# 패딩 추가
padded = np.pad(image, ((pad_h, pad_h), (pad_w, pad_w)), mode='edge')
result = np.zeros_like(image, dtype=np.float32)
for i in range(h):
for j in range(w):
result[i, j] = np.sum(padded[i:i+kh, j:j+kw] * kernel)
return result.astype(np.uint8)
blurred = simple_conv2d(image, kernel)
# 3. 엣지 검출 (Sobel 필터)
sobel_x = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
sobel_y = np.array([[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]])
edges_x = simple_conv2d(image, sobel_x)
edges_y = simple_conv2d(image, sobel_y)
edges = np.sqrt(edges_x**2 + edges_y**2)
print(f"처리된 이미지들의 통계:")
print(f" 원본 - 평균: {image.mean():.1f}, 표준편차: {image.std():.1f}")
print(f" 밝기 조정 - 평균: {brightened.mean():.1f}, 표준편차: {brightened.std():.1f}")
print(f" 블러 - 평균: {blurred.mean():.1f}, 표준편차: {blurred.std():.1f}")
print(f" 엣지 - 평균: {edges.mean():.1f}, 최댓값: {edges.max():.1f}")
# 실전 예제 2: 시계열 데이터 분석
print("\n=== 예제 2: 시계열 데이터 분석 ===")
# 주식 가격 시뮬레이션 (기하 브라운 운동)
n_days = 252 # 1년 거래일
initial_price = 100
drift = 0.0002 # 일일 수익률
volatility = 0.02 # 일일 변동성
# 일일 수익률 생성
daily_returns = np.random.normal(drift, volatility, n_days)
price_series = initial_price * np.exp(np.cumsum(daily_returns))
# 기술적 지표 계산
# 1. 이동 평균
window_sizes = [5, 20, 50]
moving_averages = {}
for window in window_sizes:
ma = np.convolve(price_series, np.ones(window)/window, mode='valid')
moving_averages[f'MA{window}'] = ma
print(f"MA{window} - 최종값: {ma[-1]:.2f}")
# 2. 볼린저 밴드
window = 20
rolling_mean = np.convolve(price_series, np.ones(window)/window, mode='valid')
rolling_std = np.array([price_series[i:i+window].std()
for i in range(len(price_series)-window+1)])
upper_band = rolling_mean + 2 * rolling_std
lower_band = rolling_mean - 2 * rolling_std
print(f"\n볼린저 밴드 (마지막 값):")
print(f" 상단: {upper_band[-1]:.2f}")
print(f" 중간: {rolling_mean[-1]:.2f}")
print(f" 하단: {lower_band[-1]:.2f}")
# 3. RSI (Relative Strength Index)
price_diff = np.diff(price_series)
gains = np.where(price_diff > 0, price_diff, 0)
losses = np.where(price_diff < 0, -price_diff, 0)
avg_gain = np.convolve(gains, np.ones(14)/14, mode='valid')
avg_loss = np.convolve(losses, np.ones(14)/14, mode='valid')
rs = avg_gain / (avg_loss + 1e-10) # 0으로 나누기 방지
rsi = 100 - (100 / (1 + rs))
print(f"\nRSI (마지막 값): {rsi[-1]:.2f}")
# 실전 예제 3: 머신러닝 데이터 전처리
print("\n=== 예제 3: 머신러닝 데이터 전처리 ===")
# 가상의 데이터셋 생성
n_samples = 1000
n_features = 5
# 특성 생성 (서로 다른 스케일)
X = np.column_stack([
np.random.normal(100, 15, n_samples), # 특성 1: 평균 100
np.random.normal(50, 5, n_samples), # 특성 2: 평균 50
np.random.exponential(10, n_samples), # 특성 3: 지수 분포
np.random.uniform(0, 1, n_samples), # 특성 4: 0-1 균등
np.random.randint(0, 100, n_samples) # 특성 5: 정수
])
# 타겟 변수 (특성들의 선형 결합 + 노이즈)
weights = np.array([0.3, 0.2, 0.1, 0.3, 0.1])
y = X @ weights + np.random.normal(0, 10, n_samples)
print("원본 데이터 통계:")
for i in range(n_features):
print(f" 특성 {i+1}: 평균={X[:, i].mean():.2f}, 표준편차={X[:, i].std():.2f}")
# 1. 표준화 (Standardization)
X_mean = X.mean(axis=0)
X_std = X.std(axis=0)
X_standardized = (X - X_mean) / X_std
print("\n표준화 후 통계:")
for i in range(n_features):
print(f" 특성 {i+1}: 평균={X_standardized[:, i].mean():.2f}, 표준편차={X_standardized[:, i].std():.2f}")
# 2. 정규화 (Normalization)
X_min = X.min(axis=0)
X_max = X.max(axis=0)
X_normalized = (X - X_min) / (X_max - X_min)
print("\n정규화 후 범위:")
for i in range(n_features):
print(f" 특성 {i+1}: [{X_normalized[:, i].min():.2f}, {X_normalized[:, i].max():.2f}]")
# 3. 이상치 탐지
# IQR 방법 사용
Q1 = np.percentile(X, 25, axis=0)
Q3 = np.percentile(X, 75, axis=0)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = np.zeros_like(X, dtype=bool)
for i in range(n_features):
outliers[:, i] = (X[:, i] < lower_bound[i]) | (X[:, i] > upper_bound[i])
print("\n이상치 탐지 결과:")
for i in range(n_features):
n_outliers = outliers[:, i].sum()
print(f" 특성 {i+1}: {n_outliers}개 ({n_outliers/n_samples*100:.1f}%)")
# 4. 주성분 분석 (PCA) 간단 버전
# 공분산 행렬
X_centered = X_standardized - X_standardized.mean(axis=0)
cov_matrix = (X_centered.T @ X_centered) / (n_samples - 1)
# 고유값 분해
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
# 고유값 기준 정렬
idx = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 설명된 분산 비율
explained_variance_ratio = eigenvalues / eigenvalues.sum()
print("\n주성분 분석 결과:")
for i, ratio in enumerate(explained_variance_ratio):
print(f" PC{i+1}: {ratio*100:.1f}% 분산 설명")
# 첫 2개 주성분으로 변환
n_components = 2
X_pca = X_centered @ eigenvectors[:, :n_components]
print(f"\nPCA 변환 후 shape: {X_pca.shape}")
# 최종 요약
print("\n=== 분석 요약 ===")
print(f"원본 데이터: {X.shape}")
print(f"표준화 완료: 평균 0, 표준편차 1")
print(f"정규화 완료: 범위 [0, 1]")
print(f"이상치 비율: {outliers.any(axis=1).sum()/n_samples*100:.1f}%")
print(f"PCA: {n_features}차원 -> {n_components}차원")
print(f"설명된 분산: {explained_variance_ratio[:n_components].sum()*100:.1f}%")
마무리
이 튜토리얼에서는 NumPy의 핵심 기능들을 실제 예제와 함께 살펴보았습니다. NumPy는 Python에서 수치 계산을 위한 가장 기본적이고 강력한 도구입니다. 배열 연산, 선형대수, 통계, 난수 생성 등 다양한 기능을 제공하여 과학 계산과 데이터 분석의 기초가 됩니다.
- NumPy 공식 문서를 참고하여 더 많은 함수와 기능을 탐색하세요
- 실제 데이터로 연습하면서 벡터화 연산의 효율성을 체감해보세요
- SciPy, Pandas, Matplotlib과 함께 사용하여 더 강력한 분석을 수행하세요
- 메모리 효율성과 연산 속도를 고려한 코드 작성을 연습하세요
- 선형대수와 통계학 기초를 학습하면 NumPy를 더 잘 활용할 수 있습니다
#python #파이썬 #넘파이 #numpy #tutorial #튜토리얼 #기초 #가이드