- Published on
pandas SettingWithCopyWarning 0으로 잡는 6가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서브셋을 만들고 값을 수정했는데, 어떤 때는 원본 DataFrame이 바뀌고 어떤 때는 안 바뀌는 경험을 해봤다면 SettingWithCopyWarning을 이미 만난 겁니다. 이 경고는 단순히 “거슬리는 로그”가 아니라, 내 코드가 뷰(view)인지 복사(copy)인지 불명확한 상태에서 값을 썼다는 경고입니다. 즉, 결과가 환경·버전·최적화에 따라 달라질 수 있어 재현성까지 흔들립니다.
핵심은 하나입니다.
- 필터링(선택)과 할당(수정)을 분리하지 말고, 한 번에 명시적으로 수행하거나
- 복사를 의도했으면 복사를 명시하거나
- 원본을 수정하려면 원본을 대상으로
.loc등 명확한 API로 수정해야 합니다.
아래 6가지는 경고를 “숨기는” 게 아니라, 경고가 뜨는 원인을 제거해 경고 0개를 만드는 패턴입니다. 더 확장된 케이스는 pandas SettingWithCopyWarning 완전 해결법 8가지도 함께 참고하면 좋습니다.
왜 SettingWithCopyWarning이 뜨는가
대표적인 트리거는 체인 인덱싱입니다. 예를 들어 아래 코드는 경고가 뜰 수 있습니다.
import pandas as pd
df = pd.DataFrame({
"team": ["A", "A", "B"],
"score": [10, 20, 30]
})
# 체인 인덱싱: df[mask][col] = ...
df[df["team"] == "A"]["score"] = 0
df[df["team"] == "A"]가 뷰인지 복사본인지 pandas가 보장하지 못하기 때문에, 그 다음 [...]로 컬럼을 뽑아 할당하는 순간 “원본에 반영 안 될 수도 있음” 경고가 발생합니다.
이제부터는 같은 작업을 “안전하게” 바꾸는 방법들입니다.
1) .loc로 필터링과 할당을 한 번에 끝내기
가장 정석이며, 팀 내 표준으로 삼기 좋은 방법입니다.
mask = df["team"] == "A"
df.loc[mask, "score"] = 0
mask로 행을 고르고.loc[행, 열]로 원본df에 직접 할당합니다.
여러 컬럼을 동시에 수정
mask = df["team"] == "A"
df.loc[mask, ["score"]] = df.loc[mask, ["score"]] * 2
여러 컬럼이면 리스트로 열을 지정하세요. 이 패턴만 잘 지켜도 경고 대부분이 사라집니다.
2) 서브셋을 따로 쓸 거면 .copy()로 의도를 확정하기
서브셋을 만든 뒤 그 서브셋을 독립적으로 가공할 계획이라면, 복사를 명시해야 경고도 사라지고 의미도 분명해집니다.
subset = df.loc[df["team"] == "A"].copy()
subset["score"] = 0
subset은 원본과 분리된 데이터가 됩니다.- 원본을 바꾸고 싶다면 이 방법은 목적에 맞지 않습니다.
언제 .copy()가 특히 필요한가
- 필터링한 결과를 여러 단계로 가공할 때
- 함수 인자로 넘겨서 내부에서 수정할 때
SettingWithCopyWarning이 간헐적으로만 뜰 때(환경에 따라 뷰/복사 판정이 흔들리는 케이스)
3) assign으로 파이프라인 스타일 유지하기(불변식에 가깝게)
원본을 직접 수정하는 대신, 새 DataFrame을 반환하는 방식으로 작업하면 경고 자체가 발생할 여지가 줄어듭니다.
out = (
df
.assign(score=lambda x: x["score"].where(x["team"] != "A", 0))
)
.assign은 새 컬럼(또는 기존 컬럼 덮어쓰기)을 명시적으로 생성합니다.where는 조건에 따라 값을 유지하거나 바꿉니다.
이 방식은 데이터 파이프라인을 단계별로 합성할 때 특히 좋습니다. 프론트엔드에서 타입 안정성을 높이기 위해 제약을 명시하는 방식과 결이 비슷한데, 타입스크립트의 satisfies처럼 의도를 코드로 드러내는 접근은 TS 5.x satisfies로 타입 오류 줄이는 실전과도 통하는 면이 있습니다.
4) query와 .loc를 조합해 가독성 유지하기
조건이 복잡해질수록 마스크가 길어져 실수하기 쉽습니다. 이때 query로 조건을 읽기 좋게 만들고, 수정은 .loc로 처리하면 안전합니다.
mask = df.query("team == 'A' and score >= 10").index
df.loc[mask, "score"] = 0
포인트는 query 결과를 그대로 수정하지 않는 것입니다. query로는 “대상 인덱스”만 뽑고, 할당은 원본 df.loc[...]에서 수행하세요.
5) 그룹 단위 계산은 transform로 원본 인덱스 정렬을 보장하기
그룹별 평균을 빼는 정규화 같은 작업을 체인 인덱싱으로 하다 보면 경고뿐 아니라 정렬 미스매치 버그도 자주 납니다. groupby().transform()은 원본과 동일한 길이와 인덱스를 유지하므로 안전합니다.
# 팀별 평균을 뺀 centered_score 컬럼 생성
out = df.assign(
centered_score=df["score"] - df.groupby("team")["score"].transform("mean")
)
원본을 직접 수정하고 싶다면 아래처럼도 가능합니다.
df.loc[:, "centered_score"] = df["score"] - df.groupby("team")["score"].transform("mean")
여기서도 .loc[:, ...]로 “원본에 쓰겠다”를 명시합니다.
6) 경고를 0으로 만드는 마지막 점검: 체인 인덱싱 금지 규칙과 테스트
코드베이스에서 경고를 “완전히 0”으로 만들려면 개인의 주의만으로는 부족합니다. 아래 2가지를 팀 규칙으로 두면 효과가 큽니다.
6-1) 금지 패턴을 룰로 정하기
다음 형태는 원칙적으로 금지하세요.
df[mask]["col"] = valuedf["col"][mask] = value
대신 아래만 허용합니다.
df.loc[mask, "col"] = valuesubset = df.loc[mask].copy()후subset[...] = ....assign(...)으로 새 프레임 생성
6-2) SettingWithCopyWarning을 테스트에서 실패로 만들기
로컬에서는 넘어가도 CI에서 경고를 잡아내면 “경고 0” 상태를 유지하기 쉽습니다.
import pandas as pd
pd.options.mode.chained_assignment = "raise" # 경고를 예외로
# 이후 체인 인덱싱이 발생하면 SettingWithCopyError가 발생
- 개발/테스트 환경에서만
raise를 켜고 - 운영 환경에서는 기본값을 유지하는 전략이 현실적입니다.
경고를 억지로 끄는 아래 설정은 권장하지 않습니다.
pd.options.mode.chained_assignment = None # 경고 숨김(비권장)
경고가 사라져도 버그 가능성은 그대로 남습니다.
자주 나오는 Q&A
원본을 수정해야 할 때와 복사해서 수정해야 할 때는 어떻게 구분하나
- 원본
df가 이후 로직에서 그대로 쓰여야 하고, 수정이 전역적으로 반영되어야 한다면.loc로 원본을 수정하세요. - 특정 분석 단계의 중간 산출물로만 쓸 거라면
.copy()로 분리하세요.
.loc를 썼는데도 경고가 나는 경우가 있나
대부분은 .loc 사용 위치가 “서브셋 객체”에 적용된 경우입니다. 예를 들어 tmp = df[mask]를 만든 다음 tmp.loc[...] = ...를 하면 tmp 자체가 뷰/복사 불명확이라 경고가 날 수 있습니다. 이때는 tmp = df.loc[mask].copy()로 바꾸거나, 애초에 df.loc[mask, ...]로 원본에 직접 쓰세요.
정리: 경고 0의 기준은 ‘의도 명시’다
SettingWithCopyWarning을 0으로 만든다는 건, 단순히 로그를 없애는 게 아니라 수정 대상이 원본인지 복사본인지 매번 명확히 선언한다는 뜻입니다.
- 원본 수정:
.loc[mask, col] = value - 서브셋 가공:
.loc[mask].copy() - 파이프라인:
.assign(...) - 그룹 계산:
transform으로 인덱스 정렬 보장 - 품질 유지: 테스트에서
chained_assignment = 'raise'
이 6가지를 팀 규칙으로 고정하면, 경고를 “우연히” 피하는 게 아니라 “구조적으로” 제거할 수 있습니다.