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?

Gyula Sámuel Karli
źródło
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).

Andy Hayden
źródło
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
M. Kiewisch
źródło
@ 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
cs95
źródło
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.

Kamaraju Kusumanchi
źródło
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ć).

Tuetschek
źródło
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.

Grant Langseth
źródło
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.

Brian Preslopsky
źródło
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

EzrealReal
źródło
0

df = df.fillna("") to działało dla mnie

Vineet Kumar
źródło