[PYTHON] C'est pourquoi j'ai quitté pandas [Trois façons de groupby.mean () avec juste NumPy]

C'est pourquoi j'ai quitté pandas [Trois façons de groupby.mean () avec juste NumPy]

Quel est cet article ludique

Les gens qui disent que les pandas sont déjà en désordre sur les PC à faible spécification (Tohoho) gèrent probablement des données dans un désordre avec NumPy. Cependant, NumPy n'a pas de fonction de type pd.DataFrame.groupby, donc certaines personnes peuvent être dérangées par des pandas en pleurs. Mais si vous êtes stupide et que vous ne pouvez pas faire ce que les pandas peuvent faire avec NumPy, faites de votre mieux pour retourner la documentation NumPy et regarder les différentes fonctions (mais les boucles for sont NG). Oui, vous pouvez.

Pour ceux qui ne savent pas quoi penser, un sage du passé a dit: "Comment agréger par groupe uniquement avec Numpy et calculer la moyenne (pour la phrase, pas de Pandas) / numpy-grouping-no-pandas /) »(Dieu). Cependant, à l'ère actuelle où la science et la technologie se sont développées, j'ai senti qu'il pourrait y avoir une meilleure façon, alors j'ai pensé à diverses choses tout en étudiant les fonctions de Numpy et j'ai écrit une note écrite en japonais qui ne relie pas le récit de la bataille. Cet article est consacré à.

Choses à faire

Cette fois, je veux trouver la valeur moyenne et l'écart type pour chaque groupe, c'est-à-dire pour calculer pd.DataFrame.groupby.mean ()pd.DataFrame.groupby.std () uniquement avec NumPy.

Données à utiliser

Données d'iris familières. Les lignes sont mélangées.

import numpy as np
import pandas as pd

df = (pd.read_csv('https://raw.githubusercontent.com/pandas-dev/pandas/'
                  'master/pandas/tests/data/iris.csv')
      .sample(frac=1, random_state=1))

df.head()
SepalLength SepalWidth PetalLength PetalWidth Name
14 5.8 4 1.2 0.2 Iris-setosa
98 5.1 2.5 3 1.1 Iris-versicolor
75 6.6 3 4.4 1.4 Iris-versicolor
16 5.4 3.9 1.3 0.4 Iris-setosa
131 7.9 3.8 6.4 2 Iris-virginica

Une note avant de convertir en un objet Numpy. Afin de gérer efficacement les données avec NumPy, il est essentiel de convertir des données autres que de type numérique (chaîne de caractères, type d'objet, etc.) en type numérique. Si vous souhaitez manipuler des chaînes, il est préférable d'utiliser une liste standard. La colonne "Nom" ne représente cette fois que la catégorie, et la chaîne de caractères n'a aucune signification dans les données, ce que l'on appelle un codage d'étiquette est effectué pour la convertir en nombre. Ceci est possible avec np.unique (array, return_inverse = True).

unique_names, df['Name'] = np.unique(df['Name'].to_numpy(), return_inverse=True)
# (array(['Iris-setosa', 'Iris-versicolor', 'Iris-virginica'], dtype=object),
#  array([0, 1, 1, 0, 2, 1, 2, 0, 0, 2, 1, 0, 2, 1, 1, 0, 1, 1, 0, 0, 1, 1,
#         ...,
#         1, 0, 1, 1, 1, 1, 2, 0, 0, 2, 1, 2, 1, 2, 2, 1, 2, 0], dtype=int64))

#C'est également possible
df['Name'], unique_names = pd.factorize(df['Name'], sort=True)

<détails>

En dehors </ summary>

A ce moment, on sait que pd.factorize () est plus rapide que np.unique () quand il y a un certain nombre de données.

import perfplot

test_df = pd.DataFrame(['a'])
perfplot.show(
    setup=lambda n: pd.concat([test_df]*n,
                              ignore_index=True, axis=0).iloc[:, 0],
    n_range=[2 ** k for k in range(20)],
    kernels=[
        lambda series: np.unique(series.to_numpy(), return_inverse=True),
        lambda series: pd.factorize(series, sort=True),
    ],
    labels=["np.unique", "pd.factorize"],
    equality_check=None,
    xlabel="Nombre de lignes de données", logx=True, logy=True, time_unit="s", automatic_order=False
)

pd_to_np_1.png

Apparemment, vous devriez utiliser pd.factorize () lors de la conversion de Series (pas seulement ndarray) avec plus de 1000 lignes. </ div> </ détails>

Convertir en tableau NumPy (ici, le tableau d'enregistrement est utilisé pour faciliter l'explication). Le moment de rompre avec les pandas.

arr = df.to_records(index=False)

Comment grouper par

Partie 1 (onehot)

Tout d'abord, je présenterai la méthode de l'article ci-dessus.

One-hot vectoriser le type d'iris. Multipliez cela par le vecteur de longueur de pétale que vous souhaitez agréger et prenez la moyenne pour chaque colonne. Si le vecteur one-hot est moyenné tel quel, il sera affecté par 0, alors remplacez-le par NaN à l'avance.

Partie 1


onehot = np.eye(arr['Name'].max()+1)[arr['Name']]
onehot[onehot == 0] = np.nan
onehot_value = onehot * arr['PetalWidth'][:, None]

result_mean = np.nanmean(onehot_value, 0)
# [0.244 1.326 2.026]
result_std = np.nanstd(onehot_value, 0)
# [0.10613199 0.19576517 0.27188968]

Le problème avec cette méthode est que «np.nanmean ()» et «np.nanstd ()» sont connus pour être lents. La formule moyenne est

mean = \frac{1}{n} \sum_{i=1}^{N} x_i

Par conséquent, si vous le remplacez par une expression qui utilise np.nansum (), ce sera comme suit.

python


result_mean = np.nansum(onehot_value, 0)/(~np.isnan(onehot_value)).sum(0)
result_std = np.sqrt(np.nansum(onehot_value**2, 0)
                     / (~np.isnan(onehot_value)).sum(0) - result_mean**2)

De plus, np.nan dans ʻonehot_value remplace 0, mais comme le total ne change pas quel que soit le nombre de 0s ajoutés, ne le remplacez pas par np.nan. Vous pouvez utiliser np.sum ()` tel quel.

Le fait que vous n'ayez pas besoin d'assigner np.nan à ʻonehot signifie que vous n'avez pas à utiliser le type de données float, donc vous pouvez obtenir ʻonehot plus rapidement par la méthode suivante.

%timeit np.eye(arr['Name'].max()+1)[arr['Name']]
# 21.2 µs ± 919 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
# array([[1., 0., 0.],
#        [0., 1., 0.],
#        [0., 1., 0.],
#        ...,
#        [1., 0., 0.]])


%timeit np.arange(arr['Name'].max()+1) == arr['Name'][:, None]
# 20.1 µs ± 205 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
# array([[ True, False, False],
#        [False,  True, False],
#        [False,  True, False],
#        ...,
#        [ True, False, False]])

Donc,

Partie 1 pause


onehot = np.arange(arr['Name'].max()+1) == arr['Name'][:, None]
onehot_value = onehot * arr['PetalWidth'][:, None]
sums = onehot_value.sum(0)
counts = onehot.sum(0)

result_mean = sums/counts
result_std = np.sqrt((onehot_value**2).sum(0)/counts - result_mean**2)

Partie 2 (réduire)

Le but de groupby est de savoir comment calculer pour chaque groupe. Dans la partie 1, nous avons pu obtenir le même résultat que groupby.mean () en déplaçant les valeurs dans différentes colonnes pour chaque groupe et en les faisant la moyenne.

Partie 1 Façon de penser


g |  A  B  B  C  A  B  A  C  C
                 v
A |  1  0  0  0  1  0  1  0  0 |    mean_A
B |  0  1  1  0  0  1  0  0  0 | -> mean_B
C |  0  0  0  1  0  0  0  1  1 |    mean_C

Cependant, dans le tableau d'origine, si les éléments sont disposés en groupes, l'idée suivante peut être envisagée.

Partie 2 façon de penser


g |  A  A  A  B  B  B  C  C  C
    ----A--- ----B--- ----C---
                 v
     mean_A   mean_B   mean_C

Par conséquent, pour organiser d'abord par groupe, le tri est effectué en fonction de la colonne Nom.

sorter_index = np.argsort(arr['Name'])
name_s = arr['Name'][sorter_index]
# array([0, 0, 0, ..., 1, 1, 1, ..., 2, 2, 2], dtype=int64)
pwidth_s = arr['PetalWidth'][sorter_index]

Ensuite, la frontière du groupe est obtenue. Cela peut être fait en regardant chaque valeur adjacente dans la colonne Nom triée. Par exemple

ex_array = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3])
#                    0------  1------  2------  3------← Je veux la position de cette frontière

ex_array[1:] != ex_array[1:]
"""Façon de penser
 0 [0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]    # ex_array[1:]
   [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3] 3  # ex_array[:-1]
   [F, F, T, F, F, T, F, F, T, F, F]    # ex_array[1:] != ex_array[1:]
(Si les deux premiers correspondent, F,S'ils ne correspondent pas, T)

[0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]    # ex_array
[T, F, F, T, F, F, T, F, F, T, F, F, T] #Le tableau que je voulais vraiment
 0------  1------  2------  3------
# ex_array[1:] != ex_array[1:]Si vous ajoutez un Vrai avant et après,
"T-avant le prochain T" devient un groupe.
"""

#Version complémentaire avant et après
np.concatenate(([True], ex_array[1:] != ex_array[:-1], [True]))
# array([ True, False, False, False, False, False, False, False, False, False, False, False, True])

Si vous obtenez la position Vrai de ce tableau avec np.ndarray.nonzero (), ce sera la position de la limite de chaque groupe.

cut_index = np.concatenate(([True], ex_array[1:] != ex_array[:-1], [True])).nonzero()[0]
# array([ 0,  3,  6,  9, 12], dtype=int64)
"""Façon de penser
 0  1  2  3  4  5  6  7  8  9 10 11 12   # ex_index du tableau
[0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]     # ex_array
[0,       3,       6,       9,      12]  # cut_index:Limite (point de départ) de chaque groupe
"""

Encore une fois, la moyenne est "total ÷ nombre", alors trouvez chacun. Le nombre d'éléments dans un groupe est la différence entre les valeurs adjacentes de cut_index, vous pouvez donc utiliser np.diff () ʻet np.ediff1d ()` pour le trouver d'un seul coup.

counts = np.ediff1d(cut_index)
# array([3, 3, 3, 3], dtype=int64)

Ensuite, la somme des éléments du groupe est calculée. Au fait, il existe une ** fonction universelle Numpy ** qui peut ajouter deux nombres et tableaux appelés np.add (x1, x2).

np.add(1, 2)
# 3
np.add([0, 1], [0, 2])
# array([0, 3])

Fondamentalement, np.add () est juste +, il est donc peu probable que vous utilisiez cette fonction elle-même. Ce qui est important, c'est la méthode de la fonction universelle. Par exemple, il y a np.ufunc.reduce (array), qui vous permet d'extraire des éléments de ʻarray` et d'appliquer des fonctions. Qu'est-ce que cela signifie, par exemple

ex_array = np.arange(10)

result = ex_array[0]
for value in ex_array[1:]:
    result = np.add(result, value)
print(result)
# 45

np.add.reduce(ex_array)
# 45

De plus, np.ufunc.reduceat (tableau, indices) donne un tableau de np.ufunc.reduce (a [indices [i]: indices [i + 1]]).

ex_array = np.arange(10)
indices = [0, 1, 6, len(ex_array)]

results = []
for i in range(len(indices) - 1):
    result = np.add.reduce(ex_array[indices[i]:indices[i+1]])
    results.append(result)
print(results)
# [0, 15, 30]

indices = [0, 1, 6]
np.add.reduceat(ex_array, indices)
# array([0, 15, 30], dtype=int32)

Ici, «index» est exactement la «frontière de groupe» recherchée précédemment. En d'autres termes, en résumé

Partie 2


sorter_index = np.argsort(arr['Name'])
name_s = arr['Name'][sorter_index]
pwidth_s = arr['PetalWidth'][sorter_index]
cut_index, = np.concatenate(([True], name_s[1:] != name_s[:-1], [True])).nonzero()

sums = np.add.reduceat(pwidth_s, cut_index[:-1])
counts = np.ediff1d(cut_index)

result_mean = sums/counts
# [0.244 1.326 2.026]
result_std = np.array([np.std(pwidth_s[s:e], ddof=0)
                       for s, e in zip(cut_index[:-1], cut_index[1:])])
# [0.10613199 0.19576517 0.27188968]

Le plus triste à propos de cette technique est que vous ne pouvez pas utiliser correctement np.ufunc.reduceat () pour trouver l'écart type, vous devez donc vous fier à la boucle for (à la place, utilisez np.std () directement. Peut être utilisé).

Partie 3 (bincount)

Le moyen le plus simple de rendre la partie 1 ou 2 ridicule.

Puisque la colonne Nom est un tableau obtenu par encodage d'étiquettes, «{0, 1, 2}» est aligné. Vous pouvez tout compter en même temps en utilisant np.bincount (). Qu'est-ce que cela signifie, par exemple?

ex_array = np.array([3, 4, 3, 3, 2, 2, 4, 4, 1, 3])

results = []
for value in range(ex_array.max()+1):
    result = sum(ex_array == value)
    results.append(result)
print(results)
# [0, 1, 2, 4, 3]
# ex_Dans le tableau, 0 vaut 0, 1 vaut 1, 2 vaut 2, 3 vaut 4 et 4 vaut 3.

np.bincount(ex_array)
# array([0, 1, 2, 4, 3], dtype=int64)

np.bincount () a une option poids qui permet la pondération. Ainsi, par exemple,

ex_array = np.array([1, 2, 2, 3, 3, 3, 3, 4, 4, 4])
weights = np.array([2, 1, 1, 1, 1, 1, 1, 2, 3, 4])

results = []
for value in range(ex_array.max()+1):
    idx = np.nonzero(ex_array == value)[0]
    result = sum(weights[idx])
    results.append(result)
print(results)
# [0, 2, 2, 4, 9]

np.bincount(ex_array, weights)
# array([0., 2., 2., 4., 9.])

En utilisant cette pondération, la somme des éléments peut être calculée. Donc,

Partie 3


counts = np.bincount(arr['Name'])
sums = np.bincount(arr['Name'], arr['PetalWidth'])

result_mean = sums/counts
# [0.244 1.326 2.026]

deviation_array = result_mean[arr['Name']] - arr['PetalWidth']
result_std = np.sqrt(np.bincount(arr['Name'], deviation_array**2) / counts)
# [0.10613199 0.19576517 0.27188968]

Résumé (les personnes qui ont été capturées dans le titre doivent lire uniquement ici)

numpy_groupby.py


import numpy as np
import pandas as pd
import perfplot


def type1(value, group):
    onehot = np.eye(group.max()+1)[group]
    onehot[onehot == 0] = np.nan
    onehot_value = onehot * value[:, None]

    result_mean = np.nanmean(onehot_value, 0)
    result_std = np.nanstd(onehot_value, 0)
    return result_mean, result_std


def type1rev(value, group):
    onehot = np.arange(group.max()+1) == group[:, None]
    onehot_value = onehot * value[:, None]
    sums = onehot_value.sum(0)
    counts = onehot.sum(0)

    result_mean = sums/counts
    result_std = np.sqrt((onehot_value**2).sum(0)/counts - result_mean**2)
    return result_mean, result_std


def type2(value, group):
    sorter_index = np.argsort(group)
    group_s = group[sorter_index]
    value_s = value[sorter_index]
    cut_index, = np.concatenate(([True], group_s[1:] != group_s[:-1], [True])).nonzero()

    sums = np.add.reduceat(value_s, cut_index[:-1])
    counts = np.ediff1d(cut_index)

    result_mean = sums/counts
    result_std = np.array([np.std(value_s[s:e], ddof=0)
                           for s, e in zip(cut_index[:-1], cut_index[1:])])
    return result_mean, result_std


def type3(value, group):
    counts = np.bincount(group)
    sums = np.bincount(group, value)

    result_mean = sums/counts
    result_std = np.sqrt(
        np.bincount(group, (result_mean[group] - value)**2) / counts)
    return result_mean, result_std


df = (pd.read_csv('https://raw.githubusercontent.com/pandas-dev/pandas/'
                  'master/pandas/tests/data/iris.csv')
      .sample(frac=1, random_state=1))
unique_names, df['Name'] = np.unique(df['Name'].to_numpy(), return_inverse=True)
arr = df.to_records(index=False)[:5]

#Comparaison
perfplot.show(
    setup=lambda n: np.concatenate([arr]*n),
    n_range=[2 ** k for k in range(21)],
    kernels=[
        lambda arr: type1(arr['PetalWidth'], arr['Name']),
        lambda arr: type1rev(arr['PetalWidth'], arr['Name']),
        lambda arr: type2(arr['PetalWidth'], arr['Name']),
        lambda arr: type3(arr['PetalWidth'], arr['Name']),
    ],
    labels=["type1", "type1rev", "type2", "type3"],
    equality_check=None,
    xlabel="Nombre de lignes de données (× 5)", logx=True, logy=True,
    time_unit="s", automatic_order=False
)

pd_to_np_2.png

La partie 3 gagne (selon le nombre de groupes).

Si vous faites une erreur, veuillez commenter.


Bonus: fonctions apparues cette fois

Souvenons-nous! (Cliquez pour accéder au document officiel)

np.nansum () --Traite le non-nombre (NaN) comme zéro, Renvoie la somme des éléments du tableau sur l'axe spécifié. np.nanmean () - Ignorez NaN et déplacez-vous vers l'axe spécifié Calculez la moyenne arithmétique en suivant. np.nanstd () - Ignorez NaN et déplacez-vous vers l'axe spécifié Calculez l'écart type le long.

np.add () --Ajoutez des arguments élément par élément. np.ufunc.reduce ()- Le long d'un axe En appliquant ufunc, la dimension du tableau est réduite de un. np.ufunc.reduceat () sur un seul axe Effectue une réduction (locale) à l'aide de la tranche spécifiée.

np.argsort ()- Renvoie l'index pour trier le tableau. np.nonzero ()- Renvoie l'index des éléments non nuls.

np.bincount () --Chaque valeur dans un tableau composé uniquement de nombres naturels Compte le nombre d'occurrences de. np.unique ()-Trouver des éléments uniques du tableau.

np.ediff1d () - Différences entre les éléments consécutifs d'un tableau.

np.concatenate () - Combinez des séquences de séquences le long des axes existants Faire.

Recommended Posts