Python 表格生成:pandas Styler 与格式导出
报告里放一张表,先要判断它值多少工夫。控制台上扫一眼就能得到的信息,不值得写排版代码;要跟着报告反复重跑、被正文引用的表,值得;交给期刊或不做分析的合作者、他们会直接在 Word 里改的表,最值得。这一篇按这个顺序讲三档做法:to_markdown() 起步,Styler 补细节,to_excel() 与 to_latex() 做交付。
先把数据整理成「表」的形状
Section titled “先把数据整理成「表」的形状”排版之前,先把要展示的内容算成一张规整的汇总表。原始数据几乎从来不是能直接印出来的样子。
import pandas as pdfrom sklearn.datasets import load_iris
iris = load_iris(as_frame=True).frameiris.columns = ["sepal_len", "sepal_wid", "petal_len", "petal_wid", "species"]iris["species"] = iris["species"].map({0: "setosa", 1: "versicolor", 2: "virginica"})
agg = ( iris.groupby("species") .agg( 样本量=("petal_len", "count"), 花瓣长均值=("petal_len", "mean"), 花瓣长标准差=("petal_len", "std"), ) .round(3))print(agg) 样本量 花瓣长均值 花瓣长标准差speciessetosa 50 1.462 0.174versicolor 50 4.260 0.470virginica 50 5.552 0.552命名聚合(新列名=("原列名", "函数名"))让结果直接带上中文列名,省掉一轮 rename()。分组键落在索引上,to_markdown() 和 to_latex() 都会把它当作行标签输出,这一层不用额外处理;只有要继续参与计算时才需要 reset_index()。细节见 Pandas分组聚合。
小数位不要设得比数据本身的精度还高。仪器读数到小数点后两位,表里写三位就是虚假精度;样本量这类整数也不该被补成 50.0。经验做法是均值保留的位数与原始测量一致,标准差可以多留一位。
先用 to_markdown 把表做出来
Section titled “先用 to_markdown 把表做出来”to_markdown() 把 DataFrame 变成 Markdown 管道表,粘进任何支持 Markdown 的文档都是张像样的表。
print(agg.to_markdown())| species | 样本量 | 花瓣长均值 | 花瓣长标准差 ||:-----------|------:|--------:|---------:|| setosa | 50 | 1.462 | 0.174 || versicolor | 50 | 4.26 | 0.47 || virginica | 50 | 5.552 | 0.552 |对齐方式按列的类型自动决定:文本列左对齐,数值列右对齐,分隔行里的 ---: 就是这个含义。宽度交给渲染端算,数字不会被补零。
floatfmt 统一小数位。上面那张表里 4.26 和 1.462 位数不一致,是各自保留三位有效数字的结果,正式输出时通常要拉齐:
print(agg.to_markdown(floatfmt=".2f"))| species | 样本量 | 花瓣长均值 | 花瓣长标准差 ||:-----------|------:|--------:|---------:|| setosa | 50.00 | 1.46 | 0.17 || versicolor | 50.00 | 4.26 | 0.47 || virginica | 50.00 | 5.55 | 0.55 |代价是样本量也被写成了 50.00。计数列和测量列的小数位要求不同,一张表里混着两类列时,floatfmt 只能二选一。要分开控制,得改用 Styler。
to_markdown() 依赖第三方包 tabulate,没装会直接抛 ImportError。pandas 不自带它,换一台机器跑脚本时容易在这里断掉。
Styler 补细节
Section titled “Styler 补细节”Styler 是 pandas 的表格样式对象,通过 .style 访问器拿到。它不改变数据,只挂上一层展示规则。
st = agg.style.format(precision=2).set_caption("三种鸢尾的花瓣长度(单位:cm)")format() 控制数字位数,set_caption() 加表标题。用 .style 有一个前提:必须装 jinja2,否则访问 .style 会直接报错:
AttributeError: The '.style' accessor requires jinja2条件着色用 map()。它接受一个函数,返回 CSS 字符串,逐格决定样式:
def highlight_large(v): return "background-color: #ffe0b2" if v > 5 else ""
st2 = agg.style.map(highlight_large, subset=["花瓣长均值"])subset 限定作用范围,不写就作用于整张表。函数对每个单元格调用一次,返回空字符串表示不加样式。
早期版本的 pandas 里这个函数叫 applymap()。2.1 起改名为 map(),pandas 3.0 已彻底移除旧名,调用 st.applymap(...) 会得到 AttributeError: 'Styler' object has no attribute 'applymap'。网上大量教程还在用旧写法,照抄会直接失败。
按数值大小自动映射颜色,用 background_gradient(),不必自己写判断函数:
agg.style.background_gradient(subset=["花瓣长均值"], cmap="Blues")cmap 接受任何 Matplotlib 的色图名。深色背景会让黑字看不清,配 Blues、Reds 这类浅色端颜色更稳妥。另外两个常用方法:highlight_max() 高亮每列最大值,bar() 在单元格里画一根长度正比于数值的数据条——后者适合展示占比或得分,不适合已经是均值的列。
表头样式归 set_table_styles() 管,它接受一组 CSS 选择器:
styled = agg.style.set_table_styles( [ {"selector": "th", "props": [("background-color", "#f5f5f5"), ("text-align", "center")]}, {"selector": "caption", "props": [("caption-side", "top"), ("font-size", "14px")]}, ]).set_caption("表 1 三种鸢尾的花瓣长度")Styler 能改的样式止步于 CSS。单元格合并、跨行表头这类结构改动它做不了,那属于 to_excel() 配合 openpyxl 直接操作工作表,或者干脆换用专门的表格库。
在终端里 print(st) 只会得到普通的 DataFrame 文本,样式完全不体现:
样本量 花瓣长均值 花瓣长标准差speciessetosa 50 1.46 0.17versicolor 50 4.26 0.47virginica 50 5.55 0.55Styler 的样式只在 Jupyter Notebook、导出的 HTML 和支持样式的 Excel 里生效。用 st.to_html() 可以把带样式的表格存成独立 HTML 片段,嵌进网页或邮件。
交付:Excel 与 LaTeX
Section titled “交付:Excel 与 LaTeX”Styler.to_excel() 会把样式一并写进 xlsx,合作者打开看到的就是着色后的表:
agg.style.background_gradient(subset=["花瓣长均值"], cmap="Blues").to_excel( "花瓣长度汇总.xlsx", engine="openpyxl")写进去的是真实的单元格填充色:读回文件可以看到三行的填充分别是 00F7FBFF、003282BE、0008306B,正是 Blues 色图由浅到深的三档。这条路要求装 openpyxl,engine 参数不写也能自动识别,显式写出来是为了在同事的机器上报错更清楚。
论文正文要的是 LaTeX 三线表。to_latex() 的默认输出就已经是三线表:
print(agg.to_latex(caption="三种鸢尾的花瓣长度", label="tbl:iris"))\begin{table}\caption{三种鸢尾的花瓣长度}\label{tbl:iris}\begin{tabular}{lrrr}\toprule & 样本量 & 花瓣长均值 & 花瓣长标准差 \\species & & & \\\midrulesetosa & 50 & 1.462000 & 0.174000 \\versicolor & 50 & 4.260000 & 0.470000 \\virginica & 50 & 5.552000 & 0.552000 \\\bottomrule\end{tabular}\end{table}\toprule、\midrule、\bottomrule 来自 LaTeX 的 booktabs 宏包,需要在导言区写 \usepackage{booktabs}。
注意默认的数字格式:1.462 被写成了 1.462000。这是 to_latex() 沿用 DataFrame 内部浮点表示的结果,正式输出必须显式指定 float_format:
print( agg.to_latex( column_format="lccc", escape=False, float_format="%.2f", caption="花瓣长度汇总", label="tbl:iris2", ))\label{tbl:iris2}\begin{tabular}{lccc}\toprule & 样本量 & 花瓣长均值 & 花瓣长标准差 \\species & & & \\\midrulesetosa & 50 & 1.46 & 0.17 \\versicolor & 50 & 4.26 & 0.47 \\virginica & 50 & 5.55 & 0.55 \\\bottomrule\end{tabular}\end{table}三个参数各管一件事。column_format 指定每列对齐,lccc 是首列左对齐、其余居中。escape=False 关掉特殊字符转义——列名里有中文时保留原样,但如果列名含 _、&、% 这些 LaTeX 保留字符,转义就必须打开,否则编译报错。float_format 统一小数位。
label 的值会成为 \label{...} 的内容,正文里用 \ref{tbl:iris2} 引用,编号由 LaTeX 自动处理。
需要说明一处版本差异:pandas 3.0 移除了 to_latex() 的 booktabs 参数,因为三线表现在是默认输出。旧代码里常见的 to_latex(booktabs=True) 在这个版本会抛 TypeError: NDFrame.to_latex() got an unexpected keyword argument 'booktabs'。2.x 及更早的版本则需要显式传 booktabs=True 才能得到上面的效果。
和 R 语言做同一件事的对照:
| 任务 | R | Python |
|---|---|---|
| 基础表格 | knitr::kable() |
df.to_markdown() |
| 手工调样式 | kableExtra |
Styler.set_table_styles() |
| 管道式排版 | gt() |
Styler 的链式方法 |
| 导出 Word | gtsave(".docx") |
无直接对应,走 to_excel() |
| 导出 LaTeX | kable(format = "latex") |
df.to_latex() |
R 的 gt 在导出 Word 原生表格上明显更顺手,gtsave() 一行就能交付;Python 这边要么交 Excel、要么交 LaTeX,中间地带没有等价的工具。R 版的完整做法见 /r/reporting/tables/。
什么时候不值得做表
Section titled “什么时候不值得做表”- 只有两三个数字:写进句子里,读者一眼扫过,比让他在表里对行对列快得多。
- 列数超过十列:拆成两张表,或者把次要列挪进附录,别用缩小字号硬塞。
- 表格只给同事看一眼:
print()就够,不要为它写 Styler。 - 交付给期刊:先确认对方要 Word 表格还是 LaTeX 源码。要 LaTeX 就交
.tex片段,要 Word 就用to_excel()或手工排。
表格的数字从哪来、半年后还能不能重跑出同样一张表,是比排版更靠前的问题,接着看 /python/reporting/reproducible/。