讨论用于缺失值、重复值、字符串操作和其他分析数据转换的工具
7.1 处理缺失值
pandas对象的所有描述性统计信息默认情况下是排除缺失值的
-
对于数值型数据,pandas使用浮点值NaN(Not a Number来表示缺失值)
- NA(not available,不可用)
- Python内建的None值在对象数组中也被当作NA处理
-
NA处理方法
过滤缺失值
-
过滤缺失值方法
-
使用pandas.isnull和布尔值索引
-
使用dropna
-
在Series上使用,会返回Series中所有的非空数据及其索引值
import pandas as pd from numpy import nan as NA data = pd.Series([1,NA,3.5,NA,7]) data.dropna()
等价表示:
data[data.notnull()]
-
处理DataFrame对象时,dropna默认删除包含缺失值的行
-
传入how='all’时,将删除所有值均为NA的行
-
删除列,传入参数axis=1
data.dropna(axis=1,how='all')
-
使用thresh参数保留一定数量的观察值的行
df.dropna(thresh=2)
-
-
-
补全缺失值
-
使用fillna方法来补全缺失值
- 可以使用一个常数来代替缺失值:
df.fillna(0)
- 调用时使用字典来为不同列设定不同的填充值:
df.fillna({1:0,2:0})
- fillna返回一个新对象,也可以修改意见存在的对象:
df.fillna(0,inplace=True)
- 用于重建索引的相同的插值方法也可以用于fillna:
df.fillna(method=‘ffill’,limit=2)
- 使用fillna完成创造性填充:
data.fillna(data.mean())
- 可以使用一个常数来代替缺失值:
-
fillna函数参数
7.2 数据转换
删除重复值
-
有些数据会出现重复行
-
DataFrame的duplicated方法返回一个布尔值的Series,反映每一行是否与之前出现过相同的行。
data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'], 'k2': [1, 1, 2, 3, 3, 4, 4]}) data """ k1 k2 0 one 1 1 two 1 2 one 2 3 two 3 4 one 3 5 two 4 6 two 4 """ data.duplicated() """ 0 False 1 False 2 False 3 False 4 False 5 False 6 True dtype: bool """
-
drop_duplicates返回duplicated返回数组中为False的部分,格式为DataFrame
data.drop_duplicates() """ k1 k2 0 one 1 1 two 1 2 one 2 3 two 3 4 one 3 5 two 4 """
-
方法默认针对列,可以指定数据的子集来检测是否有重复
# 假设有一个额外的列,并想基于’k1’列去除重复值 data['v1'] = range(7) data.drop_duplicates(['k1']) """ k1 k2 v1 0 one 1 0 1 two 1 1 """
-
【注意】:uplicated和drop_duplicates默认都是保留第一个观测到的值。传入参数keep='last’将会返回最后一个
-
使用函数或映射进行数据转换
基于DataFrame中的数组、列或列中的数值进行一些转换
map是一种可以便捷执行按元素转换及其他清洗相关操作的方法
收集到的基于肉类的假设数据
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]})
-
添加一列用于表明每种食物的动物肉类型,先写下一个食物和肉类的映射
meat_to_animal = { 'bacon': 'pig', 'pulled pork': 'pig', 'pastrami': 'cow', 'corned beef': 'cow', 'honey ham': 'pig', 'nova lox': 'salmon' }
-
使用Series的str.lower方法将每个值都转换为小写
lowercased = data['food'].str.lower()
-
Series的map方法接收一个函数或一个包含映射关系的字典型对象
data['animal'] = lowercased.map(meat_to_animal) data
总结:传入一个能够完成所有工作的函数
data['food'].map(lambda x: meat_to_animal[x.lower()])
替代值
- 使用replace方法更灵活地替代缺失值:
data.replace(-999,np.nan)
- 一次替代多个值,传入一个列表和一个替代值:
data.replace([-999,-1000],np.nan)
- 将不同的值替换为不同的值,可以传入替代值的列表:
data.replace([-999, -1000], [np.nan, 0])
;也可通过字典传递:data.replace({-999: np.nan, -1000: 0})
data.replace方法与data.str.replace方法是不同的
data.str. replace是对字符串进行按元素替代的
重命名轴索引
数据
-
修改轴
transform = lambda x : x[:4].upper() data.index.map(transform) # Index(['OHIO', 'COLO', 'NEW '], dtype='object') # 赋值给index,修改DataFrame data.index = data.index.map(transform) data
-
rename:创建数据集转换后的版本,并且不修改原有的数据集
data.rename(index=str.title,columns=str.upper) # rename可以结合字典型对象使用,为轴标签的子集提供新的值 data.rename(index={'OHIO':'INDIANA'}, columns={'three':'peekaboo'})
想要修改原有的数据,传入
inplace=True
离散化和分箱
连续值经常需要离散化,或者分离成”箱子“进行分析
假设研究一组人群的数据,将他们进行分组,放入离散的年龄框中
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
-
使用pandas中的cut将这些年龄分为18~25、26~35、36~60以及61及以上等若干组
bins = [18,25,35,60,100] cats = pd.cut(ages,bins) cats # 特殊的Categorical对象
返回值:一个特殊的Categorical对象,可当作表示箱名字的字符串数组,在内部包含一个categories(类别)数组,指定了不同的类别名称以及codes属性的ages(年龄)数据标签
cats.codes # array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8) cats.categories # IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]], dtype='interval[int64, right]') pd.value_counts(cats) # 对pandas.cut的结果中的箱数量的计数
-
小括号表示边开放,中括号表示封闭;通过传递right=False来改变哪一边是封闭的
pd.cut(ages,[18,26,36,61,100],right=False)
-
向labels选项传递一个列表或数组来传入自定义的箱名
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior'] pd.cut(ages, bins, labels=group_names) """ [[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36, 61), [36, 61), [26, 36)] Length: 12 Categories (4, interval[int64, left]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)] """
-
传给cut整数个的箱来代替显示的箱边,自动根据数据最值计算等长的箱
# 均匀分布的数据被切成四份 data = np.random.rand(20) pd.cut(data,4,precision=2) # precision=2将十进制精度限制在两位
-
qcut基于样本分位数进行分箱,可以使用qcut获得等长的箱
data = np.random.randn(1000) # Normally distributed cats = pd.qcut(data, 4) # Cut into quartiles cats pd.value_counts(cats) # 每个箱数量都是250
可以传入自定义的分位数(0和1之间的数据,包括边)
pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
检测和过滤异常值
-
举例
# 具有正态分布数据的DataFrame data = pd.DataFrame(np.random.randn(1000,4)) # 假设你想要找出一列中绝对值大于三的值 col = data[2] col[np.abs(col) > 3] # 选出所有值大于3或小于-3的行,对布尔值DataFrame使用any方法 data[(np.abs(data)>3).any(1)] # 限制-3到3之间的数值 data[np.abs(data)>3] = np.sign(data) * 3 data.describe()
-
np.sign(data)根据数据中的值的正负分别生成1和-1的数值:
np.sign(data).head()
置换和随机抽样
使用numpy.random.permutation对DataFrame中的Series或行进行置换(随机重排序)是非常方便的
-
调用permutation时可根据轴长度产生一个新顺序的整数数组
df = pd.DataFrame(np.arange(5*4).reshape((5,4))) sampler = np.random.permutation(5) sampler # array([1, 4, 3, 2, 0]) # 整数数组可以用在基于iloc的索引或等价的take函数中 df.take(sampler)
-
选出不含有替代值的随机子集,可以使用sample方法
df.sample(n=3) # 生成带有替代值的样本(允许有重复选择),将replace=True传入sample方法 choices = pd.Series([5,7,-1,6,4]) draws = choices.sample(n=10,replace=True) draws
计算指标、虚拟变量
将分类变量转换为“虚拟”或“指标”矩阵
-
如果DataFrame中的一列有k个不同的值,则可以衍生一个k列的值为1和0的矩阵或DataFrame(pandas有一个get_dummies函数用于实现该功能)
df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'data1': range(6)}) pd.get_dummies(df['key']) # 在指标DataFrame的列上加入前缀,然后与其他数据合并 dummies = pd.get_dummies(df['key'], prefix='key') df_with_dummy = df[['data1']].join(dummies) df_with_dummy
-
多成员构建指标变量
-
DataFrame中的一行属于多个类别,则情况略为复杂
-
每个电影流派添加指标变量
-
取出所有不同的流派的列表
all_genres = [] for x in movies.genres: all_genres.extend(x.split('|')) genres = pd.unique(all_genres) genres
-
用全0的DataFrame构建指标
zero_matrix = np.zeros((len(movies),len(genres))) dummies = pd.DataFrame(zero_matrix,columns=genres) # 遍历每一部电影,将dummies每一行的条目设置为1 # 使用dummies.columns来计算每一个流派的列指标 gen = movies.genres[0] gen.split('|') dummies.columns.get_indexer(gen.split('|')) # array([0, 1, 2], dtype=int64) for i,gen in enumerate(movies.genres): indices = dummies.columns.get_indexer(gen.split('|')) dummies.iloc[i,indices] = 1 # 将结果与movies进行联合 movies_windic = movies.join(dummies.add_prefix('Genre_')) movies_windic.iloc[0]
-
-
对于大数据的快速方法
# 直接将数据写为NumPy数组的底层函数,然后将结果封装进DataFrame # get_dummies与cut等离散化函数结合使用 np.random.seed(12345) # 置随机种子以确保示例的确定性 values = np.random.rand(10) values # array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503, # 0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987]) bins = [0, 0.2, 0.4, 0.6, 0.8, 1] pd.get_dummies(pd.cut(values, bins))
-
7.3 字符串操作
pandas允许你将字符串和正则表达式简洁地应用到整个数组上
还能处理缺失值带来的困扰
字符串对象方法
-
一个逗号分隔的字符串可以使用split方法拆分成多块
val = 'a,b, guido' val.split(',') #val = 'a,b, guido'a
split常和strip一起使用,用于清除空格(包括换行)
val = 'a,b, guido' val.split(',') #val = 'a,b, guido'a pieces = [x.strip() for x in val.split(',')] pieces # ['a', 'b', 'guido'] # 子字符串可以使用加法与两个冒号分隔符连接在一起 first ,second , third=pieces first+'::'+second+'::'+third # 'a::b::guido' # 更通用方法 '::'.join(pieces)
-
尽管index和find也可,Python的in关键字是检测子字符串的最佳方法
index在字符串没有找到时会抛出一个异常(而find是返回-1)
'guido' in val # True val.index(',') # 1 val.find(':') # -1
-
count返回子字符串出现的次数:
val.count(‘,’)
-
replace将用一种模式替代另一种模式。它通常也用于传入空字符串来删除某个模式
val.replace(',','::') # 'a::b:: guido' val.replace(',',' ') # 'a b guido'
-
Python内建字符串方法
正则表达式
一种在文本中灵活查找或匹配(通常更为复杂的)字符串模式的方法。单个表达式通常被称为regex,是根据正则表达式语言形成的字符串.
-
Python的re模块是将正则表达式应用到字符串上的库
-
re模块的三个主题
- 模式匹配
- 替代
- 拆分
-
描述一个或多个空白字符的正则表达式是**\s+**
# # 将含有多种空白字符(制表符、空格、换行符)的字符串拆分开 import re text = "foo bar\t baz \tqux" re.split('\s+', text) # ['foo', 'bar', 'baz', 'qux']
-
用re.compile自行编译,形成一个可复用的正则表达式对象
regex = re.compile('\s+') regex.split(text) # ['foo', 'bar', 'baz', 'qux']
-
使用findall获得所有匹配正则表达式模式的列表
regex.findall(text) # [' ', '\t ', ' \t']
-
-
在正则表达式中避免转义符\的影响,可以使用原生字符串语法,比如
r'C:\x'
或者用等价的’C:\\x'
-
使用re.compile创建一个正则表达式对象,可以将相同的表达式应用到多个字符串上,这样有利于节约CPU周期
-
match、search、findall
-
findall返回字符串中所有匹配项
-
search仅返回第一个匹配项
-
match更为严格,只在字符串的起始位置进行匹配
-
例子:识别大部分电子邮件地址的正则表达式
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 makes the regex case-insensitive regex = re.compile(pattern, flags=re.IGNORECASE) # 使用findall会生成一个电子邮件地址的列表 regex.findall(text) # ['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com'] # search返回第一个匹配到的电子邮件地址 m =regex.search(text) m # <re.Match object; span=(5, 20), match='dave@google.com'> # 匹配对象只能告诉我们模式在字符串中起始和结束的位置 text[m.start():m.end()] # 'dave@google.com' # regex.match只在模式出现于字符串起始位置时进行匹配,如果没有匹配到,返回None print(regex.match(text)) # None
-
-
sub会返回一个新的字符串,原字符串会被新的字符串替代
print(regex.sub('REDACTED',text)) """ Dave REDACTED Steve REDACTED Rob REDACTED Ryan REDACTED """
-
将电子邮件地址分为三个部分:用户名、域名和域名后缀,可以用括号将模式包起来
pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})' regex = re.compile(pattern, flags=re.IGNORECASE) # 使用修改后的对象的groups方法,返回的是模式组件的元组 m = regex.match('wesm@bright.net') m.groups() # ('wesm', 'bright', 'net')
-
模式可以分组时,findall返回的是包含元组的列表
regex.findall(text) """ [('dave', 'google', 'com'), ('steve', 'gmail', 'com'), ('rob', 'gmail', 'com'), ('ryan', 'yahoo', 'com')] """
-
sub也可以使用特殊符号,如\1和\2,访问每个匹配对象中的分组
print(regex.sub(r'Username: \1,Domain: \2,Suffix: \3',text)) """ Dave Username: dave,Domain: google,Suffix: com Steve Username: steve,Domain: gmail,Suffix: com Rob Username: rob,Domain: gmail,Suffix: com Ryan Username: ryan,Domain: yahoo,Suffix: com """
-
正则表达式方法
pandas中的向量化字符串函数
-
包含字符串的列有时会含有缺失数据
可以使用data.map将字符串和有效的正则表达式方法(以lambda或其他函数的方式传递)应用到每个值上,但是在NA(null)值上会失败
-
Series有面向数组的方法用于跳过NA值的字符串操作。这些方法通过Series的str属性进行调用
- str.contains来检查每个电子邮件地址是否含有’gmail’:
data.str.contains(‘gmail’)
- str.contains来检查每个电子邮件地址是否含有’gmail’:
-
正则表达式也可以结合任意的re模块选项使用,例如IGNORECASE
pattern data.str.findall(pattern, flags=re.IGNORECASE) """ Dave [(dave, google, com)] Steve [(steve, gmail, com)] Rob [(rob, gmail, com)] Wes NaN dtype: object """
-
进行向量化元素检查
-
使用str.get或在str属性内部索引
matches = data.str.match(pattern,flags=re.IGNORECASE)
-
要访问嵌入式列表中的元素,将索引传递给这些函数中的任意一个
matches.str.get(1) matches.str[0]
-
使用字符串切片的类似语法进行向量化切片
data.str[5] """ Dave dave@ Steve steve Rob rob@g Wes NaN dtype: object """
-
-
部分向量化字符串方法列表