목차
메타 설명
Python 리스트로 수백만 개의 숫자를 계산하면 왜 느려질까요? 과학 계산과 데이터 분석의 핵심 라이브러리 NumPy를 이용해 배열 생성, 인덱싱, 브로드캐스팅, 형태 변경, 통계 계산, 행렬 연산, 난수 생성까지 실습 중심으로 알아봅니다.
지난 시간에는 Matplotlib를 이용해 숫자 데이터를 그래프로 표현하는 방법을 배웠습니다.
월별 매출은 선 그래프로, 상품별 판매량은 막대그래프로, 점수 분포는 히스토그램으로 바꾸었습니다.
그런데 그래프를 그리기 전에 한 가지 중요한 과정이 필요합니다.
바로 숫자를 계산하는 일입니다.
예를 들어 다음과 같은 작업을 처리해야 한다고 가정해 보겠습니다.
100만 명의 시험 점수 평균 계산
1,000개 센서의 온도 데이터 변환
수백 장의 이미지 밝기 조정
주식 가격의 일별 수익률 계산
인공지능 모델의 가중치 행렬 연산Python 리스트와 반복문만으로도 계산할 수 있습니다.
numbers = [1, 2, 3, 4, 5]
result = []
for number in numbers:
result.append(number * 10)
print(result)결과:
[10, 20, 30, 40, 50]데이터가 다섯 개라면 아무 문제가 없습니다.
하지만 숫자가 백만 개, 천만 개로 늘어나면 반복문은 갑자기 야근 모드에 들어갑니다.
Python 반복문:
“숫자를 하나씩 불러 주세요.”
NumPy:
“전원 집합. 한 번에 계산합니다.”이번 시간의 주인공은 숫자 군단을 한꺼번에 움직이는 계산 지휘관, NumPy입니다. 🧙♂️🔢
1. NumPy란 무엇인가?
NumPy는 Numerical Python의 줄임말로, Python에서 대규모 숫자와 다차원 배열을 효율적으로 처리하기 위한 오픈소스 라이브러리입니다.
NumPy는 다차원 배열 객체인 `ndarray`를 중심으로 수학, 통계, 논리 연산, 정렬, 입출력, 푸리에 변환, 선형대수, 난수 생성 등의 기능을 제공합니다.
NumPy는 다음과 같은 분야에서 폭넓게 사용됩니다.
데이터 분석
인공지능과 머신러닝
이미지 처리
음성 신호 처리
과학 계산
통계 분석
금융 데이터 분석
센서 데이터 처리
시뮬레이션
행렬 계산pandas와 Matplotlib를 비롯한 수많은 Python 데이터 라이브러리도 NumPy 배열과 긴밀하게 연결되어 있습니다.
NumPy는 2005년 Numeric과 Numarray라는 초기 배열 프로젝트의 성과를 바탕으로 만들어졌으며, BSD 계열 라이선스로 공개되어 있습니다.
2. NumPy가 필요한 이유
Python에는 이미 리스트가 있습니다.
numbers = [10, 20, 30, 40]그렇다면 굳이 NumPy 배열을 사용해야 하는 이유는 무엇일까요?
이유 1. 배열 전체를 한 번에 계산할 수 있다
Python 리스트의 모든 값에 2를 곱하려면 일반적으로 반복문이나 리스트 컴프리헨션을 사용합니다.
numbers = [10, 20, 30, 40]
result = [
number * 2
for number in numbers
]
print(result)NumPy 배열에서는 다음처럼 작성합니다.
import numpy as np
numbers = np.array([
10,
20,
30,
40
])
result = numbers * 2
print(result)결과:
[20 40 60 80]이유 2. 코드가 간결하다
두 목록의 같은 위치에 있는 값을 더해 보겠습니다.
Python 리스트
a = [1, 2, 3]
b = [10, 20, 30]
result = []
for first, second in zip(a, b):
result.append(first + second)
print(result)NumPy 배열
import numpy as np
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
result = a + b
print(result)결과:
[11 22 33]이유 3. 대규모 수치 연산에 적합하다
NumPy 배열은 일반적으로 같은 데이터 타입의 값을 연속적인 구조로 관리하며, 대량의 데이터를 Python 기본 시퀀스보다 적은 코드로 효율적으로 계산할 수 있도록 설계되어 있습니다.
Python 반복문이 숫자들에게 한 명씩 출석을 부른다면, NumPy는 전교생에게 방송으로 명령을 전달합니다. 📢
3. NumPy 설치하기
터미널이나 명령 프롬프트에서 다음 명령어를 실행합니다.
pip install numpyPython 실행 환경을 명확하게 지정하려면 다음과 같이 작성합니다.
python -m pip install numpymacOS나 Linux에서 `python3`를 사용한다면 다음 명령어를 사용할 수 있습니다.
python3 -m pip install numpyNumPy 공식 설치 페이지에서는 `pip`, `conda`, 시스템 패키지 관리자 등을 이용한 설치 방법을 안내하고 있습니다. 일반적인 Python 프로젝트에서는 가상환경 안에서 `pip`로 설치하는 방법을 사용할 수 있습니다.
Windows 가상환경
python -m venv venv
venv\Scripts\activate
python -m pip install numpymacOS 또는 Linux 가상환경
python3 -m venv venv
source venv/bin/activate
python -m pip install numpy설치 확인하기
import numpy as np
print(np.__version__)오류 없이 버전이 출력되면 설치가 완료된 것입니다.
NumPy는 일반적으로 다음 별명으로 불러옵니다.
import numpy as np`np`는 NumPy 세계에서 사실상 공식 활동명처럼 사용됩니다.
4. ndarray란?
NumPy의 핵심 객체는 `ndarray`입니다.
`ndarray`는 다음 단어의 줄임말입니다.
N-dimensional array
→ N차원 배열NumPy 배열은 일반적으로 같은 데이터 타입의 요소로 구성된 다차원 배열입니다. NumPy에서는 배열의 각 차원을 `axis`라고 부릅니다.
예를 들어 다음은 1차원 배열입니다.
import numpy as np
array = np.array([
10,
20,
30
])
print(array)
print(type(array))결과:
[10 20 30]
<class 'numpy.ndarray'>`np.array()`는 배열을 생성하는 함수이고, 생성된 객체의 타입이 `numpy.ndarray`입니다.
np.array()
→ 배열을 만드는 함수
ndarray
→ 만들어진 배열 객체의 타입이름이 비슷해 처음에는 문 앞에서 명찰 검사를 당하는 기분이 들지만, 역할은 분명히 다릅니다.
5. Python 리스트와 NumPy 배열의 차이
Python 리스트와 NumPy 배열은 겉모습이 비슷하지만 연산 방식이 다릅니다.
리스트에 2를 곱한 경우
numbers = [
1,
2,
3
]
print(numbers * 2)결과:
[1, 2, 3, 1, 2, 3]리스트 자체가 두 번 반복됩니다.
NumPy 배열에 2를 곱한 경우
import numpy as np
numbers = np.array([
1,
2,
3
])
print(numbers * 2)결과:
[2 4 6]각 요소에 2가 곱해집니다.
주요 차이 비교
| 구분 | Python 리스트 | NumPy 배열 |
|---|---|---|
| 데이터 타입 | 서로 다른 타입 저장 가능 | 일반적으로 같은 타입으로 통일 |
| 사칙연산 | 반복문 필요 | 배열 전체 연산 가능 |
| 다차원 처리 | 중첩 리스트 사용 | 다차원 배열 기능 제공 |
| 통계 함수 | 직접 구현하거나 모듈 사용 | 평균·합계·표준편차 제공 |
| 행렬 계산 | 직접 구현 필요 | 선형대수 기능 제공 |
| 대규모 수치 계산 | 상대적으로 불편 | 효율적인 연산에 적합 |
NumPy 배열에 문자열과 숫자를 함께 넣으면 공통으로 표현할 수 있는 타입으로 변환될 수 있습니다.
import numpy as np
data = np.array([
10,
20,
"Python"
])
print(data)
print(data.dtype)결과 예시:
['10' '20' 'Python']
<U21숫자도 문자열로 변환되었습니다.
NumPy 배열은 한 팀에 같은 유니폼을 입히려는 성향이 강합니다.
6. 첫 번째 NumPy 배열 만들기
리스트를 배열로 변환하기
import numpy as np
numbers = np.array([
10,
20,
30,
40
])
print(numbers)튜플을 배열로 변환하기
numbers = np.array(
(10, 20, 30, 40)
)
print(numbers)2차원 배열 만들기
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(matrix)결과:
[[1 2 3]
[4 5 6]]데이터 타입 지정하기
numbers = np.array(
[1, 2, 3, 4],
dtype=np.float64
)
print(numbers)
print(numbers.dtype)결과:
[1. 2. 3. 4.]
float64정수를 입력했지만 `dtype`을 지정했기 때문에 실수 배열이 만들어졌습니다.
7. 배열의 핵심 속성 확인하기
NumPy 배열에는 배열 구조를 설명하는 여러 속성이 있습니다.
import numpy as np
array = np.array([
[10, 20, 30],
[40, 50, 60]
])ndim
배열의 차원 수를 확인합니다.
print(array.ndim)결과:
2shape
각 차원의 크기를 확인합니다.
print(array.shape)결과:
(2, 3)의미:
2개의 행
3개의 열size
전체 요소 개수를 확인합니다.
print(array.size)결과:
6dtype
배열 요소의 데이터 타입을 확인합니다.
print(array.dtype)결과 예시:
int64운영체제와 환경에 따라 정수 타입의 세부 결과는 달라질 수 있습니다.
itemsize
요소 하나가 사용하는 바이트 수를 확인합니다.
print(array.itemsize)nbytes
전체 배열 데이터가 사용하는 바이트 수를 확인합니다.
print(array.nbytes)다음 관계가 성립합니다.
nbytes = size × itemsizeNumPy 공식 빠른 시작 문서에서도 `ndim`, `shape`, `size`, `dtype`을 ndarray의 핵심 속성으로 소개합니다.
8. 1차원·2차원·3차원 배열 이해하기
0차원 배열
값 하나만 담은 배열입니다.
scalar = np.array(10)
print(scalar.ndim)
print(scalar.shape)결과:
0
()1차원 배열
숫자가 한 줄로 나열됩니다.
vector = np.array([
10,
20,
30
])
print(vector.ndim)
print(vector.shape)결과:
1
(3,)2차원 배열
행과 열로 구성된 표와 비슷합니다.
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(matrix.ndim)
print(matrix.shape)결과:
2
(2, 3)3차원 배열
여러 개의 2차원 배열이 겹쳐 있는 구조입니다.
tensor = np.array([
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]
]
])
print(tensor.ndim)
print(tensor.shape)결과:
3
(2, 2, 2)3차원 배열은 다음과 같이 해석할 수 있습니다.
2개의 2차원 배열
각 배열에 2개의 행
각 행에 2개의 열이미지는 보통 높이, 너비, 색상 채널을 가진 3차원 배열로 표현할 수 있습니다.
세로 × 가로 × 색상 채널동영상은 시간 차원이 추가된 4차원 배열로 표현할 수 있습니다.
프레임 × 세로 × 가로 × 색상 채널차원이 늘어날수록 배열은 숫자 서랍장이 아니라 숫자 아파트 단지가 됩니다. 🏢
9. 데이터 타입 dtype 이해하기
NumPy에서는 배열이 어떤 종류의 데이터를 저장하는지 `dtype`으로 관리합니다.
대표적인 데이터 타입
| 타입 | 설명 |
|---|---|
| `int8` | 8비트 정수 |
| `int16` | 16비트 정수 |
| `int32` | 32비트 정수 |
| `int64` | 64비트 정수 |
| `uint8` | 부호 없는 8비트 정수 |
| `float32` | 32비트 실수 |
| `float64` | 64비트 실수 |
| `bool` | 참과 거짓 |
| `complex64` | 복소수 |
| `str_` | 문자열 |
NumPy는 Python보다 더 다양한 수치 데이터 타입을 제공합니다. 데이터 범위와 정밀도, 메모리 사용량에 따라 적절한 타입을 선택할 수 있습니다.
정수 타입 지정하기
numbers = np.array(
[1, 2, 3],
dtype=np.int32
)
print(numbers.dtype)실수 타입 지정하기
numbers = np.array(
[1, 2, 3],
dtype=np.float32
)
print(numbers)결과:
[1. 2. 3.]타입 변환하기
numbers = np.array([
1.2,
2.8,
3.5
])
converted = numbers.astype(
np.int32
)
print(converted)결과:
[1 2 3]실수를 정수로 변환하면 소수 부분이 반올림되지 않고 제거됩니다.
dtype을 작게 잡을 때 주의하기
numbers = np.array(
[100, 200, 300],
dtype=np.int8
)`int8`이 표현할 수 있는 범위를 넘어서는 값은 올바르게 저장할 수 없습니다.
데이터 타입을 작게 만들면 메모리를 줄일 수 있지만, 값의 범위와 정밀도를 반드시 확인해야 합니다.
작은 여행 가방에 코끼리를 넣으려고 하면 숫자도 곤란해집니다. 🧳🐘
10. 다양한 배열 생성 함수
NumPy는 데이터를 직접 입력하지 않고도 특정 형태의 배열을 만들 수 있는 함수를 제공합니다.
모든 값이 0인 배열
zeros = np.zeros(
(2, 3)
)
print(zeros)결과:
[[0. 0. 0.]
[0. 0. 0.]]모든 값이 1인 배열
ones = np.ones(
(2, 3)
)
print(ones)특정 값으로 채운 배열
filled = np.full(
(2, 3),
7
)
print(filled)결과:
[[7 7 7]
[7 7 7]]단위 행렬
identity = np.eye(3)
print(identity)결과:
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]대각 행렬
diagonal = np.diag([
10,
20,
30
])
print(diagonal)결과:
[[10 0 0]
[ 0 20 0]
[ 0 0 30]]NumPy는 Python 구조 변환, 일정한 값 채우기, 수열 생성, 난수 생성, 파일 읽기 등 다양한 배열 생성 방법을 제공합니다.
11. arange와 linspace 사용하기
arange
`arange()`는 일정한 간격의 숫자를 생성합니다.
numbers = np.arange(
0,
10,
2
)
print(numbers)결과:
[0 2 4 6 8]구조:
np.arange(
시작값,
종료값,
간격
)종료값은 포함되지 않습니다.
np.arange(1, 5)결과:
[1 2 3 4]linspace
`linspace()`는 시작값과 종료값 사이를 지정한 개수만큼 균등하게 나눕니다.
numbers = np.linspace(
0,
10,
5
)
print(numbers)결과:
[ 0. 2.5 5. 7.5 10. ]구조:
np.linspace(
시작값,
종료값,
생성할 개수
)기본적으로 종료값도 포함됩니다.
arange와 linspace 비교
np.arange()
→ 간격을 기준으로 생성
np.linspace()
→ 개수를 기준으로 생성np.arange(
0,
10,
2
)0부터 10 전까지
2 간격으로 생성np.linspace(
0,
10,
5
)0부터 10까지
총 5개로 균등 분할실수 구간을 정확한 개수로 나눌 때는 `linspace()`가 편리합니다.
12. 배열 인덱싱
NumPy 배열도 Python 리스트처럼 인덱스를 이용해 값을 선택할 수 있습니다.
1차원 배열
import numpy as np
numbers = np.array([
10,
20,
30,
40
])
print(numbers[0])
print(numbers[2])
print(numbers[-1])결과:
10
30
40값 수정하기
numbers[1] = 200
print(numbers)결과:
[ 10 200 30 40]2차원 배열
matrix = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
print(matrix[0, 1])결과:
20구조:
matrix[행, 열]print(matrix[2, 0])결과:
70한 행 선택하기
print(matrix[1])결과:
[40 50 60]한 열 선택하기
print(matrix[:, 1])결과:
[20 50 80]다차원 배열은 각 차원의 인덱스를 튜플 형태로 지정해 선택할 수 있습니다.
13. 배열 슬라이싱
슬라이싱을 사용하면 배열의 특정 범위를 선택할 수 있습니다.
1차원 슬라이싱
numbers = np.array([
10,
20,
30,
40,
50
])
print(numbers[1:4])결과:
[20 30 40]처음부터 특정 위치까지
print(numbers[:3])결과:
[10 20 30]특정 위치부터 마지막까지
print(numbers[2:])결과:
[30 40 50]간격 지정하기
print(numbers[::2])결과:
[10 30 50]역순으로 뒤집기
print(numbers[::-1])결과:
[50 40 30 20 10]2차원 슬라이싱
matrix = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
result = matrix[
0:2,
1:3
]
print(result)결과:
[[2 3]
[6 7]]의미:
0번째부터 1번째 행
1번째부터 2번째 열14. 조건에 맞는 값 필터링하기
NumPy 배열에 조건식을 적용하면 각 값이 조건을 만족하는지 확인할 수 있습니다.
scores = np.array([
55,
72,
88,
91,
63
])
condition = scores >= 80
print(condition)결과:
[False False True True False]이 조건을 배열에 다시 전달하면 `True`에 해당하는 값만 선택할 수 있습니다.
high_scores = scores[
scores >= 80
]
print(high_scores)결과:
[88 91]여러 조건 사용하기
70점 이상이면서 90점 미만인 점수를 찾습니다.
result = scores[
(scores >= 70)
& (scores < 90)
]
print(result)결과:
[72 88]또는 조건
60점 미만이거나 90점 이상인 값을 선택합니다.
result = scores[
(scores < 60)
| (scores >= 90)
]
print(result)결과:
[55 91]반대 조건
result = scores[
~(scores >= 80)
]
print(result)결과:
[55 72 63]pandas에서 사용했던 조건 필터링이 낯익어 보이는 이유는 pandas가 NumPy의 배열 연산 방식과 긴밀하게 연결되어 있기 때문입니다.
15. 여러 위치의 값 선택하기
선택할 위치를 배열로 전달할 수 있습니다.
numbers = np.array([
10,
20,
30,
40,
50
])
result = numbers[
[0, 2, 4]
]
print(result)결과:
[10 30 50]순서 변경하기
result = numbers[
[4, 0, 3]
]
print(result)결과:
[50 10 40]2차원 배열에서 특정 위치 선택하기
matrix = np.array([
[10, 20, 30],
[40, 50, 60],
[70, 80, 90]
])
rows = np.array([
0,
1,
2
])
columns = np.array([
2,
1,
0
])
print(
matrix[
rows,
columns
]
)결과:
[30 50 70]선택된 위치:
matrix[0, 2] → 30
matrix[1, 1] → 50
matrix[2, 0] → 70정수 배열 인덱싱을 이용하면 다차원 배열의 임의 위치를 선택할 수 있습니다.
16. 배열의 사칙연산
NumPy 배열의 사칙연산은 기본적으로 같은 위치에 있는 요소끼리 수행됩니다.
a = np.array([
10,
20,
30
])
b = np.array([
1,
2,
3
])덧셈
print(a + b)결과:
[11 22 33]뺄셈
print(a - b)결과:
[ 9 18 27]곱셈
print(a * b)결과:
[10 40 90]주의할 점은 `*`가 행렬 곱셈이 아니라 요소별 곱셈이라는 점입니다.
나눗셈
print(a / b)결과:
[10. 10. 10.]거듭제곱
print(a ** 2)결과:
[100 400 900]나머지
print(a % 3)결과:
[1 2 0]스칼라 연산
배열과 숫자 하나를 계산할 수도 있습니다.
print(a + 100)결과:
[110 120 130]print(a * 0.9)결과:
[ 9. 18. 27.]모든 요소에 같은 계산이 적용됩니다.
17. 유니버설 함수 ufunc
NumPy에는 배열 요소에 수학 연산을 적용하는 다양한 함수가 있습니다.
이러한 함수를 유니버설 함수, 줄여서 `ufunc`라고 부릅니다.
ufunc는 ndarray의 각 요소에 연산을 적용하면서 브로드캐스팅과 데이터 타입 변환 등을 지원하는 벡터화된 함수입니다.
제곱근
numbers = np.array([
1,
4,
9,
16
])
print(np.sqrt(numbers))결과:
[1. 2. 3. 4.]절댓값
numbers = np.array([
-10,
20,
-30
])
print(np.abs(numbers))결과:
[10 20 30]지수 함수
numbers = np.array([
1,
2,
3
])
print(np.exp(numbers))자연로그
numbers = np.array([
1,
np.e,
np.e ** 2
])
print(np.log(numbers))삼각함수
angles = np.array([
0,
np.pi / 2,
np.pi
])
print(np.sin(angles))
print(np.cos(angles))반올림
numbers = np.array([
1.234,
5.678,
9.876
])
print(
np.round(
numbers,
2
)
)결과:
[1.23 5.68 9.88]최소값과 최대값 비교
a = np.array([
10,
50,
30
])
b = np.array([
20,
40,
35
])
print(np.maximum(a, b))
print(np.minimum(a, b))결과:
[20 50 35]
[10 40 30]숫자들에게 함수를 하나씩 배달할 필요 없이 ufunc가 단체 배송을 담당합니다. 📦
18. 배열 통계 계산하기
NumPy는 배열의 기본 통계를 계산하는 함수를 제공합니다.
scores = np.array([
75,
82,
90,
68,
95
])합계
print(np.sum(scores))또는 다음처럼 작성할 수 있습니다.
print(scores.sum())평균
print(scores.mean())최솟값
print(scores.min())최댓값
print(scores.max())중앙값
print(np.median(scores))표준편차
print(scores.std())분산
print(scores.var())최솟값의 위치
print(scores.argmin())최댓값의 위치
print(scores.argmax())전체 예제:
print(
f"합계: {scores.sum()}"
)
print(
f"평균: {scores.mean():.2f}"
)
print(
f"중앙값: {np.median(scores):.2f}"
)
print(
f"최솟값: {scores.min()}"
)
print(
f"최댓값: {scores.max()}"
)
print(
f"표준편차: {scores.std():.2f}"
)19. axis 완벽 이해하기
NumPy 초보자가 가장 자주 길을 잃는 곳이 `axis`입니다.
먼저 다음 배열을 보겠습니다.
scores = np.array([
[80, 90, 70],
[60, 75, 85],
[95, 88, 92]
])배열 구조:
| 학생 | Python | SQL | Java |
|---|---|---|---|
| 1번 | 80 | 90 | 70 |
| 2번 | 60 | 75 | 85 |
| 3번 | 95 | 88 | 92 |
전체 평균
print(scores.mean())모든 값을 하나로 모아 평균을 계산합니다.
axis=0
print(
scores.mean(
axis=0
)
)결과:
[78.33333333 84.33333333 82.33333333]각 열의 평균입니다.
Python 평균
SQL 평균
Java 평균`axis=0`을 제거하며 계산한다고 생각할 수 있습니다.
행 방향으로 내려가며 계산
→ 행 축이 사라짐
→ 열별 결과가 남음axis=1
print(
scores.mean(
axis=1
)
)결과:
[80. 73.33333333 91.66666667]각 행의 평균입니다.
1번 학생 평균
2번 학생 평균
3번 학생 평균열 방향으로 옆으로 계산
→ 열 축이 사라짐
→ 행별 결과가 남음기억 방법
axis=0
→ 세로 방향으로 모아 계산
→ 열별 결과
axis=1
→ 가로 방향으로 모아 계산
→ 행별 결과keepdims 사용하기
계산 후에도 차원 수를 유지하고 싶다면 `keepdims=True`를 사용합니다.
result = scores.mean(
axis=1,
keepdims=True
)
print(result)
print(result.shape)결과:
[[80. ]
[73.33333333]
[91.66666667]]
(3, 1)`axis`는 배열 계산에서 교통정리를 담당합니다.
방향을 잘못 지정하면 평균은 계산되지만 전혀 다른 이야기를 발표할 수 있습니다. 🚦
20. 배열 형태 변경하기
`reshape()`를 사용하면 배열의 형태를 변경할 수 있습니다.
numbers = np.arange(
1,
13
)
print(numbers)결과:
[ 1 2 3 4 5 6 7 8 9 10 11 12]3행 4열로 변경하기
matrix = numbers.reshape(
3,
4
)
print(matrix)결과:
[[ 1 2 3 4]
[ 5 6 7 8]
[ 9 10 11 12]]4행 3열로 변경하기
matrix = numbers.reshape(
4,
3
)
print(matrix)자동으로 크기 계산하기
한쪽 크기에 `-1`을 지정하면 NumPy가 나머지 크기를 계산합니다.
matrix = numbers.reshape(
3,
-1
)
print(matrix.shape)결과:
(3, 4)matrix = numbers.reshape(
-1,
2
)
print(matrix.shape)결과:
(6, 2)요소 개수가 맞아야 한다
12개의 요소를 5행 3열로 변경할 수는 없습니다.
numbers.reshape(
5,
3
)5행 3열에는 15개의 요소가 필요하기 때문입니다.
현재 요소: 12개
필요한 요소: 15개
→ 형태 변경 실패옷장 칸의 모양은 바꿀 수 있지만 양말 개수가 갑자기 늘어나지는 않습니다.
21. 배열 평탄화하기
다차원 배열을 1차원으로 만들 때 `flatten()`이나 `ravel()`을 사용할 수 있습니다.
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])flatten
flattened = matrix.flatten()
print(flattened)결과:
[1 2 3 4 5 6]`flatten()`은 새로운 배열 복사본을 만듭니다.
ravel
flattened = matrix.ravel()
print(flattened)결과:
[1 2 3 4 5 6]`ravel()`은 가능하면 원본 데이터를 공유하는 1차원 뷰를 반환하고, 필요한 경우 복사본을 만들 수 있습니다.
flatten()
→ 항상 독립된 복사본
ravel()
→ 가능하면 원본 데이터 공유원본과 완전히 분리된 결과가 필요하면 `flatten()`을 사용하는 편이 명확합니다.
22. 배열 전치하기
전치는 배열의 행과 열을 뒤바꾸는 작업입니다.
matrix = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(matrix.shape)결과:
(2, 3)T 속성 사용하기
transposed = matrix.T
print(transposed)
print(transposed.shape)결과:
[[1 4]
[2 5]
[3 6]]
(3, 2)transpose 함수 사용하기
transposed = np.transpose(
matrix
)2차원 배열에서는 `.T`와 비슷하게 행과 열을 교환합니다.
변경 전
2행 × 3열
변경 후
3행 × 2열23. 여러 배열 합치기
concatenate
기존 축을 따라 배열을 연결합니다.
a = np.array([
1,
2,
3
])
b = np.array([
4,
5,
6
])
result = np.concatenate([
a,
b
])
print(result)결과:
[1 2 3 4 5 6]2차원 배열 세로 연결
a = np.array([
[1, 2],
[3, 4]
])
b = np.array([
[5, 6]
])
result = np.concatenate(
[a, b],
axis=0
)
print(result)결과:
[[1 2]
[3 4]
[5 6]]2차원 배열 가로 연결
a = np.array([
[1, 2],
[3, 4]
])
b = np.array([
[5],
[6]
])
result = np.concatenate(
[a, b],
axis=1
)
print(result)결과:
[[1 2 5]
[3 4 6]]stack
새로운 축을 만들어 배열을 쌓습니다.
a = np.array([
1,
2,
3
])
b = np.array([
4,
5,
6
])
result = np.stack(
[a, b]
)
print(result)결과:
[[1 2 3]
[4 5 6]]vstack
세로 방향으로 쌓습니다.
result = np.vstack([
a,
b
])hstack
가로 방향으로 연결합니다.
result = np.hstack([
a,
b
])concatenate와 stack 차이
concatenate
→ 기존 축을 따라 연결
stack
→ 새로운 축을 생성해 쌓기24. 배열 나누기
split
배열을 동일한 크기로 나눕니다.
numbers = np.array([
1,
2,
3,
4,
5,
6
])
result = np.split(
numbers,
3
)
print(result)결과:
[array([1, 2]), array([3, 4]), array([5, 6])]특정 위치를 기준으로 나누기
result = np.split(
numbers,
[2, 5]
)
print(result)결과:
[array([1, 2]), array([3, 4, 5]), array([6])]array_split
동일한 크기로 정확히 나누어지지 않아도 분할할 수 있습니다.
numbers = np.array([
1,
2,
3,
4,
5
])
result = np.array_split(
numbers,
3
)
print(result)결과:
[array([1, 2]), array([3, 4]), array([5])]2차원 배열 나누기
matrix = np.arange(
1,
13
).reshape(
3,
4
)
print(
np.hsplit(
matrix,
2
)
)열을 기준으로 두 부분으로 나눕니다.
print(
np.vsplit(
matrix,
3
)
)행을 기준으로 세 부분으로 나눕니다.
25. 브로드캐스팅 이해하기
브로드캐스팅은 NumPy의 가장 강력하면서도 처음 보면 살짝 마법처럼 느껴지는 기능입니다.
배열과 숫자 하나를 더해 보겠습니다.
numbers = np.array([
10,
20,
30
])
print(numbers + 5)결과:
[15 25 35]숫자 `5`가 배열의 모든 요소에 적용되었습니다.
개념적으로는 다음과 비슷합니다.
[10, 20, 30]
+
[ 5, 5, 5]하지만 NumPy는 반드시 같은 크기의 배열을 실제로 새로 만들지 않고 연산할 수 있습니다.
브로드캐스팅은 서로 다른 형태의 배열을 특정 규칙에 따라 호환시켜 연산하는 기능이며, 불필요한 데이터 복사를 피하면서 벡터화된 계산을 가능하게 합니다.
2차원 배열과 1차원 배열 연산
matrix = np.array([
[10, 20, 30],
[40, 50, 60]
])
offset = np.array([
1,
2,
3
])
print(matrix + offset)결과:
[[11 22 33]
[41 52 63]]`offset`이 각 행에 적용됩니다.
열 방향 브로드캐스팅
matrix = np.array([
[10, 20, 30],
[40, 50, 60]
])
column = np.array([
[100],
[200]
])
print(matrix + column)결과:
[[110 120 130]
[240 250 260]]브로드캐스팅 규칙
NumPy는 배열의 마지막 차원부터 크기를 비교합니다.
두 차원이 다음 조건 중 하나를 만족하면 호환됩니다.
두 차원의 크기가 같다.
둘 중 하나의 크기가 1이다.예시:
(2, 3)
( 3)
→ 호환 가능(2, 3)
(2, 1)
→ 호환 가능(2, 3)
(2, 2)
→ 호환 불가능실전 예제: 학생별 점수 보정
scores = np.array([
[80, 75, 90],
[60, 85, 70],
[95, 88, 92]
])
bonus = np.array([
5,
3,
2
])
adjusted = scores + bonus
print(adjusted)각 과목에 서로 다른 보너스 점수가 적용됩니다.
브로드캐스팅은 작은 배열이 큰 배열의 무대에 맞춰 몸집을 늘린 것처럼 행동하는 기능입니다.
실제로 거대한 복사본을 만들지 않는다는 점이 반전입니다. 🎩
26. where와 clip 활용하기
where로 조건에 따라 값 선택하기
scores = np.array([
55,
72,
88,
91,
63
])
result = np.where(
scores >= 80,
"합격",
"재시험"
)
print(result)결과:
['재시험' '재시험' '합격' '합격' '재시험']구조:
np.where(
조건,
조건이 True일 때 값,
조건이 False일 때 값
)값 변경하기
adjusted = np.where(
scores < 60,
60,
scores
)
print(adjusted)결과:
[60 72 88 91 63]60점 미만의 값을 60으로 변경했습니다.
조건을 만족하는 위치 찾기
positions = np.where(
scores >= 80
)
print(positions)결과:
(array([2, 3]),)clip으로 범위 제한하기
values = np.array([
-10,
20,
50,
120
])
result = np.clip(
values,
0,
100
)
print(result)결과:
[ 0 20 50 100]0보다 작으면 0
100보다 크면 100
나머지는 원래 값 유지이미지 픽셀 값, 점수, 확률, 센서 허용 범위 등을 제한할 때 활용할 수 있습니다.
27. 결측값 NaN 처리하기
NumPy에서는 실수 배열의 결측값을 주로 `np.nan`으로 표현합니다.
values = np.array([
10.0,
20.0,
np.nan,
40.0
])
print(values)결과:
[10. 20. nan 40.]NaN 확인하기
print(
np.isnan(values)
)결과:
[False False True False]일반 평균 계산하기
print(
values.mean()
)결과:
nanNaN이 포함되어 있어 계산 결과도 NaN이 됩니다.
NaN을 제외한 평균
print(
np.nanmean(values)
)결과:
23.333333333333332NaN을 제외한 통계 함수
print(np.nansum(values))
print(np.nanmin(values))
print(np.nanmax(values))
print(np.nanstd(values))NaN 제거하기
cleaned = values[
~np.isnan(values)
]
print(cleaned)결과:
[10. 20. 40.]NaN을 특정 값으로 변경하기
replaced = np.nan_to_num(
values,
nan=0.0
)
print(replaced)결과:
[10. 20. 0. 40.]정수 배열은 일반적인 `np.nan`을 직접 표현할 수 없으므로, 결측값이 필요한 수치 데이터는 실수 배열로 변환하는 경우가 많습니다.
values = np.array(
[10, 20, 30],
dtype=np.float64
)
values[1] = np.nan결측값은 숫자 세계의 빈 의자입니다.
빈 의자를 0점이라고 단정하기 전에 정말 결석인지, 아직 점수가 입력되지 않은 것인지 확인해야 합니다.
28. 정렬과 고유값 찾기
정렬하기
numbers = np.array([
50,
10,
40,
20,
30
])
sorted_numbers = np.sort(
numbers
)
print(sorted_numbers)결과:
[10 20 30 40 50]기본적으로 `np.sort()`는 정렬된 새 배열을 반환합니다.
원본 배열 정렬하기
numbers.sort()
print(numbers)내림차순 정렬하기
result = np.sort(
numbers
)[::-1]
print(result)정렬 순서의 인덱스 찾기
scores = np.array([
75,
92,
68,
88
])
order = np.argsort(
scores
)
print(order)결과:
[2 0 3 1]높은 순서로 인덱스를 얻으려면 다음과 같이 작성합니다.
descending = np.argsort(
scores
)[::-1]
print(descending)고유값 찾기
categories = np.array([
"A",
"B",
"A",
"C",
"B",
"A"
])
unique_values = np.unique(
categories
)
print(unique_values)결과:
['A' 'B' 'C']고유값별 개수 계산하기
values, counts = np.unique(
categories,
return_counts=True
)
print(values)
print(counts)결과:
['A' 'B' 'C']
[3 2 1]딕셔너리 형태로 묶어 보겠습니다.
summary = dict(
zip(
values,
counts
)
)
print(summary)결과:
{'A': 3, 'B': 2, 'C': 1}29. 난수 생성하기
난수는 시뮬레이션, 테스트 데이터 생성, 머신러닝 데이터 분할 등에 사용됩니다.
현대적인 NumPy 코드에서는 `default_rng()`로 난수 생성기 객체를 만드는 방식을 사용할 수 있습니다.
import numpy as np
rng = np.random.default_rng()NumPy는 난수 생성 기능을 비롯해 수치 계산, 선형대수, 푸리에 변환 등을 제공합니다.
0 이상 1 미만의 실수
numbers = rng.random(5)
print(numbers)정수 난수
numbers = rng.integers(
low=1,
high=101,
size=5
)
print(numbers)1 이상 101 미만의 정수 다섯 개를 생성합니다.
2차원 난수 배열
matrix = rng.integers(
0,
10,
size=(3, 4)
)
print(matrix)정규분포 난수
numbers = rng.normal(
loc=100,
scale=15,
size=1000
)평균 loc: 100
표준편차 scale: 15
개수: 1000데이터 무작위 선택
items = np.array([
"사과",
"바나나",
"포도",
"딸기"
])
selected = rng.choice(
items,
size=2,
replace=False
)
print(selected)배열 순서 섞기
numbers = np.array([
1,
2,
3,
4,
5
])
rng.shuffle(numbers)
print(numbers)같은 결과 재현하기
고정된 시드를 사용하면 동일한 난수 흐름을 재현할 수 있습니다.
rng = np.random.default_rng(
2026
)
print(
rng.integers(
1,
100,
size=5
)
)테스트와 예제에서는 결과 재현을 위해 시드를 지정하는 것이 유용합니다.
난수라고 해서 완전히 자유롭게 뛰어다니는 숫자는 아닙니다.
시드를 지정하면 같은 안무를 다시 공연할 수 있습니다. 🎲
30. 복사본과 뷰의 차이
NumPy 배열을 다룰 때 반드시 알아야 할 개념이 복사본과 뷰입니다.
일반 대입
original = np.array([
10,
20,
30
])
other = original
other[0] = 999
print(original)결과:
[999 20 30]`other`와 `original`이 같은 배열 객체를 가리키기 때문입니다.
copy 사용하기
original = np.array([
10,
20,
30
])
copied = original.copy()
copied[0] = 999
print(original)
print(copied)결과:
[10 20 30]
[999 20 30]두 배열이 독립적으로 관리됩니다.
슬라이싱 결과는 원본을 공유할 수 있다
original = np.array([
10,
20,
30,
40
])
view = original[1:3]
view[0] = 999
print(original)결과:
[ 10 999 30 40]슬라이싱 결과가 원본 데이터의 뷰이기 때문에 수정 사항이 원본에 반영됩니다.
NumPy에서 기본 슬라이싱은 뷰를 반환하는 경우가 많고, 고급 인덱싱은 복사본을 반환합니다. 뷰는 같은 데이터를 공유하므로 메모리를 절약하지만 예상하지 못한 원본 변경을 일으킬 수 있습니다.
원본과 완전히 분리하려면 명시적으로 복사합니다.
independent = original[
1:3
].copy()메모리 공유 여부 확인하기
print(
np.shares_memory(
original,
view
)
)결과:
True배열이 원본과 같은 방을 쓰는지, 독립된 집으로 이사했는지 확인하는 과정입니다. 🏠
31. 행렬과 선형대수 계산하기
NumPy는 행렬 곱셈, 연립방정식, 고유값, 행렬식 등의 선형대수 기능을 제공합니다.
NumPy의 선형대수 함수는 가능한 경우 BLAS와 LAPACK 같은 최적화된 저수준 라이브러리를 활용합니다.
요소별 곱셈
a = np.array([
[1, 2],
[3, 4]
])
b = np.array([
[5, 6],
[7, 8]
])
print(a * b)결과:
[[ 5 12]
[21 32]]행렬 곱셈
행렬 곱셈에는 `@` 연산자를 사용할 수 있습니다.
result = a @ b
print(result)결과:
[[19 22]
[43 50]]다음과 같이 작성할 수도 있습니다.
result = np.matmul(
a,
b
)2차원 행렬 곱셈에서는 `@` 또는 `matmul()` 사용이 명확합니다. `np.dot()`도 2차원 배열의 행렬 곱셈을 수행하지만 공식 문서는 이 경우 `matmul()`이나 `@`를 선호한다고 설명합니다.
벡터 내적
a = np.array([
1,
2,
3
])
b = np.array([
4,
5,
6
])
print(
np.dot(
a,
b
)
)결과:
32계산:
1×4 + 2×5 + 3×6 = 32행렬식
matrix = np.array([
[1, 2],
[3, 4]
])
determinant = np.linalg.det(
matrix
)
print(determinant)역행렬
inverse = np.linalg.inv(
matrix
)
print(inverse)연립방정식 풀기
다음 방정식을 계산해 보겠습니다.
2x + y = 5
x - y = 1행렬로 표현합니다.
coefficients = np.array([
[2, 1],
[1, -1]
])
answers = np.array([
5,
1
])
solution = np.linalg.solve(
coefficients,
answers
)
print(solution)결과:
[2. 1.]따라서 다음과 같습니다.
x = 2
y = 1연립방정식을 풀 때는 역행렬을 직접 계산해 곱하기보다 `np.linalg.solve()`를 사용하는 방식이 일반적으로 더 직접적입니다.
고유값과 고유벡터
matrix = np.array([
[2, 0],
[0, 3]
])
eigenvalues, eigenvectors = (
np.linalg.eig(matrix)
)
print(eigenvalues)
print(eigenvectors)선형대수는 인공지능, 컴퓨터 그래픽, 물리 시뮬레이션, 통계 모델링 등의 기반이 됩니다.
32. NumPy 배열 저장하고 불러오기
NumPy 배열을 파일로 저장해 나중에 다시 사용할 수 있습니다.
배열 하나 저장하기
array = np.array([
[1, 2, 3],
[4, 5, 6]
])
np.save(
"numbers.npy",
array
)`numbers.npy` 파일이 생성됩니다.
배열 불러오기
loaded = np.load(
"numbers.npy",
allow_pickle=False
)
print(loaded)여러 배열 저장하기
scores = np.array([
80,
90,
70
])
names = np.array([
"민준",
"서연",
"지훈"
])
np.savez(
"student_data.npz",
scores=scores,
names=names
)여러 배열 불러오기
data = np.load(
"student_data.npz",
allow_pickle=False
)
print(data["scores"])
print(data["names"])`savez()`는 여러 배열을 하나의 `.npz` 파일에 이름과 함께 저장할 수 있습니다. NumPy 공식 문서는 보안과 이식성을 위해 객체 배열이 필요하지 않다면 `allow_pickle=False` 사용을 권장합니다.
압축해서 저장하기
np.savez_compressed(
"student_data.npz",
scores=scores,
names=names
)텍스트 파일로 저장하기
np.savetxt(
"numbers.csv",
array,
delimiter=",",
fmt="%d"
)텍스트 파일 불러오기
loaded = np.loadtxt(
"numbers.csv",
delimiter=",",
dtype=np.int64
)
print(loaded)NumPy는 `.npy`, `.npz` 바이너리 파일과 `loadtxt()`, `savetxt()`, `genfromtxt()` 등을 이용한 텍스트 입출력을 지원합니다.
33. pandas·Matplotlib와 함께 사용하기
NumPy는 혼자 움직이는 고독한 계산 기계가 아닙니다.
pandas와 Matplotlib의 든든한 숫자 엔진 역할을 합니다.
NumPy 배열을 DataFrame으로 변환하기
import numpy as np
import pandas as pd
scores = np.array([
[80, 90, 70],
[60, 75, 85],
[95, 88, 92]
])
df = pd.DataFrame(
scores,
columns=[
"Python",
"SQL",
"Java"
],
index=[
"민준",
"서연",
"지훈"
]
)
print(df)DataFrame을 NumPy 배열로 변환하기
array = df.to_numpy()
print(array)
print(type(array))NumPy 배열 그래프로 표현하기
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(
0,
2 * np.pi,
200
)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(
x,
y
)
ax.set_title(
"Sine Wave"
)
ax.set_xlabel("x")
ax.set_ylabel("sin(x)")
ax.grid(
alpha=0.3
)
plt.show()무작위 데이터의 분포 확인하기
rng = np.random.default_rng(
2026
)
values = rng.normal(
loc=100,
scale=15,
size=1000
)
fig, ax = plt.subplots()
ax.hist(
values,
bins=30,
edgecolor="black"
)
ax.set_title(
"Random Data Distribution"
)
plt.show()NumPy
→ 숫자 배열과 계산
pandas
→ 행과 열이 있는 데이터 분석
Matplotlib
→ 계산 결과 시각화세 라이브러리가 모이면 데이터 처리 삼총사가 완성됩니다. 🐼📊🔢
34. NumPy 성능을 높이는 방법
Python 반복문보다 배열 연산 사용하기
좋지 않은 예:
result = []
for value in array:
result.append(
value * 2 + 10
)NumPy 방식:
result = (
array * 2
+ 10
)NumPy의 ufunc는 배열의 각 요소에 벡터화된 방식으로 연산을 적용합니다.
불필요한 복사 줄이기
view = array[
100:200
]원본과 데이터를 공유해도 괜찮다면 뷰를 활용할 수 있습니다.
반대로 원본 보호가 중요하면 명시적으로 복사합니다.
independent = array[
100:200
].copy()적절한 dtype 사용하기
small_values = np.array(
[1, 2, 3, 4],
dtype=np.int16
)값의 범위가 작고 정밀도가 충분하다면 더 작은 타입을 검토할 수 있습니다.
단, 표현 범위를 넘어서는 값이 들어오지 않는지 확인해야 합니다.
중간 배열을 줄이기
다음 계산은 중간 결과 배열을 만들 수 있습니다.
result = (
array * 2
+ 10
)메모리 사용을 세밀하게 제어해야 하는 고급 상황에서는 `out` 인수를 활용할 수 있습니다.
result = np.empty_like(
array,
dtype=np.float64
)
np.multiply(
array,
2,
out=result
)
np.add(
result,
10,
out=result
)브로드캐스팅 활용하기
큰 배열을 반복해서 생성하지 않고 작은 배열을 브로드캐스팅할 수 있습니다.
matrix = np.ones(
(1000, 3)
)
offset = np.array([
10,
20,
30
])
result = matrix + offset대용량 파일은 메모리 매핑 검토하기
매우 큰 배열을 한 번에 메모리에 올리기 어려울 때는 메모리 매핑을 검토할 수 있습니다.
mapped = np.memmap(
"large_array.dat",
dtype=np.float32,
mode="w+",
shape=(10000, 1000)
)`np.memmap`은 전체 파일을 메모리에 한 번에 읽지 않고 디스크에 저장된 큰 배열의 일부 구간에 접근할 수 있는 배열 형태의 객체입니다.
반복문과 배열 연산 속도 비교하기
import time
import numpy as np
size = 1_000_000
python_numbers = list(
range(size)
)
numpy_numbers = np.arange(
size
)
start = time.perf_counter()
python_result = [
number * 2
for number in python_numbers
]
python_time = (
time.perf_counter()
- start
)
start = time.perf_counter()
numpy_result = (
numpy_numbers * 2
)
numpy_time = (
time.perf_counter()
- start
)
print(
f"Python 리스트: "
f"{python_time:.6f}초"
)
print(
f"NumPy 배열: "
f"{numpy_time:.6f}초"
)실행 시간은 컴퓨터, Python 버전, NumPy 빌드 환경에 따라 달라집니다.
따라서 특정 배수보다 직접 측정한 결과를 기준으로 판단해야 합니다.
35. 실전 미니 프로젝트: 센서 데이터 이상 탐지기
이번에는 30일 동안 수집한 시간별 온도 데이터를 분석하는 프로그램을 만들어 보겠습니다.
프로젝트 목표
30일 × 24시간 온도 데이터 생성
→ 비정상적인 온도 데이터 삽입
→ 전체 평균과 표준편차 계산
→ 일별·시간대별 평균 계산
→ 이상 데이터 위치 탐색
→ 결과 파일 저장
→ 그래프로 시각화전체 코드
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
OUTPUT_FILE = Path(
"temperature_analysis.npz"
)
CHART_FILE = Path(
"daily_temperature.png"
)
def create_sensor_data() -> np.ndarray:
"""30일간의 시간별 온도 데이터를 생성합니다."""
rng = np.random.default_rng(
2026
)
temperatures = rng.normal(
loc=24.0,
scale=2.5,
size=(30, 24)
)
# 테스트를 위한 이상값 삽입
temperatures[4, 14] = 38.5
temperatures[12, 3] = 7.0
temperatures[21, 18] = 41.0
return temperatures
def analyze_temperatures(
temperatures: np.ndarray
) -> dict[str, np.ndarray | float]:
"""온도 배열의 통계와 이상값을 계산합니다."""
overall_mean = float(
temperatures.mean()
)
overall_std = float(
temperatures.std()
)
daily_mean = temperatures.mean(
axis=1
)
daily_max = temperatures.max(
axis=1
)
daily_min = temperatures.min(
axis=1
)
hourly_mean = temperatures.mean(
axis=0
)
z_scores = (
temperatures - overall_mean
) / overall_std
anomaly_mask = (
np.abs(z_scores) >= 3
)
anomaly_positions = np.argwhere(
anomaly_mask
)
anomaly_values = temperatures[
anomaly_mask
]
return {
"overall_mean": overall_mean,
"overall_std": overall_std,
"daily_mean": daily_mean,
"daily_max": daily_max,
"daily_min": daily_min,
"hourly_mean": hourly_mean,
"anomaly_positions": anomaly_positions,
"anomaly_values": anomaly_values
}
def print_report(
analysis: dict[str, np.ndarray | float]
) -> None:
"""분석 결과를 화면에 출력합니다."""
print(
"===== 온도 센서 분석 결과 ====="
)
print(
"전체 평균 온도: "
f"{analysis['overall_mean']:.2f}℃"
)
print(
"전체 표준편차: "
f"{analysis['overall_std']:.2f}"
)
positions = analysis[
"anomaly_positions"
]
values = analysis[
"anomaly_values"
]
print(
f"이상 데이터 개수: {len(values)}"
)
for position, value in zip(
positions,
values
):
day_index, hour = position
print(
f"{day_index + 1}일차 "
f"{hour}시: "
f"{value:.2f}℃"
)
def save_result(
temperatures: np.ndarray,
analysis: dict[str, np.ndarray | float]
) -> None:
"""원본 배열과 분석 결과를 NPZ 파일로 저장합니다."""
np.savez_compressed(
OUTPUT_FILE,
temperatures=temperatures,
daily_mean=analysis[
"daily_mean"
],
daily_max=analysis[
"daily_max"
],
daily_min=analysis[
"daily_min"
],
hourly_mean=analysis[
"hourly_mean"
],
anomaly_positions=analysis[
"anomaly_positions"
],
anomaly_values=analysis[
"anomaly_values"
]
)
def create_chart(
analysis: dict[str, np.ndarray | float]
) -> None:
"""일별 평균·최고·최저 온도를 그래프로 저장합니다."""
days = np.arange(
1,
31
)
daily_mean = analysis[
"daily_mean"
]
daily_max = analysis[
"daily_max"
]
daily_min = analysis[
"daily_min"
]
fig, ax = plt.subplots(
figsize=(12, 6)
)
ax.plot(
days,
daily_mean,
marker="o",
label="Daily mean"
)
ax.plot(
days,
daily_max,
linestyle="--",
label="Daily max"
)
ax.plot(
days,
daily_min,
linestyle=":",
label="Daily min"
)
ax.set_title(
"Daily Temperature Analysis"
)
ax.set_xlabel("Day")
ax.set_ylabel("Temperature")
ax.grid(
alpha=0.3
)
ax.legend()
fig.savefig(
CHART_FILE,
dpi=200,
bbox_inches="tight"
)
plt.show()
def main() -> None:
temperatures = create_sensor_data()
analysis = analyze_temperatures(
temperatures
)
print_report(
analysis
)
save_result(
temperatures,
analysis
)
create_chart(
analysis
)
print()
print(
f"데이터 저장: "
f"{OUTPUT_FILE.resolve()}"
)
print(
f"그래프 저장: "
f"{CHART_FILE.resolve()}"
)
if __name__ == "__main__":
main()데이터 구조
temperatures.shape
→ (30, 24)의미:
30개의 행
→ 30일
24개의 열
→ 하루 24시간일별 평균
daily_mean = temperatures.mean(
axis=1
)각 행을 가로 방향으로 계산합니다.
1일차 24개 값의 평균
2일차 24개 값의 평균
...
30일차 24개 값의 평균시간대별 평균
hourly_mean = temperatures.mean(
axis=0
)각 열을 세로 방향으로 계산합니다.
30일 동안의 0시 평균
30일 동안의 1시 평균
...
30일 동안의 23시 평균이상값 판단
z_scores = (
temperatures - overall_mean
) / overall_std평균으로부터 표준편차의 세 배 이상 떨어진 값을 이상 후보로 선택합니다.
anomaly_mask = (
np.abs(z_scores) >= 3
)이상값 위치 찾기
anomaly_positions = np.argwhere(
anomaly_mask
)결과 예시:
[[ 4 14]
[12 3]
[21 18]]의미:
4번 인덱스 날짜의 14시
12번 인덱스 날짜의 3시
21번 인덱스 날짜의 18시사용자에게 보여 줄 때는 날짜 인덱스에 1을 더합니다.
day_index + 1이번 프로젝트에서 사용한 핵심 기능은 다음과 같습니다.
default_rng()
mean()
std()
max()
min()
axis
브로드캐스팅
불리언 배열
argwhere()
savez_compressed()수백 개의 센서 데이터가 NumPy에 들어가자마자 줄을 맞춰 서고, 수상한 온도 세 개가 결국 검문소에서 붙잡혔습니다. 🚨
36. 자주 발생하는 오류
오류 1. NumPy를 찾을 수 없음
ModuleNotFoundError:
No module named 'numpy'해결:
python -m pip install numpy현재 실행 중인 Python 환경에 설치되었는지 확인합니다.
python -m pip show numpy오류 2. 배열 형태가 일정하지 않음
data = np.array([
[1, 2, 3],
[4, 5]
])행마다 길이가 다르면 일반적인 2차원 숫자 배열을 만들 수 없습니다.
다음처럼 길이를 맞춰야 합니다.
data = np.array([
[1, 2, 3],
[4, 5, 0]
])결측값을 의미한다면 실수 배열과 `np.nan`을 검토할 수 있습니다.
data = np.array([
[1, 2, 3],
[4, 5, np.nan]
])오류 3. reshape 요소 개수가 맞지 않음
ValueError:
cannot reshape array현재 요소 개수와 목표 크기를 확인합니다.
print(array.size)목표 행 × 목표 열
= 현재 size오류 4. 브로드캐스팅 실패
ValueError:
operands could not be broadcast together예:
a = np.ones(
(2, 3)
)
b = np.ones(
(2, 2)
)
print(a + b)형태:
a: (2, 3)
b: (2, 2)마지막 차원의 크기 `3`과 `2`가 다르고 어느 쪽도 `1`이 아니므로 호환되지 않습니다.
먼저 `shape`를 출력합니다.
print(a.shape)
print(b.shape)오류 5. 배열 비교에 and 또는 or 사용
다음 코드는 오류가 발생할 수 있습니다.
result = array[
(array >= 10)
and (array <= 20)
]NumPy 조건은 `&`와 `|`를 사용합니다.
result = array[
(array >= 10)
& (array <= 20)
]각 조건을 괄호로 감싸야 합니다.
오류 6. 원본 배열이 예상하지 못하게 변경됨
part = original[1:4]
part[0] = 999슬라이싱 결과가 원본 데이터를 공유하는 뷰일 수 있습니다.
독립된 배열이 필요하면 복사합니다.
part = original[
1:4
].copy()오류 7. NaN 평균 결과가 NaN
values.mean()배열에 `np.nan`이 포함되어 있다면 다음 함수를 검토합니다.
np.nanmean(values)또는 결측값을 먼저 제거합니다.
cleaned = values[
~np.isnan(values)
]오류 8. 정수 배열에 NaN 입력
numbers = np.array([
1,
2,
3
])
numbers[1] = np.nan정수 배열은 일반적인 NaN을 저장할 수 없습니다.
실수 배열로 변환합니다.
numbers = numbers.astype(
np.float64
)
numbers[1] = np.nan오류 9. 요소별 곱셈과 행렬 곱셈 혼동
a * b요소별 곱셈입니다.
a @ b행렬 곱셈입니다.
수학 기호는 비슷해 보여도 계산 회의의 안건이 완전히 다릅니다.
37. 연습 문제
문제 1
1부터 10까지의 정수를 가진 NumPy 배열을 만들어 보세요.
힌트
np.arange(
1,
11
)문제 2
다음 배열의 `ndim`, `shape`, `size`, `dtype`을 출력해 보세요.
array = np.array([
[10, 20, 30],
[40, 50, 60]
])문제 3
1부터 12까지의 숫자를 3행 4열 배열로 변경해 보세요.
힌트
np.arange(
1,
13
).reshape(
3,
4
)문제 4
다음 점수에서 80점 이상인 값만 선택해 보세요.
scores = np.array([
55,
82,
91,
68,
77,
95
])문제 5
80점 이상이면 `"합격"`, 미만이면 `"재시험"`을 저장한 배열을 만들어 보세요.
힌트
np.where(
scores >= 80,
"합격",
"재시험"
)문제 6
다음 2차원 배열의 행별 평균과 열별 평균을 계산해 보세요.
scores = np.array([
[80, 90, 70],
[60, 75, 85],
[95, 88, 92]
])힌트
scores.mean(
axis=1
)scores.mean(
axis=0
)문제 7
다음 두 배열을 세로 방향과 가로 방향으로 각각 합쳐 보세요.
a = np.array([
[1, 2],
[3, 4]
])
b = np.array([
[5, 6],
[7, 8]
])힌트
np.vstack([
a,
b
])np.hstack([
a,
b
])문제 8
0부터 100까지 균등하게 나눈 숫자 11개를 생성해 보세요.
힌트
np.linspace(
0,
100,
11
)문제 9
난수 생성기를 이용해 1부터 45까지 중복되지 않는 숫자 6개를 선택해 보세요.
힌트
rng = np.random.default_rng()
numbers = rng.choice(
np.arange(
1,
46
),
size=6,
replace=False
)정렬까지 적용해 보세요.
numbers = np.sort(
numbers
)문제 10
다음 행렬의 행렬 곱셈 결과를 구해 보세요.
a = np.array([
[1, 2],
[3, 4]
])
b = np.array([
[5, 6],
[7, 8]
])힌트
a @ b문제 11
평균이 100이고 표준편차가 15인 정규분포 난수 1,000개를 생성한 뒤 평균과 표준편차를 확인해 보세요.
문제 12
생성한 난수 데이터를 Matplotlib 히스토그램으로 표현해 보세요.
fig, ax = plt.subplots()
ax.hist(
values,
bins=30,
edgecolor="black"
)
plt.show()38. 핵심 요약
NumPy 불러오기
import numpy as np배열 생성
array = np.array([
1,
2,
3
])배열 정보
array.ndim
array.shape
array.size
array.dtype
array.itemsize
array.nbytes기본 배열 만들기
np.zeros((2, 3))
np.ones((2, 3))
np.full((2, 3), 7)
np.eye(3)숫자 범위 생성
np.arange(
0,
10,
2
)
np.linspace(
0,
10,
5
)조건 필터링
array[
array >= 10
]여러 조건
array[
(array >= 10)
& (array <= 20)
]배열 계산
array + 10
array * 2
array ** 2통계 계산
array.sum()
array.mean()
array.min()
array.max()
array.std()
array.var()축별 계산
array.mean(
axis=0
)
array.mean(
axis=1
)형태 변경
array.reshape(
3,
4
)평탄화
array.flatten()
array.ravel()전치
array.T배열 결합
np.concatenate([
a,
b
])
np.vstack([
a,
b
])
np.hstack([
a,
b
])조건에 따른 값 생성
np.where(
condition,
true_value,
false_value
)범위 제한
np.clip(
array,
minimum,
maximum
)난수 생성
rng = np.random.default_rng(
2026
)
rng.random(5)
rng.integers(
1,
100,
size=5
)행렬 곱셈
a @ b배열 저장과 불러오기
np.save(
"array.npy",
array
)
loaded = np.load(
"array.npy",
allow_pickle=False
)39. 마무리
이번 시간에는 Python 수치 계산의 핵심 라이브러리인 NumPy를 알아보았습니다.
NumPy를 사용하면 다음과 같은 작업을 효율적으로 처리할 수 있습니다.
대규모 숫자 배열 생성
다차원 데이터 관리
조건에 맞는 값 검색
배열 전체 사칙연산
평균과 표준편차 계산
배열 형태 변경
브로드캐스팅 연산
난수와 테스트 데이터 생성
행렬 곱셈과 연립방정식 계산
바이너리 배열 파일 저장
pandas와 Matplotlib 연동NumPy를 처음 접하면 다음과 같은 코드가 낯설 수 있습니다.
adjusted = (
scores
- scores.mean(
axis=0
)
) / scores.std(
axis=0
)하지만 한 단계씩 해석하면 어렵지 않습니다.
과목별 평균을 계산한다
→ 원래 점수에서 평균을 뺀다
→ 과목별 표준편차로 나눈다
→ 점수를 같은 기준으로 변환한다NumPy의 핵심은 단순히 숫자를 저장하는 것이 아닙니다.
숫자 하나씩 처리하던 사고방식에서 벗어나 배열 전체를 하나의 단위로 계산하는 것입니다.
Python 반복문:
첫 번째 값 계산
두 번째 값 계산
세 번째 값 계산
...
NumPy:
배열 전체 계산 시작!pandas가 데이터를 표로 정리하고 Matplotlib가 데이터를 그래프로 보여 준다면, NumPy는 그 아래에서 숫자를 빠르게 운반하는 엔진입니다.
화면에는 잘 보이지 않지만 엔진이 멈추면 데이터 분석 열차도 출발할 수 없습니다. 🚂
이제 수백만 개의 숫자가 등장해도 당황할 필요가 없습니다.
리스트와 반복문을 먼저 꺼내기 전에 이렇게 질문해 보세요.
“이 계산, NumPy 배열 전체에 한 번에 시킬 수 있지 않을까?” 🔢⚡
다음 편 예고
[Python 완전정복 시리즈 #27] SciPy 완벽 이해하기 | NumPy를 넘어 과학 계산과 최적화 문제를 해결하는 방법
다음 시간에는 NumPy의 배열 계산 능력을 확장하는 SciPy를 알아봅니다.
통계 분석, 적분, 최적화, 보간, 신호 처리, 거리 계산, 연립방정식까지 과학과 공학 문제를 Python으로 해결하는 방법을 실습해 보겠습니다.
#Python #파이썬 #Python강좌 #파이썬기초 #NumPy #넘파이 #ndarray #배열 #다차원배열 #브로드캐스팅 #행렬연산 #선형대수 #데이터분석 #수치계산 #pandas #Matplotlib #머신러닝 #인공지능 #코딩공부 #프로그래밍
