NumPy가 수치 배열 계산에 강하다면 Pandas는 행과 열이 있는 표 형태 데이터를 다루는 데 강하다.
CSV, Excel, 데이터베이스 조회 결과, API 응답처럼 실제 분석에서 만나는 데이터는 대부분 표 형태로 정리할 수 있다.
Pandas의 핵심 객체는 다음 두 가지다.
Series: 1차원 데이터DataFrame: 2차원 표 데이터이 글에서는 DataFrame 생성부터 인덱싱, 파일 입출력, 컬럼 변환, 결측치 처리, 정렬까지 현재 Pandas 3.x 기준으로 정리한다.
import pandas as pd
관례적으로 pd라는 별칭을 사용한다.
Series는 인덱스를 가진 1차원 데이터다.
scores = pd.Series(
[90, 85, 78],
index=[
"user_a",
"user_b",
"user_c",
],
)
print(scores)
인덱스로 접근:
print(
scores.loc[
"user_a"
]
)
위치로 접근:
print(
scores.iloc[0]
)
여러 Series가 열 방향으로 결합된 표라고 이해할 수 있다.
df = pd.DataFrame(
{
"user_id": [
101,
102,
103,
],
"age": [
24,
31,
28,
],
"active": [
True,
False,
True,
],
}
)
print(df)
처음 데이터를 받으면 구조부터 확인한다.
df.head()
df.tail()
df.shape
df.columns
df.index
df.dtypes
전체 요약:
df.info()
수치형 기술 통계:
df.describe()
최근 Pandas 3.x에서는 문자열 데이터가 예전처럼 항상 object로만 추론되지 않고 전용 문자열 dtype을 사용할 수 있다.
따라서 문자열 컬럼인지 확인할 때 단순히 다음 조건에만 의존하는 코드는 주의해야 한다.
df["name"].dtype == "object"
데이터의 의미와 Pandas의 dtype 시스템을 함께 확인하는 습관이 좋다.
하나의 컬럼:
ages = df["age"]
결과는 Series다.
여러 컬럼:
subset = df[
[
"user_id",
"age",
]
]
결과는 DataFrame이다.
locloc는 라벨 기반 선택이다.
df.loc[0]
행과 열을 함께 지정:
df.loc[
0,
"age",
]
여러 행과 컬럼:
df.loc[
0:1,
[
"user_id",
"age",
],
]
ilociloc는 정수 위치 기반 선택이다.
df.iloc[0]
첫 번째 행, 두 번째 열:
df.iloc[
0,
1,
]
앞 두 행과 앞 두 열:
df.iloc[
:2,
:2,
]
loc와 iloc 차이| 기능 | 기준 |
|---|---|
loc | 인덱스/컬럼 라벨 |
iloc | 정수 위치 |
데이터를 다룰 때 두 방식을 명확하게 구분하는 것이 중요하다.
df[
df["age"] >= 28
]
여러 조건:
df[
(df["age"] >= 28)
& (df["active"])
]
Pandas에서는 and, or 대신 배열 단위 논리 연산자인 &, |를 사용한다.
괄호도 함께 작성하는 것이 중요하다.
.loc로 조건 필터링필터링과 컬럼 선택을 동시에 할 수 있다.
df.loc[
df["active"],
[
"user_id",
"age",
],
]
.locdf.loc[
df["age"] >= 30,
"segment",
] = "30+"
최근 Pandas 3.x에서는 chained assignment가 동작하지 않는 방향으로 정리되었기 때문에, 다음과 같은 코드는 피해야 한다.
# 피해야 하는 방식
df["segment"][
df["age"] >= 30
] = "30+"
수정 대상 DataFrame에 한 번에 접근하는 .loc를 사용하는 것이 안전하다.
df.loc[
df["age"] >= 30,
"segment",
] = "30+"
df["age_next_year"] = (
df["age"] + 1
)
조건 기반 컬럼:
df["age_group"] = (
df["age"]
.ge(30)
.map(
{
True: "30+",
False: "under-30",
}
)
)
NumPy를 함께 사용할 수도 있다.
import numpy as np
df["age_group"] = np.where(
df["age"] >= 30,
"30+",
"under-30",
)
assign()기존 DataFrame을 직접 수정하지 않고 새 DataFrame을 만들고 싶을 때 사용할 수 있다.
result = df.assign(
age_next_year=(
df["age"] + 1
)
)
메서드 체이닝에서 유용하다.
df = df.drop(
columns=[
"age_next_year",
]
)
행 삭제:
df = df.drop(
index=[0]
)
df = df.rename(
columns={
"user_id": "id",
"active": "is_active",
}
)
전체 컬럼명을 직접 지정할 수도 있다.
df.columns = [
"id",
"age",
"is_active",
]
특정 컬럼을 인덱스로 사용:
df = df.set_index(
"user_id"
)
일반 컬럼으로 되돌리기:
df = df.reset_index()
기존 인덱스를 버리려면:
df = df.reset_index(
drop=True
)
값 기준:
df = df.sort_values(
by="age"
)
내림차순:
df = df.sort_values(
by="age",
ascending=False,
)
여러 컬럼:
df = df.sort_values(
by=[
"active",
"age",
],
ascending=[
False,
False,
],
)
인덱스 기준:
df = df.sort_index()
df.duplicated()
중복 행 제거:
df = df.drop_duplicates()
특정 컬럼 기준:
df = df.drop_duplicates(
subset=[
"user_id",
],
keep="last",
)
Pandas 데이터에는 NaN, NA, NaT 같은 결측값이 나타날 수 있다.
결측 여부:
df.isna()
컬럼별 개수:
df.isna().sum()
df = df.dropna()
특정 컬럼이 비어 있을 때만 제거:
df = df.dropna(
subset=[
"age",
]
)
df["age"] = (
df["age"]
.fillna(
df["age"].median()
)
)
범주형:
df["region"] = (
df["region"]
.fillna("unknown")
)
무조건 0으로 채우기보다 그 컬럼에서 결측이 무엇을 의미하는지 먼저 생각해야 한다.
df["status"] = (
df["status"]
.replace(
{
"Y": "active",
"N": "inactive",
}
)
)
map()Series의 값을 변환한다.
label_map = {
1: "paid",
0: "unpaid",
}
df["payment_label"] = (
df["payment"]
.map(label_map)
)
Series:
def normalize_name(
value,
):
return (
value
.strip()
.lower()
)
df["name"] = (
df["name"]
.map(normalize_name)
)
DataFrame 단위 처리는 필요한 경우 메서드의 최신 동작을 확인하면서 사용하는 것이 좋다.
벡터화 연산으로 해결할 수 있다면 반복적인 Python 함수 적용보다 우선 고려할 수 있다.
예:
df["name"] = (
df["name"]
.str.strip()
.str.lower()
)
.strdf["email"].str.lower()
공백 제거:
df["name"].str.strip()
포함 여부:
df[
df["email"]
.str.contains(
"@example.com",
na=False,
)
]
import pandas as pd
df = pd.read_csv(
"orders.csv"
)
자주 사용하는 옵션:
df = pd.read_csv(
"orders.csv",
usecols=[
"order_id",
"amount",
"status",
],
)
날짜 컬럼은 읽은 후 명시적으로 변환하는 방법도 이해하기 쉽다.
df["ordered_at"] = (
pd.to_datetime(
df["ordered_at"]
)
)
df.to_csv(
"orders-clean.csv",
index=False,
)
index=False를 빼면 DataFrame 인덱스도 파일에 저장된다.
df = pd.read_excel(
"sales.xlsx",
sheet_name="Sales",
)
모든 시트를 읽으면 딕셔너리 형태로 받을 수 있다.
sheets = pd.read_excel(
"sales.xlsx",
sheet_name=None,
)
.xlsx 처리에는 환경에 따라 openpyxl 같은 Excel 엔진 패키지가 필요할 수 있다.
df.to_excel(
"result.xlsx",
index=False,
)
여러 시트:
with pd.ExcelWriter(
"report.xlsx"
) as writer:
sales_df.to_excel(
writer,
sheet_name="Sales",
index=False,
)
users_df.to_excel(
writer,
sheet_name="Users",
index=False,
)
df = pd.read_json(
"users.json"
)
중첩된 API JSON을 표로 펼칠 때:
flat = pd.json_normalize(
data
)
concat()행 방향:
combined = pd.concat(
[
january,
february,
],
ignore_index=True,
)
열 방향:
combined = pd.concat(
[
user_info,
user_score,
],
axis=1,
)
merge()사용자 정보:
users = pd.DataFrame(
{
"user_id": [
1,
2,
],
"name": [
"kim",
"lee",
],
}
)
주문 정보:
orders = pd.DataFrame(
{
"user_id": [
1,
1,
2,
],
"amount": [
12000,
35000,
8000,
],
}
)
결합:
result = users.merge(
orders,
on="user_id",
how="left",
)
주요 방식:
innerleftrightouterSQL JOIN과 비슷하게 이해할 수 있다.
Pandas 3.x에서는 Copy-on-Write가 기본 동작이 되어, 다른 DataFrame에서 만든 부분 집합을 수정했을 때 원본까지 우연히 바뀌는 오래된 모호한 동작을 줄였다.
핵심 규칙은 단순하다.
원본을 수정하고 싶다면 원본 DataFrame 자체를 한 번의 인덱싱으로 수정한다.
df.loc[
df["amount"] < 0,
"amount",
] = 0
chained assignment에 의존하지 않는 습관이 중요하다.
import pandas as pd
orders = pd.DataFrame(
{
"order_id": [
101,
102,
103,
103,
],
"amount": [
12000,
None,
35000,
35000,
],
"status": [
"PAID",
"CANCELLED",
"PAID",
"PAID",
],
}
)
중복 제거:
orders = (
orders
.drop_duplicates(
subset="order_id"
)
)
상태 문자열 정리:
orders["status"] = (
orders["status"]
.str.lower()
)
결측값:
orders["amount"] = (
orders["amount"]
.fillna(0)
)
결제 완료만 선택:
paid_orders = (
orders.loc[
orders["status"]
== "paid"
]
)
이런 흐름이 실제 데이터 전처리의 기본 형태다.
Series와 2차원 DataFrame이다.head(), shape, dtypes, info(), describe()로 구조부터 확인하는 것이 좋다.loc는 라벨 기반, iloc는 위치 기반 인덱싱이다.&, |로 결합한다..loc로 한 번에 대입하는 패턴을 사용해야 한다.read_*, 저장은 to_* 계열 메서드로 처리한다.concat()은 축 방향 결합, merge()는 키 기반 관계형 결합에 적합하다.