# 第5章 Pandas高级操作
# 在数据分析和数据建模的过程中需要对数据进行清洗和整理等工作,有时需要对数据增删字段。本章将介绍Pandas对数据的复杂查询、数据类型转换、数据排序、数据的修改、数据迭代以及函数的使用。
# 5.1 复杂查询
# 第4章介绍了.loc[]等几个简单的数据筛选操作,但实际业务需求往往需要按照一定的条件甚至复杂的组合条件来查询数据。本节将介绍如何发挥Pandas数据筛选的无限可能,随心所欲地取用数据。
# 5.1.1 逻辑运算
# 类似于Python的逻辑运算,我们以DataFrame其中一列进行逻辑计算,会产生一个对应的由布尔值组成的Series,真假值由此位上的数据是否满足逻辑表达式决定。例如下例中索引为0的数据值为89,大于
# 36,所以最后值为True。
# # Q1成绩大于36
import numpy as np
import pandas as pd
df = pd.read_excel('https://www.gairuo.com/file/data/dataset/team.xlsx')
df.Q1 > 36
# 一个针对索引的逻辑表达式会产生一个array类型数组,该数组由布尔值组成。根据逻辑表达式,只有索引为1的值为True,其余全为False。
# 索引等于1
df.index == 1
# 再看一下关于DataFrame的逻辑运算,判断数值部分的所有值是否大于60,满足表达式的值显示为True,不满足表达式的值显示为False。
# df.loc[:,'Q1':'Q4']部分只取数字部分,否则会因字符无大于运算而报错
df.loc[:,'Q1':'Q4'] > 60
# 除了逻辑运算,Pandas还支持组合条件的Python位运算:
# Q1成绩不小于60分,并且是C组成员
~(df.Q1 < 60) & (df['team'] == 'C')
# 5.1.2 逻辑筛选数据
# 切片([])、.loc[]和.iloc[]均支持上文所介绍的逻辑表达式。通过逻辑表达式进行复杂条件的数据筛选时需要注意,表达式输出的结果必须是一个布尔序列或者符合其格式要求的数据形式。
# 例如,df.iloc[1+1]和df.iloc[lambda df: len(df)-1]计算出一个数值,符合索引的格式,df.iloc[df.index==8]返回的是一个布尔序列,df.iloc[df.index]返回的是一个索引,它们都是有效的表达式。
# 以下是切片([])的一些逻辑筛选的示例:
df[df['Q1'] == 8] # Q1等于8
df[~(df['Q1'] == 8)] # 不等于8
df[df.name == 'Ben'] # 姓名为Ben
df[df.Q1 > df.Q2]
# 以下是.loc[]和.lic[]的一些示例:
# 表达式与切片一致
df.loc[df['Q1'] > 90, 'Q1':] # Q1大于90,只显示Q1
df.loc[(df.Q1 > 80) & (df.Q2 < 15)] # and关系
df.loc[(df.Q1 > 90) | (df.Q2 < 90)] # or关系
df.loc[df['Q1'] == 8] # 等于8
df.loc[df.Q1 == 8] # 等于8
df.loc[df['Q1'] > 90, 'Q1':] # Q1大于90,显示Q1及其后所有列
# 需要注意的是在进行或(|)、与(&)、非(~)运算时,各个独立逻辑表达式需要用括号括起来。
# any和all对逻辑计算后的布尔序列再进行判断,序列中所有值都为True时all才返回True,序列中只要有一个值为True时any就返回True。
# 它们还可以传入axis参数的值,用于指定判断的方向,与Pandas的axis参数整体约定一样,默认为0列方向,传入1为行方向。利用这两个方法,我们可以对整体数据进行逻辑判断,例如:
# Q1、Q2成绩全为超过80分的
df[(df.loc[:,['Q1','Q2']] > 80).all(1)]
print(df[(df.loc[:,['Q1','Q2']] > 80).all(1)])
# Q1、Q2成绩至少有一个超过80分的
# df[(df.loc[:,['Q1','Q2']] > 80).any(1)] # 这里存在函数相互调用的问题。是函数调用sellf与类内函数相互调用的一个结果,有一个函数没有被调用的情况可导致问题。
# 上例对两个列整体先做逻辑计算得到一个两列的布尔序列,然后用all和any在行方向上做逻辑计算。
# 5.1.3 函数筛选
# 可以在表达式处使用lambda函数,默认变量是其操作的对象。如果操作的对象是一个DataFrame,那么变量就是这个DataFrame;如果是一个Series,那么就是这个Series。
# 可以看以下例子,s就是指df.Q1这个Series:
# 查询最大索引的值
df.Q1[lambda s: max(s.index)] # 值为21
# 计算最大值
max(df.Q1.index) # 99
df.Q1[df.index==99]
# 下面是一些示例:
df[lambda df: df['Q1'] == 8] # Q1为8的
df.loc[lambda df: df.Q1 == 8, 'Q1':'Q2'] # Q1为8的,显示 Q1、Q2
df.loc[:, lambda df: df.columns.str.len()==4] # 由真假值组成的序列
df.loc[:, lambda df: [i for i in df.columns if 'Q' in i]] # 列名列表
df.iloc[:3, lambda df: df.columns.str.len()==2] # 由真假值组成的序列
# 5.1.4 比较函数
# Pandas提供了一些比较函数,使我们可以将逻辑表达式替换为函数形式。
# # 以下相当于 df[df.Q1 == 60]
df[df.Q1.eq(60)]
# 除了.eq(),还有:
# df = input(df)
# df = int(df)
# df.ne() # 不等于 !=
#df.le() # 小于等于 <= # input()返回的数据类型是str,不能直接和整数进行比较,必须先把str换成整数,使用int()方法因此,将input变量转换为int型即可。
# print(df.le())
# df.lt() # 小于 <
# df.ge() # 大于等于 >=
# df.gt() # 大于 >
# 使用示例如下:
# df[df.Q1.ne(89)] # Q1不等于89
# print(df[df.Q1.ne(89)])
# df.loc[df.Q1.gt(90) & df.Q2.lt(90)] # and关系,Q1>90,Q2<90
# 这些函数可以传入一个定值、数列、布尔序列、Series或DataFrame,来与原数据比较。
# 另外还有一个. isin()函数,用于判断数据是否包含指定内容。可以传入一个列表,原数据只需要满足其中一个存在即可;也可以传入一个字典,键为列名,值为需要匹配的值,以实现按列个性化匹配存在值。
# isin
df[df.team.isin(['A','B'])] # 包含A、B两组的
df[df.isin({'team': ['C', 'D'], 'Q1':[36,93]})] # 复杂查询,其他值为NaN
# 5.1.5 查询df.query()
# df.query(expr)使用布尔表达式查询DataFrame的列,表达式是一个字符串,类似于SQL中的where从句,不过它相当灵活。
df.query('Q1 > Q2 > 90') # 直接写类型SQL where语句
df.query('Q1 + Q2 > 180')
df.query('Q1 == Q2')
df.query('(Q1<50) & (Q2>40) and (Q3>90)')
df.query('Q1 > Q2 > Q3 > Q4')
df.query('team != "C"')
df.query('team not in ("E","A","B")')
# 对于名称中带有空格的列,可以使用反引号引起来
# df.query('B == `team`')
# 还支持使用@符引入变量:
# 支持传入变量,如大于平均分40分的
a = df.Q1.mean()
df.query('Q1 > @a+40')
df.query('Q1 > `Q2`+@a')
# df.eval()与df.query()类似,也可以用于表达式筛选:
# df.eval()用法与df.query类似
# df[df.eval("Q1 > 90 > Q3 > 10")]
# df[df.eval("Q1 > `Q2`+@a")]
# 5.1.6 筛选df.filter()
# df.filter()可以对行名和列名进行筛选,支持模糊匹配、正则表达式。
df.filter(items=['Q1', 'Q2']) # 选择两列
df.filter(regex='Q', axis=1) # 列名包含Q的列
df.filter(regex='e$', axis=1) # 以e结尾的列
df.filter(regex='1$', axis=0) # 正则,索引名以1结尾
df.filter(like='2', axis=0) # 索引中有2的
# 索引中以2开头、列名有Q的
df.filter(regex='^2', axis=0).filter(like='Q', axis=1)
# 5.1.7 按数据类型查询
# Pandas提供了一个按列数据类型筛选的功能df.select_dtypes(include=None, exclude=None),它可以指定包含和不包含的数据类型,如果只有一个类型,传入字符;如果有多个类型,传入列表。
df.select_dtypes(include=['float64']) # 选择float64型数据
df.select_dtypes(include='bool')
df.select_dtypes(include=['number']) # 只取数字型
df.select_dtypes(exclude=['int']) # 排除int类型
df.select_dtypes(exclude=['datetime64'])
# 如果没有满足条件的数据,会返回一个仅有索引的DataFrame。
# 5.1.8 小结
# 本节介绍了如何实现复杂逻辑的数据查询需求,复杂的数据查询功能是Pandas的杀手锏,这些功能Excel实现起来会比较困难,有些甚至无法实现,这正是Pandas的优势所在。
# 5.2 数据类型转换
# 在开始数据分析前,我们需要为数据分配好合适的类型,这样才能够高效地处理数据。不同的数据类型适用于不同的处理方法。之前的章节中介绍过,加载数据时可以指定数据各列的类型:
# data没有指定。
# 对所有字段指定统一类型
# df = pd.DataFrame(data, dtype='float32')
# 对每个字段分别指定
# df = pd.read_excel(data, dtype={'team': 'string', 'Q1': 'int32'})
# 本节来介绍如何把数据转换成我们所期望的类型。
# 5.2.1 推断类型
# Pandas可以用以下方法智能地推断各列的数据类型,会返回一个按推断修改后的DataFrame。如果需要使用这些类型的数据,可以赋值替换。
# 自动转换合适的数据类型
df.infer_objects() # 推断后的DataFrame
df.infer_objects().dtypes
# 推荐这个新方法,它支持string类型
df.convert_dtypes() # 推断后的DataFrame
df.convert_dtypes().dtypes
# 5.2.2 指定类型
# pd.to_XXX系统方法可以将数据安全转换,errors参数可以实现无法转换则转换为兜底类型:
# 按大体类型推定
m = [1, 2, 3]
# s = pd.to_numeric(s) # 转成数字 原因就是没有s这个参数
pd.to_datetime(m) # 转成时间
print(pd.to_datetime(m))
pd.to_timedelta(m) # 转成时间差
pd.to_datetime(m, errors='coerce') # 错误处理
pd.to_numeric(m, errors='ignore')
# pd.to_numeric(m, errors='coerce').fillna(0) # 兜底填充
# pd.to_datetime(df[['year', 'month', 'day']]) # 组合成日期
# 转换为数字类型时,默认返回的dtype是float64还是int64取决于提供的数据。使用downcast参数获得向下转换后的其他类型。
# 最低期望
pd.to_numeric(m, downcast='integer') # 至少为有符号int数据类型
# array([1, 2, 3], dtype=int8)
pd.to_numeric(m, downcast='signed') # 同上
# array([1, 2, 3], dtype=int8)
pd.to_numeric(m, downcast='unsigned') # 至少为无符号int数据类型
# array([1, 2, 3], dtype=uint8)
pd.to_numeric(m, downcast='float') # 至少为float浮点类型
# array([1., 2., 3.], dtype=float32)
# 可以应用在函数中:
df = df.select_dtypes(include='number')
# 应用函数
df.apply(pd.to_numeric)
# 5.2.3 类型转换astype()
# astype()是最常见也是最通用的数据类型转换方法,一般我们使用astype()操作数据转换就可以了。
df.Q1.astype('int32').dtypes
# dtype('int32')
df.astype({'Q1': 'int32','Q2': 'int32'}).dtypes
# 以下是一些使用示例:
df.index.astype('int64') # 索引类型转换
df.astype('int32') # 所有数据转换为int32
# df.astype({'col1': 'int32'}) # 指定字段转指定类型
'''
s.astype('int64')
s.astype('int64', copy=False) # 不与原数据关联
s.astype(np.uint8)
df['name'].astype('object')
data['Q4'].astype('float')
s.astype('datetime64[ns]')
data['状态'].astype('bool')
# 当数据的格式不具备转换为目标类型的条件时,需要先对数据进行处理。例如"89.3%"是一个字符串,要转换为数字,要先去掉百分号:
# 将"89.3%"这样的文本转为浮点数
data.rate.apply(lambda x: x.replace('%', '')).astype('float')/100
'''
# 5.2.4 转为时间类型
# 我们通常使用pd.to_datetime()和s.astype('datetime64[ns]')来做时间类型转换,第14章会专门介绍这两个函数。
t = pd.Series(['20200801', '20200802'])
print(t)
pd.to_datetime(t)
t.astype('datetime64[ns]')
print(t.astype('datetime64[ns]'))
# 5.2.5 小结
# 本节介绍的数据类型匹配和转换是高效处理数据的前提。每种数据类型都有自己独有的方法和属性,所以数据类型的转换是非常有必要的。
# 5.3 数据排序
# 数据排序是指按一定的顺序将数据重新排列,帮助使用者发现数据的变化趋势,同时提供一定的业务线索,还具有对数据纠错、分类等作用。
# 本节将介绍一些Pandas用来进行数据排序的方法。
# 5.3.1 索引排序
# df.sort_index()实现按索引排序,默认以从小到大的升序方式排列。如希望按降序排序,传入ascending=False:
# 索引降序
df.sort_index(ascending=False)
print(df.sort_index(ascending=False))
# 在列索引方向上排序
df.sort_index(axis=1, ascending=False)
print(df.sort_index(axis=1, ascending=False))
'''
s.sort_index() # 升序排列
df.sort_index() # df也是按索引进行排序
df.team.sort_index()
s.sort_index(ascending=False) # 降序排列
s.sort_index(inplace=True) # 排序后生效,改变原数据
# 索引重新0-(n-1)排,很有用,可以得到它的排序号
s.sort_index(ignore_index=True)
s.sort_index(na_position='first') # 空值在前,另'last'表示空值在后
s.sort_index(level=1) # 如果多层,排一级
s.sort_index(level=1, sort_remaining=False) # 这层不排
'''
# 行索引排序,表头排序
df.sort_index(axis=1) # 会把列按列名顺序排列
# df.reindex()指定自己定义顺序的索引,实现行和列的顺序重新定义
df = pd.DataFrame({
'A': [1,2,4],
'B': [3,5,6]
}, index=['a', 'b', 'c'])
print(df)
# 按要求重新指定索引顺序
df.reindex(['c', 'b', 'a'])
# 指定列顺序
df.reindex(['B', 'A'], axis=1)
# 5.3.2 数值排序
df1 = pd.read_excel('https://www.gairuo.com/file/data/dataset/team.xlsx',header = None)
# 数据值的排序主要使用sort_values(),数字按大小顺序,字符按字母顺序。Series和DataFrame都支持此方法:
# df.Q1.sort_values()
# DataFrame需要传入一个或多个排序的列名:
# df.sort_values('Q4')
# 默认排序是升序,但可以指定排序方式,下例先按team升序排列,
# 如遇到相同的team再按name降序排列。
# df.sort_values(by=['team', 'name'], ascending=[True, False]) # KeyError: 'team'
#其他常用方法如下:
'''
s.sort_values(ascending=False) # 降序
s.sort_values(inplace=True) # 修改生效
s.sort_values(na_position='first') # 空值在前
'''
# df按指定字段排列
df1.sort_values(by=['team'])
df1.sort_values('Q1')
# 按多个字段,先排team,在同team内再看Q1
df1.sort_values(by=['team', 'Q1'])
# 全降序
df1.sort_values(by=['team', 'Q1'], ascending=False)
# 对应指定team升Q1降
df1.sort_values(by=['team', 'Q1'], ascending=[True, False])
# 索引重新0-(n-1)排
df1.sort_values('team', ignore_index=True)
# 5.3.3 混合排序
# 有时候需要用索引和数据值混合排序。下例中假如name是索引,我
# 们需要先按team排名,再按索引排名:
df1.set_index('name', inplace=True) # 设置name为索引
df1.index.names = ['s_name'] # 给索引起名
df1.sort_values(by=['s_name', 'team']) # 排序
# 以下方法也可以实现上述需求,不过要注意顺序:
# 设置索引,按team排序,再按索引排序
# df.set_index('name').sort_values('team').sort_index()
# 另外,还可以使用df.reindex(),通过给定新的索引方式来排名,按照这个思路可以实现人工指定任意顺序。
# 按姓名排序后取出排名后的索引列表
# df.name.sort_values().index
# 将新的索引应用到数据中
# df.reindex(df.name.sort_values().index)
# 5.3.4 按值大小排序
# nsmallest()和nlargest()用来实现数字列的排序,并可指定返回的个数:
# 先按Q1最小在前,如果相同,Q2小的在前
# df.nsmallest(5, ['Q1', 'Q2'])
# 以上显示了前5个最小的值,仅支持数字类型的排序。下面是几个
# 其他示例:
# s.nsmallest(3) # 最小的3个
# s.nlargest(3) # 最大的3个
# # 指定列
df1.nlargest(3, 'Q1')
df1.nlargest(5, ['Q1', 'Q2'])
df1.nsmallest(5, ['Q1', 'Q2'])
# 5.3.5 小结
# 本节介绍了索引的排序、数值的排序以及索引和数值混合的排序方法。在实际需求中,更加复杂的排序可能需要通过计算增加辅助列来实现。
# 5.4 添加修改
# 对数据的修改、增加和删除在数据整理过程中时常发生。修改的情况一般是修改错误,还有一种情况是格式转换,如把中文数字修改为阿拉伯数字。修改也会涉及数据的类型修改。
# 删除一般会通过筛选的方式,筛选完成后将最终的结果重新赋值给变量,达到删除的目的。增加行和列是最为常见的操作,数据分析过程中会计算出新的指标以新列展示。
# 5.4.1 修改数值
# 在Pandas中修改数值非常简单,先筛选出需要修改的数值范围,再为这个范围重新赋值。
df1.iloc[0,0] # 查询值
df1.iloc[0,0] = 'Lily' # 修改值
print(df.iloc[0,0])
# 以上修改了一个具体的数值,还可以修改更大范围的值:
# # 将小于60分的成绩修改为60
df1[df1.Q1 < 60] = 60
# # 查看
print(df1.Q1)
# 以上操作df变量的内容被修改,这里指定的是一个定值,所有满足条件的数据均被修改为这个定值。还可以传一个同样形状的数据来修改值:
# 生成一个长度为100的列表
v = [1, 3, 5, 7, 9] * 20
print(v)
# 修改
df.Q1 = v
# 查看新值
print(df1.Q1)
# 对于修改DataFrame,会按对应的索引位进行修改:
# 筛选数据
df1.loc[1:2, 'Q1':'Q2'] # TypeError: cannot do slice indexing on Index with these indexers [1] of type int # raise KeyError(key) KeyError: 'team'
# # 指定修改的目的数据
df1 = pd.DataFrame({'Q1':[1,2,3],'Q2':[4,5,6]})
print(df1)
# 执行修改
df.loc[1:3, 'Q1':'Q2'] = df1
# 查看结果
# 执行修改
df.loc[1:3, 'Q1':'Q2']
# 5.4.2 替换数据
# replace方法可以对数据进行批量替换:
# s.replace(0, 5) # 将列数据中的0换为5
df.replace(0, 5) # 将数据中的所有0换为5
df.replace([0, 1, 2, 3], 4) # 将0~3全换成4
df.replace([0, 1, 2, 3], [4, 3, 2, 1]) # 对应修改
# {'pad', 'ffill', 'bfill', None} 试试
# s.replace([1, 2], method='bfill') # 向下填充
df.replace({0: 10, 1: 100}) # 字典对应修改
df.replace({'Q1': 0, 'Q2': 5}, 100) # 将指定字段的指定值修改为100
df.replace({'Q1': {0: 100, 4: 400}}) # 将指定列里的指定值替换为另一个指定的值
# 使用正则表达式
df.replace(to_replace=r'^ba.$', value='new', regex=True)
df.replace({'A': r'^ba.$'}, {'A': 'new'}, regex=True)
df.replace(regex={r'^ba.$': 'new', 'foo': 'xyz'})
df.replace(regex=[r'^ba.$', 'foo'], value='new')
# 5.4.3 填充空值
# fillna对空值填入指定数据,通常应用于数据清洗。还有一种做法是删除有空值的数据,后文会介绍。
df.fillna(0) # 将空值全修改为0
# # {'backfill', 'bfill', 'pad', 'ffill', None}, 默认为None
df.fillna(method='ffill') # 将空值都修改为其前一个值
values = {'A': 0, 'B': 1, 'C': 2, 'D': 3}
df.fillna(value=values) # 为各列填充不同的值
df.fillna(value=values, limit=1) # 只替换第一个
# 5.4.4 修改索引名
# 修改索引名最简单也最常用的办法就是将df.index和df.columns重新赋值为一个类似于列表的序列值,这会将其覆盖为指定序列中的名称。
# 使用df.rename和df.rename_axis对轴名称进行修改。以下案例将列名team修改为class:
df.rename(columns={'team':'class'})
# 常用方法如下:
df.rename(columns={"Q1": "a", "Q2": "b"}) # 对表头进行修改
df.rename(index={0: "x", 1: "y", 2: "z"}) # 对索引进行修改
df.rename(index=str) # 对类型进行修改
df.rename(str.lower, axis='columns') # 传索引类型
df.rename({1: 2, 2: 4}, axis='index')
# 对索引名进行修改
# s.rename_axis("animal")
df.rename_axis("animal") # 默认是列索引
df.rename_axis("limbs", axis="columns") # 指定行索引
# 索引为多层索引时可以将type修改为class
df.rename_axis(index={'type': 'class'})
# 可以用set_axis进行设置修改
# s.set_axis(['a', 'b', 'c'], axis=0)
df.set_axis(['I', 'II'], axis='columns')
df.set_axis(['i', 'ii'], axis='columns', inplace=True)
# 5.4.5 增加列
# 增加列是数据处理中最常见的操作,Pandas可以像定义一个变量一样定义DataFrame中新的列,新定义的列是实时生效的。与数据修改的逻辑一样,新列可以是一个定值,所有行都为此值,也可以是一个同等长度的序列数据,各行有不同的值。接下来我们增加总成绩total列:
# # 四个季度的成绩相加为总成绩
df['total'] = df.Q1 + df.Q2 + df.Q3 + df.Q4
df['total'] = df.sum(1) # 与以上代码效果相同
print(df)
# 还可以在筛选数据时传入一个不存在的列,并为其赋值以增加新列,如df.loc[:, 'QQ'] = 10,QQ列不存在,但我们赋值为10,就会新增加一个名为QQ、值全是10的列。以下是一些更加复杂的案例:
df['foo'] = 100 # 增加一列foo,所有值都是100
df['foo'] = df.Q1 + df.Q2 # 新列为两列相加
df['foo'] = df['Q1'] + df['Q2'] # 同上
# 把所有为数字的值加起来
df['total'] = df.select_dtypes(include=['int']).sum(1)
df['total'] = df.loc[:,'Q1':'Q4'].apply(lambda x: sum(x), axis='columns')
df.loc[:, 'Q10'] = '我是新来的' # 也可以
# 增加一列并赋值,不满足条件的为NaN
df.loc[df.num >= 60, '成绩'] = '合格'
df.loc[df.num < 60, '成绩'] = '不合格'
# 5.4.6 插入列df.insert()
# Pandas提供了insert()方法来为DataFrame插入一个新列。insert()方法可以传入三个主要参数:loc是一个数字,代表新列所在的位置,使用列的数字索引,如0为第一列;第二个参数column为新的列名;最后一个参数value为列的值,一般是一个Series。
# 在第三列的位置上插入新列total列,值为每行的总成绩
df.insert(2, 'total', df.sum(1))
# 如果已经存在相同的数据列,会报错,可传入allow_duplicates=True插入一个同名的列。如果希望新列位于最后,可以在第一个参数位loc传入len(df.columns)。
# 5.4.7 指定列df.assign()
# df.assign(k=v)为指定一个新列的操作,k为新列的列名,v为此列的值,v必须是一个与原数据同索引的Series。今后我们会频繁用到它,它在链式编程技术中相当重要,因此这里专门介绍一下。
# 我们平时在做数据探索分析时会增加一些临时列,如果新列全部使用赋值的方式生成,则会造成原数据混乱,因此就需要一个方法来让我们不用赋值也可以创建一个临时的列。
# 这种思路适用于所有对原数据的操作,建议在未最终确定数据处理方案时,除了必要的数据整理工作,均使用链式方法,我们在学习完所有的常用功能后会专门介绍这个技术。
# 我们把上面的增加总分total列的例子用它来实现一下:
# 增加total列
df.assign(total=df.sum(1))
print(df)
# 再增加一个Q列,它的元素均为定值100,用逗号分隔再增加一个表达式,或者在语句后继续使用assign方法:
# 增加两列
df.assign(total=df.sum(1), Q=100)
df.assign(total=df.sum(1)).assign(Q=100) # 效果同上
# 我们再增加两列name_len和avg,name_len值为name的长度,avg为平均分。这时有了过多的assign语句,为了美观整齐,将代码放在括号里:
# 使用了链式方法
(
df.assign(total=df.sum(1)) # 总成绩
.assign(Q=100) # 目标满分值
.assign(name_len=df.name.str.len()) # 姓名长度
.assign(avg=df.mean(1)) # 平均值
.assign(avg2=lambda d: d.total/4) # 平均值2
)
# 以上是使用了链式方法的典型代码形式,后期会以这种风格进行代码编写。特别要说明的是avg2列的计算过程,因为df实际是没有total这一列的,如果我们需要使用total列,就需要用lambda来调用。
# lambda中第一个变量d是代码执行到本行前的DataFrame内容,可以认为是一个虚拟的DataFrame实体,然后用变量d使用这个DataFrame的数据。作为变量名,d可以替换为其他任意合法的名称,但为了代码可读性,建议使用d,代表它是一个DataFrame。如果是Series,建议使用s。
# 以下是其他一些使用示例:
df.assign(Q5=[100]*100) # 新增加一列Q5
df = df.assign(Q5=[100]*100) # 赋值生效
df.assign(Q6=df.Q2/df.Q1) # 计算并增加Q6
df.assign(Q7=lambda d: d.Q1 * 9 / 5 + 32) # 使用lambda
# 添加一列,值为表达式结果:True或False
df.assign(tag=df.Q1>df.Q2)
# 比较计算,True为1,False为0
df.assign(tag=(df.Q1>df.Q2).astype(int))
# 映射文案
df.assign(tag=(df.Q1>60).map({True:'及格',False:'不及格'}))
# 增加多个
df.assign(Q8=lambda d: d.Q1*5,
Q9=lambda d: d.Q8+1) # Q8没有生效,不能直接用df.Q8
# 5.4.8 执行表达式df.eval()
# df.eval()与之前介绍过的df.query()一样,可以以字符的形式传入表达式,增加列数据。下面以增加总分为例:
# # 传入求总分表达式
df.eval('total = Q1+Q3+Q3+Q4')
# 其他常用方法如下:
df['C1'] = df.eval('Q2 + Q3')
df.eval('C2 = Q2 + Q3') # 计算
a = df.Q1.mean()
df.eval("C3 = `Q3`+@a") # 使用变量
df.eval("C3 = Q2 > (`Q3`+@a)") # 加一个布尔值
df.eval('C4 = name + team', inplace=True) # 立即生效
# 5.4.9 增加行
# 可以使用loc[]指定索引给出所有列的值来增加一行数据。目前我们的df最大索引是99,增加一条索引为100的数据:
# 新增索引为100的数据
df.loc[100] = ['tom', 'A', 88, 88, 88, 88]
# 成功增加了一行,数据变为101行。以下是一些其他用法:
df.loc[101]={'Q1':88,'Q2':99} # 指定列,无数据列值为NaN
df.loc[df.shape[0]+1] = {'Q1':88,'Q2':99} # 自动增加索引
df.loc[len(df)+1] = {'Q1':88,'Q2':99}
# 批量操作,可以使用迭代
rows = [[1,2],[3,4],[5,6]]
for row in rows:
df.loc[len(df)] = row
# 5.4.10 追加合并
# 增加行数据的使用场景相对较少,一般是采用数据追加的模式。数
# 据追加会在后续章节中介绍。
# df.append()可以追加一个新行:
df = pd.DataFrame([[1, 2], [3, 4]], columns=list('AB'))
df2 = pd.DataFrame([[5, 6], [7, 8]], columns=list('AB'))
df.append(df2)
# pd.concat([s1, s2])可以将两个df或s连接起来:
s1 = pd.Series(['a', 'b'])
s2 = pd.Series(['c', 'd'])
pd.concat([s1, s2])
pd.concat([s1, s2], ignore_index=True) # 索引重新编
# 原数索引不变,增加一个一层索引(keys里的内容),变成多层索引
pd.concat([s1, s2], keys=['s1', 's2'])
pd.concat([s1, s2], keys=['s1', 's2'],
names=['Series name', 'Row ID'])
# df同理
pd.concat([df1, df2])
pd.concat([df1, df3], sort=False)
pd.concat([df1, df3], join="inner") # 只连相同列
pd.concat([df1, df4], axis=1) # 连接列
# 5.4.11 删除
# 删除有两种方法,一种是使用pop()函数。使用pop(),Series会删除指定索引的数据同时返回这个被删除的值,DataFrame会删除指定列并返回这个被删除的列。以上操作都是实时生效的。
# # 删除索引为3的数据
# s.pop(3)
# # 93
# s
# 删除Q1列
df.pop('Q1') #
print(df)
# 还有一种方法是使用反选法,将需要的数据筛选出来赋值给原变量,最终实现删除。
# 5.4.12 删除空值
# 在一些情况下会删除有空值、缺失不全的数据,df.dropna可以执行这种操作:
# df.dropna() # 一行中有一个缺失值就删除
# df.dropna(axis='columns') # 只保留全有值的列
# df.dropna(how='all') # 行或列全没值才删除
# df.dropna(thresh=2) # 至少有两个空值时才删除
# df.dropna(inplace=True) # 删除并使替换生效
# 5.4.13 小结
# 我们可以利用数据查询的功能,确定未知的数据位置并将其作为变量,再将数据内容赋值给它,从而完成数据的添加。
# 数据修改也类似,它通过查询的已知的数据位置,重新赋值以覆盖原有数据。以上操作会让原数据变量发生改变,但在数据探索阶段存在各种操作实验,
# 原数据变量变化会造成频繁的撤销操作,带来不便,所以Pandas引入了df.assign()等操作。
# 5.5 高级过滤
# 本节介绍几个非常好用的数据过滤输出方法,它们经常用在一些复
# 杂的数据处理过程中。df.where()和df.mask()通过给定的条件对原数据是
# 否满足条件进行筛选,最终返回与原数据形状相同的数据。为了方便讲
# 解,我们仅取我们的数据集的数字部分,即只有Q1到Q4列:
# # 只保留数字类型列
df = df.select_dtypes(include='number')
# 5.5.1 df.where()
# df.where()中可以传入一个布尔表达式、布尔值的
# Series/DataFrame、序列或者可调用的对象,然后与原数据做对比,返回
# 一个行索引与列索引与原数据相同的数据,且在满足条件的位置保留原
# 值,在不满足条件的位置填充NaN。
# # 数值大于70
df.where(df > 70)
print(df)
# 传入一个可调用对象,这里我们用lambda:
# Q1列大于50
df.where(lambda d: d.Q1>50)
# 条件为一个布尔值的Series:
# 传入布尔值Series,前三个为真
df.Q1.where(pd.Series([True]*3))
# 上例中不满足条件的都返回为NaN,我们可以指定一个值或者算法来替换NaN:
# 大于等于60分的显示成绩,小于的显示“不及格”
df.where(df>=60, '不及格')
# 给定一个算法:
# c 定义一个数是否为偶数的表达式
c = df%2 == 0
# # 传入c, 为偶数时显示原值减去20后的相反数
df.where(~c, -(df-20))
# 5.5.2 np.where()
# np.where()是NumPy的一个功能,虽然不是Pandas提供的,但可以弥补df.where()的不足,所以有必要一起介绍。
# df.where()方法可以将满足条件的值筛选出来,将不满足的值替换为另一个值,但无法对满足条件的值进行替换,而np.where()就实现了这种功能,达到SQL中if(条件,条件为真的值,条件为假的值)的效果。
# np.where()返回的是一个二维array:
# 小于60分为不及格
np.where(df>=60, '合格', '不合格')
# 可以使用df.where()来应用它:
# 让df.where()中的条件为假,从而应用np.where()的计算结果
df.where(df==9999999, np.where(df>=60, '合格', '不合格'))
#下例是np.where()对一个Series(d.avg为计算出来的虚拟列)进行判断,返回一个包含是、否结果的Series。
(
df.assign(avg=df.mean(1)) # 计算一个平均数
# 通过np.where()及判断平均分是否及格
.assign(及格=lambda d: np.where(d.avg>=60, '是', '否'))
)
# 5.5.3 df.mask()
# df.mask()的用法和df.where()基本相同,唯一的区别是df.mask()将满
# 足条件的位置填充为NaN。
# # 符合条件的为NaN
# df.mask(s > 80)
# 可以指定填充值:
# 对满足条件的位置指定填充值
# df.Q1.mask(s > 80, '优秀')
# df.mask()和df.where()还可以通过数据筛选返回布尔序列:
# 返回布尔序列,符合条件的行值为True
(df.where((df.team=='A') & (df.Q1>60)) == df).Q1
# 返回布尔序列,符合条件的行值为False
(df.mask((df.team=='A') & (df.Q1>60)) == df).Q1
# 5.5.4 df.lookup()
# 语法为df.lookup(行标签,列标签),返回一个numpy.ndarray,标
# 签必须是一个序列。
# # 行列相同数量,返回一个array
df.lookup([1,3,4], ['Q1','Q2','Q3']) # array([36, 96, 61])
df.lookup([1], ['Q1']) # array([36])
# 5.5.5 小结
# 本节介绍了几个非常实用的数据过滤函数,df.where()与df.mask()都
# 可以按条件筛选数据,df.where()将不满足条件的值替换为NaN,
# df.mask()将满足条件的值替换为NaN。np.where()是NumPy的一个方法,
# 在满足条件和不满足条件的情况下都可指定填充值。
# 5.6 数据迭代
# 数据迭代和数据遍历都是按照某种顺序逐个对数据进行访问和操作,在Python中大多由for语句来引导。Pandas中的迭代操作可以将数据按行或者按列遍历,我们可以进行更加细化、个性化的数据处理。
# 5.6.1 迭代Series
# Series本身是一个可迭代对象,Series df.name.values返回array结构数据可用于迭代,不过可直接对Series使用for语句来遍历它的值:
# 迭代指定的列
for i in df.name:
print(i)
# 迭代索引和指定的多列,使用Python内置的zip函数将其打包为可迭代的zip对象:
# 迭代索引和指定的两列
for i,n,q in zip(df.index, df.name, df.Q1):
print(i, n, q)
# 5.6.2 df.iterrows()
# df.iterrows()生成一个可迭代对象,将DataFrame行作为(索引,行
# 数据)组成的Series数据对进行迭代。在for语句中需要两个变量来承接
# 数据:一个为索引变量,即使索引在迭代中不会使用(这种情况可用
# useless作为变量名);另一个为数据变量,读取具体列时,可以使用字
# 典的方法和对象属性的方法。
# # 迭代,使用name、Q1数据
for index, row in df.iterrows():
print(index, row['name'], row.Q1)
# df.iterrows()是最常用、最方便的按行迭代方法。
# 5.6.3 df.itertuples()
# df.itertuples()生成一个namedtuples类型数据,name默认名为Pandas,可以在参数中指定。
for row in df.itertuples():
print(row)
# 以下是一些使用方法示例:
# 不包含索引数据
for row in df.itertuples(index=False):
print(row)
# Pandas(name='Liver', team='E', Q1=89, Q2=21, Q3=24, Q4=64)
# 自定义name
for row in df.itertuples(index=False, name='Gairuo'):# namedtuples
print(row)
# Gairuo(name='Liver', team='E', Q1=89, Q2=21, Q3=24, Q4=64)
# 使用数据
for row in df.itertuples():
print(row.Index, row.name)
# 5.6.4 df.items()
# df.items()和df.iteritems()功能相同,它迭代时返回一个(列名,本列的Series结构数据),实现对列的迭代:
# Series取前三个
for label, ser in df.items():
print(label)
print(ser[:3], end='\n\n')
# 如果需要对Series的数据再进行迭代,可嵌套for循环。
# 5.6.5 按列迭代
# 除了df.items(),如需要迭代一个DataFrame的列,可以直接对DataFrame迭代,会循环得到列名:
# 直接对DataFrame迭代
for column in df:
print(column)
# 再利用df [列名]的方法迭代列:
# 依次取出每个列
for column in df:
print(df[column])
# 可对每个列的内容进行迭代
for column in df:
for i in df[column]:
print(i)
# 可以迭代指定列
for i in df.name:
print(i)
# 只迭代想要的列
l = ['name', 'Q1']
cols = df.columns.intersection(l)
for col in cols:
print (col)
# 5.6.6 小结
# 本节介绍了Pandas各个维度的数据迭代方法,DataFrame和Series本身就是可迭代对象,以上专门的迭代函数为我们提供了十分方便的迭代功能。
# 与df.iterrows()相比,df.itertuples()运行速度会更快一些,推荐在数据量庞大的情况下优先使用。
# 迭代的优势是可以把大量重复的事务按规定的逻辑依次处理,处理逻辑部分的也能随心所欲地去发挥,同时它简单清晰,初学者也很容易理解。
# 如果需要再提升代码的执行效率,就要将逻辑处理代码写成函数,使用Pandas的调用函数方法迭代调用。下节我们将介绍如何调用函数。
# 5.7 函数应用
# 我们知道,函数可以让复杂的常用操作模块化,既能在需要使用时直接调用,达到复用的目的,也能简化代码。Pandas提供了几个常用的调用函数的方法。
# pipe():应用在整个DataFrame或Series上。apply():应用在DataFrame的行或列中,默认为列。applymap():应用在DataFrame的每个元素中。map():应用在Series或DataFrame的一列的每个元素中。
# 5.7.1 pipe()
# Pandas提供的pipe()叫作管道方法,它可以让我们写的分析过程标准化、流水线化,达到复用目标,它也是最近非常流行的链式方法的重要代表。
# DataFrame和Series都支持pipe()方法。pipe()的语法结构为df.pipe(<函数名>, <传给函数的参数列表或字典>)。
# 它将DataFrame或Series作为函数的第一个参数(见图5-1),可以根据需求返回自己定义的任意类型数据。
# pipe()可以将复杂的调用简化,看下面的例子:
# 对df多重应用多个函数
f(g(h(df), arg1=a), arg2=b, arg3=c)
# 用pipe可以把它们连接起来
(df.pipe(h)
.pipe(g, arg1=a)
.pipe(f, arg2=b, arg3=c)
)
# 以下是将'arg2'参数传给函数f,然后作为函数整体接受后面的参数
(df.pipe(h)
.pipe(g, arg1=a)
.pipe((f, 'arg2'), arg1=a, arg3=c)
)
# 函数h传入df的值返回的结果作为函数g的第一个参数值,g同时还传入了参数arg1;再将返回结果作为函数f的第一个参数,最终得到计算结果,这个调用过程显得异常复杂。
# 使用pipe改造后代码逻辑复杂度大大降低,通过链式调用pipe()方法,对数据进行层层处理,大大提高代码的可读性。
# 接下来我们看一下实际案例:
# 定义一个函数,给所有季度的成绩加n,然后增加平均数
# 其中n中要加的值为必传参数
def add_mean(rdf, n):
df = rdf.copy()
df = df.loc[:,'Q1':'Q4'].applymap(lambda x: x+n)
df['avg'] = df.loc[:,'Q1':'Q4'].mean(1)
return df
# 调用
df.pipe(add_mean, 100)
# 函数部分可以使用lambda。下例完成了一个数据筛选需求,lambda的第一个参数为self,即使用前的数据本身,后面的参数可以在逻辑代码中使用。
# 筛选出Q1大于等于80且Q2大于等于90的数据
df.pipe(lambda df_, x, y: df_[(df_.Q1 >= x) & (df_.Q2 >= y)], 80,
# 5.7.2 apply()
# apply()可以对DataFrame按行和列(默认)进行函数处理,也支持
# Series。如果是Series,逐个传入具体值,DataFrame逐行或逐列传入,
# 下例中的函数使用了lambda,将文本转换为全小写:
# 将name全部变为小写
df.name.apply(lambda x: x.lower())
# 下面看一个DataFrame的例子。我们需要计算每个季度的平均成绩,计算方法为去掉一个最高分和一个最低分,剩余成绩的平均值为最终的平均分。
# 去掉一个最高分和一个最低分再算出平均分
def my_mean(s):
max_min_ser = pd.Series([-s.max(), -s.min()])
return s.append(max_min_ser).sum()/(s.count()-2)
# # 对数字列应用函数
df.select_dtypes(include='number').apply(my_mean)
# 分析一下代码:函数my_mean接收一个Series,从此Series中取出最大值和最小值的负值组成一个需要减去的负值Series;
# 传入的Series追加此负值Series,最后对Series求和,求和过程中就减去了两个极值;
# 由于去掉了两个值,分母不能取Series的长度,需要减去2,最终计算出结果。这是函数的代码逻辑。
# 应用函数时,我们只选择数字类型的列,再使用apply调用函数my_mean,执行后,结果返回了每个季度的平均分。
# 希望以此算法计算每个学生的平均成绩,在apply中传入axis=1则每行的数据组成一个Series传入自定义函数中。
# 同样的算法以学生为维度计算
(
df.set_index('name') # 设定name为索引
# .select_dtypes(include='number')
# .apply(my_mean, axis=1) # 横向计算
)
# 由上面的案例可见,直接调用lambda函数非常方便。在今后的数据处理中,我们会经常使用这种操作。
# 以下是一个判断一列数据是否包含在另一列数据中的案例。
# 判断一个值是否在另一个类似列表的列中
df.apply(lambda d: d.s in d.s_list, axis=1) # 布尔序列
df.apply(lambda d: d.s in d.s_list, axis=1).astype(int) # 0 和 1 序
# 它常被用来与NumPy库中的np.where()方法配合使用,如下例:
# 函数,将大于90分数标记为good
fun = lambda x: np.where(x.team=='A' and x.Q1>90, 'good' ,'other')
df.apply(fun, axis=1)
# 同上效果
(df.apply(lambda x: x.team=='A' and x.Q1>90, axis=1)
.map({True:'good', False:'other'})
)
df.apply(lambda x: 'good' if x.team=='A' and x.Q1>90 else '', axis=1)
# 总结一下,apply()可以应用的函数类型如下:
df.apply(fun) # 自定义
df.apply(max) # Python内置函数
df.apply(lambda x: x*2) # lambda
df.apply(np.mean) # NumPy等其他库的函数
# 面介绍到的其他调用函数的方法也适用这个规则。
# 5.7.3 applymap()
# df.applymap()可实现元素级函数应用,即对DataFrame中所有的元素(不包含索引)应用函数处理,如图5-3所示。
# 使用lambda时,变量是指每一个具体的值。
# 计算数据的长度
def mylen(x):
return len(str(x))
df.applymap(lambda x:mylen(x)) # 应用函数
df.applymap(mylen) # 效果同上
# 5.7.4 map()
# map()根据输入对应关系映射值返回最终数据,用于Series对象或DataFrame对象的一列。传入的值可以是一个字典,键为原数据值,值为替换后的值。
# 可以传入一个函数(参数为Series的每个值),还可以传入一个字符格式化表达式来格式化数据内容。
df.team.map({'A':'一班', 'B':'二班','C':'三班', 'D':'四班',}) # 枚举替换
df.team.map('I am a {}'.format)
df.team.map('I am a {}'.format, na_action='ignore')
t = pd.Series({'six': 6., 'seven': 7.})
s.map(t)
# 应用函数
def f(x):
return len(str(x))
df['name'].map(f)
# 5.7.5 agg()
# agg()一般用于使用指定轴上的一项或多项操作进行汇总,可以传入一个函数或函数的字符,还可以用列表的形式传入多个函数。
# 每列的最大值
df.agg('max')
# 将所有列聚合产生sum和min两行
df.agg(['sum', 'min'])
# 序列多个聚合
df.agg({'Q1' : ['sum', 'min'], 'Q2' : ['min', 'max']})
# 分组后聚合
df.groupby('team').agg('max')
df.Q1.agg(['sum', 'mean'])
def mymean(x):
return x.mean()
df.Q2.agg(['sum', mymean])
# 另外,agg()还支持传入函数的位置参数和关键字参数,支持每个列分别用不同的方法聚合,支持指定轴的方向。
# 每列使用不同的方法进行聚合
df.agg(a=('Q1', max),
b=('Q2', 'min'),
c=('Q3', np.mean),
d=('Q4', lambda s:s.sum()+1)
)
# 按行聚合
df.loc[:,'Q1':].agg("mean", axis="columns")
# 利用pd.Series.add方法对所有数据加分,other是add方法的参数
df.loc[:,'Q1':].agg(pd.Series.add, other=10)
# agg()的用法整体上与apply()极为相似。
# 5.7.6 transform()
# DataFrame或Series自身调用函数并返回一个与自身长度相同的数据。
df.transform(lambda x: x*2) # 应用匿名函数
df.transform([np.sqrt, np.exp]) # 调用多个函数
df.transform([np.abs, lambda x: x + 1])
df.transform({'A': np.abs, 'B': lambda x: x + 1})
df.transform('abs')
df.transform(lambda x: x.abs())
# 可以对比下面两个操作:
df.groupby('team').sum()
df.groupby('team').transform(sum)
# B分组后,直接使用计算函数并按分组显示合计数据。使用transform()调用计算函数,返回的是原数据的结构,但在指定位置上显示聚合计算后的结果,这样方便我们了解数据所在组的情况。
# 5.7.7 copy()
# 类似于Python中copy()函数,df.copy()方法可以返回一个新对象,这个新对象与原对象没有关系。
# 当deep = True(默认)时,将创建一个新对象,其中包含调用对象的数据和索引的副本。
# 对副本数据或索引的修改不会反映在原始对象中。当deep = False时,将创建一个新对象而不复制调用对象的数据或索引(仅复制对数据和索引的引用)。
# 原始数据的任何更改都将对浅拷贝的副本进行同步更改,反之亦然。
s = pd.Series([1, 2], index=["a", "b"])
s_1 = s
s_copy = s.copy()
s_1 is s # True
s_copy is s # False
# 5.7.8 小结
# 本节介绍了一些非常实用的DataFrame和Series函数,大量使用自定义函数是我们对Pandas掌握程度的一次升级。熟练使用函数可以帮助我们抽象问题,复用解决方案,同时大大减少代码量。
# 5.8 本章小结
# 本章信息量较大,主要介绍了Pandas的一些高级应用功能。我们可以利用本章介绍的高级筛选技巧对数据进行任意逻辑的查询;
# 利用类型转换功能,将数据转换为方便使用的类型;对数据进行个性化排序,探索数据的变化规律;
# 对数据进行增删修改操作,对异常数据进行修正;以迭代形成编写复杂的数据处理逻辑;利用函数完成重复工作,让代码更加高效。
# 到此,我们已经具备了较为完善的Pandas使用技能,可以解决数据分析中遇到的大多数问题。后续章节中的案例将大量使用这些操作方法。