데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      [Pandas] Pandas의 inplace=True동작에 대해서

      Teus 24.07.19
      2,905 5 0
      DEVOTEE 요약
      Pandas의 `inplace=True` 옵션은 객체를 직접 변경하는 대신 새로운 객체를 반환하는 것을 방지하는 기능입니다. 그러나 이 옵션을 사용하면 코드의 가독성과 디버깅이 어려워지며 메모리 효율성에도 큰 이점이 없기 때문에 Pandas 개발팀에서도 사용을 권장하지 않습니다. 따라서 데이터의 불변성을 유지하는 방식으로 코드를 작성하는 것이 바람직합니다.

      안녕하세요. 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정보가 담겨있다고 했던거 내용이 기억 나시나요?

      (이전 pandas Series의 구조에서 봤었죠?)


      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의 불변성을 추구하면서 전체적인 패러다임이 데이터의 불변성을 확보하는 방식으로 가고 있습니다.(React라던가 React라던가 React라던가...)

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

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      Teus 님의 최신 블로그

      더보기
      동영상 기고하기