이미지 로딩 중...
AI Generated
2025. 11. 21. · 7 Views
NumPy 실전 활용 완벽 가이드 - 행렬 연산과 성능 최적화
데이터 과학과 머신러닝의 필수 도구인 NumPy의 핵심 기능을 마스터하세요. 기본 배열 생성부터 고급 행렬 연산, 브로드캐스팅, 벡터화까지 실무에서 바로 활용할 수 있는 모든 것을 다룹니다. 성능 최적화 기법까지 배워서 데이터 처리 속도를 획기적으로 향상시켜보세요.
목차
- NumPy 배열 생성과 기본 연산
- 브로드캐스팅으로 차원 다른 배열 연산하기
- 벡터화로 반복문 제거하고 성능 극대화하기
- 행렬 연산과 선형대수 실전 활용
- 인덱싱과 슬라이싱으로 데이터 자유자재로 다루기
- 배열 연결과 분할로 데이터 재구성하기
- 유니버설 함수로 빠른 수학 연산 수행하기
- 배열 형태 변환으로 데이터 차원 조작하기
- 통계 함수로 데이터 분석하기
- 난수 생성과 시뮬레이션
1. NumPy 배열 생성과 기본 연산
시작하며
여러분이 데이터 분석을 하다가 100만 개의 숫자를 처리해야 하는 상황을 생각해보세요. Python의 기본 리스트로 이걸 처리하면 시간이 엄청나게 오래 걸립니다.
마치 손으로 계산기를 100만 번 두드리는 것과 비슷하죠. 이런 문제는 실제 데이터 과학 현장에서 매일 발생합니다.
큰 데이터셋을 다루다 보면 프로그램이 너무 느려서 커피를 마시러 가도 아직 계산 중인 경우가 많습니다. 이건 단순히 불편한 게 아니라 프로젝트 전체의 생산성을 떨어뜨리는 심각한 문제입니다.
바로 이럴 때 필요한 것이 NumPy 배열입니다. NumPy는 마치 계산 전용 고속도로 같아서, 같은 계산을 10배에서 100배까지 빠르게 처리할 수 있게 해줍니다.
개요
간단히 말해서, NumPy 배열은 같은 종류의 숫자들을 효율적으로 저장하고 계산하는 특별한 상자입니다. NumPy 배열이 필요한 이유는 간단합니다.
Python의 기본 리스트는 편리하지만 느립니다. 리스트는 여러 종류의 데이터를 담을 수 있도록 만들어졌기 때문에, 숫자만 빠르게 계산하는 데는 비효율적이에요.
예를 들어, 1000개의 숫자를 모두 2배로 만들어야 한다면, 리스트는 하나하나 확인하면서 처리하지만 NumPy는 한 번에 쭉 처리합니다. 기존에는 for 문을 돌면서 하나씩 계산했다면, 이제는 배열 전체에 한 번에 연산을 적용할 수 있습니다.
이걸 '벡터화 연산'이라고 부릅니다. NumPy 배열의 핵심 특징은 세 가지입니다.
첫째, 모든 원소가 같은 타입이어서 메모리를 효율적으로 사용합니다. 둘째, C 언어로 만들어져서 엄청나게 빠릅니다.
셋째, 다차원 배열을 쉽게 다룰 수 있어서 이미지나 표 형태의 데이터를 처리하기 좋습니다. 이러한 특징들이 NumPy를 데이터 과학의 필수 도구로 만들어줍니다.
코드 예제
import numpy as np
# 1차원 배열 생성 - 간단한 숫자 리스트
arr1d = np.array([1, 2, 3, 4, 5])
print("1차원 배열:", arr1d)
# 2차원 배열 생성 - 표 형태의 데이터
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
print("2차원 배열:\n", arr2d)
# 특수 배열 생성 - 자주 사용하는 패턴
zeros = np.zeros((3, 3)) # 0으로 채워진 3x3 배열
ones = np.ones((2, 4)) # 1로 채워진 2x4 배열
range_arr = np.arange(0, 10, 2) # 0부터 10까지 2씩 증가
# 기본 연산 - 배열 전체에 한 번에 적용됨
result = arr1d * 2 # 모든 원소를 2배로
print("2배 결과:", result)
설명
이것이 하는 일: NumPy 배열은 같은 타입의 데이터를 연속된 메모리 공간에 저장하여 빠른 계산을 가능하게 합니다. 마치 책을 한 권씩 여기저기 흩어놓는 게 아니라 책장에 순서대로 정리해두면 찾기 쉬운 것과 같은 원리입니다.
첫 번째로, np.array()를 사용해서 Python 리스트를 NumPy 배열로 변환합니다. 이 과정에서 NumPy는 모든 원소의 타입을 확인하고 가장 적절한 데이터 타입을 자동으로 선택합니다.
1차원 배열은 단순한 숫자 리스트이고, 2차원 배열은 엑셀의 표처럼 행과 열로 구성된 데이터를 표현합니다. 그 다음으로, zeros(), ones(), arange() 같은 특수 생성 함수들이 실행되면서 자주 사용하는 패턴의 배열을 빠르게 만들어줍니다.
zeros()는 머신러닝에서 가중치를 초기화할 때, ones()는 마스크를 만들 때, arange()는 그래프의 x축 값을 만들 때 주로 사용됩니다. 세 번째로, 배열에 직접 연산을 적용하면 for 문 없이도 모든 원소에 자동으로 적용됩니다.
arr1d * 2는 내부적으로 최적화된 C 코드가 실행되어 Python for 문보다 훨씬 빠릅니다. 이게 바로 NumPy의 핵심 장점인 '벡터화'입니다.
마지막으로, 배열의 shape(모양)을 확인하고 변경할 수 있습니다. 최종적으로 여러분은 데이터를 원하는 형태로 자유롭게 변환하면서도 성능 저하 없이 빠르게 계산할 수 있게 됩니다.
여러분이 이 코드를 사용하면 대량의 데이터를 처리할 때 시간을 획기적으로 줄일 수 있습니다. 백만 개의 숫자를 처리하는 데 Python 리스트로는 1초 걸릴 일을 NumPy로는 0.01초에 끝낼 수 있어요.
또한 코드가 훨씬 간결해지고 읽기 쉬워집니다.
실전 팁
💡 배열을 만들 때는 항상 dtype(데이터 타입)을 명시하세요. np.array([1,2,3], dtype=np.float32)처럼 하면 메모리 사용량을 절반으로 줄일 수 있습니다.
💡 큰 배열을 다룰 때는 zeros()나 empty()로 먼저 공간을 확보한 후 값을 채우는 게 append()보다 훨씬 빠릅니다.
💡 배열의 shape를 확인하는 습관을 들이세요. print(arr.shape)로 차원을 확인하면 디버깅이 쉬워집니다.
💡 Python 리스트와 NumPy 배열을 섞어 쓰지 마세요. 처음부터 NumPy 배열로 시작하면 성능이 좋습니다.
💡 np.random.seed(42)를 사용해서 랜덤 배열을 재현 가능하게 만들면 디버깅과 테스트가 훨씬 쉬워집니다.
2. 브로드캐스팅으로 차원 다른 배열 연산하기
시작하며
여러분이 100명의 학생 시험 점수가 있고, 모든 학생에게 보너스 5점을 주려고 한다고 생각해보세요. 일반적으로는 100번 반복해야 하지만, NumPy는 그냥 한 번에 처리해줍니다.
이런 상황은 실제 데이터 분석에서 정말 자주 발생합니다. 이미지 처리에서 모든 픽셀에 같은 값을 더하거나, 데이터셋 전체를 정규화하거나, 통계 계산을 할 때마다 이런 패턴이 나타납니다.
크기가 다른 배열끼리 연산하려고 하면 에러가 나거나 복잡한 반복문을 써야 했습니다. 바로 이럴 때 필요한 것이 브로드캐스팅입니다.
브로드캐스팅은 크기가 다른 배열들을 자동으로 맞춰서 연산할 수 있게 해주는 NumPy의 마법 같은 기능입니다.
개요
간단히 말해서, 브로드캐스팅은 작은 배열을 자동으로 확장해서 큰 배열과 계산할 수 있게 해주는 기능입니다. 브로드캐스팅이 필요한 이유는 실제 데이터 분석에서 차원이 다른 데이터를 계속 다루기 때문입니다.
예를 들어, 각 학생의 과목별 점수 테이블(2차원)에서 각 과목의 평균(1차원)을 빼서 편차를 구하는 경우가 많습니다. 브로드캐스팅 없이는 이걸 하려면 복잡한 반복문을 작성해야 하지만, 브로드캐스팅을 쓰면 한 줄이면 됩니다.
기존에는 배열의 크기를 수동으로 맞춰줘야 했다면, 이제는 NumPy가 자동으로 규칙에 따라 확장해줍니다. 메모리도 실제로 복사하는 게 아니라 논리적으로만 확장하기 때문에 효율적입니다.
브로드캐스팅의 핵심 특징은 두 가지입니다. 첫째, 차원의 크기가 1이거나 같으면 자동으로 맞춰집니다.
둘째, 실제로 메모리를 복사하지 않아서 빠릅니다. 이러한 특징들이 복잡한 수학 연산을 간단한 코드로 표현할 수 있게 해줍니다.
코드 예제
import numpy as np
# 서로 다른 차원의 배열 준비
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]) # 3x3 행렬
row_vector = np.array([10, 20, 30]) # 1차원 배열 (크기 3)
col_vector = np.array([[1], [2], [3]]) # 2차원 배열 (3x1)
# 브로드캐스팅 - 행 벡터가 자동으로 3행으로 확장됨
result1 = matrix + row_vector
print("행 브로드캐스팅:\n", result1)
# 브로드캐스팅 - 열 벡터가 자동으로 3열로 확장됨
result2 = matrix + col_vector
print("열 브로드캐스팅:\n", result2)
# 스칼라 브로드캐스팅 - 가장 간단한 형태
result3 = matrix * 2 # 2가 모든 원소에 곱해짐
설명
이것이 하는 일: 브로드캐스팅은 차원이 다른 배열들을 연산할 때 작은 배열을 큰 배열의 모양에 맞춰 논리적으로 반복시켜서 계산을 가능하게 합니다. 실제로는 메모리에 복사하지 않고 계산 시점에만 확장된 것처럼 동작합니다.
첫 번째로, matrix + row_vector 연산에서 NumPy는 row_vector의 shape (3,)을 확인하고 matrix의 shape (3, 3)과 비교합니다. 마지막 차원의 크기가 3으로 같으므로 row_vector를 3번 반복한 것처럼 동작합니다.
즉, [10, 20, 30]이 각 행마다 더해지는 것이죠. 그 다음으로, matrix + col_vector 연산이 실행되면서 col_vector의 shape (3, 1)이 (3, 3)으로 확장됩니다.
이번에는 각 열에 [1, 2, 3]이 더해집니다. 첫 번째 행에는 1이, 두 번째 행에는 2가, 세 번째 행에는 3이 더해지는 방식입니다.
세 번째로, 스칼라 값 2는 가장 작은 단위이므로 모든 원소에 브로드캐스팅됩니다. NumPy는 내부적으로 2를 matrix와 같은 모양의 배열로 확장한 것처럼 계산합니다.
이 모든 과정이 C 레벨에서 최적화되어 실행되기 때문에 Python 반복문보다 훨씬 빠릅니다. 마지막으로, 브로드캐스팅 규칙이 적용됩니다.
두 배열의 차원을 뒤에서부터 비교해서 각 차원의 크기가 같거나, 둘 중 하나가 1이면 브로드캐스팅이 가능합니다. 이 규칙만 이해하면 복잡한 다차원 배열도 쉽게 다룰 수 있습니다.
여러분이 이 코드를 사용하면 데이터 정규화, 중심화, 스케일링 같은 전처리 작업을 한 줄로 처리할 수 있습니다. 예를 들어 데이터셋에서 각 특징의 평균을 빼는 작업이 data - data.mean(axis=0) 한 줄이면 끝납니다.
코드도 간결해지고 실행 속도도 빠릅니다.
실전 팁
💡 브로드캐스팅이 잘 안 될 때는 reshape()나 np.newaxis를 사용해서 차원을 맞춰주세요. arr[:, np.newaxis]는 열 벡터로 만들어줍니다.
💡 디버깅할 때는 print(arr1.shape, arr2.shape)로 shape를 먼저 확인하세요. 브로드캐스팅 에러의 90%는 shape 불일치입니다.
💡 이미지 처리에서 RGB 채널별로 다른 연산을 할 때 브로드캐스팅이 엄청 유용합니다. shape를 잘 맞추면 반복문 없이 처리 가능합니다.
💡 브로드캐스팅은 메모리를 복사하지 않지만, 결과 배열은 큰 배열 크기로 생성됩니다. 메모리가 부족하면 청크 단위로 나눠서 처리하세요.
3. 벡터화로 반복문 제거하고 성능 극대화하기
시작하며
여러분이 백만 개의 데이터를 처리해야 하는데 for 문으로 돌리면 10초가 걸린다고 상상해보세요. 근데 같은 작업을 0.1초에 끝낼 수 있다면 어떨까요?
이런 성능 차이는 실제 프로젝트에서 엄청난 영향을 미칩니다. 머신러닝 모델 학습, 대용량 데이터 전처리, 실시간 데이터 분석 같은 작업에서는 100배의 속도 차이가 몇 시간과 몇 분의 차이로 나타납니다.
Python의 for 문은 편하지만 느립니다. 특히 숫자 계산에서는 치명적으로 느려요.
바로 이럴 때 필요한 것이 벡터화입니다. 벡터화는 Python 반복문을 NumPy의 최적화된 C 코드로 대체해서 성능을 극적으로 향상시키는 기법입니다.
개요
간단히 말해서, 벡터화는 for 문을 없애고 배열 전체에 한 번에 연산을 적용하는 프로그래밍 방식입니다. 벡터화가 필요한 이유는 Python의 반복문이 근본적으로 느리기 때문입니다.
Python은 모든 변수의 타입을 실행 시점에 확인하고, 각 연산마다 여러 검사를 합니다. 이건 유연성을 주지만 성능을 희생합니다.
예를 들어, 100만 개의 숫자를 제곱하는 작업에서 for 문은 100만 번 타입 체크를 하지만, NumPy 벡터화는 한 번만 체크합니다. 기존에는 for i in range(len(arr)): result[i] = arr[i] ** 2 같은 코드를 썼다면, 이제는 result = arr ** 2 한 줄이면 됩니다.
코드가 짧아질 뿐만 아니라 의도도 더 명확해집니다. 벡터화의 핵심 특징은 세 가지입니다.
첫째, NumPy의 유니버설 함수(ufunc)를 사용해서 C 속도로 실행됩니다. 둘째, SIMD(Single Instruction Multiple Data) 명령어를 활용해서 CPU가 여러 데이터를 동시에 처리합니다.
셋째, 메모리 접근 패턴이 최적화되어 캐시 효율이 높습니다. 이러한 특징들이 데이터 과학자들이 NumPy를 사랑하는 이유입니다.
코드 예제
import numpy as np
import time
# 백만 개의 랜덤 데이터 생성
data = np.random.rand(1000000)
# 방법 1: Python for 문 (느림)
start = time.time()
result_loop = []
for x in data:
result_loop.append(x ** 2 + 2 * x + 1)
time_loop = time.time() - start
# 방법 2: NumPy 벡터화 (빠름)
start = time.time()
result_vectorized = data ** 2 + 2 * data + 1
time_vectorized = time.time() - start
print(f"for 문: {time_loop:.4f}초")
print(f"벡터화: {time_vectorized:.4f}초")
print(f"속도 향상: {time_loop/time_vectorized:.1f}배")
# 조건부 연산도 벡터화 가능
result = np.where(data > 0.5, data * 2, data / 2)
설명
이것이 하는 일: 벡터화는 Python 인터프리터의 느린 반복문 대신 NumPy의 최적화된 C 코드로 배열 연산을 수행합니다. CPU의 SIMD 명령어를 활용해서 한 번에 여러 데이터를 처리하기 때문에 극적인 성능 향상이 가능합니다.
첫 번째로, for 문 방식은 각 원소마다 Python 인터프리터를 거쳐야 합니다. x ** 2 + 2 * x + 1을 계산할 때마다 타입 체크, 메모리 할당, 리스트 append 등의 오버헤드가 발생합니다.
100만 번 반복하면 이 오버헤드가 누적되어 엄청난 시간 손실이 됩니다. 그 다음으로, 벡터화 방식 data ** 2 + 2 * data + 1이 실행되면 NumPy는 이걸 하나의 연산으로 컴파일합니다.
내부적으로 C 레벨에서 메모리를 순차적으로 읽으면서 계산하고, CPU 캐시를 효율적으로 사용합니다. 타입 체크도 한 번만 하고 모든 연산이 최적화된 경로로 실행됩니다.
세 번째로, np.where() 같은 조건부 연산도 벡터화할 수 있습니다. if x > 0.5: x * 2 else: x / 2 같은 로직을 반복문 없이 한 줄로 처리합니다.
이건 데이터 클리닝이나 전처리에서 정말 자주 사용하는 패턴입니다. 마지막으로, 실행 시간을 측정해보면 보통 50~200배 정도의 성능 차이가 납니다.
이 차이는 데이터 크기가 커질수록 더 벌어집니다. 최종적으로 여러분은 같은 로직을 더 짧은 코드로, 훨씬 빠르게 실행할 수 있게 됩니다.
여러분이 이 코드를 사용하면 대규모 데이터 처리 파이프라인의 병목을 제거할 수 있습니다. 머신러닝에서 특징 엔지니어링, 이미지 처리에서 필터 적용, 금융 데이터에서 지표 계산 등 모든 곳에서 성능 향상을 경험할 수 있습니다.
실전 팁
💡 모든 NumPy 연산은 기본적으로 벡터화되어 있습니다. np.sin(), np.exp(), np.log() 같은 함수들도 배열 전체에 바로 적용하세요.
💡 pandas DataFrame도 내부적으로 NumPy를 쓰므로 .apply() 대신 벡터화 연산을 쓰면 훨씬 빠릅니다.
💡 복잡한 로직은 np.vectorize()로 함수를 벡터화할 수 있지만, 이건 for 문을 숨긴 것뿐이라 진짜 벡터화보다 느립니다. 가능하면 NumPy 내장 함수를 조합하세요.
💡 %%timeit 매직 명령어로 Jupyter에서 쉽게 성능을 측정할 수 있습니다. 최적화 전후를 비교해보세요.
💡 메모리가 부족하면 벡터화해도 소용없습니다. 큰 배열은 청크로 나눠서 처리하는 게 좋습니다.
4. 행렬 연산과 선형대수 실전 활용
시작하며
여러분이 추천 시스템을 만들려고 하는데, 사용자 1000명과 상품 5000개의 관계를 계산해야 한다고 생각해보세요. 이건 5백만 개의 계산이 필요한데, 어떻게 효율적으로 처리할까요?
이런 문제는 머신러닝, 데이터 마이닝, 컴퓨터 그래픽스 등 거의 모든 분야에서 나타납니다. 신경망의 가중치 업데이트, 이미지 변환, 주성분 분석(PCA) 등은 모두 행렬 연산입니다.
수학 공식을 코드로 옮기는 게 어렵고, 성능도 중요합니다. 바로 이럴 때 필요한 것이 NumPy의 행렬 연산 기능입니다.
NumPy는 선형대수 연산을 간단한 함수로 제공하고, 내부적으로는 최적화된 BLAS/LAPACK 라이브러리를 사용해서 엄청나게 빠릅니다.
개요
간단히 말해서, NumPy의 행렬 연산은 선형대수의 복잡한 계산을 간단한 함수 호출로 처리할 수 있게 해주는 기능입니다. 행렬 연산이 필요한 이유는 현대 데이터 과학의 거의 모든 알고리즘이 선형대수 기반이기 때문입니다.
머신러닝의 회귀 분석은 행렬 곱셈과 역행렬로 해결되고, 딥러닝의 신경망은 수많은 행렬 연산의 연속입니다. 예를 들어, 이미지를 회전시키는 것도 사실은 회전 행렬을 곱하는 것입니다.
기존에는 이중 for 문으로 행렬 곱셈을 구현했다면, 이제는 @ 연산자나 np.dot() 한 줄이면 됩니다. 성능 차이는 수백 배에서 수천 배까지 날 수 있습니다.
NumPy 선형대수의 핵심 특징은 세 가지입니다. 첫째, 수학 표기법과 거의 동일한 직관적인 문법을 제공합니다.
둘째, BLAS(Basic Linear Algebra Subprograms) 같은 고도로 최적화된 라이브러리를 내부에서 사용합니다. 셋째, GPU 가속을 지원하는 라이브러리들과 쉽게 연동됩니다.
이러한 특징들이 NumPy를 과학 계산의 표준으로 만들었습니다.
코드 예제
import numpy as np
# 행렬 생성
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# 행렬 곱셈 - @ 연산자 사용 (권장)
C = A @ B
print("행렬 곱셈 A @ B:\n", C)
# 전치 행렬 - 행과 열을 바꿈
A_T = A.T
print("전치 행렬:\n", A_T)
# 역행렬 - 연립방정식 풀기 등에 사용
A_inv = np.linalg.inv(A)
print("역행렬:\n", A_inv)
# 고유값과 고유벡터 - PCA 등에 필수
eigenvalues, eigenvectors = np.linalg.eig(A)
print("고유값:", eigenvalues)
# 행렬식 - 행렬의 스케일 변화 측정
det = np.linalg.det(A)
print("행렬식:", det)
# 연립방정식 풀기: Ax = b
b = np.array([1, 2])
x = np.linalg.solve(A, b)
print("해:", x)
설명
이것이 하는 일: NumPy의 선형대수 모듈(linalg)은 행렬 곱셈, 역행렬, 고유값 분해 등 수학적으로 복잡한 연산을 최적화된 알고리즘으로 제공합니다. 내부적으로 BLAS와 LAPACK 같은 업계 표준 라이브러리를 사용해서 최고 성능을 보장합니다.
첫 번째로, @ 연산자를 사용한 행렬 곱셈은 수학 표기법과 거의 동일해서 직관적입니다. A @ B는 A의 각 행과 B의 각 열을 내적(dot product)한 결과를 새로운 행렬로 만듭니다.
내부적으로는 캐시 최적화된 알고리즘이 실행되어 일반 for 문보다 수백 배 빠릅니다. 그 다음으로, .T 속성으로 전치 행렬을 구하는 것은 데이터의 뷰만 바꾸는 거라 메모리 복사가 일어나지 않습니다.
공분산 행렬을 계산할 때 X.T @ X 같은 패턴을 자주 쓰는데, 이게 매우 효율적인 이유입니다. 세 번째로, np.linalg.inv()로 역행렬을 구하면 가우스 소거법이나 LU 분해 같은 수치적으로 안정적인 알고리즘이 자동으로 선택됩니다.
하지만 실무에서는 역행렬을 직접 구하기보다 np.linalg.solve()를 쓰는 게 더 빠르고 정확합니다. 연립방정식 Ax=b를 풀 때 x = inv(A) @ b 대신 x = solve(A, b)를 쓰세요.
네 번째로, 고유값과 고유벡터는 주성분 분석(PCA), 그래프 알고리즘, 양자역학 등에서 핵심적입니다. eig() 함수는 수치적으로 안정적인 QR 알고리즘을 사용해서 정확한 결과를 제공합니다.
마지막으로, 행렬식은 행렬이 역행렬을 가지는지(det ≠ 0) 확인하거나, 선형 변환의 부피 변화를 측정할 때 사용합니다. 최종적으로 여러분은 수학 교과서의 공식을 거의 그대로 코드로 옮길 수 있게 됩니다.
여러분이 이 코드를 사용하면 머신러닝 알고리즘을 직접 구현할 수 있습니다. 선형 회귀의 정규 방정식 (X.T @ X)^-1 @ X.T @ y를 한 줄로 작성할 수 있고, PCA로 차원 축소를 할 수 있으며, 이미지 변환도 행렬 곱셈으로 빠르게 처리할 수 있습니다.
실전 팁
💡 역행렬을 직접 구하지 말고 solve()를 사용하세요. 수치적으로 더 안정적이고 2배 정도 빠릅니다.
💡 대칭 행렬의 고유값은 eigh()를 쓰세요. eig()보다 빠르고 정확합니다. 공분산 행렬은 항상 대칭이므로 eigh()가 적합합니다.
💡 큰 행렬의 곱셈은 @ 대신 np.einsum()을 쓰면 메모리를 절약할 수 있습니다. 복잡하지만 매우 강력합니다.
💡 행렬의 조건수(condition number)를 확인하세요. np.linalg.cond(A)가 크면 수치적으로 불안정해서 결과를 신뢰하기 어렵습니다.
💡 GPU를 쓰고 싶다면 CuPy를 사용하세요. NumPy와 거의 같은 API인데 CUDA로 실행됩니다.
5. 인덱싱과 슬라이싱으로 데이터 자유자재로 다루기
시작하며
여러분이 수천 개의 데이터 중에서 특정 조건을 만족하는 것만 골라내야 하는 상황을 생각해보세요. 예를 들어 "점수가 80점 이상인 학생만" 또는 "양수만" 선택해야 할 때요.
이런 데이터 필터링과 선택은 데이터 분석의 가장 기본적이면서도 중요한 작업입니다. 잘못된 데이터를 제거하거나, 특정 범위의 값만 분석하거나, 조건에 맞는 데이터를 추출하는 건 매일 하는 일입니다.
Python 리스트로 이걸 하려면 복잡한 리스트 컴프리헨션이나 filter 함수를 써야 합니다. 바로 이럴 때 필요한 것이 NumPy의 고급 인덱싱입니다.
NumPy는 불린 인덱싱, 팬시 인덱싱 등 강력한 선택 기능을 제공해서 원하는 데이터를 쉽고 빠르게 골라낼 수 있게 해줍니다.
개요
간단히 말해서, NumPy 인덱싱은 배열에서 원하는 데이터를 선택하고 수정하는 다양한 방법을 제공하는 기능입니다. 인덱싱이 중요한 이유는 실제 데이터 분석에서 전체 데이터를 다 쓰는 경우가 거의 없기 때문입니다.
이상치 제거, 특정 기간 데이터 선택, 조건부 변환 등 대부분의 작업이 부분 선택과 관련됩니다. 예를 들어, 주가 데이터에서 "거래량이 평균 이상인 날만" 분석하고 싶을 때 불린 인덱싱이 완벽한 해결책입니다.
기존에는 [x for x in arr if x > 0] 같은 리스트 컴프리헨션을 썼다면, 이제는 arr[arr > 0] 한 줄이면 됩니다. 더 읽기 쉽고 훨씬 빠릅니다.
NumPy 인덱싱의 핵심 특징은 네 가지입니다. 첫째, 기본 슬라이싱은 뷰를 반환해서 메모리를 절약합니다.
둘째, 불린 인덱싱으로 조건에 맞는 원소만 선택할 수 있습니다. 셋째, 팬시 인덱싱으로 임의의 위치에 있는 원소들을 선택할 수 있습니다.
넷째, 이 모든 방법을 조합해서 복잡한 선택도 간단히 표현할 수 있습니다.
코드 예제
import numpy as np
# 2차원 배열 생성
data = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# 기본 슬라이싱 - 뷰를 반환 (메모리 공유)
subset = data[0:2, 1:3] # 첫 2행, 두 번째~세 번째 열
print("슬라이싱:\n", subset)
# 불린 인덱싱 - 조건에 맞는 원소만 선택
mask = data > 6
print("마스크:\n", mask)
filtered = data[mask] # 6보다 큰 값만
print("필터링:", filtered)
# 팬시 인덱싱 - 특정 인덱스들 선택
rows = np.array([0, 2])
cols = np.array([1, 3])
selected = data[rows, cols] # (0,1)과 (2,3) 위치
print("팬시 인덱싱:", selected)
# 조건부 수정 - 조건에 맞는 값만 변경
data_copy = data.copy()
data_copy[data_copy < 5] = 0 # 5 미만은 0으로
print("조건부 수정:\n", data_copy)
설명
이것이 하는 일: NumPy 인덱싱은 다양한 방법으로 배열의 부분을 선택하고 조작할 수 있게 합니다. 기본 슬라이싱은 메모리를 공유하는 뷰를 반환하고, 불린/팬시 인덱싱은 조건이나 인덱스 배열로 원하는 원소들을 선택합니다.
첫 번째로, 기본 슬라이싱 data[0:2, 1:3]은 Python 리스트 슬라이싱과 비슷하지만 다차원을 지원합니다. 중요한 점은 이게 뷰를 반환한다는 겁니다.
즉, 원본 배열과 메모리를 공유해서 subset[0, 0] = 999를 하면 원본도 바뀝니다. 메모리를 절약할 수 있지만 의도치 않은 수정을 조심해야 합니다.
그 다음으로, 불린 인덱싱 data[data > 6]이 실행되면 먼저 data > 6이 같은 shape의 불린 배열을 만듭니다. 그 다음 True인 위치의 값만 선택해서 1차원 배열로 반환합니다.
이건 데이터 클리닝에서 정말 많이 쓰는 패턴입니다. 예를 들어 data[np.isnan(data)] = 0으로 NaN 값을 제거할 수 있습니다.
세 번째로, 팬시 인덱싱 data[rows, cols]는 정수 배열로 인덱스를 지정합니다. rows와 cols가 같은 길이면 (rows[0], cols[0]), (rows[1], cols[1]) 위치의 값들을 선택합니다.
이건 랜덤 샘플링이나 특정 패턴의 데이터를 뽑을 때 유용합니다. 네 번째로, 조건부 수정 data[data < 5] = 0은 선택과 할당을 한 번에 합니다.
조건에 맞는 모든 원소를 한 번에 바꿀 수 있어서 이상치 처리, 데이터 클리핑 등에 매우 편리합니다. 마지막으로, 이 방법들을 조합할 수 있습니다.
예를 들어 data[data[:, 0] > 5, :]는 "첫 번째 열 값이 5보다 큰 행 전체"를 선택합니다. 최종적으로 여러분은 SQL의 WHERE절처럼 복잡한 조건으로 데이터를 필터링할 수 있게 됩니다.
여러분이 이 코드를 사용하면 데이터 전처리가 훨씬 쉬워집니다. 이상치 제거, 결측치 처리, 특정 범위 데이터 선택 등이 한두 줄로 끝나고, pandas DataFrame과 결합하면 더욱 강력해집니다.
실전 팁
💡 슬라이싱이 뷰를 반환한다는 걸 꼭 기억하세요. 독립적인 복사본이 필요하면 .copy()를 명시적으로 호출하세요.
💡 복잡한 조건은 &(and), |(or), ~(not)로 조합할 수 있습니다. 단, and/or 키워드가 아니라 비트 연산자를 써야 하고 괄호로 감싸야 합니다: data[(data > 5) & (data < 10)]
💡 np.where(condition, x, y)는 조건에 따라 다른 값을 할당할 때 유용합니다. if-else의 벡터화 버전입니다.
💡 다차원 배열에서 특정 축만 선택하려면 :를 쓰세요. data[:, 0]은 모든 행의 첫 번째 열입니다.
💡 팬시 인덱싱은 복사본을 반환합니다. 원본을 수정하려면 인덱싱된 결과에 직접 할당하세요.
6. 배열 연결과 분할로 데이터 재구성하기
시작하며
여러분이 여러 개의 데이터 파일을 읽어서 하나로 합치거나, 큰 데이터를 학습용과 테스트용으로 나눠야 하는 상황을 생각해보세요. 이건 데이터 분석에서 매일 하는 작업입니다.
이런 데이터 재구성 작업은 실제 프로젝트에서 정말 자주 일어납니다. 여러 센서의 데이터를 합치거나, 시계열 데이터를 시간 윈도우로 나누거나, 배치 처리를 위해 데이터를 청크로 분할하는 등의 작업이 계속 필요합니다.
Python 리스트의 + 연산자나 슬라이싱으로도 가능하지만 비효율적이고 코드가 복잡해집니다. 바로 이럴 때 필요한 것이 NumPy의 배열 연결(concatenate)과 분할(split) 함수들입니다.
이 함수들은 효율적으로 배열을 합치고 나누는 다양한 방법을 제공합니다.
개요
간단히 말해서, NumPy의 배열 조작 함수들은 여러 배열을 하나로 합치거나 하나의 배열을 여러 개로 나누는 기능을 제공합니다. 배열 조작이 필요한 이유는 데이터가 항상 우리가 원하는 형태로 오지 않기 때문입니다.
여러 소스에서 온 데이터를 합쳐야 하고, 큰 데이터는 처리 가능한 크기로 나눠야 합니다. 예를 들어, 머신러닝에서 데이터를 학습/검증/테스트 세트로 나누는 건 기본 중의 기본입니다.
기존에는 여러 리스트를 list1 + list2 + list3로 합쳤다면, 이제는 np.concatenate([arr1, arr2, arr3])로 한 번에 처리합니다. 메모리도 효율적으로 할당되고 속도도 빠릅니다.
NumPy 배열 조작의 핵심 특징은 세 가지입니다. 첫째, 여러 축(axis)을 따라 자유롭게 합치고 나눌 수 있습니다.
둘째, vstack, hstack 같은 편의 함수로 직관적으로 작업할 수 있습니다. 셋째, 원본 데이터를 복사하지 않고 뷰로 작업할 수 있어 메모리 효율적입니다.
이러한 특징들이 대용량 데이터 처리를 가능하게 합니다.
코드 예제
import numpy as np
# 여러 배열 준비
arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[5, 6], [7, 8]])
arr3 = np.array([[9, 10]])
# 수직 연결 (행 추가) - vstack 또는 concatenate
vertical = np.vstack([arr1, arr2]) # 행 방향으로 쌓기
print("수직 연결:\n", vertical)
# 수평 연결 (열 추가) - hstack
horizontal = np.hstack([arr1, arr2]) # 열 방향으로 연결
print("수평 연결:\n", horizontal)
# axis 지정 연결 - 더 유연함
concat_axis0 = np.concatenate([arr1, arr2], axis=0) # 행 방향
concat_axis1 = np.concatenate([arr1, arr2], axis=1) # 열 방향
# 배열 분할 - 균등하게 나누기
data = np.arange(12).reshape(3, 4)
split_data = np.split(data, 3, axis=0) # 3개로 나누기
print("분할 결과:", [x.shape for x in split_data])
# 불균등 분할 - 특정 인덱스에서 나누기
parts = np.array_split(data, [1, 3], axis=0) # 인덱스 1, 3에서 분할
print("불균등 분할:", len(parts), "개")
설명
이것이 하는 일: NumPy의 배열 조작 함수들은 메모리 효율적으로 배열들을 결합하거나 분리합니다. 내부적으로 새로운 메모리를 할당하고 데이터를 복사하되, 가능한 한 효율적인 방법을 사용합니다.
첫 번째로, vstack()은 배열들을 수직으로 쌓습니다. 각 배열의 열 개수가 같아야 하고, 결과는 행이 늘어난 배열입니다.
마치 엑셀에서 여러 시트의 데이터를 하나로 합치는 것과 비슷합니다. 시계열 데이터를 여러 파일에서 읽어서 하나로 합칠 때 자주 씁니다.
그 다음으로, hstack()은 배열들을 수평으로 연결합니다. 행 개수가 같아야 하고, 결과는 열이 늘어난 배열입니다.
여러 특징(feature)을 하나의 데이터셋으로 합칠 때 유용합니다. 예를 들어 이미지와 메타데이터를 합치는 경우입니다.
세 번째로, concatenate()는 더 일반적인 함수로 axis 매개변수로 어느 방향으로 합칠지 지정합니다. axis=0은 행 방향(vstack과 같음), axis=1은 열 방향(hstack과 같음)입니다.
3차원 이상의 배열을 다룰 때는 이게 필수입니다. 네 번째로, split()은 배열을 균등하게 나눕니다.
np.split(data, 3, axis=0)은 데이터를 3등분합니다. 크기가 딱 나누어떨어지지 않으면 에러가 나므로 주의해야 합니다.
배치 처리나 교차 검증에서 데이터를 나눌 때 사용합니다. 다섯 번째로, array_split()은 불균등하게도 나눌 수 있습니다.
크기가 나누어떨어지지 않아도 가능한 한 균등하게 분할합니다. 또는 인덱스 배열을 주면 그 위치에서 나눕니다.
마지막으로, reshape()와 결합하면 더 복잡한 변형도 가능합니다. 최종적으로 여러분은 데이터를 원하는 형태로 자유자재로 변형할 수 있게 됩니다.
여러분이 이 코드를 사용하면 데이터 전처리 파이프라인을 쉽게 만들 수 있습니다. 여러 CSV 파일을 하나로 합치거나, 데이터를 학습/테스트로 나누거나, 시계열을 윈도우로 분할하는 등의 작업이 몇 줄이면 됩니다.
실전 팁
💡 np.r_[]와 np.c_[]는 간편한 연결 문법을 제공합니다. np.r_[arr1, arr2]는 vstack, np.c_[arr1, arr2]는 hstack과 비슷합니다.
💡 큰 배열을 합칠 때는 미리 크기를 계산해서 np.empty()로 공간을 확보한 후 채우는 게 여러 번 연결하는 것보다 빠릅니다.
💡 np.tile()과 np.repeat()로 배열을 반복시킬 수 있습니다. tile은 전체를 반복, repeat은 각 원소를 반복합니다.
💡 머신러닝에서 데이터 분할할 때는 train_test_split() 같은 sklearn 함수를 쓰는 게 더 편합니다. 내부적으로 NumPy를 사용합니다.
💡 메모리가 부족하면 연결 대신 제너레이터나 메모리 맵을 고려하세요. 수십 GB 데이터는 한 번에 메모리에 올리지 마세요.
7. 유니버설 함수로 빠른 수학 연산 수행하기
시작하며
여러분이 백만 개의 숫자에 제곱근을 구해야 하는데, Python의 math.sqrt()를 for 문으로 돌리면 너무 느립니다. 더 빠른 방법이 없을까요?
이런 수학 함수 적용은 과학 계산과 데이터 분석의 기본입니다. 로그 변환, 삼각함수, 지수함수 등을 대량의 데이터에 적용하는 건 통계 분석, 신호 처리, 이미지 처리에서 매일 하는 일입니다.
Python 표준 라이브러리의 math 모듈은 하나씩 계산하도록 설계되어서 배열 전체에 적용하기 비효율적입니다. 바로 이럴 때 필요한 것이 NumPy의 유니버설 함수(ufunc)입니다.
ufunc는 배열의 모든 원소에 빠르게 함수를 적용하도록 최적화된 특별한 함수들입니다.
개요
간단히 말해서, 유니버설 함수는 NumPy 배열의 각 원소에 최적화된 방식으로 연산을 수행하는 벡터화된 함수입니다. 유니버설 함수가 필요한 이유는 수학 연산을 빠르게 하기 위해서입니다.
NumPy의 np.sqrt(), np.sin(), np.log() 같은 함수들은 Python의 math 모듈 함수보다 10배에서 100배 빠릅니다. 예를 들어, 이미지의 모든 픽셀에 감마 보정을 적용하려면 pixel ** 2.2 같은 거듭제곱 연산을 수백만 번 해야 하는데, ufunc를 쓰면 실시간으로 가능합니다.
기존에는 [math.sqrt(x) for x in data] 같은 리스트 컴프리헨션을 썼다면, 이제는 np.sqrt(data) 한 줄이면 됩니다. 코드도 간단하고 훨씬 빠릅니다.
유니버설 함수의 핵심 특징은 네 가지입니다. 첫째, C로 구현되어 Python 오버헤드가 없습니다.
둘째, 브로드캐스팅을 자동으로 지원합니다. 셋째, 여러 배열에 동시에 적용되는 이항 연산도 지원합니다.
넷째, out 매개변수로 결과를 기존 배열에 저장해서 메모리를 절약할 수 있습니다. 이러한 특징들이 NumPy를 과학 계산의 표준으로 만들었습니다.
코드 예제
import numpy as np
# 1백만 개의 랜덤 데이터
data = np.random.rand(1000000)
# 기본 수학 함수들 - 모두 벡터화됨
sqrt_data = np.sqrt(data) # 제곱근
log_data = np.log(data + 1) # 자연로그 (0 방지)
exp_data = np.exp(data) # 지수
power_data = np.power(data, 2.5) # 거듭제곱
# 삼각함수
angles = np.linspace(0, 2*np.pi, 100)
sin_vals = np.sin(angles)
cos_vals = np.cos(angles)
# 비교 연산도 ufunc
positive = data > 0.5 # 불린 배열 반환
# 집계 함수 - reduce 패턴
total = np.add.reduce(data) # sum과 같음
product = np.multiply.reduce(data) # prod와 같음
# out 매개변수로 메모리 절약
result = np.empty_like(data)
np.sqrt(data, out=result) # 결과를 result에 직접 저장
# 사용자 정의 ufunc 만들기
def custom_func(x):
return x ** 2 + 2 * x + 1
vectorized = np.vectorize(custom_func)
result = vectorized(data)
설명
이것이 하는 일: 유니버설 함수는 배열의 각 원소에 동일한 연산을 C 레벨에서 빠르게 적용합니다. Python 반복문의 오버헤드 없이 CPU의 벡터 연산 기능을 활용해서 최고 성능을 냅니다.
첫 번째로, np.sqrt(data) 같은 기본 수학 함수들은 모두 ufunc로 구현되어 있습니다. 내부적으로 C 코드가 실행되고 SIMD 명령어를 사용할 수 있어서 Python의 math.sqrt()보다 훨씬 빠릅니다.
백만 개 원소에 대해 Python for 문으로는 1초 걸릴 작업이 0.01초에 끝납니다. 그 다음으로, 삼각함수, 지수/로그 함수 등 거의 모든 수학 함수가 ufunc로 제공됩니다.
np.sin(), np.cos(), np.exp(), np.log() 등은 모두 배열 전체에 바로 적용할 수 있습니다. 신호 처리나 과학 계산에서 이런 함수들을 계속 쓰는데, NumPy 덕분에 성능 걱정 없이 쓸 수 있습니다.
세 번째로, 비교 연산자(>, <, == 등)도 ufunc입니다. data > 0.5는 같은 shape의 불린 배열을 반환하고, 이걸 바로 불린 인덱싱에 사용할 수 있습니다.
이게 데이터 필터링이 쉬운 이유입니다. 네 번째로, ufunc의 reduce() 메서드는 배열을 하나의 값으로 줄입니다.
np.add.reduce()는 모든 원소를 더하고(sum), np.multiply.reduce()는 곱합니다(prod). 이건 맵리듀스 패턴의 기본입니다.
다섯 번째로, out 매개변수를 쓰면 결과를 새 배열에 저장하지 않고 기존 배열을 재사용합니다. 메모리가 부족한 상황에서 매우 유용합니다.
np.sqrt(data, out=data)는 원본을 제곱근으로 교체합니다. 마지막으로, np.vectorize()로 일반 Python 함수를 ufunc처럼 사용할 수 있습니다.
하지만 이건 for 문을 숨긴 것뿐이라 진짜 ufunc보다 느립니다. 가능하면 NumPy 내장 함수를 조합해서 쓰세요.
여러분이 이 코드를 사용하면 복잡한 수학 공식을 코드 한 줄로 표현할 수 있습니다. 예를 들어 정규분포 확률밀도함수 (1/sqrt(2*pi)) * exp(-x**2/2)를 (1/np.sqrt(2*np.pi)) * np.exp(-x**2/2)로 바로 쓸 수 있고, 배열 전체에 즉시 적용됩니다.
실전 팁
💡 np.log1p(x)는 np.log(1+x)보다 정확합니다. 작은 x에 대해 수치 오차가 적어요. 비슷하게 np.expm1(x)는 np.exp(x)-1의 정확한 버전입니다.
💡 각도를 라디안으로 변환할 때는 np.deg2rad()를, 반대는 np.rad2deg()를 쓰세요. * np.pi / 180 같은 계산을 직접 할 필요 없습니다.
💡 np.clip(arr, min, max)로 값의 범위를 제한할 수 있습니다. 이미지 픽셀 값을 0-255로 제한할 때 유용합니다.
💡 np.isnan(), np.isinf()로 잘못된 값을 찾을 수 있습니다. 계산 결과를 항상 검증하세요.
💡 복잡한 수식은 중간 결과를 저장하지 말고 한 번에 쓰세요. NumPy가 자동으로 최적화합니다.
8. 배열 형태 변환으로 데이터 차원 조작하기
시작하며
여러분이 이미지 데이터를 딥러닝 모델에 넣으려고 하는데, 모델은 (배치, 높이, 너비, 채널) 형태를 원하는데 데이터는 (높이, 너비, 채널) 형태라면 어떻게 할까요? 이런 shape 불일치 문제는 데이터 과학에서 정말 자주 발생합니다.
딥러닝 모델, NumPy 함수, pandas DataFrame 등 각각 다른 형태의 데이터를 기대하기 때문에 계속 변환해야 합니다. 예를 들어 시계열 데이터를 2D 이미지처럼 변환해서 CNN에 넣거나, 3D 데이터를 평평하게 펴서 전통적인 머신러닝 알고리즘에 넣는 경우가 많습니다.
바로 이럴 때 필요한 것이 NumPy의 배열 형태 변환 함수들입니다. reshape(), transpose(), expand_dims() 등으로 데이터의 논리적 구조를 바꿀 수 있습니다.
개요
간단히 말해서, 배열 형태 변환은 데이터의 내용은 그대로 두고 차원과 shape만 바꾸는 기능입니다. 형태 변환이 필요한 이유는 각 알고리즘과 라이브러리가 특정한 형태의 입력을 기대하기 때문입니다.
scikit-learn은 2D 배열(샘플 x 특징)을 원하고, Keras는 4D 배열(배치 x 높이 x 너비 x 채널)을 원하며, matplotlib은 특정 형태의 배열만 그려줍니다. 예를 들어 MNIST 이미지 데이터는 (60000, 28, 28)인데 이걸 CNN에 넣으려면 (60000, 28, 28, 1)로 바꿔야 합니다.
기존에는 for 문으로 데이터를 재배치했다면, 이제는 reshape() 한 줄이면 됩니다. 중요한 건 실제로 데이터를 복사하지 않고 메모리 뷰만 바꾸는 경우가 많아서 빠르고 효율적입니다.
배열 형태 변환의 핵심 특징은 네 가지입니다. 첫째, 원소의 총 개수는 유지되어야 합니다(12개 원소를 3x4나 2x6으로는 가능하지만 5x5로는 불가능).
둘째, 가능하면 뷰를 반환해서 메모리를 절약합니다. 셋째, -1을 사용해서 자동으로 크기를 계산할 수 있습니다.
넷째, transpose()로 축의 순서를 바꿀 수 있습니다. 이러한 특징들이 유연한 데이터 처리를 가능하게 합니다.
코드 예제
import numpy as np
# 1차원 배열 생성
data = np.arange(12)
print("원본:", data.shape)
# reshape - 형태 변경
reshaped = data.reshape(3, 4) # 3x4 행렬로
print("3x4 reshape:\n", reshaped)
# -1 사용 - 자동 계산
auto_shaped = data.reshape(2, -1) # 2행, 열은 자동(6)
print("자동 reshape:", auto_shaped.shape)
# 3차원으로 변환
tensor = data.reshape(2, 2, 3)
print("3D 텐서:", tensor.shape)
# transpose - 축 순서 바꾸기
matrix = np.array([[1, 2, 3], [4, 5, 6]])
transposed = matrix.T # 행과 열 바꾸기
print("전치:\n", transposed)
# 차원 추가 - expand_dims
image = np.random.rand(28, 28) # 2D 이미지
batched = np.expand_dims(image, axis=0) # 배치 차원 추가
print("배치 추가:", batched.shape) # (1, 28, 28)
# 차원 제거 - squeeze
single_batch = np.random.rand(1, 28, 28, 1)
squeezed = np.squeeze(single_batch) # 크기 1인 차원 제거
print("squeeze:", squeezed.shape) # (28, 28)
# flatten - 1차원으로 평평하게
flat = reshaped.flatten()
print("flatten:", flat.shape)
설명
이것이 하는 일: 배열 형태 변환은 메모리에 저장된 데이터의 순서는 그대로 두고, NumPy가 그 데이터를 해석하는 방식만 바꿉니다. 대부분의 경우 실제 복사 없이 뷰만 생성해서 매우 빠릅니다.
첫 번째로, reshape(3, 4)는 12개의 원소를 3행 4열로 재배치합니다. NumPy는 C 순서(row-major)로 데이터를 저장하므로 첫 4개가 첫 번째 행, 다음 4개가 두 번째 행이 됩니다.
원소의 총 개수가 맞지 않으면 에러가 납니다. reshape()는 보통 뷰를 반환하지만, 연속되지 않은 메모리면 복사본을 만듭니다.
그 다음으로, -1을 사용하면 NumPy가 자동으로 크기를 계산합니다. reshape(2, -1)은 "2행으로 만들고 열은 알아서 계산해줘"라는 의미입니다.
12개 원소를 2행으로 나누면 자동으로 6열이 됩니다. 이건 한 차원만 확실하고 나머지는 유동적일 때 편리합니다.
세 번째로, 3차원 이상의 텐서로 변환할 수 있습니다. reshape(2, 2, 3)은 (2, 2, 3) shape의 3D 배열을 만듭니다.
딥러닝에서 이미지는 (배치, 높이, 너비, 채널)의 4D 텐서로 다뤄지므로 이런 변환이 필수입니다. 네 번째로, transpose() 또는 .T는 축의 순서를 바꿉니다.
2D 배열에서는 행과 열을 바꾸고, 3D 이상에서는 transpose((2, 0, 1)) 처럼 축 순서를 명시할 수 있습니다. 이미지 처리에서 (높이, 너비, 채널)을 (채널, 높이, 너비)로 바꿀 때 자주 씁니다.
다섯 번째로, expand_dims()는 크기 1인 차원을 추가합니다. 단일 이미지를 배치로 만들 때 유용합니다.
squeeze()는 반대로 크기 1인 차원을 제거합니다. 마지막으로, flatten()과 ravel()은 다차원 배열을 1차원으로 만듭니다.
ravel()은 가능하면 뷰를 반환하고 flatten()은 항상 복사본을 만듭니다. 최종적으로 여러분은 데이터를 어떤 형태로든 변환할 수 있게 됩니다.
여러분이 이 코드를 사용하면 다양한 머신러닝 라이브러리 간 데이터를 쉽게 주고받을 수 있습니다. scikit-learn에서 학습한 모델의 출력을 이미지로 시각화하거나, pandas DataFrame을 딥러닝 모델에 넣거나, 3D 데이터를 2D로 펼쳐서 분석하는 등 모든 게 가능해집니다.
실전 팁
💡 reshape()가 에러 나면 np.ascontiguousarray()로 메모리를 연속되게 만든 후 다시 시도하세요.
💡 -1은 최대 한 번만 사용할 수 있습니다. reshape(-1, -1)은 불가능합니다.
💡 이미지 배치는 항상 첫 번째 차원이 배치 크기입니다. expand_dims(image, axis=0)로 배치 차원을 추가하세요.
💡 np.newaxis를 슬라이싱과 함께 쓰면 expand_dims()보다 간결합니다. image[np.newaxis, ...]는 expand_dims(image, 0)과 같습니다.
💡 reshape()와 transpose()를 섞어 쓸 때는 순서가 중요합니다. 원하는 결과를 얻으려면 shape를 계속 확인하세요.
9. 통계 함수로 데이터 분석하기
시작하며
여러분이 학생들의 시험 점수 데이터를 분석해서 평균, 표준편차, 중앙값을 구해야 한다고 생각해보세요. 이건 데이터 분석의 가장 기본적인 작업입니다.
이런 통계 계산은 데이터를 이해하는 첫 단계입니다. 데이터의 중심 경향, 분산, 분포를 파악해야 이상치를 찾고, 패턴을 발견하고, 가설을 검증할 수 있습니다.
예를 들어 센서 데이터에서 평균을 벗어난 값을 찾아내거나, 주가의 변동성을 측정하거나, A/B 테스트 결과를 비교하는 모든 작업이 통계 함수 기반입니다. 바로 이럴 때 필요한 것이 NumPy의 통계 함수들입니다.
mean(), std(), percentile() 등으로 데이터의 특성을 빠르게 파악할 수 있습니다.
개요
간단히 말해서, NumPy 통계 함수는 배열의 기술통계량(descriptive statistics)을 빠르게 계산하는 함수들입니다. 통계 함수가 필요한 이유는 숫자 더미만 봐서는 데이터를 이해할 수 없기 때문입니다.
1000개의 숫자가 있을 때 평균이 얼마인지, 얼마나 퍼져있는지, 이상치가 있는지 등을 알아야 의미 있는 분석이 가능합니다. 예를 들어 고객 구매 금액의 평균은 100달러인데 중앙값이 20달러라면, 소수의 고액 구매자가 평균을 끌어올리는 것을 알 수 있습니다.
기존에는 for 문으로 합을 구하고 개수로 나눠서 평균을 계산했다면, 이제는 np.mean() 한 줄이면 됩니다. 더 정확하고 빠르며 코드의 의도도 명확합니다.
NumPy 통계 함수의 핵심 특징은 네 가지입니다. 첫째, axis 매개변수로 특정 축을 따라 계산할 수 있습니다.
둘째, NaN 값을 무시하는 버전(nanmean, nanstd 등)을 제공합니다. 셋째, 가중치를 적용한 계산도 가능합니다.
넷째, 최적화된 알고리즘으로 수치적으로 안정적입니다. 이러한 특징들이 데이터 분석을 쉽고 정확하게 만들어줍니다.
코드 예제
import numpy as np
# 샘플 데이터 - 학생들의 과목별 점수
scores = np.array([[85, 90, 78],
[92, 88, 95],
[78, 85, 82],
[95, 92, 88]])
# 기본 통계량
mean_score = np.mean(scores) # 전체 평균
median_score = np.median(scores) # 중앙값
std_score = np.std(scores) # 표준편차
var_score = np.var(scores) # 분산
print(f"평균: {mean_score:.2f}, 중앙값: {median_score:.2f}")
print(f"표준편차: {std_score:.2f}")
# 축을 따라 계산 - axis 매개변수
student_avg = np.mean(scores, axis=1) # 각 학생의 평균
subject_avg = np.mean(scores, axis=0) # 각 과목의 평균
print("학생별 평균:", student_avg)
print("과목별 평균:", subject_avg)
# 백분위수 - 데이터 분포 파악
percentiles = np.percentile(scores, [25, 50, 75])
print("25%, 50%, 75% 백분위:", percentiles)
# 최소/최대와 범위
min_score = np.min(scores)
max_score = np.max(scores)
range_score = np.ptp(scores) # peak to peak (범위)
# NaN 처리 - 결측치가 있을 때
data_with_nan = np.array([1, 2, np.nan, 4, 5])
safe_mean = np.nanmean(data_with_nan) # NaN 무시
print("NaN 무시 평균:", safe_mean)
# 상관계수 - 두 변수의 관계
subject1 = scores[:, 0]
subject2 = scores[:, 1]
correlation = np.corrcoef(subject1, subject2)
print("과목 간 상관계수:\n", correlation)
설명
이것이 하는 일: NumPy 통계 함수는 수치적으로 안정적인 알고리즘으로 배열의 통계량을 계산합니다. 단순히 합을 구해서 나누는 것보다 부동소수점 오차를 줄이는 정교한 방법을 사용합니다.
첫 번째로, np.mean()은 산술 평균을 계산합니다. 내부적으로 Kahan summation 같은 기법을 써서 큰 배열에서도 정확합니다.
axis 매개변수 없이 호출하면 전체 평균을, axis=0을 주면 각 열의 평균을 구합니다. 2D 배열에서 axis=0은 "행 방향으로 계산"이라는 의미입니다.
그 다음으로, np.std()와 np.var()는 표준편차와 분산을 구합니다. 분산은 데이터가 평균에서 얼마나 멀리 퍼져있는지를 나타내고, 표준편차는 분산의 제곱근입니다.
기본적으로 전체 모집단 기준(ddof=0)이지만, ddof=1을 주면 표본 표준편차를 구합니다. 세 번째로, np.median()은 중앙값을 구합니다.
데이터를 정렬했을 때 가운데 값이죠. 이상치에 강건해서 평균보다 더 대표성이 있을 때가 많습니다.
예를 들어 소득 분포처럼 극단값이 있는 데이터는 중앙값이 더 유용합니다. 네 번째로, np.percentile()은 백분위수를 계산합니다.
25%, 50%(중앙값), 75% 백분위를 구하면 데이터의 사분위 범위(IQR)를 알 수 있고, 이걸로 이상치를 탐지할 수 있습니다. 박스플롯의 기초 데이터입니다.
다섯 번째로, nanmean(), nanstd() 같은 함수들은 NaN(Not a Number) 값을 무시합니다. 실제 데이터는 항상 결측치가 있으므로 이 함수들이 필수입니다.
일반 mean()은 NaN이 하나라도 있으면 결과가 NaN이 됩니다. 여섯 번째로, np.corrcoef()는 상관계수 행렬을 계산합니다.
두 변수가 얼마나 함께 움직이는지 -1(완전 반대)에서 1(완전 일치) 사이의 값으로 나타냅니다. 피어슨 상관계수를 사용합니다.
마지막으로, axis 매개변수를 이해하는 게 핵심입니다. 2D 배열에서 axis=0은 "각 열에 대해", axis=1은 "각 행에 대해"입니다.
최종적으로 여러분은 복잡한 다차원 데이터의 통계를 자유롭게 계산할 수 있게 됩니다. 여러분이 이 코드를 사용하면 탐색적 데이터 분석(EDA)을 빠르게 할 수 있습니다.
데이터셋을 받으면 먼저 평균, 표준편차, 백분위수를 확인해서 데이터의 특성을 파악하고, 이상치를 찾고, 전처리 방향을 결정할 수 있습니다.
실전 팁
💡 표본 통계를 구할 때는 ddof=1을 명시하세요. np.std(data, ddof=1)은 표본 표준편차입니다.
💡 이상치 탐지는 IQR 방법을 쓰세요. Q1 - 1.5*IQR보다 작거나 Q3 + 1.5*IQR보다 크면 이상치입니다.
💡 np.histogram()으로 분포를 확인하세요. 데이터가 정규분포인지, 편향되었는지 알 수 있습니다.
💡 큰 배열의 평균을 구할 때는 dtype=np.float64를 명시해서 오버플로우를 방지하세요.
💡 pandas의 describe()는 내부적으로 NumPy 통계 함수를 쓰지만 더 편리합니다. DataFrame에는 pandas를 쓰세요.
10. 난수 생성과 시뮬레이션
시작하며
여러분이 몬테카를로 시뮬레이션으로 주가를 예측하거나, 머신러닝 모델을 학습하기 위해 데이터를 랜덤하게 섞어야 하는 상황을 생각해보세요. 무작위성이 필요한 순간입니다.
이런 난수 생성은 현대 데이터 과학의 핵심입니다. 머신러닝에서 가중치 초기화, 데이터 셔플링, 드롭아웃 등은 모두 난수가 필요합니다.
통계 시뮬레이션, 부트스트래핑, A/B 테스트 샘플링 등도 난수 기반입니다. Python의 random 모듈도 있지만 배열 전체를 다루기에는 느리고 불편합니다.
바로 이럴 때 필요한 것이 NumPy의 random 모듈입니다. 다양한 확률 분포에서 빠르게 난수를 생성하고 재현 가능한 결과를 보장합니다.
개요
간단히 말해서, NumPy random은 통계적 확률 분포에서 난수를 효율적으로 생성하는 모듈입니다. 난수 생성이 필요한 이유는 실제 데이터를 얻기 어렵거나, 다양한 시나리오를 시뮬레이션하거나, 알고리즘에 무작위성을 추가해야 하기 때문입니다.
예를 들어 신경망의 가중치를 랜덤하게 초기화하지 않으면 학습이 제대로 안 되고, 데이터를 셔플하지 않으면 모델이 순서에 과적합될 수 있습니다. 기존에는 Python random.random()을 for 문으로 여러 번 호출했다면, 이제는 np.random.rand(1000000) 한 줄로 백만 개의 난수를 생성합니다.
수백 배 빠릅니다. NumPy random의 핵심 특징은 네 가지입니다.
첫째, 균등분포, 정규분포, 이항분포 등 다양한 확률분포를 지원합니다. 둘째, seed를 설정해서 재현 가능한 난수를 생성할 수 있습니다.
셋째, 배열 전체를 한 번에 생성해서 매우 빠릅니다. 넷째, Generator API로 병렬 처리에 안전한 난수 생성이 가능합니다.
이러한 특징들이 과학 계산과 머신러닝을 지원합니다.
코드 예제
import numpy as np
# 재현성을 위한 시드 설정 - 디버깅에 필수
np.random.seed(42)
# 균등분포 (0~1 사이)
uniform_data = np.random.rand(1000) # 0~1 균등분포
print("균등분포 평균:", np.mean(uniform_data))
# 정규분포 (평균 0, 표준편차 1)
normal_data = np.random.randn(1000) # 표준 정규분포
custom_normal = np.random.normal(100, 15, 1000) # 평균 100, 표준편차 15
print("정규분포 평균:", np.mean(custom_normal))
# 정수 난수 - 주사위 시뮬레이션
dice_rolls = np.random.randint(1, 7, size=1000) # 1~6 사이
print("주사위 평균:", np.mean(dice_rolls))
# 선택 - 배열에서 랜덤 샘플링
population = np.arange(100)
sample = np.random.choice(population, size=10, replace=False)
print("샘플:", sample)
# 셔플 - 배열 섞기 (원본 수정)
data = np.arange(10)
np.random.shuffle(data)
print("셔플된 데이터:", data)
# 다양한 확률분포
binomial = np.random.binomial(10, 0.5, 1000) # 이항분포
poisson = np.random.poisson(5, 1000) # 포아송 분포
exponential = np.random.exponential(2, 1000) # 지수분포
# 새로운 Generator API (권장)
rng = np.random.default_rng(42) # 시드 42로 생성기 생성
better_random = rng.random(1000) # 균등분포
better_normal = rng.standard_normal(1000) # 정규분포
설명
이것이 하는 일: NumPy random 모듈은 의사난수 생성기(pseudorandom number generator)를 사용해서 통계적으로 무작위처럼 보이는 숫자를 생성합니다. seed를 설정하면 같은 시퀀스가 반복되어 결과를 재현할 수 있습니다.
첫 번째로, np.random.seed(42)로 난수 생성기의 시작점을 고정합니다. 같은 seed를 쓰면 항상 같은 난수가 나와서 디버깅과 재현 가능한 연구에 필수적입니다.
42는 관례적으로 많이 쓰는 숫자일 뿐 특별한 의미는 없습니다. 그 다음으로, rand()와 randn()이 가장 기본적인 난수 생성 함수입니다.
rand()는 0~1 균등분포, randn()은 표준 정규분포(평균 0, 표준편차 1)를 따릅니다. 균등분포는 모든 값이 같은 확률로 나타나고, 정규분포는 평균 근처 값이 많고 극단값은 드뭅니다.
세 번째로, randint(low, high, size)는 정수 난수를 생성합니다. 주사위 굴리기, 랜덤 인덱스 생성 등에 유용합니다.
low는 포함되고 high는 제외되므로 1~6 주사위는 randint(1, 7)입니다. 네 번째로, choice()는 배열에서 랜덤하게 샘플링합니다.
replace=False를 주면 중복 없이 선택하고(비복원 추출), replace=True면 중복 허용입니다(복원 추출). 데이터셋에서 랜덤 샘플을 뽑거나 부트스트래핑할 때 씁니다.
다섯 번째로, shuffle()은 배열을 제자리에서 섞습니다(in-place). 머신러닝에서 에포크마다 데이터를 셔플할 때 필수입니다.
순서대로 학습하면 모델이 순서에 과적합될 수 있습니다. 여섯 번째로, 다양한 확률분포를 지원합니다.
이항분포는 동전 던지기 같은 베르누이 시행의 합, 포아송 분포는 단위 시간당 사건 발생 횟수, 지수분포는 사건 간 시간 간격을 모델링합니다. 각 분포는 특정 현상을 시뮬레이션하는 데 적합합니다.
마지막으로, 새로운 Generator API(default_rng())가 권장됩니다. 기존 API보다 빠르고 병렬 처리에 안전하며 더 좋은 난수 품질을 제공합니다.
새 프로젝트는 이걸 쓰세요. 여러분이 이 코드를 사용하면 현실 세계의 불확실성을 시뮬레이션할 수 있습니다.
몬테카를로 시뮬레이션으로 복잡한 확률 문제를 풀거나, 합성 데이터를 생성해서 모델을 테스트하거나, 가설 검정에 필요한 부트스트랩 샘플을 만들 수 있습니다.
실전 팁
💡 재현 가능한 결과를 위해 항상 코드 시작 부분에 np.random.seed()를 설정하세요. 논문이나 프로덕션 코드에 필수입니다.
💡 여러 난수 생성기가 필요하면 각각 독립적인 Generator를 만드세요. rng1 = default_rng(42), rng2 = default_rng(123)
💡 큰 배열을 셔플할 때는 permutation()을 쓰면 복사본을 반환합니다. 원본을 유지하고 싶을 때 유용합니다.
💡 가중치를 적용한 샘플링은 choice(arr, p=weights)로 가능합니다. 각 원소가 선택될 확률을 다르게 할 수 있습니다.
💡 정규분포에서 난수를 뽑을 때 clip()으로 범위를 제한하세요. 물리적으로 불가능한 값(음수 길이 등)을 방지할 수 있습니다.
댓글 (0)
함께 보면 좋은 카드 뉴스
데이터 증강과 정규화 완벽 가이드
머신러닝 모델의 성능을 극대화하는 핵심 기법인 데이터 증강과 정규화에 대해 알아봅니다. 실무에서 바로 활용할 수 있는 다양한 기법과 실전 예제를 통해 과적합을 방지하고 모델 성능을 향상시키는 방법을 배웁니다.
ResNet과 Skip Connection 완벽 가이드
딥러닝 모델이 깊어질수록 성능이 떨어지는 문제를 해결한 혁신적인 기법, ResNet과 Skip Connection을 초급자도 이해할 수 있도록 쉽게 설명합니다. 실제 구현 코드와 함께 배워보세요.
CNN 아키텍처 완벽 가이드 LeNet AlexNet VGGNet
컴퓨터 비전의 기초가 되는 세 가지 핵심 CNN 아키텍처를 배웁니다. 손글씨 인식부터 이미지 분류까지, 딥러닝의 발전 과정을 따라가며 각 모델의 구조와 특징을 실습 코드와 함께 이해합니다.
CNN 기초 Convolution과 Pooling 완벽 가이드
CNN의 핵심인 Convolution과 Pooling을 초급자도 쉽게 이해할 수 있도록 설명합니다. 이미지 인식의 원리부터 실제 코드 구현까지, 실무에서 바로 활용 가능한 내용을 담았습니다.
TensorFlow와 Keras 완벽 입문 가이드
머신러닝과 딥러닝의 세계로 들어가는 첫걸음! TensorFlow와 Keras 프레임워크를 처음 접하는 분들을 위한 친절한 가이드입니다. 실무에서 바로 활용할 수 있는 핵심 개념과 예제를 통해 AI 모델 개발의 기초를 탄탄히 다져보세요.