- Published on
Pandas SettingWithCopyWarning 완전 해결 6가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서브셋에 값을 대입했는데 값이 반영되지 않거나, 어떤 환경에서는 되고 어떤 환경에서는 안 되는 경험을 해봤다면 SettingWithCopyWarning을 만났을 확률이 높습니다. 이 경고는 “지금 수정하는 대상이 원본 DataFrame의 뷰(view)인지, 복사본(copy)인지 확실하지 않다”는 신호입니다.
핵심은 간단합니다.
- 체인 인덱싱(예:
df[df["A"] > 0]["B"] = 1)을 피한다 loc/iloc로 한 번에 선택하고 한 번에 대입한다- 필요하면 복사본을 명시적으로 만들고(
copy) 그 복사본을 수정한다
아래 6가지 해결책은 실무에서 가장 자주 쓰이는 패턴이며, 서로 대체재가 아니라 상황별로 선택하는 도구입니다. 더 많은 케이스는 내부 글인 Pandas SettingWithCopyWarning 근본 해결 7가지도 함께 참고하면 좋습니다.
SettingWithCopyWarning이 뜨는 진짜 이유
Pandas는 성능을 위해 슬라이싱 결과를 항상 복사하지 않습니다. 어떤 연산은 뷰를, 어떤 연산은 복사본을 만들고, 또 같은 연산이라도 내부 최적화에 따라 결과가 달라질 수 있습니다. 그래서 “슬라이스한 결과에 대입”하면 원본이 바뀌는지 보장할 수 없고, Pandas가 경고로 알려주는 것입니다.
대표적인 문제 코드:
import pandas as pd
df = pd.DataFrame({
"A": [1, -1, 2],
"B": [10, 20, 30]
})
# 체인 인덱싱: 필터링 결과에서 다시 컬럼 선택 후 대입
# SettingWithCopyWarning 발생 가능
filtered = df[df["A"] > 0]
filtered["B"] = 0
위 코드는 filtered가 뷰인지 복사본인지 불명확합니다. 그리고 설령 filtered가 잘 바뀌어도, 원본 df가 바뀐다는 보장이 없습니다.
해결 1) loc로 “선택과 대입”을 한 번에 끝내기 (정석)
가장 표준적이고 권장되는 해결책입니다. 조건 필터와 대상 컬럼을 한 번에 지정해 원본 df에 직접 대입합니다.
mask = df["A"] > 0
df.loc[mask, "B"] = 0
포인트:
df.loc[행조건, 열] = 값형태는 Pandas가 의도를 명확히 이해합니다.- 경고를 없애는 수준이 아니라, “원본이 확실히 수정되는 코드”가 됩니다.
응용: 여러 컬럼을 동시에 수정
mask = df["A"] > 0
df.loc[mask, ["B"]] = 0
해결 2) 체인 인덱싱을 “중간 변수”로 분리하지 말고, 아예 금지하기
다음처럼 “중간에 슬라이스를 변수로 받는” 습관이 경고를 자주 부릅니다.
subset = df[df["A"] > 0]
subset["B"] = subset["B"] * 10
이 패턴을 유지하되 경고만 없애고 싶다면 결국 subset을 복사본으로 확정하거나(해결 3), 원본에 직접 대입(해결 1)해야 합니다.
원본을 업데이트하려는 의도라면, 아래처럼 loc로 직접 쓰는 편이 코드 리뷰에서 훨씬 안전합니다.
mask = df["A"] > 0
df.loc[mask, "B"] = df.loc[mask, "B"] * 10
해결 3) “복사본을 수정하는 게 목적”이라면 copy()로 의도를 명시하기
분석 파이프라인에서 원본을 보존하고, 필터된 데이터만 별도로 가공하고 싶을 때가 많습니다. 이때는 경고를 없애기 위해서가 아니라, 데이터 소유권을 분명히 하기 위해 copy()를 씁니다.
subset = df.loc[df["A"] > 0].copy()
subset["B"] = 0
이제 subset은 독립 객체이므로 SettingWithCopyWarning이 발생하지 않고, 원본 df도 변하지 않습니다.
실무 팁:
- “원본을 수정할 거냐, 파생 데이터를 만들 거냐”를 먼저 결정하세요.
- 파생 데이터면
copy()를 비용으로 지불하고 안정성을 얻는 편이 낫습니다.
관련해서 더 깊게는 pandas SettingWithCopyWarning 확실히 없애는 법도 도움이 됩니다.
해결 4) assign으로 새 DataFrame을 반환하는 함수형 스타일로 바꾸기
assign은 “새로운 데이터프레임을 반환”하는 API라서, 체인 인덱싱 대입 문제를 구조적으로 피할 수 있습니다.
subset = (
df.loc[df["A"] > 0]
.assign(B=lambda x: 0)
)
조건에 따라 값 설정도 가능합니다.
subset = df.assign(
B=lambda x: x["B"].where(x["A"] <= 0, 0)
)
장점:
- 원본 불변(immutability)에 가까운 파이프라인 구성 가능
- 중간 결과를 안전하게 이어붙이기 쉬움
단점:
- 대규모 데이터에서 불필요한 복사가 늘 수 있어 성능을 고려해야 함
해결 5) query + loc 조합으로 가독성과 안정성 함께 잡기
조건식이 복잡해질수록 mask = ...가 길어지고 실수도 늘어납니다. 이때 query로 행을 읽기 좋게 표현하고, 대입은 loc로 안전하게 처리합니다.
mask = df.eval("A > 0")
df.loc[mask, "B"] = 0
또는 query로 인덱스를 얻어 loc에 쓰는 방식도 가능합니다.
idx = df.query("A > 0").index
df.loc[idx, "B"] = 0
주의:
query는 문자열 기반이라 컬럼명에 공백/특수문자가 있으면 백틱 처리가 필요합니다. 예를 들어 컬럼명이total price라면query("total price> 0")처럼 써야 합니다. 백틱은 마크다운 백틱과 혼동될 수 있으니 코드 블록 안에서만 쓰는 것을 권합니다.
해결 6) iloc로 위치 기반 대입을 확정하고, 인덱스 정렬을 의식하기
조건 필터 후 특정 열을 수정하는 대신, “수정할 행/열의 위치”가 확실할 때는 iloc가 가장 명시적입니다.
예: B 컬럼이 1번 열이고, A > 0인 행들의 B를 0으로 바꾸기
mask = df["A"] > 0
rows = df.index[mask]
# B가 두 번째 컬럼(인덱스 1)이라고 가정
col_pos = df.columns.get_loc("B")
df.iloc[rows, col_pos] = 0
장점:
- “뷰/복사” 논란에서 더 멀어짐(원본 배열을 직접 겨냥)
- 열 이름이 바뀌거나 중복될 위험이 있으면 위치 기반이 안전할 때가 있음
주의:
rows가 정수 위치가 아니라 “라벨 인덱스”인 경우가 있으니, 위처럼df.index[mask]를iloc에 넣는 패턴은 인덱스 형태에 따라 혼란을 줄 수 있습니다. 가장 안전하게는np.where(mask.to_numpy())[0]로 순수 위치 인덱스를 만드는 방법도 있습니다.
import numpy as np
row_pos = np.where(mask.to_numpy())[0]
col_pos = df.columns.get_loc("B")
df.iloc[row_pos, col_pos] = 0
(하면 안 되는) 경고를 끄는 방식은 해결이 아니다
다음은 증상만 숨기고 버그를 남길 수 있어 권장하지 않습니다.
pd.options.mode.chained_assignment = None
이 설정은 팀 코드베이스에서 특히 위험합니다. 어떤 셀/모듈에서 설정했는지 추적이 어려워지고, “원본이 안 바뀌는” 조용한 데이터 오류가 생길 수 있습니다.
실전 체크리스트: 내 코드가 무엇을 의도하는가
마지막으로, 경고를 없애는 것보다 중요한 질문은 하나입니다.
- 원본
df를 수정하려는가?- 그렇다면
df.loc[조건, 컬럼] = 값으로 끝내기
- 그렇다면
- 파생 데이터프레임을 만들려는가?
- 그렇다면
df.loc[조건].copy()후 수정하기
- 그렇다면
파이프라인이 길고 여러 단계로 이어진다면 assign 스타일도 좋은 선택입니다. 반대로 “특정 셀/블록을 반드시 업데이트”해야 한다면 loc 또는 iloc로 타겟을 확정하는 것이 가장 안전합니다.
추가로 더 많은 패턴/함정 사례는 Pandas SettingWithCopyWarning 근본 해결 7가지에서 확장된 예시로 정리해두었습니다.