原文:
towardsdatascience.com/efficient-testing-of-etl-pipelines-with-python-f4373ed5a5ff
在今天以数据驱动的世界中,组织高度依赖准确的数据来做出关键的商业决策。作为一名负责任且值得信赖的数据工程师,确保数据质量至关重要。即使在仪表板上显示错误数据的一小段时间,也可能导致整个组织迅速传播错误信息,就像高度传染性的病毒在生物体中传播一样。
但我们如何预防这种情况呢?理想情况下,我们应完全避免数据质量问题。然而,令人遗憾的是,这是不可能完全避免的。尽管如此,我们仍可以采取两个关键措施来减轻影响。
-
当数据质量问题时,第一时间知道
-
最小化修复问题所需的时间
在这篇博客中,我将向您展示如何在代码中直接实现第二点。我将使用 Mockaroo 生成的数据在 Python 中创建一个数据管道,并利用 Tableau 快速识别任何失败的根源。如果您正在寻找替代的测试框架,请查看我的文章Python 中 Great Expectations 简介。
所有文件、代码、Tableau 仪表板,以及在此博客中使用的所有数据文件,您都可以在我的GitHub 账户上访问。
本地化问题
让我们先从如何最小化修复问题所需的时间开始。想象一下,我们已经有了一个数据管道在运行,并且我们收到通知,输出数据是错误的。问题是:我们如何尽快解决问题?
第一步始终是问:在哪里? 问题最初发生在哪里?为了最小化修复数据质量问题的耗时,我们需要一种快速识别出错的方法。想象一下,你有一张地图,它会立即指出,“问题就在这里。”一种有用的可视化方式可能是彩色编码的流程图,它可以一目了然地突出显示问题区域。
图片由作者提供。
在这个例子中,我们有一个简单的 ETL 流程,其中我们读取数据,过滤到相关的列和行,更改特定列的数据类型,最后将数据写入目标源。每个矩形代表我们工作流程的一部分,我们在每个步骤之后都实施了检查,以验证是否满足某些条件。虽然白色矩形表示数据按预期流动,但红色矩形表示在那个点发生了意外情况。
对于我们关于问题出现位置的问题,这种可视化非常有帮助。最早表明问题的验证步骤是数据类型更改步骤。例如,如果一个数值列无法从字符串转换为整数,它可能导致数值列(本意是通过折线图在仪表板上显示并用于聚合)因为被加载为字符串而无法求和。
我们不仅能够立即定位问题,还可以定义导致意外结果的可能原因的数量。通过扩展我们的初始 ETL 管道,我们可以通过流程图可视化日益增加的复杂性和它带来的价值。
图片由作者提供
我们再次遇到了加载的数据质量问题。然而,这次问题不仅在于数据类型的变化,还在于读取数据 3。可能没有行被加载,或者列与之前的加载不同。
我们可以看到,这种用于监控数据质量的可视化可以节省在识别和解决问题上的时间。但我们是怎样从我们的管道中生成这样的图表的呢?为了回答这个问题,让我们来看一个实际例子。
我们如何实现这一点?
在我们深入具体用例之前,让我们考虑我们需要哪些信息来构建可视化。最终,我们的目标是连接 Tableau 到一个表格,以创建此类图表。
由于我们想要跟踪每个步骤的测试结果,我们需要为每个步骤的名称有一个列。我们可以通过使用存储我们的 DataFrames 的变量的名称来实现这一点。
我们还需要一个用于测试结果的列,它可以是二进制的,并将用于为相应的方格着色。
理论上,我们还需要放置方格的坐标数据。这确保了例如,读取数据 1 位于左上角,其后的步骤位于其右侧。我们将为每个变量分配一个x和y值。
我已经准备了一个 Python 代码片段,您可以将其用于所有管道以获取 x 和 y 值,以及变量名称。该代码以您的 Python 文件源代码作为输入,并提取用于存储您的 DataFrames 的变量名称。您只需要提供 Python 文件的文件路径。如果您有任何问题或需要进一步澄清,请随时联系。
对于测试结果,我们将遵循以下步骤的特定模式:
-
定义测试函数:每个函数代表一个单独的测试。
-
定义变量列表:此列表指定了应收集测试结果的变量。
-
收集测试结果。
在第二步中,我们简单地创建一个 Python DataFrames 列表,这些测试将在其中执行。想法是某些测试也应该适用于多种类型的步骤。
作者图片。
用例
假设你是某家在线商店的数据工程师,营销团队有一个请求。他们希望更好地了解他们的客户,并检查 2023 年全年按年龄组分布的销售情况。由于他们不确定如何分组年龄,如果你们能准备数据,以便为每个单独的年龄提供销售数据,他们将不胜感激。
为了提供营销团队所需的结果,我们首先需要检查哪些数据实际上是可用的。让我们从订单数据开始。
作者图片。由 Mockaroo 生成数据。
每个订单-产品组合代表数据集中的一行。由于一个客户在购物篮中可能有几个不同的产品,因此对于同一个 order_id 可能会有重复的行,每行都与不同的 product_id 相关联。customer_id 使我们能够识别谁下了订单,而 order_date 提供了关于订单何时被下架的信息。在这个例子中,total_price 列是我们分析的关键列。
现在我们有了相应订单的信息,缺少的是关于年龄的信息,或者更普遍地说,是关于客户的信息。我们可以从客户表中获取这些信息。
作者图片。由 Mockaroo 生成数据。
在这里,我们有一些关于客户的信息,特别是他们的年龄。现在我们有了进行信息合并所需的一切。接下来,我们只需要导入我们的库并创建我们的管道。
import pandas as pd
import ast
import json
import re
#Load Data 1
df1_read = pd.read_csv(r"...order_table.csv")
# Load Data 2
df2_read = pd.read_csv(r"...customer_table.csv")
# 1.2 Adjust column
df1_change_datatype_orderdate = df1_read.assign(order_date=pd.to_datetime(df1_read['order_date']))
#1.3 Add new column
df1_add_column_year = df1_change_datatype_orderdate.assign(year=lambda x: x['order_date'].dt.year)
#1.4 Filter year 2023
df1_filtered_year = df1_add_column_year[df1_add_column_year['year']==2023]
#1.5 Aggregate
df1_aggregated = df1_filtered_year.groupby(['customer_id']).agg(
total_price=('total_price', 'sum'),
unique_order=('order_id', 'nunique')
).reset_index()
# 1.6 merge
merged_df1_df2 = pd.merge(df1_aggregated,df2_read,left_on='customer_id',right_on='id')
在加载了两个 DataFrame 之后,我们将 order_date 列转换为 datetime 数据类型,提取了年份,然后筛选出 2023 年下架的订单。接下来,我们按客户汇总了总价。最后,我们将客户信息与每个客户的销售数据结合起来。现在,我们可以直接将数据交付给营销团队,或者交付给数据消费者,例如数据分析师。
对于技术背景,由于我们从源代码中提取变量名,我们需要使用特定的命名模式来标记它们。在这种情况下,模式由"df"后跟我们要读取的 DataFrame 的编号,然后是一个下划线,接着是报告的操作(例如,“add_new_column”),最后是一个下划线后跟对操作的指定。遵守此模式很重要,因为它有助于从源代码中提取变量名,并有助于自动维护技术文档。
现在我们只需要遵循我们的测试方法。首先,我们将定义我们的测试函数。我们将关注一个函数,如果 DataFrame 包含行则返回 1,如果没有行则返回 0。
# Define Test Functions
def check_row_count(df):
return 1 if df.shape[0] > 0 else 0
在你的情况下,你需要实现多个函数。例如,你可以包括不仅数据测试函数,还包括逻辑测试,如单元测试。
在第二步中,我们定义了我们想要测试的变量列表。由于我们的每个 DataFrame 都应该包含行,我们将对所有它们应用测试。
test_variable = [] # List to store the names of the tested variables
test_result = [] # List to store the results of the tests
# List of DataFrames to be tested
tested_dataframes = [
'df1_read',
'df1_change_datatype_orderdate',
'df1_add_column_year',
'df1_filtered_year',
'df1_aggregated',
'merged_df1_df2',
'df2_read'
]
在这里,你应该注意两点。首先,我们创建了两个列表来存储我们测试的变量及其相应的测试结果。其次,测试的 DataFrame 名称存储为字符串。我们需要字符串而不是 DataFrame,这样我们才能将每个 DataFrame 的名称添加到我们的测试变量列表中。如果我们想检索结果 DataFrame,我们可以简单地使用 eval 函数,该函数将字符串作为代码执行。
现在,我们只需遍历我们的 DataFrame,应用我们的函数,并收集我们的测试结果。
# Iterate over each DataFrame name in the tested_dataframes list
for i in tested_dataframes:
# Append the result of the row count check for the current DataFrame to the test_result list
test_result += [check_row_count(eval(i))]
# Append the current DataFrame name to the test_variable list
test_variable += [i]
# Create a DataFrame to store the test results with variable names and their corresponding results
result_test_dataframe = pd.DataFrame({'Variable': test_variable, 'Test_Result': test_result})
由于我们可以在一个步骤中进行多个测试,我们希望使用最小函数来汇总结果。这样,如果至少有一个测试对一个变量失败,整体测试结果将为 0。
# Summarize the test results by taking the minimum result for each variable
result_test_dataframe_summarized = result_test_dataframe.groupby('Variable').agg({'Test_Result': 'min'}).reset_index()
最后,我们将我们的结果 DataFrame 与另一个名为 relationship_merged 的 DataFrame 合并。我在介绍中提到,我不会关注返回变量 x 和 y 坐标的代码。relationship_merged DataFrame 正好包含这些信息。
# Merge the summarized test results with the relationship_merged DataFrame
endresult = pd.merge(relationship_merged, result_test_dataframe_summarized, on='Variable', how='left')
在写出结果后,我们得到了最终输出,看起来像这样:
作者图片
我们可以确定有三个 DataFrame 的测试失败了。这表明在 df1_filtered_year 之后没有行存在。虽然我们可以检查 df1_filtered_year 的输入和输出,但我可以确切地解释发生了什么。当我过滤 2023 年时,很明显我们的源数据不包含该年的任何数据。可能 df1_read,由另一个过程输出,从未在其文件中包含 2023 年。通过这种可视化,我们现在可以一眼就发现数据质量问题。
结论
在数据工程快速发展的领域中,确保数据质量至关重要。通过在我们的 ETL 管道中实施有效的测试策略,我们可以主动识别和解决数据质量问题,防止其影响决策。所讨论的可视化技术不仅简化了异常检测,还增强了数据团队之间的协作。随着我们继续改进我们的流程并利用 Python 和 Tableau 等工具,我们能够提供可靠的见解,推动业务成功。
非常感谢你们坚持看到最后!如果你觉得我的内容有帮助,或者至少有点娱乐性,如果你能给我点个赞或者点击关注按钮,我会非常高兴!
对于那些热爱深入代码的你们,你可以在我的GitHub个人资料页上找到所有数据和神奇的脚本。如果你在寻找一种在 ETL 代码中实现单元测试的好方法,我可以推荐你观看 Jacqueline Bilston 的这个视频。她深入探讨了如何使用 PySpark 在她的 ETL 管道中快速创建单元测试——绝对值得一看!
1524

被折叠的 条评论
为什么被折叠?



