数据清洗与准备——《利用Python进行数据分析》第七章阅读笔记

数据清洗与准备——《利用Python进行数据分析》第七章阅读笔记

前言

又来了,希望快点写完吧!

环境还是 Pycharm 2021.3 + Anaconda3

在进行数据分析和建模的过程中,大量的时间华仔数据准备上:加载、清理、转换和重新排列。pandas 以及内置的 Python 语言功能为我们提供了一个高级、灵活和快速的工具集,能够将数据处理为正确的形式。

在本节中,将介绍用于缺失值、重复值、字符串操作和其他分析数据转换的工具。

一 处理缺失值

缺失数据会在很多数据分析中出现。pandas 的目标之一就是 尽可能无痛地处理缺失值。对于数值型数据,pandas使用浮点数值NaN(Not a Number)来表示缺失值。

在pandas中,采用了R语言的编程惯例,将缺失值成为NA,意思是not available(不可用)。在统计学应用中,NA数据可以是不存在的数据或者使存在但不可观察的数据(例如在数据收集过程中出现了问题)。在清洗数据用于分析使,对缺失值本身进行分析以确定数据收集问题或数据丢弃导致的数据偏差通常很重要。

python 内建的 None值在对象数组中也被当做NA处理

import pandas as pd
import numpy as np

string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
print(string_data)
print('----------------------')
print(string_data.isnull())
print('----------------------')
string_data[0] = None
print(string_data.isnull())

表7-1 归纳了一些NA处理方法

1. 过滤缺失值

主要使用dropna方法,在Series使用会返回Series 中所有的非空数据及其索引值

当出路DataFrame时,dropna默认情况下会删除包含缺失值的行。传入how = ‘all’ 时,将删除所有值为NA的行。如果要是删除列,需要传入axis=1。

过滤DataFrame 的行的相关方法往往涉及时间序列数。如果指向保留包含一定数量的观察值的行,可以用thresh参数来表示。

import pandas as pd
import numpy as np
from numpy import nan as NA

data = pd.Series([1, NA, 3.5, NA, 7])
print(data.dropna())
# 下述代码与上面等价
print('---------------------------')
print(data[data.notnull()])
print('---------------------------')
data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA],
                    [NA, NA, NA], [NA, 6.5, 3.]])
cleaned = data.dropna()
print(data)
print('---------------------------')
print(cleaned)
print('---------------------------')
# 删除全为空的行
print(data.dropna(how='all'))
print('---------------------------')
data[4] = NA
print(data)
print('---------------------------')
# 删除全为空的列
print(data.dropna(axis=1, how='all'))
print('---------------------------')
df = pd.DataFrame(np.random.randn(7, 3))
df.iloc[:4, 1] = NA
df.iloc[:2, 2] = NA
print(df)
print('---------------------------')
print(df.dropna())
print('---------------------------')
# thresh 表示观察值,即非空值的数量
print(df.dropna(thresh=2))

2. 补全缺失值

主要使用fillna来补全缺失值。可以使用常数补全,也可以使用字典,对不同的列设定不同的填充值。

fillna 返回的是一个新的对象,但是也可以修改已经存在的对象。

用于重建索引的相同的插值方法也可以用于fillna。

可以将Series 的平均值或中位数用于填充缺失值

import pandas as pd
import numpy as np
from numpy import nan as NA

df = pd.DataFrame(np.random.randn(7, 3))
df.iloc[:4, 1] = NA
df.iloc[:2, 2] = NA
print(df.fillna(0))
print('--------------------')
print(df.fillna({1: 0.5, 2: 0}))
print('--------------------')
_ = df.fillna(0, inplace=True)
print(df)
print('--------------------')
df = pd.DataFrame(np.random.randn(6, 3))
df.iloc[2:, 1] = NA
df.iloc[4:, 2] = NA
print(df)
print('--------------------')
print(df.fillna(method='ffill'))
print('--------------------')
print(df.fillna(method='ffill', limit=2))
print('--------------------')
data = pd.Series([1, NA, 3.5, NA, 7])
print(data.fillna(data.mean()))

表7-2提供了fillna的函数参数参考

二 数据转换

前面主要讲了数据的重新排列,过滤、清洗和其他转换是另外一系列重要的操作。

1. 删除重复值

由于各种原因,DataFrame 会出现重复行。

DataFrame 的duplicated 方法返回的是一个布尔值Series,其反映的是每一行是否存在重复的情况(与之前出现过的行相同)。

drop_duplicates 返回的是一个DataFrame,内容是duplicated 返回数组中为False的部分。

这些方法默认都是对列进行操作,可以指定数据的任何子集来检测是否又重复。

duplicated 和 drop_duplicates 默认都是保留第一个观测到的值。传入参数 keep = ‘last’ 将会返回最后一个。

import pandas as pd
import numpy as np
from numpy import nan as NA

data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                     'k2': [1, 1, 2, 3, 3, 4, 4]})
print(data)
print('----------------------------')
print(data.duplicated())
print('----------------------------')
print(data.drop_duplicates())
print('----------------------------')
data['v1'] = range(7)
# 基于k1 列删除重复值
print(data.drop_duplicates(['k1']))
print('----------------------------')
print(data.drop_duplicates(['k1', 'k2'], keep='last'))

2. 使用函数或映射进行数据转换

对基于DataFrame 中的数组、列或列中的数值进行一些转换。

Series 的 map 方法接收一个函数 或一个包含映射关系的字典型对象,str.lower 方法将每个大写转换为小写。

也可以传入一个能够完成所有的工作的函数。

使用map 是一种可以便捷执行按元素转换及其其他清洗相关操作的方法。

import pandas as pd
import numpy as np
from numpy import nan as NA

data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon',
                              'Pastrami', 'corned beef', 'Bacon',
                              'pastrami', 'honey ham', 'nova lox'],
                     'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
print(data)
print('----------------------------------')
meat_to_animal = {
    'bacon': 'pig',
    'pulled pork': 'pig',
    'pastrami': 'cow',
    'corned beef': 'cow',
    'honey ham': 'pig',
    'nova lox': 'salmon'
}
lowercased = data['food'].str.lower()
print(lowercased)
print('----------------------------------')
data['animal'] = lowercased.map(meat_to_animal)
print(data)
print('----------------------------------')
print(data['food'].map(lambda x: meat_to_animal[x.lower()]))

3. 替代值

主要使用replace方法。对于一个Series,使用replace 方法生成新的 Series,除非设置inplace = True。

如果想要一次替代多个值,可以传入一个列表和替代值或列表。

参数可以通过字典传递

data.replace 与 data.str.replcae 不同,后者是对字符串进行按元素替代。

import pandas as pd
import numpy as np
from numpy import nan as NA

data = pd.Series([1., -999., 2., -999., -1000., 3.])
print(data)
print('---------------------------------')
print(data.replace(-999, np.nan))
print('---------------------------------')
print(data.replace([-999, -1000], np.nan))
print('---------------------------------')
print(data.replace([-999, -1000], [np.nan, 0]))
print('---------------------------------')
print(data.replace({-999: np.nan, -1000: 0}))

4. 重命名轴索引

和Seires 中的值一样,可以通过函数或某种形式的映射对轴标签进行类似的转换,生成新的且带有不同标签的对象,也可以在不生成新的数据结构的情况下修改轴。

和Series 类似,轴索引也由一个map方法。

可以赋值给index,修改DataFrame。

如果想要创建数据集转换后的版本,并且不修改原有的数据集,可以使用rename。值得注意的是,rename可以结合字典型对象使用,为轴标签的子集提供新的值。

rename 可以让我们从手动复制DataFrame 并为其分配索引和列属性的烦琐工作中解放出来。如果想要修改原有的数据集,传入inplace = True

import pandas as pd
import numpy as np
from numpy import nan as NA

data = pd.DataFrame(np.arange(12).reshape((3, 4)),
                    index=['Ohio', 'Colorado', 'New York'],
                    columns=['one', 'two', 'three', 'four'])
transform = lambda x: x[:4].upper()
print(data.index.map(transform))
print('----------------------------------')
data.index = data.index.map(transform)
print(data)
print('----------------------------------')
print(data.rename(index=str.title, columns=str.upper))
print('----------------------------------')
print(data.rename(index={'OHIO': 'INDIANA'},
                  columns={'three': 'peekaboo'}))
data.rename(index={'OHIO': 'INDIANA'}, inplace=True)
print('----------------------------------')
print(data)

5. 离散化和分箱

连续值经常需要离散化,或者分离成“箱子”进行分析。可以使用pandas中的cut。

pandas 返回的对象是一个特殊的Categorical对象。可以将它当作一个表示箱名的字符串数组,它在内部包含一个categories(类别)的数组,制定了不同的类别的名称以及codes属性中的ages(年龄)数据标签。

pd.value_counts 是对pandas对象结果计数。

与区间表示一致,小括号表示开放,中括号表示封闭。可以通过传递right=False来改变那一边是封闭的。

可以通过向labels 选项传递一个列表或数组来传入自定义的箱名。

如果传入cut整数个的箱来代替显示的箱边, pandas 将根据数据中的最小值和最大值计算出等长的箱。

qcut 是一个与分箱密切相关的函数, 基于样本分位数进行分箱。取决于数据的分布,使用cut通常不会使每个箱具有相同数据量的数据点。可以通过qcut获得等长的箱。与cut类似,可以传入自定义的分位数。

import pandas as pd
import numpy as np

ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)
print(cats)
print('-------------------------------------------')
print(cats.codes)
print('-------------------------------------------')
print(cats.categories)
print('-------------------------------------------')
print(pd.value_counts(cats))
print('-------------------------------------------')
print(pd.cut(ages, [18, 26, 36, 61, 100], right=False))
print('-------------------------------------------')
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
print(pd.cut(ages, bins, labels=group_names))
print('-------------------------------------------')
data = np.random.rand(20)
# precision = 2 表示将十进制进度限制在两位
print(pd.cut(data, 4, precision=2))
print('-------------------------------------------')
data = np.random.randn(1000)    # 正态分布
cats = pd.qcut(data, 4)         # 切成四份
print(cats)
print('-------------------------------------------')
print(pd.value_counts(cats))
print('-------------------------------------------')
print(pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]))

6. 检测和过滤异常值

过滤和转换异常值在很大程度上是应用数组操作的事情。

值可以根据这些标准来设置,下面代码限制-3到3之间的数值。np.sign() 根据数据中的正负分别生成1和-1的数值。

import pandas as pd
import numpy as np

data = pd.DataFrame(np.random.randn(1000, 4))
print(data.describe())
print('------------------------------')
# 找出一列中绝对值大于三的值
col = data[2]
print(col[np.abs(col) > 3])
print('------------------------------')
# 要选出所有值大于3或者小于-3 的行,可以对布尔值DataFrame 使用any方法
print(data[(np.abs(data) > 3).any(1)])
print('------------------------------')
data[np.abs(data) > 3] = np.sign(data) * 3
print(data.describe())
print('------------------------------')
print(np.sign(data).head())

7. 置换和随机抽样

使用numpy.random.permutation 对 DataFrame 中的 Series 或行进行置换(随机重排序)是非常方便的。在调用permutation时根据你想要的轴长度可以产生一个表示新顺序的整数数组。

整数数组可以用基于iloc 的索引 或等价的take函数。

要选出一个不含有替代值的随机子集,可以使用Series 和 DataFrame 的 sample方法。

要生成一个含有替代值的样本(允许有重复选择),将replace=True传入smaple方法。

import pandas as pd
import numpy as np

df = pd.DataFrame(np.arange(5 * 4).reshape((5, 4)))
sampler = np.random.permutation(5)
print(sampler)
print('---------------------------')
print(df)
print('---------------------------')
print(df.take(sampler))
print('---------------------------')
print(df.sample(n=3))
print('---------------------------')
choices = pd.Series([5, 7, -1, 6, 4])
draws = choices.sample(n=10, replace=True)
print(draws)

8. 计算指标/虚拟变量

将分类变量转换为“虚拟”或指标矩阵式另一种用于统计建模或机器学习的转换操作。如果DataFrame 中的一列有k个不同的值,则可以衍生出一个k列的值为1和0的矩阵或DataFrame。pandas 有一个 get_dummies 函数用于实现该功能。

在某些情况下,你可能想在指标DataFrame 的列上加入前缀,然后与其他数据合并。在get_dummies 方法中有一个前缀参数实现。

如果DataFrame 中的一行属于多个类别,则情况略为复杂。

书中用了一个例子,需要从github仓库再下载以下文件 movies.dat

为每个电影流派添加指定变量需要进行一些数据处理。首先,我们从数据集中提取出所有不同的流派的列表。

使用全0的DataFrame 是构建 指标 DataFrame 的一种方式。

现在遍历每一部电影,将dummies 每一行的条目设置为1,为了实现该功能,使用demmies.columns 来计算每一个流派的列指标。之后使用iloc根据这些指标来设置值。可以将结果和movies进行联合。

对于更大的数据,上面这种使用多成员构建指标变量并不是特别快速。更好的写法是写一个直接将数据写为NumPy 数组的底层函数,然后将结果封装进DataFrame。

将 get_dummies与 cut 等离散化函数结合使用是统计应用的一个有用方法。使用seed来设置随机种子以确保示例的确定性。我们将在本书后面的内容再次讨论pandas.get_dummies。

import pandas as pd
import numpy as np

df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
                   'data1': range(6)})
print(pd.get_dummies(df['key']))
print('-------------------------------------------')
dummies = pd.get_dummies(df['key'], prefix='key')
df_with_dummy = df[['data1']].join(dummies)
print(df)
print('-------------------------------------------')
print(df_with_dummy)
print('-------------------------------------------')
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('examples/movies.dat', sep='::',
                       header=None, names=mnames)
print(movies[:10])
print('-------------------------------------------')
all_genres = []
for x in movies.genres:
    all_genres.extend(x.split('|'))

genres = pd.unique(all_genres)
print(genres)
print('-------------------------------------------')
zero_matrix = np.zeros((len(movies), len(genres)))
dummies = pd.DataFrame(zero_matrix, columns=genres)
print('-------------------------------------------')
gen = movies.genres[0]
print(gen.split('|'))
print('-------------------------------------------')
print(dummies.columns.get_indexer(gen.split('|')))
print('-------------------------------------------')
for i, gen in enumerate(movies.genres):
    indices = dummies.columns.get_indexer(gen.split('|'))
    dummies.iloc[i, indices] = 1

movies_windic = movies.join(dummies.add_prefix('Genre_'))
print(movies_windic.iloc[0])
print('-------------------------------------------')
np.random.seed(12345)
values = np.random.randn(10)
print(values)
print('-------------------------------------------')
bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
print(pd.get_dummies(pd.cut(values, bins)))

三 字符串操作

pandas 允许将字符串和正则表达式简洁地应用到整个数组上,此外还能处理数据确实带来的困扰。

1. 字符串对象方法

很多字符串处理和脚本应用中,内建的字符串方法是足够的。

split 常和 strip 一起使用,用于清除空格(包括换行)。

这些字符串可以使用加法与两个冒号分隔符连接起来,但这并不是一个实用的通用方法,在字符串"::" 的join方法中传入一个列表或元组是一种更快且更加Python风格化的方法。

其他方法涉及定位子字符串,使用python的 in 关键字是检测总字符串的最佳方法。find 和 index 的区别在于index 在字符串没有找到时会抛出一个异常。

相关地,count返回的是某个特定的字符串在字符串中出现的次数, replace 将用一种模式替代另一种模式。

import pandas as pd
import numpy as np

val = 'a,b,    guido'
print(val.split((',')))
print('--------------------------')
pieces = [x.strip() for x in val.split(',')]
print(pieces)
print('--------------------------')
first, second, third = pieces
print(first + "::" + second + "::" + third)
print('::'.join(pieces))
print('--------------------------')
print('guido' in val)
print(val.index(','))
print(val.find(','))
print('--------------------------')
print(val.count(','))
print(val.replace(',', '::'))
print(val.replace(',', ''))

更多python的内建字符串方法请参考表7-3

2. 正则表达式

正则表达式提供了一种在文本中灵活查找或匹配字符串模式的方法。我本人也在做用到正则表达式的项目,后面会更新相关的blog。python中主要使用到 re库。

re模块主要有三个主题:模式匹配、替代、拆分。

当你调用re.split(’\s+’, text),正则表达式首先会被编译,然后正则表达式的split方法在传入文本上被调用。

为了在正则表达式避免转义符\的影响,可以使用原生字符串语法,比如r’C:\x’ 或者用等价的’C:\x’

如果需要将相同的表达式应用到多个字符串上,推荐使用compile创建一个正则表达式对象,这样做有利于节约CPU周期。

match 和 search 与 findall 相关性很大。 findall 返回的是 字符串 中所有的 匹配项。而search 返回的仅仅是第一个匹配项。match更为严格,只在字符串的起始位置进行匹配。

使用groups 方法可以完成分组。

sub 也可以使用特殊符号,如\1 和 \2,访问每个匹配对象中的分组。\1 表示第一个分组, \2 代表的是第二个匹配分组。

import re

text = 'foo    bar\t   baz     \tqux'
print(re.split('\s+', text))
regx = re.compile('\s+')
print(regx.split(text))
print('-----------------------------------')
print(regx.findall(text))
print('-----------------------------------')
text = """Dave dave@google.com
Steve steve#gmail.com
Rob rob@gmail.com
Ryan ryan@yahoo.com
"""
pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
# re.IGNORECASE 使正则表达式不区分大小写
regex = re.compile(pattern, flags=re.IGNORECASE)
print(regex.findall(text))
m = regex.search(text)
print('-----------------------------------')
print(m)
print(text[m.start():m.end()])
print('-----------------------------------')
print(regex.match(text))
print(regex.sub('REDACTED', text))
print('-----------------------------------')
pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
regex = re.compile(pattern, flags=re.IGNORECASE)
m = regex.match('wesm@bringht.net')
print(m.groups())
print('-----------------------------------')
print(regex.findall(text))
print('-----------------------------------')
print(regex.sub(r'Username:  \1, Domain: \2, Suffix:  \3', text))

更多正则表达式参考表7-4和python的re官方文档。

3. pandas 中的向量化字符串函数

清理杂乱的数据集用于分析通常需要大量的字符串处理和正则化。

可以使用data.map 将字符串和有效的正则表达式方法应用到每个值上,但是在NA(null)值上会失败。为了解决这问题,Series 有面向数组的方法用于跳过NA值的字符串操作。这些方法通过Series 的str属性进行调用。

正则表达式也可以结合任意的re模块选项使用。

可以使用str.get 或在 str 属性内部索引。

要访问嵌入式列表中的元素,可以将索引传递给函数。

可以使用字符串切片的类似语法进行向量化切片。

import re
import pandas as pd
import numpy as np

data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com',
        'Rob': 'rob@gmail.com', 'Wes': np.nan}
data = pd.Series(data)
print(data)
print('------------------------------------')
print(data.isnull())
print('------------------------------------')
print(data.str.contains('gmail'))
print('------------------------------------')
pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
print(data.str.findall(pattern, flags=re.IGNORECASE))
print('------------------------------------')
matches = data.str.match(pattern, flags=re.IGNORECASE)
print(matches)
print('------------------------------------')
print(data.str[0])
print('------------------------------------')
print(data.str[:5])

更多向量化字符串方法列表参考表7-5

小结

高效的数据准备工作使我们可以将更多的时间用于分析而不是准被,从而显著提升生产效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值