Python之美——一隻數據狗的筆記[長期更新]
來自專欄新交通人的技術閑談5 人贊了文章
兩年前咬咬牙跳了Matlab的坑,入手了Python,從此一發不可收的成了PY的重度依賴者。本人研究工作皆涉及大量的數據處理工作,PY和R作為數據分析的兩駕馬車,得其一者得天下。另外,我接觸的許多軟體皆比較小眾,每次在涉及二次開發時,很多都是Matlab之流不支持的,而PY又往往是官方指定介面。因此,PY作為程序界的黏合劑,實在是方便至極。
如今機器學習和深度學習之熱,再次炒熱了PY。當然,涉及到統計模型,R的功力還是更深的。很多前沿或者有一定深度的統計模型,在R中都能快速實現,但在PY中則沒有現成的package。因此,現在不得不承認,PY和R,各有千秋,要都熟稔才行。
寫此文,是為記錄一些靈感,供廣大PY愛好者,也供自己,學習與查閱。
List形式的for in if else
爬到一組房價數據,但經緯度皆以121.43247的string形式存儲於DataFrame的一列中,且對於空缺值,以int形式的0或者float行駛的0.00填充。也就是說,該列存在多種數據格式,必須寫條件判斷才能循環。現需要將其進行修正提取,將121.43247提取為121.43247,而對於空值,統一以int形式的0填充。
於是,最低級的寫法出現了:
for jj in range(0, len(all_fangjia)): if all_fangjia.loc[jj, len] > 3: all_fangjia.loc[jj, new_lat] = all_fangjia.loc[jj, lat].split()[1] all_fangjia.loc[jj, new_lon] = all_fangjia.loc[jj, lon].split()[1]
該法思路清晰,但速度奇慢。對該列數據進行遍歷,先判斷該數據長度,如果大於3,說明是string形式的,然後再按照進行拆分(需要用來轉義),選取第二個值進行提取。
思路是對的,但速度實在太慢了。於是,就要請出循環的list風格化了:
all_fangjia[new_lon] = [var.split()[1] if len(var) > 3 else 0 for var in all_fangjia[lon]]all_fangjia[new_lat] = [var.split()[1] if len(var) > 3 else 0 for var in all_fangjia[lat]]
將代碼壓縮至了兩行,速度更是提升了幾十上百倍(具體提升量級沒算,但反正速度是飛快的了)。此法非常關鍵,掌握了對之後的數據處理效率大有提升。
佛系空格分隔符的處理
在拿到某些奇葩的原始數據文件時,其不同列間的分隔不是傳統的,,而是奇葩的不規整的空格符,也就是說,某兩列用了三個空格符來分隔,某兩列則用了四個,甚至在一列中,某兩行用了2個空格分隔,某兩行則用了3個。。
對於這種佛系空格分隔符,一種處理方法就是用正則(re)表達式,而另一種非常簡單的方法,則是:
import pandas as pdtem=pd.read_csv(583211-2017.out, delim_whitespace=True, engine=python)
即在熊貓包裡面的read_csv中,設置delim_whitespace=True即可。
字元串數據轉化為數字編號
比如有N個樣本,且存在一列專門對其類別進行標記,但標記用的全是字元串,如「大」、「中」、「小」。為了之後處理方便,需要將其變成0、1、2這種數字形式。這時就需要請出category類型來操作了。相關操作皆針對DataFrame格式實現。
obj_df["body_style"] = obj_df[「body_style"].astype(category)obj_df["body_style_cat"] = obj_df["body_style"].cat.codes
繪圖時批量改變所有字體大小
在利用matplotlib繪圖時,題目、坐標軸標籤、坐標軸名稱等等的字體大小都需要分別設置,非常麻煩,而下面的方法則可以批量一次性設置,修改起來也就隨之方便了。
應注意,如果有多個ax,則還需要再嵌套一層循環,先指向某一個ax.
import matplotlib.pyplot as pltfig,ax=plt.subplots()for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] + ax.get_xticklabels() + ax.get_yticklabels()): item.set_fontsize(20)
批量快速導入Oracle
做數據工作的,拿Python去接資料庫是非常常見的事情,而Oracle又是資料庫裡面的老大哥。在此不介紹如何安裝介麵包cx_Oracle,只介紹如何快速將大量數據一次性導入到Oracle中。
在沒Get到此技能之前,我都是一條條的往裡面插入數據的,數據量小還好,一旦大起來,速度就奇慢無比了。
於是,便有了下面的思路:先打包,再導入:
#導入連接包import cx_Oracle as oracledb = oracle.connect(scott/redhat@192.168.223.138:1521/oracle.test)#對待導入的數據進行處理DFV = DFV.fillna(None)DFV = DFV.values.tolist()rows = []for jj in range(len(DFV)): # 轉list row = (DFV[jj][0], DFV[jj][1], DFV[jj][2], DFV[jj][3], DFV[jj][4], DFV[jj][5], DFV[jj][6]) rows.append(row)# 寫入數據cr = db.cursor()cr.prepare( insert into OTJ_WATERLINK_WK2 (linkid,fromnode,LONGITUDE,LATITUDE,GRIDID,ROADNAME,SECT) values (:1, :2, :3, :4, :5, :6, :7))cr.executemany(None, rows)db.commit()cr.close()
試過的都知道,速度杠杠的。再也不用擔心大型數據文件要花上好幾天才能擼進Oracle了。
Groupby不支持的函數如何使用
數據處理裡面的groupby簡直就是小白第一課也得學會的技能了。但groupby方便雖方便,很多時候卻不支持一些函數。比如,我要對某一列進行groupby,並對groupby後的數據塊內的另一列求分位數。這時:
train_day=data.groupby([『TIMEID]).percentile()[GOSPEED]
卻顯示報錯,原因是groupby之後的數據塊不支持percentile()這個函數。
這時你想到的可能就是只能寫循環一步步進去了,不慌,groupby還給我們留了後路:
dg=data.groupby(TIMEID)for a,b in dg: z = np.percentile(b[GOSPEED],5)
不只是percentile(),其他什麼函數,都是可以這麼玩的。速度雖然比groupby慢了一些,但比直接寫循環進去要快不少。
指定區間,計算頻率
做頻率分布直方圖大家都會做,非常簡單,對離散型變數做頻數統計也很簡單,value_counts()函數就行,但如何對連續型變數按照指定的區間就行頻率統計呢?這裡就需要用到cut和value_counts()的結合了。
cut函數可以將一個區間進行切割,返回切割後的小塊,再將其作為參數傳遞給value_counts()函數,就可以得出結果了。
xse = range(1, 5000, 1)fanwei = list(range(0, 4500, 500))fenzu = pd.cut(xse, fanwei, right=False)print(fenzu.categories) # 分組區間,長度8pinshu = fenzu.value_counts() # series,區間-個數
讀入輸出文件的中文亂碼問題
這個問題大家幾乎都會遇到,解決方法也非常簡單,只要指定對了編碼,自然就不會亂碼了:
輸出CSV亂碼的話:
import codecsFGIS.to_csv(『FINALPOINT.csv,encoding="utf_8_sig",index=False)
導入CSV亂碼的話:
test=pd.read_csv(『busgps_0309_0310_71.txt,encoding=gbk) #gbk不行就改成『gb18030』
不論讀入導出啥文件,記住encoding不要亂,編碼就不會亂。
數據結構化輸出及讀取
某個變數需要先保存好,下次再來直接讀取,而不是重新計算?MATLAB裡面可以直接保存WORKPLACE裡面的變數,PY怎麼做呢?用pickle
import pickle#導出output = open(FWRON.pkl, wb)pickle.dump(FWRON, output, -1)output.close()#讀取pkl_file = open(FWRON.pkl, rb)FWRON = pickle.load(pkl_file)pkl_file.close()
多版本PY的管理
由於不同的包可能在不同版本下才能生存,所以一台電腦有好幾個PY很正常,而解決他們的共生問題也是十分的頭疼。比如我的電腦里就有三個版本的PY(我也不知道怎麼這麼多)。。其中,conda管理的兩個:2.7和3.4;還有在系統下的一個3.6。
對於用conda來管理的各種版本,則可以使用conda來進行切換,相對要簡單很多。切換完畢後,就可以在該版本下進行包的安裝管理。強烈建議用conda而非pip來安裝package。
conda info --envssource activate python34conda install -c conda-forge osmnxsudo pip install [package_name] --upgrade
而我之前沒用conda之前,一直都在用系統的3.6。所以,很多時候我還是要對3.6系統下的環境做配置。下面記錄了更新pip以及利用pip指定版本安裝包的過程。注意全程加上python3來指代PY3的版本(我默認是用的2.7),以及,記得加上--user,否則會一直報錯[Errno 13] Permission denied。
curl https://bootstrap.pypa.io/get-pip.py | python3 - --userpython3 -m pip install --user osmnx
PY版本是非常頭疼的事情。最好的辦法是完全基於conda來配置自己的環境。不要和我一樣,多個PY版本分散在各個地址,配置起來非常麻煩。
一行代碼解決兩個字元串組的匹配
近期在做特徵的時候,需要對異常站點進行清洗。其間遇到一個問題,記錄如下:
有一個list A,裡面存儲著系列表徵站點錯誤的關鍵詞,如「關閉」、「不開放」、「非運營」。
另外有一個list B,裡面存儲著所有站點的名稱,如「虹橋站」、「上海南站」、「五角場站」。
在list B中,有部分站點是出錯的,這些站點會在名稱中進行標記,如虹橋站出錯了,該站的名字會改成「(關閉)虹橋站」,當然,也可能改成「(不開放)虹橋站」。
現在需要把list B中所有的出錯站點找出來。
問題複述一遍就是:以list A中的每一個元素為關鍵詞,對list B中每一個元素進行匹配,如果list B中某個元素包含list A中的任意一個元素,則將list B中的該元素標記為FALSE。
當然寫循環,用 A in B,是肯定可以做的。但是,有沒有更簡潔的寫法呢?
嘗試了一下,是有的:
Wrong_list=[關閉,不開放,非運營]Test_list=[虹橋站,(不開放)虹橋站]Bool_result=[any(list(wrongs in var for wrongs in Wrong_list)) for var in Test_list]
最後返回:Bool_result=[FALSE, TRUE]
需要注意:
1)兩個for的順序:先寫for wrongs in Wrong_list,再寫for var in Test_list,最終得出的Bool_result才是針對Test_list的。
2)list在此的作用:將generator object 轉化為bool格式。
3)any在此的作用:表示「只要有一即可」。
4)括弧在此的作用:為any提供計算優先順序。
applymap與匿名函數
常常會遇到需要對矩陣中的所有數值執行某個函數的情況,但又懶得寫def,這時候就可以祭出applymap大殺器了:
DF.applymap(lambda x: -(x * math.log(x)) if x > 0 else np.nan)
這句話的功能是,對DF這個矩陣裡面的每一個大於0的值,執行-(x * math.log(x))的運算,如果該值小於0,則置為nan。
要注意applymap和apply的區別。後者是對行或列進行處理:
DF.apply(lambda x: sum(x != 0), axis=1)
如上面的代碼,則是返回每一行(axis=1)中不等於0的個數。
去除DF中含有重複名字的列
有時候MERGE多了,難免會出現一個DF裡面有好一些列完全一致——內容一致,列名也一致。這在某些時候,列名一致是容易出錯的,最好需要及時清理他們。清理方法是:
DF=DF.ix[:,~DF.columns.duplicated()]
一句話就可以去重啦,非常的利索有沒有。
選取groupby後某列最小值對應的行
做數據處理的時候常常會遇到這樣的問題:對於一個DF,我們按照A、B兩列進行groupby後,選取每個group內C列最小值所對應的行並返回。
DF1=DF.loc[DF.groupby([A,B])[C].idxmin()]
原理其實很簡單,用到了一個idxmin(),可以返回最小值對應的行索引。
根據列類型選取列
很多時候如果列很多,而且我們需要選取特定類型的列進行變化。比如,在做線性回歸時,把所有BOOL類型的列改為0,1類型:
Exposure_DATA_NEW.loc[:, Exposure_DATA_NEW.dtypes == np.bool] = Exposure_DATA_NEW.loc[:,Exposure_DATA_NEW.dtypes == np.bool].astype(int)
這裡用到了DF.dtypes == np.bool,來對列進行圈取。
對每一個group進行NA均值填充
很多時候我們在做缺失值填充時,會需要先groupby,然後再對每一個group,計算該group的均值,並填充至該group的缺失值處:
Exposure_DATA["surfacewid"] = Exposure_DATA.groupby("rank_artclass")["surfacewid"].transform( lambda x: x.fillna(x.mean()))
CX_ORACLE的中文亂碼問題
在利用CX_ORACLE讀入數據時,不做處理,中文就會直接跳問號。需要在程序前加上:
import osimport sysreload(sys)sys.setdefaultencoding("gbk")os.environ[NLS_LANG] = SIMPLIFIED CHINESE_CHINA.UTF8
去除列名,豎向疊加
npCombined_Net = np.concatenate([metro_route.as_matrix(), tem_null.as_matrix()], axis=0)metro_route = pd.DataFrame(npCombined_Net, columns=metro_route.columns)
Brew的安裝與運用
如何安裝:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安裝完還找不到怎麼辦:
export PATH=/usr/local/bin:$PATH
利用brew來安裝一個mysql:
brew install mysql
矩陣的去方向groupby
在做出一個網路邊矩陣時,常常會出現這樣的情況,我們需要的是無向的,但邊矩陣卻是有向的。即,假如矩陣一共三類,『FROM』,『TO』,『VALUE』,我們認為FROM 3 TO 1和FROM 1 TO 3是一類的,因此,我們需要把FROM 3 TO 1和FROM 1 TO 3的VALUE 求均值。怎麼做:
edge_centrality_nodir = pd.DataFrame(np.sort(edge_centrality[[from, to]], axis=1), edge_centrality.index, edge_centrality[[from, to]].columns)edge_centrality_nodir[bc] = edge_centrality[bc]edge_centrality_nodir_f = edge_centrality_nodir.groupby([from, to]).mean()[bc].reset_index()
其中edge_centrality為有向矩陣,edge_centrality_nodir為矩陣無向化,edge_centrality_nodir_f為最終groupby後的結果。矩陣無向化的過程,實際是對每一行進行重新排序的過程。注意把from 和 to兩列單獨拎出來。
繪圖label只顯示兩位小數
from matplotlib.ticker import FormatStrFormatterfig, ax = plt.subplots()ax.yaxis.set_major_formatter(FormatStrFormatter(%.2f))
range()不能產生float??
不要慌,用arange:
import numpy as npnp.arange(1,10,0.1)
DF某一列中是否包含某個字元
比如要判斷DF的某一列中是否含有「A」這個字元:
DF[Names].str.contains(A)
那如果要把「A」這個字元替換成「-」怎麼辦呢:
DF[Names].str.replace(A,-)
真心的方便呀。比如你在處理時間欄位時,有些直接就成「XX年XX月XX日」這種格式了,這時你為了轉化為datetime,首先就是把「年」、「月」、「日」都替換成「-」。
多個DF的merge
譬如你有N個DF,這些DF具有相同的KEY列,你需要把他們按照這個KEY列一併MERGE起來。怎麼做?
首先把需要MERGE的放在dfs這個list裡面,然後用reduce來解決:
from functools import reducedfs = [ord_count, ord_real_mn, order_coup_mn, order_final_mn]df_final = reduce(lambda left, right: pd.merge(left, right, on=authid_s), dfs)
我們的宗旨是:代碼這東西,多寫一行都是在浪費生命。
list以及numpy的repeat
在構造全序列時,常常需要對一個list進行重複,重複又分為兩種:
- [1,2,3]--->[1,1,2,2,3,3]
- [1,2,3]--->[1,2,3,1,2,3]
需要注意,在python中這兩種寫法是截然不同的。假設我們需要構建三列,第一列為站點ID,第二列為每一天,第三列為每一個小時:
timerange = pd.DataFrame(pd.date_range(start=1/1/2017, end=12/31/2017, freq=D))timerange[0] = timerange[0].astype(str)full_time_range = pd.DataFrame({SHOPSEQ: np.repeat(list(shop_need_seq), 365 * 24), date: list(np.repeat(list(timerange[0]), 24)) * len(shop_need_seq), HOUR0: list(range(1, 25, 1)) * len(shop_need_seq) * 365})
DF中的mean和count是怎麼對待NAN的?
The internalcount()
function will ignoreNaN
values, and so willmean()
. The only point where we getNaN
, is when the only value isNaN
. Then, we take the mean value of an empty set, which turns out to beNaN
即:默認情況下,DF的 count()和mean()函數都是自動忽視NAN的,在計算均值時,除非你的所有數都是NAN,才會出現NAN的結果。
reshape(-1)?-1是什麼size?
這是非常能提現python之懶的一個點,懶得什麼境界呢?就是你只知道變形後的列數,懶得算變形後的行數,你就拿-1代替好了。。:
假如是這麼一個array:
z = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
直接-1之後,變成12行1列的矩陣:
z.reshape(-1)array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
我想把他變成2列,但我懶得算有幾行:在行數那裡寫-1,系統自動幫你補全為6行:
z.reshape(-1, 2)array([[ 1, 2], [ 3, 4], [ 5, 6], [ 7, 8], [ 9, 10], [11, 12]])
推薦閱讀:
※頁面運行代碼 Jupyter Notebook
※Python數據分析數據化運營:商品數據化運營概述與關鍵指標
※老婆最近不開心,發消息老撤回,用Python破解了撤回的消息
※在 Kubernetes 上運行一個 Python 應用程序
※Python教程02-數字類型和運算符