À quoi ça sert

Polars résout le problème classique de pandas : dès qu'un fichier dépasse quelques millions de lignes, pandas devient lent et consomme énormément de mémoire (tout est chargé d'un coup, sur un seul cœur CPU). Polars fait le même genre d'opérations — charger, filtrer, grouper, joindre — mais en utilisant tous les cœurs de la machine et un moteur optimisé écrit en Rust.

Polars apporte aussi un mode lazy : tu décris ce que tu veux faire, et Polars planifie l'exécution complète avant de la lancer. Résultat : il peut supprimer les colonnes inutiles, réordonner les opérations, et même traiter un fichier qui ne tient pas en RAM (streaming).

  • API expressions — au lieu de df["col"] * 2, on écrit pl.col("col") * 2. Plus verbeux, mais Polars peut optimiser et paralléliser.
  • Lazy vs Eagerscan_csv ne charge rien tant que tu n'appelles pas .collect(). read_csv charge tout de suite, comme pandas.
  • Lecture rapide — Parquet, CSV, JSON, Arrow, Excel.
  • Compatible Arrow — partage la mémoire avec DuckDB, PySpark, etc. sans copie.
Polars, pandas, PySpark… lequel choisir ?

pandas reste l'écosystème le plus mature (intégré partout : scikit-learn, matplotlib, etc.) et suffit pour des fichiers de quelques Go. Polars est imbattable sur une seule machine quand les données font 1 à 100 Go : souvent 5 à 30× plus rapide. PySpark ne devient utile que sur un vrai cluster, quand les données dépassent ce qu'une seule grosse machine peut gérer.

Un exemple d'usage

Même cas que la fiche pandas — un CSV de ventes — pour pouvoir comparer. On veut les 10 produits qui rapportent le plus :

python
import polars as pl

# Lecture lazy : rien n'est chargé tout de suite
totaux = (
    pl.scan_csv("ventes.csv")
      .filter(pl.col("montant") > 100)
      .group_by("produit")
      .agg(pl.col("montant").sum().alias("total"))
      .sort("total", descending=True)
      .head(10)
      .collect()           # déclenche l'exécution
)
print(totaux)

Tout est chaîné dans une seule expression lisible de haut en bas. Polars regarde le plan complet avant d'exécuter : il sait qu'on n'a besoin que des colonnes produit et montant, donc il ignore les autres dès la lecture du CSV. C'est ce qu'on appelle predicate pushdown et projection pushdown.

How-to : installer et utiliser Polars

  1. Installer Polars dans ton projet

    Comme pour pandas, Polars est une dépendance principale (cf. UV) :

    bash
    uv add polars

    Si tu veux la lecture Excel ou le format Delta, des extras sont disponibles :

    bash
    uv add "polars[excel,deltalake]"
  2. Charger un fichier (eager vs lazy)

    Deux familles de fonctions selon que tu veux exécuter tout de suite (read_*) ou différer (scan_*) :

    python
    import polars as pl
    
    # Eager : renvoie un DataFrame, lit tout en RAM
    df = pl.read_csv("data.csv")
    df = pl.read_parquet("data.parquet")
    df = pl.read_excel("data.xlsx")
    
    # Lazy : renvoie un LazyFrame, ne lit rien tant que .collect()
    lf = pl.scan_csv("data.csv")
    lf = pl.scan_parquet("data.parquet")
    Quand utiliser lazy ?

    À peu près tout le temps en pipeline de données. L'eager est utile pour de l'exploration rapide en Jupyter, quand on veut voir le résultat ligne par ligne. En production (script ETL, batch), préfère le lazy : moins de mémoire, plus rapide, et passe à l'échelle.

  3. Explorer un DataFrame

    python
    df.head()           # 5 premières lignes
    df.tail(10)         # 10 dernières
    df.shape             # (nb_lignes, nb_colonnes)
    df.columns           # noms des colonnes
    df.dtypes            # types Polars (Int64, Utf8, Float64…)
    df.describe()       # stats sur les colonnes numériques
    df.null_count()     # nombre de null par colonne
    df.schema            # dict {colonne: type}
  4. Sélectionner et filtrer avec des expressions

    C'est la différence syntaxique majeure avec pandas. En Polars, on manipule des expressions (pl.col(...)) à l'intérieur de méthodes comme select, filter, with_columns :

    python
    # Sélectionner des colonnes
    df.select(["produit", "montant"])
    df.select(pl.col("^.*_id$"))   # par regex
    
    # Filtrer (équivalent du df[df["x"] > 0] de pandas)
    df.filter(pl.col("montant") > 100)
    
    # Conditions multiples — pas besoin de parenthèses pénibles
    df.filter(
        (pl.col("montant") > 100) &
        (pl.col("produit") == "A")
    )
  5. Créer et modifier des colonnes

    Polars ne modifie jamais un DataFrame en place : chaque opération renvoie un nouveau DataFrame. with_columns sert à ajouter ou remplacer des colonnes.

    python
    # Ajouter une colonne calculée
    df = df.with_columns(
        (pl.col("montant") * 1.20).alias("ttc")
    )
    
    # Plusieurs colonnes d'un coup
    df = df.with_columns([
        (pl.col("montant") * 1.20).alias("ttc"),
        pl.col("produit").str.to_uppercase().alias("produit_maj"),
    ])
    
    # Renommer
    df = df.rename({"montant": "montant_ht"})
    
    # Supprimer
    df = df.drop("colonne_inutile")
  6. Grouper et agréger (group_by)

    La syntaxe est plus uniforme qu'en pandas : on passe toujours des expressions dans agg().

    python
    # Total des ventes par produit
    df.group_by("produit").agg(
        pl.col("montant").sum()
    )
    
    # Plusieurs agrégations en même temps
    df.group_by("produit").agg([
        pl.col("montant").sum().alias("total"),
        pl.col("montant").mean().alias("moyenne"),
        pl.col("montant").count().alias("nb_ventes"),
    ])
    
    # Grouper par plusieurs colonnes
    df.group_by(["region", "produit"]).agg(
        pl.col("montant").sum()
    )
  7. Gérer les valeurs manquantes (null)

    Polars utilise null (et pas NaN comme pandas) pour les valeurs manquantes — c'est plus propre, surtout pour les chaînes et les dates.

    python
    # Compter les nulls
    df.null_count()
    
    # Supprimer les lignes avec des null
    df.drop_nulls()
    
    # Remplir les nulls
    df.with_columns(
        pl.col("montant").fill_null(0)
    )
    
    # Remplir par la moyenne de la colonne
    df.with_columns(
        pl.col("age").fill_null(pl.col("age").mean())
    )
  8. Sauvegarder le résultat

    python
    df.write_csv("resultat.csv")
    df.write_parquet("resultat.parquet")
    df.write_json("resultat.json")

    Note la convention write_* (et pas to_* comme pandas) — un petit piège pour qui jongle entre les deux. À chaque doute : lecture = read_ ou scan_, écriture = write_.

Aide-mémoire

python (lecture / écriture)
pl.read_csv("f.csv", separator=";")
pl.read_parquet("f.parquet")
pl.scan_csv("f.csv")                # lazy
df.write_csv("out.csv")
python (exploration)
df.head(), df.tail(), df.shape, df.columns, df.dtypes
df.describe(), df.null_count(), df.schema
df["col"].value_counts()
df["col"].unique()
df.sort("col", descending=True)
python (transformations)
df.filter(pl.col("x") > 0)
df.with_columns((pl.col("a") + pl.col("b")).alias("c"))
df.group_by("cat").agg(pl.col("val").sum())
df.join(other, on="id", how="left")
pl.concat([df1, df2])
df.pivot(index="r", on="c", values="v")
python (dates)
df.with_columns(
    pl.col("date").str.to_date("%Y-%m-%d")
)
df.with_columns([
    pl.col("date").dt.year().alias("annee"),
    pl.col("date").dt.month().alias("mois"),
])
python (lazy)
lf = pl.scan_parquet("big.parquet")
result = (
    lf.filter(pl.col("x") > 0)
      .group_by("cat")
      .agg(pl.col("val").sum())
      .collect()                # exécute le plan
)
# Inspecter le plan optimisé sans l'exécuter
print(lf.explain())

Polars et le reste de l'écosystème

Polars se branche naturellement aux outils que tu utilises déjà — il est conçu pour cohabiter avec eux, pas pour les remplacer en bloc.

  • pandas — conversion immédiate dans les deux sens : df.to_pandas() et pl.from_pandas(df_pd). Utile quand une lib (scikit-learn, matplotlib) n'accepte que des DataFrames pandas.
  • PySpark — même philosophie de plan logique optimisé et d'opérations lazy, mais Polars tient sur une machine. La règle d'usage : Polars jusqu'à ~100 Go sur une grosse VM, PySpark au-delà sur un cluster.
  • Jupyter — l'affichage par défaut d'un DataFrame Polars est plus compact que pandas, avec les types affichés en en-tête. Idéal pour l'exploration.
  • MinIO — Polars lit directement depuis S3/MinIO via scan_parquet("s3://bucket/file.parquet", storage_options={...}). Pas besoin de télécharger d'abord.
  • MLflow — pour l'entraînement, on convertit souvent en pandas juste avant de passer le dataset à scikit-learn. Polars sert au pipeline de préparation (où la rapidité compte), pandas à l'interface modèle.
Le piège du passage pandas → Polars

Quand on migre du code pandas, on a tendance à écrire df["col"] et à enchaîner les modifications en place. Polars refuse : il faut passer par des expressions (pl.col("col")) à l'intérieur de select/filter/with_columns. C'est plus verbeux au début, mais c'est ce qui permet à Polars d'optimiser et de paralléliser.

Pour aller plus loin