티스토리 뷰

728x90
반응형

안녕하세요. 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
class Index(IndexOpsMixin, PandasObject):
    ...
    @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
class Index(IndexOpsMixin, PandasObject):
    ...
    @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를 통한 살펴보기

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를 통한 살펴보기

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

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에 대해서 조금 이해가 되시나요?

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