Type to search…

DataFrame

Un DataFrame és un conjunt de sèries.

Introducció

Encara que un Dataframe i una Taula d’una base de dades poden semblar el mateix, la seva disposició en memòria és completament diferent perquè tenen proposits d’ús diferent.

Taula

Una taula d’una base de dades és una agrupació de files, on cada fila és un conjunt de dades relacionades.

En una base de dades la unitat de treball són les files, i les tasques habituals són afegir files, actualitzar les files, eliminar les files, localitzar files, etc.

La memòria de l’ordinador no és bidimensional.

Les files es disposen una darrere a l’altre en memòria:

Per calcular la mitja dels valors de la columna height de la taula, cal recorrer tota la memòria on està emmagatzemada la taula.

L’operació més habitual és buscar files, i per tal que aquesta operació sigui eficient, s’utilitzen índexs que permeten localitzar les files en un temps constant.

El problema és que els índexs ocupen molta memòria!

DataFrame

Un dataframe és un conjunt de Series (o columnes) encara que es presenti com una taula.

I en memòria es disposen en columnes.

Per calcular la mitja dels valors de la sèrie height només cal recorrer una part mínima de la memòria on està emmagatzemant el dataframe.

DataFrame

El següent fragment mostra com crear un dataframe a partir d’un diccionari de llistes:

python
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

A Series vas veure que pots operar amb diferents sèries de dades de diferents maneres.

Amb un dataframe pots fer el mateix, l’únic que has de fer és seleccionar les columnes (sèries) que vols utilitzar amba en el context select:

Per exemple, pots seleccionar la columna birthdate i obtenir l’any:

python
years = df.select(pl.col("birthdate").dt.year())
print(years)
shape: (4, 1)
┌───────────┐
│ birthdate │
│ ---       │
│ i32       │
╞═══════════╡
│ 1997      │
│ 1985      │
│ 1983      │
│ 1981      │
└───────────┘

El resultat és un dataframe en qué només està la columna que has seleccionat amb les operacions que has fet en aquella columna.

Naturalment, pots seleccionar tantes columnes com vulguis, i no cal que facis res amb elles.

Per exemple, pots seleccionar també el nom de les persones per no perdre aquesta informació en el nou dataframe:

python
df = df.select(pl.col("name"), pl.col("birthdate").dt.year())
print(df.head(1))
shape: (1, 2)
┌──────────────┬───────────┐
│ name         ┆ birthdate │
│ ---          ┆ ---       │
│ str          ┆ i32       │
╞══════════════╪═══════════╡
│ Alice Archer ┆ 1997      │
└──────────────┴───────────┘
Task

Enlloc de l’any de naixement volem tenir l’edat:

Show solution
python
df = df.select(
    pl.col("name"),
    (date.today().year - pl.col("birthdate").dt.year()).alias("age"),
)
print(df)
shape: (4, 2)
┌────────────────┬─────┐
│ name           ┆ age │
│ ---            ┆ --- │
│ str            ┆ i32 │
╞════════════════╪═════╡
│ Alice Archer   ┆ 28  │
│ Ben Brown      ┆ 40  │
│ Chloe Cooper   ┆ 42  │
│ Daniel Donovan ┆ 44  │
└────────────────┴─────┘
Task

Calcula l’índex de massa corporal (BMI) de cada persona.

El bmi és el valor de la columna weight dividida per la columna height al quadrat:

Show solution
python
df = df.select(
    pl.col("name"),
    (pl.col("weight") / (pl.col("height") ** 2)).alias("bmi"),
)
print(df)
shape: (4, 2)
┌────────────────┬───────────┐
│ name           ┆ bmi       │
│ ---            ┆ ---       │
│ str            ┆ f64       │
╞════════════════╪═══════════╡
│ Alice Archer   ┆ 23.791913 │
│ Ben Brown      ┆ 23.141498 │
│ Chloe Cooper   ┆ 19.687787 │
│ Daniel Donovan ┆ 27.134694 │
└────────────────┴───────────┘
Task

A continuació tens un dataframe amb les notes dels alumnes de l’assignatura de matemàtiques:

python
df = pl.DataFrame(
    {
        "name": ["David", "Maria", "Sandra", "Jordi"],
        "algebra": [9, 5, 10, 9],
        "calculus": [10, 9, 7, 8],
        "probability": [10, 5, None, 4]
    }
)

Calcula la nota final (tots els parcials tenen el mateix pes):

Show solution
python
result = df.select(
    pl.col("name"),
    (pl.col("algebra") + pl.col("calculus") + pl.col("probability")) / 3
)
print(result)
shape: (4, 2)
┌────────┬──────────┐
│ name   ┆ algebra  │
│ ---    ┆ ---      │
│ str    ┆ f64      │
╞════════╪══════════╡
│ David  ┆ 9.666667 │
│ Maria  ┆ 6.333333 │
│ Sandra ┆ null     │
│ Jordi  ┆ 7.0      │
└────────┴──────────┘

Esquema

L’esquema d’un dataframe està format per l’associació entre els noms de cada sèrie (o columna) i els tipus de dades d’aquestes mateixes sèries (o columnes).

Igual que amb les Series, Polars infereix l’esquema d’un dataframe quan el crees:

python
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)
    }
)

print(df.schema)
Schema({'name': String, 'birthdate': Date, 'weight': Float64, 'height': Float64})

Si vols, i en molts casos ho has de fer, pots especificar el tipus de dades de cada columna.

De la mateixa manera que amb una Series pots sobreescriure el tipus que utilitza Polars mitjançant un diccionari que mapi els noms de columna als tipus de dades.

Pots usar el valor None si no vols sobreescriure la inferència per a una columna determinada.

python
df = pl.DataFrame(
    {
        "name": ["Alice", "Ben", "Chloe", "Daniel"],
        "age": [27, 39, 41, 43],
    },
    schema={"name": None, "age": pl.UInt8},
)

print(df)
shape: (4, 2)
┌────────┬─────┐
│ name   ┆ age │
│ ---    ┆ --- │
│ str    ┆ u8  │
╞════════╪═════╡
│ Alice  ┆ 27  │
│ Ben    ┆ 39  │
│ Chloe  ┆ 41  │
│ Daniel ┆ 43  │
└────────┴─────┘

Si només necessites sobreescriure la inferència d’algunes columnes, el paràmetre schema_overrides sol ser més convenient perquè et permet ometre les columnes per a les quals no vols sobreescriure la inferència:

python
df = pl.DataFrame(
    {
        "name": ["Alice", "Ben", "Chloe", "Daniel"],
        "age": [27, 39, 41, 43],
    },
    schema_overrides={"age": pl.UInt8},
)

print(df)
shape: (4, 2)
┌────────┬─────┐
│ name   ┆ age │
│ ---    ┆ --- │
│ str    ┆ u8  │
╞════════╪═════╡
│ Alice  ┆ 27  │
│ Ben    ┆ 39  │
│ Chloe  ┆ 41  │
│ Daniel ┆ 43  │
└────────┴─────┘

CSV

Un dels formats de text més populars és csv, que significa valors separats per comes.*

Aquest format pot emmagatzemar dades tabulars: cada fila d’un fitxer representa una fila d’una taula, i els valors corresponents a diferents columnes estan separats per comes.

read_csv

Suposem que tens el fitxer students.csv que emmagatzema les dades d’uns estudiants:

csv
First Name,Family Name,Age
Anna,Smith,21
Bob,Jones,20
Maria,Williams,25
Jack,Brown,22

Per moure les dades dels estudiants a un DataFrame, pots utilitzar la funció read_csv()

python
df = pl.read_csv('students.csv')
print(df.schema)
Schema({'First Name': String, 'Family Name': String, 'Age': Int64})

La funció read_csv té diversos paràmetres amb valors per defecte.

Un dels més importants és separator, que indica el delimitador que s’utilitza per separar els camps és ,.

Et pots trobar un csv que utilitza un delimitador diferent, per exemple ;.

Task

Torna a carregar el fitxer students.csv:

  • Només amb les columnes First Name i Age
  • La columna First Name ha de tenir el nom Name:
  • La columna Age ha de ser de tipus UInt8:
Show solution
python
df = pl.read_csv(
    'students.csv',
    columns= ["First Name", "Age"],
    new_columns=["Name", "Age"],
    schema_overrides={"Age": pl.UInt8}
)
print(df)
shape: (4, 2)
┌───────┬─────┐
│ Name  ┆ Age │
│ ---   ┆ --- │
│ str   ┆ u8  │
╞═══════╪═════╡
│ Anna  ┆ 21  │
│ Bob   ┆ 20  │
│ Maria ┆ 25  │
│ Jack  ┆ 22  │
└───────┴─────┘
Task

Crea el fitxer cars.csv:

csv
Car Name;Price;Condition;Year;Fuel Type
Honda Civic;22000;Used;2021;Gasoline
Ford Mustang;35000;New;2023;Gasoline
Chevrolet Camaro;40000.5;Used;2020;Gasoline
Tesla Model 3;50000;New;2023;Electric
BMW X5;60000;Used;2022;Gasoline
Audi A4;30000;New;2023;Diesel
Toyota Corolla;24999.9;New;2023;Gasoline

Carrega les dades especificant els tipus de les columnes :

Show solution
python
df = pl.read_csv(
    'cars.csv',
    separator=';',
    schema_overrides={
        'Price': pl.Float64,
        'Condition': pl.Enum(["Used", "New"]),
        'Year': pl.UInt16,
        'Fuel Type': pl.Enum(["Gasoline", "Diesel", "Electric"]),
    }
)
print(df)
shape: (7, 5)
┌──────────────────┬─────────┬───────────┬──────┬───────────┐
│ Car Name         ┆ Price   ┆ Condition ┆ Year ┆ Fuel Type │
│ ---              ┆ ---     ┆ ---       ┆ ---  ┆ ---       │
│ str              ┆ f64     ┆ enum      ┆ u16  ┆ enum      │
╞══════════════════╪═════════╪═══════════╪══════╪═══════════╡
│ Honda Civic      ┆ 22000.0 ┆ Used      ┆ 2021 ┆ Gasoline  │
│ Ford Mustang     ┆ 35000.0 ┆ New       ┆ 2023 ┆ Gasoline  │
│ Chevrolet Camaro ┆ 40000.5 ┆ Used      ┆ 2020 ┆ Gasoline  │
│ Tesla Model 3    ┆ 50000.0 ┆ New       ┆ 2023 ┆ Electric  │
│ BMW X5           ┆ 60000.0 ┆ Used      ┆ 2022 ┆ Gasoline  │
│ Audi A4          ┆ 30000.0 ┆ New       ┆ 2023 ┆ Diesel    │
│ Toyota Corolla   ┆ 24999.9 ┆ New       ┆ 2023 ┆ Gasoline  │
└──────────────────┴─────────┴───────────┴──────┴───────────┘

Temporal types

It’s important to understand that dates and times can be represented in various formats. Here are a few common string date formats:

  • YYYY-MM-DD (e.g., “2024-09-05”)
  • DD/MM/YYYY (e.g., “05/09/2024”)
  • YYYY-MM-DD HH:MM:SS (e.g., “2024-09-05 14:30:00”)

write_csv

python
import polars as pl
import datetime as dt

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

En l’exemple següent escrivim el dataframe a un fitxer csv anomenat output.csv.

Després, el tornem a llegir utilitzant read_csv i tot seguit n’imprimim el resultat per a la seva inspecció.

python
df.write_csv("data/output.csv")
df_csv = pl.read_csv("data/output.csv", try_parse_dates=True)
print(df_csv)

Polars et permet escanejar una entrada CSV. L’escaneig retarda l’anàlisi real del fitxer i, en lloc d’això, retorna un contenidor de càlcul “lazy” anomenat LazyFrame.

python
df = pl.scan_csv("data/output.csv")

Si vols saber per què això és desitjable, pots llegir més sobre aquestes optimitzacions de Polars aquí: [Lazy-API](https://docs.pola.rs/user-guide/concepts/lazy-api/](https://docs.pola.rs/user-guide/concepts/lazy-api/).

Inspeccionar

PENDENT amb un DF gran

A continuació veurem alguns mètodes útils per inspeccionar ràpidament un dataframe.

La funció head mostra les primeres files d’un dataframe.

Per defecte, obtens les primeres 5 files, però també pots especificar el nombre de files que vols:

python
print(df.head(2))
shape: (2, 4)
┌───────┬─────────┬──────────┬─────────────┐
│ name  ┆ algebra ┆ calculus ┆ probability │
│ ---   ┆ ---     ┆ ---      ┆ ---         │
│ str   ┆ i64     ┆ i64      ┆ i64         │
╞═══════╪═════════╪══════════╪═════════════╡
│ David ┆ 9       ┆ 10       ┆ 10          │
│ Maria ┆ 5       ┆ 9        ┆ 5           │
└───────┴─────────┴──────────┴─────────────┘

glimpse

La funció glimpse és una altra funció que mostra els valors de les primeres files d’un dataframe, però formata la sortida de manera diferent que head.

Aquí, cada línia de la sortida correspon a una única columna, fent que sigui més fàcil inspeccionar dataframes més amples:

python
print(df.glimpse(return_as_string=True))
Rows: 4
Columns: 4
$ name        <str> 'David', 'Maria', 'Sandra', 'Jordi'
$ algebra     <i64> 9, 5, 10, 9
$ calculus    <i64> 10, 9, 7, 8
$ probability <i64> 10, 5, None, 4

tail

La funció tail mostra les últimes files d’un dataframe.

Per defecte, obtens les últimes 5 files, però també pots especificar el nombre de files que vols, de manera similar a com funciona head:

python
print(df.tail(3))
shape: (2, 4)
┌────────┬─────────┬──────────┬─────────────┐
│ name   ┆ algebra ┆ calculus ┆ probability │
│ ---    ┆ ---     ┆ ---      ┆ ---         │
│ str    ┆ i64     ┆ i64      ┆ i64         │
╞════════╪═════════╪══════════╪═════════════╡
│ Sandra ┆ 10      ┆ 7        ┆ null        │
│ Jordi  ┆ 9       ┆ 8        ┆ 4           │
└────────┴─────────┴──────────┴─────────────┘

sample

Si creus que les primeres o últimes files del teu dataframe no són representatives de les teves dades, pots utilitzar sample per obtenir un nombre arbitrari de files seleccionades aleatòriament del dataframe.

Tingues en compte que les files no necessàriament es retornen en el mateix ordre en què apareixen al dataframe:

python
import random

random.seed(42)  # For reproducibility.

print(df.sample(2))
shape: (2, 4)
┌──────────────┬────────────┬────────┬────────┐
│ name         ┆ birthdate  ┆ weight ┆ height │
│ ---          ┆ ---        ┆ ---    ┆ ---    │
│ str          ┆ date       ┆ f64    ┆ f64    │
╞══════════════╪════════════╪════════╪════════╡
│ Alice Archer ┆ 1997-01-10 ┆ 57.9   ┆ 1.56   │
│ Ben Brown    ┆ 1985-02-15 ┆ 72.5   ┆ 1.77   │
└──────────────┴────────────┴────────┴────────┘

describe

També pots utilitzar describe per calcular estadístiques resum per a totes les columnes del teu DataFrame:

python
print(df.describe())
shape: (9, 5)
┌────────────┬────────────────┬─────────────────────┬───────────┬──────────┐
│ statistic  ┆ name           ┆ birthdate           ┆ weight    ┆ height   │
│ ---        ┆ ---            ┆ ---                 ┆ ---       ┆ ---      │
│ str        ┆ str            ┆ str                 ┆ f64       ┆ f64      │
╞════════════╪════════════════╪═════════════════════╪═══════════╪══════════╡
│ count      ┆ 4              ┆ 4                   ┆ 4.0       ┆ 4.0      │
│ null_count ┆ 0              ┆ 0                   ┆ 0.0       ┆ 0.0      │
│ mean       ┆ null           ┆ 1986-09-04 00:00:00 ┆ 66.775    ┆ 1.6825   │
│ std        ┆ null           ┆ null                ┆ 13.560082 ┆ 0.097082 │
│ min        ┆ Alice Archer   ┆ 1981-04-30          ┆ 53.6      ┆ 1.56     │
│ 25%        ┆ null           ┆ 1983-03-22          ┆ 57.9      ┆ 1.65     │
│ 50%        ┆ null           ┆ 1985-02-15          ┆ 72.5      ┆ 1.75     │
│ 75%        ┆ null           ┆ 1985-02-15          ┆ 72.5      ┆ 1.75     │
│ max        ┆ Daniel Donovan ┆ 1997-01-10          ┆ 83.1      ┆ 1.77     │
└────────────┴────────────────┴─────────────────────┴───────────┴──────────┘

Parquet

Apache Parquet és un format de fitxer de dades de codi obert, orientat a columnes, dissenyat per a l’emmagatzematge i la recuperació eficients de dades. Proporciona esquemes de compressió i codificació d’alt rendiment per gestionar dades complexes a gran escala i és compatible amb molts llenguatges de programació i eines analítiques.

Carregar o escriure fitxers Parquet és molt ràpid, ja que la disposició de les dades en un DataFrame de Polars a memòria reflecteix en molts aspectes la disposició d’un fitxer Parquet en disc.

A diferència del CSV, Parquet és un format columnar. Això vol dir que les dades s’emmagatzemen per columnes en lloc de per files. És una manera més eficient d’emmagatzemar dades perquè permet una millor compressió i un accés més ràpid a les dades.

Pots llegir un fitxer Parquet en un DataFrame amb la funció read_parquet:

python
df = pl.read_parquet("docs/assets/data/path.parquet")

write_parquet s’utilitza per escriure un DataFrame en un fitxer Parquet:

python
df = pl.DataFrame({"foo": [1, 2, 3], "bar": [None, "bak", "baz"]})
df.write_parquet("docs/assets/data/path.parquet")

Polars et permet escanejar una entrada Parquet.

L’escaneig retarda l’anàlisi real del fitxer i, en lloc d’això, retorna un contenidor de càlcul “lazy” anomenat LazyFrame.

python
df = pl.scan_parquet("docs/assets/data/path.parquet")

Quan escanegem un fitxer Parquet emmagatzemat al núvol, també podem aplicar predicate i projection pushdowns. Això pot reduir significativament la quantitat de dades que cal descarregar.

Per a l’escaneig d’un fitxer Parquet al núvol, consulta Cloud storage.

JSON

Polars pot llegir i escriure tant JSON estàndard com JSON delimitat per noves línies (NDJSON).

La lectura d’un fitxer JSON hauria de ser familiar:

python
df = pl.read_json("docs/assets/data/path.json")

Els objectes JSON delimitats per noves línies es poden llegir a Polars d’una manera molt més eficient que el JSON estàndard.

Polars pot llegir un fitxer NDJSON en un DataFrame utilitzant la funció read_ndjson:

python
df = pl.read_ndjson("docs/assets/data/path.json")
python
df = pl.DataFrame({"foo": [1, 2, 3], "bar": [None, "bak", "baz"]})
df.write_json("docs/assets/data/path.json")
df.write_ndjson("docs/assets/data/path.json")

Polars et permet escanejar una entrada JSON només per a JSON delimitat per noves línies.

L’escaneig retarda l’anàlisi real del fitxer i, en lloc d’això, retorna un contenidor de càlcul mandrós “lazy” LazyFrame.

python
df = pl.scan_ndjson("docs/assets/data/path.json")

Arrow

Apache Arrow és un format columnar universal i una caixa d’eines multillenguatge per a l’intercanvi ràpid de dades i l’analítica en memòria.

El projecte especifica un format de memòria orientat a columnes, independent del llenguatge, per a dades planes i jeràrquiques, organitzat per a operacions analítiques eficients en maquinari modern. El projecte conté una col·lecció de biblioteques activament desenvolupades en molts llenguatges per resoldre problemes relacionats amb la transferència de dades i el processament analític en memòria. Això inclou aspectes com:

  • Moviment de dades amb memòria compartida sense còpies (zero-copy) i RPC
  • Lectura i escriptura de formats de fitxer (com CSV, Apache ORC i Apache Parquet)
  • Analítica en memòria i processament de consultes