
Podstawy pakietu Pandas - część 2
Wstęp
- Podstawy pakietu Numpy - część 1
- Podstawy pakietu Numpy - część 2
- Podstawy pakietu Pandas - część 1
- Podstawy pakietu Pandas - część 2
- Wizualizacja danych - podstawy pakietu Matplotlib
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.
df = pd.DataFrame(np.random.rand(7,4), columns=list('ABCD'), index=list('abcdefg'))
dfdf['B']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.
df[0:3]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].
df.loc['b':'d', :]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ść.
df.loc['c', ['B', 'D']]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.
df.loc['a', 'D']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.
df.at['a', 'D']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.
df.iloc[1:4, 1:3]df.iloc[2:, :]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).
df.iloc[[0, 2, 4], [2, 0]]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.
df.iloc[2]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.
df.iloc[2, 3]df.iat[2, 3]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.
df[df > 0.5]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.
df[df['A'] > 0.5]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.
df['E'] = ['foo', 'bar', 'biz', 'fis', 'bar', 'biz', 'foo']
dfdf[df['E'].isin(['foo', 'fis'])]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.
s = pd.Series(np.arange(7), index=list('abcdefg')) df['F'] = s
dfdf.loc['c':'e', ['B', 'C']] = 0
dfZdecydowanie 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ą.
df[df['F'] > 3] = 1
dfOperacje
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.
df = pd.DataFrame(np.random.randn(5,4), columns=list('ABCD'))
dfMetody 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.
df.mean()df.mean(1)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.
df.cumsum()
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.
np.sum(df['A'])Dwoma bardzo przydatnymi metodami, które dadzą nam sporo informacji o DataFrame są .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.
df.idxmax()df.idxmin(1)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.
s = pd.Series(np.random.randint(0, 6, size=7))
ss.value_counts()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.
df.describe()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.
df1 = pd.DataFrame({'not_num': ['a', 'b', 'c', np.nan, 'e']})
df1.describe()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.
df1['num'] = np.arange(5)
df1df1.describe(include='all')
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.
df.apply(np.sum)df.apply(np.sum, axis=1)
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ą.
df.apply(lambda x: x.max() - x.min())
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.
df = pd.DataFrame(np.random.randn(5,3), columns=list('ABC'))
df['A'] = df['A'].shift(periods=2)
df.iat[3, 1] = np.nan
dfMetoda, 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.
df.dropna()df.dropna(axis=1)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.
df['D'] = np.nan
dfdf.dropna(axis=1, how='all')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ą.
df.fillna(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ł.
df.fillna(method='ffill')df.fillna(method='bfill')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.
df.fillna('?', limit=1)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.
df = pd.DataFrame({'A': ['foo', 'bar', 'bar', 'foo', 'foo', 'bar'],
'B': np.random.randn(6),
'C': np.random.randn(6)})
dfdf.groupby('A').mean()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>. Aby pobrać określoną grupę, czyli wiersze jej odpowiadające, musimy zastosować metodę .get_group().
df.groupby('A').get_group('foo')Łą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.
df1 = pd.DataFrame(np.random.randn(4,4))
df2 = pd.DataFrame(np.random.randn(4,4))
pd.concat([df1, df2])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.
pd.concat([df1, df2], ignore_index=True)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.
pd.concat([df1, df2], axis=1, ignore_index=True)Metoda .join()
W metodzie .join() dla odróżnienia, stosowane jest bardziej podejście SQL-owe.
left = pd.DataFrame({'key': ['K1', 'K2', 'K3'],
'A': ['A1', 'A2', 'A3']})
leftright = pd.DataFrame({'key': ['K0', 'K1', 'K2'],
'B': ['B0', 'B1', 'B2']})
rightGdy 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.
pd.merge(left, right, on='key')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.
pd.merge(left, right, how='outer')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.
pd.merge(left, right, how='left')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).