Published on

pandas SettingWithCopyWarning 완전 해결법 8가지

Authors

서로 다른 분석 단계에서 필터링한 뒤 컬럼을 수정했는데 값이 반영되지 않거나, 어느 날부터 SettingWithCopyWarning이 갑자기 쏟아지기 시작했다면 대부분 원인은 하나입니다. 체인 인덱싱(chained indexing) 때문에 pandas가 “지금 수정한 게 원본인지, 임시 뷰(view)인지 확신할 수 없다”고 경고하는 상황입니다.

이 경고는 단순히 귀찮은 로그가 아니라, 실제로 수정이 유실될 수 있는 버그 가능성을 알려줍니다. 따라서 목표는 “경고를 끄기”가 아니라, 할당(assign) 경로를 단일화하고 복사/뷰를 의도적으로 선택하는 것입니다.

아래에서는 SettingWithCopyWarning이 발생하는 대표 패턴을 먼저 재현하고, 이후 완전 해결법 8가지를 실전 코드와 함께 정리합니다.

SettingWithCopyWarning이 뜨는 근본 원인

pandas에서 다음 형태가 대표적인 위험 신호입니다.

  • df[mask]["col"] = ...
  • df["col"][mask] = ...
  • df[df["a"] > 0]["b"] = ...

이런 코드는 중간 결과가 원본의 뷰인지 복사본인지 상황에 따라 달라질 수 있습니다. pandas는 안전하지 않다고 판단하면 경고를 띄웁니다.

경고 재현 예시

import pandas as pd

df = pd.DataFrame({
    "group": ["A", "A", "B", "B"],
    "x": [1, 2, 3, 4],
    "y": [10, 20, 30, 40],
})

# 체인 인덱싱: df[mask]가 임시 객체일 수 있음
mask = df["group"] == "A"
df[mask]["y"] = 999  # SettingWithCopyWarning 가능

이 코드는 경고가 뜰 수 있고, 더 중요한 건 원본 df의 y가 실제로 바뀌지 않을 수도 있다는 점입니다.

해결법 1) .loc로 한 번에 선택+할당하기 (정석)

가장 권장되는 해결책입니다. 행 선택과 열 선택, 그리고 할당을 단일 연산으로 묶어 pandas가 모호함 없이 처리하게 만듭니다.

mask = df["group"] == "A"
df.loc[mask, "y"] = 999
  • 장점: 가장 명확하고 빠르며, 팀 코드 스타일로 강제하기 좋습니다.
  • 팁: 복수 컬럼도 가능
df.loc[mask, ["y", "x"]] = [999, 0]

해결법 2) .copy()로 “복사해서 수정할 것”을 명시하기

필터링 결과를 별도 데이터프레임으로 다루려면, 애초에 복사본을 만들어 경고 자체가 의미 없게 만드는 것이 좋습니다.

sub = df[df["group"] == "A"].copy()
sub["y"] = 999
  • 장점: 의도가 분명함(원본을 수정하지 않겠다는 선언)
  • 단점: 데이터가 크면 메모리 비용 증가

언제 .copy()가 특히 유용한가

  • 전처리 파이프라인에서 중간 산출물을 독립적으로 변형할 때
  • 조인/머지 전후로 스키마를 크게 바꿀 때

머지 이후 예상치 못한 행 수 폭증이 함께 나타나면, 경고 해결과 별개로 키 중복 진단도 같이 점검하는 게 좋습니다. 관련해서는 pandas merge 후 행 수 폭증? 중복키 진단·해결도 함께 참고하면 원인 분리가 빨라집니다.

해결법 3) 체인 인덱싱을 “두 단계”로 나누지 말고, 변수로 잡아 .loc에 재사용

체인 인덱싱이 습관처럼 나오면, 중간 결과를 변수로 받되 할당은 원본에 .loc 하도록 패턴을 고정하세요.

mask = (df["group"] == "A") & (df["x"] >= 2)

# 좋은 패턴: mask는 분리하되, 할당은 loc로
idx = df.index[mask]
df.loc[idx, "y"] = df.loc[idx, "y"] * 10
  • 장점: 조건식이 복잡해도 읽기 쉬움
  • 포인트: 인덱스를 추출해도 결국 할당은 df.loc[...] = ...로 끝낸다

해결법 4) .assign()으로 새 데이터프레임을 반환하는 함수형 스타일

원본을 직접 수정하지 않고, 새 데이터프레임을 반환하는 방식은 SettingWithCopyWarning을 구조적으로 줄입니다.

out = (
    df
    .assign(y=lambda d: d["y"].where(d["group"] != "A", 999))
)
  • 장점: 체인 인덱싱 대신 “계산 결과 컬럼 생성”으로 우회
  • 단점: 익숙하지 않은 팀에서는 가독성 논쟁이 있을 수 있음

.where / .mask 조합도 자주 쓰는 패턴

out = df.copy()
out["y"] = out["y"].mask(out["group"] == "A", 999)

해결법 5) .query() + .loc로 가독성 올리기 (할당은 반드시 .loc)

query는 필터 조건을 문자열로 표현해 가독성을 높일 수 있습니다. 다만 query 결과에 바로 할당하지 말고, 인덱스를 얻어 .loc로 반영하세요.

idx = df.query("group == 'A' and x >= 2").index
df.loc[idx, "y"] = 999
  • 장점: 조건이 길어질수록 읽기 쉬움
  • 주의: df.query(...)["y"] = ... 형태는 다시 체인 인덱싱이 될 수 있음

해결법 6) 그룹별/조건별 값 갱신은 transform / map으로 “정렬된 벡터 연산”으로 끝내기

특정 그룹에 대한 값을 계산해 넣는 상황에서, 행 필터링 후 대입을 반복하면 경고뿐 아니라 성능도 나빠지기 쉽습니다. 이럴 땐 transform이나 map으로 전체 벡터를 한 번에 생성하세요.

예: 그룹별 평균으로 표준화한 값 만들기

df["y_centered"] = df["y"] - df.groupby("group")["y"].transform("mean")

예: 그룹별 룰 테이블을 map으로 적용

rule = {"A": 999, "B": 0}
df["y2"] = df["group"].map(rule).fillna(df["y"])
  • 장점: SettingWithCopyWarning 회피 + 빠름
  • 포인트: “부분집합 수정” 대신 “전체 컬럼 생성”으로 접근

해결법 7) 슬라이싱 후 수정이 필요하면, 슬라이스를 명시적으로 복사하거나 iloc로 원본을 직접 찍기

특히 다음처럼 정렬/슬라이스 후 일부 범위를 수정하는 코드에서 경고가 자주 나옵니다.

sub = df.sort_values("x")[:2]
sub["y"] = 1  # 경고 가능

의도에 따라 둘 중 하나로 정리하세요.

(A) 부분집합은 독립 객체로 다룬다

sub = df.sort_values("x")[:2].copy()
sub["y"] = 1

(B) 원본의 특정 행 위치를 직접 수정한다

df2 = df.sort_values("x")
idx = df2.index[:2]
df.loc[idx, "y"] = 1
  • 핵심: “정렬 결과의 앞 2개”를 수정하려면, 결국 원본의 인덱스를 찾아 .loc로 수정해야 안전합니다.

해결법 8) 경고를 끄지 말고, 재현 가능한 테스트와 옵션으로 “발생 지점”을 고정하기

SettingWithCopyWarning을 근본적으로 없애려면, 팀/프로젝트에서 경고를 빨리 발견하고 수정하도록 만드는 게 중요합니다.

(A) pandas 옵션으로 경고를 예외처럼 다루기

환경에 따라 경고가 묻히면, 운영/배치에서 데이터 품질 이슈로 번질 수 있습니다. 개발 환경에서는 경고를 더 공격적으로 노출시키는 편이 낫습니다.

import pandas as pd

# 경고 동작을 더 엄격하게(환경에 따라 동작은 달라질 수 있음)
pd.options.mode.chained_assignment = "raise"

mask = df["group"] == "A"
# 여기서 문제가 있으면 Warning이 아니라 예외로 터져서 위치를 고정할 수 있음
# df[mask]["y"] = 999
  • 장점: CI에서 조기 차단 가능
  • 단점: 기존 레거시 코드가 많으면 한 번에 터질 수 있으니 단계적 적용 권장

(B) “원본이 바뀌어야 하는지”를 테스트로 명확히 하기

예를 들어 원본 df가 바뀌어야 하는 로직이라면, 아래처럼 간단한 단언을 두면 경고가 숨어도 문제를 빨리 잡을 수 있습니다.

import pandas as pd

df = pd.DataFrame({"group": ["A", "B"], "y": [10, 20]})
mask = df["group"] == "A"

df.loc[mask, "y"] = 999

assert df.loc[0, "y"] == 999

데이터 파이프라인을 GitHub Actions로 돌리면서 이런 류의 미묘한 실패를 잡고 있다면, 캐시/경로 문제로 재현이 흔들리는 경우도 있습니다. 그런 경우엔 GitHub Actions 캐시 미스 - 키·경로 디버깅 실전처럼 실행 환경을 안정화하는 팁도 도움이 됩니다.

자주 묻는 질문: 그냥 경고를 무시하거나 끄면 안 되나

SettingWithCopyWarning을 무시하면 위험한 이유는 두 가지입니다.

  1. 원본이 수정되지 않을 수 있음: 분석 결과가 조용히 틀어집니다.
  2. 버전/상황에 따라 동작이 달라질 수 있음: 오늘은 우연히 반영되다가 내일은 안 될 수 있습니다.

정말로 “부분집합만 수정하고 원본은 안 건드릴 것”이라면, 경고를 끄는 대신 .copy()로 의도를 코드로 표현하는 편이 훨씬 안전합니다.

체크리스트: SettingWithCopyWarning을 영구 차단하는 팀 규칙

  • 할당은 기본적으로 df.loc[rows, cols] = value만 허용
  • 필터링 결과를 별도 객체로 다룰 땐 ... .copy()를 습관화
  • df[mask][col] = ... 형태는 코드리뷰에서 즉시 수정
  • 복잡한 조건 갱신은 where/mask/assign/transform로 벡터화
  • 개발 환경에서 pd.options.mode.chained_assignment = "raise"로 조기 발견

결론

SettingWithCopyWarning은 pandas가 친절하게 알려주는 “잠재 버그 경보”입니다. 해결의 핵심은 단순합니다.

  • 원본을 수정할 거면 .loc로 한 번에 할당
  • 원본과 분리할 거면 .copy()로 복사 의도를 명시

여기에 assign/mask/transform 같은 벡터화 패턴을 섞으면, 경고를 없애는 수준을 넘어 코드가 더 빠르고 예측 가능해집니다.