- Published on
pandas SettingWithCopyWarning 완전 정리
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서브셋을 만든 뒤 컬럼 값을 바꾸려는데 SettingWithCopyWarning이 뜨면, 많은 사람이 “그냥 경고니까 무시해도 되나?” 혹은 “.copy()를 무조건 붙이면 되나?” 사이에서 헤맵니다. 이 경고는 단순한 스타일 이슈가 아니라, **내가 수정한 값이 원본 DataFrame에 반영될지 말지(또는 일부만 반영될지)**가 모호하다는 신호입니다.
이 글에서는 SettingWithCopyWarning이 발생하는 내부 맥락(뷰와 복사의 불확실성), 재현 패턴, 그리고 실무에서 가장 안전한 해결책을 코드로 정리합니다.
SettingWithCopyWarning이 말하는 핵심
SettingWithCopyWarning은 pandas가 다음 상황을 감지했을 때 띄웁니다.
- 어떤
DataFrame에서 부분집합을 만든 객체가 원본의 뷰(view)인지 복사(copy)인지 확실하지 않다 - 그 부분집합에 대해 체인 인덱싱(chained indexing) 형태로 값을 할당했다
- 그래서 “너 지금 수정한 게 원본에 반영될 수도, 안 될 수도 있어”라고 경고한다
여기서 중요한 포인트는 “뷰냐 복사냐”가 항상 결정적으로 고정되지 않는다는 점입니다. pandas 내부 최적화, 블록 매니저, 컬럼 dtype 구성 등에 따라 결과가 달라질 수 있습니다. 즉, 동일한 코드가 데이터/버전/상황에 따라 조용히 다른 결과를 만들 수 있습니다.
가장 흔한 발생 패턴: 체인 인덱싱
아래가 대표적인 문제 코드입니다.
import pandas as pd
df = pd.DataFrame({
"user": ["a", "b", "c", "d"],
"age": [17, 20, 16, 30],
"score": [80, 90, 70, 95],
})
# 1) 필터로 일부 행을 고르고
# 2) 그 결과에 다시 컬럼 선택 후 할당
# 이 형태가 체인 인덱싱의 전형
df[df["age"] >= 18]["adult"] = True
위 코드는 흔히 SettingWithCopyWarning을 발생시키며, 더 나쁜 점은 실제로 df에 adult 컬럼이 생기지 않거나, 생기더라도 기대와 다르게 동작할 수 있다는 것입니다.
왜 위험한가
df[df["age"] >= 18] 결과가:
- 원본
df의 뷰면: 뒤의 할당이 원본에 영향을 줄 수도 있음 - 복사본이면: 복사본만 바뀌고 원본은 그대로임
pandas는 이 둘을 확정할 수 없으니 경고를 띄웁니다.
정석 해결: .loc로 한 번에 인덱싱하고 할당
가장 권장되는 해결책은 조건과 컬럼 선택을 .loc 한 번으로 끝내는 것입니다.
mask = df["age"] >= 18
df.loc[mask, "adult"] = True
이 패턴은 다음 장점이 있습니다.
- 원본
df에 대해 명시적으로 할당 - 체인 인덱싱 제거
- 경고 제거 + 동작 확정
else 값까지 채우고 싶다면
df["adult"] = False
mask = df["age"] >= 18
df.loc[mask, "adult"] = True
또는 한 줄로:
df["adult"] = df["age"].ge(18)
“부분집합을 따로 다룰 거면” .copy()로 의도를 고정
필터링한 결과를 별도 DataFrame으로 두고 계속 가공할 생각이라면, 애초에 복사본임을 확정하는 것이 좋습니다.
adults = df.loc[df["age"] >= 18].copy()
adults["grade"] = "A" # 경고 없음, 원본 df와 독립
이 경우 핵심은 “원본을 바꾸려는 게 아니라, 파생 테이블을 만들겠다”는 의도를 코드로 표현하는 것입니다.
.copy()를 남발하면 안 되나?
- 단순히 경고를 끄려고 무조건
.copy()를 붙이면 메모리 비용이 증가합니다. - 하지만 파생 테이블을 만들고 그걸 기준으로 downstream 작업을 한다면
.copy()는 오히려 안전장치입니다.
실무에서는 아래 기준이 깔끔합니다.
- 원본을 수정해야 한다:
.loc[...] = ... - 파생 테이블을 만들고 독립적으로 수정한다:
.copy()
또 다른 흔한 케이스: 컬럼 슬라이싱 후 할당
컬럼을 일부만 뽑아 만든 DataFrame에 값을 넣을 때도 경고가 나올 수 있습니다.
sub = df[["user", "score"]]
sub["score"] = sub["score"] * 10
이때도 상황에 따라 뷰/복사가 섞일 수 있습니다. 안전하게 하려면:
sub를 독립 객체로 쓰려면sub = df[[...]].copy()- 원본을 바꾸려면
df.loc[:, "score"] = df["score"] * 10
# 원본 수정
df.loc[:, "score"] = df["score"] * 10
# 파생 테이블로 독립 수정
sub = df[["user", "score"]].copy()
sub.loc[:, "score"] = sub["score"] * 10
.loc를 써도 경고가 나는 경우가 있나?
대부분의 경고는 체인 인덱싱에서 오지만, 간혹 이런 식의 코드도 문제를 만듭니다.
sub = df[df["age"] >= 18]
sub.loc[:, "score"] = 0
여기서 할당은 .loc지만, 이미 sub = df[...] 단계에서 sub가 뷰인지 복사인지 불확실합니다. 따라서 sub 자체가 불안정한 객체일 수 있어 경고가 계속 날 수 있습니다.
해결은 앞에서 말한 것처럼 의도를 고정하면 됩니다.
sub = df.loc[df["age"] >= 18].copy()
sub.loc[:, "score"] = 0
또는 원본을 수정하려는 목적이면 sub를 만들지 말고 바로 원본에 .loc 할당을 합니다.
mask = df["age"] >= 18
df.loc[mask, "score"] = 0
실무에서 추천하는 안전한 패턴 4가지
1) 조건부 업데이트: .loc[mask, col] = value
mask = (df["age"] >= 18) & (df["score"] >= 90)
df.loc[mask, "vip"] = True
2) 새 컬럼 생성: assign
assign은 체인 인덱싱 위험을 줄이고, 파이프라인에 잘 맞습니다.
out = (
df
.assign(
adult=lambda x: x["age"].ge(18),
score2=lambda x: x["score"] * 2,
)
)
3) 파생 테이블: .copy()로 독립성 명시
report = df.loc[df["age"] >= 18, ["user", "score"]].copy()
report["tier"] = pd.cut(report["score"], bins=[0, 80, 90, 100], labels=["B", "A", "S"])
4) 함수형 파이프라인: pipe로 단계 분리
def add_flags(x: pd.DataFrame) -> pd.DataFrame:
x = x.copy()
x["adult"] = x["age"].ge(18)
return x
out = df.pipe(add_flags)
이 패턴은 “함수 안에서 복사 후 수정”을 강제해, 사이드 이펙트를 줄입니다.
경고를 끄는 건 해결이 아니다
아래처럼 옵션으로 경고를 숨길 수는 있지만, 문제의 본질(불확실한 할당)을 제거하지 못합니다.
pd.options.mode.chained_assignment = None # 경고 비활성화
이 설정은 팀/프로젝트 차원에서 디버깅 난이도를 급격히 올릴 수 있습니다. 특히 데이터 파이프라인에서 “어느 단계에서 값이 안 바뀌었는지” 추적이 어려워집니다.
권장 순서는:
- 체인 인덱싱 제거
- 원본 수정이면
.loc로 직접 - 파생 테이블이면
.copy()로 의도 고정
디버깅 체크리스트
SettingWithCopyWarning을 봤을 때 아래 질문을 순서대로 던지면 빠르게 정리됩니다.
- 지금 수정하려는 대상은 원본
df인가, 파생DataFrame인가? - 코드에
df[...][...] = ...형태가 있는가? - 필터링 결과를 변수에 담았는가? 담았다면 그 변수는
.copy()가 필요한가? - 같은 결과를
.loc[mask, col] = ...로 표현할 수 있는가?
이 경고는 본질적으로 “데이터가 조용히 틀릴 수 있다”는 경고라서, 빨리 제거할수록 좋습니다. 이런 점은 DB에서 데드락이나 N+1처럼 “성공하는 것처럼 보이지만 운영에서 터지는” 유형과 닮았습니다. 원인 추적/재현/해결의 사고방식은 비슷하니, 장애 대응 관점이 궁금하다면 MySQL InnoDB Deadlock 원인 쿼리 추적·해결 가이드도 함께 참고할 만합니다.
결론
SettingWithCopyWarning은 “뷰인지 복사인지 불확실한 객체에 할당했다”는 경고다.- 가장 흔한 원인은 체인 인덱싱
df[...][...] = .... - 원본을 바꾸려면
.loc[mask, col] = value로 한 번에 할당한다. - 파생 테이블을 독립적으로 다룰 거면
.copy()로 의도를 명확히 한다. - 경고를 끄는 방식은 근본 해결이 아니며, 데이터 품질 문제를 숨길 수 있다.
위 원칙대로 코드를 정리하면 경고를 없애는 것을 넘어, pandas 조작이 “항상 같은 결과를 내는” 예측 가능한 형태로 바뀝니다.