From 6b8de2538393597654c7d92c92947d006952b8ed Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 31 Jul 2026 08:06:56 +1000 Subject: [PATCH 1/3] Update translation: lectures/polars.md --- lectures/polars.md | 818 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 818 insertions(+) create mode 100644 lectures/polars.md diff --git a/lectures/polars.md b/lectures/polars.md new file mode 100644 index 0000000..d8f64b6 --- /dev/null +++ b/lectures/polars.md @@ -0,0 +1,818 @@ +--- +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: 概述 + Series: Series + DataFrames: DataFrames + DataFrames::Selecting data: 选择数据 + DataFrames::Filtering by conditions: 按条件过滤 + DataFrames::Column expressions: 列表达式 + DataFrames::Missing values: 缺失值 + DataFrames::Visualization: 可视化 + Lazy evaluation: 惰性求值 + Lazy evaluation::Eager vs lazy: 立即执行与惰性执行 + Lazy evaluation::Query optimization: 查询优化 + Lazy evaluation::Performance comparison: 性能比较 + On-line data sources: 在线数据源 + Exercises: 练习 +--- + +(pl)= +```{raw} jupyter +
+ + QuantEcon + +
+``` + +# Polars + +```{index} single: Python; Polars +``` + +除了 Anaconda 中已有的库之外,本讲座还需要以下库: + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install --upgrade polars yfinance +``` + +## 概述 + +[Polars](https://pola.rs/) 是一个用 Rust 编写的快速 Python 数据处理库。 + +由于其性能优势,它作为 {doc}`pandas ` 的现代替代品已经获得了广泛的关注。 + +Polars 在设计时充分考虑了性能和内存效率,主要利用了以下技术: + +* [Apache Arrow 列式存储格式](https://arrow.apache.org/docs/format/Columnar.html),实现快速的数据访问 +* [惰性求值(lazy evaluation)](https://en.wikipedia.org/wiki/Lazy_evaluation),用于优化查询执行 +* 并行处理,充分利用所有可用的 CPU 核心 +* 围绕列表达式构建的富有表现力的 API + +```{tip} +*为什么要考虑用 Polars 替代 pandas?* + +* **内存**:pandas 通常需要相当于数据集大小 5--10 倍的内存;Polars 只需 2--4 倍 +* **速度**:对于许多常见操作,Polars 的速度快 10--100 倍 +* **参见**:[Polars TPC-H 基准测试](https://www.pola.rs/benchmarks/),可查看最新的性能对比结果 +``` + +在整个讲座中,我们假设已经执行了以下导入语句 + +```{code-cell} ipython3 +import polars as pl +import numpy as np +import matplotlib.pyplot as plt +``` + +与 {doc}`pandas` 类似,Polars 定义了两种重要的数据类型:`Series` 和 `DataFrame`。 + +你可以将 `Series` 理解为一列数据,例如某个变量的一组观测值。 + +`DataFrame` 是一个二维对象,用于存储若干相关联的数据列。 + +## Series + +```{index} single: Polars; Series +``` + +我们先从 Series 开始。 + +首先创建一个由四个随机观测值组成的 series + +```{code-cell} ipython3 +s = pl.Series(name='daily returns', values=np.random.randn(4)) +s +``` + +```{note} +与 {doc}`pandas ` 的 Series 不同,Polars 的 Series 没有行索引。 +Polars 是以列为中心的——数据访问是通过列表达式和布尔掩码来管理的,而不是通过行标签。 +更多详情请参阅 [面向 pandas 用户的 Polars 迁移指南](https://docs.pola.rs/user-guide/migration/pandas/)。 +``` + +Polars 的 `Series` 建立在 [Apache Arrow](https://arrow.apache.org/) 数组之上,并支持许多我们熟悉的操作 + +```{code-cell} ipython3 +s * 100 +``` + +绝对值可以通过一个方法来获得 + +```{code-cell} ipython3 +s.abs() +``` + +我们也可以快速获取汇总统计信息 + +```{code-cell} ipython3 +s.describe() +``` + +由于 Polars 没有行索引,带标签的数据需要用 `DataFrame` 来表示。 + +例如,要将股票代码与收益关联起来: + +```{code-cell} ipython3 +df = pl.DataFrame({ + 'company': ['AMZN', 'AAPL', 'MSFT', 'GOOG'], + 'daily returns': np.random.randn(4) +}) +df +``` + +我们通过对某一列进行过滤来访问某个值 + +```{code-cell} ipython3 +df.filter( + pl.col('company') == 'AMZN' +).select('daily returns').item() +``` + +更新数据同样使用表达式,而不是按索引赋值 + +```{code-cell} ipython3 +df = df.with_columns( + pl.when(pl.col('company') == 'AMZN') + .then(0) + .otherwise(pl.col('daily returns')) + .alias('daily returns') +) +df +``` + +我们还可以检查成员关系 + +```{code-cell} ipython3 +'AAPL' in df['company'] +``` + +## DataFrames + +```{index} single: Polars; DataFrames +``` + +`Series` 是单独一列数据,而 `DataFrame` 则包含多个列,每个变量对应一列。 + +和 {doc}`pandas` 一样,我们用 [Penn World Tables](https://www.rug.nl/ggdc/productivity/pwt/pwt-releases/pwt-7.0) 中的数据来进行操作。 + +我们使用 `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 +``` + +### 选择数据 + +我们可以通过切片来选择行,通过列名来选择列 + +```{code-cell} ipython3 +df[2:5] +``` + +要选择特定的列,可以向 `select` 传入一个名称列表 + +```{code-cell} ipython3 +df.select(['country', 'tcgdp']) +``` + +这些操作也可以组合使用 + +```{code-cell} ipython3 +df[2:5].select(['country', 'tcgdp']) +``` + +### 按条件过滤 + +`filter` 方法接受由 `pl.col` 构建的布尔表达式 + +```{code-cell} ipython3 +df.filter(pl.col('POP') >= 20000) +``` + +可以使用 `&`(与)和 `|`(或)来组合多个条件 + +```{code-cell} ipython3 +df.filter( + (pl.col('country').is_in(['Argentina', 'India', 'South Africa'])) & + (pl.col('POP') > 40000) +) +``` + +表达式可以涉及跨列的算术运算 + +```{code-cell} ipython3 +df.filter( + (pl.col('cc') + pl.col('cg') >= 80) & (pl.col('POP') <= 20000) +) +``` + +选择家庭消费占比最大的国家 + +```{code-cell} ipython3 +df.filter(pl.col('cc') == pl.col('cc').max()) +``` + +### 列表达式 + +与 pandas 的一个关键区别在于,Polars 使用**列表达式**来进行转换,而不是逐元素调用 `apply`。 + +下面是一个计算每个数值列最大值的示例 + +```{code-cell} ipython3 +df.select( + pl.col(['year', 'POP', 'XRAT', 'tcgdp', 'cc', 'cg']) + .max() + .name.suffix('_max') +) +``` + +表达式可以在 `with_columns` 内部使用,用于添加或修改列 + +```{code-cell} ipython3 +df.with_columns( + (pl.col('XRAT') / 10).alias('XRAT_scaled'), + pl.col(pl.Float64).round(2) +) +``` + +条件逻辑使用 `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 提供了 `map_elements` 作为逐行应用任意 Python 函数的应急方案, +但它会绕过经过优化的表达式引擎,因此只要存在原生表达式,就应该避免使用它。 +``` + +### 缺失值 + +让我们插入一些空值来演示插补技术 + +```{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 +``` + +将所有空值填充为零 + +```{code-cell} ipython3 +df_nulls.fill_null(0) +``` + +或者用列的均值来填充 + +```{code-cell} ipython3 +cols = ['cc', 'tcgdp', 'POP', 'XRAT'] +df_nulls.with_columns( + pl.col(cols).fill_null(pl.col(cols).mean()) +) +``` + +Polars 还支持向前填充(`fill_null(strategy='forward')`)以及插值。 + +在 scikit-learn 中还有更多[高级插补工具](https://scikit-learn.org/stable/modules/impute.html)可供使用。 + +### 可视化 + +让我们构建一个人均 GDP 列,并绘制出来 + +```{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 +``` + +我们可以直接提取列用于 matplotlib + +```{note} +Polars 还提供了基于 Altair 的内置[绘图 API](https://docs.pola.rs/user-guide/misc/visualization/) +(例如 `df.plot.bar(x=..., y=...)`)。 +为了与本系列讲座的其他部分保持一致,我们在这里使用 matplotlib。 +``` + +```{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() +``` + +## 惰性求值 + +```{index} single: Polars; Lazy Evaluation +``` + +Polars 最强大的特性之一是**惰性求值(lazy evaluation)**。 + +惰性模式并不会立即执行每一个操作,而是先收集完整的查询计划,再在运行前对其进行优化。 + +### 立即执行与惰性执行 + +```{code-cell} ipython3 +# 重新加载数据集 +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) +``` + +**立即执行(eager)** API 会立即执行(就像 pandas 那样) + +```{code-cell} ipython3 +result_eager = (df_full + .filter(pl.col('tcgdp') > 1000) + .select(['country', 'year', 'tcgdp']) + .sort('tcgdp', descending=True) +) +result_eager.head() +``` + +**惰性(lazy)** API 则会构建一个查询计划 + +```{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()) +``` + +调用 `collect` 来执行该计划 + +```{code-cell} ipython3 +result_lazy = lazy_query.collect() +result_lazy.head() +``` + +### 查询优化 + +惰性执行引擎会自动应用若干优化: + +* **谓词下推**——过滤操作会被尽可能提前应用 +* **投影下推**——只从数据源读取所需的列 +* **公共子表达式消除**——重复的计算会被合并 + +让我们看看 Polars 是如何重写一个多步骤查询的 + +```{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()) +``` + +执行该计划便可得到最终结果 + +```{code-cell} ipython3 +optimized.collect() +``` + +### 性能比较 + +让我们在同一任务上比较 pandas、Polars 立即执行模式和 Polars 惰性执行模式。 + +我们先从一个小数据集(上面使用过的 Penn World Tables 数据)开始,说明对于小数据而言,各方案之间的差异微不足道 + +```{code-cell} ipython3 +import pandas as pd +import time + +# 小数据集 -- Penn World Tables(约 8 行) +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) +``` + +现在我们对每个库中相同的过滤-选择-排序操作进行计时 + +```{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 立即执行模式 +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") +``` + +对于寥寥数行的数据,速度差异并不重要——你可以选择自己更习惯使用的 API。 + +现在让我们把数据规模扩大到 500 万行,此时差异就会变得明显。 + +我们的任务是:过滤出 `value > 0` 的行,计算加权乘积 +`value * weight`,然后对每个分组内该乘积取均值—— +也就是一个分组加权平均。 + +```{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 +}) +``` + +首先是 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") +``` + +接下来是 Polars 的立即执行模式 + +```{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") +``` + +最后是 Polars 的惰性执行模式 + +```{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") +``` + +由此可以得出以下结论: + +* 对于**小数据**(数千行),pandas 与 Polars 的表现相似—— + 可以根据 API 偏好和生态系统兼容性来选择。 +* 对于**中大型数据**(数十万行及以上),得益于其 Rust 引擎、 + 并行执行以及(在惰性模式下的)查询优化,Polars 可以显著更快。 + +当从磁盘读取数据时,惰性 API 尤其强大——`scan_csv` 会直接返回一个 `LazyFrame`,因此过滤和投影操作会被下推到文件读取器中执行。 + +```{tip} +在处理大型 CSV 文件时,使用 `pl.scan_csv(path)` 而不是 `pl.read_csv(path)`。 +这样只有你实际需要的列和行才会从磁盘中被读取。 +参见 [Polars I/O 文档](https://docs.pola.rs/user-guide/io/csv/)。 +``` + +## 在线数据源 + +```{index} single: Data Sources +``` + +与 {doc}`pandas` 一样,Python 可以非常方便地查询在线数据库。 + +对经济学家而言,一个重要的数据库是 [FRED](https://fred.stlouisfed.org/)——由圣路易斯联储维护的庞大时间序列数据集。 + +Polars 的 `read_csv` 可以直接从 URL 获取数据。 + +我们使用 `try_parse_dates=True` 来自动解析日期列 + +```{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) +``` + +让我们查看前几行 + +```{code-cell} ipython3 +data.head() +``` + +以及获取汇总统计信息 + +```{code-cell} ipython3 +data.describe() +``` + +绘制 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 支持[多种文件格式](https://docs.pola.rs/user-guide/io/),包括 Excel、JSON、Parquet,以及直接连接数据库。 + +## 练习 + +```{exercise-start} +:label: pl_ex1 +``` + +使用以下导入: + +```{code-cell} ipython3 +import datetime as dt +import yfinance as yf +``` + +编写一个程序,计算以下几只股票在 2021 年间价格的百分比变化: + +```{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'} +``` + +下面是一个将收盘价读入 Polars DataFrame 的函数: + +```{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} +Polars 的连接(join)操作不保证输出行的顺序—— +只匹配一侧的键值会被追加,而不是被插入到相应位置。 +这和前面提到的"没有索引、没有自动对齐"的主题是一致的: +由于没有行标签可供对齐,排序需要我们显式地去请求。 +因此在返回结果之前会调用 `sort('Date')`, +后续任何 `first()`/`last()` 计算都依赖于这一排序结果。 +``` + +请补充完整该程序,将结果绘制为柱状图。 + +```{exercise-end} +``` + +```{solution-start} pl_ex1 +:class: dropdown +``` + +使用 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) +``` + +直接使用 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 +``` + +使用 {ref}`pl_ex1` 中的 `read_data_polars`,求出以下指数的同比百分比变化: + +```{code-cell} ipython3 +indices_list = {'^GSPC': 'S&P 500', + '^IXIC': 'NASDAQ', + '^DJI': 'Dow Jones', + '^N225': 'Nikkei'} +``` + +将结果绘制为时间序列图。 + +```{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') +) +``` + +使用分组操作计算逐年收益: + +```{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) +``` + +汇总统计信息: + +```{code-cell} ipython3 +yearly_returns.select(list(indices_list.values())).describe() +``` + +将每个指数绘制在一个子图中: + +```{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 From 3b6cb9a19b45bc764fd2e6bc70c9b46901980f65 Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 31 Jul 2026 08:06:57 +1000 Subject: [PATCH 2/3] Update translation: .translate/state/polars.md.yml --- .translate/state/polars.md.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .translate/state/polars.md.yml 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 From 20daeddb2e0a32c23753bca27708801555d7cc1d Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Fri, 31 Jul 2026 08:06:58 +1000 Subject: [PATCH 3/3] Update translation: lectures/_toc.yml --- lectures/_toc.yml | 1 + 1 file changed, 1 insertion(+) 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: