原文:
zh.annas-archive.org/md5/6A8ACC3697FE0BCDA4D2C7EE588C4E25
译者:飞龙
前言
数据科学的目的是利用数据改变世界,这个目标主要通过颠覆和改变真实行业中的真实流程来实现。要在这个层面上运作,我们需要能够构建实质性的数据科学解决方案;解决真正的问题,并且能够可靠地运行,以便人们信任并采取行动。
本书解释了如何使用 Spark 提供生产级的数据科学解决方案,这些解决方案创新、颠覆性,并且足够可靠,值得信赖。在撰写本书时,作者的意图是提供一本超越传统食谱风格的作品:不仅提供代码示例,还要培养探索内容的技巧和心态,就像一位大师;正如他们所说,“内容为王”!读者会注意到本书在新闻分析方面的重点,并偶尔引入其他数据集,如推文和金融数据。这种对新闻的重视并非偶然;我们花了很多精力试图专注于提供全球范围内提供背景的数据集。
本书致力于解决的隐含问题是缺乏提供人们如何以及为什么做出决策的适当背景的数据。通常,直接可访问的数据源非常专注于特定问题,因此在提供行为背景所需的更广泛数据集方面可能非常有限,这导致我们无法真正理解人们做出决策的驱动因素。
考虑一个简单的例子,网站用户的关键信息,如年龄、性别、位置、购物行为、购买等都是已知的,我们可以利用这些数据来推荐产品,基于其他“和他们相似”的人购买的产品。
但要想成为杰出的人,需要更多的背景信息来解释人们为什么会表现出这样的行为。当新闻报道暗示一场大西洋飓风正在接近佛罗里达海岸线,并且可能在 36 小时内到达海岸时,也许我们应该推荐人们可能需要的产品。比如 USB 充电宝、蜡烛、手电筒、净水器等物品。通过了解决策背后的背景,我们可以进行更好的科学研究。
因此,尽管本书确实包含有用的代码,而且在许多情况下还包含独特的实现,但它进一步深入探讨了真正掌握数据科学所需的技术和技能;其中一些经常被忽视或根本没有被考虑。作者们凭借多年的商业经验,利用他们丰富的知识,将数据科学的真实而令人兴奋的世界呈现出来。
本书涵盖的内容
第一章, 大数据科学生态系统,本章介绍了一种在规模上实现数据成功的方法和相关生态系统。它侧重于数据科学工具和技术,这些工具和技术将在后面的章节中使用,并介绍了环境以及如何适当地配置它。此外,它解释了一些与整体数据架构和长期成功相关的非功能性考虑。
第二章, 数据获取,作为数据科学家,最重要的任务之一是将数据准确地加载到数据科学平台中。本章解释了如何构建 Spark 中的通用数据摄入管道,这个管道可以作为可重用的组件,服务于许多输入数据源。
第三章,“输入格式和模式”,本章演示了如何从原始格式加载数据到不同的模式,从而使得可以对相同数据运行各种不同类型的下游分析。考虑到这一点,我们将研究传统上理解的数据模式领域。我们将涵盖传统数据库建模的关键领域,并解释其中一些基石原则如何今天仍然适用于 Spark。此外,当我们磨练我们的 Spark 技能时,我们将分析 GDELT 数据模型,并展示如何以高效和可扩展的方式存储这个大型数据集。
第四章,“探索性数据分析”,一个常见的误解是,EDA 仅用于发现数据集的统计属性,并提供关于如何利用它的见解。实际上,这并不是全部。完整的 EDA 将扩展这个想法,并包括对“在生产中使用这个数据源的可行性”的详细评估。这要求我们也要了解如何为这个数据集指定一个生产级的数据加载程序,这个程序可能会在很多年内以“无人值守模式”运行。本章提供了一种使用“数据概要分析”技术加速数据质量评估的快速方法。
第五章,“地理分析的 Spark”,地理处理是 Spark 的一个强大的新用例,本章演示了如何入门。本章的目的是解释数据科学家如何使用 Spark 处理地理数据,以生成基于地图的大型数据集的强大视图。我们演示了如何通过 Spark 与 Geomesa 集成轻松处理时空数据,从而帮助将 Spark 转变为一个复杂的地理处理引擎。本章后来利用这些时空数据应用机器学习,以预测石油价格。
第六章,“抓取基于链接的外部数据”,本章旨在解释一种增强本地数据的常见模式,即在 URL 或 API 上找到的外部内容,如 GDELT 和 Twitter。我们提供了一个使用 GDELT 新闻索引服务作为新闻 URL 来源的教程,演示了如何构建一个从互联网上抓取感兴趣的全球突发新闻的 Web 规模新闻扫描器。我们进一步解释了如何使用专门的网络抓取组件来克服规模的挑战,随后总结了本章。
第七章,“构建社区”,本章旨在解决数据科学和大数据中的一个常见用例。随着越来越多的人互动,交流信息,或者简单地分享不同主题的共同兴趣,整个世界可以被表示为一个图。数据科学家必须能够检测社区,找到影响者/顶级贡献者,并检测可能的异常。
第八章,“构建推荐系统”,如果要选择一个算法来向公众展示数据科学,推荐系统肯定会成为其中之一。今天,推荐系统随处可见;它们之所以如此受欢迎,是因为它们的多功能性、实用性和广泛适用性。在本章中,我们将演示如何使用原始音频信号推荐音乐内容。
第九章,“新闻词典和实时标记系统”,虽然分层数据仓库将数据存储在文件夹中,但典型的基于 Hadoop 的系统依赖于扁平架构来存储数据。如果没有适当的数据治理或对数据的清晰理解,就有不可否认的可能性将数据湖变成沼泽,其中一个有趣的数据集,如 GDELT,将不过是一个包含大量非结构化文本文件的文件夹。在本章中,我们将描述一种创新的方式,以非监督的方式对即将到来的 GDELT 数据进行标记,并且几乎是实时的。
第十章,“故事去重和变异”,在本章中,我们对 GDELT 数据库进行去重和索引,然后跟踪故事的变化,了解它们之间的联系,它们如何变异,以及它们是否可能导致不久的将来发生的事件。本章的核心是使用 Simhash 检测近似重复项,并使用随机索引构建向量以降低维度。
第十一章,“异常检测和情感分析”,也许 2016 年最引人注目的事件是紧张的美国总统选举及其最终结果:唐纳德·特朗普当选总统,这场竞选将长久被人们铭记;尤其是因为其对社交媒体的空前利用以及激起用户激情的方式,其中大多数人通过使用标签表达了自己的情感。在本章中,我们将尝试使用实时 Twitter 信息流检测美国选举期间的异常推文,而不是试图预测选举结果本身。
第十二章,“趋势演算”,在“趋势”成为数据科学家研究的热门话题之前,早有一个更古老的概念,至今仍未得到数据科学的充分应用;那就是趋势。目前,对趋势的分析,如果可以这样称呼的话,主要是由人们“用眼睛看”时间序列图表并进行解释。但人们的眼睛究竟在做什么?本章描述了 Apache Spark 中用于数值研究趋势的新算法:趋势演算。
第十三章,“安全数据”,在本书中,我们涉及了许多数据科学领域,经常涉足那些传统上与数据科学家的核心工作知识不太相关的领域。在本章中,我们将讨论另一个经常被忽视的领域,安全数据;更具体地说,如何在数据生命周期的各个阶段保护您的数据和分析结果。本章的核心是构建一个用于 Spark 的商业级加密编解码器。
第十四章,“可扩展算法”,在本章中,我们将了解为什么有时即使基本算法在小规模下运行良好,但在“大数据”中却经常失败。我们将看到如何避免在编写运行在大规模数据集上的 Spark 作业时出现问题,并了解算法的结构以及如何编写能够在数据量达到 PB 级别时扩展的自定义数据科学分析。本章涵盖了诸如:并行化策略、缓存、洗牌策略、垃圾回收优化和概率模型等领域;解释了这些如何帮助您充分利用 Spark 范式。
阅读本书需要什么
本书中使用了 Spark 2.0 以及 Scala 2.11、Maven 和 Hadoop。这是基本的环境要求,还有许多其他在相关章节中介绍的技术。
这本书是为谁准备的
我们假设阅读本书的数据科学家对数据科学、常见的机器学习方法和流行的数据科学工具有所了解,并且在工作中进行了概念验证研究并构建了原型。我们为这个受众提供了一本介绍高级技术和方法来构建数据科学解决方案的书籍,向他们展示如何构建商业级数据产品。
约定
在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“下一行代码读取链接并将其分配给BeautifulSoup
函数。”
代码块设置如下:
import org.apache.spark.sql.functions._
val rdd = rawDS map GdeltParser.toCaseClass
val ds = rdd.toDS()
// DataFrame-style API
ds.agg(avg("goldstein")).as("goldstein").show()
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
spark.sql("SELECT V2GCAM FROM GKG LIMIT 5").show
spark.sql("SELECT AVG(GOLDSTEIN) AS GOLDSTEIN FROM GKG WHERE GOLDSTEIN IS NOT NULL").show()
任何命令行输入或输出都以以下方式编写:
$ cat 20150218230000.gkg.csv | gawk -F"\t" '{print $4}'
新术语和重要单词以粗体显示。屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“为了下载新模块,我们将转到文件 | 设置 | 项目名称 | 项目解释器。”
注意
警告或重要说明会出现在这样的框中。
提示
提示和技巧会出现在这样。
第一章:大数据科学生态系统
作为一名数据科学家,您无疑对处理文件和处理大量数据非常熟悉。然而,正如您所同意的,除了简单分析单一类型的数据之外,需要一种组织和编目数据的方法,以便有效地管理数据。事实上,这是一名优秀数据科学家的基石。随着数据量和复杂性的增加,一种一致而坚固的方法可以决定泛化的成功和过度拟合的失败之间的差异!
本章是介绍一种在大规模数据上取得成功的方法和生态系统。它侧重于数据科学工具和技术。它介绍了环境以及如何适当配置,但也解释了一些与整体数据架构相关的非功能性考虑。虽然在这个阶段几乎没有实际的数据科学,但它为本书的其余部分的成功铺平了道路。
在本章中,我们将涵盖以下主题:
-
数据管理责任
-
数据架构
-
伴侣工具
介绍大数据生态系统
数据管理尤为重要,特别是当数据处于不断变化或定期产生和更新的状态时。在这些情况下需要的是一种存储、结构化和审计数据的方式,允许对模型和结果进行持续处理和改进。
在这里,我们描述了如何最好地持有和组织您的数据,以便与 Apache Spark 和相关工具在数据架构的背景下进行整合。
数据管理
即使在中期,您只打算在家里玩一点数据;然而,如果没有适当的数据管理,往往会导致努力升级到您很容易迷失方向并且会发生错误的程度。花时间考虑数据的组织,特别是其摄入,是至关重要的。没有比等待长时间运行的分析完成,整理结果并生成报告,然后发现您使用了错误版本的数据,或者数据不完整,缺少字段,甚至更糟糕的是您删除了结果更糟糕的事情了!
坏消息是,尽管它很重要,但数据管理是一个在商业和非商业企业中一直被忽视的领域,几乎没有现成的解决方案。好消息是,使用本章描述的基本构建模块进行出色的数据科学要容易得多。
数据管理责任
当我们考虑数据时,很容易忽视我们需要考虑的范围的真正程度。事实上,大多数数据“新手”以这种方式考虑范围:
-
获取数据
-
将数据放在某个地方(任何地方)
-
使用数据
-
扔掉数据
实际上,还有许多其他考虑因素,我们有责任确定哪些适用于特定的工作。以下数据管理构建模块有助于回答或跟踪有关数据的一些重要问题:
-
文件完整性
-
数据文件是否完整?
-
你怎么知道的?
-
它是否属于一组?
-
数据文件是否正确?
-
在传输过程中是否被篡改?
-
数据完整性
-
数据是否符合预期?
-
所有字段都存在吗?
-
是否有足够的元数据?
-
数据质量是否足够?
-
是否有任何数据漂移?
-
调度
-
数据是否定期传输?
-
数据多久到达一次?
-
数据是否按时接收?
-
你能证明数据是何时接收的吗?
-
它需要确认吗?
-
模式管理
-
数据是结构化的还是非结构化的?
-
数据应该如何解释?
-
是否可以推断出模式?
-
数据是否随时间改变?
-
模式是否可以从上一个版本演变?
-
版本管理
-
数据的版本是多少?
-
版本是否正确?
-
如何处理不同版本的数据?
-
您如何知道自己使用的是哪个版本?
-
安全
-
数据是否敏感?
-
它包含个人可识别信息(PII)吗?
-
它包含个人健康信息(PHI)吗?
-
它包含支付卡信息(PCI)吗?
-
我应该如何保护数据?
-
谁有权读取/写入数据?
-
是否需要匿名化/清理/混淆/加密?
-
处置
-
我们如何处理数据?
-
我们何时处置数据?
如果经过所有这些之后,你仍然不确定,在你继续编写使用gawk
和crontab
命令的 bash 脚本之前,继续阅读,你很快就会发现有一种更快、更灵活、更安全的方法,可以让你从小处着手,逐步创建商业级的摄取管道!
合适的工具来完成工作
Apache Spark 是可扩展数据处理的新兴事实标准。在撰写本书时,它是最活跃的Apache 软件基金会(ASF)项目,并且有丰富多样的伴随工具可用。每天都会出现新项目,其中许多项目在功能上有重叠。因此,需要时间来了解它们的功能并决定是否适合使用。不幸的是,这方面没有快速的方法。通常,必须根据具体情况做出特定的权衡;很少有一刀切的解决方案。因此,鼓励读者探索可用的工具并明智地选择!
本书介绍了各种技术,希望能为读者提供一些更有用和实用的技术的入门,以至于他们可以开始在自己的项目中利用它们。此外,我们希望展示,如果代码编写得当,即使决定被证明是错误的,也可以通过巧妙地使用应用程序接口(API)(或 Spark Scala 中的高阶函数)来交换技术。
总体架构
让我们从数据架构的高层介绍开始:它们的作用是什么,为什么它们有用,何时应该使用它们,以及 Apache Spark 如何适应其中。
在最一般的情况下,现代数据架构具有四个基本特征:
-
数据摄取
-
数据湖
-
数据科学
-
数据访问
现在让我们介绍每一个,这样我们可以在后面的章节中更详细地讨论。
数据摄取
传统上,数据是根据严格的规则摄取,并根据预定的模式进行格式化。这个过程被称为提取、转换、加载(ETL),仍然是一个非常常见的做法,得到了大量商业工具以及一些开源产品的支持。
ETL 方法倾向于进行前期检查,以确保数据质量和模式一致,以简化后续的在线分析处理。它特别适用于处理具有特定特征集的数据,即与经典实体关系模型相关的数据。然而,并不适用于所有情况。
在大数据革命期间,对结构化、半结构化和非结构化数据的需求出现了象征性的爆炸,导致需要处理具有不同特征集的系统的创建。这些被定义为“4V:容量、多样性、速度和准确性”www.ibmbigdatahub.com/infographic/four-vs-big-data
。传统的 ETL 方法在这种新负担下陷入困境,因为它们处理大量数据需要太长时间,或者在面对变化时过于僵化,于是出现了一种不同的方法。进入“读时模式”范式。在这里,数据以其原始形式(或至少非常接近)被摄入,规范化、验证等细节在分析处理时进行。
这通常被称为“提取加载转换”(ELT),是对传统方法的参考:
这种方法重视及时交付数据,延迟详细处理直到绝对需要。这样,数据科学家可以立即访问数据,使用一系列传统方法不可用的技术寻找洞见。
尽管我们在这里只提供了一个高层概述,但这种方法非常重要,因此在整本书中,我们将通过实施各种读时模式算法来进一步探讨。我们将假定数据摄入采用 ELT 方法,也就是说我们鼓励用户根据自己的方便加载数据。这可以是每隔n分钟、夜间或在低使用率时进行。然后可以通过运行离线批处理作业再次由用户自行决定检查数据的完整性、质量等等。
数据湖
数据湖是一个方便、无处不在的数据存储。它很有用,因为它提供了许多关键的好处,主要包括:
-
可靠的存储
-
可扩展的数据处理能力
让我们简要地看一下每一个。
可靠的存储
数据湖有许多可靠的底层存储实现,包括 Hadoop 分布式文件系统(HDFS)、MapR-FS 和 Amazon AWS S3。
在整本书中,HDFS 将被假定为存储实现。此外,在本书中,作者使用部署在 Hortonworks HDP 环境中运行的 YARN 的分布式 Spark 设置。因此,除非另有说明,否则 HDFS 是使用的技术。如果您对这些技术不熟悉,它们将在本章后面进一步讨论。
无论如何,值得知道的是,Spark 可以本地引用 HDFS 位置,通过前缀file://
访问本地文件位置,并通过前缀s3a://
引用 S3 位置。
可扩展的数据处理能力
显然,Apache Spark 将是我们首选的数据处理平台。此外,正如您可能记得的那样,Spark 允许用户在其首选环境中执行代码,无论是本地、独立、YARN 还是 Mesos,都可以通过配置适当的集群管理器来实现;在masterURL
中。顺便说一句,这可以在以下三个位置之一完成:
-
在发出
spark-submit
命令时使用--master
选项 -
在
conf/spark-defaults.conf
文件中添加spark.master
属性 -
在
SparkConf
对象上调用setMaster
方法
如果您不熟悉 HDFS,或者没有访问集群的权限,那么可以使用本地文件系统运行本地 Spark 实例,这对于测试很有用。但是要注意,有时只有在集群上执行时才会出现不良行为。因此,如果您对 Spark 很认真,值得投资于分布式集群管理器,为什么不尝试 Spark 独立集群模式,或者亚马逊 AWS EMR?例如,亚马逊提供了许多负担得起的云计算路径,您可以探索aws.amazon.com/ec2/spot/
上的抢购实例的想法。
数据科学平台
数据科学平台提供了服务和 API,使得有效的数据科学得以进行,包括探索性数据分析、机器学习模型的创建和完善、图像和音频处理、自然语言处理和文本情感分析。
这是 Spark 真正擅长的领域,也是本书剩下部分的主要重点,利用强大的本地机器学习库、无与伦比的并行图处理能力和强大的社区。Spark 为数据科学提供了真正可扩展的机会。
剩下的章节将深入探讨这些领域,包括第六章,“抓取基于链接的外部数据”,第七章,“构建社区”,和第八章,“构建推荐系统”。
数据访问
数据湖中的数据最常由数据工程师和科学家使用 Hadoop 生态系统工具访问,比如 Apache Spark、Pig、Hive、Impala 或 Drill。然而,有时其他用户,甚至其他系统,需要访问数据,而常规工具要么太技术化,要么无法满足用户对实时延迟的苛刻期望。
在这些情况下,数据通常需要被复制到数据仓库或索引存储中,以便可以暴露给更传统的方法,比如报告或仪表盘。这个过程通常涉及创建索引和重组数据以实现低延迟访问,被称为数据出口。
幸运的是,Apache Spark 有各种适配器和连接器,可以连接传统数据库、BI 工具以及可视化和报告软件。本书将介绍其中许多。
数据技术
当 Hadoop 刚开始时,Hadoop 这个词指的是 HDFS 和 MapReduce 处理范式的组合,因为这是原始论文的概要research.google.com/archive/mapreduce.html
。自那时起,出现了大量的技术来补充 Hadoop,随着 Apache YARN 的发展,我们现在看到其他处理范式的出现,比如 Spark。
现在,Hadoop 通常被用作整个大数据软件堆栈的俗语,因此在这一点上,为本书定义该堆栈的范围是明智的。本书将在整本书中访问的一系列技术的典型数据架构如下所述:
这些技术之间的关系是一个复杂的话题,因为它们之间存在复杂的相互依赖关系,例如,Spark 依赖于 GeoMesa,而 GeoMesa 又依赖于 Accumulo,Accumulo 又依赖于 Zookeeper 和 HDFS!因此,为了管理这些关系,有一些可用的平台,比如 Cloudera 或 Hortonworks HDP hortonworks.com/products/sandbox/
。这些平台提供了集中的用户界面和集中的配置。平台的选择取决于读者,然而,不建议最初安装一些技术,然后转移到受管理的平台,因为遇到的版本问题会非常复杂。因此,通常更容易从一个干净的机器开始,并在前期做出决定。
我们在本书中使用的所有软件都是与平台无关的,因此适用于前面描述的一般架构。它可以独立安装,并且在单个或多个服务器环境中使用相对简单,而不需要使用受管理的产品。
Apache Spark 的作用
在许多方面,Apache Spark 是将这些组件联系在一起的粘合剂。它越来越多地代表了软件堆栈的中心。它与各种组件集成,但没有一个是硬连接的。事实上,甚至底层存储机制都可以被替换。将这个特性与利用不同处理框架的能力相结合,意味着最初的 Hadoop 技术有效地成为组件,而不是一个庞大的框架。我们的架构的逻辑图如下所示:
随着 Spark 的发展和广泛的行业认可,许多最初的 Hadoop 实现已经被重构为 Spark。因此,为了给这个画面增加更多的复杂性,通常有几种可能的方法来以编程方式利用任何特定的组件;尤其是根据 API 是否从最初的 Hadoop Java 实现中移植出来的命令式和声明式版本。在接下来的章节中,我们尽量保持对 Spark 精神的忠实。
伴随工具
现在我们已经建立了一个要使用的技术堆栈,让我们描述每个组件,并解释它们在 Spark 环境中的用处。本书的这一部分旨在作为参考而不是直接阅读。如果您熟悉大多数技术,那么您可以刷新您的知识并继续阅读下一节,第二章,数据采集。
Apache HDFS
Hadoop 分布式文件系统(HDFS)是一个带有内置冗余的分布式文件系统。它默认优化为在三个或更多节点上工作(尽管一个节点也可以正常工作,且限制可以增加),这提供了存储数据的能力。因此,文件不仅被分割成多个块,而且这些块的三个副本在任何时候都存在。这巧妙地提供了数据冗余(如果一个丢失了,其他两个仍然存在),同时也提供了数据局部性。当对 HDFS 运行分布式作业时,系统不仅会尝试收集作业输入所需的所有块,还会尝试仅使用与运行该作业的服务器物理接近的块;因此,它有能力减少网络带宽,只使用其本地存储上的块,或者那些接近自身的节点上的块。实际上,这是通过将 HDFS 物理磁盘分配给节点,并将节点分配给机架来实现的;块是以节点本地、机架本地和集群本地的方式写入的。所有对 HDFS 的指令都通过一个名为NameNode的中央服务器传递,因此这提供了一个可能的单点故障;有各种方法可以提供 NameNode 的冗余。
此外,在多租户 HDFS 场景中,许多进程同时访问同一文件时,通过使用多个块也可以实现负载平衡;例如,如果一个文件占用一个块,这个块被复制三次,因此可能可以同时从三个不同的物理位置读取。尽管这可能看起来不是一个很大的优势,在数百或数千个节点的集群上,网络 IO 通常是运行作业的最大限制因素–作者在多千节点集群上确实经历过作业不得不等待数小时才能完成的情况,纯粹是因为网络带宽由于大量其他线程调用数据而达到最大值。
如果您正在运行笔记本电脑,需要将数据存储在本地,或者希望使用您已经拥有的硬件,那么 HDFS 是一个不错的选择。
优势
使用 HDFS 的优势如下:
-
冗余:块的可配置复制提供了对节点和磁盘故障的容忍
-
负载平衡:块复制意味着相同的数据可以从不同的物理位置访问
-
数据本地性:分析尝试访问最接近的相关物理块,减少网络 IO。
-
数据平衡:有一个算法可以在数据块变得过于集中或碎片化时重新平衡数据块。
-
灵活的存储:如果需要更多空间,可以添加更多磁盘和节点;尽管这不是一个热过程,但集群将需要停机来添加这些资源
-
额外成本:没有第三方成本涉及
-
数据加密:隐式加密(打开时)
缺点
以下是缺点:
-
NameNode 提供了一个中心故障点;为了减轻这一点,有辅助和高可用性选项可用
-
集群需要基本的管理和可能一些硬件工作
安装
要使用 HDFS,我们应该决定是以本地、伪分布式还是完全分布式的方式运行 Hadoop;对于单个服务器,伪分布式对于分析是有用的,因为分析应该可以直接从这台机器转移到任何 Hadoop 集群。无论如何,我们应该安装 Hadoop,至少包括以下组件:
-
NameNode
-
辅助 NameNode(或高可用性 NameNode)
-
DataNode
Hadoop 可以通过hadoop.apache.org/releases.html
进行安装。
Spark 需要知道 Hadoop 配置的位置,特别是以下文件:hdfs-site.xml
,core-site.xml
。然后在 Spark 配置中设置配置参数HADOOP_CONF_DIR
。
然后 HDFS 将以本地方式可用,因此在 Spark 中可以简单地使用/user/local/dir/text.txt
来访问文件hdfs://user/local/dir/text.txt
。
亚马逊 S3
S3 将所有与并行性、存储限制和安全性相关的问题都抽象化,允许非常大规模的并行读/写操作,并提供了极低的成本和极好的服务级别协议(SLA)。如果您需要快速启动、无法在本地存储数据,或者不知道未来的存储需求是什么,这是完美的选择。需要注意的是,s3n
和S3a
采用对象存储模型,而不是文件存储,因此存在一些妥协:
-
最终一致性是指一个应用程序所做的更改(创建、更新和删除)在一段时间内不可见,尽管大多数 AWS 区域现在支持写后读一致性。
-
s3n
和s3a
利用了非原子重命名和删除操作;因此,重命名或删除大型目录需要与条目数量成比例的时间。然而,在此期间,目标文件可能对其他进程可见,直到最终一致性得到解决。
S3 可以通过命令行工具(s3cmd
)通过网页和大多数流行语言的 API 访问;它通过基本配置与 Hadoop 和 Spark 进行本地集成。
优势
以下是优势:
-
无限的存储容量
-
无硬件考虑
-
可用加密(用户存储的密钥)
-
99.9%的可用性
-
冗余
缺点
以下是缺点:
-
存储和传输数据的成本
-
没有数据局部性
-
最终一致性
-
相对较高的延迟
安装
您可以创建一个 AWS 账户:aws.amazon.com/free/
。通过这个账户,您将可以访问 S3,并且只需要创建一些凭据。
当前的 S3 标准是s3a
;要通过 Spark 使用它需要对 Spark 配置进行一些更改:
spark.hadoop.fs.s3a.impl=org.apache.hadoop.fs.s3a.S3AFileSystem
spark.hadoop.fs.s3a.access.key=MyAccessKeyID
spark.hadoop.fs.s3a.secret.key=MySecretKey
如果使用 HDP,您可能还需要:
spark.driver.extraClassPath=${HADOOP_HOME}/extlib/hadoop-aws-currentversion.jar:${HADOOP_HOME}/ext/aws-java-sdk-1.7.4.jar
然后,所有 S3 文件都可以在 Spark 中使用前缀s3a://
来访问 S3 对象引用:
val rdd = spark.sparkContext.textFile("s3a://user/dir/text.txt")
我们也可以内联使用 AWS 凭据,假设我们已经设置了spark.hadoop.fs.s3a.impl
:
spark.sparkContext.textFile("s3a://AccessID:SecretKey@user/dir/file")
然而,这种方法不会接受键中的斜杠字符/
。这通常可以通过从 AWS 获取另一个键来解决(直到没有斜杠出现为止)。
我们也可以通过 AWS 账户中的 S3 选项卡下的 Web 界面浏览对象。
Apache Kafka
Apache Kafka 是一个分布式的、用 Scala 编写的消息代理,可在 Apache 软件基金会许可下使用。该项目旨在提供一个统一的、高吞吐量、低延迟的平台,用于处理实时数据源。其结果本质上是一个大规模可扩展的发布-订阅消息队列,对于企业基础设施处理流式数据非常有价值。
优势
以下是优势:
-
发布-订阅消息
-
容错
-
保证交付
-
故障时重播消息
-
高度可扩展的共享无架构
-
支持背压
-
低延迟
-
良好的 Spark-streaming 集成
-
客户端实现简单
缺点
以下是缺点:
-
至少一次语义-由于缺乏事务管理器,无法提供精确一次性消息传递
-
需要 Zookeeper 进行操作
安装
由于 Kafka 是一个发布-订阅工具,其目的是管理消息(发布者)并将其定向到相关的端点(订阅者)。这是通过经过 Kafka 实现时安装的代理来完成的。Kafka 可以通过 Hortonworks HDP 平台获得,也可以独立安装,链接如下kafka.apache.org/downloads.html
。
Kafka 使用 Zookeeper 来管理领导选举(因为 Kafka 可以分布式,从而实现冗余),在前面的链接中找到的快速入门指南可以用于设置单节点 Zookeeper 实例,并提供客户端和消费者来发布和订阅主题,这提供了消息处理的机制。
Apache Parquet
自 Hadoop 诞生以来,基于列的格式(而不是基于行)的想法得到了越来越多的支持。Parquet 已经开发出来,以利用压缩、高效的列式数据表示,并且设计时考虑了复杂的嵌套数据结构;它借鉴了 Apache Dremel 论文中讨论的算法。Parquet 允许在每一列上指定压缩方案,并且为添加更多编码做好了未来的准备。它还被设计为在整个 Hadoop 生态系统中提供兼容性,并且像 Avro 一样,将数据模式与数据本身一起存储。
优势
以下是优势:
-
列式存储
-
高度存储效率
-
每列压缩
-
支持谓词下推
-
支持列剪枝
-
与其他格式兼容,例如 Avro
-
高效读取,设计用于部分数据检索
缺点
以下是缺点:
-
不适合随机访问
-
写入可能需要大量计算
安装
Parquet 在 Spark 中是原生可用的,并且可以直接访问如下:
val ds = Seq(1, 2, 3, 4, 5).toDS
ds.write.parquet("/data/numbers.parquet")
val fromParquet = spark.read.parquet("/data/numbers.parquet")
Apache Avro
Apache Avro 最初是为 Hadoop 开发的数据序列化框架。它使用 JSON 定义数据类型和协议(尽管还有另一种 IDL),并以紧凑的二进制格式序列化数据。Avro 既提供了持久数据的序列化格式,又提供了 Hadoop 节点之间通信的传输格式,以及客户端程序与 Hadoop 服务之间的通信格式。另一个有用的功能是它能够将数据模式与数据本身一起存储,因此任何 Avro 文件都可以在不需要引用外部源的情况下读取。此外,Avro 支持模式演变,因此旧模式版本编写的 Avro 文件可以与新模式版本兼容。
优点
以下是优点:
-
模式演变
-
节省磁盘空间
-
支持 JSON 和 IDL 中的模式
-
支持多种语言
-
支持压缩
缺点
以下是缺点:
-
需要模式才能读写数据
-
序列化计算量大
安装
由于本书中使用 Scala、Spark 和 Maven 环境,因此可以导入 Avro 如下:
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.7.7</version>
</dependency>
然后就是创建模式并生成 Scala 代码,使用模式将数据写入 Avro。这在第三章 输入格式和模式中有详细说明。
Apache NiFi
Apache NiFi 起源于美国国家安全局(NSA),并于 2014 年作为其技术转移计划的一部分发布为开源项目。NiFi 能够在简单的用户界面中生成可扩展的数据路由和转换有向图。它还支持数据溯源,各种预构建的处理器以及快速高效地构建新处理器的能力。它包括优先级设置、可调的交付容忍度和反压功能,允许用户根据特定要求调整处理器和管道,甚至允许在运行时修改流程。所有这些都使其成为一个非常灵活的工具,可以构建从一次性文件下载数据流到企业级 ETL 管道的所有内容。通常使用 NiFi 构建管道和下载文件比编写快速的 bash 脚本甚至更快,加上用于此目的的功能丰富的处理器,这使其成为一个引人注目的选择。
优点
以下是优点:
-
广泛的处理器范围
-
集线器和辐射结构
-
图形用户界面(GUI)
-
可扩展
-
简化并行处理
-
简化线程处理
-
允许运行时修改
-
通过集群实现冗余
缺点
以下是缺点:
-
没有横切错误处理程序
-
表达语言只有部分实现
-
流文件版本管理不足
安装
Apache NiFi 可以与 Hortonworks 一起安装,称为 Hortonworks Dataflow。它也可以作为 Apache 的独立安装程序使用,nifi.apache.org/
。在第二章 数据采集中有关 NiFi 的介绍。
Apache YARN
YARN 是 Hadoop 2.0 的主要组件,它基本上允许 Hadoop 插入处理范式,而不仅仅限于原始的 MapReduce。YARN 由三个主要组件组成:资源管理器、节点管理器和应用程序管理器。本书不涉及深入研究 YARN;主要要理解的是,如果我们运行 Hadoop 集群,那么我们的 Spark 作业可以在客户端模式下使用 YARN 执行,如下所示:
spark-submit --class package.Class /
--master yarn /
--deploy-mode client [options] <app jar> [app options]
优点
以下是优点:
-
支持 Spark
-
支持优先级调度
-
支持数据本地性
-
作业历史存档
-
与 HDP 一起开箱即用
缺点
以下是缺点:
-
没有 CPU 资源控制
-
不支持数据谱系
安装
YARN 作为 Hadoop 的一部分安装;这可以是 Hortonworks HDP,Apache Hadoop,或其他供应商之一。无论如何,我们应该至少安装带有以下组件的 Hadoop:
-
资源管理器
-
NodeManager(1 个或更多)
为了确保 Spark 可以使用 YARN,它只需要知道yarn-site.xml
的位置,这是通过在 Spark 配置中使用YARN_CONF_DIR
参数设置的。
Apache Lucene
Lucene 是一个最初用 Java 构建的索引和搜索库工具,但现在已经移植到其他几种语言,包括 Python。 Lucene 在其时间内产生了许多子项目,包括 Mahout,Nutch 和 Tika。这些现在已成为自己的顶级 Apache 项目,而 Solr 最近作为子项目加入。Lucene 具有全面的功能,但尤其以在问答搜索引擎和信息检索系统中的使用而闻名。
优点
以下是优点:
-
高效的全文搜索
-
可扩展的
-
多语言支持
-
出色的开箱即用功能
缺点
缺点是数据库通常更适合关系操作。
安装
如果您希望了解更多并直接与库交互,可以从lucene.apache.org/
下载 Lucene。
在使用 Lucene 时,我们只需要在项目中包含lucene-core-<version>.jar
。例如,使用 Maven 时:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>6.1.0</version>
</dependency>
Kibana
Kibana 是一个分析和可视化平台,还提供图表和流数据汇总。它使用 Elasticsearch 作为其数据源(反过来使用 Lucene),因此可以利用规模上非常强大的搜索和索引功能。Kibana 可以以许多不同的方式可视化数据,包括条形图,直方图和地图。我们在本章末尾简要提到了 Kibana,并且在本书中将广泛使用它。
优点
以下是优点:
-
在规模上可视化数据
-
直观的界面,快速开发仪表板
缺点
以下是缺点:
-
只与 Elasticsearch 集成
-
Kibana 发布与特定的 Elasticsearch 版本绑定
安装
Kibana 可以作为独立的部分轻松安装,因为它有自己的 Web 服务器。它可以从www.elastic.co/downloads/kibana
下载。由于 Kibana 需要 Elasticsearch,因此还需要安装 Elasticsearch;有关更多信息,请参见前面的链接。Kibana 配置在config/kibana.yml
中处理,如果安装了独立版本的 Elasticsearch,则不需要进行任何更改,它将立即运行!
Elasticsearch
Elasticsearch 是一个基于 Lucene(见前文)的基于 Web 的搜索引擎。它提供了一个分布式的,多租户的,无模式的 JSON 文档全文搜索引擎。它是用 Java 构建的,但由于其 HTTP Web 界面,可以从任何语言中利用。这使得它特别适用于要通过网页显示的交易和/或数据密集型指令。
优点
优点如下:
-
分布式
-
无模式
-
HTTP 接口
缺点
缺点如下
-
无法执行分布式事务
-
缺乏前端工具
安装
Elasticsearch 可以从www.elastic.co/downloads/elasticsearch
下载。为了提供对 Rest API 的访问,我们可以导入 Maven 依赖项:
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-spark_2.10</artifactId>
<version>2.2.0-m1</version>
</dependency>
还有一个很好的工具可以帮助管理 Elasticsearch 内容。在chrome.google.com/webstore/category/extensions
搜索 Chrome 扩展名 Sense。也可以在www.elastic.co/guide/en/sense/current/installing.html
找到更多解释。或者,它也适用于 Kibana,网址为www.elastic.co/guide/en/sense/current/installing.html
。
Accumulo
Accumulo 是基于 Google 的 Bigtable 设计的 NoSQL 数据库,最初由美国国家安全局开发,随后于 2011 年发布给 Apache 社区。Accumulo 为我们提供了通常的大数据优势,如批量加载和并行读取,但还具有一些额外的功能;迭代器,用于高效的服务器和客户端预计算,数据聚合,最重要的是单元级安全。Accumulo 的安全方面使其在企业使用中非常有用,因为它在多租户环境中实现了灵活的安全性。Accumulo 由 Apache Zookeeper 提供支持,与 Kafka 一样,并且利用了 Apache Thrift,thrift.apache.org/
,它实现了跨语言的远程过程调用(RPC)功能。
优点
优点如下:
-
Google Bigtable 的纯实现
-
单元级安全
-
可扩展的
-
冗余
-
为服务器端计算提供迭代器
缺点
缺点如下:
-
Zookeeper 在 DevOps 中并不普遍受欢迎
-
并不总是批量关系操作的最有效选择
安装
Accumulo 可以作为 Hortonworks HDP 发布的一部分安装,也可以作为独立实例从accumulo.apache.org/
安装。然后应根据安装文档进行配置,在撰写本文时为accumulo.apache.org/1.7/accumulo_user_manual#_installation
。
在第七章中,构建社区,我们演示了 Accumulo 与 Spark 的使用,以及一些更高级的功能,如迭代器
和输入格式
。我们还展示了如何在 Elasticsearch 和 Accumulo 之间处理数据。
总结
在本章中,我们介绍了数据架构的概念,并解释了如何将责任分组为能力,以帮助管理数据的整个生命周期。我们解释了所有数据处理都需要一定程度的尽职调查,无论是由公司规定还是其他方式,没有这一点,分析及其结果很快就会变得无效。
在确定了我们的数据架构范围后,我们已经详细介绍了各个组件及其各自的优缺点,并解释了我们的选择是基于集体经验的。事实上,在选择组件时总是有选择的,他们各自的特性在做出任何承诺之前都应该仔细考虑。
在下一章中,我们将深入探讨如何获取和捕获数据。我们将建议如何将数据带入平台,并讨论与数据处理和处理相关的方面。
第二章:数据获取
作为数据科学家,将数据加载到数据科学平台是最重要的任务之一。本章解释了如何构建 Spark 中的通用数据摄入管道,而不是采用无控制的临时过程,这个管道可以作为跨多个输入数据源的可重用组件。我们将配置并演示它如何在各种运行条件下提供重要的数据管理信息。
读者将学习如何构建内容注册并使用它来跟踪加载到系统中的所有输入,并提供摄入管道的指标,以便这些流可以可靠地作为自动化的、无人值守的过程运行。
在本章中,我们将涵盖以下主题:
-
介绍全球事件、语言和情绪数据库(GDELT)数据集
-
数据管道
-
通用摄入框架
-
实时监控新数据
-
通过 Kafka 接收流数据
-
注册新内容和保险库以进行跟踪
-
在 Kibana 中可视化内容指标,以监控摄入过程和数据健康状况
数据管道
即使是最基本的分析,我们总是需要一些数据。事实上,找到正确的数据可能是数据科学中最难解决的问题之一(但这是另一本书的整个主题!)。我们已经在上一章中看到,我们获取数据的方式可以简单或复杂,视情况而定。实际上,我们可以将这个决定分解为两个不同的领域:临时和定期。
-
临时数据获取:在原型设计和小规模分析中通常是最常见的方法,因为它通常不需要额外的软件来实现。用户获取一些数据,只需在需要时从源头下载。这种方法通常是点击一个网页链接并将数据存储在方便的地方,尽管数据可能仍然需要进行版本控制和安全保护。
-
定期数据获取:在大规模和生产分析的受控环境中使用;还有一个很好的理由将数据集摄入数据湖以备将来使用。随着物联网(IoT)的增长,在许多情况下产生了大量数据,如果数据不立即摄入,就会永远丢失。其中许多数据今天可能没有明显的用途,但将来可能会有用;因此,心态是收集所有数据以防需要时使用,并在确定不需要时稍后删除它。
很明显,我们需要一种灵活的数据获取方法,支持各种采购选项。
通用摄入框架
有许多方法可以处理数据获取,从自制的 bash 脚本到高端商业工具。本节的目的是介绍一个高度灵活的框架,我们可以用于小规模数据摄入,然后根据需求的变化扩展到完整的公司管理工作流程。这个框架将使用Apache NiFi构建。NiFi 使我们能够构建大规模的集成数据管道,将数据传输到全球各地。此外,它也非常灵活,易于构建简单的管道,通常甚至比使用 bash 或任何其他传统脚本方法更快。
注意
如果在多次采集同一数据集时采用了临时方法,那么应认真考虑是否应将其归类为定期类别,或者至少是否应引入更健壮的存储和版本控制设置。
我们选择使用 Apache NiFi,因为它提供了一个解决方案,可以创建许多不同复杂度的管道,并且可以扩展到真正的大数据和物联网级别,并且还提供了一个出色的拖放界面(使用所谓的基于流的编程 https://en.wikipedia.org/wiki/Flow-based_programming)。通过工作流生产的模式、模板和模块,它自动处理了许多传统上困扰开发人员的复杂功能,如多线程、连接管理和可扩展处理。对于我们的目的,它将使我们能够快速构建简单的原型管道,并在需要时将其扩展到完整的生产环境。
它已经被很好地记录下来,并且可以通过遵循nifi.apache.org/download.html
上的信息来轻松运行。它在浏览器中运行,看起来像这样:
我们将 NiFi 的安装留给读者自己去练习,我们鼓励您这样做,因为我们将在接下来的部分中使用它。
介绍 GDELT 新闻流
希望现在我们已经启动并运行了 NiFi,并且可以开始摄入一些数据。因此,让我们从 GDELT 获取一些全球新闻媒体数据。以下是我们从 GDELT 网站blog.gdeltproject.org/gdelt-2-0-our-global-world-in-realtime/
中摘取的简要信息:
在 GDELT 监控世界各地的新闻报道发布后的 15 分钟内,它已经将其翻译并加工处理,以识别所有事件、计数、引用、人物、组织、地点、主题、情感、相关图像、视频和嵌入式社交媒体帖子,并将其放入全球背景中,并通过实时开放的元数据火线提供所有这些内容,从而实现对地球本身的开放研究。
作为世界上最大的情感分析部署,我们希望通过将跨越多种语言和学科的许多情感和主题维度实时应用于来自全球各地的突发新闻,从而激发我们对情感的全新时代,以及它如何帮助我们更好地理解我们如何情境化、解释、回应和理解全球事件的方式。
我认为这是一个相当具有挑战性的任务!因此,与其拖延,暂停在这里指定细节,不如立即开始。我们将在接下来的章节中使用 GDELT 的各个方面。
为了开始使用这些开放数据,我们需要连接到元数据火线并将新闻流引入我们的平台。我们该如何做呢?让我们首先找出可用的数据。
实时发现 GDELT
GDELT 在其网站上发布了最新文件的列表。该列表每 15 分钟更新一次。在 NiFi 中,我们可以设置一个数据流,该数据流将轮询 GDELT 网站,从此列表中获取文件,并将其保存到 HDFS,以便我们以后使用。
在 NiFi 数据流设计师中,通过将处理器拖放到画布上并选择GetHTTP
功能来创建一个 HTTP 连接器。
要配置此处理器,您需要输入文件列表的 URL,如下所示:
data.gdeltproject.org/gdeltv2/lastupdate.txt
还要为您将要下载的文件列表提供一个临时文件名。在下面的示例中,我们使用了 NiFi 的表达式语言来生成一个通用唯一键,以便不会覆盖文件(UUID()
)。
值得注意的是,对于这种类型的处理器(GetHTTP
方法),NiFi 支持多种调度和定时选项,用于轮询和检索。目前,我们将使用默认选项,让 NiFi 为我们管理轮询间隔。
最新的 GDELT 文件列表示例如下:
接下来,我们将解析 GKG 新闻流的 URL,以便稍后可以获取它。通过将处理器拖放到画布上并选择ExtractText
来创建一个正则表达式解析器。现在,将新处理器放在现有处理器下面,并从顶部处理器向底部处理器拖动一条线。最后,在弹出的连接对话框中选择success
关系。
这在以下示例中显示:
接下来,让我们配置ExtractText
处理器,使用一个只匹配文件列表相关文本的正则表达式,例如:
([^ ]*gkg.csv.*)
从这个正则表达式中,NiFi 将创建一个新的属性(在本例中称为url
),与流程设计相关联,每个特定实例通过流程时都会采用新值。它甚至可以配置为支持多个线程。
同样,这个示例如下所示:
值得注意的是,虽然这是一个相当具体的例子,但这种技术是故意通用的,可以在许多情况下使用。
我们的第一个 GDELT 数据源
现在我们有了 GKG 数据源的 URL,通过配置InvokeHTTP
处理器来使用我们之前创建的url
属性作为其远程端点,并像以前一样拖动线。
现在剩下的就是使用UnpackContent
处理器(使用基本的.zip
格式)解压缩压缩内容,并使用PutHDFS
处理器保存到 HDFS,如下所示:
改进发布和订阅
到目前为止,这个流程看起来非常点对点,这意味着如果我们要引入新的数据消费者,例如 Spark-streaming 作业,流程必须更改。例如,流程设计可能必须更改为如下所示:
如果我们再添加另一个,流程必须再次更改。事实上,每次添加新的消费者时,流程都会变得更加复杂,特别是当所有错误处理都添加进去时。显然,这并不总是可取的,因为引入或移除数据的消费者(或生产者)可能是我们经常甚至频繁想要做的事情。此外,尽可能保持流程简单和可重用也是一个好主意。
因此,为了更灵活的模式,我们可以将数据发布到Apache Kafka,而不是直接写入 HDFS。这使我们能够随时添加和移除消费者,而不必更改数据摄入管道。如果需要,我们还可以从 Kafka 向 HDFS 写入,甚至可以通过设计一个单独的 NiFi 流程,或者直接使用 Spark-streaming 连接到 Kafka。
为了做到这一点,我们通过将处理器拖放到画布上并选择PutKafka
来创建一个 Kafka 写入器。
我们现在有了一个简单的流程,不断轮询可用文件列表,定期检索新流的最新副本,解压内容,并将其逐条记录流入 Kafka,这是一个持久的、容错的、分布式消息队列,用于 Spark-streaming 处理或在 HDFS 中存储。而且,这一切都不需要写一行 bash 代码!
内容注册
我们在本章中看到,数据摄入是一个经常被忽视的领域,它的重要性不容小觑。在这一点上,我们有一个管道,使我们能够从源头摄入数据,安排摄入,并将数据定向到我们选择的存储库。但故事并不会在这里结束。现在我们有了数据,我们需要履行我们的数据管理责任。进入内容注册。
我们将建立一个与我们摄入的数据相关的元数据索引。数据本身仍将被定向到存储(在我们的示例中是 HDFS),但另外,我们将存储有关数据的元数据,以便我们可以跟踪我们收到的数据,并了解一些基本信息,例如我们何时收到它,它来自哪里,它有多大,它是什么类型,等等。
选择和更多选择
我们使用的存储元数据的技术选择是基于知识和经验的。对于元数据索引,我们至少需要以下属性:
-
易于搜索
-
可扩展的
-
并行写入能力
-
冗余
有许多满足这些要求的方法,例如我们可以将元数据写入 Parquet,存储在 HDFS 中,并使用 Spark SQL 进行搜索。然而,在这里,我们将使用Elasticsearch,因为它更好地满足了要求,特别是因为它通过 REST API 方便地进行低延迟查询我们的元数据,这对于创建仪表板非常有用。事实上,Elasticsearch 具有与Kibana直接集成的优势,这意味着它可以快速生成我们内容注册的丰富可视化。因此,出于这个原因,我们将考虑使用 Elasticsearch。
随波逐流
使用我们当前的 NiFi 管道流,让我们从“从 URL 获取 GKG 文件”中分叉输出,以添加一组额外的步骤,以允许我们在 Elasticsearch 中捕获和存储这些元数据。这些是:
-
用我们的元数据模型替换流内容。
-
捕获元数据。
-
直接存储在 Elasticsearch 中。
在 NiFi 中的样子如下:
元数据模型
因此,这里的第一步是定义我们的元数据模型。有许多方面可以考虑,但让我们选择一组可以帮助解决之前讨论中的一些关键问题的集合。如果需要,这将为将来进一步添加数据提供一个良好的基础。因此,让我们保持简单,使用以下三个属性:
-
文件大小
-
摄取日期
-
文件名
这些将提供接收文件的基本注册。
接下来,在 NiFi 流程中,我们需要用这个新的元数据模型替换实际的数据内容。一个简单的方法是,从我们的模型中创建一个 JSON 模板文件。我们将它保存到本地磁盘,并在FetchFile
处理器中使用它,以用这个骨架对象替换流的内容。这个模板会看起来像这样:
{
"FileSize": SIZE,
"FileName": "FILENAME",
"IngestedDate": "DATE"
}
请注意在属性值的位置上使用了占位符名称(SIZE, FILENAME, DATE
)。这些将逐个被一系列ReplaceText
处理器替换,这些处理器使用 NiFi 表达式语言提供的正则表达式,例如DATE
变成${now()}
。
最后一步是将新的元数据负载输出到 Elasticsearch。同样,NiFi 已经准备了一个处理器来实现这一点;PutElasticsearch
处理器。
Elasticsearch 中的一个元数据条目示例:
{
"_index": "gkg",
"_type": "files",
"_id": "AVZHCvGIV6x-JwdgvCzW",
"_score": 1,
"source": {
"FileSize": 11279827,
"FileName": "20150218233000.gkg.csv.zip",
"IngestedDate": "2016-08-01T17:43:00+01:00"
}
现在我们已经添加了收集和查询元数据的能力,我们现在可以访问更多可用于分析的统计数据。这包括:
-
基于时间的分析,例如,随时间变化的文件大小
-
数据丢失,例如,时间轴上是否有数据空缺?
如果需要特定的分析,NIFI 元数据组件可以进行调整以提供相关的数据点。事实上,可以构建一个分析来查看历史数据,并根据需要更新索引,如果当前数据中不存在元数据。
Kibana 仪表板
在本章中我们多次提到了 Kibana。现在我们在 Elasticsearch 中有了元数据索引,我们可以使用该工具来可视化一些分析。这个简短的部分的目的是为了演示我们可以立即开始对数据进行建模和可视化。要查看 Kibana 在更复杂的场景中的使用,请参阅第九章*,新闻词典和实时标记系统*。在这个简单的示例中,我们完成了以下步骤:
-
在设置选项卡中添加了我们的 GDELT 元数据的 Elasticsearch 索引。
-
在发现选项卡下选择文件大小。
-
选择可视化以文件大小。
-
将
聚合
字段更改为范围
。 -
输入范围的值。
生成的图显示了文件大小的分布:
从这里开始,我们可以自由地创建新的可视化,甚至是一个功能齐全的仪表板,用于监控我们文件摄取的状态。通过增加从 NiFi 写入 Elasticsearch 的元数据的多样性,我们可以在 Kibana 中提供更多字段,甚至可以从这里开始我们的数据科学之旅,获得一些基于摄取的可操作见解。
现在我们有一个完全运行的数据管道,为我们提供实时数据源,那么我们如何确保接收到的数据质量呢?让我们看看有哪些选项。
质量保证
实施了初始数据摄取能力,并将数据流入平台后,您需要决定“前门”需要多少质量保证。可以完全没有初始质量控制,并随着时间和资源的允许逐渐建立起来(对历史数据进行回顾扫描)。但是,最好从一开始就安装基本的验证。例如,基本检查,如文件完整性、奇偶校验、完整性、校验和、类型检查、字段计数、过期文件、安全字段预填充、去规范化等。
您应该注意,您的前期检查不要花费太长时间。根据您的检查强度和数据的大小,遇到无法在下一个数据集到达之前完成所有处理的情况并不罕见。您始终需要监视您的集群资源,并计算最有效的时间利用方式。
以下是一些粗略容量规划计算的示例:
示例 1 - 基本质量检查,没有竞争用户
-
数据每 15 分钟摄取一次,从源头拉取需要 1 分钟
-
质量检查(完整性、字段计数、字段预填充)需要 4 分钟
-
计算集群上没有其他用户
其他任务有 10 分钟的资源可用。
由于集群上没有其他用户,这是令人满意的-不需要采取任何行动。
示例 2 - 高级质量检查,没有竞争用户
-
数据每 15 分钟摄取一次,从源头拉取需要 1 分钟
-
质量检查(完整性、字段计数、字段预填充、去规范化、子数据集构建)需要 13 分钟
-
计算集群上没有其他用户
其他任务只有 1 分钟的资源可用。
您可能需要考虑:
-
配置资源调度策略
-
减少摄取的数据量
-
减少我们进行的处理量
-
向集群添加额外的计算资源
示例 3 - 基本质量检查,由于竞争用户,效用度为 50%
-
数据每 15 分钟摄取一次,从源头拉取需要 1 分钟
-
质量检查(完整性、字段计数、字段预填充)需要 4 分钟(100%效用)
-
计算集群上有其他用户
*其他任务有 6 分钟的资源可用(15-1-(4 (100/50)))。由于还有其他用户,存在无法完成处理并出现作业积压的危险。
当遇到时间问题时,您有多种选择可以避免任何积压:
-
在某些时候协商独占资源的使用权
-
配置资源调度策略,包括:
-
YARN 公平调度程序:允许您定义具有不同优先级的队列,并通过在启动时设置
spark.yarn.queue
属性来定位您的 Spark 作业,以便始终优先考虑您的作业 -
动态资源分配:允许同时运行的作业自动扩展以匹配它们的利用率
-
Spark 调度程序池:允许您在使用多线程模型共享
SparkContext
时定义队列,并通过设置spark.scheduler.pool
属性来定位您的 Spark 作业,以便每个执行线程都优先考虑 -
在集群安静时过夜运行处理作业
无论如何,您最终会对作业的各个部分的表现有一个很好的了解,然后就能够计算出可以提高效率的改变。在使用云提供商时,总是有增加更多资源的选项,但我们当然鼓励对现有资源进行智能利用-这样更具可扩展性,更便宜,并且建立数据专业知识。
总结
在本章中,我们详细介绍了 Apache NiFi GDELT 摄取管道的完整设置,包括元数据分支和对生成数据的简要介绍。本节非常重要,因为 GDELT 在整本书中被广泛使用,而 NiFi 方法是一种可扩展和模块化的数据来源方法。
在下一章中,我们将学习一旦数据到达后该如何处理数据,包括查看模式和格式。
第三章:输入格式和模式
本章的目的是演示如何将数据从原始格式加载到不同的模式中,从而使得可以对相同的数据运行各种不同类型的下游分析。在编写分析报告,甚至更好的是构建可重用软件库时,通常需要使用固定输入类型的接口。因此,根据目的在不同模式之间灵活转换数据,可以在下游提供相当大的价值,无论是在扩大可能的分析类型还是重复使用现有代码方面。
我们的主要目标是了解伴随 Spark 的数据格式特性,尽管我们也将深入探讨数据管理的细节,介绍能够增强数据处理并提高生产力的成熟方法。毕竟,很可能在某个时候需要正式化您的工作,了解如何避免潜在的长期问题在撰写分析报告时和很久之后都是非常宝贵的。
考虑到这一点,我们将利用本章来研究传统上理解良好的数据模式领域。我们将涵盖传统数据库建模的关键领域,并解释一些这些基石原则如何仍然适用于 Spark。
此外,当我们磨练我们的 Spark 技能时,我们将分析 GDELT 数据模型,并展示如何以高效和可扩展的方式存储这个大型数据集。
我们将涵盖以下主题:
-
维度建模:与 Spark 相关的优点和缺点
-
关注 GDELT 模型
-
揭开按需模式的盖子
-
Avro 对象模型
-
Parquet 存储模型
让我们从一些最佳实践开始。
结构化生活是美好的生活
在了解 Spark 和大数据的好处时,您可能听过关于结构化数据与半结构化数据与非结构化数据的讨论。虽然 Spark 推广使用结构化、半结构化和非结构化数据,但它也为这些数据的一致处理提供了基础。唯一的约束是它应该是基于记录的。只要是基于记录的,数据集就可以以相同的方式进行转换、丰富和操作,而不管它们的组织方式如何。
然而,值得注意的是,拥有非结构化数据并不意味着采取非结构化的方法。在上一章中已经确定了探索数据集的技术,很容易就会有冲动直接将数据存储在可访问的地方,并立即开始简单的分析。在现实生活中,这种活动经常优先于尽职调查。再次,我们鼓励您考虑几个关键领域,例如文件完整性、数据质量、时间表管理、版本管理、安全性等等,在开始这项探索之前。这些都不应被忽视,许多都是非常重要的话题。
因此,虽然我们已经在第二章中涵盖了许多这些问题,数据获取,并且以后还会学习更多,例如在第十三章中,安全数据,但在本章中,我们将专注于数据输入和输出格式,探索一些我们可以采用的方法,以确保更好的数据处理和管理。
GDELT 维度建模
由于我们选择在本书中使用 GDELT 进行分析,我们将首先介绍使用这个数据集的第一个示例。首先,让我们选择一些数据。
有两个可用的数据流:全球知识图谱(GKG)和事件。
对于本章,我们将使用 GKG 数据来创建一个可以从 Spark SQL 查询的时间序列数据集。这将为我们提供一个很好的起点,以创建一些简单的入门分析。
在接下来的章节中,第四章, 探索性数据分析 和 第五章, 用于地理分析的 Spark,我们将更详细地讨论,但仍然与 GKG 保持联系。然后,在第七章, 构建社区,我们将通过生成自己的人员网络图来探索事件,并在一些酷炫的分析中使用它。
GDELT 模型
GDELT 已经存在了 20 多年,在这段时间里经历了一些重大的修订。为了保持简单,让我们限制我们的数据范围从 2013 年 4 月 1 日开始,当时 GDELT 进行了一次重大的文件结构改革,引入了 GKG 文件。值得注意的是,本章讨论的原则适用于所有版本的 GDELT 数据,但是在此日期之前的特定模式和统一资源标识符(URI)可能与描述的不同。我们将使用的版本是 GDELT v2.1,这是撰写时的最新版本。但值得注意的是,这与 GDELT 2.0 只有轻微的不同。
GKG 数据中有两条数据轨道:
-
整个知识图,以及它的所有字段。
-
包含一组预定义类别的图的子集。
我们将首先查看第一条轨道。
首次查看数据
我们在第二章中讨论了如何下载 GDELT 数据,因此,如果您已经配置了 NiFi 管道来下载 GKG 数据,只需确保它在 HDFS 中可用。但是,如果您还没有完成该章节,我们鼓励您首先这样做,因为它解释了为什么应该采取结构化方法来获取数据。
虽然我们已经竭尽全力阻止临时数据下载的使用,但本章的范围当然是已知的,因此,如果您有兴趣跟随这里看到的示例,可以跳过使用 NiFi 直接获取数据(以便尽快开始)。
如果您希望下载一个样本,这里是在哪里找到 GDELT 2.1 GKG 主文件列表的提醒:
http://data.gdeltproject.org/gdeltv2/masterfilelist.txt
记下与.gkg.csv.zip
匹配的最新条目,使用您喜欢的 HTTP 工具进行复制,并将其上传到 HDFS。例如:
wget http://data.gdeltproject.org/gdeltv2/20150218230000.gkg.csv.zip -o log.txt
unzip 20150218230000.gkg.csv.zip
hdfs dfs -put 20150218230000.gkg.csv /data/gdelt/gkg/2015/02/21/
现在您已经解压了 CSV 文件并将其加载到 HDFS 中,让我们继续并查看数据。
注意
在加载到 HDFS 之前,实际上不需要解压数据。Spark 的TextInputFormat
类支持压缩类型,并且会自动解压缩。但是,由于我们在上一章中在 NiFi 管道中解压了内容,为了保持一致性,这里进行了解压缩。
核心全球知识图模型
有一些重要的原则需要理解,这将在长远来看节省时间,无论是在计算还是人力方面。就像许多 CSV 文件一样,这个文件隐藏了一些复杂性,如果在这个阶段不理解清楚,可能会在我们进行大规模分析时成为一个真正的问题。GDELT 文档描述了数据。可以在这里找到:data.gdeltproject.org/documentation/GDELT-Global_Knowledge_Graph_Codebook-V2.1.pdf
。
它表明每个 CSV 行都是以换行符分隔的,并且结构如图 1所示:
图 1 GDELT GKG v2.1
乍一看,这似乎是一个不错的简单模型,我们可以简单地查询一个字段并使用其中的数据,就像我们每天导入和导出到 Microsoft Excel 的 CSV 文件一样。然而,如果我们更详细地检查字段,就会清楚地看到一些字段实际上是对外部来源的引用,而另一些字段是扁平化的数据,实际上是由其他表表示的。
隐藏的复杂性
核心 GKG 模型中的扁平化数据结构代表了隐藏的复杂性。例如,查看文档中的 V2GCAM 字段,它概述了这样一个想法,即这是一个包含冒号分隔的键值对的逗号分隔块的系列,这些对表示 GCAM 变量及其相应计数。就像这样:
wc:125,c2.21:4,c10.1:40,v10.1:3.21111111
如果我们参考 GCAM 规范,data.gdeltproject.org/documentation/GCAM-MASTER-CODEBOOK.TXT
,我们可以将其翻译为:
还有其他以相同方式工作的字段,比如V2Locations
,V2Persons
,V2Organizations
等等。那么,这里到底发生了什么?所有这些嵌套结构是什么意思?为什么选择以这种方式表示数据?实际上,事实证明,这是一种方便的方法,可以将维度模型折叠成单行记录,而不会丢失数据或交叉引用。事实上,这是一种经常使用的技术,称为非规范化。
非规范化模型
传统上,维度模型是一个包含许多事实和维度表的数据库表结构。它们通常被称为星型或雪花模式,因为它们在实体关系图中的外观。在这样的模型中,事实是一个可以计数或求和的值,通常在给定时间点提供测量。由于它们通常基于交易或重复事件,事实的数量很可能会变得非常庞大。另一方面,维度是信息的逻辑分组,其目的是为了限定或给事实提供背景。它们通常通过分组或聚合来解释事实的入口点。此外,维度可以是分层的,一个维度可以引用另一个维度。我们可以在图 2中看到扩展的 GKG 维度结构的图表。
在我们的 GCAM 示例中,事实是上表中的条目,维度是 GCAM 参考本身。虽然这可能看起来是一个简单的逻辑抽象,但这意味着我们有一个重要的关注点需要仔细考虑:维度建模对于传统数据库非常适用,其中数据可以分割成表 - 在这种情况下,GKG 和 GCAM 表 - 因为这些类型的数据库,本质上是针对这种结构进行了优化。例如,查找值或聚合事实的操作是本地可用的。然而,在使用 Spark 时,我们认为理所当然的一些操作可能非常昂贵。例如,如果我们想要对数百万条条目的所有 GCAM 字段进行平均,那么我们将有一个非常庞大的计算任务。我们将在下图中更详细地讨论这个问题:
图 2 GDELT GKG 2.1 扩展
扁平化数据的挑战
在探索了 GKG 数据模式之后,我们现在知道,这个分类法是一个典型的星型模式,有一个单一的事实表引用多个维度表。有了这种层次结构,如果我们需要以与传统数据库相同的方式切片和切块数据,我们肯定会遇到困难。
但是,是什么让在 Spark 上处理变得如此困难呢?让我们来看看这种类型组织固有的三个不同问题。
问题 1 - 上下文信息的丢失
首先,数据集中的每条记录中使用的各种数组是一个问题。例如,V1Locations
,V1Organizations
和V1Persons
字段都包含一个或多个对象的列表。由于我们没有用于获取此信息的原始文本(尽管有时我们可以获取到,如果来源是 WEB、JSTOR 等,因为这些将包含指向源文件的链接),我们失去了实体之间关系的上下文。
例如,如果我们的数据中有[Barack Obama, David Cameron, Francois Hollande, USA, France, GB, Texaco, Esso, Shell],那么我们可以假设这篇文章与一场关于石油危机的国家元首会议有关。然而,这只是一个假设,也许并非如此,如果我们真的客观,我们同样可以假设这篇文章与拥有著名名字的公司有关。
为了帮助我们推断实体之间的关系,我们可以开发一个时间序列模型,它接受一定时间段内 GDELT 字段的所有个体内容,并执行扩展连接。因此,在简单的层面上,那些更常见的对可能更有关联,我们可以开始做一些更具体的假设。例如,如果我们在时间序列中看到[Barack Obama, USA]出现了 10 万次,而[Barack Obama, France]只出现了 5000 次,那么第一对之间很可能存在强关系,而第二对之间存在次要关系。换句话说,我们可以识别脆弱的关系,并在需要时移除它们。这种方法可以被用来在规模上识别明显无关的实体之间的关系。在第七章, 建立社区中,我们使用这个原则来识别一些非常不太可能的人之间的关系!
问题 2:重新建立维度
对于任何非规范化的数据,应该可以重建或膨胀原始的维度模型。考虑到这一点,让我们来看一个有用的 Spark 函数,它将帮助我们扩展数组并产生一个扁平化的结果;它被称为DataFrame.explode
,下面是一个说明性的例子:
case class Grouped(locations:Array[String], people:Array[String])
val group = Grouped(Array("USA","France","GB"),
Array("Barack Obama","David Cameron", "Francois Hollande"))
val ds = Seq(group).toDS
ds.show
+-----------------+--------------------+
| locations| people|
+-----------------+--------------------+
|[USA, France, GB]|Barack Obama, Da...|
+-----------------+--------------------+
val flatLocs = ds.withColumn("locations",explode($"locations"))
flatLocs.show
+---------+--------------------+
|Locations| People|
+---------+--------------------+
| USA|[Barack Obama, Da...|
| France|[Barack Obama, Da...|
| GB|[Barack Obama, Da...|
+---------+--------------------+
val flatFolk = flatLocs.withColumn("people",explode($"people"))
flatFolk.show
+---------+-----------------+
|Locations| People|
+---------+-----------------+
| USA| Barack Obama|
| USA| David Cameron|
| USA|Francois Hollande|
| France| Barack Obama|
| France| David Cameron|
| France|Francois Hollande|
| GB| Barack Obama|
| GB| David Cameron|
| GB|Francois Hollande|
+---------+-----------------+
使用这种方法,我们可以轻松扩展数组,然后执行我们选择的分组。一旦扩展,数据就可以使用DataFrame
方法轻松聚合,甚至可以使用 SparkSQL 进行。我们的存储库中的 Zeppelin 笔记本中可以找到一个例子。
重要的是要理解,虽然这个函数很容易实现,但不一定高效,并且可能隐藏所需的底层处理复杂性。事实上,在本章附带的 Zeppelin 笔记本中有一个使用 GKG 数据的 explode 函数的例子,如果 explode 函数的范围不合理,那么函数会因为内存耗尽而返回堆空间问题。
这个函数并不能解决消耗大量系统资源的固有问题,因此在使用时仍需小心。虽然这个一般性问题无法解决,但可以通过仅执行必要的分组和连接,或者提前计算它们并确保它们在可用资源内完成来进行管理。你甚至可能希望编写一个算法,将数据集拆分并按顺序执行分组,每次持久化。我们在[第十四章中探讨了帮助我们解决这个问题以及其他常见处理问题的方法,可扩展算法。
问题 3:包含参考数据
对于这个问题,让我们来看一下我们在图 3中扩展的 GDELT 事件数据:
图 3 GDELT 事件分类
这种图解表示方式引起了对数据关系的关注,并表明了我们可能希望如何扩展它。在这里,我们看到许多字段只是代码,需要将其翻译回原始描述,以呈现有意义的内容。例如,为了解释Actor1CountryCode
(GDELT 事件),我们需要将事件数据与一个或多个提供翻译文本的单独参考数据集进行连接。在这种情况下,文档告诉我们参考位于这里的 CAMEO 数据集:data.gdeltproject.org/documentation/CAMEO.Manual.1.1b3.pdf
。
这种类型的连接在数据规模上一直存在严重问题,并且根据给定的情况有各种处理方法-在这个阶段重要的是要准确了解您的数据将如何使用,哪些连接可能需要立即执行,哪些可能推迟到将来的某个时候。
在我们选择在处理之前完全去规范化或展开数据的情况下,提前进行连接是有意义的。在这种情况下,后续的分析肯定会更有效,因为相关的连接已经完成:
因此,在我们的例子中:
wc:125,c2.21:4,c10.1:40,v10.1:3.21111111
对于记录中的每个代码,都要连接到相应的参考表,整个记录变为:
WordCount:125, General_Inquirer_Bodypt:4, SentiWordNet:40, SentiWordNet average: v10.1:3.21111111
这是一个简单的改变,但如果在大量行上执行,会占用大量磁盘空间。权衡的是,连接必须在某个时刻执行,可能是在摄取时或在摄取后作为定期批处理作业;将数据摄取为原样,并在方便用户的时候对数据集进行展开是完全合理的。无论如何,展开的数据可以被任何分析工具使用,数据分析师不需要关注这个潜在的隐藏问题。
另一方面,通常,推迟连接直到处理的后期可能意味着要连接的记录较少-因为可能在管道中有聚合步骤。在这种情况下,尽可能晚地连接到表是值得的,因为通常参考或维度表足够小,可以进行广播连接或映射端连接。由于这是一个如此重要的主题,我们将继续在整本书中探讨不同的处理连接场景的方法。
加载您的数据
正如我们在前几章中所概述的,传统的系统工程通常采用一种模式,将数据从其源移动到其目的地,即 ETL,而 Spark 倾向于依赖于读时模式。由于重要的是理解这些概念与模式和输入格式的关系,让我们更详细地描述这个方面:
表面上看,ETL 方法似乎是合理的,事实上,几乎每个存储和处理数据的组织都已经实施了这种方法。有一些非常受欢迎、功能丰富的产品非常擅长执行 ETL 任务-更不用说 Apache 的开源产品 Apache Camel 了camel.apache.org/etl-example.html
。
然而,这种表面上简单的方法掩盖了实施甚至简单数据管道所需的真正努力。这是因为我们必须确保所有数据在使用之前都符合固定的模式。例如,如果我们想要从起始目录摄取一些数据,最小的工作如下:
-
确保我们始终关注接送目录。
-
数据到达时,收集它。
-
确保数据没有遗漏任何内容,并根据预定义的规则集进行验证。
-
根据预定义的规则集提取我们感兴趣的数据部分。
-
根据预定义的模式转换这些选定的部分。
-
使用正确的版本化模式将数据加载到存储库(例如数据库)。
-
处理任何失败的记录。
我们立即可以看到这里有一些必须解决的格式问题:
-
我们有一个预定义的规则集,因此必须进行版本控制。任何错误都将意味着最终数据库中存在错误数据,并且需要通过 ETL 过程重新摄入数据以进行更正(非常耗时和资源密集)。对入站数据集格式的任何更改都将导致此规则集的更改。
-
对目标模式的任何更改都需要非常谨慎的管理。至少,ETL 中需要进行版本控制的更改,甚至可能需要重新处理之前的一些或全部数据(这可能是一个非常耗时和昂贵的回程)。
-
对终端存储库的任何更改都将导致至少一个版本控制模式的更改,甚至可能是一个新的 ETL 模块(再次非常耗时和资源密集)。
-
不可避免地,会有一些错误数据进入数据库。因此,管理员需要制定规则来监控表的引用完整性,以确保损坏最小化,并安排重新摄入任何损坏的数据。
如果我们现在考虑这些问题,并大幅增加数据的数量、速度、多样性和真实性,很容易看出我们简单的 ETL 系统已经迅速发展成一个几乎无法管理的系统。任何格式、模式和业务规则的更改都将产生负面影响。在某些情况下,甚至可能没有足够的处理器和内存资源来跟上,因为需要进行所有的处理步骤。在所有 ETL 步骤达成一致并就位之前,数据无法被摄入。在大型公司中,可能需要数月时间来达成模式转换的一致意见,然后才能开始任何实施,从而导致大量积压,甚至丢失数据。所有这些都导致了一个难以改变的脆弱系统。
模式敏捷性
为了克服这一点,基于读取的模式鼓励我们转向一个非常简单的原则:在运行时对数据应用模式,而不是在加载时应用模式(即,在摄入时)。换句话说,当数据被读取进行处理时,会对数据应用模式。这在某种程度上简化了 ETL 过程:
当然,这并不意味着你完全消除了转换步骤。你只是推迟了验证、应用业务规则、错误处理、确保引用完整性、丰富、聚合和其他膨胀模型的行为,直到你准备使用它的时候。这个想法是,到了这个时候,你应该对数据有更多了解,当然也对你希望使用数据的方式有更多了解。因此,你可以利用对数据的增加了解来提高加载方法的效率。同样,这是一个权衡。你在前期处理成本上节省的部分,可能会在重复处理和潜在的不一致性上损失。然而,持久化、索引、记忆和缓存等技术都可以在这方面提供帮助。正如前一章所述,这个过程通常被称为 ELT,因为处理步骤的顺序发生了逆转。
这种方法的一个好处是,它允许更大的自由度,以便对数据的表示和建模方式做出适当的决策,以满足特定用例的相关特定要求。例如,数据可以以各种方式进行结构化、格式化、存储、压缩或序列化,因此选择最合适的方法是有意义的,考虑到你试图解决的特定问题集。
这种方法提供的最重要的机会之一是你可以选择如何物理布置数据,也就是决定数据存放的目录结构。通常不建议将所有数据存储在单个目录中,因为随着文件数量的增长,底层文件系统需要更长的时间来处理它们。但是,理想情况下,我们希望能够指定最小可能的数据拆分,以满足功能需求并有效地存储和检索所需的数据量。因此,数据应根据所需的分析和预期接收的数据量进行逻辑分组。换句话说,数据可以根据类型、子类型、日期、时间或其他相关属性分成不同的目录,但必须确保没有单个目录承担过重的负担。另一个重要的观点是,一旦数据落地,就可以在以后重新格式化或重新组织,而在 ETL 范式中,这通常更加困难。
此外,ELT 还可以对变更管理和版本控制产生意想不到的好处。例如,如果外部因素导致数据架构发生变化,您可以简单地将不同的数据加载到数据存储的新目录中,并使用灵活的模式容忍序列化库,如 Avro 或 Parquet,它们都支持模式演化(我们将在本章后面讨论这些);或者,如果特定作业的结果不尽人意,我们只需要更改该作业的内部,然后重新运行它。这意味着模式更改变成了可以根据每个分析进行管理,而不是根据每个数据源进行管理,变更的影响得到了更好的隔离和管理。
顺便说一句,值得考虑一种混合方法,特别适用于流式使用情况,即在收集和摄取过程中可以进行一些处理,而在运行时可以进行其他处理。关于使用 ETL 或 ELT 的决定并不一定是二元的。Spark 提供了功能,让您控制数据管道。这为您提供了在合适的时候转换或持久化数据的灵活性,而不是采用一刀切的方法。
确定采取哪种方法的最佳方式是从特定数据集的实际日常使用中学习,并相应地调整其处理,随着经验的积累,识别瓶颈和脆弱性。还可能会有公司规定,如病毒扫描或数据安全,这将决定特定的路线。我们将在本章末尾更深入地讨论这一点。
现实检验
与计算机中的大多数事物一样,没有银弹。ELT 和基于读取的模式不会解决所有数据格式化问题,但它们是工具箱中有用的工具,一般来说,优点通常大于缺点。然而,值得注意的是,如果不小心,有时会引入困难的情况。
特别是在复杂数据模型上执行临时分析可能更加复杂(与数据库相比)。例如,在简单情况下,从新闻文章中提取提到的所有城市的名称列表,在 SQL 数据库中,您可以基本上运行select CITY from GKG
,而在 Spark 中,您首先需要了解数据模型,解析和验证数据,然后创建相关表并在运行时处理任何错误,有时每次运行查询都要这样做。
再次强调,这是一个权衡。使用 schema-on-read,您失去了内置的数据表示和固定模式的固有知识,但您获得了根据需要应用不同模型或视图的灵活性。像往常一样,Spark 提供了旨在帮助利用这种方法的功能,例如转换、DataFrames
、SparkSQL
和 REPL,当正确使用时,它们允许您最大限度地利用 schema-on-read 的好处。随着我们的学习,我们将进一步了解这一点。
GKG ELT
由于我们的 NiFi 管道将数据原样写入 HDFS,我们可以充分利用 schema-on-read,并立即开始使用它,而无需等待它被处理。如果您想要更加先进,那么您可以以可分割和/或压缩的格式(例如bzip2
,Spark 原生支持)加载数据。让我们看一个简单的例子。
注意
HDFS 使用块系统来存储数据。为了以最有效的方式存储和利用数据,HDFS 文件应尽可能可分割。例如,如果使用TextOutputFormat
类加载 CSV GDELT 文件,那么大于块大小的文件将被分割成文件大小/块大小的块。部分块不会占据磁盘上的完整块大小。
通过使用DataFrames
,我们可以编写 SQL 语句来探索数据,或者使用数据集我们可以链接流畅的方法,但在任何情况下都需要一些初始准备。
好消息是,通常这可以完全由 Spark 完成,因为它支持通过 case 类将数据透明地加载到数据集中,使用Encoders,所以大部分时间您不需要过多地担心内部工作。事实上,当您有一个相对简单的数据模型时,通常定义一个 case 类,将数据映射到它,并使用toDS
方法转换为数据集就足够了。然而,在大多数现实世界的场景中,数据模型更复杂,您将需要编写自己的自定义解析器。自定义解析器在数据工程中并不新鲜,但在 schema-on-read 设置中,它们通常需要被数据科学家使用,因为数据的解释是在运行时而不是加载时完成的。以下是我们存储库中可找到的自定义 GKG 解析器的使用示例:
import org.apache.spark.sql.functions._
val rdd = rawDS map GdeltParser.toCaseClass
val ds = rdd.toDS()
// DataFrame-style API
ds.agg(avg("goldstein")).as("goldstein").show()
// Dataset-style API
ds.groupBy(_.eventCode).count().show()
您可以看到,一旦数据被解析,它可以在各种 Spark API 中使用。
如果您更喜欢使用 SQL,您可以定义自己的模式,注册一个表,并使用 SparkSQL。在任何一种方法中,您都可以根据数据的使用方式选择如何加载数据,从而更灵活地决定您花费时间解析的方面。例如,加载 GKG 的最基本模式是将每个字段都视为字符串,就像这样:
import org.apache.spark.sql.types._
val schema = StructType(Array(
StructField("GkgRecordId" , StringType, true),
StructField("V21Date" , StringType, true),
StructField("V2SrcCollectionId" , StringType, true),
StructField("V2SrcCmnName" , StringType, true),
StructField("V2DocId" , StringType, true),
StructField("V1Counts" , StringType, true),
StructField("V21Counts" , StringType, true),
StructField("V1Themes" , StringType, true),
StructField("V2Themes" , StringType, true),
StructField("V1Locations" , StringType, true),
StructField("V2Locations" , StringType, true),
StructField("V1Persons" , StringType, true),
StructField("V2Persons" , StringType, true),
StructField("V1Orgs" , StringType, true),
StructField("V2Orgs" , StringType, true),
StructField("V15Tone" , StringType, true),
StructField("V21Dates" , StringType, true),
StructField("V2GCAM" , StringType, true),
StructField("V21ShareImg" , StringType, true),
StructField("V21RelImg" , StringType, true),
StructField("V21SocImage" , StringType, true),
StructField("V21SocVideo" , StringType, true),
StructField("V21Quotations" , StringType, true),
StructField("V21AllNames" , StringType, true),
StructField("V21Amounts" , StringType, true),
StructField("V21TransInfo" , StringType, true),
StructField("V2ExtrasXML" , StringType, true)
))
val filename="path_to_your_gkg_files"
val df = spark
.read
.option("header", "false")
.schema(schema)
.option("delimiter", "t")
.csv(filename)
df.createOrReplaceTempView("GKG")
现在你可以执行 SQL 查询,就像这样:
spark.sql("SELECT V2GCAM FROM GKG LIMIT 5").show
spark.sql("SELECT AVG(GOLDSTEIN) AS GOLDSTEIN FROM GKG WHERE GOLDSTEIN IS NOT NULL").show()
通过这种方法,您可以立即开始对数据进行概要分析,这对许多数据工程任务都是有用的。当您准备好时,您可以选择 GKG 记录的其他元素进行扩展。我们将在下一章中更多地了解这一点。
一旦你有了一个 DataFrame,你可以通过定义一个 case 类和转换来将其转换为一个 Dataset,就像这样:
val ds = df.as[GdeltEntity]
位置很重要
值得注意的是,当从 CSV 加载数据时,Spark 的模式匹配完全是位置的。这意味着,当 Spark 根据给定的分隔符对记录进行标记时,它将根据其位置将每个标记分配给模式中的一个字段,即使存在标题。因此,如果在模式定义中省略了一个列,或者由于数据漂移或数据版本化而导致数据集随时间变化,您可能会遇到 Spark 不一定会警告您的错位!
因此,我们建议定期进行基本数据概要和数据质量检查,以减轻这些情况。您可以使用DataFrameStatFunctions
中的内置函数来协助处理这些情况。一些示例如下所示:
df.describe("V1Themes").show
df.stat.freqItems(Array("V2Persons")).show
df.stat.crosstab("V2Persons","V2Locations").show
接下来,让我们解释一种很好的方法来给我们的代码加上一些结构,并通过使用 Avro 或 Parquet 来减少编写的代码量。
Avro
我们已经看到了如何轻松地摄取一些数据并使用 Spark 进行分析,而无需任何传统的 ETL 工具。在一个几乎忽略所有模式的环境中工作非常有用,但这在商业世界中并不现实。然而,有一个很好的折中方案,它比 ETL 和无限数据处理都有很大的优势-Avro。
Apache Avro 是一种序列化技术,类似于 Google 的协议缓冲。与许多其他序列化技术一样,Avro 使用模式来描述数据,但其有用性的关键在于它提供了以下功能:
-
它将模式与数据一起存储。这样可以有效地存储,因为模式只存储一次,位于文件顶部。这也意味着即使原始类文件不再可用,也可以读取数据。
-
它支持读取时模式和模式演变。这意味着它可以实现不同的模式来读取和写入数据,提供了模式版本控制的优势,而不会带来大量的行政开销,每次我们希望进行数据修改时。
-
它是与语言无关的。因此,它可以与允许自定义序列化框架的任何工具或技术一起使用。例如,直接写入 Hive 时特别有用。
Avro 将模式与封闭数据一起存储,它是自描述的。因此,我们可以简单地查询 Avro 文件,以获取写入数据的模式,而不是因为没有类而难以读取数据,或者尝试猜测哪个版本的模式适用,或者在最坏的情况下不得不放弃数据。
Avro 还允许以添加更改或附加的形式对模式进行修订,从而可以容纳这些更改,使特定实现向后兼容旧数据。
由于 Avro 以二进制形式表示数据,因此可以更有效地传输和操作。此外,由于其固有的压缩,它在磁盘上占用的空间更少。
基于上述原因,Avro 是一种非常流行的序列化格式,被广泛用于各种技术和终端系统,您无疑会在某个时候使用它。因此,在接下来的几节中,我们将演示读取和写入 Avro 格式数据的两种不同方法。第一种是一种优雅而简单的方法,使用一个名为spark-avro
的第三方专门构建的库,第二种是一种底层方法,有助于理解 Avro 的工作原理。
Spark-Avro 方法
为了解决实现 Avro 的复杂性,开发了spark-avro
库。这可以像往常一样使用 maven 导入:
<dependency>
<groupId>com.databricks</groupId>
<artifactId>spark-avro_2.11</artifactId>
<version>3.1.0</version>
</dependency>
对于这个实现,我们将使用StructType
对象创建 Avro 模式,使用RDD
转换输入数据,并从中创建一个DataFrame
。最后,可以使用spark-avro
库将结果以 Avro 格式写入文件。
StructType
对象是上面使用的GkgCoreSchema
的变体,在第四章中也是如此,探索性数据分析,构造如下:
val GkgSchema = StructType(Array(
StructField("GkgRecordId", GkgRecordIdStruct, true),
StructField("V21Date", LongType, true),
StructField("V2SrcCollectionId", StringType, true),
StructField("V2SrcCmnName", StringType, true),
StructField("V2DocId", StringType, true),
StructField("V1Counts", ArrayType(V1CountStruct), true),
StructField("V21Counts", ArrayType(V21CountStruct), true),
StructField("V1Themes", ArrayType(StringType), true),
StructField("V2EnhancedThemes",ArrayType(EnhancedThemes),true),
StructField("V1Locations", ArrayType(V1LocationStruct), true),
StructField("V2Locations", ArrayType(EnhancedLocations), true),
StructField("V1Persons", ArrayType(StringType), true),
StructField("V2Persons", ArrayType(EnhancedPersonStruct), true),
StructField("V1Orgs", ArrayType(StringType), true),
StructField("V2Orgs", ArrayType(EnhancedOrgStruct), true),
StructField("V1Stone", V1StoneStruct, true),
StructField("V21Dates", ArrayType(V21EnhancedDateStruct), true),
StructField("V2GCAM", ArrayType(V2GcamStruct), true),
StructField("V21ShareImg", StringType, true),
StructField("V21RelImg", ArrayType(StringType), true),
StructField("V21SocImage", ArrayType(StringType), true),
StructField("V21SocVideo", ArrayType(StringType), true),
StructField("V21Quotations", ArrayType(QuotationStruct), true),
StructField("V21AllNames", ArrayType(V21NameStruct), true),
StructField("V21Amounts", ArrayType(V21AmountStruct), true),
StructField("V21TransInfo", V21TranslationInfoStruct, true),
StructField("V2ExtrasXML", StringType, true)
))
我们已经使用了许多自定义StructTypes
,可以为GkgSchema
内联指定,但为了便于阅读,我们已经将它们拆分出来。
例如,GkgRecordIdStruct
是:
val GkgRecordIdStruct = StructType(Array(
StructField("Date", LongType),
StructField("TransLingual", BooleanType),
StructField("NumberInBatch";, IntegerType)
))
在使用此模式之前,我们必须首先通过解析输入的 GDELT 数据生成一个RDD
:
val gdeltRDD = sparkContext.textFile("20160101020000.gkg.csv")
val gdeltRowOfRowsRDD = gdeltRDD.map(_.split("\t"))
.map(attributes =>
Row(
createGkgRecordID(attributes(0)),
attributes(1).toLong,
createSourceCollectionIdentifier(attributes(2),
attributes(3),
attributes(4),
createV1Counts(attributes(5),
createV21Counts(attributes(6),
.
.
.
)
))
在这里,您可以看到许多自定义解析函数,例如createGkgRecordID
,它接受原始数据并包含读取和解释每个字段的逻辑。由于 GKG 字段复杂且通常包含嵌套数据结构,我们需要一种将它们嵌入Row
中的方法。为了帮助我们,Spark 允许我们将它们视为Rows
内部的Rows
。因此,我们只需编写返回Row
对象的解析函数,如下所示:
def createGkgRecordID(str: String): Row = {
if (str != "") {
val split = str.split("-")
if (split(1).length > 1) {
Row(split(0).toLong, true, split(1).substring(1).toInt)
}
else {
Row(split(0).toLong, false, split(1).toInt)
}
}
else {
Row(0L, false, 0)
}
}
将代码放在一起,我们可以在几行代码中看到整个解决方案:
import org.apache.spark.sql.types._
import com.databricks.spark.avro._
import org.apache.spark.sql.Row
val df = spark.createDataFrame(gdeltRowOfRowsRDD, GkgSchema)
df.write.avro("/path/to/avro/output")
将 Avro 文件读入DataFrame
同样简单:
val avroDF = spark
.read
.format("com.databricks.spark.avro")
.load("/path/to/avro/output")
这为处理 Avro 文件提供了一个简洁的解决方案,但在幕后发生了什么呢?
教学方法
为了解释 Avro 的工作原理,让我们来看一个自定义解决方案。在这种情况下,我们需要做的第一件事是为我们打算摄取的数据版本或版本创建 Avro 模式。
有几种语言的 Avro 实现,包括 Java。这些实现允许您为 Avro 生成绑定,以便您可以高效地序列化和反序列化数据对象。我们将使用一个 maven 插件来帮助我们使用 GKG 模式的 Avro IDL 表示自动编译这些绑定。这些绑定将以 Java 类的形式存在,我们以后可以使用它们来帮助我们构建 Avro 对象。在您的项目中使用以下导入:
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.7.7</version>
</dependency>
<plugin>
<groupId>org.apache.avro</groupId>
<artifactId>avro-maven-plugin</artifactId>
<version>1.7.7</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>schema</goal>
</goals>
<configuration>
<sourceDirectory>
${project.basedir}/src/main/avro/
</sourceDirectory>
<outputDirectory>
${project.basedir}/src/main/java/
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
现在我们可以看一下我们从可用 Avro 类型的子集创建的 Avro IDL 模式:
+----------------+-------------+
| primitive| complex|
+----------------+-------------+
|null | record|
|Boolean | enum|
|int | array|
|long | map|
|float | union|
|double | fixed|
|bytes | |
|string | |
+----------------+-------------+
Avro provides an extensible type system that supports **custom types**. It's also modular and offers namespaces, so that we can add new types and reuse custom types as the schema evolves. In the preceding example, we can see primitive types extensively used, but also custom objects such as `org.io.gzet.gdelt.gkg.v1.Location`.To create Avro files, we can use the following code (full example in our code repository):
val inputFile = new File(“gkg.csv”);
val outputFile = new File(“gkg.avro”);
val userDatumWriter = new
SpecificDatumWriter[Specification](classOf[Specification])
val dataFileWriter = new
DataFileWriter[Specification](userDatumWriter)
dataFileWriter.create(Specification.getClassSchema,outputFile)
对于(line <- Source.fromFile(inputFile).getLines())
dataFileWriter.append(generateAvro(line))
dataFileWriter.close()
def generateAvro(line:String):Specification = {
val values = line.split(“\t”,-1)
如果 values.length == 27){
val specification = Specification.newBuilder()
.setGkgRecordId(createGkgRecordId(values{0}))
.setV21Date(values{1}.toLong)
.setV2SourceCollectionIdentifier(
createSourceCollectionIdentifier(values{2}))
.setV21SourceCommonName(values{3})
.setV2DocumentIdentifier(values{4})
.setV1Counts(createV1CountArray(values{5}))
.setV21Counts(createV21CountArray(values{6}))
.setV1Themes(createV1Themes(values{7}))
创建 V2EnhancedThemes(values{8})
.setV1Locations(createV1LocationsArray(values{9}))
.
。
.
}
}
The `Specification` object is created for us once we compile our IDL (using the maven plugin). It contains all of the methods required to access the Avro model, for example `setV2EnhancedLocations`. We are then left with creating the functions to parse our GKG data; two examples are shown, as follows:
def createSourceCollectionIdentifier(str:String):SourceCollectionIdentifier = {
str.toInt match {
情况 1 => SourceCollectionIdentifier.WEB
情况 2 => SourceCollectionIdentifier.CITATIONONLY
情况 3 => SourceCollectionIdentifier.CORE
情况 4 => SourceCollectionIdentifier.DTIC
情况 5 => SourceCollectionIdentifier.JSTOR
情况 6 => SourceCollectionIdentifier.NONTEXTUALSOURCE
情况 _ => SourceCollectionIdentifier.WEB
}
}
def createV1LocationsArray(str:String):Array[Location] = {
val counts = str.split(“;”)
计数映射(createV1Location(_))
}
This approach creates the required Avro files, but it is shown here to demonstrate how Avro works. As it stands, this code does not operate in parallel and, therefore, should not be used on big data. If we wanted to parallelize it, we could create a custom `InputFormat`, wrap the raw data into an RDD, and perform the processing on that basis. Fortunately, we don't have to, as `spark-avro` has already done it for us.
何时执行 Avro 转换
为了最好地利用 Avro,接下来,我们需要决定何时最好转换数据。转换为 Avro 是一个相对昂贵的操作,因此应在最有意义的时候进行。再次,这是一个权衡。这一次,它是在灵活的数据模型支持非结构化处理、探索性数据分析、临时查询和结构化类型系统之间。有两个主要选项要考虑:
-
尽可能晚地转换:可以在每次作业运行时执行 Avro 转换。这里有一些明显的缺点,所以最好考虑在某个时候持久化 Avro 文件,以避免重新计算。您可以在第一次懒惰地执行此操作,但很快就会变得混乱。更容易的选择是定期对静态数据运行批处理作业。该作业的唯一任务是创建 Avro 数据并将其写回磁盘。这种方法使我们完全控制转换作业的执行时间。在繁忙的环境中,可以安排作业在安静的时期运行,并且可以根据需要分配优先级。缺点是我们需要知道处理需要多长时间,以确保有足够的时间完成。如果处理在下一批数据到达之前没有完成,那么就会积压,并且很难赶上。
-
尽早转换:另一种方法是创建一个摄取管道,其中传入的数据在飞行中转换为 Avro(在流式场景中特别有用)。通过这样做,我们有可能接近 ETL 式的场景,因此真的是一个判断,哪种方法最适合当前使用的特定环境。
现在,让我们看一下在 Spark 中广泛使用的相关技术,即 Apache Parquet。
Parquet
Apache Parquet 是专为 Hadoop 生态系统设计的列式存储格式。传统的基于行的存储格式被优化为一次处理一条记录,这意味着它们对于某些类型的工作负载可能会很慢。相反,Parquet 通过列序列化和存储数据,从而允许对存储、压缩、谓词处理和大型数据集的批量顺序访问进行优化-这正是适合 Spark 的工作负载类型!
由于 Parquet 实现了按列数据压缩,因此特别适用于 CSV 数据,特别是低基数字段,与 Avro 相比,文件大小可以大大减小。
+--------------------------+--------------+
| File Type| Size|
+--------------------------+--------------+
|20160101020000.gkg.csv | 20326266|
|20160101020000.gkg.avro | 13557119|
|20160101020000.gkg.parquet| 6567110|
|20160101020000.gkg.csv.bz2| 4028862|
+--------------------------+--------------+
Parquet 还与 Avro 原生集成。Parquet 采用 Avro 内存表示的数据,并映射到其内部数据类型。然后,它使用 Parquet 列式文件格式将数据序列化到磁盘上。
我们已经看到如何将 Avro 应用于模型,现在我们可以迈出下一步,使用这个 Avro 模型通过 Parquet 格式将数据持久化到磁盘上。再次,我们将展示当前的方法,然后为了演示目的,展示一些更低级别的代码。首先是推荐的方法:
val gdeltAvroDF = spark
.read
.format("com.databricks.spark.avro")
.load("/path/to/avro/output")
gdeltAvroDF.write.parquet("/path/to/parquet/output")
现在让我们来详细了解 Avro 和 Parquet 之间的关系:
val inputFile = new File("("/path/to/avro/output ")
val outputFile = new Path("/path/to/parquet/output")
val schema = Specification.getClassSchema
val reader = new GenericDatumReaderIndexedRecord
val avroFileReader = DataFileReader.openReader(inputFile, reader)
val parquetWriter =
new AvroParquetWriterIndexedRecord
while(avroFileReader.hasNext) {
parquetWriter.write(dataFileReader.next())
}
dataFileReader.close()
parquetWriter.close()
与之前一样,低级别的代码相当冗长,尽管它确实提供了对所需各种步骤的一些见解。您可以在我们的存储库中找到完整的代码。
现在我们有一个很好的模型来存储和检索我们的 GKG 数据,它使用 Avro 和 Parquet,并且可以很容易地使用DataFrames
来实现。
总结
在本章中,我们已经看到为什么在进行太多的探索工作之前,数据集应该被彻底理解。我们已经讨论了结构化数据和维度建模的细节,特别是关于这如何适用于 GDELT 数据集,并扩展了 GKG 模型以展示其潜在复杂性。
我们已经解释了传统 ETL 和新的基于模式读取 ELT 技术之间的区别,并且已经触及了数据工程师在数据存储、压缩和数据格式方面面临的一些问题,特别是 Avro 和 Parquet 的优势和实现。我们还演示了使用各种 Spark API 来探索数据的几种方法,包括如何在 Spark shell 上使用 SQL 的示例。
我们可以通过提到我们的存储库中的代码将所有内容汇总,并且是一个用于读取原始 GKG 文件的完整模型(如果需要一些数据,请使用 Apache NiFi GDELT 数据摄取管道来自第一章,数据获取)。
在下一章中,我们将深入探讨 GKG 模型,探索用于大规模探索和分析数据的技术。我们将看到如何使用 SQL 开发和丰富我们的 GKG 数据模型,并调查 Apache Zeppelin 笔记本如何提供更丰富的数据科学体验。
第四章:探索性数据分析
在商业环境中进行的探索性数据分析(EDA)通常作为更大的工作的一部分委托,这个工作是按照可行性评估的线索组织和执行的。这种可行性评估的目的,因此也是我们可以称之为扩展 EDA的焦点,是回答关于所检查的数据是否合适并且值得进一步投资的一系列广泛问题。
在这个一般性的任务下,数据调查预计将涵盖可行性的几个方面,包括在生产中使用数据的实际方面,如及时性、质量、复杂性和覆盖范围,以及适合于测试的预期假设。虽然其中一些方面从数据科学的角度来看可能不那么有趣,但这些以数据质量为主导的调查与纯粹的统计洞察力一样重要。特别是当涉及的数据集非常庞大和复杂,以及为数据科学准备数据所需的投资可能是巨大的时候。为了阐明这一点,并使主题更加生动,我们提出了对全球事件、语言和语调全球数据库(GDELT)项目提供的大规模和复杂的全球知识图(GKG)数据源进行探索的方法。
在本章中,我们将创建和解释一个 EDA,同时涵盖以下主题:
-
理解规划和构建扩展探索性数据分析的问题和设计目标
-
数据概要分析是什么,举例说明,并且如何围绕连续数据质量监控的技术形成一个通用框架
-
如何构建一个围绕该方法的通用基于掩码的数据概要分析器
-
如何将探索性指标存储到标准模式中,以便随时间研究指标的数据漂移,附有示例
-
如何使用 Apache Zeppelin 笔记本进行快速探索性数据分析工作,以及绘制图表和图形
-
如何提取和研究 GDELT 中的 GCAM 情感,包括时间序列和时空数据集
-
如何扩展 Apache Zeppelin 以使用
plot.ly
库生成自定义图表
问题、原则和规划
在本节中,我们将探讨为什么可能需要进行探索性数据分析,并讨论创建探索性数据分析时的重要考虑因素。
理解探索性数据分析问题
在进行 EDA 项目之前的一个困难问题是:你能给我一个关于你提议的 EDA 成本的估算和分解吗?
我们如何回答这个问题最终塑造了我们的探索性数据分析策略和战术。过去,对这个问题的回答通常是这样开始的:*基本上你按列付费……*这个经验法则是基于这样的前提:数据探索工作的可迭代单元,这些工作单元驱动了工作量的估算,从而决定了进行探索性数据分析的大致价格。
这个想法有趣的地方在于,工作单元的报价是以需要调查的数据结构而不是需要编写的函数来报价的。其原因很简单。假定数据处理管道的函数已经存在,而不是新的工作,因此所提供的报价实际上是配置新输入数据结构到我们标准的数据处理管道以探索数据的隐含成本。
这种思维方式使我们面临的主要探索性数据分析问题是,探索在规划任务和估算时间方面似乎很难确定。建议的方法是将探索视为配置驱动的任务。这有助于我们更有效地组织和估算工作,同时有助于塑造围绕配置的思维,而不是编写大量临时代码。
配置数据探索的过程也促使我们考虑可能需要的处理模板。我们需要根据我们探索的数据形式进行配置。例如,我们需要为结构化数据、文本数据、图形数据、图像数据、声音数据、时间序列数据和空间数据配置标准的探索流程。一旦我们有了这些模板,我们只需要将输入数据映射到它们,并配置摄入过滤器,以便对数据进行聚焦。
设计原则
为基于 Apache Spark 的 EDA 处理现代化这些想法意味着我们需要设计具有一些通用原则的可配置 EDA 函数和代码:
-
易重用的函数/特性*:我们需要定义我们的函数以一般方式处理一般数据结构,以便它们产生良好的探索特性,并以最小的配置工作量将它们交付给新数据集
-
最小化中间数据结构*:我们需要避免大量中间模式,帮助最小化中间配置,并在可能的情况下创建可重复使用的数据结构
-
数据驱动配置*:在可能的情况下,我们需要生成可以从元数据中生成的配置,以减少手动样板工作
-
模板化可视化*:从常见输入模式和元数据驱动的通用可重复使用的可视化
最后,虽然这不是一个严格的原则,但我们需要构建灵活的探索工具,以便发现数据结构,而不是依赖于严格预定义的配置。当出现问题时,这有助于我们通过帮助我们反向工程文件内容、编码或文件定义中的潜在错误来解决问题。
探索的一般计划
所有 EDA 工作的早期阶段都不可避免地基于建立数据质量的简单目标。如果我们在这里集中精力,创建一个广泛适用的通用入门计划,那么我们可以制定一般的任务集。
这些任务构成了拟议的 EDA 项目计划的一般形状,如下所示:
-
准备源工具,获取我们的输入数据集,审查文档等。必要时审查数据的安全性。
-
获取、解密并在 HDFS 中分阶段存储数据;收集用于规划的非功能性需求(NFRs)。
-
在文件内容上运行代码点级别的频率报告。
-
在文件字段中运行缺失数据量的人口统计检查。
-
运行低粒度格式分析器,检查文件中基数较高的字段。
-
在文件中对受格式控制字段运行高粒度格式分析器检查。
-
运行参照完整性检查,必要时。
-
运行字典检查,验证外部维度。
-
对数值数据进行基本的数字和统计探索。
-
对感兴趣的关键数据进行更多基于可视化的探索。
注意
在字符编码术语中,代码点或代码位置是构成代码空间的任何数字值。许多代码点代表单个字符,但它们也可以具有其他含义,例如用于格式化。
准备工作
现在我们有了一个行动的一般计划,在探索数据之前,我们必须首先投资于构建可重复使用的工具,用于进行探索流程的早期单调部分,帮助我们验证数据;然后作为第二步调查 GDELT 的内容。
引入基于掩码的数据分析
快速探索新类型数据的一种简单而有效的方法是利用基于掩码的数据分析。在这种情况下,掩码是将数据项泛化为特征的字符串转换函数,作为掩码集合,其基数将低于研究领域中原始值的基数。
当数据列被总结为掩码频率计数时,通常称为数据概要,它可以快速洞察字符串的常见结构和内容,从而揭示原始数据的编码方式。考虑以下用于探索数据的掩码:
-
将大写字母翻译为A
-
将小写字母翻译为a
-
将数字 0 到 9 翻译为9
乍一看,这似乎是一个非常简单的转换。例如,让我们将此掩码应用于数据的高基数字段,例如 GDELT GKG 文件的V2.1 Source Common Name字段。文档建议它记录了正在研究的新闻文章的来源的常见名称,通常是新闻文章被抓取的网站的名称,我们期望它包含域名,例如nytimes.com。
在 Spark 中实施生产解决方案之前,让我们在 Unix 命令行上原型化一个概要工具,以提供一个我们可以在任何地方运行的示例:
$ cat 20150218230000.gkg.csv | gawk -F"\t" '{print $4}' | \
sed "s/[0-9]/9/g; s/[a-z]/a/g; s/[A-Z]/A/g" | sort | \
uniq -c | sort -r -n | head -20
232 aaaa.aaa
195 aaaaaaaaaa.aaa
186 aaaaaa.aaa
182 aaaaaaaa.aaa
168 aaaaaaa.aaa
167 aaaaaaaaaaaa.aaa
167 aaaaa.aaa
153 aaaaaaaaaaaaa.aaa
147 aaaaaaaaaaa.aaa
120 aaaaaaaaaaaaaa.aaa
输出是在 Source Common Name 列中找到的记录的排序计数,以及正则表达式(regex)生成的掩码。通过查看这个概要数据的结果,应该很清楚该字段包含域名-或者是吗?因为我们只看了最常见的掩码(在这种情况下是前 20 个),也许在排序列表的另一端的长尾部分可能存在潜在的数据质量问题。
我们可以引入一个微妙的改变来提高我们的掩码函数的泛化能力,而不是只看前 20 个掩码,甚至是后 20 个。通过使正则表达式将小写字母的多个相邻出现折叠成一个a
字符,掩码的基数可以减少,而不会真正减少我们解释结果的能力。我们可以通过对我们的正则表达式进行微小的改进来原型化这个改进,并希望在一个输出页面上查看所有的掩码:
$ # note: on a mac use gsed, on linux use sed.
$ hdfs dfs -cat 20150218230000.gkg.csv | \
gawk -F"\t" '{print $4}' | sed "s/[0-9]/9/g; s/[A-Z]/A/g; \
s/[a-z]/a/g; s/a*a/a/g"| sort | uniq -c | sort -r -n
2356 a.a
508 a.a.a
83 a-a.a
58 a99.a
36 a999.a
24 a-9.a
21 99a.a
21 9-a.a
15 a9.a
15 999a.a
12 a9a.a
11 a99a.a
8 a-a.a.a
7 9a.a
3 a-a-a.a
2 AAA Aa <---note here the pattern that stands out
2 9a99a.a
2 9a.a.a
1 a9.a.a
1 a.99a.a
1 9a9a.a
1 9999a.a
非常快地,我们原型化了一个掩码,将三千多个原始值缩减为一个非常短的列表,可以轻松地通过眼睛检查。由于长尾现在变得更短了,我们可以很容易地发现这个数据字段中可能的异常值,这些异常值可能代表质量问题或特殊情况。尽管是手动的,但这种类型的检查可能非常有力。
请注意,例如输出中有一个特定的掩码,AAA Aa
,其中没有点,这与我们在域名中所期望的不符。我们解释这一发现意味着我们发现了两行不是有效域名的原始数据,而可能是一般描述符。也许这是一个错误,或者是所谓的不合逻辑的字段使用的例子,这意味着可能有其他值滑入了这一列,也许应该逻辑上属于其他地方。
这值得调查,而且很容易检查这两条记录。我们可以通过在原始数据旁边生成掩码,然后过滤掉有问题的掩码来定位原始字符串,以进行手动检查。
我们可以使用一个名为bytefreq
(字节频率的缩写)的传统数据概要工具来检查这些记录,而不是在命令行上编写一个非常长的一行代码。它有开关来生成格式化报告、数据库准备的指标,还有一个开关来输出掩码和数据并排。我们已经为本书的读者开源了bytefreq
,建议您尝试一下,以真正理解这种技术有多有用:bitbucket.org/bytesumo/bytefreq
。
$ # here is a Low Granularity report from bytefreq
$ hdfs dfs –cat 20150218230000.gkg.csv | \
gawk -F"\t" '{print $4}' | awk -F"," –f \ ~/bytefreq/bytefreq_v1.04.awk -v header="0" -v report="0" \
-v grain="L"
- ##column_100000001 2356 a.a sfgate.com
- ##column_100000001 508 a.a.a theaustralian.com.au
- ##column_100000001 109 a9.a france24.com
- ##column_100000001 83 a-a.a news-gazette.com
- ##column_100000001 44 9a.a 927thevan.com
- ##column_100000001 24 a-9.a abc-7.com
- ##column_100000001 23 a9a.a abc10up.com
- ##column_100000001 21 9-a.a 4-traders.com
- ##column_100000001 8 a-a.a.a gazette-news.co.uk
- ##column_100000001 3 9a9a.a 8points9seconds.com
- ##column_100000001 3 a-a-a.a the-american-interest.com
- ##column_100000001 2 9a.a.a 9news.com.au
- ##column_100000001 2 A Aa BBC Monitoring
- ##column_100000001 1 a.9a.a vancouver.24hrs.ca
- ##column_100000001 1 a9.a.a guide2.co.nz
$ hdfs dfs -cat 20150218230000.gkg.csv | gawk \
-F"\t" '{print $4}'|gawk -F"," -f ~/bytefreq/bytefreq_v1.04.awk\
-v header="0" -v report="2" -v grain="L" | grep ",A Aa"
BBC Monitoring,A Aa
BBC Monitoring,A Aa
当我们检查奇怪的掩码A Aa
时,我们可以看到找到的有问题的文本是BBC Monitoring
,在重新阅读 GDELT 文档时,我们会发现这不是一个错误,而是一个已知的特殊情况。这意味着在使用这个字段时,我们必须记住处理这个特殊情况。处理它的一种方法可能是包括一个更正规则,将这个字符串值替换为一个更好的值,例如有效的域名www.monitor.bbc.co.uk,这是文本字符串所指的数据源。
我们在这里介绍的想法是,掩码可以用作检索特定字段中有问题记录的关键。这种逻辑引导我们到基于掩码的分析的下一个主要好处:输出的掩码是一种数据质量错误代码。这些错误代码可以分为两类:好掩码的白名单,和用于查找低质量数据的坏掩码的黑名单。这样考虑,掩码就成为搜索和检索数据清理方法的基础,或者用于发出警报或拒绝记录。
这个教训是,我们可以创建处理函数来纠正使用特定掩码计算在特定字段数据上找到的原始字符串。这种思路导致了以下结论:我们可以创建一个围绕基于掩码的分析框架,用于在数据读取管道中进行数据质量控制和纠正。这具有一些非常有利的解决方案特性:
-
生成数据质量掩码是一个读取过程;我们可以接受新的原始数据并将其写入磁盘,然后在读取时,我们只在查询时需要时生成掩码 - 因此数据清理可以是一个动态过程。
-
处理函数可以动态应用于目标纠正工作,帮助在读取数据时清理我们的数据。
-
因为以前未见过的字符串被概括为掩码,即使从未见过这个确切的字符串,新字符串也可以被标记为存在质量问题。这种普遍性帮助我们减少复杂性,简化我们的流程,并创建可重用的智能解决方案 - 即使跨学科领域也是如此。
-
创建掩码的数据项如果不属于掩码白名单、修复列表或黑名单,可能会被隔离以供关注;人类分析师可以检查记录,并希望将它们列入白名单,或者创建新的处理函数,帮助将数据从隔离状态中取出并重新投入生产。
-
数据隔离可以简单地作为一个读取过滤器实施,当新的纠正函数被创建来清理或修复数据时,读取时自动应用的动态处理将自动将更正后的数据释放给用户,而不会有长时间的延迟。
-
最终将创建一个随时间稳定的数据质量处理库。新的工作主要是通过将现有处理映射并应用到新数据上来完成的。例如,电话号码重新格式化处理函数可以在许多数据集和项目中广泛重复使用。
现在解释了方法和架构的好处,构建一个通用的基于掩码的分析器的要求应该更清晰了。请注意,掩码生成过程是一个经典的 Hadoop MapReduce 过程:将输入数据映射到掩码,然后将这些掩码减少到总结的频率计数。还要注意的是,即使在这个简短的例子中,我们已经使用了两种类型的掩码,每种掩码都由一系列基础转换组成。这表明我们需要一个支持预定义掩码库的工具,同时也允许用户定义的掩码可以快速创建和按需使用。它还表明应该有方法来堆叠这些掩码,将它们组合成复杂的管道。
也许还不那么明显的是,以这种方式进行的所有数据概要都可以将概要度量写入一个通用输出格式。这有助于通过简化概要数据的记录、存储、检索和使用来提高我们代码的可重用性。
例如,我们应该能够使用以下模式报告所有基于掩码的概要度量:
Metric Descriptor
Source Studied
IngestTime
MaskType
FieldName
Occurrence Count
KeyCount
MaskCount
Description
一旦我们的度量被捕获在这个单一的模式格式中,我们就可以使用用户界面(如 Zeppelin 笔记本)构建辅助报告。
在我们逐步实现这些函数之前,需要介绍一下字符类掩码,因为这些与普通的概要掩码略有不同。
引入字符类掩码
还有一种简单的数据概要类型,我们也可以应用它来帮助文件检查。它涉及对构成整个文件的实际字节进行概要。这是一种古老的方法,最初来自密码学,其中对文本中字母的频率进行分析用于在解密替换代码时获得优势。
虽然在今天的数据科学圈中并不常见,但在需要时,字节级分析是令人惊讶地有用。过去,数据编码是一个巨大的问题。文件以一系列代码页编码,跨 ASCII 和 EBCDIC 标准。字节频率报告通常是发现实际编码、分隔符和文件中使用的行结束的关键。那时,能够创建文件但在技术上无法描述它们的人数是令人惊讶的。如今,随着世界越来越多地转向基于 Unicode 的字符编码,这些古老的方法需要更新。在 Unicode 中,字节的概念被现代化为多字节代码点,可以使用以下函数在 Scala 中揭示。
val tst = "Andrew "
def toCodePointVector(input: String) = input.map{
case (i) if i > 65535 =>
val hchar = (i - 0x10000) / 0x400 + 0xD800
val lchar = (i - 0x10000) % 0x400 + 0xDC00
f"\\u$hchar%04x\\u$lchar%04x"
case (i) if i > 0 => f"\\u$i%04x"
// kudos to Ben Reich: http://k.bytefreq.com/1MjyvNz
}
val out = toCodePointVector(tst)
val rows = sc.parallelize(out)
rows.countByValue().foreach(println)
// results in the following: [codepoint], [Frequency_count]
(\u0065,1)
(\u03d6,1)
(\u006e,1)
(\u0072,1)
(\u0077,1)
(\u0041,1)
(\u0020,2)
(\u6f22,1)
(\u0064,1)
(\u5b57,1)
利用这个函数,我们可以开始对我们在 GDELT 数据集中收到的任何国际字符级数据进行分析,并开始了解我们在利用数据时可能面临的复杂性。但是,与其他掩码不同,为了从代码点创建可解释的结果,我们需要一个字典,我们可以用它来查找有意义的上下文信息,比如 Unicode 类别和 Unicode 字符名称。
为了生成一个上下文查找,我们可以使用这个快速的命令行技巧从主要的unicode.org找到的字典中生成一个缩小的字典,这应该有助于我们更好地报告我们的发现:
$ wget ftp://ftp.unicode.org/Public/UNIDATA/UnicodeData.txt
$ cat UnicodeData.txt | gawk -F";" '{OFS=";"} {print $1,$3,$2}' \
| sed 's/-/ /g'| gawk '{print $1,$2}'| gawk -F";" '{OFS="\t"} \
length($1) < 5 {print $1,$2,$3}' > codepoints.txt
# use "hdfs dfs -put" to load codepoints.txt to hdfs, so
# you can use it later
head -1300 codepoints.txt | tail -4
0513 Ll CYRILLIC SMALL
0514 Lu CYRILLIC CAPITAL
0515 Ll CYRILLIC SMALL
0516 Lu CYRILLIC CAPITAL
我们将使用这个字典,与我们发现的代码点结合起来,报告文件中每个字节的字符类频率。虽然这似乎是一种简单的分析形式,但结果往往会令人惊讶,并提供对我们处理的数据、其来源以及我们可以成功应用的算法和方法类型的法医级别的理解。我们还将查找一般的 Unicode 类别,以简化我们的报告,使用以下查找表:
Cc Other, Control
Cf Other, Format
Cn Other, Not Assigned
Co Other, Private Use
Cs Other, Surrogate
LC Letter, Cased
Ll Letter, Lowercase
Lm Letter, Modifier
Lo Letter, Other
Lt Letter, Titlecase
Lu Letter, Uppercase
Mc Mark, Spacing Combining
Me Mark, Enclosing
Mn Mark, Nonspacing
Nd Number, Decimal Digit
Nl Number, Letter
No Number, Other
Pc Punctuation, Connector
Pd Punctuation, Dash
Pe Punctuation, Close
Pf Punctuation, Final quote
Pi Punctuation, Initial quote
Po Punctuation, Other
Ps Punctuation, Open
Sc Symbol, Currency
Sk Symbol, Modifier
Sm Symbol, Math
So Symbol, Other
Zl Separator, Line
Zp Separator, Paragraph
Zs Separator, Space
构建基于掩码的概要度量
让我们通过创建一个基于笔记本的工具包来逐步分析 Spark 中的数据。我们将实现的掩码函数在几个细节粒度上设置,从文件级别到行级别,然后到字段级别:
- 应用于整个文件的字符级掩码是:
-
Unicode 频率,UTF-16 多字节表示(也称为代码点),在文件级别
-
UTF 字符类频率,文件级别
-
分隔符频率,行级别
- 应用于文件中字段的字符串级掩码是:
-
ASCII 低粒度概要,每个字段
-
ASCII 高粒度概要,每个字段
-
人口检查,每个字段
设置 Apache Zeppelin
由于我们将要通过可视化方式探索我们的数据,一个非常有用的产品是 Apache Zeppelin,它可以非常方便地混合和匹配技术。Apache Zeppelin 是 Apache 孵化器产品,使我们能够创建一个包含多种不同语言的笔记本或工作表,包括 Python、Scala、SQL 和 Bash,这使其非常适合使用 Spark 进行探索性数据分析。
代码以笔记本风格编写,使用段落(或单元格),其中每个单元格可以独立执行,这样可以轻松地处理小段代码,而无需反复编译和运行整个程序。它还作为生成任何给定输出所使用的代码的记录,并帮助我们集成可视化。
Zeppelin 可以快速安装和运行,最小安装过程如下所述:
-
从这里下载并提取 Zeppelin:
zeppelin.incubator.apache.org/download.html
-
找到 conf 目录并复制
zeppelin-env.sh.template
,命名为zeppelin-env.sh
。 -
修改
zeppelin-env.sh
文件,取消注释并设置JAVA_HOME
和SPARK_HOME
条目为您机器上的相关位置。 -
如果您希望 Zeppelin 在 Spark 中使用 HDFS,请将
HADOOP_CONF_DIR
条目设置为您的 Hadoop 文件的位置;hdfs-site.xml
,core-site.xml
等。 -
启动 Zeppelin 服务:
bin/zeppelin-daemon.sh start
。这将自动获取conf/zeppelin-env.sh
中所做的更改。
在我们的测试集群上,我们使用的是 Hortonworks HDP 2.6,Zeppelin 作为安装的一部分。
在使用 Zeppelin 时需要注意的一点是,第一段应始终声明外部包。任何 Spark 依赖项都可以使用ZeppelinContext
以这种方式添加,以便在 Zeppelin 中的每次解释器重新启动后立即运行;例如:
%dep
z.reset
// z.load("groupId>:artifactId:version")
之后,我们可以在任何可用的语言中编写代码。我们将通过声明每个单元格的解释器类型(%spark
,%sql
和%shell
)在笔记本中使用 Scala,SQL 和 Bash 的混合。如果没有给出解释器,Zeppelin 默认为 Scala Spark(%spark
)。
您可以在我们的代码库中找到与本章配套的 Zeppelin 笔记本,以及其他笔记本。
构建可重用的笔记本
在我们的代码库中,我们创建了一个简单、可扩展、开源的数据分析库,也可以在这里找到:bytesumo@bitbucket.org/gzet_io/profilers.git
该库负责应用掩码到数据框架所需的框架,包括将文件的原始行转换为仅有一列的数据框架的特殊情况。我们不会逐行介绍该框架的所有细节,但最感兴趣的类在文件MaskBasedProfiler.scala
中找到,该文件还包含每个可用掩码函数的定义。
使用此库的一个很好的方法是构建一个用户友好的笔记本应用程序,允许对数据进行可视化探索。我们已经为使用 Apache Zeppelin 进行分析准备了这样的笔记本。接下来,我们将演示如何使用前面的部分构建我们自己的笔记本。我们示例中的数据是 GDELT event
文件,格式为简单的制表符分隔。
构建笔记本的第一步(甚至只是玩弄我们准备好的笔记本)是将profilers-1.0.0.jar
文件从我们的库复制到集群上 Zeppelin 用户可以访问的本地目录中,对于 Hortonworks 安装来说,这是 Namenode 上 Zeppelin 用户的主目录。
git clone https://bytesumo@bitbucket.org/gzet_io/profilers.git
sudo cp profilers-1.0.0.jar /home/zeppelin/.
sudo ls /home/zeppelin/
然后我们可以访问http://{main.install.hostname}:9995
来访问 Apache Zeppelin 主页。从该页面,我们可以上传我们的笔记本并跟随,或者我们可以创建一个新的笔记本,并通过单击创建新笔记来构建我们自己的笔记本。
在 Zeppelin 中,笔记本的第一段是我们执行 Spark 代码依赖关系的地方。我们将导入稍后需要的分析器 jar 包:
%dep
// you need to put the profiler jar into a directory
// that Zeppelin has access to.
// For example, /home/zeppelin, a non-hdfs directory on
// the namenode.
z.load("/home/zeppelin/profilers-1.0.0.jar")
// you may need to restart your interpreter, then run
// this paragraph
在第二段中,我们包括一个小的 shell 脚本来检查我们想要分析的文件,以验证我们是否选择了正确的文件。请注意column
和colrm
的使用,它们都是非常方便的 Unix 命令,用于在命令行上检查列式表数据:
%sh
# list the first two files in the directory, make sure the header file exists
# note - a great trick is to write just the headers to a delimited file
# that sorts to the top of your file glob, a trick that works well with
# Spark’s csv reader where headers are not on each file you
# hold in hdfs.
# this is a quick inspection check, see we use column and
# colrm to format it:
hdfs dfs -cat "/user/feeds/gdelt/events/*.export.CSV" \
|head -4|column -t -s $'\t'|colrm 68
GlobalEventID Day MonthYear Year FractionDate Actor1Code
610182939 20151221 201512 2015 2015.9616
610182940 20151221 201512 2015 2015.9616
610182941 20151221 201512 2015 2015.9616 CAN
在第 3、4、5 和 6 段中,我们使用 Zeppelin 的用户输入框功能,允许用户配置 EDA 笔记本,就像它是一个真正的基于 Web 的应用程序一样。这允许用户配置四个变量,可以在笔记本中重复使用,以驱动进一步的调查:YourMask,YourDelimiter,YourFilePath和YourHeaders。当我们隐藏编辑器并调整窗口的对齐和大小时,这看起来很棒:
如果我们打开准备好的笔记本并点击任何这些输入段落上的显示编辑器,我们将看到我们如何设置它们以在 Zeppelin 中提供下拉框,例如:
val YourHeader = z.select("YourHeaders", Seq( ("true", "HasHeader"), ("false", "No Header"))).toString
接下来,我们有一个用于导入我们需要的函数的段落:
import io.gzet.profilers._
import sys.process._
import org.apache.spark.sql.SQLContext
import org.apache.spark.sql.functions.udf
import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType}
import org.apache.spark.sql.SaveMode
import sqlContext.implicits._
然后我们继续到一个新的段落,配置和导入我们读取的数据:
val InputFilePath = YourFilePath
// set our input to user's file glob
val RawData = sqlContext.read
// read in tabular data
.option("header", YourHeader)
// configurable headers
.option("delimiter", YourDelimiter )
// configurable delimiters
.option("nullValue", "NULL")
// set a default char if nulls seen
.option("treatEmptyValuesAsNulls", "true")
// set to null
.option("inferschema", "false")
// do not infer schema, we'll discover it
.csv(InputFilePath)
// file glob path. Can use wildcards
RawData.registerTempTable("RawData")
// register data for Spark SQL access to it
RawData.cache()
// cache the file for use
val RawLines = sc.textFile(InputFilePath)
// read the file lines as a string
RawLines.toDF.registerTempTable("RawLines")
// useful to check for schema corruption
RawData.printSchema()
// print out the schema we found
// define our profiler apps
val ASCIICLASS_HIGHGRAIN = MaskBasedProfiler(PredefinedMasks.ASCIICLASS_HIGHGRAIN)
val CLASS_FREQS = MaskBasedProfiler(PredefinedMasks.CLASS_FREQS)
val UNICODE = MaskBasedProfiler(PredefinedMasks.UNICODE)
val HEX = MaskBasedProfiler(PredefinedMasks.HEX)
val ASCIICLASS_LOWGRAIN = MaskBasedProfiler(PredefinedMasks.ASCIICLASS_LOWGRAIN)
val POPCHECKS = MaskBasedProfiler(PredefinedMasks.POPCHECKS)
// configure our profiler apps
val Metrics_ASCIICLASS_HIGHGRAIN = ASCIICLASS_HIGHGRAIN.profile(YourFilePath, RawData)
val Metrics_CLASS_FREQS = CLASS_FREQS.profile(YourFilePath, RawLines.toDF)
val Metrics_UNICODE = UNICODE.profile(YourFilePath, RawLines.toDF)
val Metrics_HEX = HEX.profile(YourFilePath, RawLines.toDF)
val Metrics_ASCIICLASS_LOWGRAIN = ASCIICLASS_LOWGRAIN.profile(YourFilePath, RawData)
val Metrics_POPCHECKS = POPCHECKS.profile(YourFilePath, RawData)
// note some of the above read tabular data, some read rawlines of string data
// now register the profiler output as sql accessible data frames
Metrics_ASCIICLASS_HIGHGRAIN.toDF.registerTempTable("Metrics_ASCIICLASS_HIGHGRAIN")
Metrics_CLASS_FREQS.toDF.registerTempTable("Metrics_CLASS_FREQS")
Metrics_UNICODE.toDF.registerTempTable("Metrics_UNICODE")
Metrics_HEX.toDF.registerTempTable("Metrics_HEX")
Metrics_ASCIICLASS_LOWGRAIN.toDF.registerTempTable("Metrics_ASCIICLASS_LOWGRAIN")
Metrics_POPCHECKS.toDF.registerTempTable("Metrics_POPCHECKS")
现在我们已经完成了配置步骤,我们可以开始检查我们的表格数据,并发现我们报告的列名是否与我们的输入数据匹配。在一个新的段落窗口中,我们使用 SQL 上下文来简化调用 SparkSQL 并运行查询:
%sql
select * from RawData
limit 10
Zeppelin 的一个很棒的地方是,输出被格式化为一个合适的 HTML 表,我们可以轻松地用它来检查具有许多列的宽文件(例如 GDELT 事件文件):
我们可以从显示的数据中看到,我们的列与输入数据匹配;因此我们可以继续进行分析。
注意
如果您希望读取 GDELT 事件文件,您可以在我们的代码存储库中找到头文件。
如果此时列与内容之间的数据对齐存在错误,还可以选择之前配置的 RawLines Dataframe 的前 10 行,它将仅显示原始字符串数据输入的前 10 行。如果数据恰好是制表符分隔的,我们将立即看到另一个好处,即 Zeppelin 格式化输出将自动对齐原始字符串的列,就像我们之前使用 bash 命令column那样。
现在我们将继续研究文件的字节,以发现其中的编码细节。为此,我们加载我们的查找表,然后将它们与我们之前注册为表的分析器函数的输出进行连接。请注意,分析器的输出可以直接作为可调用的 SQL 表处理:
// load the UTF lookup tables
val codePointsSchema = StructType(Array(
StructField("CodePoint" , StringType, true), //$1
StructField("Category" , StringType, true), //$2
StructField("CodeDesc" , StringType, true) //$3
))
val UnicodeCatSchema = StructType(Array(
StructField("Category" , StringType, true), //$1
StructField("Description" , StringType, true) //$2
))
val codePoints = sqlContext.read
.option("header", "false") // configurable headers
.schema(codePointsSchema)
.option("delimiter", "\t" ) // configurable delimiters
.csv("/user/feeds/ref/codepoints2.txt") // configurable path
codePoints.registerTempTable("codepoints")
codePoints.cache()
val utfcats = sqlContext.read
.option("header", "false") // configurable headers
.schema(UnicodeCatSchema)
.option("delimiter", "\t" ) // configurable delimiters
.csv("/user/feeds/ref/UnicodeCategory.txt")
utfcats.registerTempTable("utfcats")
utfcats.cache()
// Next we build the different presentation layer views for the codepoints
val hexReport = sqlContext.sql("""
select
r.Category
, r.CodeDesc
, sum(maskCount) as maskCount
from
( select
h.*
,c.*
from Metrics_HEX h
left outer join codepoints c
on ( upper(h.MaskType) = c.CodePoint)
) r
group by r.Category, r.CodeDesc
order by r.Category, r.CodeDesc, 2 DESC
""")
hexReport.registerTempTable("hexReport")
hexReport.cache()
hexReport.show(10)
+--------+-----------------+---------+
|Category| CodeDesc|maskCount|
+--------+-----------------+---------+
| Cc| CTRL: CHARACTER| 141120|
| Ll| LATIN SMALL| 266070|
| Lu| LATIN CAPITAL| 115728|
| Nd| DIGIT EIGHT| 18934|
| Nd| DIGIT FIVE| 24389|
| Nd| DIGIT FOUR| 24106|
| Nd| DIGIT NINE| 17204|
| Nd| DIGIT ONE| 61165|
| Nd| DIGIT SEVEN| 16497|
| Nd| DIGIT SIX| 31706|
+--------+-----------------+---------+
在新的段落中,我们可以使用 SQLContext 来可视化输出。为了帮助查看偏斜的值,我们可以使用 SQL 语句来计算计数的对数。这将产生一个图形,我们可以在最终报告中包含,我们可以在原始频率和对数频率之间切换。
因为我们已经加载了字符类别,我们还可以调整可视化以进一步简化图表:
在进行 EDA 时,我们必须始终运行的基本检查是人口普查,我们使用 POPCHECKS 进行计算。 POPCHECKS 是我们在 Scala 代码中定义的特殊掩码,如果字段有值则返回1
,如果没有则返回0
。当我们检查结果时,我们注意到我们需要进行一些最终报告写作,以更直接地解释数字:
Metrics_POPCHECKS.toDF.show(1000, false)
我们可以分两步来做。首先,我们可以使用 SQL case 表达式将数据转换为populated或missing的值,这应该有所帮助。然后,我们可以通过对文件名、metricDescriptor
和fieldname
进行groupby
并对已填充和缺失的值进行求和来旋转这个聚合数据集。当我们这样做时,我们还可以在分析器没有找到任何数据被填充或缺失的情况下包括默认值为零。在计算百分比时,这一点很重要,以确保我们从不会有空的分子或分母。虽然这段代码可能不像它本来可以那样简短,但它演示了在SparkSQL
中操作数据的一些技术。
还要注意,在SparkSQL
中,我们可以使用 SQL coalesce
语句,这与 Spark 本机的coalesce
功能不同,用于操作 RDD。在 SQL 中,此函数将 null 转换为默认值,并且通常被滥用以捕获生产级代码中数据不太可信的特殊情况。还值得注意的是,在SparkSQL
中很好地支持子选择。您甚至可以大量使用这些,Spark 不会抱怨。这特别有用,因为它们是许多传统数据库工程师以及有各种数据库经验的人编程的最自然方式:
val pop_qry = sqlContext.sql("""
select * from (
select
fieldName as rawFieldName
, coalesce( cast(regexp_replace(fieldName, "C", "") as INT), fieldName) as fieldName
, case when maskType = 0 then "Populated"
when maskType = 1 then "Missing"
end as PopulationCheck
, coalesce(maskCount, 0) as maskCount
, metricDescriptor as fileName
from Metrics_POPCHECKS
) x
order by fieldName
""")
val pivot_popquery = pop_qry.groupBy("fileName","fieldName").pivot("PopulationCheck").sum("maskCount")
pivot_popquery.registerTempTable("pivot_popquery")
val per_pivot_popquery = sqlContext.sql("""
Select
x.*
, round(Missing/(Missing + Populated)*100,2) as PercentMissing
from
(select
fieldname
, coalesce(Missing, 0) as Missing
, coalesce(Populated,0) as Populated
, fileName
from pivot_popquery) x
order by x.fieldname ASC
""")
per_pivot_popquery.registerTempTable("per_pivot_popquery")
per_pivot_popquery.select("fieldname","Missing","Populated","PercentMissing","fileName").show(1000,false)
上述代码的输出是关于数据的字段级填充计数的干净报告表:
当在我们的 Zeppelin 笔记本中以stacked
条形图功能进行图形显示时,数据产生了出色的可视化效果,立即告诉我们文件中数据填充的水平:
由于 Zeppelin 的条形图支持工具提示,我们可以使用指针来观察列的全名,即使它们在默认视图中显示不佳。
最后,我们还可以在我们的笔记本中包含进一步的段落,以显示先前解释的ASCII_HighGrain
和ASCII_LowGrain
掩码的结果。这可以通过简单地将分析器输出作为表格查看,也可以使用 Zeppelin 中的更高级功能来完成。作为表格,我们可以尝试以下操作:
val proReport = sqlContext.sql("""
select * from (
select
metricDescriptor as sourceStudied
, "ASCII_LOWGRAIN" as metricDescriptor
, coalesce(cast( regexp_replace(fieldName, "C", "") as INT),fieldname) as fieldName
, ingestTime
, maskType as maskInstance
, maskCount
, description
from Metrics_ASCIICLASS_LOWGRAIN
) x
order by fieldNAme, maskCount DESC
""")
proReport.show(1000, false)
为了构建一个交互式查看器,当我们查看可能具有非常高基数的 ASCII_HighGrain 掩码时,我们可以设置一个 SQL 语句,接受 Zeppelin 用户输入框的值,用户可以在其中键入列号或字段名,以检索我们收集的指标的相关部分。
我们可以在新的 SQL 段落中这样做,SQL 谓词为x.fieldName like '%${ColumnName}%'
:
%sql
select x.* from (
select
metricDescriptor as sourceStudied
, "ASCII_HIGHGRAIN" as metricDescriptor
, coalesce(cast( regexp_replace(fieldName, "C", "")
as INT),fieldname) as fieldName
, ingestTime
, maskType as maskInstance
, maskCount
, log(maskCount) as log_maskCount
from Metrics_ASCIICLASS_HIGHGRAIN
) x
where x.fieldName like '%${ColumnName}%'
order by fieldName, maskCount DESC
这创建了一个交互式用户窗口,根据用户输入刷新,生成具有多个输出配置的动态分析报告。在这里,我们展示的输出不是表格,而是一个图表,显示了应该具有低基数的字段Action在事件文件中的频率计数的对数:
结果显示,即使像经度这样简单的字段在数据中也有很大的格式分布。
到目前为止审查的技术应该有助于创建一个非常可重用的笔记本,用于快速高效地对所有输入数据进行探索性数据分析,生成我们可以用来生成关于输入文件质量的出色报告和文档的图形输出。
探索 GDELT
探索 EDA 的一个重要部分是获取和记录数据源,GDELT 内容也不例外。在研究 GKG 数据集后,我们发现仅仅记录我们应该使用的实际数据源就是具有挑战性的。在接下来的几节中,我们提供了我们找到的用于使用的资源的全面列表,这些资源需要在示例中运行。
注意
关于下载时间的警告:使用典型的 5 Mb 家庭宽带,下载 2000 个 GKG 文件大约需要 3.5 小时。考虑到仅英语语言的 GKG 文件就有超过 40,000 个,这可能需要一段时间来下载。
GDELT GKG 数据集
我们应该使用最新的 GDELT 数据源,截至 2016 年 12 月的 2.1 版本。这些数据的主要文档在这里:
data.gdeltproject.org/documentation/GDELT-Global_Knowledge_Graph_Codebook-V2.1.pdf
在下一节中,我们已经包括了数据和次要参考查找表,以及进一步的文档。
文件
GKG-英语语言全球知识图谱(v2.1)
data.gdeltproject.org/gdeltv2/masterfilelist.txt
data.gdeltproject.org/gdeltv2/lastupdate.txt
GKG-翻译-非英语全球知识图谱
data.gdeltproject.org/gdeltv2/lastupdate-translation.txt
data.gdeltproject.org/gdeltv2/masterfilelist-translation.txt
GKG-TV(互联网档案馆-美国电视全球知识图谱)
data.gdeltproject.org/gdeltv2_iatelevision/lastupdate.txt
data.gdeltproject.org/gdeltv2_iatelevision/masterfilelist.txt
GKG-Visual-CloudVision
data.gdeltproject.org/gdeltv2_cloudvision/lastupdate.txt
特别收藏品
GKG-AME-非洲和中东全球知识图谱
data.gdeltproject.org/gkgv2_specialcollections/AME-GKG.CIA.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/AME-GKG.CORE.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/AME-GKG.DTIC.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/AME-GKG.IADISSERT.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/AME-GKG.IANONDISSERT.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/AME-GKG.JSTOR.gkgv2.csv.zip
GKG-HR(人权收藏)
data.gdeltproject.org/gkgv2_specialcollections/HR-GKG.AMNESTY.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/HR-GKG.CRISISGROUP.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/HR-GKG.FIDH.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/HR-GKG.HRW.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/HR-GKG.ICC.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/HR-GKG.OHCHR.gkgv2.csv.zip
data.gdeltproject.org/gkgv2_specialcollections/HR-GKG.USSTATE.gkgv2.csv.zip
参考数据
data.gdeltproject.org/documentation/GCAM-MASTER-CODEBOOK.TXT
data.gdeltproject.org/supportingdatasets/GNS-GAUL-ADM2-CROSSWALK.TXT.zip
data.gdeltproject.org/supportingdatasets/DOMAINSBYCOUNTRY-ENGLISH.TXT
data.gdeltproject.org/supportingdatasets/DOMAINSBYCOUNTRY-ALLLANGUAGES.TXT
www.unicode.org/Public/UNIDATA/UnicodeData.txt
探索 GKG v2.1
当我们审查现有的探索 GDELT 数据源的文章时,我们发现许多研究都集中在文章的人物、主题和语调上,还有一些集中在早期事件文件上。但是,几乎没有发表过探索现在包含在 GKG 文件中的全球内容分析指标(GCAM)内容的研究。当我们尝试使用我们构建的数据质量工作簿来检查 GDELT 数据源时,我们发现全球知识图难以处理,因为文件使用了多个嵌套分隔符进行编码。快速处理这种嵌套格式数据是处理 GKG 和 GCAM 的关键挑战,也是本章其余部分的重点。
在探索 GKG 文件中的 GCAM 数据时,我们需要回答一些明显的问题:
-
英语语言 GKG 文件和翻译后的跨语言国际文件之间有什么区别?在这些数据源之间,数据的填充方式是否有差异,考虑到一些实体识别算法可能在翻译文件上表现不佳?
-
如果翻译后的数据在包含在 GKG 文件中的 GCAM 情感指标数据集方面有很好的人口统计,那么它(或者英文版本)是否可信?我们如何访问和规范化这些数据,它是否包含有价值的信号而不是噪音?
如果我们能够单独回答这两个问题,我们将对 GDELT 作为数据科学信号源的实用性有了很大的了解。然而,如何回答这些问题很重要,我们需要尝试并模板化我们的代码,以便在获得这些答案时创建可重用的配置驱动的 EDA 组件。如果我们能够按照我们的原则创建可重用的探索,我们将产生比硬编码分析更多的价值。
跨语言文件
让我们重复我们之前的工作,揭示一些质量问题,然后将我们的探索扩展到这些更详细和复杂的问题。通过对正常 GKG 数据和翻译文件运行一些人口统计(POPCHECK)指标到临时文件,我们可以导入并合并结果。这是我们重复使用标准化指标格式的好处;我们可以轻松地在数据集之间进行比较!
与其详细查看代码,我们不如提供一些主要答案。当我们检查英语和翻译后的 GKG 文件之间的人口统计时,我们确实发现了内容可用性上的一些差异:
我们在这里看到,翻译后的 GKG 跨语言文件根本没有引用数据,并且在识别人员时非常低人口统计,与我们在一般英语新闻中看到的人口统计相比。因此,肯定有一些需要注意的差异。
因此,我们应该仔细检查我们希望在生产中依赖的跨语言数据源中的任何内容。稍后我们将看到 GCAM 情感内容中的翻译信息与母语英语情感相比如何。
可配置的 GCAM 时间序列 EDA
GCAM 的内容主要由字数计数组成,通过使用词典过滤器过滤新闻文章并对表征感兴趣主题的同义词进行字数计数而创建。通过将计数除以文档中的总字数,可以对结果进行规范化。它还包括得分值,提供似乎是基于直接研究原始语言文本的情感得分。
我们可以快速总结要在 GCAM 中研究和探索的情感变量范围,只需几行代码,其输出附有语言的名称:
wget http://data.gdeltproject.org/documentation/GCAM-MASTER-CODEBOOK.TXT
cat GCAM-MASTER-CODEBOOK.TXT | \
gawk 'BEGIN{OFS="\t"} $4 != "Type" {print $4,$5}' | column -t -s $'\t' \
| sort | uniq -c | gawk ' BEGIN{print "Lang Type Count" }{print $3, $2,\ $1}' | column -t -s $' '
Lang Type Count Annotation
ara SCOREDVALUE 1 Arabic
cat SCOREDVALUE 16 Catalan
deu SCOREDVALUE 1 German
eng SCOREDVALUE 30 English
fra SCOREDVALUE 1 French
glg SCOREDVALUE 16 Galician
hin SCOREDVALUE 1 Hindi
ind SCOREDVALUE 1 Indonesian
kor SCOREDVALUE 1 Korean
por SCOREDVALUE 1 Portuguese
rus SCOREDVALUE 1 Russian
spa SCOREDVALUE 29 Spanish
urd SCOREDVALUE 1 Urdu
zho SCOREDVALUE 1 Chinese
ara WORDCOUNT 1 Arabic
cat WORDCOUNT 16 Catalan
deu WORDCOUNT 44 German
eng WORDCOUNT 2441 English
fra WORDCOUNT 78 French
glg WORDCOUNT 16 Galician
hin WORDCOUNT 1 Hindi
hun WORDCOUNT 36 Hungarian
ind WORDCOUNT 1 Indonesian
kor WORDCOUNT 1 Korean
por WORDCOUNT 46 Portuguese
rus WORDCOUNT 65 Russian
spa WORDCOUNT 62 Spanish
swe WORDCOUNT 64 Swedish
urd WORDCOUNT 1 Urdu
zho WORDCOUNT 1 Chinese
基于字数计数的 GCAM 时间序列似乎是最完整的,特别是在英语中有 2441 个情感度量!处理如此多的度量似乎很困难,即使进行简单的分析也是如此。我们需要一些工具来简化事情,并且需要集中我们的范围。
为了帮助,我们创建了一个基于简单 SparkSQL 的探索者,从 GCAM 数据块中提取和可视化时间序列数据,专门针对基于字数计数的情感。它是通过克隆和调整我们在 Zeppelin 中的原始数据质量探索者创建的。
它通过调整以使用定义的模式读取 GKG 文件 glob,并预览我们想要专注的原始数据:
val GkgCoreSchema = StructType(Array(
StructField("GkgRecordId" , StringType, true), //$1
StructField("V21Date" , StringType, true), //$2
StructField("V2SrcCollectionId" , StringType, true), //$3
StructField("V2SrcCmnName" , StringType, true), //$4
StructField("V2DocId" , StringType, true), //$5
StructField("V1Counts" , StringType, true), //$6
StructField("V21Counts" , StringType, true), //$7
StructField("V1Themes" , StringType, true), //$8
StructField("V2Themes" , StringType, true), //$9
StructField("V1Locations" , StringType, true), //$10
StructField("V2Locations" , StringType, true), //$11
StructField("V1Persons" , StringType, true), //$12
StructField("V2Persons" , StringType, true), //$13
StructField("V1Orgs" , StringType, true), //$14
StructField("V2Orgs" , StringType, true), //$15
StructField("V15Tone" , StringType, true), //$16
StructField("V21Dates" , StringType, true), //$17
StructField("V2GCAM" , StringType, true), //$18
StructField("V21ShareImg" , StringType, true), //$19
StructField("V21RelImg" , StringType, true), //$20
StructField("V21SocImage" , StringType, true), //$21
StructField("V21SocVideo" , StringType, true), //$22
StructField("V21Quotations" , StringType, true), //$23
StructField("V21AllNames" , StringType, true), //$24
StructField("V21Amounts" , StringType, true), //$25
StructField("V21TransInfo" , StringType, true), //$26
StructField("V2ExtrasXML" , StringType, true) //$27
))
val InputFilePath = YourFilePath
val GkgRawData = sqlContext.read
.option("header", "false")
.schema(GkgCoreSchema)
.option("delimiter", "\t")
.csv(InputFilePath)
GkgRawData.registerTempTable("GkgRawData")
// now we register slices of the file we want to explore quickly
val PreRawData = GkgRawData.select("GkgRecordID","V21Date","V2GCAM", "V2DocId")
// we select the GCAM, plus the story URLs in V2DocID, which later we can //filter on.
PreRawData.registerTempTable("PreRawData")
早期列选择的结果将我们的内容隔离到要探索的领域;时间(V21Date
),情感(V2GCAM
)和源 URL(V2DocID
):
+----+--------------+--------------------+--------------------+
| ID| V21Date| V2GCAM| V2DocId|
+----+--------------+--------------------+--------------------+
|...0|20161101000000|wc:77,c12.1:2,c12...|http://www.tampab...|
|...1|20161101000000|wc:57,c12.1:6,c12...|http://regator.co...|
|...2|20161101000000|wc:740,c1.3:2,c12...|http://www.9news....|
|...3|20161101000000|wc:1011,c1.3:1,c1...|http://www.gaming...|
|...4|20161101000000|wc:260,c1.2:1,c1....|http://cnafinance...|
+----+--------------+--------------------+--------------------+
在一个新的 Zeppelin 段落中,我们创建一个 SQLContext,并仔细解开 GCAM 记录的嵌套结构。请注意,V2GCAM 字段中逗号分隔的第一个内部行包含wc
维度和代表该 GkgRecordID 故事的字数的度量,然后列出其他情感度量。我们需要将这些数据展开为实际行,以及将所有基于字数计数的情感除以wc
文章的总字数以规范化分数。
在以下片段中,我们设计了一个SparkSQL
语句,以典型的洋葱风格进行操作,使用子查询。这是一种编码风格,如果你还不了解,可能希望学会阅读。它的工作方式是 - 创建最内部的选择/查询,然后运行它进行测试,然后用括号包裹起来,继续通过选择数据进入下一个查询过程,依此类推。然后,催化剂优化器会进行优化整个流程。这导致了一个既声明性又可读的 ETL 过程,同时还提供了故障排除和隔离管道中任何部分问题的能力。如果我们想要了解如何处理嵌套数组过程,我们可以轻松重建以下 SQL,首先运行最内部的片段,然后审查其输出,然后扩展它以包括包装它的下一个查询,依此类推。然后我们可以逐步审查分阶段的输出,以审查整个语句如何一起工作以提供最终结果。
以下查询中的关键技巧是如何将单词计数分母应用于其他情感单词计数,以规范化值。这种规范化方法实际上是 GKG 文档中建议的,尽管没有提供实现提示。
值得注意的是,V21Date 字段是如何从整数转换为日期的,这对于有效绘制时间序列是必要的。转换需要我们预先导入以下库,除了笔记本中导入的其他库:
import org.apache.spark.sql.functions.{Unix_timestamp, to_date}
使用Unix_timestamp
函数,我们将 V21Date 转换为Unix_timestamp
,这是一个整数,然后再次将该整数转换为日期字段,所有这些都使用本机 Spark 库来配置格式和时间分辨率。
以下 SQL 查询实现了我们期望的调查:
%sql
-- for urls containing “trump” build 15min “election fraud” sentiment time series chart.
select
V21Date
, regexp_replace(z.Series, "\\.", "_") as Series
, sum(coalesce(z.Measure, 0) / coalesce (z.WordCount, 1)) as Sum_Normalised_Measure
from
(
select
GkgRecordID
, V21Date
, norm_array[0] as wc_norm_series
, norm_array[1] as WordCount
, ts_array[0] as Series
, ts_array[1] as Measure
from
(
select
GkgRecordID
, V21Date
, split(wc_row, ":") as norm_array
, split(gcam_array, ":") as ts_array
from
(
select
GkgRecordID
, V21Date
, gcam_row[0] as wc_row
, explode(gcam_row) as gcam_array
from
(
select
GkgRecordID
, from_Unixtime(
Unix_timestamp(
V21Date, "yyyyMMddHHmmss")
, 'YYYY-MM-dd-HH-mm'
) as V21Date
, split(V2GCAM, ",") as gcam_row
from PreRawData
where length(V2GCAM) >1
and V2DocId like '%trump%'
) w
) x
) y
) z
where z.Series <> "wc" and z.Series = 'c18.134'
-- c18.134 is "ELECTION_FRAUD"
group by z.V21Date, z.Series
order by z.V21Date ASC
查询的结果在 Zeppelin 的时间序列查看器中显示。它显示时间序列数据正在正确积累,并且看起来非常可信,2016 年 11 月 8 日有一个短暂的高峰:美国总统选举的那一天。
现在我们有一个可以检查 GCAM 情绪分数的工作 SQL 语句,也许我们应该再检查一些其他指标,例如关于不同但相关主题的,比如英国的脱欧投票。
我们选择了三个看起来有趣的 GCAM 情绪指标,除了选举舞弊指标,希望能够提供与我们在美国选举中看到的结果有趣的比较。我们将研究的指标是:
-
‘c18.101’ – 移民
-
‘c18.100’ – 民主
-
‘c18.140’ – 选举
为了包括它们,我们需要扩展我们的查询以获取多个归一化的 Series,并且我们可能还需要注意结果可能不都适合 Zeppelin 的查看器,默认只接受前 1000 个结果,所以我们可能需要进一步总结到小时或天。虽然这不是一个很大的改变,但看到我们现有工作的可扩展性将是有趣的:
val ExtractGcam = sqlContext.sql("""
select
a.V21Date
, a.Series
, Sum(a.Sum_Normalised_Measure) as Sum_Normalised_Measure
from (
select
z.partitionkey
, z.V21Date
, regexp_replace(z.Series, "\\.", "_") as Series
, sum(coalesce(z.Measure, 0) / coalesce (z.WordCount, 1))
as Sum_Normalised_Measure
from
(
select
y.V21Date
, cast(cast(round(rand(10) *1000,0) as INT) as string)
as partitionkey
, y.norm_array[0] as wc_norm_series
, y.norm_array[1] as WordCount
, y.ts_array[0] as Series
, y.ts_array[1] as Measure
from
(
select
x.V21Date
, split(x.wc_row, ":") as norm_array
, split(x.gcam_array, ":") as ts_array
from
(
select
w.V21Date
, w.gcam_row[0] as wc_row
, explode(w.gcam_row) as gcam_array
from
(
select
from_Unixtime(Unix_timestamp(V21Date,
"yyyyMMddHHmmss"), 'YYYY-MM-dd-HH-mm')
as V21Date
, split(V2GCAM, ",") as gcam_row
from PreRawData
where length(V2GCAM) > 20
and V2DocId like '%brexit%'
) w
where gcam_row[0] like '%wc%'
OR gcam_row[0] like '%c18.1%'
) x
) y
) z
where z.Series <> "wc"
and
( z.Series = 'c18.134' -- Election Fraud
or z.Series = 'c18.101' -- Immigration
or z.Series = 'c18.100' -- Democracy
or z.Series = 'c18.140' -- Election
)
group by z.partitionkey, z.V21Date, z.Series
) a
group by a.V21Date, a.Series
""")
在这个第二个例子中,我们进一步完善了我们的基本查询,删除了我们没有使用的不必要的 GKGRecordIDs。这个查询还演示了如何使用一组简单的谓词来过滤对许多Series
名称的结果。请注意,我们还添加了一个使用以下内容的预分组步骤:
group by z.partitionkey, z.V21Date, z.Series
-- Where the partition key is:
-- cast(cast(round(rand(10) *1000,0) as INT) as string) as partitionkey
这个随机数用于创建一个分区前缀键,我们在内部的 group by 语句中使用它,然后再次进行分组而不使用这个前缀。查询是以这种方式编写的,因为它有助于细分和预汇总热点数据,并消除任何管道瓶颈。
当我们在 Zeppelin 的时间序列查看器中查看此查询的结果时,我们有机会进一步总结到小时计数,并使用 case 语句将神秘的 GCAM 系列代码翻译成适当的名称。我们可以在一个新的查询中执行此操作,帮助将特定的报告配置与一般的数据集构建查询隔离开来:
Select
a.Time
, a.Series
, Sum(Sum_Normalised_Measure) as Sum_Normalised_Measure
from
(
select
from_Unixtime(Unix_timestamp(V21Date,
"yyyy-MM-dd-HH-mm"),'YYYY-MM-dd-HH')
as Time
, CASE
when Series = 'c18_134' then 'Election Fraud'
when Series = 'c18_101' then 'Immigration'
when Series = 'c18_100' then 'Democracy'
when Series = 'c18_140' then 'Election'
END as Series
, Sum_Normalised_Measure
from ExtractGcam
-- where Series = 'c18_101' or Series = 'c18_140'
) a
group by a.Time, a.Series
order by a.Time
这个最终查询将数据减少到小时值,这比 Zeppelin 默认处理的 1000 行最大值要少,此外它生成了一个比较时间序列图表:
结果图表表明,在脱欧投票之前几乎没有关于选举舞弊的讨论,但是选举有高峰,而移民是一个比民主更热门的主题。再次,GCAM 英语情绪数据似乎具有真实信号。
现在我们已经为英语语言记录提供了一些信息,我们可以扩展我们的工作,探索它们与 GCAM 中的翻译数据的关系。
作为完成本笔记本中分析的最后一种方法,我们可以注释掉对特定Series
的过滤器,并将所有脱欧的 GCAM 系列数据写入我们的 HDFS 文件系统中的 parquet 文件中。这样我们就可以永久存储我们的 GCAM 数据到磁盘,甚至随着时间的推移追加新数据。以下是要么覆盖,要么追加到 parquet 文件所需的代码:
// save the data as a parquet file
val TimeSeriesParqueFile = "/user/feeds/gdelt/datastore/BrexitTimeSeries2016.parquet"
// *** uncomment to append to an existing parquet file ***
// ExtractGcam.save(TimeSeriesParqueFile
//, "parquet"
//, SaveMode.Append)
// ***************************************************************
// *** uncomment to initially load a new parquet file ***
ExtractGcam.save(TimeSeriesParqueFile
, "parquet"
, SaveMode.Overwrite)
// ***************************************************************
通过将 parquet 文件写入磁盘,我们现在建立了一个轻量级的 GCAM 时间序列数据存储,可以让我们快速检索 GCAM 情绪,以便在语言组之间进行探索。
Apache Zeppelin 上的 Plot.ly 图表
对于我们下一个的探索,我们还将扩展我们对 Apache Zeppelin 笔记本的使用,以包括使用一个名为 plotly 的外部图表库来生成%pyspark
图表,该库是由plot.ly/
开源的,可以用来创建打印质量的可视化。要在我们的笔记本中使用 plotly,我们可以升级我们的 Apache Zeppelin 安装,使用在github.com/beljun/zeppelin-plotly
中找到的代码,该代码提供了所需的集成。在它的 GitHub 页面上,有详细的安装说明,在他们的代码库中,他们提供了一个非常有帮助的示例笔记本。以下是一些在 HDP 集群上安装 plotly 以供 Zeppelin 使用的提示:
- 以 Zeppelin 用户身份登录 Namenode,并更改目录到 Zeppelin 主目录
/home/zeppelin
,在那里我们将下载外部代码:
git clone https://github.com/beljun/zeppelin-plotly
- 更改目录到保存 Zeppelin
*.war
文件的位置。这个位置在 Zeppelin配置标签中可以找到。例如:
cd /usr/hdp/current/zeppelin-server/lib
现在,按照说明,我们需要编辑在 Zeppelin war
文件中找到的 index.html 文档:
ls *war # zeppelin-web-0.6.0.2.4.0.0-169.war
cp zeppelin-web-0.6.0.2.4.0.0-169.war \
bkp_zeppelin-web-0.6.0.2.4.0.0-169.war
jar xvf zeppelin-web-0.6.0.2.4.0.0-169.war \
index.html
vi index.html
-
一旦提取了
index.html
页面,我们可以使用诸如 vim 之类的编辑器在 body 标签之前插入plotly-latest.min.js
脚本标签(按照说明),然后保存并执行文档。 -
将编辑后的
index.html
文档放回 war 文件中:
jar uvf zeppelin-web-0.6.0.2.4.0.0-169.war index.html
-
最后,登录 Ambari,并使用它重新启动 Zeppelin 服务。
-
按照其余的说明在 Zeppelin 中生成一个测试图表。
-
如果出现问题,我们可能需要安装或更新旧的库。登录 Namenode 并使用 pip 安装这些包:
sudo pip install plotly
sudo pip install plotly --upgrade
sudo pip install colors
sudo pip install cufflinks
sudo pip install pandas
sudo pip install Ipython
sudo pip install -U pyOpenSSL
# note also install pyOpenSSL to get things running.
安装完成后,我们现在应该能够创建 Zeppelin 笔记本,从%pyspark
段落中生成内联 plot.ly 图表,并且这些图表将使用本地库离线创建,而不是使用在线服务。
使用 plot.ly 探索翻译源 GCAM 情绪
对于这个比较,让我们关注一下在 GCAM 文档中找到的一个有趣的度量:c6.6;财务不确定性。这个度量计算了新闻报道和一个财务导向的不确定性词典之间的词匹配次数。如果我们追溯它的来源,我们可以发现驱动这个度量的学术论文和实际词典。然而,这个基于词典的度量是否适用于翻译后的新闻文本?为了调查这个问题,我们可以查看这个财务不确定性度量在英语、法语、德语、西班牙语、意大利语和波兰语这六个主要欧洲语言群中的差异,关于英国脱欧的主题。
我们创建一个新的笔记本,包括一个pyspark段落来加载 plot.ly 库并将其设置为离线模式运行:
%pyspark
# Instructions here: https://github.com/beljun/zeppelin-plotly
import sys
sys.path.insert(0, "/home/zeppelin/zeppelin-plotly")
import offline
sys.modules["plotly"].offline = offline
sys.modules["plotly.offline"] = offline
import cufflinks as cf
cf.go_offline()
import plotly.plotly as py
import plotly.graph_objs as go
import pandas as pd
import numpy as np
然后,我们创建一个段落来从 parquet 中读取我们缓存的数据:
%pyspark
GcamParquet = sqlContext.read.parquet("/user/feeds/gdelt/datastore/BrexitTimeSeries2016.parquet")
# register the content as a python data frame
sqlContext.registerDataFrameAsTable(GcamParquet, "BrexitTimeSeries")
然后,我们可以创建一个 SQL 查询来读取并准备数据进行绘图,并将其注册以供使用:
%pyspark
FixedExtractGcam = sqlContext.sql("""
select
V21Date
, Series
, CASE
when LangLen = 0 then "eng"
when LangLen > 0 then SourceLanguage
END as SourceLanguage
, FIPS104Country
, Sum_Normalised_Measure
from
( select *,length(SourceLanguage) as LangLen
from BrexitTimeSeries
where V21Date like "2016%"
) a
""")
sqlContext.registerDataFrameAsTable(FixedExtractGcam, "Brexit")
# pyspark accessible registration of the data
现在我们已经定义了一个适配器,我们可以创建一个查询,总结我们 parquet 文件中的数据,使其更容易适应内存:
%pyspark
timeplot = sqlContext.sql("""
Select
from_Unixtime(Unix_timestamp(Time, "yyyy-MM-dd"), 'YYYY-MM-dd HH:mm:ss.ssss') as Time
, a.Series
, SourceLanguage as Lang
--, Country
, sum(Sum_Normalised_Measure) as Sum_Normalised_Measure
from
( select
from_Unixtime(Unix_timestamp(V21Date,
"yyyy-MM-dd-HH"), 'YYYY-MM-dd') as Time
, SourceLanguage
, CASE
When Series = 'c6_6' then "Uncertainty"
END as Series
, Sum_Normalised_Measure
from Brexit
where Series in ('c6_6')
and SourceLanguage in ( 'deu', 'fra', 'ita', 'eng', 'spa', 'pol')
and V21Date like '2016%'
) a
group by a.Time, a.Series, a.SourceLanguage order by a.Time, a.Series, a.SourceLanguage
""")
sqlContext.registerDataFrameAsTable(timeplot, "timeplot")
# pyspark accessible registration of the data
这个主要的负载查询生成了一组数据,我们可以将其加载到pyspark
中的pandas
数组中,并且具有一个 plot.ly 准备好的时间戳格式:
+------------------------+-----------+----+----------------------+
|Time |Series |Lang|Sum_Normalised_Measure|
+------------------------+-----------+----+----------------------+
|2016-01-04 00:00:00.0000|Uncertainty|deu |0.0375 |
|2016-01-04 00:00:00.0000|Uncertainty|eng |0.5603189694252122 |
|2016-01-04 00:00:00.0000|Uncertainty|fra |0.08089269454114742 |
+------------------------+-----------+----+----------------------+
要将这些数据提供给 plot.ly,我们必须将我们生成的 Spark 数据框转换为一个pandas
数据框:
%pyspark
explorer = pd.DataFrame(timeplot.collect(), columns=['Time', 'Series', 'SourceLanguage','Sum_Normalised_Measure'])
当我们执行这一步时,我们必须记住要collect()
数据框架,以及重置列名以供pandas
使用。现在我们的 Python 环境中有了一个pandas
数组,我们可以轻松地将数据透视成便于进行时间序列绘图的形式:
pexp = pd.pivot_table(explorer, values='Sum_Normalised_Measure', index=['Time'], columns=['SourceLanguage','Series'], aggfunc=np.sum, fill_value=0)
最后,我们包括一个调用来生成图表:
pexp.iplot(title="BREXIT: Daily GCAM Uncertainty Sentiment Measures by Language", kind ="bar", barmode="stack")
现在我们已经生成了一个工作的 plot.ly 数据图表,我们应该创建一个自定义的可视化,这是标准的 Zeppelin 笔记本无法实现的,以展示 plotly 库为我们的探索带来的价值。一个简单的例子是生成一些小多图,就像这样:
pexp.iplot(title="BREXIT: Daily GCAM Uncertainty by Language, 2016-01 through 2016-07",subplots=True, shared_xaxes=True, fill=True, kind ="bar")
生成以下图表:
这个小多图表帮助我们看到,在意大利新闻中,2016 年 6 月 15 日似乎出现了财务不确定性的局部激增;就在选举前一周左右。这是我们可能希望调查的事情,因为在西班牙语新闻中也以较小的程度存在。
Plotly 还提供许多其他有趣的可视化方式。如果您仔细阅读了代码片段,您可能已经注意到 parquet 文件包括来自 GKG 文件的 FIPS10-4 国家代码。我们应该能够利用这些位置代码,使用 Plotly 绘制不确定性指标的区域地图,并同时利用我们先前的数据处理。
为了创建这个地理地图,我们重用了我们之前注册的 parquet 文件读取器查询。不幸的是,GKG 文件使用 FIPS 10-4 两个字符的国家编码,而 Plotly 使用 ISO-3166 三个字符的国家代码来自动为绘图处理的用户记录进行地理标记。我们可以通过在我们的 SQL 中使用 case 语句来重新映射我们的代码,然后在整个调查期间对它们进行汇总来解决这个问题。
%pyspark
mapplot = sqlContext.sql("""
Select
CountryCode
, sum(Sum_Normalised_Measure) as Sum_Normalised_Measure
from ( select
from_Unixtime(Unix_timestamp(V21Date, "yyyy-MM-dd-HH"),
'YYYY-MM') as Time
, CASE
when FIPS104Country = "AF" then "AFB"
when FIPS104Country = "AL" then "ALB"
-- I have excluded the full list of
-- countries in this code snippet
when FIPS104Country = "WI" then "ESH"
when FIPS104Country = "YM" then "YEM"
when FIPS104Country = "ZA" then "ZMB"
when FIPS104Country = "ZI" then "ZWE"
END as CountryCode
, Sum_Normalised_Measure
from Brexit
where Series in ('c6_6')
and V21Date like '2016%'
) a
group by a.CountryCode order by a.CountryCode
""")
sqlContext.registerDataFrameAsTable(mapplot, "mapplot") # python
mapplot2 = pd.DataFrame(mapplot.collect(), columns=['Country', 'Sum_Normalised_Measure'])
现在我们的数据已经准备在pandas
数据框中,我们可以使用以下一行 Python 代码调用可视化:
mapplot2.iplot( kind = 'choropleth', locations = 'Country', z = 'Sum_Normalised_Measure', text = 'Country', locationmode = 'ISO-3', showframe = True, showcoastlines = False, projection = dict(type = 'equirectangular'), colorscale = [[0,"rgb(5, 10, 172)"],[0.9,"rgb(40, 60, 190)"],[0.9,"rgb(70, 100, 245)"],[1,"rgb(90, 120, 245)"],[1,"rgb(106, 137, 247)"],[1,"rgb(220, 220, 220)"]])
最终结果是一个交互式、可缩放的世界地图。我们将其政治解释留给读者,但从技术上讲,也许这张地图显示了与新闻数量有关的效果,我们可以稍后对其进行归一化;例如通过将我们的值除以每个国家的总故事数。
结束语
值得指出的是,有许多参数驱动了我们对所有调查的探索,我们可以考虑如何将这些参数化以构建适当的探索产品来监控 GDELT。需要考虑的参数如下:
-
我们可以选择一个非 GCAM 字段进行过滤。在前面的示例中,它配置为 V2DocID,这是故事的 URL。在 URL 中找到诸如 BREXIT 或 TRUMP 之类的词将有助于将我们的调查范围限定在特定主题领域的故事中。我们还可以重复此技术以过滤 BBC 或 NYTIMES 等内容。或者,如果我们将此列替换为另一个列,例如 Theme 或 Person,那么这些列将提供新的方法来聚焦我们对特定主题或感兴趣的人的研究。
-
我们已经转换并概括了时间戳 V21Date 的粒度,以提供每小时的时间序列增量,但我们可以重新配置它以创建我们的时间序列,以月、周或日为基础 - 或者以任何其他增量为基础。
-
我们首先选择并限定了我们对感兴趣的一个时间序列c18_134,即选举舞弊,但我们可以轻松地重新配置它以查看移民或仇恨言论或其他 2400 多个基于词频的情感分数。
-
我们在笔记本的开头引入了一个文件 glob,它限定了我们在摘要输出中包含的时间量。为了降低成本,我们一开始将其保持较小,但我们可以重新聚焦这个时间范围到关键事件,甚至在有足够的处理预算(时间和金钱)的情况下打开它到所有可用的文件。
我们现在已经证明,我们的代码可以轻松调整,构建基于笔记本的 GCAM 时间序列探索器,从中我们将能够按需构建大量的专注调查;每个都以可配置的方式探索 GCAM 数据的内容。
如果您一直在仔细跟随笔记本中的 SQL 代码,并且想知道为什么它没有使用 Python API 编写,或者使用惯用的 Scala,我们将用最后一个观察来完成本节:正是因为它是由 SQL 构建的,所以它可以在 Python、R 或 Scala 上下文之间移动,几乎不需要重构代码。如果 R 中出现了新的图表功能,它可以轻松地移植到 R 中,然后可以将精力集中在可视化上。事实上,随着 Spark 2.0+的到来,也许在移植时需要最少审查的是 SQL 代码。代码可移植性的重要性无法强调得足够。然而,在 EDA 环境中使用 SQL 的最大好处是,它使得在 Zeppelin 中生成基于参数的笔记本变得非常容易,正如我们在早期的分析器部分所看到的。下拉框和其他 UI 小部件都可以与字符串处理一起创建,以在执行之前自定义代码,而不受后端语言的限制。这是一种非常快速的方式,可以在我们的分析中构建交互性和配置,而不需要涉及复杂的元编程方法。它还帮助我们避免解决 Apache Zeppelin/Spark 中可用的不同语言后端的元编程复杂性。
关于构建广泛的数据探索,如果我们希望更广泛地使用我们在 parquet 中缓存的结果,也有机会完全消除“眼睛观看图表”的需求。参见第十二章TrendCalculus,以了解我们如何可以以编程方式研究 GKG 中所有数据的趋势。
在使用 Zeppelin 时,一个需要注意的最后一个技巧是纯粹实用的。如果我们希望将图形提取到文件中,例如将它们包含在我们的最终报告中,而不是在笔记本中截图,我们可以直接从 Zeppelin 中提取可伸缩矢量图形文件(SVG),并使用此处找到的bookmarklet将它们下载到文件中nytimes.github.io/svg-crowbar/
。
可配置的 GCAM 时空 EDA
GCAM 的另一个问题仍然没有答案;我们如何开始理解它在空间上的细分?GCAM 的地理空间枢纽是否能够揭示全球新闻媒体如何呈现其聚合地缘政治观点,以详细的地理分析来深入国家级别的分析?
如果我们可以作为 EDA 的一部分构建这样的数据集,它将有许多不同的应用。例如,在城市层面上,它将是一个通用的地缘政治信号库,可以丰富各种其他数据科学项目。考虑假期旅行预订模式,与新闻中出现的地缘政治主题相结合。我们会发现全球新闻信号在城市层面上是否预测了媒体关注的地方的旅游率上升或下降?当我们将所得信息视为地缘政治态势感知的信息源时,这种数据的可能性几乎是无限的。
面对这样的机会,我们需要仔细考虑我们在这个更复杂的 EDA 中的投资。与以往一样,它将需要一个共同的数据结构,从而开始我们的探索。
作为目标,我们将致力于构建以下数据框架,以探索地缘政治趋势,我们将其称为“GeoGcam”。
val GeoGcamSchema = StructType(Array(
StructField("Date" , StringType, true), //$1
StructField("CountryCode" , StringType, true), //$2
StructField("Lat" , DoubleType, true), //$3
StructField("Long" , DoubleType, true), //$4
StructField("Geohash" , StringType, true), //$5
StructField("NewsLang" , StringType, true), //$6
StructField("Series" , StringType, true), //$7
StructField("Value" , DoubleType, true), //$8
StructField("ArticleCount" , DoubleType, true), //$9
StructField("AvgTone" , DoubleType, true) //$10
))
介绍 GeoGCAM
GeoGcam 是一个全球时空信号数据集,它源自原始的 GDELT 全球知识图(2.1)。它能够快速、轻松地探索全球新闻媒体情绪的演变地缘政治趋势。数据本身是使用一个转换管道创建的,该管道将原始的 GKG 文件转换为标准、可重复使用的全球时间/空间/情绪信号格式,这允许直接下游时空分析、地图可视化和进一步的广泛地缘政治趋势分析。
它可以用作预测模型的外部协变量的来源,特别是那些需要改进地缘政治情况意识的模型。
它是通过将 GKG 的 GCAM 情绪数据重新构建为一个空间定向模式而构建的。这是通过将每个新闻故事的情绪放置在其 GKG 记录中识别的细粒度城市/城镇级别位置上来完成的。
然后,数据按城市聚合,跨越 15 分钟的 GKG 时间窗口内的所有索引故事。结果是一个文件,它提供了在该空间和时间窗口内所有故事的聚合新闻媒体情绪共识,针对那个地方。尽管会有噪音,但我们的假设是,大而广泛的地缘政治主题将会出现。
数据集的样本(与目标模式匹配)如下:
+--------------+-------+------+--------+------------+
|Date |Country|Lat |Long |Geohash |
| |Code | | | |
+--------------+-------+------+--------+------------+
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
|20151109103000|CI |-33.45|-70.6667|66j9xyw5ds13|
+--------------+-------+------+--------+------------+
+----+------+-----+-------+----------------+
|News|Series|SUM |Article|AvgTone |
|Lang| |Value|Count | |
+----+------+-----+-------+----------------+
|E |c12_1 |16.0 |1.0 |0.24390243902439|
|E |c12_10|26.0 |1.0 |0.24390243902439|
|E |c12_12|12.0 |1.0 |0.24390243902439|
|E |c12_13|3.0 |1.0 |0.24390243902439|
|E |c12_14|11.0 |1.0 |0.24390243902439|
|E |c12_3 |4.0 |1.0 |0.24390243902439|
|E |c12_4 |3.0 |1.0 |0.24390243902439|
|E |c12_5 |10.0 |1.0 |0.24390243902439|
|E |c12_7 |15.0 |1.0 |0.24390243902439|
|E |c12_8 |6.0 |1.0 |0.24390243902439|
+----+------+-----+-------+----------------+
关于数据集的技术说明:
-
只有标记有特定城市位置的新闻文章才会被包括在内,这意味着只有那些被 GKG 标记为具有位置类型代码 3=USCITY 或 4=WORLDCITY 的文章才会被包括在内。
-
我们已经计算并包括了每个城市的完整 GeoHash(有关更多信息,请参见第五章*,地理分析的 Spark*),简化了数据的索引和总结,以供更大范围的地理区域使用。
-
文件的粒度是基于用于生成数据集的聚合键,即:
V21Date
、LocCountryCode
、Lat
、Long
、GeoHash
、Language
、Series
。 -
我们已经将 GKG 源中识别的主要位置国家代码字段传递到城市级别的聚合函数中;这使我们能够快速地按国家检查数据,而无需进行复杂的查找。
-
提供的数据是未归一化的。我们应该稍后通过位置的总文章字数来对其进行归一化,这在名为
wc
的系列中是可用的。但这只适用于基于字数的情绪测量。我们还携带了文章的计数,以便测试不同类型的归一化。 -
该数据源自英语 GKG 记录,但我们计划在相同的数据格式中包括国际跨语言源。为了做好准备,我们已经包括了一个字段,用于表示原始新闻故事的语言。
-
我们为这个数据集设计了一个摄入例程,用于 GeoMesa,这是一个可扩展的数据存储,允许我们地理上探索生成的数据;这在我们的代码库中是可用的。有关 GeoMesa 的深入探讨,请参见第五章*,地理分析的 Spark*。
以下是构建 GeoGCAM 文件的流水线:
// be sure to include a dependency to the geohash library
// here in the 1st para of zeppelin:
// z.load("com.github.davidmoten:geo:0.7.1")
// to use the geohash functionality in your code
val GcamRaw = GkgFileRaw.select("GkgRecordID","V21Date","V15Tone","V2GCAM", "V1Locations")
GcamRaw.cache()
GcamRaw.registerTempTable("GcamRaw")
def vgeoWrap (lat: Double, long: Double, len: Int): String = {
var ret = GeoHash.encodeHash(lat, long, len)
// select the length of the geohash, less than 12..
// it pulls in the library dependency from
// com.github.davidmoten:geo:0.7.1
return(ret)
} // we wrap up the geohash function locally
// we register the vGeoHash function for use in SQL
sqlContext.udf.register("vGeoHash", vgeoWrap(_:Double,_:Double,_:Int))
val ExtractGcam = sqlContext.sql("""
select
GkgRecordID
, V21Date
, split(V2GCAM, ",") as Array
, explode(split(V1Locations, ";")) as LocArray
, regexp_replace(V15Tone, ",.*$", "") as V15Tone
-- note we truncate off the other scores
from GcamRaw
where length(V2GCAM) >1 and length(V1Locations) >1
""")
val explodeGcamDF = ExtractGcam.explode("Array", "GcamRow"){c: Seq[String] => c }
val GcamRows = explodeGcamDF.select("GkgRecordID","V21Date","V15Tone","GcamRow", "LocArray")
// note ALL the locations get repeated against
// every GCAM sentiment row
GcamRows.registerTempTable("GcamRows")
val TimeSeries = sqlContext.sql("""
select -- create geohash keys
d.V21Date
, d.LocCountryCode
, d.Lat
, d.Long
, vGeoHash(d.Lat, d.Long, 12) as GeoHash
, 'E' as NewsLang
, regexp_replace(Series, "\\.", "_") as Series
, coalesce(sum(d.Value),0) as SumValue
-- SQL’s "coalesce” means “replaces nulls with"
, count(distinct GkgRecordID ) as ArticleCount
, Avg(V15Tone) as AvgTone
from
( select -- build Cartesian join of the series
-- and granular locations
GkgRecordID
, V21Date
, ts_array[0] as Series
, ts_array[1] as Value
, loc_array[0] as LocType
, loc_array[2] as LocCountryCode
, loc_array[4] as Lat
, loc_array[5] as Long
, V15Tone
from
(select -- isolate the data to focus on
GkgRecordID
, V21Date
, split(GcamRow, ":") as ts_array
, split(LocArray, "#") as loc_array
, V15Tone
from GcamRows
where length(GcamRow)>1
) x
where
(loc_array[0] = 3 or loc_array[0] = 4) -- city level filter
) d
group by
d.V21Date
, d.LocCountryCode
, d.Lat
, d.Long
, vGeoHash(d.Lat, d.Long, 12)
, d.Series
order by
d.V21Date
, vGeoHash(d.Lat, d.Long, 12)
, d.Series
""")
这个查询基本上做了以下几件事:它在 GCAM 情绪和记录中识别的细粒度位置(城市/地点)之间建立了一个笛卡尔连接,并继续将15 分钟窗口内所有新闻故事的 Tone 和情绪值放置在这些位置上。输出是一个时空数据集,允许我们在地图上地理化地映射 GCAM 情绪。例如,可以快速将这些数据导出并在 QGIS 中绘制,这是一个开源的地图工具。
我们的空间中心点是否有效?
当前面的 GeoGCAM 数据集被过滤以查看 GCAM 移民情绪作为 2015 年 2 月 GKG 数据的头两周的主题时,我们可以生成以下地图:
这说明了全球英语新闻媒体的语调,使用了轻(积极平均语调)和暗(消极平均语调),这些都可以在 GKG 文件中找到,并探讨了该语调如何在地图上的每个地理瓦片上映射(像素大小的计算相当准确地反映了分组的截断 GeoHash 的大小),并且与移民作为主题的情绪相关。
我们可以在这张地图上清楚地看到,移民不仅是与英国地区相关的热门话题,而且在其他地方也有强烈的空间集中。例如,我们可以看到与中东部分明显突出的浓厚负面语调。我们还看到了以前可能会错过的细节。例如,都柏林周围有关移民的浓厚负面语调,这并不是立即可以解释的,尼日利亚东北部似乎也发生了一些事情。
地图显示,我们也可能需要注意英语语言的偏见,因为非英语国家的讨论很少,这似乎有点奇怪,直到我们意识到我们还没有包括跨语言的 GKG 源。这表明我们应该扩展我们的处理,以包括跨语言数据源,以获得更全面和完整的信号,包括非英语新闻媒体。
GCAM 时间序列的完整列表在此处列出:data.gdeltproject.org/documentation/GCAM-MASTER-CODEBOOK.TXT
。
目前,在 GeoGCAM 格式中检查的英语新闻数据提供了对世界的迷人视角,我们发现 GDELT 确实提供了我们可以利用的真实信号。使用本章中开发的 GeoGCAM 格式化数据,您现在应该能够轻松快速地构建自己特定的地缘政治探索,甚至将此内容与您自己的数据集集成。
总结
在这一章中,我们回顾了许多探索数据质量和数据内容的想法。我们还向读者介绍了与 GDELT 合作的工具和技术,旨在鼓励读者扩展自己的调查。我们展示了 Zeppelin 的快速发展,并且大部分代码都是用 SparkSQL 编写的,以展示这种方法的出色可移植性。由于 GKG 文件在内容上非常复杂,本书的其余部分大部分致力于深入分析,超越了探索,我们在深入研究 Spark 代码库时也远离了 SparkSQL。
在下一章,也就是第五章,“地理分析的 Spark”,我们将探索 GeoMesa;这是一个管理和探索本章中创建的 GeoGCAM 数据集的理想工具,以及 GeoServer 和 GeoTools 工具集,以进一步扩展我们对时空探索和可视化的知识。