Published on

pandas SettingWithCopyWarning 원인·해결 5가지

Authors

서로 다른 필터링/슬라이싱 결과에 값을 대입하다 보면 SettingWithCopyWarning을 자주 만나게 됩니다. 이 경고는 단순히 “귀찮은 메시지”가 아니라, 내가 수정한 값이 원본 DataFrame에 반영될 수도 있고 안 될 수도 있는 모호한 상태를 알려주는 신호입니다.

특히 데이터 전처리 파이프라인에서 이 경고를 무시하면, 일부 단계에서만 값이 반영되어 재현 어려운 품질 이슈로 이어질 수 있습니다. 이 글에서는 경고의 본질(뷰 vs 복사본), 대표적인 발생 패턴(원인), 그리고 실무에서 권장되는 해결책 5가지를 예제와 함께 정리합니다.

디버깅 관점에서 보면 SettingWithCopyWarning은 “원인 추적 가능한 경고”입니다. 운영 장애에서 원인을 로그로 추적하듯, 데이터 변형도 경고를 단서로 추적해야 합니다. 비슷한 접근법은 리눅스 OOM Kill 원인 추적 - dmesg·cgroup·journalctl 같은 글에서도 동일하게 중요합니다.

SettingWithCopyWarning이 뜨는 진짜 이유

pandas는 성능을 위해, 슬라이싱/필터링 결과를 원본의 뷰(view) 로 돌려주기도 하고, 새 객체(복사본, copy) 로 돌려주기도 합니다. 그런데 사용자가 다음처럼 “부분 집합을 만든 뒤 그 부분 집합에 대입”을 하면 pandas 입장에서는 난감합니다.

  • 이 부분 집합이 원본을 바라보는 뷰라면 원본이 바뀔 수 있음
  • 복사본이라면 원본은 안 바뀜
  • 어떤 경우인지 내부 상태와 연산 경로에 따라 달라질 수 있음

그래서 pandas는 안전하지 않은 대입 패턴을 만나면 SettingWithCopyWarning으로 경고합니다.

가장 흔한 재현 코드

import pandas as pd

pd.set_option("mode.chained_assignment", "warn")

df = pd.DataFrame({
    "name": ["a", "b", "c", "d"],
    "score": [10, 20, 30, 40],
    "group": ["x", "x", "y", "y"],
})

subset = df[df["group"] == "x"]
subset["score"] = subset["score"] + 100  # SettingWithCopyWarning 가능

이 코드의 의도는 “groupx인 행의 score를 올린다”인데, 실제로는 df가 바뀌지 않거나(복사본), 바뀌더라도(뷰) 코드가 불안정합니다.

원인 1) 체인 인덱싱(chained indexing)으로 대입

가장 대표적인 원인입니다.

# 나쁜 예: 두 번의 인덱싱이 체인으로 연결됨
df[df["group"] == "x"]["score"] = 999

df[조건] 결과가 뷰인지 복사본인지 불명확한데, 거기에 다시 ["score"]로 접근해 대입합니다. pandas는 이 패턴을 특히 위험하게 봅니다.

해결 1) .loc로 한 번에 선택 + 대입

대부분의 경우 정답에 가깝습니다. 행 선택과 열 선택을 한 번에 하고, 그 자리에 바로 대입합니다.

mask = df["group"] == "x"
df.loc[mask, "score"] = df.loc[mask, "score"] + 100
  • mask로 행을 고르고
  • "score" 열만 지정해
  • 원본 df에 직접 대입

이렇게 하면 경고가 사라지고, 의도한 대로 원본이 확실히 변경됩니다.

원인 2) 슬라이스 결과에 바로 대입

: 슬라이스도 상황에 따라 뷰/복사본이 될 수 있습니다.

part = df[:2]
part["score"] = 0  # 경고 가능

해결 2) 슬라이스를 “데이터셋”로 쓸 거면 .copy()를 명시

원본과 분리된 데이터로 다룰 목적이라면, 애초에 복사본임을 선언하는 게 안전합니다.

part = df[:2].copy()
part["score"] = 0  # 경고 없음, 원본 df는 그대로

실무 기준으로는 다음 규칙이 명확합니다.

  • 원본을 바꿀 거면 .loc[...] = ...
  • 원본과 독립적으로 쓸 거면 .copy()

원인 3) 중간 변수(부분 DataFrame)에 대입 후 원본 반영을 기대

아래 코드는 “부분집합을 수정하면 원본도 바뀌겠지”라는 기대를 만들지만, pandas는 이를 보장하지 않습니다.

subset = df[df["group"] == "y"]
subset["score"] *= 2  # 경고 가능

# 이후 df의 group==y score가 바뀌었다고 가정하면 위험

해결 3) 원본에 직접 대입하거나, assign으로 새 프레임 생성

(A) 원본에 직접 대입

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

(B) 원본을 불변처럼 다루고 새 DataFrame 만들기

불변 스타일로 파이프라인을 구성하면 경고도 줄고, 테스트도 쉬워집니다.

mask = df["group"] == "y"

df2 = df.assign(
    score=lambda d: d["score"].where(~mask, d["score"] * 2)
)
  • df2는 새 객체
  • df는 그대로
  • 조건부 변환을 where로 명확화

원인 4) inplace=True와 부분 선택을 섞어 쓰기

inplace=True 자체가 항상 경고를 유발하는 건 아니지만, 부분 선택 결과에 대해 inplace 변형을 시도하면 불명확성이 커집니다.

subset = df[df["group"] == "x"]
subset.drop(columns=["group"], inplace=True)  # 경고 또는 의도 불일치 가능

해결 4) inplace를 피하고, 반환값을 재할당

pandas에서도 점점 inplace 사용을 권장하지 않는 흐름이 강합니다. 반환값을 받는 방식이 더 예측 가능합니다.

subset = df[df["group"] == "x"].copy()
subset = subset.drop(columns=["group"])  # 명확

원본을 바꿔야 한다면 부분집합이 아니라 원본에 직접 적용합니다.

mask = df["group"] == "x"
df.loc[mask, "group"] = "X"  # 예시: 원본에 직접

원인 5) 함수 체이닝 중간에 []로 꺼내 대입

메서드 체이닝을 하다가 중간 결과에 []로 접근해 대입하면, 체인 인덱싱과 유사한 문제가 생깁니다.

(
    df
    .query("group == 'x'")
)["score"] = 1  # 경고 가능

해결 5) pipe로 변환 함수를 분리하고 .loc를 내부에서 사용

체이닝은 유지하되, “대입”은 항상 원본(혹은 명시적 복사본)에서 .loc로 수행되게 만듭니다.

def bump_score_x(d: pd.DataFrame) -> pd.DataFrame:
    d = d.copy()
    mask = d["group"] == "x"
    d.loc[mask, "score"] = d.loc[mask, "score"] + 100
    return d

out = (
    df
    .pipe(bump_score_x)
    .sort_values("score")
)

이 패턴의 장점:

  • 각 변환이 입력/출력이 명확
  • 경고가 줄어듦
  • 테스트 가능한 단위 함수로 분해 가능

경고를 “끄는 것”은 해결이 아니다

검색해보면 다음 설정으로 경고를 숨기는 방법이 나오지만, 이는 원인 제거가 아니라 증상 숨기기입니다.

import pandas as pd
pd.set_option("mode.chained_assignment", None)  # 경고 숨김(비추천)

실무에서 경고를 숨기면, 데이터가 일부만 바뀌는 버그가 조용히 들어갈 수 있습니다. 장애에서 500을 숨긴다고 문제가 해결되지 않는 것과 비슷합니다. 디버깅/원인 분석이 중요한 사례는 Next.js 14 서버액션 500·CSRF·캐시 꼬임 해결 같은 글에서도 같은 결론으로 이어집니다.

실전 체크리스트: 내 코드가 안전한지 빠르게 점검

  1. df[조건][열] = 값 형태가 보이면 바로 .loc[조건, 열] = 값으로 바꾸기
  2. 부분집합을 “독립 데이터”로 쓸 거면 ... .copy()를 습관화
  3. 중간 변수에 대입 후 원본 반영을 기대하지 말기
  4. inplace=True는 가능하면 피하고 반환값 재할당
  5. 체이닝 파이프라인에서는 대입 로직을 pipe 내부로 넣고 .loc 사용

마무리

SettingWithCopyWarning은 pandas가 “이 대입은 뷰인지 복사본인지 애매해서 결과를 보장 못 한다”고 알려주는 경고입니다. 해결의 핵심은 단순합니다.

  • 원본을 바꿀 때는 항상 .loc로 한 번에 선택 후 대입
  • 원본과 분리된 데이터를 만들려면 항상 .copy()로 의도를 명시

이 두 가지 원칙만 지켜도 경고의 대부분은 사라지고, 데이터 전처리 결과의 신뢰성이 크게 올라갑니다.