Polars - Consulta

  • Una consulta és un conjunt d'expressions que s'apliquen a un context

    Introducció

    Crea el projecte polars-query.

    Expressió

    A Polars, una expressió és una representació “lazy” d’una transformació de dades.

    Les expressions són modulars i flexibles, és a dir, les pots fer servir com a blocs de construcció per crear expressions més complexes.

    Aquí tens un exemple d’expressió de Polars:

    import polars as pl
    
    pl.col("weight") / (pl.col("height") ** 2)

    Com pots intuir, aquesta expressió agafa una columna anomenada “weight” i divideix els seus valors pel quadrat dels valors de la columna “height”, calculant el BMI (o IMC, índex de massa corporal) d’una persona.

    El codi anterior expressa un càlcul abstracte que pots desar en una variable, manipular més endavant o simplement imprimir:

    bmi_expr = pl.col("weight") / (pl.col("height") ** 2)
    print(bmi_expr)
    [(col("weight")) / (col("height").pow([dyn int: 2]))]

    Com que les expressions són “lazy”, encara no s’ha realitzat cap càlcul.

    Context

    Les expressions de Polars necessiten un context en el qual s’executen per produir un resultat.

    Depenent del context en què s’utilitzi, la mateixa expressió de Polars pot produir resultats diferents.

    Com que les expressions són “lazy”, quan utilitzes una expressió dins d’un context, Polars pot intentar simplificar-la abans d’executar la transformació de dades que expressa.

    Les expressions separades dins d’un context es poden executar en paral·le i Polars se n’aprofita, alhora que paral·lelitza l’execució d’expressions quan s’utilitza l’expansió d’expressions.

    Encara es poden obtenir més guanys de rendiment quan s’utilitza l’API “lazy” de Polars.

    En aquesta secció aprendrem sobre els quatre contexts més comuns que ofereix Polars:

    1. select
    2. with_columns
    3. filter
    4. group_by

    Farem servir el dataframe següent per mostrar com funciona cada context.

    import polars as pl
    from datetime import date
    
    df = pl.DataFrame(
        {
            "name": ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
            "birthdate": [
                date(1997, 1, 10),
                date(1985, 2, 15),
                date(1983, 3, 22),
                date(1981, 4, 30),
            ],
            "weight": [57.9, 72.5, 53.6, 83.1],  # (kg)
            "height": [1.56, 1.77, 1.65, 1.75],  # (m)
        }
    )

    select

    El context de selecció select aplica expressions sobre columnes.

    El context select pot produir columnes noves que siguin agregacions, combinacions d’altres columnes o literals:

    result = df.select(
        pl.col("name"),
        bmi=bmi_expr,
        avg_bmi=bmi_expr.mean(),
        ideal_max_bmi=25,
    )
    
    print(result)
    shape: (4, 4)
    ┌────────────────┬───────────┬───────────┬───────────────┐
    │ name           ┆ bmi       ┆ avg_bmi   ┆ ideal_max_bmi │
    │ ---            ┆ ---       ┆ ---       ┆ ---           │
    │ str            ┆ f64       ┆ f64       ┆ i32           │
    ╞════════════════╪═══════════╪═══════════╪═══════════════╡
    │ Alice Archer   ┆ 23.791913 ┆ 23.438973 ┆ 25            │
    │ Ben Brown      ┆ 23.141498 ┆ 23.438973 ┆ 25            │
    │ Chloe Cooper   ┆ 19.687787 ┆ 23.438973 ┆ 25            │
    │ Daniel Donovan ┆ 27.134694 ┆ 23.438973 ┆ 25            │
    └────────────────┴───────────┴───────────┴───────────────┘

    Les expressions en un context select han de produir sèries totes de la mateixa longitud o bé produir un escalar.

    Els escalars es difonen (broadcast) per igualar la longitud de les sèries restants. Els literals, com el nombre utilitzat abans, també es difonen.

    Tingues en compte que la difusió també pot passar dins de les expressions.

    Per exemple, considera l’expressió següent:

    result = df.select(
        pl.col("name"),
        deviation=(bmi_expr - bmi_expr.mean()) / bmi_expr.std()
    )
    
    print(result)
    shape: (4, 2)
    ┌────────────────┬───────────┐
    │ name           ┆ deviation │
    │ ---            ┆ ---       │
    │ str            ┆ f64       │
    ╞════════════════╪═══════════╡
    │ Alice Archer   ┆ 0.115645  │
    │ Ben Brown      ┆ -0.097471 │
    │ Chloe Cooper   ┆ -1.22912  │
    │ Daniel Donovan ┆ 1.210946  │
    └────────────────┴───────────┘

    Tant la resta com la divisió utilitzen difusió (broadcasting) dins de l’expressió perquè les subexpressions que calculen la mitjana i la desviació estàndard s’avaluen a valors únics.

    El context select és molt flexible i potent, i et permet avaluar expressions arbitràries de manera independent i en paral·lel entre elles.

    Això també és cert per als altres contexts que veurem a continuació.

    with_columns

    El context with_columns és molt similar al context select.

    La diferència principal entre tots dos és que el context with_columns crea un dataframe nou que conté les columnes del dataframe original i les noves columnes segons les seves expressions d’entrada, mentre que el context select només inclou les columnes seleccionades per les seves expressions d’entrada:

    result = df.with_columns(
        bmi=bmi_expr,
        avg_bmi=bmi_expr.mean(),
        ideal_max_bmi=25,
    )
    print(result)
    shape: (4, 7)
    ┌────────────────┬────────────┬────────┬────────┬───────────┬───────────┬───────────────┐
    │ name           ┆ birthdate  ┆ weight ┆ height ┆ bmi       ┆ avg_bmi   ┆ ideal_max_bmi │
    │ ---            ┆ ---        ┆ ---    ┆ ---    ┆ ---       ┆ ---       ┆ ---           │
    │ str            ┆ date       ┆ f64    ┆ f64    ┆ f64       ┆ f64       ┆ i32           │
    ╞════════════════╪════════════╪════════╪════════╪═══════════╪═══════════╪═══════════════╡
    │ Alice Archer   ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   ┆ 23.791913 ┆ 23.438973 ┆ 25            │
    │ Ben Brown      ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   ┆ 23.141498 ┆ 23.438973 ┆ 25            │
    │ Chloe Cooper   ┆ 1983-03-22 ┆ 53.6   ┆ 1.65   ┆ 19.687787 ┆ 23.438973 ┆ 25            │
    │ Daniel Donovan ┆ 1981-04-30 ┆ 83.1   ┆ 1.75   ┆ 27.134694 ┆ 23.438973 ┆ 25            │
    └────────────────┴────────────┴────────┴────────┴───────────┴───────────┴───────────────┘

    A causa d’aquesta diferència entre select i with_columns, les expressions utilitzades en un context with_columns han de produir sèries que tinguin la mateixa longitud que les columnes originals del dataframe, mentre que en el context select n’hi ha prou que les expressions produeixin sèries de la mateixa longitud entre elles.

    filter

    El context filter filtra les files d’un dataframe basant-se en una o més expressions que avaluen al tipus de dada booleà.

    result = df.filter(
        pl.col("birthdate").is_between(date(1982, 12, 31), date(1996, 1, 1)),
        pl.col("height") > 1.7
    )
    print(result)
    shape: (1, 4)
    ┌───────────┬────────────┬────────┬────────┐
    │ name      ┆ birthdate  ┆ weight ┆ height │
    │ ---       ┆ ---        ┆ ---    ┆ ---    │
    │ str       ┆ date       ┆ f64    ┆ f64    │
    ╞═══════════╪════════════╪════════╪════════╡
    │ Ben Brown ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   │
    └───────────┴────────────┴────────┴────────┘

    group_by

    En el context group_by, les files s’agrupen segons els valors únics de les expressions d’agrupació.

    A continuació pots aplicar expressions als grups resultants, que poden tenir longituds variables.

    Quan uses el context group_by, pots fer servir una expressió per calcular les agrupacions dinàmicament:

    result = df.group_by(
        (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
    ).agg(pl.col("name"))
    print(result)
    shape: (2, 2)
    ┌────────┬─────────────────────────────────┐
    │ decade ┆ name                            │
    │ ---    ┆ ---                             │
    │ i32    ┆ list[str]                       │
    ╞════════╪═════════════════════════════════╡
    │ 1990   ┆ ["Alice Archer"]                │
    │ 1980   ┆ ["Ben Brown", "Chloe Cooper", … │
    └────────┴─────────────────────────────────┘

    Després d’utilitzar group_by fem servir agg per aplicar expressions d’agregació als grups.

    Com que a l’exemple anterior només vam especificar el nom d’una columna, obtenim els grups d’aquella columna com a llistes.

    Pots especificar tantes expressions d’agrupació com vulguis, i el context group_by agruparà les files segons els valors distintius a través de les expressions especificades.

    Aquí agrupem per una combinació de la dècada de naixement i si la persona és més baixa d’1,7 metres:

    result = df.group_by(
        (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
        (pl.col("height") < 1.7).alias("short?"),
    ).agg(pl.col("name"))
    
    print(result)
    shape: (3, 3)
    ┌────────┬────────┬─────────────────────────────────┐
    │ decade ┆ short? ┆ name                            │
    │ ---    ┆ ---    ┆ ---                             │
    │ i32    ┆ bool   ┆ list[str]                       │
    ╞════════╪════════╪═════════════════════════════════╡
    │ 1980   ┆ true   ┆ ["Chloe Cooper"]                │
    │ 1980   ┆ false  ┆ ["Ben Brown", "Daniel Donovan"… │
    │ 1990   ┆ true   ┆ ["Alice Archer"]                │
    └────────┴────────┴─────────────────────────────────┘

    El dataframe resultant, després d’aplicar expressions d’agregació, conté una columna per a cada expressió d’agrupació a l’esquerra i tantes columnes com calgui per representar els resultats de les expressions d’agregació.

    Al seu torn, pots especificar tantes expressions d’agregació com vulguis:

    result = df.group_by(
        (pl.col("birthdate").dt.year() // 10 * 10).alias("decade"),
        (pl.col("height") < 1.7).alias("short?"),
    ).agg(
        pl.len(),
        pl.col("height").max().alias("tallest"),
        pl.col("weight", "height").mean().name.prefix("avg_"),
    )
    print(result)
    shape: (3, 6)
    ┌────────┬────────┬─────┬─────────┬────────────┬────────────┐
    │ decade ┆ short? ┆ len ┆ tallest ┆ avg_weight ┆ avg_height │
    │ ---    ┆ ---    ┆ --- ┆ ---     ┆ ---        ┆ ---        │
    │ i32    ┆ bool   ┆ u32 ┆ f64     ┆ f64        ┆ f64        │
    ╞════════╪════════╪═════╪═════════╪════════════╪════════════╡
    │ 1980   ┆ true   ┆ 1   ┆ 1.65    ┆ 53.6       ┆ 1.65       │
    │ 1990   ┆ true   ┆ 1   ┆ 1.56    ┆ 57.9       ┆ 1.56       │
    │ 1980   ┆ false  ┆ 2   ┆ 1.77    ┆ 77.8       ┆ 1.76       │
    └────────┴────────┴─────┴─────────┴────────────┴────────────┘

    Expansió d’expressions

    __PENDENT moure a select

    L’últim exemple contenia dues expressions d’agrupació i tres expressions d’agregació, així i tot, el dataframe resultant contenia sis columnes en comptes de cinc.

    Si hi parem atenció, l’última expressió d’agregació esmentava dues columnes diferents: “weight” i “height”.

    Les expressions de Polars admeten una funcionalitat anomenada expansió d’expressions.

    L’expansió d’expressions és com una notació abreujada per quant vols aplicar la mateixa transformació a múltiples columnes.

    Com has vist, l’expressió

    pl.col("weight", "height").mean().name.prefix("avg_")

    calcula el valor mitjà de les columnes “weight” i “height” i les reanomena “avg_weight” i “avg_height”, respectivament.

    [
        pl.col("weight").mean().alias("avg_weight"),
        pl.col("height").mean().alias("avg_height"),
    ]

    En aquest cas, aquesta expressió s’expandeix en dues expressions independents que Polars pot executar en paral·lel.

    En altres casos, pot ser que no sapigues per endavant en quantes expressions independents es desplegarà una expressió.

    Considera aquest exemple simple però il·lustratiu:

    (pl.col(pl.Float64) * 1.1).name.suffix("*1.1")

    Aquesta expressió multiplicarà totes les columnes amb tipus de dada Float64 per 1.1.

    El nombre de columnes a les quals s’aplica depèn de l’esquema de cada dataframe.

    En el cas del dataframe que has estat utilitzant, s’aplica a dues columnes:

    expr = (pl.col(pl.Float64) * 1.1).name.suffix("*1.1")
    result = df.select(expr)
    print(result)
    shape: (4, 2)
    ┌────────────┬────────────┐
    │ weight*1.1 ┆ height*1.1 │
    │ ---        ┆ ---        │
    │ f64        ┆ f64        │
    ╞════════════╪════════════╡
    │ 63.69      ┆ 1.716      │
    │ 79.75      ┆ 1.947      │
    │ 58.96      ┆ 1.815      │
    │ 91.41      ┆ 1.925      │
    └────────────┴────────────┘

    En el cas del dataframe df2 següent, la mateixa expressió s’expandeix a 0 columnes perquè cap columna té el tipus de dada Float64:

    df2 = pl.DataFrame(
        {
            "ints": [1, 2, 3, 4],
            "letters": ["A", "B", "C", "D"],
        }
    )
    result = df2.select(expr)
    print(result)
    shape: (0, 0)
    ┌┐
    ╞╡
    └┘

    API Lazy

    Polars admet dos modes d’operació: lazy i eager.

    Fins ara, els exemples han utilitzat l’API eager, en què la consulta s’executa immediatament.

    A l’API lazy, la consulta només s’avalua quan es fa la col·lecció dels resultats (collect).

    Ajornar l’execució fins a l’últim moment pot oferir avantatges de rendiment significatius i per això l’API lazy és la preferida en la majoria de casos.

    En general, s’hauria de preferir l’API “lazy”, tret que t’interessin els resultats intermedis o estiguis fent treball exploratori i encara no sàpigues com serà la teva consulta.

    Vegem-ho amb un exemple:

    data_uri = "https://gitlab.com/xtec/python/data/-/raw/main/iris.csv"
    
    df = pl.read_csv(data_uri)
    df_small = df.filter(pl.col("sepal_length") > 5)
    df_agg = df_small.group_by("species").agg(pl.col("sepal_width").mean())
    
    print(df_agg)
    shape: (3, 2)
    ┌────────────┬─────────────┐
    │ species    ┆ sepal_width │
    │ ---        ┆ ---         │
    │ str        ┆ f64         │
    ╞════════════╪═════════════╡
    │ Setosa     ┆ 3.713636    │
    │ Versicolor ┆ 2.804255    │
    │ Virginica  ┆ 2.983673    │
    └────────────┴─────────────┘

    En aquest exemple fas servir l’API eager per:

    1. Llegir el dataset iris.
    2. Filtrar el conjunt de dades segons la longitud del sèpal.
    3. Calcular la mitjana de l’amplada del sèpal per espècie.

    Cada pas s’executa immediatament retornant resultats intermedis.

    Això pot ser força ineficient, ja que podem estar fent feina o carregant dades addicionals que no s’acabaran utilitzant.

    Si en comptes d’això utilitzes l’API “lazy” i esperes a executar fins que estiguin definits tots els passos, el planificador de consultes pot aplicar diverses optimitzacions.

    En aquest cas:

    • Predicate pushdown: aplicar els filtres tan aviat com sigui possible mentre es llegeix el conjunt de dades, de manera que només es llegeixin files amb longitud de sèpal superior a 5.

    • Projection pushdown: seleccionar només les columnes necessàries mentre es llegeix el conjunt de dades, evitant carregar columnes addicionals (p. ex., llargada i amplada del pètal).

    q = (
        pl.scan_csv(data_uri)
        .filter(pl.col("sepal_length") > 5)
        .group_by("species")
        .agg(pl.col("sepal_width").mean())
    )
    
    df = q.collect()

    Això redueix significativament la càrrega de memòria i CPU, cosa que permet encaixar conjunts de dades més grans a memòria i processar-los més ràpid. Un cop definida la consulta, crides collect per indicar a Polars que la vols executar.

    Pots aprendre més sobre l’API “lazy” al seu capítol dedicat: https://docs.pola.rs/user-guide/lazy/.

    Previsualitza el pla de consulta

    Quan utilitzes l’API “lazy”, pots fer servir la funció explain per demanar a Polars que creï una descripció del pla de consulta que s’executarà quan col·leccionis els resultats.

    Això pot ser útil per veure quines optimitzacions aplica Polars a les teves consultes

    Podem demanar a Polars que expliqui la consulta q que has definit abans:

    print(q.explain())
    AGGREGATE
        [col("sepal_width").mean()] BY [col("species")] FROM
      Csv SCAN [docs/assets/data/iris.csv]
      PROJECT 3/5 COLUMNS
      SELECTION: [(col("sepal_length")) > (5.0)]

    De seguida veiem a l’explicació que Polars ha aplicat el predicate pushdown, ja que només llegeix files on la longitud del sèpal és superior a 5, i també ha aplicat el projection pushdown, ja que només llegeix les columnes necessàries per a la consulta.

    La funció explain també es pot utilitzar per veure com s’expandeixen les expressions en el context d’un esquema donat.

    Considera un exemple que hem explicat anteriorment:

    (pl.col(pl.Float64) * 1.1).name.suffix("*1.1")

    Pots fer servir explain per veure com s’avaluaria aquesta expressió contra un esquema arbitrari:

    schema = pl.Schema(
        {
            "int_1": pl.Int16,
            "int_2": pl.Int32,
            "float_1": pl.Float64,
            "float_2": pl.Float64,
            "float_3": pl.Float64,
        }
    )
    
    print(
        pl.LazyFrame(schema=schema)
        .select((pl.col(pl.Float64) * 1.1).name.suffix("*1.1"))
        .explain()
    )
     SELECT [[(col("float_1")) * (1.1)].alias("float_1*1.1"), [(col("float_2")) * (1.1)].alias("float_2*1.1"), [(col("float_3")) * (1.1)].alias("float_3*1.1")] FROM
      DF ["int_1", "int_2", "float_1", "float_2"]; PROJECT 3/5 COLUMNS; SELECTION: None

    Tycho

    Project Tycho té un dataset que 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 tycho-mini.zip (120 MB un cop descomprimit):

    from io import BytesIO
    from zipfile import ZipFile
    import os
    import polars as pl
    import urllib3
    import streamlit as st
    
    filepath = "data/tycho-mini.csv"
    
    if not os.path.isfile(filepath):
        load = st.text('Loading data...')
        resp = urllib3.request(
            "GET",
            "https://gitlab.com/xtec/python/data/-/raw/main/tycho-mini.zip",
        )
        zipfile = ZipFile(BytesIO(resp.data))
        zipfile.extractall(path="data")
        load.text('Loading data... done!')
    
    df = pl.scan_csv(filepath)

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

    epi_weekSetmana 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.
    countryPaís. En aquest dataset només hi ha mostres dels Estats Units, per tant podrem ometre-la. US
    stateSigles de l’estat dels EEUU.
    locNom complet de l’estat dels EEUU. Guardar state i loc (info redundant) només si volem visualitzar mapes.
    loc_typeEn el dataset pot ser CITY o STATE.
    diseaseEnfermetat. Entre [] ens indica informació addicional, que potser en el nostre estudi és necessària i ometre-la ens pot estalviar memòria del dataFrame.
    eventCada 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)
    numberImportant! Número de casos (si event=‘CASES’) o número de morts (si event=‘DEATHS’)
    from_date to_dateDates 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.
    urlEl 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, cal remarcar que podem observar que el dataset està en Format Tidy:

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

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

    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.

    Activitat

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

    Us n’haurien de sortir 247.

    ┌──────────────────┐
    │ city            │
    ╞══════════════════╡
    │ OAKLAND          │
    │ ANN ARBOR        │
    │ BIDDEFORD        │
    │ SARATOGA SPRINGS │
    │ …                │
    │ BENNINGTON       │
    │ BRADDOCK         │
    │ CHARLOTTE        │
    │ CHICAGO          │
    └──────────────────┘
    Activitat

    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
    └────────────────┴────────┘
    Activitat

    Mostra el número de morts per la tuberculosi, a Nova York, l’any 1910: 32403

    Activitat
    1. Elimina les columnes country i url
    2. Reanomena evnt a event
    3. Neteja les malalties eliminant els noms entre claudàtors.
    4. Afegeix una nova columna anomenada year de tipus int amb l’any extret de l’epi_week.
    5. Selecciona les files on l’any és 1910 o 1911.
    6. Afegeix una nova columna anomenada id amb un identificador únic numèric que comenci des de 0
    7. Reanomena loc a city, number a deaths
    8. Reordena les columnes de la manera següent: [‘id’, ‘year’, ‘epi_week’, ‘from_date’, ‘to_date’, ‘state’, ‘city’, ‘disease’, ‘deaths’]

    Electric Vehicle Population

    Treballaràs amb dades de població de vehicles elèctrics.

    Aquest conjunt de dades conté informació sobre vehicles elèctrics i híbrids registrats al Departament de Llicències de l’Estat de Washington.

    Cada fila de les dades representa un cotxe, i cada columna conté informació sobre el cotxe.

    El conjunt de dades està en format CSV, i pots accedir-hi mitjançant l’enllaç següent: https://data.wa.gov/api/views/f6w7-q2d2/rows.csv?accessType=DOWNLOAD

    filepath = "data/electric-cars.csv"
    
    if not os.path.isfile(filepath):
        load = st.write("Downloading data")
        response = urllib3.request(
            "GET",
            "https://data.wa.gov/api/views/f6w7-q2d2/rows.csv?accessType=DOWNLOAD",
            preload_content=False
        )
    
        total_size = int(response.headers.get('Content-Length', 0))
        downloaded = 0
    
        progress_bar = st.progress(0)
    
        with open(filepath, "wb") as out:
            for chunk in response.stream(4096):
                out.write(chunk)
                downloaded += len(chunk)
                if total_size > 0:
                    progress_bar.progress(downloaded / total_size)
    
        response.release_conn()
        progress_bar.empty()
    
    df = pl.scan_csv(filepath)

    Pots executar la consulta següent:

    query = (
        df.filter((pl.col("Model Year") >= 2018))
        .filter(pl.col("Electric Vehicle Type") == "Battery Electric Vehicle (BEV)")
        .group_by(["State", "Make"])
        .agg(
            pl.mean("Electric Range").alias("Average Electric Range"),
            pl.min("Model Year").alias("Oldest Model Year"),
            pl.len().alias("Number of Cars"),
        )
        .filter(pl.col("Average Electric Range") > 0)
        .filter(pl.col("Number of Cars") > 5)
        .sort(pl.col("Number of Cars"), descending=True)
    )
    
    print(query.collect())

    En aquesta consulta:

    • Filtres les dades de tots els cotxes on l’any del model és 2018 o posterior i el tipus de vehicle elèctric és Vehicle Elèctric de Bateria (BEV).
    • A continuació calcules el rang elèctric mitjà, l’any del model més antic i el nombre de cotxes per a cada estat i marca.
    • Finalment, filtres més les dades on el rang elèctric mitjà és positiu i on el nombre de cotxes per a l’estat i la marca és superior a cinc.
    shape: (22, 5)
    ┌───────┬───────────┬────────────────────────┬───────────────────┬────────────────┐
    │ State ┆ Make      ┆ Average Electric Range ┆ Oldest Model Year ┆ Number of Cars │
    │ ---   ┆ ---       ┆ ---                    ┆ ---               ┆ ---            │
    │ str   ┆ str       ┆ f64                    ┆ i64               ┆ u32            │
    ╞═══════╪═══════════╪════════════════════════╪═══════════════════╪════════════════╡
    │ WA    ┆ TESLA     ┆ 56.715893              ┆ 2018              ┆ 85408          │
    │ WA    ┆ CHEVROLET ┆ 88.73235               ┆ 2018              ┆ 8881           │
    │ WA    ┆ NISSAN    ┆ 67.008487              ┆ 2018              ┆ 7423           │
    │ WA    ┆ FORD      ┆ 0.083241               ┆ 2018              ┆ 7208           │
    │ WA    ┆ KIA       ┆ 35.681039              ┆ 2018              ┆ 6239           │
    │ …     ┆ …         ┆ …                      ┆ …                 ┆ …              │
    │ MD    ┆ TESLA     ┆ 33.733333              ┆ 2018              ┆ 15             │
    │ TX    ┆ TESLA     ┆ 105.785714             ┆ 2018              ┆ 14             │
    │ NC    ┆ TESLA     ┆ 16.538462              ┆ 2018              ┆ 13             │
    │ FL    ┆ TESLA     ┆ 63.875                 ┆ 2019              ┆ 8              │
    │ CO    ┆ TESLA     ┆ 35.833333              ┆ 2018              ┆ 6              │
    └───────┴───────────┴────────────────────────┴───────────────────┴────────────────┘

    Mapa de cultius

    Noticia

    Per la part pràctica has d’utilitzar un CSV que té les dades de l’any 2016 de tots els polígons dels cultius declarats en la declaració única agrària (DUN), declaració anual de l’explotació agrària, que de forma obligatòria ha de fer la persona titular que disposa de superfície productiva agrícola (excloses les de consum propi), i vol sol·licitar determinats ajuts o hagi de realitzar determinats tràmits amb el DARP.

    Aquestes dades s’obté fusionant els recintes SIGPAC d’un mateix producte/conreu declarat en la DUN, i el mateix sistema d’explotació (secà/regadiu) per a cada municipi. És un fitxer CSV de 958 MB que has de descarregar mitjançant codi (i no ha de formar part de l’entrega!)

    En aquest enllaç tens les dades que necessites: Mapa de cultivos de Cataluña con origen DUN 2016

    Has de crear una aplicació que mostri diferents dades dels cultius utilitzant els gràfics corresponents.