- Published on
pandas SettingWithCopyWarning 완전 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서브셋을 만든 뒤 컬럼을 수정했는데 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 완전 해결법에서 확인할 수 있습니다.