Published on

pandas SettingWithCopyWarning 완전 해결 5패턴

Authors

서로 다른 데이터 전처리 파이프라인을 합치다 보면 pandasSettingWithCopyWarning은 거의 필연적으로 마주칩니다. 문제는 “경고가 떠도 결과는 얼추 맞는 것처럼 보인다”는 점입니다. 하지만 이 경고는 값이 실제로 원본에 반영되지 않거나, 반대로 예상치 못한 객체에 쓰기가 발생하는 상황을 알려줍니다.

이 글에서는 경고를 억지로 끄는 대신, 실무에서 반복적으로 쓰는 완전 해결 5패턴을 정리합니다. 각 패턴은 “왜 안전한지”와 “어떤 상황에서 선택해야 하는지”까지 포함합니다.

참고로 원인과 기본 해결을 빠르게 훑고 싶다면 내부 글인 Pandas SettingWithCopyWarning 원인·해결 5분도 함께 보시면 좋습니다.

SettingWithCopyWarning이 뜨는 진짜 이유

핵심은 pandas에서 df[...]항상 복사본을 반환하는 게 아니라, 상황에 따라 view(원본을 바라보는 얕은 참조) 또는 copy(독립된 복사본) 가 될 수 있다는 점입니다.

특히 아래처럼 체인 인덱싱을 하면 경고가 잘 뜹니다.

import pandas as pd

df = pd.DataFrame({
    "team": ["A", "A", "B"],
    "score": [10, 20, 30],
    "active": [True, False, True],
})

# 체인 인덱싱(경고 유발 가능)
df[df["team"] == "A"]["score"] = 999

위 코드는 “필터링 결과”가 view인지 copy인지 불명확합니다. 그래서 pandas가 “이거 원본에 쓰는 거 맞아?”라고 경고합니다.

이제부터는 이 경고를 논리적으로 제거하는 패턴 5개를 소개합니다.

패턴 1) 조건 필터 후 값 대입은 무조건 .loc로 끝낸다

가장 표준적이고, 가장 추천되는 방식입니다.

언제 쓰나

  • df의 일부 행/열을 조건으로 골라 원본 df를 수정하려는 경우
  • 체인 인덱싱이 보이면 거의 무조건 이 패턴으로 바꿉니다

예제

mask = df["team"] == "A"

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

왜 안전한가

.loc[행조건, 열]은 “원본 df의 특정 위치에 대입”이라는 의도가 명확합니다. view/copy 모호성이 사라져 경고도 사라지고, 결과도 예측 가능합니다.

여러 열을 한 번에 바꿀 수도 있습니다.

cols = ["score", "active"]
df.loc[df["team"] == "A", cols] = [999, True]

패턴 2) 부분집합을 따로 쓰고 싶다면 .copy()를 ‘의도적으로’ 만든다

필터링한 결과를 별도 데이터프레임으로 두고, 그걸 독립적으로 가공하고 싶을 때가 많습니다. 이때는 명시적으로 copy를 만들어 경고를 원천 차단합니다.

언제 쓰나

  • subset을 만들어 여러 변환을 하고, 원본 df와 분리해서 관리하고 싶은 경우
  • 중간 결과를 캐시하거나, 함수에서 안전하게 반환해야 하는 경우

예제

subset = df.loc[df["team"] == "A"].copy()
subset["score"] = subset["score"] * 10

왜 안전한가

subset은 독립 객체라는 의도가 명확합니다. 이후 subset[...] = ...는 원본과 무관하게 동작합니다.

주의

.copy()는 메모리를 추가로 사용합니다. 대용량 데이터에서는 필요한 구간에서만 쓰고, 불필요한 중간 복사는 피하는 게 좋습니다.

패턴 3) 파이프라인은 assign으로 “대입 없이” 컬럼을 생성한다

체인 인덱싱이 자주 나오는 이유 중 하나는 “필터링하고, 그 결과에 컬럼을 추가/수정”하는 흐름이 많기 때문입니다. 이때 assign을 쓰면 대입 연산 자체를 줄여 경고를 회피하면서도 코드가 함수형으로 깔끔해집니다.

언제 쓰나

  • df를 직접 수정하기보다, 변환된 새 DataFrame을 반환하는 스타일을 선호할 때
  • pipe와 함께 읽기 좋은 전처리 파이프라인을 만들고 싶을 때

예제

result = (
    df.loc[df["active"]]
      .assign(score=lambda x: x["score"] * 2)
)

왜 안전한가

assign은 “새 컬럼을 만들어 반환”하는 방식이라, 모호한 view에 대한 in-place 대입이 줄어듭니다. 또한 lambda x:로 참조하는 대상이 명확합니다.

여러 컬럼을 한 번에 만들 수 있습니다.

result = df.assign(
    score2=lambda x: x["score"] * 2,
    grade=lambda x: pd.cut(x["score"], bins=[0, 15, 25, 100], labels=["L", "M", "H"])
)

패턴 4) where / mask / np.where로 조건부 업데이트를 ‘벡터화’한다

조건에 따라 값을 바꾸는 경우 .loc도 좋지만, 특정 상황에서는 where/mask가 더 자연스럽습니다. 이 방식은 “어떤 값을 유지하고 어떤 값을 대체하는지”가 한 줄로 드러나서 유지보수에 유리합니다.

언제 쓰나

  • 특정 조건에서만 값을 바꾸고, 나머지는 기존 값을 유지해야 할 때
  • 결측치 처리나 클리핑 같은 “조건부 치환”이 잦을 때

예제 1: mask로 조건에 맞는 곳만 바꾸기

# team이 A인 행의 score만 0으로 치환
new_score = df["score"].mask(df["team"] == "A", 0)

df = df.assign(score=new_score)

예제 2: where로 조건을 만족하면 유지, 아니면 대체

# active가 True인 score는 유지, False면 -1
new_score = df["score"].where(df["active"], -1)

df = df.assign(score=new_score)

예제 3: np.where로 2분기 조건

import numpy as np

df = df.assign(
    score=np.where(df["team"] == "A", df["score"] + 100, df["score"])
)

왜 안전한가

이 패턴은 대입이 “새 시리즈 생성 후 재할당” 형태로 이뤄져, view/copy 모호성이 줄어듭니다. 특히 assign과 결합하면 경고가 거의 끼어들 여지가 없습니다.

패턴 5) 그룹/정렬/슬라이스 후 수정은 “원본에 다시 반영”하거나 “처음부터 원본에서 .loc”로 처리한다

실무에서 가장 까다로운 케이스는 다음과 같습니다.

  • sort_values 후 일부만 슬라이스
  • groupby 후 특정 그룹만 뽑아 수정
  • dropnaquery로 뽑은 결과에 수정

이때 “부분집합을 수정해서 원본에도 반영되길 기대”하면 경고가 터지기 쉽습니다.

5-A) 정렬 후 상위 N개만 원본에 반영: 인덱스로 다시 .loc

# score 상위 2개 행에 bonus 부여
idx = df.sort_values("score", ascending=False).head(2).index

df.loc[idx, "score"] = df.loc[idx, "score"] + 5

핵심은 “부분집합을 수정”이 아니라, 부분집합의 인덱스를 얻고 원본에 .loc로 반영하는 것입니다.

5-B) groupby 결과로 특정 그룹만 수정: 그룹 키로 마스크 생성

# team A의 score만 2배
mask = df["team"].eq("A")
df.loc[mask, "score"] *= 2

그룹 연산이 복잡해지면 transform을 활용해 원본 크기와 동일한 시리즈를 만든 뒤 대입하는 것도 안전합니다.

# 팀별 평균보다 낮으면 1, 아니면 0
team_mean = df.groupby("team")["score"].transform("mean")
df = df.assign(is_below_mean=(df["score"] < team_mean).astype(int))

5-C) query 결과를 수정해야 한다면: .copy()로 분리하거나 .loc로 원본 수정

# 원본 수정이 목적이면 query로 인덱스 확보 후 loc
idx = df.query("team == 'A'").index

df.loc[idx, "active"] = True

또는 결과를 독립적으로 다룰 거면:

subset = df.query("team == 'A'").copy()
subset["active"] = True

왜 안전한가

정렬/그룹/쿼리로 얻은 결과는 내부적으로 copy가 되기도, view가 되기도 합니다. 따라서 “원본을 수정할 의도”라면 **원본 기준의 인덱스와 .loc**가 가장 확실합니다.

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

아래는 검색하면 자주 나오는 방식이지만, 근본 해결이 아닙니다.

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

이렇게 하면 경고가 사라져서 깔끔해 보이지만, 원인이 사라진 게 아니라 알림만 꺼진 것입니다. 특히 전처리 결과가 조용히 틀어질 수 있어, 운영 배치나 리포팅 파이프라인에서 치명적입니다.

디버깅 체크리스트: 어떤 패턴을 골라야 하나

  • 원본 df를 수정해야 한다: 패턴 1 또는 패턴 5
  • 필터 결과를 별도 객체로 안전하게 가공: 패턴 2
  • 함수형 파이프라인으로 새 DataFrame을 반환: 패턴 3
  • 조건부 치환/파생 컬럼 생성이 중심: 패턴 4

마무리: “의도를 코드로 고정”하면 경고는 사라진다

SettingWithCopyWarning은 귀찮은 경고가 아니라, pandas가 “지금 코드가 모호하다”고 알려주는 신호입니다. 해결의 핵심은 단순합니다.

  • 원본을 수정할 거면 df.loc[...] = ...로 의도를 고정
  • 분리해서 쓸 거면 .copy()로 독립성을 고정
  • 변환 파이프라인은 assign/transform/where로 대입을 줄여 고정

이 5패턴만 습관화해도 경고를 끄지 않고도 대부분의 케이스를 안정적으로 처리할 수 있습니다.

추가로 원인(뷰/복사, 체인 인덱싱)과 짧은 처방전을 먼저 보고 싶다면 Pandas SettingWithCopyWarning 원인·해결 5분을 함께 참고하세요.