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.
$ 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 a
spouses.csv.
A až bude nainstalováno, spusťte si nový Notebook. (Viz lekce o Notebooku.)
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í.
Práce datového analytika se většinou drží následujícího postupu:
(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 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, 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.
import pandas
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:
actors = pandas.read_csv('static/actors.csv', index_col=None)
actors
Případně lze tabulku vytvořit ze seznamu seznamů:
items = pandas.DataFrame([
["Book", 123],
["Computer", 2185],
])
items
…nebo seznamu slovníků:
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á:
print(actors)
Základní informace o tabulce se dají získat metodou info:
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.
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:
birth_years = actors['birth']
birth_years
type(birth_years)
birth_years.name
birth_years.index
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:
ages = 2016 - birth_years
ages
century = birth_years // 100 + 1
century
To platí jak pro aritmetické operace (+, -, *, /, //, %, **), tak pro porovnávání:
birth_years > 1940
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.
actors['name'] + [' (1)', ' (2)', ' (3)', ' (4)', ' (5)', ' (6)']
Řetězcové operace se u řetězcových sloupců schovávají pod jmenným prostorem str:
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ů:
birth_years[2]
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:
# 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:
# 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. Povědomí o operacích, které sloupce umožňují, je základní znalost datového analytika.
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:
actors['name'].apply(lambda x: ''.join(reversed(x)))
actors['alive'].apply({True: 'alive', False: 'deceased'}.get)
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á:
actors['name'] # Jméno sloupce
actors[1:-1] # Interval řádků
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.
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.
actors
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:
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.)
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:
actors.loc[2:4, 'name']
actors.loc[2:4, 'name':'name']
Chcete-li vybrat sloupec, na místě řádků uveďte dvojtečku – t.j. kompletní interval.
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:
actors.loc[:, ['name', 'alive']]
actors.loc[[3, 2, 4, 4], :]
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.
actors
actors.iloc[0, 0]
Protože iloc pracuje s čísly, záporná čísla a intervaly fungují jako ve standardním Pythonu:
actors.iloc[-1, 1]
actors.iloc[:, 0:1]
Indexování seznamem ale funguje jako u loc:
actors.iloc[[0, -1, 3], [-1, 1, 0]]
Jak loc tak iloc fungují i na sloupcích (Series), takže se dají kombinovat:
actors.iloc[-1].loc['name']
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:
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.
actors.index
actors.columns
Klíč se dá změnit tím, že do něj přiřadíme sloupec (nebo jinou sekvenci):
actors.index = actors['name']
actors
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:
actors = actors.sort_index()
actors
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:
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 se dočtete i o dalších možnostech.
Přesuňte teď do klíče dva sloupce najednou:
indexed_actors = actors.set_index(['name', 'birth'])
indexed_actors
Vznikl tím víceúrovňový klíč:
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í:
indexed_actors.loc['Terry']
indexed_actors.loc['Terry'].loc[1940]
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:
indexed_actors
last_names = pandas.Series(['Gilliam', 'Jones', 'Cleveland'],
index=[('Terry', 1940), ('Terry', 1942), ('Carol', 1942)])
last_names
indexed_actors['last_name'] = last_names
indexed_actors
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:
'(' + 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():
indexed_actors['last_name'].isnull()
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:
indexed_actors.fillna('')
Nebo se můžeme zbavit všech řádků, které nějaký NaN obsahují:
indexed_actors.dropna()
Bohužel existuje jistá nekonzistence mezi NaN a slovy null či na v názvech funkcí. C'est la vie.
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.
actors = pandas.read_csv('static/actors.csv', index_col=None)
actors
spouses = pandas.read_csv('static/spouses.csv', index_col=None)
spouses
actors.merge(spouses)
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.
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:
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í:
# Prvních pár řádků (dá se použít i např. head(10), bylo by jich víc)
data.head()
# Celkový počet řádků
len(data)
data['sales'].describe()
Pomocí set_index nastavíme, které sloupce budeme brát jako hlavičky:
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 k unstack a reverzní operaci stack.
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.
unstacked.columns = unstacked.columns.droplevel()
unstacked
A teď můžeme data analyzovat. Kolik se celkem utratilo za elektroniku?
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?
unstacked.loc[['Electronics', 'Power Tools'], '2016-03':'2016-05']
A jak se prodávalo oblečení?
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.
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í:
import matplotlib
# Zapnout zobrazování grafů (procento uvozuje „magickou” zkratku IPythonu):
%matplotlib inline
a pak můžete přímo použít metodu plot(), která bez dalších argumentů vynese data z tabulky proti indexu:
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:
# 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:
# 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()
# 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.
Č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.
data.head()
Samotný výsledek groupby() je jen objekt:
data.groupby('category')
... na který musíme zavolat příslušnou agregující funkci. Tady je například součet částek podle kategorie:
data.groupby('category').sum()
Nebo počet záznamů:
data.groupby('category').count()
Groupby umí agregovat podle více sloupců najednou (i když u našeho příkladu nedává velký smysl):
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:
data.groupby('category').agg(['mean', 'median', sum, pandas.Series.kurtosis])
Případně použijeme zkratku pro základní analýzu:
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ů:
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
by_thousands[('sales', 'sum')].plot()