web-dev-qa-db-fra.com

Performances de Pandas appliquent vs np.vectorize pour créer une nouvelle colonne à partir de colonnes existantes

J'utilise Pandas dataframes et je veux créer une nouvelle colonne en fonction des colonnes existantes. Je n'ai pas vu une bonne discussion sur la différence de vitesse entre df.apply() et np.vectorize(), alors j'ai pensé poser la question ici.

La fonction Pandas apply()] est lente. D'après ce que j'ai mesuré (illustré ci-dessous dans certaines expériences), utiliser np.vectorize() est 25 fois plus rapide (ou plus) que Utilisation de la fonction DataFrame apply(), au moins sur mon MacBook Pro 2016 S'agit-il d'un résultat attendu et pourquoi?

Par exemple, supposons que j'ai le dataframe suivant avec N rows:

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

Supposons en outre que je veuille créer une nouvelle colonne en fonction des deux colonnes A et B. Dans l'exemple ci-dessous, j'utiliserai une fonction simple divide(). Pour appliquer la fonction, je peux utiliser soit df.apply() ou 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

Si j'augmente N aux tailles réelles, telles que 1 million ou plus, j'observe que np.vectorize() est 25 fois plus rapide ou plus que df.apply().

Vous trouverez ci-dessous un code de référence complet:

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']))

Les résultats sont montrés plus bas:

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

Si np.vectorize() est en général toujours plus rapide que df.apply(), alors pourquoi np.vectorize() n'est-il pas mentionné davantage? Je ne vois jamais que les publications StackOverflow liées à df.apply(), telles que:

Les pandas créent une nouvelle colonne basée sur les valeurs des autres colonnes

Comment utiliser la fonction Pandas 'apply' sur plusieurs colonnes?

Comment appliquer une fonction à deux colonnes de Pandas dataframe

32

Je vais commencer en disant que la puissance des tableaux Pandas et NumPy est dérivée de la haute performance vectorisée calculs sur des tableaux numériques.1 Le but des calculs vectorisés est d'éviter les boucles au niveau Python en déplaçant les calculs vers un code C hautement optimisé et en utilisant des blocs de mémoire contigus.2

Boucles de niveau Python

Maintenant, nous pouvons regarder quelques timings. Vous trouverez ci-dessous toutes les boucles de niveau Python qui produisent des objets pd.Series, np.ndarray Ou list contenant les mêmes valeurs. Aux fins de l'affectation à une série dans une base de données, les résultats sont comparables.

# 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

Quelques plats à emporter:

  1. Les méthodes basées sur Tuple (les 4 premières) sont un facteur plus efficace que les méthodes basées sur pd.Series (Les 3 dernières).
  2. np.vectorize, La compréhension de la liste + Zip et map méthodes, c’est-à-dire les 3 meilleurs, ont à peu près les mêmes performances. C'est parce qu'ils utilisent Tuple et en contournent certains Pandas overhead de pd.DataFrame.itertuples.
  3. L'utilisation de raw=True Avec pd.DataFrame.apply Par rapport à sans amélioration est très rapide. Cette option alimente les tableaux NumPy avec la fonction personnalisée à la place des objets pd.Series.

pd.DataFrame.apply: Juste une autre boucle

Pour voir exactement les objets Pandas circule, vous pouvez modifier votre fonction de manière triviale:

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

Sortie: <class 'pandas.core.series.Series'>. Créer, transmettre et interroger un objet de la série Pandas entraîne des frais généraux importants par rapport aux tableaux NumPy. Cela ne devrait pas vous surprendre: Pandas incluent une quantité décente d'échafaudages contenir un index, des valeurs, des attributs, etc.

Répétez le même exercice avec raw=True Et vous verrez <class 'numpy.ndarray'>. Tout cela est décrit dans la documentation, mais le voir est plus convaincant.

np.vectorize: Fausse vectorisation

La documentation pour np.vectorize a la note suivante:

La fonction vectorisée évalue pyfunc sur des nuplets successifs des tableaux d'entrée tels que la fonction python map), sauf qu'elle utilise les règles de diffusion de numpy.

Les "règles de diffusion" sont sans importance ici, car les tableaux d'entrée ont les mêmes dimensions. Le parallèle avec map est instructif, car la version map ci-dessus a des performances presque identiques. Le code source montre ce qui se passe: np.vectorize Convertit votre fonction d'entrée en un fonction universelle ("ufunc") via np.frompyfunc . Il y a une optimisation, par exemple. la mise en cache, ce qui peut conduire à une amélioration des performances.

En bref, np.vectorize Fait ce qu’une boucle de niveau Python devrait faire, mais pd.DataFrame.apply Ajoute une surcharge épaisse. Il n'y a pas de compilation JIT que vous voyez avec numba (voir ci-dessous). C'est juste une commodité .

Véritable vectorisation: ce que vous devriez utilisez

Pourquoi les différences ci-dessus ne sont-elles mentionnées nulle part? Parce que les performances des calculs réellement vectorisés les rendent non pertinents:

%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

Oui, c'est environ 40 fois plus rapide que la plus rapide des solutions ci-dessus. Les deux sont acceptables. À mon avis, le premier est succinct, lisible et efficace. Ne regardez que les autres méthodes, par exemple numba ci-dessous, si les performances sont critiques et que cela fait partie de votre goulot d'étranglement.

numba.njit: Une plus grande efficacité

Lorsque les boucles are sont considérées comme viables, elles sont généralement optimisées via numba avec les tableaux NumPy sous-jacents de manière à ce que le maximum possible soit déplacé vers C.

En effet, numba améliore les performances de microsecondes. Sans travail fastidieux, il sera difficile d’être beaucoup plus efficace que cela.

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

Utiliser @njit(parallel=True) peut donner un nouvel élan aux tableaux de grande taille.


1 Les types numériques incluent: int, float, datetime, bool, category. Ils excludeobject dtype et peuvent être conservés dans des blocs de mémoire contigus.

2 Il existe au moins deux raisons pour lesquelles les opérations NumPy sont efficaces par rapport à Python:

  • Tout ce qui est Python est un objet. Cela inclut, contrairement au C, des nombres. Python ont donc une surcharge qui n’existe pas avec les types C natifs.
  • Les méthodes NumPy sont généralement basées sur le langage C. De plus, des algorithmes optimisés sont utilisés dans la mesure du possible.
45
jpp

Plus vos fonctions sont complexes (c'est-à-dire que moins numpy peut se déplacer vers ses propres internes), plus vous verrez que la performance ne sera pas si différente. Par exemple:

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)

Faire des timings:

tiliser Apply

%timeit name_series.apply(parse_name)

Résultats:

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

En utilisant np.vectorize

%timeit parse_name_vec(name_series)

Résultats:

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

Numpy essaie de transformer python fonctions en numpy ufunc objets lorsque vous appelez np.vectorize. Comment cela se fait-il, je ne le sais pas vraiment - il faudrait creuser davantage dans les éléments internes de numpy que ce que je suis disposé à utiliser dans un guichet automatique. Cela dit, il semble faire ici un meilleur travail sur des fonctions simplement numériques que cette fonction basée sur des chaînes.

Cranking la taille jusqu'à 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)

Résultats:

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

np.vectorize

%timeit parse_name_vec(name_series)

Résultats:

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

Un meilleur moyen ( vectorisé ) avec 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()

Horaires:

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

Résultats:

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