Mesures de vitesse sur les dataframes

Le notebook montre comment lire un DataFrame avec un itérateur quand on ne connaît pas sa taille, ou lire un array avec un itérateur.

Création d’un dataframe à partir d’un itérateur

On cherche à créer un dataframe à partir d’un ensemble de lignes dont on ne connaît pas le nombre au moment où on créé le dataframe car on les reçoit sous la forme d’un itérateur ou un générateur.

[1]:
import random


def enumerate_row(nb=10000, n=10):
    for i in range(nb):
        # on retourne un tuple, les données sont
        # plus souvent recopiées car le type est immuable
        yield tuple(random.random() for k in range(n))
        # on retourne une liste, ces listes ne sont pas
        # recopiées en général, seule la liste qui les tient
        # l'est
        # yield list(random.random() for k in range(n))


list(enumerate_row(2))
[1]:
[(0.4584214264768637,
  0.0957370472492135,
  0.825720254865909,
  0.056222146826998554,
  0.012568801665460705,
  0.20797581971445256,
  0.6508447830614892,
  0.817974554103244,
  0.04182207570159391,
  0.591375261282058),
 (0.5818213564160107,
  0.3384435930913253,
  0.5900215149482624,
  0.9556893663618211,
  0.9156247392985197,
  0.20153581804870713,
  0.893987513368823,
  0.11112779556835362,
  0.043959856261986174,
  0.233344273733338)]
[2]:
import pandas

nb, n = 10, 10
df = pandas.DataFrame(enumerate_row(nb=nb, n=n), columns=["c%d" % i for i in range(n)])
df.head()
[2]:
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9
0 0.155969 0.431193 0.995451 0.081467 0.257834 0.457617 0.773857 0.843436 0.842255 0.570137
1 0.876386 0.702447 0.130592 0.084160 0.782795 0.065442 0.682476 0.077565 0.444916 0.025166
2 0.854808 0.873240 0.055319 0.518709 0.486142 0.034237 0.979128 0.997898 0.472220 0.512437
3 0.476952 0.250016 0.964843 0.579930 0.693238 0.103160 0.249000 0.850935 0.632083 0.738248
4 0.773502 0.237446 0.974755 0.564504 0.684763 0.361164 0.152243 0.320242 0.218529 0.411604
[3]:
nb, n = 100000, 10

On compare plusieurs constructions :

[4]:
print(nb, n)
%timeit pandas.DataFrame(enumerate_row(nb=nb,n=n), columns=["c%d" % i for i in range(n)])
100000 10
230 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
[5]:
print(nb, n)
%timeit pandas.DataFrame(list(enumerate_row(nb=nb,n=n)), columns=["c%d" % i for i in range(n)])
100000 10
225 ms ± 8.84 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

On décompose :

[8]:
def cache():
    return list(enumerate_row(nb=nb, n=n))


print(nb, n)
%timeit -n 3 cache()
100000 10
145 ms ± 18.6 ms per loop (mean ± std. dev. of 7 runs, 3 loops each)
[7]:
print(nb, n)
l = list(enumerate_row(nb=nb, n=n))
%timeit -n 3 pandas.DataFrame(l, columns=["c%d" % i for i in range(n)])
100000 10
87.7 ms ± 2.24 ms per loop (mean ± std. dev. of 7 runs, 3 loops each)

D’après ces temps, pandas convertit probablement l’itérateur en liste. On essaye de créer le dataframe vide, puis avec la méthode from_records.

[10]:
%timeit -n 3 pandas.DataFrame(columns=["c%d" % i for i in range(n)], index=range(n))
1.94 ms ± 540 µs per loop (mean ± std. dev. of 7 runs, 3 loops each)
[11]:
def create_df3():
    return pandas.DataFrame.from_records(
        enumerate_row(nb=nb, n=n), columns=["c%d" % i for i in range(n)]
    )


print(nb, n)
%timeit create_df3()
100000 10
224 ms ± 4.46 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Création d’un array à partir d’un itérateur

On cherche à créer un dataframe à partir d’un ensemble de lignes dont on ne connaît pas le nombre au moment où on créé le dataframe car on les reçoit sous la forme d’un itérateur ou un générateur. La documentation de la fonction numpy.fromiter est intéressante à ce sujet.

[12]:
def enumerate_row2(nb=10000, n=10):
    for i in range(nb):
        for k in range(n):
            yield random.random()


import numpy

nb, n = 100000, 10
# on précise la taille du tableau car cela évite à numpy d'agrandir le tableau
# au fur et à mesure, ceci ne fonctionne pas
print(nb, n)
m = numpy.fromiter(enumerate_row2(nb=nb, n=n), float, nb * n)
m.resize((nb, n))
m[:5, :]
100000 10
[12]:
array([[0.18094164, 0.98726051, 0.15154422, 0.02532254, 0.13567288,
        0.52949799, 0.9955031 , 0.56441516, 0.95278832, 0.37068437],
       [0.97776124, 0.1088838 , 0.72051064, 0.79808152, 0.25334263,
        0.04203916, 0.8290536 , 0.32045666, 0.48908504, 0.70058525],
       [0.03562189, 0.45141838, 0.98266729, 0.36282507, 0.74903618,
        0.36675298, 0.30681627, 0.86053065, 0.36733881, 0.03716365],
       [0.8255547 , 0.31025914, 0.61405287, 0.2289358 , 0.87746991,
        0.98780181, 0.99195587, 0.6592586 , 0.90237022, 0.73119145],
       [0.79096242, 0.72046597, 0.87479709, 0.75549334, 0.2525281 ,
        0.91680528, 0.97679278, 0.92947194, 0.2344261 , 0.67808894]])
[13]:
def create_array():
    m = numpy.fromiter(enumerate_row2(nb=nb, n=n), float, nb * n)
    m.resize((nb, n))
    return m


print(nb, n)
%timeit create_array()
100000 10
106 ms ± 7.24 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
[14]:
def create_array2():
    m = list(enumerate_row(nb=nb, n=n))
    ml = numpy.array(m, float)
    return ml


print(nb, n)
%timeit create_array2()
100000 10
175 ms ± 5.22 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Et si on ne précise pas la taille du tableau créé avec la fonction fromiter :

[16]:
def create_array3():
    m = numpy.fromiter(enumerate_row2(nb=nb, n=n), float)
    m.resize((nb, n))
    return m


print(nb, n)
%timeit -n 3 create_array3()
100000 10
110 ms ± 7.48 ms per loop (mean ± std. dev. of 7 runs, 3 loops each)

On retrouve des temps similaires que ceux obtenus avec une liste. En conclusion, pour créer un array, il vaut mieux :

  • connaître la taille finale

  • éviter de créer une liste

Pour finir, je recommande la lecture de Enhancing Performance qui étudie différent scénari avec cython, eval, numba.


Notebook on github