
Podstawy pakietu Pandas - część 1
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
Czym jest Pandas?
Pandas jest w tym momencie podstawowym pakietem służącym do przetwarzania i analizy danych w języku Python. Oparty na NumPy, poznanym we wcześniejszych wpisach, dziedziczy po nim wiele funkcjonalności i zasad działania. Wiele metod i podejść, jak optymalizacja operacji element po elemencie na całych tablicach, sposoby indeksowania czy też wykorzystanie uniwersalnych funkcji (unfunc), działają zazwyczaj tak samo, lub są tylko rozwinięciem tego, co znamy z NumPY.
Pandas świetnie nadaje się do pracy ze wszelkiego rodzaju samymi tabelarycznymi. Nie jesteśmy tu ograniczeni wymogiem znanym z NumPy, aby wszystkie dane były tego samego typu. W tym pakiecie wystarczy, aby wszystkie dane w jednej kolumnie posiadały ten sam tym. Dzięki temu usprawnieniu Pandas z powodzeniem pozwala zapisać dane o strukturze tablic SQL, arkuszy Excel'a, ciągów czasowych czy ogólnie wszelkiego rodzaju danych pomiarowych, czy statystycznych.
Struktury danych dostępne w Pandas
Podstawowymi dwoma strukturami zapisu danych w Pandas są Series dla danych jednowymiarowych oraz DataFrame dla dwuwymiarowych. Aby zacząć opis struktur danych, musimy w pierwszej kolejności zaimportować biblioteki. Poza pandas zaimportujmy również numpy, gdyż w bardzo wielu przykładach będziemy korzystać również z tej biblioteki, zwłaszcza do budowy Series i DataFrames na podstawie ndarrays.
import pandas as pd
import numpy as npGdy w naszym jupyter notebook'u mamy poprawne importy, możemy przejść do pierwszej, prostszej w opisie struktury, a mianowicie Series.
Series
Seria w Pandas jest najprostszym obiektem przechowującym dane. Jest strukturą jednowymiarową, posiadającą etykiety każdego elementu. Etykiety (labels) możemy w uproszczeniu rozumieć jak indeksy jednowymiarowej tabeli czy tablicy ndarray, z tą jednak różnicą, że możemy nadać serii dowolne etykiety.
Najprostszym sposobem stworzenia serii w pandas jest przekazanie do konstruktora iterowalnego obiektu, takiego jak lista, tupla czy ndarray.
pd.Series([1, 3.2, 0.1, 2.])s = pd.Series(np.random.randn(4), index=['a', 'b', 'c', 'd'])
sW wypadku, gdy nie sprecyzujemy jakie etykiety mają być, staną się nimi kolejne liczny całkowite numerowane od zera. Gdy jednak chcemy określić dokładnie, jakie chcemy etykiety nadać naszej serii. Zrobimy to, definiując je w wywołaniu pd.Series() parametrem index. Możemy do niego przekazać dowolnym obiekt listopodobny z tym tylko zastrzeżeniem, że musi być tej samej długości co dane. Każda seria posiada również atrybut index, przy pomocy którego możemy pobrać jej etykiety.
s.indexKolejnym powszechnym sposobem tworzenia serii w Pandas jest przekazanie słownika. W takim wypadku nie jest konieczne precyzowanie etykiet, gdyż zostaną za nie przypisane klucze słownika. Gdy jednak przekażemy etykiety w parametrze index, nadpiszą one te wygenerowane na podstawie kluczy.
pd.Series({'f':1, 'g':5, 'h':3})Jesteśmy również w stanie utworzyć serię na podstawie wartości skalarnej. W tym jednak wypadku musimy zdefiniować listę z etykietami, gdyż na tej podstawie Pandas będzie wiedzieć, ile elementów z tą samą wartością skalarną ma się znaleźć w serii.
pd.Series(1., index=['a', 'b', 'c'])O seriach pakietu Pandas możemy z powodzeniem myśleć jak o Pythonowych listach. Zadziała indeksowanie czy tworzenie elementów na podstawie numeru kolejnego elementu, nawet jeśli etykiety nie są kolejnymi liczbami całkowitymi. Pandas zrozumie nas, kiedy będziemy, dla przykładu, chcieli pobrać pierwszy, bądź dwa ostatnie elementy korzystając z zapisu znanego z indeksowania pythonowych list czy ndarrays.
s[0]s[2:]Jeśli chcemy, możemy również przekształcić serię właśnie w ndarray za pomocą metody pd.Series.to_numpy(). Spowoduje to utratę etykiet, zapisany zostanie tylko ciąg wartości.
s.to_numpy()O pd.Series możemy również myśleć jak o swego rodzaju słowniku. Tak jak wartość słownika możemy uzyskać, korzystając z zapisu dictionary['key'], tak w serii przekazując do tego samego zapisu nazwę etykiety, otrzymamy odpowiadającą jej wartość. Również przypisanie wartości do etykiety nieistniejącej w serii spowoduje dodanie tej wartości wraz z etykietą.
Do tego, tak samo, jak w przypadku słowników możemy sprawdzić, czy dana etykieta znajduje się w serii za pomocą operatora is.
s['a']s['e'] = 10
s'e' in s'f' in sO seriach możemy również myśleć jak o ndarrays. Wszystkie metody pakietu NumPy przyjmujące tablicę, zadziałają również dla serii Pandas. Na seriach można również wykonywać operacje arytmetycznie podobnie jak na tablicach. Wykonają się one analogicznie, element po elemencie, zwracając serię o tej samej długości.
np.sin(s)s*10DataFrame
Kolejną obiektem przechowującym dane w Pandas jest DataFrame. Jest to zdecydowanie najczęściej wykorzystywana struktura w Pandas i ogólnie w data science opartym o Python. DataFrames są to dwuwymiarowe tabelaryczne obiekty, składające się z kolumn, gdzie kolumny mogą się różnic między sobą typem danych.
Zazwyczaj zmienną przechowującą DataFrame nazywamy w skrócie od pierwszych liter nazwy obiektu df.
Najbardziej bezpośrednim sposobem na utworzenie DataFrame jest przekazanie do konstruktora słownika, którego wartościami są Series. W ten sposób każda seria stanie się kolumną tablicy, a klucze słownika staną się nazwami kolumn.
d = {'fst': pd.Series(np.random.rand(3), index=list('abc')),
'snd': pd.Series(np.random.rand(4), index=list('abcd'))}
df = pd.DataFrame(d)
dfJeśli obiekty, z których mają zostać zbudowane kolumny tabeli, w tym wypadku pd.Series(), są różnej długości, lub ich indeksy się różnią, kolumny będą zawierały wszystkie elementy, a w wypadku braku odpowiadającego indeksu w którejś z nich, zostanie wstawiona wartość np.NaN.
Każda z tablic posiada atrybuty index (podobnie jak serie) oraz columns, za pomocą których możemy otrzymać nazwy kolumn i etykiet (wierszy) tabeli.
df.columnsdf.indexPodobnie jak w przypadku pd.Series() jeśli nie przekażemy za pomocą atrybutu index listy etykiet, za etykiety (indeksy) przypisany zostanie ciąg liczby całkowitych zaczynających się od zera. Gdy natomiast któraś wartość tablicy indeksów nie będzie miała odzwierciedlenia w kolumnie, nie zostanie ona pominięta, jedynie w tym miejscu zostaje wstawione np.NaN. Również, gdy indeks nie występuje w żadnej z kolumn.
pd.DataFrame(d, index=list('bcde'))W wypadku budowy pd.DataFrame() na podstawie słownika Pandas automatycznie przypisuje klucze słownika jako nazwy kolumn. Jeśli jednak chcemy zmienić ich kolejność albo wykorzystać tylko część z nich, możemy przekazać w parametrze columns listę wybranych kluczy słownika, w wybranej przez nas kolejności.
pd.DataFrame(d, columns=['snd', 'fst'])DataFrame nie potrzebuje słownika serii, w zupełności wystarczy, aby słownik jako wartości zawierał pythonowe obiekty, po których można iterować, typu listy, tuple czy ndarray. W tym wypadku, gdy nie przekażemy listy z indeksami, zostaną przypisane kolejne liczby całkowite.
d = {'one': [1, 2, 3, 4], 'two': [11, 12, 13, 14]}
pd.DataFrame(d)pd.DataFrame(d, index=list('abcd'))Jeśli w momencie tworzenia
pd.DataFrame()nie zdefiniowaliśmy indeksów, wartości im można również przypisać korzystając z atrybutupd.DataFrame.index, przypisując do niego listę z indeksami.
df.index = list('abcd')Oczywiście również w tym przypadku długość listy indeksów musi być zgodna z długością kolumny tablicy.
Wygodnym sposobem tworzenia tablic DataFrame jest też przekazanie listy słowników. W takim wypadku każdy słownik odpowiada jednemu wierszowi tablicy, a każdy klucz w słowniku nazwie kolumny, do której zostanie przypisana jego odpowiadająca wartość.
W przypadku, gdy w którymś słowniku brakuje jakiegoś klucza występującego w innych słownikach, do tego pola zostanie przypisane np.NaN.
l = [{'a': 1, 'b': 2, 'c': 3}, {'a': 11, 'b': 12}]
pd.DataFrame(l)Ostatnim z najbardziej popularnych sposobów 'ręcznego' tworzenia tabel DataFrame jest przekazanie dwuwymiarowej tablicy ndarray. Pamiętać jednak należy, że bez dodatkowego przekazania parametrów index i columns, tabela taka zarówno w miejscu indeksów, jak i nazw kolumn będzie posiadała kolejne liczby całkowite liczone od zera.
arr = np.random.rand(4,4)
pd.DataFrame(arr)pd.DataFrame(arr, columns=['A', 'B', 'C', 'D'])Na temat indeksowania czy wybierania wycinków tabel DataFrame powiemy sobie dużo więcej w kolejnym wpisie. Tutaj jedynie chciałbym przybliżyć najprostszy sposób pobierania oraz tworzenia i usuwania kolumn.
Jak zobaczyliśmy wyżej, jednym z najprostszych sposobów tworzenia DataFrames jest przekazanie do konstruktora słownika, który jako wartości zawiera Series. Również, gdy chcemy operować na kolumnach, możemy w uproszczeniu myśleć o nich jak o elementach słownika.
dfPobieranie pojedynczej kolumny z DataFrame znając jej nazwę, wygląda dokładnie tak samo, jak pobieranie wartości ze słownika na podstawie klucza.
Wartość z DataFrame można również pobrać korzystając z metody odpowiadającej nazwie kolumny, dla poniższego przypadku będzie to:
df.fstKażda kolumna ma przypisaną w namespace swoją nazwę do metody, taki zapis jednak może sprawiać problemy, gdy kolumny nazywają się podobnie lub nawet tak samo, jak wbudowane w DataFrame inne metody. Z tego też względu pozostaniemy przy zapisie podobnym do tego znanego ze słowników.
df['fst']Tworzenie nowej kolumny zapisem także przypomina dodawanie elementu do słownika. Jako że kolumny możemy traktować jako Series, działania arytmetyczne na nich wykonywane są jak w ndarray element po elemencie. Możemy więc dzięki temu tworzyć nowe kolumny jako wyniki operacji matematycznych na innych kolumnach, czy za pomocą funkcji uniwersalnych znanych z NumPy. Wystarczy tylko jako nazwę kolumny wybrać nazwę niewystępującą w liście df.columns.
df['trd'] = df['fst'] + df['snd']
dfRównież usuwanie kolumn możemy wykonać dokładnie tak samo, jak w przypadku słowników Python.
del df['trd']
dfNic nie stoi na przeszkodzie, aby utworzyć kolumnę zawierającą ciąg tych samych wartości. Wystarczy do nowo utworzonej kolumny przekazać tę właśnie wartość. Jej długość zostanie dostosowana do długości DataFrame.
df['ones'] = 1.0
dfWszystkie omawiane wyżej sposoby dodawania kolumn powodują dodanie nowej kolumny jako ostatniej w DataFrame. Gdy jednak chcemy, aby kolumna znalazła się w innym miejscu, możemy skorzystać z metody pd.DataFrame.insert(), która przyjmuje trzy podstawowe argumenty:
- miejsce, w którym kolumna ma zostać wstawiona (rozumiane jako indeks w tabeli
df.columns); - nazwę kolumny;
- zawartość kolumny.
df.insert(0, 'zero', [1, 2, 3, 4])
dfRozdział ten nie wyczerpuje oczywiście tematu powstawania obiektów Pandas, jeszcze w tym wpisie omówimy tworzenie DataFrames na podstawie lokalnych plików w różnych, popularnych formatach. W kolejnych wpisach zostaną też przedstawione bardziej zoptymalizowane metody tworzenia wycinków czy przetwarzania danych w Pandas.
Prezentacja danych
Zanim zaczniemy zajmować się większymi tabelami, gdzie niemożliwe jest wyświetlenie całości na ekranie komputera w czytelny sposób, wart opisać pokrótce sposoby prezentacji fragmentów danych. Dwie podstawowe metody służące do tego to df.head() i df.tail(). Jak wskazują nazwy pierwsza z nich, zwraca górę tabeli ('head'), a druga dół ('tail'), czyli ostatnie wiersze. W obu z nich możemy zdefiniować, ile wierszy ma zostać zwróconych, przekazując do metody ich liczbę jako parametr. Gdy nie przekażemy nic, zostanie zwróconych, odpowiednio pięć pierwszych bądź ostatnich wierszy.
df = pd.DataFrame(np.random.rand(8, 4), columns=list('ABCD'))df.head()df.tail(3)Odczytywanie danych z pliku
Ostatnim tematem poruszanym w tym wpisie jest tworzenie obiektów DataFrame zaczytując do naszego programu dane z plików lokalnych. Możliwości czytania danych z plików w Pandas jest bardzo dużo, my skupimy się jednak na trzech podstawowych i najprostszych formatach:
- .csv - pliki tekstowe z wartościami oddzielonymi przecinkiem
- .txt - również pliki textowe, ale z wartościami oddzielonymi znakiem tabulacji
- .xlsx - pliki excel
Zacznijmy od tych, które zwłaszcza na początku pracy z data science będą pojawiać się najczęściej. Jest to format .csv, czyli 'comma separated values'. Większość treningowych zbiorów danych, które można znaleźć w sieci, jest właśnie w tym formacie, stąd umiejętność wczytania tego typu danych do DataFrame jest tak przydatna.
Przykładowy zbiór danych, podobny do tego, z którego tu korzystamy, można znaleźć na stronie www.kaggle.com, a dokładnie tutaj.
Dodam, że strona ta jest świetnym źródłem przykładowych zbiorów danych i wiedzy na temat data science.
Aby pobrać zbiór danych w formacie .csv z pliku, wystarczy do metody pd.read_csv() przekazać względną ścieżkę do tego pliku. W moim przypadku znajduje się on w tym samym folderze co plik jupyter notbooka, do którego go zaczytuje. Jeśli plik jest rzeczywiście tekstem oddzielonym przecinkami, nie musimy robić nic więcej.
df_csv = pd.read_csv('iris.csv')
df_csv.head()Gdy jednak chcemy zaczytać dane z pliku tekstowego .txt, musimy mieć pewność, czym oddzielone są od siebie poszczególne wartości. Zazwyczaj jest to znak tabulacji. Takie dane również zaczytujemy metodą pd.read_csv(), z tą jednak różnicą, że musimy przekazać parametr delimiter='\t', gdzie '\t' oznacza właśnie znak tabulacji.
df_txt = pd.read_csv('iris.txt', delimiter='\t')
df_txt.head()Ostatnim formatem, który chcemy tu omówić, są pliki programu Excel. Pandas posiada dedykowaną dla nich metodę, mianowicie pd.read_excel(). Podobnie jak w powyższej metodzie, musimy przekazać względną ścieżkę do pliku, a do tego nazwę arkusza, z którego zaczytać dane, gdyż jak wiemy, plik .xlsx może mieć wiele arkuszy z tabelarycznymi danymi.
df_xls = pd.read_excel('iris.xlsx', sheet_name='Sheet1')
df_xls.head()Przy odczycie z plików Excel możemy napotkać na błąd związany z nieposiadaniem w naszej instalacji Pythona silnika niezbędnego do odczytu plików .xlsx i podobnych. Komunikat o błędzie poinformuje nas jakiego pakietu brakuje, u mnie było to xlrd. Wystarczy ten pakiet doinstalować za pomocą pip i nie powinniśmy mieć już żadnych problemów z odczytywaniem tego typu danych.
pip install xlrd
Skoro możemy odczytywać pliki Pandas daje nam również możliwość zapisu danych do plików. Często używamy tego, aby zapisać wynikowe datasety, bądź tylko checkpointy, do których możemy wrócić, gdy coś pójdzie nie tak w naszym przetwarzaniu.
Do zapisu plików tekstowych będziemy używać pd.to_csv(), wystarczy do metody przekazać nazwę pliku, który chcemy utworzyć. Gdy dodamy samą nazwę, plik zostanie utworzony w tym samym folderze, co nasz program (w tym wypadku jupyter notebook). Automatycznie metoda zapisze plik rozdzielony przecinkami. Aby nadpisać tę opcję, musimy przekazać w parametrze sep, czym wartości mają być rozdzielone.
df_csv.to_csv('iris2.csv', index=False)df_txt.to_csv('iris2.txt', sep='\t', index=False)Aby Pandas nie dodał do naszego zbioru danych kolumny z indeksami na początku, musimy zdefiniować to w parametrze index=False.
Podsumowanie
Pierwszy wpis na temat pakietu Pandas uważam za zakończony. Omówiliśmy sposoby tworzenia tabel i serii na podstawie innych struktur danych znanych Pythona bądź z NumPy, ale również na podstawie danych z lokalnych plików. W kolejnych wpisach poznamy metody przetwarzania danych, które udostępnia nam Pandas, oraz bardziej zaawansowane struktury danych, jak Index, MultiIndex, TimeSeries, czy Pivot Tables.