Published on

pandas SettingWithCopyWarning 완전정복 - 원인부터 해결까지

Authors

서론

pandas를 쓰다 보면 한 번쯤은 아래 경고를 마주칩니다.

> SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame

이 경고는 단순히 “귀찮은 로그”가 아니라, 내가 의도한 DataFrame에 값이 실제로 반영되지 않을 수 있다는 신호입니다. 특히 전처리 파이프라인, 피처 엔지니어링, 리포팅 코드에서 조용히 데이터가 틀어지는 원인이 되기도 합니다.

이 글에서는 SettingWithCopyWarning을 **왜 발생하는지(뷰 vs 복사)**부터, **안전하게 수정하는 표준 패턴(.loc, .copy, assign, pipe)**까지 정리합니다. 이미 관련 이슈를 빠르게 해결하고 싶다면 내부 글인 pandas SettingWithCopyWarning 완전 해결법도 함께 참고하면 좋습니다.

SettingWithCopyWarning이 발생하는 진짜 이유

핵심은 pandas의 인덱싱 결과가 상황에 따라 **view(원본을 바라보는 창)**일 수도 있고 **copy(복사본)**일 수도 있다는 점입니다.

  • df[mask] 같은 필터링은 대개 “새로운 DataFrame”을 반환합니다.
  • 그런데 그 결과가 내부적으로 원본과 메모리를 공유하는지(view) 완전히 분리된 복사본인지(copy)는 케이스마다 다릅니다.
  • 이 모호성 때문에 pandas는 “지금 너가 변경하는 게 원본에 반영될지 확신할 수 없다”고 경고합니다.

즉, 경고는 다음 상황을 말합니다.

  1. 어떤 slice(부분집합)를 만들었고
  2. 그 slice에 값을 세팅했는데
  3. 그 slice가 원본의 view인지 copy인지 애매해서
  4. 원본이 바뀌지 않을 수 있으니 조심하라

가장 흔한 재현 패턴: 체인 인덱싱(Chained Indexing)

SettingWithCopyWarning의 대표 원인은 체인 인덱싱입니다.

나쁜 예: df[mask][col] = ...

import pandas as pd

df = pd.DataFrame({
    "name": ["a", "b", "c", "d"],
    "score": [10, 55, 70, 40],
    "grade": [None, None, None, None]
})

mask = df["score"] >= 60

# 체인 인덱싱: df[mask] 결과에 다시 ["grade"]
df[mask]["grade"] = "PASS"  # SettingWithCopyWarning 가능

이 코드는 “PASS”가 기대대로 들어간 것처럼 보여도, 실제로는 원본 df가 바뀌지 않거나 일부만 바뀌는 식의 불안정성이 생길 수 있습니다.

좋은 예: .loc[row_indexer, col_indexer]

df.loc[mask, "grade"] = "PASS"  # 안전하고 명확

.loc원본 df에 대해 ‘행/열을 동시에’ 지정하므로 pandas가 모호함 없이 처리할 수 있습니다.

.loc를 표준으로 삼아야 하는 이유

.loc는 단순한 “경고 회피”가 아니라, 코드의 의미를 명확히 해줍니다.

  • df.loc[mask, "grade"] = ... : 원본 df의 특정 행/열을 수정한다
  • tmp = df[mask] : 조건에 맞는 행들을 뽑아 새 객체를 만든다(대개)

이 둘을 섞어서 tmp["grade"] = ...처럼 쓰면, “tmp가 원본의 일부인가?”가 불분명해집니다.

언제 .copy()가 필요한가?

필터링한 결과를 독립된 테이블로 취급하고 싶을 때가 있습니다. 예를 들어:

  • 조건에 맞는 행만 뽑아 리포트/모델 입력으로 사용
  • 이후 변형이 원본에 절대 영향을 주면 안 됨

이럴 때는 명시적으로 .copy()를 붙여 “나는 복사본을 만들겠다”고 선언하는 게 좋습니다.

filtered = df.loc[df["score"] >= 60, ["name", "score"]].copy()
filtered["bucket"] = "high"  # 경고 없음 + 원본 영향 없음

여기서 중요한 점:

  • df.loc[...]로 먼저 슬라이싱을 명확하게 하고
  • .copy()로 의도를 못 박으면
  • SettingWithCopyWarning도 사라지고, 데이터 흐름도 안전해집니다.

자주 나오는 케이스별 해결 패턴

1) 특정 컬럼만 변환하고 싶다: assign 사용

assign은 새로운 DataFrame을 반환하며, 체인 인덱싱을 피하기 좋습니다.

result = (
    df
    .assign(score2=lambda x: x["score"] * 2)
)

필터링 후 변환이라면:

result = (
    df.loc[df["score"] >= 60].copy()
    .assign(grade="PASS")
)

2) 여러 조건으로 부분 업데이트: .loc + 마스크 조합

df["grade"] = "UNKNOWN"

df.loc[df["score"] >= 60, "grade"] = "PASS"
df.loc[df["score"] < 60, "grade"] = "FAIL"

이 방식은 “원본 df를 단계적으로 업데이트한다”는 의도가 명확합니다.

3) inplace=True와 결합된 체인 호출 피하기

pandas에서 inplace=True는 종종 혼란을 키웁니다. 특히 체인 호출과 결합하면 디버깅이 어려워집니다.

# 비추천: 중간 객체가 무엇인지 모호
# df[df["score"] >= 60].rename(columns={"score": "s"}, inplace=True)

# 추천: 명시적으로 새 객체 받기
high = df.loc[df["score"] >= 60].copy()
high = high.rename(columns={"score": "s"})

4) 그룹 연산 후 결과를 원본에 반영: transform 활용

그룹별 평균을 빼서 정규화한 값을 원본에 넣고 싶다면 .loc로 억지로 slice를 만들기보다 transform이 깔끔합니다.

df = pd.DataFrame({
    "team": ["A", "A", "B", "B"],
    "score": [10, 30, 20, 40]
})

team_mean = df.groupby("team")["score"].transform("mean")
df["score_centered"] = df["score"] - team_mean

“경고를 끄면 되지 않나요?”가 위험한 이유

SettingWithCopyWarning을 무시하거나 전역 옵션으로 끄는 글도 종종 보입니다.

pd.options.mode.chained_assignment = None  # 경고 비활성화(비추천)

이건 증상을 숨길 뿐입니다. 진짜 문제는 다음 중 하나일 수 있습니다.

  • 원본 df가 수정되지 않아 결과가 틀림
  • 특정 버전/환경에서만 동작(재현 불가 버그)
  • 파이프라인 중간 결과가 조용히 누락

경고를 끄는 대신, 경고가 나오는 지점을 “원본을 수정할 건지/복사본을 만들 건지”로 분기시키는 게 정답입니다.

디버깅 체크리스트: 경고가 뜨면 이렇게 확인

  1. 경고가 뜬 줄의 왼쪽이 df[...][...] = ... 형태인가?

    • 그렇다면 .loc로 합치기
  2. 내가 수정하려는 대상은 원본 df인가, 필터링된 결과인가?

    • 원본이면 df.loc[mask, col] = value
    • 결과 테이블이면 filtered = df.loc[mask].copy() 후 수정
  3. 함수 체인에서 중간 객체가 눈에 안 보이는가?

    • 중간 변수를 도입하거나 pipe로 단계 분리
  4. 같은 코드가 버전/환경에 따라 동작이 다른가?

    • 체인 인덱싱 가능성이 큼. .loc/.copy()로 의도 고정

실전 예제: 전처리 함수에서 안전하게 쓰는 패턴

아래는 흔한 전처리 요구사항(필터링, 파생 컬럼 생성, 부분 업데이트)을 경고 없이 구현한 예입니다.

import pandas as pd


def preprocess(df: pd.DataFrame) -> pd.DataFrame:
    # 원본을 건드리지 않겠다는 정책이면 첫 줄에서 copy
    out = df.copy()

    # 결측 처리(예시)
    out["score"] = out["score"].fillna(0)

    # 파생 컬럼
    out = out.assign(is_pass=out["score"] >= 60)

    # grade 업데이트는 .loc로 명확하게
    out["grade"] = "FAIL"
    out.loc[out["is_pass"], "grade"] = "PASS"

    return out


df = pd.DataFrame({"name": ["a", "b"], "score": [70, 40]})
print(preprocess(df))

포인트는 다음과 같습니다.

  • “원본 불변” 정책이면 초기에 copy()
  • 파생 컬럼은 assign
  • 조건부 업데이트는 .loc

이 조합이면 SettingWithCopyWarning을 구조적으로 피할 수 있습니다.

결론

SettingWithCopyWarning은 pandas가 “너 지금 원본을 수정하는지 복사본을 수정하는지 애매하다”고 알려주는 안전장치입니다. 해결의 핵심은 간단합니다.

  • 원본을 수정할 거면: .loc[rows, cols] = value
  • 복사본으로 작업할 거면: .copy()로 의도를 명시
  • 체인 인덱싱(df[...][...] = ...)은 피하기

한 번 패턴을 정해두면 경고를 ‘없애는 것’이 아니라, 데이터 처리 코드를 더 예측 가능하고 견고하게 만들 수 있습니다.

추가로, SettingWithCopyWarning을 더 빠르게 진단하고 케이스별로 정리한 글이 필요하다면 pandas SettingWithCopyWarning 완전 해결법을 함께 보면 도움이 됩니다.