2016-04-03 21 views
5

DataFrameGroupby.filter Methode filtert die Gruppen und gibt DataFrame zurück, die die Zeilen enthält, die den Filter bestanden haben.Verkettungsgruppierung, Filterung und Aggregation

Aber was kann ich tun, um nach der Filtration ein neues DataFrameGroupBy Objekt anstelle einer DataFrame zu erhalten?

Zum Beispiel habe ich eine DataFramedf mit zwei Spalten A und B. Ich möchte B Mittelwert der Spalte erhalten für jeden Wert der Spalte A, solange es in dieser Gruppe mindestens 5 Zeilen ist:

# pandas 0.18.0 
# doesn't work because `filter` returns a DF not a GroupBy object 
df.groupby('A').filter(lambda x: len(x)>=5).mean() 
# works but slower and awkward to write because needs to groupby('A') twice 
df.groupby('A').filter(lambda x: len(x)>=5).reset_index().groupby('A').mean() 
# works but more verbose than chaining 
groups = df.groupby('A') 
groups.mean()[groups.size() >= 5] 

Antwort

2

Hier einige reproduzierbaren Daten:

np.random.seed(0) 

df = pd.DataFrame(np.random.randint(0, 10, (10, 2)), columns=list('AB')) 

>>> df 
    A B 
0 5 0 
1 3 3 
2 7 9 
3 3 5 
4 2 4 
5 7 6 
6 8 8 
7 1 6 
8 7 7 
9 8 1 

Eine Probe Filter-Anwendung zeigt, dass es auf die Daten funktioniert.

gb = df.groupby('A') 
>>> gb.filter(lambda group: group.A.count() >= 3) 
    A B 
2 7 9 
5 7 6 
8 7 7 

Hier sind einige der Optionen:

1) Sie können auch erste Filter basierend auf den Wert zählt, und dann Gruppe.

vc = df.A.value_counts() 

>>> df.loc[df.A.isin(vc[vc >= 2].index)].groupby('A').mean() 
      B 
A   
3 4.000000 
7 7.333333 
8 4.500000 

2) Führen Sie groupby zweimal, vor und nach dem Filter:

>>> (df.groupby('A', as_index=False) 
     .filter(lambda group: group.A.count() >= 2) 
     .groupby('A') 
     .mean()) 
      B 
A   
3 4.000000 
7 7.333333 
8 4.500000 

3) In Anbetracht, dass Ihre erste groupby die Gruppen zurückgibt, können Sie auch auf diejenigen filtern:

d = {k: v 
    for k, v in df.groupby('A').groups.items() 
    if len(v) >= 2} # gb.groups.iteritems() for Python 2 

>>> d 
{3: [1, 3], 7: [2, 5, 8], 8: [6, 9]} 

Dies ist ein bisschen ein Hack, sollte aber relativ effizient sein, da Sie sich nicht neu gruppieren müssen.

>>> pd.DataFrame({col: [df.ix[d[col], 'B'].mean()] for col in d}).T.rename(columns={0: 'B'}) 
      B 
3 4.000000 
7 7.333333 
8 4.500000 

Timings mit 100k Reihen

np.random.seed(0) 
df = pd.DataFrame(np.random.randint(0, 10, (100000, 2)), columns=list('AB')) 

%timeit df.groupby('A', as_index=False).filter(lambda group: group['A'].count() >= 5).groupby('A').mean() 
100 loops, best of 3: 18 ms per loop 

%%timeit 
vc = df.A.value_counts() 
df.loc[df.A.isin(vc[vc >= 2].index)].groupby('A').mean() 
100 loops, best of 3: 15.7 ms per loop 
+0

Aber ich möchte den Mittelwert für jede der Gruppen erhalten, nach der Filtration. Sie erhalten den Mittelwert des gesamten verbleibenden Datensatzes. – max

+0

Ich denke, es wird falsche Ergebnisse produzieren ... Versuchen Sie einen anderen Datensatz, wo Sie mindestens zwei verschiedene Gruppen von Werten für 'A' haben würden – MaxU

+0

hmm, ich bekomme unterschiedliche Ergebnis-Set mit Ihrer _hack_ Version ... – MaxU

3

Sie können es auf diese Weise tun:

In [310]: df 
Out[310]: 
    a b 
0 1 4 
1 7 3 
2 6 9 
3 4 4 
4 0 2 
5 8 4 
6 7 7 
7 0 5 
8 8 5 
9 8 7 
10 6 1 
11 3 8 
12 7 4 
13 8 0 
14 5 3 
15 5 3 
16 8 1 
17 7 2 
18 9 9 
19 3 2 
20 9 1 
21 1 2 
22 0 3 
23 8 9 
24 7 7 
25 8 1 
26 5 8 
27 9 6 
28 2 8 
29 9 0 

In [314]: r = df.groupby('a').apply(lambda x: x.b.mean() if len(x)>=5 else -1) 

In [315]: r 
Out[315]: 
a 
0 -1.000000 
1 -1.000000 
2 -1.000000 
3 -1.000000 
4 -1.000000 
5 -1.000000 
6 -1.000000 
7 4.600000 
8 3.857143 
9 -1.000000 
dtype: float64 

In [316]: r[r>0] 
Out[316]: 
a 
7 4.600000 
8 3.857143 
dtype: float64 

One-Liner, die anstelle von Serien-Datenrahmen zurück:

df.groupby('a') \ 
    .apply(lambda x: x.b.mean() if len(x)>=5 else -1) \ 
    .to_frame() \ 
    .rename(columns={0:'mean'}) \ 
    .query('mean > 0') 

Timeit Vergleich gegen einen DF mit 100.000 Zeilen:

def maxu(): 
    r = df.groupby('a').apply(lambda x: x.b.mean() if len(x)>=5 else -1) 
    return r[r>0] 

def maxu2(): 
    return df.groupby('a') \ 
      .apply(lambda x: x.b.mean() if len(x)>=5 else -1) \ 
      .to_frame() \ 
      .rename(columns={0:'mean'}) \ 
      .query('mean > 0') 

def alexander(): 
    return df.groupby('a', as_index=False).filter(lambda group: group.a.count() >= 5).groupby('a').mean() 

def alexander2(): 
    vc = df.a.value_counts() 
    return df.loc[df.a.isin(vc[vc >= 5].index)].groupby('a').mean() 

Ergebnisse:

In [419]: %timeit maxu() 
1 loop, best of 3: 1.12 s per loop 

In [420]: %timeit maxu2() 
1 loop, best of 3: 1.12 s per loop 

In [421]: %timeit alexander() 
1 loop, best of 3: 34.9 s per loop 

In [422]: %timeit alexander2() 
10 loops, best of 3: 66.6 ms per loop 

Check:

In [423]: alexander2().sum() 
Out[423]: 
b 19220943.162 
dtype: float64 

In [424]: maxu2().sum() 
Out[424]: 
mean 19220943.162 
dtype: float64 

Fazit:

klare Sieger ist alexander2() Funktion

@Alexander, Glückwunsch!

+0

Plus eine für den Wettbewerb, aber ich habe gerade mein Spiel erhöht ... – Alexander

+0

@Alexander, danke! Ich mag deine Antwort auch, also habe ich sie ebenfalls aufgewertet. Lass mich deine neue Lösung _timeit_ ... – MaxU