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

신고하기

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

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

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

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

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

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

      임시저장함

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

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

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

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

      회원정보 연결

      [Pandas] Series의 구조에 대해서 알아보자2

      Teus 24.06.03
      826 3 0
      DEVOTEE 요약
      이번 블로그는 Pandas Series의 데이터 접근을 용이하게 해주는 Pandas Index에 대해 다룹니다. Index Class는 불변형의 시퀀스 객체로, 데이터의 인덱스를 저장 및 관리하며 데이터 타입에 따라 적절한 하위 클래스 객체를 반환합니다. 이 과정을 통해 loc나 sort_values와 같은 메서드로 Series의 특정 데이터를 효율적으로 추출하고 정렬할 수 있습니다.
      DEVOTEE 추천 블로그

      안녕하세요. Teus입니다.


      지난 포스팅을 통해서 Pandas Series가 어떻게 Data를 저장하는지 확인 하였습니다.

      이번 포스팅은 Pandas Series Data에 접근하기 용이하게 해주는 Pandas Index에 대해서 알아보겠습니다.


      1. BlockManager

      지난시간 Pandas Series는 내부에 _mgr라는 곳에 Data와 Index를 저장하고

      이 Object를 통해서 Data를 접근, 통제한다고 했습니다.

      그리고 이 Manager는 BlockManager Object였습니다.

      #https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/internals/managers.py#L1787
      class SingleBlockManager(BaseBlockManager, SingleDataManager):
          """manage a single block with"""
          ...
          def __init__(
              self,
              block: Block,
              axis: Index,
              verify_integrity: bool = False,
          ) -> None:
      
              self.axes = [axis]
              self.blocks = (block,)        
          ...
          @classmethod
          def from_array(
              cls, array: ArrayLike, index: Index, refs: BlockValuesRefs | None = None
          ) -> SingleBlockManager:
              array = maybe_coerce_values(array)
              bp = BlockPlacement(slice(0, len(index)))
              block = new_block(array, placement=bp, ndim=1, refs=refs)
              return cls(block, index)  

      이때 from_array에서 index라는 매개변수를 사용하고

      index는 Index Class Object입니다.

      그럼 이 Index Class Object에 대해서 한번 알아보겠습니다.


      2. Index

      Index의 경우 아래와 같은 상속구조를 가지고 있습니다.

      DirNamesMixin

      ↑

      PandasObject

      ↑

      Index -> IndexOpsMixin - >OpsMixin


      Index Class는 주석에 써있듯

      불변형의 Sequence Object이면서 pandas Object를 다루기위한 axis label을 저장, 관리합니다.

      #https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/indexes/base.py#L307C1-L311C65
      class Index(IndexOpsMixin, PandasObject):
          """
          Immutable sequence used for indexing and alignment.
          The basic object storing axis labels for all pandas objects.
          ...
          Parameters
          ----------
          data : array-like (1-dimensional)
          dtype : NumPy dtype (default: object)
              If dtype is None, we find the dtype that best fits the data.
              If an actual dtype is provided, we coerce to that dtype if it's safe.
              Otherwise, an error will be raised.
          copy : bool
              Make a copy of input ndarray.
          name : object
              Name to be stored in the index.
          tupleize_cols : bool (default: True)
              When True, attempt to create a MultiIndex if possible.
          """
          def __new__(
              cls,
              data=None,
              dtype=None,
              copy: bool = False,
              name=None,
              tupleize_cols: bool = True,
          ) -> Index:
              ...
              try:
                  arr = sanitize_array(data, None, dtype=dtype, copy=copy)
              except ValueError as err:
                  ...
              arr = ensure_wrapped_if_datetimelike(arr)
      
              klass = cls._dtype_to_subclass(arr.dtype)
      
              arr = klass._ensure_array(arr, arr.dtype, copy=False)
              return klass._simple_new(arr, name, refs=refs)
              ...

      코드에서 유추해보면, Index Class 생성자같은 경우 Index의 자식 Class를 가지고고

      그 자식 Class를 기반으로 새로운 Object를 반환해 줍니다.

      이때 array가 가지고있는 dtype을 기반으로 dtypeIndex Class를 반환해 줍니다.

      #https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/indexes/base.py#L592C5-L614C25
          @classmethod
          def _dtype_to_subclass(cls, dtype: DtypeObj):        
              if isinstance(dtype, ExtensionDtype):
                  if isinstance(dtype, DatetimeTZDtype):
                      from pandas import DatetimeIndex
                      return DatetimeIndex
                  elif isinstance(dtype, CategoricalDtype):
                      from pandas import CategoricalIndex
                      return CategoricalIndex
                  elif isinstance(dtype, IntervalDtype):
                      from pandas import IntervalIndex
                      return IntervalIndex
                  elif isinstance(dtype, PeriodDtype):
                      from pandas import PeriodIndex
                      return PeriodIndex
                  return Index
              ...

      그리고 ensure_array를 통해서 arraylike Object여부를 판단하고

      Index._simple_new를 통해서 실제 Object를 만들어 반환해 줍니다.

      #https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/indexes/base.py#L648C5-L671C22
          @classmethod
          def _simple_new(
              cls, values: ArrayLike, name: Hashable | None = None, refs=None
          ) -> Self:
              assert isinstance(values, cls._data_cls), type(values)
      
              result = object.__new__(cls)
              result._data = values
              result._name = name
              result._cache = {}
              result._reset_identity()
              if refs is not None:
                  result._references = refs
              else:
                  result._references = BlockValuesRefs()
              result._references.add_index_reference(result)
      
              return result

      일반적인 Class 상속, 생성자 패턴과는 조금 다른것을 볼 수 있습니다.

      독특하지만 이런 방식을 통해서 새로운 Object를 생성하고, property를 초기화하는 모습을 볼 수 있습니다.

      property를 초기화한 뒤에

      Index Object는 _data에는 arraylike object를 저장하게 됩니다.

      결국 Data를 위한 Numpy-Array 하나, Index를위한 Numpy-Array 하나를 따로따로 저장하게 됩니다.

      이제 그러면 Data와 Index를 동시에 사용할 경우 어떻게 동작하는지 볼까요?


      3. loc를 통한 살펴보기

      image.png

      loc같은 경우 location indexing으로, 단순 slicing이 아니라 Row Index기반으로 Data를 추출합니다.

      import pandas as pd
      
      dt = dt = pd.Series(
          data  = [5,9,7,8,2,3],
          index = ["a","b","c","d","f","e"]
      )
      dt.loc[["a", "f"]]
      '''
      a    5
      e    3
      dtype: int64
      '''

      Row Index를 통해서 Data를 가지고 오는것을 볼 수 있죠?

      즉, Index Object를 활용해서 Row를 뽑아내고, 그 Row기반으로 Data를 가지고 올 것입니다.

      한번 볼까요?


      NDFrame

      ↑

      Series → IndexingMixin

      #https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/indexing.py#L145
      class IndexingMixin:
          """
          Mixin for adding .loc/.iloc/.at/.iat to Dataframes and Series.
          """
          @property
          def loc(self) -> _LocIndexer:
              #name : "loc", obj : self(pandas Series Object)
              return _LocIndexer("loc", self)
      
      #https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/indexing.py#L1177
      @doc(IndexingMixin.loc)
      class _LocIndexer(_LocationIndexer):
          ...
          
      #https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/indexing.py#L709C1-L709C44
      class _LocationIndexer(NDFrameIndexerBase):
          ...
      
      #https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/_libs/indexing.pyx    
      cdef class NDFrameIndexerBase:
          """
          A base class for _NDFrameIndexer for fast instantiation and attribute access.
          """
          ...
          def __init__(self, name: str, obj):
              self.obj = obj
              self.name = name
              self._ndim = -1

      Pandas Series에서 loc난 iloc를 사용할 경우 Series Object를 Property로 가지고 있는

      NDFrameIndexerBase 하부 Object가 생성됩니다.

      이때 self.obj에 Series 원본이 저장되게 됩니다.

      이때 _LocationIndexer에서 구현된 self.__getitems__를 살펴보겠습니다.

      #https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/indexing.py#L1139C1-L1153C65
      class _LocationIndexer(NDFrameIndexerBase):
          ...
          @final
          def __getitem__(self, key):
              check_dict_or_set_indexers(key)
              if type(key) is tuple:
                  ...
              else:
                  # we by definition only have the 0th axis
                  axis = self.axis or 0
      
                  maybe_callable = com.apply_if_callable(key, self.obj)
                  return self._getitem_axis(maybe_callable, axis=axis)

      현재 위 코드에서 key는 ["a", "f"]값이고, maybe_callable도 동일한 값을 갖게 됩니다.

      이때 self._getitme_axis는 하위 Class _LocIndexer에서 정의되어 있습니다.

      #https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/indexing.py#L1359
      @doc(IndexingMixin.loc)
      class _LocIndexer(_LocationIndexer):
          ...
          def _getitem_axis(self, key, axis: AxisInt):
              key = item_from_zerodim(key)
              if is_iterator(key):
                  key = list(key)
              if key is Ellipsis:
                  key = slice(None)
              
              #NDFrame에 정의되어 있는 method로
              #Index Object를 return해줌
              labels = self.obj._get_axis(axis)
      
              ...
              elif is_list_like_indexer(key):
                  # an iterable multi-selection
                  if not (isinstance(key, tuple) and isinstance(labels, MultiIndex)):
                      ...
                      return self._getitem_iterable(key, axis=axis)
                      
      #https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/indexing.py#L1296
                    
          def _getitem_iterable(self, key, axis: AxisInt):        
              ...
              keyarr, indexer = self._get_listlike_indexer(key, axis)
              return self.obj._reindex_with_indexers(
                  {axis: [keyarr, indexer]}, copy=True, allow_dups=True
              )
      
      #https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/indexing.py#L1494C5-L1522C31
          def _get_listlike_indexer(self, key, axis: AxisInt):
              #pandas Series가 가지고있는 Index정보를 가지고옴
              ax = self.obj._get_axis(axis)
              axis_name = self.obj._get_axis_name(axis)
              
              #Index, np.ndarray를 반환받음
              #이때 np.ndarray는 key(row index를 string으로 가지고있는 list)
              #에 대응되는 row number 정보를 저장하는 arraylike
              keyarr, indexer = ax._get_indexer_strict(key, axis_name)
      
              return keyarr, indexer

      위 보면 알 수 있듯, loc에 입력했던 String형태의 row-index는

      결국 loc를 통한 접근을 할 때

      Pandas Index를 통해서 입력받은 slicing 혹은 list-like object가 row number형태로 변경되게 됩니다.


      4. sort_values를 통한 살펴보기

      image.png

      import pandas as pd
      
      dt = dt = pd.Series(
          data  = [5,9,7,8,2,3],
          index = ["a","b","c","d","f","e"]
      )
      dt.sort_values()
      '''
      Output:
      
      f    2
      e    3
      a    5
      c    7
      d    8
      b    9
      dtype: int64
      '''

      sort_values는 특정 조건 기준으로 정렬시킨 뒤 새로운 Series Object를 반환 해 줍니다.

      한번 코드를 보시겠습니다.

      #https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/series.py#L245
      class Series(base.IndexOpsMixin, NDFrame):
          ...
          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,
          )
          ...
              else:
                  values_to_sort = self._values
              sorted_index = nargsort(values_to_sort, kind, bool(ascending), na_position)
              ...
              
              #numpy fancy indexing을 통해서
              #정렬된 상태의 numpy array와 Index Object를 가지고 새로운 Pandas Series를 만들어냄
              result = self._constructor(
                  self._values[sorted_index], index=self.index[sorted_index], copy=False
              )
              ...
              
              if not inplace:
                  return result.__finalize__(self, method="sort_values")

      보시면 sort_values에서도 value와 index가 짝으로 행동하는 것을 볼 수 있습니다.

      이처럼 Pandas Series는 Index와 value쌍으로 이뤄진 _mgr Object를 통해서 같이 행동하게 됩니다.

      이제 Pandas Series에 대해서 조금 이해가 되시나요?

      댓글 0

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

      Teus 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기