Python for Process Innovation
#Python, #Javascript, #SAP, #Automation, #ML, #AI
Pandas Indexing & Selecting 주의할 점
Update: 2019-03-03

Pandas를 사용하다 보면 은근히 자주 보게되는 경고 문구가 있습니다. 저 역시 분석을 수행하는 과정이나 어플리케이션을 제작하는 과정에서 많이 보곤 합니다. StackOverflow에도 관련 이슈가 있고 질문 답변 모두 upvote가 많은 것을 보면, 집고 넘어갈만 한 의미 있는 주제라는 생각이 듭니다.

Traceback (most recent call last)
     ...
SettingWithCopyWarning:
     A value is trying to be set on a copy of a slice from a DataFrame.
     Try using .loc[row_index,col_indexer] = value instead
1
2
3
4
5

바로 아래와 같이 chained indexing을 사용해 데이터를 선택한 다음 여기에 값을 할당하는 경우에 발생하는 경고 문구입니다.

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


# This will show the SettingWithCopyWarning
# but the frame values will be set
dfb['c'][dfb.a.str.startswith('o')] = 42
1
2
3
4
5
6
7
8

경험을 해 보신 분은 알겠지만, 저런 chained indexing을 통한 value assign 방식은 제대로 작동을 하지 않는 경우가 있습니다. 공식 문서에 따르면 pandas가 보장할 수 없는 배열의 메모리 구조에 따라, 데이터프레임 view 또는 copy를 반환하는 상황이 발생하기 때문이라고 합니다.

it depends on the memory layout of the array, about which pandas makes no guarantees

실제로 경고 문구에서 추천하는 방식과 chained indexing을 통한 방식의 작동 방식을 살펴보면 아래와 같습니다(참고로 .loc의 경우에는 dfmi에 직접 getitem / setitem을 적용시킨다는 것을 보장).

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


dfmi

"""
    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
"""


# recommended method
dfmi.loc[:, ('one', 'second')] = value
# becomes
dfmi.loc.__setitem__((slice(None), ('one', 'second')), value)

# chained indexing method
dfmi['one']['second'] = value
# becomes
dfmi.__getitem__('one').__setitem__('second', value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

다만, 저렇게 명시적인 chained indexing을 사용하지 않는 경우에도 해당 경고 문구를 보기도 합니다. 하지만 이는 SettingWithCopy warning이 해당 코드의 잠재적인 버그를 주지시키기 위한 의도된 동작입니다. 아래 코드를 보면 이해가 가능합니다.

def do_something(df):
    foo = df[['bar', 'baz']]  # Is foo a view? A copy? Nobody knows!
    # ... many lines here ...
    # We don't know whether this will modify df or not!
    foo['quux'] = value
    return foo
1
2
3
4
5
6

공식 문서에서는 별도로 언급하고 있진 않지만 개인적으로 즐겨 사용하는 방법은, 위와 같이 구조적으로 view 또는 copy가 모호해질 수 밖에 없는 부분에는 .copy() 메소드를 통해 명시적으로 copy를 수행하는 것도 하나의 방법입니다.

def do_something(df):
    foo = df[['bar', 'baz']].copy()  # It's a copy!
    # ... many lines here ...
    foo['quux'] = value
    return foo
1
2
3
4
5

그럼에도 불구하고 Pandas는 해당 경고에 대해 아래와 같은 옵션을 제공합니다.

TIP

  • pd.set_option('mode.chained_assignment', 'warn'): (SettingWithCopyWarning이 출력되는) 디폴트 입니다.
  • pd.set_option('mode.chained_assignment', 'raise'): SettingWithCopyException 오류를 발생시킵니다.
  • pd.set_option('mode.chained_assignment', None): 해당 경고 자체를 하지 않습니다.

None의 경우 당장 편할 순 있어도, 특별히 의도된 경우가 아닌 한 유지보수에 더욱 강건한 코드를 위해서는 'raise' 설정이 적절해 보입니다.

Powered by Vue.js(Vuepress + Vuetify)