Na dnešní lekci si do virtuálního prostředí nainstalujte následující balíčky.
$ 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,
spouses.csv a
sales.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.)
Čísla můžeme buď prozkoumávat, hrát si s nimi, zjišťovat zajímavé souvislosti; anebo můžeme připravovat programy, které nějaké výpočty provedou automaticky. 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 vědcům a datovým analytikům, často na úkor robustnosti nebo „dobrých programátorských mravů”. Například některé funkce 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 pandas 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.
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.
Typy jsou v Pandas dynamické: když do sloupce zapíšeme „nekompatibilní”
hodnotu, kterou Pandas neumí převést na aktuální typ sloupce, 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, podobně jako seznam. 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:
# Vytvoření sloupce s věkem v roce 2020
ages = 2020 - birth_years
ages
# Vytvoření sloupce se jménem v závorkách
parenthesized = '(' + actors['name'] + ')'
parenthesized
To platí jak pro aritmetické operátory (+
, -
, *
, /
, //
, %
, **
), 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:
# Sloupec typu bool s "maskou"
birth_years > 1940
# 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, ne kulaté.
Použijeme-li k indexování n-tici, prvním prvkem se indexují řádky a druhým sloupce:
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ů.
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.
Tabulka obsahuje kalendářní data. Výchozí chování fukce read_csv
tato data automaticky nedetekuje, ale pomocí parse_dates
lze nastavit, které sloupce načítat jako kalendářní. A v dalším kódu nám v případech, kdy je jasné že se má nějaká hodnota interpretovat jako datum, Pandas dovolí místo objektů datetime
zadávat data řetězcem.
Mimochodem, tabulka byla získána následujícím kódem:
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])
data = pandas.read_csv('static/sales.csv', index_col=0, parse_dates=['month'])
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)
# Informace např. o datových typech
data.info()
# Statistické informace (u číselných sloupců)
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.
V Notebooku můžete přímo použít metodu plot()
, která bez dalších argumentů vynese data z tabulky proti indexu:
unstacked.loc['Clothing'].dropna().plot()
Ve starších verzích matplotlib/Notebooku je potřeba integraci pro kreslení grafů zapnout pomocí „magické” zkratky IPythonu:
import matplotlib
%matplotlib inline
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.bar()