原文:https://towardsdatascience.com/etl-pipelines-in-python-best-practices-and-techniques-0c148452cc68

照片由 Produtora Midtrack 拍摄并从 Pexels.com 获得的照片
当构建一个新的 ETL 管道时,考虑三个关键要求至关重要:泛化性、可扩展性和可维护性。这些支柱在数据工作流程的有效性和持久性中发挥着至关重要的作用。然而,挑战通常在于在这三者之间找到正确的平衡——有时,提高一个方面可能会以牺牲另一个方面为代价。例如,优先考虑泛化性可能会导致可维护性降低,从而影响整体架构的效率。
在这篇博客中,我们将深入探讨这三个概念的本质,探讨如何有效地优化您的 ETL 管道。我将分享实用的工具和技术,这些可以帮助您提高工作流程的泛化性、可扩展性和可维护性。此外,我们还将研究现实世界的用例,以分类不同的场景,并明确定义满足您组织特定需求的 ETL 需求。
泛化性
在 ETL 的背景下,泛化性指的是管道处理输入数据变化而不需要大量重新配置的能力。这是一个非常理想化的特性,因为其固有的灵活性可以在您的工作中节省大量时间。这种灵活性在涉及许多数据利益相关者提供输入数据时尤其有益。涉及的人越多,输入数据源发生变化的可能性就越大。
此外,如果您想在不同项目中重用您的管道,泛化性允许快速适应以满足每个新项目的特定要求。这种能力不仅提高了效率,还确保了您的 ETL 流程能够保持稳健并对不断变化的数据需求做出响应。
为了更具体地理解泛化性实际上意味着什么,我想提供一个简单的例子,这是一个绝对不具有泛化性的小管道。这个例子涉及一个产品每年的销售数据,高级领导团队每年都感兴趣进行审查。
sales = pd.DataFrame({
'Year': [2019, 2020, 2021, 2022, 2023],
'Sales': [30000, 25000, 35000, 40000, 45000]
})
领导团队每年年底都会讨论当年的销售数据。到 2023 年底,他们要求你提供那一年的数据。提供这些数据的一种非泛化方法涉及一个简单的过滤解决方案,其中您将过滤数据集,只包括 2023 年的记录。
sales[sales['Year'] == 2023]
这个管道根本不具备通用性。明年,你需要再次运行代码,并将年份从 2023 手动更改为 2024。对于我们的用例,如果我们构建的管道能够自动检索最高可用的销售数据,那么它将具有更强的通用性。
sales[sales['Year'] == sales['Year'].max()]
通过进行一次调整,你可以避免每年都要进入代码中更改年份数字的情况。这种调整使得管道能够自动选择最近可用的销售数据。
当然,在这个例子中,这不是一个重大的变化。然而,在现实中,管道可能要大得多,调整 30 个不同位置的一个数字或字符串可能会变得相当耗时。当变化出乎意料时,这种情况变得更加复杂,例如当系统中的列被重命名或列的数据类型发生变化时。因此,在考虑通用性时,需要考虑几个方面。
但我们如何使我们的管道更具通用性呢?不幸的是,在提高工作流程通用性方面,没有一种一刀切的解决方案。每个用例都是独特的,因此解决方案必须相应定制。然而,有一些技巧和技巧可以应用于各种问题。
-
避免硬编码:硬编码涉及直接将特定值输入到你的表达式中,就像我们在我们的销售示例中所做的那样。相反,目标是要使用可以适应数据变化的动态表达式。
-
使用映射表:如果你预计列标题可能会变化,考虑创建一个字典或映射表来处理潜在的变体。这允许你根据数据集中可能出现的名称动态地重命名列。
# Mapping table (dictionary) for renaming
column_mapping = {
'SALES': 'Sales',
'Revenue': 'Sales',
'Sales_2023': 'Sales'
}
# Rename columns using the mapping table
df_renamed = df.rename(columns=column_mapping)
- 利用正则表达式:类似于使用映射表,正则表达式也可以用来动态重命名列标题。正则表达式方法的优点是,你不必知道标题将如何变化。相反,你可以定义一个捕获潜在变体的模式,并相应地将所有相关列重命名为你希望的名字。
import pandas as pd
import re
# Regex pattern for possible variations of "Sales"
pattern = r"(?i)(.*sales.*|.*verkäufe.*)" # Case-insensitive, also match "Verkäufe" in German
# Find columns matching the pattern
sales_column = [col for col in df.columns if re.match(pattern, col)]
# Rename the matching columns
for col in sales_column:
df.rename(columns={col: 'Sales'}, inplace=True)
- 强制数据类型:ETL 管道在期望为数值的列被更改为字符串时可能会遇到问题。这可能导致某些聚合函数或数学运算失败。为了减轻这种情况,Pandas 提供了有用的函数来强制特定的数据类型。任何无法转换的值将被替换为 NaN。此外,你可以在使用 dtype 参数加载数据时定义数据类型。
# Convert to numeric, forcing invalid values to NaN
df['col2'] = pd.to_numeric(df['col2'], errors='coerce')
# Convert to datetime, forcing invalid dates to NaT
df['date_col'] = pd.to_datetime(df['date_col'], errors='coerce')
# Specify data types while reading a CSV file
df = pd.read_csv('data.csv', dtype={'col1': int, 'col2': float, 'col3': str})
-
不同场景的覆盖范围:如果你清楚地了解你的管道中哪些部分需要更强的通用性,你可以实现 if-else 逻辑来处理各种输入。虽然这可以增强通用性,但通常是以可扩展性和可维护性为代价的。
-
使用配置文件:将管道配置(如输入/输出路径、列名、过滤器和其他参数)存储在外部的 YAML、JSON 或 TOML 文件中。这种方法将逻辑与配置分离,使得调整不同环境或数据集的参数变得更加容易,而无需修改代码。
import yaml
with open('config.yaml') as file:
config = yaml.safe_load(file)
可扩展性
可扩展性指的是 ETL 管道处理数据量增加或数据源数量增长的能力。不可扩展的 ETL 管道在处理大型数据集时遇到挑战,导致计算成本更高和更长的处理时间。这个问题在未来预期数据量显著增长的情况下尤其关键。
我们能做些什么来防止这种情况?
-
尽早进行过滤:在管道的早期阶段应用过滤器,以最小化后续步骤中处理的数据量。
-
避免不必要的转换:避免不必要的转换,如排序,这些转换可能会增加开销并减慢处理速度。
-
使用增量加载:每次重新加载整个数据集可能会非常低效且成本高昂,尤其是当数据量增长时。相反,专注于仅转换新数据或更改的数据。
-
优化数据格式:选择高效的数据格式进行加载和输出。对于列式数据,考虑使用 Parquet 文件,它优化了存储和处理时间。对于半结构化数据,使用压缩的 Avro 或 JSON。
-
最小化连接:涉及大型数据集的连接可能会在性能方面造成高昂的成本。考虑对表进行反规范化,以减少对复杂连接的需求。
-
监控:实施监控以估计不同数据量下 ETL 过程所需的时间。通过测量执行 ETL 过程的不同行数所需的时间,你可以更好地了解性能。考虑将你的 ETL 过程封装在函数中,以便更容易进行测量。
为了说明我们如何监控数据,我将使用我在关于**使用 Python 高效测试 ETL 管道**的博客中提到过的示例数据。在本节中,我不会深入探讨这个示例的细节。如果你对更全面的解释感兴趣,请参考其他博客。
我们将首先复制我们的 ETL 管道,并将其封装在一个函数中:
import pandas as pd
def run_etl_pipeline(order_df, customer_df):
# 1.2 Adjust column (convert 'order_date' to datetime)
df1_change_datatype_orderdate = order_df.assign(order_date=pd.to_datetime(order_df['order_date']))
# 1.3 Add new column 'year'
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 data
df1_aggregated = df1_filtered_year.groupby(['customer_id']).agg(
total_price=('total_price', 'sum'),
unique_order=('order_id', 'nunique')
).reset_index()
# 1.6 Merge the aggregated data with customer data
merged_df1_df2 = pd.merge(df1_aggregated, customer_df, left_on='customer_id', right_on='id')
return merged_df1_df2
请注意,我们不会在函数内读取数据。相反,输入的 DataFrames 将作为参数提供。
# Load data externally
import time
import pandas as pd
order_df = pd.read_csv(r"order_table.csv")
customer_df = pd.read_csv(r"customer_table.csv")
sample_size=[]
duration_list =[]
for i in range(0,1000001,10000):
start_time=time.time()
order_df_sample = order_df.sample(n=i,replace=True)
run_etl_pipeline(order_df_sample, customer_df)
end_time = time.time()
duration = end_time - start_time
sample_size+=[i]
duration_list+=[duration]
scalability_tracker = pd.DataFrame({'Sample':sample_size,'Duration':duration_list})
接下来,我们将从 10,000 步循环到 1,000,000 步,通过其中一个 DataFrame 进行循环。在这个过程中,我们将从 DataFrame 中随机选择 10,000 行,允许选择相同的行多次。我们将使用模拟数据测量执行前和执行后的时间。最终,这将为我们提供一个可扩展性跟踪器,我们也可以将其可视化。
作者图片
在这个例子中,按顺序表转换 1,000,000 行大约需要 0.8 秒。更有趣的是,我们观察到转换时间呈线性关系。这使我们能够轻松估计我们预计在下个月收到的数据所需的时间,使我们能够主动预防任何数据交付延迟。
可维护性
可维护性指的是 ETL 管道随时间更新、修改或修复的难易程度。在一个数据量不断增加、数据团队不断扩大的快速变化的世界里,拥有可维护的工作流程至关重要。这样的工作流程使新团队成员更容易理解流程,并有助于保持对管道的整体概述。此外,在处理错误和随着需求演变进行更新时,可维护的工作流程修复起来更快,也更易于更新。
-
文档: 为您的 ETL 管道编写清晰简洁的文档。这应包括如何运行管道、配置、依赖项、预期输入/输出和常见故障排除步骤。
-
测试: 实施测试可以在查找和修复错误时节省您大量时间。您可以使用各种测试策略。我强烈推荐您查看我的关于使用 Python 高效测试 ETL 管道的博客 – 晚些时候您可以感谢我!😊
-
使用数据血缘: 维护对您的 ETL 管道及其相互依赖关系的概述,有助于在整个工作流程中定位问题。
-
定义目的: 当评估一个新的 ETL 管道时,您应该问的第一个问题之一是:为什么? 理解管道的目的以及为什么它需要存在。这个定义不仅有助于其他团队成员更好地理解您的管道,而且由于目标和目标明确,可以更快地解决错误。
这些只是维护 ETL 工作流程的一些关键提示,但我知道还有许多其他策略。对我来说,这些是最重要的实践,有助于有效地管理我的工作流程。
您如何保持工作流程的可维护性? 我非常想听听您的想法,所以请在评论中分享您的技巧!
用例
用例 1:一次性获取的数据迁移
你被要求在收购后迁移两家公司之间的数据。你需要将收购公司系统的数据集成到自己的系统中,用于财务报告、客户记录和库存管理。这是一个一次性项目,其中主要挑战是收购公司的数据结构完全不同,需要灵活处理不同的数据格式和模式。
被收购的公司可能会使用各种系统,例如 Salesforce、QuickBooks 和专有数据库。一个通用的 ETL 管道必须能够适应从这些不同的系统中提取数据,无论它们的格式或结构如何。由于集成是一次性工作,系统不需要高度可维护。你不需要经常重新访问或更新管道,因此可维护性是一个较低优先级。可扩展性不是问题,因为数据迁移涉及的是来自被收购公司的有限、固定的数据量。系统不需要随着时间的推移处理越来越多的数据量,这与常规的运营 ETL 流程不同。
作者图片
用例 2:电子商务销售的实时数据处理
想象一个在用户流量和交易量快速增长的电子商务平台,尤其是在黑色星期五或假日销售这样的高峰季节。该平台需要处理和分析实时销售数据以生成见解,优化库存,并向用户提供个性化推荐。
平台在高峰购物期间预计会有数百万笔交易。ETL 管道必须处理这种大量的数据流入,而不会降低性能。随着业务的增长, incoming 数据量将增加,需要可扩展的解决方案,以便轻松适应这种增长。系统必须适应越来越广泛的数据来源,包括新的销售渠道和支付处理器。ETL 管道专门针对电子商务销售环境,因此可能不需要推广到其他数据来源或此背景之外的使用案例。在高峰季节,重点是最大化吞吐量和最小化延迟。长期的可维护性和处理未来项目的能力可能不如确保当前管道能够有效扩展。
作者图片
结论
总之,设计有效的可通用、可扩展和可维护的 ETL 管道对于现代数据工作流程至关重要。通过关注这三个支柱,数据工程师可以创建不仅满足当前需求,而且能够适应未来变化和增长的管道。
随着数据生态系统的不断发展,采用这些原则将使组织能够有效地利用其数据,从而推动更深入的洞察和决策。我鼓励您反思自己的 ETL 实践,并考虑如何将这些策略融入您的流程中。在下面的评论中分享您的经验和技巧!
感谢您与我一同踏上 ETL 最佳实践的旅程!如果您喜欢分享的见解或觉得它们有用,我将非常感激您的支持。一个简单的点赞或关注对我来说意义重大!您的参与激励我继续创作帮助我们一起在数据景观中导航的内容。
Alteryx 社区,2023. 实施工作流程策略:泛化性。Alteryx 社区。可在以下网址获取:community.alteryx.com/t5/Engine-Works/Implementing-Workflow-Strategy-Generalizability/ba-p/1296109 [于 2024 年 10 月 18 日访问]。
Alteryx 社区,2023. 实施工作流程策略:可扩展性。Alteryx 社区。可在以下网址获取:community.alteryx.com/t5/Engine-Works/Implementing-Workflow-Strategy-Scalability/ba-p/1297766 [于 2024 年 10 月 18 日访问]。
1144

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



