학습 목표
- ndarray를 생성하고 dtype·shape로 배열의 구조를 파악할 수 있다
- 인덱싱·슬라이싱과 불리언 마스크로 원하는 원소를 선택할 수 있다
- for 루프 없이 벡터화 연산과 브로드캐스팅으로 배열을 계산할 수 있다
- axis 인자로 행·열 방향 집계를 구분해 수행할 수 있다
JS에는 대응이 없는 첫 개념입니다. Array는 있지만 "숫자 n차원 배열을 한 번에 계산하는" 도구는 없습니다(TypedArray가 가장 가깝지만 벡터 연산은 없습니다). NumPy의 ndarray는 pandas와 scikit-learn이 딛고 선 토대이고, AICE 실기의 모든 데이터가 결국 이 배열 위에서 움직입니다. 이 챕터에서 얻어야 할 단 하나는 **"루프 대신 배열 통째로 계산한다"**는 사고 전환입니다.
Python 리스트는 벡터 연산을 못 합니다. [1,2,3] * 2는 원소가 2배가 아니라 리스트가 2번 반복됩니다(JS의 배열과 같은 함정). 계산이 목적이면 np.array로 감쌉니다.
| JavaScript | Python (NumPy) |
|---|---|
const a = new Float64Array([1,2,3]); |
a = np.array([1, 2, 3]) |
a.length |
a.shape |
import numpy as np
nums = [1, 2, 3]
print(nums * 2) # 리스트: 반복
# 출력: [1, 2, 3, 1, 2, 3]
arr = np.array([1, 2, 3])
print(arr * 2) # 배열: 원소별 2배
# 출력: [2 4 6]자주 쓰는 생성 함수들입니다. AICE 실기에서 초기 배열·가중치·더미 데이터를 만들 때 등장합니다.
z = np.zeros(3) # [0. 0. 0.]
o = np.ones((2, 3)) # 2행 3열 1로 채움
r = np.arange(0, 10, 2) # range의 배열판
print(r)
# 출력: [0 2 4 6 8]
lin = np.linspace(0, 1, 5) # 0~1 균등 5개
print(lin)
# 출력: [0. 0.25 0.5 0.75 1. ]
⚠️ 함정:arr * 2가 원소별 연산인 것은 NumPy 배열일 때만입니다. 순수 Python 리스트에* 2를 쓰면 JSArray.prototype.concat처럼 이어붙이기가 됩니다. 벡터 연산이 필요하면 먼저np.array로 변환하세요.
배열은 리스트와 달리 원소 타입이 하나로 고정됩니다. dtype이 그 타입, shape가 차원 구조입니다.
mat = np.array([[1, 2, 3],
[4, 5, 6]])
print(mat.shape) # (행, 열)
# 출력: (2, 3)
print(mat.ndim) # 차원 수
# 출력: 2
print(mat.dtype) # 원소 타입
# 출력: int64정수 배열에 실수를 섞으면 전체가 float64로 올라갑니다. 타입을 바꾸려면 astype을 씁니다.
f = np.array([1, 2, 3], dtype="float64")
print(f)
# 출력: [1. 2. 3.]
i = f.astype("int64")
print(i.dtype)
# 출력: int64reshape로 shape를 바꿉니다. 원소 수만 맞으면 됩니다. -1은 "나머지 자동 계산"입니다.
a = np.arange(6)
print(a.reshape(2, 3))
# 출력: [[0 1 2]
print(a.reshape(3, -1).shape) # -1 = 자동
# 출력: (3, 2)🎯 AICE: 모델에 데이터를 넣기 전
X.shape를 찍어(샘플 수, 특성 수)를 확인하는 것은 실기의 기본 습관입니다. shape가 맞지 않아 나는 에러가 가장 흔하므로, 배열을 만들 때마다 shape를 눈으로 확인하세요.
1차원 인덱싱·슬라이싱은 4장에서 배운 리스트와 똑같습니다. 새로운 것은 2차원을 쉼표로 접근하는 문법입니다. JS에서 mat[i][j]로 쓰던 것을 NumPy는 mat[i, j] 한 쌍으로 씁니다.
| JavaScript | Python (NumPy) |
|---|---|
mat[0][1] |
mat[0, 1] |
mat.map(row => row[0]) |
mat[:, 0] (0열 전체) |
mat = np.array([[10, 20, 30],
[40, 50, 60]])
print(mat[0, 1]) # 0행 1열
# 출력: 20
print(mat[:, 0]) # 모든 행의 0열
# 출력: [10 40]
print(mat[1, :]) # 1행 전체
# 출력: [40 50 60]
print(mat[:, 1:]) # 1열부터 끝까지
# 출력: [[20 30]불리언 인덱싱은 pandas의 핵심 전환점으로 이어지는 가장 중요한 문법입니다. 조건식이 True/False 배열을 만들고, 그 마스크로 원소를 골라냅니다.
data = np.array([12, 5, 30, 8, 21])
mask = data > 10
print(mask)
# 출력: [ True False True False True]
print(data[data > 10]) # 마스크로 필터
# 출력: [12 30 21]
data[data > 10] = 0 # 조건부 대입도 가능
print(data)
# 출력: [ 0 5 0 8 0]
⚠️ 함정: 조건을 여러 개 결합할 때and/or가 아니라&/|를 쓰고, 각 조건을 괄호로 감싸야 합니다.(data > 10) & (data < 25)가 맞고,data > 10 and data < 25는 에러입니다. 연산자 우선순위 때문에 괄호를 빠뜨리면 조용히 틀립니다.
d = np.array([12, 5, 30, 8, 21])
print(d[(d > 10) & (d < 25)])
# 출력: [12 21]이 챕터의 핵심입니다. JS라면 for나 map으로 돌 계산을, NumPy는 배열 통째로 한 줄에 씁니다. C 레벨에서 실행되므로 빠르고, 무엇보다 코드가 수식 그대로입니다.
JavaScript
const celsius = [0, 25, 100];
const fahrenheit = celsius.map(c => c * 9 / 5 + 32);
// [32, 77, 212]Python (NumPy)
celsius = np.array([0, 25, 100])
fahrenheit = celsius * 9 / 5 + 32 # 루프 없음
print(fahrenheit)
# 출력: [ 32. 77. 212.]배열끼리의 연산도 원소별로 정렬되어 계산됩니다. 내장 함수(np.sqrt, np.exp, np.log 등)도 전부 벡터화되어 있습니다.
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
print(a + b)
# 출력: [11 22 33]
print(np.sqrt(np.array([1, 4, 9])))
# 출력: [1. 2. 3.]🎯 AICE: 파생 변수를 만들 때
df['bmi'] = df['weight'] / (df['height'] ** 2)처럼 열 전체를 한 번에 계산하는 것이 이 벡터화 사고입니다. pandas의 열(Series)이 내부적으로 NumPy 배열이라, 여기서 익힌 감각이 그대로 이어집니다.
shape가 다른 배열끼리 연산할 때, NumPy는 작은 쪽을 자동으로 "펼쳐" 맞춥니다. 이것이 브로드캐스팅입니다. 가장 흔한 경우는 배열과 스칼라(위에서 * 2가 이미 브로드캐스팅입니다)이고, 그다음이 행렬의 각 행/열에 벡터를 더하는 경우입니다.
mat = np.array([[1, 2, 3],
[4, 5, 6]])
col_mean = mat.mean(axis=0) # 열별 평균: [2.5 3.5 4.5]
centered = mat - col_mean # (2,3) - (3,) → 행마다 빼기
print(centered)
# 출력: [[-1.5 -1.5 -1.5]
# [ 1.5 1.5 1.5]]규칙은 "뒤 차원부터 맞춰보고, 크기가 같거나 한쪽이 1이면 늘린다"입니다. 표준화(각 열에서 평균을 빼고 표준편차로 나누기)가 정확히 이 패턴이며, 15장 스케일링의 밑그림입니다.
std = mat.std(axis=0)
standardized = (mat - col_mean) / std
print(standardized.mean(axis=0).round(1)) # 각 열 평균 0
# 출력: [0. 0. 0.]
⚠️ 함정: 브로드캐스팅은 shape가 규칙에 맞을 때만 동작합니다.(2,3)배열에 크기 2인 벡터를 더하면ValueError가 납니다. "한쪽이 1이거나 크기가 같아야 한다"를 기억하세요. 열 방향으로 맞추고 싶으면(2,1)모양으로reshape해야 합니다.
sum, mean, max 같은 집계 함수에 axis를 주면 방향이 정해집니다. **axis=0은 행을 따라 아래로(→ 열별 결과), axis=1은 열을 따라 옆으로(→ 행별 결과)**입니다. 이 방향 감각이 pandas groupby까지 이어집니다.
mat = np.array([[1, 2, 3],
[4, 5, 6]])
print(mat.sum()) # 축 없음: 전체 합
# 출력: 21
print(mat.sum(axis=0)) # 열별 합 (세로로 접기)
# 출력: [5 7 9]
print(mat.sum(axis=1)) # 행별 합 (가로로 접기)
# 출력: [ 6 15]🎯 AICE: "각 특성(열)의 평균"이 필요하면
axis=0, "각 샘플(행)의 합"이 필요하면axis=1입니다. 헷갈릴 때는 "없어지는 축이 axis"라고 외우세요.axis=0을 주면 행 방향이 접혀 사라지고 열별 결과가 남습니다.
Q1. 1부터 12까지의 정수를 3행 4열 배열로 만들려고 합니다. 빈칸을 채우세요.
import numpy as np
arr = np.arange(1, ____).____(3, 4)
print(arr.shape) # (3, 4)정답
import numpy as np
arr = np.arange(1, 13).reshape(3, 4)
print(arr.shape)
# 출력: (3, 4)arange(1, 13)은 1~12(끝값 제외)로 12개를 만들고, reshape(3, 4)로 3×4에 담습니다.
Q2. 배열 scores에서 60점 이상인 값만 골라내려 합니다. 빈칸을 채우세요.
scores = np.array([50, 72, 88, 41, 65])
passed = scores[____ >= 60]
print(passed) # [72 88 65]정답
scores = np.array([50, 72, 88, 41, 65])
passed = scores[scores >= 60]
print(passed)
# 출력: [72 88 65]불리언 마스크 scores >= 60이 True/False 배열을 만들고, 대괄호 안에 넣으면 True 위치만 남습니다.
Q3. 2차원 배열 mat의 열별 평균을 구하려 합니다. 빈칸을 채우세요.
mat = np.array([[1, 2], [3, 4], [5, 6]])
col_avg = mat.mean(axis=____)
print(col_avg) # [3. 4.]정답
mat = np.array([[1, 2], [3, 4], [5, 6]])
col_avg = mat.mean(axis=0)
print(col_avg)
# 출력: [3. 4.]열별 결과를 원하므로 행 방향(axis=0)을 접습니다. "없어지는 축이 axis"를 기억하세요.
| JS | Python (NumPy) | 비고 |
|---|---|---|
new Float64Array([...]) |
np.array([...]) |
벡터 연산 가능한 배열 |
arr.length |
arr.shape |
shape는 튜플(다차원) |
mat[i][j] |
mat[i, j] |
쉼표로 다차원 접근 |
arr.map(f) |
arr 통째 벡터 연산 |
루프·map 불필요 |
arr.filter(x => x > 10) |
arr[arr > 10] |
불리언 인덱싱 |
조건 결합 && / || |
& / | (괄호 필수) |
and/or 아님 |
arr.reduce((a,b)=>a+b) |
arr.sum() / arr.sum(axis=0) |
axis로 방향 지정 |