Clojure 数据科学(一)

原文:annas-archive.org/md5/bd3fb3fcef82cc10763b55da27a85129

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

“统计思维有一天将像读写能力一样,成为高效公民所必需的。”
H. G. Wells
“我有一个很好的主题[统计学]可以写,但我深感我的文学能力不足,无法在不牺牲准确性和彻底性的情况下使其易于理解。”
Sir Francis Galton

在网络上搜索“数据科学韦恩图”会返回许多关于成为一名有效数据科学家所需技能的解读(看起来数据科学评论者特别喜欢韦恩图)。作者和数据科学家 Drew Conway 在 2010 年提出了原型图,将数据科学置于黑客技能、实质性专业知识(即学科领域理解)和数学统计知识的交集处。介于黑客技能和实质性专业知识之间——那些缺乏扎实数学和统计学知识的从业者——存在着“危险区”。

五年过去了,随着越来越多的开发者希望填补数据科学技能的短缺,统计学和数学教育比以往任何时候都更加重要,它能帮助开发者摆脱这一危险区域。因此,当 Packt 出版社邀请我为 Clojure 程序员编写一本适合的数据科学书籍时,我欣然同意。除了意识到此类书籍的必要性外,我还将其视为一个机会,来巩固我作为自己基于 Clojure 的数据分析公司 CTO 所学到的许多知识。最终结果是这本书,正是我希望在起步之前就能读到的书。

*《数据科学与 Clojure》*旨在成为一本远远超过 Clojure 程序员的统计学书籍。数据科学在众多领域的广泛传播,很大程度上得益于机器学习的巨大力量。在本书中,我将展示如何使用纯 Clojure 函数和第三方库来构建机器学习模型,解决回归、分类、聚类和推荐等主要任务。

对于数据科学家来说,可以扩展到非常大数据集的处理方法,所谓的“大数据”,尤为重要,因为它们可以揭示在较小样本中丢失的细微之处。本书展示了如何使用 Clojure 简洁地表达要在 Hadoop 和 Spark 分布式计算框架上运行的作业,并且通过使用专门的外部库和通用优化技术,来结合机器学习。

最重要的是,本书不仅旨在让读者了解如何执行特定类型的分析,更希望让你理解这些技术为何有效。除了提供实践知识(本书几乎每个概念都以可运行的示例呈现)外,我还希望解释理论,帮助你将一个原理应用到相关问题中。我希望这种方法能够帮助你在未来的各种情况中有效地应用统计思维,无论你是否决定从事数据科学职业。

本书内容

第一章,统计学,介绍了 Incanter,Clojure 的主要统计计算库,贯穿全书使用。通过引用来自英国和俄罗斯的选举数据,我们演示了如何使用汇总统计和统计分布的价值,同时展示了多种对比可视化。

第二章,推断,介绍了样本与总体、统计量与参数的区别。我们将假设检验作为一种正式的方法,确定在 A/B 测试网站设计的背景下差异是否显著。我们还讨论了样本偏差、效应大小以及多重检验问题的解决方案。

第三章,相关性,展示了我们如何发现变量之间的线性关系,并利用该关系预测某些变量在已知其他变量的情况下的值。我们实现了线性回归——一种机器学习算法——以预测奥林匹克游泳运动员的体重,基于他们的身高,并仅使用核心 Clojure 函数。随后,我们使用矩阵和更多数据来改进模型,提升其准确性。

第四章,分类,描述了如何实现几种不同类型的机器学习算法(逻辑回归、朴素贝叶斯、C4.5 和随机森林),以预测泰坦尼克号乘客的生存率。我们学习了适用于类别数据而非连续值的统计显著性检验方法,解释了在训练机器学习模型时可能遇到的各种问题,如偏差和过拟合,并演示了如何使用 clj-ml 机器学习库。

第五章,大数据,展示了 Clojure 如何利用各种规模计算机的并行能力,通过 reducers 库,并如何将这些技术扩展到 Hadoop 集群中的机器,结合 Tesser 和 Parkour。通过使用来自 IRS 的 ZIP 代码级别税收数据,我们展示了如何以可扩展的方式执行统计分析和机器学习。

第六章,聚类,展示了如何使用 Hadoop 和 Java 机器学习库 Mahout 识别具有相似主题的文本文件。我们描述了与文本处理相关的各种技术以及与聚类相关的更一般性概念。我们还介绍了 Parkour 的一些高级功能,帮助从 Hadoop 作业中获得最佳性能。

第七章, 推荐系统,介绍了处理推荐问题的多种不同方法。除了使用核心 Clojure 函数实现推荐系统外,我们还通过使用主成分分析和奇异值分解来解决降维问题,以及使用布隆过滤器和 MinHash 算法进行概率集压缩。最后,我们介绍了用于 Spark 分布式计算框架的 Sparkling 和 MLlib 库,并使用它们通过交替最小二乘法生成电影推荐。

第八章, 网络分析,展示了分析图结构数据的多种方法。我们使用 Loom 库演示了遍历方法,然后展示了如何使用 Spark 的 Glittering 和 GraphX 库来发现社交网络中的社区和影响者。

第九章, 时间序列,演示了如何拟合曲线以处理简单的时间序列数据。通过使用月度航空公司乘客数量数据,我们展示了如何通过训练自回归滑动平均模型来预测更复杂序列的未来值。我们通过实现一种称为最大似然估计的参数优化方法,并借助 Apache Commons Math 库来完成这一过程。

第十章, 可视化,展示了如何使用 Clojure 库 Quil 创建自定义可视化图表,以便绘制 Incanter 未提供的图表,并制作能清晰传达发现结果的吸引人图形,无论你的听众背景如何。

本书所需的工具

每章的代码都已作为项目在 GitHub 上发布,地址为 github.com/clojuredatascience。你可以从该网站下载示例代码的压缩包,或使用 Git 命令行工具进行克隆。所有书中的示例都可以按照第一章,统计学,中描述的方式使用 Leiningen 构建工具进行编译和运行。

本书假设你已经能够使用 Leiningen 编译和运行 Clojure 代码(leiningen.org/)。如果你还没有设置好,参考 Leiningen 网站进行配置。

此外,许多示例章节的代码使用了外部数据集。如果可能,这些数据集已与示例代码一起提供。如果无法提供,数据的下载说明已经包含在示例代码的 README 文件中。相关的 Bash 脚本也已与示例代码一起提供,用于自动化此过程。只要安装了 curl、wget、tar、gzip 和 unzip 等工具,Linux 和 OS X 用户可以按照相关章节的说明直接运行这些脚本。Windows 用户可能需要安装 Cygwin 等 Linux 模拟器(www.cygwin.com/)来运行这些脚本。

适合谁阅读

本书面向中级和高级 Clojure 程序员,旨在帮助他们建立统计学知识,应用机器学习算法,或使用 Hadoop 和 Spark 处理大量数据。许多有志成为数据科学家的读者也会从学习这些技能中受益,《数据科学的 Clojure》应按顺序从头到尾阅读。按此方式阅读的读者将发现,每一章都在前一章的基础上进一步展开。

如果你还不熟悉阅读 Clojure 代码,可能会觉得本书特别具有挑战性。幸运的是,现在有许多优秀的资源可以帮助学习 Clojure,我在这里并不重复这些资源的内容。写作时,《勇敢与真诚的 Clojure》(www.braveclojure.com/)是一个很棒的免费学习资源。请访问clojure.org/getting_started以获得适合新手的其他书籍和在线教程链接。

约定

本书中会使用多种文本样式,区分不同类型的信息。以下是这些样式的一些示例及其含义解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名将按如下方式显示:“每个示例都是cljds.ch1.examples命名空间中的一个函数,可以执行。”

代码块的格式如下:

(defmulti load-data identity)

(defmethod load-data :uk [_]
  (-> (io/resource "UK2010.xls")
      (str)
      (xls/read-xls)))

当我们希望特别提醒你注意代码块中的某个部分时,相关行或项目将以粗体显示:

    (q/fill (fill-fn x y))
    (q/rect x-pos y-pos x-scale y-scale))
 (q/save "heatmap.png"))]
    (q/sketch :setup setup :size size))

任何命令行输入或输出都将按以下格式书写:

lein run –e 1.1

新术语重要单词以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,显示为这样的格式:“每次按下新样本按钮时,都会生成一对来自指数分布的新样本,样本的总体均值从滑块中获取。”

注意

警告或重要说明通常会以框体形式出现。

提示

提示和技巧通常以这种形式出现。

读者反馈

我们始终欢迎读者的反馈。让我们知道你对本书的看法——你喜欢或不喜欢的地方。读者反馈对我们非常重要,它帮助我们开发出你会真正受益的书籍。

要向我们发送一般反馈,只需通过电子邮件 <feedback@packtpub.com> 与我们联系,并在邮件主题中提到书名。

如果你在某个主题方面有专业知识并且有兴趣参与写作或贡献一本书,请查看我们的作者指南:www.packtpub.com/authors

客户支持

现在,既然你已成为一本 Packt 书籍的骄傲拥有者,我们为你提供了许多帮助,让你从购买中获得最大收益。

下载示例代码

你可以从你的账户中下载所有你购买的 Packt Publishing 书籍的示例代码文件,访问www.packtpub.com。如果你是在其他地方购买的本书,可以访问www.packtpub.com/support并注册以便将文件直接通过电子邮件发送给你。

下载本书的彩色图片

我们还为你提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图片。这些彩色图片将帮助你更好地理解输出结果中的变化。你可以从www.packtpub.com/sites/default/files/downloads/Clojure_for_Data_Science_ColorImages.pdf下载该文件。

勘误

尽管我们已尽最大努力确保内容的准确性,但仍然会出现错误。如果你在我们的书籍中发现错误——可能是文本或代码中的错误——我们非常感激你能向我们报告。通过这样做,你可以帮助其他读者避免沮丧,并帮助我们改进后续版本的书籍。如果你发现任何勘误,请访问www.packtpub.com/submit-errata提交报告,选择你的书籍,点击勘误提交表格链接,并填写勘误详情。一旦你的勘误经过验证,将被接受,并上传到我们的网站,或添加到该书标题下的勘误列表中。

要查看之前提交的勘误,访问www.packtpub.com/books/content/support并在搜索框中输入书名。所需的信息将在勘误部分显示。

盗版

互联网版权物品盗版问题在所有媒体上都在持续发生。在 Packt,我们非常重视版权和许可证的保护。如果你在互联网上发现我们作品的任何非法复制,请立即提供该位置地址或网站名称,以便我们采取相应措施。

请通过<copyright@packtpub.com>与我们联系,并附上涉嫌盗版材料的链接。

我们感谢你在保护作者权益和我们提供有价值内容方面的帮助。

问题

如果你对本书的任何部分有疑问,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章. 统计学

“投票的人决定不了什么,计算票数的人决定了一切。”
约瑟夫·斯大林

在接下来的十章中,我们将尝试在数据科学的 Clojure中探索一条大体线性的路径。事实上,我们会发现这条路径并不是那么线性,细心的读者应当注意到沿途会有许多反复出现的主题。

描述性统计学关注的是总结数字序列,它们将在本书的每一章中以某种程度出现。在本章中,我们将通过实现函数来计算数字序列的均值、中位数、方差和标准差,为后续内容奠定基础。在此过程中,我们将尽力消除对数学公式解释的恐惧。

一旦我们有多个数字需要分析,询问这些数字是如何分布的就变得有意义了。你可能已经听过像“长尾效应”和“80/20 法则”这样的表达。它们关注的是数字在一个范围内的分布情况。本章中,我们将展示分布的价值,并介绍最有用的分布:正态分布。

分布的研究得到了可视化的大力帮助,为此我们将使用 Clojure 库 Incanter。我们将展示如何使用 Incanter 加载、转换和可视化真实数据。我们将比较两次国家选举的结果——2010 年英国大选和 2011 年俄罗斯总统选举——并看看即使是基础分析也能提供潜在欺诈行为的证据。

下载示例代码

本书的所有示例代码都可以在 Packt 出版公司的官方网站www.packtpub.com/support或 GitHub 上github.com/clojuredatascience找到。每章的示例代码都有自己的仓库。

注意

第一章的示例代码,统计学可以从github.com/clojuredatascience/ch1-statistics下载。

可执行示例会定期出现在所有章节中,要么演示刚刚解释的代码效果,要么演示已引入的统计原理。所有示例函数的名称都以ex-开头,并且在每章中按顺序编号。所以,第一章的第一个可运行示例统计学名为ex-1-1,第二个名为ex-1-2,依此类推。

运行示例

每个示例是cljds.ch1.examples命名空间中的一个函数,可以通过两种方式运行——要么从REPL,要么通过Leiningen在命令行运行。如果你想在 REPL 中运行示例,可以执行:

lein repl

在命令行中,默认情况下,REPL 将在 examples 命名空间中打开。或者,如果你想运行某个特定的示例,可以执行:

lein run –-example 1.1

或者使用单个字母的等效命令:

lein run –e 1.1

本书假设你对基本的命令行操作有一定了解。只需能够运行 Leiningen 和 Shell 脚本即可。

提示

如果你在任何阶段遇到困难,请参考本书的维基:wiki.clojuredatascience.com。维基将提供已知问题的故障排除提示,包括在不同平台上运行示例的建议。

事实上,Shell 脚本仅用于自动从远程位置获取数据。本书的维基也会为不愿意或无法执行 Shell 脚本的读者提供替代的说明。

下载数据

本章的数据集由维也纳医科大学的复杂系统研究小组提供。我们将进行的分析与他们的研究紧密相连,旨在确定全球各国全国选举中系统性选举舞弊的信号。

注意

如需了解更多关于研究的信息,以及下载其他数据集的链接,请访问本书的维基或研究小组的网站:www.complex-systems.meduniwien.ac.at/elections/election.html

在本书中,我们将使用大量数据集。在可能的情况下,我们已将数据与示例代码一起提供。如果由于数据量太大或许可限制无法提供数据,我们则提供了下载数据的脚本。

第一章,统计学就是这样的一章。如果你已经克隆了该章节的代码,并打算跟随示例进行操作,请通过在项目目录中的命令行执行以下命令来下载数据:

script/download-data.sh

脚本将会下载并解压样本数据到项目的数据目录中。

提示

如果你在运行下载脚本时遇到困难,或者希望按照手动说明进行操作,请访问本书的维基:wiki.clojuredatascience.com获取帮助。

我们将在下一节开始调查数据。

检查数据

在本章以及本书的许多其他章节中,我们将使用 Incanter 库(incanter.org/)来加载、处理和显示数据。

Incanter 是一套模块化的 Clojure 库,提供统计计算和可视化功能。它模仿了广受欢迎的数据分析环境 R,将 Clojure 的强大功能、交互式 REPL 和处理数据的强大抽象结合在一起。

Incanter 的每个模块专注于特定的功能领域。例如,incanter-stats包含一套相关函数,用于分析数据并生成摘要统计信息,而incanter-charts提供大量的可视化功能。incanter-core提供了最基本且通常有用的用于数据转换的函数。

每个模块可以单独包含在你的代码中。如果需要访问统计、图表和 Excel 功能,你可以在project.clj中包含以下内容:

  :dependencies [[incanter/incanter-core "1.5.5"]
                 [incanter/incanter-stats "1.5.5"]
                 [incanter/incanter-charts "1.5.5"]
                 [incanter/incanter-excel "1.5.5"]
                 ...]

如果你不介意包含比所需更多的库,你也可以直接包含完整的 Incanter 分发包:

:dependencies [[incanter/incanter "1.5.5"]
               ...]

Incanter 的核心概念是数据集——一个包含行和列的结构。如果你有关系型数据库的经验,可以将数据集视为一个表格。数据集中的每一列都有名称,数据集中的每一行都有与其他行相同数量的列。有几种方式可以将数据加载到 Incanter 数据集中,具体使用哪种方式取决于我们的数据存储方式:

  • 如果我们的数据是文本文件(CSV 或制表符分隔文件),我们可以使用incanter-io中的read-dataset函数。

  • 如果我们的数据是 Excel 文件(例如,.xls.xlsx文件),我们可以使用incanter-excel中的read-xls函数。

  • 对于任何其他数据源(外部数据库、网站等),只要我们能将数据转换成 Clojure 数据结构,就可以使用incanter-core中的dataset函数来创建数据集。

本章使用了 Excel 数据源,因此我们将使用read-xls函数。该函数需要一个必需的参数——要加载的文件——以及一个可选的关键字参数,用于指定工作表的编号或名称。我们所有的示例只有一个工作表,因此我们只需提供文件参数作为字符串:

(ns cljds.ch1.data
  (:require [clojure.java.io :as io]
            [incanter.core :as i]
            [incanter.excel :as xls]))

通常情况下,我们不会在示例代码中重复命名空间声明。这是为了简洁,并且因为所需的命名空间通常可以通过引用它们的符号推断出来。例如,在本书中,我们将始终把clojure.java.io称为io,将incanter.core称为I,将incanter.excel称为xls,无论何时使用它们。

在本章中,我们将加载多个数据源,因此我们在cljds.ch1.data命名空间中创建了一个名为load-data的多方法:

(defmulti load-data identity)

(defmethod load-data :uk [_]
  (-> (io/resource "UK2010.xls")
      (str)
      (xls/read-xls)))

在上面的代码中,我们定义了load-data多方法,它根据第一个参数的identity进行分派。我们还定义了当第一个参数为:uk时被调用的实现。因此,调用(load-data :uk)将返回一个包含英国数据的 Incanter 数据集。在本章后面,我们将为其他数据集定义额外的load-data实现。

UK2010.xls 电子表格的第一行包含列名。Incanter 的 read-xls 函数会将这些列名作为返回数据集的列名。让我们现在通过检查它们来开始探索数据——incanter.core 中的 col-names 函数将列名作为向量返回。在接下来的代码中(以及本书中,我们使用来自 incanter.core 命名空间的函数时),我们将其命名为 i

(defn ex-1-1 []
  (i/col-names (load-data :uk)))

如前所述,在运行示例时,前缀为ex-的函数可以像下面这样通过 Leiningen 在命令行上运行:

lein run –e 1.1

前述命令的输出应该是以下 Clojure 向量:

["Press Association Reference" "Constituency Name" "Region" "Election Year" "Electorate" "Votes" "AC" "AD" "AGS" "APNI" "APP" "AWL" "AWP" "BB" "BCP" "Bean" "Best" "BGPV" "BIB" "BIC" "Blue" "BNP" "BP Elvis" "C28" "Cam Soc" "CG" "Ch M" "Ch P" "CIP" "CITY" "CNPG" "Comm" "Comm L" "Con" "Cor D" "CPA" "CSP" "CTDP" "CURE" "D Lab" "D Nat" "DDP" "DUP" "ED" "EIP" "EPA" "FAWG" "FDP" "FFR" "Grn" "GSOT" "Hum" "ICHC" "IEAC" "IFED" "ILEU" "Impact" "Ind1" "Ind2" "Ind3" "Ind4" "Ind5" "IPT" "ISGB" "ISQM" "IUK" "IVH" "IZB" "JAC" "Joy" "JP" "Lab" "Land" "LD" "Lib" "Libert" "LIND" "LLPB" "LTT" "MACI" "MCP" "MEDI" "MEP" "MIF" "MK" "MPEA" "MRLP" "MRP" "Nat Lib" "NCDV" "ND" "New" "NF" "NFP" "NICF" "Nobody" "NSPS" "PBP" "PC" "Pirate" "PNDP" "Poet" "PPBF" "PPE" "PPNV" "Reform" "Respect" "Rest" "RRG" "RTBP" "SACL" "Sci" "SDLP" "SEP" "SF" "SIG" "SJP" "SKGP" "SMA" "SMRA" "SNP" "Soc" "Soc Alt" "Soc Dem" "Soc Lab" "South" "Speaker" "SSP" "TF" "TOC" "Trust" "TUSC" "TUV" "UCUNF" "UKIP" "UPS" "UV" "VCCA" "Vote" "Wessex Reg" "WRP" "You" "Youth" "YRDPL"]

这是一个非常宽的数据集。数据文件中的前六列描述如下;后续列按党派细分投票数:

  • 新闻社参考:这是一个识别选区(投票区,由一名议员代表)的数字

  • 选区名称:这是给投票区(选区)起的常用名称

  • 区域:这是选区所在的英国地理区域

  • 选举年份:这是选举举行的年份

  • 选民:这是选区内有资格投票的总人数

  • 投票数:这是总投票数

每当我们面对新数据时,理解数据是非常重要的。如果没有详细的数据定义,我们可以通过验证自己对数据的假设来开始理解它。例如,我们预期这个数据集包含关于 2010 年选举的信息,那么让我们先回顾一下 选举年份 列的内容。

Incanter 提供了 i/$ 函数(i,如前所述,表示 incanter.core 命名空间)用于从数据集中选择列。我们将在本章中经常遇到这个函数——它是 Incanter 从各种数据表示中选择列的主要方式,并且提供了多个不同的重载。目前,我们只需要提供我们想提取的列名和要提取的 dataset:

(defn ex-1-2 []
  (i/$ "Election Year" (load-data :uk)))

;; (2010.0 2010.0 2010.0 2010.0 2010.0 ... 2010.0 2010.0 nil)

这些年份作为一个单一的值序列返回。由于数据集包含很多行,输出可能难以理解。为了知道列中包含哪些唯一值,我们可以使用 Clojure 的核心函数 distinct。使用 Incanter 的一个优点是它的有用数据操作函数增强了 Clojure 已经提供的函数,正如下面的示例所示:

(defn ex-1-3 []
  (->> (load-data :uk)
       (i/$ "Election Year")
       (distinct)))

;; (2010 nil)

2010 年份在很大程度上确认了我们的预期——这些数据来自 2010。然而,nil 值则出乎意料,可能表明数据存在问题。

我们目前还不知道数据集中有多少个 nil 值,确定这一点可能帮助我们决定接下来该做什么。计数这类值的一个简单方法是使用核心库函数 frequencies,它返回一个值与计数的映射:

(defn ex-1-4 [ ]
  (->> (load-data :uk)
       (i/$ "Election Year")
       (frequencies)))

;; {2010.0 650 nil 1}

在前面的示例中,我们使用了 Clojure 的线程最后宏 ->> 来将多个函数连接在一起,提升可读性。

提示

除了 Clojure 大量的核心数据处理函数外,像前面讨论的宏——包括线程最后宏 ->>——也是使用 Clojure 进行数据分析的另一个重要原因。在本书中,我们将看到 Clojure 如何使即使是复杂的分析也能简洁且易于理解。

我们很快就能确认,2010 年英国有 650 个选区,称为选区。像这样的领域知识在对新数据进行合理性检查时非常宝贵。因此,nil 值很可能是多余的,可以删除。我们将在下一节看到如何做这件事。

数据清洗

有一个常见的统计数据表明,数据科学家至少 80% 的工作是数据清洗。这是检测潜在的损坏或错误数据,并进行修正或过滤的过程。

注意

数据清洗是处理数据时最重要(也是最耗时)的步骤之一。它是确保后续分析基于有效、准确且一致的数据进行的关键步骤。

选举年列末尾的 nil 值可能表示需要清除的脏数据。我们已经看到,通过 Incanter 的 i/$ 函数可以过滤 数据。要过滤 数据,我们可以使用 Incanter 的 i/query-dataset 函数。

我们通过传递一个包含列名和谓词的 Clojure 映射,让 Incanter 知道我们希望过滤哪些行。只有所有谓词返回 true 的行才会被保留。例如,要从数据集中仅选择 nil 值:

(-> (load-data :uk)
    (i/query-dataset {"Election Year" {:$eq nil}}))

如果你了解 SQL,你会发现这与 WHERE 子句非常相似。事实上,Incanter 还提供了 i/$where 函数,这是 i/query-dataset 的别名,反转了参数的顺序。

查询是一个将列名映射到谓词的映射,每个谓词本身是一个操作符到操作数的映射。可以通过指定多个列和多个操作符来构建复杂的查询。查询操作符包括:

  • :$gt 大于

  • :$lt 小于

  • :$gte 大于或等于

  • :$lte 小于或等于

  • :$eq 等于

  • :$ne 不等于

  • :$in 用于测试是否为某集合的成员

  • :$nin 用于测试是否不为某集合的成员

  • :$fn 一个谓词函数,应该返回 true 以保留该行

如果内置操作符不足以满足需求,最后一个操作符提供了传递自定义函数的能力。

我们将继续使用 Clojure 的线程最后宏(thread-last macro)来使代码的意图更清晰,并使用 i/to-map 函数将行返回为键值对映射:

(defn ex-1-5 []
  (->> (load-data :uk)
       (i/$where {"Election Year" {:$eq nil}})
       (i/to-map)))

;; {:ILEU nil, :TUSC nil, :Vote nil ... :IVH nil, :FFR nil}

仔细查看结果,很明显这一行中所有(除了一个)列的值都是nil。事实上,经过进一步的探索,确认非nil行是一个汇总总数,应该从数据中删除。我们可以通过更新谓词映射,使用:$ne操作符来删除有问题的行,只返回选举年份不等于nil的行:

(->> (load-data :uk)
      (i/$where {"Election Year" {:$ne nil}}))

上述函数是我们几乎总是希望在使用数据之前调用的。实现这一点的一种方式是添加另一个load-data多方法的实现,其中也包括此过滤步骤:

(defmethod load-data :uk-scrubbed [_]
  (->> (load-data :uk)
       (i/$where {"Election Year" {:$ne nil}})))

现在,无论我们写什么代码,都可以选择引用:uk:uk-scrubbed数据集。

通过始终加载源文件并在其上执行数据清洗,我们保留了我们所做转换的审计记录。这使我们——以及未来的代码读者——能够清楚地了解对源数据做了哪些调整。它还意味着,如果我们需要使用新的源数据重新运行分析,我们可能只需将新文件加载到现有文件的位置。

描述性统计

描述性统计是用来总结和描述数据的数字。在下一章,我们将关注更复杂的分析方法——所谓的推论统计,但现在我们只限于简单地描述文件中数据的观察内容。

为了演示我们的意思,我们来看一下数据中的Electorate列。该列列出了每个选区注册选民的总数:

(defn ex-1-6 []
  (->> (load-data :uk-scrubbed)
       (i/$ "Electorate")
       (count)))

;; 650

我们已经从数据集中过滤掉了nil字段;上述代码应该返回一个包含650个数字的列表,代表每个英国选区的选民。

描述性统计,也叫做汇总统计,是衡量数值序列特征的方式。它们有助于表征序列,并可以作为进一步分析的指导。让我们从计算数值序列中最基本的两个统计量开始——均值和方差。

均值

测量数据集平均值最常见的方法是均值。它实际上是衡量数据集中趋势的几种方法之一。均值,或者更准确地说,是算术均值,是一种直接的计算方法——简单地将数值加起来并除以数量——但尽管如此,它的数学符号看起来还是让人有些畏惧:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_01.jpg

其中https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_02.jpg被读作x-bar,是常用于表示均值的数学符号。

对于从数学或科学以外领域进入数据科学的程序员来说,这种符号可能会让人感到困惑和陌生。其他人可能对这种符号完全熟悉,他们可以放心跳过下一节。

解释数学符号

尽管数学符号看起来可能晦涩难懂,实际上只有少数几个符号会在本书的公式中频繁出现。

Σ 读作 sigma,意思是 。当你看到它出现在数学符号中时,意味着一个序列正在被求和。位于 sigma 上下的符号表示我们将要进行求和的范围。它们类似于 C 风格的 for 循环,在之前的公式中,表示我们会从 i=1i=n 进行求和。按照惯例,n 是序列的长度,且数学符号中的序列是从 1 开始索引的,而不是从 0 开始,因此从 1n 求和意味着我们在求整个序列的和。

紧跟着 sigma 的表达式是要被求和的序列。在我们之前的平均数公式中,x[i] 紧跟在 sigma 后面。由于 i 将表示从 1n 的每个索引,x[i] 代表 xs 序列中的每个元素。

最后,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_03.jpg 出现在 sigma 之前,表示整个表达式应乘以 1 除以 n(也称为 n 的倒数)。这可以简化为只除以 n

名称数学符号Clojure 对应
n(count xs)
Sigma 符号https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_04.jpg(reduce + xs)
Pi 符号https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_05.jpg(reduce * xs)

将这一切结合起来,我们得出“将序列中的元素从第一个加到最后一个,然后除以元素的数量”。在 Clojure 中,这可以写成:

(defn mean [xs]
  (/ (reduce + xs)
     (count xs)))

其中,xs 代表“xs 序列”。我们可以使用新的 mean 函数来计算英国选民的平均数:

(defn ex-1-7 []
  (->> (load-data :uk-scrubbed)
       (i/$ "Electorate")
       (mean)))

;; 70149.94

实际上,Incanter 已经在 incanter.stats 命名空间中包含了一个非常高效的计算序列平均数的函数 mean。在本章(以及全书)中,任何使用 incanter.stats 命名空间的地方都会用 s 作为简写。

中位数

中位数是另一种常见的描述性统计量,用于衡量序列的集中趋势。如果将所有数据从低到高排序,中位数就是中间的值。如果序列中的数据点数量是偶数,中位数通常定义为中间两个值的平均数。

中位数通常用 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_06.jpg 表示,发音为 x-tilde。这是数学符号中的一个不足之处,因为没有特别标准的方式来表示中位数公式,但在 Clojure 中仍然相当直接:

(defn median [xs]
  (let [n   (count xs)
        mid (int (/ n 2))]
    (if (odd? n)
      (nth (sort xs) mid)
      (->> (sort xs)
           (drop (dec mid))
           (take 2)
           (mean)))))

英国选民的中位数是:

(defn ex-1-8 []
  (->> (load-data :uk-scrubbed)
       (i/$ "Electorate")
       (median)))

;; 70813.5

Incanter 也提供了一个用于计算中位数的函数 s/median

方差

均值和中位数是描述序列中间值的两种替代方法,但单独使用它们几乎无法告诉我们序列中包含的值。例如,如果我们知道一个包含 99 个值的序列的均值是 50,我们也无法仅凭此判断序列包含哪些值。

它可能包含从 1 到 99 的所有整数,或者 49 个零和 50 个 99。也许它包含了负一的值 98 次,且有一个 5000 和 48。或者,可能所有的值都是恰好 50。

序列的方差是其关于均值的“分散度”,前面每个例子的方差都会不同。在数学符号中,方差表示为:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_07.jpg

其中,s²是通常用于表示方差的数学符号。

这个方程与之前计算均值的方程有许多相似之处。不同的是,我们不是求单一值x[i]的总和,而是求https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_08.jpg函数的总和。回忆一下,符号https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_02.jpg表示均值,因此这个函数计算了xi相对于所有xs均值的平方偏差。

我们可以将表达式https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_08.jpg转化为一个函数square-deviation,然后应用于xs序列。我们还可以利用已经创建的mean函数,计算序列中的值的总和,并除以计数。

(defn variance [xs]
  (let [x-bar (mean xs)
        n     (count xs)
        square-deviation (fn [x]
                           (i/sq (- x x-bar)))]
    (mean (map square-deviation xs))))

我们使用 Incanter 的i/sq函数来计算表达式的平方。

由于我们在取均值之前已经对偏差进行了平方,因此方差的单位也会被平方,所以英国选民的方差单位是“人平方”。这种单位有些不太自然。我们可以通过取方差的平方根,使得单位再次变为“人”,结果称为标准差

(defn standard-deviation [xs]
  (i/sqrt (variance xs)))

(defn ex-1-9 []
  (->> (load-data :uk-scrubbed)
       (i/$ "Electorate")
       (standard-deviation)))

;; 7672.77

Incanter 实现了分别计算方差和标准差的函数,分别为s/variances/sd

分位数

中位数是计算列表中中间值的一种方法,方差则提供了一种衡量数据相对于该中点的分布情况的方式。如果整个数据的分布在零到一的尺度上表示,中位数将是位于 0.5 处的值。

例如,考虑以下数字序列:

[10 11 15 21 22.5 28 30]

序列中有七个数字,因此中位数是第四个,或者是 21。这个值也称为 0.5 分位数。我们可以通过查看 0、0.25、0.5、0.7 和 1.0 分位数,获得序列数字的更丰富画面。将这些数字放在一起,它们不仅显示了中位数,还总结了数据的范围和数字在其中的分布情况。它们有时被称为五数概括

计算英国选民数据的五数概括的一种方法如下所示:

(defn quantile [q xs]
  (let [n (dec (count xs))
        i (-> (* n q)
              (+ 1/2)
              (int))]
    (nth (sort xs) i)))

(defn ex-1-10 []
  (let [xs (->> (load-data :uk-scrubbed)
                (i/$ "Electorate"))
        f (fn [q]
            (quantile q xs))]
    (map f [0 1/4 1/2 3/4 1])))

;; (21780.0 66219.0 70991.0 75115.0 109922.0)

也可以直接使用 Incanter 的s/quantile函数计算分位数。所需的分位数序列作为关键字参数:probs传入。

注意

Incanter 的quantile函数使用一种称为phi-quantile的算法变体,在某些情况下,它会在连续数字之间进行线性插值。计算分位数的方式有很多种——请参阅en.wikipedia.org/wiki/Quantile了解不同算法之间的差异。

当分位数将范围分为四个相等的范围时,它们被称为四分位数。下四分位数和上四分位数之间的差异称为四分位距,通常简写为IQR。像均值的方差一样,IQR 提供了关于中位数数据分布的度量。

分箱数据

为了理解这些方差计算所衡量的内容,我们可以使用一种称为分箱的技术。对于连续数据,使用frequencies(正如我们在选举数据中用来计算零值一样)并不实用,因为两个值可能不完全相同。然而,通过将数据分为离散的区间,我们可以大致了解数据的结构。

分箱过程是将数值范围划分为若干个连续、等大小的较小箱子。原始数据中的每个值都会落入其中的一个箱子。通过统计每个箱子中的点数,我们可以大致了解数据的分布情况:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_100.jpg

上述示例展示了将十五个x值分为五个相等大小的箱子。通过统计每个箱子内的点数,我们可以清楚地看到,大多数点落在中间的箱子中,而靠近边缘的箱子内的点数较少。我们可以使用以下bin函数在 Clojure 中实现相同的功能:

(defn bin [n-bins xs]
  (let [min-x    (apply min xs)
        max-x    (apply max xs)
        range-x  (- max-x min-x)
        bin-fn   (fn [x]
                   (-> x
                       (- min-x)
                       (/ range-x)
                       (* n-bins)
                       (int)
                       (min (dec n-bins))))]
    (map bin-fn xs)))

例如,我们可以将范围 0-14 分成5个箱子,如下所示:

(bin 5 (range 15))

;; (0 0 0 1 1 1 2 2 2 3 3 3 4 4 4)

一旦我们将值分箱,我们可以再次使用frequencies函数统计每个箱子中的点数。在以下代码中,我们使用该函数将英国选民数据分为五个箱子:

(defn ex-1-11 []
  (->> (load-data :uk-scrubbed)
       (i/$ "Electorate")
       (bin 10)
       (frequencies)))

;; {1 26, 2 450, 3 171, 4 1, 0 2}

极端箱子(0 和 4)的点数远低于中间的箱子——这些点数似乎向中位数处上升,然后又下降。在接下来的部分,我们将可视化这些点数的分布形态。

直方图

直方图是一种可视化单一数据序列分布的方法。直方图通过将连续分布进行分箱,并绘制每个箱子中点数的频率来呈现数据。直方图中每个条形的高度表示数据中有多少点落在该箱子内。

我们已经看过如何自己进行数据分箱,但incanter.charts包含一个histogram函数,可以通过两个步骤对数据进行分箱并可视化为直方图。在本章(以及全书)中,我们需要将incanter.charts作为c

(defn ex-1-12 []
  (-> (load-data :uk-scrubbed)
      (i/$ "Electorate")
      (c/histogram)
      (i/view)))

前面的代码生成了以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_110.jpg

我们可以通过将关键字参数:nbins作为第二个参数传递给histogram函数,来配置数据被分为多少个箱:

(defn ex-1-13 []
  (-> (uk-electorate)
      (c/histogram :nbins 200)
      (i/view)))

前面的图表显示了一个单一的高峰,但相对粗略地表达了数据的形状。下面的图表显示了更精细的细节,但柱状图的体积遮蔽了分布的形状,特别是在尾部:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_120.jpg

选择表示数据的箱数是一个精细的平衡——箱数太少,数据的形状将只会被粗略表示;箱数太多,噪声特征可能会遮蔽底层结构。

(defn ex-1-14 []
  (-> (i/$ "Electorate" (load-data :uk-scrubbed))
      (c/histogram :x-label "UK electorate"
                   :nbins 20)
      (i/view)))

下面显示的是20个柱状条的直方图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_130.jpg

这个包含20个箱的最终图表似乎是目前为止最好的数据表示。

除了均值和中位数,众数是另一种衡量序列平均值的方式——它被定义为序列中最常出现的值。众数严格来说仅定义在至少有一个重复值的序列中;对于许多分布,情况并非如此,因此众数是未定义的。然而,直方图的峰值通常被称为众数,因为它对应于最流行的箱。

我们可以清晰地看到,分布在众数附近非常对称,值在两侧急剧下降,尾部较浅。这是数据遵循大致正态分布的表现。

正态分布

直方图将告诉你数据如何大致分布在其范围内,并提供一种将数据分类为几种常见分布之一的可视化手段。在数据分析中,许多分布经常出现,但没有比正态分布更为常见的,正态分布也叫做高斯分布

注意

该分布被称为正态分布,是因为它在自然界中出现的频率非常高。伽利略注意到,他的天文测量误差遵循一个分布,即从均值的偏差较小的出现频率比偏差较大的频率更高。正是伟大的数学家高斯对这些误差的数学形状的描述,使得该分布也被称为高斯分布,以此纪念他。

分布就像是一种压缩算法:它允许将大量数据非常高效地进行总结。正态分布只需要两个参数,其他数据可以从这两个参数中进行近似——均值和标准差。

中心极限定理

正态分布的普遍性部分可以通过中心极限定理来解释。来自不同分布的数值在特定条件下会趋向于收敛到正态分布,接下来我们将展示这一点。

在编程中,一种常见的分布是均匀分布。这是由 Clojure 的rand函数生成的数字分布:对于一个公平的随机数生成器,所有数字生成的机会是相等的。我们可以通过多次生成一个 0 到 1 之间的随机数并绘制结果,来在直方图中可视化这一点。

(defn ex-1-15 []
  (let [xs (->> (repeatedly rand)
                (take 10000))]
    (-> (c/histogram xs
                     :x-label "Uniform distribution"
                     :nbins 20)
        (i/view))))

前面的代码将生成以下直方图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_140.jpg

每个直方图的条形高度大致相同,对应于生成落入每个区间的数字的均等概率。条形的高度并不完全相同,因为均匀分布描述的是我们的随机抽样无法精确反映的理论输出。在接下来的几章中,我们将学习如何精确量化理论与实践之间的差异,以确定这些差异是否足够大,值得关注。在这个案例中,它们并不大。

如果我们生成的是数列均值的直方图,结果会呈现出与之前截然不同的分布。

(defn ex-1-16 []
  (let [xs (->> (repeatedly rand)
                (partition 10)
                (map mean)
                (take 10000))]
    (-> (c/histogram xs
                     :x-label "Distribution of means"
                     :nbins 20)
        (i/view))))

前面的代码将生成类似于以下直方图的输出:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_150.jpg

虽然均值接近零或一并非不可能,但这种情况极为不可能,而且随着平均数的数量和抽样平均值的数量增加,这种不可能性会变得越来越大。事实上,输出值极其接近正态分布。

这个结果——即许多小的随机波动的平均效应导致正态分布——被称为中心极限定理,有时简称为CLT,它在很大程度上解释了为什么正态分布在自然现象中如此频繁地出现。

中心极限定理直到 20 世纪才得以命名,尽管这一效应早在 1733 年就被法国数学家阿布拉罕·德·莫伊夫(Abraham de Moivre)记录下来,他用正态分布来近似公平投掷硬币时的正面出现次数。硬币投掷的结果最适合用二项分布来建模,我们将在第四章 分类中介绍二项分布。虽然中心极限定理提供了一种从近似正态分布中生成样本的方法,但 Incanter 的distributions命名空间提供了从多种分布中高效生成样本的函数,包括正态分布:

(defn ex-1-17 []
  (let [distribution (d/normal-distribution)
        xs (->> (repeatedly #(d/draw distribution))
                (take 10000))]
    (-> (c/histogram xs
                     :x-label "Normal distribution"
                     :nbins 20)
        (i/view))))

前面的代码生成了以下直方图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_155.jpg

d/draw函数将从提供的分布返回一个样本。d/normal-distribution的默认均值和标准差分别为零和一。

庞加莱的面包师

有一个故事说,虽然几乎可以确定是虚构的,但它使我们更详细地了解中心极限定理如何帮助我们推断分布的形成方式。这个故事涉及到著名的十九世纪法国多面手亨利·庞加莱,据说他一年中每天称量自己的面包。

烘焙是一个受监管的行业,庞加莱发现,虽然面包的重量符合正态分布,但峰值在 950 克,而不是宣传的 1 公斤。他再次向当局举报面包师,于是面包师被罚款。

第二年,庞加莱继续从同一位面包师那里购买面包并称重。他发现均值现在是 1 公斤,但围绕均值的分布不再对称。分布向右偏,与面包师只给庞加莱最重的面包相符。庞加莱再次向当局举报面包师,面包师第二次被罚款。

现在不必关心这个故事是否真实;这个故事仅用来说明一个关键点——一系列数字的分布可以告诉我们生成它的过程中的一些重要信息。

生成分布

为了发展我们对正态分布和方差的直觉,让我们使用 Incanter 的分布函数来模拟一个诚实和不诚实的面包师。我们可以将诚实的面包师建模为均值为 1000 的正态分布,对应于 1 公斤的公平面包。我们假设在烘焙过程中存在方差,导致标准差为 30 克。

(defn honest-baker [mean sd]
  (let [distribution (d/normal-distribution mean sd)]
    (repeatedly #(d/draw distribution))))

(defn ex-1-18 []
  (-> (take 10000 (honest-baker 1000 30))
      (c/histogram :x-label "Honest baker"
                   :nbins 25)
      (i/view)))

上述代码将生成类似以下直方图的输出:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_160.jpg

现在,让我们模拟一个只卖最重的面包的面包师。我们将序列分成 13 个一组(“面包师的一打”),然后选择最大值:

(defn dishonest-baker [mean sd]
  (let [distribution (d/normal-distribution mean sd)]
    (->> (repeatedly #(d/draw distribution))
         (partition 13)
         (map (partial apply max)))))

(defn ex-1-19 []
  (-> (take 10000 (dishonest-baker 950 30))
      (c/histogram :x-label "Dishonest baker"
                   :nbins 25)
      (i/view)))

上述代码将生成类似以下直方图的输出:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_170.jpg

显而易见,这个直方图看起来与我们之前看到的不太一样。均值仍然是 1 公斤,但围绕均值的数值分布不再对称。我们称这个直方图显示出一个偏态正态分布

偏度

偏度是分布围绕其众数的不对称性的名称。负偏态左偏态表明图形下众数左侧的面积较大。正偏态右偏态表明图形下众数右侧的面积较大。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_180.jpg

Incanter 在stats命名空间中有一个内置函数用于测量偏度:

(defn ex-1-20 []
  (let [weights (take 10000 (dishonest-baker 950 30))]
    {:mean (mean weights)
     :median (median weights)
     :skewness (s/skewness weights)}))

上面的示例显示,不诚实面包师输出的偏度约为 0.4,量化了在直方图中显示的偏斜。

分位数-分位数图

我们在本章前面已经遇到过分位数,作为描述数据分布的一种方式。回想一下,quantile 函数接受介于零和一之间的数字,并返回该点的序列值。0.5 对应于中位数值。

将数据的分位数与正态分布的分位数进行绘制,可以让我们看到测量数据与理论分布的比较。这种图形被称为Q-Q 图,它提供了一种快速且直观的方式来判断正态性。对于接近正态分布的数据,Q-Q 图呈现直线。偏离直线的部分表明数据偏离理想化的正态分布的方式。

让我们并排绘制诚实和不诚实面包师的 Q-Q 图。Incanter 的 c/qq-plot 函数接受数据点列表,并生成一个样本分位数与理论正态分布的分位数绘制的散点图:

(defn ex-1-21 []
  (->> (honest-baker 1000 30)
       (take 10000)
       (c/qq-plot)
       (i/view))
  (->> (dishonest-baker 950 30)
       (take 10000)
       (c/qq-plot)
       (i/view)))

上述代码将生成以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_190.jpg

诚实面包师的 Q-Q 图在前面已经展示。下面是指不诚实面包师的图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_200.jpg

线条弯曲表明数据是正偏的;反向弯曲则表明数据是负偏的。实际上,Q-Q 图使得我们更容易识别各种偏离标准正态分布的情况,如下图所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_210.jpg

Q-Q 图比较了诚实与不诚实面包师的分布与理论正态分布的对比。在接下来的部分,我们将比较几种不同的方式来直观地比较两个(或更多)测量值序列。

比较可视化

Q-Q 图提供了一种极好的方式,用来将测量得到的经验分布与理论正态分布进行比较。如果我们想要比较两个或多个经验分布之间的关系,我们不能使用 Incanter 的 Q-Q 图表。不过,我们有其他多种选择,如接下来的两部分所示。

箱形图

箱形图,或称为箱线图,是一种可视化描述统计中的中位数和方差的方式。我们可以使用以下代码生成它们:

(defn ex-1-22 []
  (-> (c/box-plot (->> (honest-baker 1000 30)
                       (take 10000))
                  :legend true
                  :y-label "Loaf weight (g)"
                  :series-label "Honest baker")
      (c/add-box-plot (->> (dishonest-baker 950 30)
                           (take 10000))
                      :series-label "Dishonest baker")
      (i/view)))

这将生成以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_220.jpg

图中间的框表示四分位数范围。中位数是通过盒子中间的线,而均值则是大的黑点。对于诚实的面包师,中位数穿过圆形的中心,表明均值和中位数差不多。而对于不诚实的面包师,均值偏离中位数,表明存在偏斜。

须根表示数据的范围,异常值用空心圆表示。在一个图表中,我们比在单独的直方图或 Q-Q 图上更清楚地看到了两个分布之间的差异。

累积分布函数

累积分布函数,也称为 CDF,描述了从一个分布中抽取的值小于 x 的概率。像所有概率分布一样,它们的值在 01 之间,0 代表不可能,1 代表确定性。例如,假设我即将掷一个六面骰子。掷出小于六的概率是多少?

对于一颗公平的骰子,掷出 5 或更小的概率是 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_09.jpg。相反,掷出 1 的概率只有 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_10.jpg。掷出 3 或更小的结果对应着平等的几率——50% 的概率。

骰子掷出的 CDF 遵循与所有 CDF 相同的模式——对于数值范围较低的部分,CDF 接近零,表示选择该范围或以下的数字的概率较低。对于范围的高端,CDF 接近一,因为大多数从序列中抽取的值会较小。

注意

CDF 和分位数密切相关——CDF 是分位数函数的逆函数。如果 0.5 分位数对应的值是 1,000,那么 1,000 的 CDF 就是 0.5。

就像 Incanter 的 s/quantile 函数允许我们在特定点从分布中采样值一样,s/cdf-empirical 函数允许我们输入一个来自序列的值,并返回一个介于零和一之间的值。它是一个高阶函数——接受值(在此情况下是一个值的序列)并返回一个函数。然后可以多次调用返回的函数,传入不同的输入值,从而返回它们各自的 CDF。

注意

高阶函数是接受或返回函数的函数。

让我们并排绘制诚实和不诚实面包师的 CDF。我们可以使用 Incanter 的 c/xy-plot 来通过绘制源数据——来自诚实和不诚实面包师的样本——与针对经验 CDF 计算出的概率,来可视化 CDF。c/xy-plot 函数期望 x 值和 y 值作为两个单独的值序列提供。

为了在同一个图表上绘制这两个分布,我们需要能够为我们的 xy-plot 提供多个系列。Incanter 为其许多图表提供了添加附加系列的功能。对于 xy-plot,我们可以使用函数 c/add-lines,它的第一个参数是图表,接下来的两个参数分别是 x 系列和 y 系列的数据。你还可以传递一个可选的系列标签。我们在以下代码中这么做,以便在最终的图表上区分这两个系列:

(defn ex-1-23 []
  (let [sample-honest    (->> (honest-baker 1000 30)
                              (take 1000))
        sample-dishonest (->> (dishonest-baker 950 30)
                              (take 1000))
        ecdf-honest    (s/cdf-empirical sample-honest)
        ecdf-dishonest (s/cdf-empirical sample-dishonest)]
    (-> (c/xy-plot sample-honest (map ecdf-honest sample-honest)
                   :x-label "Loaf Weight"
                   :y-label "Probability"
                   :legend true
                   :series-label "Honest baker")
        (c/add-lines sample-dishonest
                     (map ecdf-dishonest sample-dishonest)
                     :series-label "Dishonest baker")
        (i/view))))

上面的代码生成了如下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_230.jpg

尽管看起来很不一样,这个图表实际上展示了与箱线图相同的信息。我们可以看到,两条线大约在 0.5 的中位数处交叉,对应于 1,000 克。那个不诚实的线在下尾处被截断,上尾则更长,表明其分布是偏斜的。

可视化的重要性

像前面那样的简单可视化是传达大量信息的简洁方式。它们补充了我们在本章前面计算的摘要统计量,使用它们非常重要。像均值和标准差这样的统计量不可避免地会隐藏大量信息,因为它们将一系列数据压缩为一个单一的数字。

统计学家弗朗西斯·安斯科姆(Francis Anscombe)设计了一组四个散点图,称为安斯科姆四重奏,它们具有几乎相同的统计特性(包括均值、方差和标准差)。尽管如此,xsys的分布在视觉上是截然不同的:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_240.jpg

数据集在绘制图表时不必经过人为设计就能揭示有价值的见解。以 2013 年波兰全国马图拉考试成绩的直方图为例:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_250.jpg

我们可能期望学生的能力呈正态分布,实际上——除了大约 30%的急剧峰值——它确实是正态分布的。我们可以清楚地看到,考官人为地把学生的成绩推高,使其超过及格线。

实际上,从大样本中提取的序列的分布可以如此可靠,以至于任何偏离它们的情况都可能是非法活动的证据。本福德定律,也叫做首位数字定律,是一种关于大范围随机数的奇特特性。数字 1 大约有 30%的时间作为首位数字出现,而较大的数字则越来越少。比如,数字 9 作为首位数字的概率不到 5%。

注意

本福德定律以物理学家弗兰克·本福德(Frank Benford)的名字命名,他于 1938 年提出该定律,并展示了它在各种数据源中的一致性。早在 50 多年前,西蒙·纽科姆(Simon Newcomb)就曾注意到,本福德定律曾被他发现过,纽科姆观察到,他的对数表书页在数字以 1 开头的地方更为磨损。

本福德展示了该定律适用于各种各样的数据,例如电费账单、街道地址、股价、人口数据、死亡率以及河流的长度。这一定律在涵盖大范围数值的数据集中的一致性如此之高,以至于其偏离被接受作为金融欺诈审判中的证据。

可视化选民数据

让我们回到选举数据,并将我们之前创建的选民序列与理论正态分布的 CDF 进行比较。我们可以使用 Incanter 的s/cdf-normal函数根据值序列生成正态分布的 CDF。默认均值为 0,标准差为 1,因此我们需要提供选民数据的测量均值和标准差。对于我们的选民数据,这些值分别为 70,150 和 7,679。

在本章前面,我们生成了一个经验性的 CDF。以下示例仅生成了两个 CDF,并将它们绘制在单个c/xy-plot上:

(defn ex-1-24 []
  (let [electorate (->> (load-data :uk-scrubbed)
                        (i/$ "Electorate"))
        ecdf   (s/cdf-empirical electorate)
        fitted (s/cdf-normal electorate
                             :mean (s/mean electorate)
                             :sd   (s/sd electorate))]
    (-> (c/xy-plot electorate fitted
                   :x-label "Electorate"
                   :y-label "Probability"
                   :series-label "Fitted"
                   :legend true)
        (c/add-lines electorate (map ecdf electorate)
                     :series-label "Empirical")
        (i/view))))

前面的示例生成了以下绘图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_260.jpg

通过两条线的接近程度,您可以看出数据多么接近正态分布,尽管稍有偏斜。偏斜方向与我们之前绘制的不诚实面包师的 CDF 相反,因此我们的选民数据略微向左倾斜。

因为我们正在将我们的分布与理论正态分布进行比较,让我们使用 Q-Q 图,默认情况下将执行此操作:

(defn ex-1-25 []
  (->> (load-data :uk-scrubbed)
       (i/$ "Electorate")
       (c/qq-plot)
       (i/view)))

下面的 Q-Q 图更好地突显了数据中明显的左偏态:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_270.jpg

正如我们预期的那样,曲线与本章早期不诚实的面包师 Q-Q 图相反。这表明,如果数据更接近正态分布,比我们预期的小选区数目更多。

添加列

到目前为止,本章中我们通过过滤行和列来减少数据集的大小。通常,我们会希望向数据集添加行,Incanter 支持几种方式来实现这一点。

首先,我们可以选择是替换数据集中的现有列还是追加附加列到数据集。其次,我们可以选择是直接提供新列值以替换现有列值,还是通过对数据的每一行应用函数来计算新值。

下图列出了我们的选项及相应的 Incanter 函数使用方法:

替换数据追加数据
通过提供序列i/replace-columni/add-column
通过应用函数i/transform-columni/add-derived-column

当基于函数转换或派生列时,我们将传递新列的名称以创建,应用于每行的函数,以及现有列名称的序列。每个现有列中包含的值将构成函数的参数。

让我们通过一个实际示例来展示如何使用 i/add-derived-column 函数。2010 年的英国大选结果是悬浮议会,没有任何政党获得绝对多数席位。保守党和自由民主党之间形成了联合政府。在下一节中,我们将找出每个党派的支持人数,并计算其在总投票中的比例。

添加衍生列

要找出选民中投票支持保守党或自由民主党的比例,我们需要计算每个政党的得票总和。由于我们是基于现有数据创建一个新的数据字段,因此我们需要使用 i/add-derived-column 函数。

(defn ex-1-26 []
  (->> (load-data :uk-scrubbed)
       (i/add-derived-column :victors [:Con :LD] +)))

然而,如果我们现在运行这个操作,将会生成一个异常:

ClassCastException java.lang.String cannot be cast to java.lang.Number  clojure.lang.Numbers.add (Numbers.java:126)

不幸的是,Clojure 报告错误,指出我们试图添加一个 java.lang.String 类型的值。显然,ConLD 列中某个(或两个)包含了字符串值,但到底是哪个呢?我们可以再次使用频率统计来查看问题的范围:

(->> (load-data :uk-scrubbed)
     ($ "Con")
     (map type)
     (frequencies))

;; {java.lang.Double 631, java.lang.String 19}

(->> (load-data :uk-scrubbed)
     ($ "LD")
     (map type)
     (frequencies))

;; {java.lang.Double 631, java.lang.String 19}

让我们使用本章早些时候提到的 i/$where 函数,仅查看这些数据行:

(defn ex-1-27 []
  (->> (load-data :uk-scrubbed)
       (i/$where #(not-any? number? [(% "Con") (% "LD")]))
       (i/$ [:Region :Electorate :Con :LD])))

;; |           Region | Electorate | Con | LD |
;; |------------------+------------+-----+----|
;; | Northern Ireland |    60204.0 |     |    |
;; | Northern Ireland |    73338.0 |     |    |
;; | Northern Ireland |    63054.0 |     |    |
;; ...

这一部分的探索应该足以让我们确信,这些字段为空的原因是没有在相应选区推出候选人。我们应该过滤掉这些数据,还是认为它们的值为零呢?这是一个有趣的问题。我们选择过滤掉这些数据,因为在这些选区内,选民根本无法选择自由民主党或保守党候选人。如果我们假定为零,将人为地降低那些本可以选择其中一个政党投票的选民的平均数。

现在我们知道如何过滤掉有问题的行,接下来添加胜选者及其投票份额、选民投票率的衍生列。我们过滤数据行,仅显示那些有保守党和自由民主党候选人的行:

(defmethod load-data :uk-victors [_]
  (->> (load-data :uk-scrubbed)
       (i/$where {:Con {:$fn number?} :LD {:$fn number?}})
       (i/add-derived-column :victors [:Con :LD] +)
       (i/add-derived-column :victors-share [:victors :Votes] /)
       (i/add-derived-column :turnout [:Votes :Electorate] /)))

结果是,我们的数据集中新增了三列::victors:victors-share:turnout。接下来,让我们通过 Q-Q 图展示胜选者的投票份额,看看它与理论上的正态分布有何不同:

(defn ex-1-28 []
  (->> (load-data :uk-victors)
       (i/$ :victors-share)
       (c/qq-plot)
       (i/view)))

上述代码生成了以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_275.jpg

回顾本章前面提到的各种 Q-Q 图形,结果显示胜选者的投票份额相比正态分布具有“轻尾”特性。这意味着更多的数据点集中在均值附近,超出了我们对真正正态分布数据的预期。

选民数据的对比可视化

现在我们来看另一个大选的数据集,这次是 2011 年的俄罗斯大选。俄罗斯是一个更大的国家,其选举数据也要大得多。我们将加载两个较大的 Excel 文件到内存中,这可能会超过默认的 JVM 堆大小。

为了扩展 Incanter 可用的内存,我们可以调整项目中profile.clj的 JVM 设置。可以通过:jvm-opts键提供一个 JVM 的配置标志向量。这里我们使用 Java 的Xmx标志将堆内存大小增加到 1GB,这应该足够用了。

  :jvm-opts ["-Xmx1G"]

俄罗斯的数据存储在两个数据文件中。幸运的是,每个文件中的列名相同,因此它们可以按顺序连接在一起。Incanter 的i/conj-rows函数正是为了这个目的而存在:

(defmethod load-data :ru [_]
  (i/conj-rows (-> (io/resource "Russia2011_1of2.xls")
                   (str)
                   (xls/read-xls))
               (-> (io/resource "Russia2011_2of2.xls")
                   (str)
                   (xls/read-xls))))

在前面的代码中,我们定义了load-data多重方法的第三个实现来加载并合并这两个俄罗斯文件。

注意

除了conj-rows,Incanter-core 还定义了conj-columns,它将合并具有相同行数的数据集的列。

让我们看看俄罗斯数据的列名是什么:

(defn ex-1-29 []
  (-> (load-data :ru)
      (i/col-names)))

;; ["Code for district"
;; "Number of the polling district (unique to state, not overall)"
;; "Name of district" "Number of voters included in voters list"
;; "The number of ballots received by the precinct election
;; commission" ...]

俄罗斯数据集中的列名非常具有描述性,但可能比我们想要输入的更长。而且,如果与我们之前看到的英国选举数据中表示相同属性的列(例如,获胜者的份额和投票率)在两者中有相同的标签,那将更加方便。让我们相应地重命名它们。

与数据集一起,i/rename-cols函数期望接收一个映射,其中键是当前的列名,值对应所需的新列名。如果我们将其与之前看到的i/add-derived-column数据结合起来,我们得到如下结果:

(defmethod load-data :ru-victors [_]
  (->> (load-data :ru)
       (i/rename-cols
        {"Number of voters included in voters list" :electorate
         "Number of valid ballots" :valid-ballots
         "United Russia" :victors})
       (i/add-derived-column :victors-share
                             [:victors :valid-ballots] i/safe-div)
       (i/add-derived-column :turnout
                             [:valid-ballots :electorate] /)))

i/safe-div函数与/相同,但它能够防止除以零的情况。它不会抛出异常,而是返回Infinity,该值将在 Incanter 的统计和图表功能中被忽略。

可视化俄罗斯选举数据

我们之前看到,英国选举投票率的直方图大致呈正态分布(尽管尾部较轻)。现在我们已经加载并转换了俄罗斯选举数据,让我们看看它的对比情况:

(defn ex-1-30 []
  (-> (i/$ :turnout (load-data :ru-victors))
      (c/histogram :x-label "Russia turnout"
                   :nbins 20)
      (i/view)))

上面的例子生成了以下的直方图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_320.jpg

这个直方图看起来根本不像我们之前看到的经典钟形曲线。它有明显的正偏态,选民的投票率实际上从 80%增加到 100%——这与我们对正态分布数据的预期正好相反。

根据英国数据和中心极限定理所设定的预期,这是一个有趣的结果。让我们改用 Q-Q 图来可视化数据:

(defn ex-1-31 []
  (->> (load-data :ru-victors)
       (i/$ :turnout)
       (c/qq-plot)
       (i/view)))

这将返回以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_330.jpg

这个 Q-Q 图既不是一条直线,也不是特别 S 形的曲线。实际上,Q-Q 图暗示了分布的上端有轻微的尾部,而下端则有较重的尾部。这几乎与我们在直方图中看到的情况相反,后者明显表明右尾极重。

实际上,正是因为尾部如此沉重,Q-Q 图才会产生误导:直方图上 0.5 到 1.0 之间的点密度暗示峰值应在 0.7 左右,右尾则延续到 1.0 以外。显然,百分比超过 100%是没有逻辑的,但 Q-Q 图没有考虑到这一点(它并不知道我们在绘制百分比),因此 1.0 以上数据的突然缺失被解释为被截断的右尾。

鉴于中心极限定理以及我们在英国选举数据中观察到的情况,100%的选民投票率这一趋势颇为引人注目。让我们将英国和俄罗斯的数据集并排比较。

比较可视化

假设我们想比较英国和俄罗斯选民数据的分布。我们已经在本章中学习了如何使用 CDF 和箱线图,所以让我们来研究一种类似于直方图的替代方法。

我们可以尝试在直方图上绘制这两个数据集,但这不是一个好主意。我们无法解释结果,原因有二:

  • 投票区的大小,以及因此而导致的分布均值,差异非常大。

  • 投票区的数量差异如此之大,因此直方图的条形高度会不同

解决上述问题的一个替代方法是概率质量函数PMF)。

概率质量函数

概率质量函数(PMF)与直方图有很多相似之处。不过,它不是绘制落入区间的数值计数,而是绘制从分布中抽取的数字恰好等于某一给定值的概率。由于该函数为分布中所有可能返回的值分配了概率,而且概率是在零到一的范围内度量的(其中一对应确定性),因此概率质量函数下的面积等于一。

因此,PMF 确保了我们绘制的图形下的面积在不同数据集之间是可比较的。然而,我们仍然面临投票区大小——因此分布的均值——无法直接比较的问题。这可以通过一个独立的技术——规范化来解决。

注意

数据规范化与正态分布无关。它是一个通用任务,用来将一个或多个数值序列对齐。根据具体情况,它可以仅仅意味着调整值使其落在相同的范围内,或者采取更复杂的程序来确保数据分布一致。通常,规范化的目的是为了便于比较两组或更多组数据。

规范化数据的方法有无数种,但最基本的一种是确保每个系列的数值都在零到一之间。我们所有的值都不会低于零,因此我们可以通过简单地除以最大值来实现这种规范化:

(defn as-pmf [bins]
  (let [histogram (frequencies bins)
        total     (reduce + (vals histogram))]
    (->> histogram
         (map (fn [[k v]]
                [k (/ v total)]))
         (into {}))))

使用上述函数,我们可以将英国和俄罗斯的数据进行归一化,并将它们并排绘制在相同的坐标轴上:

(defn ex-1-32 []
  (let [n-bins 40
        uk (->> (load-data :uk-victors)
                (i/$ :turnout)
                (bin n-bins)
                (as-pmf))
        ru (->> (load-data :ru-victors)
                (i/$ :turnout)
                (bin n-bins)
                (as-pmf))]
    (-> (c/xy-plot (keys uk) (vals uk)
                   :series-label "UK"
                   :legend true
                   :x-label "Turnout Bins"
                   :y-label "Probability")
        (c/add-lines (keys ru) (vals ru)
                     :series-label "Russia")
        (i/view))))

上述例子生成了以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_340.jpg

经过归一化处理后,这两个分布可以更方便地进行比较。显然,尽管俄罗斯的投票率均值低于英国,但俄罗斯选举的投票率在接近 100%的地方出现了大幅跃升。由于选举结果代表了许多独立选择的综合效应,我们预计选举结果会符合中心极限定理,呈大致正态分布。实际上,全球范围内的选举结果通常都符合这一预期。

尽管并不像分布中心的模态峰值那样高——对应大约 50%的投票率——但俄罗斯选举数据呈现出一个非常反常的结果。维也纳医科大学的研究员彼得·克里梅克及其同事甚至建议这明显是选票操控的标志。

散点图

我们已经观察到俄罗斯选举投票率的奇异结果,并且确认它与英国选举的签名不同。接下来,让我们看看获胜候选人的选票比例与投票率之间的关系。毕竟,如果出乎意料的高投票率真的是现任政府操纵选举的信号,那么我们预计他们会为自己投票,而不是为其他候选人投票。因此,我们预计大多数(如果不是全部的话)额外的选票将投给最终的选举获胜者。

第三章,相关性,将更详细地讨论相关两个变量的统计学原理,但现在,仅仅可视化投票率与获胜党派选票比例之间的关系就已经很有趣了。

本章我们将介绍的最后一个可视化图表是散点图。散点图非常适合用来可视化两个变量之间的相关性:如果存在线性相关性,它将在散点图中表现为对角线趋势。Incanter 包含了c/scatter-plot函数用于这种类型的图表,参数与c/xy-plot函数相同。

(defn ex-1-33 []
  (let [data (load-data :uk-victors)]
    (-> (c/scatter-plot (i/$ :turnout data)
                        (i/$ :victors-share data)
                        :x-label "Turnout"
                        :y-label "Victor's Share")
        (i/view))))

上述代码生成了以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_350.jpg

尽管这些点大致呈现为一个模糊的椭圆形,但在散点图中,明显存在向右上方的对角线趋势。这表明了一个有趣的结果——投票率与最终选举获胜者的选票比例之间存在关联。我们本可能预期到相反的结果:选民自满导致投票率降低,而在有明确胜者的情况下尤为如此。

注意

如前所述,2010 年英国大选远非普通选举,结果是悬浮议会和联合政府。事实上,所谓的“赢家”是指两党,这两党直到选举日之前一直是对手。选任何一方的票都算作是投给赢家的票。

接下来,我们将为俄罗斯选举创建相同的散点图:

(defn ex-1-34 []
  (let [data (load-data :ru-victors)]
    (-> (c/scatter-plot (i/$ :turnout data)
                        (i/$ :victors-share data)
                        :x-label "Turnout"
                        :y-label "Victor's Share")
        (i/view))))

这将生成以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_360.jpg

尽管俄罗斯数据中的对角趋势从点的轮廓中清晰可见,但大量数据掩盖了其内部结构。在本章的最后一部分,我们将展示一种简单的技术,利用透明度从这样的图表中提取结构。

散点透明度

在前面的情境中,当散点图被大量数据点淹没时,透明度可以帮助更好地可视化数据的结构。由于重叠的半透明点会变得更不透明,而点较少的区域会更透明,使用半透明点的散点图比使用实心点更能有效显示数据的密度。

我们可以使用c/set-alpha函数设置 Incanter 图表上绘制点的 alpha 透明度。它接受两个参数:一个图表和一个介于零到一之间的数字。1 表示完全不透明,0 表示完全透明。

(defn ex-1-35 []
  (let [data (-> (load-data :ru-victors)
                 (s/sample :size 10000))]
    (-> (c/scatter-plot (i/$ :turnout data)
                        (i/$ :victors-share data)
                        :x-label "Turnout"
                        :y-label "Victor Share")
        (c/set-alpha 0.05)
        (i/view))))

前面的例子生成了以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_01_370.jpg

前面的散点图展示了胜者的份额与选民投票率之间通常同时变化的趋势。我们可以看到这两个值之间存在一定的相关性,并且在图表的右上角有一个“热点”,它对应着接近 100%的选民投票率和赢得选举的党派几乎拿到 100%的选票。特别是,这正是维也纳医科大学的研究人员所指出的选举舞弊的标志。这一点在世界其他地方的有争议选举结果中也十分明显,比如 2011 年乌干达总统选举的结果。

提示

世界其他地方许多选举的地区级结果可以在www.complex-systems.meduniwien.ac.at/elections/election.html上查看。访问该网站可以获取研究论文的链接,并下载其他数据集,帮助你实践本章关于清理和转换真实数据的知识。

我们将在第三章中更详细地讲解相关性,相关性,届时我们将学习如何量化两个值之间关系的强度,并基于此建立预测模型。我们还将在第十章,可视化中回顾这些数据,当时我们将实现一个自定义的二维直方图,以更清晰地可视化选民投票率与获胜党派选票比例之间的关系。

总结

在本章中,我们学习了总结性统计和分布的价值。我们已经看到,即使是简单的分析,也能提供潜在欺诈活动的证据。

尤其是,我们遇到了中心极限定理,并理解了它为何如此有助于解释正态分布在数据科学中的普遍性。一个合适的分布可以用少数几个统计量来代表一大串数字的本质,我们在本章中已经使用纯 Clojure 函数实现了其中的几个。我们还介绍了 Incanter 库,并用它加载、转换和可视化地比较了几个数据集。然而,我们并未做更多的工作,只能注意到两个分布之间一个有趣的差异。

在下一章中,我们将扩展关于描述性统计的知识,涵盖推断统计。这将使我们能够量化两个或更多分布之间的测量差异,并判断这种差异是否具有统计显著性。我们还将学习假设检验——一种进行稳健实验的框架,使我们能够从数据中得出结论。

第二章 推断

“我什么也看不见,”我说着,把它递还给我的朋友。**“相反,沃森,你什么都看得见。你只是没有从你所见的事物中推理出来。然而,你在得出结论时过于胆怯。”
阿瑟·柯南·道尔,《蓝宝石冒险》

在上一章中,我们介绍了多种数值和视觉方法来理解正态分布。我们讨论了描述性统计量,例如均值和标准差,以及它们如何用于简洁地总结大量数据。

数据集通常是某个更大总体的样本。有时,这个总体过于庞大,无法完全测量。有时,它本质上是无法测量的,可能是因为它的大小是无限的,或因为其他原因无法直接访问。无论是哪种情况,我们都不得不从已有的数据中进行概括。

在本章中,我们将讨论统计推断:如何超越仅仅描述数据样本,而是描述它们来自的总体。我们将详细探讨我们对从数据样本中得出的推断的置信度。我们还将讨论假设检验:一种强健的数据分析方法,它将科学带入数据科学。我们还将使用 ClojureScript 实现一个交互式网页,以模拟样本与它们所来自总体之间的关系。

为了帮助说明这些原理,我们将虚构一个公司——AcmeContent,假设它最近聘请我们作为数据科学家。

介绍 AcmeContent

为了帮助说明本章的概念,假设我们最近被聘为 AcmeContent 公司的数据科学家。该公司运营着一个网站,让访问者分享他们在网上喜欢的视频片段。

AcmeContent 通过其网站分析跟踪的一个指标是停留时间。这是衡量访问者在网站上停留多久的指标。显然,花费较长时间在网站上的访问者通常是在享受网站的内容,AcmeContent 希望访问者尽可能长时间停留。如果平均停留时间增加,我们的首席执行官将非常高兴。

注意

停留时间是指访问者第一次到达网站和他们做出最后一次请求之间的时间长度。

跳出率是指只做出一次请求的访问者——他们的停留时间为零。

作为公司的新数据科学家,我们的任务是分析网站分析报告中的停留时间,并衡量 AcmeContent 网站的成功程度。

下载示例代码

本章的代码可以在github.com/clojuredatascience/ch2-inference上找到,也可以从 Packt Publishing 的网站获取。

这个示例数据是专门为本章生成的。它足够小,因此已与书中的示例代码一起包含在数据目录中。请查阅本书的 wiki:wiki.clojuredatascience.com以获取关于停留时间分析的进一步阅读链接。

加载并检查数据

在上一章中,我们使用 Incanter 的incanter.excel/load-xls函数加载了 Excel 电子表格。在本章中,我们将从一个以制表符分隔的文本文件中加载数据集。为此,我们将使用incanter.io/read-dataset,它期望接收一个 URL 对象或一个表示文件路径的字符串。

该文件已由 AcmeContent 的网页团队进行了有益的重新格式化,包含了两列——请求日期和停留时间(单位:秒)。第一行是列标题,因此我们向read-dataset传递:header true

(defn load-data [file]
  (-> (io/resource file)
      (iio/read-dataset :header true :delim \tab)))

(defn ex-2-1 []
  (-> (load-data "dwell-times.tsv")
      (i/view)))

如果你运行这段代码(无论是在 REPL 中还是通过命令行使用lein run –e 2.1),你应该会看到类似如下的输出:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_100.jpg

让我们看看停留时间以直方图的形式呈现出来是什么样的。

可视化停留时间

我们可以通过简单地使用i/$提取:dwell-time列来绘制停留时间的直方图:

(defn ex-2-2 []
  (-> (i/$ :dwell-time (load-data "dwell-times.tsv"))
      (c/histogram :x-label "Dwell time (s)"
                   :nbins 50)
      (i/view)))

之前的代码生成了以下的直方图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_110.jpg

显然,这不是一个正态分布的数据,甚至也不是一个非常偏斜的正态分布。峰值左侧没有尾部(访客显然不可能在我们的网站停留不到零秒)。虽然数据开始时右侧急剧下降,但它沿着x轴延伸得比我们从正态分布数据中预期的要远得多。

当遇到像这样的分布时,其中大部分值都很小,但偶尔出现极端值,使用对数尺度绘制y轴可能会很有用。对数尺度用于表示覆盖非常大范围的事件。通常,图表的坐标轴是线性的,它们将一个范围分割成相等大小的步骤,就像我们在学校学过的“数字线”。对数尺度则将范围分割成随着离原点越来越远而逐渐增大的步骤。

一些测量自然现象的系统,涵盖了非常大的范围,通常会使用对数尺度表示。例如,地震的里氏震级就是一个以 10 为底的对数尺度,这意味着震级为 5 的地震是震级为 4 的地震的 10 倍。分贝尺度也是一个对数尺度,但它有不同的底数——30 分贝的声波的强度是 20 分贝声波的 10 倍。在每种情况下,原理都是一样的——使用对数尺度可以将一个非常大的值范围压缩到一个更小的范围内。

在 Incanter 中,通过c/set-axis将我们的y轴绘制为log-axis非常简单:

(defn ex-2-3 []
  (-> (i/$ :dwell-time (load-data "dwell-times.tsv"))
      (c/histogram :x-label "Dwell time (s)"
                   :nbins 20)
      (c/set-axis :y (c/log-axis :label "Log Frequency"))
      (i/view)))

默认情况下,Incanter 将使用以 10 为底的对数尺度,这意味着轴上的每一个刻度代表的范围是前一步的 10 倍。像这样的图表——只有一个轴是对数尺度——称为对数-线性图。毫不奇怪,显示两个对数轴的图表称为对数-对数图

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_120.jpg

在对数-线性图上绘制停留时间能够显示数据中的隐藏一致性——停留时间与频率的对数之间存在线性关系。除了图表右侧(那里的访问者少于 10 个)出现关系不再清晰外,其他部分该关系非常一致。

在对数-线性图上,直线是指数分布的明显指示。

指数分布

指数分布通常出现在考虑有许多小的正量和较少的较大量的情况时。根据我们对里氏震级的了解,地震的震级遵循指数分布这一点应该不会令人惊讶。

该分布也经常出现在等待时间中——直到下一次任何震级的地震发生的时间大致也遵循指数分布。该分布常用于建模故障率,本质上是指机器故障的等待时间。我们的指数分布模型类似于故障过程——即访问者厌倦并离开我们网站的等待时间。

指数分布具有许多有趣的性质。其中之一与均值和标准差有关:

(defn ex-2-4 []
  (let [dwell-times (->> (load-data "dwell-times.tsv")
                         (i/$ :dwell-time))]
    (println "Mean:  " (s/mean dwell-times))
    (println "Median:" (s/median dwell-times))
    (println "SD:    " (s/sd dwell-times))))

Mean:   93.2014074074074
Median: 64.0
SD:     93.96972402519796

均值和标准差非常相似。实际上,对于理想的指数分布,它们是完全相同的。这个特性适用于所有的指数分布——均值越大,标准差也越大。

注意

对于指数分布,均值和标准差相等。

指数分布的第二个特性是它是无记忆的。这是一个与直觉相反的特性,最好的说明方法是通过一个例子来展示。我们通常认为,随着访问者继续浏览我们的网站,他们厌倦并离开的概率会增加。由于平均停留时间为 93 秒,因此可能会出现这样的想法:超过 93 秒后,他们继续浏览的可能性会越来越小。

指数分布的无记忆特性告诉我们,访问者在我们的网站上停留额外 93 秒的概率,与他们已经浏览了 93 秒、5 分钟、1 小时,还是刚刚到达网站时的浏览时间无关。

注意

对于无记忆分布,继续再等额外的x分钟的概率与已经经过的时间无关。

指数分布的记忆无关特性在一定程度上解释了为何如此难以预测地震何时发生。我们必须依赖其他证据(例如地磁扰动),而非时间的流逝。

由于中位数停留时间为 64 秒,大约一半的访客只在网站停留约一分钟。平均值 93 秒表明,有些访客停留的时间要长得多。这些统计数据是基于过去 6 个月所有访客计算的。可能有趣的是,看看这些统计数据在每天的变化。让我们现在计算一下。

日均值分布

网络团队提供的文件包含了访问的时间戳。为了按天汇总数据,我们需要从日期中移除时间部分。虽然我们可以通过字符串操作来完成这一任务,但更灵活的方法是使用日期和时间库,如clj-timegithub.com/clj-time/clj-time)来解析字符串。这将不仅使我们能够移除时间,还能执行任意复杂的过滤操作(例如,筛选特定的星期几,或每月的第一天或最后一天)。

clj-time.predicates 命名空间包含了多种有用的谓词,而 clj-time.format 命名空间包含了尝试使用预定义的标准格式将字符串转换为日期时间对象的解析函数。如果我们的时间戳不是标准格式,我们可以使用相同的命名空间构建自定义格式化器。更多信息和使用示例,请参考 clj-time 文档:

(defn with-parsed-date [data]
  (i/transform-col data :date (comp tc/to-local-date f/parse)))

(defn filter-weekdays [data]
  (i/$where {:date {:$fn p/weekday?}} data))

(defn mean-dwell-times-by-date [data]
  (i/$rollup :mean :dwell-time :date data))

(defn daily-mean-dwell-times [data]
  (->> (with-parsed-date data)
       (filter-weekdays)
       (mean-dwell-times-by-date)))

将前面的函数结合起来,我们可以计算日均停留时间的平均值、中位数和标准差:

(defn ex-2-5 []
  (let [means (->> (load-data "dwell-times.tsv")
                   (daily-mean-dwell-times)
                   (i/$ :dwell-time))]
    (println "Mean:   " (s/mean means))
    (println "Median: " (s/median means))
    (println "SD:     " (s/sd means))))

;; Mean:    90.210428650562
;; Median:  90.13661202185791
;; SD:      3.722342905320035

我们的日均值的平均值是 90.2 秒。这个值接近我们之前计算的整个数据集(包括周末)的平均值。然而,标准差要低得多,仅为 3.7 秒。换句话说,日均值的分布比整个数据集的标准差要小得多。接下来,让我们在图表上绘制日均停留时间:

(defn ex-2-6 []
  (let [means (->> (load-data "dwell-times.tsv")
                   (daily-mean-dwell-times)
                   (i/$ :dwell-time))]
    (-> (c/histogram means
                     :x-label "Daily mean dwell time (s)"
                     :nbins 20)
        (i/view))))

这段代码生成了以下的直方图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_130.jpg

样本均值的分布围绕整体总均值 90 秒对称,标准差为 3.7 秒。与这些均值抽样的分布——指数分布不同,样本均值的分布呈正态分布。

中心极限定理

我们在上一章中遇到了中心极限定理,当时我们从均匀分布中取样并对其求平均。实际上,中心极限定理适用于任何值的分布,只要该分布的标准差是有限的。

注意

中心极限定理指出,不管样本是从哪种分布中计算出来的,样本均值的分布将是正态分布。

无论基础分布是否为指数分布,都不重要——中心极限定理表明,从任何分布中随机抽取的样本的均值将接近正态分布。让我们在直方图上绘制正态曲线,看看它与实际数据的匹配程度。

为了在我们的直方图上绘制正态曲线,我们必须将直方图绘制为密度直方图。这样绘制的是每个桶中所有数据点的比例,而不是频率。然后,我们可以叠加具有相同均值和标准差的正态概率密度:

(defn ex-2-7 []
  (let [means (->> (load-data "dwell-times.tsv")
                   (daily-mean-dwell-times)
                   (i/$ :dwell-time))
        mean (s/mean means)
        sd   (s/sd means)
        pdf  (fn [x]
               (s/pdf-normal x :mean mean :sd sd))]
    (-> (c/histogram means
                     :x-label "Daily mean dwell time (s)"
                     :nbins 20
                     :density true)
        (c/add-function pdf 80 100)
        (i/view))))

该代码生成了以下图表:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_140.jpg

绘制在直方图上的正态曲线的标准差大约是 3.7 秒。换句话说,这量化了每天均值相对于 90 秒总体均值的变异。我们可以将每天的均值看作是来自总体的样本,而前面提到的曲线表示的是样本均值的分布。因为 3.7 秒是样本均值与总体均值的差异,所以它被称为标准误差

标准误差

标准差衡量样本内部的变异量,而标准误差衡量从同一总体中抽取样本的均值之间的变异量。

注意

标准误差是样本均值分布的标准差。

我们通过查看过去 6 个月的数据经验性地计算了停留时间的标准误差。但也有一个方程式,允许我们仅通过一个样本来计算它:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_01.jpg

这里,σ[x] 是标准差,n 是样本大小。这与我们在上一章中学习的描述性统计不同。描述性统计描述的是单个样本,而标准误差试图描述样本的一般特性——即样本均值的变异量,可以预期给定大小的样本会有变异:

(defn standard-deviation [xs]
  (Math/sqrt (variance xs)))

(defn standard-error [xs]
  (/ (standard-deviation xs)
     (Math/sqrt (count xs))))

均值的标准误差与两个因素有关:

  • 样本大小

  • 总体标准差

样本大小对标准误差的影响最大。由于我们需要对样本大小取平方根,因此必须将样本大小增加四倍才能使标准误差减半。

可能会让人感到奇怪的是,总体样本的比例对标准误差的大小没有影响。这其实是好事,因为某些总体的大小可能是无限的。

样本与总体

"样本"和"总体"这两个词对统计学家来说有着非常特殊的含义。总体是研究者希望理解或从中得出结论的所有实体的集合。例如,在 19 世纪下半叶,遗传学的奠基人格雷戈尔·约翰·孟德尔记录了豌豆植物的观察数据。尽管他是在实验室中研究特定的植物,但他的目标是理解所有可能的豌豆植物中的遗传机制。

注意

统计学家将从中抽取样本的实体群体称为总体,无论被研究的对象是否为人类。

由于总体可能非常大——或像孟德尔研究的豌豆植物那样是无限的——我们必须研究具有代表性的样本,并从中推断总体。为了区分样本的可测量属性和总体的不可得属性,我们使用"统计量"一词来指代样本属性,使用"参数"一词来指代总体属性。

注意

统计量是我们可以从样本中测量的属性。参数是我们试图推断的总体属性。

事实上,统计量和参数通过在数学公式中使用不同的符号来区分:

测量样本统计量总体参数
项目数量nN
均值https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_02.jpgµ[x]
标准差S[x]σ[x]
标准误差https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_03.jpg

这里, https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_02.jpg 发音为"x-bar",µ[x] 发音为"mu x",σ[x] 发音为"sigma x"。

如果你回顾标准误差的公式,你会注意到它是从总体标准差*σ[x]而不是样本标准差S[x]*中计算得出的。这给我们带来了一个悖论——我们无法在总体参数正是我们试图推断的值时,使用总体参数来计算样本统计量。然而,实际上,当样本量大约在 30 以上时,样本标准差和总体标准差通常假设是相同的。

让我们从某一天的均值来计算标准误差。例如,假设我们选择某一天,比如 5 月 1 日:

(defn ex-2-8 []
  (let [may-1 (f/parse-local-date "2015-05-01")]
    (->> (load-data "dwell-times.tsv")
         (with-parsed-date)
         (filtered-times {:date {:$eq may-1}})
         (standard-error))))

;; 3.627

尽管我们只从一天的数据中抽取了一个样本,但我们计算出的标准误差与所有样本均值的标准差非常接近——3.6 与 3.7。就像一个包含 DNA 的细胞一样,每个样本都编码了关于整个总体的信息。

置信区间

由于我们的样本的标准误差衡量了我们预期样本均值与总体均值之间的匹配程度,我们也可以考虑其逆向——标准误差衡量了我们预期总体均值与我们测得的样本均值之间的匹配程度。换句话说,基于我们的标准误差,我们可以推断总体均值在某个预期的样本均值范围内,并且具有一定的置信度。

总的来说,“置信度”和“预期范围”共同定义了置信区间。在陈述置信区间时,通常会陈述 95% 置信区间——我们有 95% 的信心认为总体参数位于该区间内。当然,仍然有 5% 的可能性它不在其中。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_150.jpg

无论标准误差是多少,95% 的总体均值将位于样本均值的 -1.96 和 1.96 个标准差之间。因此,1.96 是 95% 置信区间的临界 z 值

注意

z-值这个名字来源于正态分布也被称为z-分布的事实。

数字 1.96 使用得非常广泛,是一个值得记住的数字,但我们也可以使用 s/quantile-normal 函数来计算临界值。我们接下来的 confidence-interval 函数期望输入一个在零到一之间的 p 值。对于我们的 95% 置信区间,这个值为 0.95。我们需要从 1 中减去它并除以 2,以计算两个尾部的大小(95% 置信区间的 2.5%):

(defn confidence-interval [p xs]
  (let [x-bar  (s/mean xs)
        se     (standard-error xs)
        z-crit (s/quantile-normal (- 1 (/ (- 1 p) 2)))]
    [(- x-bar (* se z-crit))
     (+ x-bar (* se z-crit))]))

(defn ex-2-9 []
  (let [may-1 (f/parse-local-date "2015-05-01")]
    (->> (load-data "dwell-times.tsv")
         (with-parsed-date)
         (filtered-times {:date {:$eq may-1}})
         (confidence-interval 0.95))))

;; [83.53415272762004 97.75306531749274]

结果告诉我们,我们可以有 95% 的信心认为总体均值位于 83.53 秒到 97.75 秒之间。事实上,我们之前计算的总体均值正好位于这个范围之内。

样本比较

在一次病毒式营销活动之后,AcmeContent 的网络团队从单一天的数据中为我们提供了一个停留时间的样本以供分析。他们想知道他们最新的营销活动是否吸引了更多互动的访客。置信区间为我们提供了一种直观的方式来比较这两个样本。

我们像之前一样加载来自活动的停留时间,并以相同的方式对其进行总结:

(defn ex-2-10 []
  (let [times (->> (load-data "campaign-sample.tsv")
                   (i/$ :dwell-time))]
    (println "n:      " (count times))
    (println "Mean:   " (s/mean times))
    (println "Median: " (s/median times))
    (println "SD:     " (s/sd times))
    (println "SE:     " (standard-error times))))

;; n:       300
;; Mean:    130.22
;; Median:  84.0
;; SD:      136.13370714388046
;; SE:      7.846572839994115

这个均值似乎比我们之前看到的均值要大得多——130 秒对比 90 秒。这里可能存在显著差异,尽管标准误差是我们之前一天样本的两倍多,这是由于样本量较小且标准差较大。我们可以使用和之前相同的 confidence-interval 函数基于这些数据计算总体均值的 95% 置信区间:

(defn ex-2-11 []
  (->> (load-data "campaign-sample.tsv")
       (i/$ :dwell-time)
       (confidence-interval 0.95)))

;; [114.84099983154137 145.59900016845864]

总体均值的 95%置信区间是 114.8 秒到 145.6 秒。这与我们之前计算的 90 年代的总体均值完全不重合。看起来存在一个很大的基础群体差异,单纯通过抽样误差是不太可能产生的。现在我们的任务是找出原因。

偏差

样本应当能够代表其所抽取的总体。换句话说,它应当避免产生偏差,使得某些种类的群体成员在系统性地被排除(或包含)时,相较于其他群体而言,受到不公正的影响。

一个著名的样本偏差例子是 1936 年《文学文摘》对美国总统选举的民调。这是历史上最大、最昂贵的民调之一,共有 240 万人通过邮件接受调查。结果非常明确——堪萨斯州的共和党州长阿尔弗雷德·兰登将击败富兰克林·D·罗斯福,获得 57%的选票。然而,最终罗斯福以 62%的选票赢得了选举。

该杂志巨大的抽样误差的主要原因是样本选择偏差。在试图收集尽可能多的选民地址时,《文学文摘》翻阅了电话簿、杂志订阅名单和俱乐部会员名单。在那个电话是奢侈品的时代,这一过程必然会偏向上层和中产阶级选民,无法代表整个选民群体。偏差的次要原因是无应答偏差——不到四分之一的受访者回应了调查。这是一种选择偏差,只偏向那些真正愿意参与的受访者。

避免样本选择偏差的常见方法是确保采样在某种程度上是随机的。将随机性引入过程可以减少实验因素不公平地影响样本质量的可能性。《文学文摘》民调的重点是尽可能获得最大的样本,但一个无偏的小样本比一个错误选择的大样本更有用。

如果我们打开campaign-sample.tsv文件,我们会发现我们的样本完全来自 2015 年 6 月 6 日。这一天是周末,我们可以通过clj-time轻松确认这一点:

(p/weekend? (t/date-time 2015 6 6))
;; true

到目前为止,我们的汇总统计数据都是基于我们只筛选了工作日数据的结果。这是样本中的一种偏差,如果周末的访问者行为与工作日的行为有所不同——这是一种非常可能的情况——那么我们可以说,这些样本代表了两个不同的群体。

可视化不同群体

让我们去掉工作日的筛选条件,绘制工作日和周末的日均停留时间:

(defn ex-2-12 []
  (let [means (->> (load-data "dwell-times.tsv")
                   (with-parsed-date)
                   (mean-dwell-times-by-date)
                   (i/$ :dwell-time))]
    (-> (c/histogram means
                     :x-label "Daily mean dwell time unfiltered (s)"
                     :nbins 20)
        (i/view))))

代码生成了以下直方图:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_160.jpg

这时的分布不再是正态分布。事实上,分布是双峰分布——有两个峰值。第二个较小的峰值,代表新加入的周末数据,相对较低,这既是因为周末天数少于工作日,也因为分布的标准误差较大。

注意

通常,具有多个峰值的分布称为多峰分布。它们可能表示两个或更多的正态分布被合并,因此,可能表示两个或更多的人群被合并。一个经典的双峰分布的例子是人的身高分布,因为男性的常见身高大于女性的常见身高。

周末数据与工作日数据具有不同的特征。我们应确保在比较时是“对比相同的事物”。让我们将原始数据集过滤,仅保留周末数据:

(defn ex-2-13 []
  (let [weekend-times (->> (load-data "dwell-times.tsv")
                           (with-parsed-date)
                           (i/$where {:date {:$fn p/weekend?}})
                           (i/$ :dwell-time))]
    (println "n:      " (count weekend-times))
    (println "Mean:   " (s/mean weekend-times))
    (println "Median: " (s/median weekend-times))
    (println "SD:     " (s/sd weekend-times))
    (println "SE:     " (standard-error weekend-times))))

;; n:       5860
;; Mean:    117.78686006825939
;; Median:  81.0
;; SD:      120.65234077179436
;; SE:      1.5759770362547665

周末的总体均值(基于 6 个月的数据)为 117.8 秒,落在市场样本的 95%置信区间内。换句话说,虽然 130 秒是较高的均值,甚至对于周末来说,但这一差异并不大到无法仅归因于样本内的随机波动。

我们刚才采取的建立人群差异(即周末访问者与工作日访问者之间的差异)的方法,并不是统计检验常规的做法。更常见的方法是从一个理论开始,然后用数据检验这个理论。统计方法为此定义了一种严格的流程,称为假设检验

假设检验

假设检验是统计学家和数据科学家的正式过程。假设检验的标准方法是定义一个研究领域,决定哪些变量是必要的以测量所研究的内容,然后提出两个竞争的假设。为了避免只看符合我们偏见的数据,研究人员会事先明确陈述他们的假设。然后,统计数据可以用来根据数据确认或否定这个假设。

为了帮助留住我们的访客,设计师开始修改主页,采用所有最新的技术来吸引观众的注意力。我们希望确保我们的努力不会白费,因此我们将关注新网站上的停留时间是否有所增加。

因此,我们的研究问题是“新网站是否导致访客的停留时间增加”?我们决定用均值停留时间来进行检验。现在,我们需要列出两个假设。按惯例,数据被假设不包含研究者所寻找的内容。保守的观点是,数据不会显示任何异常。这就是零假设,通常用*H[0]*表示。

注意

假设检验假定原假设为真,直到证据的权重使得这个假设变得不太可能。这种“倒过来看”证据的方法部分是由一个简单的心理事实驱动的:当人们去寻找某样东西时,他们倾向于找到它。

研究者随后形成一个备择假设,表示为H[1]。这可能仅仅意味着总体均值与基准值不同。或者,它可能意味着总体均值大于或小于基准值,甚至可能大于或小于某个特定值。我们希望测试新网站是否能增加停留时间,因此这些将是我们的原假设和备择假设:

  • H[0]:新网站的停留时间与现有网站的停留时间没有区别。

  • H[1]:新网站的停留时间相比于现有网站更长。

我们的保守假设是新网站对用户停留时间没有影响。原假设不一定是“无效假设”(即没有效应),但在这种情况下,我们没有合理的理由假设它有所不同。如果样本数据不支持原假设(如果数据与其预测的差异过大,不可能仅由偶然造成),我们将拒绝原假设,并提出备择假设作为最好的替代解释。

在设定原假设和备择假设后,我们必须设定一个显著性水平,用来衡量我们是否在寻找某种效应。

显著性

显著性检验最初是独立于假设检验开发的,但如今这两种方法常常一起使用。显著性检验的目的是设定一个阈值,超过该阈值我们认为观察到的数据不再支持原假设。

因此,存在两个风险:

  • 我们可能会接受一个差异是显著的,而实际上它可能是偶然产生的。

  • 我们可能会将一个差异归因于偶然,而实际上它代表了一个真正的总体差异。

这两种可能性分别被称为第一类错误和第二类错误:

H[0] 错误H[0] 正确
拒绝 H[0]真阴性第一类错误
接受 H[0]第二类错误真阳性

我们减少第一类错误的风险时,第二类错误的风险就会增加。换句话说,我们希望避免在没有真实差异时错误地认为有差异,因此我们要求样本之间有更大的差异才能宣称统计显著性。这增加了我们忽略真正差异的可能性。

统计学家常用两个显著性阈值。这些分别是 5%的显著性水平和 1%的显著性水平。5%的差异通常被称为显著,而 1%的差异被称为高度显著。阈值的选择通常用希腊字母α(α)在公式中表示。由于找不到效果可能被认为是失败(无论是实验失败还是新网站失败),我们可能会倾向于调整α,直到我们发现效果为止。因此,教科书中的显著性检验方法要求我们在查看数据之前就设定显著性水平。5%的水平通常是默认选择的,所以我们也选择这个水平。

测试新网站设计

AcmeContent 的网络团队一直在努力开发一个新的网站,目的是鼓励访客停留更长时间。他们采用了所有最新的技术,因此我们非常有信心这个网站能够显著提高停留时间。

AcmeContent 并不打算一次性将其推出给所有用户,而是希望先在一小部分访客中进行测试。我们已经向他们讲解了样本偏差,因此网络团队将网站流量的 5%随机导向新网站,持续一天。结果以单个文本文件的形式提供给我们,文件中包含当天所有的流量数据。每行显示的是一个访客的停留时间,并给出一个值,"0"表示他们使用了原始网站设计,"1"表示他们看到了新的(并且希望能改进的)网站。

进行 z 检验

在之前使用置信区间进行测试时,我们有一个单一的总体均值来进行比较。

通过z检验,我们可以选择比较两个样本。观看新网站的人是随机分配的,且两个组的数据是在同一天收集的,以排除其他时间相关因素的干扰。

由于我们有两个样本,因此我们也有两个标准误差。z 检验是基于合并标准误差进行的,合并标准误差只是将方差的和除以样本大小后再开方。这与我们将样本合并后的标准误差相同:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_04.jpg

在这里,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_05.jpg是样本a的方差,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_06.jpg是样本b的方差。n[a]n[b]分别是样本ab的样本大小。合并标准误差可以像这样在 Clojure 中计算:

(defn pooled-standard-error [a b]
  (i/sqrt (+ (/ (i/sq (standard-deviation a)) (count a))
             (/ (i/sq (standard-deviation b)) (count b)))))

为了确定我们所看到的差异是否异常大,我们可以计算观测到的均值差异与合并标准误差的比值。这个值被赋予变量名z

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_07.jpg

使用我们的pooled-standard-error函数,可以像这样计算z统计量:

(defn z-stat [a b]
  (-> (- (mean a)
         (mean b))
      (/ (pooled-standard-error a b))))

z比率反映了均值相差的大小,相对于标准误差的期望量。因此,z-统计量告诉我们均值之间相差多少个标准误差。由于标准误差服从正态分布,我们可以通过查找标准正态累积分布(CDF)来将这个差异与概率关联起来:

(defn z-test [a b]
  (s/cdf-normal (z-stat a b)))

以下示例使用z-检验来比较两个网站的性能。我们通过按网站分组行,将网站索引到网站行集合的映射。我们调用map-vals(partial map :dwell-time)一起,将行集合转换为停留时间集合。map-vals是 Medley(github.com/weavejester/medley)库中定义的一个轻量级工具函数:

(defn ex-2-14 []
    (let [data (->> (load-data "new-site.tsv")
                    (:rows)
                    (group-by :site)
                    (map-vals (partial map :dwell-time)))
          a (get data 0)
          b (get data 1)]
      (println "a n:" (count a))
      (println "b n:" (count b))
      (println "z-stat: " (z-stat a b))
      (println "p-value:" (z-test a b))))

;; a n: 284
;; b n: 16
;; z-stat:  -1.6467438180091214
;; p-value: 0.049805356789022426

设置 5%的显著性水平就像设置 95%的置信区间本质上是一样的。实质上,我们在查看观察到的差异是否落在 95%的置信区间之外。如果是这样,我们可以声称找到了一个在 5%的显著性水平上显著的结果。

注意

p-值是指在原假设真实的情况下,错误地拒绝原假设的概率。p-值越小,我们越能确信原假设是错误的,并且我们发现了一个真正的效应。

这段代码返回的值为 0.0498,等于 4.98%。因为这个值略低于我们的显著性阈值 5%,所以我们可以声称我们发现了显著的结果。

让我们回顾一下原假设和备择假设:

  • H[0]: 新网站的停留时间与现有网站的停留时间没有差异。

  • H[1]: 新网站的停留时间比现有网站的停留时间更长。

我们的备择假设是新网站的停留时间更长。

我们准备声称统计显著性,并且新网站的停留时间比现有网站的停留时间更长,但我们遇到一个问题——样本量较小时,样本标准差与总体标准差匹配的不确定性增加。我们的新网站样本只有 16 位访问者,如前面例子中的输出所示。像这样的样本量会使得标准误差服从正态分布的假设无效。

幸运的是,有一种统计检验和相应的分布可以模型化样本量较小时标准误差的不确定性增大。

斯图登特的 t 分布

t-分布由威廉·西利·戈塞特(William Sealy Gossett)推广,他是爱尔兰吉尼斯啤酒厂的化学家,他将其应用于他的 Stout 分析中。

注意

威廉·戈塞特在 1908 年在《Biometrika》上发表了这个检验,但由于他的雇主认为他们使用统计学是商业机密,戈塞特被迫使用笔名。他选择的笔名是“学生(Student)”。

虽然正态分布完全由两个参数——均值和标准差描述,但t分布只由一个参数描述,即自由度。自由度越大,t分布越接近均值为零、标准差为一的正态分布。当自由度减小时,分布变得更宽,尾部比正态分布更胖。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_170.jpg

前面的图表显示了t分布相对于正态分布在不同自由度下的变化。较小样本量对应着更胖的尾部,这意味着观察到大偏差的概率更高。

自由度

自由度,通常缩写为df,与样本大小密切相关。它是一个有用的统计量,也是一个直观的系列属性,可以通过简单的例子进行演示。

如果你被告知两个值的均值为 10 且其中一个值为 8,你不需要任何额外的信息就能推断出另一个值是 12。换句话说,对于样本量为 2 且均值已知的情况,一旦知道其中一个值,另一个值就有约束。

如果你被告知三个值的均值为 10 且第一个值也是 10,你就无法推断出剩余两个值是什么。因为有无限多个以 10 为起始值且均值为 10 的三个数字集合,在你能推断出第三个值之前,必须先指定第二个值。

对于任何三个数字的集合,约束是简单的:你可以自由选择前两个数字,但最后一个数字是有约束的。自由度可以通过以下方式概括:对于任何单一样本,自由度为样本大小减一。

在比较两个数据样本时,自由度是样本大小总和减去 2,即等于它们各自自由度的总和。

t 统计量

在使用t分布时,我们查找t统计量。与z统计量类似,该值量化了某一特定观察偏差的不太可能性。对于双样本t检验,t 统计量的计算方式如下:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_08.jpg

这里,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_09.jpg是合并标准误差。我们可以像之前一样计算合并标准误差:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_10.jpg

然而,该方程假设已知总体参数σ[a]σ[b],这些只能通过大样本来近似。t检验是为小样本设计的,因此不需要我们对总体方差做出假设。

因此,对于t检验,我们将合并的标准误差写为标准误差的平方和的平方根:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_11.jpg

实际上,前面提到的两条关于合并标准误差的公式在给定相同输入数据的情况下会得到相同的结果。符号的不同仅仅是为了说明在t检验中,我们只依赖样本统计量作为输入。合并标准误差https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_09.jpg可以通过以下方式计算:

(defn pooled-standard-error [a b]
  (i/sqrt (+ (i/sq (standard-error a))
             (i/sq (standard-error b)))))

尽管在数学符号上有所不同,但实际上,计算t-统计量与计算z-统计量是相同的:

(def t-stat z-stat)

(defn ex-2-15 []
    (let [data (->> (load-data "new-site.tsv")
                    (:rows)
                    (group-by :site)
                    (map-vals (partial map :dwell-time)))
          a (get data 0)
          b (get data 1)]
      (t-stat a b)))

;; -1.647

这两种统计量之间的差异是概念性的,而非算法性的——z-统计量仅适用于样本遵循正态分布的情况。

执行 t 检验

t-检验的工作方式的不同,源自用于计算p值的概率分布。计算了我们的t-统计量后,我们需要根据数据的自由度查找t-分布中的值:

(defn t-test [a b]
  (let [df (+ (count a) (count b) -2)]
    (- 1 (s/cdf-t (i/abs (t-stat a b)) :df df))))

自由度是样本总量减去二,即我们样本的自由度是 298。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_180.jpg

回想一下,我们正在进行假设检验。因此,首先让我们陈述零假设和备择假设:

  • H[0]:此样本来自具有给定均值的总体

  • H[1]:此样本来自具有更大均值的总体

让我们运行这个示例:

(defn ex-2-16 []
  (let [data (->> (load-data "new-site.tsv")
                  (:rows)
                  (group-by :site)
                  (map-vals (partial map :dwell-time)))
        a (get data 0)
        b (get data 1)]
    (t-test a b)))

;; 0.0503

这返回一个超过 0.05 的p值。由于这个值大于我们为假设检验设置的 5% α,我们无法拒绝零假设。我们对于均值差异的检验没有通过t检验发现显著差异。因此,z-检验所得到的微弱显著结果部分是由于样本量太小。

双尾检验

在我们的备择假设中,隐含假设新站点的表现会优于旧站点。假设检验的过程会尽可能避免在寻找统计显著性时暗中引入隐性假设。

只关注数量显著增加或减少的检验被称为单尾检验,通常是不被推荐的,除非发生相反方向的变化是不可行的。这个名字来源于单尾检验将所有的α分配到分布的一个尾部。通过不检验另一方向,该检验可以更有力地拒绝零假设,并在本质上降低判断结果显著性的门槛。

注意

统计功效是正确接受备择假设的概率。这可以被认为是检验发现效应的能力,在效应存在的情况下。

尽管更高的统计功效听起来是理想的,但它也意味着发生第一类错误的概率更大。一个更正确的方法是考虑新站点可能比现有站点差的可能性。这将我们的α值平分到分布的两个尾部,并确保结果的显著性不受先前假设改进的偏倚。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_190.jpg

事实上,Incanter 已经提供了执行双样本t检验的函数,即s/t-test函数。我们将数据样本作为第一个参数传递,另一个样本则通过:y关键字传递给函数进行比较。Incanter 会假设我们要进行双尾检验,除非我们传递:alternative关键字并指定:greater:lower,此时将进行单尾检验。

(defn ex-2-17 []
  (let [data (->> (load-data "new-site.tsv")
                  (:rows)
                  (group-by :site)
                  (map-vals (partial map :dwell-time)))
        a (get data 0)
        b (get data 1)]
    (clojure.pprint/print (s/t-test a :y b))))

;; {:p-value 0.12756432502462456,
;;  :df 17.7613823496861,
;;  :n2 16,
;;  :x-mean 87.95070422535211,
;;  :y-mean 122.0,
;;  :x-var 10463.941024237305,
;;  :conf-int [-78.9894629402365 10.890871390940724],
;;  :y-var 6669.866666666667,
;;  :t-stat -1.5985205593851322,
;;  :n1 284}

Incanter 的t检验返回了大量信息,包括p-值。这个p-值大约是我们为单尾检验计算值的两倍。事实上,它不是恰好是两倍,唯一的原因是 Incanter 实现了一种稍有变异的t检验,叫做Welch’s t-test,当两个样本的标准差不同时,这种检验略微更强健。由于我们知道对于指数分布,均值和方差是密切相关的,因此这个检验应用起来稍微严格一些,并返回一个更低的显著性水平。

单样本 t 检验

独立样本的t检验是最常见的统计分析方法,它提供了一种非常灵活和通用的方式,用于比较两个样本是否代表相同或不同的总体。然而,在总体均值已知的情况下,还有一种更简单的检验,即由s/simple-t-test表示。

我们通过:mu关键字传递一个样本和一个总体均值进行检验。因此,如果我们仅仅想看看我们新站点的平均停留时间是否显著不同于先前 90 秒的总体均值,我们可以进行如下测试:

(defn ex-2-18 []
  (let [data (->> (load-data "new-site.tsv")
                  (:rows)
                  (group-by :site)
                  (map-vals (partial map :dwell-time)))
        b (get data 1)]
    (clojure.pprint/pprint (s/t-test b :mu 90))))

;; {:p-value 0.13789520958229406,
;;  :df 15,
;;  :n2 nil,
;;  :x-mean 122.0,
;;  :y-mean nil,
;;  :x-var 6669.866666666667,
;;  :conf-int [78.48152745280898 165.51847254719104],
;;  :y-var nil,
;;  :t-stat 1.5672973291495713,
;;  :n1 16}

simple-t-test函数不仅返回检验的p-值,还会返回总体均值的置信区间。它很宽,从 78.5 秒到 165.5 秒,显然与我们测试中的 90 秒有重叠。这也解释了为什么我们无法拒绝原假设。

重采样

为了直观地理解t检验如何从如此少的数据中确认和计算这些统计量,我们可以应用一种叫做重采样的方法。重采样的前提是每个样本只是从一个总体中可能出现的无限多个样本之一。通过从现有样本中多次重新抽取样本,我们可以更好地理解这些其他样本的性质,从而更清楚地理解底层的总体。

实际上有几种重抽样技术,我们将讨论其中一种最简单的方法——自助法(bootstrapping)。在自助法中,我们通过反复从原始样本中随机抽取值并进行有放回抽样,直到生成一个与原始样本大小相同的新样本。由于每次随机选择后都会放回原始值,因此相同的源值可能会在新样本中出现多次。就好像我们在从一副扑克牌中反复抽取随机卡片,但每次抽完都会将卡片放回。偶尔,我们会再次抽到之前选过的卡片。

我们可以在 Incanter 中轻松地对样本进行自助法重抽样,利用bootstrap函数生成多个重抽样。bootstrap函数接受两个参数——原始样本和一个汇总统计量(该统计量将在重抽样样本上计算),以及一些可选参数——:size(需要计算的重抽样样本数量,每个样本的大小与原始样本相同)、:smooth(是否对离散统计量(如中位数)的输出进行平滑处理)、:smooth-sd:replacement,默认为true

(defn ex-2-19 []
  (let [data (->> (load-data "new-site.tsv")
                  (i/$where {:site {:$eq 1}})
                  (i/$ :dwell-time ))]
    (-> (s/bootstrap data s/mean :size 10000)
        (c/histogram :nbins 20
                     :x-label "Bootstrapped mean dwell times (s)")
        (i/view))))

让我们用直方图来可视化输出结果:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_200.jpg

直方图显示了随着重复(重)抽样新站点停留时间值的变化,均值的变化情况。尽管输入的数据只有一个 16 位访客的样本,但自助法重抽样清晰地模拟了原始样本的标准误差,并可视化了我们之前通过单一样本 t 检验计算出的置信区间(78 秒到 165 秒)。

通过自助法重抽样,尽管我们的输入只有一个样本,我们通过多次抽样进行了模拟。这是一种广泛有用的技术,可以估计那些我们无法或者不知道如何进行解析计算的参数。

测试多个设计

令人失望的是,我们发现新站点设计并未显著提高用户的停留时间。不过,我们在向全球推出之前,在一小部分用户中发现了这一问题,也算是幸运的。

不灰心丧气,AcmeContent 的网页团队加班加点,设计了一套替代的站点方案。通过从其他设计中汲取最佳元素,他们提出了 19 个变体供测试。加上我们的原始站点(作为对照组),一共有 20 个不同的站点来吸引访客。

计算样本均值

网页团队将 19 个新设计和原始设计一起部署。正如之前所提到的,每个设计都会随机分配 5%的访客。我们让测试运行 24 小时。

第二天,我们收到了一份文件,展示了每个站点设计的访客停留时间。每个设计都有一个编号,其中0代表原始未更改的设计,119代表其他设计:

(defn ex-2-20 []
  (->> (i/transform-col (load-data "multiple-sites.tsv")
                        :dwell-time float)
       (i/$rollup :mean :dwell-time :site)
       (i/$order :dwell-time :desc)
       (i/view)))

这段代码生成了如下表格:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_210.jpg

我们希望测试每个站点设计,以查看是否有任何生成统计学显著结果。为此,我们可以通过以下方式将各个站点进行比较:

(defn ex-2-21 []
  (let [data (->> (load-data "multiple-sites.tsv")
                  (:rows)
                  (group-by :site)
                  (map-vals (partial map :dwell-time)))
        alpha 0.05]
    (doseq [[site-a times-a] data
            [site-b times-b] data
            :when (> site-a site-b)
            :let [p-val (-> (s/t-test times-a :y times-b)
                            (:p-value))]]
      (when (< p-val alpha)
        (println site-b "and" site-a
                 "are significantly different:"
                 (format "%.3f" p-val))))))

然而,这并不是一个好主意。即使这些差异是偶然发生的,我们也很可能会看到在表现特别好的页面与表现特别差的页面之间存在统计学差异。如果你运行前面的示例,你会看到许多页面在统计学上彼此存在差异。

另外,我们可以将每个站点与我们当前的基准进行比较——即目前我们网站的平均停留时间为 90 秒:

(defn ex-2-22 []
  (let [data (->> (load-data "multiple-sites.tsv")
                  (:rows)
                  (group-by :site)
                  (map-vals (partial map :dwell-time)))
        baseline (get data 0)
        alpha 0.05]
    (doseq [[site-a times-a] data
            :let [p-val (-> (s/t-test times-a :y baseline)
                            (:p-value))]]
      (when (< p-val alpha)
        (println site-a
                 "is significantly different from baseline:"
                 (format "%.3f" p-val))))))

这个测试确定了两个站点与基准值有显著差异:

;; 6 is significantly different from baseline: 0.007
;; 10 is significantly different from baseline: 0.006

较小的p值(小于 1%)表示存在非常显著的统计学差异。这看起来非常有前景,但我们有一个问题。我们已经对 20 个数据样本进行了t检验,设定α为 0.05。α的定义是错误拒绝原假设的概率。通过进行 20 次t检验,实际上有可能错误地拒绝至少一个页面的原假设。

通过像这样一次性比较多个页面,我们使得t检验的结果失效。解决在统计检验中进行多重比较的问题,存在多种替代技术,我们将在后面的章节中介绍这些方法。

多重比较

事实上,随着重复试验,我们增加了发现显著效应的概率,这就是多重比较问题。通常,解决该问题的方法是,在比较多个样本时要求更显著的效应。然而,这个问题并没有简单的解决方案;即使设置α为 0.01,我们仍然会在平均 1%的时间内犯第一类错误。

为了帮助我们更直观地理解多重比较和统计显著性之间的关系,接下来我们将构建一个互动网页,来模拟进行多次采样的效果。使用像 Clojure 这样强大且通用的编程语言进行数据分析的优势之一就是,我们可以在多种环境中运行我们的数据处理代码。

我们目前为本章编写并运行的代码是为 Java 虚拟机编译的。但自 2013 年以来,我们的编译代码有了一个替代目标环境:网页浏览器。ClojureScript 将 Clojure 的应用范围进一步扩展到了任何具有 JavaScript 功能的网页浏览器的计算机。

引入模拟

为了帮助可视化与多重显著性检验相关的问题,我们将使用 ClojureScript 构建一个交互式模拟,寻找从两个指数分布中随机抽样的样本之间的统计显著差异。为了观察其他因素如何与我们的假设检验相关联,我们的模拟将允许我们改变两个分布的基础总体均值,以及设置样本大小和所需的置信水平。

如果你已下载本章的示例代码,你将在资源目录中看到一个 index.html 文件。如果你在浏览器中打开这个文件,你应该看到一个提示信息,提示你编译 JavaScript。我们可以使用名为 cljsbuild 的 Leiningen 插件来做到这一点。

编译模拟

cljsbuild 是一个将 ClojureScript 编译为 JavaScript 的 Leiningen 插件。要使用它,我们只需让编译器知道我们希望将 JavaScript 文件输出到哪里。Clojure 代码输出为 .jar 文件(即 Java 存档),而 ClojureScript 输出为单个 .js 文件。我们通过 project.clj 文件中的 :cljsbuilds 部分指定输出文件的名称和编译器设置。

该插件可以通过命令行访问,命令为 lein cljsbuild。在项目根目录中运行以下命令:

lein cljsbuild once

此命令将为我们编译一个 JavaScript 文件。另一种替代命令如下:

lein cljsbuild auto

上述内容将编译代码,但会保持活跃,监控源文件的更改。如果这些文件中的任何一个被更新,输出将会重新编译。

现在打开 resources/index.html 文件在浏览器中查看 JavaScript 的效果。

浏览器模拟

一个 HTML 页面已被提供在示例项目的资源目录中。在任何现代浏览器中打开该页面,你应该能看到类似以下的图像:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_220.jpg

页面左侧显示了两个样本的双重直方图,这些样本都来自指数分布。样本生成的总体均值由网页右上角标记为 参数 的框中的滑块控制。在直方图下方是一个图表,显示基于样本的总体均值的两个概率密度。这些值是通过 t 分布计算的,参数是样本的自由度。在这些滑块下方,在标记为 设置 的框中,有另一对滑块用于设置样本大小和置信区间。调整置信区间会裁剪 t 分布的尾部;在 95% 置信区间下,只有概率分布的中央 95% 会被显示。最后,在标记为 统计数据 的框中,显示了两个样本的均值的滑块。这些值不能更改;它们是从样本中测量得出的。一个标记为 新样本 的按钮可以用来生成两个新的随机样本。观察每次生成新样本对样本均值的波动。不断生成样本,你偶尔会看到样本均值之间有显著差异,即使底层总体均值相同。

在我们探索不同样本大小和置信度对不同总体均值的影响时,让我们看看如何使用 jStatReagentB1 库构建这个模拟。

jStat

由于 ClojureScript 编译成 JavaScript,我们不能使用有 Java 依赖的库。Incanter 强烈依赖几个底层 Java 库,因此我们必须找到一个替代 Incanter 的浏览器端统计分析工具。

注意

在构建 ClojureScript 应用程序时,我们不能使用依赖于 Java 库的库,因为它们在执行我们代码的 JavaScript 引擎中不可用。

jStat (github.com/jstat/jstat) 是一个 JavaScript 统计库。它提供了根据特定分布生成序列的函数,包括指数分布和 t 分布。

要使用它,我们必须确保它在我们的网页上可用。我们可以通过链接到远程内容分发网络 (CDN) 或者自己托管文件来实现这一点。链接到 CDN 的好处是,曾经为另一个网站下载过 jStat 的访客可以使用他们的缓存版本。然而,由于我们的模拟是为本地使用而设计的,我们已经将文件包含在内,以确保即使浏览器离线,页面也能正常工作。

jstat.min.js 文件已下载到 resources/js/vendor 目录中。该文件通过标准 HTML 标签加载到 index.html 的主体部分。

为了利用 jStat 的分布生成函数,我们必须从 ClojureScript 与 JavaScript 库进行交互。与 Java 互操作一样,Clojure 提供了务实的语法来与用主机语言编写的库进行交互。

jStat提供了各种分布,可以在jstat.github.io/distributions.html找到相关文档。要从指数分布生成样本,我们可以调用jStat.exponential.sample(lambda)函数。与 JavaScript 的互操作非常简单;我们只需在表达式前加上js/,以确保访问 JavaScript 的命名空间,并调整括号的位置:

(defn randexp [lambda]
  (js/jStat.exponential.sample lambda))

一旦我们能够从指数分布中生成样本,创建一个懒加载样本序列将变得简单,只需要重复调用该函数:

(defn exponential-distribution [lambda]
  (repeatedly #(randexp lambda)))

ClojureScript 几乎暴露了 Clojure 的所有功能,包括懒加载序列。请参考本书的 wiki wiki.clojuredatascience.com,获取关于 JavaScript 互操作的资源链接。

B1

现在我们可以在 ClojureScript 中生成数据样本,我们希望能够将它们绘制在直方图上。我们需要一个纯 Clojure 的替代方案,用于绘制可以在网页上访问的直方图;B1 库(github.com/henrygarner/b1)正提供了这样的功能。这个名字源于它是从 ClojureScript 库C2改编和简化而来的,而C2又是流行的 JavaScript 数据可视化框架D3的简化版。

我们将使用 B1 在b1.charts中的简单工具函数,将数据构建为 ClojureScript 中的直方图。B1 并不强制要求特定的显示格式;我们可以使用它在 canvas 元素上绘制,或者甚至直接从 HTML 元素中构建图表。然而,B1 确实包含将图表转换为 SVG 的函数,这些图表可以在所有现代网页浏览器中显示。

可伸缩矢量图形(Scalable Vector Graphics)

SVG 代表可伸缩矢量图形(Scalable Vector Graphics),定义了一组表示绘图指令的标签。SVG 的优势在于,结果可以在任何尺寸下渲染,而不会像按比例放大的光栅(基于像素的)图形那样模糊。另一个好处是现代浏览器知道如何渲染 SVG 绘图指令,并直接在网页中生成图像,还可以使用 CSS 对图像进行样式化和动画处理。

虽然对 SVG 和 CSS 的详细讨论超出了本书的范围,但 B1 确实提供了类似 Incanter 的语法,用于使用 SVG 构建简单的图表和图形。给定一组值,我们可以调用c/histogram函数将其转换为数据结构的内部表示。我们可以使用c/add-histogram函数添加额外的直方图,并调用svg/as-svg将图表渲染为 SVG 表示形式:

(defn sample-histograms [sample-a sample-b]
  (-> (c/histogram sample-a :x-axis [0 200] :bins 20)
      (c/add-histogram sample-b)
      (svg/as-svg :width 550 :height 400)))

与 Incanter 不同,当我们选择渲染直方图时,我们还必须指定图表的期望宽度和高度。

绘制概率密度

除了使用 jStat 从指数分布中生成样本外,我们还将使用它来计算t分布的概率密度。我们可以构建一个简单的函数来封装jStat.studentt.pdf(t, df)函数,提供正确的t统计量和自由度来参数化分布:

(defn pdf-t [t & {:keys [df]}]
  (js/jStat.studentt.pdf t df))

使用 ClojureScript 的一个优势是我们已经编写了计算样本t统计量的代码。这段代码在 Clojure 中可以正常工作,并且可以在不做任何更改的情况下编译为 ClojureScript:

(defn t-statistic [test {:keys [mean n sd]}]
  (/ (- mean test)
     (/ sd (Math/sqrt n))))

为了渲染概率密度,我们可以使用 B1 的c/function-area-plot。这将根据一个由函数描述的线生成面积图。提供的函数只需要接受一个x并返回相应的y

一个小小的复杂性是我们返回的y值对于不同的样本会有所不同。这是因为t-pdf在样本均值处(对应于t统计量为零)最高。因此,我们需要为每个样本生成一个不同的函数来传递给function-area-plot。这通过probability-density函数来实现,如下所示:

(defn probability-density [sample alpha]
  (let [mu (mean sample)
        sd (standard-deviation sample)
        n  (count sample)]
    (fn [x]
      (let [df     (dec (count sample))
            t-crit (threshold-t 2 df alpha)
            t-stat (t-statistic x {:mean mu
                                   :sd sd
                                   :n n})]
        (if (< (Math/abs t-stat) t-crit)
          (pdf-t t-stat :df df)
          0)))))

在这里,我们定义了一个高阶函数probability-density,它接受一个单一值sample。我们计算一些简单的汇总统计量,然后返回一个匿名函数,该函数计算分布中给定值的概率密度。

这个匿名函数将传递给function-area-plot。它接受一个x并计算给定样本的t统计量。返回的y值是与t统计量相关的t分布的概率:

(defn sample-means [sample-a sample-b alpha]
  (-> (c/function-area-plot (probability-density sample-a alpha)
                            :x-axis [0 200])
      (c/add-function (probability-density sample-b alpha))
      (svg/as-svg :width 550 :height 250)))

与直方图一样,生成多个图表和调用add-function一样简单,只需提供图表和我们想要添加的新函数。

状态和 Reagent

在 ClojureScript 中,状态的管理方式与 Clojure 应用程序相同——通过使用原子、引用或代理。原子提供对单一身份的非协调、同步访问,是存储应用状态的绝佳选择。使用原子确保应用始终看到数据的一致视图。

Reagent 是一个 ClojureScript 库,它提供了一种机制,用于在原子值发生变化时更新网页内容。标记和状态是绑定在一起的,因此每当应用程序状态更新时,标记将重新生成。

Reagent 还提供了语法,用于使用 Clojure 数据结构以惯用方式渲染 HTML。这意味着页面的内容和交互性可以使用一种语言来处理。

更新状态

数据保存在 Reagent atom 中,更新状态是通过调用swap!函数实现的,该函数接受两个参数——我们希望更新的 atom 和一个函数,该函数用来转换 atom 的状态。提供的函数需要接受 atom 的当前状态并返回新的状态。感叹号表示该函数具有副作用,在这里副作用是可取的;除了更新 atom 之外,Reagent 还会确保我们的 HTML 页面中的相关部分得到更新。

指数分布有一个单一的参数——速率,表示为λ(lambda)。指数分布的速率是均值的倒数,因此我们通过计算(/ 1 mean-a)来将其作为参数传递给指数分布函数:

(defn update-sample [{:keys [mean-a mean-b sample-size]
                      :as state}]
  (let [sample-a (->> (float (/ 1 mean-a))
                      (exponential-distribution)
                      (take sample-size))
        sample-b (->> (float (/ 1 mean-b))
                      (exponential-distribution)
                      (take sample-size))]
    (-> state
        (assoc :sample-a sample-a)
        (assoc :sample-b sample-b)
        (assoc :sample-mean-a (int (mean sample-a)))
        (assoc :sample-mean-b (int (mean sample-b))))))

(defn update-sample! [state]
  (swap! state update-sample))

在前面的代码中,我们定义了一个update-sample函数,它接受一个包含:sample-size:mean-a:mean-b的映射,并返回一个包含相关新样本和样本均值的新映射。

update-sample函数是纯函数,意思是它没有副作用,这使得它更容易测试。update-sample!函数通过调用swap!来封装它。Reagent 确保任何依赖于该 atom 中值的代码在 atom 中的值发生变化时都会执行。这导致我们的界面在新样本的响应下重新渲染。

绑定界面

为了将界面绑定到状态,Reagent 定义了一个render-component函数。这个函数将一个特定的函数(在此为我们的layout-interface函数)与一个特定的 HTML 节点(页面上 ID 为root的元素)连接起来:

(defn layout-interface []
  (let [sample-a (get @state :sample-a)
        sample-b (get @state :sample-b)
        alpha (/ (get @state :alpha) 100)]
    [:div
     [:div.row
      [:div.large-12.columns
       [:h1 "Parameters & Statistics"]]]
     [:div.row
      [:div.large-5.large-push-7.columns
       [controllers state]]
      [:div.large-7.large-pull-5.columns {:role :content}
       [sample-histograms sample-a sample-b]
       [sample-means sample-a sample-b alpha]]]]))

(defn run []
  (r/render-component
   [layout-interface]
   (.getElementById js/document "root")))

我们的layout-interface函数包含了作为嵌套 Clojure 数据结构表示的 HTML 标记。在对:div:h1的调用之间,有两个调用我们自己的sample-histogramssample-means函数。它们将被替换为它们的返回值——直方图的 SVG 表示以及均值的概率密度。

为了简洁起见,我们省略了controllers函数的实现,它处理滑块和新建样本按钮的渲染。请查阅示例代码中的cljds.ch2.app命名空间,查看这是如何实现的。

模拟多个测试

每次按下新建 样本按钮时,都会生成一对来自指数分布的新样本,人口均值来自滑块。样本会被绘制在直方图上,并且在下面会绘制一个概率密度函数,显示样本的标准误差。当置信区间发生变化时,可以观察到标准误差的可接受偏差也会发生变化。

每次按下按钮时,我们可以将其视为一个显著性检验,alpha 设置为置信区间的补充值。换句话说,如果样本均值的概率分布在 95%的置信区间内重叠,我们就无法在 5%的显著性水平下拒绝零假设。

请观察,即使总体均值相同,均值偶尔也会发生较大的偏差。当样本差异超过我们的标准误差时,我们可以接受备择假设。在 95%的置信水平下,即使分布的总体均值相同,我们也会在 20 次试验中发现约一次显著结果。当这种情况发生时,我们正在犯第一类错误,即将抽样误差误认为是真正的总体差异。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_230.jpg

尽管总体参数相同,但偶尔会观察到较大的样本差异。

邦费罗尼校正

因此,在进行多重测试时,我们需要一种替代方法,以应对通过重复试验发现显著效应的概率增加。邦费罗尼校正是一种非常简单的调整方法,确保我们不太可能犯第一类错误。它通过调整我们测试的显著性水平(alpha)来实现这一点。

该调整非常简单——邦费罗尼校正只需将我们期望的显著性水平(alpha)除以我们进行的测试数量。例如,如果我们有k个网站设计要测试,并且实验的显著性水平为0.05,则邦费罗尼校正公式为:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_12.jpg

这是减少在多重测试中发生第一类错误概率增加的安全方法。以下示例与ex-2-22相同,不同之处在于显著性水平的值已被除以组的数量:

(defn ex-2-23 []
  (let [data (->> (load-data "multiple-sites.tsv")
                  (:rows)
                  (group-by :site)
                  (map-vals (partial map :dwell-time)))
        alpha (/ 0.05 (count data))]
    (doseq [[site-a times-a] data
            [site-b times-b] data
            :when (> site-a site-b)
            :let [p-val (-> (s/t-test times-a :y times-b)
                            (:p-value))]]
      (when (< p-val alpha)
        (println site-b "and" site-a
                 "are significantly different:"
                 (format "%.3f" p-val))))))

如果你运行前面的示例,你会发现使用邦费罗尼校正后,任何页面都不再被视为统计显著。

显著性检验是一项平衡工作——我们降低第一类错误的几率时,第二类错误的风险会增大。邦费罗尼校正非常保守,因此有可能由于过于谨慎,我们错过了真正的差异。

在本章的最后部分,我们将研究一种替代的显著性检验方法,这种方法在减少第一类错误和第二类错误之间取得平衡,同时允许我们同时测试所有的 20 个页面。

方差分析

方差分析,通常缩写为ANOVA,是一系列用于衡量组间差异的统计显著性的方法。它由极具天赋的统计学家罗纳德·费舍尔(Ronald Fisher)开发,他通过在生物学实验中的应用推广了显著性检验。

我们的测试,使用z-统计量和t-统计量,主要集中在样本均值上,作为区分两个样本的主要机制。在每种情况下,我们都寻找均值的差异,除以我们合理预期的差异水平,并通过标准误差进行量化。

均值并不是唯一可能表明样本之间存在差异的统计量。事实上,样本方差也可以作为统计差异的一个指标。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_240.jpg

为了说明这一点,考虑前面的图示。左侧的三个组中的每一个都可以代表某一特定页面的停留时间样本,每个组都有自己的均值和标准差。如果将三个组的停留时间合并成一个,则方差比单独计算每组的平均方差要大。

方差分析(ANOVA)测试的统计显著性来源于两个方差的比率——即组间方差和组内方差。如果组间存在显著差异,但组内没有反映这种差异,那么这些分组有助于解释组间的一些方差。相反,如果组内的方差与组间的方差相同,那么这些组在统计学上并没有显著差异。

F-分布

F-分布由两个自由度参数化——一个是样本大小的自由度,另一个是组数的自由度。

第一个自由度是组数减一,第二个自由度是样本大小减去组数。如果k代表组数,n代表样本大小:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_13.jpghttps://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_14.jpg

我们可以通过 Incanter 函数图来可视化不同的F分布:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_250.jpg

前面的图示显示了不同的F分布,这些分布是基于将 100 个数据点拆分成 5 组、10 组和 50 组的结果。

F-统计量

表示组内和组间方差比率的检验统计量称为F-统计量。F-统计量越接近 1,表示两者的方差越相似。F-统计量的计算方法非常简单,如下所示:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_15.jpg

这里,https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_16.jpg是组间的方差,而https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_17.jpg是组内的方差

F比率增大时,组间方差与组内方差的比率也会增大。这意味着分组在解释整个样本观察到的方差方面表现得很好。当这个比率超过一个临界值时,我们可以说差异在统计学上是显著的。

注意

F检验始终是单尾检验,因为组间的任何方差都会使F值变大。F不可能小于零。

F检验中的组内方差是通过均值的平方偏差的平均值计算的。我们将其计算为从均值的平方偏差的和除以第一个自由度。例如,如果有k个组,每个组的均值为 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_18.jpg,我们可以这样计算组内方差

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_19.jpg

这里,SSW表示组内平方和x[jk]表示组k中*j^(th)*元素的值。

前面的计算SSW的公式看起来很复杂。但实际上,Incanter 定义了一个有用的 s/sum-of-square-devs-from-mean函数,这使得计算组内平方和变得像这样简单:

(defn ssw [groups]
  (->> (map s/sum-of-square-devs-from-mean groups)
       (reduce +)))

F检验中的组间方差有类似的公式:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_20.jpg

这里,SST总平方和SSW是我们刚刚计算的值。总平方和是从“总体”均值的平方差的总和,可以这样计算:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_21.jpg

因此,SST只是没有任何分组的整体平方和。我们可以在 Clojure 中像这样计算SSTSSW

(defn sst [groups]
  (->> (apply concat groups)
       (s/sum-of-square-devs-from-mean)))

(defn ssb [groups]
  (- (sst groups)
     (ssw groups)))

F统计量是通过组间方差与组内方差的比率来计算的。结合之前定义的ssbssw函数以及两个自由度,我们可以在 Clojure 中按如下方式计算F统计量。

因此,我们可以通过以下方式从我们的各组和两个自由度计算F统计量:

(defn f-stat [groups df1 df2]
  (let [msb (/ (ssb groups) df1)
        msw (/ (ssw groups) df2)]
    (/ msbmsw)))

现在,我们可以从各组计算出F统计量,准备在F检验中使用它。

F检验

和我们在本章中查看的所有假设检验一样,一旦我们有了统计量和分布,我们只需要选择一个α值,并查看我们的数据是否超出了该检验的临界值。

Incanter 提供了一个 s/f-test函数,但它仅衡量两组间和组内的方差。为了对我们 20 个不同的组进行F检验,我们需要实现自己的F检验函数。幸运的是,我们已经通过计算合适的F统计量,在前面的部分做了很多繁重的工作。我们可以通过查找F统计量并使用带有正确自由度的F分布来执行F检验。在下面的代码中,我们将编写一个f-test函数,利用它对任意数量的组执行检验:

(defn f-test [groups]
  (let [n (count (apply concat groups))
        m (count groups)
        df1 (- m 1)
        df2 (- n m)
        f-stat (f-stat groups df1 df2)]
    (s/cdf-f f-stat :df1 df1 :df2 df2 :lower-tail? false)))

在前述函数的最后一行,我们使用 Incanter 的s/cdf-f函数,并根据正确的自由度将F统计量转换为p值。这个p值是对整个模型的度量,表明不同页面如何解释总体停留时间的方差。我们需要做的就是选择一个显著性水平并运行测试。我们暂时选择 5%的显著性水平:

(defn ex-2-24 []
  (let [grouped (->> (load-data "multiple-sites.tsv")
                     (:rows)
                     (group-by :site)
                     (vals)
                     (map (partial map :dwell-time)))]
    (f-test grouped)))

;; 0.014

该测试返回了一个p值为 0.014,这是一个显著的结果。不同的页面确实有不同的方差,不能仅仅通过随机抽样误差来解释。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_260.jpg

我们可以使用箱线图将每个网站的分布一起可视化,并将它们并排进行比较:

(defn ex-2-25 []
  (let [grouped (->> (load-data "multiple-sites.tsv")
                     (:rows)
                     (group-by :site)
                     (sort-by first)
                     (map second)
                     (map (partial map :dwell-time)))
        box-plot (c/box-plot (first grouped)
                             :x-label "Site number"
                             :y-label "Dwell time (s)")
        add-box (fn [chart dwell-times]
                  (c/add-box-plot chart dwell-times))]
    (-> (reduce add-box box-plot (rest grouped))
        (i/view))))

在前面的代码中,我们对各组进行遍历,为每个组调用c/add-box-plot。在绘制前,组按其网站 ID 进行排序,因此我们的原始页面 0 位于图表的最左侧。

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_270.jpg

看起来网站 ID 10的停留时间最长,因为其四分位距在图表上延伸得最远。然而,如果仔细观察,你会发现它的均值低于网站 6,停留时间的平均值超过 144 秒:

(defn ex-2-26 []
  (let [data (load-data "multiple-sites.tsv")
        site-0 (->> (i/$where {:site {:$eq 0}} data)
                    (i/$ :dwell-time))
        site-10 (->> (i/$where {:site {:$eq 10}} data)
                     (i/$ :dwell-time))]
    (s/t-test site-10 :y site-0)))

;; 0.0069

现在我们已经通过F检验确认了统计显著效应,我们有理由宣称网站 ID 6在统计上与基准存在差异:

(defn ex-2-27 []
  (let [data (load-data "multiple-sites.tsv")
        site-0 (->> (i/$where {:site {:$eq 0}} data)
                    (i/$ :dwell-time))
        site-6 (->> (i/$where {:site {:$eq 6}} data)
                    (i/$ :dwell-time))]
    (s/t-test site-6 :y site-0)))

;; 0.007

最终,我们有证据表明,页面 ID 6 相较于当前网站确实有所改进。根据我们的分析,AcmeContent 的 CEO 授权启动新版本的网站。网络团队感到非常高兴!

效应大小

在本章中,我们集中讨论了统计显著性——统计学家用来确保发现的差异不能简单地归因于随机变异的方法。我们必须始终记住,发现显著效应并不等同于发现大效应。在非常大的样本中,即使样本均值之间的差异很小,也会被视为显著。为了更好地了解我们的发现是否既显著又重要,我们还应当陈述效应大小。

Cohen’s d

Cohen’s d 是一个调整指标,可以帮助我们判断我们观察到的差异不仅是统计显著的,而且实际上很大。类似于 Bonferroni 校正,这个调整非常简单:

https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/clj-ds/img/7180OS_02_22.jpg

在这里,*S[ab]*是样本的合并标准差(而非合并标准误)。它的计算方式类似于合并标准误:

(defn pooled-standard-deviation [a b]
  (i/sqrt (+ (i/sq (standard-deviation a))
             (i/sq (standard-deviation b)))))

因此,我们可以计算页面 6 的 Cohen’s d,如下所示:

(defn ex-2-28 []
  (let [data (load-data "multiple-sites.tsv")
        a (->> (i/$where {:site {:$eq 0}} data)
               (i/$ :dwell-time))
        b (->> (i/$where {:site {:$eq 6}} data)
               (i/$ :dwell-time))]
    (/ (- (s/mean b)
          (s/mean a))
       (pooled-standard-deviation a b))))

;; 0.389

p值相比,Cohen’s d 没有绝对的阈值。一个效应是否可以被认为是大的,部分取决于具体背景,但它确实提供了一个有用的、标准化的效应大小度量。大于 0.5 的值通常被认为是大的,因此 0.38 是一个适度的效应。它无疑代表了我们网站停留时间的显著增加,值得为网站升级付出努力。

总结

在本章中,我们学习了描述性统计和推断性统计的区别。我们再次看到了正态分布和中心极限定理的重要性,并了解了如何通过z检验、t检验和F检验量化总体差异。

我们了解了推断统计学的技巧如何通过分析样本本身来对所抽样的总体做出推断。我们见识了各种技术——置信区间、重抽样法和显著性检验——这些都能提供有关潜在总体参数的见解。通过使用 ClojureScript 模拟重复的测试,我们还洞察了多重比较中的显著性检验困难,并看到F检验如何试图解决这一问题,在第一类和第二类错误之间找到平衡。

在下一章,我们将把在方差和F检验中学到的知识应用到单个样本中。我们将介绍回归分析技术,并使用它来寻找奥林匹克运动员样本中变量之间的相关性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值