Pandas DataFrame-Aggregatfunktion mit mehreren Spalten

80

Gibt es eine Möglichkeit, eine Aggregationsfunktion zu schreiben, wie sie in der DataFrame.aggMethode verwendet wird und die Zugriff auf mehr als eine Spalte der Daten hat, die aggregiert werden? Typische Anwendungsfälle wären gewichtete durchschnittliche, gewichtete Standardabweichungsfunktionen.

Ich würde gerne so etwas schreiben können

def wAvg(c, w):
    return ((c * w).sum() / w.sum())

df = DataFrame(....) # df has columns c and w, i want weighted average
                     # of c using w as weight.
df.aggregate ({"c": wAvg}) # and somehow tell it to use w column as weights ...
user1444817
quelle
Schöner Artikel zu dieser speziellen SO-Frage: pbpython.com/weighted-average.html
ptim

Antworten:

101

Ja; Verwenden Sie die .apply(...)Funktion, die auf jedem Sub aufgerufen wird DataFrame. Zum Beispiel:

grouped = df.groupby(keys)

def wavg(group):
    d = group['data']
    w = group['weights']
    return (d * w).sum() / w.sum()

grouped.apply(wavg)
Wes McKinney
quelle
Es kann effizienter sein, dies wie folgt in einige Operationen aufzuteilen: (1) Erstellen einer Gewichtungsspalte, (2) Normalisieren der Beobachtungen anhand ihrer Gewichte, (3) Berechnen einer gruppierten Summe gewichteter Beobachtungen und einer gruppierten Summe von Gewichten (4) Normalisieren der gewichteten Summe der Beobachtungen durch die Summe der Gewichte.
Kalu
4
Was ist, wenn wir Wellen von vielen Variablen (Spalten) berechnen wollen, z. B. alles außer df ['Gewichte']?
CPBL
2
@Wes, gibt es eine Möglichkeit, dies einmal mit agg()und lambdaum diese herum zu tun np.average(...weights=...), oder eine neue native Unterstützung in Pandas für gewichtete Mittel, seit dieser Beitrag zum ersten Mal veröffentlicht wurde?
sparc_spread
4
@Wes McKinney: In Ihrem Buch schlagen Sie diesen Ansatz vor : get_wavg = lambda g: np.average(g['data'], weights = g['weights']); grouped.apply(wavg) Sind die beiden austauschbar?
Robroc
9

Meine Lösung ähnelt der von Nathaniel, nur für eine einzelne Spalte, und ich kopiere nicht jedes Mal den gesamten Datenrahmen tief, was unerschwinglich langsam sein kann. Der Leistungsgewinn gegenüber der Lösungsgruppe durch (...). Apply (...) beträgt ca. 100x (!)

def weighted_average(df, data_col, weight_col, by_col):
    df['_data_times_weight'] = df[data_col] * df[weight_col]
    df['_weight_where_notnull'] = df[weight_col] * pd.notnull(df[data_col])
    g = df.groupby(by_col)
    result = g['_data_times_weight'].sum() / g['_weight_where_notnull'].sum()
    del df['_data_times_weight'], df['_weight_where_notnull']
    return result
ErnestScribbler
quelle
Wäre besser lesbar, wenn Sie PEP8 konsistent verwenden und die überflüssige delZeile entfernen würden .
MERose
Vielen Dank! Die delZeile ist eigentlich nicht überflüssig, da ich den eingegebenen DataFrame an Ort und Stelle ändere, um die Leistung zu verbessern, und daher aufräumen muss.
ErnestScribbler
Sie geben das Ergebnis jedoch in der nächsten Zeile zurück, die die Funktion beendet. Sobald die Funktion beendet ist, werden alle internen Objekte trotzdem gelöscht.
MERose
1
Beachten Sie jedoch, dass df kein internes Objekt ist. Es ist ein Argument für die Funktion, und solange Sie es nie zuweisen ( df = something), bleibt es eine flache Kopie und wird an Ort und Stelle geändert. In diesem Fall werden dem DataFrame Spalten hinzugefügt. Versuchen Sie, diese Funktion zu kopieren und ohne die delZeile auszuführen, und stellen Sie sicher, dass der angegebene DataFrame durch Hinzufügen von Spalten geändert wird.
ErnestScribbler
Dies beantwortet die Frage nicht, da der gewichtete Durchschnitt nur als Beispiel für ein Aggregat in mehreren Spalten dient.
user__42
8

Es ist möglich, eine beliebige Anzahl von aggregierten Werten von einem groupby-Objekt mit zurückzugeben apply. Geben Sie einfach eine Serie zurück und die Indexwerte werden zu den neuen Spaltennamen.

Sehen wir uns ein kurzes Beispiel an:

df = pd.DataFrame({'group':['a','a','b','b'],
                   'd1':[5,10,100,30],
                   'd2':[7,1,3,20],
                   'weights':[.2,.8, .4, .6]},
                 columns=['group', 'd1', 'd2', 'weights'])
df

  group   d1  d2  weights
0     a    5   7      0.2
1     a   10   1      0.8
2     b  100   3      0.4
3     b   30  20      0.6

Definieren Sie eine benutzerdefinierte Funktion, an die übergeben wird apply. Es akzeptiert implizit einen DataFrame - was bedeutet, dass der dataParameter ein DataFrame ist. Beachten Sie, wie mehrere Spalten verwendet werden, was mit der agggroupby-Methode nicht möglich ist :

def weighted_average(data):
    d = {}
    d['d1_wa'] = np.average(data['d1'], weights=data['weights'])
    d['d2_wa'] = np.average(data['d2'], weights=data['weights'])
    return pd.Series(d)

Rufen Sie die groupby- applyMethode mit unserer benutzerdefinierten Funktion auf:

df.groupby('group').apply(weighted_average)

       d1_wa  d2_wa
group              
a        9.0    2.2
b       58.0   13.2

Sie können eine bessere Leistung erzielen, indem Sie die gewichteten Summen wie in anderen Antworten erläutert in neue DataFrame-Spalten vorberechnen und die Verwendung applyinsgesamt vermeiden .

Ted Petrou
quelle
4

Das Folgende (basierend auf Wes McKinneys Antwort) erreicht genau das, wonach ich gesucht habe. Ich würde mich freuen zu erfahren, ob es einen einfacheren Weg gibt, dies innerhalb zu tun pandas.

def wavg_func(datacol, weightscol):
    def wavg(group):
        dd = group[datacol]
        ww = group[weightscol] * 1.0
        return (dd * ww).sum() / ww.sum()
    return wavg


def df_wavg(df, groupbycol, weightscol):
    grouped = df.groupby(groupbycol)
    df_ret = grouped.agg({weightscol:sum})
    datacols = [cc for cc in df.columns if cc not in [groupbycol, weightscol]]
    for dcol in datacols:
        try:
            wavg_f = wavg_func(dcol, weightscol)
            df_ret[dcol] = grouped.apply(wavg_f)
        except TypeError:  # handle non-numeric columns
            df_ret[dcol] = grouped.agg({dcol:min})
    return df_ret

Die Funktion df_wavg()gibt einen Datenrahmen zurück, der nach der Spalte "groupby" gruppiert ist und die Summe der Gewichte für die Spalte "weight" zurückgibt. Andere Spalten sind entweder die gewichteten Durchschnittswerte oder, wenn sie nicht numerisch sind, wird die min()Funktion zur Aggregation verwendet.

dslack
quelle
4

Ich mache das oft und fand Folgendes ziemlich praktisch:

def weighed_average(grp):
    return grp._get_numeric_data().multiply(grp['COUNT'], axis=0).sum()/grp['COUNT'].sum()
df.groupby('SOME_COL').apply(weighed_average)

Dadurch wird der gewichtete Durchschnitt aller numerischen Spalten in den dfund nicht numerischen Spalten berechnet .

Santon
quelle
Das ist blitzschnell! Gut gemacht!
Shay Ben-Sasson
Das ist wirklich süß, wenn Sie mehrere Spalten haben. Nett!
Chris
@Santon, danke für die Antwort. Können Sie ein Beispiel für Ihre Lösung geben? Beim Versuch, Ihre Lösung zu verwenden, wurde die Fehlermeldung 'KeyError:' COUNT 'angezeigt.
Allen
@Allen Sie sollten den Namen der Spalte verwenden, der die Anzahl enthält, die Sie für den gewichteten Durchschnitt verwenden möchten.
Santon
4

Dies zu erreichen groupby(...).apply(...)ist nicht performant. Hier ist eine Lösung, die ich ständig benutze (im Wesentlichen unter Verwendung der Kalu-Logik).

def grouped_weighted_average(self, values, weights, *groupby_args, **groupby_kwargs):
   """
    :param values: column(s) to take the average of
    :param weights_col: column to weight on
    :param group_args: args to pass into groupby (e.g. the level you want to group on)
    :param group_kwargs: kwargs to pass into groupby
    :return: pandas.Series or pandas.DataFrame
    """

    if isinstance(values, str):
        values = [values]

    ss = []
    for value_col in values:
        df = self.copy()
        prod_name = 'prod_{v}_{w}'.format(v=value_col, w=weights)
        weights_name = 'weights_{w}'.format(w=weights)

        df[prod_name] = df[value_col] * df[weights]
        df[weights_name] = df[weights].where(~df[prod_name].isnull())
        df = df.groupby(*groupby_args, **groupby_kwargs).sum()
        s = df[prod_name] / df[weights_name]
        s.name = value_col
        ss.append(s)
    df = pd.concat(ss, axis=1) if len(ss) > 1 else ss[0]
    return df

pandas.DataFrame.grouped_weighted_average = grouped_weighted_average
Nathaniel
quelle
1
Wenn Sie nicht performant sagen. Wie groß ist der Unterschied? Haben Sie es gemessen?
Bouncner