questionet

판다스 SettingWithCopyWarning: ChainedAssignmentError: 본문

카테고리 없음

판다스 SettingWithCopyWarning: ChainedAssignmentError:

orthanc 2025. 7. 11. 15:26

amos_rawdata.csv
3.46MB
COW.ipynb
0.04MB

안녕하세요.

 

7월 10일 목요일

Day-3 판다스 강의에서

마지막 시간에 진행한 AMOS 프로젝트 실습 내용 중

수강생 중 한분이 주신 질문에 대해

답변을 작성해보았습니다.

 

최대한 이해하시기 쉽게 풀어서 설명했습니다

(쉽게 쓰려다보니 특정 부분에선 기술적 엄밀함과 정확성이 약간 부족해졌지만

해당 부분들은 입문자 수준에선 크게 문제가 되지 않는다고 생각되어

이점 양해 부탁드립니다.)

 

직접 코드를 돌려보실 수 있도록 노트북 파일로도 첨부해 드립니다.

 

먼저 문제를 살펴보기 전에

필요한 배경지식을 설명드리고,

간단한 예제로 문제상황을 짚어본 후

실습시간에 문제가 되었던 코드로 들어가겠습니다.

 

--------------------------------------------이하본문--------------------------------------------

파이썬에서 모든 건 객체(object)입니다.

객체? object?

찬찬히 설명드려 볼게요. (일단 낯선 용어는 그렇게 부르는구나 정도로 받아들여주시고 따라와주세요.)

 

파이썬에서는

 

정수 3도 영문자 "c" 도 전부 객체입니다.

여러분이 터미널에 파이썬을 실행하고
a = 1
이라고 입력할 때

변수 a은 순수하게 숫자 1 만을 가리키는 것이 아닙니다.

변수 a가 숫자 1을 나타내는 것으로 간주하거나
변수 a에 숫자 1이 담겨 있다는 표현도
엄밀히 말하면 정확한 표현이 아닙니다.

파이썬이라는 프로그래밍 언어에서는
a = 1
b = 2
a + b

위 세줄의 코드를 연달아 입력했을 때
3 이 출력됩니다.

사칙연산과 기초영어를 배운 인간이라면
너무나 당연한 사실이고 저렇게 쓰는게 너무나 자연스럽습니다.

파이썬을 만든 사람들은 위의 예처럼 코딩을 할 때
인간이 느끼는 방식과 최대한 유사하도록
당연하고 자연스럽게 느끼면서 코딩할 수 있도록 하기 위해서
'객체' 라는 걸 만들었습니다.

파이썬 인터프리터에서
type(1)
을 실행해보면
<class 'int'>
이렇게만 뜨는걸 볼 수 있습니다.

'int' 즉 integer, 정수라는 클래스구나, 이렇게만 이해하고 넘어가도 됩니다.

또는 '정수' 라는 클래스가 1 이라는 숫자의 타입이구나. 라고 이해하고 넘어가도 충분합니다.

마치 우리가 숫자 1을 보면 그냥 1이구나 라고 느끼는 것처럼요.

지금 여러분 앞에 모니터가 1개 놓여 있을 때, "모니터가 1개 있다" 라고 생각하고
그 옆에 마우스가 1개 놓여 있을 때, "마우스도 1개 있다" 라고 생각하며
또 "키보드도 1개 있다" 라고 생각하면서
이런 식으로 여러분 주변의 현실세계에서 사물의 개수를 세어 나간다고 해볼게요.

여러분이 1 이라는 숫자로 표현 될 수 있는 개수의 사물들을
전부 '1개' 라고 생각하며 말할 수 있는건

여러분이 1 이라는 숫자의 의미를 알고, 그 의미를 계속 기억하고, 그 의미를 가진 1이라는 숫자를
서로 다른 사물들에 계속 붙여 나갈 수 있기 때문입니다.

여러분의 기억력, 사고력, 인지능력, 표현능력 등 여러가지 복합적인 능력이 발휘되어야
여러분 주변의 사물에 '1개' 라는 이름을 붙일 수 가 있습니다.

컴퓨터도 비슷합니다.

파이썬 인터프리터에 type(1) 이라고 입력하면
<class 'int'> 라고 출력되지만

저 'int' 클래스가 뭔지 추적해 들어가보면

숫자 1은 사실 아래와 같은 코드로 연결됩니다.

(아래 코드는 파이썬 언어가 아니라 Cpython 이라는 언어로 짜여진 언어입니다.
파이썬 코드를 실행하면 내부적으로 호출되는 코드들이고
코드 자체는 모르셔도 됩니다.
// 기호 옆의 주석을 읽어주세요.
이 내용을 건너뛰고 싶으시다면
스크롤을 내리셔서 빨간 골뱅이 @ 부분부터 다시 이어 읽어나가 주세요)

typedef struct {
Py_ssize_t ob_refcnt; // 참조 카운트
PyTypeObject* ob_type; // 타입 정보
Py_ssize_t ob_size; // 가변 객체의 크기 (PyVarObject용)
digit ob_digit[1]; // 정수 값 데이터
} PyLongObject;

 

'참조 카운트'가 뭘까요?

참조 카운트는 쉽게 말해 아까 모니터, 키보드, 마우스 처럼
여러분 주변 사물에 1개씩 있는 물체들에게
1개 라는 표현을 썼을 때,
1이라는 숫자를 계속해서 사용해 물체들을 셌듯이,

파이썬에서
a = 1
b = a
위와 같은 코드를 실행했을 때
변수 a와 b가 동시에 1을 가리키고 있기 때문에,

(a = 1 인데 b = a 니까 b 도 가 가리키는 1을 가리키게 됩니다)

여기서 1은 두번 세어지고 있다고 볼 수 있습니다.
다시 말해 1의 참조카운트는 2가 됩니다.

위와 같이 1이라는 숫자는 코딩을 하면서 수없이 많은 다른 변수에 의해 참조될 수 있고
그 프로그램에서 1 이라는 값이 몇번이나 가리켜지고 있는지,
다시 말하면 참조되고 있는 수를 카운트한 것이라고 보시면 됩니다.

'타입 정보'는 뭘까요?

여러분은 1을 '숫자'라고 이해합니다. 더 정확히는 정수라고 부르죠.
이 정보를 가진게 타입 정보입니다.

'가변 객체'의 크기는 뭘까요?

여러분은 숫자 1을 뇌속에 있는 수많은 뉴런들 중 어느 하나에 저장하고 있습니다.
그 때 그 숫자 1을 기억하기 위해 얼마만큼의 물리화학적인 공간이 필요하겠죠.
컴퓨터도 1을 저장하고 기억하기 위해 메모리라는 곳에 숫자 1을 기록해놓기 위한 물리적인 공간이 필요합니다.

정수 값 데이터 라는 건 뭘까요?

인간은 숫자 1을 뉴런들 속에 정확히 어떤 형태로 쓰고 기억할까요?
컴퓨터는 2진수를 써서 전기신호를 켰다가 끄는 방식을 조합하여 숫자 1을 정의합니다.
그 이진수들이 담긴 배열이 정수 값 데이터라고 보시면 됩니다.

지금까지 얘기들은 그냥 다 잊어버리셔도 됩니다.
왜냐면 typedef struct 로 시작하는 위 몇줄의 코드들 중 두번째 줄에 있는
PyTypeObject* ob_type; 라는 코드는
사실 아래 보이는 또 다른 객체를 가리키고 있습니다

typedef struct _typeobject {
const char *tp_name; // 타입 이름
int tp_basicsize; // 객체 크기
destructor tp_dealloc; // 소멸자
printfunc tp_print; // 출력용 함수
getattrfunc tp_getattr; // getattr
...
struct _typeobject *tp_base; // 상속
PyObject *tp_dict; // 클래스 속성 딕셔너리
...
} PyTypeObject;

또 세번째 줄에 있는 Py_ssize_t ob_size 라는 코드도 또 다른 객체를 가리키고 있고요.

@ 위에 대해선 설명하지 않겠습니다. 컴퓨터에서 파이썬이 돌아가는 방식을 구체적으로 알 필요는 없어요.

여러분이 숫자 1을 현실세계에서 사용할때 뇌속에서 구체적으로 어떤 일이 일어나는 지를
몰라도 되는 것처럼요.

객체란,

파이썬에서 1이라는 숫자를 사용할 때, 인간이 1이라는 숫자를 사용하듯 직관적이고 편리하고 자유롭게 쓰기 위해
1이 무엇이고 어디에 쓰이고 있고(상태), 어떻게 쓰일 수 있는지(행동)에 관한 정보 등등을 담아 놓은 꾸러미 같은거라고 이해하셔도 좋습니다.

파이썬에서는 이 객체로 모든 걸 표현합니다. 변수든, 값이든, 함수든 호출할 수 있는 모든걸요.

다시 본론으로 돌아갈게요. 지금까지 말씀드린 이야기의 요점은
저런 복잡한 것들을 다 몰라도

a = 1
b = 2
a + b

위와 같이 쓰기만 하면 3이 출력될 수 있게 하기 위해
파이썬은 '객체' 라는 개념을 사용해서
복잡한 것들은 다 '객체'안에 집어 넣고
변수 a는 그 객체 1을 가리키게만 하면 되게끔
파이썬이 구현되어 있다는 사실입니다.

"파이썬에서 모든 건 객체(object)입니다." 라는 이 글의 첫번째 문장은
바로 이런 뜻입니다.

변수 a가 숫자 1을 나타내는 것으로 간주하거나
변수 a에 숫자 1이 담겨 있다는 표현을
더 정확히 표현해보면
이제 다음과 같이 말할 수 있습니다.

“a는 값이 1인 객체를 참조한다”

이제부터는 표현을 간단히 하기 위해서
그냥 변수 a 가 1을 참조한다 라고만 쓰겠습니다.

a = 3   # 변수 a 는 3을 참조
b = a   # 변수 b 는 a를 참조

print(f"a : {a}")
print(f"b : {b}")

# 아래와 같이 출력됩니다.
# a : 3
# b : 3
a = 4  # a가 4를 참조하도록 바꿨습니다.

print(f"a : {a}")
print(f"b : {b}")

# 아래와 같이 출력됩니다.
# a : 4
# b : 3
# 당연해 보이죠?
a = [1, 2, 3]  # 이번엔 a가 리스트를 참조하도록 했습니다.
b = a

print(f"a : {a}")
print(f"b : {b}")

# 아래와 같이 출력됩니다.
# a : [1, 2, 3]
# b : [1, 2, 3]
a[0] = 100  # 리스트에서 첫번째 원소1을 100으로 바꿨습니다.

print(f"a : {a}")
print(f"b : {b}")

# a : [100, 2, 3]
# b : [100, 2, 3]  자 뭔가 이상하죠? b에서도 1이 100으로 바꼈습니다.

 

위에서

a = 3
b = a
a = 4

를 했을 때

b는 여전히 3을 가리켰습니다. a 가 가리키던걸 바꾼다고 b 가 가리키던게 바뀌진 않았죠.

 

그런데 a가 리스트를 가리켰을 땐

리스트 내부를 바꿨더니

b도 똑같이 바꼈습니다. a 를 바꿨더니 b도 바꼈죠.

 

3, "c" , True 처럼 정수형, 문자열, 불리언 같은 자료형(type)을

이뮤터블(immutable) 객체라고 부릅니다.

 

이뮤터블 객체들을 가리키는 변수는

새로운 이뮤터블 객체를 참조하면 이전 참조를 버리고 새로운 참조를 만듭니다.

a = 3

a = 4

위 두 코드를 연달아 실행하면

변수 a는 3을 참조했다가, 4를 참조하게 되는데

이전에 3을 참조했던 a는 사라집니다.

 

쉽게 말하면, a는 새로운 이뮤터블 객체를 참조하도록 다시 태었났다고 볼 수 있습니다.

 

그런데 리스트는 다릅니다.

a = [1,2,3]

a[0] = 100

위 코드에서 리스트 [1,2,3] 을 가리켰던 변수 a는

a[0] = 100 을 한다고 해서 자신이 가리키던 리스트를 버리지 않습니다.

계속 그 리스트 객체를 가리키고 있어요. 심지어 그 내부에 있는 첫번째 원소를 바꿔도

여전히 가리킵니다.

다시 말해

a = [1,2,3] 을 하고

a[0] = 100 을 했을 때

a가 가리키고 있던 리스트 객체 [1,2,3] 을 버리고

새로운 리스트 객체 [100, 2, 3]을 가리키게 되는 게 아니라

기존에 가리키던 [1,2,3] 객체에서 첫번째 원소인 1을 100으로 바꾼 객체를

그대로 가리키게 된다는 말입니다.

 

리스트 같은 객체를 뮤터블(mutable) 이라고 합니다.

 

a = [1,2,3] 을 했을 때 a가 가리키는 리스트 객체 [1,2,3] 을

a가 계속해서 가리킵니다.

 

뮤터블, 이뮤터블 개념을 알아두고 있으면 앞으로 말씀드릴 내용을 이해하는데

더 편해집니다.

 

pandas의 데이터프레임(DataFrame)과 그 안에 들어 있는 칼럼(Series)들은

기본적으로 뮤터블 객체입니다.

 

리스트를 생성하면 그 안에 있는 원소들을 바꿀 수 있고, 내부 데이터가 바뀐 리스트를

기존의 변수가 계속해서 가리킬 수 있듯이

데이터프레임도 한 번 생성된 이후에 내부 데이터를 직접 바꾸거나 덮어쓸 수 있습니다.

 

판다스의 데이터프레임에 있는 각 칼럼들은 비유하자면 리스트와 비슷합니다.

뮤터블 객체에요. 안에 있는 값들을 바꿀 수 있죠.

다시 말해 데이터프레임은 원본이 바뀔 수 있는 객체들입니다.

 

df1 = pd.DataFrame({'a': [1, 2]})   # df1 은 a 라는 이름의 칼럼에 1, 2 값을 가지고 있습니다.
print("df1 :");print(df1,"\n")

df2 = df1  # df2는 df1 을 참조합니다.
df2['a'][1] = 100  # df2의 a 칼럼의 첫번째 값 1을 100으로 바꿉니다.

print("df2 :");print(df2, "\n")

print("df1 :");print(df1,"\n") # df2를 바꿨는데 df1도 바뀌어 버렸네요.
df1 = pd.DataFrame({'a': [1, 2]})   # df1 은 a 라는 이름의 칼럼에 1, 2 값을 가지고 있습니다.
print("df1 :");print(df1,"\n")

df2 = df1  # df2는 df1 을 참조합니다.
df2['a'][1] = 100  # df2의 a 칼럼의 첫번째 값 1을 100으로 바꿉니다.

print("df2 :");print(df2, "\n")

print("df1 :");print(df1,"\n") 

# 실행 결과는 아래와 같습니다. df2를 바꿨는데 df1도 바뀌어 버렸네요.
df1 :
   a
0  1
1  2 

df2 :
     a
0    1
1  100 

df1 :
     a
0    1
1  100 

/tmp/ipykernel_5891/2820178104.py:5: FutureWarning: ChainedAssignmentError: behaviour will change in pandas 3.0!
You are setting values through chained assignment. Currently this works in certain cases, but when using Copy-on-Write (which will become the default behaviour in pandas 3.0) this will never work to update the original DataFrame or Series, because the intermediate object on which we are setting values will behave as a copy.
A typical example is when you are setting values in a column of a DataFrame, like:

df["col"][row_indexer] = value

Use `df.loc[row_indexer, "col"] = values` instead, to perform the assignment in a single step and ensure this keeps updating the original `df`.

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

  df2['a'][1] = 100  # df2의 a 칼럼의 첫번째 값 1을 100으로 바꿉니다.


위 코드에서 보셨듯이

 

df1을 df2가 참조하게 하고
df2를 수정했더니 df1도 바꼈습니다.

원본 데이터를 함부로 수정하는 일은 위험합니다.
특히 중요한 문서라면 더욱 그렇죠.

원본을 수정할 땐 항상 사본을 만들어서,
원본의 어디가 바꼈는지를 참고할 수 있게 하는 일처리 방식이 필요할 때가 많지요.
그래서 문서의 수가 굉장히 많이 늘어나기도 하지만요.

위 판다스 코드에서
왜 원본이 수정됐는지를 자세히 파고 들어가지 않겠습니다.

여러분이 여기서 아셔야 할 건,
"판다스를 쓸 때 원본이 수정될 위험이 있는 방식으로 코딩할 수도 있구나" 입니다.

그래서 판다스는 경고메시지를 준 것입니다.
FutureWarning: ChainedAssignmentError:
라는 제목으로요.
(이 경고메시지를 여기서 자세히 다루진 않겠습니다. 아래서 다시 설명할게요)

이와 비슷한 경고를 7월 10일 화요일,
Day-3 교육의 마지막에 실습하셨던 프로젝트 때도 보실 수 있었습니다.

"[Project] 공항기상관측(AMOS) 기상청 데이터 분석" 에서
양양공항의 풍속 단위를 노트가 아닌 m/s 로 바꿔보는 실습 기억나시나요?

def knot_to_ms(knot):
    return knot * 0.514444

위와 같은 함수를 아래처럼 사용하셨죠.

amos_yang = amos_all[amos_all['지점명'] == '양양공항']
amos_yang['풍속(m/s)'] = knot_to_ms(amos_yang['풍속(KT)'])

실행결과 문제없이 amos_yang에 '풍속(m/s)' 라는 새로운 칼럼이 잘 생깁니다.

하지만 아래와 같은 경고가 함께 출력됩니다.

SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  amos_yangyang['풍속(m/s)'] = knot_to_ms(amos_yangyang['풍속(KT)'])

위 경고메시지는
앞서 살펴본 df2를 바꿨더니 df1도 바뀌면서 출력됐던
FutureWarning: ChainedAssignmentError: 경고 메시지와
같은 맥락의 경고입니다.

간단히 설명해 드리면,
"지금 당신이 실행한 코드의 의도가 원본을 수정하고 싶었던 것이라면 loc 메서드를 써서 값을 바꾸라"
는 내용입니다.

df1 = pd.DataFrame({'a': [1, 2]})
df2 = df1
df2['a'][1] = 100

위 코드에서는 원본이 바꼈지만
amos_yang에 새로운 칼럼을 넣었다고 해서 amos_all 원본이 바뀌진 않았습니다.

그런데도 판다스가 같은 맥락의 경고메시지를 준 까닭은

amos_yang = amos_all[amos_all['지점명'] == '양양공항']
df2['a'][1] = 100

이런 방식의 코딩이

df2['a'][1] = 100


이와 같은 코드처럼 원본을 수정할 가능성이 있는
본질적으로 위험한 방식의 코딩이기 때문입니다.

(위 두가지 코드와 같은 코딩방식을 판다스에선 체이닝 chaining 이라고 합니다.
여기서 더 깊이 들어가진 않을게요. 문제의 근본적인 원인이 아니라,
이후 부터는 대략적인 원인만 짚어드린 후,
문제에 맞닿뜨릴 수 있지 않게 하는 해결방법만 알려드리겠습니다.

바로 해결방법만 보고 싶으신 분들은
스크롤을 내리셔서 빨간 골뱅이 @@ 부분부터 다시 이어 읽어나가 주세요)

더 관심 있으신 분은 아래 참고링크를 참고해주세요.;

    1. https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html
    2. https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy

만약 여러분이 amos_all 원본을 수정하거나 원본에 새로운 데이터를 추가하려는 목적이 아니라,

amos_all 원본의 일부분을 읽기 전용으로 좀 더 보기 쉽게 보기 위해

amos_yang = amos_all[amos_all['지점명'] == '양양공항'] 

이렇게 코딩해서

amos_yang을 보기만 하려는 거라면 아무 문제도 없습니다.
실제로 위 코드를 실행한후
amos_yang 을 출력해서 보기만 할 땐

경고메시지는 출력되지 않습니다.

문제는 이렇게 추출한 amos_yang으로

amos_yang['풍속(m/s)'] = knot_to_ms(amos_yang['풍속(KT)'])

 

이와 같이 amos_yang 내부의 데이터를 변환하는 코드를 연이어 썼을 때,
amos_all 원본이 수정될 수도 있는 가능성이 존재하기 때문에
판다스가 경고 메시지를 준 것입니다.

 

여기까지 잘 따라오셨다면, 이런 의문이 드실겁니다.

"그런데 amos_all을 봤더니 '풍속(KT)' 칼럼 값은 바뀌지 않고 그대로 있잖아?
뭐가 문제라는 거야?"

실제로

amos_yang = amos_all[amos_all['지점명'] == '양양공항']
amos_yang['풍속(m/s)'] = knot_to_ms(amos_yang['풍속(KT)'])

위 코드를 실행한 후
amos_all을 보면 바뀐 것은 아무것도 없습니다.

왜일까요?

정말 쉽게 말씀드리면, 판다스가 꽤 똑똑하게 굴었기 때문입니다.

여기에는 굉장히 여러가지 이유와 메커니즘이 작동하고 있습니다.
몇 가지만 말씀드리자면,

amos_yang = amos_all[amos_all['지점명'] == '양양공항']

위 코드를 실행했을 때
amos_yang에는 양양공항 데이터만 담깁니다.
amos_all 원본보다 훨씬 적은 데이터만 담기죠. 즉 인덱스가 달라집니다.

판다스는 이렇게 일부를 슬라이싱한 데이터에 변환이 가해진 경우
(우리의 경우엔 amos_yang['풍속(KT)'] 에 knot_to_ms 함수를 적용했죠)

그리고 원본인 amos_all 이 아닌 amos_yang 이라는 원본의 참조에
'풍속(m/s)' 이라는 새로운 칼럼으로 변환된 값을 적용하는 경우

이건 코드의 의도가 원본을 바꾸려는 게 아니구나, 라고 내부적으로 판단하여
원본을 바꾸지 말고
원본의 사본을 만들어서 (정확하게는 amos_yang['풍속(KT)'] 의 사본을 만들어서)
노트 단위를 m/s 단위로 바꾼 값을 적용시킨 겁니다.

그러면

df1 = pd.DataFrame({'a': [1, 2]})
df2 = df1
df2['a'][1] = 100

여기선 왜 원본을 바꿔버렸을까요?

df2는 원본 df1 자체를 참조하고 있죠.
이 상태에서 df2의 값을 바꾼다면
판다스 입장에서도,
"df1의 일부를 슬라이싱해서 쓰는 것도 아니니 원본도 바꿔야겠다."
라고 판단한겁니다.
그래도 여전히 친절히
경고 메시지를 남겨주었죠.

그 경고메시지를 다시볼까요?

when you are setting values in a column of a DataFrame, like:
df["col"][row_indexer] = value
Use `df.loc[row_indexer, "col"] = values` instead


간단히 번역해보면,

"df2['a'][1] = 100 이런 식으로 코딩할 거면
df2.loc[0, 'a'] = 100 이렇게 코딩해서
원본을 수정할 의도가 있다는 걸 명시적으로 알 수 있게 코딩해"

라는 경고입니다.

양양공항 케이스에서의 경고메시지도 다시 볼까요?

SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

 

이것도 간단히 번역해보면,
"넌 지금 데이터프레임의 슬라이싱한 데이터의 카피본에 새로운 값을 넣으려 하고 있어.

(맞죠? amos_all에서 양양공항만 슬라이싱한 amos_yang의 '풍속(KT)' 사본을 만들어서
노트를 m/s 로 바꿔 '풍속(m/s)에 넣으려고 했으니까요)

만약 너가 이 작업으로 원본 수정을 의도한 거라면
amos_all.loc[df['지점명'] == '양양공항', "풍속(m/s)"] = knot_to_ms(amos_all.loc[df['지점명'] == '양양공항', '풍속(KT)'])
위와 같이 코딩해."

라는 경고입니다.

좀더 쉬운 예로 써드릴게요.

df1 = pd.DataFrame({'a': [1, 2]})
print("df1 :");print(df1,"\n")

df2 = df1
# df2['a'][1] = 100  # 이렇게 하지말고
df2.loc[0, 'a'] = 100 # 이렇게 하면 명시적으로 원본을 수정하려는 의도를 나타내는 코드로 읽힐수 있다.

print("df2 :");print(df2, "\n")

print("df1 :");print(df1,"\n")

# 실행결과는 아래와 같습니다. df1 원본은 수정되면서, 경고메시지도 출력되지 않습니다.
df1 :
   a
0  1
1  2

df2 :
     a
0  100
1    2

df1 :
     a
0  100
1    2

 

 

"그래 알겠어. 내가 데이터프레임 원본을 가져다가 일부를 따서

어떤 작업을 할 때, 데이터 프레임 원본이 수정되어도 괜찮은 작업이라는 걸

판다스와 같이 작업하는 동료들에게도

코드상에서 명시적으로 보여주고 싶으면

loc 메서드를 쓰라는 거지?

 

근데 나는 원본을 수정할 의도가 없어.

무엇보다 amos_yang 에서 풍속단위를 바꿀 때 경고메시지가 떴지만

어쨌든 아무문제도 없었잖아?

 

내가 원본을 수정할 의도가 없을 땐 어떻게 해야되지?"

 

제가 이 문제를 설명드리기 위해 판다스 공식 문서를 찾아보았는데요,

이 문제가 판다스 정책상 꽤 많은 이슈를 가져왔던 문제였고

정책 자체도 여러번 바뀌었다는 사실을 알게 되었습니다.

 

그 배경엔 속도와 안정성 이라는 트레이드 오프가 있었습니다.

 

항상 원본을 보존하도록 하는 정책이라면

 

원본을 수정할 위험이 있는 판다스 코드일 경우

무조건 사본을 만들어 연산되도록 해야겠죠?

그런데 뭘 할때마다 사본을 만들면

회사 캐비닛이 무거워지고, 뭐 하나 좀 거슬러 올라가서 찾아보려면 한세월 걸리고

판다스에서도 똑같은 이슈가 생깁니다.

 

그래서 원본을 보존하지 않는 정책에서는

명시적으로 원본을 수정할 의도가 있는 코드인지 알수 있는 코드로 코딩하고

사본이 필요하면, 사본을 생성하는 코드를 일일이 적어야했죠.

근데 이것도 하다보면 문제가 생깁니다.

번거롭죠. 알아서 사본을 생성해서 원본을 보존해주면

내 마음대로 더 간단한 코드로 (loc 메서드보단 직접 인덱싱하는게 더 쉽죠?)

수정할 수 있으면 편하니까요.

 

이제 결론을 말씀드리겠습니다.

 

print(pd.options.mode.copy_on_write)

 


위 코드를 판다스가 설치된 파이썬 인터프리터에서 실행시키시면
False가 나오실 겁니다.
True가 나올 수도 있는데 그건 판다스 버전에 따라 다릅니다.
(제가 테스트 하는 환경의 판다스 버전은 2.3.0 입니다.)

pd.options.mode.copy_on_write 이게 뭐냐면

copy on write 즉 쓰기 작업을 할 때 (원본은 수정할 때) 사본을 만들어서 할지 말지를
정하는 정책을 나타내는 용어입니다.

True 면 원본을 슬라이싱이나 필터링으로 참조하는 객체가
원본을 공유하는 상태일 때,
값을 변경하려고 시도하면 pandas가 그 시점에 자동으로 복사본을 생성하여 원본 보호를 보장하고요,

False면 원본이 수정될 수 있습니다.

"어라, 난 프린트해보니까 False로 나오는데,
왜 amos_yang 에선 문제가 없었던거지?"

위에서 말씀드렸다시피, 판다스 버전별로 또 내부적으로 이슈가 많아서
pd.options.mode.copy_on_write : False 로 되어있다 하더라도
대개 문제가 없도록 사본을 만들어 수정하도록 구현돼 있습니다.
여러가지 조건, 상황에 따라 유연하게 적용되는 거죠.
(판다스 버전마다 다릅니다)

복잡하죠?

@@ 그래서 결론은 세가지 입니다. 셋중 마음에 드는 걸 선택해서 판다스를 학습하세요

1.
경고메시지를 그냥 다 꺼버리고 학습을 진행하세요.

import warnings
warnings.simplefilter(action='ignore', category=Warning)
pd.options.mode.copy_on_write = True

위 코드를 입력하면, 에러메시지가 아닌 경고메시지는 모두 출력되지 않습니다.
하다가 원치 않는 원본수정 같은 문제가 발생하셨다면
그 부분이 어딘지 일일이 찾아서
디버깅을 해보세요.

경고메시지가 뜨지 않아도 결과적으로 문제가 없었다면 눈이 편하고
신경쓸게 줄어드니까 인지부하도 줄어들겠죠?

대부분은 안전합니다. pd.options.mode.copy_on_write = False 여도
어지간하면 내부적으로 복사본을 만들어 연산해 결과를 돌려줘요.

 

2.

pd.options.mode.copy_on_write = True


위 코드를 입력하고, 모든 경고메시지를 무시하고, 그냥 코딩하세요.

위 코드는 필요한 경우 사본을 생성해 원본이 수정될 수 있는 위험을 피하도록
판다스가 판단합니다.

학습수준에선 크게 문제될 일이 일어나진 않습니다. (최소한의 보호작용은 False 에도 해당됩니다)


그래도 여전히 위험소지가 있는 코드에 대해선 경고메시지를 출력합니다.

하다가 원본이 수정되면, 뭐 처음부터 다시 하세요.
1번 보다는 낫습니다. 2번 방법에선 최소한 경고메시지가 뜬 부분만 보면 되니까요.

배우는 과정에서 실수는 저지를수록 좋습니다.

지금까지 제가 '원본' 이라는 용어로 가리켰던 건,
컴퓨터 메모리상에 올라와 있는 가상의 데이터로서의 원본 입니다.

실제 파일은 하드디스크에 잘 저장돼 있어요. (변경된 원본을 여러분이 하드디스크에 저장된 원본에 덮어써서 저장하지만 않는다면요)

pd.read_csv()메서드로 다시 불러와서 하시면 됩니다.
하다가 원본이 수정되는 부분이 어딘지 찾고

그 다음에 문제를 회피할 수 있는 방법을 찾으시면 됩니다.

3.
원본을 항상 보존하는 코딩을 명시적으로 그리고 결과적으로도 남기고 싶으시다면

 

pd.options.mode.copy_on_write = True

위 코드를 실행하시고

원본이 수정될 위험이 있는 코드를 작성하는 경우 copy() 메서드를 사용하세요.

 

df1 = pd.DataFrame({'a': [1, 2]})
print("df1 :");print(df1,"\n")

df2 = df1.copy()  # df1의 복사본으로서의 df2를 생성

df2["a"][1] = 100  # 이전에는 df1도 변경됨 → 이제는 df1은 그대로, df2만 바뀜
print("df2 :");print(df2, "\n")

print("df1 :");print(df1,"\n") 

# 실행결과는 아래와 같습니다. df1은 그대로죠?
df1 :
   a
0  1
1  2 

df2 :
     a
0    1
1  100 

df1 :
   a
0  1
1  2 

 

 

 

amos_yang 케이스도 동일합니다.

amos_yang = amos_all[amos_all['지점명'] == '양양공항'].copy()
amos_yang['풍속(m/s)'] = knot_to_ms(amos_yang['풍속(KT)'])

위와 같이 코딩하면, 경고메시지 출력도 안될뿐더러 원천적으로 원본 수정 가능성을 차단합니다.

"그럼 원본을 보존하고 싶으면 매번 copy()를 하라는거야?
어떤 판다스 코드가 원본 수정될 위험이 있는지 없는지 매번 어떻게 알지?"

 

네 그렇습니다. 매번 해야 하고, 알려진 레퍼런스를 통해, 경험을 통해 알아야 합니다.


그래서 판다스의 Copy On Write 정책이 계속 바뀌고, 내부적으로 개선책을 강구중인거 같아요.

여러분 실습 수준에선 문제가 없지만,
나중에 규모가 크고 실무적으로 더 중요한 프로젝트에서 판다스를 쓰게 될 경우엔
이 문제를 고민해야 합니다.

한가지 가이드는 드릴 수 있을거 같습니다.

원본이 필요하면 원본이 있어야 합니다.

! 원본을 남겨두어야 하는 작업인거면 불편함을 감수하세요.
보수적인 작업 방식이 요구될 떈 반드시 .copy() 메서드를 뒤에 달아서
복사본을 만들어 작업하세요

! 원본을 수정하는 작업을 하는 거라면 명시적으로 loc 메서드를 사용하세요.
loc 문법이 어렵더라도요.

이것이 제가 여러분께 드릴 수 있는 실무상 가이드 라인입니다.

문제의 본질은 판다스도 결국 파이썬이라는 언어에 종속된 라이브러리이기에

파이썬 언어가 가진 본질적인 특징 ,

변수가 객체를 참조하는 형태로 값을 가지게 하는 메커니즘이

SettingWithCopyWarning / ChainedAssignmentError 문제에

환경적인 조건으로서 간접적으로 연루돼있다는 점까지

연결해 생각해보실수도 있겠습니다.

그 연결고리 사이에는

판다스가 넘파이라는 라이브러리 구현방식을 차용했다는 점도 있습니다.
(넘파이는 수치연산을 위해 사용하는 파이썬으로 개발된 라이브러리로
머신러닝, 딥러닝에서 사용되는 핵심 라이브러리중 하나입니다)

그럼 이만 마치겠습니다.

여기까지 긴 글 읽어주셔서 감사드려요.

고생하셨습니다!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Comments