Published on

pandas SettingWithCopyWarning 완전 해결 5가지

Authors

서브셋을 만든 뒤 컬럼을 수정했는데 값이 안 바뀌거나, 어떤 경우에는 바뀌는 것처럼 보이다가 나중에 깨진 경험이 있다면 SettingWithCopyWarning을 이미 만난 겁니다. 이 경고는 단순한 “귀찮은 메시지”가 아니라, 내 코드가 원본 DataFrame을 수정하는지, 임시 객체(복사본)를 수정하는지 pandas가 확신하지 못한다는 신호입니다.

핵심은 다음 한 줄입니다.

  • df[...]로 필터링한 결과가 view(원본을 바라보는 창) 로 남을 수도 있고, copy(독립 복사본) 가 될 수도 있습니다.
  • 그 상태에서 subset['col'] = ... 같은 연쇄 인덱싱(chained indexing) 을 하면, “원본이 바뀌었다고 믿어도 되는가?”가 불명확해집니다.

이 글에서는 경고를 억지로 끄지 않고, 실무에서 재발하지 않게 만드는 완전 해결 5가지 패턴을 정리합니다.

SettingWithCopyWarning이 뜨는 구조 이해하기

대표적인 재현 코드는 아래처럼 생겼습니다.

import pandas as pd

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

subset = df[df["age"] >= 30]
subset["score"] = subset["score"] * 10  # SettingWithCopyWarning 가능

문제는 subset어떤 경우에는 view, 어떤 경우에는 copy일 수 있다는 점입니다. pandas 내부 최적화, 블록 매니저 상태, 연산 과정 등에 따라 달라질 수 있어 “항상 동일하게 동작한다”는 보장이 없습니다.

따라서 해결책은 한 가지입니다.

  • 원본을 수정할 거면 원본에 한 번에 명시적으로 할당
  • 복사본을 수정할 거면 명시적으로 .copy()로 끊기

이제부터 5가지 해결 패턴을 소개합니다.

해결 1) .loc로 한 번에 조건+컬럼 지정해 원본에 대입

가장 정석이자 추천 1순위입니다. 조건 필터와 대상 컬럼을 한 번의 인덱싱으로 지정합니다.

mask = df["age"] >= 30

df.loc[mask, "score"] = df.loc[mask, "score"] * 10

포인트:

  • df.loc[rows, cols] = ... 형태는 pandas가 “원본 df에 대입한다”는 사실을 명확히 알 수 있습니다.
  • subset = df[mask]처럼 중간 객체를 만들지 않으니 경고가 사라집니다.

여러 컬럼을 동시에 수정할 때도 동일합니다.

mask = df["age"] >= 30

df.loc[mask, ["score", "age"]] = df.loc[mask, ["score", "age"]].mul([10, 1])

해결 2) 서브셋을 쓸 거면 .copy()로 명시적 복사

분석 파이프라인에서 “원본은 건드리지 않고, 필터링된 테이블을 따로 가공”하는 경우가 많습니다. 이때는 경고를 회피하는 수준이 아니라, 의도를 코드로 못 박아야 합니다.

subset = df[df["age"] >= 30].copy()
subset["score"] = subset["score"] * 10

포인트:

  • .copy()는 비용이 있습니다(메모리/시간). 하지만 “원본 불변”이 요구되는 분석/모델링 단계라면 비용을 내고 확실성을 얻는 편이 낫습니다.
  • 반대로 원본을 수정해야 한다면 .copy()를 쓰면 안 됩니다. 그 순간부터는 원본과 분리됩니다.

해결 3) 연쇄 인덱싱 금지: df[mask][col] = ... 패턴을 제거

경고가 가장 자주 뜨는 형태가 바로 아래입니다.

# 나쁜 예: 연쇄 인덱싱
mask = df["age"] >= 30

df[mask]["score"] = 0

이 코드는 “df[mask]가 view인지 copy인지”가 불명확한데, 그 결과에 다시 ['score']로 접근해 대입합니다. 즉, pandas 입장에서는 “대입이 어디로 가는지”가 모호합니다.

바로 아래처럼 바꾸면 됩니다.

# 좋은 예: 단일 인덱싱(.loc)
mask = df["age"] >= 30

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

실무 팁:

  • 코드 리뷰 규칙으로 ][ 형태의 대입을 금지해도 좋습니다.
  • 특히 전처리 코드가 길어질수록 이 패턴 하나가 데이터 품질 문제로 이어집니다.

해결 4) .assign.where/.mask로 “새 DataFrame”을 반환

원본을 직접 수정하기보다, 함수형 스타일로 새 DataFrame을 만들어 반환하면 경고를 구조적으로 피할 수 있습니다.

.assign으로 파이프라인 구성

subset = (
    df
    .loc[df["age"] >= 30]
    .assign(score=lambda x: x["score"] * 10)
)
  • .assign은 새 DataFrame을 반환합니다.
  • lambda x: 내부에서 x는 현재 단계의 DataFrame입니다.

.where 또는 .mask로 조건부 치환

원본을 유지하면서 특정 조건일 때만 값을 바꾸고 싶다면 .mask가 깔끔합니다.

mask = df["age"] >= 30

# age가 30 이상이면 score를 10배, 아니면 그대로

df["score"] = df["score"].mask(mask, df["score"] * 10)

이 방식은 SettingWithCopyWarning을 피하는 것뿐 아니라, “조건부 업데이트”를 한 줄로 표현해 가독성이 좋아집니다.

해결 5) 그룹 연산/정렬 후 대입은 transform/merge로 정렬 안정성까지 확보

경고의 출발점은 “뷰/복사”지만, 실무에서는 그룹 연산이나 정렬 후 서브셋 업데이트에서 문제가 더 크게 터집니다. 예를 들어 groupby 결과를 서브셋에 대입하려다 경고가 나거나, 인덱스 정렬이 어긋나 잘못된 행에 들어갈 수 있습니다.

groupby().transform()으로 원본 인덱스에 정렬된 결과 만들기

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

# 팀별 평균을 같은 길이로 원본 인덱스에 맞춰 반환

df["team_avg"] = df.groupby("team")["score"].transform("mean")
  • transform은 원본과 동일한 인덱스 길이를 보장하므로 대입이 안전합니다.
  • 중간 서브셋을 만들어 대입하는 실수를 줄입니다.

계산 결과를 별도 테이블로 만든 뒤 merge로 결합

agg = (
    df.groupby("team", as_index=False)
      .agg(team_avg=("score", "mean"))
)

df = df.merge(agg, on="team", how="left")
  • 집계 결과를 명시적으로 테이블로 만든 뒤 조인하면, 파이프라인이 명확해지고 디버깅이 쉬워집니다.

(비추천) 경고를 끄는 방법이 왜 위험한가

검색하면 아래 같은 코드가 자주 보입니다.

import pandas as pd
pd.options.mode.chained_assignment = None

이건 “경고를 숨기는 것”이지 “문제를 해결하는 것”이 아닙니다. 경고가 사라져도 원본이 갱신되지 않는 버그 또는 조건에 따라 동작이 달라지는 비결정성은 그대로 남습니다.

장애 대응 관점에서도, 원인이 로그에서 사라지면 추적이 훨씬 어려워집니다. 비슷한 맥락으로, 데이터 로딩 단계에서 인코딩 문제를 경고/예외 없이 넘기면 더 큰 데이터 품질 문제로 번집니다. 텍스트 데이터 처리 중 UnicodeDecodeError를 만난다면 아래 글도 함께 참고하세요.

실무 체크리스트: 경고를 “재발 방지”로 바꾸기

다음 규칙을 팀 규칙으로 삼으면 SettingWithCopyWarning은 거의 사라집니다.

  1. 대입은 무조건 .loc[행, 열] = 값 형태로 한다.
  2. 서브셋을 독립적으로 가공할 거면 필터 직후 .copy() 를 붙인다.
  3. df[mask][col] = ... 같은 연쇄 인덱싱 대입을 금지한다.
  4. 조건부 치환은 .mask/.where로 표현한다.
  5. 그룹 기반 파생 컬럼은 transform 또는 merge인덱스 정렬 안정성을 확보한다.

결론

SettingWithCopyWarning은 pandas가 까다로워서가 아니라, 데이터프레임 슬라이싱이 “뷰/복사” 사이에서 모호해질 수 있기 때문에 생깁니다. 해결의 본질은 간단합니다.

  • 원본을 바꿀 거면 .loc원본에 직접 대입
  • 서브셋을 바꿀 거면 .copy()명시적으로 분리

위 5가지 패턴을 습관화하면 경고는 사라지고, 더 중요한 “조용히 틀리는 전처리”도 함께 제거할 수 있습니다.