Python 数据清理秘籍第二版(二)
原文:
annas-archive.org/md5/2774e54b23314a6bebe51d6caf9cd592译者:飞龙
第三章:测量你的数据
在收到新数据集的一周内,至少会有人问我们一个熟悉的问题——“那么,看起来怎么样?”这个问题并不总是以轻松的语气提出,别人通常也不太兴奋地听我们已经发现的所有警告信号。可能会有一种紧迫感,希望宣告数据已经准备好进行分析。当然,如果我们太快地签字确认,这可能会带来更大的问题;无效结果的展示、变量关系的误解,以及不得不重新做大部分分析。关键是要在探索数据的其他部分之前,理清我们需要了解的数据内容。本章中的技巧提供了判断数据是否足够清晰以开始分析的方法,即使我们不能说“看起来很好”,至少我们可以说“我很确定我已经识别出主要问题,问题在这里。”
我们的领域知识通常非常有限,或者至少不如那些创建数据的人那么熟练。即使我们对数据中的个体或事件了解不多,我们也必须迅速了解我们所面对的数据内容。很多时候(对我们中的一些人来说,几乎是大多数时候)并没有类似数据字典或代码书这样的东西来帮助我们理解数据。
快速地问问自己,在这种情况下你首先想弄清楚的事情是什么;也就是说,当你获得一些你知道很少的数据时,首先要弄清楚的可能是这样的事情:
-
数据集的行是如何被唯一标识的?(分析单元是什么?)
-
数据集中有多少行和列?
-
关键的分类变量是什么?每个值的频率是多少?
-
重要的连续变量是如何分布的?
-
变量之间可能如何相关——例如,连续变量的分布如何根据数据中的类别而变化?
-
哪些变量的值超出了预期范围,缺失值是如何分布的?
本章介绍了回答前四个问题的基本工具和策略。接下来的章节我们将讨论最后两个问题。
我必须指出,尽管数据结构已经很熟悉,但对数据的第一次处理仍然很重要。例如,当我们收到同一列名和数据类型的新月度或年度数据时,很容易产生一种错误的感觉,认为我们可以直接重新运行之前的程序;我们很难像第一次处理数据时那样保持警觉。大多数人可能都有过这种经历:我们收到结构相同的新数据,但之前问题的答案却有了实质性变化:关键类别变量的新有效值;一直允许但在几期内未曾出现的稀有值;以及客户/学生/顾客状态的意外变化。建立理解数据的例程并始终遵循它们是非常重要的,不论我们是否对数据熟悉。
本章将重点介绍以下主题:
-
初步了解你的数据
-
选择和组织列
-
选择行
-
为分类变量生成频率
-
生成连续变量的汇总统计
-
使用生成性 AI 显示描述性统计数据
技术要求
本章中的食谱需要 pandas、Numpy 和 Matplotlib 库。我使用的是 pandas 2.1.4,但代码也可以在 pandas 1.5.3 或更高版本上运行。
本章中的代码可以从本书的 GitHub 仓库下载,github.com/PacktPublishing/Python-Data-Cleaning-Cookbook-Second-Edition。
初步了解你的数据
本章我们将处理两个数据集:1997 年度国家纵向青少年调查,这是美国政府进行的一项调查,跟踪了同一群体从 1997 年到 2023 年的情况;以及来自 Our World in Data 的各国 COVID-19 案例和死亡人数数据。
准备工作…
本食谱主要使用 pandas 库。我们将使用 pandas 工具更深入地了解国家纵向调查(NLS)和 COVID-19 案例数据。
数据说明
青少年 NLS 调查由美国劳工统计局进行。该调查从 1997 年开始,针对 1980 年至 1985 年之间出生的一群人,进行每年跟踪,直到 2023 年。本食谱中,我从调查中的数百个数据项中提取了关于成绩、就业、收入和对政府态度的 89 个变量。SPSS、Stata 和 SAS 的独立文件可以从仓库下载。NLS 数据可以从 www.nlsinfo.org 下载。你需要创建一个研究者账户来下载数据,但无需收费。
我们的《全球数据》提供了 COVID-19 的公共使用数据,网址是 ourworldindata.org/covid-cases。该数据集包括总病例数、死亡病例数、已做的检测、医院床位数以及人口统计数据,如中位年龄、国内生产总值和人类发展指数,后者是衡量生活水平、教育水平和寿命的综合指标。本文所用的数据集于 2024 年 3 月 3 日下载。
如何操作…
我们将初步查看 NLS 和 COVID-19 数据,包括行数、列数和数据类型:
-
导入所需的库并加载数据框:
import pandas as pd import numpy as np nls97 = pd.read_csv("data/nls97.csv") covidtotals = pd.read_csv("data/covidtotals.csv", ... parse_dates=['lastdate']) -
设置并显示
nls97数据的索引和大小。
另外,检查索引值是否唯一:
nls97.set_index("personid", inplace=True)
nls97.index
Index([100061, 100139, 100284, 100292, 100583, 100833, ...
999543, 999698, 999963],
dtype='int64', name='personid', length=8984)
nls97.shape
(8984, 88)
nls97.index.nunique()
8984
-
显示数据类型和
non-null值的计数:nls97.info()<class 'pandas.core.frame.DataFrame'> Int64Index: 8984 entries, 100061 to 999963 Data columns (total 88 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 gender 8984 non-null object 1 birthmonth 8984 non-null int64 2 birthyear 8984 non-null int64 3 highestgradecompleted 6663 non-null float64 4 maritalstatus 6672 non-null object 5 childathome 4791 non-null float64 6 childnotathome 4791 non-null float64 7 wageincome 5091 non-null float64 8 weeklyhrscomputer 6710 non-null object 9 weeklyhrstv 6711 non-null object 10 nightlyhrssleep 6706 non-null float64 11 satverbal 1406 non-null float64 12 satmath 1407 non-null float64 ... 83 colenroct15 7469 non-null object 84 colenrfeb16 7036 non-null object 85 colenroct16 6733 non-null object 86 colenrfeb17 6733 non-null object 87 colenroct17 6734 non-null object dtypes: float64(29), int64(2), object(57) memory usage: 6.1+ MB -
显示
nls97数据的前两行。
使用转置来显示更多输出:
nls97.head(2).T
personid 100061 100139
gender Female Male
birthmonth 5 9
birthyear 1980 1983
highestgradecompleted 13 12
maritalstatus Married Married
... ... ...
colenroct15 1\. Not enrolled 1\. Not enrolled
colenrfeb16 1\. Not enrolled 1\. Not enrolled
colenroct16 1\. Not enrolled 1\. Not enrolled
colenrfeb17 1\. Not enrolled 1\. Not enrolled
colenroct17 1\. Not enrolled 1\. Not enrolled
- 设置并显示 COVID-19 数据的索引和大小。
另外,检查索引值是否唯一:
covidtotals.set_index("iso_code", inplace=True)
covidtotals.index
Index(['AFG', 'ALB', 'DZA', 'ASM', 'AND', 'AGO', 'AIA', 'ATG', 'ARG',
'ARM',
...
'URY', 'UZB', 'VUT', 'VAT', 'VEN', 'VNM', 'WLF', 'YEM', 'ZMB',
'ZWE'],
dtype='object', name='iso_code', length=231)
covidtotals.shape
(231, 16)
covidtotals.index.nunique()
231
-
显示数据类型和
non-null值的计数:covidtotals.info()<class 'pandas.core.frame.DataFrame'> Index: 231 entries, AFG to ZWE Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 lastdate 231 non-null datetime64[ns] 1 location 231 non-null object 2 total_cases 231 non-null float64 3 total_deaths 231 non-null float64 4 total_cases_pm 231 non-null float64 5 total_deaths_pm 231 non-null float64 6 population 231 non-null int64 7 pop_density 209 non-null float64 8 median_age 194 non-null float64 9 gdp_per_capita 191 non-null float64 10 hosp_beds 170 non-null float64 11 vac_per_hund 13 non-null float64 12 aged_65_older 188 non-null float64 13 life_expectancy 227 non-null float64 14 hum_dev_ind 187 non-null float64 15 region 231 non-null object dtypes: datetime64ns, float64(12), int64(1), object(2) memory usage: 38.8+ KB -
显示 COVID-19 数据的两行样本:
covidtotals.sample(2, random_state=1).Tiso_code GHA NIU lastdate 2023-12-03 2023-12-31 location Ghana Niue total_cases 171,834 993 total_deaths 1,462 0 total_cases_pm 5,133 508,709 total_deaths_pm 44 0 population 33475870 1952 pop_density 127 NaN median_age 21 NaN gdp_per_capita 4,228 NaN hosp_beds 1 NaN vac_per_hund NaN NaN aged_65_older 3 NaN life_expectancy 64 74 hum_dev_ind 1 NaN region West Africa Oceania / Aus
这为我们理解数据框提供了良好的基础,包括它们的大小和列数据类型。
它是如何工作的…
在步骤 2中,我们为 nls97 数据框设置并显示了索引 personid。它是一个比默认的 pandas RangeIndex 更有意义的索引,后者本质上是从零开始的行号。通常在处理个体作为分析单元时,会有一个唯一标识符,这是一个很好的索引候选。它使得通过该标识符选择一行变得更加简单。我们不需要使用 nls97.loc[personid==1000061] 语句来获取该人的数据行,而是可以使用 nls97.loc[1000061]。我们将在下一个示例中尝试这一方法。
pandas 使得查看每列的行数和列数、数据类型、非缺失值的数量,以及数据中前几行的列值变得容易。这可以通过使用 shape 属性和调用 info 方法实现,接着使用 head 或 sample 方法。使用 head(2) 方法显示前两行,但有时从数据框中任意一行获取数据会更有帮助,这时我们可以使用 sample。 (当我们调用 sample 时,我们设置了种子(random_state=1),这样每次运行代码时都会得到相同的结果。)我们可以将对 head 或 sample 的调用链式调用 T 来转置数据框。这将反转行和列的显示顺序。当列的数量比水平方向上能显示的更多时,这个操作很有用,你可以通过转置查看所有列。通过转置行和列,我们能够看到所有的列。
nls97 DataFrame 的shape属性告诉我们该数据集有 8,984 行和 88 列非索引列。由于personid是索引,因此不包含在列数中。info方法显示,许多列的数据类型是对象型,且部分列有大量缺失值。satverbal和satmath只有大约 1,400 个有效值。
covidtotals DataFrame 的shape属性告诉我们该数据集有 231 行和 16 列,其中不包括作为索引使用的国家iso_code列(iso_code是每个国家的唯一三位数标识符)。对于我们进行的大部分分析,关键变量是total_cases、total_deaths、total_cases_pm和total_deaths_pm。total_cases和total_deaths对每个国家都有数据,但total_cases_pm和total_deaths_pm在一个国家的数据缺失。
还有更多...
我发现,在处理数据文件时,考虑索引能提醒我分析的单位。这在 NLS 数据中并不明显,因为它实际上是伪装成个人级数据的面板数据。面板数据,或称纵向数据,包含同一组个体在一段时间内的数据。在这种情况下,数据收集的时间跨度为 26 年,从 1997 年到 2023 年。调查管理员为了分析方便,通过创建某些年份响应的列(例如,大学入学情况(colenroct15至colenroct17))将数据进行了平展。这是一个相对标准的做法,但我们可能需要对某些分析进行重新整形。
我在接收任何面板数据时特别注意的是关键变量在时间上的响应下降。注意从colenroct15到colenroct17有效值的下降。到 2017 年 10 月,只有 75%的受访者提供了有效回应(6,734/8,984)。在后续分析中,必须牢记这一点,因为剩余的 6,734 名受访者可能在重要方面与整体样本 8,984 人有所不同。
另见
第一章中的一个食谱,在导入表格数据时预测数据清理问题(使用 pandas),展示了如何将 pandas DataFrame 保存为 feather 或 pickle 文件。在本章后续的食谱中,我们将查看这两个 DataFrame 的描述性统计和频率分析。
我们在第十一章中对 NLS 数据进行了整形,数据整理与重塑,恢复了其作为面板数据的实际结构。这对于生存分析等统计方法是必要的,也更接近整洁数据的理想状态。
选择和组织列
在本食谱中,我们探索了几种从 DataFrame 中选择一个或多个列的方法。我们可以通过将列名列表传递给[]括号操作符,或者使用 pandas 特定的loc和iloc数据访问器来选择列。
在清理数据或进行探索性分析或统计分析时,专注于与当前问题或分析相关的变量是非常有帮助的。这使得根据列之间的实质性或统计关系对列进行分组,或在任何时候限制我们正在研究的列变得非常重要。我们有多少次对自己说过类似 “为什么变量 A 在变量 B 为 y 时的值是 x?” 的话呢?只有当我们查看的数据量在某一时刻不超过我们当时的感知能力时,我们才能做到这一点。
准备工作……
在本食谱中,我们将继续使用国家纵向调查(NLS)数据。
如何做到……
我们将探索几种选择列的方法:
- 导入
pandas库并将 NLS 数据加载到 pandas 中。
同时,将 NLS 数据中所有对象数据类型的列转换为类别数据类型。通过使用 select_dtypes 选择对象数据类型的列,并利用 transform 及 lambda 函数将数据类型转换为 category 来实现:
import pandas as pd
import numpy as np
nls97 = pd.read_csv("data/nls97.csv")
nls97.set_index("personid", inplace=True)
nls97[nls97.select_dtypes(['object']).columns] = \
nls97.select_dtypes(['object']). \
transform(lambda x: x.astype('category'))
- 使用 pandas 的
[]括号操作符和loc以及iloc访问器选择列。
我们将一个与列名匹配的字符串传递给括号操作符,从而返回一个 pandas Series。如果我们传入一个包含该列名的单一元素的列表(nls97[['gender']]),则返回一个 DataFrame。我们还可以使用 loc 和 iloc 访问器来选择列:
analysisdemo = nls97['gender']
type(analysisdemo)
<class 'pandas.core.series.Series'>
analysisdemo = nls97[['gender']]
type(analysisdemo)
<class 'pandas.core.frame.DataFrame'>
analysisdemo = nls97.loc[:,['gender']]
type(analysisdemo)
<class 'pandas.core.frame.DataFrame'>
analysisdemo = nls97.iloc[:,[0]]
type(analysisdemo)
<class 'pandas.core.frame.DataFrame'>
- 从 pandas DataFrame 中选择多个列。
使用括号操作符和 loc 选择几个列:
analysisdemo = nls97[['gender','maritalstatus',
... 'highestgradecompleted']]
analysisdemo.shape
(8984, 3)
analysisdemo.head()
gender maritalstatus highestgradecompleted
personid
100061 Female Married 13
100139 Male Married 12
100284 Male Never-married 7
100292 Male NaN nan
100583 Male Married 13
analysisdemo = nls97.loc[:,['gender','maritalstatus',
... 'highestgradecompleted']]
analysisdemo.shape
(8984, 3)
analysisdemo.head()
gender maritalstatus highestgradecompleted
personid
100061 Female Married 13
100139 Male Married 12
100284 Male Never-married 7
100292 Male NaN nan
100583 Male Married 13
- 基于列名列表选择多个列。
如果你选择的列超过几个,最好单独创建一个列名列表。在这里,我们创建了一个用于分析的关键变量 keyvars 列表:
keyvars = ['gender','maritalstatus',
... 'highestgradecompleted','wageincome',
... 'gpaoverall','weeksworked17','colenroct17']
analysiskeys = nls97[keyvars]
analysiskeys.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8984 entries, 100061 to 999963
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 gender 8984 non-null category
1 maritalstatus 6672 non-null category
2 highestgradecompleted 6663 non-null float64
3 wageincome 5091 non-null float64
4 gpaoverall 6004 non-null float64
5 weeksworked17 6670 non-null float64
6 colenroct17 6734 non-null category
dtypes: category(3), float64(4)
memory usage: 377.7 KB
- 通过列名过滤选择一个或多个列。
使用 filter 操作符选择所有的 weeksworked## 列:
analysiswork = nls97.filter(like="weeksworked")
analysiswork.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8984 entries, 100061 to 999963
Data columns (total 18 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 weeksworked00 8603 non-null float64
1 weeksworked01 8564 non-null float64
2 weeksworked02 8556 non-null float64
3 weeksworked03 8490 non-null float64
4 weeksworked04 8458 non-null float64
5 weeksworked05 8403 non-null float64
6 weeksworked06 8340 non-null float64
7 weeksworked07 8272 non-null float64
8 weeksworked08 8186 non-null float64
9 weeksworked09 8146 non-null float64
10 weeksworked10 8054 non-null float64
11 weeksworked11 7968 non-null float64
12 weeksworked12 7747 non-null float64
13 weeksworked13 7680 non-null float64
14 weeksworked14 7612 non-null float64
15 weeksworked15 7389 non-null float64
16 weeksworked16 7068 non-null float64
17 weeksworked17 6670 non-null float64
dtypes: float64(18)
memory usage: 1.3 MB
- 选择所有类别数据类型的列。
使用 select_dtypes 方法按数据类型选择列:
analysiscats = nls97.select_dtypes(include=["category"])
analysiscats.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8984 entries, 100061 to 999963
Data columns (total 57 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 gender 8984 non-null category
1 maritalstatus 6672 non-null category
2 weeklyhrscomputer 6710 non-null category
3 weeklyhrstv 6711 non-null category
4 highestdegree 8953 non-null category
...
49 colenrfeb14 7624 non-null category
50 colenroct14 7469 non-null category
51 colenrfeb15 7469 non-null category
52 colenroct15 7469 non-null category
53 colenrfeb16 7036 non-null category
54 colenroct16 6733 non-null category
55 colenrfeb17 6733 non-null category
56 colenroct17 6734 non-null category
dtypes: category(57)
memory usage: 580.0 KB
- 使用列名列表组织列。
使用列表组织 DataFrame 中的列。通过这种方式,你可以轻松地更改列的顺序或排除一些列。在这里,我们将 demoadult 列表中的列移到前面:
demo = ['gender','birthmonth','birthyear']
highschoolrecord = ['satverbal','satmath','gpaoverall',
... 'gpaenglish','gpamath','gpascience']
govresp = ['govprovidejobs','govpricecontrols',
... 'govhealthcare','govelderliving','govindhelp',
... 'govunemp','govincomediff','govcollegefinance',
... 'govdecenthousing','govprotectenvironment']
demoadult = ['highestgradecompleted','maritalstatus',
... 'childathome','childnotathome','wageincome',
... 'weeklyhrscomputer','weeklyhrstv','nightlyhrssleep',
... 'highestdegree']
weeksworked = ['weeksworked00','weeksworked01',
... 'weeksworked02','weeksworked03','weeksworked04',
...
'weeksworked14','weeksworked15','weeksworked16',
... 'weeksworked17']
colenr = ['colenrfeb97','colenroct97','colenrfeb98',
... 'colenroct98','colenrfeb99','colenroct99',
.
... 'colenrfeb15','colenroct15','colenrfeb16',... 'colenroct16','colenrfeb17','colenroct17']
-
创建新的重新组织后的 DataFrame:
nls97 = nls97[demoadult + demo + highschoolrecord + \ ... govresp + weeksworked + colenr] nls97.dtypeshighestgradecompleted float64 maritalstatus category childathome float64 childnotathome float64 wageincome float64 ... colenroct15 category colenrfeb16 category colenroct16 category colenrfeb17 category colenroct17 category Length: 88, dtype: object
上述步骤展示了如何在 pandas DataFrame 中选择列并更改列的顺序。
它是如何工作的……
[] 括号操作符和 loc 数据访问器在选择和组织列时非常方便。当传入一个列名列表时,它们都会返回一个 DataFrame,列的顺序将按照传入的列名列表进行排列。
在步骤 1中,我们使用nls97.select_dtypes(['object'])来选择数据类型为对象的列,并将其与transform和lambda函数(transform(lambda x: x.astype('category')))链式调用,将这些列转换为类别类型。我们使用loc访问器只更新数据类型为对象的列(nls97.loc[:, nls97.dtypes == 'object'])。我们将在第六章中详细讲解transform、apply(与transform类似)和lambda函数的使用,清理和探索数据操作。
我们在步骤 6中通过数据类型选择列。select_dtypes在将列传递给如describe或value_counts等方法时非常有用,尤其是当你想将分析限制为连续变量或类别变量时。
在步骤 8中,当使用括号操作符时,我们将六个不同的列表连接起来。这将demoadult中的列名移到前面,并根据这六个组重新组织所有列。现在,我们的 DataFrame 列中有了清晰的高中记录和工作周数部分。
还有更多…
我们还可以使用select_dtypes来排除数据类型。如果我们只对info结果感兴趣,我们可以将select_dtypes与info方法链式调用:
nls97.select_dtypes(exclude=["category"]).info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8984 entries, 100061 to 999963
Data columns (total 31 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 highestgradecompleted 6663 non-null float64
1 childathome 4791 non-null float64
2 childnotathome 4 791 non-null float64
3 wageincome 5091 non-null float64
4 nightlyhrssleep 6706 non-null float64
5 birthmonth 8984 non-null int64
6 birthyear 8984 non-null int64
...
25 weeksworked12 7747 non-null float64
26 weeksworked13 7680 non-null float64
27 weeksworked14 7612 non-null float64
28 weeksworked15 7389 non-null float64
29 weeksworked16 7068 non-null float64
30 weeksworked17 6670 non-null float64
dtypes: float64(29), int64(2)
memory usage: 2.2 MB
filter操作符也可以接受正则表达式。例如,你可以返回列名中包含income的列:
nls97.filter(regex='income')
wageincome govincomediff
personid
100061 12,500 NaN
100139 120,000 NaN
100284 58,000 NaN
100292 nan NaN
100583 30,000 NaN
... ... ...
999291 35,000 NaN
999406 116,000 NaN
999543 nan NaN
999698 nan NaN
999963 50,000 NaN
另见
许多这些技巧也可以用于创建pandas的 Series 以及 DataFrame。我们在第六章中演示了这一点,清理和探索数据操作。
选择行
当我们在衡量数据并回答问题*“它看起来怎么样?”*时,我们不断地放大和缩小,查看汇总数据和特定行。但也有一些只有在中等缩放级别下才能明显看到的数据问题,只有当我们查看某些行的子集时,这些问题才会浮现。本篇食谱展示了如何使用pandas工具在数据的子集上检测数据问题。
准备就绪...
在本篇食谱中,我们将继续使用 NLS 数据。
如何操作...
我们将讨论几种选择pandas DataFrame 中行的技巧:
-
导入
pandas和numpy,并加载nls97数据:import pandas as pd import numpy as np nls97 = pd.read_csv("data/nls97.csv") nls97.set_index("personid", inplace=True) -
使用切片从第 1001 行开始,到第 1004 行结束。
nls97[1000:1004]选择从左侧冒号指示的整数所在的行(此例中是1000)开始,到右侧冒号指示的整数所在的行(此例中是1004)之前的行。由于基于零索引,1000行实际上是第 1001 行。每一行都作为列出现在输出中,因为我们对结果 DataFrame 进行了转置:
nls97[1000:1004].T
personid 195884 195891 195970 195996
gender Male Male Female Female
birthmonth 12 9 3 9
birthyear 1981 1980 1982 1980
highestgradecompleted NaN 12 17 NaN
maritalstatus NaN Never-married Never-married NaN
... ... ... ... ...colenroct15 NaN 1\. Not enrolled 1\. Not enrolled NaN
colenrfeb16 NaN 1\. Not enrolled 1\. Not enrolled NaN
colenroct16 NaN 1\. Not enrolled 1\. Not enrolled NaN
colenrfeb17 NaN 1\. Not enrolled 1\. Not enrolled NaN
colenroct17 NaN 1\. Not enrolled 1\. Not enrolled NaN
- 使用切片从第 1001 行开始,到第 1004 行结束,跳过每隔一行。
第二个冒号后的整数(在这里是2)表示步长。当步长被省略时,它默认是 1。注意,设置步长为2时,我们跳过了每隔一行的行:
nls97[1000:1004:2].T
personid 195884 195970
gender Male Female
birthmonth 12 3
birthyear 1981 1982
highestgradecompleted NaN 17
maritalstatus NaN Never-married
... ... ...
colenroct15 NaN 1\. Not enrolled
colenrfeb16 NaN 1\. Not enrolled
colenroct16 NaN 1\. Not enrolled
colenrfeb17 NaN 1\. Not enrolled
colenroct17 NaN 1\. Not enrolled
- 使用
[]操作符切片选择前三行。
在[:3]中不提供冒号左侧的值,意味着我们告诉操作符从 DataFrame 的起始位置获取行:
nls97[:3].T
personid 100061 100139 100284
gender Female Male Male
birthmonth 5 9 11
birthyear 1980 1983 1984
... ... ... ...
colenroct15 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenrfeb16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenroct16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenrfeb17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenroct17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
请注意,nls97[:3]返回的 DataFrame 与nls97.head(3)返回的是相同的。
-
使用
[]操作符切片选择最后三行:nls97[-3:].Tpersonid 999543 999698 999963 gender Female Female Female birthmonth 8 5 9 birthyear 1984 1983 1982 ... ... ... ... colenroct15 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled colenrfeb16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled colenroct16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled colenrfeb17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled colenroct17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
请注意,nls97[-3:]返回的 DataFrame 与nls97.tail(3)返回的是相同的。
- 使用
loc数据访问器选择几行。
使用loc访问器按index标签选择。我们可以传递一个索引标签的列表,或者指定一个标签范围。(回顾一下,我们已经将personid设置为索引。)请注意,nls97.loc[[195884,195891,195970]]和nls97.loc[195884:195970]返回的是相同的 DataFrame,因为这些行是连续的。
nls97.loc[[195884,195891,195970]].T
personid 195884 195891 195970
gender Male Male Female
birthmonth 12 9 3
birthyear 1981 1980 1982
highestgradecompleted NaN 12 17
maritalstatus NaN Never-married Never-married
... ... ... ...
colenroct15 NaN 1\. Not enrolled 1\. Not enrolled
colenrfeb16 NaN 1\. Not enrolled 1\. Not enrolled
colenroct16 NaN 1\. Not enrolled 1\. Not enrolled
colenrfeb17 NaN 1\. Not enrolled 1\. Not enrolled
colenroct17 NaN 1\. Not enrolled 1\. Not enrolled
nls97.loc[195884:195970].T
personid 195884 195891 195970
gender Male Male Female
birthmonth 12 9 3
birthyear 1981 1980 1982
highestgradecompleted NaN 12 17
maritalstatus NaN Never-married Never-married
... ... ... ...
colenroct15 NaN 1\. Not enrolled 1\. Not enrolled
colenrfeb16 NaN 1\. Not enrolled 1\. Not enrolled
colenroct16 NaN 1\. Not enrolled 1\. Not enrolled
colenrfeb17 NaN 1\. Not enrolled 1\. Not enrolled
colenroct17 NaN 1\. Not enrolled 1\. Not enrolled
- 使用
iloc数据访问器从 DataFrame 的开始位置选择一行。
iloc与loc的不同之处在于,它接受一组行位置的整数,而不是索引标签。因此,它的工作方式类似于括号操作符切片。在这一步中,我们首先传递一个包含值0的单元素列表。这将返回一个包含第一行的 DataFrame:
nls97.iloc[[0]].T
personid 100061
gender Female
birthmonth 5
birthyear 1980
highestgradecompleted 13
maritalstatus Married
... ...
colenroct15 1\. Not enrolled
colenrfeb16 1\. Not enrolled
colenroct16 1\. Not enrolled
colenrfeb17 1\. Not enrolled
colenroct17 1\. Not enrolled
- 使用
iloc数据访问器选择数据框的几行。
我们传递一个包含三个元素的列表[0,1,2],以返回nls97的前三行的 DataFrame:
nls97.iloc[[0,1,2]].T
personid 100061 100139 100284
gender Female Male Male
birthmonth 5 9 11
birthyear 1980 1983 1984
... ... ... ...
colenroct15 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenrfeb16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenroct16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenrfeb17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenroct17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
如果我们将[0:3]传递给访问器,结果是一样的。
- 使用
iloc数据访问器从 DataFrame 的末尾选择几行。
使用nls97.iloc[[-3,-2,-1]]来获取 DataFrame 的最后三行:
nls97.iloc[[-3,-2,-1]].T
personid 999543 999698 999963
gender Female Female Female
birthmonth 8 5 9
birthyear 1984 1983 1982
... ... ... ...
colenroct15 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenrfeb16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenroct16 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenrfeb17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
colenroct17 1\. Not enrolled 1\. Not enrolled 1\. Not enrolled
使用nls97.iloc[-3:]也能得到相同的结果。通过不在[-3:]的冒号右侧提供值,我们告诉访问器获取从倒数第三行到 DataFrame 结尾的所有行。
- 使用布尔索引按条件选择多行。
创建一个只包含睡眠时间极少的个体的 DataFrame。约有 5%的调查对象每晚睡眠时间为 4 小时或更少,调查共有 6,706 人回答了该问题。通过nls97.nightlyhrssleep<=4来测试哪些人每晚睡眠 4 小时或更少,这将生成一个True和False值的 pandas 系列,我们将其赋值给sleepcheckbool。将该系列传递给loc访问器以创建一个lowsleep DataFrame。lowsleep大约有我们预期的行数。我们不需要额外的步骤来将布尔系列赋值给变量。这里这样做仅仅是为了说明:
nls97.nightlyhrssleep.quantile(0.05)
4.0
nls97.nightlyhrssleep.count()
6706
sleepcheckbool = nls97.nightlyhrssleep<=4
sleepcheckbool
personid
100061 False
100139 False
100284 False
100292 False
100583 False
...
999291 False
999406 False
999543 False
999698 False
999963 False
Name: nightlyhrssleep, Length: 8984, dtype: bool
lowsleep = nls97.loc[sleepcheckbool]
lowsleep.shape
(364, 88)
- 基于多个条件选择行。
可能有些没有得到充足睡眠的人,也有不少孩子和他们一起生活。使用describe来了解那些有lowsleep的人群中,孩子数量的分布情况。大约四分之一的人有三个或更多孩子。创建一个新的 DataFrame,包含那些nightlyhrssleep为 4 小时或更少,且家中有 3 个或更多孩子的个体。&是 pandas 中的逻辑与运算符,表示只有当两个条件都满足时,行才会被选中:
lowsleep.childathome.describe()
count 293.00
mean 1.79
std 1.40
min 0.00
25% 1.00
50% 2.00
75% 3.00
max 9.00
lowsleep3pluschildren = nls97.loc[(nls97.nightlyhrssleep<=4) & (nls97.childathome>=3)]
lowsleep3pluschildren.shape
(82, 88)
如果我们从lowsleep DataFrame 开始,结果是一样的 – lowsleep3pluschildren = lowsleep.loc[lowsleep.childathome>=3] – 但那样我们就无法展示多个条件的测试。
- 根据多个条件选择行和列。
将条件传递给loc访问器以选择行。还可以传递一个列名的列表来选择列:
lowsleep3pluschildren = nls97.loc[(nls97.nightlyhrssleep<=4) & (nls97.childathome>=3), ['nightlyhrssleep','childathome']]
lowsleep3pluschildren
nightlyhrssleep childathome
personid
119754 4 4
141531 4 5
152706 4 4
156823 1 3
158355 4 4
... ... ...
905774 4 3
907315 4 3
955166 3 3
956100 4 6
991756 4 3
上述步骤展示了在 pandas 中选择行的关键技巧。
它是如何工作的…
在步骤 2到步骤 5中,我们使用了[]方括号运算符来做标准的类似 Python 的切片操作,选择行。这个运算符使得我们可以根据列出或范围的值,轻松选择行。切片表示法的形式为[start:end:step],其中如果没有提供step值,则默认假定为1。当start使用负数时,它表示从 DataFrame 的末尾开始算起的行数。
loc访问器,在步骤 6中使用,根据行索引标签选择行。由于personid是 DataFrame 的索引,我们可以将一个或多个personid值的列表传递给loc访问器,以获得具有这些索引标签的行的 DataFrame。我们也可以将一系列索引标签传递给访问器,这将返回一个包含所有行的 DataFrame,这些行的索引标签介于冒号左边和右边的标签之间(包含这两个标签);因此,nls97.loc[195884:195970]将返回personid在195884和195970之间的行的 DataFrame,包括这两个值。
iloc访问器的工作方式与方括号运算符非常相似。这一点在步骤 7到步骤 9中有所体现。我们可以传递整数列表或使用切片表示法传递一个范围。
pandas 最有价值的功能之一是布尔索引。它使得条件选择行变得非常简单。我们在步骤 10中看到了这一点。一个测试返回一个布尔序列。loc访问器选择所有测试为True的行。实际上,我们并不需要将布尔数据序列赋值给一个变量,再将该变量传递给loc运算符。我们本可以直接将测试条件传递给loc访问器,如nls97.loc[nls97.nightlyhrssleep<=4]。
我们应该仔细查看如何在 步骤 11 中使用 loc 访问器选择行。在 nls97.loc[(nls97.nightlyhrssleep<=4) & (nls97.childathome>=3)] 中,每个条件都被括号括起来。如果省略括号,将会产生错误。& 操作符在标准 Python 中等同于 and,意味着 两个 条件都必须为 True,对应的行才会被选择。如果我们想选择 任一 条件为 True 的行,则会使用 | 来代替 &。
最后,步骤 12 演示了如何在一次调用 loc 访问器时同时选择行和列。选择行的条件出现在逗号前,选择的列出现在逗号后,如以下语句所示:
nls97.loc[(nls97.nightlyhrssleep<=4) & (nls97.childathome>=3), ['nightlyhrssleep','childathome']]
这将返回 nightlyhrssleep 和 childathome 列的所有行,其中每行的 nightlyhrssleep 小于或等于 4,且 childathome 大于或等于 3。
还有更多…
在本食谱中,我们使用了三种不同的工具从 pandas DataFrame 中选择行:[] 方括号操作符、以及两个 pandas 特有的访问器,loc 和 iloc。如果你是 pandas 新手,这可能有点混淆,但只需几个月的使用,你就会明白在不同情况下该使用哪个工具。如果你是带着一定的 Python 和 NumPy 经验来学习 pandas 的,你可能会发现 [] 操作符最为熟悉。然而,pandas 文档建议在生产代码中不要使用 [] 操作符。我已经习惯了仅在从 DataFrame 中选择列时使用该操作符。选择行时,我使用 loc 访问器进行布尔索引或按索引标签选择,使用 iloc 访问器按行号选择行。由于我的工作流程中使用了大量布尔索引,我比其他方法使用 loc 要多得多。
另见
紧接着前面的食谱,选择和组织列,对列选择有更详细的讨论。
为分类变量生成频率
多年前,一位经验丰富的研究人员对我说,“我们要找到的 90%的内容,都会在频率分布中看到。” 这句话一直深深印在我心中。通过对 DataFrame 做更多的一维和二维频率分布(交叉表),我对其理解也越来越深刻。在本食谱中,我们将进行一维分布,之后的食谱将介绍交叉表。
准备好…
我们将继续处理 NLS 数据集。我们还会使用过滤方法进行大量的列选择。虽然不必重新审视本章关于列选择的食谱,但它可能会有所帮助。
如何操作…
我们使用 pandas 工具生成频率,尤其是非常方便的 value_counts:
- 加载
pandas库和nls97文件。
此外,将对象数据类型的列转换为类别数据类型:
import pandas as pd
nls97 = pd.read_csv("data/nls97.csv")
nls97.set_index("personid", inplace=True)
nls97[nls97.select_dtypes(['object']).columns] = \
nls97.select_dtypes(['object']). \
transform(lambda x: x.astype('category'))
- 显示类别数据类型列的名称,并检查缺失值的数量。
请注意,gender 列没有缺失值,highestdegree 只有少数缺失值,但 maritalstatus 和其他列有许多缺失值:
catcols = nls97.select_dtypes(include=["category"]).columns
nls97[catcols].isnull().sum()
gender 0
maritalstatus 2312
weeklyhrscomputer 2274
weeklyhrstv 2273
highestdegree 31
...
colenroct15 1515
colenrfeb16 1948
colenroct16 2251
colenrfeb17 2251
colenroct17 2250
Length: 57, dtype: int64
-
显示婚姻状况的频率:
nls97.maritalstatus.value_counts()Married 3066 Never-married 2766 Divorced 663 Separated 154 Widowed 23 Name: maritalstatus, dtype: int64 -
关闭按频率排序:
nls97.maritalstatus.value_counts(sort=False)Divorced 663 Married 3066 Never-married 2766 Separated 154 Widowed 23 Name: maritalstatus, dtype: int64 -
显示百分比而非计数:
nls97.maritalstatus.value_counts(sort=False, normalize=True)Divorced 0.10 Married 0.46 Never-married 0.41 Separated 0.02 Widowed 0.00 Name: maritalstatus, dtype: float64 -
显示所有政府责任列的百分比。
仅筛选出政府责任列的 DataFrame,然后使用 apply 在该 DataFrame 的所有列上运行 value_counts:
nls97.filter(like="gov").apply(pd.Series.value_counts, normalize=True)
govprovidejobs govpricecontrols ... \
1\. Definitely 0.25 0.54 ...
2\. Probably 0.34 0.33 ...
3\. Probably not 0.25 0.09 ...
4\. Definitely not 0.16 0.04 ...
govdecenthousing govprotectenvironment
1\. Definitely 0.44 0.67
2\. Probably 0.43 0.29
3\. Probably not 0.10 0.03
4\. Definitely not 0.02 0.02
- 找出所有政府责任列中已婚人数的百分比。
做我们在 第 6 步 中所做的,但首先选择仅 maritalstatus 为 Married 的行:
nls97[nls97.maritalstatus=="Married"].\
... filter(like="gov").\
... apply(pd.Series.value_counts, normalize=True)
govprovidejobs govpricecontrols ... \
1\. Definitely 0.17 0.46 ...
2\. Probably 0.33 0.38 ...
3\. Probably not 0.31 0.11 ...
4\. Definitely not 0.18 0.05 ...
govdecenthousing govprotectenvironment
1\. Definitely 0.36 0.64
2\. Probably 0.49 0.31
3\. Probably not 0.12 0.03
4\. Definitely not 0.03 0.01
- 找出所有类别列的频率和百分比。
首先,打开一个文件以写出频率:
for col in nls97.\
select_dtypes(include=["category"]):
print(col, "----------------------",
"frequencies",
nls97[col].value_counts(sort=False),
"percentages",
nls97[col].value_counts(normalize=True,
sort=False),
sep="\n\n", end="\n\n\n", file=freqout)
freqout.close()
这会生成一个文件,其开头如下所示:
gender
----------------------
frequencies
Female 4385
Male 4599
Name: gender, dtype: int64
percentages
Female 0.49
Male 0.51
Name: gender, dtype: float64
正如这些步骤所展示的,value_counts 在我们需要为一个或多个 DataFrame 列生成频率时非常有用。
它是如何工作的……
nls97 DataFrame 中大部分列(88 列中的 57 列)具有对象数据类型。如果我们处理的数据在逻辑上是类别型的,但在 pandas 中没有类别数据类型,将其转换为类别类型是有充分理由的。这不仅节省内存,还使得数据清理变得更简单,就像我们在本教程中看到的那样。
本教程的重点是 value_counts 方法。它可以为 Series 生成频率,就像我们使用 nls97.maritalstatus.value_counts 所做的那样。它也可以在整个 DataFrame 上运行,就像我们使用 nls97.filter(like="gov").apply(pd.Series.value_counts, normalize=True) 所做的那样。我们首先创建一个只包含政府责任列的 DataFrame,然后将生成的 DataFrame 传递给 value_counts 与 apply 一起使用。
你可能注意到在 第 7 步 中,我将链式操作拆分成了多行,以便更易于阅读。关于何时拆分并没有严格的规则。我通常会在链式操作涉及三次或更多操作时尝试这样做。
在 第 8 步 中,我们遍历了所有类别数据类型的列:for col in nls97.select_dtypes(include=["category"])。对于每一列,我们运行了 value_counts 获取频率,再次运行 value_counts 获取百分比。我们使用 print 函数生成换行符,使输出更易读。所有这些都保存在 views 子文件夹中的 frequencies.txt 文件中。我发现保留一组单向频率数据是非常方便的,便于在对类别变量进行任何工作之前进行检查。第 8 步 就是实现这一目标的。
还有更多……
频率分布可能是发现类别数据潜在问题最重要的统计工具。我们在本教程中生成的单向频率是进一步洞察的良好基础。
然而,我们通常只有在检查分类变量与其他变量(无论是分类的还是连续的)之间的关系时,才能发现问题。尽管在这个例子中我们没有进行双向频率分析,但我们确实在第 7 步中开始了拆分数据进行调查的过程。在那一步中,我们查看了已婚个体的政府责任回应,并发现这些回应与整体样本的回应有所不同。
这引发了我们需要探索的几个关于数据的问题。婚姻状况是否会影响回应率,这是否会对政府责任变量的分布产生影响?在得出结论之前,我们还需要谨慎考虑潜在的混杂变量。已婚的受访者是否更可能年纪较大或拥有更多孩子,这些因素是否对他们的政府责任回答更为重要?
我使用婚姻状况变量作为示例,说明生成单向频率(如本例中的频率)可能会引发的查询问题。在遇到类似问题时,准备一些双变量分析(如相关矩阵、交叉表或一些散点图)总是明智的。我们将在接下来的两章中生成这些分析。
为连续变量生成总结统计数据
pandas 提供了大量工具,我们可以利用它们了解连续变量的分布。我们将在本例中重点展示describe的强大功能,并演示直方图在可视化变量分布中的作用。
在对连续变量进行任何分析之前,理解它的分布非常重要——它的集中趋势、分布范围以及偏态性。这些理解大大帮助我们识别离群值和异常值。然而,这本身也是至关重要的信息。我认为并不夸张地说,如果我们能够很好地理解某个变量的分布,我们就能很好地理解它,而没有这种理解的任何解释都会不完整或有缺陷。
准备就绪……
在这个例子中,我们将使用 COVID-19 总数数据。你需要Matplotlib来运行此代码。如果你的机器上尚未安装它,可以通过在终端输入pip install matplotlib来安装。
如何做……
让我们看一下几个关键连续变量的分布:
-
导入
pandas、numpy和matplotlib,并加载 COVID-19 病例总数数据:import pandas as pd import numpy as np import matplotlib.pyplot as plt covidtotals = pd.read_csv("data/covidtotals.csv", ... parse_dates=['lastdate']) covidtotals.set_index("iso_code", inplace=True) -
让我们回顾一下数据的结构:
covidtotals.shape(231, 16)covidtotals.sample(1, random_state=1).Tiso_code GHA \ lastdate 2023-12-03 00:00:00 location Ghana total_cases 171,834.00 total_deaths 1,462.00 total_cases_pm 5,133.07 total_deaths_pm 43.67 population 33475870 pop_density 126.72 median_age 21.10 gdp_per_capita 4,227.63 hosp_beds 0.90 vac_per_hund NaN aged_65_older 3.38 life_expectancy 64.07 hum_dev_ind 0.61 region West Africacovidtotals.dtypeslastdate datetime64[ns] location object total_cases float64 total_deaths float64 total_cases_pm float64 total_deaths_pm float64 population int64 pop_density float64 median_age float64 gdp_per_capita float64 hosp_beds float64 vac_per_hund float64 aged_65_older float64 life_expectancy float64 hum_dev_ind float64 region object dtype: object -
获取 COVID-19 总数列的描述性统计:
totvars = ['total_cases', 'total_deaths','total_cases_pm', 'total_deaths_pm'] covidtotals[totvars].describe()total_cases total_deaths total_cases_pm total_deaths_pm count 231.0 231.0 231.0 231.0 mean 3,351,598.6 30,214.2 206,177.8 1,261.8 std 11,483,211.8 104,778.9 203,858.1 1,315.0 min 4.0 0.0 354.5 0.0 25% 25,671.5 177.5 21,821.9 141.2 50% 191,496.0 1,937.0 133,946.3 827.0 75% 1,294,286.0 14,150.0 345,689.8 1,997.5 max 103,436,829.0 1,127,152.0 763,475.4 6,507.7 -
更仔细地查看病例和死亡列的值分布。
使用 NumPy 的arange方法将从 0 到 1.0 的浮动数列表传递给 DataFrame 的quantile方法:
covidtotals[totvars].\
quantile(np.arange(0.0, 1.1, 0.1))
total_cases total_deaths total_cases_pm \
0.0 4.0 0.0 354.5
0.1 8,359.0 31.0 3,138.6
0.2 17,181.0 126.0 10,885.7
0.3 38,008.0 294.0 35,834.6
0.4 74,129.0 844.0 86,126.2
0.5 191,496.0 1,937.0 133,946.3
0.6 472,755.0 4,384.0 220,429.4
0.7 1,041,111.0 9,646.0 293,737.4
0.8 1,877,065.0 21,218.0 416,608.1
0.9 5,641,992.0 62,288.0 512,388.4
1.0 103,436,829.0 1,127,152.0 763,475.4
total_deaths_pm
0.0 0.0
0.1 32.9
0.2 105.3
0.3 210.5
0.4 498.8
0.5 827.0
0.6 1,251.3
0.7 1,697.6
0.8 2,271.7
0.9 3,155.9
1.0 6,507.7
-
查看总病例的分布:
plt.hist(covidtotals['total_cases']/1000, bins=12) plt.title("Total COVID-19 Cases (in thousands)") plt.xlabel('Cases') plt.ylabel("Number of Countries") plt.show()
图 3.1:COVID-19 总病例数的图表
前面的步骤展示了describe方法和 Matplotlib 的hist方法的使用,这些是处理连续变量时的基本工具。
它是如何工作的…
我们在步骤 3中使用了describe方法来检查一些汇总统计数据和关键变量的分布。当均值和中位数(50^(th)百分位的值)差异巨大时,这通常是一个警示信号。病例和死亡数严重偏向右侧(这一点通过均值远高于中位数得以体现)。这提示我们在上端存在离群值。即使调整了人口规模,total_cases_pm和total_deaths_pm依然表现出相同的偏态。我们将在下一章进行更多关于离群值的分析。
步骤 4中的更详细的百分位数据进一步支持了这种偏态感。例如,病例和死亡数在 90^(th)-百分位和 100^(th)-百分位之间的差距相当大。这些都是表明我们处理的数据不是正态分布的良好初步指标。即使这不是由于错误导致的,这对于我们未来进行的统计检验也是至关重要的。当被问到*“数据看起来怎么样?”*时,我们首先想说的就是这些内容。
总病例数的直方图确认,大部分数据分布在 0 到 100,000 之间,且有一些离群值和 1 个极端离群值。从视觉效果来看,分布更接近对数正态分布,而非正态分布。对数正态分布具有更胖的尾部,并且没有负值。
另见
我们将在下一章深入探讨离群值和意外值。在第五章中,我们将做更多关于可视化的工作,使用可视化识别意外值。
使用生成式 AI 显示描述性统计数据
生成式 AI 工具为数据科学家提供了一个极好的机会,以简化数据清洗和探索工作流程。尤其是大型语言模型,具有使这项工作变得更容易、更直观的潜力。通过使用这些工具,我们可以按条件选择行和列,生成汇总统计,并绘制变量图。
将生成式 AI 工具引入数据探索的一个简单方法是使用 PandasAI。PandasAI 利用 OpenAI 的 API 将自然语言查询转换为 pandas 能够理解的数据选择和操作。截至 2023 年 7 月,OpenAI 是唯一可以与 PandasAI 配合使用的大型语言模型 API,尽管该库的开发者预计将来会添加其他 API。
我们可以使用 PandasAI 大幅减少编写代码的行数,以便生成我们在本章中创建的某些表格和可视化。此处的步骤展示了如何实现这一点。
准备工作…
要运行本食谱中的代码,你需要安装 PandasAI。你可以使用pip install pandasai来安装。我们将再次使用 COVID-19 数据,该数据可在 GitHub 仓库中找到,代码也是如此。
你还需要一个来自 OpenAI 的 API 密钥。你可以在platform.openai.com获取一个。你需要设置一个帐户,然后点击右上角的个人资料,再点击查看 API 密钥。
如何操作…
我们在以下步骤中创建一个 PandasAI 实例,并使用它查看 COVID-19 数据:
-
我们首先导入
pandas和PandasAI库:import pandas as pd from pandasai.llm.openai import OpenAI from pandasai import SmartDataframe -
接下来,我们加载 COVID-19 数据并实例化一个
PandasAI SmartDataframe对象。SmartDataframe对象将允许我们使用自然语言处理数据:covidtotals = pd.read_csv("data/covidtotals.csv", parse_dates=['lastdate']) covidtotals.set_index("iso_code", inplace=True) llm = OpenAI(api_token="Your API Key") covidtotalssdf = SmartDataframe(covidtotals, config={"llm": llm}) -
让我们看看 COVID-19 数据的结构。我们可以通过向 SmartDataframe 的
chat方法传递自然语言指令来做到这一点:covidtotalssdf.chat("Show me some information about the data")<class 'pandas.core.frame.DataFrame'> RangeIndex: 231 entries, 0 to 230 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 iso_code 231 non-null object 1 lastdate 231 non-null datetime64[ns] 2 location 231 non-null object 3 total_cases 231 non-null float64 4 total_deaths 231 non-null float64 5 total_cases_pm 231 non-null float64 6 total_deaths_pm 231 non-null float64 7 population 231 non-null int64 8 pop_density 209 non-null float64 9 median_age 194 non-null float64 10 gdp_per_capita 191 non-null float64 11 hosp_beds 170 non-null float64 12 vac_per_hund 13 non-null float64 13 aged_65_older 188 non-null float64 14 life_expectancy 227 non-null float64 15 hum_dev_ind 187 non-null float64 16 region 231 non-null object dtypes: datetime64ns, float64(12), int64(1), object(3) memory usage: 30.8+ KB -
也可以轻松查看前几行数据:
covidtotalssdf.chat("Show first five rows.")lastdate location total_cases ... \ iso_code ... AFG 2024-02-04 Afghanistan 231,539 ... ALB 2024-01-28 Albania 334,863 ... DZA 2023-12-03 Algeria 272,010 ... ASM 2023-09-17 American Samoa 8,359 ... AND 2023-05-07 Andorra 48,015 ... life_expectancy hum_dev_ind region iso_code AFG 65 1 South Asia ALB 79 1 Eastern Europe DZA 77 1 North Africa ASM 74 NaN Oceania / Aus AND 84 1 Western Europe [5 rows x 16 columns] -
我们可以查看哪些地点(国家)有最高的总病例数:
covidtotalssdf.chat("Show total cases for locations with the five most total cases.")location total_cases iso_code USA United States 103,436,829 CHN China 99,329,249 IND India 45,026,139 FRA France 38,997,490 DEU Germany 38,437,756 -
我们还可以显示每百万最高总病例数,并且展示其他列数据。
注意,我们不需要在total_cases_pm或total_deaths_pm中添加下划线。chat方法会自动识别我们指的就是这些内容:
covidtotalssdf.chat("Show total cases pm, total deaths pm, and location for locations with the 10 highest total cases pm.")
total_cases_pm total_deaths_pm location
iso_code
BRN 763,475 396 Brunei
CYP 760,161 1,523 Cyprus
SMR 750,727 3,740 San Marino
AUT 680,263 2,521 Austria
KOR 667,207 693 South Korea
FRO 652,484 527 Faeroe Islands
SVN 639,408 4,697 Slovenia
GIB 628,883 3,458 Gibraltar
MTQ 626,793 3,004 Martinique
LUX 603,439 1,544 Luxembourg
-
我们还可以创建一个包含选定列的
SmartDataframe。当我们在这一步使用chat方法时,它会自动识别应该返回一个SmartDataframe:covidtotalsabb = covidtotalssdf.chat("Select total cases pm, total deaths pm, and location.") type(covidtotalsabb)pandasai.smart_dataframe.SmartDataframecovidtotalsabbtotal_cases_pm total_deaths_pm location iso_code AFG 5,630 194 Afghanistan ALB 117,813 1,268 Albania DZA 6,058 153 Algeria ASM 188,712 768 American Samoa AND 601,368 1,991 Andorra ... ... ... VNM 118,387 440 Vietnam WLF 306,140 690 Wallis and Futuna YEM 354 64 Yemen ZMB 17,450 203 Zambia ZWE 16,315 352 Zimbabwe [231 rows x 3 columns] -
我们不需要在传递给 PandasAI 的语言中非常精确。我们本可以写
Get或Grab,而不是Select:covidtotalsabb = covidtotalssdf.chat("Grab total cases pm, total deaths pm, and location.") covidtotalsabbtotal_cases_pm total_deaths_pm location iso_code AFG 5,630 194 Afghanistan ALB 117,813 1,268 Albania DZA 6,058 153 Algeria ASM 188,712 768 American Samoa AND 601,368 1,991 Andorra ... ... ... VNM 118,387 440 Vietnam WLF 306,140 690 Wallis and Futuna YEM 354 64 Yemen ZMB 17,450 203 Zambia ZWE 16,315 352 Zimbabwe [231 rows x 3 columns] -
我们可以通过汇总统计选择行。例如,我们可以选择那些每百万总病例数高于第 95 百分位的行。请注意,这可能需要一些时间在你的机器上运行:
covidtotalssdf.chat("Show total cases pm and location where total cases pm greater than 95th percentile.")location total_cases_pm iso_code AND Andorra 601,368 AUT Austria 680,263 BRN Brunei 763,475 CYP Cyprus 760,161 FRO Faeroe Islands 652,484 FRA France 603,428 GIB Gibraltar 628,883 LUX Luxembourg 603,439 MTQ Martinique 626,793 SMR San Marino 750,727 SVN Slovenia 639,408 KOR South Korea 667,207 -
我们可以通过请求它们的分布来查看连续变量是如何分布的:
covidtotalssdf.chat("Summarize values for total cases pm and total deaths pm.").Ttotal_cases_pm total_deaths_pm count 231 231 mean 206,178 1,262 std 203,858 1,315 min 354 0 25% 21,822 141 50% 133,946 827 75% 345,690 1,998 max 763,475 6,508 -
我们还可以获取各组的总数。让我们按地区获取总病例数和死亡数:
covidtotalssdf.chat("Show sum of total cases and total deaths by region.")region total_cases total_deaths 0 Caribbean 4,258,031 32,584 1 Central Africa 640,579 8,128 2 Central America 4,285,644 54,500 3 Central Asia 3,070,921 40,365 4 East Africa 2,186,107 28,519 5 East Asia 205,704,775 604,355 6 Eastern Europe 62,360,832 969,011 7 North Africa 3,727,507 83,872 8 North America 115,917,286 1,516,239 9 Oceania / Aus 14,741,706 31,730 10 South America 68,751,186 1,354,440 11 South Asia 51,507,806 632,374 12 Southern Africa 5,627,277 126,376 13 West Africa 953,756 12,184 14 West Asia 41,080,675 360,258 15 Western Europe 189,405,185 1,124,545 -
我们可以轻松生成关于 COVID-19 数据的图表:
covidtotalssdf.chat("Plot the total_cases_pm column data distribution")
这段代码生成以下图表:
图 3.2:每百万总病例数的分布
-
我们还可以生成散点图。让我们看看每百万总病例数与每百万总死亡数的关系:
covidtotalssdf.chat( "Plot total cases pm values against total deaths pm values")
这段代码生成以下图表:
图 3.3:每百万总病例数与每百万总死亡数的散点图
-
我们可以指定要使用哪个绘图工具。这里使用
regplot可能有助于更好地理解病例数与死亡数之间的关系:covidtotalssdf.chat("Use regplot to show total deaths pm against total cases pm")
这段代码生成以下图表:
图 3.4:回归图
-
对于病例或死亡的极端值,会使得在大部分范围内很难看到两者之间的关系。让我们也请求 PandasAI 去除这些极端值:
covidtotalssdf.chat("Use regplot to show total deaths pm against total cases pm without extreme values")
这将生成以下图表:
图 3.5:去除极端值后的回归图
这次移除了每百万死亡超过 350 和每百万病例超过 20,000 的数据。这样可以更容易地看到数据大部分部分的关系趋势。我们将在第五章,使用可视化方法识别意外值中与regplot和许多其他绘图工具进行更多的操作。
它是如何工作的……
这些示例展示了使用 PandasAI 的直观性。像 PandasAI 这样的生成式 AI 工具有潜力通过使我们几乎能像想出新的分析一样快速与数据互动,从而提升我们的探索性工作。我们只需将自然语言查询传递给 PandasAI 对象,就能获得我们想要的结果。
我们传递的查询并不是命令。我们可以使用任何能表达我们意图的语言。例如,回想一下,我们能够使用select、get,甚至grab来选择列。OpenAI 的大型语言模型通常非常擅长理解我们的意思。
查看 PandasAI 日志文件,以查看你传递给 SmartDataframe chat方法时生成的代码是个好主意。pandasai.log 文件将位于与你的 Python 代码同一文件夹中。
一个帮助我们更快速从问题到答案过渡的工具可以提高我们的思维和分析能力。如果你还没有尝试过这个方法,值得进行尝试,即使你已经有了查看数据的成熟流程。
另见
PandasAI 的 GitHub 仓库是获取更多信息并了解库更新的好地方。你可以通过以下链接访问:github.com/gventuri/pandas-ai。我们将在本书的各个配方中继续使用 PandasAI 库。
摘要
本章涵盖了将原始数据转换为 pandas DataFrame 后的关键步骤。我们探讨了检查数据结构的技巧,包括行数、列数和数据类型。我们还学习了如何生成类别变量的频率,并开始研究一个变量的值如何随着另一个变量的值而变化。最后,我们了解了如何检查连续变量的分布,包括使用均值、最小值和最大值等样本统计量,并通过绘图展示。这为下一章的主题做了准备,在那一章中我们将使用技术来识别数据中的异常值。
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:
第四章:在数据子集中识别异常值
异常值和意外值不一定是错误。它们通常不是。个体和事件是复杂的,常常出乎分析师的意料。有些人确实有 7 英尺 4 英寸的身高,也有些人年收入 5000 万美元。有时候,数据混乱是因为人们和情况本身就很混乱;然而,极端值可能会对我们的分析产生过大的影响,尤其是当我们使用假设正态分布的参数化技术时。
当处理数据子集时,这些问题可能会变得更加明显。这不仅仅是因为在样本较小时,极端或意外的值权重更大。还因为当考虑双变量和多变量关系时,它们可能显得不合逻辑。当一个身高 7 英尺 4 英寸的人,或一个年收入 5000 万美元的人,只有 10 岁时,警示信号会更加显眼。这可能表明某些测量或数据收集的错误。
但关键问题是异常值对我们从数据中得出的推断可能产生的不当影响。事实上,将异常值视为具有非常规变量值,或变量值之间关系的观察,可能是有帮助的,这些观察值的异常之处使得它们无法帮助解释数据中其余部分的关系。这对于统计推断非常重要,因为我们不能假设异常值对我们的总结统计量或参数估计有中立的影响。有时,我们的模型会花费大量精力去构建能够解释异常值观察模式的参数估计,这样我们就会妥协模型对所有其他观察值的解释或预测能力。如果你曾经花费数天试图解读一个模型,却在去除一些异常值后才发现你的系数和预测完全改变了,那就举手吧。
异常值的识别和处理是数据分析项目中最重要的数据准备任务之一。在本章中,我们将探讨一系列用于检测和处理异常值的策略。具体来说,本章的食谱将涵盖以下内容:
-
使用单一变量识别异常值
-
在双变量关系中识别异常值和意外值
-
使用子集来检查变量关系中的逻辑不一致性
-
使用线性回归识别具有显著影响的数据点
-
使用k最近邻(KNN)算法来发现异常值
-
使用隔离森林(Isolation Forest)来发现异常值
-
使用 PandasAI 识别异常值
技术要求
你需要使用 pandas、NumPy 和 Matplotlib 来完成本章的食谱。我使用的是 pandas 2.1.4,但代码也可以在 pandas 1.5.3 或更高版本上运行。
本章的代码可以从本书的 GitHub 仓库下载,链接为github.com/PacktPublishing/Python-Data-Cleaning-Cookbook-Second-Edition。
使用单一变量识别异常值
异常值的概念有一定的主观性,但它与特定分布的特性密切相关;即其集中趋势、分散度和形态。我们根据变量的分布假设某个值是否为预期或意外值,依据是该值出现在当前分布中的可能性。如果某个值远离均值多个标准差,并且该分布近似为正态分布(对称、偏度低且尾部较瘦),我们更倾向于将其视为异常值。
如果我们设想从均匀分布中识别异常值,这一点会变得清晰。均匀分布没有集中趋势,也没有尾部。每个值出现的概率相同。例如,如果每个国家的 COVID-19 病例数是均匀分布的,最小值为 1,最大值为 10,000,000,那么 1 或 10,000,000 都不会被认为是异常值。
我们需要了解一个变量的分布情况,之后才能识别异常值。几个 Python 库提供了帮助我们理解感兴趣变量分布的工具。在本指南中,我们将使用其中的一些工具来识别当某个值偏离范围时,是否需要引起关注。
准备工作
除了 pandas 和 numpy,你还需要 matplotlib、statsmodels 和 scipy 库来运行本指南中的代码。你可以通过在终端客户端或 PowerShell(Windows 系统)中输入 pip install matplotlib、pip install statsmodels 和 pip install scipy 来安装这些库。你可能还需要安装 openpyxl 来保存 Excel 文件。
在本指南中,我们将处理 COVID-19 病例数据。该数据集包含每个国家的 COVID-19 总病例数和死亡人数。
数据说明
Our World in Data 提供了 COVID-19 的公共数据,网址为 ourworldindata.org/covid-cases。该数据集包括总病例数、死亡人数、已进行的测试数量、医院床位数量,以及诸如中位年龄、国内生产总值和人类发展指数等人口统计数据。人类发展指数是标准生活水平、教育水平和预期寿命的综合衡量标准。本指南使用的数据集于 2024 年 3 月 3 日下载。
操作步骤...
我们详细查看 COVID-19 数据中一些关键连续变量的分布情况。我们分析分布的集中趋势和形态,生成正态性度量和可视化图表:
- 加载
pandas、numpy、matplotlib、statsmodels和scipy库,以及 COVID-19 病例数据文件。
同时,设置 COVID-19 病例和人口统计数据列:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm
import scipy.stats as scistat
covidtotals = pd.read_csv("data/covidtotals.csv")
covidtotals.set_index("iso_code", inplace=True)
totvars = ['location','total_cases',
... 'total_deaths','total_cases_pm',
... 'total_deaths_pm']
demovars = ['population','pop_density',
... 'median_age','gdp_per_capita',
... 'hosp_beds','hum_dev_ind']
- 获取 COVID-19 病例数据的描述性统计信息。
创建一个只包含关键信息的 DataFrame:
covidtotalsonly = covidtotals.loc[:, totvars]
covidtotalsonly.describe()
total_cases total_deaths total_cases_pm total_deaths_pm
count 231 231 231 231
mean 3,351,599 30,214 206,178 1,262
std 11,483,212 104,779 2 03,858 1,315
min 4 0 354 0
25% 25,672 178 21,822 141
50% 191,496 1,937 133,946 827
75% 1,294,286 14,150 345,690 1,998
max 103,436,829 1,127,152 763,475 6,508
-
显示更详细的百分位数据。我们指示只对数值进行操作,因此会跳过位置列。
covidtotalsonly.quantile(np.arange(0.0, 1.1, 0.1), numeric_only=True)total_cases total_deaths total_cases_pm total_deaths_pm 0.0 4.0 0.0 354.5 0.0 0.1 8,359.0 31.0 3,138.6 32.9 0.2 17,181.0 126.0 10,885.7 105.3 0.3 38,008.0 294.0 35,834.6 210.5 0.4 74,129.0 844.0 86,126.2 498.8 0.5 191,496.0 1,937.0 133,946.3 827.0 0.6 472,755.0 4,384.0 220,429.4 1,251.3 0.7 1,041,111.0 9,646.0 293,737.4 1,697.6 0.8 1,877,065.0 21,218.0 416,608.1 2,271.7 0.9 5,641,992.0 62,288.0 512,388.4 3,155.9 1.0 103,436,829.0 1,127,152.0 763,475.4 6,507.7
注意
从 pandas 版本 2.0.0 开始,quantile函数的numeric_only参数默认值为False。我们需要将numeric_only的值设置为True,以便让quantile跳过location列。
你还应该显示偏度和峰度。偏度和峰度分别描述了分布的对称性和尾部的肥胖程度。对于total_cases和total_deaths,这两个值显著高于如果变量呈正态分布时的预期值:
covidtotalsonly.skew(numeric_only=True)
total_cases 6.3
total_deaths 7.1
total_cases_pm 0.8
total_deaths_pm 1.3
dtype: float64
covidtotalsonly.kurtosis(numeric_only=True)
total_cases 47.1
total_deaths 61.7
total_cases_pm -0.4
total_deaths_pm 1.3
dtype: float64
原型正态分布的偏度为0,峰度为3。
- 测试 COVID-19 数据的正态性。
使用scipy库中的 Shapiro-Wilk 检验。输出检验的p值(若* p *值低于0.05,则可在 95%的置信水平上拒绝正态分布的null假设):
def testnorm(var, df):
stat, p = scistat.shapiro(df[var])
return p
print("total cases: %.5f" % testnorm("total_cases", covidtotalsonly))
print("total deaths: %.5f" % testnorm("total_deaths", covidtotalsonly))
print("total cases pm: %.5f" % testnorm("total_cases_pm", covidtotalsonly))
print("total deaths pm: %.5f" % testnorm("total_deaths_pm", covidtotalsonly))
total cases: 0.00000
total deaths: 0.00000
total cases pm: 0.00000
total deaths pm: 0.00000
- 显示总病例数和每百万总病例数的常规量化-量化图(
qqplots)。
直线显示了如果分布是正态分布时的样子:
sm.qqplot(covidtotalsonly[['total_cases']]. \
... sort_values(['total_cases']), line='s')
plt.title("QQ Plot of Total Cases")
sm.qqplot(covidtotals[['total_cases_pm']]. \
... sort_values(['total_cases_pm']), line='s')
plt.title("QQ Plot of Total Cases Per Million")
plt.show()
这将生成以下散点图:
图 4.1:COVID-19 病例分布与正态分布的比较
通过按人口调整每百万总病例数列后,分布更接近正态分布:
图 4.2:每百万 COVID-19 病例的分布与正态分布的比较
- 显示总病例的异常值范围。
定义连续变量异常值的一种方法是基于第三四分位数以上或第一四分位数以下的距离。如果该距离超过 1.5 倍的四分位差(第一四分位数和第三四分位数之间的距离),则该值被认为是异常值。本步骤中的计算表明,超过 3,197,208 的值可以被视为异常值。在这种情况下,我们可以忽略小于 0 的异常值阈值,因为这是不可能的:
thirdq, firstq = covidtotalsonly.total_cases.quantile(0.75), covidtotalsonly.total_cases.quantile(0.25)
interquartilerange = 1.5*(thirdq-firstq)
outlierhigh, outlierlow = interquartilerange+thirdq, firstq-interquartilerange
print(outlierlow, outlierhigh, sep=" <--> ")
-1877250 <--> 3197208
- 生成异常值的 DataFrame 并将其写入 Excel。
遍历四个 COVID-19 病例列。按照前一步的操作计算每列的异常值阈值。从 DataFrame 中选择那些高于上限阈值或低于下限阈值的行。添加表示所检验变量(varname)的异常值和阈值级别的列:
def getoutliers():
... dfout = pd.DataFrame(columns=covidtotals. \
... columns, data=None)
... for col in covidtotalsonly.columns[1:]:
... thirdq, firstq = covidtotalsonly[col].\
... quantile(0.75),covidtotalsonly[col].\
... quantile(0.25)
... interquartilerange = 1.5*(thirdq-firstq)
... outlierhigh, outlierlow = \
... interquartilerange+thirdq, \
... firstq-interquartilerange
... df = covidtotals.loc[(covidtotals[col]> \
... outlierhigh) | (covidtotals[col]< \
... outlierlow)]
... df = df.assign(varname = col,
... threshlow = outlierlow,
... threshhigh = outlierhigh)
... dfout = pd.concat([dfout, df])
... return dfout
...
outliers = getoutliers()
outliers.varname.value_counts()
total_deaths 39
total_cases 33
total_deaths_pm 4
Name: varname, dtype: int64
outliers.to_excel("views/outlierscases.xlsx")
这将生成以下 Excel 文件(某些列已隐藏以节省空间):
图 4.3:包含异常值案例的 Excel 文件
根据四分位数法,共识别出 39 个国家在total_deaths值上为异常值,33 个total_cases异常值。注意,total_cases_pm没有异常值。
- 更加仔细地查看每百万总死亡数的异常值。
使用我们在上一步骤中创建的varname列来选择total_deaths_pm的离群值。显示可能有助于解释这些列极端值的列(median_age和hum_dev_ind)。我们还显示了这些列的 25^(th)、50^(th)和 75^(th)百分位的全数据集对比值:
outliers.loc[outliers.varname=="total_deaths_pm",
['location','total_deaths_pm','total_cases_pm',
'median_age','hum_dev_ind']]. \
sort_values(['total_deaths_pm'], ascending=False)
location total_deaths_pm \
PER Peru 6,507.7
BGR Bulgaria 5,703.5
BIH Bosnia and Herzegovina 5,066.3
HUN Hungary 4,918.3
total_cases_pm median_age hum_dev_ind
PER 133,239.0 29.1 0.8
BGR 195,767.9 44.7 0.8
BIH 124,806.3 42.5 0.8
HUN 223,685.2 43.4 0.9
covidtotals[['total_deaths_pm','median_age',
'hum_dev_ind']]. \
quantile([0.25,0.5,0.75])
total_deaths_pm median_age hum_dev_ind
0.25 141.18 22.05 0.60
0.50 827.05 29.60 0.74
0.75 1,997.51 38.70 0.83
所有四个国家的死亡人数每百万均远超 75^(th)百分位。四个国家中的三个国家在中位年龄和人类发展指数方面接近或超过了 75^(th)百分位。出乎意料的是,人类发展指数与每百万死亡人数之间存在正相关关系。我们将在下一个配方中显示相关性矩阵。
-
显示总病例数的直方图:
plt.hist(covidtotalsonly['total_cases']/1000, bins=7) plt.title("Total COVID-19 Cases (thousands)") plt.xlabel('Cases') plt.ylabel("Number of Countries") plt.show()
这段代码会生成以下图形:
图 4.4:COVID-19 总病例数的直方图
-
对 COVID-19 数据进行对数转换。显示总病例数的对数转换后的直方图:
covidlogs = covidtotalsonly.copy() for col in covidlogs.columns[1:]: ... covidlogs[col] = np.log1p(covidlogs[col]) plt.hist(covidlogs['total_cases'], bins=7) plt.title("Total COVID-19 Cases (log)") plt.xlabel('Cases') plt.ylabel("Number of Countries") plt.show()
这段代码会生成以下内容:
图 4.5:COVID-19 总病例数的对数转换直方图
我们在前面的步骤中使用的工具为我们提供了有关 COVID-19 病例和死亡的分布,以及离群值所在位置的相当多的信息。
它是如何工作的……
步骤 3中显示的百分位数据反映了病例和死亡数据的偏斜性。例如,如果我们观察 20^(th)到 30^(th)百分位之间的数值范围,并将其与 70^(th)到 80^(th)百分位之间的范围进行比较,会发现较高百分位的每个变量的范围都远大于较低百分位的范围。这一点从偏度和峰度的非常高值中得到了证实,而正态分布的偏度和峰度值分别为0和3。我们在步骤 4中进行了正态性检验,结果表明,COVID-19 变量的分布在高度显著性水平下不符合正态分布。
这与我们在步骤 5中运行的qqplots一致。总病例数和每百万总病例数的分布与正态分布有显著差异,如直线所示。许多病例集中在零附近,右尾的斜率急剧增加。
我们在步骤 6 和 7中识别了离群值。使用 1.5 倍四分位差来确定离群值是一个合理的经验法则。我喜欢将这些值和相关数据输出到 Excel 文件中,以便查看我能在数据中发现什么模式。当然,这通常会引发更多的问题。我们将在下一个配方中尝试解答其中的一些问题,但我们现在可以考虑的一个问题是,是什么原因导致每百万死亡人数较高的国家,如步骤 8所示。中位年龄和人类发展指数似乎可能是其中的一部分原因。值得进一步探索这些双变量关系,我们将在后续的配方中进行探索。
我们在步骤 7中识别异常值的前提是假设正态分布,但这个假设我们已经证明是不成立的。查看步骤 9中的总病例分布,它看起来更像是对数正态分布,值集中在0附近,并呈右偏。我们在步骤 10中对数据进行了转换,并绘制了转换结果。
还有更多……
我们也可以使用标准差,而不是四分位差,来识别步骤 6 和 7中的异常值。
我应该在这里补充一点,异常值不一定是数据收集或测量错误,我们可能需要,也可能不需要对数据进行调整。然而,极端值可能会对我们的分析产生有意义且持久的影响,特别是在像这样的较小数据集上。
我们对 COVID-19 病例数据的总体印象是相对干净的;也就是说,严格定义下并没有太多无效值。独立地查看每个变量,而不考虑它与其他变量的关系,无法识别出明显的清晰数据错误。然而,变量的分布在统计学上是相当有问题的。基于这些变量构建统计模型将变得复杂,因为我们可能需要排除参数检验。
还值得记住的是,我们对什么构成异常值的理解是由我们对正态分布的假设所塑造的。相反,如果我们让期望值由数据的实际分布来引导,我们对极端值的理解会有所不同。如果我们的数据反映的是一个社会、或生物学、或物理过程,这些过程本身就不是正态分布(如均匀分布、对数分布、指数分布、威布尔分布、泊松分布等),那么我们对异常值的定义应该相应调整。
另见
箱线图在这里也可能会有启发作用。我们在第五章中对这些数据做了箱线图,使用可视化识别意外值。我们在第八章中更详细地探讨了变量转换,编码、转换与特征缩放。
我们将在下一个配方中探讨这个数据集中双变量关系,以便从中获得任何关于异常值和意外值的见解。在后续章节中,我们将考虑补充缺失数据和对极端值进行调整的策略。
识别双变量关系中的异常值和意外值
一个值即使不是极端值,如果它没有显著偏离分布均值,也可能是意外的。当第二个变量有某些特定值时,第一个变量的一些值会变得意外。这在一个变量是分类变量而另一个是连续变量时,尤其容易说明。
以下图表显示了几年期间每天的鸟类观察数量,但对两个地点的分布做了不同的展示。一个地点的平均每天观察数为 33,另一个为 52(这是虚拟数据)。总体平均值(未显示)为 42。那么,58 次每日观察应该如何解读?它是异常值吗?这显然取决于观察的是哪一个地点。
如果在地点 A 有 58 次观察,58 将是一个异常高的数值。而对于地点 B 来说,58 次观察与该地点的平均值并没有太大不同:
图 4.6:按地点分类的每日鸟类观察数
这提示了一个有用的经验法则:每当某个感兴趣的变量与另一个变量显著相关时,在尝试识别异常值时(或者在进行任何涉及该变量的统计分析时),我们应当考虑到这种关系。将这一点表述得更准确一点,并扩展到两个变量都是连续的情况。如果我们假设变量 x 和变量 y 之间存在线性关系,那么我们可以用熟悉的 y = mx + b 方程来描述这种关系,其中 m 是斜率,b 是 y 截距。然后我们可以预期,y 会随着 x 增加 1 单位而增加 m。异常值是那些偏离这一关系较大的值,其中 y 的值远高于或低于根据 x 的值所预测的值。这可以扩展到多个 x 或预测变量。
在本例中,我们演示了如何通过检查一个变量与另一个变量的关系来识别异常值和意外值。在本章接下来的例子中,我们使用多元方法来进一步改进我们的异常值检测。
准备工作
在本例中,我们使用了matplotlib和seaborn库。你可以通过终端客户端或 PowerShell(在 Windows 中)输入pip install matplotlib和pip install seaborn来安装它们。
操作方法...
我们检查了 COVID-19 数据库中总病例数和总死亡数之间的关系。我们仔细查看了那些死亡人数高于或低于根据病例数预期值的国家:
-
加载
pandas、matplotlib、seaborn以及 COVID-19 累积数据:import pandas as pd import matplotlib.pyplot as plt import seaborn as sns covidtotals = pd.read_csv("data/covidtotals.csv") covidtotals.set_index("iso_code", inplace=True) -
生成累积数据和人口统计学列的相关性矩阵。
不出所料,总病例数和总死亡数之间的相关性较高(0.76),而每百万总病例数与每百万总死亡数之间的相关性较低(0.44)。人均 GDP 与每百万病例数之间有强相关性(0.66)(注意,并未显示所有相关性):
covidtotals.corr(method="pearson", numeric_only=True)
total_cases total_deaths \
total_cases 1.00 0.76
total_deaths 0.76 1.00
total_cases_pm 0.10 0.01
total_deaths_pm 0.15 0.27
population 0.70 0.47
pop_density -0.03 -0.04
median_age 0.29 0.19
gdp_per_capita 0.19 0.13
hosp_beds 0.21 0.05
vac_per_hund 0.02 -0.07
aged_65_older 0.29 0.19
life_expectancy 0.19 0.11
hum_dev_ind 0.26 0.21
total_cases_pm ... aged_65_older \
total_cases 0.10 ... 0.29
total_deaths 0.01 ... 0.19
total_cases_pm 1.00 ... 0.72
total_deaths_pm 0.44 ... 0.68
population -0.13 ... -0.01
pop_density 0.19 ... 0.07
median_age 0.74 ... 0.92
gdp_per_capita 0.66 ... 0.51
hosp_beds 0.48 ... 0.65
vac_per_hund 0.24 ... 0.35
aged_65_older 0.72 ... 1.00
life_expectancy 0.69 ... 0.73
hum_dev_ind 0.76 ... 0.78
life_expectancy hum_dev_ind
total_cases 0.19 0.26
total_deaths 0.11 0.21
total_cases_pm 0.69 0.76
total_deaths_pm 0.49 0.60
population -0.04 -0.02
pop_density 0.20 0.14
median_age 0.83 0.90
gdp_per_capita 0.68 0.75
hosp_beds 0.46 0.57
vac_per_hund 0.67 0.51
aged_65_older 0.73 0.78
life_expectancy 1.00 0.91
hum_dev_ind 0.91 1.00
[13 rows x 13 columns]
- 检查一些国家是否存在与总病例数相比,总死亡人数异常高或低的情况。
使用qcut创建一个列,将数据分成分位数。显示按总死亡数分位数排列的总病例数分位数交叉表:
covidtotals['total_cases_q'] = pd.\
... qcut(covidtotals['total_cases'],
... labels=['very low','low','medium',
... 'high','very high'], q=5, precision=0)
covidtotals['total_deaths_q'] = pd.\
... qcut(covidtotals['total_deaths'],
... labels=['very low','low','medium',
... 'high','very high'], q=5, precision=0)
pd.crosstab(covidtotals.total_cases_q,
... covidtotals.total_deaths_q)
total_deaths_q very low low medium high very high
total_cases_q
very low 36 10 1 0 0
low 11 26 8 1 0
medium 0 9 27 10 0
high 0 1 8 31 6
very high 0 0 2 4 40
- 看看那些不符合对角线关系的国家。
有一个国家病例总数很高,但死亡总数很低。由于covidtotals和covidtotalsonly数据框有相同的索引,我们可以使用从后者创建的布尔序列来返回前者的选定行:
covidtotals.loc[(covidtotals. \
total_cases_q=="high") & \
(covidtotals.total_deaths_q=="low")].T
iso_code QAT
lastdate 2023-06-25
location Qatar
total_cases 514,524.00
total_deaths 690.00
total_cases_pm 190,908.72
total_deaths_pm 256.02
population 2695131
pop_density 227.32
median_age 31.90
gdp_per_capita 116,935.60
hosp_beds 1.20
vac_per_hund NaN
aged_65_older 1.31
life_expectancy 80.23
hum_dev_ind 0.85
region West Asia
- 绘制总病例数与总死亡数的散点图。
使用 Seaborn 的regplot方法,除了散点图之外,还生成线性回归线:
ax = sns.regplot(x=covidtotals.total_cases/1000, y=covidtotals.total_deaths)
ax.set(xlabel="Cases (thousands)", ylabel="Deaths", title="Total COVID-19 Cases and Deaths by Country")
plt.show()
这产生了以下散点图:
图 4.7:带有线性回归线的总病例数与死亡数散点图
- 检查回归线之上的意外值。
仔细观察数据中明显位于回归线之上或之下的国家,看看这些国家的病例数和死亡数坐标。有两个国家的病例数少于 4000 万,但死亡人数超过 40 万:
covidtotals.loc[(covidtotals.total_cases<40000000) \
& (covidtotals.total_deaths>400000)].T
iso_code BRA RUS
lastdate 2023-10-01 2024-01-28
location Brazil Russia
total_cases 37,519,960.00 23,774,451.00
total_deaths 702,116.00 401,884.00
total_cases_pm 174,257.35 164,286.55
total_deaths_pm 3,260.90 2,777.11
population 215313504 144713312
pop_density 25.04 8.82
median_age 33.50 39.60
gdp_per_capita 14,103.45 24,765.95
hosp_beds 2.20 8.05
vac_per_hund NaN NaN
aged_65_older 8.55 14.18
life_expectancy 75.88 72.58
hum_dev_ind 0.77 0.82
region South America Eastern Europe
- 检查回归线下方的意外值。
有两个国家的病例数超过 3000 万,但死亡数少于 10 万:
covidtotals.loc[(covidtotals.total_cases>30000000) \
& (covidtotals.total_deaths<100000)].T
iso_code JPN KOR
lastdate 2023-05-14 2023-09-10
location Japan South Korea
total_cases 33,803,572.00 34,571,873.00
total_deaths 74,694.00 35,934.00
total_cases_pm 272,715.69 667,207.06
total_deaths_pm 602.61 693.50
population 123951696 51815808
pop_density 347.78 527.97
median_age 48.20 43.40
gdp_per_capita 39,002.22 35,938.37
hosp_beds 13.05 12.27
vac_per_hund NaN NaN
aged_65_older 27.05 13.91
life_expectancy 84.63 83.03
hum_dev_ind 0.92 0.92
region East Asia East Asia
-
绘制每百万人总病例数与每百万人总死亡数的散点图:
ax = sns.regplot(x="total_cases_pm", y="total_deaths_pm", data=covidtotals) ax.set(xlabel="Cases Per Million", ylabel="Deaths Per Million", title="Total COVID-19 Cases per Million and Deaths per Million by Country") plt.show()
这产生了以下散点图:
图 4.8:每百万人病例数与死亡数散点图,带有线性回归线
前面的步骤考察了变量之间的关系,以识别异常值。
其工作原理…
通过观察双变量关系,我们提出了一些在前一个步骤中未显现出来的问题。我们确认了预期中的关系,比如总病例数与总死亡数的关系,但这也让偏离这种关系的情况更加引人注目。根据一定数量的病例数,有可能有实质性的解释来说明异常高的死亡率,但测量误差或病例报告不准确也不能排除。
步骤 2显示了总病例数和总死亡数之间的高度相关性(0.76),但即便如此,仍然存在一些差异。我们在步骤 3中将病例数和死亡数划分为分位数,然后做一个分位数值的交叉表。大多数国家位于对角线上或接近对角线。然而,有一个国家的病例数非常高,但死亡数较低,即卡塔尔。合理的怀疑是是否存在潜在的报告问题。
我们在步骤 5中绘制了总病例数与总死亡数的散点图。两者之间强烈的向上倾斜关系得到了确认,但有一些国家的死亡数位于回归线之上。我们可以看到,巴西和俄罗斯的死亡数比根据病例数预测的要高,而日本和韩国的死亡数则远低于预测值。
不出所料,在每百万案例和每百万死亡人数的散点图中,回归线周围的散点更为分散。虽然存在正相关关系,但回归线的斜率并不陡峭。
还有更多...
我们已经开始对数据的样貌有了较好的了解,但以这种形式的数据并不能让我们检查单变量分布和双变量关系如何随时间变化。例如,一个国家每百万死亡人数超过每百万案例人数的原因,可能是因为自首次确诊病例以来已经过去了更多的时间。我们无法在累积数据中探索这一点。为此,我们需要每日数据,在接下来的章节中我们将探讨这个问题。
这个食谱,以及之前的那个,展示了数据清理如何渗透到探索性数据分析中,即使在你刚开始对数据有所了解时。我一定会在数据探索和我们现在所做的工作之间做出区分。我们正在尝试了解数据是如何相互关联的,以及为什么在某些情况下,某些变量会取某些值而在其他情况下不取。我们希望做到当我们开始进行分析时,数据中不会有大的惊讶。
我发现做一些小的调整来规范这个过程很有帮助。我对那些尚未准备好进行分析的文件使用不同的命名规范。即使没有别的,这也帮助我提醒自己,在这个阶段生成的任何数据都远未准备好分发。
另见
我们仍然没有做太多工作来检查可能的数据问题,这些问题只有在检查数据子集时才会显现出来;例如,声称自己不工作的人的正工资收入值(这两个变量都来自国家纵向调查或NLS)。我们将在下一个食谱中解决这个问题。
在第五章中,我们将使用 Matplotlib 和 Seaborn 做更多工作,主题是利用可视化识别异常值。
使用子集工具检查变量关系中的逻辑不一致性
在某些时候,数据问题归结为推理逻辑问题,比如当变量 y 小于某个量 b 时,变量 x 必须大于某个量 a。一旦完成一些初步的数据清理,检查逻辑不一致性就变得很重要。pandas通过子集工具如loc和布尔索引,使这种错误检查相对简单。我们可以将这些工具与 Series 和 DataFrame 的汇总方法结合使用,从而轻松地将特定行的值与整个数据集或某些行的子集进行比较。我们还可以轻松地对列进行聚合。关于变量之间的逻辑关系的任何问题,都可以通过这些工具得到解答。我们将在这个食谱中演示一些示例。
准备工作
我们将使用 NLS 数据,主要涉及就业和教育方面的数据。在本例中,我们多次使用 apply 和 lambda 函数,但我们会在第九章,在聚合时修复混乱数据中详细介绍它们的使用。然而,即使你没有这些工具的经验,也不需要回顾第九章,就能跟随操作。
数据说明
美国劳工统计局进行的国家青少年纵向调查(NLS)始于 1997 年,调查对象为 1980 至 1985 年间出生的一组人群,每年进行一次跟踪,直到 2023 年。本次例子中,我从调查中的数百个数据项中提取了 106 个变量,涵盖了成绩、就业、收入和对政府态度等信息。NLS 数据可以从nlsinfo.org下载。
操作方法:
我们对 NLS 数据进行了多次逻辑检查,比如有研究生入学记录但没有本科入学记录的个体,或者有工资收入但没有工作周数的个体。我们还检查给定个体在不同时期之间关键值的巨大变化:
-
导入
pandas,然后加载 NLS 数据:import pandas as pd nls97 = pd.read_csv("data/nls97f.csv", low_memory=False) nls97.set_index("personid", inplace=True) -
查看一些就业和教育数据。
数据集中包含 2000 年至 2023 年每年的工作周数,以及 1997 年 2 月到 2022 年 10 月每月的大学入学状态。我们利用 loc 访问器来选择从冒号左侧指定的列到右侧指定的列的所有数据。例如,nls97.loc[:, "colenroct15":"colenrfeb22"]:
nls97[['wageincome20','highestgradecompleted',
'highestdegree']].head(3).T
personid 135335 999406 \
wageincome20 NaN 115,000
highestgradecompleted NaN 14
highestdegree 4\. Bachelors 2\. High School
personid 151672
wageincome20 NaN
highestgradecompleted 16
highestdegree 4\. Bachelors
nls97.loc[:, "weeksworked18":"weeksworked22"].head(3).T
personid 135335 999406 151672
weeksworked18 NaN 52 52
weeksworked19 NaN 52 9
weeksworked20 NaN 52 0
weeksworked21 NaN 46 0
weeksworked22 NaN NaN 3
nls97.loc[:, "colenroct15":"colenrfeb22"].head(2).T
personid 135335 999406
colenroct15 1\. Not enrolled 1\. Not enrolled
colenrfeb16 NaN 1\. Not enrolled
colenroct16 NaN 1\. Not enrolled
colenrfeb17 NaN 1\. Not enrolled
colenroct17 NaN 1\. Not enrolled
colenrfeb18 NaN 1\. Not enrolled
colenroct18 NaN 1\. Not enrolled
colenrfeb19 NaN 1\. Not enrolled
colenroct19 NaN 1\. Not enrolled
colenrfeb20 NaN 1\. Not enrolled
colenroct20 NaN 1\. Not enrolled
colenrfeb21 NaN 1\. Not enrolled
colenroct21 NaN 1\. Not enrolled
colenrfeb22 NaN NaN
显示有工资收入但没有工作周数的个体:
nls97.loc[(nls97.weeksworked20==0) &
(nls97.wageincome20>0),
['weeksworked20','wageincome20']]
weeksworked20 wageincome20
personid
674877 0 40,000
692251 0 12,000
425230 0 150,000
391939 0 10,000
510545 0 72,000
... ...
947109 0 1,000
706862 0 85,000
956396 0 130,000
907078 0 10,000
274042 0 130,000
[132 rows x 2 columns]
- 检查个体是否曾经在四年制大学就读过。
链接多个方法。首先,创建一个包含以 colenr 开头的列的 DataFrame(nls97.filter(like="colenr"))。这些是每年 10 月和 2 月的大学入学列。然后,使用 apply 运行一个 lambda 函数,检查每个 colenr 列的第一个字符(apply(lambda x: x.str[0:1]=='3'))。这将为所有大学入学列返回一个 True 或 False 值;如果字符串的第一个值为 3,表示四年制大学入学,则返回 True。最后,使用 any 函数测试从前一步返回的任何值是否为 True(any(axis=1))。这将识别个体是否在 1997 年 2 月到 2022 年 10 月期间曾就读四年制大学。这里的第一个语句仅用于说明前两步的结果。要获得所需的结果,只需运行第二个语句,查看个体是否曾在某个时刻入读四年制大学:
nls97.filter(like="colenr").\
apply(lambda x: x.str[0:1]=='3').\
head(2).T
personid 135335 999406
colenrfeb97 False False
colenroct97 False False
colenrfeb98 False False
colenroct98 False False
colenrfeb99 False False
colenroct99 True False
colenrfeb00 True False
colenroct00 True True
colenrfeb01 True True
colenroct01 True False
colenrfeb02 True False
colenroct02 True True
colenrfeb03 True True
nls97.filter(like="colenr").\
apply(lambda x: x.str[0:1]=='3').\
any(axis=1).head(2)
personid
135335 True
999406 True
dtype: bool
- 显示有研究生入学记录但没有本科入学记录的个体。
我们可以使用在步骤 4中测试的内容进行一些检查。我们希望找到那些在任何一个月的colenr字段的首字符为4(研究生入学),但从未出现过3(本科入学)值的个体。注意测试的第二部分前面的~符号,用于取反。共有 24 个个体符合这个条件:
nobach = nls97.loc[nls97.filter(like="colenr").\
apply(lambda x: x.str[0:1]=='4').\
any(axis=1) & ~nls97.filter(like="colenr").\
apply(lambda x: x.str[0:1]=='3').\
any(axis=1), "colenrfeb17":"colenrfeb22"]
len(nobach)
24
nobach.head(2).T
personid 793931 787976
.....abbreviated to save space
colenrfeb01 1\. Not enrolled 1\. Not enrolled
colenroct01 2\. 2-year college 1\. Not enrolled
colenrfeb02 2\. 2-year college 1\. Not enrolled
colenroct02 2\. 2-year college 1\. Not enrolled
colenrfeb03 2\. 2-year college 1\. Not enrolled
colenroct03 1\. Not enrolled 1\. Not enrolled
colenrfeb04 2\. 2-year college 1\. Not enrolled
colenroct04 4\. Graduate program 1\. Not enrolled
colenrfeb05 4\. Graduate program 1\. Not enrolled
.....
colenrfeb14 1\. Not enrolled 1\. Not enrolled
colenroct14 1\. Not enrolled 2\. 2-year college
colenrfeb15 1\. Not enrolled 2\. 2-year college
colenroct15 1\. Not enrolled 2\. 2-year college
colenrfeb16 1\. Not enrolled 1\. Not enrolled
colenroct16 1\. Not enrolled 4\. Graduate program
colenrfeb17 1\. Not enrolled 4\. Graduate program
colenroct17 1\. Not enrolled 4\. Graduate program
colenrfeb18 1\. Not enrolled 4\. Graduate program
colenroct18 1\. Not enrolled 1\. Not enrolled
.....
- 显示拥有学士学位或更高学位,但没有四年制大学入学的个体。
使用isin来将highestdegree字段的首字符与列表中的所有值进行比较(nls97.highestdegree.str[0:1].isin(['4','5','6','7'])):
nls97.highestdegree.value_counts().sort_index()
highestdegree
0\. None 877
1\. GED 1167
2\. High School 3531
3\. Associates 766
4\. Bachelors 1713
5\. Masters 704
6\. PhD 64
7\. Professional 130
Name: count, dtype: int64
no4yearenrollment = \
... nls97.loc[nls97.highestdegree.str[0:1].\
... isin(['4','5','6','7']) & \
... ~nls97.filter(like="colenr").\
... apply(lambda x: x.str[0:1]=='3').\
... any(axis=1), "colenrfeb97":"colenrfeb22"]
len(no4yearenrollment)
42
no4yearenrollment.head(2).T
personid 417244 124616
.....abbreviated to save space
colenroct04 1\. Not enrolled 2\. 2-year college
colenrfeb05 1\. Not enrolled 2\. 2-year college
colenroct05 1\. Not enrolled 1\. Not enrolled
colenrfeb06 1\. Not enrolled 1\. Not enrolled
colenroct06 1\. Not enrolled 1\. Not enrolled
colenrfeb07 1\. Not enrolled 1\. Not enrolled
colenroct07 1\. Not enrolled 1\. Not enrolled
colenrfeb08 1\. Not enrolled 1\. Not enrolled
colenroct08 1\. Not enrolled 1\. Not enrolled
colenrfeb09 2\. 2-year college 1\. Not enrolled
colenroct09 2\. 2-year college 1\. Not enrolled
colenrfeb10 2\. 2-year college 1\. Not enrolled
colenroct10 2\. 2-year college 1\. Not enrolled
colenrfeb11 2\. 2-year college 1\. Not enrolled
colenroct11 2\. 2-year college 1\. Not enrolled
colenrfeb12 2\. 2-year college 1\. Not enrolled
colenroct12 1\. Not enrolled 1\. Not enrolled
colenrfeb13 1\. Not enrolled 1\. Not enrolled
- 显示高工资收入的个体。
将高工资定义为高于平均值三个标准差的收入。看起来工资收入的值已经在 380,288 美元处被截断:
highwages = \
nls97.loc[nls97.wageincome20 >
nls97.wageincome20.mean()+ \
(nls97.wageincome20.std()*3),
['wageincome20']]
highwages
wageincome20
personid
989896 380,288
718416 380,288
693498 380,288
811201 380,288
553982 380,288
...
303838 380,288
366297 380,288
436132 380,288
964406 380,288
433818 380,288
[104 rows x 1 columns]
- 显示在最近一年中,工作周数变化较大的个体。
计算每个人 2016 到 2020 年间的平均工作周数(nls97.loc[:, "weeksworked16":"weeksworked20"].mean(axis=1))。我们通过axis=1来表示按列计算每个个体的平均值,而不是按个体计算。然后,我们找到那些平均值不在 2021 年工作周数的一半到两倍之间的行。(注意我们早些时候使用的~操作符)。我们还表示,对于那些 2021 年工作周数为null的行,我们不感兴趣。共有 1,099 个个体在 2021 年与 2016 至 2020 年平均值相比,工作周数发生了大幅变化:
workchanges = nls97.loc[~nls97.loc[:,
"weeksworked16":"weeksworked20"].mean(axis=1).\
between(nls97.weeksworked21*0.5,\
nls97.weeksworked21*2) \
& ~nls97.weeksworked21.isnull(),
"weeksworked16":"weeksworked21"]
len(workchanges)
1099
workchanges.head(6).T
personid 151672 620126 ... 692251 483488
weeksworked16 53 45 ... 0 53
weeksworked17 52 0 ... 0 52
weeksworked18 52 0 ... 0 52
weeksworked19 9 0 ... 0 52
weeksworked20 0 0 ... 0 15
weeksworked21 0 0 ... 51 13
[6 rows x 6 columns]
- 显示最高学历和最高学位的不一致之处。
使用crosstab函数,显示highestgradecompleted根据highestdegree的分类情况,筛选highestgradecompleted小于 12 的个体。这些个体中有相当一部分表明他们已经完成了高中学业,这在美国是比较不寻常的,除非完成的最高年级低于 12 年级:
ltgrade12 = nls97.loc[nls97.highestgradecompleted<12, ['highestgradecompleted','highestdegree']]
pd.crosstab(ltgrade12.highestgradecompleted, ltgrade12.highestdegree)
highestdegree 0\. None 1\. GED \
highestgradecompleted
5 0 0
6 11 4
7 23 7
8 108 82
9 98 182
10 105 207
11 113 204
highestdegree 2\. High School 3\. Associates
highestgradecompleted
5 1 0
6 0 1
7 1 0
8 7 0
9 8 1
10 14 1
11 42 2
这些步骤揭示了 NLS 数据中的若干逻辑不一致。
它是如何工作的……
如果你是第一次看到这个语法,可能会觉得做这类子集筛选的语法有点复杂。但你会逐渐习惯它,而且它允许你快速对数据运行任何你能想到的查询。
一些不一致或意外的值表明可能存在受访者或录入错误,值得进一步调查。当weeks worked的值为0时,很难解释正的工资收入值。其他意外的值可能根本不是数据问题,而是表明我们应该小心如何使用这些数据。例如,我们可能不想单独使用 2021 年的工作周数。相反,我们可能考虑在许多分析中使用三年平均值。
另见
第三章,评估你的数据中的选择和组织列与选择行食谱展示了这里使用的一些数据子集技术。我们将在第九章,修复聚合数据时的脏数据中更详细地讲解apply函数。
使用线性回归来识别具有显著影响的数据点
本章剩余的教程使用统计建模来识别离群值。这些技术的优点在于它们不太依赖于关注变量的分布,并且比单变量或双变量分析考虑得更多。这使得我们能够识别那些原本不显眼的离群值。另一方面,通过考虑更多因素,多变量技术可能会提供证据,表明一个先前可疑的值实际上在预期范围内,并提供有意义的信息。
在本教程中,我们使用线性回归来识别对目标或因变量模型有过大影响的观察值(行)。这可能表明某些观察值的一个或多个值极端,以至于影响了其他所有观察值的模型拟合。
准备开始
本教程中的代码需要 matplotlib 和 statsmodels 库。你可以通过在终端窗口或 PowerShell(在 Windows 上)输入 pip install matplotlib 和 pip install statsmodels 来安装它们。
我们将处理有关每个国家 COVID-19 总病例和死亡数据。
如何操作…
我们将使用 statsmodels 的 OLS 方法来拟合每百万人总病例数的线性回归模型。然后,我们将识别出对该模型有最大影响的国家:
-
导入
pandas、matplotlib和statsmodels,并加载 COVID-19 疫情数据:import pandas as pd import matplotlib.pyplot as plt import statsmodels.api as sm covidtotals = pd.read_csv("data/covidtotals.csv") covidtotals.set_index("iso_code", inplace=True) -
创建分析文件并生成描述性统计数据。
仅获取分析所需的列。删除分析列中有缺失数据的行:
xvars = ['pop_density','median_age','gdp_per_capita']
covidanalysis = covidtotals.loc[:,['total_cases_pm'] + xvars].dropna()
covidanalysis.describe()
total_cases_pm pop_density median_age gdp_per_capita
count 180 180 180 180
mean 167,765 204 30 18,290
std 190,965 631 9 19,392
min 354 2 15 661
25% 11,931 36 22 3,790
50% 92,973 82 29 11,822
75% 263,162 205 38 26,785
max 763,475 7,916 48 116,936
- 拟合线性回归模型。
有充分的概念性理由相信,人口密度、中位年龄和人均 GDP 可能是每百万人总病例数的预测因子。我们在模型中使用这三个变量:
def getlm(df):
... Y = df.total_cases_pm
... X = df[['pop_density',
'median_age','gdp_per_capita']]
... X = sm.add_constant(X)
... return sm.OLS(Y, X).fit()
...
lm = getlm(covidanalysis)
lm.summary()
coef std err t P>|t|
----------------------------------------------------------------
Const -2.382e+05 3.41e+04 -6.980 0.000
pop_density 12.4060 14.664 0.846 0.399
median_age 11570 1291.446 8.956 0.000
gdp_per_capita 2.9674 0.621 4.777 0.000
- 确定对模型有过大影响的国家。
Cook 距离值大于 0.5 的数据应仔细审查:
influence = lm.get_influence().summary_frame()
influence.loc[influence.cooks_d>0.5, ['cooks_d']]
cooks_d
iso_code
QAT 0.70
SGP 3.12
covidanalysis.loc[influence.cooks_d>0.5]
total_cases_pm pop_density median_age gdp_per_capita
iso_code
QAT 190,909 227 32 116,936
SGP 531,184 7,916 42 85,535
- 创建影响图。
具有更高 Cook 距离值的国家显示出更大的圆圈:
fig, ax = plt.subplots(figsize=(8,8))
sm.graphics.influence_plot(lm, ax = ax, alpha=5, criterion="cooks")
plt.show()
这将生成以下图表:
图 4.9:影响图,包括具有最高 Cook 距离的国家
- 在不包括两个离群值的情况下运行模型。
删除这些离群值会影响模型的每个系数,特别是人口密度(即使在 95% 的置信水平下仍然不显著):
covidanalysisminusoutliers = covidanalysis.loc[influence.cooks_d<0.5]
lm = getlm(covidanalysisminusoutliers)
lm.summary()
coef std err t P>|t|
--------------------------------------------------------------
const -2.158e+05 3.43e+04 -6.288 0.000
pop_density 61.2396 34.260 1.788 0.076
median_age 9968.5170 1346.416 7.404 0.000
gdp_per_capita 4.1112 0.704 5.841 0.000
这让我们大致了解了哪些国家在与人口统计变量和每百万人总病例数的关系上与其他国家最为不同。
它是如何工作的...
Cook’s 距离是衡量每个观察值对模型影响程度的指标。两个异常值对模型的巨大影响在 步骤 6 中得到了验证,当我们在不包含这些异常值的情况下重新运行模型时。分析师需要问的问题是,像这些异常值是否提供了重要的信息,还是扭曲了模型并限制了其适用性。第一次回归结果中,median_age 的系数为 11570,表示中位数年龄每增加一年,病例数每百万人增加 11570。这一数字在去除异常值后的模型中大幅缩小,降至 9969。
回归输出中的 P>|t| 值告诉我们系数是否显著不同于 0。在第一次回归中,median_age 和 gdp_per_capita 的系数在 99% 的显著性水平上显著;也就是说,P>|t| 值小于 0.01。
还有更多内容…
在这个食谱中,我们运行了一个线性回归模型,并非主要是因为我们对模型的参数估计感兴趣,而是因为我们想要确定是否有任何观察值对我们可能进行的多元分析产生了潜在的过大影响。显然,这在此情况下确实是成立的。
通常,删除异常值是有道理的,正如我们在这里所做的那样,但这并不总是如此。当我们有能够很好地捕捉异常值不同之处的自变量时,其他自变量的参数估计会更不容易受到扭曲。我们也可以考虑转换,例如我们在前一个食谱中进行的对数转换,以及我们在接下来的两个食谱中将要进行的标准化。根据你的数据,适当的转换可以通过限制极端值的残差大小来减少异常值的影响。
使用 k-最近邻找出异常值
无监督机器学习工具可以帮助我们识别与其他观察结果不同的观察值,特别是在我们没有标签数据的情况下;也就是说,当没有目标变量或因变量时。(在前一个食谱中,我们使用了每百万人总病例数作为因变量。)即使选择目标和因素相对简单,识别异常值而不对变量之间的关系做任何假设也可能会很有帮助。我们可以使用 k-最近邻(KNN)来找出与其他观察值最不同的观测值,即那些它们的值与最近邻值之间差异最大的值。
准备工作
你需要 Python 异常值检测 (PyOD) 和 scikit-learn 来运行这个食谱中的代码。你可以通过在终端或 PowerShell(在 Windows 系统中)输入 pip install pyod 和 pip install sklearn 来安装这两个工具。
操作步骤…
我们将使用 KNN 来识别属性最异常的国家:
-
加载
pandas、pyod和sklearn,以及 COVID-19 病例数据:import pandas as pd from pyod.models.knn import KNN from sklearn.preprocessing import StandardScaler covidtotals = pd.read_csv("data/covidtotals.csv") covidtotals.set_index("iso_code", inplace=True) -
为分析列创建一个标准化的 DataFrame:
standardizer = StandardScaler() analysisvars = ['location','total_cases_pm', ... 'total_deaths_pm', 'pop_density', ... 'median_age','gdp_per_capita'] covidanalysis = covidtotals.loc[:, analysisvars].dropna() covidanalysisstand = standardizer.fit_transform(covidanalysis.iloc[:, 1:]) -
运行 KNN 模型并生成异常得分。
我们通过将污染参数设置为 0.1 来创建一个任意数量的异常值:
clf_name = 'KNN'
clf = KNN(contamination=0.1)
clf.fit(covidanalysisstand)
KNN(algorithm='auto', contamination=0.1, leaf_size=30, method='largest',
metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=5, p=2,
radius=1.0)
y_pred = clf.labels_
y_scores = clf.decision_scores_
- 显示模型的预测结果。
从y_pred和y_scores的 NumPy 数组中创建一个数据框。将索引设置为covidanalysis数据框的索引,以便稍后能够轻松地将其与该数据框合并。注意,异常值的决策得分都高于内点(异常值=0)的得分:
pred = pd.DataFrame(zip(y_pred, y_scores),
... columns=['outlier','scores'],
... index=covidanalysis.index)
pred.sample(10, random_state=2)
outlier scores
iso_code
BHR 1 2.69
BRA 0 0.75
ZWE 0 0.21
BGR 1 1.62
CHN 0 0.94
BGD 1 1.52
GRD 0 0.68
UZB 0 0.37
MMR 0 0.37
ECU 0 0.58
pred.outlier.value_counts()
0 162
1 18
Name: outlier, dtype: int64
pred.groupby(['outlier'])[['scores']].agg(['min','median','max'])
scores
min median max
outlier
0 0.08 0.60 1.40
1 1.42 1.65 11.94
- 显示异常值的 COVID-19 数据。
首先,合并covidanalysis和pred数据框:
covidanalysis.join(pred).\
... loc[pred.outlier==1,\
... ['location','total_cases_pm',
... 'total_deaths_pm','scores']].\
... sort_values(['scores'],
... ascending=False).head(10)
location total_cases_pm \
iso_code
SGP Singapore 531,183.84
QAT Qatar 190,908.72
BHR Bahrain 473,167.02
LUX Luxembourg 603,439.46
PER Peru 133,239.00
BRN Brunei 763,475.44
MDV Maldives 356,423.66
MLT Malta 227,422.82
ARE United Arab Emirates 113,019.21
BGR Bulgaria 195,767.89
total_deaths_pm scores
iso_code
SGP 346.64 11.94
QAT 256.02 3.04
BHR 1,043.31 2.69
LUX 1,544.16 2.49
PER 6,507.66 2.27
BRN 396.44 2.26
MDV 603.29 1.98
MLT 1,687.63 1.96
ARE 248.81 1.69
BGR 5,703.52 1.62
这些步骤展示了我们如何利用 KNN 基于多变量关系来识别异常值。
工作原理...
PyOD 是一个 Python 异常值检测工具包。我们在这里将其作为 scikit-learn 的 KNN 包的封装器使用,这简化了某些任务。
本例的重点不是构建模型,而是通过考虑我们拥有的所有数据,了解哪些观察结果(国家)是显著的异常值。这个分析支持我们逐渐形成的看法,即新加坡和卡塔尔在我们的数据集中与其他国家有很大不同。它们具有非常高的决策得分。(步骤 5中的表格按得分降序排列。)
像巴林和卢森堡这样的国家也可能被视为异常值,尽管这种判断不那么明确。之前的方案没有表明它们对回归模型有压倒性的影响。然而,那个模型没有同时考虑每百万病例和每百万死亡人数。这也可能解释为什么新加坡在这里比卡塔尔更为异常。新加坡既有很高的每百万病例,又有低于平均水平的每百万死亡。
Scikit-learn 使得数据标准化变得非常简单。我们在步骤 2中使用了标准化器,它为数据框中的每个值返回了z分数。z分数将每个变量值减去该变量的均值,并除以该变量的标准差。许多机器学习工具需要标准化的数据才能良好运作。
还有更多...
KNN 是一个非常流行的机器学习算法。它易于运行和解释。它的主要限制是,在大型数据集上运行时速度较慢。
我们跳过了构建机器学习模型时通常会采取的一些步骤。例如,我们没有创建单独的训练集和测试集。PyOD 允许轻松完成此操作,但在这里我们并不需要这么做。
另请参见
我们在第八章《编码、转换和标准化特征》中详细介绍了数据转换。关于使用 KNN 的一个好资源是《使用机器学习进行数据清理与探索》,这本书也由我撰写。
PyOD 工具包提供了大量有监督和无监督学习技术,用于检测数据中的异常值。你可以在pyod.readthedocs.io/en/latest/找到相关文档。
使用隔离森林检测异常值
孤立森林是一种相对较新的机器学习技术,用于识别异常值。它之所以快速流行,部分原因是其算法优化于发现异常值而不是正常值。它通过对数据的连续分区来找到异常值,直到一个数据点被孤立出来。需要更少分区来孤立的点将获得较高的异常分数。这个过程在系统资源上相对容易。在这个配方中,我们演示了如何使用它来检测异常的 COVID-19 病例和死亡人数。
准备工作
要运行本配方中的代码,您需要安装 scikit-learn 和 Matplotlib。您可以在终端或 PowerShell(Windows 中)中输入pip install sklearn和pip install matplotlib来安装它们。
如何做到这一点……
我们将使用孤立森林来找出那些属性表明它们是最异常的国家:
-
加载
pandas、matplotlib以及从sklearn中加载的StandardScaler和IsolationForest模块:import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler from sklearn.ensemble import IsolationForest covidtotals = pd.read_csv("data/covidtotals.csv") covidtotals.set_index("iso_code", inplace=True) -
创建一个标准化分析的 DataFrame。
首先,删除所有带有缺失数据的行:
analysisvars = ['location','total_cases_pm','total_deaths_pm',
... 'pop_density','median_age','gdp_per_capita']
standardizer = StandardScaler()
covidtotals.isnull().sum()
lastdate 0
location 0
total_cases 0
total_deaths 0
total_cases_pm 0
total_deaths_pm 0
population 0
pop_density 11
median_age 24
gdp_per_capita 27
hosp_beds 45
region 0
dtype: int64
covidanalysis = covidtotals.loc[:, analysisvars].dropna()
covidanalysisstand = standardizer.fit_transform(covidanalysis.iloc[:, 1:])
- 运行孤立森林模型来检测异常值。
将标准化数据传递给fit方法。共有 18 个国家被识别为异常值。(这些国家的异常值为-1。)这是由0.1的污染度数确定的:
clf=IsolationForest(n_estimators=100,
max_samples='auto', contamination=.1,
max_features=1.0)
clf.fit(covidanalysisstand)
IsolationForest(contamination=0.1)
covidanalysis['anomaly'] = \
clf.predict(covidanalysisstand)
covidanalysis['scores'] = \
clf.decision_function(covidanalysisstand)
covidanalysis.anomaly.value_counts()
1 156
-1 18
Name: anomaly, dtype: int64
- 创建异常值和正常值 DataFrame。
根据异常分数列出前 10 个异常值:
inlier, outlier = \
covidanalysis.loc[covidanalysis.anomaly==1],\
covidanalysis.loc[covidanalysis.anomaly==-1]
outlier[['location','total_cases_pm',
'total_deaths_pm','median_age',
'gdp_per_capita','scores']].\
sort_values(['scores']).\
head(10)
location total_cases_pm total_deaths_pm \
iso_code
SGP Singapore 531,183.84 346.64
BHR Bahrain 473,167.02 1,043.31
BRN Brunei 763,475.44 396.44
QAT Qatar 190,908.72 256.02
PER Peru 133,239.00 6,507.66
MLT Malta 227,422.82 1,687.63
MDV Maldives 356,423.66 603.29
LUX Luxembourg 603,439.46 1,544.16
BGR Bulgaria 195,767.89 5,703.52
BGD Bangladesh 11,959.46 172.22
median_age gdp_per_capita scores
iso_code
SGP 42.40 85,535.38 -0.26
BHR 32.40 43,290.71 -0.09
BRN 32.40 71,809.25 -0.09
QAT 31.90 116,935.60 -0.08
PER 29.10 12,236.71 -0.08
MLT 42.40 36,513.32 -0.06
MDV 30.60 15,183.62 -0.06
LUX 39.70 94,277.96 -0.06
BGR 44.70 18,563.31 -0.04
BGD 27.50 3,523.98 -0.04
-
绘制异常值和正常值:
ax = plt.axes(projection='3d') ax.set_title('Isolation Forest Anomaly Detection') ax.set_zlabel("Cases Per Million") ax.set_xlabel("GDP Per Capita") ax.set_ylabel("Median Age") ax.scatter3D(inlier.gdp_per_capita, inlier.median_age, inlier.total_cases_pm, label="inliers", c="blue") ax.scatter3D(outlier.gdp_per_capita, outlier.median_age, outlier.total_cases_pm, label="outliers", c="red") ax.legend() plt.tight_layout() plt.show()
这将生成以下图表:
图 4.10:按人均 GDP、中位年龄和每百万人病例的正常值和异常值国家
上述步骤演示了使用孤立森林作为异常检测的替代方法。
它是如何工作的……
在本配方中,我们使用孤立森林的方式类似于前一配方中使用 KNN 的方式。在步骤 3中,我们传递了一个标准化的数据集给孤立森林的fit方法,然后使用它的predict和decision_function方法来获取异常标志和分数,分别在步骤 4中使用异常标志将数据分成正常值和异常值。
我们在步骤 5中绘制正常值和异常值。由于图中只有三个维度,它并没有完全捕捉到我们孤立森林模型中的所有特征,但是异常值(红色点)明显具有更高的人均 GDP 和中位年龄;这些通常位于正常值的右侧和后方。
孤立森林的结果与 KNN 结果非常相似。新加坡、巴林和卡塔尔是四个最高(最负)异常分数中的三个国家。
还有更多……
孤立森林是 KNN 的一个很好的替代方法,特别是在处理大数据集时。其算法的高效性使其能够处理大样本和大量变量。
我们在过去三个食谱中使用的异常检测技术旨在改进多变量分析和机器学习模型的训练。然而,我们可能希望排除它们帮助我们在分析过程中较早识别的异常值。例如,如果排除卡塔尔对我们的建模有意义,那么排除卡塔尔在某些描述性统计中的数据可能也是合理的。
另见
除了对异常检测有用外,Isolation Forest 算法在直观上也相当令人满意。(我认为 KNN 也可以说是一样的。)你可以在这里阅读更多关于 Isolation Forest 的内容:cs.nju.edu.cn/zhouzh/zhouzh.files/publication/icdm08b.pdf。
使用 PandasAI 识别异常值
我们可以使用 PandasAI 来支持本章中我们为识别异常值所做的一些工作。我们可以根据单变量分析检查极端值。我们还可以查看双变量和多变量关系。PandasAI 还将帮助我们轻松生成可视化。
准备工作
你需要安装 PandasAI 以运行这个食谱中的代码。你可以通过pip install pandasai来安装。我们将再次使用 COVID-19 数据,这些数据可以在 GitHub 仓库中找到,代码也是如此。
你还需要一个来自 OpenAI 的 API 密钥。你可以在platform.openai.com获取一个。你需要注册一个账户,然后点击右上角的个人资料,再点击查看 API 密钥。
PandasAI 库发展迅速,自从我开始编写这本书以来,一些内容已经发生了变化。我在本食谱中使用的是 PandasAI 版本 2.0.30。使用的 Pandas 版本也很重要。我在本食谱中使用的是 Pandas 版本 2.2.1。
如何操作...
我们通过以下步骤创建一个 PandasAI 实例,并用它来查找 COVID-19 数据中的极端和意外值:
-
我们导入
pandas和PandasAI库:import pandas as pd from pandasai.llm.openai import OpenAI from pandasai import SmartDataframe llm = OpenAI(api_token="Your API key") -
我们加载 COVID-19 数据并创建一个
PandasAI SmartDataframe:covidtotals = pd.read_csv("data/covidtotals.csv") covidtotalssdf = SmartDataframe(covidtotals, config={"llm": llm}) -
我们可以将自然语言查询传递给
chat方法的SmartDataframe。这包括绘制图表:covidtotalssdf.chat("Plot histogram of total cases per million")
这生成了以下图表:
图 4.11:每百万总病例的直方图
-
我们还可以创建一个箱型图:
covidtotalssdf.chat("Show box plot of total cases per million")
这生成了以下图表:
图 4.12:每百万总病例的箱型图
-
我们还可以显示病例与死亡之间关系的散点图。我们指定希望使用
regplot,以确保绘制回归线:covidtotalssdf.chat("regplot total_deaths_pm on total_cases_pm")
这生成了以下图表:
图 4.13:病例与死亡之间关系的散点图
-
显示总病例的高值和低值:
covidtotalssdf.chat("Show total cases per million for 7 highest values and 7 lowest values of total cases per million sorted by total cases per million")iso_code location total_cases_pm 190 SVN Slovenia 639,408 67 FRO Faeroe Islands 652,484 194 KOR South Korea 667,207 12 AUT Austria 680,263 180 SMR San Marino 750,727 52 CYP Cyprus 760,161 30 BRN Brunei 763,475 228 YEM Yemen 354 148 NER Niger 363 40 TCD Chad 434 204 TZA Tanzania 660 186 SLE Sierra Leone 904 32 BFA Burkina Faso 975 54 COD Democratic Republic of Congo 1,003
这将高组的数据按升序排序,然后对低组的数据进行升序排序。
-
我们可以找到每个地区病例数最多的国家:
covidtotalssdf.chat("Show total cases per million for locations with highest total cases per million in each region")location total_cases_pm region Caribbean Martinique 626,793 Central Africa Sao Tome and Principe 29,614 Central America Costa Rica 237,539 Central Asia Armenia 162,356 East Africa Reunion 507,765 East Asia Brunei 763,475 Eastern Europe Cyprus 760,161 North Africa Tunisia 93,343 North America Saint Pierre and Miquelon 582,158 Oceania / Aus Niue 508,709 South America Falkland Islands 505,919 South Asia Bahrain 473,167 Southern Africa Saint Helena 401,037 West Africa Cape Verde 108,695 West Asia Israel 512,388 Western Europe San Marino 750,727 -
我们可以让
chat向我们展示病例数较高而死亡数相对较低的国家:covidtotalssdf.chat("Show total cases per million and total deaths per million for locations with high total_cases_pm and low total_deaths_pm")location total_cases_pm total_deaths_pm 30 Brunei 763,475 396 46 Cook Islands 422,910 117 68 Falkland Islands 505,919 0 81 Greenland 211,899 372 93 Iceland 562,822 499 126 Marshall Islands 387,998 409 142 Nauru 424,947 79 150 Niue 508,709 0 156 Palau 346,439 498 167 Qatar 190,909 256 172 Saint Barthelemy 500,910 455 173 Saint Helena 401,037 0 177 Saint Pierre and Miquelon 582,158 340 187 Singapore 531,184 347 209 Tonga 158,608 112 214 Tuvalu 259,638 88 217 United Arab Emirates 113,019 249 226 Vietnam 118,387 440
这些只是 PandasAI 如何帮助我们通过极少的代码找到异常值或意外值的几个示例。
它是如何工作的……
我们在这个食谱中使用了 PandasAI 和 OpenAI 提供的大型语言模型。你只需要一个 API 令牌。你可以从platform.openai.com获取。获得令牌后,开始通过自然语言查询数据库时,只需导入 OpenAI 和SmartDataframe模块并实例化一个SmartDataframe对象。
我们在步骤 2中创建了一个SmartDataframe对象,代码为covidtotalssdf = SmartDataframe(covidtotals, config={"llm": llm})。一旦我们有了SmartDataframe,就可以通过其chat方法传递各种自然语言指令。在这个食谱中,这些指令从请求可视化、查找最高和最低值到检查数据子集的值不等。
定期检查pandasai.log文件是一个好主意,该文件与您的 Python 脚本位于同一文件夹中。以下是 PandasAI 在响应covidtotalssdf.chat("Show total cases per million and total deaths per million for locationss with high total_cases_pm and low total_deaths_pm")时生成的代码:
import pandas as pd
# Filter rows with high total_cases_pm and low total_deaths_pm
filtered_df = dfs[0][(dfs[0]['total_cases_pm'] > 100000) & (dfs[0]['total_deaths_pm'] < 500)]
# Select only the required columns
result_df = filtered_df[['location', 'total_cases_pm', 'total_deaths_pm']]
result = {"type": "dataframe", "value": result_df}
```
```py
2024-04-18 09:30:01 [INFO] 执行步骤 4:CachePopulation
2024-04-18 09:30:01 [INFO] 执行步骤 5:CodeCleaning
2024-04-18 09:30:01 [INFO]
代码运行中:
filtered_df = dfs[0][(dfs[0]['total_cases_pm'] > 100000) & (dfs[0]['total_deaths_pm'] < 500)]
result_df = filtered_df[['location', 'total_cases_pm', 'total_deaths_pm']]
result = {'type': 'dataframe', 'value': result_df}
还有更多…
我们在步骤 4中生成了一个箱型图,这是一个非常有用的工具,用于可视化连续变量的分布。箱体显示了四分位距,即第一四分位数和第三四分位数之间的距离。箱内的线表示中位数。在第五章《使用可视化识别异常值》中,我们会详细讲解箱型图。
另请参见
第三章《衡量您的数据》中关于使用生成型 AI 创建描述性统计的食谱提供了更多信息,介绍了 PandasAI 如何使用 OpenAI,以及如何生成总体和按组统计数据及可视化。我们在本书中随时使用 PandasAI,凡是它能够改善我们的数据准备工作或简化操作时,我们都会使用它。
总结
本章介绍了用于识别数据中异常值的 pandas 工具。我们探索了多种单变量、双变量和多变量方法,用以检测足够偏离范围或其他异常的观测值,这些异常值可能会扭曲我们的分析。这些方法包括使用四分位距来识别极值,调查与相关变量的关系,以及分别使用线性回归和 KNN 等参数化和非参数化的多变量技术。我们还展示了可视化如何帮助我们更好地理解变量的分布情况,以及它如何与相关变量一起变化。下一章我们将详细探讨如何创建和解释可视化。
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者讨论: