数据科学家的统计学实践指南-全-

数据科学家的统计学实践指南(全)

原文:zh.annas-archive.org/md5/7cc7f5be94b748e6ecbe5eff5ff3ee97

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书面向具有一定熟悉度的数据科学家,他们熟悉R和/或Python编程语言,并且在统计学方面有一些先前的(可能是零星或短暂的)接触。本书的两位作者都是从统计学领域转入数据科学领域的,对统计学在数据科学艺术中的贡献有一定的认识。同时,我们也很清楚传统统计学教学的局限性:统计学作为一门学科已有一个半世纪的历史,大多数统计学教材和课程都沉浸于像海轮的动量和惯性中。本书中的所有方法都与统计学学科有一定的历史或方法论联系。主要源于计算机科学的方法,例如神经网络,不包括在内。

本书有两个目标:

  • 以易于消化、易于导航和易于参考的形式,阐明对数据科学有关的统计学关键概念。

  • 解释哪些概念从数据科学的角度来看重要和有用,哪些不太重要,以及原因。

本书使用的约定

本书使用以下排版约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

常量宽度

用于程序清单,以及在段落中引用程序元素,如变量或函数名称,数据库,数据类型,环境变量,语句和关键字。

常量宽度粗体

显示用户应该直接输入的命令或其他文本。

提示

此元素表示提示或建议。

注意

此元素表示一般注释。

警告

此元素表示警告或注意事项。

使用代码示例

本书所有示例代码首先以R语言显示,然后以Python语言显示。为避免不必要的重复,我们通常只展示R代码生成的输出和图形。我们还跳过加载所需软件包和数据集所需的代码。您可以在https://github.com/gedeck/practical-statistics-for-data-scientists找到完整的代码以及数据集的下载链接。

本书旨在帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您复制了本书的大部分代码,否则您无需联系我们以获得许可。例如,编写一个使用本书多个代码片段的程序不需要许可。销售或分发 O’Reilly 书籍中的示例代码需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书大量示例代码整合到您产品的文档中需要许可。

我们感谢,但不需要归属。归属通常包括标题、作者、出版商和 ISBN。例如:“数据科学家的实用统计 由彼得·布鲁斯、安德鲁·布鲁斯和彼得·盖德克(O’Reilly)著。版权所有 2020 年 彼得·布鲁斯、安德鲁·布鲁斯和彼得·盖德克,978-1-492-07294-2。”

如果您觉得您对代码示例的使用超出了合理使用范围或以上授权,请随时通过permissions@oreilly.com联系我们。

O’Reilly Online Learning

注意

40 多年来,O’Reilly Media已为企业提供技术和业务培训、知识和见解,帮助它们取得成功。

我们独特的专家和创新者网络通过书籍、文章和我们的在线学习*台分享他们的知识和专业知识。O’Reilly 的在线学习*台为您提供按需访问实时培训课程、深入学习路径、交互式编码环境以及来自 O’Reilly 和其他 200 多个出版商的大量文本和视频。欲了解更多信息,请访问http://oreilly.com

如何联系我们

请将有关本书的评论和问题发送至出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(在美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为这本书建立了一个网页,列出了勘误、示例和任何额外信息。您可以访问https://oreil.ly/practicalStats_dataSci_2e

通过电子邮件bookquestions@oreilly.com评论或询问有关本书的技术问题。

关于我们的图书和课程的最新消息和更多信息,请访问我们的网站http://oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

关注我们的 Twitter:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

作者感谢帮助这本书变成现实的许多人。

数据挖掘公司 Elder Research 的首席执行官 Gerhard Pilcher 看到了书的早期草稿,并为我们提供了详细和有益的修正和意见。同样,SAS 的统计学家 Anya McGuirk 和 Wei Xiao,以及 O’Reilly 的作者 Jay Hilfiger,在书的初稿阶段提供了有益的反馈。将第一版翻译成日文的 Toshiaki Kurokawa 在审查和修正过程中做了全面的工作。Aaron Schumacher 和 Walter Paczkowski 彻底审阅了书的第二版,并提出了许多有价值的建议,对此我们非常感激。毫无疑问,任何剩余的错误都是我们自己的责任。

在 O’Reilly,香农·卡特以乐观的态度和适当的推动引导我们完成了出版过程,而克里斯汀·布朗则顺利将我们的书籍进行了生产阶段的处理。瑞秋·蒙纳汉和埃利亚胡·苏斯曼以细心和耐心纠正并改善了我们的写作,埃伦·特劳特曼-赛格则准备了索引。妮可·塔什接手了第二版的工作,并有效地指导了整个过程,并提出了许多提高书籍可读性的优秀编辑建议。我们还感谢玛丽·博戈罗在 O’Reilly 启动我们的项目,以及本·本福特,O’Reilly 作者和 Statistics.com 讲师,他介绍我们认识了 O’Reilly。

我们和这本书,在过去多年中从彼得与加利特·舒梅利(其他书籍合著者)的许多对话中获益匪浅。

最后,我们特别要感谢伊丽莎白·布鲁斯和黛博拉·唐内尔,他们的耐心和支持使这一努力成为可能。

第一章:探索性数据分析

本章重点介绍数据科学项目的第一步:探索数据。

古典统计学几乎完全专注于推断,这是一套有时复杂的程序,用于根据小样本对大总体进行结论。1962 年,约翰·W·图基在他的开创性论文《数据分析的未来》[图基-1962]中呼吁改革统计学。他提出了一门名为数据分析的新科学学科,将统计推断仅作为其中的一个组成部分。图基与工程和计算机科学界建立了联系(他创造了比特一词,代表二进制位,以及软件一词),他的最初原则令人惊讶地持久,并构成了数据科学的基础之一。探索性数据分析领域始于图基于 1977 年撰写的现已成为经典的书籍《探索性数据分析》[图基-1977]。图基提出了简单的图表(例如,箱线图、散点图),这些图表与汇总统计数据(均值、中位数、分位数等)一起,有助于描绘数据集的全貌。

随着计算能力的普及和表达性数据分析软件的出现,探索性数据分析已经远远超出了其最初的范围。推动这一学科发展的关键因素包括新技术的快速发展,获取更多更大的数据,以及在各种学科中更广泛地使用定量分析。斯坦福大学统计学教授、图基的前本科生大卫·多诺霍(David Donoho)根据他在新泽西州普林斯顿市图基百年纪念研讨会上的发言撰写了一篇优秀的文章[多诺霍-2015]。多诺霍将数据科学的起源追溯到图基在数据分析方面的开创性工作。

约翰·图基,这位杰出的统计学家的思想在 50 多年前形成了数据科学的基础。

图 1-1. 约翰·图基,这位杰出的统计学家的思想在 50 多年前形成了数据科学的基础

结构化数据元素

数据来自许多来源:传感器测量、事件、文本、图像和视频。物联网(IoT)正在喷发信息流。其中许多数据是非结构化的:图像是像素的集合,每个像素包含 RGB(红、绿、蓝)颜色信息。文本是单词和非单词字符的序列,通常按部分、子部分等组织。点击流是用户与应用程序或网页交互的行动序列。事实上,数据科学的一个主要挑战是将这些原始数据的洪流转化为可操作的信息。要应用本书涵盖的统计概念,非结构化的原始数据必须被处理和转换成结构化形式。结构化数据的一种常见形式是具有行和列的表格——数据可能来自关系数据库或为研究而收集。

有两种基本类型的结构化数据:数值和分类。数值数据有两种形式:连续,如风速或时间持续,以及离散,如事件发生的次数。分类数据只取固定集合的值,如电视屏幕类型(等离子、LCD、LED 等)或州名(阿拉巴马州、阿拉斯加州等)。二元数据是分类数据的一个重要特例,只取两个值之一,如 0/1、是/否或真/假。另一种有用的分类数据类型是有序数据,在这种数据中,类别是有序的;一个例子是数值评级(1、2、3、4 或 5)。

我们为什么要费心对数据类型进行分类?事实证明,为了数据分析和预测建模的目的,数据类型对于确定视觉显示类型、数据分析或统计模型类型是很重要的。事实上,数据科学软件如RPython使用这些数据类型来提高计算性能。更重要的是,变量的数据类型决定了软件如何处理该变量的计算。

软件工程师和数据库程序员可能会想知道为什么我们甚至需要分类有序数据用于分析。毕竟,类别只是文本(或数字)值的集合,底层数据库会自动处理内部表示。然而,将数据明确标识为分类数据,与文本不同,确实提供了一些优势:

  • 知道数据是分类的可以作为一个信号,告诉软件如何进行统计过程,比如生成图表或拟合模型。特别是,有序数据可以在R中表示为ordered.factor,在图表、表格和模型中保留用户指定的顺序。在Python中,scikit-learn通过sklearn.preprocessing.OrdinalEncoder支持有序数据。

  • 存储和索引可以进行优化(如关系数据库中)。

  • 给定分类变量可能采用的可能值在软件中被强制执行(类似枚举)。

第三个“好处”可能会导致意外或预期之外的行为:在R中,数据导入函数(例如read.csv)的默认行为是自动将文本列转换为factor。对该列的后续操作将假定该列的唯一可允许值是最初导入的值,而分配新的文本值将引发警告并生成NA(缺失值)。Python中的pandas包不会自动进行此类转换。但是,您可以在read_csv函数中显式指定列为分类。

进一步阅读

  • pandas文档描述了不同的数据类型以及它们如何在Python中操作。

  • 数据类型可能会令人困惑,因为类型可能会重叠,并且一个软件中的分类法可能与另一个软件中的不同。R 教程网站涵盖了R的分类法。pandas文档描述了不同的数据类型以及它们如何在Python中操作。

  • 数据库在数据类型的分类中更为详细,包括精度级别、固定长度或可变长度字段等考虑因素;参见SQL 的 W3Schools 指南

矩形数据

在数据科学中,分析的典型参考框架是矩形数据对象,例如电子表格或数据库表。

矩形数据是指二维矩阵,行表示记录(案例),列表示特征(变量);数据框RPython中的特定格式。数据并不总是以此形式开始:非结构化数据(例如文本)必须经过处理和操作,以便将其表示为矩形数据中的一组特征(参见“结构化数据元素”)。对于大多数数据分析和建模任务,关系数据库中的数据必须提取并放入单个表中。

表 1-1. 典型的数据框格式

类别 货币 卖家评级 时长 结束日 收盘价 开盘价 竞争性?
音乐/电影/游戏 美国 3249 5 星期一 0.01 0.01 0
音乐/电影/游戏 美国 3249 5 星期一 0.01 0.01 0
汽车行业 美国 3115 7 星期二 0.01 0.01 0
汽车行业 美国 3115 7 星期二 0.01 0.01 0
汽车行业 美国 3115 7 星期二 0.01 0.01 0
汽车行业 美国 3115 7 星期二 0.01 0.01 0
汽车行业 美国 3115 7 星期二 0.01 0.01 1
汽车行业 美国 3115 7 星期二 0.01 0.01 1

在 表 1-1 中,存在测量或计数数据(例如,持续时间和价格)和分类数据(例如,类别和货币)。如前所述,在 表 1-1 中的最右列中有一种特殊形式的分类变量,即二元(是/否或 0/1)变量 —— 一个指示变量,显示拍卖是否竞争(是否有多个竞标者)。当场景是预测拍卖是否竞争时,这个指示变量也恰好是一个 结果 变量。

数据框和索引

传统数据库表有一个或多个列被指定为索引,本质上是行号。这可以极大地提高某些数据库查询的效率。在 Python 中,使用 pandas 库,基本的矩形数据结构是一个 DataFrame 对象。默认情况下,DataFrame 根据行的顺序创建一个自动整数索引。在 pandas 中,还可以设置多级/层次化索引以提高某些操作的效率。

R 中,基本的矩形数据结构是一个 data.frame 对象。一个 data.frame 也有一个基于行顺序的隐式整数索引。原生的 R data.frame 不支持用户指定或多级索引,尽管可以通过 row.names 属性创建自定义键。为了弥补这个不足,两个新的包正在广泛使用:data.tabledplyr。两者都支持多级索引,并在处理 data.frame 时提供了显著的加速。

术语差异

矩形数据的术语可能令人困惑。统计学家和数据科学家对同一概念使用不同术语。对于统计学家来说,预测变量 在模型中用于预测 响应因变量。对于数据科学家来说,特征 用于预测 目标。一个特别令人困惑的同义词是:计算机科学家会将单个行称为 样本;而对统计学家来说,样本 意味着一组行。

非矩形数据结构

除了矩形数据外,还有其他数据结构。

时间序列数据记录了同一变量的连续测量。它是统计预测方法的原始材料,也是设备生成的数据的关键组成部分 —— 物联网数据。

用于地图和位置分析的空间数据结构比矩形数据结构更复杂和多样化。在 对象 表示中,数据的焦点是一个对象(例如,一座房子)及其空间坐标。相比之下,在 视图中,关注的是空间的小单元和相关度量的值(例如像素亮度)。

图(或网络)数据结构用于表示物理、社会和抽象关系。例如,社交网络的图表,如 Facebook 或 LinkedIn,可以表示网络上人与人之间的联系。通过道路连接的分布中心是物理网络的一个例子。图结构对于某些类型的问题很有用,比如网络优化和推荐系统。

这些数据类型中的每一个在数据科学中都有其专门的方法。本书的重点是矩形数据,这是预测建模的基本构建单元。

统计学中的图表

在计算机科学和信息技术中,术语通常用于描述实体之间的连接及其基础数据结构。在统计学中,用于指代各种绘图和可视化,不仅仅是实体之间的连接,并且该术语仅适用于可视化,而不是数据结构本身。

进一步阅读

位置估计

具有测量或计数数据的变量可能有数千个不同的值。探索数据的基本步骤是为每个特征(变量)获取一个“典型值”:数据大部分位于何处的估计(即其中心趋势)。

乍一看,总结数据似乎相当简单:只需取数据的均值。事实上,虽然均值易于计算且使用方便,但它并不总是最佳的中心值度量。因此,统计学家开发并推广了几种替代均值的估计方法。

度量和估计

统计学家经常用术语估计来表示从手头数据计算出的值,以区分我们从数据中看到的内容和理论上的真实或确切状态。数据科学家和业务分析师更倾向于将这样的值称为度量。这种差异反映了统计学与数据科学的方法:不确定性的考虑是统计学学科的核心,而具体的业务或组织目标是数据科学的重点。因此,统计学家进行估计,而数据科学家进行度量。

均值

最基本的位置估计是*均值,或者*均值。*均值是所有值的总和除以值的数量。考虑以下数字集合:{3 5 1 2}。*均值为 (3 + 5 + 1 + 2) / 4 = 11 / 4 = 2.75。你会遇到符号 x ¯(读作“x-bar”),用于表示从总体中取样的样本的均值。计算一组n个值 x 1 , x 2 , ... , x n 的公式为:

Mean = x ¯ = i=1 n x i n

注意

N(或n)指的是记录或观测的总数。在统计学中,如果它是指的总体,则大写;如果是指的总体的样本,则小写。在数据科学中,这种区别并不是很重要,所以你可能会看到两种写法。

均值的一种变体是修剪均值,你可以通过删除每端的固定数量的排序值,然后取剩余值的*均值来计算。用 x (1) , x (2) , ... , x (n) 来表示排序值,其中 x (1) 是最小值, x (n) 是最大值,计算剔除 p 个最小和最大值后的修剪均值的公式为:

Trimmed mean = x ¯ = i=p+1 n-p x (i) n-2p

修剪均值消除极端值的影响。例如,在国际跳水比赛中,五位评委的最高分和最低分被剔除,而最终得分是剩余三位评委评分的*均值。这样做可以防止单个评委操纵分数,可能有利于他们国家的参赛选手。修剪均值被广泛使用,在许多情况下比使用普通均值更可取—详见“中位数和健壮估计”进一步讨论。

另一种*均数是加权*均数,计算方法是将每个数据值 x i 乘以用户指定的权重 w i,然后除以权重的总和。加权*均数的公式为:

Weighted mean = x ¯ w = i=1 n w i x i i=1 n w i

使用加权*均数有两个主要动机:

  • 有些值本质上比其他值更变化,高变化的观察结果会被赋予较低的权重。例如,如果我们从多个传感器中取*均值,而其中一个传感器的精度较低,则我们可能会降低该传感器的数据权重。

  • 收集的数据未能*等地代表我们有兴趣测量的不同群体。例如,由于在线实验的进行方式,我们可能没有一个能够准确反映用户群体中所有群体的数据集。为了纠正这一点,我们可以给予那些代表性不足群体的值更高的权重。

中位数和鲁棒估计

中位数是数据排序列表中的中间数。如果数据值的数量是偶数,中间值实际上不在数据集中,而是分割排序数据成上半部分和下半部分的两个值的*均值。与使用所有观察值的*均数相比,中位数仅依赖于排序数据中心的值。虽然这可能看起来是一个劣势,因为*均数对数据更为敏感,但在许多情况下,中位数是位置测量的更好指标。例如,我们想要查看西雅图华盛顿州湖畔周围各个社区的典型家庭收入。如果我们比较梅迪纳社区和温德米尔社区,使用*均数会产生非常不同的结果,因为比尔·盖茨住在梅迪纳。如果我们使用中位数,比尔·盖茨有多富有并不重要——中间观察值的位置将保持不变。

基于与加权*均数相同的原因,也可以计算加权中位数。与中位数类似,我们首先对数据进行排序,尽管每个数据值都有一个关联的权重。加权中位数不是排序列表的中间数,而是这样一个值,使得排序列表的下半部分和上半部分的权重之和相等。与中位数一样,加权中位数对异常值具有鲁棒性。

异常值

中位数被称为位置的 稳健 估计,因为它不受可能扭曲结果的 异常值(极端情况)的影响。异常值是数据集中与其他值非常远的任何值。异常值的确切定义在某种程度上是主观的,尽管在各种数据摘要和图中使用了某些惯例(参见“百分位数和箱线图”)。异常值本身并不使数据值无效或错误(如前面关于比尔·盖茨的例子)。尽管如此,异常值通常是由于数据错误(例如混合不同单位的数据,如千米与米)或传感器的错误读数而导致的。当异常值是由于错误数据时,均值将导致位置估计不佳,而中位数仍然有效。无论如何,应该识别异常值,并且通常值得进一步调查。

异常检测

与典型的数据分析相比,在 异常检测 中,异常值是感兴趣的点,而更大部分的数据主要用于定义“正常”,以便测量异常。

中位数并不是唯一的位置稳健估计。事实上,修剪均值被广泛使用以避免异常值的影响。例如,修剪数据底部和顶部的 10%(一种常见选择)将在除了最小数据集外的所有情况下保护免受异常值的影响。修剪均值可以被视为中位数和均值之间的折衷:它对数据中的极值稳健,但使用更多数据来计算位置估计。

其他稳健的位置度量

统计学家已经开发出大量其他位置估计器,主要目标是开发比均值更稳健且更有效(即更能辨别数据集之间小位置差异)的估计器。虽然这些方法可能对小数据集有用,但对大或中等规模的数据集不太可能提供额外的好处。

示例:人口和谋杀率的位置估计

表 1-2 显示了包含每个美国州(2010 年人口普查)的人口和谋杀率(以每年每 100,000 人的谋杀数单位)的数据集的前几行。

表 1-2. 人口和谋杀率 data.frame 状态的几行示例

人口 谋杀率 缩写
1 阿拉巴马州 4,779,736 5.7 AL
2 阿拉斯加州 710,231 5.6 AK
3 亚利桑那州 6,392,017 4.7 AZ
4 阿肯色州 2,915,918 5.6 AR
5 加利福尼亚州 37,253,956 4.4 CA
6 科罗拉多州 5,029,196 2.8 CO
7 康涅狄格州 3,574,097 2.4 CT
8 特拉华州 897,934 5.8 DE

使用 R 计算人口的均值、修剪均值和中位数:

> state <- read.csv('state.csv')
> mean(state[['Population']])
[1] 6162876
> mean(state[['Population']], trim=0.1)
[1] 4783697
> median(state[['Population']])
[1] 4436370

要在Python中计算*均值和中位数,我们可以使用数据帧的pandas方法。修剪均值需要scipy.stats中的trim_mean函数:

state = pd.read_csv('state.csv')
state['Population'].mean()
trim_mean(state['Population'], 0.1)
state['Population'].median()

*均值大于修剪均值,后者大于中位数。

这是因为修剪均值排除了最大和最小的五个州(trim=0.1从每端剔除 10%)。如果我们想要计算全国的*均谋杀率,我们需要使用加权*均值或中位数来考虑各州的不同人口。由于基本的R没有加权中位数函数,我们需要安装诸如matrixStats的软件包:

> weighted.mean(state[['Murder.Rate']], w=state[['Population']])
[1] 4.445834
> library('matrixStats')
> weightedMedian(state[['Murder.Rate']], w=state[['Population']])
[1] 4.4

权重均值可在NumPy中找到。对于加权中位数,我们可以使用专用软件包wquantiles

np.average(state['Murder.Rate'], weights=state['Population'])
wquantiles.median(state['Murder.Rate'], weights=state['Population'])

在这种情况下,加权*均值和加权中位数大致相同。

进一步阅读

  • 维基百科关于中心趋势的文章详细讨论了各种位置测量。

  • 约翰·图基于 1977 年经典著作探索性数据分析(Pearson) 仍广泛阅读。

变异性估计

位置仅是总结特征的一个维度。第二个维度变异性,也称为分散,用于衡量数据值是密集聚集还是分散。统计学的核心在于变异性:测量它,减少它,区分随机变异和真实变异,识别真实变异的各种来源,并在其存在的情况下做出决策。

就像有不同的方法来测量位置(*均值,中位数等),也有不同的方法来测量变异性。

标准偏差及相关估计

最广为使用的变异性估计基于估计的位置与观察数据之间的差异或偏差。对于数据集{1, 4, 4},*均值为 3,中位数为 4。相对于*均值的偏差为差值:1 – 3 = –2,4 – 3 = 1,4 – 3 = 1。这些偏差告诉我们数据围绕中心值的分散程度。

一种测量变异性的方法是估计这些偏差的典型值。*均偏差本身不会告诉我们太多——负偏差抵消了正偏差。事实上,从*均值的偏差之和正好是零。相反,一种简单的方法是取偏差绝对值的*均值。在前面的例子中,偏差的绝对值为{2 1 1},它们的*均值是(2 + 1 + 1) / 3 = 1.33。这被称为*均绝对偏差,可用以下公式计算:

Mean absolute deviation = i=1 n x i -x ¯ n

其中x ¯是样本均值。

最为人熟知的变异性估计是方差标准偏差,这些都基于*方偏差。方差是*方偏差的*均数,标准偏差是方差的*方根:

Variance = s 2 = i = 1 n ( x i x ¯ ) 2 n 1 Standard deviation = s = Variance

标准差比方差更容易解释,因为它与原始数据在同一尺度上。尽管其公式更复杂且不直观,但统计学上更偏好标准差而不是均值绝对偏差。这是因为在统计理论中,数学上使用*方值比绝对值更方便,尤其是对于统计模型而言。

方差、标准差和均值绝对偏差对异常值和极端值都不稳健(有关稳健估计位置的讨论,请参见“中位数和稳健估计”)。方差和标准差特别对异常值敏感,因为它们基于*方偏差。

一个稳健的变异性估计是中位数绝对偏差或 MAD:

Median absolute deviation = Median x 1 - m , x 2 - m , ... , x N - m

其中m是中位数。与中位数类似,MAD 不受极端值的影响。还可以计算类似于修剪均值的修剪标准差(参见“均值”)。

注意

方差、标准差、均值绝对偏差和中位数绝对偏差不是等价的估计,即使数据来自正态分布的情况下也是如此。事实上,标准差总是大于均值绝对偏差,而均值绝对偏差本身大于中位数绝对偏差。有时,中位数绝对偏差会乘以一个常数缩放因子,以便在正态分布的情况下与标准差处于同一尺度上。常用的缩放因子 1.4826 意味着正态分布的 50%落在± MAD的范围内(参见,例如,https://oreil.ly/SfDk2)。

基于百分位数的估计

一个估计离散度的不同方法是基于排序数据的分散程度。基于排序(排名)数据的统计称为顺序统计。最基本的度量是范围:最大数和最小数之间的差异。最小值和最大值本身对于识别异常值是有用的,但范围对异常值非常敏感,作为数据离散度的一般度量不太有用。

为了避免对异常值过于敏感,我们可以在删除每端的值后查看数据的范围。形式上,这些类型的估计是基于百分位数之间的差异。在数据集中,第P百分位数是这样一个值,至少有P百分比的值取得此值或更小,至少有(100 – P)百分比的值取得此值或更大。例如,要找到 80th 百分位数,请对数据进行排序。然后,从最小值开始,前进 80%到最大值。注意,中位数与第 50 百分位数是相同的东西。百分位数本质上与分位数相同,分位数由分数索引(因此.8 分位数与 80th 百分位数相同)。

变异性的常见测量是第 25 百分位数和第 75 百分位数之间的差异,称为四分位距(或 IQR)。这里有一个简单的例子:{3,1,5,3,6,7,2,9}。我们对这些进行排序得到{1,2,3,3,5,6,7,9}。第 25 百分位数是 2.5,第 75 百分位数是 6.5,因此四分位距为 6.5 – 2.5 = 4。软件可能有略有不同的方法,导致不同的答案(见下面的提示);通常,这些差异较小。

对于非常大的数据集,计算精确的百分位数可能非常耗费计算资源,因为它需要对所有数据值进行排序。机器学习和统计软件使用特殊算法,比如[张-王-2007],来获取一个可以非常快速计算并且保证具有一定准确度的*似百分位数。

百分位数:精确定义

如果我们有偶数个数据(n为偶数),则根据前述定义,百分位数是模糊的。事实上,我们可以取得任何值介于顺序统计量x (j)x (j+1)之间的任何值,其中j满足:

100 j n P < 100 j+1 n

形式上,百分位数是加权*均值:

Percentile ( P ) = 1 - w x (j) + w x (j+1)

对于某些权重w,介于 0 和 1 之间。统计软件对选择w的方法有略有不同。事实上,R函数quantile提供了九种不同的替代方法来计算分位数。除了小数据集外,通常不需要担心百分位数的精确计算方式。在撰写本文时,Pythonnumpy.quantile仅支持一种方法,即线性插值。

例子:州人口的变异性估计

表 1-3(为方便起见,从表 1-2 重复)显示了包含每个州人口和谋杀率的数据集中的前几行。

表 1-3. 由州人口和谋杀率组成的data.frame数据集的前几行

州名 人口 谋杀率 缩写
1 阿拉巴马 4,779,736 5.7 AL
2 阿拉斯加 710,231 5.6 AK
3 亚利桑那 6,392,017 4.7 AZ
4 阿肯色 2,915,918 5.6 AR
5 加利福尼亚 37,253,956 4.4 CA
6 科罗拉多 5,029,196 2.8 CO
7 康涅狄格 3,574,097 2.4 CT
8 特拉华 897,934 5.8 DE

使用R内置函数来计算标准偏差、四分位距(IQR)以及中位数绝对离差(MAD),我们可以对州人口数据的变异性进行估算:

> sd(state[['Population']])
[1] 6848235
> IQR(state[['Population']])
[1] 4847308
> mad(state[['Population']])
[1] 3849870

pandas数据帧提供了计算标准偏差和百分位数的方法。使用百分位数,我们可以轻松确定 IQR。对于稳健的 MAD,我们使用statsmodels包中的robust.scale.mad函数:

state['Population'].std()
state['Population'].quantile(0.75) - state['Population'].quantile(0.25)
robust.scale.mad(state['Population'])

标准偏差几乎是 MAD 的两倍大(在R中,默认情况下,MAD 的尺度调整为与均值相同的尺度)。这并不奇怪,因为标准偏差对异常值很敏感。

进一步阅读

探索数据分布

我们讨论过的每一个估算值都总结了单个数字来描述数据的位置或变异性。总体上探索数据如何分布也是很有用的。

百分位数和箱线图

在“基于百分位数的估算”中,我们探讨了如何使用百分位数来衡量数据的扩展性。百分位数对于总结整个分布也是有价值的。通常报告四分位数(第 25、50 和 75 个百分位数)和分位数(第 10、20、…、90 个百分位数)是很常见的。百分位数对于总结分布的尾部(外围范围)尤为重要。流行文化将术语one-percenters用来指称处于财富前 99%的人群。

表 1-4 显示了按州划分的谋杀率的一些百分位数。在R中,这将由quantile函数生成:

quantile(state[['Murder.Rate']], p=c(.05, .25, .5, .75, .95))
   5%   25%   50%   75%   95%
1.600 2.425 4.000 5.550 6.510

Python中,pandas数据帧方法quantile提供了这一功能:

state['Murder.Rate'].quantile([0.05, 0.25, 0.5, 0.75, 0.95])

表 1-4. 按州划分的谋杀率的百分位数

5% 25% 50% 75% 95%
1.60 2.42 4.00 5.55 6.51

中位数为每 10 万人口 4 起谋杀案,尽管变异性很大:第 5 百分位数仅为 1.6,第 95 百分位数为 6.51。

箱线图,由 Tukey 提出[Tukey-1977],基于百分位数,可以快速可视化数据的分布。图 1-2 展示了R生成的按州人口的箱线图:

boxplot(state[['Population']]/1000000, ylab='Population (millions)')

pandas提供了许多基本的数据帧探索图,其中之一就是箱线图:

ax = (state['Population']/1_000_000).plot.box()
ax.set_ylabel('Population (millions)')

各州人口的箱线图

图 1-2. 各州人口的箱线图

从这个箱线图中,我们可以立即看出中位数州人口约为 500 万,一半的州人口介于约 200 万到约 700 万之间,还有一些高人口的异常值。箱子的顶部和底部分别是第 75 和第 25 百分位数。中位数由箱子中的水*线表示。虚线,称为,从箱子的顶部和底部延伸到表示数据大部分范围的位置。箱线图有很多变化;例如,参见R函数 boxplot 的文档 [R-base-2015]。默认情况下,R函数将须延伸到箱子之外的最远点,但不会超过 1.5 倍的四分位距。Matplotlib 使用相同的实现;其他软件可能使用不同的规则。

任何超出须外的数据都会被绘制为单个点或圆圈(通常被视为异常值)。

频率表和直方图

一个变量的频率表将变量范围分成等间距的段,并告诉我们每个段中有多少值。表 1-5 显示了在R中计算的按州人口的频率表:

breaks <- seq(from=min(state[['Population']]),
                to=max(state[['Population']]), length=11)
pop_freq <- cut(state[['Population']], breaks=breaks,
                right=TRUE, include.lowest=TRUE)
table(pop_freq)

函数 pandas.cut 创建了一个系列,将值映射到这些段中。使用 value_counts 方法,我们得到了频率表:

binnedPopulation = pd.cut(state['Population'], 10)
binnedPopulation.value_counts()

表 1-5. 按州计算的人口频率表

箱子编号 箱子范围 计数 州名
1 563,626–4,232,658 24 怀俄明州, 佛蒙特州, 北达科他州, 阿拉斯加州, 南达科他州, 特拉华州, 蒙大拿州, 罗德岛州, 新罕布什尔州, 缅因州, 夏威夷州, 爱达荷州, 内布拉斯加州, 西弗吉尼亚州, 新墨西哥州, 内华达州, 犹他州, 堪萨斯州, 阿肯色州, 密西西比州, 艾奥瓦州, 康涅狄格州, 俄克拉何马州, 俄勒冈州
2 4,232,659–7,901,691 14 肯塔基州, 路易斯安那州, 南卡罗来纳州, 阿拉巴马州, 科罗拉多州, 明尼苏达州, 威斯康星州, 马里兰州, 密苏里州, 田纳西州, 亚利桑那州, 印第安纳州, 马萨诸塞州, 华盛顿州
3 7,901,692–11,570,724 6 弗吉尼亚州, 新泽西州, 北卡罗来纳州, 乔治亚州, 密歇根州, 俄亥俄州
4 11,570,725–15,239,757 2 宾夕法尼亚州, 伊利诺伊州
5 15,239,758–18,908,790 1 佛罗里达州
6 18,908,791–22,577,823 1 纽约州
7 22,577,824–26,246,856 1 德克萨斯州
8 26,246,857–29,915,889 0
9 29,915,890–33,584,922 0
10 33,584,923–37,253,956 1 加利福尼亚州

最少人口的州是怀俄明州,有 563,626 人,而人口最多的是加利福尼亚州,有 37,253,956 人。这给我们提供了一个范围为 37,253,956 – 563,626 = 36,690,330,我们必须将其分成相等大小的箱子——假设是 10 个箱子。用 10 个相等大小的箱子,每个箱子的宽度为 3,669,033,因此第一个箱子的范围是从 563,626 到 4,232,658。相比之下,顶部箱子,从 33,584,923 到 37,253,956,只有一个州:加利福尼亚州。加利福尼亚州下面紧接着的两个箱子是空的,直到我们到达德克萨斯州。包括空箱子是很重要的;这些箱子中没有数值的事实是有用的信息。尝试不同的箱子大小也可能会很有用。如果箱子太大,可能会隐藏分布的重要特征。如果太小,则结果太细粒化,可能看不到整体情况。

频率表和百分位数都通过创建柱来总结数据。一般来说,四分位数和十分位数在每个柱中的数量相同(等数量的柱),但柱的尺寸会有所不同。相比之下,频率表在柱中的计数会不同(等尺寸的柱),而柱的尺寸相同。

直方图是可视化频率表的一种方式,其中柱在 x 轴上,数据计数在 y 轴上。例如,在图 1-3 中,以 1000 万(1e+07)为中心的柱大约从 800 万到 1200 万,并且该柱中有六个州。要创建与表 1-5 相对应的直方图,在 R 中使用 hist 函数和 breaks 参数:

hist(state[['Population']], breaks=breaks)

pandas 支持对数据框进行直方图的绘制,使用 DataFrame.plot.hist 方法。使用关键字参数 bins 来定义柱的数量。各种绘图方法返回一个轴对象,它允许使用 Matplotlib 进行进一步的可视化调整:

ax = (state['Population'] / 1_000_000).plot.hist(figsize=(4, 4))
ax.set_xlabel('Population (millions)')

直方图显示在图 1-3 中。通常,直方图是这样绘制的:

  • 空柱包含在图中。

  • 柱的宽度相等。

  • 柱数量(或者等价地,柱尺寸)由用户决定。

  • 柱是连续的——柱之间没有空白,除非有一个空柱。

州人口直方图

图 1-3. 州人口直方图

统计矩

在统计理论中,位置和变异性分别被称为分布的第一和第二。第三和第四矩被称为偏度峰度。偏度指的是数据是否偏向于较大或较小的值,而峰度则表示数据具有极端值的倾向。一般来说,不使用度量来衡量偏度和峰度;相反,这些是通过视觉显示(如图 1-2 和 1-3)来发现的。

密度图和估计

与直方图相关的是密度图,它显示数据值的分布作为一条连续线。密度图可以被认为是一个*滑的直方图,尽管它通常是直接通过核密度估计(参见[Duong-2001])从数据中计算出来的。图 1-4 显示了在直方图上叠加的密度估计。在 R 中,您可以使用 density 函数计算密度估计:

hist(state[['Murder.Rate']], freq=FALSE)
lines(density(state[['Murder.Rate']]), lwd=3, col='blue')

pandas 提供 density 方法来创建密度图。使用参数 bw_method 来控制密度曲线的*滑度:

ax = state['Murder.Rate'].plot.hist(density=True, xlim=[0,12], bins=range(1,12))
state['Murder.Rate'].plot.density(ax=ax) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
ax.set_xlabel('Murder Rate (per 100,000)')

1

绘图函数通常接受一个可选的轴(ax)参数,这将导致绘图添加到同一图中。

与图 1-3 中绘制的直方图的关键区别是 y 轴的比例尺:密度图将直方图绘制为比例而不是计数(在R中使用参数freq=FALSE指定)。注意密度曲线下的总面积=1,而不是在箱中的计数,您计算在 x 轴上任意两点之间的曲线下面积,这对应于分布位于这两点之间的比例。

各州谋杀率密度

图 1-4. 各州谋杀率密度

密度估计

密度估计是统计文献中一个丰富的主题,历史悠久。事实上,已经发表了超过 20 个R包,提供了密度估计的函数。[邓-维克姆-2011]R包进行了全面评估,特别推荐使用ASHKernSmoothpandasscikit-learn中的密度估计方法也提供了良好的实现。对于许多数据科学问题,无需担心各种类型的密度估计,直接使用基础函数即可。

进一步阅读

探索二元和分类数据

对于分类数据,简单的比例或百分比可以清楚地展示数据的情况。

对于一个二元变量或具有少数类别的分类变量进行总结是相对容易的事情:我们只需计算 1 的比例,或者重要类别的比例。例如,表 1-6 显示了自 2010 年以来达拉斯/沃斯堡机场延误航班的延误原因百分比。延误被归类为由承运人控制、空中交通管制系统延误、天气、安全或迟到的入境飞机等因素引起。

表 1-6. 达拉斯/沃斯堡机场各原因延误百分比

承运人 ATC 天气 安全 入境
23.02 30.40 4.03 0.12 42.43

条形图经常在大众媒体中见到,是显示单个分类变量的常见视觉工具。类别列在 x 轴上,频率或比例列在 y 轴上。图 1-5 展示了达拉斯/沃斯堡(DFW)每年因延误导致的机场延误情况,使用R函数barplot生成:

barplot(as.matrix(dfw) / 6, cex.axis=0.8, cex.names=0.7,
        xlab='Cause of delay', ylab='Count')

pandas也支持数据框的条形图:

ax = dfw.transpose().plot.bar(figsize=(4, 4), legend=False)
ax.set_xlabel('Cause of delay')
ax.set_ylabel('Count')

达拉斯/沃斯堡机场延误原因的条形图。

图 1-5. 达拉斯/沃斯堡机场延误原因的条形图

注意,条形图类似于直方图;在条形图中,x 轴表示因子变量的不同类别,而在直方图中,x 轴表示数值刻度上单个变量的值。在直方图中,条通常显示为相互接触,间隙表示数据中未发生的值。在条形图中,条通常显示为相互分离。

饼图是条形图的替代方案,尽管统计学家和数据可视化专家通常避免使用饼图,因为它们在视觉上信息较少(见[Few-2007])。

数值数据作为分类数据

在“频率表和直方图”中,我们看到基于数据分箱的频率表。这将数值数据隐式转换为有序因子。从这个意义上说,直方图和条形图类似,不同之处在于条形图的 x 轴上的类别没有顺序。将数值数据转换为分类数据是数据分析中重要且广泛使用的步骤,因为它减少了数据的复杂性(和大小)。这有助于在分析的初期阶段发现特征之间的关系。

众数

众数是数据中出现最频繁的值(或在并列情况下的值)。例如,达拉斯/沃斯堡机场延误原因的众数是“进站”。另一个例子,在美国大部分地区,宗教偏好的众数将是基督教。众数是分类数据的简单摘要统计量,通常不用于数值数据。

期望值

一种特殊类型的分类数据是其中类别代表或可以映射到同一比例尺上的离散值的数据。例如,某家新云技术的市场营销人员提供两种服务级别,一个每月价格为$300,另一个每月价格为$50。市场营销人员提供免费网络研讨会以生成潜在客户,公司估计 5%的参与者将注册$300 的服务,15%将注册$50 的服务,而 80%将不会注册任何服务。出于财务目的,这些数据可以通过“期望值”来总结,这是一种加权*均值,其中权重是概率。

期望值的计算如下所示:

  1. 将每个结果乘以其发生概率。

  2. 对这些值求和。

在云服务示例中,网络研讨会参与者的预期价值因此为每月 $22.50,计算方法如下:

E V = ( 0 . 05 ) ( 300 ) + ( 0 . 15 ) ( 50 ) + ( 0 . 80 ) ( 0 ) = 22 . 5

期望值实际上是加权*均的一种形式:它增加了未来预期和概率权重的概念,通常基于主观判断。期望值是业务估值和资本预算中的一个基本概念 - 例如,从新收购的五年利润的预期值,或者从诊所新患者管理软件的预期成本节省。

概率

我们上面提到了概率的概念。大多数人对概率有直观的理解,在天气预报(下雨的概率)或体育分析(获胜的概率)中经常遇到这个概念。体育和游戏更常用的表达方式是赔率,可以轻松转换为概率(如果一个团队获胜的赔率是 2 比 1,那么它获胜的概率是 2/(2+1)=2/3)。然而,令人惊讶的是,当涉及到定义概率时,概率的概念可以是深入探讨的源泉。幸运的是,在这里我们不需要一个正式的数学或哲学定义。对于我们的目的,事件发生的概率是如果情况可以一次又一次地重复发生,它将发生的比例。大多数情况下,这是一种想象出来的构造,但这是对概率的一个足够操作性的理解。

进一步阅读

没有统计课程是完整的,没有关于误导性图表的课程,这通常涉及条形图和饼图。

相关性

在许多建模项目(无论是数据科学还是研究中)的探索性数据分析中,涉及检查预测变量之间的相关性,以及预测变量与目标变量之间的相关性。变量 X 和 Y(每个都具有测量数据)被认为是正相关的,如果 X 的高值与 Y 的高值相对应,并且 X 的低值与 Y 的低值相对应。如果 X 的高值与 Y 的低值相对应,反之亦然,则这些变量是负相关的。

考虑这两个变量,在某种意义上完全相关,即每个都从低到高:

  • v1:

  • v2:

产品的向量积之和是1 · 4 + 2 · 5 + 3 · 6 = 32。现在尝试随意调整其中一个并重新计算 - 产品的向量积之和永远不会高于 32。因此,这个产品的和可以用作度量标准;也就是说,观察到的和 32 可以与大量随机洗牌进行比较(实际上,这个想法涉及到基于重采样的估计;请参阅“置换检验”)。然而,这个度量所产生的值并不那么有意义,除非参考重采样分布。

更有用的是标准化的变体:相关系数,它提供了两个变量之间相关性的估计,总是在同一刻度上。要计算Pearson 相关系数,我们将变量 1 与变量 2 的均值偏差相乘,然后除以标准差的乘积:

r = i=1 n (x i -x ¯)(y i -y ¯) (n-1)s x s y

注意,我们使用n – 1 而不是n进行除法;详见“自由度,n还是n – 1?”以获取更多细节。相关系数始终介于+1(完全正相关)和–1(完全负相关)之间;0 表示没有相关性。

变量可能具有非线性的关联,此时相关系数可能不是一个有用的度量指标。税率和筹集的收入之间的关系就是一个例子:随着税率从零增加,筹集的收入也增加。然而,一旦税率达到高水*并接* 100%,逃税增加,税收实际上会下降。

表 1-7,也称为相关性矩阵,展示了 2012 年 7 月至 2015 年 6 月期间电信股票的日收益之间的相关性。从表中可以看出,Verizon(VZ)和 ATT(T)的相关性最高。作为基础设施公司的 Level 3(LVLT)与其他公司的相关性最低。注意到对角线上的 1(股票与自身的相关性为 1),以及对角线以上和以下信息的冗余性。

表 1-7. 电信股票收益之间的相关性

T CTL FTR VZ LVLT
T 1.000 0.475 0.328 0.678 0.279
CTL 0.475 1.000 0.420 0.417 0.287
FTR 0.328 0.420 1.000 0.287 0.260
VZ 0.678 0.417 0.287 1.000 0.242
LVLT 0.279 0.287 0.260 0.242 1.000

像表 1-7 这样的相关性表通常用于可视化显示多个变量之间的关系。图 1-6 展示了主要交易所交易基金(ETFs)的日收益之间的相关性。在R中,我们可以使用corrplot包轻松创建这种图表:

etfs <- sp500_px[row.names(sp500_px) > '2012-07-01',
                 sp500_sym[sp500_sym$sector == 'etf', 'symbol']]
library(corrplot)
corrplot(cor(etfs), method='ellipse')

Python中也可以创建相同的图表,但常见的包中并没有实现。然而,大多数支持使用热图可视化相关矩阵。以下代码演示了如何使用seaborn.heatmap包。在随附的源代码仓库中,我们包含了生成更全面可视化的Python代码:

etfs = sp500_px.loc[sp500_px.index > '2012-07-01',
                    sp500_sym[sp500_sym['sector'] == 'etf']['symbol']]
sns.heatmap(etfs.corr(), vmin=-1, vmax=1,
            cmap=sns.diverging_palette(20, 220, as_cmap=True))

标准普尔 500 (SPY) 和道琼斯指数 (DIA) 的 ETF 具有很高的相关性。类似地,主要由科技公司组成的 QQQ 和 XLK 呈正相关。防御型 ETF,如跟踪黄金价格 (GLD)、石油价格 (USO) 或市场波动性 (VXX) 的 ETF,通常与其他 ETF 的相关性较弱或为负相关。椭圆的方向表明两个变量是否呈正相关(椭圆指向右上方)或负相关(椭圆指向左上方)。椭圆的阴影和宽度表示关联强度:更薄和更深的椭圆对应更强的关系。

ETF 收益之间的相关性。

图 1-6. ETF 收益之间的相关性

就像均值和标准差一样,相关系数对数据中的异常值非常敏感。软件包提供了经典相关系数的强健替代方案。例如,R 软件包 robust 使用函数 covRob 来计算相关的强健估计。scikit-learn 模块中的方法 sklearn.covariance 实现了多种方法。

其他相关估计

统计学家很久以前提出了其他类型的相关系数,例如 Spearman's rhoKendall's tau。这些是基于数据排名的相关系数。由于它们使用排名而不是值,这些估计对异常值鲁棒并且能处理某些非线性。然而,数据科学家通常可以依赖皮尔逊相关系数及其强健替代方案进行探索性分析。基于排名的估计的吸引力主要在于较小的数据集和特定的假设检验。

散点图

表示两个测量数据变量之间关系的标准方法是使用散点图。x 轴表示一个变量,y 轴表示另一个变量,图中的每个点都是一条记录。参见 图 1-7 关于 ATT 和 Verizon 每日收益相关性的图。这是在 R 中使用以下命令生成的:

plot(telecom$T, telecom$VZ, xlab='ATT (T)', ylab='Verizon (VZ)')

同样的图形可以在 Python 中使用 pandas 的散点方法生成:

ax = telecom.plot.scatter(x='T', y='VZ', figsize=(4, 4), marker='$\u25EF$')
ax.set_xlabel('ATT (T)')
ax.set_ylabel('Verizon (VZ)')
ax.axhline(0, color='grey', lw=1)
ax.axvline(0, color='grey', lw=1)

收益之间存在正相关关系:虽然它们围绕零点聚集,大多数日子里,股票上涨或下跌呈现一致性(右上和左下象限)。较少的日子里,一个股票大幅下跌,而另一个股票上涨,或反之(左上和右下象限)。

尽管图 图 1-7 仅显示了 754 个数据点,但已经明显看出在图的中部识别细节是多么困难。稍后我们将看到如何通过给点添加透明度,或者使用六角形分箱和密度图来帮助发现数据中的额外结构。

ATT 和 Verizon 收益之间的散点图。

图 1-7。ATT 和 Verizon 收益相关性的散点图

进一步阅读

统计学,第 4 版,David Freedman,Robert Pisani 和 Roger Purves(W. W. Norton,2007 年)对相关性有很好的讨论。

探索两个或更多变量

熟悉的估算器如均值和方差一次只考虑一个变量(单变量分析)。相关分析(参见“相关性”)是一种比较两个变量的重要方法(双变量分析)。在本节中,我们将探讨更多的估算和图表,以及超过两个变量的分析(多变量分析)。

与单变量分析类似,双变量分析涉及计算摘要统计信息并生成视觉显示。适当的双变量或多变量分析类型取决于数据的性质:数值还是分类。

六角形分箱和等高线(绘制数值对数值数据)

当数据值相对较少时,散点图是合适的。例如,在图 1-7 中的股票收益图仅涉及约 750 个数据点。对于包含数十万或数百万条记录的数据集,散点图会显得太密集,因此我们需要一种不同的方法来可视化关系。举例来说,考虑数据集kc_tax,它包含华盛顿州金县住宅物业的税务评估价值。为了聚焦于数据的主要部分,我们使用subset函数剔除非常昂贵或非常小/大的住宅:

kc_tax0 <- subset(kc_tax, TaxAssessedValue < 750000 &
                  SqFtTotLiving > 100 &
                  SqFtTotLiving < 3500)
nrow(kc_tax0)
432693

pandas中,我们按以下方式筛选数据集:

kc_tax0 = kc_tax.loc[(kc_tax.TaxAssessedValue < 750000) &
                     (kc_tax.SqFtTotLiving > 100) &
                     (kc_tax.SqFtTotLiving < 3500), :]
kc_tax0.shape
(432693, 3)

图 1-8 是金县住宅完成*方英尺与税务评估价值之间关系的六角形分箱图。与绘制点不同,点会形成一个巨大的暗云,我们将记录分组到六角形箱中,并用颜色表示该箱中的记录数量。在这张图中,完成*方英尺与税务评估价值之间的正向关系是明显的。一个有趣的特点是在底部主(最暗)带上方还有额外带的暗示,表明这些住宅与主带中具有相同*方英尺但税务评估价值更高的住宅相同。

图 1-8 是由强大的Rggplot2生成的,该包由 Hadley Wickham 开发[ggplot2]ggplot2是用于高级探索性数据分析的几个新软件库之一;请参见“可视化多个变量”:

ggplot(kc_tax0, (aes(x=SqFtTotLiving, y=TaxAssessedValue))) +
  stat_binhex(color='white') +
  theme_bw() +
  scale_fill_gradient(low='white', high='black') +
  labs(x='Finished Square Feet', y='Tax-Assessed Value')

Python中,可以使用pandas数据帧的hexbin方法轻松绘制六角形散点图:

ax = kc_tax0.plot.hexbin(x='SqFtTotLiving', y='TaxAssessedValue',
                         gridsize=30, sharex=False, figsize=(5, 4))
ax.set_xlabel('Finished Square Feet')
ax.set_ylabel('Tax-Assessed Value')

税务评估价值与完成*方英尺的六角形分箱图。

图 1-8。税务评估价值与完成*方英尺的六角形分箱图

图 1-9 使用等高线覆盖在散点图上以可视化两个数值变量之间的关系。这些等高线本质上是两个变量的地形图;每个等高线带代表着特定密度的点,随着接*“峰值”而增加。这幅图展示了与 图 1-8 相似的情况:主峰值的“北部”有一个次级峰值。这张图也是使用 ggplot2 中的内置 geom_density2d 函数创建的:

ggplot(kc_tax0, aes(SqFtTotLiving, TaxAssessedValue)) +
  theme_bw() +
  geom_point(alpha=0.1) +
  geom_density2d(color='white') +
  labs(x='Finished Square Feet', y='Tax-Assessed Value')

seabornkdeplot 函数在 Python 中创建了一个等高线图:

ax = sns.kdeplot(kc_tax0.SqFtTotLiving, kc_tax0.TaxAssessedValue, ax=ax)
ax.set_xlabel('Finished Square Feet')
ax.set_ylabel('Tax-Assessed Value')

税评估值与完成*方英尺的等高线图。

图 1-9. 税评估值与完成*方英尺的等高线图

其他类型的图表用于显示两个数值变量之间的关系,包括 热力图。热力图、六边形分箱图和等高线图都能直观地展示二维密度。因此,它们是直方图和密度图的自然类比。

两个分类变量

总结两个分类变量的有用方法是列联表——按类别统计的表格。 表 1-8 显示了个人贷款等级与该贷款结果之间的列联表。这些数据来自于同行借贷业务领域的领先者 Lending Club 提供的数据。等级从 A(高)到 G(低)。结果可以是全额支付、当前、逾期或已冲销(不预期收回贷款余额)。此表显示了计数和行百分比。高等级贷款的逾期/冲销百分比与低等级贷款相比非常低。

表 1-8. 贷款等级和状态的列联表

Grade Charged off Current Fully paid Late Total
A 1562 50051 20408 469 72490
0.022 0.690 0.282 0.006 0.161
B 5302 93852 31160 2056 132370
0.040 0.709 0.235 0.016 0.294
C 6023 88928 23147 2777 120875
0.050 0.736 0.191 0.023 0.268
D 5007 53281 13681 2308 74277
0.067 0.717 0.184 0.031 0.165
E 2842 24639 5949 1374 34804
0.082 0.708 0.171 0.039 0.077
F 1526 8444 2328 606 12904
0.118 0.654 0.180 0.047 0.029
G 409 1990 643 199 3241
0.126 0.614 0.198 0.061 0.007
Total 22671 321185 97316 9789 450961

列联表可以只查看计数,也可以包括列和总百分比。在 Excel 中,数据透视表可能是创建列联表最常用的工具。在 R 中,descr 包中的 CrossTable 函数用于生成列联表,以下代码用于创建 表 1-8:

library(descr)
x_tab <- CrossTable(lc_loans$grade, lc_loans$status,
                    prop.c=FALSE, prop.chisq=FALSE, prop.t=FALSE)

pivot_table 方法在 Python 中创建数据透视表。aggfunc 参数允许我们获取计数。计算百分比稍微复杂一些:

crosstab = lc_loans.pivot_table(index='grade', columns='status',
                                aggfunc=lambda x: len(x), margins=True) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)

df = crosstab.loc['A':'G',:].copy() ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/2.png)
df.loc[:,'Charged Off':'Late'] = df.loc[:,'Charged Off':'Late'].div(df['All'],
                                                                    axis=0) ![3](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/3.png)
df['All'] = df['All'] / sum(df['All']) ![4](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/4.png)
perc_crosstab = df

1

margins 关键字参数将添加列和行总和。

2

我们创建一个忽略列总和的数据透视表副本。

3

我们通过行总和划分行。

4

我们将 'All' 列除以其总和。

分类和数值数据

箱线图(见“百分位数和箱线图”)是一种简单的方式,用于根据分类变量比较数值变量的分布。例如,我们可能想比较不同航空公司的航班延误百分比。Figure 1-10 展示了一个月内由于航空公司控制内的延误占比:

boxplot(pct_carrier_delay ~ airline, data=airline_stats, ylim=c(0, 50))

pandasboxplot 方法接受 by 参数,将数据集分组并创建各自的箱线图:

ax = airline_stats.boxplot(by='airline', column='pct_carrier_delay')
ax.set_xlabel('')
ax.set_ylabel('Daily % of Delayed Flights')
plt.suptitle('')

航空公司延误百分比的箱线图。

Figure 1-10. 航空公司延误百分比的箱线图

阿拉斯加以最少的延误脱颖而出,而美国航空的延误最多:美国航空的下四分位数高于阿拉斯加的上四分位数。

小提琴图,由[Hintze-Nelson-1998]介绍,是箱线图的一种改进,绘制了密度估计,y 轴上显示了密度。密度是镜像和翻转的,形成填充的形状,类似于小提琴的形状。小提琴图的优势在于它可以显示箱线图中不易察觉的分布细微差别。另一方面,箱线图更清晰地显示了数据中的异常值。在 ggplot2 中,可以使用 geom_violin 函数创建小提琴图如下:

ggplot(data=airline_stats, aes(airline, pct_carrier_delay)) +
  ylim(0, 50) +
  geom_violin() +
  labs(x='', y='Daily % of Delayed Flights')

小提琴图可通过 seaborn 包的 violinplot 方法创建:

ax = sns.violinplot(airline_stats.airline, airline_stats.pct_carrier_delay,
                    inner='quartile', color='white')
ax.set_xlabel('')
ax.set_ylabel('Daily % of Delayed Flights')

相应的图表显示在 Figure 1-11。小提琴图显示了阿拉斯加和达美航空在接*零的分布集中,这种现象在箱线图中不太明显。您可以通过将 geom_boxplot 添加到图表中结合箱线图和小提琴图(尤其在使用颜色时效果更佳)。

航空公司延误百分比的小提琴图。

Figure 1-11. 航空公司延误百分比的小提琴图

可视化多个变量

用于比较两个变量的图表类型——散点图、六边形分箱图和箱线图——可以通过条件化的概念轻松扩展到更多变量。例如,请回顾图 1-8,展示了房屋完成*方英尺与其税评估值之间的关系。我们观察到有一组房屋具有较高的每*方英尺税评估值。深入研究,图 1-12 考虑了地理位置的影响,通过绘制一组邮政编码的数据。现在情况清晰多了:某些邮政编码(98105、98126)的税评估值要高得多,而其他邮政编码(98108、98188)较低。这种差异导致了我们在图 1-8 中观察到的集群。

我们使用ggplot2facets(或条件变量,本例中为邮政编码)创建了图 1-12:

ggplot(subset(kc_tax0, ZipCode %in% c(98188, 98105, 98108, 98126)),
         aes(x=SqFtTotLiving, y=TaxAssessedValue)) +
  stat_binhex(color='white') +
  theme_bw() +
  scale_fill_gradient(low='white', high='blue') +
  labs(x='Finished Square Feet', y='Tax-Assessed Value') +
  facet_wrap('ZipCode') ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)

1

使用ggplot函数facet_wrapfacet_grid来指定条件变量。

按邮政编码划分的税评估值与完成*方英尺。

图 1-12。按邮政编码划分的税评估值与完成*方英尺

大多数Python包基于Matplotlib进行可视化。虽然原则上可以使用Matplotlib创建分面图,但代码可能会变得复杂。幸运的是,seaborn有一种相对简单的方法来创建这些图形:

zip_codes = [98188, 98105, 98108, 98126]
kc_tax_zip = kc_tax0.loc[kc_tax0.ZipCode.isin(zip_codes),:]
kc_tax_zip

def hexbin(x, y, color, **kwargs):
    cmap = sns.light_palette(color, as_cmap=True)
    plt.hexbin(x, y, gridsize=25, cmap=cmap, **kwargs)

g = sns.FacetGrid(kc_tax_zip, col='ZipCode', col_wrap=2) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
g.map(hexbin, 'SqFtTotLiving', 'TaxAssessedValue',
      extent=[0, 3500, 0, 700000]) ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/2.png)
g.set_axis_labels('Finished Square Feet', 'Tax-Assessed Value')
g.set_titles('Zip code {col_name:.0f}')

1

使用参数colrow来指定条件变量。对于单个条件变量,使用colcol_wrap将分面图包装成多行。

2

map方法调用hexbin函数,使用原始数据集的子集来绘制不同邮政编码的六边形分箱图。extent定义了 x 轴和 y 轴的限制。

在图形系统中,条件变量的概念是由 Rick Becker、Bill Cleveland 等人在贝尔实验室开发的Trellis graphics中首次提出的[Trellis-Graphics]。这一思想已经传播到各种现代图形系统,例如R语言中的lattice[lattice]ggplot2包,以及Python中的seaborn[seaborn]Bokeh[bokeh]模块。条件变量也是诸如 Tableau 和 Spotfire 等商业智能*台的核心组成部分。随着计算能力的大幅提升,现代可视化*台已远远超出探索性数据分析的初衷。然而,半个世纪前开发的关键概念和工具(例如简单的箱线图)仍然构成这些系统的基础。

进一步阅读

  • 用 R 进行现代数据科学,作者 Benjamin Baumer,Daniel Kaplan 和 Nicholas Horton(Chapman & Hall/CRC Press,2017),对“图形语法”(在ggplot中的“gg”)有出色的阐述。

  • ggplot2: 数据分析的优雅图形,作者 Hadley Wickham(Springer,2009),是ggplot2的创建者的一个优秀资源。

  • Josef Fruehwald 有一个关于ggplot2的基于 Web 的教程。

概要

探索性数据分析(EDA),由 John Tukey 开创,为数据科学领域奠定了基础。EDA 的关键思想是,基于数据的任何项目的第一步,也是最重要的步骤是查看数据。通过对数据进行总结和可视化,您可以获得宝贵的直觉和对项目的理解。

本章回顾了从简单的度量,如位置和变异的估计,到丰富的可视化展示,探索多个变量之间的关系,如图 1-12 中所示。开源社区开发的多种工具和技术,结合RPython语言的表现力,已经创造出大量探索和分析数据的方法。探索性分析应成为任何数据科学项目的基石。

第二章:数据和抽样分布

一种普遍的误解认为大数据时代意味着不再需要抽样。事实上,各种质量和相关性的数据大量增加反而强化了抽样作为一种工具,以有效处理各种数据并尽量减少偏差。即使在大数据项目中,预测模型通常也是通过样本进行开发和试验的。样本也用于各种测试中(例如,比较网页设计对点击效果的影响)。

图 2-1 展示了本章讨论的概念的基础图示——数据和抽样分布。左侧代表了一个在统计学中假定遵循某种未知分布的总体。右侧展示了仅有的样本数据及其经验分布。从左侧到右侧,使用了一个抽样过程(用箭头表示)。传统统计学非常关注左侧,使用基于总体强假设的理论。现代统计学转向右侧,这里不需要这样的假设。

一般来说,数据科学家无需关注左侧的理论性质,而应集中精力于抽样程序和手头的数据。当然也有一些显著的例外情况。有时数据来自可以建模的物理过程。最简单的例子是抛硬币:这遵循二项分布。任何真实的二项式情况(购买或不购买、欺诈或无欺诈、点击或不点击)都可以通过抛硬币(当然要考虑到头朝上的概率被修改)有效地建模。在这些情况下,通过理解总体,我们可以获得额外的洞察。

images/sampling_schematic.png

图 2-1 总体与样本

随机抽样和样本偏差

样本是从较大数据集中取出的一部分数据;统计学家称这个较大的数据集为总体。在统计学中,总体与生物学中的概念不同——它是一个大的、定义明确(但有时是理论的或想象的)的数据集。

随机抽样是一个过程,其中总体中每个可用成员在每次抽样中被选中的机会都是相等的。结果得到的样本称为简单随机样本。抽样可以有放回进行,即在每次抽样后将观察结果放回总体,以便可能在未来再次选择。或者可以无放回进行,此时一旦选择了观察结果,它们就不再可用于未来的抽样。

在基于样本进行估计或建模时,数据质量通常比数据量更重要。数据质量在数据科学中涉及数据点的完整性、格式的一致性、数据的清洁度和准确性。统计学则增加了代表性的概念。

经典例子是 1936 年的文学文摘民意测验,预测阿尔夫·兰登将战胜富兰克林·罗斯福。当时的主要期刊文学文摘对其所有订阅者以及额外的个人名单进行了民意调查,总计超过 1000 万人,并预测兰登将大获全胜。盖洛普民意调查创始人乔治·盖洛普每两周对仅 2000 人进行调查,并准确预测了罗斯福的胜利。差异在于被调查者的选择。

文学文摘选择了数量,几乎没有关注选择方法。他们最终对具有相对较高社会经济地位的人群进行了调查(他们自己的订阅者,加上那些由于拥有电话和汽车等奢侈品而出现在市场营销名单中的人)。结果产生了样本偏差;即样本在某些有意义且非随机方式上与其所代表的更大人口不同。术语非随机很重要——几乎没有一个样本,包括随机样本,在精确代表整体人口方面都是完全代表性的。样本偏差发生在差异有意义时,并且预计在以与第一个样本相同方式抽取的其他样本中将继续存在。

自我选择抽样偏见

在像 Yelp 这样的社交媒体网站上读到的餐厅、酒店、咖啡馆等的评论往往带有偏见,因为提交评论的人并非随机选取,而是自愿撰写评论。这导致了自我选择偏见——那些有动力撰写评论的人可能有不好的经历,可能与该机构有关联,或者可能只是与不撰写评论的人群不同类型的人。需要注意的是,虽然自我选择样本可能不可靠地反映真实情况,但在简单比较一个机构与类似机构时可能更可靠;同样的自我选择偏见可能适用于每个样本。

偏差

统计偏差是指由测量或抽样过程中产生的系统性测量或抽样误差。重要的区别应该在于由随机机会引起的错误与由偏差引起的错误。考虑一支枪射击目标的物理过程。它不会每次都精确命中目标的中心,甚至几乎不会。一个无偏的过程会产生误差,但是这些误差是随机的,并且不会明显偏向任何方向(见图 2-2)。在图 2-3 中展示的结果显示了一个有偏的过程——在 x 和 y 方向上仍然存在随机误差,但也存在偏差。子弹倾向于落在右上象限。

images/Target-scatter.png

图 2-2. 有真实瞄准的枪击散点图

images/Target-scatter-bias.png

图 2-3. 有偏瞄准的枪击散点图

偏差有不同的形式,可以是可观察的,也可以是看不见的。当结果确实表明偏差时(例如,通过参考基准或实际值),这通常表明统计模型或机器学习模型被错误规定,或者遗漏了重要变量。

随机选择

为了避免样本偏差问题,这也导致文摘预测兰登胜过罗斯福,乔治·盖洛普(如图 2-4 所示)选择了更科学的方法来实现代表美国选民的样本。现在有各种方法来实现代表性,但所有方法的核心都是随机抽样

盖洛普

图 2-4. 乔治·盖洛普,通过文摘的“大数据”失败而声名鹊起

随机抽样并不总是容易。正确定义可访问人口是关键。假设我们想生成客户的代表性概况,我们需要进行一项试点客户调查。这项调查需要具有代表性,但工作量很大。

首先,我们需要定义谁是客户。我们可能选择所有购买金额大于 0 的客户记录。我们是否包括所有过去的客户?是否包括退款?内部测试购买?经销商?无论是结算代理还是客户?

接下来,我们需要指定抽样程序。可以是“随机选择 100 名客户”。如果涉及从流量中抽样(例如实时客户交易或网页访问者),时间考虑可能很重要(例如工作日上午 10 点的网页访问者可能与周末晚上 10 点的网页访问者有所不同)。

分层抽样中,将人口分成分层,并从每个分层中进行随机抽样。政治民意调查员可能希望了解白人、黑人和西班牙裔的选举偏好。从总体中进行简单随机抽样可能会产生过少的黑人和西班牙裔样本,因此在分层抽样中,这些分层可以加权,以获得等效的样本量。

规模与质量:什么时候规模重要?

在大数据时代,有时候较小的数据量更好。花在随机抽样上的时间和精力不仅可以减少偏差,还可以更多地关注数据探索和数据质量。例如,缺失数据和异常值可能包含有用的信息。追踪数百万条记录中的缺失值或评估异常值可能成本过高,但在数千条记录的样本中进行这样的操作可能是可行的。数据绘图和手动检查在数据量过大时会拖累工作进度。

当需要大量数据时

大数据的价值经典场景是当数据不仅大而且稀疏时。考虑到谷歌收到的搜索查询,其中列是术语,行是单个搜索查询,单元格的值要么是 0,要么是 1,这取决于查询是否包含术语。目标是确定给定查询的最佳预测搜索目的地。英语语言中有超过 15 万个词汇,谷歌每年处理超过一万亿次查询。这产生了一个巨大的矩阵,其中绝大多数条目都是“0”。

这是一个真正的大数据问题——只有当积累了如此庞大的数据量时,才能为大多数查询返回有效的搜索结果。而且数据积累得越多,结果就越好。对于热门搜索词来说,这并不是问题——针对某个特定时间流行的几个极为热门主题,可以相对快速地找到有效数据。现代搜索技术的真正价值在于能够为各种各样的搜索查询返回详细和有用的结果,包括那些频率可能只有百万分之一的查询。

请考虑搜索短语“瑞奇·里卡多和小红帽”。在互联网早期,这个查询可能会返回有关乐队领队瑞奇·里卡多以及他出现的电视节目I Love Lucy和儿童故事小红帽的结果。这两个单独的项目可能有很多搜索引用,但组合查询的结果可能非常少。现在,随着积累了数万亿个搜索查询,这个搜索查询返回的是确切的I Love Lucy集,其中瑞奇用戏剧化的方式向他的婴儿儿子讲述小红帽的故事,混合了英语和西班牙语。

请记住,实际相关记录的数量可能只需数千个就足够有效。然而,需要数万亿的数据点来获取这些相关记录(当然,随机抽样是无助的)。另请参见“长尾分布”。

样本均值与总体均值

符号 x ¯(发音为“x-bar”)用于表示来自总体的样本的均值,而 μ 用于表示总体的均值。为什么要区分?关于样本的信息是观察到的,而关于大群体的信息通常是从较小的样本中推断出来的。统计学家喜欢在符号中保持这两者的区分。

进一步阅读

  • 在 Ronald Fricker 的章节“在线调查的抽样方法”中,《The SAGE Handbook of Online Research Methods》第 2 版,由 Nigel G. Fielding、Raymond M. Lee 和 Grant Blank 编辑(SAGE Publications,2016)中可以找到有关抽样程序的有用审查。这一章节包括通常基于成本或可行性原因而使用的随机抽样修改的审查。

  • 文学文摘投票失败的故事可以在Capital Century website找到。

选择偏差

引用约基·贝拉的话:如果你不知道在找什么,那就努力找,你会找到的。

选择偏差是指有意或无意地选择数据的做法,以导致误导性或短暂的结论。

如果你提出一个假设并进行设计良好的实验来测试它,你可以对结论有很高的信心。然而,情况往往并非如此。通常,人们会查看现有数据并尝试识别模式。但这些模式是真实的吗?还是它们只是数据窥探的产物——即在数据中进行广泛搜索,直到发现有趣的东西?统计学家有一句话说:“如果你折磨数据足够长时间,迟早它会招供。”

当你使用实验验证假设时,你可以发现一种现象,与通过浏览现有数据发现的现象有所不同,这可以通过以下思想实验来阐明。

想象一下,有人告诉你他们可以抛硬币,使其在接下来的 10 次抛掷中都能正面朝上。你对此提出质疑(相当于一个实验),他们继续抛掷硬币 10 次,所有抛掷都正面朝上。显然,你会认为这个人有些特殊的天赋——10 次硬币抛掷都正面朝上的概率仅为 1/1,000。

现在想象一下,体育场的播音员要求出席的 20,000 人每人抛硬币 10 次,并在抛出 10 次正面时向礼官报告。在体育场中,某人得到 10 次正面的机会非常高(超过 99%——这是没有人得到 10 次正面的概率)。显然,事后选择体育场中得到 10 次正面的人(或人们)并不表示他们有任何特殊的天赋——这很可能是运气。

由于反复审查大数据集是数据科学的一个重要价值主张,选择偏差是一个需要担心的问题。数据科学家特别关注的一种选择偏差形式是约翰·埃尔德(Elder Research 创始人,一家著名的数据挖掘咨询公司)所称的vast search effect。如果你反复运行不同的模型并使用大数据集提出不同的问题,你必定会找到一些有趣的东西。但你找到的结果是否真的有趣,还是偶然的离群值?

我们可以通过使用一个保留集,有时甚至多个保留集,来验证性能。埃尔德还提倡使用他所称的目标洗牌(本质上是一种排列检验)来测试数据挖掘模型暗示的预测关联的有效性。

统计学中典型的选择偏差形式,除了广泛的搜索效应外,还包括非随机抽样(见“随机抽样和样本偏差”)、挑选数据、选择强调特定统计效应的时间间隔以及在结果看起来“有趣”时停止实验。

回归到*均

回归到*均是指在给定变量上连续测量的现象:极端观察往往会被更加中心的观察所跟随。对极端值给予特殊关注和意义可能导致一种形式的选择偏差。

体育迷熟悉“年度最佳新秀,第二年低迷”现象。在一个赛季开始职业生涯的运动员中(新秀班),总会有一个表现比其他所有人都好的。一般来说,这个“年度最佳新秀”在他的第二年表现不会那么好。为什么呢?

几乎所有主要的体育运动,至少那些使用球或冰球的运动,都有两个因素影响整体表现:

  • 技巧

  • 运气

回归到*均是特定形式选择偏差的结果。当我们选择表现最好的新秀时,技巧和好运可能都起到了作用。在他的下个赛季,技巧仍将存在,但很多时候好运不会再出现,所以他的表现会下降——即会回归。这种现象最早由弗朗西斯·高尔顿在 1886 年首次发现[高尔顿-1886],他在与遗传倾向的关联中写到了它;例如,极端高个子男子的子女往往不会像他们的父亲那样高(见图 2-5)。

高尔顿

图 2-5. 高尔顿的研究发现了回归到*均现象。
警告

回归到*均,意思是“回到原点”,与统计建模方法线性回归是不同的,线性回归是估计预测变量与结果变量之间的线性关系。

进一步阅读

  • 克里斯托弗·J·帕努奇和埃德温·G·威尔金斯在(令人惊讶的不是统计学期刊)整形外科(2010 年 8 月)中的文章“识别和避免研究中的偏差”对可以进入研究的各种类型的偏差进行了优秀的回顾,包括选择偏差。

  • 迈克尔·哈里斯的文章“被随机性愚弄:通过选择偏差”提供了对股市交易方案中选择偏差考虑的有趣回顾,从交易者的角度。

统计量的抽样分布

统计学术语中,一个统计量的抽样分布指的是同一总体中许多样本统计量的分布。经典统计学很大一部分关注于从(小)样本推断出(非常大)总体。

通常,样本是为了测量某些东西(使用样本统计量)或者建模某些东西(使用统计或机器学习模型)而抽取的。由于我们的估计或模型是基于样本的,所以可能存在误差;如果我们抽取不同的样本,可能会有所不同。因此,我们对可能存在的不同情况感兴趣—一个关键问题是抽样变异性。如果我们有大量数据,我们可以抽取额外的样本并直接观察样本统计量的分布。通常,我们将使用尽可能多的容易获取的数据来计算我们的估计或模型,因此从总体中抽取额外样本的选择并不容易。

警告

重要的是要区分个别数据点的分布,称为数据分布,和样本统计量的分布,称为抽样分布

样本统计量如均值的分布可能比数据本身的分布更加规则和钟形。统计量基于的样本越大,这种情况就越明显。此外,样本越大,样本统计量的分布就越窄。

这在一个使用 LendingClub 的贷款申请人年收入的例子中进行了说明(见“一个小例子:预测贷款违约”中对数据的描述)。从这些数据中取三个样本:1,000 个值的样本,5 个值均值的 1,000 个样本,以及 20 个值均值的 1,000 个样本。然后绘制每个样本的直方图,生成图 2-6。

贷款直方图

图 2-6. 1,000 名贷款申请人年收入的直方图(顶部),然后 5 个申请人的 1,000 个均值(中部),最后是 20 个申请人的 1,000 个均值(底部)

个别数据值的直方图广泛分布且向较高值倾斜,这在收入数据中是可以预期的。5 和 20 的均值直方图越来越紧凑,更呈钟形。以下是用可视化包ggplot2生成这些直方图的R代码:

library(ggplot2)
# take a simple random sample
samp_data <- data.frame(income=sample(loans_income, 1000),
                        type='data_dist')
# take a sample of means of 5 values
samp_mean_05 <- data.frame(
  income = tapply(sample(loans_income, 1000*5),
                  rep(1:1000, rep(5, 1000)), FUN=mean),
  type = 'mean_of_5')
# take a sample of means of 20 values
samp_mean_20 <- data.frame(
  income = tapply(sample(loans_income, 1000*20),
                  rep(1:1000, rep(20, 1000)), FUN=mean),
  type = 'mean_of_20')
# bind the data.frames and convert type to a factor
income <- rbind(samp_data, samp_mean_05, samp_mean_20)
income$type = factor(income$type,
                     levels=c('data_dist', 'mean_of_5', 'mean_of_20'),
                     labels=c('Data', 'Mean of 5', 'Mean of 20'))
# plot the histograms
ggplot(income, aes(x=income)) +
  geom_histogram(bins=40) +
  facet_grid(type ~ .)

Python代码使用seabornFacetGrid来显示这三个直方图:

import pandas as pd
import seaborn as sns

sample_data = pd.DataFrame({
    'income': loans_income.sample(1000),
    'type': 'Data',
})
sample_mean_05 = pd.DataFrame({
    'income': [loans_income.sample(5).mean() for _ in range(1000)],
    'type': 'Mean of 5',
})
sample_mean_20 = pd.DataFrame({
    'income': [loans_income.sample(20).mean() for _ in range(1000)],
    'type': 'Mean of 20',
})
results = pd.concat([sample_data, sample_mean_05, sample_mean_20])

g = sns.FacetGrid(results, col='type', col_wrap=1, height=2, aspect=2)
g.map(plt.hist, 'income', range=[0, 200000], bins=40)
g.set_axis_labels('Income', 'Count')
g.set_titles('{col_name}')

中心极限定理

我们刚才描述的现象被称为中心极限定理。它表明,从多个样本中抽取的均值将类似于熟悉的钟形正态曲线(见“正态分布”),即使源总体不服从正态分布,只要样本大小足够大且数据的偏离程度不是太大。中心极限定理允许使用类似 t 分布的正态*似公式来计算推断的抽样分布,例如置信区间和假设检验。

中心极限定理在传统统计学教材中受到广泛关注,因为它是假设检验和置信区间机制的基础,这些机制本身在这些教材中占据了相当大的篇幅。数据科学家应该意识到这一角色;然而,由于正式的假设检验和置信区间在数据科学中的作用不大,而自助法(见“自助法”)在任何情况下都可用,因此中心极限定理在数据科学实践中并不如此核心。

标准误差

标准误差是用于统计量抽样分布中的变异性的单一度量。可以使用基于样本值标准偏差s和样本大小n的统计量来估计标准误差:

Standard error = S E = s n

随着样本大小的增加,标准误差减小,这与图 2-6 中观察到的情况相对应。标准误差与样本大小之间的关系有时被称为n 的*方根规则:要将标准误差减少一半,样本大小必须增加四倍。

标准误差公式的有效性源于中心极限定理。实际上,你不需要依赖中心极限定理来理解标准误差。考虑以下测量标准误差的方法:

  1. 从总体中收集一些全新的样本。

  2. 对于每个新样本,计算统计量(例如*均值)。

  3. 计算步骤 2 中计算出的统计量的标准偏差;将其用作标准误差的估计值。

在实践中,收集新样本来估计标准误差通常是不可行的(并且在统计上非常浪费)。幸运的是,事实证明,不必抽取全新样本;相反,可以使用自助法重新取样。在现代统计学中,自助法已成为估计标准误差的标准方法。它可用于几乎任何统计量,并不依赖于中心极限定理或其他分布假设。

标准差与标准误差

不要混淆标准偏差(用于衡量单个数据点的变异性)与标准误差(用于衡量样本统计量的变异性)。

进一步阅读

David Lane 的在线统计多媒体资源具有一个有用的模拟,允许您选择一个样本统计量、一个样本大小和迭代次数,并可视化生成的频率分布直方图。

自举法

一种简单而有效的估计统计分布或模型参数的方法是从样本本身进行额外抽样,有放回地,然后对每个重新抽样计算统计量或模型。这个过程称为bootstrap,并不一定涉及数据或样本统计量正态分布的任何假设。

从概念上讲,你可以将 bootstrap 想象成复制原始样本成千上万次或百万次,这样你就有一个假设的总体,它包含来自原始样本的所有知识(只是更大)。然后,您可以从这个假设的总体中抽样,以估计抽样分布;参见图 2-7。

images/Bootstrap-schematic-1.png

图 2-7。bootstrap 的概念

实际上,并不需要大量复制样本。我们只需在每次抽取后替换每个观察值;也就是说,我们是有放回地抽样。通过这种方式,我们有效地创建了一个无限的总体,在这个总体中,从一个抽样到另一个抽样中抽取元素的概率保持不变。对于样本量为n的自举重新抽样*均值,算法如下:

  1. 抽取一个样本值,记录它,然后替换它。

  2. 重复n次。

  3. 记录n个重新抽样值的*均值。

  4. 重复步骤 1–3 R次。

  5. 利用R的结果来:

    1. 计算它们的标准差(这估计了样本均值的标准误差)。

    2. 绘制直方图或箱线图。

    3. 寻找置信区间。

R,bootstrap 迭代次数,有些是任意设置的。你做的迭代次数越多,标准误差或置信区间的估计就越准确。此过程的结果是一组 bootstrap 样本统计或估计的模型参数,您可以检查它们的变化程度。

Rboot将这些步骤结合在一个函数中。例如,以下将 bootstrap 应用于贷款人收入的情况:

library(boot)
stat_fun <- function(x, idx) median(x[idx])
boot_obj <- boot(loans_income, R=1000, statistic=stat_fun)

函数stat_fun计算给定索引idx的样本中位数。结果如下:

Bootstrap Statistics :
    original   bias    std. error
t1*    62000 -70.5595    209.1515

原始的中位数估计值为$62,000。自举分布表明,估计值存在约为–$70 的bias,标准误差为$209。算法连续运行的结果会略有不同。

主要的Python包不提供 bootstrap 方法的实现。可以使用scikit-learn中的resample方法来实现:

results = []
for nrepeat in range(1000):
    sample = resample(loans_income)
    results.append(sample.median())
results = pd.Series(results)
print('Bootstrap Statistics:')
print(f'original: {loans_income.median()}')
print(f'bias: {results.mean() - loans_income.median()}')
print(f'std. error: {results.std()}')

引导式自举可用于多变量数据,其中行作为单位进行抽样(见图 2-8)。例如,可以在引导样本数据上运行模型,以估计模型参数的稳定性(变异性),或者提高预测能力。对于分类和回归树(也称为决策树),在引导样本上运行多棵树,然后对它们的预测进行*均(或者在分类中进行多数投票),通常比使用单棵树效果更好。这个过程称为装袋(缩写为“引导聚合”;见“装袋和随机森林”)。

images/Bootstrap-multivariate-schematic.png

图 2-8. 多变量引导式抽样

引导式自举的重复重抽样在概念上很简单,经济学家兼人口统计学家朱利安·西蒙(Julian Simon)在他 1969 年的文本《社会科学基础研究方法》(Random House)中发表了包括引导式自举在内的多种重抽样实例汇编。然而,它在计算上是非常密集的,而在广泛可用计算能力之前并不可行。该技术因斯坦福统计学家布拉德利·埃弗隆在 1970 年代末至 1980 年代初的几篇期刊文章和一本书的出版而得名并流行起来。它特别受到那些使用统计学但不是统计学家的研究人员欢迎,适用于数学*似不容易得到的指标或模型。均值的抽样分布自 1908 年已得到很好的建立;许多其他指标的抽样分布并未。引导式自举可以用于样本量的确定;尝试不同的n值以查看抽样分布如何受影响。

当引导式自举首次提出时,它遭遇了相当多的怀疑;对许多人来说,它具有从稻草中纺出金的神奇色彩。这种怀疑源于对引导式自举目的的误解。

警告

引导式自举不能补偿样本量不足;它不会创建新数据,也不会填补现有数据集中的空缺。它仅仅告诉我们,当从类似于我们原始样本的总体中抽取大量额外样本时,它们会如何行为。

重抽样与引导式自举

有时术语重抽样引导式自举可以作为同义词使用,正如刚才所概述的。更多情况下,重抽样还包括置换程序(见“置换检验”),其中多个样本被合并,并且抽样可以无替换地进行。无论如何,术语引导式自举总是指从观察数据集中有替换地抽样。

进一步阅读

  • 引导式自举法,由布拉德利·埃弗隆(Bradley Efron)和罗伯特·蒂布什拉尼(Robert Tibshirani)合著(查普曼与霍尔出版社,1993 年),是第一本有关引导式自举法的书籍。至今仍广受欢迎。

  • 《统计科学》(第 18 卷,第 2 期)2003 年 5 月份关于 Bootstrap 的回顾(在彼得·霍尔的“Bootstrap 的简短前史”中讨论了其他先例,朱利安·西蒙于 1969 年首次发表了 Bootstrap 的初始论文)。

  • 参见《统计学习导论》(Springer,2013)由加雷斯·詹姆斯、丹妮拉·威特、特雷弗·哈斯蒂和罗伯特·蒂布沙尼编写,关于 Bootstrap 和尤其是装袋的章节。

置信区间

频率表、直方图、箱线图和标准误差都是了解样本估计误差潜在误差的方式。置信区间是另一种方式。

人类天生对不确定性有抵触情绪;人们(尤其是专家)很少说“我不知道”。分析师和经理在承认不确定性的同时,当将估计呈现为单一数字(点估计)时,仍然过度信任估计。将估计呈现为一个范围而不是单一数字是抵消这种倾向的一种方式。置信区间以一种根植于统计抽样原理的方式来做到这一点。

置信区间始终带有一个覆盖水*,以百分比(高)表示,例如 90%或 95%。将 90%置信区间视为以下方式之一:它是包围样本统计的自助抽样分布的中心 90%的区间(参见“Bootstrap”)。更一般地,样本估计周围的x%置信区间应该在*均情况下,包含相似的样本估计x%的时间(当遵循类似的抽样过程时)。

给定大小为n的样本和感兴趣的样本统计量,Bootstrap 置信区间的算法如下:

  1. 从数据中有放回地抽取大小为n的随机样本(重新抽样)。

  2. 记录所重新抽样的兴趣统计量。

  3. 重复步骤 1 至 2 多(R)次。

  4. 对于x%置信区间,从分布的两端修剪[(100-x) / 2]%的R重新抽样结果。

  5. 修剪点是x%自助法置信区间的端点。

图 2-9 显示了一个 90%置信区间,用于贷款申请人的年收入均值,基于一个样本,样本容量为 20,均值为$62,231。

images/bootstrap-CI.png

图 2-9. 基于 20 个样本的贷款申请人年收入的 Bootstrap 置信区间

Bootstrap 是一个通用工具,可用于生成大多数统计数据或模型参数的置信区间。根据半个世纪以上的无计算机统计分析的历史,统计教材和软件还将引用由公式生成的置信区间,特别是 t 分布(参见“学生 t 分布”)。

注意

当然,当我们有样本结果时,我们真正感兴趣的是,“真实值位于某个区间的概率是多少?”这实际上并不是置信区间回答的问题,但却是大多数人解释答案的方式。

与置信区间相关的概率问题始于短语“给定抽样程序和人口,什么是……的概率?”要反过来,“给定样本结果,什么是(人群的真实情况)的概率?”涉及到更复杂的计算和更深层次的难题。

与置信区间相关联的百分比被称为置信水*。置信水*越高,区间越宽。此外,样本越小,区间越宽(即不确定性越大)。这两者都是有道理的:你希望越有信心,而且数据越少,你必须使置信区间足够宽以确保捕捉真实值。

注意

对于数据科学家而言,置信区间是一个工具,用于了解样本结果可能的变异程度。数据科学家会利用这些信息,不是为了发表学术论文或向监管机构提交结果(像研究员可能会做的那样),而更有可能是为了传达估计误差的潜在风险,并且也许了解是否需要更大的样本。

进一步阅读

  • 有关置信区间的自举方法,请参见Introductory Statistics and Analytics: A Resampling Perspective(Peter Bruce 著,Wiley,2014)或Statistics: Unlocking the Power of Data,第 2 版,由 Robin Lock 及其四位家庭成员(Wiley,2016)撰写。

  • 工程师需要了解其测量精度的需求,或许比大多数学科更多地使用置信区间,Modern Engineering Statistics(Thomas Ryan 著,Wiley,2007)讨论了置信区间。它还审视了一种同样有用但受到较少关注的工具:预测区间(围绕单个值的区间,与*均值或其他摘要统计不同)。

正态分布

传统统计学中标志性的钟形正态分布。^(1) 样本统计量的分布通常呈正态分布的事实使其成为开发数学公式以*似这些分布的强大工具。

在正态分布中(图 2-10),数据的 68%位于*均值的一个标准偏差范围内,95%位于两个标准偏差范围内。

警告

通常存在一个误解,即正态分布之所以叫正态分布,是因为大多数数据都遵循正态分布——即它是正常的。大多数在典型数据科学项目中使用的变量——事实上,大部分原始数据——并不是正态分布的:参见“长尾分布”。正态分布的实用性源于许多统计量在其抽样分布中正态分布的。即便如此,正态性假设通常是最后的手段,当没有经验概率分布或自助抽样分布时才使用。

Normal_dist.PNG

图 2-10. 正态曲线
注意

正态分布也称为高斯分布,以 18 世纪末至 19 世纪初的杰出德国数学家卡尔·弗里德里希·高斯(Carl Friedrich Gauss)的名字命名。正态分布以前的另一个名称是“误差”分布。从统计学角度看,误差是实际值与样本均值等统计估计值之间的差异。例如,标准差(参见“变异性估计”)基于数据均值的误差。高斯对正态分布的研究源于他对天文测量误差的研究,这些误差被发现是正态分布的。

标准正态分布和 QQ 图

一个标准正态分布是指 x 轴上的单位以标准差表示离均值的距离。要将数据与标准正态分布进行比较,您需要减去均值,然后除以标准差;这也称为归一化标准化(参见“标准化(归一化,z 分数)”)。请注意,这里的“标准化”与数据库记录标准化(转换为通用格式)无关。变换后的值称为z 分数,正态分布有时也称为z 分布

QQ 图用于直观地确定样本与指定分布(在本例中是正态分布)有多接*。QQ 图将z分数按从低到高的顺序排列,并在 y 轴上绘制每个值的z分数;x 轴是该值排名对应的正态分布的分位数。由于数据已经标准化,单位对应于离均值的标准差数目。如果点大致落在对角线上,那么样本分布可以视为接*正态分布。图 2-11 显示了从正态分布中随机生成的 100 个值的 QQ 图;如预期的,点紧密跟随直线。可以使用R中的qqnorm函数生成此图:

norm_samp <- rnorm(100)
qqnorm(norm_samp)
abline(a=0, b=1, col='grey')

Python中,使用方法scipy.stats.probplot来创建 QQ 图:

fig, ax = plt.subplots(figsize=(4, 4))
norm_sample = stats.norm.rvs(size=100)
stats.probplot(norm_sample, plot=ax)

qqnorm.png

图 2-11. QQ 图,显示从标准正态分布中抽取的 100 个样本的样本
警告

将数据转换为z-分数(即标准化或归一化数据)并不会使数据呈正态分布。它只是将数据放置在与标准正态分布相同的比例上,通常用于比较目的。

长尾分布

尽管正态分布在统计学上具有重要历史意义,并与名称所暗示的相反,数据通常不服从正态分布。

尽管正态分布通常适用且有用于误差分布和样本统计数据的分布,但它通常不描述原始数据的分布。有时,分布是高度偏斜的(不对称的),例如收入数据;或者分布可以是离散的,例如二项数据。对称和非对称的分布可能都有长尾。分布的尾部对应于极端值(小和大)。长尾及其防范在实际工作中广为认可。纳西姆·塔勒布提出了黑天鹅理论,预测异常事件,如股市崩盘,比正态分布预测的更有可能发生。

说明数据长尾特性的一个很好的例子是股票收益。图 2-12 显示了 Netflix(NFLX)每日股票收益的 QQ 图。这是通过 R 生成的:

nflx <- sp500_px[,'NFLX']
nflx <- diff(log(nflx[nflx>0]))
qqnorm(nflx)
abline(a=0, b=1, col='grey')

相应的 Python 代码是:

nflx = sp500_px.NFLX
nflx = np.diff(np.log(nflx[nflx>0]))
fig, ax = plt.subplots(figsize=(4, 4))
stats.probplot(nflx, plot=ax)

nflx_qnorm.png

图 2-12. Netflix(NFLX)的收益的 QQ 图

与图 2-11 相比,低值的点远低于线,高值的点远高于线,表明数据不服从正态分布。这意味着我们更有可能观察到极端值,而不是数据具有正态分布时所预期的。图 2-12 显示了另一个常见现象:在距离均值一个标准差内的数据点接*线。Tukey 将此现象称为数据在中间部分“正常”,但尾部较长(见[Tukey-1987])。

注意

关于将统计分布拟合到观察数据的任务,有大量的统计文献。要警惕对这项工作过于依赖数据的方法,这既是艺术也是科学。数据是变量,通常表面上看,可以符合多种形状和类型的分布。通常情况下,必须运用领域和统计知识来确定适合模拟给定情况的分布类型。例如,我们可能有关于服务器在许多连续五秒时间段内的互联网流量水*的数据。了解到“每个时间段事件”的最佳分布是泊松分布(见“泊松分布”)是有用的。

进一步阅读

  • 黑天鹅,纳西姆·尼古拉斯·塔勒布著(Random House,2010 年)

  • 统计分布及其应用手册,K. Krishnamoorthy 著(Chapman & Hall/CRC Press,2016 年)

学生 t-分布

t-分布是一个形状类似正态分布的分布,不过在尾部略微厚一些且更长。它被广泛用于描述样本统计量的分布。样本均值的分布通常呈现出 t-分布的形状,并且根据样本大小的不同有一系列不同的 t-分布。样本越大,t-分布就越接*正态分布。

t-分布通常被称为学生 t,因为它是由 W. S. 戈塞特于 1908 年在《生物统计学》上以“学生”名义发表的。戈塞特的雇主,吉尼斯啤酒厂,不希望竞争对手知道它正在使用统计方法,因此坚持要求戈塞特不在文章上使用自己的名字。

戈塞特想要回答的问题是“从一个更大的人口中抽取样本的均值的抽样分布是什么?”他首先进行了一项重采样实验——从一个包含 3000 个罪犯身高和左中指长度测量值的数据集中随机抽取了 4 个样本。(在这个时代,有很多关于罪犯数据的兴趣,以及发现犯罪倾向与生理或心理属性之间的相关性。)戈塞特将标准化的结果(z-分数)绘制在 x 轴上,频率绘制在 y 轴上。此外,他还推导出一个函数,现在被称为学生 t,并将这个函数拟合到样本结果上,绘制出比较(见图 2-13)。

Gosset-curve

图 2-13。戈塞特的重采样实验结果和拟合的 t-曲线(来自他 1908 年的《生物统计学》论文)

在考虑到抽样变异性后,可以将多种不同的统计量标准化到 t-分布上,以估计置信区间。考虑一个样本大小为n的样本,已计算出样本均值x ¯。如果s是样本标准偏差,那么样本均值周围的 90%置信区间为:

x ¯ ± t n-1 ( 0 . 05 ) · s n

其中t n-1 ( . 05 )是 t-统计量的值,具有(n – 1)个自由度(见“自由度”),在 t-分布的两端“截取”了 5%。t-分布已被用作样本均值、两个样本均值之间的差异、回归参数和其他统计量的分布的参考。

如果 1908 年计算能力普遍存在,统计学无疑会更多地依赖计算密集型的重新采样方法。由于缺乏计算机,统计学家转向数学和函数,如 t-分布,来*似抽样分布。计算机能力使得在 1980 年代实现了实际的重新采样实验,但到那时,t-分布和类似分布的使用已经深入嵌入教科书和软件中。

t-分布准确地描述样本统计量的行为需要该样本统计量的分布形状类似于正态分布。事实证明,样本统计量通常是正态分布的,即使底层总体数据不是(这一事实导致广泛应用 t-分布)。这使我们回到了众所周知的中心极限定理现象(参见 “中心极限定理”)。

注意

数据科学家需要了解 t-分布和中心极限定理吗?其实并不需要太多。t-分布用于经典统计推断,但并不是数据科学目的的核心。理解和量化不确定性和变化对数据科学家非常重要,但经验性的自助法抽样可以回答大多数有关抽样误差的问题。然而,数据科学家经常会在统计软件和R中的输出中遇到 t-统计量—例如,在 A/B 测试和回归中—因此熟悉其用途是有帮助的。

进一步阅读

  • 原始的 W.S. Gosset 论文于 1908 年发表在 Biometrika 上,可作为PDF获得。

  • 关于 t-分布的标准处理可以在 David Lane 的在线资源中找到。

二项分布

是/否(二项)结果是分析的核心,因为它们常常是决策或其他过程的最终结果;买/不买,点击/不点击,存活/死亡等。理解二项分布的核心是一组试验的概念,每个试验都有两种可能的结果和明确的概率。

例如,抛硬币 10 次是一个二项实验,共有 10 次试验,每次试验有两种可能的结果(正面或反面);见 图 2-14。此类是二元结果,无需具有 50/50 的概率。任何总和为 1.0 的概率都是可能的。在统计学中,将“1”结果称为成功结果是传统做法;同时,也普遍将“1”分配给更为罕见的结果。使用术语成功并不意味着结果是理想的或有益的,但确实表明了感兴趣的结果。例如,贷款违约或欺诈交易是相对不常见的事件,我们可能对其进行预测,因此它们被称为“1s”或“成功”。

images/Indian_Head_Buffalo_Nickel.png

图 2-14。美国五分镍的反面

二项分布是在给定试验次数(n)中的成功次数(x)的频率分布,每次试验中成功的指定概率(p)。有一系列的二项分布,取决于np的值。二项分布可以回答如下问题:

如果点击转化为销售的概率为 0.02,观察到 200 次点击中没有销售的概率是多少?

函数R dbinom 计算二项概率。例如:

dbinom(x=2, size=5, p=0.1)

将返回 0.0729,即在size=5 次试验中观察到x=2 次成功的概率,每次试验成功的概率为p=0.1。对于上述示例,我们使用x=0,size=200,p=0.02。使用这些参数,dbinom返回概率为 0.0176。

我们经常有兴趣确定在n次试验中观察到x或更少成功的概率。在这种情况下,我们使用函数pbinom

pbinom(2, 5, 0.1)

这将返回 0.9914,即在五次试验中观察到两次或更少成功的概率,每次试验成功的概率为 0.1。

scipy.stats模块实现了大量的统计分布。对于二项分布,可以使用函数stats.binom.pmfstats.binom.cdf

stats.binom.pmf(2, n=5, p=0.1)
stats.binom.cdf(2, n=5, p=0.1)

二项分布的均值为n × p;你也可以把这看作是n次试验中成功的预期数量,成功概率=p

方差为n × p ( 1 - p )。当试验次数足够多时(特别是当p接* 0.50 时),二项分布几乎与正态分布无法区分。事实上,使用大样本量计算二项概率是计算密集型的,大多数统计过程使用正态分布进行*似,具有均值和方差。

进一步阅读

  • 阅读有关“四叶玫瑰”的介绍,这是一种类似弹球的模拟设备,用于说明二项分布。

  • 二项分布是统计学入门的重要内容,所有统计学入门教材都会有一两章讲解它。

卡方分布

统计学中的一个重要概念是偏离期望,特别是关于类别计数。期望松散地定义为“数据中没有异常或值得注意的事物”(例如,变量之间没有相关性或可预测的模式)。这也称为“零假设”或“零模型”(参见“零假设”)。例如,您可能希望测试一个变量(例如,表示性别的行变量)是否独立于另一个变量(例如,表示“在工作中被提升”的列变量),并且您有数据表单元格中每个计数的计数。衡量结果偏离独立性零期望程度的统计量是卡方统计量。它是观察到的值与预期值之间的差异,除以预期值的*方根,再*方,然后在所有类别上求和。这个过程标准化了统计量,使其可以与参考分布进行比较。更一般地说,卡方统计量是衡量一组观察值“适合”指定分布的程度的指标(“拟合优度检验”)。它对于确定多个处理(“A/B/C...测试”)在其效果上是否不同非常有用。

卡方分布是从零模型中重复重新抽取的数据的统计分布——详见“卡方检验”以获取详细算法,以及数据表的卡方公式。一组计数的低卡方值表明它们与预期分布非常接*。高卡方值表明它们与预期值有显著差异。与不同自由度相关的各种卡方分布存在(例如,观测数——参见“自由度”)。

进一步阅读

  • 卡尔·皮尔逊和假设检验的诞生让卡方分布在现代统计学中占有重要位置——详细内容请参阅大卫·萨尔斯伯格的《品茶的女士:统计学如何在二十世纪改变科学》(W. H. Freeman,2001 年)。

  • 欲了解更详细内容,请参阅本书中关于卡方检验的章节(“卡方检验”)。

F-分布

在科学实验中的常见程序是在多个组中测试多种处理方法——比如在田地的不同区块上使用不同的肥料。这类似于卡方分布中提到的 A/B/C 测试(参见“卡方分布”),但我们处理的是连续测量值而不是计数。在这种情况下,我们关心的是组间均值差异是否超出了正常随机变异的预期。F 统计量衡量了这一点,是组间均值变异性与每个组内变异性(也称为残差变异性)之比。这种比较称为方差分析(见“ANOVA”)。F 统计量的分布是所有在空模型中通过随机排列数据时产生的值的频率分布,其中所有组均值相等。与不同自由度相关的多种 F 分布(例如,组数——见“自由度”)存在。F 的计算在 ANOVA 部分有详细说明。F 统计量还用于线性回归中,用于比较回归模型解释的变异与数据整体变异的比例。在回归和 ANOVA 例程中,RPython会自动产生 F 统计量。

进一步阅读

乔治·科布的《实验设计与分析导论》(Wiley,2008)详细阐述了方差分解,有助于理解方差分析(ANOVA)和 F 统计量。

泊松及相关分布

许多过程以给定的总体速率随机产生事件——例如访问者访问网站或汽车通过收费站(时间分布的事件);或者在一*方米布料中的瑕疵或每 100 行代码中的错别字(空间分布的事件)。

泊松分布

通过先前的聚合数据(例如,每年的流感感染数),我们可以估计单位时间或空间内事件的*均数(例如每天的感染数或每个人口普查单位的感染数)。我们可能还想知道一个单位的时间/空间与另一个单位相比有多大的不同。泊松分布告诉我们,当我们抽样许多这样的单位时,单位时间或空间内的事件分布。在处理排队问题时很有用,例如“我们需要多少能力才能确保在任何五秒内完全处理到达服务器上的互联网流量的 95%?”

泊松分布中的关键参数是λ,即 lambda。这是在指定的时间或空间间隔内发生的事件*均数。泊松分布的方差也是λ

作为排队仿真的一部分,从泊松分布生成随机数是一种常见的技术。R 中的rpois函数完成这一任务,只需两个参数:所需的随机数数量和 lambda 值:

rpois(100, lambda=2)

相应的scipy函数是stats.poisson.rvs

stats.poisson.rvs(2, size=100)

例如,以下代码将从均值为 2 的泊松分布生成 100 个随机数。例如,如果客户服务电话*均每分钟两次,此代码将模拟 100 分钟,返回这 100 分钟内每分钟的呼叫次数。

指数分布

使用与泊松分布中相同的参数 lambda,我们还可以对事件之间的时间分布进行建模:例如网站访问之间的时间或汽车抵达收费站之间的时间。它也用于工程学中的故障时间建模以及过程管理中的模拟,例如每个服务呼叫所需的时间。从指数分布生成随机数的R代码需要两个参数:n(要生成的数的数量)和rate(每个时间段的事件数量)。例如:

rexp(n=100, rate=0.2)

在函数stats.expon.rvs中,参数的顺序是颠倒的:

stats.expon.rvs(0.2, size=100)

该代码将从均值为 0.2 的指数分布生成 100 个随机数。因此,您可以用它来模拟 100 个时间间隔(以分钟为单位),表示服务电话之间的时间间隔,其中*均呼叫率为每分钟 0.2 次。

任何模拟研究中的关键假设,无论是对泊松分布还是指数分布,都是速率 lambda 在考虑的时间段内保持恒定。从全局角度来看,这很少是合理的;例如,道路交通或数据网络的流量会随着一天中的时间和一周中的日期变化。然而,时间段或空间区域通常可以分成足够均匀的片段,以使得在这些时间段内的分析或模拟是有效的。

估计故障率

在许多应用中,事件率 λ 已知或可以从先前的数据中估算出来。然而,对于罕见的事件,情况可能并非如此。例如,飞机发动机故障是足够罕见的(幸好),对于特定类型的发动机,可能没有足够的数据用于基于故障间隔时间的估计。如果根本没有数据,那么几乎没有依据可以估计事件率。不过,你可以进行一些猜测:如果在 20 小时后没有发生任何事件,那么可以相当肯定速率不是每小时 1 次。通过模拟或直接计算概率,你可以评估不同假设的事件率,并估计在其下的速率很不可能达到的阈值。如果有一些数据但不足以提供精确可靠的速率估计,可以应用拟合度检验(参见“卡方检验”)来评估各种速率与观察数据的拟合程度。

威布尔分布

在许多情况下,事件率并不会随时间保持恒定。如果变化的时间段远远长于事件之间的典型间隔,那就没有问题;你只需将分析分成相对恒定的速率的段落,如前所述。然而,如果事件率在时间间隔内发生变化,指数(或泊松)分布就不再有用。在机械故障中很可能会出现这种情况——随着时间的推移,故障风险增加。威布尔分布是指数分布的一种扩展,其中允许事件率随时间变化,由形状参数 β 指定。如果 β > 1,则事件发生的概率随时间增加;如果 β < 1,则概率减少。因为威布尔分布用于失效时间分析而不是事件率,所以第二个参数用特征寿命表示,而不是事件每个间隔的速率。所使用的符号是 η ,希腊字母 eta。它也称为比例参数。

使用威布尔分布,估计任务现在包括估计两个参数,βη 。软件用于建模数据并给出最佳拟合威布尔分布的估计。

生成威布尔分布随机数的R代码需要三个参数:n(要生成的数字的数量)、shapescale。例如,以下代码将从形状为 1.5 和特征寿命为 5,000 的威布尔分布中生成 100 个随机数(寿命):

rweibull(100, 1.5, 5000)

要在Python中实现相同的功能,可以使用函数stats.weibull_min.rvs

stats.weibull_min.rvs(1.5, scale=5000, size=100)

进一步阅读

  • 现代工程统计由 Thomas Ryan(Wiley,2007)撰写,其中有一章专门讨论工程应用中使用的概率分布。

  • 在这里阅读关于威布尔分布的工程视角的内容herehere

总结

在大数据时代,当需要准确估计时,随机抽样原则仍然很重要。数据的随机选择可以减少偏差,并产生比仅使用方便获得的数据更高质量的数据集。了解各种抽样和数据生成分布的知识使我们能够量化由随机变异可能导致的估计误差。与此同时,自助法(从观察到的数据集中带有替换地抽样)是一种吸引人的“一刀切”方法,用于确定样本估计中可能的误差。

^(1) 钟形曲线具有标志性意义,但也许有些被高估了。乔治·W·科布,这位以其对教授统计学初级课程的贡献而闻名的马萨诸塞州圣女大学统计学家,在 2015 年 11 月的《美国统计学家》编辑中辩称,“以正态分布为核心的标准初级课程已经超越了其核心性的有用性。”

第三章:统计实验和显著性测试

实验设计是统计实践的基石,几乎在所有研究领域都有应用。目标是设计一个实验来确认或拒绝一个假设。数据科学家经常需要进行连续的实验,特别是关于用户界面和产品营销的方面。本章回顾了传统实验设计,并讨论了数据科学中一些常见挑战。还涵盖了统计推断中一些经常引用的概念,并解释了它们在数据科学中的意义和相关性(或缺乏相关性)。

每当您看到关于统计显著性、t 检验或 p 值的引用时,通常是在经典统计推断“管道”(见图 3-1)的背景下。这个过程从一个假设开始(“药物 A 比现有标准药物更好”,或者“价格 A 比现有价格 B 更有利可图”)。设计一个实验(可能是 A/B 测试)来检验这个假设——希望能够得出确切的结果。收集和分析数据,然后得出结论。术语 推断 反映了意图,即将实验结果应用于更大的过程或群体,这涉及到一组有限的数据。

图片/Inference-pipeline.png

图 3-1. 经典统计推断流程

A/B 测试

A/B 测试是一个有两个组的实验,用来确定两种处理、产品、程序或类似物中哪个更优越。通常其中一种处理是标准现有处理,或者没有处理。如果使用了标准(或没有)处理,则称为 对照组。一个典型的假设是新处理比对照更好。

A/B 测试在网页设计和营销中很常见,因为结果很容易衡量。一些 A/B 测试的例子包括:

  • 测试两种土壤处理方法,以确定哪种产生更好的种子发芽

  • 测试两种治疗方法,以确定哪种更有效抑制癌症

  • 测试两个价格,以确定哪个产生更多净利润

  • 测试两个网络标题,以确定哪一个能产生更多点击(图 3-2)

  • 测试两个网络广告,以确定哪个生成更多转化

图片/Web-test-A-B.png

图 3-2. 营销人员持续测试一种网页展示与另一种的差异

一个合适的 A/B 测试有 实验对象 可以分配到一个或另一个处理组中。实验对象可以是人、植物种子、网站访问者;关键是实验对象被暴露于处理中。理想情况下,实验对象是 随机分配 到处理中。通过这种方式,您可以知道处理组之间的任何差异是由以下两种因素之一引起的:

  • 不同治疗方法的效果

  • 分配给哪些治疗(即,随机分配可能导致自然表现更好的受试者集中在 A 或 B)

你还需要注意用于比较 A 组和 B 组的 测试统计量 或度量。在数据科学中,也许最常见的度量是二元变量:点击或不点击,购买或不购买,欺诈或非欺诈等。这些结果将总结在一个 2×2 的表格中。表格 3-1 是一个实际价格测试的 2×2 表格(有关这些结果的进一步讨论,请参阅“统计显著性和 p 值”)。

表格 3-1. 电子商务实验结果的 2×2 表格

结果 价格 A 价格 B
转化率 200 182
无转化 23,539 22,406

如果度量标准是连续变量(购买金额、利润等)或计数(例如住院天数、访问页面数),结果可能显示不同。如果我们关心的不是转化率而是每页浏览的收入,在 表格 3-1 中价格测试的结果在典型的默认软件输出中可能如下所示:

使用价格 A 的收入/页面浏览:*均 = 3.87,标准偏差 = 51.10

使用价格 B 的收入/页面浏览:*均 = 4.11,标准偏差 = 62.98

“SD” 指的是每组内数值的标准偏差。

警告

只因为统计软件(包括 RPython)默认生成输出,并不意味着所有输出都是有用或相关的。你可以看到前述的标准偏差并不那么有用;它们表明,许多数值可能是负数,但负收入是不可行的。这些数据由一小部分相对较高的值(具有转化的页面浏览)和大量的零值(没有转化的页面浏览)组成。用一个单一数字总结这样的数据的变异性是困难的,虽然与*均绝对偏差(A 组为 7.68,B 组为 8.15)相比,标准偏差更为合理。

为什么需要一个对照组?

为什么不跳过对照组,只对一个组应用感兴趣的治疗,并将结果与先前的经验进行比较?

没有对照组,无法保证“其他所有条件相同”,任何差异是否真的是由于治疗(或偶然性)。当你有一个对照组时,它接受与治疗组相同的条件(除了感兴趣的治疗)。如果你只是与“基线”或先前的经验进行比较,除了治疗之外可能还有其他因素可能会不同。

研究中的盲法

盲目研究是指受试者不知道他们是否接受了治疗 A 或治疗 B。意识到接受了特定治疗可能会影响反应。双盲研究是指调查人员和协助者(例如医学研究中的医生和护士)也不知道哪些受试者接受了哪种治疗。当治疗性质透明时不可能进行盲法,例如从计算机与心理学家进行的认知治疗。

在数据科学中,A/B 测试通常用于网络环境。治疗可以是网页设计、产品价格、标题文字或其他项目。保持随机化原则需要一些思考。通常实验中的受试者是网站访客,我们感兴趣的结果包括点击、购买、访问时长、访问页面数量、是否访问特定页面等。在标准的 A/B 实验中,需要提前决定一个度量标准。可以收集多种行为度量标准并感兴趣,但如果实验预期会在治疗 A 和治疗 B 之间做出决策,需要预先确定一个单一的度量标准或检验统计量。在实验之后选择检验统计量会导致研究人员偏见。

为什么只是 A/B?为什么不是 C、D……?

A/B 测试在营销和电子商务领域很受欢迎,但远非唯一的统计实验类型。可以包括其他治疗方式。受试者可能会重复测量。在药物试验中,由于受试者稀缺、昂贵且随时间获得,有时会设计多次机会停止实验并得出结论。

传统的统计实验设计专注于回答关于特定治疗效果的静态问题。数据科学家对以下问题不太感兴趣:

价格 A 和价格 B 之间的差异在统计上是否显著?

而对以下问题感兴趣:

在多个可能的价格中,哪一个是最好的?

为此,使用了一种相对较新的实验设计类型:多臂老丨虎丨机(参见“多臂老丨虎丨机算法”)。

获取许可

在涉及人类主体的科学和医学研究中,通常需要得到他们的许可,并获得机构审查委员会的批准。作为运营的一部分进行的业务实验几乎从不这样做。在大多数情况下(例如,定价实验或关于显示哪个标题或提供哪个优惠的实验),这种做法被广泛接受。然而,Facebook 在 2014 年却违反了这种普遍接受的实验规范,当时它对用户的新闻提要进行了情感色彩的实验。Facebook 使用情感分析将新闻提要帖子分类为积极或消极,并改变显示给用户的积极/消极*衡。随机选择的用户中有些体验到更多积极的帖子,而其他人体验到更多消极的帖子。Facebook 发现,体验到更积极的新闻提要的用户更有可能自己积极发帖,反之亦然。然而,效果的幅度很小,Facebook 因未经用户同意进行实验而受到了很多批评。有些用户推测,如果他们得到了消极版本的提要,Facebook 可能会把一些极度沮丧的用户推向绝望的边缘。

进一步阅读

  • 两组比较(A/B 测试)是传统统计学的基础,并且任何介绍性统计学文本都会广泛涵盖设计原则和推断过程。要在更多的数据科学背景中讨论 A/B 测试并使用重采样,请参阅彼得·布鲁斯(Wiley,2014 年)的Introductory Statistics and Analytics: A Resampling Perspective

  • 对于网页测试来说,测试的后勤方面与统计方面一样具有挑战性。一个好的起点是查看Google Analytics 关于实验的帮助部分

  • 要注意在网上看到的关于 A/B 测试的无处不在的指南中的建议,比如在其中一个指南中提到的这些话:“等待大约 1,000 名访客并确保您运行测试一周。”这些一般的经验法则在统计上并没有意义;更多细节请参见“功效和样本大小”。

假设检验

假设检验,也称为显著性检验,在传统的已发表研究的统计分析中无处不在。它们的目的是帮助你了解观察到的效应是否可能是由随机机会造成的。

A/B 测试(见“A/B 测试”)通常在构建假设时构建。例如,假设可能是 B 价格能带来更高的利润。为什么我们需要一个假设?为什么不只看实验的结果然后选择表现更好的治疗方法?

答案在于人类大脑倾向于低估自然随机行为的范围。其中一种表现是未能预测极端事件,或所谓的“黑天鹅”(见“长尾分布”)。另一种表现是倾向于错误地将随机事件解释为具有某种意义的模式。统计假设检验的发明是为了防止研究人员被随机事件愚弄。

在一个设计良好的 A/B 测试中,您以这样一种方式收集 A 和 B 的治疗数据,使得 A 和 B 之间的任何观察到的差异必须要么是:

  • 主观分配中的随机机会

  • A 和 B 之间的真实差异

统计假设检验是对 A/B 测试或任何随机实验的进一步分析,以评估随机机会是否可以合理解释观察到的 A 组和 B 组之间的差异。

零假设

假设检验使用以下逻辑:“鉴于人类倾向于对异常但随机的行为做出反应并将其解释为某种有意义和真实的东西,在我们的实验中,我们将需要证明组别间的差异比随机机会可能产生的更为极端。” 这涉及一个基准假设,即治疗方法是等效的,任何组别之间的差异是由于偶然产生的。这个基准假设称为零假设。因此,我们的希望是,我们实际上可以证明零假设是错误的,并展示 A 组和 B 组的结果比随机机会产生的更为不同。

其中一种方法是通过重新抽样置换过程,将 A 组和 B 组的结果混合在一起,然后重复地将数据以类似大小的组合发放,并观察我们获得的极端差异频率有多少次与观察到的差异一样极端。从 A 组和 B 组的组合洗牌结果以及从中重新抽样的过程,体现了 A 组和 B 组等效和可互换的零模型,称为零假设。详见“重新抽样”。

替代假设

假设检验的性质涉及不仅是零假设而且还有一个对立假设的抵消。以下是一些例子:

  • 零假设 = “A 组和 B 组的*均数之间没有差异”; 替代 = “A 与 B 不同”(可能更大或更小)

  • 零假设 = “A B”; 替代 = “A > B”

  • 零假设 = “B 不是比 A 大 X%”; 替代 = “B 比 A 大 X%”

总结起来,零假设和替代假设必须解释所有可能性。零假设的性质决定了假设检验的结构。

单边与双边假设检验

在 A/B 测试中经常会测试一个新选项(比如 B)与已建立的默认选项(A)相比,假设是你将坚持使用默认选项,除非新选项明显更好。在这种情况下,你希望通过假设检验避免被偶然误导,而且是在有利于 B 的方向上。你不关心在其他方向上被偶然误导,因为除非 B 明显更好,否则你将坚持使用 A。因此,你需要一个 单向(或单尾)假设检验。这意味着极端偶然结果只在一个方向上计入 p 值。

如果你希望通过假设检验避免被任何方向的偶然误导,替代假设是 双向 的(A 与 B 不同;可能更大或更小)。在这种情况下,你使用 双向(或双尾)假设。这意味着极端偶然结果在任何方向上都计入 p 值。

单尾假设检验通常适合 A/B 决策,其中需要做出决策,并且一种选项通常被指定为“默认”状态,除非另一种证明更好。然而,包括 RPython 中的 scipy 在内的软件通常以其默认输出提供双尾检验,许多统计学家选择更保守的双尾检验,只是为了避免争论。单尾与双尾是一个令人困惑的主题,在数据科学中并不那么重要,因为 p 值计算的精度并不是非常重要。

进一步阅读

  • 莱昂纳德·姆洛迪诺(Leonard Mlodinow)著 The Drunkard’s Walk(Pantheon,2008)是一本可读的调查,介绍了“随机性如何主宰我们的生活”。

  • 大卫·弗里德曼(David Freedman)、罗伯特·皮萨尼(Robert Pisani)和罗杰·珀维斯(Roger Purves)的经典统计学文本 Statistics,第四版(W. W. Norton,2007),对大多数统计学主题进行了出色的非数学处理,包括假设检验。

  • 彼得·布鲁斯(Peter Bruce)著 Introductory Statistics and Analytics: A Resampling Perspective(Wiley,2014)利用重采样开发了假设检验概念。

Resampling

在统计学中,重采样 意味着从观察到的数据中重复抽样值,其一般目标是评估统计量的随机变异性。它还可用于评估和改进某些机器学习模型的准确性(例如,基于多个自举数据集构建的决策树模型的预测可以*均化,这个过程被称为 bagging — 见 “Bagging and the Random Forest”)。

有两种主要的重采样程序:bootstrappermutation 测试。Bootstrap 用于评估估计量的可靠性;它在前一章已经讨论过(见 “The Bootstrap”)。Permutation tests 用于测试假设,通常涉及两个或更多组,我们在本节中讨论这些内容。

Permutation Test

置换过程中涉及两个或更多样本,通常是 A/B 或其他假设检验中的组。置换意味着改变一组值的顺序。假设检验中的置换检验的第一步是将 A 组和 B 组(如果有的话,还有 C、D 等)的结果结合起来。这体现了空假设的逻辑实现,即暴露于组的处理方式没有差异。然后我们通过从这个结合集中随机抽取组来测试这个假设,看看它们之间有多大差异。置换过程如下所示:

  1. 将不同组的结果合并成一个数据集。

  2. 洗牌组合数据,然后随机抽取(不替换)与 A 组大小相同的重新采样(显然它会包含来自其他组的一些数据)。

  3. 从剩余数据中随机抽取(不替换)与 B 组大小相同的重新采样。

  4. 对 C、D 等组做同样的操作。现在你已经收集到了一组与原始样本大小相匹配的重新采样。

  5. 对原始样本计算的任何统计量或估计值(例如,组间比例的差异),现在对重新采样计算,并记录;这构成一个置换迭代。

  6. 重复前面的步骤R次,以获得检验统计量的置换分布。

现在回到组间的观察差异,并将其与置换差异集合进行比较。如果观察到的差异远远落在置换差异集合之内,那么我们没有证明任何事情——观察到的差异在偶然产生的范围内。然而,如果观察到的差异大部分位于置换分布之外,则我们得出结论偶然不是负责的。在技术术语中,差异是统计显著的。(见“统计显著性和 p-值”。)

例如:网站粘性

一家销售相对高价值服务的公司想要测试两种网页展示中哪种更能促成销售。由于所售服务价值高,销售频率低且销售周期长;累积足够的销售量来判断哪种展示更胜一筹需要太长时间。因此,该公司决定使用代理变量来衡量结果,使用详细的介绍服务的内页作为代理变量。

提示

代理变量是代表真正感兴趣的变量的变量,该变量可能无法获取、成本过高或者测量时间过长。例如,在气候研究中,古冰芯的氧含量被用作温度的代理。至少对真正感兴趣的变量有一些数据是有用的,这样就可以评估它与代理的关联强度。

我们公司的一个潜在代理变量是详细落地页上的点击次数。更好的一个代理变量是人们在页面上花费的时间。合理地认为,一个能吸引人们注意力更长时间的网页会导致更多的销售。因此,我们的度量标准是*均会话时间,比较页面 A 和页面 B。

由于这是一个内部、专用页面,访问者数量不多。还要注意的是,我们使用的 Google Analytics 无法测量一个人的最后一次会话时间。而不是将该会话从数据中删除,Google Analytics 将其记录为零,因此需要额外的处理来删除这些会话。结果是两种不同演示的总共 36 个会话,其中页面 A 有 21 个,页面 B 有 15 个。使用 ggplot,我们可以通过并排箱线图直观比较会话时间:

ggplot(session_times, aes(x=Page, y=Time)) +
  geom_boxplot()

pandasboxplot 命令使用关键字参数 by 来创建图像:

ax = session_times.boxplot(by='Page', column='Time')
ax.set_xlabel('')
ax.set_ylabel('Time (in seconds)')
plt.suptitle('')

箱线图,显示在 图 3-3 中,表明页面 B 的会话时间比页面 A 长。每组的均值可以在 R 中如下计算:

mean_a <- mean(session_times[session_times['Page'] == 'Page A', 'Time'])
mean_b <- mean(session_times[session_times['Page'] == 'Page B', 'Time'])
mean_b - mean_a
[1] 35.66667

Python 中,我们首先通过页面筛选 pandas 数据帧,然后确定 Time 列的*均值:

mean_a = session_times[session_times.Page == 'Page A'].Time.mean()
mean_b = session_times[session_times.Page == 'Page B'].Time.mean()
mean_b - mean_a

页面 B 的会话时间比页面 A 的*均多出 35.67 秒。问题是这种差异是否在随机机会可能产生的范围内,即是否具有统计学意义。一个回答这个问题的方法是应用置换检验——将所有会话时间合并然后重复随机洗牌并分成 21 组(回想一下 n A = 21 代表页面 A)和 15 组( n B = 15 代表页面 B)。

要应用置换检验,我们需要一个函数将这 36 个会话时间随机分配给 21 个(页面 A)和 15 个(页面 B)的组。这个函数的 R 版本如下:

perm_fun <- function(x, nA, nB)
{
  n <- nA + nB
  idx_b <- sample(1:n, nB)
  idx_a <- setdiff(1:n, idx_b)
  mean_diff <- mean(x[idx_b]) - mean(x[idx_a])
  return(mean_diff)
}

进行这种置换检验的 Python 版本如下:

def perm_fun(x, nA, nB):
    n = nA + nB
    idx_B = set(random.sample(range(n), nB))
    idx_A = set(range(n)) - idx_B
    return x.loc[idx_B].mean() - x.loc[idx_A].mean()

不同网页演示的会话时间。

图 3-3. 网页 A 和 B 的会话时间

此函数通过抽样(不替换)n B 索引,并将它们分配给 B 组;剩余的 n A 索引被分配给 A 组。返回两个均值之间的差异。将此函数调用 R = 1,000 次,并指定 n A = 21n B = 15,得到会话时间差异的分布,可以绘制成直方图。在R中,使用hist函数可以这样做:

perm_diffs <- rep(0, 1000)
for (i in 1:1000) {
  perm_diffs[i] = perm_fun(session_times[, 'Time'], 21, 15)
}
hist(perm_diffs, xlab='Session time differences (in seconds)')
abline(v=mean_b - mean_a)

Python中,我们可以使用matplotlib创建一个类似的图形:

perm_diffs = [perm_fun(session_times.Time, nA, nB) for _ in range(1000)]

fig, ax = plt.subplots(figsize=(5, 5))
ax.hist(perm_diffs, bins=11, rwidth=0.9)
ax.axvline(x = mean_b - mean_a, color='black', lw=2)
ax.text(50, 190, 'Observed\ndifference', bbox={'facecolor':'white'})
ax.set_xlabel('Session time differences (in seconds)')
ax.set_ylabel('Frequency')

直方图在图 3-4 中显示,随机排列的*均差异经常超过会话时间的观察差异(垂直线)。对于我们的结果,这种情况发生在 12.6% 的情况下:

mean(perm_diffs > (mean_b - mean_a))
---
0.126

由于模拟使用随机数,所以百分比会有所变化。例如,在Python版本中,我们得到了 12.1%:

np.mean(perm_diffs > mean_b - mean_a)
---
0.121

这表明,页面 A 和页面 B 之间会话时间的观察差异很大程度上在偶然变异范围内,因此在统计学上并不显著。

排列过程中会话时间差异的直方图。

图 3-4. 页面 A 和页面 B 之间会话时间差异的频率分布;垂直线显示了观察差异

详尽排列和自助法排列测试

除了前面提到的随机洗牌过程,也称为随机排列测试随机化测试,排列测试还有两个变体:

  • 一个详尽排列测试

  • 一个自助法排列测试

在详尽的排列测试中,我们不仅仅是随机洗牌和分割数据,而是找出所有可能的分割方式。这只适用于相对较小的样本量。通过大量重复的洗牌次数,随机排列测试的结果逼*详尽排列测试的结果,并在极限情况下逼*它们。详尽排列测试有时也被称为精确测试,因为它们具有保证空模型不会测试为“显著”超过测试的 alpha 水*的统计特性(参见“统计显著性和 p-值”)。

在自助重排置换检验中,步骤 2 和 3 中所述的抽样是有放回地进行的,而不是无放回地进行的。这样一来,重新抽样过程不仅模拟了将处理分配给受试者中的随机元素,还模拟了从总体中选择受试者的随机元素。统计学中遇到了这两种过程,它们之间的区别有些复杂,但在数据科学的实践中并不重要。

置换检验:数据科学的底线

置换检验是探索随机变化作用的有用启发式程序。它们相对容易编码、解释和解释,并且它们为绕过基于公式的统计学形式主义和“虚假决定论”提供了一个有用的绕道,公式“答案”的精确性往往暗示了不必要的确定性。

与公式方法相比,重抽样的一个优点是更接*推断的一刀切方法。数据可以是数值型或二元的。样本大小可以相同也可以不同。不需要假设数据正态分布。

进一步阅读

  • 《随机化检验》,第 4 版,尤金·埃奇顿和帕特里克·昂海纳(Chapman & Hall/CRC Press,2007 年)—但不要太过于深入非随机抽样的林莽之中

  • 《入门统计与分析:重抽样视角》 彼得·布鲁斯(Wiley,2014 年)

统计显著性和 p 值

统计显著性是统计学家衡量实验(甚至是对现有数据进行研究)是否产生了比偶然性更极端结果的方法。如果结果超出了偶然变异的范围,就说具有统计显著性。

考虑在表 3-2 中早期显示的网络测试结果。

表 3-2. 电子商务实验结果的 2×2 表

结果 价格 A 价格 B
转化 200 182
不进行转换 23,539 22,406

价格 A 的转化率几乎比价格 B 高出 5%(0.8425% = 200/(23539+200)100,相对于 0.8057% = 182/(22406+182)100——相差 0.0368 个百分点),这在高流量业务中是有意义的。我们这里有超过 45,000 个数据点,很容易将其视为“大数据”,不需要进行统计显著性测试(主要是为了考虑样本量小的采样变异性)。然而,转化率如此之低(不到 1%),以至于实际的有意义值——转化率——仅在 100 多个,而所需的样本量实际上是由这些转化决定的。我们可以通过重新采样程序测试价格 A 和价格 B 之间的转化率差异是否在 偶然变化 范围内。通过偶然变化,我们指的是由一个概率模型产生的随机变化,该模型体现了零假设,即两个速率之间没有差异(参见“零假设”)。以下排列程序询问:“如果两个价格共享相同的转化率,偶然变化能否产生与 5% 一样大的差异?”

  1. 将标有 1 和 0 的卡片放入盒子中:这表示了 382 个 1 和 45,945 个 0 的假设共享转化率 = 0.008246 = 0.8246%。

  2. 洗牌并从中抽出大小为 23,739 的重新采样(与价格 A 相同的 n),并记录有多少个 1。

  3. 记录剩余 22,588(与价格 B 相同的 n)中的 1 的数量。

  4. 记录 1 的比例差异。

  5. 重复步骤 2–4。

  6. 差异大于等于 0.0368 的频率有多少?

在“示例:网站粘性”中定义的 perm_fun 函数可重用,我们可以创建一个在 R 中随机排列的转化率差异直方图:

obs_pct_diff <- 100 * (200 / 23739 - 182 / 22588)
conversion <- c(rep(0, 45945), rep(1, 382))
perm_diffs <- rep(0, 1000)
for (i in 1:1000) {
  perm_diffs[i] = 100 * perm_fun(conversion, 23739, 22588)
}
hist(perm_diffs, xlab='Conversion rate (percent)', main='')
abline(v=obs_pct_diff)

相应的 Python 代码如下:

obs_pct_diff = 100 * (200 / 23739 - 182 / 22588)
print(f'Observed difference: {obs_pct_diff:.4f}%')
conversion = [0] * 45945
conversion.extend([1] * 382)
conversion = pd.Series(conversion)

perm_diffs = [100 * perm_fun(conversion, 23739, 22588)
              for _ in range(1000)]

fig, ax = plt.subplots(figsize=(5, 5))
ax.hist(perm_diffs, bins=11, rwidth=0.9)
ax.axvline(x=obs_pct_diff, color='black', lw=2)
ax.text(0.06, 200, 'Observed\ndifference', bbox={'facecolor':'white'})
ax.set_xlabel('Conversion rate (percent)')
ax.set_ylabel('Frequency')

参见 1,000 次重新采样结果的直方图,链接为图 3-5:正如在这个例子中所发生的那样,在这种情况下,观察到的差异 0.0368% 完全在偶然变化范围内。

通过排列程序得到的转化率差异直方图。

图 3-5. 价格 A 和价格 B 之间的转化率差异的频率分布

p 值

简单地观察图形并不是衡量统计显著性的非常精确的方法,所以更感兴趣的是 p 值。这是偶然模型产生比观察到的结果更极端结果的频率。我们可以从我们的排列测试中估算出一个 p 值,方法是取排列测试产生的差异等于或大于观察到的差异的比例:

mean(perm_diffs > obs_pct_diff)
[1] 0.308
np.mean([diff > obs_pct_diff for diff in perm_diffs])

在这里,RPython 都使用 true 被解释为 1,false 被解释为 0 的事实。

p 值为 0.308,这意味着我们预计在 30% 的时间内会通过随机机会获得像这样的结果,或者更极端的结果。

在这种情况下,我们不需要使用排列测试来获取 p 值。由于我们有二项分布,可以*似计算 p 值。在 R 代码中,我们使用 prop.test 函数来实现这一点:

> prop.test(x=c(200, 182), n=c(23739, 22588), alternative='greater')

	2-sample test for equality of proportions with continuity correction

data:  c(200, 182) out of c(23739, 22588)
X-squared = 0.14893, df = 1, p-value = 0.3498
alternative hypothesis: greater
95 percent confidence interval:
 -0.001057439  1.000000000
sample estimates:
     prop 1      prop 2
0.008424955 0.008057376

参数 x 表示每个组的成功次数,参数 n 表示试验次数。

方法 scipy.stats.chi2_contingency 使用的数值如 表 3-2 所示:

survivors = np.array([[200, 23739 - 200], [182, 22588 - 182]])
chi2, p_value, df, _ = stats.chi2_contingency(survivors)

print(f'p-value for single sided test: {p_value / 2:.4f}')

正态*似得出一个 p 值为 0.3498,与排列测试得出的 p 值接*。

显著水*

统计学家不赞成将结果是否“太不寻常”交给研究者自行决定。相反,需要事先指定一个阈值,比如“比(零假设)结果的 5%更极端”; 这个阈值称为显著水*。常见的显著水*为 5%和 1%。任何选择的水*都是一个任意的决定——这个过程并不能保证 x% 的正确决策。这是因为所回答的概率问题不是“这是偶然事件的概率吗?”而是“在给定的偶然模型下,出现如此极端结果的概率是多少?”我们然后逆推关于偶然模型适当性的判断,但这个判断本身并不包含概率。这一点已经引起了很多混淆。

p 值争议

*年来,关于 p 值的使用引起了相当大的争议。某些心理学期刊甚至“禁止”在提交的论文中使用 p 值,理由是仅基于 p 值的出版决策导致了糟糕研究的发表。许多研究人员对 p 值的真实含义知之甚少,他们在数据中和各种可能的假设之间摸索,直到找到一个产生显著 p 值(从而适合发表的论文)的组合。

真正的问题在于,人们希望从 p 值中得到比其实际含义更多的信息。以下是我们希望 p 值能传达的信息:

结果是由偶然导致的概率。

我们希望 p 值较低,这样我们就可以得出我们已经证明了某些东西。这就是许多期刊编辑解读 p 值的方式。但 p 值 实际上 代表着以下内容:

给定偶然模型,观察结果如此极端的概率。

差异微妙但确实存在。显著的 p 值并不能像看起来承诺的那样“证明”你的结果有多可靠。当真正理解 p 值的含义时,其逻辑基础来支持“统计显著”的结论稍显薄弱。

2016 年 3 月,美国统计协会在经过长时间的内部讨论后,发布了一份关于 p 值使用的警示性声明。ASA 声明 强调了研究人员和期刊编辑的六项原则:

  1. p 值可以指示数据与指定统计模型的不兼容程度。
  2. p 值不测量所研究假设为真的概率,也不测量数据仅由随机机会产生的概率。
  3. 科学结论和商业或政策决策不应仅仅基于 p 值是否通过了特定的阈值。
  4. 适当的推断需要完整报告和透明度。
  5. p 值或统计显著性不测量效应的大小或结果的重要性。
  6. 单单 p 值本身并不能提供关于模型或假设证据的良好度量。

实际意义

即使一个结果在统计上是显著的,也不意味着它具有实际意义。如果一个没有实际意义的小差异是从足够大的样本中产生的,那么它也可能是统计上显著的。大样本确保即使是微小的、无意义的影响也足够大,可以排除偶然性作为解释。排除偶然性并不能神奇地使一个本质上不重要的结果变得重要。

类型 1 和类型 2 错误

在评估统计显著性时,可能存在两种类型的错误:

  • 类型 1 错误是指错误地得出了一个效应是真实的结论,而实际上只是由于偶然性

  • 类型 2 错误是指错误地得出一个效应不是真实的结论(即,由于偶然性),而实际上它是真实的

实际上,类型 2 错误并不是一个错误,而是判断样本量太小,无法检测到效应。当 p 值未达到统计显著性(例如,超过了 5%)时,我们真正表达的是“效应未经证实”。可能是因为更大的样本会产生更小的 p 值。

显著性检验(也称为 假设检验)的基本功能是防止被随机机会愚弄;因此,它们通常被设计成最小化类型 1 错误。

数据科学与 p 值

数据科学家的工作通常不会被发表在科学期刊上,因此对 p 值价值的争论有点学术化。对于数据科学家来说,p 值是一个有用的度量标准,在您想知道一个看起来有趣且有用的模型结果是否在正常机会变异范围内时。作为实验中的决策工具,p 值不应被认为是控制性的,而仅仅是决策上的另一个信息点。例如,p 值有时被用作某些统计或机器学习模型中的中间输入——根据其 p 值,一个特征可能被包含在模型中或从模型中排除。

进一步阅读

  • Stephen Stigler, “Fisher and the 5% Level,” Chance 21, no. 4 (2008): 12. 这篇文章是对罗纳德·费希尔(Ronald Fisher)1925 年的著作 Statistical Methods for Research Workers(奥利弗与博伊德)的简短评论,以及费希尔对 5%水*显著性的强调。

  • 另请参阅“假设检验”及其进一步阅读。

t-Tests

根据数据是否包含计数数据或测量数据、样本数量以及被测量的内容,显著性检验有许多类型。一个非常常见的是t-test,以学生 t 分布命名,最初由 W.S.高斯特开发,用于*似单个样本均值的分布(见“学生 t 分布”)。

所有显著性检验都要求您指定一个检验统计量来衡量您感兴趣的效果,并帮助您确定观察到的效果是否在正常偶然变异的范围内。在重新取样检验中(参见“排列检验”中的排列讨论),数据的尺度并不重要。您从数据本身创建参考(零假设)分布,并直接使用检验统计量。

在 20 世纪 20 年代和 30 年代开发统计假设检验时,不可能随机洗牌数据数千次以进行重新取样检验。统计学家发现,基于高斯特的 t 分布的 t-test 是排列(洗牌)分布的良好*似。它用于非常常见的两样本比较——A/B 测试,其中数据是数值的。但为了无视尺度地使用 t 分布,必须使用标准化的检验统计量。

经典的统计学文本此时将展示各种包含高斯特分布的公式,并演示如何标准化您的数据以将其与标准 t 分布进行比较。这里没有展示这些公式,因为所有统计软件以及RPython都包含了体现这些公式的命令。在R中,函数是t.test

> t.test(Time ~ Page, data=session_times, alternative='less')

	Welch Two Sample t-test

data:  Time by Page
t = -1.0983, df = 27.693, p-value = 0.1408
alternative hypothesis: true difference in means is less than 0
95 percent confidence interval:
     -Inf 19.59674
sample estimates:
mean in group Page A mean in group Page B
            126.3333             162.0000

函数scipy.stats.ttest_ind可以在Python中使用:

res = stats.ttest_ind(session_times[session_times.Page == 'Page A'].Time,
                      session_times[session_times.Page == 'Page B'].Time,
                      equal_var=False)
print(f'p-value for single sided test: {res.pvalue / 2:.4f}')

备择假设是页面 A 的会话时间均值小于页面 B 的均值。p 值为 0.1408,接*于排列测试的 p 值 0.121 和 0.126(见“示例:Web 粘性”)。

在重新取样模式下,我们将解决方案构建为反映观察到的数据和要测试的假设,而不必担心数据是数值还是二进制、样本大小是否*衡、样本方差或其他各种因素。在公式世界中,存在许多变体,它们可能令人困惑。统计学家需要在这个世界中导航并学习它的地图,但数据科学家通常不需要——他们通常不会过多关注假设检验和置信区间的细节,就像研究人员为展示准备论文时那样。

进一步阅读

  • 任何初级统计学教材都会有 t 统计量及其用途的示例;两本好书是《Statistics》,第 4 版,作者 David Freedman、Robert Pisani 和 Roger Purves(W. W. Norton,2007),以及《The Basic Practice of Statistics》,第 8 版,作者 David S. Moore、William I. Notz 和 Michael A. Fligner(W. H. Freeman,2017)。

  • 关于 t 检验和重采样程序的同时处理,请参阅 Peter Bruce(Wiley,2014)的Introductory Statistics and Analytics: A Resampling Perspective 或 Robin Lock 及其四位 Lock 家族成员(Wiley,2016)的Statistics: Unlocking the Power of Data,第 2 版。

多重检验

正如我们之前提到的,在统计学中有一句话:“折磨数据足够长时间,它就会招供。” 这意味着如果您从足够多的不同角度查看数据并提出足够多的问题,您几乎无可避免地会发现一个统计显著的效果。

例如,如果您有 20 个预测变量和一个结果变量,所有这些变量都是随机生成的,那么如果您在 alpha = 0.05 水*上进行一系列 20 次显著性检验,至少会有一个预测变量(错误地)显示出统计显著性的概率相当高。正如前面讨论的那样,这被称为Type 1 error。您可以通过首先找到所有变量在 0.05 水*上都能正确地测试为非显著的概率来计算这一概率。一个变量能正确地测试为非显著的概率是 0.95,因此所有 20 个变量都能正确地测试为非显著的概率是 0.95 × 0.95 × 0.95…,即 0.95²⁰ = 0.36.^(1) 至少有一个预测变量(错误地)显示出显著性的概率是这个概率的反面,即 1 –(所有变量都为非显著的概率)= 0.64。这被称为alpha inflation

这个问题与数据挖掘中过拟合问题相关,或者称为“将模型拟合到噪声中”。您添加的变量越多,或者运行的模型越多,某些东西被“显著”地发现的可能性就越大,仅仅是由于偶然性。

在监督学习任务中,使用一个保留集,在这个集合中模型评估数据是模型之前未见过的,可以减轻这种风险。在不涉及标记保留集的统计和机器学习任务中,基于统计噪声得出结论的风险仍然存在。

在统计学中,有一些旨在处理这个问题的程序,适用于非常具体的情况。例如,如果您正在比较多个治疗组的结果,您可能会提出多个问题。因此,对于治疗方案 A–C,您可能会问:

  • A 和 B 有何不同?

  • B 和 C 有何不同?

  • A 和 C 有何不同?

或者,在临床试验中,你可能想要在多个阶段检查治疗结果。在每种情况下,你都在提出多个问题,并且对于每个问题,你都在增加被偶然误导的机会。统计学中的调整程序可以通过将统计显著性的门槛设得比单个假设检验更严格来弥补这一点。这些调整程序通常涉及根据测试数量“分割阿尔法”。这样可以使每次测试的阿尔法(即,统计显著性的门槛)更小。其中一种调整程序,邦费罗尼校正,简单地将阿尔法除以比较数量。另一种,在比较多个组均值时使用的是图基的“诚实显著差异”,或图基的 HSD。该测试应用于组均值中的最大差异,将其与基于t-分布(大致等效于将所有值混合在一起,以原始组大小重新抽样组,并找到重新抽样组均值中的最大差异)的基准进行比较。

然而,多重比较问题不仅限于这些高度结构化的情况,并且与反复数据挖掘的现象相关,这导致了有关拷问数据的说法。换句话说,如果数据足够复杂,如果你还没有找到有趣的东西,那么你只是没有花足够长时间和精力去寻找。现在的数据比以往任何时候都更加丰富,2002 年至 2010 年间发表的期刊文章数量几乎翻了一番。这给了我们很多在数据中发现有趣事物的机会,包括多重性问题,比如:

  • 检查组间的多个成对差异

  • 查看多个子组结果(“我们在整体上没有发现显著的治疗效果,但我们发现了对于未婚女性 30 岁以下的效果”)

  • 尝试许多统计模型

  • 在模型中包含大量变量

  • 向多个不同问题提问(即,不同可能的结果)

False Discovery Rate

术语假阳性发现率最初用于描述给定一组假设检验会错误地识别显著效应的比率。随着基因组研究的出现,这个术语变得特别有用,因为可能在基因测序项目中进行大量的统计检验。在这些情况下,该术语适用于测试协议,并且单个错误的“发现”指的是假设检验的结果(例如,两个样本之间)。研究人员试图设置测试过程的参数,以控制指定水*上的假阳性发现率。在数据挖掘中,这个术语还被用于分类;它是类别 1 预测中的误分类率。或者换句话说,它是“发现”(将记录标记为“1”)是假的概率。在这里,我们通常处理 0 丰富且 1 有趣且稀少的情况(参见第五章和“罕见类问题”)。

由于“多重性”等一系列原因,更多的研究并不一定意味着更好的研究。例如,制药公司拜耳在 2011 年发现,当它试图复制 67 项科学研究时,只有 14 项可以完全复制。将*三分之二的研究根本无法复制。

无论如何,针对高度定义和结构化的统计测试的调整程序过于具体和不灵活,以至于不能普遍适用于数据科学家。对于数据科学家而言,多重性的关键点是:

  • 对于预测建模,通过交叉验证(参见“交叉验证”)和使用保留样本,可以减少因为随机偶然性而导致虚假模型的风险。

  • 对于其他没有标记保留集来检查模型的程序,您必须依赖于:

    • 请注意,您查询和操作数据越多,偶然因素可能会起到更大的作用。

    • 重新采样和模拟启发法提供了随机偶然基准,用以比较观察到的结果。

进一步阅读

  • 关于一种过程(Dunnett's test)用于多重比较校正的简短说明,请参阅 David Lane 的在线统计文本

  • Megan Goldman 提供了一个稍微更长的介绍,关于 Bonferroni 校正程序的处理方法,请参阅这里

  • 关于更灵活的统计程序调整 p 值的深入处理,请参阅《Resampling-Based Multiple Testing》一书,作者为 Peter Westfall 和 Stanley Young(Wiley,1993 年)。

  • 关于数据分区和在预测建模中使用保留样本的讨论,请参阅《Data Mining for Business Analytics》第二章,作者为 Galit Shmueli、Peter Bruce、Nitin Patel、Peter Gedeck、Inbal Yahav 和 Kenneth Lichtendahl(Wiley,2007–2020,有适用于RPython、Excel 和 JMP 的版本)。

自由度

在许多统计测试和概率分布的文档和设置中,您会看到“自由度”的引用。这个概念适用于从样本数据计算的统计量,并且指的是可以自由变化的值的数量。例如,如果您知道一个包含 10 个值的样本的均值,那么有 9 个自由度(一旦知道了 9 个样本值,第 10 个可以计算出来,不再是自由变化的)。自由度参数,适用于许多概率分布,影响分布的形状。

自由度的数量是许多统计测试的输入。例如,自由度是在方差和标准差计算中看到的n – 1 的分母。为什么这很重要?当您使用样本来估计总体方差时,如果在分母中使用n,您将得到一个稍微有偏低的估计。如果在分母中使用n – 1,估计将不受这种偏差的影响。

传统统计课程或文本的很大一部分内容用于各种假设检验(t 检验,F 检验等)。当样本统计数据标准化以用于传统统计公式时,自由度是标准化计算的一部分,以确保您的标准化数据与适当的参考分布(t 分布,F 分布等)匹配。

对数据科学重要吗?实际上并不重要,至少在显著性测试的背景下不重要。首先,形式化的统计测试在数据科学中很少使用。其次,数据规模通常足够大,以至于对于数据科学家来说,分母是n还是n – 1 几乎没有真正的区别。(随着n的增大,使用n导致的偏差消失。)

然而,有一个情境很重要:在回归(包括逻辑回归)中使用因子变量时。如果存在完全冗余的预测变量,一些回归算法可能会失败。这种情况最常出现在将分类变量转换为二元指标(虚拟变量)时。考虑变量“星期几”。虽然一周有七天,但在指定星期几时只有六个自由度。例如,一旦知道星期几不是星期一到星期六,就可以确定它一定是星期日。因此,如果包括星期一到星期六的指标,再包括星期日将导致回归失败,因为会出现多重共线性错误。

进一步阅读

几个关于自由度的网络教程

ANOVA

假设我们不是进行 A/B 测试,而是比较多个组,比如 A/B/C/D,每组都有数值数据。用于测试组间统计显著差异的统计程序称为方差分析,或简称ANOVA

表 3-3 显示了四个网页的粘性,定义为访客在页面上停留的秒数。这四个页面被随机切换,以便每个网络访客随机接收一个页面。每个页面有五名访客,表 3-3 中的每一列都是独立的数据集。第一个查看页面 1 的观众与页面 2 的第一个查看者没有联系。请注意,在这样的网络测试中,我们无法完全实施经典的随机抽样设计,即从某个庞大人口中随机选择每个访客。我们必须根据它们的到来接受访客。访客可能因每天的时间、每周的时间、年份的季节、其互联网条件、使用的设备等因素而有所不同。在审查实验结果时应考虑这些因素作为潜在的偏倚。

表 3-3. 四个网页的粘性(以秒为单位)

页面 1 页面 2 页面 3 页面 4
164 178 175 155
172 191 193 166
177 182 171 164
156 185 163 170
195 177 176 168
*均值 172 185 176 162
总*均值 173.75

现在我们面临一个难题(见图 3-6)。当我们只比较两组时,这是一个简单的问题;我们只需查看每组的*均值之间的差异。但是有了四个*均值,就有六种可能的组间比较:

  • Page 1 相比于 Page 2

  • Page 1 相比于 Page 3

  • Page 1 相比于 Page 4

  • Page 2 相比于 Page 3

  • Page 2 相比于 Page 4

  • Page 3 相比于 Page 4

我们进行的逐对比较越多,就越容易被随机机会愚弄(见“多重测试”)。与担心我们可能做出的所有不同页面之间的比较不同,我们可以进行一个单一的总体测试,来回答这个问题:“所有页面是否具有相同的基础粘性,并且它们之间的差异是否是由于随机方式分配了一组共同的会话时间?”

四个不同网页的会话时间。

图 3-6. 四组的箱线图显示它们之间存在显著差异

用于测试此项的过程是方差分析(ANOVA)。其基础可以在以下重抽样过程中看到(这里指定为页面粘性的 A/B/C/D 测试):

  1. 将所有数据汇总在一个单一的箱中。

  2. 洗牌并抽取每个五个值的四次重抽样。

  3. 记录四组中每组的*均值。

  4. 记录四组均值之间的方差。

  5. 重复步骤 2–4 多次(例如,1,000 次)。

重新抽样方差超过观察方差的比例是多少?这就是 p 值。

这种类型的排列检验比 “Permutation Test” 中使用的类型更为复杂。幸运的是,lmPerm 包中的 aovp 函数为此情况计算排列检验:

> library(lmPerm)
> summary(aovp(Time ~ Page, data=four_sessions))
[1] "Settings:  unique SS "
Component 1 :
            Df R Sum Sq R Mean Sq Iter Pr(Prob)
Page         3    831.4    277.13 3104  0.09278 .
Residuals   16   1618.4    101.15
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

p 值,由 Pr(Prob) 给出,为 0.09278。换句话说,假设存在相同的粘性底层,四页中响应率的差异有 9.3% 的可能性实际上是观察到的,这纯粹是偶然发生的。这种不太可能性不足以达到传统的统计显著性阈值 5%,因此我们得出结论,四页之间的差异可能是偶然造成的。

Iter 列出了排列检验中进行的迭代次数。其他列对应传统的 ANOVA 表,并将在下文中描述。

Python 中,我们可以使用以下代码计算排列检验:

observed_variance = four_sessions.groupby('Page').mean().var()[0]
print('Observed means:', four_sessions.groupby('Page').mean().values.ravel())
print('Variance:', observed_variance)

def perm_test(df):
    df = df.copy()
    df['Time'] = np.random.permutation(df['Time'].values)
    return df.groupby('Page').mean().var()[0]

perm_variance = [perm_test(four_sessions) for _ in range(3000)]
print('Pr(Prob)', np.mean([var > observed_variance for var in perm_variance]))

F 统计量

就像 t 检验可以用于比较两组均值而不是排列检验一样,基于 F 统计量 的 ANOVA 的统计检验也存在。F 统计量基于组均值间方差(即处理效应)与残差误差方差的比率。此比率越高,结果在统计上显著性越大。如果数据符合正态分布,则统计理论规定该统计量应该有一定分布。基于此,可以计算出 p 值。

R 中,我们可以使用 aov 函数计算 ANOVA 表

> summary(aov(Time ~ Page, data=four_sessions))
            Df Sum Sq Mean Sq F value Pr(>F)
Page         3  831.4   277.1    2.74 0.0776 .
Residuals   16 1618.4   101.2
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

statsmodels 包提供了 Python 中的 ANOVA 实现:

model = smf.ols('Time ~ Page', data=four_sessions).fit()

aov_table = sm.stats.anova_lm(model)
aov_table

Python 代码的输出与 R 的几乎完全相同。

Df 是“自由度”,Sum Sq 是“*方和”,Mean Sq 是“均方差”(即均方差偏差的缩写),而 F value 是 F 统计量。对于总*均数,*方和是总*均数与 0 的偏离值的*方乘以 20(观察数)。总*均数的自由度根据定义为 1。

对于处理均值,自由度为 3(设置了三个值后,总*均值被设置,其他处理均值不能变动)。处理均值的*方和是处理均值与总*均数之间偏离的*方和。

对于残差,自由度为 20(所有观察值均可变动),SS 是个体观察值与处理均值之间的*方差之和。均方(MS)是*方和除以自由度。

F 统计量为 MS(处理)/MS(误差)。因此,F 值仅取决于此比率,可以与标准 F 分布进行比较,以确定处理均值之间的差异是否超出了随机机会变化的预期。

方差分解

数据集中的观察值可以被视为不同组成部分的总和。对于数据集中的任何观察数据值,我们可以将其分解为总体*均值、治疗效应和残差误差。我们称之为“方差分解”:

  1. 以总体*均值开始(网页粘性数据为 173.75)。

  2. 添加治疗效果,可能是负面的(自变量=网页)。

  3. 添加残差误差,可能是负面的。

因此,A/B/C/D 测试表中左上角值的方差分解如下:

  1. 以总体*均值开始:173.75。

  2. 添加治疗(组)效应:-1.75(172 - 173.75)。

  3. 添加残差:-8(164 - 172)。

  4. 等于:164。

双向方差分析

刚刚描述的 A/B/C/D 测试是“单因素”方差分析,其中我们有一个变化的因素(组)。我们可能涉及第二个因素——比如“周末与工作日”的情况——收集每种组合的数据(A 组周末,A 组工作日,B 组周末等)。这将是“双向方差分析”,我们将通过识别“交互作用效应”类似于单因素方差分析来处理。在确定总体*均效应和治疗效应之后,我们然后将每组周末和工作日的观察结果分离,并找到这些子集的*均值与治疗*均值之间的差异。

您可以看到,方差分析,然后是双向方差分析,是通向完整统计模型(如回归和 logistic 回归)的第一步,在这些模型中,可以对多个因素及其影响进行建模(参见第四章)。

进一步阅读

  • 《入门统计与分析:重新抽样视角》(Peter Bruce 著,Wiley 出版,2014 年)有一章关于方差分析。

  • 《实验设计与分析导论》(George Cobb 著,Wiley 出版,2008 年)是其主题的全面且可读的处理。

卡方检验

网页测试通常超越 A/B 测试,同时测试多种治疗方法。卡方检验用于计数数据,以测试其与某些预期分布的拟合程度。在统计实践中,chi-square统计量最常见的用途是与r × c列联表一起使用,以评估变量之间独立性的零假设是否合理(另见“卡方分布”)。

卡方检验最初由卡尔·皮尔逊(1900 年)开发,术语chi来自皮尔逊在文章中使用的希腊字母Χ。

注意

r × c 表示“行乘以列”——一个 2 × 3 表有两行和三列。

卡方检验:重新抽样方法

假设你正在测试三个不同的标题——A、B 和 C——并将它们分别在 1,000 名访客上运行,结果显示在表 3-4 中。

表 3-4。三个不同标题的网页测试结果

标题 A 标题 B 标题 C
Click 14 8 12
No-click 986 992 988

这些标题显然有所不同。标题 A 的点击率几乎是 B 的两倍。虽然实际数字较小,但是一个重采样过程可以测试点击率是否超过了可能由偶然引起的程度。对于这个测试,我们需要有点击的“预期”分布,在这种情况下,这将是在所有三个标题共享相同点击率的零假设下的分布,总体点击率为 34/3,000。根据这一假设,我们的列联表将如表 3-5 所示。

表 3-5. 如果所有三个标题具有相同的点击率(零假设下的预期)

标题 A 标题 B 标题 C
Click 11.33 11.33 11.33
No-click 988.67 988.67 988.67

皮尔逊残差定义为:

R = Observed-Expected Expected

R测量实际计数与期望计数的差异程度(见 表 3-6)。

表 3-6. 皮尔逊残差

标题 A 标题 B 标题 C
Click 0.792 –0.990 0.198
No-click –0.085 0.106 –0.021

卡方统计量定义为*方皮尔逊残差的总和:

Χ = i r j c R 2

其中rc分别是行数和列数。该示例的卡方统计量为 1.666。这比在随机模型中可能出现的要多吗?

我们可以使用这个重采样算法进行测试:

  1. 构建一个盒子,其中包含 34 个“1”(点击)和 2,966 个“0”(无点击)。

  2. 洗牌,取三个独立的样本各 1,000 次,并计算每次的点击数。

  3. 找到洗牌计数与期望计数之间的*方差异并将它们求和。

  4. 重复步骤 2 和 3,例如 1,000 次。

  5. 重复多次的重采样*方偏差总和是否超过观察到的频次?这就是 p 值。

函数chisq.test可以用于在R中计算重采样卡方统计量。对于点击数据,卡方检验如下:

> chisq.test(clicks, simulate.p.value=TRUE)

	Pearson's Chi-squared test with simulated p-value (based on 2000 replicates)

data:  clicks
X-squared = 1.6659, df = NA, p-value = 0.4853

测试表明,这个结果很可能是随机得到的。

要在Python中运行置换检验,请使用以下实现:

box = [1] * 34
box.extend([0] * 2966)
random.shuffle(box)

def chi2(observed, expected):
    pearson_residuals = []
    for row, expect in zip(observed, expected):
        pearson_residuals.append([(observe - expect) ** 2 / expect
                                  for observe in row])
    # return sum of squares
    return np.sum(pearson_residuals)

expected_clicks = 34 / 3
expected_noclicks = 1000 - expected_clicks
expected = [34 / 3, 1000 - 34 / 3]
chi2observed = chi2(clicks.values, expected)

def perm_fun(box):
    sample_clicks = [sum(random.sample(box, 1000)),
                     sum(random.sample(box, 1000)),
                     sum(random.sample(box, 1000))]
    sample_noclicks = [1000 - n for n in sample_clicks]
    return chi2([sample_clicks, sample_noclicks], expected)

perm_chi2 = [perm_fun(box) for _ in range(2000)]

resampled_p_value = sum(perm_chi2 > chi2observed) / len(perm_chi2)
print(f'Observed chi2: {chi2observed:.4f}')
print(f'Resampled p-value: {resampled_p_value:.4f}')

卡方检验:统计理论

渐*统计理论表明,卡方统计量的分布可以*似为卡方分布(见 “卡方分布”)。适当的标准卡方分布由自由度决定(见 “自由度”)。对于列联表,自由度与行数(r)和列数(c)相关如下:

degrees of freedom = ( r - 1 ) × ( c - 1 )

卡方分布通常是偏态的,右侧有一个长尾;详见图 3-7,其中包括 1、2、5 和 20 自由度的分布。观察到的统计量越偏离卡方分布的中心,p 值就越低。

函数 chisq.test 可用于使用卡方分布作为参考计算 p 值:

> chisq.test(clicks, simulate.p.value=FALSE)

	Pearson's Chi-squared test

data:  clicks
X-squared = 1.6659, df = 2, p-value = 0.4348

Python 中,使用函数 scipy.stats.chi2_contingency

chisq, pvalue, df, expected = stats.chi2_contingency(clicks)
print(f'Observed chi2: {chi2observed:.4f}')
print(f'p-value: {pvalue:.4f}')

p 值略低于重采样 p 值;这是因为卡方分布仅是统计量实际分布的*似。

1, 2, 5 和 20 自由度的卡方分布

图 3-7. 具有不同自由度的卡方分布

费希尔精确检验

卡方分布是刚刚描述的洗牌重采样测试的良好*似,除非计数非常低(特别是五个或更少)。在这种情况下,重采样过程将产生更准确的 p 值。事实上,大多数统计软件都有一个过程,可以实际枚举所有可能的重新排列(排列),记录它们的频率,并确定观察结果的极端程度。这被称为费希尔精确检验,以伟大的统计学家 R. A. 费希尔命名。R 中费希尔精确检验的基本形式代码很简单:

> fisher.test(clicks)

	Fisher's Exact Test for Count Data

data:  clicks
p-value = 0.4824
alternative hypothesis: two.sided

p 值非常接*使用重采样方法得到的 p 值 0.4853。

在一些计数非常低而其他计数相当高的情况下(例如转化率中的分母),可能需要执行一个洗牌重排测试,而不是完全精确的检验,因为计算所有可能的排列的难度。前述的 R 函数有几个参数可以控制是否使用这种*似 (simulate.p.value=TRUE or FALSE),使用多少次迭代 (B=...),以及限制计算 精确 结果的计算约束 (workspace=...)。

Python 中,没有费希尔精确检验的易于使用的实现。

数据科学的相关性

卡方检验或费希尔精确检验,用于确定效应是否真实或可能是偶然产生的结果。在大多数经典统计应用中,卡方检验的作用是建立统计显著性,在研究或实验发表之前通常是必需的。但对于数据科学家来说,并不是那么重要。在大多数数据科学实验中,无论是 A/B 测试还是 A/B/C...,目标不仅仅是建立统计显著性,而是找到最佳处理方法。为此,多臂赌博机算法(参见“多臂赌博机算法”)提供了更完整的解决方案。

卡方检验的一个数据科学应用,特别是 Fisher 确切版本,是确定网页实验的适当样本大小。这些实验通常具有非常低的点击率,尽管曝光量可能很高,但计数率可能太低,无法在实验中得出明确的结论。在这种情况下,Fisher 确切检验、卡方检验和其他检验可以作为功效和样本大小计算的组成部分是有用的(见“功效和样本大小”)。

卡方检验广泛应用于研究中,研究人员希望找到那个难以捉摸的具有统计显著性的 p 值,这将允许发表成果。卡方检验或类似的重抽样模拟在数据科学应用中更多地作为一种筛选工具,用于确定效应或特征是否值得进一步考虑,而不是作为正式的显著性检验。例如,在空间统计和地图制作中使用它们来确定空间数据是否符合指定的空值分布(例如,犯罪是否比随机机会更集中在某一区域?)。它们还可以用于机器学习中的自动特征选择,以评估跨特征的类别普遍性,并识别类别普遍性异常高或低的特征,这与随机变化不兼容。

进一步阅读

  • R·A·费舍尔在 20 世纪初著名的“品茶女士”例子仍然是他确切检验的一个简单有效的说明。搜索“品茶女士”,你会找到一些很好的文章。

  • Stat Trek 提供了一个卡方检验的好教程

多臂老丨虎丨机算法

多臂老丨虎丨机提供了一种测试方法,特别是网页测试,它允许显式优化和比传统统计方法更快的决策制定。

传统的 A/B 测试涉及根据指定的设计收集实验数据,以回答特定问题,例如,“哪个更好,处理 A 还是处理 B?”假设一旦我们得到了这个问题的答案,实验就结束了,我们就可以根据结果采取行动。

您可能会发现这种方法存在几个困难。首先,我们的答案可能不确定:“效果未被证明”。换句话说,实验结果可能显示出效果,但如果有效果,我们没有足够大的样本来证明它(达到传统统计标准的要求)。我们应该做出什么决定?其次,我们可能希望在实验结束之前开始利用获得的结果。第三,我们可能希望根据实验结束后获得的额外数据改变主意或尝试其他方法。传统的实验和假设检验方法可以追溯到 20 世纪 20 年代,而且相当死板。计算机技术和软件的出现使得更加强大和灵活的方法成为可能。此外,数据科学(以及业务普遍)并不像传统统计学那样担心统计显著性,而更关心优化整体工作和结果。

多臂老丨虎丨机算法在网络测试中非常流行,允许您同时测试多种治疗方法,并比传统统计设计更快地得出结论。它们的名字源自赌场中的老丨虎丨机,也称为单臂老丨虎丨机(因为它们被配置成稳定地从赌徒那里提取钱财)。如果您想象一个有多个手臂的老丨虎丨机,每个手臂的支付速率都不同,那么您就有了一个多臂老丨虎丨机,这就是这种算法的完整名称。

您的目标是尽可能赢取更多的钱,更具体地说,尽早确定并决定赢利的手臂。挑战在于您不知道每个手臂的总体支付率是多少 —— 您只知道各个手臂的单次拉动结果。假设每次“赢”的金额都相同,无论是哪个手臂。不同的是赢的概率。进一步假设您最初对每个手臂进行了 50 次尝试,并获得了以下结果:

  • 手臂 A:50 次中赢了 10 次

  • 手臂 B:50 次中赢了 2 次

  • 手臂 C:50 次中赢了 4 次

一种极端的方法是说:“看起来 A 臂是赢家——让我们停止尝试其他臂并坚持使用 A。” 这充分利用了初始试验的信息。 如果 A 确实更优越,我们会从早期就获得这个好处。 另一方面,如果 B 或 C 确实更好,我们将失去发现这一点的机会。 另一种极端的方法是说:“这一切似乎都在偶然范围内——让我们保持所有的选择机会均等。” 这给了 A 的备选品最大的展示机会。 然而,在这个过程中,我们正在使用似乎是次优的处理方法。 我们要允许这样多久? 赌徒算法采用混合方法:我们开始更频繁地拉 A,以利用其表面上的优势,但我们不放弃 B 和 C。 我们只是更少地拉它们。 如果 A 继续表现优异,我们将继续将资源(拉动)从 B 和 C 转移并更频繁地拉 A。 另一方面,如果 C 开始做得更好,而 A 开始做得更差,我们可以将拉动从 A 转移到 C。 如果其中一个证明比 A 更优秀,并且由于偶然在初始试验中隐藏了这一点,那么它现在有机会通过进一步测试出现。

现在考虑将其应用于网页测试。 而不是多个老丨虎丨机臂,您可能在网站上测试多个优惠、标题、颜色等。 客户要么点击(对商家来说是“赢”),要么不点击。 最初,优惠是随机和*等地显示的。 但是,如果一个优惠开始优于其他优惠,它可以更频繁地显示(“拉动”)。 但是,修改拉动率的算法的参数应该是什么? 我们应该将“拉动率”更改为什么,以及何时更改?

这里有一个简单的算法,用于 A/B 测试的 epsilon-greedy 算法:

  1. 生成一个介于 0 和 1 之间的均匀分布随机数。

  2. 如果数字在 0 到 epsilon 之间(其中 epsilon 是介于 0 和 1 之间的数字,通常相当小),则抛硬币(50/50 概率),并且:

    1. 如果硬币正面朝上,则显示优惠 A。

    2. 如果硬币是反面朝上,则显示优惠 B。

  3. 如果数字 ≥ epsilon,则显示到目前为止响应率最高的优惠。

Epsilon 是控制此算法的单个参数。 如果 epsilon 为 1,则我们最终得到一个标准的简单 A/B 实验(对每个对象在 A 和 B 之间进行随机分配)。 如果 epsilon 为 0,则我们最终得到一个纯粹的 贪心 算法——选择最佳的可用即时选项(局部最优解)。 它不寻求进一步的实验,只是将对象(网站访问者)分配给表现最佳的处理方式。

更复杂的算法使用“汤普森抽样”。该过程在每个阶段“抽样”(拉动老丨虎丨机手臂)以最大化选择最佳手臂的概率。当然,你不知道哪个是最佳手臂——这正是整个问题的关键!但随着每次抽样的观察结果,你会获得更多信息。汤普森抽样采用贝叶斯方法:最初假设一些奖励的先验分布,使用所谓的贝塔分布(这是贝叶斯问题中指定先验信息的常见机制)。随着每次抽样积累的信息,可以更新此信息,允许更好地优化选择正确的手臂。

赌徒算法能够高效处理 3 个以上的处理,并朝向选择“最佳”方向优化。对于传统的统计测试程序,处理 3 个以上的复杂决策远远超过传统的 A/B 测试,赌徒算法的优势则更为明显。

进一步阅读

  • John Myles White 在《网站优化的赌徒算法》(O’Reilly, 2012)中对多臂老丨虎丨机算法进行了出色的简要介绍。White 包括Python代码,以及评估老丨虎丨机性能的模拟结果。

  • 关于汤普森抽样的更多(稍微技术性的)信息,请参见《多臂老丨虎丨机问题的汤普森抽样分析》,作者是 Shipra Agrawal 和 Navin Goyal。

动力和样本量

如果你进行网页测试,如何决定它应该运行多长时间(即每种处理需要多少次印象)?尽管你可能在许多网页测试指南中读到,没有好的通用指导——这主要取决于实现期望目标的频率。

在样本量统计计算中,判断一个假设检验是否能够揭示处理 A 和处理 B 之间的差异是一个步骤。假设检验的结果——p 值——取决于处理 A 和处理 B 之间的实际差异大小,也取决于实验中选择组的随机性。但逻辑上讲,处理 A 和处理 B 之间的实际差异越大,我们的实验揭示它的可能性就越大;而差异越小,就需要更多数据来检测它。在棒球中区分打击率为.350 和.200 的击球手,并不需要很多打数。而要区分打击率为.300 和.280 的击球手,则需要更多打数。

功效是指在具有特定样本特征(大小和变异性)的情况下检测到指定效应大小的概率。例如,我们可能会(假设地)说,在 25 次打击中区分 .330 的击球手和 .200 的击球手的概率是 0.75。这里的效应大小是 0.130 的差异。“检测”意味着假设检验将拒绝“没有差异”的零假设,并得出存在真实效应的结论。因此,对于两名击球手的 25 次打击实验(n = 25),效应大小为 0.130,具有(假设的)功效为 0.75,或 75%。

您可以看到这里有几个可变部分,很容易在需要的众多统计假设和公式中陷入混乱(用于指定样本变异性、效应大小、样本大小、假设检验的 alpha 水*等,并计算功效)。事实上,有专门的统计软件来计算功效。大多数数据科学家不需要经历报告功效所需的所有正式步骤,例如在发表的论文中。但是,他们可能会面临一些情况,他们希望为 A/B 测试收集一些数据,而收集或处理数据涉及一些成本。在这种情况下,知道大约需要收集多少数据可以帮助避免这样的情况,即您付出一些努力收集数据,而结果最终变得无法确定。这里有一种相当直观的替代方法:

  1. 从一些假设数据开始,这些数据代表了您对结果数据的最佳猜测(可能基于先前数据)—例如,一个包含 20 个 1 和 80 个 0 的盒子,代表一个 .200 的击球率,或者一个包含一些“网站上花费时间”的观察结果的盒子。

  2. 简单地通过在第一个样本上添加所需的效应大小来创建第二个样本—例如,一个包含 33 个 1 和 67 个 0 的第二个盒子,或者一个在每个初始“网站上花费时间”上添加 25 秒的第二个盒子。

  3. 从每个盒子中抽取大小为 n 的自举样本。

  4. 对两个自举样本进行排列(或基于公式的)假设检验,并记录它们之间的差异是否在统计上显著。

  5. 多次重复前面两个步骤,并确定差异显著的频率—这就是估计的功效。

样本大小

功效计算最常见的用途是估计您需要多大的样本。

例如,假设您正在观察点击率(点击数占曝光数的百分比),并测试一个新广告与现有广告的差异。在研究中需要累积多少点击次数?如果您只对显示出巨大差异的结果感兴趣(比如 50%的差异),可能一个相对较小的样本就足够了。另一方面,如果即使是微小的差异也值得关注,那么就需要一个更大的样本。一个标准的方法是建立一个政策,即新广告必须比现有广告好出一定百分比,比如 10%;否则,现有广告将继续使用。这个目标,即“效应大小”,驱动着样本量。

例如,假设当前的点击率约为 1.1%,您希望增加到 1.21%。所以我们有两个盒子:盒子 A 有 1.1%的 1(比如,110 个 1 和 9,890 个 0),盒子 B 有 1.21%的 1(比如,121 个 1 和 9,879 个 0)。首先,让我们从每个盒子中尝试 300 次抽样(这相当于每个广告 300 次“曝光”)。假设我们第一次抽样结果如下:

  • 盒子 A:3 个 1

  • 盒子 B:5 个 1

立即就可以看出,任何假设检验都会显示这种差异(5 比 3)远远在随机变动范围内。样本量(n = 每组 300 个)和效应大小(10%的差异)的组合太小,以至于任何假设检验都无法可靠地显示出差异。

因此,我们可以尝试增加样本量(让我们尝试 2,000 次曝光),并要求更大的改进(50%而不是 10%)。

例如,假设当前的点击率仍然是 1.1%,但我们现在希望增加 50%到 1.65%。所以我们有两个盒子:盒子 A 仍然有 1.1%的 1(比如,110 个 1 和 9,890 个 0),盒子 B 有 1.65%的 1(比如,165 个 1 和 9,868 个 0)。现在我们将从每个盒子中抽取 2,000 次。假设我们第一次抽样结果如下:

  • 盒子 A:19 个 1

  • 盒子 B:34 个 1

对于这种差异(34-19)的显著性检验仍然显示为“不显著”(尽管比之前的 5-3 差异更接*显著)。要计算功率,我们需要多次重复上述过程,或者使用可以计算功率的统计软件,但我们最初的抽样结果提示我们,即使是检测到 50%的改进,也需要数千次广告曝光。

总结一下,用于计算功率或所需样本量的有四个变动部分:

  • 样本量

  • 您希望检测的效应大小

  • 进行测试的显著水*(α)

  • 功率

指定其中三个,第四个可以计算。通常,您希望计算样本大小,因此必须指定其他三个。使用RPython,您还必须指定备择假设为“更大”或“较大”,以进行单侧检验;请参阅“单向与双向假设检验”了解更多单侧与双侧检验的讨论。这里是一个涉及两个比例的检验的R代码,其中两个样本大小相同(使用pwr包):

effect_size = ES.h(p1=0.0121, p2=0.011)
pwr.2p.test(h=effect_size, sig.level=0.05, power=0.8, alternative='greater’)
--
     Difference of proportion power calculation for binomial distribution
                                                       (arcsine transformation)

              h = 0.01029785
              n = 116601.7
      sig.level = 0.05
          power = 0.8
    alternative = greater

NOTE: same sample sizes

函数ES.h计算效应大小。我们看到,如果我们希望达到 80%的功效,我们需要* 120,000 次印象的样本大小。如果我们寻求 50%的增长(p1=0.0165),样本大小可减少到 5,500 次印象。

statsmodels包含几种功效计算方法。在这里,我们使用proportion_effectsize来计算效应大小,并使用TTestIndPower来解决样本大小:

effect_size = sm.stats.proportion_effectsize(0.0121, 0.011)
analysis = sm.stats.TTestIndPower()
result = analysis.solve_power(effect_size=effect_size,
                              alpha=0.05, power=0.8, alternative='larger')
print('Sample Size: %.3f' % result)
--
Sample Size: 116602.393

进一步阅读

  • 样本大小确定和功效,作者托马斯·瑞安(Thomas Ryan)(Wiley, 2013),对这一主题进行了全面而易读的评论。

  • 统计顾问史蒂夫·西蒙(Steve Simon)撰写了一篇非常引人入胜的叙事风格文章

摘要

实验设计原则——将受试者随机分配到接受不同处理的两个或多个组中——允许我们对处理效果进行有效结论。最好包括“不做任何变化”的对照处理。正式的统计推断——假设检验、p 值、t 检验以及更多类似内容——在传统统计课程或文本中占据了大量时间和空间,但从数据科学的角度来看,这种形式大多数情况下并不需要。然而,仍然重要的是认识到随机变异可能会在数据分析中误导人类大脑的角色。直观的重采样过程(排列和自举)使数据科学家能够评估偶然变异在其数据分析中的作用程度。

^(1) 乘法规则指出,n个独立事件同时发生的概率等于各个事件概率的乘积。例如,如果你和我各抛一次硬币,你的硬币和我的硬币都是正面朝上的概率是 0.5 × 0.5 = 0.25。

第四章:回归与预测

统计学中最常见的目标之一是回答问题“变量X(或更可能是X 1 , ... , X p)与变量Y是否相关,如果相关,它们之间的关系是什么,我们能否用它来预测Y?”

统计学和数据科学之间的联系在预测领域尤为显著,特别是基于其他“预测”变量的值来预测结果(目标)变量。在这个过程中,通过已知结果的数据训练模型,然后应用于结果未知的数据,被称为监督学习。数据科学和统计学之间的另一个重要联系在于异常检测领域,其中最初用于数据分析和改进回归模型的回归诊断方法可以用于检测异常记录。

简单线性回归

简单线性回归提供了一个描述一个变量的大小与第二个变量的大小之间关系的模型,例如,随着X的增加,Y也增加。或者随着X的增加,Y减少^(1)。相关性是衡量两个变量关系的另一种方法,请参见“相关性”部分。不同之处在于,相关性衡量了两个变量之间关系的强度,而回归则量化了这种关系的性质

回归方程

简单线性回归估计了X变化一定量时Y会如何变化。通过相关系数,变量XY是可以互换的。通过回归,我们尝试使用线性关系(即一条直线)来预测Y变量:

Y = b 0 + b 1 X

我们可以将这个关系读作“Y 等于 b[1]乘以 X,再加上一个常数 b[0]”。符号b 0被称为截距(或常数),符号b 1被称为X斜率。在R输出中,它们都显示为系数,尽管在一般用法中,术语系数通常保留给b 1Y变量被称为响应因变量,因为它取决于XX变量被称为预测自变量。机器学习社区倾向于使用其他术语,称Y目标X特征向量。在本书中,我们将预测特征这两个术语互换使用。

考虑散点图 Figure 4-1,显示工人暴露于棉尘的年数(Exposure)与肺活量的测量(PEFR或“呼气流量峰值”)。PEFRExposure有什么关系?仅凭图片很难判断。

images/lung_scatter.png

图 4-1. 棉尘暴露与肺活量

简单线性回归试图找到最佳线来预测响应变量PEFR与预测变量Exposure之间的关系:

PEFR = b 0 + b 1 Exposure

R中,lm函数可用于拟合线性回归:

model <- lm(PEFR ~ Exposure, data=lung)

lm代表线性模型~符号表示PEFRExposure预测。在此模型定义中,截距自动包含并适合。如果要从模型中排除截距,需要将模型定义写成如下形式:

PEFR ~ Exposure - 1

打印model对象将产生以下输出:

Call:
lm(formula = PEFR ~ Exposure, data = lung)

Coefficients:
(Intercept)     Exposure
    424.583       -4.185

截距,即b 0,为 424.583,可解释为对于经历零年棉尘暴露的工人预测的PEFR。回归系数,或b 1,可解释如下:每增加一年的棉尘暴露,工人的PEFR测量值减少了-4.185。

Python中,我们可以使用scikit-learn包中的LinearRegression。(statsmodels包具有更类似于R的线性回归实现(sm.OLS);我们稍后将在本章中使用它):

predictors = ['Exposure']
outcome = 'PEFR'

model = LinearRegression()
model.fit(lung[predictors], lung[outcome])

print(f'Intercept: {model.intercept_:.3f}')
print(f'Coefficient Exposure: {model.coef_[0]:.3f}')

该模型的回归线显示在 Figure 4-2 中。

images/lung_model.png

图 4-2. 适合肺活量数据的斜率和截距

拟合值和残差

回归分析中的重要概念是拟合值(预测值)和残差(预测误差)。一般来说,数据不会完全落在一条直线上,因此回归方程应包括显式的误差项 e i

Y i = b 0 + b 1 X i + e i

拟合值,也称为预测值,通常用Y ^ i(Y-hat)表示。这些值为:

Y ^ i = b ^ 0 + b ^ 1 X i

符号 b ^ 0b ^ 1 表示系数是根据已知值进行估计的。

帽子符号:估计值与已知值

“帽子”符号用于区分估计值和已知值。因此,符号 b ^(“b-hat”)是未知参数 b 的估计值。为什么统计学家要区分估计值和真实值?估计值具有不确定性,而真实值是固定的。^(2)

我们通过从原始数据中减去预测值来计算残差 e ^ i

e ^ i = Y i - Y ^ i

R中,我们可以使用predictresiduals函数获取拟合值和残差:

fitted <- predict(model)
resid <- residuals(model)

使用scikit-learnLinearRegression模型,我们在训练数据上使用predict方法获取fitted值,然后获取residuals。正如我们将看到的,这是scikit-learn中所有模型遵循的通用模式:

fitted = model.predict(lung[predictors])
residuals = lung[outcome] - fitted

图 4-3 展示了拟合到肺部数据的回归线的残差。残差是从数据点到回归线的垂直虚线的长度。

images/lung_residuals.png

图 4-3. 回归线的残差(为了容纳所有数据,y 轴的比例与图 4-2 不同,因此看起来斜率明显不同)

最小二乘法

数据如何拟合模型?当存在明显关系时,可以想象手动拟合线条。实际上,回归线是通过最小化残差*方和(也称为残差*方和RSS)的估计来确定的:

R S S = i=1 n Y i -Y ^ i 2 = i=1 n Y i -b ^ 0 -b ^ 1 X i 2

估计值 b ^ 0b ^ 1 是使残差*方和最小化的值。

最小二乘法回归,又称普通最小二乘(OLS)回归,是通过最小化残差*方和来估计拟合线的方法。虽然通常认为这个方法最早由德国数学家卡尔·弗里德里希·高斯提出,但实际上是由法国数学家阿德里安-玛丽·勒让德于 1805 年首次发表。任何标准统计软件都可以快速、轻松地计算最小二乘法回归。

历史上,计算方便是广泛使用最小二乘法回归的原因之一。随着大数据的出现,计算速度仍然是一个重要因素。最小二乘法与均值(参见“中位数和鲁棒估计”)一样,对异常值敏感,尽管这通常只在小型或中型数据集中是一个显著问题。请参见“异常值”讨论回归中的异常值。

回归术语

当分析师和研究人员单独使用术语回归时,他们通常是指线性回归;重点通常在于开发一个线性模型,以解释预测变量与数值结果变量之间的关系。在其正式统计意义上,回归还包括产生预测变量和结果变量之间功能关系的非线性模型。在机器学习社区中,这个术语偶尔也被宽泛地用来指代产生预测数值结果的任何预测模型(与预测二元或分类结果的方法相对)。

预测与解释(分析)

历史上,回归的主要用途是阐明预测变量与结果变量之间的假定线性关系。其目标是理解这种关系,并使用回归拟合的数据来解释它。在这种情况下,主要关注回归方程的估计斜率b ^ 。经济学家想要了解消费者支出与 GDP 增长之间的关系。公共卫生官员可能希望了解公共信息宣传活动是否有效促进安全性行为实践。在这些情况下,重点不在于预测个别案例,而在于理解变量之间的整体关系。

随着大数据的出现,回归广泛用于形成模型以预测新数据的个别结果(即预测模型),而不是解释手头的数据。在这种情况下,主要关注的是拟合值 Y ^ 。在营销中,回归可以用来预测广告活动规模对收入变化的影响。大学使用回归来预测学生的 GPA,基于他们的 SAT 成绩。

一个良好拟合数据的回归模型被设置为使X的变化导致Y的变化。然而,单独的回归方程并不能证明因果关系的方向。关于因果关系的结论必须来自对关系更广泛的理解。例如,回归方程可能显示网页广告点击次数与转化次数之间存在明确的关系。是我们对营销过程的了解,而不是回归方程,使我们得出点击广告导致销售而不是相反的结论。

进一步阅读

深入探讨预测与解释,请参阅加利特·舒默利的文章 “解释还是预测?”

多元线性回归

当存在多个预测变量时,方程式简单地扩展以适应它们:

Y = b 0 + b 1 X 1 + b 2 X 2 + ... + b p X p + e

现在不再是一条线,我们有一个线性模型—每个系数与其变量(特征)之间的关系是线性的。

在简单线性回归中的所有其他概念,如最小二乘法拟合以及拟合值和残差的定义,都扩展到多元线性回归设置中。例如,拟合值由以下公式给出:

Y ^ i = b ^ 0 + b ^ 1 X 1,i + b ^ 2 X 2,i + ... + b ^ p X p,i

示例:King County 房屋数据

多元线性回归的一个例子是估算房屋价值。县评估员必须估算房屋的价值以进行税收评估。房地产专业人士和购房者参考流行的网站,如Zillow,以确定公正的价格。以下是华盛顿州西雅图市金县(King County)的几行住房数据,来自house data.frame

head(house[, c('AdjSalePrice', 'SqFtTotLiving', 'SqFtLot', 'Bathrooms',
               'Bedrooms', 'BldgGrade')])
Source: local data frame [6 x 6]

  AdjSalePrice SqFtTotLiving SqFtLot Bathrooms Bedrooms BldgGrade
         (dbl)         (int)   (int)     (dbl)    (int)     (int)
1       300805          2400    9373      3.00        6         7
2      1076162          3764   20156      3.75        4        10
3       761805          2060   26036      1.75        4         8
4       442065          3200    8618      3.75        5         7
5       297065          1720    8620      1.75        4         7
6       411781           930    1012      1.50        2         8

pandas数据框的head方法列出了顶部几行:

subset = ['AdjSalePrice', 'SqFtTotLiving', 'SqFtLot', 'Bathrooms',
          'Bedrooms', 'BldgGrade']
house[subset].head()

目标是根据其他变量预测销售价格。lm函数通过在方程右侧包含更多项简单地处理多重回归情况;参数na.action=na.omit使得模型丢弃具有缺失值的记录:

house_lm <- lm(AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
               Bedrooms + BldgGrade,
               data=house, na.action=na.omit)

scikit-learnLinearRegression也可以用于多元线性回归:

predictors = ['SqFtTotLiving', 'SqFtLot', 'Bathrooms', 'Bedrooms', 'BldgGrade']
outcome = 'AdjSalePrice'

house_lm = LinearRegression()
house_lm.fit(house[predictors], house[outcome])

打印house_lm对象会产生以下输出:

house_lm

Call:
lm(formula = AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
    Bedrooms + BldgGrade, data = house, na.action = na.omit)

Coefficients:
  (Intercept)  SqFtTotLiving        SqFtLot      Bathrooms
   -5.219e+05      2.288e+02     -6.047e-02     -1.944e+04
     Bedrooms      BldgGrade
   -4.777e+04      1.061e+05

对于LinearRegression模型,截距和系数分别是拟合模型的intercept_coef_字段:

print(f'Intercept: {house_lm.intercept_:.3f}')
print('Coefficients:')
for name, coef in zip(predictors, house_lm.coef_):
    print(f' {name}: {coef}')

系数的解释与简单线性回归相同:预测值Y ^X j每单位变化时变化b j,假设所有其他变量X k对于k j保持不变。例如,将房屋增加一个完工*方英尺大约会增加大约 229 美元的估值;增加 1,000 *方英尺则意味着价值将增加约 228,800 美元。

评估模型

从数据科学的角度来看,最重要的性能指标是均方根误差,或RMSE。RMSE 是预测值y ^ i的*均*方误差的*方根:

R M S E = i=1 n y i -y ^ i 2 n

这衡量了模型的整体准确性,并且是与其他模型(包括使用机器学习技术拟合的模型)进行比较的基础。类似于 RMSE 的是残差标准误差,或者RSE。在这种情况下,我们有p个预测变量,RSE 由以下公式给出:

R S E = i=1 n y i -y ^ i 2 n-p-1

唯一的区别在于分母是自由度,而不是记录数(参见“自由度”)。实际上,对于线性回归,在大数据应用中,RMSE 和 RSE 之间的差异非常小。

R中的summary函数计算了回归模型的 RSE 以及其他指标:

summary(house_lm)

Call:
lm(formula = AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
    Bedrooms + BldgGrade, data = house, na.action = na.omit)

Residuals:
     Min       1Q   Median       3Q      Max
-1199479  -118908   -20977    87435  9473035

Coefficients:
                Estimate Std. Error t value Pr(>|t|)
(Intercept)   -5.219e+05  1.565e+04 -33.342  < 2e-16 ***
SqFtTotLiving  2.288e+02  3.899e+00  58.694  < 2e-16 ***
SqFtLot       -6.047e-02  6.118e-02  -0.988    0.323
Bathrooms     -1.944e+04  3.625e+03  -5.363 8.27e-08 ***
Bedrooms      -4.777e+04  2.490e+03 -19.187  < 2e-16 ***
BldgGrade      1.061e+05  2.396e+03  44.277  < 2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 261300 on 22681 degrees of freedom
Multiple R-squared:  0.5406,	Adjusted R-squared:  0.5405
F-statistic:  5338 on 5 and 22681 DF,  p-value: < 2.2e-16

scikit-learn提供了许多回归和分类的度量指标。这里,我们使用mean_squared_error来获取 RMSE,使用r2_score来获取决定系数:

fitted = house_lm.predict(house[predictors])
RMSE = np.sqrt(mean_squared_error(house[outcome], fitted))
r2 = r2_score(house[outcome], fitted)
print(f'RMSE: {RMSE:.0f}')
print(f'r2: {r2:.4f}')

使用statsmodels来获取Python中回归模型的更详细分析:

model = sm.OLS(house[outcome], house[predictors].assign(const=1))
results = model.fit()
results.summary()

在这里使用的pandas方法assign添加了一个值为 1 的常数列到预测因子中。这对于建模截距是必需的。

在软件输出中,您还会看到另一个有用的度量指标是决定系数,也称为R *方统计量或R 2。R *方介于 0 到 1 之间,衡量了模型解释数据变异的比例。主要用于回归分析中,帮助评估模型对数据的拟合程度。R *方的公式如下:

R 2 = 1 - i=1 n y i -y ^ i 2 i=1 n y i -y ¯ 2

分母与Y的方差成正比。R的输出还报告了调整后的 R *方,该指标考虑了自由度,有效地惩罚了向模型添加更多预测因子的情况。在大数据集上,这通常与多元回归中的R *方没有显著不同。

除了估计系数外,Rstatsmodels还报告了系数的标准误差(SE)和t 统计量

t b = b ^ SEb ^

t 统计量及其镜像,即 p 值,衡量了系数在统计上的显著性,即预测因子与目标变量的随机安排可能产生的范围之外的程度。t 统计量越高(p 值越低),预测因子越显著。由于简洁性是一个有价值的模型特征,因此有这样的工具来指导选择要包含为预测因子的变量是很有用的(参见“模型选择和逐步回归”)。

警告

除了 t 统计量外,R和其他包通常还报告p 值R输出中的Pr(>|t|))和F 统计量。数据科学家通常不会过多解释这些统计量,也不会过多关注统计显著性问题。数据科学家主要专注于 t 统计量作为是否在模型中包含预测因子的有用指南。较高的 t 统计量(与接* 0 的 p 值相对应)表明应保留预测因子在模型中,而非常低的 t 统计量表明可以删除预测因子。参见“p-Value”以获取更多讨论。

交叉验证

经典统计回归指标(、F 统计量和 p 值)都是“样本内”指标——它们适用于用于拟合模型的相同数据。直观地,您可以看到,将一些原始数据保留下来,不用于拟合模型,然后将模型应用于留出的(保留)数据,以查看其表现如何,这是非常有意义的。通常,您会使用大多数数据来拟合模型,并使用较小的部分来测试模型。

“样本外”验证的这个想法并不新鲜,但直到较大的数据集变得更普遍之前,它并没有真正被接受;对于小数据集,分析师通常希望使用所有数据并拟合最佳的模型。

然而,使用留出样本会使你受到一些不确定性的影响,这仅仅来自于小样本的变异性。如果选择不同的留出样本,评估结果会有多大不同?

交叉验证将留出样本的概念扩展到多个连续的留出样本。基本k 折交叉验证算法如下:

  1. 1/k的数据留出作为留出样本。

  2. 在剩余数据上训练模型。

  3. 将模型应用(评分)于1/k的留出样本,并记录所需的模型评估指标。

  4. 恢复数据的第1/k部分,并留出接下来的1/k部分(不包括任何第一次被选中的记录)。

  5. 重复步骤 2 和 3。

  6. 直到每条记录都在留出部分中使用为止。

  7. *均或以其他方式组合模型评估指标。

将数据分成训练样本和留出样本也称为折叠

模型选择和逐步回归

在一些问题中,可以使用许多变量作为回归的预测变量。例如,要预测房屋价值,可以使用额外的变量如地下室面积或建造年份。在R中,这些很容易添加到回归方程中:

house_full <- lm(AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
                 Bedrooms + BldgGrade + PropertyType + NbrLivingUnits +
                 SqFtFinBasement + YrBuilt + YrRenovated +
                 NewConstruction,
               data=house, na.action=na.omit)

Python中,我们需要将分类和布尔变量转换为数字:

predictors = ['SqFtTotLiving', 'SqFtLot', 'Bathrooms', 'Bedrooms', 'BldgGrade',
              'PropertyType', 'NbrLivingUnits', 'SqFtFinBasement', 'YrBuilt',
              'YrRenovated', 'NewConstruction']

X = pd.get_dummies(house[predictors], drop_first=True)
X['NewConstruction'] = [1 if nc else 0 for nc in X['NewConstruction']]

house_full = sm.OLS(house[outcome], X.assign(const=1))
results = house_full.fit()
results.summary()

然而,添加更多变量并不一定意味着我们拥有更好的模型。统计学家使用奥卡姆剃刀原则来指导模型的选择:其他条件相等时,应优先使用简单的模型而不是更复杂的模型。

添加额外变量总是降低 RMSE 并增加R 2的训练数据。因此,这些不适合帮助指导模型选择。包含模型复杂性的一种方法是使用调整后的R 2

R a d j 2 = 1 ( 1 R 2 ) n 1 n P 1

这里,n 是记录数,P 是模型中的变量数。

在 20 世纪 70 年代,日本著名统计学家赤池弘次开发了一种叫做AIC(赤池信息准则)的度量,惩罚模型中增加的项。在回归的情况下,AIC 的形式为:

  • AIC = 2P + n log(RSS/n)

其中P是变量数,n是记录数。目标是找到最小化 AIC 的模型;具有k个额外变量的模型会受到 2k的惩罚。

AIC,BIC 和 Mallows Cp

AIC 的公式可能看起来有点神秘,但实际上是基于信息理论中的渐*结果。AIC 有几个变体:

AICc

AIC 的小样本修正版本。

BIC 或贝叶斯信息准则

与 AIC 类似,对于将额外变量包括在模型中,惩罚力度更强。

Mallows Cp

Colin Mallows 开发的 AIC 的一个变体。

这些通常作为样本内度量报告(即在训练数据上),使用留出数据进行模型评估的数据科学家不需要担心它们之间的差异或其背后的理论。

如何找到最小化 AIC 或最大化调整R 2的模型?一种方法是搜索所有可能的模型,这称为全子集回归。这在具有大量数据和变量的问题中计算成本高昂,不可行。一个吸引人的替代方案是使用逐步回归。它可以从一个完整模型开始,并逐步删除没有意义的变量。这被称为向后消除。或者可以从一个常数模型开始,并逐步添加变量(前向选择)。作为第三个选择,我们还可以逐步添加和删除预测因子,以找到降低 AIC 或调整R 2的模型。Venebles 和 Ripley 的R软件包中的MASS提供了一个称为stepAIC的逐步回归函数:

library(MASS)
step <- stepAIC(house_full, direction="both")
step

Call:
lm(formula = AdjSalePrice ~ SqFtTotLiving + Bathrooms + Bedrooms +
    BldgGrade + PropertyType + SqFtFinBasement + YrBuilt, data = house,
    na.action = na.omit)

Coefficients:
              (Intercept)              SqFtTotLiving
                6.179e+06                  1.993e+02
                Bathrooms                   Bedrooms
                4.240e+04                 -5.195e+04
                BldgGrade  PropertyTypeSingle Family
                1.372e+05                  2.291e+04
    PropertyTypeTownhouse            SqFtFinBasement
                8.448e+04                  7.047e+00
                  YrBuilt
               -3.565e+03

scikit-learn没有逐步回归的实现。我们在我们的dmba包中实现了stepwise_selectionforward_selectionbackward_elimination函数:

y = house[outcome]

def train_model(variables): ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
    if len(variables) == 0:
        return None
    model = LinearRegression()
    model.fit(X[variables], y)
    return model

def score_model(model, variables): ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/2.png)
    if len(variables) == 0:
        return AIC_score(y, [y.mean()] * len(y), model, df=1)
    return AIC_score(y, model.predict(X[variables]), model)

best_model, best_variables = stepwise_selection(X.columns, train_model,
                                                score_model, verbose=True)

print(f'Intercept: {best_model.intercept_:.3f}')
print('Coefficients:')
for name, coef in zip(best_variables, best_model.coef_):
    print(f' {name}: {coef}')

1

定义一个返回给定变量集的拟合模型的函数。

2

定义一个函数,为给定的模型和变量集返回一个分数。在这种情况下,我们使用dmba包中实现的AIC_score

该函数选择了一个模型,其中从house_full中删除了几个变量:SqFtLotNbrLivingUnitsYrRenovatedNewConstruction

更简单的方法是前向选择后向选择。在前向选择中,你从零个预测变量开始逐个添加,每一步都添加对R 2 贡献最大的预测变量,并在贡献不再具有统计显著性时停止。在后向选择或后向消除中,你从完整模型开始,并去掉不具有统计显著性的预测变量,直到剩下所有预测变量都具有统计显著性的模型。

Penalized regression的灵感与 AIC 类似。与明确搜索离散模型集不同,模型拟合方程包含一个约束,对模型因过多变量(参数)而进行惩罚。与逐步、前向和后向选择完全消除预测变量不同,惩罚回归通过减少系数来施加惩罚,在某些情况下几乎降为零。常见的惩罚回归方法包括岭回归套索回归

逐步回归和全子集回归是样本内方法,用于评估和调整模型。这意味着模型选择可能会受到过度拟合(拟合数据中的噪声)的影响,并且在应用于新数据时可能表现不佳。避免这种情况的一种常见方法是使用交叉验证来验证模型。在线性回归中,过度拟合通常不是一个主要问题,因为数据上强加了简单(线性)全局结构。对于更复杂类型的模型,特别是对局部数据结构响应的迭代过程,交叉验证是一个非常重要的工具;详见“交叉验证”。

加权回归

统计学家使用加权回归来处理各种目的;特别是,在复杂调查分析中,这是很重要的。数据科学家可能会发现在两种情况下使用加权回归很有用:

  • 当不同观察结果具有不同精度时,使用反方差加权;较高方差的观察结果获得较低的权重。

  • 分析数据,其中行代表多个案例;权重变量编码每行代表多少个原始观测值。

例如,在房屋数据中,较早的销售比较不可靠,而比较*期的销售则较为可靠。使用DocumentDate确定销售年份,我们可以计算Weight作为距离 2005 年(数据开始的时间)的年数:

R

library(lubridate)
house$Year = year(house$DocumentDate)
house$Weight = house$Year - 2005

Python

house['Year'] = [int(date.split('-')[0]) for date in house.DocumentDate]
house['Weight'] = house.Year - 2005

我们可以使用lm函数中的weight参数计算加权回归:

house_wt <- lm(AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
                 Bedrooms + BldgGrade,
               data=house, weight=Weight)
round(cbind(house_lm=house_lm$coefficients,
            house_wt=house_wt$coefficients), digits=3)

                 house_lm    house_wt
(Intercept)   -521871.368 -584189.329
SqFtTotLiving     228.831     245.024
SqFtLot            -0.060      -0.292
Bathrooms      -19442.840  -26085.970
Bedrooms       -47769.955  -53608.876
BldgGrade      106106.963  115242.435

加权回归中的系数与原始回归略有不同。

大多数 scikit-learn 中的模型在调用 fit 方法时接受权重作为关键字参数 sample_weight

predictors = ['SqFtTotLiving', 'SqFtLot', 'Bathrooms', 'Bedrooms', 'BldgGrade']
outcome = 'AdjSalePrice'

house_wt = LinearRegression()
house_wt.fit(house[predictors], house[outcome], sample_weight=house.Weight)

进一步阅读

可以在《统计学习导论》(Gareth James, Daniela Witten, Trevor Hastie 和 Robert Tibshirani 著,Springer,2013)中找到关于交叉验证和重抽样的优秀处理。

使用回归进行预测

在数据科学中,回归的主要目的是预测。这一点很重要,因为回归作为一种古老而成熟的统计方法,其背景更多与其作为解释建模工具的传统角色相关,而不是预测。

外推的危险

回归模型不应用于超出数据范围之外的外推(不考虑用于时间序列预测的回归情况)。该模型仅对数据拥有足够观测值的预测变量值有效(即使在有足够数据的情况下,也可能存在其他问题—参见“回归诊断”)。举个极端的例子,假设使用model_lm来预测一个面积为 5,000 *方英尺的空地的价值。在这种情况下,所有与建筑相关的预测变量将为 0,回归方程将给出一个荒谬的预测值为–521,900 + 5,000 × –0.0605 = –$522,202。为什么会发生这种情况?数据仅包含具有建筑物的地块—没有对应于空地的记录。因此,模型无法通过数据告诉如何预测空地的销售价格。

置信区间与预测区间

统计学的大部分内容涉及理解和衡量变异性(不确定性)。回归输出中报告的 t-统计量和 p-值以正式方式处理这一点,这在变量选择时有时是有用的(参见“评估模型”)。更有用的指标是置信区间,它是放置在回归系数和预测周围的不确定性区间。理解这一点的简单方法是通过自助法(更多关于一般自助法程序的详情,请参见“自助法”)。在软件输出中遇到的最常见的回归置信区间是回归参数(系数)的置信区间。以下是用于生成数据集中 P 个预测变量和 n 条记录的回归参数(系数)置信区间的自助法算法:

  1. 将每一行(包括结果变量)视为单独的“票据”,并将所有 n 张票据放入一个箱子中。

  2. 随机抽取一张票据,记录其值,并将其放回箱中。

  3. 重复步骤 2 n 次,现在你有了一个自助法重采样。

  4. 对 Bootstrap 样本拟合回归,并记录估计的系数。

  5. 重复步骤 2 到 4,例如 1,000 次。

  6. 现在每个系数都有 1,000 个自举值;找到每个系数的适当百分位数(例如,90% 置信区间的第 5 和第 95 百分位数)。

您可以使用 R 中的 Boot 函数生成系数的实际自举置信区间,或者简单地使用基于公式的区间,这是 R 输出的常规部分。概念意义和解释是相同的,对于数据科学家来说并不是核心问题,因为它们涉及回归系数。数据科学家更感兴趣的是围绕预测 y 值(Y ^ i)的区间。对于 Y ^ i 的不确定性来自两个来源:

  • 对相关的预测变量及其系数的不确定性(见前述自举算法)

  • 个体数据点固有的附加误差

可以将个体数据点误差理解为:即使我们确定了回归方程是什么(例如,如果我们有大量记录来拟合它),给定一组预测变量的实际结果值也会有所不同。例如,几栋房子——每栋房子都有 8 个房间、6500 *方英尺的地块、3 间浴室和一个地下室——可能具有不同的价值。我们可以通过拟合值的残差来建模这个个体误差。用于模拟回归模型误差和个体数据点误差的自举算法如下所示:

  1. 从数据中进行自举抽样(在前文详细解释)。

  2. 拟合回归模型,并预测新值。

  3. 随机从原始回归拟合中取一个单个残差,加到预测值上,并记录结果。

  4. 重复步骤 1 至 3,比如说 1,000 次。

  5. 找到结果的第 2.5 和第 97.5 百分位数。

预测区间还是置信区间?

预测区间涉及到单个数值的不确定性,而置信区间涉及到从多个数值计算得出的均值或其他统计量。因此,对于相同的数值来说,预测区间通常会比置信区间宽得多。我们在自举模型中模拟这个单个数值误差,通过选择一个个体残差来添加到预测值上。应该使用哪一个?这取决于上下文和分析的目的,但一般来说,数据科学家对特定的个体预测很感兴趣,因此预测区间更为合适。如果你应该使用预测区间而使用了置信区间,那么将极大地低估给定预测值的不确定性。

回归中的因子变量

因子变量,也称为分类变量,接受有限数量的离散值。例如,贷款目的可以是“债务合并”,“婚礼”,“汽车”等。二进制(是/否)变量,也称为指示变量,是因子变量的一种特殊情况。回归需要数值输入,因此因子变量需要被重新编码以在模型中使用。最常见的方法是将一个变量转换为一组二进制虚拟变量。

虚拟变量表示

在 King County 房屋数据中,有一个属性类型的因子变量;下面展示了一个小的六条记录子集:

R:

head(house[, 'PropertyType'])
Source: local data frame [6 x 1]

   PropertyType
         (fctr)
1     Multiplex
2 Single Family
3 Single Family
4 Single Family
5 Single Family
6     Townhouse

Python:

house.PropertyType.head()

有三种可能的取值:MultiplexSingle FamilyTownhouse。要使用这个因子变量,我们需要将其转换为一组二进制变量。在R中,我们使用 model.matrix 函数来实现这一点:^(3)

prop_type_dummies <- model.matrix(~PropertyType -1, data=house)
head(prop_type_dummies)
  PropertyTypeMultiplex PropertyTypeSingle Family PropertyTypeTownhouse
1                     1                         0                     0
2                     0                         1                     0
3                     0                         1                     0
4                     0                         1                     0
5                     0                         1                     0
6                     0                         0                     1

函数 model.matrix 将数据框转换为适合线性模型的矩阵。属性类型 PropertyType 是一个具有三个不同水*的因子变量,表示为一个有三列的矩阵。在机器学习社区中,这种表示称为独热编码(见“独热编码器”)。

Python中,我们可以使用 pandasget_dummies 方法将分类变量转换为虚拟变量:

pd.get_dummies(house['PropertyType']).head() ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
pd.get_dummies(house['PropertyType'], drop_first=True).head() ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/2.png)

1

默认情况下,返回分类变量的独热编码。

2

关键字参数 drop_first 将返回 P - 1 列。使用它来避免多重共线性问题。

在某些机器学习算法中,如最*邻居和树模型,独热编码是表示因子变量的标准方式(例如,见“树模型”)。

在回归设置中,一个具有P个不同水*的因子变量通常由一个只有P - 1 列的矩阵表示。这是因为回归模型通常包括一个截距项。有了截距项后,一旦为P - 1 个二进制变量定义了值,第P个的值就已知且可能被认为是冗余的。添加第P列将导致多重共线性错误(见“多重共线性”)。

R中的默认表示是使用第一个因子水*作为参考,并将其余水*解释为相对于该因子的:

lm(AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
       Bedrooms + BldgGrade + PropertyType, data=house)

Call:
lm(formula = AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
    Bedrooms + BldgGrade + PropertyType, data = house)

Coefficients:
              (Intercept)              SqFtTotLiving
               -4.468e+05                  2.234e+02
                  SqFtLot                  Bathrooms
               -7.037e-02                 -1.598e+04
                 Bedrooms                  BldgGrade
               -5.089e+04                  1.094e+05
PropertyTypeSingle Family      PropertyTypeTownhouse
               -8.468e+04                 -1.151e+05

方法 get_dummies 可以通过可选的关键字参数 drop_first 来排除第一个因子作为参考

predictors = ['SqFtTotLiving', 'SqFtLot', 'Bathrooms', 'Bedrooms',
              'BldgGrade', 'PropertyType']

X = pd.get_dummies(house[predictors], drop_first=True)

house_lm_factor = LinearRegression()
house_lm_factor.fit(X, house[outcome])

print(f'Intercept: {house_lm_factor.intercept_:.3f}')
print('Coefficients:')
for name, coef in zip(X.columns, house_lm_factor.coef_):
    print(f' {name}: {coef}')

R回归的输出显示了两个与PropertyType对应的系数:PropertyTypeSingle FamilyPropertyTypeTownhouse。由于在PropertyTypeSingle Family == 0PropertyTypeTownhouse == 0时它被隐含定义,所以没有Multiplex的系数。这些系数被解释为相对于Multiplex,因此一个Single Family的房屋价值减少了将*$85,000,而一个Townhouse的房屋价值减少了超过$150,000。^(4)

不同的因子编码方式

编码因子变量有几种不同的方法,被称为对比编码系统。例如,偏差编码,也称为总和对比,将每个水*与总体均值进行比较。另一种对比是多项式编码,适用于有序因子;参见章节“有序因子变量”。除了有序因子外,数据科学家通常不会遇到除参考编码或独热编码之外的任何编码类型。

具有多个水*的因子变量

一些因子变量可以生成大量的二进制虚拟变量——邮政编码是一个因子变量,在美国有 43,000 个邮政编码。在这种情况下,探索数据及预测变量与结果之间的关系,以确定类别中是否包含有用信息是有用的。如果是,您必须进一步决定是保留所有因子还是合并水*。

在金县,有 80 个房屋销售的邮政编码:

table(house$ZipCode)

98001 98002 98003 98004 98005 98006 98007 98008 98010 98011 98014 98019
  358   180   241   293   133   460   112   291    56   163    85   242
98022 98023 98024 98027 98028 98029 98030 98031 98032 98033 98034 98038
  188   455    31   366   252   475   263   308   121   517   575   788
98039 98040 98042 98043 98045 98047 98050 98051 98052 98053 98055 98056
   47   244   641     1   222    48     7    32   614   499   332   402
98057 98058 98059 98065 98068 98070 98072 98074 98075 98077 98092 98102
    4   420   513   430     1    89   245   502   388   204   289   106
98103 98105 98106 98107 98108 98109 98112 98113 98115 98116 98117 98118
  671   313   361   296   155   149   357     1   620   364   619   492
98119 98122 98125 98126 98133 98136 98144 98146 98148 98155 98166 98168
  260   380   409   473   465   310   332   287    40   358   193   332
98177 98178 98188 98198 98199 98224 98288 98354
  216   266   101   225   393     3     4     9

pandas数据框的value_counts方法返回相同的信息:

pd.DataFrame(house['ZipCode'].value_counts()).transpose()

ZipCode是一个重要的变量,因为它是房屋价值上的地理位置影响的代理变量。包含所有水*需要 79 个与自由度对应的系数。原始模型house_lm只有 5 个自由度;参见“评估模型”。此外,一些邮政编码只有一个销售。在某些问题中,您可以使用前两位或三位数字合并一个邮政编码,对应一个次大都会地理区域。对于金县,几乎所有销售都发生在 980xx 或 981xx,因此这并没有帮助。

另一种方法是根据另一个变量,如销售价格,对邮政编码进行分组。更好的方法是使用初始模型的残差形成邮政编码组。以下R中的dplyr代码基于house_lm回归的残差中位数将 80 个邮政编码合并为五组:

zip_groups <- house %>%
  mutate(resid = residuals(house_lm)) %>%
  group_by(ZipCode) %>%
  summarize(med_resid = median(resid),
            cnt = n()) %>%
  arrange(med_resid) %>%
  mutate(cum_cnt = cumsum(cnt),
         ZipGroup = ntile(cum_cnt, 5))
house <- house %>%
  left_join(select(zip_groups, ZipCode, ZipGroup), by='ZipCode')

对于每个邮政编码,计算中位数残差,并使用ntile函数根据中位数对邮政编码进行分组。有关如何在回归中使用这作为改进术语的示例,请参见“混杂变量”。

Python中,我们可以计算这些信息如下:

zip_groups = pd.DataFrame([
    *pd.DataFrame({
        'ZipCode': house['ZipCode'],
        'residual' : house[outcome] - house_lm.predict(house[predictors]),
    })
    .groupby(['ZipCode'])
    .apply(lambda x: {
        'ZipCode': x.iloc[0,0],
        'count': len(x),
        'median_residual': x.residual.median()
    })
]).sort_values('median_residual')
zip_groups['cum_count'] = np.cumsum(zip_groups['count'])
zip_groups['ZipGroup'] = pd.qcut(zip_groups['cum_count'], 5, labels=False,
                                 retbins=False)

to_join = zip_groups[['ZipCode', 'ZipGroup']].set_index('ZipCode')
house = house.join(to_join, on='ZipCode')
house['ZipGroup'] = house['ZipGroup'].astype('category')

利用残差来辅助指导回归拟合的概念是建模过程中的一个基本步骤;请参阅“回归诊断”。

有序因子变量

一些因子变量反映了因子的水*;这些称为有序因子变量有序分类变量。例如,贷款等级可以是 A、B、C 等,每个等级都比前一个等级更具风险性。通常,有序因子变量可以转换为数值并直接使用。例如,变量BldgGrade是一个有序因子变量。表 4-1 中显示了几种等级的类型。虽然等级具有特定的含义,但数值是按照从低到高的顺序排列的,对应于更高等级的房屋。在“多元线性回归”中拟合的回归模型house_lm中,BldgGrade被视为数值变量。

表 4-1. 建筑等级及数值对应关系

描述
1 小木屋
2 次标准
5 中等
10 很好
12 豪华
13 豪宅

将有序因子视为数值变量可以保留包含在排序中的信息,如果将其转换为因子,则会丢失这些信息。

解释回归方程

在数据科学中,回归的最重要用途之一是预测某些依赖(结果)变量。然而,在某些情况下,从方程本身获取洞察力,以理解预测变量与结果之间的关系性质也是有价值的。本节提供了检查回归方程和解释它的指导。

相关的预测变量

在多元回归中,预测变量通常彼此相关。例如,检查在“模型选择和逐步回归”中拟合的模型step_lm的回归系数。

R

step_lm$coefficients
              (Intercept)             SqFtTotLiving                 Bathrooms
             6.178645e+06              1.992776e+02              4.239616e+04
                 Bedrooms                 BldgGrade PropertyTypeSingle Family
            -5.194738e+04              1.371596e+05              2.291206e+04
    PropertyTypeTownhouse           SqFtFinBasement                   YrBuilt
             8.447916e+04              7.046975e+00             -3.565425e+03

Python

print(f'Intercept: {best_model.intercept_:.3f}')
print('Coefficients:')
for name, coef in zip(best_variables, best_model.coef_):
    print(f' {name}: {coef}')

Bedrooms的系数是负的!这意味着向房屋添加卧室会降低其价值。这怎么可能呢?这是因为预测变量是相关的:较大的房屋通常有更多的卧室,而房屋价值受大小驱动,而不是卧室数量。考虑两个完全相同大小的房屋:合理地期望拥有更多但更小的卧室的房屋会被认为不太理想。

拥有相关的预测变量可能会使回归系数的符号和数值难以解释(并可能使估计的标准误差增大)。卧室、房屋面积和浴室数量这些变量都是相关的。这在下面的R示例中得到了说明,该示例从方程中移除变量SqFtTotLivingSqFtFinBasementBathrooms后拟合了另一个回归方程:

update(step_lm, . ~ . - SqFtTotLiving - SqFtFinBasement - Bathrooms)

Call:
lm(formula = AdjSalePrice ~ Bedrooms + BldgGrade + PropertyType +
    YrBuilt, data = house, na.action = na.omit)

Coefficients:
              (Intercept)                   Bedrooms
                  4913973                      27151
                BldgGrade  PropertyTypeSingle Family
                   248998                     -19898
    PropertyTypeTownhouse                    YrBuilt
                   -47355                      -3212

update函数可用于向模型中添加或移除变量。现在卧室的系数为正——符合我们的预期(尽管现在这实际上是作为房屋尺寸的代理,因为这些变量已被移除)。

Python中,没有相当于Rupdate函数的功能。我们需要使用修改后的预测变量列表重新拟合模型:

predictors = ['Bedrooms', 'BldgGrade', 'PropertyType', 'YrBuilt']
outcome = 'AdjSalePrice'

X = pd.get_dummies(house[predictors], drop_first=True)

reduced_lm = LinearRegression()
reduced_lm.fit(X, house[outcome])

相关变量只是解释回归系数的一个问题。在house_lm中,没有变量来说明房屋的位置,模型将非常不同类型的地区混合在一起。位置可能是一个混淆变量;请参阅“混淆变量”以进一步讨论。

多重共线性

相关变量的一个极端情况产生了多重共线性——在预测变量之间存在冗余。完美多重共线性发生在一个预测变量可以表示为其他变量的线性组合时。多重共线性发生在以下情况下:

  • 一个变量因错误而多次被包含。

  • 从一个因子变量创建P个哑变量,而不是P - 1 个哑变量(请参阅“回归中的因子变量”)。

  • 两个变量几乎完全与彼此相关。

在回归中,多重共线性必须得到解决——变量应该被移除,直到多重共线性消失。在完美多重共线性存在的情况下,回归没有一个明确定义的解。许多软件包,包括RPython,可以自动处理某些类型的多重共线性。例如,如果在house数据的回归中两次包含SqFtTotLiving,则结果与house_lm模型的结果相同。在非完美多重共线性的情况下,软件可能会得出一个解,但结果可能不稳定。

注意

多重共线性对于树、聚类和最*邻等非线性回归方法并不是一个问题,在这些方法中保留P个哑变量(而不是P - 1 个)可能是明智的选择。尽管如此,在这些方法中,预测变量的非冗余性仍然是一种美德。

混淆变量

对于相关变量,问题是包含具有与响应变量类似的预测关系的不同变量。对于混淆变量,问题是省略了一个重要变量,该变量未包含在回归方程中。对方程系数的天真解释可能导致无效结论。

例如,来自“示例:金县住房数据”的金县回归方程house_lmSqFtLotBathroomsBedrooms的回归系数都为负。原始回归模型不包含代表位置的变量——房价的一个非常重要的预测因子。为了建模位置,包括一个将邮政编码分为五个组的变量ZipGroup,从最便宜(1)到最昂贵(5):^(5)

lm(formula = AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
    Bedrooms + BldgGrade + PropertyType + ZipGroup, data = house,
    na.action = na.omit)

Coefficients:
              (Intercept)              SqFtTotLiving
               -6.666e+05                  2.106e+02
                  SqFtLot                  Bathrooms
                4.550e-01                  5.928e+03
                 Bedrooms                  BldgGrade
               -4.168e+04                  9.854e+04
PropertyTypeSingle Family      PropertyTypeTownhouse
                1.932e+04                 -7.820e+04
                ZipGroup2                  ZipGroup3
                5.332e+04                  1.163e+05
                ZipGroup4                  ZipGroup5
                1.784e+05                  3.384e+05

Python中相同的模型:

predictors = ['SqFtTotLiving', 'SqFtLot', 'Bathrooms', 'Bedrooms',
              'BldgGrade', 'PropertyType', 'ZipGroup']
outcome = 'AdjSalePrice'

X = pd.get_dummies(house[predictors], drop_first=True)

confounding_lm = LinearRegression()
confounding_lm.fit(X, house[outcome])

print(f'Intercept: {confounding_lm.intercept_:.3f}')
print('Coefficients:')
for name, coef in zip(X.columns, confounding_lm.coef_):
    print(f' {name}: {coef}')

ZipGroup显然是一个重要的变量:预计位于最昂贵邮政编码组的房屋销售价格较高,约为$340,000。SqFtLotBathrooms的系数现在为正,并且每增加一个浴室,销售价格增加$5,928。

Bedrooms的系数仍然为负。尽管这听起来不直观,但这是房地产中众所周知的现象。对于具有相同可居住面积和浴室数量的房屋来说,拥有更多但因此更小的卧室与价值较低的房屋相关联。

交互作用和主效应

统计学家喜欢区分主效应或自变量和主效应之间的交互。主效应通常在回归方程中称为预测变量。当模型中仅使用主效应时的一个隐含假设是,预测变量与响应之间的关系独立于其他预测变量。这通常并非如此。

例如,适用于“混杂变量”中的金县住房数据的模型包括几个变量作为主要效应,包括ZipCode。在房地产中,位置至关重要,可以自然地假设,例如,房屋大小与销售价格之间的关系取决于位置。在低租金区建造的大房子不会像在昂贵地区建造的大房子一样保持相同的价值。在R中使用*运算符可以包括变量之间的交互。对于金县数据,以下内容适合于SqFtTotLivingZipGroup之间的交互:

lm(formula = AdjSalePrice ~ SqFtTotLiving * ZipGroup + SqFtLot +
    Bathrooms + Bedrooms + BldgGrade + PropertyType, data = house,
    na.action = na.omit)

Coefficients:
              (Intercept)              SqFtTotLiving
               -4.853e+05                  1.148e+02
                ZipGroup2                  ZipGroup3
               -1.113e+04                  2.032e+04
                ZipGroup4                  ZipGroup5
                2.050e+04                 -1.499e+05
                  SqFtLot                  Bathrooms
                6.869e-01                 -3.619e+03
                 Bedrooms                  BldgGrade
               -4.180e+04                  1.047e+05
PropertyTypeSingle Family      PropertyTypeTownhouse
                1.357e+04                 -5.884e+04
  SqFtTotLiving:ZipGroup2    SqFtTotLiving:ZipGroup3
                3.260e+01                  4.178e+01
  SqFtTotLiving:ZipGroup4    SqFtTotLiving:ZipGroup5
                6.934e+01                  2.267e+02

结果模型有四个新项:SqFtTotLiving:ZipGroup2SqFtTotLiving:ZipGroup3等等。

Python中,我们需要使用statsmodels包来训练具有交互效应的线性回归模型。该包设计类似于R,允许使用公式接口定义模型:

model = smf.ols(formula='AdjSalePrice ~ SqFtTotLiving*ZipGroup + SqFtLot + ' +
     'Bathrooms + Bedrooms + BldgGrade + PropertyType', data=house)
results = model.fit()
results.summary()

statsmodels包处理分类变量(例如,ZipGroup[T.1]PropertyType[T.Single Family])和交互项(例如,SqFtTotLiving:ZipGroup[T.1])。

地点和房屋大小似乎存在强烈的交互作用。对于位于最低ZipGroup的房屋,其斜率与主效应SqFtTotLiving的斜率相同,即每*方英尺 118 美元(这是因为R对因子变量使用参照编码;详见“回归中的因子变量”)。对于位于最高ZipGroup的房屋,斜率为主效应加上SqFtTotLiving:ZipGroup5,即 115 美元加 227 美元 = 每*方英尺 342 美元。换句话说,在最昂贵的邮政编码组中增加一个*方英尺几乎能使预测销售价格增加三倍,相比增加一个*方英尺的*均增幅。

使用交互项进行模型选择

在涉及许多变量的问题中,决定应该包括哪些交互项在模型中可能具有挑战性。通常采用几种不同的方法:

  • 在某些问题中,先验知识和直觉可以指导选择包括在模型中的交互项。

  • 逐步选择(详见“模型选择和逐步回归”)可用于筛选各种模型。

  • 惩罚回归可以自动适配大量可能的交互项。

  • 或许最常见的方法是使用树模型及其派生物随机森林梯度提升树。这类模型会自动搜索最优的交互项;详见“树模型”。

回归诊断

在解释建模(即在研究背景下),除了前面提到的度量标准之外(见“评估模型”),还采取各种步骤来评估模型与数据的拟合程度;大多数是基于残差分析的。这些步骤并不直接解决预测精度问题,但在预测设置中提供有用的见解。

异常值

一般而言,极端值,也称为异常值,是与大多数其他观测值远离的观测值。正如需要处理位置和变异估计(见“位置估计”和“变异性估计”)中的异常值一样,异常值可能导致回归模型的问题。在回归中,异常值是其实际y值与预测值差异显著的记录。可以通过检查标准化残差来检测异常值,标准化残差是残差除以残差的标准误差。

没有将异常值与非异常值分开的统计理论。相反,有关观察结果需要与数据主体有多远的(任意的)经验法则,才能称为异常值。例如,在箱线图中,异常值是那些远离箱体边界的数据点(见“百分位数和箱线图”),其中“太远”=“超过四分位距的 1.5 倍”。在回归中,标准化残差是通常用于确定记录是否被分类为异常值的度量标准。标准化残差可以解释为“距离回归线的标准误差数”。

让我们在 R 中为 98105 邮政编码中所有销售的金县房屋销售数据拟合回归:

house_98105 <- house[house$ZipCode == 98105,]
lm_98105 <- lm(AdjSalePrice ~ SqFtTotLiving + SqFtLot + Bathrooms +
                 Bedrooms + BldgGrade, data=house_98105)

Python 中:

house_98105 = house.loc[house['ZipCode'] == 98105, ]

predictors = ['SqFtTotLiving', 'SqFtLot', 'Bathrooms', 'Bedrooms', 'BldgGrade']
outcome = 'AdjSalePrice'

house_outlier = sm.OLS(house_98105[outcome],
                       house_98105[predictors].assign(const=1))
result_98105 = house_outlier.fit()

我们使用 rstandard 函数在 R 中提取标准化残差,并使用 order 函数获取最小残差的索引:

sresid <- rstandard(lm_98105)
idx <- order(sresid)
sresid[idx[1]]
    20429
-4.326732

statsmodels 中,使用 OLSInfluence 分析残差:

influence = OLSInfluence(result_98105)
sresiduals = influence.resid_studentized_internal
sresiduals.idxmin(), sresiduals.min()

从模型中最大的高估是超过回归线四倍标准误差,对应于 757,754 美元的高估。这个异常值对应的原始数据记录在 R 中如下:

house_98105[idx[1], c('AdjSalePrice', 'SqFtTotLiving', 'SqFtLot',
              'Bathrooms', 'Bedrooms', 'BldgGrade')]

AdjSalePrice SqFtTotLiving SqFtLot Bathrooms Bedrooms BldgGrade
         (dbl)         (int)   (int)     (dbl)    (int)     (int)
20429   119748          2900    7276         3        6         7

Python 中:

outlier = house_98105.loc[sresiduals.idxmin(), :]
print('AdjSalePrice', outlier[outcome])
print(outlier[predictors])

在这种情况下,似乎记录存在问题:在该邮政编码中,这种大小的房屋通常售价远高于 119,748 美元。图 4-4 显示了这次销售的法定保证书摘录:显然,这次销售仅涉及部分产权。在这种情况下,异常值对应的是一笔异常的销售,不应包括在回归中。异常值也可能是其他问题的结果,如“手残”数据录入或单位不匹配(例如,报告销售额为千美元而不是美元)。

最大负残差的法定保证书

图 4-4。最大负残差的法定保证书

对于大数据问题,异常值通常不是拟合回归用于预测新数据时的问题。然而,在异常检测中,异常值是核心,其目的就是找到异常值。异常值也可能对应于欺诈案例或意外行为。无论哪种情况,检测异常值都可能是业务上的关键需求。

有影响力的值

一个值,如果去掉会显著改变回归方程,被称为有影响力的观测。在回归中,这样的值不一定与大的残差相关联。例如,考虑图 4-5 中的回归线。实线对应于所有数据的回归,而虚线对应于移除右上角的点的回归。显然,即使与大的异常值(来自完整回归)无关,该数据值对回归的影响仍然巨大。认为这个数据值对回归具有很高的杠杆

除了标准化残差(参见“异常值”),统计学家还开发了几个度量标准,以确定单个记录对回归的影响。一个常见的杠杆度量是帽子值;值大于2 ( P+1 ) / n表示高杠杆的数据值。^(6)

回归中有影响力的数据点的示例

图 4-5 回归中有影响力的数据点的示例

另一个度量标准是Cook 距离,它将影响定义为杠杆和残差大小的组合。一个经验法则是,如果 Cook 距离超过4 / ( n - P-1 ),则观测具有很高的影响力。

影响力图气泡图将标准化残差、帽子值和 Cook 距离结合在一个图中。图 4-6 显示了金县房屋数据的影响力图,并可以通过以下R代码创建:

std_resid <- rstandard(lm_98105)
cooks_D <- cooks.distance(lm_98105)
hat_values <- hatvalues(lm_98105)
plot(subset(hat_values, cooks_D > 0.08), subset(std_resid, cooks_D > 0.08),
     xlab='hat_values', ylab='std_resid',
     cex=10*sqrt(subset(cooks_D, cooks_D > 0.08)), pch=16, col='lightgrey')
points(hat_values, std_resid, cex=10*sqrt(cooks_D))
abline(h=c(-2.5, 2.5), lty=2)

下面是创建类似图形的Python代码:

influence = OLSInfluence(result_98105)
fig, ax = plt.subplots(figsize=(5, 5))
ax.axhline(-2.5, linestyle='--', color='C1')
ax.axhline(2.5, linestyle='--', color='C1')
ax.scatter(influence.hat_matrix_diag, influence.resid_studentized_internal,
           s=1000 * np.sqrt(influence.cooks_distance[0]),
           alpha=0.5)
ax.set_xlabel('hat values')
ax.set_ylabel('studentized residuals')

显然有几个数据点在回归中具有很大的影响。可以使用函数cooks.distance计算 Cook 距离,可以使用hatvalues计算诊断结果。帽子值绘制在 x 轴上,残差绘制在 y 轴上,点的大小与 Cook 距离的值相关。

一个绘图以确定哪些观测具有很高的影响力

图 4-6 一个绘图以确定哪些观测具有很高的影响力;Cook 距离大于 0.08 的点以灰色突出显示

表 4-2 比较了使用完整数据集和去除高度有影响力的数据点(Cook 距离 > 0.08)的回归。

Bathrooms的回归系数发生了相当大的变化。^(7)

表 4-2 比较了使用完整数据和去除有影响力数据后的回归系数

原始数据 去除影响后
(Intercept) –772,550 –647,137
SqFtTotLiving 210 230
SqFtLot 39 33
Bathrooms 2282 –16,132
Bedrooms –26,320 –22,888
BldgGrade 130,000 114,871

为了可靠地拟合可以预测未来数据的回归模型,识别具有影响力的观察结果仅在较小的数据集中有用。对于涉及大量记录的回归,单个观察结果不太可能对拟合方程产生极端影响(尽管回归仍可能存在极大的异常值)。然而,对于异常检测的目的,识别具有影响力的观察结果是非常有用的。

异方差性、非正态性和相关误差

统计学家非常关注残差的分布。事实证明,普通最小二乘法(见“最小二乘法”)在广泛的分布假设下是无偏的,并且在某些情况下是“最优”的估计量。这意味着在大多数问题中,数据科学家不需要过于担心残差的分布。

残差的分布主要与正式统计推断(假设检验和 p 值)的有效性相关,这对主要关注预测准确性的数据科学家影响较小。正态分布的误差表明模型完整;不正态分布的误差表明模型可能存在遗漏。为了使正式推断完全有效,假设残差正态分布、方差相同且独立。数据科学家可能关注的一个领域是对预测值的置信区间的标准计算,这些置信区间基于对残差的假设(见“置信区间与预测区间”)。

异方差性 指的是在预测值范围内残差方差不恒定。换句话说,某些范围内的误差比其他范围大。可视化数据是分析残差的方便方式。

R 中,以下代码绘制了lm_98105回归拟合中预测值的绝对残差与预测值的关系图,详见“异常值”:

df <- data.frame(resid = residuals(lm_98105), pred = predict(lm_98105))
ggplot(df, aes(pred, abs(resid))) + geom_point() + geom_smooth()

图 4-7 展示了得到的图形。使用 geom_smooth,可以轻松叠加绝对残差的*滑曲线。该函数调用 loess 方法(局部估计散点图*滑)生成 x 轴和 y 轴上变量关系的*滑估计散点图(见“散点图*滑处理”)。

Python 中,seaborn包中的regplot函数可以创建类似的图形:

fig, ax = plt.subplots(figsize=(5, 5))
sns.regplot(result_98105.fittedvalues, np.abs(result_98105.resid),
            scatter_kws={'alpha': 0.25}, line_kws={'color': 'C1'},
            lowess=True, ax=ax)
ax.set_xlabel('predicted')
ax.set_ylabel('abs(residual)')

绘制残差绝对值与预测值关系图

图 4-7. 绘制残差绝对值与预测值关系图

显然,残差的方差在更高价值的房屋上倾向于增加,但在更低价值的房屋上也很大。此图表明lm_98105具有异方差的误差。

数据科学家为什么要关心异方差性?

异方差性表明不同预测值范围的预测误差不同,并且可能暗示模型不完整。例如,lm_98105中的异方差性可能表明回归在高价值和低价值房屋中留下了一些未解释的因素。

图 4-8 是对lm_98105回归的标准化残差的直方图。该分布的尾部明显比正态分布长,并且在较大残差方向上呈现轻微偏斜。

房屋数据回归残差的直方图

图 4-8。房屋数据回归残差的直方图

统计学家可能还会检查误差是否独立。这对于随时间或空间收集的数据尤为重要。 德宾-沃森统计量可用于检测涉及时间序列数据的回归中是否存在显著的自相关性。如果回归模型的误差相关,则此信息可以有助于进行短期预测,并应纳入模型中。请参阅 Galit Shmueli 和 Kenneth Lichtendahl(Axelrod Schnall,2018)的用 R 进行实用时间序列预测第 2 版,以了解有关如何将自相关信息构建到用于时间序列数据的回归模型中的更多信息。如果目标是更长期的预测或解释模型,则微观层面上的过多自相关数据可能会分散注意力。在这种情况下,*滑处理或首先收集更少细粒度的数据可能是适当的。

即使回归可能违反了分布假设之一,我们是否应该在意呢?在数据科学中,兴趣主要在于预测准确性,因此可能需要对异方差性进行一些审查。您可能会发现数据中存在一些模型未捕获的信号。然而,仅仅为了验证正式的统计推断(p 值,F 统计量等)而满足分布假设并不是对于数据科学家非常重要的。

散点*滑曲线

回归是关于响应变量和预测变量之间关系建模的。在评估回归模型时,使用散点*滑曲线可以直观地突出两个变量之间的关系是很有用的。

例如,在 图 4-7 中,绝对残差与预测值之间关系的*滑显示出残差的方差取决于残差值。在这种情况下,使用了 loess 函数;loess 的工作原理是通过反复拟合一系列局部回归来实现*滑。虽然 loess 可能是最常用的*滑方法,R 中还提供了其他散点图*滑方法,如超*滑 (supsmu) 和核*滑 (ksmooth)。在 Python 中,我们可以在 scipy (wienersav) 和 statsmodels (kernel_regression) 中找到额外的*滑方法。对于评估回归模型,通常不需要担心这些散点图*滑方法的细节。

局部残差图和非线性

局部残差图 是可视化估计拟合效果和预测变量与结果之间关系的一种方式。局部残差图的基本思想是隔离一个预测变量与响应之间的关系,考虑所有其他预测变量。局部残差可以被视为一个“合成结果”值,将基于单个预测变量的预测与全回归方程的实际残差结合起来。对于预测变量 X i 的局部残差是普通残差加上与 X i 相关的回归项:

Partial residual = Residual + b ^ i X i

其中 b ^ i 是估计的回归系数。在 R 中的 predict 函数有一个选项可以返回单个回归项 b ^ i X i

terms <- predict(lm_98105, type='terms')
partial_resid <- resid(lm_98105) + terms

局部残差图显示了 X i 预测变量在 x 轴上和局部残差在 y 轴上。使用 ggplot2 能够轻松叠加局部残差的*滑曲线:

df <- data.frame(SqFtTotLiving = house_98105[, 'SqFtTotLiving'],
                 Terms = terms[, 'SqFtTotLiving'],
                 PartialResid = partial_resid[, 'SqFtTotLiving'])
ggplot(df, aes(SqFtTotLiving, PartialResid)) +
  geom_point(shape=1) + scale_shape(solid = FALSE) +
  geom_smooth(linetype=2) +
  geom_line(aes(SqFtTotLiving, Terms))

statsmodels 包中有一个名为 sm.graphics.plot_ccpr 的方法,可以创建类似的局部残差图:

sm.graphics.plot_ccpr(result_98105, 'SqFtTotLiving')

RPython 中的图表相差一个常数偏移量。在 R 中,添加一个常数以使项的*均值为零。

结果图显示在图 4-9 中。部分残差是对SqFtTotLiving对销售价格贡献的估计。SqFtTotLiving与销售价格之间的关系显然是非线性的(虚线)。回归线(实线)低估了小于 1,000 *方英尺的房屋的销售价格,并高估了 2,000 到 3,000 *方英尺之间的房屋价格。对于超过 4,000 *方英尺的房屋,数据点太少,无法得出结论。

变量 SqFtTotLiving 的部分残差图

图 4-9. 变量SqFtTotLiving的部分残差图

在这种情况下,这种非线性是有道理的:在小房屋中增加 500 *方英尺会产生比在大房屋中增加 500 *方英尺更大的差异。这表明,与其对SqFtTotLiving使用简单的线性项,不如考虑非线性项(参见“多项式和样条回归”)。

多项式和样条回归

响应与预测变量之间的关系不一定是线性的。对药物剂量的反应通常是非线性的:加倍剂量通常不会导致加倍的反应。产品需求并非是营销投入的线性函数;在某些点上,需求可能会饱和。有许多方法可以扩展回归以捕捉这些非线性效应。

非线性回归

当统计学家谈论非线性回归时,他们指的是无法使用最小二乘法拟合的模型。哪些模型是非线性的?基本上所有响应不能表达为预测变量的线性组合或其某种变换的模型。非线性回归模型更难且计算量更大,因为它们需要数值优化。因此,如果可能的话,通常更倾向于使用线性模型。

多项式

多项式回归涉及在回归方程中包含多项式项。使用多项式回归几乎可以追溯到回归本身的发展,比如 Gergonne 在 1815 年的一篇论文中。例如,响应Y与预测变量X之间的二次回归将采取以下形式:

Y = b 0 + b 1 X + b 2 X 2 + e

多项式回归可以通过R中的poly函数进行拟合。例如,以下示例使用 King County 房屋数据为SqFtTotLiving拟合二次多项式:

lm(AdjSalePrice ~  poly(SqFtTotLiving, 2) + SqFtLot +
                BldgGrade + Bathrooms + Bedrooms,
                    data=house_98105)

Call:
lm(formula = AdjSalePrice ~ poly(SqFtTotLiving, 2) + SqFtLot +
   BldgGrade + Bathrooms + Bedrooms, data = house_98105)

Coefficients:
           (Intercept)  poly(SqFtTotLiving, 2)1  poly(SqFtTotLiving, 2)2
            -402530.47               3271519.49                776934.02
               SqFtLot                BldgGrade                Bathrooms
                 32.56                135717.06                 -1435.12
              Bedrooms
              -9191.94

statsmodels中,我们使用I(SqFtTotLiving**2)将*方项添加到模型定义中:

model_poly = smf.ols(formula='AdjSalePrice ~  SqFtTotLiving + ' +
                '+ I(SqFtTotLiving**2) + ' +
                'SqFtLot + Bathrooms + Bedrooms + BldgGrade', data=house_98105)
result_poly = model_poly.fit()
result_poly.summary()  ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)

1

截距和多项式系数与R不同。这是由于不同的实现。其余系数和预测是等效的。

现在与SqFtTotLiving相关联的系数有两个:一个是线性项,另一个是二次项。

局部残差图(请参阅 “局部残差图和非线性”)表明回归方程与 SqFtTotLiving 相关的曲率。与线性拟合相比,拟合线更接*局部残差的*滑(请参阅 “样条”)而不是图 4-10 中的多项式回归拟合。

statsmodels 实现仅适用于线性项。附带的源代码提供了一个对多项式回归也适用的实现。

变量 SqFtTotLiving 的多项式回归拟合(实线)与*滑曲线(虚线)

图 4-10. 变量 SqFtTotLiving 的多项式回归拟合(实线)与*滑曲线(虚线;关于样条的详细内容请参见下一节)

样条

多项式回归仅捕捉非线性关系中的一定曲率。添加更高阶的项,如四次多项式,通常会导致回归方程中不期望的“波动”。对建模非线性关系的一种替代和通常更优越的方法是使用样条样条提供了一种在固定点之间*滑插值的方法。样条最初由制图员用于绘制*滑曲线,特别是在船舶和飞机建造中。

样条是通过使用权重弯曲薄木片(称为“鸭子”)制作的;请参阅 图 4-11。

样条最初是使用可弯曲的木材和“鸭子”制作的,并用作制图员用于拟合曲线的工具。照片由 Bob Perry 提供。

图 4-11. 样条最初是使用可弯曲的木材和“鸭子”制作的,是制图员用来拟合曲线的工具(照片由 Bob Perry 提供)

样条的技术定义是一系列分段连续的多项式。它们最早在二战期间由罗马尼亚数学家 I. J. Schoenberg 在美国阿伯丁试验场首次开发。多项式片段在预测变量的一系列固定点上*滑连接,称为节点。样条的制定比多项式回归复杂得多;统计软件通常处理样条拟合的详细信息。R 软件包 splines 包括函数 bs,用于在回归模型中创建b-样条(基础样条)项。例如,以下向房屋回归模型添加了一个 b-样条项:

library(splines)
knots <- quantile(house_98105$SqFtTotLiving, p=c(.25, .5, .75))
lm_spline <- lm(AdjSalePrice ~ bs(SqFtTotLiving, knots=knots, degree=3) +
  SqFtLot + Bathrooms + Bedrooms + BldgGrade,  data=house_98105)

需要指定两个参数:多项式的阶数和节点的位置。在本例中,预测变量 SqFtTotLiving 使用三次样条(degree=3)模型。默认情况下,bs 将节点放置在边界;此外,还在下四分位数、中位数四分位数和上四分位数处放置了节点。

statsmodels 的公式接口中,支持类似于 R 的样条用法。这里,我们使用 df 指定 b-样条,其自由度为 df。这将创建 dfdegree = 6 – 3 = 3 个内部节点,其位置与上述 R 代码中计算的方式相同:

formula = 'AdjSalePrice ~ bs(SqFtTotLiving, df=6, degree=3) + ' +
          'SqFtLot + Bathrooms + Bedrooms + BldgGrade'
model_spline = smf.ols(formula=formula, data=house_98105)
result_spline = model_spline.fit()

与线性项相比,样条项的系数没有直接含义。因此,使用视觉显示来揭示样条拟合的性质更为有用。图 4-12(#SplineRegressionPlot)显示了回归的部分残差图。与多项式模型相比,样条模型更接**滑,展示了样条的更大灵活性。在这种情况下,曲线更贴*数据。这是否意味着样条回归是更好的模型?未必:经济上不合理的是,非常小的房屋(小于 1,000 *方英尺)的价值可能比稍大的房屋高。这可能是混杂变量的结果;请参阅 “混杂变量”。

变量 SqFtTotLiving 的样条回归拟合(实线)与*滑曲线(虚线)对比

图 4-12. 变量 SqFtTotLiving 的样条回归拟合(实线)与*滑曲线(虚线)对比

广义可加模型

假设您怀疑响应与预测变量之间存在非线性关系,可以通过先验知识或检查回归诊断来确定。多项式项可能无法灵活捕捉关系,而样条项则需要指定节点。广义可加模型Generalized additive models),简称GAM,是一种灵活的建模技术,可以用于自动拟合样条回归。在 R 中,mgcv 包可用于将 GAM 模型拟合到房屋数据中:

library(mgcv)
lm_gam <- gam(AdjSalePrice ~ s(SqFtTotLiving) + SqFtLot +
                    Bathrooms +  Bedrooms + BldgGrade,
                    data=house_98105)

术语 s(SqFtTotLiving) 告诉 gam 函数查找样条项的“最佳”节点(见 图 4-13)。

变量 SqFtTotLiving 的 GAM 回归拟合(实线)与*滑曲线(虚线)对比

图 4-13. 变量 SqFtTotLiving 的 GAM 回归拟合(实线)与*滑曲线(虚线)对比

Python 中,我们可以使用 pyGAM 包。它提供了回归和分类的方法。在这里,我们使用 LinearGAM 来创建回归模型:

predictors = ['SqFtTotLiving', 'SqFtLot', 'Bathrooms',  'Bedrooms', 'BldgGrade']
outcome = 'AdjSalePrice'
X = house_98105[predictors].values
y = house_98105[outcome]

gam = LinearGAM(s(0, n_splines=12) + l(1) + l(2) + l(3) + l(4))  ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
gam.gridsearch(X, y)

1

n_splines 的默认值为 20. 对于较大的 SqFtTotLiving 值,这会导致过拟合。值为 12 可以得到一个更合理的拟合。

进一步阅读

  • 关于样条模型和 GAM,请参阅《统计学习的要素》第二版(2009),作者 Trevor Hastie、Robert Tibshirani 和 Jerome Friedman,以及其基于 R 的较短版本,《统计学习导论》(2013),作者 Gareth James、Daniela Witten、Trevor Hastie 和 Robert Tibshirani,两者均为 Springer 出版。

  • 欲了解更多关于使用回归模型进行时间序列预测的信息,请参阅 Galit Shmueli 和 Kenneth Lichtendahl 的《使用 R 进行实用时间序列预测》(Axelrod Schnall,2018)。

总结

或许没有其他统计方法像回归那样多年来被广泛使用——建立多个预测变量与一个结果变量之间关系的过程。其基本形式为线性:每个预测变量都有一个系数,描述其与结果的线性关系。更高级的回归形式,如多项式和样条回归,允许关系是非线性的。在经典统计中,重点是找到与观察数据良好匹配的模型来解释或描述某一现象,模型的强度是通过传统的样本内指标来评估。相反,在数据科学中,目标通常是对新数据进行预测,因此使用基于预测精度的样本外数据指标。变量选择方法用于减少维度并创建更紧凑的模型。

^(1) 本章及后续章节内容 © 2020 Datastats, LLC,Peter Bruce,Andrew Bruce 和 Peter Gedeck;经许可使用。

^(2) 在贝叶斯统计中,真实值被假定为具有特定分布的随机变量。在贝叶斯背景下,不是未知参数的估计,而是后验和先验分布。

^(3) model.matrix 中的 -1 参数产生一种独热编码表示(通过删除截距,因此为“-”)。否则,在 R 中默认生成一个具有 P - 1 列的矩阵,其中第一个因子水*作为参考。

^(4) 这看起来不直观,但可以解释为位置作为混杂变量的影响;详见“混杂变量”。

^(5) 金县有 80 个邮政编码,其中几个只有少量销售。ZipGroup是将相似的邮政编码聚类到一个组中,作为因子变量的一种替代方案。详见“具有多个水*的因子变量”。

^(6) 术语帽值源于回归中帽矩阵的概念。多元线性回归可以用公式表示为 Y ^ = H Y 其中 H 是帽矩阵。帽值对应于 H 的对角线。

^(7) Bathrooms 的系数变为负数,这是不直观的。没有考虑到地理位置,而且邮政编码 98105 包含了不同类型的住宅区域。参见“混杂变量”讨论混杂变量。

第五章:分类

数据科学家经常被要求自动化解决业务问题的决策。一封电子邮件是否是钓鱼尝试?客户是否可能流失?网页用户是否可能点击广告?这些都是分类问题,一种监督学习形式,我们首先在已知结果的数据上训练模型,然后将模型应用于结果未知的数据。分类可能是最重要的预测形式:目标是预测记录是 1 还是 0(钓鱼/非钓鱼,点击/不点击,流失/不流失),或者在某些情况下是多个类别之一(例如,Gmail 将您的收件箱分类为“主要”,“社交”,“促销”或“论坛”)。

通常,我们需要的不仅仅是简单的二元分类:我们想知道案例属于某个类别的预测概率。与其让模型简单地分配一个二元分类不同,大多数算法可以返回属于感兴趣类别的概率分数(倾向性)。实际上,在逻辑回归中,默认输出是在对数几率(log-odds)尺度上,必须将其转换为倾向性。在Pythonscikit-learn中,逻辑回归与大多数分类方法一样,提供两种预测方法:predict(返回类别)和predict_proba(返回每个类别的概率)。然后可以使用滑动截止点将倾向性分数转换为决策。一般的方法如下所示:

  1. 为感兴趣的类别建立一个截止概率,高于这个概率则认为记录属于该类别。

  2. 估计(使用任何模型)记录属于感兴趣类别的概率。

  3. 如果该概率高于截止概率,则将新记录分配给感兴趣的类别。

截止点越高,预测为 1 的记录越少,即认为属于感兴趣类别的记录越少。截止点越低,预测为 1 的记录越多。

本章涵盖了几种关键的分类技术和估计倾向性的方法;下一章描述了可以用于分类和数值预测的其他方法。

朴素贝叶斯

朴素贝叶斯算法使用观察到的预测器值的概率,给定一个结果,来估计真正感兴趣的内容:观察到结果Y = i的概率,给定一组预测器值。^(1)

要理解朴素贝叶斯分类,我们可以从想象完全或精确的贝叶斯分类开始。对于要分类的每条记录:

  1. 找到具有相同预测配置文件的所有其他记录(即预测值相同的地方)。

  2. 确定这些记录属于哪些类别,并且哪个类别最普遍(即最可能)。

  3. 分配该类别给新记录。

上述方法相当于找到样本中与要分类的新记录完全相同的所有记录,即所有预测变量值都相同。

注意

标签预测变量必须是标准朴素贝叶斯算法中的分类(因子)变量。有关使用连续变量的两种解决方案,请参阅“数值预测变量”。

为什么精确贝叶斯分类不切实际

当预测变量的数量超过几个时,将没有准确匹配的记录将要分类。考虑一个根据人口统计变量预测投票的模型。即使是一个相当大的样本,也可能不包含一个新记录的准确匹配,该记录是美国中西部高收入的男性拉丁裔,在上次选举中投票,在上次选举中没有投票,有三个女儿和一个儿子,离过婚。而且这只是八个变量,对于大多数分类问题来说是一个小数量。只需添加一个具有五个同等频率类别的新变量,就会将匹配概率降低 5 倍。

朴素解决方案

在朴素贝叶斯解决方案中,我们不再将概率计算限制为与要分类的记录匹配的那些记录。相反,我们使用整个数据集。朴素贝叶斯修改如下:

  1. 对于二元响应 Y = ii = 0 或 1),估算每个预测变量 P ( X j | Y = i ) 的条件概率;这些是观察 Y = i 时预测变量值在记录中的概率。该概率通过训练集中 Y = i 记录中 X[j] 值的比例来估算。

  2. 将这些概率相乘,然后乘以属于 Y = i 的记录的比例。

  3. 为所有类别重复步骤 1 和 2。

  4. 通过将步骤 2 中为第 i 类计算的值除以所有类别的这种值的总和来估算结果 i 的概率。

  5. 将记录分配给具有此预测变量集的最高概率的类别。

该朴素贝叶斯算法也可以陈述为根据一组预测变量 X 1 , , X p 观察结果 Y = i 的概率的方程:

P ( Y = i | X 1 , X 2 , , X p )

下面是使用精确贝叶斯分类计算类别概率的完整公式:

P ( Y = i | X 1 , X 2 , , X p ) = P ( Y = i ) P ( X 1 , , X p | Y = i ) P ( Y = 0 ) P ( X 1 , , X p | Y = 0 ) + P ( Y = 1 ) P ( X 1 , , X p | Y = 1 )

根据条件独立的朴素贝叶斯假设,该方程变为:

P ( Y = i | X 1 , X 2 , , X p ) = P ( Y = i ) P ( X 1 | Y = i ) P ( X p | Y = i ) P ( Y = 0 ) P ( X 1 | Y = 0 ) P ( X p | Y = 0 ) + P ( Y = 1 ) P ( X 1 | Y = 1 ) P ( X p | Y = 1 )

为什么这个公式被称为“朴素”?我们做出了一个简化的假设,即给定观察结果的预测变量向量的精确条件概率可以通过各个条件概率的乘积 P ( X j | Y = i ) 很好地估计。换句话说,在估计 P ( X j | Y = i ) 而不是 P ( X 1 , X 2 , X p | Y = i ) 的情况下,我们假设 X j 对于所有其他预测变量 X k (对于 k j )是独立的。

R中有几个包可以用来估计朴素贝叶斯模型。以下是使用klaR包对贷款支付数据拟合模型的示例:

library(klaR)
naive_model <- NaiveBayes(outcome ~ purpose_ + home_ + emp_len_,
                          data = na.omit(loan_data))
naive_model$table
$purpose_
          var
grouping   credit_card debt_consolidation home_improvement major_purchase
  paid off  0.18759649         0.55215915       0.07150104     0.05359270
  default   0.15151515         0.57571347       0.05981209     0.03727229
          var
grouping      medical      other small_business
  paid off 0.01424728 0.09990737     0.02099599
  default  0.01433549 0.11561025     0.04574126

$home_
          var
grouping    MORTGAGE       OWN      RENT
  paid off 0.4894800 0.0808963 0.4296237
  default  0.4313440 0.0832782 0.4853778

$emp_len_
          var
grouping     < 1 Year   > 1 Year
  paid off 0.03105289 0.96894711
  default  0.04728508 0.95271492

模型的输出是条件概率 P ( X j | Y = i )

Python中,我们可以使用scikit-learn中的sklearn.naive_bayes.MultinomialNB。在拟合模型之前,我们需要将分类特征转换为虚拟变量。

predictors = ['purpose_', 'home_', 'emp_len_']
outcome = 'outcome'
X = pd.get_dummies(loan_data[predictors], prefix='', prefix_sep='')
y = loan_data[outcome]

naive_model = MultinomialNB(alpha=0.01, fit_prior=True)
naive_model.fit(X, y)

可以通过feature_log_prob_属性从拟合模型中导出条件概率。

该模型可用于预测新贷款的结果。我们使用数据集的最后一个值进行测试:

new_loan <- loan_data[147, c('purpose_', 'home_', 'emp_len_')]
row.names(new_loan) <- NULL
new_loan
       	 purpose_    home_  emp_len_
	1 small_business MORTGAGE  > 1 Year

Python中,我们可以这样获取该值:

new_loan = X.loc[146:146, :]

在这种情况下,模型预测一个默认值(R):

predict(naive_model, new_loan)
$class
[1] default
Levels: paid off default

$posterior
      paid off   default
[1,] 0.3463013 0.6536987

正如我们讨论过的,scikit-learn的分类模型有两种方法——predict返回预测的类别,predict_proba返回类别概率:

print('predicted class: ', naive_model.predict(new_loan)[0])

probabilities = pd.DataFrame(naive_model.predict_proba(new_loan),
                             columns=loan_data[outcome].cat.categories)
print('predicted probabilities', probabilities)
--
predicted class:  default
predicted probabilities
    default  paid off
0  0.653696  0.346304

预测还返回一个posterior概率的估计。朴素贝叶斯分类器因产生偏倚估计而闻名。然而,如果目标是根据Y = 1 的概率对记录进行排名,则不需要无偏估计,朴素贝叶斯能够产生良好的结果。

数值预测变量

贝叶斯分类器仅适用于分类预测变量(例如垃圾邮件分类,其中词语、短语、字符等的存在与否是预测任务的核心)。要将朴素贝叶斯应用于数值预测变量,必须采用以下两种方法之一:

  • 将数值预测变量分箱并转换为分类预测变量,然后应用前一节的算法。

  • 使用概率模型(例如正态分布,参见“正态分布”)来估计条件概率P ( X j | Y = i )

警告

当训练数据中缺少预测变量类别时,该算法会将新数据中的结果变量概率分配为零概率,而不是简单地忽略该变量并使用其他变量的信息,如其他方法可能会做的那样。大多数朴素贝叶斯的实现都使用*滑参数(拉普拉斯*滑)来防止这种情况发生。

进一步阅读

  • 统计学习要素,第 2 版,作者特雷弗·哈斯蒂、罗伯特·蒂布什拉尼和杰罗姆·弗里德曼(Springer,2009)。

  • 在《商业分析数据挖掘》(Wiley,2007–2020,包括RPython、Excel 和 JMP 版本)一书中有一整章介绍朴素贝叶斯。该书的作者是加利特·舒梅利、彼得·布鲁斯、尼汀·帕特尔、彼得·格德克、因巴尔·雅哈夫和肯尼斯·利克滕达尔。

判别分析

判别分析是最早的统计分类器;它由 R·A·费舍尔于 1936 年在《遗传学年刊》期刊上发表的一篇文章中介绍。^(2)

虽然判别分析包括几种技术,但最常用的是线性判别分析,即LDA。费舍尔最初提出的方法实际上与 LDA 略有不同,但其原理基本相同。随着更复杂技术的出现,如树模型和逻辑回归,LDA 的应用变得不那么广泛了。

但在某些应用中仍可能会遇到 LDA,并且它与其他更广泛使用的方法存在联系(例如主成分分析;参见“主成分分析”)。

警告

线性判别分析不应与潜在狄利克雷分配(也称为 LDA)混淆。潜在狄利克雷分配用于文本和自然语言处理,与线性判别分析无关。

协方差矩阵

要理解判别分析,首先需要介绍两个或多个变量之间的 协方差 的概念。协方差衡量了变量 xz 之间的关系。用 x ¯z ¯ 表示每个变量的均值(参见 “均值”)。变量 xz 的协方差 s x,z 定义如下:

s x,z = i=1 n (x i -x ¯)(z i -z ¯) n-1

其中 n 为记录数(注意我们除以 n – 1 而不是 n;参见 “自由度,n 还是 n – 1?”)。

与相关系数类似(参见 “相关性”),正值表示正相关,负值表示负相关。但是,相关性的取值范围限制在 –1 到 1 之间,而协方差的尺度依赖于变量 xz 的尺度。协方差矩阵 Σ 对于变量 xz 包括对角线上的各自变量方差,s x 2s z 2 ,以及非对角线上的变量对之间的协方差:

Σ ^ = s x 2 s x,z s z,x s z 2

注意

请记住,标准偏差用于将变量标准化为 z-分数;协方差矩阵用于这一标准化过程的多变量扩展。这称为马哈拉诺比斯距离(参见 “其他距离度量”),与 LDA 函数相关。

费歇尔线性判别

简单起见,让我们专注于一个分类问题,我们希望仅使用两个连续数值变量( x , z )来预测二进制结果y。技术上,判别分析假设预测变量是正态分布的连续变量,但实际上,即使与正态分布的极端偏差,该方法也表现良好,并且适用于二元预测变量。费舍尔线性判别法区分了组间变异与组内变异。具体而言,试图将记录分为两组,线性判别分析(LDA)专注于最大化“组间”*方和SS between(衡量两组之间的变异)相对于“组内”*方和SS within(衡量组内变异)。在这种情况下,两组对应于y = 0 的记录( x 0 , z 0 )y = 1 的记录( x 1 , z 1 )。该方法找到了最大化这两个*方和比例的线性组合w x x + w z z

SS between SS within

组间*方和是两组均值之间的*方距离,组内*方和是每组内均值周围的加权协方差矩阵的扩展。直觉上,通过最大化组间*方和和最小化组内*方和,该方法实现了两组之间的最大分离。

简单示例

MASS包与 W. N. Venables 和 B. D. Ripley(Springer,1994)合著的书籍Modern Applied Statistics with S相关联,为R提供了 LDA 的函数。以下是将此函数应用于一组贷款数据样本,使用两个预测变量borrower_scorepayment_inc_ratio,并输出估计的线性判别器权重:

library(MASS)
loan_lda <- lda(outcome ~ borrower_score + payment_inc_ratio,
                     data=loan3000)
loan_lda$scaling
                          LD1
borrower_score 	   7.17583880
payment_inc_ratio -0.09967559

Python中,我们可以使用LinearDiscriminantAnalysis来自sklearn.discriminant_analysisscalings_属性给出了估计的权重:

loan3000.outcome = loan3000.outcome.astype('category')

predictors = ['borrower_score', 'payment_inc_ratio']
outcome = 'outcome'

X = loan3000[predictors]
y = loan3000[outcome]

loan_lda = LinearDiscriminantAnalysis()
loan_lda.fit(X, y)
pd.DataFrame(loan_lda.scalings_, index=X.columns)

使用判别分析进行特征选择

如果在运行 LDA 之前对预测变量进行了标准化,那么判别权重就是变量重要性的度量,从而提供了一种计算效率高的特征选择方法。

lda函数可以预测“违约”与“已结清”的概率:

pred <- predict(loan_lda)
head(pred$posterior)
   paid off   default
1 0.4464563 0.5535437
2 0.4410466 0.5589534
3 0.7273038 0.2726962
4 0.4937462 0.5062538
5 0.3900475 0.6099525
6 0.5892594 0.4107406

拟合模型的predict_proba方法返回“违约”和“已结清”结果的概率:

pred = pd.DataFrame(loan_lda.predict_proba(loan3000[predictors]),
                    columns=loan_lda.classes_)
pred.head()

预测图有助于说明线性判别分析(LDA)的工作原理。使用predict函数的输出,可以生成关于违约概率的估计的图,如下所示:

center <- 0.5 * (loan_lda$mean[1, ] + loan_lda$mean[2, ])
slope <- -loan_lda$scaling[1] / loan_lda$scaling[2]
intercept <- center[2] - center[1] * slope

ggplot(data=lda_df, aes(x=borrower_score, y=payment_inc_ratio,
                        color=prob_default)) +
  geom_point(alpha=.6) +
  scale_color_gradientn(colors=c('#ca0020', '#f7f7f7', '#0571b0')) +
  scale_x_continuous(expand=c(0,0)) +
  scale_y_continuous(expand=c(0,0), lim=c(0, 20)) +
  geom_abline(slope=slope, intercept=intercept, color='darkgreen')

在 Python 中使用以下代码创建类似的图:

# Use scalings and center of means to determine decision boundary
center = np.mean(loan_lda.means_, axis=0)
slope = - loan_lda.scalings_[0] / loan_lda.scalings_[1]
intercept = center[1] - center[0] * slope

# payment_inc_ratio for borrower_score of 0 and 20
x_0 = (0 - intercept) / slope
x_20 = (20 - intercept) / slope

lda_df = pd.concat([loan3000, pred['default']], axis=1)
lda_df.head()

fig, ax = plt.subplots(figsize=(4, 4))
g = sns.scatterplot(x='borrower_score', y='payment_inc_ratio',
                    hue='default', data=lda_df,
                    palette=sns.diverging_palette(240, 10, n=9, as_cmap=True),
                    ax=ax, legend=False)

ax.set_ylim(0, 20)
ax.set_xlim(0.15, 0.8)
ax.plot((x_0, x_20), (0, 20), linewidth=3)
ax.plot(*loan_lda.means_.transpose())

生成的图如图 5-1 所示。在对角线线左侧的数据点被预测为违约(概率大于 0.5)。

使用两个变量(借款人信用评分和还款占收入比例)的 LDA 预测贷款违约情况。对角线线左侧的数据点被预测为违约(概率大于 0.5)。

图 5-1. 使用两个变量(借款人信用评分和还款占收入比例)的 LDA 预测贷款违约情况

使用判别函数权重,LDA 将预测空间分成两个区域,如实线所示。在两个方向上距离该线更远的预测具有更高的置信度(即,概率远离 0.5)。

判别分析的扩展

更多预测变量:虽然本节的文本和示例仅使用了两个预测变量,但是 LDA 同样适用于超过两个预测变量。唯一的限制因素是记录的数量(估计协方差矩阵需要每个变量足够数量的记录,在数据科学应用中通常不是问题)。

判别分析还有其他变体。最知名的是二次判别分析(QDA)。尽管其名称如此,但 QDA 仍然是一个线性判别函数。主要区别在于,在 LDA 中,协方差矩阵被假定为对应于Y = 0 和Y = 1 的两组相同。在 QDA 中,允许两组的协方差矩阵不同。实际上,在大多数应用中,这种差异并不重要。

进一步阅读

  • Trevor Hastie、Robert Tibshirani 和 Jerome Friedman(Springer,2009)的《统计学习的要素》第二版和 Gareth James、Daniela Witten、Trevor Hastie 和 Robert Tibshirani(Springer,2013)的《统计学习简介》及其简化版都有一个关于判别分析的章节。

  • Galit Shmueli、Peter Bruce、Nitin Patel、Peter Gedeck、Inbal Yahav 和 Kenneth Lichtendahl(Wiley,2007–2020,涵盖RPython、Excel 和 JMP 版本)的《商业分析的数据挖掘》有一个关于判别分析的完整章节。

  • 出于历史兴趣,费舍尔关于这个主题的原始文章,“在分类问题中使用多个测量值”,于 1936 年发表在优生学年鉴(现称为遗传学年鉴)中,可以在网上找到。

逻辑回归

逻辑回归类似于多元线性回归(见第四章),只是结果是二元的。 各种转换被用来将问题转换为可以拟合线性模型的问题。 与 K *邻和朴素贝叶斯不同,类别判别分析与逻辑回归类似,它是一种结构化模型方法,而不是数据中心方法。 由于其快速的计算速度和输出易于快速评分新数据的模型,它是一种流行的方法。

逻辑响应函数和对数几率

逻辑回归的关键要素是 逻辑响应函数对数几率,其中我们将一个概率(在 0-1 范围内)映射到更广泛的范围,适用于线性建模。

第一步是将结果变量看作不是二元标签,而是标签为“1”的概率 p。 幼稚地说,我们可能会诱使将 p 建模为预测变量的线性函数:

p = β 0 + β 1 x 1 + β 2 x 2 + + β q x q

然而,拟合这个模型并不能确保 p 会最终落在 0 和 1 之间,因为概率必须如此。

相反,我们通过将 logistic responseinverse logit 函数应用到预测变量上来对 p 进行建模:

p = 1 1+e -(β 0 +β 1 x 1 +β 2 x 2 ++β q x q )

这个转换确保 p 保持在 0 和 1 之间。

为了将指数表达式从分母中提取出来,我们考虑赔率而不是概率。 赔率,对于所有赌徒来说都很熟悉,是“成功”(1)与“失败”(0)的比率。 就概率而言,赔率是事件发生的概率除以事件不发生的概率。 例如,如果一匹马获胜的概率为 0.5,则“不会获胜”的概率为(1 - 0.5)= 0.5,而赔率为 1.0:

Odds ( Y = 1 ) = p 1-p

我们可以使用逆赔率函数从赔率中获得概率:

p = Odds 1+ Odds

我们将这个与前面显示的 logistic response function 结合起来,得到:

Odds ( Y = 1 ) = e β 0 +β 1 x 1 +β 2 x 2 ++β q x q

最后,两边取对数,我们得到涉及预测变量的线性函数的表达式:

log ( Odds ( Y = 1 ) ) = β 0 + β 1 x 1 + β 2 x 2 + + β q x q

对数几率 函数,也称为 logit 函数,将概率 p( 0 , 1 )映射到任何值( - , + ) —见图 5-2。 转换过程完成;我们使用线性模型来预测概率,然后可以通过应用截断规则将其映射到类标签 —— 任何概率大于截断的记录都被分类为 1。

将概率映射到适合线性模型的比例的 logit 函数的图形

图 5-2. 将概率映射到适合线性模型比例的 logit 函数的图形

逻辑回归和 GLM

逻辑回归公式中的响应是二元结果 1 的对数几率。我们只观察到二元结果,而不是对数几率,因此需要特殊的统计方法来拟合方程。逻辑回归是广义线性模型(GLM)的一个特例,旨在将线性回归扩展到其他设置中。

R中,要拟合逻辑回归,使用glm函数,并将family参数设置为binomial。以下代码将逻辑回归拟合到“K-Nearest Neighbors”中介绍的个人贷款数据中:

logistic_model <- glm(outcome ~ payment_inc_ratio + purpose_ +
                        home_ + emp_len_ + borrower_score,
                      data=loan_data, family='binomial')
logistic_model

Call:  glm(formula = outcome ~ payment_inc_ratio + purpose_ + home_ +
    emp_len_ + borrower_score, family = "binomial", data = loan_data)

Coefficients:
               (Intercept)           payment_inc_ratio
                   1.63809                     0.07974
purpose_debt_consolidation    purpose_home_improvement
                   0.24937                     0.40774
    purpose_major_purchase             purpose_medical
                   0.22963                     0.51048
             purpose_other      purpose_small_business
                   0.62066                     1.21526
                  home_OWN                   home_RENT
                   0.04833                     0.15732
         emp_len_ > 1 Year              borrower_score
                  -0.35673                    -4.61264

Degrees of Freedom: 45341 Total (i.e. Null);  45330 Residual
Null Deviance:	    62860
Residual Deviance: 57510 	AIC: 57540

响应为outcome,如果贷款已还清则为 0,如果贷款违约则为 1。purpose_home_是表示贷款目的和房屋所有权状态的因子变量。与线性回归一样,具有P个级别的因子变量用P – 1 列表示。在R中,默认使用reference编码,并且所有级别都与参考级别进行比较(参见“Factor Variables in Regression”)。这些因子的参考级别分别是credit_cardMORTGAGE。变量borrower_score是一个从 0 到 1 的分数,表示借款人的信用价值(从差到优)。此变量是使用多个其他变量通过K-最*邻算法创建的—参见“KNN as a Feature Engine”。

Python中,我们使用sklearn.linear_model中的LogisticRegression类来执行逻辑回归。参数penaltyC用于通过 L1 或 L2 正则化防止过拟合。默认情况下启用正则化。为了不使用正则化来拟合,我们将C设置为一个非常大的值。参数solver选择所使用的最小化器;liblinear方法是默认方法。

predictors = ['payment_inc_ratio', 'purpose_', 'home_', 'emp_len_',
              'borrower_score']
outcome = 'outcome'
X = pd.get_dummies(loan_data[predictors], prefix='', prefix_sep='',
                   drop_first=True)
y = loan_data[outcome]

logit_reg = LogisticRegression(penalty='l2', C=1e42, solver='liblinear')
logit_reg.fit(X, y)

R不同,scikit-learny的唯一值(paid offdefault)派生类。在内部,这些类按字母顺序排序。由于这与R中使用的因子顺序相反,您会发现系数是反向的。predict方法返回类标签,predict_probalogit_reg.classes_属性中可用的顺序返回概率。

广义线性模型

广义线性模型(GLM)的两个主要组成部分:

  • 概率分布或家族(在逻辑回归的情况下为二项式)

  • 链接函数—即将响应映射到预测变量的转换函数(在逻辑回归的情况下为 logit 函数)

逻辑回归远远是广义线性模型中最常见的形式。数据科学家会遇到其他类型的广义线性模型。有时候会使用对数链接函数而不是 logit;在实践中,使用对数链接函数不太可能在大多数应用中导致非常不同的结果。泊松分布通常用于建模计数数据(例如用户在某段时间内访问网页的次数)。其他家族包括负二项分布和伽马分布,通常用于建模经过时间(例如失效时间)。与逻辑回归相比,使用这些模型的 GLM 的应用更加微妙,需要更多的注意。除非您熟悉并理解这些方法的效用和缺陷,最好避免使用这些模型。

逻辑回归预测值

从逻辑回归中得到的预测值是以对数几率的形式:Y ^ = log ( Odds ( Y = 1 ) ) 。预测概率由逻辑响应函数给出:

p ^ = 1 1+e -Y ^

例如,看一下R中模型logistic_model的预测结果:

pred <- predict(logistic_model)
summary(pred)
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max.
-2.704774 -0.518825 -0.008539  0.002564  0.505061  3.509606

Python中,我们可以将概率转换为数据框,并使用describe方法获取分布的这些特征:

pred = pd.DataFrame(logit_reg.predict_log_proba(X),
                    columns=loan_data[outcome].cat.categories)
pred.describe()

将这些值转换为概率是一个简单的变换:

prob <- 1/(1 + exp(-pred))
> summary(prob)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
0.06269 0.37313 0.49787 0.50000 0.62365 0.97096

可以直接使用scikit-learn中的predict_proba方法获取概率:

pred = pd.DataFrame(logit_reg.predict_proba(X),
                    columns=loan_data[outcome].cat.categories)
pred.describe()

这些数据在 0 到 1 的范围内,尚未声明预测值是违约还是已偿还。我们可以将大于 0.5 的任何值声明为违约。在实际操作中,如果目标是识别罕见类别的成员,则通常适合使用较低的截断值(见“罕见类问题”)。

解释系数和比率比

逻辑回归的一个优点是它能够快速对新数据进行评分,而无需重新计算。另一个优点是与其他分类方法相比,模型的解释相对容易。关键的概念是理解比率比。对于二元因子变量X,比率比最容易理解:

odds ratio = Odds (Y=1|X=1) Odds (Y=1|X=0)

这被解释为X = 1 时Y = 1 的几率与X = 0 时Y = 1 的几率之比。如果比率比为 2,则X = 1 时Y = 1 的几率是X = 0 时的两倍。

为什么要关注比率比而不是概率?我们使用比率是因为逻辑回归中的系数β jX j 的比率比的对数。

举个例子可以更清楚地说明。对于在“逻辑回归与广义线性模型”中拟合的模型,purpose_small_business 的回归系数为 1.21526。这意味着与用来偿还信用卡债务的贷款相比,向小企业贷款减少了违约的几率,相对于被偿还e x p ( 1 . 21526 ) 3 . 4。显然,用于创建或扩展小企业的贷款远比其他类型的贷款风险要高得多。

图 5-3 显示了赔率比大于 1 时赔率比和对数赔率比之间的关系。由于系数在对数尺度上,系数增加 1 会导致赔率比增加e x p ( 1 ) 2 . 72

赔率比和对数赔率比之间的关系

图 5-3. 赔率比和对数赔率比之间的关系

数值变量 X 的赔率比可以类似地解释:它们测量 X 单位变化对赔率比的影响。例如,将支付收入比率从 5 增加到 6 会使贷款违约的几率增加e x p ( 0 . 08244 ) 1 . 09。变量 borrower_score 是借款人信用评分,范围从 0(低)到 1(高)。与最差的借款人相比,最佳借款人违约贷款的几率较小,因为系数e x p ( - 4 . 61264 ) 0 . 01。换句话说,最差信用评分借款人的违约风险是最佳借款人的 100 倍!

线性回归和逻辑回归:相似与不同之处

线性回归和逻辑回归有许多共同点。两者都假设一个参数化的线性形式来关联预测变量与响应变量。探索和找到最佳模型的方法非常相似。对线性模型的扩展,比如使用预测变量的样条变换(参见“样条”),同样适用于逻辑回归设置中。逻辑回归在两个基本方面有所不同:

  • 模型拟合方式(最小二乘法不适用)

  • 模型残差的性质和分析

拟合模型

线性回归使用最小二乘法进行拟合,拟合质量使用 RMSE 和 R-squared 统计量进行评估。在逻辑回归中(不同于线性回归),没有封闭形式的解,必须使用最大似然估计(MLE)拟合模型。最大似然估计是一个过程,试图找到最有可能产生我们所看到数据的模型。在逻辑回归方程中,响应不是 0 或 1,而是响应为 1 的对数几率的估计。MLE 找到的解决方案使得估计的对数几率最能描述观察到的结果。算法的机制涉及一种拟牛顿优化,该优化在评分步骤(费舍尔评分)和根据当前参数更新参数以改进拟合之间迭代。

幸运的是,大多数从业者不需要关注拟合算法的细节,因为这是由软件处理的。大多数数据科学家不需要担心拟合方法,除了理解这是在某些假设下找到一个良好模型的方法。

处理因子变量

在逻辑回归中,因子变量应该像线性回归一样进行编码;参见“回归中的因子变量”。在R和其他软件中,这通常是自动处理的,通常使用引用编码。本章覆盖的所有其他分类方法通常使用独热编码表示(参见“独热编码器”)。在Pythonscikit-learn中,最容易使用独热编码,这意味着在回归中只能使用n – 1个结果虚拟变量。

评估模型

与其他分类方法一样,逻辑回归的评估方式是根据模型分类新数据的准确性(见“评估分类模型”)。与线性回归一样,还有一些额外的标准统计工具可用于检查和改进模型。除了估计的系数外,R报告系数的标准误(SE)、z-值和 p 值:

summary(logistic_model)

Call:
glm(formula = outcome ~ payment_inc_ratio + purpose_ + home_ +
    emp_len_ + borrower_score, family = "binomial", data = loan_data)

Deviance Residuals:
     Min        1Q    Median        3Q       Max
-2.51951  -1.06908  -0.05853   1.07421   2.15528

Coefficients:
                            Estimate Std. Error z value Pr(>|z|)
(Intercept)                 1.638092   0.073708  22.224  < 2e-16 ***
payment_inc_ratio           0.079737   0.002487  32.058  < 2e-16 ***
purpose_debt_consolidation  0.249373   0.027615   9.030  < 2e-16 ***
purpose_home_improvement    0.407743   0.046615   8.747  < 2e-16 ***
purpose_major_purchase      0.229628   0.053683   4.277 1.89e-05 ***
purpose_medical             0.510479   0.086780   5.882 4.04e-09 ***
purpose_other               0.620663   0.039436  15.738  < 2e-16 ***
purpose_small_business      1.215261   0.063320  19.192  < 2e-16 ***
home_OWN                    0.048330   0.038036   1.271    0.204
home_RENT                   0.157320   0.021203   7.420 1.17e-13 ***
emp_len_ > 1 Year          -0.356731   0.052622  -6.779 1.21e-11 ***
borrower_score             -4.612638   0.083558 -55.203  < 2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 62857  on 45341  degrees of freedom
Residual deviance: 57515  on 45330  degrees of freedom
AIC: 57539

Number of Fisher Scoring iterations: 4

statsmodels提供了广义线性模型(GLM)的实现,提供类似详细信息:

y_numbers = [1 if yi == 'default' else 0 for yi in y]
logit_reg_sm = sm.GLM(y_numbers, X.assign(const=1),
                      family=sm.families.Binomial())
logit_result = logit_reg_sm.fit()
logit_result.summary()

P 值的解释与回归中的相同注意事项,应该更多地视为变量重要性的相对指标(见“评估模型”),而不是统计显著性的正式衡量。逻辑回归模型具有二元响应,没有关联的 RMSE 或 R-squared。相反,逻辑回归模型通常使用更一般的分类指标进行评估;参见“评估分类模型”。

许多线性回归的概念也适用于逻辑回归设置(及其他广义线性模型)。例如,您可以使用逐步回归、拟合交互项或包括样条项。与逻辑回归相关的混淆和相关变量的相同问题也适用于此(参见“解释回归方程”)。您可以使用mgcv包在R中拟合广义加性模型(参见“广义加性模型”):

logistic_gam <- gam(outcome ~ s(payment_inc_ratio) + purpose_ +
                    home_ + emp_len_ + s(borrower_score),
                    data=loan_data, family='binomial')

statsmodels的公式接口也支持Python中的这些扩展:

import statsmodels.formula.api as smf
formula = ('outcome ~ bs(payment_inc_ratio, df=4) + purpose_ + ' +
           'home_ + emp_len_ + bs(borrower_score, df=4)')
model = smf.glm(formula=formula, data=loan_data, family=sm.families.Binomial())
results = model.fit()

残差分析

逻辑回归与线性回归不同的一个领域是在残差分析中。与线性回归类似(见图 4-9),在R中计算部分残差是直观的:

terms <- predict(logistic_gam, type='terms')
partial_resid <- resid(logistic_model) + terms
df <- data.frame(payment_inc_ratio = loan_data[, 'payment_inc_ratio'],
                 terms = terms[, 's(payment_inc_ratio)'],
                 partial_resid = partial_resid[, 's(payment_inc_ratio)'])
ggplot(df, aes(x=payment_inc_ratio, y=partial_resid, solid = FALSE)) +
  geom_point(shape=46, alpha=0.4) +
  geom_line(aes(x=payment_inc_ratio, y=terms),
            color='red', alpha=0.5, size=1.5) +
  labs(y='Partial Residual')

结果图显示在图 5-4 中。估计拟合线穿过两组点云。顶部点云对应响应为 1(违约贷款),底部点云对应响应为 0(偿还贷款)。这在逻辑回归的残差中非常典型,因为输出是二进制的。预测被测量为 logit(几率的对数),这总是某个有限值。实际值是绝对的 0 或 1,对应于无限的 logit,无论是正还是负,因此残差(添加到拟合值)永远不会等于 0。因此,在部分残差图中,绘制的点云要么位于拟合线的上方,要么位于下方。尽管在逻辑回归中,部分残差不如回归中有价值,但仍然有助于确认非线性行为和识别高度影响力的记录。

当前主要Python包中没有部分残差的实现。我们提供Python代码以创建配套源代码库中的部分残差图。

逻辑回归的部分残差

图 5-4。逻辑回归的部分残差
警告

某些summary函数的输出可以有效地忽略。离散参数不适用于逻辑回归,而是适用于其他类型的广义线性模型。残差偏差和评分迭代次数与最大似然拟合方法有关;参见“最大似然估计”。

进一步阅读

  • 逻辑回归的标准参考书是 David Hosmer、Stanley Lemeshow 和 Rodney Sturdivant 的《应用逻辑回归》,第 3 版(Wiley, 2013)。

  • Joseph Hilbe 的两本书也很受欢迎:《逻辑回归模型》(非常全面,2017 年)和《逻辑回归实用指南》(简洁,2015 年),均出自 Chapman & Hall/CRC Press。

  • 统计学习的要素第二版(Trevor Hastie, Robert Tibshirani, 和 Jerome Friedman 著,Springer 出版,2009 年)以及它的精简版本 统计学习导论(Gareth James, Daniela Witten, Trevor Hastie, 和 Robert Tibshirani 著,Springer 出版,2013 年)都有关于逻辑回归的章节。

  • 数据挖掘与商业分析(Galit Shmueli, Peter Bruce, Nitin Patel, Peter Gedeck, Inbal Yahav, and Kenneth Lichtendahl 著,Wiley 出版,2007–2020 年,包括 RPython、Excel 和 JMP 版本)一书有一整章讲述逻辑回归。

评估分类模型

在预测建模中,通常会训练多个不同的模型,将每个模型应用于留出样本,并评估其性能。有时,在评估和调整了多个模型之后,如果有足够的数据,会使用第三个未使用过的留出样本来估计所选模型在完全新数据上的表现。不同的学科和从业者也会使用术语 验证测试 来指代留出样本。基本上,评估过程试图了解哪个模型产生了最准确和有用的预测。

衡量分类性能的一种简单方法是计算预测正确的比例,即测量 准确性。准确性只是总误差的一种度量方式:

accuracy = True Positive + True Negative Sample Size

在大多数分类算法中,每个案例被分配一个“估计为 1 的概率”。^(3) 默认决策点或截断通常为 0.50 或 50%。如果概率高于 0.5,则分类为“1”;否则为“0”。另一个默认截断是数据中 1 的普遍概率。

混淆矩阵

分类指标的核心是 混淆矩阵。混淆矩阵是一张表,显示了按响应类型分类的正确和错误预测的数量。RPython 中有几个包可用于计算混淆矩阵,但在二元情况下,通过手动计算一个是简单的。

要说明混淆矩阵,请考虑在*衡数据集上训练的 logistic_gam 模型(参见 Figure 5-4)。按照通常的惯例,Y = 1 对应于感兴趣的事件(例如违约),而 Y = 0 对应于负面(或通常的)事件(例如已偿还)。以下计算了应用于整个(不*衡的)训练集中的 logistic_gam 模型的混淆矩阵在 R 中:

pred <- predict(logistic_gam, newdata=train_set)
pred_y <- as.numeric(pred > 0)
true_y <- as.numeric(train_set$outcome=='default')
true_pos <- (true_y==1) & (pred_y==1)
true_neg <- (true_y==0) & (pred_y==0)
false_pos <- (true_y==0) & (pred_y==1)
false_neg <- (true_y==1) & (pred_y==0)
conf_mat <- matrix(c(sum(true_pos), sum(false_pos),
                     sum(false_neg), sum(true_neg)), 2, 2)
colnames(conf_mat) <- c('Yhat = 1', 'Yhat = 0')
rownames(conf_mat) <- c('Y = 1', 'Y = 0')
conf_mat
      Yhat = 1 Yhat = 0
Y = 1 14295    8376
Y = 0 8052     14619

Python 中:

pred = logit_reg.predict(X)
pred_y = logit_reg.predict(X) == 'default'
true_y = y == 'default'
true_pos = true_y & pred_y
true_neg = ~true_y & ~pred_y
false_pos = ~true_y & pred_y
false_neg = true_y & ~pred_y

conf_mat = pd.DataFrame([[np.sum(true_pos), np.sum(false_neg)],
                         [np.sum(false_pos), np.sum(true_neg)]],
                       index=['Y = default', 'Y = paid off'],
                       columns=['Yhat = default', 'Yhat = paid off'])
conf_mat

预测结果为列,真实结果为行。矩阵的对角线元素显示了正确预测的数量,而非对角线元素显示了错误预测的数量。例如,14,295 笔违约贷款被正确预测为违约,但 8,376 笔违约贷款被错误预测为已偿还。

图 5-5 展示了二元响应 Y 的混淆矩阵与不同指标之间的关系(有关指标的更多信息,请参见“精度、召回率和特异性”)。与贷款数据示例一样,实际响应沿行,预测响应沿列。对角线框(左上角、右下角)显示了预测 Y ^ 正确预测响应的情况。一个未明确指出的重要指标是假阳率(精度的镜像)。当 1 很少见时,假阳率相对于所有预测的阳性可能会很高,导致出现这样的不直观情况,即预测的 1 最有可能是 0。这个问题困扰着广泛应用的医学筛查测试(例如,乳房 X 光检查):由于这种情况的相对罕见,阳性测试结果很可能不意味着患有乳腺癌。这导致公众非常困惑。

图像/confusion-matrix-terms.png

图 5-5. 二元响应的混淆矩阵和各种指标
警告

在这里,我们将实际响应沿行呈现,预测响应沿列呈现,但将其反转也是很常见的。一个显着的例子是 R 中流行的 caret 包。

罕见类别问题

在许多情况下,待预测的类别存在不*衡,其中一个类别比另一个类别更普遍——例如,合法的保险索赔与欺诈性索赔,或者在网站上浏览者与购买者之间的情况。罕见的类别(例如,欺诈性索赔)通常是更感兴趣的类别,并且通常被指定为 1,与更普遍的 0 相对应。在典型情况下,1 是更重要的情况,因为将它们错误分类为 0 比将 0 错误分类为 1 更为昂贵。例如,正确识别欺诈性保险索赔可能会节省数千美元。另一方面,正确识别非欺诈性索赔仅仅节省了您手动进行更仔细审查的成本和精力(如果索赔被标记为“欺诈性”,那么您会这样做)。

在这种情况下,除非类别很容易分开,否则最准确的分类模型可能是将所有东西简单地分类为 0。例如,如果网上商店的浏览者中只有 0.1% 最终购买,那么一个预测每个浏览者都会离开而不购买的模型将达到 99.9% 的准确率。但是,它将是无用的。相反,我们会满意一个总体上不太准确但擅长识别购买者的模型,即使在此过程中它会错误地将一些非购买者分类错误。

精度、召回率和特异性

除了纯准确度之外的指标——更加细致的指标——在评估分类模型时通常使用。其中几个指标在统计学中有着悠久的历史,特别是在生物统计学中,它们用于描述诊断测试的预期性能。精度衡量了预测正面结果的准确性(见图 5-5):

precision = True Positive True Positive + False Positive

召回率,也被称为敏感性,衡量了模型预测正面结果的能力——它正确识别的 1 的比例(见图 5-5)。术语敏感性在生物统计学和医学诊断中经常使用,而召回率在机器学习社区中更为常见。召回率的定义是:

recall = True Positive True Positive + False Negative

另一个使用的度量指标是特异性,它衡量了模型预测负面结果的能力:

specificity = True Negative True Negative + False Positive

我们可以从R中的conf_mat计算出三个指标:

# precision
conf_mat[1, 1] / sum(conf_mat[,1])
# recall
conf_mat[1, 1] / sum(conf_mat[1,])
# specificity
conf_mat[2, 2] / sum(conf_mat[2,])

这里是在Python中计算指标的等效代码:

conf_mat = confusion_matrix(y, logit_reg.predict(X))
print('Precision', conf_mat[0, 0] / sum(conf_mat[:, 0]))
print('Recall', conf_mat[0, 0] / sum(conf_mat[0, :]))
print('Specificity', conf_mat[1, 1] / sum(conf_mat[1, :]))

precision_recall_fscore_support(y, logit_reg.predict(X),
                                labels=['default', 'paid off'])

scikit-learn具有一个自定义方法precision_recall_fscore_support,可以一次计算精度和召回率/特异性。

ROC 曲线

你可以看到,在召回率和特异性之间存在一种权衡。捕捉更多的 1 通常意味着将更多的 0 误分类为 1。理想的分类器应该能够优秀地分类 1,而不会误将更多的 0 分类为 1。

描述这种权衡的度量指标是“接收者操作特征曲线”,通常称为ROC 曲线。ROC 曲线将召回率(敏感性)绘制在 y 轴上,特异性绘制在 x 轴上。^(4) ROC 曲线显示了随着更改分类记录的截断点来确定如何分类记录时,召回率和特异性之间的权衡。特异性在 x 轴上绘制,左侧为 1,右侧为 0。

  • x 轴以特异性为标尺,左侧为 1,右侧为 0

  • x 轴以 1-特异性为标尺,左侧为 0,右侧为 1

无论以何种方式执行,曲线看起来都是相同的。计算 ROC 曲线的过程如下:

  1. 按照被预测为 1 的概率对记录进行排序,从最可能的开始,以最不可能的结束。

  2. 根据排序记录计算累积特异性和召回率。

R中计算 ROC 曲线很简单。以下代码计算了贷款数据的 ROC:

idx <- order(-pred)
recall <- cumsum(true_y[idx] == 1) / sum(true_y == 1)
specificity <- (sum(true_y == 0) - cumsum(true_y[idx] == 0)) / sum(true_y == 0)
roc_df <- data.frame(recall = recall, specificity = specificity)
ggplot(roc_df, aes(x=specificity, y=recall)) +
  geom_line(color='blue') +
  scale_x_reverse(expand=c(0, 0)) +
  scale_y_continuous(expand=c(0, 0)) +
  geom_line(data=data.frame(x=(0:100) / 100), aes(x=x, y=1-x),
            linetype='dotted', color='red')

Python中,我们可以使用scikit-learn函数sklearn.metrics.roc_curve来计算 ROC 曲线所需的信息。你也可以找到类似的R包,例如ROCR

fpr, tpr, thresholds = roc_curve(y, logit_reg.predict_proba(X)[:,0],
                                 pos_label='default')
roc_df = pd.DataFrame({'recall': tpr, 'specificity': 1 - fpr})

ax = roc_df.plot(x='specificity', y='recall', figsize=(4, 4), legend=False)
ax.set_ylim(0, 1)
ax.set_xlim(1, 0)
ax.plot((1, 0), (0, 1))
ax.set_xlabel('specificity')
ax.set_ylabel('recall')

结果显示在图 5-6 中。虚线对角线对应于不比随机机会更好的分类器。一个极其有效的分类器(或者在医学情况下,一个极其有效的诊断测试)将有一个 ROC 曲线,它贴*左上角——能够正确识别大量的 1,而不会误将大量的 0 误分类为 1。对于这个模型,如果我们希望分类器的特异性至少为 50%,那么召回率约为 75%。

贷款数据的 ROC 曲线

图 5-6. 贷款数据的 ROC 曲线

精确率-召回率曲线

除了 ROC 曲线之外,检查精确率-召回率(PR)曲线也是很有启发性的。PR 曲线的计算方式类似,只是数据按概率从低到高排序,并计算累积的精确率和召回率统计数据。PR 曲线在评估高度不*衡结果的数据时尤为有用。

AUC

ROC 曲线是一个有价值的图形工具,但它本身并不构成分类器性能的单一度量标准。然而,ROC 曲线可以用来生成曲线下面积(AUC)指标。AUC 简单地是 ROC 曲线下的总面积。AUC 值越大,分类器越有效。AUC 为 1 表示一个完美的分类器:它正确分类所有的 1,并且不会将任何 0 误分类为 1。

一个完全无效的分类器——对角线——其 AUC 为 0.5。

图 5-7 展示了贷款模型的 ROC 曲线下面积。AUC 的值可以通过在R中进行数值积分来计算:

sum(roc_df$recall[-1] * diff(1 - roc_df$specificity))
    [1] 0.6926172

Python中,我们可以像R中显示的那样计算准确率,或者使用scikit-learn的函数sklearn.metrics.roc_auc_score。您需要提供期望值为 0 或 1:

print(np.sum(roc_df.recall[:-1] * np.diff(1 - roc_df.specificity)))
print(roc_auc_score([1 if yi == 'default' else 0 for yi in y],
                    logit_reg.predict_proba(X)[:, 0]))

模型的 AUC 约为 0.69,对应于一个相对较弱的分类器。

贷款数据的 ROC 曲线下面积

图 5-7. 贷款数据的 ROC 曲线下面积

假阳性率混淆

假阳性/假阴性率经常被与特异性或敏感性混淆或混合(即使在出版物和软件中也是如此!)。有时假阳性率被定义为测试为阳性的真阴性比例。在许多情况下(如网络入侵检测),该术语用于指代被正确分类为阴性的阳性信号的比例。

提升度

使用 AUC 作为评估模型的指标优于简单的准确度,因为它可以评估分类器在整体准确度和识别更重要的 1 的需要之间的*衡。但它并不能完全解决稀有案例问题,您需要将模型的概率截断值降低到 0.5 以下,以避免所有记录都被分类为 0。在这种情况下,要使记录被分类为 1,可能只需要概率为 0.4、0.3 或更低。实际上,我们最终过度识别 1,反映了它们更重要的情况。

调整此截断值将提高捕捉 1 的机会(以误将更多的 0 误分类为 1 为代价)。但最佳截断值是多少?

提升(lift)的概念允许您推迟回答这个问题。而是,您按其预测的概率顺序考虑记录。比如,对于被分类为 1 的前 10%,算法相较于简单随机选择,表现如何?如果在这个前 10%中,您能获得 0.3%的响应,而不是整体随机选择的 0.1%,则称算法在前 10%中具有 3 的提升(也称为增益)。提升图(增益图)在数据范围内量化这一点。它可以逐十分位生成,也可以连续覆盖整个数据范围。

要计算提升图,首先生成显示召回率在 y 轴上和记录总数在 x 轴上的累积增益图提升曲线是累积增益与对应随机选择的对角线的比率。十分位增益图是预测建模中最古老的技术之一,可以追溯到互联网商务之前的日子。它们特别受直邮专业人员欢迎。直邮是一种昂贵的广告方法,如果不加区分地应用,广告商使用预测模型(早期非常简单的模型)来识别可能有最高回报的潜在客户。

提升

有时术语提升(uplift)用于与提升相同的意思。在更严格的设置中,当进行 A/B 测试并且治疗(A 或 B)随后用作预测模型中的预测变量时,采用了替代含义。提升是预测响应改善针对个别案例,使用治疗 A 与治疗 B。这是通过首先使用设置为 A 的预测器对个别案例进行评分,然后再次切换到设置为 B 的预测器来确定的。营销人员和政治竞选顾问使用这种方法确定应向哪些客户或选民使用哪种消息处理。

提升曲线允许您查看为将记录分类为 1 而设置不同概率截断的后果。这可以是确定适当截断水*的中间步骤。例如,税务机构可能只有一定数量的资源可以用于税务审核,并希望将其用于最有可能的税务违规者。考虑到其资源限制,该机构会使用提升图来估计在哪里划定税务审核的界限,哪些税务申报可以选择进行审核,哪些可以留下。

进一步阅读

评估和评估通常在特定模型的背景下进行(例如K最*邻或决策树);三本独立章节处理该主题的书籍是:

  • 数据挖掘,第 3 版,Ian Whitten、Eibe Frank 和 Mark Hall(Morgan Kaufmann,2011 年)。

  • 现代数据科学与 R,Benjamin Baumer、Daniel Kaplan 和 Nicholas Horton(Chapman & Hall/CRC Press,2017 年)。

  • 用于商业分析的数据挖掘,Galit Shmueli、Peter Bruce、Nitin Patel、Peter Gedeck、Inbal Yahav 和 Kenneth Lichtendahl(Wiley,2007-2020 年,包括RPython、Excel 和 JMP 的版本)。

不*衡数据的策略

前一节讨论了使用超出简单准确度的指标评估分类模型,适用于不*衡数据——关注的结果(例如网站上的购买行为、保险欺诈等)很少出现的数据。在本节中,我们将探讨可以改善不*衡数据预测建模性能的额外策略。

欠采样

如果您有足够的数据,就像贷款数据一样,一个解决方案是欠采样(或降采样)主导类别,使要建模的数据在 0 和 1 之间更加*衡。欠采样的基本思想是,主导类别的数据具有许多冗余记录。处理更小、更*衡的数据集可以提升模型性能,并使数据准备、探索和试验模型变得更容易。

数据量有多少才够?这取决于具体的应用场景,但通常来说,对于较少出现的类别,拥有数万条记录就足够了。1 和 0 的区分越明显,需要的数据量就越少。

在“逻辑回归”分析的贷款数据基于一个*衡的训练集:一半的贷款已经偿还,另一半处于违约状态。预测值相似:一半的概率小于 0.5,另一半大于 0.5。在完整数据集中,只有约 19%的贷款处于违约状态,如在 R 中显示的那样:

mean(full_train_set$outcome=='default')
[1] 0.1889455

Python 中:

print('percentage of loans in default: ',
      100 * np.mean(full_train_set.outcome == 'default'))

如果我们使用完整数据集来训练模型会发生什么?让我们看看在 R 中的情况:

full_model <- glm(outcome ~ payment_inc_ratio + purpose_ + home_ +
                            emp_len_+ dti + revol_bal + revol_util,
                 data=full_train_set, family='binomial')
pred <- predict(full_model)
mean(pred > 0)
[1] 0.003942094

并在 Python 中:

predictors = ['payment_inc_ratio', 'purpose_', 'home_', 'emp_len_',
              'dti', 'revol_bal', 'revol_util']
outcome = 'outcome'
X = pd.get_dummies(full_train_set[predictors], prefix='', prefix_sep='',
                   drop_first=True)
y = full_train_set[outcome]

full_model = LogisticRegression(penalty='l2', C=1e42, solver='liblinear')
full_model.fit(X, y)
print('percentage of loans predicted to default: ',
      100 * np.mean(full_model.predict(X) == 'default'))

只有 0.39%的贷款预测会违约,即预期数量的 1/47 以下。^(5) 因为模型使用所有数据进行训练,因此已偿还贷款占据了主导地位,而未偿还贷款数量较少。从直觉上讲,即使是违约贷款,由于预测数据的不可避免的变化,模型也很可能会找到一些类似的未违约贷款。当使用*衡样本时,大约 50%的贷款预测会违约。

过采样和上/下加权

对欠采样方法的一个批评是它丢弃了数据并未充分利用手头所有的信息。如果您拥有一个相对较小的数据集,并且罕见类包含几百或几千条记录,则降低主导类的样本量有可能丢失有用的信息。在这种情况下,您应该通过抽取带替换的额外行来过采样(上采样)罕见类,而不是降低主导情况的样本量。

通过对数据进行加权,您可以达到类似的效果。许多分类算法接受一个权重参数,允许您对数据进行上/下加权。例如,在R中使用glmweight参数将权重向量应用于贷款数据:

wt <- ifelse(full_train_set$outcome=='default',
             1 / mean(full_train_set$outcome == 'default'), 1)
full_model <- glm(outcome ~ payment_inc_ratio + purpose_ + home_ +
                            emp_len_+ dti + revol_bal + revol_util,
                  data=full_train_set, weight=wt, family='quasibinomial')
pred <- predict(full_model)
mean(pred > 0)
[1] 0.5767208

大多数scikit-learn方法允许在fit函数中使用关键字参数sample_weight指定权重:

default_wt = 1 / np.mean(full_train_set.outcome == 'default')
wt = [default_wt if outcome == 'default' else 1
      for outcome in full_train_set.outcome]

full_model = LogisticRegression(penalty="l2", C=1e42, solver='liblinear')
full_model.fit(X, y, sample_weight=wt)
print('percentage of loans predicted to default (weighting): ',
      100 * np.mean(full_model.predict(X) == 'default'))

对于违约贷款,权重设置为1 p ,其中p是违约的概率。未违约贷款的权重为 1。违约贷款和未违约贷款的权重总和大致相等。预测值的均值现在约为 58%,而不是 0.39%。

请注意,加权提供了一个替代方案,既不需要过采样罕见类,也不需要降低主导类的样本量。

调整损失函数

许多分类和回归算法优化特定的标准或损失函数。例如,逻辑回归试图最小化偏差。在文献中,一些人建议修改损失函数以避免罕见类所引起的问题。实际操作中,这很难做到:分类算法可能非常复杂且难以修改。通过加权可以轻松改变损失函数,以便在记录权重较低的情况下减少误差,而不是记录权重较高的情况。

数据生成

通过引入 bootstrap 的一种上采样变体(参见“过采样和上/下加权”),可以通过扰动现有记录来生成新记录。这个想法的直觉是,由于我们只观察到有限的一组实例,算法没有丰富的信息集来构建分类“规则”。通过创建与现有记录相似但不完全相同的新记录,算法有机会学习更健壮的规则集。这个概念在精神上与增强统计模型(如 boosting 和 bagging,参见第六章)类似。

该想法随着SMOTE算法的发布而受到关注,它代表“合成少数过采样技术”。SMOTE 算法找到与正在上采样的记录相似的记录(参见“K 最*邻”),并创建一个合成记录,该记录是原始记录和相邻记录的随机加权*均值,其中权重针对每个预测变量分别生成。创建的合成过采样记录数量取决于为使数据集在结果类别方面大致*衡所需的过采样比例。

R中有几种 SMOTE 的实现。处理不*衡数据的最全面的包是unbalanced。它提供了各种技术,包括一种“Racing”算法来选择最佳方法。然而,SMOTE 算法足够简单,可以直接在R中使用FNN包实现。

Pythonimbalanced-learn实现了与scikit-learn兼容的 API,提供了各种过采样和欠采样方法,并支持将这些技术与 boosting 和 bagging 分类器一起使用。

基于成本的分类

在实践中,准确率和 AUC 是选择分类规则的一种不太精确的方法。通常,可以为假阳性与假阴性分配一个估计成本,并更适合于将这些成本合并以确定在分类 1 和 0 时的最佳截断点。例如,假设新贷款违约的预期成本为C,而已偿还贷款的预期收益为R。那么该贷款的预期收益为:

expected return = P ( Y = 0 ) × R + P ( Y = 1 ) × C

不仅仅将贷款简单标记为违约或已偿还,或者确定违约概率,更有意义的是确定贷款是否有正期望收益。预测的违约概率是一个中间步骤,必须与贷款的总价值结合起来来确定预期利润,这是业务的最终规划指标。例如,可能会选择放弃较小价值的贷款,而选择具有稍高预测违约概率的较大贷款。

探索预测

单一度量,如 AUC,不能评估模型在某种情况下的所有适用性方面。图 5-8 展示了四种不同模型对贷款数据进行拟合时使用的决策规则,仅使用两个预测变量:borrower_scorepayment_inc_ratio。这些模型包括线性判别分析(LDA)、逻辑线性回归、使用广义加性模型(GAM)拟合的逻辑回归以及树模型(参见“树模型”)。在线性判别分析(LDA)和逻辑线性回归中,这些模型在本例中给出了几乎相同的结果。树模型产生了最不规则的规则,具有两个步骤。最后,逻辑回归的 GAM 拟合代表了树模型和线性模型之间的折衷。

四种不同方法分类规则的比较图。Logistic 和 LDA 产生几乎相同且重叠的线性分类器。

图 5-8. 四种不同方法的分类规则比较

在更高维度中或在 GAM 和树模型的情况下,甚至为这些规则生成区域都不容易可视化预测规则。

无论如何,总是有必要对预测值进行探索性分析。

进一步阅读

概要

分类是预测记录属于两个或多个类别中的哪一个的过程,是预测分析的基本工具。贷款是否违约(是或否)?它是否预付?网络访问者是否会点击链接?他们是否会购买东西?保险索赔是否欺诈?在分类问题中,通常一个类别是主要关注的(例如欺诈性保险索赔),在二元分类中,这个类别被指定为 1,而其他更普遍的类别为 0。通常,过程的关键部分是估计倾向分数,即属于感兴趣类别的概率。常见的情况是感兴趣类别相对稀有。在评估分类器时,有许多模型评估指标超出简单的准确性;在罕见类情况下,将所有记录分类为 0 可能会产生高准确性。

^(1) 本章及后续章节版权 © 2020 Datastats, LLC, Peter Bruce, Andrew Bruce, and Peter Gedeck;已获授权使用。

^(2) 令人惊讶的是,第一篇关于统计分类的文章竟然发表在一本致力于优生学的期刊上。确实,统计学早期发展与优生学之间存在令人不安的联系

^(3) 并非所有方法都能提供概率的无偏估计。在大多数情况下,只要方法提供与无偏概率估计产生相同排名的排名即可满足要求;然后截止方法在功能上是等效的。

^(4) ROC 曲线首次用于描述二战期间雷达接收站的性能,其任务是正确识别(分类)反射的雷达信号并警示防御部队迎击来袭飞机。

^(5) 由于实现方式的差异,Python 中的结果略有不同:1%,大约是预期数量的 1/18。

第六章:统计机器学习

最*统计学的进展致力于开发更强大的自动化预测建模技术,包括回归和分类。这些方法,如前一章讨论的那些,是监督方法——它们在已知结果的数据上训练,并学会在新数据中预测结果。它们属于统计机器学习的范畴,与经典统计方法不同,它们是数据驱动的,不寻求对数据施加线性或其他整体结构。例如,K-最*邻方法相当简单:根据相似的记录进行分类。最成功和广泛使用的技术基于应用于决策树集成学习。集成学习的基本思想是使用多个模型来形成预测,而不是仅使用单个模型。决策树是一种灵活和自动的技术,用于学习关于预测变量和结果变量之间关系的规则。事实证明,集成学习与决策树的结合导致了一些性能最佳的现成预测建模技术。

许多统计机器学习技术的发展可以追溯到加州大学伯克利分校的统计学家列奥·布雷曼(见图 6-1)和斯坦福大学的杰瑞·弗里德曼。他们的工作与伯克利和斯坦福的其他研究人员一起,始于 1984 年的树模型的发展。随后在 1990 年代开发的装袋(bagging)和提升(boosting)等集成方法奠定了统计机器学习的基础。

列奥·布雷曼,加州大学伯克利分校的统计学教授,是今天数据科学家工具包中许多技术发展的前沿人物

图 6-1 列奥·布雷曼,加州大学伯克利分校的统计学教授,是今天数据科学家工具包中许多技术发展的前沿人物

机器学习与统计学的对比

在预测建模的背景下,机器学习与统计学的区别是什么?两个学科之间没有明显的分界线。机器学习更倾向于开发能够扩展到大数据的高效算法,以优化预测模型。统计学通常更关注模型的概率理论和基本结构。装袋法和随机森林(参见“装袋法和随机森林”)最初坚定地发展在统计学阵营中。增强(参见“增强”),另一方面,在两个学科中都有发展,但更多地受到机器学习一侧的关注。无论历史如何,增强的前景确保它将作为一种技术在统计学和机器学习中蓬勃发展。

K-最*邻

K-最*邻(KNN)的思想非常简单。^(1) 对于每个待分类或预测的记录:

  1. 找到K个具有相似特征的记录(即类似的预测值)。

  2. 为了分类,找出类似记录中的主要类别,并将该类别分配给新记录。

  3. 对于预测(也称为KNN 回归),找到类似记录中的*均值,并预测新记录的该*均值。

KNN 是较简单的预测/分类技术之一:不需要拟合模型(如回归)。这并不意味着使用 KNN 是一个自动化过程。预测结果取决于特征如何缩放、相似性如何衡量以及K的设置大小。此外,所有预测变量必须为数值形式。我们将通过分类示例说明如何使用 KNN 方法。

小例子:预测贷款违约

表 6-1 显示了来自 LendingClub 的个人贷款数据的几个记录。LendingClub 是 P2P 借贷领域的领先者,投资者汇集资金向个人提供贷款。分析的目标是预测新潜在贷款的结果:偿还还是违约。

表 6-1. LendingClub 贷款数据的几个记录和列

结果 贷款金额 收入 目的 工作年限 房屋所有权
偿还 10000 79100 债务合并 11 按揭 NV
偿还 9600 48000 搬迁 5 按揭 TN
偿还 18800 120036 债务合并 11 按揭 MD
违约 15250 232000 小企业 9 按揭 CA
偿还 17050 35000 债务合并 4 租房 MD
偿还 5500 43000 债务合并 4 租房 KS

考虑一个非常简单的模型,只有两个预测变量:dti,即债务支付(不包括抵押贷款)与收入的比率,以及 payment_inc_ratio,即贷款支付与收入的比率。这两个比率均乘以 100。使用一个小集合的 200 笔贷款,loan200,具有已知的二元结果(违约或未违约,由预测变量 outcome200 指定),并且将 K 设为 20,可以在 R 中如下计算要预测的新贷款 newloan,其 dti=22.5payment_inc_ratio=9:^(2)

newloan <- loan200[1, 2:3, drop=FALSE]
knn_pred <- knn(train=loan200[-1, 2:3], test=newloan, cl=loan200[-1, 1], k=20)
knn_pred == 'paid off'
[1] TRUE

KNN 预测是贷款违约。

虽然 R 有本地的 knn 函数,但贡献的 R 软件包 FNN, for Fast Nearest Neighbor 在大数据方面效果更好,提供了更多的灵活性。

scikit-learn 软件包在 Python 中提供了 KNN 的快速高效实现:

predictors = ['payment_inc_ratio', 'dti']
outcome = 'outcome'

newloan = loan200.loc[0:0, predictors]
X = loan200.loc[1:, predictors]
y = loan200.loc[1:, outcome]

knn = KNeighborsClassifier(n_neighbors=20)
knn.fit(X, y)
knn.predict(newloan)

图 6-2 提供了此示例的视觉展示。要预测的新贷款是中间的交叉点。方块(已还清)和圆圈(违约)表示训练数据。大黑色圆圈显示了最*的 20 个点的边界。在这种情况下,圆圈内有 9 笔违约贷款,相比之下有 11 笔已还清贷款。因此,贷款的预测结果是已还清。请注意,如果仅考虑三个最*的邻居,则预测将是贷款违约。

KNN 预测贷款违约使用两个变量:债务收入比和贷款支付收入比

图 6-2. KNN 预测贷款违约使用两个变量:债务收入比和贷款支付收入比

尽管 KNN 在分类的输出通常是二元决策,例如贷款数据中的违约或已还清,但 KNN 程序通常提供了输出介于 0 到 1 之间的概率(倾向性)。概率基于 K 个最*邻居中某一类的分数。在前述示例中,此违约的概率将被估计为 9 20,即 0.45。使用概率分数可以让您使用除简单多数投票(0.5 的概率)之外的分类规则。这在存在类别不*衡问题时尤为重要;参见 “不*衡数据的策略”。例如,如果目标是识别罕见类别的成员,则截止点通常设置在低于 50% 的概率上。一种常见的方法是将截止点设置为罕见事件的概率。

距离度量

使用距离度量来确定相似性(接*度),这是一个衡量两条记录(x[1], x[2], …, x[p])和(u[1], u[2], …, u[p])之间距离的函数。两个向量之间最流行的距离度量是欧氏距离。要测量两个向量之间的欧氏距离,将一个向量减去另一个向量,*方差值,求和,然后取*方根:

(x 1 -u 1 ) 2 + (x 2 -u 2 ) 2 + + (x p -u p ) 2 .

另一个常见的数值数据距离度量是曼哈顿距离

| x 1 u 1 | + | x 2 u 2 | + + | x p u p |

欧氏距离对应于两点之间的直线距离(例如,鸟儿飞行的距离)。曼哈顿距离是两点在单一方向上的距离(例如,沿着矩形城市街区行驶)。因此,如果相似性定义为点对点的旅行时间,则曼哈顿距离是一个有用的*似值。

在测量两个向量之间的距离时,具有相对较大规模的变量(特征)将主导测量结果。例如,在贷款数据中,距离几乎完全取决于收入和贷款金额这两个变量,这些变量的度量单位是数十或数百万。比率变量与之相比几乎不计。我们通过标准化数据来解决这个问题;参见“标准化(归一化,z 分数)”。

其他距离度量

还有许多其他衡量向量之间距离的度量标准。对于数值数据,马氏距离很有吸引力,因为它考虑了两个变量之间的相关性。这是有用的,因为如果两个变量高度相关,马氏距离本质上将这些变量视为距离上的单一变量。欧氏距离和曼哈顿距离不考虑相关性,实际上更加关注支持这些特征的属性。马氏距离是主成分之间的欧氏距离(参见“主成分分析”)。使用马氏距离的缺点是增加了计算工作量和复杂性;它是通过协方差矩阵计算的(参见“协方差矩阵”)。

独热编码器

表 6-1 中的贷款数据包含几个因子(字符串)变量。大多数统计和机器学习模型要求将这种类型的变量转换为一系列二进制虚拟变量,传达相同的信息,如表 6-2 所示。与仅仅表示家庭占有状态为“有抵押贷款”,“无抵押贷款”,“租房”或“其他”的单一变量不同,我们最终得到四个二进制变量。第一个变量将是“有抵押贷款—是/否”,第二个变量将是“无抵押贷款—是/否”,依此类推。因此,这一个预测因子,家庭占有状态,产生一个向量,其中有一个 1 和三个 0,可用于统计和机器学习算法中。短语独热编码来源于数字电路术语,用来描述电路设置中只允许一个位为正(热)的情况。

表 6-2. 将表 6-1 中的家庭占有因子数据表示为数字虚拟变量

有抵押贷款 无抵押贷款 其他 租房
1 0 0 0
1 0 0 0
1 0 0 0
1 0 0 0
0 0 0 1
0 0 0 1
注意

在线性和逻辑回归中,独热编码会导致多重共线性问题;参见“多重共线性”。在这种情况下,会省略一个虚拟变量(其值可以从其他值推断出)。但在 KNN 和本书中讨论的其他方法中,这不是一个问题。

标准化(归一化,z-分数)

在测量中,我们通常更关心的是“与*均值相比有多大差异”,而不是“具体数值是多少”。标准化,也称为归一化,通过减去均值并除以标准差,将所有变量放在类似的尺度上;这样做可以确保一个变量不会仅仅因为其原始测量的尺度而对模型产生过大影响:

z = x-x ¯ s

这种转换的结果通常被称为z 分数。然后,测量结果以“距离*均值的标准差数”表示。

警告

在这个统计上下文中的归一化数据库归一化不要混淆,后者是消除冗余数据并验证数据依赖性。

对于 KNN 和其他一些过程(例如主成分分析和聚类),在应用程序之前考虑对数据进行标准化是至关重要的。为了说明这个想法,KNN 应用于贷款数据,使用dtipayment_inc_ratio(参见“一个小例子:预测贷款违约”)以及另外两个变量:revol_bal,申请人的总可循环信贷额度(以美元计),和revol_util,使用的信贷百分比。待预测的新记录如下所示:

newloan
  payment_inc_ratio dti revol_bal revol_util
1            2.3932   1      1687        9.4

revol_bal的大小(以美元计)比其他变量大得多。knn函数将最*邻的索引作为属性nn.index返回,可以用来显示loan_df中最接*的五行:

loan_df <- model.matrix(~ -1 + payment_inc_ratio + dti + revol_bal +
                          revol_util, data=loan_data)
newloan <- loan_df[1, , drop=FALSE]
loan_df <- loan_df[-1,]
outcome <- loan_data[-1, 1]
knn_pred <- knn(train=loan_df, test=newloan, cl=outcome, k=5)
loan_df[attr(knn_pred, "nn.index"),]

        payment_inc_ratio  dti revol_bal revol_util
35537             1.47212 1.46      1686       10.0
33652             3.38178 6.37      1688        8.4
25864             2.36303 1.39      1691        3.5
42954             1.28160 7.14      1684        3.9
43600             4.12244 8.98      1684        7.2

在模型拟合后,我们可以使用scikit-learnkneighbors方法来识别训练集中与loan_df中最接*的五行:

predictors = ['payment_inc_ratio', 'dti', 'revol_bal', 'revol_util']
outcome = 'outcome'

newloan = loan_data.loc[0:0, predictors]
X = loan_data.loc[1:, predictors]
y = loan_data.loc[1:, outcome]

knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X, y)

nbrs = knn.kneighbors(newloan)
X.iloc[nbrs[1][0], :]

这些邻居中revol_bal的值非常接*新记录中的值,但其他预测变量却大相径庭,基本上不起作用。

将此与使用R函数scale对标准化数据应用 KNN 进行比较,该函数计算每个变量的z-分数:

loan_df <- model.matrix(~ -1 + payment_inc_ratio + dti + revol_bal +
                          revol_util, data=loan_data)
loan_std <- scale(loan_df)
newloan_std <- loan_std[1, , drop=FALSE]
loan_std <- loan_std[-1,]
loan_df <- loan_df[-1,]  ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
outcome <- loan_data[-1, 1]
knn_pred <- knn(train=loan_std, test=newloan_std, cl=outcome, k=5)
loan_df[attr(knn_pred, "nn.index"),]
        payment_inc_ratio   dti  revol_bal  revol_util
2081            2.61091    1.03       1218         9.7
1439            2.34343    0.51        278         9.9
30216           2.71200    1.34       1075         8.5
28543           2.39760    0.74       2917         7.4
44738           2.34309    1.37        488         7.2

1

我们还需要从loan_df中删除第一行,以便行号相互对应。

首先使用预测变量训练sklearn.preprocessing.StandardScaler方法,然后在训练 KNN 模型之前对数据集进行转换:

newloan = loan_data.loc[0:0, predictors]
X = loan_data.loc[1:, predictors]
y = loan_data.loc[1:, outcome]

scaler = preprocessing.StandardScaler()
scaler.fit(X * 1.0)

X_std = scaler.transform(X * 1.0)
newloan_std = scaler.transform(newloan * 1.0)

knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_std, y)

nbrs = knn.kneighbors(newloan_std)
X.iloc[nbrs[1][0], :]

最接*的五个邻居在所有变量上都更相似,从而提供了更合理的结果。请注意,结果显示在原始比例上,但是 KNN 是应用于经过缩放的数据和新贷款预测的。

提示

使用z-分数只是重新缩放变量的一种方法。可以使用更健壮的位置估计,例如中位数,而不是均值。同样,可以使用不同的尺度估计,例如四分位距,而不是标准偏差。有时,变量被“压缩”到 0–1 范围内也很重要。还要意识到,将每个变量缩放为单位方差在某种程度上是任意的。这意味着每个变量在预测能力中被认为具有相同的重要性。如果您有主观知识表明某些变量比其他变量更重要,那么可以将它们放大。例如,对于贷款数据,可以合理地期望支付收入比非常重要。

注意

标准化(标准化)不会改变数据的分布形状;如果数据不是正态分布的,则不会使其成为正态分布(参见“正态分布”)。

选择 K

选择K对 KNN 的性能非常重要。最简单的选择是设置K = 1,即 1-最*邻分类器。预测直观:它基于找到训练集中与待预测的新记录最相似的数据记录。设置K = 1很少是最佳选择;使用K > 1-最*邻几乎总能获得更好的性能。

一般而言,如果K太低,我们可能会过度拟合:包括数据中的噪音。较高的K值提供了*滑处理,从而降低了在训练数据中过度拟合的风险。另一方面,如果K太高,我们可能会过度*滑数据,错过 KNN 捕获数据中的局部结构的能力,这是其主要优势之一。

最好*衡过拟合和过度*滑之间的K通常由准确度指标来确定,特别是在使用留出或验证数据进行准确度评估时。关于最佳K没有通用规则——它在很大程度上取决于数据的性质。对于结构化程度高且噪音少的数据,较小的K值效果最佳。从信号处理社区借来一个术语,这种类型的数据有时被称为具有高信噪比SNR)的数据。具有典型高 SNR 的数据示例包括手写和语音识别数据集。对于噪音较多、结构较少的数据(信噪比低的数据),例如贷款数据,适合使用较大的K值。通常,K值落在 1 到 20 的范围内。通常选择奇数以避免*局。

偏差-方差权衡

过度*滑和过度拟合之间的张力是偏差-方差权衡的一个例子,这是统计模型拟合中普遍存在的问题。方差是指由于选择训练数据而产生的建模误差;也就是说,如果选择不同的训练数据集,得到的模型会不同。偏差是指由于未能正确识别出真实世界情况而产生的建模误差;如果简单地添加更多的训练数据,这种误差不会消失。当一个灵活的模型过度拟合时,方差会增加。您可以通过使用更简单的模型来减少这种情况,但由于失去了对真实情况建模的灵活性,偏差可能会增加。处理这种权衡的一般方法是通过交叉验证。详细信息请参见“交叉验证”。

作为特征引擎的 KNN

KNN 之所以受欢迎,是因为其简单直观的特性。就性能而言,KNN 本身通常无法与更复杂的分类技术竞争。然而,在实际模型拟合中,KNN 可以与其他分类技术一起以分阶段的方式使用,以添加“局部知识”:

  1. KNN 对数据进行运算,对于每条记录,都会得出一个分类(或类的准概率)。

  2. 结果被添加为记录的新特征,然后再对数据运行另一种分类方法。因此,原始预测变量被使用了两次。

起初,您可能会想知道,由于它两次使用了一些预测变量,这个过程是否会导致多重共线性问题(参见“多重共线性”)。这不是一个问题,因为被纳入第二阶段模型的信息是高度局部的,仅来自几个附*的记录,因此是额外信息而不是冗余信息。

注意

您可以将 KNN 的这种分阶段使用视为集成学习的一种形式,其中多个预测建模方法与彼此结合使用。它也可以被视为一种特征工程形式,其目的是提取具有预测能力的特征(预测变量)。通常这涉及对数据的一些手动审查;KNN 提供了一种相对自动化的方法来实现这一点。

例如,考虑金县房屋数据。在定价待售住宅时,房地产经纪人将根据最*售出的类似房屋——称为“comps”——确定价格。本质上,房地产经纪人正在执行 KNN 的手动版本:通过查看类似房屋的销售价格,他们可以估计一处住宅的销售价格。我们可以为统计模型创建一个新的特征,以模仿房地产专业人员通过 KNN 应用到最*销售中的做法。预测值是销售价格,现有的预测变量可以包括位置、总面积、建筑类型、土地面积以及卧室和浴室数量。我们通过 KNN 添加的新预测变量(特征)是每条记录的 KNN 预测器(类似于房地产经纪人的 comps)。由于我们在预测数值值,所以使用 K 最*邻居的*均值,而不是多数投票(称为KNN 回归)。

同样地,对于贷款数据,我们可以创建代表贷款流程不同方面的特征。例如,以下R代码将构建一个代表借款人信用价值的特征:

borrow_df <- model.matrix(~ -1 + dti + revol_bal + revol_util + open_acc +
                            delinq_2yrs_zero + pub_rec_zero, data=loan_data)
borrow_knn <- knn(borrow_df, test=borrow_df, cl=loan_data[, 'outcome'],
                  prob=TRUE, k=20)
prob <- attr(borrow_knn, "prob")
borrow_feature <- ifelse(borrow_knn == 'default', prob, 1 - prob)
summary(borrow_feature)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
  0.000   0.400   0.500   0.501   0.600   0.950

使用scikit-learn,我们使用训练模型的predict_proba方法来获取概率:

predictors = ['dti', 'revol_bal', 'revol_util', 'open_acc',
              'delinq_2yrs_zero', 'pub_rec_zero']
outcome = 'outcome'

X = loan_data[predictors]
y = loan_data[outcome]

knn = KNeighborsClassifier(n_neighbors=20)
knn.fit(X, y)

loan_data['borrower_score'] = knn.predict_proba(X)[:, 1]
loan_data['borrower_score'].describe()

结果是基于借款人的信用历史预测借款人违约可能性的特征。

树模型

树模型,也称为分类和回归树CART),^(3) 决策树或简称,是一种由 Leo Breiman 等人在 1984 年最初开发的有效和流行的分类(和回归)方法。树模型及其更强大的后继者随机森林提升树(见“Bagging 和随机森林”和“提升”)构成了数据科学中用于回归和分类的最广泛使用和强大的预测建模工具的基础。

树模型是一组易于理解和实现的“如果-那么-否则”规则。与线性和逻辑回归相比,树具有发现数据中复杂交互作用的隐藏模式的能力。然而,与 KNN 或朴素贝叶斯不同,简单的树模型可以用易于解释的预测者关系来表达。

运筹学中的决策树

决策树一词在决策科学和运筹学中有着不同(也更老的)含义,它指的是一种人类决策分析过程。在这个含义下,决策点、可能的结果以及它们的估计概率被列在一个分支图表中,选择具有最大期望值的决策路径。

一个简单的例子

R 中适配树模型的两个主要包是 rparttree。使用 rpart 包,将模型拟合到 3,000 条贷款数据记录样本,使用变量 payment_inc_ratioborrower_score(见“K-最*邻”以获取数据描述):

library(rpart)
loan_tree <- rpart(outcome ~ borrower_score + payment_inc_ratio,
                   data=loan3000, control=rpart.control(cp=0.005))
plot(loan_tree, uniform=TRUE, margin=0.05)
text(loan_tree)

sklearn.tree.DecisionTreeClassifier 提供了一个决策树的实现。dmba 包提供了一个方便的函数,在 Jupyter 笔记本内创建可视化:

predictors = ['borrower_score', 'payment_inc_ratio']
outcome = 'outcome'

X = loan3000[predictors]
y = loan3000[outcome]

loan_tree = DecisionTreeClassifier(random_state=1, criterion='entropy',
                                   min_impurity_decrease=0.003)
loan_tree.fit(X, y)
plotDecisionTree(loan_tree, feature_names=predictors,
                 class_names=loan_tree.classes_)

结果树显示在 图 6-3 中。由于不同的实现,您会发现 RPython 的结果并不相同;这是预期的。这些分类规则是通过遍历层次树来确定的,从根开始,如果节点为真则向左移动,否则向右移动,直到达到叶子节点。

通常,树被倒置绘制,所以根在顶部,叶在底部。例如,如果我们获得一个 borrower_score 为 0.6 和 payment_inc_ratio 为 8.0 的贷款,我们将在最左边的叶子处结束,并预测该贷款将被偿还。

一个简单的树模型拟合到贷款数据的规则。

图 6-3. 一个简单的树模型拟合到贷款数据的规则

R 中也很容易产生树的漂亮打印版本:

loan_tree
n= 3000

node), split, n, loss, yval, (yprob)
    * denotes terminal node

1) root 3000 1445 paid off (0.5183333 0.4816667)
  2) borrower_score>=0.575 878  261 paid off (0.7027335 0.2972665) *
  3) borrower_score< 0.575 2122  938 default (0.4420358 0.5579642)
    6) borrower_score>=0.375 1639  802 default (0.4893228 0.5106772)
      12) payment_inc_ratio< 10.42265 1157  547 paid off (0.5272256 0.4727744)
        24) payment_inc_ratio< 4.42601 334  139 paid off (0.5838323 0.4161677) *
        25) payment_inc_ratio>=4.42601 823  408 paid off (0.5042527 0.4957473)
          50) borrower_score>=0.475 418  190 paid off (0.5454545 0.4545455) *
          51) borrower_score< 0.475 405  187 default (0.4617284 0.5382716) *
      13) payment_inc_ratio>=10.42265 482  192 default (0.3983402 0.6016598) *
    7) borrower_score< 0.375 483  136 default (0.2815735 0.7184265) *

树的深度由缩进显示。每个节点对应于由该分区中普遍结果确定的临时分类。 “损失”是由分区中的临时分类产生的错误分类数。例如,在节点 2 中,总共有 878 条记录中的 261 条错误分类。括号中的值分别对应于已偿还或违约的记录比例。例如,在预测违约的节点 13 中,超过 60% 的记录是违约贷款。

scikit-learn 文档描述了如何创建决策树模型的文本表示。我们在我们的 dmba 包中包含了一个方便的函数:

print(textDecisionTree(loan_tree))
--
node=0 test node: go to node 1 if 0 <= 0.5750000178813934 else to node 6
  node=1 test node: go to node 2 if 0 <= 0.32500000298023224 else to node 3
    node=2 leaf node: [[0.785, 0.215]]
    node=3 test node: go to node 4 if 1 <= 10.42264986038208 else to node 5
      node=4 leaf node: [[0.488, 0.512]]
      node=5 leaf node: [[0.613, 0.387]]
  node=6 test node: go to node 7 if 1 <= 9.19082498550415 else to node 10
    node=7 test node: go to node 8 if 0 <= 0.7249999940395355 else to node 9
      node=8 leaf node: [[0.247, 0.753]]
      node=9 leaf node: [[0.073, 0.927]]
    node=10 leaf node: [[0.457, 0.543]]

递归分区算法

构建决策树的算法称为递归分区,非常直观和简单。数据使用能够最好地将数据分隔成相对同质分区的预测器值反复分区。图 6-4 显示了图 6-3 中树创建的分区。第一个规则由规则 1 描述,即borrower_score >= 0.575并分割绘图的右侧部分。第二个规则是borrower_score < 0.375并分割左侧部分。

适用于贷款数据的简单树模型的前五条规则。

图 6-4. 适用于贷款数据的简单树模型的前三条规则

假设我们有响应变量Y和一组P个预测变量X[j],对于j = 1 , , P的分区A,递归分区将找到将A分割为两个子分区的最佳方法:

  1. 对于每个预测变量X[j]

    1. 对于每个X[j]的值s[j]

      1. 将具有X[j]值小于s[j]的记录在A中拆分为一个分区,将其余X[j]值大于或等于s[j]的记录拆分为另一个分区。

      2. 测量A每个子分区内类的同质性。

    2. 选择产生类内最大同质性的s[j]的值。

  2. 选择变量X[j]和分割值s[j],以产生类内最大同质性。

现在进入递归部分:

  1. 用整个数据集初始化A

  2. 应用分区算法将A分割成两个子分区,A[1]A[2]

  3. 在子分区A[1]A[2]上重复步骤 2。

  4. 当不能再进行足够改进分区同质性的分区时,算法终止。

最终结果是数据的分区,如图 6-4 中所示,除了在P维度中,每个分区根据该分区中响应的多数投票预测为 0 或 1 的结果。

注意

除了进行二元 0/1 预测外,树模型还可以根据分区中 0 和 1 的数量生成概率估计。估计值简单地是分区中 0 或 1 的总和除以分区中的观察次数:

Prob ( Y = 1 ) = Numberof1sinthepartition Sizeofthepartition

估计值Prob ( Y = 1 )然后可以转换为二进制决策;例如,如果 Prob(Y = 1) > 0.5,则将估计值设置为 1。

测量同质性或不纯度

树模型递归地创建预测Y = 0 或Y = 1 的分区(记录集)A。从前面的算法中可以看出,我们需要一种方法来衡量分区内的同质性,也称为类纯度。或者等效地,我们需要衡量分区的不纯度。预测的准确性是分区内错误分类的记录比例p,其范围从 0(完美)到 0.5(纯粹的随机猜测)。

结果表明,准确率不是衡量纯度的好指标。相反,衡量纯度的两个常见方法是基尼不纯度信息熵。虽然这些(以及其他)纯度度量适用于具有两个以上类别的分类问题,但我们关注二元情况。一组记录A的基尼不纯度为:

I ( A ) = p ( 1 - p )

熵测量由以下公式给出:

I ( A ) = - p log 2 ( p ) - ( 1 - p ) log 2 ( 1 - p )

图 6-5 显示基尼不纯度(重新缩放)和熵测量类似,熵对中等和高准确率得分给出较高的不纯度评分。

基尼不纯度和熵测量。

图 6-5。基尼不纯度和熵测量

基尼系数

不要将基尼不纯度与基尼系数混淆。它们代表类似的概念,但基尼系数仅适用于二元分类问题,并且与 AUC 度量相关(参见“AUC”)。

不纯度度量在前面描述的分裂算法中使用。对于数据的每个拟议分区,都会测量每个分裂产生的分区的不纯度。然后计算加权*均值,并选择(在每个阶段)产生最低加权*均值的分区。

阻止树继续生长

随着树变得越来越大,分裂规则变得更加详细,树逐渐从识别数据中真实可靠的关系的“大”规则转变为反映了只有噪音的“微小”规则。完全生长的树导致完全纯净的叶子,因此在对其进行训练的数据上对数据进行分类的准确率为 100%。当然,这种准确率是虚假的——我们已经过度拟合(见“偏差-方差权衡”)数据,拟合了训练数据中的噪音,而不是我们想要在新数据中识别的信号。

我们需要一种方法来确定何时停止树的生长,以便在一个阶段上对新数据进行泛化。在RPython中,有各种方法来停止分裂:

  • 避免分裂分区,如果结果子分区太小,或者终端叶子太小。在rpartR)中,这些约束分别由参数minsplitminbucket控制,默认值分别为207。在PythonDecisionTreeClassifier中,我们可以使用参数min_samples_split(默认为2)和min_samples_leaf(默认为1)来控制这一点。

  • 如果新的分区不能“显著”减少不纯度,则不要分割分区。在rpart中,这由复杂度参数 cp 控制,它衡量树的复杂程度——越复杂,cp值越大。在实践中,cp用于通过对树中额外复杂度(分割)附加惩罚来限制树的增长。DecisionTreeClassifierPython)具有参数min_impurity_decrease,它限制基于加权不纯度减少值的分割。在这里,较小的值将导致更复杂的树。

这些方法涉及任意规则,对探索性工作可能有用,但我们无法轻易确定最优值(即最大化使用新数据预测准确性的值)。我们需要结合交叉验证和系统地更改模型参数或通过修剪修改树。

R中控制树的复杂度。

使用复杂度参数cp,我们可以估计什么样的树在新数据中表现最佳。如果cp太小,则树将过度拟合数据,适应噪声而不是信号。另一方面,如果cp太大,则树将过于简单且具有较少的预测能力。在rpart中,默认值为 0.01,尽管对于更大的数据集,你可能会发现这个值太大了。在前面的例子中,cp设置为0.005,因为默认值导致树只有一个分割。在探索性分析中,仅需尝试几个值即可。

确定最佳的cp值是偏差-方差权衡的一个实例。估计一个好的cp值的最常见方法是通过交叉验证(参见“交叉验证”):

  1. 将数据分割为训练集和验证(留置)集。

  2. 使用训练数据生长树。

  3. 逐步修剪树,在每一步中使用训练数据记录cp

  4. 注意对应于验证数据上最小错误(损失)的cp值。

  5. 将数据重新分割为训练和验证集,并重复生成、修剪和记录cp的过程。

  6. 一次又一次地执行此操作,并计算反映每棵树最小错误的cp的*均值。

  7. 回到原始数据或未来数据,并以此最佳的cp值停止生成树。

rpart中,可以使用参数cptable生成cp值及其相关的交叉验证错误(在R中为xerror)的表格,从中可以确定具有最低交叉验证错误的cp值。

Python中控制树的复杂度。

scikit-learn 的决策树实现中既没有复杂度参数,也没有剪枝功能。解决方法是对不同参数值的组合进行网格搜索。例如,我们可以将 max_depth 变化范围设置为 5 到 30,将 min_samples_split 设置为 20 到 100。scikit-learn 中的 GridSearchCV 方法是通过交叉验证组合进行穷举搜索的便捷方式。然后,通过交叉验证的模型性能选择最优参数集。

预测连续值

使用树进行连续值预测(也称为回归)遵循相同的逻辑和步骤,只是在每个子分区中,不纯度是通过与*均值的*方偏差(*方误差)来衡量的,预测性能是通过每个分区中的均方根误差(RMSE)(参见“评估模型”)来判断的。

scikit-learnsklearn.tree.DecisionTreeRegressor 方法来训练决策树回归模型。

如何使用树

组织中的预测建模者面临的一个重大障碍是所使用方法的被视为“黑盒子”的特性,这导致组织的其他部门反对使用这些方法。在这方面,树模型具有两个吸引人的方面:

  • 树模型提供了一个可视化工具,用于探索数据,以获得哪些变量是重要的,以及它们如何相互关联的想法。树可以捕捉预测变量之间的非线性关系。

  • 树模型提供了一组规则,可以有效地传达给非专业人员,用于实施或“销售”数据挖掘项目。

然而,在预测方面,利用多个树的结果通常比仅使用单个树更强大。特别是,随机森林和提升树算法几乎总是提供更优越的预测准确性和性能(参见“装袋和随机森林” 和 “提升”),但单棵树的上述优势会丧失。

进一步阅读

装袋和随机森林

1906 年,统计学家弗朗西斯·高尔顿(Francis Galton)在英格兰一个乡村展会上参与了一个竞猜展出牛的装重比赛。共有 800 个猜测,虽然个别猜测相差很大,但*均值和中位数都在牛的真实重量范围内误差不超过 1%。詹姆斯·苏罗维埃基在他的著作《众智》(The Wisdom of Crowds,Doubleday 出版,2004 年)中探讨了这一现象。这一原则同样适用于预测模型:多模型的*均值(或多数票制)——即模型的集成——比单一模型更为精确。

集成方法已应用于多种不同的建模方法,最为公众熟知的是在 Netflix Prize 中的应用,该竞赛由 Netflix 提供 100 万美元奖金,以奖励那些能提高 10%以上准确预测用户评分的模型。集成模型的简化版本如下:

  1. 开发一个预测模型,并记录给定数据集的预测结果。

  2. 在相同数据上重复多个模型的拟合。

  3. 对于每个要预测的记录,取预测结果的*均值(或加权*均值,或多数投票)。

集成方法在决策树中应用最为系统和有效。集成树模型非常强大,能够以相对较少的努力构建出良好的预测模型。

在简单的集成算法基础上,有两个主要的集成模型变体:baggingboosting。在集成树模型中,它们被称为 random forest 模型和 boosted tree 模型。本节重点介绍 baggingboosting 见 “Boosting”。

Bagging

Bagging,即“bootstrap aggregating”,由 Leo Breiman 于 1994 年提出。假设我们有一个响应 YP 个预测变量 𝐗 = X 1 , X 2 , , X P,具有 N 条记录。

Bagging 与集成的基本算法类似,不同之处在于,每个新模型是基于一个自助采样重新拟合的。以下更正式地呈现了该算法:

  1. 初始化 M,要拟合的模型数量,和 n,要选择的记录数(n < N)。设置迭代 m = 1

  2. 从训练数据中获取一个自助采样(即带有替换的样本)以形成子样本 Y m𝐗 m(称为“包”)。

  3. 使用 Y m𝐗 m 训练模型,创建一组决策规则 f ^ m ( 𝐗 )

  4. 将模型计数器递增 m = m + 1。如果 m <= M,则转到步骤 2。

f ^ m 预测概率 Y = 1 的情况下,袋装估计如下:

f ^ = 1 M f ^ 1 ( 𝐗 ) + f ^ 2 ( 𝐗 ) + + f ^ M ( 𝐗 )

随机森林

随机森林 基于对决策树应用装袋算法,但有一个重要扩展:除了对记录进行采样外,该算法还对变量进行采样。^(4) 在传统决策树中,为了确定如何创建分区 A 的子分区,算法通过最小化 Gini 不纯度等准则来选择变量和分割点(请参阅“测量同质性或不纯度”)。随机森林中,在算法的每个阶段,变量的选择限于变量的随机子集。与基本树算法(请参阅“递归分区算法”)相比,随机森林算法增加了另外两个步骤:前面讨论过的装袋(请参阅“装袋和随机森林”),以及在每次分割时对变量进行自助采样:

  1. 记录 中获取一个自助(有替换地)子样本。

  2. 对于第一个分割,随机无重复地采样 p < P 变量

  3. 对于每个采样变量 X j(1) , X j(2) , ... , X j(p),应用分割算法:

    1. 对于每个值 s j(k)X j(k)

      1. 将记录分割为分区 A,其中 X[j(k)] < s[j(k)] 作为一个分区,剩余记录,其中 X j(k) s j(k) 作为另一个分区。

      2. 测量 A 的每个子分区内类的同质性。

    2. 选择值 s j(k),以产生类内最大的分区同质性。

  4. 选择变量 X j(k) 和分割值 s j(k),以产生类内最大的分区同质性。

  5. 继续下一个拆分,并重复前面的步骤,从第二步开始。

  6. 按照相同的步骤继续进行额外的拆分,直到树生长完毕。

  7. 回到第 1 步,获取另一个自举子样本,并重新开始过程。

每一步抽样多少变量?一个经验法则是选择 P,其中 P 是预测变量的数量。包 randomForestR 中实现了随机森林。以下应用此包到贷款数据(参见 “K-最*邻” 对数据的描述):

rf <- randomForest(outcome ~ borrower_score + payment_inc_ratio,
                   data=loan3000)
rf

Call:
 randomForest(formula = outcome ~ borrower_score + payment_inc_ratio,
     data = loan3000)
           	Type of random forest: classification
                     Number of trees: 500
No. of variables tried at each split: 1

    	OOB estimate of error rate: 39.17%
Confusion matrix:
        default  paid off  class.error
default     873       572   0.39584775
paid off    603       952   0.38778135

Python 中,我们使用 sklearn.ensemble.RandomForestClassifier 方法:

predictors = ['borrower_score', 'payment_inc_ratio']
outcome = 'outcome'

X = loan3000[predictors]
y = loan3000[outcome]

rf = RandomForestClassifier(n_estimators=500, random_state=1, oob_score=True)
rf.fit(X, y)

默认情况下,训练了 500 棵树。由于预测器集中只有两个变量,算法在每个阶段随机选择变量进行拆分(即每个阶段的自举子样本大小为 1)。

袋外 (OOB) 错误估计是对留在该树训练集之外的数据应用于训练模型的错误率。使用模型输出,可以将 OOB 错误绘制成在随机森林中树的数量的图表化在 R

error_df = data.frame(error_rate=rf$err.rate[,'OOB'],
                      num_trees=1:rf$ntree)
ggplot(error_df, aes(x=num_trees, y=error_rate)) +
  geom_line()

RandomForestClassifier 的实现没有简单的方法来获取随机森林中树的数量作为袋外估计。我们可以训练一系列分类器,树的数量逐渐增加,并跟踪 oob_score_ 值。然而,这种方法并不高效:

n_estimator = list(range(20, 510, 5))
oobScores = []
for n in n_estimator:
    rf = RandomForestClassifier(n_estimators=n, criterion='entropy',
                                max_depth=5, random_state=1, oob_score=True)
    rf.fit(X, y)
    oobScores.append(rf.oob_score_)
df = pd.DataFrame({ 'n': n_estimator, 'oobScore': oobScores })
df.plot(x='n', y='oobScore')

结果显示在 图 6-6 中。错误率从超过 0.44 迅速下降,稳定在约 0.385。预测值可以从 predict 函数获取,并在 R 中按以下方式绘制:

pred <- predict(rf, prob=TRUE)
rf_df <- cbind(loan3000, pred = pred)
ggplot(data=rf_df, aes(x=borrower_score, y=payment_inc_ratio,
                       shape=pred, color=pred, size=pred)) +
    geom_point(alpha=.8) +
    scale_color_manual(values = c('paid off'='#b8e186', 'default'='#d95f02')) +
    scale_shape_manual(values = c('paid off'=0, 'default'=1)) +
    scale_size_manual(values = c('paid off'=0.5, 'default'=2))

Python 中,我们可以创建一个类似的图表如下所示:

predictions = X.copy()
predictions['prediction'] = rf.predict(X)
predictions.head()

fig, ax = plt.subplots(figsize=(4, 4))

predictions.loc[predictions.prediction=='paid off'].plot(
    x='borrower_score', y='payment_inc_ratio', style='.',
    markerfacecolor='none', markeredgecolor='C1', ax=ax)
predictions.loc[predictions.prediction=='default'].plot(
    x='borrower_score', y='payment_inc_ratio', style='o',
    markerfacecolor='none', markeredgecolor='C0', ax=ax)
ax.legend(['paid off', 'default']);
ax.set_xlim(0, 1)
ax.set_ylim(0, 25)
ax.set_xlabel('borrower_score')
ax.set_ylabel('payment_inc_ratio')

随着更多树的添加,随机森林准确性的改善。

图 6-6. 随着更多树的添加,随机森林准确性的改善示例

图 6-7 显示的图表对随机森林的性质非常揭示。

随机森林方法是一种“黑箱”方法。它产生比简单树更准确的预测,但失去了简单树直观的决策规则。随机森林的预测也有些噪音:请注意,一些信用评分非常高(表示高信用度)的借款人最终仍然会有违约预测。这是数据中一些异常记录的结果,显示了随机森林过拟合的危险(见“偏差-方差权衡”)。

随机森林应用于贷款违约数据的预测结果。

图 6-7. 随机森林应用于贷款违约数据的预测结果

变量重要性

当您为具有许多特征和记录的数据构建预测模型时,随机森林算法展现了其能力。它能够自动确定哪些预测变量是重要的,并发现与交互项相对应的预测变量之间的复杂关系(见“交互作用和主效应”)。例如,使用所有列拟合贷款违约数据的模型。以下是R中的示例:

rf_all <- randomForest(outcome ~ ., data=loan_data, importance=TRUE)
rf_all
Call:
 randomForest(formula = outcome ~ ., data = loan_data, importance = TRUE)
               Type of random forest: classification
                     Number of trees: 500
No. of variables tried at each split: 4

        OOB estimate of  error rate: 33.79%

Confusion matrix:
         paid off default class.error
paid off    14676    7995   0.3526532
default      7325   15346   0.3231000

以及在Python中:

predictors = ['loan_amnt', 'term', 'annual_inc', 'dti', 'payment_inc_ratio',
              'revol_bal', 'revol_util', 'purpose', 'delinq_2yrs_zero',
              'pub_rec_zero', 'open_acc', 'grade', 'emp_length', 'purpose_',
              'home_', 'emp_len_', 'borrower_score']
outcome = 'outcome'

X = pd.get_dummies(loan_data[predictors], drop_first=True)
y = loan_data[outcome]

rf_all = RandomForestClassifier(n_estimators=500, random_state=1)
rf_all.fit(X, y)

参数importance=TRUE要求randomForest存储有关不同变量重要性的额外信息。函数varImpPlot将绘制变量相对性能的图表(相对于置换该变量):

varImpPlot(rf_all, type=1) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
varImpPlot(rf_all, type=2) ![2](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/2.png)

1

准确性的*均降低

2

节点纯度的*均降低

Python中,RandomForestClassifier在训练过程中收集关于特征重要性的信息,并在feature_importances_字段中提供:

importances = rf_all.feature_importances_

“Gini 减少”作为拟合分类器的feature_importance_属性可用。然而,准确性的降低并不是Python的开箱即用功能。我们可以使用以下代码计算它(scores):

rf = RandomForestClassifier(n_estimators=500)
scores = defaultdict(list)

# cross-validate the scores on a number of different random splits of the data
for _ in range(3):
    train_X, valid_X, train_y, valid_y = train_test_split(X, y, test_size=0.3)
    rf.fit(train_X, train_y)
    acc = metrics.accuracy_score(valid_y, rf.predict(valid_X))
    for column in X.columns:
        X_t = valid_X.copy()
        X_t[column] = np.random.permutation(X_t[column].values)
        shuff_acc = metrics.accuracy_score(valid_y, rf.predict(X_t))
        scores[column].append((acc-shuff_acc)/acc)

结果显示在图 6-8 中。可以使用此Python代码创建类似的图表:

df = pd.DataFrame({
    'feature': X.columns,
    'Accuracy decrease': [np.mean(scores[column]) for column in X.columns],
    'Gini decrease': rf_all.feature_importances_,
})
df = df.sort_values('Accuracy decrease')

fig, axes = plt.subplots(ncols=2, figsize=(8, 4.5))
ax = df.plot(kind='barh', x='feature', y='Accuracy decrease',
             legend=False, ax=axes[0])
ax.set_ylabel('')

ax = df.plot(kind='barh', x='feature', y='Gini decrease',
             legend=False, ax=axes[1])
ax.set_ylabel('')
ax.get_yaxis().set_visible(False)

有两种衡量变量重要性的方法:

  • 当变量的值被随机置换(type=1)时,模型的准确性会下降。随机置换值的效果是移除该变量的所有预测能力。准确性是从袋外数据计算的(因此这个度量实际上是一个交叉验证的估计)。

  • 通过 Gini 不纯度得分的*均减少(参见“测量同质性或不纯度”)对所有分裂在变量上的节点(type=2)进行计算。这度量了包含该变量后节点纯度的提高程度。此度量基于训练集,因此不如在袋外数据上计算的度量可靠。

适用于贷款数据的完整模型拟合的变量重要性。

图 6-8。适用于贷款数据的完整模型拟合的变量重要性

图 6-8 的顶部和底部面板显示了根据准确度和 Gini 不纯度减少的变量重要性。两个面板中的变量都按准确度减少进行排序。这两种度量产生的变量重要性得分相当不同。

由于准确度减少是一个更可靠的度量标准,我们为什么要使用 Gini 不纯度减少度量?默认情况下,randomForest只计算这个 Gini 不纯度:Gini 不纯度是算法的副产品,而通过变量的模型准确度需要额外的计算(随机置换数据并预测这些数据)。在计算复杂度很重要的情况下,比如在拟合数千个模型的生产环境中,可能不值得额外的计算工作。此外,Gini 减少揭示了随机森林用于制定其分割规则的哪些变量(回想一下,在简单树中很容易看到的信息,在随机森林中实际上丢失了)。

超参数

与许多统计机器学习算法一样,随机森林可以被视为一个黑盒算法,其中有一些调节盒子如何工作的旋钮。这些旋钮称为超参数,它们是在拟合模型之前需要设置的参数;它们不会作为训练过程的一部分进行优化。虽然传统的统计模型需要选择(例如,在回归模型中使用的预测变量的选择),但是随机森林的超参数更为关键,特别是为了避免过度拟合。特别是,随机森林的两个最重要的超参数是:

nodesize/min_samples_leaf

终端节点(树中的叶子)的最小大小。对于分类,默认值为 1,在R中回归为 5。Python中的scikit-learn实现都默认为 1。

maxnodes/max_leaf_nodes

每个决策树中的最大节点数。默认情况下,没有限制,树的大小将根据nodesize的约束进行调整。请注意,在Python中,您指定的是最大终端节点数。这两个参数有关联:

maxnodes = 2 max leaf nodes - 1

或许会忽略这些参数,仅使用默认值。然而,当将随机森林应用于嘈杂的数据时,使用默认值可能会导致过拟合。当您增加 nodesize/min_samples_leaf 或设置 maxnodes/max_leaf_nodes 时,算法将拟合较小的树,并且不太可能创建虚假的预测规则。可以使用交叉验证(参见 “Cross-Validation”)来测试设置不同超参数值的效果。

Boosting

集成模型已成为预测建模的标准工具。Boosting 是创建模型集成的一般技术。它与 bagging 大致同时发展(参见 “Bagging and the Random Forest”)。像 bagging 一样,boosting 最常用于决策树。尽管它们有相似之处,但 boosting 采用的是一种截然不同的方法,带有更多的花里胡哨。因此,虽然 bagging 可以相对轻松地完成,但 boosting 在应用时需要更多的注意。如果将这两种方法比作汽车,那么 bagging 可以被视为本田雅阁(可靠而稳定),而 boosting 则可以被视为保时捷(强大但需要更多关注)。

在线性回归模型中,经常会检查残差,以查看是否可以改进拟合(参见 “Partial Residual Plots and Nonlinearity”)。Boosting 将这个概念推广到更深层次,并拟合一系列模型,其中每个后续模型旨在减少前一个模型的误差。通常使用几种算法变体:Adaboostgradient boostingstochastic gradient boosting。后者,即随机梯度 boosting,是最通用且广泛使用的。事实上,通过正确选择参数,该算法可以模拟随机森林。

Boosting 算法

存在各种 boosting 算法,它们的基本思想本质上是相同的。最容易理解的是 Adaboost,其过程如下:

  1. 初始化 M,要拟合的模型的最大数量,并设置迭代计数器 m = 1 。为观测权重 w i = 1 / N 初始化,其中 i = 1 , 2 , ... , N 。初始化集成模型 F ^ 0 = 0

  2. 使用观测权重w 1 , w 2 , ... , w N ,训练一个模型f ^ m,最小化由误分类观测权重定义的加权误差e m

  3. 将模型添加到集成模型中:F ^ m = F ^ m-1 + α m f ^ m,其中α m = log1-e m e m

  4. 更新权重w 1 , w 2 , ... , w N,使误分类的观测权重增加。增加的大小取决于α m,较大的α m值会导致权重增加。

  5. 增加模型计数器m = m + 1。如果m M,则转到步骤 2。

提升后的估计如下:

F ^ = α 1 f ^ 1 + α 2 f ^ 2 + + α M f ^ M

通过增加误分类观测的权重,该算法迫使模型更加深入地训练表现不佳的数据。因子α m确保误差较低的模型具有较大的权重。

梯度提升类似于 Adaboost,但将问题表述为成本函数的优化。梯度提升不是调整权重,而是对伪残差进行模型拟合,这样更重视较大残差的训练效果。与随机森林类似,随机梯度提升通过在每个阶段对观测和预测变量进行抽样,为算法引入随机性。

XGBoost

提升的最广泛使用的公共领域软件是 XGBoost,这是由华盛顿大学的 Tianqi Chen 和 Carlos Guestrin 最初开发的随机梯度提升的实现。作为一个计算效率高的实现,它在大多数主要数据科学软件语言中作为一个包提供。在R中,XGBoost 可作为xgboost和同名包在Python中使用。

xgboost方法有许多参数可以调整(参见“超参数和交叉验证”)。两个非常重要的参数是subsample,控制每次迭代应抽样的观测部分,以及eta,是应用于提升算法中的权重缩减因子(参见“提升算法”)。使用subsample使提升算法的行为类似于随机森林,不同之处在于抽样是无替换的。缩减参数eta有助于通过减小权重的变化来防止过拟合(权重变化较小意味着算法对训练集的过拟合可能性较小)。以下示例在R中应用xgboost到贷款数据中,仅使用两个预测变量:

predictors <- data.matrix(loan3000[, c('borrower_score', 'payment_inc_ratio')])
label <- as.numeric(loan3000[,'outcome']) - 1
xgb <- xgboost(data=predictors, label=label, objective="binary:logistic",
               params=list(subsample=0.63, eta=0.1), nrounds=100)
[1]	train-error:0.358333
[2]	train-error:0.346333
[3]	train-error:0.347333
...
[99]	train-error:0.239333
[100]	train-error:0.241000

注意xgboost不支持公式语法,因此预测变量需要转换为data.matrix,响应变量需要转换为 0/1 变量。objective参数告诉xgboost这是什么类型的问题;基于此,xgboost将选择一个优化度量。

Python中,xgboost有两种不同的接口:scikit-learn API 和类似R中的更功能化接口。为了与其他scikit-learn方法保持一致,一些参数已经重命名。例如,eta被重命名为learning_rate;使用eta不会失败,但不会产生预期的效果:

predictors = ['borrower_score', 'payment_inc_ratio']
outcome = 'outcome'

X = loan3000[predictors]
y = loan3000[outcome]

xgb = XGBClassifier(objective='binary:logistic', subsample=0.63)
xgb.fit(X, y)
--
XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
       colsample_bynode=1, colsample_bytree=1, gamma=0, learning_rate=0.1,
       max_delta_step=0, max_depth=3, min_child_weight=1, missing=None,
       n_estimators=100, n_jobs=1, nthread=None, objective='binary:logistic',
       random_state=0, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, seed=None,
       silent=None, subsample=0.63, verbosity=1)

预测值可以从R中的predict函数中获得,并且由于只有两个变量,可以与预测变量绘制对比图:

pred <- predict(xgb, newdata=predictors)
xgb_df <- cbind(loan3000, pred_default = pred > 0.5, prob_default = pred)
ggplot(data=xgb_df, aes(x=borrower_score, y=payment_inc_ratio,
                  color=pred_default, shape=pred_default, size=pred_default)) +
         geom_point(alpha=.8) +
         scale_color_manual(values = c('FALSE'='#b8e186', 'TRUE'='#d95f02')) +
         scale_shape_manual(values = c('FALSE'=0, 'TRUE'=1)) +
         scale_size_manual(values = c('FALSE'=0.5, 'TRUE'=2))

Python中可以使用以下代码创建相同的图形:

fig, ax = plt.subplots(figsize=(6, 4))

xgb_df.loc[xgb_df.prediction=='paid off'].plot(
    x='borrower_score', y='payment_inc_ratio', style='.',
    markerfacecolor='none', markeredgecolor='C1', ax=ax)
xgb_df.loc[xgb_df.prediction=='default'].plot(
    x='borrower_score', y='payment_inc_ratio', style='o',
    markerfacecolor='none', markeredgecolor='C0', ax=ax)
ax.legend(['paid off', 'default']);
ax.set_xlim(0, 1)
ax.set_ylim(0, 25)
ax.set_xlabel('borrower_score')
ax.set_ylabel('payment_inc_ratio')

结果显示在图 6-9 中。从质量上讲,这与随机森林的预测类似;请参见图 6-7。预测结果有些嘈杂,即一些借款人即使借款者评分非常高,最终也会被预测为违约。

应用于贷款违约数据的 XGBoost 预测结果。

图 6-9. XGBoost 应用于贷款违约数据的预测结果

正则化:避免过拟合

盲目应用 xgboost 可能导致由于对训练数据的 过度拟合 而导致的不稳定模型。过拟合问题有两个方面:

  • 模型在训练集之外的新数据上的准确性将下降。

  • 模型的预测结果变化很大,导致结果不稳定。

任何建模技术都可能存在过拟合的风险。例如,如果回归方程中包含了太多变量,模型可能会产生虚假预测。然而,对于大多数统计技术,可以通过谨慎选择预测变量来避免过拟合。即使是随机森林通常在不调整参数的情况下也能产生合理的模型。

然而,对于 xgboost 来说并非如此。在贷款数据集上使用 xgboost 进行训练集时,包括模型中的所有变量。在 R 中,可以这样做:

seed <- 400820
predictors <- data.matrix(loan_data[, -which(names(loan_data) %in%
                                       'outcome')])
label <- as.numeric(loan_data$outcome) - 1
test_idx <- sample(nrow(loan_data), 10000)

xgb_default <- xgboost(data=predictors[-test_idx,], label=label[-test_idx],
                       objective='binary:logistic', nrounds=250, verbose=0)
pred_default <- predict(xgb_default, predictors[test_idx,])
error_default <- abs(label[test_idx] - pred_default) > 0.5
xgb_default$evaluation_log[250,]
mean(error_default)
-
iter train_error
1:  250    0.133043

[1] 0.3529

我们使用 train_test_split 函数在 Python 中将数据集分割为训练集和测试集:

predictors = ['loan_amnt', 'term', 'annual_inc', 'dti', 'payment_inc_ratio',
              'revol_bal', 'revol_util', 'purpose', 'delinq_2yrs_zero',
              'pub_rec_zero', 'open_acc', 'grade', 'emp_length', 'purpose_',
              'home_', 'emp_len_', 'borrower_score']
outcome = 'outcome'

X = pd.get_dummies(loan_data[predictors], drop_first=True)
y = pd.Series([1 if o == 'default' else 0 for o in loan_data[outcome]])

train_X, valid_X, train_y, valid_y = train_test_split(X, y, test_size=10000)

xgb_default = XGBClassifier(objective='binary:logistic', n_estimators=250,
                            max_depth=6, reg_lambda=0, learning_rate=0.3,
                            subsample=1)
xgb_default.fit(train_X, train_y)

pred_default = xgb_default.predict_proba(valid_X)[:, 1]
error_default = abs(valid_y - pred_default) > 0.5
print('default: ', np.mean(error_default))

测试集包含从完整数据随机抽取的 10,000 条记录,训练集包含其余记录。增强学习导致训练集的错误率仅为 13.3%。然而,测试集的错误率要高得多,为 35.3%。这是由于过拟合造成的:虽然增强学习可以很好地解释训练集中的变异性,但预测规则不适用于新数据。

增强学习提供了几个参数来避免过拟合,包括参数 eta(或 learning_rate)和 subsample(参见 “XGBoost”)。另一种方法是 正则化,这是一种修改成本函数以 惩罚 模型复杂性的技术。决策树通过最小化诸如基尼不纯度分数之类的成本标准来拟合(参见 “测量同质性或不纯度”)。在 xgboost 中,可以通过添加一个衡量模型复杂性的项来修改成本函数。

xgboost 中有两个正则化模型的参数:alphalambda,分别对应曼哈顿距离(L1 正则化)和*方欧几里得距离(L2 正则化)(参见 “距离度量”)。增加这些参数将惩罚更复杂的模型,并减少拟合的树的大小。例如,看看如果在 R 中将 lambda 设置为 1,000 会发生什么:

xgb_penalty <- xgboost(data=predictors[-test_idx,], label=label[-test_idx],
                       params=list(eta=.1, subsample=.63, lambda=1000),
                       objective='binary:logistic', nrounds=250, verbose=0)
pred_penalty <- predict(xgb_penalty, predictors[test_idx,])
error_penalty <- abs(label[test_idx] - pred_penalty) > 0.5
xgb_penalty$evaluation_log[250,]
mean(error_penalty)
-
iter train_error
1:  250     0.30966

[1] 0.3286

scikit-learn API 中,这些参数称为 reg_alphareg_lambda

xgb_penalty = XGBClassifier(objective='binary:logistic', n_estimators=250,
                            max_depth=6, reg_lambda=1000, learning_rate=0.1,
                            subsample=0.63)
xgb_penalty.fit(train_X, train_y)
pred_penalty = xgb_penalty.predict_proba(valid_X)[:, 1]
error_penalty = abs(valid_y - pred_penalty) > 0.5
print('penalty: ', np.mean(error_penalty))

现在训练误差仅比测试集上的误差略低。

R 中,predict 方法提供了一个方便的参数 ntreelimit,强制只使用前 i 棵树进行预测。这使我们可以直接比较样本内与样本外的错误率随着模型增加的变化:

error_default <- rep(0, 250)
error_penalty <- rep(0, 250)
for(i in 1:250){
  pred_def <- predict(xgb_default, predictors[test_idx,], ntreelimit=i)
  error_default[i] <- mean(abs(label[test_idx] - pred_def) >= 0.5)
  pred_pen <- predict(xgb_penalty, predictors[test_idx,], ntreelimit=i)
  error_penalty[i] <- mean(abs(label[test_idx] - pred_pen) >= 0.5)
}

Python 中,我们可以使用 predict_proba 方法并带有 ntree_limit 参数:

results = []
for i in range(1, 250):
    train_default = xgb_default.predict_proba(train_X, ntree_limit=i)[:, 1]
    train_penalty = xgb_penalty.predict_proba(train_X, ntree_limit=i)[:, 1]
    pred_default = xgb_default.predict_proba(valid_X, ntree_limit=i)[:, 1]
    pred_penalty = xgb_penalty.predict_proba(valid_X, ntree_limit=i)[:, 1]
    results.append({
        'iterations': i,
        'default train': np.mean(abs(train_y - train_default) > 0.5),
        'penalty train': np.mean(abs(train_y - train_penalty) > 0.5),
        'default test': np.mean(abs(valid_y - pred_default) > 0.5),
        'penalty test': np.mean(abs(valid_y - pred_penalty) > 0.5),
    })

results = pd.DataFrame(results)
results.head()

模型的输出返回训练集中xgb_default$evaluation_log组件的错误信息。通过将其与样本外的错误结合起来,我们可以绘制错误与迭代次数的关系图:

errors <- rbind(xgb_default$evaluation_log,
                xgb_penalty$evaluation_log,
                ata.frame(iter=1:250, train_error=error_default),
                data.frame(iter=1:250, train_error=error_penalty))
errors$type <- rep(c('default train', 'penalty train',
                     'default test', 'penalty test'), rep(250, 4))
ggplot(errors, aes(x=iter, y=train_error, group=type)) +
  geom_line(aes(linetype=type, color=type))

我们可以使用pandas的绘图方法创建折线图。第一个图返回的轴允许我们在同一图上叠加额外的线条。这是许多Python图形包支持的模式:

ax = results.plot(x='iterations', y='default test')
results.plot(x='iterations', y='penalty test', ax=ax)
results.plot(x='iterations', y='default train', ax=ax)
results.plot(x='iterations', y='penalty train', ax=ax)

结果显示在图 6-10 中, 默认模型持续改善训练集的准确度,但实际上对于测试集来说却变得更糟。惩罚模型则没有这种行为。

默认 XGBoost 与惩罚版本 XGBoost 的错误率。

图 6-10. 默认的 XGBoost 错误率与惩罚版本的对比

超参数和交叉验证

xgboost具有让人望而却步的一系列超参数;有关讨论,请参见“XGBoost 超参数”。正如在“正则化:避免过拟合”中所看到的,具体的选择可以显著改变模型的拟合效果。面对庞大的超参数组合选择,我们该如何做出指导性的选择?解决这个问题的一种标准方法是使用交叉验证;请参见“交叉验证”。交叉验证将数据随机分成K个不同的组,也称为折叠。对于每个折叠,模型在不在折叠中的数据上进行训练,然后在折叠中的数据上进行评估。这给出了模型在样本外数据上准确度的衡量。最佳的超参数组合是由具有最低整体错误的模型给出的,该错误通过计算每个折叠的*均错误得出。

为了说明这种技术,我们将其应用于xgboost的参数选择。在这个例子中,我们探索了两个参数:收缩参数eta(即learning_rate—参见“XGBoost”)和树的最大深度max_depth。参数max_depth是叶节点到树根的最大深度,默认值为六。这为我们提供了另一种控制过拟合的方式:深树往往更复杂,可能会导致数据过拟合。首先我们设置了折叠和参数列表。在R中,可以这样做:

N <- nrow(loan_data)
fold_number <- sample(1:5, N, replace=TRUE)
params <- data.frame(eta = rep(c(.1, .5, .9), 3),
                     max_depth = rep(c(3, 6, 12), rep(3,3)))

现在我们将上述算法应用于使用五折交叉验证计算每个模型和每个折叠的错误。

error <- matrix(0, nrow=9, ncol=5)
for(i in 1:nrow(params)){
  for(k in 1:5){
    fold_idx <- (1:N)[fold_number == k]
    xgb <- xgboost(data=predictors[-fold_idx,], label=label[-fold_idx],
                   params=list(eta=params[i, 'eta'],
                               max_depth=params[i, 'max_depth']),
                   objective='binary:logistic', nrounds=100, verbose=0)
    pred <- predict(xgb, predictors[fold_idx,])
    error[i, k] <- mean(abs(label[fold_idx] - pred) >= 0.5)
  }
}

在以下Python代码中,我们创建了所有可能的超参数组合,并使用每个组合拟合和评估模型:

idx = np.random.choice(range(5), size=len(X), replace=True)
error = []
for eta, max_depth in product([0.1, 0.5, 0.9], [3, 6, 9]):  ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
    xgb = XGBClassifier(objective='binary:logistic', n_estimators=250,
                        max_depth=max_depth, learning_rate=eta)
    cv_error = []
    for k in range(5):
        fold_idx = idx == k
        train_X = X.loc[~fold_idx]; train_y = y[~fold_idx]
        valid_X = X.loc[fold_idx]; valid_y = y[fold_idx]

        xgb.fit(train_X, train_y)
        pred = xgb.predict_proba(valid_X)[:, 1]
        cv_error.append(np.mean(abs(valid_y - pred) > 0.5))
    error.append({
        'eta': eta,
        'max_depth': max_depth,
        'avg_error': np.mean(cv_error)
    })
    print(error[-1])
errors = pd.DataFrame(error)

1

我们使用Python标准库中的itertools.product函数来创建两个超参数的所有可能组合。

由于我们正在拟合总共 45 个模型,这可能需要一些时间。错误被存储为一个矩阵,模型沿行排列,而折叠沿列排列。使用函数rowMeans,我们可以比较不同参数设置的错误率。

avg_error <- 100 * round(rowMeans(error), 4)
cbind(params, avg_error)
  eta max_depth avg_error
1 0.1         3     32.90
2 0.5         3     33.43
3 0.9         3     34.36
4 0.1         6     33.08
5 0.5         6     35.60
6 0.9         6     37.82
7 0.1        12     34.56
8 0.5        12     36.83
9 0.9        12     38.18

交叉验证表明,使用较小的eta/learning_rate值和更浅的树会产生更准确的结果。由于这些模型也更加稳定,所以最佳参数是eta=0.1max_depth=3(或可能是max_depth=6)。

摘要

本章已经描述了两种分类和预测方法,这些方法灵活地和局部地从数据中“学习”,而不是从适用于整个数据集的结构模型(例如线性回归)开始。K-最*邻居是一个简单的过程,它查看类似记录并将它们的主要类别(或*均值)分配给要预测的记录。尝试不同的预测变量的截断(分割)值,树模型迭代地将数据划分为越来越同类的部分和子部分。最有效的分割值形成一条路径,同时也是通向分类或预测的“规则”。树模型是一个非常强大且受欢迎的预测工具,通常优于其他方法。它们已经衍生出各种集成方法(随机森林、提升、装袋),以提高树的预测能力。

^(1) 本章及后续章节版权所有 © 2020 Datastats, LLC, Peter Bruce, Andrew Bruce, 和 Peter Gedeck;已获授权使用。

^(2) 对于这个例子,我们将loan200数据集中的第一行作为newloan,并将其从训练数据集中排除。

^(3) CART 一词是 Salford Systems 注册商标,与他们的树模型特定实现相关。

^(4) 随机森林一词是 Leo Breiman 和 Adele Cutler 的商标,并许可给 Salford Systems。没有标准的非商标名称,而随机森林这个术语就像 Kleenex 与面巾纸一样与该算法同义。

第七章:无监督学习

无监督学习一词指的是从数据中提取意义而不是在标记数据(已知感兴趣结果的数据)上训练模型的统计方法。在第 4 到 6 章中,目标是构建一个模型(一组规则)来从一组预测变量中预测响应变量。这是监督学习。相比之下,无监督学习也构建数据模型,但不区分响应变量和预测变量。

无监督学习可以用于实现不同的目标。在某些情况下,它可以在没有标记响应的情况下创建预测规则。聚类方法可用于识别有意义的数据组。例如,利用用户在网站上的点击和人口统计数据,我们可能能够将不同类型的用户分组。然后网站可以根据这些不同类型进行个性化设置。

在其他情况下,目标可能是将数据的维度减少到一组更易管理的变量。然后可以将这个减少的集合用作预测模型的输入,例如回归或分类模型。例如,我们可能有数千个传感器来监控一个工业过程。通过将数据减少到更小的一组特征,我们可能能够构建比包含来自数千个传感器的数据流更强大且可解释的模型,以预测过程失败。

最后,无监督学习可以被看作是探索性数据分析(参见第一章)的延伸,用于处理大量变量和记录的情况。其目的是深入了解数据集以及不同变量之间的关系。无监督技术允许您筛选和分析这些变量,并发现它们之间的关系。

无监督学习与预测

无监督学习在预测中可以发挥重要作用,无论是回归问题还是分类问题。在某些情况下,我们希望在没有标记数据的情况下预测一个类别。例如,我们可能希望根据一组卫星传感器数据预测一个区域的植被类型。由于没有响应变量来训练模型,聚类为我们提供了一种识别共同模式和对区域进行分类的方法。

聚类对于“冷启动问题”尤为重要。在这种类型的问题中,比如推出新的营销活动或识别潜在的新型欺诈或垃圾邮件,我们最初可能没有任何响应来训练模型。随着时间的推移,随着数据的收集,我们可以更多地了解系统并构建传统的预测模型。但是聚类通过识别人群段落帮助我们更快地启动学习过程。

无监督学习对于回归和分类技术也很重要。在大数据中,如果小的子群体在整体群体中代表不足,那么训练模型可能在该子群体上表现不佳。通过聚类,可以识别和标记子群体。然后可以为不同的子群体拟合单独的模型。或者,可以用自己的特征表示子群体,迫使整体模型明确考虑子群体身份作为预测因子。

主成分分析

通常,变量会一起变化(共变),某些变量的变化实际上是由另一变量的变化重复的(例如,餐厅账单和小费)。主成分分析(PCA)是一种发现数值变量共变方式的技术。^(1)

PCA 的理念是将多个数值预测变量组合成较小的一组变量,这些变量是原始集合的加权线性组合。这组较小的变量,即主成分,“解释”了整个变量集合的大部分变异性,从而降低了数据的维度。用于形成主成分的权重显示了原始变量对新主成分的相对贡献。

PCA 最初由 卡尔·皮尔逊提出。在或许是第一篇无监督学习论文中,皮尔逊认识到在许多问题中,预测变量存在变异性,因此他开发了 PCA 作为一种模拟这种变异性的技术。PCA 可以看作是线性判别分析的无监督版本;参见 “判别分析”。

简单示例

对于两个变量,X 1X 2 ,有两个主成分 Z ii = 1 或 2):

Z i = w i,1 X 1 + w i,2 X 2

权重 ( w i,1 , w i,2 ) 被称为成分载荷。这些将原始变量转换为主成分。第一个主成分,Z 1 ,是最能解释总变化的线性组合。第二个主成分,Z 2 ,与第一个成分正交,并尽可能多地解释剩余的变化。(如果有额外的成分,每一个都会与其他成分正交。)

注意

通常也会计算基于预测变量偏差的主成分,而不是基于值本身。

你可以使用princomp函数在R中计算主成分。以下是对雪佛龙(CVX)和埃克森美孚(XOM)股票价格回报进行主成分分析的示例:

oil_px <- sp500_px[, c('CVX', 'XOM')]
pca <- princomp(oil_px)
pca$loadings

Loadings:
    Comp.1 Comp.2
CVX -0.747  0.665
XOM -0.665 -0.747

               Comp.1 Comp.2
SS loadings       1.0    1.0
Proportion Var    0.5    0.5
Cumulative Var    0.5    1.0

Python中,我们可以使用scikit-learn中的sklearn.decomposition.PCA实现:

pcs = PCA(n_components=2)
pcs.fit(oil_px)
loadings = pd.DataFrame(pcs.components_, columns=oil_px.columns)
loadings

雪佛龙和埃克森美孚的第一个主成分权重为-0.747 和-0.665,第二个主成分的权重为 0.665 和-0.747。如何解释这一点?第一个主成分基本上是 CVX 和 XOM 的*均值,反映了这两家能源公司之间的相关性。第二个主成分衡量了 CVX 和 XOM 股票价格分歧的时候。

将主成分与数据一起绘制非常有教育意义。在这里我们使用R创建了一个可视化效果:

loadings <- pca$loadings
ggplot(data=oil_px, aes(x=CVX, y=XOM)) +
  geom_point(alpha=.3) +
  stat_ellipse(type='norm', level=.99) +
  geom_abline(intercept = 0, slope = loadings[2,1]/loadings[1,1]) +
  geom_abline(intercept = 0, slope = loadings[2,2]/loadings[1,2])

下面的代码使用Python创建类似的可视化效果:

def abline(slope, intercept, ax):
    """Calculate coordinates of a line based on slope and intercept"""
    x_vals = np.array(ax.get_xlim())
    return (x_vals, intercept + slope * x_vals)

ax = oil_px.plot.scatter(x='XOM', y='CVX', alpha=0.3, figsize=(4, 4))
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
ax.plot(*abline(loadings.loc[0, 'CVX'] / loadings.loc[0, 'XOM'], 0, ax),
        '--', color='C1')
ax.plot(*abline(loadings.loc[1, 'CVX'] / loadings.loc[1, 'XOM'], 0, ax),
        '--', color='C1')

结果显示在图 7-1 中。

雪佛龙和埃克森美孚股票回报的主成分

图 7-1. 雪佛龙(CVX)和埃克森美孚(XOM)股票回报的主成分

虚线显示了两个主成分的方向:第一个主成分沿着椭圆的长轴,第二个主成分沿着短轴。您可以看到,雪佛龙和埃克森美孚的股票回报中的大部分变异性都由第一个主成分解释。这是有道理的,因为能源股票价格往往会作为一组移动。

注意

第一个主成分的权重都为负,但反转所有权重的符号并不会改变主成分。例如,使用第一个主成分的权重 0.747 和 0.665 等同于负权重,就像由原点和 1,1 定义的无限线条等同于由原点和-1,-1 定义的线条一样。

计算主成分

从两个变量到更多变量的过程很简单。对于第一个成分,只需将额外的预测变量包括在线性组合中,并分配权重以优化所有预测变量的协变化进入这第一个主成分(协方差是统计术语;见“协方差矩阵”)。主成分的计算是一种经典的统计方法,依赖于数据的相关矩阵或协方差矩阵,并且执行迅速,不依赖迭代。正如前面所述,主成分分析仅适用于数值变量,而不适用于分类变量。整个过程可以描述如下:

  1. 在创建第一个主成分时,PCA 得出了最大化解释总方差百分比的预测变量的线性组合。

  2. 然后,这个线性组合就成为第一个“新”预测变量Z[1]。

  3. PCA 重复此过程,使用不同的权重与相同的变量,以创建第二个新的预测变量Z[2]。权重的设置使得Z[1]和Z[2]不相关。

  4. 该过程持续进行,直到您获得与原始变量X[i]一样多的新变量或组件Z[i]。

  5. 选择保留尽可能多的组件,以解释大部分的方差。

  6. 到目前为止,每个组件都有一组权重。最后一步是通过将这些权重应用于原始值来将原始数据转换为新的主成分分数。然后可以使用这些新分数作为减少的预测变量集。

解释主成分

主成分的性质通常揭示了关于数据结构的信息。有几种标准的可视化显示方法可帮助您获取有关主成分的见解。其中一种方法是屏幕图,用于可视化主成分的相对重要性(该名称源于图表与屏坡的相似性;这里,y 轴是特征值)。以下R代码显示了 S&P 500 中几家顶级公司的示例:

syms <- c( 'AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM',
   'SLB', 'COP', 'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST')
top_sp <- sp500_px[row.names(sp500_px)>='2005-01-01', syms]
sp_pca <- princomp(top_sp)
screeplot(sp_pca)

scikit-learn结果创建加载图的信息可在explained_variance_中找到。在这里,我们将其转换为pandas数据框架,并用它制作条形图:

syms = sorted(['AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM', 'SLB', 'COP',
               'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST'])
top_sp = sp500_px.loc[sp500_px.index >= '2011-01-01', syms]

sp_pca = PCA()
sp_pca.fit(top_sp)

explained_variance = pd.DataFrame(sp_pca.explained_variance_)
ax = explained_variance.head(10).plot.bar(legend=False, figsize=(4, 4))
ax.set_xlabel('Component')

如图 7-2 所示,第一个主成分的方差非常大(通常情况下如此),但其他顶级主成分也很显著。

S&P 500 中热门股票的 PCA 的屏幕图。

图 7-2. S&P 500 中热门股票的 PCA 的屏幕图

绘制顶级主成分的权重可能特别有启发性。在R中,一种方法是使用tidyr包中的gather函数与ggplot结合使用:

library(tidyr)
loadings <- sp_pca$loadings[,1:5]
loadings$Symbol <- row.names(loadings)
loadings <- gather(loadings, 'Component', 'Weight', -Symbol)
ggplot(loadings, aes(x=Symbol, y=Weight)) +
  geom_bar(stat='identity') +
  facet_grid(Component ~ ., scales='free_y')

这是在Python中创建相同可视化的代码:

loadings = pd.DataFrame(sp_pca.components_[0:5, :], columns=top_sp.columns)
maxPC = 1.01 * np.max(np.max(np.abs(loadings.loc[0:5, :])))

f, axes = plt.subplots(5, 1, figsize=(5, 5), sharex=True)
for i, ax in enumerate(axes):
    pc_loadings = loadings.loc[i, :]
    colors = ['C0' if l > 0 else 'C1' for l in pc_loadings]
    ax.axhline(color='#888888')
    pc_loadings.plot.bar(ax=ax, color=colors)
    ax.set_ylabel(f'PC{i+1}')
    ax.set_ylim(-maxPC, maxPC)

前五个成分的负载显示在图 7-3 中。第一个主成分的负载具有相同的符号:这对于所有列共享一个公共因子的数据是典型的(在这种情况下,是整体股市趋势)。第二个成分捕捉了能源股票的价格变化相对于其他股票的情况。第三个成分主要是对比了苹果和 CostCo 的动态。第四个成分对比了斯伦贝谢(SLB)与其他能源股票的动态。最后,第五个成分主要受到金融公司的影响。

股价回报的前五个主成分的负载。

图 7-3. 股价回报的前五个主成分的负载

如何选择成分数量?

如果你的目标是降低数据的维度,你必须决定选择多少个主成分。最常见的方法是使用一种临时规则来选择解释“大部分”方差的成分。你可以通过研究屏斜图来直观地做到这一点,例如图 7-2。或者,你可以选择前几个成分,使累积方差超过一个阈值,比如 80%。此外,你还可以检查负载以确定成分是否具有直观的解释。交叉验证提供了一种更正式的方法来选择显著成分的数量(参见“交叉验证”了解更多)。

对应分析

PCA 不能用于分类数据;然而,一个有点相关的技术是对应分析。其目标是识别类别之间的关联,或者分类特征之间的关联。对应分析与主成分分析的相似之处主要在于底层——用于尺度化维度的矩阵代数。对应分析主要用于低维分类数据的图形分析,并不像 PCA 那样用于大数据的维度减少作为预处理步骤。

输入可以看作是一个表格,其中行代表一个变量,列代表另一个变量,单元格表示记录计数。输出(经过一些矩阵代数运算后)是一个双标图 —— 一个散点图,其轴经过缩放(并且通过百分比显示该维度解释的方差量)。轴上的单位含义与原始数据的直觉连接并不大,散点图的主要价值在于以图形方式说明彼此相关的变量(通过图中的接*度)。例如,参见图 7-4,在该图中,家务任务按照是否共同完成(垂直轴)和妻子或丈夫是否有主要责任(水*轴)进行排列。对应分析已经存在了几十年,就像这个示例的精神一样,根据任务的分配。

R中,有多种用于对应分析的软件包。这里我们使用ca软件包:

ca_analysis <- ca(housetasks)
plot(ca_analysis)

Python中,我们可以使用prince软件包,它使用scikit-learn API 实现了对应分析:

ca = prince.CA(n_components=2)
ca = ca.fit(housetasks)

ca.plot_coordinates(housetasks, figsize=(6, 6))

房屋任务数据的对应分析

图 7-4. 房屋任务数据的对应分析的图形表示

进一步阅读

想要详细了解主成分分析中交叉验证的使用方法,请参阅 Rasmus Bro, K. Kjeldahl, A.K. Smilde, 和 Henk A. L. Kiers 的文章,“Component Models 的交叉验证:对当前方法的批判性审视”,发表于分析与生物分析化学390 卷,5 期(2008 年)。

K-Means 聚类

聚类是一种将数据分成不同组的技术,其中每组内的记录彼此相似。聚类的目标是识别重要且有意义的数据组。这些组可以直接使用,深入分析,或者作为预测回归或分类模型的特征或结果。K-means是最早开发的聚类方法之一;它仍然被广泛使用,因为算法相对简单且能够扩展到大数据集。

K-means 通过最小化每个记录到其分配的群集的均值的*方距离来将数据分成K个群集。这被称为群内*方和群内 SSK-means 不能确保群集大小相同,但能找到最佳分离的群集。

标准化

通常会通过减去均值并除以标准差来对连续变量进行标准化。否则,具有大量数据的变量会在聚类过程中占主导地位(参见“标准化(归一化,z-分数)”)。

一个简单的例子

首先考虑一个数据集,包含n个记录和两个变量,xy 。假设我们想将数据分成K = 4个群集。这意味着将每个记录( x i , y i ) 分配给一个群集k。考虑到将n k个记录分配给群集k,群集的中心( x ¯ k , y ¯ k ) 是群集中点的均值:

x¯ k = 1nk i Cluster k x i y¯ k = 1nk i Cluster k yi

聚类*均值

在具有多个变量的记录聚类中(典型情况),术语簇均值不是指单一数字,而是指变量均值向量。

簇内*方和由以下给出:

SS k = iClusterk x i -x ¯ k 2 + y i -y ¯ k 2

K-means 找到了记录分配方式,以最小化所有四个聚类的簇内*方和SS 1 + SS 2 + SS 3 + SS 4

k=1 4 SS k

聚类的典型用途是在数据中找到自然的、分离的聚类。另一个应用是将数据分为预定数量的单独组,聚类用于确保这些组尽可能彼此不同。

例如,假设我们想将每日股票收益分为四组。可以使用K-means 聚类将数据分隔为最佳分组。请注意,每日股票收益以一种实际上是标准化的方式报告,因此我们不需要对数据进行标准化。在R中,可以使用kmeans函数执行K-means 聚类。例如,以下基于两个变量——埃克森美孚(XOM)和雪佛龙(CVX)的每日股票收益来找到四个簇:

df <- sp500_px[row.names(sp500_px)>='2011-01-01', c('XOM', 'CVX')]
km <- kmeans(df, centers=4)

我们使用Pythonscikit-learnsklearn.cluster.KMeans方法:

df = sp500_px.loc[sp500_px.index >= '2011-01-01', ['XOM', 'CVX']]
kmeans = KMeans(n_clusters=4).fit(df)

每条记录的簇分配作为cluster组件(R)返回:

> df$cluster <- factor(km$cluster)
> head(df)
                  XOM        CVX cluster
2011-01-03 0.73680496  0.2406809       2
2011-01-04 0.16866845 -0.5845157       1
2011-01-05 0.02663055  0.4469854       2
2011-01-06 0.24855834 -0.9197513       1
2011-01-07 0.33732892  0.1805111       2
2011-01-10 0.00000000 -0.4641675       1

scikit-learn中,聚类标签可在labels_字段中找到:

df['cluster'] = kmeans.labels_
df.head()

前六条记录分配到簇 1 或簇 2。聚类的均值也返回(R):

> centers <- data.frame(cluster=factor(1:4), km$centers)
> centers
  cluster        XOM        CVX
1       1 -0.3284864 -0.5669135
2       2  0.2410159  0.3342130
3       3 -1.1439800 -1.7502975
4       4  0.9568628  1.3708892

scikit-learn中,聚类中心可在cluster_centers_字段中找到:

centers = pd.DataFrame(kmeans.cluster_centers_, columns=['XOM', 'CVX'])
centers

簇 1 和簇 3 代表“下跌”市场,而簇 2 和簇 4 代表“上涨”市场。

由于K-means 算法使用随机起始点,结果可能在后续运行和不同实现方法之间有所不同。一般而言,应检查波动是否过大。

在这个例子中,只有两个变量,可以直观地展示聚类及其均值:

ggplot(data=df, aes(x=XOM, y=CVX, color=cluster, shape=cluster)) +
  geom_point(alpha=.3) +
  geom_point(data=centers,  aes(x=XOM, y=CVX), size=3, stroke=2)

seabornscatterplot函数使得可以通过属性(hue)和样式(style)轻松着色点:

fig, ax = plt.subplots(figsize=(4, 4))
ax = sns.scatterplot(x='XOM', y='CVX', hue='cluster', style='cluster',
                     ax=ax, data=df)
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)
centers.plot.scatter(x='XOM', y='CVX', ax=ax, s=50, color='black')

所得图中显示了图 7-5 中的聚类分配和聚类均值。请注意,即使这些聚类没有很好地分离,K-means 也会将记录分配到聚类中(这在需要将记录最优地分成组时非常有用)。

应用于埃克森美孚和雪佛龙的股价数据的*k*-means 聚类(聚类中心用黑色符号突出显示)。

图 7-5. 应用于埃克森美孚和雪佛龙每日股票收益的 K 均值聚类(聚类中心用黑色符号突出显示)

K 均值算法

通常情况下,K均值可以应用于具有p个变量的数据集X 1 , ... , X p 。虽然K均值的确切解决方案在计算上非常困难,但启发式算法提供了计算局部最优解的有效方法。

算法从用户指定的K和初始聚类均值开始,然后迭代以下步骤:

  1. 将每条记录分配到最*的聚类均值,方法是通过*方距离来衡量。

  2. 根据记录的分配计算新的聚类均值。

当记录分配到聚类时不再改变时,算法收敛。

对于第一次迭代,您需要指定一个初始的聚类均值集。通常情况下,您可以通过随机将每个记录分配给K个聚类之一,然后找到这些聚类的均值来完成此操作。

由于此算法不能保证找到最佳可能的解决方案,建议使用不同的随机样本多次运行算法以初始化算法。当使用多组迭代时,K均值结果由具有最低聚类内*方和的迭代给出。

R函数kmeansnstart参数允许您指定尝试的随机起始次数。例如,以下代码使用 10 个不同的起始聚类均值运行K均值以找到 5 个聚类:

syms <- c( 'AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM', 'SLB', 'COP',
           'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST')
df <- sp500_px[row.names(sp500_px) >= '2011-01-01', syms]
km <- kmeans(df, centers=5, nstart=10)

函数会自动从 10 个不同的起始点中返回最佳解决方案。您可以使用参数iter.max来设置算法允许的每个随机起始的最大迭代次数。

默认情况下,scikit-learn算法会重复 10 次(n_init)。参数max_iter(默认为 300)可用于控制迭代次数:

syms = sorted(['AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM', 'SLB', 'COP',
               'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST'])
top_sp = sp500_px.loc[sp500_px.index >= '2011-01-01', syms]
kmeans = KMeans(n_clusters=5).fit(top_sp)

解释聚类

聚类分析的一个重要部分可能涉及对聚类的解释。kmeans的两个最重要的输出是聚类的大小和聚类均值。对于上一小节的示例,生成的聚类大小由以下R命令给出:

km$size
[1] 106 186 285 288 266

Python中,我们可以使用标准库中的collections.Counter类来获取这些信息。由于实现的差异和算法固有的随机性,结果会有所不同:

from collections import Counter
Counter(kmeans.labels_)

Counter({4: 302, 2: 272, 0: 288, 3: 158, 1: 111})

聚类大小相对*衡。不*衡的聚类可能是由于远离异常值或非常不同于数据其余部分的记录组成,这两者都可能需要进一步检查。

您可以使用gather函数与ggplot结合来绘制聚类的中心:

centers <- as.data.frame(t(centers))
names(centers) <- paste("Cluster", 1:5)
centers$Symbol <- row.names(centers)
centers <- gather(centers, 'Cluster', 'Mean', -Symbol)
centers$Color = centers$Mean > 0
ggplot(centers, aes(x=Symbol, y=Mean, fill=Color)) +
  geom_bar(stat='identity', position='identity', width=.75) +
  facet_grid(Cluster ~ ., scales='free_y')

创建此可视化的Python代码与我们用于 PCA 的代码类似:

centers = pd.DataFrame(kmeans.cluster_centers_, columns=syms)

f, axes = plt.subplots(5, 1, figsize=(5, 5), sharex=True)
for i, ax in enumerate(axes):
    center = centers.loc[i, :]
    maxPC = 1.01 * np.max(np.max(np.abs(center)))
    colors = ['C0' if l > 0 else 'C1' for l in center]
    ax.axhline(color='#888888')
    center.plot.bar(ax=ax, color=colors)
    ax.set_ylabel(f'Cluster {i + 1}')
    ax.set_ylim(-maxPC, maxPC)

生成的图表显示在图 7-6 中,显示了每个集群的性质。例如,集群 4 和 5 对应于市场下跌和上涨的日子。集群 2 和 3 分别以消费者股票上涨日和能源股票下跌日为特征。最后,集群 1 捕捉到了能源股票上涨和消费者股票下跌的日子。

集群均值。

图 7-6. 每个集群中变量的均值(“集群均值”)

聚类分析与主成分分析

集群均值图表的精神类似于主成分分析(PCA)的载荷。主要区别在于,与 PCA 不同,集群均值的符号是有意义的。PCA 识别变化的主要方向,而聚类分析找到彼此附*的记录组。

选择集群数量

K-means 算法要求您指定集群数量K。有时,集群数量由应用程序驱动。例如,管理销售团队的公司可能希望将客户聚类成“人物角色”以便于专注和引导销售电话。在这种情况下,管理考虑因素将决定所需客户段数——例如,两个可能不会产生有用的客户差异化,而八个可能太多难以管理。

在缺乏实际或管理考虑因素决定的集群数量的情况下,可以使用统计方法。没有单一标准方法来找到“最佳”集群数量。

一个常见的方法是肘方法,其目标是确定集群的集合何时解释了数据的“大部分”方差。在此集合之外添加新的集群对解释的方差贡献相对较少。肘部是在累积解释的方差在陡峭上升后变得*缓的点,因此得名此方法。

图 7-7 显示了默认数据在集群数量从 2 到 15 范围内解释的累积百分比方差。在此示例中,肘部在哪里?在这个例子中没有明显的候选者,因为方差解释的增量逐渐下降。这在没有明确定义集群的数据中非常典型。这可能是肘方法的一个缺点,但它确实揭示了数据的本质。

股票数据应用肘方法。

图 7-7. 股票数据应用肘方法

R中,kmeans函数并没有提供一个单一的命令来应用肘方法,但可以从kmeans的输出中很容易地应用,如下所示:

pct_var <- data.frame(pct_var = 0,
                      num_clusters = 2:14)
totalss <- kmeans(df, centers=14, nstart=50, iter.max=100)$totss
for (i in 2:14) {
  kmCluster <- kmeans(df, centers=i, nstart=50, iter.max=100)
  pct_var[i-1, 'pct_var'] <- kmCluster$betweenss / totalss
}

对于KMeans的结果,我们从属性inertia_中获取这些信息。在转换为pandas数据框后,我们可以使用其plot方法创建图表:

inertia = []
for n_clusters in range(2, 14):
    kmeans = KMeans(n_clusters=n_clusters, random_state=0).fit(top_sp)
    inertia.append(kmeans.inertia_ / n_clusters)

inertias = pd.DataFrame({'n_clusters': range(2, 14), 'inertia': inertia})
ax = inertias.plot(x='n_clusters', y='inertia')
plt.xlabel('Number of clusters(k)')
plt.ylabel('Average Within-Cluster Squared Distances')
plt.ylim((0, 1.1 * inertias.inertia.max()))
ax.legend().set_visible(False)

在评估要保留多少个聚类时,也许最重要的测试是:这些聚类在新数据上能否被复制?这些聚类是否可解释,并且它们是否与数据的一般特征相关联,还是仅反映特定实例?部分可以通过交叉验证来评估;参见“交叉验证”。

通常情况下,没有一条单一的规则能够可靠地指导产生多少个聚类。

注意

根据统计学或信息理论有几种更正式的方法来确定聚类数。例如,Robert Tibshirani、Guenther Walther 和 Trevor Hastie 提出了基于统计理论的“间隙”统计量 来识别拐点。对于大多数应用程序来说,理论方法可能是不必要的,甚至不合适。

层次聚类

层次聚类是一种替代K-均值的方法,可以产生非常不同的聚类。层次聚类允许用户可视化指定不同聚类数的效果。在发现异常或畸变组或记录方面更为敏感。层次聚类还适合直观的图形显示,有助于更容易地解释聚类。

层次聚类的灵活性伴随着成本,且层次聚类不适用于具有数百万条记录的大数据集。即使是具有几万条记录的中等规模数据,层次聚类也可能需要大量的计算资源。实际上,大多数层次聚类的应用集中在相对小型的数据集上。

简单示例

层次聚类适用于一个具有n条记录和p个变量的数据集,并基于两个基本构建块:

  • 一个距离度量 d i,j 用于测量两个记录ij之间的距离。

  • 一个不相似度度量 D A,B 用于基于成员之间的距离 d i,j 来测量两个聚类AB之间的差异。

对于涉及数值数据的应用程序,最重要的选择是不相似度度量。层次聚类通过将每条记录设置为其自己的集群,并迭代以合并最不相似的集群来开始。

R 中,可以使用 hclust 函数执行层次聚类。与 kmeans 不同之处在于,它基于成对距离 d i,j 而不是数据本身。可以使用 dist 函数计算这些距离。例如,以下代码对一组公司的股票回报应用了层次聚类:

syms1 <- c('GOOGL', 'AMZN', 'AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM', 'SLB',
           'COP', 'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST')
# take transpose: to cluster companies, we need the stocks along the rows
df <- t(sp500_px[row.names(sp500_px) >= '2011-01-01', syms1])
d <- dist(df)
hcl <- hclust(d)

聚类算法将数据框的记录(行)进行聚类。由于我们想要对公司进行聚类,因此需要转置t)数据框,使得股票沿着行,日期沿着列。

scipy 包在 scipy.cluster.hierarchy 模块中提供了多种不同的层次聚类方法。这里我们使用 linkage 函数以“complete”方法:

syms1 = ['AAPL', 'AMZN', 'AXP', 'COP', 'COST', 'CSCO', 'CVX', 'GOOGL', 'HD',
         'INTC', 'JPM', 'MSFT', 'SLB', 'TGT', 'USB', 'WFC', 'WMT', 'XOM']
df = sp500_px.loc[sp500_px.index >= '2011-01-01', syms1].transpose()

Z = linkage(df, method='complete')

树状图

层次聚类自然适合以树状图形式展示,称为树状图。该名称源自希腊语单词dendro(树)和gramma(绘制)。在 R 中,你可以轻松地使用 plot 命令生成这个图:

plot(hcl)

我们可以使用 dendrogram 方法绘制 Pythonlinkage 函数的结果:

fig, ax = plt.subplots(figsize=(5, 5))
dendrogram(Z, labels=df.index, ax=ax, color_threshold=0)
plt.xticks(rotation=90)
ax.set_ylabel('distance')

结果显示在 Figure 7-8 中(注意,我们现在绘制的是相互相似的公司,而不是日期)。树的叶子对应记录。树中分支的长度表示相应聚类之间的差异程度。谷歌和亚马逊的回报彼此及其他股票的回报非常不同。石油股票(SLB, CVX, XOM, COP)位于它们自己的聚类中,苹果(AAPL)独立成一类,其余的股票彼此相似。

股票的树状图。

图 7-8. 股票的树状图

K-means 相比,不需要预先指定聚类的数量。在图形上,你可以通过一个水*线上下移动来识别不同数量的聚类;聚类在水*线与垂直线交点处定义。要提取特定数量的聚类,可以使用 cutree 函数:

cutree(hcl, k=4)
GOOGL  AMZN  AAPL  MSFT  CSCO  INTC   CVX   XOM   SLB   COP   JPM   WFC
    1     2     3     3     3     3     4     4     4     4     3     3
  USB   AXP   WMT   TGT    HD  COST
    3     3     3     3     3     3

Python 中,你可以使用 fcluster 方法实现同样的效果:

memb = fcluster(Z, 4, criterion='maxclust')
memb = pd.Series(memb, index=df.index)
for key, item in memb.groupby(memb):
    print(f"{key} : {', '.join(item.index)}")

需要提取的聚类数量设置为 4,你可以看到谷歌和亚马逊各自属于自己的一个聚类。所有的石油股票属于另一个聚类。剩下的股票在第四个聚类中。

凝聚算法

层次聚类的主要算法是凝聚算法,它是通过迭代地合并相似的聚类来实现的。凝聚算法首先将每个记录视为自己单独的聚类,然后逐步构建越来越大的聚类。第一步是计算所有记录对之间的距离。

对于每对记录 ( x 1 , x 2 , ... , x p )( y 1 , y 2 , ... , y p ) ,我们使用距离度量 d x,y 来衡量这两个记录之间的距离(见 “距离度量”)。例如,我们可以使用欧氏距离:

d ( x , y ) = (x 1 -y 1 ) 2 + (x 2 -y 2 ) 2 + + (x p -y p ) 2

现在我们转向簇间距离。考虑两个簇 AB,每个簇都有一组不同的记录,A = ( a 1 , a 2 , ... , a m )B = ( b 1 , b 2 , ... , b q ) 。我们可以通过使用 A 的成员与 B 的成员之间的距离来衡量簇间的不相似性 D ( A , B )

一种衡量不相似性的方法是 complete-linkage 方法,它是 AB 之间所有记录对之间的最大距离。

D ( A , B ) = max d ( a i , b j ) for all pairs i , j

这里定义了两两之间的最大差异作为不相似性的度量。

聚合算法的主要步骤包括:

  1. 创建一个初始的簇集,其中每个簇由数据中的单个记录组成。

  2. 计算所有簇对 D ( C k , C ) 之间的不相似性。

  3. 合并两个最不相似的群集 C kC ,其度量为 D ( C k , C )

  4. 如果有多个群集保留,请返回步骤 2。否则,我们完成了。

不相似度量

有四种常见的不相似度量:完全链接单链接*均链接最小方差。这些(以及其他度量)都由大多数层次聚类软件支持,包括hclustlinkage。前文定义的完全链接方法倾向于产生成员相似的群集。单链接方法是两个群集中记录之间的最小距离:

D ( A , B ) = min d ( a i , b j ) for all pairs i , j

这是一种“贪婪”方法,生成的群集可能包含非常不同的元素。*均链接方法是所有距离对的*均值,代表了单链接方法和完全链接方法之间的折衷。最后,最小方差方法,也称为Ward方法,类似于K-均值,因为它最小化了群内*方和(见“K 均值聚类”)。

图 7-9 利用四种方法对埃克森美孚和雪佛龙的股票收益进行层次聚类。每种方法都保留了四个群集。

应用于股票收益的差异度量比较;x 轴上是埃克森美孚,y 轴上是雪佛龙。

图 7-9。应用于股票数据的差异度量的比较

结果大不相同:单链接度量将几乎所有点分配到一个单一群集中。除了最小方差方法(RWard.DPythonward)之外,所有度量方法最终都至少有一个包含少数异常点的群集。最小方差方法与K-均值群集最为相似;与图 7-5 比较。

Model-Based Clustering

层次聚类和K-means 等聚类方法是基于启发式方法的,主要依赖于找到彼此接*的簇,直接使用数据进行测量(不涉及概率模型)。在过去的 20 年里,人们已经投入了大量精力来开发基于模型的聚类方法。华盛顿大学的 Adrian Raftery 和其他研究人员在模型化聚类方面做出了重要贡献,包括理论和软件两方面。这些技术基于统计理论,并提供了更严谨的方法来确定簇的性质和数量。例如,在可能存在一组记录彼此相似但不一定彼此接*的情况下(例如,具有高收益方差的科技股),以及另一组记录既相似又接*的情况下(例如,波动性低的公用事业股),这些技术可以被使用。

多元正态分布

最广泛使用的基于模型的聚类方法基于多元正态分布。多元正态分布是正态分布对一组p个变量X 1 , X 2 , ... , X p的泛化。该分布由一组均值μ = μ 1 , μ 2 , ... , μ 𝐩和一个协方差矩阵Σ定义。协方差矩阵是变量之间相关程度的度量(有关协方差的详细信息,请参阅“协方差矩阵”)。协方差矩阵Σp个方差σ 1 2 , σ 2 2 , ... , σ p 2和所有变量对σ i,j的协方差组成。将变量放在行上并复制到列上,矩阵看起来像这样:

Σ = σ 1 2 σ 1,2 σ 1,p σ 2,1 σ 2 2 σ 2,p σ p,1 σ p,2 2 σ p 2

注意协方差矩阵在从左上到右下的对角线周围是对称的。由于 σ i,j = σ j,i ,只有 ( p × ( p - 1 ) ) / 2 个协方差项。总之,协方差矩阵有 ( p × ( p - 1 ) ) / 2 + p 个参数。分布表示为:

( X 1 , X 2 , ... , X p ) N p ( μ , Σ )

这是一种符号化的表达方式,表明所有变量都服从正态分布,整体分布由变量均值向量和协方差矩阵完全描述。

图 7-10 显示了两个变量 XY 的多变量正态分布的概率轮廓(例如,0.5 概率轮廓包含分布的 50%)。

均值分别为 μ x = 0 . 5μ y = - 0 . 5 ,协方差矩阵为:

Σ = 1 1 1 2

因为协方差 σ xy 是正的,XY 是正相关的。

images/2d_normal.png

图 7-10. 两维正态分布的概率轮廓

混合正态分布

基于模型的聚类背后的关键思想是,假设每个记录都分布在K个多元正态分布之一中,其中K是聚类的数量。每个分布都有不同的均值μ和协方差矩阵Σ。例如,如果您有两个变量XY,那么每一行( X i , Y i )被建模为从K个多元正态分布N ( μ 1 , Σ 1 ) , N ( μ 2 , Σ 2 ) , ... , N ( μ K , Σ K )中的一个中抽样。

R有一个非常丰富的基于模型的聚类包mclust,最初由 Chris Fraley 和 Adrian Raftery 开发。使用这个包,我们可以将模型-based 聚类应用到之前使用K-means 和层次聚类分析过的股票收益数据中:

> library(mclust)
> df <- sp500_px[row.names(sp500_px) >= '2011-01-01', c('XOM', 'CVX')]
> mcl <- Mclust(df)
> summary(mcl)
Mclust VEE (ellipsoidal, equal shape and orientation) model with 2 components:

 log.likelihood    n df       BIC       ICL
      -2255.134 1131  9 -4573.546 -5076.856

Clustering table:
  1   2
963 168

scikit-learnsklearn.mixture.GaussianMixture类来进行基于模型的聚类:

df = sp500_px.loc[sp500_px.index >= '2011-01-01', ['XOM', 'CVX']]
mclust = GaussianMixture(n_components=2).fit(df)
mclust.bic(df)

如果您执行此代码,您会注意到计算时间明显长于其他程序。使用predict函数提取集群分配,我们可以可视化这些聚类:

cluster <- factor(predict(mcl)$classification)
ggplot(data=df, aes(x=XOM, y=CVX, color=cluster, shape=cluster)) +
  geom_point(alpha=.8)

这是创建类似图形的Python代码:

fig, ax = plt.subplots(figsize=(4, 4))
colors = [f'C{c}' for c in mclust.predict(df)]
df.plot.scatter(x='XOM', y='CVX', c=colors, alpha=0.5, ax=ax)
ax.set_xlim(-3, 3)
ax.set_ylim(-3, 3)

结果图显示在图 7-11 中。有两个聚类:一个在数据中间,另一个在数据外围。这与使用K-means(图 7-5)和层次聚类(图 7-9)获得的紧凑聚类非常不同。

使用+Mclust+获得股票收益数据的两个聚类

图 7-11. 使用mclust获得股票收益数据的两个聚类

您可以使用summary函数提取正态分布的参数:

> summary(mcl, parameters=TRUE)$mean
          [,1]        [,2]
XOM 0.05783847 -0.04374944
CVX 0.07363239 -0.21175715
> summary(mcl, parameters=TRUE)$variance
, , 1
          XOM       CVX
XOM 0.3002049 0.3060989
CVX 0.3060989 0.5496727
, , 2

         XOM      CVX
XOM 1.046318 1.066860
CVX 1.066860 1.915799

Python中,您可以从结果的means_covariances_属性中获取此信息:

print('Mean')
print(mclust.means_)
print('Covariances')
print(mclust.covariances_)

这些分布具有类似的均值和相关性,但第二个分布具有更大的方差和协方差。由于算法的随机性,结果在不同运行之间可能略有不同。

使用mclust生成的聚类可能看起来令人惊讶,但实际上,它展示了该方法的统计特性。基于模型的聚类的目标是找到最佳的多元正态分布集合。股票数据看起来具有正态分布的形状:请参见图 7-10 的轮廓。然而,事实上,股票回报比正态分布具有更长的尾部分布。为了处理这一点,mclust对大部分数据拟合一个分布,然后再拟合一个方差较大的第二个分布。

选择聚类数

K-means 和层次聚类不同,mclustR中(本例中为两个)自动选择聚类数。它通过选择 BIC 值最大的聚类数来完成此操作(BIC 类似于 AIC;请参见“模型选择和逐步回归”)。BIC 通过选择最适合的模型来*衡模型中参数数量的惩罚。在基于模型的聚类中,增加更多的聚类始终会改善拟合,但会引入更多的模型参数。

警告

请注意,在大多数情况下,BIC 通常被最小化。mclust包的作者决定将 BIC 定义为相反的符号,以便更容易地解释图表。

mclust拟合了 14 种不同的模型,并随着成分数量的增加自动选择了一个最优模型。您可以使用mclust中的一个函数绘制这些模型的 BIC 值:

plot(mcl, what='BIC', ask=FALSE)

聚类数或不同多元正态模型(成分)的数量显示在 x 轴上(请参见图 7-12)。

股票回报数据的 14 种模型的 BIC 值随成分数量增加而变化。

图 7-12. 股票回报数据的 14 种模型的 BIC 值随成分数量增加而变化

另一方面,GaussianMixture的实现不会尝试各种组合。如所示,使用Python可以轻松运行多种组合。该实现按照通常的方式定义 BIC。因此,计算出的 BIC 值将为正数,我们需要将其最小化。

results = []
covariance_types = ['full', 'tied', 'diag', 'spherical']
for n_components in range(1, 9):
    for covariance_type in covariance_types:
        mclust = GaussianMixture(n_components=n_components, warm_start=True,
                                 covariance_type=covariance_type) ![1](https://gitee.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-stat-ds/img/1.png)
        mclust.fit(df)
        results.append({
            'bic': mclust.bic(df),
            'n_components': n_components,
            'covariance_type': covariance_type,
        })

results = pd.DataFrame(results)

colors = ['C0', 'C1', 'C2', 'C3']
styles = ['C0-','C1:','C0-.', 'C1--']

fig, ax = plt.subplots(figsize=(4, 4))
for i, covariance_type in enumerate(covariance_types):
    subset = results.loc[results.covariance_type == covariance_type, :]
    subset.plot(x='n_components', y='bic', ax=ax, label=covariance_type,
                kind='line', style=styles[i])

1

使用warm_start参数,计算将重用上一次拟合的信息。这将加快后续计算的收敛速度。

该图类似于用于确定选择K-means 中的聚类数的弯曲图,但所绘制的值是 BIC 而不是解释方差的百分比(参见图 7-7)。一个显著的差异是,mclust 显示了 14 条不同的线!这是因为 mclust 实际上为每个聚类大小拟合了 14 种不同的模型,并最终选择最适合的模型。GaussianMixture 实现的方法较少,因此线的数量只有四条。

mclust 为什么要适配这么多模型来确定最佳的多变量正态分布集?这是因为有多种方法来为拟合模型参数化协方差矩阵 Σ。在大多数情况下,您无需担心模型的细节,可以简单地使用mclust选择的模型。在本例中,根据 BIC,三种不同的模型(称为 VEE、VEV 和 VVE)使用两个分量给出最佳拟合。

注意

基于模型的聚类是一个丰富且快速发展的研究领域,而本文中的覆盖范围仅涉及该领域的一小部分。事实上,mclust 的帮助文件目前长达 154 页。理解模型基础聚类的微妙之处可能比大多数数据科学家遇到的问题所需的工作还要多。

基于模型的聚类技术确实具有一些局限性。这些方法需要对数据的模型假设,而聚类结果非常依赖于该假设。计算要求甚至比层次聚类还要高,使其难以扩展到大数据。最后,该算法比其他方法更复杂,不易访问。

进一步阅读

欲了解更多关于基于模型聚类的详情,请参阅mclustGaussianMixture 文档。

缩放和分类变量

无监督学习技术通常要求数据适当缩放。这与许多回归和分类技术不同,这些技术中缩放并不重要(一个例外是K-最*邻算法;参见“K-最*邻”)。

例如,对于个人贷款数据,变量具有非常不同的单位和数量级。一些变量具有相对较小的值(例如,就业年限),而其他变量具有非常大的值(例如,以美元计的贷款金额)。如果数据未经缩放,则 PCA、K-means 和其他聚类方法将由具有大值的变量主导,并忽略具有小值的变量。

对于某些聚类过程,分类数据可能会带来特殊问题。与K最*邻算法一样,无序因子变量通常会使用独热编码转换为一组二进制(0/1)变量(有关“一热编码器”的更多信息,请参见“一热编码器”)。不仅二进制变量可能与其他数据不同尺度,而且二进制变量仅有两个值的事实可能会在 PCA 和K-means 等技术中带来问题。

缩放变量

变量的尺度和单位差异很大,在应用聚类过程之前需要适当进行标准化。例如,我们来看一下没有进行标准化的贷款违约数据的kmeans应用情况:

defaults <- loan_data[loan_data$outcome=='default',]
df <- defaults[, c('loan_amnt', 'annual_inc', 'revol_bal', 'open_acc',
                   'dti', 'revol_util')]
km <- kmeans(df, centers=4, nstart=10)
centers <- data.frame(size=km$size, km$centers)
round(centers, digits=2)

   size loan_amnt annual_inc revol_bal open_acc   dti revol_util
1    52  22570.19  489783.40  85161.35    13.33  6.91      59.65
2  1192  21856.38  165473.54  38935.88    12.61 13.48      63.67
3 13902  10606.48   42500.30  10280.52     9.59 17.71      58.11
4  7525  18282.25   83458.11  19653.82    11.66 16.77      62.27

下面是相应的Python代码:

defaults = loan_data.loc[loan_data['outcome'] == 'default',]
columns = ['loan_amnt', 'annual_inc', 'revol_bal', 'open_acc',
           'dti', 'revol_util']

df = defaults[columns]
kmeans = KMeans(n_clusters=4, random_state=1).fit(df)
counts = Counter(kmeans.labels_)

centers = pd.DataFrame(kmeans.cluster_centers_, columns=columns)
centers['size'] = [counts[i] for i in range(4)]
centers

变量annual_increvol_bal主导了聚类,而且聚类大小差异很大。聚类 1 只有 52 名成员,收入相对较高且循环信贷余额也较高。

缩放变量的常见方法是通过减去均值并除以标准差来转换它们为z-分数。这称为标准化归一化(有关使用z-分数的更多讨论,请参见“标准化(归一化,z-分数)”):

z = x-x ¯ s

查看当kmeans应用于标准化数据时,聚类发生了什么变化:

df0 <- scale(df)
km0 <- kmeans(df0, centers=4, nstart=10)
centers0 <- scale(km0$centers, center=FALSE,
                 scale=1 / attr(df0, 'scaled:scale'))
centers0 <- scale(centers0, center=-attr(df0, 'scaled:center'), scale=FALSE)
centers0 <- data.frame(size=km0$size, centers0)
round(centers0, digits=2)

  size loan_amnt annual_inc revol_bal open_acc   dti revol_util
1 7355  10467.65   51134.87  11523.31     7.48 15.78      77.73
2 5309  10363.43   53523.09   6038.26     8.68 11.32      30.70
3 3713  25894.07  116185.91  32797.67    12.41 16.22      66.14
4 6294  13361.61   55596.65  16375.27    14.25 24.23      59.61

Python中,我们可以使用scikit-learnStandardScalerinverse_transform方法允许将聚类中心转换回原始尺度:

scaler = preprocessing.StandardScaler()
df0 = scaler.fit_transform(df * 1.0)

kmeans = KMeans(n_clusters=4, random_state=1).fit(df0)
counts = Counter(kmeans.labels_)

centers = pd.DataFrame(scaler.inverse_transform(kmeans.cluster_centers_),
                       columns=columns)
centers['size'] = [counts[i] for i in range(4)]
centers

聚类大小更加*衡,聚类不再由annual_increvol_bal主导,数据中显示出更多有趣的结构。请注意,在前面的代码中,中心被重新缩放到原始单位。如果我们没有进行缩放,结果值将以z-分数的形式呈现,因此解释性会降低。

注意

PCA 也需要缩放。使用z-分数相当于在计算主成分时使用相关矩阵(有关“相关性”请参见“相关性”),而不是协方差矩阵。通常,用于计算 PCA 的软件通常有使用相关矩阵的选项(在R中,princomp函数具有cor参数)。

主导变量

即使变量在同一尺度上测量并准确反映了相对重要性(例如股票价格的变动),有时重新缩放变量也可能很有用。

假设我们在“解释主成分”中增加了 Google(GOOGL)和 Amazon(AMZN)的分析。我们来看一下下面R中是如何实现的:

syms <- c('GOOGL', 'AMZN', 'AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM',
          'SLB', 'COP', 'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST')
top_sp1 <- sp500_px[row.names(sp500_px) >= '2005-01-01', syms]
sp_pca1 <- princomp(top_sp1)
screeplot(sp_pca1)

Python中,我们得到的 screeplot 如下:

syms = ['GOOGL', 'AMZN', 'AAPL', 'MSFT', 'CSCO', 'INTC', 'CVX', 'XOM',
        'SLB', 'COP', 'JPM', 'WFC', 'USB', 'AXP', 'WMT', 'TGT', 'HD', 'COST']
top_sp1 = sp500_px.loc[sp500_px.index >= '2005-01-01', syms]

sp_pca1 = PCA()
sp_pca1.fit(top_sp1)

explained_variance = pd.DataFrame(sp_pca1.explained_variance_)
ax = explained_variance.head(10).plot.bar(legend=False, figsize=(4, 4))
ax.set_xlabel('Component')

screeplot 显示了顶级主成分的方差。在这种情况下,图 7-13 中的 screeplot 显示,第一和第二主成分的方差远大于其他成分。这通常表明一个或两个变量主导了载荷。这确实是这个例子的情况:

round(sp_pca1$loadings[,1:2], 3)
      Comp.1 Comp.2
GOOGL  0.781  0.609
AMZN   0.593 -0.792
AAPL   0.078  0.004
MSFT   0.029  0.002
CSCO   0.017 -0.001
INTC   0.020 -0.001
CVX    0.068 -0.021
XOM    0.053 -0.005
...

Python 中,我们使用以下方法:

loadings = pd.DataFrame(sp_pca1.components_[0:2, :], columns=top_sp1.columns)
loadings.transpose()

前两个主成分几乎完全由 GOOGL 和 AMZN 主导。这是因为 GOOGL 和 AMZN 的股价波动主导了变异性。

处理这种情况时,可以选择将它们保留原样,重新缩放变量(参见“缩放变量”),或者将主导变量从分析中排除并单独处理。没有“正确”的方法,处理方法取决于具体应用。

来自标准普尔 500 指数中排名前列股票的 PCA 的 screeplot,包括 GOOGL 和 AMZN。

图 7-13. 来自标准普尔 500 指数中排名前列股票 PCA 的 screeplot,包括 GOOGL 和 AMZN

分类数据和 Gower 距离

对于分类数据,必须将其转换为数值数据,可以通过排名(有序因子)或编码为一组二进制(虚拟)变量来实现。如果数据包含混合连续和二进制变量,通常需要对变量进行缩放,以使范围相似;参见“缩放变量”。一种流行的方法是使用Gower 距离

Gower 距离背后的基本思想是根据数据类型对每个变量应用不同的距离度量:

  • 对于数值变量和有序因子,距离计算为两个记录之间差值的绝对值(曼哈顿距离)。

  • 对于分类变量,如果两个记录之间的类别不同,则距离为 1;如果类别相同,则距离为 0。

Gower 距离的计算方法如下:

  1. 计算每个记录的所有变量对 ij 的距离 d i,j

  2. 缩放每对 d i,j,使最小值为 0,最大值为 1。

  3. 将变量之间的成对缩放距离相加,使用简单或加权*均,创建距离矩阵。

为了说明 Gower 距离,从 R 中的贷款数据中取几行:

> x <- loan_data[1:5, c('dti', 'payment_inc_ratio', 'home_', 'purpose_')]
> x
# A tibble: 5 × 4
    dti payment_inc_ratio   home            purpose
  <dbl>             <dbl> <fctr>             <fctr>
1  1.00           2.39320   RENT                car
2  5.55           4.57170    OWN     small_business
3 18.08           9.71600   RENT              other
4 10.08          12.21520   RENT debt_consolidation
5  7.06           3.90888   RENT              other

R 中的 cluster 包中的函数 daisy 可用于计算 Gower 距离:

library(cluster)
daisy(x, metric='gower')
Dissimilarities :
          1         2         3         4
2 0.6220479
3 0.6863877 0.8143398
4 0.6329040 0.7608561 0.4307083
5 0.3772789 0.5389727 0.3091088 0.5056250

Metric :  mixed ;  Types = I, I, N, N
Number of objects : 5

在撰写本文时,Gower 距离尚未包含在任何流行的 Python 包中。然而,正在进行的工作包括将其包含在 scikit-learn 中。一旦实施完成,我们将更新相应的源代码。

所有距离介于 0 和 1 之间。距离最大的记录对是 2 和 3:它们的homepurpose值不同,并且它们的dti(负债收入比)和payment_inc_ratio(支付收入比)水*非常不同。记录 3 和 5 的距离最小,因为它们的homepurpose值相同。

可以将从 daisy 计算出的 Gower 距离矩阵传递给 hclust 进行层次聚类(参见 “Hierarchical Clustering”):

df <- defaults[sample(nrow(defaults), 250),
               c('dti', 'payment_inc_ratio', 'home', 'purpose')]
d = daisy(df, metric='gower')
hcl <- hclust(d)
dnd <- as.dendrogram(hcl)
plot(dnd, leaflab='none')

结果显示的树状图如 图 7-14 所示。个体记录在 x 轴上无法区分,但我们可以在 0.5 处水*切割树状图,并使用以下代码检查某个子树中的记录:

dnd_cut <- cut(dnd, h=0.5)
df[labels(dnd_cut$lower[[1]]),]
        dti payment_inc_ratio home_           purpose_
44532 21.22           8.37694   OWN debt_consolidation
39826 22.59           6.22827   OWN debt_consolidation
13282 31.00           9.64200   OWN debt_consolidation
31510 26.21          11.94380   OWN debt_consolidation
6693  26.96           9.45600   OWN debt_consolidation
7356  25.81           9.39257   OWN debt_consolidation
9278  21.00          14.71850   OWN debt_consolidation
13520 29.00          18.86670   OWN debt_consolidation
14668 25.75          17.53440   OWN debt_consolidation
19975 22.70          17.12170   OWN debt_consolidation
23492 22.68          18.50250   OWN debt_consolidation

此子树完全由贷款目的标记为“债务合并”的所有者组成。尽管严格分离并非所有子树的特点,但这说明了分类变量倾向于在聚类中被组合在一起。

应用于混合变量类型贷款违约数据样本的  的树状图。

图 7-14. 应用于混合变量类型贷款违约数据样本的 hclust 的树状图

混合数据的聚类问题

K-means 和 PCA 最适合连续变量。对于较小的数据集,最好使用带有 Gower 距离的层次聚类。原则上,K-means 也可以应用于二进制或分类数据。通常会使用“独热编码器”表示法(参见 “One Hot Encoder”)将分类数据转换为数值。然而,在实践中,使用 K-means 和 PCA 处理二进制数据可能会比较困难。

如果使用标准的 z-分数,二进制变量将主导聚类的定义。这是因为 0/1 变量仅取两个值,K-means 可以通过将所有取值为 0 或 1 的记录分配到单个聚类中获得较小的簇内*方和。例如,在包括因子变量 homepub_rec_zero 的贷款违约数据中应用 kmeans,如下所示的 R 代码:

df <- model.matrix(~ -1 + dti + payment_inc_ratio + home_ + pub_rec_zero,
                   data=defaults)
df0 <- scale(df)
km0 <- kmeans(df0, centers=4, nstart=10)
centers0 <- scale(km0$centers, center=FALSE,
                 scale=1/attr(df0, 'scaled:scale'))
round(scale(centers0, center=-attr(df0, 'scaled:center'), scale=FALSE), 2)

    dti payment_inc_ratio home_MORTGAGE home_OWN home_RENT pub_rec_zero
1 17.20              9.27          0.00        1      0.00         0.92
2 16.99              9.11          0.00        0      1.00         1.00
3 16.50              8.06          0.52        0      0.48         0.00
4 17.46              8.42          1.00        0      0.00         1.00

Python 中:

columns = ['dti', 'payment_inc_ratio', 'home_', 'pub_rec_zero']
df = pd.get_dummies(defaults[columns])

scaler = preprocessing.StandardScaler()
df0 = scaler.fit_transform(df * 1.0)
kmeans = KMeans(n_clusters=4, random_state=1).fit(df0)
centers = pd.DataFrame(scaler.inverse_transform(kmeans.cluster_centers_),
                       columns=df.columns)
centers

前四个聚类实质上是因子变量不同水*的代理。为了避免这种行为,可以将二进制变量缩放到比其他变量具有更小的方差。或者,对于非常大的数据集,可以将聚类应用于具有特定分类值的数据子集。例如,可以单独将放贷给有抵押贷款、全额拥有房屋或租房的人士的贷款进行聚类。

总结

对于数值数据的降维,主要工具是主成分分析或 K-means 聚类。两者都需要注意数据的适当缩放,以确保有意义的数据降维。

对于具有高度结构化数据且簇之间分离良好的聚类,所有方法可能会产生类似的结果。每种方法都有其优势。K-means 可扩展到非常大的数据并且易于理解。层次聚类可以应用于混合数据类型——数值和分类数据,并且适合直观显示(树状图)。基于模型的聚类建立在统计理论之上,提供了更严格的方法,与启发式方法相对。然而,对于非常大的数据集,K-means 是主要的方法。

在存在噪声数据的情况下,如贷款和股票数据(以及数据科学家将面对的大部分数据),选择更加明显。K-means、层次聚类,特别是基于模型的聚类都会产生非常不同的解决方案。数据科学家该如何操作?不幸的是,没有简单的经验法则可以指导选择。最终使用的方法将取决于数据规模和应用的目标。

^(1) 本章及后续章节内容 © 2020 Datastats, LLC, Peter Bruce, Andrew Bruce, and Peter Gedeck;已获得授权使用。

posted @ 2024-06-17 17:12  绝不原创的飞龙  阅读(287)  评论(0)    收藏  举报