내 코드에서 언제 Pandas apply ()를 사용해야합니까?
Pandas 메서드 사용과 관련된 Stack Overflow 질문에 대한 많은 답변을 보았습니다 apply
. 나는 또한 사용자들이 " apply
느리고 피해야한다 "고 말하고있는 것을 보았다 .
apply
느린 성능을 설명하는 주제에 대한 많은 기사를 읽었습니다 . 나는 또한 문서에서 apply
단순히 UDF 전달을위한 편리한 기능에 대한 면책 조항을 보았습니다 (지금은 찾을 수없는 것 같습니다). 따라서 일반적인 합의는 apply
가능하면 피해야한다는 것입니다. 그러나 이것은 다음과 같은 질문을 제기합니다.
- 경우
apply
그렇게 나쁜, 왜 그것은 API에? - 언제 어떻게 코드를
apply
무료로 만들어야 합니까? - 어떤 상황이 어디 이제까지 있는가
apply
입니다 좋은 (더 나은 다른 가능한 솔루션보다가)?
apply
, 결코 필요하지 않은 편의 기능
OP의 질문을 하나씩 해결하는 것으로 시작합니다.
" 만약이 적용 후 왜 API에서 그것을, 그래서 나쁜? "
DataFrame.apply
하고 Series.apply
있는 편의 기능을 각각 객체 DataFrame 및 시리즈에 정의. apply
DataFrame에 변환 / 집계를 적용하는 모든 사용자 정의 함수를 허용합니다. apply
기존 pandas 기능이 할 수없는 일을 효과적으로 수행하는 은색 총알입니다.
다음과 같은 작업 apply
을 수행 할 수 있습니다.
- DataFrame 또는 Series에서 사용자 정의 함수 실행
- DataFrame에 행 방식 (
axis=1
) 또는 열 방식 ( ) 함수 적용axis=0
- 기능을 적용하는 동안 인덱스 정렬 수행
- 사용자 정의 함수로 집계 수행 (그러나 일반적으로 선호
agg
하거나transform
이러한 경우) - 요소 별 변환 수행
- 집계 된 결과를 원래 행으로 브로드 캐스트합니다 (
result_type
인수 참조 ). - 사용자 정의 함수에 전달할 위치 / 키워드 인수를 허용합니다.
... 다른 것들 중에서. 자세한 내용은 설명서의 행 또는 열 방식 함수 응용 프로그램 을 참조하십시오 .
따라서 이러한 모든 기능을 사용하면 왜 apply
나쁜가요? 그것은입니다 때문 apply
입니다 느린 . Pandas는 함수의 특성에 대한 가정을하지 않으므로 필요에 따라 각 행 / 열에 함수 를 반복적으로 적용합니다 . 또한 위의 모든 상황을 처리 한다는 apply
것은 각 반복에서 상당한 오버 헤드가 발생 한다는 것을 의미 합니다. 또한 apply
더 많은 메모리를 소비하므로 메모리 제한 응용 프로그램의 문제입니다.
apply
사용하기에 적절한 상황은 거의 없습니다 (아래에서 자세히 설명). 을 사용해야하는지 확실하지 않은 경우 사용 apply
해서는 안됩니다.
다음 질문에 대해 말씀 드리겠습니다.
" 코드를 언제 어떻게 무료로 적용 해야 합니까? "
다시 말하면 다음과 같은 일반적인 상황에서 에 대한 호출을 제거 해야합니다 apply
.
숫자 데이터
숫자 데이터로 작업하는 경우 수행하려는 작업을 정확히 수행하는 벡터화 된 cython 함수가 이미있을 수 있습니다 (그렇지 않은 경우 Stack Overflow에 질문하거나 GitHub에서 기능 요청을여십시오).
apply
간단한 추가 작업 을 위해 의 성능을 비교합니다 .
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df
A B
0 9 12
1 4 7
2 2 5
3 1 4
df.apply(np.sum)
A 16
B 28
dtype: int64
df.sum()
A 16
B 28
dtype: int64
성능면에서 비교가 없으며 cythonized 동등 물이 훨씬 빠릅니다. 장난감 데이터에서도 차이가 분명하기 때문에 그래프가 필요하지 않습니다.
%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
raw
인수 와 함께 원시 배열 전달을 활성화하더라도 여전히 두 배 느립니다.
%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
다른 예시:
df.apply(lambda x: x.max() - x.min())
A 8
B 8
dtype: int64
df.max() - df.min()
A 8
B 8
dtype: int64
%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()
2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
일반적으로 가능하면 벡터화 된 대안을 찾으십시오.
문자열 / 정규식
Pandas는 대부분의 상황에서 "벡터화 된"문자열 함수를 제공하지만 이러한 함수가 "적용"되지 않는 드문 경우가 있습니다.
일반적인 문제는 열의 값이 같은 행의 다른 열에 있는지 확인하는 것입니다.
df = pd.DataFrame({
'Name': ['mickey', 'donald', 'minnie'],
'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
'Value': [20, 10, 86]})
df
Name Value Title
0 mickey 20 wonderland
1 donald 10 welcome to donald's castle
2 minnie 86 Minnie mouse clubhouse
"donald"와 "minnie"가 각각의 "Title"열에 있으므로 두 번째 및 세 번째 행을 반환해야합니다.
적용을 사용하면 다음을 사용하여 수행됩니다.
df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)
0 False
1 True
2 True
dtype: bool
df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
그러나 목록 내포를 사용하는 더 나은 솔루션이 있습니다.
df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
여기서 주목할 점은 apply
오버 헤드가 낮기 때문에 반복 루틴이 . NaN 및 유효하지 않은 dtype을 처리해야하는 경우 사용자 지정 함수를 사용하여이를 빌드 한 다음 목록 이해 내에서 인수로 호출 할 수 있습니다.
목록 이해가 좋은 옵션으로 간주되어야하는 경우에 대한 자세한 내용은 내 글을 참조하십시오. For loops with pandas-When should I care? .
참고
날짜 및 날짜 시간 작업에도 벡터화 된 버전이 있습니다. 예를 들어pd.to_datetime(df['date'])
,df['date'].apply(pd.to_datetime)
.문서 에서 더 많은 것을 읽으십시오 .
일반적인 함정 : 폭발적인 목록 열
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
사람들은 사용하려는 유혹을 apply(pd.Series)
받습니다. 이것은 성능면에서 끔찍 합니다.
s.apply(pd.Series)
0 1
0 1 2
1 1 2
2 1 2
더 나은 옵션은 열을 나열하고 pd.DataFrame에 전달하는 것입니다.
pd.DataFrame(s.tolist())
0 1
0 1 2
1 1 2
2 1 2
%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())
2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
마지막으로
" 좋은 상황
apply
은 없나요? "
적용은 편의 기능이므로 용서할 수있을만큼 오버 헤드가 무시할 수있는 상황 이 있습니다. 함수가 호출되는 횟수에 따라 다릅니다.
시리즈에 대해 벡터화되지만 데이터 프레임이 아닌 함수
여러 열에 문자열 연산을 적용하려면 어떻게해야합니까? 여러 열을 datetime으로 변환하려면 어떻게해야합니까? 이러한 함수는 시리즈에 대해서만 벡터화되므로 변환 / 연산 하려는 각 열에 적용 해야합니다 .
df = pd.DataFrame(
pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2),
columns=['date1', 'date2'])
df
date1 date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30
df.dtypes
date1 object
date2 object
dtype: object
다음의 경우 허용되는 경우입니다 apply
.
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
을 stack
사용하거나 명시 적 루프를 사용하는 것도 의미가 있습니다 . 이 모든 옵션은를 사용하는 것보다 약간 빠르지 apply
만 그 차이는 용서할만큼 작습니다.
%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
문자열 연산이나 카테고리로의 변환과 같은 다른 연산에 대해서도 비슷한 경우를 만들 수 있습니다.
u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))
v / s
u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
v[c] = df[c].astype(category)
등등...
시리즈를 str
다음으로 변환 : astype
대apply
이것은 API의 특이한 것 같습니다. apply
Series의 정수를 문자열로 변환하는 데 사용 하는 것은를 사용 하는 것보다 비슷하며 때로는 더 빠릅니다 astype
.
그래프는 perfplot
라이브러리를 사용하여 그려 졌습니다.
import perfplot
perfplot.show(
setup=lambda n: pd.Series(np.random.randint(0, n, n)),
kernels=[
lambda s: s.astype(str),
lambda s: s.apply(str)
],
labels=['astype', 'apply'],
n_range=[2**k for k in range(1, 20)],
xlabel='N',
logx=True,
logy=True,
equality_check=lambda x, y: (x == y).all())
수레를 사용하면가 astype
일관되게 빠르거나 apply
. 따라서 이것은 테스트의 데이터가 정수 유형이라는 사실과 관련이 있습니다.
GroupBy
연결 변환 작업
GroupBy.apply
지금까지 논의되지 않았지만 GroupBy.apply
기존 GroupBy
함수가 처리 하지 못하는 것을 처리하기위한 반복적 인 편의 함수이기도합니다 .
한 가지 일반적인 요구 사항은 GroupBy를 수행 한 다음 "지연된 cumsum"과 같은 두 가지 주요 작업을 수행하는 것입니다.
df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df
A B
0 a 12
1 a 7
2 b 5
3 c 4
4 c 5
5 c 4
6 d 3
7 d 2
8 e 1
9 e 10
여기에 두 번의 연속적인 groupby 호출이 필요합니다.
df.groupby('A').B.cumsum().groupby(df.A).shift()
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
를 사용 apply
하면이를 단일 통화로 단축 할 수 있습니다.
df.groupby('A').B.apply(lambda x: x.cumsum().shift())
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
데이터에 의존하기 때문에 성능을 정량화하는 것은 매우 어렵습니다. 그러나 일반적으로 통화 apply
를 줄이는 것이 목표 인 경우 허용되는 솔루션입니다 groupby
( groupby
비용도 상당히 비싸기 때문 ).
기타주의 사항
Aside from the caveats mentioned above, it is also worth mentioning that apply
operates on the first row (or column) twice. This is done to determine whether the function has any side effects. If not, apply
may be able to use a fast-path for evaluating the result, else it falls back to a slow implementation.
df = pd.DataFrame({
'A': [1, 2],
'B': ['x', 'y']
})
def func(x):
print(x['A'])
return x
df.apply(func, axis=1)
# 1
# 1
# 2
A B
0 1 x
1 2 y
This behaviour is also seen in GroupBy.apply
on pandas versions <0.25 (it was fixed for 0.25, see here for more information.)
All apply
s aren't alike
The below chart suggests when to consider apply
1. Green means possibly efficient; red avoid.
Some of this is intuitive: pd.Series.apply
is a Python-level row-wise loop, ditto pd.DataFrame.apply
row-wise (axis=1
). The misuses of these are many and wide-ranging. The other post deals with them in more depth. Popular solutions are to use vectorised methods, list comprehensions (assumes clean data), or efficient tools such as the pd.DataFrame
constructor (e.g. to avoid apply(pd.Series)
).
If you are using pd.DataFrame.apply
row-wise, specifying raw=True
(where possible) is often beneficial. At this stage, numba
is usually a better choice.
GroupBy.apply
: generally favoured
Repeating groupby
operations to avoid apply
will hurt performance. GroupBy.apply
is usually fine here, provided the methods you use in your custom function are themselves vectorised. Sometimes there is no native Pandas method for a groupwise aggregation you wish to apply. In this case, for a small number of groups apply
with a custom function may still offer reasonable performance.
pd.DataFrame.apply
column-wise: a mixed bag
pd.DataFrame.apply
column-wise (axis=0
) is an interesting case. For a small number of rows versus a large number of columns, it's almost always expensive. For a large number of rows relative to columns, the more common case, you may sometimes see significant performance improvements using apply
:
# Python 3.7, Pandas 0.23.4
np.random.seed(0)
df = pd.DataFrame(np.random.random((10**7, 3))) # Scenario_1, many rows
df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns
# Scenario_1 | Scenario_2
%timeit df.sum() # 800 ms | 109 ms
%timeit df.apply(pd.Series.sum) # 568 ms | 325 ms
%timeit df.max() - df.min() # 1.63 s | 314 ms
%timeit df.apply(lambda x: x.max() - x.min()) # 838 ms | 473 ms
%timeit df.mean() # 108 ms | 94.4 ms
%timeit df.apply(pd.Series.mean) # 276 ms | 233 ms
1 There are exceptions, but these are usually marginal or uncommon. A couple of examples:
df['col'].apply(str)
may slightly outperformdf['col'].astype(str)
.df.apply(pd.to_datetime)
working on strings doesn't scale well with rows versus a regularfor
loop.
For axis=1
(i.e. row-wise functions) then you can just use the following function in lieu of apply
. I wonder why this isn't the pandas
behavior. (Untested with compound indexes, but it does appear to be much faster than apply
)
def faster_df_apply(df, func):
cols = list(df.columns)
data, index = [], []
for row in df.itertuples(index=True):
row_dict = {f:v for f,v in zip(cols, row[1:])}
data.append(func(row_dict))
index.append(row[0])
return pd.Series(data, index=index)
Are there ever any situations where apply
is good? Yes, sometimes.
Task: decode Unicode strings.
import numpy as np
import pandas as pd
import unidecode
s = pd.Series(['mañana','Ceñía'])
s.head()
0 mañana
1 Ceñía
s.apply(unidecode.unidecode)
0 manana
1 Cenia
업데이트
나는의 사용을 옹호 결코 하였다 apply
는 이후 단지 생각하고 NumPy
위의 상황을 처리 할 수없는, 그것은 좋은 후보 수 있었다 pandas apply
. 그러나 @jpp의 알림 덕분에 평범한 목록 이해를 잊었습니다.
참조 URL : https://stackoverflow.com/questions/54432583/when-should-i-ever-want-to-use-pandas-apply-in-my-code
'developer tip' 카테고리의 다른 글
두 개의 Div가 서로 나란히 있으며 반응 형 변경으로 스택됩니다. (0) | 2020.12.25 |
---|---|
완전히 파이썬으로 된 안드로이드 앱 (0) | 2020.12.25 |
프로그램이 32 비트 또는 64 비트라는 것은 무엇을 의미합니까? (0) | 2020.12.25 |
부울 getter 메소드에 대한 유효한 JavaBeans 이름 (0) | 2020.12.25 |
전체 VS 솔루션의 모든 파일에서 탭을 대체하는 도구 / 트릭 (0) | 2020.12.25 |