经过较长时间的实践和调整,我们逐渐形成了一套相对成熟的 Python 数据处理流程。这套流程覆盖了数据读取、清洗、转换、分析、可视化以及结果保存等环节。
在开发环境方面,我主要使用 marimo 进行交互式数据分析,并以 PyCharm 作为补充,用于脚本开发、代码检查、项目重构和数据库操作。Python 环境和项目依赖则交由 uv 管理。
这套工作流并不追求使用尽可能多的工具。相反,它更关注如何让不同工具各司其职,并尽量保证分析过程清晰、可复现且易于维护。
开发环境
marimo 笔记本
相较于 Jupyter Notebook,我现在更喜欢使用 marimo 进行交互式数据分析。
marimo 是一个响应式 Python 笔记本。某个变量发生变化后,依赖它的单元格会自动更新,或者被标记为需要重新运行。这种执行方式使代码、程序状态和输出结果能够保持较好的一致性。
传统笔记本的一个常见问题是隐藏状态。我们可能修改了一段代码,却忘记重新运行相关单元格;也可能删除了变量定义,但旧变量仍然保留在内存中。此时,页面中显示的代码并不一定能够完整解释当前输出。
这类问题在短时间的数据探索中可能并不明显,但随着笔记本变长,或者项目需要在几个月后重新打开,排查成本就会明显增加。
marimo 根据变量依赖关系组织执行过程,在很大程度上减少了这种不确定性。它的笔记本还会保存为普通的.py文件,因此更适合使用 Git 进行版本管理,也更方便与其他 Python 工具配合。
严格命名带来的好处
marimo 对变量定义有相对严格的要求。一个全局变量通常不应在多个单元格中被重复定义,因此我们需要更加认真地命名变量。
在临时分析中,人们经常反复使用:
df而在 marimo 中,更适合根据处理阶段分别命名:
raw_df cleaned_df summary_df这可能使变量名变长,但也让变量的含义更加明确。
从短期看,严格命名增加了一点输入成本;从长期维护的角度看,这种成本通常是值得的。清晰的变量名能够帮助我们理解数据所处的处理阶段,避免意外覆盖,也能减少后续检查未知状态的时间。
使用 uv 管理项目
我推荐使用 uv 管理 Python 项目及其依赖。相比手动创建虚拟环境并逐个安装包,uv 更适合以项目为中心的工作方式。
一个简单的初始化过程如下:
uv init uvaddmarimo pandas numpy scipy matplotlib uv run marimo edit其中:
pyproject.toml用于描述项目及其依赖;.venv保存项目的虚拟环境;uv.lock记录解析后的准确依赖版本;uv run确保命令在对应项目环境中运行。
使用下面的命令即可启动 marimo:
uv run marimo edit也可以直接打开指定笔记本:
uv run marimo edit analysis.pyuv run会在运行命令前检查项目环境是否与依赖声明保持同步。这种方式减少了手动激活环境和维护依赖版本的负担,也让项目更容易在另一台计算机上重新构建。
marimo 的基本使用方式与 Jupyter 并没有特别大的差异,依然由代码单元格、Markdown 和运行结果组成。主要区别在于,marimo 更强调变量依赖、确定性执行和可复现性。
具体可以参考:
- marimo 官方网站
- marimo 官方文档
- uv 项目管理文档
PyCharm
PyCharm 是 JetBrains 开发的 Python 集成开发环境。在代码补全、静态检查、调试、项目导航、重构、测试和版本控制等方面,PyCharm 都具有比较成熟的支持。
从 PyCharm 2025.1 开始,原来的 Community Edition 和 Professional Edition 被整合为统一的 PyCharm 产品。核心功能可以免费使用,其中也包括 Jupyter Notebook 支持;部分高级功能仍需要 Pro 订阅。
我主要在两种情况下使用 PyCharm。
脚本与模块开发
当一段数据处理逻辑仍处于探索阶段时,把它放在 marimo 中通常比较方便。我们可以直接观察中间结果,并快速修改参数。
但当这段逻辑逐渐稳定,并开始被多个分析重复使用时,就更适合将其整理成独立函数或模块。例如:
project/ ├── notebooks/ ├── src/ │ └── project_name/ │ ├── readers.py │ ├── cleaning.py │ ├── analysis.py │ ├── plotting.py │ └── storage.py ├── tests/ ├── pyproject.toml └── uv.lock这种组织方式可以将数据读取、清洗、计算、绘图和保存分别放在不同模块中。
marimo 负责交互式分析和结果展示,PyCharm 则负责维护底层代码。二者不是相互替代的关系,而是分别承担探索和工程化开发的任务。
数据库操作
PyCharm 还可以用于连接和操作数据库。在支持相应功能的版本中,可以直接查看数据表、执行 SQL 查询、修改记录和管理表结构。
对于 SQLite 这类本地数据库,使用 Python 标准库中的sqlite3已经能够完成绝大部分操作。不过,在检查表结构、编写 SQL 查询或者查看少量数据时,图形化数据库界面往往更加直观。
需要注意的是,数据库工具的具体可用范围可能取决于当前 PyCharm 版本和订阅状态。统一版 PyCharm 的核心功能可以免费使用,但部分高级数据库功能可能仍属于 Pro 功能。
PyCharm 的官方说明可以参考:
- 统一版 PyCharm 介绍
- PyCharm 数据科学工具
数据读取与清洗
数据读取和清洗通常使用 Python 标准库、pathlib和 pandas 完成。
对于大多数表格型数据,我倾向于先将其直接或间接转换为 pandas 的DataFrame。DataFrame 提供了成熟的索引、筛选、类型转换、缺失值处理、分组和聚合能力,适合作为数据处理流程中的通用中间形式。
对于规则明确的文本文件,可以使用 Python 自带的open():
withopen(path,encoding="utf-8")asfile:content=file.read()对于 CSV、Excel 等表格文件,则通常直接使用 pandas:
importpandasaspd df=pd.read_csv("data.csv")或者:
df=pd.read_excel("data.xlsx")如果原始文件结构并不标准,可以先编写专门的读取函数,将不同来源的数据统一转换成 DataFrame:
df=custom_reader(path)这样做的好处是,后面的清洗和分析代码不需要了解原始文件的具体排版。无论数据来自文本文件、电子表格还是其他格式,只要读取函数最终返回结构一致的 DataFrame,后续流程就可以保持统一。
使用 Path 管理文件
在处理大量文件时,推荐使用pathlib.Path,而不是通过字符串拼接路径。
例如:
frompathlibimportPath data_folder=Path("data")forfileindata_folder.rglob("*.csv"):print(file.name)Path提供了一组直观的属性:
file.namefile.stemfile.suffixfile.parent它不仅让代码更容易阅读,也减少了 Windows、Linux 和 macOS 路径分隔符不同所带来的问题。
清洗不仅是删除缺失值
数据清洗经常被简单地理解为删除空值,但实际工作通常还包括:
- 统一列名;
- 去除多余空格;
- 规范特殊字符;
- 转换数据类型;
- 检查重复记录;
- 处理异常值;
- 统一单位;
- 检查排序和索引;
- 记录数据来源;
- 验证关键字段是否存在。
例如,可以统一处理列名:
df=df.rename(columns=lambdacolumn:(column.strip().replace(" ","_").replace("/","_")))数值列则可以进行显式转换:
df["value"]=pd.to_numeric(df["value"],errors="coerce",)清洗规则最好保持明确和稳定。如果某个规则会改变原始数据的含义,应当将其写入代码和文档,而不是依赖人工记忆。
同时,原始数据应尽量保持不变。比较稳妥的目录结构是:
data/ ├── raw/ ├── interim/ └── processed/其中:
raw保存未经修改的原始文件;interim保存处理中间结果;processed保存最终结构化数据。
原始数据最好被视为只读输入。所有清洗和转换结果都应保存到新的位置,从而保证分析过程可以追溯和重新执行。
数据分析与数值计算
pandas
pandas 主要用于处理表格数据,适合:
- 数据筛选;
- 缺失值处理;
- 分组聚合;
- 多表合并;
- 长宽表转换;
- 时间序列处理;
- 描述性统计。
DataFrame 的优势是直观、通用,并且与大量输入输出格式兼容。因此,即使最终数据不以 DataFrame 保存,它也非常适合作为读取和清洗阶段的中间表示。
NumPy
NumPy 提供了 Python 科学计算的基础数组结构,适合:
- 向量化运算;
- 数组索引;
- 矩阵计算;
- 数值变换;
- 随机数生成;
- 底层数据结构转换。
很多 pandas、SciPy、xarray 和 Matplotlib 操作实际上都建立在 NumPy 数组之上。
SciPy
SciPy 在 NumPy 的基础上提供了更完整的科学计算能力,包括:
- 插值;
- 优化;
- 数值积分;
- 曲线拟合;
- 信号处理;
- 统计分析;
- 线性代数。
在数据分析中,如果任务已经超出简单的表格运算,通常可以先检查 SciPy 是否已经提供相应的标准实现。使用成熟函数通常比自行实现底层算法更加可靠,也更容易验证。
数据可视化
数据可视化既用于结果展示,也用于发现数据质量问题。
Matplotlib
对于静态绘图和学术图表,我主要使用 Matplotlib。它的优势在于控制能力强,可以精细调整:
- 坐标轴;
- 刻度;
- 字体;
- 图例;
- 颜色;
- 线型;
- 子图布局;
- 输出尺寸与分辨率。
Matplotlib 比较适合生成论文、报告和演示文稿中的最终图形。
随着项目增长,建议将重复的绘图逻辑封装成函数,避免不同分析使用不一致的样式。例如,可以统一设置字体、图片尺寸、颜色和导出格式。
Plotly
如果需要交互式图表,可以考虑使用 Plotly。它支持缩放、悬停提示、选择和动态图例,适合数据探索和网页展示。
Matplotlib 与 Plotly 的定位有所不同:
- Matplotlib 更适合静态图和精细排版;
- Plotly 更适合交互探索和在线展示。
在实际工作中,可以先用交互图检查数据,再使用 Matplotlib 制作最终图表。
数据存储
将处理后的数据保存为合适的结构化格式,是完整数据流程中非常重要的一步。
保存数据时,不仅要考虑“能否写入文件”,还应考虑:
- 是否保留数据类型;
- 是否支持元数据;
- 是否支持多维数据;
- 是否方便查询;
- 是否便于交换;
- 是否适合长期维护;
- 能否从保存结果重新生成分析。
不同存储格式解决的问题并不相同。
CSV
CSV 是最简单、最通用的表格格式之一。
它的优点包括:
- 易于生成;
- 易于人工查看;
- 几乎所有数据软件都能读取;
- 适合作为交换格式。
但 CSV 也存在明显限制:
- 数据类型信息有限;
- 不支持复杂层级;
- 元数据表达能力弱;
- 多表关系需要通过多个文件维护;
- 单位和说明通常需要另外保存。
因此,CSV 很适合数据交换和简单结果导出,但不一定适合作为复杂项目唯一的长期存储格式。
SQLite
SQLite 是一种轻量级关系型数据库,不需要单独运行数据库服务器,数据库可以保存在单个文件中。
它适合保存:
- 结构化记录;
- 对象之间的关系;
- 实验或业务元数据;
- 文件索引;
- 处理状态;
- 汇总结果;
- 需要按条件查询的数据。
SQLite 支持标准 SQL 查询,比大量分散的 CSV 文件更容易维护数据关系。例如,可以快速查找满足多个条件的记录,而不需要逐个读取文件。
不过,SQLite 更擅长管理行列式记录和关系数据。对于大型数组、多维数据或包含丰富坐标的数据,它未必是最自然的存储形式。
xarray 与 NetCDF
对于具有明确坐标、维度和多个变量的科学数据,xarray 是一个值得考虑的选择。
pandas 的 DataFrame 更强调行和列,而 xarray 可以更明确地表达:
- 维度;
- 坐标;
- 数据变量;
- 单位;
- 属性;
- 多维关系。
xarray 中的Dataset可以包含多个共享坐标的数据变量,并允许为数据和坐标附加元数据。
数据可以保存为 NetCDF:
dataset.to_netcdf("processed_data.nc")之后再读取:
importxarrayasxr dataset=xr.open_dataset("processed_data.nc")NetCDF 能够保留坐标、变量、维度和属性,因此比 CSV 更适合保存结构较丰富的科学数据。
如果数据具有层级结构,还可以使用 xarray 的DataTree。DataTree 可以将多个 Dataset 组织成类似文件系统的树形结构,并保存到支持分组的 NetCDF 文件中。
需要注意的是,写入 NetCDF 的属性应尽量使用字符串、整数和浮点数等简单类型。复杂 Python 对象通常不适合直接序列化。
如何选择存储方式
不同格式并没有绝对的优劣,关键在于数据的性质和使用方式。
| 数据类型 | 推荐形式 |
|---|---|
| 简单二维表格 | CSV、Parquet |
| 需要频繁条件查询的结构化记录 | SQLite |
| 带坐标和元数据的多维数组 | xarray + NetCDF |
| 多组层级化科学数据 | DataTree + NetCDF |
| 临时交换和人工查看 | CSV |
| 大型分析型表格 | Parquet |
在较复杂的项目中,也可以组合使用不同格式。例如:
- 使用 NetCDF 保存数组和坐标;
- 使用 SQLite 保存记录、索引和关系;
- 使用 CSV 导出需要共享的简单结果;
- 使用 Parquet 保存较大的分析型表格。
没有必要强迫所有数据进入同一种格式。
从笔记本走向项目
交互式笔记本非常适合探索,但随着项目变大,将所有逻辑都放在笔记本中会逐渐产生维护压力。
一种实用的演进方式是:
- 在 marimo 中探索数据和验证思路;
- 将稳定的逻辑整理为函数;
- 在 PyCharm 中将函数拆分到独立模块;
- 为关键逻辑添加测试;
- 在 marimo 中导入这些模块并完成交互分析;
- 使用 uv 管理项目依赖和运行环境;
- 将原始数据、处理结果与代码分别管理。
这种方式保留了笔记本的灵活性,同时也获得了普通 Python 项目的可维护性。
可复现性比“能够运行”更重要
一段代码在当前计算机上成功运行,并不意味着它已经构成了可靠的数据工作流。
一个相对完整的流程还应回答:
- 数据来自哪里?
- 使用了哪些依赖版本?
- 数据经过了哪些处理步骤?
- 中间结果如何产生?
- 参数是否被记录?
- 原始数据是否保持不变?
- 更换计算机后能否重新运行?
- 几个月后是否还能理解代码?
- 最终结果能否从保存的数据重新生成?
为了提高可复现性,我通常遵循以下原则:
- 原始数据只读保存;
- 使用项目环境隔离依赖;
- 将
pyproject.toml和uv.lock纳入版本控制; - 使用明确的变量名;
- 将重复逻辑封装为函数;
- 将稳定代码移出笔记本;
- 保存必要的元数据和单位;
- 在关键步骤增加输入验证;
- 避免依赖手工执行顺序;
- 尽量让结果可以从代码和原始数据重新生成。
工具之间的职责分工
目前,这套工作流中的工具大致承担以下职责:
| 类别 | 工具 | 主要用途 |
|---|---|---|
| 项目和依赖管理 | uv | 管理Python版本、依赖、虚拟环境和锁文件 |
| 交互式分析 | marimo | 数据探索、过程检查、交互分析和结果展示 |
| 工程化开发 | PyCharm | 脚本开发、调试、重构、测试和数据库操作 |
| 路径管理 | pathlib | 文件遍历和跨平台路径处理 |
| 表格数据处理 | pandas | 读取、清洗、转换、合并和统计 |
| 数值计算 | NumPy、SciPy | 数组运算、拟合、优化和科学计算 |
| 静态可视化 | Matplotlib | 学术绘图和精细排版 |
| 交互可视化 | Plotly | 数据探索和交互展示 |
| 多维数据管理 | xarray | 管理坐标、维度、变量和元数据 |
| 关系型数据管理 | SQLite | 保存结构化记录、关系和索引 |
| 表格交换 | CSV、Parquet | 数据交换和分析型表格存储 |
| 科学数据存储 | NetCDF | 保存多维数据、坐标与元数据 |
总结
这套 Python 数据处理工作流的重点并不是某一个具体工具,而是让不同工具承担适合自己的任务。
marimo 提供了响应式、可复现的交互分析环境;PyCharm 提供了成熟的代码开发、调试和重构能力;uv 让项目依赖和运行环境更加可控;pandas、NumPy 和 SciPy 构成了数据处理与数值计算的基础;Matplotlib 和 Plotly 分别服务于静态与交互式可视化;xarray、NetCDF 和 SQLite 则用于保存不同类型的结构化数据。
随着项目不断发展,工作流也会持续调整。但有几个目标始终不会改变:数据来源应当清楚,处理过程应当能够检查,运行环境应当可以重建,最终结果应当能够复现。
相比一次性得到某个结果,我更希望建立一套在较长时间后仍然能够理解、维护和继续使用的数据处理体系。