- Published on
Pandas SettingWithCopyWarning 완전 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
Pandas를 쓰다 보면 한 번쯤은 반드시 마주치는 경고가 있습니다. 바로 SettingWithCopyWarning입니다. 이 경고는 “지금 네가 수정한 게 원본(DataFrame)에 반영되는지 보장할 수 없다”는 의미로, 단순한 소음이 아니라 데이터 정합성과 직결됩니다. 특히 전처리 파이프라인에서 이 경고를 무시하면, 일부 행만 업데이트되거나 업데이트가 아예 누락되는데도 눈치채기 어렵습니다.
이 글에서는 (1) 경고가 발생하는 구조적 이유(뷰 vs 복사), (2) 재현 가능한 대표 케이스, (3) 실무에서 가장 안전한 해결 패턴(.loc, .copy(), .assign, .pipe), (4) “경고를 끄는” 안티패턴까지 한 번에 정리합니다.
SettingWithCopyWarning의 본질: View vs Copy
Pandas는 성능을 위해 일부 연산에서 원본 데이터를 그대로 바라보는 뷰(view) 를 만들기도 하고, 어떤 경우에는 완전히 새로운 복사본(copy) 을 만들기도 합니다. 문제는 많은 인덱싱/슬라이싱이 “항상 뷰” 또는 “항상 복사본”으로 딱 떨어지지 않고, 내부 최적화/상황에 따라 달라질 수 있다는 점입니다.
SettingWithCopyWarning은 이런 모호한 객체에 대해 값을 설정(set)하려 할 때 발생합니다.
- 1단계:
df[...]같은 연산으로 부분 집합을 만든다. - 2단계: 그 결과에 다시
[...] = ...형태로 체인 할당(chained assignment) 을 한다. - 결과: Pandas가 “이게 원본에 반영되는지 확신 못하겠다”고 경고한다.
핵심은 간단합니다.
- 체인 할당을 피하고
- 원본에 쓰려면
.loc로 한 번에 지정하거나 - 부분 집합을 독립적으로 쓸 거면
.copy()로 의도를 명확히 하자
가장 흔한 재현 케이스 3가지
1) 필터링 후 컬럼 수정
import pandas as pd
df = pd.DataFrame({
"user": ["a", "b", "c"],
"age": [17, 20, 15],
"score": [80, 90, 70]
})
teen = df[df["age"] < 18]
teen["grade"] = "teen" # SettingWithCopyWarning 가능
여기서 teen이 뷰인지 복사본인지 모호합니다. 의도가 “원본 df에도 grade를 채우고 싶다”인지, “teen만 따로 쓰고 싶다”인지도 코드만 보면 불명확합니다.
2) 컬럼 선택 후 다시 필터링 후 수정
sub = df[["age", "score"]]
sub[sub["age"] < 18]["score"] = 0 # 최악의 체인 할당
이 케이스는 거의 확정적으로 경고를 유발하며, 실제로 값이 반영되지 않는 경우도 많습니다.
3) inplace 연산 + 부분 집합
teen = df[df["age"] < 18]
teen["score"].fillna(0, inplace=True) # 경고 + 기대와 다르게 동작 가능
inplace=True는 특히 “어디에 inplace 되는가?”가 불명확해질 수 있어, 부분 집합과 섞이면 위험합니다.
완전 해결 1: 원본을 수정하려면 .loc로 한 번에
원본 df를 수정하는 것이 목적이라면, 조건(행)과 대상 컬럼을 .loc로 동시에 지정하세요.
mask = df["age"] < 18
df.loc[mask, "grade"] = "teen"
여기서 장점은 명확합니다.
df라는 “원본”에 쓰는 것이 분명- 체인 할당이 아님
- 경고가 사라짐
- 기대한 대로 동작
여러 컬럼을 동시에 업데이트할 때도 .loc가 가장 안정적입니다.
mask = df["age"] < 18
df.loc[mask, ["grade", "score"]] = ["teen", 0]
값이 행마다 달라지는 경우는 .loc + 벡터 연산/np.where 조합이 좋습니다.
import numpy as np
mask = df["age"] < 18
df.loc[:, "score_adj"] = np.where(mask, df["score"] * 0.9, df["score"])
완전 해결 2: 부분 집합을 독립적으로 쓸 거면 .copy()
필터링 결과를 별도 데이터셋으로 다루고 싶다면, 애초에 복사본임을 명시하세요.
teen = df.loc[df["age"] < 18].copy()
teen["grade"] = "teen" # 안전
이 패턴의 의미는 “teen은 원본과 독립적인 데이터프레임”입니다. 이후 어떤 변형을 해도 원본에 영향을 주지 않습니다.
실무 팁:
- 분석/리포팅용으로 subset을 만들 때:
.copy()권장 - 전처리 파이프라인에서 원본을 계속 변형해야 할 때:
.loc권장
완전 해결 3: .assign()으로 함수형 스타일 유지
assign은 새 컬럼을 만들거나 수정한 결과를 새 DataFrame으로 반환합니다. 체인 할당을 피하면서도 메서드 체이닝을 유지할 수 있어 전처리 코드가 깔끔해집니다.
teen = (
df
.loc[df["age"] < 18]
.assign(grade="teen")
)
조건부 할당도 가능합니다.
import numpy as np
out = df.assign(
grade=lambda x: np.where(x["age"] < 18, "teen", "adult")
)
이 방식은 “원본 df는 그대로 두고, out을 새로 만든다”는 의도가 명확합니다.
완전 해결 4: .pipe()로 전처리 단계를 분리(경고 방지에 도움)
복잡한 전처리에서 체인 할당이 숨어 들어가기 쉽습니다. 단계별 함수를 만들고 .pipe()로 연결하면, 각 단계에서 원본/복사본 의도를 명확히 하기 쉬워 경고를 구조적으로 줄일 수 있습니다.
def add_grade(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df.loc[df["age"] < 18, "grade"] = "teen"
df.loc[df["age"] >= 18, "grade"] = "adult"
return df
def normalize_score(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
df["score_norm"] = df["score"] / df["score"].max()
return df
out = (
df
.pipe(add_grade)
.pipe(normalize_score)
)
절대 비추천: 경고를 끄는 방식
검색하면 아래 같은 해결책이 자주 나오지만, 이는 “문제를 숨기는 것”에 가깝습니다.
pd.options.mode.chained_assignment = None # 비추천
경고가 사라질 뿐, 실제로 원본이 업데이트되지 않는 버그는 그대로 남을 수 있습니다. SettingWithCopyWarning은 귀찮아도 버그 조기 감지 장치로 보는 게 맞습니다.
실무에서 자주 하는 실수와 교정
실수 1) subset을 만든 뒤 원본을 바꿨다고 믿기
subset = df[df["age"] < 18]
subset["score"] = 0
# df의 score도 0이 됐을 거라 착각
교정:
- 원본을 바꾸려면
.loc로 원본에 직접 쓰기
df.loc[df["age"] < 18, "score"] = 0
실수 2) inplace=True 남용
inplace=True는 특히 “대상 객체가 원본인지 임시 객체인지”가 혼동될 때 위험합니다. Pandas 커뮤니티에서도 inplace는 점점 덜 권장되는 추세입니다.
교정:
teen = df.loc[df["age"] < 18].copy()
teen["score"] = teen["score"].fillna(0)
실수 3) 체인 인덱싱이 긴 코드에 숨어 있음
(df[["age", "score"]]
.loc[df["age"] < 18])["score"] = 0
교정: “행 조건 + 열 선택”을 한 번의 .loc로 끝내기
mask = df["age"] < 18
df.loc[mask, "score"] = 0
디버깅 체크리스트: 경고가 뜨면 이렇게 확인
내 의도는 무엇인가?
- 원본 df를 수정? →
.loc로 원본에 직접 할당 - subset을 독립적으로 가공? →
.copy()
- 원본 df를 수정? →
체인 할당이 있는가?
df[...][...] = ...형태면 거의 확정
중간 결과를 변수로 받았는가?
- 받았다면
.copy()여부를 명시
- 받았다면
함수 경계에서 df를 변형하는가?
- 함수 내부에서
df = df.copy()후.loc사용 권장
- 함수 내부에서
이런 “경고 원인 → 재현 → 수정” 루프는 다른 장애 대응에도 유사하게 적용됩니다. 예를 들어 API 호출에서 429가 나올 때도, 원인을 숨기기보다 헤더 기반으로 재시도 설계를 명확히 하는 것이 정석입니다: OpenAI 429와 Rate Limit 헤더로 재시도 설계
패턴별 정답 요약
- 원본 수정:
df.loc[rows, cols] = value - subset 독립 가공:
subset = df.loc[rows].copy()후subset[...] = ... - 함수형/체이닝:
.assign(...),.pipe(...) - 피해야 할 것:
df[...][...] = ...,pd.options.mode.chained_assignment = None, subset에 대한inplace=True
결론
SettingWithCopyWarning은 Pandas가 “너 지금 위험한 할당을 하고 있다”고 알려주는 중요한 신호입니다. 완전 해결의 핵심은 두 가지로 정리됩니다.
- 원본을 바꿀 거면
.loc로 한 번에 - 부분 집합을 쓸 거면
.copy()로 의도를 고정
이 두 규칙만 지켜도 경고는 대부분 사라지고, 더 중요한 것은 전처리 결과가 “항상 같은 방식으로” 재현 가능해진다는 점입니다. 데이터 파이프라인에서 재현성은 곧 신뢰성입니다.