PYTHON/Error Data

Pandas 오류 : SettingWithCopyWarning

  • -

오류 코드

C:\Users\@@@\PycharmProjects\back_Test\algorithm\algorithm_1.py:254: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.

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

  self.trade_trace.loc[iloc_num]['profit_rate'] = profit_rate

또는

SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

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

    iloc._setitem_with_indexer(indexer, value)

 

해결 방법을 사용하기 전에 앞서 일단 위의 두 오류의 차이를 한 번 살펴보아야 한다. 위의 오류는 작성했던 코드를 나타내주지만, 아래의 오류는 오류 내용을 알려준다. 두 오류의 차이는 코드가 실행된 후에 발생하는 오류인가 아니면 코드가 실행되지 않고 발생하는 오류인가의 차이이다. 실행된 후 발생하는 오류가 아래의 오류이고 실행되지 않고 발생하는 오류는 위의 오류이다.

  • self.trade_trace.loc[iloc_num]['profit_rate'] = profit_rate
  • iloc._setitem_with_indexer(indexer, value)

 

해결 방법(두 번째 방법을 권장)

해결 방법을 설명하기 전에 앞서, 각각의 변수에는 다음과 같은 데이터들이 입력되어 있다. 여기서 code 칼럼의 데이터값이 000020인 데이터열(인덱스 0번 자리)에서 buy 칼럼에 'no'라는 값을 입력해볼 예정이다. 

>>> df
     code      date buy sell
0  000020  20200101         
1  000040   2020102         
2  000060  20200103       

>>> dfd  ## dfd = df.copy()  dfd는 df의 복사본임
     code      date buy sell
0  000020  20200101         
1  000040   2020102         
2  000060  20200103

 

 

첫 번째 방법, 별도의 변수를 생성해서 데이터 입력 시의 조건을 별도의 변수에 저장하고 그를 바탕으로 인덱싱

이에 대해 부연설명을 해보자면, 일단 code 칼럼의 데이터가 000020인 데이터열의 buy 칼럼에는 no를 입력해보는 것이다. 이전에 작성했던 코드와 다른 코드를 작성할 예정이고, 그를 비교해보도록 하자.
※ 일단 이 방법으로는 문제가 해결된 부분도 있지만 해결되지 않은 부분도 있었음

## 이전에 사용한 방법
dfd.loc[0]['buy'] = 'no'

## 새로운 방법
code_data = dfd['code'].str.startswith('000020')	## dfd에 칼럼명이 'code'이고 데이터값이 '000020'인 경우 True를 반환
dfd.loc[code_df, 'buy'] = 'no'     					## code_data 값이 True인 경우 'buy' 칼럼에 'no'를 입력

 

 

두 번째 방법, at을 통해 데이터를 보다 직접적으로 입력(추천)

판다스는 loc에 대응하는 at과 iloc에 대응하는 iat 메서드를 별도로 지원하고 있다.   SettingWithCopy  오류는 바로 여기서의 at을 통해 데이터를 보다 직접적으로 입력하는 방식을 사용함으로써 문제를 해결할 수 있었다. 

  • 사용 방법 : DataFrame_Name.at(index_number, column_name) = insert_data
## 이전에 사용한 방법
dfd.loc[0]['buy'] = 'no'

## 새로운 방법 (복사본을 만들 필요도 없음)
df.at[0, 'buy'] = 'no'

 


 

해결 과정

① 파이썬 IDLE 프로그램 내에서 작업해본 결과, 문제 없이 데이터가 잘 입력되는 것을 확인.
    + 심지어 파이참 콘솔에서 동일한 작업을 수행해도 동일한 결과를 얻을 수 있었음.

import pandas as pd

## 데이터프레임 기본 틀 생성
aa = {'code':[], 'date':[], 'buy':[]}
df = pd.DataFrame(aa, columns=['code', 'date', 'buy'])

## 기본 틀에 입력할 데이터 생성
temporary_ = {'code':['000020'], 'date':['20210101']}
temporary_df = pd.DataFrame(temporary_, columns=['code', 'date'])

## 데이터를 기본 틀에 입력(append)
df = df.append(temporary_df)

## 원하는 지점에 데이터 입력하기
df.buy.loc[0] = 'no'

## 실행 결과
>>> print(df)
     code      date buy
0  000020  20210101  no

 

② 판다스 문서를 뒤져봄 : 아래는 문서 전문 번역본


Returning a view versus a copy

판다스 객체 내에서 데이터값을 설정할 때,   Chained indexing  이라는 문제가 발생하지 않도록 주의해야 한다. 아래가 그 예시이다. dfmi라는 데이터프레임을 생성한 후, 두 가지 방법을 사용해서 데이터에 접근해보도록 하자. 

dfmi = pd.DataFrame([list('abcd'),
                     list('efgh'),
                     list('ijkl'),
                     list('mnop')],
                    columns=pd.MultiIndex.from_product([['one', 'two'],
                                                        ['first', 'second']]))

dfmi
Out[355]: 
    one          two       
  first second first second
0     a      b     c      d
1     e      f     g      h
2     i      j     k      l
3     m      n     o      p

 

## 첫 번째 방법
dfmi['one']['second']

0    b
1    f
2    j
3    n
Name: second, dtype: object

## 두 번째 방법
dfmi.loc[:, ('one', 'second')]

0    b
1    f
2    j
3    n
Name: (one, second), dtype: object

두 가지 방법 모두 동일한 결과물을 가져오는데, 도대체 어떤 방법을 사용해야 할까? 여기서는 .loc을 사용하고 두 번째 방법이 첫 번째 방법(대괄호로 묶여 있는)에 비해 훨씬 권장되는 원리를 이해하는 것이 좋다. 아래의 내용을 살펴보자.

첫 번째 방법(  dfmi['one']['second']  )의 경우, 가장 먼저   dfmi['one']['second']  라는 코드에서   dfmi['one']  이라는 코드를 먼저 실행한다. 즉 코드 중 먼저 입력한 칼럼의 이름에 따라 칼럼을 선택하고, 단일로 인덱싱된 데이터프레임을 반환한다. (이는 한 줄의 데이터를 반환한다는 것과 동일하다) 그 다음, 또 다른 곳에서는 dfmi_with_one['second']는 'second'에 의해 인덱싱된 데이터 열을 선택한다.  에 의해 인덱싱된 시리즈(데이터)를 선택한다. 'second'.dfmi_with_one 

판다스는 이러한 동작을 서로 다른 개별적인 작업으로 보기 때문에, 이 첫 번째 작업은   dfmi_with_one  이라는 변수로 표시된다. 대표적인 예시로는 데이터를 개별적으로 호출하는 것이 있다. 다시 말해 첫 번째 방법(  dfmi['one']['second']  )은 선형 작업으로 처리(순서대로 처리한다는 말)하게 되므로 어떤 작업이 완료된 후에 다른 작업이 시행되는 것이다.

 이는 데이터를 개별적으로 호출하는 것과 다르게 (slice(None), ('one', 'second'))와 같이 중첩 튜플 형태(그냥 튜플이라고 이해하면 됨)로 데이터를 전달하는 두 번째 방법(  df.loc[:, ('one', 'second')]  )과는 완벽하게 대조된다. 이 두 번째 방법은 작업 속도가 조금 더 빨라질 수 있는 동시에, 원할 경우 두 축 모두에 인덱싱(자료에 접근)할 수 있다.

 

체인 인덱싱을 사용할 때 데이터 입력이 실패하는 이유는 무엇인가?
(Why does assignment fail when using chained indexing?)

앞서 살펴봤던 내용은 사실 오류와는 전혀 무관한 내용이고 작업 속도와 관련된 내용일 뿐이다. 그렇다면 무엇이   SettingWithCopy  경고를 발생시키는가? 판다스는 단순하게 작업 시간이 몇 초 정도가 더 길어진다고 이 오류를 띄우도록 설정하지 않았다. 하지만 chained indexing을 바탕으로 얻어온 데이터에 어떤 데이터를 입력하는 것이 예상하지 못했던 결과를 가져올 수 있다는 문제점이 있음을 확인했다. 아래의 두 코드를 확인해보자.

## 첫 번째 방법
dfmi['one']['second'] = value
# becomes
dfmi.__getitem__('one').__setitem__('second', value)

## 두 번째 방법
dfmi.loc[:, ('one', 'second')] = value
# becomes
dfmi.loc.__setitem__((slice(None), ('one', 'second')), value)

위의 두 코드는 서로 다른 방법으로 데이터를 다루고 있음을 확인할 수 있다. 즉, 첫 번째 방법(  dfmi['one']['second']  )에서는   __getitem__  과   __setitem__  이 모두 등장했지만, 두 번째 방법(  dfmi.loc[:, ('one', 'second')]  )에서는   __getitem__  을 찾아볼 수 없다. 이 단순한 예시가 아닌 다른 경우에도 원본의 데이터를 반환할지 아니면 복사본의 데이터를 반환할지를 예측하는 것은 상당히 어려운 일이다. 그러므로 __setitem__은 dfmi를 수정하거나 작업을 완료한 후 ㅁ임시적으로 생성된 객체를 삭제한다. 이것이 바로 SettingWithCopy가 당신에게 경고하는 것이다.

때때로 SettingWithCopy는 명백하지 않은 인덱싱 작업이 진행되고 있을 때에도 발생할 수 있는데, 이 때에도 그 문제를 찾아내도록 SettingWithCopy가 제작되었다. 판다스는 아마도 아래와 같은 코드에 대해 경고하고자 했을 것이다.

def do_something(df):
    foo = df[['bar', 'baz']]  ## foo라는 데이터는 복사본인가 원본인가? 그 누구도 그건 모른다.
    # 많은 데이터들이 입력될 것인데,
    # 우리는 원래의 df를 수정할지 말지를 전혀 알 수 없다!
    foo['quux'] = value
    return foo

 

이외에 발생하는 또 다른 오류를 사전에 조절하기
(Evaluation order matters)

chained indexing을 사용할 때, 인덱싱을 사용하기 위해 제작한 코드의 순서와 유형에 따라서 결과가 원본 개체로부터 떨어져 나온 값(slice)인지 또는 떨어져나온 값의 복사본인지가 결정된다. 

판다스는 일부의 데이터 복사본에 데이터를 입력하는 것이 종종 의도적이지 않은 경우가 발생할 수 있기 때문에   SettingWithCopy  라는 오류를 발생시키도록 하고 있다. 하지만 chained indexing을 사용함으로써 발생한 실수는 slice가 예상되는 지점에서 복사본을 반환하도록 한다.

만약 chianed indexing 구조의 코드를 사용할 때 오류가 발생한다면,   mode.chained_assignmen  라는 값에 아래와 같은 세 가지 값을 입력함으로써 사용할 수 있다. (이는 .loc을 사용할 때와 .iloc을 사용할 때를 구분하지 않고 적용된다.)

  • 'warn' : 기본값이며, SettingWithWarning이 출력됨
  • 'raise' : SettingWithCopyException이 출력됨
  • 'None' : 경고가 출력되지 않음
pd.set_option('mode.chained_assignment','warn')
pd.set_option('mode.chained_assignment','raise')
pd.set_option('mode.chained_assignment', None)

 

아래의 코드는 다양한 데이터에 접근하고자 할 때 .loc을 사용하고자 할 경우 권장되는 방법이다.

dfc = pd.DataFrame({'a': ['one', 'one', 'two',
                          'three', 'two', 'one', 'six'],
                    'c': np.arange(7)})


## dfc를 대상으로 복사본을 생성하여 dfd 변수에 입력
dfd = dfc.copy()


# 마스크(mask)를 이용해 복수의 데이터를 설정
mask = dfd['a'].str.startswith('o')

dfd.loc[mask, 'c'] = 42

dfd
       a   c
0    one  42
1    one  42
2    two   2
3  three   3
4    two   4
5    one  42
6    six   6


# 개별 데이터를 설정
dfd = dfc.copy()

dfd.loc[2, 'a'] = 11

dfd
       a  c
0    one  0
1    one  1
2     11  2
3  three  3
4    two  4
5    one  5
6    six  6

 

아래의 예시는 권장되지 않는 방법이다.

## 권장되지 않는 방법
dfd = dfc.copy()

dfd['a'][2] = 111

dfd 
       a  c
0    one  0
1    one  1
2    111  2
3  three  3
4    two  4
5    one  5
6    six  6

 

방법 요약

  • dfc(원래의 데이터)를 대상으로 dfd(복사본)을 생성하여 데이터에 접근한다. 
  • ① 데이터 접근 시 .loc을 이용해 인덱스 번호를 입력한 후 칼럼명을 사용한다.
  • ② 원하는 데이터를 변수로 설정해준다.

 

 

728x90
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.