Podsumowanie kursu Kaggle „Pandas” (2) — lekcje 4–6
Podsumowanie wykorzystania biblioteki Pandas do czyszczenia i przetwarzania danych. Streszczenie kursu Kaggle „Pandas” z uzupełnieniami; część 2 obejmuje lekcje 4–6.
Poniżej porządkuję notatki z nauki na kursie Kaggle Pandas.
Ponieważ materiału jest całkiem sporo, podzieliłem go na 2 części.
- Część 1: lekcje 1–3
- Część 2: lekcje 4–6 (ten wpis)
Lekcja 4. Grupowanie i sortowanie
Czasem trzeba sklasyfikować dane i wykonać pewne operacje osobno dla każdej grupy, albo posortować je według określonego kryterium.
Analiza grupowa
Metoda groupby() pozwala grupować rekordy o tych samych wartościach w wybranej kolumnie, a następnie dla każdej grupy wykonać podgląd lub przekształcenia.
Wcześniej omawialiśmy metodę value_counts(); to samo działanie da się zrealizować metodą groupby() w następujący sposób:
1
reviews.groupby('taster_name').size()
- Grupuje DataFrame
reviewswedług jednakowych wartości w kolumnietaster_name - Zwraca Series z rozmiarem (liczbą rekordów) każdej grupy
Albo:
1
reviews.groupby('taster_name').taster_name.count()
- Grupuje DataFrame
reviewswedług jednakowych wartości w kolumnietaster_name - Dla każdej grupy wybiera dane z kolumny
taster_name - Zwraca Series z liczbą wartości niepustych (bez braków) w każdej grupie
Czyli value_counts() jest w istocie skrótem dla zachowania podobnego do powyższego. Poza metodą count() można w ten sposób wykorzystywać dowolną funkcję podsumowującą. Na przykład, aby z danych o winach sprawdzić minimalną cenę dla każdego wyniku punktowego:
1
reviews.groupby('points').price.min()
1
2
3
4
5
6
7
points
80 5.0
81 5.0
...
99 44.0
100 80.0
Name: price, Length: 21, dtype: float64
- Grupuje DataFrame
reviewswedług jednakowych wartości w kolumniepoints - Dla każdej grupy wybiera dane z kolumny
price - Zwraca Series z minimalną wartością w każdej grupie
Można też grupować po więcej niż jednej kolumnie. Aby wybrać wyłącznie informacje o winie z najwyższą oceną dla każdej pary (kraj, prowincja/stan):
1
reviews.groupby(['country', 'province']).apply(lambda df: df.loc[df.points.idxmax()])
Kolejną przydatną metodą obiektu DataFrameGroupBy jest agg(). Pozwala ona po zgrupowaniu danych uruchomić jednocześnie kilka funkcji dla każdej grupy.
W tym przypadku jako argument można przekazać:
- funkcję,
- napis z nazwą funkcji,
- listę funkcji lub nazw funkcji,
- słownik, gdzie kluczem jest etykieta osi, a wartością jest funkcja lub lista funkcji do zastosowania na tej osi.
Sama funkcja musi:
- przyjmować DataFrame jako wejście, albo
- być możliwa do przekazania jako argument do (omawianej wcześniej) metody
DataFrame.apply()(DataFrame.apply()).To uzupełnienie nie występowało w oryginalnym kursie Kaggle; dopisałem je, opierając się na oficjalnej dokumentacji Pandas.
Na przykład w ten sposób można policzyć statystyki cen w podziale na kraje:
1
reviews.groupby(['country']).price.agg([len, min, max])
Tutaj
lenoznacza wbudowaną funkcję Pythonalen(); w tym przykładzie użyłem jej do wypisania liczby rekordów ceny (price) w każdej grupie (country) łącznie z brakami danych.Metoda
count()z Pandas różni się tym, że zwraca wyłącznie liczbę poprawnych (niepustych) wartości, z pominięciem braków.To uzupełnienie nie występowało w oryginalnym kursie Kaggle; dopisałem je, opierając się na oficjalnej dokumentacji Pythona i Pandas.
Indeks wielopoziomowy (MultiIndex)
Gdy przetwarza się i analizuje dane metodą groupby(), czasem zamiast zwykłych etykiet otrzymuje się DataFrame z indeksem wielopoziomowym (złożonym z dwóch lub więcej poziomów).
1
2
countries_reviewed = reviews.groupby(['country', 'province']).description.agg([len])
countries_reviewed
| len | ||
|---|---|---|
| Country | province | |
| Argentina | Mendoza Province | 3264 |
| Other | 536 | |
| ... | ... | ... |
| Uruguay | San Jose | 3 |
| Uruguay | 24 |
1
2
mi = countries_reviewed.index
type(mi)
1
pandas.core.indexes.multi.MultiIndex
Indeks wielopoziomowy ma pewne metody przeznaczone do pracy ze strukturą hierarchiczną, których nie ma zwykły indeks. Szczegółowe przykłady i zalecenia dotyczące MultiIndex znajdziesz w sekcji MultiIndex / advanced indexing w pandas User Guide.
W praktyce jednak najczęściej używaną metodą dla MultiIndex bywa reset_index(), która „spłaszcza” indeks z powrotem do zwykłego.
1
countries_reviewed.reset_index()
| country | province | len | |
|---|---|---|---|
| 0 | Argentina | Mendoza Province | 3264 |
| 1 | Argentina | Other | 536 |
| … | … | … | … |
| 423 | Uruguay | San Jose | 3 |
| 424 | Uruguay | Uruguay | 24 |
Sortowanie
Jeśli przyjrzysz się countries_reviewed, zauważysz, że wynik grupowania jest zwracany w kolejności indeksu. Innymi słowy: kolejność wierszy w wyniku groupby jest wyznaczana nie przez „zawartość” danych, tylko przez wartości indeksu.
W razie potrzeby możesz ręcznie posortować dane w inny sposób. W tym celu wygodnie użyć metody sort_values(). Na przykład poniżej sortujemy rosnąco według liczby rekordów (len):
1
2
countries_reviewed = countries_reviewed.reset_index()
countries_reviewed.sort_values(by='len')
| country | province | len | |
|---|---|---|---|
| 179 | Greece | Muscat of Kefallonian | 1 |
| 192 | Greece | Sterea Ellada | 1 |
| … | … | … | … |
| 415 | US | Washington | 8639 |
| 392 | US | California | 36247 |
sort_values() domyślnie sortuje rosnąco (od mniejszych do większych wartości). Jeśli ustawisz opcję, możesz sortować malejąco (od większych do mniejszych):
1
countries_reviewed.sort_values(by='len', ascending=False)
| country | province | len | |
|---|---|---|---|
| 392 | US | California | 36247 |
| 415 | US | Washington | 8639 |
| … | … | … | … |
| 63 | Chile | Coelemu | 1 |
| 149 | Greece | Beotia | 1 |
Aby sortować według indeksu, użyj metody sort_index(). Ma te same argumenty i domyślny kierunek sortowania co sort_values(), więc sposób użycia jest analogiczny.
1
countries_reviewed.sort_index()
| country | province | len | |
|---|---|---|---|
| 0 | Argentina | Mendoza Province | 3264 |
| 1 | Argentina | Other | 536 |
| … | … | … | … |
| 423 | Uruguay | San Jose | 3 |
| 424 | Uruguay | Uruguay | 24 |
Na koniec: można też sortować jednocześnie po więcej niż jednej kolumnie, np.:
1
countries_reviewed.sort_values(by=['country', 'len'])
Lekcja 5. Typy danych i braki danych
W praktyce rzadko zdarza się, by dane były zawsze idealnie wyczyszczone. Często trzeba konwertować typy (bo nie są takie, jakich oczekujemy) albo radzić sobie z brakami danych i poprawnie je obsłużyć. Przy przetwarzaniu i analizie danych to zwykle najtrudniejszy etap.
Typy danych
Typ danych konkretnej kolumny w DataFrame (albo typu danych Series) nazywa się dtype. Za pomocą atrybutu dtype można sprawdzić typ danej kolumny. Poniżej przykład dla kolumny price w DataFrame reviews:
1
reviews.price.dtype
1
dtype('float64')
Można też użyć atrybutu dtypes, aby jednocześnie sprawdzić dtype wszystkich kolumn w DataFrame:
1
reviews.dtypes
1
2
3
4
5
6
country object
description object
...
variety object
winery object
Length: 13, dtype: object
Typ danych mówi, jak Pandas przechowuje dane wewnętrznie. Na przykład float64 oznacza 64-bitową liczbę zmiennoprzecinkową, a int64 — 64-bitową liczbę całkowitą.
Warto też zauważyć jedną specyfikę: kolumny złożone wyłącznie z napisów (string) nie mają osobnego typu tekstowego i są po prostu traktowane jako obiekty (object).
Metoda astype() pozwala przekonwertować kolumnę z jednego typu na inny. Na przykład można przekonwertować kolumnę points z typu int64 na float64:
1
reviews.points.astype('float64')
1
2
3
4
5
6
0 87.0
1 87.0
...
129969 90.0
129970 90.0
Name: points, Length: 129971, dtype: float64
Indeks DataFrame lub Series również ma typ danych:
1
reviews.index.dtype
1
dtype('int64')
Poza tym Pandas wspiera także typy „zewnętrzne”, np. dane kategoryczne czy szeregi czasowe.
Braki danych
Wpisy bez wartości (puste) dostają wartość NaN (skrót od „Not a Number”). Z powodów technicznych NaN zawsze ma typ float64.
Pandas udostępnia kilka funkcji wyspecjalizowanych do pracy z brakami danych. Wcześniej widzieliśmy coś podobnego: poza metodami istnieją też niezależne funkcje pd.isna oraz pd.notna. Zwracają one informację, czy dany wpis jest brakiem (albo czy nim nie jest) — jako pojedynczą wartość boolowską albo tablicę wartości boolowskich — i można je wykorzystać np. tak:
1
reviews[pd.isna(reviews.country)]
Zwykle najpierw trzeba sprawdzić, czy w danych są braki, a jeśli są — uzupełnić je w odpowiedni sposób. Istnieje kilka strategii. Po pierwsze, metoda fillna() pozwala zastąpić braki jakąś sensowną wartością. Poniżej przykład, w którym wszystkie NaN w kolumnie region_2 zastępujemy napisem "Unknown":
1
reviews.region_2.fillna("Unknown")
Można też zastosować strategię forward fill lub backward fill, czyli uzupełniać braki najbliższą poprawną wartością odpowiednio z przodu albo z tyłu. Da się to zrobić metodami ffill() oraz bfill().
Dawniej można było używać
fillna()z argumentemmethodustawionym na napisy'ffill'i'bfill', ale od Pandas 2.1.0 ta metoda jest deprecated i nie jest zalecana. Zamiast tego należy używaćffill()lubbfill()odpowiednio do sytuacji.
Bywa też tak, że nawet jeśli nie mamy braków danych, trzeba masowo zamienić jedną wartość na inną. W oryginalnym kursie Kaggle jako przykład podano sytuację, w której w zbiorze recenzji zmienił się twitterowy handle konkretnego recenzenta. To dobry przykład, ale spróbujmy wymyślić inny, bardziej „namacalny” w polskim (i nie tylko) kontekście.
Wyobraźmy sobie hipotetyczną sytuację, w której w Korei Południowej wydzielono północną część prowincji Gyeonggi-do jako nową jednostkę administracyjną pod nazwą Gyeonggibuk-do, i istnieje już zbiór danych, w którym ta nazwa figuruje. Następnie ktoś wpada na absurdalny pomysł, żeby tę całkiem sensowną nazwę Gyeonggibuk-do zmienić na Pyeonghwanuri Special Self-Governing Province, i jakimś cudem doprowadza do formalnego wdrożenia tej zmiany. To tylko hipotetyczny scenariusz, ale przerażające jest to, że coś podobnego mogło się wydarzyć naprawdę. Wtedy trzeba byłoby w istniejącym zbiorze danych masowo zamienić "Gyeonggibuk-do" na nowe wartości, np. "Pyeonghwanuri State" albo "Pyeonghwanuri Special Self-Governing Province". Jednym ze sposobów wykonania takiej operacji w Pandas jest metoda replace().
1
rok_2030_census.province.replace("Gyeonggibuk-do", "Pyeonghwanuri Special Self-Governing Province")
Korzystając z powyższego kodu, można skutecznie zamienić wszystkie wystąpienia "Gyeonggibuk-do" w kolumnie province zbioru rok_2030_census na „to długie coś”. I z ulgą można stwierdzić, że na szczęście w realnym świecie nikt nie musiał tego naprawdę uruchamiać.
Tego typu zamiany napisów są przydatne również przy czyszczeniu danych i obsłudze braków, bo braki danych nie zawsze są zapisane jako NaN — często pojawiają się jako napisy typu "Unknown", "Undisclosed", "Invalid" itd. W realnych projektach, np. gdy tworzy się zbiory danych przez OCR starych dokumentów urzędowych, takie przypadki mogą wręcz dominować.
Lekcja 6. Zmiana nazw i łączenie
Czasem trzeba zmienić nazwy kolumn lub indeksu w zbiorze danych. Równie często pojawia się potrzeba łączenia wielu DataFrame lub Series.
Zmiana nazw
Metoda rename() pozwala zmieniać nazwy wybranych kolumn lub indeksu w zbiorze danych. rename() obsługuje różne formaty wejścia, ale najwygodniej zwykle użyć słownika Pythona. Poniżej przykład: zmieniamy nazwę kolumny points na score w DataFrame reviews, a także zmieniamy etykiety indeksu 0 i 1 na firstEntry oraz secondEntry.
1
reviews.rename(columns={'points': 'score'})
1
reviews.rename(index={0: 'firstEntry', 1: 'secondEntry'})
W praktyce często zmienia się nazwy kolumn, ale bardzo rzadko zmienia się konkretne wartości indeksu. Do takich zastosowań zwykle wygodniej użyć (jak widzieliśmy wcześniej) metody set_index() (set_index()).
Indeks wierszy i indeks kolumn mają też własny atrybut name; metodą rename_axis() można zmienić nazwę osi. Na przykład: osi indeksu można nadać nazwę wines, a osi kolumn — fields.
1
reviews.rename_axis("wines", axis='index').rename_axis("fields", axis='columns')
Łączenie zbiorów danych
Czasem trzeba łączyć DataFrame z DataFrame albo Series z Series. Pandas udostępnia do tego trzy kluczowe funkcje; od najprostszej do najbardziej złożonej są to: concat(), join() oraz merge(). Kurs Kaggle zaznacza, że większość tego, co da się zrobić merge(), można prościej zrobić join(), więc skupia się na dwóch pierwszych.
Funkcja concat() jest najprostsza: „dokleja” wiele DataFrame lub Series wzdłuż wybranej osi. Jest przydatna, gdy łączone obiekty mają te same pola (kolumny). Domyślnie konkatenacja idzie wzdłuż osi indeksu; można też ustawić axis=1 lub axis='columns', żeby sklejać wzdłuż osi kolumn.
1
2
3
4
5
6
7
8
>>> s1 = pd.Series(['a', 'b'])
>>> s2 = pd.Series(['c', 'd'])
>>> pd.concat([s1, s2])
0 a
1 b
0 c
1 d
dtype: object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
>>> df1 = pd.DataFrame([['a', 1], ['b', 2]],
... columns=['letter', 'number'])
>>> df1
letter number
0 a 1
1 b 2
>>> df2 = pd.DataFrame([['c', 3], ['d', 4]],
... columns=['letter', 'number'])
>>> df2
letter number
0 c 3
1 d 4
>>> pd.concat([df1, df2])
letter number
0 a 1
1 b 2
0 c 3
1 d 4
>>> df4 = pd.DataFrame([['bird', 'polly'], ['monkey', 'george']],
... columns=['animal', 'name'])
>>> df4
animal name
0 bird polly
1 monkey george
>>> pd.concat([df1, df4], axis=1)
letter number animal name
0 a 1 bird polly
1 b 2 monkey george
Według oficjalnej dokumentacji Pandas nie zaleca się tworzenia DataFrame przez dodawanie pojedynczych wierszy w pętli. Jeśli trzeba połączyć wiele wierszy, lepiej zebrać je w listę i scalić jedną operacją
concat().
Metoda join() jest nieco bardziej złożona: dokleja jeden DataFrame do drugiego na podstawie indeksu. Jeśli występują kolumny o tych samych nazwach, trzeba podać argumenty lsuffix i rsuffix, aby dodać odpowiednie sufiksy rozróżniające nakładające się nazwy kolumn.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> df = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3', 'K4', 'K5'],
... 'A': ['A0', 'A1', 'A2', 'A3', 'A4', 'A5']})
>>> df
key A
0 K0 A0
1 K1 A1
2 K2 A2
3 K3 A3
4 K4 A4
5 K5 A5
>>> other = pd.DataFrame({'key': ['K0', 'K1', 'K2'],
... 'B': ['B0', 'B1', 'B2']})
>>> other
key B
0 K0 B0
1 K1 B1
2 K2 B2
>>> df.join(other, lsuffix='_caller', rsuffix='_other')
key_caller A key_other B
0 K0 A0 K0 B0
1 K1 A1 K1 B1
2 K2 A2 K2 B2
3 K3 A3 NaN NaN
4 K4 A4 NaN NaN
5 K5 A5 NaN NaN

