Vous connaissez certainement Pandas, sorti pour la première fois en 2008. Il s'agit du framework de référence pour les dataframes conçu pour travailler essentiellement en monothread. Nous avons d'ailleurs un tutoriel sur Pandas.
Polars reprend le même métier mais avec des choix techniques différents : le framework est écrit en Rust et est multithread par défaut.
C'est quoi Polars ?
Si vous connaissez Pandas, Polars fait la même chose : manipuler des données en tableaux : lire un fichier, filtrer des lignes, faire des regroupements et des totaux. Rien de nouveau côté usage, donc. Comme on l'a vu, la grande différence c'est le multithread natif. Le framework a été créé par Ritchie Vink et compte plus de 39 000 étoiles sur GitHub.
Les performances de Polars s'expliquent par des choix techniques que vous pouvez retrouver sur la documentation :
-
Le format colonne d'Apache Arrow : les données d'une même colonne sont stockées côte à côte en mémoire
-
Le multithread par défaut
-
Un optimiseur de requêtes : en mode lazy, Polars lit d'abord tout votre enchaînement d'opérations, détermine la façon la plus efficace de l'exécuter, puis l'exécute
Si vous avez lu nos articles sur uv et ruff, vous reconnaîtrez le schéma : un outil Python historique réécrit en Rust.
Premiers pas avec Polars
C'est le moment de mettre les mains dans le cambouis !
Installation
Polars s'installe comme n'importe quel paquet :
pip install polars
Polars demande Python 3.10 minimum.
Nos données de démonstration
Pour avoir un jeu de données de départ, j'ai créé un fichier seed_data.py :
import csv import random from datetime import date, timedelta # La graine garantit que vous obtiendrez exactement les mêmes données que moi random.seed(117) commerciaux = [("Patrick", "Lyon"), ("Sebastien", "Paris"), ("Elodie", "Lille")] prix = {"Clavier": 49.90, "Souris": 25.50, "Ecran": 189.00, "Casque": 79.90} debut = date(2026, 1, 1) with open("ventes.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f, lineterminator="\n") writer.writerow(["date", "commercial", "ville", "produit", "quantite", "prix_unitaire"]) for _ in range(500): nom, ville = random.choice(commerciaux) produit = random.choice(list(prix)) writer.writerow([ (debut + timedelta(days=random.randint(0, 180))).isoformat(), nom, ville, produit, random.randint(1, 20), prix[produit], ])
Ce script vous permet de générer un fichier ventes.csv, et la graine vous permet d'avoir les mêmes données que moi.
Lire un fichier
Un DataFrame, c'est un tableau de données, un peu comme une feuille de calcul (pour ceux qui font du Excel) : des lignes, des colonnes, et un nom pour chaque colonne. La particularité, c'est qu'une colonne ne contient qu'une seule sorte de valeurs : que des nombres entiers, que du texte, que des dates. Le vocabulaire est le même que dans Pandas.
import polars as pl df = pl.read_csv("ventes.csv") print(df.head())
Cinq premières lignes
head() renvoie les cinq premières lignes de données par défaut, head(2) en renverrait deux. Pour le nom des colonnes, Polars les affiche systématiquement. Vous remarquerez que le type de chaque colonne est affiché juste sous son nom.
La colonne date contient bien des dates 💡, pourtant Polars la type en chaîne de caractères. Polars devine tout seul les types numériques, mais pas les dates. Il faut alors utiliser try_parse_dates=True.
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) print(df.head())
La type date est bien affiché
Attention
try_parse_dates=True va essayer de parser toutes les colonnes qui contiennent du texte. Donc si 2026-02-12 correspond à une référence mais pas à une date, vous risquez d'avoir un problème 😅.
Pour ne viser qu'une seule colonne, vous pouvez imposer le type via schema_overrides :
import polars as pl df = pl.read_csv("ventes.csv", schema_overrides={"date": pl.Date}) print(df.head())
À savoir que vous pouvez inspecter le schéma de votre dataframe avec l'attribut schema :
import polars as pl df = pl.read_csv("ventes.csv", schema_overrides={"date": pl.Date}) print(df.schema) # Résultat : # Schema({'date': Date, 'commercial': String, 'ville': String, 'produit': String, 'quantite': Int64, 'prix_unitaire': Float64})
Inspecter les données
Nous allons parler d'un attribut et de deux méthodes, à commencer par shape pour les dimensions :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) print(df.shape) # Résultat : (500, 6)
Ensuite, la méthode glimpse(), elle permet d'afficher une colonne par ligne, avec un échantillon de valeurs :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) df.glimpse()
Rows: 500 Columns: 6 $ date <date> 2026-02-12, 2026-05-23, 2026-05-15, 2026-04-27, 2026-04-22, 2026-04-09, 2026-02-10, 2026-06-01, 2026-04-10, 2026-01-29 $ commercial <str> 'Patrick', 'Sebastien', 'Elodie', 'Sebastien', 'Patrick', 'Patrick', 'Sebastien', 'Elodie', 'Sebastien', 'Patrick' $ ville <str> 'Lyon', 'Paris', 'Lille', 'Paris', 'Lyon', 'Lyon', 'Paris', 'Lille', 'Paris', 'Lyon' $ produit <str> 'Souris', 'Souris', 'Clavier', 'Ecran', 'Ecran', 'Casque', 'Clavier', 'Souris', 'Casque', 'Ecran' $ quantite <i64> 7, 14, 19, 2, 3, 14, 14, 5, 8, 19 $ prix_unitaire <f64> 25.5, 25.5, 49.9, 189.0, 189.0, 79.9, 49.9, 25.5, 79.9, 189.0
Pour les statistiques, vous pouvez utiliser describe() :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) print(df.describe())
Statistiques
Créer un DataFrame à la main
Pour les tests ou un petit jeu de données, vous pouvez créer un DataFrame à la main :
import polars as pl equipe = pl.DataFrame({ "commercial": ["Patrick", "Sebastien", "Elodie"], "ville": ["Lyon", "Paris", "Lille"], "anciennete": [12, 5, 3], }) print(equipe)
Le dataframe
Les expressions
Une fonctionnalité primordiale de Polars : les expressions.
Si vous ne souhaitez garder que les commandes supérieures à 19 quantités :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) resultat = df.filter(pl.col("quantite") > 19) print(resultat)
Ventes supérieurs à 19
pl.col("quantite") > 19 ne contient aucune donnée. C'est une expression, c'est-à-dire la description d'un calcul à effectuer. Vous pouvez la créer, la stocker dans une variable (grosse_commande = pl.col("quantite") > 19), la réutiliser, sans jamais toucher au moindre DataFrame. Si je m'attarde là-dessus, c'est parce qu'en décrivant vos calculs au lieu de les exécuter immédiatement, Polars peut les analyser, les réorganiser et les répartir sur tous vos cœurs CPU.
Une expression seule ne produit rien : il lui faut un contexte pour être évaluée. Nous allons voir quelques-uns de ces contextes, vous pouvez les retrouver dans la documentation officielle.
select() : choisir et calculer des colonnes
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) print(df.select("commercial", "produit", "quantite").head(3))
Résultat avec select
select() évalue les expressions qu'on lui passe. On peut ainsi ajouter de nouvelles colonnes :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) print( df.select( # On garde cette colonne telle quelle "commercial", # On multiplie deux colonnes, que l'on injecte dans une colonne montant (pl.col("quantite") * pl.col("prix_unitaire")).alias("montant"), ).head(3) )
Résultat avec la colonne montant
À noter
Le résultat ne contient que les colonnes demandées : select() ne conserve rien d'autre.
with_columns() : ajouter des colonnes
On fait à peu près comme avec select(), sauf qu'ici on garde tout. with_columns() conserve les colonnes existantes et ajoute la nouvelle à la suite. Reprenons l'exemple précédent, mais en gardant toutes les colonnes :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) df = df.with_columns( (pl.col("quantite") * pl.col("prix_unitaire")).alias("montant") ) print(df.head(3))
Ajout d'une colonne montant
À noter
with_columns() accepte autant d'expressions que vous le souhaitez, séparées par des virgules. Elles sont évaluées en parallèle.
Les expressions sont indépendantes. Par exemple, si l'on veut utiliser une deuxième expression s'appuyant sur la colonne montant créée par l'expression précédente, on obtiendra une exception polars.exceptions.ColumnNotFoundError: unable to find column "montant"; valid columns: ["date", "commercial", "ville", "produit", "quantite", "prix_unitaire"].
Je ne peux donc pas faire :
df.with_columns( (pl.col("quantite") * pl.col("prix_unitaire")).alias("montant"), (pl.col("montant") * 0.2).alias("tva"), ) # ColumnNotFoundError
Je dois effectuer deux appels successifs :
df = ( df.with_columns((pl.col("quantite") * pl.col("prix_unitaire")).alias("montant")) .with_columns((pl.col("montant") * 0.2).alias("tva")) )
filter() : filtrer les lignes
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) # On ne garde que les lignes où plus de 15 unités ont été vendues print(df.filter(pl.col("quantite") > 15).head(3)) # On combine deux conditions avec & : il faut que les DEUX soient vraies. # Ici, les ventes d'écrans réalisées par Patrick. # Chaque condition est entourée de parenthèses, c'est obligatoire. print(df.filter( (pl.col("commercial") == "Patrick") & (pl.col("produit") == "Ecran") ).head(3))
Résultat avec filter
À noter
Comme dans Pandas, on utilise & (ET), | (OU) et ~ (NON).
sort() : trier les données
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) # Les plus grosses ventes en premier print(df.sort("quantite", descending=True).head(3)) # On peut trier sur plusieurs colonnes, avec un sens différent pour chacune. # Ici : commercial par ordre alphabétique, puis quantités décroissantes # à l'intérieur de chaque commercial. print(df.sort("commercial", "quantite", descending=[False, True]).head(4))
Résultat avec sort.
Le paramètre descending indique le sens du tri ; il accepte deux formes :
-
un booléen unique appliqué à toutes les colonnes du tri
-
une liste comportant un booléen par colonne, dans le même ordre que celles-ci
Enchaîner les opérations
Tout l'intérêt de Polars réside dans la possibilité de chaîner les méthodes de son API. Chaque méthode renvoie un DataFrame :
import polars as pl resultat = ( pl.read_csv("ventes.csv", try_parse_dates=True) .with_columns((pl.col("quantite") * pl.col("prix_unitaire")).alias("montant")) .filter(pl.col("produit") == "Ecran") .sort("montant", descending=True) .head(3) )
Pas besoin de déclarer de variable intermédiaire.
Regrouper, croiser et analyser
Jusqu'ici, nous travaillions sur un seul tableau et traitions les lignes une par une. Nous passons maintenant aux calculs qui résument plusieurs lignes en une seule.
Regrouper et agréger
Combinons les méthodes group_by() et agg() : elles fonctionnent toujours ensemble, mais chacune a un rôle bien précis.
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True).with_columns( (pl.col("quantite") * pl.col("prix_unitaire")).alias("montant") ) bilan = ( df.group_by("commercial") .agg( pl.col("montant").sum().alias("chiffre_affaires"), pl.col("montant").mean().round(2).alias("panier_moyen"), pl.len().alias("nb_ventes"), # pl.len() compte les lignes du groupe ) .sort("chiffre_affaires", descending=True) ) print(bilan)
Bilan des commerciaux
group_by("commercial") rassemble les lignes qui partagent la même valeur : les ventes de Patrick, Sébastien et Élodie. Cette méthode renvoie un objet intermédiaire qui attend qu'on lui précise quoi calculer. C'est agg() qui effectue le calcul. Il s'agit d'un contexte auquel il faut passer des expressions.
Nous en passons trois :
-
La somme des montants sous la colonne
chiffre_affaires -
Le panier moyen
-
Le nombre de ventes
On peut aussi regrouper sur une expression calculée, comme par exemple le mois de la vente :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) par_mois = ( # On regroupe sur une expression calculée à la volée : le mois de la vente df.group_by(pl.col("date").dt.month().alias("mois")) # agg() étant un contexte, le montant se calcule directement ici .agg((pl.col("quantite") * pl.col("prix_unitaire")).sum().round(2).alias("ca")) .sort("mois") ) print(par_mois)
Regroupement par mois
Vous avez certainement remarqué .dt. Polars organise bien ses méthodes : la méthode .month() n'aurait pas de sens sur du texte, et to_uppercase() n'aurait aucun sens sur une date. Polars range les méthodes logiquement dans différents espaces de noms. En voici quelques-uns :
-
.dtpour les dates et les heures -
.strpour les chaînes de caractères -
.listpour les colonnes qui contiennent des listes -
.namepour agir sur le nom de la colonne plutôt que sur son contenu
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True) print( df.select( pl.col("commercial").str.to_uppercase().alias("commercial_maj"), pl.col("ville").str.len_chars().alias("longueur_ville"), pl.col("date").dt.year().alias("annee"), ).head(3) )
Illustration des différents espaces de noms
Joindre deux DataFrames
L'idée est de réunir deux tableaux via une colonne commune. Prenons un exemple simple : le chiffre d'affaires réalisé d'un côté, et les objectifs fixés de l'autre. Deux DataFrames séparés, reliés par le nom du commercial.
Nous allons utiliser deux paramètres :
-
ondésigne la colonne commune. Elle doit exister dans les deux DataFrames -
howindique quelles lignes conserver quand une valeur n'a pas de correspondance en face
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True).with_columns( (pl.col("quantite") * pl.col("prix_unitaire")).alias("montant") ) # Les objectifs annuels, fixés par la direction objectifs = pl.DataFrame({ "commercial": ["Patrick", "Sebastien", "Elodie"], "objectif": [150_000.0, 160_000.0, 120_000.0], }) realise = df.group_by("commercial").agg(pl.col("montant").sum().alias("ca")) bilan = ( realise.join(objectifs, on="commercial", how="left") .with_columns((pl.col("ca") >= pl.col("objectif")).alias("objectif_atteint")) .sort("commercial") ) print(bilan)
Les deux DataFrames sont liés
how="left" garde toutes les lignes du DataFrame de gauche, c'est-à-dire le DataFrame sur lequel la méthode join est appelée. Si un commercial est absent de la table des objectifs, il apparaîtrait quand même avec un null. Par défaut, ce n'est pas left mais inner : dans ce cas, le commercial manquant disparaît du tableau.
Calculer par groupe sans regrouper
Si nous voulons afficher sur chaque ligne la part que représente une vente dans le total du commercial, avec un group_by(), nous perdons le détail des lignes. over() permet de résoudre cela :
import polars as pl df = pl.read_csv("ventes.csv", try_parse_dates=True).with_columns( (pl.col("quantite") * pl.col("prix_unitaire")).alias("montant") ) resultat = df.select( "commercial", "produit", "montant", # Le total est calculé par commercial, mais rapporté sur chaque ligne (pl.col("montant") / pl.col("montant").sum().over("commercial") * 100) .round(2) .alias("part_pct"), ) print(resultat.head(5))
Calculer par groupe avec over
Sélectionner des colonnes par type
Le module polars.selectors permet de cibler des colonnes sans les nommer une par une. Il permet de cibler les colonnes par type :
import polars as pl import polars.selectors as cs df = pl.read_csv("ventes.csv", try_parse_dates=True) print(df.select(cs.numeric()).head(3)) # Toutes les colonnes numériques print(df.select(cs.string()).head(3)) # Toutes les colonnes texte
Sélectionner les colonnes par type
Lazy ou eager : la vraie différence
Tout ce que nous avons écrit jusqu'ici est en mode eager (impatient) : chaque méthode s'exécute immédiatement. C'est ce que vous retrouverez avec Pandas.
Polars propose un second mode : le mode lazy (paresseux). Avec ce mode, Polars va construire un plan d'exécution avant de démarrer le calcul. Il vous appartient ensuite d'exécuter ce plan pour obtenir vos résultats.
.collect() est la méthode la plus courante pour obtenir ce résultat. sink_parquet() déclenche également l'exécution, à la différence près qu'il n'instancie pas de DataFrame en mémoire et écrit directement sur le disque. Un LazyFrame n'effectue aucun calcul tant qu'aucune de ces méthodes n'est appelée.
Pour passer en mode lazy, il suffit de remplacer read_csv par scan_csv :
import polars as pl print(pl.scan_csv("ventes.csv"))
naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan) Csv SCAN [ventes.csv] PROJECT */6 COLUMNS ESTIMATED ROWS: 524
.collect() est bloquant si vous souhaitez obtenir votre DataFrame :
import polars as pl resultat = ( pl.scan_csv("ventes.csv", try_parse_dates=True) # scan_ au lieu de read_ .with_columns((pl.col("quantite") * pl.col("prix_unitaire")).alias("montant")) .filter(pl.col("produit") == "Ecran") .group_by("commercial") .agg(pl.col("montant").sum().alias("ca")) .collect() # Ici seulement, ça s'exécute ) print(resultat)
Le reste du code est identique : votre logique métier ne change pas, seule l'exécution est différente.
La méthode explain() affiche le plan de requête :
import polars as pl plan = ( pl.scan_csv("ventes.csv") .filter(pl.col("produit") == "Ecran") .group_by("commercial") .agg(pl.col("quantite").sum()) ) print(plan.explain())
AGGREGATE[maintain_order: false] [col("quantite").sum()] BY [col("commercial")] FROM simple π 2/2 ["commercial", "quantite"] Csv SCAN [ventes.csv] PROJECT 3/6 COLUMNS SELECTION: col("produit") == "Ecran" ESTIMATED ROWS: 524
PROJECT 3/6 COLUMNS : Polars a compris que seules 3 colonnes sur 6 étaient utiles.
Un exemple avec Streamlit
Si vous avez lu notre article sur Streamlit, vous avez probablement pensé à y intégrer Polars :
import polars as pl import streamlit as st st.title("Ventes 2026") # scan_csv renvoie un LazyFrame : rien n'est encore lu sur le disque ventes = pl.scan_csv("ventes.csv", try_parse_dates=True).with_columns( (pl.col("quantite") * pl.col("prix_unitaire")).alias("montant") ) commercial = st.selectbox("Commercial", ["Patrick", "Sebastien", "Elodie"]) # Le filtre s'ajoute au plan, puis collect() déclenche l'exécution resultat = ventes.filter(pl.col("commercial") == commercial).collect() st.metric("Chiffre d'affaires", f"{resultat['montant'].sum():,.2f} €") st.dataframe(resultat, hide_index=True)
Intégration avec Streamlit
Nous avons fait un tour assez complet de Polars. Si vous voulez aller plus loin, Polars propose un moteur de streaming, des méthodes pour cohabiter avec Pandas ou encore la possibilité d'écrire du SQL.