🧭 序言:为什么选择 Polars + NumPy,而不是 Pandas + NumPy?- 🦀 底层用 Rust 实现,天然多线程:同等数据量下,Polars 通常比 Pandas 快 5~20 倍,大文件处理尤为明显。
- 🏹 Apache Arrow 内存格式:列式存储,内存占用更低,与 NumPy 共享内存零拷贝互转。
- ⚡ 懒加载 + 流式处理:
scan_* + sink_* 可以处理远超内存大小的文件,全程不将数据完整载入内存。 - 🧩 表达式系统现代清晰:链式调用、
with_columns / filter / group_by 语义明确,没有 Pandas 的隐式索引对齐问题。 - 🔢 与 NumPy 配合自然:Polars DataFrame 可直接
.to_numpy() 转矩阵做科学计算,结果再转回 Polars,两者分工明确——Polars 管数据,NumPy 管计算。 - 🚫 没有历史包袱:无
inplace 歧义,无隐式 copy 问题,行为可预期。
Polars 负责高效地读、写、清洗、变换数据;
NumPy 负责数值计算与矩阵运算。
📥 读取 & 保存数据
立即加载(Eager)—— 直接读入内存
df = pl.read_csv("file.csv")df = pl.read_parquet("file.parquet")df = pl.read_excel("file.xlsx")df = pl.read_json("file.json")df = pl.read_ndjson("file.ndjson")# 每行一个JSON
懒加载(Lazy)—— 大文件首选 ⚡
lf = pl.scan_csv("file.csv")lf = pl.scan_parquet("file.parquet")lf = pl.scan_ndjson("file.ndjson")df =( pl.scan_csv("big_file.csv").filter(pl.col("age")>18).select(["name","age"]).collect())
Eager vs Lazy
| read_* | scan_* |
|---|
| DataFrame | LazyFrame |
| | .collect() |
| | |
保存(Eager)
df.write_csv("file.csv")df.write_parquet("file.parquet")df.write_excel("file.xlsx")df.write_json("file.json")df.write_ndjson("file.ndjson")
流式保存(Lazy + Streaming)
( pl.scan_csv("big_file.csv").filter(pl.col("age")>18).sink_csv("output.csv"))lf.sink_parquet("output.parquet")lf.sink_ndjson("output.ndjson")lf.sink_ipc("output.arrow")
常用参数(以 read_csv 为例)
df = pl.read_csv("file.csv", has_header =True, skip_rows =2, columns =["col1","col2"], n_rows =1000, separator =",", encoding ="utf8", schema ={"id": pl.Int32,"name": pl.Utf8},)
👀 查(Inspect / Select)
df.columnsdf.dtypesdf.schemadf.shapedf.head(5)df.tail(5)df.sample(5)df.describe()df.select(["col1","col2"])df["col1"]# 单列df[:,0]# 按位置选单列df[:,[0,1,2]]# 按位置选多列df.filter(pl.col("age")>18)df.filter(pl.col("city")=="北京")df.filter(pl.col("x").is_null())df[0:5]# 切片取行df.sort("col")df.sort("col", descending=True)df.sort(["col1","col2"], descending=[True,False])
✏️ 改(Modify)
df.rename({"old_name":"new_name"})df.with_columns(pl.col("age").cast(pl.Int32))# 条件替换df.with_columns( pl.when(pl.col("score")>60).then(pl.lit("pass")).otherwise(pl.lit("fail")).alias("result"))df.fill_null(0)df.with_columns(pl.col("age").fill_null(0))# 字符串操作df.with_columns(pl.col("name").str.to_uppercase())df.with_columns(pl.col("name").str.replace("old","new"))df.with_columns(pl.col("name").str.strip_chars())# 时间操作df.with_columns(pl.col("date_str").str.to_date("%Y-%m-%d"))df.with_columns(pl.col("time").dt.year())df.with_columns(pl.col("time").dt.month())df.with_columns(pl.col("time").dt.hour())df.with_columns((pl.col("end")- pl.col("start")).dt.total_seconds().alias("duration"))# List 列操作df.with_columns(pl.col("tags").str.split(","))df.with_columns(pl.col("scores").list.mean())df.with_columns(pl.col("scores").list.eval(pl.element()*2))df.explode("tags")
➕ 增(Add Columns / Rows)
# 增加列df.with_columns((pl.col("price")*1.1).alias("price_new"), pl.lit(0).alias("flag"),)# 增加行索引列df.with_row_index(name="index", offset=0)# 纵向拼接(行增加)pl.concat([df1, df2])# 横向拼接(列增加)pl.concat([df1, df2], how="horizontal")
➖ 删(Drop / Remove)
# 删列df.drop(["col1","col2"])# 删行(条件过滤)df.filter(pl.col("age")>=18)df.filter(~pl.col("city").is_null())# 删含空值的行df.drop_nulls(subset=["col1"])df.filter(pl.sum_horizontal(pl.all().is_null())<=2)# 允许最多2个空值# 去重df.unique(subset=["col1","col2"], keep="first")
🔢 分组聚合(group_by + agg)
df.group_by("col1").agg([ pl.col("col2").sum().alias("sum"), pl.col("col2").mean().alias("mean"), pl.len().alias("count"), pl.col("col2").n_unique().alias("unique_count"), pl.col("col2").first().alias("first"), pl.col("col2").last().alias("last"), pl.col("col2").list().alias("list_vals"),])
⚠️ group_by 结果顺序随机,需固定顺序请接 .sort("col1")
🔄 透视表(pivot)
df.pivot( on="subject", on_columns=["maths","physics"], index="name", values="test_1", aggregate_function="first")
🔗 时间对齐插值(join_asof)
最近邻插值(nearest)
df_joined = df_target_time.join_asof( df_task, left_on ="target_time", right_on ="task_time", strategy ="nearest")
线性插值(linear)
公式:$$v = v_0 + \frac{(t - t_0)}{(t_1 - t_0)} \times (v_1 - v_0)$$
data_columns = df_task_data.columnsdf_left = df_target_time.join_asof( df_task, left_on="target_time", right_on="task_time", strategy="backward",).rename({"task_time":"t0",**{c:f"{c}_v0"for c in data_columns}})df_right = df_target_time.join_asof( df_task, left_on="target_time", right_on="task_time", strategy="forward",).rename({"task_time":"t1",**{c:f"{c}_v1"for c in data_columns}})df_merged = df_left.with_columns([ df_right["t1"],*[df_right[f"{c}_v1"]for c in data_columns],])t = pl.col("target_time").dt.timestamp("us")t0 = pl.col("t0").dt.timestamp("us")t1 = pl.col("t1").dt.timestamp("us")df_result = df_merged.with_columns([( pl.col(f"{c}_v0")+(t - t0)/(t1 - t0)*(pl.col(f"{c}_v1")- pl.col(f"{c}_v0"))).alias(c)for c in data_columns]).select(data_columns)# 边界值用前向/后向填充兜底df_result = df_result.with_columns([ pl.col(c).fill_null(strategy="forward").fill_null(strategy="backward")for c in data_columns])
🔀 Polars ↔ NumPy 互转
类型兼容速览
基础转换
cols = df.columnsarr = df.to_numpy()df2 = pl.DataFrame(arr, schema=cols)
时间列处理
方案 A:剔除时间列,用完拼回
time_col = df["time"]df_numeric = df.drop("time")arr = df_numeric.to_numpy()df_result = pl.DataFrame(arr, schema=df_numeric.columns)df_result = df_result.with_columns(time_col)
方案 B:时间列转整数参与运算
df2 = df.with_columns( pl.col("time").dt.timestamp("us").alias("time_us")).drop("time")arr = df2.to_numpy()df_result = pl.DataFrame(arr, schema=df2.columns)df_result = df_result.with_columns( pl.col("time_us").cast(pl.Int64).cast(pl.Datetime("us")).alias("time")).drop("time_us")
方案 C:时间列转 NumPy 用于画图
x = df["time"].to_numpy()# → np.datetime64[],Matplotlib 原生支持y = df["value"].to_numpy()plt.plot(x, y)plt.gcf().autofmt_xdate()plt.show()
Bool 列处理
# 转 NumPy 前:Bool → Int8df2 = df.with_columns(pl.col("flag").cast(pl.Int8))arr = df2.to_numpy()# 转回 Polars 后:Int8 → Booldf_result = pl.DataFrame(arr, schema=df2.columns)df_result = df_result.with_columns(pl.col("flag").cast(pl.Boolean))
只转数值列的通用模板
numeric_types =[pl.Int8, pl.Int16, pl.Int32, pl.Int64, pl.Float32, pl.Float64]num_cols =[c for c, t inzip(df.columns, df.dtypes)if t in numeric_types]other_cols = df.select([c for c in df.columns if c notin num_cols])# NumPy 运算arr = df.select(num_cols).to_numpy()arr = arr *2# 任意 NumPy 运算# 转回并拼接非数值列df_result = pl.DataFrame(arr, schema=num_cols)df_result = pl.concat([df_result, other_cols], how="horizontal")
🐍 NumPy / Polars ↔ Python 原生类型互转
一、类型对照总览
| | | |
|---|
pl.Series | list | .to_list() | |
pl.DataFrame | list[dict] | .to_dicts() | |
pl.DataFrame | dict of list | {c: df[c].to_list() for c in df.columns} | |
np.ndarray | list | .tolist() | |
np.int64 | int | .item() | 必须使用 |
np.float64 | float | .item() | |
np.bool_ | bool | .item() | |
np.datetime64 | datetime | .astype("datetime64[us]").item() | |
list | pl.Series | pl.Series("name", lst) | |
list[dict] | pl.DataFrame | pl.DataFrame(lst) | |
dict of list | pl.DataFrame | pl.DataFrame(d) | |
二、Polars → Python 原生
Series / 列 → list
lst = df["score"].to_list()# → [90, 85, 78, ...]lst = df["time"].to_list()# → [datetime(2024,1,1,...), ...]lst = df["flag"].to_list()# → [True, False, True, ...]
DataFrame → list[dict](行优先,适合 JSON 序列化 / API 返回)
records = df.to_dicts()# → [{"name": "Alice", "score": 90}, {"name": "Bob", "score": 85}, ...]import jsonjson.dumps(records)# ✅ 可直接序列化(纯数值/字符串列)
DataFrame → dict of list(列优先,适合按列处理)
d ={c: df[c].to_list()for c in df.columns}# → {"name": ["Alice", "Bob"], "score": [90, 85]}
取单个标量
val = df["score"][0]# Polars 原生标量(int/float/str/datetime)✅val = df["score"].item()# ⚠️ 仅当 Series 长度为 1 时可用
三、Python 原生 → Polars
list → Series
s = pl.Series("score",[90,85,78])s = pl.Series("flag",[True,False,True])s = pl.Series("name",["Alice","Bob"])
list[dict] → DataFrame(行优先,API/JSON 响应常见)
records =[{"name":"Alice","score":90},{"name":"Bob","score":85}]df = pl.DataFrame(records)
dict of list → DataFrame(列优先,最常用)
d ={"name":["Alice","Bob"],"score":[90,85]}df = pl.DataFrame(d)
list[dict] vs dict of list
| list[dict] | dict of list |
|---|
| | |
| | |
| pl.DataFrame(records) | pl.DataFrame(d) |
四、NumPy → Python 原生
数组 → list
arr = np.array([[1,2],[3,4]])lst = arr.tolist()# → [[1, 2], [3, 4]](自动嵌套)
标量 → Python 原生(⚠️ 必须用 .item())
x = np.int64(5)type(x)# <class 'numpy.int64'>,不是 Python int!x.item()# → Python int(5) ✅y = np.float64(3.14)y.item()# → Python float(3.14) ✅b = np.bool_(True)b.item()# → Python bool(True) ✅# ⚠️ 为什么要用 .item()?import jsonjson.dumps({"val": np.int64(5)})# ❌ TypeError: not JSON serializablejson.dumps({"val": np.int64(5).item()})# ✅
np.datetime64 → Python datetime(⚠️ 坑最多)
from datetime import datetime# ✅ 推荐:先统一转 us,再 .item()(us / ns 精度均适用)dt64 = np.datetime64("2024-01-01T12:00:00","ns")dt = dt64.astype("datetime64[us]").item()# → Python datetime ✅dt64 = np.datetime64("2024-01-01T12:00:00","us")dt = dt64.astype("datetime64[us]").item()# → Python datetime ✅
⚠️ 为什么不能直接 .item()?
np.datetime64("2024-01-01","ns").item()# → 整数!❌np.datetime64("2024-01-01","us").item()# → Python datetime ✅
np.datetime64 数组 → list[datetime]
arr = np.array(["2024-01-01","2024-06-01"], dtype="datetime64[ns]")dt_list = arr.astype("datetime64[us]").tolist()# → [datetime(2024, 1, 1, 0, 0), datetime(2024, 6, 1, 0, 0)]
五、Polars 标量 vs NumPy 标量 对比
# ── Polars 取单值 ──────────────────────────────────val = df["score"][0]# 数值列 → Python int / float ✅# 时间列 → Python datetime ✅# 字符串列 → Python str ✅# ── NumPy 取单值 ───────────────────────────────────val = arr[0]# → np.int64 / np.float64 ⚠️ 不是 Python 原生!val = arr[0].item()# → Python int / float ✅