Published on

pandas SettingWithCopyWarning 완전 해결 6가지

Authors

서브셋을 만든 뒤 값을 대입했는데 SettingWithCopyWarning이 뜨면 대부분 이렇게 생각합니다. “경고니까 무시해도 되지 않나?” 하지만 이 경고는 뷰(view)인지 복사(copy)인지 불명확한 객체에 대입하고 있다는 신호이고, 상황에 따라 대입이 반영되지 않거나 일부만 반영되는 버그로 이어질 수 있습니다.

이 글에서는 경고가 발생하는 구조를 먼저 짚고, 실무에서 재현되는 케이스를 기준으로 완전히 해결하는 6가지 방법을 코드와 함께 정리합니다.

참고로 데이터 전처리에서 CSV 인코딩 이슈까지 같이 겪는 경우가 많습니다. CSV 로딩 단계에서 문제가 있다면 Python CSV UTF-8 깨짐·UnicodeDecodeError 5분 해결도 함께 확인해두면 좋습니다.

SettingWithCopyWarning이 발생하는 진짜 이유

pandas는 슬라이싱 결과가 원본의 뷰인지 새로운 복사본인지 내부 최적화/상황에 따라 달라질 수 있습니다. 대표적으로 아래 패턴이 위험합니다.

  • df[df["col"] == ...] 처럼 불리언 인덱싱으로 서브셋을 만든 뒤
  • 그 서브셋에 subset["new"] = ... 같은 체인 대입(chained assignment) 을 수행

pandas는 “이 대입이 원본 df에 반영될지 확신할 수 없다”는 의미로 경고를 냅니다.

최소 재현 예시

import pandas as pd

df = pd.DataFrame({
    "city": ["Seoul", "Seoul", "Busan"],
    "sales": [100, 200, 150]
})

seoul = df[df["city"] == "Seoul"]
seoul["sales"] = seoul["sales"] * 1.1  # SettingWithCopyWarning 가능

여기서 의도는 보통 둘 중 하나입니다.

  1. “서울 행만 뽑아서 그 결과를 수정하고 싶다” (원본 상관 없음)
  2. “원본 df에서 서울 행의 sales만 수정하고 싶다” (원본 반영 필요)

해결책은 결국 의도를 코드로 명확히 표현하는 것입니다.

해결 1) 원본을 수정하려면 loc로 한 번에 대입

원본 df를 수정하려는 목적이라면, 서브셋을 변수로 빼서 수정하지 말고 마스크와 loc를 사용해 단일 문장으로 대입하세요.

mask = df["city"] == "Seoul"
df.loc[mask, "sales"] = df.loc[mask, "sales"] * 1.1
  • 장점: 경고가 사라지고, 원본 반영이 100% 보장됩니다.
  • 팁: 여러 컬럼을 동시에 바꾸는 경우도 loc가 가장 안전합니다.
cols = ["sales"]
df.loc[mask, cols] = df.loc[mask, cols] * 1.1

해결 2) 새 DataFrame을 만들고 싶다면 .copy()로 의도를 고정

서브셋을 “독립적인 데이터”로 다루고 싶다면, 처음부터 복사본임을 명시하세요.

seoul = df.loc[df["city"] == "Seoul"].copy()
seoul["sales"] = seoul["sales"] * 1.1
  • 장점: seoul은 항상 독립 객체라서 경고가 사라집니다.
  • 주의: 이 경우 df는 바뀌지 않습니다. 원본 반영이 목적이면 해결 1을 쓰세요.

해결 3) 체인 인덱싱을 없애기 (특히 df[...][...] = ...)

가장 흔한 지뢰가 아래 형태입니다.

# 나쁜 예시: 두 번 인덱싱(체인)
df[df["city"] == "Seoul"]["sales"] = 0

이 코드는 “서브셋을 만든 다음 그 서브셋의 컬럼에 대입”이라서 pandas가 특히 싫어합니다.

반드시 loc로 합치세요.

mask = df["city"] == "Seoul"
df.loc[mask, "sales"] = 0

해결 4) assign/where/mask로 함수형 스타일로 쓰기

대입을 직접 하지 않고, 새 DataFrame을 반환하는 API를 쓰면 경고 자체를 구조적으로 피할 수 있습니다.

assign 예시

seoul = (
    df.loc[df["city"] == "Seoul"]
      .assign(sales=lambda x: x["sales"] * 1.1)
)

where 예시: 조건에 따라 값 유지/교체

mask = df["city"] == "Seoul"
df["sales"] = df["sales"].where(~mask, df["sales"] * 1.1)
  • 장점: 체인 대입이 아니라서 안전합니다.
  • 장점: 파이프라인 구성(pipe)과도 궁합이 좋습니다.

해결 5) 그룹별 처리 후 원본에 안전하게 반영 (transform/map)

경고는 “슬라이스 후 대입”에서 자주 발생하지만, 실무에서는 그룹별 계산을 하다가도 비슷한 실수를 합니다.

예: 도시별 평균 대비 비율 컬럼을 만들고 싶다.

# 안전한 방식: transform은 원본 인덱스와 정렬이 맞는다
mean_by_city = df.groupby("city")["sales"].transform("mean")
df["sales_ratio"] = df["sales"] / mean_by_city

또는 매핑 테이블이 있을 때 map을 사용하면 안전합니다.

rate = {"Seoul": 1.1, "Busan": 1.0}
df["adj_sales"] = df["sales"] * df["city"].map(rate).fillna(1.0)
  • 포인트: transform/map원본과 인덱스 정렬이 맞는 결과를 만들기 때문에, 불필요한 슬라이스 대입을 줄여줍니다.

해결 6) 경고를 “숨기지 말고” 개발 단계에서 실패로 만들기

SettingWithCopyWarning을 무시하도록 설정하는 글도 종종 보이지만, 장기적으로는 위험합니다. 대신 개발/테스트 단계에서 아예 예외로 만들어 조기에 잡는 편이 안전합니다.

import pandas as pd

pd.options.mode.chained_assignment = "raise"  # 'warn' 또는 None 대신

이렇게 하면 경고가 발생할 상황에서 예외가 나서, 파이프라인/배치 작업에서 “조용히 잘못된 결과”가 쌓이는 것을 막습니다.

  • 운영 환경에서까지 무조건 raise가 정답은 아닐 수 있지만
  • 최소한 ETL/분석 코드가 커질수록 테스트 환경에서는 강력 추천입니다.

자주 하는 오해와 체크리스트

오해 1) .loc만 쓰면 무조건 해결된다?

.loc는 “원본에 대한 단일 대입”을 만들 때 강력하지만, 이미 만들어진 서브셋 객체가 뷰인지 복사인지 애매한 상태라면, 그 서브셋에 .loc를 쓰는 것만으로는 의도가 해결되지 않을 수 있습니다.

subset = df[df["city"] == "Seoul"]
subset.loc[:, "sales"] = 0  # 여전히 경고 가능

이 경우는 해결 2처럼 처음부터 .copy()로 고정하거나, 해결 1처럼 원본에 바로 대입하세요.

오해 2) 경고가 떠도 값은 바뀌던데?

경고가 항상 “즉시 버그”를 의미하진 않습니다. 문제는 데이터 크기/정렬/내부 최적화에 따라 결과가 달라질 수 있다는 점입니다. 즉, 오늘은 되는데 내일은 안 될 수 있습니다.

빠른 체크리스트

  • 원본을 바꾸려는가?
    • 예: df.loc[mask, col] = ...
  • 서브셋을 독립적으로 쓰려는가?
    • 예: subset = df.loc[mask].copy()
  • df[...][...] = ... 형태가 있는가?
    • 있다면 즉시 loc로 합치기
  • 그룹별 계산 후 병합/대입이 필요한가?
    • transform, map으로 인덱스 정렬 유지
  • 배치/파이프라인에서 조용히 틀릴 위험이 큰가?
    • pd.options.mode.chained_assignment = "raise" 고려

실무 예제: 필터링 후 파생 컬럼 추가, 그리고 원본 반영

요구사항: 서울인 행에만 vip를 만들고, sales를 10% 올린다.

잘못된 예시

seoul = df[df["city"] == "Seoul"]
seoul["vip"] = True
seoul["sales"] = seoul["sales"] * 1.1

정답(원본 반영)

mask = df["city"] == "Seoul"

df.loc[mask, "vip"] = True

df.loc[mask, "sales"] = df.loc[mask, "sales"] * 1.1

정답(서브셋만 필요)

seoul = df.loc[df["city"] == "Seoul"].copy()
seoul["vip"] = True
seoul["sales"] = seoul["sales"] * 1.1

마무리

SettingWithCopyWarning은 “pandas가 까다롭다” 수준의 경고가 아니라, 데이터 파이프라인의 재현성과 신뢰성에 직결되는 신호입니다. 핵심은 단 하나입니다.

  • 원본을 바꿀 거면 loc로 한 번에
  • 원본과 분리할 거면 .copy()로 명확히

데이터 전처리에서 자주 함께 등장하는 파일 인코딩/CSV 로딩 이슈까지 한 번에 정리하고 싶다면 Python CSV UTF-8 깨짐·UnicodeDecodeError 5분 해결도 같이 읽어두면, pandas 작업의 삽질 포인트를 크게 줄일 수 있습니다.