티스토리 뷰

728x90
반응형

안녕하세요. Teus입니다.

이번 포스팅에서는 Pandas Object의 inplace=True 동작에 대해서 다룹니다.

inplace = True동작이 어떻게 동작하는지, 그리고 왜 사용을 지양하는지에 대해서 알아 볼겁니다.

1. inplace = True ? False

inplace는 Object의 불변성과 관련된 중요한 키워드 입니다.

기본적으로 불변성 Data는 Data자신이 바뀌는일 없이

Data에 변경이 생길경우 변경이 적용된 새로운 Data를 만들어 줍니다.

아래 Series Object를 정렬시켜주는 sort_values()를 보시겠습니다.

import pandas as pd
dt = pd.Series([1, 8, 6, 7, 9])
print(dt.sort_values())
#>>[1, 6, 7, 8, 9]
print(dt)
#>>[1, 8, 6, 7, 9]

위처럼 정렬이된 Series Object를 반환하지만

기존 dt변수에는 정렬되지 않은 Series가 그대로 남아있습니다.

하지만 매개번수에서 inplace = True로 설정할 경우 전혀 다른동작을 보여주게 됩니다.

import pandas as pd
dt = pd.Series([1, 8, 6, 7, 9])
print(dt.sort_values(inplace = True))
#>>None
print(dt)
#>>[1, 6, 7, 8, 9]

보는것처럼 Object가 가지고있던 Data가 바뀌어 바뀌어 버립니다!

실제 Pandas Source Code 내부에서 어떤일이 일어나는지 보시겠습니다.

2. 소스코드 톱아보기

일단 Series.sort_values 코드로 가 보겠습니다.

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/series.py#L3545
class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
    ...
    def sort_values(
        self,
        *,
        axis: Axis = 0,
        ascending: bool | Sequence[bool] = True,
        inplace: bool = False,
        kind: SortKind = "quicksort",
        na_position: NaPosition = "last",
        ignore_index: bool = False,
        key: ValueKeyFunc | None = None,
    ) -> Series | None:
        inplace = validate_bool_kwarg(inplace, "inplace")
        # Validate the axis parameter
        self._get_axis_number(axis)

        # inplace를 실행하기 위해서는 copy본을 꼭 만들어줘야됨
        # 단순하게 view를 통해 접근한 data에는 불가
        if inplace and self._is_cached:
            raise ValueError(
                "This Series is a view of some other array, to "
                "sort in-place you must create a copy"
            )

먼저 inplace일 경우 View상태에서는 실행이 불가능 합니다.

때문에 위처럼 Error처리가 되어있는것을 볼 수 있습니다.

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/series.py#L3725
class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
        ...        
        else:
            #특정 값 기준으로 sorting하기 위해서
            #sorting을 할 target data를 설정
            values_to_sort = self._values
        #value를 기반으로 fancy indexing을 진행할 fancy index를 생성함
        sorted_index = nargsort(values_to_sort, kind, bool(ascending), na_position)

        #비교결과 정렬할 필요가 없을 경우
        if is_range_indexer(sorted_index, len(sorted_index)):
            if inplace:
                #self._update_inplace(self)를 실행하고
                #None을 반환하게 되어있음.
                #self._update_inplace(self)
                #return None
                #과 동일
                return self._update_inplace(self)
            return self.copy(deep=None)

그리고 sorting을 할 결과에 따라 행동이 나뉘게 되는데

sorting을 할 필요가 없는 경우 자신의 복사본을 반환하거나

self._update_inplace method를 통해서 무언가는 업데이트 시키는것을 볼 수 있습니다.

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/series.py#L3734
        #중요!
        #정렬이 필요한 경우 정렬 후 value와 index를 fancy indexing한 뒤
        #새로운 object를 생성해줌
        result = self._constructor(
            self._values[sorted_index], index=self.index[sorted_index], copy=False
        )
        #아래 문장과 동일함
        #result = Series(
        #    self._values[sorted_index], index=self.index[sorted_index], copy=False
        #)
        #이때 copy = False를 사용해서 기존 Data를 완전 복제하지 않음
        ...

        if not inplace:
            return result.__finalize__(self, method="sort_values")
        self._update_inplace(result)
        return None

그리고 실제 정렬이 필요한 경우를 보면

Series가 가지고있던 value와 index를 가지고오고

numpy의 fancy indexing을 활용해서 정렬된 상태의 Array를 만들어 줍니다.

[주의]
Slicing과 달리 Fancy indexing은 View가 아닌 데이터의 Copy본을 만들어줌.
View는 메모리를 복사하는 것이 아니라, 해당 Data를 볼 수 있는 통로라고 할 수 있음

import numpy as np
arr = np.arange(100)
arr_copy = arr[0:10]
arr[0] = 100
print(arr[0])
#>>100
print(arr_copy[0])
#>>100

arr = np.arange(100)
arr_copy = arr[[i for i in range(100)]]
arr[0] = 100
print(arr[0])
#>>100
print(arr_copy[0])
#>>0

그리고 이 데이터 가지고 새로운 Object를 만들어 주는데

이때 copy = False를 통해서 데이터가 2배가 되는것을 막아주는것을 볼 수 있습니다.

import pandas as pd
import numpy as np
import psutil
cnt = 100000000
dt = np.arange(cnt)
p = psutil.Process()
rss = p.memory_info().rss/2**20
temp = pd.Series(dt, copy = False)
print(f'mem usage : {p.memory_info().rss/2**20-rss:10.5f}MB')
#copy = False일 경우 메모리가 증가하지 않음
#>>mem usage :    0.06250MB
p = psutil.Process()
rss = p.memory_info().rss/2**20
temp = pd.Series(dt, copy = True)
print(f'mem usage : {p.memory_info().rss/2**20-rss:10.5f}MB')
#copy = True일 경우 메모리가 증가함
#>>mem usage :  381.47266MB

이렇게 copy = False로 반환된 data를 Pandas에서는 View라고 합니다.

이런 View는 사용한 원본Data가 바뀔경우 Series의 값 역시 변경됩니다.

import pandas as pd
import numpy as np

arr = np.arange(10)
tg = pd.Series(arr, copy = False)
print(tg[0])
#>> 0
#Series Object가 아니라 Series Object를 만들때 사용된 Data를 변경
arr[0] = 200
print(tg[0])
#>> 200

이제 마지막으로 inplace 여부에따라 분기가 갈립니다.

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/series.py#L3741
    ... 
        if not inplace:
            return result.__finalize__(self, method="sort_values")
        self._update_inplace(result)
        return None

inplace=False일 경우 NDFrame.__finalize__ method가 실행되게 됩니다.

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/generic.py#L6148
class NDFrame(PandasObject, indexing.IndexingMixin):
    ...
    def __finalize__(self, other, method: str | None = None, **kwargs) -> Self:        
        if isinstance(other, NDFrame):
            for name in other.attrs:
                self.attrs[name] = other.attrs[name]

            self.flags.allows_duplicate_labels = other.flags.allows_duplicate_labels
            # For subclasses using _metadata.
            for name in set(self._metadata) & set(other._metadata):
                assert isinstance(name, str)
                object.__setattr__(self, name, getattr(other, name, None))

        if method == "concat":
            ...
        return self

NDFrame.__finzlize__()가 실행되면서

result에 해당하는 Series Object에 Original(기존)의 모든 properties를 전송하는 모습을 볼 수 있습니다.

하지만 attrs같은 경우 pandas에서 실험적으로 metadata를 관리하는 방법으로

일반적은 Series를 사용할때는 name이외에 다른 metadata는 없습니다.

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/generic.py#L238
class NDFrame(PandasObject, indexing.IndexingMixin):
    ...
    @property
    def attrs(self) -> dict[Hashable, Any]:
        """
        Dictionary of global attributes of this dataset.
        .. warning::
           attrs is experimental and may change without warning.
        """
        ...

그럼 self._update_inplace()를 확인해 볼까요?

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/generic.py#L4807
class NDFrame(PandasObject, indexing.IndexingMixin):
    ...
    @final
    def _update_inplace(self, result, verify_is_copy: bool_t = True) -> None:        
        self._reset_cache()
        self._clear_item_cache()
        self._mgr = result._mgr
        self._maybe_update_cacher(verify_is_copy=verify_is_copy, inplace=True)

다른 method는 모두 cache와 연관되어있고

중요한 부분은 바로 self._mgr = result._mgr 이 부분 입니다.

_mgr에는 data와 index정보가 담겨있다고 했던거 내용이 기억 나시나요?
(이전 포스팅1, 포스팅2)

inplace로 실행할 경우 이처럼 Class의 _mgr만 갈아끼어 넣어주는 방식으로 method가 동작합니다.

3. 아쉬우니깐 self.dropna도 살펴보고 가기

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/series.py#L5525C1-L5618C26
class Series(base.IndexOpsMixin, NDFrame):  # type: ignore[misc]
    ...
    def dropna(
        self,
        *,
        axis: Axis = 0,
        inplace: bool = False,
        how: AnyAll | None = None,
        ignore_index: bool = False,
    ) -> Series | None:
        ...
        #매개변수의 String Literal 확인
        inplace = validate_bool_kwarg(inplace, "inplace")
        ignore_index = validate_bool_kwarg(ignore_index, "ignore_index")
        # Validate the axis parameter
        self._get_axis_number(axis or 0)

        #na를 제거한 새로운 Series를 반환받음
        if self._can_hold_na:
            result = remove_na_arraylike(self)
        ...

        #inplace여부에따라 분기를 나눠서 result로 덮어씌움
        if inplace:
            return self._update_inplace(result)
        else:
            return result

self.dropna() 역시 보는것처럼

조건에따라 na가 제거된 새로운 Array를 만들고

이 Array를 가지고 inplace 매개변수의 값에 따라서 분기를 나눠 수정하는것을 볼 수가 있습니다.

정리.

결국 self.sort_values()self.dropna()를 통해서 본 소스코드를 정리해보면.

  1. data의 값 기준으로 정렬된 복사본을 만든다.
  2. 이 복사본을 가지고 copy = False로 새로운 Series Object를 만들어준다.
  3. 새로운 Object에서 inplace 여부에 따라
    -> inplace = False : 값을 바꾼 result에 기존 object의 meta data를 덮어씌어서 반환한다.
    -> inplace = True : 값을 바꾼 result의 _mgr기존 object의 _mgr에 덮어씌운다.

4. inplace. 써요 말아요?

inplace = True를 쓰면 되냐, 안되냐에 대해서 말이 많습니다.

결론부터 말하면 Pandas Dev Society에서는 inplace의 사용을 추천하지 않고 있습니다.

출처 : https://github.com/pandas-dev/pandas/issues/16529

위 코드 정리본을 보면 알 수 있지만

결국 inplace를 사용하나, 사용하지 않나 result라는 새로운 Series Object가 순간적으로 만들어지게 됩니다.

때문에 inplace를 사용하는 여부에 따라 크게 달라지는것이 없다는것을 알 수가 있습니다.

  • inplace구현을 위해서 _mgrObject가 무겁게 됩니다.
  • 마지막으로 이걸 구현하기 위해서 개발 Resource가 낭비됩니다!
  • 그리고 다른 프래임워크 등에서 Data의 불변성을 추구하면서 전체적인 패더다임이 데이터의 불변성을 확보하는 방식으로 가고 있습니다.

이러한 이유들 때문에 사용하지 않는것이 바람직하다고 볼 수 있겠습니다.

728x90
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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 30 31
글 보관함