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 메소드는 행/열의 값들의 평균을 구하는 메소드입니다.
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 메소드는 입력 매핑이나 함수에 따라 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 메소드는 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