老齐教室

数据科学的软件工程技巧和最佳实践

如果你对数据科学感兴趣,你可能对这个工作流程很熟悉:用jupyter创建一个项目,然后开始编写python代码,运行复杂的分析,训练一个模型。当notebook文件随着函数、类、绘图和日志的增加而增大时,你会发现自己面前有一个庞大的代码块。如果幸运的话,项目有可能进展顺利,这再好不过了!

然而,jupyter中隐藏了一些深坑,不小心掉进去,就如同进入地狱。下面先看看这些坑是什么样的,然后探讨如何避免。

隐藏的问题

下面这些情况,不知道你是否遇到过:

  • 在某个地方定义了一类,然后实例化。后来又想修改它了,于是不得不返回来,修改,再执行该代码块。代码量少的时候无所谓,如果多了,会让你崩溃的。为什么把这么多东西都放在一个notebook文件中呢?难道不能分开维护吗?

  • 由于Juputer的交互性和即时反馈,有不少人习惯在全局命名空间中声明变量,而不是使用函数。这在软件开发中被认为是不好的做法(https://stackoverflow.com/questions/19158339/why are global variables evil/19158418\19158418),因为它限制了有效的代码重用,还损害了可重复性,notebbook不得不保存所有变量,你必须记住哪个结果被缓存,哪个没有缓存,你还要期望其他用户遵循你的单元执行顺序。

  • notebook并不利于代码版本的管理,很少有数据科学家使用GIT来管理不同的版本,或者创建分支等,这就使团队协作变得低效和笨拙,甚至团队成员还在用电子邮件传送notebook文件,这是历史性的倒退。并且如果回到以前的某个版本的代码,更是一场噩梦,文件的管理组织混乱不堪。在没有版本控制的情况下,使用jupyter两三周后,在项目中经常看到以下文件:

    • analysis.ipynb
    • analysis_COPY(1).ipynb
    • analysis_COPY(2).ipynb
    • analysis_FINAL.ipynb
    • analysis_FINAL_2.ipynb
  • 用Jupyter很适合探索和快速构建原型,这当然不是为了可重用性或生产而设计的。如果你使用jupyter开发了一个数据处理的流程,那么,最好的情况是代码只在notebook上以线性同步方式、按照单元的执行顺序工作。这并不是代码在更复杂的环境中的运行方式,例如,更大的输入数据集、其他异步并行任务或较少的分配资源。

  • 作为一个将大部分时间花在VSCode上的人,我利用了功能强大的扩展,如代码链接、样式格式化、代码结构化、自动填补和代码库搜索。当我切换回jupyter时,我不禁感到它的能力低下了。与VSCode相比,jupyter还少很多东西。

好了,对jupyter的抨击就到这里了。实际上,我很喜欢jupyter。我认为它能够很好地实现所设计的功能需求,可以用它来创建小项目或快速构建出某些想法的原型。

为了在生产环境中部署代码,你必须遵循软件工程的原则。但是,数据科学家们往往对此置若罔闻。下面就列举几条软件工程的原则,了解一下为什么它们很重要。

写出精彩代码的技巧

下面所总结的这些技巧得自于不同的项目、我参加的会议、我与软件工程师和架构师的讨论。如果你有其他的建议和想法要分享,请随时把你的意见写到评论区。

1— 整洁的代码

代码质量最重要的一个方面是清晰整洁,清晰易读的代码对于协作和可维护性至关重要。

以下方法可以使你的代码更加清晰:

-使用有意义的描述性的变量名,并暗示其类型。例如,如果要声明一个关于属性(例如年龄)的布尔变量,以便检查一个人是否老了,那么可以使用is_old,使其既具有描述性,又能表达类型信息。

同样的方法也适用于数据集的命名,要让名称具有可读性。

1
2
3
4
5
6
# not good ... 
import pandas as pd
df = pd.read_csv(path)

# better!
transactions = pd.read_csv(path)
  • 避免使用只有你自己能理解的缩写,和没人能忍受的长变量名。

  • 不要直接在代码中硬编码“神奇数字”。在变量中定义它们,以便每个人都能理解“神奇数字”所指的内容。

1
2
3
4
# not good ...
optimizer = SGD(0.0045, momentum=True)# better !
learning_rate = 0.0045
optimizer = SGD(learning_rate, momentum=True)
  • 在命名对象时遵循PEP8惯例:例如,函数和方法的名称是小写的,单词用下划线隔开,类名首字母大写,常量是完全大写的,等等。

  • 使用缩进和空白让代码“呼吸”。有一些标准惯例,比如“每个缩进使用4个空格”,“单独的部分应该有额外的空行”…… 如果记不住,可以在VSCode中找一些扩展,比如prettier,当按下 ctrl+s时,这个扩展会自动把代码重新格式化。

2—代码模块化

当你开始构建某些代码,并且希望这些代码可以在同一个项目或其他项目中重用时,你必须把代码组织成函数和模块,这有助于更好地维护和重用。

例如,一个NLP项目,可能有不同的函数来处理文本数据(标记化、剥离URLs、词簇化等)。你可以将所有这些单元放在一个名为text_processing.py 的python模块中,并从中导入这些单元,这样主程序会变得更轻!

关于编写模块化代码,分享一些好的技巧:

  • 不要自我重复。尽可能泛化和合并已有的代码。
  • 一个函数应该做一件事。如果一个函数执行多个操作,则更难进行泛化。

  • 把函数中的逻辑抽象化,但不要过度设计。否则,最终可能会有太多的模块。可以参考流行的GitHub库,比如scikit-learn,并查看它们的编码风格。

3-重构代码

重构的目的是在不改变代码功能的情况下,重新组织代码的内部结构。它通常是在代码的工作版本上完成(但这个版本还不是很有条理)。它有助于消除重复的函数、重新组织文件结构和添加更多的抽象内容。

4-编写高效代码

编写执行速度快、占用内存空间更少的高效代码是软件开发中的另一项重要技能。

编写高效代码需要多年的经验,但这里有一些小技巧,可以帮助你发现代码是否运行缓慢、掌握提高代码效率的方法:

  • 在运行任何代码之前,请检查算法的复杂性,以评估其执行时间。

  • 通过检查每项操作的运行时间来检查脚本可能存在的瓶颈。

  • 尽可能避免for循环,并将操作矢量化,尤其是在使用NumPy或pandas之类的库时。

  • 借助多进程充分利用计算机的CPU。

5—使用GIT进行版本控制

以我个人的经验来看,GIT+Github能帮助我们提高编码技能,并且能更好地组织项目。当我们在与朋友或同事合作时,它能带来便利,并强迫我们改变过去不好的习惯。

无论是在数据科学还是在软件开发中,使用版本控制系统都有很多好处。

  • 记录你的更改
  • 回到该代码的任何旧版本
  • 通过合并请求,实现团队成员之间的高效协作
  • 提高代码质量
  • 代码评审
  • 为团队成员分配任务并监控他们的进度

下面是常用到的git命令,供参考

6 — 测试代码

如果你正在构建执行一系列操作的数据通道,有一种方法可以确保它按照设计的方式运行:编写测试,对预期行为进行检测。

测试可以像检查输出形状或检查函数返回的预期值一样简单。

为函数和模块编写测试有很多好处:

  • 它提高了代码的稳定性,使我们更容易发现错误
  • 它可以防止意外输出
  • 它有助于检测边缘情况(极端的例子)
  • 它可以防止将损坏的代码推送到生产环境中

7 — 使用日志

一旦代码的第一个版本开始运行,你肯定希望在每一步都监视它,以了解所发生的情况,跟踪进度,或发现错误行为。这就是日志的用途所在。

下面是关于高效使用日志的一些技巧:

  • 根据要记录的消息的性质,使用不同的级别(调试、信息、警告)
  • 在日志中提供有用的信息,帮助解决相关问题。
1
2
3
4
5
import logging
logging.basicConfig(filename='example.log',level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')

参考

结论

很久以前,数据科学家通过制作报告和jupyter完成了他们的工作,但那样的日子已经一去不复返了,那些报告和jupyter的notebook文件与公司的系统和基础设施没有任何联系。

如今,数据科学家开始生产可测试和可运行的代码,这些代码与IT系统无缝集成。因此,我们必须遵循软件工程最佳实践。

我希望本文能让你大致了解这些最佳实践是什么。

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能