diff --git a/.translate/state/polars.md.yml b/.translate/state/polars.md.yml new file mode 100644 index 0000000..13fcdbf --- /dev/null +++ b/.translate/state/polars.md.yml @@ -0,0 +1,6 @@ +source-sha: 1c60c7df6e0c6fbebba3df2f7a9cb1b2c0e59172 +synced-at: "2026-07-30" +model: claude-sonnet-5 +mode: NEW +section-count: 6 +tool-version: 0.24.0 diff --git a/lectures/_toc.yml b/lectures/_toc.yml index 0ea886b..2e5834c 100644 --- a/lectures/_toc.yml +++ b/lectures/_toc.yml @@ -31,6 +31,7 @@ parts: chapters: - file: pandas - file: pandas_panel + - file: polars - caption: More Python Programming numbered: true chapters: diff --git a/lectures/polars.md b/lectures/polars.md new file mode 100644 index 0000000..a2a6158 --- /dev/null +++ b/lectures/polars.md @@ -0,0 +1,824 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.16.7 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +translation: + title: Polars + headings: + Overview: Aperçu général + Series: Series + DataFrames: DataFrames + DataFrames::Selecting data: Sélection des données + DataFrames::Filtering by conditions: Filtrage par conditions + DataFrames::Column expressions: Expressions de colonnes + DataFrames::Missing values: Valeurs manquantes + DataFrames::Visualization: Visualisation + Lazy evaluation: Évaluation paresseuse + Lazy evaluation::Eager vs lazy: Mode immédiat vs mode paresseux + Lazy evaluation::Query optimization: Optimisation des requêtes + Lazy evaluation::Performance comparison: Comparaison des performances + On-line data sources: Sources de données en ligne + Exercises: Exercices +--- + +(pl)= +```{raw} jupyter +
+ + QuantEcon + +
+``` + +# Polars + +```{index} single: Python; Polars +``` + +En plus de ce qui est fourni dans Anaconda, ce cours nécessitera les bibliothèques suivantes : + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install --upgrade polars yfinance +``` + +## Aperçu général + +[Polars](https://pola.rs/) 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 à {doc}`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](https://arrow.apache.org/docs/format/Columnar.html) pour un accès rapide aux données +* L'[évaluation paresseuse](https://en.wikipedia.org/wiki/Lazy_evaluation) 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 + +```{tip} +*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](https://www.pola.rs/benchmarks/) pour des comparaisons de performance à jour +``` + +Tout au long du cours, nous supposerons que les importations suivantes ont été effectuées + +```{code-cell} ipython3 +import polars as pl +import numpy as np +import matplotlib.pyplot as plt +``` + +Comme {doc}`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. + +## Series + +```{index} single: Polars; Series +``` + +Commençons par les Series. + +Nous commençons par créer une série de quatre observations aléatoires + +```{code-cell} ipython3 +s = pl.Series(name='daily returns', values=np.random.randn(4)) +s +``` + +```{note} +Contrairement aux Series {doc}`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](https://docs.pola.rs/user-guide/migration/pandas/) pour plus de détails. +``` + +Les `Series` Polars sont construites sur les tableaux [Apache Arrow](https://arrow.apache.org/) et prennent en charge de nombreuses opérations familières + +```{code-cell} ipython3 +s * 100 +``` + +Les valeurs absolues sont disponibles sous forme de méthode + +```{code-cell} ipython3 +s.abs() +``` + +Nous pouvons également obtenir rapidement des statistiques récapitulatives + +```{code-cell} ipython3 +s.describe() +``` + +É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 : + +```{code-cell} ipython3 +df = pl.DataFrame({ + 'company': ['AMZN', 'AAPL', 'MSFT', 'GOOG'], + 'daily returns': np.random.randn(4) +}) +df +``` + +Nous accédons à une valeur en filtrant sur une expression de colonne + +```{code-cell} ipython3 +df.filter( + pl.col('company') == 'AMZN' +).select('daily returns').item() +``` + +Les mises à jour utilisent également des expressions plutôt qu'une affectation par indice + +```{code-cell} ipython3 +df = df.with_columns( + pl.when(pl.col('company') == 'AMZN') + .then(0) + .otherwise(pl.col('daily returns')) + .alias('daily returns') +) +df +``` + +Nous pouvons également vérifier l'appartenance + +```{code-cell} ipython3 +'AAPL' in df['company'] +``` + +## DataFrames + +```{index} single: Polars; DataFrames +``` + +Alors qu'une `Series` est une seule colonne de données, un `DataFrame` comporte plusieurs colonnes, une pour chaque variable. + +Comme dans {doc}`pandas`, travaillons avec les données des [Penn World Tables](https://www.rug.nl/ggdc/productivity/pwt/pwt-releases/pwt-7.0). + +Nous les lisons à l'aide de `pl.read_csv` + +```{code-cell} ipython3 +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 +``` + +### Sélection des données + +Nous pouvons sélectionner des lignes par découpage (slicing) et des colonnes par nom + +```{code-cell} ipython3 +df[2:5] +``` + +Pour sélectionner des colonnes spécifiques, passez une liste de noms à `select` + +```{code-cell} ipython3 +df.select(['country', 'tcgdp']) +``` + +Ces opérations peuvent être combinées + +```{code-cell} ipython3 +df[2:5].select(['country', 'tcgdp']) +``` + +### Filtrage par conditions + +La méthode `filter` accepte des expressions booléennes construites à partir de `pl.col` + +```{code-cell} ipython3 +df.filter(pl.col('POP') >= 20000) +``` + +Plusieurs conditions peuvent être combinées avec `&` (et) et `|` (ou) + +```{code-cell} ipython3 +df.filter( + (pl.col('country').is_in(['Argentina', 'India', 'South Africa'])) & + (pl.col('POP') > 40000) +) +``` + +Les expressions peuvent impliquer des opérations arithmétiques entre colonnes + +```{code-cell} ipython3 +df.filter( + (pl.col('cc') + pl.col('cg') >= 80) & (pl.col('POP') <= 20000) +) +``` + +Sélectionnons le pays ayant la part de consommation des ménages la plus élevée + +```{code-cell} ipython3 +df.filter(pl.col('cc') == pl.col('cc').max()) +``` + +### 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 + +```{code-cell} ipython3 +df.select( + pl.col(['year', 'POP', 'XRAT', 'tcgdp', 'cc', 'cg']) + .max() + .name.suffix('_max') +) +``` + +Les expressions peuvent être utilisées dans `with_columns` pour ajouter ou modifier des colonnes + +```{code-cell} ipython3 +df.with_columns( + (pl.col('XRAT') / 10).alias('XRAT_scaled'), + pl.col(pl.Float64).round(2) +) +``` + +La logique conditionnelle utilise `pl.when(...).then(...).otherwise(...)` + +```{code-cell} ipython3 +df.with_columns( + pl.when(pl.col('POP') >= 20000) + .then(pl.col('POP')) + .otherwise(None) + .alias('POP_filtered') +).select(['country', 'POP', 'POP_filtered']) +``` + +```{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. +``` + +### Valeurs manquantes + +Insérons quelques valeurs nulles pour démontrer les techniques d'imputation + +```{code-cell} ipython3 +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 +``` + +Remplissons toutes les valeurs nulles par zéro + +```{code-cell} ipython3 +df_nulls.fill_null(0) +``` + +Ou remplissons avec les moyennes des colonnes + +```{code-cell} ipython3 +cols = ['cc', 'tcgdp', 'POP', 'XRAT'] +df_nulls.with_columns( + pl.col(cols).fill_null(pl.col(cols).mean()) +) +``` + +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](https://scikit-learn.org/stable/modules/impute.html) disponibles dans scikit-learn. + +### Visualisation + +Construisons une colonne de PIB par habitant et traçons-la + +```{code-cell} ipython3 +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 +``` + +Nous pouvons extraire directement les colonnes pour matplotlib + +```{note} +Polars fournit également une [API de tracé intégrée](https://docs.pola.rs/user-guide/misc/visualization/) +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. +``` + +```{code-cell} ipython3 +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() +``` + +## Évaluation paresseuse + +```{index} single: Polars; Lazy Evaluation +``` + +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. + +### Mode immédiat vs mode paresseux + +```{code-cell} ipython3 +# 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) + +```{code-cell} ipython3 +result_eager = (df_full + .filter(pl.col('tcgdp') > 1000) + .select(['country', 'year', 'tcgdp']) + .sort('tcgdp', descending=True) +) +result_eager.head() +``` + +L'API **paresseuse** (lazy) construit plutôt un plan de requête + +```{code-cell} ipython3 +lazy_query = (df_full.lazy() + .filter(pl.col('tcgdp') > 1000) + .select(['country', 'year', 'tcgdp']) + .sort('tcgdp', descending=True) +) +print(lazy_query.explain()) +``` + +Appelez `collect` pour exécuter le plan + +```{code-cell} ipython3 +result_lazy = lazy_query.collect() +result_lazy.head() +``` + +### 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 + +```{code-cell} ipython3 +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()) +``` + +L'exécution du plan nous donne le résultat final + +```{code-cell} ipython3 +optimized.collect() +``` + +### 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 + +```{code-cell} ipython3 +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 + +```{code-cell} ipython3 +# 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") +``` + +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. + +```{code-cell} ipython3 +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 + +```{code-cell} ipython3 +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") +``` + +Ensuite, Polars en mode immédiat + +```{code-cell} ipython3 +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") +``` + +Et enfin, Polars en mode paresseux + +```{code-cell} ipython3 +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") +``` + +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. + +```{tip} +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](https://docs.pola.rs/user-guide/io/csv/). +``` + +## Sources de données en ligne + +```{index} single: Data Sources +``` + +Comme dans {doc}`pandas`, Python permet d'interroger facilement des bases de données en ligne. + +Une base de données importante pour les économistes est [FRED](https://fred.stlouisfed.org/) --- 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 + +```{code-cell} ipython3 +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 + +```{code-cell} ipython3 +data.head() +``` + +Et obtenons des statistiques récapitulatives + +```{code-cell} ipython3 +data.describe() +``` + +Traçons le taux de chômage de 2006 à 2012 + +```{code-cell} ipython3 +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() +``` + +Polars prend en charge [de nombreux formats de fichiers](https://docs.pola.rs/user-guide/io/) tels que Excel, JSON, Parquet, ainsi que des connexions directes à des bases de données. + +## Exercices + +```{exercise-start} +:label: pl_ex1 +``` + +Avec ces importations : + +```{code-cell} ipython3 +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 : + +```{code-cell} ipython3 +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 : + +```{code-cell} ipython3 +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. + +```{exercise-end} +``` + +```{solution-start} pl_ex1 +:class: dropdown +``` + +Calculons les variations en pourcentage à l'aide d'expressions Polars : + +```{code-cell} ipython3 +price_change = ticker.select([ + ((pl.col(tick).last() / pl.col(tick).first() - 1) * 100) + .alias(tick) + for tick in ticker_list.keys() +]).transpose( + include_header=True, + header_name='ticker', + column_names=['pct_change'] +).with_columns( + pl.col('ticker') + .replace_strict(ticker_list, default=pl.col('ticker')) + .alias('company') +).sort('pct_change') + +print(price_change) +``` + +Traçons les résultats directement avec matplotlib : + +```{code-cell} ipython3 +companies = price_change['company'].to_list() +changes = price_change['pct_change'].to_list() +colors = ['red' if x < 0 else 'blue' for x in changes] + +fig, ax = plt.subplots(figsize=(10, 8)) +ax.bar(companies, changes, color=colors) +ax.set_xlabel('stock', fontsize=12) +ax.set_ylabel('percentage change in price', fontsize=12) +plt.xticks(rotation=45, ha='right') +plt.tight_layout() +plt.show() +``` + +```{solution-end} +``` + + +```{exercise-start} +:label: pl_ex2 +``` + +En utilisant `read_data_polars` de {ref}`pl_ex1`, obtenez la variation en pourcentage d'une année sur l'autre pour les indices suivants : + +```{code-cell} ipython3 +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. + +```{exercise-end} +``` + +```{solution-start} pl_ex2 +:class: dropdown +``` + +```{code-cell} ipython3 +indices_data = read_data_polars( + indices_list, + start=dt.datetime(1971, 1, 1), + end=dt.datetime(2021, 12, 31) +) + +indices_data = indices_data.with_columns( + pl.col('Date').dt.year().alias('year') +) +``` + +Calculons les rendements annuels à l'aide d'opérations de regroupement : + +```{code-cell} ipython3 +yearly_returns = indices_data.group_by('year').agg([ + *[pl.col(idx).drop_nulls().first().alias(f'{idx}_first') + for idx in indices_list], + *[pl.col(idx).drop_nulls().last().alias(f'{idx}_last') + for idx in indices_list] +]) + +for idx, name in indices_list.items(): + yearly_returns = yearly_returns.with_columns( + ((pl.col(f'{idx}_last') - pl.col(f'{idx}_first')) + / pl.col(f'{idx}_first') * 100).alias(name) + ) + +yearly_returns = (yearly_returns + .select(['year', *indices_list.values()]) + .sort('year') +) +print(yearly_returns) +``` + +Statistiques récapitulatives : + +```{code-cell} ipython3 +yearly_returns.select(list(indices_list.values())).describe() +``` + +Traçons chaque indice dans un sous-graphique : + +```{code-cell} ipython3 +fig, axes = plt.subplots(2, 2, figsize=(12, 10)) +years = yearly_returns['year'].to_list() + +for iter_, ax in enumerate(axes.flatten()): + name = list(indices_list.values())[iter_] + values = yearly_returns[name].to_list() + ax.plot(years, values, 'o-', linewidth=2, markersize=4) + ax.axhline(y=0, color='k', linestyle='--', alpha=0.3) + ax.set_ylabel('yearly return (%)', fontsize=12) + ax.set_xlabel('year', fontsize=12) + ax.set_title(name, fontsize=12) + +plt.tight_layout() +plt.show() +``` + +```{solution-end} +``` \ No newline at end of file