Published on

pandas SettingWithCopyWarning 5분 해결법

Authors

서론부터 결론까지 한 번에 정리합니다. SettingWithCopyWarning은 에러가 아니라 경고지만, 무시하면 데이터가 바뀐 것처럼 보이는데 실제로는 안 바뀌는 상황을 만들 수 있어 실무에서 매우 위험합니다. 특히 필터링한 뒤 컬럼을 수정하거나, 체이닝 인덱싱을 습관처럼 쓰면 자주 터집니다.

이 글은 다음 목표로 구성합니다.

  • 경고가 뜨는 정확한 이유(뷰 vs 복사) 를 1분 안에 이해
  • 자주 나오는 패턴을 즉시 고치는 3가지 정답 레시피
  • 디버깅/검증 방법과, 경고를 꺼야 하는 경우/꺼서는 안 되는 경우

SettingWithCopyWarning이 뜨는 진짜 이유

pandas에서 df[...] 같은 슬라이싱은 상황에 따라 원본을 바라보는 뷰(view) 를 반환하기도 하고, 새 객체 복사(copy) 를 반환하기도 합니다. 문제는 사용자가 그 차이를 코드만 보고는 확실히 알기 어렵다는 점입니다.

경고는 보통 이런 흐름에서 발생합니다.

  1. df[df["cond"]] 처럼 필터링해서 중간 결과를 만들고
  2. 그 결과에 다시 [...] = ... 로 값을 대입

이때 pandas는 중간 결과가 원본의 뷰인지 복사인지 확신할 수 없어서 "너 지금 원본이 바뀌는지 보장 못 한다" 라고 경고합니다.

핵심은 하나입니다.

  • 체이닝 인덱싱(chained indexing) 을 피하고
  • 수정은 항상 원본 DataFrame에 대해 loc로 한 번에 하거나
  • 중간 결과를 쓸 거면 명시적으로 copy() 하라

1분 재현: 가장 흔한 경고 패턴

아래는 실무에서 제일 자주 보는 형태입니다.

import pandas as pd

df = pd.DataFrame({
    "user": ["a", "b", "c"],
    "age": [19, 25, 17],
    "score": [10, 20, 30],
})

teen = df[df["age"] < 20]
teen["score"] = teen["score"] * 2  # SettingWithCopyWarning 가능

겉보기엔 teen만 수정하는 것 같지만, 의도는 보통 둘 중 하나입니다.

  • 의도 A: 원본 df의 해당 행 score를 바꾸고 싶다
  • 의도 B: 필터링 결과 teen만 바꾸고 싶다(원본은 유지)

경고는 "너 의도가 A인지 B인지 모르겠고, 결과도 보장 못 해" 라는 신호입니다.


5분 해결 레시피 1: 원본을 바꾸려면 loc 한 방에 끝내기

원본 df를 업데이트하려는 목적이라면, 중간 DataFrame을 만들지 말고 loc로 한 번에 쓰는 게 정답입니다.

mask = df["age"] < 20
df.loc[mask, "score"] = df.loc[mask, "score"] * 2

이 방식의 장점:

  • 경고가 사라짐
  • 원본이 확실히 업데이트됨
  • 의도가 코드에 명확히 드러남

추가로 여러 컬럼을 동시에 바꾸는 것도 가능합니다.

mask = df["age"] < 20

df.loc[mask, ["score", "age"]] = df.loc[mask, ["score", "age"]].assign(
    score=lambda x: x["score"] * 2,
    age=lambda x: x["age"] + 1,
)

5분 해결 레시피 2: 필터 결과만 쓸 거면 copy()로 의도를 고정

원본은 건드리지 않고, 필터링된 결과 DataFrame을 별도로 가공하려면 copy()를 명시하세요.

teen = df[df["age"] < 20].copy()
teen["score"] = teen["score"] * 2  # 안전

이렇게 하면 pandas가 "아, 이건 복사본이구나" 를 확실히 알게 되고, 경고도 없어집니다.

실무 팁:

  • 필터링 결과를 변수에 담아 여러 단계로 가공한다면 copy()가 거의 필수입니다.
  • 특히 파이프라인 전처리에서 중간 결과를 재사용하는 패턴이면 더더욱요.

5분 해결 레시피 3: 체이닝 인덱싱을 없애기(가장 중요한 습관)

경고의 주범은 대개 아래 같은 코드입니다.

# 나쁜 예: 체이닝 인덱싱
df[df["age"] < 20]["score"] = 0

이 코드는 겉으로는 "필터한 행의 score만 0" 같지만, 실제로는

  • df[df["age"] < 20] 가 임시 객체를 만들고
  • 그 임시 객체의 score에 값을 넣는 형태

가 되어 원본 df에 반영이 안 될 수 있습니다.

정답은 항상 loc입니다.

df.loc[df["age"] < 20, "score"] = 0

자주 헷갈리는 케이스: df["col"] vs df[["col"]]

단일 컬럼 선택은 Series를 반환합니다.

s = df["score"]

복수 컬럼 선택(리스트)은 DataFrame을 반환합니다.

sub = df[["score"]]

이 자체가 경고를 만들진 않지만, 이후 대입과 결합되면 뷰/복사 판단이 더 어려워질 수 있습니다. 특히 sub = df[["score"]]sub[...] = ... 같은 패턴은 의도를 명확히 하는 편이 좋습니다.

  • 원본 수정 목적이면 df.loc[...]
  • 별도 가공 목적이면 .copy()

디버깅: 내가 바꾸는 게 원본인지 확인하는 2가지 방법

1) 수정 전후 비교를 강제로 넣기

before = df.copy()

mask = df["age"] < 20
teen = df[mask]
teen["score"] = 999  # 경고 가능

print((df != before).any())

원본이 바뀌었는지 여부를 눈으로 확인할 수 있습니다.

2) 의도를 테스트 코드로 고정

데이터 전처리는 조용히 실패하기 쉬워서, 간단한 단정문이 큰 도움이 됩니다.

mask = df["age"] < 20

# 원본을 바꾸는 의도
expected = df.copy()
expected.loc[mask, "score"] = expected.loc[mask, "score"] * 2

df.loc[mask, "score"] = df.loc[mask, "score"] * 2

pd.testing.assert_frame_equal(df, expected)

경고를 끄는 방법(하지만 권장하지 않는 이유)

인터넷에 아래 같은 코드가 종종 보입니다.

import pandas as pd
pd.options.mode.chained_assignment = None

이 설정은 경고를 숨기지만, 문제가 사라지는 게 아니라 문제를 못 보게 만드는 것입니다.

권장되는 사용 시나리오가 있다면 딱 하나입니다.

  • 레거시 코드에서 경고가 너무 많아 단계적으로 리팩터링해야 하고
  • 그 과정에서 로그 노이즈를 줄여야 하며
  • 테스트로 데이터 정합성이 보장되는 경우

그 외에는 loc/copy()로 구조적으로 해결하세요.


실전 레시피 모음: 상황별 정답만 빠르게

조건에 맞는 행의 컬럼 업데이트

df.loc[df["age"] < 20, "score"] = df.loc[df["age"] < 20, "score"] * 2

여러 조건 조합

mask = (df["age"] < 20) & (df["user"].isin(["a", "c"]))
df.loc[mask, "score"] = 0

필터링 결과에서 새 컬럼 만들기(원본 유지)

teen = df[df["age"] < 20].copy()
teen["grade"] = "teen"

query를 쓰는 경우도 동일: 복사본이면 copy()

teen = df.query("age < 20").copy()
teen["score"] = teen["score"] + 1

왜 이 경고가 데이터 파이프라인에서 특히 위험한가

SettingWithCopyWarning은 개발 환경에서는 경고로 끝나지만, 배치/ETL에서는 다음 문제로 이어지기 쉽습니다.

  • 일부 환경/버전에서만 재현되어 "가끔 틀리는" 데이터 생성
  • 중간 DataFrame을 여러 단계로 넘기며 원본 업데이트가 누락
  • 다운스트림 모델/리포트가 조용히 오염

이런 "겉보기 정상" 문제는 인프라에서도 자주 등장합니다. 예를 들어, 겉으로는 정상 로그인데 상태 체크가 실패하는 케이스처럼요. 비슷한 디버깅 감각이 필요하다면 EKS에서 Readiness 실패인데 로그는 정상일 때 글도 함께 참고하면 도움이 됩니다.


결론: 5분 체크리스트

아래 3가지만 지키면 SettingWithCopyWarning은 대부분 즉시 해결됩니다.

  1. 원본을 바꾸려는 거면 중간 객체 만들지 말고 df.loc[mask, col] = ...
  2. 중간 결과를 가공하려는 거면 df[mask].copy()로 복사 의도를 고정
  3. df[cond][col] = ... 형태의 체이닝 인덱싱은 금지

pandas 전처리는 작은 습관 차이로 데이터 품질이 갈립니다. 경고를 "귀찮은 메시지"가 아니라 "데이터가 틀릴 수 있다는 신호"로 보고, loccopy()로 의도를 명확히 표현하는 쪽으로 코드를 정리해 두면 이후 디버깅 비용이 크게 줄어듭니다.