着火的大陆
使用 BigQuery Geo Viz 可视化野火
这张图片是在 知识共享 许可下授权的
澳大利亚的野火已经烧毁了超过 1070 万公顷的土地。客观地说,这比苏格兰或韩国的面积还要大。破坏的规模确实是空前的。这场大火已经夺去了几十条生命和五亿多只动物的生命。
这个周末,当我看着悉尼港上空的雨时,我不禁想知道是否有一种方法可以让我成为这个挑战的长期解决方案的一部分。火灾一直是这片土地的特色,而且只会越来越严重。作为小小的第一步,我想至少想象一下这些火灾发生在哪里。
进入 BigQuery 的魔力。
构建数据管道
BigQuery 托管各种不同的数据集,这些数据集可以公开访问。这是一个很好的数据探索资源。在这个存储库中,我找到了一个来自 NASA 的数据集,它提供了关于全球野火位置的数据。
该数据集涵盖了位置(纬度和经度),以及上周检测到的火灾的各种属性。对于那些希望更深入了解的人,我从下面的 NASA 网站上剽窃了广告词。我说得再好不过了。
可见红外成像辐射计套件(VI IRS)375m(vnp 14 imgtdl _ NRT)主动火力产品是最新加入公司的产品。它提供来自 NASA/NOAA Suomi 国家极地轨道伙伴关系(Suomi NPP)和 NOAA-20 卫星上的 VIIRS 传感器的数据。375 m 数据补充了中分辨率成像光谱仪(MODIS)火灾探测;它们在热点探测方面都表现出良好的一致性,但 375 米数据的空间分辨率的提高对相对较小区域的火灾提供了更大的响应,并提供了较大火灾周界的改进绘图。375 米的数据也提高了夜间性能。因此,这些数据非常适合用于支持火灾管理(例如,近实时警报系统),以及需要改进火灾绘图保真度的其他科学应用。推荐阅读: VIIRS 375 m 主动火力算法用户指南
数据集的大小约为 40mb。与 Bigquery 能够处理的万亿字节和千兆字节规模的数据集相比,这实在是小得可笑。
我决定用一个简单的 SQL 语句将所有数据复制到一个项目中。
CREATE OR REPLACE TABLE
`as-ghcn.nasa_wildfire.past_week` AS
SELECT
ST_GEOGPOINT(longitude,
latitude) AS longlat,
*
FROM
`bigquery-public-data.nasa_wildfire.past_week`
我需要将纬度和经度数据(FLOAT
值)转换成GEOGRAPHY
数据类型。这是形成数据并为 BigQuery Geo Viz 做好准备所需的唯一一点预处理。我将查询安排在每周一运行,并用 NASA 的最新数据更新数据集。
探索数据
BigQuery Geo Viz 是一个使用 Google Maps APIs 在 BigQuery 中可视化地理空间数据的 web 工具。您可以运行 SQL 查询并在交互式地图上显示结果。这是一个轻量级工具,允许您快速、交互式地浏览空间数据。我无法强调这有多痛苦。没有基本地图的配置或加载。
下面的查询加载了数据集中被高置信度检测到的所有火灾。我放大了澳大利亚的东海岸来提供一个可行的例子。该样式仅用于示例目的。
澳大利亚东海岸的野火
由于底层的基本地图是由谷歌地图支持的,因此有许多丰富的纹理特征可供用户使用,例如提供地形和卫星信息的视图。
澳大利亚维多利亚州的大火
你应该知道这个工具有一些限制。
- Geo Viz 在地图上最多只能显示 2,000 个结果。
- Geo Viz 支持众所周知的文本(WKT)格式的几何输入(点、线和多边形),存储在
STRING
列中。您可以使用 BigQuery 的地理功能将纬度和经度转换为 WKT。 - 实时、交互式分析由您的浏览器在本地处理,并受浏览器功能的限制。
- Geo Viz 不支持与其他人共享可视化、保存可视化或下载可视化进行离线编辑。
我发现最后一个限制非常令人失望,我希望产品团队将来能让 GCP 的客户也能使用它。
展望未来
放眼澳大利亚之外,我想看看世界其他地方是否有大火在燃烧。乍一看,中非和东南亚的大部分地区似乎都着火了。这肯定会成为全球新闻吧?
我在网上简单查了一下,你看到的火灾很可能是低强度的农业火灾。农民们通常会点燃农田,烧掉剩余的草和灌木。这种燃烧有助于保持肥沃的土壤。我记得我在坦桑尼亚的时候。
虚惊一场?也许吧,但我们无法确定,因为世界上许多地方都没有报道或报道不足。
基于 Theil-Sen 回归的相关性度量
来源:https://www.tylervigen.com/spurious-correlations
A 关联和相关测量是描述性统计和探索性数据分析的重要工具。它们为变量之间的函数关系提供了统计证据。
一个流行的测量方法是皮尔逊相关系数 r(⋅,⋅。对于度量数据样本 x 和 y ,给出如下:
其中 s(⋅,⋅) 是样本协方差, s(⋅) 是样本标准差。
如果我们把 n 观测值 x 和 y 的序列看作欧几里得空间 ℝⁿ 中的向量,那么相关系数就可以被几何解释。首先,我们通过重新调整 x 和 y 来标准化样本,如下所示:
其中 μ(⋅) 是算术平均值(从每次观察中减去)。这里我们假设使用无偏样本协方差,即因子为 1/(n - 1) 的样本协方差。然后,我们发现归一化样本位于单位球面上,相关系数是它们的点积:
这种高维几何解释有助于理解皮尔逊相关的一些重要性质。例如,我们立即理解,该度量允许我们比较归一化样本所指向的方向: r(x,y) = 1 表示它们指向相同的方向, r(x,y) = -1 表示它们指向相反的方向。如果 r(x,y) 消失,则矢量 z(x) 和 z(y) 正交。我们甚至可以通过关系式 r(x,y) = cos(φ) 计算归一化样本之间的“关联角度” φ 。
相关和回归
皮尔逊相关系数的另一个有用解释来自基于最小二乘法的简单线性回归。(其他解释见罗杰斯和尼斯旺 1988 。)在最后一段中,我们将两个样本视为两个维度为 n 的向量。我们现在要把它们看成是二维的 n 个数据点。如果我们使用最小二乘法来确定通过这些数据点的最佳拟合直线,其中我们将 y 视为响应变量,将 x 视为解释变量,则该直线的斜率为 m(y,x) = s(x,y) / (s(x)) 。反之,如果我们把 x 作为响应变量,把 y 作为解释变量,回归线就会有斜率 m(x,y) = s(x,y) / (s(y)) 。首先,我们观察到,无论哪个变量被视为因变量或自变量,回归线的斜率都不会改变符号。这一点,再加上一点代数,让我们得出结论:
简而言之:皮尔逊系数是最佳拟合直线斜率的(带符号)几何平均值,您可以通过最小二乘法绘制数据点。这个等式非常清楚地表明,皮尔逊系数测量了 x 和 y 线性相关的程度。它还表明,皮尔逊系数与最小二乘法一样容易受到异常值的影响:它通常不被认为是稳健的统计量。Anscombe quartet 是一个旨在展示此类漏洞的合成测试数据集。尽管四个数据集以散点图的形式呈现非常不同,但基于最小二乘法的回归线是相同的:
来源:维基百科
简单线性回归的最小二乘法的另一种选择是 Theil-Sen 估计。这种更稳健的方法通过所有可通过数据点绘制的直线的斜率的中值来确定回归线的斜率:
因此,使用皮尔逊相关系数和最小二乘回归之间的上述关系,我们现在可以制定一个与泰尔-森估计相关的变量:
如果中值斜率具有相同的符号,否则为零。然而,在斜率的符号改变时将测量值设置为零的条件可能看起来是人为的:
- 当交换 x 和 y 时,中间坡度通称为不改变符号,
- 当它出现时, x 和 y 之间的肯德尔等级相关性消失。
这可以看如下。假设单个斜率的 K 为负,N-K为正。交换 x 和 y 意味着取斜率的倒数。对斜率排序以计算其中值揭示了以下关系:
我们可以看到,只有当负斜率和正斜率一样多时,中间值的符号才会发生变化。然而,这意味着肯德尔等级相关性τ消失,这可以从该度量的以下重写中看出:
我们可以在 Anscombe 四重奏上评估这个基于 Theil-Sen 的相关系数,并将其与 Pearson 系数进行比较。下表显示了这种比较,以及基于等级统计的另外两种流行的度量,Spearman 的ρ和已经提到的 Kendall 的τ:
可能有点令人惊讶的是,对于 Anscombe 4 数据集,Theil-Sen、Spearman 和 Kendall 相关性测量被证明是高度不稳定的:即使是最小幅度的任何噪声添加到数据点都会产生任意的相关性值。对于基于等级的 Spearman 和 Kendall 测量,这可以很容易地解释:如果大多数的 x 值是相同的或几乎相同的,数据中的微小变化可以产生任意的等级。
另一方面,Theil-Sen 估计器对回归线 m(y,x) 的斜率产生任意大的值,对 m(x,y) 产生任意接近于零的值,这导致在受到噪声影响时产生不明确的乘积。
一般来说,如果至少有一个要比较的样本只显示出很小的离差,那么相关测量将变得不可靠。对于稳健的相关性测量,这种离差应该通过稳健的方法来测量。Anscombe 4 数据集内的 x 值的中值绝对偏差消失,因此 Theil-Sen 估计相关性和等级相关性度量不明确也就不足为奇了。
摘要
稳健简单线性回归的 Theil-Sen 估计可用于定义相关性度量,类似于 Pearson 相关系数与最小二乘回归的关系。在 Anscombe 四重奏上进行评估,它显示出类似于等级相关性度量的特征,例如 Spearman 的ρ或 Kendall 的τ。
参考文献
安斯科姆,F. J. (1973)。“统计分析中的图表”。美国统计学家。27 (1): 17–21.
罗杰斯;西弗吉尼亚州尼斯旺德(1988 年)。“看相关系数的十三种方法”。美国统计学家。42 (1): 59–66.
森·p·k .(1968)。“基于肯德尔τ的回归系数估计值”。美国统计协会杂志。63 (324): 1379–1389.
希尔,H. (1950 年)。“线性和多项式回归分析的秩不变方法”。内德尔。阿卡德。韦滕施。,继续。53: 386–392.
在 1 毫秒内达到 99.9%准确率的 COVID 测试——如何实现?
你甚至不需要去看医生。这项测试结果总是阴性。
来源:Getty Images
截至目前,美国有超过 43.5 万例 COVID 病例,美国人口为 3.27 亿。这大约是感染 COVID 人口的 0.13%,相当于每 1000 人中有 1.3 人受到影响。虽然这听起来确实是一个相当大的百分比,但是一个一直显示阴性的测试只有 0.13%的情况是错误的,或者对其余的 99.87%是正确的!
塞犍陀·维韦克
或者,公式中正确结果的数量将是(327,000,000–435,000 ),用这个数字除以美国人口=327,000,000 得出 0.9987 或 99.87%。这告诉我们准确性可能不是衡量测试有效性的最佳方式。那还有什么?
统计数据不会说谎。但是统计数据可能会被曲解。
精密
假设我有一个更好的测试。如果你超过 85 岁,有潜在的疾病,正在经历咳嗽,发烧,疲劳,呼吸困难,需要尽快接上呼吸机,你可能患有 COVID,你不需要我告诉你去医院并被隔离。这个测试比总是阴性的测试更加精确。
塞犍陀·维韦克
再次以美国的 Covid 案例为例。在疾控中心昨天发布的数据中,住院病例比例最大的来自于>85;85 岁以上的 10 万人中有 17 人住院。这如何转化为 85 岁以上住院的美国人总数?根据老龄管理局的数据,2016 年 85 岁以上的美国人有 640 万。这相当于大约(17/100,000)*6,400,000 = 1,088 名 COVID 超过 85 岁的住院患者。假设我有一个测试,说这 1088 人有 COVID。尽管它遗漏了具有 COVID 的(435,000–1,088)中的其余部分,但它非常精确。虽然这比显示所有人都是阴性的测试稍微好一点,但也没有好到哪里去。那么我的统计工具箱里还有什么呢?
召回
我检测了美国所有人的 COVID,只发现 1088 人呈阳性(85 岁以上和住院),这意味着我错过了所有其他人。然后我把测试结果为阴性的放回他们的社区,不进行隔离。后来,我发现我的测试遗漏了大量受感染的人。我该怎么办?我应该召回那些人,直到他们开始出现更严重的症状,或者直到我确定他们不会传染给其他人。同时希望能有更好的测试。对这些我最初遗漏的阳性病例,错误地标记为阴性的测量被称为召回,顾名思义就是。它与召回被认为是好的设备或汽车有关,但结果证明是有问题的,如电池烧毁或安全气囊问题。
塞犍陀·维韦克
在测试的例子中,精确地确定 1088 名患者为阳性,但忽略了其余的阳性人,则召回将是 1088/(435000)= 0.002 或 0.2%。这太令人沮丧了。所以这很好地确定了我们的测试没有那么好。理想的情况是,我们希望所有的阳性患者都被立即确认,以及我们社区中所有其他的阳性患者,我们不知道。但那是不可能的。相反,统计学家试图在精确度(我应该相信多少阳性结果)和回忆(被正确识别的感染百分比)这两个最重要的因素之间找到平衡。
F1 分数
精确度和召回率之间的平衡是两者之间的调和平均值,称为 F1 得分。
塞犍陀·维韦克
在上面的例子中,对于精确测量的 1088 名患者,precision=1,recall=0.002。F1 得分=2*(1*0.002)/(1+0.002)=0.004。这表明测试做得不好,但比仅仅回忆好一点。
然而,这绝不是全部情况。考虑相反的情况。天理不容;美国有 3 亿人患有 COVID。记住美国人口是 3.27 亿。让我们考虑相反的第一个例子:一个分类器,说每个人都是积极的——并迅速重新计算所有的分数:
- **准确度。**我已经正确识别了 300,000,000 个具有 COVID 的。然而,我错误地将 27,000,000 个额外病例识别为阳性,尽管它们是阴性的。精度为 300,000,000/327,000,000 =0.92 。
- **精度。**这种情况下精度相同 =0.92 (感谢@lena 的修正)。
- **回忆。**我不需要回忆任何积极的东西,因为我现在正确地识别了所有积极的东西+ 27,000,000。召回 =1
- F1 比分。由于 F1 得分只是精度和召回率的组合,所以 F1 得分= 2 *(1 * 0.92)/(1+0.92)= 0.96
奇怪的是,在这个例子中,当我们面对相反的问题时,所有的测试似乎都失败了!然而,这并不是最糟糕的情况——更多的人被隔离总比没有好。这说明了阶层不平衡的问题。更高级的测试包括马修斯相关系数,这是一个更复杂的公式。该系数是一个平衡的度量,即使班级大小相差很大也可以使用。
总之,我已经展示了统计数据是如何经常误导人和难以解释的。简单的测试准确度是不够的,要谨防社交媒体上传播虚假信息和神奇测试的文章。流行病学家、疾控中心的人、医生以及医院都应该得到掌声。他们必须收集数据,进行统计分析,并以清晰、易懂的方式同时以正确的方式传达所有这些信息;因此,我们不会错误地解读数据显示的内容。下一次有人说某项测试有 90%的准确率时,想想为什么它可能没有那么有用。
编者按: 走向数据科学 是一份以数据科学和机器学习研究为主的中型刊物。我们不是健康专家或流行病学家,本文的观点不应被解释为专业建议。想了解更多关于疫情冠状病毒的信息,可以点击 这里 。
来源:
- https://en . Wikipedia . org/wiki/2019% E2 % 80% 9320 _ 冠状病毒 _ 疫情 _ 按国家和地区
- https://www.cdc.gov/mmwr/volumes/69/wr/mm6915e3.htm
- https://ACL . gov/sites/default/files/Aging % 20 和% 20 disability % 20 in % 20 America/2017 olderamericansprofile . pdf
版本控制和 Git & GitHub 速成班
约翰·霍普斯金 DS 专业化系列
关于版本控制系统、Git & GitHub 和 VCS 的最佳实践
由 Unsplash 上 Greg Rakozy 拍摄的照片
[Full series](https://towardsdatascience.com/tagged/ds-toolbox)[**Part 1**](/the-data-scientists-toolbox-part-1-c214adcc859f) - What is Data Science, Big data and the Data Science process[**Part 2**](/how-to-learn-r-for-data-science-3a7c8326f969) - The origin of R, why use R, R vs Python and resources to learn[**Part 3**](/a-crash-course-on-version-control-and-git-github-5d04e7933070) - Version Control, Git & GitHub and best practices for sharing code.[**Part 4**](/the-six-types-of-data-analysis-75517ba7ea61) - The 6 types of Data Analysis[**Part 5**](/designing-experiments-in-data-science-23360d2ddf84) - The ability to design experiments to answer your Ds questions[**Part 6**](/what-is-a-p-value-2cd0b1898e6f) - P-value & P-hacking[**Part 7**](/big-data-its-benefits-challenges-and-future-6fddd69ab927) - Big Data, it's benefits, challenges, and future
本系列基于约翰·霍普斯金大学在 Coursera 上提供的 数据科学专业 。本系列中的文章是基于课程的笔记,以及出于我自己学习目的的额外研究和主题。第一门课, 数据科学家工具箱 ,笔记会分成 7 个部分。关于这个系列的注释还可以在这里找到。
介绍
如果你是计算机科学的初学者,或者你是一个刚刚开始学习编程基础的初学程序员,无论你是在学习 C、Swift、Python 还是 JavaScript,你肯定会在你的在线课程或 YouTube 视频教程中遇到版本控制。一开始,Git 和 GitHub 这两个词对你来说可能是陌生的,但是一旦你开始编写代码并在线分享,你就会在日常生活中使用它们。这里有一个版本控制和 Git 的快速介绍,以及学习它们的资源。
Git & GitHub
Git 和 GitHub 是每个程序员、数据科学家、机器学习工程师、web 开发者、全栈开发者等事实上的版本控制工具。用途。
Git 是什么?
由于它的速度和灵活性,Git 基本上是一个非常强大的源代码控制系统。虽然您可以通过使用 Git 的基本命令来完成这项工作,但是对于初学者来说,这可能是相当具有挑战性的,
虽然 Git 的名字是 GitHub,但它们是两个独立的东西,因为 Git 在 GitHub 之前就存在了。还有其他地方可以托管您的 Git 存储库,但是 GitHub 是最受欢迎和使用最广泛的站点。
什么是 GitHub?
因此 GitHub 只是一个让你管理你的 git 仓库的网站,还有很多其他的平台,像 BitBucket,Digital Ocean,Azure DevOps,但是 GitHub 有漂亮的用户界面并且很容易使用。
一个人能做什么 GitHub?
在 GitHub 上,您可以:
- 在线共享代码,并在改进程序时不断更新它,不受本地机器的限制,
- 为你的雇主创造一个震撼的投资组合,让他们惊叹你的能力。
- 它也是一个充满学习资源、解决方案和在线课程笔记等的地方。
- 为开源项目做贡献,培养你的技能。
- 与其他开发人员合作开发酷项目
- 叉很酷的项目,并以此为乐
- 等等。
为了有效地一起使用 GitHub 和 Git,您必须理解版本控制到底是什么,以及最常用的命令来增强您的工作流,使您成为一个更有效的程序员。
版本控制
卢克·切瑟在 Unsplash 上的照片
什么是 VCS?
版本控制系统(VCS)顾名思义很直观,它基本上是一个帮助你跟踪源代码或文件变更的工具集合。它维护了变更的历史,并促进了与其他开发人员的协作*。***
你可以把 VCS 的工作想象成对你的代码和文件中的变更进行快照,并把信息存储为解释为什么做出变更以及是谁做出变更的消息。
为什么用 VCS
也就是说,VCS 非常乐于助人,主要有三个原因。
- 你的作品历史快照。—如果您想回顾项目的旧快照,以了解为什么进行了更改,同时像 Google Drive 一样作为备份存储。它还允许你在任何地方搜索你的代码或项目,只要你能登录到你的 GitHub 账户。
- 允许协作并处理冲突 —编程时协作是巨大的。协作时的一个主要问题是冲突,当两个人处理同一个文件,并上传他们各自的更改时,只有一个更改会被做出,而另一个会丢失。像这样的问题由 Git 处理,你可以在 GitHub 干净的 UI 上查看所有的问题和请求。
- 分支实验 —有时你想尝试一些新的东西,所以你会想复制你的代码,改变一些东西,而不影响你的原始源代码。这叫做分支,和 app 向公众发布 beta 程序一样,是主程序的分支。
基本 VC 词汇
- Repository —项目文件夹或目录,所有 VC 文件都在这里
- 提交— 保存所做的编辑和更改(文件快照)
- 推送 —用您的编辑更新存储库
- 拉 —将您的本地版本的回购更新到当前版本
- 暂存 —准备提交文件
- 分支 —具有两个同步副本的相同文件,创建一个分支以进行尚未与主回购共享的编辑
- 合并 —分支合并回主回购,同一文件的独立编辑合并成一个统一的文件
- 冲突 — 当两个人编辑同一行代码时,就会发生冲突,要么保留其中一个,要么保留另一个
- 克隆(仅在本地机器上) —制作包含所有跟踪变更的 repo 副本
- Fork(保留在 GitHub 中) —另一个人的回购的副本,编辑记录在您的回购上,而不是他们的
所以,VCS 的一个基本流程是——文件存放在与其他编码者在线共享的仓库里。你克隆你的回购,让本地拷贝编辑它。在做出更改之后,您存放文件并提交它。然后,你推动提交共享回购协议,在那里新文件在线,并有消息解释什么改变了,为什么改变,由谁改变。
Git 命令
如果有一天你想成为一名优秀的程序员,你应该记住一些基本的 git 命令。
以下是将 repos 克隆到本地机器时将使用的命令列表。
如果你还没有把你的机器连接到你的 GitHub 账户,那么在学习 Git 之前先这样做。这里有一篇约瑟夫·罗宾逊写的关于如何做到的文章。
基础
*git help (get help for a command)# creating a new repo1\. git init (let's you create a new git repo, stored in .git.)
2\. git add . (add your files)
3\. git commit -m "message"
4\. git push # cloning from repo on GitHub1\. **git clone** "url"
2\. git status (tell's you which files have been staged)
3\. **git add .** (use . to add all unstaged files, specify files with <filename>)
4\. **git commit -m "message"** 5\. **git push***
这些命令是非常基本的,如果你想了解更多,可以参考这个神奇的网站。
大多数人使用终端来运行这些命令,但也有其他选择来做 VCS,比如如果你喜欢点击而不是打字,可以使用 GitHub 桌面。ide 也很有用,因为它们内置了 Git 和 GitHub 特性,你可以无缝地使用它们。
VCS 的最佳实践
当你刚开始使用 VCS 时,你可能不知道什么时候提交代码,或者如何最好地使用 VCS,所以这里有一些有用的提示。
- 有目的地提交 —你的提交应该是有目的的,而不是琐碎的,它应该对你的代码做出实质性的改变。例如,键入一行新代码并提交是无效的,并且会妨碍您的工作效率。
- 关于提交的信息性消息 —您的提交应该有充分解释为什么做出某个更改的消息,以便将来您可以回去参考它。这也有助于其他人合作理解这些变化。
- 经常拉和推 —在进行公开回购时,拉是很重要的,这样你就可以随时了解最新的变化,你也应该经常推,让你的变化在 GitHub 上得到更新。
- 更多提示此处
摘要
照片由 Fotis Fotopoulos 在 Unsplash 上拍摄
获得并掌握 Git 对你成为程序员、数据科学家、web 开发人员等非常有帮助。
编写程序、创建游戏、网站、个人作品集、机器学习模型、移动应用程序等,所有这些都需要版本控制系统来帮助你有效地管理它们,当你开始与他人合作时,你会发现这是多么重要。
Github 是一个令人惊叹的网站,它不仅可以作为你的作品集,GitHub 上有大量的宝石,共享一个巨大的资源列表,这对初学者特别有帮助。
如果你正在寻找很酷的东西,GitHub 收藏是一个很好的起点。
如果您正在寻找数据科学资源,请查看我的文章。
感谢阅读,我希望你学到了一些东西。一定要看下一部分,记得注意安全。
学习 Git 的资源
由斯科特·沙孔和本·施特劳布撰写并由 Apress 出版的 Pro Git 整本书可以在这里找到。所有内容…
git-scm.com](https://git-scm.com/book/en/v2) [## 给初学者的 Git:权威实用指南
锁住了。这个问题和它的答案被锁定是因为这个问题跑题了但是有历史意义。它…
stackoverflow.com](https://stackoverflow.com/questions/315911/git-for-beginners-the-definitive-practical-guide) [## 版本控制
版本控制系统(VCS)是用来跟踪源代码(或其他文件和…
missing.csail.mit.edu](https://missing.csail.mit.edu/2020/version-control/) [## 饭桶
Git 是一个分布式开源源代码控制(也称为“版本控制”)系统,通常用于跟踪…
www.fullstackpython.com](https://www.fullstackpython.com/git.html) [## 学习 Git 分支
一个交互式 Git 可视化工具,用于教育和挑战!
learngitbranching.js.org](https://learngitbranching.js.org) [## 了解 Git- Git 教程、工作流和命令| Atlassian Git 教程
定义:一个分支代表一个独立的开发路线。分支是对…的抽象
www.atlassian.com](https://www.atlassian.com/git)
如果您对学习数据科学感兴趣,请查看“超学习”数据科学系列!
这是一个简短的指南,基于《超学习》一书,应用于数据科学
medium.com](https://medium.com/better-programming/how-to-ultralearn-data-science-part-1-92e143b7257b)
查看这些关于数据科学资源的文章。
[## 2020 年你应该订阅的 25 大数据科学 YouTube 频道
以下是你应该关注的学习编程、机器学习和人工智能、数学和数据的最佳 YouTubers
towardsdatascience.com](/top-20-youtube-channels-for-data-science-in-2020-2ef4fb0d3d5) [## 互联网上 20 大免费数据科学、ML 和 AI MOOCs
以下是关于数据科学、机器学习、深度学习和人工智能的最佳在线课程列表
towardsdatascience.com](/top-20-free-data-science-ml-and-ai-moocs-on-the-internet-4036bd0aac12) [## 机器学习和数据科学的 20 大网站
这里是我列出的最好的 ML 和数据科学网站,可以提供有价值的资源和新闻。
medium.com](https://medium.com/swlh/top-20-websites-for-machine-learning-and-data-science-d0b113130068) [## 开始数据科学之旅的最佳书籍
这是你从头开始学习数据科学应该读的书。
towardsdatascience.com](/the-best-book-to-start-your-data-science-journey-f457b0994160) [## 数据科学 20 大播客
面向数据爱好者的最佳数据科学播客列表。
towardsdatascience.com](/top-20-podcasts-for-data-science-83dc9e07448e)
连接
如果你想了解我的最新文章,请通过媒体关注我。
也关注我的其他社交资料!
请关注我的下一篇文章,记得保持安全!*
一份简历不足以让你得到一份数据科学的工作
如何在简历之外成为更有吸引力的数据科学候选人
Felicia Buitenwerf 在 Unsplash 上拍摄的照片
介绍
在我上一篇文章的后面, 找到一份数据科学的工作比以往任何时候都难 *,*我在 LinkedIn 上收到了很多关于如何对雇主更具吸引力的回复。
以及如何利用找工作的困难成为你的优势
towardsdatascience.com](/getting-a-data-science-job-is-harder-than-ever-fb796aae1922)
在获得数据科学工作方面,我绝不是专家,但我非常幸运地接触到许多从事数据科学家工作的人,在招聘领域担任招聘人员的人,以及每天审查数百份申请的经理。
许多人可能不同意,我讨厌打破它。一份简历是远远不够的。
通过追溯单词resume viate(CV)的来源,我发现这是一个拉丁短语,大致翻译为“我的生命历程”。换句话说,简历非常适合总结经验、成就和个人技能。对你生活的初步总结可能会让你表现出兴趣——以面试的形式(或公司采取的任何阶段)——然而,兴趣并不总是超越吸引力。
为了明确这两个术语之间的区别,我将兴趣定义为从方法上评估一个人是否适合长期投资的好奇心,我们将吸引力定义为身体或情感上渴望接近某人的迹象,从商业角度来说,这意味着无论是谁雇佣你都希望你加入他们的团队。
为了获得下一份工作,我一直在和各种各样的人交谈,考虑如何提升自己,让自己成为对雇主更有吸引力的候选人,从而增加我获得更理想职位的机会,以下是我想到的两个常见的想法:
为开源做贡献
为开源做贡献是立即从雇主那里获得更多吸引力的一个很好的方式,因为这告诉他们如果他们雇佣你,他们能从你那里得到什么。
这里有一个关于现代社会的简单事实(也许它只适用于技术工作,但我不确定),无论你申请哪里,鉴于他们喜欢你的简历,随之而来的是谷歌搜索。
当你的名字在谷歌上被搜索时,你想得到的第一件事就是数据科学。
开源的本质很自然地把我们推到了舒适区的边缘。无论是修复 scikit-learn 框架中的一个错误,还是为 Tensorflow/PyTorch 创建一个初学者指南,或者部署您自己的机器学习 API,贡献(以及持续贡献)都会让我们立即对雇主更具吸引力,因为它展示了任何雇主都想要的 3 个主要特征:
- 你很好奇
- 你是一个初学者
- 你决心提高自己/是一个积极主动的人
我们在总结部分简单地写下“我是一个好奇的人,喜欢学习”的日子已经一去不复返了,当我们积极地为社区做贡献时,这些事情实际上是推断出来的,因为我们被视为一个想要推动社区发展的人。
注意:对开源做出贡献的一种常见方式是一个文档化良好的项目
创建有效的自述文件
towardsdatascience.com](/how-to-make-your-data-science-projects-stand-out-b91d3861a885)
网络
很可能你的理想工作会有成百上千的其他人申请,就像你一样,他们相信自己非常适合这个角色,因此吸引公司的注意力是找工作时要克服的最重要的障碍之一。
我喜欢 T4 的杰米·哈里斯的说法:
“因此,求职不仅仅是一场技能竞赛:在很大程度上,这也是一场带宽竞赛。”
与人建立真诚的关系是非常重要的,因为如果他们将你的申请推荐给他们的公司,这很可能会比那些通过一些工作网站上的 easy apply 提交申请的人更有影响力。
我个人认为,最好的方式往往是通过已经在你感兴趣的公司工作的人,尤其是如果你可以完全跳过简历筛选这一步——这就是我如何获得上一份工作的,我是根据经验说的。
在 Covid 改变世界之前,meetup 是我经常去的地方——即使我有工作,我也会去 meetup,不是因为我在搜索,而是因为建立你的联系和发展关系在任何行业都很重要。因此,我主要是在 LinkedIn 上,我现在意识到这并不是那么糟糕。
一个典型的数据科学职位发布可以吸引数十甚至数百份申请。出于这个原因,最…
towardsdatascience.com](/what-every-aspiring-data-scientist-needs-to-know-about-networking-475cfaac15f8)
注意:上网时做好自己。不要说你不会当面说的话——那很奇怪。
包裹
对我来说,通过积极地把自己放在外面,把求职掌握在自己手中,是让自己成为更有吸引力的候选人的方法,因为这需要大量的承诺,在我看来,这让你在雇主面前更可信。另一方面,一份经过精心优化的简历最有可能获得许多雇主的关注,并仍有可能获得许多机会。
你同意/不同意这个帖子吗?让我们继续 LinkedIn 上的对话…
[## Kurtis Pykes -人工智能作家-走向数据科学| LinkedIn
在世界上最大的职业社区 LinkedIn 上查看 Kurtis Pykes 的个人资料。Kurtis 有一个工作列在他们的…
www.linkedin.com](https://www.linkedin.com/in/kurtispykes/?originalSubdomain=uk)
涉足推特分析
快速探索 Twitter 上容易获得的数据,使用数据科学方法优化您的推文:一些可视化、相关性和 NLP。
发微博是我们很多人都在做的事情,但是我们当中有多少人真正去探究是什么让我们的微博成功了呢?我想相对于那些知道的人来说,相对来说很少。我一直在自学一些数据科学技能,我想在一些真实数据上测试我的技能。
我想,我感兴趣的东西会是一个很好的开始:我经常使用 Twitter,大部分时间都在发推特,所以在我看来尝试一下是有意义的。另一个关键点是,我最近发现我可以点击一个按钮导出我的推文数据,这样有助于决定做什么!
我敢肯定,我不是一个人说,我听到很多人说,如果你有更多的标签,你会得到更大的影响,或者如果你在你的推文中加入媒体,人们更有可能参与进来。我想弄清楚这是不是真的,但也想利用我已经开始掌握的一些数据科学技能。
现在,我肯定还有更详细的分析,来告诉你你的推文表现,在这里我将告诉你我的思考过程和我的发现。
获取数据
我没有注册 Twitter 的开发者账户,我使用的数据对所有用户开放。在你的电脑上,打开 Twitter,点击左侧面板中的“更多”,然后选择“分析”。
指向分析的 Twitter 截图
一旦你这样做了,一个新的标签将会打开。点击页面顶部的“tweets”链接,它会告诉你你的 Tweets 表现如何,最重要的是一个导出数据的按钮。我导出了按“Tweets”而不是“Day”分类的数据,因为这是我更感兴趣的。
Twitter 的屏幕截图,用于指导导出数据
这里要注意一点:虽然你可以在 Twitter 分析界面上查看长达 3 个月的数据,但你只能导出其中的 28 天。很遗憾,但不是世界末日。
初始清洁和探索
与所有数据项目的开始一样,我们需要知道我们已经获得了哪些数据,并对其进行清理,以便它处于适合我们使用的状态。一开始,我有许多列数据,其中许多我并不关心。
我删除了所有只包含空值的数据,以及所有与“推广”推文相关的列。我不推广我的任何推文,所以这与我无关。我期望所有这些都是 null,但是数据在那里放了一个’-',所以我最初的删除没有影响这些列。在我最初的筛选和清理之后,我只剩下了下面这些我想要的格式的专栏。主要是,这意味着丢掉我不感兴趣的东西,确保数字就是数字,日期就是日期。我做的唯一更复杂的操作是将给定的“时间”列分成日期和时间部分。
初步查看我的数据
过去,关于我的推文的影响范围,我想到了几个问题:是否有一天/一个月的某个时间意味着我的推文会更成功?我发现一个快速的情节是获得正在发生的事情的快照的最简单的方法。
绘图显示几天内的印象数
事实证明,绘制每条推文在一段时间内获得的印象数并没有多大帮助。我认为这是由多种因素造成的:上个月我只发了大约 80 条微博,而我只有这么短时间的数据。更有用的数据表示可能是查看一天中每个小时的印象数:
绘图显示一天中不同时间的印象数
现在,这更有趣了——我可以看到我在上午和下午之间最频繁地发微博,而在晚上很少。通常(除了这里的一个异常)在这段时间的推文。这很可能是因为在这段时间里,我的目标受众中有更多的人是清醒和活跃的。我知道我在世界各地都有联系人——我在学术 Twitter 环境中特别活跃,所以理论上说,一天到晚都有观众!如果你发微博的频率比我高,或许一天中比我多一点,这个图可能对你更有用。
剩下的数据呢?
上面,我只看了其中的两列,但是其他的呢?比较两列的所有组合效率太低。回顾这些数据,我想看的东西和变量有很多,它们可能彼此相关,因此相关矩阵似乎有助于这种情况:
数值数据的相关图
在开始看矩阵之前,在你的头脑中有一个好主意,你的因变量和自变量是什么。这将帮助你看到你可以改变和影响什么来创造你想要的效果。
看这个相关图,互动和媒体互动之间有很高的相关性(0.97)。这意味着人们越来越多地参与媒体的推文,但这是有意义的参与吗,即我关心的参与,即喜欢/回复/关注等。查看媒体参与栏,媒体参与与除 url 点击之外的所有点击都呈正相关(这是意料之中的,因为 twitter 显示推文的方式:媒体优先于 URL)。我的推文中的媒体与我最关心的参与度的喜欢和关注度最高。
URL 点击与数据中的所有其他属性具有低相关性或负相关性。看起来带 url 的推文最有可能被转发,而不是其他任何东西。
自然语言处理
如果我想了解我的实际推文在说什么,就需要进行一些文本分析。我想看看标签的影响,以及它们如何影响我的推文的“成功”。我还想看看我的推文的情绪是否与它们的成功有任何关系,即我快乐的推文是否比我不快乐的推文更成功?
为了解决第一点,我收集了所有的推文,并计算了每条推文中的标签数量。我将这个值作为一个新列添加进来,并重新运行我的相关矩阵。
数字数据的相关图,加上标签数
标签的数量与印象的数量最相关,其次是转发和标签点击。一般来说,更多的标签意味着更高的印象,这是有道理的,但由于这不是一个高相关性,这表明并不是所有的标签都对印象有用。
考虑到我的推文的情感,我使用了一个内置函数来为推文文本赋值。在大约 80 条推文中,超过一半是正面的,最少的是负面的:
显示发送的推文的情绪的图
但这对我在推特上获得的印象和有意义的参与有什么影响呢?
带有数字数据的关联图,包括标签号和推文情感值
简而言之:不太多。所有的价值观和情绪之间都有轻微的负相关,除了一个:回复。然而,这里的相关性是如此之小,以至于我发送的推文数量不太可能对我的推文成功产生重大影响。
我发现了什么?
从这个初步的分析中,我想我发现了以下几点:
- 我发推特的时间可能是“成功”的标志,但我需要更多的数据来确定这一点。从我有限的数据来看,上午晚些时候到下午三点钟似乎得到了最多的印象。
- 媒体可能会增加我的推文的参与度。
- 仅仅增加一条推文中的标签数量并不能保证这条推文更“成功”——关键是要仔细挑选标签。
- 我的推文有多开心对它们的“成功”没有重大影响。
最重要的是,我已经很好地利用了我的数据大脑,将我的知识应用到现实世界的例子中,并有可能提出一个更“有效”的推特策略。
基于这一分析,我不能说我会在如何发推特上有很大的改变:我会更肯定地包括媒体,我会对我使用的标签很挑剔——无论如何我都会尝试这样做。
毫无疑问,更深入的分析可能会提供更多关于我如何才能更成功的信息。或许我可以看看“最成功”的标签?或者我可以收集更多的数据来观察一段时间内的模式。当然,有许多选择可以探索。目前,这个初步的分析,使用了我一直在学习的一些技巧,给了我 Twitter 性能的一个基本指标。
如果你对我如何实现我的分析感兴趣,请随时联系/评论,我很乐意分享。我用 Python 做了所有的事情,因为它是我的首选语言,但毫无疑问还有其他的方法。
民主辩论的数据分析
戴维·拉古萨在 Unsplash 上的照片
谁在说话,他们在说什么?
随着民主党初选的升温和辩论人数的增加,很难理解候选人在说什么。然而,由于有了在线辩论记录,检索曾经说过的每一句话变得相当容易。
让我们处理和分析文本数据,探索到目前为止发生了什么,而不是进行阅读这些文字记录的枯燥练习。
数据采集和处理
以下是我获取和处理分析数据的 5 个步骤。我建议您跳过这一部分,除非您对技术细节感兴趣:
- 从一个名为 Rev.com的网站复制每场辩论的文本,并将数据存储在一个 python 字符串中。
- 在空白处拆分每个辩论字符串,并为发言者创建一个列表,为他们的相关引用创建一个列表。
- 将演讲者和引语列表压缩在一起,并将其转换为熊猫数据帧,其中每行代表候选人的一个不间断行。
- 将辩论数据帧结合在一起,为每个辩论添加一个标识符。
- 清理和规范候选人姓名。
分析
谁在说话?
让我们先来看看每位候选人在整个辩论中的参与程度。为了衡量参与度,我们将计算每位候选人的发言占总字数的百分比。我们将只包括出现在大多数辩论中的参与者,并排除前两次辩论,因为它们都发生在两个晚上。
首先映入我眼帘的是大量的辩论。在总共近 4000 名代表中,只有 65 名代表获奖,候选人已经 9 次登上舞台。同样值得注意的是,速度已经从 2019 年底的每月一场辩论增加到 2020 年的每两周一场。
随着辩论频率的加快,发言的分布更加紧张。在第三场辩论中,领先的候选人(拜登)和落后的候选人(桑德斯)之间的差距为 12%。到第九次辩论时,这一比例仅为 3%——基本上是五方平手。
这可能是两种情况的产物。首先,范围缩小了,主持人更容易确保候选人有平等的发言时间。其次,随着最近几周赌注的增加,候选人变得更加直言不讳。
另一个有趣的趋势是——对于许多候选人来说——所说的单词的%与主要表现相关。
这方面最强有力的例子是伊丽莎白·沃伦,她的平均民调支持率从 10 月份的 22%骤降至 2 月份的不到 13%,这反映了她说的话的百分比从 24%降至 14%。
虽然很难确定其中的因果关系,但这种相关性可能有几个原因。也许主持人会根据谁在民意调查中领先来调整他们的提问行为。或者可能是候选人缺乏攻击性让潜在选民望而却步。最后,候选人可能倾向于挑战和激怒他们认为最大威胁的竞争对手。
不管原因是什么,沃伦的团队很可能在内华达州之前发现了这一趋势——在内华达州,她是所有候选人中发言次数第二多的。
另一方面,从 9 月到 2 月,随着他的辩论参与率稳步上升,伯尼·桑德斯在全国民调中的平均支持率呈指数上升:
他们都说了些什么?
既然我们已经看了谁在讲话,让我们感受一下他们都在谈些什么。
首先,我们来看一个单词云,它向我们展示了候选人辩论中最常用的单词。我已经排除了停用词——没有太多语义价值的常用词——并采用了一种叫做词条化的方法来将单词简化到它们的核心意思,而不管它们的词性。
这里没有什么大的惊喜——候选人试图吸引 人 ,并解释他们 认为 什么,他们 打算 做什么,作为 总统 让这个 国家 变得更好。
也许更有见地的分析应该是看看候选人的用词是如何变化的。下面,我创建了一些更频繁出现的单词以及每个候选人使用它们的频率的热图。颜色越深,候选人使用的单词越多。
让我们浏览一下 10 个最有趣的单词和要点:
- **人物:**桑德斯用这个词最多。这是有道理的,因为他被视为一个平民主义候选人,期待开始一场政治革命。
- 总统:Buttigieg 使用“总统”这个词的频率最高——也许是作为一种被认为是最“总统化”的策略
- 想一想:克洛布查尔以压倒性优势赢得了这场比赛。她经常以“我认为”作为自己观点的开头。我认为这与其说是缺乏自信,不如说是一种风格上的问题。
- 需要:沃伦把自己定位成一个能干的、解决问题的人,她知道这个国家及其人民需要什么*。也许这也是她使用“家庭”和“工作”这两个词最多的原因。*
- 特朗普:沃伦和拜登最少提到总统的名字。我想知道这是否是一个有意的举动,因为特朗普似乎受益于降名,有好有坏。
- **完成:拜登和克洛布查尔谈了很多关于他们所做的。这是有道理的,因为他们的吸引力很大一部分是经验。拜登从 1972 年到 2008 年担任了 8 年的副总统和参议员。Klobuchar 在明尼苏达州担任了三届参议员,是参议院中最活跃的人之一。这是与 Buttigieg 或 Steyer 等传统经验较少的候选人的一大区别。
- ****事实:拜登使用填充短语“事实上”的频率很高,是第二名的两倍多。
- 气候:对于那些不太熟悉斯泰尔的人来说,这可能是一个惊喜,但是这位亿万富翁民主党人说他的首要问题是气候变化,并且他在辩论中支持这一点。
- 医疗保健:桑德斯在这一领域领先。他是拥有最独特的医疗保健计划(全民医保)的候选人,而且他不得不花大部分时间来捍卫其成本和可行性。使用“医疗保健”一词第二多的沃伦也同意桑德斯的计划,但自以来,她的立场已经软化。
- 变革:这一点非常接近,因为大多数民主党人认为自己是变革的代表。但同样,桑德斯是最有兴趣改变政党和国家政治方向的候选人。
后续步骤
我在上面所做的仅仅触及了辩论笔录的表面。以下是扩展这一分析的三个想法:
- 探究候选人的词汇用法是如何随着时间的推移而变化的。
- 使用像 word2vec 这样的单词嵌入来衡量每个候选人之间的差异。
- 建立一个“谁说的”分类模型来预测哪个候选人说了给定的报价。
用 R 创建个人网站的数据分析师指南
忘记 Wix 或 WordPress,利用你已经知道的东西来编写你自己的站点
如果你看这篇文章有困难,你可以通过下面的链接访问我的网站,绕过 Medium 的付费墙。尽情享受吧!
数据分析师
www.bobbymuljono.com](https://www.bobbymuljono.com)
李·坎贝尔在 Unsplash 上的照片
你个人网站的重要性
对数据科学工作的需求一直在上升,申请人也加入了这一行列。由于硕士或博士是最主要的筛选条件,人力资源经理会毫不犹豫地拒绝简历平平的申请人。
一个展示你处理数据科学或分析项目能力的网站是从其他申请者中脱颖而出的一个很好的方式,尤其是如果你没有任何高等学位。
但是打住,我不知道 HTML/CSS/JS 建一个网站
如果你正在阅读这篇文章,那么你可能对数据分析感兴趣,或者你已经在处理大量的数据管理工作,并且你不会以任何特定的方式处理 HTML/CSS 代码。
无论您是数据分析师还是数据科学家,我们都同意一件事:我们的一般工作包括理解业务问题,使用定制的 SQL 查询提取必要的数据,使用 Python/R 清理和验证它们,最后可视化它们或执行某种预测以增强运营业务决策。
通常这个过程就是我们所说的模型级分析。这是大多数公司在将数据转化为可操作的见解时所寻求的基本要求。在这种情况下,您可能只需要处理很少的文件和脚本来提取和处理数据。
然而,当我们想到在移动中实现数据科学时,如 Youtube 的推荐系统,我们需要开始研究协作和 生产级脚本。这就是当您与一组数据科学家一起工作时,文件管理和版本控制变得非常重要的地方,这些科学家在一个给定的项目中分担他们的工作量。
当您构建网站时(我不是在说 Wix 或 WordPress 这样的高级网站构建者),您将处理大量 HTML/CSS 文件,这些文件将存储在您的计算机或 Github 存储库中以供部署。这是习惯生产级脚本的好方法。
好消息是。
你不需要知道任何 HTML/CSS 来用我们很多人都熟悉的东西,R 编程或 Python 来构建网站。然而,某些布局定制可能需要用户冒险进入 HTML 并修改几行代码。
你将学到什么
- 基本 Git 和 Github 导航
- 基本的 RStudio IDE(真见鬼,你甚至不需要接触任何 R 代码)
- 创建具有预定义主题的内容(这是困难的,但是一旦你习惯了,就可以管理)
- 网站部署和维护
在我开始之前,我必须表扬该表扬的地方。在我建立自己的网站之前,我从来没有想过用 R 来做这件事是可能的,因为 R 不是为建立网站而设计的。我只是浏览了 TDS 上的几篇文章,偶然发现了叶君写的这篇文章,他用 r
使用 R Blogdown 的简单指南
towardsdatascience.com](/get-your-own-website-online-in-four-steps-adef65abe8bd)
我从他的教程中得到了一些提示,但是我用一个稍微不同的工作流程建立了我的网站:
记住整个工作流程,你应该能够创建一个自己的网站。
步骤 1–1:设置 R、RStudio 和您的项目
如果您的桌面上还没有 RStudio,那么像大多数其他程序一样,将它安装到您的系统中是非常容易和直观的。但是不要忘记在获得 IDE 之前下载 R GUI。您可以通过下面的链接获得该软件:
控制你的 R 代码一个集成的 R 开发环境,有一个控制台,语法高亮编辑器…
rstudio.com](https://rstudio.com/)
启动并运行 RStudio 后,转到 console 选项卡并输入以下代码:
#To install blogdown package
install.packages("blogdown")#To install hugo within blogdown
blogdown::install_hugo()
然后,转到 IDE 的左上角,单击以下内容:
文件→新项目→新目录→使用 blogdown 的网站
在这个窗口中,你可以指定你的目录名,我把它叫做‘git projects’。接下来,您需要从这里选择一个 hugo 主题:
编辑描述
themes.gohugo.io](https://themes.gohugo.io/)
在 Hugo theme 文本框中,您需要指定所选主题的 Github 存储库,例如,上面的图片链接到 github.com/易慧/hugo-lithium 。
完成后,‘git projects’文件夹将被主题源代码和 RProject 可执行文件填充。稍后这将被手动转移到克隆的 Github 存储库文件夹中。
步骤 1–2:设置 g ithub 存储库并将其与 Git 集成
我没有必要重复从以下链接中的其他人那里获得的任务:
[## 如何正确设置您的 Github 资源库— Windows 版本
作为一名数据科学家,我经常与有志于成为数据科学家的人打交道,通常我的一个…
medium.com](https://medium.com/@aklson_DS/how-to-properly-setup-your-github-repository-windows-version-ea596b398b)
总之,你需要设置一个 Github 库并安装 Git Bash 终端,以实现从桌面到库的源代码变更的无缝管道。
不过,有些事情我做得有点不同。不要键入命令:
mkdir ~/Desktop/projects
在文件夹‘Git projects’下,我已经有了要克隆存储库的目录,所以我在 Git Bash 终端中键入了以下内容:
cd ~/Desktop/"Personal Stuff"/GitProjects
之后,我使用新的 Github 库提供的链接克隆了我的库。
一旦完成,一个与你的 Github 库同名的新文件夹将出现在“GitProjects”文件夹中。
但是记得我把所有的主题源代码和 RProject 文件放在同一个文件夹里,把所有的东西都移到新创建的文件夹,我的 Github 文件夹叫做**“my website”**,在成功地把所有东西都移过来之后,继续把目录换到新的文件夹里,如下所示:
当您在目录末尾看到(主)分支时,您就知道您已经连接到 Github 存储库了。
此时,您已经成功地用 Git 连接到 Github,但是您新导入的文件还没有被识别。
步骤 2:将主题源代码添加到 Github 存储库中
现在,您需要将主题源代码添加到 Github 存储库中。您的本地文件夹应该如下所示:
在您的 Git Bash 终端中使用以下命令将所有文件添加到您的存储库中:
git add <foldername>
某些文件不是必须添加的,但是如果是,就在你的中添加。gitignore 文件,以便网站部署引擎不会将它们应用到您的网站上:
.Rproj.user
.Rhistory
.RData
.Ruserdata
public
最后,您现在可以开始为您的站点创建内容了!
步骤 3:创建/定制内容和布局
这是最难的部分!每个主题都应该有一个文档来导航整个源代码。您可以在 Rstudio 控制台中键入以下代码,在 Rstudio 的**“查看器”**选项卡中预览您的站点:
blogdown::serve_site()
对于初学者来说,最受欢迎的主题是“学术”主题,它有全面的文档,并得到了社区的大力支持。对于简历展示和个人博客来说,这是一个非常通用的主题。事实上,您可以查看下面的视频,了解 Rstudio 中“学术”主题的详细介绍:
请注意,视频有点过时,某些代码被放在不同的文件中。我最初选择这个主题是为了练习,但后来为了更好的布局,我换了另一个主题。我不会深入到关于内容创建和布局定制的教程中,因为它应该在主题文档中有清楚的解释。
步骤 4:提交您的变更,并将变更推送到您的存储库中
一旦您对微小的更改感到满意,在 Git Bash 终端中使用以下命令将您的更改提交到 Github 存储库中:
git commit <foldername or filename> -m "Your update message here"
当您对所有的更改感到满意后,在 Git Bash 终端中输入以下代码,以使更改在 Github 存储库中发生:
git push
就是这样!每次在 RStudio 中对网站进行更改时,您只需在 Git Bash 终端中重新输入这些命令。
步骤 5:使用 Netlify 部署您的网站
Netlify 是一个万无一失的网站部署解决方案,它可以自动将您的文件从 Github 存储库转换为网站。
只需确保您连接到了正确的存储库,并且准备就绪。但是在部署成功之前,您需要为 Netlify 设置正确的环境变量来识别文件结构。
为此,请转到 Netlify 仪表板,转到设置→构建和部署→环境,并设置以下环境变量。
不过,你的雨果版本可能和我的不一样。要检查您的 hugo 版本,请返回 RStudio 并在控制台中键入以下命令:
blogdown::hugo_version()
最后一步:自定义域名
部署附带的免费域名是 *.netlify.com 格式。如果不介意子域挂在身边,可以跳过这一步。否则,您可以从 DNS 提供商(如 NameCheap)注册您的域名。对我来说,我坚持使用 Netlify,因为这是一个更便宜的选择。com 扩展名。但是你应该总是做你的研究,找到什么是最适合你的网站。您可以在此阅读更多关于您网站的域名注册信息:
[## c 域名| blogdown:用 R Markdown 创建网站
虽然您可以使用 GitHub 或 Netlify 提供的免费域名,但拥有一个
bookdown.org](https://bookdown.org/yihui/blogdown/domain-name.html)
摘要
随着您不断使用 Git 和 RStudio 更新您的站点,您的后续工作流应该如下所示:
正如承诺的那样,对于一个有预定义主题的基本网站,你不需要了解 HTML/CSS/JS 的广泛知识,尽管如果你想改变字体、重新定位某些单词和自定义颜色,这样做是有帮助的。
对于我(一个 Windows 用户)来说,我总是使用友好的“F12”按钮来检查元素并找到负责该特定布局的文件。通过这种方式,我学会了阅读一些 HTML 代码,并做了一些修改,这样我的网站看起来比原来更加个性化。
最后但并非最不重要的一点,我鼓励你向你选择的主题的所有者进行小额捐赠。一些主题创作者允许你把他们的名字从版权标志上去掉,只要你为他们的主题捐款,这样你就可以把你的网站称为你自己的创作。同时,这也是对他们辛勤工作的一种感谢,这样你的网站才能看起来漂亮。
编码快乐!
理解网络攻击的数据驱动方法
在 VERIS 社区数据集上使用 Pandas 的探索性指南
照片马库斯·斯皮斯克上下
保护让社会正常运转的数字系统、应用和网络免受有意和无意的伤害是一个非常复杂的问题,仅靠人类的直觉是无法实现的。
网络安全领域正在发生一场觉醒,这种觉醒是由同样的商品推动的,这种商品曾导致亚马逊、脸书和谷歌等公司成为世界上最强大的私营公司: Data 。
数据科学在网络安全领域的应用为组织更好地保护自己免受网络威胁提供了重要的机会。一个常见的用例是实现机器学习模型来监控恶意用户行为,这是一个不适合人类分析师执行的任务,尤其是对于拥有数千名员工的大型组织。
在本文中,我使用 Python 和 Pandas 来探索由大约 8500 个真实世界网络事件组成的 VERIS 社区数据集,以回答一个广泛的研究问题:“网络事件的共同属性是什么?”。通过回答这个简单的问题,我希望展示如何利用数据将一个复杂的问题分解成微小而可行的见解。
导入和配置数据
我通过将 GitHub 上的整个 VERIS repo 克隆到我的本地环境来导入数据。因为每个事件都存储在一个单独的 JSON 对象中,所以我使用了有用的 verispy 包将数千个 JSON 对象提取到一个 pandas 数据帧中:
import pandas as pd
from verispy import VERISdata_dir = '/Users/danielharrison/VCDB-master/data/json/validated'
v = VERIS(json_dir=data_dir) #creates a veris objectFound 8539 json files.veris_df = v.json_to_df(verbose=True) #creates a dataframe from the veris object
检查数据框架揭示了 VERIS 社区数据集的范围(截至 2020 年 1 月):
veris_df.shape()
(8539, 2347)
1.谁在引发网络事件?
了解谁会有意或无意地对数字资产造成潜在损害,是解决研究问题和探索数据的自然起点。
VERIS 数据库将网络威胁因素分为三类:
1.外部-组织之外的任何人,例如黑客、国家和前雇员
2.内部-受托访问内部系统的任何人,例如全职员工、承包商和实习生
3.合作伙伴——受影响组织的第三方供应商,通常对内部系统有一些可信的访问权限。
为了按参与者类型分解事件,我利用了 verispy 软件包提供的enum_summary
函数:
df_actors_internal = v.enum_summary(veris_df, 'actor.internal.variety', by='actor')df_actors_external = v.enum_summary(veris_df, 'actor.external.variety', by='actor')
注意:我忽略了包括合作伙伴行动者类型,因为由合作伙伴引起的事件总数很低,这影响了以后的可视化。
在对df_actors_internal
和df_actors_external
数据帧进行了一些清理之后(更多信息参见我的 GitHub 代码,我将它们连接成一个数据帧df_actors_combined
:
df_actors_combined = pd.concat([df_actors_internal, df_actors_external])df_actors_combined.head()
串联数据帧 df_actors_combined 包含内部和外部参与者
由于 Plotly express 图形库,以视觉上令人惊叹的交互式方式呈现这些数据变得非常容易。我选择用一个旭日图来展示内部和外部参与者是如何被进一步分解成他们的参与者类型的:
import plotly.express as pxfig_1 = px.sunburst(df_actors_combined, path=['Actor Origin', 'Actor Type'], values='Count')fig_1.update_layout(uniformtext_minsize=12)plot(fig_1, filename = 'Actor Origin', auto_open=True)
内部和外部参与者被进一步分解为他们的角色或联盟
关于在页面中嵌入可视化的更多信息,我推荐下面的 文章 。
显而易见的是,由内部和外部行为者引起的事件几乎是平均分布的,52%是外部的,47%是内部的(大约 1%是合作伙伴&未知)。
赛博域深度挖掘 1
组织不能低估内部威胁带来的风险,他们对内部系统的特权访问和工作知识使他们很容易造成伤害-无论他们是否有意。
对于内部参与者的已知数据,很大一部分可归因于“最终用户”,VERIS 文档将其描述为应用程序的最终用户或正式员工。我将此解释为拥有系统标准访问权限并在日常工作中使用该系统的员工。
有趣的是,根据我在 DevOps 环境中的经验,考虑到软件开发人员所拥有的高级访问和工作知识,我预计由他们引起的事故数量会高得多。例如,开发人员可以修改软件应用程序的底层源代码,以某种方式为自己谋利,或者干脆将密码和 API 密钥等秘密暴露在 GitHub 上托管的代码中:
在 Github 上搜索“删除密码”会发现很多秘密被留在源代码中的例子
为了了解开发人员是恶意行为还是仅仅是犯了错误,我通过过滤内部开发人员的veris_df
数据框架和他们的行为进一步分析了数据:
df_actors_developers = v.enum_summary(veris_df, 'action', by='actor.internal.variety')df_actors_developers = df_actors_developers[df_actors_developers['by'] == 'actor.internal.variety.Developer']df_actors_developers.plot(kind='bar', x='enum', y='x', legend=False)plt.xticks(rotation=25)
plt.ylabel('Count')
plt.savefig('df_actors_developers')
大多数由开发引起的事件是由错误引起的,很少是恶意的
生成的条形图显示,在 73 起与开发人员相关的事件中,56 起是错误(即意外),其余 17 起与误用、黑客攻击和恶意软件(即恶意软件)有关。有帮助的是,VERIS 提供了一个事件摘要,我从 14 个标记为“误用”的事件中提取了该摘要,并将其输出到一个 csv 文件中,以便于阅读:
df_actors_developers_misuse = veris_df.loc[(veris_df['actor.internal.variety.Developer'] == True) & (veris_df['action.Misuse'] == True)]df_actors_developers_misuse['summary'].to_csv('developers_misuse_summary.csv', index=False, header=False)
一个特殊的事件是由美国的一名高级 IT 主管引起的,他也是一名开发人员。该开发商:
“2005 年,他在“随机”数字生成计算机软件中添加了一个秘密代码,使他能够将多场比赛的中奖概率从高达 500 万分之一缩小到 200 分之一……他在科罗拉多州、威斯康星州、爱荷华州、堪萨斯州和俄克拉荷马州劫持了至少五张中奖彩票,奖金总额超过 2400 万美元”
确实非常淘气。[我已经将剩余的摘要上传到了我的 GitHub
本次网络领域深度探讨展示了我们如何从询问“谁造成了网络事件”这一非常普通的问题,一直到发现源代码完整性的价值。
2.什么样的行为会导致网络事故?
在探讨了开发人员通常如何引发网络事件后,我后退一步,分析了所有外部、内部和合作伙伴可能采取的威胁行动。这是很有用的,因为组织需要知道他们的内部和外部威胁可能如何实现,这样他们就可以实施足够的保护控制。
我想用更多的 Plotly 的交互式图表来形象化这种洞察力,并决定 Sankey 图将优雅地显示演员和动作之间的关系和流动。为了创建一个 Sankey,我过滤了 actor 和 action 上的veris_df
数据帧:
df_action_actor = v.enum_summary(veris_df, 'action', by='actor')
稍微整理了一下生成的数据帧:
df_action_actor.drop(['n', 'freq'], axis=1, inplace=True)df_action_actor.columns = ['Actor Origin', 'Action Type', 'Count']df_Unknown_3 = df_action_actor[df_action_actor['Actor Origin'] == 'actor.Unknown']df_action_actor.drop(df_Unknown_3.index, inplace=True)
并使用映射函数来阻止代码在每个单词前输出“actor ”,即“actor”。外在,演员。内部,演员。合作伙伴’:
map_origin = {'actor.External':'External', 'actor.Internal':'Internal', 'actor.Partner':'Partner'}df_action_actor['Actor Origin'] = df_action_actor['Actor Origin'].map(map_origin)df_action_actor.head()
生成的数据帧现在可以生成桑基图,如下所示:
df_action_actor 数据帧存储了 actor 源和它们可能的动作类型之间的关系
我将上面的df_action_actor
数据帧传递给了预建函数 gen_Sankey
,指定了生成桑基图中的级别和值的列,并通过调用plot
输出了结果图:
fig_4 = genSankey(df_action_actor, cat_cols=['Actor Origin', 'Action Type'], value_cols='Count', title='Sankey Diagram for Veris Community Database')plot(fig_4, filename = 'Cyber Actions Sankey Diagram', auto_open=True)
Sankey 图表明,数据集中的一些事件被分配了多种操作类型,因为内部、外部和合作伙伴事件的数量与之前的图表相比略有增加(按操作类型划分数据时,记录的事件有约 12%的差异)。
赛博域深潜 2
该图显示,约 90%与内部行为者相关的事件是由错误或误用造成的。这种洞察力告诉我们,安全部门可以预期内部威胁会以用户行为的形式出现,而不是恶意软件或黑客活动,后者通常需要略有不同的监控技术。这就是为什么用户行为分析(UBA)领域最近出现了爆炸式增长,并使组织能够检测到用户在一段时间内相对于其他用户或自己的异常行为。
与外部行为者相关的事件是由一系列更加多样化的行为引起的。这是有意义的,因为参与者必须采用更具创造性的方法来获得对系统的访问,以实现他们的结果。“黑客”行动似乎有点模糊:黑客到底意味着什么?我们能分离出黑客攻击的一些趋势吗?为了回答这些问题,我必须向数据帧添加一个日期时间索引,并过滤由黑客行为引起的事件(产生的数据帧称为combined
)。有关我如何做到这一点的更多信息,请参考我的 GitHub ,因为它太长了,无法包含在本文中。
清理数据集后,我能够提取以下图形:
ax1 = combined.iloc[:, 2:10].plot(kind='bar', stacked=True, figsize=(10,5))
2006-2019 年黑客活动导致的网络事件频率
图表显示了更多关于什么行为构成“黑客攻击”的细节。拒绝服务(DoS)攻击在 2013 年和 2014 年困扰着组织,但最近,对数据集中的组织来说,威胁似乎已经变得不那么大了。这可能是因为反 DoS 技术近年来变得更加先进和流行:web 应用程序防火墙(waf)能够对传入的 Web 流量进行速率限制,通常由云供应商在内容分发网络(cdn)、应用程序负载平衡器和 API 网关上作为标准提供。
然而,VERIS 数据集现在可能会过时,因为数据表明自 2013 年以来黑客攻击一直在稳步下降。有大量其他数据集和统计数据表明情况并非如此。事实上,作为一个从事网络安全工作并对各种组织有看法的人,我可以凭第一手经验说,网络攻击的数量正在上升。
3.组织检测和响应网络事件需要多长时间?
能够及时检测和响应网络事件可以拯救整个组织免于破产。检测和响应网络事件花费的时间越长,组织面临的风险就越大,潜在的危害也就越大。
VERIS 将网络事件分为 4 个阶段,并记录组织达到该阶段所需的时间单位:
- 妥协:参与者已经获得对信息资产的访问权或妥协,例如获得对销售数据库的访问权
- 泄露:参与者从受害者组织获取非公开数据的点(不适用于所有事件)
- 发现:组织意识到事件已经发生的时间点
- 遏制:组织阻止事故发生或恢复正常业务的点。
N.B. VERIS 仅记录到达每个阶段的时间单位,即秒、分、小时,出于保密原因,不记录实际时间戳。
我想了解组织检测和响应网络事件需要多长时间,并使用热图表示这些信息。
为此,我首先必须从数据集中提取时间轴信息。我编写了一个函数(get_timeline_df(x, event)
),该函数根据事件的特定阶段过滤数据集,并为下一阶段的处理格式化结果数据帧。关于get_timeline_df
功能的更多详情,请参见 my GitHub 。我调用了这个函数 4 次,事件的每个阶段调用一次:
compromise_time = get_timeline_df(‘timeline.compromise.unit’, ‘Compromise’)discovery_time = get_timeline_df('timeline.discovery.unit', 'Discovery')exfiltration_time = get_timeline_df('timeline.exfiltration.unit', 'Exfiltration')containment_time = get_timeline_df('timeline.containment.unit', 'Containment')
然后将 4 个数据帧连接成一个:
timeline_df = pd.concat([compromise_time, discovery_time, exfiltration_time, containment_time])
timeline_df.head()
生成的数据帧如下所示:
串联时间轴 _ 测向数据帧
在get_timeline_df
函数中,我将字符串时间单位映射到一个从 1 到 8 的整数值,即‘秒’:1、…、‘天’:4、…、‘从不’:8,这样我就可以从最长到最短的时间跨度对值进行排序。
通过以矩阵形式传递数据,可以在 seaborn 中创建热图。我使用pd.pivot()
函数将timeline_df
数据帧转换成矩阵,根据事件的 4 个阶段重新索引数据,并将数据从最长时间单位到最短时间单位排序:
timeline_matrix = timeline_df.pivot('Time Unit', 'Timeline Event', 'Count')timeline_matrix.columnscolumns_matrix_titles = ["Compromise","Exfiltration", "Discovery", "Containment"]timeline_matrix = timeline_matrix.reindex(columns=columns_matrix_titles)timeline_matrix.sort_index(ascending=False, inplace=True)
生成的矩阵如下所示:
根据 VERIS,时间轴矩阵按网络事件的四个阶段进行索引
现在,我简单地将矩阵传递给 seaborn 的热图函数,并将时间单位重新标记为字符串值,以便于理解:
import seaborn as snsfig_heatmap = plt.figure(figsize=(10,8))r = sns.heatmap(timeline_matrix, cmap='BuPu', cbar_kws={'label': 'Count'}, linewidths=.05)plt.yticks([7.5,6.5,5.5,4.5,3.5,2.5,1.5,0.5], ['Seconds', 'Minutes', 'Hours', 'Days', 'Weeks', 'Months', 'Years', 'Never'], rotation=0)
产生了以下热图:
热图显示了网络事件在最初发生数月后才被发现的总体趋势
赛博域深度潜水 3
从热图中显而易见的是,与组织意识到发生了任何事情相比,危害和泄露数据所需的时间更短。根据 FireEye 的数据,在首次攻击数月后发现的大量事件与公认的 2017 年行业平均 101 天发现网络事件有很好的相关性。
网络专业人士通常将最初的妥协和发现之间的时间称为“停留时间”。驻留时间数字如此之高的一个原因可能是因为被称为“高级持续威胁”或“APTs”的特定网络事件子集。apt 喜欢在一个组织的系统上长时间不被注意。留在那里有助于威胁参与者了解组织如何工作,以实现他们的目标,例如窃取数据。
为什么停留时间很重要?T4 最近的一篇论文指出,停留时间和网络攻击的平均成本之间存在关联。 停留时间越长,攻击的代价就越大。
组织可以使用平均停留时间作为 KPI 来衡量威胁检测控制的有效性。只有通过测量,组织才能真正了解他们的网络风险。
网络安全是一个数据问题
数字世界里发生的一切都会被记录下来。用户点击浏览器中的链接、员工三次输错密码、文件从一个网络位置传输到另一个网络位置,都是以数字格式记录的事件。这些数字事件可以用来保护组织免受网络事件的影响,但前提是用户、员工和系统每天产生的海量数据可以作为一种有益的工具来使用。
VERIS 社区数据集只是应用数据分析来保护数字系统的一个用例。通过观察数以千计的事件的总体趋势,组织可以了解威胁最有可能来自哪里,以及他们可能如何试图造成伤害。
正如杰弗里·摩尔所说-
没有大数据分析,公司就像瞎子和聋子,像高速公路上的小鹿一样在网络上游荡。
美国社会距离的数据驱动研究
在新冠肺炎疫情的最初几周,关于美国社会距离的有趣的、数据驱动的见解
布莱恩·麦高恩在 Unsplash 上的照片
本帖分享的分析和见解均来自safe graph分享的数据。SafeGraph 免费提供其各种数据集,以帮助世界各地的研究人员应对新冠肺炎(冠状病毒)。这些数据包括关于人们如何在美国各地移动的聚合和匿名数据集。如果你想志愿成为研究员,请在这里 报名 。
请注意,SafeGraph 提供的所有数据都是隐私安全和匿名的。为了增强隐私,如果在一个月内来自给定人口普查区块组的访问机构的设备少于五个,则 SafeGraph 会排除人口普查区块组信息。
新冠肺炎对流动性的影响
随着新冠肺炎病毒在美国迅速蔓延,查看移动数据以了解人们的行为如何随着社交距离政策的变化而变化是很重要的。
在本帖中,我将提供一些我从探索 SafeGraph 数据中收集到的有趣见解。
更多的人呆在家里吗?
数据范围为 2020 年 1 月 1 日至 2020 年 4 月 13 日
每日完全归位设备的百分比是使用 SafeGraph 的指标设备完全归位来计算的,该指标表明设备在一天中的移动没有超出其先前夜间位置的约 100 平方米。SafeGraph 还澄清说,当我们提到“家”时,我们实际上是指近几个月来最常见的夜间位置,其精度约为 100 平方米。我们不知道(也不想知道)匿名设备的确切家庭地址。
有明显的证据表明,越来越多的人呆在家里,保持着社会距离,然而,人们保持社会距离的程度因县、州等不同而有很大差异。(下文对此有更多讨论)。
更多人开始呆在家里的转变似乎也是在 2020 年 3 月 13 日之后开始的,这一天联邦政府宣布新冠肺炎进入全国紧急状态。
旅行的人变少了吗?
数据范围为 2020 年 1 月 1 日至 2020 年 4 月 13 日
不出所料,也有明显的证据表明人们正在减少旅行。从 2020 年 3 月 13 日以后,离家旅行的距离中位数也开始减少。
不同的州、县和城市之间的社交距离有什么不同?
待在家里指数将是我在下面一些图表中使用的指标。我通过观察 4 月第一周与 2 月第一周(基线)相比,完全在家的设备百分比的差异来计算。使用 7 天平均值来计算最终差异。居家指数为 0.2 表明,与 2 月的第一周相比,4 月的第一周有大约 20%的设备在家(基线)。这一指标的计算受到了 SafeGraph 在其 Shelter in Place 仪表板中使用的 Shelter in Place 指数的启发。
美国各州—居家指数
居家指数最高的前 5 个州是新泽西州、马萨诸塞州、马里兰州、罗德岛州和康涅狄格州。居家指数最低的 5 个州是南达科他州、蒙大拿州、怀俄明州、印第安纳州和新墨西哥州。
美国各县—居家指数
在各州内部,就不同的县如何实践社会距离而言,也有很多不同之处。更多的大城市和人口密集的县有更高的居家指数(更多信息见下文)。
美国城市——完全家庭化设备的百分比
这里的数据是美国人口最多的前 10 个城市的数据,范围从 2020 年 1 月 1 日到 2020 年 4 月 10 日
在这些城市中的大部分,人们在 3 月 13 日之后开始呆在家里。有趣的是,对于凤凰城等一些城市,官方的就地安置避难所命令直到 3 月 30 日才发布,但数据显示,鉴于新冠肺炎的肆虐,即使在这些城市中的一些城市,人们也从 3 月中旬开始呆在家里。
探索影响社会距离的因素
家庭收入中位数
这里的每个数据点代表一个美国邮政编码(n = 30,664)
这表明,能够呆在家里和在家工作是许多人的特权。与中等家庭收入较高的人相比,中等家庭收入较低的人不太可能呆在家里。
城市与农村人口
这里的每个数据点代表一个美国县(n = 3,228)
与农村人口比例较大的县相比,城市人口比例较大的县更有可能呆在家里。人口最稠密的县和城市已经显示出减少旅行最显著。
政治倾向(民主党对共和党)
这里的每个数据点代表一个美国县(n = 3,114)
有趣的是,在 2016 年总统大选中,民主党选民比例较大的县的人民也比共和党选民比例较大的县的人民更有可能呆在家里。这也与每个县的城乡人口比例有关。
不出所料,这些因素中有很多可能是相互交织、相互关联的,但需要注意的一点是,一个社区实施社交距离的程度是由很多外部因素决定的。虽然越来越多的人呆在家里,减少了全国各地的旅行,但数据显示,较富裕的城市工人呆在家里的时间最多,而农村地区收入较低的工人并不总是享受同样的奢侈。
使用的数据源
其他关于社交距离和移动性的有趣仪表板/报告
管理 ML 项目的数据工程方法
使用 DevOps 电池的数据科学项目
项目管理技能的展示
最近, 劳伦斯·莫罗尼 建议专业人士通过在 GitHub 上发起一个涵盖你投资组合相关方面的项目来展示你的技能,并通过工作让你的专业网络了解你。我采纳了他的建议,在 GitHub 上做了一个项目。我写这篇文章是为了激励他人,也是为了建立我自己的投资组合。我将介绍这个项目的主要组成部分。下面是我的 GitHub 项目的链接。
用机器学习预测零售店销售额的 Python 项目。这个项目是基于提供的数据…
github.com](https://github.com/amjadraza/retail-sales-prediction)
零售预测
有了数据工程和科学背景,我从 Kaggle 竞赛中选择了一个问题来预测一家零售店的销售额。
任务是使用厄瓜多尔的 Corporación Favorita 杂货店 连锁店提供的数据建立零售预测。数据是为 Kaggle 比赛提供的。我按照以下步骤来执行数据工程和建模。出于研究目的,我使用了 Jupyter 笔记本和 PyCharm IDE。
以下是我在这个项目中遵循的一些步骤。
- 关于法乌瑞塔杂货店的简要信息
- 探索性数据分析
- 特征工程和选择
- 模型训练和选择
- 准备/部署销售预测模型
我已经包括了用于探索性数据分析、特性工程和模型构建的 Jupyter 笔记本。
除了研究部分,我还使用 PyCharm IDE 和audreyr的python Project cooki cutter建立了 Python 项目。Cookiecutter 项目包括 DevOps 所需的大部分电池,但我也包括了一些自定义数据。我的项目的一些特点是
- 使用 Sphinx 和 readthedocs 模板的文档
- Jupyter 笔记本
- 使用 CLI 命令参数的运行程序
- 模块结构
- 自动测试
- 版本控制
DevOps 工具
一旦项目建立起来,就应该添加一些 DevOps 工具来构建和部署文档,运行测试,创建 PyPi 包,并在 PyPi 上部署。通常,这些步骤对数据科学家来说似乎很陌生,但是任何人只要稍加努力就能学会。
文档构建和部署
在本地测试完文档后,我在 readthedocs 服务器上创建了一个免费账户,并与 GitHub 链接。只需点击几下鼠标,即可构建和部署文档。下面是我的项目文档的链接。
[## 欢迎阅读零售预测文档!-零售预测文档
欢迎阅读零售预测文档!
-零售预测文档欢迎使用零售预测文档!零售销售预测](https://retail-sales-prediction.readthedocs.io/en/latest/)
Python 包自动测试
当构建大型项目时,无论是 PR、MR 还是 Push,只要有活动就运行自动测试是至关重要的。我已经使用 GitHub actions 设置了管道,以便在有推送时进行自动测试。目前,我只有演示测试,但它可以很容易地扩展。
创建一个 Python 包并部署在 PyPi 上
项目的最后一部分是创建一个可安装的 python 包,并在 PyPi 包发行版上发布。为此,我在 https://pypi.org/的上创建了一个免费账户。发布已经在 GitHub 上准备好了,使用 GitHub actions 工作流,我已经将包部署到 https://pypi.org/的。下面是我的包的链接。
用机器学习预测零售店销售额的 Python 项目。这个项目是基于提供的数据…
pypi.org](https://pypi.org/project/retail-sales-prediction/)
演示台
一个好的演示文稿是成功交付任何项目的关键技能。我准备了一套幻灯片来展示我的工作。您可以在 Slideshare 上的以下位置找到我的幻灯片。
结束语
在这篇文章中,我写了数据工程/科学项目的概述,包括项目所需的模型构建和 DevOps 工具。这个项目结构是可扩展的,并且以一种只需很少修改就可以用于生产的方式进行设置。
未来作品
这个项目可以增加很多东西
- 探索张量流模型
- 添加 docker 支持并发布 Docker 图像
- 用于模型开发的 Spark 集成
- 远程机器的可行部署
- 投稿指南和 Git 工作流程
- 创建和部署 Conda 包
- 云服务上的最终模型部署,例如 GCP
Go 与 Python 的数据工程观点(第 1 部分)
介绍
探索 golang——我们能放弃 Python 吗?我们(比如“经常处理大量数据的人”)最终找到 go 的用例了吗?第 1 部分探讨了 Python 和 go 之间的高级差异,并给出了这两种语言的具体示例,旨在基于 Apache Beam 和 Google Dataflow 作为真实示例来回答这个问题。
Apache Beam 是我以前在这个博客上使用过几次的东西:它是一个针对批处理和流用例的统一编程模型,可以处理令人愉快的并行工作负载,允许许多自定义 I/O 和其他连接器,并运行在多种执行平台上,最著名的是 Flink、Spark 和 Google Cloud 的数据流。
您可以使用它来传输或批处理数据,以分析数据、运行 ETL、丰富数据——应有尽有。我其实几年前就用过Beam
这里。
现在,如果你去 Beam 的网站,你会发现这个奇妙的图形:
它告诉你 Beam 要么用Java
要么用Python
来写,其他的都属于“其他语言”。
然而,有一个“实验性的”( GitHub )承诺我们使用go
而不是Python
——这让我很兴奋。
下一节将尝试对go
做一个“简短的”(~3,000 字)介绍,并根据不同的处理数据工程特定用例以及在使用多线程应用时,将其与Python
进行比较。请注意,这绝不是完全的比较。
这是第 1 部分,共 2 部分,重点介绍两种语言和 Apache Beam SDK 的当前状态。在第 2 部分(即将推出),我们将探索如何使用
*Beam*
*go*
SDK,我们将在 GCP 面临哪些限制,以及数据工程师的*go*
之旅将走向何方。
所有示例代码在 GitHub 上都有。
Go vs. Python
如果你不熟悉,让我引用维基百科:
Go 是一种静态类型的编译编程语言,由 Robert Griesemer、Rob Pike 和 Ken Thompson 在 Google 设计。Go 在语法上类似于 C,但是具有内存安全、垃圾收集、结构化类型和 CSP 风格的并发性。
Go
有许多概念,应该会让您对处理数据的并行执行框架感到兴奋。在接下来的几节中,我们将探索一些精选的概念和示例,并以一个更长、更深入的并发示例结束。
速度
go
是一种编译语言,而不是解释语言,应该比 Python 快。它还倾向于非常快速地编译东西,将编译好的代码发送给最终用户要比试图用 Python 做同样的事情容易几个数量级。
你可以在下面找到一些实际操作的例子。
使用 matplotlib 生成;CC BY-SA 3.0
静态打字
与Python
的动态类型相反,静态避免了在正确的时刻使用正确的数据类型带来的许多类型问题。
我并不想对静态和动态类型以及 Python 的复杂性进行一般性的比较,而只是从我的经验来看,特别是在数据空间中,每天都是如此。
在数据驱动的项目中,您会经常发现自己在处理非常严格的数据类型定义,这通常是某个领域的一个工件(理所当然如此),在这个领域中,所有公司数据都不可避免地存储在大型 RDMBs 中,例如 Oracle,强制执行严格的类型。
基于 Python 的框架,比如pySpark
甚至pandas
,都自带了提供类型的抽象层。
以这段代码为例,它是为pySpark
编写的,这是一个流行的数据处理框架,在 Python(以及其他语言)中可用,使用了 NYC Yellow Cab 数据[0]。
首先,将一个 CSV 文件读入一个RDD
,一个“有弹性的分布式数据集”,并对数据集中的每一行应用一些转换 lambdas。
# Read data
rdd = sc.textFile('data/yellow_tripdata_2019-01.csv')
# Parse the RDD
rdd = rdd.map(lambda r: r.split(','))\
.map(lambda r: (r[10],r[13])) # Take 'fare_amount' and 'tip_amount'
rdd = rdd.filter(lambda r: 'fare_amount' not in r) # filter header
rdd.take(1)
这产生了:
[('7', '1.65'), ('14', '1'), ('4.5', '0'), ('3.5', '0'), ('52', '0')]
如果我们研究其中一个元组:
type(rdd.take(1)[0][0])
我们将把str
视为数据类型。
现在,如果我们要减少这个数来合计出租车费和小费金额:
def sum_fares(fare, tip):
return fare + tip
rdd.map(lambda r: sum_fares(*r)).take(5)
正如上面输出中的引号所示,结果是一个串联的strings
列表。
['71.65', '141', '4.50', '3.50', '520']
而不是数学上正确的:
rdd.map(lambda r: sum_fares(*[float(x) for x in r])).take(5)
# [8.65, 15.0, 4.5, 3.5, 52.0]
新版本的 Python 支持type hints
,Python 解释器跟踪变量类型。然而,正如上面的例子所强调的,我个人发现很难维护一个一致的、可读的、可维护的代码库,尤其是在复杂的应用程序上。
另一方面,go
是静态类型的。
package main
import (
"fmt"
"reflect"
)
func main() {
// Implicit
str := "A String"
fmt.Printf("%s is %s\n", str, reflect.TypeOf(str))
// Explicit
var x float64 = 3.14
fmt.Printf("%v is %s\n", x, reflect.TypeOf(x))
}
将产生:
go run test.go
A String is string
3.14 is float64
鉴于此:
str = x
不会编译:
go run testvar.go
# command-line-arguments
./test.go:15:6: cannot use x (type float64) as type string in assignment
Go 不支持Generics
,但Python
也不支持。
我跳过了
*go*
**empty interface{}*
的概念,以支持任意值和处理未知类型;在需要弱类型抽象的情况下,可以使用这个概念*
*【0】**Spark*
的 SQL 接口将推断字符串类型,并允许对字符串进行数学运算,如果它们一致
接口和结构
Python
确实有一个class
结构(我已经广泛使用过了),而go
使用了structs
和interfaces
(这是一个可怕的简化)。go
没有继承,依赖于接口和组合。
在数据世界中,拥有严格的类结构,例如抽象转换、统计模型或简单的旧数据结构,可能既是痛苦也是诅咒。
Python
广泛使用了dicts
,它可以保存任意的键值对和嵌套结构,在语法上类似于JSON
,并且为数据的结构化抽象定义了class
。几乎每个数据框架都有自己的schema
类。从理论上讲,go
可以通过组合interfaces
(用于标准)、静态类型(确保在正确的时刻使用正确的数据类型)和structs
来定义结构和逻辑,从而避免这种情况。
这里有一个非常有用的例子,它使用接口CoordinateData
和函数calculateDistance
,在世界上最差的 GIS 分析平台上计算两个坐标元组之间的距离:)
*package main
import (
"fmt"
"math"
)
// A Resource we're trying to access
type CoordinateData interface {
calculateDistance(latTo, lonTo float64) float64
}*
然后,我们实现地理空间数据和哈弗辛函数(以近似地球上的距离):
*type GeospatialData struct {
lat, lon float64
}
const earthRadius = float64(6371)
func (d GeospatialData) calculateDistance(latTo, lonTo float64) float64 {
// Haversine distance
var deltaLat = (latTo - d.lat) * (math.Pi / 180)
var deltaLon = (lonTo - d.lon) * (math.Pi / 180)
var a = math.Sin(deltaLat / 2) * math.Sin(deltaLat / 2) +
math.Cos(d.lat * (math.Pi / 180)) * math.Cos(latTo * (math.Pi / 180)) *
math.Sin(deltaLon / 2) * math.Sin(deltaLon / 2)
var c = 2 * math.Atan2(math.Sqrt(a),math.Sqrt(1-a))
return earthRadius * c
}*
在一个简单的二维平面上也是如此:
*type CartesianPlaneData struct {
x, y float64
}
func (d CartesianPlaneData) calculateDistance(xTo, yTo float64) float64 {
// Simple 2-dimensional Euclidean distance
dx := (xTo - d.x)
dy := (yTo - d.y)
return math.Sqrt( dx*dx + dy*dy )
}*
在这种情况下,main()
函数只计算两种完全不同的距离:
*func main() {
atlanta := GeospatialData{33.753746, -84.386330}
distance := atlanta.calculateDistance(33.957409, -83.376801) // to Athens, GA
fmt.Printf("The Haversine distance between Atlanta, GA and Athens, GA is %v\n", distance)
pointA := CartesianPlaneData{1, 1}
distanceA := pointA.calculateDistance(5, 5)
fmt.Printf("The Pythagorean distance from (1,1) to (5,5) is %v\n", distanceA)
}*
当然,这是一个非常简单的例子,简单地将结构强加到你的结构上。然而,与 Python 相比,我的选择有限:
*class CoordinateData:
def calculateDistance(self, latTo, lonTo):
pass
class GeospatialData(CoordinateData):
def __init__(self, lat, lon):
self.lat = lat
self.long = lon
def calculateDistance(self, latTo, lonTo):
# Haversine goes here :)
return 95.93196816811724
class CartesianPlaneData(CoordinateData):
def __init__(self, x, y):
self.x = y
self.x = y
# Let's not implement calculateDistance()
if __name__ == "__main__":
atlanta = GeospatialData(33.753746, -84.386330)
distance = atlanta.calculateDistance(33.957409, -83.376801) # to Athens, GA
print('The Haversine distance between Atlanta, GA and Athens, GA is {}'.format(distance))
pointA = CartesianPlaneData(1,1)
distanceA = pointA.calculateDistance(5, 5)
print('The Pythagorean distance from (1,1) to (5,5) is {}'.format(distanceA))
print('pointA is of type {}'.format(pointA.__class__.__bases__))*
这是有效的 Python — CartesianPlaneData
是CoordinateData
的子类(不是接口—Python 使用 duck-typing),因此,简单地使用没有返回类型的calculateDistance
方法(参见上面的静态与动态类型),运行并返回:
*python3 interface_example.py
The Haversine distance between Atlanta, GA and Athens, GA is 95.93196816811724
The Pythagorean distance from (1,1) to (5,5) is None
pointA is of type (<class '__main__.CoordinateData'>,)*
Python 仍然允许接口抽象,你完全可以使用类来定义层次和逻辑,正如我在下面的PluginInterceptor
中所做的,来确定一个定制插件是否是一个已定义基类的一部分;然而,这并不是在运行时强制执行的,如果您不正确地实现它,可能会失败。
*class PluginInterceptor:
"""Loads all allowed plugins, when they are a subclass of `BasePlugin` and have the constant `name` set (not `__name__`)
"""
def __init__(self):
self.cls = BasePlugin
self.allowed_plugins = self.__load__allowed_plugins__()
def __get_all_subclasses__(self, cls):
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in self.__get_all_subclasses__(c)])
def __load__allowed_plugins__(self):
__allowed_plugins__ = {}
for cls in self.__get_all_subclasses__(self.cls):
if cls.name:
__allowed_plugins__[cls.name] = cls
return __allowed_plugins__*
https://github . com/otter-in-a-suit/稻草人/blob/master/plugin _ base/interceptor . py # L5
您还可以在下面的 Mandelbrot 示例中找到一个结构示例。
两颗北极指极星
go
懂指针,不懂指针算术。go
中的指针用于按指针传递操作,与按值传递相反。我不会深入讨论这个问题,给你一个来自go
的例子,你可以参考这篇关于 Python 的精彩文章。
*package main
import "fmt"
func main() {
i, j := 42, 2701
p := &i // point to i
fmt.Println(*p) // read i through the pointer
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
}*
(来自https://tour.golang.org/moretypes/1
我对go
的简短总结是:作为一名开发人员,我能够控制我是喜欢标准的C
风格的行为——按值传递——还是一个指针,我仍然按值传递——但是在这种情况下,一个指向值的指针作为函数参数。**
Mandelbrot 集的并发性
简单、开箱即用、易于使用的并发是go
中最精彩的事情之一。从同步到并发总共需要两个字母- go
。
让我们使用一种“令人愉快的并行”算法,即曼德尔布罗集合。
Mandelbrot 集合是函数
f_c(z) = z² + c
从z = 0
开始迭代时不发散的复数c
的集合,即序列f_c(0, f_c(f_c(0))
等。,保持绝对值有界。
Mandelbrot 算法的输出;CC BY-SA 3.0
计算机编程语言
我们先来看 Python。
Python 提供了多种方式来表达并发性——threading
、multiprocessing
、subprocesses
、concurrent.futures
等等——但是选择正确的方式和编写清晰的代码是一个挑战。请允许我引用文件:
**本章描述的模块为代码的并发执行提供支持。工具的适当选择将取决于要执行的任务(CPU 受限与 IO 受限)和首选的开发风格(事件驱动的协作多任务与抢占式多任务)
我不会详细讨论什么情况下什么是正确的选择,全球 Python 解释器(GIL)如何影响它,或者一切是如何工作的,因为在网上可以很容易地找到数百篇关于这个主题的文章。然而,我想关注的是代码风格、易用性和性能。
单线程的
这可以这样表达(由 danyaal 作出,由你真正调整):
请记住,有更多、更快、优化的版本,但也有更多复杂的版本;下面的例子应该是简单明了的&,并且可以在
*go*
和*Python*
之间几乎 1:1 地翻译**
首先,我们定义算法的迭代。这是可以并行运行的部分,我们马上就会看到。
**import numpy as np
import matplotlib.pyplot as plt
# counts the number of iterations until the function diverges or
# returns the iteration threshold that we check until
def countIterationsUntilDivergent(c, threshold):
z = complex(0, 0)
for iteration in range(threshold):
z = (z*z) + c
if abs(z) > 4:
break
pass
pass
return iteration**
下一个函数有点庞大,但最后,它简单地创建了实轴和虚轴,将它们分配给一个二维数组,然后运行循环。
**def mandelbrot(threshold, density):
# location and size of the atlas rectangle
# realAxis = np.linspace(-2.25, 0.75, density)
# imaginaryAxis = np.linspace(-1.5, 1.5, density)
realAxis = np.linspace(-0.22, -0.219, 1000)
imaginaryAxis = np.linspace(-0.70, -0.699, 1000)
realAxisLen = len(realAxis)
imaginaryAxisLen = len(imaginaryAxis)
# 2-D array to represent mandelbrot atlas
atlas = np.empty((realAxisLen, imaginaryAxisLen))
print('realAxisLen: {}, imaginaryAxisLen: {}'.format(realAxisLen, imaginaryAxisLen))
# color each point in the atlas depending on the iteration count
for ix in range(realAxisLen):
for iy in range(imaginaryAxisLen):
cx = realAxis[ix]
cy = imaginaryAxis[iy]
c = complex(cx, cy)
atlas[ix, iy] = countIterationsUntilDivergent(c, threshold)
pass
pass
return atlas.T**
计算在单个线程上进行,如下所示:
htop
多线程
现在,为了在多线程中运行它,我们可以使用multiprocessing
模块并像这样运行它。
calc_row()
函数可以简化,但它指出了我们的不同之处:逐行计算图像,而不是一次一个点。
**import multiprocessing as mp
import itertools
def calc_row(cx, cy, threshold=120):
c = complex(cx[1], cy[1])
return (cx[0], cy[0], countIterationsUntilDivergent(c, threshold))**
接下来,我做了一个有问题的决定,通过使用starmap
和Pool
来简化循环,直接将嵌套循环的排列作为参数。
换句话说,无论我们给进程池多少个进程,我们都在运行calc_row(cx, cy, threshold)
。multiprocessing
库负责分别传递来自list
或iterator
的参数。
我们还返回了一个看起来很奇怪的元组,所以我们可以跟踪图像中的索引。
**def mandelbrot_multi(threshold, density, cpus=4):
realAxis = np.linspace(-0.22, -0.219, 1000)
imaginaryAxis = np.linspace(-0.70, -0.699, 1000)
realAxisLen = len(realAxis)
imaginaryAxisLen = len(imaginaryAxis)
atlas = np.empty((realAxisLen, imaginaryAxisLen))
# Create list of permutations
realAxis = [(i,e ) for i,e in enumerate(realAxis)]
imaginaryAxis = [(i,e ) for i,e in enumerate(imaginaryAxis)]
paramlist = list(itertools.product(realAxis, imaginaryAxis))
paramlist = list(map(lambda t: t + (threshold,),paramlist))
# Create a multiprocessing pool
pool = mp.Pool(cpus)
n = pool.starmap(calc_row, paramlist)
pool.close()
pool.join()
return n, atlas**
它更巧妙的利用了我们现有的资源:
htop
从性能的角度来看,我们在单个 CPU 上同时使用了 8.4s 和 2.53s ,由于使用了mutliprocessing
模块,内存开销很大。
当然,有很多不同的方法可以加速这一过程,比如
*Cython*
*numpy*
*tensorflow*
,等等,但是看看现成的并发,让我们将它与*go*
进行比较。我不擅长选择例子,分形很漂亮。😃****
去
让我们看看它在go
是什么样子。
单线程的
单线程版本没有太大的不同。我重复使用了相同的代码,但是简单地在go
中重写了它。
大多数
*go*
实现都使用*Image*
包来生成 go 中的映像——这对于一个独立的项目来说是有意义的。然而,在这里,我将数组写到磁盘并在*numpy*
中读取,因此,*Python*
,以保持代码简洁。*Python*
和*go*
的性能数字都是 只是计算 ,不是 I/O 或绘图像素!
首先,我们导入所需的包并编写一个np.linespace()
等价物,它返回指定间隔内的均匀分布的数字。
**package main
import (
"bytes"
"fmt"
"log"
"math/cmplx"
"os"
"strings"
"time"
"encoding/binary"
)
func linspace(start, end float64, num int) []float64 {
result := make([]float64, num)
step := (end - start) / float64(num-1)
for i := range result {
result[i] = start + float64(i)*step
}
return result
}**
代码的其余部分应该看起来很熟悉——注意非常具体的强类型数据类型和返回类型。
**func countIterationsUntilDivergent(c complex128, threshold int64) int64 {
z := complex(0, 0)
var ix int64 = 0
for i := int64(0); i < threshold; i++ {
ix = i
z = (z * z) + c
if cmplx.Abs(z) > 4 {
return i
}
}
return ix
}
func mandelbrot(threshold, density int64) [][]int64 {
realAxis := linspace(-0.22, -0.219, 1000)
imaginaryAxis := linspace(-0.70, -0.699, 1000)
fmt.Printf("realAxis %v\n", len(realAxis))
fmt.Printf("imaginaryAxis %v\n", len(imaginaryAxis))
atlas := make([][]int64, len(realAxis))
for i := range atlas {
atlas[i] = make([]int64, len(imaginaryAxis))
}
fmt.Printf("atlas %v\n", len(atlas))
for ix, _ := range realAxis {
for iy, _ := range imaginaryAxis {
cx := realAxis[ix]
cy := imaginaryAxis[iy]
c := complex(cx, cy)
//fmt.Printf("ix, iy: %v %v\n", ix, iy)
atlas[ix][iy] = countIterationsUntilDivergent(c, threshold)
}
}
return atlas
}**
多线程
通过使用一个叫做goroutines
的概念,Go 使这变得容易多了。我们可以简单地使用go
指令,而不必处理 Python mutltiprocessing
模块、pools
、map
vs. starmap
以及 Python 解释器的复杂性。
正如我前面提到的,这里的代码是有意简单的,可以进行简单的优化,但是我尽量让
*go*
代码尽可能接近*Python*
代码。请原谅任何简化。
首先,我们将从 Python 中重新创建calc_row
方法,这一次使用一个struct
来返回索引和值,因为我们将在第二步中使用的channel
不会采用多种返回类型:
**type triple struct {
ix, iy int64
c int64
}
func calcRow(ix, iy int64, c complex128, threshold int64) triple {
return triple{ix, iy, countIterationsUntilDivergent(c, threshold)}
}**
我们的主要功能将使用两个概念:channels
和前面提到的goroutine
。
一个 goroutine 有一个简单的模型:它是一个与相同地址空间中的其他 goroutine 同时执行的函数。go 文档将其与 Unix shell &
操作符进行了比较,我发现这是一个很好的类比。
我们正在使用的通道是一个缓冲通道,其作用类似于我们并发函数的管道,因为非缓冲通道天生就是阻塞的。
这导致了下面的代码,该代码包装了内部循环(见上面关于琐碎优化的评论和这里的缺乏——我相信甚至一个指向goroutine
中的WaitGroup
和一个更小的channel buffer
的指针可能会加速这个过程,但是我还没有测试过它)。
**func mandelbrot(threshold, density int64) [][]int64 {
realAxis := linspace(-0.22, -0.219, 1000)
imaginaryAxis := linspace(-0.70, -0.699, 1000)
atlas := make([][]int64, len(realAxis))
for i := range atlas {
atlas[i] = make([]int64, len(imaginaryAxis))
}
// Make a buffered channel
ch := make(chan triple, int64(len(realAxis))*int64(len(imaginaryAxis)))
for ix, _ := range realAxis {
go func(ix int) {
for iy, _ := range imaginaryAxis {
cx := realAxis[ix]
cy := imaginaryAxis[iy]
c := complex(cx, cy)
res := calcRow(int64(ix), int64(iy), c, threshold)
ch <- res
}
}(ix)
}
for i := int64(0); i < int64(len(realAxis))*int64(len(imaginaryAxis)); i++ {
select {
case res := <-ch:
atlas[res.ix][res.iy] = res.c
}
}
return atlas
}**
现在,对于go
,我们看到的是在单个 CPU 上的0.38 秒和代码中的0.18 秒**,虽然相似,但要简洁得多。**
最终性能
我要把这个留在这里。正如我之前概述的那样,go
和Python
代码都可以进一步优化,但是我们仍然可以获得大约 45 倍的加速
使用 matplotlib 生成;CC BY-SA 3.0
现实生活中的考虑
谈论理论概念可能很有趣,但这只是难题中相对较小的一部分。虽然我确信 2020 年将是Haskell
被广泛用于生产的一年,但我选择、使用和推荐人们学习一门语言的方法主要是基于现实生活中的使用,而不是学术理想。
语言流行度
这总是一个有趣的问题。
根据 StackOverflow 的说法,围棋相关问题的受欢迎程度甚至还赶不上主要玩家 Python。
https://insights.stackoverflow.com/trends?tags=go%2Cpython
将此与其他一些大大小小的玩家——Java、Haskell、Scala、Lisp——进行对比,结果相似:
现在:提问的百分比是一个很好的衡量标准吗?大概不会。Python 是一门非常受欢迎的学习语言——我自己刚刚参与了它的培训准备——自然会吸引大量的初学者和经验丰富的专业人士。
谷歌趋势显示了一个类似的故事:
(红色—Python
;蓝色- go
)
我认为可以公平地说 Python 更受欢迎——但是go
至少有一个利基市场,如果不是一个上升轨道的话。借助欺骗性可视化的力量,我们可以放大上面的 StackOverflow 图:
https://insights.stackoverflow.com/trends?tags=go
事实上,找到了一个上升的轨迹。
此外,如果 StackOverflow 的年度开发者调查可信,当被问及“最受欢迎的技术”时,go
从 2018 年的 7.2%上升到 2019 年的 8.8%,并在 2020 年上升到 9.4%。
根据的同一项调查,美国go
的程序员也以每年 14 万美元的薪酬排名第二(高于 2019 年的第三),仅次于Scala
(15 万美元)。
有希望!😃
生态系统
解释前两张图的部分原因是 Python 的巨大生态系统——后端、前端、统计、机器学习、深度学习、图形分析、GIS、机器人——它就在那里,它将有成千上万的贡献者和成千上万的用户。
这里有对应的词,我试着总结了它们,以及它们在 GitHub 上的相对受欢迎程度:
此处链接:https://CHOL linger . com/blog/2020/06/a-data-engineering-perspective-on-go-vs .-python-part-1/#生态系统
很明显,至少从数据工程和数据科学的角度来看,围棋生态系统还有很长的路要走。
【1】阿帕奇光束不能替代 Spark
学习曲线
这个是主观的——我个人觉得Python
只在表面上更容易学。如果你真的想理解底层的概念、架构和库,Python
真的是一个兔子洞,不像Java
。
另一方面,go
是一种相当简单的语言,它关注于某些元素。请允许我引用:
设计 Go 的时候,Java 和 C++是编写服务器最常用的语言,至少在 Google 是这样。我们觉得这些语言需要太多的记账和重复。一些程序员以效率和类型安全为代价,转向更动态、更流畅的语言,如 Python。我们觉得在单一语言中实现高效、安全和流畅应该是可能的。
Go 试图在单词的两个意义上减少打字量。在整个设计过程中,我们努力减少混乱和复杂性。没有转发声明,也没有头文件;所有东西都声明一次。初始化是富于表现力的、自动的和易于使用的。语法干净,关键字少。口吃(foo。Foo myFoo = new(foo。Foo))通过使用:= declare-and-initialize 构造的简单类型派生来减少。也许最根本的是,没有类型层次结构:类型就是类型,它们不需要声明它们之间的关系。这些简化使得 Go 既有表现力又易于理解,同时又不牺牲复杂性。*
另一个重要原则是保持概念的正交性。可以为任何类型实现方法;结构代表数据,而接口代表抽象;诸如此类。正交性使得更容易理解当事物组合时会发生什么。
快速浏览一下LinkedIn Learning
就会发现go
总共有 4 道菜Python
有 168 道菜。
在我自己的经验中,最有帮助的事情是通过逐个例子并真正阅读文档。另一方面,Python 作为更广泛层次的外部教程、课程、认证、博客帖子[&mldr;]来自我这样的人,大学,大公司。
在谷歌上查询“go 语言教程”会返回 17 亿条结果,而“python 语言教程”会返回 1620 亿条结果
阿帕奇波束
现在,我们已经讨论了很多关于go
和Python
的一般性问题。但是这个帖子的开篇是什么——Beam
和Dataflow
在哪里?
这一部分将在本文的第 2 部分进行简要阐述
一般来说,beam
go
SDK 确实提供了运行相对简单的任务所需的核心功能。然而,在撰写本文时(2020-06-11),它确实有一系列的缺点。
转换
让我们看看跨beam
语言的可用转换。我挑选了几个,但是请随意参考所有的文档以获得完整的图片:
对于来自Python
的人来说,最显著的不同将是管道的整体外观。
这是在Python
中如何完成的(旁注:没有什么可以阻止你调用data.apply(func)
,因为操作符只是过载了):
**class CountWords(beam.PTransform):
def expand(self, pcoll):
return (
pcoll
# Convert lines of text into individual words.
| 'ExtractWords' >>
beam.FlatMap(lambda x: re.findall(r'[A-Za-z\']+', x))
# Count the number of times each word occurs.
| beam.combiners.Count.PerElement())
counts = lines | CountWords()**
在go
中,它看起来更像一个常规的旧函数:
**func CountWords(s beam.Scope, lines beam.PCollection) beam.PCollection {
s = s.Scope("CountWords")
// Convert lines of text into individual words.
col := beam.ParDo(s, extractFn, lines)
// Count the number of times each word occurs.
return stats.Count(s, col)
}**
我们将在第二部分探讨这些细节
输入-输出
I/O 可能是go
sdk 中最受限制的部分,有许多连接器不可用于 go。
请参见 此链接 了解最新概况。
https://beam.apache.org/documentation/io/built-in/
如果要我总结的话:beam
上的go
支持基本的Google Cloud
服务和地方发展,而Java
则涵盖了几乎所有情况。[2]
[2]请记住,一些连接器,如 *DatabaseIO*
,本质上是特定于语言的
滑行装置
最后,看看可用的运行器,go
或多或少地受限于Direct
和Dataflow
,这符合我在 I/O 上的声明。
答(是)表示这些跑步者有局限性
逐行示例
我建议通过[WordCount](https://beam.apache.org/get-started/wordcount-example/#minimalwordcount-example)
。
WordCount
是一个很好的例子,因为它展示了以下概念:
- 创建管道
- 将转换应用于管道
- 阅读输入
- 应用帕尔多变换
- 应用 SDK 提供的转换
- 写入输出(在本例中:写入文本文件)
- 运行管道
为了简洁起见,我不会在这一点上深入那些细节。
结论
首先要问的问题之一应该是:比较这两种语言有意义吗?我的答案可能是显而易见的——虽然go
可能是为不同的用例设计的(通用脚本和机器学习用例与系统/“云”编程),但从数据工程的角度来看,上面概述的概念仍然让我兴奋不已。
go
有许多概念,不需要庞大的pip
依赖树就能工作,产生干净的代码,易于编译,非常快,而且(在我看来)对未来的数据和 ML 用例有很大的潜力。
总之: **go**
和 **Python**
显然是非常不同的语言——正如我在上面的中用我精选的例子成功概括的那样。
后续步骤
由于我在这篇文章中只浏览了Apache Beam
的表面,下一篇文章将关注以下问题,以将Dataflow
用作“野外的go
”的真实示例:
go
Beam SDK 有多成熟?- 它支持什么?少了什么?
- 什么是根本区别?
- (如何)我们可以在
GCP
运行Dataflow
作业?
所有的开发和基准测试都是在 GNU/Linux [PopOS!内核 5.4 上的 19.10]在 2019 System76 Gazelle 笔记本电脑上使用 12 个英特尔 i7–9750h v cores @ 4.5 GHz 和 16GB RAM
原载于 2020 年 6 月 11 日 https://chollinger.com的 。**
从数据工程的角度看 Go 与 Python(第 2 部分——数据流)
使用 Apache Beam & Dataflow 探索、剖析和基准测试 go SDK
介绍
在我们从数据工程的角度比较 Python 和 go 的第 2 部分中,我们将最终看一下 Apache Beam 和 Google Dataflow,以及 go SDK 和 Python SDK 的不同之处,我们正在处理的缺点,通过运行大量基准测试它有多快,以及进行转换的可行性。
你可以在这里找到《T4》第一部。
阿帕奇光束和谷歌数据流
虽然我们已经在之前的中多次使用过阿帕奇光束,请允许我再简单介绍一下。
Apache Beam 是一个开源的统一模型,用于定义批处理和流数据并行处理管道。使用一个开源的 Beam SDKs,您可以构建一个定义管道的程序。然后,管道由 Beam 支持的分布式处理后端之一执行,包括 Apache Flink、Apache Spark 和 Google Cloud Dataflow。
Beam 可以用于各种数据用例,就像 Apache Spark 一样——ETL(提取、转换、加载)管道、流分析,或者从文件系统到 RDMBS 的简单数据移动。
为什么梁是有趣的
我得到的最好的专业建议之一是问“那又怎样?”。话虽如此:那又怎样,我为什么关心梁?
对我来说,Beam 模型最有趣的方面之一是管道逻辑、语言和执行环境的解耦。我可以在一张纸上设计我的逻辑(例如,用一些Beam
-特定术语画一个DAG
-我们马上就可以完成),用任何主要语言实现它-甚至是多种语言-并在任何支持的Runners
上运行它。
上次我们在这个博客上使用它时,我们用它编写了一个管道来分析 reddit 帖子,并在Dataflow
runner 上执行,如下所示:
因为这条管道是用 Python 编写的,所以没有什么可以阻止我们在本地 Hadoop 集群、AWS EMR 上运行完全相同的作业,或者只是使用DirectRunner
在本地运行它。这种水平的可移植性,结合我碰巧发现比Spark
更精简的编程模型,使得Beam
成为一个非常有趣的框架。
实际上,我们将在本文的示例部分中这样做,并设计一个管道,用Python
和go
编写它,并在不同的Runners
上运行它。
核心概念
Beam
中的一切都始于Pipeline.``Beam
编程模型的工作原理是通过代码暴露高级抽象,允许您的Pipeline
在给定数据集上执行PTransforms
的图形,这在术语上是在不可变集合上操作,称为PCollections
,可以通过IO
操作将其移动到持久状态。作业可以在不同的Runners
上执行,以提供一个(分布式)执行环境。
在我们看一个真实世界的例子之前,让我们深入研究一下这些概念。
这是一个浓缩版的 官方阿帕奇光束文档 ,建议你完整阅读以获得更深入的观点
管道
一个Pipeline
对象是任何作业的起点,它通常由一个configuration
对象初始化,该对象定义了如何和你的管道在哪里运行。
p 收藏
与 Spark RDD
或DataFrame
不同的是,PCollection
是不可变的数据集合,可以通过管道的生命周期在内存中修改。
一个PCollection
中的数据可以是任意类型,只要元素可以被编码为string
以便在工人之间序列化它们。
可以是任意大小的,但是如果集合太大而不适合单个工作节点上的内存,它们的元素可能由不同的工作节点处理。此外,它们可以是有界的(即,具有固定的大小)或者是无界的(即,是“开放式的”,就像在流场景中一样)。
转换
Transforms
通过apply
运算用户代码和逻辑,在PCollection
的单个元素上应用逻辑,并返回可在后续步骤中使用的完整的PCollection
。
https://beam . Apache . org/documentation/programming-guide/# applying-transforms
还可以通过将 DAG 分成多个独立的步骤来branch
流水线:
https://beam . Apache . org/documentation/programming-guide/# applying-transforms
自定义transforms
需要遵守一些规则,最显著的是可串行化(因为数据需要在工作线程之间传输)单线程执行(因为每个元素都应该在自己的线程中工作,由底层运行器协调),以及等幂(因为可能会发生重试)。
Beam
提供了几个可以开箱即用的内核transforms
:
ParDo
GroupByKey
CoGroupByKey
Combine
Flatten
Partition
帕尔多
ParDo
转换可能是最常见的一种,因为它类似于map
操作:将逻辑应用于PCollection
的每个元素,并返回所述元素(或者不返回它,因此,filtering
返回集合)。
将ParDo
作为构建博客的基础,在平面数据流上应用逻辑,然后可以通过grouping
、flattening
和其他聚合逻辑进一步增强。
为了理解一个ParDo
实际上做什么,Beam
文档对一个ParDo
的生命周期提供了很好的解释:
https://beam . Apache . org/documentation/programming-guide/# applying-transforms
CoGroupByKey
CoGroupByKey
通过多个输入集合中的键聚集所有输入元素。CoGroupByKey 执行两个或更多具有相同键类型的键/值 p 集合的关系连接——这是一个非常有用的东西,我们将在下面的例子中使用。
计划
一个Schema
定义了一个PCollection
中元素的逻辑结构和数据类型。Beam Schemas
类似于parquet
或database
模式,应该定义names
、types
,以及关于字段是否可以是NULL
的信息。
Beam 支持以下基本类型:
输入-输出
I/O
用于提供输入数据,并为管道提供一个或多个定义的输出。我将参考第 1 部分以获得 SDK 提供的所有I/O
连接器的完整列表。
I/O
还可以定制连接器,通过从现有基类继承来读取不支持的文件类型。这可能很有挑战性(因为您希望能够分割一个文件源,这样它就可以被多个工作人员处理),但是过一会儿,这就变得非常简单了。
滑行装置
Runners
定义哪个系统执行流水线,例如通过本地运行(DirectRunner
)、在Google Dataflow
上运行或通过Apache Spark
。
与I/O
类似,请参考第 1 部分进行概述。
一个示例作业的用例
为了展示一些基本的区别,让我们定义一个可以在Python
和go
中实现的用例。
在这个例子中,我们遵循这个简单的用户故事:“作为一个电影爱好者,我想找到符合我喜好的电影,这样我就可以看一部新电影了”。
我们的偏好应为:
- 我们希望解析标题和评级(读取数据)
- 我们只对电影感兴趣,对电视剧不感兴趣;此外,电影应该在 1970 年之后制作,以获得一些更可靠的元数据作为我们决策的基础,因为旧的电影通常只有基本信息(过滤数据)
- 我们既要基本标题,也要来自不同来源的评级信息(组合数据)
- 我们想要可用格式的数据(写数据 ) [0]
为此,我们将使用 IMDb 数据集,它可用于非商业用途,并且每天更新。
完整的源代码可以在GitHub上获得。
到现在为止,您可能已经知道这些“偏好”是为了展示各种 *Beam*
功能——这不是一个花哨的推荐引擎,我们并不真正关心输出
绘制设计
正如我在引言中所说的,我们可以先从设计管道开始,然后再讨论特定于语言的逻辑。
这里我们有两个主要的流程,它们做类似的事情:它们读取数据,解析数据格式,并对数据应用自定义过滤器。接下来,它们被组合成一个一致的集合,并写入一个或多个 I/O 目标。
让我们来看看如何把它翻译成代码。
获取数据
如果你同意 IMDb 的许可证,你可以在这里找到数据。
我们将从基本的标题数据开始,并确保我们从一个小的测试集开始,这样我们就可以在没有集群的情况下进行本地开发。为了做到这一点,我们将获取随机测试数据,以及一部我们知道符合我们标准的电影,1971 年的经典“消失点”,id 为tt0067927
(因为我们不太可能在我们将使用的 2 个数据集之间找到更多匹配,否则,鉴于体积)。
wget https://datasets.imdbws.com/title.basics.tsv.gz && gunzip title.basics.tsv.gz
# Create a small test file
head -1 title.basics.tsv > title.basics.100.tsv
shuf -n 100 title.basics.tsv >> title.basics.100.tsv
grep "tt0067927" title.basics.tsv >> title.basics.100.tsv
title.basics
该数据集包含基本标题信息,并且是一个 564MB 制表符分隔文件。
我们将用它来过滤大部分记录。
title.ratings
该数据集包含所有标题的评级,并且是一个 18MB 制表符分隔文件。
创建管道
我们的 Beam 管道的第一步是创建一个创建Pipeline
对象的框架代码,解析参数,并设置一个记录器。
计算机编程语言
我们首先需要通过运行pip3 install --upgrade pip
和pip3 install apache-beam==2.22.0 --upgrade
来安装 Beam。
我们的框架使用Python
的标准logger
模块来记录日志,使用argparse
来读取将要传递给管道的参数。
from __future__ import absolute_import
import argparse
import logging
import apache_beam as beam
from apache_beam.io import ReadFromText
from apache_beam.io import WriteToText
from apache_beam.options.pipeline_options import PipelineOptions, GoogleCloudOptions
from apache_beam.options.pipeline_options import SetupOptions
def run(argv=None):
# Parse arguments
parser = argparse.ArgumentParser()
parser.add_argument('--input-ratings',
dest='input_ratings',
required=True,
help='Input rating file to process.')
parser.add_argument('--input-titles',
dest='input_titles',
required=True,
help='Input title file to process.')
parser.add_argument('--output',
dest='output',
required=True,
help='Output to write results to.')
known_args, pipeline_args = parser.parse_known_args(argv)
pipeline_options = PipelineOptions(pipeline_args)
pipeline_options.view_as(SetupOptions).save_main_session = True
# Create the pipeline
with beam.Pipeline(options=pipeline_options) as p:
# TODO: Run it
pass
if __name__ == '__main__':
# Set the logger
logging.getLogger().setLevel(logging.INFO)
logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
datefmt='%Y-%m-%d:%H:%M:%S',
level=logging.INFO)
# Run the core pipeline
logging.info('Starting')
run()
这里没有太多需要注意的地方——我们所有的管道代码都在with
块下,一旦我们编写了自己的步骤,它将定义我们刚刚设计的DAG
。
去
对于go
,我们可以通过go get
安装beam
,而不是通过go get -u github.com/apache/beam/sdks/go/...
安装pip
接下来,我们的管道框架将使用flags
包作为参数,使用log
作为日志。
package main
import (
"context"
"flag"
"log"
"github.com/apache/beam/sdks/go/pkg/beam"
"github.com/apache/beam/sdks/go/pkg/beam/io/textio"
"github.com/apache/beam/sdks/go/pkg/beam/x/beamx"
)
func main() {
// Define arguments
var inputBasePath = flag.String("input-basics", "", "Input base file")
var inputRatingsPath = flag.String("input-ratings", "", "Input ratings file")
var outputPath = flag.String("output", "", "Output path")
// Parse flags
flag.Parse()
// Initialize Beam
beam.Init()
// Input validation. Must be after Init().
if *inputBasePath == "" || *inputRatingsPath == "" || *outputPath == "" {
log.Fatal("Usage: movie_pipeline --input-basics $PATH, --input-ratings $PATH --output $PATH")
}
// Create a Pipeline
p := beam.NewPipeline()
s := p.Root()
// Pipeline code
// Concept #1: The beamx.Run convenience wrapper allows a number of
// pre-defined runners to be used via the --runner flag.
if err := beamx.Run(context.Background(), p); err != nil {
log.Fatalf("Failed to execute job: %v", err)
}
}
这里有几个值得注意的地方。首先,flags
包不支持强制属性,因此我们必须手动检查由flag.Parse()
传递的字符串指针。你会在整个代码中发现类似的块,因为go
不知道Exception
的概念,因此,错误是返回元素(例如,一个函数可能返回一个数据元组和一个可选错误),需要手动检查。
此外,注意如何在输入验证之前调用beam.Init()
。
读取和解析数据
接下来,我们需要读取数据并解析TSV
格式。我们可以使用我们的第一个ParDo
调用来实现这一点。
计算机编程语言
首先,让我们通过创建beam.DoFn
的子类来定义ParDo
操作,如下所示:
class ParseCsv(beam.DoFn):
def __init__(self, col_names: list):
self.col_names = col_names
def process(self, string: str):
reader = csv.DictReader(string.splitlines(), fieldnames=self.col_names, delimiter='\t')
for row in reader:
yield row
这个类将简单地把我们的 CSV 从行str
解析成一个dict
,给我们dict
作为单独的元素在下一个transforms
中使用。
对于自定义ParDo
,方法process
必须被覆盖。process
应该是一个generator
并因此,不得不yield
单独记录。对于 *PCollection*
中的每条记录,该函数将被调用*。如果我们用return
代替yield
,你马上就会看到会发生什么。*
自定义参数可以通过覆盖__init__()
中的constructor
传递给类。
为了将它与管道集成,我们需要定义流程。在Beam Python SDK
中,>>
和|
操作符在PCollection
之上定义了我们单独的加工(apply
)步骤。
对于每个步骤,我们可以调用beam.ParDo
并提供一个DoFn
子类的实例。
with beam.Pipeline(options=pipeline_options) as p:
(p | 'Read data' >> beam.io.ReadFromText(known_args.input_titles, skip_header_lines=1)
| 'Parse CSV' >> beam.ParDo(ParseCsv(['titleId', 'ordering','title','region','language','types','attributes','isOriginalTitle']))
| 'Print' >> beam.Map(print)
)
这将读取文本文件,解析TSV
,产生一个PCollection
,并使用map
简单地打印值。
输出如下所示:
{'titleId': 'tt0000001', 'ordering': '1', 'title': 'Карменсіта', 'region': 'UA', 'language': '\\N', 'types': 'imdbDisplay', 'attributes': '\\N', 'isOriginalTitle': '0'}
{'titleId': 'tt0000001', 'ordering': '2', 'title': 'Carmencita', 'region': 'DE', 'language': '\\N', 'types': '\\N', 'attributes': 'literal title', 'isOriginalTitle': '0'}
这个简单的起点展示了Beam
与Python
一起使用的非常具体的语法。虽然它确实创建了一个清晰易读的逻辑,但它肯定比常规的方法链接方法更容易让人混淆。
去
在这里,我们可能会看到Python
和go
之间一些最显著的差异——但正如我所发现的,也有同样显著的相似之处。
读取数据并应用下一个ParDo
并不遵循Python
的重载语法(使用>>
和|
),而是导致在每个步骤后返回各种 *PCollections*
:
// Parse the movies file
lines_movies := textio.Read(s, *inputBasePath)
base_movies := beam.ParDo(s, &movieFn{}, lines_movies)
虽然这可能看起来与Python
非常不同,但从根本上来说,发生了同样的事情:图形的每一步都返回一个新的 *PCollection*
,下一步可以对其进行处理。
这里要提到的另一件事是指针的使用。*inputBasePath
是指向我们之前给出的flags
参数的指针。在go
中,一个*string
可以是nil
,而普通的字符串则不能(因为go
中的nil
表示“它不指向任何东西”,而string
只能是空的或者被字符填充——下面会详细介绍)。众所周知,Python
没有暴露的指针(T21 的另一种主要语言Java
也没有)。
下一个看起来非常不同的东西是实际的ParDo
。我们来分析一下。
Beam
的泛型ParDo
函数签名如下:
func ParDo(s Scope, dofn interface{}, col PCollection, opts ...Option) PCollection {
ret := MustN(TryParDo(s, dofn, col, opts...))
if len(ret) != 1 {
panic(formatParDoError(dofn, len(ret), 1))
}
return ret[0]
}
其中s Scope
是你的pipeline
对象,dofn interface{}
使用go
的empty interface
逻辑来定义一个可能保存任何类型的值的接口(我一会儿再回到那个),而col
显然是我们的PCollection
,类似于我们在Python
中看到的。
这意味着beam.ParDo(s, &movieFn{}, lines_movies)
简单地声明:应用函数moveFn
,它必须是一个ParDo
(记住:静态类型!),并将名为line_movies
的PCollection
作为输入。
一旦理解了语法,这与
| 'Parse CSV' >> beam.ParDo(ParseCsv(['titleId', 'ordering','title','region','language','types','attributes','isOriginalTitle']))
在 Python 中。
我们将在下一节看看&movieFn{}
做了什么。无论如何,在这一点上,我们有了输入TSV
中每一行的结构化表示。
转换数据
接下来,我们需要转换我们的数据,以确保我们有可以在输出中使用的正确的数据类型,并确保我们在接下来的步骤中的filter
逻辑是清晰的。
计算机编程语言
请注意上面输出中的NULL
是如何被引用为\N
的,以及isOriginalTitle
听起来像是boolean
,但实际上是integer
。
我们可以简单地创建另一个ParDo
转换来处理这个场景。为了避免每个文件只有一个ParDo
类,我们将使它动态化。
class CleanData(beam.DoFn):
def __init__(self, bool_cols=[]):
self.bool_cols = bool_cols
def process(self, record: dict):
for k in record:
# Map \N to None
if record[k] == '\\N':
record[k] = None
# Convert e.g., `isOriginalTitle` to Boolean
for col in self.bool_cols:
if record[col] == '0':
record[col] = False
else:
record[col] = True
# Return
yield record
因为Python
dicts
可以接受任意类型,所以简单地在 dict 中改变值是很容易的。请注意我们必须如何将0
视为string
,因为Python
是动态类型的,在这里不强制类型,并且给定 TSV 输入,一切都是str
。
此时,您可能会发现自己处于两种思想流派之一:要么,您很高兴Python
允许您使用dicts
编写管道,而不关心它们各自的类型(甚至不关心dict
中字段的数量或名称)!)——这使得编写管道更加容易。
或者,你遗漏了Java
和go
强制的严格打字。在这两种情况下,在将任何数据持久化到任何地方之前,我们都被迫关心结构和类型,以避免无效的转换或错误的输出到我们的目标系统中。
去
对于go
,让我们重温一下之前的&movieFn{}
论点。
我们已经创建了一个名为moveFn
的struct
,它保存了我们的数据结构及其类型:
type movieFn struct {
tconst, titleType, primaryTitle, originalTitle string
isAdult bool
startYear, endYear, runtimeMinutes int64
genres string
}
然后我们在这个struct
上定义一个ParDo
方法:
func (f *movieFn) ProcessElement(line string, emit func(movieFn)) {
row := strings.Split(line, "\t")
fmt.Printf("%v\n", row)
// Skip the header
if row[0] != "tconst" {
// Map nulls
// Convert the types
startYear, err1 := strconv.ParseInt(row[5], 10, 64)
endYear, _ := strconv.ParseInt(row[6], 10, 64)
runtimeMinutes, err3 := strconv.ParseInt(row[7], 10, 64)
// Convert Boolean
isAdultInt, err4 := strconv.ParseInt(row[4], 10, 64)
var isAdult bool
if isAdultInt == 0 {
isAdult = false
} else {
isAdult = true
}
if err1 == nil && err3 == nil && err4 == nil {
// If the types match, return a rating struct
m := movieFn{
tconst: row[0],
titleType: row[1],
primaryTitle: row[2],
originalTitle: row[3],
isAdult: isAdult,
startYear: startYear,
endYear: endYear,
runtimeMinutes: runtimeMinutes,
genres: row[8],
}
fmt.Printf("%v\n", m)
emit(m)
}
}
}
它将解析行和将类型转换成单个的ParDo
结合起来。我们在 Python 中对此进行了分解以解释概念,但从根本上来说,发生了同样的事情:我们需要通过分隔符tab
来分割行,并创建一个类似json
的结构(这就是这个struct
将在内部序列化的内容!).
为了避免不得不处理多个structs
,因为我们(与 Python 相反)不能简单地“重用dict
和改变类型【0】,我们在一个单一的步骤中进行转换。
在对ParDo
的实际调用中,&movieFn{}
简单地翻译为“对movieFn
结构的内存位置的引用,它被初始化为空”。
最后但同样重要的是,注意函数的返回是怎样的emit func(movieFn))
。Beam
SDK 使用反射从传入和传出的PCollections
中收集类型,在我们的例子中,输入为line string
,输出为movieFn
——通过指定一个函数作为输入,我们称之为类似于Python
的本机yield
。请注意实际的函数没有返回任何东西,因为emit
函数是我们的ParDo
的一个参数!
您可以将这个函数称为任何东西— emit
只是在 go 示例中找到的样式。
应该注意的是,这个struct
当然也可以通过简单地提供更多的方法来保存自定义方法(类似于Python
类)。
让我们重新访问一下string
和nil
注释,并尝试将'\N'
转换为nil
以匹配我们的Python
管道,假设我们想要一个NULL
类型用于将数据写入例如BigQuery
:
func (f *movieFn) CleanNulls(row []string, nullColIds []int) []string {
for i, _ := range row {
if row[i] == "\\N" {
row[i] = nil
}
}
return row
}
当然,编译器不接受:
cannot use nil as type string in assignment
一种解决方法是使用*string
或者跟随,例如[bigquery->InterSchema](https://godoc.org/cloud.google.com/go/bigquery#InferSchema)
将NULL
映射到期望它用于字符串的系统。
此外,我们需要确保我们不会意外地导出这个方法,否则,我们将得到:
graph.AsDoFn: unexpected exported method CleanNulls present. Valid methods are: [Setup StartBundle ProcessElement FinishBundle Teardown CreateInitialRestriction SplitRestriction RestrictionSize CreateTracker
这可以通过以小写字母“c”开始函数而简单地不导出它来解决:
func (f *movieFn) cleanNulls(row []string) []string {
for i, _ := range row {
if row[i] == "\\N" {
row[i] = ""
}
}
return row
}
这是与Python
的另一个区别,因为go
确保我们遵循预期的interface
。
虽然我个人确实认为逻辑go
强加了一个String
要么为空要么不为空,拥有两个不同的“空”类型并没有增加多少价值,但是从Python
或Java
的角度来看,我可以看到这对来自go
的人来说是非常令人不快的。
我想我们可以用一个 *interface{}*
过滤数据
接下来,我们要过滤数据,以确保我们只获得我们想要的电影和收视率。
计算机编程语言
过滤数据可以通过使用另一个ParDo
类来完成,但是带有一个 catch。让我们试试这个:
class FilterBasicData(beam.DoFn):
def process(self, record: dict):
if record['titleType'] == 'movie' and not record['isAdult']:
yield record
else:
yield None
输出看起来很糟糕:
None
{'tconst': 'tt3080844', 'titleType': 'movie', 'primaryTitle': 'Almost Holy', 'originalTitle': 'Crocodile Gennadiy', 'isAdult': False, 'startYear': '2015', 'endYear': None, 'runtimeMinutes': '96', 'genres': 'Biography,Documentary,Drama'}
None
然而,如果我们回忆起DoFn
是一个生成器(而不仅仅是一个出于某种原因使用yield
而不是return
的方法),我们可以快速使用它,简单地让不返回无效记录(以及添加我们的其他过滤标准),从而创建一个更小的PCollection
来处理:
class FilterBasicData(beam.DoFn):
def process(self, record: dict):
if record['titleType'] == 'movie' and not record['isAdult']:
yield record
# No else - no yield
并获得看起来不错的输出:
{'tconst': 'tt3080844', 'titleType': 'movie', 'primaryTitle': 'Almost Holy', 'originalTitle': 'Crocodile Gennadiy', 'isAdult': False, 'startYear': '2015', 'endYear': None, 'runtimeMinutes': '96', 'genres': 'Biography,Documentary,Drama'}
{'tconst': 'tt7497202', 'titleType': 'movie', 'primaryTitle': 'Wonderful Losers: A Different World', 'originalTitle': 'Wonderful Losers: A Different World', 'isAdult': False, 'startYear': '2017', 'endYear': None, 'runtimeMinutes': '71', 'genres': None}
但是我们的按年过滤呢?如果我们尝试把and record['startYear'] >= 1970:
作为一个条件,我们会遇到:
TypeError: '>=' not supported between instances of 'str' and 'int' [while running 'Filter data']
因为 Python 缺乏严格的类型(还记得我们是如何在任何地方都没有定义模式的)。我们可以通过扩展CleanData
来修复这个问题:
class CleanData(beam.DoFn):
def __init__(self, bool_cols=[], int_cols=[]):
self.bool_cols = bool_cols
self.int_cols = int_cols
def process(self, record: dict):
for k in record:
# Map \N to None
if record[k] == '\\N':
record[k] = None
# Convert e.g., `isOriginalTitle` to Boolean
for col in self.bool_cols:
if record[col] == '0':
record[col] = False
else:
record[col] = True
# Force-parse numeric values
for col in self.int_cols:
if record[col] and record[col].isdigit():
record[col] = int(record[col])
# Return
yield record
并将我们的过滤器调整为:
class FilterBasicData(beam.DoFn):
def process(self, record: dict):
if record['titleType'] == 'movie' and not record['isAdult'] and record['startYear'] and record['startYear'] >= 1970:
yield record
# No else - no yield
这给了我们一个电影列表。
去
这里的过滤和go
没有太大区别。我们可以犯和在Python
中一样的错误,但是混淆return
和(在本例中)一个emit
函数:
// Filters Movies
func filterMovies(movie movieFn) movieFn {
if !movie.isAdult && movie.startYear >= 1970 {
return movie
}
return movieFn{}
}
它会回来的
{ false 0 0 0 }
{tt3080844 movie Almost Holy Crocodile Gennadiy false 2015 0 96 Biography,Documentary,Drama}
{ false 0 0 0 }
而我们在上一节中讨论的方法是可行的:
// Filters Movies
func filterMovies(movie movieFn, emit func(movieFn)) {
if !movie.isAdult && movie.startYear >= 1970 {
emit(movie)
}
}
请注意我们的静态类型的struct
不会有任何TypeError: '>=' not supported between instances of 'str' and 'int'
类型的问题
侧面输入、CoGroupByKey 和连接
由于IMDb
数据的关系性质,我们将不得不处理多个文件来获得我们需要的所有标准。
有两种主要的方法可以做到这一点:Side Inputs
,如果数据足够小,可以轻松地放入内存,或者CoGroupByKey
,这是一种更昂贵的变体,会导致shuffle
。
尽管我们只需要一种方法,但我们将两者都考虑。
让我们获取评级数据,并从上面的输出中抓取一些记录,以确保我们至少有一个匹配:
wget https://datasets.imdbws.com/title.ratings.tsv.gz && gunzip title.ratings.tsv.gz
# Create a small test file
head -1 title.ratings.tsv > title.ratings.100.tsv # header
shuf -n 100 title.ratings.tsv >> title.ratings.100.tsv
grep "tt0067927" title.ratings.tsv >> title.ratings.100.tsv
CoGroupByKey
首先我们来看看CoGroupByKey
。这通常是更明智的选择,除非其中一个数据集的大小要小得多,并且可以作为内存中的数据传递Side Input
。
计算机编程语言
我们可以简单地分割我们的管道,这一次,返回一个PCollection
而不是将它传递给beam.Map(print)
。
with beam.Pipeline(options=pipeline_options) as p:
basic_data = (p | 'Read data' >> beam.io.ReadFromText(known_args.input_basics, skip_header_lines=1)
| 'Parse CSV' >> beam.ParDo(ParseCsv(columns_title_basic))
| 'Clean data' >> beam.ParDo(CleanData(bool_cols=['isAdult'], int_cols=['startYear', 'endYear', 'runtimeMinutes']))
| 'Filter data' >> beam.ParDo(FilterBasicData())
)
由于side inputs
不是PCollections
,我们可以使用Map
将ratings
转换为静态的json
:
rating_data = (p | 'Read data (Details)' >> beam.io.ReadFromText(known_args.input_ratings, skip_header_lines=1)
| 'Parse CSV (Details)' >> beam.ParDo(ParseCsv(columns_ratings))
| 'Clean data (Details)' >> beam.ParDo(CleanData(int_cols=['numVotes'], float_cols=['averageRating']))
| 'Filter data (Details)' >> beam.ParDo(FilterRatingData())
)
rating_data = (p | 'Read data (Details)' >> beam.io.ReadFromText(known_args.input_ratings, skip_header_lines=1)
| 'Parse CSV (Details)' >> beam.ParDo(ParseCsv(columns_ratings))
| 'Clean data (Details)' >> beam.ParDo(CleanData(int_cols=['numVotes'], float_cols=['averageRating']))
| 'Filter data (Details)' >> beam.ParDo(FilterRatingData())
)
一旦我们都有了PCollections
,我们就可以为CoGroupByKey
准备数据:
# Create keys
movie_keys = (basic_data
| 'movie key' >> beam.Map(lambda r: (r['tconst'], r))
#| 'Print' >> beam.Map(print)
)
rating_keys = (rating_data
| 'rating key' >> beam.Map(lambda r: (r['tconst'], r))
)
最后,应用CoGroupByKey
变换和FlatMap
它们在一起:
joined_dicts = (
{'movie_keys': movie_keys, 'rating_keys': rating_keys}
| beam.CoGroupByKey()
| beam.FlatMap(join_ratings)
| 'mergedicts' >> beam.Map(lambda dd: {**dd[0], **dd[1]})
| 'Print' >> beam.Map(print)
)
产生一条记录(我们随机选择输入):
{'tconst': 'tt0067927', 'titleType': 'movie', 'primaryTitle': 'Vanishing Point', 'originalTitle': 'Vanishing Point', 'isAdult': False, 'startYear': 1971, 'endYear': None, 'runtimeMinutes': 99, 'genres': 'Action,Crime,Thriller', 'averageRating': 7.2, 'numVotes': 25933}
去
你会注意到缺少这些“高级”主题的例子,比如CoGroupByKey
,尽管它们实际上存在于 godocs 中。因此,弄清楚这一部分花费的时间比我希望的要长一些,但是一旦理解了其中的逻辑,这是有意义的。
为了使用CoGroupByKey
,我们需要为每个PCollection
创建一个KV
对:
func extractRatingId(r ratingFn) (string, ratingFn) {
return r.tconst, r
}
func extractMovieId(m movieFn) (string, movieFn) {
return m.tconst, m
}
改变我们的产品系列:
// Combine
combined := beam.CoGroupByKey(s,
beam.ParDo(s, extractMovieId, filtered_movies),
beam.ParDo(s, extractRatingId, filtered_ratings))
并按如下方式匹配它们:
func combineFn(tconst string, movieIter func(*movieFn) bool, ratingIter func(*ratingFn) bool, emit func(targetMovie)) {
// Pointers to structs
m := &movieFn{tconst: tconst}
r := &ratingFn{tconst: tconst}
// If match, emit
if movieIter(m) && ratingIter(r) {
fmt.Printf("%v %v\n", tconst, m)
emit(targetMovie{
Id: m.tconst,
TitleType: m.titleType,
PrimaryTitle: m.primaryTitle,
OriginalTitle: m.originalTitle,
IsAdult: m.isAdult,
StartYear: m.startYear,
EndYear: m.endYear,
RuntimeMinutes: m.runtimeMinutes,
Genres: m.genres,
AverageRating: r.averageRating,
NumVotes: r.numVotes,
})
}
}
注意func(*movieFn) bool
,期待一个指向struct
的指针,它将告诉我们是否有匹配。
侧面输入
Side Inputs
比它们在go
中看起来要复杂得多,但在Python
中相对简单。
计算机编程语言
如果我们想使用Side Inputs
,我们可以将我们更小的ratings
PCollection
作为list
:
joined_dicts = (
basic_data
| 'Join' >> beam.ParDo(JoinRatings(), AsList(rating_data))
)
到新的ParDo
:
class JoinRatings(beam.DoFn):
def process(self, movie: dict, ratings_side):
for k in ratings_side:
if k['tconst'] == movie['tconst']:
yield {**movie, **k}
或者用AsDict
调用[0]:
class JoinRatings(beam.DoFn):
def process(self, movie: dict, ratings_side):
if 'tconst' in movie and movie['tconst'] in ratings_side:
yield {**movie, **ratings_side[movie['tconst']]}
并得到相同的结果,这次使用侧面输入。
【0】一个 *dict*
在这里会比一个列表高效很多;不过据我看, *go*
SDK 并不支持 SideInputs 作为*map*
;因此,我用 *lists*
和 *maps*
/ *dicts*
实现了 *Python*
和*SideInputs*
。**
去
我们将在SideInput
这里强调我在Python
部分指出的可能性。
首先,我们需要另一个结构作为我们的输出结构:
*type targetMovie struct {
Id string
TitleType string
PrimaryTitle string
OriginalTitle string
IsAdult bool
StartYear int64
EndYear int64
RuntimeMinutes int64
Genres string
// Ratings
AverageRating float64
NumVotes int64
}*
注意这些值是从导出的,即以大写字母开始。
我们简单地定义了一个combine
类型的函数,它需要一个slice
或ratingFn
,而不是一个PCollection
:
*func combineMoviesRatings(movie movieFn, ratings []ratingFn, emit func(targetMovie)) {
for _, r := range ratings {
if r.tconst == movie.tconst {
emit(targetMovie{
Id: movie.tconst,
TitleType: movie.titleType,
PrimaryTitle: movie.primaryTitle,
OriginalTitle: movie.originalTitle,
IsAdult: movie.isAdult,
StartYear: movie.startYear,
EndYear: movie.endYear,
RuntimeMinutes: movie.runtimeMinutes,
Genres: movie.genres,
AverageRating: r.averageRating,
NumVotes: r.numVotes,
})
}
}
}*
并且用Side Input
这样称呼它:
*combined := beam.ParDo(s, combineMoviesRatings, filtered_movies,
beam.SideInput{Input: filtered_ratings})*
这样,我们得到了相同的输出:
*{"Id":"tt0067927","TitleType":"movie","PrimaryTitle":"Vanishing Point","OriginalTitle":"Vanishing Point","IsAdult":false,"StartYear":1971,"EndYear":0,"RuntimeMinutes":99,"Genres":"Action,Crime,Thriller","AverageRating":7.2,"NumVotes":25933}*
类似于我们在Python
中看到的,做 n 次这个列表比较,我们得到 O(nn)并且会非常无效。*
由于我仍然不知道go
SDK 是否有一个等同于Python
的apache_beam.pvalue.AsDict
,我想出了这个可怕的变通方法,将一个PCollection
作为单例侧输入传递给一个ParDo
,并创建一个map[string]ratingFn
:
*func makeRatingsMap(rr int, ratings []ratingFn, emit func(map[string]ratingFn)) {
m := make(map[string]ratingFn)
for _, r := range ratings {
m[r.tconst] = r
}
emit(m)
}
func combineMoviesRatings(movie movieFn, ratings map[string]ratingFn, emit func(targetMovie)) {
r, ok := ratings[movie.tconst]
if ok {
emit(targetMovie{
Id: movie.tconst,
TitleType: movie.titleType,
PrimaryTitle: movie.primaryTitle,
OriginalTitle: movie.originalTitle,
IsAdult: movie.isAdult,
StartYear: movie.startYear,
EndYear: movie.endYear,
RuntimeMinutes: movie.runtimeMinutes,
Genres: movie.genres,
AverageRating: r.averageRating,
NumVotes: r.numVotes,
})
}
}*
我们称之为:
*// Fake PCollection to only run the next parDo once
fakePCol := beam.CreateList(s, [1]int{
0,
})
// To Map
filteredRatingsMap := beam.ParDo(s, makeRatingsMap, fakePCol, beam.SideInput{Input: filtered_ratings})
// And match
combined := beam.ParDo(s, combineMoviesRatings, filtered_movies,
beam.SideInput{Input: filteredRatingsMap})*
虽然这可能属于“如果它有效,它就不愚蠢!”,我们将在后面的章节中查看在尝试使用Dataflow
时导致的许多问题——不过,请注意,这段代码确实可以在DirectRunner
上工作。
输入-输出
最后,让我们写我们的数据。我们将首先关注写入磁盘。
计算机编程语言
在Python
中,一个简单的调用将创建一个txt
文件,包含上面的json
:
*joined_dicts | 'write' >> beam.io.WriteToText('./movies.txt')*
去
Go
在这方面也非常相似——然而,我们必须将struct
“编码”为内联ParDo
中的JSON
:
*combinedString := beam.ParDo(s, func(v targetMovie) string {
j, _ := json.Marshal(v)
return fmt.Sprintf(string(j))
}, combined)
// Write
textio.Write(s, *output, combinedString)*
DirectRunner(本地)
让我们快速运行并讨论一下性能。上次,在我们的Mandelbrot
基准测试中,go
几乎比Python
快了 45 倍。让我们看看Beam
是如何支撑的。
在DirectRunner
上比较性能几乎是毫无意义的,因为这个运行器是为本地调试和开发而设计的,而不是生产使用。
标杆管理
也就是说,我确实运行了一些基准测试,比较了分别使用Side Inputs
和lists
或dicts
/ maps
的go
和Python
,以及使用三个不同测试数据集的CoGroupByKey
:100 条记录@ 9KB、100,000 条记录@ 9MB 和 1M 条记录@ 100MB。
先看各种Side Inputs
:
这里的速度明显更快,但这是意料之中的,因为它本质上是单线程运行的。使用lists
作为辅助输入要慢得多,以至于我已经放弃了在 1M 记录上运行go
和Python
的as list
用例。
现在,对于CoGroupByKey
:
我们看到一个非常相似的趋势,即go
快了几个数量级。印象深刻!
推荐工作
我还用DirectRunner
上的title.basics
数据对wordcount
例子进行了基准测试,以确保我不会因为无意中编写了根本不同的代码而将苹果和橙子进行比较。
我在go
中是这样做的:
*go install github.com/apache/beam/sdks/go/examples/wordcount
time wordcount --input ~/workspace/beam-examples/data/title.basics.tsv --output countsgo.txt*
和python
:
*time python3 -m apache_beam.examples.wordcount --input ~/workspace/beam-examples/data/title.basics.tsv --output wordcountpy.txt # without multiple workers*
产生了:
这确实符合我们的观察,即Beam
和go
仍然比Python
快一点。
数据流(谷歌云)
现在,在外部运行程序上运行这个程序只给我们留下了一个选项(因为go
不支持其他运行程序),即Google Cloud
的Dataflow
。我应该补充一下,go
的Dataflow
仍然在版本0.5
中,还没有得到官方支持。
确保您拥有:
- 谷歌云项目
- 启用计费(或自由级合格帐户)
- 数据流 API 已启用
gcloud sdk
已安装,已运行gcloud init
和gcloud auth login
GOOGLE_APPLICATION_CREDENTIALS
被设定- 如果你在谷歌上看到这篇文章,确保克里斯蒂安获得更多免费学分(可选步骤)**
计算机编程语言
对于Python
,我们需要使用经过认证的gcloud sdk
来运行它:
*pip3 install --upgrade "apache-beam[gcp]"
python3 movie_pipeline.py --input-basics "${BUCKET}/title.basics.tsv" \
--input-ratings "${BUCKET}/title.ratings.tsv" \
--output "${BUCKET}/out" \
--runner dataflow \
--project "${PROJECT}" \
--region "${REGION}" \
--temp_location "${BUCKET}/tmp/" \
--staging_location "${BUCKET}/binaries/" #\
#--num_workers 4*
但是,它将在Python 3.8
失败,原因是
*Exception: Dataflow only supports Python versions 2 and 3.5+, got: (3, 8)*
所以这里用一个旧版本的Python
,3.7
效果很好。
当作业运行时,它看起来像这样:
一旦完成,就像这样:
Go 数据流
本节将带您了解整个探索和开发过程,包括一些死胡同,以说明我所面临的一些挑战。如果你对解决方案感兴趣,跳到 最后一节 。
对于go
,我们需要使用经过认证的gcloud sdk
来构建和运行它。
首先,确保所有的定制structs
都在init()
中注册,类似于你在Spark
中用Scala
处理Kryo
的操作。
*func init() {
beam.RegisterType(reflect.TypeOf((*ratingFn)(nil)).Elem())
beam.RegisterType(reflect.TypeOf((*movieFn)(nil)).Elem())
beam.RegisterType(reflect.TypeOf((*targetMovie)(nil)).Elem())
}*
构建并运行:
*go build movie_pipeline.go
./movie_pipeline --input-basics "${BUCKET}/title.basics.tsv" \
--input-ratings "${BUCKET}/title.ratings.tsv" \
--output "${BUCKET}/out" \
--runner dataflow \
--project "${PROJECT}" \
--region "${REGION}" \
--temp_location "${BUCKET}/tmp/" \
--staging_location "${BUCKET}/binaries/" \
--worker_harness_container_image=apache/beam_go_sdk:latest #\
#--num_workers 4*
但是当试图使用 **Side Input**
和 **map**
路线时,它不会运行*😗**
*panic: Failed to encode custom coder for type json. Make sure the type was registered before calling beam.Init. For example: beam.RegisterType(reflect.TypeOf((*TypeName)(nil)).Elem())*
注册数据流的类型
这里显而易见的方法是简单地注册类型。然而,在 Beam 中注册类型时,我遇到了一些棘手的问题。
RegisterType 将“外部”类型插入到全局类型注册表中,以绕过序列化并保留完整的方法信息。应该只在 init()中调用它。TODO(wcn):“外部”的规范定义在 v1.proto 中。我们需要这个重要概念的面向用户的副本。
https://godoc . org/github . com/Apache/beam/sdks/go/pkg/beam # register type
我们可以尝试这样注册map
:
*func init() {
// ..
var m map[string]ratingFn
beam.RegisterType(reflect.TypeOf(m).Elem())
}*
产量:
*panic: Failed to encode custom coder for type json. Make sure the type was registered before calling beam.Init. For example: beam.RegisterType(reflect.TypeOf((*TypeName)(nil)).Elem())
Full error:
encoding custom coder map[string]main.ratingFn[json] for type map[string]main.ratingFn
unencodable type map[string]main.ratingFn*
当然,我们可以试着注册一个map
:
*beam.RegisterType(reflect.MapOf(reflect.TypeOf(""), reflect.TypeOf((*ratingFn)(nil)).Elem()))*
但这并没有达到预期的效果。为什么?
深入研究go
SDK 源代码,我发现了以下函数:
*// TypeKey returns the external key of a given type. Returns false if not a
// candidate for registration.
func TypeKey(t reflect.Type) (string, bool) {
fmt.Printf("%v => PckPath: %v Name: %v Kind: %v\n", t, t.PkgPath(), t.Name(), t.Kind())
if t.PkgPath() == "" || t.Name() == "" {
return "", false // no pre-declared or unnamed types
}
return fmt.Sprintf("%v.%v", t.PkgPath(), t.Name()), true
}*
我已经添加了fmt.Printf
用于调试。
*main.ratingFn => PckPath: main Name: ratingFn Kind: struct
main.movieFn => PckPath: main Name: movieFn Kind: struct
main.targetMovie => PckPath: main Name: targetMovie Kind: struct
map[string]main.ratingFn => PckPath: Name: Kind: map
panic: invalid registration type: map[string]main.ratingFn*
此函数检查注册的类型是否实际上是自定义类型;一个map
不是。PckPath
和Name
从未设置,因为map
不是可以通过reflection
注册的自定义类型。
通过吉拉,我找到这个 PR ,把我带到这个单元测试——让我相信go
SDK 不允许maps
使用定制类型。
我们能不能用一个list
代替Side Input
来让我们的生活更轻松?我想你知道答案——但是让我们来谈谈调试,因为我们已经在调试了。
性能工具—性能分析Go
如果我们要使用list
而不是map
,让我们快速使用来描述一下引擎盖下发生了什么。
对于这个测试,我使用了一个有 5,000,000 行的文件,总时钟大约为 500MB。
首先,按照说明为 profiler 添加额外的flags
后运行作业:
*go run movie_pipeline.go --input-basics ../data/title.basics.5M.tsv --input-ratings ../data/title.ratings.5M.tsv --output ./test.txt -cpuprofile ../profiler/cpu.out -memprofile ../profiler/mem.out*
尽管配备了英特尔 i7–9750h、16GB DDR 4 内存和 M.2 NVMe 固态硬盘,这项工作还是在本地花费了 84 分钟。
听听你可怜的笔记本电脑像喷气发动机的声音,并打印结果:
*go tool pprof --svg ./cpu.out > cpu.svg
go tool pprof --svg ./mem.out > mem.svg*
这就是结果:
我们可以看到combineMovieRatings
,低效的列表迭代占用了大部分时间,通过监控htop
,我可以告诉你这个任务一直使用一个线程。
现在,看到迭代一个列表效率非常低并不奇怪——但是结合单线程执行(我认为这是由Splittable DoFn
问题引起的),正在导致大规模的运行时峰值。
定制编码器之旅
在深入研究了吉拉和 GitHub 的 Pull 请求之后,我偶然发现了自定义编码者和他们对内部类型的用法。
我有一个绝妙的主意,为我们的地图类型注册一个自定义编码器:
*func NewCustomMap() (*coder.CustomCoder, error) {
return coder.NewCustomCoder("customMap", reflect.MapOf(reflect.TypeOf(""), reflect.TypeOf((*ratingFn)(nil)).Elem()), encCustomMap, decCustomMap)
}
func encCustomMap(v typex.T) []byte {
dat, _ := json.Marshal(v)
return dat
}
func decCustomMap(data []byte) typex.T {
return string(data)
}*
不幸的是,[RegisterCoder](https://github.com/apache/beam/blob/master/sdks/go/pkg/beam/forward.go#L95)
看起来是这样的:
*func RegisterCoder(t reflect.Type, encoder, decoder interface{}) {
runtime.RegisterType(t)
runtime.RegisterFunction(encoder)
runtime.RegisterFunction(decoder)
coder.RegisterCoder(t, encoder, decoder)
}*
因此,也调用RegisterType(t)
,这将再次无法注册我们的类型或编码器。
地图-> JSON ->地图
虽然我很有可能误解了上面所有的go
代码和Jira
标签,但我的下一个方法是自己做json
解析。
首先,我们没有返回地图,而是返回了[]byte
,它返回了一个json
字符串:
*func makeRatingsMap(rr int, ratings []ratingFn, emit func([]byte)) {
m := make(map[string]ratingFn)
for _, r := range ratings {
m[r.tconst] = r
}
jsonMap, err := json.Marshal(m)
if err != nil {
log.Fatal(err)
}
emit(jsonMap)
}*
然后,在我们的组合函数中,我们将数据unmarshall
到一个map[string]ratingFn
中。
*func combineMoviesRatings(movie movieFn, ratings []byte, emit func(targetMovie)) {
ratingsMap := make(map[string]ratingFn)
err := json.Unmarshal(ratings, &ratingsMap)
if err != nil {
log.Fatal(err)
}
r, ok := ratingsMap[movie.tconst]
if ok {
emit(targetMovie{
Id: movie.tconst,
TitleType: movie.titleType,
PrimaryTitle: movie.primaryTitle,
OriginalTitle: movie.originalTitle,
IsAdult: movie.isAdult,
StartYear: movie.startYear,
EndYear: movie.endYear,
RuntimeMinutes: movie.runtimeMinutes,
Genres: movie.genres,
AverageRating: r.averageRating,
NumVotes: r.numVotes,
})
}
}*
不幸的是,现在每一步都需要执行marshalling
,这极大地改变了本地性能数据:
如您所见,json (*decodeState)
和mapassign
导致了作业的巨大开销,至少在DirectRunner
上,不是一个可行的替代方案。
从头再来
此时,我们必须重新访问CoGroupByKey
或其他选项;然而,这些问题确实显示了一些更奇怪的(我可以补充一下,完全没有记录的)问题和 SDK 的不足。
说一个Side Input
是一个比CoGroupByKey
更好的解决方案是有争议的,但是在我的例子中,我一般不能使用一个map
作为一个Side Input
。**
最终在数据流上运行
在切换回CoGroupByKey
之后,这些变化最终允许我将任务提交给Dataflow
。请记住,*go*
SDK 还没有得到数据流的官方支持。
这里,我们还可以通过向我们的struct
添加元数据来为BigQuery
添加另一个输出:
*type targetMovie struct {
Id string `bigquery:"Id"`
TitleType string `bigquery:"TitleType"`
PrimaryTitle string `bigquery:"PrimaryTitle"`
OriginalTitle string `bigquery:"OriginalTitle"`
IsAdult bool `bigquery:"IsAdult"`
StartYear int64 `bigquery:"StartYear"`
EndYear int64 `bigquery:"EndYear"`
RuntimeMinutes int64 `bigquery:"RuntimeMinutes"`
Genres string `bigquery:"Genres"`
// Ratings
AverageRating float64 `bigquery:"AverageRating"`
NumVotes int64 `bigquery:"NumVotes"`
}*
并写信给 BQ:
*if *bq != "" {
project := gcpopts.GetProject(ctx)
bigqueryio.Write(s, project, *bq, combined)
}*
提交后,我们将看到我们的 DAG:
可拆分 DoFns
免责声明:在需要加载大文件的用例中go
SDK 的性能可能不如Python
和Java
。**
这是因为***go**
SDK 不支持 可拆分 DoFns 。参考见 BEAM-3301 。***
数据流性能
在比较性能时,我们将考察所有 3 种方法:
- 使用简单列表作为
Side Input
- 使用
dict
作为Side Input
(仅限 Python) - 使用
CoGroupByKey
实际上,我必须计算 Python 上的list
性能,因为作业在大约 2 小时后仍在运行,每秒大约 8-15 个元素。我通过查看CoGroupByKey
运行的预期输出和运行时间来估算运行时间,因此实际数字可能会更差!
这符合我们在DirectRunner
- go
上看到的更快,尽管没有上一个 Mandelbrot 示例中的“常规”go
与Python
代码快。我们无法比较dict
/ map
逻辑,因为它只在DirectRunner
上运行。
[0]我不确定为什么 *go*
SDK 不产生“记录/秒”指标
绩效总结
上次,在我们的Mandelbrot
基准测试中,go
几乎比Python
快了 45 倍。
这一次,总结是这样的:
所有运行程序和用例的平均性能增益(由于开销和近似运行时间,不包括 100 条记录)是1290.19%——如果我们从list
运行时间中取近似值,我们会看到 1351.40% ,尽管这并不真正公平。**
无论如何,这确实令人印象深刻。
结论
我们学到了什么?
动态与静态类型& dict
与struct
这部分虽然非常主观,但让我思考了很多。当我第一次开始深入研究第 1 部分时,对我来说很明显Python
缺乏静态类型往往是一种诅咒,而不是一种祝福。我不得不花费大量的时间夜以继日地想弄明白为什么一项工作(无论是Beam
、Spark
、Pandas
、Dask
还是其他什么)不能完成我想要的工作,或者更迫切地说,当我从用户那里得到反馈说他们的数据看起来“不正常”时,这通常是由类型问题引起的。
我在第 1 部分中对此进行了阐述——Python 不强制类型化。我经常使用外部库,比如numpy
,来编写在处理数据管道时使用外部配置强制输入的类。这可能遵循这样的逻辑“你的字典应该有属性averageTemp
( if 'averageTemp' in dict
),那就是一个FLOAT64
。FLOAT64
映射到 numpy 的np.float64
;为了确保它确实是FLOAT64
,我们将在作业的某一点尝试解析;如果失败,使用except
静默捕捉并将其设置为None
。
另一方面,在go
中,我不能舒服地那样做。我被迫遵守结构和类型——我甚至不能设置一个string
到nil
,尽管我可能已经非常习惯了。
现在,什么更好?嗯,我认为这是风格和设计的角度问题。虽然我发现在 Python 中“生产”级的工作通常很困难,但是由于上面的边缘情况和解决方法(请记住:如果我的数据不符合数据库规定的格式,我就是问题的一方!),可以是挑战。
与此同时,就我个人而言,在go
写我的工作比在Python
花的时间要长得多——因为我需要更多地思考我要做什么。经常像我对待bash
或zsh
一样对待Python
——输入一些东西,希望它有用。引导我去…
编译程序
…编译器。编译器是我已经学会既讨厌又喜欢的东西。由于我没有提到它,让我用几个精选的例子告诉你编译器(或者更确切地说是go
工具链)是做什么的:
- 不能有未使用的变量
- 您不能有未使用的导入
- 你不能使用错误的类型
我说的cannot
是指:“无论是go run
还是go build
都不会产生二进制”。
似乎很合理,对吧?嗯——借用我的评论,我发现它既有帮助又乏味。编写go
代码就像编写Python
代码,而pylint
处于高度戒备状态——它不会让你做任何可能让编译器不舒服的事情,同时,确保你所产生的东西将实际工作,而没有副作用。
同时,具体到Dataflow
,它显然不会捕捉任何特定于工具的东西——例如,不在init()
中注册类只会在您提交作业时成为一个问题。
Python
,另一方面,让我写的代码得到 1/10 的分数pyLint
,这可能是未来 me 的问题。
可读性
我喜欢Python
中Beam
的语法。一旦你克服了它看起来有多“奇怪”,用类似伪代码的东西来构建DAG
确实是一个很好的方法。
go
的方法更加学术化。它遵循标准的语言流程——每个函数返回一个PCollection
。这更加直观,但是我发现它失去了Python
的“DAG-as-code”逻辑的好处。
但是我会让你自己判断——在 GitHub 上查看Python
和go
的完整代码。
可拆分 DoFns 和性能
go
中的性能提升是巨大的,老实说,出乎意料。平均而言, 1290.19% 的提高是不可小觑的。
Splittable DoFns
另一方面,一旦我们越过相对较小的数据集,开始谈论多个 GB 或 TB,这实际上是一件大事——即使不是交易的破坏者,也使这种练习本质上主要是学术性的。
证明文件
这是我对 SDK 最大的不满之一,抱怨让我感觉很糟糕,因为绝对没有什么能阻止我致力于此(除了时间)。
go
的Beam
文档非常乏味——许多功能和示例只适用于Python
和Java
。至少可以说,围绕go
寻找例子、逻辑和整体架构细节是一项挑战。我主要依靠godocs
和阅读相当多的源代码。
上面的报道应该可以证明这一点——我遇到了很多死胡同。
我使用go
SDK 的过程看起来有点像这样:
- 在 Beam 的网站上吗?(通常没有)
- 有带注释的例子吗?我克隆了回购,用
grep
搜索关键字;没有很好的例子概述,提供的例子通常没有注释 - 难道是在
godocs
里?如果是,他们给出例子或者解释如何使用函数性吗?(参见上面的coder
死胡同) - 读取
go
SDK 源代码
不用说,Google / StackOverflow 的结果几乎为零。
和睦相处
我已经在第 1 部分提到过这一点,但是考虑到go
SDK 的实验性质,与Beam
特性和Runners
的兼容性显然不如在Python
或Java
中那么好。然而,对于简单的管道和使用Dataflow
作为流道,go
确实涵盖了基本内容。然而,你确实失去了我在引言中提到的一些好处——更少的跑步者意味着更少的选择。
那又怎样?
go
是一种有趣的语言,但是在花了很多时间写这些文章、摆弄代码、文档和环境之后,我现在不推荐使用go
作为Beam
管道——至少不推荐作为一个总括声明。
当然——它比Python
有优势,语言本身清晰,有据可查,而且快速。
然而,一旦你超越了标准用例、Splittable DoFns
问题、文档和兼容环境,功能性的缺乏使得使用Python
(或Java
)作为默认更加合理。虽然更熟悉Beam
的go
SDK 的人肯定可以解决我的一些问题——就像上面的map
问题——但是仍然有很多事情使得在Dataflow
管道的生产中使用go
至少是有问题的。
然而,对于更简单的管道——我相信,如果你可以接受一个官方尚未支持的运行器(Dataflow
),那么性能提升是值得的。
也就是说,我肯定会密切关注Beam
SDK 的进展,如果有机会,我希望在未来致力于它——因为go
是一门非常有趣的语言,不能忽视。
所有的开发和基准测试都是在 GNU/Linux [PopOS!20.04 在内核 5.4 上]在 2019 System76 Gazelle 笔记本电脑上使用 12 个英特尔 i7–9750h v cores @ 4.5 GHz 和 16GB RAM,使用 0.5 版本的 data flows Go Runner
最初发表@chollinger.com/blog
原载于 2020 年 7 月 6 日https://chollinger.com。**