Obsah
1. Knihovna Polars: výkonnější alternativa ke knihovně Pandas (líné vyhodnocování operací)
2. Líné operace a líné vyhodnocování v IT
3. Koncept líných datových rámců
4. Líné načtení dat do datového rámce ze souboru ve formátu CSV
5. Převod běžného datového rámce na líný datový rámec
6. Převod líného datového rámce na běžný datový rámec
7. Ukázka podpory líných operací v knihovně Polars
8. Konstrukce plánu s větším množstvím operací; realizace plánu
9. Větší množství naplánovaných operací
10. Ladění naplánovaných operací s využitím omezeného objemu dat
11. Limity operace fetch při agregaci dat
12. Další snížení počtu řádků vracených operací fetch: zvýraznění nekorektních výsledků
13. Operace head aplikovaná na líný rámec
14. Rozvětvení a opětovné spojení plánů
16. Vizualizace plánu s operacemi SLICE
17. Obsah závěrečné části seriálu o knihovně Polars
18. Repositář s demonstračními příklady
1. Knihovna Polars: výkonnější alternativa ke knihovně Pandas (líné vyhodnocování operací)
Jak jsme si již několikrát připomenuli v předchozí dvojici článků [1] [2], je knihovna Polars navržena takovým způsobem, aby byly operace s daty uloženými v datových řadách nebo v datových rámcích realizovány co nejrychleji, ideálně s využitím souběžně běžících úloh, ale i s využitím moderních SIMD operací. Mnohem užitečnější je však další vlastnost této knihovny – schopnost pracovat s daty, jejichž objem je větší než volná kapacita operační paměti. Vzhledem k tomu, že se jedná o velmi důležitou vlastnost (a v mnoha případech vlastně o jediný důvod, proč vlastně uvažovat o přechodu od Pandas k Polars), budeme se touto velmi zajímavou problematikou zabývat v dnešním článku.
2. Líné operace a líné vyhodnocování v IT
V informatice se na mnoha místech setkáme s využitím takzvaných „líných“ operací popř. „líných datových struktur“ resp. „líného vyhodnocování“. Jedná se o koncept, který je založen na tom, že se nějaký výpočet či operace neprovede ihned ve chvíli, kdy je v programu zapsána, ale obecně se její vykonání přesune do budoucnosti s tím předpokladem, že mnohdy vlastně není nutné operaci provádět vůbec nebo ne v plném rozsahu. Připomeňme si například, jak jsou realizovány „líné sekvence“ v programovacím jazyku Clojure. Obecně platí, že se prvky v líných sekvencích vyhodnocují až tehdy, kdy je to nezbytně nutné a předpokládá se, že k vyhodnocení nemusí dojít vůbec.
Příkladem může být líná sekvence, která vznikne aplikací funkcí range + filter + map + take. V Clojure můžeme pro větší čitelnost použít threading makro, takže výsledný zápis připomíná klasickou pipelinu:
(->> (range)
(map #(* % 3))
(filter #(even %))
(take 10))
Funkce range obecně (pokud se jí nezadají další parametry) generuje nekonečnou sekvenci, ovšem díky pozdějšímu použití take se z této nekonečné sekvence získá jen prvních n prvků – a až za podmínky, kdy se musí pracovat s hodnotou prvku (například když se má výsledek vytisknout). Aplikace funkcí range, filter a map jsou tedy provedeny později či vůbec ne. V našem konkrétním případě bude výsledkem tato konečná a realizovaná sekvence:
(0 6 12 18 24 30 36 42 48 54)
3. Koncept líných datových rámců
V tomto seriálu jsme se již několikrát zmínili o funkci nazvané read_csv. Připomeňme si, že tato funkce slouží pro načtení dat uložených ve formátu CSV (comma separated values), TSV (tab separated values) popř. z textového souboru s pevně zadanou strukturou. Výsledkem je plnohodnotný datový rámec, jenž je uložený v operační paměti a na který je možné aplikovat všechny předminule i minule popsané operace, včetně seskupení dat s jejich následnou agregací. Například:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# přečtení zdrojových dat
df = polars.read_csv("hall_of_fame.csv")
# maximální počet zobrazených řádků
polars.Config.set_tbl_rows(100)
# seskupení podle názvu jazyka
df = df.groupby("Winner", maintain_order=True).agg([polars.col("Year").len().alias("Zvítězil")]). \
sort("Zvítězil"). \
reverse(). \
head(5)
# zobrazíme datový rámec
print(df)
Jakým způsobem je ale vůbec možné pracovat s daty, která mají větší objem, než je volná kapacita operační paměti? Řešením jsou takzvané líné datové rámce. V případě použití líných rámců se operace vyžadované uživatelem neprovádí hned, ale až ve chvíli, kdy jsou výsledky skutečně zapotřebí – vyžadované operace jsou tedy zapamatovány ve formě plánu. A navíc je vykonávání operací řešeno formou „streamu“, tj. v naprosté většině případů se nevyžaduje, aby byl celý datový rámec uložen v operační paměti. Práci s línými datovými rámci si ostatně ukážeme v navazujících kapitolách.
4. Líné načtení dat do datového rámce ze souboru ve formátu CSV
Podívejme se nyní na způsob „líného“ načtení datového rámce ze souboru, v němž jsou data uložena ve formátu CSV. Namísto funkce read_csv použijeme funkci nazvanou scan_csv, která má stejné povinné i nepovinné parametry, jako již zmíněná funkce read_csv, takže záměna v existujících skriptech je možná a hlavně snadná:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme líně načtený datový rámec
print(df)
print()
Výsledek, který získáme po spuštění tohoto skriptu, je zcela odlišný od výsledku operace read_csv. Funkce scan_csv totiž pouze zaznamená, jaká operace se má provést a uloží tento záznam do plánu. A tento plán je skriptem zobrazen:
naive plan: (run LazyFrame.describe_optimized_plan() to see the optimized plan) CSV SCAN hall_of_fame.csv PROJECT */2 COLUMNS
Výsledkem je tedy líný datový rámec. Naproti tomu použití funkce read_csv vede k okamžitému načtení dat a výsledkem bude běžný datový rámec:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# okamžité přečtení zdrojových dat
df = polars.read_csv("hall_of_fame.csv")
# zobrazíme načtený datový rámec
print(df)
print()
S výsledkem:
shape: (20, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ ... ┆ ... │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴────────┘
5. Převod běžného datového rámce na líný datový rámec
Knihovna Polars umožňuje provést převod běžného datového rámce (tj. rámce s vyhodnocenými daty) na líný datový rámec. Pro tento účel se používá metoda nazvaná příznačně lazy. Podívejme se nyní na to, jak tento převod může proběhnout v praxi. V dalším demonstračním příkladu nejdříve načteme obsah souboru ve formátu CSV do běžného datového rámce, jehož obsah je následně zobrazen na terminálu. Posléze z tohoto rámce vytvoříme líný datový rámec s využitím již zmíněné metody lazy a následně tento líný datový rámec zobrazíme:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# přečtení zdrojových dat
df = polars.read_csv("hall_of_fame.csv")
# převod na líný datový rámec
df2 = df.lazy()
# zobrazíme načtený datový rámec
print(df)
print()
# následně zobrazíme líný datový rámec
print(df2)
print()
Ze zobrazených výsledků je patrné, že se nejdříve skutečně zobrazí obsah běžného datového rámce:
shape: (20, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ ... ┆ ... │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴────────┘
Následně se namísto obsahu líného datového rámce vzniklého konverzí zobrazí – přesně podle očekávání – pouze plán, tj. seznam operací, které se mají provést v budoucnosti:
naive plan: (run LazyFrame.describe_optimized_plan() to see the optimized plan) DF ["Year", "Winner"]; PROJECT */2 COLUMNS; SELECTION: "None"
6. Převod líného datového rámce na běžný datový rámec
Způsob převodu běžného datového rámce na líný datový rámec jsme si ukázali v předchozí kapitole. Mnohdy je však mnohem důležitější provést opačný převod, tedy převod líného rámce na běžný rámec. V tomto případě však není slovo „převod“ zcela přesné, protože se nejedná o transformaci dat, ale o „realizaci“ (uskutečnění) všech operací, které byly pouze naplánovány. Podívejme se na jednoduchý příklad, v němž se převod/realizace provádí metodou nazvanou collect:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme líně načtený datový rámec
print(df)
print()
# převod na běžný datový rámec
df2 = df.collect()
# zobrazíme běžný (výsledný) datový rámec
print(df2)
print()
print(df2.columns)
print(df2.dtypes)
Po spuštění tohoto demonstračního příkladu se nejdříve zobrazí plán pro líný datový rámec:
naive plan: (run LazyFrame.describe_optimized_plan() to see the optimized plan) CSV SCAN hall_of_fame.csv PROJECT */2 COLUMNS
Ve druhém kroku se zobrazí realizovaný konkrétní datový rámec vytvořený metodou collect:
shape: (20, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ ... ┆ ... │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴────────┘ ['Year', 'Winner'] [Int64, Utf8]
7. Ukázka podpory líných operací v knihovně Polars
Nyní se dostáváme k velmi důležité vlastnosti knihovny Polars. Připomeňme si, že dokud není nutné pracovat s daty podrobenými nějaké operaci nebo sérií operací, není vlastně nutné tyto operace ani provádět – postačuje si pouze zapamatovat jejich pořadí a případný použitý parametr nebo parametry. A přesně tímto způsobem se pracuje s línými datovými rámci, protože každá další operace nad rámcem se „pouze“ zapíše do plánu.
Samozřejmě si tento koncept můžeme velmi snadno otestovat, a to konkrétně na demonstračním příkladu, v němž se pokusíme záznamy v líném datovém rámci seřadit podle sloupce „Winner“. V případě, že by se následně nezavolala operace collect, k vlastnímu řazení by vůbec nedošlo:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme líně načtený datový rámec
print(df)
print()
# aplikace operace na líný datový rámec
df2 = df.sort("Winner")
# převod na běžný datový rámec
df3 = df2.collect()
# zobrazíme druhý líny datový rámec
print(df2)
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
Opět je vhodné si alespoň ve stručnosti okomentovat jednotlivé informace vypsané po spuštění tohoto demonstračního příkladu. Nejdříve se líně načtou data ze souboru s formátem CSV. Výsledkem je líný datový rámec, který je zobrazen formou svého plánu, tedy následovně:
naive plan: (run LazyFrame.describe_optimized_plan() to see the optimized plan) CSV SCAN hall_of_fame.csv PROJECT */2 COLUMNS
Z tohoto líného datového rámce je s využitím operace sort vytvořen nový líný datový rámec, jehož plán je pochopitelně odlišný – obsahuje totiž i onu operaci seřazení:
naive plan: (run LazyFrame.describe_optimized_plan() to see the optimized plan)
SORT BY [col("Winner")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
A konečně se po provedení operace collect realizují obě naplánované operace, tedy načtení dat ze souboru typu CSV a seřazení záznamů na základě obsahu sloupce „Winner“. Výsledkem těchto dvou operací je již běžný datový rámec s tímto obsahem:
shape: (20, 2) ┌──────┬──────────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪══════════════╡ │ 2019 ┆ C │ │ 2017 ┆ C │ │ 2008 ┆ C │ │ 2022 ┆ C++ │ │ ... ┆ ... │ │ 2010 ┆ Python │ │ 2007 ┆ Python │ │ 2006 ┆ Ruby │ │ 2013 ┆ Transact-SQL │ └──────┴──────────────┘ ['Year', 'Winner'] [Int64, Utf8]
8. Konstrukce plánu s větším množstvím operací; realizace plánu
Samotný plán postupně vytvářený pro líné datové rámce pochopitelně může obsahovat i větší množství operací a dokonce je ho možné i větvit. Nejdříve si ukažme, jak by vypadal plán se třemi operacemi:
- (Líné) načtení datového rámce
- Seřazení záznamů podle zvoleného sloupce
- Otočení pořadí všech záznamů
Tyto operace jsou postupně definovány v následujícím skriptu:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme líně načtený datový rámec
print(df)
print()
# aplikace operace na líný datový rámec
df2 = df.sort("Winner").reverse()
# převod na běžný datový rámec
df3 = df2.collect()
# zobrazíme druhý líny datový rámec
print(df2)
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
Povšimněte si, jak se liší druhý plán od plánu z předchozího demonstračního příkladu – operace reverse je rozepsána na dvě paralelně probíhající operace:
naive plan: (run LazyFrame.describe_optimized_plan() to see the optimized plan)
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
naive plan: (run LazyFrame.describe_optimized_plan() to see the optimized plan)
LOCAL SELECT [col("Year").reverse(), col("Winner").reverse()] FROM
SORT BY [col("Winner")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
shape: (20, 2)
┌──────┬──────────────┐
│ Year ┆ Winner │
│ --- ┆ --- │
│ i64 ┆ str │
╞══════╪══════════════╡
│ 2013 ┆ Transact-SQL │
│ 2006 ┆ Ruby │
│ 2007 ┆ Python │
│ 2010 ┆ Python │
│ ... ┆ ... │
│ 2022 ┆ C++ │
│ 2008 ┆ C │
│ 2017 ┆ C │
│ 2019 ┆ C │
└──────┴──────────────┘
['Year', 'Winner']
[Int64, Utf8]
9. Větší množství naplánovaných operací
Počet operací postupně „líně“ aplikovaných na data není prakticky nijak omezen, takže ke dvojici operací scan_csv+sort můžeme velmi snadno přidat operaci další, například reverse. I tato operace bude do výsledného plánu přidána a vykonána později, pokud to bude explicitně požadováno:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme plán pro líně načtený datový rámec
print(df.describe_plan())
print(df.describe_optimized_plan())
print()
# aplikace operace na líný datový rámec
df2 = df.sort("Winner").reverse()
# převod na běžný datový rámec
df3 = df2.collect()
# zobrazíme plán pro druhý líny datový rámec
print(df2.describe_plan())
print(df2.describe_optimized_plan())
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
Výsledkem činnosti tohoto skriptu bude plán obsahující operaci sort; výsledný datový rámec je pak složen ze dvou datových řad (series), jejichž prvky jsou explicitně otočeny. To je ostatně zajímavý koncept – sort je operace naplánovaná pro celý datový rámec zatímco reverse jako operace aplikované na jednotlivé sloupce:
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
LOCAL SELECT [col("Year").reverse(), col("Winner").reverse()] FROM
SORT BY [col("Winner")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
LOCAL SELECT [col("Year").reverse(), col("Winner").reverse()] FROM
SORT BY [col("Winner")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
shape: (20, 2)
┌──────┬──────────────┐
│ Year ┆ Winner │
│ --- ┆ --- │
│ i64 ┆ str │
╞══════╪══════════════╡
│ 2013 ┆ Transact-SQL │
│ 2006 ┆ Ruby │
│ 2007 ┆ Python │
│ 2010 ┆ Python │
│ ... ┆ ... │
│ 2022 ┆ C++ │
│ 2008 ┆ C │
│ 2017 ┆ C │
│ 2019 ┆ C │
└──────┴──────────────┘
['Year', 'Winner']
[Int64, Utf8]
10. Ladění naplánovaných operací s využitím omezeného objemu dat
Prozatím jsme při převodu líného datového rámce na běžný datový rámec používali metodu collect, která spustila všechny operace a posléze výsledek těchto operací zkonvertovala do běžného datového rámce. Namísto metody collect je však možné použít například i metodu fetch, které se předá požadovaný počet řádků ve výsledku. Tato metoda se používá například tehdy, pokud je nutné provést ladění celého skriptu a vstupní data jsou zbytečně objemná (ovšem přesný počet řádků ve výsledku není garantován – je pouze přibližný):
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme plán pro líně načtený datový rámec
print(df.describe_plan())
print()
# aplikace operace na líný datový rámec
df2 = df.sort("Winner").reverse()
# převod vybraných prvků na běžný datový rámec
df3 = df2.fetch(5)
# zobrazíme plán pro druhý líny datový rámec
print(df2.describe_plan())
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
Výsledný plán je naprosto stejný, jako v předchozím příkladu:
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
LOCAL SELECT [col("Year").reverse(), col("Winner").reverse()] FROM
SORT BY [col("Winner")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
Liší se ovšem výsledná podoba získaného datového rámce, který bude obsahovat jen pět řádků (záznamů):
shape: (5, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2018 ┆ Python │ │ 2020 ┆ Python │ │ 2021 ┆ Python │ │ 2022 ┆ C++ │ │ 2019 ┆ C │ └──────┴────────┘ ['Year', 'Winner'] [Int64, Utf8]
11. Limity operace fetch při agregaci dat
Demonstrační příklad z předchozí kapitoly byl založen na operaci fetch, která z líného datového rámce přečetla v daném případě přesný počet řádků. Ovšem tak tomu nemusí být vždy. Podívejme se na následující skript, kde vyžadujeme přečtení pěti řádků, ovšem z líného datového rámce, který vznikl agregací dat:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme plán pro líně načtený datový rámec
print(df.describe_plan())
print()
# seskupení podle názvu jazyka
df2 = df.groupby("Winner", maintain_order=True).agg([polars.col("Year")])
# převod vybraných prvků na běžný datový rámec
df3 = df2.fetch(5)
# zobrazíme plán pro druhý líny datový rámec
print(df2.describe_plan())
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
Za povšimnutí stojí v tomto případě nikoli vlastní plán, ale počet řádků uložených ve výsledném datovém rámci. Vrátí se tři řádky a nikoli pět řádků. Je tomu tak proto, že při agregaci se pracuje s pěti hodnotami „Year“, které jsou však agregovány do již zmíněných třech řádků:
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
Aggregate
[col("Year")] BY [col("Winner")] FROM
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
shape: (3, 2)
┌────────┬────────────────────┐
│ Winner ┆ Year │
│ --- ┆ --- │
│ str ┆ list[i64] │
╞════════╪════════════════════╡
│ C++ ┆ [2022] │
│ Python ┆ [2021, 2020, 2018] │
│ C ┆ [2019] │
└────────┴────────────────────┘
['Winner', 'Year']
[Utf8, List(Int64)]
12. Další snížení počtu řádků vracených operací fetch: zvýraznění nekorektních výsledků
Pro zajímavost se podívejme, jaká situace nastane ve chvíli, kdy ještě více snížíme počet operací pomocí fetch, a to konkrétně na pouhé dva záznamy. Nyní bude výsledný datový rámec získaný po provedení všech operací obsahovat dva řádky (což bychom mohli očekávat), ale navíc tyto řádky nebudou obsahovat všechny potřebné údaje:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme plán pro líně načtený datový rámec
print(df.describe_plan())
print()
# seskupení podle názvu jazyka
df2 = df.groupby("Winner", maintain_order=True).agg([polars.col("Year")])
# převod vybraných prvků na běžný datový rámec
df3 = df2.fetch(2)
# zobrazíme plán pro druhý líny datový rámec
print(df2.describe_plan())
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
Nyní obsahuje výsledný datový rámec informaci o tom, že C++ vyhrál pouze v roce 2022 (a nikoli 2×) a Python v roce 2021 (a nikoli celkem 5×)! Už z těchto výsledků vyplývá, že fetch se skutečně hodí jen pro ladicí účely a nikoli pro „zkrácené“ výpočty s reálnými daty a očekávanými reálnými výsledky:
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
Aggregate
[col("Year")] BY [col("Winner")] FROM
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
shape: (2, 2)
┌────────┬───────────┐
│ Winner ┆ Year │
│ --- ┆ --- │
│ str ┆ list[i64] │
╞════════╪═══════════╡
│ C++ ┆ [2022] │
│ Python ┆ [2021] │
└────────┴───────────┘
['Winner', 'Year']
[Utf8, List(Int64)]
13. Operace head aplikovaná na líný rámec
Pokud skutečně vyžadujeme už v rámci líného vyhodnocování operací prováděných nad datovými rámci zmenšit množství dat ve zpracovávaných datových rámcích, musí se namísto poněkud problematické a neintuitivní (viz výše) operace fetch použít líná varianta operace head nebo tail. Tyto operace vrací nový líný rámec, takže pokud se má výsledek vytisknout či jiným způsobem zpracovat, musí následovat operace collect. Podívejme se na jednoduchý příklad:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme plán pro líně načtený datový rámec
print(df.describe_plan())
print()
# seskupení podle názvu jazyka
df2 = (
df.groupby("Winner", maintain_order=True)
.agg([polars.col("Year").len().alias("Zvítězil")])
.sort("Zvítězil")
.reverse()
.head(5)
)
# převod prvků na běžný datový rámec
df3 = df2.collect()
# zobrazíme plán pro druhý líny datový rámec
print(df2.describe_plan())
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
Nyní je zajímavé se podívat na plán, který nově obsahuje operaci SLICE, která z líného datového rámce přečte pouze prvních pět záznamů:
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
SLICE[offset: 0, len: 5]
LOCAL SELECT [col("Winner").reverse(), col("Zvítězil").reverse()] FROM
SORT BY [col("Zvítězil")]
Aggregate
[col("Year").count().alias("Zvítězil")] BY [col("Winner")] FROM
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
A výsledný rámec bude vypadat takto:
shape: (5, 2) ┌─────────────┬──────────┐ │ Winner ┆ Zvítězil │ │ --- ┆ --- │ │ str ┆ u32 │ ╞═════════════╪══════════╡ │ Python ┆ 5 │ │ C ┆ 3 │ │ Objective-C ┆ 2 │ │ Java ┆ 2 │ │ Go ┆ 2 │ └─────────────┴──────────┘ ['Winner', 'Zvítězil'] [Utf8, UInt32]
Operace head a tail je možné zřetězit, ale zajímavé je, že nedojde k optimalizaci těchto operací do jediné operace SLICE (alespoň ne v současné variantě knihovny Polars):
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme plán pro líně načtený datový rámec
print(df.describe_plan())
print()
# seskupení podle názvu jazyka
df2 = (
df.groupby("Winner", maintain_order=True)
.agg([polars.col("Year").len().alias("Zvítězil")])
.sort("Zvítězil")
.reverse()
.head(10)
.tail(5)
)
# převod prvků na běžný datový rámec
df3 = df2.collect()
# zobrazíme plán pro druhý líny datový rámec
print(df2.describe_plan())
print(df2.describe_optimized_plan())
print()
# zobrazíme běžný (výsledný) datový rámec
print(df3)
print()
print(df3.columns)
print(df3.dtypes)
S výsledky:
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
SLICE[offset: -5, len: 5]
SLICE[offset: 0, len: 10]
LOCAL SELECT [col("Winner").reverse(), col("Zvítězil").reverse()] FROM
SORT BY [col("Zvítězil")]
Aggregate
[col("Year").count().alias("Zvítězil")] BY [col("Winner")] FROM
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
SLICE[offset: -5, len: 5]
SLICE[offset: 0, len: 10]
LOCAL SELECT [col("Winner").reverse(), col("Zvítězil").reverse()] FROM
SORT BY [col("Zvítězil")]
Aggregate
[col("Year").count().alias("Zvítězil")] BY [col("Winner")] FROM
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
shape: (5, 2)
┌──────────────┬──────────┐
│ Winner ┆ Zvítězil │
│ --- ┆ --- │
│ str ┆ u32 │
╞══════════════╪══════════╡
│ C++ ┆ 2 │
│ PHP ┆ 1 │
│ Ruby ┆ 1 │
│ Transact-SQL ┆ 1 │
│ JavaScript ┆ 1 │
└──────────────┴──────────┘
['Winner', 'Zvítězil']
[Utf8, UInt32]
14. Rozvětvení a opětovné spojení plánů
V předchozím textu jsme si řekli, že může dojít k rozvětvení plánů (z líného datového rámce je odvozeno více nových datových rámců aplikací nějaké operace) nebo dokonce i ke spojení plánů. A právě tyto situace si otestujeme v dnešním posledním demonstračním příkladu, v němž z jediného zdrojového datového rámce df1 aplikací různých operací odvodíme plány df2, df3, df4 a df5. A nakonec tyto odvozené líné datové rámce opět spojíme operací concat (tu jsme si sice ještě nepopisovali, ale v našem případě dojde ke spojení rámců „pod sebou“, protože všechny datové rámce mají stejné typy i názvy sloupců):
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df1 = polars.scan_csv("hall_of_fame.csv")
# seřazení podle zvoleného sloupce
df2 = df1.sort("Year")
# seřazení podle zvoleného sloupce
df3 = df1.sort("Year").reverse()
# seskupení podle názvu jazyka
df4 = (
df2.groupby("Winner", maintain_order=True)
.agg([polars.col("Year").len().alias("Zvítězil")])
.sort("Zvítězil")
)
# otočení prvků + získání pěti výsledků
df5 = df4.reverse().head(5)
# spojení několika datových rámců - spojení plánů
df6 = polars.concat([df2, df3, df4, df5], how="vertical")
# zobrazíme plány pro všechny líné datové rámce
print("df1")
print(df1.describe_plan())
print()
print("df2")
print(df2.describe_plan())
print()
print("df3")
print(df3.describe_plan())
print()
print("df4")
print(df4.describe_plan())
print()
print("df5")
print(df5.describe_plan())
print()
print("df6")
print(df6.describe_plan())
print()
Po spuštění tohoto skriptu se všechny plány postupně vypíšou:
df1
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
df2
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
df3
LOCAL SELECT [col("Year").reverse(), col("Winner").reverse()] FROM
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
df4
SORT BY [col("Zvítězil")]
Aggregate
[col("Year").count().alias("Zvítězil")] BY [col("Winner")] FROM
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
df5
SLICE[offset: 0, len: 5]
LOCAL SELECT [col("Winner").reverse(), col("Zvítězil").reverse()] FROM
SORT BY [col("Zvítězil")]
Aggregate
[col("Year").count().alias("Zvítězil")] BY [col("Winner")] FROM
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
Za povšimnutí stojí především poslední plán pro líný datový rámec df6, protože tento plán vznikl sloučením (union) všech předchozích plánů, což je z výsledku patrné:
df6
RECHUNK
UNION:
PLAN 0:
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
PLAN 1:
LOCAL SELECT [col("Year").reverse(), col("Winner").reverse()] FROM
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
PLAN 2:
SORT BY [col("Zvítězil")]
Aggregate
[col("Year").count().alias("Zvítězil")] BY [col("Winner")] FROM
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
PLAN 3:
SLICE[offset: 0, len: 5]
LOCAL SELECT [col("Winner").reverse(), col("Zvítězil").reverse()] FROM
SORT BY [col("Zvítězil")]
Aggregate
[col("Year").count().alias("Zvítězil")] BY [col("Winner")] FROM
SORT BY [col("Year")]
CSV SCAN hall_of_fame.csv
PROJECT */2 COLUMNS
END UNION
15. Vizualizace plánu
Namísto metody describe_plan je možné plány operací nad líným datovým rámcem zobrazit (resp. přesněji řečeno vizualizovat) metodou show_graph. Pro tuto operaci je nutné mít nainstalovánu knihovnu Matplotlib. Skript z předchozí kapitoly nepatrně upravíme takovým způsobem, že namísto tisku plánů na terminál je zobrazíme v grafickém okně:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df1 = polars.scan_csv("hall_of_fame.csv")
# seřazení podle zvoleného sloupce
df2 = df1.sort("Year")
# seřazení podle zvoleného sloupce
df3 = df1.sort("Year").reverse()
# seskupení podle názvu jazyka
df4 = (
df2.groupby("Winner", maintain_order=True)
.agg([polars.col("Year").len().alias("Zvítězil")])
.sort("Zvítězil")
)
# otočení prvků + získání pěti výsledků
df5 = df4.reverse().head(5)
# spojení několika datových rámců - spojení plánů
df6 = polars.concat([df2, df3, df4, df5], how="vertical")
# zobrazíme plány pro všechny líné datové rámce
# v grafické podobě
df1.show_graph()
df2.show_graph()
df3.show_graph()
df4.show_graph()
df5.show_graph()
df6.show_graph()
Vizualizované výsledky vypadají následovně:
Obrázek 1: Vizualizovaný plán pro datový rámec #1.
Obrázek 2: Vizualizovaný plán pro datový rámec #2.
Obrázek 3: Vizualizovaný plán pro datový rámec #3.
Obrázek 4: Vizualizovaný plán pro datový rámec #4.
Obrázek 5: Vizualizovaný plán pro datový rámec #5.
Obrázek 6: Vizualizovaný plán pro datový rámec #5.
16. Vizualizace plánu s operacemi SLICE
Na závěr si ukažme, jak vypadá vizualizovaný plán, v němž jsou použity operace SLICE:
#!/usr/bin/env python3
# vim: set fileencoding=utf-8
import polars
# líné přečtení zdrojových dat
df = polars.scan_csv("hall_of_fame.csv")
# zobrazíme plán pro líně načtený datový rámec
print(df.describe_plan())
print()
# seskupení podle názvu jazyka
df2 = (
df.groupby("Winner", maintain_order=True)
.agg([polars.col("Year").len().alias("Zvítězil")])
.sort("Zvítězil")
.reverse()
.head(10)
.tail(5)
)
# zobrazíme plán pro druhý líny datový rámec
# v grafické podobě
df2.show_graph()
V tomto případě by se měl zobrazit tento diagram:
Obrázek 7: Vizualizovaný plán pro výsledný líný datový rámec.
17. Obsah závěrečné části seriálu o knihovně Polars
Ve čtvrté a současně i poslední části miniseriálu o knihovně Pandas se budeme zabývat velmi častou operací – spojením dvou (nebo i v případě potřeby většího množství) datových rámců. V knihovně Polars je možné rámce spojit jak „po řádcích“, tak i „po sloupcích“ a popř. i vyřešit splynutí hodnot z těch sloupců, které si logicky odpovídají. To však není vše, protože lze provést i operace typu join (což je jméno převzaté ze SQL). K dispozici je vnitřní join, levý join, pravý join i vnější join.
18. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro programovací jazyk Python 3 (nikoli ovšem pro starší verze Pythonu 2!) byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem stále velmi malý, dnes má velikost zhruba několik desítek kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
19. Odkazy na Internetu
- Projekt Polars na GitHubu
https://github.com/pola-rs/polars - Dokumentace k projektu Polars (popis API)
https://pola-rs.github.io/polars/py-polars/html/reference/index.html - Polars: The Next Big Python Data Science Library… written in RUST?
https://www.youtube.com/watch?v=VHqn7ufiilE - Polars API: funkce pro načtení datového rámce z CSV
https://pola-rs.github.io/polars/py-polars/html/reference/api/polars.read_csv.html - Polars API: funkce pro načtení datového rámce z relační databáze
https://pola-rs.github.io/polars/py-polars/html/reference/api/polars.read_sql.html - Python’s Pandas vs Polars: Who Wins this Fight in Library
https://analyticsindiamag.com/pythons-pandas-vs-polars-who-wins-this-fight-in-library/ - Polars vs Pandas: what is more convenient?
https://medium.com/@ilia.ozhmegov/polars-vs-pandas-what-is-more-convenient-331956742a69 - A Gentle Introduction to Pandas Data Analysis (on Kaggle)
https://www.youtube.com/watch?v=_Eb0utIRdkw&list=PL7RwtdVQXQ8oYpuIIDWR0SaaSCe8ZeZ7t&index=4 - Speed Up Your Pandas Dataframes
https://www.youtube.com/watch?v=u4_c2LDi4b8&list=PL7RwtdVQXQ8oYpuIIDWR0SaaSCe8ZeZ7t&index=5 - pandas.read_csv
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html - How to define format when using pandas to_datetime?
https://stackoverflow.com/questions/36848514/how-to-define-format-when-using-pandas-to-datetime - Pandas : skip rows while reading csv file to a Dataframe using read_csv() in Python
https://thispointer.com/pandas-skip-rows-while-reading-csv-file-to-a-dataframe-using-read_csv-in-python/ - Skip rows during csv import pandas
https://stackoverflow.com/questions/20637439/skip-rows-during-csv-import-pandas - Denni kurz
https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt - UUID objects according to RFC 4122 (knihovna pro Python)
https://docs.python.org/3.5/library/uuid.html#uuid.uuid4 - Object identifier (Wikipedia)
https://en.wikipedia.org/wiki/Object_identifier - Digital object identifier (Wikipedia)
https://en.wikipedia.org/wiki/Digital_object_identifier - voluptuous na (na PyPi)
https://pypi.python.org/pypi/voluptuous - Repositář knihovny voluptuous na GitHubu
https://github.com/alecthomas/voluptuous - pytest-voluptuous 1.0.2 (na PyPi)
https://pypi.org/project/pytest-voluptuous/ - pytest-voluptuous (na GitHubu)
https://github.com/F-Secure/pytest-voluptuous - schemagic 0.9.1 (na PyPi)
https://pypi.python.org/pypi/schemagic/0.9.1 - Schemagic / Schemagic.web (na GitHubu)
https://github.com/Mechrophile/schemagic - schema 0.6.7 (na PyPi)
https://pypi.python.org/pypi/schema - schema (na GitHubu)
https://github.com/keleshev/schema - KX v DBOps Benchmark Results by Ferenc Bodon
https://community.kx.com/t5/Community-Blogs/KX-v-DBOps-Benchmark-Results-by-Ferenc-Bodon/ba-p/12182 - TIOBE Index for January 2023
https://www.tiobe.com/tiobe-index/ - Lazy evaluation
https://en.wikipedia.org/wiki/Lazy_evaluation