原文:Introduction
译者:飞龙
学习成果
了解 Data 100 的总体目标
了解数据科学生命周期的阶段
数据科学是一个跨学科领域,具有各种应用,并且在解决具有挑战性的社会问题方面具有巨大潜力。通过建立数据科学技能,您可以赋予自己参与和引领塑造您的生活和整个社会对话的能力,无论是与气候变化作斗争、推出多样性倡议,还是其他方面。
这个领域正在迅速发展;现代数据科学中许多关键技术基础在 21 世纪初得到了普及。
它基本上是以人为中心的,并通过定量平衡权衡来促进决策。为了可靠地量化事物,我们必须适当地使用和分析数据,对每一步都要进行批判性思考和怀疑,并考虑我们的决定如何影响他人。
最终,数据科学是将以数据为中心的、计算性的和推理性的思维应用于:
了解世界(科学)。
解决问题(工程)。
对数据科学的真正掌握需要深刻的理论理解和对领域专业知识的牢固掌握。本课程将帮助您建立在前者基础上的技术知识,使您能够获取数据并对世界上最具挑战性和模糊的问题产生有用的见解。
课程目标
为您准备伯克利高级课程,包括数据管理、机器学习和统计学
使您能够在数据科学领域开展职业生涯
使您能够通过计算和推理思维解决现实世界的问题
我们将涵盖的一些主题
Pandas 和 NumPy
探索性数据分析
正则表达式
可视化
抽样
模型设计和损失公式
线性回归
梯度下降
逻辑回归
还有更多!
为了让您成功,我们将 Data 100 中的概念组织成了数据科学生命周期:一个迭代过程,涵盖了数据科学的各种统计和计算构建模块。
数据科学生命周期是对数据科学工作流程的高级概述。这是一个数据科学家在对数据驱动的问题进行彻底分析时应该探索的阶段循环。
数据科学生命周期中存在许多关键思想的变体。在 Data 100 中,我们使用流程图来可视化生命周期的各个阶段。请注意,有两个入口点。
无论是出于好奇还是出于必要,数据科学家不断提出问题。例如,在商业世界中,数据科学家可能对预测某项投资产生的利润感兴趣。在医学领域,他们可能会问一些患者是否比其他人更有可能从治疗中受益。
提出问题是数据科学生命周期开始的主要方式之一。它有助于充分定义问题。在构建问题之前,以下是一些您应该问自己的事情。
我们想要知道什么?
我们试图解决什么问题?
我们想要测试的假设是什么?
我们的成功指标是什么?
生命周期的第二个入口是通过获取数据。对任何问题的仔细分析都需要使用数据。数据可能对我们而言是 readily available,或者我们可能不得不着手收集数据。在这样做时,至关重要的是要问以下问题:
我们有什么数据,我们需要什么数据?
我们如何取样更多的数据?
我们的数据是否代表我们想研究的人群?
关键程序:数据获取,数据清洗
原始数据本身并不具有固有的用处。如果不仔细调查,就不可能辨别出所有变量之间的模式和关系。因此,将纯数据转化为可操作的见解是数据科学家的一项关键工作。例如,我们可以选择问:
我们的数据是如何组织的,它包含了什么?
我们有相关的数据吗?
数据中存在什么偏见、异常或其他问题?
我们如何转换数据以进行有效分析?
关键程序:探索性数据分析,数据可视化。
在观察了数据中的模式之后,我们可以开始回答我们的问题。这可能需要我们预测一个数量(机器学习),或者衡量某种处理的效果(推断)。
从这里,我们可以选择报告我们的结果,或者可能进行更多的分析。我们可能对我们的发现不满意,或者我们的初步探索可能提出了需要新数据的新问题。
数据对世界有何影响?
它是否回答了我们的问题或准确解决了问题?
我们的结论有多可靠,我们能相信这些预测吗?
关键程序:模型创建,预测,推断。
数据科学生命周期旨在成为一组一般性指导方针,而不是一套硬性要求。在探索生命周期的过程中,我们将涵盖数据科学中使用的基本理论和技术。在课程结束时,我们希望您开始把自己看作是一名数据科学家。
因此,我们将首先介绍探索性数据分析中最重要的工具之一:pandas
。
原文:Pandas I
译者:飞龙
学习成果
建立对pandas
和pandas
语法的熟悉度。
学习关键数据结构:DataFrame
、Series
和Index
。
了解提取数据的方法:.loc
、.iloc
和[]
。
在这一系列讲座中,我们将让您直接探索和操纵真实世界的数据。我们将首先介绍pandas
,这是一个流行的 Python 库,用于与表格数据交互。
数据科学家使用各种格式存储的数据。本课程的主要重点是理解表格数据——存储在表格中的数据。
表格数据是数据科学家用来组织数据的最常见系统之一。这在很大程度上是因为表格的简单性和灵活性。表格允许我们将每个观察,或者从个体收集数据的实例,表示为其自己的行。我们可以将每个观察的不同特征,或者特征,记录在单独的列中。
为了看到这一点,我们将探索elections
数据集,该数据集存储了以前年份竞选美国总统的政治候选人的信息。
代码
import pandas as pd
pd.read_csv("data/elections.csv")
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
4 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.574789 |
… | … | … | … | … | … | … |
177 | 2016 | Jill Stein | Green | 1457226 | loss | 1.073699 |
178 | 2020 | Joseph Biden | Democratic | 81268924 | win | 51.311515 |
179 | 2020 | Donald Trump | Republican | 74216154 | loss | |
180 | 2020 | Jo Jorgensen | Libertarian | 1865724 | loss | 1.177979 |
181 | 2020 | Howard Hawkins | Green | 405035 | loss | 0.255731 |
182 行×6 列
在elections
数据集中,每一行代表一个候选人在特定年份竞选总统的一个实例。例如,第一行代表安德鲁·杰克逊在 1824 年竞选总统。每一列代表每个总统候选人的一个特征信息。例如,名为“结果”的列存储候选人是否赢得选举。
你在 Data 8 中的工作帮助你非常熟悉使用和解释以表格格式存储的数据。那时,你使用了datascience
库的Table
类,这是专门为 Data 8 学生创建的特殊编程库。
在 Data 100 中,我们将使用编程库pandas
,这在数据科学界被普遍接受为操纵表格数据的行业和学术标准工具(也是我们熊猫吉祥物 Petey 的灵感来源)。
使用pandas
,我们可以
以表格格式排列数据。
提取由特定条件过滤的有用信息。
对数据进行操作以获得新的见解。
将NumPy
函数应用于我们的数据(我们来自 Data 8 的朋友)。
执行矢量化计算以加快我们的分析速度(实验室 1)。
Series
、DataFrame
和索引要开始我们在pandas
中的工作,我们必须首先将库导入到我们的 Python 环境中。这将允许我们在我们的代码中使用pandas
数据结构和方法。
# `pd` is the conventional alias for Pandas, as `np` is for NumPy
import pandas as pd
pandas
中有三种基本数据结构:
Series:1D 带标签的数组数据;最好将其视为列数据。
DataFrame:带有行和列的 2D 表格数据。
索引:一系列行/列标签。
DataFrame
,Series
和索引可以在以下图表中以可视化方式表示,该图表考虑了elections
数据集的前几行。
注意DataFrame是一个二维对象——它包含行和列。上面的Series是这个DataFrame
的一个单独的列,即Result
列。两者都包含一个索引,或者共享的行标签列表(从 0 到 4 的整数,包括 0)。
Series 表示DataFrame
的一列;更一般地,它可以是任何 1 维类似数组的对象。它包含:
相同类型的值序列。
索引称为数据标签的序列。
在下面的单元格中,我们创建了一个名为s
的Series
。
s = pd.Series(["welcome", "to", "data 100"])
s
0 welcome
1 to
2 data 100
dtype: object
s.values # Data values contained within the Series
array(['welcome', 'to', 'data 100'], dtype=object)
s.index # The Index of the Series
RangeIndex(start=0, stop=3, step=1)
默认情况下,Series 的索引是从 0 开始的整数的顺序列表。可以将所需索引的手动指定列表传递给index
参数。
s = pd.Series([-1, 10, 2], index = ["a", "b", "c"])
s
a -1
b 10
c 2
dtype: int64
s.index
Index(['a', 'b', 'c'], dtype='object')
初始化后也可以更改索引。
s.index = ["first", "second", "third"]
s
first -1
second 10
third 2
dtype: int64
s.index
Index(['first', 'second', 'third'], dtype='object')
Series
中的选择就像在使用NumPy
数组时一样,我们可以从Series
中选择单个值或一组值。为此,有三种主要方法:
单个标签。
标签列表。
过滤条件。
为了证明这一点,让我们定义ser
系列。
ser = pd.Series([4, -2, 0, 6], index = ["a", "b", "c", "d"])
ser
a 4
b -2
c 0
d 6
dtype: int64
ser["a"] # We return the value stored at the Index label "a"
4
ser[["a", "c"]] # We return a *Series* of the values stored at the Index labels "a" and "c"
a 4
c 0
dtype: int64
也许从Series
中选择数据的最有趣(和有用)的方法是使用过滤条件。
首先,我们对Series
应用布尔运算。这将创建一个新的布尔值系列。
ser > 0 # Filter condition: select all elements greater than 0
a True
b False
c False
d True
dtype: bool
然后,我们使用这个布尔条件来索引我们原始的Series
。pandas
将只选择原始Series
中满足条件的条目。
ser[ser > 0]
a 4
d 6
dtype: int64
通常,我们将使用Series
的角度来处理它们,认为它们是DataFrame
中的列。我们可以将DataFrame视为所有共享相同索引的Series的集合。
在 Data 8 中,您遇到了datascience
库的Table
类,它表示表格数据。在 Data 100 中,我们将使用pandas
库的DataFrame
类。
DataFrame
有许多创建DataFrame
的方法。在这里,我们将介绍最流行的方法:
从 CSV 文件中。
使用列名和列表。
从字典中。
从Series
中。
更一般地,创建DataFrame
的语法是:pandas.DataFrame(data, index, columns)
。
在 Data 100 中,我们的数据通常以 CSV(逗号分隔值)文件格式存储。我们可以通过将数据路径作为参数传递给以下pandas
函数来将 CSV 文件导入DataFrame
。
pd.read_csv("filename.csv")
现在,我们可以认识到pandas
DataFrame 表示的是elections
数据集。
elections = pd.read_csv("data/elections.csv")
elections
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
4 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.574789 |
… | … | … | … | … | … | … |
177 | 2016 | Jill Stein | Green | 1457226 | loss | 1.073699 |
178 | 2020 | Joseph Biden | Democratic | 81268924 | win | 51.311515 |
179 | 2020 | Donald Trump | Republican | 74216154 | loss | 46.858542 |
180 | 2020 | Jo Jorgensen | Libertarian | 1865724 | loss | 1.177979 |
181 | 2020 | Howard Hawkins | Green | 405035 | loss | 0.255731 |
182 行×6 列
这段代码将我们的“DataFrame”对象存储在“选举”变量中。经过检查,我们的“选举”DataFrame 有 182 行和 6 列(“年份”,“候选人”,“党派”,“普选票”,“结果”,“%”)。每一行代表一条记录——在我们的例子中,是某一年的总统候选人。每一列代表记录的一个属性或特征。
我们现在将探讨如何使用我们自己的数据创建“DataFrame”。
考虑以下例子。第一个代码单元创建了一个只有一个列“Numbers”的“DataFrame”。第二个创建了一个有“Numbers”和“Description”两列的“DataFrame”。请注意,需要一个二维值列表来初始化第二个“DataFrame”——每个嵌套列表代表一行数据。
df_list = pd.DataFrame([1, 2, 3], columns=["Numbers"])
df_list
Numbers | |
---|---|
0 | 1 |
1 | 2 |
2 | 3 |
df_list = pd.DataFrame([[1, "one"], [2, "two"]], columns = ["Number", "Description"])
df_list
Numbers | Description | |
---|---|---|
0 | 1 | one |
1 | 2 | two |
第三种(更常见的)创建“DataFrame”的方法是使用字典。字典的键代表列名,字典的值代表列的值。
以下是实现这种方法的两种方式。第一种是基于指定“DataFrame”的列,而第二种是基于指定“DataFrame”的行。
df_dict = pd.DataFrame({"Fruit": ["Strawberry", "Orange"], "Price": [5.49, 3.99]})
df_dict
Fruit | Price | |
---|---|---|
0 | Strawberry | 5.49 |
1 | Orange | 3.99 |
df_dict = pd.DataFrame([{"Fruit":"Strawberry", "Price":5.49}, {"Fruit": "Orange", "Price":3.99}])
df_dict
Fruit | Price | |
---|---|---|
0 | Strawberry | 5.49 |
1 | Orange | 3.99 |
早些时候,我们解释了“Series”与“DataFrame”中的列是同义词。因此,“DataFrame”相当于共享相同索引的“Series”集合。
事实上,我们可以通过合并两个或更多的“Series”来初始化“DataFrame”。
# Notice how our indices, or row labels, are the same
s_a = pd.Series(["a1", "a2", "a3"], index = ["r1", "r2", "r3"])
s_b = pd.Series(["b1", "b2", "b3"], index = ["r1", "r2", "r3"])
pd.DataFrame({"A-column": s_a, "B-column": s_b})
A-column | B-column | |
---|---|---|
r1 | a1 | b1 |
r2 | a2 | b2 |
r3 | a3 | b3 |
pd.DataFrame(s_a)
0 | |
---|---|
r1 | a1 |
r2 | a2 |
r3 | a3 |
s_a.to_frame()
0 | |
---|---|
r1 | a1 |
r2 | a2 |
r3 | a3 |
在技术上,索引不一定是整数,也不一定是唯一的。例如,我们可以将“选举”DataFrame 的索引设置为总统候选人的名字。
# Creating a DataFrame from a CSV file and specifying the Index column
elections = pd.read_csv("data/elections.csv", index_col = "Candidate")
elections
Year | Party | Popular vote | Result | % | |
---|---|---|---|---|---|
Candidate | |||||
Andrew Jackson | 1824 | Democratic-Republican | 151271 | loss | 57.210122 |
John Quincy Adams | 1824 | Democratic-Republican | 113142 | win | 42.789878 |
Andrew Jackson | 1828 | Democratic | 642806 | win | 56.203927 |
John Quincy Adams | 1828 | National Republican | 500897 | loss | 43.796073 |
Andrew Jackson | 1832 | Democratic | 702735 | win | 54.574789 |
… | … | … | … | … | … |
Jill Stein | 2016 | Green | 1457226 | loss | 1.073699 |
Joseph Biden | 2020 | Democratic | 81268924 | win | 51.311515 |
Donald Trump | 2020 | Republican | 74216154 | loss | 46.858542 |
Jo Jorgensen | 2020 | Libertarian | 1865724 | loss | 1.177979 |
Howard Hawkins | 2020 | Green | 405035 | loss | 0.255731 |
182 行×5 列
我们还可以选择一个新的列,并将其设置为 DataFrame 的索引。例如,我们可以将“选举”DataFrame 的索引设置为候选人的党派。
elections.reset_index(inplace = True) # Resetting the index so we can set the Index again
# This sets the index to the "Party" column
elections.set_index("Party")
Candidate | Year | Popular vote | Result | % | |
---|---|---|---|---|---|
Party | |||||
Democratic-Republican | Andrew Jackson | 1824 | 151271 | loss | 57.210122 |
Democratic-Republican | John Quincy Adams | 1824 | 113142 | win | 42.789878 |
Democratic | Andrew Jackson | 1828 | 642806 | win | 56.203927 |
National Republican | John Quincy Adams | 1828 | 500897 | loss | 43.796073 |
Democratic | Andrew Jackson | 1832 | 702735 | win | 54.574789 |
… | … | … | … | … | … |
Green | Jill Stein | 2016 | 1457226 | loss | 1.073699 |
Democratic | Joseph Biden | 2020 | 81268924 | win | 51.311515 |
Republican | Donald Trump | 2020 | 74216154 | loss | |
Libertarian | Jo Jorgensen | 2020 | 1865724 | loss | 1.177979 |
Green | Howard Hawkins | 2020 | 405035 | loss | 0.255731 |
182 行×5 列
如果需要,我们可以将索引恢复为默认的整数列表。
# This resets the index to be the default list of integer
elections.reset_index(inplace=True)
elections.index
RangeIndex(start=0, stop=182, step=1)
还需要注意的是,构成索引的行标签不一定是唯一的。虽然索引值可以是唯一的和数字的,充当行号,但它们也可以是命名的和非唯一的。
这里我们看到唯一和数字的索引值。
然而,这里的索引值是非唯一的。
DataFrame
属性:索引、列和形状另一方面,DataFrame
中的列名几乎总是唯一的。回顾elections
数据集,有两列命名为“Candidate”是没有意义的。
有时,您可能希望提取这些不同的值,特别是行和列标签的列表。
对于索引/行标签,请使用DataFrame.index
:
elections.set_index("Party", inplace = True)
elections.index
Index(['Democratic-Republican', 'Democratic-Republican', 'Democratic',
'National Republican', 'Democratic', 'National Republican',
'Anti-Masonic', 'Whig', 'Democratic', 'Whig',
...
'Constitution', 'Republican', 'Independent', 'Libertarian',
'Democratic', 'Green', 'Democratic', 'Republican', 'Libertarian',
'Green'],
dtype='object', name='Party', length=182)
对于列标签,请使用DataFrame.columns
:
elections.columns
Index(['index', 'Candidate', 'Year', 'Popular vote', 'Result', '%'], dtype='object')
对于 DataFrame 的形状,我们可以使用DataFrame.shape
:
elections.shape
(182, 6)
DataFrame
中的切片现在我们已经更多地了解了DataFrame
,让我们深入了解它们的功能。
DataFrame
类的 API(应用程序编程接口)是庞大的。在本节中,我们将讨论DataFrame
API 的几种方法,这些方法允许我们提取数据子集。
操作DataFrame
最简单的方法是提取行和列的子集,称为切片。
我们可能希望提取数据的常见方式包括:
DataFrame
中的第一行或最后一行。
具有特定标签的数据。
特定位置的数据。
我们将使用 DataFrame 类的四种主要方法:
.head
和.tail
.loc
.iloc
[]
.head
和.tail
提取数据我们希望提取数据的最简单的情况是当我们只想选择DataFrame
的前几行或最后几行时。
要提取 DataFrame df
的前n
行,我们使用语法df.head(n)
。
elections = pd.read_csv("data/elections.csv")
# Extract the first 5 rows of the DataFrame
elections.head(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
4 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.574789 |
类似地,调用df.tail(n)
允许我们提取 DataFrame 的最后n
行。
# Extract the last 5 rows of the DataFrame
elections.tail(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
177 | 2016 | Jill Stein | Green | 1457226 | loss | 1.073699 |
178 | 2020 | Joseph Biden | Democratic | 81268924 | win | 51.311515 |
179 | 2020 | Donald Trump | Republican | 74216154 | loss | |
180 | 2020 | Jo Jorgensen | Libertarian | 1865724 | loss | 1.177979 |
181 | 2020 | Howard Hawkins | Green | 405035 | loss | 0.255731 |
.loc
进行索引对于使用特定列或索引标签提取数据的更复杂任务,我们可以使用.loc
。.loc
访问器允许我们指定我们希望提取的行和列的标签。标签(通常称为索引)是 DataFrame 最左边的粗体文本,而列标签是 DataFrame 顶部的列名。
使用.loc
获取数据时,我们必须指定数据所在的行和列标签。行标签是.loc
函数的第一个参数;列标签是第二个参数。
.loc
的参数可以是:
一个单一的值。
一个切片。
一个列表。
例如,要选择单个值,我们可以从elections
DataFrame
中选择标记为0
的行和标记为Candidate
的列。
elections.loc[0, 'Candidate']
'Andrew Jackson'
请记住,只传入一个参数作为单个值将产生一个Series
。下面,我们提取了"Popular vote"
列的子集作为Series
。
elections.loc[[87, 25, 179], "Popular vote"]
87 15761254
25 848019
179 74216154
Name: Popular vote, dtype: int64
要选择多个行和列,我们可以使用 Python 切片表示法。在这里,我们选择从标签0
到3
的行和从标签"Year"
到"Popular vote"
的列。
elections.loc[0:3, 'Year':'Popular vote']
Year | Candidate | Party | Popular vote | |
---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 |
2 | 1828 | Andrew Jackson | Democratic | 642806 |
3 | 1828 | John Quincy Adams | National Republican | 500897 |
假设相反,我们想要提取elections
DataFrame 中前四行的所有列值。这时,缩写:
就很有用。
elections.loc[0:3, :]
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
我们可以使用相同的缩写来提取所有行。
elections.loc[:, ["Year", "Candidate", "Result"]]
Year | Candidate | Result | |
---|---|---|---|
0 | 1824 | Andrew Jackson | loss |
1 | 1824 | John Quincy Adams | win |
2 | 1828 | Andrew Jackson | win |
3 | 1828 | John Quincy Adams | loss |
4 | 1832 | Andrew Jackson | win |
… | … | … | … |
177 | 2016 | Jill Stein | loss |
178 | 2020 | Joseph Biden | win |
179 | 2020 | Donald Trump | |
180 | 2020 | Jo Jorgensen | loss |
181 | 2020 | Howard Hawkins | loss |
182 行×3 列
有几件事情我们应该注意。首先,与传统的 Python 不同,pandas
允许我们切片字符串值(在我们的例子中,是列标签)。其次,使用.loc
进行切片是包含的。请注意,我们的结果DataFrame
包括我们指定的切片标签之间和包括这些标签的每一行和列。
同样,我们可以使用列表在elections
DataFrame 中获取多行和多列。
elections.loc[[0, 1, 2, 3], ['Year', 'Candidate', 'Party', 'Popular vote']]
Year | Candidate | Party | Popular vote | |
---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 |
2 | 1828 | Andrew Jackson | Democratic | 642806 |
3 | 1828 | John Quincy Adams | National Republican | 500897 |
最后,我们可以互换列表和切片表示法。
elections.loc[[0, 1, 2, 3], :]
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
.iloc
进行索引使用.iloc
进行切片与.loc
类似。但是,.iloc
使用的是行和列的索引位置,而不是标签(想一想:loc 使用labels;iloc 使用indices)。.iloc
函数的参数也类似地行为 —— 允许单个值、列表、索引和这些的任意组合。
让我们开始重现上面的结果。我们将从我们的elections
DataFrame 中选择第一个总统候选人开始:
# elections.loc[0, "Candidate"] - Previous approach
elections.iloc[0, 1]
'Andrew Jackson'
请注意,.loc
和.iloc
的第一个参数是相同的。这是因为标签为 0 的行恰好在elections
DataFrame 的
0
t
h
0^{th}
0th(或者说第一个位置)上。通常情况下,任何 DataFrame 中的行标签都是从 0 开始递增的,这一点是正确的。
并且,就像以前一样,如果我们只传入一个单一的值参数,我们的结果将是一个Series
。
elections.iloc[[1,2,3],1]
1 John Quincy Adams
2 Andrew Jackson
3 John Quincy Adams
Name: Candidate, dtype: object
然而,当我们使用.iloc
选择前四行和列时,我们注意到了一些东西。
# elections.loc[0:3, 'Year':'Popular vote'] - Previous approach
elections.iloc[0:4, 0:4]
Year | Candidate | Party | Popular vote | |
---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 |
2 | 1828 | Andrew Jackson | Democratic | 642806 |
3 | 1828 | John Quincy Adams | National Republican | 500897 |
切片在.iloc
中不再是包容的——它是排他的。换句话说,使用.iloc
时,切片的右端点不包括在内。这是pandas
语法的微妙之处之一;通过练习你会习惯的。
列表行为与预期的一样。
#elections.loc[[0, 1, 2, 3], ['Year', 'Candidate', 'Party', 'Popular vote']] - Previous Approach
elections.iloc[[0, 1, 2, 3], [0, 1, 2, 3]]
Year | Candidate | Party | Popular vote | |
---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 |
2 | 1828 | Andrew Jackson | Democratic | 642806 |
3 | 1828 | John Quincy Adams | National Republican | 500897 |
就像使用.loc
一样,我们可以使用冒号与.iloc
一起提取所有行或列。
elections.iloc[:, 0:3]
Year | Candidate | Party | |
---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican |
1 | 1824 | John Quincy Adams | Democratic-Republican |
2 | 1828 | Andrew Jackson | Democratic |
3 | 1828 | John Quincy Adams | National Republican |
4 | 1832 | Andrew Jackson | Democratic |
… | … | … | … |
177 | 2016 | Jill Stein | Green |
178 | 2020 | Joseph Biden | Democratic |
179 | 2020 | Donald Trump | Republican |
180 | 2020 | Jo Jorgensen | Libertarian |
181 | 2020 | Howard Hawkins | Green |
182 行×3 列
这个讨论引出了一个问题:我们什么时候应该使用.loc
和.iloc
?在大多数情况下,.loc
通常更安全。你可以想象,当应用于数据集的顺序可能会改变时,.iloc
可能会返回不正确的值。然而,.iloc
仍然是有用的——例如,如果你正在查看一个排序好的电影收入的DataFrame
,并且想要得到给定年份的收入中位数,你可以使用.iloc
来索引到中间。
总的来说,重要的是要记住:
.loc
执行label-based 提取。
.iloc
执行integer-based 提取。
[]
进行索引[]
选择运算符是最令人困惑的,但也是最常用的。它只接受一个参数,可以是以下之一:
一系列行号。
一系列列标签。
单列标签。
也就是说,[]
是上下文相关的。让我们看一些例子。
假设我们想要我们的elections
DataFrame 的前四行。
elections[0:4]
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
假设我们现在想要前四列。
elections[["Year", "Candidate", "Party", "Popular vote"]]
Year | Candidate | Party | Popular vote | |
---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 |
2 | 1828 | Andrew Jackson | Democratic | 642806 |
3 | 1828 | John Quincy Adams | National Republican | 500897 |
4 | 1832 | Andrew Jackson | Democratic | 702735 |
… | … | … | … | … |
177 | 2016 | Jill Stein | Green | 1457226 |
178 | 2020 | Joseph Biden | Democratic | 81268924 |
179 | 2020 | Donald Trump | Republican | 74216154 |
180 | 2020 | Jo Jorgensen | Libertarian | 1865724 |
181 | 2020 | Howard Hawkins | Green | 405035 |
182 行×4 列
最后,[]
允许我们仅提取Candidate
列。
elections["Candidate"]
0 Andrew Jackson
1 John Quincy Adams
2 Andrew Jackson
3 John Quincy Adams
4 Andrew Jackson
...
177 Jill Stein
178 Joseph Biden
179 Donald Trump
180 Jo Jorgensen
181 Howard Hawkins
Name: Candidate, Length: 182, dtype: object
输出是一个Series
!在本课程中,我们将非常熟悉[]
,特别是用于选择列。在实践中,[]
比.loc
更常见,特别是因为它更加简洁。
pandas
库非常庞大,包含许多有用的函数。这是一个指向文档的链接。我们当然不指望您记住库中的每一个方法。
入门级的 Data 100 pandas
讲座将提供对关键数据结构和方法的高层次视图,这些将构成您pandas
知识的基础。本课程的目标是帮助您建立对真实世界编程实践的熟悉度……谷歌搜索!您的问题的答案可以在文档、Stack Overflow 等地方找到。能够搜索、阅读和实施文档是任何数据科学家的重要生活技能。
有了这个,我们将继续学习 Pandas II。
原文:Pandas II
译者:飞龙
学习成果
继续熟悉pandas
语法。
使用条件选择从DataFrame
中提取数据。
识别聚合有用的情况,并确定执行聚合的正确技术。
上次,我们介绍了pandas
库作为处理数据的工具包。我们学习了DataFrame
和Series
数据结构,熟悉了操作表格数据的基本语法,并开始编写我们的第一行pandas
代码。
在本讲座中,我们将开始深入了解一些高级的pandas
语法。当我们逐步学习这些新的代码片段时,您可能会发现跟着自己的笔记本会很有帮助。
我们将开始加载babynames
数据集。
代码
# This code pulls census data and loads it into a DataFrame
# We won't cover it explicitly in this class, but you are welcome to explore it on your own
import pandas as pd
import numpy as np
import urllib.request
import os.path
import zipfile
data_url = "https://www.ssa.gov/oact/babynames/state/namesbystate.zip"
local_filename = "data/babynamesbystate.zip"
if not os.path.exists(local_filename): # If the data exists don't download again
with urllib.request.urlopen(data_url) as resp, open(local_filename, 'wb') as f:
f.write(resp.read())
zf = zipfile.ZipFile(local_filename, 'r')
ca_name = 'STATE.CA.TXT'
field_names = ['State', 'Sex', 'Year', 'Name', 'Count']
with zf.open(ca_name) as fh:
babynames = pd.read_csv(fh, header=None, names=field_names)
babynames.head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
条件选择允许我们选择满足某些指定条件的DataFrame
中的行的子集。
要了解如何使用条件选择,我们必须看一下.loc
和[]
方法的另一个可能的输入 - 布尔数组,它只是一个数组或Series
,其中每个元素都是True
或False
。这个布尔数组的长度必须等于DataFrame
中的行数。它将返回数组中对应True
值的所有行。我们在上一堂课中从Series
中执行条件提取时使用了非常类似的技术。
为了看到这一点,让我们选择我们DataFrame
的前 10 行中的所有偶数索引行。
# Ask yourself: why is :9 is the correct slice to select the first 10 rows?
babynames_first_10_rows = babynames.loc[:9, :]
# Notice how we have exactly 10 elements in our boolean array argument
babynames_first_10_rows[[True, False, True, False, True, False, True, False, True, False]]
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
2 | CA | F | 1910 | Dorothy | 220 |
4 | CA | F | 1910 | Frances | 134 |
6 | CA | F | 1910 | Evelyn | 126 |
8 | CA | F | 1910 | Virginia | 101 |
我们可以使用.loc
执行类似的操作。
babynames_first_10_rows.loc[[True, False, True, False, True, False, True, False, True, False], :]
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
2 | CA | F | 1910 | Dorothy | 220 |
4 | CA | F | 1910 | Frances | 134 |
6 | CA | F | 1910 | Evelyn | 126 |
8 | CA | F | 1910 | Virginia | 101 |
这些技术在这个例子中运行良好,但是你可以想象在更大的DataFrame
中为每一行列出True
和False
可能会有多么乏味。为了简化事情,我们可以提供一个逻辑条件作为.loc
或[]
的输入,返回一个具有必要长度的布尔数组。
例如,要返回与F
性别相关的所有名称:
# First, use a logical condition to generate a boolean array
logical_operator = (babynames["Sex"] == "F")
# Then, use this boolean array to filter the DataFrame
babynames[logical_operator].head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
从上一讲中回忆,.head()
将只返回DataFrame
中的前几行。实际上,babynames[logical operator]
包含与原始babynames
DataFrame
中性别为"F"
的条目一样多的行。
在这里,logical_operator
评估为长度为 407428 的布尔值Series
。
代码
print("There are a total of {} values in 'logical_operator'".format(len(logical_operator)))
There are a total of 407428 values in 'logical_operator'
从第 0 行开始到第 239536 行的行评估为True
,因此在DataFrame
中返回。从第 239537 行开始的行评估为False
,因此在输出中被省略。
代码
print("The 0th item in this 'logical_operator' is: {}".format(logical_operator.iloc[0]))
print("The 239536th item in this 'logical_operator' is: {}".format(logical_operator.iloc[239536]))
print("The 239537th item in this 'logical_operator' is: {}".format(logical_operator.iloc[239537]))
The 0th item in this 'logical_operator' is: True
The 239536th item in this 'logical_operator' is: True
The 239537th item in this 'logical_operator' is: False
将Series
作为babynames[]
的参数传递与使用布尔数组具有相同的效果。实际上,[]
选择运算符可以将布尔Series
、数组和列表作为参数。在整个课程中,这三种方法可以互换使用。
我们也可以使用.loc
来实现类似的结果。
babynames.loc[babynames["Sex"] == "F"].head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
布尔条件可以使用各种位运算符进行组合,从而可以根据多个条件过滤结果。在下表中,p 和 q 是布尔数组或Series
。
符号 | 用法 | 意义 |
---|---|---|
~ | ~p | 返回 p 的否定 |
| | p | q | p 或 q |
& | p & q | p 和 q |
^ | p ^ q | p 异或 q(排他或) |
当使用逻辑运算符结合多个条件时,我们用一组括号()
括起每个单独的条件。这样可以对pandas
评估您的逻辑施加操作顺序,并可以避免代码错误。
例如,如果我们想要返回所有性别为“F”,出生在 2000 年之前的名字数据,我们可以写成:
babynames[(babynames["Sex"] == "F") & (babynames["Year"] < 2000)].head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
如果我们想要返回所有性别为“F”或出生在 2000 年之前的所有名字数据,我们可以写成:
babynames[(babynames["Sex"] == "F") | (babynames["Year"] < 2000)].head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
布尔数组选择是一个有用的工具,但对于复杂条件可能导致代码过于冗长。在下面的示例中,我们的布尔条件足够长,以至于需要多行代码来编写。
# Note: The parentheses surrounding the code make it possible to break the code on to multiple lines for readability
(
babynames[(babynames["Name"] == "Bella") |
(babynames["Name"] == "Alex") |
(babynames["Name"] == "Ani") |
(babynames["Name"] == "Lisa")]
).head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
6289 | CA | F | 1923 | Bella | 5 |
7512 | CA | F | 1925 | Bella | 8 |
12368 | CA | F | 1932 | Lisa | 5 |
14741 | CA | F | 1936 | Lisa | 8 |
17084 | CA | F | 1939 | Lisa | 5 |
幸运的是,pandas
提供了许多构建布尔过滤器的替代方法。
.isin
函数就是一个例子。该方法评估Series
中的值是否包含在不同序列(列表、数组或Series
)的值中。在下面的单元格中,我们用更简洁的代码实现了与上面的DataFrame
等效的结果。
names = ["Bella", "Alex", "Narges", "Lisa"]
babynames["Name"].isin(names).head()
0 False
1 False
2 False
3 False
4 False
Name: Name, dtype: bool
babynames[babynames["Name"].isin(names)].head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
6289 | CA | F | 1923 | Bella | 5 |
7512 | CA | F | 1925 | Bella | 8 |
12368 | CA | F | 1932 | Lisa | 5 |
14741 | CA | F | 1936 | Lisa | 8 |
17084 | CA | F | 1939 | Lisa | 5 |
函数str.startswith
可用于基于Series
对象中的字符串值定义过滤器。它检查Series
中的字符串值是否以特定字符开头。
# Identify whether names begin with the letter "N"
babynames["Name"].str.startswith("N").head()
0 False
1 False
2 False
3 False
4 False
Name: Name, dtype: bool
# Extracting names that begin with the letter "N"
babynames[babynames["Name"].str.startswith("N")].head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
76 | CA | F | 1910 | Norma | 23 |
83 | CA | F | 1910 | Nellie | 20 |
127 | CA | F | 1910 | Nina | 11 |
198 | CA | F | 1910 | Nora | 6 |
310 | CA | F | 1911 | Nellie | 23 |
在许多数据科学任务中,我们可能需要以某种方式更改DataFrame
中包含的列。幸运的是,这样做的语法非常简单。
要向DataFrame
添加新列,我们使用的语法与访问现有列时类似。通过写入df["column"]
来指定新列的名称,然后将其分配给包含将填充此列的值的Series
或数组。
# Create a Series of the length of each name.
babyname_lengths = babynames["Name"].str.len()
# Add a column named "name_lengths" that includes the length of each name
babynames["name_lengths"] = babyname_lengths
babynames.head(5)
State | Sex | Year | Name | Count | name_lengths | |
---|---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 | 4 |
1 | CA | F | 1910 | Helen | 239 | 5 |
2 | CA | F | 1910 | Dorothy | 220 | 7 |
3 | CA | F | 1910 | Margaret | 163 | 8 |
4 | CA | F | 1910 | Frances | 134 | 7 |
如果我们需要稍后修改现有列,可以通过再次引用该列的语法df["column"]
,然后将其重新分配给适当长度的新Series
或数组来实现。
# Modify the “name_lengths” column to be one less than its original value
babynames["name_lengths"] = babynames["name_lengths"] - 1
babynames.head()
State | Sex | Year | Name | Count | name_lengths | |
---|---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 | 3 |
1 | CA | F | 1910 | Helen | 239 | 4 |
2 | CA | F | 1910 | Dorothy | 220 | 6 |
3 | CA | F | 1910 | Margaret | 163 | 7 |
4 | CA | F | 1910 | Frances | 134 | 6 |
我们可以使用.rename()
方法重命名列。.rename()
接受一个将旧列名映射到新列名的字典。
# Rename “name_lengths” to “Length”
babynames = babynames.rename(columns={"name_lengths":"Length"})
babynames.head()
State | Sex | Year | Name | Count | Length | |
---|---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 | 3 |
1 | CA | F | 1910 | Helen | 239 | 4 |
2 | CA | F | 1910 | Dorothy | 220 | 6 |
3 | CA | F | 1910 | Margaret | 163 | 7 |
4 | CA | F | 1910 | Frances | 134 | 6 |
如果我们想要删除DataFrame
的列或行,我们可以调用.drop
方法。使用axis
参数来指定是应该删除列还是行。除非另有说明,否则pandas
将默认假定我们要删除一行。
# Drop our new "Length" column from the DataFrame
babynames = babynames.drop("Length", axis="columns")
babynames.head(5)
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
请注意,我们重新分配了babynames
到babynames.drop(...)
的结果。这是一个微妙但重要的观点:pandas
表操作不会发生在原地。调用df.drop(...)
将输出一个删除感兴趣的行/列的副本df
,而不会修改原始的df
表。
换句话说,如果我们简单地调用:
# This creates a copy of `babynames` and removes the column "Name"...
babynames.drop("Name", axis="columns")
# ...but the original `babynames` is unchanged!
# Notice that the "Name" column is still present
babynames.head(5)
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
pandas
包含大量的函数库,可以帮助缩短设置和从其数据结构中获取信息的过程。在接下来的部分中,我们将概述每个主要实用程序函数,这些函数将帮助我们在 Data 100 中使用。
讨论pandas
提供的所有功能可能需要一个学期的时间!我们将带领您了解最常用的功能,并鼓励您自行探索和实验。
NumPy
和内置函数支持
.shape
.size
.describe()
.sample()
.value_counts()
.unique()
.sort_values()
pandas
文档将是 Data 100 及以后的宝贵资源。
NumPy
pandas
旨在与您在Data 8中遇到的数组计算框架NumPy
良好配合。几乎任何NumPy
函数都可以应用于pandas
的DataFrame
和Series
。
# Pull out the number of babies named Yash each year
yash_count = babynames[babynames["Name"] == "Yash"]["Count"]
yash_count.head()
331824 8
334114 9
336390 11
338773 12
341387 10
Name: Count, dtype: int64
# Average number of babies named Yash each year
np.mean(yash_count)
17.142857142857142
# Max number of babies named Yash born in any one year
np.max(yash_count)
29
.shape
和 .size
.shape
和.size
是Series
和DataFrame
的属性,用于测量结构中存储的数据的“数量”。调用.shape
返回一个元组,其中包含DataFrame
或Series
中存在的行数和列数。.size
用于找到结构中元素的总数,相当于行数乘以列数。
许多函数严格要求参数沿着某些轴的维度匹配。调用这些维度查找函数比手动计算所有项目要快得多。
# Return the shape of the DataFrame, in the format (num_rows, num_columns)
babynames.shape
(407428, 5)
# Return the size of the DataFrame, equal to num_rows * num_columns
babynames.size
2037140
.describe()
如果需要从DataFrame
中获取许多统计信息(最小值,最大值,平均值等),则可以使用.describe()
一次计算所有这些统计信息。
babynames.describe()
Year | Count | |
---|---|---|
count | 407428.000000 | 407428.000000 |
mean | 1985.733609 | 79.543456 |
std | 27.007660 | 293.698654 |
min | 1910.000000 | 5.000000 |
25% | 1969.000000 | 7.000000 |
50% | 1992.000000 | 13.000000 |
75% | 2008.000000 | 38.000000 |
max | 2022.000000 | 8260.000000 |
如果在Series
上调用.describe()
,将报告一组不同的统计信息。
babynames["Sex"].describe()
count 407428
unique 2
top F
freq 239537
Name: Sex, dtype: object
.sample()
正如我们将在本学期后面看到的,随机过程是许多数据科学技术的核心(例如,训练-测试拆分,自助法和交叉验证)。.sample()
让我们快速选择随机条目(如果从DataFrame
调用,则是一行,如果从Series
调用,则是一个值)。
默认情况下,.sample()
选择不替换的条目。传入参数 replace=True
以进行替换采样。
# Sample a single row
babynames.sample()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
119438 | CA | F | 1991 | Madaline | 6 |
当然,这可以与其他方法和运算符(iloc
等)链接在一起。
# Sample 5 random rows, and select all columns after column 2
babynames.sample(5).iloc[:, 2:]
Year | Name | Count | |
---|---|---|---|
360264 | 2006 | Rosalio | 7 |
103104 | 1987 | Paola | 86 |
261680 | 1950 | Perry | 62 |
68249 | 1973 | Lilian | 13 |
239652 | 1910 | Eddie | 5 |
# Randomly sample 4 names from the year 2000, with replacement, and select all columns after column 2
babynames[babynames["Year"] == 2000].sample(4, replace = True).iloc[:, 2:]
Year | Name | Count | |
---|---|---|---|
150871 | 2000 | Josette | 12 |
151230 | 2000 | Alanah | 9 |
342709 | 2000 | Conner | 147 |
150683 | 2000 | Kaci | 14 |
.value_counts()
Series.value_counts()
方法计算Series
中每个唯一值的出现次数。换句话说,它计算每个唯一值出现的次数。这通常对于确定Series
中最常见或最不常见的条目很有用。
在下面的示例中,我们可以通过计算每个名称在babynames
的"Name"
列中出现的次数来确定至少有一个人在该名称下使用了最多年份的名称。请注意,返回值也是一个Series
。
babynames["Name"].value_counts().head()
Jean 223
Francis 221
Guadalupe 218
Jessie 217
Marion 214
Name: Name, dtype: int64
.unique()
如果我们有一个具有许多重复值的Series
,那么.unique()
可以用于仅识别唯一值。在这里,我们返回babynames
中所有名称的数组。
babynames["Name"].unique()
array(['Mary', 'Helen', 'Dorothy', ..., 'Zae', 'Zai', 'Zayvier'],
dtype=object)
.sort_values()
对DataFrame
进行排序可以用于隔离极端值。例如,按降序排序的行的前 5 个条目(即从最高到最低)是最大的 5 个值。.sort_values
允许我们按指定列对DataFrame
或Series
进行排序。我们可以选择按升序
(默认)或降序
的顺序接收行。
# Sort the "Count" column from highest to lowest
babynames.sort_values(by="Count", ascending=False).head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
268041 | CA | M | 1957 | Michael | 8260 |
267017 | CA | M | 1956 | Michael | 8258 |
317387 | CA | M | 1990 | Michael | 8246 |
281850 | CA | M | 1969 | Michael | 8245 |
283146 | CA | M | 1970 | Michael | 8196 |
与在DataFrame
上调用.value_counts()
不同,当在Series
上调用.value_counts()
时,我们不需要显式指定用于排序的列。我们仍然可以指定排序范式 - 即值是按升序还是降序排序。
# Sort the "Name" Series alphabetically
babynames["Name"].sort_values(ascending=True).head()
366001 Aadan
384005 Aadan
369120 Aadan
398211 Aadarsh
370306 Aaden
Name: Name, dtype: object
现在让我们尝试应用我们刚刚学到的知识来解决一个排序问题,使用不同的方法。假设我们想要找到最长的婴儿名字,并相应地对我们的数据进行排序。
其中一种方法是首先创建一个包含名字长度的列。
# Create a Series of the length of each name
babyname_lengths = babynames["Name"].str.len()
# Add a column named "name_lengths" that includes the length of each name
babynames["name_lengths"] = babyname_lengths
babynames.head(5)
State | Sex | Year | Name | Count | name_lengths | |
---|---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 | 4 |
1 | CA | F | 1910 | Helen | 239 | 5 |
2 | CA | F | 1910 | Dorothy | 220 | 7 |
3 | CA | F | 1910 | Margaret | 163 | 8 |
4 | CA | F | 1910 | Frances | 134 | 7 |
然后,我们可以使用.sort_values()
按该列对DataFrame
进行排序:
# Sort by the temporary column
babynames = babynames.sort_values(by="name_lengths", ascending=False)
babynames.head(5)
State | Sex | Year | Name | Count | name_lengths | |
---|---|---|---|---|---|---|
334166 | CA | M | 1996 | Franciscojavier | 8 | 15 |
337301 | CA | M | 1997 | Franciscojavier | 5 | 15 |
339472 | CA | M | 1998 | Franciscojavier | 6 | 15 |
321792 | CA | M | 1991 | Ryanchristopher | 7 | 15 |
327358 | CA | M | 1993 | Johnchristopher | 5 | 15 |
最后,我们可以从babynames
中删除name_length
列,以防止我们的表变得混乱。
# Drop the 'name_length' column
babynames = babynames.drop("name_lengths", axis='columns')
babynames.head(5)
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
334166 | CA | M | 1996 | Franciscojavier | 8 |
337301 | CA | M | 1997 | Franciscojavier | 5 |
339472 | CA | M | 1998 | Franciscojavier | 6 |
321792 | CA | M | 1991 | Ryanchristopher | 7 |
327358 | CA | M | 1993 | Johnchristopher | 5 |
key
参数进行排序另一种方法是使用.sort_values()
的key
参数。在这里,我们可以指定我们想要按长度对"Name"
值进行排序。
babynames.sort_values("Name", key=lambda x: x.str.len(), ascending=False).head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
334166 | CA | M | 1996 | Franciscojavier | 8 |
327472 | CA | M | 1993 | Ryanchristopher | 5 |
337301 | CA | M | 1997 | Franciscojavier | 5 |
337477 | CA | M | 1997 | Ryanchristopher | 5 |
312543 | CA | M | 1987 | Franciscojavier | 5 |
map
函数进行排序我们还可以在Series
上使用map
函数来解决这个问题。假设我们想要按每个“名字”中的“dr”和“ea”的数量对babynames
表进行排序。我们将定义函数dr_ea_count
来帮助我们。
# First, define a function to count the number of times "dr" or "ea" appear in each name
def dr_ea_count(string):
return string.count('dr') + string.count('ea')
# Then, use `map` to apply `dr_ea_count` to each name in the "Name" column
babynames["dr_ea_count"] = babynames["Name"].map(dr_ea_count)
# Sort the DataFrame by the new "dr_ea_count" column so we can see our handiwork
babynames = babynames.sort_values(by="dr_ea_count", ascending=False)
babynames.head()
State | Sex | Year | Name | Count | dr_ea_count | |
---|---|---|---|---|---|---|
115957 | CA | F | 1990 | Deandrea | 5 | 3 |
101976 | CA | F | 1986 | Deandrea | 6 | 3 |
131029 | CA | F | 1994 | Leandrea | 5 | 3 |
108731 | CA | F | 1988 | Deandrea | 5 | 3 |
308131 | CA | M | 1985 | Deandrea | 6 | 3 |
我们可以在使用完dr_ea_count
后删除它,以保持一个整洁的表格。
# Drop the `dr_ea_count` column
babynames = babynames.drop("dr_ea_count", axis = 'columns')
babynames.head(5)
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
115957 | CA | F | 1990 | Deandrea | 5 |
101976 | CA | F | 1986 | Deandrea | 6 |
131029 | CA | F | 1994 | Leandrea | 5 |
108731 | CA | F | 1988 | Deandrea | 5 |
308131 | CA | M | 1985 | Deandrea | 6 |
.groupby
聚合数据直到这一点,我们一直在处理DataFrame
的单个行。作为数据科学家,我们经常希望调查我们数据的更大子集中的趋势。例如,我们可能希望计算我们DataFrame
中一组行的一些摘要统计(均值、中位数、总和等)。为此,我们将使用pandas
的GroupBy
对象。
假设我们想要聚合babynames
中给定年份的所有行。
babynames.groupby("Year")
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x117197460>
这个奇怪的输出是什么意思?调用.groupby
生成了一个GroupBy
对象。你可以把它想象成一组“迷你”子数据框,其中每个子框包含与特定年份对应的babynames
的所有行。
下面的图表显示了babynames
的简化视图,以帮助说明这个想法。
创建一个 GroupBy 对象
我们不能直接使用GroupBy
对象——这就是为什么你之前看到了奇怪的输出,而不是DataFrame
的标准视图。要实际操作这些“迷你”DataFrame 中的值,我们需要调用聚合方法。这是一种告诉pandas
如何聚合GroupBy
对象中的值的方法。一旦应用了聚合,pandas
将返回一个正常的(现在是分组的)DataFrame
。
我们将考虑的第一种聚合方法是.agg
。.agg
方法将函数作为其参数;然后将该函数应用于“迷你”分组的每一列 DataFrame。我们最终得到一个新的DataFrame
,每个子框架都有一行聚合。
babynames[["Year", "Count"]].groupby("Year").agg(sum).head(5)
Count | |
---|---|
Year | |
1910 | 9163 |
1911 | 9983 |
1912 | 17946 |
1913 | 22094 |
1914 | 26926 |
我们可以将这一点与我们之前使用的图表联系起来。请记住,图表使用了“babynames”的简化版本,这就是为什么我们看到总计数的值较小。
执行聚合
调用.agg
已将每个子框架压缩为单个行。这给了我们最终的输出:一个现在由“Year”索引的DataFrame
,原始babynames
DataFrame 中每个唯一年份都有一行。
也许你会想:"State"
、"Sex"
和"Name"
列去哪了?从逻辑上讲,对这些列中的字符串数据进行sum
是没有意义的(我们怎么将“Mary”+“Ann”相加呢?)。因此,在对DataFrame
进行聚合时,我们需要省略这些列。
# Same result, but now we explicitly tell pandas to only consider the "Count" column when summing
babynames.groupby("Year")[["Count"]].agg(sum).head(5)
Count | |
---|---|
Year | |
1910 | 9163 |
1911 | 9983 |
1912 | 17946 |
1913 | 22094 |
1914 | 26926 |
有许多不同的聚合可以应用于分组数据。主要要求是聚合函数必须:
接收一系列数据(分组子框架的单个列)。
返回一个聚合了这个Series
的单个值。
由于这个相当广泛的要求,pandas
提供了许多计算聚合的方法。
内置的 Python 操作——如sum
、max
和min
——会被pandas
自动识别。
# What is the minimum count for each name in any year?
babynames.groupby("Name")[["Count"]].agg(min).head()
Count | |
---|---|
Name | |
Aadan | 5 |
Aadarsh | 6 |
Aaden | 10 |
Aadhav | 6 |
Aadhini | 6 |
# What is the largest single-year count of each name?
babynames.groupby("Name")[["Count"]].agg(max).head()
Count | |
---|---|
Name | |
Aadan | 7 |
Aadarsh | 6 |
Aaden | 158 |
Aadhav | 8 |
Aadhini | 6 |
如前所述,NumPy
库中的函数,如np.mean
、np.max
、np.min
和np.sum
,也是pandas
中的合理选择。
# What is the average count for each name across all years?
babynames.groupby("Name")[["Count"]].agg(np.mean).head()
Count | |
---|---|
Name | |
Aadan | 6.000000 |
Aadarsh | 6.000000 |
Aaden | 46.214286 |
Aadhav | 6.750000 |
Aadhini | 6.000000 |
pandas
还提供了许多内置函数。pandas
本地的函数可以在调用.agg
时使用它们的字符串名称进行引用。一些例子包括:
.agg("sum")
.agg("max")
.agg("min")
.agg("mean")
.agg("first")
.agg("last")
列表中的后两个条目——“first”和“last”——是“pandas”独有的。它们返回子框架列中的第一个或最后一个条目。为什么这可能有用呢?考虑一个情况,即组中的多个列共享相同的信息。为了在分组输出中表示这些信息,我们可以简单地获取第一个或最后一个条目,我们知道它将与所有其他条目相同。
让我们举个例子来说明这一点。假设我们向“babynames”添加一个新列,其中包含每个名字的第一个字母。
# Imagine we had an additional column, "First Letter". We'll explain this code next week
babynames["First Letter"] = babynames["Name"].str[0]
# We construct a simplified DataFrame containing just a subset of columns
babynames_new = babynames[["Name", "First Letter", "Year"]]
babynames_new.head()
Name | First Letter | Year | |
---|---|---|---|
115957 | Deandrea | D | 1990 |
101976 | Deandrea | D | 1986 |
131029 | Leandrea | L | 1994 |
108731 | Deandrea | D | 1988 |
308131 | Deandrea | D | 1985 |
如果我们为数据集中的每个名称形成分组,“首字母”
将对该组的所有成员都相同。这意味着如果我们只是选择组中“首字母”
的第一个条目,我们将代表该组中的所有数据。
我们可以使用字典在分组期间对每列应用不同的聚合函数。
使用“first”进行聚合
babynames_new.groupby("Name").agg({"First Letter":"first", "Year":"max"}).head()
First Letter | Year | |
---|---|---|
Name | ||
Aadan | A | 2014 |
Aadarsh | A | 2019 |
Aaden | A | 2020 |
Aadhav | A | 2019 |
Aadhini | A | 2022 |
一些聚合函数非常常见,以至于pandas
允许直接调用它们,而无需显式使用.agg
。
babynames.groupby("Name")[["Count"]].mean().head()
Count | |
---|---|
Name | |
Aadan | 6.000000 |
Aadarsh | 6.000000 |
Aaden | 46.214286 |
Aadhav | 6.750000 |
Aadhini | 6.000000 |
我们还可以定义自己的聚合函数!这可以使用def
或lambda
语句来完成。同样,自定义聚合函数的条件是它必须接受一个Series
并输出单个标量值。
babynames = babynames.sort_values(by="Year", ascending=True)
def ratio_to_peak(series):
return series.iloc[-1]/max(series)
babynames.groupby("Name")[["Year", "Count"]].agg(ratio_to_peak)
Year | Count | |
---|---|---|
Name | ||
Aadan | 1.0 | 0.714286 |
Aadarsh | 1.0 | 1.000000 |
Aaden | 1.0 | 0.063291 |
Aadhav | 1.0 | 0.750000 |
Aadhini | 1.0 | 1.000000 |
… | … | … |
Zymir | 1.0 | 1.000000 |
Zyon | 1.0 | 1.000000 |
Zyra | 1.0 | 1.000000 |
Zyrah | 1.0 | 0.833333 |
Zyrus | 1.0 | 1.000000 |
20437 行×2 列
# Alternatively, using lambda
babynames.groupby("Name")[["Year", "Count"]].agg(lambda s: s.iloc[-1]/max(s))
Year | Count | |
---|---|---|
Name | ||
Aadan | 1.0 | 0.714286 |
Aadarsh | 1.0 | 1.000000 |
Aaden | 1.0 | 0.063291 |
Aadhav | 1.0 | 0.750000 |
Aadhini | 1.0 | 1.000000 |
… | … | … |
Zymir | 1.0 | 1.000000 |
Zyon | 1.0 | 1.000000 |
Zyra | 1.0 | 1.000000 |
Zyrah | 1.0 | 0.833333 |
Zyrus | 1.0 | 1.000000 |
20437 行×2 列
操纵DataFrames
不是一天就能掌握的技能。由于pandas
的灵活性,有许多不同的方法可以从 A 点到 B 点。我们建议尝试多种不同的方法来解决同一个问题,以获得更多的练习并更快地达到精通的水平。
接下来,我们将开始深入挖掘数据分组背后的机制。**
原文:Pandas III
译者:飞龙
学习成果
使用.groupby()
执行高级聚合
使用pd.pivot_table
方法构建一个数据透视表
使用pd.merge()
在 DataFrame 之间执行简单的合并
上次,我们介绍了数据聚合的概念 - 我们熟悉了GroupBy
对象,并将它们用作汇总和总结 DataFrame 的工具。在本讲座中,我们将探讨使用不同的聚合函数以及深入研究一些高级的.groupby
方法,以展示它们在理解我们的数据方面有多么强大。我们还将介绍其他数据聚合技术,以提供在如何操作我们的表格方面的灵活性。
.agg()
函数我们将从加载babynames
数据集开始。请注意,此数据集已经被过滤,只包含来自加利福尼亚州的数据。
代码
# This code pulls census data and loads it into a DataFrame
# We won't cover it explicitly in this class, but you are welcome to explore it on your own
import pandas as pd
import numpy as np
import urllib.request
import os.path
import zipfile
data_url = "https://www.ssa.gov/oact/babynames/state/namesbystate.zip"
local_filename = "data/babynamesbystate.zip"
if not os.path.exists(local_filename): # If the data exists don't download again
with urllib.request.urlopen(data_url) as resp, open(local_filename, 'wb') as f:
f.write(resp.read())
zf = zipfile.ZipFile(local_filename, 'r')
ca_name = 'STATE.CA.TXT'
field_names = ['State', 'Sex', 'Year', 'Name', 'Count']
with zf.open(ca_name) as fh:
babynames = pd.read_csv(fh, header=None, names=field_names)
babynames.tail(10)
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
407418 | CA | M | 2022 | Zach | 5 |
407419 | CA | M | 2022 | Zadkiel | 5 |
407420 | CA | M | 2022 | Zae | 5 |
407421 | CA | M | 2022 | Zai | 5 |
407422 | CA | M | 2022 | Zay | 5 |
407423 | CA | M | 2022 | Zayvier | 5 |
407424 | CA | M | 2022 | Zia | 5 |
407425 | CA | M | 2022 | Zora | 5 |
407426 | CA | M | 2022 | Zuriel | 5 |
407427 | CA | M | 2022 | Zylo | 5 |
让我们首先使用.agg
来找出每年出生的婴儿总数。回想一下,使用.agg
和.groupby()
的格式是:df.groupby(column_name).agg(aggregation_function)
。下面的代码行给出了每年出生的婴儿总数。
babynames.groupby("Year")[["Count"]].agg(sum).head(5)
Count | |
---|---|
Year | |
1910 | 9163 |
1911 | 9983 |
1912 | 17946 |
1913 | 22094 |
1914 | 26926 |
这里有一个过程的示例:
现在让我们深入研究groupby
。正如我们在上一堂课中学到的,groupby
操作涉及将 DataFrame 拆分为分组的子框架,应用函数,并组合结果的某种组合。
对于下面的任意 DataFrame df
,代码df.groupby("year").agg(sum)
执行以下操作:
将DataFrame
拆分为属于同一年份的子DataFrame
。
将sum
函数应用到每个子DataFrame
的每一列。
将sum
的结果组合成一个由year
索引的单个DataFrame
。
可以应用许多不同的聚合函数到分组的数据上。.agg()
可以接受任何将多个值聚合为一个摘要值的函数。
因为这个相当广泛的要求,pandas
提供了许多计算聚合的方法。
pandas
会自动识别内置的 Python 操作。例如:
.agg(sum)
.agg(max)
.agg(min)
pandas
中也可以使用**NumPy
**函数:
.agg(np.sum)
.agg(np.max)
.agg(np.min)
.agg("mean")
pandas
还提供了许多内置函数,包括:
.agg("sum")
.agg("max")
.agg("min")
.agg("mean")
.agg("first")
.agg("last")
一些常用的聚合函数甚至可以直接调用,而不需要显式使用.agg()
。例如,我们可以在.groupby()
上调用.mean()
:
babynames.groupby("Year").mean().head()
现在我们可以将所有这些付诸实践。假设我们想要找出在加利福尼亚州最不受欢迎的“F”性别的婴儿名字。为了计算这个,我们可以首先创建一个指标:“峰值比”(RTP)。RTP 是 2022 年出生具有给定名字的婴儿与任何年份出生具有该名字的最大数量之比。
让我们从计算一个名为“Jennifer”的婴儿开始。
# We filter by babies with sex "F" and sort by "Year"
f_babynames = babynames[babynames["Sex"] == "F"]
f_babynames = f_babynames.sort_values(["Year"])
# Determine how many Jennifers were born in CA per year
jenn_counts_series = f_babynames[f_babynames["Name"] == "Jennifer"]["Count"]
# Determine the max number of Jennifers born in a year and the number born in 2022
# to calculate RTP
max_jenn = max(f_babynames[f_babynames["Name"] == "Jennifer"]["Count"])
curr_jenn = f_babynames[f_babynames["Name"] == "Jennifer"]["Count"].iloc[-1]
rtp = curr_jenn / max_jenn
rtp
0.018796372629843364
通过创建一个计算 RTP 并将其应用到我们的DataFrame
的函数,我们可以一次轻松计算所有名字的 RTP!
def ratio_to_peak(series):
return series.iloc[-1] / max(series)
#Using .groupby() to apply the function
rtp_table = f_babynames.groupby("Name")[["Year", "Count"]].agg(ratio_to_peak)
rtp_table.head()
Year | Count | |
---|---|---|
Name | ||
Aadhini | 1.0 | 1.000000 |
Aadhira | 1.0 | 0.500000 |
Aadhya | 1.0 | 0.660000 |
Aadya | 1.0 | 0.586207 |
Aahana | 1.0 | 0.269231 |
在上面显示的行中,我们可以看到每一行都有一个年
值为1.0
。
这是你在 Data 8 中看到的逻辑的“pandas
-ification”。你在 Data 8 中学到的许多逻辑在 Data 100 中也会对你有所帮助。
请注意,你必须小心选择哪些列应用.agg()
函数。如果我们尝试通过f_babynames.groupby("Name").agg(ratio_to_peak)
对整个表应用我们的函数,执行.agg()
调用将导致TypeError
。
我们可以通过在调用.agg()
之前显式选择要应用聚合函数的列来避免这个问题(并防止无意中丢失数据),
默认情况下,.groupby
不会重命名任何聚合列。正如我们在上表中看到的,聚合列仍然被命名为Count
,即使它现在代表 RTP。为了更好地可读性,我们可以将Count
重命名为Count RTP
rtp_table = rtp_table.rename(columns = {"Count": "Count RTP"})
rtp_table
Year | Count RTP | |
---|---|---|
Name | ||
Aadhini | 1.0 | 1.000000 |
Aadhira | 1.0 | 0.500000 |
Aadhya | 1.0 | 0.660000 |
Aadya | 1.0 | 0.586207 |
Aahana | 1.0 | 0.269231 |
… | … | … |
Zyanya | 1.0 | 0.466667 |
Zyla | 1.0 | 1.000000 |
Zylah | 1.0 | 1.000000 |
Zyra | 1.0 | 1.000000 |
Zyrah | 1.0 | 0.833333 |
13782 行×2 列
通过对rtp_table
进行排序,我们可以看到受欢迎程度下降最多的名字。
rtp_table = rtp_table.rename(columns = {"Count": "Count RTP"})
rtp_table.sort_values("Count RTP").head()
Year | Count RTP | |
---|---|---|
Name | ||
Debra | 1.0 | 0.001260 |
Debbie | 1.0 | 0.002815 |
Carol | 1.0 | 0.003180 |
Tammy | 1.0 | 0.003249 |
Susan | 1.0 | 0.003305 |
要可视化上述Dataframe
,让我们看看下面的折线图:
代码
import plotly.express as px
px.line(f_babynames[f_babynames["Name"] == "Debra"], x = "Year", y = "Count")
我们可以得到前 10 个名字的列表,然后用以下代码绘制受欢迎程度:
top10 = rtp_table.sort_values("Count RTP").head(10).index
px.line(
f_babynames[f_babynames["Name"].isin(top10)],
x = "Year",
y = "Count",
color = "Name"
)
作为一个快速练习,考虑一下什么样的代码可以计算每个名字的婴儿总数。
代码
babynames.groupby("Name")[["Count"]].agg(sum).head()
# alternative solution:
# babynames.groupby("Name")[["Count"]].sum()
Count | |
---|---|
Name | |
Aadan | 18 |
Aadarsh | 6 |
Aaden | 647 |
Aadhav | 27 |
Aadhini | 6 |
现在,让我们考虑计算每年出生的婴儿总数的代码。你会看到有多种方法可以实现这一点,其中一些列在下面列出。
代码
babynames.groupby("Year")[["Count"]].agg(sum).head()
# Alternative 1
# babynames.groupby("Year")[["Count"]].sum()
# Alternative 2
# babynames.groupby("Year").sum(numeric_only=True)
Count | |
---|---|
Year | |
1910 | 9163 |
1911 | 9983 |
1912 | 17946 |
1913 | 22094 |
1914 | 26926 |
对于第二种选择,注意我们如何通过向groupby
传递numeric_only=True
参数来避免我们之前在聚合非数字列时遇到的错误。
绘制Dataframe
后,我们得到了一个有趣的故事。
代码
import plotly.express as px
puzzle2 = babynames.groupby("Year")[["Count"]].agg(sum)
px.line(puzzle2, y = "Count")
警告: 当我们决定使用这个数据集来估计出生率时,我们做出了一个巨大的假设。根据来自立法分析办公室的这篇文章,2020 年加利福尼亚州出生的婴儿实际数量为 421,275。然而,我们的图表显示 362,882 个婴儿 - 发生了什么?
GroupBy()
,继续我们将再次使用elections
DataFrame。
代码
import pandas as pd
import numpy as np
elections = pd.read_csv("data/elections.csv")
elections.head(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
4 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.574789 |
GroupBy
对象应用于DataFrame
的groupby
的结果是一个DataFrameGroupBy
对象,而不是一个DataFrame
。
grouped_by_year = elections.groupby("Year")
type(grouped_by_year)
pandas.core.groupby.generic.DataFrameGroupBy
有几种方法可以查看DataFrameGroupBy
对象:
grouped_by_party = elections.groupby("Party")
grouped_by_party.groups
{'American': [22, 126], 'American Independent': [115, 119, 124], 'Anti-Masonic': [6], 'Anti-Monopoly': [38], 'Citizens': [127], 'Communist': [89], 'Constitution': [160, 164, 172], 'Constitutional Union': [24], 'Democratic': [2, 4, 8, 10, 13, 14, 17, 20, 28, 29, 34, 37, 39, 45, 47, 52, 55, 57, 64, 70, 74, 77, 81, 83, 86, 91, 94, 97, 100, 105, 108, 111, 114, 116, 118, 123, 129, 134, 137, 140, 144, 151, 158, 162, 168, 176, 178], 'Democratic-Republican': [0, 1], 'Dixiecrat': [103], 'Farmer–Labor': [78], 'Free Soil': [15, 18], 'Green': [149, 155, 156, 165, 170, 177, 181], 'Greenback': [35], 'Independent': [121, 130, 143, 161, 167, 174], 'Liberal Republican': [31], 'Libertarian': [125, 128, 132, 138, 139, 146, 153, 159, 163, 169, 175, 180], 'National Democratic': [50], 'National Republican': [3, 5], 'National Union': [27], 'Natural Law': [148], 'New Alliance': [136], 'Northern Democratic': [26], 'Populist': [48, 61, 141], 'Progressive': [68, 82, 101, 107], 'Prohibition': [41, 44, 49, 51, 54, 59, 63, 67, 73, 75, 99], 'Reform': [150, 154], 'Republican': [21, 23, 30, 32, 33, 36, 40, 43, 46, 53, 56, 60, 65, 69, 72, 79, 80, 84, 87, 90, 96, 98, 104, 106, 109, 112, 113, 117, 120, 122, 131, 133, 135, 142, 145, 152, 157, 166, 171, 173, 179], 'Socialist': [58, 62, 66, 71, 76, 85, 88, 92, 95, 102], 'Southern Democratic': [25], 'States' Rights': [110], 'Taxpayers': [147], 'Union': [93], 'Union Labor': [42], 'Whig': [7, 9, 11, 12, 16, 19]}
grouped_by_party.get_group("Socialist")
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
58 | 1904 | Eugene V. Debs | Socialist | 402810 | loss | 2.985897 |
62 | 1908 | Eugene V. Debs | Socialist | 420852 | loss | 2.850866 |
66 | 1912 | Eugene V. Debs | Socialist | 901551 | loss | 6.004354 |
71 | 1916 | Allan L. Benson | Socialist | 590524 | loss | 3.194193 |
76 | 1920 | Eugene V. Debs | Socialist | 913693 | loss | 3.428282 |
85 | 1928 | Norman Thomas | Socialist | 267478 | loss | 0.728623 |
88 | 1932 | Norman Thomas | Socialist | 884885 | loss | 2.236211 |
92 | 1936 | Norman Thomas | Socialist | 187910 | loss | 0.412876 |
95 | 1940 | Norman Thomas | Socialist | 116599 | loss | 0.234237 |
102 | 1948 | Norman Thomas | Socialist | 139569 | loss | 0.286312 |
GroupBy
方法有许多聚合方法可以使用.agg
。一些有用的选项是:
.mean
:创建一个新的DataFrame
,其中包含每个组的平均值
.sum
:创建一个新的DataFrame
,其中包含每个组的总和
.size
:创建一个新的Series,其中包含每个组的条目数
.count
:创建一个新的DataFrame,其中包含条目数,不包括缺失值。
让我们通过创建一个名为df
的DataFrame
来举例说明一些例子。
df = pd.DataFrame({'letter':['A','A','B','C','C','C'],
'num':[1,2,3,4,np.NaN,4],
'state':[np.NaN, 'tx', 'fl', 'hi', np.NaN, 'ak']})
df
letter | num | State | |
---|---|---|---|
0 | A | 1.0 | NaN |
1 | A | 2.0 | tx |
2 | B | 3.0 | fl |
3 | C | 4.0 | hi |
4 | C | NaN | NaN |
5 | C | 4.0 | ak |
请注意.size()
和.count()
之间的细微差别:虽然.size()
返回一个Series
并计算包括缺失值在内的条目数,.count()
返回一个DataFrame
并计算每列中不包括缺失值的条目数。
df.groupby("letter").size()
letter
A 2
B 1
C 3
dtype: int64
df.groupby("letter").count()
num | State | |
---|---|---|
letter | ||
— | — | — |
A | 2 | 1 |
B | 1 | 1 |
C | 2 | 2 |
您可能还记得前一个笔记中的value_counts()
函数做了类似的事情。原来value_counts()
和groupby.size()
是一样的,只是value_counts()
会自动按降序排序结果Series
。
df["letter"].value_counts()
C 3
A 2
B 1
Name: letter, dtype: int64
这些(和其他)聚合函数是如此常见,以至于 pandas
允许使用简写。我们可以直接在 GroupBy
对象上调用函数,而不是明确地声明使用 .agg
。
例如,以下是等价的:
elections.groupby("Candidate").agg(mean)
elections.groupby("Candidate").mean()
pandas
还支持许多其他方法。您可以在pandas
文档中查看它们。
GroupBy
对象的另一个常见用途是按组过滤数据。
groupby.filter
接受一个参数 func
,其中 func
是一个函数,它:
以 DataFrame
对象作为输入
返回每个子 DataFrame
的单个 True
或 False
返回对应于 True
的子 DataFrame
,而具有 False
值的则不返回。重要的是,groupby.filter
与 groupby.agg
不同,因为最终的 DataFrame
中返回的是整个子 DataFrame
,而不仅仅是单行。因此,groupby.filter
保留了原始索引。
为了说明这是如何发生的,让我们回到 elections
数据集。假设我们想要识别“紧张”的选举年份 - 也就是说,我们想要找到所有对应于那一年的行,其中所有候选人在那一年赢得了相似比例的总票数。具体来说,让我们找到所有对应于没有候选人赢得超过总票数 45%的年份的行。
换句话说,我们想要:
找到最大 %
小于 45% 的年份
返回对应于这些年份的所有 DataFrame
行
对于每一年,我们需要找到该年所有行中的最大 %
。如果这个最大 %
小于 45%,我们将告诉 pandas
保留该年对应的所有行。
elections.groupby("Year").filter(lambda sf: sf["%"].max() < 45).head(9)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
23 | 1860 | Abraham Lincoln | Republican | 1855993 | win | 39.699408 |
24 | 1860 | John Bell | Constitutional Union | 590901 | loss | 12.639283 |
25 | 1860 | John C. Breckinridge | Southern Democratic | 848019 | loss | 18.138998 |
26 | 1860 | Stephen A. Douglas | Northern Democratic | 1380202 | loss | 29.522311 |
66 | 1912 | Eugene V. Debs | Socialist | 901551 | loss | 6.004354 |
67 | 1912 | Eugene W. Chafin | Prohibition | 208156 | loss | 1.386325 |
68 | 1912 | Theodore Roosevelt | Progressive | 4122721 | loss | 27.457433 |
69 | 1912 | William Taft | Republican | 3486242 | loss | 23.218466 |
70 | 1912 | Woodrow Wilson | Democratic | 6296284 | win | 41.933422 |
这里发生了什么?在这个例子中,我们将我们的过滤函数 func
定义为 lambda sf: sf["%"].max() < 45
。这个过滤函数将在分组的子 DataFrame
中的所有条目中找到最大的 "%"
值,我们称之为 sf
。如果最大值小于 45,则过滤函数将返回 True
,并且该分组的所有行将出现在最终的输出 DataFrame
中。
检查上面的 DataFrame
。请注意,在这个前 9 行的预览中,所有来自 1860 年和 1912 年的条目都出现了。这意味着在 1860 年和 1912 年,那一年没有候选人赢得超过总票数 45%。
你可能会问:groupby.filter
过程与我们之前看到的布尔过滤有何不同?布尔过滤在应用布尔条件时考虑单个行。例如,代码 elections[elections["%"] < 45]
将检查 elections
中每一行的 "%"
值;如果小于 45,则该行将保留在输出中。相比之下,groupby.filter
在整个组的所有行上应用布尔条件。如果该组中并非所有行都满足过滤器指定的条件,则整个组将在输出中被丢弃。
lambda
函数进行聚合如果我们希望使用非标准函数(例如我们自己设计的函数)对我们的DataFrame
进行聚合,我们可以通过将.agg
与lambda
表达式结合使用来实现。
让我们首先考虑一个谜题来唤起我们的记忆。我们将尝试找到每个Party
中获得最高%
选票的Candidate
。
一个天真的方法可能是按Party
列分组并按最大值聚合。
elections.groupby("Party").agg(max).head(10)
Year | Candidate | Popular vote | Result | % | |
---|---|---|---|---|---|
Party | |||||
American | 1976 | Thomas J. Anderson | 873053 | loss | 21.554001 |
American Independent | 1976 | Lester Maddox | 9901118 | loss | 13.571218 |
Anti-Masonic | 1832 | William Wirt | 100715 | loss | 7.821583 |
Anti-Monopoly | 1884 | Benjamin Butler | 134294 | loss | 1.335838 |
Citizens | 1980 | Barry Commoner | 233052 | loss | 0.270182 |
Communist | 1932 | William Z. Foster | 103307 | loss | 0.261069 |
Constitution | 2016 | Michael Peroutka | 203091 | loss | 0.152398 |
Constitutional Union | 1860 | John Bell | 590901 | loss | 12.639283 |
Democratic | 2020 | Woodrow Wilson | 81268924 | win | 61.344703 |
Democratic-Republican | 1824 | John Quincy Adams | 151271 | win | 57.210122 |
这种方法显然是错误的-DataFrame
声称伍德罗·威尔逊在 2020 年赢得了总统大选。
为什么会发生这种情况?这里,max
聚合函数是独立地应用于每一列。在民主党人中,max
正在计算:
民主党候选人竞选总统的最近年份(2020)
具有字母顺序“最大”名称(“伍德罗·威尔逊”)的Candidate
具有字母顺序“最大”结果(“赢”)的Result
相反,让我们尝试一种不同的方法。我们将:
对数据框进行排序,使行按%
的降序排列
按Party
分组并选择每个子数据框的第一行
虽然这可能看起来不直观,但按%
的降序对elections
进行排序非常有帮助。然后,如果我们按Party
分组,每个 groupby 对象的第一行将包含有关具有最高选民%
的Candidate
的信息。
elections_sorted_by_percent = elections.sort_values("%", ascending=False)
elections_sorted_by_percent.head(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
114 | 1964 | Lyndon Johnson | Democratic | 43127041 | win | 61.344703 |
91 | 1936 | Franklin Roosevelt | Democratic | 27752648 | win | 60.978107 |
120 | 1972 | Richard Nixon | Republican | 47168710 | win | 60.907806 |
79 | 1920 | Warren Harding | Republican | 16144093 | win | 60.574501 |
133 | 1984 | Ronald Reagan | Republican | 54455472 | win | 59.023326 |
elections_sorted_by_percent.groupby("Party").agg(lambda x : x.iloc[0]).head(10)
# Equivalent to the below code
# elections_sorted_by_percent.groupby("Party").agg('first').head(10)
Year | Candidate | Popular vote | Result | % | |
---|---|---|---|---|---|
Party | |||||
American | 1856 | Millard Fillmore | 873053 | loss | 21.554001 |
American Independent | 1968 | George Wallace | 9901118 | loss | 13.571218 |
Anti-Masonic | 1832 | William Wirt | 100715 | loss | 7.821583 |
Anti-Monopoly | 1884 | Benjamin Butler | 134294 | loss | 1.335838 |
Citizens | 1980 | Barry Commoner | 233052 | loss | 0.270182 |
Communist | 1932 | William Z. Foster | 103307 | loss | 0.261069 |
Constitution | 2008 | Chuck Baldwin | 199750 | loss | 0.152398 |
Constitutional Union | 1860 | John Bell | 590901 | loss | 12.639283 |
Democratic | 1964 | Lyndon Johnson | 43127041 | win | 61.344703 |
Democratic-Republican | 1824 | Andrew Jackson | 151271 | loss | 57.210122 |
以下是该过程的示例:
请注意,我们的代码正确确定了来自民主党的林登·约翰逊拥有最高的选民%
。
更一般地,lambda
函数用于设计 Python 中未预定义的自定义聚合函数。lambda
函数的输入参数x
是一个GroupBy
对象。因此,lambda x : x.iloc[0]
选择每个 groupby 对象中的第一行应该是有意义的。
事实上,解决这个问题有几种不同的方法。每种方法在可读性、性能、内存消耗、复杂性等方面都有不同的权衡。我们在下面给出了一些示例。
注意:不需要理解这些替代解决方案。它们是为了展示pandas
中众多问题解决方法的多样性。
# Using the idxmax function
best_per_party = elections.loc[elections.groupby('Party')['%'].idxmax()]
best_per_party.head(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
22 | 1856 | Millard Fillmore | American | 873053 | loss | 21.554001 |
115 | 1968 | George Wallace | American Independent | 9901118 | loss | 13.571218 |
6 | 1832 | William Wirt | Anti-Masonic | 100715 | loss | 7.821583 |
38 | 1884 | Benjamin Butler | Anti-Monopoly | 134294 | loss | 1.335838 |
127 | 1980 | Barry Commoner | Citizens | 233052 | loss | 0.270182 |
# Using the .drop_duplicates function
best_per_party2 = elections.sort_values('%').drop_duplicates(['Party'], keep='last')
best_per_party2.head(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
148 | 1996 | John Hagelin | Natural Law | 113670 | loss | 0.118219 |
164 | 2008 | Chuck Baldwin | Constitution | 199750 | loss | 0.152398 |
110 | 1956 | T. Coleman Andrews | States’ Rights | 107929 | loss | 0.174883 |
147 | 1996 | Howard Phillips | Taxpayers | 184656 | loss | 0.192045 |
136 | 1988 | Lenora Fulani | New Alliance | 217221 | loss | 0.237804 |
我们现在知道.groupby
让我们能够在 DataFrame 中对数据进行分组和聚合。上面的示例使用 DataFrame 中的一列形成了分组。通过传递一个列名的列表给.groupby
,可以一次按多列进行分组。
让我们再次考虑babynames
数据集。在这个问题中,我们将找到与每个年份和性别相关联的婴儿名字的总数。为此,我们将同时按"年份"
和"性别"
列进行分组。
babynames.head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
0 | CA | F | 1910 | Mary | 295 |
1 | CA | F | 1910 | Helen | 239 |
2 | CA | F | 1910 | Dorothy | 220 |
3 | CA | F | 1910 | Margaret | 163 |
4 | CA | F | 1910 | Frances | 134 |
# Find the total number of baby names associated with each sex for each
# year in the data
babynames.groupby(["Year", "Sex"])[["Count"]].agg(sum).head(6)
Count | ||
---|---|---|
Year | Sex | |
— | — | — |
1910 | F | 5950 |
M | 3213 | |
1911 | F | 6602 |
M | 3381 | |
1912 | F | 9804 |
M | 8142 |
请注意,"年份"和"性别"都作为 DataFrame 的索引(它们都以粗体呈现)。我们创建了一个多索引DataFrame,其中使用两个不同的索引值,年份和性别,来唯一标识每一行。
这不是表示这些数据的最直观的方式 - 而且,因为多索引的 DataFrame 在其索引中有多个维度,它们通常很难使用。
另一种跨两列进行聚合的策略是创建一个数据透视表。你在Data 8中看到过这些。一组值用于创建数据透视表的索引;另一组用于定义列名。表中每个单元格中包含的值对应于每个索引-列对的聚合数据。
这是一个过程的示例:
理解数据透视表的最佳方法是看它的实际应用。让我们回到我们最初的目标,即对每个年份和性别组合的名字总数进行求和。我们将调用pandas
的.pivot_table
方法来创建一个新表。
# The `pivot_table` method is used to generate a Pandas pivot table
import numpy as np
babynames.pivot_table(
index = "Year",
columns = "Sex",
values = "Count",
aggfunc = np.sum,
).head(5)
Sex | F | M |
---|---|---|
Year | ||
— | — | — |
1910 | 5950 | 3213 |
1911 | 6602 | 3381 |
1912 | 9804 | 8142 |
1913 | 11860 | 10234 |
1914 | 13815 | 13111 |
看起来好多了!现在,我们的 DataFrame 结构清晰,具有清晰的索引列组合。数据透视表中的每个条目表示给定“Year”和“Sex”组合的名称总数。
让我们更仔细地看一下上面实施的代码。
index = "Year"
指定应用于数据透视表的原始“DataFrame”中用作索引的列名
columns = "Sex"
指定应用于生成数据透视表的列的原始“DataFrame”中的列名
values = "Count"
指示应用于填充每个索引列组合的条目的原始“DataFrame”中的哪些值
aggfunc = np.sum
告诉“pandas”在聚合由“values”指定的数据时使用什么函数。在这里,我们正在对每对“Year”和“Sex”的名称计数求和
我们甚至可以在数据透视表的索引或列中包含多个值。
babynames_pivot = babynames.pivot_table(
index="Year", # the rows (turned into index)
columns="Sex", # the column values
values=["Count", "Name"],
aggfunc=max, # group operation
)
babynames_pivot.head(6)
Count | Name | |
---|---|---|
Sex | F | M |
Year | ||
1910 | 295 | 237 |
1911 | 390 | 214 |
1912 | 534 | 501 |
1913 | 584 | 614 |
1914 | 773 | 769 |
1915 | 998 | 1033 |
在进行数据科学项目时,我们不太可能在单个“DataFrame”中包含我们想要的所有数据-现实世界的数据科学家需要处理来自多个来源的数据。如果我们可以访问具有相关信息的多个数据集,我们可以将两个或多个表连接成一个单独的 DataFrame。
要将其付诸实践,我们将重新审视“elections”数据集。
elections.head(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 |
4 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.574789 |
假设我们想了解 2022 年每位总统候选人的名字的受欢迎程度。为此,我们需要“babynames”和“elections”的合并数据。
我们将首先创建一个新列,其中包含每位总统候选人的名字。这将帮助我们将“elections”中的每个名字与“babynames”中的相应名字数据连接起来。
# This `str` operation splits each candidate's full name at each
# blank space, then takes just the candidiate's first name
elections["First Name"] = elections["Candidate"].str.split().str[0]
elections.head(5)
Year | Candidate | Party | Popular vote | Result | % | First Name | |
---|---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 | Andrew |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 | John |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 | Andrew |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 | John |
4 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.574789 | Andrew |
# Here, we'll only consider `babynames` data from 2022
babynames_2022 = babynames[babynames["Year"]==2020]
babynames_2022.head()
State | Sex | Year | Name | Count | |
---|---|---|---|---|---|
228550 | CA | F | 2020 | Olivia | 2353 |
228551 | CA | F | 2020 | Camila | 2187 |
228552 | CA | F | 2020 | Emma | 2110 |
228553 | CA | F | 2020 | Mia | 2043 |
228554 | CA | F | 2020 | Sophia | 1999 |
现在,我们准备好连接这两个表了。pd.merge
是用于将 DataFrame 连接在一起的“pandas”方法。
merged = pd.merge(left = elections, right = babynames_2022, \
left_on = "First Name", right_on = "Name")
merged.head()
# Notice that pandas automatically specifies `Year_x` and `Year_y`
# when both merged DataFrames have the same column name to avoid confusion
Year_x | Candidate | Party | Popular vote | Result | % | First Name | State | Sex | Year_y | Name | Count | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.210122 | Andrew | CA | M | 2020 | Andrew | 874 |
1 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.203927 | Andrew | CA | M | 2020 | Andrew | 874 |
2 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.574789 | Andrew | CA | M | 2020 | Andrew | 874 |
3 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.789878 | John | CA | M | 2020 | John | 623 |
4 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.796073 | John | CA | M | 2020 | John | 623 |
让我们更仔细地看看这些参数:
left
和right
参数用于指定要连接的数据框。
left_on
和right_on
参数被分配给要在执行连接时使用的列的字符串名称。这两个on
参数告诉pandas
应该将哪些值作为配对键来确定要在数据框之间合并的行。我们将在下一堂课上更多地讨论这个配对键的概念。
恭喜!我们终于解决了pandas
。如果你对它仍然感到不太舒服,不要担心——在接下来的几周里,你将有足够的机会练习。
接下来,我们将动手处理一些真实世界的数据集,并利用我们的pandas
知识进行一些探索性数据分析。
译者:飞龙
代码
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
#%matplotlib inline
plt.rcParams['figure.figsize'] = (12, 9)
sns.set()
sns.set_context('talk')
np.set_printoptions(threshold=20, precision=2, suppress=True)
pd.set_option('display.max_rows', 30)
pd.set_option('display.max_columns', None)
pd.set_option('display.precision', 2)
# This option stops scientific notation for pandas
pd.set_option('display.float_format', '{:.2f}'.format)
# Silence some spurious seaborn warnings
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)
学习成果
识别常见文件格式
按其变量类型对数据进行分类
建立对数据可信度问题的认识,并制定有针对性的解决方案
此内容在第 4、5 和 6 讲中涵盖。
在过去的几堂课上,我们已经学到pandas
是一个重塑、修改和探索数据集的工具包。我们还没有涉及的是如何做出这些数据转换决策。当我们从“现实世界”收到一组新数据时,我们如何知道我们应该做什么处理来将这些数据转换为可用的形式?
数据清洗,也称为数据整理,是将原始数据转换为便于后续分析的过程。它通常用于解决诸如:
结构不清晰或格式不正确
缺失或损坏的值
单位转换
…等等
**探索性数据分析(EDA)**是了解新数据集的过程。这是一种开放式、非正式的分析,涉及熟悉数据中存在的变量,发现潜在的假设,并识别数据可能存在的问题。这最后一点通常会激发进一步的数据清洗,以解决数据集格式的任何问题;因此,EDA 和数据清洗通常被认为是一个“无限循环”,每个过程都推动着另一个过程。
在本讲座中,我们将考虑在进行数据清洗和 EDA 时要考虑的数据的关键属性。在这个过程中,我们将为您制定一个“清单”,以便在处理新数据集时考虑。通过这个过程,我们将更深入地了解数据科学生命周期的这个早期阶段(但非常重要!)。
有许多用于存储结构化数据的文件类型:TSV、JSON、XML、ASCII、SAS 等。在讲座中,我们只会涵盖 CSV、TSV 和 JSON,但在处理不同数据集时,您可能会遇到其他格式。阅读文档是了解如何处理多种不同文件类型的最佳方法。
CSV,代表逗号分隔值,是一种常见的表格数据格式。在过去的两堂pandas
讲座中,我们简要涉及了文件格式的概念:数据在文件中的编码方式。具体来说,我们的elections
和babynames
数据集是以 CSV 格式存储和加载的:
pd.read_csv("data/elections.csv").head(5)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.21 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.79 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.20 |
3 | 1828 | John Quincy Adams | National Republican | 500897 | loss | 43.80 |
4 | 1832 | Andrew Jackson | Democratic | 702735 | win | 54.57 |
为了更好地了解 CSV 的属性,让我们来看看原始数据文件的前几行,看看在加载到DataFrame
之前它是什么样子的。我们将使用repr()
函数返回带有特殊字符的原始字符串:
with open("data/elections.csv", "r") as table:
i = 0
for row in table:
print(repr(row))
i += 1
if i > 3:
break
'Year,Candidate,Party,Popular vote,Result,%\n'
'1824,Andrew Jackson,Democratic-Republican,151271,loss,57.21012204\n'
'1824,John Quincy Adams,Democratic-Republican,113142,win,42.78987796\n'
'1828,Andrew Jackson,Democratic,642806,win,56.20392707\n'
数据中的每一行,或记录,由换行符\n
分隔。数据中的每一列,或字段,由逗号,
分隔(因此是逗号分隔的!)。
另一种常见的文件类型是TSV(制表符分隔值)。在 TSV 中,记录仍然由换行符\n
分隔,而字段由制表符\t
分隔。
让我们来看看原始 TSV 文件的前几行。同样,我们将使用repr()
函数,以便print
显示特殊字符。
with open("data/elections.txt", "r") as table:
i = 0
for row in table:
print(repr(row))
i += 1
if i > 3:
break
'\ufeffYear\tCandidate\tParty\tPopular vote\tResult\t%\n'
'1824\tAndrew Jackson\tDemocratic-Republican\t151271\tloss\t57.21012204\n'
'1824\tJohn Quincy Adams\tDemocratic-Republican\t113142\twin\t42.78987796\n'
'1828\tAndrew Jackson\tDemocratic\t642806\twin\t56.20392707\n'
TSV 可以使用pd.read_csv
加载到pandas
中。我们需要使用参数sep='\t'
来指定分隔符(文档)。
pd.read_csv("data/elections.txt", sep='\t').head(3)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.21 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.79 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.20 |
CSV 和 TSV 的问题出现在记录中有逗号或制表符的情况下。pandas
如何区分逗号分隔符与字段本身中的逗号,例如8,900
?为了解决这个问题,可以查看quotechar
参数。
**JSON(JavaScript 对象表示)**文件的行为类似于 Python 字典。下面显示了原始 JSON。
with open("data/elections.json", "r") as table:
i = 0
for row in table:
print(row)
i += 1
if i > 8:
break
[
{
"Year": 1824,
"Candidate": "Andrew Jackson",
"Party": "Democratic-Republican",
"Popular vote": 151271,
"Result": "loss",
"%": 57.21012204
},
可以使用pd.read_json
将 JSON 文件加载到pandas
中。
pd.read_json('data/elections.json').head(3)
Year | Candidate | Party | Popular vote | Result | % | |
---|---|---|---|---|---|---|
0 | 1824 | Andrew Jackson | Democratic-Republican | 151271 | loss | 57.21 |
1 | 1824 | John Quincy Adams | Democratic-Republican | 113142 | win | 42.79 |
2 | 1828 | Andrew Jackson | Democratic | 642806 | win | 56.20 |
伯克利市政府开放数据网站有一个关于伯克利居民 COVID-19 确诊病例的数据集。让我们下载文件并将其保存为 JSON(请注意,源 URL 文件类型也是 JSON)。为了可重复的数据科学,我们将以程序方式下载数据。我们在ds100_utils.py
文件中定义了一些辅助函数,我们可以在许多不同的笔记本中重用这些辅助函数。
from ds100_utils import fetch_and_cache
covid_file = fetch_and_cache(
"https://data.cityofberkeley.info/api/views/xn6j-b766/rows.json?accessType=DOWNLOAD",
"confirmed-cases.json",
force=False)
covid_file # a file path wrapper object
Using cached version that was downloaded (UTC): Fri Aug 18 22:19:42 2023
PosixPath('data/confirmed-cases.json')
让我们通过对数据集的大小进行粗略估计来确定我们用于查看数据的工具。对于相对较小的数据集,我们可以使用文本编辑器或电子表格。对于较大的数据集,更多的编程探索或分布式计算工具可能更合适。在这里,我们将使用Python
工具来探查文件。
由于似乎存在文本文件,让我们调查一下行数,这通常对应于记录的数量。
import os
print(covid_file, "is", os.path.getsize(covid_file) / 1e6, "MB")
with open(covid_file, "r") as f:
print(covid_file, "is", sum(1 for l in f), "lines.")
data/confirmed-cases.json is 0.116367 MB
data/confirmed-cases.json is 1110 lines.
作为 EDA 工作流的一部分,Unix 命令非常有用。事实上,有一本名为“Data Science at the Command Line”的整本书深入探讨了这个想法!在 Jupyter/IPython 中,您可以使用!
前缀执行任意的 Unix 命令,并且在这些行内,您可以使用{expr}
语法引用Python
变量和表达式。
在这里,我们使用ls
命令列出文件,使用-lh
标志,请求“以人类可读的形式显示详细信息”。我们还使用wc
命令进行“字数统计”,但使用-l
标志,该标志请求行数而不是单词数。
这两个代码给出了与上面的代码相同的信息,尽管形式略有不同:
!ls -lh {covid_file}
!wc -l {covid_file}
-rw-r--r-- 1 Ishani staff 114K Aug 18 22:19 data/confirmed-cases.json
1109 data/confirmed-cases.json
让我们使用Python
来探索数据格式。
with open(covid_file, "r") as f:
for i, row in enumerate(f):
print(repr(row)) # print raw strings
if i >= 4: break
'{\n'
' "meta" : {\n'
' "view" : {\n'
' "id" : "xn6j-b766",\n'
' "name" : "COVID-19 Confirmed Cases",\n'
我们可以使用head
Unix 命令(这也是pandas
的head
方法的来源!)来查看文件的前几行:
!head -5 {covid_file}
{
"meta" : {
"view" : {
"id" : "xn6j-b766",
"name" : "COVID-19 Confirmed Cases",
为了将 JSON 文件加载到pandas
中,让我们首先使用Python
的json
包进行一些 EDA,以了解 JSON 文件的特定结构,以便决定是否(以及如何)将其加载到pandas
中。由于 JSON 数据与内部 Python 对象模型非常匹配,Python
对 JSON 数据有相对良好的支持。在下面的单元格中,我们使用json
包将整个 JSON 数据文件导入 Python 字典。
import json
with open(covid_file, "rb") as f:
covid_json = json.load(f)
covid_json
变量现在是一个编码文件中数据的字典:
type(covid_json)
dict
我们可以通过列出键来检查顶级 JSON 对象中有哪些键。
covid_json.keys()
dict_keys(['meta', 'data'])
观察:JSON 字典包含一个meta
键,这可能是指元数据(关于数据的数据)。元数据通常与数据一起维护,并且可以成为额外信息的良好来源。
我们可以通过检查与元数据相关联的键来进一步调查元数据。
covid_json['meta'].keys()
dict_keys(['view'])
meta
键包含另一个名为view
的字典。这可能是关于某个基础数据库的特定“视图”的元数据。我们将在后面的课程中学习更多关于视图的知识。
covid_json['meta']['view'].keys()
dict_keys(['id', 'name', 'assetType', 'attribution', 'averageRating', 'category', 'createdAt', 'description', 'displayType', 'downloadCount', 'hideFromCatalog', 'hideFromDataJson', 'newBackend', 'numberOfComments', 'oid', 'provenance', 'publicationAppendEnabled', 'publicationDate', 'publicationGroup', 'publicationStage', 'rowsUpdatedAt', 'rowsUpdatedBy', 'tableId', 'totalTimesRated', 'viewCount', 'viewLastModified', 'viewType', 'approvals', 'columns', 'grants', 'metadata', 'owner', 'query', 'rights', 'tableAuthor', 'tags', 'flags'])
请注意,这是一个嵌套/递归数据结构。随着我们深入挖掘,我们会揭示更多的键和相应的数据:
meta
|-> data
| ... (haven't explored yet)
|-> view
| -> id
| -> name
| -> attribution
...
| -> description
...
| -> columns
...
在视图子字典中有一个名为描述的键。这可能包含了数据的描述:
print(covid_json['meta']['view']['description'])
Counts of confirmed COVID-19 cases among Berkeley residents by date.
我们可以查看data
字段中的一些条目。这是我们将加载到pandas
中的数据。
for i in range(3):
print(f"{i:03} | {covid_json['data'][i]}")
000 | ['row-kzbg.v7my-c3y2', '00000000-0000-0000-0405-CB14DE51DAA7', 0, 1643733903, None, 1643733903, None, '{ }', '2020-02-28T00:00:00', '1', '1']
001 | ['row-jkyx_9u4r-h2yw', '00000000-0000-0000-F806-86D0DBE0E17F', 0, 1643733903, None, 1643733903, None, '{ }', '2020-02-29T00:00:00', '0', '1']
002 | ['row-qifg_4aug-y3ym', '00000000-0000-0000-2DCE-4D1872F9B216', 0, 1643733903, None, 1643733903, None, '{ }', '2020-03-01T00:00:00', '0', '1']
观察:* 这些看起来像等长的记录,所以也许data
是一个表格!* 但记录中的每个值代表什么?我们在哪里可以找到列标题?
为此,我们需要元数据字典中的columns
键。这将返回一个列表:
type(covid_json['meta']['view']['columns'])
list
上述元数据告诉我们很多关于数据中的列,包括列名、潜在的数据异常和基本统计信息。
由于其非表格结构,JSON 比 CSV 更容易创建自描述数据,这意味着数据的信息存储在与数据相同的文件中。
自描述数据可能会有所帮助,因为它保留了自己的描述,并且这些描述更有可能随着数据的变化而更新。
pandas
中最后,让我们将数据(而不是元数据)加载到pandas
的DataFrame
中。在下面的代码块中,我们:
将 JSON 记录翻译成DataFrame
:
字段:covid_json['meta']['view']['columns']
记录:covid_json['data']
删除没有元数据描述的列。一般来说,这是一个坏主意,但在这里我们删除这些列,因为上面的分析表明它们不太可能包含有用的信息。
检查表的tail
。
# Load the data from JSON and assign column titles
covid = pd.DataFrame(
covid_json['data'],
columns=[c['name'] for c in covid_json['meta']['view']['columns']])
covid.tail()
sid | id | position | created_at | created_meta | updated_at | updated_meta | meta | Date | New Cases | Cumulative Cases | |
---|---|---|---|---|---|---|---|---|---|---|---|
699 | row-49b6_x8zv.gyum | 00000000-0000-0000-A18C-9174A6D05774 | 0 | 1643733903 | None | 1643733903 | None | { } | 2022-01-27T00:00:00 | 106 | 10694 |
700 | row-gs55-p5em.y4v9 | 00000000-0000-0000-F41D-5724AEABB4D6 | 0 | 1643733903 | None | 1643733903 | None | { } | 2022-01-28T00:00:00 | 223 | 10917 |
701 | row-3pyj.tf95-qu67 | 00000000-0000-0000-BEE3-B0188D2518BD | 0 | 1643733903 | None | 1643733903 | None | { } | 2022-01-29T00:00:00 | 139 | 11056 |
702 | row-cgnd.8syv.jvjn | 00000000-0000-0000-C318-63CF75F7F740 | 0 | 1643733903 | None | 1643733903 | None | { } | 2022-01-30T00:00:00 | 33 | 11089 |
703 | row-qywv_24x6-237y | 00000000-0000-0000-FE92-9789FED3AA20 | 0 | 1643733903 | None | 1643733903 | None | { } | 2022-01-31T00:00:00 | 42 | 11131 |
将数据加载到文件后,花时间了解数据集中编码的信息是一个好主意。特别是,我们想要确定我们的数据中存在哪些变量类型。广义上说,我们可以将变量分类为两种主要类型之一。
定量变量描述一些数值数量或量。我们可以进一步将定量数据分为:
连续定量变量:可以在连续尺度上以任意精度测量的数值数据。连续变量没有严格的可能值集 - 它们可以记录到任意数量的小数位。例如,重量、GPA 或 CO[2]浓度。
离散定量变量:只能取有限可能值的数值数据。例如,某人的年龄或他们的兄弟姐妹数量。
定性变量,也称为分类变量,描述的是不测量某种数量或量的数据。分类数据的子类别包括:
有序定性变量:具有有序级别的类别。具体来说,有序变量是指级别之间的差异没有一致的、可量化的含义。一些例子包括教育水平(高中、本科、研究生等)、收入档次(低、中、高)或 Yelp 评分。
无序定性变量:没有特定顺序的类别。例如,某人的政治立场或 Cal ID 号码。
变量类型的分类
请注意,许多变量不会完全属于这些类别中的一个。定性变量可能具有数值级别,反之亦然,定量变量可以存储为字符串。
上次,我们介绍了.merge
作为pandas
方法,用于将多个DataFrame
连接在一起。在我们讨论连接时,我们提到了使用“键”来确定应该从每个表中合并哪些行的想法。让我们花点时间更仔细地研究这个想法。
主键是表中唯一确定其余列值的列或列集。它可以被认为是表中每一行的唯一标识符。例如,Data 100 学生表可能使用每个学生的 Cal ID 作为主键。
Cal ID | Name | Major | |
---|---|---|---|
0 | 3034619471 | Oski | Data Science |
1 | 3035619472 | Ollie | Computer Science |
2 | 3025619473 | Orrie | Data Science |
3 | 3046789372 | Ollie | Economics |
外键是表中引用其他表主键的列或列集。在分配.merge
的left_on
和right_on
参数时,了解数据集的外键可以很有用。在下面的办公时间票表中,“Cal ID”是引用前表的外键。
OH Request | Cal ID | Question | |
---|---|---|---|
0 | 1 | 3034619471 | HW 2 Q1 |
1 | 2 | 3035619472 | HW 2 Q3 |
2 | 3 | 3025619473 | Lab 3 Q4 |
3 | 4 | 3035619472 | HW 2 Q7 |
在了解数据集的结构之后,下一个任务是确定数据究竟代表什么。我们将通过考虑数据的粒度、范围和时间性来做到这一点。
数据集的粒度是单行代表的内容。您也可以将其视为数据中包含的细节级别。要确定数据的粒度,可以问:数据集中的每一行代表什么?细粒度数据包含大量细节,单行代表一个小的个体单位。例如,每条记录可能代表一个人。粗粒度数据被编码,以便单行代表一个大的个体单位-例如,每条记录可能代表一组人。
数据集的范围是数据所涵盖的人口子集。如果我们调查数据科学课程中学生的表现,一个范围较窄的数据集可能包括所有注册 Data 100 课程的学生,而一个范围较广的数据集可能包括加利福尼亚州的所有学生。
数据集的时间性描述了数据收集的周期性,以及数据最近收集或更新的时间。
数据集的时间和日期字段可能代表一些内容:
“事件”发生的时间
数据收集的时间,或者数据输入系统的时间
数据复制到数据库中的时间
为了充分了解数据的时间性,还可能需要标准化时区或检查数据中的重复时间趋势(模式是否在 24 小时内重复?一个月内?季节性?)。标准化时间的惯例是协调世界时(UTC),这是一个国际时间标准,在 0 度纬度上测量,整年保持一致(没有夏令时)。我们可以表示伯克利的时区,太平洋标准时间(PST),为 UTC-7(夏令时)。
pandas
的dt
访问器进行时间处理让我们简要地看一下如何使用pandas
的dt
访问器来处理数据集中的日期/时间,使用你在实验 3 中看到的数据集:伯克利警察服务呼叫数据集。
Code
calls = pd.read_csv("data/Berkeley_PD_-_Calls_for_Service.csv")
calls.head()
CASENO | OFFENSE | EVENTDT | EVENTTM | CVLEGEND | CVDOW | InDbDate | Block_Location | BLKADDR | City | State | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 21014296 | THEFT MISD. (UNDER $950) | 04/01/2021 12:00:00 AM | 10:58 | LARCENY | 4 | 06/15/2021 12:00:00 AM | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
1 | 21014391 | THEFT MISD. (UNDER $950) | 04/01/2021 12:00:00 AM | 10:38 | LARCENY | 4 | 06/15/2021 12:00:00 AM | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
2 | 21090494 | THEFT MISD. (UNDER $950) | 04/19/2021 12:00:00 AM | 12:15 | LARCENY | 1 | 06/15/2021 12:00:00 AM | 2100 BLOCK HASTE ST\nBerkeley, CA\n(37.864908,… | 2100 BLOCK HASTE ST | Berkeley | CA |
3 | 21090204 | THEFT FELONY (OVER $950) | 02/13/2021 12:00:00 AM | 17:00 | LARCENY | 6 | 06/15/2021 12:00:00 AM | 2600 BLOCK WARRING ST\nBerkeley, CA\n(37.86393… | 2600 BLOCK WARRING ST | Berkeley | CA |
4 | 21090179 | BURGLARY AUTO | 02/08/2021 12:00:00 AM | 6:20 | BURGLARY - VEHICLE | 1 | 06/15/2021 12:00:00 AM | 2700 BLOCK GARBER ST\nBerkeley, CA\n(37.86066,… | 2700 BLOCK GARBER ST | Berkeley | CA |
看起来有三列带有日期/时间:EVENTDT
,EVENTTM
和InDbDate
。
很可能,EVENTDT
代表事件发生的日期,EVENTTM
代表事件发生的时间(24 小时制),InDbDate
是这个呼叫被记录到数据库的日期。
如果我们检查这些列的数据类型,我们会发现它们被存储为字符串。我们可以使用 pandas 的to_datetime
函数将它们转换为datetime
对象。
calls["EVENTDT"] = pd.to_datetime(calls["EVENTDT"])
calls.head()
CASENO | OFFENSE | EVENTDT | EVENTTM | CVLEGEND | CVDOW | InDbDate | Block_Location | BLKADDR | City | State | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 21014296 | THEFT MISD. (UNDER $950) | 2021-04-01 | 10:58 | LARCENY | 4 | 06/15/2021 12:00:00 AM | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
1 | 21014391 | THEFT MISD. (UNDER $950) | 2021-04-01 | 10:38 | LARCENY | 4 | 06/15/2021 12:00:00 AM | Berkeley, CA\n(37.869058, -122.270455) | NaN | Berkeley | CA |
2 | 21090494 | THEFT MISD. (UNDER $950) | 2021-04-19 | 12:15 | LARCENY | 1 | 06/15/2021 12:00:00 AM | 2100 BLOCK HASTE ST\nBerkeley, CA\n(37.864908,… | 2100 BLOCK HASTE ST | Berkeley | CA |
3 | 21090204 | THEFT FELONY (OVER $950) | 2021-02-13 | 17:00 | LARCENY | 6 | 06/15/2021 12:00:00 AM | 2600 BLOCK WARRING ST\nBerkeley, CA\n(37.86393… | 2600 BLOCK WARRING ST | Berkeley | CA |
4 | 21090179 | BURGLARY AUTO | 2021-02-08 | 6:20 | BURGLARY - VEHICLE | 1 | 06/15/2021 12:00:00 AM | 2700 BLOCK GARBER ST\nBerkeley, CA\n(37.86066,… | 2700 BLOCK GARBER ST | Berkeley | CA |
现在,我们可以在这一列上使用dt
访问器。
我们可以得到月份:
calls["EVENTDT"].dt.month.head()
0 4
1 4
2 4
3 2
4 2
Name: EVENTDT, dtype: int64
日期是一周中的哪一天:
calls["EVENTDT"].dt.dayofweek.head()
0 3
1 3
2 0
3 5
4 0
Name: EVENTDT, dtype: int64
检查最小值,看看是否有任何看起来可疑的 70 年代日期:
calls.sort_values("EVENTDT").head()
CASENO | OFFENSE | EVENTDT | EVENTTM | CVLEGEND | CVDOW | InDbDate | Block_Location | BLKADDR | City | State | |
---|---|---|---|---|---|---|---|---|---|---|---|
2513 | 20057398 | BURGLARY COMMERCIAL | 2020-12-17 | 16:05 | BURGLARY - COMMERCIAL | 4 | 06/15/2021 12:00:00 AM | 600 BLOCK GILMAN ST\nBerkeley, CA\n(37.878405,… | 600 BLOCK GILMAN ST | Berkeley | CA |
624 | 20057207 | ASSAULT/BATTERY MISD. | 2020-12-17 | 16:50 | ASSAULT | 4 | 06/15/2021 12:00:00 AM | 2100 BLOCK SHATTUCK AVE\nBerkeley, CA\n(37.871… | 2100 BLOCK SHATTUCK AVE | Berkeley | CA |
154 | 20092214 | THEFT FROM AUTO | 2020-12-17 | 18:30 | LARCENY - FROM VEHICLE | 4 | 06/15/2021 12:00:00 AM | 800 BLOCK SHATTUCK AVE\nBerkeley, CA\n(37.8918… | 800 BLOCK SHATTUCK AVE | Berkeley | CA |
659 | 20057324 | THEFT MISD. (UNDER $950) | 2020-12-17 | 15:44 | LARCENY | 4 | 06/15/2021 12:00:00 AM | 1800 BLOCK 4TH ST\nBerkeley, CA\n(37.869888, -… | 1800 BLOCK 4TH ST | Berkeley | CA |
993 | 20057573 | BURGLARY RESIDENTIAL | 2020-12-17 | 22:15 | BURGLARY - RESIDENTIAL | 4 | 06/15/2021 12:00:00 AM | 1700 BLOCK STUART ST\nBerkeley, CA\n(37.857495… | 1700 BLOCK STUART ST | Berkeley | CA |
看起来不像!我们做得很好!
我们还可以使用dt
访问器执行许多操作,例如切换时区和将时间转换回 UNIX/POSIX 时间。查看.dt
访问器和时间序列/日期功能的文档。
在数据清理和 EDA 工作流的这个阶段,我们已经取得了相当大的成就:我们已经确定了数据的结构,了解了它所编码的信息,并获得了有关它是如何生成的见解。在整个过程中,我们应该始终记住数据科学工作的最初目的 - 使用数据更好地理解和建模现实世界。为了实现这一目标,我们需要确保我们使用的数据忠实于现实;也就是说,我们的数据准确地捕捉了“真实世界”。
用于研究或工业的数据通常是“混乱的” - 可能存在影响数据集忠实度的错误或不准确性。数据可能不忠实的迹象包括:
不切实际或“错误”的值,例如负计数、不存在的位置或设置在未来的日期
违反明显依赖关系的迹象,例如年龄与生日不匹配
明显表明数据是手工输入的迹象,这可能导致拼写错误或字段错误移位
数据伪造的迹象,例如虚假的电子邮件地址或重复使用相同的名称
包含相同信息的重复记录或字段
截断数据,例如 Microsoft Excel 将行数限制为 655536,列数限制为 255
我们通常通过以下方式解决一些更常见的问题:
拼写错误:应用更正或删除不在字典中的记录
时区不一致:转换为通用时区(例如 UTC)
重复的记录或字段:识别和消除重复项(使用主键)
未指定或不一致的单位:推断单位并检查数据中的值是否在合理范围内
现实世界数据集经常遇到的另一个常见问题是缺失数据。解决这个问题的一种策略是从数据集中简单地删除任何具有缺失值的记录。然而,这会引入引入偏见的风险 - 缺失或损坏的记录可能与数据中感兴趣的某些特征有系统关联。另一个解决方案是将数据保留为NaN
值。
解决缺失数据的第三种方法是执行插补:使用数据集中的其他数据推断缺失值。可以实施各种插补技术;以下是一些最常见的插补技术。
平均插补:用该字段的平均值替换缺失值
热卡插补:用某个随机值替换缺失值
回归插补:开发模型以预测缺失值
多重插补:用多个随机值替换缺失值
无论使用何种策略来处理缺失数据,我们都应该仔细考虑为什么特定记录或字段可能丢失 - 这可以帮助确定这些值的缺失是否重要或有意义。
现在,让我们走一遍数据清理和 EDA 工作流程,看看我们能从美国的结核病情况中学到什么!
我们将检查2021 年发表的原始 CDC 文章中包含的数据。
假设表 1 被保存为位于data/cdc_tuberculosis.csv
的 CSV 文件。
然后,我们可以以多种方式探索 CSV(这是一个文本文件,不包含二进制编码数据):1. 使用文本编辑器如 emacs,vim,VSCode 等。2. 直接在 DataHub(只读),Excel,Google Sheets 等中打开 CSV。3. Python
文件对象 4. pandas
,使用pd.read_csv()
要尝试选项 1 和 2,您可以在左侧菜单中的data
文件夹下查看或下载来自演示笔记本的结核病数据。请注意,CSV 文件是一种矩形数据(即表格数据),存储为逗号分隔的值。
接下来,让我们尝试使用Python
文件对象的选项 3。我们将查看前四行:
代码
with open("data/cdc_tuberculosis.csv", "r") as f:
i = 0
for row in f:
print(row)
i += 1
if i > 3:
break
,No. of TB cases,,,TB incidence,,
U.S. jurisdiction,2019,2020,2021,2019,2020,2021
Total,"8,900","7,173","7,860",2.71,2.16,2.37
Alabama,87,72,92,1.77,1.43,1.83
哇,为什么在 CSV 的行之间有空行?
您可能还记得文本文件中的所有换行符都被编码为特殊的换行符\n
。 Python 的print()
打印每个字符串(包括换行符),并在此基础上再添加一个换行符。
如果您感兴趣,我们可以使用repr()
函数返回带有所有特殊字符的原始字符串:
代码
with open("data/cdc_tuberculosis.csv", "r") as f:
i = 0
for row in f:
print(repr(row)) # print raw strings
i += 1
if i > 3:
break
',No. of TB cases,,,TB incidence,,\n'
'U.S. jurisdiction,2019,2020,2021,2019,2020,2021\n'
'Total,"8,900","7,173","7,860",2.71,2.16,2.37\n'
'Alabama,87,72,92,1.77,1.43,1.83\n'
最后,让我们尝试选项 4,并使用经过验证的 Data 100 方法:pandas
。
tb_df = pd.read_csv("data/cdc_tuberculosis.csv")
tb_df.head()
Unnamed: 0 | TB cases | Unnamed: 2 | Unnamed: 3 | TB incidence | Unnamed: 5 | Unnamed: 6 | |
---|---|---|---|---|---|---|---|
0 | U.S. jurisdiction | 2019 | 2020 | 2021 | 2019.00 | 2020.00 | 2021.00 |
1 | Total | 8,900 | 7,173 | 7,860 | 2.71 | 2.16 | 2.37 |
2 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 |
3 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 |
4 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 |
您可能会注意到这个表格有一些奇怪的地方:列名中的“未命名”是怎么回事,以及第一行是什么?
恭喜 - 您已经准备好整理您的数据了!由于数据的存储方式,我们需要稍微清理一下数据,以更好地命名我们的列。
一个合理的第一步是识别正确标题的行。pd.read_csv()
函数(文档)具有方便的header
参数,我们可以将其设置为使用第 1 行的元素作为适当的列:
tb_df = pd.read_csv("data/cdc_tuberculosis.csv", header=1) # row index
tb_df.head(5)
U.S. jurisdiction | 2019 | 2020 | 2021 | 2019.1 | 2020.1 | 2021.1 | |
---|---|---|---|---|---|---|---|
0 | Total | 8,900 | 7,173 | 7,860 | 2.71 | 2.16 | 2.37 |
1 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 |
2 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 |
3 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 |
4 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 |
等等…现在我们无法区分“结核病病例数”和“结核病发生率”年列。 pandas
已经尝试通过自动向后面的列添加“.1”来简化我们的生活,但这并不能帮助我们,作为人类,理解数据。
我们可以使用df.rename()
(文档)手动执行此操作:
rename_dict = {'2019': 'TB cases 2019',
'2020': 'TB cases 2020',
'2021': 'TB cases 2021',
'2019.1': 'TB incidence 2019',
'2020.1': 'TB incidence 2020',
'2021.1': 'TB incidence 2021'}
tb_df = tb_df.rename(columns=rename_dict)
tb_df.head(5)
U.S. jurisdiction | TB cases 2019 | TB cases 2020 | TB cases 2021 | TB incidence 2019 | TB incidence 2020 | TB incidence 2021 | |
---|---|---|---|---|---|---|---|
0 | Total | 8,900 | 7,173 | 7,860 | 2.71 | 2.16 | 2.37 |
1 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 |
2 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 |
3 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 |
4 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 |
你可能已经在想:第一条记录怎么了?
第 0 行是我们所谓的汇总记录,或摘要记录。在向人类显示表格时,它通常很有用。记录 0(总计)的粒度与其他记录(州)的粒度不同。
好的,探索性数据分析第二步。汇总记录是如何聚合的?
让我们检查总结核病例是否是所有州结核病例的总和。如果我们对所有行求和,我们应该得到每年结核病病例的总数的2 倍(你认为这是为什么?)。
代码
tb_df.sum(axis=0)
U.S. jurisdiction TotalAlabamaAlaskaArizonaArkansasCaliforniaCol...
TB cases 2019 8,9008758183642,111666718245583029973261085237...
TB cases 2020 7,1737258136591,706525417194122219282169239376...
TB cases 2021 7,8609258129691,750585443194992281064255127494...
TB incidence 2019 109.94
TB incidence 2020 93.09
TB incidence 2021 102.94
dtype: object
哇,2019 年、2020 年和 2021 年的结核病病例怎么了?查看列类型:
代码
tb_df.dtypes
U.S. jurisdiction object
TB cases 2019 object
TB cases 2020 object
TB cases 2021 object
TB incidence 2019 float64
TB incidence 2020 float64
TB incidence 2021 float64
dtype: object
由于结核病病例的值中有逗号,数字被读取为object
数据类型,或存储类型(接近Python
字符串数据类型),因此pandas
正在连接字符串而不是添加整数(回想一下Python
可以“求和”或连接字符串在一起:"data" + "100"
的结果是"data100"
)。
幸运的是,read_csv
还有一个thousands
参数(文档):
# improve readability: chaining method calls with outer parentheses/line breaks
tb_df = (
pd.read_csv("data/cdc_tuberculosis.csv", header=1, thousands=',')
.rename(columns=rename_dict)
)
tb_df.head(5)
U.S. jurisdiction | TB cases 2019 | TB cases 2020 | TB cases 2021 | TB incidence 2019 | TB incidence 2020 | TB incidence 2021 | |
---|---|---|---|---|---|---|---|
0 | Total | 8900 | 7173 | 7860 | 2.71 | 2.16 | 2.37 |
1 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 |
2 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 |
3 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 |
4 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 |
tb_df.sum()
U.S. jurisdiction TotalAlabamaAlaskaArizonaArkansasCaliforniaCol...
TB cases 2019 17800
TB cases 2020 14346
TB cases 2021 15720
TB incidence 2019 109.94
TB incidence 2020 93.09
TB incidence 2021 102.94
dtype: object
总结核病例看起来没问题。哦!
让我们只看具有州级粒度的记录:
代码
state_tb_df = tb_df[1:]
state_tb_df.head(5)
U.S. jurisdiction | TB cases 2019 | TB cases 2020 | TB cases 2021 | TB incidence 2019 | TB incidence 2020 | TB incidence 2021 | |
---|---|---|---|---|---|---|---|
1 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 |
2 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 |
3 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 |
4 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 |
5 | California | 2111 | 1706 | 1750 | 5.35 | 4.32 | 4.46 |
美国人口普查人口估计来源(2019 年),来源(2020-2021 年)。
运行下面的单元格清理数据。这里有一些新的方法:* df.convert_dtypes()
(文档)方便地将所有浮点数类型转换为整数,超出了课程范围。* df.drop_na()
(文档)将在下次详细解释。
代码
# 2010s census data
census_2010s_df = pd.read_csv("data/nst-est2019-01.csv", header=3, thousands=",")
census_2010s_df = (
census_2010s_df
.reset_index()
.drop(columns=["index", "Census", "Estimates Base"])
.rename(columns={"Unnamed: 0": "Geographic Area"})
.convert_dtypes() # "smart" converting of columns, use at your own risk
.dropna() # we'll introduce this next time
)
census_2010s_df['Geographic Area'] = census_2010s_df['Geographic Area'].str.strip('.')
# with pd.option_context('display.min_rows', 30): # shows more rows
# display(census_2010s_df)
census_2010s_df.head(5)
Geographic Area | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | American | 309,321,666 | 311,556,874 | 313,830,990 | 315,993,715 | 318,301,008 | 320,635,163 | 322,941,311 | 324,985,539 | 326,687,501 | 328,239,523 |
1 | Northeast | 55380134 | 55604223 | 55775216 | 55901806 | 56006011 | 56034684 | 56042330 | 56059240 | 56046620 | 55982803 |
2 | Midwest | 66974416 | 67157800 | 67336743 | 67560379 | 67745167 | 67860583 | 67987540 | 68126781 | 68236628 | 68329004 |
3 | South | 114866680 | 116006522 | 117241208 | 118364400 | 119624037 | 120997341 | 122351760 | 123542189 | 124569433 | 125580448 |
4 | West | 72100436 | 72788329 | 73477823 | 74167130 | 74925793 | 75742555 | 76559681 | 77257329 | 77834820 | 78347268 |
有时,您会想要修改导入的代码。要重新导入这些修改,您可以使用python
的importlib
库:
from importlib import reload
reload(utils)
或者使用iPython
魔术,它将在文件更改时智能地导入代码:
%load_ext autoreload
%autoreload 2
代码
# census 2020s data
census_2020s_df = pd.read_csv("data/NST-EST2022-POP.csv", header=3, thousands=",")
census_2020s_df = (
census_2020s_df
.reset_index()
.drop(columns=["index", "Unnamed: 1"])
.rename(columns={"Unnamed: 0": "Geographic Area"})
.convert_dtypes() # "smart" converting of columns, use at your own risk
.dropna() # we'll introduce this next time
)
census_2020s_df['Geographic Area'] = census_2020s_df['Geographic Area'].str.strip('.')
census_2020s_df.head(5)
Geographic Area | 2020 | 2021 | 2022 | |
---|---|---|---|---|
0 | American | 331511512 | 332031554 | 333287557 |
1 | Northeast | 57448898 | 57259257 | 57040406 |
2 | Midwest | 68961043 | 68836505 | 68787595 |
3 | South | 126450613 | 127346029 | 128716192 |
4 | West | 78650958 | 78589763 | 78743364 |
时间merge
!这里我们使用DataFrame
方法df1.merge(right=df2, ...)
在DataFrame
df1
上(文档)。与函数pd.merge(left=df1, right=df2, ...)
(文档)进行对比。可以随意使用任何一个。
# merge TB DataFrame with two US census DataFrames
tb_census_df = (
tb_df
.merge(right=census_2010s_df,
left_on="U.S. jurisdiction", right_on="Geographic Area")
.merge(right=census_2020s_df,
left_on="U.S. jurisdiction", right_on="Geographic Area")
)
tb_census_df.head(5)
U.S. jurisdiction | TB cases 2019 | TB cases 2020 | TB cases 2021 | TB incidence 2019 | TB incidence 2020 | TB incidence 2021 | Geographic Area_x | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | Geographic Area_y | 2020 | 2021 | 2022 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 | Alabama | 4785437 | 4799069 | 4815588 | 4830081 | 4841799 | 4852347 | 4863525 | 4874486 | 4887681 | 4903185 | Alabama | 5031362 | 5049846 | 5074296 |
1 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 | Alaska | 713910 | 722128 | 730443 | 737068 | 736283 | 737498 | 741456 | 739700 | 735139 | 731545 | Alaska | 732923 | 734182 | 733583 |
2 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 | Arizona | 6407172 | 6472643 | 6554978 | 6632764 | 6730413 | 6829676 | 6941072 | 7044008 | 7158024 | 7278717 | Arizona | 7179943 | 7264877 | 7359197 |
3 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 | Arkansas | 2921964 | 2940667 | 2952164 | 2959400 | 2967392 | 2978048 | 2989918 | 3001345 | 3009733 | 3017804 | Arkansas | 3014195 | 3028122 | 3045637 |
4 | California | 2111 | 1706 | 1750 | 5.35 | 4.32 | 4.46 | California | 37319502 | 37638369 | 37948800 | 38260787 | 38596972 | 38918045 | 39167117 | 39358497 | 39461588 | 39512223 | California | 39501653 | 39142991 | 39029342 |
拥有所有这些列有点不方便。我们现在可以删除不需要的列,或者只是合并较小的人口普查“DataFrame”。让我们选择后者。
# try merging again, but cleaner this time
tb_census_df = (
tb_df
.merge(right=census_2010s_df[["Geographic Area", "2019"]],
left_on="U.S. jurisdiction", right_on="Geographic Area")
.drop(columns="Geographic Area")
.merge(right=census_2020s_df[["Geographic Area", "2020", "2021"]],
left_on="U.S. jurisdiction", right_on="Geographic Area")
.drop(columns="Geographic Area")
)
tb_census_df.head(5)
U.S. jurisdiction | TB cases 2019 | TB cases 2020 | TB cases 2021 | TB incidence 2019 | TB incidence 2020 | TB incidence 2021 | 2019 | 2020 | 2021 | |
---|---|---|---|---|---|---|---|---|---|---|
0 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 | 4903185 | 5031362 | 5049846 |
1 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 | 731545 | 732923 | 734182 |
2 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 | 7278717 | 7179943 | 7264877 |
3 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 | 3017804 | 3014195 | 3028122 |
4 | California | 2111 | 1706 | 1750 | 5.35 | 4.32 | 4.46 | 39512223 | 39501653 | 39142991 |
让我们重新计算发病率,以确保我们知道原始 CDC 数字来自何处。
根据疾病控制和预防中心的报告:TB 发病率计算为“使用美国人口普查局的中期人口估计,每 10 万人的病例数”。
如果我们将一个群体定义为 10 万人,那么我们可以计算给定州人口的 TB 发病率为
TB?发病率 = 人口中的?TB?病例 人口中的群体 = 人口中的?TB?病例 人口 / 100000 \text{TB 发病率} = \frac{\text{人口中的 TB 病例}}{\text{人口中的群体}} = \frac{\text{人口中的 TB 病例}}{\text{人口}/100000} TB?发病率=人口中的群体人口中的?TB?病例?=人口/100000人口中的?TB?病例?
= 人口中的?TB?病例 人口 × 100000 = \frac{\text{人口中的 TB 病例}}{\text{人口}} \times 100000 =人口人口中的?TB?病例?×100000
让我们尝试 2019 年的情况:
tb_census_df["recompute incidence 2019"] = tb_census_df["TB cases 2019"]/tb_census_df["2019"]*100000
tb_census_df.head(5)
U.S. jurisdiction | TB Cases 2019 | TB Cases 2020 | TB Cases 2021 | TB Incidents 2019 | TB Incidents 2020 | TB Incidents 2021 | 2019 | 2020 | 2021 | recompute incidence 2019 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 | 4903185 | 5031362 | 5049846 | 1.77 |
1 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 | 731545 | 732923 | 734182 | 7.93 |
2 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 | 7278717 | 7179943 | 7264877 | 2.51 |
3 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 | 3017804 | 3014195 | 3028122 | 2.12 |
4 | California | 2111 | 1706 | 1750 | 5.35 | 4.32 | 4.46 | 39512223 | 39501653 | 39142991 | 5.34 |
太棒了!!!
让我们使用 for 循环和Python
格式字符串来计算所有年份的 TB 发病率。Python
f-strings 仅用于此演示目的,但在探索本课程之外的数据时,它们很方便(文档)。
# recompute incidence for all years
for year in [2019, 2020, 2021]:
tb_census_df[f"recompute incidence {year}"] = tb_census_df[f"TB cases {year}"]/tb_census_df[f"{year}"]*100000
tb_census_df.head(5)
U.S. jurisdiction | TB Cases 2019 | TB Cases 2020 | TB Cases 2021 | TB Incidents 2019 | TB Incidents 2020 | TB Incidents 2021 | 2019 | 2020 | 2021 | recompute incidence 2019 | recompute incidence 2020 | recompute incidence 2021 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 | 4903185 | 5031362 | 5049846 | 1.77 | 1.43 | 1.82 |
1 | Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 | 731545 | 732923 | 734182 | 7.93 | 7.91 | 7.90 |
2 | Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 | 7278717 | 7179943 | 7264877 | 2.51 | 1.89 | 1.78 |
3 | Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 | 3017804 | 3014195 | 3028122 | 2.12 | 1.96 | 2.28 |
4 | California | 2111 | 1706 | 1750 | 5.35 | 4.32 | 4.46 | 39512223 | 39501653 | 39142991 | 5.34 | 4.32 | 4.47 |
这些数字看起来非常接近!!!特别是在 2021 年,百分位数的小数位上有一些错误。进一步探讨这种差异背后的原因可能是有用的。
tb_census_df.describe()
TB Cases 2019 | TB Cases 2020 | TB Cases 2021 | TB Incidents 2019 | TB Incidents 2020 | TB Incidents 2021 | 2019 | 2020 | 2021 | recompute incidence 2019 | recompute incidence 2020 | recompute incidence 2021 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 | 51.00 |
mean | 174.51 | 140.65 | 154.12 | 2.10 | 1.78 | 1.97 | 6436069.08 | 6500225.73 | 6510422.63 | 2.10 | 1.78 | 1.97 |
mean | 341.74 | 271.06 | 286.78 | 1.50 | 1.34 | 1.48 | 7360660.47 | 7408168.46 | 7394300.08 | 1.50 | 1.34 | 1.47 |
min | 1.00 | 0.00 | 2.00 | 0.17 | 0.00 | 0.21 | 578759.00 | 577605.00 | 579483.00 | 0.17 | 0.00 | 0.21 |
25% | 25.50 | 29.00 | 23.00 | 1.29 | 1.21 | 1.23 | 1789606.00 | 1820311.00 | 1844920.00 | 1.30 | 1.21 | 1.23 |
50% | 70.00 | 67.00 | 69.00 | 1.80 | 1.52 | 1.70 | 4467673.00 | 4507445.00 | 4506589.00 | 1.81 | 1.52 | 1.69 |
75% | 180.50 | 139.00 | 150.00 | 2.58 | 1.99 | 2.22 | 7446805.00 | 7451987.00 | 7502811.00 | 2.58 | 1.99 | 2.22 |
min | 2111.00 | 1706.00 | 1750.00 | 7.91 | 7.92 | 7.92 | 39512223.00 | 39501653.00 | 39142991.00 | 7.93 | 7.91 | 7.90 |
我们如何重现原始CDC 报告中报告的统计数据?
报告的结核病发病率(每 10 万人口的病例数)增加了9.4%,从 2020 年的2.2增加到 2021 年的2.4,但低于 2019 年的发病率(2.7)。美国出生和非美国出生人群的发病率均有所增加。
这是在整个美国人口中计算的结核病发病率!我们如何重现这一点?*我们需要重现我们滚动记录中的“总”结核病发病率。*但是我们当前的tb_census_df
只有 51 个条目(50 个州加上华盛顿特区)。没有滚动记录。*发生了什么…?
让我们开始探索吧!
在我们继续探索之前,我们将所有索引设置为更有意义的值,而不仅仅是与某一行相关的数字。这将使我们的清理稍微容易一些。
代码
tb_df = tb_df.set_index("U.S. jurisdiction")
tb_df.head(5)
TB Cases 2019 | TB Cases 2020 | TB Cases 2021 | TB Incidents 2019 | TB Incidents 2020 | TB Incidents 2021 | |
---|---|---|---|---|---|---|
U.S. jurisdiction | ||||||
Total | 8900 | 7173 | 7860 | 2.71 | 2.16 | 2.37 |
Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 |
Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 |
Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 |
Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 |
census_2010s_df = census_2010s_df.set_index("Geographic Area")
census_2010s_df.head(5)
2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | |
---|---|---|---|---|---|---|---|---|---|---|
Geographic Area | ||||||||||
American | 309321666 | 311556874 | 313830990 | 315993715 | 318301008 | 320635163 | 322941311 | 324985539 | 326687501 | 328239523 |
Northeast | 55380134 | 55604223 | 55775216 | 55901806 | 56006011 | 56034684 | 56042330 | 56059240 | 56046620 | 55982803 |
Midwest | 66974416 | 67157800 | 67336743 | 67560379 | 67745167 | 67860583 | 67987540 | 68126781 | 68236628 | 68329004 |
South | 114866680 | 116006522 | 117241208 | 118364400 | 119624037 | 120997341 | 122351760 | 123542189 | 124569433 | 125580448 |
West | 72100436 | 72788329 | 73477823 | 74167130 | 74925793 | 75742555 | 76559681 | 77257329 | 77834820 | 78347268 |
census_2020s_df = census_2020s_df.set_index("Geographic Area")
census_2020s_df.head(5)
2020 | 2021 | 2022 | |
---|---|---|---|
Geographic Area | |||
American | 331511512 | 332031554 | 333287557 |
Northeast | 57448898 | 57259257 | 57040406 |
Midwest | 68961043 | 68836505 | 68787595 |
South | 126450613 | 127346029 | 128716192 |
West | 78650958 | 78589763 | 78743364 |
事实证明,我们上面的合并只保留了州记录,即使我们原始的tb_df
中有“总计”滚动记录:
tb_df.head()
TB Cases 2019 | TB Cases 2020 | TB Cases 2021 | TB Incidents 2019 | TB Incidents 2020 | TB Incidents 2021 | |
---|---|---|---|---|---|---|
U.S. jurisdiction | ||||||
Total | 8900 | 7173 | 7860 | 2.71 | 2.16 | 2.37 |
Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 |
Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 |
Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 |
Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 |
请记住,默认情况下,merge
执行内部合并,默认情况下,这意味着它只保留在两个DataFrame
中都存在的键。
我们人口普查DataFrame
中的滚动记录具有不同的地理区域
字段,这是我们合并的关键:
census_2010s_df.head(5)
2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | |
---|---|---|---|---|---|---|---|---|---|---|
Geographic Area | ||||||||||
American | 309321666 | 311556874 | 313830990 | 315993715 | 318301008 | 320635163 | 322941311 | 324985539 | 326687501 | 328239523 |
Northeast | 55380134 | 55604223 | 55775216 | 55901806 | 56006011 | 56034684 | 56042330 | 56059240 | 56046620 | 55982803 |
Midwest | 66974416 | 67157800 | 67336743 | 67560379 | 67745167 | 67860583 | 67987540 | 68126781 | 68236628 | 68329004 |
South | 114866680 | 116006522 | 117241208 | 118364400 | 119624037 | 120997341 | 122351760 | 123542189 | 124569433 | 125580448 |
West | 72100436 | 72788329 | 73477823 | 74167130 | 74925793 | 75742555 | 76559681 | 77257329 | 77834820 | 78347268 |
人口普查DataFrame
有几个已经合并的记录。我们正在寻找的聚合记录实际上将地理区域命名为“美国”。
有一个直接的方法来获得正确的合并,那就是重命名值本身。因为我们现在有地理区域索引,我们将使用df.rename()
(文档):
# rename rolled record for 2010s
census_2010s_df.rename(index={'United States':'Total'}, inplace=True)
census_2010s_df.head(5)
2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | |
---|---|---|---|---|---|---|---|---|---|---|
Geographic Area | ||||||||||
Total | 309321666 | 311556874 | 313830990 | 315993715 | 318301008 | 320635163 | 322941311 | 324985539 | 326687501 | 328239523 |
Northeast | 55380134 | 55604223 | 55775216 | 55901806 | 56006011 | 56034684 | 56042330 | 56059240 | 56046620 | 55982803 |
Midwest | 66974416 | 67157800 | 67336743 | 67560379 | 67745167 | 67860583 | 67987540 | 68126781 | 68236628 | 68329004 |
South | 114866680 | 116006522 | 117241208 | 118364400 | 119624037 | 120997341 | 122351760 | 123542189 | 124569433 | 125580448 |
West | 72100436 | 72788329 | 73477823 | 74167130 | 74925793 | 75742555 | 76559681 | 77257329 | 77834820 | 78347268 |
# same, but for 2020s rename rolled record
census_2020s_df.rename(index={'United States':'Total'}, inplace=True)
census_2020s_df.head(5)
2020 | 2021 | 2022 | |
---|---|---|---|
Geographic Area | |||
Total | 331511512 | 332031554 | 333287557 |
Northeast | 57448898 | 57259257 | 57040406 |
Midwest | 68961043 | 68836505 | 68787595 |
South | 126450613 | 127346029 | 128716192 |
West | 78650958 | 78589763 | 78743364 |
接下来让我们重新运行我们的合并。请注意不同的链接方式,因为我们现在是在索引上进行合并(df.merge()
文档)。
tb_census_df = (
tb_df
.merge(right=census_2010s_df[["2019"]],
left_index=True, right_index=True)
.merge(right=census_2020s_df[["2020", "2021"]],
left_index=True, right_index=True)
)
tb_census_df.head(5)
TB Cases 2019 | TB Cases 2020 | TB Cases 2021 | TB Incidents 2019 | TB Incidents 2020 | TB Incidents 2021 | 2019 | 2020 | 2021 | |
---|---|---|---|---|---|---|---|---|---|
Total | 8900 | 7173 | 7860 | 2.71 | 2.16 | 2.37 | 328239523 | 331511512 | 332031554 |
Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 | 4903185 | 5031362 | 5049846 |
Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 | 731545 | 732923 | 734182 |
Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 | 7278717 | 7179943 | 7264877 |
Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 | 3017804 | 3014195 | 3028122 |
最后,让我们重新计算我们的发病率:
# recompute incidence for all years
for year in [2019, 2020, 2021]:
tb_census_df[f"recompute incidence {year}"] = tb_census_df[f"TB cases {year}"]/tb_census_df[f"{year}"]*100000
tb_census_df.head(5)
TB Cases 2019 | TB Cases 2020 | TB Cases 2021 | TB Incidents 2019 | TB Incidents 2020 | TB Incidents 2021 | 2019 | 2020 | 2021 | recompute incidence 2019 | recompute incidence 2020 | recompute incidence 2021 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Total | 8900 | 7173 | 7860 | 2.71 | 2.16 | 2.37 | 328239523 | 331511512 | 332031554 | 2.71 | 2.16 | 2.37 |
Alabama | 87 | 72 | 92 | 1.77 | 1.43 | 1.83 | 4903185 | 5031362 | 5049846 | 1.77 | 1.43 | 1.82 |
Alaska | 58 | 58 | 58 | 7.91 | 7.92 | 7.92 | 731545 | 732923 | 734182 | 7.93 | 7.91 | 7.90 |
Arizona | 183 | 136 | 129 | 2.51 | 1.89 | 1.77 | 7278717 | 7179943 | 7264877 | 2.51 | 1.89 | 1.78 |
Arkansas | 64 | 59 | 69 | 2.12 | 1.96 | 2.28 | 3017804 | 3014195 | 3028122 | 2.12 | 1.96 | 2.28 |
我们正确地重现了美国的总发病率!
我们快要完成了。让我们重新审视这段引用:
报告的结核病发病率(每 10 万人口的病例)增加了9.4%,从 2020 年的2.2增加到 2021 年的2.4,但低于 2019 年的发病率(2.7)。美国出生和非美国出生人群的发病率均有所增加。
回想一下,从 A A A到 B B B的百分比变化计算公式为 percent?change = B ? A A × 100 \text{percent change} = \frac{B - A}{A} \times 100 percent?change=AB?A?×100。
incidence_2020 = tb_census_df.loc['Total', 'recompute incidence 2020']
incidence_2020
2.1637257652759883
incidence_2021 = tb_census_df.loc['Total', 'recompute incidence 2021']
incidence_2021
2.3672448914298068
difference = (incidence_2021 - incidence_2020)/incidence_2020 * 100
difference
9.405957511804143
毛纳罗亚观测站 自 1958 年以来一直在监测二氧化碳浓度
co2_file = "data/co2_mm_mlo.txt"
让我们做一些EDA!
让我们来看看这个.txt
文件。要记住的一些问题:我们信任这个文件扩展名吗?它的结构是什么?
第 71-78 行(包括)如下所示:
line number | file contents
71 | # decimal average interpolated trend #days
72 | # date (season corr)
73 | 1958 3 1958.208 315.71 315.71 314.62 -1
74 | 1958 4 1958.292 317.45 317.45 315.29 -1
75 | 1958 5 1958.375 317.50 317.50 314.71 -1
76 | 1958 6 1958.458 -99.99 317.10 314.85 -1
77 | 1958 7 1958.542 315.86 315.86 314.98 -1
78 | 1958 8 1958.625 314.93 314.93 315.94 -1
注意:
这些值由空格分隔,可能是制表符。
数据在行上排列。例如,每行的第 7 到第 8 个位置显示了月份。
文件的第 71 和 72 行包含分布在两行上的列标题。
我们可以使用read_csv
将数据读入pandas
的DataFrame
,并提供几个参数来指定分隔符是空格,没有标题(我们将设置自己的列名),并跳过文件的前 72 行。
co2 = pd.read_csv(
co2_file, header = None, skiprows = 72,
sep = r'\s+' #delimiter for continuous whitespace (stay tuned for regex next lecture))
)
co2.head()
0 1 2 3 4 5 6
0 1958 3 1958.21 315.71 315.71 314.62 -1
1 1958 4 1958.29 317.45 317.45 315.29 -1
2 1958 5 1958.38 317.50 317.50 314.71 -1
3 1958 6 1958.46 -99.99 317.10 314.85 -1
4 1958 7 1958.54 315.86 315.86 314.98 -1
恭喜!你已经整理好了数据!
…但是我们的列没有命名。我们需要做更多的 EDA。
NOAA 网页 可能有一些有用的信息(在这种情况下没有)。
利用这些信息,我们将重新运行pd.read_csv
,但这次使用一些自定义列名。
co2 = pd.read_csv(
co2_file, header = None, skiprows = 72,
sep = '\s+', #regex for continuous whitespace (next lecture)
names = ['Yr', 'Mo', 'DecDate', 'Avg', 'Int', 'Trend', 'Days']
)
co2.head()
Yr Mo DecDate Avg Int Trend Days
0 1958 3 1958.21 315.71 315.71 314.62 -1
1 1958 4 1958.29 317.45 317.45 315.29 -1
2 1958 5 1958.38 317.50 317.50 314.71 -1
3 1958 6 1958.46 -99.99 317.10 314.85 -1
4 1958 7 1958.54 315.86 315.86 314.98 -1
科学研究往往具有非常干净的数据,对吧…?让我们立即制作二氧化碳月均值的时间序列图。
代码
sns.lineplot(x='DecDate', y='Avg', data=co2);
上面的代码使用了seaborn
绘图库(缩写为sns
)。我们将在可视化讲座中介绍这一点,但现在你不需要担心它是如何工作的!
天啊!绘制数据揭示了一个问题。明显的垂直线表明我们有一些缺失值。这里发生了什么?
co2.head()
Yr Mo DecDate Avg Int Trend Days
0 1958 3 1958.21 315.71 315.71 314.62 -1
1 1958 4 1958.29 317.45 317.45 315.29 -1
2 1958 5 1958.38 317.50 317.50 314.71 -1
3 1958 6 1958.46 -99.99 317.10 314.85 -1
4 1958 7 1958.54 315.86 315.86 314.98 -1
co2.tail()
Yr Mo DecDate Avg Int Trend Days
733 2019 4 2019.29 413.32 413.32 410.49 26
734 2019 5 2019.38 414.66 414.66 411.20 28
735 2019 6 2019.46 413.92 413.92 411.58 27
736 2019 7 2019.54 411.77 411.77 411.43 23
737 2019 8 2019.62 409.95 409.95 411.84 29
一些数据有异常值,如-1 和-99.99。
让我们再次检查文件顶部的描述。
-1 表示该月设备运行的天数Days
的缺失值。
-99.99 表示缺失的月度平均Avg
我们该如何解决这个问题?首先,让我们探索数据的其他方面。了解我们的数据将帮助我们决定如何处理缺失值。
首先,我们考虑数据的形状。我们应该有多少行?
如果按时间顺序,我们应该每个月有一条记录。
数据从 1958 年 3 月到 2019 年 8 月。
我们应该有$ 12 (2019-1957) - 2 - 4 = 738 $条记录。
co2.shape
(738, 7)
太好了!行数(即记录)与我们的预期相匹配。
现在让我们检查每个特征的质量。
Days
Days
是一个时间字段,所以让我们分析其他时间字段,看看是否有关于操作天数缺失的解释。
让我们从月份Mo
开始。
我们有没有缺失的记录?月份的数量应该有 62 或 61 个实例(1957 年 3 月-2019 年 8 月)。
co2["Mo"].value_counts().sort_index()
1 61
2 61
3 62
4 62
5 62
6 62
7 62
8 62
9 61
10 61
11 61
12 61
Name: Mo, dtype: int64
如预期的那样,1 月、2 月、9 月、10 月、11 月和 12 月有 61 个实例,其余的有 62 个。
接下来让我们探索天数Days
本身,这是测量设备运行的天数。
代码
sns.displot(co2['Days']);
plt.title("Distribution of days feature"); # suppresses unneeded plotting output
/Users/Ishani/micromamba/lib/python3.9/site-packages/seaborn/axisgrid.py:118: UserWarning:
The figure layout has changed to tight
就数据质量而言,少数月份的平均值是基于少于一半天数的测量得出的。此外,有近 200 个缺失值-大约占数据的 27%!
最后,让我们检查最后一个时间特征,年份Yr
。
让我们检查一下缺失和记录年份之间是否有任何联系。
代码
sns.scatterplot(x="Yr", y="Days", data=co2);
plt.title("Day field by Year"); # the ; suppresses output
观察:
所有缺失的数据都在运营初期。
似乎 80 年代中后期可能出现了设备问题。
潜在的下一步:
通过有关历史读数的文档来确认这些解释。
也许删除最早的记录?但是,在我们检查时间趋势并评估是否存在潜在问题之后,我们会推迟这样的行动。
Avg
接下来,让我们回到Avg
中的-99.99 值,分析二氧化碳测量的整体质量。我们将绘制平均 CO[2]测量的直方图
代码
# Histograms of average CO2 measurements
sns.displot(co2['Avg']);
/Users/Ishani/micromamba/lib/python3.9/site-packages/seaborn/axisgrid.py:118: UserWarning:
The figure layout has changed to tight
非缺失值在 300-400 范围内(二氧化碳水平的常规范围)。
我们还看到只有少数缺失的“Avg”值(<1%的值)。让我们检查所有这些值:
co2[co2["Avg"] < 0]
Yr | Mo | DecDate | Avg | Int | Trend | Days | |
---|---|---|---|---|---|---|---|
3 | 1958 | 6 | 1958.46 | -99.99 | 317.10 | 314.85 | -1 |
7 | 1958 | 10 | 1958.79 | -99.99 | 312.66 | 315.61 | -1 |
71 | 1964 | 2 | 1964.12 | -99.99 | 320.07 | 319.61 | -1 |
72 | 1964 | 3 | 1964.21 | -99.99 | 320.73 | 319.55 | -1 |
73 | 1964 | 4 | 1964.29 | -99.99 | 321.77 | 319.48 | -1 |
213 | 1975 | 12 | 1975.96 | -99.99 | 330.59 | 331.60 | 0 |
313 | 1984 | 4 | 1984.29 | -99.99 | 346.84 | 344.27 | 2 |
这些值似乎没有任何模式,除了大多数记录也缺少了Days
数据。
NaN
或填补缺失的Avg
数据?我们应该如何处理无效的Avg
数据?
删除记录
设置为 NaN
使用某种策略填补
记住我们想要修复以下的图表:
代码
sns.lineplot(x='DecDate', y='Avg', data=co2)
plt.title("CO2 Average By Month");
由于我们正在绘制Avg
vs DecDate
,我们应该专注于处理Avg
的缺失值。
让我们考虑几个选项:1. 删除这些记录 2. 用 NaN 替换-99.99 3. 用平均 CO2 的可能值替换它?
你认为每种可能行动的利弊是什么?
让我们检查这三个选项。
# 1\. Drop missing values
co2_drop = co2[co2['Avg'] > 0]
co2_drop.head()
Yr | Mo | DecDate | Avg | Int | Trend | Days | |
---|---|---|---|---|---|---|---|
0 | 1958 | 3 | 1958.21 | 315.71 | 315.71 | 314.62 | -1 |
1 | 1958 | 4 | 1958.29 | 317.45 | 317.45 | 315.29 | -1 |
2 | 1958 | 5 | 1958.38 | 317.50 | 317.50 | 314.71 | -1 |
4 | 1958 | 7 | 1958.54 | 315.86 | 315.86 | 314.98 | -1 |
5 | 1958 | 8 | 1958.62 | 314.93 | 314.93 | 315.94 | -1 |
# 2\. Replace NaN with -99.99
co2_NA = co2.replace(-99.99, np.NaN)
co2_NA.head()
Year | Month | DecDate | Avg | Int | Trend | Days | |
---|---|---|---|---|---|---|---|
0 | 1958 | 3 | 1958.21 | 315.71 | 315.71 | 314.62 | -1 |
1 | 1958 | 4 | 1958.29 | 317.45 | 317.45 | 315.29 | -1 |
2 | 1958 | 5 | 1958.38 | 317.50 | 317.50 | 314.71 | -1 |
3 | 1958 | 6 | 1958.46 | NaN | 317.10 | 314.85 | -1 |
4 | 1958 | 7 | 1958.54 | 315.86 | 315.86 | 314.98 | -1 |
我们还将使用数据的第三个版本。
首先,我们注意到数据集已经为-99.99 提供了一个替代值。
从文件描述:
“插值”列包括前一列(“平均值”)的平均值和数据缺失时的插值值。插值值是通过两个步骤计算出来的…
Int
特征的值与Avg
完全匹配,只有当Avg
为-99.99 时,才会使用一个合理的估计。
因此,我们的数据的第三个版本将使用Int
特征而不是Avg
。
# 3\. Use interpolated column which estimates missing Avg values
co2_impute = co2.copy()
co2_impute['Avg'] = co2['Int']
co2_impute.head()
Year | Month | DecDate | Avg | Int | Trend | Days | |
---|---|---|---|---|---|---|---|
0 | 1958 | 3 | 1958.21 | 315.71 | 315.71 | 314.62 | -1 |
1 | 1958 | 4 | 1958.29 | 317.45 | 317.45 | 315.29 | -1 |
2 | 1958 | 5 | 1958.38 | 317.50 | 317.50 | 314.71 | -1 |
3 | 1958 | 6 | 1958.46 | 317.10 | 317.10 | 314.85 | -1 |
4 | 1958 | 7 | 1958.54 | 315.86 | 315.86 | 314.98 | -1 |
一个合理的估计是什么?
为了回答这个问题,让我们放大到一个短时间段,比如 1958 年的测量数据(我们知道有两个缺失值)。
代码
# results of plotting data in 1958
def line_and_points(data, ax, title):
# assumes single year, hence Mo
ax.plot('Mo', 'Avg', data=data)
ax.scatter('Mo', 'Avg', data=data)
ax.set_xlim(2, 13)
ax.set_title(title)
ax.set_xticks(np.arange(3, 13))
def data_year(data, year):
return data[data["Yr"] == 1958]
# uses matplotlib subplots
# you may see more next week; focus on output for now
fig, axes = plt.subplots(ncols = 3, figsize=(12, 4), sharey=True)
year = 1958
line_and_points(data_year(co2_drop, year), axes[0], title="1\. Drop Missing")
line_and_points(data_year(co2_NA, year), axes[1], title="2\. Missing Set to NaN")
line_and_points(data_year(co2_impute, year), axes[2], title="3\. Missing Interpolated")
fig.suptitle(f"Monthly Averages for {year}")
plt.tight_layout()
从大局来看,由于只有 7 个Avg
值缺失(占 738 个月的 <1%),任何这些方法都可以使用。
然而,选项 C:插补也有一定吸引力:
显示二氧化碳的季节性趋势
我们正在绘制数据中所有月份的线图
让我们用选项 3 重新绘制我们的原始图表:
代码
sns.lineplot(x='DecDate', y='Avg', data=co2_impute)
plt.title("CO2 Average By Month, Imputed");
看起来与 NOAA 网站上看到的差不多!
从描述:
月度测量是平均每日测量的平均值。
NOAA GML 网站也有每日/每小时测量的数据集。
您呈现的数据取决于您的研究问题。
二氧化碳水平如何随季节变化?
过去 50 多年来,二氧化碳水平是否上升,与全球变暖的预测一致?
代码
co2_year = co2_impute.groupby('Yr').mean()
sns.lineplot(x='Yr', y='Avg', data=co2_year)
plt.title("CO2 Average By Year");
事实上,自从毛纳罗亚开始记录以来,二氧化碳上升了近 100ppm。
我们在本讲座中涵盖了很多内容;让我们总结一下最重要的要点:
我们可以采取几种方法来处理缺失数据:
删除缺失记录
保留NaN
缺失值
使用插值列进行插补
有几种方法可以处理探索性数据分析和数据整理:
分析数据和元数据:数据的日期、大小、组织和结构是什么?
逐个检查每个字段/属性/维度。
逐对相关维度进行检查(例如,按专业分解等级)。
在这个过程中,我们可以:
可视化或总结数据。
验证关于数据及其收集过程的假设。特别注意数据收集的时间。
识别和解决异常。
应用数据转换和校正(我们将在即将到来的讲座中介绍)。
**记录你所做的一切!**在 Jupyter Notebook 中开发可以促进你自己工作的可重复性!