随着使用 Python 和 R 语言次数的增加,对于这两门语言在数据科学领域的优劣性有着深刻的体会。
R 语言社区活跃且包丰富多样,Tidyverse 风潮更是让这门语法怪异的编程语言焕发新生,也让其在数据处理和分析的能力上更进一步,但 R 语言相比于 Python 来说又缺乏了通用性;数据科学对于 Python 来说仅仅只是其中一个领域,随着 Numpy 和 Pandas 构建起来的生态圈蓬勃发展,也成为了一个与 R 语言在数据科学领域强有力的竞争对手,但尽管 Pandas 已经涵盖了大部分我们平时处理和分析数据时的基本需求,可在流程和方法上却又总比 R 语言匮乏不少。
通常来说,如果是一些数据处理或清洗的工作或任务,我更喜欢使用 R 语言,因为得益于 Hadly Wickham 等人的努力,R 语言有着一套舒服的操作流程,如管道操作符 %>%
、函数式编程 purrr
包、使用 nest()
函数来构造统一颗粒度的包裹性数据等。但在工作中使用一门编程语言往往既要考虑通用性,还要考虑团队的协作性,因此在实际工作中我使用更多的是 Python 而非 R 语言。
在使用 Pandas 进行数据处理时,有时候会碰上一些本该很容易处理但却还要额外多定义一个函数的情况。
比如我数据中有两个字段 a
和 b
,但是两个字段或多或少都有缺失值。
In [2]: import pandas as pd
...: import numpy as np
...:
...: df = pd.DataFrame(
...: {
...: "a": [None, 2, None, None, 5, 6],
...: "b": [1, None, None, 4, None, 6]
...: }
...: )
...: df
Out[2]:
a b
0 NaN 1.0
1 2.0 NaN
2 NaN NaN
3 NaN 4.0
4 5.0 NaN
5 6.0 6.0
所以我需要定义一个新的字段 c
,它由两个字段构建而来。如果第一个字段中存在缺失值,则取第二个字段中的值,反之亦可;如果两者都为缺失,则保留缺失值。
为了实现这个目的通常来说都是定义一个函数,然后用 apply()
方法来生成:
In [3]: def get_valid_value(col_x, col_y):
...: if not pd.isna(col_x) and pd.isna(col_y):
...: return col_x
...: elif pd.isna(col_x) and not pd.isna(col_y):
...: return col_y
...: elif not (pd.isna(col_x) or pd.isna(col_y)):
...: return col_x
...: else:
...: return np.nan
...:
...: df['c'] = df.apply(lambda x: get_valid_value(x['a'], x['b']), axis=1)
...: df
Out[3]:
a b c
0 NaN 1.0 1.0
1 2.0 NaN 2.0
2 NaN NaN NaN
3 NaN 4.0 4.0
4 5.0 NaN 5.0
5 6.0 6.0 6.0
这种需求其实很常见,在 SQL 中存在 coalesc()
这样一个函数,实现的就是上述我所描述的这种拼凑字段的做法;在 R 语言的 dplyr
包中也已经实现了 SQL 同名函数一样的方法。而 Pandas 只实现了不同 DataFrame 间的方法 DataFrame.combine()
,并没有实现单个 DataFrame 中字段的 coalesc()
方法。但好在 pyjanitor
弥补了 Pandas 在处理数据时的一些不足,而且也能更好地嵌入到我们的工作流中。这也就是为什么本文要谈论 pyjanitor
的原因。
与链式方法紧密结合的操作方式
pyjanitor
库的灵感来自于 R 语言的 janitor
包,英文单词即为清洁工之意,也就是通常用来进行数据处理或清洗数据。pyjanitor
脱胎于 Pandas 生态圈,其使用的核心也是围绕着链式展开,可以使得我们更加专注于每一步操作的动作或谓词(Verbs)。
pyjanitor
的 API 文档并不复杂,大多数 API 都是围绕着通用的清洗任务而设计。这主要涉及为几部分:
- 操作列的方法(Modify columns)
- 操作值的方法(Modify values)
- 用于筛选的方法(Filtering)
- 用于数据预处理的方法(Preprocessing),主要是机器学习特征处理的一些方法
- 其他方法
由于篇幅有限,不能将每个方法都一一举例,这里我就只挑其中几个方法给出使用示例。
需要注意的是,尽管 pyjanitor
库名称带有 py
二字,但是在导入时则是输入 janitor
;就像 Beautifulsoup4
库在导入时写为 bs4
一样,以免无法导入而报错。
coalesc
有了 pyjanitor
之后,开头我举的例子其实就可以通过 coalesc()
方法来快速实现,就像这样:
In [8]: import pandas as pd
...: import janitor
...:
...: df = pd.DataFrame(
...: {
...: "a": [None, 2, None, None, 5, 6],
...: "b": [1, None, None, 4, None, 6]
...: }
...: )
...:
...: df.coalesce(column_names=['a','b'],
...: new_column_name='c',
...: delete_columns=False)
Out[8]:
a b c
0 NaN 1.0 1.0
1 2.0 NaN 2.0
2 NaN NaN NaN
3 NaN 4.0 4.0
4 5.0 NaN 5.0
5 6.0 6.0 6.0
从结果上可以看到,我们不需要再额外写一个方法,直接就可以以符合直觉的方式来完成相应的操作。
concatenate_columns 和 deconcatnate_column
如果你有使用过 R 语言 tidyr
包的 unite()
函数和 separate()
函数,那么其实使用 pyjanitor
的 concatenate_columns()
和 deconcatnate_column()
就不会陌生,前者是将多个列根据某个分隔符合并成一个新列,而后者则是将单个列拆分成多个列。这里我们假设数据中有一个关于日期时间的字段,围绕这个字段来进行演示:
In [1]: import pandas as pd
...: import janitor
...:
...: df = pd.DataFrame({"date_time": ["2020-02-01 11:00:00",
...: "2020-02-03 12:10:11",
...: "2020-03-24 13:24:31"]})
In [2]: (
...: df
...: .deconcatenate_column(
...: column_name="date_time",
...: new_column_names=['date', 'time'],
...: sep=' ',
...: preserve_position=False
...: )
...: .deconcatenate_column(
...: column_name="date",
...: new_column_names=['year', 'month', 'day'],
...: sep='-',
...: preserve_position=True
...: )
...: .concatenate_columns(
...: column_names=['year', 'month', 'day'],
...: new_column_name='new_date',
...: sep='-'
...: )
...: )
Out[2]:
date_time year month day time new_date
0 2020-02-01 11:00:00 2020 02 01 11:00:00 2020-02-01
1 2020-02-03 12:10:11 2020 02 03 12:10:11 2020-02-03
2 2020-03-24 13:24:31 2020 03 24 13:24:31 2020-03-24
这个例子可能有些无聊,但是能很清楚地看到这两个方法帮我们顺利地将数据中的字段进行拆分和合并,虽然说我们可以直接通过 assign()
方法来实现变量赋值,但是不可避免的要写三遍;同时尽管 Pandas 已经可以通过 str.split(sep, expand=True)
的方式来对字符类型字段进行分隔并转换成相应的字段,但是最后返回的是一个新的 DataFrame
,不能直接和原有的数据合并在一起。
从结果中我们可以看到,pyjanitor
提供的方法可以帮助我们很好地保持数据的一致性和统一性。
take_first
有的时候,我们会 groupby()
某个字段并对一些数值列进行操作、倒序排列,最后每组取最大的数即倒序后的第一行。在 R 语言中我们可以很轻易直接这么实现:
library(dplyr)
df <- data.frame(a = c("x", "x", "y", "y", "y"),
b = c(1, 3, 2, 5, 4))
df %>%
group_by(a) %>%
arrange(desc(b)) %>%
slice(1) %>%
ungroup()
# A tibble: 2 x 2
# a b
# <fct> <dbl>
# 1 x 3
# 2 y 5
在没使用 pyjanitor
之前,我往往都是通过 Pandas 这么实现的:
In [1]: import pandas as pd
...:
...: df = pd.DataFrame({"a":["x", "x", "y", "y", "y"],
...: "b":[1,3,2,5,4]})
...: (
...: df
...: .groupby("a")
...: .apply(lambda grp: grp
...: .sort_values(by="b", ascending=False)
...: .head(1))
...: .reset_index(drop=True)
...: )
Out[1]:
a b
0 x 3
1 y 5
这里利用了 groupby
之后的生成的 DataFrameGroupBy
对象再进行多余的降序取第一个的操作,最后将分组后产生的索引值删除。现在可以直接使用 pyjanitor
中的 take_first
方法直接一步到位:
In [1]: import pandas as pd
...: import janitor
...:
...: df = pd.DataFrame({"a":["x", "x", "y", "y", "y"],
...: "b":[1,3,2,5,4]})
...: df.take_first(subset="a", by="b", ascending=False)
Out[1]:
a b
3 y 5
1 x 3
除了以上列举的方法之外,还有许多方法等待各位去探索,详见官方文档,官方文档上还贴心的给出了一些实际的用法和案例;只要你熟练使用了 Pandas 那么很快就能掌握 pyjanitor
库的大部分方法。
「有 Pandas 内味儿」——实现你的 janitor 方法
pyjanitor
中的方法仅仅只是一些通用的实现方法,不同的人在使用过程中可能也会有不同的需要。但好在我们也可以实现自己的 「janitor」 方法。
pyjanitor
得益于 pandas-flavor
库的加持得以轻松实现链式方法,链式方法的简单实现原理见我之前的文章《5 分钟解读 Python 中的链式调用》。
pandas-flavor
提供了能让使用者简单且快速地编写出**带有「 Pandas 味儿」**的方法:
- 第一步,只需要在你编写的函数、方法或类中添加对应的装饰器即可;
- 第二步,确保最后返回的是 DataFrame 或 Series 类的对象即可。
比如我们写一个简单清理数据字段或变量名称多余空格的方法:
import pandas as pd
import pandas_flavor as pf
@pf.register_dataframe_method
def strip_names(df):
import re
colnames = df.columns.tolist()
colnames = list(map(lambda col: '_'.join(re.findall(r"\w+", col)), colnames))
df.columns = colnames
return df
最后结果如下:
In [14]: data = pd.DataFrame({" a ": [1,1], " b zz ": [2,1]})
...: data
Out[14]:
a b zz
0 1 2
1 1 1
In [15]: data.strip_names()
Out[15]:
a b_zz
0 1 2
1 1 1
本质上来说,pandas-flavor
库中提供的装饰器就等价于重写或新增了 DataFrame 类的方法,在使用过程中如果方法有报错,那就需要还原加载 pandas
库之后再重新写入。
关于 pandas-flavor
装饰器的用法,详见项目的 Github(https://github.com/Zsailer/pandas_flavor)
结尾
通过 pyjanitor
库我们可以更进一步地丰富我们在处理数据时的工作流,并且借助链式方法的特性来缩短数据分析或挖掘过程的耗时。
但也正如我在之前谈论有关链式调用的文章中所提到的,随着链式调用的方法或过程的增多,出错的几率也会大大增加。只有当你确定以及肯定经过每一步处理后返回的结果与你预期中的呈现形式相符时,才能保证链式方法链的稳健。
无论如何,pyjanitor
从一定程度上也扩展了 Pandas 生态在处理数据上的多样性和玩法。
作者:100gle,练习时长不到两年的非正经文科生一枚,喜欢敲代码、写写文章、捣鼓捣鼓各种新事物;现从事有关大数据分析与挖掘的相关工作。