科研项目组织
一个完整的科研项目涉及数据、代码和论文报告等多个方面,合理地组织这些内容有利于项目管理,便于复现、备份和留痕。
这里结合实践经验,给出一套项目组织方法,灵感来源于 Mario Krapp/semic-project 和 Joshua Cook 的工作。
该方法适用于以下类型的项目:
- 核心代码以 Python 为主;
- 论文报告主要使用 LaTeX 和 Markdown 编写;
- 使用 Git 进行版本管理。
此外,本文建议与以下工具配合使用:
uv
:用于安装依赖;cookiecutter
:生成项目结构;Sphinx
:自动生成 API 文档。 这些工具虽然不是必需的,也可以使用其他替代方案,选择自己熟悉的工具即可。本文使用这些工具来创建和组织项目。
项目结构搭建
Cookiecutter 是一个命令行工具,用于从预先制作的模板快速生成项目结构。
快速使用
首先,使用如下命令安装该工具:
uv pip install cookiecutter
之后,调用该工具安装 Mingzefei/cookiecutter-science 所提供的项目结构模板:
cookiecutter https://github.com/Mingzefei/cookiecutter-science.git
注:其他项目模板可参考 special templates
依据提示,填写相关内容,最终会在当前位置下生成项目结构。
项目结构
.
├── AUTHORS.md <- 项目作者信息
├── LICENSE <- 项目使用的开源协议
├── README.md <- 项目的说明文件,包含项目介绍、安装说明等信息
├── backup <- 项目的备份文件夹,保存配置和结果等备份数据,不被版本控制追踪
├── config <- 配置文件存放目录
│ └── config.yaml <- 配置文件,包含计算参数和设置
├── data <- 项目使用的数据,不被版本控制追踪
│ ├── external <- 外部数据集,例如其他研究团队获取的验证或对比数据
│ ├── interim <- 中间数据,经过清洗、分组等初步处理,用于探索和后续步骤
│ ├── processed <- 最终处理后的数据集,用于建模和分析
│ └── raw <- 原始数据,未经任何处理
├── docs <- 项目的文档资料,包括技术文档和研究文献
├── notebooks <- 包含 Jupyter Notebook 文件,用于前期探索、代码实验与展示
│ └── 00_draft_example.ipynb <- 示例 Notebook 文件,作为草稿
├── pyproject.toml <- 项目的配置文件,用于定义项目依赖、打包等设置
├── reports <- 学术报告和项目报告相关的内容
│ ├── Makefile <- 用于编译 LaTeX 报告的 Makefile 文件
│ ├── archive <- 归档的草稿
│ ├── figures <- 报告中的图表和图片
│ ├── main.tex <- 主报告的 LaTeX 源文件
│ └── si.tex <- 附加材料或补充信息的 LaTeX 文件
├── results <- 实验结果或分析结果的存储位置,不被版本控制追踪
├── scripts <- 各种可执行脚本,完成数据下载、清洗、备份等操作
│ ├── __init__.py <- 脚本模块初始化文件
│ ├── backup.py <- 数据备份脚本
│ ├── clean.py <- 数据清洗脚本
│ ├── config_loader.py <- 配置文件加载脚本
│ ├── data_downloader.py <- 数据下载脚本
│ └── paths.py <- 项目路径配置脚本,定义各文件夹路径
└── {{cookiecutter.project_slug}} <- 项目核心代码(简称 src),可以独立打包成 Python package
├── __init__.py <- 项目包初始化文件
├── cli.py <- 命令行接口,用于调用项目核心功能
├── data <- 数据处理的逻辑函数
│ ├── __init__.py
│ └── clean_data.py <- 数据清洗的具体实现逻辑
├── external <- 外部的代码或库,不被版本控制追踪
├── models <- 模型构建和训练的代码
│ └── __init__.py
├── plot <- 数据可视化的逻辑代码
│ ├── __init__.py
│ └── plot_style.py <- 定义数据可视化风格和样式的脚本
└── utils <- 工具类函数,包括文件读写、日志记录等辅助功能
├── __init__.py
├── file_io.py <- 文件输入输出处理逻辑
└── logger.py <- 日志记录逻辑
说明
(以下 {{cookiecutter.project_slug}}
简称为 src
)
src
是核心代码,输入输出均为抽象的数据结构,不涉及具体的文件或路径;独立于项目其余内容,可以作为独立的 package 发布。src/data
:数据处理的逻辑函数。src/utils
:工具函数和辅助类,如文件读写、日志记录等。src/models
:模型构建的逻辑函数和类。src/plot
:可视化的逻辑函数和类。
scripts
主要包含可执行脚本,如数据下载、模型训练、结果备份等。这些脚本可以调用scripts/paths.py
、scripts/config_loader.py
和src
,以获取路径、配置和逻辑代码。scripts/paths.py
:项目的文件夹路径定义,用于指定输入输出,通常固定不变。scripts/config_loader.py
:项目的配置文件加载器,用于指定实验参数等,可根据需要修改。
notebooks
包含所有的 Jupyter Notebook 文件,早期用于快速构建代码和探索,后期用于执行和展示。- 建议定期将
notebooks
中的代码封装进src
和scripts
中,保持 notebook 文件的整洁,并便于项目自动化操作。 - Jupyter Notebook 文件的命名格式为
<递增编号>_<描述性名称>.ipynb
,如01_data_process.ipynb
。其中,<递增编号>
建议使用两位数字xy
,y
为递增数字,x
的取值含义如下:- 0:草稿
- 1:数据
- 2:模型
- 3:结果(如可视化)
- 4:报告
- 建议定期将
docs
用于存放项目的相关文献和技术文档。reports
存放项目的最终学术报告(如 LaTeX 文件),以及归档的草稿文件(如 md、docx、pptx、pdf 等)。data
存放项目使用的数据文件,通常体积较大,不受 git 版本控制管理。data/raw
:原始数据,从数据源下载,不应手动修改。data/interim
:中间数据,经过初步处理,可用于后续步骤。data/processed
:最终处理后数据,用于建模和分析。data/external
:外部研究团队的数据集,用于验证和比较。
版本管理
使用 git
进行版本管理,将项目上传至 github
平台,方便团队协作和备份。
相关内容请自行查阅 git
和 github
的文档。
虚拟环境管理
也可以用 conda
创建虚拟环境,不过个人更推荐在项目文件夹内创建专用于项目的虚拟环境,避免 conda
的名称标识。
uv venv # 创建虚拟环境
source .venv/bin/activate # 激活虚拟环境
之后可用如下命令安装依赖:
uv pip install <library> # 安装依赖
核心代码开发
开发 src
中的逻辑函数和类,注意输入输出均为抽象的数据结构,不涉及具体的文件或路径。
src
会被作为 package 安装入本地的虚拟环境。
完善项目配置文件
项目配置文件 pyproject.toml
用于构建和管理项目,按需修改其中的内容。
以下是一个简单示例:
# 构建系统相关配置
[build-system]
requires = [
"setuptools",
"setuptools_scm[toml]",
"wheel"]
build-backend = "setuptools.build_meta"
# 项目元数据
[project]
name = "my_project" # 项目名称
description = "project description" # 项目描述
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
dynamic = ["version"]
requires-python = ">=3.8" # 最低Python版本
dependencies = [
"numpy", # 项目基础依赖,非全部
"pandas"
"jupyterlab",
"matplotlib"
]
classifiers = ["Private :: Do Not Upload"] # 如果为私有项目,不希望发布到PYPI
# 可选开发工具配置
[project.optional-dependencies]
dev = [
"ruff", # 代码规范工具
"pytest" # 单元测试工具
]
[tool.setuptools]
packages = ["src"] # 项目代码所在路径 src
[tool.setuptools_scm] # 使用 github tag 作为版本号
version_scheme = "post-release"
local_scheme = "no-local-version"
完整版如下:
# 构建系统相关配置
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
# 项目元数据
[project]
name = "<example_project>"
version = "<0.1.0>"
description = "<An example Python project.>"
readme = "README.md" # 项目的README文件
license = {file = "LICENSE"}
authors = [
{name = "<name>", email = "<e-mail>"}
]
maintainers = [
{name = "<name>", email = "<e-mail>"}
]
keywords = ["example", "sample", "project"]
repository = "https://github.com/example/example_project"
documentation = "https://docs.example.com"
requires-python = ">=3.8"
classifiers = [
"Development Status :: 4 - Beta",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Operating System :: OS Independent"
]
dependencies = [
"jupyterlab",
"matplotlib",
"numpy",
"pandas"
]
# 可选依赖项配置
[project.optional-dependencies]
dev = [
"ruff",
"pytest>=6.0",
"black",
"flake8",
"mypy"
]
docs = [
"sphinx",
"sphinx-rtd-theme"
]
# 插件和工具的配置部分
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310']
[tool.flake8]
max-line-length = 88
exclude = ["tests/*", "build/*"]
[tool.mypy]
strict = true
[tool.setuptools]
packages = ["src"] # 项目代码所在路径 src
[tool.setuptools_scm] # 使用 github tag 作为版本号
version_scheme = "post-release"
local_scheme = "no-local-version"
安装核心代码到本地
uv pip install -e .
或者使用开发依赖:
pip install -e .[dev]
NOTE: -e
或 --editable
以可编辑的方式安装包。好处是src
代码的更改会立即生效,而无需重新安装。
例如在 ipython
或脚本中可以直接导入:
import my_project
更新项目配置文件
项目开发过程中,会动态地增删新的依赖,需要更新项目配置文件中的依赖部分。
使用 pipreqs
自动生成具体的依赖列表,再手动更新到 pyproject.toml
中。
uv pip install pipreqs
pipreqs <project_path>
创建命令行模式(可选)
利用 Typer
创建命令行界面,方便使用。
在项目 package 中创建 cli.py
文件,编写命令行界面代码:
import typer
app = typer.Typer()
@app.command()
def hello_world(
name: str = typer.Option(..., help="你的名字"), # 必需参数
shout: bool = typer.Option(False, help="是否大声问候") # 可选参数,布尔值
) -> None:
"""
向用户问候。
"""
greeting = f"Hello, {name}!"
if shout:
greeting = greeting.upper()
print(greeting)
@app.command()
def goodbye_world(
reason: str = typer.Option(None, help="离开的原因"), # 可选参数
formal: bool = typer.Option(True, help="是否使用正式告别") # 可选参数,布尔值
) -> None:
"""
向用户告别。
"""
if formal:
message = f"Goodbye! Reason: {reason or '没有提供原因。'}"
else:
message = "Bye!"
print(message)
if __name__ == "__main__":
app()
将命令行入口点添加到pyproject.toml
中:
[project.scripts]
"proj" = "my_project:cli.app"
从而在命令行中使用:
proj --help
proj hello-world --name Alice --shout
proj goodbye-world --resason "I have to go"
NOTE:typer
自动将选项名称中的下划线(_
)转换为连字符(-
)
规范代码
使用 ruff
、black
、flake8
和 mypy
等工具规范代码。
项目核心代码发布
声明
在pyproject.toml
中做如下修改:
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
Development Status :: 4 - Beta
: 表明项目处于测试阶段(Beta),功能较为完整,但仍可能有较大的改进空间和错误。Programming Language :: Python :: 3
: 项目是用 Python 3 语言编写的,支持 Python 3 及以上版本。License :: OSI Approved :: MIT License
: 项目采用了 MIT 许可证,表示它是开源的并且可以自由使用、修改和分发。Operating System :: OS Independent
: 项目可以在任意操作系统上运行,与操作系统无关。
生成发布文件
python -m build
上传至 PyPI
twine upload dist/*
NOTE: 可以借助 github 的 Action 自动发布。
项目文档生成
使用 Sphinx
自动生成 Python 项目的 API 文档,要求代码注释规范。
sphinx 使用
# 0. 安装
pip install sphinx
# 1. 初始化
cd docs
sphinx-quickstart
# 2. 配置
vi source/conf.py
# ---进入文件编辑
# 1) 添加路径
import os
import sys
sys.path.insert(0, os.path.abspath('../../src'))
# 2) 添加插件
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.doctest',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.mathjax',
]
# 3) 指定风格
html_theme = 'sphinx_rtd_theme'
# ---退出文件编辑
# 3. 编译
sphinx-apidoc -o source ../src/
make html
未来可能会添加的特性
- code tests
- code format check
- continuous integration
参考资料
- 编写代码时的一些原则:Guiding Design Principles(本项目的许多设计思路受此启发)
- 一些值得借鉴的 cookiecutter 模板