[PYTHON] Deshalb beende ich Pandas [Drei Möglichkeiten, um groupby.mean () mit nur NumPy]

Deshalb beende ich Pandas [Drei Möglichkeiten, um groupby.mean () mit nur NumPy]

Was ist dieser spielerische Artikel

Leute, die sagen, dass Pandas auf Low-Spec-PCs (Tohoho) bereits ein Chaos sind, verarbeiten Daten wahrscheinlich in einem Chaos mit NumPy. NumPy hat jedoch keine "pd.DataFrame.groupby" -ähnliche Funktion, so dass einige Leute möglicherweise mit weinenden Pandas durcheinander geraten. Aber wenn Sie dumm sind und nicht das tun können, was Pandas mit NumPy tun können, geben Sie Ihr Bestes, um die NumPy-Dokumentation umzublättern und sich die verschiedenen Funktionen anzusehen (aber für Schleifen sind NG). Ja, du kannst.

Für diejenigen, die nicht wissen, was sie denken sollen, sagte ein weiser Mann in der Vergangenheit: "Wie man nur mit Numpy nach Gruppen aggregiert und den Durchschnitt berechnet (für Satz keine Pandas) / numpy-grouping-no-pandas /) ”(Gott). In der heutigen Zeit, in der sich Wissenschaft und Technologie entwickelt haben, hatte ich jedoch das Gefühl, dass es einen besseren Weg geben könnte. Deshalb habe ich beim Studium von Numpys Funktionen über verschiedene Dinge nachgedacht und ein Memo auf Japanisch geschrieben, das die Aufzeichnung des Kampfes nicht in Verbindung bringt. In diesem Artikel geht es darum, sich zu widmen.

Dinge die zu tun sind

Dieses Mal möchte ich den Durchschnittswert und die Standardabweichung für jede Gruppe ermitteln, dh "pd.DataFrame.groupby.mean ()" d.pd.DataFrame.groupby.std () "nur mit NumPy berechnen.

Zu verwendende Daten

Vertraute Irisdaten. Die Zeilen werden gemischt.

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

Eine Notiz vor dem Konvertieren in ein Numpy-Objekt. Um Daten mit NumPy effizient verarbeiten zu können, müssen andere Daten als der numerische Typ (Zeichenfolge, Objekttyp usw.) in den numerischen Typ konvertiert werden. Wenn Sie Zeichenfolgen bearbeiten möchten, verwenden Sie am besten eine Standardliste. Die Spalte "Name" stellt diesmal nur die Kategorie dar, und die Zeichenfolge hat in den Daten keine Bedeutung. Daher wird eine sogenannte Etikettencodierung durchgeführt, um sie in eine Zahl umzuwandeln. Dies ist möglich mit 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))

#Dies ist auch möglich
df['Name'], unique_names = pd.factorize(df['Name'], sort=True)
Neben

Zu diesem Zeitpunkt ist bekannt, dass "pd.factorize ()" schneller ist als "np.unique ()", wenn eine bestimmte Anzahl von Daten vorhanden ist.

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="Anzahl der Datenzeilen", logx=True, logy=True, time_unit="s", automatic_order=False
)

pd_to_np_1.png

Anscheinend sollten Sie "pd.factorize ()" verwenden, wenn Sie "Series" (nicht nur "ndarray") mit mehr als 1000 Zeilen konvertieren. </ div> </ details>

In NumPy-Array konvertieren (hier wird das Datensatzarray zur Vereinfachung der Erklärung verwendet). Der Moment, um mit Pandas Schluss zu machen.

arr = df.to_records(index=False)

Gruppieren nach

Teil 1 (onehot)

Zunächst werde ich die Methode des obigen Artikels vorstellen.

One-Hot-Vektorisierung der Art der Iris. Multiplizieren Sie dies mit dem Blütenblattlängenvektor, den Sie aggregieren möchten, und nehmen Sie den Durchschnitt für jede Spalte. Wenn der One-Hot-Vektor so gemittelt wird, wie er ist, wird er durch 0 beeinflusst. Ersetzen Sie ihn daher vorab durch NaN.

Teil 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]

Das Problem bei dieser Methode ist, dass np.nanmean () und np.nanstd () bekanntermaßen langsam sind. Die durchschnittliche Formel lautet

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

Wenn Sie es daher durch einen Ausdruck ersetzen, der "np.nansum ()" verwendet, sieht es wie folgt aus.

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)

Außerdem ersetzt "np.nan" in "onehot_value" "0". Da sich die Summe jedoch nicht ändert, egal wie viele "0" hinzugefügt werden, ersetzen Sie sie nicht durch "np.nan". Sie können np.sum () so wie es ist verwenden.

Die Tatsache, dass Sie "onpot" nicht "np.nan" zuweisen müssen, bedeutet, dass Sie nicht den Datentyp "float" verwenden müssen, sodass Sie mit der folgenden Methode schneller "onehot" erhalten können.

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

Deshalb,

Teil 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)

Teil 2 (Reduktion)

Der Punkt von groupby ist, wie für jede Gruppe berechnet wird. In Teil 1 konnten wir das gleiche Ergebnis wie "groupby.mean ()" erzielen, indem wir die Werte für jede Gruppe in verschiedene Spalten verschoben und gemittelt haben.

Teil 1 Denkweise


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

Wenn die Elemente im ursprünglichen Array in Gruppen angeordnet sind, kann die folgende Idee berücksichtigt werden.

Teil 2 Denkweise


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

Um zuerst nach Gruppen zu ordnen, wird daher die Sortierung anhand der Spalte Name durchgeführt.

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]

Dann wird die Grenze der Gruppe erhalten. Dies kann durch Betrachten jedes benachbarten Werts in der Spalte sortierter Name erfolgen. Zum Beispiel

ex_array = np.array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3])
#                    0------  1------  2------  3------← Ich möchte die Position dieser Grenze

ex_array[1:] != ex_array[1:]
"""Denkweise
 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:]
(Wenn die beiden besten übereinstimmen, F.,Wenn sie nicht übereinstimmen, 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] #Das Array, das ich wirklich wollte
 0------  1------  2------  3------
# ex_array[1:] != ex_array[1:]Wenn Sie vorher und nachher ein True hinzufügen,
"T-vor dem nächsten T" wird eine Gruppe.
"""

#Komplementäre Version vorher und nachher
np.concatenate(([True], ex_array[1:] != ex_array[:-1], [True]))
# array([ True, False, False, False, False, False, False, False, False, False, False, False, True])

Wenn Sie die wahre Position dieses Arrays mit np.ndarray.nonzero () erhalten, ist dies die Position der Grenze jeder Gruppe.

cut_index = np.concatenate(([True], ex_array[1:] != ex_array[:-1], [True])).nonzero()[0]
# array([ 0,  3,  6,  9, 12], dtype=int64)
"""Denkweise
 0  1  2  3  4  5  6  7  8  9 10 11 12   # ex_Index des Arrays
[0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]     # ex_array
[0,       3,       6,       9,      12]  # cut_index:Grenze (Startpunkt) jeder Gruppe
"""

Auch hier ist der Durchschnitt "Gesamtzahl ÷ Zahl", also finden Sie jede. Die Anzahl der Elemente in einer Gruppe ist die Differenz zwischen benachbarten Werten von "cut_index". Sie können also "np.diff ()" und "np.ediff1d ()" verwenden, um sie auf einmal zu finden.

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

Dann wird die Summe der Elemente in der Gruppe berechnet. Übrigens gibt es eine ** Numpy Universalfunktion **, die zwei Zahlen und Arrays mit dem Namen np.add (x1, x2) hinzufügen kann.

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

Grundsätzlich ist "np.add ()" nur "+", daher ist es unwahrscheinlich, dass Sie diese Funktion selbst verwenden. Wichtig ist die Methode der Universalfunktion. Zum Beispiel gibt es "np.ufunc.reduce (Array)", mit dem Sie Elemente aus "Array" extrahieren und Funktionen anwenden können. Was bedeutet das zum Beispiel?

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

Zusätzlich gibt "np.ufunc.reduceat (Array, Indizes)" ein Array von "np.ufunc.reduce (a [Indizes [i]: Indizes [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)

Die "Indizes" hier sind genau die zuvor gesuchte "Gruppengrenze". Mit anderen Worten, zusammenfassend

Teil 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]

Das Traurige an dieser Technik ist, dass Sie np.ufunc.reduceat () nicht gut verwenden können, wenn Sie die Standardabweichung finden. Sie müssen sich also auf die for-Schleife verlassen (verwenden Sie stattdessen np.std () direkt. Kann verwendet werden).

Teil 3 (bincount)

Der einfachste Weg, Teil 1 oder 2 lächerlich zu machen.

Da die Name-Spalte ein Array ist, das durch Label-Codierung erhalten wird, wird "{0, 1, 2}" in einer Reihe angeordnet. Sie können dies alles auf einmal zählen, indem Sie "np.bincount ()" verwenden. Was bedeutet das zum Beispiel?

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_Im Array ist 0 0, 1 ist 1, 2 ist 2, 3 ist 4 und 4 ist 3.

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

np.bincount () hat eine weight-Option, die die Gewichtung ermöglicht. So zum Beispiel

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

Mit dieser Gewichtung kann die Summe der Elemente berechnet werden. Deshalb,

Teil 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]

Zusammenfassung (Personen, die im Titel gefangen wurden, sollten nur hier lesen)

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]

#Vergleich
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="Anzahl der Datenzeilen (× 5)", logx=True, logy=True,
    time_unit="s", automatic_order=False
)

pd_to_np_2.png

Teil 3 gewinnt (abhängig von der Anzahl der Gruppen).

Wenn Sie einen Fehler machen, kommentieren Sie bitte.


Bonus: Funktionen, die dieses Mal erschienen sind

Lass uns erinnern! (Klicken Sie hier, um zum offiziellen Dokument zu springen.)

np.nansum () - Behandeln Sie Nicht-Zahlen (NaN) als Null, Gibt die Summe der Array-Elemente auf der angegebenen Achse zurück. np.nanmean () - Ignorieren Sie NaN und bewegen Sie sich zur angegebenen Achse Berechnen Sie den arithmetischen Durchschnitt entlang. np.nanstd () - Ignorieren Sie NaN und bewegen Sie sich zur angegebenen Achse Berechnen Sie die Standardabweichung entlang.

np.add () - Argumente Element für Element hinzufügen. np.ufunc.reduce ()- Auf einer Achse Durch Anwenden von ufunc wird die Dimension des Arrays um eins reduziert. np.ufunc.reduceat () - auf einer einzelnen Achse Führt eine (lokale) Reduzierung mit dem angegebenen Slice durch.

np.argsort ()- Gibt den Index zurück, um das Array zu sortieren. np.nonzero ()- Gibt den Index der Nicht-Null-Elemente zurück.

np.bincount () - Jeder Wert in einem Array, das nur aus natürlichen Zahlen besteht Zählt die Anzahl der Vorkommen von. np.unique ()-Finden Sie eindeutige Elemente des Arrays.

np.ediff1d ()-Differenzen zwischen aufeinanderfolgenden Elementen eines Arrays.

np.concatenate ()- Kombinieren Sie Sequenzen von Sequenzen entlang vorhandener Achsen Machen.

Recommended Posts