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 pdManipulace 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
planetyPř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
planetyPř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")   
planetyDatové 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()
countriesNamá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: objectUž 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.dtypesiso                              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: objectTypy 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: int64pd.Series([0, 123, 12345])
# pd.Series([0, 123, 12345], dtype="int64")   # totéž0        0
1      123
2    12345
dtype: int64Pomocí 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: int16Když 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ž 
pandasnecháme dělat jeho práci, použije se obecný typobjecta 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: boolJde 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: objectToto 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"
)
mazlicci0          pes
1        kočka
2       křeček
3    tarantule
4      hroznýš
dtype: string# mazlicci[0] = 42  # ChybaDatový 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: objectPozor, 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"] * 365name
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: float64Varová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") * 2Porovná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"] > 15name
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"] > 15np.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: boolPodobně 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: boolFiltrová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: float64Nepovinný 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: float64V 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. 👋