R-和-JavaScript-高级数据可视化-全-

news/2024/10/2 3:56:17

R 和 JavaScript 高级数据可视化(全)

原文:Pro Data Visualization Using R and JavaScript

协议:CC BY-NC-SA 4.0

一、背景

当本文的第一版发布时,在 web 开发领域出现了一个新概念:使用数据可视化作为交流工具。今天,网络上到处都是信息图;然而,这个概念在其他领域和部门已经存在了好几代了。在您工作的公司,您的财务部门可能使用数据可视化来表示内部和外部的财务信息;看看几乎所有上市公司的季度收益报告就知道了。它们充满了图表,以显示季度收入,或年同比收入,或其他大量的历史财务数据。所有这些都是为了在一个简单易懂的图形中显示大量的数据点,可能是一页又一页的数据点。

比较一下 2007 年谷歌季度收益报告中的柱状图(啊,那时谷歌还是一家“小”公司;见图 1-1 )以表格形式显示其所基于的数据子集(见图 1-2 )。

img/313452_2_En_1_Fig2_HTML.jpg

图 1-2

表格形式的类似收入数据

img/313452_2_En_1_Fig1_HTML.jpg

图 1-1

谷歌 2007 年第四季度收入以条形图显示

条形图的可读性更强了。从它的形状我们可以清楚地看到收益在上升,并且每个季度都在稳步上升。通过颜色编码,我们可以看到收益的来源,通过注释,我们可以看到这些颜色编码代表的精确数字以及年同比百分比。

对于表格数据,您必须阅读左边的标签,将右边的数据与这些标签对齐,进行自己的汇总和比较,并得出自己的结论。需要做更多的前期工作来获取表格数据,并且存在这样一种非常真实的可能性,即你的受众要么不理解这些数据(因此围绕这些数据产生了他们自己的不正确的故事),要么因为获取这些信息需要做大量的工作而完全听不进去。

不仅仅是财务部门使用可视化来交流大量的数据。也许您的运营部门使用图表来传达服务器的正常运行时间,或者您的客户支持部门使用图表来显示呼叫量。不管是哪种情况,工程和 web 开发团队最终接受这一点也就不足为奇了。

作为任何部门、团体或行业的一部分,我们都有大量的相关数据,这些数据对我们来说非常重要,首先要了解这些数据,以便我们能够完善和改进我们所做的事情,同时还要与我们的利益相关方进行沟通,展示我们的成功或验证资源需求,或者规划来年的战术路线图。

在我们这样做之前,我们需要了解我们在做什么。我们需要了解什么是数据可视化,对它们的历史有一个大致的了解,何时使用它们,以及如何在技术上和伦理上使用它们。

什么是数据可视化?

好吧,那么什么是数据可视化呢?数据可视化是收集、分析和图形化表示经验信息的艺术和实践。它们有时被称为信息图形(“信息图形”),甚至只是。不管你叫它什么,可视化数据的目标是讲述数据中的故事。讲述这个故事的前提是在非常深的层次上理解数据,并从数字中数据点的比较中收集洞察力。

存在用于制作数据可视化的语法,即具有直接已知上下文的图表形式的模式。在本书的后面,我们将专门用一章来介绍每一种重要的图表类型。

时间序列图表

时序图显示随时间的变化。参见图 1-3 中的时间序列图,该图显示了来自 Google Trends ( www.google.com/trends/ )的关键词“数据可视化”的加权流行度。

img/313452_2_En_1_Fig3_HTML.jpg

图 1-3

Google Trends 中关键字“数据可视化”的加权趋势时间序列

请注意,垂直 y 轴显示的是一系列数字,从 20 增加到 100。这些数字代表加权搜索量,其中 100 是我们术语的峰值搜索量。在水平 x 轴上,我们看到从 2007 年到 2012 年。图表中的线条代表两个轴,即每个日期的给定搜索量。

从这个小样本中,我们可以看到这个词的受欢迎程度增加了两倍多,从 2007 年初的 29 到 2012 年底的 100。

条形图

条形图显示数据点的比较。参见图 1-4 中的条形图,该图展示了按国家对关键词“数据可视化”的搜索量,其数据也来自 Google Trends。

img/313452_2_En_1_Fig4_HTML.jpg

图 1-4

谷歌趋势按地区对关键词“数据可视化”的搜索量进行细分

我们可以在 y 轴上看到国家的名称,在 x 轴上看到从 0 到 100 的标准化搜索量。但是,请注意,没有给出时间度量。此图表代表一天、一个月还是一年的数据?

还要注意,我们不知道度量单位是什么。我强调这些要点不是为了回答它们,而是为了展示这种特殊图表类型的局限性和缺陷。我们必须始终意识到,我们的观众不会带来与我们相同的体验和背景,所以我们必须努力让我们的视觉化故事尽可能不言自明。

直方图

直方图是一种在两个轴上显示连续数据的条形图。它用于显示数据的分布或信息组出现在数据中的频率。参见图 1-5 中的柱状图,它显示了从 1980 年到 2012 年,纽约时报每年发表的多少篇文章以某种方式与数据可视化主题相关。从图表中我们可以看出,自 2009 年以来,这个话题的频率一直在上升。

img/313452_2_En_1_Fig5_HTML.jpg

图 1-5

显示纽约时报关于数据可视化文章分布的直方图

dota 地图

数据图用于显示空间区域上的信息分布。图 1-6 显示了一张数据图,用于展示美国各州对搜索词“数据可视化”的兴趣。

img/313452_2_En_1_Fig6_HTML.jpg

图 1-6

美国各州对“数据可视化”的兴趣数据图(数据来自 Google Trends)

在本例中,阴影较深的州表示对搜索词更感兴趣。(这些数据也来自 Google Trends,谷歌对“数据可视化”一词的搜索频率证明了这一点。)同样值得注意的是,虽然深色往往被用来表示更大的影响,但如果没有图例,我们无法确定这一点。

散点图

像条形图一样,散点图用于比较数据,但特别是暗示数据中的相关性,或者数据可能以某种方式依赖或相关。参见图 1-7 ,其中我们使用来自 Google Correlate ( www.google.com/trends/correlate )的数据,寻找关键词“什么是数据可视化”和关键词“如何创建数据可视化”的搜索量之间的关系

img/313452_2_En_1_Fig7_HTML.jpg

图 1-7

散点图,检查与“数据可视化”、“如何创建”和“是什么”相关的搜索量之间的相关性

这张图表显示了数据中的正相关性,这意味着随着一个术语受欢迎程度的上升,另一个术语也会上升。因此,这张图表表明,随着越来越多的人了解数据可视化,越来越多的人希望学习如何创建数据可视化。

关于相关性,需要记住的重要一点是,它并不暗示直接原因——相关性不是因果关系。仅仅因为两个数向同一个方向移动,并不意味着一个引起另一个的变化。总会有第三个变量,或者说是巧合,导致了这种相关性。

历史

如果我们在谈论数据可视化的历史,那么数据可视化的现代概念很大程度上是从 William Playfair 开始的。威廉·普莱费尔是一名工程师、会计师、银行家,也是一名多才多艺的人,他一手创建了时间序列图、条形图和泡沫图。Playfair 的图表出版于 18 世纪末到 19 世纪初。他非常清楚自己的创新是同类创新中的第一个,至少在统计信息交流领域是如此,他在书中花了大量篇幅描述如何实现思维跳跃,将线条和线条视为货币等实物。

Playfair 最出名的是他的两本书:商业和政治地图集和 ?? 统计年鉴。《?? 商业和政治地图集》出版于 1786 年,关注从国债到贸易数字甚至军费开支等不同方面的经济数据。它还首次印刷了时间序列图和条形图。

他的统计年鉴聚焦于当时欧洲主要国家资源的统计信息,并引入了气泡图。

Playfair 的图表有几个目标,其中可能会引起争议,评论工人阶级的消费能力下降,甚至展示大英帝国进出口数据的优势平衡,但最终他最广泛的目标是以容易消化、普遍理解的格式传达复杂的统计信息。

Note

多亏了霍华德·怀纳、伊恩·斯彭斯和剑桥大学出版社,这两本书最近才再版。

Playfair 有几个同时代的人,包括约翰·斯诺博士,他制作了我个人最喜欢的图表:霍乱地图。霍乱地图是一个信息图形应该有的一切:它易于阅读,信息丰富,最重要的是,它解决了一个真正的问题。

霍乱地图是一种数据地图,它概述了 1854 年伦敦爆发的所有霍乱诊断病例的位置(见图 1-8 )。阴影区域是记录的霍乱死亡人数,地图上的阴影圆圈是水泵。仔细观察,记录的死亡人数似乎是从布罗德街的水泵辐射出来的。

img/313452_2_En_1_Fig8_HTML.jpg

图 1-8

约翰·斯诺的霍乱地图

斯诺博士关闭了宽街水泵,疫情结束了。

漂亮,简洁,有逻辑性。

另一个具有历史意义的信息图表是佛罗伦萨·南丁格尔和威廉·法尔绘制的《东部军队死亡原因图》。该图表如图 1-9 所示。

img/313452_2_En_1_Fig9_HTML.jpg

图 1-9

弗洛伦斯·南丁格尔和威廉·法尔绘制的东部军队死亡原因图表

南丁格尔和法尔在 1856 年制作了这张图表,以展示可预防死亡的相对数量,并在更高的层面上改善军事设施的卫生条件。请注意,南丁格尔和法尔可视化是一个风格化的饼图。饼图通常是一个圆形,代表给定数据集的整体,圆形的切片代表整体的百分比。饼图的有用性有时会引起争论,因为可以认为辨别角度之间的差异比确定条形的长度或笛卡尔坐标中线条的位置更难。Nightingale 似乎避免了这个陷阱,它不仅有楔形持有值的角度,而且还改变了切片的相对大小,使它们避开了包含圆的界限,并表示相对值。这可能会赢得一些饼状图的批评者;但是,在科学和学术界的某些圈子里,并不存在一个好的饼状图!

以上所有例子都有他们试图解决的特定目标或问题。

Note

丰富全面的历史超出了本书的范围,但如果你对一个深思熟虑的、令人难以置信的研究分析感兴趣,请务必阅读爱德华·塔夫特的量化信息的可视化显示

现代景观

数据可视化正处于现代复兴的过程中,这在很大程度上是由于用于存储日志的廉价存储空间以及用于分析和绘制这些日志中的信息的免费和开源工具的激增。

从消费和欣赏的角度来看,有专门研究和讨论信息图形的网站。有一些通用的网站,如 FlowingData,它们聚集并讨论来自网络的数据可视化,从天体物理学时间表到国会上使用的模拟可视化。

来自 FlowingData About 页面( http://flowingdata.com/about/ )的使命陈述恰当地如下:“FlowingData 探索了设计师、统计学家和计算机科学家如何使用数据来更好地了解我们自己——主要是通过数据可视化。”

还有更专业的网站,如 quantifiedself.com,专注于收集和可视化关于自己的信息。甚至还有关于数据可视化的网络漫画,最典型的是兰道尔·门罗运营的 xkcd.com。兰德尔迄今为止创造的最著名和最热门的可视化工具之一是辐射剂量图。我们可以看到图 1-10 中的辐射剂量图(此处有高分辨率: http://xkcd.com/radiation/ )。

img/313452_2_En_1_Fig10_HTML.jpg

图 1-10

兰道尔·门罗的辐射剂量图。请注意,在此可视化中,以一个图表中的单个块来表示的比例范围被分解,以显示全新的上下文和信息的微观世界。这种模式一遍又一遍地重复,显示出难以置信的信息深度

这张图表是为了应对 2011 年福岛第一核电站的核灾难而制作的,旨在澄清围绕灾难所做比较的错误信息和误解。它通过展示来自其他人或一根香蕉等辐射源的辐射量的规模差异来实现这一点,最终达到致命剂量的辐射量——这与在切尔诺贝利灾难附近度过十分钟相比是如何的。

在过去的 25 年里,作家兼耶鲁大学名誉教授爱德华·塔夫特一直致力于提高信息图形的标准。他出版了开创性的书籍,详细介绍了数据可视化的历史,甚至可以追溯到比 Playfair 更早的制图学的起源。他的原则之一是,通过增加图表中变量或数据点的数量,并消除他所创造的图表垃圾的使用,最大限度地增加每个图表中包含的信息量。根据 Tufte 的说法,图表垃圾是包含在图表中的任何不是信息的东西,包括装饰或粗而俗气的箭头。

Tufte 还发明了迷你图,这是一种时间序列图,去掉了所有轴,只保留了趋势线,以显示数据点的历史变化,而无需考虑确切的背景。迷你图应该足够小,以便与文本正文对齐,大小与周围的字符相似,并且无论文本的上下文是什么,都可以显示最近或历史的趋势。

为什么要数据可视化?

在威廉·普莱费尔对《商业与政治地图集》的介绍中,他合理地解释说,正如代数是算术的缩写,图表也是“简化和促进从一个人向另一个人传递信息的方式”将近 300 年后,这一原则依然如故。

数据可视化是呈现复杂多变的信息量的通用方式,正如我们在季度收益报告的开头示例中看到的那样。它们也是用数据讲述故事的有力方式。

假设您面前有 Apache 日志,其中有数千行都类似于以下内容:

127.0.0.1 - - [10/Dec/2012:10:39:11 +0300] "GET / HTTP/1.1" 200 468 "-" "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.3) Gecko/20061201 Firefox/2.0.0.3 (Ubuntu-feisty)"
127.0.0.1 - - [10/Dec/2012:10:39:11 +0300] "GET /favicon.ico HTTP/1.1" 200 766 "-" "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.3) Gecko/20061201 Firefox/2.0.0.3 (Ubuntu-feisty)"

其中,我们看到 IP 地址、日期、请求的资源和客户机用户代理。现在想象这种情况重复了数千次——如此多次以至于你的眼睛有点呆滞,因为每条线都与其周围的线非常相似,以至于很难辨别每条线的终点,更不用说其中存在什么累积趋势了。

通过使用一些分析和可视化工具(如 R ),甚至是商业产品(如 Splunk ),我们可以巧妙地从日志中提取出各种有意义和有趣的故事,从特定 HTTP 错误发生的频率和针对哪些资源,到我们最广泛使用的 URL,到我们用户群的地理分布。

这只是我们的 Apache 访问日志。想象一下撒下一张大网,收集发布信息、错误和生产事件。对于我们所做的事情,我们可以收集什么样的见解:从我们的速度如何影响我们的缺陷密度,到我们的 bug 如何分布在我们的特性集中。还有什么更好的方式来交流这些发现,讲述这些故事,而不是通过一个普遍可消化的媒介,如数据可视化?

这本书的重点是探索我们作为开发人员如何利用这种实践和媒介作为持续改进的一部分——既识别和量化我们的成功和改进的机会,又更有效地交流我们的学习和进展。

工具

我们可以使用许多优秀的工具、环境和库来分析和可视化我们的数据。接下来的两节将对它们进行描述。

语言、环境和库

与 web 开发人员最相关的工具是 Splunk、R 和 D3(数据驱动文档)JavaScript 库。参见图 1-11 了解他们的兴趣对比(来自 Google Trends)。

img/313452_2_En_1_Fig11_HTML.jpg

图 1-11

对 Splunk、R 和 D3 的兴趣随时间变化的 Google 趋势分析

从图中我们可以看出,R 从 200 开始就有了稳定一致的利息额;Splunk 在 2005 年左右推出了该图表,在 2006 年左右出现了兴趣高峰,此后一直稳步增长,直到 2019 年才开始逐渐减弱。至于 D3,我们看到它在 2011 年左右开始达到顶峰,当时它被引入,它的前身 Protovis 已经日落。自 2013 年以来,r 和 D3 的兴趣保持相对稳定。

让我们从许多开发人员、科学家和统计学家选择的工具开始:R 语言。我们将在下一章深入探讨 R 环境和语言,但是现在,知道它是用于统计分析和图形显示的开源环境和语言就足够了。它功能强大,使用有趣,最重要的是,它是免费的。

在过去的几年里,人们对 Splunk 的兴趣稳步增长,这是有充分理由的。一旦设置好,它就很容易使用,具有极好的伸缩性,支持多个并发用户,并使每个人都能轻松地获得数据报告。您只需设置它来使用您的日志文件;然后,您可以进入 Splunk 仪表板,运行这些日志中关键值的报告。Splunk 创建可视化作为其报告功能和警报的一部分。虽然 Splunk 是一款商业产品,但它也提供免费试用版,可在此处获得: www.splunk.com/download

D3 是一个 JavaScript 库,允许我们制作交互式可视化。这是 Protovis 的官方后续版本。Protovis 是一个 JavaScript 库,由斯坦福大学的斯坦福可视化小组于 2009 年创建。Protovis 于 2011 年日落,创作者推出了 D3。我们将在第四章详细探讨 D3 库。

分析工具

除了前面提到的语言和环境,还有许多在线分析工具。

一个伟大的分析和研究托管工具是谷歌趋势。Google Trends 允许你比较搜索词的趋势。它提供了围绕这些趋势的各种伟大的统计信息,包括比较它们的相对搜索量(见图 1-12 )、这些趋势来自的地理区域(见图 1-13 )以及相关的关键词。

img/313452_2_En_1_Fig13_HTML.jpg

图 1-13

谷歌趋势数据地图显示了对关键词产生兴趣的地理位置

img/313452_2_En_1_Fig12_HTML.jpg

图 1-12

随着时间的推移,谷歌“数据科学家”和“计算机科学家”的趋势;请注意,从 2011 年开始,人们对术语“数据科学家”的兴趣迅速增长,与对术语“计算机科学家”的兴趣相匹配

另一个伟大的分析工具是 Wolfram|Alpha ( http://wolframalpha.com )。Wolfram|Alpha 主页截图见图 1-14 。

img/313452_2_En_1_Fig14_HTML.jpg

图 1-14

Wolfram|Alpha 的主页

Wolfram|Alpha 不是一个搜索引擎。搜索引擎蜘蛛和索引内容。相反,Wolfram|Alpha 是一个问答(QA)引擎,它通过自然语言处理来解析人类可读的句子,并以计算结果做出响应。比方说,你想搜索光速。你可以去 Wolfram|Alpha 网站输入“光速是多少”。请记住,它使用自然语言处理来解析您的搜索查询,而不是关键字查找。

该查询的结果可以在图 1-15 中看到。Wolfram|Alpha 本质上是查找所有关于光速的数据,并以结构化、分类的方式呈现出来。您还可以导出每个结果的原始数据。

img/313452_2_En_1_Fig15_HTML.jpg

图 1-15

Wolfram|Alpha 查询“光速是多少”的结果

流程概述

因此,我们了解什么是数据可视化,对它的历史有一个高层次的理解,并对当前的形势有一个想法。我们开始对如何在我们的世界中使用它有所了解。我们知道一些有助于分析和创建图表的工具。现在让我们来看看涉及的过程。

创建数据可视化包括四个核心步骤:

  1. 识别问题。

  2. 收集数据。

  3. 分析数据。

  4. 将数据可视化。

让我们浏览一下流程中的每个步骤,并重新创建一个以前的图表来演示流程。

发现问题

第一步是确定我们想要解决的问题。这几乎可以是任何事情——从找出为什么您的 bug 积压似乎没有减少并保持下来,到查看在给定的时间内什么功能发布导致了最多的生产事件以及为什么。

对于我们的示例,让我们重新创建图 1-5 ,并尝试量化随着时间的推移,人们对数据可视化的兴趣,正如《?? 时报》关于该主题的文章的数量所代表的那样。

收集数据

我们已经知道要调查什么了,所以我们开始研究吧。如果你试图解决一个问题或者讲述一个关于你自己产品的故事,你当然会从你自己的数据开始——可能是你的 Apache 日志,可能是你的 bug backlog,可能是你的项目跟踪软件的导出。

Note

如果您正专注于围绕您的产品收集指标,并且手头没有数据,那么您需要在仪器上投资。有许多方法可以做到这一点,通常是将日志记录放在您的代码中。至少,您希望记录错误状态并对其进行监控,但是您可能希望扩大跟踪的范围以包括调试目的,同时仍然尊重用户的隐私和公司的隐私政策。在我的书Pro JavaScript Performance:Monitoring and Visualization中,我探索了跟踪和可视化 web 和运行时性能的方法。

数据收集的一个重要方面是决定你的数据应该是哪种格式(如果你幸运的话)或者发现你的数据可以用哪种格式。接下来,我们将了解一些目前常用的数据格式。

JSON 是代表 JavaScript 对象符号的首字母缩写词。您可能知道,它本质上是一种将数据作为序列化 JavaScript 对象发送的方法。我们将 JSON 格式化如下:

[object]{[attribute]: [value],[method] : function(){},[array]: [item, item]
}

另一种传输数据的方式是 XML 格式。XML 有一个预期的语法,其中元素可以有属性,有值,值总是在引号中,并且每个元素必须有一个结束元素。XML 看起来像这样:

<parent attribute="value"><child attribute="value">node data</child>
</parent>

一般来说,我们可以期望 API(或应用程序编程接口)返回 XML 或 JSON 给我们,我们的首选通常是 JSON,因为正如我们所看到的,就使用的字符数量而言,它是一个轻量级得多的选项。

但是如果我们从应用程序中导出数据,它很可能是以逗号分隔值文件或 CSV 的形式。CSV 就像它听起来的那样:用逗号或其他某种分隔符分隔的值:

value1,value2,value3
value4,value5,value6

对于我们的例子,我们将使用纽约时报 API(应用程序编程接口)工具(需要免费注册),可从 http://prototype.nytimes.com/gst/apitool/index.html 获得。API 工具公开了纽约时报提供的所有 API,包括文章搜索 API、竞选财务 API 和电影评论 API。我们需要做的就是选择 API 按钮,然后从显示的选项中选择文章搜索 API 按钮,选择/articlesearch.json 路径,键入我们的搜索查询或我们想要搜索的短语,然后单击“提出请求”。

这会查询 API 并将数据返回给我们,格式为 JSON。我们可以在图 1-16 中看到结果。

img/313452_2_En_1_Fig16_HTML.jpg

图 1-16

纽约时报 API 工具

然后,我们可以将返回的 JSON 数据复制并粘贴到我们自己的文件中,或者我们可以进一步获取一个 API 键,这样我们就可以从我们自己的应用程序中查询 API。

为了我们的例子,我们将把 JSON 数据保存到一个文件中,我们将这个文件命名为 jsNYTimesData.txt 。文件内容的结构如下:

{
"offset": "0",
"results": [{"body": "BODY COPY","byline": "By AUTHOR","date": "20121011","title": "TITLE","url": "http:\/\/  www.nytimes.com/foo.html  "}, {"body": "BODY COPY","byline": "By AUTHOR","date": "20121021","title": "TITLE","url": "http:\/\/  www.nytimes.com/bar.html  "}],
"tokens": ["JavaScript"
],
"total": 2
}

查看高级 JSON 结构,我们看到一个名为offset的属性、一个名为results的数组、一个名为tokens的数组和另一个名为total的属性。offset变量是用于分页的(我们从哪一页的结果开始)。total变量就像它听起来的那样:为我们的查询返回的结果的数量。我们真正关心的是results数组;它是一个对象数组,每个对象对应一篇文章。

文章对象具有名为bodybylinedatetitleurl的属性。

我们现在有了可以开始研究的数据。这让我们进入下一步,分析我们的数据。

DATA SCRUBBING

这里通常有一个隐藏的步骤,任何处理过数据的人都知道:清理数据。通常数据要么没有按照我们需要的格式进行格式化,或者在更糟糕的情况下,数据是脏的或不完整的。

在最好的情况下,您的数据只需要重新格式化甚至连接,继续这样做,但一定不要丢失数据的完整性。

脏数据包含无序的字段、包含明显错误信息的字段(想想邮政编码中的破折号)或数据中的缺口。如果您的数据是脏的,您有几个选择:

  • 您可以删除有问题的行,但这可能会损害数据的完整性—一个很好的例子是,如果您正在创建直方图,删除行可能会改变分布并改变您的结果。

  • 更好的选择是联系管理你的数据源的人,如果有更好的版本,试着去获取。

无论是哪种情况,如果数据是脏的,或者只是需要重新格式化才能导入到 R 中,那么在开始分析之前,一定要清理数据。

分析数据

有数据固然很棒,但这意味着什么呢?我们通过分析来确定。

分析是创建数据可视化最关键的部分。只有通过分析,我们才能理解我们的数据,也只有通过理解它,我们才能编造故事与他人分享。

为了开始分析,让我们将数据导入到 R 中。我们将在下一章深入研究这种语言。如果你还不熟悉 R,不要担心与下面的例子一起编码:只需跟随理解发生了什么,并在阅读第 3 和 4 章后返回这些例子。

因为我们的数据是 JSON,所以我们用一个叫rjson的 R 包。这将允许我们用fromJSON()函数读入并解析 JSON:

install.packages("rjson")
library(rjson)
json_data <- fromJSON(paste(readLines("jsNYTimesData.txt"), collapse=""))

这很好,只是数据是以纯文本的形式读入的,包括日期信息。我们不能从文本中提取信息,因为很明显,文本除了是原始字符之外没有上下文意义。因此,我们需要遍历数据,并将其解析为更有意义的类型。

让我们创建一个数据框(一个类似数组的数据类型,我们将在下一章讨论它),循环遍历我们的json_data对象,并解析出date属性中的年、月和日部分。让我们从byline中解析出作者姓名,并检查以确保如果作者姓名不存在,我们用字符串"unknown"替换空值。

df <- data.frame()
for(n in json_data$response$docs){year <-substr(n$pub_date, 0, 4)month <- substr(n$pub_date, 6, 7)day <- substr(n$pub_date, 9, 10)author <- substr(n$byline$original, 4, 30)title <- n$headline$mainif(length(author) < 1){author <- "unknown"}

接下来,我们可以将日期重组为一个 MM/DD/YYYY 格式的字符串,并将其转换为一个日期对象:

datestamp <-paste(month, "/", day, "/", year, sep="")
datestamp <- as.Date(datestamp,"%m/%d/%Y")

最后,在我们离开循环之前,我们应该将这个新解析的作者和日期信息添加到一个临时行中,并将该行添加到我们的新数据框中:

      newrow <- data.frame(datestamp, author, title, stringsAsFactors=FALSE, check.rows=FALSE)df <- rbind(df, newrow)
}
rownames(df) <- df$datestamp
Our complete loop should look like the following:
df <- data.frame()
for(n in json_data$response$docs){year <-substr(n$pub_date, 0, 4)month <- substr(n$pub_date, 6, 7)day <- substr(n$pub_date, 9, 10)author <- substr(n$byline$original, 4, 30)title <- n$headline$mainif(length(author) < 1){author <- "unknown"}datestamp <-paste(month, "/", day, "/", year, sep="")datestamp <- as.Date(datestamp,"%m/%d/%Y")newrow <- data.frame(datestamp, author, title, stringsAsFactors=FALSE, check.rows=FALSE)df <- rbind(df, newrow)
}
rownames(df) <- df$datestamp

请注意,我们的示例假设返回的数据集具有唯一的日期值。如果出现错误,您可能需要清理返回的数据集,以清除任何重复的行。还要注意的是,纽约时报 API 可能会随着时间而改变。在这本书的两次修订之间,API 工具改变了各种标题(例如,“标题”变成了“标题”)。如果这段代码看起来不起作用,您可能需要通读 JSON 数据,看看他们是否又按了一个开关!

一旦我们的数据框被填充,我们就可以开始对数据进行一些分析。让我们首先从每个条目中提取年份,并快速绘制一个茎和叶图,以查看数据的形状。

Note

约翰·图基在他的开创性工作探索性数据分析中创建了茎叶图。茎图和叶图是查看数据形状的快速、高级方式,很像直方图。在茎和叶图中,我们在左边构造“茎”列,在右边构造“叶”列。词干由结果集中最重要的唯一元素组成。叶由与每个茎相关联的值的剩余部分组成。在下面的茎和叶图中,年份是茎,R 表示与给定年份相关的每一行的零。另外需要注意的是,为了获得更简洁的可视化效果,交替的连续行通常被组合成一行。

首先,我们将创建一个新变量来保存年份信息:

yearlist <- as.POSIXlt(df$datestamp)$year+1900

如果我们检查这个变量,我们会看到它看起来像这样:

> yearlist
[1] 2012 2012 2012 2012 2012 2012 2012 2012 2012 2012 2012 2012 2012 2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 2011 2011
[30] 2011 2011 2011 2011 2010 2010 2010 2010 2010 2010 2010 2010 2010 2010 2009 2009 2009 2009 2009 2009 2009 2008 2008 2008 2007 2007 2007 2007 2006
[59] 2006 2006 2006 2005 2005 2005 2005 2005 2005 2004 2003 2003 2003 2002 2002 2002 2002 2001 2001 2000 2000 2000 2000 2000 2000 1999 1999 1999 1999
[88] 1999 1999 1998 1998 1998 1997 1997 1996 1996 1995 1995 1995 1993 1993 1993 1993 1992 1991 1991 1991 1990 1990 1990 1990 1989 1989 1989 1988 1988
[117] 1988 1986 1985 1985 1985 1984 1982 1982 1981

太好了,这正是我们想要的:一年时间来代表每一篇被退回的文章。接下来,让我们创建茎叶图:

> stem(yearlist)
1980 | 0
1982 | 00
1984 | 0000
1986 | 0
1988 | 000000
1990 | 0000000
1992 | 00000
1994 | 000
1996 | 0000
1998 | 000000000
2000 | 00000000
2002 | 0000000
2004 | 0000000
2006 | 00000000
2008 | 0000000000
2010 | 000000000000000000000000000000
2012 | 0000000000000

很有意思。我们看到在 20 世纪 90 年代中期有一些下降的逐步建立,在 2000 年代中期有另一个下降的逐步建立,以及自 2010 年以来的强烈爆发(茎和叶图将年份分成两个一组)。

看到这里,我的脑海里开始想象一个关于越来越受欢迎的主题的故事。但是这些文章的作者呢?也许它们是一两个非常感兴趣的作者的成果,他们对这个主题有相当多的话要说。

让我们探讨一下这个想法,看看我们解析出来的作者数据。让我们来看看数据框中的独特作者:

> length(unique(df$author))
[1] 81

我们看到这些文章有 81 个独特的作者或作者组合!出于好奇,我们来看看每篇文章的作者分类。让我们快速创建一个条形图来查看数据的整体形状(条形图如图 1-17 ):

img/313452_2_En_1_Fig17_HTML.jpg

图 1-17

按作者列出的文章数量的条形图,以便快速可视化

plot(table(df$author), axes=FALSE)

我们去掉了 x 轴和 y 轴,这样我们就可以专注于数据的形状,而不用太担心粒度细节。从形状上,我们可以看到大量相同值的条形;这些作者只写过一篇文章。较高的栏是写了多篇文章的作者。基本上每个条都是一个独特的作者,条的高度表示他们写的文章的数量。我们可以看到,虽然大约有五个杰出的贡献者,但大多数作者平均只有一篇文章。

请注意,我们刚刚创建了几个可视化,作为我们分析的一部分。这两个步骤并不相互排斥;我们经常创建快速的可视化来促进我们自己对数据的理解。正是创建它们的意图使它们成为分析阶段的一部分。这些可视化旨在提高我们对数据的理解,以便我们能够准确地讲述数据中的故事。

我们在这个特定的数据集中看到的讲述了一个主题越来越受欢迎的故事,由各种作者(在柱状图中)越来越多的文章(在 stem 图中)所证明。现在让我们为大众消费做准备。

Note

我们不是在编造或发明这个故事。像信息考古学家一样,我们通过筛选原始数据来揭示这个故事。

可视化数据

一旦我们分析了数据并理解了它(我的意思是真正理解了数据,以至于我们熟悉了它周围的所有细节),一旦我们看到了数据中包含的故事,是时候分享这个故事了。

对于当前的例子,我们已经制作了一个茎和叶图以及一个条形图作为我们分析的一部分。然而,茎和叶图对于分析数据来说是很好的,但是对于传达研究结果来说就不那么好了。茎和叶图中的数字所代表的上下文并不明显。我们制作的柱状图支持了故事的主题,而不是传达主题。

因为我们想展示文章按年份的分布,所以让我们用一个柱状图来讲述这个故事:

hist(yearlist)

参见图 1-18 了解对hist()函数的调用所产生的结果。

img/313452_2_En_1_Fig18_HTML.jpg

图 1-18

年鉴直方图

这是一个好的开始,但是让我们进一步完善它。让我们给条形着色,给图表起一个有意义的标题,并严格定义年份的范围:

hist(yearlist, breaks=(1981:2012), freq=TRUE, col="#CCCCCC", main="Distribution of Articles about Data Visualization\nby the NY Times", xlab = "Year")

这产生了我们在图 1-5 中看到的直方图。

数据可视化的伦理

还记得本章开头的图 1-3 吗,我们在那里看到了搜索词“数据可视化”的加权流行度?通过将数据限制在 2006 年至 2012 年,我们讲述了一个关键词越来越受欢迎的故事,在六年期间,其受欢迎程度几乎翻了一番。但是,如果我们在样本中包括更多的数据点,并将我们的视角扩展到包括 2004 年,会怎么样呢?该扩展时序图如图 1-19 所示。

img/313452_2_En_1_Fig19_HTML.jpg

图 1-19

扩展时间范围的谷歌趋势时间序列图表。请注意,额外的数据点提供了一个更大的背景,讲述了一个不同的故事

这张扩展图讲述了一个不同的故事:描述了 2005 年至 2009 年间受欢迎程度的下降。这张展开的图表还展示了用数据可视化有意或无意地歪曲事实是多么容易。

引用来源

当 Playfair 第一次出版他的商业和政治地图集时,他不得不对抗的最大偏见之一是他的同行对图表准确表示数据的固有不信任。他试图通过在这本书的前两个版本中加入数据表来克服这个问题。

同样,在发布图表时,我们应该始终包括我们的来源,以便我们的受众可以回去独立验证数据,如果他们愿意的话。这很重要,因为我们试图分享信息,而不是囤积信息,我们应该鼓励其他人自己检查数据,并对结果感到兴奋。

注意视觉线索

使用图表作为视觉速记的一个副作用是,当我们查看图表时,我们会带来自己的观点和背景。我们习惯于某些事物,例如红色被用来表示危险或引起注意,绿色表示安全。这些颜色的内涵是色彩理论的一个分支,叫做色彩和谐,至少应该知道你的颜色选择可能意味着什么。

当有疑问时,征求第二意见。当创建我们的图形时,我们经常会选择某种布局或图表。这很自然,因为我们花了时间分析和制作图表。一双新鲜、客观的眼睛应该指出无意的含义或过于复杂的设计,使视觉效果更加清晰。

摘要

本章介绍了一些关于数据可视化的介绍性概念,从进行数据收集和探索到查看构成可视模式的图表,这些可视模式定义了我们如何与数据进行通信。我们回顾了一下数据可视化的历史,从早期的威廉·普莱费尔和佛罗伦萨·南丁格尔到现代的 xkcd.com。

虽然我们在本章中看到了一点代码,但在下一章,我们将开始深入学习 R 的策略,并着手读取数据、塑造数据以及制作我们自己的可视化效果。

二、R 语言入门

在上一章中,我们定义了什么是数据可视化,看了一点媒体的历史,并探索了创建它们的过程。本章深入探讨了创建数据可视化最重要的工具之一:r。

在创建数据可视化时,R 是分析数据和创建可视化不可或缺的工具。我们将在本书的其余部分广泛使用 R,所以我们最好先设置水平。

r 既是一种环境,也是一种语言,用于运行统计计算和生成数据图形。它是由罗斯·伊哈卡和罗伯特·绅士于 1993 年在奥克兰大学创建的。R 环境是您开发和运行 R 的运行时环境。R 语言是你用来开发的编程语言。

r 是 S 语言的继承者,S 语言是一种统计编程语言,于 1976 年诞生于贝尔实验室。

了解 R 控制台

让我们从下载和安装 R 开始。R 可以从位于 www.r-project.org/ 的 R 基金会获得。R 基金会主页截图见图 2-1 。

img/313452_2_En_2_Fig1_HTML.jpg

图 2-1

R 基金会主页

可从综合 R 档案网(CRAN)网站获得预编译二进制: http://cran.r-project.org/ (见图 2-2 )。我们只需选择我们的操作系统和我们想要的 R 版本,就可以开始下载了。

img/313452_2_En_2_Fig2_HTML.jpg

图 2-2

CRAN 网站

下载完成后,我们可以运行安装程序。macOS 的 R 安装程序截图见图 2-3 。

img/313452_2_En_2_Fig3_HTML.jpg

图 2-3

r 在 Mac 上安装

一旦我们完成了安装,我们就可以启动 R 应用程序,并且我们会看到 R 控制台,如图 2-4 所示。

img/313452_2_En_2_Fig4_HTML.jpg

图 2-4

R 控制台

命令行

R 控制台是奇迹发生的地方!这是一个命令行环境,我们可以在其中运行 R 表达式。提高 R 语言速度的最好方法是在控制台中编写脚本,一次编写一部分,通常是尝试您正在尝试做的事情,并对其进行调整,直到获得您想要的结果。当您最终有了一个工作示例时,获取您想要的代码,并将其保存为 R 脚本文件。

R 脚本文件只是包含纯 R 的文件,可以在控制台中使用source命令运行:

> source("someRfile.R")

查看前面的代码片段,我们假设 R 脚本位于当前的工作目录中。我们可以使用getwd()函数查看当前工作目录的方式是:

> getwd()
[1] "/Users/tomjbarker"

我们也可以使用setwd()函数来设置工作目录。注意,除非保存会话,否则对工作目录所做的更改不会跨 R 会话持久化。

> setwd("/Users/tomjbarker/Downloads")
> getwd()
[1] "/Users/tomjbarker/Downloads"

命令历史

R 控制台存储您输入的命令,您可以通过按向上箭头在前面的命令之间循环。点击 escape 按钮返回到命令提示符。我们可以通过单击控制台顶部的“显示/隐藏命令历史记录”按钮,在单独的窗口窗格中查看历史记录。“显示/隐藏命令历史记录”按钮是带有黄色和绿色交替条纹的矩形图标。参见图 2-5 中显示命令历史的 R 控制台。

img/313452_2_En_2_Fig5_HTML.jpg

图 2-5

显示命令历史的 r 控制台

访问文档

要阅读关于特定函数或关键字的 R 文档,只需在关键字前键入一个问号:

> ?setwd

如果要在文档中搜索特定的单词或短语,可以在搜索查询前键入两个问号:

> ??"working directory"

这段代码启动一个显示搜索结果的窗口(见图 2-6 )。“搜索结果”窗口中包含搜索短语的每个主题都有一行,其中包含帮助主题的名称、帮助主题所讨论的功能所在的包以及帮助主题的简短描述。

img/313452_2_En_2_Fig6_HTML.jpg

图 2-6

帮助搜索结果窗口

包装

说到包裹,它们到底是什么?是函数、数据集或对象的集合,可以导入到当前会话或工作区,以扩展我们在 r 中的功能。任何人都可以制作包并分发它。

要安装一个软件包,我们只需输入:

install.packages([package name])

例如,如果我们想要安装ggplot2包——这是一个广泛使用且非常方便的图表包——我们只需在控制台中键入:

> install.packages("ggplot2")

系统会立即提示我们选择想要使用的镜像位置,通常是离我们当前位置最近的位置。从那里开始安装。我们可以在图 2-7 中看到结果。

img/313452_2_En_2_Fig7_HTML.jpg

图 2-7

安装 ggplot2 软件包

压缩后的包被下载并展开到我们的 R 安装中。

如果我们想要使用已经安装的包,我们必须首先将它包含在我们的工作区中。为此,我们使用library()函数:

> library(ggplot2)

可在此处找到 CRAN 提供的套餐列表: http://cran.r-project.org/web/packages/available_packages_by_name.html

要查看已经安装的软件包列表,我们可以简单地调用不带参数的library()函数(根据您的安装和环境,您的软件包列表可能会有所不同):

> library()
Packages in library '/Library/Frameworks/R.framework/Versions/2.15/Resources/library':
barcode                         Barcode distribution plots
base                            The R Base Package
boot                            Bootstrap Functions (originally by Angelo Canty for S)
class                           Functions for Classification
cluster                         Cluster Analysis Extended Rousseeuw et al.
codetools                       Code Analysis Tools for R
colorspace                      Color Space Manipulation
compiler                        The R Compiler Package
datasets                        The R Datasets Package
dichromat                       Color schemes for dichromats
digest                          Create cryptographic hash digests of R objects
foreign                         Read Data Stored by Minitab, S, SAS, SPSS, Stata, Systat, dBase,...
ggplot2                         An implementation of the Grammar of Graphics
gpairs                          gpairs: The Generalized Pairs Plot
graphics                        The R Graphics Package
grDevices                       The R Graphics Devices and Support for Colours and Fonts
grid                            The Grid Graphics Package
gtable                          Arrange grobs in tables
KernSmooth                      Functions for kernel smoothing for Wand & Jones (1995)
labeling                        Axis Labeling
lattice                         Lattice Graphics
mapdata                         Extra Map Databases
mapproj                         Map Projections
maps                            Draw Geographical Maps

导入数据

现在我们的环境已经下载并安装好了,我们知道如何安装我们可能需要的任何包。现在我们可以开始使用 r。

我们通常要做的第一件事是导入您的数据。有几种方法可以导入数据,但最常用的方法是使用read()函数,它有几种风格:

read.table("[file to read]")
read.csv(["file to read"])

为了看到这一点,让我们首先创建一个名为temptext.txt的文本文件,格式如下:

134,432,435,313,11
403,200,500,404,33
77,321,90,2002,395

我们可以将它读入一个名为temptxt的变量:

> temptxt <- read.table("temptext.txt")

注意,当我们给这个变量赋值时,我们没有使用等号作为赋值操作符。我们改为使用箭头<-。这是 R 的赋值操作符,尽管如果你愿意,它也支持等号。但是标准是箭头,我们将在本书中展示的所有示例都将使用箭头。

如果我们打印出temptxt变量,我们会看到它的结构如下:

> temptxtV1
1 134,432,435,313,11
2 403,200,500,404,33
3 77,321,90,2002,395

我们看到我们的变量是一个叫做数据帧的类似表格的结构,R 为我们的数据结构分配了一个列名(V1)和行 id。很快会有更多关于列名的内容。

read()函数有许多参数,一旦数据被导入,您可以使用这些参数来优化数据导入和格式化的方式。

使用标题

header参数告诉 R 将外部文件中的第一行视为包含头信息。第一行成为数据框的列名。

例如,假设我们有一个日志文件,其结构如下:

url, day, date, loadtime, bytes, httprequests, loadtime_repeatview
http://apress.com , Sun, 01 Jul 2012 14:01:28 +0000,7042,956680,73,3341
http://apress.com , Sun, 01 Jul 2012 14:01:31 +0000,6932,892902,76,3428
http://apress.com , Sun, 01 Jul 2012 14:01:33 +0000,4157,594908,38,1614

我们可以像这样将它加载到一个名为wpo的变量中:

  1. apress.com

  2. apress.com

  3. apress.com

> wpo <- read.table("wpo.txt", header=TRUE, sep=",")
> wpourl  day date loadtime bytes httprequests loadtime_repeatview

当我们调用colnames()函数来查看wpo的列名时,我们会看到以下内容:

> colnames(wpo)
[1] "url"     "day"     "date"     "loadtime"
[5] "bytes"     "httprequests"     "loadtime_repeatview"

指定字符串分隔符

sep属性告诉read()函数使用什么作为字符串分隔符来解析外部数据文件中的列。在我们到目前为止看到的所有例子中,逗号是我们的分隔符(正如我们在 wpo 的那一行中明确告诉 R 的那样),但是我们可以使用竖线|或任何其他我们想要的字符。

比方说,我们前面的temptxt例子使用了管道;我们只需将代码更新如下:

  1. 134 432 435 313 11

  2. 403 200 500 404 33

  3. 77 321 90 2002 395

134|432|435|313|11
403|200|500|404|33
77|321|90|2002|395
> temptxt <- read.table("temptext.txt", sep="|")
> temptxtV1  V2  V3   V4  V5

哦,注意到了吗?这次我们实际上得到了不同的列名(V1V2V3V4V5)。之前,我们没有指定分隔符,所以 R 假设每一行都是一大块文本,并将其集中成一列(V1)。

指定行标识符

属性允许我们为我们的行指定标识符。默认情况下,正如我们在前面的例子中看到的,R 使用递增的数字作为行 id。请记住,每行的行名必须是唯一的。

记住这一点,让我们看一下导入一些不同的日志数据,这些数据具有唯一 URL 的性能指标:

url, day, date, loadtime, bytes, httprequests, loadtime_repeatview
http://apress.com, Sun, 01 Jul 2012 14:01:28 +0000,7042,956680,73,3341
http://google.com, Sun, 01 Jul 2012 14:01:31 +0000,6932,892902,76,3428
http://apple.com, Sun, 01 Jul 2012 14:01:33 +0000,4157,594908,38,1614

当我们读入数据时,我们将确保指定将url列中的数据用作数据框的行名:

> wpo <- read.table("wpo.txt", header=TRUE, sep=",", row.names="url")
> wpoday  date                          loadtime   bytes         httprequests  loadtime_repeatview
http://apress.com   Sun  01 Jul 2012 14:01:28  +0000   7042       956680         73            3341
http://google.com   Sun  01 Jul 2012 14:01:31  +0000   6932       892902         76            3428
http://apple.com    Sun  01 Jul 2012 14:01:33  +0000   4157       594908         38            31614

使用自定义列名

我们开始吧。但是如果我们想要列名,但是文件中的第一行不是标题信息呢?我们可以使用col.names参数来指定一个可以用作列名的向量。

让我们来看看。在本例中,我们将使用之前使用的竖线分隔的文本文件:

134|432|435|313|11
403|200|500|404|33
77|321|90|2002|395

首先,我们将创建一个名为columnNames的向量,它将保存我们将用作列名的字符串:

> columnNames <- c("resource_id", "dns_lookup", "cache_load", "file_size", "server_response")

然后,我们将读入数据,将我们的向量传递给col.names参数:

> resource_log <- read.table("temptext.txt", sep="|", col.names=columnNames)
> resource_logresource_id dns_lookup cache_load file_size server_response
1        134        432        435       313              11
2        403        200        500       404              33
3         77        321         90      2002             395

数据结构和数据类型

在前面的例子中,我们触及了很多概念;我们创造了变量,包括向量和数据框架;但我们没怎么谈论它们是什么。让我们后退一步,看看 R 支持的数据类型以及如何使用它们。

R 中的数据类型称为模式,可以是以下几种:

  • 数字的

  • 性格;角色;字母

  • 逻辑学的

  • 复杂的

  • 生的

  • 目录

我们可以使用mode()函数来检查变量的模式。

字符和数字模式对应于字符串和数字(整数和浮点)数据类型。逻辑模式是布尔值。

> n <- 122132
> mode(n)
[1] "numeric"
> c <- "test text"
> mode(c)
[1] "character"
> l <- TRUE
> mode(l)
[1] "logical"

我们可以使用paste()函数执行字符串连接。我们可以使用substr()函数从字符串中提取字符。让我们看一些代码中的例子。

通常,我会保存一个目录列表,我可以从中读取数据或向其中写入图表。然后,当我想要引用数据目录中存在的新数据文件时,我只需将新文件名附加到数据目录中:

> dataDirectory <- "/Users/tomjbarker/org/data/"
> buglist <- paste(dataDirectory, "bugs.txt", sep="")
> buglist
[1] "/Users/tomjbarker/org/data/bugs.txt"

paste()函数获取N数量的字符串并将它们连接在一起。它接受一个名为sep的参数,这个参数允许我们指定一个字符串,我们可以用它作为连接字符串之间的分隔符。我们不希望任何东西来分隔我们传入的空字符串。

如果我们想从字符串中提取字符,我们使用substr()函数。substr()函数接受一个要解析的字符串、一个起始位置和一个终止位置。它返回从起始位置到结束位置的所有字符。(请记住,在 R 中,列表不像大多数其他语言那样基于 0,而是从 1 开始索引。)

> substr("test", 1,2)
[1] "te"

在前面的例子中,我们传入字符串“test”并告诉substr()函数返回第一个和第二个字符。

复数模式适用于复数。原始模式用于存储原始字节数据。

列表数据类型或模式可以是以下三类之一:向量、矩阵或数据框。如果我们为向量或矩阵调用mode(),它们返回它们包含的数据的模式;class()返回类。如果我们在数据帧上调用mode(),它返回类型list

> v <- c(1:10)
> mode(v)
[1] "numeric"
> m <- matrix(c(1:10), byrow=TRUE)
> mode(m)
[1] "numeric"
> class(m)
[1] "matrix" "array"
> d <- data.frame(c(1:10))
> mode(d)
[1] "list"
> class(d)
[1] "data.frame"

请注意,我们只是键入了1:10,而不是 1 到 10 之间的整个数字序列:

v <- c(1:10)

向量是一维数组,一次只能保存单一模式的值。当我们谈到数据框架和矩阵时,R 才真正开始变得有趣。接下来的两节将介绍这些类。

数据帧

我们在本章开始时看到,read()函数接收外部数据并将其保存为数据帧。数据帧就像大多数其他松散类型语言中的数组:它们是保存不同类型数据的容器,由索引引用。但是,需要认识到的主要问题是,数据框将它们包含的数据视为行、列以及两者的组合。

例如,假设数据帧的格式如下:

      col  col  col  col  col
row [ 1 ] [ 1 ] [ 1 ] [ 1 ] [ 1 ]
row [ 1 ] [ 1 ] [ 1 ] [ 1 ] [ 1 ]
row [ 1 ] [ 1 ] [ 1 ] [ 1 ] [ 1 ]
row [ 1 ] [ 1 ] [ 1 ] [ 1 ] [ 1 ]

如果我们试图引用前面数据帧中的第一个索引,就像我们传统上使用数组一样,比如说dataframe[1],R 将返回数据的第一列,而不是第一项。因此数据框是通过它们的列和行来引用的。所以dataframe[1]返回第一列,dataframe[,2]返回第一行。

让我们用代码演示一下。

首先,让我们使用组合函数c()创建一些向量。请记住,向量是同一类型的数据集合。combine 函数接受一系列值,并将它们组合成向量。

> col1 <- c(1,2,3,4,5,6,7,8)
> col2 <- c(1,2,3,4,5,6,7,8)
> col3 <- c(1,2,3,4,5,6,7,8)
> col4 <- c(1,2,3,4,5,6,7,8)

然后,让我们将这些向量组合成一个数据帧:

> df <- data.frame(col1,col2,col3,col4)

现在让我们打印数据框以查看其内容和结构:

> dfcol1 col2 col3 col4
1    1    1    1    1
2    2    2    2    2
3    3    3    3    3
4    4    4    4    4
5    5    5    5    5
6    6    6    6    6
7    7    7    7    7
8    8    8    8    8

请注意,它采用了每个向量,并使每个向量成为一列。还要注意,每一行都有一个 ID;默认情况下,它是一个数字,但我们可以覆盖它。

如果我们引用第一个索引,我们会看到数据框返回第一列:

> df[1]col1
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8

如果我们在 1 前面加一个逗号,我们引用第一行:

> df[,1]
[1] 1 2 3 4 5 6 7 8

因此,通过指定[column, row]来访问数据帧的内容。

矩阵的工作方式大致相同。

矩阵

矩阵就像数据框一样,它们包含行和列,可以被任意一个引用。两者的核心区别在于数据帧可以保存不同的数据类型,而矩阵只能保存一种类型的数据。

这体现了哲学上的差异。通常,您使用数据框来保存从外部读入的数据,比如从平面文件或数据库中读入的数据,因为这些数据通常是混合型的。您通常将数据存储在要应用函数的矩阵中(稍后将详细介绍如何对列表应用函数)。

要创建一个矩阵,我们必须使用matrix()函数,传入一个向量,并告诉该函数如何分配向量:

  • nrow参数指定矩阵应该有多少行。

  • ncol参数指定列的数量。

  • byrow参数告诉 R,如果TRUE的话,向量的内容应该通过跨行迭代来分布,如果FALSE的话,应该通过列来分布。

> content <- c(1,2,3,4,5,6,7,8,9,10)
> m1 <- matrix(content, nrow=2, ncol=5, byrow=TRUE)
> m1[,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    6    7    8    9   10
>

注意,在前面的例子中,m1矩阵是一行一行水平填充的。在下面的例子中,m1矩阵是按列垂直填充的:

> content <- c(1,2,3,4,5,6,7,8,9,10)
> m1 <- matrix(content, nrow=2, ncol=5, byrow=FALSE)
> m1[,1] [,2] [,3] [,4] [,5]
[1,]    1    3    5    7    9
[2,]    2    4    6    8   10

请记住,如果这些数字是一个序列,我们可以只键入以下内容,而不是手动键入前面的content向量中的所有数字:

content <- (1:10)

我们用方括号引用矩阵中的内容,分别指定行和列:

> m1[1,4]
[1] 7

如果数据帧只包含单一类型的数据,我们可以将数据帧转换为矩阵。为此,我们使用了as.matrix()函数。通常,我们会在将数据框传递给绘图函数以绘制图表时这样做。

> barplot(as.matrix(df))

在下文中,我们创建了一个名为df的数据帧。我们用十个连续的数字填充数据帧。然后我们使用as.matrix()df转换成一个矩阵,并将结果保存到一个名为m的新变量中。

> df <- data.frame(1:10)
> dfX1.10
1      1
2      2
3      3
4      4
5      5
6      6
7      7
8      8
9      9
10     10
> class(df)
[1] "data.frame"
> m <- as.matrix(df)
> class(m)
[1] "matrix" "array"

请记住,因为它们都是相同的数据类型,矩阵需要的开销更少,本质上比数据帧更有效。如果我们比较我们的矩阵m和我们的数据帧df的大小,我们看到只有十个项目,存在大小差异。

> object.size(m)
552 bytes
> object.size(df)
776 bytes

也就是说,如果我们扩大规模,效率的提高并不等同。比较以下内容:

> big_df <- data.frame(1:40000000)
> big_m <- matrix(1:40000000)
> object.size(big_m)
160000216 bytes
> object.size(big_df)
160000736 bytes

我们可以看到,小数据集的第一个示例显示矩阵比数据框小 30 %,但在第二个示例的较大比例中,矩阵仅比数据框小 0 . 00018%。

添加列表

当组合或添加数据帧或矩阵时,通常使用rbind()cbind()按行或按列进行添加。

为了演示这一点,让我们向数据框df添加一个新行。我们将把df和新行一起传递给rbind()并添加到df。新行只包含一个元素,数字 11。

> df <- rbind(df, 11)
> dfX1.10
1      1
2      2
3      3
4      4
5      5
6      6
7      7
8      8
9      9
10     10
11     11

现在让我们向矩阵m中添加一个新列。为此,我们简单地将m作为第一个参数传递给cbind();第二个参数是将被追加到新列的新矩阵。

> m <- rbind(m, 11)
> m <- cbind(m, matrix(c(50:60), byrow=FALSE))
> mX1.10
[1,]     1  50
[2,]     2  51
[3,]     3  52
[4,]     4  53
[5,]     5  54
[6,]     6  55
[7,]     7  56
[8,]     8  57
[9,]     9  58
[10,]    10 59
[11,]    11 60

你可能会问,向量呢?好吧,让我们看看添加到我们的content向量。我们只需使用 combine 函数将当前向量与一个新向量合并:

> content <- c(1,2,3,4,5,6,7,8,9,10)
> content <- c(content, c(11:20))
> content
[1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20

遍历列表

作为通常使用过程化语言工作的开发人员,或者至少是使用过程化语言的开发人员(尽管近年来函数式编程范式变得更加主流),当我们想要处理数组中的数据时,我们很可能习惯于遍历数组。这与纯函数式语言相反,在纯函数式语言中,我们会将函数应用于列表,比如map()函数。r 支持这两种范式。让我们首先看看如何循环遍历我们的列表。

R 支持的最有用的循环是for in循环。这里可以看到for in回路的基本结构:

> for(i in 1:5){print(i)}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5

变量i的值在迭代的每一步中递增。我们可以使用for in循环遍历列表。我们可以指定一个特定的列进行迭代,如下所示,我们循环遍历数据帧df的 X1.10 列。

> for(n in df$X1.10){ print(n)}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5
[1] 6
[1] 7
[1] 8
[1] 9
[1] 10
[1] 11

注意,我们是通过美元符号操作符来访问数据帧的列的。大致格局是[data frame]$[column name]

将函数应用于列表

但是 R 真正想被使用的方式是对列表的内容应用函数(见图 2-8 )。

img/313452_2_En_2_Fig8_HTML.jpg

图 2-8

对列表元素应用函数

我们在 R 中用apply()函数来做这件事。

apply()函数有几个参数:

  • 首先是我们的列表。

  • 接下来是一个数字向量,表示我们如何在列表中应用这个函数(1表示行,2表示列,c[1,2]表示行和列)。

  • 最后是应用于列表的函数:

apply([list], [how to apply function], [function to apply])

让我们看一个例子。让我们做一个新的矩阵,我们称之为m。矩阵m将有十列和四行:

> m <- matrix(c(1:40), byrow=FALSE, ncol=10)
> m[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    1    5    9   13   17   21   25   29   33    37
[2,]    2    6   10   14   18   22   26   30   34    38
[3,]    3    7   11   15   19   23   27   31   35    39
[4,]    4    8   12   16   20   24   28   32   36    40

现在,假设我们想要递增m矩阵中的每个数字。我们可以简单地如下使用apply():

> apply(m, 2, function(x) x <- x + 1)[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    2    6   10   14   18   22   26   30   34    38
[2,]    3    7   11   15   19   23   27   31   35    39
[3,]    4    8   12   16   20   24   28   32   36    40
[4,]    5    9   13   17   21   25   29   33   37    41

你看到我们在那里做了什么吗?我们传入了m,我们指定我们想要跨列应用函数,最后我们传入了一个匿名函数。该函数接受一个我们称为x的参数。参数x是对当前矩阵元素的引用。从这里开始,我们只需将x的值增加 1。

好吧,假设我们想做一些稍微有趣的事情,比如将矩阵中的所有偶数归零。我们可以做到以下几点:

> apply(m,c(1,2),function(x){if((x %% 2) == 0) x <- 0 else x <- x})[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    1    5    9   13   17   21   25   29   33    37
[2,]    0    0    0    0    0    0    0    0    0     0
[3,]    3    7   11   15   19   23   27   31   35    39
[4,]    0    0    0    0    0    0    0    0    0     0

为了清楚起见,让我们分解一下我们正在应用的函数。我们只是通过检查当前元素除以 2 时是否有余数来检查它是否是偶数。如果是,我们将其设置为零;如果不是,我们把它设定为它自己:

function(x){if((x %% 2) == 0)x <- 0elsex <- x
}

功能

说到函数,在 R 中创建函数的语法与大多数其他语言非常相似。我们使用function关键字,给函数起一个名字,用左括号和右括号指定参数,并用花括号将函数体括起来:

function function name
{[body of function]
}

R 允许的有趣的东西是...参数(有时称为点参数)。这允许我们向函数传递可变数量的参数。在该函数中,我们可以将...参数转换成一个列表,并遍历该列表以检索其中的值:

> offset <- function (...){for(i in list(...)){print(i)}
}
> offset(23,11)
[1] 23
[1] 11

我们甚至可以在...参数中存储不同数据类型(模式)的值:

> offset("test value", 12, 100, "19ANM")
[1] "test value"
[1] 12
[1] 100
[1] "19ANM"

r 使用词法范围。这意味着当我们调用一个函数并试图引用没有在该函数的局部范围内定义的变量时,R 解释器会在创建该函数的工作区或范围内寻找这些变量。如果 R 解释器在那个作用域中找不到那些变量,它就在那个作用域的父作用域中查找。

如果我们在函数 B 中创建一个函数 A,那么函数 A 的创建范围就是函数 B,例如,参见下面的代码片段:

> x <- 10
> wrapper <- function(y){x <- 99c<- function(y){print(x + y)}return(c)
}
> t <- wrapper()
> t(1)
[1] 100
> x
[1] 10

我们在全局空间中创建了一个变量x,并赋予它一个值10。我们创建了一个函数,命名为wrapper,并让它接受一个名为y的参数。在wrapper()函数中,我们创建了另一个名为x的变量,并赋予它一个值99。我们还创建了一个名为c的函数。函数wrapper()将参数y传递给函数c(),函数c()输出x加到y的值。最后,wrapper()函数返回c()函数。

我们创建了一个变量t,并将其设置为wrapper()函数的返回值,也就是函数c()。当我们运行t()函数并传入一个值1时,我们看到它输出100,因为它从函数wrapper()中引用变量x

能够进入已经执行的函数的范围被称为闭包

但是,你可能会问,我们如何确定我们正在执行返回的函数,而不是每次都重新运行wrapper()?r 有一个非常好的特性,如果你输入不带括号的函数名,解释器会输出函数体。

当我们这样做时,我们实际上是在引用返回的函数,并使用闭包来引用x变量:

> t
function(y){print(x + y)}
<environment: 0x17f1d4c4>

摘要

在这一章中,我们下载并安装了 R。我们探索了命令行,检查了数据类型,并开始运行导入到 R 环境中的数据以供分析。我们看了列表,如何创建列表、添加列表、循环列表,以及对列表中的元素应用函数。

我们看了函数,讨论了词法范围,并了解了如何在 r 中创建闭包。

下一章,我们将更深入地研究 R,看看对象,用 R 中的统计分析来熟悉一下,并探索创建 R Markdown 文档以便在 Web 上分发。

三、对 R 的深入探究

最后一章探索了 R 中的一些介绍性概念,从使用控制台到导入数据。我们安装了包并讨论了数据类型,包括不同的列表类型。我们最后讨论了函数并创建了闭包。

本章将着眼于 R 中面向对象的概念,探索统计分析中的概念,最后看看 R 如何被合并到 R Markdown 中用于实时分发。

R 语言中的面向对象编程

r 支持两种不同的创建对象的系统:S3 和 S4 方法。S3 是 r 中处理对象的默认方式。我们一直在使用和制作 S3 对象。S4 是一种在 R 中创建对象的新方法,它有更多的内置验证,但是开销更大。让我们来看看这两种方法。

好了,传统的、基于类的、面向对象的设计的特点是创建类,这些类是实例化对象的蓝图(见图 3-1 )。

img/313452_2_En_3_Fig1_HTML.jpg

图 3-1

matrix 类用于创建变量m1m2,两者都是矩阵

在非常高的层次上,在传统的面向对象语言中,类可以扩展其他类来继承父类的行为,类也可以实现接口,接口是定义对象的公共签名应该是什么的契约。参见图 3-2 中的一个例子,其中我们创建了一个IUser接口来描述任何用户类型类的公共接口,以及一个BaseUser类来实现接口并提供基本功能。在某些语言中,我们可能会将BaseUser变成一个抽象类,一个可以扩展但不能直接实例化的类。UserSuperUser类扩展了BaseClass,并为自己的目的定制了现有的功能。

img/313452_2_En_3_Fig2_HTML.jpg

图 3-2

一个由子类UserSuperUser扩展的超类BaseUser实现的IUser接口

还存在多态性的概念,我们可以通过继承链改变功能。具体来说,我们将从基类继承一个函数,但覆盖它,保持签名(函数名、它接受的参数类型和数量以及它返回的数据类型)不变,但改变函数的功能。将重写函数与重载函数的概念进行比较,重载函数的名字相同,但签名和功能不同。

S3 班级

S3,之所以这么叫是因为它首先在 S 语言的版本 3 中实现,它使用了一个叫做通用函数的概念。R 中的所有东西都是一个对象,每个对象都有一个名为class的字符串属性,表示对象是什么。它周围没有验证,我们可以临时覆盖class属性。这就是 S3 的主要问题——缺乏认可。如果您曾经在尝试使用一个函数时返回了一个深奥的错误消息,那么您很可能亲身经历了这种缺乏验证的后果。错误消息可能不是由 R 检测到传入了不正确的类型而生成的,而是由试图执行传入内容的函数在执行过程中的某个步骤失败而生成的。

请参见下面的代码,其中我们创建了一个矩阵,并将其类更改为 vector:

> m <- matrix(c(1:10), nrow=2)
> m[,1] [,2] [,3] [,4] [,5]
[1,]    1    3    5    7    9
[2,]    2    4    6    8   10
> class(m) <- "vector"
> m[,1] [,2] [,3] [,4] [,5]
[1,]    1    3    5    7    9
[2,]    2    4    6    8   10
attr(,"class")
[1] "vector"

通用函数是检查传递给它们的对象的class属性的对象,并根据该属性表现出不同的行为。这是实现多态性的好方法。通过将通用函数传递给methods()函数,我们可以看到通用函数使用的方法。下面的代码展示了plot()通用函数的方法:

> methods(plot)
[1] plot.acf*            plot.data.frame*    plot.decomposed.ts*        plot.default        plot.dendrogram*
[6] plot.density         plot.ecdf           plot.factor*        plot.formula*       plot.function
[11] plot.hclust*        plot.histogram*     plot.HoltWinters*          plot.isoreg*        plot.lm
[16] plot.medpolish*     plot.mlm            plot.ppr*                  plot.prcomp*        plot.princomp*
[21] plot.profile.nls*   plot.spec           plot.stepfun               plot.stl*           plot.table*
[26] plot.ts             plot.tskernel*      plot.TukeyHSD
Non-visible functions are asterisked

请注意,在通用的plot()函数中有无数的方法来处理可以传递给它的所有不同类型的数据,例如当我们将数据帧传递给plot();或者如果我们想要绘制一个TukeyHSD对象plot()plot.TukeyHSD已经为我们准备好了。

Note

类型?TukeyHSD 了解关于该对象的更多信息。

现在你已经知道了 S3 面向对象的概念在 R 中是如何工作的,让我们看看如何创建我们自己的定制 S3 对象和通用函数。

S3 类是一个属性和函数的列表,其属性名为class。属性告诉通用函数如何处理实现特定类的对象。让我们使用图 3-2 中的UserClass想法来创建一个例子:

> tom <- list(userid = "tbarker", password = "password123", playlist=c(12,332,45))
> class(tom) <- "user"

我们可以通过使用attributes()函数来检查我们的新对象,它告诉我们该对象的属性以及它的类:

> attributes(tom)
$names
[1] "userid"   "password" "playlist"
$class
[1] "user"

现在要创建我们可以在新类中使用的通用函数,首先创建一个只处理我们的用户对象的函数;然后将其一般化,这样任何类都可以使用它。这将是createPlaylist()函数,它将接受要对其执行操作的用户和要设置的播放列表。这个的语法是[ function name ].[ class name ]。注意,我们使用美元符号来访问 S3 对象的属性。

>createPlaylist.user <- function(user, playlist=NULL){user$playlist <- playlistreturn(user)
}

请注意,当您直接在控制台中键入时,R 使您能够跨越几行而不执行输入,直到您完成一个表达式。你的表达完成后,就会被解读。如果想一次执行几个表达式,可以复制粘贴到命令行中。

让我们测试一下,确保它能按预期工作。它应该将传入对象的playlist属性设置为传入的向量:

> tom <- createPlaylist.user(tom, c(11,12))
> tom
$userid
[1] "tbarker"
$password
[1] "password123"
$playlist
[1] 11 12
attr(,"class")
[1] "user"

太棒了。现在让我们将createPlaylist()函数推广为一个通用函数。为此,我们只需创建一个名为createPlaylist的函数,并让它接受一个对象和一个值。在我们的函数中,我们使用UseMethod()函数将功能委托给我们特定于类的createPlaylist()函数:createPlaylist.user

UseMethod()函数是通用函数的核心:它评估对象,确定其类,并分派给正确的特定于类的函数:

> createPlaylist <- function(object, value)
{UseMethod("createPlaylist", object)
}

现在让我们试试看它是否有效:

> tom <- createPlaylist(tom, c(21,31))
> tom
$userid
[1] "tbarker"
$password
[1] "password123"
$playlist
[1] 21 31
attr(,"class")
[1] "user"

太棒了。

S4 班级

让我们看看 S4 物体。记住,对 S3 的主要抱怨是缺乏验证。S4 通过在类结构中内置开销来解决这一不足。让我们来看看。

首先,我们将创建user类。我们用setClass()函数来做这件事。

  • setClass()函数中的第一个参数是一个字符串,表示我们正在创建的类的名称。

  • 下一个参数称为表示,它是命名属性的列表。

setClass("user",
representation(userid="character",password="character",playlist="vector"
)
)

我们可以通过从这个类创建一个新对象来测试它。我们使用new()函数来创建该类的一个新实例:

> lynn <- new("user", userid="lynn", password="test", playlist=c(1,2))
> lynn
An object of class "user"
Slot "userid":
[1] "lynn"
Slot "password":
[1] "test"
Slot "playlist":
[1] 1 2

非常好。注意,对于 S4 对象,我们使用@符号来引用对象的属性:

> lynn@playlist
[1] 1 2
> class(lynn)
[1] "user"
attr(,"package")
[1] ".GlobalEnv

让我们通过使用setMethod()函数为这个类创建一个通用函数。我们只需传入函数名、类名,然后传入一个匿名函数作为泛型函数:

> setMethod("createPlaylist", "user", function(object, value){object@playlist <- valuereturn(object)})
Creating a generic function from function 'createPlaylist' in the global environment
[1] "createPlaylist"
>

让我们试一试:

> lynn <- createPlaylist(lynn, c(1001, 400))
> lynn
An object of class "user"
Slot "userid":
[1] "lynn"
Slot "password":
[1] "test"
Slot "playlist":
[1] 1001  400

太棒了。

虽然有些人喜欢 S3 方式的简单和灵活,但有些人喜欢 S4 方式的结构;选择 S3 或 S4 的对象纯粹是个人喜好。我自己更喜欢 S3 的简洁,这也是我们在本书剩余部分要用到的。谷歌在其 R 风格指南中反映了我自己对 S3 的感受,称“除非有充分的理由使用 S4 的对象或方法,否则就使用 S3 的对象和方法。”

R 中带有描述性度量的统计分析

现在让我们来看看统计分析中的一些概念,以及如何在 r 中实现它们。你可能记得大学统计学导论课上讲过的本章中的大部分概念;它们是开始思考和讨论您的数据所需的基本概念。

首先,让我们获得一些数据,我们将对这些数据进行统计分析。r 预装了许多数据集,我们可以将它们用作样本数据。要查看您安装的可用数据集列表,只需在控制台键入data()。您将看到如图 3-3 所示的屏幕。

img/313452_2_En_3_Fig3_HTML.jpg

图 3-3

R 中的可用数据集

要查看数据集的内容,您可以在控制台中通过名称调用它。让我们看看USArrests数据集,我们将在接下来的几个主题中使用它。

> USArrestsMurder  Assault   UrbanPop Rape
Alabama          13.2    236       58      21.2
Alaska           10.0    263       48      44.5
Arizona           8.1    294       80      31.0
Arkansas          8.8    190       50      19.5
California        9.0    276       91      40.6
Colorado          7.9    204       78      38.7
Connecticut       3.3     110       77     11.1
Delaware          5.9     238       72     15.8
Florida          15.4     335       80     31.9
Georgia          17.4     211       60     25.8
Hawaii            5.3      46       83     20.2
Idaho             2.6     120       54     14.2
Illinois         10.4     249       83     24.0
Indiana           7.2     113       65     21.0
Iowa              2.2      56       57     11.3
Kansas            6.0     115       66     18.0
Kentucky          9.7     109       52     16.3
Louisiana        15.4     249       66     22.2
Maine             2.1      83       51      7.8
Maryland         11.3     300       67     27.8
Massachusetts     4.4     149       85     16.3
Michigan         12.1     255       74     35.1
Minnesota         2.7      72       66     14.9
Mississippi      16.1     259       44     17.1
Missouri          9.0     178       70     28.2
Montana           6.0     109       53     16.4
Nebraska          4.3     102       62     16.5
Nevada           12.2     252       81     46.0
New Hampshire     2.1      57       56      9.5
New Jersey        7.4     159       89     18.8
New Mexico       11.4     285       70     32.1
New York         11.1     254       86     26.1
North Carolina   13.0     337       45     16.1
North Dakota      0.8      45       44      7.3
Ohio              7.3     120       75     21.4
Oklahoma          6.6     151       68     20.0
Oregon            4.9     159       67     29.3
Pennsylvania      6.3     106       72     14.9
Rhode Island      3.4     174       87      8.3
South Carolina   14.4     279       48     22.5
South Dakota      3.8      86       45     12.8
Tennessee        13.2     188       59     26.9
Texas            12.7     201       80     25.5
Utah              3.2     120       80     22.9
Vermont           2.2      48       32     11.2
Virginia          8.5     156       63     20.7
Washington        4.0     145       73     26.2
West Virginia     5.7      81       39      9.3
Wisconsin         2.6      53       66     10.8
Wyoming           6.8     161       60     15.6
>

我们要看的 R 中的第一个函数是summary()函数,它接受一个对象并返回以下关键描述性指标,按列分组:

  • 最小值

  • 最大值

  • 数字的中值和字符串的频率

  • 平均

  • 第一四分位数

  • 第三个四分位数

让我们通过summary()函数运行USArrests数据集:

> summary(USArrests)Murder          Assault         UrbanPop          Rape
Min.   : 0.800   Min.   : 45.0   Min.   :32.00   Min.   : 7.30
1st Qu.: 4.075   1st Qu.:109.0   1st Qu.:54.50   1st Qu.:15.07
Median : 7.250   Median :159.0   Median :66.00   Median :20.10
Mean   : 7.788   Mean   :170.8   Mean   :65.54   Mean   :21.23
3rd Qu.:11.250   3rd Qu.:249.0   3rd Qu.:77.75   3rd Qu.:26.18
Max.   :17.400   Max.   :337.0   Max.   :91.00   Max.   :46.00

让我们详细看看这些指标,以及标准偏差。

中位数和平均值

请注意,中位数是数据集中的中间值,确切地说,是数据集中大于和小于自身的数字数量相同的数字。如果我们的数据集如下所示,则中位数为 3:

1, 2, 3, 4, 5

但是请注意,当数据集中有奇数个项目时,很容易找到中间值。假设数据集中有偶数个项目,如下所示:

1, 2, 3, 4, 5, 6

在这种情况下,我们取中间的一对,3 和 4,并得到这两个数字的平均值。中位数是 3.5。

为什么中位数很重要?当你看一个数据集时,通常在光谱的两端都有异常值,这些值比数据集的其余部分高得多或低得多。收集中值排除了这些异常值,给出了更真实的平均值视图。

将这种想法与平均值进行对比,平均值就是数据集中值的总和除以项目的数量。这些值包含异常值,因此平均值可能会因为有显著的异常值而有所偏差,从而真正代表整个数据集。

例如,看看下面的数据集:

1, 2, 3, 4, 30

该数据集的中值仍为 3,但平均值为 8,原因如下:

median = [1,2] 3 [4,30]
mean =  1 + 2 + 3 + 4 + 30 = 4040 / 5 = 8

四重奏乐团

中位数是数据集的中心,这意味着中位数是第二个四分位数。四分位数是将数据集分成四个均匀部分的点。我们可以使用quantile()函数从数据集中提取四分位数。

> quantile(USArrests$Murder)0%    25%    50%    75%   100%
0.800  4.075  7.250 11.250 17.400

summary()函数只是返回四分位数,以及最小值、最大值和平均值。以下是用于比较的summary()结果,突出显示了之前的四分位数:

> summary(USArrests)Murder          Assault         UrbanPop          Rape
Min.   : 0.800   Min.   : 45.0   Min.   :32.00   Min.   : 7.30
1st Qu.: 4.075   1st Qu.:109.0   1st Qu.:54.50   1st Qu.:15.07
Median : 7.250   Median :159.0   Median :66.00   Median :20.10
Mean   : 7.788   Mean   :170.8   Mean   :65.54   Mean   :21.23
3rd Qu.:11.250   3rd Qu.:249.0   3rd Qu.:77.75   3rd Qu.:26.18
Max.   :17.400   Max.   :337.0   Max.   :91.00   Max.   :46.00

标准偏差

说到平均值的概念,还有一个概念是数据具有正态分布,或者数据通常密集地聚集在平均值周围,在平均值上下有较轻的分组。这通常用钟形曲线来说明,其中平均值位于曲线的顶部,异常值分布在曲线的两端(见图 3-4 )。

img/313452_2_En_3_Fig4_HTML.jpg

图 3-4

正态分布的钟形曲线

标准差是一个度量单位,它描述了数据分布与平均值的平均距离,因此我们可以用标准差来详细说明每个数据点与平均值的距离。

在 R 中,我们可以使用sd()函数来确定标准差。sd()函数需要一个数值向量:

> sd(USArrests$Murder)
[1] 4.35551

如果我们想收集一个矩阵的标准偏差,我们可以使用 s apply()函数来应用sd()函数,如下所示:

> sapply(USArrests, sd)Murder    Assault   UrbanPop       Rape murderRank
4.355510  83.337661  14.474763   9.366385  14.574930

RStudio IDE

如果您喜欢在集成开发环境(IDE)中开发,而不是在命令行中,您可以使用一个名为 RStudio IDE 的免费产品。RStudio IDE 由 RStudio 公司制造,它不仅仅是一个 IDE(您很快就会看到)。RStudio 公司由 ColdFusion 的创始人 JJ·阿莱尔创立。RStudio IDE 可在 www.rstudio.com/ide/ 下载(下载页面截图见图 3-5 )。

img/313452_2_En_3_Fig5_HTML.jpg

图 3-5

RStudio IDE 主页

Note

您应该现在安装 RStudio IDE,因为您将在本章的剩余部分使用它。

安装后,IDE 分成四个窗格(参见图 3-6 )。

img/313452_2_En_3_Fig6_HTML.jpg

图 3-6

RStudio 接口

左上方的窗格是 R 脚本文件,我们在其中编辑 R 源代码。左下方的窗格是 R 命令行。右上侧窗格包含命令历史以及当前工作区中的所有对象。右下方的窗格分为多个选项卡,可显示以下内容:

  • 当前工作目录的文件系统的内容

  • 已经生成的图或图表

  • 当前安装的软件包

  • r 帮助页面

虽然把你需要的所有东西都放在一个地方很棒,但是这里才是事情变得真正有趣的地方。

r 降价

在 RStudio 的 0.96 版本中,团队宣布使用 knitr 包支持 R Markdown。我们现在可以将 R 代码嵌入到 markdown 文档中,这些文档可以被 knitr 解释成 HTML(超文本标记语言)。但是还有更好的。

RStudio 公司还开发了一款名为 RPubs 的产品,它允许用户创建账户并托管他们的 R Markdown 文件,以便在网上发布。

Note

Markdown 是一种纯文本标记语言,由约翰·格鲁伯和艾伦·施瓦茨创建。在 markdown 中,您可以使用简单和轻量级的文本编码来表示格式。降价文档被读取和解释,并输出一个 HTML 文件。

下面是降价语法的简要概述:

header 1
=========
header 2
--------------
###header 3
####header 4
*italic*
**bold**
link text
![alt text](https://gitee.com/OpenDocCN/vkdoc-ds-zh/raw/master/docs/pro-data-vis-r-js/img/[path to image])

R Markdown 的伟大之处在于我们可以在 Markdown 文档中嵌入 R 代码。我们使用三个刻度线和花括号中的字母r来嵌入 R:

```{r}
[R code]
```r

我们需要三样东西来开始创建 R Markdown ( .rmd)文档:

  • 稀有

  • R Studio IDE 版本 0.96 或更高版本

  • 针织包装

knitr 包用于将 R 重新格式化成几种不同的输出格式,包括 HTML、markdown 甚至纯文本。

Note

有关编织机包装的信息,请访问 http://yihui.name/knitr/

因为您已经安装了 R 和 RStudio IDE,所以您将首先安装 knitr。R Studio IDE 有一个很好的安装包的界面:只需进入工具文件菜单,然后单击安装包。您应该会看到如图 3-7 所示的弹出窗口,您可以在其中指定包名(R Studio IDE 前面有一个很好的类型用于包发现)和要安装到哪个库。

img/313452_2_En_3_Fig7_HTML.jpg

图 3-7

安装编织机组件

安装 knitr 后,您需要关闭并重新启动 RStudio IDE。然后你进入文件菜单,选择文件➤新,在其中你会看到一些选项,包括 R Markdown。如果您选择 R Markdown,并选择默认选项“Document”和“HTML”作为默认输出格式,您将获得一个新文件,其模板如图 3-8 所示。

img/313452_2_En_3_Fig8_HTML.jpg

图 3-8

RStudio IDE

R Markdown 模板具有以下代码:

---
title: "Untitled"
output: html_document
---```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```r## R MarkdownThis is an R Markdown document. Markdown is a simple formatting syntax for authoring HTML, PDF, and MS Word documents. For more details on using R Markdown, see <http://rmarkdown.rstudio.com>.When you click the **Knit** button, a document will be generated that includes both content and the output of any embedded R code chunks within the document. You can embed an R code chunk like this:```{r cars}
summary(cars)
```r## Including PlotsYou can also embed plots, for example:```{r pressure, echo=FALSE}
plot(pressure)
```rNote that the `echo = FALSE` parameter was added to the code chunk to prevent printing of the R code that generated the plot.

这是模板,当你点击 Knit 按钮时,你会看到如图 3-9 所示的输出。

img/313452_2_En_3_Fig9_HTML.jpg

图 3-9

RMarkdown 模板的 HTML 输出

你注意到图 3-9 顶部的发布按钮了吗?这就是我们如何将 R Markdown 文件推送到 RPubs,以便在 Web 上托管和分发。

RPubs

RPubs 是 R Markdown 文件的免费网络发布平台,由 RStudio(该公司)提供。您可以通过访问 www.rpubs.com 创建免费账户。图 3-10 显示了 RPubs 主页的截图。

img/313452_2_En_3_Fig10_HTML.jpg

图 3-10

RPubs 主页

只需点击注册按钮,并填写表格,以创建您的免费帐户。RPubs 棒极了;这是一个我们可以发布 R Markdown 文档以供分发的平台。

Caution

请注意,你放在 RPubs 上的每个文件都是公开的,所以一定不要把任何敏感或专有的信息放在里面。如果您不想将 R Markdown 文件放在全世界都可以看到的地方,您可以点击“发布”按钮旁边的“另存为”按钮,将文件保存为普通的 HTML 格式。

单击发布按钮后,系统会提示您使用 RPubs 帐户登录。登录后,您将被引导至文档详情页面,如图 3-11 所示。

img/313452_2_En_3_Fig11_HTML.jpg

图 3-11

发布到 RPubs

填写完文档详细信息、文档标题和描述后,您将被定向到 RPubs 中的文档。图 3-9 托管在 RPubs 的模板见图 3-12 此处公开: www.rpubs.com/tomjbarker/3370

img/313452_2_En_3_Fig12_HTML.jpg

图 3-12

RMarkdown 模板发布到 RPubs

对于 R 文档和交流数据可视化来说,这是一种强大的分发方法。在接下来的章节中,我们将把所有完成的 R 图表放在 RPubs 上供公众阅读。

摘要

本章探讨了 R 中的一些更深层次的概念,从不同的面向对象设计模型到如何使用 R 进行统计分析。我们甚至研究了如何使用 RMarkdown 和 RPubs 使 R 中的数据可视化可用于公共分发。

在下一章中,我们将关注 D3,一个 JavaScript 库,它使我们能够在浏览器中分析和可视化数据,并为可视化添加交互性。

四、使用 D3 实现数据可视化

到目前为止,当我们谈论用于创建数据可视化的技术时,我们一直在谈论 R。我们已经花了最后两章来探索 R 环境和学习命令行。我们讨论了 R 语言的入门主题,从数据类型、函数到面向对象编程。我们甚至讨论了如何使用 RPubs 将我们的 R 文档发布到 Web 上。

本章我们将看一个叫做 D3 的 JavaScript 库,它被用来创建交互式数据可视化。首先是一个关于 HTML、CSS 和 JavaScript 的快速入门,D3 的支持语言。然后我们将深入研究 D3,并探索如何在 D3 中制作一些更常用的图表。

初步概念

D3 是一个 JavaScript 库。具体来说,这意味着它是用 JavaScript 编写的,并嵌入在 HTML 页面中。我们可以在自己的 JavaScript 代码中引用 D3 的对象和函数。所以让我们从头开始。下一节的目的不是深入研究 HTML CSS 和 JavaScript 还有很多其他的资源,包括我参与撰写的基金会网站创建。目的是对我们将直接用 D3 处理的概念有一个非常高层次的回顾。如果你已经熟悉 HTML、CSS 和 JavaScript,你可以跳到本章的“D3 历史”部分。

超文本标记语言

HTML 是一种标记语言;事实上,它代表超文本标记语言。它是一种表示语言,由表示格式和布局的元素组成。元素包含属性,这些属性具有指定元素、标记和内容的详细信息的值。为了解释,让我们看看我们的基本 HTML 框架结构,我们将在本章的大多数例子中使用它:

<!DOCTYPE html>
<html>
<head></head>
<body></body>
</html>

让我们从第一行开始。这是告诉浏览器的渲染引擎使用什么规则集的 doctype。浏览器可以支持多个版本的 HTML,每个版本都有稍微不同的规则集。这里指定的文档类型是 HTML5 文档类型。doctype 的另一个例子是:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN""  http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd  ">

这是 XHTML 1.1 的 doctype。注意,它指定了文档类型定义的 URL(.dtd)。如果我们阅读 URL 上的纯文本,我们会看到它是一个如何解析 HTML 标签的规范。W3C 在这里维护了一个文档类型列表: www.w3.org/QA/2002/04/valid-dtd-list.html

Modern Browser Architecture

现代浏览器由封装了特定功能的模块组成。这些模块也可以获得许可并嵌入到其他应用中:

  • 它们有一个 UI 层来处理浏览器用户界面的绘制,比如窗口、状态栏和后退按钮。

  • 他们有渲染引擎来解析、标记和绘制 HTML。

  • 它们有一个网络层来处理检索 HTML 和页面上所有资源所涉及的网络操作。

  • 他们有一个 JavaScript 引擎来解释和执行页面中的 JavaScript。

请参见图 4-1 了解该架构的图示。

img/313452_2_En_4_Fig1_HTML.jpg

图 4-1

现代浏览器架构

回到 HTML 的框架结构。下一行是<html>标签;这是文档的根级标记,包含我们将使用的所有其他 HTML 元素。请注意,文档的最后一行有一个结束标记。

接下来是<head>标签,它是一个容器,通常保存页面上没有显示的信息(例如,标题和元信息)。在<head>标签之后是<body>标签,它是一个容器,保存所有将在页面上显示的 HTML 元素,例如段落:

<p> this is a paragraph </p>

或链接:

<a href="[URL]">link text or image here</a>

或图像:

<img src="[URL]"/>

当谈到 D3 时,我们将要编写的大部分 JavaScript 将在主体部分,而大部分 CSS 将在头部分。

半铸钢ˌ钢性铸铁(Cast Semi-Steel)

CSS 代表级联样式表,用于设计网页上 HTML 元素的样式。样式表要么包含在<style>标签中,要么通过<link>标签外部链接,由样式规则和选择器组成。选择器将 web 页面上的元素作为样式的目标,样式规则定义应用什么样式。让我们看一个例子:

<style>
p{color: #AAAAAA;
}
</style>

在前面的代码片段中,样式表在一个style标签中。p选择器,它告诉浏览器将网页上的每个段落标记作为目标。样式规则用花括号括起来,由属性和值组成。本例将所有段落中文本的颜色设置为#AAAAAA,这是浅灰色的十六进制值。

选择器是 CSS 的真正微妙之处。这与我们相关,因为 D3 也使用 CSS 选择器来定位元素。类似于 S3/S4 类如何在 R 中相互继承,我们可以通过类或 id 使用选择器和目标元素变得非常具体,或者我们可以使用伪类来针对抽象概念,比如当元素悬停在上面时。我们可以在 DOM 中上下定位元素的祖先和后代。

Note

DOM 代表文档对象模型,是允许 JavaScript 与网页上的 HTML 元素交互的应用程序编程接口(API)。

.classname{
/* style sheet for a class*/
}
#id{
/*style sheet for an id*/
}
element:pseudo-class{
}

挽救(saving 的简写)

D3 的下一个介绍性概念是 SVG,它代表可伸缩矢量图形。SVG 是一种在浏览器中创建矢量图形的标准化方法,D3 用它来创建数据可视化。我们在 SVG 中关心的核心功能是绘制形状和文本并将它们集成到 DOM 中的能力,以便我们的形状可以通过 JavaScript 编写脚本。

Note

矢量图形是使用点和线创建的图形,这些点和线由渲染引擎进行数学计算和显示。将这种想法与位图或光栅图形进行对比,在位图或光栅图形中,像素显示是预先渲染的。向量,因为它们是简单的方程,往往规模更好,也更小。但是,它们缺乏位图或光栅图形的深度。

SVG 本质上是它自己的标记语言,有自己的 doctype。我们可以在外部.svg文件中编写 SVG,或者将 SVG 标签直接包含在我们的 HTML 中。在 HTML 页面中编写 SVG 标签允许我们通过 JavaScript 与形状进行交互。

SVG 支持预定义的形状以及画线的能力。SVG 中预定义的形状如下:

  • <rect>画矩形

  • <circle>画圆

  • <ellipse>画椭圆

  • <line>画线条;还有<polyline><polygon>用多个点画线

让我们看一些代码示例。如果我们将 SVG 写入 HTML 文档,我们使用<svg>标签包装我们的形状。<svg>带有xmlnsversion属性。xmlns属性应该是 SVG 名称空间的路径,而version显然是 SVG 的版本:

<svg xmlns:="  http://www.w3.org/2000/svg  " version="1.1">
</svg>

如果我们正在编写独立的.svg文件,我们将完整的doctypexml标签包含到页面文件中:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "  http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd  ">
<svg xmlns:=" http://www.w3.org/2000/svg " version="1.1">
</svg>

无论哪种方式,我们都在<svg>标签中创建我们的形状。让我们在<svg>标签中创建一些示例形状:

<svg xmlns:="  http://www.w3.org/2000/svg  " version="1.1" viewBox="0 0 500 500"><rect x="10" y="10" width="10" height="100" stroke="#000000" fill="#AAAAAA" /><circle cx="70" cy="50" r="40" stroke="#000000" fill="#AAAAAA" /><ellipse cx="230" cy="60" rx="100" ry="50" stroke="#000000" fill="#AAAAAA" />
</svg>

该代码产生如图 4-2 所示的形状。

img/313452_2_En_4_Fig2_HTML.jpg

图 4-2

在 SVG 中绘制的矩形、圆形和椭圆形

请注意,我们为所有形状分配了 x 和 y 坐标,在圆形和椭圆形的情况下,分配了 cx 和 cy 坐标,以及填充颜色和描边颜色。这只是最小的味道;我们也可以创建渐变和过滤器,然后将它们应用到我们的形状。我们还可以使用<text>标签创建文本,用于我们的 SVG 绘图。

让我们来看看。我们将更新前面的 SVG 代码,为每个形状添加文本标签:

<svg xmlns:="  http://www.w3.org/2000/svg  " version="1.1" viewBox="0 0 500 500"><rect x="80" y="20" width="10" height="100" stroke="#000000" fill="#AAAAAA" /><text x="55" y="145" fill="#000000">rectangle</text><circle cx="170" cy="60" r="40" stroke="#000000" fill="#AAAAAA" /><text x="150" y="145" fill="#000000">circle</text><ellipse cx="330" cy="70" rx="100" ry="50" stroke="#000000" fill="#AAAAAA" /><text x="295" y="145" fill="#000000">ellipse</text>
</svg>

该代码创建如图 4-3 所示的图形。

img/313452_2_En_4_Fig3_HTML.jpg

图 4-3

带文本标签的 SVG 形状

现在,我们可以开始看到用这些基本构件创建数据可视化的可能性。因为 D3 是一个 JavaScript 库,而且我们用 D3 做的大部分工作都是在 JavaScript 中完成的,所以在我们深入研究 D3 之前,让我们先从高层次上了解一下 JavaScript。

Java Script 语言

JavaScript 是网络的脚本语言。通过将script标签内嵌在文档中或者链接到外部 JavaScript 文档,可以将 JavaScript 包含在 HTML 文档中:

<script>
//javascript goes here
</script>
<script src="pathto.js"></script>

JavaScript 可用于处理信息、对事件做出反应以及与 DOM 交互。在 JavaScript 中,我们使用关键字var创建变量。

var foo = "bar";

注意,如果我们不使用var关键字,我们创建的变量将被赋给全局范围。我们不想这样做,因为我们的全局变量可能会被网页上的其他代码覆盖。

JavaScript 看起来很像其他基于 C 的语言,因为每个表达式都以分号结尾,函数和条件体等代码块都用花括号括起来。

条件语句通常是格式如下的if-else语句:

if([condition]){[code to execute]
}else{[code to execute]
}

函数的格式如下:

function [function name] ([arguments]){[code to execute]
}

在 JavaScript 中,我们通常通过引用元素的id属性来访问 DOM 元素。我们像使用getElementById()函数一样做这件事:

var header = document.getElementById("header");

前面的代码存储了对 web 页面上 ID 为header的元素的引用。然后,我们可以更新该元素的属性,包括添加新元素或完全删除该元素。

JavaScript 中的对象通常是对象文字,这意味着我们在运行时创建它们,由属性和方法组成。我们像这样创建对象文字:

var myObj = {myProp: 20,myfunc: function(){}
}

我们使用点运算符引用对象的属性和方法:

myObj.myprop = 10;

看,这又快又无痛。好的,接下来是 D3!

D3 的历史

D3 代表数据驱动文档,是一个用于创建交互式数据可视化的 JavaScript 库。将成为 D3 的想法的种子始于 2009 年的 Protovis,由迈克·博斯托克、瓦迪姆·奥吉夫茨基和杰夫·赫尔在斯坦福可视化小组工作时创建。

Note

史丹福可视化小组的信息可以在它的网站上找到: http://vis.stanford.edu/ 。Protovis 的原始白皮书可在 http://vis.stanford.edu/papers/protovis 找到。

Protovis 是一个 JavaScript 库,它提供了创建不同类型可视化的接口。根名称空间是pv,它提供了一个 API 来创建条、点和区域等。像 D3 一样,Protovis 使用 SVG 来创建这些形状,但与 D3 不同,它将 SVG 调用包装在自己专有的术语中。

Protovis 在 2011 年被放弃了,所以它的创造者可以学习并专注于 D3。Protovis 和 D3 在哲学上是有区别的。Protovis 的目标是提供用于创建数据可视化的包装功能,而 D3 则通过使用现有的 web 标准和术语来简化数据可视化的创建。在 D3 中,我们在 SVG 中创建了矩形和圆形,这得益于 D3 的语法优势。

使用 D3

我们需要做的第一件事就是去 D3 网站 http://d3js.org/ ,下载最新版本(见图 4-4 )。

img/313452_2_En_4_Fig4_HTML.jpg

图 4-4

D3 主页

安装后,您可以设置一个项目。

设置项目

我们可以在页面上直接包含.js文件,就像这样:

<script src="d3.v3.js"></script>

根命名空间是d3;我们从 D3 发出的所有命令都将使用d3对象。

使用 D3

我们使用select()函数来定位特定的元素,或者使用selectAll()函数来定位所有特定的元素类型:

var body = d3.select("body");

前一行选择了body标签,并将其存储在名为body的变量中。然后,我们可以根据需要更改几何体的属性,或者向几何体添加新元素:

var allParagraphs = d3.select("body").selectAll("p");

前一行选择了body标签,然后选择了正文中的所有段落标签。

请注意,我们在第二行将两个动作链接在一起。我们选择了正文,然后选择了所有的段落,这两个动作是连在一起的。还要注意,我们使用 CSS 选择器来指定目标元素。

好了,一旦我们选择了一个元素,它现在就被认为是我们的选择,我们可以对这个选择执行操作。我们可以在选择中选择元素,就像我们在前面的例子中所做的那样。

我们可以用attr()函数更新选择的属性。attr()函数接受两个参数:第一个是属性的名称,第二个是属性的设置值。假设我们想改变当前文档的背景颜色。我们可以选择主体,并通过将它添加到我们的脚本块来设置bgcolor属性:

<script>d3.select("body").attr("bgcolor", "#000000");
</script>

请注意,在前面的代码片段中,我们将链式属性函数调用带到了下一行。我们这样做是为了可读性。

真正有趣的是,因为我们在谈论 JavaScript,而函数是 JavaScript 中的一级对象,我们可以将函数作为属性值传入,这样无论它评估为什么,都成为设置的值:

<script>d3.select("body").attr("bgcolor", function(){return "#000000";
});
</script>

我们也可以使用append()函数向我们的选择添加元素。append()函数接受一个标签名作为第一个参数。它将创建指定类型的新元素,并将该新元素作为当前选择返回:

<script>
var svg = d3.select("body").append("svg");
</script>

前面的代码在页面主体中创建了一个新的 SVG 标记,并将该选择存储在变量svg中。

接下来,让我们用我们刚刚学到的关于 D3 的知识重新创建图 4-3 中的形状:

<script>var svg = d3.select("body").append("svg").attr("width", 800);var r = svg.append("rect").attr("x", 80).attr("y", 20).attr("height", 100).attr("width", 10).attr("stroke", "#000000").attr("fill", "#AAAAAA");var c = svg.append("circle").attr("cx", 170).attr("cy", 60).attr("r", 40).attr("stroke", "#000000").attr("fill", "#AAAAAA");var e = svg.append("ellipse").attr("cx", 330).attr("cy", 70).attr("rx", 100).attr("ry", 50).attr("stroke", "#000000").attr("fill", "#AAAAAA");
</script>

对于每个形状,我们向 SVG 元素添加一个新元素并更新属性。

如果我们比较这两种方法,我们可以看到我们只是在 D3 中创建了 SVG 元素,就像我们在直接标记中所做的一样。然后,我们在 SVG 元素中创建一个 SVG 矩形、圆形和椭圆形,以及我们在 SVG 标记中指定的相同属性。但是我们的 D3 例子有一个非常重要的不同:我们现在有了对页面上每个可以交互的元素的引用。

让我们来看看 D3 的交互。

绑定数据

对于数据可视化,我们与 SVG 形状最重要的交互是将数据绑定到它们。这使我们能够在形状的属性中反映这些数据。

为了绑定数据,我们简单地调用选择的data()方法:

<script>
var rect = svg.append("rect").data([1,2,3]);
</script>

这相当简单。然后,我们可以通过匿名函数引用绑定的数据,并将其传递给我们的attr()函数调用。让我们看一个例子。

首先,让我们创建一个名为dataSet的数组。为了开始设想这将如何与创建数据可视化相关联,您可以将dataSet视为一个非连续值的列表,可能是一个类的测试分数或一组区域的总降雨量:

<script>
var dataSet = [ 84,62,40,109];
</script>

接下来,我们将在页面上创建一个 SVG 元素。为此,我们将选择正文并附加一个宽度为 800 像素的 SVG 元素。我们将在一个名为svg的变量中保存对这个 SVG 元素的引用:

<script>
var svg = d3.select("body").append("svg").attr("width", 800);
</script>

这就是能够绑定数据改变事情的地方。我们将一系列命令连接在一起,这些命令将根据数据数组中存在的元素数量在 SVG 元素中创建占位符矩形。

我们将首先使用selectAll()返回对 SVG 元素中所有矩形的引用。现在还没有,但是在链执行完的时候会有。接下来,我们绑定我们的dataSet变量并调用enter()enter()函数从绑定数据中创建占位符对象。最后,我们调用append()enter()创建的每个占位符处创建一个矩形。

<script>
bars = svg.selectAll("rect").data(dataSet).enter().append("rect");
</script>

如果我们在浏览器中查看到目前为止的工作,我们会看到一个空白页,但是如果我们在 web inspector(如 Firebug)中查看 HTML,我们会看到 SVG 元素以及创建的矩形,但是还没有指定样式或属性,类似于图 4-5 。

img/313452_2_En_4_Fig5_HTML.jpg

图 4-5

Firebug 检查界面

接下来,让我们来设计刚刚制作的矩形。我们在变量bars中有一个对所有矩形的引用,所以让我们把一堆attr()调用链接在一起来设计矩形的样式。现在,让我们使用绑定的数据来确定条形的高度。

<script>
bars.attr("width", 15 ).attr("height", function(x){return x;}).attr("x", function(x){return x + 40;}).attr("fill", "#AAAAAA").attr("stroke", "#000000");
</script>

完整的源代码如下所示,并做出了我们在图 4-6 中看到的形状:

img/313452_2_En_4_Fig6_HTML.jpg

图 4-6

条形图的矩形样式

<script>
var dataSet = [84,62,40,109];
var svg = d3.select("body").append("svg").attr("width", 800);
bars = svg.selectAll("rect").data(dataSet).enter().append("rect");
bars.attr("width", 15 ).attr("height", function(x){return x;}).attr("x", function(x){return x + 40;}).attr("fill", "#AAAAAA").attr("stroke", "#000000");
</script>

现在再看看 Firebug 或者你的浏览器的调试工具;可以看到生成的标记,如图 4-7 所示。

img/313452_2_En_4_Fig7_HTML.jpg

图 4-7

Firebug 中显示为 SVG 源代码的矩形

现在,您可以真正看到我们如何通过将数据绑定到 SVG 形状来开始用 D3 进行数据可视化。让我们把这个概念向前推进一步。

创建条形图

到目前为止,我们的示例看起来很像一个条形图的开始,因为我们有许多高度代表数据的条形。让我们给它一些结构。

首先,让我们给我们的 SVG 容器一个更具体的宽度和高度。这很重要,因为 SVG 容器的大小决定了我们用来标准化图表其余部分的比例。因为我们将在整个代码中引用这个大小,所以让我们确保将这些值抽象到它们自己的变量中。

我们将为我们的 SVG 容器定义一个高度和宽度。我们还将创建保存我们将在轴上使用的最小值和最大值的变量:分别是 0 和 109(最大的数据点)。我们还将定义一个偏移值,这样我们就可以绘制比图表略大的 SVG 容器,以给出它周围的图表边距。

<script>
var chartHeight = 460,chartWidth = 400,chartMin = 0,chartMax = 109,offset = 60
var svg = d3.select("body").append("svg").attr("width", chartWidth).attr("height", chartHeight + offset);
</script>

接下来我们需要确定我们的酒吧的方向。如图 4-6 所示,这些条是从上往下画的,所以尽管它们的高度是准确的,但它们看起来是朝下的,因为 SVG 是从左上方画形状和定位形状的。因此,为了使它们的方向正确,使条形看起来像是从图表的底部向上,让我们给条形添加一个 y 属性。

y 属性应该是引用数据的函数;该函数应该从图表高度中减去条形高度值。该函数返回的值是 y 坐标中使用的值。

<script>
bars.attr("width", 15 ).attr("height", function(x){return x;}).attr("y", function(x){return (chartHeight - x);}).attr("x", function(x){return x;}).attr("fill", "#AAAAAA").attr("stroke", "#000000");
</script>

这会将条形翻转到 SVG 元素的底部。我们可以在图 4-8 中看到结果。

img/313452_2_En_4_Fig8_HTML.jpg

图 4-8

条形图中的矩形不再反转

现在让我们缩放条形以适应 SVG 元素的高度。为此,我们将使用 D3 scale()函数。scale()函数用于获取一个范围内的数字,并将其转换为另一个数字范围内该数字的等效值,本质上是将值换算为等效值。

在这种情况下,我们有一个数字范围,它表示我们的dataSet数组中的值的范围,它表示条形的高度,我们希望将这些数字转换成等价的值:

<script>
var yscale = d3.scaleLinear().domain([chartMin,chartMax]).range([0,(chartHeight)]);
</script>

确保将这段代码放在声明图表变量的部分之后,最好是在声明“svg”变量之前。然后,我们只需使用yscale()函数更新条形的高度和 y 属性:

<script>
bars.attr("width", 15 ).attr("height", function(x){ return yscale(x);}).attr("y", function(x){return (chartHeight - yscale(x));}).attr("x", function(x){return x;}).attr("fill", "#AAAAAA").attr("stroke", "#000000");
</script>

这产生了如图 4-9 所示的图形。

img/313452_2_En_4_Fig9_HTML.jpg

图 4-9

正确缩放的条形图矩形

非常好!但到目前为止,我们只是根据高度来放置条形,而不是根据它们在数组中的位置。让我们改变这一点,使它们的数组位置更有意义,这样条形就会以正确的顺序显示。

为此,我们只需更新条形的 x 值。我们已经看到,我们可以将一个匿名函数传递给attr()函数的 value 参数。匿名函数中的第一个参数是数组中当前元素的值。如果我们在匿名函数中指定第二个参数,它将保存当前的索引号。

然后,我们可以引用该值并偏移它来放置每个条形:

<script>
bars.attr("width", 15 ).attr("height", function(x){ return yscale(x);}).attr("y", function(x){return (chartHeight - yscale(x));}).attr("x", function(x, i){return (i * 20);}).attr("fill", "#AAAAAA").attr("stroke", "#000000");
</script>

这为我们提供了图 4-10 中所示的条的顺序。只需目测一下,我们就可以知道现在条形更接近于数组中数据的表示形式——不仅仅是高度,还包括数组中指定顺序的高度。

img/313452_2_En_4_Fig10_HTML.jpg

图 4-10

条形图中的矩形按照数据中的顺序排列

现在,让我们添加文本标签,以便我们可以更好地看到条形的高度所表示的值。

我们通过创建 SVG 文本元素来实现这一点,其方式与创建条的方式非常相似。我们为数据数组中的每个元素创建文本占位符,然后设置文本元素的样式。您会注意到,我们传递到 x 和 y 属性调用中的匿名函数对于文本元素和对于条形几乎是相同的,只是进行了偏移,以便文本位于每个条形的上方和中心:

<script>
svg.selectAll("text").data(dataSet).enter().append("text").attr("x", function(d, i) { return ((i * 20) + offset/4); }).attr("y", function(x, i){return (chartHeight - yscale(x) - 24) ;}).attr("dx", -15/2).attr("dy", "1.2em").attr("text-anchor", "middle").text(function(d) { return d;}).attr("fill", "black");
</script>

该代码生成如图 4-11 所示的图表。

img/313452_2_En_4_Fig11_HTML.jpg

图 4-11

带文本标签的条形图

参见以下完整的源代码:

<html>
<head>
<title></title>
<script src="d3.js"></script></head>
<body>
<script>
var dataSet = [84,62,40,109];
var chartHeight = 460,chartWidth = 400,chartMin = 0,chartMax = 115,offset = 60;
var yscale = d3.scaleLinear().domain([chartMin,chartMax]).range([0,(chartHeight)]);
var svg = d3.select("body").append("svg").attr("width", chartWidth).attr("height", chartHeight + offset);
bars = svg.selectAll("rect").data(dataSet).enter().append("rect");
bars.attr("width", 15 ).attr("height", function(x){ return yscale(x);}).attr("y", function(x){return (chartHeight - yscale(x));}).attr("x", function(x, i){return (i * 20);}).attr("fill", "#AAAAAA").attr("stroke", "#000000");
svg.selectAll("text").data(dataSet).enter().append("text").attr("x", function(d, i) { return ((i * 20) + offset/4); }).attr("y", function(x, i){return (chartHeight - yscale(x) - 24) ;}).attr("dx", -15/2).attr("dy", "1.2em").attr("text-anchor", "middle").text(function(d) { return d;}).attr("fill", "black");
</script>
</body>
</html>

最后,让我们从外部文件读入数据,而不是在页面中硬编码。

加载外部数据

首先,我们将从文件中取出数组,并将其放入自己的外部文件:sampleData.csvsampleData.csv的内容简单如下:

84,62,40,109

接下来,我们将使用d3.text()函数加载sampleData.csvd3.text()的工作方式是获取一个外部文件的路径,然后将它赋给一个变量(在本例中为数据)。该函数接收一个参数,该参数是外部文件的内容:

<script>
d3.text("sampleData.csv").then((data) => {});
</script>

问题是,在开始对数据进行任何制图之前,我们需要外部文件的内容。因此,在回调函数中,我们将解析文件,然后包装所有现有的功能,如下所示:

<html>
<head>
<title></title>
<script src="d3.js"></script>
</head>
<body>
<script>
d3.text("sampleData.csv").then((data) =>  {
var dataSet = data.split(",");
var chartHeight = 460,chartWidth = 400,chartMin = 0,chartMax = 115,offset = 60;
var yscale = d3.scaleLinear().domain([chartMin,chartMax]).range([0,(chartHeight)]);
var svg = d3.select("body").append("svg").attr("width", chartWidth).attr("height", chartHeight + offset);
bars = svg.selectAll("rect").data(dataSet).enter().append("rect");
bars.attr("width", 15 ).attr("height", function(x){ return yscale(x);}).attr("y", function(x){return (chartHeight - yscale(x));}).attr("x", function(x, i){return (i * 20);}).attr("fill", "#AAAAAA").attr("stroke", "#000000");
svg.selectAll("text").data(dataSet).enter().append("text").attr("x", function(d, i) { return ((i * 20) + offset/4); }).attr("y", function(x, i){return (chartHeight - yscale(x) - 24) ;}).attr("dx", -15/2).attr("dy", "1.2em").attr("text-anchor", "middle").text(function(d) { return d;}).attr("fill", "black");})
</script>
</body>
</html>

需要注意的是,如果您在本地计算机上运行这段代码,而不是在 web 服务器上运行,您将得到类似“跨源请求仅支持 HTTP”的错误。这是一种安全措施,您的浏览器使用它来防止恶意代码在您的本地计算机上运行。建议在编程时使用本地 web 服务器来解决这个问题。

回到我们的 d3.text()函数——CSV 文件不是我们唯一可以读取的格式。事实上,d3.text()只是语法糖——D3 实现XMLHttpRequest对象d3.xhr()的便利方法或特定类型包装器。

作为参考,XMLHttpRequest对象是 AJAX 事务中用来从客户端异步加载内容而无需刷新页面的对象。在纯 JavaScript 中,我们实例化 XHR 对象,传入资源的 URL,以及检索资源的方法(GET 或 POST)。我们还指定了一个回调函数,该函数将在 XHR 对象更新时被调用。在这个函数中,我们可以解析数据并开始使用它。参见图 4-12 以获得该过程的高级图。

img/313452_2_En_4_Fig12_HTML.jpg

图 4-12

XHR 交易顺序图

在 D3 里,d3.xhr()函数是 D3 对XMLHttpRequest对象的包装。它的工作方式与我们刚刚看到的d3.text()非常相似,我们传入一个资源的 URL 和一个要执行的回调函数。

D3 具有的其他特定类型的便利功能是d3.csv()d3.json()d3.xml()d3.html()

摘要

本章探讨了 D3。我们开始讲述 HTML、CSS、SVG 和 JavaScript 的介绍性概念,至少是与实现 D3 相关的内容。从那里,我们深入研究了 D3,查看了一些介绍性的概念,比如创建我们的第一个 SVG 形状,通过将这些形状制作成条形图来扩展这个想法。

D3 是制作数据可视化的极好的库。要查看完整的 API 文档,请参见 https://github.com/mbostock/d3/wiki/API-Reference

我们将回到 D3,但是首先,我们将探索一些我们可以创建的数据可视化,它们在 web 开发的世界中有实际的应用。我们要看的第一个是你可能在谷歌分析仪表板或类似的东西上看到过的东西:基于用户访问的数据地图。

五、可视化访问日志中的空间数据

在上一章中,我们讨论了 D3,并研究了从制作简单形状到用这些形状制作条形图的概念。在前两章中,我们深入研究了 r。现在,您已经熟悉了我们将使用的核心技术,让我们开始看一些例子,作为 web 开发人员,我们如何创建数据可视化来交流我们领域内的有用信息。

我们要看的第一个是从我们的访问日志中创建一个数据映射。

什么是数据图?

首先,让我们进行水平设置,并确保我们清楚地定义了一个数据映射。数据地图是空间领域的信息表示,是统计学和制图学的结合。数据地图是最容易理解和最广泛使用的数据可视化工具,因为它们的数据表达在我们都熟悉和使用的东西中:地图。

回想一下琼恩·雪诺在 1854 年绘制的霍乱地图第一章的讨论。这被认为是数据地图的最早例子之一,尽管有几个著名的同时代人,包括十九世纪法国工程师查尔斯·密纳德的几个。他因 1812 年拿破仑入侵俄罗斯的数据可视化而广为人知。

米纳德还创建了几个突出的数据地图。他的两个最著名的数据地图包括展示法国消费的牛的来源地区和百分比的数据地图(见图 5-1 )和展示葡萄酒从法国出口的路径和目的地的数据地图(见图 5-2 )。

img/313452_2_En_5_Fig2_HTML.jpg

图 5-2

米纳德展示葡萄酒出口路径和目的地的数据地图

img/313452_2_En_5_Fig1_HTML.jpg

图 5-1

来自 Charles Minard 的早期数据地图展示了法国的来源地区和牛的消费情况

今天,我们到处都能看到数据地图。它们可以是知识性和艺术性的表达,就像费尔南达·维埃加斯和马丁·瓦滕伯格的风地图项目(见图 5-3 )。在 http://hint.fm/wind 可以看到,风力项目展示了美国上空气流的路径和力量。

img/313452_2_En_5_Fig3_HTML.jpg

图 5-3

风地图,显示飓风桑迪登陆时各地区的风速(经费尔南达·维埃加斯和马丁·瓦滕伯格许可使用)

数据图可能很深奥,例如 energy.gov 大学提供的数据图展示了各州的能源消耗(见图 5-4 )甚至各州的可再生能源产量等概念。

img/313452_2_En_5_Fig4_HTML.jpg

图 5-4

描述 energy.gov 各州能源消耗的数据图(可在 http://energy.gov/maps/2009-energy-consumption-person 获得)

现在,您已经看到了数据地图的历史和当代示例。在本章中,您将了解如何从 web 服务器访问日志创建自己的数据映射。

访问日志

访问日志是 web 服务器保存的记录,用于跟踪请求了哪些资源。每当从服务器请求网页、图像或任何其他类型的文件时,服务器都会为该请求创建一个日志条目。每个请求都有与之相关联的某些数据点,通常是关于资源请求者的信息(例如,IP 地址和用户代理)以及诸如一天中的时间和请求了什么资源之类的一般信息。

我们来看看访问日志。一个示例条目如下所示:

msnbot-157-55-17-199.search.msn.com - - [18/Jan/2013:13:32:15 -0400] "GET /robots.txt HTTP/1.1" 404 208 "-" "Mozilla/5.0 (compatible; bingbot/2.0; +  http://www.bing.com/bingbot.htm)"

这是一个样本 Apache 访问日志的片段。Apache 访问日志遵循组合日志格式,这是万维网联盟(W3C)通用日志格式标准的扩展。通用日志格式的文档可在此处找到:

www.w3.org/Daemon/User/Config/Logging.html#common-logfile-format

通用日志格式定义了以下字段,用制表符分隔:

  • 远程主机的 IP 地址或 DNS 名称

  • 远程用户的日志名

  • 远程用户的用户名

  • 日期戳

  • 请求—通常包括请求方法和所请求资源的路径

  • 为请求返回的 HTTP 状态代码

  • 请求的资源的总文件大小

组合日志格式添加了参考和用户代理字段。组合日志格式的 Apache 文档可以在下面找到:

http://httpd.apache.org/docs/current/logs.html#combined

注意,不可用的字段由单个破折号-表示。

让我们仔细分析一下前面的日志条目:

  • 第一个字段是msnbot-157-55-17-199.search.msn.com。这是一个 DNS 名称,只是碰巧内置了 IP 地址。我们不能指望解析出这个域的 IP 地址,所以现在,就忽略这个 IP 地址。当我们开始以编程方式解析日志时,我们将使用本机 PHP 函数gethostbyname()来查找给定域名的 IP 地址。

  • 接下来的两个字段,日志名和用户,是空的。

  • 接下来是日戳:[18/Jan/2013:13:32:15 -0400]

  • 日期戳之后是请求:"GET /robots.txt HTTP/1.1"。如果你还没有从 DNS 名称中猜到,这是一个机器人,具体来说是微软的msnbot替代品:??。在这个记录中,bingbot正在请求robots.txt文件。

  • 接下来是请求的 HTTP 状态:404。显然,没有可用的robots.txt文件。

  • 接下来是请求的总负载。显然 404 需要 208 字节。

  • 接下来是一个破折号,表示引用地址为空。

  • 最后是 useragent: "Mozilla/5.0 (compatible; bingbot/2.0; + http://www.bing.com/bingbot.htm )",它明确地告诉我们,它确实是一个 bot。

现在您已经有了访问日志并理解了其中的内容,您可以解析它以编程方式使用其中的每个字段。

解析访问日志

解析访问日志的过程如下:

  1. 读取访问日志。

  2. 解析它并根据存储的 IP 地址收集地理数据。

  3. 为我们的可视化输出我们感兴趣的字段。

  4. 读入这个输出并可视化。

前三步我们用 PHP,最后一步用 R。请注意,您需要运行 PHP 5.4.10 或更高版本才能成功运行以下 PHP 代码。

读取访问日志

创建一个名为parseLogs.php的新 PHP 文档,首先创建一个函数来读取文件。调用这个函数parseLog()并让它接受文件的路径:

function parseLog($file){
}

在这个函数中,您将编写一些代码来打开传入的文件进行读取,并遍历文件的每一行,直到到达文件的末尾。迭代中的每一步都将读入的行存储在变量$line中:

$logArray = array();
$file_handle = fopen($file, "r");
while (!feof($file_handle)) {$line = fgets($file_handle);
}
fclose($file_handle);

目前为止 PHP 中相当标准的文件 I/O 功能。在这个循环中,您将对一个您将调用parseLogLine()的函数和另一个您将调用getLocationbyIP()的函数进行函数调用。在parseLogLine()中,你将拆分行并将值存储在一个数组中。在getLocationbyIP()中,你将使用 IP 地址获取地理信息。然后,您将把这个返回的数组存储在一个名为$ logArray的更大的数组中。

$lineArr = parseLogLine($line);
$lineArr = getLocationbyIP($lineArr);
$logArray[count($logArray)] = $lineArr;

不要忘记在函数的顶部创建$logArray变量。

完成的函数应该是这样的:

function parseLog($file){
$logArray = array();
$file_handle = fopen($file, "r");
while (!feof($file_handle)) {$line = fgets($file_handle);$lineArr = parseLogLine($line);$lineArr = getLocationbyIP($lineArr);$logArray[count($logArray)] = $lineArr;
}
fclose($file_handle);
return $logArray;
}

解析日志文件

接下来,您将充实parseLogLine()函数。首先,您将创建一个空函数:

function parseLogLine($logLine){
}

该函数需要一行访问日志。

请记住,访问日志的每一行都是由空格分隔的信息部分组成的。您的第一反应可能是在每个空格处拆分该行,但这会导致以意想不到的方式拆分用户代理字符串(可能还有其他字段)。

就我们的目的而言,解析该行的一个更干净的方法是使用正则表达式。正则表达式,简称 regex,是使您能够快速有效地进行字符串匹配的模式。

正则表达式使用特殊字符来定义这些模式:单个字符、字符文字或字符集。对正则表达式的深入探讨超出了本章的范围,但是阅读不同正则表达式模式的一个很好的参考是微软正则表达式快速参考,可从这里获得: http://msdn.microsoft.com/en-us/library/az24scfc.aspx

Grant Skinner 还提供了一个很棒的创建和调试正则表达式的工具(见图 5-5 ,这里有: https://regexr.com

img/313452_2_En_5_Fig5_HTML.jpg

图 5-5

格兰特·斯金纳的正则表达式工具

要使用 Grant 的工具,将顶部的模式从 JavaScript 更改为 PCRE(这是 PHP 解释正则表达式的方式)。然后将以下内容粘贴到大的“文本”框中:

114.119.143.124---[14/Jun/2021:14:21:03-0400]" GET/2007/12/your-daddy-comment-leads-to-parking-lot-attack-northwes-Florida-daily-news/HTTP/1.1 " 200 19591 "-" Mozilla/5.0(Linux;安卓 7.0;)AppleWebKit/537.36 (KHTML,喜欢壁虎)手机 Safari/537.36(兼容;PetalBot+ https://webmaster.petalsearch.com/site/petalbot )”

最后,在“表达式”框中输入以下正则表达式:^([\d.:]+)(\ s+)(\ s+)[([\ w /]+)😦[\ w:]+)\ s([+-]\ d { 4 })]"(。+?) (.+?) (.+?)"(\d{3}) (\d+|(?:.+?)) "([^"]|(?:.+?))" "([^"]|(?:.+?))"

单击表达式匹配将让您了解正则表达式的每个部分是如何在我们粘贴的日志条目中找到的。

转向我们的 PHP 代码,让我们定义正则表达式模式,并将其存储在一个名为$pattern的变量中。

如果你不精通正则表达式,你可以使用 Grant Skinner 的工具很容易地创建它们(参见图 5-5 )。使用此工具,您可以得出以下模式:

$pattern = '/^([\d.:]+) (\S+) (\S+) \[([\w\/]+):([\w:]+)\s([+\-]\d{4})\] "(.+?) (.+?) (.+?)" (\d{3}) (\d+|(?:.+?)) "([^"]*|(?:.+?))" "([^"]*|(?:.+?))"/';

在该工具中,您可以看到它是如何将字符串分成以下几组的(参见图 5-6 )。

img/313452_2_En_5_Fig6_HTML.jpg

图 5-6

日志文件行被分成多个组

您现在有了一个可以使用的正则表达式。让我们使用 PHP 的preg_match()函数。它将正则表达式、与之匹配的字符串和作为模式匹配输出的数组作为参数:

preg_match($pattern,$logLine,$logs);

从那里,我们可以创建一个带有命名索引的关联数组来保存我们解析的上行:

$logArray = array();
$logArray['ip'] = gethostbyname($logs[1]);
$logArray['identity'] = $logs[2];
$logArray['user'] = $logs[2];
$logArray['date'] = $logs[4];
$logArray['time'] = $logs[5];
$logArray['timezone'] = $logs[6];
$logArray['method'] = $logs[7];
$logArray['path'] = $logs[8];
$logArray['protocol'] = $logs[9];
$logArray['status'] = $logs[10];
$logArray['bytes'] = $logs[11];
$logArray['referer'] = $logs[12];
$logArray['useragent'] = $logs[13];

我们完整的parseLogLine()函数现在应该是这样的:

function parseLogLine($logLine){$pattern = '/^([\d.:]+) (\S+) (\S+) \[([\w\/]+):([\w:]+)\s([+\-]\d{4})\] "(.+?) (.+?) (.+?)" (\d{3}) (\d+|(?:.+?)) "([^"]*|(?:.+?))" "([^"]*|(?:.+?))"/';preg_match($pattern,$logLine,$logs);$logArray = array();$logArray['ip'] = gethostbyname($logs[1]);$logArray['identity'] = $logs[2];$logArray['user'] = $logs[2];$logArray['date'] = $logs[4];$logArray['time'] = $logs[5];$logArray['timezone'] = $logs[6];$logArray['method'] = $logs[7];$logArray['path'] = $logs[8];$logArray['protocol'] = $logs[9];$logArray['status'] = $logs[10];$logArray['bytes'] = $logs[11];$logArray['referer'] = $logs[12];$logArray['useragent'] = $logs[13];return $logArray;
}

接下来,您将为getLocationbyIP()函数创建功能。

通过 IP 进行地理定位

getLocationbyIP()函数中,您可以通过解析访问日志的一行来获取数组,并使用 IP 字段来获取地理位置。通过 IP 地址获取地理位置的方法有很多种;大多数情况下,要么调用第三方 API,要么下载预先填充了 IP 位置信息的第三方数据库。其中一些第三方是免费提供的;有些是有成本的。

出于我们的目的,您可以使用 hostip.info 上的免费 API。图 5-7 显示了 hostip.info 主页。

img/313452_2_En_5_Fig7_HTML.jpg

图 5-7

hostip.info 主页

hostip.info 服务收集来自 ISP 的地理定位信息以及来自用户的直接反馈。它公开了一个 API 和一个可供下载的数据库。

该 API 在 http://api.hostip.info/ 可用。如果没有提供参数,API 将返回客户端的地理位置。默认情况下,API 返回 XML。返回值如下所示:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<HostipLookupResultSet version="1.0.1" xmlns:gml="  http://www.opengis.net/gml  " xmlns:xsi="  http://www.w3.org/2001/XMLSchema-instance  " xsi:noNamespaceSchemaLocation="  http://www.hostip.info/api/hostip-1.0.1.xsd  "><gml:description>This is the Hostip Lookup Service</gml:description><gml:name>hostip</gml:name><gml:boundedBy><gml:Null>inapplicable</gml:Null></gml:boundedBy><gml:featureMember><Hostip><ip>71.225.152.145</ip><gml:name>Chalfont, PA</gml:name><countryName>UNITED STATES</countryName><countryAbbrev>US</countryAbbrev><!-- Co-ordinates are available as lng,lat --><ipLocation><gml:pointProperty><gml:Point srsName="  http://www.opengis.net/gml/srs/epsg.xml#4326  "><gml:coordinates>-75.2097,40.2889</gml:coordinates></gml:Point></gml:pointProperty></ipLocation></Hostip></gml:featureMember>
</HostipLookupResultSet>

您可以细化 API 调用。如果你只需要国家信息,你可以呼叫 http://api.hostip.info/country.php 。它返回一个带有国家代码的字符串。如果 JSON 优于 XML,可以调用 http://api.hostip.info/get_json.php ,得到如下结果:

{"country_name":"UNITED STATES","country_code":"US","city":"Chalfont, PA","ip":"71.225.152.145"}

要指定 IP 地址,添加参数?ip=xxxx,如下所示:

http://api.hostip.info/get_json.php?ip=100.43.83.146

好了,我们来编写函数吧!

我们将剔除这个函数,让它接受一个数组。我们将从数组中提取 IP 地址,将其存储在一个变量中,并将该变量连接到一个包含 hostip.info API 路径的字符串:

function getLocationbyIP($arr){$IPAddress = $arr['ip'];$IPCheckURL = "  http://api.hostip.info/get_json.php?ip=$IPAddress  ";
}

您将把这个字符串传递给本机 PHP 函数file get_contents(),并将返回值(API 调用的结果)存储在一个名为jsonResponse的变量中。您将使用 PHP json_decode()函数将返回的 JSON 数据转换成原生 PHP 对象:

$jsonResponse =  file_get_contents($IPCheckURL);
$geoInfo = json_decode($jsonResponse);

接下来,从对象中提取地理位置数据,并将其添加到传递给函数的数组中。城市和州信息是由逗号和空格分隔的单个字符串(“费城,宾夕法尼亚州”),因此您需要在逗号处进行拆分,并将每个字段分别保存在数组中。

$arr['country'] = $geoInfo->{"country_code"};
$arr['city'] = explode(",",$geoInfo->{"city"})[0];
$arr['state'] = explode(",",$geoInfo->{"city"})[1];

接下来,让我们做一点错误检查,这将使后面的过程更容易。您将检查状态字符串是否有任何值;如果没有,就设置为“XX”。一旦您开始解析 r 中的数据,这将很有帮助。最后,您将返回更新后的数组:

if(count($arr['state']) < 1)$arr['state'] = "XX";
return $arr;

完整的函数应该是这样的:

function getLocationbyIP($arr){$IPAddress = $arr['ip'];$IPCheckURL = "  http://api.hostip.info/get_json.php?ip=$IPAddress  ";$jsonResponse =  file_get_contents($IPCheckURL);$geoInfo = json_decode($jsonResponse);$arr['country'] = $geoInfo->{"country_code"};$arr['city'] = explode(",",$geoInfo->{"city"})[0];$arr['state'] = explode(",",$geoInfo->{"city"})[1];if(count($arr['state']) < 1)$arr['state'] = "XX";return $arr;
}

最后,让我们创建一个函数,将处理后的数据写出到一个文件中。

输出字段

您将创建一个名为writeRLog()的函数,它接受两个参数——填充了修饰日志数据的数组和文件路径:

function writeRLog($arr, $file){
}

您需要创建一个名为writeFlag的变量,它将是一个标志,告诉 PHP 向文件中写入或追加数据。您检查文件是否存在;如果是这样,您将追加内容而不是覆盖内容。检查完毕后,打开文件:

writeFlag = "w";
if(file_exists($file)){$writeFlag = "a";
}
$fh = fopen($file, $writeFlag) or die("can't open file");

然后遍历传入的数组;构建一个包含每个日志条目的 IP 地址、日期、HTTP 状态、国家代码、州和城市的字符串;并将该字符串写入文件。一旦遍历完数组,就关闭文件。

for($x = 0; $x < count($arr); $x++){if($arr[$x]['country'] != "XX"){$data = $arr[$x]['ip'] . "," . $arr[$x]['date'] . "," . $arr[$x]['status'] . "," . $arr[$x]['country'] . "," . $arr[$x]['state'] . "," . $arr[$x]['city'];}fwrite($fh, $data . "\n");}

我们完成的writeRLog()函数应该是这样的:

function writeRLog($arr, $file){$writeFlag = "w";if(file_exists($file)){$writeFlag = "a";}$fh = fopen($file, $writeFlag) or die("can't open file");for($x = 0; $x < count($arr); $x++){if($arr[$x]['country'] != "XX"){$data = $arr[$x]['ip'] . "," . $arr[$x]['date'] . "," . $arr[$x]['status'] . "," . $arr[$x]['country'] . "," . $arr[$x]['state'] . "," . $arr[$x]['city'];}fwrite($fh, $data . "\n");}fclose($fh);echo "log created";
}

添加控制逻辑

最后,您将创建一些控制逻辑来调用您刚刚创建的所有这些函数。您将声明访问日志的路径和输出平面文件的路径,调用parseLog(),并将输出发送到writeRLog()

$logfile = "access_log";
$chartingData = "accessLogData.txt";
$logArr = parseLog($logfile);
writeRLog($logArr, $chartingData);

我们完成的 PHP 代码应该如下所示:

<html>
<head></head>
<body>
<?php
$logfile = "access_log";
$chartingData = "accessLogData.txt";
$logArr = parseLog($logfile);
writeRLog($logArr, $chartingData);
function parseLog($file){$logArray = array();$file_handle = fopen($file, "r");while (!feof($file_handle)) {$line = fgets($file_handle);$lineArr = parseLogLine($line);$lineArr = getLocationbyIP($lineArr);$logArray[count($logArray)] = $lineArr;}fclose($file_handle);return $logArray;
}
function parseLogLine($logLine){$pattern = '/^([\d.:]+) (\S+) (\S+) \[([\w\/]+):([\w:]+)\s([+\-]\d{4})\] "(.+?) (.+?) (.+?)" (\d{3}) (\d+|(?:.+?)) "([^"]*|(?:.+?))" "([^"]*|(?:.+?))"/';preg_match($pattern,$logLine,$logs);$logArray = array();$logArray['ip'] = gethostbyname($logs[1]);$logArray['identity'] = $logs[2];$logArray['user'] = $logs[2];$logArray['date'] = $logs[4];$logArray['time'] = $logs[5];$logArray['timezone'] = $logs[6];$logArray['method'] = $logs[7];$logArray['path'] = $logs[8];$logArray['protocol'] = $logs[9];$logArray['status'] = $logs[10];$logArray['bytes'] = $logs[11];$logArray['referer'] = $logs[12];$logArray['useragent'] = $logs[13];return $logArray;
}
function getLocationbyIP($arr){$IPAddress = $arr['ip'];$IPCheckURL = "http://api.hostip.info/get_json.php?ip=$IPAddress";$jsonResponse =  file_get_contents($IPCheckURL);$geoInfo = json_decode($jsonResponse);$arr['country'] = $geoInfo->{"country_code"};$arr['city'] = explode(",",$geoInfo->{"city"})[0];$arr['state'] = explode(",",$geoInfo->{"city"})[1];return $arr;
}
function writeRLog($arr, $file){$writeFlag = "w";if(file_exists($file)){$writeFlag = "a";}$fh = fopen($file, $writeFlag) or die("can't open file");for($x = 0; $x < count($arr); $x++){if($arr[$x]['country'] != "XX"){$data = $arr[$x]['ip'] . "," . $arr[$x]['date'] . "," . $arr[$x]['status'] . "," . $arr[$x]['country'] . "," . $arr[$x]['state'] . "," . $arr[$x]['city'];}fwrite($fh, $data . "\n");}fclose($fh);echo "log created";
}
?>
</body>
</html>

它应该会生成一个类似如下的平面文件:

71.225.152.145,18/Jan/2013,404,US, PA,Chalfont
114.119.143.124,14/Jun/2021,200,AU,,Canberra

我们在这里制作了一个示例访问日志: https://jonwestfall.com/data/access_log

在 R 中创建数据映射

到目前为止,您已经解析了访问日志,清理了数据,用位置信息修饰了数据,并创建了一个包含信息子集的平面文件。下一步是可视化这些数据。

因为您正在制作地图,所以需要安装地图包。开 R;在控制台中,键入以下内容:

> install.packages('maps')
> install.packages('mapproj')

现在我们可以开始了!要在 R 脚本中引用地图包,需要通过调用library()函数将其加载到内存中:

library(maps)
library(mapproj)

接下来创建几个变量——一个指向格式化的访问日志数据;另一个是列名列表。您创建了第三个变量logData,用于保存在读取平面文件时创建的数据帧。

logDataFile <- '/Applications/MAMP/htdocs/accessLogData.txt'
logColumns <- c("IP", "date", "HTTPstatus", "country", "state", "city")
logData <- read.table(logDataFile, sep=",", col.names=logColumns)

如果您在控制台中键入 logData ,您会看到数据帧的格式如下:

> logDataIP             date         HTTPstatus  country  state  city
1    100.43.83.146  25/Jan/2013  404         US       NV     Las Vegas
2    100.43.83.146  25/Jan/2013  301         US       NV     Las Vegas
3    64.29.151.221  25/Jan/2013  200         US       XX     (Unknown city)
4    180.76.6.26    25/Jan/2013  200         CN       XX     Beijing

显然,你可以从这里开始追踪几个不同的数据点。让我们首先来看一下流量来自哪些国家。

测绘地理数据

您可以从logData中提取唯一的国家名称开始。您将把它存储在一个名为country:的变量中

> country <- unique(logData$country)

如果您在控制台中键入 country ,数据如下所示:

> country
[1] US CN CA SE UA
Levels: CA CN SE UA US

这些是您从 iphost.info 获得的国家代码。R 使用不同的国家代码集,因此您需要将 iphost 国家代码转换为 R 国家代码。您可以通过对国家列表应用函数来实现这一点。

您将使用sapply()将您自己设计的匿名函数应用到国家代码列表中。在匿名函数中,您将修剪任何空白并直接替换国家代码。您将使用gsub()函数替换传入参数的所有实例。

country <- sapply(country, function(countryCode){#trim whitespaces from the country codecountryCode <- gsub("(^ +)|( +$)", "", countryCode)if(countryCode == "US"){countryCode<- "USA"}else if(countryCode == "AU"){countryCode<- "Australia"}}
)

您会注意到,您对每个国家代码都进行了硬编码。当然,这是一种不好的形式,一旦你深入研究了状态数据,你就会用一种非常不同的方式来处理这个问题。

如果您再次在控制台中键入 country ,您将会看到以下内容:

> countryUS          AU"USA" "Australia"

接下来使用match.map()函数将国家与地图包的国家列表进行匹配。函数创建一个数字向量,其中每个元素对应世界地图上的一个国家。交叉点的元素(国家列表中的国家与世界地图中的国家相匹配)具有分配给它们的值,特别是原始国家列表中的索引号。所以对应于美国的元素有 1,对应于加拿大的元素有 2,以此类推。在没有交集的地方,元素的值为 NA。

countryMatch <-  match.map("world2", country)

接下来让我们使用countryMatch列表来创建一个颜色编码的国家匹配。为此,只需应用一个检查每个元素的函数。如果不是 NA,将颜色#C6DBEF 分配给元素,这是一种很好的浅蓝色。如果元素是 NA,则将元素设置为 white 或#FFFFFF。您将把这个结果保存在一个新的列表中,您将称之为colorCountry

colorCountry <- sapply(countryMatch, function(c){if(!is.na(c)) c <- "#C6DBEF"else c <- "#FFFFFF"
})

现在让我们用map()函数创建我们的第一个可视化!map()函数接受几个参数:

  • 第一个是要使用的数据库的名称。数据库名称可以是worldusa statecounty;每个都包含与map()函数将要绘制的地理区域相关的数据点。

  • 如果您只想绘制更大的地理数据库的子集,您可以指定一个名为region的可选参数,该参数列出了要绘制的区域。

  • 您也可以指定要使用的地图投影。一个地图投影基本上是一种在平面上表现三维弯曲空间的方式。有许多预定义的投影,R 中的mapproj包支持许多这样的投影。对于您将要制作的世界地图,您将使用等面积投影,其标识符为“azequalarea”。有关地图投影的更多信息,请参见 http://xkcd.com/977/

  • 您还可以使用orientation参数指定地图的中心点,用纬度和经度表示。

  • 最后,您将把刚刚创建的colorCountry列表传递给col参数。

map('world', proj='azequalarea', orient=c(41,-74,0), boundary=TRUE, col=colorCountry, fill=TRUE)

这段代码生成的地图如图 5-8 所示。

img/313452_2_En_5_Fig8_HTML.jpg

图 5-8

使用世界地图的数据地图

从这张地图上,我们可以看到唯一列表中的国家为蓝色阴影,其余国家为白色。这很好,但我们可以做得更好。

添加纬度和经度

让我们从添加纬度和经度线开始,这将突出地球的曲率,并给出极点在哪里的背景。为了创建纬度和经度线,我们首先创建一个新的地图对象,但是我们将把plot设置为FALSE,这样地图就不会被绘制到屏幕上。我们将这个地图对象保存到一个名为m的变量中:

m <- map('world',plot=FALSE)

接下来我们将调用map.grid()并传入我们存储的地图对象:

map.grid(m, col="blue", label=FALSE, lty=2, pretty=TRUE)

请注意,如果您在命令窗口中一行一行地运行这段代码,那么在您输入代码时保持 Quartz 图形窗口打开是很重要的,这样 R 就可以更新图表。如果您在一行一行地输入时关闭 Quartz 窗口,您可能会得到一个错误消息,说明还没有调用plot.new。或者您可以将每一行输入到一个文本文件中,然后一次将它们复制到 R 命令行中。

现在,让我们在图表中添加一个刻度来显示

map.scale()

我们完成的 R 代码现在应该看起来像这样:

library(maps)
library(mapproj)
logDataFile <- '/Applications/MAMP/htdocs/accessLogData.txt'
logColumns <- c("IP", "date", "HTTPstatus", "country", "state", "city")
logData <- read.table(logDataFile, sep=",", col.names=logColumns)
country <- unique(logData$country)
country <- sapply(country, function(countryCode){#trim whitespaces from the country codecountryCode <- gsub("(^ +)|( +$)", "", countryCode)if(countryCode == "US"){countryCode<- "USA"}else if(countryCode == "CN"){countryCode<- "China"}else if(countryCode == "CA"){countryCode<- "Canada"}else if(countryCode == "SE"){countryCode<- "Sweden"}else if(countryCode == "UA"){countryCode<- "USSR"}
})
countryMatch <-  match.map("world", country)
#color code any states with visit data as light blue
colorCountry <- sapply(countryMatch, function(c){if(!is.na(c)) c <- "#C6DBEF"else c <- "#FFFFFF"
})
m <- map('world',plot=FALSE)
map('world',proj='azequalarea',orient=c(41,-74,0), boundary=TRUE, col=colorCountry,fill=TRUE)
map.grid(m,col="blue", label=FALSE, lty=2, pretty=TRUE)
map.scale()

并且这段代码输出如图 5-9 所示的世界地图。

img/313452_2_En_5_Fig9_HTML.jpg

图 5-9

带有经纬线和比例尺的全球数据地图

非常好!接下来,让我们深入分析美国各州的访问情况。

显示区域数据

让我们从隔离用户数据开始;我们可以通过选择状态不等于“XX”的所有行来实现这一点。还记得我们在 PHP 中解析访问日志时将 state 列中的值设置为“XX”吗?这就是为什么。美国以外的国家没有与之相关的州数据,所以我们可以简单地只提取有州数据的行。

usData <- logData[logData$state != "XX", ]

接下来,我们需要用完整的州名替换从 hostip.info 获得的州名缩写,这样我们就可以创建一个match.map查找列表,就像我们对前面的国家数据所做的那样。

州数据的好处是,R 有一个数据集,其中包含美国所有 50 个州的名称、缩写,甚至更深奥的信息,如州的区域和命名的部门(新英格兰、中大西洋等)。有关更多信息,请在 R 控制台键入?state.name

我们可以使用该数据集中的信息将州缩写与地图包所需的完整州名进行匹配。为此,我们使用apply()函数运行一个匿名函数,该函数遍历state.abb数据集,找到传入的州名缩写的匹配项,然后使用返回值作为索引,从state.name数据集检索完整的州名:

usData$state <- apply(as.matrix(usData$state), 1, function(s){#trim the abbreviation of whitespacess <- gsub("(^ +)|( +$)", "", s)s <- state.name[grep(s, state.abb)]
})

我们实现了与之前的国家比赛相同的功能,但更加优雅。如果我们愿意的话,我们可以回去创建我们自己的国家名称数据集,以备将来使用,从而为国家匹配提供一个类似的优雅解决方案。

现在我们有了完整的州名,我们可以提取一个唯一的州名列表,并使用该列表创建一个地图匹配列表(同样,就像我们对国家所做的一样):

states <- unique(usData$state)
stateMatch <- match.map("state", states)

使用我们的状态匹配列表,我们可以再次对其应用一个函数,该函数将在我们的匹配列表中查找不具有 NA 值的匹配元素,并将这些元素的值设置为我们漂亮的浅蓝色,而所有具有 NA 值的元素都设置为白色。我们将这个列表保存在一个名为colorMatch的变量中。

#color code any states with visit data as light blue
colorMatch <- sapply(stateMatch, function(s){if(!is.na(s)) s <- "#C6DBEF"else s <- "#FFFFFF"
})

然后我们可以在对map()函数的调用中使用colorMatch:

map("state", resolution = 0,lty = 0,projection = "azequalarea", col=colorMatch,fill=TRUE)

嗯,但是注意到什么了吗?只有彩色区域被绘制到舞台上,如图 5-10 所示。

img/313452_2_En_5_Fig10_HTML.png

图 5-10

仅显示有数据的州的数据映射

我们需要进行第二次map()调用来绘制地图的剩余部分。在这个map()调用中,我们将把add参数设置为TRUE,这将导致我们正在绘制的新地图被添加到当前地图中。在此过程中,让我们也为这张地图创建一个比例:

map("state", col = "black", fill=FALSE, add=TRUE, lty=1, lwd=1, projection="azequalarea")
map.scale()

该代码产生图 5-11 中的完成状态图。

img/313452_2_En_5_Fig11_HTML.jpg

图 5-11

已完成的状态数据映射

分发可视化

好了,现在让我们把 R 代码放在 R Markdown 文件中进行分发。让我们进入 RStudio,点击文件➤新➤ R Markdown。让我们添加一个标题,并确保我们的 R 代码包含在```r{r}标签中,并且我们的图表有指定的高度和宽度。我们完成的 R Markdown 文件应该如下所示:

Visualizing Spatial Data from Access Logs
========================================================
```r{r}
library(maps)
library(mapproj)
logDataFile <- '/Applications/MAMP/htdocs/accessLogData.txt'
logColumns <- c("IP", "date", "HTTPstatus", "country", "state", "city")
logData <- read.table(logDataFile, sep=",", col.names=logColumns)
#chart worldwide visit data
#unfortunately there is no state.name equivalent for countries so we must check
#the explicit country names. In the us states below we are able to accomplish this much
#more efficientlycountry <- unique(logData$country)
country <- sapply(country, function(countryCode){#trim whitespaces from the country codecountryCode <- gsub("(^ +)|( +$)", "", countryCode)if(countryCode == "US"){countryCode<- "USA"}else if(countryCode == "CN"){countryCode<- "China"}else if(countryCode == "CA"){countryCode<- "Canada"}else if(countryCode == "SE"){countryCode<- "Sweden"}else if(countryCode == "UA"){countryCode<- "USSR"}
})
countryMatch <-  match.map("world", country)
#color code any states with visit data as light blue
colorCountry <- sapply(countryMatch, function(c){if(!is.na(c)) c <- "#C6DBEF"else c <- "#FFFFFF"
})
m <- map('world',plot=FALSE)
map('world',proj='azequalarea',orient=c(41,-74,0), boundary=TRUE, col=colorCountry,fill=TRUE)
map.grid(m,col="blue", label=FALSE, lty=2, pretty=FALSE)
map.scale()
#isolate the US data, scrub any unknown statesusData <- logData[logData$state != "XX", ]
usData$state <- apply(as.matrix(usData$state), 1, function(s){#trim the abbreviation of whitespacess <- gsub("(^ +)|( +$)", "", s)s <- state.name[grep(s, state.abb)]
})
s <- map('state',plot=FALSE)
states <- unique(usData$state)
stateMatch <- match.map("state", states)
#color code any states with visit data as light blue
colorMatch <- sapply(stateMatch, function(s){if(!is.na(s)) s <- "#C6DBEF"else s <- "#FFFFFF"
})
map("state", resolution = 0,lty = 0,projection = "azequalarea", col=colorMatch,fill=TRUE)
map("state", col = "black",fill=FALSE,add=TRUE,lty=1,lwd=1,projection="azequalarea")
map.scale()

该代码产生如图 5-12 所示的输出。我还在本书的代码下载中提供了这个 R 脚本。![img/313452_2_En_5_Fig12a_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-ds-zh/raw/master/docs/pro-data-vis-r-js/img/313452_2_En_5_Fig12a_HTML.jpg) ![img/313452_2_En_5_Fig12b_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-ds-zh/raw/master/docs/pro-data-vis-r-js/img/313452_2_En_5_Fig12b_HTML.jpg)图 5-12R Markdown 中的数据映射## 摘要本章讨论了解析访问日志以生成数据映射可视化。您查看了地图中的全球国家数据和更多的本地化州数据。这是您开始将使用数据应用到生活中的第一次尝试。下一章在时序图的上下文中查看 bug backlog 数据。# 六、可视化随着时间的推移的数据最后一章讨论了使用访问日志来创建表示用户地理位置的数据地图。我们使用`map`和`mapproj`(用于地图投影)包来创建这些可视化。本章探讨如何创建时间序列图表,这是一种比较数值随时间变化的图表。它们通常从左到右读取,x 轴代表某个时间度量,y 轴代表值的范围。本章讨论可视化缺陷随时间的变化。随着时间的推移,跟踪缺陷不仅允许我们识别问题中的尖峰,还允许我们识别工作流中更大的模式,特别是当我们包括更细粒度的细节(如错误的严重性)并包括交叉引用数据(如迭代开始和结束等事件的日期)时。我们开始揭示一些趋势,比如在一个迭代中什么时候打开了 bug,什么时候打开了大部分阻塞 bug,或者什么迭代产生了最多的 bug。这种自我评估和反思让我们能够识别并关注盲点或需要改进的地方。它还允许我们在更大的范围内识别胜利,而这些胜利在没有上下文的情况下查看每日数据时可能会被错过。一个恰当的例子:最近我们的组织设立了一个更大的团队目标,即在年底达到一定的 bug 数量,占我们在年初打开的所有 bug 的百分比。与我们的同事和我们的管理人员一起,我们指导所有的开发人员,创建过程改进,并为这个目标赢得人心。年底时,我们仍未解决的 bug 数量与我们开始时大致相同。我们感到困惑和担忧。但是当我们合计每天的数字时,我们意识到我们取得了比预期更大的成就:与前一年相比,我们实际上每年打开的错误减少了三分之一。这是一个巨大的问题,如果我们不是以批判的眼光看待这些数据,很容易被忽略。## 收集数据创建缺陷时间序列图的第一步是决定我们想要查看和收集数据的时间段。这意味着获取给定时间段内所有 bug 的导出。这一步完全依赖于您可能使用的错误跟踪软件。也许您使用惠普的质量中心,因为它对您组织的其余测试需求有意义(例如能够使用 LoadRunner)。也许你使用一个托管的基于网络的解决方案,比如 Rally,因为你将缺陷管理与你的用户故事和发布跟踪捆绑在一起。也许你有自己安装的 Bugzilla,因为它是开放和免费的。不管是哪种情况,所有的缺陷管理软件都有方法导出你当前的缺陷列表。根据所使用的缺陷跟踪软件,您可以导出到一个平面文件,比如一个逗号或者制表符分隔的文件。该软件还允许通过 API 访问其内容,因此您可以创建一个访问 API 并公开内容的脚本。无论哪种方式,随着时间的推移,有两种重要的主要情况:*   按日期运行的错误总数*   按日期排列的新 bug对于这两种情况中的任何一种,当我们从缺陷跟踪软件中导出时,我们关心的最少字段如下:*   开业日期*   缺陷 id*   缺陷状态*   缺陷的严重程度*   缺陷的描述导出的 bug 数据应该如下所示:```r
Date, ID, Severity, Status, Summary
6/7/20,DE45091,Minor,Open,videos not playing
8/21/20,DE45092,Blocker,Open,alignment off
3/7/20,DE45093,Moderate,Closed,monsters attacking

让我们处理数据,以便能够可视化。

R 数据分析

第一件事是读入和排序数据。假设数据被导出到一个名为allbugs.csv的平面文件中,我们可以如下读入数据(我们已经在 http://jonwestfall.com/data/allbugs.csv 为其提供了示例数据):

bugExport <- "/Applications/MAMP/htdocs/allbugs.csv"
bugs <- read.table(bugExport, header=TRUE, sep=",")

让我们按日期排列数据框。为此,我们必须使用as.Date()函数将作为字符串读入的Date列转换为Date对象。as.Date()函数接受几个符号来表示如何读取和构造日期对象,如表 6-1 所示。

表 6-1

作为。日期( )函数符号

|

标志

|

意义

|
| --- | --- |
| %m | 数字月 |
| %b | 字符串形式的月份名称,缩写 |
| %B | 字符串形式的完整月份名称 |
| %d | 数字日 |
| %a | 缩写字符串形式的工作日 |
| %A | 字符串形式的完整工作日 |
| %y | 两位数的年份 |
| %Y | 四位数的年份 |

所以对于日期"04/01/2013",我们传入"%m/%d/%Y";对于"April 01, 13",我们传入"%B %d, %Y"。您可以看到模式是如何匹配的:

as.Date(bugs$Date,"%m/%d/%y")

我们将在order()函数中使用转换后的日期,该函数返回来自bugs数据框的索引号列表,对应于数据框中值的正确排序方式:

> order(as.Date(bugs$Date,"%m/%d/%y"))[1] 127  90 187 112  13 119 137 101  37  53  52  67 125   4  81  93 136   3  55  62  33  25 130  75  85  28[27]  44 159 126 107  30 191  80 124  36 104  18  24  82  20  21  34  56 147  29 156  16  59  51 139   1 123[53] 113 146 148   5 103  43  83  23 173  11 168  99  35   7 192  42 142 121   9  69   2 171  60  94 164  17[79]  91  84 178  96 105   8 110  39 177 109  97 120 135  58  79  15 111  49 117  50  57  92 129 114 145 158
[105] 116 151 143 162  31  73  77 182  26  74 195  10  48  88  76 183 115 184 189 108  61 174 144 186  12 134
[131] 157  41  86  27 175   6 165  46 118 188  65 141  22 169 190  72  66 154  40  47  64 166  14  87  95 155
[157] 193 133 179  54 140 128  89 102 161  63  45  78 138 180 149 185 106  38 181 172 176 153 160 150 170 122
[183] 194 100 167  68  98 132  70 152  19 163  71  32 131

最后,我们将使用order()函数的结果作为bugs数据帧的索引,并将结果传回bugs数据帧:

bugs <- bugs[order(as.Date(bugs$Date," %m/%d/%y ")),]

这段代码根据在order()函数中返回的索引顺序对bugs数据帧重新排序。当我们开始分割数据时,它会很方便。数据帧现在应该是按时间顺序排列的错误列表,如下所示:

> bugsDate      ID Severity Status                 Summary
127  1/3/20 DE45217    Minor   Open     Mug of coffee empty
90   1/4/20 DE45180    Minor Closed mug of coffee destroyed
187  1/5/20 DE45277    Minor   Open             Zerg attack
112  1/9/20 DE45202  Blocker Closed                 Monkeys
13  1/12/20 DE45103    Minor   Open     Mug of coffee empty
119 1/13/20 DE45209  Blocker Closed     The plague occurred
Let's write this newly ordered list back out to a new file that we will reference later called allbugsOrdered.csv:
write.table(bugs, col.names=TRUE, row.names=FALSE, file="allbugsOrdered.csv", quote = FALSE, sep = ",")

当我们在 D3 中查看这些数据时,这将派上用场。

计算 Bug 数量

接下来,我们将按日期计算总 bug 数。这将显示每天有多少新的 bug 被打开。

为此,我们将bugs$Date传递给table()函数,该函数在bugs数据帧中构建一个每个日期计数的数据结构:

totalBugsByDate <- table(bugs$Date)

所以totalBugsByDate的结构看起来如下:

> totalBugsByDate1/11/21  1/12/20  1/12/21  1/13/20  1/17/21  1/18/21   1/2/21  1/21/20  1/22/201        1        3        1        2        1        1        1        11/24/20  1/24/21  1/25/20  1/27/21  1/29/21   1/3/20   1/4/20   1/5/20   1/5/211        1        1        1        1        1        1        1        11/9/20  10/1/20 10/10/20 10/15/20 10/16/20 10/18/20 10/21/20 10/25/20 10/26/201        1        1        1        1        2        2        1        1
10/29/20 10/30/20  10/6/20 11/17/20 11/18/20 11/19/20 11/21/20 11/23/20 11/26/202        1        1        1        1        1        1        1        211/4/20  11/8/20 12/14/20 12/15/20 12/17/20 12/21/20 12/22/20 12/23/20 12/24/202        1        2        1        1        1        2        1        1
12/27/20 12/29/20  12/3/20 12/31/20  2/12/21  2/13/21  2/14/20  2/15/20  2/15/211        1        1        1        1        1        1        1        12/16/20  2/22/21  2/24/20  2/25/21  2/26/21  2/28/21   2/3/21   2/4/21   2/8/211        2        1        1        2        1        1        1        13/1/20   3/1/21  3/11/21  3/14/21  3/17/21   3/2/20   3/2/21  3/22/20  3/23/212        1        3        1        1        1        1        2        13/24/20  3/25/21  3/26/20  3/28/20   3/3/21  3/31/20  3/31/21   3/6/21   3/7/201        1        1        1        1        1        1        1        13/7/21  4/12/21  4/13/20  4/15/21  4/18/21  4/19/21  4/20/20  4/25/20  4/26/211        1        1        1        2        1        1        1        14/27/20  4/29/21   4/4/20   4/5/21   4/7/20   4/8/20   5/1/20  5/10/20  5/11/211        1        1        3        1        2        2        1        15/12/20  5/14/21  5/16/21  5/17/20  5/17/21   5/2/21  5/20/20  5/20/21  5/22/202        1        1        1        1        1        1        2        25/24/21  5/25/20  5/26/21  5/27/20  5/27/21  5/28/20  5/28/21  5/29/21  5/30/201        1        1        1        1        1        1        2        15/31/20   5/6/20   5/8/20  6/11/20  6/11/21  6/14/20  6/16/21   6/2/21  6/20/201        1        1        1        1        1        2        1        16/28/20   6/3/20   6/3/21   6/4/20   6/4/21   6/6/21   6/7/20   6/7/21   6/8/211        1        1        1        1        1        2        1        16/9/21  7/14/20  7/18/20   7/2/20  7/22/20  7/23/20  7/25/20  7/28/20  7/29/201        1        2        1        1        1        1        1        17/9/20  8/10/20  8/17/20   8/2/20  8/21/20  8/22/20  8/23/20  8/24/20  8/26/201        1        2        1        1        1        1        2        18/27/20  8/28/20  8/29/20   8/3/20   8/6/20  9/10/20  9/11/20  9/14/20  9/16/201        1        1        1        1        1        1        1        19/2/20  9/21/20   9/8/201        1        1

让我们将这些数据绘制出来,以了解每天有多少 bug 被打开:

plot(totalBugsByDate, type="l", main="New Bugs by Date", col="red", ylab="Bugs")

这段代码创建了如图 6-1 所示的图表。

img/313452_2_En_6_Fig1_HTML.jpg

图 6-1

按日期排列的新 bug 时间序列

现在我们已经知道了每天产生多少个 bug,我们可以通过使用cumsum()函数得到一个累计总数。它获取每天打开的新 bug,并创建它们的运行总和,每天更新总数。它允许我们为一段时间内累积的 bug 计数生成一条趋势线。

> runningTotalBugs <- cumsum(totalBugsByDate)
>
> runningTotalBugs1/11/21  1/12/20  1/12/21  1/13/20  1/17/21  1/18/21   1/2/21  1/21/20  1/22/201        2        5        6        8        9       10       11       121/24/20  1/24/21  1/25/20  1/27/21  1/29/21   1/3/20   1/4/20   1/5/20   1/5/2113       14       15       16       17       18       19       20       211/9/20  10/1/20 10/10/20 10/15/20 10/16/20 10/18/20 10/21/20 10/25/20 10/26/2022       23       24       25       26       28       30       31       32
10/29/20 10/30/20  10/6/20 11/17/20 11/18/20 11/19/20 11/21/20 11/23/20 11/26/2034       35       36       37       38       39       40       41       4311/4/20  11/8/20 12/14/20 12/15/20 12/17/20 12/21/20 12/22/20 12/23/20 12/24/2045       46       48       49       50       51       53       54       55
12/27/20 12/29/20  12/3/20 12/31/20  2/12/21  2/13/21  2/14/20  2/15/20  2/15/2156       57       58       59       60       61       62       63       642/16/20  2/22/21  2/24/20  2/25/21  2/26/21  2/28/21   2/3/21   2/4/21   2/8/2165       67       68       69       71       72       73       74       753/1/20   3/1/21  3/11/21  3/14/21  3/17/21   3/2/20   3/2/21  3/22/20  3/23/2177       78       81       82       83       84       85       87       883/24/20  3/25/21  3/26/20  3/28/20   3/3/21  3/31/20  3/31/21   3/6/21   3/7/2089       90       91       92       93       94       95       96       973/7/21  4/12/21  4/13/20  4/15/21  4/18/21  4/19/21  4/20/20  4/25/20  4/26/2198       99      100      101      103      104      105      106      1074/27/20  4/29/21   4/4/20   4/5/21   4/7/20   4/8/20   5/1/20  5/10/20  5/11/21108      109      110      113      114      116      118      119      1205/12/20  5/14/21  5/16/21  5/17/20  5/17/21   5/2/21  5/20/20  5/20/21  5/22/20122      123      124      125      126      127      128      130      1325/24/21  5/25/20  5/26/21  5/27/20  5/27/21  5/28/20  5/28/21  5/29/21  5/30/20133      134      135      136      137      138      139      141      1425/31/20   5/6/20   5/8/20  6/11/20  6/11/21  6/14/20  6/16/21   6/2/21  6/20/20143      144      145      146      147      148      150      151      1526/28/20   6/3/20   6/3/21   6/4/20   6/4/21   6/6/21   6/7/20   6/7/21   6/8/21153      154      155      156      157      158      160      161      1626/9/21  7/14/20  7/18/20   7/2/20  7/22/20  7/23/20  7/25/20  7/28/20  7/29/20163      164      166      167      168      169      170      171      1727/9/20  8/10/20  8/17/20   8/2/20  8/21/20  8/22/20  8/23/20  8/24/20  8/26/20173      174      176      177      178      179      180      182      1838/27/20  8/28/20  8/29/20   8/3/20   8/6/20  9/10/20  9/11/20  9/14/20  9/16/20184      185      186      187      188      189      190      191      1929/2/20  9/21/20   9/8/20193      194      195

这正是我们现在需要的,来规划 bug 积压每天增长或减少的方式。为此,让我们将runningTotalBugs传递给plot()函数。我们将类型设置为"l",以表示我们正在创建一个折线图,然后将该图命名为随时间累积的缺陷。在plot()函数中,我们还关闭了轴,这样我们就可以为这个图表绘制自定义轴。我们将希望绘制自定义轴,以便我们可以将日期指定为 x 轴标签。

为了绘制自定义轴,我们使用了axis()函数。axis()函数中的第一个参数是一个数字,它告诉 R 在哪里画轴。

  • 1对应图表底部的 x 轴。

  • 2在图表的左边。

  • 3到图表的顶端。

  • 在图表的右边。

plot(runningTotalBugs, type="l", xlab="", ylab="", pch=15, lty=1, col="red", main="Cumulative Defects Over Time", axes=FALSE)
axis(1, at=1: length(runningTotalBugs), lab= row.names(totalBugsByDate))
axis(2, las=1, at=10*0:max(runningTotalBugs))

请注意,绘图类型设置为小写 L,而不是大写 I 或 1。这段代码创建了如图 6-2 所示的时序图。

img/313452_2_En_6_Fig2_HTML.jpg

图 6-2

随着时间的推移累积的缺陷

这显示了按日期逐渐增加的 bug backlog。

到目前为止,完整的 R 代码如下:

bugExport <- "allbugs.csv"
bugs <- read.table(bugExport, header=TRUE, sep=",")
as.Date(bugs$Date,"%m/%d/%y")
order(as.Date(bugs$Date,"%m/%d/%y"))
bugs <- bugs[order(as.Date(bugs$Date," %m/%d/%y ")),]
write.table(bugs, col.names=TRUE, row.names=FALSE, file="allbugsOrdered.csv", quote = FALSE, sep = ",")totalBugsByDate <- table(bugs$Date)
plot(totalBugsByDate, type="l", main="New Bugs by Date", col="red", ylab="Bugs")
runningTotalBugs <- cumsum(totalBugsByDate)
runningTotalBugs
plot(runningTotalBugs, type="l", xlab="", ylab="", pch=15, lty=1, col="red", main="Cumulative Defects Over Time", axes=FALSE)
axis(1, at=1: length(runningTotalBugs), lab= row.names(totalBugsByDate))
axis(2, las=1, at=10*0:max(runningTotalBugs))

让我们来看看 bug 的关键程度,它不仅显示了 bug 何时被打开,还显示了最严重(或非严重)的 bug 何时被打开。

检查错误的严重性

请记住,当我们导出 bug 数据时,我们包括了Severity字段,它指示每个 bug 的严重程度。每个团队和组织可能有自己的严重性分类,但通常包括以下内容:

  • 阻塞程序是非常严重的错误,它们会阻止大量工作的启动。它们通常具有不完整的功能,或者缺少广泛使用的功能的某些部分。它们也可能是与合同或法律约束功能(如隐藏式字幕或数字版权保护)的差异。

  • 中度错误是严重的错误,但没有严重到导致发布的程度。它们可能会破坏不常用功能的功能。可访问性的范围,或者一个特性被广泛使用的程度,通常是使一个 bug 成为一个阻止者或者一个关键的决定因素。

  • minor是影响极小的 bug,甚至可能不会被最终用户注意到。

为了按严重程度分类 bug,我们简单地调用table()函数,就像我们按日期分类 bug 一样,但是这次也添加了Severity列:

bugsBySeverity <- table(factor(bugs$Date),bugs$Severity)

这段代码创建了一个如下所示的数据结构:

          Blocker Minor Moderate1/11/21       0     1        01/12/20       0     1        01/12/21       1     2        01/13/20       1     0        01/17/21       2     0        01/18/21       0     0        11/2/21        0     1        01/21/20       1     0        01/22/20       1     0        01/24/20       0     1        0

然后我们可以绘制这个数据对象。我们这样做的方法是使用plot()函数为其中一列创建一个图表,然后使用lines()函数在图表上为其余的列绘制线条:

plot(bugsBySeverity[,3], type="l", xlab="", ylab="", pch=15, lty=1, col="orange", main="New Bugs by Severity and Date", axes=FALSE)
lines(bugsBySeverity[,1], type="l", col="red", lty=1)
lines(bugsBySeverity[,2], type="l", col="yellow", lty=1)
axis(1, at=1: length(runningTotalBugs), lab= row.names(totalBugsByDate))
axis(2, las=1, at=0:max(bugsBySeverity[,3]))
legend("topleft", inset=.01, title="Legend", colnames(bugsBySeverity), lty=c(1,1,1), col= c("red", "yellow", "orange"))

该代码生成如图 6-3 所示的图表。

img/313452_2_En_6_Fig3_HTML.jpg

图 6-3

我们的 plot()和 lines()函数按照严重性绘制了错误图表

这很好,但是如果我们想按严重性查看累积的 bug 呢?我们可以简单地使用前面的 R 代码,但是我们可以绘制出每列的累积和,而不是绘制出每列:

plot(cumsum(bugsBySeverity[,3]), type="l", xlab="", ylab="", pch=15, lty=1, col="orange", main="Running Total of Bugs by Severity", axes=FALSE)
lines(cumsum(bugsBySeverity[,1]), type="l", col="red", lty=1)
lines(cumsum(bugsBySeverity[,2]), type="l", col="yellow", lty=1)
axis(1, at=1: length(runningTotalBugs), lab= row.names(totalBugsByDate))
axis(2, las=1, at=0:max(cumsum(bugsBySeverity[,3])))
legend("topleft", inset=.01, title="Legend", colnames(bugsBySeverity), lty=c(1,1,1), col= c("red", "yellow", "orange"))

该代码生成如图 6-4 所示的图表。

img/313452_2_En_6_Fig4_HTML.jpg

图 6-4

按严重性列出的运行错误总数

添加与 D3 的交互性

前面的例子是可视化和传播关于缺陷产生的信息的好方法。但是,如果我们能更进一步,让我们可视化的消费者更深入地研究他们感兴趣的数据点,会怎么样呢?假设我们希望允许用户将鼠标悬停在时间序列中的特定点上,并查看构成该数据点的所有 bug 的列表。我们可以用 D3 做到这一点;让我们走一遍并找出方法。

首先,让我们创建一个引用了D3.js的具有基本 HTML 框架结构的新文件,并将其保存为timeseriesGranular.htm。在这个例子中,我们想要使用 D3 的旧版本——版本 3 (d3.v3.js,可以在本书的代码下载中找到),因为它比新的代码结构允许更多的灵活性和逐步构建。

<html>
<head></head>
<body>
<script src="d3.v3.js"></script>
</body>
</html>

接下来,我们在一个新的script标签中设置一些初步数据。我们创建一个对象来保存图形的边距数据以及高度和宽度。我们还创建了一个 D3 时间格式化程序,将从 string 读入的日期转换成一个本地的Date对象。

<script>
var margin = {top: 20, right: 20, bottom: 30, left: 50},width = 960 - margin.left - margin.right,height = 500 - margin.top - margin.bottom;
var parseDate = d3.timeFormat("%m/%d/%y").parse;
</script>

读入数据

我们添加一些代码来读入数据(之前从 R 输出的allbugsOrdered.csv文件)。回想一下,这个文件包含了按日期排序的全部 bug 数据。

我们使用d3.csv()函数来读取这个文件:

  • 第一个参数是文件的路径。

  • 第二个参数是读入数据后要执行的函数。正是在这个匿名函数中,我们添加了大部分功能,或者至少是依赖于要处理的数据的功能。

匿名函数接受两个参数:

  • 第一个捕获任何可能发生的错误。

  • 第二个是正在读入的文件的内容。

在该函数中,我们首先遍历数据的内容,并使用日期格式化程序将Date列中的所有值转换为本地 JavaScript Date对象:

d3.csv("allbugsOrdered.csv", function(error, data) {data.forEach(function(d) {d.Date = parseDate(d.Date);
});
});

如果我们要console.log()数据,它将是一个看起来像图 6-5 的对象数组。

img/313452_2_En_6_Fig5_HTML.jpg

图 6-5

我们的 bug 数据对象

在匿名函数中,但在循环之后,我们使用d3.nest()函数来创建一个变量,该变量保存按日期分组的 bug 数据。我们将这个变量命名为nested_data:

nested_data = d3.nest()
.key(function(d) { return d.Date; })
.entries(data);

nested_data变量现在是一个树形结构——特别是一个按日期索引的列表,每个索引都有一个 bug 列表。如果我们去console.log() nested_data,它将是一个看起来像图 6-6 的对象数组。

img/313452_2_En_6_Fig6_HTML.jpg

图 6-6

包含 bug 数据对象的数组

在页面上绘图

我们准备开始绘制页面。因此,让我们跳出回调函数,转到script标记的根,并使用之前定义的边距、宽度和高度将 SVG 标记写出到页面:

var svg = d3.select("body").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

这是我们绘制轴和趋势线的容器。

仍然在根级别,我们为 x 轴和 y 轴添加一个 D3 scale对象,使用变量width表示 x 轴范围,使用变量height表示 y 轴范围。我们在根级别添加 x 轴和 y 轴,传入它们各自的缩放对象,并将它们定位在底部和左侧。

var xScale = d3.time.scale().range([0, width]);
var yScale= d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yAxis = d3.svg.axis().scale(yScale).orient("left");

但是它们仍然没有显示在页面上。我们需要返回到我们在d3.csv()调用中创建的匿名函数,并添加我们创建的nested_data列表,作为新创建的秤的域数据:

xScale.domain(d3.extent(nested_data, function(d) { return new Date(d.key); }));
yScale.domain(d3.extent(nested_data, function(d) { return d.values.length; }));

从这里,我们需要生成轴。我们通过添加和选择一个用于通用分组的 SVG g元素,并将这个选择添加到xAxis()yAxis() D3 函数中来实现。这也包含在加载数据时调用的匿名回调函数中。

我们还需要通过添加图表的高度来转换 x 轴,以便将其绘制在图表的底部:

svg.append("g").attr("transform", "translate(0," + height + ")").call(xAxis);
svg.append("g").call(yAxis)

这将创建图表的起点,其有意义的轴如图 6-7 所示。

img/313452_2_En_6_Fig7_HTML.jpg

图 6-7

时间序列开始形成;x 轴和 y 轴,但还没有线条

需要添加趋势线。回到根级别,让我们创建一个名为line的变量作为 SVG 行。假设我们已经为该行设置了data属性。我们还没有,但一会儿就会了。对于线条的 x 值,我们将有一个函数返回通过xScale刻度对象过滤的日期。对于线条的 y 值,我们将创建一个函数,返回通过yScale scale 对象运行的 bug 计数值。

var line = d3.svg.line().x(function(d) { return xScale(new Date(d.key)); }).y(function(d) { return yScale(d.values.length); });

接下来,我们回到处理数据的匿名函数。在添加的轴的正下方,我们将追加一个 SVG 路径。我们设置nested_data变量作为路径的基准,新创建的line对象作为d属性。作为参考,d属性是我们指定路径描述的地方。关于d属性的文档见此处: https://developer.mozilla.org/en-US/docs/SVG/Attribute/d

svg.append("path").datum(nested_data).attr("d", line);

我们现在可以开始在浏览器中看到一些东西。到目前为止,代码应该是这样的:

<!DOCTYPE html>
<head>
<meta charset="utf-8">
</head>
<body><script src="d3.v3.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 50},width = 960 - margin.left - margin.right,height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%m-%d-%Y").parse;
var xScale = d3.time.scale().range([0, width]);
var yScale = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yAxis = d3.svg.axis().scale(yScale).orient("left");
var line = d3.svg.line().x(function(d) { return xScale(new Date(d.key)); }).y(function(d) { return yScale(d.values.length); });
var svg = d3.select("body").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("allbugsOrdered.csv", function(error, data) {data.forEach(function(d) {d.Date = parseDate(d.Date);});nested_data = d3.nest().key(function(d) { return d.Date; }).entries(data);xScale.domain(d3.extent(nested_data, function(d) { return new Date(d.key); }));yScale.domain(d3.extent(nested_data, function(d) { return d.values.length; }));svg.append("g").attr("transform", "translate(0," + height + ")").call(xAxis);svg.append("g").call(yAxis);svg.append("path").datum(nested_data).attr("d", line);
});
</script></body>
</html>

该代码产生如图 6-8 所示的图形。

img/313452_2_En_6_Fig8_HTML.jpg

图 6-8

具有行数据但填充不正确的时间序列

但这并不完全正确。路径的阴影是基于浏览器对意图的最佳猜测,对其感知的封闭区域进行阴影处理。让我们使用 CSS 显式关闭阴影,改为设置路径线的颜色和宽度:

<style>
.trendLine {fill: none;stroke: #CC0000;stroke-width: 1.5px;
}
</style>

我们用类trendLine为页面上的任何元素创建了样式规则。接下来,让我们在创建路径的同一个代码块中将该类添加到 SVG 路径中:

Svg.append("path").datum(nested_data).attr("d", line).attr("class", "trendLine");

该代码生成如图 6-9 所示的图表。

img/313452_2_En_6_Fig9_HTML.jpg

图 6-9

具有校正的线条但无样式轴的时间序列

看起来好多了!我们应该做一些小的改动,比如在 y 轴上添加文本标签,并调整轴线的宽度,使其更加整洁:

.axis path{fill: none;stroke: #000;shape-rendering: crispEdges;
}

这将使我们的斧头看起来更紧。我们只需要在创建轴时将样式应用到轴上:

svg.append("g").attr("transform", "translate(0," + height + ")").call(xAxis).attr("class", "axis");
svg.append("g").call(yAxis).attr("class", "axis");

结果如图 6-10 所示。

img/313452_2_En_6_Fig10_HTML.jpg

图 6-10

用样式化轴更新的时间序列

到目前为止,这很好,但它没有显示出在 r 中这样做的真正好处。事实上,我们写了相当多的额外代码只是为了获得奇偶校验,甚至没有做我们在 r 中做的任何数据清理。

使用 D3 的真正好处是增加了交互性。

添加交互性

假设我们有这个新错误的时间序列,我们很好奇在二月中旬的大高峰中有什么错误。通过利用我们在 HTML 和 JavaScript 中工作的事实,我们可以通过添加一个工具提示框来扩展这一功能,该框列出了每个日期的错误。

要做到这一点,我们首先应该创建用户可以鼠标悬停的明显区域,例如每个数据点或离散日期处的红圈。要做到这一点,我们只需要在我们添加路径的地方下面创建 SVG 圆圈,在读入外部数据时触发匿名函数。我们将nested_data变量设置为圆圈的data属性,将它们设置为半径为 3.5 的红色,并将它们的 x 和 y 属性分别设置为与日期和 bug 总数相关联:

svg.selectAll("circle")
.data(nested_data)
.enter().append("circle").attr("r", 3.5).attr("fill", "red").attr("cx", function(d) { return xScale(new Date(d.key)); }).attr("cy", function(d) { return yScale(d.values.length);})

这段代码更新了现有的时间序列,看起来如图 6-11 所示。这些红圈现在是焦点区域,用户可以将鼠标放在上面查看更多信息。

img/313452_2_En_6_Fig11_HTML.jpg

图 6-11

添加到线上每个数据点的圆

让我们接下来编写一个div作为工具提示,我们将显示相关的错误数据。为此,我们将创建一个新的div,就在我们在script标签的根处创建line变量的下方。我们在 D3 中再次这样做,选择body标签并给它附加一个div,给它一个类和 idtooltip——这样我们就可以对它应用tooltip样式(我们将在一分钟内创建它),这样我们就可以在本章的后面通过 ID 与它交互。我们将默认隐藏它。我们将把对这个div的引用存储在一个我们称之为tooltip的变量中。

var tooltip = d3.select("body").append("div").attr("class", "tooltip").attr("id", "tooltip").style("position", "absolute").style("z-index", "10").style("visibility", "hidden");

我们接下来需要使用 CSS 样式化这个div。我们将不透明度调整为只有 75%可见,这样当工具提示出现在趋势线上时,我们可以看到它后面的趋势线。我们对齐文本,设置字体大小,使 div 有一个白色背景,并给它圆角。

.tooltip{opacity: .75;text-align:center;font-size:12px;width:100px;padding:5px;border:1px solid #a8b6ba;background-color:#fff;margin-bottom:5px;border-radius: 19px;-moz-border-radius: 19px;-webkit-border-radius: 19px;
}

接下来,我们必须向圆圈添加一个mouseover事件处理程序,用信息填充工具提示并取消隐藏工具提示。为此,我们返回到创建圆圈的代码块,并添加一个触发匿名函数的mousemove事件处理程序。

在匿名函数中,我们覆盖了工具提示的innerHTML,以显示当前红圈的日期以及与该日期相关的错误数量。然后,我们遍历 bug 列表,写出每个 bug 的 ID。

svg.selectAll("circle").data(nested_data).enter().append("circle").attr("r", 3.5).attr("fill", "red").attr("cx", function(d) { return xScale(new Date(d.key)); }).attr("cy", function(d) { return yScale(d.values.length);}).on("mouseover", function(d){document.getElementById("tooltip").innerHTML = d.key + " " + d.values.length + " bugs<br/>";for(x=0;x<d.values.length;x++){document.getElementById("tooltip").innerHTML += d.values[x].ID + "<br/>";}tooltip.style("visibility", "visible");})

如果我们想更进一步,我们可以为每个 bug ID 创建链接,链接到 bug 跟踪软件,列出每个 bug 的描述,如果 bug 跟踪软件有一个 API 接口,我们甚至可以有表单字段,让我们可以直接从这个工具提示更新 bug 信息。只有我们的想象力和可用的工具限制了我们将这个概念延伸到什么程度的可能性。

最后,我们向红圈添加了一个mousemove事件处理程序,这样每当用户将鼠标放在红圈上时,我们就可以根据上下文重新定位工具提示。为此,我们使用d3.mouse对象获取当前鼠标坐标。我们使用这些坐标来简单地用 CSS 重新定位工具提示。所以我们没有用工具提示覆盖红色圆圈,我们将顶部属性偏移 25 像素,将左侧属性偏移 75 像素。

svg.selectAll("circle").data(nested_data).enter().append("circle").attr("r", 3.5).attr("fill", "red").attr("cx", function(d) { return xScale(new Date(d.key)); }).attr("cy", function(d) { return yScale(d.values.length);}).on("mouseover", function(d){document.getElementById("tooltip").innerHTML = d.key + " " + d.values.length + " bugs<br/>";for(x=0;x<d.values.length;x++){document.getElementById("tooltip").innerHTML += d.values[x].ID + "<br/>";}tooltip.style("visibility", "visible");}).on("mousemove", function(){return tooltip.style("top", (d3.mouse(this)[1] + 25)+"px").style("left", (d3.mouse(this)[0] + 70)+"px");});

当鼠标悬停在其中一个红色圆圈上时,应显示工具提示(参见图 6-12 )。

img/313452_2_En_6_Fig12_HTML.jpg

图 6-12

显示翻转的完整时间序列

完整的源代码现在应该是这样的:

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
<style>
body {font: 15px sans-serif;
}
.trendLine {fill: none;stroke: #CC0000;stroke-width: 1.5px;
}
.axis path{fill: none;stroke: #000;shape-rendering: crispEdges;
}
.tooltip{opacity: .75;text-align:center;font-size:12px;width:100px;padding:5px;border:1px solid #a8b6ba;background-color:#fff;margin-bottom:5px;border-radius: 19px;-moz-border-radius: 19px;-webkit-border-radius: 19px;
}
</style>
</head>
<body><script src="d3.v3.js"></script>
<script>var margin = {top: 20, right: 20, bottom: 30, left: 50},width = 960 - margin.left - margin.right,height = 500 - margin.top - margin.bottom;
var parseDate = d3.time.format("%m/%d/%y").parse;
var xScale = d3.time.scale().range([0, width]);
var yScale = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yAxis = d3.svg.axis().scale(yScale).orient("left");
var line = d3.svg.line().x(function(d) { return xScale(new Date(d.key)); }).y(function(d) { return yScale(d.values.length); });
var tooltip = d3.select("body").append("div").attr("class", "tooltip").attr("id", "tooltip").style("position", "absolute").style("z-index", "10").style("visibility", "hidden");
var svg = d3.select("body").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("https://jonwestfall.com/data/allbugsOrdered.csv", function(error, data) {data.forEach(function(d) {d.Date = parseDate(d.Date);});nested_data = d3.nest().key(function(d) { return d.Date; }).entries(data);xScale.domain(d3.extent(nested_data, function(d) { return new Date(d.key); }));yScale.domain(d3.extent(nested_data, function(d) { return d.values.length; }));svg.append("g").attr("transform", "translate(0," + height + ")").call(xAxis).attr("class", "axis");svg.append("g").call(yAxis).attr("class", "axis");svg.append("path").datum(nested_data).attr("d", line).attr("class", "trendLine");svg.selectAll("circle").data(nested_data).enter().append("circle").attr("r", 3.5).attr("fill", "red").attr("cx", function(d) { return xScale(new Date(d.key)); }).attr("cy", function(d) { return yScale(d.values.length);}).on("mouseover", function(d){document.getElementById("tooltip").innerHTML = d.key + " " + d.values.length + " bugs<br/>";for(x=0;x<d.values.length;x++){document.getElementById("tooltip").innerHTML += d.values[x].ID + "<br/>";}tooltip.style("visibility", "visible");}).on("mousemove", function(){return tooltip.style("top", (d3.mouse(this)[1] + 25)+"px").style("left", (d3.mouse(this)[0] + 70)+"px");});
});
</script>
</body>
</html>

摘要

本章探索了时间序列图,既有哲学上的,也有使用它们来跟踪一段时间内的 bug 创建的上下文。我们从所选的错误跟踪软件中导出原始错误数据,并将其导入到 R 中进行清理和分析。

在 R 中,我们研究了建模和可视化数据的不同方法,研究了聚合和粒度细节,例如新的 bug 如何随着时间的推移对运行总数产生影响,或者新的 bug 是何时随着时间的推移引入的。当我们能够把我们正在看的日期联系起来时,这是特别有价值的。

然后,我们将数据读入 D3,并创建了一个交互式时间序列,使我们能够从高级趋势数据深入到所创建的每个 bug 的细节。

下一章将探讨如何创建条形图,以及如何使用它们来确定需要关注和改进的领域。

七、条形图

第六章探讨了如何使用时序图来查看一段时间内的缺陷数据,这一章介绍了条形图,它显示了相对于特定数据集的有序或分级数据。它们通常由 x 轴和 y 轴组成,并有条形或彩色矩形来表示类别的值。

威廉·普莱费尔(William Playfair)在 1786 年的第一版《商业与政治地图集》(The Commercial and Political Atlas)中创建了条形图,以显示苏格兰与世界不同地区的进出口数据(见图 7-1 )。他出于需要创造了它;地图册中的其他图表是时间序列图表,展示了数百年的贸易数据,但对于苏格兰来说,只有一年的数据。在使用时间序列图时,Playfair 认为这是一种低劣的可视化;一种与现有资源的妥协,因为它“不包含时间的任何部分,并且在效用上比包含时间的部分差得多”(Playfair,1786,第 101 页)。

img/313452_2_En_7_Fig1_HTML.jpg

图 7-1

威廉·普莱费尔的条形图显示了苏格兰的进出口数据

Playfair 最初对他的发明评价很低,以至于他都懒得把它收录到随后的第二版和第三版地图集里。他继续设想一种不同的方式来展示整体的一部分;为此,他在 1801 年出版的统计年鉴中发明了饼状图。

条形图是展示分级数据的好方法,不仅因为条形图是显示数值差异的清晰方式,而且通过使用不同类型的条形图(如堆积条形图和分组条形图),该模式还可以扩展为包括更多的数据点。

标准条形图

让我们来看看您已经熟悉的数据——上一章的bugsBySeverity数据:

head(bugsBySeverity)Blocker Minor Moderate1/11/21       0     1        01/12/20       0     1        01/12/21       1     2        01/13/20       1     0        01/17/21       2     0        01/18/21       0     0        1

您可以创建一个新的列表,其中包含每种错误类型的总和,并以条形图的形式显示总数,如下所示:

totalBugsBySeverity <- c(sum(bugsBySeverity[,1]), sum(bugsBySeverity[,2]), sum(bugsBySeverity[,3]))
barplot(totalBugsBySeverity, main="Total Bugs by Severity")
axis(1, at=1: length(totalBugsBySeverity), lab=c("Blocker", "Critical", "Minor"))

该代码生成如图 7-2 所示的图表。

img/313452_2_En_7_Fig2_HTML.jpg

图 7-2

按严重性划分的错误条形图

堆积条形图

堆积条形图允许我们显示类别中的子部分或分段。假设您使用bugsBySeverity时间序列数据,并希望查看每天新出现的 bug 的危险程度:

t(bugsBySeverity)1/11/21 1/12/20 1/12/21 1/13/20 1/17/21 1/18/21 1/2/21 1/21/20 1/22/20
Blocker   0       0       1       1       2       0      0       1       1
Minor     1       1       2       0       0       0      1       0       0
Moderate  0       0       0       0       0       1      0       0       01/24/20 1/24/21 1/25/20 1/27/21 1/29/21 1/3/20 1/4/20 1/5/20 1/5/21
Blocker   0       0       1       0       0      0      0      0      0
Minor     1       1       0       1       0      1      1      1      1
Moderate  0       0       0       0       1      0      0      0      01/9/20 10/1/20 10/10/20 10/15/20 10/16/20 10/18/20 10/21/20 10/25/20
Blocker  1       0        0        1        0        0        0        1
Minor    0       1        0        0        1        0        1        0
Moderate 0       0        1        0        0        2        1        010/26/20 10/29/20 10/30/20 10/6/20 11/17/20 11/18/20 11/19/20 11/21/20
Blocker    0        1        0       0        0        1        0        0
Minor      0        0        1       1        1        0        1        1
Moderate   1        1        0       0        0        0        0        011/23/20 11/26/20 11/4/20 11/8/20 12/14/20 12/15/20 12/17/20 12/21/20
Blocker    0        2       1       1        1        1        0        1
Minor      1        0       1       0        0        0        1        0
Moderate   0        0       0       0        1        0        0        012/22/20 12/23/20 12/24/20 12/27/20 12/29/20 12/3/20 12/31/20 2/12/21
Blocker    1        0        1        0        0       1        0       1
Minor      0        1        0        0        1       0        1       0
Moderate   1        0        0        1        0       0        0       02/13/21 2/14/20 2/15/20 2/15/21 2/16/20 2/22/21 2/24/20 2/25/21
Blocker   0       1       0       1       1       1       1       0
Minor     0       0       1       0       0       1       0       1
Moderate  1       0       0       0       0       0       0       02/26/21 2/28/21 2/3/21 2/4/21 2/8/21 3/1/20 3/1/21 3/11/21 3/14/21
Blocker   1       1      1      1      1      0      1       2       0
Minor     1       0      0      0      0      0      0       1       1
Moderate  0       0      0      0      0      2      0       0       03/17/21 3/2/20 3/2/21 3/22/20 3/23/21 3/24/20 3/25/21 3/26/20 3/28/20
Blocker   1      1      1       1       0       0       1       0       1
Minor     0      0      0       1       1       0       0       1       0
Moderate  0      0      0       0       0       1       0       0       03/3/21 3/31/20 3/31/21 3/6/21 3/7/20 3/7/21 4/12/21 4/13/20 4/15/21
Blocker  1       0       1      1      0      0       0       0       0
Minor    0       0       0      0      0      0       0       1       0
Moderate 0       1       0      0      1      1       1       0       14/18/21 4/19/21 4/20/20 4/25/20 4/26/21 4/27/20 4/29/21 4/4/20 4/5/21
Blocker   0       0       1       0       1       1       1      0      2
Minor     2       1       0       1       0       0       0      1      1
Moderate  0       0       0       0       0       0       0      0      04/7/20 4/8/20 5/1/20 5/10/20 5/11/21 5/12/20 5/14/21 5/16/21 5/17/20
Blocker  1      1      2       0       1       1       0       1       1
Minor    0      0      0       1       0       1       1       0       0
Moderate 0      1      0       0       0       0       0       0       05/17/21 5/2/21 5/20/20 5/20/21 5/22/20 5/24/21 5/25/20 5/26/21 5/27/20
Blocker   1      1       0       1       2       0       0       1       1
Minor     0      0       0       0       0       1       0       0       0
Moderate  0      0       1       1       0       0       1       0       05/27/21 5/28/20 5/28/21 5/29/21 5/30/20 5/31/20 5/6/20 5/8/20 6/11/20
Blocker  1       0       1       2       1       1      0      1       1
Minor    0       1       0       0       0       0      1      0       0
Moderate 0       0       0       0       0       0      0      0       06/11/21 6/14/20 6/16/21 6/2/21 6/20/20 6/28/20 6/3/20 6/3/21 6/4/20
Blocker   1       1       2      1       1       1      0      1      0
Minor     0       0       0      0       0       0      0      0      1
Moderate  0       0       0      0       0       0      1      0      06/4/21 6/6/21 6/7/20 6/7/21 6/8/21 6/9/21 7/14/20 7/18/20 7/2/20
Blocker  0      1      0      1      0      0       1       2      0
Minor    1      0      1      0      1      1       0       0      1
Moderate 0      0      1      0      0      0       0       0      07/22/20 7/23/20 7/25/20 7/28/20 7/29/20 7/9/20 8/10/20 8/17/20 8/2/20
Blocker   1       0       0       1       0      0       0       0      0
Minor     0       1       0       0       1      1       1       0      1
Moderate  0       0       1       0       0      0       0       2      08/21/20 8/22/20 8/23/20 8/24/20 8/26/20 8/27/20 8/28/20 8/29/20 8/3/20
Blocker   1       0       0       2       1       0       0       1      0
Minor     0       0       1       0       0       1       1       0      1
Moderate  0       1       0       0       0       0       0       0      08/6/20 9/10/20 9/11/20 9/14/20 9/16/20 9/2/20 9/21/20 9/8/20
Blocker  1       1       1       0       0      0       0      0
Minor    0       0       0       0       0      1       1      0
Moderate 0       0       0       1       1      0       0      1

您可以用堆积条形图表示以下数据,如图 7-3 所示:

img/313452_2_En_7_Fig3_HTML.jpg

图 7-3

按严重性和日期排列的错误堆积条形图。因为每天臭虫的总数不同,所以这些条的高度也不一样

barplot(t(bugsBySeverity), col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))

总的缺陷由条形的全高表示,每个条形的彩色部分表示缺陷的严重程度。堆积条形图使我们能够显示数据中的细微差别,尽管人们可能希望在可视化时减少日期的数量以获得更清晰的图片。

分组条形图

分组条形图使我们能够显示与堆叠条形图相同的细微差别,但我们不是将各段放在彼此的顶部,而是将它们分成并排的分组。图 7-4 显示 x 轴上的每个日期都有三个与之相关的条形,每个条形代表一个关键程度类别:

img/313452_2_En_7_Fig4_HTML.jpg

图 7-4

按严重性和日期分组的错误条形图

barplot(t(bugsBySeverity), beside=TRUE, col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))

由于数据的密度,乍一看,数字 7-3 和 7-4 可能是相同的。为了避免这种情况,我们可以使用下面的代码来减少数据点的数量,只显示五天的数据。尝试使用这两个代码片段来查看更改。

barplot(t(bugsBySeverity[1:10,]), col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))versusbarplot(t(bugsBySeverity[1:10,]), beside=TRUE, col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))

可视化和分析生产事故

如果您开发的产品被某个人使用——最终用户、消费服务甚至内部客户——您很可能经历过生产事故。当应用程序的某个部分在生产中对用户不正常时,就会发生生产事故。它非常像一个 bug,但它是您的客户所经历和报告的 bug。

就像 bug 一样,生产事件是正常的,是软件开发的预期结果。谈论事件时,有三个主要问题需要考虑:

  • 报告的错误的严重性或影响程度:站点中断和小的布局错误之间有很大的区别。

  • 频率,或者说事件发生或重复发生的频率:如果你的网络应用充满问题,你的客户体验、你的品牌和你正常的工作流程都会受到影响。

  • 持续时间,或个别事件持续多长时间:持续时间越长,受影响的顾客就越多,对你的品牌影响就越大。

处理生产事故是产品运营和组织成熟的重要组成部分。根据事件的严重程度,它们可能会破坏你的日常工作;团队可能需要停止一切工作,努力解决这个问题。优先级较低的项目可以排队,并与常规功能工作一起引入常规工作主体。

与处理生产事故同样重要的是能够分析生产事故的趋势,以确定问题领域。问题区域通常是生产中经常出现问题的特征或部分。一旦我们确定了问题领域,我们就可以进行根本原因分析,并有可能开始围绕这些领域构建主动的脚手架。

Note

主动脚手架是我创造的一个术语,用来描述建立故障转移或额外的安全围栏,以防止问题区域的问题再次出现。主动搭建可以是从检测用户何时接近容量限制(如浏览器 cookie 限制或应用程序堆大小,并在问题发生前纠正)到注意第三方资产的性能问题,并在将它们呈现给客户端之前拦截和优化它们。

另一种处理生产事故的有趣方式是 Heroku 过去处理事故的方式:将事故与逐月正常运行时间可视化一起放在时间线上,并公开发布。Heroku 的生产事件时间表在 https://status.heroku.com/可用;见图 7-5 。

img/313452_2_En_7_Fig5_HTML.jpg

图 7-5

Heroku 状态页面

GitHub 过去也有一个很棒的状态页面,可以可视化关于其性能和正常运行时间的关键指标(见图 7-6 )。具有讽刺意味的是,他们现在已经切换到 Heroku 放弃的时间线方法(见图 7-7 ,来自 www.githubstatus.com/history )。

img/313452_2_En_7_Fig7_HTML.jpg

图 7-7

GitHub 的时间表

img/313452_2_En_7_Fig6_HTML.jpg

图 7-6

GitHub 状态页

就我们的目的而言,本章使用条形图按特征查看生产事故,以开始识别我们自己产品中的问题区域。

用 R 在条形图上绘制数据

如果我们想要规划出我们的生产事件,我们必须首先获得数据的导出,就像我们需要为 bug 做的那样。因为生产事故通常是一次性事件,公司通常使用一系列方法来跟踪它们,从吉拉( www.atlassian.com/software/jira/overview )等票务系统到维护项目的电子表格,只要我们能检索到原始数据,什么都行。(乔恩在这里做了样本数据: http://jonwestfall.com/data/productionincidents.csv )。)

一旦我们有了原始数据,它可能看起来像下面这样:一个逗号分隔的平面列表,包含 ID、日期戳和描述列。还应该有一列列出发生事件的应用程序的功能或部分。

ID,DateOpened,DateClosed,Description,Feature,Severity
880373,5/22/21 10:14,5/25/21 11:52,Fwd: 2 new e-books Associate Editors,General Inquiry,1
837947,4/29/21 12:35,5/7/21 14:09,Fwd: New Resource to Post,General Inquiry,2
489036,4/23/21 14:38,4/27/21 9:00,STP ebook editor with finished book,General Inquiry,1
443617,1/25/21 17:43,1/26/21 8:49,New member - IRC Committee at STP,General Inquiry,2
911894,1/18/21 10:25,1/20/21 8:51,Fwd: Updates to International Relations Committee page,General Inquiry,1
974124,1/11/21 14:55,1/12/21 10:55,Fwd: New Resource to Post,General Inquiry,2
341352,1/2/21 10:51,1/5/21 16:26,New eBooks,eBook Publishing,1

让我们将原始数据读入 R 并存储在一个名为prodData的变量中:

> prodIncidentsFile <- "http://jonwestfall.com/data/productionincidents.csv";
> prodData <- read.table(prodIncidentsFile, sep=",", header=TRUE)
> prodDataID    DateOpened     DateClosed  Description              Feature           Severity
1 880373 5/22/21 10:14  5/25/21 11:52  Fwd: 2 new e-books Associate Editors    General Inquiry   1
2 837947 4/29/21 12:35   5/7/21 14:09  Fwd: New Resource to Post    General Inquiry   2
3 489036 4/23/21 14:38   4/27/21 9:00  STP ebook editor with finished book    General Inquiry   1
4 443617 1/25/21 17:43   1/26/21 8:49  New member - IRC Committee at STP    General Inquiry   2
5 911894 1/18/21 10:25   1/20/21 8:51  Fwd: Updates to International                                        Relations Committee page    General Inquiry   1
6 974124 1/11/21 14:55  1/12/21 10:55  Fwd: New Resource to Post    General Inquiry   2
7 341352  1/2/21 10:51   1/5/21 16:26  New eBooks     eBook Publishing  1

我们希望按照Feature列对它们进行分组,这样我们就可以绘制特性总数的图表。为此,我们在 R 中使用了aggregate()函数。aggregate()函数接受一个 R 对象、一个用作分组元素的列表和一个应用于分组元素的函数。因此,假设我们调用aggregate()函数,将 ID 列作为 R 对象传入,让它按Feature列分组,并让 R 获得每个特性分组的长度:

prodIncidentByFeature <- aggregate(prodData$ID, by=list(Feature=prodData$Feature), FUN=length)

这段代码创建了一个如下所示的对象:

> prodIncidentByFeatureFeature x
1 eBook Publishing 1
2  General Inquiry 6

然后我们可以将这个对象传递给barplot()函数,得到如图 7-8 所示的图表。

img/313452_2_En_7_Fig8_HTML.jpg

图 7-8

开始绘制条形图

barplot(prodIncidentByFeature$x)

这是一个很好的开始,确实讲述了一个故事,但它不是很有描述性。除了 x 轴没有被标记的事实之外,问题区域由于没有对结果排序而变得模糊。

订购结果

让我们使用order()函数按照每个事件的总数对结果进行排序:

prodIncidentByFeature <- prodIncidentByFeature[order(prodIncidentByFeature$x),]

然后,我们可以通过水平分层条形图并将文本旋转 90 度来设置条形图的格式,以突出显示这种顺序。

要旋转文本,我们必须使用par()功能改变我们的图形参数。更新图形参数具有全局影响,这意味着我们在更新后创建的任何图表都会继承这些更改,因此我们需要保留当前设置,并在创建条形图后重置它们。我们将当前设置存储在一个名为opar的变量中:

opar <- par(no.readonly=TRUE)

Note

如果您在 R 命令行中跟随,前面的行本身不会生成任何东西;它只是设置图形参数。

然后,我们将新参数传递给par()调用。我们可以使用las参数来格式化轴。las参数接受以下值:

  • 0 是文本平行于轴的默认行为。

  • 1 显式使文本水平。

  • 2 使文本垂直于轴。

  • 3 显式使文本垂直。

par(las=3)

然后我们再次调用barplot(),但是这次传入参数horiz=TRUE,让 R 水平而不是垂直绘制线条:

barplot(prodIncidentByFeature$x, xlab="Number of Incidents", names.arg=prodIncidentByFeature$Featurehoriz =真,space=1, cex.axis=0.6, cex.names=0.8, main="Production Incidents by Feature", col= "#CCCCCC")

And, finally, we restore the saved settings so that future charts don't inherit this chart's settings:
> par(opar)

这段代码产生了如图 7-9 所示的可视化效果。

img/313452_2_En_7_Fig9_HTML.jpg

图 7-9

按功能划分的生产事故条形图

从这张图表中,你可以看到最大的问题领域是标签为一般查询的类别,其次是电子书出版。

创建堆积条形图

围绕这些功能的问题有多严重?接下来,让我们创建一个堆积条形图,查看每个生产事件的严重性细分。为此,我们必须创建一个表,在该表中,我们按特征和严重性对生产事件进行细分。我们可以为此使用table()函数,就像我们在上一章中对 bug 所做的那样:

prodIncidentByFeatureBySeverity <- table(factor(prodData$Feature),prodData$Severity)

该代码创建一个如图 7-10 所示格式的变量,其中行代表每个特性,列代表每个严重级别:

img/313452_2_En_7_Fig11_HTML.jpg

图 7-11

按功能和严重性划分的生产事件堆积条形图

img/313452_2_En_7_Fig10_HTML.jpg

图 7-10

按功能和严重性划分的生产事件堆积条形图

prodIncidentByFeatureBySeverity1 2eBook Publishing 1 0General Inquiry  3 3
opar <- par(no.readonly=TRUE)
par(las=3, mar=c(5,5,5,5))
barplot(t(prodIncidentByFeatureBySeverity), xlab="Number of Incidents", names.arg=rownames(prodIncidentByFeatureBySeverity), horiz=TRUE, space=1, cex.axis=0.6, cex.names=0.8, main="Production Incidents by Feature", col=c("#CCCCCC", "#666666", "#AAAAAA", "#333333"))
legend("bottom", inset=.01, title="Legend", c("Sev1", "Sev2"), fill=c("#CCCCCC", "#666666"))
par(opar)

有意思!我们失去了排序,但那是因为我们有许多新的数据点可供选择。高级总量与此图的相关性较低;更重要的是严重性的分解。

D3 的条形图

现在,您已经知道了使用条形图在较高层次上汇总数据的好处,以及获得堆积条形图所能揭示的粒度细分的好处。让我们换个角度,使用 D3 来看看如何创建一个高级条形图,它允许我们深入每个条形图,以查看运行时数据的粒度表示。

我们首先在 D3 版本 3 中创建一个条形图,然后创建一个堆叠条形图。当我们的用户将鼠标放在条形图上时,我们将叠加堆叠的条形图,以显示数据如何实时分解。

创建垂直条形图

因为我们在第四章中已经在 D3 中制作了一个水平条形图,现在我们将制作一个垂直条形图。遵循我们在前几章中建立的相同模式,我们首先创建一个基本的 HTML 框架结构,它包括一个到 D3 版本 3 库的链接。我们使用上一章中用于正文和轴路径的相同的基本样式规则,以及一个额外的规则来将 bar 类中的所有元素着色为深灰色。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="d3.v3.js"></script>
<style type="text/css">body {font: 15px sans-serif;}.axis path{fill: none;stroke: #000;shape-rendering: crispEdges;}.bar {fill: #666666;}
</style>
</head>
<body></body>
</html>

接下来,我们创建script标签来保存所有的图表代码,以及保存尺寸信息的初始变量集:基本高度和宽度,用于 x 和 y 坐标信息的 D3 scale 对象,保存边距信息的对象,以及从总高度中去掉上下边距的调整后的高度值:

<script>
var w = 960,h = 500,x = d3.scale.ordinal().rangeRoundBands([0, w]),y = d3.scale.linear().range([0, h]),z = d3.scale.ordinal().range(["lightpink", "darkgray", "lightblue"])margin = {top: 20, right: 20, bottom: 30, left: 40},adjustedHeight = 500 - margin.top - margin.bottom;
</script>

接下来,我们创建 x 轴对象。请记住,在前面的章节中,轴还没有画出来,所以我们需要稍后在可缩放矢量图形(SVG)标记中调用它,我们将创建这个标记来画轴:

var xAxis = d3.svg.axis().scale(x).orient("bottom");

让我们将 SVG 容器绘制到页面上。这将是我们将绘制到页面上的所有其他内容的父容器。

var svg = d3.select("body").append("svg").attr("width", w).attr("height", h).append("g")

下一步是读入数据。我们将使用与 R 示例相同的数据源:平面文件productionIncidents.txt。我们可以使用d3.csv()函数读取并解析文件。一旦文件的内容被读入,它们就被存储在变量data中,但是如果出现任何错误,我们将把错误细节存储在一个我们称之为error的变量中。

d3.csv("http://jonwestfall.com/data/productionincidents.csv", function(error, data) {}

在这个d3.csv()函数的范围内,我们将放置大部分剩余的功能,因为这些功能依赖于数据的处理。

让我们按特征汇总数据。为此,我们使用d3.nest()函数并将键设置为Feature列:

nested_data = d3.nest().key(function(d) { return d.Feature; }).entries(data);

这段代码创建了一个对象数组。

在这个数组中,每个对象都有一个列出特性的键和一个列出每个生产事件的对象数组。

我们使用这个数据结构来创建核心条形图。我们为此创建了一个函数:

function barchart(){
}

在这个函数中,我们设置了svg元素的transform属性,它设置了包含将要绘制的图像的坐标。在这种情况下,我们将其限制为左边距和上边距值:

svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

我们还为 x 轴和 y 轴创建缩放对象。对于条形图,我们通常对 x 轴使用顺序刻度,因为它们用于离散值,如类别。更多关于 D3 顺序音阶的信息可以在 https://github.com/mbostock/d3/wiki/Ordinal-Scales 的文档中找到。

我们还创建了 scale 对象来将数据映射到图表的边界:

var xScale = d3.scale.ordinal().rangeRoundBands([0, w], .1);
var yScale = d3.scale.linear().range([h, 0]);
xScale.domain(data.map(function(d) { return d.key; }));
yScale.domain([0, d3.max(nested_data, function(d) { return d.values.length; })]);

我们接下来需要画出栅栏。我们基于分配给条的级联样式表(CSS)类创建一个选择。我们将nested_data绑定到条上,为nested_data中的每个键值创建 SVG 矩形,并将bar类分配给每个矩形;我们将很快定义类样式规则。我们将每个条形的 x 坐标设置为顺序刻度,并将 y 坐标和height属性都设置为线性刻度。

我们还添加了一个mouseover事件处理程序,并调用了一个我们很快就会创建的函数transitionVisualization()。当鼠标悬停在其中一个条形图上时,此函数会转换我们将在条形图上制作的堆叠条形图。

svg.selectAll(".bar").data(nested_data).enter().append("rect").attr("class", "bar").attr("x", function(d) { return xScale(d.key); }).attr("width", xScale.rangeBand()).attr("y", function(d) { return yScale(d.values.length) - 50; }).attr("height", function(d) { return h - yScale(d.values.length); }).on("mouseover", function(d){transitionVisualization (1)})

让我们添加一个对我们将要创建的函数drawAxes()的调用:

drawAxes()

完整的barchart()函数如下所示:

  function barchart(){svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");var xScale = d3.scale.ordinal().rangeRoundBands([0, w], .1);var yScale = d3.scale.linear().range([h, 0]);xScale.domain(nested_data.map(function(d) { return d.key; }));yScale.domain([0, d3.max(nested_data, function(d) { return d.values.length; })]);svg.selectAll(".bar").data(nested_data).enter().append("rect").attr("class", "bar").attr("x", function(d) { return xScale(d.key); }).attr("width", xScale.rangeBand()).attr("y", function(d) { return yScale(d.values.length) - 50; }).attr("height", function(d) { return h - yScale(d.values.length); }).on("mouseover", function(d){transitionVisualization (1)})drawAxes()}

让我们创建drawAxes()函数。我们把这个函数放在了d3.csv()函数的范围之外,在script标签的根处。

对于这个图表,让我们用更简单的方法,只画 x 轴。就像上一章一样,我们绘制 SVG g元素并调用xAxis对象:

function drawAxes(){svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + adjustedHeight + ")").call(xAxis);
}

这将绘制 x 轴,该轴为条形图提供其类别标签。

创建堆积条形图

现在我们有了一个条形图,让我们创建一个堆积条形图。首先,让我们塑造数据。我们需要一个对象数组,其中每个对象代表一个特性,并有每个级别的事件总数。

让我们从一个名为grouped_data的新数组开始:

var grouped_data = new Array();

让我们通过nested_data进行迭代,因为nested_data已经按照特性进行了分组:

nested_data.forEach(function (d) {
}

在每次遍历nested_data时,我们创建一个临时对象,并遍历values数组中的每个事件:

tempObj = {"Feature": d.key, "Sev1":0, "Sev2":0, "Sev3":0, "Sev4":0};d.values.forEach(function(e){}

values数组的每次迭代中,我们测试当前事件的严重性,并增加临时对象的适当属性:

if(e.Severity == 1)tempObj.Sev1++;
else if(e.Severity == 2)tempObj.Sev2++
else if(e.Severity == 3)tempObj.Sev3++;
else if(e.Severity == 4)tempObj.Sev4++;

创建grouped_data数组的完整代码如下所示:

nested_data.forEach(function (d) {tempObj = {"Feature": d.key, "Sev1":0, "Sev2":0, "Sev3":0, "Sev4":0};d.values.forEach(function(e){if(e.Severity == 1)tempObj.Sev1++;else if(e.Severity == 2)tempObj.Sev2++else if(e.Severity == 3)tempObj.Sev3++;else if(e.Severity == 4)tempObj.Sev4++;})grouped_data[grouped_data.length] = tempObj
});

完美!接下来,我们创建一个函数,在该函数中,我们在d3.csv()函数的范围内绘制堆积条形图:

function stackedBarChart(){
}

这就是有趣的地方。使用d3.layout.stack()函数,我们转置我们的数据,这样我们就有了一个数组,其中每个索引代表一个严重级别,并包含每个特征的一个对象,该对象具有相应严重级别的每个事件的计数:

var sevStatus = d3.layout.stack()(["Sev1", "Sev2", "Sev3", "Sev4"].map(function(sevs){return grouped_data.map(function(d) {return {x: d.Feature, y: +d[sevs]};});}));

接下来,我们使用sevStatus为将要绘制的条形段的 x 和 y 值创建域图:

x.domain(sevStatus[0].map(function(d) { return d.x; }));
y.domain([0, d3.max(sevStatus[sevStatus.length - 1], function(d) { return d.y0 + d.y; })]);

接下来,我们为sevStatus数组中的每个索引绘制 SVG g元素。它们充当容器,我们在其中画出线条。我们将sevStatus绑定到这些分组元素,并设置fill属性来返回颜色数组中的一种颜色。

var sevs = svg.selectAll("g.sevs").data(sevStatus).enter().append("g").attr("class", "sevs").style("fill", function(d, i) { return z(i); });

最后,我们在刚刚创建的分组中绘制条形。我们将一个通用函数绑定到条形的data属性,该属性只传递传递给它的任何数据;这继承自 SVG 分组。

我们在不透明度设置为 0 的情况下绘制条形,因此条形最初是不可见的。我们还附加了mouseovermouseout事件处理程序,以调用transitionVisualization()——当mouseover事件被触发时传递 1,当mouseout事件被触发时传递 0(我们将很快充实transitionVisualization()的功能)。

var rect = sevs.selectAll("rect").data(function(data){ return data; }).enter().append("svg:rect").attr("x", function(d) { return x(d.x) + 13; }).attr("y", function(d) { return -y(d.y0) - y(d.y) + adjustedHeight; }).attr("class", "groupedBar").attr("opacity", 0).attr("height", function(d) { return y(d.y) ; }).attr("width", x.rangeBand() - 20).on("mouseover", function(d){transitionVisualization (1)}).on("mouseout", function(d){transitionVisualization (0)});

完整的堆积条形图代码应该如下所示

function groupedBarChart(){var sevStatus = d3.layout.stack()(["Sev1", "Sev2", "Sev3", "Sev4"].map(function(sevs){return grouped_data.map(function(d) {return {x: d.Feature, y: +d[sevs]};});}));x.domain(sevStatus[0].map(function(d) { return d.x; }));y.domain([0, d3.max(sevStatus[sevStatus.length - 1], function(d) { return d.y0 + d.y; })]);// Add a group for each sev category.var sevs = svg.selectAll("g.sevs").data(sevStatus).enter().append("g").attr("class", "sevs").style("fill", function(d, i) { return z(i); }).style("stroke", function(d, i) { return d3.rgb(z(i)).darker(); });var rect = sevs.selectAll("rect"). data(function(data){ return data; }).enter().append("svg:rect").attr("x", function(d) { return x(d.x) + 13; }).attr("y", function(d) { return -y(d.y0) - y(d.y) + adjustedHeight; }).attr("class", "groupedBar").attr("opacity", 0).attr("height", function(d) { return y(d.y) ; }).attr("width", x.rangeBand() - 20).on("mouseover", function(d){transitionVisualization (1)}).on("mouseout", function(d){transitionVisualization (0)});}

创建覆盖的可视化

但是我们还没有完成。我们一直在引用这个transitionVisualization()函数,但是我们还没有定义它。让我们现在就解决这个问题。还记得我们是如何使用它的吗:当用户将鼠标放在我们的条形图上时,我们调用transitionVisualization()并传入 1。当用户将鼠标放在我们的堆积条形图上时,我们也调用transitionVisualization()并传入一个 1。但是当用户鼠标离开堆叠条形图中的一个条时,我们调用transitionVisualization()并传入一个 0。

因此,我们传入的参数设置了堆叠条形图的不透明度。因为我们最初绘制的堆叠条形图的不透明度为 0,所以只有当用户滑过条形图中的某个条时,我们才会看到它,而当用户滚离该条时,它又会隐藏起来。

为了创造这种效果,我们使用 D3 过渡。过渡很像其他语言(如 ActionScript 3)中的补间。我们创建一个 D3 选择(在这种情况下,我们可以选择类groupedBar的所有元素),调用transition(),并设置我们想要更改的选择的属性:

function transitionVisualization(vis){var rect = svg.selectAll(".groupedBar").transition().attr("opacity", vis)
}

我们现在已经有了完整的可视化,如图 7-11 所示。

完整的代码如下所示,虽然很难通过印刷媒体演示这一功能,但您可以在 Jon 的网站上看到工作模型(可在 https://jonwestfall.com/d3/ch7.d3.example.htm 找到)或将代码放在本地 web 服务器上并自己运行:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title><script src="d3.v3.js"></script><style type="text/css">body {font: 15px sans-serif;}.axis path{fill: none;stroke: #000;shape-rendering: crispEdges;}.bar {fill: #666666;}</style>  </head><body><script type="text/javascript">
var w = 960,h = 500,x = d3.scale.ordinal().rangeRoundBands([0, w]),y = d3.scale.linear().range([0,h]),z = d3.scale.ordinal().range(["lightpink", "darkgray", "lightblue"])margin = {top: 20, right: 20, bottom: 30, left: 40},adjustedHeight = 500 - margin.top - margin.bottom;var xAxis = d3.svg.axis().scale(x).orient("bottom");var svg = d3.select("body").append("svg").attr("width", w).attr("height", h).append("g")function drawAxes(){svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + adjustedHeight + ")").call(xAxis);}function transitionVisuaization(vis){var rect = svg.selectAll(".groupedBar").transition().attr("opacity", vis)}d3.csv("https://jonwestfall.com/data/productionincidents.csv", function(error, data) {nested_data = d3.nest().key(function(d) { return d.Feature; }).entries(data);var grouped_data = new Array();//for stacked bar chartnested_data.forEach(function (d) {tempObj = {"Feature": d.key, "Sev1":0, "Sev2":0, "Sev3":0, "Sev4":0};d.values.forEach(function(e){if(e.Severity == 1)tempObj.Sev1++;else if(e.Severity == 2)tempObj.Sev2++else if(e.Severity == 3)tempObj.Sev3++;else if(e.Severity == 4)tempObj.Sev4++;})grouped_data[grouped_data.length] = tempObj});
function stackedBarChart(){var sevStatus = d3.layout.stack()(["Sev1", "Sev2", "Sev3", "Sev4"].map(function(sevs) {return grouped_data.map(function(d) {return {x: d.Feature, y: +d[sevs]};});}));x.domain(sevStatus[0].map(function(d) { return d.x; }));y.domain([0, d3.max(sevStatus[sevStatus.length - 1], function(d) { return d.y0 + d.y; })]);// Add a group for each sev category.var sevs = svg.selectAll("g.sevs").data(sevStatus).enter().append("g").attr("class", "sevs").style("fill", function(d, i) { return z(i); });var rect = sevs.selectAll("rect").data(function(data){ return data; }).enter().append("svg:rect").attr("x", function(d) { return x(d.x) + 13; }).attr("y", function(d) { return -y(d.y0) - y(d.y) + adjustedHeight; }).attr("class", "groupedBar").attr("opacity", 0).attr("height", function(d) { return y(d.y) ; }).attr("width", x.rangeBand() - 20).on("mouseover", function(d){transitionVisuaization(1)}).on("mouseout", function(d){transitionVisuaization(0)});}function barchart(){svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");var xScale = d3.scale.ordinal().rangeRoundBands([0, w], .1);var yScale = d3.scale.linear().range([h, 0]);xScale.domain(nested_data.map(function(d) { return d.key; }));yScale.domain([0, d3.max(nested_data, function(d) { return d.values.length; })]);svg.selectAll(".bar").data(nested_data).enter().append("rect").attr("class", "bar").attr("x", function(d) { return xScale(d.key); }).attr("width", xScale.rangeBand()).attr("y", function(d) { return yScale(d.values.length) - 50; }).attr("height", function(d) { return h - yScale(d.values.length); }).on("mouseover", function(d){transitionVisuaization(1)})stackedBarChart()drawAxes()}barchart();
});</script></body>
</html>

摘要

本章介绍了如何使用条形图来显示生产事故环境中的分级数据。因为生产事故本质上是来自用户群的关于你的产品如何不正常或失败的直接反馈,管理生产事故是任何成熟的工程组织的关键部分。

然而,管理生产事故不仅仅是在问题出现时做出反应;它还与分析围绕您的事件的数据有关:您的应用程序的哪些领域经常中断,您在生产中看到哪些意外的使用模式可能会导致这些重复出现的问题,如何构建主动式脚手架来防止这些问题以及未来的问题。所有这些问题只有通过充分了解你的产品和数据才能回答。在这一章中,你朝着更大的理解迈出了第一步。

八、散点图相关分析

在上一章中,您了解了如何使用条形图来分析生产事故。您看到条形图非常适合显示已排序数据集中的差异,并且您使用这一想法来确定问题重复出现的区域。您还使用堆积条形图查看了生产事故严重性的细分。

本章着眼于散点图的相关性分析。散点图是在各自的轴上绘制两个独立数据集的图表,显示为笛卡尔网格(x 和 y 坐标)上的点。正如您将看到的,散点图用于尝试和识别两个数据点之间的关系。

Note

Michael Friendly 和 Daniel Denis 发表了一篇关于散点图历史的经过深思熟虑和彻底研究的论文,最初发表于 2005 年行为科学史杂志,第 41 卷,并可在 Friendly 的网站 www.datavis.ca/papers/friendly-scat.pdf 上获得。这篇文章绝对值得一读,因为它试图追溯最早记录的散点图和图表第一次被称为散点图,并且非常巧妙地描述了散点图和时间序列之间的区别(换句话说,所有的时间序列都是以时间为轴的散点图,而不是所有的散点图都是时间序列!).

在数据中查找关系

散点图上的点形成的模式或缺乏模式表明了这种关系。在很高的层面上,关系可以是

img/313452_2_En_8_Fig1_HTML.jpg

图 8-1

散点图显示了北美和欧洲手机总数之间的正相关关系

  • 正相关,其中一个变量随着另一个变量的增加而增加。这可以通过从左到右形成一条对角线的点来证明(见图 8-1 )。

img/313452_2_En_8_Fig2_HTML.jpg

图 8-2

散点图显示体重和时间流逝之间的负相关关系(对于正在节食的人)

  • 负相关,其中一个变量增加,另一个减少。这可以通过形成一条从左到右向下的线的点来证明(见图 8-2 )。

img/313452_2_En_8_Fig3_HTML.jpg

图 8-3

散点图显示美国历年意外死亡人数之间没有关联

  • 无相关性,由散点图显示(或不显示),散点图没有可辨别的趋势线(见图 8-3 )。

当然,简单地识别两个数据点或数据集之间的相关性并不意味着这种关系中有直接的原因——因此习惯上认为相关性并不意味着因果关系。例如,参见图 8-2 中的负相关图。如果我们假设两个轴——体重和天数——之间有直接的因果关系,我们将假设时间的流逝导致体重下降。

虽然散点图对于分析两组数据之间的关系非常有用,但是也有一种相关的模式可以用来引入第三组数据。这种可视化被称为气泡图,它使用散点图中的点的半径来展示数据的第三维。

参见图 8-4 中的气泡图,该图显示了豚鼠牙齿生长长度与服用维生素 C 剂量之间的相关性。第三个数据点是给药方式:要么补充维生素,要么喝橙汁。它作为图形中每个点的半径添加;大圆圈是维生素补充剂,小圆圈是橙汁。

img/313452_2_En_8_Fig4_HTML.jpg

图 8-4

豚鼠牙齿生长与维生素 C 剂量的相关性,包括维生素补充剂和橙汁

为了本章的目的,我们将使用散点图和气泡图来查看团队速度与我们关注的其他领域的隐含关系,实际上是对团队动态进行相关性分析。我们将比较团队规模和速度、速度和生产事故等等。

敏捷开发的介绍性概念

让我们从介绍敏捷开发的一些初步概念开始。如果你已经精通敏捷,这一节将是一个回顾。敏捷开发有很多种风格,但是最常见的高级概念是将大量工作时间打包的思想。时间盒使团队能够专注于一件事情并完成它,允许涉众对完成的内容快速给出反馈。这个简短的反馈循环允许团队和涉众随着需求甚至行业的变化而改变方向。

团队在工作主体上工作的这段时间——无论是一周、三周还是其他——被称为冲刺。在 sprint 结束时,团队应该有可发布的代码,尽管并不要求在每次 sprint 之后发布。

sprint 以团队定义工作主体的计划会议开始,以团队检查已完成的工作主体的回顾会议结束。在冲刺阶段,团队定期培训新的工作来完成;它定义了列出验收标准的用户故事中的工作。正是这些用户故事在每个 sprint 开始时举行的计划会议中得到优先考虑和承诺。

该流程的高级工作流程如图 8-5 所示。

img/313452_2_En_8_Fig5_HTML.jpg

图 8-5

敏捷开发的高级工作流

用户故事有与之相关的故事点。故事点是对故事复杂程度的估计,通常是一个数值。当团队完成冲刺时,他们开始形成一致的速度。速度是团队在一次冲刺中完成的故事点的平均数量。

速度是很重要的,因为你用它来估计你的团队在每个 sprint 开始时可以完成多少,并预测团队在一年中根据你的路线图可以完成多少积压的工作。

有很多工具可以用来管理敏捷项目,比如 Rally ( www.rallydev.com/ )或者 Atlassian 的 green hopper(www.atlassian.com/software/greenhopper/overview),也是这家公司制造了吉拉和 Confluence。无论您使用什么工具,都应该能够导出您的数据,包括每个 sprint 的用户点数。

相关分析

为了开始分析,让我们导出每个 sprint 的故事点总和以及团队名称。我们应该将所有这些数据点编译成一个文件,命名为teamvelocity.txt。我们的文件应该看起来像下面这样,它显示了名为 Red 和 Gold 的团队的 12.1 和 12.2 sprints 的数据(任意的名称,这些团队使用不同的工作主体开发相同的产品):

Sprint,TotalPoints,Team
12.1,25,Gold
12.1,63,Red
12.2,54,Red
...

让我们在那里添加一个额外的列来表示每个 sprint 的每个团队的总成员。数据现在应该是这样的:

Sprint,TotalPoints,TotalDevs,Team
12.1,25,6,Gold
12.1,63,10,Red
12.2,54,9,Red
...

我们也提供了这个样本数据集,有更多的点,这里: https://jonwestfall.com/data/teamvelocity.txt

太棒了。现在让我们将它读入 R,将第一行中的路径改为您放置它的位置:

tvFile <- "/Applications/MAMP/htdocs/teamvelocity.txt"
teamvelocity <- read.table(tvFile, sep=",", header=TRUE)

创建散点图

现在使用plot()函数创建一个散点图,比较团队在每个 sprint 中完成的总分数和每个 sprint 中团队成员的数量。我们将teamvelocity$TotalPointsteamvelocity$TotalDevs作为前两个参数传递,将类型设置为p,并为轴赋予有意义的标签:

plot(teamvelocity$TotalPoints,teamvelocity$TotalDevs, type="p", ylab="Team Members", xlab="Velocity", bg="#CCCCCC", pch=21)

这就产生了我们在图 8-6 中看到的散点图;我们可以看到,随着我们向团队中添加更多的成员,他们在迭代或 sprint 中可以完成的故事点的数量也在增加。

img/313452_2_En_8_Fig6_HTML.jpg

图 8-6

团队速度和团队成员总数的相关性

创建气泡图

如果我们想要更深入地了解到目前为止的数据,例如,显示哪些点属于哪个团队,我们可以用气泡图来可视化这些信息。我们可以使用symbols()函数创建气泡图。我们将TotalPointsTotalDevs传入symbols(),就像我们对plot()所做的一样,但是我们也将Team列传入一个名为circles的参数。这指定了要在图表上绘制的圆的半径。因为在我们的例子中Team是一个字符串,R 将其转换成一个因子。我们也用bg参数设置圆的颜色,用fg参数设置圆的笔画颜色。

symbols(teamvelocity$TotalPoints, teamvelocity$TotalDevs, circles=as.factor(teamvelocity$Team), inches=0.35, fg="#000000", bg="#CCCCCC", ylab="Team Members", xlab="Velocity")

前面的 R 代码应该会产生一个类似图 8-7 的气泡图。

img/313452_2_En_8_Fig7_HTML.png

图 8-7

团队速度、团队成员总数与表示团队的气泡大小的相关性

可视化 bug

图 8-7 所示的气泡图用处有限,主要是因为团队分解并不是真正相关的数据点。让我们拿起teamvelocity.txt文件,开始添加更多的信息。我们已经在第六章中讨论过追踪 bug 数据;现在让我们使用我们的 bug 跟踪软件,添加两个新的与 bug 相关的数据点:每个 sprint 结束时每个团队的 backlog 中的总 bug,以及每个 sprint 中打开了多少个 bug。我们将这些新数据点的列分别命名为BugBacklogBugsOpened

更新后的文件应该如下所示:

Sprint,TotalPoints,TotalDevs,Team,BugBacklog,BugsOpened
12.1,25,6,Gold,125,10
12.2,42,8,Gold,135,30
12.3,45,8,Gold,150,25

接下来,让我们用这些新数据创建一个散点图。我们首先将速度与每次迭代中打开的 bug 进行比较:

plot(teamvelocity$TotalPoints,teamvelocity$BugsOpened, type="p", xlab="Velocity", ylab="Bugs Opened During Sprint", bg="#CCCCCC", pch=21)

这将创建如图 8-8 所示的散点图。

img/313452_2_En_8_Fig8_HTML.jpg

图 8-8

团队速度和打开的 bug 的相关性

这很有趣。团队中有更多的人和完成更多的工作(或者至少完成更复杂的工作)之间存在正相关,完成的故事点越多,产生的 bug 就越多。因此,复杂性的增加与给定 sprint 中产生的 bug 数量的增加相关。至少我的数据似乎暗示了这一点。

让我们在现有的气泡图中反映这个新的数据点;我们不是按团队来划分圈子,而是按打开的 bug 来划分圈子:

symbols(teamvelocity$TotalPoints, teamvelocity$TotalDevs, circles= teamvelocity$BugsOpened, inches=0.35, fg="#000000", bg="#CCCCCC", ylab="Team Members", xlab="Velocity", main = "Velocity by Team Size by Bugs Opened")

这段代码生成如图 8-9 所示的气泡图;您可以看到气泡的大小遵循现有的正相关模式,气泡随着团队成员数量和团队速度的增加而变大。

img/313452_2_En_8_Fig9_HTML.jpg

图 8-9

团队速度和团队规模的相关性,其中圆圈大小表示打开的 bug

接下来,让我们创建一个散点图来查看每个 sprint 之后的总 bug backlog:

plot(teamvelocity$TotalPoints,teamvelocity$BugBacklog, type="p", xlab="Velocity", ylab="Total Bug Backlog", bg="#CCCCCC", pch=21)

该代码生成如图 8-10 所示的图表。

img/313452_2_En_8_Fig10_HTML.jpg

图 8-10

团队速度与总 bug 积压的相关性

这个数字表明不存在相关性。这可能是因为许多原因:也许团队一直在 sprint 期间修复 bug,或者他们正在关闭迭代过程中打开的所有 bug。确定根本原因超出了散点图的范围,但是我们可以看出,当 bug 被打开并且复杂性增加时,bug 的总积压量并没有增加。

可视化生产事件

让我们在另一个数据点进入文件的下一层;我们将针对 sprint 期间完成的工作添加一个生产事件列。具体来说,当一个 sprint 中的一部分工作完成后,它就被发布到产品中,并且一个发布版本号通常与这个发布版本相关联。我们讨论的最后一个数据点是关于在给定迭代的发布中跟踪生产中的问题。而不是迭代过程中出现的问题;迭代中完成的工作被推向生产时出现的问题。

现在让我们添加最后一列,名为ProductionIncidents:

Sprint,TotalPoints,TotalDevs,Team,BugBacklog,BugsOpened,ProductionIncidents
12.1,25,6,Gold,125,10,1
12.2,42,8,Gold,135,30,3
12.3,45,8,Gold,150,25,2

太好了!接下来,让我们用这些数据创建一个新的气泡图,比较完成的总故事点,每个迭代中打开的 bug,以及每个发布的生产事件:

symbols(teamvelocity$TotalPoints, teamvelocity$BugsOpened, circles=teamvelocity$ProductionIncidents, inches=0.35, fg="#000000", bg="#CCCCCC", ylab="Bugs Opened", xlab="Velocity", main = "Velocity by Bugs Opened by Production Incidents Opened")

该代码创建如图 8-11 所示的图表。

img/313452_2_En_8_Fig11_HTML.jpg

图 8-11

团队速度和打开的 bug 的相关性,其中圆圈的大小表示生产事件的数量

从这个图表中,您可以看到,至少根据我们的样本数据,对于一个给定的 sprint,完成的总故事点、打开的 bug 和打开的生产事件之间存在正相关。

最后,现在所有的数据都被分层到平面文件中,我们可以创建一个散点图矩阵。这是所有列的矩阵,用散点图相互比较。我们可以使用散点图矩阵一次性查看所有数据,并快速挑选出数据集中可能存在的任何相关模式。我们可以只用图形包中的plot()函数或pairs()函数创建散点图矩阵:

plot(teamvelocity)
pairs(teamvelocity)

任何一种都会产生如图 8-12 所示的图表。

img/313452_2_En_8_Fig12_HTML.jpg

图 8-12

我们完整数据集的散点图矩阵

在图 8-12 中,每行代表数据框中的一列,每个散点图代表这些列的交叉点。当您浏览矩阵中的每个散点图时,您可以清楚地看到本章已经介绍过的组合中的相关模式。虽然这是一种有效的可视化,但同时查看如此多的变量,眼睛很容易疲劳。重要的是要考虑到,仅仅因为你可以把所有的事情都放在一个数字中,你可能就不想这样做了。您可以考虑将您的数据子集化到感兴趣的特定列,使这样的图形更容易浏览。

D3 中的交互式散点图

到目前为止,在本章中,我们已经创建了不同的散点图来表示我们想要查看的数据组合。但是,如果我们想要创建一个散点图,让我们能够选择轴所基于的数据点呢?借助 D3,我们可以做到这一点!

添加基本 HTML 和 JavaScript

让我们从包含d3.js的基本 HTML 结构以及基本 CSS 开始:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title>
<style>
body {font: 15px sans-serif;
}
.axis path{fill: none;stroke: #000;shape-rendering: crispEdges;
}
.dot {stroke: #000;
}
</style>
</head>
<body>
<script src="d3.v3.js"></script>
</body>
</html>

接下来让我们添加script标签来保存图表。就像前面的 D3 例子一样,包括起始变量、边距、x 和 y 范围对象,以及 x 和 y 轴对象:

<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},width = 960 - margin.left - margin.right,height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
</script>

让我们像前面的例子一样在页面上创建 SVG 标记:

var svg = d3.select("body").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

加载数据

现在我们需要使用d3.csv()函数加载数据。在所有之前的 D3 例子中,大部分工作都是在回调函数的范围内完成的,但是对于这个例子,我们需要公开我们的功能,这样我们就可以通过 form select元素来改变数据点。然而,我们仍然需要从回调函数驱动初始功能,因为那时我们将拥有我们的数据,所以我们将设置我们的回调函数来调用存根公共函数。

我们为从平面文件返回的数据设置一个名为chartData的公共变量,并调用两个名为removeDots()setChartDots()的函数:

d3.csv("teamvelocity.txt", function(error, data) {chartData = data;removeDots()setChartDots("TotalDevs", "TotalPoints")
});

注意,我们将"TotalDevs""TotalPoints"传递给了setChartDots()函数。这是为了启动泵,因为它们将是页面加载时我们显示的初始数据点。

添加交互式功能

现在我们需要真正创造出我们已经熄灭的东西。首先,让我们在script标签的根位置创建变量chartData,在这里我们设置其他变量:

var margin = {top: 20, right: 20, bottom: 30, left: 40},width = 960 - margin.left - margin.right,height = 500 - margin.top - margin.bottom,chartData;

接下来,我们创建removeDots()函数,该函数选择页面上的任何圆或轴并删除它们:

function removeDots(){svg.selectAll("circle").transition().duration(0).remove()svg.selectAll(".axis").transition().duration(0).remove()
}

最后,我们创建了setChartDots()功能。该函数接受两个参数:xvalyval。因为我们希望确保 D3 转换已经运行完毕,并且它们有一个 250 毫秒的默认运行时间,即使我们将持续时间设置为 0,我们也将在一个setTimeout()调用中包装函数的内容,所以我们在开始绘制图表之前等待 300 毫秒。如果我们不这样做,我们可能会进入一种竞争状态,在这种状态下,当过渡从屏幕上消失时,我们正在向屏幕上绘制。

function setChartDots(xval, yval){setTimeout(function() {}, 300);}

在该函数中,我们使用xvalyval参数设置 x 和 y 缩放对象的域。这些参数对应于我们将绘制图表的数据点的列名:

x.domain(d3.extent(chartData, function(d) { return d[xval];}));
y.domain(d3.extent(chartData, function(d) { return d[yval];}));

接下来,我们将圆绘制到屏幕上,使用全局变量chartData来填充它,并将传入的列数据作为圆的 x 和 y 坐标。我们还在这个函数中增加了坐标轴,这样每次坐标轴改变时,我们都会重新绘制值。

svg.selectAll(".dot").data(chartData).enter().append("circle").attr("class", "dot").attr("r", 3).attr("cx", function(d) { return x(d[xval]);}).attr("cy", function(d) { return y(d[yval]);}).style("fill", "#CCCCCC");
svg.append("g").attr("class", "axis").attr("transform", "translate(0," + height + ")").call(xAxis)svg.append("g").attr("class", "axis").call(yAxis)

完整的函数应该如下所示:

function setChartDots(xval, yval){setTimeout(function() {x.domain(d3.extent(chartData, function(d) { return d[xval];}));y.domain(d3.extent(chartData, function(d) { return d[yval];}));svg.selectAll(".dot").data(chartData).enter().append("circle").attr("class", "dot").attr("r", 3).attr("cx", function(d) { return x(d[xval]);}).attr("cy", function(d) { return y(d[yval]);}).style("fill", "#CCCCCC");svg.append("g").attr("class", "axis").attr("transform", "translate(0," + height + ")").call(xAxis)svg.append("g").attr("class", "axis").call(yAxis)}, 300);
}

太棒了。

添加表单域

接下来让我们添加表单字段。我们将添加两个select元素,其中每个option对应于平面文件中的一列。这些元素调用一个 JavaScript 函数,getFormData(),我们将很快定义它:

<form>Y-Axis:<select id="yval" onChange="getFormData()"><option value="TotalPoints">Total Points</option><option value="TotalDevs">Total Devs</option><option value="Team">Team</option><option value="BugsOpened">Bugs Opened</option><option value="ProductionIncidents">Production Incidents</option></select>X-Axis:<select id="xval" onChange="getFormData()"><option value="TotalPoints">Total Points</option><option value="TotalDevs">Total Devs</option><option value="Team">Team</option><option value="BugsOpened">Bugs Opened</option><option value="ProductionIncidents">Production Incidents</option></select>
</form>

正在检索表单数据

剩下的最后一点功能是编写getFormData()函数。这个函数从两个select元素中提取选择的选项,并使用这些值传递给setChartDots()——当然是在调用removeDots()之后。

function getFormData(){var xEl = document.getElementById("xval")var yEl = document.getElementById("yval")var x = xEl.options[xEl.selectedIndex].valuevar y = yEl.options[yEl.selectedIndex].valueremoveDots()setChartDots(x,y)
}

太好了!

使用可视化

完整的源代码应该如下所示:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title>
<style>
body {font: 10px sans-serif;
}
.axis path,
.axis line {fill: none;stroke: #000;shape-rendering: crispEdges;
}
.dot {stroke: #000;
}
</style>
</head>
<body><form>Y-Axis:<select id="yval" onChange="getFormData()"><option value="TotalPoints">Total Points</option><option value="TotalDevs">Total Devs</option><option value="Team">Team</option><option value="BugsOpened">Bugs Opened</option><option value="ProductionIncidents">Production Incidents</option></select>X-Axis:<select id="xval" onChange="getFormData()"><option value="TotalPoints">Total Points</option><option value="TotalDevs">Total Devs</option><option value="Team">Team</option><option value="BugsOpened">Bugs Opened</option><option value="ProductionIncidents">Production Incidents</option></select></form>
<script src="d3.v3.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},width = 960 - margin.left - margin.right,height = 500 - margin.top - margin.bottom,chartData;
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
var svg = d3.select("body").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + height + ")").call(xAxis)svg.append("g").attr("class", "y axis").call(yAxis)function getFormData(){var xEl = document.getElementById("xval")var yEl = document.getElementById("yval")var x = xEl.options[xEl.selectedIndex].valuevar y = yEl.options[yEl.selectedIndex].valueremoveDots()setChartDots(x,y)}function removeDots(){svg.selectAll("circle").transition().duration(0).remove()svg.selectAll(".axis").transition().duration(0).remove()}function setChartDots(xval, yval){setTimeout(function() {x.domain(d3.extent(chartData, function(d) { return d[xval];}));y.domain(d3.extent(chartData, function(d) { return d[yval];}));svg.selectAll(".dot").data(chartData).enter().append("circle").attr("class", "dot").attr("r", 3).attr("cx", function(d) { return x(d[xval]);}).attr("cy", function(d) { return y(d[yval]);}).style("fill", "#CCCCCC");svg.append("g").attr("class", "axis").attr("transform", "translate(0," + height + ")").call(xAxis)svg.append("g").attr("class", "axis").call(yAxis)}, 300);}
d3.csv("teamvelocity.txt", function(error, data) {chartData = data;removeDots()setChartDots("TotalDevs", "TotalPoints")
});
</script>
</body>
</html>

它应该创建如图 8-13 所示的交互式可视化。

img/313452_2_En_8_Fig13_HTML.jpg

图 8-13

使用 D3 的交互式散点图

摘要

这一章着眼于团队行动的速度与漏洞和生产问题的出现之间的相互关系。这些数据点之间自然存在正相关关系:当我们制造新的东西时,我们为那些新的东西和现有的东西创造了新的机会去打破。

当然,这并不意味着我们应该停止制造新的东西,即使出于某种原因,我们的业务部门和我们的行业允许这样做。这意味着我们需要在创造新事物和培育及维护现有事物之间找到平衡。这正是我们将在下一章看到的。

九、使用平行坐标可视化交付和质量的平衡

最后一章着眼于使用散点图来确定数据集之间的关系。它讨论了数据集之间可能存在的不同类型的关系,例如正相关和负相关。我们在团队动力学的前提下表达了这个想法:你看到团队中的人数和团队能够完成的工作量之间,或者完成的工作量和产生的缺陷数量之间有任何关联吗?

在这一章中,我们将一直在谈论的关键概念联系在一起:可视化、团队特征工作、缺陷和生产事件。我们将使用名为平行坐标的数据可视化将它们联系在一起,以显示这些工作之间的平衡。

什么是平行坐标图?

平行坐标图是由 N 个垂直轴组成的可视化图,每个轴代表一个唯一的数据集,并在轴上绘制线条。线条显示了轴之间的关系,很像散点图,线条形成的图案表明了这种关系。当我们看到一簇线时,我们还可以收集关于轴之间关系的细节。让我们以图 9-1 中的图表为例来看看这一点。

img/313452_2_En_9_Fig1_HTML.jpg

图 9-1

安全带数据集的平行坐标

我从 r 内置的数据集Seatbelts构建了图 9-1 中的图表。安全带在 R 命令行。我提取了可用列的子集,以便更好地突出显示数据中的关系:

cardeaths <- data.frame(Seatbelts[,1], Seatbelts[,5], Seatbelts[,6], Seatbelts[,8])
colnames(cardeaths) <- c("DriversKilled", "DistanceDriven", "PriceofGas", "SeatbeltLaw")

该数据集代表了强制系安全带前后英国死于车祸的司机人数。坐标轴代表死亡司机的数量,行驶的距离,当时的汽油成本,以及是否有安全带法。

查看平行坐标有许多有用的方法。如果我们观察一对轴之间的连线,我们可以看到这些数据集之间的关系。例如,如果我们观察汽油价格和安全带法律之间的关系,我们可以看到,当安全带法律存在时,汽油价格受到非常严格的约束,但当安全带法律不存在时,汽油价格涵盖了很大范围的价格(即,许多不同的线汇聚在代表法律之前的时间的点上,一条窄带线汇聚在法律通过后的时间上)。这种关系可能意味着许多不同的事情,但因为我知道这些数据,我知道这是因为在法律实施后,我们的死亡样本量要小得多:在安全带法律实施前有 14 年的数据,但在安全带法律实施后只有 2 年的数据。

我们还可以沿着所有轴追踪线条,看看每个轴是如何关联的。对于所有颜色相同的线条,这是很难做到的,但是当我们改变线条的颜色和阴影时,我们可以更容易地看到图表上的图案。让我们拿现有的图表,给线条分配颜色(结果如图 9-2 所示;此外,如果您还没有软件包,您需要安装它):

img/313452_2_En_9_Fig2_HTML.jpg

图 9-2

安全带数据集的平行坐标,每条线有不同的灰色阴影

library(MASS)
parcoord(cardeaths, col=rainbow(length(cardeaths[,1])), var.label=TRUE)

Note

您需要导入批量库来使用parcoord()功能。

图 9-2 开始显示数据中存在的模式。死亡人数最少的线路行驶的距离也最长,并且主要落在安全带法颁布后的时间点。同样,请注意,安全带后法律的样本量确实比安全带前法律的样本量小得多,但您可以看到,能够追踪这些数据点的相互联系是多么有用和有意义。

平行坐标图的历史

在纵轴上使用平行坐标的想法是 1885 年由 Maurice d'Ocagne 发明的,当时他创建了诺模图和诺模图领域。诺模图是根据数学规则计算数值的工具。至今仍在使用的诺谟图的经典例子是温度计上同时显示华氏温度和摄氏温度的线条。或者想想尺子,一边以英寸显示数值,另一边以厘米显示数值。

Note

罗恩·多弗勒写了一篇关于诺模图的长篇论文,请点击这里: http://myreckonings.com/wordpress/2008/01/09/the-art-of-nomography-i-geometric-design/ 。Doerfler 还主持了一个名为 modern nomograms(www.myreckonings.com/modernnomograms/))的网站,该网站“提供专为当今应用而设计的引人注目且有用的图形计算器”

你可以在图 9-3 和 9-4 中看到现代诺谟图的例子,由罗恩·多弗勒提供。

img/313452_2_En_9_Fig4_HTML.jpg

图 9-4

罗恩·多弗勒、叶小开·罗舍尔和乔·马拉斯科提供的曲线比例尺诺谟图

img/313452_2_En_9_Fig3_HTML.jpg

图 9-3

展示函数 S、P、R 和 T 之间数值转换的诺模图,这是顺序概率比测试的基础

Note

术语平行坐标及其代表的概念是阿尔弗雷德·因塞尔伯格在伊利诺伊大学学习期间推广并重新发现的。Inselberg 博士目前是特拉维夫大学的教授和圣地亚哥超级计算中心的高级研究员。Inselberg 博士还出版了一本关于这个主题的书,平行坐标:视觉多维几何及其应用 (Springer,2009)。他还发表了一篇关于如何有效阅读和使用平行坐标的论文,题目是“多维侦探”,可从 IEEE 获得。

寻找平衡

我们知道平行坐标用于可视化多个变量之间的关系,但这如何应用于我们在本书中迄今为止一直在谈论的内容呢?到目前为止,我们讨论了量化和可视化缺陷积压、生产事件的来源,甚至我们团队承诺的工作量。可以说,平衡产品开发的这些方面可能是团队所做的最具挑战性的活动之一。

在每一次正式或非正式的迭代中,团队成员都必须决定他们应该在每一个关注点上投入多少精力:开发新功能、修复现有功能的缺陷,以及根据用户的直接反馈解决生产事件。这些只是每个产品团队必须处理的细微差别的一个例子;他们可能还必须考虑花费在技术债务或更新基础设施上的时间。

我们可以使用平行坐标来形象化这种平衡,既可以作为文档,也可以作为开始新的 sprints 时的分析工具。

创建平行坐标图

创建平行坐标图有几种不同的方法。使用前一章的数据,我们可以查看每次迭代的运行总数。回想一下,数据是每个迭代中提交的点的总数,以及每个团队的 backlog 中有多少 bug 和产品事件,在迭代中有多少新 bug,以及团队中有多少成员。数据看起来很像这样:

  Sprint TotalPoints TotalDevs Team   BugBacklog BugsOpened ProductionIncidents1      12.10       25        6 Gold 125        10         12      12.20       42        8 Gold 135        30         33      12.30       45        8 Gold 150        25         24      12.40       43        8 Gold 149        23         35      12.50       32        6 Gold 155        24         16      12.60       43        8 Gold 140        22         47      12.70       35        7 Gold 132         9         1
...

为了利用这些数据,我们可以把它读入 R,就像我们在上一章所做的那样:

tvFile <- "/Applications/MAMP/htdocs/teamvelocity.txt"
teamvelocity <- read.table(tvFile, sep=",", header=TRUE)

然后,我们可以创建一个新的数据框,其中包含来自teamvelocity变量的所有列,除了Team列。该列是一个字符串,如果我们在传递给它的对象中包含字符串,我们在本例中使用的 R parcoord()函数就会抛出一个错误。团队信息在这种情况下也没有意义。图表中的线条将代表我们的团队:

t<- data.frame(teamvelocity$Sprint, teamvelocity$TotalPoints, teamvelocity$TotalDevs, teamvelocity$BugBacklog, teamvelocity$BugsOpened, teamvelocity$ProductionIncidents)
colnames(t) <- c("sprint", "points", "devs", "total bugs", "new bugs", "prod incidents")

我们将新对象传递给parcoord()函数。我们还将rainbow()函数传递给color参数,并将var.label参数设置为true,以使每个轴的上下边界在图表上可见:

parcoord(t, col=rainbow(length(t[,1])), var.label=TRUE)

这段代码产生了如图 9-5 所示的可视化效果。

img/313452_2_En_9_Fig5_HTML.jpg

图 9-5

整个组织度量的不同方面的平行坐标图,包括每个迭代的承诺点、团队的总开发人员、总 bug backlog、新 bugs open 和生产事件

图 9-5 为我们呈现了一些有趣的故事。我们可以看到,在我们的数据集中,一些团队在承担更多分价值的工作时会产生更多的错误。其他团队有大量的 bug backlog,但在每次迭代中没有创建大量的新 bug,这意味着他们没有关闭他们打开的 bug。有些团队比其他团队更稳定。所有这些都包含团队可以用来反思和持续改进的见解。但最终这张图表是反应性的,围绕主要问题展开讨论。它告诉我们每个 sprint 对我们各自的积压工作的影响,包括 bug 和生产事件。它还告诉我们在每个 sprint 期间打开了多少个 bug。

图中没有显示的是针对每个积压工作所花费的工作量。为了说明这一点,我们需要做一些准备工作。

增加努力

在过去的章节中,我提到了 Greenhopper 和 Rally,它们是计划迭代、对积压工作进行优先级排序以及跟踪用户故事进展的方法。无论你选择哪种产品,它都应该提供某种方式来用元数据对你的用户故事进行分类或标记。不需要您的软件支持就可以完成这种分类的一些非常简单的方法包括:

img/313452_2_En_9_Fig6_HTML.jpg

图 9-6

按类别、缺陷、特性或产品事件标记的用户故事(由 Rally 提供)

  • 在每个用户故事的标题中添加标签(参见图 9-6 中的例子,看看在 Rally 中会是什么样子)。使用这种方法,您需要手动地或者以编程的方式合计每个类别的工作量。

  • 为每个工作描述嵌套子项目。

无论您如何着手创建这些存储桶,您都应该有一种方法来跟踪您的类别在每个 sprint 期间所花费的工作量。为了直观显示这一点,只需将其从您最喜欢的工具中导出到一个平面文件中,类似于下面所示的结构:

iteration,defect,prodincidents,features,techdebt,innovation
13.1,6,3,13,2,1
13.2,8,1,7,2,1
13.3,10,1,9,3,2
13.5,9,2,18,10,3
13.6,7,5,19,8,3
13.7,9,5,21,12,3
13.8,6,7,11,14,3
13.9,8,3,16,18,3
13.10,7,4,15,5,3

为了开始使用这些数据,我们需要将平面文件的内容导入到 r 中。我们将数据存储在一个名为teamEffort and的变量中,并将teamEffort传递给parcoord()函数:

teFile <- "/Applications/MAMP/htdocs/teamEffort.txt"
teamEffort <- read.table(teFile, sep=",", header=TRUE)
parcoord(teamEffort, col=rainbow(length(teamEffort[,1])), var.label=TRUE, main="Level of Effort Spent")

该代码生成如图 9-7 所示的图表。

img/313452_2_En_9_Fig7_HTML.jpg

图 9-7

每个计划所花费的努力水平的平行坐标图

这个图表不是关于数据隐含的关系,而是关于每个 sprint 的明确的努力程度。在真空中,这些数据点是没有意义的,但是当您查看这两个图表并比较总的 bug 积压和总的生产事件时,与解决其中任何一个所花费的努力水平相比,您开始看到团队需要解决的盲点。盲点可能是有大量 bug 积压或生产事件计数的团队没有花费足够的精力来解决这些积压。

用 D3 刷平行坐标图

阅读密集平行坐标图的诀窍是使用一种称为刷的技术。笔刷会淡化图表上所有线条的颜色或不透明度,除了您想要沿坐标轴描摹的线条。我们可以使用 D3 实现这种程度的交互性。

创建基础结构

让我们首先使用我们的基本 HTML 框架结构创建一个新的 HTML 文件:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title>
</head>
<body>
<script src="d3.v3.js"></script>
</body>
</html>

然后我们创建一个新的script标签来保存图表的 JavaScript。在这个标记中,我们首先创建设置图表的高度和宽度所需的变量、保存边距值的对象、轴列名的数组以及 x 对象的 scale 对象。

我们还创建了引用 D3 SVG line 对象的变量,一个对 D3 轴的引用,以及一个名为foreground的变量来保存所有路径的分组,这些路径将是图表中轴之间绘制的线:

<script>
var margin = {top: 80, right: 160, bottom: 200, left: 160},width = 1280 - margin.left - margin.right,height = 800 - margin.top - margin.bottom,cols = ["iteration","defect","prodincidents","features","techdebt","innovation"]
var x = d3.scale.ordinal().domain(cols).rangePoints([0, width]),y = {};
var line = d3.svg.line(),axis = d3.svg.axis().orient("left"),foreground;
</script>

我们将 SVG 元素绘制到页面上,并将其存储在一个名为 svg 的变量中:

var svg = d3.select("body").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
We use d3.csv to load in the teameffort.txt flat file:
d3.csv("teameffort.txt", function(error, data) {
}

到目前为止,我们遵循与前几章相同的格式:在顶部布置变量,创建 SVG 元素,并加载数据;大多数数据相关的逻辑发生在匿名函数中,该函数在数据加载后触发。

对于平行坐标,这个过程在这里稍有改变,因为我们需要为数据中的每一列创建 y 轴。

为每列创建一个 Y 轴

要为每一列创建 y 轴,我们必须遍历保存列名的数组,将每一列的内容显式转换为数字,在y变量中为每一列创建一个索引,并为每一列创建一个 D3 scale对象:

cols.forEach(function(d) {//convert to numbersdata.forEach(function(p) { p[d] = +p[d]; });//create y scale for each columny[d] = d3.scale.linear().domain(d3.extent(data, function(p) { return p[d]; })).range([height, 0]);
});

划清界限

我们需要画出穿过每个轴的线,所以我们创建一个 SVG 分组来聚集和保存所有的线。我们将foreground类分配给分组(这样做很重要,因为我们将通过 CSS 处理刷行):

foreground = svg.append("g").attr("class", "foreground")

我们将 SVG 路径附加到这个分组中。我们将数据附加到路径上,将路径的颜色设置为随机生成的颜色,并找出mouseovermouseout事件处理程序。我们还将路径的d属性设置为我们将要创建的函数path()

我们一会儿将回到这些事件处理程序。

foreground = svg.append("g").attr("class", "foreground").selectAll("path").data(data).enter().append("path")
.attr("stroke", function(){return "#" + Math.floor(Math.random()*16777215).toString(16);}).attr("d", path).attr("width", 16).on("mouseover", function(d){}).on("mouseout", function(d){})

让我们来充实一下path()函数。在这个函数中,我们接受一个名为d的参数,它将是data变量的索引。该函数返回带有 x 和 y 刻度的路径坐标映射。

function path(d) {return line(cols.map(function(p) { return x(p), y[p]; }));
}

path()函数返回如下所示的数据——一个多维数组,每个索引和数组由两个坐标值组成:

[[0, 520], [192, 297.14285714285717], [384, 346.6666666666667], [576, 312], [768, 491.1111111111111], [960, 520]]

淡化线条

让我们退一步想想。为了处理笔刷,我们需要创建一个样式规则来淡化线条的不透明度。所以让我们回到页面的head部分,创建一个style标签和一些样式规则。

我们将path.fade设置为选择器,并将笔画不透明度设置为 4%。同时,我们还设置了正文字体样式和路径样式。

<style>
body {font: 15px sans-serif;font-weight:normal;
}
path{fill: none;shape-rendering: geometricPrecision;stroke-width:1;
}
path.fade {stroke: #000;stroke-opacity: .04;
}
</style>

让我们回到 stubbed out 事件处理程序。D3 提供了一个名为classed()的函数,允许我们将类添加到选择中。在mouseover处理程序中,我们使用classed()函数将刚刚创建的fade样式应用于前景中的每条路径。它会淡出每一行。接下来,我们将针对当前选择,使用d3.select(this)classed()来关闭淡入淡出样式。

mouseout处理程序中,我们关闭了fade样式:

foreground = svg.append("g").attr("class", "foreground").selectAll("path").data(data).enter().append("path").attr("stroke", function(){return "#" + Math.floor(Math.random()*16777215).toString(16);}).attr("d", path).attr("width", 16).on("mouseover", function(d){foreground.classed("fade",true)d3.select(this).classed("fade", false)}).on("mouseout", function(d){foreground.classed("fade",false)})

创建轴

最后,我们需要创建轴:

var g = svg.selectAll(".column").data(cols).enter().append("svg:g").attr("class", "column").attr("stroke", "#000000").attr("transform", function(d) { return "translate(" + x(d) + ")"; })// Add an axis and title.g.append("g").attr("class", "axis").each(function(d) { d3.select(this).call(axis.scale(y[d])); }).append("svg:text").attr("text-anchor", "middle").attr("y", -19).text(String);

我们的完整代码如下:

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title></title>
<style>
body {font: 15px sans-serif;font-weight:normal;
}
path{fill: none;shape-rendering: geometricPrecision;stroke-width:1;
}
path.fade {stroke: #000;stroke-opacity: .04;
}
</style>
</head>
<body>
<script src="d3.v3.js"></script>
<script>
var margin = {top: 80, right: 160, bottom: 200, left: 160},width = 1280 - margin.left - margin.right,height = 800 - margin.top - margin.bottom,cols = ["iteration","defect","prodincidents","features","techdebt","innovation"]
var x = d3.scale.ordinal().domain(cols).rangePoints([0, width]),y = {};
var line = d3.svg.line(),axis = d3.svg.axis().orient("left"),foreground;
var svg = d3.select("body").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("teameffort.txt", function(error, data) {cols.forEach(function(d) {//convert to numbersdata.forEach(function(p) { p[d] = +p[d]; });y[d] = d3.scale.linear().domain(d3.extent(data, function(p) { return p[d]; })).range([height, 0]);});foreground = svg.append("g").attr("class", "foreground").selectAll("path").data(data).enter().append("path").attr("stroke", function(){return "#" + Math.floor(Math.random()*16777215).toString(16);}).attr("d", path).attr("width", 16).on("mouseover", function(d){foreground.classed("fade",true)d3.select(this).classed("fade", false)}).on("mouseout", function(d){foreground.classed("fade",false)})var g = svg.selectAll(".column").data(cols).enter().append("svg:g").attr("class", "column").attr("stroke", "#000000").attr("transform", function(d) { return "translate(" + x(d) + ")"; })// Add an axis and title.g.append("g").attr("class", "axis").each(function(d) { d3.select(this).call(axis.scale(y[d])); }).append("svg:text").attr("text-anchor", "middle").attr("y", -19).text(String);function path(d) {return line(cols.map(function(p) { return x(p), y[p]; }));}});
</script>
</body>
</html>

该代码生成如图 9-8 所示的图表。

img/313452_2_En_9_Fig8_HTML.jpg

图 9-8

在 D3 中创建的平行坐标图

如果我们将鼠标滑过任何线条,我们会看到如图 9-9 所示的笔刷效果,其中除了鼠标当前滑过的线条,所有线条的不透明度都会缩小。

img/313452_2_En_9_Fig9_HTML.jpg

图 9-9

交互式刷图平行坐标图

摘要

本章介绍了平行坐标图。你可以领略一下它们的历史——它们最初是如何以列线图的形式出现的,用来显示价值转换。您在可视化团队如何在迭代过程中平衡产品开发的不同方面的上下文中查看了它们的实际应用。

平行坐标是本书涵盖的最后一种可视化类型,但它远不是最后一种可视化类型。这本书远不是这个问题的最终结论。每学期结束时,我都会告诉我的学生,我希望他们能继续使用他们在我的课上学到的东西。只有通过使用所涵盖的语言或主题,通过不断地使用它,探索它,并测试它的边界,学生才会将这一新工具融入他们自己的技能组合中。否则,如果他们离开课堂(或者,在这种情况下,合上书)并且很长一段时间不思考这个主题,他们可能会忘记我们所学的大部分内容。

如果你是一名开发人员或技术领导者,我希望你读了这本书,并受到启发,开始跟踪自己的数据。这只是你可以追踪的一小部分东西。正如我的书Pro JavaScript Performance:Monitoring and Visualization中所述,您可以对代码进行检测以跟踪性能指标,或者您可以使用 Splunk 等工具来创建仪表板,以可视化使用数据和错误率。您可以直接进入源代码存储库数据库,获得诸如一周中哪些时间和哪些天有最多的提交活动之类的指标,以避免在这些时间安排会议。

所有这些数据跟踪的要点是自我完善——建立你目前所处位置的基线,并跟踪你想要达到的目标,不断完善你的技能,并在你所做的事情上表现出色。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/66994.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

P5661 [CSP-J2019] 公交换乘P2952 [USACO09OPEN] Cow Line S

自己写的第一个c++博客(因为懒得写两个,所以合成一篇写) [CSP-J2019] 公交换乘 题目描述 著名旅游城市 B 市为了鼓励大家采用公共交通方式出行,推出了一种地铁换乘公交车的优惠方案:在搭乘一次地铁后可以获得一张优惠票,有效期为 45 分钟,在有效期内可以消耗这张优惠票,…

关于实体机安装Ubuntu 22.04.3-desktop-amd64遇见的一些问题

安装准备:U启动盘,Ubuntu系统 插入启动盘,开启电脑选择启动项为U启动(我的电脑为F12)安装Ubuntu系统选启动盘启动后,出现Ubuntu的图标后直接黑屏,无法看到安装界面。 原因:linux内核要加载第三方显卡驱动nouveau驱动。 解决方法:在启动出现Ubuntu的图标后,在选择安装…

忘记帝国 CMS 密码怎么办?教你一招轻松重置

如果您无法通过“找回密码”功能重置密码,可以尝试手动重置密码。备份数据库:在开始任何操作之前,请先备份数据库,以防万一。连接数据库:使用数据库管理工具(如 phpMyAdmin)连接到帝国CMS的数据库。找到用户表:导航到用户表,通常是 phome_enewsuser。查找用户记录:在…

dedecms(织梦)网站安全防护设置

织梦CMS 是国内常用的免费开源管理系统之一,但由于其广泛使用,也存在许多已知的安全漏洞。为了提高织梦CMS网站的安全性,以下是一些有效的安全防护设置步骤: 1. 修改网站后台的访问路径修改后台路径:默认后台路径为 http://域名/dede/。 修改为更复杂的路径,例如 http://…

静态QQ登录代码学习

记录学习 @搬砖界泰斗这只小狐狸 的静态QQ登陆页面源码,了解静态登陆页面如何书写&&拓宽自己对css的理解 Q1:用css调节子级元素位置时什么时候调节margin,什么时候调节padding? A1:margin对外,padding对内 e.g.要实现一个这样的排版 有一个大大盒子fafather,里面…

帝国CMS后台登陆时错误_enewsloginfail

当你在迁移帝国CMS网站后,遇到后台登录时出现“Table phome.***_enewsloginfail doesnt exist”的错误时,通常是因为数据库没有正确恢复。以下是详细的解决步骤: 1. 检查数据库恢复情况登录数据库管理工具:使用 phpMyAdmin 或其他数据库管理工具登录到数据库。检查数据库表…

解决 DedeCMS 报错“Please set ‘request_order’”的问题

如果你使用的是虚拟主机,无法直接修改 php.ini 文件,可以通过修改 DedeCMS 的代码来解决这个问题。找到 common.inc.php 文件:打开织梦CMS安装目录下的 include/common.inc.php 文件。修改代码:使用文本编辑器打开 common.inc.php 文件。找到第 34 行:phpif (strtoupper(i…

织梦错误Please set ‘request_order’

当你在使用 DedeCMS 并遇到错误提示“DedeCMS Error: (PHP 5.3 and above) Please set ‘request_order’ ini value to include C,G and P (recommended: ‘CGP’) in php.ini, more…”时,可以通过以下两种方法来解决这个问题: 方法 1:修改 php.ini 文件找到 php.ini 文件…