Published on

pandas SettingWithCopyWarning 완전 정복

Authors

서론

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

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

처음엔 “경고니까 그냥 무시해도 되나?” 싶지만, 이 경고는 데이터가 실제로 수정되지 않았는데 수정된 줄 착각하게 만드는 대표적인 함정입니다. 특히 전처리 파이프라인, 피처 엔지니어링, 리포트 산출처럼 “한 번 잘못되면 결과 전체가 조용히 틀리는” 작업에서 치명적입니다.

이 글에서는 SettingWithCopyWarning의 본질(뷰 vs 복사), 어떤 코드가 위험한지, 그리고 실무에서 재발하지 않게 만드는 안전한 패턴(.loc, .copy, assign, pipe)을 코드로 정리합니다. 운영에서 조용히 망가지는 문제를 빠르게 진단하는 관점은 Cloudflare 520·521, Nginx·ALB 로그로 30분 진단처럼 “원인-증상 연결을 끊김 없이 추적”하는 습관과도 닮아 있습니다.

SettingWithCopyWarning은 왜 뜨는가

핵심은 다음 한 줄입니다.

  • pandas에서 df[...]부분을 뽑아 만든 객체는 상황에 따라 원본의 뷰(view) 일 수도, 복사(copy) 일 수도 있습니다.
  • 그 상태에서 다시 [...] = ...로 값을 대입하면, pandas는 “이게 원본에 반영되는지 확신할 수 없다”고 판단해 경고를 띄웁니다.

뷰(view) vs 복사(copy) 직관

  • : 원본과 메모리를 공유. 뷰를 수정하면 원본도 바뀔 수 있음.
  • 복사: 별도 메모리. 복사를 수정해도 원본은 안 바뀜.

문제는 pandas가 내부 최적화/상황에 따라 뷰를 줄 때도 있고 복사를 줄 때도 있어서, 코드가 어떤 환경/버전/데이터 형태에서 조용히 달라질 수 있다는 점입니다.

가장 흔한 원인: 체인 인덱싱(Chained indexing)

SettingWithCopyWarning의 90%는 체인 인덱싱에서 옵니다.

위험한 예시 1: 필터 후 컬럼 대입

import pandas as pd

df = pd.DataFrame({
    "user": ["a", "b", "c"],
    "age": [10, 20, 30],
    "score": [1.0, 2.0, 3.0]
})

teen = df[df["age"] < 20]     # 슬라이스(뷰/복사 불명)
teen["score"] = 0            # SettingWithCopyWarning 가능

이 코드가 위험한 이유:

  • teen이 원본 df의 뷰인지 복사인지 확정 불가
  • 따라서 teen["score"] = 0df에 반영될 수도/안 될 수도

위험한 예시 2: 컬럼 선택 후 다시 행 필터링

sub = df[["age", "score"]]
sub[sub["age"] >= 20]["score"] = 99  # 최악의 체인 인덱싱

위 코드는 거의 확실히 의도대로 동작하지 않습니다. “중간 결과”에 대입하고 있기 때문입니다.

정답 패턴 1: .loc로 한 번에 인덱싱하고 대입

가장 권장되는 정석입니다.

mask = df["age"] < 20

df.loc[mask, "score"] = 0

장점:

  • 원본 df에 대해 명시적으로 “mask인 행의 score 컬럼”을 지정
  • 뷰/복사 모호성이 사라짐
  • 경고도 사라지고, 결과도 확정적

여러 컬럼을 동시에 수정

mask = df["age"] >= 20

df.loc[mask, ["score", "user"]] = [99, "adult"]

정답 패턴 2: 부분 DataFrame을 독립적으로 쓰려면 .copy()

필터 결과를 별도 테이블로 쓰고 싶다면, “난 복사본을 원한다”를 명시해야 합니다.

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

이때 중요한 포인트:

  • .copy()는 메모리를 더 씁니다.
  • 하지만 “원본과 분리된 전처리”가 목적이라면 비용 대비 안정성이 압도적입니다.

정답 패턴 3: assign으로 새 컬럼/변환을 함수형으로

전처리 파이프라인에서 경고를 줄이고, 중간 상태를 명확히 하려면 assign이 유용합니다.

teen = (
    df.loc[df["age"] < 20, ["user", "age", "score"]]
      .assign(score=lambda x: 0)
)
  • loc로 필요한 범위를 확정
  • assign으로 새 DataFrame을 반환(원본 불변)

“경고는 떴는데 결과는 맞는 것 같은데?”가 더 위험하다

SettingWithCopyWarning이 무서운 이유는 항상 틀리게 동작하지 않기 때문입니다.

  • 어떤 경우엔 우연히 뷰로 잡혀서 원본이 바뀌어 “맞는 것처럼” 보임
  • 다른 경우엔 복사로 잡혀서 원본이 안 바뀌는데도 코드가 계속 진행됨

즉, 테스트 데이터에서는 통과하고 운영 데이터에서 틀릴 수 있습니다. 이런 “조용한 실패”는 인프라에서도 자주 보이는데, 예를 들어 캐시가 안 먹어서 성능이 떨어지는데도 CI는 초록불인 상황처럼요. 비슷한 진단 관점은 GitHub Actions 캐시가 안 먹을 때 - key·restore-keys·권한 같은 글이 도움이 됩니다.

경고를 재현하고, 실제로 원본이 바뀌는지 확인하는 법

디버깅할 때는 “내가 수정한 게 원본에 반영됐나?”를 명확히 확인해야 합니다.

import pandas as pd

df = pd.DataFrame({"x": [1, 2, 3], "y": [10, 20, 30]})
part = df[df["x"] > 1]

part["y"] = 999  # 경고 가능

print("part:\n", part)
print("df:\n", df)  # 원본이 바뀌었는지 반드시 확인

또한 개발 환경에서는 경고를 “보이게” 만드는 것이 좋습니다.

import pandas as pd

pd.options.mode.chained_assignment = "warn"   # 기본에 가깝게 경고
# pd.options.mode.chained_assignment = "raise" # 예외로 승격(강력 추천: 테스트에서)
  • 로컬/CI 테스트에서는 raise로 올려서 문제 코드를 즉시 잡고
  • 노트북 탐색 단계에서는 warn으로 흐름을 유지하는 식으로 운영하면 좋습니다.

자주 나오는 실전 시나리오별 처방

1) 필터링한 뒤 기존 컬럼 값 일부만 바꾸기

나쁜 코드

df[df["x"] > 1]["y"] = 0

좋은 코드

df.loc[df["x"] > 1, "y"] = 0

2) 필터링한 결과를 새 DataFrame로 만들고 거기서 마음껏 수정

좋은 코드

sub = df.loc[df["x"] > 1, ["x", "y"]].copy()
sub["y"] = sub["y"] * 10

3) 그룹별로 조건부 수정(자주 체인 인덱싱으로 망가짐)

예: 그룹 평균보다 큰 값은 1로, 아니면 0으로.

import numpy as np

df = pd.DataFrame({
    "g": ["A", "A", "B", "B"],
    "v": [1, 3, 2, 10]
})

mean_by_g = df.groupby("g")["v"].transform("mean")

df.loc[:, "flag"] = np.where(df["v"] > mean_by_g, 1, 0)

포인트:

  • transform은 원본과 같은 길이로 정렬된 시리즈를 돌려주므로 .loc[:, "flag"] = ...가 안전합니다.

4) 정렬/슬라이스 후 수정

정렬 결과를 변수로 받아 수정하는 경우도 경고가 날 수 있습니다.

sorted_df = df.sort_values("v")
# sorted_df가 원본의 뷰인지 복사인지 상황에 따라 다름
sorted_df["v"] = sorted_df["v"] + 1  # 경고 가능

의도가 “정렬된 사본에서 작업”이라면:

sorted_df = df.sort_values("v").copy()
sorted_df["v"] = sorted_df["v"] + 1

의도가 “원본 df를 조건에 따라 수정”이라면 .loc로 직접 원본을 수정해야 합니다.

SettingWithCopyWarning을 ‘근본적으로’ 줄이는 설계 습관

1) 원본을 수정할지, 사본을 만들지 먼저 결정

  • 원본 수정: df.loc[mask, col] = ...
  • 사본 생성: df.loc[mask, cols].copy()

이 결정을 코드에 드러내면 경고는 자연히 줄어듭니다.

2) 중간 결과에 대입하지 말기

다음 형태는 습관적으로 피하세요.

something = df[cond]
something["col"] = ...
  • 원본을 수정하고 싶다면 df.loc[cond, "col"] = ...
  • 사본을 수정하고 싶다면 something = df[cond].copy()

3) 테스트에서 chained_assignment를 raise로

데이터 파이프라인은 “한 줄이 조용히 무시되는” 것이 가장 무섭습니다. 인프라에서 403이 갑자기 터질 때 빠르게 원인을 좁히듯(S3 AccessDenied 403 급발생 - OAC·정책·KMS 30분 진단), 데이터 코드도 경고를 예외로 올려 조기에 차단하는 편이 결국 시간을 절약합니다.

# conftest.py 같은 곳에 넣어두면 좋습니다.
import pandas as pd

pd.options.mode.chained_assignment = "raise"

결론

SettingWithCopyWarning은 단순한 “귀찮은 경고”가 아니라, pandas에서 뷰/복사 모호성 + 체인 인덱싱이 만나 생기는 신호입니다. 해결은 어렵지 않습니다.

  • 원본을 수정할 땐: .loc[행조건, 열] = 값
  • 사본을 만들 땐: .copy()
  • 파이프라인을 깔끔하게: assign, pipe, transform
  • 테스트에서 조기 차단: pd.options.mode.chained_assignment = "raise"

이 원칙만 지켜도 “돌아가긴 하는데 가끔 결과가 이상한” 데이터 버그를 크게 줄일 수 있습니다.