- Published on
Pandas SettingWithCopyWarning 근본 해결 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
SettingWithCopyWarning은 Pandas를 쓰다 보면 거의 반드시 마주치는 경고입니다. 많은 글이 “.loc 쓰면 된다” 정도로 끝나지만, 실제 현장에서는 필터링/정렬/체인 인덱싱/함수 체이닝이 섞이면서 경고가 다시 튀어나오고, 더 심각하게는 값이 일부만 반영되거나 반영되지 않는 침묵 오류로 이어지기도 합니다.
핵심은 간단합니다.
- Pandas는 어떤 연산의 결과가 원본의 뷰(view) 인지, 독립된 복사(copy) 인지 상황에 따라 달라집니다.
- 뷰인지 복사인지 애매한 객체에 대입이 발생하면, Pandas는 “원본에 쓰려는 건지, 사본에 쓰려는 건지” 확신하지 못해 경고를 냅니다.
이 글에서는 경고를 무시/비활성화하지 않고, 코드 구조를 바꿔서 근본적으로 불확실성을 제거하는 7가지 방법을 예제와 함께 정리합니다. (성격은 다르지만, ‘경고를 숨기지 말고 원인을 추적/제거한다’는 관점은 Chrome Forced reflow 경고 원인·해결 7단계와도 통합니다.)
SettingWithCopyWarning이 나는 대표 패턴
가장 흔한 재현 코드는 아래입니다.
import pandas as pd
df = pd.DataFrame({
"team": ["A", "A", "B"],
"score": [10, 20, 30]
})
sub = df[df["team"] == "A"] # 필터 결과가 view/copy 애매
sub["score"] = sub["score"] + 1 # SettingWithCopyWarning 가능
위 코드는 의도상 team == "A"인 행의 score를 올리고 싶지만, 실제로는 sub가 복사본이면 원본 df는 바뀌지 않습니다. Pandas는 이 불확실성을 경고로 알려줍니다.
1) 체인 인덱싱(chain indexing) 제거: 한 번에 .loc로 끝내기
가장 강력하고 범용적인 해결책입니다. “선택(행/열) + 대입”을 한 문장으로 합치면 경고가 사라지고 의도도 명확해집니다.
나쁜 예(체인 인덱싱)
sub = df[df["team"] == "A"]
sub["score"] = sub["score"] + 1
좋은 예(단일 .loc)
mask = df["team"] == "A"
df.loc[mask, "score"] = df.loc[mask, "score"] + 1
포인트
df.loc[mask, "score"]는 “원본 df의 해당 위치”를 명시합니다.- 중간 객체(
sub)를 만들지 않으므로 view/copy 애매함이 없어집니다.
2) 슬라이스/필터 결과를 ‘독립 객체’로 쓸 거면 즉시 .copy()로 확정
원본을 수정할 의도가 전혀 없고, 필터 결과를 별도 데이터셋으로 다룰 거라면 처음부터 복사본임을 선언하세요.
sub = df.loc[df["team"] == "A"].copy()
sub["score"] = sub["score"] + 1 # 경고 없이 안전
언제 이게 정답인가?
sub를 이후 여러 단계 변환에 사용하고, 원본df와 생명주기를 분리하고 싶을 때- 함수가
df를 받아 가공한 새 DataFrame을 반환해야 할 때
> 팁: .copy(deep=True)/deep=False 논쟁이 있지만, SettingWithCopyWarning의 관점에서는 “복사본임을 명시”하는 것 자체가 중요합니다.
3) df[...] = ... 대신 assign() + pipe()로 ‘대입 없는 파이프라인’ 만들기
경고는 “애매한 객체에 대한 대입”에서 자주 발생합니다. 함수형 스타일로 대입을 최소화하면 경고가 줄고, 테스트도 쉬워집니다.
import numpy as np
result = (
df
.assign(
score_plus=lambda d: np.where(d["team"].eq("A"), d["score"] + 1, d["score"])
)
)
기존 컬럼을 직접 덮어쓰고 싶다면:
result = (
df
.assign(
score=lambda d: np.where(d["team"].eq("A"), d["score"] + 1, d["score"])
)
)
포인트
assign은 새 DataFrame을 반환하므로, 중간 슬라이스에 대한 불확실한 대입이 사라집니다.- 복잡한 전처리 파이프라인에서 특히 유용합니다.
4) query()/eval()로 필터·계산을 표현하고, 최종 대입은 .loc로 고정
query()는 가독성이 좋지만, df.query(...)["col"] = ... 같은 체인 대입을 하면 다시 위험해집니다. 필터는 query, 대입은 loc로 분리하세요.
mask = df.eval('team == "A"')
df.loc[mask, "score"] = df.loc[mask, "score"] + 1
또는 query로 인덱스를 얻어 대입:
idx = df.query('team == "A"').index
df.loc[idx, "score"] = df.loc[idx, "score"] + 1
포인트
query결과에 바로 대입하지 말고, 인덱스/마스크를 추출해 원본에.loc로 대입합니다.
5) 정렬/중복제거/리샘플 후 ‘원본처럼 보이는 중간 결과’에 대입하지 않기
다음 같은 코드도 현장에서 자주 경고를 만듭니다.
sub = df.sort_values("score")
sub[sub["team"] == "A"]["score"] = 0 # 위험: 체인 + 중간객체
해결은 동일합니다. “최종 대입은 원본에 .loc 한 번” 원칙을 지키세요.
mask = df["team"].eq("A")
df.loc[mask, "score"] = 0
만약 정렬된 결과를 기준으로 일부만 수정해야 한다면, 정렬된 인덱스를 이용해 원본을 찍습니다.
sorted_idx = df.sort_values("score").index
# 예: 점수가 낮은 순으로 1개만 team A라면 0으로
low_idx = df.loc[sorted_idx].query('team == "A"').head(1).index
df.loc[low_idx, "score"] = 0
포인트
sort_values,drop_duplicates,reset_index등은 “원본처럼 보이는” 새 객체를 만들기 쉬워 대입 실수가 잦습니다.
6) 그룹 연산은 transform/agg를 활용하고, 결과는 정렬된 인덱스로 병합/대입
그룹별 계산 후 일부 컬럼을 업데이트하려다 경고가 나는 경우가 많습니다.
안전한 패턴: transform으로 같은 길이 결과 만들기
# 팀별 평균 점수 대비 편차 컬럼 추가
result = df.assign(
score_centered=df["score"] - df.groupby("team")["score"].transform("mean")
)
업데이트가 필요하면 .loc로 명시
mask = df["team"].eq("A")
df.loc[mask, "score"] = (
df.loc[mask, "score"] - df.groupby("team")["score"].transform("mean").loc[mask]
)
포인트
groupby().apply()는 반환 형태가 유동적이라 인덱스 정합성이 깨지기 쉽습니다.- 가능하면
transform으로 “원본과 동일 길이”를 유지하면 대입이 단순해집니다.
7) 경고를 ‘끄지 말고’, 테스트/검증으로 원본 반영 여부를 확인하는 습관 만들기
pd.options.mode.chained_assignment = None처럼 경고를 끄는 방법이 인터넷에 많이 돌아다니지만, 이는 근본 해결이 아닙니다. 오히려 “원본이 안 바뀌었는데도 조용히 넘어가는” 상황을 만들 수 있습니다.
대신 아래처럼 원본이 실제로 수정됐는지를 짧게 검증하는 습관이 좋습니다.
import pandas as pd
before = df.copy()
mask = df["team"].eq("A")
df.loc[mask, "score"] += 1
# 검증: team A의 score만 바뀌었는지 확인
changed = df["score"].ne(before["score"])
assert changed.equals(mask), "의도치 않은 행이 변경되었거나 변경이 누락됨"
데이터 파이프라인이 커질수록 “어디서 원본이 바뀌었는지/안 바뀌었는지” 추적이 어려워집니다. 경고를 숨기는 대신, 이런 검증을 통해 문제를 조기에 잡는 편이 운영 비용이 훨씬 낮습니다. 대규모 장애에서 원인을 추적하는 관점은 PostgreSQL autovacuum 지연으로 팽창·슬로우쿼리 잡기 같은 글에서 다루는 ‘관측/검증’의 중요성과도 연결됩니다.
실전 체크리스트: 경고를 없애는 게 아니라 ‘불확실성’을 없애라
아래 7가지만 기억하면 SettingWithCopyWarning은 대부분 사라집니다.
- 체인 인덱싱 금지: 선택+대입은 한 번에
.loc[rows, cols] = ... - 슬라이스를 독립적으로 쓸 거면 즉시
.copy() - 가능하면
assign()로 새 DF 생성(대입 최소화) query/eval은 필터 표현에만, 대입은.loc로- 정렬/중복제거/리셋인덱스 후 중간 객체에 대입하지 말고 원본 인덱스로 대입
- 그룹 연산은
transform우선, 인덱스 정합성 유지 - 경고를 끄지 말고 assert로 원본 반영 검증
마무리
SettingWithCopyWarning은 귀찮은 잡음이 아니라, Pandas가 “지금 코드가 데이터 무결성을 깨뜨릴 수 있다”고 알려주는 신호입니다. 해결의 본질은 .loc 자체가 아니라, 원본에 대한 쓰기(write)가 어떤 객체에 대해 일어나는지 명확히 만드는 것입니다.
전처리 코드가 길어질수록 “중간 결과를 변수에 담아 여기저기 수정”하는 스타일은 위험해집니다. 위 7가지 패턴(특히 .loc 단일 대입, .copy() 선언, assign 기반 파이프라인)을 팀 규칙으로 정하면 SettingWithCopyWarning은 물론, 데이터 품질 이슈까지 함께 줄어듭니다.