pandas 치트시트
마지막 업데이트
DataFrame 만들기
예제, 테스트, 조회용 테이블에 쓰는 작은 DataFrame을 직접 만들어요.
| 작업 | 코드 |
|---|---|
| pandas와 NumPy 임포트 | import pandas as pd
import numpy as np |
| 열 딕셔너리로 만들기 | df = pd.DataFrame({"name": ["Ada", "Ben"], "age": [36, 28]}) |
| 행 리스트로 만들기 | pd.DataFrame([{"name": "Ada", "age": 36}, {"name": "Ben", "age": 28}]) |
| NumPy 배열로 만들기 | pd.DataFrame(np.arange(6).reshape(3, 2), columns=["x", "y"]) |
| 레이블이 있는 Series | s = pd.Series([3, 5, 2], index=["a", "b", "c"]) |
| 날짜 범위 | pd.date_range("2026-01-01", periods=7, freq="D") |
파일 읽기와 쓰기
한 번의 호출로 파일을 DataFrame으로 불러오고, 한 번의 호출로 다시 저장해요.
| 작업 | 코드 |
|---|---|
| CSV 파일 읽기 | df = pd.read_csv("sales.csv") |
| 일부 열만, 날짜는 날짜 타입으로 | pd.read_csv("sales.csv", usecols=["date", "city", "sales"], parse_dates=["date"]) |
| 열을 인덱스로 사용 | pd.read_csv("sales.csv", index_col="id") |
| 탭 등 다른 구분자 | pd.read_csv("sales.tsv", sep="\t") |
| 추가 표기를 결측값으로 처리 | pd.read_csv("sales.csv", na_values=["n/a", "-"]) |
| 앞부분 행만 읽기 | pd.read_csv("sales.csv", nrows=1000) |
| Excel 시트 읽기 (openpyxl 필요) | pd.read_excel("report.xlsx", sheet_name="2026") |
| JSON 레코드 읽기 | pd.read_json("records.json") |
| SQL 데이터베이스에서 읽기 | pd.read_sql("SELECT * FROM orders", conn) |
| 인덱스 없이 CSV 저장 | df.to_csv("clean.csv", index=False) |
| Excel 또는 Parquet 저장 | df.to_excel("clean.xlsx", index=False), df.to_parquet("clean.parquet") |
데이터 살펴보기
새 데이터셋을 받으면 가장 먼저 실행할 호출들이에요.
| 작업 | 코드 |
|---|---|
| 처음과 마지막 행 | df.head(), df.tail(3) |
| 행과 열의 개수 | df.shape # (6, 7) |
| 열 타입과 non-null 개수 | df.info() |
| 숫자 열의 요약 통계 | df.describe() |
| 모든 열의 요약 | df.describe(include="all") |
| 열 이름과 타입 | df.columns, df.dtypes |
| 값별 개수 세기 | df["city"].value_counts() |
| 값별 비율 | df["city"].value_counts(normalize=True) |
| 고유값과 그 개수 | df["city"].unique(), df["city"].nunique() |
| 열별 결측값 개수 | df.isna().sum() |
| 무작위 행 샘플 | df.sample(3, random_state=0) |
| 숫자 열의 상관관계 | df.corr(numeric_only=True) |
열과 행 선택
loc은 레이블로 선택하고 슬라이스 끝을 포함해요. iloc은 위치로 선택하고 끝을 제외해요.
| 작업 | 코드 |
|---|---|
| 열 하나를 Series로 | df["age"] |
| 여러 열을 DataFrame으로 | df[["name", "age"]] |
| 레이블로 행과 열 선택 | df.loc[0:2, ["name", "age"]] |
| 위치로 행과 열 선택 | df.iloc[0:2, 0:3] |
| 값 하나 | df.at[0, "age"], df.iat[0, 3] |
| 마지막 행 | df.iloc[-1] |
| 특정 타입의 열 | df.select_dtypes(include="number") |
| 텍스트 열 (pandas 3) | df.select_dtypes(include="str") |
| 이름이 일치하는 열 | df.filter(like="sal"), df.filter(regex="^(age|salary)$") |
| 열 값으로 행 조회 | df.set_index("name").loc["Ada", "salary"] |
| 0, 1, 2 인덱스로 되돌리기 | df.reset_index(drop=True) |
| 열 또는 행 삭제 | df.drop(columns=["email"]), df.drop(index=[0, 1]) |
행 필터링
조건마다 괄호로 감싸고 &(and), |(or), ~(not)로 조합하세요.
| 작업 | 코드 |
|---|---|
| 조건에 맞는 행 | df[df["age"] > 30] |
| 두 조건 모두 | df[(df["age"] > 30) & (df["city"] == "Oslo")] |
| 둘 중 하나의 조건 | df[(df["city"] == "Oslo") | (df["salary"] > 6000)] |
| 리스트에 있는 값 | df[df["city"].isin(["Oslo", "Rome"])] |
| 리스트에 없는 값 | df[~df["city"].isin(["Oslo", "Rome"])] |
| 두 값 사이 (양 끝 포함) | df[df["age"].between(30, 40)] |
| 쿼리 문자열로 필터링 | df.query("age > 30 and city == 'Oslo'") |
| 쿼리에서 변수 사용 | min_age = 30
df.query("age > @min_age") |
| 텍스트 포함 여부 | df[df["name"].str.contains("e", case=False)] |
| 결측값 또는 값이 있는 행 | df[df["age"].isna()], df[df["age"].notna()] |
| 행 필터링과 열 선택 동시에 | df.loc[df["age"] > 30, ["name", "salary"]] |
| 상위 또는 하위 n개 행 | df.nlargest(3, "salary"), df.nsmallest(2, "age") |
| 중복된 값이 있는 행 | df[df.duplicated(subset=["city"], keep=False)] |
정렬과 순위
| 작업 | 코드 |
|---|---|
| 열 기준 정렬 | df.sort_values("salary") |
| 큰 값부터 | df.sort_values("salary", ascending=False) |
| 여러 열 기준 정렬 | df.sort_values(["city", "salary"], ascending=[True, False]) |
| 결측값을 앞으로 | df.sort_values("age", na_position="first") |
| 인덱스 기준 정렬 | df.sort_index() |
| 순위 매기기 (가장 큰 값이 1) | df["salary"].rank(ascending=False) |
열 추가와 변경
열 전체 연산은 빨라요. 기본 제공 메서드로 안 될 때만 apply를 쓰세요.
| 작업 | 코드 |
|---|---|
| 다른 열로 새 열 만들기 | df["bonus"] = df["salary"] * 0.1 |
| 체인 안에서 열 추가 | df.assign(bonus=lambda d: d["salary"] * 0.1) |
| 조건에 따른 값 | df["level"] = np.where(df["age"] >= 40, "senior", "junior") |
| 여러 조건에 따른 값 | df["band"] = np.select([df["age"] < 30, df["age"] < 45], ["young", "mid"], default="senior") |
| 조건을 만족하는 곳의 값 변경 | df.loc[df["city"] == "Lima", "salary"] = 4000 |
| 딕셔너리로 값 매핑 | df["country"] = df["city"].map({"Oslo": "NO", "Rome": "IT", "Lima": "PE"}) |
| 열에 함수 적용 | df["salary"].apply(lambda x: round(x, -3)) |
| 모든 셀에 함수 적용 | df[["age", "salary"]].map(lambda x: x / 1000) |
| 열 이름 변경 | df.rename(columns={"salary": "pay"}) |
| 열 타입 변경 | df["salary"].astype("float64") |
| 텍스트를 숫자로, 잘못된 값은 NaN으로 | pd.to_numeric(s, errors="coerce") |
| 숫자를 레이블 구간으로 | pd.cut(df["age"], bins=[0, 30, 45, 120], labels=["young", "mid", "senior"]) |
| 숫자를 같은 크기 그룹으로 | pd.qcut(df["salary"], q=4, labels=False) |
| 행 반복 (느림, 최후의 수단) | for row in df.itertuples():
print(row.name, row.salary) |
결측값과 데이터 정제
숫자 결측값은 NaN, 날짜 결측값은 NaT로 표시돼요. 아래 호출은 모두 새 객체를 반환하니 결과를 변수에 할당하세요.
| 작업 | 코드 |
|---|---|
| 열별 결측값 비율 | df.isna().mean() |
| 결측값이 하나라도 있는 행 삭제 | df.dropna() |
| 핵심 열이 비어 있는 행 삭제 | df.dropna(subset=["age"]) |
| 열마다 다른 값으로 채우기 | df.fillna({"age": df["age"].median()}) |
| 이전 또는 다음 행 값으로 채우기 | df["age"].ffill(), df["age"].bfill() |
| 보간으로 빈칸 채우기 | df["age"].interpolate() |
| 특정 표기를 결측값으로 바꾸기 | df.replace(["n/a", "-", ""], np.nan) |
| 중복 행 삭제 | df.drop_duplicates() |
| 키별 마지막 행만 남기기 | df.drop_duplicates(subset=["city"], keep="last") |
| 열 이름 정리 | df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_") |
| 극단값 제한 | df["salary"].clip(lower=4000, upper=6000) |
| 반복되는 텍스트는 category 타입으로 | df["city"] = df["city"].astype("category") |
텍스트 열 (.str)
pandas 3에서는 텍스트 열이 기본적으로 str dtype이 돼요. .str 메서드는 열 전체에 동작하고, 결측값은 그대로 결측값으로 남아요.
| 작업 | 코드 |
|---|---|
| 대문자, 소문자, 첫 글자 대문자 | df["name"].str.upper(), df["name"].str.lower(), df["name"].str.title() |
| 공백 제거 | df["name"].str.strip() |
| 텍스트 바꾸기 | df["email"].str.replace("mail.com", "example.com") |
| 정규식으로 바꾸기 | df["email"].str.replace(r"@.*", "", regex=True) |
| 포함 여부, 시작 문자열 | df["name"].str.contains("a"), df["name"].str.startswith("E") |
| 각 값의 길이 | df["name"].str.len() |
| 앞쪽 문자 | df["name"].str[:2] |
| 여러 열로 분리 | df["email"].str.split("@", expand=True) |
| 정규식 그룹으로 추출 | df["email"].str.extract(r"@(\w+)") |
| 텍스트 열 합치기 | df["name"] + " (" + df["city"] + ")" |
| 숫자 앞을 0으로 채우기 | df["salary"].astype("str").str.zfill(6) |
| 새 텍스트 dtype | df["name"].dtype # str |
날짜와 시간
.dt 접근자는 datetime 열에서 동작해요. 정렬된 날짜 인덱스(아래의 ts)가 있으면 텍스트로 슬라이싱하고 리샘플링할 수 있어요.
| 작업 | 코드 |
|---|---|
| 열을 datetime으로 변환 | df["date"] = pd.to_datetime(df["date"]) |
| 정확한 형식으로 변환 | pd.to_datetime(["03/01/2026", "15/01/2026"], format="%d/%m/%Y") |
| 연도, 월, 요일 이름 | df["date"].dt.year, df["date"].dt.month, df["date"].dt.day_name() |
| 날짜를 월별로 묶기 | df["date"].dt.to_period("M") |
| 두 날짜 사이의 일수 | (pd.Timestamp("2026-12-31") - df["date"]).dt.days |
| 일 또는 월 더하기 | df["date"] + pd.Timedelta(days=30), df["date"] + pd.DateOffset(months=1) |
| 날짜 범위 안의 행 | df[df["date"].between("2021-01-01", "2022-12-31")] |
| 날짜 인덱스, 텍스트로 슬라이싱 | ts = df.set_index("date").sort_index()
ts.loc["2021":"2022"] |
| 월별 합계 | ts["salary"].resample("ME").sum() |
주별 평균, "QE"는 분기, "YE"는 연도 | ts["salary"].resample("W").mean() |
| 이동 평균 | ts["salary"].rolling(3).mean() |
| 이전 행 대비 변화 | ts["salary"].diff(), ts["salary"].pct_change() |
| 값을 한 행 아래로 밀기 | ts["salary"].shift(1) |
| 누적 합계 | ts["salary"].cumsum() |
| 시간대 지정과 변환 | ts.index.tz_localize("UTC").tz_convert("Europe/Oslo") |
그룹화와 집계
groupby는 키로 행을 나누고, 그룹마다 함수를 적용한 뒤 결과를 합쳐요.
| 작업 | 코드 |
|---|---|
| 그룹별 평균 | df.groupby("city")["salary"].mean() |
| 여러 통계를 한 번에 | df.groupby("city")["salary"].agg(["mean", "max", "count"]) |
| 결과 열 이름 지정 | df.groupby("city").agg(avg_pay=("salary", "mean"), people=("name", "count")) |
| 두 열로 그룹화 | df.groupby(["city", "team"])["salary"].sum() |
| 키를 열로 유지 | df.groupby("city", as_index=False)["salary"].mean() |
| 그룹별 행 개수 | df.groupby("city").size() |
| 그룹 결과를 모든 행에 | df["city_avg"] = df.groupby("city")["salary"].transform("mean") |
| 일부 그룹만 남기기 | df.groupby("city").filter(lambda g: len(g) >= 2) |
| 그룹별 최댓값 행 | df.loc[df.groupby("city")["salary"].idxmax()] |
| 그룹별 상위 n개 행 | df.sort_values("salary", ascending=False).groupby("city").head(2) |
| 그룹 안에서 행 번호 매기기 | df.groupby("city").cumcount() |
| 그룹별로 반복 | for city, group in df.groupby("city"):
print(city, len(group)) |
| 필터링, 그룹화, 정렬을 한 체인으로 | (df.query("age > 30")
.groupby("city", as_index=False)
.agg(avg_pay=("salary", "mean"))
.sort_values("avg_pay", ascending=False)) |
피벗과 재구조화
롱 데이터는 관측값 하나가 한 행이에요. 와이드 데이터는 변수 하나를 여러 열에 펼쳐 놓아요.
| 작업 | 코드 |
|---|---|
| 피벗 테이블 | df.pivot_table(index="city", columns="team", values="salary", aggfunc="mean", fill_value=0) |
| 합계가 있는 피벗 테이블 | df.pivot_table(index="city", values="salary", aggfunc="sum", margins=True) |
| 조합별 개수 | pd.crosstab(df["city"], df["team"]) |
| 행별 비율 | pd.crosstab(df["city"], df["team"], normalize="index") |
| 와이드에서 롱으로 | long = df.melt(id_vars="name", value_vars=["age", "salary"], var_name="field", value_name="value") |
| 롱에서 와이드로 | long.pivot(index="name", columns="field", values="value") |
| 안쪽 인덱스 레벨을 열로 | df.groupby(["city", "team"])["salary"].sum().unstack(fill_value=0) |
| 리스트 항목마다 한 행 | pd.DataFrame({"id": [1, 2], "tags": [["a", "b"], ["c"]]}).explode("tags") |
| 열 원-핫 인코딩 | pd.get_dummies(df["team"], prefix="team") |
| 행과 열 바꾸기 | df.T |
DataFrame 합치기
concat은 프레임을 이어 붙이고, merge는 SQL처럼 키 열을 기준으로 조인해요.
| 작업 | 코드 |
|---|---|
| 행 이어 붙이기 | pd.concat([df_2025, df_2026], ignore_index=True) |
| 옆으로 나란히 붙이기 | pd.concat([left, right], axis=1) |
| 키 열로 조인 | orders.merge(customers, on="customer_id", how="left") |
| 조인 종류 | how="inner", "left", "right", "outer" |
| 이름이 다른 키 | orders.merge(users, left_on="customer_id", right_on="id") |
| 조인 검증과 행 출처 표시 | orders.merge(customers, on="customer_id", how="left", validate="many_to_one", indicator=True) |
| 인덱스로 조인 | left.join(right, how="inner") |
| 다른 테이블에 짝이 없는 행 | orders[~orders["customer_id"].isin(customers["customer_id"])] |
pandas로 시각화
pandas는 Matplotlib으로 그려요. 모든 plot 호출은 Matplotlib ax를 반환하므로 이어서 스타일을 꾸밀 수 있어요.
| 작업 | 코드 |
|---|---|
| Series 선 그래프 | ts["salary"].plot() |
| 두 열로 선 그래프 | df.plot(x="date", y="salary") |
| 요약값 막대 그래프 | df.groupby("city")["salary"].mean().plot.bar() |
| 정렬된 가로 막대 | df["city"].value_counts().sort_values().plot.barh() |
| 히스토그램 | df["salary"].plot.hist(bins=20) |
| 산점도 | df.plot.scatter(x="age", y="salary") |
| 그룹별 박스 플롯 | df.boxplot(column="salary", by="city") |
| 제목과 축 레이블 | ax = df.plot(x="date", y="salary", title="Salary")
ax.set_ylabel("EUR") |
| 차트 보기 또는 저장 | plt.show(), ax.figure.savefig("salary.png", dpi=150) |
pandas 3: 예전 방식과 지금 쓰는 방식
예전 튜토리얼에서 복사한 코드는 pandas 3에서 오류나 경고가 나는 경우가 많아요. 대신 pandas 3 열의 방식으로 작성하세요.
| 작업 | pandas 3 | 예전 방식 |
|---|---|---|
| 모든 셀에 함수 적용 | df.map(f) | df.applymap(f) |
| 이전 행 값으로 채우기 | df.ffill() | df.fillna(method="ffill") |
| 행 추가 | pd.concat([df, new_rows]) | df.append(new_rows) |
| 월말 기준 리샘플링 | ts["salary"].resample("ME").sum() | resample("M") |
| 시간 단위 날짜 | pd.date_range("2026-01-01", periods=24, freq="h") | freq="H" |
| 값 하나 설정 | df.loc[0, "age"] = 37 | df["age"][0] = 37 |
| 필터링한 부분 변경 | high = df[df["salary"] > 5000]
high["salary"] = 0 | high = df[df["salary"] > 5000].copy() |
| 텍스트 열인지 확인 | df["name"].dtype == "str" | df["name"].dtype == object |
현업 데이터 분석가가 매일 쓰는 pandas 코드를 작업별로 모았어요. 데이터 만들기와 불러오기, 살펴보기, 행 선택과 필터링, 열 추가, 결측값 정리, 텍스트와 날짜 다루기, 그룹화와 집계, 재구조화, 병합, 시각화까지 담았어요. 각 행에는 바로 붙여 넣을 수 있는 짧은 코드와 무엇을 하는지 설명하는 한 줄이 있어요.
모든 내용은 Coddy에서 실행하는 버전인 pandas 3 기준이에요. 텍스트 열은 새로운 str dtype을 쓰고, Copy-on-Write는 항상 켜져 있으며, applymap이나 fillna(method=...)처럼 제거된 API는 예전 방식 표에만 나와요. 예제는 name, city, team, age, salary, date, email 열이 있는 DataFrame df를 사용해요. Python 자체가 처음이라면 Python 치트시트와 Python 플레이그라운드부터 시작하고, NumPy, Matplotlib, scikit-learn 치트시트도 함께 띄워 두세요.
pandas 치트시트 자주 묻는 질문
이 pandas 치트시트는 무료인가요?
이 치트시트는 어떤 pandas 버전을 다루나요?
str dtype을 쓰고, Copy-on-Write는 항상 켜져 있으며, pandas 3에서 제거된 호출(applymap, fillna(method=...), append, M과 H 빈도 별칭)은 마지막 표에만 대신 쓸 코드와 함께 나와요. str dtype이나 Copy-on-Write에 의존하는 행을 빼면 거의 모든 코드가 pandas 2.2에서도 실행돼요.loc과 iloc의 차이는 무엇인가요?
loc은 레이블, 즉 인덱스 값과 열 이름으로 선택하며, df.loc[0:2] 같은 슬라이스는 끝을 포함해요. iloc은 Python 리스트처럼 정수 위치로 선택하므로 df.iloc[0:2]는 처음 두 행을 반환하고 끝은 제외해요. loc에 조건을 넣으면 행 필터링과 열 선택을 한 번에 할 수 있어요: df.loc[df["age"] > 30, ["name"]].DataFrame을 여러 조건으로 필터링하려면 어떻게 하나요?
&, or는 |, not은 ~로 연결하세요: df[(df["age"] > 30) & (df["city"] == "Oslo")]. Python의 and와 or는 열 전체에는 동작하지 않아요. 필터가 길다면 df.query("age > 30 and city == 'Oslo'")가 더 읽기 쉬워요.merge, join, concat의 차이는 무엇인가요?
merge는 SQL 조인처럼 키 열을 기준으로 두 DataFrame을 합치며, how="inner", "left", "right", "outer"를 지정해요. join은 인덱스 기준으로 merge하는 단축 방법이에요. concat은 키를 전혀 맞추지 않고 프레임을 위아래로 쌓거나, axis=1이면 옆으로 나란히 붙여요.값을 할당했는데 왜 DataFrame이 바뀌지 않나요?
df["age"][0] = 37 같은 연쇄 할당은 임시 객체만 바꾸고 df는 그대로 둬요. 대신 loc 호출 하나로 쓰세요: df.loc[0, "age"] = 37. dropna, fillna 같은 메서드도 새 객체를 반환하니 결과를 할당하세요.