developer tip

내 코드에서 언제 Pandas apply ()를 사용해야합니까?

copycodes 2020. 12. 25. 10:21
반응형

내 코드에서 언제 Pandas apply ()를 사용해야합니까?


Pandas 메서드 사용과 관련된 Stack Overflow 질문에 대한 많은 답변을 보았습니다 apply. 나는 또한 사용자들이 " apply느리고 피해야한다 "고 말하고있는 것을 보았다 .

apply느린 성능을 설명하는 주제에 대한 많은 기사를 읽었습니다 . 나는 또한 문서에서 apply단순히 UDF 전달을위한 편리한 기능에 대한 면책 조항을 보았습니다 (지금은 찾을 수없는 것 같습니다). 따라서 일반적인 합의는 apply가능하면 피해야한다는 것입니다. 그러나 이것은 다음과 같은 질문을 제기합니다.

  1. 경우 apply그렇게 나쁜, 왜 그것은 API에?
  2. 언제 어떻게 코드를 apply무료로 만들어야 합니까?
  3. 어떤 상황이 어디 이제까지 있는가 apply입니다 좋은 (더 나은 다른 가능한 솔루션보다가)?

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을 처리해야하는 경우 사용자 지정 함수를 사용하여이를 빌드 한 다음 목록 이해 내에서 인수로 호출 할 수 있습니다.

목록 이해가 좋은 옵션으로 간주되어야하는 경우에 대한 자세한 내용은 내 글을 참조하십시오. 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다음으로 변환 : astypeapply

이것은 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비용도 상당히 비싸기 때문 ).


기타주의 사항

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 applys aren't alike

The below chart suggests when to consider apply1. 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:

  1. df['col'].apply(str) may slightly outperform df['col'].astype(str).
  2. df.apply(pd.to_datetime) working on strings doesn't scale well with rows versus a regular for 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

반응형