apply, 결코 필요하지 않은 편의 기능
OP의 질문을 하나씩 해결하는 것으로 시작합니다.
" 만약이 적용 후 왜 API에서 그것을, 그래서 나쁜? "
DataFrame.apply하고 Series.apply있는 편의 기능을 각각 객체 DataFrame 및 시리즈에 정의. applyDataFrame에 변환 / 집계를 적용하는 모든 사용자 정의 함수를 허용합니다. 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의 특이한 것 같습니다. applySeries의 정수를 문자열로 변환하는 데 사용 하는 것은를 사용 하는 것보다 비슷하며 때로는 더 빠릅니다 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.applypandas 버전 <0.25 에서도 볼 수 있습니다 (0.25에서 수정되었습니다 . 자세한 내용은 여기를 참조하세요 ).
returns.add(1).apply(np.log)vs.np.log(returns.add(1)는apply일반적으로 약간 더 빠를 경우이며 아래의 jpp 다이어그램의 오른쪽 하단 녹색 상자입니다.