- Published on
pandas SettingWithCopyWarning 완전 해결 - copy·loc
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
pandas를 쓰다 보면 한 번쯤은 아래 경고를 만나게 됩니다.
> SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
처음엔 “경고니까 무시해도 되나?” 싶지만, 이 경고는 단순 스타일 문제가 아니라 내가 수정했다고 믿는 값이 실제 원본 DataFrame에 반영되지 않을 수 있음을 의미합니다. 즉, 데이터 정합성 버그로 이어질 수 있습니다.
이 글에서는 SettingWithCopyWarning의 발생 원리를 짧고 정확하게 짚고, 실무에서 copy()와 loc를 기준으로 “완전히” 해결하는 패턴을 제시합니다. 경고를 끄는 방식(pd.options.mode.chained_assignment = None)은 마지막에 왜 지양해야 하는지도 함께 다룹니다.
SettingWithCopyWarning이 생기는 진짜 이유
핵심은 pandas의 인덱싱 결과가 상황에 따라:
- 원본을 바라보는 view(뷰) 일 수도 있고
- 독립된 copy(복사본) 일 수도 있다는 점입니다.
문제는 사용자가 df[...]로 잘라낸 결과에 다시 [...]로 값을 대입하는 chained indexing(연쇄 인덱싱) 을 하면, pandas가 “이게 원본에 반영되는 뷰인지, 복사본인지” 확신할 수 없다는 것입니다.
대표적인 위험 코드(연쇄 인덱싱)
import pandas as pd
df = pd.DataFrame({
"user": ["a", "b", "c", "d"],
"age": [17, 20, 15, 33],
"score": [80, 90, 70, 88]
})
# 경고가 뜰 수 있고, 실제로 원본 df에 반영이 안 될 수도 있음
teen = df[df["age"] < 19]
teen["grade"] = "teen" # SettingWithCopyWarning 가능
이 코드는 teen이 뷰인지 복사본인지 애매합니다. 어떤 경우엔 운 좋게 동작하는 것처럼 보이지만, 버전/옵션/데이터 배치에 따라 결과가 달라질 수 있습니다.
이 경고는 인프라에서 “간헐적 타임아웃”처럼 재현이 들쭉날쭉해서 더 위험합니다. (타임아웃 재현과 해결을 체계적으로 접근하는 방식은 OpenAI Responses API 504 Timeout 재현·해결 같은 글의 접근법과도 결이 비슷합니다.)
해결 원칙 2가지: copy() 또는 loc
SettingWithCopyWarning을 제대로 없애는 방법은 사실 단순합니다.
- 새 DataFrame으로 독립시켜서 작업하려면
copy() - 원본을 수정하려면
loc로 “한 번에” 선택+대입
이 두 가지 중 하나로 코드를 정리하면 경고는 사라지고, 의도도 명확해집니다.
패턴 1) 잘라낸 뒤 별도 테이블로 쓸 거면 .copy()
필터링 결과를 “리포트용”, “모델 입력용”처럼 원본과 독립된 데이터셋으로 다룰 거라면, 처음부터 복사본임을 명시하세요.
teen = df.loc[df["age"] < 19].copy()
teen["grade"] = "teen" # 안전: teen은 독립 copy
여기서 포인트는:
df[...]대신df.loc[조건]을 쓰는 습관- 그리고
.copy()로 의도를 확정
언제 copy()가 특히 필요한가
- 필터링 결과를 여러 단계 변환(파생 컬럼 추가, 타입 변환, 결측 처리 등)할 때
- 함수 내부에서 부분집합을 만든 다음 반환할 때(호출자가 원본을 기대하지 않도록)
- 파이프라인에서 중간 산출물을 캐싱/저장할 때
복사 비용이 걱정된다면, “정말 원본과 분리되어야 하는가?”를 먼저 판단하고, 분리되어야 한다면 copy 비용을 지불하는 게 버그 비용보다 훨씬 저렴합니다.
패턴 2) 원본 df를 수정하려면 loc[rows, cols] = value
원본 DataFrame에 값을 반영하려면, 선택과 대입을 한 문장으로 묶어야 합니다.
mask = df["age"] < 19
df.loc[mask, "grade"] = "teen" # 안전: 원본 df를 명확히 수정
이 패턴은 pandas가 가장 확실하게 이해할 수 있는 형태입니다.
여러 컬럼을 동시에 세팅
mask = df["age"] < 19
df.loc[mask, ["grade", "is_minor"]] = ["teen", True]
조건에 따라 값 다르게 넣기: np.where
import numpy as np
df["age_group"] = np.where(df["age"] < 19, "teen", "adult")
이 방식은 연쇄 인덱싱을 만들지 않으므로 경고가 나지 않습니다.
자주 하는 실수: df[cond][col] = ...
아래는 SettingWithCopyWarning “교과서”입니다.
# 나쁜 예
(df[df["age"] < 19])["score"] = 0
이 코드는 “필터링한 결과”에서 다시 "score"를 골라 대입합니다. 즉, 연쇄 인덱싱입니다.
올바른 코드는 다음 중 하나입니다.
# 원본을 수정하고 싶다면
mask = df["age"] < 19
df.loc[mask, "score"] = 0
# 별도 데이터셋을 만들고 싶다면
teen = df.loc[mask].copy()
teen.loc[:, "score"] = 0
.loc[:, col]이 필요한 순간: 부분집합 DataFrame에 컬럼 추가
copy()를 했더라도, “나는 이 DataFrame 전체 행에 대해 이 컬럼을 세팅한다”를 명확히 하고 싶으면 .loc[:, ...]를 쓰는 게 좋습니다.
teen = df.loc[df["age"] < 19].copy()
teen.loc[:, "score_scaled"] = teen["score"] / 100
이 코드는 가독성 측면에서도 “전체 행에 대해 score_scaled를 만든다”가 분명합니다.
함수로 감쌀 때의 안전한 템플릿
실무에서는 전처리를 함수로 빼는 경우가 많습니다. 이때 SettingWithCopyWarning은 더 자주 등장합니다. 아래 템플릿을 권장합니다.
(A) 원본을 절대 건드리지 않는 함수
def build_teen_report(df: pd.DataFrame) -> pd.DataFrame:
out = df.loc[df["age"] < 19].copy()
out.loc[:, "grade"] = "teen"
out.loc[:, "score_scaled"] = out["score"] / 100
return out
(B) 원본을 수정하는 함수(명시적으로)
def tag_minors_inplace(df: pd.DataFrame) -> None:
mask = df["age"] < 19
df.loc[mask, "is_minor"] = True
df.loc[~mask, "is_minor"] = False
이렇게 “복사본 반환” 또는 “inplace 수정”을 API 레벨에서 분리하면, 팀 단위로도 실수가 줄어듭니다.
.at, .iat로 단일 셀 수정하기
단일 셀을 수정할 때도 연쇄 인덱싱을 피해야 합니다.
# 나쁜 예(상황에 따라 경고/오동작 소지)
df[df["user"] == "a"]["score"] = 100
# 좋은 예: 조건으로 특정 행 인덱스를 찾고 at 사용
idx = df.index[df["user"] == "a"][0]
df.at[idx, "score"] = 100
.at: 라벨 기반 단일 값.iat: 정수 위치 기반 단일 값
성능도 좋고 의도도 명확합니다.
경고를 끄는 방식이 위험한 이유
아래처럼 경고를 전역으로 끄면 “조용히 실패하는 데이터 수정”이 발생할 수 있습니다.
import pandas as pd
pd.options.mode.chained_assignment = None # 비추천
SettingWithCopyWarning은 귀찮은 경고가 아니라, 데이터 파이프라인에서 가장 치명적인 종류의 버그(조용한 불일치) 를 막아주는 안전장치입니다.
인프라에서 재시도/백오프를 설계할 때도 “에러를 숨기지 말고 관측 가능하게 만들라”는 원칙이 중요하듯, 데이터 처리에서도 경고를 끄기보다 원인을 제거하는 게 정석입니다. (재시도 설계 관점은 Claude API 529 Overloaded 재시도·백오프 설계 같은 글이 참고가 됩니다.)
디버깅 팁: 내가 가진 게 view인지 copy인지 의심될 때
pandas 내부 구현상 100% 확정적으로 “이건 view다”를 일반 API로 판정하기는 어렵지만, 다음과 같은 신호가 있으면 위험 구간입니다.
df2 = df[조건]또는df2 = df[[col1, col2]]같은 슬라이싱 직후 수정df2[col] = ...형태로 대입.sort_values(),.dropna(),.rename()등을 중간에 섞고 다시 대입
가장 안전한 결론은:
- 부분집합을 새로 써야 하면
.copy() - 원본을 바꿔야 하면
df.loc[mask, col] = ...
실전 예제: 파생 컬럼 + 조건부 업데이트를 한 번에 정리
상황: 로그 데이터에서 status == "ERROR" 인 행의 severity를 올리고, 별도 리포트용 DataFrame도 만든다.
import pandas as pd
import numpy as np
logs = pd.DataFrame({
"service": ["api", "api", "worker", "api"],
"status": ["OK", "ERROR", "OK", "ERROR"],
"latency_ms": [120, 900, 200, 1500]
})
# 1) 원본 수정: ERROR면 severity=high, 아니면 low
mask_err = logs["status"] == "ERROR"
logs.loc[:, "severity"] = "low"
logs.loc[mask_err, "severity"] = "high"
# 2) 리포트용 부분집합: ERROR만 뽑아 별도 테이블로 가공
err_report = logs.loc[mask_err, ["service", "latency_ms", "severity"]].copy()
err_report.loc[:, "latency_bucket"] = np.where(err_report["latency_ms"] >= 1000, "p99+", "p95")
print(logs)
print(err_report)
이 예제는 “원본 수정”과 “부분집합 가공”을 명확히 분리해 SettingWithCopyWarning 여지를 제거합니다.
체크리스트: SettingWithCopyWarning을 ‘완전’ 제거하는 규칙
df[cond][col] = ...형태를 발견하면 즉시 리팩터링한다.- 원본 수정은 항상
df.loc[rows, cols] = value로 한다. - 부분집합을 독립적으로 다룰 땐
df.loc[rows, cols].copy()로 시작한다. - 함수 API에서 “inplace 수정”과 “복사본 반환”을 섞지 않는다.
- 경고를 끄지 말고, 경고가 가리키는 모호성을 코드에서 제거한다.
결론
SettingWithCopyWarning은 pandas가 귀찮게 구는 게 아니라, 뷰/복사본 모호성으로 인해 데이터 수정이 실패할 수 있음을 알려주는 중요한 신호입니다. 해결책은 복잡하지 않습니다.
- 독립 데이터셋이면
.copy() - 원본 수정이면
.loc[...] = ...
이 두 가지 원칙만 팀 코딩 규칙으로 고정해도, 데이터 전처리/피처 엔지니어링/리포팅 단계에서 발생하는 “조용한 데이터 오류”를 상당 부분 제거할 수 있습니다.