티스토리 뷰
안녕하세요. 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에 대해서 조금 이해가 되시나요?
'Python 잡지식 > 소스코드 톱아보기' 카테고리의 다른 글
[Pandas] Pandas의 apply동작에 대해서 (0) | 2023.12.06 |
---|---|
[Pandas]inplace=True동작에 대해서 (0) | 2023.12.06 |
[Pandas]Series의 구조에 대해서 알아보자1 (0) | 2023.12.06 |
[Python]Requests Library의 동작 Part2 (0) | 2022.08.12 |
[Python]Requests Library의 동작 Part1 (0) | 2022.08.12 |
- Total
- Today
- Yesterday
- 이분탐색
- GDC
- git
- C++
- 병렬처리
- 코딩테스트
- Sort알고리즘
- 동적계획법
- heap
- Greedy알고리즘
- 컴퓨터그래픽스
- 분할정복
- Search알고리즘
- 알고리즘
- 사칙연산
- Python
- 프로그래머스
- hash
- prime number
- SIMD
- stack
- AVX
- 완전탐색 알고리즘
- 자료구조
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |