函数和DAG:介绍用于数据帧生成的微框架Hamilton

Stefan Krawczyk、Elijah ben Izzy和Danielle Quinn
-旧金山,CA

这篇文章展示了平台和DS团队在Stitch Fix中可以合作完成什么。

创建数据帧很容易,但是管理代码库来实现这一目标可能会变得困难和复杂。当一个数据科学团队寻求帮助,为问题带来更多的结构时,我们一起努力构建汉密尔顿(我们今天也是开源的)!这篇文章是附带的背景故事+介绍。

出身背景

数据科学家的一个常见任务是生成一个数据帧(又名特性工程),该数据帧将用作创建模型的输入。188体育直播网站官方我相信,对于大多数读者来说,下面的情景一定是熟悉的:

df=load_some_data('位置/SQL查询')df[“b列”]=SOME_CONSTANT*df[“a列”]df[“c列”]=df[“b列”].申请(a_transform)#…更多的功能工程188体育直播网站官方模型=适合(df)

对于简单的域和模型,上面的代码可能不会太难管理。但是,如果你在一个领域,比如时间序列预测,你创建的很多列都是其他列的函数[1],其代码可能会变得非常复杂。

这个问题

现在想象一下上面的代码帮助创建某种类型的模型的情况。这些模型是成功的,对业务至关重要,需要每周更新和使用。例如,它正在做一些关键的运营预测。没问题,上面的代码存在于一个脚本中,该脚本可以由计划任务执行。为了便于创建和策划更好的模型,围绕这项关键业务任务组建了一个团队,他们遵循最佳实践来维护工作秩序,例如,代码是版本控制的,因此可以审查任何更改,恢复任何中断的更改,等等。

现在快进5年了,团队的增长刚好跟上业务的增长。你能想象上面的代码库发生了什么吗?它可能变得更加复杂;适应业务变化,新模型,新功能。但是它是一种很复杂的东西吗,是那种你愿意日复一日地工作的东西吗?可能不是。它并不是由于恶意或糟糕的软件工程实践而变得复杂,它只是复杂,因为每次需要更改的时候,数据帧(示例中的df)都需要修改188体育直播网站官方。这就导致了一些问题:

  • 很难确定列(即特征)依赖关系。
    • 举个例子,如果我创建列,列随后使用c, d,然后那些是用作输入创建其他列,这些反过来作为输入,等没有一个简单的方法来确定这些依赖项,除非你读的所有代码。这在大型代码库中是很困难的。
  • 文档编制很难。
    • 如果有多行代码正在执行:
      df[“column_z”]=df[“专栏”]*日志(df[“column_o”])+1.

      那么就很难自然地放置文档。

  • 单元测试很难。
    • 与文档示例类似,如何轻松地对内联数据帧转换进行单元测试?
  • 你需要执行并理解整个脚本以获得任何列(即特性)。
    • 如果您只需要计算整个数据帧的一部分,而需要计算整个数据帧,那么这可能会成为开发的负担。例如,您正在开发一种不需要所有列的新模型…我去煮点咖啡…

在应用程序中,这意味着一个新加入团队的人有很多提升工作要做,任期与调试问题或添加新列(功能)所需的时间直接相关。生产率的学习曲线变得陡峭而漫长;每一次新的修改都会加剧这条曲线——经过多年的发展,学习曲线看起来更像黎明之墙[2]

你们中的一些人可能正在读这篇文章并想知道——“这些人到底怎么了?””- - -很明显解决办法是在函数中加入更多的内容。可以记录功能,并进行单元测试。在这一点上我们不反对你。功能很棒!然而,将代码组织成函数仅仅是一种工具,并不能为我们上面看到的问题提供足够的防护。例如,您将什么作为输入传递给这些函数?整个数据帧?数据帧的特定列?很可能,这仍然意味着您必须执行整个脚本才能获得任何内容。它也不能解决知道哪些列被使用和未使用,以及它们之间的关系的问题。

缝合方法

在Stitch Fix,需求预测和估计(FED)团队负责业务决策所用的运营预测。作为其工作流程的一部分,他们需要大型复杂数据帧来训练和执行他们的模型。很自然,他们会遇到我们上面描述的每一个问题。谢天谢地,现在他们不必自己解决这个问题。

Stitch Fix的Algo平台团队的功能之一是帮助构建库、工具和平台,使数据科学家能够更快地开展工作。Algo平台团队被组织成子团队,专注于数据科学与业务连接所需的不同功能。每个子团队在创建解决方案方面都有很大的自由度。我们的模型生命周期团队(ModelLifecycle Team)的目标是简化模型产品,包括简化特性创建,解决了这个问题。

在进一步讨论之前,我们首先要提到的是,尽管我们研究了各种产品,但我们没有发现任何开源工具能够显著提高我们解决上述问题的能力。第二,我们在这里并没有解决大数据挑战,所以一个基本假设是所有数据都可以放在内存中[3]

结果是汉密尔顿

为了解决美联储的问题,美联储和模型生命周期团队之间建立了合作关系,并且诞生了一个项目来帮助重新思考他们的代码库。从中产生了汉密尔顿,一个用于生成数据帧的python微框架[4].这个框架是专门为解决创建数据帧(包含数千个工程特性和多年的业务逻辑)作为时间序列预测的输入而设计的。Hamilton的主要“技巧”是如何改变数据帧创建和操作的范式来处理这种令人痛苦的复杂性。下面有更多内容。

随着Hamilton于2019年11月问世,美联储的数据科学家专门编写了特殊形状的python函数来生成数据帧。正如前面提到的,简洁的函数有助于解决单元测试和文档的问题,这里我们不是在开辟新领域。然而,如何解决在数据帧中跟踪依赖关系和选择性执行的问题呢?简单;我们建立一个有向无环图,简称DAG,使用python函数的形状属性。

:等等,我糊涂了?解释

而不是让数据科学家编写列转换,如:

df[“C列”]=df[“A列”]+df[“B列”]

Hamilton使数据科学家能够将它们表示为类似以下的函数[5]:

defC列(A列:pd系列,B列:pd系列)->pd系列:“文档”””返回A列+B列

这个函数名等于列的名称。这个输入参数函数所依赖的输入列(或其他输入变量)的名称。这个函数文档字符串[6]成为仅用于此业务逻辑的文档函数体计算是否正常。对于大多数意图和目的,输出应该是一个系列或一个数据帧,但这不是一个硬要求。

注意范式的转变!汉密尔顿没有让数据科学家编写他们随后在大量程序混乱中执行的代码,而是利用函数的定义方式创建DAG并为数据科学家执行。通过这种方法,列关系和业务逻辑可以很容易地被记录和单元测试。

汉密尔顿达格

示例显示了之前的代码、之后的代码以及构造的DAG。

在列定义与函数定义对等的情况下,我们使用python的内置函数检查[7]模块创建所有这些声明函数的DAG。这使我们能够确定需要DAG的哪些部分来计算任何给定节点,即列。这反过来使我们能够减少给定所需输出列列表所需的输入和执行集。

数据科学家现在可以通过两个简单的步骤创建他们想要的数据帧:初始化和执行。这些步骤的内部工作被框架清晰地抽象出来。数据科学家需要为DAG的初始化指定的是一些初始配置参数和python包/模块,以抓取函数定义。要执行,他们只需要指定在最终的数据帧中他们想要的列。

功能示例1:

进口importlib汉密尔顿进口司机初始列={# load from actuals或其他地方#这是我们用作输入的初始数据。“注册”:pd系列([1.,10,50,100,200,400]),“花费”:pd系列([10,10,20,40,40,50]),}#要从中导入函数的模块模块名称=“my_functions”py_模块=importlib导入模块(模块名称)#创建DAG博士=司机驾驶员(初始列,py_模块)#最后决定我们想要什么输出列=[“注册”,“平均工作周支出”,“some_column”]df=博士处决(输出列,显示图形=)

能够将列关系表示为DAG还有一些其他好处:

  • 通过函数上的类型提示,我们可以在运行DAG之前编译它来进行基本的类型检查和验证。
  • 我们可以想象DAG。这是一个快速了解复杂关系的好方法——无论是对于新手还是老手都是如此。
  • 我们可以执行其他图形分析,例如,我们可以删除什么?例如,未使用的列将表示为孤立的DAG。
  • 通过使用DAG对函数执行进行逻辑建模,我们可以为不同的执行环境编译执行。例如,Spark,或适当利用系统上的多核等。

就是这样。它非常简单,而且相对轻量级。

如果你还不相信,这里有一个来自Stitch Fix的数据科学家加入美联储团队的证明:

我之前在一家处理多层信息依赖关系的公司就职。“数据产品”是多年来多个作者在没有系统检查坑洞(如循环引用)的情况下添加层的结果。正因为如此,产品变得非常脆弱,知识转移是通过新成员的一系列特别试验和错误进行的;他们遇到问题,要求主管澄清,然后他们得到一个不透明的解释和变通方案,然后通过电话游戏传播给下一个人(解决方案的机制被传播,但解决方案背后的原因没有传播)。根据我自己的经验,直到我固执地按照信息线索,自己造了一只狗,才出现了“啊哈”的时刻。

有了这方面的经验,在一个已经拥有图表结构来体现复杂依赖性的数据产品上工作是一件愉快的事;它的主要优点之一是,该产品适用于其他通用的图形分析方法。此外,由于抽象将数据帧结构从定量规范中分离出来,它可以帮助新手在不需要先验领域知识的情况下处理信息,因为依赖关系是明确指定的,功能是简单和简洁的。

真人而不是演员

(这是指雪佛兰的“真人非演员”广告——这里的DS是真人,不是演员眨眼)

学习

使Hamilton代码更易于维护

随着移植数据科学代码以使用Hamilton的工作的进展,显然还有改进我们使用Hamilton定义函数的方法的空间。这里有两个我们认为读者会觉得最有趣的例子:

  • 如何处理条件执行?我们是想要一个带有if else语句的函数,还是需要依赖于某些输入的每个单独情况的多个函数?例如,Stitch Fix有多个独立建模的业务线,但是我们希望在可能的地方共享代码。在每个业务线可能需要一些不同逻辑的情况下,我们如何在保持代码简单、易于理解和遵循的同时最好地处理它?

    这里有一个人为的例子。这里的要点是,每个业务线的逻辑可能会变得任意复杂,需要不同的输入,从而混淆依赖结构,并带回我们最初试图与Hamilton一起解决的问题。

    人为示例1:

    deftotal_marketing_spend(业务线:str,tv_spend:pd系列,广播费:pd系列,财政支出:pd系列)->pd系列:”““总营销花。”""如果业务线==“女子”:返回tv_spend+广播费+财政支出否则如果业务线==“男人”:返回广播费+财政支出否则如果业务线==“孩子”:返回财政支出其他的:提高数值误差(未知业务线{业务线}')
  • 我们如何在转换之间保持相似的逻辑? 不同于单个值的重复函数使代码重复且不必要地冗长。例如,设置假期、特殊事件发生时、网站重新设计或新产品发布时的指标变量。

    人为示例2:

    defmlk_假日酒店2020(日期索引:pd系列)->pd系列:“”“MLK假日2020指标”“”返回(日期索引=="2020-01-20")。A型(int)def2020年美国大选(日期索引:pd系列)->pd系列:“”“2020年美国选举指标”“”返回(日期索引==“2020-11-03”)。A型(int)defthanksgiving_2020(日期索引:pd系列)->pd系列:“”“感恩节2020的指标”“”返回(日期索引==“2020-11-26”)。A型(int)

幸运的是,我们处理的是函数,所以我们用来帮助处理上述情况的常用策略是创建装饰师. 这些作为语法甜头以帮助保持代码更简洁和愉快的编写。让我们描述一下改进上述示例的装饰器。

避免使用@config.when*的if else语句

在上面的“人为示例1”中,我们有一系列否则基于业务线的报表。为了使依赖关系更清晰,我们定义了三个独立的函数,并用@config.when描述该定义适用的条件。

@配置(业务线=“孩子”)deftotal_marketing_spend__kids(财政支出:pd系列)->pd系列:"""儿童的全部营销费用。"""返回财政支出@配置(业务线=“男人”)def营销费用总额(业务线:str,广播费:pd系列,财政支出:pd系列)->pd系列:"""为男士所花的全部营销费用。"""返回广播费+财政支出@配置(业务线=“女子”)deftotal_marketing_spend__womens(业务线:str,tv_spend:pd系列,广播费:pd系列,财政支出:pd系列)->pd系列:“”“女性的总营销支出。”“”返回tv_spend+广播费+财政支出

在构造DAG时,我们只保留满足装饰器中指定的过滤条件的函数。过滤标准来自实例化时提供的配置/初始数据(功能示例1,变量初始列)。这使我们能够根据一些配置参数选择性地包括或排除函数。

你可能也注意到了__函数名中的后缀指示符。我们希望目标列名在不同的配置中保持不变,但是一个函数在给定的文件中只能有一个定义,因此我们不得不对其进行不同的命名。在框架中使用decorator+dunder命名约定,我们可以有条件地定义输出列的底层函数;如果我们检测到函数名中的后缀,框架就会知道如何去除它,从而适当地创建输出列名和定义。

使用@config.when有助于确保避免复杂的代码,并在DAG构建时捕获错误的配置。例如,在前一个版本中,如果我们提供了一个错误的business_line值,那么直到代码执行时(如果有的话!),我们才知道它。使用时@config.when,如果传入不正确的business_line值,则在请求该输出列时会出错,因为无法满足该请求。

减少使用@参数化维护的代码

天真地使用Hamilton会让简单的函数感到不必要的冗长:一行定义函数,另几行用于文档,然后是一个简单的正文。为了减少这种感觉,我们可以创建一个函数来跨多个列定义共享代码。使我们能够做到这一点的是用@parametrized告诉Hamilton它代表什么列(或者说,可以调用这个函数的所有参数)。例如,我们可以按照以下方式重写“contrived example 2”:

我们在这里定义输入SOME_DATES={#(输出名称,文档):要传递的值(“mlk_假日2020”,“MLK 2020”):"2020-01-20",(“2020年美国大选”,“美国2020年选举日”):“2020-11-03”,(“2020年感恩节”,“2020年感恩节”):“2020-11-26”,}@函数修饰符参数化(参数=“单身日”,assigned_output=SOME_DATES)def创建日期指示器_(日期索引:pd系列,single_date:str)->pd系列:帮助从单个日期创建指示符系列。""返回(日期索引==single_date)。A型(int)

除了更简洁之外,我们更喜欢使用“人为示例2”,主要原因有两个:

  1. 如果我们改变逻辑(例如在我们的例子中如何创建日期指示器),我们只需要在一个地方改变它。
  2. 如果我们有更多的这些要创建,它只需要添加一条线。

我们喜欢这个decorator的额外原因:

  1. 文档即使我们只添加了一行,我们仍然可以正确地说明文档,这确保了每个函数仍然是文档化的。

在这里,我们只讨论了其中的两个decorator,汉密尔顿附带。我们创建的其他装饰器对其他上下文有帮助,这些上下文来自于我们对框架的其他学习。

代码检查更简单

在创建Hamilton时,简化代码审查过程并不是预期的目标。当您强制将业务逻辑更紧密地封装到功能中时,检查更改就会容易得多。

例如,汉密尔顿不必跟踪涉及大量文件和代码行的更改,因为逻辑封装得不太好,所以事情要简单得多。迫使数据科学家编写清晰的函数来解释他们需要什么样的输入以及他们创建了什么样的输出,这对于审查者来说是一个更简单、更容易理解的工件。这样做的结果是,代码审阅者效率更高,在审阅过程中出现的错误更少。

结论

Hamilton是一个框架,通过编写特殊形状的函数,帮助数据科学家团队在共享代码库中管理复杂数据帧的创建。通过处理如何处理,汉密尔顿允许数据科学家关注什么。这是Stitch Fix的Model Lifecycle和FED团队成功跨职能合作的结果,自2019年11月以来一直在生产中运行。我们认为我们在Stitch Fix创建数据帧的新方法很适合我们的背景,并很高兴与您分享我们的经验。

最后,我们很高兴地宣布我们是开源的汉密尔顿! 如果您必须维护包含数百(甚至数千)列的数据帧,并且所有数据都可以放在内存中,那么它将非常适合您[8]. 只是pip安装sf hamilton开始。有关开始使用的更多细节汉密尔顿以及如何使用它,我们建议您参考自述.如果你看过《汉密尔顿》,我们会对你的反馈或想法感兴趣;看到github问题组为了我们正在策划的创意!我们还创建了一个不协调服务器作为与感兴趣的人接触的另一种方式。

另外,一如既往,我们正在招聘

另外,我们要感谢审稿人和编辑Albert Yuen和Sven Schmit的反馈。非常感谢!

脚注

[1]↩按我们的说法,专栏是特色。在本文中,我们将坚持将特性描述为数据框架中的列。

[2]↩黎明之墙是一条上升路线的名字埃尔卡皮坦酒店在约塞米蒂国家公园。很陡,很难爬上去。

[3]↩这是我们希望消除的限制。

[4]↩这个框架比仅仅创建数据帧更强大,但这是它的第一个应用。

[5]↩由于使用Hamilton编写函数会稍微增加一些冗长,所以我们使用了decorator来帮助保存代码. 继续阅读,看看一些例子。

[6]↩你知道吗斯芬克斯,一个python文档工具,您也可以轻松地编写这个函数文档。我们有一个后合并工作,从这些代码构建sphinx文档,以帮助更好地显示它。

[7]↩inspect模块上的TL;DR允许您非常容易地访问函数的名称和签名。

[8]↩现在Hamilton在一台机器上执行,所以您需要能够将数据加载到内存中,以便它运行。幸运的是,像AWS这样的服务使您能够轻松地扩展所使用的机器,因此您可以使用这种方法走得很远。

发这个帖子! 在LinkedIn上发布
188滚球注册平台

来和我们一起工作吧!

我们是一个多元化的团队,致力于打造卓越的产品,我们希望您的帮助。您是否希望与卓越的同行一起打造卓越的产品?加入我们吧!

Baidu