0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何高效的使用Python和pandas清理非结构化文本字段技巧

数据分析与开发 来源:数据分析与开发 作者:数据分析与开发 2021-04-06 13:43 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

介绍

大家都知道数据清洗是数据分析过程中的一个重要部分。pandas有多种清洗文本字段的方法,可以用来为进一步分析做准备。随着数据集越来越大,文本清洗的过程会逐渐变长,寻找一个能在合理时间内有效运行并可维护的方法变得非常重要。

本文将展示清洗大数据文件中文本字段的示例,帮助大家学习使用 Python 和 pandas 高效清理非结构化文本字段的技巧。

问题

假设你有一批全新工艺的威士忌想出售。你所在的爱荷华州,刚好有一个公开的数据集显示了该州所有的酒类销售情况。这似乎是一个很好的机会,你可以利用你的分析技能,看看谁是这个州最大的客户。有了这些数据,你甚至可以为每个客户规划销售流程。

你对这个机会感到兴奋,但下载了数据后发现它相当大。这个数据集是一个565MB的CSV文件,包含24列和2.3百万行。它虽然不是我们平时说的“大数据”,但它依然足够大到可以让Excel卡死。同时它也大到让一些pandas方法在比较慢的笔记本电脑上运行地非常吃力。

本文中,我们将使用包括2019年所有销售额的数据。当然你也可以从网站上下载其他不同时间段的数据。

我们从导入模块和读取数据开始,我会使用sidetable包来查看数据的概览。这个包虽然不能用来做清洗,但我想强调一下它对于这些数据探索场景其实很有用。

数据

读取数据:

import pandas as pd

import numpy as np

import sidetable

df = pd.read_csv(‘2019_Iowa_Liquor_Sales.csv’)

数据长这样:

7240cd9e-9531-11eb-8b86-12bb97331649.png

我们大概率要做的第一件事是看每一家商店的购买量,并将它们从大到小排序。资源有限所以我们应该集中精力在那些我们能从中获得最好回报的地方。我们更应该打电话给几个大公司的账户,而不是那些夫妻小店。

sidetable是以可读格式汇总数据的快捷方式。另一种方法是groupby加上其他操作。

df.stb.freq([‘Store Name’], value=‘Sale (Dollars)’, style=True, cum_cols=False)

724cd544-9531-11eb-8b86-12bb97331649.png

很明显在大多数情况下,每个位置的商店名称都是唯一的。理想情况下我们希望看到的是Hy-Vee, Costco, Sam’s 等聚合在一起的内容。

看来我们需要清洗数据了。

清洗尝试·1

我们可以研究的第一种方法是使用.loc以及str的布尔过滤器来搜索Store Name列中的相关字符串。

df.loc[df[‘Store Name’].str.contains(‘Hy-Vee’, case=False), ‘Store_Group_1’] = ‘Hy-Vee’

上述代码使用不区分大小写的方式来搜索字符串“Hy Vee”,并将值“Hy Vee”存储在名为Store_Group_1的新列中。这个代码可以有效地将“Hy Vee#3/BDI/Des Moines”或“Hy Vee Food Store/Urbandale”等名称转换为正常的“Hy Vee”。

用%%timeit来计算此操作的时间:

1.43 s ± 31.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

我们不想过早地进行优化,但我们可以使用regex=False参数来稍微加速一下:

df.loc[df[‘Store Name’].str.contains(‘Hy-Vee’, case=False, regex=False), ‘Store_Group_1’] = ‘Hy-Vee’

804 ms ± 27.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

来看下新列的情况:

df[‘Store_Group_1’].value_counts(dropna=False)

NaN 1617777

Hy-Vee 762568

Name: Store_Group_1, dtype: int64

可以看到我们已经清理了Hy-Vee,但还有很多其他值需要我们处理。

.loc方法内部包含大量代码,速度其实可能很慢。我们可以利用这个思想,来寻找一些更易于执行和维护的替代方案。

清洗尝试·2

另一种非常有效和灵活的方法是使用np.select来进行多匹配并在匹配时指定值。

有几个很好的资源可以帮你学习如何使用np.select。这篇来自Dataquest的文章就是一个很好的概述。Nathan Cheever的这篇演讲也十分有趣,内容丰富。我建议你们可以看下这两篇文章。

关于np.select的作用最简单的解释是,它计算一个条件列表,如果有条件为真,就应用相应值的列表。

在我们的例子中,我们想查找不同的字符串,来替换为我们想要的规范值。

浏览完我们的数据后,我们把条件和值列表总结在store_patterns列表中。列表中的每个元组都是一个str.contains()方法,来查找和替换对应的我们想要做聚合的规范值。

store_patterns = [

(df[‘Store Name’].str.contains(‘Hy-Vee’, case=False, regex=False), ‘Hy-Vee’),

(df[‘Store Name’].str.contains(‘Central City’,

case=False, regex=False), ‘Central City’),

(df[‘Store Name’].str.contains(“Smokin‘ Joe’s”,

case=False, regex=False), “Smokin‘ Joe’s”),

(df[‘Store Name’].str.contains(‘Walmart|Wal-Mart’,

case=False), ‘Wal-Mart’),

(df[‘Store Name’].str.contains(‘Fareway Stores’,

case=False, regex=False), ‘Fareway Stores’),

(df[‘Store Name’].str.contains(“Casey‘s”,

case=False, regex=False), “Casey’s General Store”),

(df[‘Store Name’].str.contains(“Sam‘s Club”, case=False, regex=False), “Sam’s Club”),

(df[‘Store Name’].str.contains(‘Kum & Go’, regex=False, case=False), ‘Kum & Go’),

(df[‘Store Name’].str.contains(‘CVS’, regex=False, case=False), ‘CVS Pharmacy’),

(df[‘Store Name’].str.contains(‘Walgreens’, regex=False, case=False), ‘Walgreens’),

(df[‘Store Name’].str.contains(‘Yesway’, regex=False, case=False), ‘Yesway Store’),

(df[‘Store Name’].str.contains(‘Target Store’, regex=False, case=False), ‘Target’),

(df[‘Store Name’].str.contains(‘Quik Trip’, regex=False, case=False), ‘Quik Trip’),

(df[‘Store Name’].str.contains(‘Circle K’, regex=False, case=False), ‘Circle K’),

(df[‘Store Name’].str.contains(‘Hometown Foods’, regex=False,

case=False), ‘Hometown Foods’),

(df[‘Store Name’].str.contains(“Bucky‘s”, case=False, regex=False), “Bucky’s Express”),

(df[‘Store Name’].str.contains(‘Kwik’, case=False, regex=False), ‘Kwik Shop’)

使用np.select很容易遇到条件和值不匹配的情况。所以我们将其合并为元组,以便更容易地跟踪数据匹配。

想使用这种数据结构,我们需要将元组分成两个单独的列表。使用zip来把store_patterns分为store_criteria和store_values:

store_criteria, store_values = zip(*store_patterns)

df[‘Store_Group_1’] = np.select(store_criteria, store_values, ‘other’)

上述代码将用文本值填充每个匹配项。如果没有匹配项,我们给它赋值‘other’。

数据现在长这样:

df.stb.freq([‘Store_Group_1’], value=‘Sale (Dollars)’, style=True, cum_cols=False)

72fa307c-9531-11eb-8b86-12bb97331649.png

看起来比之前好,但仍然有32.28%的‘other’。

思考下这样做是不是更好:如果帐户不匹配,我们使用Store Name字段,而不是‘other’。这样来实现:

df[‘Store_Group_1’] = np.select(store_criteria, store_values, None)

df[‘Store_Group_1’] = df[‘Store_Group_1’].combine_first(df[‘Store Name’])

这里使用了combine_first方法来将Store Name填充None值,这是清理数据时要记住的一个简便技巧。

再来看下数据:

df.stb.freq([‘Store_Group_1’], value=‘Sale (Dollars)’, style=True, cum_cols=False)

73267eb6-9531-11eb-8b86-12bb97331649.png

这样看起来更好了,我们可以根据需要继续细化分组。例如我们可能需要为Costco构建一个字符串查找。

对于这个大型数据集来说,性能也还不错:

13.2 s ± 328 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

这个方法的好处是你可以使用np.select来做数值分析或者上面展示的文本示例,非常灵活。这个方法也有一个弊端,那就是代码量很大。如果你要清理的数据集非常大,那么用这个方法可能导致很多数据和代码混合在一起。那么有没有其他方法可以有差不多的性能,代码更整洁一些?

清洗尝试·3

这里要介绍的解决方案基于Matt Harrison的优秀代码示例,他开发了一个可以做匹配和清洗的generalize函数。我做了一些修改,让这个方法可以在这个示例中使用,我想给Matt一个大大的赞。如果没有他前期99%的工作,我永远不会想到这个解决方案!

def generalize(ser, match_name, default=None, regex=False, case=False):

“”“ Search a series for text matches.

Based on code from https://www.metasnake.com/blog/pydata-assign.html

ser: pandas series to search

match_name: tuple containing text to search for and text to use for normalization

default: If no match, use this to provide a default value, otherwise use the original text

regex: Boolean to indicate if match_name contains a regular expression

case: Case sensitive search

Returns a pandas series with the matched value

”“”

seen = None

for match, name in match_name:

mask = ser.str.contains(match, case=case, regex=regex)

if seen is None:

seen = mask

else:

seen |= mask

ser = ser.where(~mask, name)

if default:

ser = ser.where(seen, default)

else:

ser = ser.where(seen, ser.values)

return ser

这个函数可以在pandas上调用,传参是一个元组列表。第一个元组项是要搜索的值,第二个是要为匹配值填充的值。

以下是等效的模式列表:

store_patterns_2 = [(‘Hy-Vee’, ‘Hy-Vee’), (“Smokin‘ Joe’s”, “Smokin‘ Joe’s”),

(‘Central City’, ‘Central City’),

(‘Costco Wholesale’, ‘Costco Wholesale’),

(‘Walmart’, ‘Walmart’), (‘Wal-Mart’, ‘Walmart’),

(‘Fareway Stores’, ‘Fareway Stores’),

(“Casey‘s”, “Casey’s General Store”),

(“Sam‘s Club”, “Sam’s Club”), (‘Kum & Go’, ‘Kum & Go’),

(‘CVS’, ‘CVS Pharmacy’), (‘Walgreens’, ‘Walgreens’),

(‘Yesway’, ‘Yesway Store’), (‘Target Store’, ‘Target’),

(‘Quik Trip’, ‘Quik Trip’), (‘Circle K’, ‘Circle K’),

(‘Hometown Foods’, ‘Hometown Foods’),

(“Bucky‘s”, “Bucky’s Express”), (‘Kwik’, ‘Kwik Shop’)]

这个方案的一个好处是,与前面的store_patterns示例相比,维护这个列表要容易得多。

我对generalize函数做的另一个更改是,如果没有提供默认值,那么将保留原始值,而不是像上面那样使用combine_first函数。最后,为了提高性能,我默认关闭了正则匹配。

现在数据都设置好了,调用它很简单:

df[‘Store_Group_2’] = generalize(df[‘Store Name’], store_patterns_2)

性能如何?

15.5 s ± 409 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

比起上面它稍微有一点慢,但我认为它是一个更优雅的解决方案,如果我要做一个类似的文本清理工作,我会用这个方法。

这种方法的缺点是,它只能做字符串清洗。而np.select也可以应用于数值,所以应用范围更广。

关于数据类型

在pandas的最新版本中,有一个专用的字符串类型。我尝试将Store Name转换为该字符串类型,想看是否有性能优化。结果没有看到任何变化。不过,未来有可能会有速度的提升,这点大家可以关注一下。

虽然string类型没有什么区别,但是category类型在这个数据集上显示了很大的潜力。有关category数据类型的详细信息,可以参阅我的上一篇文章:https://pbpython.com/pandas_dtypes_cat.html。

我们可以使用astype将数据转换为category类型:

df[‘Store Name’] = df[‘Store Name’].astype(‘category’)

然后我们跟之前那样在运行np.select的方法

df[‘Store_Group_3’] = np.select(store_criteria, store_values, None)

df[‘Store_Group_3’] = df[‘Store_Group_1’].combine_first(df[‘Store Name’])

786 ms ± 108 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

我们只做了一个简单的改动,运行时间从13秒到不到1秒。太神了!效果这么明显的原因其实很简单。当pandas将列转换为分组类型时,它只会对每个唯一的文本值调用珍贵的str.contains()函数。因为这个数据集有很多重复的数据,所以我们得到了巨大的性能提升。

让我们看看这是否适用于我们的generalize函数:

df[‘Store_Group_4’] = generalize(df[‘Store Name’], store_patterns_2)

不幸的是报错了:

ValueError: Cannot setitem on a Categorical with a new category, set the categories first

这个错误让我回忆起我过去处理分组数据时遇到的一些挑战。当你合并和关联分组数据时,你很容易遇到这些错误。

我试图找到一个比较好的方法来修改generage(),想让它起作用,但目前还没找到。如果有任何读者能找到方法,可以联系我获得奖金。这里,我们通过构建一个查找表来复制Category方法。

查找表

正如我们通过分类方法了解到的,这个数据集有很多重复的数据。我们可以构建一个查找表,每个字符串处理一次资源密集型函数。

为了说明它是如何在字符串上工作的,我们将值从category转换回字符串类型:

df[‘Store Name’] = df[‘Store Name’].astype(‘string’)

首先,我们构建一个包含所有唯一值的lookup DataFrame并运行generalize函数:

lookup_df = pd.DataFrame()

lookup_df[‘Store Name’] = df[‘Store Name’].unique()

lookup_df[‘Store_Group_5’] = generalize(lookup_df[‘Store Name’], store_patterns_2)

7338d782-9531-11eb-8b86-12bb97331649.png

我们可以把它合并到最终的DataFrame:

df = pd.merge(df, lookup_df, how=‘left’)

1.38 s ± 15.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

比起np.select使用分组数据的方法稍慢一些,但是代码可读性和易维护程度更高,性能和这两者之间其实需要掌握一个平衡。

此外,中间的lookup_df可以很好的输出给分析师共享,从而让分析师帮助你清洗更多数据。这可能节省你几小时的时间!

总结

根据我的经验,通过本文中概述的清洗示例,你可以了解很多关于底层数据的信息。

我推测你会在你的日常分析中发现很多需要进行文本清理的案例,就像我在本文中展示的那样。

下面是本文解决方案的简要总结:

解决方案执行时间注释

np.select13s可用于非文本分析

generalize15s只支持文本

分组数据和np.select786ms在合并和关联时,分组数据可能会变得棘手

查找表和generalize1.3s查找表可以由其他人维护

对于一些数据集来说,性能不是问题,所以你可以随意选择。

然而,随着数据规模的增长(想象一下对50个州的数据进行分析),你需要了解如何高效地使用pandas进行文本清洗。我建议你可以收藏这篇文章,当你面对类似的问题时可以再回来看看。

当然,如果你有一些其他的建议,可能会对别人有用,可以写在评论里。如果你知道如何使我的generalize函数与分组数据一起工作,也记得告诉我。
编辑:lyn

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据分析
    +关注

    关注

    2

    文章

    1508

    浏览量

    35945
  • python
    +关注

    关注

    57

    文章

    4858

    浏览量

    89602

原文标题:用 pandas 高效清洗文本数据

文章出处:【微信号:DBDevs,微信公众号:数据分析与开发】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    功率放大器在磁场激励下对狭窄结构化液体环境适应性研究的应用

    实验名称: 磁场激励下对狭窄结构化液体环境的卓越环境适应性和出色的3D可控性验证 研究方向: 在临床实践中,天然孔口通常为医疗器械提供对各种靶组织的侵入性通路。这些体腔/腔(例如泌尿和消化系统
    的头像 发表于 11-25 10:04 87次阅读
    功率放大器在磁场激励下对狭窄<b class='flag-5'>非</b><b class='flag-5'>结构化</b>液体环境适应性研究的应用

    结构化布线中使用电缆标签的4个主要优势

    在现代通信和网络基础设施建设中,结构化布线系统扮演着至关重要的角色。它不仅确保了网络的高效传输和稳定运行,还为未来的扩展和维护提供了便利。而在结构化布线中,电缆标签的使用虽看似是一个小小的细节,却能
    的头像 发表于 11-24 10:34 171次阅读

    图文详情编辑接口

    逐步讲解接口的设计、实现和使用,帮助您理解如何构建一个可靠的技术方案。 1. 接口设计原则 设计图文详情编辑接口时,需考虑以下关键点: 数据模型 :图文内容通常包含文本字段(如标题、正文)和图片字段(如图片URL或二进制数据)。例
    的头像 发表于 10-23 16:24 116次阅读
    图文详情编辑接口

    Python 给 Amazon 做“全身 CT”——可量产、可扩展的商品详情爬虫实战

    一、技术选型:为什么选 Python 而不是 Java? 结论: “调研阶段用 Python,上线后如果 QPS 爆表再考虑 Java 重构。” 二、整体架构速览(3 分钟看懂) 三、开发前准备(5
    的头像 发表于 10-21 16:59 388次阅读
    用 <b class='flag-5'>Python</b> 给 Amazon 做“全身 CT”——可量产、可扩展的商品详情爬虫实战

    NetApp助力对象存储现代,提升速度、可扩展性和安全性

    计。最新版本的StorageGRID将引入新功能,旨在推进人工智能计划、提高数据安全性并实现企业数据基础设施的现代。 无论企业是处于数据湖现代的早期阶段,还是正在试验高级人工智能应用程序,他们都需要管理并存储激增的
    的头像 发表于 09-11 10:41 332次阅读

    深度学习对工业物联网有哪些帮助

    、实施路径三个维度展开分析: 一、深度学习如何突破工业物联网的技术瓶颈? 1. 结构化数据处理:解锁“沉睡数据”价值 传统困境 :工业物联网中70%以上的数据为结构化数据(如设备振
    的头像 发表于 08-20 14:56 772次阅读

    智能体AI面临结构化数据难题:IBM推出解决方案

    ,同时提供一个开放的混合数据基础架构和企业级的结构化结构化数据管理。 智能体AI面临结构化数据难题:IBM推出解决方案 测试结果显示,
    的头像 发表于 07-02 09:40 393次阅读

    DDN携手NVIDIA释放结构化数据的AI价值

    DDN 通过与 NVIDIA AI 数据平台的合作,推出联合解决方案,简化了企业对结构化数据的存储、访问和激活方式,助力客户释放生成式 AI 的巨大商业潜能。DDN 全球合作伙伴负责人
    的头像 发表于 06-10 10:31 784次阅读

    从零到一:如何利用结构化数据中台快速部署AI解决方案

    在企业数字转型的浪潮中,AI已从概念走向落地应用。然而,真正能高效、安全地部署AI解决方案的企业仍属少数,瓶颈往往出在“数据”——尤其是非结构化数据的管理与应用上。文件、音频、视频、图像、邮件
    的头像 发表于 04-14 13:50 609次阅读

    树莓派AI套件:如何将混乱的数据变成有序的魔法

    使用RaspberryPiAI套件进行结构化数据处理——Hailo边缘AI结构化数据处理、RaspberryPi5、RaspberryPiAI套件、Milvus、Zilliz、数据
    的头像 发表于 03-25 09:37 775次阅读
    树莓派AI套件:如何将混乱的数据变成有序的魔法

    爬虫数据获取实战指南:从入门到高效采集

    高效又合规地获取数据?本文将为你拆解完整流程,并推荐一款助力数据采集的“黄金搭档”——IPIDEA全球代理,让你的数据获取事半功倍! 一、需求规划: 明确目标,精准出击 关键问题: 数据类型:需要文本、图片、视频,还是结构化
    的头像 发表于 03-24 14:08 1259次阅读

    VirutualLab Fusion应用:结构光照明的显微镜系统

    摘要 与阿贝理论预测的分辨率相比,用于荧光样品的结构照明显微镜系统可以将显微镜系统的分辨率提高2倍。 VirutualLab Fusion提供了一种通过入射波属性来研究结构化照明模式的快速方法
    发表于 03-21 09:26

    结构化数据中台:企业AI应用安全落地的核心引擎

    在数字转型浪潮中,结构化数据(如文档、图片、音视频等)已成为企业核心资产,其价值挖掘能力直接影响AI应用的效能与安全性。然而,数据分散、多模态处理复杂、安全合规风险高等问题,严重制约了企业AI
    的头像 发表于 02-27 17:06 851次阅读

    对象存储是什么结构类型?

    对象存储属于结构化数据存储架构,采用扁平命名空间结构。其核心通过唯一标识符(ObjectID)定位数据对象,突破传统文件系统的层级目录限制,形成"桶-对象"两级逻辑模型。数据以独立
    的头像 发表于 02-10 11:14 696次阅读

    戴尔数据湖仓助力企业数字转型

    在数字转型的浪潮下,企业正面临着前所未有的数据挑战。从传统的结构化数据到如今的结构化数据、半结构化数据,每一种类型都对企业的存储和分析能
    的头像 发表于 12-20 09:31 774次阅读