为什么写 TidyPy4DS
做数据分析的人,大概率都在 pandas 和 tidyverse 之间摇摆过。
如果你长期写 R,dplyr / tidyr / stringr 那套东西会很顺手:选列有 selector,批量变换有 across(),宽长表转换、字符串处理、缺失值处理都有比较统一的入口。
但一旦切到 Python,虽然 pandas 功能很强,日常清洗时还是经常会冒出几个问题:
- 想按前缀、类型、条件批量选列,不够统一
- 想对一批列做同样的处理,
assign(...)经常要自己手搓字典 - 想把清洗步骤写成稳定、可复用、可读的链,容易堆很多一次性 lambda
- 从 tidyverse 迁移过来时,肌肉记忆没地方放
所以我写了 TidyPy4DS。
它不是想重写 pandas,也不是想做另一个复杂框架,而是只补一小块真正不够顺手的地方:列选择、批量变换、字符串处理、宽长表转换,以及更自然的 .pipe() 链式体验。
项目地址:https://github.com/lenardar/TidyPy4DS
这个项目想解决什么
一句话说,函数名尽量跟 tidyverse,接口行为坚持 Python / pandas。
也就是说:
- 不做 SQL DSL
- 不做惰性执行引擎
- 不替代
groupby、merge、assign这些已经很成熟的pandas原生能力 - 只把最容易重复、最容易写散的那部分动作收成统一入口
目前项目里最核心的几组能力是:
- selector 系统:
starts_with、contains、numeric、where等 - 批量变换:
mutate_across - 批量改名:
rename_with - 汇总:
summarize - 结构查看:
glimpse - 条件映射:
case_when、if_else、recode - 宽长表转换:
pivot_longer、pivot_wider - 缺失值处理:
drop_na、fill_na、replace_na - 表头清理:
clean_names、remove_empty、row_to_names
一个简单对比:纯 pandas vs tidypy
先看一份很常见的数据:
1 | import pandas as pd |
目标很普通:
- 只保留 ID、分组、分数字段和名字
- 用每列中位数填补缺失值
- 去掉
score_前缀 - 去掉名字前后空格
- 按数学成绩做一个等级
- 最后按分组汇总
纯 pandas 写法
1 | pandas_result = ( |
这段代码当然没问题,而且完全是标准 pandas。
但你会发现几个特点:
- 分数字段被显式写了两遍
- 改名前后的列名都要手动维护
- 当类似列变多时,改动面会越来越大
tidypy 写法
1 | from tidypy.tidy import ( |
它并不是神奇地比 pandas 少很多代码。
真正的区别在于:你开始把“哪些列属于同一类”和“这批列要做什么”分开表达了。
这个分离在小数据上可能只是“看着顺一点”,但一旦你多加一个 score_logic 列,差别就出来了:
- 纯 pandas 版本通常要改多处显式列名
- selector 版本通常只要让新列符合
score_规则,主流程基本不用改
如果再往真实一点的数据走,差异会更明显。比如很多 Excel 导出来的原始表根本不是“干净 DataFrame”,而是:
- 列名里有空格、括号和标点
- 第一行才是真正的表头
- 后面还夹着全空行、全空列
这时候我最近补进去的一批 janitor 风格函数就比较顺手了:
1 | from tidypy.tidy import clean_names, remove_empty, row_to_names |
这类东西单看都不复杂,但它们经常正好卡在“pandas 原生当然能做,只是每次都要重写一遍”的位置上。
核心设计:先把列选择系统做对
这个项目里最重要的抽象其实不是 mutate_across(),而是 ColSelector。
所有 helper 都返回一个 selector 对象,真正操作时再结合 df 解析出列名。
比如:
1 | select(df, numeric() | starts_with("id")) |
这样做有几个明显好处:
- helper 可以访问列名,也可以访问 dtype 或数据本身
- selector 可以组合
- 列选择逻辑可以复用,而不是散落在各处
我最后定下来的规则也比较简单:
|表示并集-表示排除- 结果去重且保序
- 解析不到列时直接报错,不静默忽略
这套东西一旦稳定下来,后面的 select、mutate_across、rename_with、pivot_longer 都会自然顺很多。
一个我刻意没有做的东西:裸列名 NSE
如果熟悉 tidyverse,可能第一反应会问:
能不能做成
mutate(df, total=a + b)这种效果?
答案是:理论上可以硬做,实际上不值得。
在 R 里,mutate() 这种体验背后是 tidy evaluation;Python 没有这一层语言机制。真要做,只能走 eval、AST 改写、代理对象这些路线。
这种东西最大的风险不是“写不出来”,而是:
- 报错不直观
- 调试很差
- 和
pandas生态不一致 - 一旦边界复杂起来,很容易变成一套脆弱的语法魔法
所以我最后的取舍是:
- 不做裸列名 NSE
- 保留
assign(...)处理少量显式新列 - 用
mutate_across(...)解决批量变换 - 用
case_when(...)、if_else(...)、recode(...)解决条件映射和重编码
这套方案不花哨,但更稳。
glimpse() 也是我很想要的一个小东西
在 notebook 里,head() 看值,info() 看结构,但两者之间总觉得差一口气。
所以我顺手加了一个 glimpse():
1 | from tidypy.tidy import glimpse |
它会给出:
- 行数、列数
- 每列 dtype
- 非空数、缺失数
- 唯一值数量
- 前几条样本值预览
而且支持:
1 | glimpse(df, cols=starts_with("score_")) |
这玩意不大,但在实际 notebook 里非常顺手。
现在这个项目是什么状态
目前 TidyPy4DS 已经有一版可以直接用的实现,仓库里也把几件基础设施补上了:
- 双语 README
- 双语函数文档
- 双语 notebook 示例
unittest测试- GitHub Actions CI
- 一批偏 janitor 风格的小函数
示例 notebook 分成了三类:
- Why tidypy:直接对比纯 pandas 和 tidypy
- Core APIs:看 selector、
mutate_across、summarize这些核心能力 - Reshape and missing values:看
pivot_longer、pivot_wider、separate、unite、缺失值处理
项目整体还在早期阶段,但核心方向已经比较清楚:
- selector 系统要稳定
- API 要尽量少而清晰
- 不做多余包装
- 不和 Python 自身的直觉打架
- 单文件实现先保持,但内部已经按模块整理,后续可拆
最后
这个项目本质上不是在和 pandas 对抗,而是在承认一件事:
pandas很强,但确实有一些高频清洗动作,写起来就是不够顺。
如果能用一层很薄的 helper,把这些动作收成更统一、可复用、可读的入口,那它就值得存在。
如果你也经常在 pandas 和 tidyverse 的思路之间切换,TidyPy4DS 也许会正好补上你手感里缺的那一块。
最近我自己的感受也更明确了:这个项目最有价值的地方,不是“把 tidyverse 完整搬到 Python”,而是把那些每天都会写、但又总写得零碎的数据清洗动作,收成一套尽量干净的小工具。