V minulé lekci jsme si představili knihovnu pandas a její základní třídy: Series
, DataFrame
a Index
. Brali jsme je ovšem jako statické objekty, které jsme si pouze prohlíželi.
V této lekci začneme upravovat existující tabulky. Ukážeme si:
- jak přidat či ubrat sloupce a řádky
- jak změnit hodnotu konkrétní buňky
- jaké datové typy se hodí pro který účel
- aritmetické a logické operace, které lze se sloupci provádět
- filtrování a řazení řádků
A jelikož o výsledky práce určitě nechceš přijít, nakonec se bude hodit i ukládání výsledků do externích souborů.
# Obligátní import
import pandas as pd
Manipulace s DataFrames¶
Pro rozehřátí budeme pracovat s malou tabulkou obsahující několik základních informací o planetách, které snadno najdeš např. na wikipedii.
planety = pd.DataFrame({
"jmeno": ["Merkur", "Venuše", "Země", "Mars", "Jupiter", "Saturn", "Uran", "Neptun"],
"symbol": ["☿", "♀", "⊕", "♂", "♃", "♄", "♅", "♆"],
"obezna_poloosa": [0.39, 0.72, 1.00, 1.52, 5.20, 9.54, 19.22, 30.06],
"obezna_doba": [0.24, 0.62, 1, 1.88, 11.86, 29.46, 84.01, 164.8],
})
planety = planety.set_index("jmeno") # Se jmenným indexem se ti bude snáze pracovat
planety
Přidání nového sloupce¶
Když chceme přidat nový sloupec (Series
), přiřadíme ho do DataFrame
jako hodnotu do slovníku - tedy v hranatých závorkách s názvem sloupce. Dobrá zpráva je, že stejně jako v konstruktoru si pandas
poradí jak se Series
, tak s obyčejným seznamem.
V našem konkrétním případě si najdeme a přidáme počet známých měsíců (velkých i malých).
mesice = [0, 0, 1, 2, 79, 82, 27, 14] # Alternativně mesice = pd.Series([...])
planety["mesice"] = mesice
planety
💡 V tomto případě jsme přímo upravili existující DataFrame
. Většina metod / operací v pandas
(už znáš např. set_index
) ve výchozím nastavení vždy vrací nový objekt s aplikovanou úpravou a ten původní objekt nechá v nezměněném stavu. Je to dobrým zvykem, který budeme dodržovat. Přiřazování sloupců je jednou z akceptovaných výjimek tohoto jinak uznávaného pravidla, zejména když se tabulka upravuje jen v úzkém rozsahu řádků kódu (případně kdyby kopírování bylo příliš náročné na paměť).
DataFrame
však nabízí ještě metodu assign
, která nemění tabulku, ale vytváří její kopii s přidanými (nebo nahrazenými) sloupci. Pokud se chceš vyhnout nepříjemnému sledování, kterou tabulku jsi změnil/a či nikoliv, assign
ti můžeme jen doporučit.
Mimochodem, kopii tabulky můžeš kdykoliv vytvořit metodou copy
- to se hodí třeba při psaní funkcí, kde se vstupní tabulka z různých důvodů upravuje.
# Nový dočasný DataFrame
planety.assign(
je_stavebnice=[True, False, False, False, False, False, False, False],
ma_vztah_k_vestonicim=[False, True, False, False, False, False, False, False],
)
# Objekt `planety` zůstal nezměněn.
planety2 = planety.copy()
planety2["je_nezdrava_tycinka"] = [False, False, False, True, False, False, False, False]
planety2
# Ani teď se původní `planety` nezmění
Úkol: Zkus (jedním či druhým způsobem) přidat sloupec s rokem objevu ("objeveno"
). Údaje najdeš např. na https://cs.wikipedia.org/wiki/Sluneční_soustava.
Pro hodnoty nového sloupce lze použít i jednu skalární hodnotu (v praxi se ale s touto potřebou nepotkáme tak často) - stejná hodnota se pak použije ve všech řádcích:
planety["je_planeta"] = True
planety
Přidání nového řádku (volitelné)¶
🤔 Tohle je něco, co se dělá vlastně dost málo. Obvykle pracujeme na celých sloupcích (nebo slučujeme několik tabulek dohromady), a k přidání nového řádku dojde spíš omylem. Ale pro úplnost uvádíme.
Když se strojem času vrátíme do dětství (nebo rané dospělosti) autorů těchto materiálů, tedy před rok 2006, kdy se v Praze konal astronomický kongres, který definoval pojem “planeta” (ale ne před rok 1930!), přibude nám nová planeta: Pluto.
Do naší tabulky ho coby nový řádek vložíme pomocí indexeru loc
, který jsme již dříve používali pro “koukání” do tabulky:
planety.loc["Pluto"] = ["♇", 39.48, 247.94, 5, True] # Seznam hodnot v řádku
planety
Úkol: Zkus přidat Slunce nebo nějakou zcela smyšlenou planetu.
Změna hodnoty buňky¶
🤔 I toto se nedělá příliš často...
“Indexery” .loc
a .iloc
se dvěma argumenty v hranatých závorkách odkazují přímo na konkrétní buňku, a přiřazením do nich (opět, podobně jako ve slovníku) se hodnota na příslušné místo zapíše. Jen je třeba zachovat pořadí (řádek, sloupec).
Vrátíme se opět do současnosti a Pluto zbavíme jeho statutu:
planety.loc["Pluto", "je_planeta"] = False
planety
⚠ Pozor: Podobně jako u slovníku, ale možná poněkud neintuitivně, je možné zapsat hodnotu do řádku i sloupce, které neexistují!
planety_bad = planety.copy() # Pro jistotu si uděláme kopii
planety_bad.loc["Zeme", "planeta"] = True
planety_bad
💡 Jistě se ptáš, co znamená NaN v tabulce. Hodnota NaN (Not a Number) označuje chybějící, neplatnou nebo neznámou hodnotu. V našem příkladu jsme ji nezadali, tedy se není co divit. O problematice chybějících hodnot a jejich napravování si budeme povídat někdy příště, prozatím se jimi nenech znervóznit.
Přiřazovat je možné i do rozsahů v indexech - jen je potřeba hlídat, abychom přiřazovali buď skalární hodnotu (tedy jedna hodnota pro celou oblast, bezrozměrné ne-pole), nebo vícerozměrný objekt (Series, DataFrame, seznam, ...) stejného tvaru (počtu řádků a sloupců) jako oblast, do které přiřazujeme:
planety.loc["Merkur":"Mars", "je_obr"] = False
planety.loc["Jupiter":"Neptun", "je_obr"] = [True, True, True, True]
planety
Úkol: Shodou okolností (nebo jde o astronomickou nevyhnutelnost?) mají všichni planetární obři alespoň nějaký prstenec. Dokážeš jednoduše vytvořit sloupec "ma_prstenec"
?
Odstranění řádku (volitelné)¶
🤔 Obvykle neodstraňujeme řádky podle indexu, ale spíš hromadně na základě nějaké podmínky (viz Filtrování), nicméně do základní sady manipulačních nástrojů to patří.
Pro odebrání sloupce či řádku z DataFrame slouží metoda drop
. Ta umí odstraňovat řádky i sloupce, proto bychom jí explicitně měli říct, ve které ose budeme mazat. Jde to buď pomocí argumentu axis
(na výběr je 0 či “index”, 1 či “columns”), nebo o něco čitelněji pomocí pojmenovaného argumentu index
, který rovnou specifikuje, že se chceme zbavit řádků:
Když už jsme se vrátili do budoucnosti (resp. současnosti), vypořádejme se nemilosrdně s Plutem:
planety = planety.drop(index="Pluto")
planety
Úkol: Zkus z planety
vytvořit tabulku, která nebude obsahovat ani Uran, ani Neptun (jedním příkazem).
Odstranění sloupce¶
U sloupce funguje metoda drop
velmi podobně, jen argument se teď jmenuje columns
.
Odstraňme zbytečný sloupec s informační hodnotou na úrovni “stěrače stírají, klakson troubí”...
planety = planety.drop(columns="je_planeta")
planety
Datové typy¶
Příprava dat¶
Nyní opustíme planety a podíváme se na některé zajímavé charakteristiky zemí kolem světa (ježto definice toho, co je to země, je poněkud vágní, bereme v potaz členy OSN), zachycené k jednomu konkrétnímu roku uplynulé dekády (protože ne vždy jsou všechny údaje k dispozici, bereme poslední rok, kde je známo dost ukazatelů). Data pocházejí povětšinou z projektu Gapminder, doplnili jsme je jen o několik dalších informací z wikipedie.
Soubor otevřeme ho pomocí již známé funkce read_csv
# Místo `set_index` vybereme index rovnou při načítání
countries = pd.read_csv("countries.csv", index_col="name")
countries = countries.sort_index()
countries
Namátkou si vybereme nějakou zemi a podíváme se, jaké údaje o ní v tabulce máme.
countries.loc["Czechia"]
iso CZE
world_6region europe_central_asia
world_4region europe
income_groups high_income
is_eu True
is_oecd True
eu_accession 2004-05-01
year 2018
area 78870.0
population 10590000.0
alcohol_adults 16.47
bmi_men 27.91
bmi_women 26.51
car_deaths_per_100000_people 5.72
calories_per_day 3256.0
infant_mortality 2.8
life_expectancy 79.37
life_expectancy_female 81.858
life_expectancy_male 76.148
un_accession 1993-01-19
Name: Czechia, dtype: object
Už na první pohled je každé pole jiného typu. Ale jakého? Na to nám odpoví atribut dtypes
naší tabulky (u Series
použiješ dtype
, resp. raději dtype.name
, pokud chceš stejně pěknou řetězcovou reprezentaci).
countries.dtypes
iso object
world_6region object
world_4region object
income_groups object
is_eu bool
is_oecd bool
eu_accession object
year int64
area float64
population float64
alcohol_adults float64
bmi_men float64
bmi_women float64
car_deaths_per_100000_people float64
calories_per_day float64
infant_mortality float64
life_expectancy float64
life_expectancy_female float64
life_expectancy_male float64
un_accession object
dtype: object
Typy v pandas vycházejí z toho, jak je definuje knihovna numpy
(obecně užitečná pro práci s numerickými poli a poskytující vektorové operace s rychlostí řádově vyšší než v Pythonu jako takovém). Ta potřebuje především vědět, jak alokovat pole pro prvky daného typu na to, aby mohly být seřazeny efektivně jeden za druhým, a tedy i kolik bajtů paměti každý zabírá. Kopíruje přitom “nativní” datové typy, které už můžeš znát z jiných jazyků (např. C). Umístění v paměti je něco, co v Pythonu obvykle neřešíme, ale rychlé počítání se bez toho neobejde. My nepůjdeme do detailů, ale požadavek na rychlost se nám tu a tam vynoří a my budeme klást důraz na to, aby se operace prováděly na úrovni numpy a nikoliv v Pythonu.
Poněkud tajuplný systém typů v numpy
(popsaný v dokumentaci) je naštěstí v pandas
(mírně) zjednodušen a nabízí jen několik užitečných základních (rodin) typů, které si teď představíme.
💡 Novější verze pandas
umožňují používat pro data tzv. “arrow” backend, který zrychluje a zefektivňuje některé operace. Nicméně je třeba jej explicitně vyžádat a kromě “pouhé” efektivity nepřináší mnoho výhod. My se tím nebudeme zabývat, ale můžete se podívat na příslušnou část dokumentace.
Celá čísla (integers)¶
V Pythonu je pro celá čísla vyhrazen přesně jeden typ: int
, který možňuje pracovat s libovolně velkými celými čísly (0, -58 nebo třeba 123456789012345678901234567890). V pandas
se můžeš setkat s int8
, int16
, int32
, int64
, uint8
, uint16
, uint32
a uint64
- všechny mají stejné základní vlastnosti a každý z nich má jen určitý rozsah čísel, která do něj lze uložit. Liší se velikostí paměti, kterou jedno číslo zabere (číslovka v názvu vyjadřuje počet bitů), a tím, zda jsou podporována i záporná čísla (předpona u
znamená unsigned (bez znaménka), tedy že počítáme pouze s nulou a kladnými čísly).
Rozsahy:
int8
: -128 až 127uint8
: 0 až 255int16
: -32 768 až 32 767uint16
: 0 až 65 535int32
: -2 147 483 648 až 2 147 483 647 (tedy +/- ~2 miliardy)uint32
: 0 až 4 294 967 295 (tedy až ~4 miliardy)int64
: -9 223 372 036 854 775 808 až 9 223 372 036 854 775 807 (tedy +/- ~9 trilionů)uint64
: 0 až 18 446 744 073 709 551 615 (tedy až ~18 trilionů)
💡 Aby toho nebylo málo, ke každému int?
/ uint?
typu existuje ještě jeho alternativa, která umožňuje ve sloupci použít chybějící hodnoty, t.j. NaN
. Místo malého i
, případně u
v názvu se použije písmeno velké. Tato vlastnost (tzv. “nullable integer types”) je relativně užitečná, ale je dosud poněkud experimentální. My ji nebudeme v kurzu využívat.
Detailní vysvětlení toho, jak jsou celá čísla v paměti počítače reprezentována, najdeš třeba ve wikipedii.
V pandas
je výchozí celočíselný typ int64
, a pokud neřekneš jinak, automaticky se pro celá čísla použije (ve většině případů to bude vhodná volba):
countries["year"]
name
Afghanistan 2018
Albania 2018
Algeria 2018
Andorra 2017
Angola 2018
...
Venezuela 2018
Vietnam 2018
Yemen 2018
Zambia 2018
Zimbabwe 2018
Name: year, Length: 193, dtype: int64
pd.Series([0, 123, 12345])
# pd.Series([0, 123, 12345], dtype="int64") # totéž
0 0
1 123
2 12345
dtype: int64
Pomocí argumentu dtype
můžeš ovšem přesně specifikovat, který typ celých čísel chceš:
pd.Series([0, 123, 12345], dtype="int16")
0 0
1 123
2 12345
dtype: int16
Když se pokusíš do nějakého typu vložit číslo, které se do něj “nevleze”, pandas vyhodí výjimku (dříve to taky nebylo). Zkusme do nejširšího celočíselného typu (int64
) vložit veliké číslo (třeba 123456789012345678901234567890) a uvidíme, co se stane:
# Toto vyhodí výjimku:
# pd.Series([0, 123, 123456789012345678901234567890], dtype="int64")
# Toto projde, ale už to není int64:
pd.Series([0, 123, 123456789012345678901234567890])
0 0
1 123
2 123456789012345678901234567890
dtype: object
- Když ho budeme explicitně požadovat, vyhodí se výjimka.
- Když
pandas
necháme dělat jeho práci, použije se obecný typobject
a přijdeme o jistou část výhod: sloupec nám zabere násobně více paměti a aritmetické operace s ním jsou o řád až dva pomalejší. Pokud to není naší prioritou, není to zase takový problém.
Obecně proto doporučujeme držet se int64
, resp. nechat pandas
, aby jej za nás automaticky použil. Teprve v případě, že si to budou žádat přísné paměťové nároky, se ti vyplatí hledat ten “nejvíce růžový” typ.
Úkol: Zkus vytvořit Series
s datovým typem uint8
, obsahující (alespoň) jedno malé záporné číslo. Co se stane?
Čísla s plovoucí desetinnou čárkou (floats)¶
Podobně jako u celočíselných hodnot, i jednomu typu v Python (float
) odpovídá několik typů v pandas
: float16
, float32
, float64
. Součástí názvu je opět počet bitů, které jedno číslo potřebuje ke svému uložení. Naštěstí v tomto případě float64
přesně odpovídá svým chováním float
z Pythonu, zbylé dva typy nejsou tak přesné a mají menší rozsah - kromě optimalizace paměťových nároků u specifického druhu dat je nejspíš nepoužiješ.
Více teoretického čtení o reprezentaci čísel s desetinnou čárkou najdeš na wiki.
countries["bmi_men"]
name
Afghanistan 20.62
Albania 26.45
Algeria 24.60
Andorra 27.63
Angola 22.25
...
Venezuela 27.45
Vietnam 20.92
Yemen 24.44
Zambia 20.68
Zimbabwe 22.03
Name: bmi_men, Length: 193, dtype: float64
# Docela přesné pí
pd.Series([3.14159265])
0 3.141593
dtype: float64
# Ne už tak přesné pí
pd.Series([3.14159265], dtype="float16")
C:\Users\janpi\Documents\code\collaboration\pyladies-kurz\.venv\Lib\site-packages\pandas\io\formats\format.py:1458: RuntimeWarning: overflow encountered in cast
has_large_values = (abs_vals > 1e6).any()
0 3.140625
dtype: float16
Úkol: Vytvoř pole typu float64
jen ze samých celých čísel. Co se stane?
Logické hodnoty (booleans)¶
Toto je asi nejméně překvapivý datový typ. Chová se v zásadě stejně jako typ bool
v Pythonu. Nabírá hodnot True
a False
(které lze též pokládat za 1 a 0 v některých operacích). Má ještě jednu skvělou vlastnost - objekty Series
i DataFrame
jde filtrovat právě pomocí sloupce logického typu (o tom viz níže).
countries["is_oecd"].iloc[:20]
name
Afghanistan False
Albania False
Algeria False
Andorra False
Angola False
Antigua and Barbuda False
Argentina False
Armenia False
Australia True
Austria True
Azerbaijan False
Bahamas False
Bahrain False
Bangladesh False
Barbados False
Belarus False
Belgium True
Belize False
Benin False
Bhutan False
Name: is_oecd, dtype: bool
# Vytvoření nového sloupce
pd.Series([True, False, False])
0 True
1 False
2 False
dtype: bool
Jde to ovšem i takto:
pd.Series([1, 0, 0], dtype="bool")
0 True
1 False
2 False
dtype: bool
Úkol: Co se stane, když vytvoříš Series
typu bool
z řetězců "True"
a "False"
(nezapomeň na uvozovky)?
Řetězce a obecné objekty (strings, objects)¶
Aktuální verze knihovny pandas
(2.2) má k řetězcům poněkud schizofrenní postoj, respektive je v procesu přechodu od ne úplně šťastného přístupu (obecný datový typ object
) k o něco lepšímu (speciální typ string
) a ještě lepšímu (typ string[pyarrow]
) - v dokumentaci se doporučuje používat přístup druhý, přestože to je zároveň označeno za experimentální. Rozdíl je v současnosti víceméně estetický (a my z pohodlnosti obvykle nebudeme sloupce na string
převádět).
countries["iso"]
name
Afghanistan AFG
Albania ALB
Algeria DZA
Andorra AND
Angola AGO
...
Venezuela VEN
Vietnam VNM
Yemen YEM
Zambia ZMB
Zimbabwe ZWE
Name: iso, Length: 193, dtype: object
Toto tě pravděpodobně překvapí - ve výchozím stavu řetězce spadají společně s dalšími neurčenými nebo nerozpoznanými hodnotami do kategorie object
, která umožňuje v daném sloupci mít cokoliv, co znáš z Pythonu, a chová se tak do značné míry jako obyčejný seznam s výhodami (žádné podivné konverze, sledování rozsahů, ...) i nevýhodami (je to pomalejší, než by mohlo; nikdo ti nezaručí, že ve sloupci budou jen řetězce).
Budeš-li chtít být explicitní či získat navíc trochu typové kontroly, můžeš datový typ string
uvést v konstuktoru, případně konvertovat sloupec pomocí metody astype
:
# countries["iso"].astype("string")
# Domácí mazlíčci
mazlicci = pd.Series(
["pes", "kočka", "křeček", "tarantule", "hroznýš"],
dtype="string"
)
mazlicci
0 pes
1 kočka
2 křeček
3 tarantule
4 hroznýš
dtype: string
# mazlicci[0] = 42 # Chyba
Datový typ objekt je jedinou možností v případě, že máme v Series
heterogenní data:
pd.Series([1, "dvě", 3.0]) # Řetězec a další "smetí"
0 1
1 dvě
2 3.0
dtype: object
Pozor, třeba i takový seznam může být hodnotou v sloupci typu object
:
# Objednávky
pd.Series(
[["řízek", "brambory", "cola"], ["smažák", "hranolky"], ["sodovka"]],
index=["Eva", "Evelína", "Evženie"])
Eva [řízek, brambory, cola]
Evelína [smažák, hranolky]
Evženie [sodovka]
dtype: object
Úkol: Co za druh objektu (a jaký dtype
) dostaneme, když se pokusíme získat jeden řádek z tabulky planety
?
Úkol: Co se stane, když sloupec planety["obezna_doba"]
převedeš na object
, resp. string
?
Datum / čas (datetime)¶
Časovými daty se blíže zabývá jedna z následujících lekcí, nicméně nějaká v tabulce zemí už máme, a tak alespoň pro úplnost uvedeme, co v tomto směru pandas
nabízí:
Časové či datumové údaje (datetime) jakožto body na časové ose.
Časové údaje s označením časové zóny (datetimes with time zone).
Časové úseky (timedeltas) jakožto určení délky nějakého úseku (počítáno v nanosekundách)
Období (periods) udávají nějak určená časová období (třeba “únor 2020”)
💡 Pro převod z nejrůznějších formátů na datum / čas slouží funkce to_datetime
, kterou použijeme pro následující ukázku:
pd.to_datetime(countries["un_accession"])
name
Afghanistan 1946-11-19
Albania 1955-12-14
Algeria 1962-10-08
Andorra 1993-07-28
Angola 1976-12-01
...
Venezuela 1945-11-15
Vietnam 1977-09-20
Yemen 1947-09-30
Zambia 1964-12-01
Zimbabwe 1980-08-25
Name: un_accession, Length: 193, dtype: datetime64[ns]
Kategorické (category)¶
Pokud chceme být efektivní při práci se sloupci, kde se často opakují hodnoty (zejména řetězcové), můžeme je zakódovat do kategorií. Tím mnohdy ušetříme zabrané místo a urychlíme některé operace. Při takové konverzi pandas
najde všechny unikátní hodnoty v daném sloupci, uloží si je do zvláštního seznamu a do sloupce uloží jenom indexy z tohoto seznamu. Vše se chová transparentně a při používání tak většinou ani nepoznáte, jestli máte sloupec typu object
nebo category
.
💡 Pro převod mezi různými datovými typy slouží metoda astype
, která jako svůj argument akceptuje jméno dtype, na který chceme převést:
countries["income_groups"].astype("category")
name
Afghanistan low_income
Albania upper_middle_income
Algeria upper_middle_income
Andorra high_income
Angola upper_middle_income
...
Venezuela upper_middle_income
Vietnam lower_middle_income
Yemen lower_middle_income
Zambia lower_middle_income
Zimbabwe low_income
Name: income_groups, Length: 193, dtype: category
Categories (4, object): ['high_income', 'low_income', 'lower_middle_income', 'upper_middle_income']
Úkol: Napadne tě, které sloupce z tabulky countries
bychom měli překonvertovat na nějaký jiný typ?
Matematika¶
Počítání se Series
v pandas
je navrženo tak, aby co nejméně překvapilo. Jednotlivé sloupce se tak můžou stát součástí aritmetických výrazů společně se skalárními hodnotami, s jinými sloupci, numpy
poli příslušného tvaru, a dokonce i seznamy.
# Očekávaná doba života ve dnech
countries["life_expectancy"] * 365
name
Afghanistan 21421.85
Albania 28473.65
Algeria 28418.90
Andorra 30130.75
Angola 23794.35
...
Venezuela 27707.15
Vietnam 27331.20
Yemen 24506.10
Zambia 21699.25
Zimbabwe 21965.70
Name: life_expectancy, Length: 193, dtype: float64
# Hustota obyvatelstva
countries["population"] / countries["area"]
name
Afghanistan 52.844408
Albania 112.626087
Algeria 15.526464
Andorra 189.170213
Angola 16.611855
...
Venezuela 33.265720
Vietnam 273.924591
Yemen 49.927079
Zambia 19.013832
Zimbabwe 34.113011
Length: 193, dtype: float64
# Jak nám podražily obědy
pd.Series([109, 99], index=["řízek", "smažák"]) + [20.9, 10.9] # sčítání se seznamem
řízek 129.9
smažák 109.9
dtype: float64
Úkol: Spočti celkový počet mrtvých v automobilových haváriích v jednotlivých zemích (použij sloupce “population” a “car_deaths_per_100000_people” a jednoduchou aritmetiku). Sedí výsledek pro ČR?
# Jak dlouho jsou v OSN?
from datetime import datetime
datetime.now() - pd.to_datetime(countries["un_accession"])
name
Afghanistan 28431 days 21:11:03.493724
Albania 25119 days 21:11:03.493724
Algeria 22629 days 21:11:03.493724
Andorra 11378 days 21:11:03.493724
Angola 17461 days 21:11:03.493724
...
Venezuela 28800 days 21:11:03.493724
Vietnam 17168 days 21:11:03.493724
Yemen 28116 days 21:11:03.493724
Zambia 21844 days 21:11:03.493724
Zimbabwe 16098 days 21:11:03.493724
Name: un_accession, Length: 193, dtype: timedelta64[ns]
💡 Čísla s plovoucí desetinnou čárkou mohou obsahovat i speciální hodnoty “not a number” a plus nebo mínus nekonečno. Vzniknou např. při nevhodném dělení nulou:
pd.Series([0, -1, 1]) / pd.Series([0, 0, 0])
0 NaN
1 -inf
2 inf
dtype: float64
Varování: Nabádáme tě k opatrnosti při práci s omezenými celočíselnými typy. Podobně jako při jejich nevhodné konverzi, i tady na vás může vyskočit výjimka. O důvod víc, proč se držet int64
.
# pd.Series([7, 14, 149], dtype="int8") * 2
Porovnávání¶
Pro Series
lze použít nejen operátory početní, ale také logické. Výsledkem pak není jedna logická hodnota, ale sloupec logických hodnot.
# 15 litrů čistého alkoholu na osobu na rok budeme považovat za hranici nadměrného pití
# (nekonzultováno s adiktology!)
# Kde se hodně pije?
countries["alcohol_adults"] > 15
name
Afghanistan False
Albania False
Algeria False
Andorra False
Angola False
...
Venezuela False
Vietnam False
Yemen False
Zambia False
Zimbabwe False
Name: alcohol_adults, Length: 193, dtype: bool
# Skoro nikde. A jak jsme na tom u nás?
countries.loc["Czechia", "alcohol_adults"] > 15
np.True_
# Jsou muži v jednotlivých zemích tlustší než ženy?
countries["bmi_men"] > countries["bmi_women"]
name
Afghanistan False
Albania True
Algeria False
Andorra True
Angola False
...
Venezuela False
Vietnam False
Yemen False
Zambia False
Zimbabwe False
Length: 193, dtype: bool
Úkol: Zjistěte, jestli se v jednotlivých zemích dožívají více muži nebo ženy.
# Leží země v Africe?
countries["world_4region"] == "africa"
name
Afghanistan False
Albania False
Algeria True
Andorra False
Angola True
...
Venezuela False
Vietnam False
Yemen False
Zambia True
Zimbabwe True
Name: world_4region, Length: 193, dtype: bool
Podobně jako v Pythonu lze podmínky kombinovat pomocí operátorů. Vzhledem k jistým syntaktickým požadavkům Pythonu je ale potřeba použít místo vám známých logických operátorů jejich alternativy: &
(místo and
), |
(místo or
) a ~
(místo not
). Protože mají jiné priority než jejich klasičtí bratříčci, bude lepší, když při kombinaci s jinými operátory vždycky použiješ závorky.
# Kde se ženy i muži dožívají přes 75 let?
(countries["life_expectancy_male"] > 75) & (countries["life_expectancy_female"] > 75)
name
Afghanistan False
Albania True
Algeria True
Andorra False
Angola False
...
Venezuela False
Vietnam False
Yemen False
Zambia False
Zimbabwe False
Length: 193, dtype: bool
Filtrování¶
Pokud chceš z tabulky vybrat řádky, které splňují nějaké kritérium, musíš (není to vždy těžké :-)) toto kritérium převést do podoby sloupce logických hodnot. Potom tento sloupec (sloupec samotný, nikoliv jeho název!) vložíš do hranatých závorek jako index DataFrame
.
Když budeš například chtít informace jen o členech EU, můžeš k tomu přímo použít sloupec “is_eu”, který logické hodnoty obsahuje:
countries[countries["is_eu"]]
Nemusíš použít existující sloupec v tabulce, ale i jakoukoliv vypočítanou hodnotu stejného tvaru:
# Prťavé země
countries[countries["population"] < 100_000] # Podtržítko pomáhá oddělit tisíce vizuálně
...a samozřejmě kombinace:
# Chudší země EU
countries[countries["is_eu"] & (countries["income_groups"] != "high_income")]
# Které země OECD mají očekávanou dobu dožití méně 78 let?
countries[countries["is_oecd"] & (countries["life_expectancy"] < 78)]
Protože tento způsob filtrování je poněkud nešikovný, existuje ještě metoda query
, která umožňuje vybírat řádky na základě řetězce, který popisuje nějakou (ne)rovnost z názvů sloupců a číselných hodnot (což poměrně často jde, někdy ovšem nemusí).
# Opravdu veliké země (počet obyvatel nad 100 milionů)
countries.query("population > 100_000_000")
# V kterých zemích EU se hodně jí?
countries.query("is_eu & (calories_per_day > 3500)")
Úkol: Která jediná země Afriky patří do skupiny s vysokými příjmy?
Úkol: Ve kterých zemích se pije opravdu hodně (použij výše uvedené nebo jakékoliv jiné kritérium)
Řazení¶
V úvodní lekci pandas
jsme si již ukázali, jak pomocí metody sort_index
seřadit řádky podle indexu. Jelikož countries
už jsou srovnané, vyzkoušíme si to ještě jednou na planetách:
planety.sort_index()
Pro řazení hodnot v Series
se použije metoda sort_values
:
# 10 zemí s nejmenším počtem obyvatel
countries["population"].sort_values().head(10)
name
Tuvalu 9888.0
Nauru 10440.0
Palau 20920.0
San Marino 32160.0
Monaco 35460.0
Liechtenstein 36870.0
Saint Kitts and Nevis 54340.0
Marshall Islands 56690.0
Dominica 67700.0
Seychelles 87420.0
Name: population, dtype: float64
Nepovinný argument ascending
říká, kterým směrem máme řadit. Výchozí hodnota je True
, změnou na False
tedy budeme řadit od největšího k nejmenšímu:
# Největších 10 zemí podle rozlohy
countries["area"].sort_values(ascending=False).head(10)
name
Russia 17098250.0
Canada 9984670.0
United States 9831510.0
China 9562911.0
Brazil 8515770.0
Australia 7741220.0
India 3287259.0
Argentina 2780400.0
Kazakhstan 2724902.0
Algeria 2381740.0
Name: area, dtype: float64
V případě tabulky je třeba jako první argument uvést jméno sloupce (nebo sloupců), podle kterých chceme řadit:
# 10 zemí s největší spotřebou alkoholu na jednoho obyvatele
countries.sort_values("alcohol_adults", ascending=False).head(10)
💡 V následující buňce je celý kód uzavřen do závorky. Umožnili jsme si tím roztáhnout jeden výraz na více řádků, abychom jeho části mohli náležitě okomentovat.
(
# Uvažuj jenom EU
countries[countries["is_eu"]]
# Seřaď nejdřív podle data vstupu do EU, pak podle vstupu do OSN
.sort_values(["eu_accession", "un_accession"])
# Zobraz si jen ty dva sloupce
[["eu_accession", "un_accession"]]
)
Úkol: Seřaď země světa podle hustoty obyvatel.
Úkol: Které země mají problémy s nadváhou (průměrné BMI mužů a žen je přes 25)?
Úkol: V kterých 20 zemích umře absolutně nejvíc lidí při automobilových haváriích?
Ulož výsledky!¶
A tím už pomalu končíme. Jenže jsme udělali (skoro) netriviální množství práce a ta bude do příště ztracená. Naštěstí zapsat DataFrame
do externího souboru v některém z typických formátů není vůbec komplikované. K sadě funkcí pd.read_XXX
existují jejich protějšky DataFrame.to_XXX
. Liší se různými parametry, ale základní použití je velmi jednoduché:
planety.to_csv("planety.csv")
planety.to_excel("planety.xlsx")
Excel ani CSV nejsou formáty pro ukládání velikých dat zcela vhodné (jako alternativy se nabízí třeba parquet), pro naše účely (malé soubory, čitelný textový formát) ale budou CSV postačovat.
Jednou z možností je i vytvoření HTML tabulky (které lze dodat i různé formátování, což ovšem nechme raději na jindy nebo na doma, viz dokumentace “Styling”):
planety.to_html("planety.html")
Úkol: Podívej se, co ve výstupních souborech najdeš.
Úkol: Podívej se na seznam možných výstupních formátů a zkus si planety nebo země zapsat do nějakého z nich: https://
A to už je opravdu všechno. 👋