인지야공

인지야공/인공 지능 공부 치트 시트 정리/2번째 글

pandas 치트시트를 다시 쓴다 — loc, 원본이 바뀌는 자리, 그리고 사라진 append

pandas 치트시트는 함수가 빽빽한데, 정작 시험장에서 시간을 잡아먹는 것은 함수 이름이 아니라 경계와 부작용이다. 슬라이스가 끝을 포함하느냐, 방금 만든 것이 원본이냐 복사냐, 결측이 조용히 빠졌느냐. 그 자리들만 모아 다시 썼다.

확인 환경은 pandas 2.2.3 이다. ADP 시험장은 1.1.2 라 반대로 도는 것이 있는데 마지막 절에 따로 적었다. 배열 쪽은 NumPy 글에 있다.

고르기 — loc 은 끝을 포함하고 iloc 은 포함하지 않는다

이것 하나가 치트시트에서 가장 자주 지나치는 차이다.

기준슬라이스의 끝
df.loc[...]라벨(인덱스 값, 열 이름)포함한다
df.iloc[...]위치(0부터 세는 정수)포함하지 않는다
df = pd.DataFrame({"a":[4,5,6], "b":[7,8,9], "c":[10,11,12]}, index=[1,2,3])

df.loc[1:2]          # 2행. 라벨 1 부터 2 까지 — 2 를 포함한다
df.iloc[1:2]         # 1행. 파이썬 슬라이스와 같아서 끝을 뺀다
df.loc[:, 'a':'b']   # 열도 마찬가지로 'b' 까지 포함한다

인덱스가 0,1,2… 인 데이터에서는 둘이 같아 보여서 넘어가다가, 정렬하거나 필터링해서 인덱스가 뒤섞인 뒤에 사고가 난다. 그때부터 df.loc[0] 은 “첫 행”이 아니라 “라벨이 0인 행”이다.

df[df.Length > 7]                  # 조건으로 행 고르기
df.loc[df['a'] > 10, ['a', 'c']]   # 조건 + 열 고르기는 loc 한 번에
df.iloc[:, [1, 2, 5]]              # 위치로 열 고르기
df.at[4, 'A']; df.iat[1, 2]        # 값 하나만 꺼낼 때. loc/iloc 보다 빠르다
df.query('Length > 7 and Width < 8')   # 조건이 길어지면 이쪽이 읽기 쉽다

원본이 바뀌는 자리 — SettingWithCopyWarning

거른 결과에 값을 넣는 이 코드는 경고만 뜨고 아무 일도 일어나지 않는다.

sub = base[base.a > 1]   # 이것이 뷰인지 복사인지 pandas 도 장담하지 못한다
sub["b"] = 0             # SettingWithCopyWarning
base.b.tolist()          # [4, 5, 6] — 원본은 그대로다

원본을 고칠 생각이었다면 .loc 으로 한 번에 쓴다. 원본을 건드리지 않을 생각이었다면 .copy() 를 명시한다. 둘 중 무엇이었는지 코드에 드러나야 한다.

base.loc[base.a > 1, "b"] = 0    # 원본을 고친다
sub = base[base.a > 1].copy()    # 따로 뜬다. 이후 무엇을 하든 원본은 안전하다

경고를 본 순간 “돌아갔으니 됐다”고 넘기면 안 된다. 경고는 값이 안 들어갔다는 뜻이다.

결측 — 조용히 빠지는 곳이 세 군데다

df.dropna()               # 결측이 하나라도 있는 행을 버린다
df.dropna(subset=["a"])   # 특정 열만 기준으로
df.fillna(value)
df.ffill(); df.bfill()    # 앞/뒤 값으로 채운다

df["x"].mean() 같은 집계는 결측을 알아서 빼고 계산한다. NumPy 가 nan 을 뱉는 것과 정반대라 둘을 섞어 쓰면 값이 달라진다. 그리고 조용히 빠지는 자리가 세 군데 더 있다.

g.groupby("k")["v"].sum()               # {'a': 3}      ← 키가 결측인 행이 통째로 빠졌다
g.groupby("k", dropna=False)["v"].sum() # {'a': 3, nan: 3}

s.value_counts()                        # {'a': 2}      ← 결측은 안 센다
s.value_counts(dropna=False)            # {'a': 2, None: 1}
s.value_counts(normalize=True)          # 비율로

“합계가 안 맞는다”의 태반이 이것이다. 결측 개수를 먼저 찍고 시작한다 — df.isna().sum().

새 열 만들기 — apply 는 마지막 수단이다

df["Volume"] = df.Length * df.Height * df.Depth        # 벡터화. 이게 기본이다
df.assign(Area=lambda d: d.Length * d.Height)          # 체이닝 중간에 열을 더할 때
pd.qcut(df.col, 4, labels=False)                       # 분위수로 4등분 (구간 폭이 아니라 개수 기준)
pd.cut(df.col, bins=[0, 10, 20])                       # 값의 구간으로 나눌 때
np.where(df.a > 10, "높음", "낮음")                     # 조건 두 갈래

20만 행에서 재 보면 차이가 분명하다.

big["x"].apply(lambda v: v * 2 + 1)   # 23ms
big["x"] * 2 + 1                      #  1ms  ← 20배

apply 는 파이썬 함수를 행마다 부른다. 벡터 연산으로 쓸 수 있으면 쓰고, 정말 안 되는 로직만 apply 로 남긴다.

묶기 — agg 와 transform 은 길이가 다르다

g.groupby("k")["v"].mean()                # 그룹당 한 줄. 길이 2
g.groupby("k")["v"].transform("mean")     # 원래 행마다 그 그룹의 값. 길이 3

그룹 평균을 원래 표에 새 열로 붙이려면 transform 이다. agg 로 만든 뒤 merge 하는 사람이 많은데 한 줄이면 된다.

df["그룹평균"] = df.groupby("k")["v"].transform("mean")
df["편차"] = df["v"] - df["그룹평균"]

df.groupby("k").agg(합=("v","sum"), 평균=("v","mean"))   # 여러 통계를 이름 붙여 한 번에
df.groupby("k", as_index=False)["v"].sum()               # 키를 인덱스 말고 열로 받는다

붙이기 — 조인은 행을 불린다

방식남는 것
how='inner'양쪽에 다 있는 키만
how='left'왼쪽 전부 + 맞는 오른쪽
how='right'오른쪽 전부
how='outer'양쪽 전부
pd.merge(adf, bdf, how="left", on="x1")
pd.concat([df1, df2])            # 아래로 쌓기 (행 추가)
pd.concat([df1, df2], axis=1)    # 옆으로 붙이기 (열 추가)
adf[adf.x1.isin(bdf.x1)]         # 조인 말고 "있는 것만 거르기"

키가 유일하지 않으면 행이 곱해진다. 3행과 3행을 붙였는데 5행이 나오는 식이다.

len(a.merge(b, on="id"))                    # 5  ← id=1 이 양쪽에 2개씩 있었다
a.merge(b, on="id", validate="one_to_one")  # MergeError 로 미리 잡는다
a.merge(b, on="id", how="left", indicator=True)  # _merge 열로 어디서 왔는지 본다

합친 뒤에는 len(df) 을 반드시 확인한다. 조인 전후로 행 수가 달라졌다면 거기서 멈춰야 한다.

모양 바꾸기 — pivot 은 중복을 못 견딘다

pd.melt(df)                                  # 넓은 표 → 긴 표 (열을 행으로)
df.pivot(index="i", columns="c", values="v") # 긴 표 → 넓은 표
df.pivot_table(index="i", columns="c", values="v", aggfunc="mean")

같은 (index, columns) 자리에 값이 둘 이상이면 pivot 은 ValueError 를 낸다. pivot_table 은 대신 평균을 내 버린다(기본 aggfunc="mean"). 조용히 평균이 되는 쪽이 더 위험하니, 집계할 생각이 없었으면 pivot 을 쓰고 에러로 받는 편이 낫다.

df.sort_values("mpg", ascending=False)
df.rename(columns={"y": "year"})
df.reset_index(drop=True)     # drop=True 를 빼면 옛 인덱스가 열로 남는다
df.drop(columns=["Length"])

순위와 창

s.rank(method="dense")   # [10,20,20,30] → [1, 2, 2, 3]  동점 뒤 번호를 안 건너뛴다
s.rank(method="min")     # 동점은 작은 순위. pct=True 면 0~1 로
s.shift(1)               # 한 칸 밀기 (전날 값 만들 때)
s.rolling(2).mean()      # [nan, 15, 20, 25]  ← 앞쪽은 창이 안 차서 결측이다
s.expanding().mean()     # 처음부터 누적

rolling 결과 앞부분의 결측은 오류가 아니라 정의다. 시계열 피처를 만든 뒤 모델에 넣기 전 dropna() 를 잊지 않는다.

2.x 에서 없어졌거나 바뀐 것들

옛 코드지금(2.2.3)대신
df.append(other)없다(2.0 에서 제거)pd.concat([df, other])
df.fillna(method="ffill")FutureWarningdf.ffill()
df.ix[...]없다(1.0 에서 제거)df.loc / df.iloc
df.mean() (문자열 열 포함)TypeErrordf.mean(numeric_only=True)
df.sort_values(..., inplace=True)돌지만 None 을 준다반환값을 받아 쓴다. 체이닝이 끊긴다

inplace=True 는 메모리를 아껴 주지도 않고 체이닝만 끊는다. 새로 받아 쓰는 습관이 낫다.

그리고 여기서도 방향이 반대인 함정이 있다. df.append 는 시험장 환경(1.1.2)에서는 멀쩡히 돌고 요즘 환경에서만 없다. 집에서 concat 으로 연습하면 시험장에서도 그대로 되지만, 남의 옛 코드를 그대로 가져오면 요즘 환경에서 깨진다. 시험장 환경 글에 적은 것과 같은 이야기다.

출처

pandas 공식 사이트의 Data Wrangling with pandas Cheat Sheet(Irv Lustig, Princeton Consultants)를 보고 다시 쓴 것이다. 원본은 pandas.pydata.org 에서 받을 수 있다. 여기 있는 문장과 코드, 확인 결과는 내가 쓰고 내가 돌려 본 것이다.

표시는 이 브라우저에만 남는다. 서버로 가는 것은 없다.