Pandas Copy Operations

Understanding .copy() and Memory Management

파이썬 pandas에서 .copy() 메서드의 역할과 중요성을 이해하고, 얕은 복사(shallow copy)와 깊은 복사(deep copy)의 차이점, 그리고 SettingWithCopyWarning이 발생하는 원인과 해결 방법을 알아본다. 데이터프레임 조작 시 메모리 관리와 예상치 못한 부작용을 방지하는 방법을 다룬다.

Engineering
Python
저자

Kwangmin Kim

공개

2023년 01월 02일

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 모범 사례

  1. 데이터를 수정할 목적으로 하위 집합을 만들 때는 .loc를 사용한다. view/copy 모호함 없이 항상 원본을 정확하게 수정한다.

    df_original.loc[df_original['A'] > 10, 'A'] = 0
  2. 하위 집합을 독립적으로 사용하고 싶다면, 명시적으로 .copy()를 붙여 복사본임을 확실히 한다.

    subset_independent = df_original[df_original['A'] > 10].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() 사용을 권장하는 경우:

  1. 함수 내에서 DataFrame 수정: 원본 데이터 보호
  2. 조건부 필터링 후 수정: SettingWithCopyWarning 방지
  3. 명시적 독립성 보장: 코드 의도를 명확히 표현
  4. mutable 객체 포함: 깊은 복사가 필요한 경우

8.2 copy()가 불필요한 경우:

  1. 단순 변환 체인: 메서드 체이닝만 사용
  2. 읽기 전용 작업: 데이터 수정이 없는 경우
  3. 집계/요약 작업: 새로운 구조의 결과 생성

8.3 핵심 원칙:

  • 의심스러우면 copy() 사용: 안전성 우선
  • 성능이 중요하면 프로파일링: 실제 영향 측정
  • 코드 의도를 명확히: copy() 사용으로 의도 표현
  • 팀 컨벤션 따르기: 일관된 코딩 스타일 유지

Subscribe

Enjoy this blog? Get notified of new posts by email: