上一篇文章一場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方面我們新建了一張表,并把同樣的數據加載進了表中,后續直接使用即可。
開始學習
一、字符串的截取
對于原始數據集中的一列,我們常常要截取其字串作為新的列來使用。例如我們想求出每一條訂單對應的日期。需要從訂單時間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開始算。實現上面效果的代碼如下:
圖片中的代碼:
#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這兩個函數更靈活地實現目標。接下來我們舉例說明。
- 假設要實現篩選訂單時間中包含“08-01”的訂單。pandas和SQL代碼如下所示,注意使用like時,%是通配符,表示匹配任意長度的字符。
圖片中的代碼:
#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函數,通過正則表達式實現。
圖片中的代碼
#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函數。代碼如下:
圖片中代碼:
#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'})
可以看到,同樣得到了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;
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])
五、窗口函數 lag,lead
lag和lead函數也是Hive SQL中常用的窗口函數,他們的格式為:
lag(字段名,N) over(partition by 分組字段 order by 排序字段 排序方式) lead(字段名,N) over(partition by 分組字段 order by 排序字段 排序方式)
lag函數表示,取分組排序之后比該條記錄序號小N的對應記錄的指定字段的值。lead剛好相反,是比當前記錄大N的對應記錄的指定字段值。我們來看例子。
例子中的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。
代碼如下:
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的顯示方式。
可以看到,同一個uid對應的訂單id已經顯示在同一行了,訂單id之間以逗號分隔。
在Hive中實現同樣的效果要方便多了,我們可以使用collect_set/collect_list函數,,二者的區別在于前者在聚合時會進行去重,別忘了加上group by。
select uid, collect_set(orderid) as order_listfrom t_ordergroup by uid;
可以看出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;
我們來看在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
這樣的結果中會有一個空行,這是因為用逗號分隔的時候,最后一個元素為空。后續可以使用我們之前學習的方法進行過濾或刪除。這里省略這一步驟。
八、數組元素解析
這一小節我們引入一個新的數據集,原因是我想分享的內容,目前的數據集不能夠體現,哈哈。下面是在Hive和pandas中查看數據樣例的方式。我們的目標是將原始以字符串形式存儲的數組元素解析出來。
先來看pandas中如何實現,這里我們需要用到literal_eval這個包,能夠自動識別以字符串形式存儲的數組。我定義了一個解析函數,將arr列應用該函數多次,解析出的結果作為新的列,代碼如下:
這里需要注意解析出的結果是object類型的,如果想讓它們參與數值計算,需要再轉換為int類型,可以在解析的時候增加轉換的代碼。
new_data['arr_1'] = new_data.arr.apply(extract_num, args=(0,)).astype(int)
回到Hive SQL,實現起來比較容易。我們可以通過split函數將原來的字符串形式變為數組,然后依次取數組的元素即可,但是要注意使用substr函數處理好前后的中括號,代碼如下:
可以看到最終我們得到的結果是字符串的形式,如果想要得到數值,可以再進行一步截取。
可以看到,我們這里得到的依然是字符串類型,和pandas中的強制轉換類似,hive SQL中也有類型轉換的函數cast,使用它可以強制將字符串轉為整數,使用方法如下面代碼所示。
小結
本文涉及的操作概括如下表所示,雖然內容沒有上篇文章多,但相對難度還是比上篇高一些。
如果你認真讀了本文,會發現有一些情況下,Hive SQL比pandas更方便,為了達到同樣的效果,pandas可能要用一種全新的方式來實現。實際工作中,如果數據存在數據庫中,使用SQL語句來處理還是方便不少的,尤其是如果數據量大了,pandas可能會顯得有點吃力。本文的出發點僅僅是對比兩者的操作,方便從兩個角度理解常見的數據處理手段,也方便工作中的轉換查閱,不強調孰優孰劣。對于文中遺留的不是很完美的地方,如果您想到了好的方案,歡迎一起探討交流~文中用到的數據和代碼我已經打包整理好,在公眾號后臺回復“對比二”即可獲得,祝您練習愉快!
推薦閱讀: