本节目标:对于字符串的清洗,空值的处理,重复值的处理.
本节技术点:dropna(),strip(),replace,extract,isna
本节阅读需要(20)min。 本节实操需要(20)min。
- pandas数据清洗实战
- 前言
- 一、字符串类型
- 类型约束
- str方法容器
- 常见的清洗
- 提取和拆分(高级)
- 二、数值类型
- 类型约束
- NA的处理
- 三、其他的操作
- 总结
数据清洗的目的是为了得到我们的目标值.也就是成为我们需要的样子。 所以这个概念很宽泛。
理论上来说只要学会行列操作不就好了吗?
😄不对的,因为我们上游的数据往往是残缺的,格式奇怪的,有重复的。 所以我们需要针对一些常见的过滤情况做一下学习。
因为我们需要处理的数据总体分为两大类,string类型和数值类型。 所以清洗也就分为了两大块。
一、字符串类型先熟悉一下字符串的常见操作。因为我们处理数据是以一个数据列为单元。 所以我们用series来举例。字符串一般也是按列处理的。因为一般都有具体含义
类型约束首先我们要确保类型是字符串,尤其是数值格式的字符串。 我们可以在初始化读入的时候通过dtype
s = pd.Series(["a", 2, np.nan], dtype="string")
或者之后astype约束类型为字符串类型
s.astype("string")
注意:
np.nan不会被转化为string,相当于跳过这个。
s.dropna() 可以去除np.nan。一般都会在处理之前先过滤空值!!!
str方法容器我个人认为str是pandas处理字符串的容器,相当于抽象数据类型。
s = pd.Series(["a", None, "b"], dtype="string")
s.str.count("a") # 计数a这个字符串出现的次数。
s.str.len()
首先都需要str之后再调用各种pd重载的字符串方法。
s.str.isdigit() # 判断是否是小数
s.str.match("a")
s.str.contains("a", na=False)
# 进一步筛选
s[s.str.contains("a", na=False)]
返回的都是boolean类型的,所以根据前面所学可以作为清洗的输入参数。 比如s[s.str.contains(“a”, na=False)]那么只剩下a一个了。
字符串的连接
我们有的时候需要个性化的修改某一列的字符串内容。
# 没有参数时自身像列表一样链接
s = pd.Series(["a", "b", "c", "d"], dtype="string")
s.str.cat(sep=",") # 'a,b,c,d'
# 有参数时,像广播一样对位相加。
t = pd.Series(["a", "b", np.nan, "d"], dtype="string")
s.str.cat(t)
0 aa
1 bb
2 # 注意nan
3 dd
dtype: string
d = pd.concat([t, s], axis=1)
s.str.cat(d, na_rep="-") # 针对na进行了替换
此外,cat还有其他参数甚至接近于merge,但是我不建议用cat处理过于复杂的数据。 一般都是通过已有的两个字符串列合并为一个新的列.
常见的清洗我根据我的经验按照常见的清洗顺序整理如下:
- 读入之后一般是去除空值比较多。所以一般s.dropna()。
- 字符串原始文件中常常还有多余的空格,s.str.strip()去除两边的多余空格。
- 格式一致化如果是纯字母的往往需要大小写一致,s.str.lower()等来大小写一致。或者使用replace(" ", “_”)来替换
- 这一步一般是过滤,无非两种,通过匹配过滤;或者通过统计过滤比如len()长度。
# 去除两边的多余字符
s.str.strip()
s.str.lstrip()
s.str.rstrip()
s.str.removeprefix("str_")
s.str.removesuffix("_str")
replace大魔王!!!
dollars = pd.Series(["12", "-$10", "$10,000"], dtype="string")
dollars.str.replace(r"-\$", "-", regex=True)
dollars.str.replace("-$", "-", regex=False)
replace对于清洗中的格式一致化十分重要功能也很强大。 上面的效果一致。我建议在复杂的时候用regex,简单的时候关闭直接替换就行。 看一个比较复杂的replace的实例
pat = r"[a-z]+"
def repl(m):
return m.group(0)[::-1]
pd.Series(["foo 123", "bar baz", np.nan], dtype="string").str.replace(
pat, repl, regex=True
)
group(0)的意思是re.match之后的捕获分组的第一个。[::-1]字符串反转
0 oof 123
1 rab zab
2
dtype: string
所以结果如此也不难理解。 总体来看要想replace用的好需要很要的regex功底。 基本都是如下格式:
s[s.str.contains(“a”, na=False)]
字符串的过滤一般是匹配为主
提取和拆分(高级)有的时候比如说我们面对这样的字符串。比如说“数学一般语文优秀”。 我们需要拆分为两列,一列描述语文,一列描述数学。
用到str.extract,返回的是一个df对象!!!类似于Excel分列
s = pd.Series(["语文优秀数学一般", "语文一般数学优秀", "语文优秀数学优秀"], index=["A", "B", "C"], dtype="string")
two_groups = u"语文(?P.*)数学(?P.*)"
pd1 = s.str.extract(two_groups, expand=True)
# pd1.columns = ["语文","数学"] # 可省略
pd1
?P通过分组匹配添加标签,我们可以省略如上的列名。 P一定要大写
二、数值类型数值类型相对于字符串还简单点. 基本就是通过基础运算和比较运算.
类型约束数值信息的每一列都需要严格的约束数据类型.比如int8,int16等.
pd.array([1, 2, np.nan, None, pd.NA], dtype="Int64")
s.astype("Int64")
正常的数会强制转换为dtype的类型.np.nan, None, pd.NA都会转化为pd.NA类型. pd.NA一般是不可以参加计算的!!! 或者也可以认为pd.NA参与的运算结果基本还是pd.NA. groupby等都是直接忽略掉。
# 抽样
s = pd.Series([0, 1, 2, 3, 4, 5])
s.sample()
s.sample(n=3)
s.sample(frac=0.5)
一般抽样之后会评估一下数据如何处理,是作为预处理步骤的。
NA的处理个人经验来看,因为大部分情况下数据都是有意义的。 所以要么用对应的空值代替,要么用对应的平均值等代替。 这个要看具体的业务内容了。
pandas官网写了一大通。 **但是第一剔除,第二填充。**相信我这是真理
粗暴的删除
# 删除有两种
df.dropna(axis=0) # 按行删除
df.dropna(axis=1) # 按列删除
一般按行多一点,删除无效的数据条目。 有选择的删除
df = pd.DataFrame(
np.random.randn(5, 3),
index=["a", "c", "e", "f", "h"],
columns=["one", "two", "three"],
)
df2 = df.reindex(["a", "b", "c", "d", "e", "f", "g", "h"]) # 制造NA
df2.isna()
s1 = df2["one"]
s1[~s1.isna()] # 等价于s1[s1.notna()]
s2 = s1[s1.notna()]
s2 # series会自动删除NA
series会自动删除NA,但是df不会。但是够了,我们可以用列进一步删选出行。
df2.loc[s2.index]
我们两次用到了筛选器,第一次是series通过【boolean列表】筛选, 第二次是df对象通过loc【index】的方式筛选的。
填充
df2.fillna(0)
df2["one"].fillna("missing")
三、其他的操作
dates = pd.date_range('1/1/2000', periods=8)
df = pd.DataFrame(np.random.randn(8, 4),
index=dates, columns=['A', 'B', 'C', 'D'])
# 添加自然数index
dfa.A = list(range(len(dfa.index))) # A需要提前存在
dfa['A'] = list(range(len(dfa.index))) # 如果没有就初始化,有就替换
# 交换两列的位置.
df[['B', 'A']] = df[['A', 'B']] # 等价于df.loc[:, ['B', 'A']] = df[['A', 'B']]
# 重新设置index
df.index = df['A'] # 不等价于
df.reindex(df['A']) # 上下截然不同,会有很多NA,因为没有意义
df.loc[:, [‘B’, ‘A’]]还可以这么写df.loc[:, lambda df: [‘A’, ‘B’]] dfd.columns.get_loc(‘A’) dfd.columns.get_indexer([‘A’, ‘B’]) 我只想说,秀!!!但是我觉得没有记忆的必要。。。
其实意思是一样的都是传入位置信息的列表。
s.reindex([1, 2, 3])虽然设置index但是是相对于原来的主键扩充或减少,是会维持原来主键的意义以及关系的。
总结tips: 大部分的聚类算法都是忽略NA值的。 reindex之后很容易产生大片的NA。
一个重要的方法论。
很多时候,我们的数据的量是很大的。而我们只是为了获得某一个宏观的结果!!!
所以我们的过滤必须要和我们的业务保持密切的联系。
不应该在过滤上花费过多的时间和精力,少部分杂项也是不会影响整体结论的。
吾生也有涯而知也无涯以有涯随无涯殆已 --庄子