- Published on
Pandas SettingWithCopyWarning 완전정복 - 원인·해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서브셋 DataFrame에 값을 넣었는데 경고가 뜨고, 어떤 때는 값이 반영되고 어떤 때는 반영되지 않는 경험이 있다면 SettingWithCopyWarning을 정확히 이해해야 합니다. 이 경고는 “지금 수정한 대상이 원본의 뷰(view) 일 수도 있고 복사(copy) 일 수도 있어서, 당신이 기대한 대로 원본이 바뀐다는 보장이 없다”는 신호입니다.
Pandas는 성능을 위해 슬라이싱 결과를 항상 복사로 만들지 않습니다. 어떤 연산은 뷰를 반환할 수 있고(특히 열/행 슬라이싱, 체인 인덱싱), 어떤 연산은 복사를 반환합니다. 그런데 사용자가 그 결과에 다시 값을 대입하면, Pandas 입장에서는 원본을 바꿔야 하는지, 임시 객체를 바꿔야 하는지 확신하기 어렵습니다. 그래서 “확실한 방식으로 쓰라”는 의미로 경고를 띄웁니다.
이 글에서는 경고가 생기는 핵심 원리, 자주 나오는 재현 코드, 그리고 실무에서 안전하게 없애는 패턴(.loc, .copy, .assign, .where 등)을 정리합니다. 디버깅 체크리스트 관점은 다른 글의 방식과 비슷하게, 원인→증상→확실한 해결책 순서로 가져가겠습니다. (참고: 장애/경고를 “재현 → 원인 격리 → 안전한 수정”으로 푸는 접근은 systemd 서비스가 반복 재시작될 때 원인 추적법 같은 글과도 결이 같습니다.)
SettingWithCopyWarning이 정확히 의미하는 것
경고 메시지의 요지
대표 메시지는 다음과 같습니다.
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
핵심은 두 가지입니다.
df[...]같은 슬라이싱 결과가 원본의 뷰일 수도 있고 복사본일 수도 있다.- 그 결과에 다시
[...] = ...형태로 값을 대입하면, 원본에 반영된다는 보장이 없다.
왜 “체인 인덱싱(chained indexing)”이 특히 위험한가
체인 인덱싱은 보통 이런 형태입니다.
# 나쁜 예: 체인 인덱싱
subset = df[df["country"] == "KR"]
subset["age"] = subset["age"].fillna(0)
df[df[...]]가 이미 “중간 객체”를 만들었고, 거기에 다시 subset["age"] = ...를 하니 Pandas는 원본과의 연결성을 확신할 수 없습니다.
가장 흔한 재현 패턴 5가지
아래 예제는 모두 경고가 나거나, 더 위험하게는 “경고 없이도 의도와 다르게 동작”할 수 있는 패턴입니다.
1) boolean filter 후 컬럼 대입
import pandas as pd
df = pd.DataFrame({
"country": ["KR", "US", "KR"],
"score": [10, 20, 30]
})
kr = df[df["country"] == "KR"]
kr["score"] = kr["score"] * 2 # SettingWithCopyWarning 가능
2) 슬라이싱 후 컬럼 대입
part = df[:2]
part["score"] = 0 # SettingWithCopyWarning 가능
3) 열 선택 후 조건 대입(또는 그 반대)
# 특히 이런 형태가 자주 문제를 만듭니다.
df[df["country"] == "KR"]["score"] = 999 # 거의 확정적으로 위험
위 코드는 “필터링 결과에서 score만 선택해서 대입”이라는 두 단계가 체인으로 이어져, 원본 df에 반영되지 않을 수 있습니다.
4) inplace 연산과 결합
kr = df[df["country"] == "KR"]
kr["score"].replace(10, 11, inplace=True) # 경고/비권장 패턴
Pandas에서 inplace=True는 점점 권장되지 않는 흐름이며(특히 중간 객체에서), SettingWithCopyWarning과 결합하면 디버깅이 더 어려워집니다.
5) 함수 체이닝 중 중간 슬라이스 수정
(
df[df["country"] == "KR"]
.assign(score=lambda x: x["score"] * 2) # assign은 안전한 편
)
이 경우는 assign을 쓰면 안전하지만, 중간에 [...] = ...로 끼워 넣으면 다시 위험해집니다.
해결의 정석: “원본에 .loc로 한 번에 쓰기”
SettingWithCopyWarning을 가장 확실하게 없애는 방법은 원본 DataFrame에 대해 .loc[행조건, 열] = 값 형태로 한 번에 대입하는 것입니다.
boolean 조건에 대한 안전한 대입
mask = df["country"] == "KR"
df.loc[mask, "score"] = df.loc[mask, "score"] * 2
mask로 행을 고르고- 같은
mask를 사용해 원본df에서 값을 읽고 - 원본
df에 다시 씁니다.
이 방식은 “원본을 수정한다”는 의도가 명확해서 경고가 사라지고, 동작도 결정적입니다.
여러 컬럼을 동시에 수정
mask = df["country"] == "KR"
df.loc[mask, ["score"]] = df.loc[mask, ["score"]].mul(2)
또는 상수 대입이라면 더 간단합니다.
df.loc[df["country"] == "KR", "score"] = 0
“부분집합을 따로 쓰고 싶다”면 .copy()로 의도를 고정
원본을 수정하는 게 아니라, 필터링 결과를 독립된 데이터로 다루고 싶다면 .copy()를 붙여 “이건 복사본이다”를 명시하세요.
kr = df.loc[df["country"] == "KR"].copy()
kr["score"] = kr["score"] * 2 # 경고 없음 (독립 복사본)
여기서 중요한 포인트:
.copy()를 하지 않으면kr이 뷰인지 복사인지 애매할 수 있음.copy()를 하면 이후 연산은kr내부에서만 일어나며, 원본df는 바뀌지 않음
즉, “원본을 바꾸고 싶다”면 .loc로 원본에 쓰고, “원본과 분리하고 싶다”면 .copy()로 분리합니다.
실무에서 자주 쓰는 안전 패턴들
1) .assign()으로 새 컬럼 만들기 (함수형 스타일)
kr = (
df.loc[df["country"] == "KR"].copy()
.assign(score2=lambda x: x["score"] * 2)
)
assign은 “새 DataFrame을 반환”하는 스타일이라 중간 수정의 모호함이 줄어듭니다.
2) .where()/.mask()로 조건부 치환
df["score"] = df["score"].where(df["country"] != "KR", df["score"] * 2)
where(cond, other)는 cond가 True면 원래값 유지, False면 other로 대체- 조건을 반대로 쓰고 싶으면
.mask()가 직관적인 경우도 많습니다.
3) np.where로 조건부 값 생성 후 대입
import numpy as np
df["score"] = np.where(df["country"] == "KR", df["score"] * 2, df["score"])
이 방식은 “새 시리즈를 만들어 컬럼에 통째로 대입”이므로 경고가 날 여지가 적습니다.
경고를 “끄는 것”은 해결이 아니라 위험 은폐다
다음처럼 옵션으로 경고를 끌 수는 있습니다.
pd.options.mode.chained_assignment = None # 경고 비활성화
하지만 이는 문제를 해결하지 않고 신호만 제거합니다. 원본이 실제로 수정됐는지/안 됐는지 모호한 상태가 그대로 남아, 데이터 품질 이슈로 이어질 수 있습니다.
경고를 끄는 것은 시스템에서 오류 로그를 꺼두는 것과 비슷합니다. 디버깅 관점에서는 “재현 가능한 최소 코드로 원인을 확인하고, 안전한 수정 경로로 바꾸는 것”이 정석입니다. (장애 대응에서 재시도/백오프를 설계하듯이 신호를 무시하지 않는 것이 중요합니다. 관련해서는 Anthropic Claude 429 레이트리밋 재시도 설계법도 참고할 만합니다.)
디버깅 체크리스트: 경고가 났을 때 무엇을 확인할까
1) “원본을 바꾸려는가, 복사본을 만들려는가”를 먼저 결정
- 원본 변경 목적:
.loc[mask, col] = ... - 복사본 목적:
.loc[mask].copy()후 수정
이 결정을 못 하면 코드가 흔들립니다.
2) 체인 인덱싱을 한 번에 펼치기
# 나쁜 예
# df[df['A'] > 0]['B'] = 1
# 좋은 예
mask = df["A"] > 0
df.loc[mask, "B"] = 1
3) 중간 변수로 받은 DataFrame이 어디서 왔는지 추적
subset = df[cond]형태면 의심subset = df.loc[cond].copy()면 안전
4) inplace 사용을 줄이기
- 특히
subset["col"].method(..., inplace=True)는 피하기 - 반환값을 다시 받는 형태로 변경
subset["col"] = subset["col"].replace({"": None})
자주 묻는 질문(실무형)
Q1. .loc를 썼는데도 경고가 날 수 있나?
대부분은 .loc로 해결되지만, 여전히 “중간 객체”에 .loc를 쓰면 애매해질 수 있습니다.
subset = df[df["country"] == "KR"]
subset.loc[:, "score"] = 0 # subset 자체가 뷰/복사 불명확
이 경우도 정석은 둘 중 하나입니다.
- 원본에 직접 쓰기:
df.loc[mask, "score"] = 0 - 복사본을 명시:
subset = df.loc[mask].copy()
Q2. 성능 때문에 .copy()가 부담되면?
- 원본을 수정해도 된다면
.copy()가 아니라.loc로 원본에 직접 쓰는 게 오히려 효율적입니다. - 복사본이 필요하다면, 필요한 컬럼만 선택해서 복사 크기를 줄이세요.
cols = ["country", "score"]
kr = df.loc[df["country"] == "KR", cols].copy()
Q3. 경고가 안 떴는데도 위험할 수 있나?
네. 경고는 “항상” 뜨는 게 아니라, Pandas 내부 판단에 따라 뜨거나 안 뜰 수 있습니다. 그래서 경고 유무보다 코딩 패턴 자체를 안전하게 바꾸는 게 중요합니다.
결론: 한 문장으로 정리
SettingWithCopyWarning은 “슬라이스 결과가 뷰인지 복사인지 애매한데 거기에 값을 써서, 원본 반영이 보장되지 않는다”는 경고입니다.- 해결은 단순합니다: 원본을 바꾸려면
.loc로 한 번에, 부분집합을 따로 쓰려면.copy()로 의도를 고정하세요.
데이터 전처리 파이프라인에서 이 경고를 방치하면, 조용히 값이 누락되거나 일부만 바뀌는 형태로 품질 문제가 누적됩니다. 경고를 끄기보다, 위의 패턴으로 코드를 정리해 “결정적(deterministic)인 대입”을 만드는 것이 가장 확실한 해결입니다.