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:
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
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
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%C4%8Dn%C3%AD_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
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.
"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"
?
Pro odebrání sloupce či řádku z DataFrame slouží metoda drop
. Její první argument očekává označení (index) jednoho nebo více řádků či sloupců, které chceš odebrat. Argument axis
označuje, ve které dimenzi se operace má aplikovat. Můžeš použít buď číslo 0 či 1 (odpovídá pořadí od nuly, ve kterém se uvádějí klíče při odkazování na buňky), anebo pojmenování dané dimenze:
Osa (axis):
Tento argument používají i četné další metody a funkce, proto se ujisti, že mu rozumíš
Když už jsme se vrátili do budoucnosti (resp. současnosti), vypořádejme se nemilosrdně s Plutem (pro metodu drop
je výchozí hodnotou argumentu axis
0, nemusíme ho tedy psát):
planety = planety.drop("Pluto") # Přidej axis="rows", chceš-li být explicitní
planety
Úkol: Zkus z planety
vytvořit tabulku, která nebude obsahovat ani Uran, ani Neptun (jedním příkazem).
U sloupce funguje metoda drop
velmi podobně, jen tentokrát argument axis
uvést musíme.
Odstraňme zbytečný sloupec s informační hodnotou na úrovni "stěrače stírají, klakson troubí"...
planety = planety.drop("je_planeta", axis="columns")
planety
Metoda drop
, v souladu s výše zmíněnou konvencí, vrací nový DataFrame
(a proto výsledek operace musíme přiřadit do planety
). Pokud chceš operovat rovnou na tabulce, můžeš použít příkaz del
(funguje stejně jako u slovníku) nebo poprosit pandí bohy (a autory těchto materiálů) o odpuštění a přidat argument inplace=True
(tento argument lze, bohužel, použít i u mnoha dalších operací):
# Jen na vlastní nebezpečí
# Alternativa 1)
# del planety["je_planeta"]
# Alternativa 2)
# planety.drop("je_planeta", axis=1, inplace=True)
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"]
countries.dtypes
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.
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ž 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"]
pd.Series([0, 123, 12345])
# pd.Series([0, 123, 12345], dtype="int64") # totéž
Pomocí argumentu dtype
můžeš ovšem přesně specifikovat, který typ celých čísel chceš:
pd.Series([0, 123, 12345], dtype="int16")
⚠ Pozor: Když vybíráš konkrétní celočíselný typ, musíš si dát pozor na rozsahy, protože pandas
tě nebude varovat, pokud se nějaká z tvých hodnot do rozsahu "nevleze" a vesele zahodí tu část binární reprezentace, která je navíc a dostaneš mnohem menší číslo, než jsi čekal/a:
pd.Series([0, 123, 12345], dtype="int8")
Toto naštěstí neplatí pro typ s nejširším rozsahem (int64
). Zkusme do něj 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])
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?
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"]
# Docela přesné pí
pd.Series([3.14159265])
# Ne už tak přesné pí
pd.Series([3.14159265], dtype="float16")
Úkol: Vytvoř pole typu float64
jen ze samých celých čísel. Co se stane?
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]
# Vytvoření nového sloupce
pd.Series([True, False, False])
Jde to ovšem i takto:
pd.Series([1, 0, 0], dtype="bool")
Úkol: Co se stane, když vytvoříš Series
typu bool
z řetězců "True"
a "False"
(nezapomeň na uvozovky)?
Aktuální verze knihovny pandas
(1.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
) - 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"]
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
# 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í"
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"])
Ú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
?
Č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"])
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")
Úkol: Napadne tě, které sloupce z tabulky countries
bychom měli překonvertovat na nějaký jiný typ?
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
# Hustota obyvatelstva
countries["population"] / countries["area"]
# Jak nám podražily obědy
pd.Series([109, 99], index=["řízek", "smažák"]) + [20.9, 10.9] # sčítání se seznamem
Ú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"])
💡 Čí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])
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 může výsledek takzvaně přetéct a ukazovat pochybné výsledky. O důvod víc, proč se držet int64
.
pd.Series([7, 14, 149], dtype="int8") * 2
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
# Skoro nikde. A jak jsme na tom u nás?
countries.loc["Czechia", "alcohol_adults"] > 15
# Jsou muži v jednotlivých zemích tlustší než ženy?
countries["bmi_men"] > countries["bmi_women"]
Ú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"
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)
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)
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)
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)
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?
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")
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. 👋