pandy GroupBy z wartościami NaN (brakującymi)


147

Mam DataFrame z wieloma brakującymi wartościami w kolumnach, które chcę pogrupować według:

import pandas as pd
import numpy as np
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})

In [4]: df.groupby('b').groups
Out[4]: {'4': [0], '6': [2]}

zobacz, że Pandy porzuciły wiersze z wartościami docelowymi NaN. (Chcę uwzględnić te wiersze!)

Ponieważ potrzebuję wielu takich operacji (wiele kolumn ma brakujące wartości) i używam bardziej skomplikowanych funkcji niż tylko mediany (zazwyczaj losowe lasy), chcę uniknąć pisania zbyt skomplikowanych fragmentów kodu.

Jakieś sugestie? Powinienem napisać funkcję do tego, czy istnieje proste rozwiązanie?


1
@PhillipCloud Zredagowałem to pytanie, aby uwzględnić tylko pytanie, które jest całkiem dobre, dotyczące ulepszenia otwartych pand w Jeff's.
Andy Hayden,

1
Brak możliwości włączania (i propagowania) NaN w grupach jest dość denerwujący. Cytowanie R nie jest przekonujące, ponieważ takie zachowanie nie jest spójne z wieloma innymi rzeczami. W każdym razie, sztuczny hack jest również dość zły. Jednak rozmiar (obejmuje NaN) i liczba (ignoruje NaN) grupy będą się różnić, jeśli istnieją NaN. dfgrouped = df.groupby (['b']). a.agg (['sum', 'size', 'count']) dfgrouped ['sum'] [dfgrouped ['size']! = dfgrouped ['count ']] = Brak
Brian Preslopsky

Czy możesz podsumować, co konkretnie chcesz osiągnąć? tzn. widzimy wynik, ale jaki jest „pożądany” wynik?
około

2
Dzięki pandas 1.1 wkrótce będziesz mógł określić dropna=Falsew, groupby()aby uzyskać pożądany rezultat. Więcej informacji
cs95

Odpowiedzi:


130

Wspomniano o tym w sekcji Brakujące dane w dokumentach :

Grupy NA w GroupBy są automatycznie wykluczane. To zachowanie jest na przykład zgodne z R.

Jednym obejściem jest użycie symbolu zastępczego przed wykonaniem grupowania (np. -1):

In [11]: df.fillna(-1)
Out[11]: 
   a   b
0  1   4
1  2  -1
2  3   6

In [12]: df.fillna(-1).groupby('b').sum()
Out[12]: 
    a
b    
-1  2
4   1
6   3

To powiedziawszy, wydaje się dość okropny hack ... być może powinna istnieć opcja włączenia NaN w groupby (zobacz ten problem na github - który używa tego samego hackowania).


4
To logiczne, ale trochę zabawne rozwiązanie, o którym myślałem wcześniej, Pandy tworzy pola NaN z pustych i musimy je zmienić z powrotem. To jest powód, dla którego myślę o szukaniu innych rozwiązań, takich jak uruchomienie serwera SQL i odpytywanie stamtąd tabel (wygląda to trochę zbyt skomplikowane), lub szukanie innej biblioteki pomimo Pand, lub użycie własnej (którą chcę pozbyć się). Dzięki
Gyula Sámuel Karli

@ GyulaSámuelKarli Wydaje mi się, że jest to mały błąd (zobacz raport o błędzie powyżej), a moje rozwiązanie to obejście. Wydaje mi się dziwne, że odpisujesz całą bibliotekę.
Andy Hayden

1
Nie chcę zapisywać Pandy, po prostu szukam narzędzia, które najbardziej pasuje do moich życzeń.
Gyula Sámuel Karli

1
Spójrz na moją odpowiedź poniżej, wydaje mi się, że znalazłem całkiem dobre (czystsze i prawdopodobnie szybsze) rozwiązanie. stackoverflow.com/a/43375020/408853
ok.

4
Nie, nie jest to spójne z R. df%>% group_by będzie również dawać podsumowania NA z ostrzeżeniem, którego można uniknąć, przekazując kolumnę grupowania przez fct_explicit_na, a następnie tworzony jest (Brak) poziom.
Ravaging Care,

40

Starożytny temat, jeśli ktoś wciąż się o to potyka - innym obejściem jest przekonwertowanie przez .astype (str) na string przed grupowaniem. To ochroni NaN.

in:
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})
df['b'] = df['b'].astype(str)
df.groupby(['b']).sum()
out:
    a
b   
4   1
6   3
nan 2

@ K3 --- rnc: Zobacz komentarz do twojego linku - autor posta w twoim linku zrobił coś nie tak.
Thomas

@Thomas, tak, dokładnie tak, jak w powyższym przykładzie. Edytuj, jeśli możesz uczynić przykład bezpiecznym (i tak banalnym).
K3 --- rnc

sumO ato ciąg konkatenacji tutaj, a nie suma numeryczny. To tylko „działa”, ponieważ „b” składa się z odrębnych wpisów. Potrzebujesz „a” jako liczby, a „b” jako ciągu znaków
BallpointBen

28

pandy> = 1.1

Od pandy 1.1 masz lepszą kontrolę nad tym zachowaniem, wartości NA są teraz dozwolone w grupie przy użyciu dropna=False:

pd.__version__
# '1.1.0.dev0+2004.g8d10bfb6f'

# Example from the docs
df

   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2

# without NA (the default)
df.groupby('b').sum()

     a  c
b        
1.0  2  3
2.0  2  5
# with NA
df.groupby('b', dropna=False).sum()

     a  c
b        
1.0  2  3
2.0  2  5
NaN  1  4

4
Miejmy nadzieję, że ta odpowiedź to stopniowy marsz na szczyt. To właściwe podejście.
kdbanman

Nie sądzę, aby wersja 1.1 została jeszcze wydana. Sprawdzone na conda i pip a wersje tam jeszcze 1.0.4
sammywemmy

1
@sammywemmy Tak, na razie można to uruchomić tylko w środowisku programistycznym . Lubię mieć przewagę, jeśli chodzi o wprowadzanie nowych funkcji do starych postów SO. ;-)
cs95

9

Nie mogę dodać komentarza do M. Kiewischa, ponieważ nie mam wystarczającej liczby punktów reputacji (mam tylko 41, ale potrzebuję więcej niż 50, aby skomentować).

W każdym razie chcę tylko zaznaczyć, że rozwiązanie M. Kiewischa nie działa tak, jak jest i może wymagać dalszych poprawek. Rozważmy na przykład

>>> df = pd.DataFrame({'a': [1, 2, 3, 5], 'b': [4, np.NaN, 6, 4]})
>>> df
   a    b
0  1  4.0
1  2  NaN
2  3  6.0
3  5  4.0
>>> df.groupby(['b']).sum()
     a
b
4.0  6
6.0  3
>>> df.astype(str).groupby(['b']).sum()
      a
b
4.0  15
6.0   3
nan   2

co pokazuje, że dla grupy b = 4,0, odpowiadająca jej wartość to 15 zamiast 6. Tutaj jest to po prostu konkatenacja 1 i 5 jako łańcuchy zamiast dodawania ich jako liczb.


12
To dlatego, że przekonwertowałeś cały DF na str, a nie tylko bkolumnę
Korem

Zauważ, że zostało to teraz naprawione we wspomnianej odpowiedzi.
Shaido - Przywróć Monikę

1
Nowe rozwiązanie jest lepsze, ale moim zdaniem nadal nie jest bezpieczne. Rozważmy przypadek, w którym jeden z wpisów w kolumnie `` b '' jest taki sam, jak z ciągiem np.NaN. Następnie te rzeczy są łączone razem. df = pd.DataFrame ({'a': [1, 2, 3, 5, 6], 'b': ['foo', np.NaN, 'bar', 'foo', 'nan']}) ; df ['b'] = df ['b']. astype (str); df.groupby (['b']). sum ()
Kamaraju Kusumanchi

6

Jedna mała uwaga na temat rozwiązania Andy'ego Haydena - nie działa (już?), Ponieważ np.nan == np.nandaje False, więc replacefunkcja tak naprawdę nic nie robi.

U mnie zadziałało:

df['b'] = df['b'].apply(lambda x: x if not np.isnan(x) else -1)

(Przynajmniej takie jest zachowanie w przypadku Pand w wersji 0.19.2. Przepraszam, że dodam to jako inną odpowiedź, nie mam wystarczającej reputacji, aby komentować).


12
Jest też df['b'].fillna(-1).
K3 --- rnc

6

Wszystkie odpowiedzi udzielone do tej pory skutkują potencjalnie niebezpiecznym zachowaniem, ponieważ jest całkiem możliwe, że wybierzesz wartość fikcyjną, która jest w rzeczywistości częścią zbioru danych. Jest to coraz bardziej prawdopodobne, gdy tworzysz grupy o wielu atrybutach. Mówiąc najprościej, podejście to nie zawsze dobrze uogólnia.

Mniej hakerskim rozwiązaniem jest użycie pd.drop_duplicates () do stworzenia unikalnego indeksu kombinacji wartości, z których każda ma własny identyfikator, a następnie grupowanie według tego identyfikatora. Jest bardziej szczegółowy, ale spełnia swoje zadanie:

def safe_groupby(df, group_cols, agg_dict):
    # set name of group col to unique value
    group_id = 'group_id'
    while group_id in df.columns:
        group_id += 'x'
    # get final order of columns
    agg_col_order = (group_cols + list(agg_dict.keys()))
    # create unique index of grouped values
    group_idx = df[group_cols].drop_duplicates()
    group_idx[group_id] = np.arange(group_idx.shape[0])
    # merge unique index on dataframe
    df = df.merge(group_idx, on=group_cols)
    # group dataframe on group id and aggregate values
    df_agg = df.groupby(group_id, as_index=True)\
               .agg(agg_dict)
    # merge grouped value index to results of aggregation
    df_agg = group_idx.set_index(group_id).join(df_agg)
    # rename index
    df_agg.index.name = None
    # return reordered columns
    return df_agg[agg_col_order]

Pamiętaj, że możesz teraz po prostu wykonać następujące czynności:

data_block = [np.tile([None, 'A'], 3),
              np.repeat(['B', 'C'], 3),
              [1] * (2 * 3)]

col_names = ['col_a', 'col_b', 'value']

test_df = pd.DataFrame(data_block, index=col_names).T

grouped_df = safe_groupby(test_df, ['col_a', 'col_b'],
                          OrderedDict([('value', 'sum')]))

Spowoduje to zwrócenie pomyślnego wyniku bez martwienia się o nadpisanie rzeczywistych danych, które są mylone jako wartość fikcyjna.


Jest to najlepsze rozwiązanie dla ogólnego przypadku, ale w przypadkach, gdy wiem, że mogę użyć nieprawidłowego ciągu / numeru, prawdopodobnie pójdę z odpowiedzią Andy'ego Haydena poniżej ... Mam nadzieję, że pandy wkrótce naprawią to zachowanie.
Sarah Messer

4

Odpowiedziałem już na to, ale z jakiegoś powodu odpowiedź została zamieniona na komentarz. Niemniej jest to najbardziej wydajne rozwiązanie:

Brak możliwości włączenia (i propagowania) NaN w grupach jest dość denerwujący. Cytowanie R nie jest przekonujące, ponieważ takie zachowanie nie jest spójne z wieloma innymi rzeczami. W każdym razie, sztuczny hack jest również dość zły. Jednak rozmiar (obejmuje NaN) i liczba (ignoruje NaN) grupy będą się różnić, jeśli istnieją NaN.

dfgrouped = df.groupby(['b']).a.agg(['sum','size','count'])

dfgrouped['sum'][dfgrouped['size']!=dfgrouped['count']] = None

Jeśli te wartości się różnią, można ustawić wartość z powrotem na Brak dla wyniku funkcji agregującej dla tej grupy.


1
To było dla mnie bardzo pomocne, ale odpowiada na nieco inne pytanie niż oryginalne. IIUC, Twoje rozwiązanie propaguje NaN w sumowaniu, ale elementy NaN w kolumnie „b” nadal są usuwane jako wiersze.
Andrew

0

Zainstalowałem Pandy 1.1 w Anaconda

Nie jestem w stanie skomentować odpowiedzi cs95, ale pomógł mi rozwiązać problem.

Próbowałem zainstalować Pandas 1.1, ale nie udało mi się użyć jego kodu, więc przeszukałem go i mogłem zainstalować.

Najpierw uruchamiam monit anaconda jako administrator i wklejam następujący kod:

pip install pandas==1.1.0rc0

Następnie należy użyć dropna = False

Link: https://libraries.io/pypi/pandas


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.