科研项目组织

目录

一个完整的科研项目涉及数据、代码和论文报告等多个方面,合理地组织这些内容有利于项目管理,便于复现、备份和留痕。  

这里结合实践经验,给出一套项目组织方法,灵感来源于 Mario Krapp/semic-projectJoshua 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

  1. src 是核心代码,输入输出均为抽象的数据结构,不涉及具体的文件或路径;独立于项目其余内容,可以作为独立的 package 发布。
    • src/data:数据处理的逻辑函数。
    • src/utils:工具函数和辅助类,如文件读写、日志记录等。
    • src/models:模型构建的逻辑函数和类。
    • src/plot:可视化的逻辑函数和类。
  2. scripts 主要包含可执行脚本,如数据下载、模型训练、结果备份等。这些脚本可以调用 scripts/paths.pyscripts/config_loader.pysrc,以获取路径、配置和逻辑代码。
    • scripts/paths.py:项目的文件夹路径定义,用于指定输入输出,通常固定不变。
    • scripts/config_loader.py:项目的配置文件加载器,用于指定实验参数等,可根据需要修改。
  3. notebooks 包含所有的 Jupyter Notebook 文件,早期用于快速构建代码和探索,后期用于执行和展示。
    • 建议定期将 notebooks 中的代码封装进 srcscripts 中,保持 notebook 文件的整洁,并便于项目自动化操作。
    • Jupyter Notebook 文件的命名格式为 <递增编号>_<描述性名称>.ipynb,如 01_data_process.ipynb。其中,<递增编号> 建议使用两位数字 xyy 为递增数字,x 的取值含义如下:
      • 0:草稿
      • 1:数据
      • 2:模型
      • 3:结果(如可视化)
      • 4:报告
  4. docs 用于存放项目的相关文献和技术文档。
  5. reports 存放项目的最终学术报告(如 LaTeX 文件),以及归档的草稿文件(如 md、docx、pptx、pdf 等)。
  6. data 存放项目使用的数据文件,通常体积较大,不受 git 版本控制管理。
    • data/raw:原始数据,从数据源下载,不应手动修改。
    • data/interim:中间数据,经过初步处理,可用于后续步骤。
    • data/processed:最终处理后数据,用于建模和分析。
    • data/external:外部研究团队的数据集,用于验证和比较。

版本管理

使用 git 进行版本管理,将项目上传至 github 平台,方便团队协作和备份。

相关内容请自行查阅 gitgithub 的文档。

虚拟环境管理

也可以用 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 自动将选项名称中的下划线(_)转换为连字符(-

规范代码

使用 ruffblackflake8mypy 等工具规范代码。

项目核心代码发布

声明

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

参考资料