一場pandas與SQL的巔峰大戰(二)

上一篇文章一場pandas與SQL的巔峰大戰中,我們對比了pandas與SQL常見的一些操作,我們的例子雖然是以MySQL為基礎的,但換作其他的數據庫軟件,也一樣適用。工作中除了MySQL,也經常會使用Hive SQL,相比之下,后者有更為強大和豐富的函數。本文將延續上一篇文章的風格和思路,繼續對比Pandas與SQL,一方面是對上文的補充,另一方面也繼續深入學習一下兩種工具。方便起見,本文采用hive環境運行SQL,使用jupyter lab運行pandas。關于hive的安裝和配置,我在之前的文章MacOS 下hive的安裝與配置提到過,不過僅限于mac版本,供參考,如果你覺得比較困難,可以考慮使用postgreSQL,它比MySQL支持更多的函數(不過代碼可能需要進行一定的改動)。而jupyter lab和jupyter notebook功能相同,界面相似,完全可以用notebook代替,我在Jupyter notebook使用技巧大全一文的最后有提到過二者的差別,感興趣可以點擊藍字閱讀。希望本文可以幫助各位讀者在工作中進行pandas和Hive SQL的快速轉換。本文涉及的部分hive 函數我在之前也有總結過,可以參考常用Hive函數的學習和總結

在公眾號后臺回復“對比二”可以獲取本文的PDF版本以及全部的數據和代碼。對于文中圖片代碼不清晰的,可以放大查看。

數據概況

數據上,我們還是使用上一篇中虛擬的數據,只是在ts的格式上有些小改動,在使用之前同樣需要先用read_csv的方式讀取,具體可以參考上篇文章。本文不做這一步的演示。hive方面我們新建了一張表,并把同樣的數據加載進了表中,后續直接使用即可。

image
image

開始學習

一、字符串的截取

對于原始數據集中的一列,我們常常要截取其字串作為新的列來使用。例如我們想求出每一條訂單對應的日期。需要從訂單時間ts或者orderid中截取。在pandas中,我們可以將列轉換為字符串,截取其子串,添加為新的列。代碼如下圖左側所示,我們使用了.str將原字段視為字符串,從ts中截取了前10位,從orderid中截取了前8位。經驗表明有時在.str之前需要加上astype,能夠避免不必要的麻煩。兩種寫法供參考。

對于字符串截取的操作,Hive SQL中有substr函數,它在MySQL和Hive中的用法是一樣的substr(string A,int start,int len)表示從字符串A中截取起始位置為start,長度為len的子串,其中起始位置從1開始算。實現上面效果的代碼如下:

image

圖片中的代碼:

#pythonimport pandas as pdorder = pd.read_csv('order.csv', names=['id', 'ts', 'uid', 'orderid', 'amount'])order.head()order['dt'] = order['ts'].str[:10]order.head()order['dt2'] = order['orderid'].astype(str).str[:8]order.head()#Hive SQLselect *, substr(ts, 1, 10) as dt, substring(orderid, 1, 8) as dt2from t_order;

二、字符串匹配

這一節我們來研究提取包含特定字符的字段。沿用上一節的寫法,在pandas中我們可以使用字符串的contains,extract,replace方法,支持正則表達式。而在hive SQL中,既有簡易的Like關鍵字匹配特定的字符,也可以使用regexp_extract,regexp_replace這兩個函數更靈活地實現目標。接下來我們舉例說明。

  1. 假設要實現篩選訂單時間中包含“08-01”的訂單。pandas和SQL代碼如下所示,注意使用like時,%是通配符,表示匹配任意長度的字符。
image

圖片中的代碼:

#pythonorder_08_01 = order[order['ts'].astype(str).str.contains('08-01')]order_08_01#Hive SQLselect * from t_orderwhere ts like "%08-01%"; 

2.假設要實現提取ts中的日期信息(前10位),pandas里支持正則表達式的extract函數,而hive里除了前文提到的substr函數可以實現外,這里我們可以使用regexp_extract函數,通過正則表達式實現。

image

圖片中的代碼

#pythonorder['dt3'] = order['ts'].astype(str).str.extract('(\d{4}-\d{2}-\d{2}).*')#這個正則表達式表示"4位數字橫杠兩位數字橫杠兩位數字",后面是任意字符,#我們提取的目標要放在小括號里order.head()#Hive SQLselect *, regexp_extract(ts, '(\\d{4}-\\d{2}-\\d{2}).*', 1) as dt3from t_order;#我們的目標同樣是在小括號里,1表示取第一個匹配的結果

3.假設我們要去掉ts中的橫杠,即替換ts中的“-”為空,在pandas中可以使用字符串的replace方法,hive中可以使用regexp_replace函數。代碼如下:

image

圖片中代碼:

#pythonorder['dt4'] = order['ts'].astype(str).str.replace('-', '')order.head()#Hive SQLselect *, regexp_replace(ts, '-', '') as dt4from t_order;

三、帶條件的計數:count(distinct case when …end)

我們在上一篇文章中分別討論過分組聚合和case操作。實際中,經常會遇到二者嵌套的情況,例如,我們想統計:ts中含有‘2019-08-01’的不重復訂單有多少,ts中含有‘2019-08-02’的不重復訂單有多少,這在Hive SQL中比較容易,代碼和得到的結果為:

select count(distinct case when ts like '%2019-08-01%' then orderid end) as 0801_cnt,count(distinct case when ts like '%2019-08-02%' then orderid end) as 0802_cntfrom t_order;#運行結果:5    11

你當然可以直接對日期進行分組,同時計算所有日期的訂單數,此處我們僅僅是為了演示兩種操作的結合。

pandas中實現這個問題可能比較麻煩,也可能有很多不同的寫法。這里說一下我的思路和實現方式。

我定義了兩個函數,第一個函數給原數據增加一列,標記我們的條件,第二個函數再增加一列,當滿足條件時,給出對應的orderid,然后要對整個dataframe應用這兩個函數。對于我們不關心的行,這兩列的值都為nan。第三步再進行去重計數操作。代碼和結果如下:

#第一步:構造一個輔助列def func_1(x):    if '2019-08-01' in x['ts']:        return '2019-08-01'#這個地方可以返回其他標記    elif '2019-08-02' in x['ts']:        return '2019-08-02'    else:        return None#第二步:將符合條件的order作為新的一列def func_2(x):    if '2019-08-01' in x['ts']:        return str(x['orderid'])    elif '2019-08-02' in x['ts']:        return str(x['orderid'])    else:        return None#應用兩個函數,查看結果#注意這里必須加上axis=1,你可以嘗試下不加會怎樣order['cnt_condition'] = order.apply(func_1, axis=1)order['cnt'] = order.apply(func_2, axis=1)order[order['cnt'].notnull()]#進行分組計數order.groupby('cnt_condition').agg({'cnt': 'nunique'})
image

可以看到,同樣得到了5,11的結果。如果你有其他更好的實現方法,歡迎一起探討交流。

四、窗口函數 row_number

hive中的row_number函數通常用來分組計數,每組內的序號從1開始增加,且沒有重復值。比如我們對每個uid的訂單按照訂單時間倒序排列,獲取其排序的序號。實現的Hive SQL代碼如下,可以看到,每個uid都會有一個從1開始的計數,這個計數是按時間倒序排的。

select *, row_number() over (partition by uid order by ts desc) as rkfrom t_order;
image

pandas中我們需要借助groupby和rank函數來實現同樣的效果。改變rank中的method參數可以實現Hive中其他的排序,例如dense,rank等。

#由于我們的ts字段是字符串類型,先轉換為datetime類型order['ts2'] =  pd.to_datetime(order['ts'], format='%Y-%m-%d %H:%M:%S')#進行分組排序,按照uid分組,按照ts2降序,序號默認為小數,需要轉換為整數#并添加為新的一列rkorder['rk'] = order.groupby(['uid'])['ts2'].rank(ascending=False, method='first').astype(int)#為了便于查看rk的效果,對原來的數據按照uid和時間進行排序,結果和SQL一致order.sort_values(['uid','ts'], ascending=[True, False])
image

五、窗口函數 lag,lead

lag和lead函數也是Hive SQL中常用的窗口函數,他們的格式為:

lag(字段名,N) over(partition by 分組字段 order by 排序字段 排序方式) lead(字段名,N) over(partition by 分組字段 order by 排序字段 排序方式) 

lag函數表示,取分組排序之后比該條記錄序號小N的對應記錄的指定字段的值。lead剛好相反,是比當前記錄大N的對應記錄的指定字段值。我們來看例子。

image

例子中的lag表示分組排序后,前一條記錄的ts,lead表示后一條記錄的ts。不存在的用NULL填充。

對應的代碼為:

select *, lag(ts, 1) over (partition by uid order by ts desc) as lag,lead(ts, 1) over (partition by uid order by ts desc) as leadfrom t_order;

pandas中我們也有相應的shift函數來實現這樣的需求。shift的參數為負數時,表示lag,為正數時,表示lead。

image

代碼如下:

order['lag'] =  order.groupby(['uid'])['ts2'].shift(-1)order['lead'] =  order.groupby(['uid'])['ts2'].shift(1)#依然是為了看效果,對原來的數據按照uid和時間進行排序,結果和SQL一致order.sort_values(['uid','ts'], ascending=[True, False])

六、列轉行,collect_list

在我們的數據中,一個uid會對應多個訂單,目前這多個訂單id是分多行顯示的。現在我們要做的是讓多個訂單id顯示在同一行,用逗號分隔開。在pandas中,我們采用的做法是先把原來orderid列轉為字符串形式,并在每一個id末尾添加一個逗號作為分割符,然后采用字符串相加的方式,將每個uid對應的字符串類型的訂單id拼接到一起。代碼和效果如下所示。為了減少干擾,我們將order數據重新讀入,并設置了pandas的顯示方式。

image

可以看到,同一個uid對應的訂單id已經顯示在同一行了,訂單id之間以逗號分隔。

在Hive中實現同樣的效果要方便多了,我們可以使用collect_set/collect_list函數,,二者的區別在于前者在聚合時會進行去重,別忘了加上group by。

select uid, collect_set(orderid) as order_listfrom t_ordergroup by uid;
image

可以看出hive實現的效果中,將同一個uid的orderid作為一個“數組”顯示出來。雖然和pandas實現的效果不完全一樣,但表達的含義是一致的。我沒有找到pandas實現這樣數組形式比較好的方法,如果你知道,歡迎一起交流.另外,pandas在聚合時,如何去重,也是一個待解決的問題。

七 行轉列 later view explode

行轉列的操作在Hive SQL中有時會遇到,可以理解為將上一小節的結果還原為每個orderid顯示一行的形式。hive中有比較方便的explode函數,結合lateral view,可以很容易實現。代碼和效果如下:

-- 使用上一節的結果,定義為tmp表,后面可以直接用with tmp as (select uid, collect_set(orderid) as order_listfrom t_ordergroup by uid)select uid, o_listfrom tmp lateral view explode(order_list) t as o_list;
image

我們來看在pandas中的實現。目標是把上一節合并起來的用逗號分隔的數組拆分開。這里給出一個參考鏈接:

https://blog.csdn.net/sscc_learning/article/details/89473151

首先我們要把groupby的結果索引重置一下,然后再進行遍歷,和賦值,最后將每一個series拼接起來。我采用的是鏈接中的第一種方式。由于是遍歷,效率可能比較低下,讀者可以嘗試下鏈接里的另一種方式。我先給出我的代碼:

order_group = order_group.reset_index()order_grouporder_group1 = pd.concat([pd.Series(row['uid'], row['orderid'].split(',')) for _ , row in order_group.iterrows()]).reset_index()order_group1

這樣的結果中會有一個空行,這是因為用逗號分隔的時候,最后一個元素為空。后續可以使用我們之前學習的方法進行過濾或刪除。這里省略這一步驟。

image

八、數組元素解析

這一小節我們引入一個新的數據集,原因是我想分享的內容,目前的數據集不能夠體現,哈哈。下面是在Hive和pandas中查看數據樣例的方式。我們的目標是將原始以字符串形式存儲的數組元素解析出來。

image
image

先來看pandas中如何實現,這里我們需要用到literal_eval這個包,能夠自動識別以字符串形式存儲的數組。我定義了一個解析函數,將arr列應用該函數多次,解析出的結果作為新的列,代碼如下:

image

這里需要注意解析出的結果是object類型的,如果想讓它們參與數值計算,需要再轉換為int類型,可以在解析的時候增加轉換的代碼。

new_data['arr_1'] =  new_data.arr.apply(extract_num, args=(0,)).astype(int)

回到Hive SQL,實現起來比較容易。我們可以通過split函數將原來的字符串形式變為數組,然后依次取數組的元素即可,但是要注意使用substr函數處理好前后的中括號,代碼如下:

image

可以看到最終我們得到的結果是字符串的形式,如果想要得到數值,可以再進行一步截取。

image

可以看到,我們這里得到的依然是字符串類型,和pandas中的強制轉換類似,hive SQL中也有類型轉換的函數cast,使用它可以強制將字符串轉為整數,使用方法如下面代碼所示。

image

小結

本文涉及的操作概括如下表所示,雖然內容沒有上篇文章多,但相對難度還是比上篇高一些。

image

如果你認真讀了本文,會發現有一些情況下,Hive SQL比pandas更方便,為了達到同樣的效果,pandas可能要用一種全新的方式來實現。實際工作中,如果數據存在數據庫中,使用SQL語句來處理還是方便不少的,尤其是如果數據量大了,pandas可能會顯得有點吃力。本文的出發點僅僅是對比兩者的操作,方便從兩個角度理解常見的數據處理手段,也方便工作中的轉換查閱,不強調孰優孰劣。對于文中遺留的不是很完美的地方,如果您想到了好的方案,歡迎一起探討交流~文中用到的數據和代碼我已經打包整理好,在公眾號后臺回復“對比二”即可獲得,祝您練習愉快!

推薦閱讀:

一場pandas與SQL的巔峰大戰

Hive基礎學習

常用Hive函數的學習和總結

Jupyter notebook使用技巧大全

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容