#!/usr/bin/env python # coding: utf-8 # Na dnešní lekci si do virtuálního prostředí nainstalujte následující balíčky. # Můžete použít prostředí z lekce o NumPy. # # ```console # $ python -m pip install --upgrade pip # $ python -m pip install notebook pandas matplotlib # ``` # # Pro případ, že by vaše verze `pip`-u neuměla *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áte, zkuste instalaci přímo – *wheels* pro většinu operačních systémů existují – a až kdyby to nefungovalo, instalujte překladače a hlavičky. # # Mezitím co se instaluje, stáhněte si do adresáře `static` potřebné soubory: # [actors.csv](static/actors.csv) a # [spouses.csv](static/spouses.csv). # # A až bude nainstalováno, spusťte si nový Notebook. (Viz [lekce o Notebooku](../notebook/).) # # --- # # 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 specializovanou „datovou” knihovnu – Pandas. # # # Pandas # # Pandas 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.) # Jak bylo řečeno u [NumPy](../numpy/), analytici – cílová skupina této knihovny – mají rádi zkratky. Ve spoustě materiálů na Webu proto najdete `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[1]: import pandas # ## 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. # Nejpoužívanější způsob, jak naplnit první DataFrame, je načtení ze souboru. Na to má Pandas sadu funkcí začínající `read_`. (Některé z nich potřebují další knihovny, viz dokumentace.) # # Jeden z nejpříjemnějších formátů je CSV: # In[2]: actors = pandas.read_csv('static/actors.csv', index_col=None) actors # Případně lze tabulku vytvořit ze seznamu seznamů: # In[3]: items = pandas.DataFrame([ ["Book", 123], ["Computer", 2185], ]) items # …nebo seznamu slovníků: # In[4]: items = pandas.DataFrame([ {"name": "Book", "price": 123}, {"name": "Computer", "price": 2185}, ]) items # V Jupyter Notebooku se tabulka vykreslí „graficky”. # V konzoli se vypíše textově, ale data v ní jsou stejná: # In[5]: print(actors) # Základní informace o tabulce se dají získat metodou `info`: # In[6]: actors.info() # 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. # # Na rozdíl od NumPy jsou typy 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[7]: birth_years = actors['birth'] birth_years # In[8]: type(birth_years) # In[9]: birth_years.name # In[10]: birth_years.index # In[11]: birth_years.dtype # 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[12]: ages = 2016 - birth_years ages # In[13]: century = birth_years // 100 + 1 century # To platí jak pro aritmetické operace (`+`, `-`, `*`, `/`, `//`, `%`, `**`), tak pro porovnávání: # In[14]: birth_years > 1940 # In[15]: birth_years == 1940 # 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[16]: actors['name'] + [' (1)', ' (2)', ' (3)', ' (4)', ' (5)', ' (6)'] # Řetězcové operace se u řetězcových sloupců schovávají pod jmenným prostorem `str`: # In[17]: actors['name'].str.upper() # ... 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[18]: birth_years[2] # In[19]: birth_years[2:-2] # 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[20]: # Roky narození po roce 1940 birth_years[birth_years > 1940] # 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[21]: # Roky narození v daném rozmezí birth_years[(birth_years > 1940) & (birth_years < 1943)] # 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 hledejte v [dokumentaci](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.html). Povědomí o operacích, které sloupce umožňují, je základní znalost datového analytika. # In[22]: 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()) # Zvláště mocná je metoda `apply`, která nám dovoluje aplikovat jakoukoli funkci na všechny hodnoty sloupce: # In[23]: actors['name'].apply(lambda x: ''.join(reversed(x))) # In[24]: actors['alive'].apply({True: 'alive', False: 'deceased'}.get) # ## 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[25]: actors['name'] # Jméno sloupce # In[26]: actors[1:-1] # Interval řádků # In[27]: actors[['name', 'alive']] # Seznam sloupců # 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`. # ### Indexer `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[28]: actors # In[29]: actors.loc[2] # 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 – podobně jako u NumPy: # In[30]: actors.loc[2, 'birth'] # 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[31]: actors.loc[2:4, 'birth':'alive'] # Když uvedeme jen jednu hodnotu, sníží se dimenzionalita – z tabulky na sloupec (případně řádek – taky Series), ze sloupce na skalární hodnotu. Porovnejte: # In[32]: actors.loc[2:4, 'name'] # In[33]: actors.loc[2:4, 'name':'name'] # Chcete-li vybrat sloupec, na místě řádků uveďte dvojtečku – t.j. kompletní interval. # In[34]: actors.loc[:, 'alive'] # Další možnost indexování je seznamem hodnot. Tím se dají řádky či sloupce vybírat, přeskupovat, nebo i duplikovat: # In[35]: actors.loc[:, ['name', 'alive']] # In[36]: actors.loc[[3, 2, 4, 4], :] # ### Indexer `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ů. Funguje tedy jako indexování v NumPy. # In[37]: actors # In[38]: actors.iloc[0, 0] # Protože `iloc` pracuje s čísly, záporná čísla a intervaly fungují jako ve standardním Pythonu: # In[39]: actors.iloc[-1, 1] # In[40]: actors.iloc[:, 0:1] # Indexování seznamem ale funguje jako u `loc`: # In[41]: actors.iloc[[0, -1, 3], [-1, 1, 0]] # Jak `loc` tak `iloc` fungují i na sloupcích (Series), takže se dají kombinovat: # In[42]: actors.iloc[-1].loc['name'] # ## 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. # Načtěte si znovu stejnou tabulku: # In[43]: actors = pandas.read_csv('static/actors.csv', index_col=None) actors # Tato tabulka má dva klíče: jeden pro řádky, `index`, a druhý pro sloupce, který se jmenuje `columns`. # In[44]: actors.index # In[45]: actors.columns # Klíč se dá změnit tím, že do něj přiřadíme sloupec (nebo jinou sekvenci): # In[46]: actors.index = actors['name'] actors # In[47]: actors.index # 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[48]: actors = actors.sort_index() actors # In[49]: actors.loc[['Eric', 'Graham']] # Pozor ale na situaci, kdy hodnoty v klíči nejsou unikátní. To Pandas podporuje, ale chování nemusí být podle vašich představ: # In[50]: actors.loc['Terry'] # 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](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.set_index.html) se dočtete i o dalších možnostech. # Přesuňte teď do klíče dva sloupce najednou: # In[51]: indexed_actors = actors.set_index(['name', 'birth']) indexed_actors # Vznikl tím víceúrovňový klíč: # In[52]: indexed_actors.index # Řá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[53]: indexed_actors.loc['Terry'] # In[54]: indexed_actors.loc['Terry'].loc[1940] # In[55]: indexed_actors.loc[('Terry', 1942)] # 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[56]: indexed_actors # In[57]: last_names = pandas.Series(['Gilliam', 'Jones', 'Cleveland'], index=[('Terry', 1940), ('Terry', 1942), ('Carol', 1942)]) last_names # In[58]: indexed_actors['last_name'] = last_names indexed_actors # ## 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[59]: '(' + indexed_actors['last_name'] + ')' # 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[60]: indexed_actors['last_name'].isnull() # Abychom se `NaN` zbavili, máme dvě možnosti. Buď je zaplníme pomocí metody [`fillna`](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.fillna.html) hodnotou jako `0`, `False` nebo, pro přehlednější výpis, prázdným řetězcem: # In[61]: indexed_actors.fillna('') # Nebo se můžeme zbavit všech řádků, které nějaký `NaN` obsahují: # In[62]: indexed_actors.dropna() # 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[63]: actors = pandas.read_csv('static/actors.csv', index_col=None) actors # In[64]: spouses = pandas.read_csv('static/spouses.csv', index_col=None) spouses # In[65]: actors.merge(spouses) # Mají-li spojované tabulky sloupce stejných jmen, Pandas je spojí podle těchto sloupců. V [dokumentaci](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.merge.html) 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](http://pandas.pydata.org/pandas-docs/stable/comparison_with_sql.html). # ## 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[66]: 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[67]: # Prvních pár řádků (dá se použít i např. head(10), bylo by jich víc) data.head() # In[68]: # Celkový počet řádků len(data) # In[69]: data['sales'].describe() # Pomocí `set_index` nastavíme, které sloupce budeme brát jako hlavičky: # In[70]: indexed = data.set_index(['category', 'month']) indexed.head() # 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](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.unstack.html) k `unstack` a reverzní operaci [`stack`](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.stack.html). # In[71]: unstacked = indexed.unstack('month') unstacked # Teď je sloupcový klíč dvouúrovňový, ale úroveň `sales` je zbytečná. Můžeme se jí zbavit pomocí [`MultiIndex.droplevel`](http://pandas.pydata.org/pandas-docs/version/0.18.0/generated/pandas.MultiIndex.droplevel.html). # In[72]: unstacked.columns = unstacked.columns.droplevel() unstacked # A teď můžeme data analyzovat. Kolik se celkem utratilo za elektroniku? # In[73]: unstacked.loc['Electronics'].sum() # Jak to vypadalo se všemi elektrickými zařízeními v třech konkrétních měsících? # In[74]: unstacked.loc[['Electronics', 'Power Tools'], '2016-03':'2016-05'] # A jak se prodávalo oblečení? # In[75]: unstacked.loc['Clothing'] # 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](http://pandas.pydata.org/pandas-docs/stable/reshaping.html). # ## 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áte-li Jupyter Notebook, zapněte integraci pro kreslení grafů pomocí: # In[76]: import matplotlib # Zapnout zobrazování grafů (procento uvozuje „magickou” zkratku IPythonu): get_ipython().run_line_magic('matplotlib', 'inline') # a pak můžete přímo použít metodu `plot()`, která bez dalších argumentů vynese data z tabulky proti indexu: # In[77]: unstacked.loc['Clothing'].dropna().plot() # Jste-li v příkazové řádce, napřed použij `plot()` a potom se na graf buď podívete, nebo ho uložte: # # ```python # # Setup # import matplotlib.pyplot # # # 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[78]: # 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.fillna(0).cumsum().plot() # In[79]: # 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) # Další informace jsou, jak už to bývá, [v dokumentaci](http://pandas.pydata.org/pandas-docs/version/0.19.0/visualization.html). # ## 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[80]: data.head() # Samotný výsledek `groupby()` je jen objekt: # In[81]: data.groupby('category') # ... na který musíme zavolat příslušnou agregující funkci. Tady je například součet částek podle kategorie: # In[82]: data.groupby('category').sum() # Nebo počet záznamů: # In[83]: data.groupby('category').count() # Groupby umí agregovat podle více sloupců najednou (i když u našeho příkladu nedává velký smysl): # In[84]: data.groupby(['category', 'month']).sum().head() # 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[85]: data.groupby('category').agg(['mean', 'median', sum, pandas.Series.kurtosis]) # Případně použijeme zkratku pro základní analýzu: # In[86]: g = data.groupby('month') g.describe() # 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[87]: 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 # In[88]: by_thousands[('sales', 'sum')].plot()