Polars ofereix un rendiment superior a Pandas al tractar DataFrames de gran tamany, especialment en consultes. Per provar-ho usarem el terminal de Linux o de Windows per filtrar fitxers, de text pla i tamany superior a 5 MB.

Google Docs - Polars

Tasques.

Verificar text i codi del document. Provar de fer un gràfic amb un df de Polars. Preprocessament de dades.

Polars vs Pandas (revisat)

Si heu estat al dia dels avenços dels marcs de dades (dataFrames) de Python, que tenen moltes aplicacions durant els últims anys, no podríeu evitar sentir parlar de Polars, la potent biblioteca de marcs de dades dissenyada per treballar amb grans conjunts de dades.

A diferència d'altres biblioteques per treballar amb grans conjunts de dades, com ara Spark, Dask i [Ray][https://www.ray.io/] , Polars està dissenyat per utilitzar-se en una única màquina, la qual cosa provoca moltes comparacions amb pandes. Tanmateix, els polars es diferencien dels pandes en diversos aspectes importants, com ara com funciona amb les dades i quines són les seves aplicacions òptimes.

A l'article següent, explorarem els detalls tècnics que diferencien aquestes dues biblioteques de marcs de dades i veurem els punts forts i les limitacions de cadascuna.

Per què utilitzar els polars sobre els pandes?

En una paraula: rendiment. Els polars es van construir des de la base per ser increïblement ràpids i poden fer operacions habituals entre 5 i 10 vegades més ràpid que els pandes. A més, el requisit de memòria per a les operacions Polars és significativament més petit que per als pandes: els pandes requereixen entre 5 i 10 vegades més RAM que la mida del conjunt de dades per dur a terme operacions, en comparació amb les 2 o 4 vegades necessàries per als Polars.

Podeu fer-vos una idea de com funciona Polars en comparació amb altres biblioteques de marcs de dades aquí. Com podeu veure, Polars és entre 10 i 100 vegades més ràpid que els pandes per a les operacions habituals i en realitat és una de les biblioteques de DataFrame més ràpides en general. A més, pot gestionar conjunts de dades més grans que els pandes abans d'arribar a errors sense memòria.

Per què Polars és tan ràpid?

Aquests resultats són extremadament impressionants, així que potser us preguntareu: com poden els Polars obtenir aquest tipus de rendiment mentre encara funcionen amb una sola màquina? La biblioteca s'ha dissenyat tenint en compte el rendiment des del principi, i això s'aconsegueix mitjançant diferents mitjans.

Escrit en Rust

Un dels fets més coneguts sobre Polars és que està escrit en Rust, un llenguatge de baix nivell que és gairebé tan ràpid com C i C++. En canvi, els pandas es construeixen sobre les biblioteques de Python, una d'aquestes és NumPy. Tot i que el nucli de NumPy està escrit en C, encara està afectat per problemes inherents a la forma en què Python gestiona certs tipus a la memòria, com ara cadenes per a dades categòriques, la qual cosa condueix a un rendiment deficient quan es maneja aquests tipus (vegeu aquesta fantàstica publicació de bloc de Wes McKinney per a més detalls).

Un dels altres avantatges d'utilitzar Rust és que permet una concurrència segura; és a dir, està dissenyat per fer el paral·lelisme el més previsible possible. Això vol dir que Polars pot utilitzar amb seguretat tots els nuclis de la vostra màquina fins i tot per a consultes complexes que incloguin diverses columnes, cosa que va portar a Ritchie Vink a descriure el rendiment de Polar com a "vergonyosament paral·lel". Això proporciona a Polars un augment de rendiment massiu respecte als pandes, que només utilitza un nucli per dur a terme operacions. Fes un cop d'ull a aquesta excel·lent xerrada de Nico Kreiling de PyCon DE aquest any, que detalla com ho aconsegueix Polars.

Basat en Arrow

Un altre factor que contribueix al rendiment impressionant de Polars és Apache Arrow, un format de memòria independent del llenguatge de programació. Arrow va ser co-creat per Wes McKinney en resposta a molts dels problemes que va veure amb els pandes a mesura que la mida de les dades va explotar. Pyarrow també és el backend per a pandas 2.0, una versió més eficient de pandas llançada al març de l'any 2023. Tanmateix, els backends d'Arrow de les biblioteques difereixen lleugerament: mentre que pandas 2.0 es basa en PyArrow, l'equip de Polars va crear la seva pròpia implementació d'Arrow.

Un dels principals avantatges de crear una biblioteca de dades a Arrow és la interoperabilitat. Arrow s'ha dissenyat per estandarditzar el format de dades en memòria utilitzat a les biblioteques, i ja l'utilitzen diverses biblioteques i bases de dades importants, com podeu veure a continuació, al web de Pyarrow.

Aquesta interoperabilitat accelera el rendiment, ja que evita la necessitat de convertir les dades en un format diferent per passar-les entre diferents passos de la canalització de dades (és a dir, evita la necessitat de serialitzar i deserialitzar les dades).

També és més eficient en memòria, ja que dos processos poden compartir les mateixes dades sense necessitat de fer-ne una còpia. Com que s'estima que la serialització/deserialització representa entre el 80 i el 90% dels costos informàtics dels fluxos de treball de dades, el format de dades comú d'Arrow proporciona a Polars guanys de rendiment significatius.

Arrow també té suport integrat per a una gamma més àmplia de tipus de dades que els pandes. Com que Pandas es basa en NumPy, és excel·lent per manejar columnes senceres i flotants, però té problemes amb altres tipus de dades. En canvi , Arrow té un suport sofisticat per a tipus de columnes datetime, boolean, binari i fins i tot complexos, com els que contenen llistes. A més, Arrow és capaç de gestionar de manera nativa les dades que falten, cosa que requereix una solució alternativa a NumPy.

Finalment, Arrow utilitza l'emmagatzematge de dades en columna, el que significa que, independentment del tipus de dades, totes les columnes s'emmagatzemen en un bloc continu de memòria. Això no només facilita el paral·lelisme, sinó que també facilita la recuperació de dades.

Otimització de consultes

Un dels altres nuclis del rendiment de Polars és com avalua el codi. Pandas, per defecte, utilitza eager execution, realitzant operacions en l'ordre que les heu escrit. En canvi, Polars té la capacitat de fer una execució amb lazy execution, on prèviament un optimitzador de consultes avaluarà totes les operacions necessàries i traçarà la forma més eficient d'executar el codi.

Això pot incloure, entre altres coses, reescriure l'ordre d'execució de les operacions o eliminar els càlculs redundants. Preneu, per exemple, l'expressió següent per obtenir la mitjana de la columna Number1 per a cadascuna de les categories “A” i “B” a Category.

Si aquesta expressió s'executa de forma anosiosa (eager), el groupby L'operació es realitzarà innecessàriament per a tot el DataFrame i després es filtrarà per Category. Amb una execució mandrosa (lazy), el DataFrame es pot filtrar i groupby realitzat només amb les dades requerides.

API expressiva

Finalment, Polars té una API extremadament expressiva, el que significa que bàsicament qualsevol operació que vulgueu realitzar es pot expressar com un mètode Polars. En canvi, les operacions més complexes en pandes sovint s'han de passar a applymètode com a expressió lambda. El problema amb el applyEl mètode és que fa un bucle sobre les files del DataFrame, executant seqüencialment l'operació en cadascuna. Poder utilitzar mètodes integrats us permet treballar a nivell columnar i aprofitar una altra forma de paral·lelisme anomenada SIMD.

Quan t'has de quedar amb els pandes?

Tot això sona tan sorprenent que probablement us preguntareu per què us molesteu més amb els pandes. No tant ràpid! Tot i que Polars és excel·lent per fer transformacions de dades extremadament eficients, actualment no és l'opció òptima per a l'exploració de dades o per utilitzar-la com a part de canalitzacions d'aprenentatge automàtic. Són zones on els pandes segueixen brillant.

Una de les raons d'això és que, tot i que Polars té una gran interoperabilitat amb altres paquets que utilitzen Arrow, encara no és compatible amb la majoria dels paquets de visualització de dades de Python ni amb biblioteques d'aprenentatge automàtic com scikit-learn i PyTorch. L'única excepció és Plotly, que us permet crear gràfics directament des de Polars DataFrames.

Una solució que s'està discutint és utilitzar el protocol d'intercanvi de marcs de dades Python en aquests paquets per permetre-los suportar una sèrie de biblioteques de marcs de dades, cosa que significaria que els fluxos de treball de ciència de dades i aprenentatge automàtic ja no es veurien bloquejats pels pandes. No obstant això, aquesta és una idea relativament nova, i aquests projectes necessitaran temps per implementar-se.

Provem Polars i Pandas.

Una de les maneres de familaritzar-nos amb Polars i provar el seu rendiment és creant un dataFrame amb Polars i Pandas; amb Python i realitzar una comparativa del temps que triguen en les operacions més habituals.

En aquesta guia queda pendent provar Polars amb Rust i Javascript.

Les dades que generarem i usarem pel dataframe les tenim en aquest diccionari:

import pandas as pd 
import polars as pl
import io
import time
import numpy as np
import matplotlib.pyplot as plt

def create_dataframe(nrows, library):
    if library == 'pandas':
        data = {
            'name': np.random.choice(['Alice', 'Bob', 'Charlie', 'David', 'Eva'], nrows),
            'age': np.random.randint(18, 65, size=nrows),
            'city': np.random.choice(['New York', 'San Francisco', 'Los Angeles'], nrows),
            'income': np.random.normal(50000, 10000, size=nrows),
            'gender': np.random.choice(['Male', 'Female'], nrows),
            'is_married': np.random.choice([True, False], nrows),
            'children': np.random.randint(0, 5, size=nrows),
            'zip_code': np.random.randint(10000, 99999, size=nrows),
            'interest_rate': np.random.uniform(0, 0.1, size=nrows),
            'is_default': np.random.choice([True, False], nrows)
        }
        return pd.DataFrame(data)

    elif library == 'polars':
        data = {
            'name': np.random.choice(['Alice', 'Bob', 'Charlie', 'David', 'Eva'], nrows).astype('str'),
            'age': np.random.randint(18, 65, size=nrows).astype('int'),
            'city': np.random.choice(['New York', 'San Francisco', 'Los Angeles'], nrows).astype('str'),
            'income': np.random.normal(50000, 10000, size=nrows).astype('float'),
            'gender': np.random.choice(['Male', 'Female'], nrows).astype('str'),
            'is_married': np.random.choice([True, False], nrows).astype('bool'),
            'children': np.random.randint(0, 5, size=nrows).astype('int'),
            'zip_code': np.random.randint(10000, 99999, size=nrows).astype('str'),
            'interest_rate': np.random.uniform(0, 0.1, size=nrows).astype('float'),
            'is_default': np.random.choice([True, False], nrows).astype('bool')
        }
        return pl.DataFrame(data)
  
def save_dataframe_to_csv(df, file_name):
    if isinstance(df, pd.DataFrame):
        df.to_csv(file_name)
    elif isinstance(df, pl.DataFrame):
        df.write_csv(file_name)

def read_dataframe_from_csv(file_name, library):
    if library == 'pandas':
        return pd.read_csv(file_name)
    elif library == 'polars':
        return pl.read_csv(file_name)

def sort_dataframe(df, column_name, ascending=True):
    if isinstance(df, pd.DataFrame):
        return df.sort_values(by=column_name, ascending=ascending)
    elif isinstance(df, pl.DataFrame):
        return df.sort(column_name, descending=not ascending)

def filter_dataframe(df, column, value):
    if isinstance(df, pd.DataFrame):
        return df[df[column] == value]
    elif isinstance(df, pl.DataFrame):
        return df.filter(pl.col(column) == value)

def groupby_dataframe(df, column_name):
    start_time = time.time()
    if isinstance(df, pd.DataFrame):
        result = df.groupby(column_name).size()
    elif isinstance(df, pl.DataFrame):
        result = df.groupby(column_name).agg(pl.count())
    end_time = time.time()
    return end_time - start_time

def compare_runtimes(pandas_times, polars_times):
    fig, ax = plt.subplots(figsize=(12, 4))
    ax.set_title("Comparison of Runtimes for Pandas and Polars")
    ax.set_xlabel("Operations")
    ax.set_ylabel("Time (Seconds)")

    operations = pandas_times.keys()
    num_operations = len(operations)
    bar_width = 0.35
    pandas_x = np.arange(num_operations)
    polars_x = pandas_x + bar_width

    pandas_y = list(pandas_times.values())
    polars_y = list(polars_times.values())

    pandas_bar = ax.bar(pandas_x, pandas_y, bar_width, color='tab:blue', label='Pandas')
    polars_bar = ax.bar(polars_x, polars_y, bar_width, color='tab:orange', label='Polars')

    ax.set_xticks(pandas_x + bar_width / 2)
    ax.set_xticklabels(operations)

    ax.set_ylim(0, max(max(pandas_y), max(polars_y)) * 1.2)

    for i, bars in enumerate(zip(pandas_bar, polars_bar)):
        for bar in bars:
            height = bar.get_height()
            ax.annotate(f"{height:.4f}", xy=(bar.get_x() + bar.get_width() / 2, height),
                        xytext=(0, 3), textcoords="offset points", ha='center', va='bottom')

    ax.legend()
    plt.show()

# main

nrows = 1000000
file_name_pan = 'data_pandas.csv'
file_name_pol = 'data_polars.csv'

ops=['Create','Save','Read','Filter','Sort','GroupBy']
pandas_times = {}
polars_times = {}

# Creating DataFrames
start_time = time.time()
df_pandas = create_dataframe(nrows, 'pandas')
pandas_times['Create'] = time.time() - start_time
print(f'pandas_times.Create: {pandas_times["Create"]}')

start_time = time.time()
df_polars = create_dataframe(nrows, 'polars')
polars_times['Create'] = time.time() - start_time
print(f'polars_times.Create: {polars_times["Create"]}')

# Saving DataFrames
start_time = time.time()
save_dataframe_to_csv(df_pandas, file_name_pan)
pandas_times['Save'] = time.time() - start_time
print(f'pandas_times.Save: {pandas_times["Save"]}')

start_time = time.time()
save_dataframe_to_csv(df_polars, file_name_pol)
polars_times['Save'] = time.time() - start_time
print(f'polars_times.Save: {polars_times["Save"]}')

# Reading DataFrames
start_time = time.time()
df_pandas = read_dataframe_from_csv(file_name_pan, 'pandas')
pandas_times['Read'] = time.time() - start_time
print(f'pandas_times.Read: {pandas_times["Read"]}')

start_time = time.time()
df_polars = read_dataframe_from_csv(file_name_pol, 'polars')
polars_times['Read'] = time.time() - start_time
print(f'polars_times.Read: {polars_times["Read"]}')

# Sorting DataFrames
start_time = time.time()
sort_dataframe(df_pandas, 'gender', ascending=True)
pandas_times['Sort'] = time.time() - start_time
print(f'pandas_times.Sort: {pandas_times["Sort"]}')

start_time = time.time()
sort_dataframe(df_polars, 'gender', ascending=True)
polars_times['Sort'] = time.time() - start_time
print(f'polars_times.Sort: {polars_times["Sort"]}')

# Filtering DataFrames
start_time = time.time()
filter_dataframe(df_pandas, 'gender', 'Male')
pandas_times['Filter'] = time.time() - start_time
print(f'pandas_times.Filter: {pandas_times["Filter"]}')

start_time = time.time()
filter_dataframe(df_polars, 'gender', 'Male')
polars_times['Filter'] = time.time() - start_time
print(f'polars_times.Filter: {polars_times["Filter"]}')

# Grouping DataFrames
start_time = time.time()
pandas_times['GroupBy'] = groupby_dataframe(df_pandas, 'gender')
print(f'pandas_times.GroupBy: {pandas_times["GroupBy"]}')

start_time = time.time()
polars_times['GroupBy'] = groupby_dataframe(df_polars, 'gender')
print(f'polars_times.GroupBy: {polars_times["GroupBy"]}')

compare_runtimes(pandas_times, polars_times)

Resultat (al incloure aleatorietat pot canviar una mica):

pandas_times.Create: 0.759660005569458
polars_times.Create: 2.485807180404663
pandas_times.Save: 8.762519121170044
polars_times.Save: 1.3340067863464355
pandas_times.Read: 1.1826341152191162
polars_times.Read: 0.4700584411621094
pandas_times.Sort: 0.9801979064941406
polars_times.Sort: 0.2534444332122803
pandas_times.Filter: 0.11046671867370605
polars_times.Filter: 0.07709980010986328
pandas_times.GroupBy: 0.061829566955566406
polars_times.GroupBy: 0.03566145896911621

Gràfic de resultats:

Tal i com podem veure, a excepció de la creació de dataFrames que Pandas la executa uns 3 cops més ràpidament, la resta d'operacions les executa força més ràpid, tant guardar com les consultes, ordenacions, agrupació...

Faltaria una altra prova: llegir un dataFrame de dades existents de gran tamany (+10000 linies i +1MB).

Aquesta prova, i moltes d'altres les han realitzat en aquest article d'inicis del 2024; on comparen Pola.rs i Pandas amb Numpy i Pandas amb Pyarrow.

És molt recomanable que el consulteu, i les imatges dels gràfics resultants són molt divertides.

En conclusió, si volem mostrar estadístiques i gràfics senzills de grans volums de dades que provinguin d'un o més fitxers, val la pena que usem Pola.rs

--

Preprocessament previ de dades amb Linux: Cas enfermentats EEUU Tycho.

Tant si utilitzem dataFrames de Pola.rs com de Pandas, sempre és una bona pràctica filtrar les dades que necessitem (per files i columnes). Quan el tamany de les dades és molt gran (més d'1MB) surt més a compte realitzar el filtratge amb les funcions del sistema operatiu, especialment si comptem amb el llenguatge bash, present al terminal de Linux.

En aquesta secció repassarem com podem utilitzar els mètodes de bash més comuns amb el voluminós dataset d'exemple del Projecte Tycho

Aquest dataset conté un historial del recompte de casos de diverses enfermetats que han afectat als EEUU des del 1888 al 2014.

En alguns anys hi ha molts registres (1900-1950) i d’altres menys, però en total podem tenir més d’10M d’observacions; cadascuna de les quals té 10 columnes d'interès.

Per accedir-hi ens podem registrar (és gratuït) però no ens cal, ja que tenim les dades de fa 2 anys i no ens cal que siguin actualitzades per aquest exemple.

Hem penjat un subset de 1M de línies en aquest fitxer (120 MB un cop descomprimit):

Versió reduïda Tycho

Un cop descarregat, obriu un terminal de Linux i proveu aquestes operacions.

⚠ No obriu el fitxer dirèctament o us arrisqueu a que la màquina se us pengi, o perdreu temps esperant la càrrega ⚠

Mostra les 10 primeres línies del fitxer (la capçalera i 9 més)

cat tycho.csv 

⚠ Mostra tot el contingut del fitxer pel terminal. Si executeu la comanda per accident i no voleu esperar, premeu Ctrl+C per finalitzar el procés. ⚠

cat tycho.csv 

Compta el número de línies en total.

cat tycho.csv | wc -l 

Mostra les 10 primeres línies que continguin el text 1888. En el nostre cas l'any 1888.

head tycho-mini.csv | grep 1888

Fixeu-vos que amb el tros de comanda '| grep 1888' podem filtrar per files, de tot el fitxer només les línies que continguin el número 1888. No és perfecte, però segur que seleccionarà totes les mostres de l'any 1888 (i potser algún altre any amb 1888 casos, però això es pot perfeccinar més endavant amb altres comandes o amb Python)

Com podeu veure, tots els filtres que apliquem es mostren per pantalla, però si volem que es guardin en un fitxer disposem de l'operador de redirecció '>'.

Amb aquesta comanda, guardo la info del fixer de 3 anys en el fitxer anomenat tycho-optim.csv:

cat tycho-mini.csv | grep -E ‘1910|1911|1912> tycho-optim.csv

Amb una sola comanda hem aconseguit reduïr el fitxer a les 78000 línies (aproximadament) que ens interessen. Oi que val la pena aquest preprocessament ?

--

Pandas i Polars amb dades preprocessades: Cas enfermentats EEUU Tycho.

Fixa't amb les columnes que disposem originalment al fitxer CSV de Tycho:

epi_week Setmana epidemiològica (de l'1 al 52 normalment, algún cop hi ha 53). És una mètrica necessària i molt habitual en la informàtica mèdica.

country País. En aquest dataset només hi ha mostres dels Estats Units, per tant podrem ometre-la. US

state Sigles de l'estat dels EEUU.

loc Nom complet de l'estat dels EEUU. Guardar state i loc (info redundant) només si volem visualitzar mapes.

loc_type En el dataset pot ser CITY o STATE.

disease Enfermetat. Entre [] ens indica informació addicional, que potser en el nostre estudi és necessària i ometre-la ens pot estalviar memòria del dataFrame.

event Cada event pot ser de 2 tipus, i és important distingir-los segons el que volguem estudiar: CASES (número de casos), DEATHS(número de morts causats per la enfermetat). Si volem treballar bé aquest estudi es pot calcular una ràtio de CASES i DEATHS, que el seu resultat serà entre 0 i 1 (1 si tots els casos han estat mortals)

number Important! Número de casos (si event='CASES') o número de morts (si event='DEATHS')

from_date,to_date Dates d'inici i de fi en què es mesura el número de casos. Són (o haurien de ser) intèrvals d'una setmana.

url El projecte Tycho ha escanejat i/o digitalitzat documents de paper a PDF (anys 1980 i anteriors) que demostren els registres realitzats i han de ser molt interessants. Desgraciadament l'enllaç proporcionat no funciona.

Finalment, remarcar que podem observar que el dataset està en format Tidy, que és el que desitgem per poder realitzar estadítica i gràfics. Aquest punt és fonamental verificar-lo abans de començar a investigar un dataset.

  • Cada fila és una observació
  • Cada columna és una variable
  • Cada valor té una única dada

Si el dataset no fos Tidy, hauriem de preprocessar-lo i arreglar-lo fins que ho sigui.

EXEMPLE. Agafa el fitxer de Tycho Dataset de 78 mil línies aproximadament que hem vist i crea una funció anonemanda fix_broken_tycho() que realitzi aquesta selecció de dades amb Pandas i Polars.

# -----------------------------------------------------------------------------
# Question: fix_broken_tycho()
# -----------------------------------------------------------------------------
# 
# - You are given a broken Tycho dataset. Write a function to fix it.
# - The function fix_broken_tycho() must do the following:
#   1. Drop 'country' and 'url' columns
#   2. Rename 'evnt' to 'event'
#   3. Cleanup the diseases removing the names in square brackets. (See hint below)
#   4. Add a new column called 'year' of type 'int' with the year from the epi_week.
#   5. Select rows where the year is 1910 or 1911.
#   6. Add a new column called 'id' with a numerical unique identifier starting from 0
#   7. Rename 'loc' to 'city', 'number' to 'deaths'
#   8. Reorder columns as follows: ['id', 'year', 'epi_week', 'from_date', 'to_date', 'state', 'city', 'disease', 'deaths']
# 
# - Return parameters:
#   - Return the fixed entries as a dataframe.
#   - The index must be numerical, starting from 0.
# 
# - Hints:
#   .str.replace(pat=r' \[.*\]', repl='', regex=True)
#   Use auxiliar method get_year(epi_week: int) -> int
# 
# - Remember:
#   - Write your solution inside the given function.
#   - Functions must be pure. Remember to delete your print() calls when done.
#   - Run pytest to be sure you succeeded.
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
def get_year(epi_week: int) -> int:
    ''' 
    Pure function get transforms an epidemic week
    and returns the year.
    '''
    epi_week_str: str = str(epi_week)
    year_str:     str = epi_week_str[0:4]
    year_int:     int = int(year_str)

    return year_int
# -----------------------------------------------------------------------------

Codi complet Pandas.

import pandas as pd

def get_year(epi_week: int) -> int:
    ''' 
    Pure function get transforms an epidemic week
    and returns the year.
    '''
    epi_week_str: str = str(epi_week)
    year_str:     str = epi_week_str[0:4]
    year_int:     int = int(year_str)

    return year_int

def fix_broken_tycho(entries: pd.DataFrame) -> pd.DataFrame:

    alt_name_regex:   str       = r' \[.*\]'
    sorted_columns: list[str] = ['id', 'year', 'epi_week', 'from_date', 'to_date', 'state', 'city', 'disease', 'deaths']
    valid_years: list[int] = [1910,1911]

    # Drop and rename columns. 
    fixed_entries: pd.DataFrame = (entries.drop(  columns=['country', 'url'])
                                        .rename(columns={'evnt': 'event', 'loc': 'city', 'number': 'deaths'})             
                                        .assign(disease=lambda df: df.disease.str.replace(pat=alt_name_regex, repl='', regex=True)) #elimina square brackets, seleccionas misma columna
                                         .assign(year   =lambda df: df.epi_week.apply(get_year) )
                                         .query('year in @valid_years')
                                         .reset_index(drop=True)
                                         .assign(id=lambda df: df.index) #copio a "mà" el index, a una nova columna id
                                         .rename(columns={'loc': 'city', 'number': 'deaths'})
                                         .reindex(columns=sorted_columns)
    )

    return fixed_entries
if __name__ == "__main__":

    # https://gitlab.com/xtec/bio/pandas/-/raw/main/data/tycho/tycho78k.csv
    file_csv: str = "tycho78k.csv"
    broken_entries: pd.DataFrame = pd.read_csv(file_csv, sep=",")
    print(broken_entries)

    fixed_entries: pd.DataFrame = fix_broken_tycho(broken_entries)

    print(fixed_entries.head())

Codi complet Polars.

import polars as pl

def get_year(epi_week: int) -> int:
    ''' 
    Pure function get transforms an epidemic week
    and returns the year.
    '''
    epi_week_str: str = str(epi_week)
    year_str:     str = epi_week_str[0:4]
    year_int:     int = int(year_str)

    return year_int


def fix_broken_tycho_polars(entries: pl.DataFrame) -> pl.DataFrame:

    alt_name_regex:   str        = r' \[.*\]'
    sorted_columns: list[str]  = ['id', 'year', 'epi_week', 'from_date', 'to_date', 'state', 'city', 'disease', 'deaths']
    valid_years: list[int] = [1910,1911]

    # Transformació pas a pas
    fixed_entries: pl.DataFrame = (
        entries
        .drop(['country', 'url'])  # Elimina columnes innecessàries
        .rename({'loc': 'city', 'number': 'deaths'})  # Reanomena columnes
        .with_columns(
            pl.col('disease').str.replace_all(alt_name_regex, '').alias('disease'),  # Neteja la columna 'disease'
            pl.col('epi_week').apply(get_year).alias('year')  # Extreu l'any de 'epi_week'
        )
        .filter(pl.col('year').is_in(valid_years))  # Filtra per anys vàlids
        .with_row_count(name='id')  # Afegeix una columna 'id' que comença des de 0
        .select(sorted_columns)  # Reordena les columnes
    )

    return fixed_entries
if __name__ == "__main__":

    # Llegeix el fitxer CSV amb polars
    # https://gitlab.com/xtec/bio/pandas/-/raw/main/data/tycho/tycho78k.csv
    file_csv: str = "tycho78k.csv"

    broken_entries: pl.DataFrame = pl.read_csv(source=file_csv, separator=",")
    print(broken_entries)
    
    # Aplica la funció per corregir el dataset
    fixed_entries = fix_broken_tycho_polars(broken_entries)

    # Mostra les primeres files del dataframe corregit
    print(fixed_entries.head())

Exercicis. Consultes en Pandas i Polars.

Realitza les següents consultes, a partir del fitxer generat a l'anterior exemple:

  1. Llista de totes les ciutats que surten al fitxer, que no es repeteixin.

  2. Llista el número total de morts de cada malaltia, ordenada pel número de morts.

  3. Mostra el número de morts per la tuberculosi, a Nova York, l'any 1910.

Aquest fitxer filtrat el pots trobar en aquest repositori de Gitlab que conté codi font:

Resultat esperat; per a poder dissenyar tests.

  1. Llista de ciutats: Us n'haurien de sortir 247.
┌──────────────────┐
│ city            │
╞══════════════════╡
│ OAKLAND          │
│ ANN ARBOR        │
│ BIDDEFORD        │
│ SARATOGA SPRINGS │
│ …                │
│ BENNINGTON       │
│ BRADDOCK         │
│ CHARLOTTE        │
│ CHICAGO          │
└──────────────────┘
  1. Llista el número total de morts de cada malaltia, ordenada pel número de morts.
┌────────────────┬────────┐
│ disease        ┆ deaths │
╞════════════════╪════════╡
│ TUBERCULOSIS   ┆ 181972 │
│ SCARLET FEVER  ┆ 110893 │
│ DIPHTHERIA     ┆ 100049 │
│ TYPHOID FEVER  ┆ 44291  │
│ WHOOPING COUGH ┆ 14713  │
└────────────────┴────────┘
  1. Nombre de morts per TUBERCULOSIS a NEW YORK l'any 1910 (Polars): 32403

Solucions Pandas

import pandas as pd

# Suposem que 'fixed_entries' és el DataFrame que conté les dades del CSV
# https://gitlab.com/xtec/bio/pandas/-/raw/main/data/tycho/fixed_entries.csv?ref_type=heads

file_csv: str = "fixed_entries.csv"
broken_entries: pd.DataFrame = pd.read_csv(file_csv, sep=",")

# 1. Llista de totes les ciutats
cities = fixed_entries['city'].unique()
print("Llista de ciutats:", cities)

# 2. Llista de malalties ordenada pel número de morts
disease_deaths = fixed_entries.groupby('disease')['deaths'].sum().sort_values(ascending=False)
print("Malalties ordenades per número de morts:\n", disease_deaths)

# 3. Filtrar per les condicions especificades
tuberculosis_ny_1910 = fixed_entries[
    (fixed_entries['disease'] == 'TUBERCULOSIS') & 
    (fixed_entries['city'] == 'NEW YORK') & 
    (fixed_entries['year'] == 1910)
]

# Sumar el número de morts
num_deaths = tuberculosis_ny_1910['deaths'].sum()
print(f"Nombre de morts per TUBERCULOSIS a NEW YORK l'any 1910 (Pandas): {num_deaths}")

Solucions Polars

import polars as pl

# Suposem que 'fixed_entries' és el DataFrame que conté les dades del CSV
# https://gitlab.com/xtec/bio/pandas/-/raw/main/data/tycho/fixed_entries.csv?ref_type=heads

file_csv: str = "fixed_entries.csv"
broken_entries: pl.DataFrame = pl.read_csv(source=file_csv, separator=",")

# 1. Llista de totes les ciutats
cities = fixed_entries.select('city').unique()
print("Llista de ciutats:", cities)

# 2. Llista de malalties ordenada pel número de morts
disease_deaths = (
    fixed_entries.groupby('disease')
    .agg(pl.col('deaths').sum())
    .sort('deaths', descending=True)
)
print("Malalties ordenades per número de morts:\n", disease_deaths)

# 3. Filtrar per les condicions especificades
tuberculosis_ny_1910 = fixed_entries.filter(
    (pl.col('disease') == 'TUBERCULOSIS') & 
    (pl.col('city') == 'NEW YORK') & 
    (pl.col('year') == 1910)
)

# Sumar el número de morts
num_deaths = tuberculosis_ny_1910.select(pl.col('deaths').sum()).item()
print(f"Nombre de morts per TUBERCULOSIS a NEW YORK l'any 1910 (Polars): {num_deaths}")

--

Ara faltatia explotar una mica més aquest fitxer, realitzant algún gràfic o altres estadístiques. Encara que sigui un historial de malalties de fa molts anys, la forma d'organitzar les dades que s'usa avui en dia és força semblant, bancs de dades amb format Tidy.

Per això considerem que és molt útil i adient realitzar consultes com aquestes.

Referències