第3章
10條資料化營運不得不知道的資料預處理經驗
資料預處理是資料化營運過程中的重要環節,它直接決定了後期所有資料工作的品質和價值輸出。從資料預處理的主要内容看,包括資料清洗、轉換、歸約、聚合、抽樣等。本章将摒棄理論和方法說教,直接介紹預處理本身可能遇到的問題及應對方法。
3.1 資料清洗:缺失值、異常值和重複值的處理
在資料清洗過程中,主要處理的是缺失值、異常值和重複值。所謂清洗,是對資料集通過丢棄、填充、替換、去重等操作,達到去除異常、糾正錯誤、補足缺失的目的。
3.1.1 資料列缺失的4種處理方法
資料缺失分為兩種:一種是行記錄的缺失,這種情況又稱資料記錄丢失;另一種是資料列值的缺失,即由于各種原因導緻的資料記錄中某些列的值空缺。不同的資料存儲和環境中對于缺失值的表示結果也不同,例如,資料庫中是Null,Python傳回對象是None,Pandas或Numpy中是NaN。
在極少數情況下,部分缺失值也會使用空字元串來代替,但空字元串絕對不同于缺失值。從對象的實體來看,空字元串是有實體的,實體為字元串類型;而缺失值其實是沒有實體的,即沒有資料類型。
丢失的資料記錄通常無法找回,這裡重點讨論資料列類型缺失值的處理思路。通常有4種思路。
1.丢棄
這種方法簡單明了,直接删除帶有缺失值的行記錄(整行删除)或者列字段(整列删除),減少缺失資料記錄對總體資料的影響。但丢棄意味着會消減資料特征,以下任何一種場景都不宜采用該方法。
- 資料集總體中存在大量的資料記錄不完整情況且比例較大,例如超過10%,删除這些帶有缺失值的記錄意味着會損失過多有用資訊。
- 帶有缺失值的資料記錄大量存在着明顯的資料分布規律或特征,例如帶有缺失值的資料記錄的目标标簽(即分類中的Label變量)主要集中于某一類或幾類,如果删除這些資料記錄将使對應分類的資料樣本丢失大量特征資訊,導緻模型過拟合或分類不準确。
2.補全
相對丢棄而言,補全是更加常用的缺失值處理方式。通過一定的方法将缺失的資料補上,進而形成完整的資料記錄,對于後續的資料處理、分析和模組化至關重要。常用的補全方法如下。
- 統計法:對于數值型的資料,使用均值、權重均值、中位數等方法補足;對于分類型資料,使用類别衆數最多的值補足。
- 模型法:更多時候我們會基于已有的其他字段,将缺失字段作為目标變量進行預測,進而得到最為可能的補全值。如果帶有缺失值的列是數值變量,采用回歸模型補全;如果是分類變量,則采用分類模型補全。
- 專家補全:對于少量且具有重要意義的資料記錄,專家補足也是非常重要的一種途徑。
- 其他方法:例如随機法、特殊值法、多重填補等。
3.真值轉換法
在某些情況下,我們可能無法得知缺失值的分布規律,并且無法對于缺失值采用上述任何一種補全方法做處理;或者我們認為資料缺失也是一種規律,不應該輕易對缺失值随意處理,那麼還有一種缺失值處理思路—真值轉換。
該思路的根本觀點是,我們承認缺失值的存在,并且把資料缺失也作為資料分布規律的一部分,将變量的實際值和缺失值都作為輸入次元參與後續資料處理和模型計算中。但是變量的實際值可以作為變量值參與模型計算,而缺失值通常無法參與運算,是以需要對缺失值進行真值轉換。
以使用者性别字段為例,很多資料庫集都無法對會員的性别進行補足,但又舍不得将其丢棄掉,那麼我們将選擇将其中的值,包括男、女、未知從一個變量的多個值分布狀态轉換為多個變量的真值分布狀态。
- 轉換前:性别(值域:男、女、未知)。
- 轉換後:性别_男(值域1或0)、性别_女(值域1或0)、性别_未知(值域1或0)。
然後将這3列新的字段作為輸入次元替換原來的1個字段參與後續模型計算。有關真值轉換的具體方法和知識話題,會在3.2節中具體介紹。
4.不處理
在資料預處理階段,對于具有缺失值的資料記錄不做任何處理,也是一種思路。這種思路主要看後期的資料分析和模組化應用,很多模型對于缺失值有容忍度或靈活的處理方法,是以在預處理階段可以不做處理。常見的能夠自動處理缺失值的模型包括:KNN、決策樹和随機森林、神經網絡和樸素貝葉斯、DBSCAN(基于密度的帶有噪聲的空間聚類)等。這些模型對于缺失值的處理思路是:
- 忽略,缺失值不參與距離計算,例如KNN。
- 将缺失值作為分布的一種狀态,并參與到模組化過程,例如各種決策樹及其變體。
- 不基于距離做計算,是以基于值的距離做計算本身的影響就消除了,例如DBSCAN。
在資料模組化前的資料歸約階段,有一種歸約的思路是降維,降維中有一種直接選擇特征的方法。假如我們通過一定方法确定帶有缺失值(無論缺少字段的值缺失數量有多少)的字段對于模型的影響非常小,那麼我們根本就不需要對缺失值進行處理。是以,後期模組化時的字段或特征的重要性判斷也是決定是否處理字段缺失值的重要參考因素之一。
對于缺失值的處理思路是先通過一定方法找到缺失值,接着分析缺失值在整體樣本中的分布占比,以及缺失值是否具有顯著的無規律分布特征,然後考慮後續要使用的模型中是否能滿足缺失值的自動處理,最後決定采用哪種缺失值處理方法。在選擇處理方法時,注意投入的時間、精力和産出價值,畢竟,處理缺失值隻是整個資料工作的冰山一角而已。
在資料采集時,可在采集端針對各個字段設定一個預設值。以MySQL為例,在設計資料庫表時,可通過default指定每個字段的預設值,該值必須是常數。在這種情況下,假如原本資料采集時沒有采集到資料,字段的值應該為Null,雖然由于在建立庫表時設定了預設值會導緻“缺失值”看起來非常正常,但本質上還是缺失的。對于這類資料需要尤其注意。
3.1.2 不要輕易抛棄異常資料
異常資料是資料分布的常态,處于特定分布區域或範圍之外的資料通常會被定義為異常或“噪音”。産生資料“噪音”的原因很多,例如業務營運操作、資料采集問題、資料同步問題等。對異常資料進行處理前,需要先辨識出到底哪些是真正的資料異常。從資料異常的狀态看分為兩種:
- 一種是“僞異常”,這些異常是由于業務特定營運動作産生的,其實是正常反映業務狀态,而不是資料本身的異正常律。
- 一種是“真異常”,這些異常并不是由于特定的業務動作引起的,而是客觀地反映了資料本身分布異常的分布個案。
大多數資料挖掘或資料工作中,異常值都會在資料的預處理過程中被認為是噪音而剔除,以避免其對總體資料評估和分析挖掘的影響。但在以下幾種情況下,我們無須對異常值做抛棄處理。
1.異常值正常反映了業務營運結果
該場景是由業務部門的特定動作導緻的資料分布異常,如果抛棄異常值将導緻無法正确回報業務結果。
例如:公司的A商品正常情況下日銷量為1000台左右。由于昨日舉行優惠促銷活動導緻總銷量達到10000台,由于後端庫存備貨不足導緻今日銷量又下降到100台。在這種情況下,10000台和100台都正确地反映了業務營運的結果,而非資料異常案例。
2.異常檢測模型
異常檢測模型是針對整體樣本中的異常資料進行分析和挖掘,以便找到其中的異常個案和規律,這種資料應用圍繞異常值展開,是以異常值不能做抛棄處理。
異常檢測模型常用于客戶異常識别、信用卡欺詐、貸款審批識别、藥物變異識别、惡劣氣象預測、網絡入侵檢測、流量作弊檢測等。在這種情況下,異常資料本身是目标資料,如果被處理掉将損失關鍵資訊。
3.包容異常值的資料模組化
如果資料算法和模型對異常值不敏感,那麼即使不處理異常值也不會對模型本身造成負面影響。例如在決策樹中,異常值本身就可以作為一種分裂節點。

3.1.3 資料重複就需要去重嗎
資料集中的重複值包括以下兩種情況:
- 資料值完全相同的多條資料記錄。這是最常見的資料重複情況。
- 資料主體相同但比對到的唯一屬性值不同。這種情況多見于資料倉庫中的變化次元表,同一個事實表的主體會比對同一個屬性的多個值。
去重是重複值處理的主要方法,主要目的是保留能顯示特征的唯一資料記錄。但當遇到以下幾種情況時,請慎重(不建議)執行資料去重。
- 1.重複的記錄用于分析演變規律
以變化次元表為例。例如在商品類别的次元表中,每個商品對應的同1個類别的值應該是唯一的,例如蘋果iPhone7屬于個人電子消費品,這樣才能将所有商品配置設定到唯一類别屬性值中。但當所有商品類别的值重構或更新時(大多數情況下随着公司的發展都會這麼做),原有的商品可能被配置設定了類别中的不同值。如
此時,我們在資料中使用Full join做跨重構時間點的類别比對時,會發現蘋果iPhone7會同時比對到個人電子消費品和手機數位2條記錄。對于這種情況,需要根據具體業務需求處理。
- 如果跟業務溝通,兩條資料需要做整合,那麼需要确定一個整合字段用來涵蓋2條記錄。其實就是将2條資料再次映射到一個類别主體中。
- 如果跟業務溝通,需要同時儲存兩條資料,那麼此時不能做任何處理。後續的具體處理根據模組化需求而定。
2.重複的記錄用于樣本不均衡處理
在開展分類資料模組化工作時,樣本不均衡是影響分類模型效果的關鍵因素之一。解決分類方法的一種方法是對少數樣本類别做簡單過采樣,通過随機過采樣,采取簡單複制樣本的政策來增加少數類樣本。經過這種處理方式後,也會在資料記錄中産生相同記錄的多條資料。此時,我們不能對其中的重複值執行去重操作。
有關樣本不均衡的相關内容将在3.4節中介紹。
3.重複的記錄用于檢測業務規則問題
對于以分析應用為主的資料集而言,存在重複記錄不會直接影響實際營運,畢竟資料集主要是用來做分析的。但對于事務型的資料而言,重複資料可能意味着重大營運規則問題,尤其當這些重複值出現在與企業經營中與金錢相關的業務場景時,例如:重複的訂單、重複的充值、重複的預約項、重複的出庫申請等。
這些重複的資料記錄通常是由于資料采集、存儲、驗證和稽核機制的不完善等問題導緻的,會直接反映到前台生産和營運系統。以重複訂單為例,假如前台的送出訂單功能不做唯一性限制,那麼在一次訂單中重複點選送出訂單按鈕,就會觸發多次重複送出訂單的申請記錄,如果該操作審批通過後,會關聯帶動營運後端的商品分揀、出庫、送貨,如果使用者接收重複商品則會導緻重大損失;如果使用者退貨則會增加反向訂單,并影響物流、配送和倉儲相關的各個營運環節,導緻營運資源無端消耗、商品損耗增加、倉儲物流成本增加等問題。
是以,這些問題必須在前期資料采集和存儲時就通過一定機制解決和避免。如果确實産生了此類問題,那麼資料工作者或營運工作者可以基于這些重複值來發現規則漏洞,并配合相關部門,最大限度地降低由此而帶來的營運風險。
3.1.4 代碼實操:Python資料清洗
1.缺失值處理
在缺失值的處理上,主要配合使用sklearn.preprocessing中的Imputer類、Pandas和Numpy。其中由于Pandas對于資料探索、分析和探查的支援較為良好,是以圍繞Pandas的缺失值處理較為常用。
第1部分為導入庫,該代碼示例中用到Pandas、Numpy和sklearn。
import pandas as pd # 導入Pandas庫
import numpy as np # 導入Numpy庫
from sklearn.preprocessing import Imputer # 導入sklearn.preprocessing中的Imputer庫
第2部分生成缺失資料。
# 生成缺失資料
df = pd.DataFrame(np.random.randn(6, 4), columns=['col1', 'col2', 'col3', 'col4']) # 生成一份資料
df.iloc[1:2, 1] = np.nan # 增加缺失值
df.iloc[4, 3] = np.nan # 增加缺失值
print(df)
通過Pandas生成一個6行4列,列名分别為'col1'、'col2'、'col3'、'col4'的資料框。同時,資料框中增加兩個缺失值資料。除了示例中直接通過pd.DataFrame來直接建立資料框外,還可以使用資料框對象的df.from_records、df.from_dict、df.from_items來從元組記錄、字典和鍵值對對象建立資料框,或使用pandas.read_csv、pandas.read_table、pandas.read_clipboard等方法讀取檔案或剪貼闆建立資料框。該代碼段執行後傳回了定義含有缺失值的資料框,結果如下:
第3部分判斷缺失值。
# 檢視哪些值缺失
nan_all = df.isnull() # 獲得所有資料框中的N值
print(nan_all) # 列印輸出
# 檢視哪些列缺失
nan_col1 = df.isnull().any() # 獲得含有NA的列
nan_col2 = df.isnull().all() # 獲得全部為NA的列
print(nan_col1) # 列印輸出
print(nan_col2) # 列印輸出
通過df.null()方法找到所有資料框中的缺失值(預設缺失值是NaN格式),然後使用any()或all()方法來查找含有至少1個或全部缺失值的列,其中any()方法用來傳回指定軸中的任何元素為True,而all()方法用來傳回指定軸的所有元素都為True。該代碼段執行後傳回如下結果。
判斷元素是否是缺失值(第2行第2列和第5行第4列):
col1 col2 col3 col4
0 False False False False
1 False True False False
2 False False False False
3 False False False False
4 False False False True
5 False False False False
列出至少有一個元素含有缺失值的列(該示例中為col2和col4):
col1 False
col2 True
col3 False
col4 True
dtype: bool
列出全部元素含有缺失值的列(該示例中沒有):
col2 False
col4 False
第4部分丢棄缺失值。
df2 = df.dropna() # 直接丢棄含有NA的行記錄
print(df2) # 列印輸出
通過Pandas預設的dropna()方法丢棄缺失值,傳回無缺失值的資料記錄。該代碼段執行後傳回如下結果(第2行、第5行資料記錄被删除):
第5部分通過sklearn的資料預處理方法對缺失值進行處理。
nan_model = Imputer(missing_values='NaN', strategy='mean', axis=0) # 建立替換規則:将值為NaN的缺失值以均值做替換
nan_result = nan_model.fit_transform(df) # 應用模型規則
print(nan_result) # 列印輸出
首先通過Imputer方法建立一個預處理對象,其中missing_values為預設缺失值的字元串,預設為NaN;示例中選擇缺失值替換方法是均值(預設),還可以選擇使用中位數和衆數進行替換,即strategy值設定為median或most_frequent;後面的參數axis用來設定輸入的軸,預設值為0,即使用列做計算邏輯。然後使用預處理對象的fit_transform方法對df(資料框對象)進行處理,該方法是将fit和transform組合起來使用。代碼執行後傳回如下結果:
代碼中的第2行第2列和第5行第4列分别被各自列的均值替換。為了驗證,我們手動計算一下各自列的均值,通過使用df['col2'].mean()和df['col4'].mean()分别獲得這兩列的均值為-0.4494679289032068和-0.16611331259664791,與sklearn傳回的結果一緻。
第6部分使用Pandas做缺失值處理。
nan_result_pd1 = df.fillna(method='backfill') # 用後面的值替換缺失值
nan_result_pd2 = df.fillna(method='bfill', limit=1) # 用後面的值替代缺失值,限制每列隻能替代一個缺失值
nan_result_pd3 = df.fillna(method='pad') # 用前面的值替換缺失值
nan_result_pd4 = df.fillna(0) # 用0替換缺失值
nan_result_pd5 = df.fillna({'col2': 1.1, 'col4': 1.2}) # 用不同值替換不同列的缺失值
nan_result_pd6 = df.fillna(df.mean()['col2':'col4']) # 用各自列的平均數替換缺失值
# 列印輸出
print(nan_result_pd1) # 列印輸出
print(nan_result_pd2) # 列印輸出
print(nan_result_pd3) # 列印輸出
print(nan_result_pd4) # 列印輸出
print(nan_result_pd5) # 列印輸出
print(nan_result_pd6) # 列印輸出
Pandas對缺失值的處理方法是df.fillna(),該方法中最主要的兩個參數是value和method。前者通過固定(或手動指定)的值替換缺失值,後者使用Pandas提供的預設方法替換缺失值。以下是method支援的方法。
- pad和ffill:使用前面的值替換缺失值,示例中nan_result_pd3使用了pad方法。
- backfill和bfill:使用後面的值替換缺失值,示例中nan_result_pd1和nan_result_pd2使用了該方法。
- None:無。
在示例中,nan_result_pd4、nan_result_pd5、nan_result_pd6分别使用0、不同的值、平均數替換缺失值。需要注意的是,如果要使用不同具體值替換,需要使用scalar、dict、Series或DataFrame的格式定義。
上述代碼執行後傳回如下結果。
用後面的值(method='backfill')替換缺失值:
col1 col2 col3 col4
0 -0.112415 -0.768180 -0.084859 0.296691
1 -1.777315 1.892790 -0.166615 -0.628756
2 -0.629461 1.892790 -1.850006 0.157567
3 0.544860 -1.230804 0.836615 -0.945712
4 0.703394 -0.764552 -1.214379 0.289643
5 1.928313 -1.376593 -1.557721 0.289643
用後面的值(method='bfill', limit = 1)替換缺失值:
col1 col2 col3 col4
用前面的值替換缺失值(method='pad'):
col1 col2 col3 col4
1 -1.777315 -0.768180 -0.166615 -0.628756
4 0.703394 -0.764552 -1.214379 -0.945712
用0替換缺失值:
col1 col2 col3 col4
1 -1.777315 0.000000 -0.166615 -0.628756
4 0.703394 -0.764552 -1.214379 0.000000
手動指定兩個缺失值分布為1.1和1.2:
col1 col2 col3 col4
1 -1.777315 1.100000 -0.166615 -0.628756
4 0.703394 -0.764552 -1.214379 1.200000
用平均數代替,選擇各自列的均值替換缺失值:
col1 col2 col3 col4
1 -1.777315 -0.449468 -0.166615 -0.628756
4 0.703394 -0.764552 -1.214379 -0.166113
以上示例中,直接指定method的方法适用于大多數情況,較為簡單直接;但使用value的方法則更為靈活,原因是可以通過函數的形式将缺失值的處理規則寫好,然後直接指派即可。限于篇幅,不對所有方法做展開講解。
另外,如果是直接替換為特定值的應用,也可以考慮使用Pandas的replace功能。本示例的df(原始資料框)可直接使用df.replace(np.nan,0),這種用法更加簡單粗暴,但也能達到效果。當然,replace的出現是為了解決各種替換應用的,缺失值隻是其中的一種應用而已。
上述過程中,主要需要考慮的關鍵點是缺失值的替換政策,可指定多種方法替換缺失值,具體根據實際需求而定,但大多數情況下均值、衆數和中位數的方法較為常用。如果場景固定,也可以使用特定值(例如0)替換。
在使用不同的缺失值政策時,需要注意以下幾個問題:
1)缺失值的處理的前提是已經可以正确識别所有缺失值字段,關于識别的問題在使用Pandas讀取資料時可通過設定na_values的值指定。但是如果資料已經讀取完畢并且不希望再重新讀取,那可以使用Pandas的replace功能将指定的字元串(或清單)替換為NaN。更有效的是,如果資料中的缺失值太多而無法通過清單形式窮舉時,replace還支援正規表達式的寫法。
2)當列中的資料全部為空值時,任何替換方法都将失效,任何基于中位數、衆數和均值的政策都将失效。除了可以使用固定值替換外(這種情況下即使替換了該特征也沒有實際參與模型的價值),最合理的方式是先将全部為缺失值的列删除,然後再做其他處理。
3)當列中含有極大值或極小值的inf或-inf時,會使得mean()這種方法失效,因為這種情況下将無法計算出均值。應對思路是使用median中位數做兜底政策,隻要列中有資料,就一定會有中位數。
2.異常值處理
有關異常值的确定有很多規則和方法,這裡使用Z标準化得到的門檻值作為判斷标準:當标準化後的得分超過門檻值則為異常。完整代碼如下。
示例代碼分為3個部分。
第1部分導入本例需要的Pandas庫。
import pandas as pd # 導入Pandas庫
第2部分生成異常資料。
df = pd.DataFrame({'col1': [1, 120, 3, 5, 2, 12, 13],
'col2': [12, 17, 31, 53, 22, 32, 43]})
print(df) # 列印輸出
直接通過DataFrame建立一個7行2列的資料框,列印輸出結果如下:
col1 col2
0 1 12
1 120 17
2 3 31
3 5 53
4 2 22
5 12 32
6 13 43
第3部分為通過Z-Score方法判斷異常值。
df_zscore = df.copy() # 複制一個用來存儲Z-score得分的資料框
cols = df.columns # 獲得資料框的列名
for col in cols: # 循環讀取每列
df_col = df[col] # 得到每列的值
z_score = (df_col - df_col.mean()) / df_col.std() # 計算每列的Z-score得分
df_zscore[col] = z_score.abs() > 2.2 # 判斷Z-score得分是否大于2.2,如果是則為True,否則為False
print(df_zscore) # 列印輸出
本過程中,先通過df.copy()複制一個原始資料框的副本,用來存儲Z-Score标準化後的得分,再通過df.columns獲得原始資料框的列名,接着通過循環判斷每一列中的異常值。在判斷邏輯中,對每一列的資料進行使用自定義的方法做Z-Score值标準化得分計算,然後與門檻值2.2做比較,如果大于門檻值則為異常。标準化的計算還有更多自動化的方法和場景,有關資料标準化的話題,将在3.9節中具體介紹。本段代碼傳回結果如下:
col1 col2
0 False False
1 True False
2 False False
3 False False
4 False False
5 False False
6 False False
在本示例方法中,門檻值的設定是确定異常與否的關鍵,通常當門檻值大于2.2時,就是相對異常的表現值。
第4部分删除帶有異常值所在的記錄行。
df_drop_outlier = df[df_zscore['col1'] == False]
print(df_drop_outlier)
本段代碼裡我們直接使用了Pandas的選擇功能,即隻保留在df_zscore中異常列(col1)為False的列。完成後在輸出的結果中可以看到,删除了index值為1的資料行。
上述過程中,主要需要考慮的關鍵點是:如何判斷異常值。
對于有固定業務規則的可直接套用業務規則,而對于沒有固定業務規則的,可以采用常見的數學模型進行判斷:基于機率分布的模型(例如正态分布的标準差範圍)、基于聚類的方法(例如KMeans)、基于密度的方法(例如LOF)、基于分類的方法(例如KNN)、基于統計的方法(例如分位數法)等。異常值的定義帶有較強的主觀判斷色彩,具體需要根據實際情況選擇。
3.重複值處理
有關重複值的處理代碼分為4個部分。
第1部分為導入用到的Pandas庫。
第2部分生成重複資料。
data1, data2, data3, data4 = ['a', 3], ['b', 2], ['a', 3], ['c', 2]
df = pd.DataFrame([data1, data2, data3, data4], columns=['col1', 'col2'])
在代碼中,我們在一列中直接給4個對象指派,也可以拆分為4行分别指派。該資料是一個4行2列資料框,資料結果如下:
col1 col2
0 a 3
1 b 2
2 a 3
3 c 2
第3部分判斷重複資料。
isDuplicated = df.duplicated() # 判斷重複資料記錄
print(isDuplicated) # 列印輸出
判斷資料記錄是否為重複值,傳回每條資料記錄是否重複結果,取值為True或False。判斷方法為df.duplicated(),該方法中兩個主要的參數是subset和keep。
- subset:要判斷重複值的列,可以指定特定列或多個列。預設使用全部列。
- keep:當重複時不标記為True的規則,可設定為第1個(first)、最後一個(last)和全部标記為True(False)。預設使用first,即第1個重複值不标記為True。
結果如下:
0 False
1 False
2 True
3 False
第4部分删除重複值。
print(df.drop_duplicates()) # 删除資料記錄中所有列值相同的記錄
print(df.drop_duplicates(['col1'])) # 删除資料記錄中col1值相同的記錄
print(df.drop_duplicates(['col2'])) # 删除資料記錄中col2值相同的記錄
print(df.drop_duplicates(['col1', 'col2'])) # 删除資料記錄中指定列(col1/col2)值相同的記錄
該操作的核心方法是df.drop_duplicates(),該方法的作用是基于指定的規則判斷為重複值之後,删除重複值,其參數跟df.duplicated()完全相同。在該部分方法示例中,依次使用預設規則(全部列相同的資料記錄)、col1列相同、col2列相同以及指定col1和col2完全相同4種規則進行去重。傳回結果如下。
删除資料記錄中所有列值相同的記錄,index為2的記錄行被删除:
删除資料記錄中col1值相同的記錄,index為2的記錄行被删除:
删除資料記錄中col2值相同的記錄,index為2和3的記錄行被删除:
删除資料記錄中指定列(col1和col2)值相同的記錄,index為2的記錄行被删除:
由于資料是通過随機數産生,是以讀者操作的結果可能與上述示例的資料結果不同。
除了可以使用Pandas來做重複值判斷和處理外,也可以使用Numpy中的unique()方法,該方法傳回其參數數組中所有不同的值,并且按照從小到大的順序排列。Python自帶的内置函數set方法也能傳回唯一進制素的集合。
上述過程中,主要需要考慮的關鍵點是:如何對重複值進行處理。重複值的判斷相對簡單,而判斷之後如何處理往往不是一個技術特征明顯的工作,而是側重于業務和模組化需求的工作。
代碼實操小結:本節示例中,主要用了幾個知識點:
- 通過pd.DataFrame建立資料框。
- 通過df.iloc[]來選擇特定的列或對象。
- 使用Pandas的isnull()判斷值是否為空。
- 使用all()和any()判斷每列是否包含至少1個為True或全部為True的情況。
- 使用Pandas的dropna()直接删除缺失值。
- 使用sklearn.preprocessing中的Imputer方法對缺失值進行填充和替換,支援3種填充方法。
- 使用Pandas的fillna填充缺失值,支援更多自定義的值和常用預定義方法。
- 通過copy()獲得一個對象副本,常用于原始對象和複制對象同時進行操作的場景。
- 通過for循環周遊可疊代的清單值。
- 自定義代碼實作了Z-Score計算公式。
- 通過Pandas的duplicated()判斷重複資料記錄。
- 通過Pandas的drop_duplicates()删除資料記錄,可指定特定列或全部。
3.2 将分類資料和順序資料轉換為标志變量
分類資料和順序資料是常見的資料類型,這些值主要集中在圍繞資料實體的屬性和描述的相關字段和變量中。
3.2.1 分類資料和順序資料是什麼
在資料模組化過程中,很多算法無法直接處理非數值型的變量。例如,KMeans算法基于距離的相似度計算,而字元串則無法直接計算距離。另外,即使算法本身支援,很多算法實作包也無法直接基于字元串做矩陣運算,例如Numpy以及基于Numpy的sklearn。雖然這些庫允許直接使用和存儲字元串型變量,但卻無法發揮矩陣計算的優勢。這些類型的資料變量可以分為兩類。
1)分類資料:分類資料指某些資料屬性隻能歸于某一類别的非數值型資料,例如性别中的男、女就是分類資料。分類資料中的值沒有明顯的高、低、大、小等包含等級、順序、排序、好壞等邏輯的劃分,隻是用來區分兩個或多個具有相同或相當價值的屬性。例如:性别中的男和女,顔色中的紅、黃和藍,它們都是相同衡量次元上的不同屬性分類而已。
2)順序資料:順序資料隻能歸于某一有序類别的非數值型資料,例如使用者的價值度分為高、中、低,學曆分為博士、碩士、學士,這些都屬于順序資料。在順序資料中,有明顯的排序規律和邏輯層次的劃分。例如:高價值的使用者就比低價值的使用者價值高(業務定義該分類時已經賦予了這樣的價值含義)。
3.2.2 運用标志方法處理分類和順序變量
分類資料和順序資料要參與模型計算,通常都會轉化為數值型資料。當然,某些算法是允許這些資料直接參與計算的,例如分類算法中的決策樹、關聯規則等。将非數值型資料轉換為數值型資料的最佳方法是:将所有分類或順序變量的值域從一列多值的形态轉換為多列隻包含真值的形态,其中的真值可用True、False或0、1的方式來表示。這種标志轉換的方法有時候也稱為真值轉換。以使用者性别變量舉例,原有的使用者資料如表3-2所示。
經過轉換後的資料如表3-3所示。
為什麼不能直接用數字來表示不同的分類和順序資料,而一定要做标志轉換?這是因為在用數字直接表示分類和順序變量的過程中,無法準确還原不同類别資訊之間的資訊差異和互相關聯性。
- 針對分類資料:性别變量的屬性值是男和女,無論用什麼值來表示都無法表達出兩個值的價值相等且帶有區分的含義。如果用1和2區分,那麼1和2本身已經帶有距離為1的差異,但實際上二者是不具有這種差異性的,其他任意數字都是如此;如果用相同的數字來表示,則無法達到區分的目的。
- 針對順序資料:學曆變量的屬性值是博士、碩士和學士,可以用3-2-1來表示順序和排列關系,那麼如何表示3個值之間的差異是3-2-1而不是30-20-10或者1000-100-2呢?是以,任何一個有序數字的排序也都無法準确表達出順序資料的差異性。
3.2.3 代碼實操:Python标志轉換
在本示例中,将模拟有兩列資料分别出現分類資料和順序資料的情況,并通過自定義代碼及sklearn代碼分别進行标志轉換。
第1部分導入庫。
本示例使用Pandas庫和sklearn,sklearn中的OneHotEncoder用來将數值型類别變量轉換為0-1的标志型變量,LabelEncoder用來将字元串型變量轉換為數值型變量。
import pandas as pd # 導入Pandas庫
from sklearn.preprocessing import OneHotEncoder, LabelEncoder # 導入庫
第2部分生成原始資料。
df = pd.DataFrame({'id': [3566841, 6541227, 3512441],
'sex': ['male', 'Female', 'Female'],
'level': ['high', 'low', 'middle'],
'score': ['1', '2', '3']})
print(df) # 列印輸出原始資料框
資料為3行3列的資料框,分别包含id、sex和level列,其中的id為模拟的使用者ID,sex為使用者性别(英文),level為使用者等級(分别用high、middle和low代表3個等級)。該段代碼輸出原始資料框如下:
id sex level score
0 3566841 male high 1
1 6541227 Female low 2
2 3512441 Female middle 3
第3部分使用sklearn.preprocessing中的OneHotEncoder方法進行标志轉換。
1)拆分ID和資料列。
id_data = df[['id']] # 獲得ID列
raw_convert_data = df.iloc[:, 1:] # 指定要轉換的列
print(raw_convert_data)
這裡直接使用資料框的指定列名來拆分ID列,使用iloc方法拆分出字元串和數值型的分類變量。
原始字元串型資料列的輸出如下:
level sex
0 high male
1 low Female
2 middle Female
2)對資料列中的字元串列資料做轉換。
model_enc = OneHotEncoder() # 建立标志轉換模型對象(也稱為啞編碼對象)
df_new2 = model_enc.fit_transform(raw_convert_data).toarray() # 标志轉換
在該過程中,先建立一個LabelEncoder對象model_LabelEncoder,然後使用model_Label-Encoder做fit_transform轉換,轉換後的值直接替換上一步建立的副本transform_data_copy,然後使用toarray方法輸出為矩陣。
3)合并資料。
df_all = pd.concat((id_data, pd.DataFrame(df_new2)), axis=1) # 重新組合為資料框
print(df_all) # 列印輸出轉換後的資料框
轉換完成後,使用Pandas的concat方法,将ID列與轉換後的列拼接為完整主體。Df_new2是一個numpy數組,如果不轉換為DataFrame則無法直接與其他兩個資料框合并。程式執行得到的結果如下:
id 0 1 2 3 4 5 6 7
0 3566841 0.0 1.0 1.0 0.0 0.0 1.0 0.0 0.0
1 6541227 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0
2 3512441 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0
第4部分使用Pandas的get_dummies做标志轉換。
df_new3 = pd.get_dummies(raw_convert_data)
df_all2 = pd.concat((id_data, pd.DataFrame(df_new3)), axis=1) # 重新組合為資料框
print(df_all2) # 列印輸出轉換後的資料框
該過程中,用到了Pandas的get_dummies方法。該方法的主要參數如下。
- data:要轉換的對象,可以是類數組、Series或DataFrame。
- prefix:轉換後列名的字首,可以是字元串或None,或由字元串組成的清單、字典等。
- prefix_sep:字首分隔符,字元串,預設是_(下劃線)。
- dummy_na:增加一清單示NA值,布爾型,預設為False。如果是False就忽略NA值。
- columns:要轉換的列名,類清單,預設為None,表示所有類型為object和category類型的列都将被轉換。
- sparse:是否為稀疏矩陣,布爾型,預設為False。
該部分代碼完成後的輸出結果如下:
id score sex_Female sex_male level_high level_low level_middle
0 3566841 1 0 1 1 0 0
1 6541227 2 1 0 0 1 0
2 3512441 3 1 0 0 0 1
上述過程中,主要需要考慮的關鍵點是:
- 如何判斷要轉換的資料是分類或順序變量。
- 分類型變量或順序意義的變量不一定都是字元串類型。
代碼實操小結:本小節示例中,主要用了以下幾個知識點。
- 通過pd.DataFrame建構新的資料框。
- 通過Pandas中的df[[col_name]]和iloc[]進行資料切片或字段選擇。
- 通過Pandas中的drop()方法删除特定列,當然也可以用于删除行。
- 通過Pandas的dtype獲得對象的dtype類型,df.dtypes也能實作所有對象的類型。
- 通過unique()方法獲得唯一值。
- 通過字元串組合(示例中直接使用的+)建立一個新的字元串。
- 直接使用DataFrame和Series對象而無須周遊每個值進行矩陣比較和數值計算。
- 通過Pandas的df_new[col_name_new]方法直接新增列值。
- 使用OneHotEncoder将數值型分類向量轉換為标志變量。
- 使用Pandas的concat方法合并多個資料框。
- 使用Pandas的get_dummies方法做标志轉換。
3.3 大資料時代的資料降維
資料降維就是降低資料的次元數量,資料降維是維數歸約的一個重要課題。
3.3.1 需要資料降維的情況
資料降維可以降低模型的計算量并減少模型運作時間,降低噪音變量資訊對于模型結果的影響,便于通過可視化方式展示歸約後的次元資訊,并減少資料存儲空間。是以,大多數情況下,當我們面臨高維資料時,都需要對資料做降維處理。是否進行降維主要考慮以下方面:
1)次元數量。降維的基本前提是高維,假如模型隻有幾個次元,那就不一定需要降維,具體取決于次元本身的重要性、共線性以及其他排除關系,而不是出于高維的考慮。
2)模組化輸出是否必須保留原始次元。某些場景下,我們需要完整保留參與模組化的原始次元并在最終模組化輸出時能夠得以分析、解釋和應用,這種情況下不能進行轉換方式降維,隻能選擇特征篩選的方式降維。
3)對模型的計算效率與模組化時效性有要求。當面臨高維資料模組化時,資料模型的消耗将呈幾何倍數增長,這種增長帶來的結果便是運算效率慢、耗時長。如果對模組化時間和時效性有要求,那麼降維幾乎是必要步驟。
4)是否要保留完整資料特征。資料降維的基本出發點是在盡量(或最大化)保留原始資料特征的前提下,降低參與模組化的次元數。在降維過程中,無論未被表示出來的特征是噪音還是正常分布,這部分資訊都無法參與模組化。如果某些場景下需要所有資料集的完整特征,那麼通常不選擇降維。
資料降維隻是處理高維資料的思路和方法之一,除此之外,國内外學者也在研究其他高維資料模組化的方法。以高維資料聚類為例,除了可以通過降維來應對之外,其他思路還包括基于超圖的聚類,基于子空間的聚類、聯合聚類等,這些都是非降維的方法。
3.3.2 基于特征選擇的降維
基于特征選擇的降維指的是根據一定規則和經驗,直接選取原有次元的一部分參與後續的計算和模組化過程,用選擇的次元代替所有次元,整個過程不産生新的次元。
基于特征選擇的降維方法有4種思路。
- 經驗法:根據業務專家或資料專家的以往經驗、實際資料情況、業務了解程度等進行綜合考慮。業務經驗依靠的是業務背景,從衆多元度特征中選擇對結果影響較大的特征;而資料專家則依靠的是資料工作經驗,基于資料的基本特征及對後期資料處理和模組化的影響來選擇或排除次元,例如去掉缺失值較多的特征。
- 測算法:通過不斷測試多種次元選擇參與計算,通過結果來反複驗證和調整,并最終找到最佳特征方案。
- 基于統計分析的方法:通過相關性分析不同次元間的線性相關性,在相關性高的次元中進行人工去除或篩選;或者通過計算不同次元間的互資訊量,找到具有較高互資訊量的特征集,然後去除或留下其中一個特征。
- 機器學習算法:通過機器學習算法得到不同特征的特征值或權重,然後再根據權重來選擇較大的特征。如圖3-1所示是通過CART決策樹模型得到不同變量的重要性,然後根據實際權重值進行選擇。
這種資料降維方法的好處是,在保留了原有次元特征的基礎上進行降維,既能滿足後續資料處理和模組化需求,又能保留次元原本的業務含義,以便于業務了解和應用。對于業務分析型的應用而言,模型的可了解性、可解釋性和可應用性在很多時候的優先級要高于模型本身的準确率、效率等技術類名額,要知道,如果沒有業務的了解和支援,再好的資料、模型和算法都無法落地。
例如,通過決策樹得到的特征規則,可以作為選擇使用者樣本的基礎條件,而這些特征規則便是基于輸入的次元産生。假如我們在決策樹之前将原有次元用表達式(例如PCA的主成分)方法進行轉換,那麼即使得到了決策樹規則,也無法直接提供給業務應用。
3.3.3 基于特征轉換的降維
基于特征轉換的降維是按照一定的數學變換方法,把給定的一組相關變量(特征)通過數學模型将高維空間的資料點映射到低次元空間中,然後利用映射後變量的特征來表示原有變量的總體特征。這種方式是一種産生新次元的過程,轉換後的次元并非原有次元的本體,而是其綜合多個次元轉換或映射後的表達式。
通過資料次元變換進行降維是非常重要的降維方法,這種降維方法分為線性降維和非線性降維兩種,其中常用的代表算法包括獨立成分分析(ICA)、主成分分析(PCA)、因子分析(Factor Analysis,FA)、線性判别分析(LDA,也叫Fisher線性判别FLD)、局部線性嵌入(LLE)、核主成分分析(Kernel PCA)等。
(1)PCA(主成分分析)
主成分分析的基本方法是按照一定的數學變換方法,把給定的一組相關變量(次元)通過線性變換轉成另一組不相關的變量,這些新的變量按照方差依次遞減的順序排列。在數學變換中保持變量的總方差不變,使第1變量具有最大的方差,稱為第1主成分,第2變量的方差次大,并且和第1變量不相關,稱為第2主成分。依次類推,I個變量就有I個主成分。
例如,假設原始資料集中有10個次元分别是tenure、cardmon、lncardmon、cardten、lncardten、wiremon、lnwiremon、wireten、lnwireten、hourstv,現在用主成分分析進行降維,降維後選擇具有顯著性的前3個主成分示例。
方程式用于 主成分-1
0.006831 * tenure +
0.007453 * cardmon +
0.1861 * lncardmon +
0.0001897 * cardten +
0.1338 * lncardten +
+ -4.767
方程式用于 主成分-2
-0.4433 * lnwiremon +
-0.0001222 * wireten +
-0.1354 * lnwireten +
0.008099 * hourstv +
+ -0.272
方程式用于 主成分-3
-0.01809 * tenure +
0.0124 * cardmon +
0.00002565 * wireten +
-0.1644 * lnwireten +
0.03984 * hourstv +
+ -4.076
上述轉換結果就是主成分分析後提取的3個能基本代表原始10個次元的新“次元”。通過上述結果發現,主成分(也就是新的“次元”)是一個多元一次方程,其含義無法直接展現和了解。但對于不關注每個次元業務含義的系統級應用和內建來講是沒有關系的,因為後續算法不需要知道每個變量的具體業務含義。
為了更形象地解釋映射關系,現在使用一個序列圖來表示整個映射過程。如圖3-2所示為原始資料集在經過順時針旋轉(映射變換)後的可視化特征變化過程。從圖①到圖⑧的序列中可以看出,原始資料的分布呈現明顯的曲線分布特征,而旋轉後的資料樣本則呈現直線分布特征。
PCA主要适用的應用場景如下:
- 非監督式類型的資料集。它是一種非監督式的降維方法,是以适用于不帶有标簽的資料集;而對于帶有标簽的資料集則可以采用LDA。
- 根據方差自主要制特征數量。最大的主成分的數量≤特征的數量,這意味着,PCA也可以輸出數量完全相同的特征,具體取決于選擇特征中解釋的方差比例。
- 更少的正則化處理。選擇較多的主成分将導緻較少的平滑,因為我們将能夠保留更多的資料特征,進而減少正則化。
- 資料量較大的資料集。資料量大包括資料記錄多和資料次元多兩種情況,PCA對大型資料集的處理效率較高。
- 資料分布是位于相同平面上(非曲面),資料中存線上性結構。
(2)FA(因子分析)
因子分析(Factor Analysis)是指研究從變量群中提取共性因子的統計技術,這裡的共性因子指的是不同變量之間内在的隐藏因子。例如,一個學生的英語、資料、國文成績都很好,那麼潛在的共性因子可能是智力水準高。是以,因子分析的過程其實是尋找共性因子和個性因子并得到最優解釋的過程。
FA與PCA對比,二者的相同點如下:
- PCA和FA都是資料降維的重要方法,都對原始資料進行标準化處理,都消除了原始名額的相關性對綜合評價所造成的資訊重複的影響。
- 二者構造綜合評價時所涉及的權數具有客觀性,在原始資訊損失不大的前提下,減少了後期資料挖掘和分析的工作量。
- 二者由于側重點都是進行資料降維,是以很少單獨使用,大多數情況下都會有一些模型組合使用。
之是以大多數情況下,很難感性地區分因子分析和主成分分析,原因是二者的降維結果都是對原有次元進行一定的處理,在處理的結果上都偏離了原有基于次元的認識。但隻要清楚二者的邏輯一個是基于變量的線性組合,一個是基于因子的組合,便能很好地進行區分。
FA與PCA對比,二者的主要差別如下:
- 原理不同。主成分分析的基本原理是利用降維(線性變換)的思想,在損失很少資訊的前提下把多個名額轉化為幾個不相關的主成分,每個主成分都是原始變量的線性組合;而因子分析基本原理是從原始變量相關矩陣内部的依賴關系出發,把因子表達成能表示成少數公共因子和僅對某一個變量有作用的特殊因子的線性組合。因子分析是主成分的推廣,相對于主成分分析,更傾向于描述原始變量之間的相關關系。
- 假設條件不同。主成分分析不需要有假設,而因子分析需要假設各個共同因子之間不相關,特殊因子(specificfactor)之間也不相關,共同因子和特殊因子之間也不相關。
- 求解方法不同。主成分分析的求解方法從協方差陣出發,而因子分析的求解方法包括主成分法、主軸因子法、極大似然法、最小二乘法、a因子提取法等。
- 降維後的“次元”數量不同,即因子數量和主成分的數量。主成分分析的數量最多等于次元數;而因子分析中的因子個數需要分析者指定(SPSS和SAS根據一定的條件自動設定,隻要是特征值大于1的因子可進入分析),指定的因子數量不同而結果也不同。
綜合來看,因子分析在實作中可以使用旋轉技術,是以可以得到更好的因子解釋,這一點比主成分占優勢。是以如果後續資料挖掘或處理過程需要解釋或者通過原始變量的意義去應用,那麼選擇因子分析更合适。另外,因子分析不需要舍棄原有變量,而是将原有變量間的共性因子作為下一步應用的前提,其實就是由表及裡去發現内在規律。但是,主成分分析由于不需要假設條件,并且可以最大限度地保持原有變量的大多數特征,是以适用範圍更廣泛,尤其是宏觀的未知資料的穩定度更高。
(3)LDA(線性判别分析)
判别分析(Discriminant Analysis)是一種分類方法,它通過一個已知類别的“訓練樣本”來建立判别準則,并通過預測變量來為未知類别的資料進行分類。線性判别式分析(Linear Discriminant Analysis,簡稱為LDA)是其中一種,也是模式識别的經典算法,在1996年由Belhumeur引入模式識别和人工智能領域。
基本思想是将高維的模式樣本投影到最佳鑒别矢量空間,以達到抽取分類資訊和壓縮特征空間維數的效果。投影後保證模式樣本在新的子空間有最大的類間距離和最小的類内距離,即模式在該空間中有最佳的可分離性。
PCA與LDA相比有以下不同:
- 出發思想不同。PCA主要是從特征的協方差角度,去找到比較好的投影方式,即選擇樣本點投影具有最大方差的方向;而LDA則更多地考慮了分類标簽資訊,尋求投影後不同類别之間資料點距離更大化以及同一類别資料點距離最小化,即選擇分類性能最好的方向。
- 學習模式不同。PCA屬于無監督式學習,是以大多場景下隻作為資料處理過程的一部分,需要與其他算法結合使用,例如與聚類、判别分析、回歸分析等組合使用;LDA是一種監督式學習方法,本身除了可以降維外,還可以進行預測應用,是以既可以組合其他模型一起使用,也可以獨立使用。
- 降維後可用次元數量不同。LDA降維後最多可生成C-1維子空間(分類标簽數-1),是以LDA與原始次元數量無關,隻有資料标簽分類數量有關;而PCA最多有n次元可用,即最大可以選擇全部可用次元。
從直接可視化的角度,以二維資料降維為例,PCA和LDA的差別如圖3-3所示。
圖3-3左側是PCA的降維思想,它所做的隻是将整組資料整體映射到最友善表示這組資料的坐标軸上,映射時沒有利用任何資料内部的分類資訊。是以,雖然PCA後的資料在表示上更加友善(降低了維數并能最大限度地保持原有資訊),但在分類上也許會變得更加困難;圖3-3右側是LDA的降維思想,可以看到,LDA充分利用了資料的分類資訊,将兩組資料映射到了另外一個坐标軸上,使得資料更易區分了(在低維上就可以區分,減少了運算量)。
線性判别分析LDA算法由于其簡單有效性在多個領域都得到了廣泛應用,是目前機器學習、資料挖掘領域經典且熱門的一個算法。但是算法本身仍然存在一些局限性。
- 當樣本數量遠小于樣本的特征維數時,樣本與樣本之間的距離變大使得距離度量失效,使LDA算法中的類内、類間離散度矩陣奇異,不能得到最優的投影方向,在人臉識别領域中表現得尤為突出。
- LDA不适合對非高斯分布的樣本進行降維。
- LDA在樣本分類資訊依賴方差而不是均值時,效果不好。
- LDA可能過度拟合資料。
LDA是一個經典的機器學習算法,它是判别分析中的線性分類器,在很多應用情況下會面臨資料稀疏的問題,尤其是在面部識别的場景。
資料的次元很可能大于資料的樣本量,甚至可能呈幾倍的差異。此時,LDA的預測準确率會表現較差,當次元數/樣本量達到4倍時,準确率會隻有50%左右,解決方法之一是對LDA算法進行收縮,Python的sklearn中的LDA算法支援這一收縮規則。預設情況下,solver的值被設定為“svd”,這在大資料量下的表現很好,但不支援收縮規則;當面臨資料稀疏時,我們需要使用“lsqr”或“eigen”。另外,與之配合的shrinkage參數需要設定成“auto”,以便于算法自動調整收縮值。當然也可以自己憑借經驗将值設定在0~1之間(越大收縮越厲害:0時不收縮,1時意味着對角線方差矩陣将被用作協方差矩陣值的估計),效果如圖3-4所示。
(4)ICA(獨立成分分析)
傳統的降維方法,包括PCA、LDA等都是以觀測資料點呈高斯分布模型為基本假設前提的,在已經先驗經驗知道觀測資料集為非高斯分布模型的前提下,PCA和LDA的降維效果并不好。而ICA将适用于非高斯分析資料集,它是主成分分析(PCA)和因子分析(Factor Analysis)的一種有效擴充。
獨立成分分析(Independent Component Analysis,簡稱ICA)是一種利用統計原理進行計算的方法,它是一個線性變換,這個變換把資料或信号分離成統計獨立的非高斯的信号源的線性組合。
獨立成分分析的最重要的假設就是信号源統計獨立,并且這個假設在大多數盲信号分離(blind signal separation)的情況中符合實際情況;即使當該假設不滿足時,仍然可以用獨立成分分析來把觀察信号統計獨立化,進而進一步分析資料的特性。
ICA應用前提很簡單:資料信号源是獨立的且資料非高斯分布(或者信号源中最多隻有一個成分是高斯分布)。另外觀測信号源的數目不能少于源信号數目(為了友善一般要求二者相等即可)。如圖3-5所示,原始觀測信号源有3種獨立信号源混合(正弦、方形和鋸齒波形),通過ICA可以較好地分離出3種信号源。
獨立成分分析法最初是用來解決“雞尾酒會”的問題,ICA基于信号高階統計特性的分析方法,經ICA分解出的各信号成分(或者叫分量)之間是互相獨立的。正是因為這一特點,ICA在信号處理領域受到了廣泛的關注。除了經典的盲源分離外,它的應用領域還包括以下這些:
- 圖像識别,去除噪音資訊。
- 語言識别,分離音源并去除噪音(如去除噪音,隻保留輸入語音)。
- 通信、生物醫學信号處理,從這些信号中單獨區分某些信号(如區分胎兒和孕婦的心電信号)。
- 故障診斷,去除非自然資訊。
- 特征提取和降維。
- 自然資訊處理,如地震聲音分離。
3.3.4 基于特征組合的降維
基于特征的組合降維,實際上是将輸入特征與目标預測變量做拟合的過程,它将輸入特征經過運算,并得出能對目标變量做出很好解釋(預測性)的複合特征,這些特征不是原有的單一特征,而是經過組合和變換後的新特征。從這一點來講,原理類似于特征轉換,更準确地來講是類似于特征轉換中的LDA(有監督式的機器學習)。
經過特征組合後形成的新特征,具有以下優點:
- 在一定程度上解決了單一特征的離散和稀疏的問題,新組合特征對目标變量的解釋能力增加。
- 降低原有特征中噪音資訊的幹擾,使得模型魯棒性更強。
- 降低了模型的複雜性并提高模型效率,資料模組化可基于能表達原有資訊的組合特征展開。
- 如果将原有特征與新組合特征共同加入到訓練集中,能有效兼顧全局特征(原有單一特征表達的資訊)和個性化特征(新組合特征所表達的資訊),在很多場景下能有效提高準确率。
特征的組合方法有以下多種形式:
- 基于單一特征離散化後的組合。這種方式下先将連續型特征離散化,然後基于離散化後的特征組合成新的特征。常見的RFM模型就是其中一種,這種方式先将R、F、M分别離散化,然後做權重或直接組合,生成新的RFM等分。有關該方法的更多資訊,會在5.7節中介紹。
- 基于單一特征的運算後的組合。這種方式下,對單一列基于不同條件下獲得的資料記錄做求和、均值、最大值、最小值、中位數、分位數、标準差、偏度、峰度等計算,進而獲得新的特征。
- 基于多個特征的運算後的組合。這種方式下,将對多個單一特征直接做複合計算,而計算一般都是基于數值型特征的,常見方式包括加、減、乘、除、取餘、對數、正弦、餘弦等操作,進而形成新的特征。
- 基于模型的特征最優組合。這種方式下,特征間的組合将不再是簡單的數學運算,而是基于輸入特征與目标變量,在特定的優化函數的前提下做模型疊代計算,以達到滿足模型最優的解。常見的方式包括:基于多項式的特征組合、基于GBDT的特征組合、基于基因工程的特征組合(這3種方法會在3.3.5節的代碼實操中具體介紹)。
但是,特征組合的方法,很多時候其實并不能減少特征的數量,反而可能會增加特征。是以從嚴格意義上,特征組合不屬于降維的過程,而是特征工程中與降維并行的子產品。有關特征組合後的數量控制,會在下面代碼實操中具體介紹。
在實際工作中,我們可以選擇人工複雜特征+簡單模型的思路,也可以選擇簡單特征+複雜模型的思路,在操作得當的前提下二者都會有較好的效果表現。
本節介紹的特征選擇、轉換群組合其實都是在做人工選擇複雜特征的過程,實施的還是第1種思路。現在,在深度學習和神經網絡大行其道的時代,很多公司也在探索不做複雜特征工程,直接将簡單處理的資料放到神經網絡或其他複雜模型中,模型會自動從中提取特征并進行計算。在這方面最典型的應用領域是圖像識别。
3.3.5 代碼實操:Python資料降維
本示例中,将分别使用sklearn的DecisionTreeClassifier來判斷變量重要性并選擇變量,通過PCA進行次元轉換。資料源檔案data1.txt和data5.txt位于“附件-chapter3”中。
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn import feature_selection
from sklearn.svm import SVC
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.preprocessing import PolynomialFeatures as plf
from sklearn.ensemble import GradientBoostingClassifier as GBDT
from gplearn.genetic import SymbolicTransformer
from sklearn import datasets
本節用到的庫較多,除了我們常用的Numpy外,還包括sklearn以及gplearn。其中gplearn是我們本書中新用到的庫,該庫可通過!pip3 install gplearn直接安裝。各個庫的主要作用如下。
- Numpy:基本的資料讀取和預處理。
- DecisionTreeClassifier:決策樹分類器,用于結合SelectFromModel提取特征。
- SVC:支援向量機分類器,用于結合RFE提取特征。
- PolynomialFeatures:從多項式模型中提取特征。
- GradientBoostingClassifier:使用預設的GBDT的方法提取組合特征。
- SymbolicTransformer:從遺傳基因中的符号方法中提取組合特征。
第2部分導入資料檔案。
data = np.loadtxt('data1.txt') # 讀取文本資料檔案
x, y = data[:, :-1], data[:, -1] # 獲得輸入的x和目标變量y
print(x[:3]) # 列印輸出x的前3條記錄
該部分的資料檔案為具有分類特征的資料集。通過Numpy的loadtxt方法讀取資料檔案得到矩陣,然後對矩陣進行切割,得到要輸入變量集x和目标變量y。輸出x的前3條記錄如下:
[[ 1.88622997 1.31785876 -0.16480621 0.56536882 -1.11934542 -0.53218995
-0.6843102 1.24149827 1.00579225 0.45485041]
[ 0.45016257 0.67080853 -1.16571355 1.16653938 3.27605586 -0.87270624
-0.31067627 -0.94946505 -0.33194209 -2.94399437]
[ 0.48158666 0.33524676 0.72210929 -2.01794519 -0.4255258 -0.98050463
1.57086924 1.46919579 -1.68387822 1.44933243]]
該記錄可用于與下面特征選擇的記錄做對比。
第3部分基于sklearn的feature_selection做特征選擇。
該部分用到了SelectPercentile、VarianceThreshold、RFE、SelectFromModel四種方法。
使用SelectPercentile選擇特征:
selector_1 = feature_selection.SelectPercentile(percentile=30)
sel_features1 = selector_1.fit_transform(x, y) # 訓練并轉換資料
print(sel_features1.shape) # 列印形狀
print(sel_features1[:3]) # 列印前3條記錄
該部分先建構了一個選擇器模型對象selector_1,設定選擇總體30%的特征。然後基于selector_1做訓練和轉換,最後輸出轉換後的形狀和資料。結果如下:
(1000, 3)
[[-1.11934542 -0.6843102 0.45485041]
[ 3.27605586 -0.31067627 -2.94399437]
[-0.4255258 1.57086924 1.44933243]]
原資料中的x是一個(1000, 10)的資料集,經過處理後的特征為(1000, 3)。對比第2部分輸出的原始記錄,原始列索引為4/6/9的3個特征被保留下來。
使用VarianceThreshold選擇特征:
selector_2 = feature_selection.VarianceThreshold(1)
sel_features2 = selector_2.fit_transform(x) # 訓練并轉換資料
print(sel_features2.shape) # 列印形狀
print(sel_features2[:3]) # 列印前3條記錄
該過程的實作與上面的步驟類似,僅在初始化VarianceThreshold時設定了門檻值為1,即方差高于1的特征才能保留下來。上述代碼輸出結果如下:
(1000, 7)
[[ 1.31785876 0.56536882 -1.11934542 -0.53218995 1.24149827 1.00579225
0.45485041]
[ 0.67080853 1.16653938 3.27605586 -0.87270624 -0.94946505 -0.33194209
-2.94399437]
[ 0.33524676 -2.01794519 -0.4255258 -0.98050463 1.46919579 -1.68387822
1.44933243]]
原始特征中有7個特征符合條件,保留了列索引值為1/3/4/5/7/8/9的特征。
使用RFE選擇特征:
model_svc = SVC(kernel="linear")
selector_3 = feature_selection.RFE(model_svc, 3)
sel_features3 = selector_3.fit_transform(x, y) # 訓練并轉換資料
print(sel_features3.shape) # 列印形狀
print(sel_features3[:3]) # 列印前3條記錄
在上述過程中,相對于前兩種方法,增加了一個指定模型的過程。由于我們的資料帶有分類标志,是以我們設定SVC(支援向量機分類器)為基礎模型,并設定線性核心。在selector_3的設定中,我們指定保留3個得分最高的特征。最終輸出結果如下:
上述結果得出最終的特征集保留了3個特征,原始列索引為4/6/9的3個特征被保留下來,該結果與使用SelectPercentile得到的結果一緻,但SelectPercentile的預設函數為f_classif。
使用SelectFromModel選擇特征:
model_tree = DecisionTreeClassifier(random_state=0) # 建立分類決策樹模型對象
selector_4 = feature_selection.SelectFromModel(model_tree)
sel_features4 = selector_4.fit_transform(x, y) # 訓練并轉換資料
print(sel_features4.shape) # 列印形狀
print(sel_features4[:3]) # 列印前3條記錄
在該過程中,我們指定了基礎模型器為決策樹,但這裡我們不設定特征重要性的過濾門檻值,使用預設值None(原因是在不确定特征重要性的分布前提下,很難得到較好的分割門檻值)。得到結果如下:
[[ 0.56536882 -1.11934542 -0.6843102 ]
[ 1.16653938 3.27605586 -0.31067627]
[-2.01794519 -0.4255258 1.57086924]]
上述結果顯示了預設的參數下我們得到了3個顯著性特征,原始列索引為3/4/6的特征被保留下來。
第4部分使用sklearn的LDA進行次元轉換。
model_lda = LDA() # 建立LDA模型對象
model_lda.fit(x, y) # 将資料集輸入模型并訓練
convert_features = model_lda.transform(x) # 轉換資料
print(convert_features.shape) # 列印形狀
print(model_lda.explained_variance_ratio_) # 獲得各成分解釋方差占比
print(convert_features[:3]) # 列印前3條記錄
有資料集中帶有Label(分類标志),是以這裡我們選擇使用LDA方法做特征轉換。上述的代碼過程比較簡單,輸出結果如下:
(1000, 1)
[ 1.]
[[-1.08138044]
[ 2.89531581]
[-0.11256936]]
上述結果顯示,轉換後的特征隻有1個特征,後面輸出了方差比例及新特征的前3條資料。LDA轉換後的特征已經不是原有特征,而是經過變換後新的特征。
LDA方法轉換後的特征數量小于等于目标Label唯一值的個數。假如原始資料中的目标變量y的唯一值數量有n個,那麼可指定的特征個數最多為n-1。我們通過set(y)可以看到,y的唯一值域隻有2個值,是以LDA的成分最多隻能有1個;如果y中有10個唯一值,那麼最多可能指定為9。
LDA模型轉換後的成分的解釋方法比例為100%。如果有多個成分,那麼這裡會展示每個成分解釋方差的比例。如果沒有設定n_components,所有的成分都會被展示出來,并且其總和為1。
第5部分使用sklearn的GBDT方法組合特征。
model_gbdt = GBDT()
model_gbdt.fit(x, y)
conbine_features = model_gbdt.apply(x)[:, :, 0]
print(conbine_features.shape) # 列印形狀
print(conbine_features[0]) # 列印第1條記錄
在上述過程中,我們建立GBDT模型對象model_gbdt并做訓練之後,使用了apply做特征提取,該方法傳回的是葉子的索引,預設情況下,其形狀是[n_samples, n_estimators, n_classes]。如果是二分類的情況下,n_classes為1。一般情況下,在sklearn中的算法和模型用到的輸入資料都是二維空間矩陣(即shape = [n_samples, n_features]),是以這裡隻取n_samples和n_estimators。
上述代碼輸出的結果如下:
(1000, 100)
[ 4. 3. 4. 3. 5. 4. 5. 4. 4. 7. 7. 7. 7. 7. 7.
-
-
-
-
-
-
-
-
-
-
-
-
-
- 7.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 12.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 9.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 14.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 14.]
-
-
-
-
-
-
-
-
其中的(1000, 100)是GBDT提取後的索引節點的形狀,我們看到新的組合特征是100個;而通過下面的資料輸出我們看到,第1條資料包含的100個特征。對于該組合特征,本質上已經根據最佳分裂節點做離散化處理了,是以後面可以接其他模型做處理。例如:接OneHotEncode+LR做分類模組化。
在我們的模型中,都是采用預設的GBDT參數,參數的預設值如下:
GradientBoostingClassifier(criterion='friedman_mse', init=None,
learning_rate=0.1, loss='deviance', max_depth=3,
max_features=None, max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=100,
presort='auto', random_state=None, subsample=1.0, verbose=0,
warm_start=False)
在目前資料集及預設參數下,我們獲得了100個特征。讀者可自行設定n_estimators的值,會發現,最終的特征數量等于設定的n_estimators(小樹)的個數。
第6部分使用sklearn的PolynomialFeatures方法組合特征。
model_plf = plf(2)
plf_features = model_plf.fit_transform(x, y)
print(plf_features.shape) # 列印形狀
print(plf_features[0]) # 列印第1條資料
在代碼中,指定多項式的項數為2(degree的值為2),建立多項式對象model_plf後,我們直接使用fit_transform方法,然後輸出轉換後的特征形狀和第1條資料。結果如下:
(1000, 66)
[ 1. 1.88622997 1.31785876 -0.16480621 0.56536882 -1.11934542
-0.53218995 -0.6843102 1.24149827 1.00579225 0.45485041 3.55786351
2.4857847 -0.31086242 1.06641562 -2.11134288 -1.00383263 -1.2907664
2.34175125 1.89715548 0.85795248 1.73675172 -0.21719131 0.74507626
-1.47513917 -0.70135119 -0.90182419 1.63611938 1.32549213 0.5994286
0.02716109 -0.0931763 0.18447508 0.08770821 0.11277857 -0.20460663
-0.16576081 -0.07496217 0.31964191 -0.632843 -0.30088361 -0.38688765
0.70190442 0.56864358 0.25715824 1.25293417 0.59570438 0.76597948
-1.3896654 -1.12582894 -0.50913473 0.28322614 0.36418301 -0.6607129
-0.53527252 -0.24206682 0.46828045 -0.84956993 -0.68827389 -0.31125878
1.54131796 1.24868934 0.564696 1.01161804 0.45748502 0.2068889 ]
上述輸出了組合特征的個數是66個以及第1行組合特征結果。我們可以通過model_plf.get_feature_names()方法可以獲得每個特征的名稱,結果如下(為了節省版面,這裡我将多行合并為一行,且隻列出其中一部分名稱,是以版式跟實際不一緻):
['1', 'x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x0^2', 'x0 x1', 'x0 x2', 'x0 x3', 'x0 x4', 'x0 x5', 'x0 x6', 'x0 x7', 'x0 x8', 'x0 x9', 'x1^2', 'x1 x2',...'x8 x9', 'x9^2']
由于我們指定的是二項式,通過特征名稱可以看到其中有如下規律:
- 第1列特征的值全為1(讀者可通過plf_features[:,0]檢視)。
- 第2列到第10列為原始特征的值(讀者可通過x[0]對比檢視這10列值)。
- 後面的列都是原始10個特征兩兩(包含自身)之間的乘積。
第7部分使用gplearn的genetic方法組合特征。
在本部分示例中,我們将使用一個新的方法來做特征組合應用。Gplearn的庫主要有以下2個:
- SymbolicRegressor(符号回歸)是一種機器學習技術,旨在識别最能描述關系的基礎數學表達式。它首先建立一個簡單随機公式的數量來表示已知的自變量與其因變量目标之間的關系,以預測新資料。它可以利用遺傳算法得到的公式,直接預測目标變量的值,是以屬于回歸應用的一種方法。
- SymbolicTransformer(符号轉換器)是一種監督式的特征處理技術,它首先建立一組簡單的随機公式來表示關系,然後通過從群體中選擇最适合的個體進行遺傳操作,最終找出最适合彼此相關性最小的個體。本例就用了該方法,它是在特征工程過程中,非常有效的做特征轉換群組合的方法。
本示例完整代碼如下:
raw_data = datasets.load_boston() # 加載資料集
代碼中,我們分别進行了如下操作:
第1行代碼我們使用sklearn的demo資料集,該資料集為波士頓房價資料。
x, y = raw_data.data, raw_data.target # 分割形成x和y
第2行代碼根據列索引将原始資料分割為x和y,用于下面的特征處理。需要注意的是,這裡的y是一個數值型而非分類型字段。
print(x.shape) # 檢視x的形狀
print(x[0]) # 檢視x的第1條資料
第3、第4行直接列印原始資料集x的形狀和第1條資料,結果如下:
(506, 13)
[ 6.32000000e-03 1.80000000e+01 2.31000000e+00 0.00000000e+00
5.38000000e-01 6.57500000e+00 6.52000000e+01 4.09000000e+00
1.00000000e+00 2.96000000e+02 1.53000000e+01 3.96900000e+02
4.98000000e+00]
資料集x中有506條資料,13個特征。
第5行代碼使用SymbolicTransformer方法建立方法組合對象model_symbolic。
model_symbolic = SymbolicTransformer(n_components=5, generations=18, function_set=('add', 'sub', 'mul', 'div', 'sqrt', 'log', 'abs', 'neg', 'inv','max', 'min'), max_samples=0.9, metric='pearson', random_state=0, n_jobs=2)
n_components設定組合後的特征為5個,該值設定的是組合後的特征數量,為整數型數值。該值要小于等于參數hall_of_fame的值(該參數本示例中未設定,預設值為100)。
generations設定了演變疊代的次數。
function_set設定的是特征組合函數,這些函數會對一個或多個數組做對應的計算操作,預設功能函數包括add、sub、mul、div。所有的功能函數對應的邏輯如下。
- add:兩個變量(或數組)相加求和,基于np.add計算。
- sub:兩個變量(或數組)相減求差,基于np.subtract計算。
- mul:兩個變量(或數組)相乘求積,基于np.multiply計算。
- div:兩個變量(或數組)相除求商,基于np.divide計算。
- sqrt:特定變量(或數組)絕對值的平方根,基于np.sqrt計算。
- log:特定變量(或數組)絕對值的平方根,基于np.log計算。
- abs:特定變量(或數組)絕對值,基于np.abs計算。
- neg:特定變量(或數組)相反數,基于np.negative計算。
- inv:特定變量(或數組)的倒數,基于1/x計算。
- max:特定變量(或數組)最大值,基于np.maximum計算。
- min:特定變量(或數組)最小值,基于np.minimum計算。
- sin:特定變量(或數組)正弦,基于np.sin計算。
- cos:特定變量(或數組)餘弦,基于np.cos計算。
- tan:特定變量(或數組)正切,基于np.tan計算。
max_samples設定從x中每次抽取的樣本的比例。
meric設定的是拟合名額,該名額可設定為pearson(預設)或spearman,當組合的特征後面使用樹分類器(例如随機森林、GBDT等)時,建議設定為spearman;如果後面使用線性模型做拟合時,建議設定為pearson。
random_state用來控制每次随機時使用相同的初始化種子,避免由于初始随機的不同導緻結果的差異,這在測試過程中經常用到。
n_jobs用來控制參與計算的CPU核心的個數,該參數在skleran的很多算法中都能用到,其值可根據計算機情況做設定,為了使用全部CPU資源,可直接設定為-1。
第6行代碼對model_symbolic對象做fit訓練。fit對象除了設定x和y之外,還可以通過設定sample_weight指定每個樣本的權重。
model_symbolic.fit(x, y) # 訓練資料
第7行代碼對x做特征轉換,傳回一個形狀為[n_samples(與原始x的輸入記錄數一緻),n_components(自定義的組合對象數量)]的數組對象。
symbolic_features = model_symbolic.transform(x) # 轉換資料
print(symbolic_features.shape) # 列印形狀
print(symbolic_features[0]) # 列印第1條資料
最後兩行列印形狀和第1條資料。結果如下:
(506, 5)
[ 1.2132401 0.83252613 -1.84617541 0.11456174 0.48038727]
上述結果顯示了資料集最後保留了5個組合特征。如果我們想要檢視這些特定是通過什麼公式組合生成的,可以直接使用print(model_symbolic),可輸出如下結果:
[inv(sqrt(inv(sqrt(log(sqrt(inv(mul(X10, X12)))))))),
sqrt(inv(sqrt(sqrt(log(mul(X10, X12)))))),
inv(log(inv(sqrt(sqrt(sqrt(mul(X10, X12))))))),
inv(sqrt(mul(X10, X12))),
inv(sqrt(log(mul(X10, X12))))]
通過上述公式發現:生成的特征主要是基于第11和13個原始特征,并做複雜的公式組合生成的,這些公式的複雜性不在于公式本身,而在于其組合的過程略顯複雜,公式間的組合值域會呈指數級增長。
另外,最後生成的組合特征并不一定是唯一的。在本節代碼後面有一段注釋的代碼,代碼中引用了data5.txt資料檔案,使用跟上面的示例完全相同的設定和操作步驟,最後的輸出會發現5個特征是完全相同的,這意味着這些特征都是重複的;并且無論n_components如何設定,都将得到相同的結果。
有關符号回歸的gplearn還有很多知識,限于篇幅本書無法一一介紹,更多知識請讀者查閱
http://gplearn.readthedocs.io/en/stable/intro.html。
在本節中,我們使用了多種方法做資料降維,其中,需要讀者重點考慮的關鍵點如下:
- 根據不同的場景選擇最佳降維方法,該過程需要經驗積累并需要做資料測試。
- 将通過特征選擇、轉換以及組合後的複合特征與後續的算法模型相結合,尤其是找到與後續模型能搭配使用的方法。
代碼實操小結:本節示例中,主要用了以下幾個知識點。
- 通過Numpy的loadtxt讀取資料檔案。
- 使用sklearn的dataset讀取demo資料集。
- 對Numpy矩陣進行切片。
- 使用sklearn的feature_selection中的多種方法做特征選擇,主要包括SelectPercentile、VarianceThreshold、RFE、SelectFromModel。
- 使用sklearn的LDA進行次元轉換、降維。
- 使用sklearn的GBDT方法、PolynomialFeatures方法組合特征。
- 使用gplearn的genetic方法組合特征。
3.4 解決樣本類别分布不均衡的問題
所謂的不均衡指的是不同類别的樣本量差異非常大。樣本類别分布不均衡主要出現在與分類相關的模組化問題上。樣本類别分布不均衡從資料規模上可以分為大資料分布不均衡和小資料分布不均衡兩種。
- 大資料分布不均衡;這種情況下整體資料規模大,隻是其中的小樣本類的占比較少。但是從每個特征的分布來看,小樣本也覆寫了大部分或全部的特征。例如,在擁有1000萬條記錄的資料集中,其中占比50萬條的少數分類樣本便于屬于這種情況。
- 小資料分布不均衡;這種情況下整體資料規模小,并且占據少量樣本比例的分類數量也少,這會導緻特征分布的嚴重不均衡。例如,擁有1000條資料樣本的資料集中,占有10條樣本的分類,其特征無論如何拟合也無法實作完整特征值的覆寫,此時屬于嚴重的資料樣本分布不均衡。
樣本分布不均衡将導緻樣本量少的分類所包含的特征過少,并很難從中提取規律。即使得到分類模型,也容易産生過度依賴于有限的資料樣本而導緻過拟合的問題。當模型應用到新的資料上時,模型的準确性和健壯性将很差。
樣本分布不均衡主要在于不同類别間的樣本比例差異。以筆者的工作經驗看,如果不同分類間的樣本量差異超過10倍就需要引起警覺,并應考慮處理該問題,超過20倍就一定要想法解決了。
3.4.1 哪些營運場景中容易出現樣本不均衡
在資料化營運過程中,以下場景中會經常出現樣本分布不均衡的問題:
- 異常檢測場景。大多數企業中的異常個案都是少量的,比如惡意刷單、黃牛訂單、信用卡欺詐、電力竊電、裝置故障等。這些資料樣本所占的比例通常是整體樣本中很少的一部分。以信用卡欺詐為例,刷實體信用卡欺詐的比例一般在0.1%以内。
- 客戶流失場景。大型企業的流失客戶相對于整體客戶通常是少量的,尤其對于具有壟斷地位的行業巨擘,例如電信、石油、網絡營運商等更是如此。
- 罕見事件的分析。罕見事件與異常檢測類似,都屬于發生個案較少的情況;但不同點在于異常檢測通常都有是預先定義好的規則和邏輯,并且大多數異常事件都對會企業營運造成負面影響,是以針對異常事件的檢測和預防非常重要;但罕見事件則無法預判,并且也沒有明顯的積極和消極影響傾向。例如,由于某網絡大V無意中轉發了企業的一條趣味廣告,導緻使用者流量明顯提升便屬于此類。
- 發生低頻率的事件。這種事件是預期或計劃性事件,但是發生頻率非常低。例如,每年一次的“雙11”購物節一般都會産生較高的銷售額,但放到全年來看,這一天的銷售額占比很可能隻有不到1%,尤其對于很少參與活動的公司而言,這種情況更加明顯。這種就屬于典型的低頻率事件。
3.4.2 通過過抽樣和欠抽樣解決樣本不均衡
抽樣是解決樣本分布不均衡相對簡單且常用的方法,包括過抽樣和欠抽樣兩種。
1)過抽樣:又稱上采樣(over-sampling),其通過增加分類中少數類樣本的數量來實作樣本均衡,最直接的方法是簡單複制少數類樣本以形成多條記錄。這種方法的缺點是,如果樣本特征少則可能導緻過拟合的問題。經過改進的過抽樣方法會在少數類中加入随機噪聲、幹擾資料,或通過一定規則産生新的合成樣本,例如SMOTE算法。
2)欠抽樣:又稱下采樣(under-sampling),其通過減少分類中多數類樣本的數量來實作樣本均衡,最直接的方法是随機去掉一些多數類樣本來減小多數類的規模。缺點是會丢失多數類樣本中的一些重要資訊。
總體上,過抽樣和欠抽樣更适合大資料分布不均衡的情況,尤其是過抽樣方法,應用極為廣泛。
3.4.3 通過正負樣本的懲罰權重解決樣本不均衡
通過正負樣本的懲罰權重解決樣本不均衡的問題的思想是:在算法實作過程中,對于分類中不同樣本數量的類别分别賦予不同的權重(一般思路分類中的小樣本量類别權重高,大樣本量類别權重低),然後進行計算和模組化。
使用這種方法時,不需要對樣本本身做額外處理,隻需在算法模型的參數中進行相應設定即可。很多模型和算法中都有基于類别參數的調整設定,以scikit-learn中的SVM為例,通過在class_weight : {dict, 'balanced'}中針對不同類别來手動指定權重,如果使用其預設的方法balanced,那麼SVM會将權重設定為與不同類别樣本數量呈反比的權重來進行自動均衡處理,計算公式如下:
n_samples / (n_classes * np.bincount(y))
如果算法本身支援,這種思路是更加簡單且高效的方法。
3.4.4 通過組合/內建方法解決樣本不均衡
組合/內建方法指的是在每次生成訓練集時使用所有分類中的小樣本量,同時從分類中的大樣本量中随機抽取資料來與小樣本量合并構成訓練集,這樣反複多次會得到很多訓練集和訓練模型。最後在應用時,使用組合方法(例如投票、權重投票等)産生分類預測結果。
例如,資料集中的正、負例的樣本分别為100條和10000條,比例為1:100。此時可以将負例樣本(類别中的大量樣本集)随機分為100份(當然也可以分更多),每份100條資料;然後每次形成訓練集時使用所有的正樣本(100條)和随機抽取的負樣本(100條)形成新的資料集。如此反複可以得到100個訓練集和對應的訓練模型。
這種解決問題的思路類似于随機森林。在随機森林中,雖然每個小決策樹的分類能力很弱,但是通過大量的“小樹”組合形成的“森林”具有良好的模型預測能力。
如果計算資源充足,并且對于模型的時效性要求不高,這種方法比較合适。
3.4.5 通過特征選擇解決樣本不均衡
上述幾種方法都是基于資料行的操作,通過多種途徑可使不同類别的樣本資料行記錄均衡。除此以外,還可以考慮使用或輔助于基于列的特征選擇方法。
一般情況下,樣本不均衡也會導緻特征分布不均衡,但如果小類别樣本量具有一定的規模,那麼意味着其特征值的分布較為均勻,可通過選擇具有顯著型的特征配合參與解決樣本不均衡問題,也能在一定程度上提高模型效果。
上述幾種方法的思路都是基于分類問題解決的。實際上,這種從大規模資料中尋找罕見資料的情況,也可以使用非監督式的學習方法,例如使用One-class SVM進行異常檢測。分類是監督式方法,前期是基于帶有标簽(Label)的資料進行分類預測;而采用非監督式方法,則是使用除了标簽以外的其他特征進行模型拟合,這樣也能得到異常資料記錄。是以,要解決異常檢測類的問題,先是考慮整體思路,然後再考慮方法模型。
3.4.6 代碼實操:Python處理樣本不均衡
本示例中,我們主要使用一個新的專門用于不平衡資料處理的Python包imbalanced-learn。除此之外,我們還會使用sklearn的SVM在算法中通過調整類别權重來處理樣本不均衡問題。本示例使用的資料源檔案data2.txt位于“附件-chapter3”中。
import pandas as pd
from imblearn.over_sampling import SMOTE # 過抽樣處理庫SMOTE
from imblearn.under_sampling import RandomUnderSampler # 欠抽樣處理庫
RandomUnderSampler
from sklearn.svm import SVC # SVM中的分類算法SVC本示例中用到了第三方庫imbalanced-learn實作主要的樣本不均衡處理,而Pandas的引入主要用于解釋和說明不同處理方法得到的結果集樣本的分布情況,sklearn.svm中的SVC主要用于說明SVM如何在算法中自動調整分類權重。
df = pd.read_table('data2.txt', sep=' ', names=['col1', 'col2', 'col3', 'col4', 'col5', 'label']) # 讀取資料檔案
x, y = df.iloc[:, :-1],df.iloc[:, -1] # 切片,得到輸入x,标簽y
groupby_data_orgianl = df.groupby('label').count() # 對label做分類彙總
print(groupby_data_orgianl) # 列印輸出原始資料集樣本分類分布
該過程中使用Pandas的read_table讀取本地檔案,為了更好地差別不同的列,通過names指定列名;對資料框做切片分割得到輸入的x和目标變量y;通過Pandas的groupby()方法按照label列做分類彙總,彙總方式是使用count()函數計數。輸入原始資料集樣本分類分布如下:
col1 col2 col3 col4 col5
label
0.0 942 942 942 942 942
1.0 58 58 58 58 58
輸出結果顯示,原始資料集中,正樣本(label為1)的數量僅有58個,占總樣本量的5.8%,屬于嚴重不均衡分布。
第3部分使用SMOTE方法進行過抽樣處理。
model_smote = SMOTE() # 建立SMOTE模型對象
x_smote_resampled, y_smote_resampled = model_smote.fit_sample(x, y)
\# 輸入資料并做過抽樣處理
x_smote_resampled = pd.DataFrame(x_smote_resampled, columns=['col1', 'col2', 'col3', 'col4', 'col5']) # 将資料轉換為資料框并命名列名
y_smote_resampled = pd.DataFrame(y_smote_resampled, columns=['label'])
\# 将資料轉換為資料框并命名列名
smote_resampled = pd.concat([x_smote_resampled, y_smote_resampled], axis=1)
\# 按列合并資料框
groupby_data_smote = smote_resampled.groupby('label').count() # 對label做分類彙總
print(groupby_data_smote) # 列印輸出經過SMOTE處理後的資料集樣本分類分布
該過程中首先建立SMOTE模型對象,并直接應用fit_sample對資料進行過抽樣處理,如果要獲得有關SMOTE的具體參數資訊,可先使用fit(x,y)方法獲得模型資訊,并得到模型不同參數和屬性;從fit_sample方法分别得到對x和y過抽樣處理後的資料集,将兩份資料集轉換為資料框然後合并為一個整體資料框;最後通過Pandas提供的groupby()方法按照label類做分類彙總,彙總方式是使用count()函數計數。經過SMOTE處理後的資料集樣本分類分布如下:
col1 col2 col3 col4 col5
1.0 942 942 942 942 942
通過對比第2部分代碼段的原始資料集傳回結果發現,該結果中的正樣本(label為1)的數量增加,并與負樣本數量相同,均為942條,資料分類樣本得到平衡。
第4部分使用RandomUnderSampler方法進行欠抽樣處理。
model_RandomUnderSampler = RandomUnderSampler() # 建立RandomUnderSampler模型對象
x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled = model_RandomUnderSampler.fit_sample( x, y) # 輸入資料并做欠抽樣處理
x_RandomUnderSampler_resampled = pd.DataFrame(x_RandomUnderSampler_resampled, columns=['col1', 'col2', 'col3', 'col4', 'col5']) # 将資料轉換為資料框并命名列名
y_RandomUnderSampler_resampled = pd.DataFrame(y_RandomUnderSampler_resampled, columns=['label']) # 将資料轉換為資料框并命名列名
RandomUnderSampler_resampled = pd.concat([x_RandomUnderSampler_resampled, y_RandomUnderSampler_resampled], axis=1) # 按列合并資料框
groupby_data_RandomUnderSampler = RandomUnderSampler_resampled.groupby('label').count() # 對label做分類彙總
print(groupby_data_RandomUnderSampler) # 列印輸出經過RandomUnderSa-mpler處理後的資料集樣本分類分布
該過程與第3部分步驟完全相同,在此略過各子產品介紹,用途都已在代碼備注中注明。經過RandomUnderSampler處理後的資料集樣本分類分布如下:
col1 col2 col3 col4 col5
0.0 58 58 58 58 58
對比第2部分代碼段的原始資料集傳回的結果,該結果中的負樣本(label為0)的數量減少,并跟正樣本相同,均為58條,樣本得到平衡。
第5部分使用SVM的權重調節處理不均衡樣本。
model_svm = SVC(class_weight='balanced',gamma='scale') # 建立SVC模型對象并指定類别權重
model_svm.fit(x, y) # 輸入x和y并訓練模型
該過程主要通過SVC中的class_weight參數和值的設定來處理樣本權重,該參數可設定為字典、None或字元串balanced 3種模式。
- 字典:通過手動指定的不同類别的權重,例如{1:10,0:1}。
- None:代表類别的權重相同。
-
balanced:代表算法将自動調整與輸入資料中的類頻率成反比的權重,具體公式如下。
n_samples /(n_classes * np.bincount(y))
- 程式示例中使用了該方法。
經過設定後,算法自動處理樣本分類權重,無須使用者做其他處理。要對新的資料集做預測,隻需要調用model_svm模型對象的predict方法即可。
上述過程中,主要需要考慮的關鍵點是:如何針對不同的具體場景選擇最合适的樣本均衡解決方案,選擇過程中既要考慮每個類别樣本的分布情況以及總樣本情況,又要考慮後續資料模組化算法的适應性,以及整個資料模型計算的資料時效性。
- 通過Pandas的read_table方法讀取文本資料檔案,并指定列名。
- 對資料框做切片處理。
- 通過Pandas提供的groupby()方法配合count()做分類彙總。
- 使用imblearn.over_sampling中的SMOTE做過抽樣處理。
- 使用imblearn.under_sampling中的RandomUnderSampler做欠抽樣處理。
- 使用sklearn.svm 中的SVC自動調整算法設定不同類别的權重。
3.5 資料化營運要抽樣還是全量資料
抽樣是從整體樣本中通過一定的方法選擇一部分樣本。抽樣是資料處理的基本步驟之一,也是科學實驗、品質檢驗、社會調查普遍采用的一種經濟有效的工作和研究方法。
3.5.1 什麼時候需要抽樣
抽樣工作在資料擷取較少或處理大量資料比較困難的時期非常流行,這主要有以下幾方面原因:
- 資料計算資源不足。計算機軟硬體的限制是導緻抽樣産生的基本原因之一,尤其是在資料密集的生物、科學工程等領域,不抽樣往往無法對海量資料進行計算。
- 資料采集限制。很多時候抽樣從資料采集端便已經開始,例如做社會調查必須采用抽樣方法進行研究,因為根本無法對所有人群做調查。
- 時效性要求。抽樣帶來的是以局部反映全局的思路,如果方法正确,可以以極小的資料計算量來實作對整體資料的統計分析,在時效性上會大大增強。
如果存在上述條件限制或有類似強制性要求,那麼抽樣工作仍然必不可少。但是在目前資料化營運的大背景下,資料計算資源充足、資料采集端可以采集更多的資料并且可以通過多種方式滿足時效性的要求,抽樣工作是否就沒有必要了?其實不是的,即使上述限制條件都滿足,還有很多場景依然需要通過抽樣方法來解決具體問題。
- 通過抽樣來實作快速的概念驗證。資料工作中可能會包括創新性或常識性項目,對于這類項目進行快速驗證、疊代和傳遞結論往往是概念驗證的關鍵,通過抽樣方法帶來的不僅是計算效率的提升,還有前期資料準備、資料預處理、算法實作等各個方面的開發,以及伺服器、硬體的配套方案的部署等内容的可行性、簡單化和可操作性。
- 通過抽樣來解決樣本不均衡問題。在3.4節中我們提到,通過欠抽樣、過抽樣以及組合/內建的方法解決不均衡的問題,這個過程就用到了抽樣方法。
- 無法實作對全部樣本覆寫的資料化營運場景。典型場景包括市場研究、客戶線下調研分析、産品品質檢驗、使用者電話滿意度調查等,在這些場景下無法實作對所有樣本的采集、分析、處理和模組化。
- 定性分析的工作需要。在定性分析工作中,通常不需要定量分析時的完整假設、精确資料和複雜統計分析過程,更多的是采用通路、觀察和文獻法收集資料并通過主觀了解和定性分析找到問題答案,該過程中主要依靠人自身的能力而非密集的計算機能力來完成研究工作。如果不使用抽樣方法,那麼定性分析将很難完成。
3.5.2 如何進行抽樣
抽樣方法從整體上分為非機率抽樣和機率抽樣兩種。非機率抽樣不是按照等機率的原則進行抽樣,而是根據人類的主觀經驗和狀态進行判斷;機率抽樣則是以數學機率論為基礎,按照随機的原則進行抽樣。本節以下内容介紹的抽樣方法屬于機率抽樣。
(1)簡單随機抽樣
該抽樣方法是按等機率原則直接從總樣本中抽取n個樣本,這種随機抽樣方法簡單、易于操作,但是它并不能保證樣本能完美代表總體。這種抽樣的基本前提是所有樣本個體都是等機率分布的,但真實情況卻是多數樣本都不是或無法判斷是否是等機率分布的。在簡單随機抽樣中,得到的結果是不重複的樣本集,還可以使用有放回的簡單随機抽樣,這樣得到的樣本集中會存在重複資料。該方法适用于個體分布均勻的場景。
(2)等距抽樣
等距抽樣是先将總體中的每個個體按順序編号,然後計算出抽樣間隔,再按照固定抽樣間隔抽取個體。這種操作方法易于了解、簡便易行,但當總體樣本的分布呈現明顯的分布規律時容易産生偏差,例如增減趨勢、周期性規律等。該方法适用于個體分布均勻或呈現明顯的均勻分布規律,無明顯趨勢或周期性規律的資料。
(3)分層抽樣
分層抽樣是先将所有個體樣本按照某種特征劃分為幾個類别,然後從每個類别中使用随機抽樣或等距抽樣的方法選擇個體組成樣本。這種操作方法能明顯降低抽樣誤差,并且便于針對不同類别的資料樣本進行單獨研究,是以是一種較好的實作方法。該方法适用于帶有分類邏輯的屬性、标簽等特征的資料。
(4)整群抽樣
整群抽樣是先将所有樣本分為幾個小群體集,然後随機抽樣幾個小群體集來代表總體。這種操作方法與之前的3種方法的差異點在于該方法抽取的是小群體集,而不是每個資料個體本身。該方法雖然簡單易行,但是樣本的分布受限于小群體集的劃分,抽樣誤差較大。這種方法适用于小群體集的特征差異比較小的資料,并且對劃分小群體集有更高要求。
3.5.3 抽樣需要注意的幾個問題
1.資料抽樣要能反映營運背景
資料能正确反映營運背景,這看起來非常簡單,但實際上需要資料工作者對于營運環節和流程非常熟悉才有可能實作。以下是常見的抽樣不能反映營運背景的情況。
- 資料時效性問題:使用過時的資料(例如1年前的資料)來分析現在的營運狀态。
- 缺少關鍵因素資料:沒有将營運分析涉及的主要因素所産生的資料放到抽樣資料中,導緻無法根據主要因素産生有效結論,模型效果差,例如抽樣中沒有覆寫大型促銷活動帶來的銷售增長。
- 不具備業務随機性:有意/無意多抽取或覆寫特定資料場景,使得資料明顯趨向于特定分布規律,例如在做社會調查時使用北京市的抽樣資料來代表全國。
-
沒有考慮業務增長性:在成長型公司中,公司的發展不都是呈現線性趨勢的,很多時候會呈現指數趨勢。
這時需要根據這種趨勢來使業務滿足不同增長階段的分析需求,而不隻是集中于增長爆發區間。
- 沒有考慮資料來源的多樣性:隻選擇某一來源的資料做抽樣,使得資料的分布受限于資料源。例如在做各分公司的銷售分析時,僅将北方大區的資料納入其中做抽樣,而忽視了其他大區的資料,其結果必然有所偏頗。
- 業務資料可行性問題:很多時候,由于受到經費、權限、職責等方面的限制,在資料抽樣方面無法按照資料工作要求來執行,此時要根據營運實際情況調整。這點往往被很多資料工作者忽視。
2.資料抽樣要能滿足資料分析和模組化需求
資料抽樣必須兼顧後續的其他資料處理工作,尤其是分析和模組化需求。這時需要注意以下幾個方面的問題。
(1)抽樣樣本量的問題
對于大多數資料分析模組化而言,資料規模越大,模型拟合結果越準确。但到底如何定義資料量的大小,筆者根據不同類型的資料應用總結為以下幾個次元:
- 以時間為次元分布的,至少包含一個能滿足預測的完整業務周期。例如,做月度銷售預測的,至少包含12個月的資料;做日銷售預測的,至少包含30天的資料,如果一天中包含特定周期,則需要重複多個周期。同時,時間性特征的要充分考慮季節性、波動性、節假日等特殊規律,這些都要盡量包含在抽樣資料中。
- 做預測(包含分類和回歸)分析模組化的,需要考慮特征數量和特征值域(非數值型)的分布,通常資料記錄數要同時是特征數量和特征值域的100倍以上。例如資料集有5個特征,假如每個特征有2個值域,那麼資料記錄數需要至少在1000(100×5×2)條以上。
- 做關聯規則分析模組化的,根據關聯前後項的數量(每個前項或後項可包含多個要關聯的主體,例如品牌+商品+價格關聯),每個主體需要至少1000條資料。例如隻做單品銷售關聯,那麼單品的銷售記錄需要在1000條以上;如果要同時做單品+品牌的關聯,那麼需要至少2000條資料。
- 對于異常檢測類分析模組化的,無論是監督式還是非監督式模組化,由于異常資料本來就是小機率分布的,是以異常資料記錄一般越多越好。
以上的資料記錄數不是固定的,在實際工作時,如果沒有特定時間要求,筆者一般會選擇一個适中的樣本量做分析,此時應綜合考慮特征數、特征值域分布數、模型算法适應性、模組化需求等;如果是面向機器計算的工作項目,一般會選擇盡量多的資料參與計算,而有關算法實時性和效率的問題會讓技術和運維人員配合實作,例如提高伺服器配置、擴大分布式叢集規模、優化底層程式代碼、使用實時計算的引擎和機制等。
(2)抽樣樣本在不同類别中的分布問題
做分類分析模組化問題時,不同類别下的資料樣本需要均衡分布,有關資料樣本均衡分布的更多話題參見3.4節。
抽樣樣本能準确代表全部整體特征:
- 非數值型的特征值域(例如各值頻數相對比例、值域範圍等)分布需要與總體一緻。
- 數值型特征的資料分布區間和各個統計量(如均值、方差、偏度等)需要與整體資料分布區間一緻。
- 缺失值、異常值、重複值等特殊資料的分布要與整體資料分布一緻。
異常檢測類資料的處理:
- 對于異常檢測類的應用要包含全部異常樣本。對于異常檢測類的分析模組化,本來異常資料就非常稀少,是以抽樣時要優先将異常資料包含進去。
- 對于需要去除非業務因素的資料異常,如果有類别特征需要與類别特征分布一緻;如果沒有類别特征,屬于非監督式的學習,則需要與整體分布一緻。
3.5.4 代碼實操:Python資料抽樣
本示例中,将使用random包以及自定義代碼實作抽樣處理。資料源檔案data2.txt、data3.txt和data4.txt位于“附件-chapter3”中。
整個示例代碼分為5部分。
第1部分導入需要的庫。
import random # 導入标準庫
import numpy as np # 導入第三方庫
這裡用到了Python内置标準庫random以及第三方庫Numpy,前者用于做随機抽樣,後者用于讀取檔案并做資料切片使用。
第2部分實作了簡單随機抽樣。
data = np.loadtxt('data3.txt') # 導入普通資料檔案
data_sample = data[random.sample([i for i in range(len(data))], 2000)]
第3部分實作了等距抽樣。
data = np.loadtxt('data3.txt') # 導入普通資料檔案
sample_count = 2000 # 指定抽樣數量
record_count = data.shape[0] # 擷取最大樣本量
width = record_count / sample_count # 計算抽樣間距
data_sample = [] # 初始化空白清單,用來存放抽樣結果資料
i = 0 # 自增計數以得到對應索引值
while len(data_sample) <= sample_count and i * width <= record_count - 1:
# 當樣本量小于等于指定抽樣數量并且矩陣索引在有效範圍内時
data_sample.append(data[int(i \* width)]) # 新增樣本
i += 1 # 自增長
print(data_sample[:2]) # 列印輸出前2條資料
print(len(data_sample)) # 列印輸出樣本數量
首先使用Numpy的loadtxt方法讀取資料檔案;然後指定抽樣樣本量為2000,并通過讀取原始資料的形狀找到最大樣本量邊界,這可以用來作為循環的終止條件之一;接着通過最大樣本量除抽樣樣本量得到抽樣間距;建立一個空清單用于存儲最終抽樣結果資料,通過一個變量i做循環增長并用來做索引遞增,然後進入抽樣條件判斷過程。
- 當樣本量小于等于指定抽樣數量并且矩陣索引在有效範圍内時做處理,這裡需要注意的是索引從0開始,是以最大數量值減去1得到循環邊界,否則會報索引溢出錯誤。
- 通過清單的append方法不斷追加通過間距得到的新增樣本,在本節後面的方法中還會提到清單追加的extend方法,前者用于每次追加1個元素,後者用于批量追加多個元素。
- i += 1指的是每次循環都增加1,可以寫成i = i + 1。
- 最後列印輸出前2條資料和抽樣樣本量。
傳回結果如下:
[array([-3.08057779, 8.09020329, 2.02732982, 2.92353937, -6.06318211]), array([-2.11984871, 7.74916701, 5.7318711 , 4.75148273, -5.68598747])]
2000
第4部分實作了分層抽樣。
data2 = np.loadtxt('data2.txt') # 導入帶有分層邏輯的資料
each_sample_count = 200 # 定義每個分層的抽樣數量
label_data_unique = np.unique(data2[:, -1]) # 定義分層值域
sample_data = [] # 定義空清單,用于存放最終抽樣資料
sample_dict = {} # 定義空字典,用來顯示各分層樣本數量
for label_data in label_data_unique: # 周遊每個分層标簽
sample_list = [] # 定義空清單,用于存放臨時分層資料
for data_tmp in data2: # 讀取每條資料
if data_tmp[-1] == label_data: # 如果資料最後一列等于标簽
sample_list.append(data_tmp) # 将資料加入分層資料中
each_sample_data = random.sample(sample_list, each_sample_count)
# 對每層資料都随機抽樣
sample_data.extend(each_sample_data) # 将抽樣資料追加到總體樣本集
sample_dict[label_data] = len(each_sample_data) # 樣本集統計結果
print(sample_dict) # 列印輸出樣本集統計結果
首先使用Numpy的loadtxt方法導入帶有分層邏輯的資料。在該示例中,讀取的資料檔案中包含了分類标簽,放在最後一列。該列分類标簽用于做分層抽樣的辨別。接着通過unique方法擷取分層(分類标簽)的值域,用于後續做循環處理。然後分别定義了用于存放臨時分層資料、最終抽樣資料、顯示各分層樣本數量的空清單和空字典。
下面進入正式的主循環過程,實作分層抽樣:
- 周遊每個分層标簽,用來做資料的分層劃分,資料一共分為2類标簽(0和1)。
- 讀取每條資料并判斷資料的分層标簽是否與分層标簽相同,如果是則将資料加入各分層資料清單中。
- 當每個分層标簽處理完成後會得到該分層标簽下的所有資料,此時使用Python内置的random庫的sample方法進行抽樣。由于抽樣結果是一個清單,是以這裡使用extend(而不是append)批量追加到最終抽樣資料清單中。然後将每個分層标簽得到的樣本數量,通過len方法對清單長度進行統計,并列印輸出各個分層對應的樣本數量。結果是每個分層都按照指定數量抽取樣本,輸出如下:
{0.0: 200, 1.0: 200}
第5部分實作了整群抽樣。
data3 = np.loadtxt('data4.txt') # 導入已經劃分好整群的資料集
label_data_unique = np.unique(data3[:, -1]) # 定義整群标簽值域
print(label_data_unique) # 列印輸出所有整群标簽
sample_label = random.sample(set(label_data_unique), 2) # 随機抽取2個整群
sample_data = [] # 定義空清單,用來存儲最終抽樣資料
for each_label in sample_label: # 周遊每個整群标簽值域
for data_tmp in data3: # 周遊每個樣本
if data_tmp[-1] == each_label: # 判斷樣本是否屬于抽樣整群
sample_data.append(data_tmp) # 樣本添加到最終抽樣資料集
print(sample_label) # 列印輸出樣本整群标簽
print(len(sample_data)) # 列印輸出總抽樣資料記錄條數
首先使用Numpy的loadtxt方法導入已經劃分好整群的資料集。在該示例中,讀取的資料檔案中的最後一列存放了不同整群的辨別,整群一共被劃分為4個群組,辨別分别為0、1、2、3。接着通過unique方法擷取整群标簽的值域,用于基于整群的抽樣。列印輸出結果如下:
[ 0. 1. 2. 3.]
然後使用Random的sample方法從整群标簽中進行抽樣,這裡定義抽取2個整群。最後将所有屬于抽取到的整群下的資料進行讀取和追加,并得到最終樣本集,列印輸出樣本集的整群标簽和總樣本數量,結果如下:
[3.0, 1.0]
502
上述過程中,需要考慮的關鍵點是:如何根據不同的資料特點、模組化需求、業務背景綜合考慮抽樣方法,得到最适合的結果
- 使用Numpy的loadtxt方法讀取資料檔案。
- 使用内置标準庫Random庫中的sample方法做資料抽樣。
- 對清單通過索引做截取、通過len方法做長度統計、通過append和extend做追加等操作。
- 字典指派操作。
- 使用Numpy的unique方法獲得唯一值。
- 通過for和while循環,周遊一個可疊代的對象。
- if條件語句的使用,尤其是單條件和多條件判斷。
3.6 解決營運資料的共線性問題
所謂共線性(又稱多重共線性)問題指的是輸入的自變量之間存在較高的線性相關度。共線性問題會導緻回歸模型的穩定性和準确性大大降低,另外,過多無關的次元參與計算也會浪費計算資源和時間。
共線性問題是否常見取決于具體業務場景。常見的具有明顯的共線性的次元或變量如下:
- 通路量和頁面浏覽量。
- 頁面浏覽量和通路時間。
- 訂單量和銷售額。
- 訂單量和轉化率。
- 促銷費用和銷售額。
- 網絡展示廣告費用和訪客數。
導緻出現變量間共線性的原因可能包括:
- 資料樣本不夠,導緻共線性存在偶然性,這其實反映了缺少資料對于資料模組化的影響,共線性僅僅是影響的一部分。
- 多個變量都基于時間,有共同或相反的演變趨勢,例如,春節期間的網絡銷售量和銷售額都相對于正常時間有下降趨勢。
- 多個變量間存在一定的推移關系,但總體上變量間的趨勢一緻,隻是發生的時間點不一緻,例如品牌廣告費用和銷售額之間,通常是品牌廣告先進行大範圍的曝光和資訊推送,經過一定時間傳播之後,銷售額才能爆發出來。
- 多個變量間存在近似線性的關系。例如,用y代表訪客數,用x代表展示廣告費用,那麼二者的關系很可能是y=2*x+b,即每投放1元錢,可以帶來大概2~3個訪客。
3.6.1 如何檢驗共線性
共線性一般通過容忍度、方差膨脹因子、特征值這幾個特征資料來做檢驗。
- 容忍度(Tolerance):容忍度是每個自變量作為因變量對其他自變量進行回歸模組化時得到的殘差比例,大小用1減得到的決定系數來表示。容忍度的值介于0和1之間,值越小,說明這個自變量與其他自變量間存在共線性問題的可能性越大。
- 方差膨脹因子(Variance Inflation Factor,VIF):VIF是容忍度的倒數,值越大則共線性問題越明顯,通常以10作為判斷邊界。VIF<10,不存在多重共線性;10≤VIF<100,存在較強的多重共線性;VIF≥100,存在嚴重多重共線性。
- 特征值(Eigenvalue):該方法實際上就是對自變量進行主成分分析,如果多個次元的特征值等于0,則可能有比較嚴重的共線性。
除此以外,還可以使用相關系數輔助判斷,當相關系數R>0.8時,表示可能存在較強的相關性。有關相關性的更多話題會在3.8節中探讨。通常這些方法得到的結果都是相關的,即滿足其中某個特征後,其他特征也基本滿足。是以可以通過多種方法共同驗證。
3.6.2 解決共線性的5種常用方法
解決共線性的5種常用方法如下。
(1)增大樣本量
通過增加樣本量來消除由于資料量不足而出現的偶然共線性現象,在可行的前提下這種方法是需要優先考慮的。但即使增加了樣本量,可能也無法解決共線性問題,原因是很可能變量間确實存在這個問題。
(2)嶺回歸法
嶺回歸(Ridge Regression)分析是一種專用于共線性問題的有偏估計回歸方法,實質上是一種改良的最小二乘估計法。它通過放棄最小二乘法的無偏性,以損失部分資訊、降低精度為代價來獲得更實際和可靠性更強的回歸系數。是以嶺回歸在存在較強共線性的回歸應用中較為常用。
(3)逐漸回歸法
逐漸回歸法(Stepwise Regression)是每次引入一個自變量并進行統計檢驗,然後逐漸引入其他變量,同時對所有變量的回歸系數進行檢驗。如果原來引入的變量由于後面變量的引入而變得不再顯著,那麼就将其剔除,逐漸得到最優回歸方程。
(4)主成分回歸(Principal Components Regression)
通過主成分分析,将原始參與模組化的變量轉換為少數幾個主成分,每個主成分是原變量的線性組合,然後基于主成分做回歸分析,這樣也可以在不丢失重要資料特征的前提下避開共線性問題。
(5)人工去除
直接結合人工經驗,對參與回歸模型計算的自變量進行删減,也是一個較為常用的方法。但這種方法需要操作者對于業務、模型和資料都有相對深入的了解,才有可能做出正确的操作。從專業角度分析,如果缺少上述3個方面中的任何一個,那麼人工去除的方式都有可能産生偏差,導緻結果不準确。
3.6.3 代碼實操:Python處理共線性問題
本示例中,将通過sklearn進行共線性處理。源檔案data5.txt位于“附件-chapter3”中。
在自動化工作(尤其是以Python為代表的智能化資料工作)中,通常不會通過人工的方法參與算法結果觀察、調優、選擇等。同樣,在解決共線性的方法中,通過程式的方式自動選擇或規避是最佳解決方法。在本示例中,将主要使用嶺回歸和主成分回歸解決共線性問題。
完整代碼如下。
第1部分導入相關庫。
from sklearn.linear_model import Ridge
from sklearn.decomposition import PCA
from sklearn.linear_model import LinearRegression
示例中用到了Numpy、sklearn中的Ridge(嶺回歸)、PCA(主成分分析)和LinearRegres-sion(普通線性回歸)。由于本書用到的Python庫中沒有直接內建主成分回歸方法,是以這裡将通過PCA+LinearRegression的形式組合實作。
第2部分導入資料。
data = np.loadtxt('data5.txt', delimiter='t') # 讀取資料檔案
x,y = data[:, :-1],data[:, -1] # 切分自變量和預測變量
使用Numpy的loadtxt方法讀取資料檔案,資料檔案以tab分隔,共1000條資料,有9個自變量和1個因變量。因變量處于最後一列,使用Numpy矩陣進行資料切分。
第3部分使用嶺回歸算法進行回歸分析。
model_ridge = Ridge(alpha=1.0) # 建立嶺回歸模型對象
model_ridge.fit(x, y) # 輸入x、y訓練模型
print(model_ridge.coef_) # 列印輸出自變量的系數
print(model_ridge.intercept_) # 列印輸出截距
該過程中,先建立嶺回歸模型對象,指定alpha值為0.1,接着通過fit方法将x和y分别輸入模型做訓練,然後列印輸出回歸方程中自變量的系數和截距。結果如下:
[ 8.50164360e+01 -1.18330186e-03 9.80792921e-04 -8.54201056e-04
2.10489064e-05 2.20180449e-04 -3.00990875e-06 -9.30084240e-06
-2.84498824e-08]
-7443.98652868
上述結果中包含了各個輸入變量的系數以及截距,假設因變量為y,自變量為x1、x2、…、x9,那麼該方程可以寫成:
y = 85.016436×x1+(-0.00118330186)×x2+0.000980792921×x3+(-0.000854201056)×x4+0.0000210489064×x5+0.000220180449×x6+(-0.00000300990875)×x7+(-0.0000093008424)× x8+(-0.0000000284498824)×x9-7443.98652868
第4部分使用主成分回歸進行回歸分析。
model_pca = PCA() # 建立PCA模型對象
data_pca = model_pca.fit_transform(x) # 對x進行主成分分析
ratio_cumsm = np.cumsum(model_pca.explained_variance_ratio_)
# 得到所有主成分方差占比的累積資料
print(ratio_cumsm) # 列印輸出所有主成分方差占比累積
rule_index = np.where(ratio_cumsm > 0.8) # 擷取方差占比超過0.8的所有索引值
min_index = rule_index0 # 擷取最小索引值
data_pca_result = data_pca[:, :min_index + 1] # 根據最小索引值提取主成分
model_liner = LinearRegression() # 建立回歸模型對象
model_liner.fit(data_pca_result, y) # 輸入主成分資料和預測變量y并訓練模型
print(model_liner.coef_) # 列印輸出自變量的系數
print(model_liner.intercept_) # 列印輸出截距
該過程主要分為以下步驟:
- 先建立PCA模型對象,然後使用fit_transform方法直接進行主成分轉換。這裡沒有指定具體主成分的數量,原因是在得到主成分的方差貢獻率之前,無法知曉到底選擇多少個合适,後續會通過指定門檻值的方式自動實作成分選擇。
- 通過PCA模型對象的explained_variance_ratio_得到各個主成分的方差貢獻率(方差占比),然後使用Numpy的cumsum方法計算累積占比。得到如下輸出結果:
[ 0.9028 0.98570494 0.99957412 0.99995908 0.99999562 0.99999939
0.99999999 1. 1. ]
直覺上分析,前2個主成分基本已經完全代表所有成分參與模型計算。但是如果自動化實作,我們不可能每次先中斷程式并列印出結果,再使用人工判斷。是以我們需要一個門檻值,當方差貢獻達到門檻值時,就自動選擇前n個主成分作為最終轉換次元。
- 通過使用Numpy的where方法找到主成分方差占比累積>0.8的值索引,通常0.8就已經能代表大部分的資料特征,本示例較為特殊,第1個主成分就已經非常明顯。該方法傳回的是一個元組,從元組中得到最小的索引值(越往後累積占比越大,索引值也越大,是以第1個符合條件的索引就是我們要取出的最後一個主成分)。
- 在獲得最小索引值之後,根據最小索引值提取主成分。之是以切片時在索引列值上加1,是由于預設索引值的右側是不包含的,例如[0:1]隻取出第0列,而不是我們需要的0和1兩列。
- 接着進入線性回歸的環節。與嶺回歸的步驟類似,建立回歸模型對象并輸入x和y做模型訓練,最後列印輸出回歸方程中自變量的系數和截距。結果如下:
[ 1.26262171e-05]
1058.52726
上述結果中包含了輸入變量的系數以及截距,假設因變量為y,滿足自變量方差占比大于0.9的主成分為第1個主成分。假設其為x1,那麼該方程可以寫成:
y = 0.0000126262171×x1+1058.52726
3.7 有關相關性分析的混沌
相關性分析是指對多個具備相關關系的變量進行分析,進而衡量變量間的相關程度或密切程度。相關性可以應用到所有資料的分析過程中,任何事物之間都是存在一定的聯系。相關性用R(相關系數)表示,R的取值範圍是[-1, 1]。
3.7.1 相關和因果是一回事嗎
相關性不等于因果。用x1和x2作為兩個變量進行解釋,相關意味着x1和x2是邏輯上的并列相關關系,而因果聯系可以解釋為因為x1是以x2(或因為x2是以x1)的邏輯關系,二者是完全不同的。
用一個營運示例來說明二者的關系:做商品促銷活動時,通常都會以較低的價格進行銷售,以此來實作較高的商品銷量;随着商品銷售的提升,也給線下物流配送體系帶來了更大的壓力,在該過程中通常會導緻商品破損量的增加。
本案例中,商品低價與破損量增加并不是因果關系,即不能說因為商品價格低是以商品破損量增加;二者的真實關系是都是基于促銷這個大背景下,低價和破損量都是基于促銷産生的。
相關性的真實價值不是用來分析“為什麼”的,而是通過相關性來描述無法解釋的問題背後真正成因的方法。相關性的真正的價值是能知道“是什麼”,即無論通過何種因素對結果産生影響,最終出現的規律就是二者會一起增加或降低等。
仍然是上面的案例,通過相關性分析我們可以知道,商品價格低和破損量增加是相伴發生的,這意味着當價格低的時候(通常是做銷售活動,也有可能産品品質問題、物流配送問題、包裝問題等),我們就想到破損量可能也會增加。但是到底由什麼導緻的破損量增加,是無法通過相關性來得到的。
3.7.2 相關系數低就是不相關嗎
R(相關系數)低就是不相關嗎?其實不是。
1)R的取值可以為負,R= -0.8代表的相關性要高于R=0.5。負相關隻是意味着兩個變量的增長趨勢相反,是以需要看R的絕對值來判斷相關性的強弱。
2)即使R的絕對值低,也不一定說明變量間的相關性低,原因是相關性衡量的僅僅是變量間的線性相關關系,變量間除了線性關系外,還包括指數關系、多項式關系、幂關系等,這些“非線性相關”的相關性不在R(相關性分析)的衡量範圍之内。
3.7.3 代碼實操:Python相關性分析
本示例中,将使用Numpy進行相關性分析。源檔案data5.txt位于“附件-chapter3”中。
import numpy as np # 導入庫
data = np.loadtxt('data5.txt', delimiter='t') # 讀取資料檔案
x = data[:, :-1] # 切分自變量
correlation_matrix = np.corrcoef(x, rowvar=0) # 相關性分析
print(correlation_matrix.round(2)) # 列印輸出相關性結果
示例中實作過程如下:
1)先導入Numpy庫;
2)使用Numpy的loadtxt方法讀取資料檔案,資料檔案以tab分隔;
3)矩陣切片,切分出自變量用來做相關性分析;
4)使用Numpy的corrcoef方法做相關性分析,通過參數rowvar = 0控制對列做分析;
5)列印輸出相關性矩陣,使用round方法保留2位小數。結果如下:
[[ 1. -0.04 0.27 -0.05 0.21 -0.05 0.19 -0.03 -0.02]
[-0.04 1. -0.01 0.73 -0.01 0.62 0. 0.48 0.51]
[ 0.27 -0.01 1. -0.01 0.72 0. 0.65 0.01 0.02]
[-0.05 0.73 -0.01 1. 0.01 0.88 0.01 0.7 0.72]
[ 0.21 -0.01 0.72 0.01 1. 0.02 0.91 0.03 0.03]
[-0.05 0.62 0. 0.88 0.02 1. 0.03 0.83 0.82]
[ 0.19 0. 0.65 0.01 0.91 0.03 1. 0.03 0.03]
[-0.03 0.48 0.01 0.7 0.03 0.83 0.03 1. 0.71]
[-0.02 0.51 0.02 0.72 0.03 0.82 0.03 0.71 1. ]]
相關性矩陣的左側和頂部都是相對的變量,從左到右、從上到下依次是列1到列9。從結果看出:
- 第5列和第7列相關性最高,系數達到0.91。
- 第4列和第6列相關性較高,系數達到0.88。
- 第8列和第6列相關性較高,系數達到0.83。
為了更好地展示相關性結果,我們可以配合Matplotlib展示圖像。代碼如下:
fig = plt.figure() # 調用figure建立一個繪圖對象
ax = fig.add_subplot(111) # 設定1個子網格并添加子網格對象
hot_img = ax.matshow(np.abs(correlation_matrix), vmin=0, vmax=1)
# 繪制熱力圖,值域從0到1
fig.colorbar(hot_img) # 為熱力圖生成顔色漸變條
ticks = np.arange(0, 9, 1) # 生成0~9,步長為1
ax.set_xticks(ticks) # 生成x軸刻度
ax.set_yticks(ticks) # 設定y軸刻度
names = ['x' + str(i) for i in range(x.shape[1])] # 生成坐标軸标簽文字
ax.set_xticklabels(names) # 生成x軸标簽
ax.set_yticklabels(names) # 生成y軸标簽
上述代碼的功能都已經在注釋中注明。有以下幾點需要注意:
- 由于相關性結果中看的是絕對值的大小,是以需要對correlation_matrix做取絕對值操作,其對應的值域會變為[0, 1]。
- 原始資料中由于沒有列标題,是以這裡使用清單推導式生成從x0到x8代表原始的9個特征。
展示結果如圖3-7所示。
從圖像中配合顔色可以看出:顔色越亮(彩色顔色為越黃),則相關性結果越高,是以從左上角到右下角呈現一條黃色斜線;而顔色較亮的第5列和第7列、第4列和第6列及第8列和第6列分别對應x4和x6、x3和x5、x7和x5。
上述過程中,主要需要考慮的關鍵點是:如何了解相關性和因果關系的差異,以及如何應用相關性。相關性分析除了可以用來分析不同變量間的相關伴生關系以外,也可以用來做多重共線性檢驗。有關共線性的問題請參照3.7節。
代碼實操小結:本節示例中,主要用了如下幾個知識點。
- 通過Numpy的loadtxt方法讀取文本資料檔案,并指定分隔符;
- 對Numpy矩陣做切塊處理;
- 使用Numpy中的corrcoef做相關性分析;
- 使用round方法保留2位小數;
- 使用np.abs取絕對值;
- 使用清單推導式生成新清單;
- 使用Matplotlib的熱力圖配合相關性結果做圖像展示。
3.8 标準化,讓營運資料落入相同的範圍
資料标準化是一個常用的資料預處理操作,目的是處理不同規模和量綱的資料,使其縮放到相同的資料區間和範圍,以減少規模、特征、分布差異等對模型的影響。除了用作模型計算,标準化後的資料還具有直接計算并生成複合名額的意義,是權重名額的必要步驟。
3.8.1 實作中心化和正态分布的Z-Score
Z-Score标準化是基于原始資料的均值和标準差進行的标準化,假設原轉換的資料為x,新資料為x′,那麼x'=(x-mean)/std,其中mean和std為x所在列的均值和标準差。
這種方法适合大多數類型的資料,也是很多工具的預設标準化方法。标準化之後的資料是以0為均值,方差為1的正态分布。但是Z-Score方法是一種中心化方法,會改變原有資料的分布結構,不适合對稀疏資料做處理。
3.8.2 實作歸一化的Max-Min
Max-Min标準化方法是對原始資料進行線性變換,假設原轉換的資料為x,新資料為x′,那麼x'=(x-min)/(max-min),其中min和max為x所在列的最小值和最大值。
這種标準化方法的應用非常廣泛,得到的資料會完全落入[0, 1]區間内(Z-Score則沒有類似區間)。這種方法能使資料歸一化而落到一定的區間内,同時還能較好地保持原有資料結構。
3.8.3 用于稀疏資料的MaxAbs
最大值絕對值标準化(MaxAbs)即根據最大值的絕對值進行标準化。假設原轉換的資料為x,新資料為x′,那麼x'=x/|max|,其中max為x所在列的最大值。
MaxAbs方法與Max-Min用法類似,也是将資料落入一定區間,但該方法的資料區間為[-1, 1]。MaxAbs也具有不破壞原有資料分布結構的特點,是以也可以用于稀疏資料、稀疏的CSR或CSC矩陣。
3.8.4 針對離群點的RobustScaler
某些情況下,假如資料集中有離群點,我們可以使用Z-Score進行标準化,但是标準化後的資料并不理想,因為異常點的特征往往在标準化之後容易失去離群特征。此時,可以使用RobustScaler針對離群點做标準化處理,該方法對資料中心化和資料的縮放魯棒性有更強的參數控制。
3.8.5 代碼實操:Python資料标準化處理
本示例中,将使用Numpy、sklearn進行标準化相關處理。源檔案data6.txt位于“附件-chapter3”中。完整代碼如下。
from sklearn import preprocessing
import matplotlib.pyplot as plt
示例中用到了Numpy做資料讀取,sklearn中的preprocessing子產品做标準化處理,matp-lotlib.pyplot子產品做可視化圖形展示。
第2部分使用Numpy的loadt方法導入資料檔案,檔案以tab分隔。
data = np.loadtxt('data6.txt', delimiter='t') # 讀取資料
第3部分進行Z-Score标準化。
zscore_scaler = preprocessing.StandardScaler() # 建立StandardScaler對象
data_scale_1 = zscore_scaler.fit_transform(data) # StandardScaler标準化處理
通過preprocessing.StandardScaler建立模型,然後應用fit_transform方法進行轉換。除了StandardScaler類外,還可直接使用preprocessing.scale(X)做标準化處理,二者的應用差别是preprocessing.StandardScaler方法既可以滿足一次性的标準化處理,又能将轉換資料集的特征儲存下來,分别通過fit和transform對多個資料集(例如測試集和訓練集)做相同規則的轉換。該方法針對訓練集、測試集和應用集分别應用時非常有效。
第4部分進行Max-Min标準化。
minmax_scaler = preprocessing.MinMaxScaler() # 建立MinMaxScaler模型對象
data_scale_2 = minmax_scaler.fit_transform(data) # MinMaxScaler标準化處理
通過preprocessing. MinMaxScaler建立模型,然後應用fit_transform方法進行轉換。其中的fit_transform方法可以分别使用fit然後使用transform做多資料集的應用。該方法也可以使用沒有面向對象API的等效函數sklearn.preprocessing.minmax_scale。
第5部分進行MaxAbsScaler标準化。
maxabsscaler_scaler = preprocessing.MaxAbsScaler() # 建立MaxAbsScaler對象
data_scale_3 = maxabsscaler_scaler.fit_transform(data) # MaxAbsScaler标準化處理
通過preprocessing. MaxAbsScaler建立模型,然後應用fit_transform方法進行轉換。也可以使用沒有面向對象API的等效函數sklearn.preprocessing. maxabs_scale。
第6部分進行RobustScaler标準化。
robustscalerr_scaler = preprocessing.RobustScaler() # 建立RobustScaler标準化對象
data_scale_4 = robustscalerr_scaler.fit_transform(data) # RobustScaler标準化處理
通過preprocessing. RobustScaler建立模型,然後應用fit_transform方法進行轉換。也可以使用沒有面向對象API的等效函數sklearn.preprocessing. robust_scale。
第7部分展示原始資料和4種标準化的結果。
data_list = [data, data_scale_1, data_scale_2, data_scale_3, data_scale_4]
# 建立資料集清單
color_list = ['black', 'green', 'blue', 'yellow', 'red'] # 建立顔色清單
merker_list = ['o', ',', '+', 's', 'p'] # 建立樣式清單
title_list = ['source data', 'zscore_scaler', 'minmax_scaler', 'maxabsscaler_scaler', 'robustscalerr_scaler'] # 建立标題清單
plt.figure(figsize=(16, 3))
for i, data_single in enumerate(data_list): # 循環得到索引和每個數值
plt.subplot(1, 5, i + 1) # 确定子網格
plt.scatter(data_single[:, :-1], data_single[:, -1], s=10, marker=merker_list[i], c=color_list[i]) # 自網格展示散點圖
plt.title(title_list[i]) # 設定自網格标題
plt.suptitle("raw data and standardized data") # 設定總标題
在該部分功能中,将原始資料以及通過上述4種方法得到的結果資料統一對比展示。主要步驟如下:
- 先建立4個清單,用于存儲原始資料和4個标準化後的資料、顔色、樣式、标題等。
- 通過for循環結合enumerate将索引值和4份資料循環讀出;通過plt.figure來設定圖像尺寸,通過plt.subplot為不同的資料設定不同的網格,該網格為1行5列;在每個網格中通過plt.scatter畫出散點圖,并通過索引将清單中對應的值作為參數傳給scatter;然後通過title方法為每個子網格設定标題。
- 最後設定整個圖像的總标題并展示圖像,如圖3-8所示。
上述過程中,主要需要考慮的關鍵點是如何根據不同的資料分布特征和應用選擇合适的标準化方式。具體來說包含如下幾點:
- 如果要做中心化處理,并且對資料分布有正态需求,那麼使用Z-Score方法;
- 如果要進行0-1标準化或将要指定标準化後的資料分布範圍,那麼使用Max-Min标準化或MaxAbs标準化方式是比較好的選擇,尤其是前者;
- 如果要對稀疏資料進行處理,Max-Min标準化或MaxAbs标準化仍然是理想方法;
- 如果要最大限度地保留資料集中的異常,那麼使用RobustScaler方法更佳。
- 使用sklearn.preprocessing的StandardScaler方法做Z-Score标準化處理;
- 使用sklearn.preprocessing的MinMaxScaler方法做Max-Min标準化處理;
- 使用sklearn.preprocessing的MaxAbsScaler方法做最大值絕對值标準化處理;
- 使用sklearn.preprocessing的RobustScaler方法做針對異常資料的處理;
- 通過matplotlib.pyplot畫圖,并在一幅大圖中設定使用subplot設定多個子網格,使用scatter畫出散點圖并設定顔色、樣式、大小,使用title和suptitle設定子網格和整體圖像标題展示圖像。
3.9 離散化,對營運資料做邏輯分層
所謂離散化,就是把無限空間中有限的個體映射到有限的空間中。資料離散化操作大多是針對連續資料進行的,處理之後的資料值域分布将從連續屬性變為離散屬性,這種屬性一般包含2個或2個以上的值域。離散化處理的必要性如下:
- 節約計算資源,提高計算效率。
- 算法模型(尤其是分類模型)的計算需要。雖然很多模型,例如決策樹可以支援輸入連續型資料,但是決策樹本身會先将連續型資料轉化為離散型資料,是以離散化轉換是一個必要步驟。
- 增強模型的穩定性和準确度。資料離散化之後,處于異常狀态的資料不會明顯地突出異常特征,而是會被劃分為一個子集中的一部分,是以異常資料對模型的影響會大大降低,尤其是基于距離計算的模型(例如K均值、協同過濾等)效果明顯。
- 特定資料處理和分析的必要步驟,尤其在圖像處理方面應用廣泛。大多數圖像做特征檢測(以及其他基于特征的分析)時,都需要先将圖像做二值化處理,二值化也是離散化的一種。
- 模型結果應用和部署的需要。如果原始資料的值域分布過多,或值域劃分不符合業務邏輯,那麼模型結果将很難被業務了解并應用。
3.9.1 針對時間資料的離散化
針對時間資料的離散化主要用于以時間為主要特征的資料集中和粒度轉換,離散化處理後将分散的時間特征轉換為更高層次的時間特征。
在帶有時間的資料集中,時間可能作為行記錄的序列,也可能作為列(次元)記錄資料特征。常見的針對時間資料的離散化操作有以下兩類:
- 針對一天中的時間離散化。一般是将時間戳轉換為秒、分鐘、小時或上下午。
- 針對日粒度以上資料的離散化。一般是将日期轉化為周數、周幾、月、工作日或休息日、季度、年等。
針對時間資料的離散化可以将細粒度的時間序列資料離散化為粗粒度的3類資料:
- 離散化為分類資料,例如上午、下午。
- 離散化為順序資料,例如周一、周二、周三等。
- 離散化為數值型資料,例如一年有52個周,周數是數值型資料。
3.9.2 針對多值離散資料的離散化
針對多值離散資料的離散化指的是要進行離散化處理的資料本身不是數值型資料,而是分類或順序資料。
例如,使用者收入變量的值原來可能劃分為10個區間,根據新的模組化需求,隻需要劃分為4個區間,那麼就需要對原來的10個區間進行合并。
多值離散資料要進行離散化還有可能是劃分的邏輯有問題,需要重新劃分。這種問題通常都是由于業務邏輯的變更,導緻在原始資料中存在不同曆史資料下的不同值域定義。
例如,使用者活躍度變量的值,原來分為高價值、中價值和低價值3個類别。根據業務發展的需要,新的使用者活躍度變量的值定義為:高價值、中價值、低價值和負價值。此時需要對不同類别的資料進行統一規則的離散化處理。
3.9.3 針對連續資料的離散化
針對連續資料的離散化是主要的離散化應用,在分類或關聯分析中應用尤其廣泛。這些算法的結果以類别或屬性辨別為基礎,而非數值标記。例如,分類規則的典型結果邏輯如下:
如果 變量1 = 值1 并且 變量2 = 值2
那麼 目标變量(T)
連續資料的離散化結果可以分為兩類:一類是将連續資料劃分為特定區間的集合,例如{(0,10], (10, 20], (20, 50], (50, 100]};一類是将連續資料劃分為特定類,例如類1、類2、類3。
常見實作針對連續資料化離散化的方法如下。
- 分位數法:使用四分位、五分位、十分位等分位數進行離散化處理,這種方法簡單易行。
- 距離區間法:可使用等距區間或自定義區間的方式進行離散化,這種操作更加靈活且能滿足自定義需求。另外該方法(尤其是等距區間)可以較好地保持資料原有的分布。
- 頻率區間法:将資料按照不同資料的頻率分布進行排序,然後按照等頻率或指定頻率離散化,這種方法會把資料變換成均勻分布。好處是各區間的觀察值是相同的,不足是已經改變了原有資料的分布狀态。
- 聚類法:例如使用K均值将樣本集分為多個離散化的簇。
- 卡方:通過使用基于卡方的離散化方法,找出資料的最佳臨近區間并合并,形成較大的區間。
3.9.4 針對連續資料的二值化
在很多場景下,我們可能需要将變量特征進行二值化操作:每個資料點跟門檻值比較,大于門檻值設定為某一固定值(例如1),小于門檻值設定為某一固定值(例如0),然後得到一個隻擁有兩個值域的二值化資料集。
二值化應用的前提是資料集中所有的屬性值所代表的含義相同或類似,例如讀取圖像所獲得的資料集是顔色值的集合(具體顔色模式取決于讀取圖像時的模式設定,例如灰階、RGB等),是以每一個資料點都代表顔色,此時可對整體資料集做二值化處理。某些情況下,也可能隻針對特定列做二值化,這樣不同列的屬性雖然不同,但同一列内産生的二值化結果卻仍然具有比較和分類意義。
3.9.5 代碼實操:Python資料離散化處理
本示例中,将使用Pandas、sklearn進行離散化相關處理。資料源檔案data7.txt位于“附件-chapter3”中。
示例代碼分為6個部分。
from sklearn.cluster import KMeans
代碼中用到了Pandas和sklearn。前者主要用來做檔案讀取、切塊、時間處理、關聯合并和部分離散化操作;後者主要用來做二值化和聚類模組化離散化。
第2部分使用Pandas的read_table方法讀取資料檔案,并指定列名。
df = pd.read_table('data7.txt', names=['id', 'amount', 'income', 'datetime', 'age']) # 讀取資料檔案
print(df.head(5)) # 列印輸出前5條資料
資料集為100行5列的資料框,包含id、amount、income、datetime和age五個字段。原始資料前5條資料如下:
id amount income datetime age
0 15093 1390 10.40 2017-04-30 19:24:13 0-10
1 15062 4024 4.68 2017-04-27 22:44:59 70-80
2 15028 6359 3.84 2017-04-27 10:07:55 40-50
3 15012 7759 3.70 2017-04-04 07:28:18 30-40
4 15021 331 4.25 2017-04-08 11:14:00 70-80
第3部分針對時間資料的離散化。
df['datetime'] = list(map(pd.to_datetime,df['datetime'])) # 将時間轉換為datetime格式
df['datetime'] = [i.weekday() for i in df['datetime']] # 離散化為周幾
print(df.head(5)) # 列印輸出前5條資料
該過程中,先直接使用map配合pandas的to_datetime方法将字元串轉換為datetime格式,由于Python 3的map方法傳回的是一個map對象,需要用list方法做轉換才能正确處理;然後清單推導式配合weekday方法擷取周幾,新擷取的周幾的資料直接替換原資料框的時間戳。最後列印輸出離散化後的結果。
id amount income datetime age
0 15093 1390 10.40 6 0-10
1 15062 4024 4.68 3 70-80
2 15028 6359 3.84 3 40-50
3 15012 7759 3.70 1 30-40
4 15021 331 4.25 5 70-80
從結果中看到,datetime列的值由原來的日期時間格式,轉換為由0~6組成的周幾值,0代表周一,6代表周日。
第4部分針對多值離散資料的離散化。
map_df = pd.DataFrame(
[['0-10', '0-40'], ['10-20', '0-40'], ['20-30', '0-40'], ['30-40', '0-40'], ['40-50', '40-80'], ['50-60', '40-80'], ['60-70', '40-80'], ['70-80', '40-80'], ['80-90', '>80'], ['>90', '>80']],
columns=['age', 'age2']) # 定義一個要轉換的新區間
df_tmp = df.merge(map_df, left_on='age', right_on='age', how='inner')
# 資料框關聯比對
df = df_tmp.drop('age', 1) # 丢棄名為age的列
print(df.head(5)) # 列印輸出前5條資料
該過程中先通過dp.Dataframe定義一個新的轉換區間,用來将原資料映射到新區間來;然後通過merge方法将原資料框和新定義的資料框進行關聯,關聯的兩個key(left_on和right_on)分别是age和age2,關聯模式為inner(内關聯);接着我們通過drop方法去除原始資料框中的age列,隻保留新的轉換後的區間。得到如下結果:
id amount income datetime age2
0 15093 1390 10.40 6 0-40
1 15064 7952 4.40 0 0-40
2 15080 503 5.72 5 0-40
3 15068 1668 3.19 5 0-40
4 15019 6710 3.20 0 0-40
上述傳回結果中,age2列是轉換後新的列,原來的分類區間已經被映射到新的類别區間。
第5部分針對連續資料的離散化。該部分包含3種常用方法。
方法1:自定義分箱區間實作離散化。
bins = [0, 200, 1000, 5000, 10000] # 自定義區間邊界
df['amount1'] = pd.cut(df['amount'], bins) # 使用邊界做離散化
print(df.head(5)) # 列印輸出前5條資料
首先定義一個自定義區間邊界清單,用來将資料做劃分;然後使用Pandas的cut方法做離散化;并将結果生成一個新的名為amount1的列追加到原資料框。列印輸出結果如下:
id amount income datetime age2 amount1
0 15093 1390 10.40 6 0-40 (1000, 5000]
1 15064 7952 4.40 0 0-40 (5000, 10000]
2 15080 503 5.72 5 0-40 (200, 1000]
3 15068 1668 3.19 5 0-40 (1000, 5000]
4 15019 6710 3.20 0 0-40 (5000, 10000]
上述傳回結果中,amount1列是轉換後産生的列,每行對應的區間左側是區間開始值(不包含),右側是區間結束值(包含)。除了顯示區間外,cut方法還可以通過自定義labels(值為清單的形式,用來表示不同分類區間的标簽)用标簽代替上述區間,例如labels = ['bad','medium','good','awesome'],那麼顯示在amount1裡面的資料就是對應lables裡面的字元串。這裡沒有将原始amount列删除,原因是下面的方法中還會用到該列原始資料。
方法2:使用聚類法實作離散化。
data = df['amount'] # 擷取要聚類的資料,名為amount的列
data_reshape = data.values.reshape((data.shape[0], 1)) # 轉換資料形狀
model_kmeans = KMeans(n_clusters=4, random_state=0) # 建立KMeans模型并指定要聚類數量
keames_result = model_kmeans.fit_predict(data_reshape) # 模組化聚類
df['amount2'] = keames_result # 新離散化的資料合并到原資料框
print(df.head(5)) # 列印輸出前5條資料
該過程使用了sklearn.cluster的KMeans算法實作。首先通過指定資料框的列名獲得要模組化的資料列;然後将資料的形狀進行轉換,否則算法會認為該資料隻有1行(通過Pandas指定列名獲得的資料預設都沒有列值,例如示例中的data的形狀是(100,),是以大多數場景下作為輸入變量都需要做形狀轉換);接着建立KMeans模型并指定要聚類數量為4表,并設定初始化随機種子為固定值0(否則每次得到的聚類結果很可能都不一樣),并使用fit_predict方法直接模組化輸出結果;最後将新的結果追加到原始資料框中。最終列印輸出前5條結果如下:
id amount income datetime age2 amount1 amount2
0 15093 1390 10.40 6 0-40 (1000, 5000] 2
1 15064 7952 4.40 0 0-40 (5000, 10000] 1
2 15080 503 5.72 5 0-40 (200, 1000] 2
3 15068 1668 3.19 5 0-40 (1000, 5000] 2
4 15019 6710 3.20 0 0-40 (5000, 10000] 1
上述傳回結果中,amount2列是轉換後産生的列,列的值域是0、1、2,代表3類資料。
方法3:使用4分位數實作離散化。
df['amount3'] = pd.qcut(df['amount'], 4, labels=['bad', 'medium', 'good', 'awesome']) # 按四分位數進行分隔
df = df.drop('amount', 1) # 丢棄名為amount的列
print(df.head(5)) # 列印輸出前5條資料
該過程中,使用了Pandas的qcut方法指定做4分位數分隔,同時設定不同四分位得到的區間的标簽分别為['bad','medium','good','awesome']);将得到的結果以列名為amount3追加到原始資料框中;然後通過drop方法丢棄名為amount的列。列印輸出結果如下:
id income datetime age2 amount1 amount2 amount3
0 15093 10.40 6 0-40 (1000, 5000] 2 bad
1 15064 4.40 0 0-40 (5000, 10000] 1 awesome
2 15080 5.72 5 0-40 (200, 1000] 2 bad
3 15068 3.19 5 0-40 (1000, 5000] 2 bad
4 15019 3.20 0 0-40 (5000, 10000] 1 awesome
上述傳回結果中,amount3列是轉換後産生的列,其結果構成與方法1完全相同,差異僅在于區間邊界不同。
第6部分做特征二值化處理。
binarizer_scaler = preprocessing.Binarizer(threshold=df['income'].mean())
# 建立Binarizer模型對象
income_tmp = binarizer_scaler.fit_transform(df[['income']]) # Binarizer标準化轉換
income_tmp.resize(df['income'].shape) # 轉換資料形狀
df['income'] = income_tmp # Binarizer标準化轉換
print(df.head(5)) # 列印輸出前5條資料
先建立Binarizer模型對象,然後使用fit_transform方法進行二值化轉換,門檻值設定為該列的均值;轉換後得到的資料的形狀是(1, 100),通過resize更改為與原始資料框income列相同的尺寸,并将結果直接替換原始列的值。最後列印輸出前5條資料結果如下:
id income datetime age2 amount1 amount2 amount3
0 15093 1.0 6 0-40 (1000, 5000] 2 bad
1 15064 1.0 0 0-40 (5000, 10000] 1 awesome
2 15080 1.0 5 0-40 (200, 1000] 2 bad
3 15068 0.0 5 0-40 (1000, 5000] 2 bad
4 15019 0.0 0 0-40 (5000, 10000] 1 awesome
上述傳回結果中,income列的值已經離散為由0和1組成的二值化資料。
上述過程中,主要需要考慮的關鍵點是:如何根據不同的資料特點和模組化需求選擇最合适的離散化方式,因為離散化方式是否合理會直接影響後續資料模組化和應用效果。
除了本節介紹的相關類型的轉換離散化制約外,不同模型對于離散化的限制如下:
- 使用決策樹時往往傾向于少量的離散化區間,原因是過多的離散化将使得規則過多受到碎片區間的影響。
- 關聯規則需要對所有特征一起離散化,原因是關聯規則關注的是所有特征的關聯關系,如果對每個列單獨離散化将失去整體規則性。
- 通過Pandas的read_table方法讀取文本資料檔案,并指定列名;
- 通過Pandas的to_datetime方法将字元串轉換為datetime格式,使用weekday提取周幾的資料;
- 使用map函數做批量資料處理;
- 使用Pandas的head方法隻展示前n條資料;
- 通過Pandas的merge方法合并多個資料框,實作類似SQL的資料關聯查詢;
- 使用Pandas的drop方法丢棄特定資料列;
- 使用Pandas的cut和qcut方法實作基于自定義區間和分位數方法的資料離散化;
- 使用sklearn.cluster的KMeans方法實作聚類分析;
- 使用shape方法擷取矩陣形狀,并使用resize方法對矩陣實作形狀轉換;
- 使用sklearn.preprocessing的Binarizer方法做二值化處理。
3.10 内容延伸:非結構化資料的預處理
3.10.1 網頁資料解析
本節通過一個稍微複雜一點的示例,來示範如何抓取并解析網頁資料。之是以說複雜,是因為本節中會出現幾個本書中未曾提及的知識和方法,從代碼數量來看也會比之前的示例稍微長一點。
本示例中,将使用requests、bs4、re、time、pandas庫進行網頁資料讀取、解析和相關處理。
由于網頁抓取需要對頁面本身做分析,而筆者撰寫時的網頁環境很可能與讀者應用時不一緻,導緻代碼不可用。是以,本節将使用筆者自己的網站做抓取和解析應用,該網站的結構不會發生變化。
網頁抓取中,輿情分析是常見應用場景,例如在特定論壇抓取使用者的發帖資料,然後做使用者評價傾向分析(喜歡、不喜歡還是中立),提取使用者喜好特征,使用者對品牌商品等的印象關鍵字等。本節的目标是抓取目标網頁的發帖的标題、時間、文章分類以及關鍵字。
在抓取和解析網頁資料之前,首先要做的是網頁内容分析。
- 要抓取的内容格式:文本、圖像或其他檔案。
- 是否存在重定向:重定向往往根據User-Agent來判斷,例如手機端、台式計算機端看到的頁面資訊不同。
- 是否需要驗證:很多網頁的爬蟲都需要使用者登入、驗證碼等。
- 目标資料是否具有統一标簽規則:要抓取的資料是否具有統一的HTML标簽,便于後期處理。
- URL規則:大多數情況下網頁抓取都不隻有一個頁面,而是多個頁面,是以需要了解不同頁面的URL規則,尤其是帶有條件查詢的,需要了解具體參數。
- 業務常識性分析:根據實際要抓取的資料,分析可能會産生哪些字段,會有哪些沖突和包含關系以及關聯性影響等。
進入筆者網站
http://www.dataivy.cn/,調出開發者工具,例如Chrome,按快捷鍵F12打開。打開開發者工具(或者單擊右上角
,在彈出的菜單中選擇“更多工具–開發者工具”),單擊
切換到檢視頁面元素視圖。滑鼠單擊開發者工具欄左側的
通過單擊頁面上的标題、釋出時間、分類、标簽等内容發現如下規律:
- 所有的文章都以article标簽開始,是以一個article内是一篇文章,如圖3-9所示。
- 釋出時間、分類、标簽都分别在span标簽下,并通過不同的class可以區分,如圖3-10所示。
上面的定位還是有點泛,需要再繼續拆分:
- 釋出時間位于span标簽裡面time标簽中,如圖3-11所示。
- 分類内容位于a标簽中,如圖3-12所示。
- 上面兩個值都可以直接從tag标簽的值中取出,但是tag标簽卻存在多标簽的情況,是以它是一個清單,内容位于内層,如圖3-13所示。
URL規則分析:
由于我們要抓取所有頁面,是以需要找到不同頁面的URL特點,這樣在構造URL的時候就能通過程式實作。我們點選下一頁面以及不同頁面,發現URL的基本特征是
http://www.dataivy.cn/page/2/,其中page後面通過不同的數字代表頁面的序号值。
到此為止我們基本确定了抓取思路:先構造一個URL,然後獲得所有的article,在每個article中獲得對應的标簽并解析出目标值。
在本節中,我将使用面向對象的程式設計方式。
這兩類程式設計思想都是常用的程式設計思路,本例将通過面向對象的程式設計來實作網頁解析。清楚了上述邏輯後,我們開始編寫代碼。完整代碼如下。
第1部分導入庫,具體用途在注釋中已經注明。
import requests # 用于發出HTML請求
from bs4 import BeautifulSoup # 用于HTML格式化處理
import re # 用于解析HTML配合查找條件
import time # 用于檔案名儲存
import pandas as pd # 格式化資料
第2部分,我們通過class函數來建立名為WebParse的對象,後續的所有應用都将基于該對象建立的執行個體使用。
class WebParse:
# 初始化對象
def __init__(self, headers):
self.headers = headers
self.article_list = []
self.home_page = 'http://www.dataivy.cn/'
self.nav_page = 'http://www.dataivy.cn/page/{0}/'
self.art_title = None
self.art_time = None
self.art_cat = None
self.art_tags = None
# 擷取頁面數量
def get_max_page_number(self):
res = requests.get(self.home_page, headers=self.headers) # 發送請求
html = res.text # 獲得請求中的傳回文本資訊
html_soup = BeautifulSoup(html, "html.parser") # 建立soup對象
page_num_code = html_soup.findAll('a', attrs={"class": "page-numbers"})
num_sets = [re.findall(r'(\d+)', i.text)
for i in page_num_code] # 獲得頁面字元串類别
num_int = [int(i[0]) for i in num_sets if len(i) > 0] # 獲得數值頁碼
return max(num_int) # 最大頁碼
# 獲得文章清單
def find_all_articles(self, i):
url = self.nav_page.format(i)
res = requests.get(url, headers=headers) # 發送請求
html = res.text # 獲得請求中的傳回文本資訊
html_soup = BeautifulSoup(html, "html.parser") # 建立soup對象,用于處理HTML
self.article_list = html_soup.findAll('article')
# 解析單文章
def parse_single_article(self, article):
self.art_title = article.find('h2', attrs={"class": "entry-title"}).text
self.art_time = article.find('time', attrs={"class": {"entry-date published", "entry-date published updated"}}).text
self.art_cat = article.find('a', attrs={"rel": "category tag"}).text
tags_code = article.find('span', attrs={"class": "tags-links"})
self.art_tags = '' if tags_code is None else WebParse._parse_tags(self, tags_code)
# 内部用解析tag函數
def _parse_tags(self, tags_code):
tag_strs = ''
for i in tags_code.findAll('a'):
tag_strs = tag_strs + '/' + i.text
return tag_strs
# 格式化資料
def format_data(self):
return self.art_title, self.art_time, self.art_cat, self.art_tags
在對象定義中,每個def都是一個功能子產品。
首先要做的是初始化對象屬性,我們通過__init__來實作。__init__的意思是初始化對象,所有的WebParse的屬性都以self為開頭,它們支援通過不同的方式傳值。
- 固定值,例如self.home_page,代表了我們要抓取的初值目标頁面。
- 通過字元串占位符占位,後續再通過format傳值,例如self.nav_page,該屬性是所有帶有頁面導航頁碼的URL位址。
- 空值,例如self.art_title、self.art_time、self.art_cat、self.art_tags和self.article_list,前4個用于存儲每篇文章的标題、釋出時間、類别和标簽,第5個值是文章清單。這些值後續會在不同的function中更新,并用于其他方法。
- 外部傳值,例如self.headers,代表的是頁面headers,用于request請求時使用,在初始化的時候傳入。
第2個get_max_page_number子產品的作用是,擷取頁面數量,即有多少個文章頁面。具體實作過程如下:
- 用requests.get方法送出請求并傳回對象res,一般情況下網頁請求多用get和post方法,具體方法以網頁的請求方式為準,具體可通過http工具檢視。為了防止被屏蔽,我們通過傳入headers資訊,來模拟使用者通路情況。
- 用res.text傳回資訊中的文本html,除了text文本外,res傳回的資訊還包括status_code、cookies、encoding、headers等多種資訊,這些資訊可以用來判斷資訊傳回狀态等其他用途。
- 用BeautifulSoup建立針對html的解析對象html_soup,解析中指定的解析器為html.parser,這是預設的解析器,在後面的文章中我們還會用到其他自定義解析器。
- 從html_soup中使用findAll方法找到所有的a标簽,并且其class為page-numbers,這些标簽都是文章底部的導航功能。
- 在num_sets的實作中我們用到了清單推導式,配合正規表達式re的findall方法,來找到所有的頁面代碼中的數字字元串。注意這裡傳回的仍然是字元串,而不是頁碼數字本身。
- num_int的實作也是基于清單推導式,基本邏輯是從上面的num_sets中擷取每個元素,如果元素>0,即元素有數字字元串,那麼将其數字取出來并轉換為int類型。這樣,我們就得到了由頁碼數字組成的清單。
- 最後的return傳回的是清單中的最大值,通過max實作。後續應用時,會通過一個對象來承接該值。
回顧上面的過程,基本邏輯是,送出請求→獲得傳回文本→建立bs解析對象→找到符合特定條件的标簽→解析标簽中的目标元素值,該步驟可能要通過多個功能組合實作。下面所有的請求和解析都是同樣的邏輯。
第3個find_all_articles的目标是獲得所有的文章清單,作為下面解析用的清單。該子產品的實作與get_max_page_number基本一緻,其中主要差異點如下:
- 在方法的參數中,self用來獲得初始化時的所有屬性,i則可以通過外部傳值。
- 最後我們并沒有傳回對象,而是通過self.article_list = html_soup.findAll('article')的方式,對初始化的self.article_list做更新,更新後,當對象的其他方法在使用該對象時,則會用到其最新的值,而不是初始值。
第4個parse_single_article的目标是解析單文章裡面的不同元素,包括文章、釋出時間、分類和标簽。
這段方法中,傳入的article是一個bs建立的對象,而不是一個字元串,是以我們直接用find方法找到在本節開始時确定的每個元素的規則。但在查找tag标簽時,我們發現某些文章沒有标簽,會導緻無法解析出文本(也就是目标标簽值),是以這裡用了一個條件表達式來實作:如果tags_code的值為空,那麼self.art_tags的值為'',否則調用解析tag标簽的功能_parse_tags。
第5個_parse_tags的目标是解析tags并傳回字元串型的tag清單。tags_code應用findAll(而不是find)方法找到所有的tag清單,然後将清單中的文字組合為新的字元串,以“/”分割。
第6個format_data的目标是傳回組合後的格式化清單格式的資料,功能簡單。
第3部分是主程式應用。
if __name__ == '__main__':
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) Apple-WebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36'}
\# 定義頭資訊
data_cols = ['title', 'time', 'cat', 'tags']
app = WebParse(headers)
max_num = app.get_max_page_number()
data_list = []
for ind in range(max_num):
app.find_all_articles(ind + 1) # ind從0開始,是以要加1
for article in app.article_list:
app.parse_single_article(article)
data_list.append(app.format_data())
data_pd = pd.DataFrame(data_list,columns=data_cols)
print(data_pd.head(2))
if__name__ == '__main__'中__name__是目前子產品名,當子產品被直接運作時子產品名為__main__,這段代碼的意思就是,當子產品被直接運作時(例如通過Python直接執行或在編譯器中執行),以下代碼塊将被運作;當子產品是被導入時(例如使用import導入該子產品時),代碼塊不被運作。接下來分别獲得總頁面數量,讀取每個頁面的标題和價格并寫入檔案。
- headers定義了公用的http請求的頭資訊。
- data_cols定義了後續資料框的頭資訊。
- app = WebParse(headers)的作用是将對象初始化為執行個體,執行個體名為app,後續就用app來代替WebParse,包括調用其屬性和方法。
- app.get_max_page_number()的作用是傳回頁碼最大值,并指派給max_num。在這裡我們看到,app.get_max_page_number()方法本身可直接從app執行個體中調取使用。
- 下面的循環代碼段實作了針對每篇文章的解析。這裡用到了兩層for循環,外層循環用來周遊所有頁面,内層循環用來找到所有的文章。文章解析完成之後傳到data_list,再基于data_list建立資料框,并列印輸出。
上述代碼執行後傳回結果如下:
大多數情況下,通過網絡爬蟲擷取資料都是輔助方式,原因是現在基本上所有的網站都有防爬蟲的意識和方式,這導緻資料爬取因受到外部很多因素的影響而導緻資料品質低下。基于爬蟲的主要工作内容包括輿情監測、市場口碑、使用者情緒、市場營銷等方面,屬于外部屬性較強的“附加”工作。這些工作其實都不是公司的核心營運内容,這就會導緻這些工作看似有趣并且有價值,但真正對企業來講價值很難實際展現。
上述過程中,主要需要考慮的關鍵點是:如何根據不同網頁的實際特點,尤其是對于反爬蟲的應對來正确讀取到網頁源代碼,讀取後源代碼的解析往往不是主要問題。
本節示例中,主要用了幾個知識點:
- 通過requests庫發送帶有自定義head資訊的網絡請求;
- 通過requests傳回對象的text方法擷取源代碼文本資訊;
- 使用bs4的BeautifulSoup庫配合find和findAll方法進行目标标簽查找和解析,并通過其text方法獲得标簽文本資訊;
- 通過re的正規表達式功能,實作對于特定數字規律的查找;
- 通過定義function函數來實作特定功能或傳回特定結果;
- 通過定義對象來實作特定功能;
- 通過for循環讀取資料清單;
- 通過if條件表達式指派;
- 對文本檔案的讀寫操作;
- 使用time的localtime、time、strftime方法進行日期擷取及格式化操作。
3.10.2 網絡使用者日志解析
網絡使用者日志屬于非結構化資料的一種,其解析方法根據配合的伺服器和跟蹤實施的不同而需要自定義子產品。本節将用一個示例來示範如何進行日志解析。
本示例中,将使用正規表達式配合自定義函數子產品實作日志解析功能。資料源檔案traffic_log_for_dataivy位于“附件-chapter3”中,預設工作目錄為“附件-chapter3”。
背景:我們在這個網站上部署了Google Analytics的代碼用來監測使用者的行為,由于該工具是SAAS工作模式,資料采集之後直接發送到Google雲端伺服器。我們通過一定的方式将每次發送給谷歌的資料同時“備份”了一條儲存為本地伺服器日志。其中日志請求内容的部分以
開頭便是日志記錄。我們的目标是找到這些日志,然後對日志做初步的解析,并儲存為本地檔案,便于後期做進一步資料解析和應用。
要實作這一目标,基本思路是:先從日志檔案夾中讀取日志檔案清單(本示例中僅有1個檔案,是以省略該步驟);然後依次讀取每個日志檔案中的資料;接着對日志檔案中的每條資料進行判斷,符合條件才能成為我們要的目标資料;最後将資料做初步解析并寫到檔案裡面。
本節我們依然以函數的方式來撰寫各個功能子產品,如下是完整代碼。
import gzip
import re
import time
- 在之前的讀寫檔案中,我們都是直接基于原始資料做操作,此次我們直接對gzip壓縮檔案操作。Python預設提供了gzip庫,可用于讀寫gz壓縮封包件。
- re庫用來做正規表達式比對。讀者可以發現,凡是涉及文本類内容處理的基本上都離不開該庫。
- time庫用來給檔案命名。
- pandas用來格式化輸出檔案。
第2部分判斷日志中是否為爬蟲日志。在日志中,很多搜尋引擎公司的網絡爬蟲在抓取頁面時會産生日志資料,除了谷歌外,百度、搜狗、雅虎、有道、必應等都有類似的爬蟲(或者稱為機器人)。代碼如下:
def is_spider(log_record,spiders):
detect_result = [True if log_record.find(spider) == -1 else False for spider in spiders]
is_exist = True if all(detect_result) else False
return is_exist
代碼中我們先使用了一個清單推導式,目的是檢測目前log日志中是否存在spider元素,如果不存在則為False,否則為True;接着通過條件指派的方式來判斷,如果全部為True(沒有任何一個spider元素),則為True,否則為False。
第3部分判斷是否為UA記錄。
def is_ua_record(log_record):
is_ua = True if log_record.find() != -1 else False
return is_ua
這裡的實作是基于條件表達式的,如果日志記錄中包含“GET /__ua.gif?”則為UA記錄,傳回True,否則為False。
第4部分解析每條日志資料。
該子產品定義了日志下所有字段的分割規則,首先我們針對日志記錄中要解析的每個資料字段定義了一個正規表達式構成的規則集,以用于目标資料的解析;接着将分散的規則通過re庫的compile方法合成一個比對模式;然後基于比對模式使用match方法比對每條資料記錄,并由此傳回比對的字段值清單,最後解析出所有定義的字段值并傳回給其他函數使用。
第5部分讀取日志資料。
def get_ua_data(file,spiders):
ua_data = []
with gzip.open(file, 'rt') as fn: # 打開要讀取的日志檔案對象
content = fn.readlines() # 以清單形式讀取日志資料
for single_log in content: # 循環判斷每天記錄
rule1 = is_spider(single_log,spiders)
rule2 = is_ua_record(single_log)
if rule1 and rule2: # 如果同時符合2條規則,則執行
ua_data.append(split_ua_data(single_log))
ua_pd = pd.DataFrame(ua_data)
return ua_pd
這裡涉及基于with方法的上下文管理,先通過gzip.open方法以文本隻讀模式打開日志檔案,然後通過readlines方法以清單的形式讀取日志記錄,讀取完成之後關閉日志檔案對象。
通過一個for循環将清單中的每條日志讀取出來,然後定義了兩條規則用于判斷日志是否符合條件。兩個條件同時滿足時,解析該條日志并将結果追加到清單ua_data。最後基于ua_data形成新的資料框并傳回。
第6部分是主要程式子產品。這部分功能的意義是将前文提到的功能整合到該子產品中。
#主程式
if name == '__main__':
file = 'dataivy.cn-Feb-2018.gz' # 定義原始日志的檔案名
ua_data = split_ua_data(file) # 讀取非結構化文本資料
spiders = [
'AhrefsBot',
'archive.org_bot',
'baiduspider',
'Baiduspider',
'bingbot',
'DeuSu',
'DotBot',
'Googlebot',
'iaskspider',
'MJ12bot',
'msnbot',
'Slurp',
'Sogou web spider',
'Sogou Push Spider',
'SputnikBot',
'Yahoo! Slurp China',
'Yahoo! Slurp',
'YisouSpider',
'YodaoBot',
'bot.html'
]
ua_pd = get_ua_data(file,spiders)
ua_pd.columns = ['ip_add','requet_time','request_info','status','bytes_info','referral','ua']
print(ua_pd.head(2))
output_file = 'ua_result_{0}.xlsx'.format(time.strftime('%Y%m%d%H%M%S',time.localtime(time.time())))
ua_pd.to_excel(output_file, index=False)
整個代碼實作功能如下:
- file定義了要讀取的gzip壓縮封包件。
- Spiders定義的是搜尋引擎關鍵字清單,讀者可根據自己需要增加。
- 使用get_ua_data對日志檔案做解析,并獲得資料框ua_pd。
- ua_pd.columns代碼段實作了對資料框重命名,這樣便于識别不同字段的含義。
- output_file為輸入的檔案名,這裡定義了一個Excel檔案,檔案名中通過時間戳的方式增加字尾,便于識别。字尾名使用time.strftime方法,含義是擷取目前執行的時間戳,并按照年月日時分秒的格式轉換為字元串。
- ua_pd.to_excel方法實作了導出為Excel檔案,設定index=False的意思是不導出index值。
代碼執行後,會在目前執行目錄下生成一個Excel結果檔案,如圖3-14所示。
總結:上述過程看似略顯複雜,但每個函數功能比較簡單、易懂且邏輯清晰。當然,後續應用可能需要再做二次解析,例如從reqeust_info中解析出URL位址,從request_time中解析出時區、年月日,基于IP位址映射出不同城市、省份等,後續需要做很多應用适配。
上述過程中,主要需要考慮的關鍵點如下:
- 如何根據不同的伺服器日志配置以及前端代碼跟蹤實施的具體情況,編寫日志過濾規則。
- 有關爬蟲資料的排除也是需要額外注意的資訊點。
本節示例中,主要用了如下幾個知識點:
- 使用gzip庫讀取壓縮封包件;
- 通過find方法查找符合條件的字元串;
- 通過if條件表達式實作指派;
- 通過re的正規表達式功能,實作對于特定字段的查找和比對;
- 将Pandas資料框導出為Excel;
- 基于time.strftime将特定時間轉換為字元串。
3.10.3 圖像的基本預處理
本示例中,将使用OpenCV來做圖像基本預處理操作,基本處理内容包括圖像縮放、平移、旋轉、透視變換、圖像色彩模式轉換、邊緣檢測、二值化操作、平滑處理、形态學處理。
資料源檔案sudoku.png、j.png位于“附件-chapter3”中。
代碼分為12個部分,涵蓋了日常圖像處理的常用操作。
第1部分為導入庫,本代碼中除了OpenCV庫外,還有Numpy用于定義圖像處理的核心、Matplotlib用于展示多圖圖像。
import cv2 # 導入圖像處理庫
import numpy as np # 導入Numpy庫
from matplotlib import pyplot as plt # 導入展示庫
第2部分定義了一個函數,用來做單圖像展示。
def img_show(img_name, img):
cv2.imshow(img_name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
下面的每個功能子產品,當隻有一個圖像做展示時會直接調用該子產品,而無須重複寫展示功能代碼。cv2.show()方法必須與cv2.waitKey()、cv2.destroyAllWindows()一起使用,才能保證圖像正常展示及關閉功能。
第3部分讀取原始圖像并展示。
img_file = 'sudoku.png' # 定義原始資料檔案
img = cv2.imread(img_file) # 以彩色模式讀取圖像檔案
rows, cols, ch = img.shape # 擷取圖像形狀
img_show('raw img', img) # 展示彩色圖像
通過cv2的imread方法以彩色模式讀取圖像,然後獲得彩色圖像的長、高和通道形狀,最終調用img_show做圖像展示,結果如圖3-15中①所示。
第4部分圖像縮放處理。
img_scale = cv2.resize(img, None, fx=0.6, fy=0.6, interpolation=cv2.INTER_CUBIC) # 圖像縮放
img_show('scale img', img_scale) # 展示縮放後的圖像
直接使用cv2.resize方法設定縮放比例、方法等,并展示輸出為原圖像60%的新圖像,結果如圖3-15中②所示。
第5部分圖像平移處理。
M = np.float32([[1, 0, 100], [0, 1, 50]]) # 定義平移中心
img_transform = cv2.warpAffine(img, M, (cols, rows)) # 平移圖像
img_show('transform img', img_transform) # 展示平移後的圖像
先定義圖像平移中心,然後使用cv2.warpAffine方法根據平移中心移動圖像,移動後的圖像如圖3-15中③所示。
第6部分圖像旋轉處理。
M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 45, 0.6) # 定義旋轉中心
img_rotation = cv2.warpAffine(img, M, (cols, rows))
# 第1個參數為旋轉中心,第2個參數為旋轉角度,第3個參數為旋轉後的縮放因子
img_show('rotation img', img_rotation) # 展示旋轉後的圖像
與圖像平移類似,先定義旋轉中心,然後使用cv2.warpAffine進行旋轉,同時設定旋轉角度為45度、縮放因子為0.6,結果如圖3-15中④所示。
第7部分透視變換處理。
pts1 = np.float32([[76, 89], [490, 74], [37, 515], [520, 522]]) # 定義變換前的4個校準點
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]]) # 定義變換後的4個角點
M = cv2.getPerspectiveTransform(pts1, pts2) # 定義變換中心點
img_perspective = cv2.warpPerspective(img, M, (300, 300)) # 透視變換
img_show('perspective img', img_perspective) # 展示透視變換後的圖像
先定義變換前的4個校準點,然後定義變換後的4個角點,可用來控制圖像大小。接着定義變換中心點,并應用cv2.warpPerspective進行透視變換,結果如圖3-15中⑤所示。
第8部分轉換為灰階圖像。
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 圖像轉灰階
img_show('gray img', img_gray) # 展示灰階圖像
使用cv2.cvtColor将BRG模式轉為GRAY模式,結果如圖3-15中⑥所示。
第9部分邊緣檢測處理。
img_edges = cv2.Canny(img, 100, 200) # 檢測圖像邊緣
img_show('edges img', img_edges) # 展示圖像邊緣
使用cv2.Canny檢測圖像邊緣,結果如圖3-16所示。
第10部分圖像二值化處理。
ret, th1 = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY) # 簡單門檻值
th2 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2) # 自适應均值門檻值
th3 = cv2.adaptiveThreshold(img_gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 自适應高斯門檻值
titles = ['Gray Image', 'Global Thresholding (v = 127)', 'Adaptive Mean Thresholding', 'Adaptive Gaussian Thresholding'] # 定義圖像标題
images = [img_gray, th1, th2, th3] # 定義圖像集
for i in range(4):
plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray') # 以灰階模式展示每個子網格的圖像
plt.title(titles[i]) # 設定每個子網格标題
plt.xticks([]), plt.yticks([]) # 設定x軸和y軸标題
這裡分别應用了簡單門檻值、自适應均值門檻值、自适應高斯門檻值3種方法做二值化處理,并使用Matplotlib做多網格圖像,同時展示原始圖像和3種門檻值下二值化圖像處理結果,如圖3-17所示。
第11部分圖像平滑處理。
kernel = np.ones((5, 5), np.float32) / 25 # 設定平滑核心大小
img_smoth_filter2D = cv2.filter2D(img, -1, kernel) # 2D卷積法
img_smoth_blur = cv2.blur(img, (5, 5)) # 平均法
img_smoth_gaussianblur = cv2.GaussianBlur(img, (5, 5), 0) # 高斯模糊
img_smoth_medianblur = cv2.medianBlur(img, 5) # 中值法
titles = ['filter2D', 'blur', 'GaussianBlur', 'medianBlur'] # 定義标題集
images = [img_smoth_filter2D, img_smoth_blur, img_smoth_gaussianblur, img_smoth_medianblur] # 定義圖像集
plt.subplot(2, 2, i + 1), plt.imshow(images[i], 'gray')
\# 以灰階模式展示每個子網格的圖像
plt.title(titles[i]) # 設定每個子網格标題
plt.xticks([]), plt.yticks([]) # 設定x軸和y軸标題
先設定平滑核心大小,然後分别使用cv2.filter2D(3D卷積)、cv2.blur(平均法)、cv2.GaussianBlur(高斯模糊)、cv2.medianBlur(中值法)進行平滑結果對比,如圖3-18所示。
第12部分形态學處理。
img2 = cv2.imread('j.png', 0) # 以灰階模式讀取圖像
kernel = np.ones((5, 5), np.uint8) # 設定形态學處理核心大小
erosion = cv2.erode(img2, kernel, iterations=1) # 腐蝕
dilation = cv2.dilate(img2, kernel, iterations=1) # 膨脹
plt.subplot(1, 3, 1), plt.imshow(img2, 'gray') # 設定子網格1圖像
plt.subplot(1, 3, 2), plt.imshow(erosion, 'gray') # 設定子網格2圖像
plt.subplot(1, 3, 3), plt.imshow(dilation, 'gray') # 設定子網格3圖像
這裡重新以灰階模式讀取一個圖像,定義處理核心之後,通過cv2.erode和cv2.dilate分别實作腐蝕和膨脹操作。原圖和腐蝕、膨脹處理後的圖像對比如圖3-19所示。
上述過程中,主要需要考慮的關鍵點是:如何根據不同的圖像處理需求,實作圖像的基本預處理任務,尤其是對于每種方法下參數的具體設定,需要根據實際情況加以選擇。另外,在程式自動化過程中,是不可能依靠人工參與每次邊界調整、門檻值優化等具體過程,而應該通過一定專用的參數優化模型來實作。例如,對于透視圖像的處理,首先要做的是識别出透視圖像的矯正參照點,而該過程就是一個融合了業務場景、圖像處理技術、數學知識和計算方法等多學科知識的模組化過程。
- 通過cv2.imread對圖像檔案的資料讀取,并分别以彩色和灰階模式讀取圖像;
- 通過cv2.imshow、cv2.waitKey()、cv2.destroyAllWindows()實作圖像展示;
- 通過Matplotlib庫實作多子網格圖的展示;
- 通過cv2.resize實作圖像縮放;
- 通過cv2.warpAffine實作圖像平移;
- 通過cv2.warpAffine和cv2.getRotationMatrix2D實作圖像旋轉;
- 通過cv2. warpPerspective和cv2.getPerspectiveTransform實作圖像透視變換;
- 通過cv2.cvtColor實作圖像顔色模式轉換;
- 通過cv2.Canny實作圖像邊緣檢測;
- 通過cv2.threshold實作圖像二值化處理,并通過簡單門檻值、自适應均值門檻值、自适應高斯門檻值等方法尋找最佳門檻值;
- 通過cv2.filter2D、cv2.blur、cv2.GaussianBlur、cv2.medianBlur等方法實作圖像平滑處理;
- 通過for循環做資料循環輸出;
- 通過cv2.erode、cv2.dilate等方法實作圖像腐蝕、膨脹等形态學處理。
3.10.4 自然語言文本預處理
與資料庫中的結構化資料相比,文本具有有限的結構,某些類型的資料源甚至沒有資料結構。是以,預處理就是要對半結構化或非結構化的文本進行格式和結構的轉換、分解和預處理等,以得到能夠用于進一步處理的基礎文本。不同環境下,文本所需的預處理工作内容有所差異,大體上分為以下幾個部分。
1.基本處理
根據不同的文本資料來源,可能涉及的基本文本處理包括去除無效标簽、編碼轉換、文檔切分、基本糾錯、去除空白、大小寫統一、去标點符号、去停用詞、保留特殊字元等。
- 去除無效标簽:例如從網頁源代碼擷取的文本資訊中包含HTML标簽,此時要提取特定标簽内容并去掉标簽。
- 編碼轉換:不同編碼轉換對于中文處理具有較大影響,例如UTF-8、UTF-16、GBK、GB2312等之間的轉換。
- 文檔切分:如果要獲得的單個文檔中包含多個檔案,此時需要進行單獨切分以将不同的文檔拆分出來。
- 基本糾錯:對于文本中明顯的人名、地名等常用語和特定場景用語的錯誤進行糾正。
- 去除空白:去除文本中包含的大量空格、空行等。
- 大小寫統一:将文本中的英文統一為大寫或小寫。
- 去标點符号:去除句子中的标點符号、特殊符号等。
- 去停用詞:常見的停用詞包括the、a、an、and、this、those、over、under、above、on等。
- 保留特殊字元:某些場景下可能需要隻針對漢字、英文或數字進行處理,其他字元都需要過濾掉。
2.分詞
分詞是将一系列連續的字元串按照一定邏輯分割成單獨的詞。在英文中,單詞之間是以空格作為自然分界符的;而中文隻有字、句和段能通過明顯的分界符來簡單劃界,而作為詞則沒有形式上的分界符。是以,中文分詞要比英語等語種分詞困難和複雜得多。對于複雜的中文分詞而言,常用的分詞方法包括最大比對法、逆向最大比對法、雙向比對法、最佳比對法、聯想-回溯法等。
3.文本轉向量(word to vector)
人們通常采用向量空間模型來描述文本向量,即将文檔作為行,将分詞後得到的單詞(單詞在向量空間模型裡面被稱為向量,也被稱為特征、次元或維)作為列,而矩陣的值則是通過詞頻統計算法得到的值。這種空間向量模型也稱為文檔特征矩陣,其表示方法如表3-6所示。
本示例中,将僅對自然語言文本做分詞和word to vector處理,更多有關文本分析的内容,例如詞性标注、關鍵字提取、詞頻統計、文本聚類、相似關鍵字分析等将在第4章介紹。資料源檔案text.txt位于“附件-chapter3”中。完整代碼如下。
第1部分導入庫。本代碼中用到了jieba做中文分詞,sklearn用來做word to vector轉換,Pandas用于做格式化輸出。
import jieba # 結巴分詞
from sklearn.feature_extraction.text import TfidfVectorizer # 基于TF-IDF的詞頻轉向量庫
第2部分建立一個分詞函數。
def jieba_cut(string):
return list(jieba.cut(string)) # 精确模式分詞
該函數直接調用jieba.cut做中文分詞,用于下面的中文分詞配合map函數使用。jieba.cut傳回是一個分詞對象,使用list方法轉換為清單。
第3部分讀取自然語言檔案和停用詞。
with open('text.txt', encoding='utf8') as fn1, open('stop_words.txt', encoding= 'utf8') as fn2:
string_lines = fn1.readlines()
stop_words = fn2.read()
stop_words = stop_words.split('n')
stop_words.append('n')
讀取檔案時基于with方法做上下文管理,同時使用Python标準方法open方法打開兩個檔案對象,使用read方法分别讀取自然文本以及停用詞。由于使用read方法讀取的是字元串,我們需要将其轉換為清單,這裡分别使用split方法對字元串做拆分,分割符為“n”。
第4部分中文分詞。
seg_list = list(map(jieba_cut,string_lines)) # 存儲所有分詞結果
for i in range(5): # 列印輸出第1行的前5條資料
print(seg_list[1][i])
直接使用map方法建立seg_list,批量對上面的文本清單string_lines用jieba_cut做分詞,然後将map對象轉換為list清單。最後通過循環列印輸出第2條資料的前3個詞,如下:
對于
資料
化
第6部分word to vector。
vectorizer = TfidfVectorizer(stop_words=stop_words, tokenizer=jieba_cut)
# 建立詞向量模型
vector_value = vectorizer.fit_transform(string_lines).toarray()
# 将文本資料轉換為向量空間模型
vector = vectorizer.get_feature_names() # 獲得詞向量
vector_pd = pd.DataFrame(vector_value, columns=vector) # 建立用于展示的資料框
print(vector_pd.head(1)) # 列印輸出第1條資料
先定義一個要去除的停用詞庫,然後使用TfidfVectorizer方法建立詞向量模型,使用fit_transform方法對輸入分詞後的清單做轉換,并使用toarray方法将向量結果轉換為數組。最後通過詞向量的get_feature_names獲得向量名稱,再通過資料框做資料格式化。列印輸出第1條資料如下:
在本示例中,沒有涉及更多的自然語言文本預處理環節,例如無效标簽、編碼轉換、文檔切分、基本糾錯、去除空白、大小寫統一、去标點符号、去停用詞、保留特殊字元等,原因是這些内容都是針對不同案例展開的,而本示例僅做功能示範使用,具體應對不同文本時,很難具有通用性和可複制性。另外,由于測試檔案為本書的部分文字内容,文本規模本身有限,是以可能難以提取出真正有價值的資料和規律,尤其是基于向量化的分詞由于資料量的限制以及作為擴充内容,無法做更多有價值的探索。
上述過程中,主要需要考慮的關鍵點是:如何根據不同自然語言的來源特點、應用場景、語言文法、目标應用做綜合的文本處理,文本進行中用到的針對文字内容的過濾、篩選、去除、替換等主要基于字元串的操作居多。
- 使用jieba.cut做中文分詞,并可設定不同的分詞模式;
- 通過append方法對清單追加元素;
- 使用字元串的split方法,将其分割為多個字元串清單;
- 使用with方法管理多個檔案對象;
- 通過sklearn.feature_extraction.text的TfidfVectorizer方法做word to vector處理;
- 通過詞向量的get_feature_names獲得向量名稱;
- 通過轉換後的向量使用toarray方法将向量結果轉換為數組;
- 使用pandas.DataFrame建立數組,并通過head方法輸出前n條資料。
3.11 本章小結
内容小結:本章介紹了10個有關資料化營運過程中的資料預處理經驗,涵蓋了常見的資料清洗、标志轉換、資料降維、樣本不均衡、資料源沖突、抽樣、共線性、相關性分析、資料标準化、資料離散化等内容,并在最後提出了營運業務對于資料處理的影響和應對措施。在擴充内容中簡單介紹了有關網頁、日志、圖像、自然語言的文本預處理工作,作為拓展知識閱讀。本章涉及技術的部分都有對應示例代碼,該代碼可在“附件-chapter3”中的名為chapter3_code.ipynb的檔案中找到。
資料的預處理其實包含了基本的資料清洗、加工以及對特征的處理,其中有關特征工程的部分是整個預處理的核心,包括特征的選擇、轉換、組合、重構等都是為了更好地描述資料本身,一定程度上,特征的品質決定了後續模型結果的品質,是以特征工程本身是既複雜又重要的關鍵過程。
重點知識:客觀上講,本章的每一節内容都非常重要,原因是所有的内容都沒有唯一答案,都需要讀者根據不同的場景進行判别,然後選擇最合适的方法進行處理。是以,掌握每種方法的适用條件以及如何辨識其應用前提是關鍵知識。
外部參考:限于篇幅,本書涉及很多内容無法一一展開介紹,以下給出更多外部參考資源供讀者學習:
- Python有一個第三方庫Featuretools,它能自動實作對特征的加工和處理,例如基于深度特征綜合建立新特征。有興趣的讀者可以學習一下,對于特征的派生思路很有幫助。
- Python第三方庫imblearn提供了非常多的樣本不均衡處理方法,尤其是SMOTE、組合/內建方法的應用非常廣泛。讀者可在 https://github.com/scikit-learn-contrib/imbalanced-learn 中找到更多資訊。
- 本書中多次引用了sklearn中的processing庫,裡面還有更多有關資料處理的方法,讀者可查閱 http://scikit-learn.org/stable/modules/preprocessing.html 了解更多。
- 關于資料的預處理,Pandas庫真的非常好用,尤其是對于互動式或探索式的資料分析而言,Pandas更是利器,推薦讀者更加深入地了解和學習,在 http://pandas.pydata.org/pandas-docs/stable/ 中可檢視更多。
- 對于自然語言處理而言,語料庫的豐富程度決定了可供訓練的“原材料”的多寡,在本章的附錄中,将有單獨的部分提供外部語料庫的參考資訊。
應用實踐:本章幾乎每節都帶有示例代碼,讀者可直接使用附件中的示例資料進行模拟操作,以了解實作方法。同時,推薦讀者從自己所在環境中找到一些真實資料,針對每個子產品進行操作練習。