- Published on
pandas SettingWithCopyWarning 완전 해결 - 원인·대안
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
pandas를 쓰다 보면 한 번쯤은 다음 경고를 마주칩니다.
> SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
이 경고는 “코드가 느슨해서”가 아니라, pandas의 인덱싱 결과가 원본의 뷰(view) 일 수도 있고 복사(copy) 일 수도 있는 애매한 상황에서, 그 결과에 다시 값을 대입하려 할 때 발생합니다. 문제는 경고를 무시하거나 pd.options.mode.chained_assignment = None로 끄는 순간, 어떤 경우에는 값이 반영되고 어떤 경우에는 반영되지 않는 비결정적 버그를 만들 수 있다는 점입니다.
이 글에서는 SettingWithCopyWarning을 ‘완전히 해결’하기 위해,
- 경고가 발생하는 정확한 조건(원인)
- 재현 가능한 최소 예제
- 안전한 대안 패턴(권장)
- “경고만 없애는” 안티패턴
- 디버깅/검증 방법
까지 한 흐름으로 정리합니다. 더 넓은 케이스/심화 내용은 내부 글인 pandas SettingWithCopyWarning 완전정복 - 원인부터 해결까지도 함께 참고하면 좋습니다.
SettingWithCopyWarning의 본질: 체인 인덱싱 + 뷰/복사 불확실성
경고가 자주 발생하는 전형적인 형태는 체인 인덱싱(chained indexing) 입니다.
df[mask][col] = valuedf[col][mask] = valuedf[df['A'] > 0]['B'] = 1
여기서 핵심은 df[mask] 또는 df[col] 같은 중간 결과가 원본을 바라보는 뷰일 수도, 복사본일 수도 있다는 점입니다. pandas는 내부적으로 성능 최적화를 위해 상황에 따라 뷰를 돌려주기도, 복사를 돌려주기도 하는데, 사용자가 이를 확실히 보장받기 어렵습니다.
따라서 pandas 입장에서는 “이 대입이 원본 df에 반영될지 확신할 수 없다” → 경고를 띄웁니다.
최소 재현 예제: 왜 위험한지 직접 보기
아래 예제는 경고가 뜨는 전형적인 코드입니다.
import pandas as pd
pd.set_option("mode.chained_assignment", "warn")
df = pd.DataFrame({
"user": ["a", "b", "c", "d"],
"score": [10, 20, 30, 40],
"active": [True, False, True, False],
})
# 체인 인덱싱: 중간 결과 df[df['active']]가 뷰인지 복사인지 불명확
subset = df[df["active"]]
subset["score"] = subset["score"] * 10 # SettingWithCopyWarning 가능
print("subset:\n", subset)
print("df:\n", df)
이 코드가 위험한 이유는 다음입니다.
subset만 바뀌고df는 안 바뀔 수 있음- 반대로 어떤 상황에서는 우연히
df까지 바뀌어 “되는 것처럼” 보일 수 있음
즉, 테스트 데이터에서는 통과했는데 운영 데이터/버전/환경에서 조용히 깨지는 타입의 버그가 됩니다.
정답 패턴 1: .loc[row_indexer, col_indexer]로 한 번에 대입
SettingWithCopyWarning을 근본적으로 피하는 가장 표준적인 방법은 .loc로 행/열 인덱서를 한 번에 지정하는 것입니다.
import pandas as pd
df = pd.DataFrame({
"user": ["a", "b", "c", "d"],
"score": [10, 20, 30, 40],
"active": [True, False, True, False],
})
mask = df["active"]
df.loc[mask, "score"] = df.loc[mask, "score"] * 10
print(df)
이 방식이 안전한 이유:
- 대입 대상이 명확히 “원본 df의 특정 행/열”로 지정됨
- 중간 객체(뷰/복사 불명확)를 만들지 않음
- 코드 의도가 명확하고 리뷰/유지보수에 강함
정답 패턴 2: 필터링 결과를 ‘독립 데이터’로 쓸 거면 .copy()를 명시
가끔은 원본을 바꾸고 싶은 게 아니라, 필터링된 결과를 별도의 데이터로 가공하고 싶을 때가 있습니다. 이때는 경고를 피하려고 .loc로 원본을 수정하는 게 아니라, 처음부터 복사본임을 명시해야 합니다.
subset = df[df["active"]].copy() # 의도를 명확히: subset은 독립 데이터
subset["score"] = subset["score"] * 10
포인트는 “경고를 없애는 용도”가 아니라, 데이터 소유권(ownership) 을 명확히 하는 것입니다.
- 원본을 수정해야 한다 →
.loc - 원본과 독립적으로 다룰 것이다 →
.copy()
정답 패턴 3: assign/where/mask로 선언적으로 변환
조건부 변환을 ‘대입’ 대신 ‘표현식’으로 풀면 체인 인덱싱을 피하면서 가독성도 좋아집니다.
where로 조건부 치환
import numpy as np
mask = df["active"]
df["score"] = df["score"].where(~mask, df["score"] * 10)
mask로 조건부 치환(반대 조건)
mask = df["active"]
df["score"] = df["score"].mask(mask, df["score"] * 10)
assign로 파이프라인 유지
mask = df["active"]
df = df.assign(score=lambda x: x["score"].mask(mask, x["score"] * 10))
이 방식은 특히 메서드 체이닝을 많이 쓰는 코드베이스에서 유용합니다.
자주 하는 실수: df[mask]["col"] = ...를 고치지 않고 우회하는 것
안티패턴 1: 경고 끄기
pd.options.mode.chained_assignment = None
이건 “문제 해결”이 아니라 “알람 제거”입니다. 데이터가 조용히 잘못될 가능성을 그대로 남깁니다.
안티패턴 2: inplace=True로 해결하려 하기
inplace=True는 SettingWithCopyWarning을 해결해주지 않습니다. 오히려 중간 객체에 inplace 연산을 걸면, 원본에 반영되지 않는 상황을 더 헷갈리게 만들 수 있습니다.
안티패턴 3: df2 = df[mask] 후 df2만 믿기
df2가 뷰인지 복사인지 불명확한 상태에서 df2를 수정하면, “원본 df가 바뀌었을 것”이라는 기대가 깨질 수 있습니다. 원본을 바꾸려면 .loc로 원본에 직접 쓰고, 독립 데이터가 필요하면 .copy()로 분리하세요.
케이스별 처방전: 내 의도가 무엇인지부터 결정
SettingWithCopyWarning을 만났을 때 가장 먼저 해야 할 질문은 하나입니다.
- 원본 DataFrame을 수정하려는가?
- 아니면 필터링 결과를 별도 데이터로 다루려는가?
1) 원본을 수정하려는 경우
- 무조건
.loc[rows, cols] = ...
rows = df["active"] & (df["score"] >= 20)
df.loc[rows, "score"] += 5
2) 별도 데이터로 다루려는 경우
- 필터링 직후
.copy()
report = df.loc[df["active"], ["user", "score"]].copy()
report["grade"] = pd.cut(report["score"], bins=[0, 20, 50, 100], labels=["low", "mid", "high"])
디버깅 팁: 경고를 ‘에러’로 승격해 CI에서 잡기
경고를 런타임에서 놓치지 않으려면, 개발/테스트 환경에서 SettingWithCopyWarning을 에러로 바꾸는 것이 효과적입니다.
import pandas as pd
pd.set_option("mode.chained_assignment", "raise")
# 이제 체인 인덱싱 대입이 발생하면 예외가 터져서 즉시 수정 가능
이 패턴은 “배포 겹침을 시스템적으로 막는” 접근과 비슷합니다. 예를 들어 파이프라인에서 동시성 이슈를 원천 차단하는 방식은 GitHub Actions 동시성 꼬임으로 배포 겹침 막기 같은 글에서 다루는 방향과도 결이 같습니다. 즉, 문제를 ‘운’에 맡기지 않고, 실패를 빠르게 드러내도록 설계하는 겁니다.
자주 묻는 질문(실전에서 많이 헷갈리는 포인트)
Q1. df.loc[mask]["col"] = ...는 왜 안 되나요?
loc를 썼더라도 df.loc[mask]가 먼저 평가되어 중간 객체가 만들어지고, 거기에 다시 ["col"]로 체인 인덱싱이 됩니다. 즉, 아래는 여전히 위험합니다.
df.loc[mask]["score"] = 0 # 여전히 체인 인덱싱
정답은 한 번에:
df.loc[mask, "score"] = 0
Q2. df[df['A'] > 0].copy()는 성능이 나쁘지 않나요?
복사는 비용이 듭니다. 하지만 “독립 데이터”가 필요하다면 복사 비용은 정당합니다. 반대로 원본 수정이 목적이라면 .copy()를 피하고 .loc로 원본에 직접 쓰는 것이 메모리/성능에 유리합니다.
Q3. 이 경고는 pandas 버전에 따라 다르게 보이던데요?
맞습니다. 내부 최적화/블록 매니저/Copy-on-Write(CoW) 관련 변화로 인해, 어떤 연산이 뷰를 반환하는지는 버전과 데이터 형태에 따라 달라질 수 있습니다. 그래서 더더욱 “경고가 안 뜨면 안전”이 아니라, 항상 안전한 패턴(.loc 또는 .copy)을 습관화해야 합니다.
실무 체크리스트: SettingWithCopyWarning을 ‘완전히’ 없애는 규칙
- 체인 인덱싱 대입(
df[...][...] = ...)은 금지 - 원본 수정은 항상
df.loc[rows, cols] = ... - 필터링 결과를 가공할 땐 즉시
.copy()로 독립성 확보 - 테스트/CI에서는
pd.set_option("mode.chained_assignment", "raise")로 조기 검출 - “경고 끄기”는 마지막 수단이 아니라 금지에 가깝게 운영
결론
SettingWithCopyWarning은 귀찮은 경고가 아니라, pandas가 알려주는 데이터 무결성 경보입니다. 해결의 핵심은 단순합니다.
- 원본을 바꿀 거면
.loc로 한 번에 지정해서 쓰기 - 원본과 분리할 거면
.copy()로 의도를 명시하기
이 두 가지 규칙만 팀 컨벤션으로 고정해도, SettingWithCopyWarning은 “가끔 뜨는 경고”가 아니라 “아예 설계에서 제거되는 문제”가 됩니다.