Published on

pandas SettingWithCopyWarning 완전 해결 7가지

Authors

서론

pandas를 쓰다 보면 한 번쯤은 반드시 마주치는 경고가 있습니다. 바로 SettingWithCopyWarning입니다. 많은 개발자가 이 경고를 “귀찮은 경고” 정도로 치부하고 pd.options.mode.chained_assignment = None 같은 방식으로 숨기곤 하지만, 이 경고는 데이터가 의도와 다르게 수정되지 않을 수 있음을 알려주는 중요한 신호입니다.

핵심 원인은 간단합니다.

  • 어떤 연산은 원본 DataFrame의 뷰(view) 를 반환할 수 있고
  • 어떤 연산은 복사본(copy) 을 반환할 수 있는데
  • 그 상태에서 “체인 인덱싱(chained indexing)”으로 값을 설정하면
  • pandas가 “이게 원본에 반영되는 건지, 복사본에만 반영되는 건지” 확신할 수 없어 경고를 냅니다.

이 글에서는 경고를 억지로 끄는 대신, 코드를 명확하고 안전하게 만드는 7가지 해결 패턴을 정리합니다. 더 넓은 배경/원리를 함께 보고 싶다면 pandas SettingWithCopyWarning 완전 해결법도 같이 참고하면 좋습니다.


1) 체인 인덱싱을 끊고 .loc로 한 번에 설정하기 (가장 정석)

가장 흔한 패턴은 아래처럼 “필터링 후 다시 컬럼 설정”을 체인으로 하는 경우입니다.

import pandas as pd

df = pd.DataFrame({
    "name": ["A", "B", "C"],
    "score": [50, 80, 90]
})

# ❌ 흔한 문제 코드
high = df[df["score"] >= 80]
high["grade"] = "A"  # SettingWithCopyWarning 가능

정석은 .loc[행조건, 열] = 값 형태로 한 번에 원본에 반영하는 것입니다.

# ✅ 원본 df에 안전하게 반영
mask = df["score"] >= 80
df.loc[mask, "grade"] = "A"

이 방식의 장점은 다음과 같습니다.

  • 원본에 반영되는지 여부가 명확
  • 중간 객체(view/copy) 문제 제거
  • 코드 리뷰/유지보수 시 의도가 분명

2) 필터링 결과를 “독립 데이터”로 쓸 거면 .copy()로 의도를 선언하기

경고가 뜨는 코드가 항상 “버그”는 아닙니다. 때로는 원본과 분리된 DataFrame을 만들고, 그 복사본을 가공하고 싶은 경우도 있습니다. 그럴 땐 .copy()로 명확히 선언하세요.

# ✅ 복사본을 만들어 이후 가공은 복사본에서만 수행
high = df[df["score"] >= 80].copy()
high["grade"] = "A"  # 경고 없음, 의도도 명확

실무 팁:

  • 분석 파이프라인에서 “원본 보존”이 중요하면 .copy()를 습관화
  • 메모리가 큰 데이터라면 .copy() 비용을 고려해 1번 패턴(.loc) 우선

3) .assign()으로 새 컬럼을 함수형으로 추가하기

assign은 새 컬럼을 추가할 때 체인 인덱싱 문제를 줄이고, 파이프라인 형태로 읽기 좋은 코드를 만들 수 있습니다.

high = (
    df[df["score"] >= 80]
      .copy()
      .assign(grade="A")
)

조건부 컬럼도 가능합니다.

import numpy as np

df2 = df.assign(
    grade=np.where(df["score"] >= 80, "A", "B")
)
  • assign은 “새로운 DataFrame을 반환”하는 성격이 강해 부작용이 적습니다.
  • 단, 원본을 직접 수정해야 한다면 1번의 .loc가 더 직관적입니다.

4) .query() + .loc 조합으로 가독성 높이기

조건이 복잡해지면 df[df[...]]가 읽기 어려워지고 체인 인덱싱이 섞이기 쉽습니다. 이때 .query()를 쓰면 조건을 문자열로 분리해 가독성을 개선할 수 있습니다.

# 조건으로 행을 찾고, 원본 df에 반영
mask = df.eval("score >= 80")
df.loc[mask, "grade"] = "A"

또는 결과를 별도로 쓸 거면:

high = df.query("score >= 80").copy()
high["grade"] = "A"

주의:

  • .query()는 컬럼명에 공백/특수문자가 있으면 백틱(`) 처리 필요
  • 성능은 케이스에 따라 다르므로 대용량에서는 벤치마크 권장

5) inplace에 기대지 말고 “반환값 기반”으로 작성하기

pandas는 inplace=True가 항상 메모리/성능 이득을 보장하지 않습니다. 또한 “중간 슬라이스 객체에 inplace 적용” 같은 패턴은 경고/오해를 키웁니다.

나쁜 예(의도 불명확 + 경고 유발 가능):

high = df[df["score"] >= 80]
high.rename(columns={"score": "points"}, inplace=True)  # 권장 X

좋은 예(반환값을 명확히 받기):

high = df[df["score"] >= 80].copy()
high = high.rename(columns={"score": "points"})

원본을 바꾸고 싶으면 처음부터 원본에 적용하세요.

df = df.rename(columns={"score": "points"})

6) 그룹/정렬/필터 후 값 설정은 transform, map, merge로 우회하기

SettingWithCopyWarning은 “필터한 결과에 다시 대입”에서 자주 터집니다. 특히 groupby 결과를 일부 행에만 대입하려고 할 때 위험합니다. 이때는 “계산 결과를 원본과 같은 인덱스로 맞춘 뒤” 대입하는 방식이 안전합니다.

예: 그룹 평균 대비 차이를 컬럼으로 추가

df = pd.DataFrame({
    "team": ["X", "X", "Y", "Y"],
    "score": [10, 30, 20, 40]
})

# ✅ transform은 원본과 동일한 길이/인덱스를 반환
team_mean = df.groupby("team")["score"].transform("mean")
df["diff"] = df["score"] - team_mean

예: 매핑 테이블로 등급 부여(merge/map)

grade_map = {10: "C", 20: "B", 30: "A", 40: "A"}
df["grade"] = df["score"].map(grade_map)

이런 방식은 “슬라이스에 대입” 자체를 피하기 때문에 경고를 근본적으로 줄입니다.


7) 경고를 끄는 대신 “재현 가능한 테스트”로 안전성 확보하기

마지막은 약간 다른 방향입니다. SettingWithCopyWarning을 없애는 것이 목적이 아니라, 데이터 변경이 의도대로 반영되는지를 테스트로 고정하는 방법입니다.

예를 들어, 필터링 후 특정 컬럼을 바꾸는 로직이 있다면 다음을 테스트합니다.

import pandas as pd

def mark_high(df: pd.DataFrame) -> pd.DataFrame:
    df = df.copy()
    df.loc[df["score"] >= 80, "grade"] = "A"
    df.loc[df["score"] < 80, "grade"] = "B"
    return df


def test_mark_high():
    df = pd.DataFrame({"score": [50, 80]})
    out = mark_high(df)

    assert out.loc[0, "grade"] == "B"
    assert out.loc[1, "grade"] == "A"
    # 원본 불변도 보장
    assert "grade" not in df.columns

이 접근의 장점:

  • 경고가 “안 뜨게” 만드는 수준을 넘어, 실제 결과의 정확성을 보장
  • 리팩터링/성능 개선 시에도 동작 회귀(regression)를 막아줌

추가로, 파이썬/패키지 환경 문제로 경고/에러가 꼬여 보일 때는 Python 3.12에서 pkg_resources 에러 근본 해결처럼 런타임 환경을 먼저 정리하는 것도 도움이 됩니다.


실전에서 자주 나오는 패턴별 처방전

패턴 A: df[df['cond']]['col'] = ...

  • 처방: 무조건 .loc[cond, 'col'] = ...
cond = df["score"] >= 80
df.loc[cond, "grade"] = "A"

패턴 B: 필터링 결과를 리포팅/시각화용으로 가공

  • 처방: .copy() 후 가공
report_df = df.query("score >= 80").copy()
report_df["rank"] = report_df["score"].rank(ascending=False)

패턴 C: 파이프라인이 길어서 어디서 경고가 나는지 모르겠음

  • 처방: 중간 산출물을 변수로 끊고, 원본 수정은 .loc로만
filtered = df[df["score"] >= 80]
# 여기서는 읽기 전용으로만 사용

mask = df["score"] >= 80
# 수정은 원본에만

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

결론: “경고 제거”가 아니라 “의도 명확화”가 목표

SettingWithCopyWarning은 pandas가 까다로워서가 아니라, 개발자가 원본을 수정하려는지, 복사본을 만들려는지가 코드에서 애매할 때 발생합니다. 따라서 해결책도 단순합니다.

  • 원본을 바꿀 거면: df.loc[조건, 컬럼] = 값
  • 분리된 결과를 만들 거면: ... .copy()
  • 파이프라인을 함수형으로 만들 거면: assign, transform, map 활용
  • 그리고 중요한 로직은 테스트로 고정

운영 환경에서 “경고를 끄는” 선택은 문제를 숨길 뿐, 데이터 품질 이슈를 키울 수 있습니다. 마치 운영에서 성능 경고를 무시하면 나중에 더 큰 장애로 번지는 것처럼요. 비슷한 맥락의 ‘경고를 추적해 근본 원인을 찾는’ 접근은 Spring Boot 3.2 HikariCP 커넥션 누수 경고 추적법 같은 글에서도 동일하게 중요합니다.

이제부터는 SettingWithCopyWarning을 “없애야 할 잡음”이 아니라, 코드를 더 안전하게 만드는 가이드로 활용해 보세요.