Další čtení pro dlouhé večery v tomto ročním období:
import pandas as pd
%matplotlib inline
Jedním ze základních postupů v datové analýze je rozdělení dat do skupin, aplikace nějaké operace na jednotlivé skupiny a nakonec kombinace výsledků do vhodného datasetu. Anglicky se tento postup označuje jako split-apply-combine. Skupiny jsou často, byť ne nutně, definovány nějakou (kategorickou) proměnnou, např. by to mohla být barva, pohlaví nebo kontinent. Skupiny lze ale vytvářet i odvozováním, např. pomocí rozsahu nebo nebo vlastností časových řad. Oboje už jsme vlastně viděli v EDA 3, kdy jsme skupiny vytvářeli pro deštivé dny nebo jednotlivé roky.
Pojďme si to vysvětlit prakticky. Použijeme k tomu hezký dataset s údaji z amerického Kongresu, který obsahuje seznam všech mužů a žen, kteří kdy v jedné z jeho komor (Sněmovně nebo Senátu) zasedli.
# odkaz přímo na csv soubor na internetu
LEGISLATORS_HISTORICAL_URL_CSV = (
"https://theunitedstates.io/congress-legislators/legislators-historical.csv"
)
legislators_historical = pd.read_csv(
LEGISLATORS_HISTORICAL_URL_CSV,
usecols=["gender", "type", "state", "party", "birthday", "first_name", "last_name"],
parse_dates=["birthday"],
)
legislators_historical
Na rozdělení dat do skupin slouží metoda groupby
. Nejjednodušší a možná i nejčastější použití je seskupení podle existujícího sloupce v tabulce. Třeba podle type
, tedy u nás konkrétně podle komory: rep
je Sněmovna reprezentatntů, sen
je Senát.
legislators_historical_by_type = legislators_historical.groupby("type")
legislators_historical_by_type
Dostali jsme objekt typu GroupBy
. Pokud vám to připomíná DatetimeIndexResampler
z Explorativní analýzy a statistiky jedné proměnné, je to velice dobrá asociace. Tato operace totiž data jen rozdělila do skupin, ještě jsme ale neřekli, co s těmi skupinami chceme dělat.
Na skupiny, které jsme vytvořili pomocí groupby
, teď můžeme aplikovat nějakou operaci. To je právě krok apply. Nemá pro nás moc cenu oddělovat tento krok od třetího kroku combine, protože když už nějakou opraci aplikujeme, tak bychom rádi výsledek sestavili do výsledného datasetu. Pandas navíc tyto kroky sám nijak neodděluje.
Pozn.: Apply a combine začne být více odděleno v nástrojích na zpracování velkých dat, které už se nevejdou pohodlně do operační paměti počítače a pandas na ně už nestačí. Apply pak probíhá po částech, třeba i distribuovaně na oddělených serverech, a výsledné combine se provádí sesbíráním částečných výsledků.
Použijeme teď jednoduchou agregační metodu count
, která nám vrátí počet hodnot (po skupinách samozřejmě).
legislators_historical_by_type.count()
Vidíme, že v datech je zaznamenáno 1830 senátorů a 10151 kongresmanů. Na levé straně v indexu vidíme skupiny, podle kterých se dataset agregoval a do sloupců se daly všechny sloupce, na které bylo možné aplikovat naši agregační funkci (v tom případě na všechny zbylé).
Z čísel si můžeš všimnout, že u některých chybí údaje o datu narození nebo straně.
Než aplikujeme krok apply, můžeme si vybrat, na který ze sloupců tak učiníme, trochu si tím zpřehledníme výstup. Pokud si vybereme jen jeden sloupec, dostaneme Series.
legislators_historical_by_type['party'].count()
Tento krok si můžeme trochu zjednodušit - než abychom se doptávali na count
nad jedním sloupcem v rámci agregace, můžeme se doptat na velikost každé ze skupin.
legislators_historical_by_type.size()
Úkol: Rozděl data podle strany (party
) a vypiš počet záznamů v každé skupině. Dokážeš výsledek setřídit podle velikosti skupin?
Možná sis všimla, jaký je u výsledku index. Pokud ne, nevadí, určitě si všimneš teď. Zkusíme totiž vytvořit skupiny ne z jednoho sloupce, ale ze dvou. Pojďme si rozdělit zákonodárce podle států, a každou skupinu za jeden stát ještě podle pohlaví.
legislators_by_state_gender_counts = legislators_historical.groupby(["state", "gender"]).count()
legislators_by_state_gender_counts
Máme tedy skupiny, které jsou definované dvojicí hodnot stát a pohlaví (state
, gender
). A to je přesně důvod, proč existuje v Pandas MultiIndex
.
Vlastnosti MultiIndexu, vlastně takového víceúrovňového či vícerozměrného indexu, můžeme prozkoumat (kromě prostého zobrazení) pomocí několika užitečných atributů (properties).
# počet úrovní
legislators_by_state_gender_counts.index.nlevels
# jména úrovní
legislators_by_state_gender_counts.index.names
# mohutnost (počet hodnot) jednotlivých úrovní
legislators_by_state_gender_counts.index.levshape
# hodnoty v jednotlivých úrovních
legislators_by_state_gender_counts.index.levels
Víme tedy, že náš (multi) index má dvě úrovně. Abychom dostali konkrétní řádek, musíme tím pádem zadat dvě hodnoty. K tomu nám poslouží tuple
(pozor, musí to opravdu být tuple
a ne list
, tj. musíme použít kulaté a ne hranaté závorky).
legislators_by_state_gender_counts.loc[("WY", "F")]
Co kdybychom zadali jen polovinu indexu? Dostaneme celou skupinu, v našem případě celý stát.
legislators_by_state_gender_counts.loc["WY"]
Otázka: Jaký je index výsledné tabulky?
Pokud bychom chtěli jedno pohlaví, můžeme indexu změnit pořadí.
swapped_index = legislators_by_state_gender_counts.index.swaplevel(0, 1)
legislators_by_gender_state_counts = legislators_by_state_gender_counts.set_index(swapped_index)
legislators_by_gender_state_counts
legislators_by_gender_state_counts.loc["F"].head()
Více o (pokročileší) práci s indexy a multiindexy najdeš v dokumentaci.
Pokud bychom se chtěli multiindexu "zbavit", můžeme to udělat pomocí .reset_index()
legislators_by_state_gender_counts.reset_index().head(5)
Anebo rovnou použít groupby
s as_index=False
.
legislators_historical.groupby(["state", "gender"], as_index=False).count().head(5)
DataFrameGroupBy object
?Na začátku jsme si udělali základní agregaci bez aplikace funkcí a dostali jsme jakýsi objekt. Je možné s ním něco dělat, aniž bychom agregovali? Ukazuje se, že ano.
Než se k tomu dostaneme, zkusme jeden úkol: Rozděl náš dataframe podle stran - tzn. pro každou stranu vytvoř dataframe a ten ulož do zvláštního souboru. Např. Democrat
půjde do Democrat.csv atd.
legislators_historical.groupby('party')
Jedna klíčová funkce, kterou nám tento objekt nabízí, je iterace.
groups = legislators_historical.groupby('party')
next(iter(groups)) # timhle ziskame prvni element pri iteraci (for cyklu)
K čemu nám to může být? Při agregaci se data drasticky zjednodušují a nemusíme si vždy být jisti, že naše agregace jsou napsané správně. Pomocí iterace nad skupinami si můžeme zobrazit všechna data před agregací.
for party, df in legislators_historical.groupby('party'):
df.to_csv(party + '.csv')
Tuto funkcionalitu asi tolik neoceníte, když jde jen o jeden sloupec, zde party
, tedy alternativa není tak složitá. Ale jakmile začnete agregovat nad více sloupci, začne být iterace čím dál užitečnější.
Dosud jsme agregovali jen pomocí .size
nebo .count
, ale existuje spousta další agregačních metod, zejm. těch numerických.
Pro snadnější práci s agregacemi budeme používat metodu .agg
, která akceptuje slovník. Tento slovník udává, co chceme agregovat (klíč) a jak to chceme agregovat (hodnota). S tím, že způsobů agregace pro jeden sloupec může být více naráz. Ukážeme si.
legislators_historical.groupby('state').agg({'birthday': 'max'}).head()
legislators_historical.groupby('state').agg({
'birthday': ['min', 'max'],
'party': 'nunique'}
).head()
Většinu času jsme pracovali s daty, které jsme načetli ze souboru a krom nějakého základního čištění jsme je žádným způsobem neměnili. Teď si ukážeme, jak udělat některé základní transformace.
Může nás například zajímat, které hodnoty máme či nemáme v daném sloupci. K získání takové informace sloužít metody .isnull
a .notnull
, které jsou navzájem inverzní.
legislators_historical['party'].isnull()
Series má metodu isnull
, která nám vrátí True/False hodnoty podle toho, jestli daná hodnota chybí nebo ne (NULL v SQL). Pro snadnější pochopení je možné použít inverzní metodu notnull
.
Může náš též zajímat, zda řádky nabývají některou z vybraných hodnot.
legislators_historical['first_name'].isin(['Richard', 'John'])
... nebo jak se hodnoty liší mezi řádky (dává smysl jen pro číselná data nebo pro sloupce obsahující datum)
legislators_historical['birthday'].diff()
legislators_historical['first_name'].str.len()
Často používanými metodami v rámci .str
je contains
nebo .lower/upper
names = pd.Series(['JOHN', 'Jean-Luc', 'Mary-Jane', 'Kate', 'John'])
names.str.contains('-')
names[names.str.contains('-')]
Zatímco .str.contains
použijeme zpravidla na filtrování, .lower
poslouží třeba na unifikaci dat, která se pak lépe agregují (a deduplikují).
names.value_counts()
names.str.lower().value_counts()
Z .dt
si ukážeme jak vytáhnout z data rok.
birth_years = legislators_historical['birthday'].dt.year
birth_years#.value_counts()
Úkol: vyfiltruj politiky narozené v roce 1980 či později.
Dosud jsme vždy agregovali nad něčím, co jsme měli v DataFramu. Je ale možné agregovat nad daty, které tam vůbec nemáme, alespoň ne explicitně. K tomu budou sloužit transformace, které jsme si právě ukázali.
Kromě názvu sloupce můžeme do groupby
vložit nějakou Series (!), která má stejný tvar jako naše sloupce a pandas podle toho bude umět agregovat. Jaká taková Series se nabízí? Nejlépe transformace nějakého existujícího sloupce.
Můžeme tak agregovat data na základě měsíce, kdy se daný člověk narodil. A to aniž bychom tento sloupec přidávali do dataframu. Tato metoda nám tak pomůže dělat kreativní agregace bez nutnosti měnit naše data.
legislators_historical.groupby([
legislators_historical['birthday'].dt.month,
]).size()
Můžeme samozřejmě přidávat další a další (ne)sloupce.
legislators_historical.groupby([
legislators_historical['type'],
legislators_historical['birthday'].dt.month,
]).size()
Měsíce jsou fajn, je jich jen 12, ale co když budeme chtít agregovat nad roky narození?
legislators_historical.groupby([
legislators_historical['type'],
legislators_historical['birthday'].dt.year,
]).size().count()
Dostáváme poněkud velký dataset, který nám toho moc neřekne. S tím nám pomůže další kapitola.
Sice jsme si vytáhli jednotlivé roky narození, ale přeci jen jich je spousta a moc nám to neřekne, určitě se podle nich nedá dobře agregovat.
birth_years = legislators_historical['birthday'].dt.year.astype('Int16')
legislators_historical = legislators_historical.assign(birth_year=birth_years)
legislators_historical['birth_year'].hist(bins=25)
legislators_historical['birth_year'].value_counts()
Ani histogram, ani value_counts
nám žádné moc hodnotné informace nepřinesl. Budeme muset data trochu seskupit. Na to v pandas existuje několik možností.
První z nich je nám již známý .value_counts
, kterému můžeme přihodit argument bins
, který znamená, že nechceme frekvence jednotlivých hodnot, ale že chceme seskupit data do několika intervalů.
legislators_historical['birth_year'].value_counts(bins=10)
Co když nám takové samorozdělení nestačí? Na to je pandas.cut
, resp. pd.cut
. Má spoustu možností, doporučujeme projít dokumentaci.
bins = [1700, 1750, 1800, 1850, 1900, 1950]
pd.cut(legislators_historical['birth_year'], bins)
pd.cut(legislators_historical['birth_year'], bins).value_counts()
Nevýhodou pd.cut
je, že intervaly jsou určeny uživatelem a může chvíli trvat, než je člověk odladí. Více automatická je možnost určit intervaly pomocí statistického rozložení dat, k tomu slouží pd.qcut
, ten místo hranic intervalů bere kvantily.
pd.qcut(legislators_historical['birth_year'], [0, .1, .5, .9, 1]).value_counts()
Samozřejmostí tohoto seskupování je, že tyto transformované sloupce opět můžeme použít pro agregaci.
Úkol: Kolik bylo dohromady poslanců a kolik bylo senátorů, rozděleno podle století, kdy byli narozeni. (Např. mezi lety 1900 a 2000 bylo narozeno 200 senátorů a 800 poslanců atd.)
Pokud budeme agregovat nad několika sloupci, může se nám stát, že z DataFramu se nám stane jedna dlouhá nudle. V tom se nedá moc dobře vyznat. Pokud jste s takovými daty někdy pracovali v Excelu, možná vám bude povědomá funkce kontigenčních tabulek, v angličtině pivot tables.
Než začneme pivotovat, vytvořme si malý dataset - bude to jednoduchá agregace na základě typu angažmá z parlamentu a pohlaví.
summary = legislators_historical.groupby(['type', 'gender'], as_index=False)[['last_name']].count()
summary = summary.rename(columns={'last_name': 'count'})
summary
Součástí každého pivot
u jsou tři faktory - specifikace,
Hezky je to vidět na tomto diagramu.
summary.pivot(index='gender', columns='type', values='count')
Omezením metody pivot
je to, že umí jen otáčet DataFrame, ale pokud některému z políček odpovídá více hodnot, vyhodí vám chybu, protože neví, jak je má agregovat. Pivot je opravdu jen pro otáčení.
Na komplexnější agregace tu je podobně pojmenovaný pivot_table
.
summary = legislators_historical.groupby(['type', 'state', 'gender'], as_index=False)[['last_name']].count()
summary = summary.rename(columns={'last_name': 'count'})
summary
pivot_table
nabízí možnost přiřadit do některé z dimenzí (do indexu či sloupců) vícero sloupců, a následně pak vytvoří víceúrovňový index. Jde též specifikovat, jaká funkce se aplikuje, pokud na buňku připadá více hodnot (jako v Excelu).
Více detailů najdete v dokumentaci.
wide = summary.pivot_table(index='state', columns=['type', 'gender'], values='count', fill_value='')
wide.head(10)
Kdyby ti přišlo matoucí, že tu jsou dvě podobné funkce na kontigenční tabulky, tak tě ještě víc zmateme, protože existuje ještě pd.crosstab
. Do detailů zde už zacházet nebudeme, doporučujeme dokumentaci.
Zatímco na kontigenční tabulky mnozí narazili, jejich inverzní funkcionalita je celkem neznámá, a zajímavá. Funguje přesně tak, jak píšeme - vezme se široká tabulka, ve které je jedna z dimenzí ve sloupcích, a zúží a prodlouží se tím, že se ony názvy sloupcí překonvertují do samotného sloupce. Příklad bude názornější.
V pandas se na tuto inverzní operaci používá metoda melt
. Její delší výčet argumentů je tradičně v dokumentaci.
Mějme dataset, kde je výkon v různých regionech fiktivní země. Nevýhodou je, že nemůžeme úplně dívat na časové řady, protože roky, ke kterým údaje platí, jsou ve sloupcích, ne v řádcích. Takto široké tabulky jsou celkem populární třeba u dat o počasí.
df = pd.DataFrame({
'region': ['North', 'South', 'East', 'West'],
'2000': [200, 100, 50, 1000],
'2005': [450, 10, 510, 1040],
'2010': [10, 500, 950, 500],
'2015': [550, 20, 50, 10],
'2020': [1, 1, 5, 10],
})
df
pd.melt
zajímá, který ze sloupců nějak identifikuje dané řádky (id_vars
) a které sloupce obsahují hodnoty (value_vars
). Zbylé dva argumenty slouží jen k přejmenování sloupců (var_name
, value_name
).
long = df.melt(id_vars=['region'], value_vars=['2000', '2005', '2010', '2015', '2020'],
var_name='year', value_name='output')
long
Úkol: vem tento dlouhý dataset a udělej z něj ten původní, široký. (Neřeš, pokud máš někde index, kde před tím nebyl.)