数据管道与 Airflow 和 AWS 工具(S3、Lambda 和 Glue)
了解这些工具及其集成方式
·
关注 发表在 Towards Data Science · 17 分钟阅读 · 2023 年 4 月 6 日
–
图片由Nolan Krattinger拍摄,发布在Unsplash
介绍
几周前,当我在思考新的文章创意时,我想:嗯,我需要更多地了解(和讨论)云计算和这些相关内容。我在本地环境中已经练习了很多,使用了开源工具,远离了专有解决方案……但世界是云的,我认为这种情况不会很快改变……
然后我写了一篇关于创建 使用本地 Spark 和 GCP 的数据管道 的文章,这是我第一次使用云基础设施。今天的文章遵循相同的理念:将本地和云端的组件组合在一起,构建数据管道。但这次,我们将使用 AWS,而不是 GCP。
AWS 是迄今为止最受欢迎的云计算平台,它拥有大量的产品来解决你想象中的每种特定问题。至于数据工程解决方案,它也不例外:它们有数据库、ETL 工具、流媒体平台等等 — 一整套工具,让我们的生活更轻松(只要你为它们付费)。
所以,请跟随我在这篇文章中从零开始开发一个完整的数据管道,使用 AWS 工具集中的一些组件。
未赞助。
工具 — TLDR
Lambda functions 是 AWS 最著名的无服务器计算解决方案。“无服务器”意味着应用程序不依附于特定的服务器。相反,每当发出请求时,会快速启动一个新的计算实例,应用程序响应后,该实例将被终止。因此,这些应用程序应该是小型的、无状态的。
Glue 是 AWS 的一种简单的无服务器 ETL 解决方案。使用可视化界面、代码编辑器或 Jupyter notebooks 创建 Python 或 Spark 处理作业。按需运行作业,只为执行时间付费。
S3 是 AWS 的 blob 存储。这个概念很简单:创建一个存储桶并在其中存储文件。稍后通过它们的“路径”读取这些文件。文件夹是虚假的,对象是不可变的。
Airflow 是一个“工作流协调器”。它是一个开发、组织、排序、调度和监控任务的工具,使用一种称为 DAG 的结构 — 有向无环图,用 Python 代码定义。
数据
为了充分探索这些工具的功能,我选择使用来自巴西 ENEM(国家高中考试)的数据。这个考试每年举行,是大多数巴西公立和私立大学的主要入学门槛;它在四个主要知识领域评估学生:人文科学、自然科学、数学和语言(每个领域 45 道题目)。
ENEM 2010,人文科学及其技术。图片来源:作者。
我们的任务是从实际的考试中提取这些问题,这些考试以 PDF 形式在 MEC(教育部)网站上提供 [CC BY-ND 3.0]。
从 PDF 中提取问题。图片来源:作者。
实现过程
在阅读了关于 AWS 可用的数据处理工具的一两行内容后,我决定用 Lambda 和 Glue 作为数据处理组件,S3 作为存储,和本地 Airflow 来协调一切,来构建一个数据管道。
简单的想法,对吧?
嗯,可以说是这样。
正如你在这篇帖子中会注意到的那样,问题在于有很多配置、授权、角色、用户、连接和密钥需要创建,以使这些工具能够顺利协作。
我保证会尽量覆盖大部分步骤,但为了缩短帖子,我需要省略一些细节。
说到这里,让我们看看每个工具的功能,见下图。
提议的管道。图像由作者提供。
本地 Airflow 实例将负责协调所有操作,从 MEC 网站下载 PDF 并将其上传到 S3。此过程应该会自动触发 Lambda 函数执行,该函数将读取 PDF,提取其文本,并将结果保存到 S3 的“另一个地方”。然后,Airflow 应触发一个 Glue 作业,该作业将读取这些文本,提取问题,并将结果以 CSV 格式保存到 S3。
步骤:
-
(Airflow) 下载 PDF 并上传到 S3
-
(Lambda) 从 PDF 中提取文本,将结果以 JSON 格式写入 S3
-
(Airflow->Glue) 读取文本,拆分问题,添加适当的元数据,并将结果保存为 CSV
0. 设置环境
本项目中使用的所有代码都可以在这个 GitHub 仓库中找到。
第一步是配置本地环境。
你需要在本地机器上安装 Docker 来创建 Airflow 集群。Docker 镜像已经配置好,可以自动从头创建一个新环境,因此我们可以更多地关注实现部分。
在 docker-compose.yaml 文件的相同文件夹中,使用以下命令启动环境:
docker compose up
在初始配置后,airflow Web 服务应该在 localhost:8080 启动。默认的用户名和密码都是 ‘airflow’。
如果在启动 Airflow 时遇到问题,请尝试为新创建的卷赋予读写权限,例如:chmod 777 。
接下来进入云环境。
你需要一个 AWS 账户,这里有一个警告——注意账单。S3 存储和 Lambda 函数的使用将会在免费配额范围内(如果你还没有用完),但 Glue 执行会收取一些美元美分。记得在工作完成后关闭所有服务。
一旦你创建了账户,请按照以下步骤操作:
-
在 S3 中创建一个名为 enem-bucket 的新 Bucket。
-
创建一个新的 IAM 用户,授权读取和写入 S3 并运行 Glue 作业,存储生成的访问密钥对。
-
在 airflow UI(localhost:8080)中,点击 admin->connections 标签,创建一个新的 AWS 连接,命名为 AWSConnection,使用之前创建的访问密钥对**。**
创建 AWS 连接。图像由作者提供。
可能还需要一些其他小调整,AWS 是一个疯狂的地方,但上面的列表应该涵盖了整体过程。
曾经有一个人吃了一整架飞机。秘密在于这个过程持续了 2 年,他一块一块地吃掉了它。请将这种哲学带到本文中。接下来的部分将详细介绍每个管道的实现,一步一步地构建完整的项目。
1. 使用 Airflow 上传文件到 AWS
首先,在**/dags文件夹中创建一个 Python 文件,我将其命名为process_enem_pdf.py**。这是 Airflow 默认搜索 dags 定义的文件夹。在脚本中,导入以下依赖项:
# import airflow dependencies
from airflow import DAG
from airflow.models import Variable
from airflow.operators.python_operator import PythonOperator
from airflow.providers.amazon.aws.hooks.s3 import S3Hook
from airflow.providers.amazon.aws.hooks.base_aws import AwsGenericHook
from datetime import datetime
import requests
在实际场景中,网络抓取应用程序会在 MEC 页面上搜索 PDF 的下载链接,但为了简化,我手动收集了这些链接(数量不多)并将它们硬编码在一个字典中。
LINKS_ENEM = {
"2010_1":'https://download.inep.gov.br/educacao_basica/enem/provas/2010/dia1_caderno1_azul_com_gab.pdf',
"2010_2":'https://download.inep.gov.br/educacao_basica/enem/provas/2010/dia2_caderno7_azul_com_gab.pdf',
"2010_3":'https://download.inep.gov.br/educacao_basica/enem/provas/2010/AZUL_quarta-feira_GAB.pdf',
"2010_4":'https://download.inep.gov.br/educacao_basica/enem/provas/2010/AZUL_quinta-feira_GAB.pdf',
"2011_1":'https://download.inep.gov.br/educacao_basica/enem/provas/2011/01_AZUL_GAB.pdf',
"2011_2":'https://download.inep.gov.br/educacao_basica/enem/provas/2011/07_AZUL_GAB.pdf',
"2011_3":'https://download.inep.gov.br/educacao_basica/enem/ppl/2011/PPL_ENEM_2011_03_BRANCO.pdf',
# OMITTED TO MAKE THIS CODE BLOCK SMALLER
# ...
}
规划创建网络抓取器时要始终负责:检查网站的使用条款和托管内容的版权。
为了更好地模拟抓取应用程序的行为,我还在 Airflow UI 中创建了一个“年”变量(admin->variables)。这个变量模拟了抓取脚本应执行的“年份”,从 2010 年开始,并在任务执行结束时自动递增(+1)。这样,每次任务运行将仅处理一年的数据。
变量列表。图像由作者提供。
Airflow 变量和连接在代码中通过其 ID(名称)引用。我通常将它们的名称作为常量:
# Connections & Variables
AWS_CONN_ID = "AWSConnection"
YEAR_VARIABLE = "year"
在 Airflow DAGs 中执行 Python 代码最常见的方式是使用 PythonOperator,它基于 Python 函数创建任务。
因此,下载 PDF 并将其上传到 S3 桶的过程需要封装在一个函数中。见下文。
AWS_CONN_ID = "AWSConnection"
YEAR_VARIABLE = "year"
def download_pdfs_from_year(
year_variable,
bucket
):
# Create a S3 connection using the AWS Connection defined in the UI
conn = S3Hook(aws_conn_id=AWS_CONN_ID)
client = conn.get_conn()
year = Variable.get(year_variable)
year_keys = [key for key in LINKS_ENEM.keys() if year in key]
for key in year_keys:
print(f"Downloading {key}")
url = LINKS_ENEM[key]
r = requests.get(
url,
allow_redirects=True,
verify=False
)
client.put_object(
Body=r.content,
Key=f"pdf_{key}.pdf",
Bucket=bucket,
)
# increase the year
year = str(int(year)+1)
Variable.set(year_variable, year)
现在,只需实例化 DAG 对象本身:
# Some airflow boilerplate and blah blah blah
default_args = {
'owner': 'ENEM_PDF',
'depends_on_past': False,
'start_date': datetime(2021, 1, 1),
}
dag = DAG(
'process_enem_pdf_aws',
default_args=default_args,
description='Process ENEM PDFs using AWS',
tags=['enem'],
catchup=False,
)
书写任务:
with dag:
download_pdf_upload_s3 = PythonOperator(
task_id='download_pdf_upload_s3',
python_callable=download_pdfs_from_year,
op_kwargs={
'year_variable': YEAR_VARIABLE ,
'bucket': 'enem-bucket',
},
)
DAG 将在 Airflow UI 中可见,我们可以激活它并触发执行:
DAG 列表。图像由作者提供。
这是(第一次)关键时刻,触发 dag 并查看 S3 桶。如果一切顺利,PDF 应出现在 S3 桶中。
上传 PDF 的 S3 桶。图像由作者提供。
如果没有(这很可能,因为技术领域的事情往往会出错),开始调试 DAG 日志并搜索配置错误。
DAG 运行中的错误。图像由作者提供。
2. 使用 Lambda Functions 提取 PDF 文本
PDF 文件已经上传到 S3,现在是下一步:提取它们的文本。
这是使用 AWS Lambda Functions 实现的完美任务:一个无状态、小型且快速的过程。
简单回顾一下无服务器技术的工作原理。在常规“服务器”应用程序中,我们购买一个特定的服务器(机器),具有合适的 IP 地址,将我们的应用程序安装在其中,并保持 24/7 运行(或类似的状态)以满足我们的需求。
使用这种方法来处理像这样简单的文本提取预处理任务的问题在于,我们需要从头构建一个完整的健壮服务器,这需要时间,并且从长远来看可能不够成本效益。无服务器技术的到来是为了解决这个问题。
在无服务器环境中,每当发出请求时,都会快速启动一个新的小型服务器实例,应用程序响应后,该实例会被终止。
就像租车vs叫 Uber 去进行一次小的 5 分钟行程。
让我们回到编码。
在你的 AWS 账户中搜索 Lambda,并创建一个新的 lambda 函数,与之前使用的 S3 桶在同一区域,否则它将无法使用触发器与之互动(更多细节稍后说明)。
搜索 AWS Lambda。图片由作者提供。
从头创建一个新函数,将其命名为process-enem-pdf,选择 Python 3.9 运行时,就可以开始了。AWS 可能会指导你创建一个新的 IAM 角色用于 Lambda 函数,确保这个角色在enem-bucket S3 桶中拥有读写权限。
你可能还需要将函数的最大执行时间增加到大约 3 分钟,默认值是 3 秒(或接近的值),这对于我们的目的来说是不够的。
AWS 中的 Python Lambda 函数呈现为一个名为lambda_function.py的简单 Python 文件,其中包含一个**lambda_handler(event, context)**函数,其中‘event’是一个 JSON 对象,表示触发执行的事件。
你可以直接在 AWS 内置 IDE 中编辑 Python 文件,或使用压缩的 zip 文件上传本地文件。
Lambda 函数代码编辑器中的示例代码。图片由作者提供。
这时事情变得有点棘手。
要从 PDF 中提取文本,我们将使用 PyPDF2 包。然而,在 AWS Lambda 函数环境中安装这个依赖项并不像运行‘pip install’那么简单。
我们需要本地安装这些包,并将代码和依赖项**压缩(zip)**一起发送。
为此,按照以下步骤操作:
-
创建一个 Python 虚拟环境,使用venv: python3 -m venv pdfextractor
-
激活环境并安装依赖项。
source pdfextractor/bin/activate
pip3 install pypdf2 typing_extensions
创建一个本地lambda_function.py文件,并包含lambda_handler函数。
import boto3
from PyPDF2 import PdfReader
import io
import json
def lambda_handler(event, context):
# The code goes here blah blah blah
# Detailed latter
# ...
将lambda_function.py复制到**pdfextractor/lib/python3/site-packages/**路径下。
将**pdfextractor/lib/python3/site-packages/**文件夹的内容压缩成一个.zip 文件。
将这个文件上传到 Lambda 函数 UI 中。
现在你(可能)已经了解了这个过程,我们可以继续开发代码本身。
想法很简单:每当向 S3 桶添加一个新的 PDF 对象时,Lambda 函数应该被触发,提取其文本,并将结果写入 S3。
幸运的是,我们不需要手动编写这个触发规则,因为 AWS 提供了与其基础设施不同部分交互的内置触发器。在process-enem-pdf页面,点击添加触发器。
添加触发器。作者提供的图片。
现在,基于 S3 配置一个新规则……
配置触发器。作者提供的图片。
桶:enem-bucket;事件类型:所有对象创建事件;后缀:.pdf
正确添加后缀是非常重要的。 我们将使用这个功能将新文件写入相同的桶中,如果后缀过滤器配置不正确,可能会导致无限递归循环,从而消耗无限的资金。
现在,每当 S3 桶中创建一个新对象时,它将触发一次新的执行。参数event将存储一个 JSON,描述这个新创建的对象,其格式大致如下:
{
"Records": [
{
# blah blah blah blah blah
"s3": {
# blah blah blah blah blah
"bucket": {
"name": "enem-bucket",
"ownerIdentity": {
# blah blah blah
},
"arn": "arn:aws:s3:::enem-bucket"
},
"object": {
"key": "pdf_2010_1.pdf",
"size": 1024,
}
}
# blah blah blah
}
]
}
利用这些信息,函数可以从 S3 读取 PDF,提取其文本,并保存结果。请参见下面的代码。
import boto3
from PyPDF2 import PdfReader
import io
import json
def lambda_handler(event, context):
object_key = event["Records"][0]["s3"]["object"]["key"]
bucket = event["Records"][0]["s3"]["bucket"]["name"]
object_uri = f"s3://{bucket}/{object_key}"
if not object_uri.endswith(".pdf"):
# Just to make sure that this function will not
# cause a recursive loop
return "Object is not a PDF"
# Create a S3 client
# Remember to configure the Lambda role used
# with read and write permissions to the bucket
client = boto3.client("s3")
try:
pdf_file = client.get_object(Bucket=bucket, Key=object_key)
pdf_file = io.BytesIO(pdf_file["Body"].read())
except Exception as e:
print(e)
print(f"Error. Lambda was not able to get object from bucket {bucket}")
raise e
try:
pdf = PdfReader(pdf_file)
text = ""
for page in pdf.pages:
text += page.extract_text()
except Exception as e:
print(e)
print(f"Error. Lambda was not able to parse PDF {object_uri}")
raise e
try:
# Save the results as JSON
text_object = {
"content": text,
"original_uri": object_uri
}
client.put_object(
Body=json.dumps(text_object).encode("utf-8"),
Bucket=bucket,
Key=f"content/{object_key[:-4]}.json" ,
)
except Exception as e:
print(e)
print(f"Error. Lambda was not able to put object in bucket {bucket}")
raise e
创建这个功能并重复之前解释的部署步骤(venv、zip 和上传),一切应该运行良好(可能)。一旦我们的 airflow 管道将新的 PDF 保存到桶中,其文本应该被提取并作为 JSON 保存到**/content** “文件夹”(记住,文件夹是虚假的)。
提取的文本 JSON。作者提供的图片。
3. 使用 Glue 处理文本
最后,我们完成了管道的最后一部分。文本已经被提取并以大多数数据处理引擎可以轻松处理的格式(JSON)存储。
最后的任务是处理这些文本以孤立出单个问题,这就是 AWS Glue 的作用。
Glue 是一对解决方案:一个数据目录,带有爬虫来查找和编目数据以及映射模式,还有无服务器 ETL 引擎,负责数据处理。
在 AWS 控制台中搜索 Glue 并选择它。
搜索 Glue。作者提供的图片。
在编写作业之前,我们将使用爬虫在数据目录中创建一个新的数据集。我知道新概念太多了,但过程很简单。在 Glue 的主页面,转到左侧菜单中的爬虫。
AWS Glue 侧边栏。作者提供的图片。
创建一个新的爬虫,在步骤 1 中给它命名,然后进入步骤 2。在这里,添加一个新的数据源,指向s3://enem-bucket/content,这是我们存储所有文本的“文件夹”。
配置爬虫。作者提供的图片。
进入步骤 3,如果需要,创建一个新的IAM角色。步骤 4 将要求你选择一个数据库,点击添加数据库并创建一个名为enem_pdf_project的新数据库。在步骤 5 中查看信息并保存爬虫。
你将被重定向到爬虫页面。现在进入危险区域(这会花费你几分钱 ;-;),点击运行爬虫,它将开始在指定的源(s3://enem-bucket/content)中映射数据。几秒钟后,处理完成,如果一切顺利,enem_pdf_project 数据库中应该会出现一个名为content的新表。
现在,Glue 作业将能够读取引用目录中此表的 S3 JSON 文件。
我认为这实际上是不必要的,因为你可以直接查询 S3,但这个教训仍然适用。
现在,我们准备好编码我们的作业了。
在 Jobs 任务中,你可以选择多种方式来开发新作业:可视化连接块,使用交互式 pyspark 笔记本会话,或直接在脚本编辑器中编写代码。
Glue 作业界面。图片来源:作者。
我建议你自己探索这些选项(注意笔记本会话,你需要为它们付费)。无论你选择什么,都将创建的作业命名为Spark_EnemExtractQuestionsJSON。我选择使用 Spark,因为我对它更熟悉。见下面的代码。
from awsglue.transforms import *
from pyspark.context import SparkContext
import pyspark.sql.functions as F
from awsglue.context import GlueContext
from awsglue.job import Job
sc = SparkContext.getOrCreate()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
# Reading the table content from the Data Catalog
dyf = glueContext.create_dynamic_frame.from_catalog(
database="enem_pdf_project", table_name="content"
)
dyf.printSchema()
# Just pyspark script below
df = dyf.toDF()
# Create a new column with the year
df = df.withColumn(
"year", F.regexp_extract(F.col("original_uri"), ".+pdf_([0-9]{4})", 1)
)
# Split the text using the 'questão XX' regex
# and explode the resultant list
# resulting in one row per question
df = (
df.withColumn("text", F.lower(F.col("content")))
.withColumn(
"text",
F.regexp_replace(
F.col("text"), "(questão [0-9]+)", "<QUESTION_START_MARKER>$1"
),
)
.withColumn("text", F.split(F.col("text"), "<QUESTION_START_MARKER>"))
.withColumn("question", F.explode(F.col("text")))
.withColumn(
"question_number", F.regexp_extract(F.col("question"), "questão ([0-9]+)", 1)
)
.drop("content", "text")
)
# Save the result in CSV to S3
df.write.csv("s3://enem-bucket/processed/", mode="overwrite", header=True)
job.commit()
除了与 AWS 基础设施(读取和写入)交互所需的一些额外代码外,所有处理逻辑都是使用标准 pyspark 操作编写的。如果你对了解更多关于 Spark 的内容感兴趣,可以查看我之前的一篇文章。
默认情况下,Glue 作业配置为按需运行,这意味着我们必须手动触发其执行,使用 AWS 界面或通过 API 调用。
因此,我们只需要在 Airflow DAG 中添加一个新任务来触发作业并完成管道。
幸运的是,所需的代码非常简单,所以让我们回到process_enem_pdf.py 文件中创建一个新函数。
def trigger_process_enem_pdf_glue_job(
job_name
):
session = AwsGenericHook(aws_conn_id=AWS_CONN_ID)
# Get a client in the same region as the Glue job
boto3_session = session.get_session(
region_name='us-east-1',
)
# Trigger the job using its name
client = boto3_session.client('glue')
client.start_job_run(
JobName=job_name,
)
并将此函数作为任务添加到 DAG 中…
with dag:
download_pdf_upload_s3 = PythonOperator(
task_id='download_pdf_upload_s3',
python_callable=download_pdfs_from_year,
op_kwargs={
'year_variable': 'year',
'bucket': 'enem-bucket',
},
)
trigger_glue_job = PythonOperator(
task_id='trigger_glue_job',
python_callable=trigger_process_enem_pdf_glue_job,
op_kwargs={
'job_name': 'Spark_EnemExtractQuestionsJSON'
},
)
download_pdf_upload_s3 >> trigger_glue_job
而且,瞧,管道完成了。
DAG 的图形表示。图片来源:作者。
现在,每次运行时,管道应该会下载最新的 PDF 文件,并将它们上传到 S3,这会触发一个 Lambda 函数,该函数提取文本并将其保存到**/content** 路径。这个路径是由爬虫映射的,并在数据目录中可用。当管道触发 Glue 作业时,它读取这些文本,提取每个问题,并将结果保存为 CSV 文件在**/processed** 路径中。
‘processed’ 路径在 S3 中。图片来源:作者。
请查看下面的结果…
在 S3 中创建的 CSV 文件。图片来源:作者。
结论
这是一场漫长的冒险。
在这篇文章中,我们从头开始构建了一个完整的数据管道,混合了多种著名的数据工具的强大功能,包括 AWS 云(Lambda、Glue 和 S3)和本地环境(Airflow+Docker)。
我们探讨了 Lambda 和 Glue 在数据处理中的功能,讨论了它们的优点和使用案例。我们还学到了一些关于 Airflow 的知识,Airflow 是数据管道中最著名的编排工具。
这些工具每一个都是一个独立的世界。我试图将项目开发期间学到的所有信息压缩成尽可能小的文章,因此不可避免地,一些信息被遗漏了。如果你遇到问题或有疑问,请在评论中告诉我。
我知道所提议的数据管道可能不是最优的,尤其是在成本与效率方面,但我认为这篇文章的主要观点(对我来说,我希望对你也是如此)是学习使用所涉及工具开发数据产品的整体过程。
此外,如今大多数可用数据,特别是在互联网上,都是所谓的非结构化格式,如 PDF、视频、图像等。处理这种数据是一项关键技能,涉及到知道超出常见的 Pandas/Spark/SQL 工具集的更广泛工具。我们今天构建的管道正是解决这一问题,通过将存储在网站上的原始 PDF 转化为存储在我们云基础设施中的半结构化 CSV 文件。
对我来说,这个管道的一个亮点是使用 AWS Lambda 部署的文本提取步骤,因为仅仅依靠 Spark 实施这一任务可能是不可能或非常困难的(据我所知)。
我希望你从这篇文章中得到的主要信息是:构建良好的数据基础设施不仅需要对数据架构、数据建模或流媒体的理论知识,还需要对可以帮助实现你愿景的可用工具有良好的理解。
和往常一样,我并不是所讨论主题的专家,我强烈推荐进一步阅读和讨论(请参见下面的一些参考资料)。
这让我花费了 36 美分 + 税费 ;-;
感谢你的阅读! 😉
参考文献
所有代码均可在这个 GitHub 仓库中找到。
使用的数据 — ENEM PDF,[CC BY-ND 3.0],巴西教育部。
[1] 亚马逊网络服务拉丁美洲. (2021 年 12 月 6 日). 使用 AWS Glue 转化和目录化数据第一部分 — 葡萄牙语 [视频]. YouTube. 链接。
[2] Bakshi, U. (2023 年 2 月 9 日). 如何使用 Python AWS Lambda 将文件上传到 S3 — 极客文化 — Medium. Medium. 链接。
[3] Cairocoders. (2020 年 3 月 5 日). 如何在 AWS Lambda 函数中导入自定义 Python 包 [视频]. YouTube. 链接。
[4] 如何使用 AWS Glue 提取、转换和加载数据以进行分析处理(第二部分) | 亚马逊网络服务。(2022 年 4 月 4 日)。亚马逊网络服务。 Link.
[5] 如何使用 boto3 向 S3 对象写入文件或数据。(无日期)。Stack Overflow。 Link.
[6] 教程:使用 Amazon S3 触发器调用 Lambda 函数 — AWS Lambda。(无日期)。 Link.
[7] Um Inventor Qualquer。(2022 年 1 月 10 日)。在这个免费的实用课程中学习 AWS Lambda!| 课程 17 — #70 [视频]。YouTube。 Link.
[8] Chambers, B., & Zaharia, M.(2018 年)。Spark: 权威指南:大数据处理变得简单。 “O’Reilly Media, Inc.”。
使用 Polars 构建的数据管道:逐步指南
原文:
towardsdatascience.com/data-pipelines-with-polars-step-by-step-guide-f5474accacc4
使用 Polars 构建可扩展且快速的数据管道
·发表在 Towards Data Science ·阅读时间 14 分钟·2023 年 7 月 24 日
–
图片由 Filippo Vicini 提供,来源于 Unsplash
介绍
本文的目的是解释并展示如何使用 Polars 构建数据管道。它整合并使用了你从本系列前两部分获得的所有知识,因此如果你还没有阅读这些内容,我强烈建议你先去阅读,然后再回来这里。
## 使用 Polars 进行 EDA:Pandas 用户的逐步指南(第一部分)
提升你在 Polars 上的数据分析技能
towardsdatascience.com ## 使用 Polars 进行 EDA:汇总和分析函数的逐步指南(第二部分)
使用 Polars 以闪电般的速度进行高级聚合和滚动平均
towardsdatascience.com
设置
你可以在这个 repository 中找到所有的代码,所以不要忘记克隆/拉取并给它加星。特别是,我们将探索这个 file,这意味着我们最终将从笔记本走向实际应用!
本项目中使用的数据可以从 Kaggle 下载(CC0: Public Domain)。这与前两部分中使用的 YouTube 趋势数据集相同。我假设你已经安装了 Polars,因此只需确保通过 pip install -U polars
更新到最新版本。
数据管道
简单来说,数据管道是一个自动化的步骤序列,它从一个或多个位置提取数据,应用处理步骤,并将处理后的数据保存到其他地方,使其可以用于进一步的使用。
Polars 中的管道
Polars 处理数据的方式非常适合构建可扩展的数据管道。首先,我们可以如此轻松地链式调用方法,这使得一些相当复杂的管道可以优雅地编写。
例如,假设我们想找出 2018 年每个月中哪个趋势视频的观看次数最多。下面你可以看到一个完整的管道,用于计算这个指标并将其保存为 parquet 文件。
import polars as pl
csv_path = "./youtube/GBvideos.csv"
pl.read_csv(csv_path).with_columns(
# Original date is in string format like 17.01.01
pl.col("trending_date").str.to_date(format="%y.%d.%m")
).filter(pl.col("trending_date").dt.year() == 2018).with_columns(
pl.col("views")
.rank(descending=True)
# Rank is calculated over a month
.over(pl.col("trending_date").dt.month())
.alias("monthly_rank")
).filter(
pl.col("monthly_rank") == 1
).select(
pl.col("trending_date"), pl.col("title"), pl.col("channel_title"), pl.col("views")
).write_parquet(
"top_monthly_videos.parquet"
)
相当不错,对吧?如果你了解 SQL,这很容易阅读和理解。但我们可以做得更好吗?当然可以,使用 Polars 的 .pipe()
方法。这种方法为我们提供了一种有结构的方式来将顺序函数应用于 DataFrame。为了使其有效,让我们将上述代码重构成函数。
def process_date(df, date_column, format):
result = df.with_columns(pl.col(date_column).str.to_date(format))
return result
def filter_year(df, date_column, year):
result = df.filter(pl.col(date_column).dt.year() == year)
return result
def get_first_by_month(df, date_column, metric):
result = df.with_columns(
pl.col(metric)
.rank(method="ordinal", descending=True)
.over(pl.col(date_column).dt.month())
.alias("rank")
).filter(pl.col("rank") == 1)
return result
def select_data_to_write(df, columns):
result = df.select([pl.col(c) for c in columns])
return result
注意这些函数接受一个 Polars DataFrame 作为输入(以及一些其他参数),并输出已经修改的 Polars DataFrame。通过 .pipe()
方法将这些方法链在一起是一件轻而易举的事。
(
pl.read_csv(csv_path)
.pipe(process_date, date_column="trending_date", format="%y.%d.%m")
.pipe(filter_year, date_column="trending_date", year=2018)
.pipe(get_first_by_month, date_column="trending_date", metric="views")
.pipe(
select_data_to_write,
columns=["trending_date", "title", "channel_title", "views"],
)
).write_parquet("top_monthly_videos.parquet")
首先,重格式化后的代码更容易理解。其次,关注点分离通常是一个很好的编程原则,因为它可以更容易地调试和保持代码整洁。对于这个简单的示例,将管道模块化可能有些过于复杂,但你会看到它在下一个部分的更大示例中的有用之处。现在,让我们使用 懒模式 来加快整体速度。
懒模式允许我们编写查询和管道,将它们全部组合在一起,然后让后台引擎进行优化。例如,上述代码显然不是最优的。我把列选择作为最后一步,这意味着处理的数据量不必要地大。幸运的是,Polars 足够聪明,能够识别这一点,因此它会优化代码。此外,我们只需在代码中做两个小的更改即可获得速度提升,这点非常不可思议。首先,我们将 pl.read_csv
更改为 pl.scan_csv
以在懒模式下读取数据。然后,在查询的末尾添加 .collect()
,以告知 Polars 我们希望执行优化后的查询。
(
pl.scan_csv(csv_path).pipe(process_date, date_column="trending_date", format="%y.%d.%m")
.pipe(filter_year, date_column="trending_date", year=2018)
.pipe(get_first_by_month, date_column="trending_date", metric="views")
.pipe(
select_data_to_write,
columns=["trending_date", "title", "channel_title", "views"],
)
).collect().write_parquet("top_monthly_videos.parquet")
在我的机器上,我得到了约 3 倍的速度提升,考虑到我们仅做了两个非常简单的编辑,这点非常令人印象深刻。现在你已经掌握了管道和延迟评估的概念,让我们进入一个更复杂的示例。
机器学习特征的数据管道
警告:有很多文本和代码!各部分应该按顺序跟随,因为它们构建了管道。
根据我们手头的数据集(YouTube Trending Videos),我们来构建用于预测视频在流行中持续时间的特征。虽然这听起来很简单,但创建这些特征的过程将会相当复杂。数据集的最终格式应该是每个视频 ID 一行,视频进入流行时可用的特征,以及视频在流行中停留的实际天数(目标)。
模拟最终数据集格式。作者创建。
在我们的预测任务中可能有用的特征包括:
-
视频特征(例如,类别)
-
进入流行时的观看次数、点赞数、评论数等
-
频道在流行中的过去表现(例如,过去 7 天的流行视频数量)
-
一般流行特征(例如,过去 30 天所有视频的平均流行时间)
下面你可以看到创建该数据集所需的管道的图示表示(确保放大查看)。
数据管道流程。作者创建。
我知道这信息量很大,所以我们一口一口地消化它。下面你可以找到每个管道步骤的描述和代码。此外,这个管道将使用 YAML 配置文件进行参数化,所以你也会找到每个步骤的配置参数。这是如何读取名为 pipe_config.yaml
的 YAML 文件的示例,你可以在仓库中找到。
import yaml
# Read config
with open("pipe_config.yaml", "r") as file:
pipe_config = yaml.safe_load(file)
因此,对于管道的每一步,你会发现:
-
步骤描述
-
相关功能
-
相关配置参数
-
运行到此步骤的管道代码
这样,我们将逐步建立完整的管道,你将对发生的事情有深入了解,并学会如何为自己的数据创建类似的内容。
读取数据
这一步的目标不言自明——读取数据集以进行进一步处理。我们有两个输入——一个包含主要数据的 csv 文件和一个包含类别映射数据的 json 文件。此步骤的参数如下:
data_path: "./youtube/GBvideos.csv"
category_map_path: "./youtube/GB_category_id.json"
不需要编写读取 csv 数据的函数(因为在 Polars 中已经存在),所以我们只编写了读取类别映射的函数。
def read_category_mappings(path: str) -> Dict[int, str]:
with open(path, "r") as f:
categories = json.load(f)
id_to_category = {}
for c in categories["items"]:
id_to_category[int(c["id"])] = c["snippet"]["title"]
return id_to_category
使用此函数,读取所需文件的代码非常简单。
# Create mapping
id_to_category = read_category_mappings(pipe_config["category_map_path"])
col_mappings = {"category_id": id_to_category}
# Pipeline
output_data = pl.scan_csv(pipe_config["data_path"]).collect()
现在,让我们进入一个非常不令人兴奋但至关重要的步骤——数据清理。
数据清理
这个数据集已经相当干净,但我们需要对日期和类别列做一些额外的预处理。
-
trending_date
和publish_time
需要格式化为pl.datetime
-
category_id
需要从 ID 映射到实际的类别名称
Polars 需要知道日期将以何种格式提供,因此最好在 pipe_config.yaml
文件中对数据格式进行编码,并在相应的日期列中进行说明,以使其清晰且易于更改。
# Pre-processing config
date_column_format:
trending_date: "%y.%d.%m"
publish_time: "%Y-%m-%dT%H:%M:%S%.fZ"
由于我们希望使用 Polars 管道来模块化代码,我们需要创建两个函数——parse_dates
和 map_dict_columns
,它们将执行两个所需的转换。然而,有个问题——将这些操作分成两个步骤会使代码变得更慢,因为 Polars 无法有效地使用并行化。你可以通过计时这两个 Polars 表达式的执行来自己测试一下。
slow = df.with_columns(
# Process dates
pl.col("trending_date").str.to_date("%y.%d.%m"),
pl.col("publish_time").str.to_date("%Y-%m-%dT%H:%M:%S%.fZ"),
).with_columns(
# Then process category
pl.col("category_id").map_dict(id_to_category)
)
fast = df.with_columns(
# Process all together
pl.col("trending_date").str.to_date("%y.%d.%m"),
pl.col("publish_time").str.to_date("%Y-%m-%dT%H:%M:%S%.fZ"),
pl.col("category_id").map_dict(id_to_category)
)
对我来说,第一个表达式慢了 ~2 倍,这非常显著。那么我们该怎么办呢?好吧,这里有个秘密:
我们应该在将表达式传递给
.with_columns
方法之前构建它们。
因此,函数 parse_dates
和 map_dict_columns
应该返回表达式列表,而不是转换后的数据帧。这些表达式可以在最终清理函数中组合并应用,我们将称之为 clean_data
。
def parse_dates(date_cols: Dict[str, str]) -> List[pl.Expr]:
expressions = []
for date_col, fmt in date_cols.items():
expressions.append(pl.col(date_col).str.to_date(format=fmt))
return expressions
def map_dict_columns(
mapping_cols: Dict[str, Dict[str | int, str | int]]
) -> List[pl.Expr]:
expressions = []
for col, mapping in mapping_cols.items():
expressions.append(pl.col(col).map_dict(mapping))
return expressions
def clean_data(
df: pl.DataFrame,
date_cols_config: Dict[str, str],
mapping_cols_config: Dict[str, Dict[str | int, str | int]],
) -> pl.DataFrame:
parse_dates_expressions = parse_dates(date_cols=date_cols_config)
mapping_expressions = map_dict_columns(mapping_cols_config)
df = df.with_columns(parse_dates_expressions + mapping_expressions)
return df
如你所见,我们现在只有一个 .with_columns
操作,这使得代码更优化。请注意,所有函数的参数都作为字典提供。这是因为 YAML 被读取为字典。现在,让我们将清理步骤添加到管道中。
# Create mapping
id_to_category = read_category_mappings(pipe_config["category_map_path"])
col_mappings = {"category_id": id_to_category}
# Read in configs
date_column_format = pipe_config["date_column_format"]
# Pipeline
output_data = pl.scan_csv(pipe_config["data_path"]).pipe(
clean_data, date_column_format, col_mappings
).collect()
干净、模块化、快速——还有什么不喜欢的?让我们继续下一步。
基础特征工程
这一步在清理的数据上做一些基本的特征工程,即:
-
计算比率特征——点赞与踩的比率、点赞与观看的比率以及评论与观看的比率
-
计算发布和趋势之间的天数差异
-
从
trending_date
列中提取工作日
让我们在配置文件中对这些特征的计算进行参数化。我们要指定一个要创建的特征的名称以及数据集中用于计算的相应列。
# Feature engineering config
ratio_features:
# feature name
likes_to_dislikes:
# features used in calculation
- likes
- dislikes
likes_to_views:
- likes
- views
comments_to_views:
- comment_count
- views
difference_features:
days_to_trending:
- trending_date
- publish_time
date_features:
trending_date:
- weekday
函数的逻辑仍然是一样的——构建表达式并将其传递给 .with_columns
方法。因此,函数 ratio_features
、diff_features
和 date_features
都在名为 basic_feature_engineering
的主函数中调用。
def ratio_features(features_config: Dict[str, List[str]]) -> List[pl.Expr]:
expressions = []
for name, cols in features_config.items():
expressions.append((pl.col(cols[0]) / pl.col(cols[1])).alias(name))
return expressions
def diff_features(features_config: Dict[str, List[str]]) -> List[pl.Expr]:
expressions = []
for name, cols in features_config.items():
expressions.append((pl.col(cols[0]) - pl.col(cols[1])).alias(name))
return expressions
def date_features(features_config: Dict[str, List[str]]) -> List[pl.Expr]:
expressions = []
for col, features in features_config.items():
if "weekday" in features:
expressions.append(pl.col(col).dt.weekday().alias(f"{col}_weekday"))
if "month" in features:
expressions.append(pl.col(col).dt.month().alias(f"{col}_month"))
if "year" in features:
expressions.append(pl.col(col).dt.year().alias(f"{col}_year"))
return expressions
def basic_feature_engineering(
data: pl.DataFrame,
ratios_config: Dict[str, List[str]],
diffs_config: Dict[str, List[str]],
dates_config: Dict[str, List[str]],
) -> pl.DataFrame:
ratio_expressions = ratio_features(ratios_config)
date_diff_expressions = diff_features(diffs_config)
date_expressions = date_features(dates_config)
data = data.with_columns(
ratio_expressions + date_diff_expressions + date_expressions
)
return data
类似于前一步,我们只需将主函数传递给 pipe
并将所有所需的配置作为参数提供。
# Create mapping
id_to_category = read_category_mappings(pipe_config["category_map_path"])
col_mappings = {"category_id": id_to_category}
# Read in configs
date_column_format = pipe_config["date_column_format"]
ratios_config = pipe_config["ratio_features"]
diffs_config = pipe_config["difference_features"]
dates_config = pipe_config["date_features"]
# Pipeline
output_data = (
pl.scan_csv(pipe_config["data_path"])
.pipe(clean_data, date_column_format, col_mappings)
.pipe(basic_feature_engineering, ratios_config, diffs_config, dates_config)
).collect()
很好,我们已经完成了一半的管道!现在,让我们将数据集转换为正确的格式,并最终计算我们的目标——趋势中的天数。
数据转换
提醒一下,原始数据集中每个视频有多个条目,因为它详细记录了每一天的趋势。如果一个视频在趋势中停留了五天,这个视频在数据集中会出现五次。我们的目标是得到一个每个视频只有一个条目的数据集(请参见下图)。
数据转换步骤示例。作者创建。
我们可以通过 .groupby
和 .agg
方法的组合来实现这一点。这里唯一需要配置的参数是过滤那些由于视频花费太长时间才进入流行榜的视频,因为这些视频是前一部分中识别出的离群点。在得到包含 video_ids
和相应目标(流行天数)的表后,我们还需要记得从原始数据集中联接特征,因为这些特征不会在 groupby
操作中传递。因此,我们还需要指定要联接的特征和作为联接键的列。
# Filter videos
max_time_to_trending: 60
# Features to join to the transformed data
base_columns:
- views
- likes
- dislikes
- comment_count
- comments_disabled
- ratings_disabled
- video_error_or_removed
- likes_to_dislikes
- likes_to_views
- comments_to_views
- trending_date_weekday
- channel_title
- tags
- description
- category_id
# Use these columns to join transformed data with original
join_columns:
- video_id
- trending_date
为了执行所需的步骤,我们将设计两个函数——join_original_features
和 create_target_df
。
def join_original_features(
main: pl.DataFrame,
original: pl.DataFrame,
main_join_cols: List[str],
original_join_cols: List[str],
other_cols: List[str],
) -> pl.DataFrame:
original_features = original.select(original_join_cols + other_cols).unique(
original_join_cols
) # unique ensures one row per video + date
main = main.join(
original_features,
left_on=main_join_cols,
right_on=original_join_cols,
how="left",
)
return main
def create_target_df(
df: pl.DataFrame,
time_to_trending_thr: int,
original_join_cols: List[str],
other_cols: List[str],
) -> pl.DataFrame:
# Create a DF with video ID per row and corresponding days to trending and days in trending (target)
target = (
df.groupby(["video_id"])
.agg(
pl.col("days_to_trending").min().dt.days(),
pl.col("trending_date").min().dt.date().alias("first_day_in_trending"),
pl.col("trending_date").max().dt.date().alias("last_day_in_trending"),
# our TARGET
(pl.col("trending_date").max() - pl.col("trending_date").min()).dt.days().alias("days_in_trending"),
)
.filter(pl.col("days_to_trending") <= time_to_trending_thr)
)
# Join features to the aggregates
target = join_original_features(
main=target,
original=df,
main_join_cols=["video_id", "first_day_in_trending"],
original_join_cols=original_join_cols,
other_cols=other_cols,
)
return target
注意,在 create_target_df
函数中,groupby
操作以创建目标和 join_original_features
函数都是运行的,因为它们都使用原始数据集作为输入。这意味着即使我们有一个中间输出(target
变量),我们仍然可以在 pipe
方法中运行此函数,而不会出现问题。
# Create mapping
id_to_category = read_category_mappings(pipe_config["category_map_path"])
col_mappings = {"category_id": id_to_category}
# Read in configs
date_column_format = pipe_config["date_column_format"]
ratios_config = pipe_config["ratio_features"]
diffs_config = pipe_config["difference_features"]
dates_config = pipe_config["date_features"]
# Pipeline
output_data = (
pl.scan_csv(pipe_config["data_path"])
.pipe(clean_data, date_column_format, col_mappings)
.pipe(basic_feature_engineering, ratios_config, diffs_config, dates_config)
.pipe(
create_target_df,
time_to_trending_thr=pipe_config["max_time_to_trending"],
original_join_cols=join_cols,
other_cols=base_features,
)
).collect()
对于最后一步,让我们使用动态和滚动聚合生成更高级的特征(在上一篇文章中详细介绍)。
高级聚合
这一步负责生成基于时间的聚合。我们需要提供的唯一配置是聚合的窗口期。
aggregate_windows:
- 7
- 30
- 180
滚动聚合
让我们从滚动特征开始。下方是一个示例,展示了一个 abc
频道在两天窗口期内的两个滞后滚动特征。
滚动特征示例。图片由作者提供。
在 Polars 中,滚动特征非常简单,你只需要 .groupby_rolling()
方法和 .agg()
命名空间中的一些聚合。可能有用的聚合包括:
-
先前流行视频的数量
-
先前流行视频的平均流行天数
-
先前流行视频的最大流行天数
鉴于此,让我们构建一个名为 build_channel_rolling
的函数,该函数可以将所需的周期作为输入,这样我们就可以轻松创建任何我们想要的滚动特征,并输出这些所需的聚合。by
参数应设置为 channel_title
,因为我们希望按频道创建聚合,而索引列应为 first_day_in_trending
,因为这是我们的主要日期列。这两列也将用于将这些滚动聚合联接到原始数据框中。
def build_channel_rolling(df: pl.DataFrame, date_col: str, period: int) -> pl.DataFrame:
channel_aggs = (
df.sort(date_col)
.groupby_rolling(
index_column=date_col,
period=f"{period}d",
by="channel_title",
closed="left", # only left to not include the actual day
)
.agg(
pl.col("video_id").n_unique().alias(f"channel_num_trending_videos_last_{period}_days"),
pl.col("days_in_trending").max().alias(f"channel_max_days_in_trending_{period}_days"),
pl.col("days_in_trending").mean().alias(f"channel_avg_days_in_trending_{period}_days"),
)
.fill_null(0)
)
return channel_aggs
def add_rolling_features(
df: pl.DataFrame, date_col: str, periods: List[int]
) -> pl.DataFrame:
for period in periods:
rolling_features = build_channel_rolling(df, date_col, period)
df = df.join(rolling_features, on=["channel_title", "first_day_in_trending"])
return df
add_rolling_features
是一个包装函数,可以传递到我们的管道中。它生成并联接配置中指定的周期的聚合。现在,让我们进入最终的特征生成步骤。
周期聚合
这些聚合类似于滚动聚合,但它们旨在衡量“流行”标签中的一般行为。
周期特征示例。图片由作者提供。
如果滚动聚合旨在捕捉频道的过去行为,这些聚合将捕捉一般趋势。这可能是有用的,因为决定谁进入流行和停留多长时间的算法不断变化。因此,我们想要创建的聚合是:
-
最近一段时间内流行的视频数量
-
最近一段时间内的平均流行天数
-
最近一段时间内的最大流行天数
函数的逻辑是相同的 — 我们将创建一个函数来构建这些聚合,以及一个包装函数来构建并连接所有时期的聚合。请注意,我们不指定by
参数,因为我们想要计算每天所有视频的这些特征。还要注意,我们需要在聚合上使用shift
,因为我们希望使用最后一段时间的特征,而不是当前的。
def build_period_features(df: pl.DataFrame, date_col: str, period: int) -> pl.DataFrame:
general_aggs = (
df.sort(date_col)
.groupby_dynamic(
index_column=date_col,
every="1d",
period=f"{period}d",
closed="left",
)
.agg(
pl.col("video_id").n_unique().alias(f"general_num_trending_videos_last_{period}_days"),
pl.col("days_in_trending").max().alias(f"general_max_days_in_trending_{period}_days"),
pl.col("days_in_trending").mean().alias(f"general_avg_days_in_trending_{period}_days"),
)
.with_columns(
# shift match values with previous period
pl.col(f"general_num_trending_videos_last_{period}_days").shift(period),
pl.col(f"general_max_days_in_trending_{period}_days").shift(period),
pl.col(f"general_avg_days_in_trending_{period}_days").shift(period),
)
.fill_null(0)
)
return general_aggs
def add_period_features(
df: pl.DataFrame, date_col: str, periods: List[int]
) -> pl.DataFrame:
for period in periods:
rolling_features = build_period_features(df, date_col, period)
df = df.join(rolling_features, on=["first_day_in_trending"])
return df
最后,让我们将这些都整合到我们的管道中吧!
# Create mapping
id_to_category = read_category_mappings(pipe_config["category_map_path"])
col_mappings = {"category_id": id_to_category}
# Read in configs
date_column_format = pipe_config["date_column_format"]
ratios_config = pipe_config["ratio_features"]
diffs_config = pipe_config["difference_features"]
dates_config = pipe_config["date_features"]
output_data = (
pl.scan_csv(pipe_config["data_path"])
.pipe(clean_data, date_column_format, col_mappings)
.pipe(basic_feature_engineering, ratios_config, diffs_config, dates_config)
.pipe(
create_target_df,
time_to_trending_thr=pipe_config["max_time_to_trending"],
original_join_cols=pipe_config["join_columns"],
other_cols=pipe_config["base_columns"],
)
.pipe(
add_rolling_features,
"first_day_in_trending",
pipe_config["aggregate_windows"],
)
.pipe(
add_period_features,
"first_day_in_trending",
pipe_config["aggregate_windows"],
)
).collect()
我希望你和我一样激动,因为我们快到了!最后一步 — 写出数据。
写数据
保存转换后的数据非常简单,因为我们可以在collect()
操作之后直接使用.save_parquet()
。下面你可以看到文件data_preparation_pipeline.py
中包含的完整代码。
def pipeline():
"""Pipeline that reads, cleans, and transofrms data into
the format we need for modelling
"""
# Read and unwrap the config
with open("pipe_config.yaml", "r") as file:
pipe_config = yaml.safe_load(file)
date_column_format = pipe_config["date_column_format"]
ratios_config = pipe_config["ratio_features"]
diffs_config = pipe_config["difference_features"]
dates_config = pipe_config["date_features"]
id_to_category = read_category_mappings(pipe_config["category_map_path"])
col_mappings = {"category_id": id_to_category}
output_data = (
pl.scan_csv(pipe_config["data_path"])
.pipe(clean_data, date_column_format, col_mappings)
.pipe(basic_feature_engineering, ratios_config, diffs_config, dates_config)
.pipe(
create_target_df,
time_to_trending_thr=pipe_config["max_time_to_trending"],
original_join_cols=pipe_config["join_columns"],
other_cols=pipe_config["base_columns"],
)
.pipe(
add_rolling_features,
"first_day_in_trending",
pipe_config["aggregate_windows"],
)
.pipe(
add_period_features,
"first_day_in_trending",
pipe_config["aggregate_windows"],
)
).collect()
return output_data
if __name__ == "__main__":
t0 = time.time()
output = pipeline()
t1 = time.time()
print("Pipeline took", t1 - t0, "seconds")
print("Output shape", output.shape)
print("Output columns:", output.columns)
output.write_parquet("./data/modelling_data.parquet")
我们可以像运行其他 Python 文件一样运行这个管道。
python data_preparation_pipeline.py
Pipeline took 0.3374309539794922 seconds
Output shape (3196, 38)
Output columns: [
'video_id', 'days_to_trending', 'first_day_in_trending',
'last_day_in_trending', 'days_in_trending', 'views', 'likes', 'dislikes',
'comment_count', 'comments_disabled', 'ratings_disabled',
'video_error_or_removed', 'likes_to_dislikes', 'likes_to_views',
'comments_to_views', 'trending_date_weekday', 'channel_title',
'tags', 'description', 'category_id', 'channel_num_trending_videos_last_7_days',
'channel_max_days_in_trending_7_days', 'channel_avg_days_in_trending_7_days',
'channel_num_trending_videos_last_30_days', 'channel_max_days_in_trending_30_days',
'channel_avg_days_in_trending_30_days', 'channel_num_trending_videos_last_180_days',
'channel_max_days_in_trending_180_days', 'channel_avg_days_in_trending_180_days',
'general_num_trending_videos_last_7_days', 'general_max_days_in_trending_7_days',
'general_avg_days_in_trending_7_days', 'general_num_trending_videos_last_30_days',
'general_max_days_in_trending_30_days', 'general_avg_days_in_trending_30_days',
'general_num_trending_videos_last_180_days', 'general_max_days_in_trending_180_days',
'general_avg_days_in_trending_180_days'
]
在我的笔记本电脑上,这些步骤不到半秒钟完成,这很令人印象深刻,考虑到我们链在一起的操作数量和生成的特征数量。最重要的是,管道看起来很干净,非常容易调试,并且可以在很短时间内扩展/更改/裁剪。我们做得很棒!
结论
如果你按照这些步骤走到了这里 — 做得好!以下是你应该在这篇文章中学到的内容的简要总结:
-
如何将多个操作链在一起形成管道
-
如何使这个管道高效
-
如何结构化你的管道项目并使用 YAML 文件对其进行参数化
确保将这些学习应用到你自己的数据中。我建议从小处着手(2-3 步),然后随着需求的增长扩展管道。确保保持模块化,惰性,并将尽可能多的操作组合到.with_columns()
中,以确保适当的并行处理。
还不是 Medium 会员?
[## 使用我的推荐链接加入 Medium — Antons Tocilins-Ruberts
阅读 Antons Tocilins-Ruberts 的每个故事(以及 Medium 上成千上万其他作家的故事)。你的会员费直接…
medium.com](https://medium.com/@antonsruberts/membership?source=post_page-----f5474accacc4--------------------------------)
数据平台架构类型
原文:
towardsdatascience.com/data-platform-architecture-types-f255ac6e0b7
它在多大程度上满足你的业务需求?选择的困境。
·发表于 Towards Data Science ·9 分钟阅读·2023 年 2 月 20 日
–
图片来源:Brooke Lark 在 Unsplash
在当前市场上,数据工具琳琅满目,很容易迷失。互联网充斥着关于使用哪些数据工具和如何使我们的数据堆栈 在今年特别现代化的意见故事(通常是推测性的)。哪些数据工具是最好的?谁是领导者?如何选择合适的工具? 这个故事是为那些在“领域”中并且正在建立世界上最佳数据平台的人准备的。
那么,“现代数据堆栈”是什么,它有多现代呢?
简而言之,它是一个工具集合,用于处理数据。根据我们打算如何处理数据,这些工具可能包括以下内容:
-
管理的 ETL/ELT 数据管道服务
-
基于云的管理数据仓库/数据湖,作为数据的目的地
-
数据转换工具
-
商业智能或数据可视化平台
-
机器学习和数据科学能力
有时,现代化的程度并不重要。
确实,如果我们的 BI 工具非常现代,具备定制的 OLAP 数据建模和 git 集成,但无法将报告渲染到电子邮件中,这也是不重要的。
这些小细节往往至关重要。业务需求和数据管道要求是最重要的。
在下图中,我们可以看到数据流转过程以及在数据管道的每个步骤中可以使用的一些相关工具。
数据流动和工具。图片来源:作者
Redshift、Postgres、Google BigQuery、Snowflake、Databricks、Hadoop、Dataproc、Spark,还是 Elastic Map Reduce?
为你的数据平台选择哪个产品?
这取决于你计划用数据执行的日常 任务,数据处理和数据存储架构*,哪个最适合这些任务。*
数据平台架构类型
我记得几年前,互联网充斥着“Hasdoop 已死”这类的故事。数据仓库架构的趋势明显发生了变化。到 2023 年,每个人似乎都对实时数据流和可扩展性着迷,暗示 Spark 和 Kafka 很快会成为公共基准的领导者。
那么哪一种是最好的?谁是领导者,选择哪些数据工具?如何选择?
我理解的是这些基准判断非常主观,应当以一种保留的态度来看待。真正重要的是这些工具与我们的业务需求对齐的程度,以便我们构建一个数据平台。
数据仓库
一种无服务器、分布式的 SQL 引擎(BigQuery、Snowflake、Redshift、Microsoft Azure Synapse、Teradata)。这是一个以 SQL 为优先的数据架构,你的数据存储在一个数据仓库中,你可以充分利用去规范化星型模式数据集的所有优势。当然,我们能做到这一点,因为大多数现代数据仓库都是分布式的,扩展性良好,这意味着你不需要担心表的键和索引。它非常适合进行与大数据相关的临时分析。
大多数现代数据仓库解决方案可以处理结构化和非结构化数据,如果你的用户主要是数据分析师且具备良好的 SQL 技能,这些解决方案确实非常方便。现代数据仓库能够轻松与业务智能解决方案如Looker、Tableau、Sisense 或 Mode集成,这些解决方案也大量依赖于ANSI-SQL。它不设计用于存储图像、视频或文档。然而,使用 SQL 你几乎可以做任何事情,甚至在一些供应商解决方案中训练机器学习模型。
在 1 到 10 的范围内,你的数据仓库技能有多好?
数据湖(Databricks、Dataproc、EMR)
一种架构类型是你的数据存储在云存储中,即 AWS S3、Google Cloud Storage、ABS。当然,也自然可以用于图像、视频或文档以及任何其他文件类型(JSON、CSV、PARQUET、AVRO 等),但要分析这些数据,你的用户需要编写一些代码。
最常用的编程语言是Python,有许多可用的库。JAVA、Scala 或 PySpark 也是此任务的另一种流行选择。
令人惊叹的好处伴随代码而来。
这是数据处理的最高灵活性水平。我们的用户只需要知道如何做到这一点。
湖屋
这是数据仓库和数据湖架构的结合,兼具两者的优点,服务于程序员和普通业务用户,如数据分析师。它使业务能够运行交互式 SQL 查询,同时在定制方面保持很大的灵活性。现代数据仓库解决方案大多能够对存储在数据湖中的数据运行交互式查询,即外部表。例如,一个数据管道可能如下所示:
湖屋管道示例。图像来源于作者
数据建模、Python、DAGs、大数据文件格式、成本……它涵盖了所有内容
pub.towardsai.net](https://pub.towardsai.net/supercharge-your-data-engineering-skills-with-this-machine-learning-pipeline-b69d159780b7?source=post_page-----f255ac6e0b7--------------------------------)
数据网格
数据网格架构是一种去中心化的方法,使公司能够自主管理数据、进行跨团队/跨领域的数据分析并共享数据。
每个业务单元可能具有不同的编程技能组合,即SQL 或 Python,以及各种数据工作负载需求(灵活的数据处理与交互式 SQL 查询)。尽管如此,每个业务单元可以自由选择自己的数据仓库/数据湖解决方案,但仍能够与其他单元共享数据而无需数据移动。
选择合适的架构及其示例
towardsdatascience.com
关系型和非关系型数据库管理系统
关系型数据库管理系统(RDS)将数据存储在一个以行作为单位的表中,列连接相关的数据元素。它旨在记录和优化以快速获取当前数据。流行的关系型数据库有PostgreSQL, MySQL, Microsoft SQL Server 和 Oracle。NoSQL 数据库不仅支持简单事务,而关系型数据库还支持复杂的事务和联接。NoSQL 数据库用于处理高速流入的数据。流行的 NoSQL 数据库有:
-
文档数据库:MongoDB 和 CouchDB
-
键值数据库:Redis 和 DynamoDB
数据仓库具有类似的列式结构,与 RDS 一样,它是关系型的。数据也被组织成表格、行和列。然而,它与数据库的主要不同在于数据组织和存储的方式,数据库数据按行存储,而数据仓库数据按列存储,以便进行在线分析处理(OLAP),而数据库则使用在线事务处理(OLTP)。例如,AWS Redshift支持数据仓库和数据湖方法,使其能够访问和分析大量数据。
数据仓库设计用于数据分析,包括大量历史数据。使用数据仓库要求用户提前创建预定义的固定模式,这大大有助于数据分析。表格必须简单(去规范化),以便计算大量数据。
RDS 数据库表和连接较为复杂,因为它们是标准化的。因此,传统数据库与数据仓库之间的主要区别在于,传统数据库是为记录数据而设计和优化的,而数据仓库是为响应分析而设计和优化的。当你运行一个应用程序并需要快速获取当前数据时,你会使用数据库。RDS 存储了运行应用程序所需的当前数据。
你需要决定哪一个适合你。
商业智能堆栈
现代数据堆栈应包括帮助数据建模和可视化的 BI 工具。以下是一些高层次的概述。当然这不是一个详尽的列表,但这些是 2023 年市场上最受欢迎的 BI 工具:
Looker Data Studio(Google Looker Studio)
主要特点:
-
以前称为 Google Data Studio 的免费版本。这是一个出色的免费 BI 工具,具有社区支持。
-
丰富的小部件和图表
-
丰富的社区数据连接器
-
免费的电子邮件调度和投递。完美地将报告渲染到电子邮件中。
-
免费的数据治理功能
-
由于这是一个免费的社区工具,它的 API 还稍显不完善
Looker(付费版)
主要特点:
-
强大的数据建模功能和自助服务能力。适合中型和大型公司。
-
API 功能
Tableau
主要特点:
-
出色的可视化效果
-
合理的定价
-
专利 VizQL 引擎驱动其直观的分析体验
-
与许多数据源的连接,如 HADOOP、SAP 和 DB 技术,提高数据分析质量。
-
与 Slack、Salesforce 及其他众多工具的集成。
AWS Quicksight
主要特点:
-
定制品牌的电子邮件报告
-
无服务器且易于管理
-
强大的 API
-
无服务器自动扩展
-
按需付费定价
Power BI
主要特点:
-
Excel 集成
-
强大的数据摄取和连接能力
-
从 Excel 数据中轻松创建共享仪表板
-
丰富的视觉效果和图形
Sisense(前身为 Periscope)
Sisense 是一个端到端的数据分析平台,通过可嵌入的、可扩展的架构,使客户和员工都可以进行数据发现和分析。
关键功能:
-
提供几乎每个主要服务和数据源的数据连接器
-
为非技术用户提供无代码体验,尽管平台也支持 Python、R 和 SQL。
-
Git 集成和自定义数据集
-
可能会有点贵,因为它基于按许可证按用户计费的模式
-
一些功能仍在建设中,即报告电子邮件发送和报告渲染
ThoughtSpot
关键功能:
- 查询的自然语言
Mode
关键功能:
-
仪表板的 CSS 设计
-
协作功能允许在承诺高级计划之前进行快速原型制作
-
笔记本支持
-
Git 支持
Metabase
关键功能:
-
适合初学者,非常灵活
-
有 docker 镜像,因此我们可以立即运行它
-
自助分析
Redash
关键功能:
-
API
-
用自然语法编写查询并探索模式
-
使用查询结果作为数据源来连接不同的数据库
这些工具中的一些有免费版本。例如,Looker Data Studio 提供了基本的仪表板功能,如电子邮件,即拖放小部件构建器,以及良好的图表选择。其他工具则提供付费功能,即数据建模、警报、笔记本和 git 集成。
这些都是很棒的工具,各有利弊。有些工具更用户友好,有些则提供更强大的 API、CI/CD 功能和 git 集成。对于一些工具,这些功能仅在付费版本中提供。
结论
现代数据驱动的应用程序将需要一个数据库来存储当前的应用程序数据。所以如果你有一个应用程序要运行,请考虑 OLTP 和 RDS 架构。
数据湖、数据仓库、湖屋和数据库各有其优点,并且各司其职。
希望对历史数据执行复杂 SQL 查询的大数据分析公司可能会选择用数据仓库(或湖屋)来补充其数据库。这使得数据堆栈更灵活、更现代。
一般来说,答案总是一样的:
选择最便宜的或与您的开发堆栈兼容性最好的工具
试试看,你会发现关系型数据库可以很容易地集成到数据平台中。无论是数据湖还是数据仓库,各种数据连接器将使数据提取变得简单流畅。
但是,有几个问题需要考虑。
这里的关键是尝试数据工具,看看它们能多好地与我们的业务需求对接。
例如,一些 BI 工具只提供按用户付费的定价方式,如果我们需要与外部用户共享仪表板,这将不太适合。
如果有任何节省成本的好处,最好将数据工具与开发堆栈所在的同一云供应商保持一致。
我们可能需要检查工具之间是否存在功能重叠,例如,当我们已经在数据仓库中进行数据建模时,我们是否真的需要一个在其自身的 OLAP 立方体中进行数据建模的 BI 解决方案?
数据建模很重要
确实,它定义了我们处理数据的频率,这将不可避免地反映在处理成本上。
向数据湖或数据仓库的转变主要取决于用户的技能水平。数据仓库解决方案将实现更多的互动,并将我们的选择范围缩小到以 SQL 为首的产品(如 Snowflake、BigQuery 等)。
数据湖适用于具有编程技能的用户,我们会选择以 Python 为首的产品,如 Databricks、Galaxy、Dataproc、EMR。
推荐阅读
机器翻译的数据预处理
清洗、归一化和分词
·
关注 发表在 Towards Data Science · 14 min read · 2023 年 2 月 25 日
–
图片来自 Pixabay。
数据预处理是任何机器学习任务中的关键步骤。数据必须是正确的、清洁的,并且符合预期的格式。
在这篇博客文章中,我解释了预处理用于训练、验证和评估机器翻译系统的数据所需的所有步骤。
我通过示例和代码片段解释每一步预处理步骤,以便你可以自行重现。
在本文的预处理示例中,我使用了西班牙语-英语(Es→En)ParaCrawl v9语料库的前 100,000 个段落(CC0)。我直接提供了这个数据集(大小:9Mb)。
如果你想自己制作这个语料库,请按照这些步骤操作(耐心点,原始数据集已压缩,但仍重达 24Gb):
#Download the corpus
wget https://opus.nlpl.eu/download.php?f=ParaCrawl/v9/moses/en-es.txt.zip
#Uncompress it
unzip en-es.txt.zip
#Keep only the first 100,000 lines
head -n 100000 ParaCrawlV9.es.txt > train.es
head -n 100000 ParaCrawlV9.en.txt > train.en
#Discard the original files
rm en-es.txt.zip
在我之前的文章中,我介绍了用于训练、验证和评估的机器翻译数据集的所有主要特征:
选择、检查和拆分
[towardsdatascience.com
数据格式:TXT、TSV 和 TMX
在寻找机器翻译数据集时,你通常会发现它们以不同的格式出现,这些格式试图最好地处理其多语言性质。
无论原始格式是什么,大多数用于训练机器翻译系统的框架只接受原始文本格式的数据。
因此,如果数据集不是文本文件,你可能需要对其进行转换。
你可能会发现的最常见格式有:
-
平行文本(.txt):这是理想的格式。我们不需要进行任何转换。源语言段落在一个文本文件中,目标语言段落在另一个文本文件中。大多数接下来的预处理步骤将并行应用于这两个文件。在介绍部分,我们下载了这种格式的 ParaCrawl 数据。
-
制表符分隔值(.tsv):这是一个单文件,每对源语言和目标语言段落在同一行中由制表符分隔。用“cut”命令将其转换为文本文件是直接的:
#The source segments
cut -f1 data.tsv > train.es
#The target segments
cut -f2 data.tsv > train.en
- 翻译记忆交换(.tmx):这是一种 XML 格式,专业翻译人员经常使用。这是一种非常详细的格式。这就是为什么它很少用于大型语料库。处理 TMX 稍微困难一些。我们可以先去除 XML 标签。为此,我使用了来自 Moses 项目的脚本(LGPL 许可):
strip-xml.perl < data.tmx > data.txt
#Then we have to remove the empty lines:
sed -i '/^$/d' data.txt
#Finally, we have to separate source and target segments into two files. We can use "sed" to do this:
sed -n 'n;p' data.txt > train.en
sed -n 'p;n' data.txt > train.es
不要修改评估数据集的目标端
在深入之前,预处理机器翻译数据集时有一个非常重要的规则:
绝不要对评估数据的目标端进行预处理!
这些被称为“参考翻译”。由于它们是“参考”,我们不应对它们进行修改。
这样做有几个原因。其中最主要的原因是评估数据的目标端应该与您希望系统生成的数据相似。
例如,在某些预处理步骤中,我们将移除空行并对段落进行分词。
你可能希望系统返回空行,例如在翻译空文本时,当然你不希望返回标记化文本作为机器翻译系统的最终输出。
如果你从参考中删除空行,你将无法直接评估系统在需要时生成空行的能力。而如果你对参考进行标记化,你只能知道系统生成标记化文本的效果。如我们将看到的,标记化文本不是你希望系统生成的内容。
此外,参考翻译用于计算自动度量分数以评估机器翻译质量。如果我们修改这些翻译,就会修改分数。这样,分数将不再与其他已发布的参考翻译分数可比,因为我们修改了这些参考。
因此,保留原始参考翻译对于确保可重复性和可比性至关重要。
如果在预处理的某个点,评估数据的目标端与原始数据不同,说明出现了问题。
步骤 1:清理和过滤
**步骤适用于:**训练和验证数据的源端和目标端。
出于各种原因,公开的平行数据可能需要一些清理。
如果数据是从网络上抓取的文本自动创建的,这一点尤其正确。
清理通常意味着从平行数据中删除以下段落(或句子):
-
空白或大多包含不可打印字符。
-
带有无效 UTF8,即未正确编码的。
-
包含极长的标记(或“单词”),因为它们通常无法翻译,例如 DNA 序列、数字序列、无意义的内容等。
-
有时,重复,即如果一个段或一对段在平行数据中出现多于一次,我们只保留一个实例。
这不是必需的,但我通常会删除训练平行数据中的段对重复项,原因有多个:
-
它们在训练中很少有用。
-
它们在训练数据中对某个特定翻译赋予更多权重,且没有充分理由。
-
它们通常是获取数据过程中出现的缺陷产品(例如爬取),换句话说,这些重复本不应该存在。
所有这些过滤规则的应用是为了只保留对训练神经模型有用的内容。它也删除了可能在后续预处理步骤中引发错误的段落。
它们也略微减少了平行数据的大小。
请记住,我们在清理/过滤平行数据。每条过滤规则应同时应用于数据的两侧。例如,如果源段为空且应删除,则目标段也应删除,以保持数据的平行性。
除了我上面提到的规则外,我们还应该过滤掉包含以下内容的片段对:
-
非常长的片段
-
非常短的片段
-
高繁殖率,即当一个片段比其对应的片段出现得不成比例地更长或更短时
这些规则继承自统计机器翻译时代,在那个时代,这些片段显著增加了计算成本,而对于训练翻译模型没有用处。
由于今天使用的神经算法进行训练,这些规则已经不再必要。然而,这些片段在训练翻译模型时仍大多无用,因此可以安全地从训练数据中移除,以进一步减少其大小。
有许多公共工具可以执行此清理操作。
预处理(LGPL 许可)是一个高效的框架,可以执行许多过滤操作。它被机器翻译研讨会用于准备主要国际机器翻译竞赛的数据。
我通常会用自制脚本和额外的框架来补充,比如Moses 脚本(LGPL 许可)。
在接下来的段落中,我将逐步描述我通常应用于原始平行数据的整个清理和过滤过程。
实践
我们希望清理我们的 Paracrawl 西班牙语-英语平行数据(见本文介绍部分)。
在内存方面,最昂贵的步骤之一是删除重复项(即“去重”)。在去重之前,我们应该尽可能地移除更多的片段对。
我们可以通过应用clean-n-corpus.perl来开始(这个脚本不需要安装 Moses),如下所示:注意:此步骤假设源语言和目标语言中都存在空格。如果其中一种语言(大多)不使用空格,如日语或中文,你必须首先对源文本和目标文本文件进行分词。如果适用于你的用例,直接跳到“步骤 2 和 3”,然后在完成后再回来这里。
要了解如何使用 clean-n-corpus.perl,调用脚本而不带任何参数。它应返回:
syntax: clean-corpus-n.perl [-ratio n] corpus l1 l2 clean-corpus min max [lines retained file]
参数如下:
-
比例:这是繁殖率。默认情况下,它设置为 9。我们通常不需要修改它,因此不使用此参数。
-
语料库:这是清理数据集的路径,不包含扩展名。脚本假设你将源文件和目标文件命名为相同,使用语言 ISO 代码作为扩展名,例如,在我们的例子中是 train.es 和 train.en。如果你采用了我为 ParaCrawl 使用的相同文件名约定,你只需在那里输入:“train”(假设你在包含数据的目录中)。
-
l1: 其中一个平行文件的扩展名,例如“es”。
-
l2: 其他平行文件的扩展名,例如“en”。
-
clean-corpus: 清理后的文件名。例如,如果输入“train.clean”,脚本将把过滤后的平行数据保存到“train.clean.es”和“train.clean.en”。
-
min: 应丢弃的段落的最小标记数。
-
max: 应丢弃的段落的最大标记数。
-
max-word-length(此处未显示):一个标记中的最大字符数。如果一个段落对包含的标记长度超过 max-word-length,则会被移除。
要清理我们的 ParaCrawl 语料库,运行的完整命令是:
clean-corpus-n.perl -max-word-length 50 train es en train.clean 0 150
这个命令从 train.es 和 train.en 中移除带有以下条件的段落:
-
空段落
-
超过 150 个单词(或标记)的段落
-
高度的繁殖力
-
单词(或标记)包含超过 50 个字符的段落
并将结果保存到“train.clean.es”和“train.clean.en”。
脚本会显示移除的段落数量。如果你做的和我一样,数据中应该剩下 99976 个段落:
clean-corpus.perl: processing train.es & .en to train.clean, cutoff 0–150, ratio 9
……….(100000)
Input sentences: 100000 Output sentences: 99976
注意,每次移除一个段落时,其平行段落也会被移除。train.clean.es 和 train.clean.en 应该有相同数量的行。你可以用以下命令检查:
wc -l train.clean.es train.clean.en
接下来,我们用 preprocess 移除段落:
我们需要先编译它(需要 cmake):
git clone https://github.com/kpu/preprocess.git
cd preprocess
mkdir build
cd build
cmake ..
make -j4
然后,我们可以使用 preprocess 来移除包含以下内容的行:
-
无效的 UTF-8
-
控制字符(除了制表符和换行符)
-
过多的常见和继承的 Unicode 脚本字符(如数字)
-
标点符号过多或过少
-
期望脚本中的内容过少(例如移除英文数据中的中文句子)
我们使用“simple_cleaning”二进制文件来处理平行数据:
preprocess/build/bin/simple_cleaning -p train.clean.es train.clean.en train.clean.pp.es train.clean.pp.en
过滤后的数据保存在两个新文件中,我将其命名为“train.clean.pp.es”和“train.clean.pp.en”。
并且应该打印:
Kept 85127 / 99976 = 0.851474
最后,我们可以用“dedupe”来去除重复项:
preprocess/build/bin/dedupe -p train.clean.pp.es train.clean.pp.en train.clean.pp.dedup.es train.clean.pp.dedup.en
并且应该打印:
Kept 84838 / 85127 = 0.996605
我们已经完成了数据清理。
我们几乎移除了 15%的段落。这意味着神经机器翻译的每个训练周期将快 15%(大约)。
第 2 步:标准化
适用步骤:所有数据集的源侧,可能还包括训练和验证数据集的目标侧。
标准化的目标是确保在所有数据集中使用相同的符号,如标点符号、数字和空格,并且具有相同的 UTF8 编码。
实际上,这一步也可以减少词汇量(不同标记类型的数量),通过将具有类似作用或意义的符号映射到相同的符号。
例如,这一步可以将这些不同的引号标准化为相同的引号,如下所示:
-
‘ → “
-
« → “
-
《 → “
此步骤还可以确保您的系统不会生成具有不同标点风格的翻译,如果你将其应用于训练数据的目标侧。
当然,如果我们还规范化训练数据的目标侧,我们必须确保映射到所需的字符。
例如,如果你偏好使用“《”,因为你的目标语言使用这种类型的引号标记,那么你应该做不同的映射,如下所示:
-
‘ →《
-
« →《
-
“ →《
由于在准备机器翻译数据时通常会执行此步骤,因此有多种工具可以完成它。
我使用了sacremoses(MIT 许可)。它是Moses 项目的normalize-punctuation.perl的 Python 实现。
实践
sacremoses 规范化工具 映射了来自多种语言的几十种符号。这些规则可以轻松编辑,以更好地符合你的期望。
sacremoses 可以通过 pip 安装(需要 Python 3):
pip install sacremoses
然后你可以使用 CLI 来规范化数据:
sacremoses normalize < train.clean.pp.dedup.es > train.clean.pp.dedup.norm.es
sacremoses normalize < train.clean.pp.dedup.en > train.clean.pp.dedup.norm.en
差异示例(使用命令“diff”获得):注意:我选择截图而不是复制粘贴这些句子,因为博客编辑器会自动应用其自身的规范化规则。
ParaCrawl V9 (CC0) 的句子在规范化前后的对比。截图由作者提供。
你可以为此命令传递多个选项,例如,如果你想规范化数字,可以添加选项“-n”。要查看所有选项,请运行:
sacremoses normalize –help
第三步:分词
适用步骤:所有数据集的源侧,以及训练和验证数据集的目标侧。
传统上,机器翻译的数据集使用基于规则的分词器进行分词。它们通常仅使用空格来分隔词元,并附加规则来处理特殊情况。
让我们以以下英文句子为例:
However, if you deactivate the cookies, you may not be able to access the full range of functions on this website.
Thirty-four retreatants gathered for a Memorial Day weekend of inspiring teachings by Ven.
Facebook: Brisbane City Council - Personal Safety
Tráiler de "The Imaginarium of Doctor Parnassus"
Smoke safety pressure gauge - UNICAL: 04953D
You can also book a holiday rental directly with the property owner or manager.
使用 sacremoses 分词器进行分词后,我们得到:
However , if you deactivate the cookies , you may not be able to access the full range of functions on this website .
Thirty-four retreatants gathered for a Memorial Day weekend of inspiring teachings by Ven .
Facebook : Brisbane City Council - Personal Safety
Tráiler de " The Imaginarium of Doctor Parnassus "
Smoke safety pressure gauge - UNICAL : 04953D
You can also book a holiday rental directly with the property owner or manager .
分词前后的差异很难察觉。如果你看不到它们,可以注意标点符号附近的空格。
出于几个原因,这些基于规则的分词器对神经机器翻译并不实用。例如,它们生成了过多的稀有词元,这些词元无法被神经模型正确建模。
数据必须“子分词”。例如,传统分词器生成的词元被拆分为更小的词元。这就是字节对编码方法 (BPE)的作用。
更简单的是,SentencePiece 方法 甚至不需要传统的分词。因此,我们少了一个工具(传统分词器),从而减少了预处理中的潜在错误/问题来源。
SentencePiece 可以直接应用于任何字符序列。这对于像日语、中文和泰语这样空格稀少的语言尤其实用。
实际上,SentencePiece 目前是大语言模型(如T5 或 FLAN 系列)中最常用的标记化算法之一。
让我们通过使用相同的英文句子来看一下它是如何工作的。我们将获得:
▁However , ▁if ▁you ▁de ac tiva te ▁the ▁cookies , ▁you ▁may ▁not ▁be ▁able ▁to ▁access ▁the ▁full ▁range ▁of ▁function s ▁on ▁this ▁website .
▁Th ir ty - fo ur ▁re tre at ants ▁gather ed ▁for ▁a ▁Me mor ial ▁Day ▁weekend ▁of ▁in spi ring ▁te ach ing s ▁by ▁Ven .
▁Facebook : ▁B ris ban e ▁City ▁Council ▁- ▁Personal ▁Safety
▁T rá il er ▁de ▁" The ▁I ma gin ar ium ▁of ▁Do ctor ▁Par nas s us "
▁S mo ke ▁safety ▁pressure ▁ga u ge ▁- ▁UN ICA L : ▁ 04 95 3 D
▁You ▁can ▁also ▁book ▁a ▁holiday ▁rental ▁directly ▁with ▁the ▁property ▁owner ▁or ▁manage r .
这些句子对人类来说更难阅读,标记化也不直观。然而,对于神经模型,这种方法效果很好。
为了获得这个结果,你首先需要训练一个 SentencePiece 模型。然后使用该模型对数据进行标记化,以及对所有将发送到我们的机器翻译系统的新输入进行标记化。
实践
要训练这个模型,我们首先需要通过以下方式安装SentencePiece:
pip install sentencepiece
然后,将并行数据的源侧和目标侧合并到一个单独的文本文件中。这将允许我们训练一个双语标记化模型,而不是为源语言和目标语言训练不同的模型。
cat train.clean.pp.dedup.norm.es train.clean.pp.dedup.norm.en > train.clean.pp.dedup.norm.es-en
在进入训练之前,我们必须决定词汇表的大小。
这个选择是一个困难但重要的决策。我们通常使用一个经验法则:8,000 到 16,000 之间的值适用于大多数用例。如果你有非常大的并行数据,可以选择更高的值;如果你的训练数据较小,例如少于 100,000 个片段对,则可以选择较低的值。
原因是如果你设置的词汇表大小过高,你的词汇表将包含更稀有的标记。如果你的训练并行数据中没有足够这些标记的实例,它们的嵌入会被估计得很差。
相反,如果你将值设置得过低,神经模型将不得不处理更小的标记,这些标记需要在其嵌入中携带更多信息。在这种情况下,模型可能难以生成良好的翻译。
由于我们的并行数据量较小,我随意选择了 8,000 作为词汇表的大小。
要开始训练:
spm_train -input=train.clean.pp.dedup.norm.es-en -model_prefix=es-en.8kspm -vocab_size=8000
这应该很快(少于 2 分钟)。
参数如下:
-
input:用于训练 SentencePiece 模型的数据。
-
model_prefix:SentencePiece 模型的名称。
-
vocab_size:词汇表的大小。
然后,你必须将模型应用于所有数据,除了测试集的目标侧(记住:我们从不接触这一部分)。
对于我们的 ParaCrawl 语料库,我们做:
spm_encode -model=es-en.8kspm.model < train.clean.pp.dedup.norm.es > train.clean.pp.dedup.norm.spm8k.es
spm_encode -model=es-en.8kspm.model < train.clean.pp.dedup.norm.en > train.clean.pp.dedup.norm.spm8k.en
就这样!我们的数据集都已预处理完毕。我们现在可以开始训练机器翻译系统了。
可选步骤:truecasing 和打乱
还有两个步骤,你可能会在一些机器翻译的预处理管道中看到:truecasing 和打乱。
Truecasing 正在被淘汰,但可能会略微提高翻译质量。这个预处理步骤将仅因其在句子中的位置而大写的字符转换为小写。
例如:
He will go to Canada and I will go to England.
I am not sure why.
以 truecased 形式表示:
he will go to Canada and I will go to England.
I am not sure why.
“He”中的“h”被小写化,因为它只因在句中的位置而被大写。这是唯一的区别。
这一步骤略微减少了词汇表的大小。
sacremoses 实现了 truecasing。
至于打乱段落对,它可能已经集成在你用来训练机器翻译系统的框架中。
训练数据通常会在每个训练周期自动重新打乱。
结论
过滤和归一化是可以显著降低训练神经机器翻译计算成本的步骤。
这些步骤也可能提高翻译质量,特别是当训练数据非常嘈杂时。
我在本文中建议的过滤和归一化规则并不适用于所有用例。它们对于大多数语言对都能很好地工作,但你可能需要根据你的语言进行调整,例如,当处理日语等亚洲语言时,你可能需要更改大多数归一化规则,以避免在日语文本中生成英文标点符号。
分词尤为关键。幸运的是,这也是最直接的步骤。大多数机器翻译的预处理管道对分词的处理是一样的,只是超参数不同。
在下一篇文章中,我将解释你需要知道的一切,以使用你刚刚预处理的数据来训练机器翻译系统:框架、神经网络结构和超参数。
我所有的文章都发布在《The Kaitchup》这本通讯中。订阅以接收每周的新闻、技巧和教程,以便在你的计算机上运行大型语言模型和机器翻译系统。
## The Kaitchup - AI on a Budget | Benjamin Marie, PhD | Substack
订阅每周的 AI 新闻、技巧和有关微调、运行和服务大型语言模型的教程……
实用数据质量审计:综合指南
原文:
towardsdatascience.com/data-quality-auditing-a-comprehensive-guide-66b7bfe2aa1a
探索如何利用 Python 生态系统进行数据质量审计
·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 5 月 1 日
–
图像作者提供。
你无法管理你无法测量的东西 —— 彼得·德鲁克
介绍
数据质量审计是在我们快速发展的、人工智能赋能的世界中不可或缺的技能。正如原油需要精炼,数据也需要清理和处理才能发挥作用。古老的格言“垃圾进,垃圾出”在今天仍然与计算机早期时代一样相关。
在本文中,我们将探讨 Python 如何帮助我们确保数据集符合成功项目的质量标准。我们将深入探讨 Python 库、代码片段和示例,供你在自己的工作流程中使用。
目录:
-
理解数据质量及其维度
-
使用 Pydantic 和 pandas_dq 验证数据
-
比较 Pydantic 和 pandas_dq
-
探索准确性和一致性
-
使用 pandas_dq 进行数据质量审计
-
结论
数据质量审计
在深入探讨工具和技术之前,让我们首先回顾数据质量的概念。根据广泛接受的行业定义,数据质量指的是数据集在准确性、完整性、时效性、有效性、唯一标识属性以及一致性方面的程度。
数据质量维度。图像作者提供。
完整性
数据质量的完整性涵盖了完成特定目标所需的所有关键数据元素。例如,针对营销目的的客户数据库,如果缺少某些客户的电话或电子邮件等关键信息,则被视为不完整。
为确保数据的完整性,组织可以使用数据分析技术。
数据分析是系统地检查和评估数据集,以发现模式、不一致性和异常。
通过仔细审查数据,可以识别出差距、特异性或缺失值,从而采取纠正措施,例如获取缺失的信息或实施强健的数据验证流程。结果是一个更可靠、更完整和更具可操作性的数据集,能够支持更好的决策制定、优化的营销工作,并最终推动业务成功。
但在进行全面的数据分析之前,任何数据质量审核的第一步都是审查数据字典:一个简明的描述性参考,定义数据集中数据元素的结构、属性和关系,作为理解和解释数据含义及目的的指南。
数据字典示例。图片由作者提供。
在手中拥有全面的审查或创建的数据字典后,当你利用如 Sweetviz、Missingno 或 Pandas_DQ 等低代码库的强大功能时,评估完整性变得轻而易举。
import missingno as msno
import sweetviz as sv
from pandas_dq import dq_report
# completeness check
msno.matrix(df)
# data profiling
Report = sv.analyze(df)
Report.show_notebook()
就个人而言,我倾向于使用 Pandas-Matplotlib-Seaborn 组合,因为它提供了我对输出的完全控制。这使我可以制作出引人入胜且视觉上吸引人的分析。
# check for missing values
import seaborn as sns
import matplotlib.pyplot as plt
def plot_missing_values(df: pd.DataFrame,
title="Missing Values Plot"):
plt.figure(figsize=(10, 6))
sns.displot(
data=df.isna().melt(value_name="missing"),
y="variable",
hue="missing",
multiple="fill",
aspect=1.25
)
plt.title(title)
plt.show()
plot_missing_values(df)
缺失值图。图片由作者提供。
唯一性
唯一性是一个数据质量维度,强调在具有唯一性约束的列中不存在重复数据。每条记录应代表一个唯一的实体,没有冗余。例如,用户列表应为每个注册用户提供唯一的 ID;多个具有相同 ID 的记录表示缺乏唯一性。
在下面的示例中,我模拟了合并两个结构相同的数据集的数据集成步骤。如果唯一性被违反,Pandas concat 函数的参数 verify_integrity
将抛出错误:
# verify integrity check
df_loans = pd.concat([df, df_pdf], verify_integrity=True)
# check duplicated ids
df_loans[df_loans.duplicated(keep=False)].sort_index()
唯一性违反。图片由作者提供。
理想情况下,你应将检查重复的存在作为数据质量审核的一部分。
def check_duplicates(df, col):
'''
Check how many duplicates are in col.
'''
# first step set index
df_check = df.set_index(col)
count = df_check.index.duplicated().sum()
del df_check
print("There are {} duplicates in {}".format(count, col))
时效性
时效性是数据质量的一个方面,关注数据的可用性和频率。最新和随时可用的数据对准确的分析和决策至关重要。例如,及时的销售报告应包含尽可能最新的数据,而不仅仅是几个月前的数据。到目前为止我们用于示例的数据集没有时间维度,因此我们无法更深入地探索频率。
时效性示例。图片由作者提供。
有效性
当我们过渡到有效性的概念时,应该认识到其在确保数据遵守既定规则、格式和标准方面的作用。有效性保证数据符合为数据集指定的模式、约束和数据类型。我们可以使用强大的 Python 库 Pydantic 来实现这一点:
# data validation on the data dictionary
from pydantic import BaseModel, Field, conint, condecimal, constr
class LoanApplication(BaseModel):
Loan_ID: int
Gender: conint(ge=1, le=2)
Married: conint(ge=0, le=1)
Dependents: conint(ge=0, le=3)
Graduate: conint(ge=0, le=1)
Self_Employed: conint(ge=0, le=1)
ApplicantIncome: condecimal(ge=0)
CoapplicantIncome: condecimal(ge=0)
LoanAmount: condecimal(ge=0)
Loan_Amount_Term: condecimal(ge=0)
Credit_History: conint(ge=0, le=1)
Property_Area: conint(ge=1, le=3)
Loan_Status: constr(regex="^[YN]$")
# Sample loan application data
loan_application_data = {
"Loan_ID": 123456,
"Gender": 1,
"Married": 1,
"Dependents": 2,
"Graduate": 1,
"Self_Employed": 0,
"ApplicantIncome": 5000,
"CoapplicantIncome": 2000,
"LoanAmount": 100000,
"Loan_Amount_Term": 360,
"Credit_History": 1,
"Property_Area": 2,
"Loan_Status": "Y"
}
# Validate the data using the LoanApplication Pydantic model
loan_application = LoanApplication(**loan_application_data)
一旦用示例测试过后,我们可以通过验证检查运行整个数据集,如果成功,应该会打印“没有数据验证问题”:
# data validation on the data dictionary
from pydantic import ValidationError
from typing import List
# Function to validate DataFrame and return a list of failed LoanApplication objects
def validate_loan_applications(df: pd.DataFrame) -> List[LoanApplication]:
failed_applications = []
for index, row in df.iterrows():
row_dict = row.to_dict()
try:
loan_application = LoanApplication(**row_dict)
except ValidationError as e:
print(f"Validation failed for row {index}: {e}")
failed_applications.append(row_dict)
return failed_applications
# Validate the entire DataFrame
failed_applications = validate_loan_applications(df_loans.reset_index())
# Print the failed loan applications or "No data quality issues"
if not failed_applications:
print("No data validation issues")
else:
for application in failed_applications:
print(f"Failed application: {application}")
我们可以用 pandas_dq 完成相同的操作,代码量要少得多:
from pandas_dq import DataSchemaChecker
schema = {
'Loan_ID': 'int64',
'Gender': 'int64',
'Married': 'int64',
'Dependents': 'int64',
'Graduate': 'int64',
'Self_Employed': 'int64',
'ApplicantIncome': 'float64',
'CoapplicantIncome': 'float64',
'LoanAmount': 'float64',
'Loan_Amount_Term': 'float64',
'Credit_History': 'int64',
'Property_Area': 'int64',
'Loan_Status': 'object'
}
checker = DataSchemaChecker(schema)
checker.fit(df_loans.reset_index())
这返回了一个易于阅读的 Pandas dataframe 风格的报告,详细说明了遇到的任何验证问题。我提供了一个不正确的 schema,其中 int64
变量被报告为 float64
变量。该库已正确识别这些:
DataSchemaChecker 输出。图片由作者提供。
数据类型不匹配通过使用从 DataSchemaChecker
类创建的检查器对象的一行代码得到纠正:
# fix issues
df_fixed = checker.transform(df_loans.reset_index())
DataSchemaChecker 转换输出。图片由作者提供。
Pydantic 还是 pandas_dq?
Pydantic 和 pandas_dq 之间有一些差异:
-
声明式语法:可以说,Pydantic 允许你使用更简洁和易读的语法定义数据 schema 和验证规则。这可以使理解和维护代码变得更容易。我发现能够定义可能值的范围而不仅仅是数据类型非常有帮助。
-
内置验证函数:Pydantic 提供了各种强大的内置验证函数,如
conint
、condecimal
和constr
,这些函数允许你对数据施加约束,而无需编写自定义验证函数。 -
全面的错误处理:使用 Pydantic 时,如果输入数据不符合定义的 schema,它会抛出一个
ValidationError
,并提供关于错误的详细信息。这可以帮助你轻松识别数据问题并采取必要的措施。 -
序列化和反序列化:Pydantic 自动处理数据的序列化和反序列化,使得处理不同的数据格式(如 JSON)及其之间的转换变得非常方便。
总之,Pydantic 提供了比 pandas_dq 的 DataSchemaChecker
类更简洁、更具功能丰富性且用户友好的数据验证方法。
Pydantic 可能是生产环境中验证数据 schema 的更好选择。但如果你只是想快速启动一个原型,可能会更喜欢 DataSchemaChecker
的低代码特性。
准确性与一致性
还有两个数据质量维度我们尚未探讨:
-
准确性 是一个数据质量维度,涉及数据的正确性,确保它能无误地表示现实世界的情况。例如,一个准确的客户数据库应该包含所有客户的正确和最新地址。
-
一致性 涉及数据在不同来源或数据集中的统一性。数据在格式、单位和数值上应该保持一致。例如,一个跨国公司应以单一货币报告收入数据,以保持在不同国家办公室间的一致性。
你可以使用dq_report函数检查数据集中存在的所有数据质量问题:
from pandas_dq import dq_report
dq_report(df_loans.reset_index(), target=None, verbose=1)
它检测以下数据质量问题:
-
强关联变量(多重共线性)
-
无差异的列(冗余特征)
-
不对称的数据分布(异常值、离群点等)
-
不频繁出现的类别
来自 pandas_dq 库的 DQ 报告。图片由作者提供。
结论
执行数据质量审计对维护高质量的数据集至关重要,这反过来又推动了更好的决策和业务成功。Python 提供了丰富的库和工具,使审计过程更为便捷和高效。
通过理解和应用本文讨论的概念和技术,你将能确保你的数据集符合项目所需的质量标准。
完整代码链接: github.com/mohwarsame273/Medium-Articles/blob/main/DataQualityAudit.ipynb
参考文献
[1] Pydantic(2023):文档 docs.pydantic.dev/
(访问于 2023 年 4 月 24 日)
[2] Pandas_dq(2023):文档 github.com/AutoViML/pandas_dq
(访问于 2023 年 4 月 24 日)
[3] 数据质量维度(企业数据管理委员会 EDM):cdn.ymaws.com/edmcouncil.org/resource/resmgr/featured_documents/BP_DQ_Dimensions_Oct17.pdf
(访问于 2023 年 4 月 24 日)
[4] Batini, C., Cappiello, C., Francalanci, C. 和 Maurino, A.(2009)。数据质量评估和改进的方法论。ACM 计算调查(CSUR),41(3),第 1–52 页。
[5] Günther, L.C., Colangelo, E., Wiendahl, H.H. 和 Bauer, C.(2019)。改进决策制定的数据质量评估:中小型企业的方法论。Procedia Manufacturing,29,第 583–591 页。