UCB Data100:数据科学的原理和技巧:第一章到第五章

发布时间 2024-01-12 23:40:29作者: 绝不原创的飞龙

一、引言

原文:Introduction

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 了解 Data 100 的总体目标

  • 了解数据科学生命周期的阶段

数据科学是一个跨学科领域,具有各种应用,并且在解决具有挑战性的社会问题方面具有巨大潜力。通过建立数据科学技能,您可以赋予自己参与和引领塑造您的生活和整个社会对话的能力,无论是与气候变化作斗争、推出多样性倡议,还是其他方面。

这个领域正在迅速发展;现代数据科学中许多关键技术基础在 21 世纪初得到了普及。

它基本上是以人为中心的,并通过定量平衡权衡来促进决策。为了可靠地量化事物,我们必须适当地使用和分析数据,对每一步都要进行批判性思考和怀疑,并考虑我们的决定如何影响他人。

最终,数据科学是将以数据为中心的、计算性的和推理性的思维应用于:

  • 了解世界(科学)。

  • 解决问题(工程)。

venn

对数据科学的真正掌握需要深刻的理论理解和对领域专业知识的牢固掌握。本课程将帮助您建立在前者基础上的技术知识,使您能够获取数据并对世界上最具挑战性和模糊的问题产生有用的见解。

课程目标

  • 为您准备伯克利高级课程,包括数据管理、机器学习和统计学

  • 使您能够在数据科学领域开展职业生涯

  • 使您能够通过计算和推理思维解决现实世界的问题

我们将涵盖的一些主题

  • Pandas 和 NumPy

  • 探索性数据分析

  • 正则表达式

  • 可视化

  • 抽样

  • 模型设计和损失公式

  • 线性回归

  • 梯度下降

  • 逻辑回归

  • 还有更多!

为了让您成功,我们将 Data 100 中的概念组织成了数据科学生命周期:一个迭代过程,涵盖了数据科学的各种统计和计算构建模块。

1.1 数据科学生命周期

数据科学生命周期是对数据科学工作流程的高级概述。这是一个数据科学家在对数据驱动的问题进行彻底分析时应该探索的阶段循环。

数据科学生命周期中存在许多关键思想的变体。在 Data 100 中,我们使用流程图来可视化生命周期的各个阶段。请注意,有两个入口点。

data_life_cycle

1.1.1 提出问题

无论是出于好奇还是出于必要,数据科学家不断提出问题。例如,在商业世界中,数据科学家可能对预测某项投资产生的利润感兴趣。在医学领域,他们可能会问一些患者是否比其他人更有可能从治疗中受益。

提出问题是数据科学生命周期开始的主要方式之一。它有助于充分定义问题。在构建问题之前,以下是一些您应该问自己的事情。

  • 我们想要知道什么?

    • 一个过于模糊的问题可能会导致混乱。
  • 我们试图解决什么问题?

    • 问一个问题的目标应该是清晰的,以便为利益相关者的努力提供合理的理由。
  • 我们想要测试的假设是什么?

    • 这为我们提供了一个清晰的视角,以分析最终结果。
  • 我们的成功指标是什么?

    • 这为我们建立了一个明确的观点,知道何时结束项目。

ask_question

1.1.2 获取数据

生命周期的第二个入口是通过获取数据。对任何问题的仔细分析都需要使用数据。数据可能对我们而言是 readily available,或者我们可能不得不着手收集数据。在这样做时,至关重要的是要问以下问题:

  • 我们有什么数据,我们需要什么数据?

    • 定义数据的单位(人、城市、时间点等)和要测量的特征。
  • 我们如何取样更多的数据?

    • 抓取网页,手动收集,进行实验等。
  • 我们的数据是否代表我们想研究的人群?

    • 如果我们的数据不代表我们感兴趣的人群,那么我们可能得出错误的结论。

关键程序:数据获取数据清洗

data_acquisition

1.1.3 理解数据

原始数据本身并不具有固有的用处。如果不仔细调查,就不可能辨别出所有变量之间的模式和关系。因此,将纯数据转化为可操作的见解是数据科学家的一项关键工作。例如,我们可以选择问:

  • 我们的数据是如何组织的,它包含了什么?

    • 了解数据对世界有何影响有助于我们更好地理解世界。
  • 我们有相关的数据吗?

    • 如果我们收集的数据对于手头的问题没有用处,那么我们必须收集更多的数据。
  • 数据中存在什么偏见、异常或其他问题?

    • 如果忽视这些问题,可能会导致许多错误的结论,因此数据科学家必须始终注意这些问题。
  • 我们如何转换数据以进行有效分析?

    • 数据并不总是一眼就容易解释的,因此数据科学家应该努力揭示隐藏的见解。

关键程序:探索性数据分析数据可视化

understanding_data

1.1.4 理解世界

在观察了数据中的模式之后,我们可以开始回答我们的问题。这可能需要我们预测一个数量(机器学习),或者衡量某种处理的效果(推断)。

从这里,我们可以选择报告我们的结果,或者可能进行更多的分析。我们可能对我们的发现不满意,或者我们的初步探索可能提出了需要新数据的新问题。

  • 数据对世界有何影响?

    • 根据我们的模型,数据将引导我们对真实世界的某些结论。
  • 它是否回答了我们的问题或准确解决了问题?

    • 如果我们的模型和数据不能实现我们的目标,那么我们必须改革我们的问题、模型,或者两者兼而有之。
  • 我们的结论有多可靠,我们能相信这些预测吗?

    • 不准确的模型可能导致错误的结论。

关键程序:模型创建预测推断

understand_world

1.2 结论

数据科学生命周期旨在成为一组一般性指导方针,而不是一套硬性要求。在探索生命周期的过程中,我们将涵盖数据科学中使用的基本理论和技术。在课程结束时,我们希望您开始把自己看作是一名数据科学家。

因此,我们将首先介绍探索性数据分析中最重要的工具之一:pandas

二、Pandas I

原文:Pandas I

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 建立对pandaspandas语法的熟悉度。

  • 学习关键数据结构:DataFrameSeriesIndex

  • 了解提取数据的方法:.loc.iloc[]

在这一系列讲座中,我们将让您直接探索和操纵真实世界的数据。我们将首先介绍pandas,这是一个流行的 Python 库,用于与表格数据交互。

2.1 表格数据

数据科学家使用各种格式存储的数据。本课程的主要重点是理解表格数据——存储在表格中的数据。

表格数据是数据科学家用来组织数据的最常见系统之一。这在很大程度上是因为表格的简单性和灵活性。表格允许我们将每个观察,或者从个体收集数据的实例,表示为其自己的。我们可以将每个观察的不同特征,或者特征,记录在单独的中。

为了看到这一点,我们将探索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)。

2.2 SeriesDataFrame和索引

要开始我们在pandas中的工作,我们必须首先将库导入到我们的 Python 环境中。这将允许我们在我们的代码中使用pandas数据结构和方法。

# `pd` is the conventional alias for Pandas, as `np` is for NumPy
import pandas as pd

pandas中有三种基本数据结构:

  1. Series:1D 带标签的数组数据;最好将其视为列数据。

  2. DataFrame:带有行和列的 2D 表格数据。

  3. 索引:一系列行/列标签。

DataFrameSeries和索引可以在以下图表中以可视化方式表示,该图表考虑了elections数据集的前几行。

注意DataFrame是一个二维对象——它包含行和列。上面的Series是这个DataFrame的一个单独的列,即Result列。两者都包含一个索引,或者共享的行标签列表(从 0 到 4 的整数,包括 0)。

2.2.1 系列

Series 表示DataFrame的一列;更一般地,它可以是任何 1 维类似数组的对象。它包含:

  • 相同类型的序列。

  • 索引称为数据标签的序列。

在下面的单元格中,我们创建了一个名为sSeries

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')

2.2.1.1 Series中的选择

就像在使用NumPy数组时一样,我们可以从Series中选择单个值或一组值。为此,有三种主要方法:

  1. 单个标签。

  2. 标签列表。

  3. 过滤条件。

为了证明这一点,让我们定义ser系列。

ser = pd.Series([4, -2, 0, 6], index = ["a", "b", "c", "d"])
ser
a    4
b   -2
c    0
d    6
dtype: int64
2.2.1.1.1 单个标签
ser["a"] # We return the value stored at the Index label "a"
4
2.2.1.1.2 标签列表
ser[["a", "c"]] # We return a *Series* of the values stored at the Index labels "a" and "c"
a    4
c    0
dtype: int64
2.2.1.1.3 过滤条件

也许从Series中选择数据的最有趣(和有用)的方法是使用过滤条件。

首先,我们对Series应用布尔运算。这将创建一个新的布尔值系列

ser > 0 # Filter condition: select all elements greater than 0
a     True
b    False
c    False
d     True
dtype: bool

然后,我们使用这个布尔条件来索引我们原始的Seriespandas将只选择原始Series中满足条件的条目。

ser[ser > 0] 
a    4
d    6
dtype: int64

2.2.2 数据框

通常,我们将使用Series的角度来处理它们,认为它们是DataFrame中的列。我们可以将DataFrame视为所有共享相同索引Series的集合。

在 Data 8 中,您遇到了datascience库的Table类,它表示表格数据。在 Data 100 中,我们将使用pandas库的DataFrame类。

2.2.2.1 创建DataFrame

有许多创建DataFrame的方法。在这里,我们将介绍最流行的方法:

  1. 从 CSV 文件中。

  2. 使用列名和列表。

  3. 从字典中。

  4. Series中。

更一般地,创建DataFrame的语法是:pandas.DataFrame(data, index, columns)

2.2.2.1.1 从 CSV 文件中

在 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 列(“年份”,“候选人”,“党派”,“普选票”,“结果”,“%”)。每一行代表一条记录——在我们的例子中,是某一年的总统候选人。每一列代表记录的一个属性或特征。

2.2.2.1.2 使用列表和列名

我们现在将探讨如何使用我们自己的数据创建“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
2.2.2.1.3 从字典

第三种(更常见的)创建“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
2.2.2.1.4 从“Series”

早些时候,我们解释了“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

2.2.3 索引

在技术上,索引不一定是整数,也不一定是唯一的。例如,我们可以将“选举”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)

还需要注意的是,构成索引的行标签不一定是唯一的。虽然索引值可以是唯一的和数字的,充当行号,但它们也可以是命名的和非唯一的。

这里我们看到唯一和数字的索引值。

然而,这里的索引值是非唯一的。

2.3 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)

2.4 DataFrame中的切片

现在我们已经更多地了解了DataFrame,让我们深入了解它们的功能。

DataFrame类的 API(应用程序编程接口)是庞大的。在本节中,我们将讨论DataFrame API 的几种方法,这些方法允许我们提取数据子集。

操作DataFrame最简单的方法是提取行和列的子集,称为切片

我们可能希望提取数据的常见方式包括:

  • DataFrame中的第一行或最后一行。

  • 具有特定标签的数据。

  • 特定位置的数据。

我们将使用 DataFrame 类的四种主要方法:

  1. .head.tail

  2. .loc

  3. .iloc

  4. []

2.4.1 使用.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

2.4.2 基于标签的提取:使用.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 切片表示法。在这里,我们选择从标签03的行和从标签"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

2.4.3 基于整数的提取:使用.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^{th}\)(或者说第一个位置)上。通常情况下,任何 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 提取。

2.4.4 上下文相关的提取:使用[]进行索引

[]选择运算符是最令人困惑的,但也是最常用的。它只接受一个参数,可以是以下之一:

  1. 一系列行号。

  2. 一系列列标签。

  3. 单列标签。

也就是说,[]上下文相关的。让我们看一些例子。

2.4.4.1 一系列行号

假设我们想要我们的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

2.4.4.2 一系列列标签

假设我们现在想要前四列。

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 列

2.4.4.3 单列标签

最后,[]允许我们仅提取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更常见,特别是因为它更加简洁。

2.5 结语

pandas库非常庞大,包含许多有用的函数。这是一个指向文档的链接。我们当然不指望您记住库中的每一个方法。

入门级的 Data 100 pandas 讲座将提供对关键数据结构和方法的高层次视图,这些将构成您pandas知识的基础。本课程的目标是帮助您建立对真实世界编程实践的熟悉度……谷歌搜索!您的问题的答案可以在文档、Stack Overflow 等地方找到。能够搜索、阅读和实施文档是任何数据科学家的重要生活技能。

有了这个,我们将继续学习 Pandas II。

三、Pandas II

原文:Pandas II

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 继续熟悉pandas语法。

  • 使用条件选择从DataFrame中提取数据。

  • 识别聚合有用的情况,并确定执行聚合的正确技术。

上次,我们介绍了pandas库作为处理数据的工具包。我们学习了DataFrameSeries数据结构,熟悉了操作表格数据的基本语法,并开始编写我们的第一行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

3.1 条件选择

条件选择允许我们选择满足某些指定条件的DataFrame中的行的子集。

要了解如何使用条件选择,我们必须看一下.loc[]方法的另一个可能的输入 - 布尔数组,它只是一个数组或Series,其中每个元素都是TrueFalse。这个布尔数组的长度必须等于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中为每一行列出TrueFalse可能会有多么乏味。为了简化事情,我们可以提供一个逻辑条件作为.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

3.2 添加、删除和修改列

在许多数据科学任务中,我们可能需要以某种方式更改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

请注意,我们重新分配babynamesbabynames.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

3.3 实用程序函数

pandas包含大量的函数库,可以帮助缩短设置和从其数据结构中获取信息的过程。在接下来的部分中,我们将概述每个主要实用程序函数,这些函数将帮助我们在 Data 100 中使用。

讨论pandas提供的所有功能可能需要一个学期的时间!我们将带领您了解最常用的功能,并鼓励您自行探索和实验。

  • NumPy和内置函数支持

  • .shape

  • .size

  • .describe()

  • .sample()

  • .value_counts()

  • .unique()

  • .sort_values()

pandas 文档将是 Data 100 及以后的宝贵资源。

3.3.1 NumPy

pandas旨在与您在Data 8中遇到的数组计算框架NumPy良好配合。几乎任何NumPy函数都可以应用于pandasDataFrameSeries

# 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

3.3.2 .shape.size

.shape.sizeSeriesDataFrame的属性,用于测量结构中存储的数据的“数量”。调用.shape返回一个元组,其中包含DataFrameSeries中存在的行数和列数。.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

3.3.3 .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

3.3.4 .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

3.3.5 .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

3.3.6 .unique()

如果我们有一个具有许多重复值的Series,那么.unique() 可以用于仅识别唯一值。在这里,我们返回babynames中所有名称的数组。

babynames["Name"].unique()
array(['Mary', 'Helen', 'Dorothy', ..., 'Zae', 'Zai', 'Zayvier'],
      dtype=object)

3.3.7 .sort_values()

DataFrame进行排序可以用于隔离极端值。例如,按降序排序的行的前 5 个条目(即从最高到最低)是最大的 5 个值。.sort_values 允许我们按指定列对DataFrameSeries进行排序。我们可以选择按升序(默认)或降序的顺序接收行。

# 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

3.4 自定义排序

现在让我们尝试应用我们刚刚学到的知识来解决一个排序问题,使用不同的方法。假设我们想要找到最长的婴儿名字,并相应地对我们的数据进行排序。

3.4.1 方法 1:创建一个临时列

其中一种方法是首先创建一个包含名字长度的列。

# 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

3.4.2 方法 2:使用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

3.4.3 方法 3:使用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

3.5 使用.groupby聚合数据

直到这一点,我们一直在处理DataFrame的单个行。作为数据科学家,我们经常希望调查我们数据的更大子集中的趋势。例如,我们可能希望计算我们DataFrame中一组行的一些摘要统计(均值、中位数、总和等)。为此,我们将使用pandasGroupBy对象。

假设我们想要聚合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,原始babynamesDataFrame 中每个唯一年份都有一行。

也许你会想:"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 操作——如summaxmin——会被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.meannp.maxnp.minnp.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

我们还可以定义自己的聚合函数!这可以使用deflambda语句来完成。同样,自定义聚合函数的条件是它必须接受一个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 列

3.6 结语

操纵DataFrames不是一天就能掌握的技能。由于pandas的灵活性,有许多不同的方法可以从 A 点到 B 点。我们建议尝试多种不同的方法来解决同一个问题,以获得更多的练习并更快地达到精通的水平。

接下来,我们将开始深入挖掘数据分组背后的机制。**

四、Pandas III

原文:Pandas III

译者:飞龙

协议:CC BY-NC-SA 4.0

学习成果

  • 使用.groupby()执行高级聚合

  • 使用pd.pivot_table方法构建一个数据透视表

  • 使用pd.merge()在 DataFrame 之间执行简单的合并

上次,我们介绍了数据聚合的概念 - 我们熟悉了GroupBy对象,并将它们用作汇总和总结 DataFrame 的工具。在本讲座中,我们将探讨使用不同的聚合函数以及深入研究一些高级的.groupby方法,以展示它们在理解我们的数据方面有多么强大。我们还将介绍其他数据聚合技术,以提供在如何操作我们的表格方面的灵活性。

4.1 重新审视.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

这里有一个过程的示例:

aggregation

现在让我们深入研究groupby。正如我们在上一堂课中学到的,groupby操作涉及将 DataFrame 拆分为分组的子框架,应用函数,并组合结果的某种组合。

对于下面的任意 DataFrame df,代码df.groupby("year").agg(sum)执行以下操作:

  • DataFrame拆分为属于同一年份的子DataFrame

  • sum函数应用到每个子DataFrame的每一列。

  • sum的结果组合成一个由year索引的单个DataFrame

groupby_demo

4.1.1 聚合函数

可以应用许多不同的聚合函数到分组的数据上。.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 中也会对你有所帮助。

4.1.2 烦人的列

请注意,你必须小心选择哪些列应用.agg()函数。如果我们尝试通过f_babynames.groupby("Name").agg(ratio_to_peak)对整个表应用我们的函数,执行.agg()调用将导致TypeError

错误

我们可以通过在调用.agg()之前显式选择要应用聚合函数的列来避免这个问题(并防止无意中丢失数据),

4.1.3 分组后重命名列

默认情况下,.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 列

4.1.4 一些数据科学回报

通过对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参数来避免我们之前在聚合非数字列时遇到的错误。

4.1.5 绘制出生计数

绘制Dataframe后,我们得到了一个有趣的故事。

代码

import plotly.express as px
puzzle2 = babynames.groupby("Year")[["Count"]].agg(sum)
px.line(puzzle2, y = "Count")

警告: 当我们决定使用这个数据集来估计出生率时,我们做出了一个巨大的假设。根据来自立法分析办公室的这篇文章,2020 年加利福尼亚州出生的婴儿实际数量为 421,275。然而,我们的图表显示 362,882 个婴儿 - 发生了什么?

4.2 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

4.2.1 原始GroupBy对象

应用于DataFramegroupby的结果是一个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

4.2.2 其他GroupBy方法

有许多聚合方法可以使用.agg。一些有用的选项是:

  • .mean:创建一个新的DataFrame,其中包含每个组的平均值

  • .sum:创建一个新的DataFrame,其中包含每个组的总和

  • .max.min:创建一个新的DataFrame,其中包含每个组的最大/最小值

  • .first.last:创建一个新的DataFrame,其中包含每个组的第一行/最后一行

  • .size:创建一个新的Series,其中包含每个组的条目数

  • .count:创建一个新的DataFrame,其中包含条目数,不包括缺失值。

让我们通过创建一个名为dfDataFrame来举例说明一些例子。

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文档中查看它们。

4.2.3 按组进行过滤

GroupBy 对象的另一个常见用途是按组过滤数据。

groupby.filter 接受一个参数 func,其中 func 是一个函数,它:

  • DataFrame 对象作为输入

  • 返回每个子 DataFrame 的单个 TrueFalse

返回对应于 True 的子 DataFrame,而具有 False 值的则不返回。重要的是,groupby.filtergroupby.agg 不同,因为最终的 DataFrame 中返回的是整个DataFrame,而不仅仅是单行。因此,groupby.filter 保留了原始索引。

groupby_demo

为了说明这是如何发生的,让我们回到 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 在整个组的所有行上应用布尔条件。如果该组中并非所有行都满足过滤器指定的条件,则整个组将在输出中被丢弃。

4.2.4 使用 lambda 函数进行聚合

如果我们希望使用非标准函数(例如我们自己设计的函数)对我们的DataFrame进行聚合,我们可以通过将.agglambda表达式结合使用来实现。

让我们首先考虑一个谜题来唤起我们的记忆。我们将尝试找到每个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

相反,让我们尝试一种不同的方法。我们将:

  1. 对数据框进行排序,使行按%的降序排列

  2. 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

以下是该过程的示例:

groupby_demo

请注意,我们的代码正确确定了来自民主党的林登·约翰逊拥有最高的选民%

更一般地,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

4.3 使用数据透视表聚合数据

我们现在知道.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中看到过这些。一组值用于创建数据透视表的索引;另一组用于定义列名。表中每个单元格中包含的值对应于每个索引-列对的聚合数据。

这是一个过程的示例:

groupby_demo

理解数据透视表的最佳方法是看它的实际应用。让我们回到我们最初的目标,即对每个年份和性别组合的名字总数进行求和。我们将调用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

4.4 连接表

在进行数据科学项目时,我们不太可能在单个“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

让我们更仔细地看看这些参数:

  • leftright参数用于指定要连接的数据框。

  • left_onright_on参数被分配给要在执行连接时使用的列的字符串名称。这两个on参数告诉pandas应该将哪些值作为配对键来确定要在数据框之间合并的行。我们将在下一堂课上更多地讨论这个配对键的概念。

4.5 结语

恭喜!我们终于解决了pandas。如果你对它仍然感到不太舒服,不要担心——在接下来的几周里,你将有足够的机会练习。

接下来,我们将动手处理一些真实世界的数据集,并利用我们的pandas知识进行一些探索性数据分析。

五、数据清洗和探索性数据分析

原文:Data Cleaning and EDA

译者:飞龙

协议:CC BY-NC-SA 4.0

代码

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 时要考虑的数据的关键属性。在这个过程中,我们将为您制定一个“清单”,以便在处理新数据集时考虑。通过这个过程,我们将更深入地了解数据科学生命周期的这个早期阶段(但非常重要!)。

5.1 结构

5.1.1 文件格式

有许多用于存储结构化数据的文件类型:TSV、JSON、XML、ASCII、SAS 等。在讲座中,我们只会涵盖 CSV、TSV 和 JSON,但在处理不同数据集时,您可能会遇到其他格式。阅读文档是了解如何处理多种不同文件类型的最佳方法。

5.1.1.1 CSV

CSV,代表逗号分隔值,是一种常见的表格数据格式。在过去的两堂pandas讲座中,我们简要涉及了文件格式的概念:数据在文件中的编码方式。具体来说,我们的electionsbabynames数据集是以 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分隔。数据中的每一列,或字段,由逗号,分隔(因此是逗号分隔的!)。

5.1.1.2 TSV

另一种常见的文件类型是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参数

5.1.1.3 JSON

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
5.1.1.3.1 使用 JSON 进行 EDA:伯克利 COVID-19 数据

伯克利市政府开放数据网站有一个关于伯克利居民 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')
5.1.1.3.1.1 文件大小

让我们通过对数据集的大小进行粗略估计来确定我们用于查看数据的工具。对于相对较小的数据集,我们可以使用文本编辑器或电子表格。对于较大的数据集,更多的编程探索或分布式计算工具可能更合适。在这里,我们将使用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.
5.1.1.3.1.2 Unix Commands

作为 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
5.1.1.3.1.3 文件内容

让我们使用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 命令(这也是pandashead方法的来源!)来查看文件的前几行:

!head -5 {covid_file}
{
  "meta" : {
    "view" : {
      "id" : "xn6j-b766",
      "name" : "COVID-19 Confirmed Cases",

为了将 JSON 文件加载到pandas中,让我们首先使用Pythonjson包进行一些 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.
5.1.1.3.1.4 检查记录的数据字段

我们可以查看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
5.1.1.3.1.5 探索 JSON 文件的总结
  1. 上述元数据告诉我们很多关于数据中的列,包括列名、潜在的数据异常和基本统计信息。

  2. 由于其非表格结构,JSON 比 CSV 更容易创建自描述数据,这意味着数据的信息存储在与数据相同的文件中。

  3. 自描述数据可能会有所帮助,因为它保留了自己的描述,并且这些描述更有可能随着数据的变化而更新。

5.1.1.3.1.6 将 COVID 数据加载到pandas

最后,让我们将数据(而不是元数据)加载到pandasDataFrame中。在下面的代码块中,我们:

  1. 将 JSON 记录翻译成DataFrame

    • 字段:covid_json['meta']['view']['columns']

    • 记录:covid_json['data']

  2. 删除没有元数据描述的列。一般来说,这是一个坏主意,但在这里我们删除这些列,因为上面的分析表明它们不太可能包含有用的信息。

  3. 检查表的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

5.1.2 变量类型

将数据加载到文件后,花时间了解数据集中编码的信息是一个好主意。特别是,我们想要确定我们的数据中存在哪些变量类型。广义上说,我们可以将变量分类为两种主要类型之一。

定量变量描述一些数值数量或量。我们可以进一步将定量数据分为:

  • 连续定量变量:可以在连续尺度上以任意精度测量的数值数据。连续变量没有严格的可能值集 - 它们可以记录到任意数量的小数位。例如,重量、GPA 或 CO[2]浓度。

  • 离散定量变量:只能取有限可能值的数值数据。例如,某人的年龄或他们的兄弟姐妹数量。

定性变量,也称为分类变量,描述的是不测量某种数量或量的数据。分类数据的子类别包括:

  • 有序定性变量:具有有序级别的类别。具体来说,有序变量是指级别之间的差异没有一致的、可量化的含义。一些例子包括教育水平(高中、本科、研究生等)、收入档次(低、中、高)或 Yelp 评分。

  • 无序定性变量:没有特定顺序的类别。例如,某人的政治立场或 Cal ID 号码。

变量类型的分类

请注意,许多变量不会完全属于这些类别中的一个。定性变量可能具有数值级别,反之亦然,定量变量可以存储为字符串。

5.1.3 主键和外键

上次,我们介绍了.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

外键是表中引用其他表主键的列或列集。在分配.mergeleft_onright_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

5.2 粒度、范围和时间性

在了解数据集的结构之后,下一个任务是确定数据究竟代表什么。我们将通过考虑数据的粒度、范围和时间性来做到这一点。

5.2.1 粒度

数据集的粒度是单行代表的内容。您也可以将其视为数据中包含的细节级别。要确定数据的粒度,可以问:数据集中的每一行代表什么?细粒度数据包含大量细节,单行代表一个小的个体单位。例如,每条记录可能代表一个人。粗粒度数据被编码,以便单行代表一个大的个体单位-例如,每条记录可能代表一组人。

5.2.2 范围

数据集的范围是数据所涵盖的人口子集。如果我们调查数据科学课程中学生的表现,一个范围较窄的数据集可能包括所有注册 Data 100 课程的学生,而一个范围较广的数据集可能包括加利福尼亚州的所有学生。

5.2.3 时间性

数据集的时间性描述了数据收集的周期性,以及数据最近收集或更新的时间。

数据集的时间和日期字段可能代表一些内容:

  1. “事件”发生的时间

  2. 数据收集的时间,或者数据输入系统的时间

  3. 数据复制到数据库中的时间

为了充分了解数据的时间性,还可能需要标准化时区或检查数据中的重复时间趋势(模式是否在 24 小时内重复?一个月内?季节性?)。标准化时间的惯例是协调世界时(UTC),这是一个国际时间标准,在 0 度纬度上测量,整年保持一致(没有夏令时)。我们可以表示伯克利的时区,太平洋标准时间(PST),为 UTC-7(夏令时)。

5.2.3.1 使用pandasdt访问器进行时间处理

让我们简要地看一下如何使用pandasdt访问器来处理数据集中的日期/时间,使用你在实验 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

看起来有三列带有日期/时间:EVENTDTEVENTTMInDbDate

很可能,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访问器时间序列/日期功能的文档。

5.3 忠实度

在数据清理和 EDA 工作流的这个阶段,我们已经取得了相当大的成就:我们已经确定了数据的结构,了解了它所编码的信息,并获得了有关它是如何生成的见解。在整个过程中,我们应该始终记住数据科学工作的最初目的 - 使用数据更好地理解和建模现实世界。为了实现这一目标,我们需要确保我们使用的数据忠实于现实;也就是说,我们的数据准确地捕捉了“真实世界”。

用于研究或工业的数据通常是“混乱的” - 可能存在影响数据集忠实度的错误或不准确性。数据可能不忠实的迹象包括:

  • 不切实际或“错误”的值,例如负计数、不存在的位置或设置在未来的日期

  • 违反明显依赖关系的迹象,例如年龄与生日不匹配

  • 明显表明数据是手工输入的迹象,这可能导致拼写错误或字段错误移位

  • 数据伪造的迹象,例如虚假的电子邮件地址或重复使用相同的名称

  • 包含相同信息的重复记录或字段

  • 截断数据,例如 Microsoft Excel 将行数限制为 655536,列数限制为 255

我们通常通过以下方式解决一些更常见的问题:

  • 拼写错误:应用更正或删除不在字典中的记录

  • 时区不一致:转换为通用时区(例如 UTC)

  • 重复的记录或字段:识别和消除重复项(使用主键)

  • 未指定或不一致的单位:推断单位并检查数据中的值是否在合理范围内

5.3.1 缺失值

现实世界数据集经常遇到的另一个常见问题是缺失数据。解决这个问题的一种策略是从数据集中简单地删除任何具有缺失值的记录。然而,这会引入引入偏见的风险 - 缺失或损坏的记录可能与数据中感兴趣的某些特征有系统关联。另一个解决方案是将数据保留为NaN值。

解决缺失数据的第三种方法是执行插补:使用数据集中的其他数据推断缺失值。可以实施各种插补技术;以下是一些最常见的插补技术。

  • 平均插补:用该字段的平均值替换缺失值

  • 热卡插补:用某个随机值替换缺失值

  • 回归插补:开发模型以预测缺失值

  • 多重插补:用多个随机值替换缺失值

无论使用何种策略来处理缺失数据,我们都应该仔细考虑为什么特定记录或字段可能丢失 - 这可以帮助确定这些值的缺失是否重要或有意义。

6 EDA 演示 1:美国的结核病

现在,让我们走一遍数据清理和 EDA 工作流程,看看我们能从美国的结核病情况中学到什么!

我们将检查2021 年发表的原始 CDC 文章中包含的数据。

6.1 CSV 和字段名称

假设表 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

6.2 记录粒度

你可能已经在想:第一条记录怎么了?

第 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

6.3 收集人口普查数据

美国人口普查人口估计来源(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

有时,您会想要修改导入的代码。要重新导入这些修改,您可以使用pythonimportlib库:

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

6.4 合并数据(合并“DataFrame”)

时间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

6.5 再现数据:计算发病率

让我们重新计算发病率,以确保我们知道原始 CDC 数字来自何处。

根据疾病控制和预防中心的报告:TB 发病率计算为“使用美国人口普查局的中期人口估计,每 10 万人的病例数”。

如果我们将一个群体定义为 10 万人,那么我们可以计算给定州人口的 TB 发病率为

\[\text{TB 发病率} = \frac{\text{人口中的 TB 病例}}{\text{人口中的群体}} = \frac{\text{人口中的 TB 病例}}{\text{人口}/100000} \]

\[= \frac{\text{人口中的 TB 病例}}{\text{人口}} \times 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

6.6 奖励 EDA:重现报告的统计数据

我们如何重现原始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\)\(B\)的百分比变化计算公式为\(\text{percent change} = \frac{B - A}{A} \times 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

7 EDA 演示 2:毛纳罗亚 CO[2]数据 - 数据忠实度的一课

毛纳罗亚观测站 自 1958 年以来一直在监测二氧化碳浓度

co2_file = "data/co2_mm_mlo.txt"

让我们做一些EDA

7.1 将此文件读入 Pandas?

让我们来看看这个.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将数据读入pandasDataFrame,并提供几个参数来指定分隔符是空格,没有标题(我们将设置自己的列名),并跳过文件的前 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。

7.2 探索变量特征类型

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

7.3 可视化 CO[2]

科学研究往往具有非常干净的数据,对吧…?让我们立即制作二氧化碳月均值的时间序列图。

代码

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

我们该如何解决这个问题?首先,让我们探索数据的其他方面。了解我们的数据将帮助我们决定如何处理缺失值。

7.4 合理性检查:对数据进行推理

首先,我们考虑数据的形状。我们应该有多少行?

  • 如果按时间顺序,我们应该每个月有一条记录。

  • 数据从 1958 年 3 月到 2019 年 8 月。

  • 我们应该有$ 12 (2019-1957) - 2 - 4 = 738 $条记录。

co2.shape
(738, 7)

太好了!行数(即记录)与我们的预期相匹配。

现在让我们检查每个特征的质量。

7.5 理解缺失值 1: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 年代中后期可能出现了设备问题。

潜在的下一步

  • 通过有关历史读数的文档来确认这些解释。

  • 也许删除最早的记录?但是,在我们检查时间趋势并评估是否存在潜在问题之后,我们会推迟这样的行动。

7.6 理解缺失值 2: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数据。

7.7 删除、NaN或填补缺失的Avg数据?

我们应该如何处理无效的Avg数据?

  1. 删除记录

  2. 设置为 NaN

  3. 使用某种策略填补

记住我们想要修复以下的图表:

代码

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 网站上看到的差不多!

7.8 展示数据:关于数据粒度的讨论

从描述:

  • 月度测量是平均每日测量的平均值。

  • 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。

8 总结

我们在本讲座中涵盖了很多内容;让我们总结一下最重要的要点:

8.1 处理缺失值

我们可以采取几种方法来处理缺失数据:

  • 删除缺失记录

  • 保留NaN缺失值

  • 使用插值列进行插补

8.2 探索性数据分析和数据整理

有几种方法可以处理探索性数据分析和数据整理:

  • 分析数据和元数据:数据的日期、大小、组织和结构是什么?

  • 逐个检查每个字段/属性/维度

  • 逐对相关维度进行检查(例如,按专业分解等级)。

  • 在这个过程中,我们可以:

    • 可视化或总结数据。

    • 验证关于数据及其收集过程的假设。特别注意数据收集的时间。

    • 识别和解决异常

    • 应用数据转换和校正(我们将在即将到来的讲座中介绍)。

    • 记录你所做的一切!在 Jupyter Notebook 中开发可以促进你自己工作的可重复性