- Published on
Pandas SettingWithCopyWarning 완전 정복 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 데이터 전처리 파이프라인을 연결하다 보면, 판다스에서 가장 자주 마주치는 경고 중 하나가 SettingWithCopyWarning입니다. 이 경고는 단순히 “경고가 뜨니 없애자” 수준이 아니라, 내가 수정했다고 믿는 값이 실제 원본에 반영되지 않을 수 있다는 신호입니다. 즉, 조용히 데이터가 틀어지는 버그로 이어질 수 있습니다.
이 글에서는 SettingWithCopyWarning가 왜 생기는지(뷰와 카피, 체인 인덱싱), 어떤 코드가 위험한지, 그리고 실무에서 가장 안전한 해결 패턴(.loc, .assign, .copy)을 코드로 정리합니다.
SettingWithCopyWarning이 의미하는 것
판다스는 성능을 위해 어떤 연산에서는 원본 데이터의 뷰(view) 를 반환하고, 어떤 연산에서는 복사본(copy) 을 반환합니다. 문제는 사용자가 보기엔 둘 다 DataFrame처럼 보이기 때문에, 아래처럼 작성했을 때:
- 내가 수정한 대상이 원본의 뷰인지
- 아니면 복사본인지
판다스가 확신할 수 없는 경우가 생깁니다. 그때 판다스는 이렇게 말합니다.
- “지금 수정이 원본에 반영될지 보장할 수 없다”
이게 바로 SettingWithCopyWarning입니다.
가장 흔한 원인: 체인 인덱싱(chain indexing)
체인 인덱싱은 인덱싱을 두 번에 걸쳐 연속으로 수행하는 패턴입니다. 대표적으로 다음 형태가 위험합니다.
df[condition][col] = valuedf[col][mask] = value
경고를 재현해보기
import pandas as pd
pd.set_option("mode.chained_assignment", "warn")
df = pd.DataFrame({
"name": ["a", "b", "c", "d"],
"age": [10, 20, 30, 40],
"city": ["seoul", "seoul", "busan", "busan"],
})
# 체인 인덱싱: df[mask]가 뷰인지 카피인지 불명확
mask = df["city"] == "seoul"
df[mask]["age"] = 999
위 코드는 SettingWithCopyWarning을 유발할 가능성이 높고, 더 심각한 건 실제로 값이 바뀌지 않을 수도 있다는 점입니다.
정답 패턴 1: .loc로 한 번에 인덱싱하고 대입하기
판다스에서 조건 기반 대입은 원칙적으로 .loc를 쓰는 것이 가장 안전합니다.
mask = df["city"] == "seoul"
df.loc[mask, "age"] = 999
이 패턴은 다음 장점이 있습니다.
- 인덱싱과 대입이 한 번에 일어나서 모호함이 사라짐
- 원본
df에 확실히 반영됨 - 경고가 아니라 의도가 명확한 코드가 됨
여러 컬럼을 동시에 수정
mask = df["city"] == "busan"
df.loc[mask, ["age", "name"]] = [111, "x"]
단, 여러 컬럼에 서로 다른 값을 넣을 때는 브로드캐스팅 규칙을 고려해야 합니다.
정답 패턴 2: 필터링 결과를 별도 데이터로 쓸 거면 .copy()
필터링한 결과를 “부분집합 데이터프레임”으로 따로 다룰 때는, 원본과 분리된 복사본임을 명시하는 게 안전합니다.
seoul_df = df[df["city"] == "seoul"].copy()
seoul_df["age"] = seoul_df["age"] + 1
이렇게 하면:
seoul_df는 원본과 독립- 수정이 원본에 반영되지 않는 것이 의도된 동작
- 경고가 사라지고 코드의 의미가 명확해짐
언제 .copy()가 특히 유용한가
- 전처리 파이프라인에서 중간 산출물을 분리해 저장할 때
- 분석/리포트용으로 일부 데이터만 떼어내 가공할 때
- 원본을 절대 건드리면 안 되는 상황(재현성, 감사 로그 등)
정답 패턴 3: .assign()으로 함수형 스타일로 안전하게
assign은 새 컬럼을 추가하거나 기존 컬럼을 대체하면서 새 데이터프레임을 반환합니다. 체인 인덱싱을 줄이고, 파이프라인을 깔끔하게 만들 때 좋습니다.
result = (
df
.loc[df["city"] == "seoul"]
.assign(age=lambda x: x["age"] + 100)
)
여기서 중요한 점:
.loc[...]로 먼저 안전하게 부분집합을 만들고.assign(...)으로 파생 컬럼을 만들며- 결과를
result에 담아 원본과 분리
“경고만 끄면 안 되나요?”: 근본 원인을 숨기는 옵션들
판다스에는 체인 대입 경고 동작을 바꾸는 옵션이 있습니다.
pd.set_option("mode.chained_assignment", None) # 경고 숨김
# 또는
pd.set_option("mode.chained_assignment", "raise") # 예외로 승격
실무 권장
- 로컬 개발이나 CI에서는
raise로 두고, 위험한 코드를 초기에 잡는 것을 추천합니다. None으로 경고를 숨기는 것은, 데이터가 조용히 틀어질 가능성을 키웁니다.
이 접근은 장애를 “조기에 크게 터뜨려서” 잡는 방식인데, 운영 이슈를 빠르게 좁혀가는 관점에서 비슷한 철학을 다른 영역에서도 자주 씁니다. 예를 들어 API 요청이 실패할 때 원인을 빠르게 특정하는 방식은 OpenAI Responses API 400 invalid_request_error 원인과 해결 같은 글의 문제 접근과도 닮아 있습니다.
자주 헷갈리는 케이스 1: 컬럼 선택 후 마스크 대입
다음 코드는 언뜻 정상처럼 보이지만, 체인 인덱싱이 섞이기 쉬운 형태입니다.
# 위험할 수 있음
s = df["age"]
s[df["city"] == "seoul"] = 0
안전하게 쓰려면 원본에 대해 .loc로 직접 대입하세요.
df.loc[df["city"] == "seoul", "age"] = 0
자주 헷갈리는 케이스 2: 정렬/슬라이싱 이후 대입
정렬이나 슬라이싱 결과가 뷰인지 복사본인지 애매한 경우가 있어, 아래처럼 작성하면 경고가 발생할 수 있습니다.
sorted_df = df.sort_values("age")
# 이후 대입이 원본/복사 어디에 반영될지 모호해질 수 있음
sorted_df["age"] = sorted_df["age"] + 1
이 경우는 의도에 따라 선택합니다.
- 정렬된 결과를 독립적으로 쓰려면
sorted_df = df.sort_values("age").copy() - 원본을 수정하려면 정렬 결과를 기준으로 다시 원본에
.loc로 반영하는 로직을 설계
자주 헷갈리는 케이스 3: 함수에서 부분집합을 받아 수정하기
다음처럼 함수가 DataFrame을 받아 내부에서 필터링 후 수정하면, 호출자는 원본이 바뀌었다고 기대할 수 있습니다.
def normalize_seoul_age(data):
sub = data[data["city"] == "seoul"]
sub["age"] = sub["age"] / sub["age"].max()
return sub
이 코드는 경고가 날 수 있고, 더 중요한 건 “원본을 수정하는 함수인지/아닌지”가 불명확합니다.
안전한 설계 1: 원본을 수정하는 함수라면 .loc로 명시
def normalize_seoul_age_inplace(data):
mask = data["city"] == "seoul"
data.loc[mask, "age"] = data.loc[mask, "age"] / data.loc[mask, "age"].max()
return data
안전한 설계 2: 복사본을 반환하는 함수라면 .copy()로 명시
def normalize_seoul_age_copy(data):
sub = data.loc[data["city"] == "seoul"].copy()
sub["age"] = sub["age"] / sub["age"].max()
return sub
디버깅 팁: “원본이 실제로 바뀌었는지”를 검증하라
SettingWithCopyWarning을 만났을 때 가장 위험한 지점은 “경고는 떴지만 결과가 얼추 맞는 것 같아 보이는” 상황입니다. 반드시 원본이 바뀌었는지 확인하세요.
before = df.copy()
mask = df["city"] == "seoul"
df.loc[mask, "age"] = 999
changed = (before["age"] != df["age"]).sum()
print("changed rows:", changed)
또는 테스트에서 특정 행의 값이 기대대로 바뀌었는지 assert를 걸어두면, 데이터 전처리 코드의 회귀(regression)를 빠르게 잡을 수 있습니다.
실무 체크리스트: SettingWithCopyWarning을 없애는 습관
1) 조건 대입은 무조건 .loc[rows, cols] = ...
df[mask][col] = ...금지df.loc[mask, col] = ...사용
2) 부분집합을 새 데이터로 쓸 땐 .copy()
- 필터링 결과를 리턴하거나 저장할 계획이면
.copy()를 기본값으로
3) 파이프라인에는 .assign()과 .pipe()를 적극 활용
- 중간 단계에서 원본/부분집합의 경계를 명확히
4) 개발 환경에서는 mode.chained_assignment를 raise로 고려
- 경고를 예외로 바꿔 조기 탐지
이런 “애매함을 없애는” 습관은 프론트엔드나 인프라에서도 동일하게 중요합니다. 예를 들어 캐시로 인해 오래된 UI가 뜨는 문제를 다룰 때도, 원인과 경계를 명확히 하지 않으면 수정이 재발합니다. 관련해서는 Next.js 15 RSC 캐시로 stale UI 뜰 때 해결법도 같은 문제 해결 태도를 참고할 만합니다.
결론
SettingWithCopyWarning은 판다스가 까다로워서가 아니라, 내 코드가 원본을 수정하는지 복사본을 수정하는지 모호하기 때문에 뜨는 경고입니다. 해결은 간단합니다.
- 원본을 수정하려면
df.loc[mask, col] = value - 부분집합을 독립적으로 다루려면
df[mask].copy() - 파이프라인을 깔끔하게 하려면
.assign()
경고를 숨기는 대신, 데이터 변경의 의도를 코드에 드러내면 전처리 코드의 신뢰도가 올라가고, 나중에 분석 결과가 틀어지는 사고를 크게 줄일 수 있습니다.