pandas 주요 메소드 조사(+벡터 연산 여부 )

손호준·2022년 11월 7일
0

add

add함수는 DataFrame에 다른 DataFrame이나, Series, scalar 등의 데이터를 더하는 메소드입니다. 단순 df + df 등의 계산과 차별화되는 것은 fill_value 인수를 통해 계산 불가한 값을 채워 넣는다는 것입니다.사칙 연산 메소드(sub,div,mul...) 모두 동일한 로직을 따릅니다.

DataFrame.add(other, axis='columns', level=None, fill_value=None)

주요 파라미터

other : DataFrame, Series, scalar, dict 등의 데이터
axis : 더할 레이블 축 설정(0이면 행(index), 1이면 열(columns))
level : multiIndex인 경우, 계산할 index 레벨 혹은 int
fill_value : NaN 값 같은 누락 요소를 계산 전에 해당 값을 대체 (float 혹은 None). 기본값 None

return 값 : 연산 결과의 DataFrame

#예제
import pandas as pd

df = pd.DataFrame({'angles': [1, 3, 4],
                   'degrees': [360, 180, 360]},
                  index=['circle', 'triangle', 'rectangle'])

df_multindex = pd.DataFrame({'angles': [1, 3, 4, 4, 5, 6],
                             'degrees': [360, 180, 360, 360, 540, 720]},
                            index=[['A', 'A', 'A', 'B', 'B', 'B'],
                                   ['circle', 'triangle', 'rectangle',
                                    'square', 'pentagon', 'hexagon']])

print(df.div(df_multindex, level=1, fill_value=0))

# A circle        1.0      1.0
#   triangle      1.0      1.0
#   rectangle     1.0      1.0
# B square        0.0      0.0
#   pentagon      0.0      0.0
#   hexagon       0.0      0.0
#소스 코드
@Appender(doc)
    def f(self, other, axis=default_axis, level=None, fill_value=None):

        ###중략###

        if isinstance(other, ABCDataFrame):#데이터프레임인 경우
            # Another DataFrame
            new_data = self._combine_frame(other, na_op, fill_value)

        elif isinstance(other, ABCSeries):#시리즈인 경우
            new_data = self._dispatch_frame_op(other, op, axis=axis)
        else:#그 외(스칼라인 경우)
            # in this case we always have `np.ndim(other) == 0`
            if fill_value is not None:
                self = self.fillna(fill_value)

            new_data = self._dispatch_frame_op(other, op)

        return self._construct_result(new_data) #리턴값
        
        
        
def _dispatch_frame_op(self, right, func: Callable, axis: int | None = None):
    
        ###중략###
   
        elif isinstance(right, DataFrame):
            assert self.index.equals(right.index)
            assert self.columns.equals(right.columns)
  
            with np.errstate(all="ignore"):
                bm = self._mgr.operate_blockwise( # block manager 등장
                    right._mgr,
                    array_op,
                )
            return self._constructor(bm)
            
        ###중략###
        
        )
        
        
def operate_blockwise(
    left: BlockManager, right: BlockManager, array_op
) -> BlockManager:

    res_blks: list[Block] = []
    for lvals, rvals, locs, left_ea, right_ea, rblk in _iter_block_pairs(left, right):
        res_values = array_op(lvals, rvals)
        if left_ea and not right_ea and hasattr(res_values, "reshape"):
            res_values = res_values.reshape(1, -1)
        nbs = rblk._split_op_result(res_values)

        _reset_block_mgr_locs(nbs, locs)

        res_blks.extend(nbs)

    new_mgr = type(right)(tuple(res_blks), axes=right.axes, verify_integrity=False)
    return new_mgr

BlockManager가 존재하는 것으로 보아, 벡터 연산이 되고 있다고 추측할 수 있습니다.

mean

mean 메소드는 행/열의 값들의 평균을 구하는 메소드입니다.

DataFrame.mean(axis=None, skipna=None, level=None, numeric_only=None, kwargs)

주요 파라미터

axis : 기준 축 설정(0이면 index, 1이면 columns). 기본값 0
skipna : 결측치를 무시할지 여부 (기본값 True)
level : DataFrame 및 Series Aggregation에서 level 키워드를 사용하는 것은 더 이상 사용되지 않으며 이후 버전에서 제거될 예정입니다. 대신 groupby를 사용합니다.
numeric_only : 숫자, 소수, 부울만 이용할지 여부(기본값 None)
kwargs : 함수에 전달할 추가 키워드

return값 : Series 혹은 DataFrame

#예제
idx = [['IDX1','IDX1','IDX2','IDX2'],['row1','row2','row3','row4']]
col = [['COL1','COL1','COL2','COL2'],['val1','val2','val3','val4']]
data = [[None,13,3,4],[5,7,10,8],[15,6,None,3],[2,14,9,1]]
df = pd.DataFrame(data,idx,col)
print(df.mean())

# 출력
# COL1  val1     7.333333
#       val2    10.000000
# COL2  val3     7.333333
#       val4     4.000000
# dtype: float64
#소스코드
def mean(
        self,
        axis: Axis | None | lib.NoDefault = lib.no_default,
        skipna: bool_t = True,
        level: Level | None = None,
        numeric_only: bool_t | None = None,
        **kwargs,
    ) -> Series | float:
        return self._stat_function(
            "mean", nanops.nanmean, axis, skipna, level, numeric_only, **kwargs
        )
        
        
def _stat_function(
        self,
        name: str,
        func,
        axis: Axis | None | lib.NoDefault = None,
        skipna: bool_t = True,
        level: Level | None = None,
        numeric_only: bool_t | None = None,
        **kwargs,
    ):
        if name == "median":
            nv.validate_median((), kwargs)
        else:
            nv.validate_stat_func((), kwargs, fname=name)

        validate_bool_kwarg(skipna, "skipna", none_allowed=False)

		##axis가 None, level이 None, ndim이 2이상일때
        if axis is None and level is None and self.ndim > 1:
            # user must have explicitly passed axis=None
            # GH#21597
            warnings.warn(
                f"In a future version, DataFrame.{name}(axis=None) will return a "
                f"scalar {name} over the entire DataFrame. To retain the old "
                f"behavior, use 'frame.{name}(axis=0)' or just 'frame.{name}()'",
                FutureWarning,
                stacklevel=find_stack_level(),
            )
        if axis is lib.no_default:
            axis = None

        if axis is None:
            axis = self._stat_axis_number
        if level is not None:
            warnings.warn(
                "Using the level keyword in DataFrame and Series aggregations is "
                "deprecated and will be removed in a future version. Use groupby "
                "instead. df.median(level=1) should use df.groupby(level=1).median().",
                FutureWarning,
                stacklevel=find_stack_level(),
            )
            return self._agg_by_level( # grouped.aggregate(applyf)를 리턴
                name, axis=axis, level=level, skipna=skipna, numeric_only=numeric_only
            )
        return self._reduce(
            func, name=name, axis=axis, skipna=skipna, numeric_only=numeric_only
        )
        
        
        

def aggregate(self, func=None, *args, engine=None, engine_kwargs=None, **kwargs):

        if maybe_use_numba(engine):
            with self._group_selection_context():
                data = self._selected_obj
            result = self._aggregate_with_numba(
                data.to_frame(), func, *args, engine_kwargs=engine_kwargs, **kwargs
            )
            index = self.grouper.result_index
            return self.obj._constructor(result.ravel(), index=index, name=data.name)
		
        ###중략###
        
        else:
            cyfunc = com.get_cython_func(func)
            if cyfunc and not args and not kwargs:
                return getattr(self, cyfunc)()

            if self.grouper.nkeys > 1:
                return self._python_agg_general(func, *args, **kwargs)

            try:
                return self._python_agg_general(func, *args, **kwargs)
            except KeyError:
            
                ###중략###
                
                )
        
        
                
def get_cython_func(arg: Callable) -> str | None:
    """
    if we define an internal function for this argument, return it
    """
    return _cython_table.get(arg)
    
    
    
_cython_table = {
    builtins.sum: "sum",
    builtins.max: "max",
    builtins.min: "min",
    np.all: "all",
    np.any: "any",
    np.sum: "sum",
    np.nansum: "sum",
    np.mean: "mean",
    np.nanmean: "mean",
    np.prod: "prod",
    np.nanprod: "prod",
    np.std: "std",
    np.nanstd: "std",
    np.var: "var",
    np.nanvar: "var",
    np.median: "median",
    np.nanmedian: "median",
    np.max: "max",
    np.nanmax: "max",
    np.min: "min",
    np.nanmin: "min",
    np.cumprod: "cumprod",
    np.nancumprod: "cumprod",
    np.cumsum: "cumsum",
    np.nancumsum: "cumsum",
}



def _python_agg_general(self, func, *args, raise_on_typeerror=False, **kwargs):
        func = com.is_builtin_func(func)
        f = lambda x: func(x, *args, **kwargs)

def is_builtin_func(arg):
    """
    if we define a builtin function for this argument, return it,
    otherwise return the arg
    """
    return _builtin_table.get(arg, arg)
    
    
_builtin_table = {
    builtins.sum: np.sum,
    builtins.max: np.maximum.reduce,
    builtins.min: np.minimum.reduce,
}

cython_table내에 numpy 함수들이 존재하고 이것들과 매핑하는식으로 연산되는 것을 확인할 수 있습니다. 이를 통해 mean메소드가 벡터 연산을 시행한다는 것을 알 수 있습니다.

map

map 메소드는 입력 매핑이나 함수에 따라 Series의 각 값을 다른 값으로 대체합니다. arg가 딕셔너리일때, Series에 없는 딕셔너리 키 값은 NaN으로 변환됩니다. 하지만 딕셔너리가 __missing__ 을 정의하는 dict의 하위 클래스인 경우(즉, 기본값에 대한 메서드를 제공함) NaN 대신 이 기본값이 사용됩니다.

Series.map(arg, na_action=None)

주요 파라미터

arg : 함수, collections.abc.Mapping의 서브클래스 혹은 Series
na_action : None 혹은 ignore. 만약 ignore 라면 NaN 값을 매팽시키지 않음. 기본값은 None

return 값 : Series

#예제
s = pd.Series(['cat', 'dog', np.nan, 'rabbit'])

s
# 0      cat
# 1      dog
# 2      NaN
# 3   rabbit
# dtype: object

s.map({'cat': 'kitten', 'dog': 'puppy'})
# 0   kitten
# 1    puppy
# 2      NaN
# 3      NaN
# dtype: object

s.map('I am a {}'.format)
# 0       I am a cat
# 1       I am a dog
# 2       I am a nan
# 3    I am a rabbit
# dtype: object

s.map('I am a {}'.format, na_action='ignore')
# 0     I am a cat
# 1     I am a dog
# 2            NaN
# 3  I am a rabbit
# dtype: object
#소스코드
def map(
        self,
        arg: Callable | Mapping | Series,
        na_action: Literal["ignore"] | None = None,
    ) -> Series:

        new_values = self._map_values(arg, na_action=na_action)
        return self._constructor(new_values, index=self.index).__finalize__(
            self, method="map"
        )
        

def _map_values(self, mapper, na_action=None):
        """
        dict, Series의 입력에 대해 값을 매핑하는 내부 함수
        """
        if is_dict_like(mapper): # mapper가 딕셔너리 형태인 경우
            if isinstance(mapper, dict) and hasattr(mapper, "__missing__"):
                # 딕셔너리의 하위 클래스에서 기본값 메소드(__missing__)를 정의한 경우
                dict_with_default = mapper
                mapper = lambda x: dict_with_default[x]
            else: # 딕셔너리가 기본값을 갖지 않는 경우
                # 효율성을 위해 Series로 변환하는 것이 안전
                # 튜플일 가능성을 처리하기 위해 여기에 키를 지정
                
                # 빈 mapper와 매핑의 반환값은 pd.Series(np.nan, ...)가 될 것으로 예상
                # np.nan의 데이터타입이 float64이므로 이 방법의 반환값도 float64
                mapper = create_series_with_explicit_dtype(
                    mapper, dtype_if_empty=np.float64
                )

        if isinstance(mapper, ABCSeries):
            if na_action not in (None, "ignore"):
            # na_action이 None이나 ignore가 아닐 경우
                msg = (
                    "na_action must either be 'ignore' or None, "
                    f"{na_action} was passed"
                )
                raise ValueError(msg) # 에러 발생시키기
			
            if na_action == "ignore":# na_action이 ignore인 경우
                mapper = mapper[mapper.index.notna()]

            # 딕셔너리나 Series로 부터 값이 입력되었으므로 mapper는 인덱스가 되어야함
            if is_categorical_dtype(self.dtype):
                # 모든 값 대신 범주를 매핑하여 시간을 절약하는 Series mapper사용

                cat = cast("Categorical", self._values)
                return cat.map(mapper)

            values = self._values

            indexer = mapper.index.get_indexer(values)
            new_values = algorithms.take_nd(mapper._values, indexer)

            return new_values
            
            # we must convert to python types
        if is_extension_array_dtype(self.dtype) and hasattr(self._values, "map"):
            # GH#23179 some EAs do not have `map`
            values = self._values
            if na_action is not None:
                raise NotImplementedError
            map_f = lambda values, f: values.map(f)
        else:
            values = self._values.astype(object)
            if na_action == "ignore":
                map_f = lambda values, f: lib.map_infer_mask(
                    values, f, isna(values).view(np.uint8)
                )
            elif na_action is None:
                map_f = lib.map_infer
            else:
                msg = (
                    "na_action must either be 'ignore' or None, "
                    f"{na_action} was passed"
                )
                raise ValueError(msg)

        # mapper is a function
        new_values = map_f(values, mapper)

        return new_values
 
def take_nd(
    arr: np.ndarray,
    indexer,
    axis: int = ...,
    fill_value=...,
    allow_fill: bool = ...,
) -> np.ndarray: # ndarray 반환
    ...

최종적으로 반환하는 값이 ndarray이고, _take_nd_ndarray함수 내부에서 transpose시키는 부분이 존재하기 때문에 벡터 연산이 시행된다고 추측해볼 수 있습니다.

assign

assign 메소드는 DataFrame에 새 열을 할당하는 메소드입니다. 할당할 새 열이 기존열과 이름이 같을 경우 덮어씌워집니다.

DataFrame.assign(kwargs)

주요 파라미터

kwargs : 새로운 열 이름 = 내용 형식으로 입력되는 키워드. 콤마로 여러개 입력 가능

#예제
df = pd.DataFrame(index = ['row1','row2','row3'],data={'col1':[1,2,3]})
print(df.assign(col1=[0,0,0],col2=lambda x : x.col1+2,col3=df['col1']*(-2)))
#소스코드
def assign(self, **kwargs) -> DataFrame:
        data = self.copy()

        for k, v in kwargs.items():
            data[k] = com.apply_if_callable(v, data)
        return data
profile
Rustacean🦀/Data engineer💻

0개의 댓글