- Published on
pandas SettingWithCopyWarning 완전정복 - 원인부터 해결까지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
pandas를 쓰다 보면 한 번쯤은 아래 경고를 마주칩니다.
> SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
이 경고는 단순히 “귀찮은 로그”가 아니라, 내가 의도한 DataFrame에 값이 실제로 반영되지 않을 수 있다는 신호입니다. 특히 전처리 파이프라인, 피처 엔지니어링, 리포팅 코드에서 조용히 데이터가 틀어지는 원인이 되기도 합니다.
이 글에서는 SettingWithCopyWarning을 **왜 발생하는지(뷰 vs 복사)**부터, **안전하게 수정하는 표준 패턴(.loc, .copy, assign, pipe)**까지 정리합니다. 이미 관련 이슈를 빠르게 해결하고 싶다면 내부 글인 pandas SettingWithCopyWarning 완전 해결법도 함께 참고하면 좋습니다.
SettingWithCopyWarning이 발생하는 진짜 이유
핵심은 pandas의 인덱싱 결과가 상황에 따라 **view(원본을 바라보는 창)**일 수도 있고 **copy(복사본)**일 수도 있다는 점입니다.
df[mask]같은 필터링은 대개 “새로운 DataFrame”을 반환합니다.- 그런데 그 결과가 내부적으로 원본과 메모리를 공유하는지(view) 완전히 분리된 복사본인지(copy)는 케이스마다 다릅니다.
- 이 모호성 때문에 pandas는 “지금 너가 변경하는 게 원본에 반영될지 확신할 수 없다”고 경고합니다.
즉, 경고는 다음 상황을 말합니다.
- 어떤 slice(부분집합)를 만들었고
- 그 slice에 값을 세팅했는데
- 그 slice가 원본의 view인지 copy인지 애매해서
- 원본이 바뀌지 않을 수 있으니 조심하라
가장 흔한 재현 패턴: 체인 인덱싱(Chained Indexing)
SettingWithCopyWarning의 대표 원인은 체인 인덱싱입니다.
나쁜 예: df[mask][col] = ...
import pandas as pd
df = pd.DataFrame({
"name": ["a", "b", "c", "d"],
"score": [10, 55, 70, 40],
"grade": [None, None, None, None]
})
mask = df["score"] >= 60
# 체인 인덱싱: df[mask] 결과에 다시 ["grade"]
df[mask]["grade"] = "PASS" # SettingWithCopyWarning 가능
이 코드는 “PASS”가 기대대로 들어간 것처럼 보여도, 실제로는 원본 df가 바뀌지 않거나 일부만 바뀌는 식의 불안정성이 생길 수 있습니다.
좋은 예: .loc[row_indexer, col_indexer]
df.loc[mask, "grade"] = "PASS" # 안전하고 명확
.loc는 원본 df에 대해 ‘행/열을 동시에’ 지정하므로 pandas가 모호함 없이 처리할 수 있습니다.
.loc를 표준으로 삼아야 하는 이유
.loc는 단순한 “경고 회피”가 아니라, 코드의 의미를 명확히 해줍니다.
df.loc[mask, "grade"] = ...: 원본 df의 특정 행/열을 수정한다tmp = df[mask]: 조건에 맞는 행들을 뽑아 새 객체를 만든다(대개)
이 둘을 섞어서 tmp["grade"] = ...처럼 쓰면, “tmp가 원본의 일부인가?”가 불분명해집니다.
언제 .copy()가 필요한가?
필터링한 결과를 독립된 테이블로 취급하고 싶을 때가 있습니다. 예를 들어:
- 조건에 맞는 행만 뽑아 리포트/모델 입력으로 사용
- 이후 변형이 원본에 절대 영향을 주면 안 됨
이럴 때는 명시적으로 .copy()를 붙여 “나는 복사본을 만들겠다”고 선언하는 게 좋습니다.
filtered = df.loc[df["score"] >= 60, ["name", "score"]].copy()
filtered["bucket"] = "high" # 경고 없음 + 원본 영향 없음
여기서 중요한 점:
df.loc[...]로 먼저 슬라이싱을 명확하게 하고.copy()로 의도를 못 박으면- SettingWithCopyWarning도 사라지고, 데이터 흐름도 안전해집니다.
자주 나오는 케이스별 해결 패턴
1) 특정 컬럼만 변환하고 싶다: assign 사용
assign은 새로운 DataFrame을 반환하며, 체인 인덱싱을 피하기 좋습니다.
result = (
df
.assign(score2=lambda x: x["score"] * 2)
)
필터링 후 변환이라면:
result = (
df.loc[df["score"] >= 60].copy()
.assign(grade="PASS")
)
2) 여러 조건으로 부분 업데이트: .loc + 마스크 조합
df["grade"] = "UNKNOWN"
df.loc[df["score"] >= 60, "grade"] = "PASS"
df.loc[df["score"] < 60, "grade"] = "FAIL"
이 방식은 “원본 df를 단계적으로 업데이트한다”는 의도가 명확합니다.
3) inplace=True와 결합된 체인 호출 피하기
pandas에서 inplace=True는 종종 혼란을 키웁니다. 특히 체인 호출과 결합하면 디버깅이 어려워집니다.
# 비추천: 중간 객체가 무엇인지 모호
# df[df["score"] >= 60].rename(columns={"score": "s"}, inplace=True)
# 추천: 명시적으로 새 객체 받기
high = df.loc[df["score"] >= 60].copy()
high = high.rename(columns={"score": "s"})
4) 그룹 연산 후 결과를 원본에 반영: transform 활용
그룹별 평균을 빼서 정규화한 값을 원본에 넣고 싶다면 .loc로 억지로 slice를 만들기보다 transform이 깔끔합니다.
df = pd.DataFrame({
"team": ["A", "A", "B", "B"],
"score": [10, 30, 20, 40]
})
team_mean = df.groupby("team")["score"].transform("mean")
df["score_centered"] = df["score"] - team_mean
“경고를 끄면 되지 않나요?”가 위험한 이유
SettingWithCopyWarning을 무시하거나 전역 옵션으로 끄는 글도 종종 보입니다.
pd.options.mode.chained_assignment = None # 경고 비활성화(비추천)
이건 증상을 숨길 뿐입니다. 진짜 문제는 다음 중 하나일 수 있습니다.
- 원본 df가 수정되지 않아 결과가 틀림
- 특정 버전/환경에서만 동작(재현 불가 버그)
- 파이프라인 중간 결과가 조용히 누락
경고를 끄는 대신, 경고가 나오는 지점을 “원본을 수정할 건지/복사본을 만들 건지”로 분기시키는 게 정답입니다.
디버깅 체크리스트: 경고가 뜨면 이렇게 확인
경고가 뜬 줄의 왼쪽이
df[...][...] = ...형태인가?- 그렇다면
.loc로 합치기
- 그렇다면
내가 수정하려는 대상은 원본 df인가, 필터링된 결과인가?
- 원본이면
df.loc[mask, col] = value - 결과 테이블이면
filtered = df.loc[mask].copy()후 수정
- 원본이면
함수 체인에서 중간 객체가 눈에 안 보이는가?
- 중간 변수를 도입하거나
pipe로 단계 분리
- 중간 변수를 도입하거나
같은 코드가 버전/환경에 따라 동작이 다른가?
- 체인 인덱싱 가능성이 큼.
.loc/.copy()로 의도 고정
- 체인 인덱싱 가능성이 큼.
실전 예제: 전처리 함수에서 안전하게 쓰는 패턴
아래는 흔한 전처리 요구사항(필터링, 파생 컬럼 생성, 부분 업데이트)을 경고 없이 구현한 예입니다.
import pandas as pd
def preprocess(df: pd.DataFrame) -> pd.DataFrame:
# 원본을 건드리지 않겠다는 정책이면 첫 줄에서 copy
out = df.copy()
# 결측 처리(예시)
out["score"] = out["score"].fillna(0)
# 파생 컬럼
out = out.assign(is_pass=out["score"] >= 60)
# grade 업데이트는 .loc로 명확하게
out["grade"] = "FAIL"
out.loc[out["is_pass"], "grade"] = "PASS"
return out
df = pd.DataFrame({"name": ["a", "b"], "score": [70, 40]})
print(preprocess(df))
포인트는 다음과 같습니다.
- “원본 불변” 정책이면 초기에
copy() - 파생 컬럼은
assign - 조건부 업데이트는
.loc
이 조합이면 SettingWithCopyWarning을 구조적으로 피할 수 있습니다.
결론
SettingWithCopyWarning은 pandas가 “너 지금 원본을 수정하는지 복사본을 수정하는지 애매하다”고 알려주는 안전장치입니다. 해결의 핵심은 간단합니다.
- 원본을 수정할 거면:
.loc[rows, cols] = value - 복사본으로 작업할 거면:
.copy()로 의도를 명시 - 체인 인덱싱(
df[...][...] = ...)은 피하기
한 번 패턴을 정해두면 경고를 ‘없애는 것’이 아니라, 데이터 처리 코드를 더 예측 가능하고 견고하게 만들 수 있습니다.
추가로, SettingWithCopyWarning을 더 빠르게 진단하고 케이스별로 정리한 글이 필요하다면 pandas SettingWithCopyWarning 완전 해결법을 함께 보면 도움이 됩니다.