- Published on
Pandas SettingWithCopyWarning 원인·해결 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
Pandas를 쓰다 보면 가장 자주 마주치는 경고 중 하나가 SettingWithCopyWarning입니다. 많은 경우 코드가 “잘 동작하는 것처럼 보이는데” 경고만 뜨기 때문에 무시하기 쉽습니다. 하지만 이 경고는 단순한 소음이 아니라, 내가 수정한 값이 원본 DataFrame에 반영되지 않을 수 있음을 알려주는 신호입니다. 특히 전처리 파이프라인이나 피처 엔지니어링처럼 단계가 길어질수록, 이 경고를 방치하면 결과가 조용히 틀어져 디버깅이 어려워집니다.
핵심 원인은 간단합니다.
- Pandas에서 슬라이싱 결과는 뷰(view) 일 수도 있고 복사(copy) 일 수도 있습니다.
- 그 상태에서
df[조건][컬럼] = 값같은 체인 인덱싱(chain indexing) 으로 대입하면, Pandas가 “이게 원본을 수정하는 건지, 임시 객체를 수정하는 건지” 확신할 수 없어 경고를 띄웁니다.
이 글에서는 SettingWithCopyWarning이 발생하는 전형적인 패턴을 재현하고, 실무에서 가장 안전하게 쓰는 해결책 7가지를 정리합니다. (경고를 숨기는 방법이 아니라, 의도를 명확히 하는 방법 위주)
SettingWithCopyWarning이 뜨는 대표 재현 코드
아래 코드는 경고를 거의 확실히 유발합니다.
import pandas as pd
df = pd.DataFrame({
"name": ["a", "b", "c", "d"],
"score": [10, 20, 30, 40],
"passed": [False, False, True, True],
})
# 체인 인덱싱: df[조건]으로 한 번 자르고, 다시 [컬럼]에 대입
(df[df["score"] >= 30])["passed"] = True
문제는 (df[df["score"] >= 30])가 원본의 뷰인지 복사인지 상황에 따라 달라질 수 있고, 그 결과 passed 수정이 원본 df에 반영되지 않을 수 있다는 점입니다.
원인 1) 체인 인덱싱으로 대입한다
가장 흔한 원인입니다.
df[mask][col] = valuedf[col][mask] = value
이런 형태는 “중간 결과 객체”가 생기므로 경고가 뜹니다.
해결 1) .loc로 한 번에 선택하고 한 번에 대입
가장 권장되는 정석 패턴입니다.
mask = df["score"] >= 30
df.loc[mask, "passed"] = True
- 행 선택과 열 선택을
.loc한 번으로 끝내기 - Pandas가 원본
df에 대한 대입임을 명확히 알 수 있음
원인 2) 필터링한 결과를 변수에 담고, 그 변수에 대입한다
아래처럼 “부분 집합”을 따로 변수에 담아 수정하면 경고가 자주 발생합니다.
high = df[df["score"] >= 30]
high["passed"] = True
high가 뷰인지 복사인지 불명확하기 때문입니다.
해결 2) 명시적으로 .copy() 해서 “이건 복사본”을 확정한다
부분 집합을 독립적으로 다룰 의도라면 복사본임을 선언하세요.
high = df[df["score"] >= 30].copy()
high["passed"] = True
- 이렇게 하면 경고도 사라지고
- 원본
df가 수정되지 않는 것도 “의도된 동작”이 됩니다
반대로 원본을 수정하고 싶다면, 해결 1처럼 .loc를 사용해야 합니다.
원인 3) 슬라이스로 뽑은 DataFrame을 수정한다
행 범위를 슬라이스로 가져온 뒤 수정할 때도 경고가 뜰 수 있습니다.
part = df[:2]
part["passed"] = True
특히 인덱스가 정렬돼 있거나, 내부 블록 구조에 따라 뷰가 될 가능성이 생기면 경고가 나타납니다.
해결 3) 원본을 수정할 거면 슬라이스 대신 .loc 범위를 명시한다
# 인덱스가 기본 RangeIndex라고 가정
df.loc[0:1, "passed"] = True
만약 part를 독립 데이터로 쓰려면 해결 2처럼 part = df[:2].copy()가 맞습니다.
원인 4) .rename/.drop/.assign 등을 섞다가 중간 객체에 대입한다
메서드 체이닝을 하다 보면 중간 결과를 변수로 받거나, 체인 중간에 대입하려는 유혹이 생깁니다.
tmp = df.rename(columns={"score": "s"})
tmp[tmp["s"] >= 30]["passed"] = True
여기서도 체인 인덱싱이 핵심 문제입니다.
해결 4) .assign 또는 .loc 기반으로 “대입까지” 함수형으로 끝낸다
assign은 새 컬럼을 만드는 데 특히 깔끔합니다.
tmp = (
df.rename(columns={"score": "s"})
.assign(passed=lambda x: x["s"] >= 30)
)
이미 컬럼이 있고 일부만 바꾸려면 .loc가 더 직관적입니다.
tmp = df.rename(columns={"score": "s"}).copy()
mask = tmp["s"] >= 30
tmp.loc[mask, "passed"] = True
원인 5) .iloc/.iat/.at을 잘못된 대상(부분 집합)에 사용한다
스칼라 접근자 자체는 안전하지만, “부분 집합 객체”에 대해 쓰면 결국 같은 문제가 됩니다.
sub = df[df["score"] >= 30]
sub.iloc[0, sub.columns.get_loc("passed")] = True
sub가 뷰인지 복사인지 불명확하니 경고가 나올 수 있습니다.
해결 5) 원본 df에 대해 인덱스를 확정한 뒤 .at/.iat을 쓴다
mask = df["score"] >= 30
idx = df.index[mask][0]
df.at[idx, "passed"] = True
- “어느 행을 바꿀지”를 원본 기준으로 확정
- 성능상 한두 개 스칼라 수정이면
.at/.iat도 유용
원인 6) inplace=True 사용과 중간 객체가 섞여 의도가 흐려진다
inplace=True는 Pandas에서 점점 권장되지 않는 흐름이며, 특히 체인과 섞이면 코드 의도가 불명확해집니다.
sub = df[df["score"] >= 30]
sub.drop(columns=["name"], inplace=True)
이 코드는 경고가 뜨거나, 기대와 다르게 동작할 수 있습니다.
해결 6) inplace를 피하고, 반환값을 받는 방식으로 일관성 유지
sub = df[df["score"] >= 30].copy()
sub = sub.drop(columns=["name"])
- 복사본을 만들고
- 변환은 반환값으로 받기
데이터 파이프라인이 길어질수록 이런 스타일이 안정적입니다. (분산 시스템에서 재시도 폭주를 막기 위해 상태를 명확히 하는 것과 비슷한 결입니다. 예를 들어 gRPC MSA에서 데드라인·재시도 폭주 막는 법처럼 “의도를 명시”하는 접근이 운영 안정성을 만듭니다.)
원인 7) 경고를 억지로 끄고 문제를 숨긴다
아래처럼 경고를 끄면 당장은 조용해지지만, 실제로는 데이터가 잘못 수정되는 문제가 남습니다.
import pandas as pd
pd.options.mode.chained_assignment = None
해결 7) 경고를 끄기보다 “대입 경로”를 단일화한다
정리하면 다음 규칙만 지켜도 대부분의 SettingWithCopyWarning은 사라집니다.
- 원본을 수정하려면
df.loc[rows, cols] = value - 부분 집합을 독립적으로 수정하려면
df[mask].copy()후 수정 - 체인 인덱싱
df[...][...] = ...는 피하기
예시로, 여러 컬럼을 동시에 갱신하는 경우도 .loc가 깔끔합니다.
mask = df["score"] >= 30
df.loc[mask, ["passed", "name"]] = [True, "high"]
실무에서 자주 쓰는 패턴 모음
패턴 A) 조건부 업데이트는 무조건 .loc
mask = (df["score"] >= 30) & (df["name"].isin(["c", "d"]))
df.loc[mask, "passed"] = True
패턴 B) 파생 컬럼은 .assign
out = df.assign(grade=lambda x: pd.cut(x["score"], bins=[0, 20, 40], labels=["low", "high"]))
패턴 C) 필터 결과를 리포팅용으로만 쓰면 .copy()
report = df[df["passed"]].copy()
report["score"] = report["score"] * 1.1
디버깅 체크리스트
경고가 떴을 때 아래 질문에 답하면 해결 방향이 빠르게 정해집니다.
- 지금 수정하려는 대상은 원본
df인가, 아니면 부분 집합인가 - 코드에
df[...][...] = ...형태가 있는가 - 필터링 결과를 변수에 담고 그 변수를 수정하고 있는가
- 슬라이스
df[:n],df[a:b]를 수정하고 있는가 inplace=True가 중간에 섞여 있는가
운영에서 인증 오류나 타임아웃처럼 “원인이 여러 갈래”인 문제는 체크리스트가 큰 도움이 됩니다. 비슷한 접근으로 원인을 빠르게 좁히는 글로는 Responses API 401인데 키가 맞는 7가지 이유도 참고할 만합니다.
결론
SettingWithCopyWarning은 Pandas가 친절하게 알려주는 “데이터가 기대대로 안 바뀔 수 있다”는 경고입니다. 해결의 핵심은 단순합니다.
- 원본을 바꾸려면
.loc로 한 번에 대입 - 부분 집합을 바꾸려면
.copy()로 의도를 확정 - 체인 인덱싱을 습관적으로 제거
이 3가지만 팀 컨벤션으로 정해도, 전처리 코드의 재현성·신뢰성이 크게 올라갑니다.