Obsah
1. Knihovna Polars: výkonnější alternativa ke knihovně Pandas (datové rámce)
2. Zobrazení úvodních záznamů uložených v datovém rámci metodou head
3. Specifikace maximálního počtu zobrazených řádků s obsahem datového rámce
4. Volba naformátování datového rámce
5. Zobrazení naformátovaného datového rámce bez specifikace jeho tvaru
6. Seřazení záznamů v datovém rámci metodou sort
7. Odstranění duplicitních záznamů na základě hodnot ve specifikovaném sloupci
8. Zpracování jednotlivých sloupců v datových rámcích – map
9. Použití alternativní metody apply
10. Rozdělení a následná agregace údajů s využitím metody groupby
11. Praktická ukázka použití groupby
12. Seřazení hodnot v průběhu jejich agregace
13. Seřazení záznamů v datovém rámci před provedením operace groupby s následnou agregací
14. Výpočet celkového pořadí vítězných programovacích jazyků
15. Seřazení jazyků na základě jejich celkového pořadí
16. Dva způsoby korektního seřazení výsledků
17. Příklad na závěr – složitější „pipeline“, změna názvu sloupce
18. Obsah třetí části seriálu o knihovně Polars
19. Repositář s demonstračními příklady
1. Knihovna Polars: výkonnější alternativa ke knihovně Pandas (datové rámce)
Na úvodní článek o knihovně Polars, jejímž cílem je být výkonnější alternativou ke známé knihovně Pandas, dnes navážeme. Ukážeme si některé další operace určené pro zpracování dat uložených v datových rámcích (data frame). V první řadě se jedná o seřazení dat, zobrazení určité části datového rámce, naformátování výstupu s datovým rámcem, získání prvků s unikátními hodnotami atd. Ovšem nesmíme zapomenout ani na velmi důležitou sekvenci operací: rozdělení vstupních záznamů do skupin na základě zadaného kritéria následovanému agregací dat (což může být například zjištění, kolik záznamů se v každé skupině nachází atd.). Všechny zmíněné operace je přitom možné různým způsobem kombinovat a zapsat tak do jediného výrazu i poměrně složité transformace dat.
2. Zobrazení úvodních záznamů uložených v datovém rámci metodou head
Ve skriptech uvedených minule jsme se většinou snažili zobrazit celý obsah datového rámce. Existují ovšem metody head a tail, které dokážou zobrazit pouze několik úvodních záznamů v datovém rámci, popř. naopak poslední záznamy v rámci. Nejdříve se podívejme na metodu head která, pokud se jí nepředají další parametry, zobrazí prvních pět úvodních záznamů:
print(df.head())
S výsledkem:
shape: (5, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ 2018 ┆ Python │ └──────┴────────┘
Podobně lze použít metodu tail:
print(df.head())
S odlišným výsledkem:
shape: (5, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2007 ┆ Python │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴────────┘
Celý skript může vypadat následovně:
#!/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") # zobrazíme datový rámec, resp. jeho hlavičku print(df.head())
Pokud metodám head či tail předáme celočíselnou hodnotu, bude se jednat o počet záznamů, které se mají zobrazit. Ovšem je důležité dát si pozor na to, že existuje ještě dále popsané omezení maximálního počtu zobrazených řádků na terminálu.
Použití metody head s parametrem:
print(df.head(30))
Výsledek:
shape: (20, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ ... ┆ ... │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴────────┘
Použití metody tail s parametrem:
print(df.tail(30))
Výsledek:
shape: (20, 2) ┌──────┬────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ ... ┆ ... │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴────────┘
Celý skript vypadá následovně:
#!/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") # zobrazíme datový rámec, resp. jeho hlavičku print(df.head(30))
3. Specifikace maximálního počtu zobrazených řádků s obsahem datového rámce
Funkcí polars.Config.set_tbl_rows() je možné zvětšit či naopak zmenšit maximální počet řádků s obsahem datového rámce, které budou vypsány na terminál (resp. obecně na standardní výstup). V případě, že bude datový rámec obsahovat méně řádků, bude zobrazen celý, pokud bude obsahovat více řádků, bude datový rámec v polovině zkrácen:
#!/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) # zobrazíme datový rámec print(df.head(30))
Výsledek bude nyní vypadat následovně – zobrazí se všech dvacet řádků:
shape: (20, 2) ┌──────┬──────────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪══════════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ 2018 ┆ Python │ │ 2017 ┆ C │ │ 2016 ┆ Go │ │ 2015 ┆ Java │ │ 2014 ┆ JavaScript │ │ 2013 ┆ Transact-SQL │ │ 2012 ┆ Objective-C │ │ 2011 ┆ Objective-C │ │ 2010 ┆ Python │ │ 2009 ┆ Go │ │ 2008 ┆ C │ │ 2007 ┆ Python │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴──────────────┘
4. Volba naformátování datového rámce
Datové rámce je možné zobrazit mnoha různými způsoby. Volba formátování se provádí funkcí polars.Config.set_tbl_formatting, které se předává řetězcová konstanta (literál) s popisem formátu. Všechny v současnosti podporované formáty zobrazení naleznete v následujícím skriptu, konkrétně v n-tici formattings:
#!/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) formattings = ( "ASCII_FULL", "ASCII_FULL_CONDENSED", "ASCII_NO_BORDERS", "ASCII_BORDERS_ONLY", "ASCII_BORDERS_ONLY_CONDENSED", "ASCII_HORIZONTAL_ONLY", "ASCII_MARKDOWN", "UTF8_FULL", "UTF8_FULL_CONDENSED", "UTF8_NO_BORDERS", "UTF8_BORDERS_ONLY", "UTF8_HORIZONTAL_ONLY", "NOTHING", ) for formatting in formattings: print() print(formatting) print() polars.Config.set_tbl_formatting(formatting) # zobrazíme datový rámec print(df)
Výsledné datové rámce zobrazené na terminálu jsou naformátovány takto: https://raw.githubusercontent.com/tisnik/most-popular-python-libs/master/polars/formatted_data_frames_with_shapes.txt.
5. Zobrazení naformátovaného datového rámce bez specifikace jeho tvaru
Alternativně je možné příkazem:
polars.Config.set_tbl_hide_dataframe_shape(True)
zakázat zobrazení tvaru (shape) datového rámce. Výsledné formy naformátování datových rámců naleznete na stránce https://raw.githubusercontent.com/tisnik/most-popular-python-libs/master/polars/formatted_data_frames_without_shapes.txt a jsou výsledkem činnosti tohoto skriptu:
#!/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) # nezobrazovat tvar (shape) datových rámců polars.Config.set_tbl_hide_dataframe_shape(True) formattings = ( "ASCII_FULL", "ASCII_FULL_CONDENSED", "ASCII_NO_BORDERS", "ASCII_BORDERS_ONLY", "ASCII_BORDERS_ONLY_CONDENSED", "ASCII_HORIZONTAL_ONLY", "ASCII_MARKDOWN", "UTF8_FULL", "UTF8_FULL_CONDENSED", "UTF8_NO_BORDERS", "UTF8_BORDERS_ONLY", "UTF8_HORIZONTAL_ONLY", "NOTHING", ) for formatting in formattings: print() print(formatting) print() polars.Config.set_tbl_formatting(formatting) # zobrazíme datový rámec print(df)
6. Seřazení záznamů v datovém rámci metodou sort
Záznamy v datovém rámci je možné seřadit podle hodnot uložených ve vybraném sloupci. K tomuto účelu slouží metoda nazvaná sort, které se předá jméno sloupce. Důležité je, že původní datový rámec zůstane nezměněn, ale vytvoří a vrátí se rámec nový (podobně jako u standardní funkce sort). Takto například zajistíme seřazení hodnot v datovém rámci podle jména programovacího jazyka:
# seřadit a vytvořit nový datový rámec df = df.sort("Winner")
S výsledkem:
shape: (20, 2) ┌──────┬──────────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪══════════════╡ │ 2019 ┆ C │ │ 2017 ┆ C │ │ 2008 ┆ C │ │ 2022 ┆ C++ │ │ 2003 ┆ C++ │ │ 2016 ┆ Go │ │ 2009 ┆ Go │ │ 2015 ┆ Java │ │ 2005 ┆ Java │ │ 2014 ┆ JavaScript │ │ 2012 ┆ Objective-C │ │ 2011 ┆ Objective-C │ │ 2004 ┆ PHP │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2018 ┆ Python │ │ 2010 ┆ Python │ │ 2007 ┆ Python │ │ 2006 ┆ Ruby │ │ 2013 ┆ Transact-SQL │ └──────┴──────────────┘
Úplný skript, který načte, seřadí a zobrazí nový datový rámec, vypadá takto:
#!/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) # seřadit a vytvořit nový datový rámec df = df.sort("Winner") # zobrazíme datový rámec print(df)
Pokud budeme potřebovat seřadit záznamy v opačném pořadí, nabízí se použití další metody nazvané reverse:
# seřadit a vytvořit nový datový rámec df = df.sort("Winner").reverse()
S výsledky:
shape: (20, 2) ┌──────┬──────────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪══════════════╡ │ 2013 ┆ Transact-SQL │ │ 2006 ┆ Ruby │ │ 2007 ┆ Python │ │ 2010 ┆ Python │ │ 2018 ┆ Python │ │ 2020 ┆ Python │ │ 2021 ┆ Python │ │ 2004 ┆ PHP │ │ 2011 ┆ Objective-C │ │ 2012 ┆ Objective-C │ │ 2014 ┆ JavaScript │ │ 2005 ┆ Java │ │ 2015 ┆ Java │ │ 2009 ┆ Go │ │ 2016 ┆ Go │ │ 2003 ┆ C++ │ │ 2022 ┆ C++ │ │ 2008 ┆ C │ │ 2017 ┆ C │ │ 2019 ┆ C │ └──────┴──────────────┘
Opět si uveďme celý skript, který tuto operaci provede:
#!/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) # seřadit a vytvořit nový datový rámec df = df.sort("Winner").reverse() # zobrazíme datový rámec print(df)
Rychlejší i paměťově méně náročné je ovšem předání nepovinného parametru reverse metodě Sort. Ušetří se tím vytvoření jednoho datového rámce navíc a současně i provedení operace reverse:
#!/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) # seřadit a vytvořit nový datový rámec df = df.sort("Winner", reverse=True) # zobrazíme datový rámec print(df)
Výsledek bude totožný s výsledkem získaným z předchozího skriptu.
7. Odstranění duplicitních záznamů na základě hodnot ve specifikovaném sloupci
Metodou unique, která opět vytvoří nový datový rámec, je možné vybrat jen tolik záznamů z původního datového rámce, aby hodnoty ve vybraném sloupci byly unikátní. Co to například znamená v našem konkrétním případě? Názvy programovacích jazyků v původním datovém sloupci nejsou unikátní, protože se některé jazyky opakují:
shape: (20, 2) ┌──────┬──────────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪══════════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2020 ┆ Python │ │ 2019 ┆ C │ │ 2018 ┆ Python │ │ 2017 ┆ C │ │ 2016 ┆ Go │ │ 2015 ┆ Java │ │ 2014 ┆ JavaScript │ │ 2013 ┆ Transact-SQL │ │ 2012 ┆ Objective-C │ │ 2011 ┆ Objective-C │ │ 2010 ┆ Python │ │ 2009 ┆ Go │ │ 2008 ┆ C │ │ 2007 ┆ Python │ │ 2006 ┆ Ruby │ │ 2005 ┆ Java │ │ 2004 ┆ PHP │ │ 2003 ┆ C++ │ └──────┴──────────────┘
Pokud ovšem vytvoříme nový datový rámec následující operací:
# vyfiltrovat a vytvořit nový datový rámec df = df.unique(subset="Winner")
Bude ve výsledku každý jazyk reprezentován jediným záznamem:
shape: (10, 2) ┌──────┬──────────────┐ │ Year ┆ Winner │ │ --- ┆ --- │ │ i64 ┆ str │ ╞══════╪══════════════╡ │ 2022 ┆ C++ │ │ 2021 ┆ Python │ │ 2019 ┆ C │ │ 2016 ┆ Go │ │ 2015 ┆ Java │ │ 2014 ┆ JavaScript │ │ 2013 ┆ Transact-SQL │ │ 2012 ┆ Objective-C │ │ 2006 ┆ Ruby │ │ 2004 ┆ PHP │ └──────┴──────────────┘
Opět si pro úplnost ukažme celý skript, který tuto operaci provádí:
#!/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) # seřadit a vytvořit nový datový rámec df = df.unique(subset="Winner") # zobrazíme datový rámec print(df)
8. Zpracování jednotlivých sloupců v datových rámcích – map
Vyzkoušejme si nyní vytvoření nového sloupce pojmenovaného „Ratings as ratio“ a založeného na hodnotách z existujícího sloupce „Ratings“. Použijeme přitom metodu map, která postupně aplikuje zadanou funkci (v našem případě anonymní funkci zapsanou pomocí lambda) na hodnoty uložené ve vybraném sloupci. Zpracování hodnot je jednoduché – hodnoty v procentech se převedou na hodnoty ležící v rozsahu od 0,1 do 1,0, tedy na poměr popularity jednotlivých programovacích jazyků:
# převod na skutečný poměr <0, 1> df2 = df.with_column( polars.col("Ratings").map(lambda x: x / 100.0).alias("Ratings as ratio") )
Výsledkem by měl být nový datový rámec (df2) s novým sloupcem:
shape: (20, 7) ┌──────────┬──────────┬────────┬───────────────────┬─────────┬─────────┬──────────────────┐ │ Sep 2020 ┆ Sep 2019 ┆ Change ┆ Language ┆ Ratings ┆ Changep ┆ Ratings as ratio │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ i64 ┆ str ┆ str ┆ f64 ┆ f64 ┆ f64 │ ╞══════════╪══════════╪════════╪═══════════════════╪═════════╪═════════╪══════════════════╡ │ 1 ┆ 2 ┆ change ┆ C ┆ 15.95 ┆ 0.74 ┆ 0.1595 │ │ 2 ┆ 1 ┆ change ┆ Java ┆ 13.48 ┆ -3.18 ┆ 0.1348 │ │ 3 ┆ 3 ┆ null ┆ Python ┆ 10.47 ┆ 0.59 ┆ 0.1047 │ │ 4 ┆ 4 ┆ null ┆ C++ ┆ 7.11 ┆ 1.48 ┆ 0.0711 │ │ 5 ┆ 5 ┆ null ┆ C# ┆ 4.58 ┆ 1.18 ┆ 0.0458 │ │ 6 ┆ 6 ┆ null ┆ Visual Basic ┆ 4.12 ┆ 0.83 ┆ 0.0412 │ │ 7 ┆ 7 ┆ null ┆ JavaScript ┆ 2.54 ┆ 0.41 ┆ 0.0254 │ │ 8 ┆ 9 ┆ change ┆ PHP ┆ 2.49 ┆ 0.62 ┆ 0.0249 │ │ 9 ┆ 19 ┆ change ┆ R ┆ 2.37 ┆ 1.33 ┆ 0.0237 │ │ 10 ┆ 8 ┆ change ┆ SQL ┆ 1.76 ┆ -0.19 ┆ 0.0176 │ │ 11 ┆ 14 ┆ change ┆ Go ┆ 1.46 ┆ 0.24 ┆ 0.0146 │ │ 12 ┆ 16 ┆ change ┆ Swift ┆ 1.38 ┆ 0.28 ┆ 0.0138 │ │ 13 ┆ 20 ┆ change ┆ Perl ┆ 1.3 ┆ 0.26 ┆ 0.013 │ │ 14 ┆ 12 ┆ change ┆ Assembly language ┆ 1.3 ┆ -0.08 ┆ 0.013 │ │ 15 ┆ 15 ┆ null ┆ Ruby ┆ 1.24 ┆ 0.03 ┆ 0.0124 │ │ 16 ┆ 18 ┆ change ┆ MATLAB ┆ 1.1 ┆ 0.04 ┆ 0.011 │ │ 17 ┆ 11 ┆ change ┆ Groovy ┆ 0.99 ┆ -0.52 ┆ 0.0099 │ │ 18 ┆ 33 ┆ change ┆ Rust ┆ 0.92 ┆ 0.55 ┆ 0.0092 │ │ 19 ┆ 10 ┆ change ┆ Objective-C ┆ 0.85 ┆ -0.99 ┆ 0.0085 │ │ 20 ┆ 24 ┆ change ┆ Dart ┆ 0.77 ┆ 0.13 ┆ 0.0077 │ └──────────┴──────────┴────────┴───────────────────┴─────────┴─────────┴──────────────────┘
Opět si pro úplnost ukažme celý skript:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import polars # maximální počet zobrazených řádků polars.Config.set_tbl_rows(100) # přečtení zdrojových dat df = polars.read_csv("tiobe.tsv", sep="\t") # převod na skutečný poměr <0, 1> df2 = df.with_column( polars.col("Ratings").map(lambda x: x / 100.0).alias("Ratings as ratio") ) # datový rámec zobrazíme print(df) print() # datový rámec zobrazíme print(df2) print()
9. Použití alternativní metody apply
Namísto metody map, která interně operuje vždy s celým sloupcem (tedy s datovou řadou) je možné použít metodu apply. Ta v tomto případě provádí (z pohledu uživatele) shodnou operaci (výsledek bude totožný), ovšem interně je výpočet proveden zcela odlišným způsobem:
# převod na skutečný poměr <0, 1> df2 = df.with_column( polars.col("Ratings").apply(lambda x: x / 100.0).alias("Ratings as ratio") )
Výsledkem bude v tomto konkrétním případě naprosto stejný datový rámec, jaký byl získán skriptem z předchozí kapitoly. Mohlo by se tedy zdát, že map a apply mají stejný význam, ale příště si ukážeme, že tomu tak vždy není:
shape: (20, 7) ┌──────────┬──────────┬────────┬───────────────────┬─────────┬─────────┬──────────────────┐ │ Sep 2020 ┆ Sep 2019 ┆ Change ┆ Language ┆ Ratings ┆ Changep ┆ Ratings as ratio │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ i64 ┆ str ┆ str ┆ f64 ┆ f64 ┆ f64 │ ╞══════════╪══════════╪════════╪═══════════════════╪═════════╪═════════╪══════════════════╡ │ 1 ┆ 2 ┆ change ┆ C ┆ 15.95 ┆ 0.74 ┆ 0.1595 │ │ 2 ┆ 1 ┆ change ┆ Java ┆ 13.48 ┆ -3.18 ┆ 0.1348 │ │ 3 ┆ 3 ┆ null ┆ Python ┆ 10.47 ┆ 0.59 ┆ 0.1047 │ │ 4 ┆ 4 ┆ null ┆ C++ ┆ 7.11 ┆ 1.48 ┆ 0.0711 │ │ 5 ┆ 5 ┆ null ┆ C# ┆ 4.58 ┆ 1.18 ┆ 0.0458 │ │ 6 ┆ 6 ┆ null ┆ Visual Basic ┆ 4.12 ┆ 0.83 ┆ 0.0412 │ │ 7 ┆ 7 ┆ null ┆ JavaScript ┆ 2.54 ┆ 0.41 ┆ 0.0254 │ │ 8 ┆ 9 ┆ change ┆ PHP ┆ 2.49 ┆ 0.62 ┆ 0.0249 │ │ 9 ┆ 19 ┆ change ┆ R ┆ 2.37 ┆ 1.33 ┆ 0.0237 │ │ 10 ┆ 8 ┆ change ┆ SQL ┆ 1.76 ┆ -0.19 ┆ 0.0176 │ │ 11 ┆ 14 ┆ change ┆ Go ┆ 1.46 ┆ 0.24 ┆ 0.0146 │ │ 12 ┆ 16 ┆ change ┆ Swift ┆ 1.38 ┆ 0.28 ┆ 0.0138 │ │ 13 ┆ 20 ┆ change ┆ Perl ┆ 1.3 ┆ 0.26 ┆ 0.013 │ │ 14 ┆ 12 ┆ change ┆ Assembly language ┆ 1.3 ┆ -0.08 ┆ 0.013 │ │ 15 ┆ 15 ┆ null ┆ Ruby ┆ 1.24 ┆ 0.03 ┆ 0.0124 │ │ 16 ┆ 18 ┆ change ┆ MATLAB ┆ 1.1 ┆ 0.04 ┆ 0.011 │ │ 17 ┆ 11 ┆ change ┆ Groovy ┆ 0.99 ┆ -0.52 ┆ 0.0099 │ │ 18 ┆ 33 ┆ change ┆ Rust ┆ 0.92 ┆ 0.55 ┆ 0.0092 │ │ 19 ┆ 10 ┆ change ┆ Objective-C ┆ 0.85 ┆ -0.99 ┆ 0.0085 │ │ 20 ┆ 24 ┆ change ┆ Dart ┆ 0.77 ┆ 0.13 ┆ 0.0077 │ └──────────┴──────────┴────────┴───────────────────┴─────────┴─────────┴──────────────────┘
Opět se podívejme na úplný zdrojový kód skriptu, který vygeneroval předchozí datový rámec:
#!/usr/bin/env python3 # vim: set fileencoding=utf-8 import polars # maximální počet zobrazených řádků polars.Config.set_tbl_rows(100) # přečtení zdrojových dat df = polars.read_csv("tiobe.tsv", sep="\t") # převod na skutečný poměr <0, 1> df2 = df.with_column( polars.col("Ratings").apply(lambda x: x / 100.0).alias("Ratings as ratio") ) # datový rámec zobrazíme print(df) print() # datový rámec zobrazíme print(df2) print()
10. Rozdělení a následná agregace údajů s využitím metody groupby
Ve druhé polovině dnešního článku si popíšeme způsob zpracování a agregace údajů v datových rámcích s využitím metody pojmenované groupby, což je velmi často využívaná operace (u níž by se navíc měly projevit výkonnostní rozdíly mezi Pandas a Polars). V případě groupby se jedná o metodu, která umožňuje údaje z datových rámců nejprve rozdělit do skupin (tedy vlastně seskupit) s tím, že se údaje z každé skupiny posléze nějakým způsobem agregují; například se zjistí jejich počet, součet hodnot ve vybraném sloupci atd.
Zpracovávat přitom budeme následující datový soubor, který obsahuje jména těch programovacích jazyků, které byly serverem Tiobe každý rok vybrány na základě aktivity jejich uživatelů (či naopak začátečníků) na webu. Tento datový soubor můžete nalézt na adrese https://github.com/tisnik/most-popular-python-libs/blob/master/polars/hall_of_fame.csv. Jedná se o klasický CSV soubor s hlavičkami sloupců:
Year,Winner 2022,C++ 2021,Python 2020,Python 2019,C 2018,Python 2017,C 2016,Go 2015,Java 2014,JavaScript 2013,Transact-SQL 2012,Objective-C 2011,Objective-C 2010,Python 2009,Go 2008,C 2007,Python 2006,Ruby 2005,Java 2004,PHP 2003,C++
Povšimněte si, že některé jazyky vyhrály vícekrát, což nám umožňuje analýzu těchto dat právě s využitím operace groupby.
11. Praktická ukázka použití groupby
Metoda groupby nám umožňuje s datovým rámcem provést následující operace:
- Rozdělení do skupin údajů v datovém rámci na základě zadané podmínky nebo jen podle uložených hodnot ve sloupci (rozdělení na základě hodnot je přímočaré a použijeme ho v demonstračních příkladech).
- Aplikace vybrané funkce na výsledný datový blok, resp. na jeh sloupce.
- Kombinace či agregace výsledků (součet všech sdružených záznamů, jejich počet atd.)
Příkladem může být vytvoření nového datového rámce, který bude pro každý programovací jazyk obsahovat počet let, v nichž tento jazyk zvítězil (resp. kdy byl vybrán). Jazyky tedy nejprve rozdělíme podle jejich jména (což jsou údaje uložené ve sloupci „Winner“). Nepovinným parametrem maintain_order se určuje, zda se má zachovat pořadí skupin při provádění operací; což je ovšem časově náročná operace:
df.groupby("Winner", maintain_order=True)
Výsledkem bude nový datový blok (ale ne datový rámec!), nad nímž můžeme provádět další operace, zejména již výše zmíněnou agregaci. Můžeme například zjistit, pro jaký rok se každý jazyk ve své skupině vyskytuje. K tomuto účelu se použije agregační metoda zavolaná na objekt, jenž je výsledkem volání groupby (výsledkem totiž není plnohodnotný datový rámec, ale instance třídy polars.internals.dataframe.groupby.GroupBy):
df.groupby("Winner", maintain_order=True).agg([polars.col("Year")])
Výsledek agregace je reprezentován ve formě nového datového rámce, který lze zobrazit či dále zpracovat libovolným způsobem:
df = df.groupby("Winner", maintain_order=True).agg([polars.col("Year")]) print(df)
S tímto výsledkem:
shape: (10, 2) ┌──────────────┬────────────────────────┐ │ Winner ┆ Year │ │ --- ┆ --- │ │ str ┆ list[i64] │ ╞══════════════╪════════════════════════╡ │ C++ ┆ [2022, 2003] │ │ Python ┆ [2021, 2020, ... 2007] │ │ C ┆ [2019, 2017, 2008] │ │ Go ┆ [2016, 2009] │ │ Java ┆ [2015, 2005] │ │ JavaScript ┆ [2014] │ │ Transact-SQL ┆ [2013] │ │ Objective-C ┆ [2012, 2011] │ │ Ruby ┆ [2006] │ │ PHP ┆ [2004] │ └──────────────┴────────────────────────┘
Uveďme si úplný zdrojový kód skriptu, který rozdělení a agregaci dat provádí:
#!/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")]) # zobrazíme datový rámec print(df)
12. Seřazení hodnot v průběhu jejich agregace
Ve chvíli, kdy máme k dispozici objekt vzniklý po rozdělení hodnot ze vstupního datového rámce do skupin, můžeme v metodě agg specifikovat nejenom nový sloupec (který bude v našem případě obsahovat seznam let, kdy daný programovací jazyk vyhrál), ale navíc můžeme hodnoty uložené v tomto sloupci seřadit:
df = df.groupby("Winner", maintain_order=True).agg([polars.col("Year").sort()])
Povšimněte si, že metodu sort voláme pro definici sloupce „Year“ obsahujícího agregovaná data, konkrétně seznam s roky. To tedy znamená, že dojde k seřazení hodnot uložených v těchto seznamech, nikoli k seřazení jednotlivých řádků (záznamů) ve výsledném datovém rámci. Konkrétně bude výsledek vypadat takto:
shape: (10, 2) ┌──────────────┬────────────────────────┐ │ Winner ┆ Year │ │ --- ┆ --- │ │ str ┆ list[i64] │ ╞══════════════╪════════════════════════╡ │ C++ ┆ [2003, 2022] │ │ Python ┆ [2007, 2010, ... 2021] │ │ C ┆ [2008, 2017, 2019] │ │ Go ┆ [2009, 2016] │ │ Java ┆ [2005, 2015] │ │ JavaScript ┆ [2014] │ │ Transact-SQL ┆ [2013] │ │ Objective-C ┆ [2011, 2012] │ │ Ruby ┆ [2006] │ │ PHP ┆ [2004] │ └──────────────┴────────────────────────┘
Rozdíl mezi předchozím příkladem a tímto příkladem uvidíme na řádku s Pythonem:
# seskupení podle názvu jazyka df1 = df.groupby("Winner", maintain_order=True).agg([polars.col("Year")]) df2 = df.groupby("Winner", maintain_order=True).agg([polars.col("Year").sort()]) # výběr řádků s Pythonem, výběr dat ze sloupce Year a převod na seznam print(df1[1]["Year"].to_list()[0]) print(df2[1]["Year"].to_list()[0])
Ve druhém případě dojde k seřazení seznamu let:
[2021, 2020, 2018, 2010, 2007] [2007, 2010, 2018, 2020, 2021]
Podívejme se na úplný zdrojový kód tohoto demonstračního příkladu:
#!/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").sort()]) # zobrazíme datový rámec print(df)
13. Seřazení záznamů v datovém rámci před provedením operace groupby s následnou agregací
Podívejme se nyní na to, jak bude výsledný datový rámec vytvořený operací groupby následovanou agregací vypadat v případě, že zdrojový datový rámec nejprve seřadíme podle názvu programovacího jazyka, tj. podle položek uložených ve sloupci „Winner“. Celá operace se tedy skládá z několika podoperací:
- Seřazení zdrojového datového rámce podle vybraného sloupce; výsledkem bude nový datový rámec.
- Rozdělení datového rámce na základě jména programovacího jazyka.
- Agregace podle sloupce „Year“
- Seřazení hodnot v agregovaném sloupci „Year“
Všechny tři kroky výše popsané operace lze zapsat jediným (poměrně přímočarým) výrazem:
df = df.sort("Winner"). \ groupby("Winner", maintain_order=True).agg([polars.col("Year").sort()])
Výsledný datový rámec bude v tomto případě vypadat následovně:
┌──────────────┬────────────────────────┐ │ Winner ┆ Year │ │ --- ┆ --- │ │ str ┆ list[i64] │ ╞══════════════╪════════════════════════╡ │ C ┆ [2008, 2017, 2019] │ │ C++ ┆ [2003, 2022] │ │ Go ┆ [2009, 2016] │ │ Java ┆ [2005, 2015] │ │ JavaScript ┆ [2014] │ │ Objective-C ┆ [2011, 2012] │ │ PHP ┆ [2004] │ │ Python ┆ [2007, 2010, ... 2021] │ │ Ruby ┆ [2006] │ │ Transact-SQL ┆ [2013] │ └──────────────┴────────────────────────┘
Opět si samozřejmě uvedeme úplný zdrojový kód upraveného skriptu, který tento datový rámec vytvoří a vypíše:
#!/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.sort("Winner"). \ groupby("Winner", maintain_order=True).agg([polars.col("Year").sort()]) # zobrazíme datový rámec print(df)
14. Výpočet celkového pořadí vítězných programovacích jazyků
Výsledkem agregace provedené v předchozím příkladu je seznam let, v nichž byl daný programovací jazyk vítězný. Vzhledem k tomu, že se vždy jedná o seznam let reprezentovaných seznamem (hodnot typu int64), můžeme velmi snadno příklad upravit takovým způsobem, aby vracel nikoli tento seznam, ale počet let, v nichž programovací jazyk vyhrál. V případě Pythonu by se tedy měla vypočítat hodnota 5, u C hodnota 3 atd. Tohoto cíle dosáhneme snadno – použitím metody len aplikované na agregované výsledky:
# seskupení podle názvu jazyka df = df.groupby("Winner", maintain_order=True).agg([polars.col("Year").len()])
Výsledkem by měl být datový rámec, který obsahuje ve sloupci „Year“ celkový počet let, v nichž daný programovací jazyk vyhrál:
shape: (10, 2) ┌──────────────┬──────┐ │ Winner ┆ Year │ │ --- ┆ --- │ │ str ┆ u32 │ ╞══════════════╪══════╡ │ C++ ┆ 2 │ │ Python ┆ 5 │ │ C ┆ 3 │ │ Go ┆ 2 │ │ Java ┆ 2 │ │ JavaScript ┆ 1 │ │ Transact-SQL ┆ 1 │ │ Objective-C ┆ 2 │ │ Ruby ┆ 1 │ │ PHP ┆ 1 │ └──────────────┴──────┘
Upravený skript, který vypočítal předchozí datový rámec, vypadá takto:
#!/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()]) # zobrazíme datový rámec print(df)
15. Seřazení jazyků na základě jejich celkového pořadí
Vzhledem k tomu, že jazyky (alespoň prozatím) nejsou seřazeny podle jejich celkového pořadí, musíme toto seřazení provést explicitním zavoláním metody sort na agregované výsledky reprezentované novým datovým rámcem. Povšimněte si, že jsme poměrně snadným způsobem nadefinovali „pipelinu“ s několika operacemi a poměrně sofistikovaným chováním:
# seskupení podle názvu jazyka df = df.groupby("Winner", maintain_order=True).agg([polars.col("Year").len()]).sort("Year")
S tímto výsledkem:
shape: (10, 2) ┌──────────────┬──────┐ │ Winner ┆ Year │ │ --- ┆ --- │ │ str ┆ u32 │ ╞══════════════╪══════╡ │ JavaScript ┆ 1 │ │ Transact-SQL ┆ 1 │ │ Ruby ┆ 1 │ │ PHP ┆ 1 │ │ C++ ┆ 2 │ │ Go ┆ 2 │ │ Java ┆ 2 │ │ Objective-C ┆ 2 │ │ C ┆ 3 │ │ Python ┆ 5 │ └──────────────┴──────┘
Upravený skript:
#!/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()]).sort("Year") # zobrazíme datový rámec print(df)
16. Dva způsoby korektního seřazení výsledků
Vzhledem k tomu, že nás v celkovém pořadí většinou zajímají vítězové a nikoli nutně dolní část žebříčku, je vhodnější provést seřazení jazyků na základě agregovaných počtů „vítězných let“ od nejvyšší hodnoty k hodnotě nejnižší. K tomuto účelu můžeme použít dvojici operací sort+reverse, kdy se však v paměti zbytečně vytvoří datový rámec, který je po provedení další operace zahozen:
# seskupení podle názvu jazyka df = df.groupby("Winner", maintain_order=True).agg([polars.col("Year").len()]).sort("Year").reverse()
Paměťově i z hlediska potřebného výpočetního výkonu lepší varianta spočívá v použití metody sort, které se kromě názvu sloupce předá i nepovinný parametr reverse=True:
# seskupení podle názvu jazyka df = df.groupby("Winner", maintain_order=True).agg([polars.col("Year").len()]).sort("Year", reverse=True)
V obou případech dostaneme naprosto stejný výsledek:
shape: (10, 2) ┌──────────────┬──────┐ │ Winner ┆ Year │ │ --- ┆ --- │ │ str ┆ u32 │ ╞══════════════╪══════╡ │ Python ┆ 5 │ │ C ┆ 3 │ │ Objective-C ┆ 2 │ │ Java ┆ 2 │ │ Go ┆ 2 │ │ C++ ┆ 2 │ │ PHP ┆ 1 │ │ Ruby ┆ 1 │ │ Transact-SQL ┆ 1 │ │ JavaScript ┆ 1 │ └──────────────┴──────┘
Skript, který tento datový rámec vypočítá, vypadá následovně:
#!/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()]).sort("Year").reverse() # zobrazíme datový rámec print(df)
Alternativní varianta skriptu:
#!/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()]).sort("Year", reverse=True) # zobrazíme datový rámec print(df)
17. Příklad na závěr – složitější „pipeline“, změna názvu sloupce
V knihovně Polars (a prakticky stejně dobře v knihovně Pandas) můžeme se znalostí již známých operací definovat i složitější „pipeline“, například pipeline, která provede seskupení záznamů, jejich agregaci, seřazení, otočení a výběr prvních pěti záznamů z výsledného datového rámce. Navíc pro zajímavost ještě přejmenujeme sloupec „Year“ na „Zvítězil“ (pochopitelně lze v tomto případě použít celé Unicode, nejsme tedy omezeni jen na ASCII znaky):
#!/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)
S tímto výsledkem:
shape: (5, 2) ┌─────────────┬──────────┐ │ Winner ┆ Zvítězil │ │ --- ┆ --- │ │ str ┆ u32 │ ╞═════════════╪══════════╡ │ Python ┆ 5 │ │ C ┆ 3 │ │ Objective-C ┆ 2 │ │ Java ┆ 2 │ │ Go ┆ 2 │ └─────────────┴──────────┘
18. Obsah třetí části seriálu o knihovně Polars
Jak jsme si již několikrát připomenuli v předchozím textu, 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í. Vzhledem k tomu, že se jedná o velmi důležitou vlastnost (a vlastně o jediný důvod, proč vlastně přejít od Pandas k Polars), budeme se touto velmi zajímavou problematikou zabývat v navazujícím článku.
19. 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:
20. 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/