Podstawy pakietu Pandas - część 2

Wstęp

Praca z danymi w Pandas

W poprzednim wpisie poznaliśmy różne sposoby tworzenia DataFrames. Możemy teraz zająć się pracą z tymi strukturami. Dlatego też w tym wpisie poruszymy między innymi tematy indeksowania i wycinania fragmentów, podstawowych operacje na całych tablicach, łączenia tablic, czy grupowania elementów. Wprowadzenie to pozwoli nam na rozpoczęcie prawdziwej pracy z pakietem Pandas.

Pobieranie wartości - selecting

Pakiet Pandas daje nam kilka różnych możliwości pobierania konkretnych danych z DataFrame. Można je zawęzić do czterech szczególnych grup:

  • proste pobierania;
  • pobieranie na podstawie indeksu;
  • pobieranie na podstawie etykiety;
  • pobieranie na podstawie wartości logicznych.

Proste pobieranie kolumny / wierszy - getting

Najprostszym sposobem na pobranie kolumny z DataFrame jest przekazanie jej nazwy w nawiasach kwadratowych, w ten sam sposób jak przy pobieraniu wartości ze słownika na podstawie klucza. Takie wywołanie zwraca kolumnę jako serię pd.Series.

In [40]:
df = pd.DataFrame(np.random.rand(7,4), columns=list('ABCD'), index=list('abcdefg'))
df
Out[40]:
A B C D
a 0.808135 0.864173 0.986293 0.376971
b 0.249514 0.634011 0.251610 0.887188
c 0.115817 0.364428 0.040306 0.804634
d 0.685172 0.824623 0.731735 0.014764
e 0.661840 0.673784 0.446016 0.924192
f 0.966234 0.721057 0.297543 0.825636
g 0.459524 0.196557 0.001539 0.543766
In [41]:
df['B']
Out[41]:
a    0.864173
b    0.634011
c    0.364428
d    0.824623
e    0.673784
f    0.721057
g    0.196557
Name: B, dtype: float64

Możemy również w ten sposób pobrać wycinek z DataFrame, przekazując w nawiasach kwadratowych indeksy wierszy, które chcemy wyciąć, na tych samych zasadach, jak wycinając fragmenty listy.

Aby jednak pobrać bardziej specyficzne fragmenty, bądź pojedyncze wartości, musimy posłużyć się wbudowanymi w Pandas metodami.

In [42]:
df[0:3]
Out[42]:
A B C D
a 0.808135 0.864173 0.986293 0.376971
b 0.249514 0.634011 0.251610 0.887188
c 0.115817 0.364428 0.040306 0.804634

Pobieranie na podstawie etykiet

W Pandas, gdy zdefiniujemy nazwy kolumn, oraz etykiety odzwierciedlające wiersze tabeli DataFrame (również Series), możemy korzystać z pobierania wycinków właśnie na podstawie tych etykiet i nazw kolumn.

Podstawowa metoda do tego służąca to .loc[]. Należy zwrócić uwagę, że wartości do metody przekazujemy w nawiasach kwadratowych [ ]. Metoda ta działa niemal identycznie jak pobieranie wycinków w ndarray pakietu NumPy, z tą różnicą, że zamiast indeksów kolumn i wierszy przekazujemy odpowiadające im etykiety (w przypadku kolumn są to po prostu ich nazwy). Gdy pobieramy zakres od - do, obie wartości wchodzą w skład wycinka - [start, stop].

In [43]:
df.loc['b':'d', :]
Out[43]:
A B C D
b 0.249514 0.634011 0.251610 0.887188
c 0.115817 0.364428 0.040306 0.804634
d 0.685172 0.824623 0.731735 0.014764

Możemy również przekazywać listę etykiet, które mają zostać zwrócone. Działa to zarówno dla kolumn, jak i wierszy. Mogą one być w innej kolejności niż w początkowym DataFrame.

Gdy w którymś z wymiarów przekażemy tylko jedną wartość, niezależnie czy są to wiersze, czy kolumny, wynikowy wycinek zostanie zwrócony jako jednowymiarowa seria z indeksami w postaci etykiet. Należy zauważyć, że każdy taki wycinek ma wtedy atrybut name, który odpowiada etykiecie z osi, na której wybieraliśmy pojedynczą wartość.

In [44]:
df.loc['c', ['B', 'D']]
Out[44]:
B    0.364428
D    0.804634
Name: c, dtype: float64

Przy pomocy etykiet możemy również pobrać pojedyncze wartości skalarne z tabeli, określając ich dokładne położenie w obu osiach.

In [45]:
df.loc['a', 'D']
Out[45]:
0.3769706017945337

W przypadku pobieranie pojedynczej wartości skalarnej możemy również skorzystać z metody .at[]. Jej zachowanie i wynik są dokładnie takie same jak .loc[], ale działa ona tylko w przypadku skalarów. Jest ona za to lepiej zoptymalizowana do tego celu.

In [46]:
df.at['a', 'D']
Out[46]:
0.3769706017945337

Pobieranie na podstawie indeksów

Kolejna niezwykle przydatna metoda służąca pobieraniu fragmentów DataFrame to .iloc[], różniąca się od wyżej omawianego .loc[] tylko tym, że przyjmuje indeksy wierszy i kolumn, zamiast etykiet. Pamiętać należy, że aby wybrać kolumny, również należy podać ich numery porządkowe, czyli jakby indeksy z ndarray. Metoda ta właściwie działa jak indeksowanie po ndarray.

In [47]:
df.iloc[1:4, 1:3]
Out[47]:
B C
b 0.634011 0.251610
c 0.364428 0.040306
d 0.824623 0.731735
In [48]:
df.iloc[2:, :]
Out[48]:
A B C D
c 0.115817 0.364428 0.040306 0.804634
d 0.685172 0.824623 0.731735 0.014764
e 0.661840 0.673784 0.446016 0.924192
f 0.966234 0.721057 0.297543 0.825636
g 0.459524 0.196557 0.001539 0.543766

Z racji na podobieństwa do pracy z ndarray nie ma za wiele co tłumaczyć dodatkowo o .iloc[]. Pamiętajmy tylko, że w przeciwieństwie do .loc[], w przypadku wycinaniu zakresu używając znaku :, koniec zakresu nie wchodzi do wybieranego zbioru - [start, stop).

In [49]:
df.iloc[[0, 2, 4], [2, 0]]
Out[49]:
C A
a 0.986293 0.808135
c 0.040306 0.115817
e 0.446016 0.661840

Gdy w metodzie .iloc[] przekażemy tylko jedną wartość, zwrócona zostanie pd.Series zawierająca wiersz o podanym indeksie. Gdy natomiast przekażemy tylko jeden zakres od : do, Pandas potraktuje to jako wartości indeksów wycinka w osi wierszy i automatycznie przekaże wszystkie kolumny.

In [50]:
df.iloc[2]
Out[50]:
A    0.115817
B    0.364428
C    0.040306
D    0.804634
Name: c, dtype: float64

W wypadku pobierania pojedynczej wartość metoda .iloc[] działa tak samo, jak odpowiedniczka działająca na etykietach. Tutaj też mamy do czynienia, z dodatkową metodą, .iat[] do pobierania skalarów, która jest znacznie bardziej zoptymalizowana.

In [51]:
df.iloc[2, 3]
Out[51]:
0.804633928699436
In [52]:
df.iat[2, 3]
Out[52]:
0.804633928699436

Pobieranie na podstawie wartości logicznych

Kolejnym sposobem na wybieranie elementów DataFrame jest przekazanie w nawiasach kwadratowych wartości logicznej wyliczanej na całym df lub jego fragmencie.

Gdy operacja logiczna zostanie przeprowadzona na całym DataFrame, wynikiem operacji będzie DataFrame o tych samych wymiarach, który ma wartości w miejscach, gdzie wartość logiczna była prawną, oraz np.NaN tam, gdzie była fałszem.

In [53]:
df[df > 0.5]
Out[53]:
A B C D
a 0.808135 0.864173 0.986293 NaN
b NaN 0.634011 NaN 0.887188
c NaN NaN NaN 0.804634
d 0.685172 0.824623 0.731735 NaN
e 0.661840 0.673784 NaN 0.924192
f 0.966234 0.721057 NaN 0.825636
g NaN NaN NaN 0.543766

Jeśli natomiast operację logiczną wykonamy dla pojedynczej kolumny i tak zbudowana maskę przekażemy do DataFrame, wynikiem będą wyłącznie te kolumny, dla których maska zwróciła True.

In [54]:
df[df['A'] > 0.5]
Out[54]:
A B C D
a 0.808135 0.864173 0.986293 0.376971
d 0.685172 0.824623 0.731735 0.014764
e 0.661840 0.673784 0.446016 0.924192
f 0.966234 0.721057 0.297543 0.825636

Gdy mówimy o pobieraniu fragmentu na podstawie wartości logicznych, bardzo przydatną metodą okazuje się .isin(). Metoda ta działa bardzo podobnie jak do sprawdzania, czy klucz występuje w słowniku. Potrzebujemy do tego kolumny, która zawiera ograniczoną ilość różniących się od siebie elementów.

Metodę tą wykonujemy na wybranej kolumnie, przekazując jej wartości, których występowanie chcemy sprawdzić w DataFrame. Jako wynik dostajemy wyłącznie wiersze, które w sprawdzanej kolumnie zawierają sprawdzane wartości.

In [55]:
df['E'] = ['foo', 'bar', 'biz', 'fis', 'bar', 'biz', 'foo']
df
Out[55]:
A B C D E
a 0.808135 0.864173 0.986293 0.376971 foo
b 0.249514 0.634011 0.251610 0.887188 bar
c 0.115817 0.364428 0.040306 0.804634 biz
d 0.685172 0.824623 0.731735 0.014764 fis
e 0.661840 0.673784 0.446016 0.924192 bar
f 0.966234 0.721057 0.297543 0.825636 biz
g 0.459524 0.196557 0.001539 0.543766 foo
In [56]:
df[df['E'].isin(['foo', 'fis'])]
Out[56]:
A B C D E
a 0.808135 0.864173 0.986293 0.376971 foo
d 0.685172 0.824623 0.731735 0.014764 fis
g 0.459524 0.196557 0.001539 0.543766 foo

Przypisywanie wartości

Wszystkie omawiane powyżej metody pobierania wartości z DataFrame mogą służyć również do przypisywania wartości większym fragmentom danych.

In [57]:
s = pd.Series(np.arange(7), index=list('abcdefg')) 
In [58]:
df['F'] = s
df
Out[58]:
A B C D E F
a 0.808135 0.864173 0.986293 0.376971 foo 0
b 0.249514 0.634011 0.251610 0.887188 bar 1
c 0.115817 0.364428 0.040306 0.804634 biz 2
d 0.685172 0.824623 0.731735 0.014764 fis 3
e 0.661840 0.673784 0.446016 0.924192 bar 4
f 0.966234 0.721057 0.297543 0.825636 biz 5
g 0.459524 0.196557 0.001539 0.543766 foo 6
In [59]:
df.loc['c':'e', ['B', 'C']] = 0
df
Out[59]:
A B C D E F
a 0.808135 0.864173 0.986293 0.376971 foo 0
b 0.249514 0.634011 0.251610 0.887188 bar 1
c 0.115817 0.000000 0.000000 0.804634 biz 2
d 0.685172 0.000000 0.000000 0.014764 fis 3
e 0.661840 0.000000 0.000000 0.924192 bar 4
f 0.966234 0.721057 0.297543 0.825636 biz 5
g 0.459524 0.196557 0.001539 0.543766 foo 6

Zdecydowanie najczęściej wykorzystywanym w przypisywaniu sposobem wybierania obszaru do nadpisania jest wybieranie go przy pomocy wartości logicznych, czyli maskowania. Każda komórka, dla której maska zwróciła True zostanie nadpisana przypisaną wartością.

In [60]:
df[df['F'] > 3] = 1
df
Out[60]:
A B C D E F
a 0.808135 0.864173 0.986293 0.376971 foo 0
b 0.249514 0.634011 0.251610 0.887188 bar 1
c 0.115817 0.000000 0.000000 0.804634 biz 2
d 0.685172 0.000000 0.000000 0.014764 fis 3
e 1.000000 1.000000 1.000000 1.000000 1 1
f 1.000000 1.000000 1.000000 1.000000 1 1
g 1.000000 1.000000 1.000000 1.000000 1 1

Operacje

Operacje matematyczne przeprowadzane na DataFrame odbywają się w podobny sposób jak na ndarray z pakietu NumPy. Mianowicie przeprowadzane są na całej tabeli, a dokładniej na wszystkich numerycznych kolumnach tej tabeli. Zostaną również zastosowane wszystkie reguły związane z broadcastingiem znanym z NumPy.

Statystyka

Większość regularnie używanych metod dających informacje o DataFrame wylicza znane ze statystyki wartości, np. średnia, mediana, odchylenie standardowe, czy po prostu oblicza skumulowane wartości dla tabeli jak suma.

In [61]:
df = pd.DataFrame(np.random.randn(5,4), columns=list('ABCD'))
df
Out[61]:
A B C D
0 0.690684 0.324174 -0.335372 1.313379
1 -0.467937 0.162174 -0.616831 1.829192
2 -0.235984 0.278024 1.417945 0.129966
3 -1.254355 0.905757 -0.195888 -0.131335
4 0.532403 -0.054235 0.277141 -0.787686

Metody te głównie zwracają skumulowane wartości wyliczane kolumna po kolumnie. Aby nadpisać to zachowanie i wymusić na metodzie, aby zwróciła wartość wiersz po wierszu, jako parametr należy przekazać axis=1 (lub po prostu 1 ), co wyznaczy kierunek liczenia wartości.

Wszystkie te metody automatycznie próbują wykorzystać tylko wartości numeryczne i pomijają wartości NaN. Gdy kolumna zawiera same wartości tekstowe, zostanie ona pominięta w wynikowej pd.Series.

In [62]:
df.mean()
Out[62]:
A   -0.147038
B    0.323179
C    0.109399
D    0.470703
dtype: float64
In [63]:
df.mean(1)
Out[63]:
0    0.498216
1    0.226649
2    0.397488
3   -0.168955
4   -0.008094
dtype: float64

Metody typu .cumsum() nie zmniejszają wymiaru wynikowej struktury. Zwracają wynik o tym samym kształcie. Również w tym przypadku zdefiniowanie axis pozwoli wybrać, w którym kierunku (po wierszach, czy kolumnach) mają zostać wyliczone skumulowane sumy tabeli.

In [64]:
df.cumsum()
  
Out[64]:
A B C D
0 0.690684 0.324174 -0.335372 1.313379
1 0.222746 0.486348 -0.952202 3.142571
2 -0.013238 0.764372 0.465742 3.272537
3 -1.267592 1.670129 0.269854 3.141202
4 -0.735189 1.615894 0.546995 2.353516

Na tablicach DataFrame można z powodzeniem również wykonywać metody z pakietu NumPy. Wystarczy jako argument przekazać cały DataFrame lub jego fragment. Gdy do metody np.sum() przekażemy kolumnę, zwróci ona zsumowane wszystkie wartości tej kolumny. Gdy za to przekażemy wielokolumnowy DataFrame otrzymamy sumy policzone dla każdej z kolumn osobno. Również tutaj argument axis pozwoli zmienić to, po której osi zostanie suma wyliczona.

In [65]:
np.sum(df['A'])
Out[65]:
-0.7351892998655505

Dwoma bardzo przydatnymi metodami, które dadzą nam sporo informacji o DataFrame.idxmax() i .idxmin. Zwracają one indeksy największej i najmniejszej wartości w kolumnie. Argument axis=1 spowoduje, że zwrócone zostaną odpowiadające wartości dla wierszy.

Obie metody zadziałają również, gdy zostaną wykonane na serii, zwrócą wtedy pojedyncza wartość indeksu odpowiadającego odpowiednio największej i najmniejszej wartości w serii.

In [66]:
df.idxmax()
Out[66]:
A    0
B    3
C    2
D    1
dtype: int64
In [67]:
df.idxmin(1)
Out[67]:
0    C
1    C
2    A
3    A
4    D
dtype: object

Kolejną bardzo przydatną metodą, którą możemy wykonać na serii, jest metoda .values_count(). Zwraca ona, jak często poszczególna wartość wystąpiła w podanej serii. Świetnie nadaje się ona do budowania prostych histogramów.

Metoda ta z automatu pomija wartości np.NaN, możemy to zachowanie zmienić, przekazując do metody parametr dropna=False. W tym wypadku, jeśli w serii występują brakujące wartości, zostaną one również policzone i zwrócone w zestawieniu.

In [68]:
s = pd.Series(np.random.randint(0, 6, size=7))
s
Out[68]:
0    3
1    1
2    4
3    3
4    2
5    1
6    4
dtype: int64
In [69]:
s.value_counts()
Out[69]:
4    2
3    2
1    2
2    1
dtype: int64

Informacje o zbiorze danych

Chyba najważniejszą i najprostszą w użyciu metodą, która daje nam bardzo szybkie i dokładne informacje o tym, co znajduje się w tabeli DataFrame jest metoda .describe(). Zwraca ona zestaw statystyk opisowych wyliczonych kolumna po kolumnie dla całej tabeli.

In [70]:
df.describe()
Out[70]:
A B C D
count 5.000000 5.000000 5.000000 5.000000
mean -0.147038 0.323179 0.109399 0.470703
std 0.790641 0.357002 0.799822 1.074441
min -1.254355 -0.054235 -0.616831 -0.787686
25% -0.467937 0.162174 -0.335372 -0.131335
50% -0.235984 0.278024 -0.195888 0.129966
75% 0.532403 0.324174 0.277141 1.313379
max 0.690684 0.905757 1.417945 1.829192

Gdy wartości w tabeli są wartościami nienumerycznymi, .describe() zwróci podstawowe statystyki tych danych takie jak ilość wartości unikalnych, ogólną ilość elementów czy wartość najczęstszą. Wynik również będzie wyliczony dla każdej z kolumn oddzielnie.

In [71]:
df1 = pd.DataFrame({'not_num': ['a', 'b', 'c', np.nan, 'e']})
df1.describe()
Out[71]:
not_num
count 4
unique 4
top a
freq 1

Gdy tabela zawiera kolumny numeryczne, jak i nienumeryczne .describe() z automatu wyliczy statystyki tylko dla tych zawierających wartości numeryczne. Można to zachowanie nadpisać, definiując w metodzie parametr include='all'. Zostaną wtedy zwrócone również statystyki dla nienumerycznych kolumn.

In [72]:
df1['num'] = np.arange(5)
df1
Out[72]:
not_num num
0 a 0
1 b 1
2 c 2
3 NaN 3
4 e 4
In [73]:
df1.describe(include='all')
Out[73]:
not_num num
count 4 5.000000
unique 4 NaN
top b NaN
freq 1 NaN
mean NaN 2.000000
std NaN 1.581139
min NaN 0.000000
25% NaN 1.000000
50% NaN 2.000000
75% NaN 3.000000
max NaN 4.000000

Apply

Metoda .apply() służy do aplikowania funkcji wzdłuż jednej z osi. Parametr axis definiuje, czy zadana jako pierwszy parametr funkcja jest wykonywana kolumna po kolumnie, czy wiersz po wierszu.

In [74]:
df.apply(np.sum)
Out[74]:
A   -0.735189
B    1.615894
C    0.546995
D    2.353516
dtype: float64
In [75]:
df.apply(np.sum, axis=1)
Out[75]:
0    1.992865
1    0.906598
2    1.589950
3   -0.675821
4   -0.032377
dtype: float64

Jako funkcję, która ma zostać wykonana, możemy przekazać metody z NumPy, funkcje napisane przez nas, oraz oczywiście funkcje lambda. Te ostatnie są chyba używane najczęściej, ze względu na prostotę zapisu i zrozumienia co robią.

In [76]:
df.apply(lambda x: x.max() - x.min())
Out[76]:
A    1.945038
B    0.959993
C    2.034775
D    2.616878
dtype: float64

Pamiętajmy, że zanim zaczniemy pisać skomplikowane funkcje, które przekażemy do .apply(), upewnijmy się, czy w pakiecie NumPy lub Pandas nie ma wbudowanych funkcji robiących to samo. Będą one zapewne dużo wydajniejsze i lepiej zoptymalizowane.

Missing data

Jak dotąd zajmowaliśmy się wyłącznie wygenerowanymi sztucznie tabelami, w których nie brakowało żadnych elementów. Taka sytuacja w realnych zbiorach danych zdarza się jednak niezmiernie rzadko. Z tego też względu bardzo ważne w dalszej pracy będzie poznanie metod radzenia sobie właśnie z brakującymi wartościami, czyli np.NaN.

Aby nie utrudnić czytania tego wpisu, nie będę tutaj importował dużego zbioru danych zawierającego 'missing values'. Posłużymy się prostymi przekształceniami Pandas, aby nieznacznie 'popsuć' prosty, sztuczny DataFrame.

In [77]:
df = pd.DataFrame(np.random.randn(5,3), columns=list('ABC'))
df['A'] = df['A'].shift(periods=2)
df.iat[3, 1] = np.nan
df
Out[77]:
A B C
0 NaN 0.552327 -1.219446
1 NaN 0.942665 -1.607528
2 -0.547551 0.208549 -0.425632
3 -0.519772 NaN -0.091534
4 1.196414 -0.460693 0.335744

Metoda, której możemy na tym etapie nie znać, to .shift(), która jak nazwa wskazuje, przesuwa elementy w serii o zadany parametr periods. Tworzy to wartości np.NaN w miejscach, z których przesunięto elementy.

Usuwanie brakujących wartości

Pierwszą z metod do radzenia sobie z missing values jest .dropna(). Usuwa ona wiersze, w których znajdują się wartości NaN. Przekazując parametr axis=1 mówimy metodzie, aby zamiast wierszy, usuwała kolumny z brakującymi wartościami.

In [78]:
df.dropna()
Out[78]:
A B C
2 -0.547551 0.208549 -0.425632
4 1.196414 -0.460693 0.335744
In [79]:
df.dropna(axis=1)
Out[79]:
C
0 -1.219446
1 -1.607528
2 -0.425632
3 -0.091534
4 0.335744

Domyślnym działaniem metody jest usuwanie kolumn (wierszy), gdzie występuje chociaż jedno NaN, dzieje się tak, gdyż metoda domyślnie ma ustawiony parametr how='any', gdy jednak zmienimy to, przekazując parametr how='all' usunięte zostaną tylko kolumny (wiersze, w zależności od parametru axis), w których wszystkie wartości to NaN.

In [80]:
df['D'] = np.nan
df
Out[80]:
A B C D
0 NaN 0.552327 -1.219446 NaN
1 NaN 0.942665 -1.607528 NaN
2 -0.547551 0.208549 -0.425632 NaN
3 -0.519772 NaN -0.091534 NaN
4 1.196414 -0.460693 0.335744 NaN
In [81]:
df.dropna(axis=1, how='all')
Out[81]:
A B C
0 NaN 0.552327 -1.219446
1 NaN 0.942665 -1.607528
2 -0.547551 0.208549 -0.425632
3 -0.519772 NaN -0.091534
4 1.196414 -0.460693 0.335744

Nadpisywanie brakujących wartości

Bardzo podobną metodą do .dropna() jest metoda .fillna(). W przeciwieństwie do poprzedniczki nadpisuje ona brakujące elementy zdefiniowanymi przez użytkownika wartościami.

Najprostszym sposobem użycia jest nadpisanie wszystkich wartości pojedyncza liczbą.

In [82]:
df.fillna(0)
Out[82]:
A B C D
0 0.000000 0.552327 -1.219446 0.0
1 0.000000 0.942665 -1.607528 0.0
2 -0.547551 0.208549 -0.425632 0.0
3 -0.519772 0.000000 -0.091534 0.0
4 1.196414 -0.460693 0.335744 0.0

Możemy również zdefiniować szczególne zachowanie parametrem method. Gdy wpiszemy 'ffill' metoda uzupełni brakujące wartości prawidłowymi wartościami je poprzedzającymi - uzupełni do przodu. 'bfill' natomiast, uzupełni NaNy wartościami znajdującymi się bezpośrednio po nich - uzupełni w tył.

In [83]:
df.fillna(method='ffill')
Out[83]:
A B C D
0 NaN 0.552327 -1.219446 NaN
1 NaN 0.942665 -1.607528 NaN
2 -0.547551 0.208549 -0.425632 NaN
3 -0.519772 0.208549 -0.091534 NaN
4 1.196414 -0.460693 0.335744 NaN
In [84]:
df.fillna(method='bfill')
Out[84]:
A B C D
0 -0.547551 0.552327 -1.219446 NaN
1 -0.547551 0.942665 -1.607528 NaN
2 -0.547551 0.208549 -0.425632 NaN
3 -0.519772 -0.460693 -0.091534 NaN
4 1.196414 -0.460693 0.335744 NaN

Problem może polegać na tym, że gdy np. przy parametrze method='ffill' pierwszy element kolumny wynosi NaN, metoda nie będzie miała go czym uzupełnić, bo nic przed nim nie ma. Pozostawi więc NaN.

Metodzie .fillna() możemy również zadać limit elementów do nadpisania w każdej kolumnie.

In [85]:
df.fillna('?', limit=1)
Out[85]:
A B C D
0 ? 0.552327 -1.219446 ?
1 NaN 0.942665 -1.607528 NaN
2 -0.547551 0.208549 -0.425632 NaN
3 -0.519772 ? -0.091534 NaN
4 1.19641 -0.460693 0.335744 NaN

Jak widzimy wyżej, wartość nadpisująca nie musi być wcale wartością numeryczną. Możemy, łącząc .fillna() z innymi metodami, nadpisywać brakujące wartości np. wartościami najczęściej występującymi dla danej kolumny itp.

Grupowanie

DataFrame pakietu Pandas umożliwia nam grupowanie tabeli na podstawie wartości w wybranej kolumnie (bądź kilku kolumnach). Metoda .groupby() przyjmuje kolumnę, po której ma zostać przeprowadzone grupowanie. Dla tak pogrupowanego DataFrame możemy zaaplikować np. metody statystyczne, które zostaną wyliczone oddzielnie dla każdej z grup.

In [86]:
df = pd.DataFrame({'A': ['foo', 'bar', 'bar', 'foo', 'foo', 'bar'],
                   'B': np.random.randn(6),
                   'C': np.random.randn(6)})
df
Out[86]:
A B C
0 foo -0.712422 0.197933
1 bar -1.389049 0.608469
2 bar -1.672393 1.340892
3 foo -1.902060 -0.524089
4 foo -0.592200 -1.105602
5 bar 1.641235 -0.256648
In [87]:
df.groupby('A').mean()
Out[87]:
B C
A
bar -0.473402 0.564237
foo -1.068894 -0.477253

Obiekt, który otrzymujemy z samego procesu grupowania, nie pozwala nam na wyświetlenie poszczególnych grup w bezpośredni sposób. Gdy spróbujemy wyświetlić df.groupby(df['A']) otrzymamy <pandas.core.groupby.generic.DataFrameGroupBy object at 0x7f2e48565e50&gt;. Aby pobrać określoną grupę, czyli wiersze jej odpowiadające, musimy zastosować metodę .get_group().

In [88]:
df.groupby('A').get_group('foo')
Out[88]:
A B C
0 foo -0.712422 0.197933
3 foo -1.902060 -0.524089
4 foo -0.592200 -1.105602

Łączenie tabel

Ostatnim tematem, który chciałbym poruszyć w tym wpisie, jest łączenie tablic. Często będzie się zdarzało, że problem będzie wymagał rozdzielania, przekształceń i ponownego łączenia ze sobą zbiorów danych. Pandas udostępnia nam do tego dwie bardzo użyteczne metody, pozwalające na spajanie ze sobą tabel, na bardzo wiele konfigurowalnych sposobów. Różnią się one zamysłem, na podstawie którego łączą zbiory.

Metoda .concat()

W podstawowym zastosowaniu metody .concat() przekazujemy do niej listę DataFrames, które mają zostać ze sobą połączone. Domyślnie zbiory zostaną złączone vertykalnie, czyli drugi poniżej pierwszego.

In [89]:
df1 = pd.DataFrame(np.random.randn(4,4))
df2 = pd.DataFrame(np.random.randn(4,4))
pd.concat([df1, df2])
Out[89]:
0 1 2 3
0 0.203264 0.149827 -0.430527 -0.738968
1 1.445423 -1.394058 0.100795 -1.327643
2 -0.680555 0.051062 0.030236 0.844866
3 0.666290 0.273346 2.364498 -1.628630
0 -0.152519 -0.003016 -0.232511 -1.000228
1 2.729718 -0.249056 -1.895550 1.315563
2 2.323606 1.622645 2.364899 -0.191765
3 -0.595703 -0.449733 0.490235 -1.384348

Jak widzimy, takie proste zastosowanie spowodowało, że nawet po połączeniu oba zbiory zachowały swoje pierwotne indeksy. Aby temu zapobiec, musimy do metody .concat() przekazać parametr ignore_index=True. Spowoduje on zresetowanie indeksów w wynikowym DataFrame.

In [90]:
pd.concat([df1, df2], ignore_index=True)
Out[90]:
0 1 2 3
0 0.203264 0.149827 -0.430527 -0.738968
1 1.445423 -1.394058 0.100795 -1.327643
2 -0.680555 0.051062 0.030236 0.844866
3 0.666290 0.273346 2.364498 -1.628630
4 -0.152519 -0.003016 -0.232511 -1.000228
5 2.729718 -0.249056 -1.895550 1.315563
6 2.323606 1.622645 2.364899 -0.191765
7 -0.595703 -0.449733 0.490235 -1.384348

Możemy również sterować sposobem łączenia ze sobą zbiorów. Gdy chcemy, aby zostały one połączone horyzontalnie, wystarczy przekazać do metody parametr axis=1.

In [91]:
pd.concat([df1, df2], axis=1, ignore_index=True)
Out[91]:
0 1 2 3 4 5 6 7
0 0.203264 0.149827 -0.430527 -0.738968 -0.152519 -0.003016 -0.232511 -1.000228
1 1.445423 -1.394058 0.100795 -1.327643 2.729718 -0.249056 -1.895550 1.315563
2 -0.680555 0.051062 0.030236 0.844866 2.323606 1.622645 2.364899 -0.191765
3 0.666290 0.273346 2.364498 -1.628630 -0.595703 -0.449733 0.490235 -1.384348

Metoda .join()

W metodzie .join() dla odróżnienia, stosowane jest bardziej podejście SQL-owe.

In [92]:
left = pd.DataFrame({'key': ['K1', 'K2', 'K3'],
                       'A': ['A1', 'A2', 'A3']})
left
Out[92]:
key A
0 K1 A1
1 K2 A2
2 K3 A3
In [93]:
right = pd.DataFrame({'key': ['K0', 'K1', 'K2'],
                        'B': ['B0', 'B1', 'B2']})
right
Out[93]:
key B
0 K0 B0
1 K1 B1
2 K2 B2

Gdy posiadamy dwa zbiory danych, które mają wspólną kolumną, możemy połączyć je za pomocą metody .join(), definiując w parametrze on, według której kolumny przeprowadzić łączenie.

In [94]:
pd.merge(left, right, on='key')
Out[94]:
key A B
0 K1 A1 B1
1 K2 A2 B2

Metoda posiada domyślny parametr how='inner', co, dla nieznających SQL, oznacza, że do połączenia wykorzystane zostaną tylko te wiersze, które występują w obu łączonych tabelach.

Gdy zmienimy manualnie wartość parametru how na 'outer', po połączeniu uzyskamy zbiór zawierający wszystkie wiersze występujące w obu zbiorach. Miejsca z brakującymi danymi zostaną uzupełnione wartościami np.NaN.

In [95]:
pd.merge(left, right, how='outer')
Out[95]:
key A B
0 K1 A1 B1
1 K2 A2 B2
2 K3 A3 NaN
3 K0 NaN B0

W parametrze how możemy też określić, na podstawie której tabeli przeprowadzić łączeni. 'left' mówi, żeby do łączenia wziąć tylko te kolumny występujące w 'lewym' zbiorze danych. Niezależnie co znajduje się w 'prawym'.

Po więcej informacji jak działa JOIN w SQL odsyłam do mojego wcześniejszego wpisu: Podstawy SQL na przykładzie MySQL oraz do dokumentacji.

In [96]:
pd.merge(left, right, how='left')
Out[96]:
key A B
0 K1 A1 B1
1 K2 A2 B2
2 K3 A3 NaN

Podsumowanie

Teraz znamy już podstawy pracy z Pandas. Omówiliśmy zagadnienia związane z indeksowaniem, nadpisywaniem, wybieraniem, czy grupowaniem tablic. Wiemy jak radzić sobie z brakującymi wartościami, jak łączyć tablice, oraz jak otrzymać informacje o tym, co znajduje się w DataFrame. Wiedza ta pozwoli nam poznawać kolejne narzędzia Pythona służące przetwarzaniu i analizie danych oraz rozpocząć zabawę ze sztuczną inteligencją, co jest głównym celem całego cyklu wpisów.

Jeśli chodzi o Pandas w najbliższym czasie pojawią się jeszcze dwa wpisy bezpośrednio odnoszące się do pakietu. Omówimy w nich bardziej zaawansowane koncepcje oraz przykład wykonania analizy danych (EDA).