Skip to article frontmatterSkip to article content

Pandas - datové typy a základní operace

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
Loading...

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
Loading...

💡 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.
Loading...
planety2 = planety.copy()
planety2["je_nezdrava_tycinka"] = [False, False, False, True, False, False, False, False]
planety2
# Ani teď se původní `planety` nezmění
Loading...

Ú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
Loading...

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
Loading...

Ú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
Loading...

⚠ 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
Loading...

💡 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
Loading...

Ú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
Loading...

Ú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
Loading...

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
Loading...

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ž 127
  • uint8: 0 až 255
  • int16: -32 768 až 32 767
  • uint16: 0 až 65 535
  • int32: -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ý typ object 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"]]
Loading...

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ě
Loading...

...a samozřejmě kombinace:

# Chudší země EU
countries[countries["is_eu"] & (countries["income_groups"] != "high_income")]
Loading...
# Které země OECD mají očekávanou dobu dožití méně 78 let?
countries[countries["is_oecd"] & (countries["life_expectancy"] < 78)]
Loading...

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")
Loading...
# V kterých zemích EU se hodně jí?
countries.query("is_eu & (calories_per_day > 3500)")
Loading...

Ú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()
Loading...

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)
Loading...

💡 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"]]
)
Loading...

Ú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://pandas.pydata.org/pandas-docs/stable/reference/frame.html#serialization-io-conversion

A to už je opravdu všechno. 👋