跳至主要内容

使用 pandas 进行数据准备

在本教程中,您将了解为何预处理数据很重要,以及如何在 pandas 中完成这些工作。
· 15分钟 阅读

使用 AI 探索

本教程将带您了解数据准备的一些基本概念和步骤。数据准备是在拿到任何数据集后的第一步。这一步会将原始数据预处理为便于且准确分析的形式。恰当的数据准备能提高分析效率——它可以消除在数据收集过程中可能出现的错误与不准确,从而有助于减轻由数据质量较差带来的偏差。因此,分析师的大量时间都会花在这个关键步骤上。

数据准备涉及的步骤包括:加载数据、清洗数据(移除不必要或错误的数据)、转换数据格式以及重排数据。在本教程中,您将使用 Python 的 Pandas 库进行数据准备。

请确保您已经理解 Pandas 的 DataFrame、Series 等概念,因为本教程中会频繁提及。您可以在 DataCamp 的 Pandas 教程中深入学习 pandas。

在本教程中:

在此过程中,还会提供一些提示与技巧,并给出更深入资料的参考链接,方便您进一步探索。

让我们先来简要介绍一下Pandas...

Pandas

Pandas 是为 Python 编写的软件库。它在数据科学社区非常知名,因为它提供了强大、表达力强且灵活的数据结构,使数据操作与分析变得简单,而且还是免费可用的。此外,如果您有特定且全新的用例,您甚至可以在 Python 的邮件列表或 pandas 的 GitHub 站点上分享——事实上,pandas 中的大多数功能正是由真实世界的用例驱动的。

如需优秀的 pandas 入门介绍,请务必查看 DataCamp 的 Pandas Foundation 课程。它非常互动,而且第一章免费!

要使用 pandas 库,您需要先导入它。只需在您的 Python 控制台中输入:

import pandas as pd

加载数据

数据准备的第一步就是……获取一些数据。如果您有 .csv 文件,可以使用 pandas 中的 .read_csv() 函数轻松加载。您可以这样写:

data = pd.read_csv('path_to_file.csv')

不过,在本教程中,您将使用一些更小、易于理解的 DataFrame 和 Series,并在运行时创建它们。

缺失数据

处理缺失数据

这是数据分析领域非常普遍的问题。

handling missing data diagram


来源

数据集中出现缺失数据的原因可能有很多:某个字段的数据未被用户/数据收集应用添加、手工传输时数据丢失、程序错误等。有时了解原因很重要,因为这会影响您如何处理这些数据。但这部分内容会在本教程稍后提到。现在先来关注遇到缺失数据时该怎么做……

对于数值型数据,pandas 使用浮点值 NaN(Not a Number)来表示缺失数据。它是在NumPy库中定义的一个特殊值,因此您也需要导入它。出于计算速度和便利性的考虑,NaN 被用作默认的缺失值标记。它是一个哨兵值,即一种可用来占位或标记的虚拟数据,pandas 中的函数可以轻松检测并处理它。

nan vs null

让我们来玩一玩数据……

import numpy as np

# Creating a pandas series
data = pd.Series([0, 1, 2, 3, 4, 5, np.nan, 6, 7, 8])

# To check if and what index in the dataset contains null value
data.isnull()
0    False
1    False
2    False
3    False
4    False
5    False
6     True
7    False
8    False
9    False
dtype: bool

上面我们使用了 isnull() 函数,它返回布尔值 true 或 false。当某个索引位置的数据确实缺失或为 NaN 时为 True。与之相反的是 notnull() 函数。

# To check where the dataset does not contain null value - opposite of isnull()
data.notnull()
0     True
1     True
2     True
3     True
4     True
5     True
6    False
7     True
8     True
9     True
dtype: bool

此外,我们可以使用 dropna() 函数过滤缺失数据并移除空值(缺失值),只查看非空值。不过,NaN 实际并没有被真正删除,仍可在原始数据集中找到。

# Will not show the index 6 cause it contains null (NaN) value
data.dropna()
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
5    5.0
7    6.0
8    7.0
9    8.0
dtype: float64
data
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
5    5.0
6    NaN
7    6.0
8    7.0
9    8.0
dtype: float64

如果您确实想“丢弃”或删除 NaN 值,要么将新数据集(不含 NaN)另存,这样就不会改动原始 Series,要么在原地(inplace)执行删除。inplace 参数的默认值为 false。

not_null_data = data.dropna()
not_null_data
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
5    5.0
7    6.0
8    7.0
9    8.0
dtype: float64
# Drop the 6th index in the original 'data' since it has a NaN place
data.dropna(inplace = True)
data
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
5    5.0
7    6.0
8    7.0
9    8.0
dtype: float64

不过,DataFrame 可能更复杂,是二维的,包含行和列。Pandas 依然能满足您的需求……

# Creating a dataframe with 4 rows and 4 columns (4*4 matrix)
data_dim = pd.DataFrame([[1,2,3,np.nan],[4,5,np.nan,np.nan],[7,np.nan,np.nan,np.nan],[np.nan,np.nan,np.nan,np.nan]])
data_dim
 0123
01.02.03.0NaN
14.05.0NaNNaN
27.0NaNNaNNaN
3NaNNaNNaNNaN

现在,假设您只想丢弃全为 null 的行或列,或者只丢弃包含一定数量 null 值的那些。来看几个不同场景,请注意这些删除并非inplace,因此不会改动真实数据集。留意传给 dropna() 的参数,以决定如何丢弃缺失数据。

# Drop all rows and columns containing NaN value
data_dim.dropna()
 0123
# Drop all rows and columns containing entirely of NaN value
data_dim.dropna(how = 'all')
 0123
01.02.03.0NaN
14.05.0NaNNaN
27.0NaNNaNNaN
# Drop only columns that contain entirely NaN value
# Default is 0 - which signifies rows
data_dim.dropna(axis = 1, how = 'all')
 012
01.02.03.0
14.05.0NaN
27.0NaNNaN
3NaNNaNNaN
# Drop all columns that have more than 2 NaN values
data_dim.dropna(axis = 1, thresh = 2)
 01
01.02.0
14.05.0
27.0NaN
3NaNNaN
# Drop all rows that have more than 2 NaN values
data_dim.dropna(thresh = 2)
 0123
01.02.03.0NaN
14.05.0NaNNaN

现在,您已经知道如何识别并丢弃缺失值——无论是仅查看结果数据集,还是进行inplace删除。在很多情况下,简单地丢弃空值并不可行,您可能希望用其他值填补缺失数据。我们来看看该如何做……

填充缺失数据

要替换或“填充”空值,您可以使用 fillna() 函数。比如,让我们继续使用上面的数据集,尝试将 NaN 值填充为 0。

# Check what the dataset looks like again
data_dim
 0123
01.02.03.0NaN
14.05.0NaNNaN
27.0NaNNaNNaN
3NaNNaNNaNNaN
# Fill the NaN values with 0
data_dim_fill = data_dim.fillna(0)
data_dim_fill
 0123
01.02.03.00.0
14.05.00.00.0
27.00.00.00.0
30.00.00.00.0

与 dropna() 类似,您也可以根据传入的参数完成更多事情。再提醒一下,传入 inplace = True 参数会对原始数据集进行修改。

# Pass a dictionary to use differnt values for each column
data_dim_fill = data_dim.fillna({0: 0, 1: 8, 2: 9, 3: 10})
data_dim_fill
 0123
01.02.03.010.0
14.05.09.010.0
27.08.09.010.0
30.08.09.010.0

您可以向 fillna() 传入 method 参数,以自动向前(ffill 或 pad)或向后(bfill 或 backfill)传播非空值。

# Pass method to determine how to fill-up the column - forward here
data_dim_fill = data_dim.fillna(method='ffill')
data_dim_fill
 0123
01.02.03.0NaN
14.05.03.0NaN
27.05.03.0NaN
37.05.03.0NaN

您还可以限制上述填充的数量。例如,每列只填充两个位置……另外,如果传入 axis = 1,则会相应地按行进行填充。

# Pass method to determine how to fill-up the column - forward here
data_dim_fill = data_dim.fillna(method='ffill', limit = 2)
data_dim_fill
 0123
01.02.03.0NaN
14.05.03.0NaN
27.05.03.0NaN
37.05.0NaNNaN
# Pass method to determine how to fill-up the row - forward here
data_dim_fill = data_dim.fillna(axis = 1, method='ffill')
data_dim_fill
 0123
01.02.03.03.0
14.05.05.05.0
27.07.07.07.0
3NaNNaNNaNNaN

结合对数据及用例的理解,您可以用 fillna() 做的远不止填数字。您也可以用均值 mean() 或中位数 median() 来填充……

# Check the data_dim dataset
data_dim
 0123
01.02.03.0NaN
14.05.0NaNNaN
27.0NaNNaNNaN
3NaNNaNNaNNaN
# Fill the NaN value with mean values in the corresponding column
data_dim_fill = data_dim.fillna(data_dim.mean())
data_dim_fill
 0123
01.02.03.0NaN
14.05.03.0NaN
27.03.53.0NaN
34.03.53.0NaN

数据转换

替换数值

到目前为止,您只处理了缺失数据(NaN),但在某些情况下,您可能希望将某个非空值替换为不同的值。或者某个空值被记录成了一个随机数字,因此需要将其作为 NaN 而不是数字来处理。这时 replace() 函数就派上用场了……

这次我们创建一个不同的数据集。

data = pd.Series([1,2,-99,4,5,-99,7,8,-99])
data
0     1
1     2
2   -99
3     4
4     5
5   -99
6     7
7     8
8   -99
dtype: int64
# Replace the placeholder -99 as NaN
data.replace(-99, np.nan)
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
5    5.0
7    6.0
8    7.0
9    8.0
dtype: float64

您将不再看到 -99,因为它被替换为了 NaN,因此不再显示。类似地,您可以传入多个待替换的值。为此,我们将创建另一个 Series,然后把原始数据 Series 与新 Series 连接起来,再应用多值替换函数。

连接 Pandas Series

为此我们可以使用 pandas 的 concat() 函数。要在连接后重新连续编号索引,可以传入 ignore_index = True 参数。

# Create a new Series
new_data = pd.Series([-100, 11, 12, 13])
combined_series = pd.concat([data, new_data], ignore_index = True)
combined_series
0       1
1       2
2     -99
3       4
4       5
5     -99
6       7
7       8
8     -99
9    -100
10     11
11     12
12     13
dtype: int64
# Let's replace -99 and -100 as NaN in the new combined_series
data_replaced = combined_series.replace([-99, -100], np.nan)
data_replaced
0      1.0
1      2.0
2      NaN
3      4.0
4      5.0
5      NaN
6      7.0
7      8.0
8      NaN
9      NaN
10    11.0
11    12.0
12    13.0
dtype: float64
# Argument passed can also be a dictionary with separate values
data_replaced = combined_series.replace({-99: np.nan, -100: 0})

# Same as: new_data.replace([-99, -100], [np.nan, 0])
data_replaced
0      1.0
1      2.0
2      NaN
3      4.0
4      5.0
5      NaN
6      7.0
7      8.0
8      NaN
9      0.0
10    11.0
11    12.0
12    13.0
dtype: float64

添加知识——Map 函数

在某些情况下,您可能希望基于某些逻辑为现有数据添加更多洞见。这时,您可以借助映射和函数组合来实现目标。逻辑可以更复杂,但先通过下面的例子来理解。

假设您有一个数据框,将数字与其对应的英文单词进行映射。

data_number = pd.DataFrame({'english': ['zero','one','two','three','four','five'],
'digits': [0,1,2,3,4,5]})
data_number
 englishdigits
0zero0
1one1
2two2
3three3
4four4
5five5

现在,假设您想再添加一列,用“yes”标识 2 的倍数,其余标为“no”。您可以为每个不同的英文单词写一个映射,标注对应的“yes”或“no”。

english_to_multiple = {
    'two': 'yes',
    'four': 'yes'
}

然后您可以调用 map 函数,在 English 列为 2 的倍数时添加该列。其他非倍数的行会填入什么?让我们看看……

data_number['multiple'] = data_number['english'].map(english_to_multiple)
data_number
 englishdigitsmultiple
0zero0NaN
1one1NaN
2two2yes
3three3NaN
4four4yes
5five5NaN

其他列被填充为 NaN,而您已经知道如何进一步处理缺失值。这个例子很简单,但希望能给您一些启发,利用这里的思路通过 map 函数做更多事情,并在您的具体用例中加以运用。

离散化——cut 函数

有时您可能希望基于某些逻辑进行分类,把所有数据放入若干个离散的桶(bins)以便分析。您可以使用 cut() 函数。例如,我们先创建一个包含 1 到 100 之间 30 个随机数的数据集。

import random

data = random.sample(range(1, 101), 30)
# data
[45,
 39,
 25,
 83,
 27,
 6,
 73,
 43,
 36,
 93,
 97,
 17,
 15,
 99,
 37,
 5,
 31,
 22,
 65,
 30,
 3,
 26,
 91,
 12,
 52,
 76,
 63,
 84,
 59,
 53]

假设我们想用自定义的桶来分类:1 - 25,然后 25 - 35、40 - 60、60 - 80,剩下的为最后一组。于是我们定义一个桶……

然后使用cut 函数。

# Defining the starting value for each bucket
bucket = [1, 25, 35, 60, 80, 100]

cut_data = pd.cut(data, bucket)
cut_data
[(35, 60], (35, 60], (1, 25], (80, 100], (25, 35], ..., (60, 80], (60, 80], (80, 100], (35, 60], (35, 60]]
Length: 30
Categories (5, interval[int64]): [(1, 25] < (25, 35] < (35, 60] < (60, 80] < (80, 100]]

cut() 是一个非常有用的函数,它还能做更多事情。强烈建议您查阅 Pandas 文档以进一步了解。

虚拟变量与独热编码

当您需要将某些分类数据转换为数值以用于数据分析模型时,这个主题尤其有用。在进行机器学习建模时,独热编码这一概念非常常见。更技术性地说:独热编码是将分类取值转换为一维数值向量的过程。

在 pandas 中实现这一点的方法之一是使用 get_dummies() 函数。如果数据框中的某列有 n 个不同取值,该函数会生成一个包含 n 列的矩阵,元素为 1 和 0。我们通过一个例子来更好地理解这一概念……

# Creating a DataFrame consiting individual characters in the list
data = pd.Series(list('abcdababcdabcd'))
data
0     a
1     b
2     c
3     d
4     a
5     b
6     a
7     b
8     c
9     d
10    a
11    b
12    c
13    d
dtype: object

现在,假设您想为每个字符的出现构建单独的向量,以便传递给某个函数。

比如像这样:对于“a” = [1,0,0,0,1,0,1,0,0,0,1,0,0,0],其中 1 表示对应位置存在“a”,0 表示不存在。使用 get_dummies() 函数可以更轻松地完成此任务。

pd.get_dummies(data)
 abcd
01000
10100
20010
30001
41000
50100
61000
70100
80010
90001
101000
110100
120010
130001

不过,这只是一个帮助您入门的简单示例。在真实应用中,一个字符的成员关系可能会同时属于多个类别,因此您需要学习更复杂的方法。而且对于更大的数据量,这个函数在速度方面可能并不高效。DataCamp 的 Handling Categorical Data in Python 对此类情况和主题有更深入的讨论。

接下来呢?

到这里,您已经完成了本教程,学会了开启数据分析之路第一步的一些基础工具。数据准备步骤的其他技能还包括学习如何在数据集中检测并过滤异常值,以及当数据集非常庞大时——或许先从随机抽样开始。但这些本身又是较为广泛的主题,难以一概而论。

真实世界的用例往往更复杂,可能需要独特的解决方案,但关键是从小处着手,在实践中学习,勇于探索,并多接触不同的数据集!务必查看 DataCamp 的 Cleaning Data in Python 课程,学习更多工具和技巧。最后,您将有机会处理来自 Gapminder 基金会的真实且“凌乱”的数据集。动手开始吧!

主题