PUBLISHED

Pandas

작성일: 2025.02.01

Pandas

pandas

pandas는 표 형식 데이터를 다루기 위한 파이썬의 대표 도구다. 핵심은 “데이터를 표로 생각한다”는 관점이고, 이를 Series(열)와 DataFrame(표)로 모델링한다. list와 dict로도 데이터를 다룰 수 있지만, 컬럼 단위 연산과 결측치 처리, 그룹화 같은 작업이 필요해지면 pandas가 훨씬 명확하고 안전하다. 결국 pandas는 계산 도구라기보다 “데이터 작업의 규약”을 제공하는 라이브러리다.

pandas는 느린 파이썬 루프 대신 벡터화 연산을 전제로 설계된다. 그래서 “한 행씩 처리”하려는 습관을 버리고 “열 전체를 계산”하는 방식으로 접근해야 한다. 이 사고 전환이 되면 코드가 짧아지고 성능도 좋아진다. 반대로 루프와 apply를 남발하면 pandas의 장점이 사라진다.

untitled
PY
import pandas as pd

data = {
    "name": ["Ayden", "Jin", "Mina"],
    "age": [28, 30, 26],
    "score": [88, 92, 75]
}

df = pd.DataFrame(data)
print(df)

처음에는 DataFrame만 익히면 된다고 생각하기 쉽지만, 실제로는 Series를 제대로 이해해야 DataFrame을 잘 다룰 수 있다. Series가 곧 “한 열의 규약”이고, DataFrame은 여러 Series의 정렬된 집합이기 때문이다.

 

Series

Series는 “인덱스를 가진 1차원 배열”이다. list와 달리 값마다 인덱스 라벨이 붙어 있고, 이 라벨을 기준으로 정렬·연산이 이루어진다. 그래서 같은 길이의 Series끼리 연산을 하면 단순한 위치가 아니라 인덱스 기준으로 맞춰진다. 이 점이 데이터 정합성을 보장해주지만, 동시에 의도하지 않은 정렬 문제가 생길 수도 있다.

Series의 인덱스는 단순한 숫자가 아니라 의미 있는 라벨이 될 수 있다. 즉, 값과 인덱스가 함께 도메인 의미를 갖게 된다. 이 특성을 이해하면 단순 배열보다 훨씬 안전한 데이터 모델을 만들 수 있다.

untitled
CSS
s = pd.Series([10, 20, 30], index=["a", "b", "c"])
print(s["b"])
print(s + 5)

s2 = pd.Series([1, 2, 3], index=["b", "c", "d"])
print(s + s2)  # 인덱스 기준 정렬 연산

Series 연산에서 NaN이 생기는 이유는 “인덱스가 맞지 않기 때문”인 경우가 많다. 그래서 인덱스를 맞추는 방식이 실무에서 중요한 설계 포인트가 된다.

 

DataFrame

DataFrame은 여러 개의 Series가 같은 인덱스를 공유하는 구조다. 쉽게 말해 “열이 정렬된 표”인데, 각 열은 독립적인 타입과 의미를 가질 수 있다. 그래서 pandas는 열 단위 연산을 중심으로 설계되며, 특정 컬럼만 뽑거나 새로운 컬럼을 만들어 계산하는 작업이 자연스럽다.

DataFrame의 인덱스는 행을 식별하는 키다. 기본값은 0부터 시작하는 정수지만, 실제 업무에서는 날짜나 고유 ID를 인덱스로 쓰는 경우가 많다. 이 결정이 이후 조인과 그룹화의 기준이 되기 때문에, 인덱스를 어떤 의미로 쓸지 먼저 정하는 편이 안전하다.

untitled
SH
df = pd.DataFrame({
    "name": ["Ayden", "Jin", "Mina"],
    "age": [28, 30, 26],
    "score": [88, 92, 75]
})

df["passed"] = df["score"] >= 80
print(df)

DataFrame은 컬럼마다 타입이 다를 수 있어서 유연하지만, 이 때문에 타입 혼합이 늘어나면 성능이 떨어질 수 있다. 그래서 데이터가 커질수록 타입을 명확히 유지하는 습관이 중요해진다.

 

읽기/쓰기

pandas는 CSV, Excel, JSON, Parquet 등 다양한 포맷을 지원한다. 실무에서는 데이터를 읽는 순간이 가장 위험하다. 인코딩, 결측치, 타입 추론이 자동으로 일어나기 때문이다. 그래서 read_csv에서 dtype을 명시하거나, parse_dates 같은 옵션을 적극적으로 쓰는 편이 안전하다.

쓰기에서도 마찬가지다. 결과물을 어디에 전달할지에 따라 포맷이 달라지고, 그에 맞는 옵션이 필요하다. 즉, 입출력은 단순한 IO가 아니라 데이터 계약을 정의하는 과정이다.

untitled
PY
df = pd.read_csv("data.csv", dtype={"age": "int64"}, parse_dates=["created_at"])
df.to_csv("out.csv", index=False)

df.to_parquet("out.parquet")
df.to_json("out.json", orient="records")

입출력에서 자동 추론을 믿을지, 명시적으로 통제할지에 따라 데이터 신뢰도가 달라진다. 작은 프로젝트에서는 자동 추론이 편하지만, 규모가 커질수록 명시가 안전하다.

 

선택, 필터링, 정렬

pandas의 가장 중요한 습관은 “필터링을 벡터화로 처리하기”다. 조건식을 그대로 Series로 만들고, 그 결과를 마스크로 사용하면 된다. 이 방식은 읽기 쉽고 빠르며, 명확한 의도를 담을 수 있다. 반대로 for 루프를 돌며 조건을 검사하면 pandas의 장점을 잃는다.

선택은 loc(라벨 기반)과 iloc(위치 기반)의 차이를 이해해야 한다. loc는 인덱스 의미를 존중하는 방식이고, iloc는 순수한 위치다. 이 차이를 혼동하면 잘못된 행이 선택되는 버그가 생긴다.

untitled
SH
filtered = df[df["score"] >= 80]
sorted_df = df.sort_values(by="age", ascending=False)

df.loc[0, "name"]   # 라벨 기반
df.iloc[0, 0]        # 위치 기반

정렬은 단순히 보기 좋게 만드는 작업이 아니라, 이후 연산 결과를 안정화하는 중요한 단계다. 특히 시간 데이터는 정렬 여부에 따라 결과가 달라질 수 있다.

 

결측치와 타입 관리

pandas에서 가장 현실적인 문제는 결측치다. NaN은 숫자 타입에 자연스럽게 들어가지만, 문자열 컬럼에서는 dtype을 object로 바꿔버린다. 이 변화가 성능과 연산 결과에 영향을 준다. 그래서 결측치를 먼저 어떻게 처리할지 결정해야 한다.

결측치는 삭제, 대체, 유지라는 세 가지 선택지가 있다. 어느 것을 택하느냐는 도메인 정책이다. 데이터를 잃어도 되는지, 평균으로 대체해도 되는지, 혹은 결측 자체가 의미를 갖는지를 먼저 판단해야 한다.

untitled
SH
df.isna().sum()

df = df.dropna()  # 결측치 제거
df = df.fillna(0) # 결측치 대체

df["age"] = df["age"].astype("int64")

타입 변환은 메모리와 성능에도 영향을 준다. 특히 문자열을 object로 두면 연산이 느려지므로, 범주형 데이터는 category 타입을 고려하는 것이 좋다.

 

그룹화와 집계

groupby는 pandas의 진짜 힘이다. 데이터를 특정 기준으로 묶고, 그 결과를 요약하는 작업이기 때문이다. 이때 중요한 건 “무엇을 그룹의 키로 삼을 것인가”다. 키의 의미가 데이터의 구조를 결정한다.

집계 함수는 평균, 합계 같은 기본 값뿐 아니라, 사용자 정의 함수도 가능하다. 하지만 복잡한 함수는 성능을 떨어뜨릴 수 있으므로, 가능한 한 내장 집계 함수를 활용하는 편이 안전하다.

untitled
PY
result = df.groupby("name")["score"].mean()
summary = df.groupby("name").agg({"score": ["mean", "max"], "age": "min"})
print(result)

groupby 결과는 MultiIndex로 나오는 경우가 많다. 이 구조가 복잡해지면 출력과 후속 연산이 어려워지기 때문에, reset_index로 평탄화하는 작업을 자주 하게 된다.

 

merge, concat, join

데이터를 합치는 작업은 pandas에서 가장 위험한 작업 중 하나다. merge는 SQL join과 유사하고, concat은 단순한 행/열 결합이다. 어떤 키로 결합할지, 누락된 행은 어떻게 처리할지에 따라 결과가 완전히 달라진다. 이 선택은 비즈니스 규칙에 가깝다.

merge는 키가 중복될 때 폭발적인 행 증가가 생길 수 있다. 그래서 실제로는 merge 전에 키의 유일성을 확인하거나, 중복을 제어하는 절차가 필요하다. concat도 axis를 잘못 선택하면 컬럼 정합성이 깨진다.

untitled
CSS
left = pd.DataFrame({"id": [1, 2], "name": ["A", "B"]})
right = pd.DataFrame({"id": [1, 3], "score": [90, 70]})

merged = pd.merge(left, right, on="id", how="left")
concat = pd.concat([left, right], axis=1)

결합은 단순한 기술이 아니라 데이터의 정합성을 정의하는 작업이다. 그래서 merge는 항상 결과의 행 수와 누락 여부를 점검하는 습관이 필요하다.

 

성능과 메모리 팁

pandas는 편리하지만 메모리를 많이 쓴다. 따라서 데이터가 커질수록 타입 최적화와 연산 방식이 중요해진다. 필요 없는 컬럼은 읽을 때부터 제외하고, category나 bool 같은 경량 타입을 활용하는 편이 좋다. 또한 apply는 마지막 수단으로 두고, 가능하면 벡터화된 연산으로 처리하는 게 기본이다.

정말 큰 데이터라면 chunk 단위로 읽는 것도 고려해야 한다. 한 번에 다 읽지 않고 스트리밍 처리하면 메모리를 크게 줄일 수 있다. 이런 선택은 성능보다도 안정성을 높인다.

untitled
SH
df = pd.read_csv("data.csv", usecols=["name", "age", "score"])
df["name"] = df["name"].astype("category")

for chunk in pd.read_csv("data.csv", chunksize=10000):
    pass

성능 최적화는 “빠르게 만들기”보다 “안정적으로 돌리기”에 가깝다. pandas는 메모리 한계에 부딪히는 순간 갑자기 느려지거나 죽는다.

 

실무에서 흔한 함정

pandas에서 자주 나오는 문제는 SettingWithCopyWarning이다. 이는 체이닝된 인덱싱이 원본을 수정하는지 복사본을 수정하는지 불명확할 때 발생한다. 해결책은 간단하다. 중간 결과를 명시적으로 복사하거나, loc를 사용해 “이건 원본 수정이다”라고 명시하는 것이다.

또 하나는 인덱스 혼동이다. merge나 groupby 이후 인덱스가 바뀌었는데도, 이전 인덱스로 접근하는 실수가 자주 나온다. 그래서 복잡한 연산 후에는 항상 인덱스를 다시 정리하는 습관이 필요하다.

untitled
SH
filtered = df[df["score"] > 80].copy()
filtered.loc[:, "grade"] = "A"

df = df.reset_index(drop=True)

pandas는 실수를 쉽게 만들고, 그 실수가 조용히 지나가기도 한다. 그래서 경고 메시지를 무시하지 말고, 결과의 행 수와 샘플 값을 확인하는 습관이 중요하다.