개요
AI 활용 도구에 필수로 쓰이는 데이터 가공 Python 라이브러리들(Numpy, Pandas)을 정리 해두려 한다. Pandas는 데이터를 읽고, 정리하고 탐색하는데 이용하고 NumPy는 정리된 데이터(숫자)를 계산하는데 이용한다.
Pandas = 표를 다루는 손 / NumPy = 숫자를 계산하는 머리
식당으로 비유하면 Pandas는 재료를 다듬고 분류하는 주방 보조, NumPy는 실제로 불을 다루는 주방장이다. 둘은 경쟁 관계가 아니라, Pandas가 내부에서 NumPy를 부려 쓰는 상하 관계다. 실제로 df[“price”].values를 찍어보면 정체가 numpy.ndarray로 나온다.
print(type(df["price"])) # <class 'pandas.core.series.Series'>
print(type(df["price"].values)) # <class 'numpy.ndarray'> ← 껍질만 벗기면 NumPy
NumPy
Python의 기본 List는 결국에 값의 주소를 참조하기에 성능적으로 느릴 수 밖에 없는 구조였다. 이러한 단점을 보완하기 위해 데이터를 값을 직접 관리하는 ndarray(배열)를 사용하기 위한 라이브러리이다.
쉽게 말하면 List는 사물함이다. 칸마다 “3번 창고에 있음” 같은 쪽지(주소)가 들어 있어서, 값을 꺼내려면 매번 창고까지 다녀와야 한다. 반면 ndarray는 긴 서랍장이라 값이 나란히 붙어 있다. 심부름을 안 가도 되니 훨씬 빠르다.
ndarray
위에 설명한 배열구조로 내부 연산은 C로 구현되어 있다. 내부에는 mean, sum, max와 같은 통계함수도 있으며 dtype과 shape정보까지 가진다.
( dtype = 데이터타입 int64, float64 등등 / shape = 배열의 차원별 크기를 튜플 형태로 반환하는 속성 )
- 배열 초기화 및 연산 방식
import numpy
# 리스트(파이썬)와 배열(Numpy)의 차이
py_list = [1, 2, 3]
print(py_list * 3) # [1, 2, 3, 1, 2, 3, 1, 2, 3] == 이어 붙이기
np_array = numpy.array([1, 2, 3])
print(np_array * 3) # [3 6 9] == 원소별 3배
print(np_array.dtype, np_array.shape) # int64 (3,)
# dtype == 데이터타입 / shape == 1차원이고 원소가 3개라는 뜻
# Numpy 배열 생성
print(numpy.arange(5)) # [0 1 2 3 4]
print(numpy.zeros(4)) # [0. 0. 0. 0.]
print(numpy.ones(3)) # [1. 1. 1.]
print(numpy.linspace(0, 3, 5)) # [0. 0.75 1.5 2.25 3.]
shape을 읽는 법만 알아두면 절반은 먹고 들어간다. 괄호 안 숫자 개수가 차원이고, 각 숫자가 그 방향의 길이다.
| shape | 뜻 | 부르는 이름 |
|---|---|---|
| (3,) | 숫자 3개짜리 줄 | 벡터(Vector) |
| (3, 2) | 3줄 2칸짜리 표 | 행렬(Matrix) |
| (2, 4, 3) | 표 2장이 겹쳐진 것 | 텐서(Tensor) |
- 통계, 조건 필터, 정렬
import numpy as np
price = np.array([8000, 4500, 52000, 500, 6600])
print(price.sum()) # 71600 총 가격
print(price.mean()) # 14320.0 평균 가격
print(price.max()) # 52000 최고 가격
print(price.std()) # 19009.2 표준편차
# 조건 필터: 범위 지정은 각 조건을 괄호로 감싸고 &(=and) |(=or)로 결합
print(price[price >= 20000]) # [52000]
print(price[(price >= 5000) & (price <= 10000)]) # [8000 6600]
# 정렬
print(np.sort(price)) # [ 500 4500 6600 8000 52000]
print(np.sort(price)[::-1]) # [52000 8000 6600 4500 500] 내림차순
print(np.argsort(price)) # [3 1 4 0 2] == 작은 값부터의(정렬될) 인덱스
여기서 and / or를 쓰면 바로 에러가 난다. 파이썬의 and는 “참이냐 거짓이냐” 딱 하나만 판단하는데, 배열은 값이 여러 개라서 하나로 답할 수가 없기 때문이다. 그래서 원소마다 따로 판단하는 &, |를 쓴다. 그리고 &가 >=보다 먼저 계산되기 때문에 괄호는 선택이 아니라 필수다.
- 2차원 배열
기본적으로 파이썬에서 사용했던 2차원 List 초기화 방법과 유사한 것과, reshape 메서드로 모양을 바꿀 수 있다.
( reshape에서 인자에 한쪽 차원에 -1를 넘겨줄 경우 남은 크기에 맞춰서 배열을 생성해준다. )
sheet = np.arange(6).reshape(2, 3) # [[0 1 2]
# [3 4 5]]
sheet.T # zip과 같은 효과 # [[0 3]
# [1 4]
# [2 5]]
sheet.sum(axis=0) # [3 5 7], 열별 합
sheet.sum(axis=1) # [3 12], 행별 합
axis는 처음 배울 때 가장 많이 헷갈리는 부분이라 그림으로 정리했다.
외우는 요령은 하나다. “axis에 적은 방향의 축이 사라진다.”
(2, 3) 배열에 axis=0을 주면 0번째 자리인 2가 사라져서 (3,)이 남고, axis=1을 주면 1번째 자리인 3이 사라져서 (2,)가 남는다.
- 브로드캐스팅
브로드캐스팅이란 특성으로 모양이 다른 배열끼리 연산할 땐 작은 쪽을 자동으로 늘려 맞춰준다.
단가 = np.array([4000, 5000, 6000]) # shape (3,)
수량 = np.array([[1, 2], [3, 1], [5, 2]]) # shape (3, 2)
# 그냥 곱하면 에러 → 세로로 세워줘야 한다
결과 = 단가[:, np.newaxis] * 수량 # 또는 단가.reshape(3, 1) * 수량
# [[ 4000 8000]
# [15000 5000]
# [30000 12000]]
규칙은 딱 두 가지다. 뒤에서부터 짝을 맞춰 봤을 때 ① 숫자가 같거나 ② 한쪽이 1이면 통과다. 둘 다 아니면 에러가 난다. 그리고 늘린 값을 메모리에 실제로 복사하진 않는다. 계산할 때만 그런 척하기 때문에 빠르고 가볍다.
- 정말 그렇게 빠른가?
말로만 “빠르다”고 하면 와닿지 않으니 직접 재봤다.
import time
start = time.perf_counter()
list_sum = sum(range(1_000_000))
print(f"Python List : {time.perf_counter() - start:.5f}초") # 0.00480초
arr = np.arange(1_000_000)
start = time.perf_counter()
arr_sum = arr.sum()
print(f"NumPy Array : {time.perf_counter() - start:.5f}초") # 0.00034초
같은 100만 개 덧셈인데 약 14배 차이가 났다. 데이터가 백만 줄, 천만 줄로 커질수록 이 차이는 눈덩이처럼 불어난다.
Pandas
표 형태의 데이터(DataFrame, Series)를 쉽게 다루고 조작할 수 있는 라이브러리로 내부적으로 NumPy 배열을 활용한다.
그럼 NumPy만으로는 왜 안 될까? 실제로 titanic.csv 같은 실무 데이터를 NumPy로 읽으면 이런 일이 생긴다.
- 글자와 숫자가 섞여 있으면 전부 문자열이 된다 → 나이 평균을 못 구한다
- 열 이름이 없다 → 요금을 보려면 data[:, 6]처럼 매번 몇 번째인지 세야 한다
- 빈 칸(결측치)을 표현할 방법이 없다
mixed = np.array([1, 2, "a", True])
print(mixed.dtype) # <U21 ← 전부 문자열로 통일됨 (U = 유니코드 문자열)
Pandas는 이 세 가지를 전부 해결한다. 열마다 다른 자료형을 쓰고, 이름표를 붙이고, 빈 칸은 NaN으로 표시한 뒤 계산할 때 알아서 빼준다.
import pandas as pd
df = pd.read_csv("titanic.csv")
print(df["age"].mean()) # 29.699... 결측치는 알아서 제외
print(df.groupby("sex")["survived"].mean())
# female 0.742038
# male 0.188908
한 줄로 “여성 생존률 74.2%, 남성 18.9%”가 나온다. 이걸 순수 파이썬으로 짜면 최소 열 줄은 걸린다.
DataFrame
행과 열로 구성된 2차원 표로 열마다 다른 자료형을 사용할 수 있다.
| 메서드 명 | 용도 | 반환 타입 |
|---|---|---|
| dataframe.head(n) | 위에서 부터 n행 미리보기 (기본 5행) | DataFrame |
| dataframe.tail(n) | 아래에서 부터 n행 미리보기 | DataFrame |
| dataframe.info() | 필드, 자료형, 결측 요약 | 출력만 (None) |
| dataframe.isna().sum() | 필드별 결측치 개수 | Series |
| dataframe.describe() | 필드 통계(count,mean,std,min,max) | DataFrame |
| dataframe.shape | 전체 크기 확인 | Tuple (row, column) |
| dataframe.dtypes | 각 필드별 자료형 | Series |
| dataframe[‘column’].value_counts() | 값별 빈도 | Series |
import numpy as np
import pandas as pd
n = 200
rng = np.random.default_rng(42) # ← 씨앗(seed) 고정: 몇 번을 돌려도 같은 값이 나온다
# 랜덤 값 200개 생성
local = rng.choice(["전주", "울산", "대전", "서울"], n)
menu = rng.choice(["아메리카노", "카페라떼", "바닐라라떼", "프라푸치노"], n)
price = rng.choice([4000, 4500, 5000, 5500, 6000], n)
count = rng.integers(1, 5, n)
df = pd.DataFrame({"local": local, "menu": menu, "price": price, "count": count})
rows, columns = df.shape
print("행:", rows, "| 열:", columns) # 행: 200 | 열: 4
df.info() # 컬럼, 자료형, 결측 요약
랜덤 데이터를 만들 땐 default_rng(42)처럼 씨앗을 고정하는 습관이 중요하다. 안 그러면 어제 블로그에 적어둔 결과와 오늘 돌린 결과가 달라서, 코드가 틀린 건지 랜덤이라 그런 건지 구분이 안 된다. (np.random.seed()는 옛날 방식이고, 요즘은 default_rng()를 권장한다.)
- 조건 필터
dataframe[조건]의 형태로 NumPy와 동일한 방식으로 사용한다.
# 조건 필터: 범위 지정은 각 조건을 괄호로 감싸고 &(=and) |(=or) 로 결합
df[(df["local"] == "서울") | (df["local"] == "전주")] # 괄호 필수
df[df["local"].isin(["서울", "전주"])] # 값이 많아지면 isin이 훨씬 깔끔
df.head() # 상위 5개 확인
| 조건이 3개 이상 늘어나면 | 를 계속 잇는 것보다 isin()이 읽기도 쉽고 실수도 줄어든다. |
- 추가 조회 메서드와 loc / iloc 인덱싱
loc는 Location의 약자로 이름표를 기준으로 행이나 열 데이터를 선택하는 걸 의미한다. iloc는 integer Location을 의미하며 기존 인덱스 방식(자리 번호)과 똑같다.
| 메서드 명 | 동작방식 |
|---|---|
| dataframe.nlargest(n, ‘column’) | 해당 컬럼 기준 상위 n행 |
| dataframe.nsmallest(n, ‘column’) | 해당 컬럼 기준 하위 n행 |
| dataframe.sample(n, random_state=42) | 무작위 n행 (seed 고정 가능) |
| dataframe.sort_values(‘column’, ascending=False) | 컬럼 기준 정렬 |
| dataframe.loc[dataframe[“column”] == n, “another column”] | 조건에 맞는 행의 다른 필드만 추출 |
| dataframe.iloc[0, 1] | [행, 열]의 위치 값 조회 |
여기서 반드시 짚고 갈 함정이 하나 있다. 끝 값을 포함하느냐가 서로 다르다.
df.loc['a':'c'] # a, b, c ← c 포함! (이름표는 끝까지)
df.iloc[0:2] # 0, 1 ← 2는 빠짐 (숫자는 파이썬 슬라이싱 규칙)
그리고 조건으로 고르는 건 loc만 가능하다. iloc에 조건식을 넣으면 에러가 난다.
- Group By 집계
이전에 배웠던 SQL 속 사용처와 동일하게 집계 함수(sum, mean, count)를 적용하여 필드별 결과 요약에 사용한다.
print(df.groupby("menu")["price"].mean().round(1))
# menu
# 바닐라라떼 4967.4
# 아메리카노 5196.1
# 카페라떼 4964.9
# 프라푸치노 4934.8
# Name: price, dtype: float64
SQL과 나란히 놓고 보면 구조가 똑같다.
| SQL | Pandas |
|---|---|
| WHERE price >= 5000 | df[df[“price”] >= 5000] |
| GROUP BY menu | df.groupby(“menu”) |
| SELECT AVG(price) | [“price”].mean() |
| ORDER BY price DESC | .sort_values(“price”, ascending=False) |
| LIMIT 3 | .head(3) 또는 .nlargest(3, “price”) |
주의할 점은 count()와 size()의 차이다. count()는 빈 칸을 빼고 세고, size()는 빈 칸까지 포함해서 센다. 결측치가 있는 데이터에서 이 둘을 섞어 쓰면 숫자가 미묘하게 안 맞는다.
- agg() : 컬럼마다 다른 집계를 한 번에
groupby를 여러 번 돌려서 나중에 합치는 것보다, agg()로 한 번에 뽑는 게 훨씬 빠르고 읽기 좋다. 결과 열 이름까지 직접 지정할 수 있는 게 핵심이다.
df.groupby("menu").agg(
판매건수=("count", "count"),
총잔수=("count", "sum"),
평균단가=("price", "mean"),
).round(1).reset_index()
# menu 판매건수 총잔수 평균단가
# 0 바닐라라떼 46 125 4967.4
# 1 아메리카노 51 119 5196.1
# 2 카페라떼 57 152 4964.9
# 3 프라푸치노 46 121 4934.8
reset_index()를 붙이면 그룹 이름이 인덱스에서 평범한 열로 내려온다. 나중에 다른 표와 합치거나 CSV로 저장할 때 이게 편하다.
- Pivot Table
엑셀에 있는 그 기능과 동일하게 여러 필드명에 따라 값을 집계하는 테이블을 만든다.
( agg() 함수와 마찬가지로 여러 요소들을 한번에 계산 가능하다 )
pivot = df.pivot_table(index="local", columns="menu", values="price", aggfunc="sum")
# menu 바닐라라떼 아메리카노 카페라떼 프라푸치노
# local
# 대전 56000 82000 50000 50500
# 서울 71500 45000 107000 37000
# 울산 58000 68000 85500 57500
# 전주 43000 70000 40500 82000
groupby([“local”, “menu”])의 결과를 unstack()한 것과 사실상 같다. 다만 pivot_table은 빈 조합을 fill_value로 채워주고, 여러 집계를 동시에 낼 수 있어서 손이 덜 간다.
df.pivot_table(index="local", values="price", aggfunc=["mean", "max", "count"])
df.pivot_table(index="local", columns="menu", values="price",
aggfunc="sum", fill_value=0) # 안 팔린 조합을 NaN 대신 0으로
실무에서 반드시 만나는 함정 5가지
여기서부터는 실제로 데이터를 만지다 보면 거의 100% 한 번씩 밟게 되는 지뢰들이다. 미리 알아두면 며칠을 아낀다.
1. 분명히 값을 바꿨는데 원본이 그대로다
가장 악명 높은 함정이다.
seoul = df[df["local"] == "서울"] # 이건 원본일까, 복사본일까?
seoul["price"] = 0 # SettingWithCopyWarning 발생
print(df.loc[df["local"] == "서울", "price"].iloc[0]) # 5500 ← 원본은 안 바뀜!
df[조건]이 원본을 가리키는 창문일지, 따로 떠낸 복사본일지 Pandas 자신도 그때그때 다르게 판단한다. 그래서 경고만 띄우고 조용히 넘어가는데, 결과적으로 원본은 그대로 남는다.
해결법은 두 가지, 둘 다 간단하다.
# ① 원본을 진짜 바꾸고 싶다 → loc로 한 번에 지목한다
df.loc[df["local"] == "서울", "price"] = 0
# ② 원본은 두고 따로 작업하고 싶다 → 복사본임을 명시한다
seoul = df[df["local"] == "서울"].copy()
seoul["price"] = 0
“대괄호를 두 번 연달아 쓰면 의심하라”가 좋은 습관이다. df[조건][“열”] = 값 형태는 거의 항상 잘못된 코드다.
2. 결측치(NaN)는 0이 아니다
빈 칸은 “0”이 아니라 “모른다”이다. 그래서 계산할 때 아예 빠진다.
s = pd.Series([10, np.nan, 30])
print(s.sum()) # 40.0
print(s.count()) # 2 ← NaN은 안 셈
print(len(s)) # 3 ← 줄 개수는 3
print(s.mean()) # 20.0 (= 40 / 2) 내가 원한 게 13.3이었다면?
mean()은 40을 2로 나눈다. 만약 “빈 칸은 0으로 치고 3으로 나눠야 한다”면 결과가 20이 아니라 13.3이어야 하는데, 이 차이를 모르고 지나가면 보고서 숫자가 통째로 틀린다.
그리고 NaN은 자기 자신과도 같지 않다.
np.nan == np.nan # False !!!
df[df["age"] == np.nan] # 절대 안 됨. 항상 빈 결과
df[df["age"].isna()] # 이렇게 써야 한다
df["age"].isna().sum() # 열별 결측치 개수 세기
df["age"].fillna(df["age"].median()) # 중앙값으로 채우기
작업 시작 전에 df.isna().sum() 한 번 찍어보기를 습관으로 만들면 나중에 원인 모를 숫자 오류로 헤맬 일이 크게 줄어든다.
3. 메모리 다이어트 — dtype만 바꿔도 30배
데이터가 커지면 “느리다”보다 “메모리가 터진다”가 먼저 온다. 그런데 자료형만 바꿔줘도 극적으로 줄어든다.
big = pd.DataFrame({
"local": rng.choice(["전주", "울산", "대전", "서울"], 1_000_000),
"price": rng.choice([4000, 4500, 5000], 1_000_000),
})
print(big.memory_usage(deep=True).sum() / 1024**2) # 89.6 MB
big["local"] = big["local"].astype("category") # 반복되는 글자 → 번호표로 저장
big["price"] = big["price"].astype("int16") # 6000까지면 int64는 과분하다
print(big.memory_usage(deep=True).sum() / 1024**2) # 2.9 MB
89.6MB → 2.9MB, 약 31배가 줄었다. 원리는 단순하다. category는 “전주”라는 글자를 백만 번 저장하는 대신, 사전 하나(전주=0, 울산=1…)를 만들고 번호만 저장한다. 도서관에서 책 제목을 다 적는 대신 청구기호만 적는 것과 같다.
단, int16은 -32,768 ~ 32,767까지만 담긴다. 넘칠 여지가 있는 값에는 쓰면 안 된다. (바로 다음 항목 참고)
4. 반복문 대신 벡터 연산
파이썬 습관대로 for문을 돌리면 NumPy를 쓰는 의미가 없어진다.
# 나쁜 예 : 한 줄씩 꺼내서 곱하기
total = [df["price"].iloc[i] * df["count"].iloc[i] for i in range(len(df))] # 1.37ms
# 좋은 예 : 열끼리 통째로 곱하기
total = df["price"] * df["count"] # 0.09ms
겨우 200줄인데도 15배 차이가 났다. 100만 줄이면 몇 초 대 몇 분의 싸움이 된다.
df.apply(함수, axis=1)도 겉보기엔 세련돼 보이지만 속은 for문이라 느리다. “열 단위로 한 번에 처리할 방법이 없나?”를 먼저 떠올리는 것이 NumPy·Pandas식 사고방식이다.
조건 분기가 필요하면 np.where를 쓰면 된다.
df["등급"] = np.where(df["price"] >= 5000, "고가", "저가") # for문 없이 한 줄
5. 숫자가 조용히 망가지는 순간
NumPy는 속도를 위해 넘치는 값을 검사하지 않는다. 그래서 조용히 틀린 답을 내놓는다.
small = np.array([2_000_000_000], dtype=np.int32)
print(small + small) # [-294967296] ← 40억이 나와야 하는데 음수!
print(small.astype(np.int64) + small.astype(np.int64)) # [4000000000] 정상
파이썬의 기본 정수는 무한정 커지지만, NumPy의 int32는 21억 근처가 한계다. 자동차 계기판이 999,999km를 넘으면 000,000으로 돌아가는 것과 같다. 돈·조회수처럼 커질 수 있는 값은 int64로 두는 게 안전하다.
실수(소수점)도 마찬가지다.
print(0.1 + 0.2 == 0.3) # False !!!
print(np.isclose(0.1 + 0.2, 0.3)) # True ← 실수 비교는 항상 isclose
컴퓨터는 0.1을 2진수로 딱 떨어지게 표현하지 못한다. 우리가 1/3을 0.3333…으로 밖에 못 쓰는 것과 같은 이치다. 그래서 실수끼리 ==로 비교하면 안 되고, np.isclose()나 반올림 후 비교를 써야 한다.
보강 — 나중에 다시 만난 pandas 문법 네 가지
AI 실습 시리즈를 정리하다가 이 글에서 다뤘어야 했다 싶은 것들이 나와서 여기에 덧붙인다. 전부 실제로 한 번씩 틀려본 것들이다.
① 행과 열을 동시에 고르려면 .loc
# ❌ 에러가 난다
df[df["가격"] >= 1000, "판매수량"]
# ✅ 이렇게 해야 한다
df.loc[df["가격"] >= 1000, "판매수량"]
df[...]라는 대괄호는 부탁을 한 가지만 들어준다.
| 하고 싶은 것 | 문법 |
|---|---|
| 조건으로 행만 고르기 | df[조건] |
| 이름으로 열만 고르기 | df["열이름"] |
| 행 조건 + 열 이름 동시에 | df.loc[조건, "열이름"] |
.loc는 콤마로 두 개를 받도록 만들어진 전용 도구다. 조건과 열을 같이 쓰고 싶을 때는 무조건 .loc라고 외워두면 편하다.
② max()와 idxmax()는 다른 질문에 답한다
df["판매수량"].max() # 340 ← 가장 많이 팔린 "수량"
df["판매수량"].idxmax() # 17 ← 가장 많이 팔린 상품의 "행 번호"
반 친구들 점수로 비유하면 이렇다.
- “가장 높은 점수는 몇 점?” → 95점 ←
max()- “그 점수를 받은 사람은 누구?” → 영희 ←
idxmax()
idx가 붙으면 값이 아니라 이름표를 준다고 기억하면 안 헷갈린다.
실전에서는 둘을 조합해서 쓴다.
# 가장 많이 팔린 상품의 "이름"을 알고 싶다
가장_잘팔린_상품 = df.loc[df["판매수량"].idxmax(), "상품명"]
idxmax()로 위치를 찾고, 그 위치의 다른 열 값을 .loc로 꺼내는 패턴이다.
③ 상위 N%만 뽑기 — quantile()
기준값 = df["판매수량"].quantile(0.9) # 상위 10% 경계선
상위그룹 = df[df["판매수량"] >= 기준값]
quantile(0.9)는 “아래에서 90% 지점의 값”이다. 이것보다 크면 상위 10%가 된다.
이 함수는 나중에 이상치 판별(IQR)에서 그대로 다시 쓰인다. quantile(0.25)와 quantile(0.75)로 가운데 절반의 범위를 구하는 방식이다.
④ .index는 리스트가 아니다
이건 진짜 한참 헤맸다.
상위5개 = df["판매수량"].nlargest(5).index
# ⚠️ 위험한 코드
합친목록 = 상위5개 + ["판매수량"]
.index로 나온 결과는 파이썬 리스트가 아니라 pandas.Index라는 별도 타입이다. 겉보기엔 리스트 같은데 +의 동작이 다르다.
-
파이썬 리스트의
+= 이어 붙이기 →[a,b] + [c]→[a,b,c] -
pandas·NumPy의
+= 각 원소끼리 계산하기 → 길이가 안 맞으면 에러거나 엉뚱한 결과
# ✅ 안전한 코드
합친목록 = list(상위5개) + ["판매수량"]
합친목록 = 상위5개.tolist() + ["판매수량"] # 이것도 같다
이 글 앞쪽에서 “NumPy의
+는 원소별 계산”이라고 정리했는데, 그 성질이 여기서 발목을 잡은 것이다..index나.columns같은 pandas 결과물을 다룰 때는list()로 한 번 감싸는 습관을 들이는 게 안전하다.
AI에서 이 둘은 어디에 쓰이나
“데이터 가공 라이브러리인데 왜 AI 필수라고 할까?” 답은 모델은 숫자 배열만 먹는다는 데 있다.
CSV / DB / 로그 → [Pandas] → [NumPy] → [모델 학습]
표 정리 숫자 계산
결측치·이상치 행렬 곱셈
타입 변환 정규화
- Pandas 구간 : 빠진 값 채우고, 글자를 숫자로 바꾸고, 필요 없는 열을 버린다. 실무에서 AI 프로젝트 시간의 7~8할이 여기서 소모된다. 모델 코드는 정작 열 줄이면 끝나는 경우가 많다.
- NumPy 구간 : 정리된 표를 df.values로 배열로 바꿔 모델에 넘긴다. 이미지 한 장은 (높이, 너비, 3) 짜리 ndarray고, 딥러닝의 학습이란 결국 거대한 행렬 곱셈의 반복이다.
- 브로드캐스팅은 여기서 다시 등장한다. 데이터 1000장에 가중치 하나를 곱할 때, 가중치를 1000번 복사하지 않고 자동으로 맞춰주는 게 바로 그 기능이다.
- PyTorch의 Tensor, TensorFlow의 Tensor 모두 ndarray를 본떠 만든 것이라 사용법이 거의 같다. 여기서 익힌 shape, axis, 브로드캐스팅 감각이 그대로 딥러닝으로 이어진다.
정리하면, NumPy와 Pandas는 AI의 곁다리가 아니라 입구다.
다음에 볼 것
- matplotlib / seaborn : 숫자를 그림으로 바꿔야 비로소 눈에 보인다
한줄 평
- 데이터 전처리, 가공 과정이 SQL이 많이 생각나는 공부였다.