- Published on
Pandas SettingWithCopyWarning 완전정복
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서브셋을 만든 뒤 값을 수정했는데, 결과가 반영된 것 같기도 하고 아닌 것 같기도 한 경험이 있다면 SettingWithCopyWarning을 이미 만났을 가능성이 큽니다. 이 경고는 단순히 “귀찮은 메시지”가 아니라, 내 코드가 의도한 데이터프레임을 수정하지 않았을 수 있다는 신호입니다. 특히 전처리 파이프라인, 피처 엔지니어링, 리포팅 자동화처럼 재현성과 정확성이 중요한 작업에서 이 경고를 무시하면, 조용히 잘못된 결과가 누적될 수 있습니다.
이 글에서는 SettingWithCopyWarning이 왜 뜨는지(내부적으로 어떤 상황을 의심하는지), 그리고 경고를 없애는 것이 아니라 의도를 명확히 해서 안전하게 수정하는 방법을 패턴별로 정리합니다.
참고로, 파이썬 환경 이슈로 디버깅이 막히는 경우도 많습니다. 예를 들어 pkg_resources 관련 오류로 실행 자체가 안 된다면 먼저 환경을 정리하는 게 우선입니다. 필요하면 Python 3.12에서 pkg_resources 에러 근본 해결도 함께 확인해보세요.
SettingWithCopyWarning이 의미하는 것
핵심은 Pandas의 인덱싱 결과가 뷰(view)인지 복사(copy)인지 불명확하다는 점입니다.
- 뷰(view): 원본 데이터의 “창” 같은 것. 뷰를 수정하면 원본이 바뀔 수 있습니다.
- 복사(copy): 원본과 별개. 복사를 수정해도 원본은 바뀌지 않습니다.
문제는 Pandas에서 어떤 인덱싱은 뷰를 반환할 때도 있고, 복사를 반환할 때도 있다는 점입니다(데이터 레이아웃, 블록 구조, 연산 경로에 따라 달라질 수 있음). 그래서 Pandas는 다음 같은 코드를 보면 경고를 띄웁니다.
import pandas as pd
df = pd.DataFrame({
"team": ["A", "A", "B"],
"score": [10, 20, 30]
})
sub = df[df["team"] == "A"]
sub["score"] = sub["score"] + 1 # SettingWithCopyWarning 가능
위 코드는 개발자 의도가 둘 중 무엇인지 모호합니다.
sub만 바꾸고 싶다(원본df는 유지)df의 특정 행을 바꾸고 싶다
Pandas는 이 상황에서 “너 지금 원본을 바꾼다고 생각했는데 사실은 복사본만 바꾼 걸 수도 있어”라고 경고합니다.
대표적인 발생 패턴: 체인 인덱싱
SettingWithCopyWarning의 가장 흔한 원인은 체인 인덱싱(chained indexing) 입니다. 즉, 인덱싱을 두 번 이상 이어서 쓰는 패턴입니다.
# 나쁜 예: 체인 인덱싱
df[df["team"] == "A"]["score"] = 999 # 경고 + 의도 불명확
이 코드는 아래처럼 해석될 수 있습니다.
df[df["team"] == "A"]가 임시 객체(뷰 또는 복사)를 만들고- 그 임시 객체의
"score"컬럼을 수정
임시 객체가 복사라면 원본 df는 전혀 바뀌지 않습니다. 뷰라면 바뀔 수도 있습니다. 따라서 Pandas는 경고합니다.
해결 원칙 1: 원본을 수정하려면 loc로 한 번에
원본 df를 수정하려는 목적이라면, 조건과 컬럼 선택을 한 번에 표현하세요.
mask = df["team"] == "A"
df.loc[mask, "score"] = df.loc[mask, "score"] + 1
loc[행조건, 열선택]형태는 “원본의 이 위치를 수정한다”는 의도를 명확히 합니다.- 경고가 사라질 뿐 아니라, 코드 리뷰 관점에서도 안전합니다.
여러 컬럼을 동시에 수정
mask = df["team"] == "A"
df.loc[mask, ["score"]] = df.loc[mask, ["score"]] * 2
또는 서로 다른 컬럼을 한 번에:
df.loc[mask, "flag"] = True
해결 원칙 2: 부분 데이터프레임을 따로 쓸 거면 copy()로 의도 고정
반대로 “원본은 건드리지 않고, 필터된 데이터프레임만 별도로 가공”하려면 애초에 명시적으로 복사본을 만드세요.
sub = df.loc[df["team"] == "A"].copy()
sub["score"] = sub["score"] + 1 # 경고 없음, 원본 영향 없음
여기서 copy()는 단순히 경고 제거용이 아니라, 데이터 파이프라인에서 부작용을 차단하는 경계선 역할을 합니다.
해결 원칙 3: 컬럼 생성은 assign을 우선 고려
새 컬럼을 만들 때는 assign이 의도를 더 명확히 해주는 경우가 많습니다. 특히 메서드 체이닝 스타일에서는 효과가 큽니다.
sub = (
df.loc[df["team"] == "A"].copy()
.assign(score_plus=lambda x: x["score"] + 1)
)
assign은 새로운 데이터프레임을 반환합니다.- 중간 단계가 뷰인지 복사인지 애매한 상황을 줄여줍니다.
언제 SettingWithCopyWarning이 특히 자주 뜨나
1) 필터링한 뒤 바로 컬럼 수정
sub = df[df["team"] == "A"]
sub["score"] = 0 # 경고 가능
해결:
sub = df.loc[df["team"] == "A"].copy()
sub["score"] = 0
또는 원본 수정이면:
df.loc[df["team"] == "A", "score"] = 0
2) 슬라이스 후 수정
sub = df[:2]
sub["score"] = 100 # 경고 가능
해결:
sub = df.iloc[:2].copy()
sub["score"] = 100
원본 수정이면:
df.iloc[:2, df.columns.get_loc("score")] = 100
iloc에서 컬럼을 인덱스로 넣기 번거롭다면, 일반적으로는 loc 기반으로 컬럼명을 쓰는 편이 가독성이 좋습니다.
3) inplace=True와 결합된 체인
inplace=True는 많은 경우 “원본을 직접 바꾼다”는 인상을 주지만, 체인 인덱싱과 결합되면 애초에 원본이 아닌 객체에 대해 inplace가 실행될 수 있습니다.
# 나쁜 예: 임시 객체에 inplace
df[df["team"] == "A"].fillna(0, inplace=True) # 의도 불명확
해결(원본 수정):
mask = df["team"] == "A"
df.loc[mask] = df.loc[mask].fillna(0)
해결(부분만 따로 가공):
sub = df.loc[df["team"] == "A"].copy()
sub = sub.fillna(0)
“경고만 끄면” 안 되는 이유
다음처럼 경고를 끄는 방법이 인터넷에 종종 보이지만, 추천하지 않습니다.
import pandas as pd
pd.options.mode.chained_assignment = None # 경고 숨김
이 설정은 문제를 해결하는 게 아니라 문제 신호를 제거합니다. 실제로 원본이 수정되지 않았는데도 파이프라인이 계속 진행되어, 나중에 결과 검증이 더 어려워집니다.
경고를 끄는 대신, 아래 체크리스트로 코드를 정리하는 게 좋습니다.
실전 체크리스트
1) 내가 수정하려는 대상은 원본인가, 서브셋인가
- 원본 수정:
df.loc[mask, col] = value - 서브셋 가공:
sub = df.loc[mask].copy()후sub[...] = ...
2) 체인 인덱싱을 하고 있지 않은가
df[...][...] = ...형태는 대부분 위험 신호- 조건과 컬럼 선택을
loc한 번으로 합치기
3) 함수로 캡슐화해서 “부작용”을 통제했나
전처리 함수가 원본을 바꾸는지 여부를 명확히 하세요.
def add_flag_copy(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out["flag"] = out["score"] >= 20
return out
def add_flag_inplace(df: pd.DataFrame) -> None:
df.loc[:, "flag"] = df["score"] >= 20
- 반환형과 함수명으로 의도를 드러내면 팀 단위 협업에서 사고가 줄어듭니다.
디버깅 팁: 수정이 실제로 반영됐는지 검증
경고가 사라졌더라도, 의도대로 반영되었는지 “눈으로 확인”하는 습관이 중요합니다.
mask = df["team"] == "A"
before = df.loc[mask, "score"].copy()
df.loc[mask, "score"] = df.loc[mask, "score"] + 1
after = df.loc[mask, "score"]
print(before.to_list(), "->", after.to_list())
위 출력에서 화살표는 ->처럼 부등호가 포함될 수 있으니, 블로그/문서에서는 반드시 인라인 코드로 감싸는 습관이 좋습니다.
성능/재현성 이슈도 같이 겪는다면, 문제를 “경고”가 아니라 “원인 추적” 관점으로 접근하는 게 도움이 됩니다. 웹 성능에서 Long Task를 추적하듯이, 데이터 파이프라인도 단계별로 변화를 기록하면 원인 찾기가 쉬워집니다. 비슷한 접근 방식이 궁금하다면 Chrome INP 급락 원인 - Long Task 3분 추적기도 참고할 만합니다.
자주 묻는 질문
copy()는 무조건 써야 하나
아닙니다. 원본을 수정할 목적이면 loc로 직접 수정하는 편이 더 낫습니다. copy()는 “서브셋을 독립적으로 가공”하려는 경우에 명시적으로 쓰는 게 좋습니다.
SettingWithCopyWarning이 안 뜨면 안전한가
대부분 안전하지만, “안 뜬다”가 “항상 의도대로 동작한다”를 100퍼센트 보장하진 않습니다. 다만 경고가 뜨는 코드는 거의 확실히 개선 여지가 있습니다.
정리
SettingWithCopyWarning을 완전히 정복하는 요령은 단순합니다.
- 원본 수정은
loc로 한 번에 표현한다 - 서브셋을 별도로 가공할 때는
copy()로 경계를 만든다 - 체인 인덱싱을 피하고,
assign같은 반환형 API를 적극 활용한다 - 경고를 숨기지 말고, 코드 의도를 명확히 만든다
이 원칙만 지켜도 Pandas 전처리 코드의 안정성이 눈에 띄게 올라가고, “분명 수정했는데 왜 결과가 그대로지” 같은 디버깅 시간을 크게 줄일 수 있습니다.