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을 처리해야하는 경우 사용자 지정 함수를 사용하여이를 기반으로 빌드 한 다음 목록 이해 내에서 인수를 사용하여 호출 할 수 있습니다.
목록 이해가 좋은 옵션으로 간주되어야하는 경우에 대한 자세한 내용은 내 글 : 판다를 사용한 루프-언제 신경 써야합니까?를 참조하십시오 . .
참고
날짜 및 날짜 시간 작업에도 벡터화 된 버전이 있습니다. 예를 들어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
비용도 상당히 비싸기 때문 ).
기타주의 사항
위에서 언급 한주의 사항 외에도 apply
첫 번째 행 (또는 열)에서 두 번 작동 한다는 점도 언급 할 가치가 있습니다 . 이것은 기능에 부작용이 있는지 확인하기 위해 수행됩니다. 그렇지 않으면 apply
결과를 평가하기 위해 빠른 경로를 사용할 수 있습니다. 그렇지 않으면 느린 구현으로 돌아갑니다.
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
이 동작은 GroupBy.apply
pandas 버전 <0.25 에서도 볼 수 있습니다 (0.25에서 수정되었습니다 . 자세한 내용은 여기를 참조하세요 ).
답변
모든 apply
s가 동일 하지는 않습니다.
아래 차트는 apply
1 을 고려할 때를 제안 합니다. 녹색은 효율성을 의미합니다. 빨간색 피하십시오.
이 중 일부 는 직관적 pd.Series.apply
입니다. Python 수준의 행 방식 루프, 마찬가지로 pd.DataFrame.apply
행 방식 ( axis=1
)입니다. 이것들의 오용은 많고 광범위합니다. 다른 포스트는 그것들을 더 깊이 다루고 있습니다. 인기있는 솔루션은 벡터화 된 방법, 목록 이해 (깨끗한 데이터 가정) 또는 pd.DataFrame
생성자 와 같은 효율적인 도구 (예 :)를 사용하는 것 apply(pd.Series)
입니다.
pd.DataFrame.apply
행 방식을 사용 하는 raw=True
경우 가능한 경우 지정 하는 것이 도움이되는 경우가 많습니다. 이 단계에서는 numba
일반적으로 더 나은 선택입니다.
GroupBy.apply
: 일반적으로 선호
groupby
피하기 위해 작업을 반복 apply
하면 성능이 저하됩니다. GroupBy.apply
사용자 지정 함수에서 사용하는 메서드가 자체적으로 벡터화 된 경우 일반적으로 여기에서는 괜찮습니다. 적용하려는 그룹 별 집계에 대한 기본 Pandas 메서드가없는 경우가 있습니다. 이 경우 apply
사용자 지정 기능이있는 소수의 그룹 에 대해 여전히 합리적인 성능을 제공 할 수 있습니다.
pd.DataFrame.apply
컬럼 방식 : 혼합 백
pd.DataFrame.apply
열 단위 ( axis=0
)는 흥미로운 경우입니다. 적은 수의 행과 많은 수의 열의 경우 거의 항상 비용이 많이 듭니다. 열을 기준으로 많은 수의 행의 경우, 일반적인 경우, 당신은 할 수 있습니다 가끔 사용하여 상당한 성능 향상을 볼 수 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 예외가 있지만 일반적으로 미미하거나 드문 경우입니다. 몇 가지 예 :
df['col'].apply(str)
약간의 실적을 올릴 수 있습니다df['col'].astype(str)
.df.apply(pd.to_datetime)
문자열 작업은 일반for
루프에 비해 행과 잘 맞지 않습니다 .
답변
의 경우 axis=1
(즉, 행 방향 기능) 다음은 대신에 다음과 같은 기능을 사용할 수 있습니다 apply
. 왜 이것이 pandas
행동 이 아닌지 궁금합니다 . (복합 인덱스로 테스트되지 않았지만보다 훨씬 빠른 것 같습니다. 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)
답변
apply
좋은 상황 이 있습니까? 예, 가끔 요.
작업 : 유니 코드 문자열 디코딩.
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의 알림 덕분에 평범한 목록 이해를 잊었습니다.
답변
