- Published on
Pandas SettingWithCopyWarning 완전 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서브셋을 만든 뒤 값을 바꾸려는데 SettingWithCopyWarning이 뜨면, 대부분은 “경고만 없애면 되지 않나?”로 흘러갑니다. 하지만 이 경고는 단순한 소음이 아니라 내가 수정한 값이 원본에 반영될 수도, 안 될 수도 있는 모호한 상태를 알려줍니다. 즉, 같은 코드가 환경이나 데이터 상태에 따라 조용히 틀린 결과를 만들 수 있습니다.
이 글에서는 경고가 발생하는 구조(뷰 vs 복사본), 대표적인 위험 패턴(체이닝 인덱싱, 필터링 후 대입, 컬럼 슬라이싱 후 대입)을 분류하고, 항상 의도대로 동작하는 패턴으로 바꾸는 방법을 코드로 정리합니다.
참고로 CSV를 읽는 과정에서 인코딩 문제로 전처리 단계가 흔들리면(컬럼 타입이 달라지거나 결측이 달라짐) 경고 재현이 더 어려워집니다. CSV 인코딩이 불안정한 환경이라면 Python UnicodeDecodeError - CSV 인코딩 자동탐지 실전도 함께 점검하는 걸 추천합니다.
SettingWithCopyWarning이 의미하는 것
Pandas는 성능을 위해 어떤 연산은 원본의 뷰(view) 를 반환하고, 어떤 연산은 복사본(copy) 을 반환합니다. 문제는 사용자가 코드를 읽을 때 이 차이가 항상 명확하지 않다는 점입니다.
- 뷰: 원본과 메모리를 공유할 수 있어, 뷰를 수정하면 원본도 바뀔 수 있습니다.
- 복사본: 원본과 분리되어, 복사본을 수정해도 원본은 안 바뀝니다.
SettingWithCopyWarning은 “지금 너는 원본인지 복사본인지 확실하지 않은 객체에 대입하고 있다”고 경고합니다. 즉, 아래 두 의도가 섞여 있을 때 발생합니다.
- 원본
df의 일부 행만 골라서 원본을 수정하고 싶다 - 부분집합
sub만 수정하고 원본은 건드리고 싶지 않다
경고를 없애는 핵심은 단순합니다.
- 원본을 수정하려면: 한 번의 인덱싱으로 원본에 대입한다 (
loc사용) - 부분집합만 수정하려면: 명시적으로
copy()해서 “복사본이다”를 확정한다
가장 흔한 원인: 체이닝 인덱싱
다음 패턴이 대표적인 지뢰입니다.
import pandas as pd
df = pd.DataFrame({
"name": ["a", "b", "c"],
"age": [10, 20, 30],
"score": [1.0, 2.0, 3.0],
})
# 체이닝 인덱싱: df[mask][col] = value
mask = df["age"] >= 20
df[mask]["score"] = 0 # SettingWithCopyWarning 가능
df[mask]가 뷰인지 복사본인지 확실하지 않은 상태에서, 그 결과에 다시 ["score"]로 접근해 대입합니다. 이때 Pandas는 “원본에 적용되는지 보장 못 한다”고 경고합니다.
해결: loc로 원본에 단일 대입
mask = df["age"] >= 20
df.loc[mask, "score"] = 0
loc[행조건, 컬럼] = 값은 원본df에 대한 단일 대입이므로 의도가 명확합니다.- 이 패턴 하나만 습관화해도 경고의 80%는 사라집니다.
필터링 후 컬럼 추가/수정: 부분집합을 수정하려면 copy()
다음은 “부분집합만 따로 만들고 거기서 작업”하는 코드에서 자주 나옵니다.
sub = df[df["age"] >= 20]
sub["grade"] = "adult" # SettingWithCopyWarning 가능
여기서 의도가 둘 중 무엇인지가 중요합니다.
의도 A: 원본 df에 grade를 채우고 싶다
mask = df["age"] >= 20
df.loc[mask, "grade"] = "adult"
의도 B: sub만 독립적으로 쓰고 싶다
sub = df[df["age"] >= 20].copy()
sub["grade"] = "adult" # 경고 없이 안전
copy()는 비용이 있지만, “이 객체는 원본과 분리됐다”는 계약을 명시합니다. 데이터 크기가 큰 경우에는 아래 assign 패턴도 고려할 수 있습니다.
assign으로 함수형 스타일 유지하기
체이닝을 유지하고 싶다면 assign이 안전한 대안이 될 수 있습니다. assign은 새 DataFrame을 반환하므로, “원본 수정”이 아니라 “새 결과 생성”이라는 의도가 분명합니다.
sub = (
df.loc[df["age"] >= 20]
.assign(grade="adult")
)
- 원본
df는 유지 sub는 새 객체- 경고 가능성이 낮고 파이프라인 구성에 유리
inplace=True와 경고의 관계
inplace=True가 경고를 해결해주지 않습니다. 오히려 뷰/복사본 모호성을 숨기고 디버깅을 어렵게 만들 수 있습니다. 예를 들어 sub가 복사본이라면 sub.rename(..., inplace=True)는 원본에 영향이 없고, 기대와 다르게 동작합니다.
권장 방향은 다음 중 하나입니다.
- 원본을 바꿀 거면:
df.loc[...] = ...같은 명시적 대입 - 새 객체를 만들 거면:
df = df.rename(...)처럼 반환값을 다시 받기
컬럼 슬라이스 후 대입도 위험하다
컬럼 부분집합을 만든 뒤 수정하는 경우도 마찬가지입니다.
cols = ["age", "score"]
sub = df[cols]
sub["score"] = sub["score"] * 10 # 경고 가능
해결: 원본을 바꿀지, 부분집합을 바꿀지 결정
원본을 바꾸려면:
df.loc[:, "score"] = df["score"] * 10
부분집합만 바꾸려면:
sub = df[cols].copy()
sub["score"] = sub["score"] * 10
안전한 패턴 모음 (치트시트)
1) 조건에 맞는 행의 특정 컬럼 업데이트
df.loc[df["age"] >= 20, "score"] = 0
2) 여러 컬럼을 한 번에 업데이트
mask = df["age"] >= 20
df.loc[mask, ["score", "name"]] = [0, "adult"]
3) 조건부 계산 결과를 넣기
mask = df["age"] >= 20
df.loc[mask, "score"] = df.loc[mask, "score"] / 2
4) 새 컬럼을 조건부로 채우기
import numpy as np
df["grade"] = np.where(df["age"] >= 20, "adult", "child")
np.where는 원본 DataFrame에 컬럼을 한 번에 할당하므로 명확합니다.
5) 그룹 연산 결과를 맞춰 넣기 (정렬/인덱스 정합성 주의)
mean_by_name = df.groupby("name")["score"].transform("mean")
df.loc[:, "score_mean_by_name"] = mean_by_name
transform은 원본 인덱스와 길이를 맞춰주기 때문에 대입이 안전해집니다.
왜 어떤 때는 경고가 안 뜨는데도 위험한가
SettingWithCopyWarning은 “항상” 뜨는 게 아닙니다. 같은 코드라도 내부적으로 뷰가 만들어졌는지, 최적화 경로가 무엇인지에 따라 경고가 생략될 수 있습니다. 그래서 다음이 중요합니다.
- 경고가 안 떠도 체이닝 인덱싱은 피한다
- “원본 수정”은
loc단일 대입으로 고정한다 - “부분집합 수정”은
.copy()로 고정한다
디버깅: 내가 가진 객체가 뷰인지 복사본인지 힌트 얻기
Pandas가 뷰/복사본을 완전히 명시적으로 알려주지는 않지만, 실무에서 도움이 되는 점검이 있습니다.
- 부분집합을 만든 직후에 원본을 바꿔보고 부분집합이 같이 바뀌는지 확인
sub = df[df["age"] >= 20]
before = sub["score"].copy()
df.loc[df["age"] >= 20, "score"] = 999
after = sub["score"]
print(before.equals(after))
- 애초에 불확실성을 없애기
- 원본을 바꿀 목적이면
sub를 만들지 말고loc로 바로 대입 sub가 필요하면.copy()를 명시
경고를 “끄는” 접근이 위험한 이유
다음처럼 전역 옵션으로 경고를 끄는 글이 종종 있는데, 추천하지 않습니다.
import pandas as pd
pd.options.mode.chained_assignment = None
이건 “모호한 대입”을 해결하는 게 아니라 경고만 숨깁니다. 데이터 파이프라인이 길어질수록, 어느 지점에서 원본이 안 바뀌었는지 추적하기가 매우 어려워집니다.
특히 전처리 결과를 Parquet으로 저장하거나 Arrow 기반 처리로 넘길 때는 스키마/결측/타입이 조용히 달라져 장애로 이어질 수 있습니다. Parquet 스키마 충돌을 겪고 있다면 pyarrow TableInvalid - Parquet 스키마 충돌 해결처럼 후속 단계에서 폭발하기도 합니다.
실전 리팩터링 예시: 위험 코드를 안전 코드로
기존 코드 (경고 + 의도 불명)
# 1) 필터
sub = df[df["country"] == "KR"]
# 2) 파생 컬럼
sub["revenue_krw"] = sub["revenue"] * 1350
# 3) 일부 업데이트
sub[sub["revenue_krw"] < 0]["revenue_krw"] = 0
문제점:
sub = df[mask]가 뷰/복사본 불명sub[cond][col] = ...체이닝 인덱싱
의도 1: 원본 df에 반영하고 싶다
mask_kr = df["country"] == "KR"
df.loc[mask_kr, "revenue_krw"] = df.loc[mask_kr, "revenue"] * 1350
df.loc[mask_kr & (df["revenue_krw"] < 0), "revenue_krw"] = 0
의도 2: KR 데이터셋만 별도로 만들고 싶다
mask_kr = df["country"] == "KR"
sub = df.loc[mask_kr].copy()
sub["revenue_krw"] = sub["revenue"] * 1350
sub.loc[sub["revenue_krw"] < 0, "revenue_krw"] = 0
이렇게 바꾸면:
- 경고가 사라지고
- 원본 반영 여부가 명확해지고
- 테스트가 쉬워집니다
체크리스트: SettingWithCopyWarning을 “완전 해결”하는 습관
df[mask][col] = ...형태를 발견하면 즉시df.loc[mask, col] = ...로 바꾸기- 필터 결과를 별도 변수로 들고 가야 하면
.copy()를 붙여 의도를 고정하기 - 파이프라인 스타일이면
assign으로 “새 객체 생성”을 명시하기 - 경고를 전역으로 끄지 말고, 경고가 가리키는 모호한 대입을 제거하기
마무리
SettingWithCopyWarning은 Pandas의 “불친절한 경고”가 아니라, 데이터가 조용히 틀어질 수 있는 지점을 알려주는 안전장치입니다. 해결의 핵심은 단 하나입니다.
- 원본 수정은
loc단일 대입 - 부분집합 수정은
copy()로 분리
이 두 규칙을 팀 컨벤션으로 고정해두면, 경고를 없애는 수준을 넘어 재현 가능한 데이터 처리 코드를 만들 수 있습니다.