1 Pandas Copy Operations 이해하기
- pandas에서
.copy()메서드는 데이터프레임이나 시리즈의 복사본을 만들 때 사용 - 메모리 독립성과 예상치 못한 부작용 방지를 위해 매우 중요한 개념
2 copy()와 view의 차이점
2.1 View
- View는 원본 데이터의 ‘바로가기’ 와 같다.
- 메모리 공유: View는 원본 데이터와 같은 메모리 공간을 바라본다. 데이터를 복사해서 새로운 공간을 만들지 않는다.
- 양방향 변경: View를 통해 데이터를 바꾸면 원본 데이터도 함께 바뀐다. 반대로 원본 데이터가 바뀌면 View도 그 변경을 그대로 보여준다.
- 성능: 데이터를 복사하지 않으므로 매우 빠르고 메모리를 효율적으로 사용한다.
| 개념 | 비유 | 설명 |
|---|---|---|
| 원본 DataFrame | 공유 클라우드 문서 (구글 시트) | 모든 사람이 보는 원본 파일 |
| View (뷰) | 문서의 ‘바로가기’ 링크 | 링크를 통해 문서를 열면 원본을 직접 수정하게 된다. |
| Copy (복사본) | 문서를 ’사본으로 저장’한 것 | 완전히 별개의 파일. 사본을 수정해도 원본은 그대로 있다. |
- View는 어떻게 만들어지나?
- pandas에서 view가 생성되는 가장 일반적인 경우는 단순 슬라이싱(slicing) 이다.
import pandas as pd
# 원본 데이터프레임
df_original = pd.DataFrame({
'A': [10, 20, 30, 40],
'B': [100, 200, 300, 400]
})
# 행 슬라이싱으로 view 생성
df_view = df_original[1:3] # 1번, 2번 행을 선택
print("--- 변경 전 ---")
print("원본:\n", df_original) # 원본 데이터
print("\nView:\n", df_view) # 원본 데이터의 1번, 2번 행을 선택한 view
--- 변경 전 ---
원본:
A B
0 10 100
1 20 200
2 30 300
3 40 400
View:
A B
1 20 200
2 30 300
df_view.loc[1, 'A'] = 999 # View의 1번 행, 'A'열 값을 변경
print("\n--- View 수정 후 ---")
print("원본 (함께 변경됨):\n", df_original) # 원본 데이터의 1번 행, 'A'열 값을 변경
print("\nView:\n", df_view) # View의 1번 행, 'A'열 값을 변경
--- View 수정 후 ---
원본 (함께 변경됨):
A B
0 10 100
1 999 200 <-- View를 바꿨는데 원본이 바뀜!
2 30 300
3 40 400
View:
A B
1 999 200
2 30 300
df_original.loc[2, 'B'] = 777 # 원본의 2번 행, 'B'열 값을 변경
print("\n--- 원본 수정 후 ---") # 원본 데이터의 2번 행, 'B'열 값을 변경
print("원본:\n", df_original) # 원본 데이터의 2번 행, 'B'열 값을 변경
print("\nView (함께 변경됨):\n", df_view) # View의 2번 행, 'B'열 값을 변경
--- 원본 수정 후 ---
원본:
A B
0 10 100
1 999 200
2 30 777 <-- 원본을 바꿈
3 40 400
View (함께 변경됨):
A B
1 999 200
2 30 777 <-- View도 함께 바뀜!3 View가 문제를 일으키는 경우: SettingWithCopyWarning
df[df['A'] > 20]처럼 조건부 필터링을 할 때, pandas는 상황에 따라 view를 반환할 수도 있고, 복사본을 반환할 수도 있다.- 이 모호함 때문에 문제가 발생한다.
subset = df_original[df_original['A'] > 10] # 이 결과가 view일까, copy일까?
subset['A'] = 0 # 이 코드는 의도대로 동작하지 않을 수 있다!- pandas는
subset이 view인지 복사본인지 헷갈리기 때문에 “네가 지금 수정하는 게 원본에 적용될지 안 될지 나도 모르겠어!” 라는 의미로SettingWithCopyWarning경고를 보낸다.
4 결론 및 해결책
- View: 원본 데이터의 ‘창문’. 메모리를 공유하며, 한쪽을 바꾸면 다른 쪽도 바뀐다.
- Copy: 원본 데이터의 ‘완전한 복사본’. 서로 영향을 주지 않는다.
4.1 모범 사례
데이터를 수정할 목적으로 하위 집합을 만들 때는
.loc를 사용한다. view/copy 모호함 없이 항상 원본을 정확하게 수정한다.하위 집합을 독립적으로 사용하고 싶다면, 명시적으로
.copy()를 붙여 복사본임을 확실히 한다.
4.2 기본 개념
import pandas as pd
# 원본 데이터
df_original = pd.DataFrame({
'A': [1, 2, 3, 4],
'B': [10, 20, 30, 40],
'C': [100, 200, 300, 400]
})
# copy() 사용 - 완전히 독립적인 복사본
df_copy = df_original.copy()
# copy() 없이 할당 - 같은 객체를 참조
df_reference = df_original
# view (슬라이싱) - 원본 데이터를 다른 관점에서 보는 것
df_view = df_original[['A', 'B']] # 새로운 DataFrame (copy)
df_slice = df_original.iloc[1:] # 새로운 DataFrame (copy)4.3 메모리 독립성 테스트
print("=== 메모리 주소 확인 ===")
print(f"원본 ID: {id(df_original)}")
print(f"copy() ID: {id(df_copy)}")
print(f"참조 ID: {id(df_reference)}")
print(f"원본과 copy 같은 객체?: {df_original is df_copy}") # False
print(f"원본과 참조 같은 객체?: {df_original is df_reference}") # True
# 값 변경 테스트
df_copy.loc[0, 'A'] = 999
df_reference.loc[0, 'B'] = 888
print("\n=== 변경 후 결과 ===")
print("원본:")
print(df_original)
print("\ncopy():")
print(df_copy)```5 SettingWithCopyWarning의 실제 원인
5.1 경고가 발생하는 실제 케이스
import pandas as pd
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
df = pd.DataFrame({
'A': [1, 2, 3, 4, 5],
'B': [10, 20, 30, 40, 50],
'C': ['X', 'Y', 'Z', 'X', 'Y']
})
# SettingWithCopyWarning이 발생하는 케이스
# 1. 체인드 인덱싱 (chained indexing)
try:
subset = df[df['C'] == 'X'] # 조건부 선택
subset['new_column'] = 100 # 여기서 경고 발생 가능
print("체인드 인덱싱으로 경고 발생 가능")
except:
pass
# 2. 해결 방법 - copy() 사용
subset_safe = df[df['C'] == 'X'].copy()
subset_safe['new_column'] = 100
print("copy() 사용으로 경고 방지")
# 3. 더 나은 방법 - loc 사용
df.loc[df['C'] == 'X', 'new_column'] = 100
print("loc 사용으로 직접 수정")5.2 drop_duplicates()에서 copy()가 필요한 경우
# 일반적인 경우 - copy() 없이도 안전
df_dedup = df.drop_duplicates(subset=['C'])
df_dedup['processed'] = True # 경고 없음
print("일반적인 경우는 copy() 없이도 안전:")
print(df_dedup)
# copy()가 필요한 경우 - 명시적으로 독립성을 보장하고 싶을 때
df_dedup_safe = df.drop_duplicates(subset=['C']).copy()
df_dedup_safe['definitely_safe'] = True
print("\ncopy() 사용으로 명시적 독립성 보장:")
print(df_dedup_safe)6 얕은 복사 vs 깊은 복사
- df.copy(deep=True)는 pandas에서 구조적인 복사만 깊게 하고, 셀 내부 객체는 얕게 복사한다.
- 만약 데이터프레임 안에 list, dict, set 등 mutable 객체가 들어 있다면 copy.deepcopy()를 써야 진짜로 독립된 복사본이 된다.
- 대부분의 수치 데이터나 문자열 기반 작업에서는 df.copy(deep=True)로 충분하지만, mutable 객체를 다루는 특수한 상황에서는 copy.deepcopy()로 관리한다.
6.1 기본 copy() - 얕은 복사
import pandas as pd
# mutable 객체를 포함한 데이터
df_with_lists = pd.DataFrame({
'id': [1, 2, 3],
'data': [[1, 2], [3, 4], [5, 6]] # mutable 리스트
})
# 얕은 복사 (기본 copy())
df_shallow = df_with_lists.copy()
# 리스트 내부 수정
df_shallow.loc[0, 'data'][0] = 999
print("원본 데이터 (리스트 내부가 변경됨):")
print(df_with_lists)
print("\n얕은 복사본:")
print(df_shallow)6.2 깊은 복사
# 깊은 복사
df_deep = df_with_lists.copy(deep=True)
# 또는 copy 모듈 사용
import copy
df_deep_alt = copy.deepcopy(df_with_lists)
# 새로운 원본 데이터로 테스트
df_original = pd.DataFrame({
'id': [1, 2, 3],
'data': [[1, 2], [3, 4], [5, 6]]
})
df_deep = df_original.copy(deep=True)
df_deep.loc[0, 'data'][0] = 999
print("깊은 복사 후 원본 (변경되지 않음):")
print(df_original)
print("\n깊은 복사본 (변경됨):")
print(df_deep)7 실제 메모리 공유 케이스
- pandas에서 실제로 메모리를 공유하는 경우는 매우 제한적이다.
# 실제 메모리 공유가 발생하는 케이스
df = pd.DataFrame({
'A': [1, 2, 3, 4],
'B': [10, 20, 30, 40]
})
# 컬럼 선택 - 새로운 DataFrame
series_a = df['A'] # 새로운 Series
df_subset = df[['A']] # 새로운 DataFrame
print(f"원본 DataFrame과 컬럼 시리즈 메모리 공유?: {df['A']._mgr is series_a._mgr}")
print(f"원본 DataFrame과 서브셋 메모리 독립적: {df is df_subset}")
# 실제 공유되는 것은 내부 데이터 블록
print(f"데이터 배열 주소 - 원본: {id(df.values)}")
print(f"데이터 배열 주소 - 컬럼: {id(df['A'].values)}")8 결론 및 권장사항
8.1 copy() 사용을 권장하는 경우:
- 함수 내에서 DataFrame 수정: 원본 데이터 보호
- 조건부 필터링 후 수정: SettingWithCopyWarning 방지
- 명시적 독립성 보장: 코드 의도를 명확히 표현
- mutable 객체 포함: 깊은 복사가 필요한 경우
8.2 copy()가 불필요한 경우:
- 단순 변환 체인: 메서드 체이닝만 사용
- 읽기 전용 작업: 데이터 수정이 없는 경우
- 집계/요약 작업: 새로운 구조의 결과 생성
8.3 핵심 원칙:
- 의심스러우면 copy() 사용: 안전성 우선
- 성능이 중요하면 프로파일링: 실제 영향 측정
- 코드 의도를 명확히: copy() 사용으로 의도 표현
- 팀 컨벤션 따르기: 일관된 코딩 스타일 유지