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


前言

由于Python在字符串和文本操作上的便利性,使得Python成为一个流行的原生数据集操作语言已经有很长时间了。字符串对象的内建方法使得大部分文本操作非常简单。但对于更为复杂的模式匹配和文本操作,正则表达式是可能需要的。pandas允许你将字符串和正则表达式简洁地应用到整个数组上,此外还能处理数据缺失带来的困扰。下面我们将介绍,Python内建字符串的方法、正则表达式以及向量化字符串函数。


一、字符串操作

1.1 字符串对象方法

在很多字符串处理和脚本应用中,内建的字符串方法是足够的。例如,一个逗号分隔的字符串可以使用split方法拆分成多块:

val = 'A,B,  GUIDO'
a = val.split(',')
print(a)
--------------------------------------------------------------------------
['A', 'B', '  GUIDO']

splitstrip一起使用,用于清楚空格(包括换行):

p = [x.strip() for x in val.split(',')]
print(p)
--------------------------------------------------------------------------
['A', 'B', 'GUIDO']

这些子字符串可以使用加法与两个冒号分隔符连接在一起:

first,second,third = p
print(first+'::'+second+'::'+third)
--------------------------------------------------------------------------
A::B::GUIDO

但是这并不是一种实用的通用方法,在字符串’::'的join方法中传入一个列表或元组是一种更快且更加Python风格化的方法:

a = '::'.join(p)
print(a)
--------------------------------------------------------------------------
A::B::GUIDO

其他方法涉及定位子字符串。使用Python的in关键字是检测子字符串的最佳方法,尽管indexfind也能实现同样的功能:

print('GUIDO' in val)
print(val.index(','))
print(val.find(':'))
---------------------------------------------------------
True
1
-1

find和index的区别在于index在字符串没有找到时会抛出一个异常(而find返回的是-1).

相关地,count返回的是某个特定的字符出现在字符串的次数:

a = val.count(',')
print(a)
---------------------------------------------------------
2

replace将用一种字符串替代另一种字符串。它通常也用于传入空字符串来删除某个模式。

a = val.replace(',','::')
print(a)
a = val.replace(',','')
print(a)
---------------------------------------------------------
A::B::  GUIDO
AB  GUIDO

1.2 正则表达式

正则表达式提供了一种在文本中灵活查找或匹配(通常更为复杂的)字符串模式的方法。单个表达式通常被称为regex,是根据正则表达式语言形成的字符串。Python内建的re模块是用于将正则表达式应用到字符串上的库。

而正则表达式是通过一行字符串,来描述一定的规则。有关更多关于正则表达式的内容可以看看下面这篇文章:

一看就懂:正则表达式

re模块主要有三个主题:模式匹配、替代、拆分。当然,这三部分主题是相关联的。一个正则表达式描述了在文本中需要定位的一种模式,可以用于多种目标。让我们来看一个简单的示例:假设我们想将含有多种空白字符(制表符、空格、换行符)的字符串拆分开。描述一个或多个空白字符的正则表达式是 \s+ :

import re
text = 'foo     bar\t baz \tqux'
a = re.split('\s+', text)
print(a)
---------------------------------------------------------
['foo', 'bar', 'baz', 'qux']

当你调用re.split(‘\s+’, text),正则表达式首先会被编译,然后正则表达式的split方法传入文本上被调用。你可以使用re.compile 自行编译,形成一个可复用的正则表达式对象:

regex = re.compile('\s+')
a = regex.split(text)
print(a)
---------------------------------------------------------
['foo', 'bar', 'baz', 'qux']

如果你想要获得的是一个所有匹配正则表达式的模式的列表,你可以使用findall方法

regex1 = regex.findall(text)
print(regex1)
---------------------------------------------------------
['     ', '\t ', ' \t']

如果你需要将相同的表达式应用到多个字符串上,推荐使用re.compile创建一个正则表达式对象,这样做有利于节约cpu周期。match和search与findall相关性很大。

  1. findall返回的是字符串中所有的匹配项,
  2. search返回的仅仅是第一个匹配项。
  3. 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 使正则表达式不区分大小写
regex = re.compile(pattern,flags=re.IGNORECASE)

其中pattern是一个正则表达式,[A-Z0-9._%±] 表示所匹配的字符可以是:数字、字母、下划线、英文点以及%±,且至少出现1次。[A-Z0-9.-] 同理,而**[A-Z]{2,4}** 匹配2到4个字母。

在这文本上使用findall,依据正则表达式就会生成一个电子邮件地址的列表:

a = regex.findall(text)
print(a)
---------------------------------------------------------
['dave@google.com', 
'steve@gmail.com', 
'rob@gmail.com', 
'ryan@yahoo.com']

search返回的是文本中第一个匹配到的电子邮件地址。对于前面提到的正则表达式,匹配对象只能告诉我们模式在字符串中起始和结束的位置:

a = text[m.start():m.end()]
print(a)
---------------------------------------------------------
dave@google.com

regex.match只在模式出现于字符串起始位置时进行匹配,如果没有匹配到,返回None:

print(regex.match(text))
---------------------------------------------------------
None

相关地,sub会返回一个新的字符串,原字符串中的模式会被一个新的字符串替代:

a = regex.sub('REDACTED',text)
print(a)
---------------------------------------------------------
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')
a = m.groups()
print(a)
---------------------------------------------------------
('wesm', 'bright', 'net')

当模式可以分组时,findall返回的是包含元组的列表:

a = regex.findall(text)
print(a)
---------------------------------------------------------
[('dave', 'google', 'com'), 
('steve', 'gmail', 'com'), 
('rob', 'gmail', 'com'), 
('ryan', 'yahoo', 'com')]

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

a = regex.sub(r'Username: \1,Domain: \2, Suffix: \3',text)
print(a)
---------------------------------------------------------
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

下面提供一个简要的正则表达式方法表格:

方法描述
findall将字符串中所有的非重叠匹配模式以列表形式返回
finditer与findall类似,但返回的是迭代器
match在字符串起始位置匹配模式,也可以将模式组建匹配到分组中;如果模式匹配上了,返回的一个匹配对象,否则返回None
search扫描字符串的匹配模式,如果扫描到了返回匹配对象,与match方法不同的是,search方法的匹配可以是字符串的任意位置,而不仅仅是字符串的起始位置。
split根据模式,将字符串拆分为多个部分
sub,subn用替换表达式替换字符串中所有匹配(sub)或第n个出现的匹配串(subn),使用符号\1、\2…来引用替换字符串中的匹配组元素

1.3 pandas中的向量化字符串函数

清理杂乱的数据集用于分析通常需要大量的字符串处理和正则化。包含字符串的列有时会含有缺失数据,使事情变得复杂:

import numpy as np
import pandas as pd

data = {'Dave': 'dave@google.com',
        'Steve': 'steve@gmail.com',
        'Rob': 'rob@gmail.com',
        'Wes': np.nan}


data = pd.Series(data)
print(data)
a = data.isnull()
print(a)
---------------------------------------------------------
Dave     dave@google.com
Steve    steve@gmail.com
Rob        rob@gmail.com
Wes                  NaN
dtype: object


Dave     False
Steve    False
Rob      False
Wes       True
dtype: bool

你可以使用data.map将字符串和有效的正则表达式方法(以lambda或其他函数的方式传递)应用到每个值上,但是NA(null)值上会失败。为了解决问题,Series有面向数组的方法用于跳过NA值的字符串操作。这些方法通过Series的str属性进行调用,例如,我们可以通过str.contains来检查每个电子邮件地址是否含有‘gmail’:

a = data.str.contains('gmail')
print(a)
---------------------------------------------------------
Dave     False
Steve     True
Rob       True
Wes        NaN
dtype: object

正则表达式也可以结合任意的re模块选项使用,例如IGNORECASE:

pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
print(pattern)

a = data.str.findall(pattern,flags= re.IGNORECASE)
print(a)
---------------------------------------------------------
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)
print(matches)
---------------------------------------------------------
Dave     True
Steve    True
Rob      True
Wes       NaN
dtype: object

要访问嵌入式列表中的元素,我们可以将索引传递给这些函数中的任意一个:

a1 = a.str.get(1)
print(a1)
a2 = a.str[0]
print(a2)
---------------------------------------------------------
Dave    NaN
Steve   NaN
Rob     NaN
Wes     NaN
dtype: float64

Dave     (dave, google, com)
Steve    (steve, gmail, com)
Rob        (rob, gmail, com)
Wes                      NaN
dtype: object

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

a = data.str[:5]
print(a)
---------------------------------------------------------
Dave     dave@
Steve    steve
Rob      rob@g
Wes        NaN
dtype: object

下表有更多pandas字符串方法。

部分向量化字符串方法列表:

方法描述
cat根据可选的分隔符按元素黏合字符串
contains返回是否含有某个模式/正则表达式的布尔值数组
count模式出现次数的计数
extract使用正则表达式从字符串Series中分组抽取一个或多个字符串;返回的结果是每个分组形成一列的DataFrame
endswith等价于对每个元素使用x.endswith
startswith等价于对每个元素使用x.startwith(模式)
findall找出字符串中所有的模式/正则表达式匹配项,以列表返回
get对每个元素进行索引(获得第i个元素)
isalnum等价于内建的str.isalnum
isalpha等价于内建的str.isalpha
isdecimal等价于内建的str.isdecimal
isdigit等价于内建的str.isdigit
islower等价于内建的str.islower
isnumeric等价于内建的str.isnumeric
isupper等价于内建的str.isupper
join根据传递的分隔符,将Series中的字符串联合
len计算每个字符串的长度
lower,upper转换大小写;等价于对每个元素进行x.lower()或x.upper()
match使用re.match将正则表达式应用到每个元素上,将匹配分组以列表形式返回
pad将空白加到字符串的左边、右边或两边
center等价于pad(side= ‘both’)
repeat重复值(例如s.str.repeat(3)等价于对每个字符串进行x*3)
replace以其他字符串替代模式/正式表达式的匹配项
slice对Series中的字符串进行切片
split以分隔符或正则表达式对字符串进行拆分
strip对字符串两侧的空白进行消除,包括换行符
rstrip消除字符串右边的空白
lstrip消除字符串左边的空白

总结

以上就是今天要讲的内容,本文中我们已经探索了多个工具,但覆盖范围仍然不全面。下一章,将探索pandas的数据连接和分组功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值