Nauč se Python > Kurzy > MI-PYT > Obsah > Pandas

Analýza dat v Pythonu

Jedna z oblastí, kde popularita Pythonu neustále roste, je analýza dat. Co tenhle termín znamená?

Máme nějaká data; je jich moc a jsou nepřehledná. Datový analytik je zpracuje, přeskládá, najde v nich smysl, vytvoří shrnutí toho nejdůležitějšího nebo barevnou infografiku.

Ze statistických údajů o obyvatelstvu zjistíme, jak souvisí příjmy s dostupností škol. Zpracováním měření z fyzikálního experimentu ověříme, jestli platí hypotéza. Z log přístupů na webovou službu určíme, co uživatelé čtou a kde stránky opouštějí.

Na podobné úkoly je možné použít jazyky vyvinuté přímo pro analýzu dat, jako R, které takovým úkolům svojí syntaxí a filozofií odpovídají víc. Python jako obecný programovací jazyk sice místy vyžaduje krkolomnější zápis, ale zato nabízí možnost data spojit s jinými oblastmi – od získávání informací z webových stránek po tvoření webových či desktopových rozhraní.

Proces analýzy dat

Práce datového analytika se většinou drží následujícího postupu:

  • Formulace otázky, kterou chceme zodpovědět
  • Identifikace dat, která můžeme použít
  • Získání dat (stažení, převod do použitelného formátu)
  • Uložení dat
  • Zkoumání dat
  • Publikace výsledků

*(založeno na diagramu z knihy *Data Wrangling in Python* od Jacqueline Kazil & Katharine Jarmul, str. 3)*

S prvními dvěma kroky Python příliš nepomůže; k těm jen poznamenám, že „Co zajímavého se z těch dat dá vyčíst?” je validní otázka. Na druhé dva kroky se dá s úspěchem použít pythonní standardní knihovna: json, csv, případně doinstalovat requests, lxml pro XML či xlwt/openpyxl na excelové soubory.

Na zkoumání dat a přípravu výsledků pak použijeme specializované „datové” knihovny, se kterými se seznámíme v této a následující lekci.

Podobně jako „Djangonauti” kolem webového frameworku Django tvoří datoví analytici a vědci tvoří podskupinu pythonní komunity s vlastními konferencemi (PyData), organizacemi (NumFocus, Continuum Analytics) a knihovnami jako Pandas, NumPy, SciPy, Matplotlib či Astropy. Potřeby této komunity se samozřejmě odrážejí i v Pythonu samotném – např. ... a @, které si ukážeme příště, byly do jazyka přidány pro ulehčení výpočtů – a naopak – na rozdíl od R nebo Matlabu se tu stále indexuje od nuly. Většina těchto knihoven ale má jednu zvláštnost, kterou ve zbytku pythonního světa tolik nevidíme: důraz na použití v interaktivním režimu.

Nejednoznačnost a zkratky

Data můžeme buď prozkoumávat, hrát si s nimi, zjišťovat zajímavé souvislosti; anebo můžeme připravovat programy, které danou analýzu provedou automaticky (např. vygenerují měsíční zprávu o hospodaření společnosti). Na obojí se používají podobné nástroje. Automaticky pouštěné skripty musí být samozřejmě robustní, nástroje ke zkoumání dat ale bývají přívětivé k analytikovi, často na úkor robustnosti nebo „dobrých programátorských mravů”. Například některé operátory tak trochu „hádají”, co uživatel chtěl, a v tutoriálech se setkáte se zkratkami jako import pandas as pd či dokonce from numpy import *.

Toto je kurz programovací, kde nám záleží více na znovupoužitelném kódu než na jednom konkrétním výsledku. Budeme proto preferovat explicitní a jednoznačné operace. Ty jsou v použitých knihovnách vždy vedle zkratek k dispozici a popsány v dokumentaci.

Jupyter Notebook

Užitečný nástroj, který pythonním datovým analytikům ulehčuje práci, se jmenuje Jupyter Notebook. Je to webová verze pythonní konzole, kde můžeme psát příkazy a kontrolovat výstup.

Na rozdíl od klasické konzole se v Notebooku příkazy (a jejich výstup) ukládají, a je tedy jednoduché se k nim vracet, upravovat je a přidávat komentáře. Mezi příkazy se pak dá psát text ve značkovacím jazyce Markdown, a plynule tak přecházet od pokusů a poznámek přes kód, který se dá sdílet, až po slajdy k prezentaci nebo dokonce publikovatelnou vědeckou práci (na což už jsou ale potřeba další nástroje). V Jupyter Notebooku jsou psány i tyto materiály.

Použití Jupyteru není v tomto kurzu potřeba, ale doporučujeme se s ním seznámit.

Samotný Jupyter je napsaný v Pythonu, ale podporuje i jiné jazyky. Název pochází z JUlia, PYThon, R; kromě nich existují kernely pro desítky dalších jazyků. Pro pythonní verzi stačí z PyPI nainstalovat balíček notebook (nebo jupyter, který „přitáhne” víc funkcionality). Před instalací ale doporučuji aktualizovat samotný pip:

python -m pip install --upgrade pip
python -m pip install notebook

Tato instalace v mnoha případech vyžaduje nainstalované překladače jazyků jako C. Na školních systémech by měly být nainstalovány; v Linuxových distribucích jsou potřeba balíčky jako gcc a python3-devel. Kdyby se instalace nepovedla, potřebná závislost lze většinou dohledat pomocí chybových hlášek.

Nainstalovaný Notebook pusť pomocí:

python -m jupyter notebook

V prohlížeči se otevře stránka se seznamem souborů v aktuálním adresáři; nový notebook se dá vytvořit přes tlačítko NewPython 3.

Jak na Notebook

Nově vytvořený notebook má jednu buňku (cell), do které zapiš kód a stiskni Shift+Enter. Tím se kód vykoná, zobrazí se výstup a vytvoří se nová buňka, kam se dá psát další kód.

Kód se spouští pomocí Shift+Enter (a podobných příkazů); nezáleží na pořadí buněk v dokumentu. Je ale dobré psát buňky tak, aby při postupném spouštění (nebo Run All z menu Cell) kód fungoval – např. dávat importy na začátek.

Notebook je založený na konzoli IPython, která přidává některé vychytávky: doplňování pomocí tab, spouštění shellových příkazů pomocí ! nebo zobrazení nápovědy pomocí zadání ? za výrazem. Vyzkoušej např.:

str.l<TAB>
! ls -a<SHIFT+ENTER>
str.lower?<SHIFT+ENTER>

Kód v buňce může být víceřádkový. Je-li poslední příkaz v buňce výrazem, jeho hodnota se vypíše jako výsledek buňky:

In [1]:
1+1  # Nevypíše se (není poslední příkaz)
2+2  # Vypíše se
Out[1]:
4
In [2]:
seznam = [5, 6, 3, 2]
print(seznam)  # print() funguje
seznam.sort()
seznam         # Poslední výraz se vypíše
[5, 6, 3, 2]
Out[2]:
[2, 3, 5, 6]

Další možnosti, jako např. změna typu buňky na Markdown, jsou dostupné z menu nebo klávesovými zkratkami.

Pandas

První „datová” knihovna, se kterou se seznámíme, se jmenuje Pandas a slouží pro analýzu dat, které lze reprezentovat 2D tabulkou. Tento „tvar” dat najdeme v SQL databázích, souborech CSV nebo tabulkových procesorech. Stručně řečeno, co jde dělat v Excelu, jde dělat i v Pandas. (Pandas má samozřejmě funkce navíc, a hlavně umožňuje analýzu automatizovat.)

Než nainstalujeme Pandas, doporučuji aktualizovat pip. Virtuální prostředí bývají občas vytvářena s verzí, která neumí pracovat s wheels – binárním formátem, ze kterého se instaluje mnohem rychleji než ze zdrojového kódu. Pandas se pak instalují jako ostatní knihovny.

python -m pip install --upgrade pip
python -m pip install pandas

Kromě toho nainstalujeme knihovnu Matplotlib, kterou ke konci lekce využijeme ke kreslení grafů.

python -m pip install matplotlib

Pro případ, že by tvůj pip neuměl wheels nebo na PyPI nebyly příslušné wheel balíčky, je dobré mít na systému nainstalovaný překladač C a Fortranu (např. gcc, gcc-gfortran) a hlavičkové soubory Pythonu (např. python3-devel). Jestli je ale nemáš, zkus instalaci přímo – wheels pro většinu operačních systémů existují – a až kdyby to nefungovalo, instaluj překladače a hlavičky.

In [3]:
# Vykřičníky na začátku těchto řádků značí, že příkazy zadávám do
# systémové příkazové řádky – ne do Pythonu:

! python -m pip install --upgrade pip
! python -m pip install pandas matplotlib
Requirement already up-to-date: pip in ./__venv__/lib/python3.5/site-packages
Requirement already satisfied: pandas in ./__venv__/lib/python3.5/site-packages
Requirement already satisfied: matplotlib in ./__venv__/lib/python3.5/site-packages
Requirement already satisfied: pytz>=2011k in ./__venv__/lib/python3.5/site-packages (from pandas)
Requirement already satisfied: numpy>=1.7.0 in ./__venv__/lib/python3.5/site-packages (from pandas)
Requirement already satisfied: python-dateutil>=2 in ./__venv__/lib/python3.5/site-packages (from pandas)
Requirement already satisfied: cycler in ./__venv__/lib/python3.5/site-packages (from matplotlib)
Requirement already satisfied: pyparsing!=2.0.0,!=2.0.4,!=2.1.2,>=1.5.6 in ./__venv__/lib/python3.5/site-packages (from matplotlib)
Requirement already satisfied: six>=1.5 in ./__venv__/lib/python3.5/site-packages (from python-dateutil>=2->pandas)

Nainstalováno? Můžeme pandas naimportovat.

Jak bylo řečeno v úvodu, analytici – cílová skupina této knihovny – mají rádi zkratky. Ve spoustě materiálů na Webu proto najdeš import pandas as pd, případně rovnou (a bez vysvětlení) použité pd jako zkratku pro pandas. Tento návod ale používá plné jméno.

In [4]:
import pandas

Po importu si ještě stáhněte do svého pracovního adresáře tři potřebné soubory: actors.csv, spouses.csv a style-table.css.

Tabulky

Základní datový typ, který Pandas nabízí, je DataFrame, neboli lidově „tabulka”. Jednotlivé záznamy jsou v ní uvedeny jako řádky a části těchto záznamů jsou úhledně srovnány ve sloupcích. Tabulka se dá vytvořit ze seznamu řádků – taky seznamů. Udělejme si třeba tabulku šesti britských herců:

In [5]:
actors = pandas.DataFrame([
    ["Terry", 1942, True],
    ["Michael", 1965, True],
    ["Eric", 1967, True],
    ["Graham", 1941, False],
    ["Terry", 1940, True],
    ["John", 1939, True],
])
actors
Out[5]:
0 1 2
0 Terry 1942 True
1 Michael 1965 True
2 Eric 1967 True
3 Graham 1941 False
4 Terry 1940 True
5 John 1939 True

V Jupyter Notebooku se tabulka vykreslí „graficky”. V konzoli se vypíše textově, ale data v ní jsou stejná:

In [6]:
print(actors)
         0     1      2
0    Terry  1942   True
1  Michael  1965   True
2     Eric  1967   True
3   Graham  1941  False
4    Terry  1940   True
5     John  1939   True

V Notebooku, který je nakonec jen HTML, si navíc můžeme tabulky pomocí CSS trochu obarvit, aby byly přehlednější:

In [7]:
from IPython.core.display import HTML
with open('style-table.css') as css:
    html = HTML('<style>{}</style>'.format(css.read()))
html
Out[7]:

Jiný způsob, jak vytvořit tabulku, je pomocí seznamu slovníků. Takhle se dají jednotlivé sloupce pojmenovat:

In [8]:
actors = pandas.DataFrame([
    {'name': "Terry", 'birth': 1942, 'alive': True},
    {'name': "Michael", 'birth': 1965, 'alive': True},
    {'name': "Eric", 'birth': 1967, 'alive': True},
    {'name': "Graham", 'birth': 1941, 'alive': False},
    {'name': "Terry", 'birth': 1940, 'alive': True},
    {'name': "John", 'birth': 1939, 'alive': True},
])
actors
Out[8]:
alive birth name
0 True 1942 Terry
1 True 1965 Michael
2 True 1967 Eric
3 False 1941 Graham
4 True 1940 Terry
5 True 1939 John

Asi nejpoužívanější způsob, jak naplnit první DataFrame, je ale načtení ze souboru:

In [9]:
actors = pandas.read_csv('actors.csv', index_col=None)
actors
Out[9]:
name birth alive
0 Terry 1942 True
1 Michael 1943 True
2 Eric 1943 True
3 Graham 1941 False
4 Terry 1940 True
5 John 1939 True

Základní informace o tabulce se dají získat metodou info:

In [10]:
actors.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 3 columns):
name     6 non-null object
birth    6 non-null int64
alive    6 non-null bool
dtypes: bool(1), int64(1), object(1)
memory usage: 182.0+ bytes

Vidíme, že je to tabulka (DataFrame), má 6 řádků indexovaných (pomocí automaticky vygenerovaného indexu) od 0 do 5 a 3 sloupce: jeden s objekty, jeden s int64 a jeden s bool.

Tyto datové typy (dtypes) se doplnily automaticky podle zadaných hodnot. Pandas je používá hlavně pro šetření pamětí: pythonní objekt typu bool zabírá v paměti desítky bytů, ale v bool sloupci si každá hodnota vystačí s jedním bytem.

(Typy jsou dynamické: když do sloupce zapíšeme „nekompatibilní” hodnotu, kterou Pandas neumí převést na daný typ, typ sloupce se automaticky zobecní. Některé automatické převody ovšem nemusí být úplně intuitivní, např. None na NaN.)

Sloupce

Sloupec, neboli Series, je druhý základní datový typ v Pandas. Obsahuje sérii hodnot, jako seznam, ale navíc má jméno, datový typ a „index”, který jednotlivé hodnoty pojmenovává. Sloupce se dají získat vybráním z tabulky:

In [11]:
birth_years = actors['birth']
birth_years
Out[11]:
0    1942
1    1943
2    1943
3    1941
4    1940
5    1939
Name: birth, dtype: int64
In [12]:
type(birth_years)
Out[12]:
pandas.core.series.Series
In [13]:
birth_years.name
Out[13]:
'birth'
In [14]:
birth_years.index
Out[14]:
RangeIndex(start=0, stop=6, step=1)
In [15]:
birth_years.dtype
Out[15]:
dtype('int64')

S informacemi ve sloupcích se dá počítat. Základní aritmetické operace (jako sčítání či dělení) se sloupcem a skalární hodnotou (číslem, řetězcem, ...) provedou danou operaci nad každou hodnotou ve sloupci. Výsledek je nový sloupec:

In [16]:
ages = 2016 - birth_years
ages
Out[16]:
0    74
1    73
2    73
3    75
4    76
5    77
Name: birth, dtype: int64
In [17]:
century = birth_years // 100 + 1
century
Out[17]:
0    20
1    20
2    20
3    20
4    20
5    20
Name: birth, dtype: int64

To platí jak pro aritmetické operace (+, -, *, /, //, %, **), tak pro porovnávání:

In [18]:
birth_years > 1940
Out[18]:
0     True
1     True
2     True
3     True
4    False
5    False
Name: birth, dtype: bool
In [19]:
birth_years == 1940
Out[19]:
0    False
1    False
2    False
3    False
4     True
5    False
Name: birth, dtype: bool

Když sloupec nesečteme se skalární hodnotou (číslem) ale sekvencí, např. seznamem nebo dalším sloupcem, operace se provede na odpovídajících prvcích. Sloupec a druhá sekvence musí mít stejnou délku.

In [20]:
actors['name'] + [' (1)', ' (2)', ' (3)', ' (4)', ' (5)', ' (6)']
Out[20]:
0      Terry (1)
1    Michael (2)
2       Eric (3)
3     Graham (4)
4      Terry (5)
5       John (6)
Name: name, dtype: object

Řetězcové operace se u řetězcových sloupců schovávají pod jmenným prostorem str:

In [21]:
actors['name'].str.upper()
Out[21]:
0      TERRY
1    MICHAEL
2       ERIC
3     GRAHAM
4      TERRY
5       JOHN
Name: name, dtype: object

... a operace s daty a časy (datetime) najdeme pod dt.

Ze slupců jdou vybírat prvky či podsekvence podobně jako třeba ze seznamů:

In [22]:
birth_years[2]
Out[22]:
1943
In [23]:
birth_years[2:-2]
Out[23]:
2    1943
3    1941
Name: birth, dtype: int64

A navíc je lze vybírat pomocí sloupce typu bool, což vybere ty záznamy, u kterých je odpovídající hodnota true. Tak lze rychle vybrat hodnoty, které odpovídají nějaké podmínce:

In [24]:
# Roky narození po roce 1940
birth_years[birth_years > 1940]
Out[24]:
0    1942
1    1943
2    1943
3    1941
Name: birth, dtype: int64

Protože Python neumožňuje předefinovat chování operátorů and a or, logické spojení operací se tradičně dělá přes bitové operátory & (a) a | (nebo). Ty mají ale neintuitivní prioritu, proto se jednotlivé výrazy hodí uzavřít do závorek:

In [25]:
# Roky narození v daném rozmezí
birth_years[(birth_years > 1940) & (birth_years < 1943)]
Out[25]:
0    1942
3    1941
Name: birth, dtype: int64

Sloupce mají zabudovanou celou řadu operací, od základních (např. column.sum(), která bývá rychlejší než vestavěná funkce sum()) po roztodivné statistické specialitky. Kompletní seznam hledej v dokumentaci. Povědomí o operacích, které sloupce umožňují, je základní znalost datového analytika.

In [26]:
print('Součet: ', birth_years.sum())
print('Průměr: ', birth_years.mean())
print('Medián: ', birth_years.median())
print('Počet unikátních hodnot: ', birth_years.nunique())
print('Koeficient špičatosti: ', birth_years.kurtosis())
Součet:  11648
Průměr:  1941.33333333
Medián:  1941.5
Počet unikátních hodnot:  5
Koeficient špičatosti:  -1.48125

Zvláště mocná je metoda apply, která nám dovoluje aplikovat jakoukoli funkci na všechny hodnoty sloupce:

In [27]:
actors['name'].apply(lambda x: ''.join(reversed(x)))
Out[27]:
0      yrreT
1    leahciM
2       cirE
3     maharG
4      yrreT
5       nhoJ
Name: name, dtype: object
In [28]:
actors['alive'].apply({True: 'alive', False: 'deceased'}.get)
Out[28]:
0       alive
1       alive
2       alive
3    deceased
4       alive
5       alive
Name: alive, dtype: object

Tabulky a vybírání prvků

Prvky ze sloupců jdou vybírat jako u seznamů. Ale z tabulek v Pandas jde vybírat spoustou různých způsobů. Tradiční hranaté závorky plní několik funkcí najednou, takže někdy není na první pohled jasné, co jaké indexování znamená:

In [29]:
actors['name']  # Jméno sloupce
Out[29]:
0      Terry
1    Michael
2       Eric
3     Graham
4      Terry
5       John
Name: name, dtype: object
In [30]:
actors[1:-1]  # Interval řádků
Out[30]:
name birth alive
1 Michael 1943 True
2 Eric 1943 True
3 Graham 1941 False
4 Terry 1940 True
In [31]:
actors[['name', 'alive']]  # Seznam sloupců
Out[31]:
name alive
0 Terry True
1 Michael True
2 Eric True
3 Graham False
4 Terry True
5 John True

Toto je příklad nejednoznačného chování, které zjednodušuje život datovým analytikům, pro které je knihovna Pandas primárně určena.

My, coby programátoři píšící robustní kód, budeme čisté indexování ([]) používat jen pro výběr sloupců podle jména. Pro ostatní přístup použijeme tzv. indexery, jako loc a iloc.

loc

Indexer loc zprostředkovává primárně řádky, a to podle indexu, tedy hlaviček tabulky. V našem příkladu jsou řádky očíslované a sloupce pojmenované, ale dále uvidíme, že v obou indexech můžou být jakékoli hodnoty.

In [32]:
actors
Out[32]:
name birth alive
0 Terry 1942 True
1 Michael 1943 True
2 Eric 1943 True
3 Graham 1941 False
4 Terry 1940 True
5 John 1939 True
In [33]:
actors.loc[2]
Out[33]:
name     Eric
birth    1943
alive    True
Name: 2, dtype: object

Všimněte si, že loc není metoda: používají se s ním hranaté závorky.

Použijeme-li k indexování n-tici, prvním prvkem se indexují řádky a druhým sloupce:

In [34]:
actors.loc[2, 'birth']
Out[34]:
1943

Na obou pozicích může být „interval”, ale na rozdíl od klasického Pythonu jsou ve výsledku obsaženy obě koncové hodnoty. (S indexem, který nemusí být vždy číselný, to dává smysl.)

In [35]:
actors.loc[2:4, 'alive':'birth']
Out[35]:
2
3
4

Když uvedeme jen jednu hodnotu, sníží se dimenzionalita – z tabulky na sloupec (případně řádek – taky Series), ze sloupce na skalární hodnotu. Porovnej:

In [36]:
actors.loc[2:4, 'name']
Out[36]:
2      Eric
3    Graham
4     Terry
Name: name, dtype: object
In [37]:
actors.loc[2:4, 'name':'name']
Out[37]:
name
2 Eric
3 Graham
4 Terry

Chceš-li vybrat sloupec, na místě řádků uveď dvojtečku – t.j. interval obsahující všechno.

In [38]:
actors.loc[:, 'alive']
Out[38]:
0     True
1     True
2     True
3    False
4     True
5     True
Name: alive, dtype: bool

Další možnost indexování je seznamem hodnot. Tím se dají řádky či sloupce vybírat, přeskupovat, nebo i duplikovat:

In [39]:
actors.loc[:, ['name', 'alive']]
Out[39]:
name alive
0 Terry True
1 Michael True
2 Eric True
3 Graham False
4 Terry True
5 John True
In [40]:
actors.loc[[3, 2, 4, 4], :]
Out[40]:
name birth alive
3 Graham 1941 False
2 Eric 1943 True
4 Terry 1940 True
4 Terry 1940 True

iloc

Druhý indexer, který si v krátkosti ukážeme, je iloc. Umí to samé co loc, jen nepracuje s klíčem, ale s pozicemi řádků či sloupců.

In [41]:
actors
Out[41]:
name birth alive
0 Terry 1942 True
1 Michael 1943 True
2 Eric 1943 True
3 Graham 1941 False
4 Terry 1940 True
5 John 1939 True
In [42]:
actors.iloc[0, 0]
Out[42]:
'Terry'

Protože iloc pracuje s čísly, záporná čísla a intervaly fungují jako ve standardním Pythonu:

In [43]:
actors.iloc[-1, 1]
Out[43]:
1939
In [44]:
actors.iloc[:, 0:1]
Out[44]:
name
0 Terry
1 Michael
2 Eric
3 Graham
4 Terry
5 John

Indexování seznamem ale funguje jako u loc:

In [45]:
actors.iloc[[0, -1, 3], [-1, 1, 0]]
Out[45]:
alive birth name
0 True 1942 Terry
5 True 1939 John
3 False 1941 Graham

Jak loc tak iloc fungují i na sloupcích (Series), takže se dají kombinovat:

In [46]:
actors.iloc[-1].loc['name']
Out[46]:
'John'

at a iat

Chceme-li získat jedno políčko tabulky, můžeme použít rychlejší at (přes klíč) a iat (přes pozici):

In [47]:
actors.at[0, 'name']
Out[47]:
'Terry'
In [48]:
actors.iat[0, 1]
Out[48]:
1942

Indexy

V minulé sekci jsme naťukli indexy – jména jednotlivých sloupců nebo řádků. Teď se podívejme, co všechno s nimi lze dělat. Vytvořím si znovu stejnou tabulku:

In [49]:
actors = pandas.read_csv('actors.csv', index_col=None)
actors
Out[49]:
name birth alive
0 Terry 1942 True
1 Michael 1943 True
2 Eric 1943 True
3 Graham 1941 False
4 Terry 1940 True
5 John 1939 True

Tato tabulka má dva klíče: jeden pro řádky, index, a druhý pro sloupce, který se jmenuje columns.

In [50]:
actors.index
Out[50]:
RangeIndex(start=0, stop=6, step=1)
In [51]:
actors.columns
Out[51]:
Index(['name', 'birth', 'alive'], dtype='object')

Klíč se dá změnit tím, že do něj přiřadíme sloupec (nebo jinou sekvenci):

In [52]:
actors.index = actors['name']
actors
Out[52]:
name birth alive
name
Terry Terry 1942 True
Michael Michael 1943 True
Eric Eric 1943 True
Graham Graham 1941 False
Terry Terry 1940 True
John John 1939 True
In [53]:
actors.index
Out[53]:
Index(['Terry', 'Michael', 'Eric', 'Graham', 'Terry', 'John'], dtype='object', name='name')

Potom jde pomocí tohoto klíče vyhledávat. Chceme-li vyhledávat efektivně (což dává smysl, pokud by řádků byly miliony), je dobré nejdřív tabulku podle indexu seřadit:

In [54]:
actors = actors.sort_index()
actors
Out[54]:
name birth alive
name
Eric Eric 1943 True
Graham Graham 1941 False
John John 1939 True
Michael Michael 1943 True
Terry Terry 1942 True
Terry Terry 1940 True
In [55]:
actors.loc[['Eric', 'Graham']]
Out[55]:
name birth alive
name
Eric Eric 1943 True
Graham Graham 1941 False

Pozor ale na situaci, kdy hodnoty v klíči nejsou unikátní. To Pandas podporuje, ale chování nemusí být podle našich představ:

In [56]:
actors.loc['Terry']
Out[56]:
name birth alive
name
Terry Terry 1942 True
Terry Terry 1940 True

Trochu pokročilejší možnost, jak klíč nastavit, je metoda set_index. Nejčastěji se používá k přesunutí sloupců do klíče, ale v dokumentaci se dočtete i o dalších možnostech. Přesuňme teď do klíče dva sloupce najednou:

In [57]:
indexed_actors = actors.set_index(['name', 'birth'])
indexed_actors
Out[57]:
alive
name birth
Eric 1943 True
Graham 1941 False
John 1939 True
Michael 1943 True
Terry 1942 True
1940 True

Vznikl tím víceúrovňový klíč:

In [58]:
indexed_actors.index
Out[58]:
MultiIndex(levels=[['Eric', 'Graham', 'John', 'Michael', 'Terry'], [1939, 1940, 1941, 1942, 1943]],
           labels=[[0, 1, 2, 3, 4, 4], [4, 2, 0, 4, 3, 1]],
           names=['name', 'birth'])

Řádky z tabulky s víceúrovňovým klíčem se dají vybírat buď postupně po jednotlivých úrovních, nebo n-ticí:

In [59]:
indexed_actors.loc['Terry']
Out[59]:
alive
birth
1942 True
1940 True
In [60]:
indexed_actors.loc['Terry'].loc[1940]
Out[60]:
alive    True
Name: 1940, dtype: bool
In [61]:
indexed_actors.loc[('Terry', 1942)]
Out[61]:
alive    True
Name: (Terry, 1942), dtype: bool

Kromě výběru dat mají klíče i jinou vlastnost: přidáme-li do tabulky nový sloupec s klíčem, jednotlivé řádky se seřadí podle něj:

In [62]:
indexed_actors
Out[62]:
alive
name birth
Eric 1943 True
Graham 1941 False
John 1939 True
Michael 1943 True
Terry 1942 True
1940 True
In [63]:
last_names = pandas.Series(['Gilliam', 'Jones', 'Cleveland'],
                           index=[('Terry', 1940), ('Terry', 1942), ('Carol', 1942)])
last_names
Out[63]:
(Terry, 1940)      Gilliam
(Terry, 1942)        Jones
(Carol, 1942)    Cleveland
dtype: object
In [64]:
indexed_actors['last_name'] = last_names
indexed_actors
Out[64]:
alive last_name
name birth
Eric 1943 True NaN
Graham 1941 False NaN
John 1939 True NaN
Michael 1943 True NaN
Terry 1942 True Jones
1940 True Gilliam

NaN neboli NULL či N/A

V posledním příkladu vidíme, že Pandas doplňuje za neznámé hodnoty NaN, tedy "Not a Number" – hodnotu, která plní podobnou funkci jako NULL v SQL nebo None v Pythonu. Znamená, že daná informace chybí, není k dispozici nebo ani nedává smysl ji mít. Naprostá většina operací s NaN dává opět NaN:

In [65]:
'(' + indexed_actors['last_name'] + ')'
Out[65]:
name     birth
Eric     1943           NaN
Graham   1941           NaN
John     1939           NaN
Michael  1943           NaN
Terry    1942       (Jones)
         1940     (Gilliam)
Name: last_name, dtype: object

NaN se chová divně i při porovnávání; (NaN == NaN) je nepravda. Pro zjištění chybějících hodnot máme metodu isnull():

In [66]:
indexed_actors['last_name'].isnull()
Out[66]:
name     birth
Eric     1943      True
Graham   1941      True
John     1939      True
Michael  1943      True
Terry    1942     False
         1940     False
Name: last_name, dtype: bool

Abychom se NaN zbavili, máme dvě možnosti. Buď je zaplníme pomocí metody fillna hodnotou jako 0, False nebo, pro přehlednější výpis, prázdným řetězcem:

In [67]:
indexed_actors.fillna('')
Out[67]:
alive last_name
name birth
Eric 1943 True
Graham 1941 False
John 1939 True
Michael 1943 True
Terry 1942 True Jones
1940 True Gilliam

Nebo se můžeme zbavit všech řádků, které nějaký NaN obsahují:

In [68]:
indexed_actors.dropna()
Out[68]:
alive last_name
name birth
Terry 1942 True Jones
1940 True Gilliam

Bohužel existuje jistá nekonzistence mezi NaN a slovy null či na v názvech funkcí. C'est la vie.

Merge

Někdy se stane, že máme více souvisejících tabulek, které je potřeba spojit dohromady. Na to mají DataFrame metodu merge(), která umí podobné operace jako JOIN v SQL.

In [69]:
actors = pandas.read_csv('actors.csv', index_col=None)
actors
Out[69]:
name birth alive
0 Terry 1942 True
1 Michael 1943 True
2 Eric 1943 True
3 Graham 1941 False
4 Terry 1940 True
5 John 1939 True
In [70]:
spouses = pandas.read_csv('spouses.csv', index_col=None)
spouses
Out[70]:
name birth spouse_name
0 Graham 1941 David Sherlock
1 John 1939 Connie Booth
2 John 1939 Barbara Trentham
3 John 1939 Alyce Eichelberger
4 John 1939 Jennifer Wade
5 Terry 1940 Maggie Westo
6 Eric 1943 Lyn Ashley
7 Eric 1943 Tania Kosevich
8 Terry 1942 Alison Telfer
9 Terry 1942 Anna Söderström
10 Michael 1943 Helen Gibbins
In [71]:
actors.merge(spouses)
Out[71]:
name birth alive spouse_name
0 Terry 1942 True Alison Telfer
1 Terry 1942 True Anna Söderström
2 Michael 1943 True Helen Gibbins
3 Eric 1943 True Lyn Ashley
4 Eric 1943 True Tania Kosevich
5 Graham 1941 False David Sherlock
6 Terry 1940 True Maggie Westo
7 John 1939 True Connie Booth
8 John 1939 True Barbara Trentham
9 John 1939 True Alyce Eichelberger
10 John 1939 True Jennifer Wade

Mají-li spojované tabulky sloupce stejných jmen, Pandas je spojí podle těchto sloupců. V dokumentaci se dá zjistit, jak explicitně určit podle kterých klíčů spojovat, co udělat když v jedné z tabulek chybí odpovídající hodnoty apod.

Fanoušky SQL ještě odkážu na porovnání mezi SQL a Pandas.

Přesýpání dat

Dostáváme se do bodu, kdy nám jednoduchá tabulka přestává stačit. Pojďme si vytvořit tabulku větší: fiktivních prodejů v e-shopu, ve formátu jaký bychom mohli dostat z SQL databáze nebo datového souboru.

Použijeme k tomu mimo jiné date_range, která vytváří kalendářní intervaly. Zde, i v jiných případech, kdy je jasné, že se má nějaká hodnota interpretovat jako datum, nám Pandas dovolí místo objektů datetime zadávat data řetězcem:

In [72]:
import itertools
import random
random.seed(0)

months = pandas.date_range('2015-01', '2016-12', freq='M')
categories = ['Electronics', 'Power Tools', 'Clothing']
data = pandas.DataFrame([{'month': a, 'category': b, 'sales': random.randint(-1000, 10000)}
                          for a, b in itertools.product(months, categories)
                          if random.randrange(20) > 0])

Tabulka je celkem dlouhá (i když v analýze dat bývají ještě delší). Podívejme se na několik obecných informací:

In [73]:
# Prvních pár řádků (dá se použít i např. head(10), bylo by jich víc)
data.head()
Out[73]:
category month sales
0 Electronics 2015-01-31 5890
1 Power Tools 2015-01-31 3242
2 Clothing 2015-01-31 6961
3 Electronics 2015-02-28 3969
4 Power Tools 2015-02-28 4866
In [74]:
# Celkový počet řádků
len(data)
Out[74]:
67
In [75]:
data['sales'].describe()
Out[75]:
count      67.000000
mean     4795.552239
std      3101.026552
min      -735.000000
25%      2089.000000
50%      4448.000000
75%      7874.000000
max      9817.000000
Name: sales, dtype: float64

Pomocí set_index nastavíme, které sloupce budeme brát jako hlavičky:

In [76]:
indexed = data.set_index(['category', 'month'])
indexed.head()
Out[76]:
sales
category month
Electronics 2015-01-31 5890
Power Tools 2015-01-31 3242
Clothing 2015-01-31 6961
Electronics 2015-02-28 3969
Power Tools 2015-02-28 4866

Budeme-li chtít z těchto dat vytvořit tabulku, která má v řádcích kategorie a ve sloupcích měsíce, můžeme využít metodu unstack, která "přesune" vnitřní úroveň indexu řádků do sloupců a uspořádá podle toho i data.

Můžeme samozřejmě použít kteroukoli úroveň klíče; viz dokumentace k unstack a reverzní operaci stack.

In [77]:
unstacked = indexed.unstack('month')
unstacked
Out[77]:
sales
month 2015-01-31 2015-02-28 2015-03-31 2015-04-30 2015-05-31 2015-06-30 2015-07-31 2015-08-31 2015-09-30 2015-10-31 ... 2016-02-29 2016-03-31 2016-04-30 2016-05-31 2016-06-30 2016-07-31 2016-08-31 2016-09-30 2016-10-31 2016-11-30
category
Clothing 6961.0 2578.0 9131.0 618.0 4796.0 8052.0 7989.0 NaN 31.0 7896.0 ... 4194.0 2059.0 471.0 5410.0 8663.0 9817.0 6969.0 -735.0 4448.0 -259.0
Electronics 5890.0 3969.0 1281.0 7725.0 4409.0 4180.0 6253.0 NaN 7086.0 8298.0 ... 6290.0 2966.0 9039.0 1450.0 3515.0 8497.0 349.0 9324.0 919.0 18.0
Power Tools 3242.0 4866.0 1289.0 1407.0 8171.0 9492.0 3267.0 5534.0 2996.0 2909.0 ... 8769.0 2012.0 6807.0 314.0 2858.0 6382.0 9039.0 2119.0 5095.0 1397.0

3 rows × 23 columns

Teď je sloupcový klíč dvouúrovňový, ale úroveň sales je zbytečná. Můžeme se jí zbavit pomocí MultiIndex.droplevel.

In [78]:
unstacked.columns = unstacked.columns.droplevel()
unstacked
Out[78]:
month 2015-01-31 00:00:00 2015-02-28 00:00:00 2015-03-31 00:00:00 2015-04-30 00:00:00 2015-05-31 00:00:00 2015-06-30 00:00:00 2015-07-31 00:00:00 2015-08-31 00:00:00 2015-09-30 00:00:00 2015-10-31 00:00:00 ... 2016-02-29 00:00:00 2016-03-31 00:00:00 2016-04-30 00:00:00 2016-05-31 00:00:00 2016-06-30 00:00:00 2016-07-31 00:00:00 2016-08-31 00:00:00 2016-09-30 00:00:00 2016-10-31 00:00:00 2016-11-30 00:00:00
category
Clothing 6961.0 2578.0 9131.0 618.0 4796.0 8052.0 7989.0 NaN 31.0 7896.0 ... 4194.0 2059.0 471.0 5410.0 8663.0 9817.0 6969.0 -735.0 4448.0 -259.0
Electronics 5890.0 3969.0 1281.0 7725.0 4409.0 4180.0 6253.0 NaN 7086.0 8298.0 ... 6290.0 2966.0 9039.0 1450.0 3515.0 8497.0 349.0 9324.0 919.0 18.0
Power Tools 3242.0 4866.0 1289.0 1407.0 8171.0 9492.0 3267.0 5534.0 2996.0 2909.0 ... 8769.0 2012.0 6807.0 314.0 2858.0 6382.0 9039.0 2119.0 5095.0 1397.0

3 rows × 23 columns

A teď můžeme data analyzovat. Kolik se celkem utratilo za elektroniku?

In [79]:
unstacked.loc['Electronics'].sum()
Out[79]:
103742.0

Jak to vypadalo se všemi elektrickými zařízeními v třech konkrétních měsících?

In [80]:
unstacked.loc[['Electronics', 'Power Tools'], '2016-03':'2016-05']
Out[80]:
month 2016-03-31 00:00:00 2016-04-30 00:00:00 2016-05-31 00:00:00
category
Electronics 2966.0 9039.0 1450.0
Power Tools 2012.0 6807.0 314.0

A jak se prodávalo oblečení?

In [81]:
unstacked.loc['Clothing']
Out[81]:
month
2015-01-31    6961.0
2015-02-28    2578.0
2015-03-31    9131.0
2015-04-30     618.0
2015-05-31    4796.0
2015-06-30    8052.0
2015-07-31    7989.0
2015-08-31       NaN
2015-09-30      31.0
2015-10-31    7896.0
2015-11-30    7016.0
2015-12-31    7969.0
2016-01-31    8627.0
2016-02-29    4194.0
2016-03-31    2059.0
2016-04-30     471.0
2016-05-31    5410.0
2016-06-30    8663.0
2016-07-31    9817.0
2016-08-31    6969.0
2016-09-30    -735.0
2016-10-31    4448.0
2016-11-30    -259.0
Name: Clothing, dtype: float64

Metody stack a unstack jsou sice asi nejužitečnější, ale stále jen jeden ze způsobů jak v Pandas tabulky přeskládávat. Náročnější studenti najdou další možnosti v dokumentaci.

Grafy

Je-li nainstalována knihovna matplotlib, Pandas ji umí využít k tomu, aby kreslil grafy. Nastavení je trochu jiné pro Jupyter Notebook a pro příkazovou řádku.

Používáš-li Jupyter Notebook, zapni integraci pro kreslení grafů pomocí:

In [82]:
import matplotlib
import matplotlib.style
matplotlib.style.use('ggplot')

# Zapnout zobrazování grafů (procento uvozuje „magickou” zkratku IPythonu):
%matplotlib inline

a pak můžeš přímo použít metodu plot(), která bez dalších argumentů vynese data z tabulky proti indexu:

In [83]:
unstacked.loc['Clothing'].dropna().plot()
Out[83]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f050fd4ef60>

Jsi-li v příkazové řádce, napřed použij plot() a potom se na graf buď podívej, nebo ho ulož:

# Setup
import matplotlib.pyplot
matplotlib.style.use('ggplot')

# Plot
unstacked.loc['Clothing'].plot()
matplotlib.pyplot.show()
matplotlib.pyplot.savefig('graph.png')

Funkce show a savefig pracují s „aktuálním” grafem – typicky posledním, který se vykreslil. Pozor na to, že funkce savefig aktuální graf zahodí; před dalším show nebo savefig je potřeba ho vykreslit znovu.

V kombinaci s dalšími funkcemi Series a DataFrame umožňují grafy získat o datech rychlý přehled:

In [84]:
# Jak se postupně vyvíjely zisky z oblečení?
# `.T` udělá transpozici tabulky (vymění řádky a sloupce)
# `cumsum()` spočítá průběžný součet po sloupcích
unstacked.T.dropna().cumsum().plot()
Out[84]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f04e2854390>
In [85]:
# Jak si proti sobě stály jednotlivé kategorie v březnu, dubnu a květnu 2016?
unstacked.loc[:, '2016-03':'2016-05'].plot.bar(legend=False)
Out[85]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f04e079db00>

Další informace jsou, jak už to bývá, v dokumentaci.

Groupby

Často používaná operace pro zjednodušení tabulky je groupby, která sloučí dohromady řádky se stejnou hodnotou v některém sloupci a sloučená data nějak agreguje.

In [86]:
data.head()
Out[86]:
category month sales
0 Electronics 2015-01-31 5890
1 Power Tools 2015-01-31 3242
2 Clothing 2015-01-31 6961
3 Electronics 2015-02-28 3969
4 Power Tools 2015-02-28 4866

Samotný výsledek groupby() je jen objekt:

In [87]:
data.groupby('category')
Out[87]:
<pandas.core.groupby.DataFrameGroupBy object at 0x7f04e0773dd8>

... na který musíme zavolat příslušnou agregující funkci. Tady je například součet částek podle kategorie:

In [88]:
data.groupby('category').sum()
Out[88]:
sales
category
Clothing 112701
Electronics 103742
Power Tools 104859

Nebo počet záznamů:

In [89]:
data.groupby('category').count()
Out[89]:
month sales
category
Clothing 22 22
Electronics 22 22
Power Tools 23 23

Groupby umí agregovat podle více sloupců najednou (i když u našeho příkladu nedává velký smysl):

In [90]:
data.groupby(['category', 'month']).sum().head()
Out[90]:
sales
category month
Clothing 2015-01-31 6961
2015-02-28 2578
2015-03-31 9131
2015-04-30 618
2015-05-31 4796

Chceme-li aplikovat více funkcí najednou, předáme jejich seznam metodě agg. Časté funkce lze předat jen jménem, jinak předáme funkci či metodu přímo:

In [91]:
data.groupby('category').agg(['mean', 'median', sum, pandas.Series.kurtosis])
Out[91]:
sales
mean median sum kurt
category
Clothing 5122.772727 6185.5 112701 -1.298035
Electronics 4715.545455 4294.5 103742 -1.353210
Power Tools 4559.086957 3769.0 104859 -1.044767

Případně použijeme zkratku pro základní analýzu:

In [92]:
g = data.groupby('month')
g.describe().unstack(0)
Out[92]:
sales
month 2015-01-31 2015-02-28 2015-03-31 2015-04-30 2015-05-31 2015-06-30 2015-07-31 2015-08-31 2015-09-30 2015-10-31 ... 2016-02-29 2016-03-31 2016-04-30 2016-05-31 2016-06-30 2016-07-31 2016-08-31 2016-09-30 2016-10-31 2016-11-30
count 3.000000 3.000000 3.000000 3.000000 3.0000 3.000000 3.000000 1.0 3.00000 3.000000 ... 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000
mean 5364.333333 3804.333333 3900.333333 3250.000000 5792.0000 7241.333333 5836.333333 5534.0 3371.00000 6367.666667 ... 6417.666667 2345.666667 5439.000000 2391.333333 5012.000000 8232.000000 5452.333333 3569.333333 3487.333333 385.333333
std 1914.414880 1152.853995 4529.891978 3895.490855 2069.3412 2747.220656 2388.415653 NaN 3542.41796 3002.029702 ... 2290.170372 537.738164 4444.797408 2675.235566 3178.877632 1732.765131 4539.188621 5183.962802 2247.644174 887.008643
min 3242.000000 2578.000000 1281.000000 618.000000 4409.0000 4180.000000 3267.000000 5534.0 31.00000 2909.000000 ... 4194.000000 2012.000000 471.000000 314.000000 2858.000000 6382.000000 349.000000 -735.000000 919.000000 -259.000000
25% 4566.000000 3273.500000 1285.000000 1012.500000 4602.5000 6116.000000 4760.000000 5534.0 1513.50000 5402.500000 ... 5242.000000 2035.500000 3639.000000 882.000000 3186.500000 7439.500000 3659.000000 692.000000 2683.500000 -120.500000
50% 5890.000000 3969.000000 1289.000000 1407.000000 4796.0000 8052.000000 6253.000000 5534.0 2996.00000 7896.000000 ... 6290.000000 2059.000000 6807.000000 1450.000000 3515.000000 8497.000000 6969.000000 2119.000000 4448.000000 18.000000
75% 6425.500000 4417.500000 5210.000000 4566.000000 6483.5000 8772.000000 7121.000000 5534.0 5041.00000 8097.000000 ... 7529.500000 2512.500000 7923.000000 3430.000000 6089.000000 9157.000000 8004.000000 5721.500000 4771.500000 707.500000
max 6961.000000 4866.000000 9131.000000 7725.000000 8171.0000 9492.000000 7989.000000 5534.0 7086.00000 8298.000000 ... 8769.000000 2966.000000 9039.000000 5410.000000 8663.000000 9817.000000 9039.000000 9324.000000 5095.000000 1397.000000

8 rows × 23 columns

A perlička nakonec – agregovat se dá i podle sloupců, které nejsou v tabulce. Následující kód rozloží data na slabé, průměrné a silné měsíce podle toho, kolik jsme v daném měsíci vydělali celých tisícikorun, a zjistí celkový zisk ze slabých, průměrných a silných měsíců:

In [93]:
bin_size = 10000
by_month = data.groupby('month').sum()
by_thousands = by_month.groupby(by_month['sales'] // bin_size * bin_size).agg(['count', 'sum'])
by_thousands
Out[93]:
sales
count sum
sales
0 5 30651
10000 15 218870
20000 3 71781
In [94]:
by_thousands[('sales', 'sum')].plot()
Out[94]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f04e2832e80>