V této lekci se podíváme na základní nástroje a postupy, které se hodí pro analýzu jedné proměnné. Nebudeme se tedy zabývat vztahy a souvislostmi mezi více proměnnými - to bude předmětem mnohých dalších lekcí. Na pomoc si pro tento účel vezmeme především časové řady s údaji o počasí (teplota, tlak apod.). V práci nám bude významě pomáhat vizualizace.
Abychom s daty mohli efektivně pracovat, budeme muset data ještě pročistit. To je (bohužel) běžnou součástí datové analýzy, protože zdrojová data často obsahují chyby. Při práci s časovými řadami využijeme bohaté možnosti pandas
pro práci s časovými údaji.
Podíváme se na základy statistiky. Dozvíme se, jak pracovat s pojmy střední hodnota, standardní odchylka, medián, kvantil či kvartil. Naučíme se pracovat s histogramy, s boxploty a s distribuční funkcí.
V této lekci se naučíš:
Budeme používat samozřejmě pandas
, pro vizualizaci pak matplotlib
a seaborn
.
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
Naší základní datovou sadou budou data o počasí v České republice.
Data na svých stránkách poskytuje hydrometeorologický ústav (ČHMÚ): https://www.chmi.cz/historicka-data/pocasi/denni-data. Možná překvapivě není úplně snadné dobrá a podrobná data získat. Naštěstí ale existuje služba meteostat a stejnojmenná knihovna pro Python, kterou jsme použili pro stažení dat pro několik českých stanic. Notebook weather_data.ipynb
obsahuje kód pro to použitý, ale nebudeme se jím zde zabývat.
Samotné načtení souboru je poměrně snadné. Použijeme na to třídu ExcelFile
, protože kromě načítání poskytuje také property sheet_names
.
Často stačí (a je jednodušší) použít funkci read_excel
. V dokumentaci si všimni velkého množství argumentů, které umožňují správně načíst všelijak formátované soubory a hodnoty v nich.
DATA_FILENAME = "data-daily.xlsx"
# otevření Excel souboru
denni_excel = pd.ExcelFile(DATA_FILENAME)
denni_excel
Zobrazíme si seznam listů v souboru.
denni_excel.sheet_names
Zatím jsme žádná data nenačetli (nemáme žádný DataFrame
s daty ze souboru). Data načte ve formě DataFrame
až metoda parse
.
# načti data z jednoho listu
denni_ruzyne_original = denni_excel.parse("Praha-Ruzyne")
denni_ruzyne_original
denni_ruzyne_original.dtypes
Pro vysvětlenou dodejme, co který sloupec znamená (viz https://dev.meteostat.net/formats.html#meteorological-parameters):
time
: čas (nebo spíš datum) měřenítavg
: průměrná denní teplota (°C)tmin
: nejnižší denní teplota (°C)tmax
: nejvyšší denní teplota (°C)prcp
: celkový úhrn srážek (mm)snow
: výška sněhové pokrývky (mm)wdir
: směr větru (°)wspd
: rychlost větru (km/h)wpgt
: max. rychlost větru v poryvech (km/h)pres
: tlak (hPa)tsun
: doba slunečního svitu (min)Protože se budeme zabývat časovými řadami, je zcela přirozené, že ze sloupce "time" uděláme index:
denni_ruzyne_index = denni_ruzyne_original.set_index("time")
denni_ruzyne_index
type(denni_ruzyne_index.index)
Vidíme, že mnoho sloupců obsahuje "NaN", neboli "not a number" - to může znamenat ledacos.
⚠️ Poznámka: NaN je původně numerickou reprezentací neplatné matematické operace (jako např. 0 / 0), nicméně v pandas se tento význam směšuje s chybějící hodnotou (missign value, N/A). Je to podobné, ale ne totéž. Naopak některé jiné knihovny (např. polars) mezi oběma významy rigorózně rozlišují, což může být zdrojem nepěkných chyb, když mezi knihovnami člověk přechází.
Pojďme se podívat, jak chybějící/neplatné hodnoty odhalit a co se s nimi dá dělat.
Obecně máme tři základní možnosti.
Nedělat nic, tj. nechat chybějící data chybět. To je možná překvapivě často dobrá volba, protože mnoho funkcí si s chybějícími daty poradí správně. To je rozdíl oproti numpy
, kde funkce typicky NaN
y nemají rády. Často existují varianty funkcí (např. numpy.mean
-> numpy.nanmean
), které NaN
y berou jako chybějící data.
Pozorování (tj. řádky, protože máme tidy data) s chybějícími záznamy vynechat. K tomu slouží metoda dropna
.
Chybějící data nahradit nějakou vhodnou hodnotou. Co jsou vhodné hodnoty, záleží na povaze dat a na tom, co s daty dále děláme. Někdy je vhodné nahradit chybějící hodnoty nějakou "typickou" hodnotou, třeba průměrem. Pro časové řady je většinou logičtější nahradit hodnotou z okolí (předchozí nebo následující). O nahrazování typickými hodnotami (angl. imputation) se dočteš https://scikit-learn.org/stable/modules/impute.html a možná dozvíš víc i později. K nahrazování hodnotami z okolí pak slouží metoda fillna
.
Nejprve bychom ale měli zjistit, kde přesně ty chybějící hodnoty jsou. Metoda isna
nám dokáže "najít" nedefinované hodnoty:
denni_ruzyne_index.isna()
Použití sum
na True
a False
je užitečný trik, True
se počítá jako 1, False
jako 0.
denni_ruzyne_index.isna().sum()
Vidíme tedy, že ve sloupcích s průměrnou teplotou je 19 nedefinovaných hodnot, zatímco pro sloupec wdir
jsou všechny hodnoty NaN (koncept průměrného denního směru větru patrně nedává úplně smysl).
Opak nám ukaže metoda .count
:
denni_ruzyne_index.count()
Otázka: Co vrátí denni_ruzyne_index.isna().count()
?
Sloupec wdir
patrně můžeme vyhodit, protože neobsahuje žádnou užitečnou informaci. Na to stačí .drop
:
denni_ruzyne_bez_wdir = denni_ruzyne_index.drop(columns="wdir")
denni_ruzyne_bez_wdir
A co s onou teplotou?
denni_ruzyne_bez_wdir.loc[denni_ruzyne_bez_wdir["tavg"].isna()]
Oněch 19 řádků odpovídá patrně dnům, kdy meteostanice "neměla" svůj den. Tyto řádky má asi smysl úplně odstranit. Na to použijeme metodu .dropna
, jen budeme muset specifikovat, který sloupec nám vadí:
denni_ruzyne = denni_ruzyne_bez_wdir.dropna(subset=["tavg"])
denni_ruzyne
Mohli bychom zkusit odstranit všechny řádky, kde něco chybí:
denni_ruzyne.dropna()
Popravdě, zbylo by nám jen nějakých 34 řádků, v zimním období, a všechny jsou novější než rok 2022. Proč tomu tak je? Svou roli zde hraje několik faktorů dohromady, které nepůjde snadno rozklíčovat bez vizualizace. A bohužel tedy ani nevím, jak bychom správně aplikovali fillna
.
Úkol: Zkuste odstranit všechny řádky, pro které není definován tsun
. Co nám to říká?
Úkol: Zkuste odstranit všechny řádky, pro které není definován snow
. Co nám to říká?
denni_ruzyne.index.to_period("D")
Data, která jsme načetli a vyčistili, tvoří vlastně několik časových řad v jednotlivých sloupcích tabulky denni_ruzyne
. Granularita (nebo frekvence či časové rozlišení) je jeden den.
Pomocí to_period()
bychom mohli datový typ indexu převést i na period[D]
. To může zrychlit některé operace, pro naše použití to ale není nezbytné.
Už jsme si ukazovali indexování (výběr intervalu) pomocí .loc
. U časových řad to funguje samozřejmě také. Pozor na to, že .loc
vrací data včetně horní meze, na rozdíl od indexování list
ů nebo numpy polí.
Konkrétní období můžeme vybrat třeba takto:
denni_ruzyne.loc[pd.Timestamp(2017, 12, 24) : pd.Timestamp(2018, 1, 1)]
Anebo dokonce i takto zjednodušeně:
denni_ruzyne.loc["2017":"2019"]
Časové proměnné typu DatetimeIndex
poskytují velice užitečnou sadu atributů vracející
.year
vrátí pouze rok, .month
měsíc apod.,.weekday
nebo .weekofyear
is_quarter_start
nebo is_year_end
, které by bylo poměrně náročné zjišťovat numericky.Pokud se jedná o sloupec, je potřeba před atribut vložit ještě .dt
accessor.
denni_ruzyne.index.year
Můžeme tak vybrat jeden celý rok např. takto:
denni_ruzyne.loc[denni_ruzyne.index.year == 2018]
Nebo můžeme získat data pro všechny dny před rokem 1989, které jsou začátky kvartálů a zároveň to jsou pondělky.
denni_ruzyne.loc[
denni_ruzyne.index.is_quarter_start
& (denni_ruzyne.index.weekday == 0)
& (denni_ruzyne.index.year < 1989)
]
Úkol: Nejstarší tag Pandas na https://github.com/pandas-dev/pandas je verze 0.3.0 z 20. února 2011. Jaké bylo v ten den v Praze - Ruzyni počasí?
Úkol: Jaká byla průměrná teplota první (a jedinou) neděli v roce 2010, která byla zároveň začátkem měsíce? Pokud máte řešení a čas, zkuste vymyslet aternativní způsob(y).
# odkomentuj a doplň
# denni_ruzyne.loc[
# ___
# & (___)
# & (___),
# "teplota průměrná",
# ]
Vykreslíme si naše data co nejjednoduššími způsoby, jaké nám pandy nabízí. Zkusme třeba rovnou .plot()
denni_ruzyne.plot();
Tam toho moc vidět není. Jedním z problémů je různá škála veličin. A také bychom si graf mohli trochu zvětšit.
denni_ruzyne.plot(subplots=True, figsize=(12, 9));
Tohle už je docela užitečné, leccos na grafu vidět je. Výchozí čárový (line
) graf je pro časové řady často vhodný.
Navíc už si graficky odpovídáme na to, proč máme tolik hodnot, kolik jich máme: Různé veličiny se v některých letech prostě vůbec neměřily, jiné se měřily jen občas.
Pomocí argumentu layout
můžeme podgrafy uspořádat do více sloupců. Pokud navíc vybereme kratší časové období, dostaneme už celkem srozumitelný výsledek.
denni_ruzyne.loc["2023"].plot(
subplots=True, layout=(4, 3), figsize=(12, 9)
);
I tady je ale dat poměrně hodně a na grafech vidíme spoustu rozptylu - hodnoty skáčou rychle nahoru / dolů. V takovém případě je na čase vzít si na pomoc statistiku!
Není cílem tohoto kurzu (a ani v jeho možnostech) podrobně a rigorózně učit statistiku ("Statistika nuda je..."). Jednoduché základy, které zvládají i 🐼🐼🐼, spolu jistě zvládneme a přesvědčíme se, že jsou i užitečné ("...má však cenné údaje...").
Pokud se budeš chtít dozvědět víc, koukni třeba na https://www.poritz.net/jonathan/share/ldlos.pdf, nebo na http://greenteapress.com/thinkstats2/thinkstats2.pdf nebo třeba i na Bayesovskou statistiku http://www.greenteapress.com/thinkbayes/thinkbayes.pdf.
Metoda DataFrame.describe
je jednoduchou volbou pro získání základních statistik celé tabulky.
denni_ruzyne.describe()
Pro každý sloupec vidíme několik souhrnných (statistických údajů).
count
udává počet hodnot.mean
je střední hodnota, vypočítaná jako aritmetický průměr.std
je směrodatná odchylka, která ukazuje rozptyl dat - jak moc můžeme očekávat, že se data v souboru budou lišit od střední hodnoty.min
a max
jsou nejmenší a největší hodnoty ve sloupci.25%
a 75%
je hodnota prvního a třetího "kvartilu". Pokud bychom sloupec seřadili podle velikosti, bude čtvrtina dat menší než hodnota prvního kvartilu a čtvrtina dat bude větší než hodnota třetího kvartilu. Konkrétně čtvrtina všech dní v našich datech měla minimální teplotu menší než -0.7 °C a čtvrtina dní zase měla maximální teplotu větší než 20.5 °C.50%
se označuje jako medián - polovina dat je menší než medián (a ta druhá polovina je samozřejmě zase větší než medián).Za chvilku si ještě ukážeme, jak tyto hodnoty souvisí s distribuční funkcí a vše ti bude hned jasnější :)
describe
můžeme samozřejmě použít i na nějakou podmožinu dat. Takto třeba vypadá statistika počasí v Ruzyni v lednu.
denni_ruzyne[denni_ruzyne.index.month == 1].describe()
Pojďme zkusit pojmy kolem pravděpodobnosti, jako třeba rozdělovací funkce nebo hustota pravděpodobnosti, jejichž formální definice a vlastnosti lze najít v knihách (např. v těch uvedených výše) nebo na wikipedii, objevovat a zkoumat spíš názorně a intuitivně.
Jedním ze základních a nesmírně užitečných nástrojů na vizualizaci souboru dat je histogram. Zjednodušeně řečeno, histogram vytvoří chlívečky podle velikosti dat - do každého chlívečku patří data v nějakém intervalu od - do. Počet hodnot, které ze souboru dat spadnou do daného chlívečku, určuje velikost (výšku) chlívečku.
Histogram zobrazíme pomocí .plot.hist()
denni_ruzyne["pres"].plot.hist(edgecolor="black");
Histogram nám říká, že někde v rozmezí 1008 - 1023 je nejvíce hodnot (~7000). Okolní chlívečky mají už výrazně menší velikost, sotva poloviční. Na okrajích jsou jen nízké chlívečky, jakési ocasy.
Přemýšlej - kdybychom si vybrali náhodně jeden den.
Pokud dokážeš na otázky odpovědět, tak už vlastně víš, že histogram udává hustotu pravděpodobnosti a že tahle hustota se dá sčítat, čímž se dostane kumulovaná pravděpodobnost, neboli také distribuční funkce.
Definice je vlastně docela jednoduchá (zdroj wikipedia):
Distribuční funkce, funkce rozdělení (pravděpodobnosti) nebo (spíše lidově) (zleva) kumulovaná pravděpodobnost (anglicky Cumulative Distribution Function, CDF) je funkce, která udává pravděpodobnost, že hodnota náhodné proměnné je menší než zadaná hodnota.
Hustota pravděpodobnosti vyjadřuje, kolik "pravděpodobnosti" přibude na daném intervalu, neboli o kolik se změní distrubuční funkce. Matematicky je hustota pravděpodobnosti derivací distribuční funkce.
Poměrně důležitým parametrem u histogramu je počet chlívků. Když jich je málo, může zaniknout důležitá informace, moc chlívků může zase vnést velký šum.
Pro naše data vypadá histogram s třiceti chlívky celkem rozumně.
denni_ruzyne["pres"].plot.hist(bins=30, figsize=(12, 6), edgecolor="black");
Argument cumulative=True
nám pak zobrazí postupný (kumulativní) součet velikosti chlívků. Použijeme ještě density=True
, abychom zobrazili distribuční funkci. Takto nám graf říká, jaká je pravděpodobnost (hodnota na vertikální ose), že tlak bude menší než daná hodnota (na horizontální ose).
V grafu jsou ještě přidané svislé čáry pro střední hodnotu (černá), medián (červená) a 25% a 75% kvantily (červené přerušované).
ax = denni_ruzyne["pres"].plot.hist(
bins=30, figsize=(12, 6), cumulative=True, density=True, grid=True
)
ax.set_yticks(np.arange(0, 1.1, 0.25))
ax.axvline(denni_ruzyne["pres"].mean(), color="k")
ax.axvline(denni_ruzyne["pres"].median(), color="r")
ax.axvline(denni_ruzyne["pres"].quantile(0.25), color="r", ls="--")
ax.axvline(denni_ruzyne["pres"].quantile(0.75), color="r", ls="--")
Podívejme se, jak vypadají histogramy všech devíti veličin.
denni_ruzyne.hist(figsize=(12, 9), bins=30);
denni_ruzyne.hist(figsize=(12, 9), bins=30, cumulative=True, density=True);
Je poměrně zajímavé a příhodné, že dostáváme poměrně hezkou paletu různých typů rozdělovací funkce. Tlak vzduchu má přibližně normální (Gaussovo) rozdělení. O tom jste možná slyšeli, protože se vyskytuje a používá poměrně často (někdy až příliš často). U teploty je zajímavé, že má tzv. bi-modální rozdělení - na histogramu jsou dvě maxima. U dalších veličin se můžeme zamyslet, která z mnoha známých distribucí by se na jejich popis více či méně hodila. Logaritmicko-normální na rychlost větru? Nějaká exponenciální (nebo obecně gamma) distribuce výšky sněhu, úhrnu srážek a možná slunečního svitu? Toto ponechejme na nějaký podrobnější kurz statistiky, meteorologie či klimatologie :)
Různorodé distribuční funkce nám ale umožní ukázat některé vlastnosti střední hodnoty a mediánu. To jsou (společně s módy, tedy maximy hustoty pravděpodobnosti) ukazatele centrální tendence souboru dat. Medián a střední hodnota se poměrně často neliší a u "hezkých" (symetrických) distribucí, jako je normální rozdělení, jsou totožné. Lišit se budou zejména tehdy, když je distribuce sešikmená (angl. skewed) nebo pokud jsou v datech odlehlé hodnoty, spíše známé pod anglickým výrazem outliers.
Zabalíme do funkce vykreslovaní histogramu spolu se střední hodnotou a kvantily, které jsme použili již dříve. Poté použijeme velice užitečnou knihovnu seaborn na vykreslení histogramů pro jednotlivé veličiny.
def hist_plot_with_extras(data, bins=30, cumulative=False, density=False, **kwargs):
"""Plot histogram with mean and quantiles"""
ax = kwargs.pop("ax", plt.gca())
ax.hist(data, bins=bins, cumulative=cumulative, density=density, **kwargs)
ax.grid(True)
if density:
ax.set_yticks(np.arange(0, 1.1, 0.25))
ax.axvline(data.mean(), color="k")
ax.axvline(data.median(), color="r")
ax.axvline(data.quantile(0.25), color="r", ls="--")
ax.axvline(data.quantile(0.75), color="r", ls="--")
return ax
Teď můžeme použít FacetGrid
, který vytváří sadu grafů, rozdělených do mřížky podle nějaké vlastnosti dat (kategorie).
Jenže facetgrid očekává přesně jeden sloupec s hodnotami a jeden sloupec s kategorií, podle které má vytvořit podgrafy. Musíme pro něj vytvořit "dlouhou" tabulku: vždy název proměnné ("variable") a její hodnota ("value"). Datum (index) můžeme zahodit, protože pro účely histogramu ho nepotřebujeme. K tomuto převedení tabulky z široké na dlouhou slouží metoda .melt
:
# Začneme s jednoduchou tabulkou
df = pd.DataFrame(
{
"A": [1, 2, 3],
"B": [4, 5, 6],
"C": [7, 8, 9],
}
)
df
# A převedeme její sloupce na řádky
df.melt()
Aplikováno na naše data:
denni_ruzyne.melt()
grid = sns.FacetGrid(
denni_ruzyne.melt(),
col="variable",
col_wrap=3,
sharey=False,
sharex=False,
aspect=2,
)
grid.map(hist_plot_with_extras, "value");
Kromě histogramu se velice často používá pro zobrazení distribuce tzv. boxplot. "Krabička" (obdélník) uprostřed vymezuje oblast mezi prvním a třetím kvartilem (Q1 a Q3), dělicí čára odpovídá mediánu, a "vousy" (anglicky whiskers) značí rozsah dat. Obvykle je to poslední bod, který je blíže než 1,5násobek "inter-quartile range" IQR, IQR = Q3 - Q1
od Q1 či Q3. Tento rozsah se obvykle považuje za mez pro odlehlé hodnoty, které jsou pak v boxplotu vyznačeny jako symboly (kosočtverce v našem případě).
grid = sns.FacetGrid(
denni_ruzyne.melt(),
col="variable",
col_wrap=2,
sharey=False,
sharex=False,
aspect=2,
)
grid.map(sns.boxplot, "value");
Seaborn se často dá použít velice jednoduše, pokud zobrazujeme jednu veličinu, a někdy stráví i "wide-format" data. U našich dat můžeme takto porovnat průměrnou, minimální a maximální teplotu. Na pomoc si vezmeme catplot
, který vytváří graf (nebo i sadu grafů) různých typů (boxplot nebo třeba violinplot) z dat obsahujících jednu či více kategorických proměnných.
sns.catplot(
data=denni_ruzyne[["tavg", "tmin", "tmax"]],
orient="h",
kind="box",
aspect=2,
);
Úkol: Doplňte pomocné sloupce season
a significant_precipitation
(jistě uhádnete jakého pandas-typu budou :). První definuje roční období (jen jednoduše podle kalendářních měsíců), druhý označuje dny, kdy byly srážky vyšší než v 90 % všech dní v našich datech (můžete zkusit i jiný limit).
sns.catplot
pro vizuální srovnání distribučních funkcí pro jednotlivá roční období a dny s málo / hodně srážkami.# odkomentuj a doplň
# season = denni_ruzyne.index.___.map({
# 1: "zima",
# 2: "zima",
# 3: "jaro",
# ...
# })
# significant_precipitation = denni_ruzyne["prcp"] > denni_ruzyne[___].quantile(___)
# úkol - jednoduché srovnání statistik pomocí rozdílu
# (denni_ruzyne.loc[___]
# .describe()
# ) - \
# denni_ruzyne.___()
# úkol - vizuální srovnání statistik
# sns.catplot(
# data=denni_ruzyne.assign(
# significant_precipitation=___,
# season=___,
# ),
# kind="box",
# aspect=2,
# hue=___,
# y=___,
# x=___,
# );
rocni_ruzyne = denni_ruzyne.resample("1YE")
Co že jsme to vlastně vytvořili?
rocni_ruzyne
Dostali jsme instanci třídy DatetimeIndexResampler
. To zní logicky, ale kde jsou data? Ta zatím nejsou, protože jsme ještě pandám neřekli, jak vlastně mají ze všech těch denních údajů v rámci jednoho roku vytvořit ta roční data. Neboli, jak data agregovat.
Ze statistiky víme, že jedním z ukazatelů může být střední hodnota. Zkusíme vypočítat průměrnou "teplotu průměrnou" (není to překlep) a rovnou vykreslit.
rocni_ruzyne["tavg"].mean().plot();
Trochu podobnou operací jako resampling je rolování. To spočívá v plynulém posouvání "okna", které slouží pro (vážený) výběr dat a následné aplikaci agregační funkce (jako u resample
). Pojďme pomocí rolling
vytvořit podobný pohled na roční průměrnou teplotu. Rozdíl oproti resample
je v tom, že dostaneme pro každý den jednu hodnotu, nikoli jen jednu hodnotu pro celý rok. A také už nemůžeme použít interval "1 rok", protože jeden (kalendářní) rok není dobře definovaný interval díky přestupným rokům.
denni_ruzyne["pres"].rolling("365D", min_periods=365).mean().plot();
Pro podrobnější přehled práce s časovými řadami se podívej např. na https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html nebo třeba na hezký článek s podobnými daty https://www.dataquest.io/blog/tutorial-time-series-analysis-with-pandas/.
Úkol: Navrhněte vhodnou roční agregaci pro maximální teplotu (ne průměr) a vykreslete.
# odkomentuj a doplň
# rocni_ruzyne[___].___().___();
Úkol: Najděte, kdy v období 2000-2025 napršelo nejvíc za týden (tedy za průběžných 7 dnů). Odpovídá to nějaké vaší historické zkušenosti?
# doplňte nebo vyřešte po svém :)
# denni_ruzyne.loc[___][___].rolling(___).sum().sort_values(ascending=False).iloc[:10]
Nepovinný úkol: Rozšiřte hledání na celé období v naší datové sadě - jsou některé hodnoty realistické?
Zatím jsme měli data s jednodenní hustotou a příliš jsme se nezabývali hodinami, časovými zónami, přechodem mezi letním a zimním časem. Zkusme si načíst výrazně jemnější data o pražském počasí:
hodinove_ruzyne_original = pd.read_parquet("praha-meteostat.parquet")
hodinove_ruzyne_original
hodinove_ruzyne_original.dtypes
U pozorování máme přirazený nějaký čas. Ale co to znamená? Možná je z kontextu patrné, kde bylo "time" hodin, ale pak se dost pravděpodobně mýlíte - důležitá je totiž definice podle zdroje data. Pokud chcete cokoliv správně vyhodnocovat s časovými daty, musí jim být přiřazeno časové pásmo. Mnohdy se na to zapomíná ("tohle opravíme potom") a používají se "naivní" časové údaje, ale bývá to často příčinou mnohých zmatků.
Při čtení definice dat se dočteme, že všechna data jsou uložena v UTC (takže v Praze je v létě o 2 h více, v zimě o 1 h). Měli bychom to naší tabulce explicitně říct. Naštěstí to není tak složité - metoda .dt.tz_localize
slouží přesně k tomu:
hodinove_ruzyne_original["time"].dt.tz_localize("UTC")
hodinove_ruzyne_utc = hodinove_ruzyne_original.assign(
time=hodinove_ruzyne_original["time"].dt.tz_localize("UTC")
)
hodinove_ruzyne_utc
Pokud si pak chceme ukázat data (a to nás zajímá?) v středovském časovém pásmu, slouží k tomu .dt.tz_convert
:
hodinove_ruzyne = hodinove_ruzyne_utc.assign(
time_prague=hodinove_ruzyne_utc["time"].dt.tz_convert("Europe/Prague")
).set_index("time_prague")
# Podíváme se na data v době přechodu z letního na zimní čas
hodinove_ruzyne["2024-10-27":"2024-10-27"]
Poznámka: Uvědomme si, že i po operaci tz_convert
řádky odkazují na tentýž okamžik v historii světa (pomiňme relativistickou fyziku ;-)), mění se jen jejich reprezentace. Naopak přiřazením různých časových pásem k naivním časům vznikají jiné okamžiky.
Potom už si pěkně můžeme vykreslit průběh teploty v době slavného poklesu teplot na Silvestra 1978 (a budeme to mít správně i po hodinách):
hodinove_ruzyne["1978-12-31":"1979-01-01"]["temp"].plot();
Úkol: Jak silný foukal vítr (jak moc pršelo) v kterou hodinu den tvého narození?