19. Polars#

En plus de ce qui est fourni dans Anaconda, ce cours nécessitera les bibliothèques suivantes :

!pip install --upgrade polars yfinance

Hide code cell output

Collecting polars
  Downloading polars-1.43.1-py3-none-any.whl.metadata (11 kB)
Collecting yfinance
  Downloading yfinance-1.5.2-py2.py3-none-any.whl.metadata (6.2 kB)
Collecting polars-runtime-32==1.43.1 (from polars)
  Downloading polars_runtime_32-1.43.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.5 kB)
Requirement already satisfied: pandas>=1.3.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (3.0.3)
Requirement already satisfied: numpy>=1.16.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (2.4.6)
Requirement already satisfied: requests>=2.31 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (2.34.2)
Collecting multitasking>=0.0.7 (from yfinance)
  Downloading multitasking-0.0.13-py3-none-any.whl.metadata (16 kB)
Requirement already satisfied: platformdirs>=2.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (4.9.4)
Collecting pytz>=2022.5 (from yfinance)
  Downloading pytz-2026.3.post1-py2.py3-none-any.whl.metadata (22 kB)
Collecting peewee>=3.16.2 (from yfinance)
  Downloading peewee-4.2.6-py3-none-any.whl.metadata (10 kB)
Requirement already satisfied: beautifulsoup4>=4.11.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (4.14.3)
Collecting curl_cffi>=0.15 (from yfinance)
  Downloading curl_cffi-0.15.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (18 kB)
Requirement already satisfied: protobuf>=3.19.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (6.33.5)
Requirement already satisfied: websockets>=13.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from yfinance) (15.0.1)
Requirement already satisfied: soupsieve>=1.6.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from beautifulsoup4>=4.11.1->yfinance) (2.5)
Requirement already satisfied: typing-extensions>=4.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from beautifulsoup4>=4.11.1->yfinance) (4.15.0)
Requirement already satisfied: cffi>=2.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from curl_cffi>=0.15->yfinance) (2.0.0)
Requirement already satisfied: certifi>=2024.2.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from curl_cffi>=0.15->yfinance) (2026.5.20)
Requirement already satisfied: rich in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from curl_cffi>=0.15->yfinance) (15.0.0)
Requirement already satisfied: pycparser in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from cffi>=2.0.0->curl_cffi>=0.15->yfinance) (3.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pandas>=1.3.0->yfinance) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from python-dateutil>=2.8.2->pandas>=1.3.0->yfinance) (1.17.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.31->yfinance) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.31->yfinance) (3.18)
Requirement already satisfied: urllib3<3,>=1.26 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests>=2.31->yfinance) (2.7.0)
Requirement already satisfied: markdown-it-py>=2.2.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from rich->curl_cffi>=0.15->yfinance) (3.0.0)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from rich->curl_cffi>=0.15->yfinance) (2.20.0)
Requirement already satisfied: mdurl~=0.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from markdown-it-py>=2.2.0->rich->curl_cffi>=0.15->yfinance) (0.1.2)
Downloading polars-1.43.1-py3-none-any.whl (846 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 846.8/846.8 kB 94.5 MB/s  0:00:00
?25hDownloading polars_runtime_32-1.43.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (57.3 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/57.3 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 57.1/57.3 MB 324.4 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 MB 243.9 MB/s  0:00:00
?25hDownloading yfinance-1.5.2-py2.py3-none-any.whl (144 kB)
Downloading curl_cffi-0.15.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (11.1 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.1/11.1 MB 364.9 MB/s  0:00:00
?25h
Downloading multitasking-0.0.13-py3-none-any.whl (16 kB)
Downloading peewee-4.2.6-py3-none-any.whl (173 kB)
Downloading pytz-2026.3.post1-py2.py3-none-any.whl (508 kB)
Installing collected packages: pytz, peewee, multitasking, polars-runtime-32, polars, curl_cffi, yfinance
?25l
   ━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1/7 [peewee]
   ━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━ 3/7 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━ 3/7 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━ 3/7 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━ 3/7 [polars-runtime-32]
   ━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━ 4/7 [polars]
   ━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━ 4/7 [polars]
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━ 5/7 [curl_cffi]
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 7/7 [yfinance]
Successfully installed curl_cffi-0.15.0 multitasking-0.0.13 peewee-4.2.6 polars-1.43.1 polars-runtime-32-1.43.1 pytz-2026.3.post1 yfinance-1.5.2

19.1. Aperçu général#

Polars est une bibliothèque de manipulation de données rapide pour Python, écrite en Rust.

Elle a gagné une popularité considérable en tant qu’alternative moderne à pandas en raison de ses avantages en matière de performance.

Polars est conçue en tenant compte de la performance et de l’efficacité mémoire, en s’appuyant sur :

  • Le format colonnaire Apache Arrow pour un accès rapide aux données

  • L”évaluation paresseuse pour optimiser l’exécution des requêtes

  • Le traitement parallèle pour utiliser tous les cœurs de processeur disponibles

  • Une API expressive construite autour d’expressions de colonnes

Astuce

Pourquoi envisager Polars plutôt que pandas ?

  • Mémoire : pandas nécessite généralement 5 à 10 fois la taille de votre jeu de données en RAM ; Polars n’en nécessite que 2 à 4 fois

  • Vitesse : Polars est 10 à 100 fois plus rapide pour de nombreuses opérations courantes

  • Voir : Les benchmarks TPC-H de Polars pour des comparaisons de performance à jour

Tout au long du cours, nous supposerons que les importations suivantes ont été effectuées

import polars as pl
import numpy as np
import matplotlib.pyplot as plt

Comme Pandas, Polars définit deux types de données importants : Series et DataFrame.

Vous pouvez considérer une Series comme une colonne de données, telle qu’une collection d’observations sur une seule variable.

Un DataFrame est un objet à deux dimensions permettant de stocker des colonnes de données liées entre elles.

19.2. Series#

Commençons par les Series.

Nous commençons par créer une série de quatre observations aléatoires

s = pl.Series(name='daily returns', values=np.random.randn(4))
s
shape: (4,)
daily returns
f64
2.218478
-0.37565
-1.465469
-1.298266

Note

Contrairement aux Series pandas, les Series Polars n’ont pas d’indice de ligne. Polars est centré sur les colonnes — l’accès aux données est géré par des expressions de colonnes et des masques booléens plutôt que par des étiquettes de ligne. Consultez le guide de migration Polars pour les utilisateurs de pandas pour plus de détails.

Les Series Polars sont construites sur les tableaux Apache Arrow et prennent en charge de nombreuses opérations familières

s * 100
shape: (4,)
daily returns
f64
221.847765
-37.564952
-146.546938
-129.826624

Les valeurs absolues sont disponibles sous forme de méthode

s.abs()
shape: (4,)
daily returns
f64
2.218478
0.37565
1.465469
1.298266

Nous pouvons également obtenir rapidement des statistiques récapitulatives

s.describe()
shape: (9, 2)
statisticvalue
strf64
"count"4.0
"null_count"0.0
"mean"-0.230227
"std"1.701356
"min"-1.465469
"25%"-1.298266
"50%"-0.37565
"75%"-0.37565
"max"2.218478

Étant donné que Polars n’a pas d’indice de ligne, les données étiquetées nécessitent un DataFrame.

Par exemple, pour associer des symboles boursiers à des rendements :

df = pl.DataFrame({
    'company': ['AMZN', 'AAPL', 'MSFT', 'GOOG'],
    'daily returns': np.random.randn(4)
})
df
shape: (4, 2)
companydaily returns
strf64
"AMZN"-0.428645
"AAPL"0.039412
"MSFT"-1.396405
"GOOG"-1.28706

Nous accédons à une valeur en filtrant sur une expression de colonne

df.filter(
    pl.col('company') == 'AMZN'
).select('daily returns').item()
-0.42864520897469816

Les mises à jour utilisent également des expressions plutôt qu’une affectation par indice

df = df.with_columns(
    pl.when(pl.col('company') == 'AMZN')
    .then(0)
    .otherwise(pl.col('daily returns'))
    .alias('daily returns')
)
df
shape: (4, 2)
companydaily returns
strf64
"AMZN"0.0
"AAPL"0.039412
"MSFT"-1.396405
"GOOG"-1.28706

Nous pouvons également vérifier l’appartenance

'AAPL' in df['company']
True

19.3. DataFrames#

Alors qu’une Series est une seule colonne de données, un DataFrame comporte plusieurs colonnes, une pour chaque variable.

Comme dans Pandas, travaillons avec les données des Penn World Tables.

Nous les lisons à l’aide de pl.read_csv

url = ('https://raw.githubusercontent.com/QuantEcon/'
       'lecture-python-programming/main/lectures/_static/'
       'lecture_specific/pandas/data/test_pwt.csv')
df = pl.read_csv(url)
df
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.6530.9995295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.8942364.43645110.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.93983227242.3694972.718715.726546
"United States""USA"2000282171.9571.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

19.3.1. Sélection des données#

Nous pouvons sélectionner des lignes par découpage (slicing) et des colonnes par nom

df[2:5]
shape: (3, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.8942364.43645110.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954

Pour sélectionner des colonnes spécifiques, passez une liste de noms à select

df.select(['country', 'tcgdp'])
shape: (8, 2)
countrytcgdp
strf64
"Argentina"295072.21869
"Australia"541804.6521
"India"1.7281e6
"Israel"129253.89423
"Malawi"5026.221784
"South Africa"227242.36949
"United States"9.8987e6
"Uruguay"25255.961693

Ces opérations peuvent être combinées

df[2:5].select(['country', 'tcgdp'])
shape: (3, 2)
countrytcgdp
strf64
"India"1.7281e6
"Israel"129253.89423
"Malawi"5026.221784

19.3.2. Filtrage par conditions#

La méthode filter accepte des expressions booléennes construites à partir de pl.col

df.filter(pl.col('POP') >= 20000)
shape: (4, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.6530.9995295072.2186975.7168055.578804
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"South Africa""ZAF"200045064.0986.93983227242.3694972.718715.726546
"United States""USA"2000282171.9571.09.8987e672.3470546.032454

Plusieurs conditions peuvent être combinées avec & (et) et | (ou)

df.filter(
    (pl.col('country').is_in(['Argentina', 'India', 'South Africa'])) &
    (pl.col('POP') > 40000)
)
shape: (2, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"South Africa""ZAF"200045064.0986.93983227242.3694972.718715.726546

Les expressions peuvent impliquer des opérations arithmétiques entre colonnes

df.filter(
    (pl.col('cc') + pl.col('cg') >= 80) & (pl.col('POP') <= 20000)
)
shape: (2, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

Sélectionnons le pays ayant la part de consommation des ménages la plus élevée

df.filter(pl.col('cc') == pl.col('cc').max())
shape: (1, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

19.3.3. Expressions de colonnes#

Une différence essentielle avec pandas est que Polars utilise des expressions de colonnes pour les transformations plutôt que des appels apply élément par élément.

Voici un exemple calculant le maximum de chaque colonne numérique

df.select(
    pl.col(['year', 'POP', 'XRAT', 'tcgdp', 'cc', 'cg'])
    .max()
    .name.suffix('_max')
)
shape: (1, 6)
year_maxPOP_maxXRAT_maxtcgdp_maxcc_maxcg_max
i64f64f64f64f64f64
20001.0063e659.5438089.8987e678.9787414.072206

Les expressions peuvent être utilisées dans with_columns pour ajouter ou modifier des colonnes

df.with_columns(
    (pl.col('XRAT') / 10).alias('XRAT_scaled'),
    pl.col(pl.Float64).round(2)
)
shape: (8, 9)
countrycountry isocodeyearPOPXRATtcgdpcccgXRAT_scaled
strstri64f64f64f64f64f64f64
"Argentina""ARG"200037335.651.0295072.2275.725.580.09995
"Australia""AUS"200019053.191.72541804.6567.766.720.172483
"India""IND"20001006300.344.941.7281e664.5814.074.49416
"Israel""ISR"20006114.574.08129253.8964.4410.270.407733
"Malawi""MWI"200011801.559.545026.2274.7111.665.954381
"South Africa""ZAF"200045064.16.94227242.3772.725.730.693983
"United States""USA"2000282171.961.09.8987e672.356.030.1
"Uruguay""URY"20003219.7912.125255.9678.985.111.209959

La logique conditionnelle utilise pl.when(...).then(...).otherwise(...)

df.with_columns(
    pl.when(pl.col('POP') >= 20000)
    .then(pl.col('POP'))
    .otherwise(None)
    .alias('POP_filtered')
).select(['country', 'POP', 'POP_filtered'])
shape: (8, 3)
countryPOPPOP_filtered
strf64f64
"Argentina"37335.65337335.653
"Australia"19053.186null
"India"1.0063e61.0063e6
"Israel"6114.57null
"Malawi"11801.505null
"South Africa"45064.09845064.098
"United States"282171.957282171.957
"Uruguay"3219.793null

Note

Polars fournit map_elements comme solution de secours pour appliquer des fonctions Python arbitraires ligne par ligne, mais cela contourne le moteur d’expression optimisé et devrait être évité lorsqu’une expression native existe.

19.3.4. Valeurs manquantes#

Insérons quelques valeurs nulles pour démontrer les techniques d’imputation

df_nulls = df.with_row_index().with_columns(
    pl.when(pl.col('index') == 0)
    .then(None).otherwise(pl.col('XRAT')).alias('XRAT'),
    pl.when(pl.col('index') == 3)
    .then(None).otherwise(pl.col('cc')).alias('cc'),
    pl.when(pl.col('index') == 5)
    .then(None).otherwise(pl.col('tcgdp')).alias('tcgdp'),
    pl.when(pl.col('index') == 6)
    .then(None).otherwise(pl.col('POP')).alias('POP'),
).drop('index')
df_nulls
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.653null295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.89423null10.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.93983null72.718715.726546
"United States""USA"2000null1.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

Remplissons toutes les valeurs nulles par zéro

df_nulls.fill_null(0)
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.6530.0295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.894230.010.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.939830.072.718715.726546
"United States""USA"20000.01.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

Ou remplissons avec les moyennes des colonnes

cols = ['cc', 'tcgdp', 'POP', 'XRAT']
df_nulls.with_columns(
    pl.col(cols).fill_null(pl.col(cols).mean())
)
shape: (8, 8)
countrycountry isocodeyearPOPXRATtcgdpcccg
strstri64f64f64f64f64f64
"Argentina""ARG"200037335.65318.618141295072.2186975.7168055.578804
"Australia""AUS"200019053.1861.72483541804.652167.7590266.720098
"India""IND"20001.0063e644.94161.7281e664.57555114.072206
"Israel""ISR"20006114.574.07733129253.8942372.40050210.266688
"Malawi""MWI"200011801.50559.5438085026.22178474.70762411.658954
"South Africa""ZAF"200045064.0986.939831.8033e672.718715.726546
"United States""USA"2000161269.8717141.09.8987e672.3470546.032454
"Uruguay""URY"20003219.79312.09959225255.96169378.978745.108068

Polars prend également en charge le remplissage en avant (fill_null(strategy='forward')) et l’interpolation.

Il existe des outils d’imputation plus avancés disponibles dans scikit-learn.

19.3.5. Visualisation#

Construisons une colonne de PIB par habitant et traçons-la

df = (df
    .select(['country', 'POP', 'tcgdp'])
    .rename({'POP': 'population', 'tcgdp': 'total GDP'})
    .with_columns(
        (pl.col('population') * 1e3).alias('population')
    )
    .with_columns(
        (pl.col('total GDP') * 1e6 / pl.col('population'))
        .alias('GDP percap')
    )
    .sort('GDP percap', descending=True)
)
df
shape: (8, 4)
countrypopulationtotal GDPGDP percap
strf64f64f64
"United States"2.82171957e89.8987e635080.381854
"Australia"1.9053186e7541804.652128436.433261
"Israel"6.11457e6129253.8942321138.672749
"Argentina"3.7335653e7295072.218697903.229085
"Uruguay"3.219793e625255.9616937843.97062
"South Africa"4.5064098e7227242.369495042.647686
"India"1.0063e91.7281e61717.324719
"Malawi"1.1801505e75026.221784425.896679

Nous pouvons extraire directement les colonnes pour matplotlib

Note

Polars fournit également une API de tracé intégrée basée sur Altair (par exemple, df.plot.bar(x=..., y=...)). Nous utilisons matplotlib ici pour rester cohérents avec le reste de la série de cours.

fig, ax = plt.subplots()
ax.bar(df['country'].to_list(), df['GDP percap'].to_list())
ax.set_xlabel('country', fontsize=12)
ax.set_ylabel('GDP per capita', fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
_images/23e0e9be12d69f289c65fe840bdcb0f11ce8746534b42cae321b9564b0a85301.png

19.4. Évaluation paresseuse#

L’une des fonctionnalités les plus puissantes de Polars est l”évaluation paresseuse (lazy evaluation).

Au lieu d’exécuter chaque opération immédiatement, le mode paresseux collecte le plan de requête complet et l’optimise avant de l’exécuter.

19.4.1. Mode immédiat vs mode paresseux#

# Rechargement du jeu de données
url = ('https://raw.githubusercontent.com/QuantEcon/'
       'lecture-python-programming/main/lectures/_static/'
       'lecture_specific/pandas/data/test_pwt.csv')
df_full = pl.read_csv(url)

L’API immédiate (eager) s’exécute directement (comme pandas)

result_eager = (df_full
    .filter(pl.col('tcgdp') > 1000)
    .select(['country', 'year', 'tcgdp'])
    .sort('tcgdp', descending=True)
)
result_eager.head()
shape: (5, 3)
countryyeartcgdp
stri64f64
"United States"20009.8987e6
"India"20001.7281e6
"Australia"2000541804.6521
"Argentina"2000295072.21869
"South Africa"2000227242.36949

L’API paresseuse (lazy) construit plutôt un plan de requête

lazy_query = (df_full.lazy()
    .filter(pl.col('tcgdp') > 1000)
    .select(['country', 'year', 'tcgdp'])
    .sort('tcgdp', descending=True)
)
print(lazy_query.explain())
SORT BY [descending: [true]] [col("tcgdp")]
  FILTER col("tcgdp") > 1000.0
  FROM
    DF ["country", "country isocode", "year", "POP", ...]; PROJECT["country", "year", "tcgdp"] 3/8 COLUMNS

Appelez collect pour exécuter le plan

result_lazy = lazy_query.collect()
result_lazy.head()
shape: (5, 3)
countryyeartcgdp
stri64f64
"United States"20009.8987e6
"India"20001.7281e6
"Australia"2000541804.6521
"Argentina"2000295072.21869
"South Africa"2000227242.36949

19.4.2. Optimisation des requêtes#

Le moteur paresseux applique automatiquement plusieurs optimisations :

  • Descente des prédicats (predicate pushdown) — les filtres sont appliqués le plus tôt possible

  • Descente des projections (projection pushdown) — seules les colonnes nécessaires sont lues depuis la source

  • Élimination des sous-expressions communes — les calculs en double sont fusionnés

Voyons comment Polars réécrit une requête à plusieurs étapes

optimized = (df_full.lazy()
    .select(['country', 'year', 'tcgdp', 'POP'])
    .filter(pl.col('tcgdp') > 500)
    .with_columns(
        (pl.col('tcgdp') / pl.col('POP')).alias('gdp_per_capita')
    )
    .filter(pl.col('gdp_per_capita') > 10)
    .select(['country', 'year', 'gdp_per_capita'])
)

print("Optimized plan:")
print(optimized.explain())
Optimized plan:
FILTER col("gdp_per_capita") > 10.0
FROM
  simple π 3/3 ["country", "year", ... 1 other column]
     WITH_COLUMNS:
     [(col("tcgdp") / col("POP")).alias("gdp_per_capita")] 
      FILTER col("tcgdp") > 500.0
      FROM
        DF ["country", "country isocode", "year", "POP", ...]; PROJECT["country", "year", "tcgdp", "POP"] 4/8 COLUMNS

L’exécution du plan nous donne le résultat final

optimized.collect()
shape: (3, 3)
countryyeargdp_per_capita
stri64f64
"Australia"200028.436433
"Israel"200021.138673
"United States"200035.080382

19.4.3. Comparaison des performances#

Comparons pandas, Polars en mode immédiat, et Polars en mode paresseux sur la même tâche.

Nous commençons avec un petit jeu de données (les Penn World Tables utilisées ci-dessus) pour montrer que pour de petites données, les différences sont négligeables

import pandas as pd
import time

# Petit jeu de données -- Penn World Tables (~8 lignes)
url = ('https://raw.githubusercontent.com/QuantEcon/'
       'lecture-python-programming/main/lectures/_static/'
       'lecture_specific/pandas/data/test_pwt.csv')
small_pd = pd.read_csv(url)
small_pl = pl.read_csv(url)

Maintenant, chronométrons la même opération de filtrage-sélection-tri dans chaque bibliothèque

# pandas
start = time.perf_counter()
_ = (small_pd
     .query('tcgdp > 500')
     [['country', 'year', 'tcgdp', 'POP']]
     .assign(gdp_pc=lambda d: d['tcgdp'] / d['POP'])
     .sort_values('gdp_pc', ascending=False))
pd_small = time.perf_counter() - start

# Polars immédiat
start = time.perf_counter()
_ = (small_pl
     .filter(pl.col('tcgdp') > 500)
     .select(['country', 'year', 'tcgdp', 'POP'])
     .with_columns((pl.col('tcgdp') / pl.col('POP')).alias('gdp_pc'))
     .sort('gdp_pc', descending=True))
pl_small = time.perf_counter() - start

print(f"Small data  --  pandas: {pd_small:.4f}s | Polars eager: {pl_small:.4f}s")
Small data  --  pandas: 0.0064s | Polars eager: 0.0010s

Sur quelques lignes, la différence de vitesse est négligeable — utilisez celle des API que vous trouvez la plus pratique.

Passons maintenant à 5 millions de lignes, où la différence devient évidente.

La tâche consiste à : filtrer les lignes où value > 0, calculer un produit pondéré value * weight, puis calculer la moyenne de ce produit au sein de chaque groupe — une moyenne pondérée groupée.

n = 5_000_000
np.random.seed(42)

groups = np.random.choice(['A', 'B', 'C', 'D'], n)
values = np.random.randn(n)
weights = np.random.rand(n)
extra1 = np.random.randn(n)
extra2 = np.random.randn(n)

big_pd = pd.DataFrame({
    'group': groups, 'value': values,
    'weight': weights, 'extra1': extra1, 'extra2': extra2
})
big_pl = pl.DataFrame({
    'group': groups, 'value': values,
    'weight': weights, 'extra1': extra1, 'extra2': extra2
})

D’abord, la référence pandas

start = time.perf_counter()
tmp = big_pd[big_pd['value'] > 0][['group', 'value', 'weight']].copy()
tmp['weighted'] = tmp['value'] * tmp['weight']
_ = tmp.groupby('group')['weighted'].mean()
pd_time = time.perf_counter() - start
print(f"pandas:       {pd_time:.4f}s")
pandas:       0.1252s

Ensuite, Polars en mode immédiat

start = time.perf_counter()
_ = (big_pl
    .filter(pl.col('value') > 0)
    .select(['group', 'value', 'weight'])
    .with_columns(
        (pl.col('value') * pl.col('weight')).alias('weighted'))
    .group_by('group')
    .agg(pl.col('weighted').mean()))
eager_time = time.perf_counter() - start
print(f"Polars eager: {eager_time:.4f}s")
Polars eager: 0.0364s

Et enfin, Polars en mode paresseux

start = time.perf_counter()
_ = (big_pl.lazy()
    .filter(pl.col('value') > 0)
    .select(['group', 'value', 'weight'])
    .with_columns(
        (pl.col('value') * pl.col('weight')).alias('weighted'))
    .group_by('group')
    .agg(pl.col('weighted').mean())
    .collect())
lazy_time = time.perf_counter() - start
print(f"Polars lazy:  {lazy_time:.4f}s")
Polars lazy:  0.0308s

Ce qu’il faut retenir :

  • Pour de petits volumes de données (milliers de lignes), pandas et Polars se comportent de manière similaire — choisissez en fonction de vos préférences d’API et de l’écosystème.

  • Pour des volumes de données moyens à grands (centaines de milliers de lignes et plus), Polars peut être significativement plus rapide grâce à son moteur Rust, à l’exécution parallèle et (en mode paresseux) à l’optimisation des requêtes.

L’API paresseuse est particulièrement puissante lors de la lecture depuis le disque — scan_csv retourne directement un LazyFrame, de sorte que les filtres et projections sont poussés jusqu’au lecteur de fichier.

Astuce

Utilisez pl.scan_csv(path) plutôt que pl.read_csv(path) lorsque vous travaillez avec de gros fichiers CSV. Seules les colonnes et lignes réellement nécessaires seront lues depuis le disque. Voir la documentation E/S de Polars.

19.5. Sources de données en ligne#

Comme dans Pandas, Python permet d’interroger facilement des bases de données en ligne.

Une base de données importante pour les économistes est FRED — une vaste collection de séries temporelles maintenue par la Fed de St. Louis.

La méthode read_csv de Polars peut récupérer des données directement depuis une URL.

Nous utilisons try_parse_dates=True pour analyser automatiquement la colonne de date

fred_url = ('https://fred.stlouisfed.org/graph/fredgraph.csv?'
            'bgcolor=%23e1e9f0&chart_type=line&drp=0&'
            'fo=open%20sans&graph_bgcolor=%23ffffff&'
            'height=450&mode=fred&recession_bars=on&'
            'txtcolor=%23444444&ts=12&tts=12&width=1318&'
            'nt=0&thu=0&trc=0&show_legend=yes&'
            'show_axis_titles=yes&show_tooltip=yes&'
            'id=UNRATE&scale=left&cosd=1948-01-01&'
            'coed=2024-06-01&line_color=%234572a7&'
            'link_values=false&line_style=solid&'
            'mark_type=none&mw=3&lw=2&ost=-99999&'
            'oet=99999&mma=0&fml=a&fq=Monthly&fam=avg&'
            'fgst=lin&fgsnd=2020-02-01&line_index=1&'
            'transformation=lin&vintage_date=2024-07-29&'
            'revision_date=2024-07-29&nd=1948-01-01')
data = pl.read_csv(fred_url, try_parse_dates=True)

Examinons les premières lignes

data.head()
shape: (5, 2)
observation_dateUNRATE
datef64
1948-01-013.4
1948-02-013.8
1948-03-014.0
1948-04-013.9
1948-05-013.5

Et obtenons des statistiques récapitulatives

data.describe()
shape: (9, 3)
statisticobservation_dateUNRATE
strstrf64
"count""918"918.0
"null_count""0"0.0
"mean""1986-03-17 06:30:35.294117"5.693246
"std"null1.710248
"min""1948-01-01"2.5
"25%""1967-02-01"4.4
"50%""1986-04-01"5.5
"75%""2005-05-01"6.7
"max""2024-06-01"14.8

Traçons le taux de chômage de 2006 à 2012

filtered = data.filter(
    (pl.col('observation_date') >= pl.date(2006, 1, 1)) &
    (pl.col('observation_date') <= pl.date(2012, 12, 31))
)

fig, ax = plt.subplots()
ax.plot(filtered['observation_date'].to_list(),
        filtered['UNRATE'].to_list())
ax.set_title('US Unemployment Rate')
ax.set_xlabel('year', fontsize=12)
ax.set_ylabel('%', fontsize=12)
plt.show()
_images/819acd9695c1a6a9590673aa3b29d7e6c3108d13ef9c1331e9807e5466d6ea21.png

Polars prend en charge de nombreux formats de fichiers tels que Excel, JSON, Parquet, ainsi que des connexions directes à des bases de données.

19.6. Exercices#

Exercice 19.1

Avec ces importations :

import datetime as dt
import yfinance as yf

Écrivez un programme pour calculer la variation en pourcentage du prix sur l’année 2021 pour les actions suivantes :

ticker_list = {'INTC': 'Intel',
               'MSFT': 'Microsoft',
               'IBM': 'IBM',
               'BHP': 'BHP',
               'TM': 'Toyota',
               'AAPL': 'Apple',
               'AMZN': 'Amazon',
               'C': 'Citigroup',
               'QCOM': 'Qualcomm',
               'KO': 'Coca-Cola',
               'GOOG': 'Google'}

Voici une fonction qui lit les cours de clôture dans un DataFrame Polars :

def read_data_polars(ticker_list,
                     start=dt.datetime(2021, 1, 1),
                     end=dt.datetime(2021, 12, 31)):
    """
    Read closing price data from Yahoo Finance
    and return a Polars DataFrame.
    """
    dataframes = []

    for tick in ticker_list:
        stock = yf.Ticker(tick)
        prices = stock.history(start=start, end=end)
        df = pl.DataFrame({
            'Date': list(prices.index.date),
            tick: prices['Close'].values
        }).with_columns(pl.col('Date').cast(pl.Date))
        dataframes.append(df)

    result = dataframes[0]
    for df in dataframes[1:]:
        result = result.join(
            df, on='Date', how='full', coalesce=True
        )
    return result.sort('Date')

ticker = read_data_polars(ticker_list)

Note

Les jointures Polars ne garantissent pas l’ordre des lignes en sortie — les clés présentes uniquement d’un côté sont ajoutées plutôt qu’insérées à leur place. Il s’agit du même thème « pas d’indice, pas d’alignement automatique » évoqué plus haut : en l’absence d’étiquettes de ligne sur lesquelles s’aligner, l’ordre est quelque chose que l’on demande explicitement. D’où le sort('Date') avant le retour, dont dépend tout calcul ultérieur de type first()/last().

Complétez le programme pour tracer le résultat sous forme de graphique à barres.

Exercice 19.2

En utilisant read_data_polars de Exercice 19.1, obtenez la variation en pourcentage d’une année sur l’autre pour les indices suivants :

indices_list = {'^GSPC': 'S&P 500',
               '^IXIC': 'NASDAQ',
               '^DJI': 'Dow Jones',
               '^N225': 'Nikkei'}

Tracez le résultat sous forme de graphique en série temporelle.