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에서 수정되었습니다 . 자세한 내용은 여기를 참조하세요 ).
returns.add(1).apply(np.log)
vs.np.log(returns.add(1)
는apply
일반적으로 약간 더 빠를 경우이며 아래의 jpp 다이어그램의 오른쪽 하단 녹색 상자입니다.