第7章 文本数据
import pandas as pd
import numpy as np
一、string类型的性质
1. string与object的区别
string类型和object不同之处有三:
① 字符存取方法(string accessor methods,如str.count)会返回相应数据的Nullable类型,而object会随缺失值的存在而改变返回类型
② 某些Series方法不能在string上使用,例如: Series.str.decode(),因为存储的是字符串而不是字节
③ string类型在缺失值存储或运算时,类型会广播为pd.NA,而不是浮点型np.nan
其余全部内容在当前版本下完全一致,但迎合Pandas的发展模式,我们仍然全部用string来操作字符串
2. string类型的转换
如果将一个其他类型的容器直接转换string类型可能会出错:
当下正确的方法是分两部转换,先转为str型object,在转为string类型:
pd. Series( [ 1 , '1.' ] ) . astype( 'str' ) . astype( 'string' )
0 1
1 1.
dtype: string
pd. Series( [ 1 , 2 ] ) . astype( 'str' ) . astype( 'string' )
0 1
1 2
dtype: string
pd. Series( [ True , False ] ) . astype( 'str' ) . astype( 'string' )
0 True
1 False
dtype: string
二、拆分与拼接
1. str.split方法
(a)分割符与str的位置元素选取
s = pd. Series( [ 'a_b_c' , 'c_d_e' , np. nan, 'f_g_h' ] , dtype= "string" )
s
0 a_b_c
1 c_d_e
2 <NA>
3 f_g_h
dtype: string
根据某一个元素分割,默认为空格
s. str . split( '_' )
0 [a, b, c]
1 [c, d, e]
2 <NA>
3 [f, g, h]
dtype: object
这里需要注意split后的类型是object,因为现在Series中的元素已经不是string,而包含了list,且string类型只能含有字符串
对于str方法可以进行元素的选择,如果该单元格元素是列表,那么str[i]表示取出第i个元素,如果是单个元素,则先把元素转为列表在取出
s. str . split( '_' ) . str [ 1 ]
0 b
1 d
2 <NA>
3 g
dtype: object
pd. Series( [ 'a_b_c' , [ 'a' , 'b' , 'c' ] ] , dtype= "object" ) . str [ 1 ]
0 _
1 b
dtype: object
(b)其他参数
expand参数控制了是否将列拆开,n参数代表最多分割多少次
s. str . split( '_' , expand= True )
0 1 2 0 a b c 1 c d e 2 <NA> <NA> <NA> 3 f g h
s. str . split( '_' , n= 1 )
0 [a, b_c]
1 [c, d_e]
2 <NA>
3 [f, g_h]
dtype: object
s. str . split( '_' , expand= True , n= 1 )
0 1 0 a b_c 1 c d_e 2 <NA> <NA> 3 f g_h
2. str.cat方法
(a)不同对象的拼接模式
cat方法对于不同对象的作用结果并不相同,其中的对象包括:单列、双列、多列
① 对于单个Series而言,就是指所有的元素进行字符合并为一个字符串
s = pd. Series( [ 'ab' , None , 'd' ] , dtype= 'string' )
s
0 ab
1 <NA>
2 d
dtype: string
s. str . cat( )
'abd'
其中可选sep分隔符参数,和缺失值替代字符na_rep参数
s. str . cat( sep= ',' )
'ab,d'
s. str . cat( sep= ',' , na_rep= '*' )
'ab,*,d'
② 对于两个Series合并而言,是对应索引的元素进行合并
s2 = pd. Series( [ '24' , None , None ] , dtype= 'string' )
s2
0 24
1 <NA>
2 <NA>
dtype: string
s. str . cat( s2)
0 ab24
1 <NA>
2 <NA>
dtype: string
同样也有相应参数,需要注意的是两个缺失值会被同时替换
s. str . cat( s2, sep= ',' , na_rep= '*' )
0 ab,24
1 *,*
2 d,*
dtype: string
③ 多列拼接可以分为表的拼接和多Series拼接
表的拼接
s. str . cat( pd. DataFrame( { 0 : [ '1' , '3' , '5' ] , 1 : [ '5' , 'b' , None ] } , dtype= 'string' ) , na_rep= '*' )
0 ab15
1 *3b
2 d5*
dtype: string
多个Series拼接
s. str . cat( [ s+ '0' , s* 2 ] )
0 abab0abab
1 <NA>
2 dd0dd
dtype: string
(b)cat中的索引对齐
当前版本中,如果两边合并的索引不相同且未指定join参数,默认为左连接,设置join=‘left’
s2 = pd. Series( list ( 'abc' ) , index= [ 1 , 2 , 3 ] , dtype= 'string' )
s2
1 a
2 b
3 c
dtype: string
s. str . cat( s2, na_rep= '*' )
0 ab*
1 *a
2 db
dtype: string
三、替换
广义上的替换,就是指str.replace函数的应用,fillna是针对缺失值的替换,上一章已经提及
提到替换,就不可避免地接触到正则表达式,这里默认读者已掌握常见正则表达式知识点,若对其还不了解的,可以通过这份资料 来熟悉
1. str.replace的常见用法
s = pd. Series( [ 'A' , 'B' , 'C' , 'Aaba' , 'Baca' , '' , np. nan, 'CABA' , 'dog' , 'cat' ] , dtype= "string" )
s
0 A
1 B
2 C
3 Aaba
4 Baca
5
6 <NA>
7 CABA
8 dog
9 cat
dtype: string
第一个值写r开头的正则表达式,后一个写替换的字符串
s. str . replace( r'^[AB]' , '***' )
0 ***
1 ***
2 C
3 ***aba
4 ***aca
5
6 <NA>
7 CABA
8 dog
9 cat
dtype: string
2. 子组与函数替换
通过正整数调用子组(0返回字符本身,从1开始才是子组)
s. str . replace( r'([ABC])(\w+)' , lambda x: x. group( 2 ) [ 1 : ] + '*' )
0 A
1 B
2 C
3 ba*
4 ca*
5
6 <NA>
7 BA*
8 dog
9 cat
dtype: string
利用?P<…>表达式可以对子组命名调用
s. str . replace( r'(?P<one>[ABC])(?P<two>\w+)' , lambda x: x. group( 'two' ) [ 1 : ] + '*' )
0 A
1 B
2 C
3 ba*
4 ca*
5
6 <NA>
7 BA*
8 dog
9 cat
dtype: string
3. 关于str.replace的注意事项
首先,要明确str.replace和replace并不是一个东西:
str.replace针对的是object类型或string类型,默认是以正则表达式为操作,目前暂时不支持DataFrame上使用
replace针对的是任意类型的序列或数据框,如果要以正则表达式替换,需要设置regex=True,该方法通过字典可支持多列替换
但现在由于string类型的初步引入,用法上出现了一些问题,这些issue有望在以后的版本中修复
(a)str.replace赋值参数不得为pd.NA
这听上去非常不合理,例如对满足某些正则条件的字符串替换为缺失值,直接更改为缺失值在当下版本就会报错
此时,可以先转为object类型再转换回来,曲线救国:
pd. Series( [ 'A' , 'B' ] , dtype= 'string' ) . astype( 'O' ) . replace( r'[A]' , pd. NA, regex= True ) . astype( 'string' )
0 <NA>
1 B
dtype: string
至于为什么不用replace函数的regex替换(但string类型replace的非正则替换是可以的),原因在下面一条
(b)对于string类型Series,在使用replace函数时不能使用正则表达式替换
该bug现在还未修复
pd. Series( [ 'A' , 'B' ] , dtype= 'string' ) . replace( r'[A]' , 'C' , regex= True )
0 A
1 B
dtype: string
pd. Series( [ 'A' , 'B' ] , dtype= 'O' ) . replace( r'[A]' , 'C' , regex= True )
0 C
1 B
dtype: object
(c)string类型序列如果存在缺失值,不能使用replace替换
pd. Series( [ 'A' , np. nan] , dtype= 'string' ) . str . replace( 'A' , 'B' )
0 B
1 <NA>
dtype: string
综上,概况的说,除非需要赋值元素为缺失值(转为object再转回来),否则请使用str.replace方法
四、子串匹配与提取
1. str.extract方法
(a)常见用法
pd. Series( [ '10-87' , '10-88' , '10-89' ] , dtype= "string" ) . str . extract( r'([\d]{2})-([\d]{2})' )
使用子组名作为列名
pd. Series( [ '10-87' , '10-88' , '-89' ] , dtype= "string" ) . str . extract( r'(?P<name_1>[\d]{2})-(?P<name_2>[\d]{2})' )
name_1 name_2 0 10 87 1 10 88 2 <NA> <NA>
利用?正则标记选择部分提取
pd. Series( [ '10-87' , '10-88' , '-89' ] , dtype= "string" ) . str . extract( r'(?P<name_1>[\d]{2})?-(?P<name_2>[\d]{2})' )
name_1 name_2 0 10 87 1 10 88 2 <NA> 89
pd. Series( [ '10-87' , '10-88' , '10-' ] , dtype= "string" ) . str . extract( r'(?P<name_1>[\d]{2})-(?P<name_2>[\d]{2})?' )
name_1 name_2 0 10 87 1 10 88 2 10 <NA>
(b)expand参数(默认为True)
对于一个子组的Series,如果expand设置为False,则返回Series,若大于一个子组,则expand参数无效,全部返回DataFrame
对于一个子组的Index,如果expand设置为False,则返回提取后的Index,若大于一个子组且expand为False,报错
s = pd. Series( [ "a1" , "b2" , "c3" ] , [ "A11" , "B22" , "C33" ] , dtype= "string" )
s. index
Index(['A11', 'B22', 'C33'], dtype='object')
s. str . extract( r'([\w])' )
s. str . extract( r'([\w])' , expand= False )
A11 a
B22 b
C33 c
dtype: string
s. index. str . extract( r'([\w])' )
s. index. str . extract( r'([\w])' , expand= False )
Index(['A', 'B', 'C'], dtype='object')
s. index. str . extract( r'([\w])([\d])' )
2. str.extractall方法
与extract只匹配第一个符合条件的表达式不同,extractall会找出所有符合条件的字符串,并建立多级索引(即使只找到一个)
s = pd. Series( [ "a1a2" , "b1" , "c1" ] , index= [ "A" , "B" , "C" ] , dtype= "string" )
two_groups = '(?P<letter>[a-z])(?P<digit>[0-9])'
s. str . extract( two_groups, expand= True )
s. str . extractall( two_groups)
letter digit match A 0 a 1 1 a 2 B 0 b 1 C 0 c 1
s[ 'A' ] = 'a1'
s. str . extractall( two_groups)
letter digit match A 0 a 1 B 0 b 1 C 0 c 1
如果想查看第i层匹配,可使用xs方法
s = pd. Series( [ "a1a2" , "b1b2" , "c1c2" ] , index= [ "A" , "B" , "C" ] , dtype= "string" )
s. str . extractall( two_groups) . xs( 1 , level= 'match' )
3. str.contains和str.match
前者的作用为检测是否包含某种正则模式
pd. Series( [ '1' , None , '3a' , '3b' , '03c' ] , dtype= "string" ) . str . contains( r'[0-9][a-z]' )
0 False
1 <NA>
2 True
3 True
4 True
dtype: boolean
可选参数为na
pd. Series( [ '1' , None , '3a' , '3b' , '03c' ] , dtype= "string" ) . str . contains( 'a' , na= False )
0 False
1 False
2 True
3 False
4 False
dtype: boolean
str.match与其区别在于,match依赖于python的re.match,检测内容为是否从头开始包含该正则模式
pd. Series( [ '1' , None , '3a_' , '3b' , '03c' ] , dtype= "string" ) . str . match( r'[0-9][a-z]' , na= False )
0 False
1 False
2 True
3 True
4 False
dtype: boolean
pd. Series( [ '1' , None , '_3a' , '3b' , '03c' ] , dtype= "string" ) . str . match( r'[0-9][a-z]' , na= False )
0 False
1 False
2 False
3 True
4 False
dtype: boolean
五、常用字符串方法
1. 过滤型方法
(a)str.strip
常用于过滤空格
pd. Series( list ( 'abc' ) , index= [ ' space1 ' , 'space2 ' , ' space3' ] , dtype= "string" ) . index. str . strip( )
Index(['space1', 'space2', 'space3'], dtype='object')
(b)str.lower和str.upper
pd. Series( 'A' , dtype= "string" ) . str . lower( )
0 a
dtype: string
pd. Series( 'a' , dtype= "string" ) . str . upper( )
0 A
dtype: string
(c)str.swapcase和str.capitalize
分别表示交换字母大小写和大写首字母
pd. Series( 'abCD' , dtype= "string" ) . str . swapcase( )
0 ABcd
dtype: string
pd. Series( 'abCD' , dtype= "string" ) . str . capitalize( )
0 Abcd
dtype: string
2. isnumeric方法
检查每一位是否都是数字,请问如何判断是否是数值?(问题二)
pd. Series( [ '1.2' , '1' , '-0.3' , 'a' , np. nan] , dtype= "string" ) . str . isnumeric( )
0 False
1 True
2 False
3 False
4 <NA>
dtype: boolean
六、问题与练习
1. 问题
【问题一】 str对象方法和df/Series对象方法有什么区别?
string类型和object不同之处有三: ① 字符存取方法(string accessor methods,如str.count)会返回相应数据的Nullable类型,而object会随缺失值的存在而改变返回类型 ② 某些Series方法不能在string上使用,例如: Series.str.decode(),因为存储的是字符串而不是字节 ③ string类型在缺失值存储或运算时,类型会广播为pd.NA,而不是浮点型np.nan 其余全部内容在当前版本下完全一致,但迎合Pandas的发展模式,我们仍然全部用string来操作字符串
【问题二】 给出一列string类型,如何判断单元格是否是数值型数据?
s.str.isnumeric()
【问题三】 rsplit方法的作用是什么?它在什么场合下适用?
按照一定规则拆分某列字符串,split后的类型是object
【问题四】 在本章的第二到第四节分别介绍了字符串类型的5类操作,请思考它们各自应用于什么场景?
2.字符串拆分和拼接 - str.split:对字符串进行拆分 - str.cat:不同对象进行拼接 3.str.replace:默认以正则表达式进行替换 4.子串匹配与提取 - str.extract方法:匹配和提取字符串 - str.extractall方法:找出所有符合条件的字符串,并建立多级索引(即使只找到一个) - str.contains:检测是否包含某种正则模式 - str.match:依赖于python的re.match,检测内容为是否从头开始包含该正则模式
2. 练习
【练习一】 现有一份关于字符串的数据集,请解决以下问题:
data = pd. read_csv( 'data/String_data_one.csv' , index_col= '人员编号' )
data. head( )
姓名 国籍 性别 出生年 出生月 出生日 人员编号 1 aesfd 2 男 1942 8 10 2 fasefa 5 女 1985 10 4 3 aeagd 4 女 1946 10 15 4 aef 4 男 1999 5 13 5 eaf 1 女 2010 6 24
(a)现对字符串编码存储人员信息(在编号后添加ID列),使用如下格式:“×××(名字):×国人,性别×,生于×年×月×日”
ID = data[ '姓名' ] + ':' + data[ '国籍' ] . apply ( str ) + '国人,性别' + data[ '性别' ] . apply ( str ) + ',生于' + data[ '出生年' ] . apply ( str ) + '年' + data[ '出生月' ] . apply ( str ) + '月' + data[ '出生日' ] . apply ( str ) + '日'
data. insert( 0 , 'ID' , ID)
data. head( )
ID 姓名 国籍 性别 出生年 出生月 出生日 人员编号 1 aesfd:2国人,性别男,生于1942年8月10日 aesfd 2 男 1942 8 10 2 fasefa:5国人,性别女,生于1985年10月4日 fasefa 5 女 1985 10 4 3 aeagd:4国人,性别女,生于1946年10月15日 aeagd 4 女 1946 10 15 4 aef:4国人,性别男,生于1999年5月13日 aef 4 男 1999 5 13 5 eaf:1国人,性别女,生于2010年6月24日 eaf 1 女 2010 6 24
(b)将(a)中的人员生日信息部分修改为用中文表示(如一九七四年十月二十三日),其余返回格式不变。
import re
num_dict= { "0" : u"零" , "1" : u"一" , "2" : u"二" , "3" : u"三" , "4" : u"四" , "5" : u"五" , "6" : u"六" , "7" : u"七" , "8" : u"八" , "9" : u"九" }
data. loc[ : , 'ID' ] = data[ 'ID' ] . str . replace( r'(生于)(\d*)(年)' , lambda x: x. group( 1 ) + '' . join( map ( lambda x: num_dict[ x] , list ( x. group( 2 ) ) ) ) + x. group( 3 ) )
data. loc[ : , 'ID' ] = data[ 'ID' ] . str . replace( r'(年)(\d*)(\d)(月)' , lambda x: x. group( 1 ) + \
( '十' if len ( x. group( 2 ) ) > 0 else '' ) + ( '' if x. group( 3 ) == str ( 0 ) else num_dict[ x. group( 3 ) ] ) + x. group( 4 ) )
data. loc[ : , 'ID' ] = data[ 'ID' ] . str . replace( r'(月)(\d*)(\d)(日)' , lambda x: x. group( 1 ) + \
( num_dict[ x. group( 2 ) ] if len ( x. group( 2 ) ) > 0 and int ( x. group( 2 ) ) > 1 else '' ) + ( '十' if len ( x. group( 2 ) ) > 0 else '' ) + ( '' if x. group( 3 ) == str ( 0 ) else num_dict[ x. group( 3 ) ] ) + x. group( 4 ) )
data. head( )
ID 姓名 国籍 性别 出生年 出生月 出生日 人员编号 1 aesfd:2国人,性别男,生于一九四二年八月十日 aesfd 2 男 1942 8 10 2 fasefa:5国人,性别女,生于一九八五年十月四日 fasefa 5 女 1985 10 4 3 aeagd:4国人,性别女,生于一九四六年十月十五日 aeagd 4 女 1946 10 15 4 aef:4国人,性别男,生于一九九九年五月十三日 aef 4 男 1999 5 13 5 eaf:1国人,性别女,生于二零一零年六月二十四日 eaf 1 女 2010 6 24
data. index
Int64Index([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
...
1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000],
dtype='int64', name='人员编号', length=2000)
(c)将(b)中的ID列结果拆分为原列表相应的5列,并使用equals检验是否一致。
name = data[ 'ID' ] . str . split( ':' , expand= True )
country = name. iloc[ : , - 1 ] . str . split( '国人,性别' , expand= True )
gender = country. iloc[ : , - 1 ] . str . split( ',生于' , expand= True )
year = gender. iloc[ : , - 1 ] . str . split( '年' , expand= True )
month = year. iloc[ : , - 1 ] . str . split( '月' , expand= True )
day = month. iloc[ : , - 1 ] . str . split( '日' , expand= True )
pd. DataFrame( { '姓名' : name. iloc[ : , 0 ] , '国籍' : country. iloc[ : , 0 ] , '性别' : gender. iloc[ : , 0 ] , '出生年' : year. iloc[ : , 0 ] , '出生月' : month. iloc[ : , 0 ] , '出生日' : day. iloc[ : , 0 ] , } )
姓名 国籍 性别 出生年 出生月 出生日 人员编号 1 aesfd 2 男 一九四二 八 十 2 fasefa 5 女 一九八五 十 四 3 aeagd 4 女 一九四六 十 十五 4 aef 4 男 一九九九 五 十三 5 eaf 1 女 二零一零 六 二十四 6 faefdf 1 男 一九四二 一 二十一 7 daf 4 女 一九五七 十一 五 8 faefdf 3 男 一九九八 四 八 9 fs 3 女 一九五七 四 十四 10 faefdffa 3 女 一九八八 九 十五 11 arege 4 男 一九三三 四 十二 12 ase 3 女 一九四一 六 十八 13 fasf 3 男 一九八七 七 三 14 gfa 5 女 一九六七 十 三 15 asf 3 男 一九九零 十 二十 16 eter 4 男 二零零一 二 十三 17 esf 4 女 一九八七 四 二十八 18 srgdsrg 5 男 一九九三 八 十二 19 aefase 4 男 一九三五 十 二 20 szf 1 女 一九三六 六 三 21 eae 2 女 一九三四 二 二十八 22 dsrgdrg 4 男 一九六八 七 四 23 ggase 4 女 一九九四 九 三 24 hgdrg 1 女 一九九一 五 二十七 25 fef 3 男 一九四五 三 十一 26 zsgdrhdth 4 女 一九四一 七 十六 27 gfe 4 女 一九七七 四 二十六 28 esfa 4 女 一九九八 一 三 29 efaeg 3 女 一九三四 九 十八 30 fasef 1 男 一九八零 六 二十四 ... ... ... ... ... ... ... 1971 fse 2 男 一九八六 七 九 1972 asef 5 女 一九九三 十二 十七 1973 aesfaes 2 男 二零零九 三 十四 1974 fesf 4 女 一九五二 九 十五 1975 se 3 男 一九八五 三 十一 1976 asef 4 男 一九五二 一 九 1977 fas 2 女 一九三七 十 十四 1978 zgzd 5 女 一九七七 七 十六 1979 aesfa 4 女 二零一零 二 十八 1980 awe 1 男 一九六九 九 十九 1981 gzg 3 女 一九五八 四 二十三 1982 fsf 3 男 一九六九 十一 十六 1983 sg 2 女 二零一五 三 二十七 1984 gfdg 4 女 一九八七 四 九 1985 fg 2 女 一九八零 八 三 1986 rgd 4 女 二零一五 十二 十五 1987 zes 5 男 二零一一 十二 二十六 1988 hxr 1 女 一九三五 六 二十七 1989 dxrg 3 女 一九四八 十二 二十 1990 xdr 3 女 一九八三 四 九 1991 gzseg 2 女 一九七零 二 二十三 1992 efs 3 男 二零零九 八 九 1993 gx 1 女 一九九三 七 二十四 1994 rdxg 2 男 一九九七 十 十 1995 hx 1 男 一九七七 九 九 1996 sdf 5 男 一九八四 四 十七 1997 hx 1 男 一九四三 七 十六 1998 drg 5 女 二零一八 四 六 1999 zfgzdrg 5 男 二零零五 一 三 2000 fsdf 3 女 一九六二 七 二十三
2000 rows × 6 columns
【练习二】 现有一份半虚拟的数据集,第一列包含了新型冠状病毒的一些新闻标题,请解决以下问题:
(a)选出所有关于北京市和上海市新闻标题的所在行。
(b)求col2的均值。
(c)求col3的均值。
data1 = pd. read_csv( 'data/String_data_two.csv' )
data1. head( )
col1 col2 col3 0 鄂尔多斯市第2例确诊患者治愈出院 19 363.6923 1 云南新增2例,累计124例 -67 -152.281 2 武汉协和医院14名感染医护出院 -86 325.6221 3 山东新增9例,累计307例 -74 -204.9313 4 上海开学日期延至3月 -95 4.05
data2 = data1[ data1[ 'col1' ] . map ( lambda x: '北京' in x or '上海' in x) ]
data2. head( )
col1 col2 col3 4 上海开学日期延至3月 -95 4.05 5 北京新增25例确诊病例,累计确诊253例 -4 -289.1719 6 上海新增10例,累计243例 2 -73.7105 36 上海新增14例累计233例 -55 -83 40 上海新增14例累计233例 -88 -99
data2[ 'col2' ] . map ( eval ) . mean( )
-20.152173913043477
data2. iloc[ : , - 1 ] . map ( eval ) . mean( )
-6.998510869565218