- Published on
Pandas SettingWithCopyWarning 원인·해결 5분
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
데이터 전처리하다가 아래 경고를 본 적이 있을 겁니다.
> SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame
처음엔 “경고니까 무시해도 되나?” 싶지만, 이 경고는 실제로 값이 반영되지 않거나(조용히 실패), 반영되더라도 어떤 객체가 바뀌었는지 확신할 수 없다는 신호입니다. 즉, 재현성과 디버깅 난이도를 크게 올립니다.
이 글은 5분 안에 다음을 끝내는 걸 목표로 합니다.
- SettingWithCopyWarning의 정확한 원인(뷰 vs 복사) 이해
- 자주 터지는 대표 패턴 4~5개 분류
- 실무에서 안전한 해결 템플릿 정리
- “경고를 끄는 것”이 왜 위험한지
비슷한 맥락의 “원인-진단-해결” 흐름은 장애 글에서도 중요합니다. 예를 들어 Python OpenAI SDK 400 invalid_json 원인과 해결처럼, 증상만 누르지 말고 원인을 제거하는 접근이 가장 빠릅니다.
SettingWithCopyWarning 한 줄 정의
Pandas는 어떤 연산이 원본 DataFrame의 뷰(view) 를 반환했는지, 아니면 복사(copy) 를 반환했는지 상황에 따라 다를 수 있습니다. 그 상태에서 체이닝(chained)으로 값을 대입하면:
- “원본에 쓰는 건가?”
- “임시 복사본에 쓰는 건가?”
가 불명확해져서 경고를 띄웁니다.
핵심은 이겁니다.
- 슬라이싱/필터링 결과에 바로 대입하지 말고
- 원본에
.loc[row_indexer, col_indexer] = ...형태로 한 번에 대입하거나 - 명시적으로
.copy()해서 “이건 복사본이다”를 선언하라는 뜻입니다.
가장 흔한 재현 예제 (왜 위험한지)
아래 코드는 “될 때도 있고 안 될 때도 있는” 대표 케이스입니다.
import pandas as pd
df = pd.DataFrame({
"user": ["a", "b", "c", "d"],
"age": [10, 20, 30, 40],
"score": [1.1, 2.2, 3.3, 4.4]
})
# 필터링 결과에 바로 대입 (체이닝)
df[df["age"] >= 30]["score"] = 0
이 코드는 의도상으로는 age >= 30인 행의 score를 0으로 만들고 싶지만,
df[df["age"] >= 30]가 뷰일 수도 있고 복사본일 수도 있고- 그 다음
["score"]로 다시 한 번 슬라이스
…를 하면서 Pandas가 “이거 원본에 쓰는 거 맞아?”라고 경고합니다.
원인 패턴 1) Boolean 필터링 + 컬럼 선택 후 대입
문제 코드
filtered = df[df["age"] >= 30]
filtered["score"] = 0 # 경고 가능
filtered가 뷰인지 복사인지 불명확합니다.
해결 1: 원본에 .loc로 한 번에 대입 (가장 권장)
mask = df["age"] >= 30
df.loc[mask, "score"] = 0
mask로 행을 고르고"score"로 열을 고른 뒤- 원본 df에 직접 대입합니다.
해결 2: 복사본이 목적이면 .copy()로 명시
filtered = df.loc[df["age"] >= 30].copy()
filtered["score"] = 0 # 이제 경고 없음 (복사본에 쓰는 게 확실)
“원본은 건드리지 않고 부분집합만 편집”이 목적이라면 .copy()가 정답입니다.
원인 패턴 2) 체인 인덱싱(Chained indexing)
SettingWithCopyWarning의 거의 모든 사례는 “한 줄에서 두 번 이상 인덱싱”에서 시작합니다.
문제 코드
# 행 슬라이스 -> 열 슬라이스 -> 대입
# (중간 결과가 무엇인지 불명확)
df[df["age"] >= 30]["score"] = 0
해결: .loc로 합치기
df.loc[df["age"] >= 30, "score"] = 0
규칙: df[...][...] = ... 형태를 보면 반사적으로 .loc[..., ...]로 바꾸세요.
원인 패턴 3) 슬라이스(df[:10]) 후 대입
슬라이스는 경우에 따라 뷰처럼 동작할 수 있어 경고를 유발합니다.
문제 코드
head = df[:2]
head["age"] = head["age"] + 1 # 경고 가능
해결: 목적에 따라 선택
- 원본을 바꾸고 싶다 →
.loc로 원본에 반영
df.loc[:1, "age"] = df.loc[:1, "age"] + 1
- 복사본을 바꾸고 싶다 →
.copy()
head = df.iloc[:2].copy()
head["age"] = head["age"] + 1
원인 패턴 4) 정렬/드롭/리네임 등 중간 결과에 대입
메서드 체이닝을 하다가 중간 결과를 변수로 받지 않고 곧바로 대입하면 애매해질 수 있습니다.
문제 코드(전형적인 실수)
# 정렬한 결과에서 특정 열을 수정하고 싶다
sorted_df = df.sort_values("age")
sorted_df[sorted_df["age"] >= 30]["score"] = 0 # 경고 가능
해결: 파이프라인은 assign/loc로
sorted_df = df.sort_values("age").copy() # 정렬 결과를 독립적으로 쓰려면 copy
sorted_df.loc[sorted_df["age"] >= 30, "score"] = 0
또는 “새 컬럼 생성/변환”은 assign이 깔끔합니다.
sorted_df = (
df.sort_values("age")
.assign(score=lambda x: x["score"].where(x["age"] < 30, 0))
)
where는 조건에 따라 값을 유지/대체하는데 유용- 대입이 아닌 “새 Series 생성”이라 경고 가능성이 줄어듭니다.
원인 패턴 5) 함수 내부에서 부분집합 수정
함수에서 DataFrame을 받아 특정 조건만 수정하는 경우, 의도치 않게 “부분집합”을 만들어 수정하는 형태가 많습니다.
문제 코드
def normalize_adults(df: pd.DataFrame) -> pd.DataFrame:
adults = df[df["age"] >= 20]
adults["score"] = adults["score"] / adults["score"].max()
return adults
이 함수는 “성인만 반환”이 목적일 수 있지만, 경고가 뜨고 동작이 불명확합니다.
해결: 반환 정책을 명확히
- 부분집합을 반환하며 수정도 한다 →
.copy()
def normalize_adults(df: pd.DataFrame) -> pd.DataFrame:
adults = df.loc[df["age"] >= 20].copy()
adults["score"] = adults["score"] / adults["score"].max()
return adults
- 원본을 수정하고 전체를 반환한다 →
.loc로 원본에 반영
def normalize_adults_inplace(df: pd.DataFrame) -> pd.DataFrame:
mask = df["age"] >= 20
df.loc[mask, "score"] = df.loc[mask, "score"] / df.loc[mask, "score"].max()
return df
“경고 끄기”가 위험한 이유
가끔 아래처럼 경고를 꺼버리는 글도 보이는데, 추천하지 않습니다.
import pandas as pd
pd.options.mode.chained_assignment = None # 경고 숨김
이건 원인을 해결하지 않고 증상만 숨기는 것입니다.
- 값이 실제로 반영되지 않아도 조용히 지나갈 수 있고
- 코드 리뷰/유지보수 시 버그가 잠복합니다.
실무에서 이런 “숨김”은 장애를 키우는 패턴입니다. 예를 들어 403/404 같은 에러도 로그를 끄면 더 큰 문제로 번지듯, 원인을 추적해 제거해야 합니다. 같은 결의 접근으로 OpenAI Responses API 403 model_not_found 해결 가이드처럼 정확한 원인과 재현 조건을 먼저 고정하는 게 빠릅니다.
5분 해결 체크리스트 (바로 적용용)
아래 질문에 “예”가 나오면 대응을 바꾸면 됩니다.
df[...][...] = ...형태인가?
- 예 →
.loc[..., ...] = ...로 합치기
- 부분집합을 만들어 편집하고 있는가?
- 예 → “원본 수정”이면
.loc/ “부분집합 반환”이면.copy()
- 함수가 DataFrame을 받아 내부에서 필터링 후 수정하는가?
- 예 → 반환 정책을 명확히 하고
.copy()또는.loc
- 새 컬럼을 만들거나 변환하는가?
- 예 → 가능하면
assign,where,mask로 “대입”을 줄이기
실전 예제: 경고 나는 전처리 코드를 안전하게 리팩터링
Before
# 목표: 서울 사용자 중 30세 이상은 grade를 'A'로
import pandas as pd
df = pd.DataFrame({
"city": ["Seoul", "Busan", "Seoul", "Seoul"],
"age": [25, 31, 40, 18],
"grade": ["C", "B", "B", "D"]
})
seoul = df[df["city"] == "Seoul"]
seoul[seoul["age"] >= 30]["grade"] = "A" # 경고 + 의도 불명확
After (원본 df를 수정하는 의도)
mask = (df["city"] == "Seoul") & (df["age"] >= 30)
df.loc[mask, "grade"] = "A"
After (서울만 별도 테이블로 만들어 수정하는 의도)
seoul = df.loc[df["city"] == "Seoul"].copy()
seoul.loc[seoul["age"] >= 30, "grade"] = "A"
이렇게 “원본을 바꾸는지/복사본을 바꾸는지” 의도를 코드로 고정하면 경고도 사라지고, 결과도 예측 가능해집니다.
마무리
SettingWithCopyWarning은 Pandas가 까다로워서가 아니라, 당신의 코드가 ‘어느 객체를 수정하는지’ 모호하다는 신호입니다. 해결은 단순합니다.
- 원본을 수정할 땐
df.loc[rows, cols] = value - 부분집합을 편집할 땐
.copy()로 의도를 선언 - 체인 인덱싱(
df[...][...])은 피하기
이 3가지만 습관화하면 SettingWithCopyWarning은 거의 다시 보지 않게 됩니다.