使用Makefile为你的数据科学项目提供代码开发的最佳实践

使用Makefile为你的数据科学项目提供代码开发的最佳实践

当一个数据科学家团队参与一个项目时,每个人都有自己的代码编写风格。例如,一个人可能会将所有函数参数塞满一行,而另一个人则喜欢将每个参数分散到每一行。此外,不同开发人员对脚本不同部分的实验可能会在合并时引入未分组或重复的软件包导入。在这种情况下,制定一个每个人都必须遵守的规定无疑是保持代码一致性的好方法。

在这篇文章中,你将学习如何使用市场上热门的工具来样式化和格式化你的代码。如果你想了解更多关于数据科学的相关内容,可以阅读以下这些文章:
所有数据科学家都应该知道的三个常见假设检验
如何开始自己的第一个数据科学项目?
导航数据驱动时代:为什么你需要掌握数据科学基础
数据科学家常见的13个统计错误,你有过吗?

  • Black:符合PEP8标准的代码格式化工具。
  • flake8:检查代码的样式和质量。
  • isort:按字母顺序排列导入内容,并自动将其分为不同部分和类型。

这项工作可以轻松实现自动化,这样数据科学家就可以专注于更重要的任务。稍后,你还将使用Makefile完成这项工作。

Table of Contents
  ⚙️ Install Packages
  🎨 Create Styling Configuration
  🤖 Automate Using Makefile
  🚀 Push Your Project to GitHub
  🍻 Congrats!

首先,让我们从GitHub克隆该项目,建立Python环境,并在终端运行以下命令创建一个名为style的新Git分支。

$ git clone https://github.com/dwiuzila/tagolym-ml.git
$ cd tagolym-ml
$ git checkout documentation
$ python3 -m venv venv
$ source venv/bin/activate
$ python3 -m pip install --upgrade pip
$ python3 -m pip install -e ".[dev]"
$ git checkout -b style
Switched to a new branch 'style'

如果你的本地计算机上已经有了这个项目—也许看完前面的内容之后—你可以直接进入项目目录,创建新的分支。

$ git checkout documentation
$ source venv/bin/activate
$ git checkout -b style
Switched to a new branch 'style'

现在,我们准备好了!

安装软件包

让我们先安装所需的软件包。

$ pip install black flake8 isort

我们不会将这些软件包添加到requirements.txt中,因为它们不是我们项目的核心库。相反,我们可以创建一个包含这些软件包的单独列表,并将其附加到setup.py中的dev选项。

from pathlib import Path
from setuptools import find_namespace_packages, setup

# load libraries from requirements.txt
BASE_DIR = Path(__file__).parent
with open(Path(BASE_DIR, "requirements.txt")) as file:
    required_packages = [ln.strip() for ln in file.readlines()]

docs_packages = [
    "mypy==1.5.1",
    "mkdocs-material==9.3.1",
    "mkdocstrings-python==1.7.0"
]

style_packages = [
    "black==23.12.1",
    "flake8==7.0.0",
    "isort==5.13.2"
]

# define the package
setup(
    name="tagolym",
    version=0.3,
    description="Classify math olympiad problems.",
    author="Albers Uzila",
    author_email="tagolym@gmail.com",
    url="https://dwiuzila.github.io/tagolym-ml/",
    python_requires=">=3.9",
    packages=find_namespace_packages(),
    install_requires=[required_packages],
    extras_require={
        "dev": docs_packages + style_packages,
        "docs": docs_packages,
    },
)

请注意,我们没有为样式包创建新的extras_require元素。因为它们是辅助性的,没有人会在开发项目时只安装样式包。

创建样式配置

PEP 518将pyproject.toml定义为一个配置文件,用于存储Python项目的构建系统依赖关系,包括样式。如果你从一开始就关注这个MLOps Megaproject,你会发现我们曾经使用pyproject.toml作为mypy的键入工具。

有关Black配置的示例,请参阅文档。对于isort来说,有很多配置选项,你可以从中挑选,以避免与Black冲突。让我们把它们写入pyproject.toml。

[tool.mypy]
disable_error_code = ["import", "var-annotated", "assignment"]

[tool.black]
line-length = 88
include = '\.pyi?$'
# excludes files or directories in addition to the defaults
extend-exclude = '''
(
      .eggs
    | .git
    | .hg
    | .mypy_cache
    | .tox
    | venv
    | _build
    | buck-out
    | build
    | dist
)
'''

[tool.isort]
profile = "black"
line_length = 79
multi_line_output = 3
include_trailing_comma = true
virtual_env = "venv"

此配置表示:

  • Black将允许每行代码最多包含88个字符,包括.py和.pyi文件,并排除预定的文件和目录列表。
  • isort将使用Black作为基本配置文件类型,每行导入最多允许79个字符。它参考我们的虚拟环境venv来判断软件包是否属于第三方。对于多行导入,它使用垂直悬挂缩进并包含尾部逗号。我的意思是将third_party import lib1、lib2、lib3改为类似的内容:
from third_party import (
    lib1,
    lib2,
    lib3,
)

flake8本身不支持通过pyproject.toml进行配置。因此,你需要在项目目录中新建一个名为.flake8的文件,然后在其中添加配置选项。

$ touch .flake8
[flake8]
exclude = venv
per-file-ignores = tagolym/_typing.py:F401
max-line-length = 88
extend-ignore = E203, E501, E704

# E203: Whitespace before ':'
# F401: Module imported but unused
# E501: Line too long
# E704: Multiple statements on one line

该配置意味着:

  • flake8将排除对虚拟环境文件夹venv的检查,忽略某些flake8规则,以便一切都能在Black和isort配置下运行,并将任何行中的最大字符数设置为88。

由于tagolym/_typing.py仅用于导入,因此会出现多个F401警告。因此,我们将使用per-file-ignores功能,专门针对tagolym/_typing.py以忽略这些警告。

要运行我们使用的所有样式工具,请执行以下命令:

$ black .
$ isort .
$ flake8

你会发现文件中发生了许多变化。如果flake8仍然给出错误或警告,请相应地编辑文件,然后再次运行flake8命令。

使用Makefile自动运行

简单地说,Makefile是一个文件,它定义了一系列要执行的规则,是自动执行任务的一种方式。让我们创建这个文件。

$ touch Makefile

下面是Makefile的模板。在文件顶部,指定执行规则的shell环境。在下面,定义规则。一条规则包括一个“目标”和可选的“先决条件”,然后是一些“配方”,说明如何制作目标。

SHELL = shell_environment

target: prerequisites
<TAB> recipe

在我们的使用案例中,我们可以通过定义style目标来自动完成代码样式化过程,具体如下。
SHELL = /bin/bash

style:
  black .
  isort .
  flake8

然后在终端运行make style,执行该规则。

$ make style
black .
All done! ✨ 🍰 ✨
9 files left unchanged.
isort .
Skipped 2 files
flake8

但为什么要到此为止呢?

让我们添加一个名为”clean”的新目标,它主要用于删除项目中未使用的文件和文件夹,如.DS_Store(在Mac上)或缓存。这一次,将style目标作为先决条件,这意味着运行clean之前也要运行样式。

SHELL = /bin/bash

# Styling
style:
  black .
  isort .
  flake8

# Cleaning
clean: style
  find . -type f -name "*.DS_Store" -ls -delete
  find . | grep -E "(__pycache__|\.pyc|\.pyo)" | xargs rm -rf
  find . | grep -E ".pytest_cache" | xargs rm -rf
  find . | grep -E ".ipynb_checkpoints" | xargs rm -rf
  find . | grep -E ".trash" | xargs rm -rf
  rm -rf .coverage*

我们还可以自动创建虚拟环境,例如,定义venv target如下。这对刚刚开始项目工作的开发人员很有用,就像我们在本文开头所做的那样。

SHELL = /bin/bash

# Set variable
PROJECT_PATH := "."

# Styling
style:
  black .
  isort .
  flake8

# Cleaning
clean: style
  find . -type f -name "*.DS_Store" -ls -delete
  find . | grep -E "(__pycache__|\.pyc|\.pyo)" | xargs rm -rf
  find . | grep -E ".pytest_cache" | xargs rm -rf
  find . | grep -E ".ipynb_checkpoints" | xargs rm -rf
  find . | grep -E ".trash" | xargs rm -rf
  rm -rf .coverage*

# Environment
venv:
  python3 -m venv venv
  source venv/bin/activate && \
  python3 -m pip install --upgrade pip && \
  python3 -m pip install -e ${PROJECT_PATH}

请注意,我们使用了一个名为PROJECT_PATH的可选变量,它接受安装环境的路径。目前,根据setup.py,PROJECT_PATH的可能值是”.”(默认值)、”.[dev]”或”.[docs]”。

我们还使用了&&,因为我们希望在一个shell中执行配方第24、25和26行。这是因为激活虚拟环境后,安装软件包应在虚拟环境中进行。

现在我们遇到了一个问题:在终端运行make venv会出错。你能猜到原因吗?

$ make venv
make: `venv' is up to date.

这是因为Makefile中的目标原本应该是我们可以制作的文件,因此才有了这个名字。不过,由于我们已经有了一个名为venv的文件夹作为虚拟环境,Makefile不想再制作该目标。

要覆盖这一行为,可以明确声明目标为假目标,方法是将其作为特殊目标.PHONY的先决条件(请参阅特殊内置目标名称),如下所示。最后,你还可以添加help target来提供有关如何使用Makefile的通用信息。下面是Makefile的最终版本。

SHELL = /bin/bash

# Set variable
PROJECT_PATH := "."

# Help
.PHONY: help
help:
  @echo "Commands:"
  @echo "venv    : creates a virtual environment."
  @echo "style   : executes style formatting."
  @echo "clean   : cleans all unnecessary files."

# Styling
.PHONY: style
style:
  black .
  isort .
  flake8

# Cleaning
.PHONY: clean
clean: style
  find . -type f -name "*.DS_Store" -ls -delete
  find . | grep -E "(__pycache__|\.pyc|\.pyo)" | xargs rm -rf
  find . | grep -E ".pytest_cache" | xargs rm -rf
  find . | grep -E ".ipynb_checkpoints" | xargs rm -rf
  find . | grep -E ".trash" | xargs rm -rf
  rm -rf .coverage*

# Environment
.PHONY: venv
venv:
  python3 -m venv venv
  source venv/bin/activate && \
  python3 -m pip install --upgrade pip && \
  python3 -m pip install -e ${PROJECT_PATH}

将项目推送到GitHub

你可能需要先更新README.md,将Makefile包括在内。

Input text:

Find all functions $f:(0,\infty)\rightarrow (0,\infty)$ such that for any $x,y\in (0,\infty)$,

$$xf(x^2)f(f(y)) + f(yf(x)) = f(xy) \left(f(f(x^2)) + f(f(y^2))\right).$$

Predicted tags:

["algebra", "function"]

Virtual Environment

$ git clone https://github.com/dwiuzila/tagolym-ml.git
$ cd tagolym-ml
$ git checkout style
$ make venv

Directory

config/
├── args_opt.json         - optimized parameters
├── args.json             - preprocessing/training parameters
├── config.py             - configuration setup
├── run_id.txt            - run id of the last model training
├── test_metrics.json     - model performance on test split
├── train_metrics.json    - model performance on train split
└── val_metrics.json      - model performance on validation split

docs/
├── tagolym/
│   ├── data.md           - documentation for data.py
│   ├── evaluate.md       - documentation for evaluate.py
│   ├── main.md           - documentation for main.py
│   ├── predict.md        - documentation for predict.py
│   ├── train.md          - documentation for train.py
│   └── utils.md          - documentation for utils.py
├── index.md              - homepage
├── license.md            - project license
└── logo.png              - project logo

tagolym/
├── _typing.py            - type hints
├── data.py               - data processing components
├── evaluate.py           - evaluation components
├── main.py               - training/optimization pipelines
├── predict.py            - inference components
├── train.py              - training components
└── utils.py              - supplementary utilities

.flake8                   - code quality assurance

.gitignore                - files/folders that git will ignore

LICENSE                   - project license

Makefile                  - task automation

mkdocs.yml                - configuration file for docs

pyproject.toml            - build system dependencies

README.md                 - longform description of the project

requirements.txt          - package dependencies

setup.py                  - code packaging

Workflow

由于数据访问限制,您无法执行下面代码片段中的#查询数据部分。为此,你需要我的证书,但不幸的是,我的证书不能共享。不过不用担心,我会提供示例供你使用。你只需下载样本label_data.json,并将文件保存到工作目录中名为data的文件夹中即可。

from pathlib import Path
from config import config
from tagolym import main

# query data
key_path = "credentials/bigquery-key.json"
main.elt_data(key_path)

# optimize model
args_fp = Path(config.CONFIG_DIR, "args.json")
main.optimize(args_fp, study_name="optimization", num_trials=10)

# train model
args_fp = Path(config.CONFIG_DIR, "args_opt.json")
main.train_model(args_fp, experiment_name="baselines", run_name="sgd")

# inference
texts = [
    "Let $c,d \geq 2$ be naturals. Let $\{a_n\}$ be the sequence satisfying $a_1 = c, a_{n+1} = a_n^d + c$ for $n = 1,2,\cdots$.Prove that for any $n \geq 2$, there exists a prime number $p$ such that $p|a_n$ and $p \not | a_i$ for $i = 1,2,\cdots n-1$.",
    "Let $ABC$ be a triangle with circumcircle $\Gamma$ and incenter $I$ and let $M$ be the midpoint of $\overline{BC}$. The points $D$, $E$, $F$ are selected on sides $\overline{BC}$, $\overline{CA}$, $\overline{AB}$ such that $\overline{ID} \perp \overline{BC}$, $\overline{IE}\perp \overline{AI}$, and $\overline{IF}\perp \overline{AI}$. Suppose that the circumcircle of $\triangle AEF$ intersects $\Gamma$ at a point $X$ other than $A$. Prove that lines $XD$ and $AM$ meet on $\Gamma$.",
    "Find all functions $f:(0,\infty)\rightarrow (0,\infty)$ such that for any $x,y\in (0,\infty)$, $$xf(x^2)f(f(y)) + f(yf(x)) = f(xy) \left(f(f(x^2)) + f(f(y^2))\right).$$",
    "Let $n$ be an even positive integer. We say that two different cells of a $n \times n$ board are [b]neighboring[/b] if they have a common side. Find the minimal number of cells on the $n \times n$ board that must be marked so that any cell (marked or not marked) has a marked neighboring cell."
]
main.predict_tag(texts=texts)

Documentation

打开完整的文档:

https://dwiuzila.github.io/tagolym-ml/

$ git checkout documentation
$ pip install -e ".[docs]"
$ mkdocs gh-deploy --force

Makefile

$ make help
Commands:
venv    : creates a virtual environment.
style   : executes style formatting.
clean   : cleans all unnecessary files.

然后,按如下步骤将所有更改推送到GitHub。

$ git add .
$ git commit -m "Code styling"
$ git push origin style

你可以在这里看到结果。

恭喜!

你已经获得了很大进步。你现在知道如何对代码进行样式设计和质量保证,然后使用Makefile自动完成整个过程。希望你能学到一二。现在,享受一下成就感吧!

不过,你的MLOps之旅才刚开始。还有很长的路要走。敬请期待!

感谢阅读!你还可以订阅我们的YouTube频道,观看大量大数据行业相关公开课:https://www.youtube.com/channel/UCa8NLpvi70mHVsW4J_x9OeQ;在LinkedIn上关注我们,扩展你的人际网络!https://www.linkedin.com/company/dataapplab/

原文作者:Albers Uzila
翻译作者:Qing
美工编辑:过儿
校对审稿:Jason
原文链接:https://levelup.gitconnected.com/code-development-best-practices-for-your-data-science-projects-with-makefile-5fc0e0ce87ce