Wpis

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.

Podsumowanie kursu Kaggle „Pandas” (2) — 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.

Certyfikat ukończenia

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()
  1. Grupuje DataFrame reviews według jednakowych wartości w kolumnie taster_name
  2. Zwraca Series z rozmiarem (liczbą rekordów) każdej grupy

Albo:

1
reviews.groupby('taster_name').taster_name.count()
  1. Grupuje DataFrame reviews według jednakowych wartości w kolumnie taster_name
  2. Dla każdej grupy wybiera dane z kolumny taster_name
  3. 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
  1. Grupuje DataFrame reviews według jednakowych wartości w kolumnie points
  2. Dla każdej grupy wybiera dane z kolumny price
  3. 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:

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 len oznacza wbudowaną funkcję Pythona len(); 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
Countryprovince
ArgentinaMendoza Province3264
Other536
.........
UruguaySan Jose3
Uruguay24
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()
 countryprovincelen
0ArgentinaMendoza Province3264
1ArgentinaOther536
423UruguaySan Jose3
424UruguayUruguay24

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')
 countryprovincelen
179GreeceMuscat of Kefallonian1
192GreeceSterea Ellada1
415USWashington8639
392USCalifornia36247

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)
 countryprovincelen
392USCalifornia36247
415USWashington8639
63ChileCoelemu1
149GreeceBeotia1

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()
 countryprovincelen
0ArgentinaMendoza Province3264
1ArgentinaOther536
423UruguaySan Jose3
424UruguayUruguay24

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 argumentem method ustawionym 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() lub bfill() 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
Ten wpis jest objęty licencją CC BY-NC 4.0 przez autora.