Pandas의 성능은 기존 열에서 새 열을 만들기 위해 np.vectorize와 비교하여 적용됩니다.


81

Pandas 데이터 프레임을 사용하고 있으며 기존 열의 함수로 새 열을 만들고 싶습니다. 나는 속도 차이에 대한 좋은 토론을 보지 못했습니다.df.apply()np.vectorize()내가 여기 물어 것이라고 생각 때문에.

Pandas apply()기능이 느립니다. 내가 측정 한 것 (일부 실험에서 아래에 표시됨)에서 np.vectorize()사용하면 apply()적어도 2016 MacBook Pro에서 DataFrame 기능을 사용하는 것보다 25 배 더 빠릅니다 (또는 그 이상) . 이것은 예상 된 결과이며 그 이유는 무엇입니까?

예를 들어 N행 이있는 다음 데이터 프레임이 있다고 가정 합니다.

N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
#     A   B
# 0  78  50
# 1  23  91
# 2  55  62
# 3  82  64
# 4  99  80

추가로 두 개의 열 A및 의 함수로 새 열을 생성한다고 가정 B합니다. 아래 예에서는 간단한 함수를 사용합니다 divide(). 기능을 적용하려면 df.apply()또는 np.vectorize()다음 중 하나를 사용할 수 있습니다 .

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)

df['result2'] = np.vectorize(divide)(df['A'], df['B'])

df.head()
#     A   B    result   result2
# 0  78  50  1.560000  1.560000
# 1  23  91  0.252747  0.252747
# 2  55  62  0.887097  0.887097
# 3  82  64  1.281250  1.281250
# 4  99  80  1.237500  1.237500

내가 늘리면 N100 만 이상의처럼 실제 크기에, 나는 그 관찰 np.vectorize()25 배 빠른 이상보다 df.apply().

다음은 완전한 벤치마킹 코드입니다.

import pandas as pd
import numpy as np
import time

def divide(a, b):
    if b == 0:
        return 0.0
    return float(a)/b

for N in [1000, 10000, 100000, 1000000, 10000000]:    

    print ''
    A_list = np.random.randint(1, 100, N)
    B_list = np.random.randint(1, 100, N)
    df = pd.DataFrame({'A': A_list, 'B': B_list})

    start_epoch_sec = int(time.time())
    df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
    end_epoch_sec = int(time.time())
    result_apply = end_epoch_sec - start_epoch_sec

    start_epoch_sec = int(time.time())
    df['result2'] = np.vectorize(divide)(df['A'], df['B'])
    end_epoch_sec = int(time.time())
    result_vectorize = end_epoch_sec - start_epoch_sec


    print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
            (N, result_apply, result_vectorize)

    # Make sure results from df.apply and np.vectorize match.
    assert(df['result'].equals(df['result2']))

결과는 다음과 같습니다.

N=1000, df.apply: 0 sec, np.vectorize: 0 sec

N=10000, df.apply: 1 sec, np.vectorize: 0 sec

N=100000, df.apply: 2 sec, np.vectorize: 0 sec

N=1000000, df.apply: 24 sec, np.vectorize: 1 sec

N=10000000, df.apply: 262 sec, np.vectorize: 4 sec

경우는 np.vectorize()항상 속도보다 일반적이다 df.apply(), 왜되어 np.vectorize()더 언급하지? 다음 df.apply()과 같은 관련 StackOverflow 게시물 만 볼 수 있습니다.

팬더는 다른 열의 값을 기반으로 새 열을 만듭니다.

Pandas '적용'기능을 여러 열에 어떻게 사용합니까?

Pandas 데이터 프레임의 두 열에 함수를 적용하는 방법


나는 당신의 질문에 대한 세부 사항을 파헤 np.vectorizeforapply
치지 않았지만

"np.vectorize ()가 일반적으로 항상 df.apply ()보다 빠르다면 np.vectorize ()가 더 이상 언급되지 않는 이유는 무엇입니까?" 필요한 apply경우가 아니면 행 단위 로 사용해서는 안되며 , 분명히 벡터화 된 함수는 벡터화되지 않은 함수보다 성능이 뛰어납니다.
PMende

1
@PMende이지만 np.vectorize벡터화되지 않았습니다. 잘 알려진 잘못된 이름입니다
roganjosh 2010 년

1
@PMende, 물론, 나는 달리 암시하지 않았습니다. 타이밍에서 구현에 대한 의견을 도출해서는 안됩니다. 예, 그들은 통찰력이 있습니다. 그러나 그들은 사실이 아닌 것을 추측하게 만들 수 있습니다.
jpp

3
@PMende는 pandas .str접근 자 와 놀아요 . 많은 경우에 목록 이해력보다 느립니다. 우리는 너무 많이 가정합니다.
roganjosh

답변:


115

먼저 Pandas 및 NumPy 배열의 힘이 숫자 배열에 대한 고성능 벡터화 된 계산에서 파생되었다고 말씀 드리겠습니다 . 1 벡터화 된 계산의 전체 요점은 계산을 고도로 최적화 된 C 코드로 이동하고 연속적인 메모리 블록을 활용하여 Python 수준의 루프를 방지하는 것입니다. 2

Python 수준 루프

이제 몇 가지 타이밍을 볼 수 있습니다. 아래는 모든 파이썬 수준 중 하나를 생산 루프 pd.Series, np.ndarray또는 list같은 값을 포함하는 객체. 데이터 프레임 내에서 시리즈에 할당 할 목적으로 결과는 비슷합니다.

# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0

np.random.seed(0)
N = 10**5

%timeit list(map(divide, df['A'], df['B']))                                   # 43.9 ms
%timeit np.vectorize(divide)(df['A'], df['B'])                                # 48.1 ms
%timeit [divide(a, b) for a, b in zip(df['A'], df['B'])]                      # 49.4 ms
%timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)]     # 112 ms
%timeit df.apply(lambda row: divide(*row), axis=1, raw=True)                  # 760 ms
%timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1)              # 4.83 s
%timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()]  # 11.6 s

몇 가지 요점 :

  1. tuple기반 방법 (제 4) 이외의 요소가 더 효율적 pd.Series기반 방법 (마지막 3).
  2. np.vectorize, 목록 이해력 + zipmap방법, 즉 상위 3 개는 모두 거의 동일한 성능을 갖습니다. 이는 .NET에서 일부 Pandas 오버 헤드를 사용 tuple 하고 우회 하기 때문 입니다 pd.DataFrame.itertuples.
  3. 사용 raw=True하는 경우 pd.DataFrame.apply와 사용하지 않는 경우 속도가 크게 향상됩니다 . 이 옵션은 NumPy 배열을 pd.Series개체 대신 사용자 지정 함수에 제공 합니다.

pd.DataFrame.apply: 또 다른 루프

Pandas가 전달하는 객체를 정확하게 보려면 함수를 간단하게 수정할 수 있습니다.

def foo(row):
    print(type(row))
    assert False  # because you only need to see this once
df.apply(lambda row: foo(row), axis=1)

출력 : <class 'pandas.core.series.Series'>. Pandas 시리즈 객체를 생성, 전달 및 쿼리하면 NumPy 배열에 비해 상당한 오버 헤드가 발생합니다. 이것은 놀라운 일이 아닙니다. Pandas 시리즈에는 색인, 값, 속성 등을 보유하기위한 적절한 양의 스캐 폴딩이 포함되어 있습니다.

와 같은 운동을 다시 raw=True하면 <class 'numpy.ndarray'>. 이 모든 것은 문서에 설명되어 있지만 더 설득력이 있습니다.

np.vectorize: 가짜 벡터화

에 대한 문서 np.vectorize에는 다음과 같은 메모가 있습니다.

벡터화 된 함수 pyfunc는 numpy의 브로드 캐스팅 규칙을 사용하는 것을 제외하고는 python map 함수와 같은 입력 배열의 연속적인 튜플에 대해 평가 합니다.

여기서 "방송 규칙"은 입력 배열이 동일한 차원을 갖기 때문에 관련이 없습니다. 위 mapmap버전이 거의 동일한 성능을 갖기 때문에 병렬 은 유익 합니다. 소스 코드 무슨 일이 일어나고 있는지 쇼 : np.vectorize로 입력 기능을 변환 범용 기능 을 통해 ( "ufunc") np.frompyfunc. 성능 향상으로 이어질 수있는 캐싱과 같은 최적화가 있습니다.

요컨대, np.vectorize파이썬 수준의 루프 가해야 할 일 수행하지만 pd.DataFrame.apply덩어리 오버 헤드를 추가합니다. 표시되는 JIT 컴파일이 없습니다 numba(아래 참조). 그것은 단지 편의 입니다.

진정한 벡터화 : 사용해야 하는 것

위의 차이점이 어디에도 언급되지 않은 이유는 무엇입니까? 진정으로 벡터화 된 계산의 성능으로 인해 관련성이 없기 때문입니다.

%timeit np.where(df['B'] == 0, 0, df['A'] / df['B'])       # 1.17 ms
%timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0)  # 1.96 ms

예, 위의 루프 솔루션 중 가장 빠른 것보다 40 배 더 빠릅니다. 둘 중 하나가 허용됩니다. 제 생각에 첫 번째는 간결하고 읽기 쉽고 효율적입니다. numba성능이 중요하고 이것이 병목 현상의 일부인 경우에만 아래의 다른 방법을 살펴보십시오 .

numba.njit: 더 큰 효율성

루프 실행 가능한 것으로 간주 되면 일반적으로 다음을 통해 최적화됩니다.numba 으로 기본 NumPy 배열을 되어 가능한 한 C로 이동합니다.

실제로 numba성능을 마이크로 초로 향상시킵니다 . 번거로운 작업 없이는 이것보다 훨씬 더 효율적으로되기 어려울 것입니다.

from numba import njit

@njit
def divide(a, b):
    res = np.empty(a.shape)
    for i in range(len(a)):
        if b[i] != 0:
            res[i] = a[i] / b[i]
        else:
            res[i] = 0
    return res

%timeit divide(df['A'].values, df['B'].values)  # 717 µs

사용하면 @njit(parallel=True)더 큰 어레이에 대한 추가 향상을 제공 할 수 있습니다.


1 개 숫자 유형은 다음과 같습니다 : int, float, datetime, bool, category. 그들은 제외 object DTYPE 및 연속 메모리 블록에서 유지 될 수있다.

2 NumPy 작업이 Python에 비해 효율적인 이유는 최소한 두 가지입니다.

  • 파이썬의 모든 것은 객체입니다. 여기에는 C와 달리 숫자가 포함됩니다. 따라서 Python 유형에는 네이티브 C 유형에는 존재하지 않는 오버 헤드가 있습니다.
  • NumPy 메서드는 일반적으로 C 기반입니다. 또한 가능한 경우 최적화 된 알고리즘이 사용됩니다.

1
@jpp :로 장식을 사용하여 parallel인수하는 @njit(parallel=True)이상 단지 나에게 더 향상을 제공합니다 @njit. 아마도 당신도 그것을 추가 할 수 있습니다.
Sheldore

1
b [i]! = 0을 다시 확인했습니다. 일반적인 Python 및 Numba 동작은 0을 확인하고 오류를 던지는 것입니다. 이로 인해 SIMD 벡터화가 중단 될 수 있으며 일반적으로 실행 속도에 큰 영향을 미칩니다. 그러나 Numba 내에서 @njit (error_model = 'numpy')로 변경하여 0으로 나누는 이중 검사를 방지 할 수 있습니다. 또한 np.empty로 메모리를 할당하고 else 문 내에서 결과를 0으로 설정하는 것이 좋습니다.
max9111

1
error_model numpy는 프로세서가 0-> NaN으로 나누는 것을 사용합니다. 적어도 Numba 0.41dev에서는 두 버전 모두 SIMD 벡터화를 사용합니다. 여기에 설명 된대로 확인할 수 있습니다 . numba.pydata.org/numba-doc/dev/user/faq.html (1.16.2.3. 내 루프가 벡터화되지 않은 이유는 무엇입니까?) 함수에 else 문을 추가하기 만하면됩니다 (res [ i] = 0.) np.empty로 메모리를 할당합니다. 이를 error_model = 'numpy'와 함께 사용하면 성능이 약 20 % 향상됩니다. 이전 Numba 버전에서 성능에 높은 영향을 미치는 약물이 있었다
max9111

2
@ stackoverflowuser2010, "임의의 기능에 대한"보편적 인 대답은 없습니다. 프로그래밍 / 알고리즘 이해의 일부인 올바른 작업에 적합한 도구를 선택해야합니다.
jpp

1
즐거운 휴일 보내세요!
cs95

5

함수가 복잡해질수록 (즉, numpy내부로 이동할 수 있는 것이 적을수록 ) 성능이 크게 다르지 않다는 것을 더 많이 알 수 있습니다. 예를 들면 :

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))

def parse_name(name):
    if name.lower().startswith('a'):
        return 'A'
    elif name.lower().startswith('e'):
        return 'E'
    elif name.lower().startswith('i'):
        return 'I'
    elif name.lower().startswith('o'):
        return 'O'
    elif name.lower().startswith('u'):
        return 'U'
    return name

parse_name_vec = np.vectorize(parse_name)

몇 가지 타이밍 수행 :

적용 사용

%timeit name_series.apply(parse_name)

결과 :

76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

사용 np.vectorize

%timeit parse_name_vec(name_series)

결과 :

77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Numpy는 파이썬 함수를 numpy로 바꾸려고합니다. ufunc 호출 할 때 객체 합니다 np.vectorize. 이것이 어떻게 작동하는지, 나는 실제로 알지 못합니다. ATM을 사용하는 것보다 numpy의 내부를 더 많이 파헤쳐 야합니다. 즉, 여기에서이 문자열 기반 함수보다 단순히 숫자 함수에서 더 나은 작업을 수행하는 것 같습니다.

크기를 최대 1,000,000까지 크 랭킹 :

name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))

apply

%timeit name_series.apply(parse_name)

결과 :

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

결과 :

794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

더 나은 ( 벡터화 ) 방법 np.select:

cases = [
    name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
    name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
    name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()

타이밍 :

%timeit np.select(cases, replacements, default=name_series)

결과 :

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

최대 size=1000000(1 백만) 크랭크하면 어떨까요?
stackoverflowuser2010

2
여기에있는 당신의 주장이 틀렸다고 확신합니다. 내가 할 수없는 백업하는 것이 지금은 코드 문까지, 잘하면 다른 사람 캔
roganjosh

@ stackoverflowuser2010 실제 벡터화 접근 방식 과 함께 업데이트했습니다 .
PMende

0

저는 파이썬을 처음 사용합니다. 그러나 아래 예제에서 '적용'은 '벡터화'보다 빠르게 작동하는 것 같거나 뭔가 빠졌습니다.

 import numpy as np
 import pandas as pd

 B = np.random.rand(1000,1000)
 fn = np.vectorize(lambda l: 1/(1-np.exp(-l)))
 print(fn(B))

 B = pd.DataFrame(np.random.rand(1000,1000))
 fn = lambda l: 1/(1-np.exp(-l))
 print(B.apply(fn))
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.