[Pandas] SettingWithCopyWarning
정의
SettingWithCopyWarning 은 pandas 가 가장 자주 던지는 경고. chained indexing 으로 view 인지 copy 인지 모호한 상태에 값을 할당 할 때 발생.
sub = df[df['age'] > 30]
sub['new'] = 1 # ⚠️ SettingWithCopyWarning
# sub 가 view 인지 copy 인지 pandas 가 모름
# 원본 df 가 영향받을 수도, 안 받을 수도
사용 상황
다음 상황에서 가장 자주 발생한다.
| 상황 | 예시 패턴 | 발생 이유 |
|---|---|---|
| 필터링 후 컬럼 추가 | df[mask]['col'] = v | chained indexing |
| 슬라이스 후 수정 | df[5:10]['x'] = 1 | slice 가 view 일 수 있음 |
| groupby 결과 수정 | get_group(...) 후 할당 | view/copy 불확실 |
| 함수 인자 df 수정 | 받은 df 에 직접 컬럼 추가 | 호출자 df 변경 여부 불확실 |
| 함수형 체이닝 후 수정 | .sort_values()[...] = v | 체이닝 결과가 copy |
원인: view vs copy 흐름
flowchart TD
A["df[mask] 실행"] --> B{"내부 메모리 구조"}
B -->|"연속 블록"| C["view 반환"]
B -->|"불연속 또는 복합 dtype"| D["copy 반환"]
C --> E["원본 df 도 수정됨"]
D --> F["원본 df 는 변경 없음"]
E --> WARN["결과 비결정적, 경고 발생"]
F --> WARN
pandas 는 어떤 경우에 view 가, 어떤 경우에 copy 가 반환될지 버전마다, dtype 마다, 메모리 레이아웃마다 다르다. 사용자는 예측할 수 없다.
왜 위험한가
sub가 copy 면:sub['new'] = 1은 sub 만 변경, 원본 df 는 안 바뀜sub가 view 면: 원본 df 도 함께 변경됨 (의도일 수도, 아닐 수도)
두 경우 모두 조용히 지나가기 때문에 버그를 늦게 발견하는 경우가 많다.
4 가지 해결 패턴
1. .loc[] 한 줄에 통합 (가장 권장)
# ❌ chained indexing: 두 번의 indexing
df[df['age'] > 30]['salary'] = 0
# ✓ loc 로 한 번에: 행 + 열 동시 지정
df.loc[df['age'] > 30, 'salary'] = 0
행 / 열을 한 번의 indexer 로 처리. [[Pandas .loc / .iloc]] 참고.
2. 명시적 .copy()
sub = df[df['age'] > 30].copy() # 명시적 copy
sub['new'] = 1 # sub 만 변경, 원본 df 불변
원본 보존 의도가 명확. 단, 대용량 df 에서 메모리 복사 비용 발생.
3. .assign() 으로 method chaining
# 새 컬럼 추가 / 기존 컬럼 변환
df = df.assign(
salary=lambda d: d['salary'].where(d['age'] > 30, 0),
is_senior=lambda d: d['age'] > 30,
)
원본 df 를 직접 변경하지 않고 새 df 를 반환한다. [[Pandas pipe / method chaining]] 참고.
4. np.where / np.select 벡터화
import numpy as np
# 조건부 컬럼 생성: apply 보다 훨씬 빠름
df['category'] = np.where(df['age'] > 30, 'senior', 'junior')
# 다중 분기
conditions = [df['age'] < 20, df['age'] < 60, df['age'] >= 60]
choices = ['youth', 'adult', 'senior']
df['age_group'] = np.select(conditions, choices, default='unknown')
흔한 함정 패턴
chained indexing
df[df['age'] > 30]['salary'] = 0
# 내부 동작: df.__getitem__(mask).__setitem__('salary', 0)
# 첫 번째 __getitem__ 결과가 view/copy 불확실
slice 후 수정
sub = df[5:10] # slice 는 view (대부분의 경우)
sub['x'] = 1 # 원본 df 의 5~10 행도 영향받을 수 있음
loc + chained
df.loc[df['age'] > 30]['salary'] = 0 # ❌ 여전히 chained
df.loc[df['age'] > 30, 'salary'] = 0 # ✓ 한 번에
.loc 를 썼더라도 두 번의 indexing 이면 같은 문제가 발생한다.
view vs copy 판별 (비공식)
pandas 내부 동작이 복잡하고 버전마다 다르다. 확신할 수 없으면 항상 .copy() 또는 한 줄 .loc[행, 열] 을 쓴다.
df._is_view # 비공식 속성, 의존하지 마라
df._is_copy # 동일
pandas 2.x Copy-on-Write (CoW)
pandas 2.0 부터 CoW 가 opt-in 으로 도입됐고, pandas 3.0 부터 기본 활성화.
# pandas 2.x: 수동 활성화
import pandas as pd
pd.options.mode.copy_on_write = True
# CoW 동작 원리: lazy copy
sub = df[df['age'] > 30] # 아직 실제 복사 없음
sub['new'] = 1 # write 시점에 물리적 복사 수행
# 원본 df 는 항상 불변
CoW 활성화 환경에서의 동작 변화:
| 코드 패턴 | pandas 2.x 이전 | CoW 활성화 후 |
|---|---|---|
df[mask]['col'] = v | SettingWithCopyWarning | ChainedAssignmentError |
df.loc[mask, 'col'] = v | 정상 | 정상 |
sub = df[mask].copy() 후 수정 | 정상 | 정상 |
# pandas 3.0+ (CoW 기본)
df[df['age'] > 30]['salary'] = 0 # ChainedAssignmentError
df.loc[df['age'] > 30, 'salary'] = 0 # ✓ 정상 동작
IMPORTANT
pandas 3.0 에서는 SettingWithCopyWarning 이 에러로 격상된다. 지금부터 .loc[행, 열] 패턴으로 작성하는 습관을 들여야 한다.
성능
| 방법 | 메모리 비용 | 속도 | 비고 |
|---|---|---|---|
.loc[행, 열] = v | 없음 | 빠름 | 인플레이스 수정 |
.copy() 후 수정 | df 전체 복사 | 중간 | 명확한 의도 표현 |
.assign() | 새 df 생성 | 중간 | 함수형 style |
np.where / np.select | 없음 | 빠름 | 벡터 연산 |
대용량 DataFrame 에서 .copy() 는 메모리 / 시간 비용이 크다. 원본 수정이 목적이면 .loc[행, 열] = v 가 최선.
import pandas as pd
import numpy as np
# 1,000,000 행 DataFrame 테스트 (예시)
df = pd.DataFrame({'age': np.random.randint(20, 60, 1_000_000),
'salary': np.random.randint(3000, 8000, 1_000_000)})
# 빠름: 메모리 추가 없음
df.loc[df['age'] > 30, 'bonus'] = 500
# 느림: 전체 복사
sub = df[df['age'] > 30].copy()
sub['bonus'] = 500
자주 묻는 질문
Q. 경고를 무시해도 되나?
A. 무시하지 마라. 경고는 의도와 다른 결과를 낳을 수 있다는 신호. pandas 3.0 에서는 에러로 바뀐다.
Q. inplace=True 가 해결인가?
A. 별 도움 안 됨. inplace 자체도 deprecation 방향. 명시적 할당을 권장.
Q. .copy() 가 항상 안전한가?
A. ✓. 단, 큰 DataFrame 에서 메모리 / 시간 비용이 있다. 원본 수정이 목적이라면 .loc[] 가 낫다.
Q. pandas 2.x 에서 CoW 를 켜면 경고가 사라지나?
A. 경고가 ChainedAssignmentError 에러로 격상된다. 체이닝 자체를 원천 차단한다.
Q. 경고 없이 코드가 돌아가는데 결과가 이상하다면?
A. 체이닝으로 copy 에 썼기 때문에 원본이 변경되지 않은 것. .loc[행, 열] = v 로 고쳐야 한다.
함정
WARNING
아래 함정들은 데이터가 기대와 다르게 수정(또는 미수정)되어 조용한 버그로 이어질 수 있다.
1. groupby 결과 수정
# ❌
g = df.groupby('city').get_group('Seoul')
g['new'] = 1
# ✓
g = df.groupby('city').get_group('Seoul').copy()
g['new'] = 1
2. boolean mask + 컬럼 추가
# ❌
sub = df[df['age'] > 30]
sub.loc[:, 'flag'] = True # 여전히 경고
# ✓
sub = df[df['age'] > 30].copy()
sub['flag'] = True
3. 경고 출처가 다른 줄
경고 메시지의 라인 정보가 실제 원인 라인과 다를 수 있다. 한 단계 앞에서 발생한 chained indexing 이 진짜 원인인 경우가 많다.
4. 함수 인자로 받은 df
# ❌ 호출자의 df 도 변경될 수 있음
def add_flag(df):
df['flag'] = True # ⚠️ 체이닝 아니어도 경고
# ✓ 명시적 copy 후 반환
def add_flag(df):
df = df.copy()
df['flag'] = True
return df
5. 경고 억제 (권장하지 않음)
# 경고를 억제하는 것은 문제를 숨기는 것
import warnings
pd.options.mode.chained_assignment = None # 비권장
관련 위키
이 글의 용어 (5개)
- [Pandas] .loc / .ilocpandas
- 정의 - : 라벨 기반 인덱싱 (index/column 이름) - : 정수 위치 기반 인덱싱 (0-based) 이 두 indexer 가 pandas 선택의 표준. 만으로는 헷갈리…
- [Pandas] 성능 / 메모리 최적화pandas
- 정의 pandas 의 흔한 성능 함정과 최적화 패턴. 벡터화 + dtype 선택 + 알고리즘 의 조합이 핵심. 사용 상황 | 상황 | 권장 접근 | |:---|:---| | 단순…
- [Pandas] Boolean Indexingpandas
- 정의 Boolean Indexing 은 True/False 의 Series 를 인덱서로 전달 해 행을 선택하는 패턴. pandas 의 가장 흔한 필터링 방법. 마스크 생성과 적용…
- [Pandas] groupbypandas
- 정의 는 데이터를 그룹으로 나누고 (split), 각 그룹에 함수를 적용 (apply), 결과를 합쳐 (combine) 새 DataFrame 으로 만드는 split-apply-c…
- [Pandas] pipe / method chainingpandas
- 정의 는 DataFrame/Series 자체를 함수의 첫 인자로 넘겨 method chaining 흐름에 사용자 함수를 끼워 넣는 API. functional style pand…
💬 댓글