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


111

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

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

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

1
returns.add(1).apply(np.log)vs. np.log(returns.add(1)apply일반적으로 약간 더 빠를 경우이며 아래의 jpp 다이어그램의 오른쪽 하단 녹색 상자입니다.
Alexander

@Alexander 감사합니다. 이러한 상황을 철저히 지적하지는 않았지만 알아두면 유용합니다!
cs95

답변:


108

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다음으로 변환 : 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비용도 상당히 비싸기 때문 ).


기타주의 사항

위에서 언급 한주의 사항 외에도 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에서 수정되었습니다 . 자세한 내용은 여기를 참조하세요 ).


조심해야 할 것 같아 .. %timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')첫 번째 반복 후에는 확실히 datetime... 로 변환 하기 때문에 훨씬 빨라질 것입니다 . datetime?
jpp

@jpp 나는 같은 관심사를 가졌습니다. 그러나 여전히 선형 스캔을 수행해야합니다. 문자열에서 to_datetime을 호출하는 것은 빠르지는 않지만 datetime 객체에서 호출하는 것만 큼 빠릅니다. 야구장 타이밍은 동일합니다. 대안은 요점에서 벗어나 모든 시간 제한 솔루션에 대해 몇 가지 사전 복사 단계를 구현하는 것입니다. 그러나 그것은 유효한 관심사입니다.
cs95

" to_datetime문자열 호출 은 ... datetime객체 만큼 빠르다 ".. 정말? I는 dataframe에서 생성 (고정비)을 포함 applyVS for루프 타이밍과의 차이는 매우 작다.
jpp

@jpp 글쎄, 그것이 내 (허용되는 제한적) 테스트에서 얻은 것입니다. 나는 그것이 데이터에 의존한다고 확신하지만, 일반적인 생각은 설명을 위해 차이가 "진지하게, 그것에 대해 걱정하지 마십시오"라는 것입니다.
cs95

1
@ cs95, 새해 복 많이 받으세요!
jpp

49

모든 applys가 동일 하지는 않습니다.

아래 차트는 apply1 을 고려할 때를 제안 합니다. 녹색은 효율성을 의미합니다. 빨간색 피하십시오.

여기에 이미지 설명 입력

이 중 일부 는 직관적 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 예외가 있지만 일반적으로 미미하거나 드문 경우입니다. 몇 가지 예 :

  1. df['col'].apply(str)약간의 실적을 올릴 수 있습니다 df['col'].astype(str).
  2. df.apply(pd.to_datetime)문자열 작업은 일반 for루프에 비해 행과 잘 맞지 않습니다 .


1
@coldspeed, 감사합니다, 귀하의 게시물에는 별 문제가 없습니다 (일부 모순 된 벤치마킹 대 내 것 제외, 입력 또는 설정 기반 일 수 있음). 문제를 보는 다른 방법이 있다고 느꼈습니다.
jpp

@jpp 나는 항상 내가 오늘 볼 때까지 당신에게 지침으로있는 거 우수한 플로우 차트를 사용하여 행이 많다는apply 빠르게보다 훨씬입니다 내 솔루션 과 함께 any. 이것에 대한 어떤 생각?
Stef

1
@jpp : 당신 말이 맞습니다 : 1mio 행 x 100 cols의 any경우 apply. 2000 행 x 1000 열로 첫 번째 테스트를 수행했으며 여기 apply보다 두 배 빨랐습니다any
Stef

1
@jpp 프레젠테이션 / 기사에 이미지를 사용하고 싶습니다. 괜찮 으세요? 나는 분명히 출처를 언급 할 것이다. 감사합니다
Erfan

3

의 경우 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)

나는 이것이 어떤 경우에 더 나은 성능을 제공한다는 사실에 매우 놀랐습니다. 열 값의 하위 집합이 서로 다른 여러 작업을 수행해야 할 때 특히 유용했습니다. "모든 적용이 동일하지 않음"답변은 도움이 될 가능성이있는시기를 파악하는 데 도움이 될 수 있지만 데이터 샘플을 테스트하는 것은 그리 어렵지 않습니다.
denson 19

몇 가지 지침 : 성능을 위해 목록 이해가 for 루프를 능가합니다. zip(df, row[1:])여기서 충분합니다. numba실제로이 단계에서 func가 숫자 계산인지 고려 하십시오. 설명 은 이 답변 을 참조하십시오 .
jpp

@jpp-더 나은 기능이 있으면 공유하십시오. 내 분석에서 이것이 최적에 가깝다고 생각합니다. 예 numba는 더 빠르며, faster_df_apply그와 동등한 것을 원하지만 DataFrame.apply(이상하게 느리게) 더 빠른 사람들을위한 것입니다 .
Pete Cacioppi

2

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의 알림 덕분에 평범한 목록 이해를 잊었습니다.


음 ... 아니. [unidecode.unidecode(x) for x in s]또는 보다 나은 점은 list(map(unidecode.unidecode, s))무엇입니까?
jpp

1
이미 pandas 시리즈 였기 때문에 나는 apply를 사용하고 싶었습니다. 맞습니다. list-comp를 사용하는 것이 apply보다 낫습니다. 그러나 downvote는 약간 가혹했습니다. 옹호하지 apply않았습니다. 사용 사례.
astro123
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.