- Published on
pandas SettingWithCopyWarning 완전 해결 - loc·copy
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
pandas를 쓰다 보면 한 번쯤은 아래 경고를 만나게 됩니다.
> SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
처음엔 “경고니까 무시해도 되나?” 싶지만, 이 경고는 조용히 데이터가 안 바뀌는 버그로 이어질 수 있어 위험합니다. 특히 필터링한 결과에 컬럼을 만들거나 값을 수정하는 전처리 파이프라인에서 자주 터집니다.
이 글에서는 SettingWithCopyWarning의 본질을 정확히 이해하고, loc와 copy()를 중심으로 의도를 명확히 표현해 경고를 “완전히” 없애는 패턴을 정리합니다. (경고를 끄는 방식은 해결이 아니라 은폐이므로 마지막에만 언급합니다.)
SettingWithCopyWarning이 뜨는 진짜 이유
핵심은 pandas가 어떤 객체가 원본(DataFrame)의 뷰(view) 인지, 아니면 독립된 복사본(copy) 인지 확실히 알기 어려운 경우가 있다는 점입니다.
df[...]같은 슬라이싱/필터링은 상황에 따라 view가 될 수도, copy가 될 수도 있습니다.- 그 결과로 만든
tmp에 값을 대입하면, 그 변경이 원본df에 반영될지 보장할 수 없습니다. - pandas는 “이거 위험한 패턴인데?”라는 신호로 SettingWithCopyWarning을 띄웁니다.
즉, 이 경고는 단순한 스타일 경고가 아니라 데이터 무결성 경고에 가깝습니다.
재현: 가장 흔한 실수 패턴
아래는 전형적인 예시입니다.
import pandas as pd
df = pd.DataFrame({
"user": ["a", "b", "c", "d"],
"score": [10, 55, 70, 40],
"group": ["x", "x", "y", "y"],
})
filtered = df[df["score"] >= 50]
filtered["passed"] = True # SettingWithCopyWarning 가능
여기서 filtered가 view인지 copy인지 불명확합니다. 따라서 filtered["passed"] = True는
- 경고만 뜨고 잘 되는 것처럼 보이거나
- 어떤 경우에는 원본 df에 반영되지 않거나
- 이후 연산에서 예상치 못한 결과를 만들 수 있습니다.
해결 원칙 1: “원본을 바꿀 거면” loc로 한 번에
원본 df를 수정하려는 의도라면, 중간 객체에 대입하지 말고 원본에 loc로 직접 대입하세요.
mask = df["score"] >= 50
df.loc[mask, "passed"] = True
# 조건을 만족하지 않는 행도 명확히 채우고 싶다면
# df.loc[~mask, "passed"] = False
이 패턴의 장점:
- pandas가 “원본에 대한 명확한 인덱싱 대입”으로 이해하므로 경고가 사라집니다.
- 코드 리뷰 관점에서도 “원본 변경”이 분명합니다.
체이닝 인덱싱(Chained indexing)을 피하라
경고를 가장 자주 만드는 형태는 체이닝 인덱싱입니다.
# 나쁜 예: 두 번 인덱싱
# df[df["score"] >= 50]["passed"] = True
위 코드는 df[...]가 만든 임시 객체에 다시 ["passed"]로 접근한 뒤 대입하는 형태라, pandas가 안전하게 처리하기 어렵습니다.
해결 원칙 2: “부분집합을 새 데이터로 쓸 거면” copy()로 의도 선언
원본을 변경할 생각이 없고, 필터링 결과를 별도의 데이터셋으로 다룰 거라면 명시적으로 복사본을 만들고 그 위에서 작업하세요.
filtered = df.loc[df["score"] >= 50].copy()
filtered["passed"] = True
여기서 중요한 포인트는:
df.loc[mask]로 행 선택을 명확히 하고.copy()로 “이건 독립 객체다”를 선언한다는 점입니다.
이러면 SettingWithCopyWarning은 사라지고, 원본 df가 변하지 않는 것도 보장됩니다.
해결 원칙 3: 새로운 컬럼 생성은 assign으로 함수형 스타일로
필터링 후 컬럼을 추가하는 목적이라면 assign을 사용하면 경고를 원천 차단하면서 파이프라인을 깔끔하게 만들 수 있습니다.
filtered = (
df.loc[df["score"] >= 50]
.assign(passed=True)
)
assign은 새 DataFrame을 반환하므로 “대입” 자체가 줄어들고, 체이닝 인덱싱 문제도 덜 발생합니다.
케이스별 정답 패턴 정리
1) 특정 조건의 행만 일부 컬럼 업데이트
mask = (df["group"] == "x") & (df["score"] < 50)
df.loc[mask, "score"] = 50
2) 필터링 결과를 별도 테이블로 만들고 수정
x = df.loc[df["group"] == "x"].copy()
x["score_rank"] = x["score"].rank(ascending=False)
3) 여러 컬럼을 한 번에 업데이트
mask = df["score"] >= 50
df.loc[mask, ["passed", "grade"]] = [True, "P"]
값이 행별로 달라지는 경우에는 벡터 연산을 쓰는 게 일반적입니다.
mask = df["score"] >= 50
df.loc[:, "passed"] = mask
df.loc[:, "grade"] = "F"
df.loc[mask, "grade"] = "P"
“경고가 떴는데 결과는 맞는 것 같은데요?”가 위험한 이유
SettingWithCopyWarning은 재현이 들쭉날쭉해 보일 수 있습니다. 이유는 다음과 같습니다.
- 어떤 슬라이싱은 운 좋게 view처럼 동작하고
- 어떤 슬라이싱은 copy가 되어 변경이 반영되지 않으며
- pandas 버전/내부 최적화/컬럼 타입에 따라 결과가 달라질 수 있습니다.
즉, 테스트 데이터에서는 통과해도 운영 데이터에서 깨질 수 있는 유형입니다. 이런 문제는 웹/인프라에서 “가끔만 터지는” 장애처럼 디버깅 비용을 폭발시킵니다. (장애를 빠르게 진단하는 관점은 Cloudflare 520·521, Nginx·ALB 로그로 30분 진단 같은 글의 접근과도 닮아 있습니다.)
실전 디버깅 체크리스트
경고가 떴을 때 아래 질문을 순서대로 답하면 해결이 빨라집니다.
- 원본 df를 바꾸려는가?
- Yes →
df.loc[mask, col] = value형태로 고친다.
- Yes →
- 필터링 결과를 새 데이터로 쓸 건가?
- Yes →
df.loc[mask].copy()로 복사본을 명시한다.
- Yes →
- 체이닝 인덱싱을 하고 있나?
df[...][...] = ...형태가 보이면 거의 확정으로 위험하다.
- 함수형 파이프라인이 가능한가?
assign,where,mask,np.where등을 고려한다.
(비추천) SettingWithCopyWarning을 끄는 방법이 왜 안 좋은가
아래처럼 전역 옵션으로 경고를 끌 수는 있습니다.
import pandas as pd
pd.options.mode.chained_assignment = None # 경고 비활성화
하지만 이건 “문제가 없어졌다”가 아니라 “경고등을 떼버렸다”에 가깝습니다. 데이터가 실제로 수정되지 않는 버그가 숨어도 감지하기 어려워집니다.
운영에서 이런 식의 은폐는 결국 장애로 돌아오며, 장애 대응 비용은 누적됩니다. 예를 들어 외부 API 호출이 폭주할 때 429를 무시하면 더 큰 비용/지연으로 되돌아오는 것처럼, 경고를 무시하면 더 큰 데이터 품질 문제로 돌아옵니다(OpenAI API 429 폭탄 대응 실전 가이드 지수 백오프 큐잉 토큰 버짓으로 비용과 지연을 함께 줄이기).
결론: loc와 copy로 “의도”를 코드에 박아라
SettingWithCopyWarning을 완전히 해결하는 방법은 단순합니다.
- 원본을 수정하려면: 중간 슬라이스에 대입하지 말고
df.loc[...] = ...로 한 번에. - 부분집합을 새 데이터로 사용하려면:
df.loc[...].copy()로 복사본을 명시하고 그 위에서 수정. - 가능하면
assign등으로 대입을 줄이고 파이프라인을 함수형으로.
이 원칙만 지키면 경고를 억누를 필요가 없고, 전처리 코드가 버전/데이터 변화에도 안정적으로 동작합니다.