Published on

pandas SettingWithCopyWarning 완전 해결 가이드

Authors

서브셋을 만든 뒤 컬럼을 수정했는데 SettingWithCopyWarning이 뜨면, 많은 사람이 pd.options.mode.chained_assignment = None 같은 방식으로 경고를 숨깁니다. 하지만 이 경고는 단순한 소음이 아니라 내가 수정한 값이 원본에 반영될 수도, 안 될 수도 있는 애매한 상태를 알려주는 신호입니다. 즉, 경고를 무시하면 테스트에서는 우연히 맞고 운영에서는 조용히 틀릴 수 있습니다.

이 글은 SettingWithCopyWarning을 “안 뜨게” 만드는 요령이 아니라, 왜 뜨는지 구조적으로 이해하고, 코드 규칙으로 완전히 제거하는 방법을 목표로 합니다. 더 짧은 요약이 필요하면 내부 글인 pandas SettingWithCopyWarning 완전 해결법도 함께 참고하세요.

SettingWithCopyWarning이 의미하는 것

pandas에서 df[...] 같은 인덱싱 결과는 크게 두 종류로 나뉩니다.

  • view(뷰): 원본 데이터를 참조하는 얕은 객체
  • copy(복사본): 원본과 분리된 별도 데이터

문제는 pandas가 어떤 상황에서는 뷰를, 어떤 상황에서는 복사본을 돌려주며, 그 판단이 데이터 정렬 상태, 블록 구조, 인덱싱 방식 등에 따라 달라질 수 있다는 점입니다. 그래서 아래처럼 “부분집합을 만든 다음 다시 인덱싱해서 대입”하는 형태(체인 인덱싱)가 나오면 pandas는 경고를 띄웁니다.

  • 내가 지금 수정하는 대상이 원본인지(뷰) 복사본인지 확신할 수 없음
  • 복사본이면 수정이 원본에 반영되지 않음

핵심은 하나입니다.

  • 체인 인덱싱을 피하고, 한 번의 .loc 또는 .iloc로 행과 열을 동시에 지정해 대입하라

재현: 가장 흔한 경고 패턴

아래 코드는 매우 흔합니다.

import pandas as pd

df = pd.DataFrame({
    "user": ["a", "b", "c", "d"],
    "country": ["KR", "US", "KR", "JP"],
    "score": [10, 20, 30, 40],
})

kr = df[df["country"] == "KR"]
kr["score"] = kr["score"] + 1  # SettingWithCopyWarning 가능

이 코드는 “KR만 뽑아서 점수 올리기”라는 의도가 명확하지만, kr가 뷰인지 복사본인지 애매합니다. 따라서 수정이 원본 df에 반영될지 보장되지 않습니다.

완전 해결 1: .loc로 한 번에 대입하기

가장 권장되는 정석입니다.

mask = df["country"] == "KR"
df.loc[mask, "score"] = df.loc[mask, "score"] + 1

장점

  • 원본 df에 대해 직접 대입하므로 의미가 명확
  • 경고가 사라지고, 동작이 결정적

추가로 여러 컬럼을 동시에 수정할 때도 .loc가 깔끔합니다.

mask = df["country"] == "KR"
df.loc[mask, ["score", "user"]] = df.loc[mask, ["score", "user"]].assign(
    score=lambda x: x["score"] + 1,
    user=lambda x: x["user"].str.upper(),
)

완전 해결 2: 서브셋을 정말로 별도로 쓸 거면 .copy()

원본과 분리된 데이터프레임을 만들고 그 안에서 마음껏 수정하려면, 의도를 코드로 고정해야 합니다.

kr = df.loc[df["country"] == "KR"].copy()
kr["score"] = kr["score"] + 1  # 경고 없음, 원본 df는 영향 없음

이 접근은 다음 상황에서 특히 유용합니다.

  • 특정 국가 데이터만 떼어내 별도 파이프라인을 태움
  • 원본은 보존하고, 가공 결과를 다른 곳에 저장

주의할 점

  • .copy()는 메모리를 추가로 사용합니다. 큰 데이터에서는 비용이 큽니다.
  • 원본까지 바꾸려는 목적이라면 .loc 대입이 더 적절합니다.

완전 해결 3: inplace=True를 기대하지 말기

SettingWithCopyWarning과 함께 자주 섞이는 오해가 “inplace면 원본이 바뀌겠지”입니다. 하지만 서브셋이 복사본이면 inplace=True도 복사본에만 적용됩니다.

kr = df[df["country"] == "KR"]
kr.drop(columns=["user"], inplace=True)  # 경고가 뜰 수 있고, 원본 df는 그대로일 수 있음

해결은 동일합니다.

  • 원본을 바꾸려면 .loc로 원본을 직접 수정
  • 복사본을 바꾸려면 .copy()로 의도를 고정

원본을 바꾸는 방식 예시입니다.

mask = df["country"] == "KR"
df.loc[mask, "user"] = None

경고가 자주 발생하는 패턴과 교정 레시피

1) 체인 인덱싱: df[...][...] = ...

문제 코드

df[df["country"] == "KR"]["score"] = 0

교정

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

2) 서브셋 변수에 담아놓고 수정

문제 코드

sub = df[df["score"] > 10]
sub["grade"] = "A"

교정 1: 원본 수정 목적

mask = df["score"] > 10
df.loc[mask, "grade"] = "A"

교정 2: 서브셋 독립 목적

sub = df.loc[df["score"] > 10].copy()
sub["grade"] = "A"

3) 정렬, 슬라이스 후 대입

문제 코드

sub = df.sort_values("score")[:2]
sub["score"] = -1

교정

  • 원본을 바꾸려면 원본 인덱스를 이용해 .loc로 지정
sub = df.sort_values("score").head(2)
df.loc[sub.index, "score"] = -1

4) query 결과에 대입

문제 코드

sub = df.query("country == 'KR'")
sub["score"] = sub["score"] + 1

교정

mask = df["country"].eq("KR")
df.loc[mask, "score"] = df.loc[mask, "score"] + 1

또는 독립 사본이면

sub = df.query("country == 'KR'").copy()
sub["score"] += 1

실전 규칙: 팀 코드 컨벤션으로 박아두기

SettingWithCopyWarning은 개인 실수라기보다 “pandas 인덱싱이 가진 함정”에 가깝습니다. 팀 단위로 재발을 막으려면 아래 규칙을 추천합니다.

규칙 1: 대입은 무조건 .loc 또는 .iloc

  • 행과 열을 한 번에 지정해서 대입
  • df[...][...] = ... 형태 금지

규칙 2: 서브셋은 두 종류로만 만든다

  • 원본을 수정할 목적이면 서브셋 변수에 담지 말고, 마스크를 만들어 .loc로 원본에 대입
  • 원본과 분리할 목적이면 ... .copy()를 강제

규칙 3: 경고를 끄지 않는다

다음 설정은 문제를 숨깁니다.

pd.options.mode.chained_assignment = None

경고가 사라져도 “원본 반영 여부가 비결정적”인 상태는 그대로입니다. 특히 데이터 파이프라인에서 조용한 데이터 오염으로 이어질 수 있어 더 위험합니다.

디버깅: 내 객체가 view인지 copy인지 확인하기

pandas는 view와 copy 여부를 사용자에게 완벽히 노출하지 않습니다. 그래도 아래 방법들이 실전에서 도움이 됩니다.

1) 수정 후 원본이 바뀌었는지 테스트로 고정

가장 확실한 방법은 “의도한 결과가 원본에 반영되는지”를 테스트로 박는 것입니다.

import pandas as pd


def test_update_score_updates_original():
    df = pd.DataFrame({"country": ["KR", "US"], "score": [1, 2]})
    mask = df["country"].eq("KR")
    df.loc[mask, "score"] += 10

    assert df.loc[0, "score"] == 11

2) 인덱스를 이용해 원본에 다시 매핑하기

서브셋을 만든 다음 원본에 반영해야 한다면, 서브셋의 .index를 이용해 원본에 대입합니다.

sub = df.loc[df["country"].eq("KR")].copy()
sub["score"] += 1

# 원본에 반영
df.loc[sub.index, "score"] = sub["score"]

이 방식은 “가공은 서브셋에서, 반영은 원본에 명시적으로”라는 구조를 만들어 경고를 원천 차단합니다.

자주 묻는 질문

경고가 안 뜨는데도 위험할 수 있나

그럴 수 있습니다. 경고는 “항상” 뜨는 게 아니라 “pandas가 위험하다고 판단한 경우”에 뜹니다. 즉, 경고가 없다고 해서 체인 인덱싱이 안전하다는 보장은 없습니다. 그래서 컨벤션 차원에서 체인 인덱싱 자체를 금지하는 게 좋습니다.

.at, .iat는 어떤가

단일 스칼라 값 대입은 .at, .iat도 안전하고 빠릅니다.

df.at[0, "score"] = 999

다만 조건 기반 다건 업데이트는 .loc가 가장 명확합니다.

마무리: 해결의 핵심은 “의도 고정”

SettingWithCopyWarning을 완전히 해결하는 요령은 단순합니다.

  • 원본을 바꾸려면 df.loc[mask, col] = value
  • 원본과 분리하려면 subset = df.loc[mask].copy()
  • 체인 인덱싱 형태를 팀 규칙으로 금지

이 3가지만 지켜도 경고는 사라지고, 더 중요한 문제인 “데이터가 조용히 안 바뀌는 버그”를 예방할 수 있습니다.

추가로 같은 주제를 더 압축해 정리한 글은 pandas SettingWithCopyWarning 완전 해결법에서 확인할 수 있습니다.