用 Pyjanitor 更好地進(jìn)行數(shù)據(jù)清洗與處理

隨著使用 Python 和 R 語言次數(shù)的增加,對(duì)于這兩門語言在數(shù)據(jù)科學(xué)領(lǐng)域的優(yōu)劣性有著深刻的體會(huì)。
R 語言社區(qū)活躍且包豐富多樣,Tidyverse 風(fēng)潮更是讓這門語法怪異的編程語言煥發(fā)新生,也讓其在數(shù)據(jù)處理和分析的能力上更進(jìn)一步,但 R 語言相比于 Python 來說又缺乏了通用性;數(shù)據(jù)科學(xué)對(duì)于 Python 來說僅僅只是其中一個(gè)領(lǐng)域,隨著 Numpy 和 Pandas 構(gòu)建起來的生態(tài)圈蓬勃發(fā)展,也成為了一個(gè)與 R 語言在數(shù)據(jù)科學(xué)領(lǐng)域強(qiáng)有力的競(jìng)爭(zhēng)對(duì)手,但盡管 Pandas 已經(jīng)涵蓋了大部分我們平時(shí)處理和分析數(shù)據(jù)時(shí)的基本需求,可在流程和方法上卻又總比 R 語言匱乏不少。
通常來說,如果是一些數(shù)據(jù)處理或清洗的工作或任務(wù),我更喜歡使用 R 語言,因?yàn)榈靡嬗?Hadly Wickham 等人的努力,R 語言有著一套舒服的操作流程,如管道操作符 %>%、函數(shù)式編程 purrr 包、使用 nest() 函數(shù)來構(gòu)造統(tǒng)一顆粒度的包裹性數(shù)據(jù)等。但在工作中使用一門編程語言往往既要考慮通用性,還要考慮團(tuán)隊(duì)的協(xié)作性,因此在實(shí)際工作中我使用更多的是 Python 而非 R 語言。
在使用 Pandas 進(jìn)行數(shù)據(jù)處理時(shí),有時(shí)候會(huì)碰上一些本該很容易處理但卻還要額外多定義一個(gè)函數(shù)的情況。
比如我數(shù)據(jù)中有兩個(gè)字段 a 和 b,但是兩個(gè)字段或多或少都有缺失值。
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
所以我需要定義一個(gè)新的字段 c,它由兩個(gè)字段構(gòu)建而來。如果第一個(gè)字段中存在缺失值,則取第二個(gè)字段中的值,反之亦可;如果兩者都為缺失,則保留缺失值。
為了實(shí)現(xiàn)這個(gè)目的通常來說都是定義一個(gè)函數(shù),然后用 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
這種需求其實(shí)很常見,在 SQL 中存在 coalesc() 這樣一個(gè)函數(shù),實(shí)現(xiàn)的就是上述我所描述的這種拼湊字段的做法;在 R 語言的 dplyr 包中也已經(jīng)實(shí)現(xiàn)了 SQL 同名函數(shù)一樣的方法。而 Pandas 只實(shí)現(xiàn)了不同 DataFrame 間的方法 DataFrame.combine(),并沒有實(shí)現(xiàn)單個(gè) DataFrame 中字段的 coalesc() 方法。但好在 pyjanitor 彌補(bǔ)了 Pandas 在處理數(shù)據(jù)時(shí)的一些不足,而且也能更好地嵌入到我們的工作流中。這也就是為什么本文要談?wù)?pyjanitor 的原因。
與鏈?zhǔn)椒椒ňo密結(jié)合的操作方式
pyjanitor 庫的靈感來自于 R 語言的 janitor 包,英文單詞即為清潔工之意,也就是通常用來進(jìn)行數(shù)據(jù)處理或清洗數(shù)據(jù)。pyjanitor 脫胎于 Pandas 生態(tài)圈,其使用的核心也是圍繞著鏈?zhǔn)秸归_,可以使得我們更加專注于每一步操作的動(dòng)作或謂詞(Verbs)。
pyjanitor 的 API 文檔并不復(fù)雜,大多數(shù) API 都是圍繞著通用的清洗任務(wù)而設(shè)計(jì)。這主要涉及為幾部分:
操作列的方法(Modify columns)
操作值的方法(Modify values)
用于篩選的方法(Filtering)
用于數(shù)據(jù)預(yù)處理的方法(Preprocessing),主要是機(jī)器學(xué)習(xí)特征處理的一些方法
其他方法
由于篇幅有限,不能將每個(gè)方法都一一舉例,這里我就只挑其中幾個(gè)方法給出使用示例。
需要注意的是,盡管 pyjanitor 庫名稱帶有 py 二字,但是在導(dǎo)入時(shí)則是輸入 janitor;就像 Beautifulsoup4 庫在導(dǎo)入時(shí)寫為 bs4 一樣,以免無法導(dǎo)入而報(bào)錯(cuò)。
coalesc
有了 pyjanitor 之后,開頭我舉的例子其實(shí)就可以通過 coalesc() 方法來快速實(shí)現(xiàn),就像這樣:
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
從結(jié)果上可以看到,我們不需要再額外寫一個(gè)方法,直接就可以以符合直覺的方式來完成相應(yīng)的操作。
concatenate_columns 和 deconcatnate_column
如果你有使用過 R 語言 tidyr 包的 unite() 函數(shù)和 separate() 函數(shù),那么其實(shí)使用 pyjanitor 的 concatenate_columns() 和 deconcatnate_column() 就不會(huì)陌生,前者是將多個(gè)列根據(jù)某個(gè)分隔符合并成一個(gè)新列,而后者則是將單個(gè)列拆分成多個(gè)列。這里我們假設(shè)數(shù)據(jù)中有一個(gè)關(guān)于日期時(shí)間的字段,圍繞這個(gè)字段來進(jìn)行演示:
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
這個(gè)例子可能有些無聊,但是能很清楚地看到這兩個(gè)方法幫我們順利地將數(shù)據(jù)中的字段進(jìn)行拆分和合并,雖然說我們可以直接通過 assign() 方法來實(shí)現(xiàn)變量賦值,但是不可避免的要寫三遍;同時(shí)盡管 Pandas 已經(jīng)可以通過 str.split(sep, expand=True) 的方式來對(duì)字符類型字段進(jìn)行分隔并轉(zhuǎn)換成相應(yīng)的字段,但是最后返回的是一個(gè)新的 DataFrame,不能直接和原有的數(shù)據(jù)合并在一起。
從結(jié)果中我們可以看到,pyjanitor 提供的方法可以幫助我們很好地保持?jǐn)?shù)據(jù)的一致性和統(tǒng)一性。
take_first
有的時(shí)候,我們會(huì) groupby() 某個(gè)字段并對(duì)一些數(shù)值列進(jìn)行操作、倒序排列,最后每組取最大的數(shù)即倒序后的第一行。在 R 語言中我們可以很輕易直接這么實(shí)現(xiàn):
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
#
# 1 x 3
# 2 y 5
在沒使用 pyjanitor 之前,我往往都是通過 Pandas 這么實(shí)現(xiàn)的:
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 對(duì)象再進(jìn)行多余的降序取第一個(gè)的操作,最后將分組后產(chǎn)生的索引值刪除。現(xiàn)在可以直接使用 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
除了以上列舉的方法之外,還有許多方法等待各位去探索,詳見官方文檔,官方文檔上還貼心的給出了一些實(shí)際的用法和案例;只要你熟練使用了 Pandas 那么很快就能掌握 pyjanitor 庫的大部分方法。
「有 Pandas 內(nèi)味兒」——實(shí)現(xiàn)你的 janitor 方法
pyjanitor 中的方法僅僅只是一些通用的實(shí)現(xiàn)方法,不同的人在使用過程中可能也會(huì)有不同的需要。但好在我們也可以實(shí)現(xiàn)自己的 「janitor」 方法。
pyjanitor 得益于 pandas-flavor 庫的加持得以輕松實(shí)現(xiàn)鏈?zhǔn)椒椒?,鏈?zhǔn)椒椒ǖ暮?jiǎn)單實(shí)現(xiàn)原理見我之前的文章《5 分鐘解讀 Python 中的鏈?zhǔn)秸{(diào)用》。
pandas-flavor 提供了能讓使用者簡(jiǎn)單且快速地編寫出**帶有「 Pandas 味兒」**的方法:
第一步,只需要在你編寫的函數(shù)、方法或類中添加對(duì)應(yīng)的裝飾器即可;
第二步,確保最后返回的是 DataFrame 或 Series 類的對(duì)象即可。
比如我們寫一個(gè)簡(jiǎn)單清理數(shù)據(jù)字段或變量名稱多余空格的方法:
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
最后結(jié)果如下:
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
本質(zhì)上來說,pandas-flavor 庫中提供的裝飾器就等價(jià)于重寫或新增了 DataFrame 類的方法,在使用過程中如果方法有報(bào)錯(cuò),那就需要還原加載 pandas 庫之后再重新寫入。
關(guān)于 pandas-flavor 裝飾器的用法,詳見項(xiàng)目的 Github(https://github.com/Zsailer/pandas_flavor)
結(jié)尾
通過 pyjanitor 庫我們可以更進(jìn)一步地豐富我們?cè)谔幚頂?shù)據(jù)時(shí)的工作流,并且借助鏈?zhǔn)椒椒ǖ奶匦詠砜s短數(shù)據(jù)分析或挖掘過程的耗時(shí)。
但也正如我在之前談?wù)撚嘘P(guān)鏈?zhǔn)秸{(diào)用的文章中所提到的,隨著鏈?zhǔn)秸{(diào)用的方法或過程的增多,出錯(cuò)的幾率也會(huì)大大增加。只有當(dāng)你確定以及肯定經(jīng)過每一步處理后返回的結(jié)果與你預(yù)期中的呈現(xiàn)形式相符時(shí),才能保證鏈?zhǔn)椒椒ㄦ湹姆€(wěn)健。
無論如何,pyjanitor 從一定程度上也擴(kuò)展了 Pandas 生態(tài)在處理數(shù)據(jù)上的多樣性和玩法。
作者:100gle,練習(xí)時(shí)長(zhǎng)不到兩年的非正經(jīng)文科生一枚,喜歡敲代碼、寫寫文章、搗鼓搗鼓各種新事物;現(xiàn)從事有關(guān)大數(shù)據(jù)分析與挖掘的相關(guān)工作。
贊 賞 作 者

Python中文社區(qū)作為一個(gè)去中心化的全球技術(shù)社區(qū),以成為全球20萬Python中文開發(fā)者的精神部落為愿景,目前覆蓋各大主流媒體和協(xié)作平臺(tái),與阿里、騰訊、百度、微軟、亞馬遜、開源中國(guó)、CSDN等業(yè)界知名公司和技術(shù)社區(qū)建立了廣泛的聯(lián)系,擁有來自十多個(gè)國(guó)家和地區(qū)數(shù)萬名登記會(huì)員,會(huì)員來自以工信部、清華大學(xué)、北京大學(xué)、北京郵電大學(xué)、中國(guó)人民銀行、中科院、中金、華為、BAT、谷歌、微軟等為代表的政府機(jī)關(guān)、科研單位、金融機(jī)構(gòu)以及海內(nèi)外知名公司,全平臺(tái)近20萬開發(fā)者關(guān)注。
長(zhǎng)按掃碼添加“Python小助手”
進(jìn)入 P Y 交 流 群
▼點(diǎn)擊成為社區(qū)會(huì)員 喜歡就點(diǎn)個(gè)在看吧
