今天我們來做一些學習前的準備工作。
本日大綱如下:
from datetime import date
import polars as pl
df = pl.DataFrame(
{
"col1": [1, 2, 3],
"col2": ["x", "y", "z"],
"col3": pl.date_range(
date(2025, 1, 1), date(2025, 1, 3), eager=True
),
}
)
shape: (3, 3)
┌──────┬──────┬────────────┐
│ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ date │
╞══════╪══════╪════════════╡
│ 1 ┆ x ┆ 2025-01-01 │
│ 2 ┆ y ┆ 2025-01-02 │
│ 3 ┆ z ┆ 2025-01-03 │
└──────┴──────┴────────────┘
使用pip一鍵安裝:
pip install polars
建議使用傳統的Jupyter Notebook。
但是如果您有著勇敢冒險精神的話,marimo絕對值得一試,而我會試著在[Day29]中簡單介紹marimo。
全部程式碼皆使用Ruff排版。針對引入模組,為節省版面空間,可能使用isort排版。
多數情況,我會使用Fluent API的方式來編寫query,也就是將操作串接在一起。相對於在每行行末加上\
,用以接續多行,如:
df\
.select(pl.col("col1"))\
.filter(pl.col("col1").gt(0).and_(pl.col("col1").lt(3)))\
.sort(by="col1", descending=True)
我比較習慣在外頭包上一個()
,如:
(
df.select(pl.col("col1"))
.filter(pl.col("col1").gt(0).and_(pl.col("col1").lt(3)))
.sort(by="col1", descending=True)
)
最後,下面這種將不同操作指定給多個暫時變數的寫法,我會視為是一種anti-pattern。
selected_df = df.select(pl.col("col1"))
filtered_df = selected_df.filter(
pl.col("col1").gt(0).and_(pl.col("col1").lt(3))
)
sorted_df = filtered_df.sort(by="col1", descending=True)
原因是定義多個暫時變數會增加記憶體消耗。
如果這些暫時變數在之後的程式中不會用到,僅是做為中間的計算過程,我會建議使用Fluent API的寫法。
pl.Config可以幫助我們依照使用者喜好,在全域或是局部,調整Polars的參數。
以下舉一個我最常使用的例子,使用pl.Config.set_tbl_cols()調整顯示的列數。
使用pl.Config.set_tbl_cols(2)
在全域範圍內,將顯式列數設定為兩列。
pl.Config.set_tbl_cols(2)
df
shape: (3, 3)
┌──────┬───┬────────────┐
│ col1 ┆ … ┆ col3 │
│ --- ┆ ┆ --- │
│ i64 ┆ ┆ date │
╞══════╪═══╪════════════╡
│ 1 ┆ … ┆ 2025-01-01 │
│ 2 ┆ … ┆ 2025-01-02 │
│ 3 ┆ … ┆ 2025-01-03 │
└──────┴───┴────────────
之後可以使用pl.Config.restore_defaults()在全域範圍內,恢復原始設定。
pl.Config.restore_defaults() # global
df
shape: (3, 3)
┌──────┬──────┬────────────┐
│ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ date │
╞══════╪══════╪════════════╡
│ 1 ┆ x ┆ 2025-01-01 │
│ 2 ┆ y ┆ 2025-01-02 │
│ 3 ┆ z ┆ 2025-01-03 │
└──────┴──────┴────────────┘
可以使用context manager的型式,在局部設定顯示列數為兩列,如:
with pl.Config() as cfg:
cfg.set_tbl_cols(2)
print(df)
shape: (3, 3)
┌──────┬───┬────────────┐
│ col1 ┆ … ┆ col3 │
│ --- ┆ ┆ --- │
│ i64 ┆ ┆ date │
╞══════╪═══╪════════════╡
│ 1 ┆ … ┆ 2025-01-01 │
│ 2 ┆ … ┆ 2025-01-02 │
│ 3 ┆ … ┆ 2025-01-03 │
└──────┴───┴────────────┘
此外,Polars還提供了另一種比較方便的寫法,讓使用者可以將參數設定於pl.Config
中,如:
with pl.Config(tbl_cols=2):
print(df)
shape: (3, 3)
┌──────┬───┬────────────┐
│ col1 ┆ … ┆ col3 │
│ --- ┆ ┆ --- │
│ i64 ┆ ┆ date │
╞══════╪═══╪════════════╡
│ 1 ┆ … ┆ 2025-01-01 │
│ 2 ┆ … ┆ 2025-01-02 │
│ 3 ┆ … ┆ 2025-01-03 │
└──────┴───┴────────────┘
請留意,如果使用此種方法時,需要記得將開頭的「"set_"」去掉以做為參數。
可以使用pl.Config.save_to_file()將常用的config存為JSON格式。
pl.Config.save_to_file("pl_config.json")
{
"environment": {
"POLARS_AUTO_STRUCTIFY": null,
"POLARS_ENGINE_AFFINITY": null,
"POLARS_FMT_MAX_COLS": null,
"POLARS_FMT_MAX_ROWS": null,
"POLARS_FMT_NUM_DECIMAL": null,
"POLARS_FMT_NUM_GROUP_SEPARATOR": null,
"POLARS_FMT_NUM_LEN": null,
"POLARS_FMT_STR_LEN": null,
"POLARS_FMT_TABLE_CELL_ALIGNMENT": null,
"POLARS_FMT_TABLE_CELL_LIST_LEN": null,
"POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT": null,
"POLARS_FMT_TABLE_DATAFRAME_SHAPE_BELOW": null,
"POLARS_FMT_TABLE_FORMATTING": null,
"POLARS_FMT_TABLE_HIDE_COLUMN_DATA_TYPES": null,
"POLARS_FMT_TABLE_HIDE_COLUMN_NAMES": null,
"POLARS_FMT_TABLE_HIDE_COLUMN_SEPARATOR": null,
"POLARS_FMT_TABLE_HIDE_DATAFRAME_SHAPE_INFORMATION": null,
"POLARS_FMT_TABLE_INLINE_COLUMN_DATA_TYPE": null,
"POLARS_FMT_TABLE_ROUNDED_CORNERS": null,
"POLARS_MAX_EXPR_DEPTH": null,
"POLARS_STREAMING_CHUNK_SIZE": null,
"POLARS_TABLE_WIDTH": null,
"POLARS_VERBOSE": null,
"POLARS_WARN_UNSTABLE": null
},
"direct": {
"set_fmt_float": "mixed",
"set_float_precision": null,
"set_thousands_separator": "",
"set_decimal_separator": ".",
"set_trim_decimal_zeros": false
}
}
之後可以使用pl.Config.load_from_file()將config讀入。這邊需留意,Polars讀取config後,會接著進行config內的設定。
pl.Config.load_from_file("pl_config.json")
我認為Polars的核心思想為:挑選正確的context並搭配合適的expression來達成目的。
Context為Polars表達操作意圖的稱呼,可以分為三類操作:
DataFrame.with_columns()
及DataFrame.select()
)。DataFrame.filter()
)。DataFrame.group_by().agg()
)。Expression則是Polars表達具體操作的稱呼,同一個expression搭配不同的context將會產生不同的結果。
例如有一個expression為pl.col("col1").add(1)
,當其用在DataFrame.select()
中僅會得到一個單列的dataframe:
df.select(pl.col("col1").add(1))
shape: (3, 1)
┌──────┐
│ col1 │
│ --- │
│ i64 │
╞══════╡
│ 2 │
│ 3 │
│ 4 │
└──────┘
可以想成選擇出「"col1"」列後加1。
但當其用在DataFrame.with_columns()
中,則會得到三列的dataframe:
df.with_columns(pl.col("col1").add(1))
shape: (3, 3)
┌──────┬──────┬────────────┐
│ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ date │
╞══════╪══════╪════════════╡
│ 2 ┆ x ┆ 2025-01-01 │
│ 3 ┆ y ┆ 2025-01-02 │
│ 4 ┆ z ┆ 2025-01-03 │
└──────┴──────┴────────────┘
可以想成在原先dataframe的最後新增一列。但因為新增的列名與原先的列名一樣皆為「"col1"」,所以會將計算後的結果「貼」至原先「"col1"」列的位置。
必須特別注意的一點是,所有的操作都不會mutate
原先的dataframe。每一個context在操作後,都會返回一個新的dataframe。
Lazy模式是Polars提升效率的利器,但是為方便系列文說明,在基本介紹([Day02]~[Day18])部份,我將採用eager模式編寫範例。而Lazy模式將會在[Day19]介紹,並於[Day20]~[Day24]中應用。
在Polars的API文件,建置有AI chatbot,對尚未熟悉Polars操作的朋友們,是個方便的輔助工具。