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