Python 数据处理(三十五)—— 文本数据处理

1 文本数据类型

pandas 中,存储文本主要有两种方式

  1. object 类型
  2. StringDtype 扩展类型

但一般建议使用 StringDtype 类型存储文本数据。都是由于各种原因,现在字符串数据的默认存储类型还是 object

In [1]: pd.Series(["a", "b", "c"])
Out[1]: 
0    a
1    b
2    c
dtype: object

要存储为 string 类型,需要显式的设置 dtype 参数

In [2]: pd.Series(["a", "b", "c"], dtype="string")
Out[2]: 
0    a
1    b
2    c
dtype: string

In [3]: pd.Series(["a", "b", "c"], dtype=pd.StringDtype())
Out[3]: 
0    a
1    b
2    c
dtype: string

或者在创建 SeriesDataFrame 之后,使用 astype 转换类型

In [4]: s = pd.Series(["a", "b", "c"])

In [5]: s
Out[5]: 
0    a
1    b
2    c
dtype: object

In [6]: s.astype("string")
Out[6]: 
0    a
1    b
2    c
dtype: string

也可以使用 StringDtype/"string" 转换其他非字符串类型的数据

In [7]: s = pd.Series(["a", 2, np.nan], dtype="string")

In [8]: s
Out[8]: 
0       a
1       2
2    <NA>
dtype: string

In [9]: type(s[1])
Out[9]: str

转换现有数据的类型

In [10]: s1 = pd.Series([1, 2, np.nan], dtype="Int64")

In [11]: s1
Out[11]: 
0       1
1       2
2    <NA>
dtype: Int64

In [12]: s2 = s1.astype("string")

In [13]: s2
Out[13]: 
0       1
1       2
2    <NA>
dtype: string

In [14]: type(s2[0])
Out[14]: str
1.1 行为差异

StringDtype 类型对象与 object 类型之间存在一些差异

  1. 对于 StringDtype,对于返回数值型输出字符串方法将始终返回非空的 integer 类型。而不是 intfloat 类型。对于布尔型输出方法,返回可空的布尔类型
In [15]: s = pd.Series(["a", None, "b"], dtype="string")

In [16]: s
Out[16]: 
0       a
1    <NA>
2       b
dtype: string

In [17]: s.str.count("a")
Out[17]: 
0       1
1    <NA>
2       0
dtype: Int64

In [18]: s.dropna().str.count("a")
Out[18]: 
0    1
2    0
dtype: Int64

两个结果的输出都是 Int64 类型。将其与 object 类型比较

In [19]: s2 = pd.Series(["a", None, "b"], dtype="object")

In [20]: s2.str.count("a")
Out[20]: 
0    1.0
1    NaN
2    0.0
dtype: float64

In [21]: s2.dropna().str.count("a")
Out[21]: 
0    1
2    0
dtype: int64

当存在 NA 值时,输出为 float64。类似地,对于返回布尔值的方法

In [22]: s.str.isdigit()
Out[22]: 
0    False
1     <NA>
2    False
dtype: boolean

In [23]: s.str.match("a")
Out[23]: 
0     True
1     <NA>
2    False
dtype: boolean
  1. 一些字符串方法,如 Series.str.decode()StringArray 上是不可用的。因为 StringArray 只保存字符串,而不是字节

  2. 在比较操作中,arrays.StringArrayStringArray 支持的 Series 将返回一个具有 BooleanDtype 的对象,而不是一个 bool 对象。StringArray 中的缺失值会在比较操作中传播,而不是像 numpy.nan 那样总是比较不等

2 字符串方法

SeriesIndex 有一套字符串处理方法,可以方便地对数组的每个元素进行操作,最重要的是,这些方法会自动忽略缺失值。

这些方法可以通过 str 属性访问,通常具有与内置字符串方法相匹配的名称

In [24]: s = pd.Series(
   ....:     ["A", "B", "C", "Aaba", "Baca", np.nan, "CABA", "dog", "cat"], dtype="string"
   ....: )
   ....: 

In [25]: s.str.lower()
Out[25]: 
0       a
1       b
2       c
3    aaba
4    baca
5    <NA>
6    caba
7     dog
8     cat
dtype: string

In [26]: s.str.upper()
Out[26]: 
0       A
1       B
2       C
3    AABA
4    BACA
5    <NA>
6    CABA
7     DOG
8     CAT
dtype: string

In [27]: s.str.len()
Out[27]: 
0       1
1       1
2       1
3       4
4       4
5    <NA>
6       4
7       3
8       3
dtype: Int64
In [28]: idx = pd.Index([" jack", "jill ", " jesse ", "frank"])

In [29]: idx.str.strip()
Out[29]: Index(['jack', 'jill', 'jesse', 'frank'], dtype='object')

In [30]: idx.str.lstrip()
Out[30]: Index(['jack', 'jill ', 'jesse ', 'frank'], dtype='object')

In [31]: idx.str.rstrip()
Out[31]: Index([' jack', 'jill', ' jesse', 'frank'], dtype='object')

Index 上的字符串方法对于清理或转换 DataFrame 的列特别有用。

例如,您可能有带有前导或后置空格的列

In [32]: df = pd.DataFrame(
   ....:     np.random.randn(3, 2), columns=[" Column A ", " Column B "], index=range(3)
   ....: )
   ....: 

In [33]: df
Out[33]: 
    Column A    Column B 
0    0.469112   -0.282863
1   -1.509059   -1.135632
2    1.212112   -0.173215

因为 df.columns 是一个 Index 对象,所以我们可以使用 .str 访问器

In [34]: df.columns.str.strip()
Out[34]: Index(['Column A', 'Column B'], dtype='object')

In [35]: df.columns.str.lower()
Out[35]: Index([' column a ', ' column b '], dtype='object')

我们可以根据需要对列名进行处理,然后重新设置列名。

例如,我们删除列名的前后空格,并将其改为小写字母,同时用 _ 替换剩余的空格

In [36]: df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_")

In [37]: df
Out[37]: 
   column_a  column_b
0  0.469112 -0.282863
1 -1.509059 -1.135632
2  1.212112 -0.173215

3 切割和替换字符串

split 方法会返回一个值为 listSeries

In [38]: s2 = pd.Series(["a_b_c", "c_d_e", np.nan, "f_g_h"], dtype="string")

In [39]: s2.str.split("_")
Out[39]: 
0    [a, b, c]
1    [c, d, e]
2         <NA>
3    [f, g, h]
dtype: object

可以使用 get[] 访问拆分后的列表中的元素

In [40]: s2.str.split("_").str.get(1)
Out[40]: 
0       b
1       d
2    <NA>
3       g
dtype: object

In [41]: s2.str.split("_").str[1]
Out[41]: 
0       b
1       d
2    <NA>
3       g
dtype: object

更简单的方法是设置 expand 参数,返回一个 DataFrame

In [42]: s2.str.split("_", expand=True)
Out[42]: 
      0     1     2
0     a     b     c
1     c     d     e
2  <NA>  <NA>  <NA>
3     f     g     h

当原来的 Series 包含 StringDtype 类型的数据时,输出列也将全部为 StringDtype

当然,也可以设置切割次数

In [43]: s2.str.split("_", expand=True, n=1)
Out[43]: 
      0     1
0     a   b_c
1     c   d_e
2  <NA>  <NA>
3     f   g_h

它还有个对应的 rsplit 方法,从右边起始对字符串进行拆分

In [44]: s2.str.rsplit("_", expand=True, n=1)
Out[44]: 
      0     1
0   a_b     c
1   c_d     e
2  <NA>  <NA>
3   f_g     h

replace 参数支持使用正则表达式,前两个参数是 pat(匹配模式) 和 repl(替换字符串)

In [45]: s3 = pd.Series(
   ....:     ["A", "B", "C", "Aaba", "Baca", "", np.nan, "CABA", "dog", "cat"],
   ....:     dtype="string",
   ....: )
   ....: 

In [46]: s3
Out[46]: 
0       A
1       B
2       C
3    Aaba
4    Baca
5        
6    <NA>
7    CABA
8     dog
9     cat
dtype: string

In [47]: s3.str.replace("^.a|dog", "XX-XX ", case=False, regex=True)
Out[47]: 
0           A
1           B
2           C
3    XX-XX ba
4    XX-XX ca
5            
6        <NA>
7    XX-XX BA
8      XX-XX 
9     XX-XX t
dtype: string

如果只是想要替换字符串字面值,可以将 regex 参数设置为 False,而不需要对每个特殊字符进行转义。此时 patrepl 参数必须是字符串

In [48]: dollars = pd.Series(["12", "-$10", "$10,000"], dtype="string")

# These lines are equivalent
In [49]: dollars.str.replace(r"-\$", "-", regex=True)
Out[49]: 
0         12
1        -10
2    $10,000
dtype: string

In [50]: dollars.str.replace("-$", "-", regex=False)
Out[50]: 
0         12
1        -10
2    $10,000
dtype: string

此外,replace 方法还接受一个可调用的替换函数,会使用 re.sub() 方法在每个匹配的模式上调用该函数

该函数需要传入一个正则对象作为位置参数,并返回一个字符串。例如

# 反转每个小写单词的顺序
In [51]: pat = r"[a-z]+"

In [52]: def repl(m):
   ....:     return m.group(0)[::-1]
   ....: 

In [53]: pd.Series(["foo 123", "bar baz", np.nan], dtype="string").str.replace(
   ....:     pat, repl, regex=True
   ....: )
   ....: 
Out[53]: 
0    oof 123
1    rab zab
2       <NA>
dtype: string

# 使用 regex 捕获分组
In [54]: pat = r"(?P<one>\w+) (?P<two>\w+) (?P<three>\w+)"

In [55]: def repl(m):
   ....:     return m.group("two").swapcase()
   ....: 

In [56]: pd.Series(["Foo Bar Baz", np.nan], dtype="string").str.replace(
   ....:     pat, repl, regex=True
   ....: )
   ....: 
Out[56]: 
0     bAR
1    <NA>
dtype: string

replace 方法的 pat 参数还接受 re.compile() 编译的正则表达式对象。所有的 flags 需要在编译正则对象时设置

In [57]: import re

In [58]: regex_pat = re.compile(r"^.a|dog", flags=re.IGNORECASE)

In [59]: s3.str.replace(regex_pat, "XX-XX ", regex=True)
Out[59]: 
0           A
1           B
2           C
3    XX-XX ba
4    XX-XX ca
5            
6        <NA>
7    XX-XX BA
8      XX-XX 
9     XX-XX t
dtype: string

如果在 replace 中设置 flags 参数,则会抛出异常

In [60]: s3.str.replace(regex_pat, 'XX-XX ', flags=re.IGNORECASE)
---------------------------------------------------------------------------
ValueError: case and flags cannot be set when pat is a compiled regex

4 连接

有几种方法可以将一个 SeriesIndex 与自己或其他的 SeriesIndex 相连接,所有这些方法都是基于 cat() 方法

4.1 将单个 Series 对象连接成字符串

可以连接一个 SeriesIndex 的内容

In [61]: s = pd.Series(["a", "b", "c", "d"], dtype="string")

In [62]: s.str.cat(sep=",")
Out[62]: 'a,b,c,d'

如果未指定 sep 参数,则默认为空字符串

In [63]: s.str.cat()
Out[63]: 'abcd'

默认会跳过缺失值,也可以使用 na_rep 指定缺失值的表示方式

In [64]: t = pd.Series(["a", "b", np.nan, "d"], dtype="string")

In [65]: t.str.cat(sep=",")
Out[65]: 'a,b,d'

In [66]: t.str.cat(sep=",", na_rep="-")
Out[66]: 'a,b,-,d'
4.2 连接 Series 与一个类似列表的对象

cat() 的第一个参数 others 可以是类似列表的对象,但是其长度需要和调用对象一致

In [67]: s.str.cat(["A", "B", "C", "D"])
Out[67]: 
0    aA
1    bB
2    cC
3    dD
dtype: string

只要两个对象中存在缺失值,对应的结果中也是缺失值,除非指定了 na_rep

In [68]: s.str.cat(t)
Out[68]: 
0      aa
1      bb
2    <NA>
3      dd
dtype: string

In [69]: s.str.cat(t, na_rep="-")
Out[69]: 
0    aa
1    bb
2    c-
3    dd
dtype: string
4.3 连接 Series 与多个类似列表的对象

others 参数也可以是二维的,但是得保证其行数必须与调用的对象一致

In [70]: d = pd.concat([t, s], axis=1)

In [71]: s
Out[71]: 
0    a
1    b
2    c
3    d
dtype: string

In [72]: d
Out[72]: 
      0  1
0     a  a
1     b  b
2  <NA>  c
3     d  d

In [73]: s.str.cat(d, na_rep="-")
Out[73]: 
0    aaa
1    bbb
2    c-c
3    ddd
dtype: string
4.4 连接一个 Series 和一个带索引的对象

对于 SeriesDataFrame 的连接,可以通过设置 join 参数指定对齐方式

In [74]: u = pd.Series(["b", "d", "a", "c"], index=[1, 3, 0, 2], dtype="string")

In [75]: s
Out[75]: 
0    a
1    b
2    c
3    d
dtype: string

In [76]: u
Out[76]: 
1    b
3    d
0    a
2    c
dtype: string

In [77]: s.str.cat(u)
Out[77]: 
0    aa
1    bb
2    cc
3    dd
dtype: string

In [78]: s.str.cat(u, join="left")
Out[78]: 
0    aa
1    bb
2    cc
3    dd
dtype: string

通常 join 可选范围为: 'left', 'outer', 'inner', 'right'。此时,不再要求两个对象长度一致

In [79]: v = pd.Series(["z", "a", "b", "d", "e"], index=[-1, 0, 1, 3, 4], dtype="string")

In [80]: s
Out[80]: 
0    a
1    b
2    c
3    d
dtype: string

In [81]: v
Out[81]: 
-1    z
 0    a
 1    b
 3    d
 4    e
dtype: string

In [82]: s.str.cat(v, join="left", na_rep="-")
Out[82]: 
0    aa
1    bb
2    c-
3    dd
dtype: string

In [83]: s.str.cat(v, join="outer", na_rep="-")
Out[83]: 
-1    -z
 0    aa
 1    bb
 2    c-
 3    dd
 4    -e
dtype: string

others 参数是 DataFrame 时,也可以使用

In [84]: f = d.loc[[3, 2, 1, 0], :]

In [85]: s
Out[85]: 
0    a
1    b
2    c
3    d
dtype: string

In [86]: f
Out[86]: 
      0  1
3     d  d
2  <NA>  c
1     b  b
0     a  a

In [87]: s.str.cat(f, join="left", na_rep="-")
Out[87]: 
0    aaa
1    bbb
2    c-c
3    ddd
dtype: string
4.5 连接 Series 和多个对象

可以将一些类似数组的对象(如 SeriesIndex 等)放在一个类似列表的容器中,然后传递给 cat

In [88]: s
Out[88]: 
0    a
1    b
2    c
3    d
dtype: string

In [89]: u
Out[89]: 
1    b
3    d
0    a
2    c
dtype: string

In [90]: s.str.cat([u, u.to_numpy()], join="left")
Out[90]: 
0    aab
1    bbd
2    cca
3    ddc
dtype: string

对于没有索引的对象,其长度必须与调用 cat 的对象相同。但是 SeriesIndex 可以是任意的,除非设置了 json=None

In [91]: v
Out[91]: 
-1    z
 0    a
 1    b
 3    d
 4    e
dtype: string

In [92]: s.str.cat([v, u, u.to_numpy()], join="outer", na_rep="-")
Out[92]: 
-1    -z--
 0    aaab
 1    bbbd
 2    c-ca
 3    dddc
 4    -e--
dtype: string

如果在 others 参数上包含不同索引的对象,且设置了 join='right',则最后的结果将会是这些索引的并集

In [93]: u.loc[[3]]
Out[93]: 
3    d
dtype: string

In [94]: v.loc[[-1, 0]]
Out[94]: 
-1    z
 0    a
dtype: string

In [95]: s.str.cat([u.loc[[3]], v.loc[[-1, 0]]], join="right", na_rep="-")
Out[95]: 
-1    --z
 0    a-a
 3    dd-
dtype: string
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容