[TOC]
## 數據清洗
* 來了項目,首先是分析項目的目的和需求,了解這個項目屬于什么問題,要達到什么效果。然后提取數據,做基本的數據清洗。第三步是特征工程,這個屬于臟活累活,需要耗費很大的精力,如果特征工程做的好,那么,后面選擇什么算法其實差異不大,反之,不管選擇什么算法,效果都不會有突破性的提高。第四步,是跑算法,通常情況下,我會把所有能跑的算法先跑一遍,看看效果,分析一下precesion/recall和f1-score,看看有沒有什么異常(譬如有好幾個算法precision特別好,但是recall特別低,這就要從數據中找原因,或者從算法中看是不是因為算法不適合這個數據),如果沒有異常,那么就進行下一步,選擇一兩個跑的結果最好的算法進行調優。調優的方法很多,調整參數的話可以用網格搜索、隨機搜索等,調整性能的話,可以根據具體的數據和場景進行具體分析。調優后再跑一邊算法,看結果有沒有提高,如果沒有,找原因,數據 or 算法?是數據質量不好,還是特征問題還是算法問題。一個一個排查,找解決方法。特征問題就回到第三步再進行特征工程,數據質量問題就回到第一步看數據清洗有沒有遺漏,異常值是否影響了算法的結果,算法問題就回到第四步,看算法流程中哪一步出了問題。如果實在不行,可以搜一下相關的論文,看看論文中有沒有解決方法。這樣反復來幾遍,就可以出結果了,寫技術文檔和分析報告,再向業務人員或產品講解我們做的東西,然后他們再提建議/該需求,不斷循環,最后代碼上線,改bug,直到結項。
* 直觀來看,可以用一個流程圖來表示:
* 
* 為什么要進行數據清洗呢?我們在書上看到的數據,譬如常見的iris數據集,房價數據,電影評分數據集等等,數據質量都很高,沒有缺失值,沒有異常點,也沒有噪音,而在真實數據中,我們拿到的數據可能包含了大量的缺失值,可能包含大量的噪音,也可能因為人工錄入錯誤導致有異常點存在,對我們挖據出有效信息造成了一定的困擾,所以我們需要通過一些方法,盡量提高數據的質量。數據清洗一般包括以下幾個步驟:
* 一、分析數據
* 二、缺失值處理
* 三、異常值處理
* 四、去重處理
* 五、噪音數據處理
* 六、一些實用的數據處理小工具
### 一、分析數據
* 在實際項目中,當我們確定需求后就會去找相應的數據,拿到數據后,首先要對數據進行描述性統計分析,查看哪些數據是不合理的,也可以知道數據的基本情況。如果是銷售額數據可以通過分析不同商品的銷售總額、人均消費額、人均消費次數等,同一商品的不同時間的消費額、消費頻次等等,了解數據的基本情況。此外可以通過作圖的方式了解數據的質量,有無異常點,有無噪音等。舉個例子(這里數據較少,就直接用R作圖了):
```
#一組年薪超過10萬元的經理收入
pay=c(11,19,14,22,14,28,13,81,12,43,11,16,31,16,23.42,22,26,17,22,13,27,180,16,43,82,14,11,51,76,28,66,29,14,14,65,37,16,37,35,39,27,14,17,13,38,28,40,85,32,25,26,16,12,54,40,18,27,16,14,33,29,77,50,19,34)
par(mfrow=c(2,2))#將繪圖窗口改成2*2,可同時顯示四幅圖
hist(pay)#繪制直方圖
dotchart(pay)#繪制點圖
barplot(pay,horizontal=T)#繪制箱型圖
qqnorm(pay);qqline(pay)#繪制Q-Q圖
```

* 從上面四幅圖可以很清楚的看出,180是異常值,即第23個數據需要清理。
* python中也包含了大量的統計命令,其中主要的統計特征函數如下圖所示:

### 二、缺失值處理
* 缺失值在實際數據中是不可避免的問題,有的人看到有缺失的數據就直接刪除了,有的人直接賦予0值或者某一個特殊的值,那么到底該怎么處理呢?對于不同的數據場景應該采取不同的策略,首先應該判斷缺失值的分布情況:
```
1 import scipy as sp
2 data = sp.genfromtxt("web_traffic.tsv",delimiter = "\t")
```
* 數據情況如下:
```
>>>data
array([[ 1.00000000e+00, 2.27200000e+03],
[ 2.00000000e+00, nan],
[ 3.00000000e+00, 1.38600000e+03],
...,
[ 7.41000000e+02, 5.39200000e+03],
[ 7.42000000e+02, 5.90600000e+03],
[ 7.43000000e+02, 4.88100000e+03]])
>>> print data[:10]
[[ 1.00000000e+00 2.27200000e+03]
[ 2.00000000e+00 nan]
[ 3.00000000e+00 1.38600000e+03]
[ 4.00000000e+00 1.36500000e+03]
[ 5.00000000e+00 1.48800000e+03]
[ 6.00000000e+00 1.33700000e+03]
[ 7.00000000e+00 1.88300000e+03]
[ 8.00000000e+00 2.28300000e+03]
[ 9.00000000e+00 1.33500000e+03]
[ 1.00000000e+01 1.02500000e+03]]
>>> data.shape
(743, 2)
```
* 可以看到,第2列已經出現了缺失值,現在我們來看一下缺失值的數量:
```
1 >>> x = data[:,0]
2 >>> y = data[:,1]
3 >>> sp.sum(sp.isnan(y))
4 8
```
* 在743個數據里只有8個數據缺失,所以刪除它們對于整體數據情況影響不大。當然,這是缺失值少的情況下,在缺失值值比較多,而這個維度的信息還很重要的時候(因為缺失值如果占了95%以上,可以直接去掉這個維度的數據了),直接刪除會對后面的算法跑的結果造成不好的影響。我們常用的方法有以下幾種:
> 1. 直接刪除----適合缺失值數量較小,并且是隨機出現的,刪除它們對整體數據影響不大的情況
> 2. 使用一個全局常量填充---譬如將缺失值用“Unknown”等填充,但是效果不一定好,因為算法可能會把它識別為一個新的類別,一般很少用
> 3. 使用均值或中位數代替----優點:不會減少樣本信息,處理簡單。缺點:當缺失數據不是隨機數據時會產生偏差.對于正常分布的數據可以使用均值代替,如果數據是傾斜的,使用中位數可能更好。
>
> 4. 插補法
> 1)隨機插補法----從總體中隨機抽取某個樣本代替缺失樣本
> 2)多重插補法----通過變量之間的關系對缺失數據進行預測,利用蒙特卡洛方法生成多個完整的數據集,在對這些數據集進行分析,最后對分析結果進行匯總處理
> 3)熱平臺插補----指在非缺失數據集中找到一個與缺失值所在樣本相似的樣本(匹配樣本),利用其中的觀測值對缺失值進行插補。
> 優點:簡單易行,準去率較高
> 缺點:變量數量較多時,通常很難找到與需要插補樣本完全相同的樣本。但我們可以按照某些變量將數據分層,在層中對缺失值實用均值插補
> 4)拉格朗日差值法和牛頓插值法(簡單高效,數值分析里的內容,數學公式以后再補 = =)
> 5. 建模法
> 可以用回歸、使用貝葉斯形式化方法的基于推理的工具或決策樹歸納確定。例如,利用數據集中其他數據的屬性,可以構造一棵判定樹,來預測缺失值的值。
* 以上方法各有優缺點,具體情況要根據實際數據分分布情況、傾斜程度、缺失值所占比例等等來選擇方法。一般而言,建模法是比較常用的方法,它根據已有的值來預測缺失值,準確率更高。
### 三、異常值處理
* 異常值我們通常也稱為“離群點”。在講分析數據時,我們舉了個例子說明如何發現離群點,除了畫圖(畫圖其實并不常用,因為數據量多時不好畫圖,而且慢),還有很多其他方法:
1. 簡單的統計分析
拿到數據后可以對數據進行一個簡單的描述性統計分析,譬如最大最小值可以用來判斷這個變量的取值是否超過了合理的范圍,如客戶的年齡為-20歲或200歲,顯然是不合常理的,為異常值。
在python中可以直接用pandas的describe( ):
```
>>> import pandas as pd
>>> data = pd.read_table("web_traffic.tsv",header = None)
>>> data.describe()
0 1
count 743.000000 735.000000
mean 372.000000 1962.165986
std 214.629914 860.720997
min 1.000000 472.000000
25% 186.500000 1391.000000
50% 372.000000 1764.000000
75% 557.500000 2217.500000
max 743.000000 5906.000000
```
2. 3?原則
如果數據服從正態分布,在3?原則下,異常值為一組測定值中與平均值的偏差超過3倍標準差的值。如果數據服從正態分布,距離平均值3?之外的值出現的概率為P(|x-u| > 3?) <= 0.003,屬于極個別的小概率事件。如果數據不服從正態分布,也可以用遠離平均值的多少倍標準差來描述。
3. 箱型圖分析
箱型圖提供了識別異常值的一個標準:如果一個值小于QL01.5IQR或大于OU-1.5IQR的值,則被稱為異常值。QL為下四分位數,表示全部觀察值中有四分之一的數據取值比它小;QU為上四分位數,表示全部觀察值中有四分之一的數據取值比它大;IQR為四分位數間距,是上四分位數QU與下四分位數QL的差值,包含了全部觀察值的一半。箱型圖判斷異常值的方法以四分位數和四分位距為基礎,四分位數具有魯棒性:25%的數據可以變得任意遠并且不會干擾四分位數,所以異常值不能對這個標準施加影響。因此箱型圖識別異常值比較客觀,在識別異常值時有一定的優越性。
4. 基于模型檢測
首先建立一個數據模型,異常是那些同模型不能完美擬合的對象;如果模型是簇的集合,則異常是不顯著屬于任何簇的對象;在使用回歸模型時,異常是相對遠離預測值的對象
* 優缺點:1.有堅實的統計學理論基礎,當存在充分的數據和所用的檢驗類型的知識時,這些檢驗可能非常有效;2.對于多元數據,可用的選擇少一些,并且對于高維數據,這些檢測可能性很差。
5. 基于距離
通常可以在對象之間定義鄰近性度量,異常對象是那些遠離其他對象的對象
* 優缺點:1.簡單;2.缺點:基于鄰近度的方法需要O(m2)時間,大數據集不適用;3.該方法對參數的選擇也是敏感的;4.不能處理具有不同密度區域的數據集,因為它使用全局閾值,不能考慮這種密度的變化。
6. 基于密度
當一個點的局部密度顯著低于它的大部分近鄰時才將其分類為離群點。適合非均勻分布的數據。
* 優缺點:1.給出了對象是離群點的定量度量,并且即使數據具有不同的區域也能夠很好的處理;2.與基于距離的方法一樣,這些方法必然具有O(m2)的時間復雜度。對于低維數據使用特定的數據結構可以達到O(mlogm);3.參數選擇困難。雖然算法通過觀察不同的k值,取得最大離群點得分來處理該問題,但是,仍然需要選擇這些值的上下界。
7. 基于聚類:
基于聚類的離群點:一個對象是基于聚類的離群點,如果該對象不強屬于任何簇。離群點對初始聚類的影響:如果通過聚類檢測離群點,則由于離群點影響聚類,存在一個問題:結構是否有效。為了處理該問題,可以使用如下方法:對象聚類,刪除離群點,對象再次聚類(這個不能保證產生最優結果)。
* 優缺點:1.基于線性和接近線性復雜度(k均值)的聚類技術來發現離群點可能是高度有效的;2.簇的定義通常是離群點的補,因此可能同時發現簇和離群點;3.產生的離群點集和它們的得分可能非常依賴所用的簇的個數和數據中離群點的存在性;4.聚類算法產生的簇的質量對該算法產生的離群點的質量影響非常大。
**處理方法:**
1. 刪除異常值----明顯看出是異常且數量較少可以直接刪除
2. 不處理---如果算法對異常值不敏感則可以不處理,但如果算法對異常值敏感,則最好不要用,如基于距離計算的一些算法,包括kmeans,knn之類的。
3. 平均值替代----損失信息小,簡單高效。
4. 視為缺失值----可以按照處理缺失值的方法來處理
### 四、去重處理
以DataFrame數據格式為例:
```
#創建數據,data里包含重復數據
>>> data = pd.DataFrame({'v1':['a']*5+['b']* 4,'v2':[1,2,2,2,3,4,4,5,3]})
>>> data
v1 v2
0 a 1
1 a 2
2 a 2
3 a 2
4 a 3
5 b 4
6 b 4
7 b 5
8 b 3
#DataFrame的duplicated方法返回一個布爾型Series,表示各行是否是重復行
>>> data.duplicated()
0 False
1 False
2 True
3 True
4 False
5 False
6 True
7 False
8 False
dtype: bool
#drop_duplicates方法用于返回一個移除了重復行的DataFrame
>>> data.drop_duplicates()
v1 v2
0 a 1
1 a 2
4 a 3
5 b 4
7 b 5
8 b 3
#這兩個方法默認會判斷全部列,你也可以指定部分列進行重復項判斷。假設你還有一列值,且只希望根據v1列過濾重復項:
>>> data['v3']=range(9)
>>> data
v1 v2 v3
0 a 1 0
1 a 2 1
2 a 2 2
3 a 2 3
4 a 3 4
5 b 4 5
6 b 4 6
7 b 5 7
8 b 3 8
>>> data.drop_duplicates(['v1'])
v1 v2 v3
0 a 1 0
5 b 4 5
#duplicated和drop_duplicates默認保留的是第一個出現的值組合。傳入take_last=True則保留最后一個:
>>> data.drop_duplicates(['v1','v2'],take_last = True)
v1 v2 v3
0 a 1 0
3 a 2 3
4 a 3 4
6 b 4 6
7 b 5 7
8 b 3 8
```
* 如果數據是列表格式的,有以下幾種方法可以刪除:
```
list0=['b','c', 'd','b','c','a','a']
方法1:使用set()
list1=sorted(set(list0),key=list0.index) # sorted output
print( list1)
方法2:使用 {}.fromkeys().keys()
list2={}.fromkeys(list0).keys()
print(list2)
方法3:set()+sort()
list3=list(set(list0))
list3.sort(key=list0.index)
print(list3)
方法4:迭代
list4=[]
for i in list0:
if not i in list4:
list4.append(i)
print(list4)
方法5:排序后比較相鄰2個元素的數據,重復的刪除
def sortlist(list0):
list0.sort()
last=list0[-1]
for i in range(len(list0)-2,-1,-1):
if list0[i]==last:
list0.remove(list0[i])
else:
last=list0[i]
return list0
print(sortlist(list0))
```
### 五、噪音處理
* ?噪音,是被測量變量的隨機誤差或方差。我們在上文中提到過異常點(離群點),那么離群點和噪音是不是一回事呢?我們知道,觀測量(Measurement) = 真實數據(True Data) + 噪聲 (Noise)。離群點(Outlier)屬于觀測量,既有可能是真實數據產生的,也有可能是噪聲帶來的,但是總的來說是和大部分觀測量之間有明顯不同的觀測值。。噪音包括錯誤值或偏離期望的孤立點值,但也不能說噪聲點包含離群點,雖然大部分數據挖掘方法都將離群點視為噪聲或異常而丟棄。然而,在一些應用(例如:欺詐檢測),會針對離群點做離群點分析或異常挖掘。而且有些點在局部是屬于離群點,但從全局看是正常的。
* 在quora上看到過一個解釋噪音與離群點的有趣的例子:
> 離群點: 你正在從口袋的零錢包里面窮舉里面的錢,你發現了3個一角,1個五毛,和一張100元的毛爺爺向你微笑。這個100元就是個離群點,因為并不應該常出現在口袋里..
噪聲: 你晚上去三里屯喝的酩酊大醉,很需要買點東西清醒清醒,這時候你開始翻口袋的零錢包,嘛,你發現了3個一角,1個五毛,和一張100元的毛爺爺向你微笑。但是你突然眼暈,把那三個一角看成了三個1元...這樣錯誤的判斷使得數據集中出現了噪聲
* 那么對于噪音我們應該如何處理呢?有以下幾種方法:
*
**1. 分箱法**
分箱方法通過考察數據的“近鄰”(即,周圍的值)來光滑有序數據值。這些有序的值被分布到一些“桶”或箱中。由于分箱方法考察近鄰的值,因此它進行局部光滑。
* 用箱均值光滑:箱中每一個值被箱中的平均值替換。
* 用箱中位數平滑:箱中的每一個值被箱中的中位數替換。
* 用箱邊界平滑:箱中的最大和最小值同樣被視為邊界。箱中的每一個值被最近的邊界值替換。
一般而言,寬度越大,光滑效果越明顯。箱也可以是等寬的,其中每個箱值的區間范圍是個常量。分箱也可以作為一種離散化技術使用.
**2.? 回歸法**
* 可以用一個函數擬合數據來光滑數據。線性回歸涉及找出擬合兩個屬性(或變量)的“最佳”直線,使得一個屬性能夠預測另一個。多線性回歸是線性回歸的擴展,它涉及多于兩個屬性,并且數據擬合到一個多維面。使用回歸,找出適合數據的數學方程式,能夠幫助消除噪聲。
### 六、一些實用的數據處理小工具
1. **去掉文件中多余的空行**
空行主要指的是(\\n,\\r,\\r\\n,\\n\\r等),在python中有個strip()的方法,該方法可以去掉字符串兩端多余的“空白”,此處的空白主要包括空格,制表符(\\t),換行符。不過親測以后發現,strip()可以匹配掉\\n,\\r\\n,\\n\\r等,但是過濾不掉單獨的\\r。為了萬無一失,我還是喜歡用麻煩的辦法,如下:
```
#-*- coding :utf-8 -*-
#文本格式化處理,過濾掉空行
file = open('123.txt')
i = 0
while 1:
line = file.readline().strip()
if not line:
break
i = i + 1
line1 = line.replace('\r','')
f1 = open('filename.txt','a')
f1.write(line1 + '\n')
f1.close()
print str(i)
```
2. **如何判斷文件的編碼格式**
```
#-*- coding:utf8 -*-
#批量處理編碼格式轉換(優化)
import os
import chardet
path1 = 'E://2016txtutf/'
def dirlist(path):
filelist = os.listdir(path)
for filename in filelist:
filepath = os.path.join(path, filename)
if os.path.isdir(filepath):
dirlist(filepath)
else:
if filepath.endswith('.txt'):
f = open(filepath)
data = f.read()
if chardet.detect(data)['encoding'] != 'utf-8':
print filepath + "----"+ chardet.detect(data)['encoding']
dirlist(path1)
```
3. **文件編碼格式轉換,gbk與utf-8之間的轉換**
這個主要是在一些對文件編碼格式有特殊需求的時候,需要批量將gbk的轉utf-8的或者將utf-8編碼的文件轉成gbk編碼格式的。
```
#-*- coding:gbk -*-
#批量處理編碼格式轉換
import codecs
import os
path1 = 'E://dir/'
def ReadFile(filePath,encoding="utf-8"):
with codecs.open(filePath,"r",encoding) as f:
return f.read()
def WriteFile(filePath,u,encoding="gbk"):
with codecs.open(filePath,"w",encoding) as f:
f.write(u)
def UTF8_2_GBK(src,dst):
content = ReadFile(src,encoding="utf-8")
WriteFile(dst,content,encoding="gbk")
def GBK_2_UTF8(src,dst):
content = ReadFile(src,encoding="gbk")
WriteFile(dst,content,encoding="utf-8")
def dirlist(path):
filelist = os.listdir(path)
for filename in filelist:
filepath = os.path.join(path, filename)
if os.path.isdir(filepath):
dirlist(filepath)
else:
if filepath.endswith('.txt'):
print filepath
#os.rename(filepath, filepath.replace('.txt','.doc'))
try:
UTF8_2_GBK(filepath,filepath)
except Exception,ex:
f = open('error.txt','a')
f.write(filepath + '\n')
f.close()
dirlist(path1)
```