Versions¶

In [1]:
#numpy
import numpy
numpy.__version__
Out[1]:
'2.2.2'
In [2]:
#pandas
import pandas
pandas.__version__
Out[2]:
'2.2.3'

Générer les données¶

In [3]:
#initaliser le générateur
rng = numpy.random.default_rng(seed=1)
In [4]:
#générer une matrice de dim (n, p)
n = 1000
p = 50
mat = rng.random(size=(n,p))

#vérif.
print(mat.shape)
(1000, 50)
In [5]:
#transformer en data frame pandas
import pandas
df = pandas.DataFrame(mat,columns=["V"+str(i+1) for i in range(p)])

print(df.shape)
print(df.columns)
(1000, 50)
Index(['V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10', 'V11',
       'V12', 'V13', 'V14', 'V15', 'V16', 'V17', 'V18', 'V19', 'V20', 'V21',
       'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28', 'V29', 'V30', 'V31',
       'V32', 'V33', 'V34', 'V35', 'V36', 'V37', 'V38', 'V39', 'V40', 'V41',
       'V42', 'V43', 'V44', 'V45', 'V46', 'V47', 'V48', 'V49', 'V50'],
      dtype='object')

Fonction de transformation¶

In [6]:
#transformation
#remplacer la valeur par le quartile le plus proche
#x est une Serie d'un data frame
def ftrans(x):
    #calculer les quartiles
    q = x.quantile(q=[0.25,0.5,0.75])
    #remplacer la valeur par le quartile le plus proche
    res = x.apply(lambda v:q.iloc[((v-q)**2).argmin()])
    #renvoyer la serie
    return res

Application séquentielle¶

In [7]:
#boucle
def fun_boucle(D):
    #liste vide
    lst = []
    #boucler sur les colonnes
    for x in D.columns:
        lst.append(ftrans(D[x]))
    #transformer en data frame
    res_df = pandas.DataFrame(lst).transpose()
    return res_df
In [8]:
%%time

#appel
res_boucle = fun_boucle(df)

#dimension
res_boucle.shape
CPU times: total: 6.52 s
Wall time: 6.91 s
Out[8]:
(1000, 50)
In [9]:
#moyennes par colonne - juste pour contrôle
avg_boucle = res_boucle.mean(axis=0)
print(avg_boucle[:5])
V1    0.517097
V2    0.502991
V3    0.486669
V4    0.527154
V5    0.515145
dtype: float64

Utilisation de "apply" de Pandas¶

In [10]:
#utilisation de apply de Pandas
#très proche dans son expression de celui de R
def fun_apply(D):
    #appel avec passage de ftrans en callback
    #par défaut : axis = 0, calcul par colonne
    res = D.apply(func=ftrans)
    #renvoyer direct - déjà un data frame
    return res
In [11]:
%%time

#appel - en réalité : plus pratique mais pas plus performant qu'une boucle
res_apply = fun_apply(df)

#dim.
print(res_apply.shape)
(1000, 50)
CPU times: total: 6.47 s
Wall time: 6.85 s
In [12]:
#vérif. -- ok
numpy.sum((avg_boucle-res_apply.mean(axis=0))**2)
Out[12]:
np.float64(7.157371970925172e-28)
In [13]:
# /!\ REMARQUE
# on peut utiliser un "engine" numba
# cf. https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.apply.html
# cf. https://www.youtube.com/watch?v=BkJoHz0NGlE
# mais bien sûr il faut installer le package
# il faut aussi redéfinir ftrans() avec les directives adéquates

Parallélisation avec "Modin"¶

In [14]:
#choix du "moteur" (engine) - Dask
#qui a été installé dans l'environnement
import os
os.environ["MODIN_ENGINE"] = "dask"
In [15]:
# importation du package
# il faut avoir installé "dask" - https://www.youtube.com/watch?v=qOSFKtJPASg
# /!\ il faut aussi installer manuellement le package "distributed"
import modin.pandas as pdm

#fonction avec apply de modin
def fun_modin(D):
    #appel avec passage de ftrans en callback
    #cast du data frame pandas en df modin
    res = pdm.DataFrame(D).apply(ftrans)
    #renvoyer direct - déjà un data frame
    return res
In [17]:
%%time

#appel
res_modin = fun_modin(df)

#dim.
res_modin.shape
CPU times: total: 1.09 s
Wall time: 5.53 s
Out[17]:
(1000, 50)
In [18]:
#vérification
numpy.sum((avg_boucle-res_modin.mean(axis=0))**2)
UserWarning: __array_ufunc__ is not currently supported by PandasOnDask, defaulting to pandas implementation.
Please refer to https://modin.readthedocs.io/en/stable/supported_apis/defaulting_to_pandas.html for explanation.
Out[18]:
np.float64(7.157926638749156e-28)
In [19]:
# dask a aussi sa propre version des data frame
# cf. https://www.youtube.com/watch?v=yDHzpSobUro
# mais il ne sait utiliser apply qu'avec axis = 1
# il faudrait réaliser une transposition 2 fois
# pas probant par rapport à "modin"

Parallélisation avec "pandarallel"¶

In [20]:
from pandarallel import pandarallel

#fonction avec apply
def fun_pandarallel(D):
    #initialisation - spécifier le nb de "workers"
    #pas verbose
    pandarallel.initialize(nb_workers=5, verbose=0)
    #appel avec passage de ftrans en callback
    res = D.parallel_apply(ftrans)
    #renvoyer direct - déjà un data frame
    return res
In [21]:
%%time

#appel
res_pdrl = fun_pandarallel(df)

#dim.
res_pdrl.shape
CPU times: total: 375 ms
Wall time: 3.81 s
Out[21]:
(1000, 50)
In [22]:
#vérification
numpy.sum((avg_boucle-res_pdrl.mean(axis=0))**2)
Out[22]:
np.float64(7.157371970925172e-28)

Comparaison des temps de traitement¶

In [23]:
#apply de pandas
%timeit -r 10 -n 1 fun_apply(df)
8.87 s ± 194 ms per loop (mean ± std. dev. of 10 runs, 1 loop each)
In [24]:
#apply de modin
%timeit -r 10 -n 1 fun_modin(df)
5.34 s ± 128 ms per loop (mean ± std. dev. of 10 runs, 1 loop each)
In [25]:
#apply de pandarallel
%timeit -r 10 -n 1 fun_pandarallel(df)
3.9 s ± 72.8 ms per loop (mean ± std. dev. of 10 runs, 1 loop each)