Published on

pandas SettingWithCopyWarning 완전 정리

Authors

서브셋을 만든 뒤 컬럼 값을 바꾸려는데 SettingWithCopyWarning이 뜨면, 많은 사람이 “그냥 경고니까 무시해도 되나?” 혹은 “.copy()를 무조건 붙이면 되나?” 사이에서 헤맵니다. 이 경고는 단순한 스타일 이슈가 아니라, **내가 수정한 값이 원본 DataFrame에 반영될지 말지(또는 일부만 반영될지)**가 모호하다는 신호입니다.

이 글에서는 SettingWithCopyWarning이 발생하는 내부 맥락(뷰와 복사의 불확실성), 재현 패턴, 그리고 실무에서 가장 안전한 해결책을 코드로 정리합니다.

SettingWithCopyWarning이 말하는 핵심

SettingWithCopyWarning은 pandas가 다음 상황을 감지했을 때 띄웁니다.

  • 어떤 DataFrame에서 부분집합을 만든 객체가 원본의 뷰(view)인지 복사(copy)인지 확실하지 않다
  • 그 부분집합에 대해 체인 인덱싱(chained indexing) 형태로 값을 할당했다
  • 그래서 “너 지금 수정한 게 원본에 반영될 수도, 안 될 수도 있어”라고 경고한다

여기서 중요한 포인트는 “뷰냐 복사냐”가 항상 결정적으로 고정되지 않는다는 점입니다. pandas 내부 최적화, 블록 매니저, 컬럼 dtype 구성 등에 따라 결과가 달라질 수 있습니다. 즉, 동일한 코드가 데이터/버전/상황에 따라 조용히 다른 결과를 만들 수 있습니다.

가장 흔한 발생 패턴: 체인 인덱싱

아래가 대표적인 문제 코드입니다.

import pandas as pd

df = pd.DataFrame({
    "user": ["a", "b", "c", "d"],
    "age": [17, 20, 16, 30],
    "score": [80, 90, 70, 95],
})

# 1) 필터로 일부 행을 고르고
# 2) 그 결과에 다시 컬럼 선택 후 할당
# 이 형태가 체인 인덱싱의 전형

df[df["age"] >= 18]["adult"] = True

위 코드는 흔히 SettingWithCopyWarning을 발생시키며, 더 나쁜 점은 실제로 dfadult 컬럼이 생기지 않거나, 생기더라도 기대와 다르게 동작할 수 있다는 것입니다.

왜 위험한가

df[df["age"] >= 18] 결과가:

  • 원본 df의 뷰면: 뒤의 할당이 원본에 영향을 줄 수도 있음
  • 복사본이면: 복사본만 바뀌고 원본은 그대로임

pandas는 이 둘을 확정할 수 없으니 경고를 띄웁니다.

정석 해결: .loc로 한 번에 인덱싱하고 할당

가장 권장되는 해결책은 조건과 컬럼 선택을 .loc 한 번으로 끝내는 것입니다.

mask = df["age"] >= 18

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

이 패턴은 다음 장점이 있습니다.

  • 원본 df에 대해 명시적으로 할당
  • 체인 인덱싱 제거
  • 경고 제거 + 동작 확정

else 값까지 채우고 싶다면

df["adult"] = False
mask = df["age"] >= 18

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

또는 한 줄로:

df["adult"] = df["age"].ge(18)

“부분집합을 따로 다룰 거면” .copy()로 의도를 고정

필터링한 결과를 별도 DataFrame으로 두고 계속 가공할 생각이라면, 애초에 복사본임을 확정하는 것이 좋습니다.

adults = df.loc[df["age"] >= 18].copy()

adults["grade"] = "A"  # 경고 없음, 원본 df와 독립

이 경우 핵심은 “원본을 바꾸려는 게 아니라, 파생 테이블을 만들겠다”는 의도를 코드로 표현하는 것입니다.

.copy()를 남발하면 안 되나?

  • 단순히 경고를 끄려고 무조건 .copy()를 붙이면 메모리 비용이 증가합니다.
  • 하지만 파생 테이블을 만들고 그걸 기준으로 downstream 작업을 한다면 .copy()는 오히려 안전장치입니다.

실무에서는 아래 기준이 깔끔합니다.

  • 원본을 수정해야 한다: .loc[...] = ...
  • 파생 테이블을 만들고 독립적으로 수정한다: .copy()

또 다른 흔한 케이스: 컬럼 슬라이싱 후 할당

컬럼을 일부만 뽑아 만든 DataFrame에 값을 넣을 때도 경고가 나올 수 있습니다.

sub = df[["user", "score"]]
sub["score"] = sub["score"] * 10

이때도 상황에 따라 뷰/복사가 섞일 수 있습니다. 안전하게 하려면:

  • sub를 독립 객체로 쓰려면 sub = df[[...]].copy()
  • 원본을 바꾸려면 df.loc[:, "score"] = df["score"] * 10
# 원본 수정

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

# 파생 테이블로 독립 수정

sub = df[["user", "score"]].copy()
sub.loc[:, "score"] = sub["score"] * 10

.loc를 써도 경고가 나는 경우가 있나?

대부분의 경고는 체인 인덱싱에서 오지만, 간혹 이런 식의 코드도 문제를 만듭니다.

sub = df[df["age"] >= 18]
sub.loc[:, "score"] = 0

여기서 할당은 .loc지만, 이미 sub = df[...] 단계에서 sub가 뷰인지 복사인지 불확실합니다. 따라서 sub 자체가 불안정한 객체일 수 있어 경고가 계속 날 수 있습니다.

해결은 앞에서 말한 것처럼 의도를 고정하면 됩니다.

sub = df.loc[df["age"] >= 18].copy()
sub.loc[:, "score"] = 0

또는 원본을 수정하려는 목적이면 sub를 만들지 말고 바로 원본에 .loc 할당을 합니다.

mask = df["age"] >= 18
df.loc[mask, "score"] = 0

실무에서 추천하는 안전한 패턴 4가지

1) 조건부 업데이트: .loc[mask, col] = value

mask = (df["age"] >= 18) & (df["score"] >= 90)
df.loc[mask, "vip"] = True

2) 새 컬럼 생성: assign

assign은 체인 인덱싱 위험을 줄이고, 파이프라인에 잘 맞습니다.

out = (
    df
    .assign(
        adult=lambda x: x["age"].ge(18),
        score2=lambda x: x["score"] * 2,
    )
)

3) 파생 테이블: .copy()로 독립성 명시

report = df.loc[df["age"] >= 18, ["user", "score"]].copy()
report["tier"] = pd.cut(report["score"], bins=[0, 80, 90, 100], labels=["B", "A", "S"])

4) 함수형 파이프라인: pipe로 단계 분리

def add_flags(x: pd.DataFrame) -> pd.DataFrame:
    x = x.copy()
    x["adult"] = x["age"].ge(18)
    return x

out = df.pipe(add_flags)

이 패턴은 “함수 안에서 복사 후 수정”을 강제해, 사이드 이펙트를 줄입니다.

경고를 끄는 건 해결이 아니다

아래처럼 옵션으로 경고를 숨길 수는 있지만, 문제의 본질(불확실한 할당)을 제거하지 못합니다.

pd.options.mode.chained_assignment = None  # 경고 비활성화

이 설정은 팀/프로젝트 차원에서 디버깅 난이도를 급격히 올릴 수 있습니다. 특히 데이터 파이프라인에서 “어느 단계에서 값이 안 바뀌었는지” 추적이 어려워집니다.

권장 순서는:

  1. 체인 인덱싱 제거
  2. 원본 수정이면 .loc로 직접
  3. 파생 테이블이면 .copy()로 의도 고정

디버깅 체크리스트

SettingWithCopyWarning을 봤을 때 아래 질문을 순서대로 던지면 빠르게 정리됩니다.

  1. 지금 수정하려는 대상은 원본 df인가, 파생 DataFrame인가?
  2. 코드에 df[...][...] = ... 형태가 있는가?
  3. 필터링 결과를 변수에 담았는가? 담았다면 그 변수는 .copy()가 필요한가?
  4. 같은 결과를 .loc[mask, col] = ...로 표현할 수 있는가?

이 경고는 본질적으로 “데이터가 조용히 틀릴 수 있다”는 경고라서, 빨리 제거할수록 좋습니다. 이런 점은 DB에서 데드락이나 N+1처럼 “성공하는 것처럼 보이지만 운영에서 터지는” 유형과 닮았습니다. 원인 추적/재현/해결의 사고방식은 비슷하니, 장애 대응 관점이 궁금하다면 MySQL InnoDB Deadlock 원인 쿼리 추적·해결 가이드도 함께 참고할 만합니다.

결론

  • SettingWithCopyWarning은 “뷰인지 복사인지 불확실한 객체에 할당했다”는 경고다.
  • 가장 흔한 원인은 체인 인덱싱 df[...][...] = ....
  • 원본을 바꾸려면 .loc[mask, col] = value로 한 번에 할당한다.
  • 파생 테이블을 독립적으로 다룰 거면 .copy()로 의도를 명확히 한다.
  • 경고를 끄는 방식은 근본 해결이 아니며, 데이터 품질 문제를 숨길 수 있다.

위 원칙대로 코드를 정리하면 경고를 없애는 것을 넘어, pandas 조작이 “항상 같은 결과를 내는” 예측 가능한 형태로 바뀝니다.