- 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
많은 글에서 “그냥 무시해도 된다”, “옵션으로 끄면 된다” 정도로 끝나지만, 이 경고는 데이터가 실제로 수정되지 않았는데도 수정된 것처럼 보이는 치명적인 버그로 이어질 수 있습니다. 특히 전처리 파이프라인이 길거나, 중간 결과를 재사용하는 분석 코드에서 한 번 섞이면 재현도 어렵습니다.
이 글에서는 SettingWithCopyWarning이 왜 생기는지(뷰 vs 복사), 어떤 패턴이 위험한지, 그리고 항상 안전하게 해결하는 표준 패턴을 코드로 정리합니다.
SettingWithCopyWarning이 의미하는 것
핵심은 pandas가 어떤 연산의 결과를 원본을 바라보는 뷰(view) 로 만들지, 아니면 새 객체(복사본) 로 만들지 케이스마다 달라진다는 점입니다.
- 뷰(view): 원본과 메모리를 공유할 수 있어, 수정이 원본에 반영될 수도 있음
- 복사(copy): 원본과 독립적이라, 수정이 원본에 반영되지 않음
문제는 pandas가 내부 최적화/상황에 따라 결과가 뷰인지 복사인지 확실히 보장하지 못하는 경우가 있고, 그 상태에서 “체이닝 인덱싱(chained indexing)”으로 값을 설정하면 사용자가 기대한 대상(원본/슬라이스)과 실제 수정 대상이 불명확해집니다. 그래서 경고를 띄워 “이거 진짜 원본 바뀌는 거 맞아?”라고 알려주는 것입니다.
가장 흔한 원인: Chained Indexing
다음 패턴이 대표적입니다.
import pandas as pd
pd.set_option("mode.chained_assignment", "warn") # 기본 warn
df = pd.DataFrame({
"user": ["a", "b", "c", "d"],
"age": [19, 25, 17, 40],
"score": [10, 20, 30, 40]
})
# 나이 20 이상만 필터링한 뒤 score 수정
adult = df[df["age"] >= 20]
adult["score"] = adult["score"] * 2 # SettingWithCopyWarning 가능
여기서 adult = df[df["age"] >= 20] 결과가 뷰인지 복사인지 애매합니다. 그리고 adult["score"] = ...로 설정하면, 원본 df가 바뀔 수도/안 바뀔 수도 있습니다.
또 다른 흔한 형태:
# 체이닝 인덱싱의 전형
df[df["age"] >= 20]["score"] = 0 # 거의 확실히 위험
이 코드는 “필터링 결과에서 score를 0으로” 같은 의도지만, 실제로는 df[df["age"] >= 20]가 먼저 평가되어 임시 객체가 만들어지고, 그 임시 객체의 컬럼에 값을 넣는 꼴이 되어 원본 df는 안 바뀔 가능성이 큽니다.
완전 해결의 1원칙: 할당은 무조건 .loc로
SettingWithCopyWarning을 근본적으로 피하는 가장 안정적인 규칙은 다음 하나로 요약됩니다.
- 조건 필터링 + 값 할당은 한 줄에서
.loc[row_selector, col_selector] = value로 처리한다.
위 예제를 올바르게 고치면:
mask = df["age"] >= 20
df.loc[mask, "score"] = df.loc[mask, "score"] * 2
이 방식은 “원본 df의 특정 행/열에 값을 설정한다”가 명확해서 경고도 없고, 의도대로 동작합니다.
여러 컬럼을 동시에 수정하기
mask = df["age"] >= 20
df.loc[mask, ["score", "age"]] = df.loc[mask, ["score", "age"]].assign(
score=lambda x: x["score"] * 2,
age=lambda x: x["age"] + 1
)
또는 단순한 경우:
mask = df["age"] >= 20
df.loc[mask, "score"] *= 2
df.loc[mask, "age"] += 1
완전 해결의 2원칙: 슬라이스를 “독립 객체”로 쓸 거면 .copy()
필터링 결과를 별도 데이터프레임으로 만들어 후속 처리(피처 엔지니어링, 리포팅 등)를 하고 싶다면, 처음부터 복사본임을 명시하세요.
adult = df.loc[df["age"] >= 20].copy()
adult["score"] = adult["score"] * 2 # 경고 없음, 원본 df는 유지
여기서 중요한 포인트:
.copy()를 붙이면 “이후 adult 수정은 원본과 무관”이 명확해집니다.- 반대로 원본을 수정하고 싶은 목적이면
.copy()를 쓰면 안 됩니다(수정이 원본에 반영되지 않음).
케이스별 실전 패턴 정리
1) 조건부 값 채우기(라벨링)
# 나이가 20 미만이면 group='minor', 아니면 'adult'
df.loc[df["age"] < 20, "group"] = "minor"
df.loc[df["age"] >= 20, "group"] = "adult"
2) 여러 조건(AND/OR) 결합
mask = (df["age"] >= 20) & (df["score"] >= 20)
df.loc[mask, "flag"] = True
3) 문자열 조건 + 결측치 처리
# user가 'a'로 시작하고 score가 결측이면 0으로
mask = df["user"].str.startswith("a") & df["score"].isna()
df.loc[mask, "score"] = 0
4) groupby 결과를 원본에 반영
groupby 결과를 슬라이스에 바로 넣다가 경고/버그가 나는 경우가 많습니다. 안전한 방법은 transform을 쓰는 것입니다.
# user별 score 평균을 mean_score 컬럼으로 추가
df["mean_score"] = df.groupby("user")["score"].transform("mean")
경고를 “끄는 것”이 왜 위험한가
아래처럼 옵션을 바꿔 경고를 숨길 수는 있습니다.
pd.set_option("mode.chained_assignment", None) # 경고 숨김
하지만 이것은 문제를 해결한 게 아니라 문제가 드러나는 신호를 제거한 것입니다. 특히 다음 상황에서 위험합니다.
- 전처리 함수가 여러 단계로 나뉘어 있고, 중간 산출물을 재사용하는 경우
- 주피터 노트북에서 셀 실행 순서가 뒤섞인 경우
- “일부 행만 수정되어야 하는데 전혀 수정이 안 됐거나, 반대로 원본이 오염된” 경우
SettingWithCopyWarning이 뜬 코드는 대개 “원본을 바꾸려는 건지, 복사본을 바꾸려는 건지” 의도가 불명확합니다. 따라서 해결책은 경고를 끄는 것이 아니라 의도를 코드로 명시(.loc 또는 .copy) 하는 것입니다.
디버깅: 내 코드가 뷰/복사 중 무엇인지 빠르게 확인하기
pandas는 내부적으로 뷰/복사를 100% 일관되게 노출하지 않습니다. 그래도 실무에서 다음 방법이 도움이 됩니다.
1) 의심 구간을 .copy()로 강제해보기
경고가 사라지고 동작이 안정되면, 해당 구간은 “슬라이스를 독립 객체로 쓰는 게 맞는 코드”였을 가능성이 큽니다.
2) 할당을 .loc로 바꿔보기
원본을 수정하려는 의도였다면 .loc로 바꾸는 순간 경고가 사라지고 결과도 일관됩니다.
3) 체이닝 인덱싱을 검색으로 제거
다음 같은 패턴을 코드베이스에서 찾아 제거하세요.
df[...][...] = ...df[...]["col"].something = ...(직접 속성 할당)
자주 하는 질문(실전)
Q1. df_slice = df[cond] 후 df_slice.loc[...] = ...도 경고가 나는데요?
가능합니다. df_slice 자체가 뷰/복사 불명확한 상태라서, .loc를 써도 “원본과의 관계”가 애매하면 경고가 날 수 있습니다.
해결은 둘 중 하나입니다.
- 원본을 바꾸려면: 처음부터 원본 df에
.loc로 할당 - 슬라이스를 독립적으로 쓰려면: 슬라이스 생성 시
.copy()
# 원본 수정 목적
mask = df["age"] >= 20
df.loc[mask, "score"] = 0
# 독립 객체 목적
adult = df.loc[df["age"] >= 20].copy()
adult.loc[:, "score"] = 0
Q2. inplace=True가 SettingWithCopyWarning과 관련 있나요?
직접적인 원인은 아니지만, 슬라이스에 대해 inplace=True를 호출하면 경고/비일관성이 더 잘 드러납니다. 실무에서는 inplace=True를 피하고 대입으로 명시하는 편이 디버깅에 유리합니다.
(중요) “항상 안전한” 체크리스트
다음 체크리스트만 지키면 SettingWithCopyWarning은 사실상 종결됩니다.
- 원본을 수정할 때:
df.loc[rows, cols] = ...만 사용 (체이닝 인덱싱 금지) - 슬라이스를 새 데이터프레임으로 쓸 때: 만들 때
.copy() - 필터링 결과를 변수에 담아 수정해야 한다면: 그 변수는
.copy()인지, 원본을 가리키는지 의도를 명확히 - 경고를 끄지 말고, 경고가 나는 코드를 리팩터링
마무리
SettingWithCopyWarning은 “pandas가 귀찮게 구는 경고”가 아니라, 데이터 파이프라인의 신뢰성을 깨뜨릴 수 있는 잠재 버그 알림입니다. 해결은 어렵지 않습니다.
- 원본 수정은
.loc로 - 슬라이스를 독립적으로 쓸 땐
.copy()로
데이터 분석에서도 인프라 장애를 디버깅하듯, 증상을 숨기지 말고 원인을 제거하는 습관이 결국 시간을 아낍니다. (디버깅 관점은 쿠버네티스 장애 분석 글인 Kubernetes CrashLoopBackOff 원인별 로그·Probe·리소스 디버깅에서도 같은 원리로 통합니다.)