- Published on
Pandas SettingWithCopyWarning 완전 정복
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 데이터 전처리 코드가 같은 입력을 받았는데 결과가 가끔 달라진다면, 그 원인 중 하나가 SettingWithCopyWarning일 수 있습니다. 이 경고는 “체인 인덱싱(chained indexing)으로 인해 뷰(view)인지 복사(copy)인지 불명확해서, 대입이 원본에 반영되지 않을 수 있다”는 뜻입니다. 무시하면 조용히 잘못된 결과를 만들기도 하고, 반대로 과하게 copy()를 남발하면 메모리와 성능을 갉아먹습니다.
이 글에서는 경고의 발생 원리, 재현 가능한 예제, 안전한 수정 패턴, 그리고 팀 코드베이스에서 재발을 막는 규칙까지 한 번에 정리합니다.
SettingWithCopyWarning이 정확히 의미하는 것
Pandas에서 어떤 연산은 원본 DataFrame의 뷰를 반환할 수도 있고, 복사본을 반환할 수도 있습니다. 문제는 많은 경우 “항상 뷰” 혹은 “항상 복사”로 보장되지 않는다는 점입니다. 특히 아래 같은 형태는 위험합니다.
df[df["cond"]]["col"] = ...같은 체인 인덱싱- 중간 결과를 변수에 담아 수정한 뒤, 원본이 바뀌길 기대하는 코드
Pandas는 이런 상황에서 경고를 띄우며, 실제로는 다음 중 하나가 벌어질 수 있습니다.
- 원본이 바뀐다(우연히 뷰였던 경우)
- 원본이 안 바뀐다(복사본이었던 경우)
- 일부만 바뀌거나, 버전/옵션/메모리 레이아웃에 따라 달라진다
즉, 경고는 “지금 코드가 결정론적이지 않을 수 있다”는 신호입니다.
가장 흔한 재현 예제(체인 인덱싱)
아래 코드는 많은 사람이 한 번쯤 작성합니다.
import pandas as pd
df = pd.DataFrame({
"name": ["a", "b", "c", "d"],
"score": [10, 60, 70, 20],
"passed": [False, True, True, False],
})
# 위험: 체인 인덱싱
high = df[df["score"] >= 60]
high["passed"] = True # SettingWithCopyWarning 가능
print(df)
의도는 score가 60 이상인 행의 passed를 True로 만들고 싶다는 것입니다. 하지만 high가 뷰인지 복사인지 보장되지 않으므로, df가 실제로 업데이트되지 않을 수 있습니다.
올바른 수정: .loc로 한 번에
Pandas가 권장하는 정석은 “필터링과 대입을 한 번의 인덱싱으로” 수행하는 것입니다.
mask = df["score"] >= 60
df.loc[mask, "passed"] = True
mask는 불리언 인덱스.loc[행조건, 열] = 값형태는 명확하게 원본df에 대입합니다.
왜 .loc가 안전한가
.loc는 “어떤 객체에 값을 쓸지”가 명확합니다. 반면 df[cond][col] = ...는
df[cond]로 중간 객체를 만들고- 그 중간 객체의
[col]을 선택한 뒤 - 거기에 값을 씁니다
이 과정에서 중간 객체가 복사본이면 원본은 바뀌지 않습니다. .loc는 이 중간 단계를 없애서 경고의 근본 원인을 제거합니다.
자주 나오는 패턴별 해결법
1) 특정 조건의 여러 컬럼을 동시에 수정
mask = df["score"] >= 60
df.loc[mask, ["passed", "name"]] = [True, "winner"]
여러 컬럼을 한 번에 수정할 때도 .loc를 유지하세요.
2) 새 컬럼 생성: .assign을 습관화
필터링 후 새 컬럼을 추가하는 과정에서 경고가 자주 납니다.
high = df[df["score"] >= 60]
# 위험: high["grade"] = ...
대신 .assign을 사용하면 “새 객체를 만들고 거기에 컬럼을 추가”한다는 의도가 명확해집니다.
high = (
df.loc[df["score"] >= 60]
.assign(grade="A")
)
high를 별도 결과로 쓰려면, 애초에 원본 업데이트를 기대하지 않는 형태로 작성하는 편이 안전합니다.
3) 필터링 결과를 수정해서 다시 원본에 반영하고 싶다
이 경우는 두 가지 중 하나를 선택해야 합니다.
- 원본을 직접 수정한다:
.loc사용 - 복사본을 수정한다:
.copy()로 명시
원본을 직접 수정:
mask = df["score"] >= 60
df.loc[mask, "score"] += 5
복사본을 만들고 복사본만 수정:
high = df.loc[df["score"] >= 60].copy()
high["score"] += 5
여기서 .copy()는 “나는 원본과 분리된 결과가 필요하다”는 선언입니다.
4) 함수 체이닝에서의 안전한 대입
전처리를 체이닝으로 구성할 때, 중간 결과에 대입하는 코드가 끼면 경고가 나기 쉽습니다. 가능한 한 pipe, assign, where, mask 등을 활용해 “대입 없이 변환”으로 모델링하는 것이 좋습니다.
out = (
df
.assign(passed=lambda x: x["score"].ge(60))
.assign(score=lambda x: x["score"].where(x["score"].lt(60), x["score"] + 5))
)
ge,lt같은 벡터화 연산을 활용where로 조건부 치환
디버깅: 경고가 난 위치가 진짜 원인과 다를 때
SettingWithCopyWarning은 종종 “대입 라인”에서 터지지만, 실제 원인은 그 이전에 만들어진 중간 객체가 뷰인지 복사인지 애매한 데 있습니다. 아래 체크리스트로 접근하면 빠릅니다.
체크리스트
- 대입문이
df[cond][col] = ...형태인가 - 먼저 슬라이싱/필터링한 변수를 만들고, 그 변수에 대입하고 있나
df.loc[...]로 한 번에 대입할 수 있나- 원본을 바꿔야 하나, 결과만 필요하나(의도 결정)
- 결과만 필요하면
.copy()또는.assign으로 명시했나
데이터 파이프라인에서 이런 “원인-증상” 추적은 운영 장애 디버깅과도 유사합니다. 체크리스트 기반으로 의심 지점을 좁히는 접근은 인프라 트러블슈팅에서도 그대로 통합니다. 예를 들어 장애를 단계적으로 진단하는 방식은 systemd 서비스가 자꾸 재시작될 때 7단계 진단 같은 글의 흐름과도 닮아 있습니다.
경고를 끄면 안 되나
가능은 합니다. 하지만 권장하지 않습니다. 경고를 끄는 순간, “원본이 바뀌지 않는 버그”가 조용히 숨어들기 쉽습니다.
그래도 레거시 코드에서 점진적으로 정리해야 한다면, 최소한 “테스트로 의도를 고정”한 다음 경고를 제거하는 순서가 안전합니다. 성능이나 안정성 문제를 디버깅할 때도 마찬가지로, 증상을 덮기보다 원인을 제거해야 재발이 줄어듭니다. 이런 관점은 PostgreSQL VACUUM 안 돌 때 bloat 진단·해결처럼 근본 원인 분석이 중요한 글들과도 결이 같습니다.
실전 규칙: 팀 코드에서 재발 방지
규칙 1) “필터링 후 대입” 금지, “.loc로 대입” 강제
- 금지:
tmp = df[cond]다음tmp[col] = ... - 권장:
df.loc[cond, col] = ...
규칙 2) 결과 테이블을 만들 목적이면 .copy() 또는 .assign
- 결과를 별도 객체로 쓰려면
.copy()로 분리 - 새 컬럼 추가는
.assign선호
규칙 3) 리뷰 포인트를 단순화
코드 리뷰에서 아래 패턴이 보이면 무조건 수정 요청하기 좋습니다.
df[...][...] = ...subset = df[...]직후subset[...] = ...
이런 룰 기반 리뷰는 CI에서의 반복 실수 방지와도 비슷합니다. 예를 들어 캐시가 안 먹는 문제를 매번 수동으로 찾기보다 규칙으로 점검하는 방식은 GitLab CI 캐시 안 먹을 때 - 키·경로·권한 점검 같은 글의 접근과 같습니다.
자주 묻는 질문(실무에서 헷갈리는 지점)
Q1. .copy()를 항상 붙이면 끝 아닌가
끝은 맞지만 비용이 있습니다.
- 큰
DataFrame에서 불필요한 복사는 메모리 사용량과 GC 부담을 키움 - 파이프라인이 길수록 복사 비용이 누적
의도가 “원본 수정”이면 .loc가 정답이고, 의도가 “부분집합 결과 생성”이면 그때 .copy()가 정답입니다.
Q2. .loc를 썼는데도 경고가 날 수 있나
대부분의 케이스는 사라집니다. 다만 이미 df 자체가 다른 객체에서 파생된 뷰인 경우(예: 상위 슬라이스에서 만들어진 객체)라면, 더 상위 단계에서 .copy()로 의도를 고정해야 할 수 있습니다.
Q3. 어떤 연산이 뷰를 반환하는지 외우기 어렵다
외우기보다 습관을 바꾸는 게 낫습니다.
- 대입은 항상
.loc로 - 부분집합을 새 결과로 쓸 거면
.copy()로 명시 - 새 컬럼 추가는
.assign
이 3가지만 지켜도 경고의 90퍼센트는 제거됩니다.
마무리: 경고 제거가 아니라 “의도 명시”가 목표
SettingWithCopyWarning의 핵심은 “Pandas가 무엇을 수정해야 하는지 확신할 수 없다”는 데 있습니다. 따라서 해결책도 단순합니다.
- 원본을 바꾸려면:
df.loc[조건, 컬럼] = 값 - 결과만 만들려면:
df.loc[조건].copy()또는.assign
경고를 없애는 것이 목표가 아니라, 코드의 의도를 명확히 해서 데이터 전처리 결과를 결정론적으로 만드는 것이 목표입니다. 이 원칙을 팀 규칙으로 정착시키면, 데이터 품질 이슈와 디버깅 비용이 눈에 띄게 줄어듭니다.