前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UCB Data100:数据科学的原理和技巧:第一章到第五章

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

作者头像
ApacheCN_飞龙
发布2024-01-13 10:01:32
6790
发布2024-01-13 10:01:32
举报
文章被收录于专栏:信数据得永生

一、引言

原文:Introduction 译者:飞龙 协议:CC BY-NC-SA 4.0

学习成果

  • 了解 Data 100 的总体目标
  • 了解数据科学生命周期的阶段

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

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

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

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

  • 了解世界(科学)。
  • 解决问题(工程)。

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

课程目标

  • 为您准备伯克利高级课程,包括数据管理、机器学习和统计学
  • 使您能够在数据科学领域开展职业生涯
  • 使您能够通过计算和推理思维解决现实世界的问题

我们将涵盖的一些主题

  • Pandas 和 NumPy
  • 探索性数据分析
  • 正则表达式
  • 可视化
  • 抽样
  • 模型设计和损失公式
  • 线性回归
  • 梯度下降
  • 逻辑回归
  • 还有更多!

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

1.1 数据科学生命周期

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

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

1.1.1 提出问题

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

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

  • 我们想要知道什么?
    • 一个过于模糊的问题可能会导致混乱。
  • 我们试图解决什么问题?
    • 问一个问题的目标应该是清晰的,以便为利益相关者的努力提供合理的理由。
  • 我们想要测试的假设是什么?
    • 这为我们提供了一个清晰的视角,以分析最终结果。
  • 我们的成功指标是什么?
    • 这为我们建立了一个明确的观点,知道何时结束项目。
1.1.2 获取数据

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

  • 我们有什么数据,我们需要什么数据?
    • 定义数据的单位(人、城市、时间点等)和要测量的特征。
  • 我们如何取样更多的数据?
    • 抓取网页,手动收集,进行实验等。
  • 我们的数据是否代表我们想研究的人群?
    • 如果我们的数据不代表我们感兴趣的人群,那么我们可能得出错误的结论。

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

1.1.3 理解数据

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

  • 我们的数据是如何组织的,它包含了什么?
    • 了解数据对世界有何影响有助于我们更好地理解世界。
  • 我们有相关的数据吗?
    • 如果我们收集的数据对于手头的问题没有用处,那么我们必须收集更多的数据。
  • 数据中存在什么偏见、异常或其他问题?
    • 如果忽视这些问题,可能会导致许多错误的结论,因此数据科学家必须始终注意这些问题。
  • 我们如何转换数据以进行有效分析?
    • 数据并不总是一眼就容易解释的,因此数据科学家应该努力揭示隐藏的见解。

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

1.1.4 理解世界

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

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

  • 数据对世界有何影响?
    • 根据我们的模型,数据将引导我们对真实世界的某些结论。
  • 它是否回答了我们的问题或准确解决了问题?
    • 如果我们的模型和数据不能实现我们的目标,那么我们必须改革我们的问题、模型,或者两者兼而有之。
  • 我们的结论有多可靠,我们能相信这些预测吗?
    • 不准确的模型可能导致错误的结论。

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

1.2 结论

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

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

二、Pandas I

原文:Pandas I 译者:飞龙 协议:CC BY-NC-SA 4.0

学习成果

  • 建立对pandaspandas语法的熟悉度。
  • 学习关键数据结构:DataFrameSeriesIndex
  • 了解提取数据的方法:.loc.iloc[]

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

2.1 表格数据

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

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

为了看到这一点,我们将探索elections数据集,该数据集存储了以前年份竞选美国总统的政治候选人的信息。

代码

代码语言:javascript
复制
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数据结构和方法。

代码语言:javascript
复制
# `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

代码语言:javascript
复制
s = pd.Series(["welcome", "to", "data 100"])
s
代码语言:javascript
复制
0     welcome
1          to
2    data 100
dtype: object
代码语言:javascript
复制
s.values # Data values contained within the Series
代码语言:javascript
复制
array(['welcome', 'to', 'data 100'], dtype=object)
代码语言:javascript
复制
s.index # The Index of the Series
代码语言:javascript
复制
RangeIndex(start=0, stop=3, step=1)

默认情况下,Series 的索引是从 0 开始的整数的顺序列表。可以将所需索引的手动指定列表传递给index参数。

代码语言:javascript
复制
s = pd.Series([-1, 10, 2], index = ["a", "b", "c"])
s
代码语言:javascript
复制
a    -1
b    10
c     2
dtype: int64
代码语言:javascript
复制
s.index
代码语言:javascript
复制
Index(['a', 'b', 'c'], dtype='object')

初始化后也可以更改索引。

代码语言:javascript
复制
s.index = ["first", "second", "third"]
s
代码语言:javascript
复制
first     -1
second    10
third      2
dtype: int64
代码语言:javascript
复制
s.index
代码语言:javascript
复制
Index(['first', 'second', 'third'], dtype='object')
2.2.1.1 Series中的选择

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

  1. 单个标签。
  2. 标签列表。
  3. 过滤条件。

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

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

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

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

代码语言:javascript
复制
ser > 0 # Filter condition: select all elements greater than 0
代码语言:javascript
复制
a     True
b    False
c    False
d     True
dtype: bool

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

代码语言:javascript
复制
ser[ser > 0] 
代码语言:javascript
复制
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数据集。

代码语言:javascript
复制
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”——每个嵌套列表代表一行数据。

代码语言:javascript
复制
df_list = pd.DataFrame([1, 2, 3], columns=["Numbers"])
df_list

Numbers

0

1

1

2

2

3

代码语言:javascript
复制
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”的行。

代码语言:javascript
复制
df_dict = pd.DataFrame({"Fruit": ["Strawberry", "Orange"], "Price": [5.49, 3.99]})
df_dict

Fruit

Price

0

Strawberry

5.49

1

Orange

3.99

代码语言:javascript
复制
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”。

代码语言:javascript
复制
# 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

代码语言:javascript
复制
pd.DataFrame(s_a)

0

r1

a1

r2

a2

r3

a3

代码语言:javascript
复制
s_a.to_frame()

0

r1

a1

r2

a2

r3

a3

2.2.3 索引

在技术上,索引不一定是整数,也不一定是唯一的。例如,我们可以将“选举”DataFrame 的索引设置为总统候选人的名字。

代码语言:javascript
复制
# 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 的索引设置为候选人的党派。

代码语言:javascript
复制
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 列

如果需要,我们可以将索引恢复为默认的整数列表。

代码语言:javascript
复制
# This resets the index to be the default list of integer
elections.reset_index(inplace=True) 
elections.index
代码语言:javascript
复制
RangeIndex(start=0, stop=182, step=1)

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

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

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

2.3 DataFrame属性:索引、列和形状

另一方面,DataFrame中的列名几乎总是唯一的。回顾elections数据集,有两列命名为“Candidate”是没有意义的。

有时,您可能希望提取这些不同的值,特别是行和列标签的列表。

对于索引/行标签,请使用DataFrame.index

代码语言:javascript
复制
elections.set_index("Party", inplace = True)
elections.index
代码语言:javascript
复制
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

代码语言:javascript
复制
elections.columns
代码语言:javascript
复制
Index(['index', 'Candidate', 'Year', 'Popular vote', 'Result', '%'], dtype='object')

对于 DataFrame 的形状,我们可以使用DataFrame.shape

代码语言:javascript
复制
elections.shape
代码语言:javascript
复制
(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)

代码语言:javascript
复制
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行。

代码语言:javascript
复制
# 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的列。

代码语言:javascript
复制
elections.loc[0, 'Candidate']
代码语言:javascript
复制
'Andrew Jackson'

请记住,只传入一个参数作为单个值将产生一个Series。下面,我们提取了"Popular vote"列的子集作为Series

代码语言:javascript
复制
elections.loc[[87, 25, 179], "Popular vote"]
代码语言:javascript
复制
87     15761254
25       848019
179    74216154
Name: Popular vote, dtype: int64

要选择多个行和列,我们可以使用 Python 切片表示法。在这里,我们选择从标签03的行和从标签"Year""Popular vote"的列。

代码语言:javascript
复制
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 中前四行的所有列值。这时,缩写:就很有用。

代码语言:javascript
复制
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

我们可以使用相同的缩写来提取所有行。

代码语言:javascript
复制
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 中获取多行和多列。

代码语言:javascript
复制
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

最后,我们可以互换列表和切片表示法。

代码语言:javascript
复制
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 中选择第一个总统候选人开始:

代码语言:javascript
复制
# elections.loc[0, "Candidate"] - Previous approach
elections.iloc[0, 1]
代码语言:javascript
复制
'Andrew Jackson'

请注意,.loc.iloc的第一个参数是相同的。这是因为标签为 0 的行恰好在elections DataFrame 的

0^{th}

(或者说第一个位置)上。通常情况下,任何 DataFrame 中的行标签都是从 0 开始递增的,这一点是正确的。

并且,就像以前一样,如果我们只传入一个单一的值参数,我们的结果将是一个Series

代码语言:javascript
复制
elections.iloc[[1,2,3],1]
代码语言:javascript
复制
1    John Quincy Adams
2       Andrew Jackson
3    John Quincy Adams
Name: Candidate, dtype: object

然而,当我们使用.iloc选择前四行和列时,我们注意到了一些东西。

代码语言:javascript
复制
# 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语法的微妙之处之一;通过练习你会习惯的。

列表行为与预期的一样。

代码语言:javascript
复制
#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一起提取所有行或列。

代码语言:javascript
复制
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 的前四行。

代码语言:javascript
复制
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 一系列列标签

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

代码语言:javascript
复制
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列。

代码语言:javascript
复制
elections["Candidate"]
代码语言:javascript
复制
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数据集。

代码

代码语言:javascript
复制
# 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 行中的所有偶数索引行。

代码语言:javascript
复制
# 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执行类似的操作。

代码语言:javascript
复制
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性别相关的所有名称:

代码语言:javascript
复制
# 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

代码

代码语言:javascript
复制
print("There are a total of {} values in 'logical_operator'".format(len(logical_operator)))
代码语言:javascript
复制
There are a total of 407428 values in 'logical_operator'

从第 0 行开始到第 239536 行的行评估为True,因此在DataFrame中返回。从第 239537 行开始的行评估为False,因此在输出中被省略。

代码

代码语言:javascript
复制
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]))
代码语言:javascript
复制
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来实现类似的结果。

代码语言:javascript
复制
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 年之前的名字数据,我们可以写成:

代码语言:javascript
复制
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 年之前的所有名字数据,我们可以写成:

代码语言:javascript
复制
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

布尔数组选择是一个有用的工具,但对于复杂条件可能导致代码过于冗长。在下面的示例中,我们的布尔条件足够长,以至于需要多行代码来编写。

代码语言:javascript
复制
# 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等效的结果。

代码语言:javascript
复制
names = ["Bella", "Alex", "Narges", "Lisa"]
babynames["Name"].isin(names).head()
代码语言:javascript
复制
0    False
1    False
2    False
3    False
4    False
Name: Name, dtype: bool
代码语言:javascript
复制
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中的字符串值是否以特定字符开头。

代码语言:javascript
复制
# Identify whether names begin with the letter "N"
babynames["Name"].str.startswith("N").head()
代码语言:javascript
复制
0    False
1    False
2    False
3    False
4    False
Name: Name, dtype: bool
代码语言:javascript
复制
# 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或数组。

代码语言:javascript
复制
# 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或数组来实现。

代码语言:javascript
复制
# 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()接受一个将旧列名映射到新列名的字典。

代码语言:javascript
复制
# 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将默认假定我们要删除一行。

代码语言:javascript
复制
# 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表。

换句话说,如果我们简单地调用:

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# Pull out the number of babies named Yash each year
yash_count = babynames[babynames["Name"] == "Yash"]["Count"]
yash_count.head()
代码语言:javascript
复制
331824     8
334114     9
336390    11
338773    12
341387    10
Name: Count, dtype: int64
代码语言:javascript
复制
# Average number of babies named Yash each year
np.mean(yash_count)
代码语言:javascript
复制
17.142857142857142
代码语言:javascript
复制
# Max number of babies named Yash born in any one year
np.max(yash_count)
代码语言:javascript
复制
29
3.3.2 .shape.size

.shape.sizeSeriesDataFrame的属性,用于测量结构中存储的数据的“数量”。调用.shape返回一个元组,其中包含DataFrameSeries中存在的行数和列数。.size用于找到结构中元素的总数,相当于行数乘以列数。

许多函数严格要求参数沿着某些轴的维度匹配。调用这些维度查找函数比手动计算所有项目要快得多。

代码语言:javascript
复制
# Return the shape of the DataFrame, in the format (num_rows, num_columns)
babynames.shape
代码语言:javascript
复制
(407428, 5)
代码语言:javascript
复制
# Return the size of the DataFrame, equal to num_rows * num_columns
babynames.size
代码语言:javascript
复制
2037140
3.3.3 .describe()

如果需要从DataFrame中获取许多统计信息(最小值,最大值,平均值等),则可以使用.describe() 一次计算所有这些统计信息。

代码语言:javascript
复制
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(),将报告一组不同的统计信息。

代码语言:javascript
复制
babynames["Sex"].describe()
代码语言:javascript
复制
count     407428
unique         2
top            F
freq      239537
Name: Sex, dtype: object
3.3.4 .sample()

正如我们将在本学期后面看到的,随机过程是许多数据科学技术的核心(例如,训练-测试拆分,自助法和交叉验证)。.sample() 让我们快速选择随机条目(如果从DataFrame调用,则是一行,如果从Series调用,则是一个值)。

默认情况下,.sample() 选择替换的条目。传入参数 replace=True 以进行替换采样。

代码语言:javascript
复制
# Sample a single row
babynames.sample()

State

Sex

Year

Name

Count

119438

CA

F

1991

Madaline

6

当然,这可以与其他方法和运算符(iloc等)链接在一起。

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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

代码语言:javascript
复制
babynames["Name"].value_counts().head()
代码语言:javascript
复制
Jean         223
Francis      221
Guadalupe    218
Jessie       217
Marion       214
Name: Name, dtype: int64
3.3.6 .unique()

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

代码语言:javascript
复制
babynames["Name"].unique()
代码语言:javascript
复制
array(['Mary', 'Helen', 'Dorothy', ..., 'Zae', 'Zai', 'Zayvier'],
      dtype=object)
3.3.7 .sort_values()

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

代码语言:javascript
复制
# 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()时,我们不需要显式指定用于排序的列。我们仍然可以指定排序范式 - 即值是按升序还是降序排序。

代码语言:javascript
复制
# Sort the "Name" Series alphabetically
babynames["Name"].sort_values(ascending=True).head()
代码语言:javascript
复制
366001      Aadan
384005      Aadan
369120      Aadan
398211    Aadarsh
370306      Aaden
Name: Name, dtype: object

3.4 自定义排序

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

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

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

代码语言:javascript
复制
# 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进行排序:

代码语言:javascript
复制
# 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列,以防止我们的表变得混乱。

代码语言:javascript
复制
# 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"值进行排序。

代码语言:javascript
复制
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来帮助我们。

代码语言:javascript
复制
# 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后删除它,以保持一个整洁的表格。

代码语言:javascript
复制
# 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中给定年份的所有行。

代码语言:javascript
复制
babynames.groupby("Year")
代码语言:javascript
复制
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x117197460>

这个奇怪的输出是什么意思?调用.groupby生成了一个GroupBy对象。你可以把它想象成一组“迷你”子数据框,其中每个子框包含与特定年份对应的babynames的所有行。

下面的图表显示了babynames的简化视图,以帮助说明这个想法。

创建一个 GroupBy 对象

我们不能直接使用GroupBy对象——这就是为什么你之前看到了奇怪的输出,而不是DataFrame的标准视图。要实际操作这些“迷你”DataFrame 中的值,我们需要调用聚合方法。这是一种告诉pandas如何聚合GroupBy对象中的值的方法。一旦应用了聚合,pandas将返回一个正常的(现在是分组的)DataFrame

我们将考虑的第一种聚合方法是.agg.agg方法将函数作为其参数;然后将该函数应用于“迷你”分组的每一列 DataFrame。我们最终得到一个新的DataFrame,每个子框架都有一行聚合。

代码语言:javascript
复制
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进行聚合时,我们需要省略这些列。

代码语言:javascript
复制
# 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自动识别。

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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中的合理选择。

代码语言:javascript
复制
# 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”添加一个新列,其中包含每个名字的第一个字母。

代码语言:javascript
复制
# 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”进行聚合

代码语言:javascript
复制
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

代码语言:javascript
复制
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并输出单个标量值。

代码语言:javascript
复制
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 列

代码语言:javascript
复制
# 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数据集开始。请注意,此数据集已经被过滤,只包含来自加利福尼亚州的数据。

代码

代码语言:javascript
复制
# 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)。下面的代码行给出了每年出生的婴儿总数。

代码语言:javascript
复制
babynames.groupby("Year")[["Count"]].agg(sum).head(5)

Count

Year

1910

9163

1911

9983

1912

17946

1913

22094

1914

26926

这里有一个过程的示例:

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

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

  • DataFrame拆分为属于同一年份的子DataFrame
  • sum函数应用到每个子DataFrame的每一列。
  • sum的结果组合成一个由year索引的单个DataFrame
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()

代码语言:javascript
复制
babynames.groupby("Year").mean().head()

现在我们可以将所有这些付诸实践。假设我们想要找出在加利福尼亚州最不受欢迎的“F”性别的婴儿名字。为了计算这个,我们可以首先创建一个指标:“峰值比”(RTP)。RTP 是 2022 年出生具有给定名字的婴儿与任何年份出生具有该名字的最大数量之比。

让我们从计算一个名为“Jennifer”的婴儿开始。

代码语言:javascript
复制
# 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
代码语言:javascript
复制
0.018796372629843364

通过创建一个计算 RTP 并将其应用到我们的DataFrame的函数,我们可以一次轻松计算所有名字的 RTP!

代码语言:javascript
复制
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

代码语言:javascript
复制
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进行排序,我们可以看到受欢迎程度下降最多的名字。

代码语言:javascript
复制
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,让我们看看下面的折线图:

代码

代码语言:javascript
复制
import plotly.express as px
px.line(f_babynames[f_babynames["Name"] == "Debra"], x = "Year", y = "Count")

我们可以得到前 10 个名字的列表,然后用以下代码绘制受欢迎程度:

代码语言:javascript
复制
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"
)

作为一个快速练习,考虑一下什么样的代码可以计算每个名字的婴儿总数。

代码

代码语言:javascript
复制
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

现在,让我们考虑计算每年出生的婴儿总数的代码。你会看到有多种方法可以实现这一点,其中一些列在下面列出。

代码

代码语言:javascript
复制
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后,我们得到了一个有趣的故事。

代码

代码语言:javascript
复制
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。

代码

代码语言:javascript
复制
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

代码语言:javascript
复制
grouped_by_year = elections.groupby("Year")
type(grouped_by_year)
代码语言:javascript
复制
pandas.core.groupby.generic.DataFrameGroupBy

有几种方法可以查看DataFrameGroupBy对象:

代码语言:javascript
复制
grouped_by_party = elections.groupby("Party")
grouped_by_party.groups
代码语言:javascript
复制
{'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]}
代码语言:javascript
复制
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来举例说明一些例子。

代码语言:javascript
复制
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并计算每列中不包括缺失值的条目数。

代码语言:javascript
复制
df.groupby("letter").size()
代码语言:javascript
复制
letter
A    2
B    1
C    3
dtype: int64
代码语言:javascript
复制
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

代码语言:javascript
复制
df["letter"].value_counts()
代码语言:javascript
复制
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 保留了原始索引。

为了说明这是如何发生的,让我们回到 elections 数据集。假设我们想要识别“紧张”的选举年份 - 也就是说,我们想要找到所有对应于那一年的行,其中所有候选人在那一年赢得了相似比例的总票数。具体来说,让我们找到所有对应于没有候选人赢得超过总票数 45%的年份的行。

换句话说,我们想要:

  • 找到最大 % 小于 45% 的年份
  • 返回对应于这些年份的所有 DataFrame

对于每一年,我们需要找到该年所有行中的最大 %。如果这个最大 % 小于 45%,我们将告诉 pandas 保留该年对应的所有行。

代码语言:javascript
复制
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列分组并按最大值聚合。

代码语言:javascript
复制
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的信息。

代码语言:javascript
复制
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

代码语言:javascript
复制
elections_sorted_by_percent.groupby("Party").agg(lambda x : x.iloc[0]).head(10)

# Equivalent to the below code
# elections_sorted_by_percent.groupby("Party").agg('first').head(10)

Year

Candidate

Popular vote

Result

%

Party

American

1856

Millard Fillmore

873053

loss

21.554001

American Independent

1968

George Wallace

9901118

loss

13.571218

Anti-Masonic

1832

William Wirt

100715

loss

7.821583

Anti-Monopoly

1884

Benjamin Butler

134294

loss

1.335838

Citizens

1980

Barry Commoner

233052

loss

0.270182

Communist

1932

William Z. Foster

103307

loss

0.261069

Constitution

2008

Chuck Baldwin

199750

loss

0.152398

Constitutional Union

1860

John Bell

590901

loss

12.639283

Democratic

1964

Lyndon Johnson

43127041

win

61.344703

Democratic-Republican

1824

Andrew Jackson

151271

loss

57.210122

以下是该过程的示例:

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

更一般地,lambda函数用于设计 Python 中未预定义的自定义聚合函数。lambda函数的输入参数x是一个GroupBy对象。因此,lambda x : x.iloc[0]选择每个 groupby 对象中的第一行应该是有意义的。

事实上,解决这个问题有几种不同的方法。每种方法在可读性、性能、内存消耗、复杂性等方面都有不同的权衡。我们在下面给出了一些示例。

注意:不需要理解这些替代解决方案。它们是为了展示pandas中众多问题解决方法的多样性。

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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数据集。在这个问题中,我们将找到与每个年份和性别相关联的婴儿名字的总数。为此,我们将同时"年份""性别"列进行分组。

代码语言:javascript
复制
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

代码语言:javascript
复制
# Find the total number of baby names associated with each sex for each 
# year in the data
babynames.groupby(["Year", "Sex"])[["Count"]].agg(sum).head(6)

Count

Year

Sex

1910

F

5950

M

3213

1911

F

6602

M

3381

1912

F

9804

M

8142

请注意,"年份"和"性别"都作为 DataFrame 的索引(它们都以粗体呈现)。我们创建了一个多索引DataFrame,其中使用两个不同的索引值,年份和性别,来唯一标识每一行。

这不是表示这些数据的最直观的方式 - 而且,因为多索引的 DataFrame 在其索引中有多个维度,它们通常很难使用。

另一种跨两列进行聚合的策略是创建一个数据透视表。你在Data 8中看到过这些。一组值用于创建数据透视表的索引;另一组用于定义列名。表中每个单元格中包含的值对应于每个索引-列对的聚合数据。

这是一个过程的示例:

理解数据透视表的最佳方法是看它的实际应用。让我们回到我们最初的目标,即对每个年份和性别组合的名字总数进行求和。我们将调用pandas.pivot_table方法来创建一个新表。

代码语言:javascript
复制
# 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”的名称计数求和

我们甚至可以在数据透视表的索引或列中包含多个值。

代码语言:javascript
复制
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”数据集。

代码语言:javascript
复制
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”中的相应名字数据连接起来。

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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”方法。

代码语言:javascript
复制
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

代码

代码语言:javascript
复制
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 格式存储和加载的:

代码语言:javascript
复制
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()函数返回带有特殊字符的原始字符串:

代码语言:javascript
复制
with open("data/elections.csv", "r") as table:
 i = 0
 for row in table:
 print(repr(row))
 i += 1
 if i > 3:
 break
代码语言:javascript
复制
'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显示特殊字符。

代码语言:javascript
复制
with open("data/elections.txt", "r") as table:
 i = 0
 for row in table:
 print(repr(row))
 i += 1
 if i > 3:
 break
代码语言:javascript
复制
'\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'来指定分隔符(文档)

代码语言:javascript
复制
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。

代码语言:javascript
复制
with open("data/elections.json", "r") as table:
 i = 0
 for row in table:
 print(row)
 i += 1
 if i > 8:
 break
代码语言:javascript
复制
[

 {

   "Year": 1824,

   "Candidate": "Andrew Jackson",

   "Party": "Democratic-Republican",

   "Popular vote": 151271,

   "Result": "loss",

   "%": 57.21012204

 }, 

可以使用pd.read_json将 JSON 文件加载到pandas中。

代码语言:javascript
复制
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文件中定义了一些辅助函数,我们可以在许多不同的笔记本中重用这些辅助函数。

代码语言:javascript
复制
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
代码语言:javascript
复制
Using cached version that was downloaded (UTC): Fri Aug 18 22:19:42 2023
代码语言:javascript
复制
PosixPath('data/confirmed-cases.json')
5.1.1.3.1.1 文件大小

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

由于似乎存在文本文件,让我们调查一下行数,这通常对应于记录的数量。

代码语言:javascript
复制
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.")
代码语言:javascript
复制
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标志,该标志请求行数而不是单词数。

这两个代码给出了与上面的代码相同的信息,尽管形式略有不同:

代码语言:javascript
复制
!ls -lh {covid_file}
!wc -l {covid_file}
代码语言:javascript
复制
-rw-r--r--  1 Ishani  staff   114K Aug 18 22:19 data/confirmed-cases.json
代码语言:javascript
复制
 1109 data/confirmed-cases.json
5.1.1.3.1.3 文件内容

让我们使用Python来探索数据格式。

代码语言:javascript
复制
with open(covid_file, "r") as f:
 for i, row in enumerate(f):
 print(repr(row)) # print raw strings
 if i >= 4: break
代码语言:javascript
复制
'{\n'
'  "meta" : {\n'
'    "view" : {\n'
'      "id" : "xn6j-b766",\n'
'      "name" : "COVID-19 Confirmed Cases",\n'

我们可以使用head Unix 命令(这也是pandashead方法的来源!)来查看文件的前几行:

代码语言:javascript
复制
!head -5 {covid_file}
代码语言:javascript
复制
{
  "meta" : {
    "view" : {
      "id" : "xn6j-b766",
      "name" : "COVID-19 Confirmed Cases",

为了将 JSON 文件加载到pandas中,让我们首先使用Pythonjson包进行一些 EDA,以了解 JSON 文件的特定结构,以便决定是否(以及如何)将其加载到pandas中。由于 JSON 数据与内部 Python 对象模型非常匹配,Python对 JSON 数据有相对良好的支持。在下面的单元格中,我们使用json包将整个 JSON 数据文件导入 Python 字典。

代码语言:javascript
复制
import json

with open(covid_file, "rb") as f:
 covid_json = json.load(f)

covid_json变量现在是一个编码文件中数据的字典:

代码语言:javascript
复制
type(covid_json)
代码语言:javascript
复制
dict

我们可以通过列出键来检查顶级 JSON 对象中有哪些键。

代码语言:javascript
复制
covid_json.keys()
代码语言:javascript
复制
dict_keys(['meta', 'data'])

观察:JSON 字典包含一个meta键,这可能是指元数据(关于数据的数据)。元数据通常与数据一起维护,并且可以成为额外信息的良好来源。

我们可以通过检查与元数据相关联的键来进一步调查元数据。

代码语言:javascript
复制
covid_json['meta'].keys()
代码语言:javascript
复制
dict_keys(['view'])

meta键包含另一个名为view的字典。这可能是关于某个基础数据库的特定“视图”的元数据。我们将在后面的课程中学习更多关于视图的知识。

代码语言:javascript
复制
covid_json['meta']['view'].keys()
代码语言:javascript
复制
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'])

请注意,这是一个嵌套/递归数据结构。随着我们深入挖掘,我们会揭示更多的键和相应的数据:

代码语言:javascript
复制
meta
|-> data
    | ... (haven't explored yet)
|-> view
    | -> id
    | -> name
    | -> attribution 
    ...
    | -> description
    ...
    | -> columns
    ...

在视图子字典中有一个名为描述的键。这可能包含了数据的描述:

代码语言:javascript
复制
print(covid_json['meta']['view']['description'])
代码语言:javascript
复制
Counts of confirmed COVID-19 cases among Berkeley residents by date.
5.1.1.3.1.4 检查记录的数据字段

我们可以查看data字段中的一些条目。这是我们将加载到pandas中的数据。

代码语言:javascript
复制
for i in range(3):
 print(f"{i:03} | {covid_json['data'][i]}")
代码语言:javascript
复制
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键。这将返回一个列表:

代码语言:javascript
复制
type(covid_json['meta']['view']['columns'])
代码语言:javascript
复制
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
代码语言:javascript
复制
# 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

代码语言:javascript
复制
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对象。

代码语言:javascript
复制
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访问器。

我们可以得到月份:

代码语言:javascript
复制
calls["EVENTDT"].dt.month.head()
代码语言:javascript
复制
0    4
1    4
2    4
3    2
4    2
Name: EVENTDT, dtype: int64

日期是一周中的哪一天:

代码语言:javascript
复制
calls["EVENTDT"].dt.dayofweek.head()
代码语言:javascript
复制
0    3
1    3
2    0
3    5
4    0
Name: EVENTDT, dtype: int64

检查最小值,看看是否有任何看起来可疑的 70 年代日期:

代码语言:javascript
复制
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。我们将查看前四行:

代码

代码语言:javascript
复制
with open("data/cdc_tuberculosis.csv", "r") as f:
 i = 0
 for row in f:
 print(row)
 i += 1
 if i > 3:
 break
代码语言:javascript
复制
,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()函数返回带有所有特殊字符的原始字符串:

代码

代码语言:javascript
复制
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
代码语言:javascript
复制
',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

代码语言:javascript
复制
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 行的元素作为适当的列:

代码语言:javascript
复制
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()文档)手动执行此操作:

代码语言:javascript
复制
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 倍(你认为这是为什么?)。

代码

代码语言:javascript
复制
tb_df.sum(axis=0)
代码语言:javascript
复制
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 年的结核病病例怎么了?查看列类型:

代码

代码语言:javascript
复制
tb_df.dtypes
代码语言:javascript
复制
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参数(文档):

代码语言:javascript
复制
# 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

代码语言:javascript
复制
tb_df.sum()
代码语言:javascript
复制
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

总结核病例看起来没问题。哦!

让我们只看具有州级粒度的记录:

代码

代码语言:javascript
复制
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() (文档)将在下次详细解释。

代码

代码语言:javascript
复制
# 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库:

代码语言:javascript
复制
from importlib import reload
reload(utils)

或者使用iPython魔术,它将在文件更改时智能地导入代码:

代码语言:javascript
复制
%load_ext autoreload
%autoreload 2

代码

代码语言:javascript
复制
# 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, ...)文档)进行对比。可以随意使用任何一个。

代码语言:javascript
复制
# 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”。让我们选择后者。

代码语言:javascript
复制
# 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 年的情况:

代码语言:javascript
复制
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 仅用于此演示目的,但在探索本课程之外的数据时,它们很方便(文档)。

代码语言:javascript
复制
# 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 年,百分位数的小数位上有一些错误。进一步探讨这种差异背后的原因可能是有用的。

代码语言:javascript
复制
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 个州加上华盛顿特区)。没有滚动记录。*发生了什么…?

让我们开始探索吧!

在我们继续探索之前,我们将所有索引设置为更有意义的值,而不仅仅是与某一行相关的数字。这将使我们的清理稍微容易一些。

代码

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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中有“总计”滚动记录:

代码语言:javascript
复制
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中的滚动记录具有不同的地理区域字段,这是我们合并的关键:

代码语言:javascript
复制
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() (文档):

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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() 文档)。

代码语言:javascript
复制
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

最后,让我们重新计算我们的发病率:

代码语言:javascript
复制
# 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

代码语言:javascript
复制
incidence_2020 = tb_census_df.loc['Total', 'recompute incidence 2020']
incidence_2020
代码语言:javascript
复制
2.1637257652759883
代码语言:javascript
复制
incidence_2021 = tb_census_df.loc['Total', 'recompute incidence 2021']
incidence_2021
代码语言:javascript
复制
2.3672448914298068
代码语言:javascript
复制
difference = (incidence_2021 - incidence_2020)/incidence_2020 * 100
difference
代码语言:javascript
复制
9.405957511804143

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

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

代码语言:javascript
复制
co2_file = "data/co2_mm_mlo.txt"

让我们做一些EDA

7.1 将此文件读入 Pandas?

让我们来看看这个.txt文件。要记住的一些问题:我们信任这个文件扩展名吗?它的结构是什么?

第 71-78 行(包括)如下所示:

代码语言:javascript
复制
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 行。

代码语言:javascript
复制
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,但这次使用一些自定义列名

代码语言:javascript
复制
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]

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

代码

代码语言:javascript
复制
sns.lineplot(x='DecDate', y='Avg', data=co2);

上面的代码使用了seaborn绘图库(缩写为sns)。我们将在可视化讲座中介绍这一点,但现在你不需要担心它是如何工作的!

天啊!绘制数据揭示了一个问题。明显的垂直线表明我们有一些缺失值。这里发生了什么?

代码语言:javascript
复制
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

代码语言:javascript
复制
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 条记录。
代码语言:javascript
复制
co2.shape
代码语言:javascript
复制
(738, 7)

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

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

7.5 理解缺失值 1:Days

Days是一个时间字段,所以让我们分析其他时间字段,看看是否有关于操作天数缺失的解释。

让我们从月份Mo开始。

我们有没有缺失的记录?月份的数量应该有 62 或 61 个实例(1957 年 3 月-2019 年 8 月)。

代码语言:javascript
复制
co2["Mo"].value_counts().sort_index()
代码语言:javascript
复制
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本身,这是测量设备运行的天数。

代码

代码语言:javascript
复制
sns.displot(co2['Days']);
plt.title("Distribution of days feature"); # suppresses unneeded plotting output
代码语言:javascript
复制
/Users/Ishani/micromamba/lib/python3.9/site-packages/seaborn/axisgrid.py:118: UserWarning:

The figure layout has changed to tight 

就数据质量而言,少数月份的平均值是基于少于一半天数的测量得出的。此外,有近 200 个缺失值-大约占数据的 27%

最后,让我们检查最后一个时间特征,年份Yr

让我们检查一下缺失和记录年份之间是否有任何联系。

代码

代码语言:javascript
复制
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]测量的直方图

代码

代码语言:javascript
复制
# Histograms of average CO2 measurements
sns.displot(co2['Avg']);
代码语言:javascript
复制
/Users/Ishani/micromamba/lib/python3.9/site-packages/seaborn/axisgrid.py:118: UserWarning:

The figure layout has changed to tight 

非缺失值在 300-400 范围内(二氧化碳水平的常规范围)。

我们还看到只有少数缺失的“Avg”值(<1%的值)。让我们检查所有这些值:

代码语言:javascript
复制
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. 使用某种策略填补

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

代码

代码语言:javascript
复制
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 的可能值替换它?

你认为每种可能行动的利弊是什么?

让我们检查这三个选项。

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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

代码语言:javascript
复制
# 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 年的测量数据(我们知道有两个缺失值)。

代码

代码语言:javascript
复制
# 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 重新绘制我们的原始图表:

代码

代码语言:javascript
复制
sns.lineplot(x='DecDate', y='Avg', data=co2_impute)
plt.title("CO2 Average By Month, Imputed");

看起来与 NOAA 网站上看到的差不多!

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

从描述:

  • 月度测量是平均每日测量的平均值。
  • NOAA GML 网站也有每日/每小时测量的数据集。

您呈现的数据取决于您的研究问题。

二氧化碳水平如何随季节变化?

  • 您可能希望保留每月的平均数据。

过去 50 多年来,二氧化碳水平是否上升,与全球变暖的预测一致?

  • 您可能更喜欢使用年平均数据的粗粒度

代码

代码语言:javascript
复制
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 中开发可以促进你自己工作的可重复性
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、引言
    • 1.1 数据科学生命周期
      • 1.1.1 提出问题
      • 1.1.2 获取数据
      • 1.1.3 理解数据
      • 1.1.4 理解世界
    • 1.2 结论
    • 二、Pandas I
      • 2.1 表格数据
        • 2.2 Series、DataFrame和索引
          • 2.2.1 系列
          • 2.2.2 数据框
          • 2.2.3 索引
        • 2.3 DataFrame属性:索引、列和形状
          • 2.4 DataFrame中的切片
            • 2.4.1 使用.head和.tail提取数据
            • 2.4.2 基于标签的提取:使用.loc进行索引
            • 2.4.3 基于整数的提取:使用.iloc进行索引
            • 2.4.4 上下文相关的提取:使用[]进行索引
          • 2.5 结语
          • 三、Pandas II
            • 3.1 条件选择
              • 3.2 添加、删除和修改列
                • 3.3 实用程序函数
                  • 3.3.1 NumPy
                  • 3.3.2 .shape 和 .size
                  • 3.3.3 .describe()
                  • 3.3.4 .sample()
                  • 3.3.5 .value_counts()
                  • 3.3.6 .unique()
                  • 3.3.7 .sort_values()
                • 3.4 自定义排序
                  • 3.4.1 方法 1:创建一个临时列
                  • 3.4.2 方法 2:使用key参数进行排序
                  • 3.4.3 方法 3:使用map函数进行排序
                • 3.5 使用.groupby聚合数据
                  • 3.6 结语
                  • 四、Pandas III
                    • 4.1 重新审视.agg()函数
                      • 4.1.1 聚合函数
                      • 4.1.2 烦人的列
                      • 4.1.3 分组后重命名列
                      • 4.1.4 一些数据科学回报
                      • 4.1.5 绘制出生计数
                    • 4.2 GroupBy(),继续
                      • 4.2.1 原始GroupBy对象
                      • 4.2.2 其他GroupBy方法
                      • 4.2.3 按组进行过滤
                      • 4.2.4 使用 lambda 函数进行聚合
                    • 4.3 使用数据透视表聚合数据
                      • 4.4 连接表
                        • 4.5 结语
                        • 五、数据清洗和探索性数据分析
                          • 5.1 结构
                            • 5.1.1 文件格式
                            • 5.1.2 变量类型
                            • 5.1.3 主键和外键
                          • 5.2 粒度、范围和时间性
                            • 5.2.1 粒度
                            • 5.2.2 范围
                            • 5.2.3 时间性
                          • 5.3 忠实度
                            • 5.3.1 缺失值
                        • 6 EDA 演示 1:美国的结核病
                          • 6.1 CSV 和字段名称
                            • 6.2 记录粒度
                              • 6.3 收集人口普查数据
                                • 6.4 合并数据(合并“DataFrame”)
                                  • 6.5 再现数据:计算发病率
                                    • 6.6 奖励 EDA:重现报告的统计数据
                                    • 7 EDA 演示 2:毛纳罗亚 CO[2]数据 - 数据忠实度的一课
                                      • 7.1 将此文件读入 Pandas?
                                        • 7.2 探索变量特征类型
                                          • 7.3 可视化 CO[2]
                                            • 7.4 合理性检查:对数据进行推理
                                              • 7.5 理解缺失值 1:Days
                                                • 7.6 理解缺失值 2:Avg
                                                  • 7.7 删除、NaN或填补缺失的Avg数据?
                                                    • 7.8 展示数据:关于数据粒度的讨论
                                                    • 8 总结
                                                      • 8.1 处理缺失值
                                                        • 8.2 探索性数据分析和数据整理
                                                        相关产品与服务
                                                        对象存储
                                                        对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                                                        领券
                                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档