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.
💡 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.
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")
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])
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 (2.1) 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"]
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 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
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. 👋