티스토리 뷰

728x90
반응형

안녕하세요 Teus입니다.

이번 포스팅은 Pandas DataFrame을 이해하기 위한

Pandas Series 알아보기 시간 입니다.

1. Pandas Series

Pandas Series는 Pandas의 자료구조 중 1dim의 Array형태의 Object입니다

다들 DataFrame이 익숙 하시겠지만, DataFrame을 이해하기 위해서는 Series를 먼저 이해할 필요가 있습니다.

그럼 Pandas Series Source Code가 어떤 형태로 되어있는지 보도록 하겠습니다.

image.png


(초록색은 일반 Cls, 주황색은Mixin Cls입니다)

Series가 NDFrame을 상속받고, 이 NDFrame이 PandasObject를 상속받는 구조 입니다.

Series : drop_duplicates, reset_index, unique 등 Series Object에서 사용되는 다양한 method를 정의하는 부분
NDFrame : 내부에 Manager(약어로 mgr)을 활용해서 입력받은 Data를 저장함. DataFrame과 공유하는 Class로 shape, axes, ndim 등 Data의 형태에 대한 정보를 저장
PandasObject : Pandas Package가 가지고있는 다양한 Object가 기본적으로 상속받는 Class입니다. __repr__과 sizeof 등 일부 built-in-method를 정의해줌

자 그러면 Pandas Series가 어떤식으로 Data를 저장하는지 살펴볼까요?

1_1. Pandas Series

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/series.py#L245
class Series(base.IndexOpsMixin, NDFrame):
    ...
    #static variable로 나중에 중요함
    _mgr: SingleManager

    def __init__(
        self,
        data=None,
        index=None,
        dtype: Dtype | None = None,
        name=None,
        copy: bool | None = None,
        fastpath: bool = False,
    ) -> None:
    ...

Series Class의경우 일반적으로 ArrayLike Object를 받고, 이 Data를 기반으로 Pandas Series를 만들어 줍니다.

import pandas as pd
my_arr = pd.Series([i for i in range(10)])

이때 별도의 매개변수를 주는 경우를 빼고, 위와같이 Data만 주는 경우를 생각해 보겠습니다.

그러면 생성자에 매개변수중 data이외에는 모두 기본값을 갖게 됩니다.

이때 생성자 내부에는 fastpath라고 하여서 조건문 검토를 최소화 하고, 빠르게 Series를 만들 수 있는 코드가 있습니다.

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/series.py#L404C9-L420C19
        # we are called internally, so short-circuit
        if fastpath:
            # data is a ndarray, index is defined
            if not isinstance(data, (SingleBlockManager, SingleArrayManager)):
                #윈도우환경에서 Series 생성 시 "block"값을 return해줌
                manager = get_option("mode.data_manager")
                if manager == "block":
                    #static method인 from_array의 결과값을 data에 저장함
                    data = SingleBlockManager.from_array(data, index)
                elif manager == "array":
                    data = SingleArrayManager.from_array(data, index)
            elif using_copy_on_write() and not copy:
                data = data.copy(deep=False)
            if copy:
                data = data.copy()
            # skips validation of the name
            object.__setattr__(self, "_name", name)
            #만들어진 data를 활용해서 부모생성자 실행
            NDFrame.__init__(self, data)
            return

위 코드를 살펴보면, data가 SingleBlockManager혹은 SingleArrayManager의 static method인 from_array를 통해서 만들어진 Object를 가지고 data를 만들고

이 data를 사용해서 부모Cls인 NDFrame의 생성자를 실행하는 모습을 볼 수 있습니다.

그러면 이 data의 정체가 무엇인지 알기 위해서 SingleBlockManager Class를 잠시 살펴보겠습니다.

1_2. SingleBlockManager

#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:
        """
        Constructor for if we have an array that is not yet a Block.
        """
        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 method의 경우 친절하게 주석으로 Block이 되지못한 Array로 Class Object를 만들 때 쓴다고 되어있습니다.

이때 maybe_coerce_value의 경우 ArrayLike Object의 datetime 혹은 time format을 보정해주는 역할을 합니다.

따라서 array는 그냥 입력받은 array로 아직까지 사용중 입니다.

이제 아래 new_block을 통해서 array가 block이라고 하는 Object로 만들어지고

Block Object와 Index Object를 활용해서 Class Object가 만들어지는것을 볼 수 있습니다

1_3. func new_block

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas/core/internals/blocks.py#L2388
def new_block(
    values,
    placement: BlockPlacement,
    *,
    ndim: int,
    refs: BlockValuesRefs | None = None,
) -> Block:
    klass = get_block_type(values.dtype)
    return klass(values, ndim=ndim, placement=placement, refs=refs)

위 함수를 보면, get_block_type의 결과로 Callable 한 Object를 가지고 오고

이 Object에 받은 매개변수를 넣어주면 Block Class Object가 나오는것을 Type Hinting으로 알 수 있습니다.

그럼 결국 get_block_type이 뭔지를 알아야 합니다.

1_4. func get_block_type

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/internals/blocks.py#L2346
def get_block_type(dtype: DtypeObj) -> type[Block]:
    if isinstance(dtype, DatetimeTZDtype):
        return DatetimeTZBlock
    elif isinstance(dtype, PeriodDtype):
        return NDArrayBackedExtensionBlock
    elif isinstance(dtype, ExtensionDtype):
        # Note: need to be sure NumpyExtensionArray is unwrapped before we get here
        return ExtensionBlock

    # We use kind checks because it is much more performant
    #  than is_foo_dtype
    kind = dtype.kind
    if kind in "Mm":
        return DatetimeLikeBlock

    return NumpyBlock

소스코드를 보면 알 수 있지만, 특수한 경우를 제외하고는 NumpyBlock을 사용하는것을 볼 수 있습니다.

실제 소스코드에서 NumpyBlock 이외에 일부의 Block은 하위호환성을 위해서 남겨져 있습니다

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/internals/blocks.py#L2232
class NumericBlock(NumpyBlock):
    # this Block type is kept for backwards-compatibility
    # TODO(3.0): delete and remove deprecation in __init__.py.
    __slots__ = ()

class ObjectBlock(NumpyBlock):
    # this Block type is kept for backwards-compatibility
    # TODO(3.0): delete and remove deprecation in __init__.py.
    __slots__ = ()

1_5. NumpyBlock

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/internals/blocks.py#L2232
class NumpyBlock(libinternals.NumpyBlock, Block):
    values: np.ndarray
    __slots__ = ()

    @property
    def is_view(self) -> bool:
        """return a boolean if I am possibly a view"""
        return self.values.base is not None

    @property
    def array_values(self) -> ExtensionArray:
        return NumpyExtensionArray(self.values)

    def get_values(self, dtype: DtypeObj | None = None) -> np.ndarray:
        if dtype == _dtype_obj:
            return self.values.astype(_dtype_obj)
        return self.values

    @cache_readonly
    def is_numeric(self) -> bool:  # type: ignore[override]
        dtype = self.values.dtype
        kind = dtype.kind

        return kind in "fciub"

NumpyBlock같은 경우 Block Class와 libinternals.NumpyBlock을 다중상속 받고 있는것을 볼 수 있습니다.

이때 Block Class를 먼저 봐볼까요?

1_6. Block

#https://github.com/pandas-dev/pandas/blob/e86ed377639948c64c429059127bcf5b359ab6be/pandas/core/internals/blocks.py#L154
class Block(PandasObject):
    """
    Canonical n-dimensional unit of homogeneous dtype contained in a pandas
    data structure
    """
    values: np.ndarray | ExtensionArray
    ndim: int
    refs: BlockValuesRefs
    __init__: Callable

    __slots__ = ()
    is_numeric = False
    ...

이 Block에서는 @final을 이용해서 Pandas Data처리를위해 사용되는 상당수의 method가 정의되어있습니다.

하지만 이때 Block Class와 상위 Class는 생성자가 없습니다.

Python의 경우 Class가 매개변수를 받지만, 생성자가 없을경우 부모생성자가 자동으로 실행 됩니다.

class a:
    def __init__(self, value):
        self.value = value

class b:
    def search(self):
        return self.value

class c(a, b):
    value : str

temp = c("kmsdls")
temp.search()
#>>"kmsdls"

그러므로 Block Class가 아니라, libinternals.NumpyBlock를 살펴볼 경우 생성자를 확인할 수 있을 겁니다.

1_7. cdef NumpyBlock

#https://github.com/pandas-dev/pandas/blob/v2.1.1/pandas//_libs/internals.pyx#L703
cdef class NumpyBlock(SharedBlock):
    cdef:
        public ndarray values

    def __cinit__(
        self,
        ndarray values,
        BlockPlacement placement,
        int ndim,
        refs: BlockValuesRefs | None = None,
    ):
        # set values here; the (implicit) call to SharedBlock.__cinit__ will
        # set placement, ndim and refs
        self.values = values

보는것처럼 최상위 NumpyBlock Cython Class에서 cinit을 통해서 value를 받고

self.values = values를 통해서 self.values에 데이터를 ndarray를 저장하는것을 확인할 수가 있습니다.

자 이제 다시 SingleBlockManager로 돌아가 보겠습니다.

2_1. SingleBlockManager2

#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)    

이제 SingleBlockManager.from_array로 반환되는것은 SingleBlockManager Object입니다.

self.axes = Index Object
self.blocks = NumpyBlock Object

로 property를 설정하고, 이 SingleBlockManager Object를 NDFrame의 생성자에 넣게 됩니다.

그러면 NDFrame을 봐볼까요?

2_2. NDFrame

#https://github.com/pandas-dev/pandas/blob/2a65fdd227734cbda5d8e33171e857ab4aaeb9d5/pandas/core/generic.py#L241C1-L277C47
class NDFrame(PandasObject, indexing.IndexingMixin):
    """
    N-dimensional analogue of DataFrame. Store multi-dimensional in a
    size-mutable, labeled data structure

    Parameters
    ----------
    data : BlockManager
    axes : list
    copy : bool, default False
    """

    _internal_names: list[str] = [
        "_mgr",
        "_cacher",
        "_item_cache",
        "_cache",
        "_is_copy",
        "_name",
        "_metadata",
        "_flags",
    ]
    _internal_names_set: set[str] = set(_internal_names)
    _accessors: set[str] = set()
    _hidden_attrs: frozenset[str] = frozenset([])
    _metadata: list[str] = []
    _is_copy: weakref.ReferenceType[NDFrame] | str | None = None
    _mgr: Manager
    _attrs: dict[Hashable, Any]
    _typ: str

    # ----------------------------------------------------------------------
    # Constructors

    def __init__(self, data: Manager) -> None:
        object.__setattr__(self, "_is_copy", None)
        object.__setattr__(self, "_mgr", data)
        ...

Series의 상위 클래스인 NDFrame에서 _mgr property에서 할당하게 됩니다.
(이때 object.__setattr__을 사용하는데, 이는 static variable을 설정할때 쓰게 됩니다)

Pandas Series 정리1

  1. Axes 정보와 Data는 BlockManager가 소유하고 있음
  2. NDFrame에서 _mgr(매니저) property에 BlockManager를 할당함
  3. _mgr에 접근해서 Pandas Series의 Data를 처리하는 다양한 Method를 사용하게됨
  4. Series의 경우 Data를 Numpy array형태로 저장해서 빠른 연산속도를 확보함

여기까지 보면, Pandas Series의 반만 알게된 상태 입니다.

BlockManager를 만들때 사용되는 Data는 확인 하였으니, 이제 Axes에 해당하는 Index Object가 무엇인지 알아야 합니다.

해당 내용은 2편에서 이어집니다.

감사합니다!

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
글 보관함