11주차 · NumPy와 SciPy 기초
11주차. NumPy와 SciPy 기초
Section titled “11주차. NumPy와 SciPy 기초”- 라이브러리와
import개념 list와numpy.ndarray의 핵심 차이shape/dtype를 점검하는 습관- 반복문 대신 vectorization 기초
scipy.signal의 스무딩/피크 탐색 기초- 흔한 오류를 스스로 진단하는 체크리스트
import numpy as np,np.array(...)기본 문법을 자신 있게 읽기- C 배열 연산과 NumPy 배열 연산의 차이를 설명하기
np.dot,@연산자,np.linalg.solve로 행렬 연산 수행하기- 브로드캐스팅 형태 호환 규칙을 표로 설명하고 EE 예제에 적용하기
NumPy 문법 quickstart: 처음 보는 학생은 이것부터
Section titled “NumPy 문법 quickstart: 처음 보는 학생은 이것부터”NumPy를 쓸 때는 보통 아래 3단계를 반복합니다.
import numpy as np
arr = np.array([1, 2, 3], dtype=float)print(arr.shape)print(arr * 2)처음 쓸 때 체크할 4가지
Section titled “처음 쓸 때 체크할 4가지”import numpy as np:numpy를np라는 짧은 이름으로 가져온다.np.array([...]): 리스트를 배열로 바꾼다.arr.shape,arr.dtype: 계산 전에 구조와 타입을 확인한다.arr * 2,arr + arr: 반복문 없이 원소별 연산이 된다.
1) 라이브러리란 무엇인가?
Section titled “1) 라이브러리란 무엇인가?”Python 기본 문법만으로도 계산은 가능하지만, 반복적 수치 계산은 느리고 코드가 길어집니다.
- 라이브러리: 재사용 가능한 기능 묶음
import로 가져와 사용- 검증된 함수를 쓰면 실수를 줄일 수 있음
import numpy as np
from scipy import signalnumpy as np: 가장 널리 쓰는 별칭scipy.signal: 1차원 신호 처리 함수 집합
2) list와 ndarray: 왜 바꿔야 하나?
Section titled “2) list와 ndarray: 왜 바꿔야 하나?”| 항목 | Python 리스트 | NumPy 배열(ndarray) |
|---|---|---|
| 주 용도 | 일반 컨테이너 | 수치 계산 |
a + b | 이어붙이기 | 원소별 덧셈 |
| shape 정보 | 없음 | 있음 (arr.shape) |
| dtype 제어 | 약함 | 명시 가능 |
오해와 반례
Section titled “오해와 반례”오해: “list도 숫자니까 계산 결과가 같겠지”
Section titled “오해: “list도 숫자니까 계산 결과가 같겠지””a = [1, 2, 3]b = [10, 20, 30]print(a + b) # [1, 2, 3, 10, 20, 30]의도했던 [11, 22, 33]이 아닙니다.
올바른 방식
Section titled “올바른 방식”import numpy as np
arr_a = np.array([1, 2, 3])arr_b = np.array([10, 20, 30])print(arr_a + arr_b) # [11 22 33]C와 비교: 배열 계산 감각이 이렇게 달라집니다
Section titled “C와 비교: 배열 계산 감각이 이렇게 달라집니다”| 항목 | C | Python + NumPy | 기억할 점 |
|---|---|---|---|
| 배열 생성 | 타입/크기 선언 | np.array([...]) | Python은 런타임에 더 유연하다 |
| 원소별 덧셈 | 보통 루프 직접 작성 | arr_a + arr_b | NumPy가 브로드캐스트/원소연산 수행 |
| 평균 계산 | 루프 + 누적 변수 | arr.mean() | 의도가 더 직접적이다 |
| 구조 점검 | 수동 관리 | shape, dtype | 계산 전 검사 루틴이 쉽다 |
double a[3] = {1, 2, 3};double b[3] = {10, 20, 30};double out[3];
for (int i = 0; i < 3; i++) { out[i] = a[i] + b[i];}import numpy as np
a = np.array([1, 2, 3], dtype=float)b = np.array([10, 20, 30], dtype=float)out = a + bprint(out)핵심은 NumPy는 루프를 숨기고 수식 자체를 드러내므로, 수치 코드를 더 짧고 덜 실수하게 만든다는 점입니다.
3) shape와 dtype은 “계산 전 건강검진”
Section titled “3) shape와 dtype은 “계산 전 건강검진””import numpy as np
arr = np.array([1, 2, 3, 4], dtype=float)print(arr.shape) # (4,)print(arr.dtype) # float64 (환경에 따라 다를 수 있음)shape: 데이터 구조가 기대와 같은지 확인dtype: 정밀도/연산 방식 확인
4) Vectorization: 반복문보다 간결하고 안전한 배열 연산
Section titled “4) Vectorization: 반복문보다 간결하고 안전한 배열 연산”Loop 버전
Section titled “Loop 버전”values = [1.0, 2.0, 3.0]out = []for v in values: out.append(v * 2)print(out)Vectorization 버전
Section titled “Vectorization 버전”import numpy as np
arr = np.array([1.0, 2.0, 3.0])out = arr * 2print(out)초보자에게 vectorization이 좋은 이유:
- 코드 줄 수 감소
- 인덱스 실수 감소
- 수식 의도가 더 잘 보임
5) SciPy signal 기초: 스무딩과 피크 탐색
Section titled “5) SciPy signal 기초: 스무딩과 피크 탐색”SciPy는 NumPy 위에서 동작하는 과학 계산 라이브러리입니다. 이번 주에는 1차원 신호 처리에 자주 쓰이는 두 함수를 배웁니다.
주요 함수 설명
Section titled “주요 함수 설명”| 함수 | 역할 | 주요 파라미터 |
|---|---|---|
signal.savgol_filter(y, window_length, polyorder) | Savitzky-Golay 방식으로 신호를 스무딩(노이즈 완화) | window_length: 창 크기(홀수), polyorder: 다항식 차수 |
signal.find_peaks(y, height, distance) | 극대값(봉우리) 인덱스 탐색 | height: 최소 높이, distance: 피크 간 최소 간격 |
스무딩이 필요한 이유: 센서나 측정 데이터에는 항상 잡음이 섞입니다. 잡음을 그대로 두면 find_peaks가 작은 흔들림을 피크로 잘못 감지합니다. 스무딩 후 피크를 찾으면 더 안정적입니다.
파라미터 사용 요령
Section titled “파라미터 사용 요령”from scipy import signal
# window_length는 반드시 홀수 (짝수이면 오류 발생)y_smooth = signal.savgol_filter(y, window_length=11, polyorder=2)
# height=0.5: 높이 0.5 이상인 봉우리만 탐색# distance=10: 두 피크 사이 최소 간격 10 인덱스peaks, props = signal.find_peaks(y_smooth, height=0.5, distance=10) 예제 1 줄별 해설
Section titled “예제 1 줄별 해설”- list를 곧바로 계산에 쓰지 않고 ndarray로 변환해 연산 의미를 고정합니다.
dtype=float를 명시해 정밀도 관련 혼란을 줄입니다.shape와dtype를 먼저 출력해 입력 상태를 진단합니다.- 정규화 수식은 배열 전체에 한 번에 적용됩니다(vectorization).
+ 1e-12는 0으로 나누는 위험을 완화하는 안전장치입니다.- 동일 기호
+가 list와 ndarray에서 다르게 동작함을 직접 비교합니다. assert로 구조와 dtype을 마지막에 확인해 실수를 빠르게 잡습니다.
예제 2에서 새로 등장하는 함수
Section titled “예제 2에서 새로 등장하는 함수”예제 2를 읽기 전에 아래 함수들의 역할을 먼저 파악하세요.
| 함수 | 역할 | 예시 |
|---|---|---|
np.linspace(start, stop, num) | start부터 stop까지 num개 등간격 숫자 배열 생성 | np.linspace(0, 1, 5) → [0, 0.25, 0.5, 0.75, 1] |
np.random.default_rng(seed) | 재현 가능한 난수 생성기 객체 생성. seed를 고정하면 매번 같은 난수 | rng = np.random.default_rng(42) |
rng.normal(size=n) | 평균 0, 표준편차 1의 정규분포 난수 n개 생성 | rng.normal(size=200) |
np.diff(arr) | 배열에서 인접 원소 간 차이(차분) 계산. 길이가 1 줄어듦 | np.diff([1,3,6]) → [2,3] |
np.round(arr, decimals) | 배열 원소를 지정 소수점 자리로 반올림 | np.round([1.2345], 2) → [1.23] |
raw vs smoothed vs peaks를 눈으로 읽는 미니 차트
Section titled “raw vs smoothed vs peaks를 눈으로 읽는 미니 차트”| index | raw | smoothed | peaks | quick read |
|---|---|---|---|---|
| 0 | 0.046 | 0.008 | 시작점은 낮음 | |
| 1 | -0.092 | 0.073 | 노이즈 때문에 잠깐 내려감 | |
| 2 | 0.238 | 0.133 | 전체 흐름은 다시 상승 | |
| 3 | 0.330 | 0.189 | 완만하게 올라가는 중 | |
| 4 | -0.042 | 0.241 | raw는 흔들려도 smooth는 상승 유지 | |
| 5 | 0.119 | 0.290 | 작은 흔들림이 줄어듦 | |
| 6 | 0.392 | 0.379 | 첫 큰 봉우리 쪽으로 접근 | |
| 7 | 0.381 | 0.454 | 아직 검출 조건 전 |
index 0 1 2 3 4 5 6 7raw ▂ ▁ ▄ ▅ ▁ ▃ ▆ ▆smoothed ▁ ▂ ▂ ▃ ▃ ▄ ▅ ▆peaks · · · · · · · ·- raw: 노이즈 때문에 위아래 흔들림이 큽니다.
- smoothed: 작은 흔들림을 줄여서 큰 흐름을 읽기 쉽습니다.
- peaks: 이 초반 8개 샘플에서는 아직 검출되지 않고, 실제 첫 피크는 인덱스
14, 25, 35에서 잡힙니다.
예제 2 줄별 해설
Section titled “예제 2 줄별 해설”- 난수 시드를 고정하면 다른 수강생의 결과와도 동일한 값을 재현해 비교할 수 있습니다.
np.linspace(0, 4*np.pi, 200)으로 등간격 x축을 만듭니다. 200개 점이 생깁니다.- 원 신호
y는 일부러 잡음을 포함해 현실성을 높였습니다. np.diff(y)는 인접 값의 차이를 보여주어 신호가 얼마나 빠르게 변하는지 확인합니다.sample raw와sample smooth를 함께 보면, 작은 흔들림이 줄어드는 방향을 숫자로도 확인할 수 있습니다.savgol_filter로 급격한 흔들림을 완화해 피크 탐색 안정성을 높입니다.find_peaks의height는 최소 높이,distance는 피크 간 최소 간격입니다.- 인덱스
i로peaks[i]를 직접 꺼내 실제 좌표값을 출력합니다. assert len(peaks) > 0로 파라미터가 완전히 실패하지 않았는지 확인합니다.
6) 행렬 연산: np.dot, @ 연산자, np.linalg.solve
Section titled “6) 행렬 연산: np.dot, @ 연산자, np.linalg.solve”공학 문제에서는 단순 원소별 연산 외에도 행렬 곱셈과 연립방정식 풀기가 자주 필요합니다.
핵심 함수 정리
Section titled “핵심 함수 정리”| 함수/연산자 | 역할 | 예시 |
|---|---|---|
np.dot(A, B) | 행렬 곱셈 또는 벡터 내적 | np.dot(A, x) → 행렬-벡터 곱 |
A @ B | np.dot와 동일 (Python 3.5+, 가독성 좋음) | A @ x |
np.linalg.solve(A, b) | 연립방정식 Ax = b 풀기 | x = np.linalg.solve(Z, V) |
EE 예제 A: 임피던스 행렬과 전압 계산
Section titled “EE 예제 A: 임피던스 행렬과 전압 계산”2포트 회로에서 임피던스 행렬 Z와 전류 벡터 I가 주어지면, 전압 V = Z @ I 로 계산합니다.
import numpy as np
# 임피던스 행렬 Z (단위: Ω)Z = np.array([[50, 10], [10, 75]], dtype=float)
# 전류 벡터 I (단위: A)I = np.array([2.0, 1.0])
# 전압 벡터 V = Z @ IV = Z @ Iprint("V (Volt):", V) # [110. 95.]EE 예제 B: 연립방정식으로 분압 계산
Section titled “EE 예제 B: 연립방정식으로 분압 계산”저항 회로에서 KVL/KCL을 행렬 방정식 Ax = b 로 세우면 np.linalg.solve로 한 번에 풀 수 있습니다.
import numpy as np
# 예: 두 절점의 전압 V1, V2 를 구하는 방정식# 방정식 1: 3*V1 - V2 = 12# 방정식 2: -V1 + 4*V2 = 8A = np.array([[3, -1], [-1, 4]], dtype=float)b = np.array([12.0, 8.0])
x = np.linalg.solve(A, b)print("V1, V2 (Volt):", np.round(x, 3))
# 검증: A @ x 가 b 와 일치하는지print("residual:", np.round(A @ x - b, 10))7) 브로드캐스팅: 형태가 다른 배열 간 연산
Section titled “7) 브로드캐스팅: 형태가 다른 배열 간 연산”브로드캐스팅은 형태(shape)가 다른 두 배열을 자동으로 맞춰서 연산하는 NumPy 규칙입니다. 반복문 없이 배열-스칼라, 2D-1D 연산이 가능합니다.
브로드캐스팅 형태 호환 규칙
Section titled “브로드캐스팅 형태 호환 규칙”두 배열의 shape을 오른쪽부터 비교합니다. 각 자리에서:
- 두 값이 같으면: 그대로 연산
- 한쪽이 1이면: 1인 쪽을 상대 크기로 늘려서 연산
- 둘 다 1보다 크고 다르면: 오류
| A shape | B shape | 결과 shape | 가능 여부 |
|---|---|---|---|
(4,) | () (스칼라) | (4,) | 가능 |
(3, 4) | (4,) | (3, 4) | 가능 (행 방향 복제) |
(3, 1) | (4,) | (3, 4) | 가능 (양방향 복제) |
(3, 4) | (3,) | 오류 | 불가 (오른쪽 정렬 후 불일치) |
(3, 4) | (1, 4) | (3, 4) | 가능 |
EE 예제: 전압 분배기 배열
Section titled “EE 예제: 전압 분배기 배열”동일 저항망에서 여러 입력 전압을 한 번에 계산합니다.
import numpy as np
# 분배비 배열 (각 채널의 분압 비율)ratios = np.array([0.5, 0.33, 0.25, 0.20]) # shape (4,)
# 여러 공급 전압 (shape (3, 1) 로 세로 배열)supply_V = np.array([[5.0], [9.0], [12.0]]) # shape (3, 1)
# 브로드캐스팅: (3,1) x (4,) → (3, 4)output_V = supply_V * ratiosprint("shape:", output_V.shape) # (3, 4)print(np.round(output_V, 3))# 각 행 = 공급 전압, 각 열 = 분배비 채널신호 상관(correlation) 예제
Section titled “신호 상관(correlation) 예제”두 센서 신호의 정규화된 내적(유사도)을 브로드캐스팅으로 계산합니다.
import numpy as np
rng = np.random.default_rng(0)sig_a = rng.normal(size=100)sig_b = sig_a + 0.3 * rng.normal(size=100) # sig_a 에 노이즈 추가
# 정규화sig_a_n = (sig_a - sig_a.mean()) / sig_a.std()sig_b_n = (sig_b - sig_b.mean()) / sig_b.std()
# 내적 = 상관계수(표준화 시)corr = np.dot(sig_a_n, sig_b_n) / len(sig_a_n)print(f"신호 유사도(상관계수): {corr:.3f}")초보자 오해/안티패턴 표
Section titled “초보자 오해/안티패턴 표”| 안티패턴 | 증상 | 원인 | 개선 방법 |
|---|---|---|---|
import numpy 후 np.array 사용 | NameError | 별칭 불일치 | import numpy as np 고정 |
| list 상태로 수식 적용 | 이어붙이기/타입 혼란 | list와 ndarray 의미 차이 미이해 | 계산 전 np.array(...) 변환 |
| shape 확인 생략 | 브로드캐스팅 오류 | 입력 구조 미검증 | 계산 직전 print(arr.shape) |
| dtype 무시 | 예상과 다른 정밀도 | 기본 dtype 착각 | dtype=float 명시 |
find_peaks 파라미터 감으로 설정 | 피크 과다/누락 | 기준값 의미 미이해 | height/distance를 단계 조정 |
짝수 window_length 사용 | 함수 오류 | 필터 조건 미확인 | Savitzky-Golay 창 길이는 홀수 사용 |
실습 문제 · 직접 코딩
Section titled “실습 문제 · 직접 코딩”[기초] 배열 생성과 기본 연산: np.linspace로 배열을 만들고 원소별 연산을 적용하세요.
[기초] 불리언 인덱싱: 조건에 맞는 원소만 골라내는 NumPy 기능을 연습하세요.
[응용] 2D 배열과 reshape: 1D 배열을 2D 행렬로 변환하고 행/열별 통계를 구하세요.
[응용] 잡음 신호 스무딩과 피크 탐색: sin 파형에 노이즈를 추가하고 스무딩 후 피크를 찾으세요.
[도전] 행렬 연산과 연립방정식: 임피던스 행렬을 구성하고 @ 연산자와 np.linalg.solve로 전압/전류를 계산하세요.