今天給大家推薦一個數據分析與挖掘的實戰項目案例“基於京東手機銷售數據用回歸決策樹預測價格”。該項目先基於京東手機銷售數據做出一系列分析后,利用回歸決策樹僅根據手機外部特征進行價格預測。
本項目來自於實驗樓《樓+ 數據分析與挖掘實戰》第五期學員:Ted_Wei。
數據獲取
由於手機的價格以及評論數是需要經過 javascript 渲染的動態信息,單純用 requests 模塊是爬取不到的。我的解決方案是首先使用 selenium 的 webbrowser 模塊使用本地 Chrome 瀏覽器爬取每一款手機在京東的內部id,價格,以及評論數。然后再根據爬取到的id找到每款手機的銷售頁面,爬取每款手機的關鍵參數。
爬蟲代碼
parsing_code.py
可通過此頁面: https://www.kaggle.com/ted0001/dm05-998494/data 獲取。
數據清洗
獲取到的數據包含1199行,21列,每一行代表一款正在銷售的手機,每一列包含關於手機的一項參數(比如價格,內存大小,像素,等等)。 獲取到的數據大多為自然語言,非數值信息,對於分析十分不友好。所以數據分析的重點在於,找到實際上的缺失信息(如‘無’,‘參考官方數據’等都可認為是缺失數據NaN),以使用re模塊解析自然語言,提取其中有用的數值信息。
數據清洗的步驟其實相當繁瑣,如果把代碼貼出也會占用過多篇幅,因此數據清洗的代碼
data_clean.py
可通過此頁面: https://www.kaggle.com/ted0001/dm05-998494/data 獲取。
數據分析
下面直接進入正題,主要完成估計各品牌手機銷量並進行比較分析、對決定手機價格因素的探索、嘗試用機器學習方法預測手機價格。
首先調用所需的模塊
import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_absolute_error as mae
讀取已經清洗好的數據,數據也可在 https://www.kaggle.com/ted0001/dm05-998494/data 獲取。
data=pd.read_csv('../input/data-acquisitioncleaning/cleaned_data.csv')
data=data.set_index(['Unnamed: 0']) #DataFrame在存儲為csv file以后原來的index會變為一個列,因此要重新設置index
data.shape
輸出結果:
(1199,21)
估計各品牌手機銷量並進行比較分析
拿到清洗好的數據后的第一個想法,便是對各個品牌的手機的銷量進行一個比較。由於京東只顯示了每部手機的評論數量而不是具體銷量,我們只好默認評論數量comments和銷量成正比,從而估計各個手機品牌的銷量占比
pie_plt=data.groupby(['brand']).sum()['comments'].sort_values(ascending=False)#統計每個品牌評論總數,以此作為我們對銷量的估計
pie_plt
輸出結果:
brand
HUAWEI 11320592.0
Apple 9797100.0
XIAOMI 7995236.0
NOKIA 1324000.0
Philips 1227100.0
OPPO 1205300.0
vivo 1101330.0
K-Touch 793300.0
MEIZU 574900.0
SAMSUNG 539800.0
smartisan 364000.0
lenovo 275500.0
realme 157000.0
Meitu 109200.0
nubia 86000.0
chilli 58000.0
360 31000.0
ZTE 17000.0
Coolpad 12000.0
BlackBerry 10000.0
WE 90.0
Name: comments, dtype: float64
我們可以對以上數據用扇形圖更好地展現出來
#繪制各個手機品牌估計銷量的占比扇形圖
fig,axes=plt.subplots(figsize=(12,12))
comment_sum=pie_plt.values.sum()
percen=[np.round(each/comment_sum*100,2) for each in pie_plt.values]
axes.pie(pie_plt.values,labels=pie_plt.index,labeldistance=1.2,autopct = '%3.1f%%')
axes.legend([pie_plt.index[i]+': '+str(percen[i])+"%" for i in range(len(percen))],loc='upper right',bbox_to_anchor=(1, 0, 1, 1))
axes.set_title('Estimated Handphone Market Share in China')
plt.show()
輸出結果:
從以上扇形圖我們可以估計,在中國市場銷量前三的手機品牌分別是華為,蘋果,小米,分別占到了總銷量的30.6%,26.5%,21.6%,剩下的品牌銷量則遠遠不及銷量前三的品牌。 為了驗證以上估計的正確性,我查閱了2019年中國手機市場各個品牌份額的相關資料。發現華為,蘋果,小米的占比分別為34%,9%,12%,除華為外,小米和蘋果的市場份額數據均有較大出入。而vivo,oppo的市場份額則分別是19%和18%,與原來的數據差距就更大了。 如果京東的評論數量可以大致反映手機線上銷售情況的話,我想造成數據有如此大差異的原因可能在於沒有考慮中國市場的線下銷售。根據平時的生活經驗,蘋果和小米的線下門店數量(特別是在小城市)是遠遠不及oppo,vivo的門店數量的。整個手機市場既包括線上市場也包括線下市場,而造成我統計到的數據與權威數據差異的原因,很可能是因為我的數據沒有包含線下銷售。 如果能夠獲得線下銷售數據,那么也可以對以上推論進行進一步的驗證。
還有一點出乎我意料的是,諾基亞和飛利浦手機的估計銷量占比竟然也都超過了3%(甚至超過vivo,oppo),於是便想看看這兩個品牌分別銷售哪種價位手機
data[(data['brand']=='NOKIA')|(data['brand']=='Philips')]['price'].median()#諾基亞和飛利浦手機價格中位數
輸出結果:
208.5
data[(data['brand']=='NOKIA')|(data['brand']=='Philips')]['price'].mean()#諾基亞和飛利浦手機價格平均數
輸出結果:
336.1190476190476
這樣看來,諾基亞和飛利浦的手機價格多在200-300元左右,再根據item id訪問京東網站后,發現果不其然,這兩個品牌所銷售的大多是功能機。(諾基亞有部分智能手機) 在這個智能手機已經全面普及的年代,想不到功能機也還是有它的一畝三分地,並沒有完全被市場淘汰(尤其是線上銷售渠道)。這可能是因為部分老人仍然更習慣使用功能機,以及功能機待機時間長,鈴聲音量大,能滿足部分人群的特殊需求。
對決定手機價格因素的探索
另一個值得研究的問題便是手機的價格和手機配置參數的關系。為了讓我們有一個整體把握,我們會先畫出各個數值數據間的correlation matrix,然后再探索非數值數據(categorical data),如品牌,屏幕材料,對價格的影響。
畫出Correlation Matrix 並進行分析
由於蘋果手機和安卓手機的價格、配置差異較大,而且蘋果手機配置參數在我們的數據集中大多缺失,我們研究這個問題時,就暫且只考慮安卓手機。(價格為9999的手機是華為將於2019年7月發行的榮耀X9,由於該型號價格異常,故也將其從我們的分析中排除。)
correlation=data[(data['brand']!='Apple')&(data['price']!=9999)].corr()
#繪制相應correlation matrix的heatmap
fig,axes=plt.subplots(figsize=(8,8))
cax=sns.heatmap(correlation,vmin=-0.25, vmax=1,square=True,annot=True)
axes.set_xticklabels(['RAM', 'ROM', 'battery', 'comments', 'price', 'rear camera',
'resolution', 'screen size', 'weight'])
axes.set_yticklabels(['RAM', 'ROM', 'battery', 'comments', 'price', 'rear camera',
'resolution', 'screen size', 'weight'])
axes.set_title('Heatmap of Correlation Matrix of numerical data')
plt.show()
輸出結果:
從上圖我們可以看出,價格 price 和存儲空間 ROM 以及內存 RAM 的關聯度最大,分別達到了0.71和0.68。其次便是電池容量battery,后置攝像頭個數rear camera,屏幕大小screen size,關聯度都分別達到了0.42。這也比較符合我們常識性的判斷。 我們還可以很明顯的看到,評價個數comments的column和row都呈現深紫色,代表comments和各個數值參數的關聯都很小。 (由於我們的數據集缺失值較多,在舍棄掉部分缺失值的行后,以上correlation matrix的數值會出現一定程度的變化)
手機品牌對手機價格影響的探索
當然,決定手機價格很關鍵的因素還有一些非數值數據(categorical data)。最容易想到的便是手機的品牌了。
data.groupby(['brand']).median()['price'].sort_values(ascending=False).values.std() #計算不同品牌價格中位數集合的標准差
輸出結果:
1409.0576123336064
以上數據也可以通過柱狀圖來更直觀地展示。
bar_plt=data.groupby(['brand']).median()['price']
fig,axes=plt.subplots(figsize=(20,8))
axes.bar(bar_plt.index,bar_plt.values)
axes.set_title('Median price of handphones of various brands')
輸出結果:
Text(0.5, 1.0, 'Median price of handphones of various brands')
我們可以看到,各個品牌手機中位數價格層次不齊,這也和我們的常識性判斷吻合,因為不同手機品牌的定位以及消費群體均有較大差異。
不同屏幕材料對手機價格影響的探索
還有一個很關鍵的因素其實是手機的屏幕材料,我們也可以用同樣的方法比較不同屏幕材料對價格的影響。
data.groupby(['screen material']).median()['price'].sort_values(ascending=False).values.std() #計算不同屏幕材料價格中位數集合的標准差
輸出結果:
1523.0026019740856
各種屏幕材料的手機的價格中位數展示如下
data.groupby(['screen material']).median()['price'].sort_values(ascending=False)
輸出結果:
screen material
Dynamic AMOLED 5999.0
OLED曲面屏 5488.0
OLED 4288.0
Super AMOLED 2998.0
AMOLED曲面屏 2908.0
LCD 2399.0
AMOLED 2299.0
TFT LCD(IPS) 1999.0
LTPS 1999.0
IPS 1399.0
TFT 1199.0
Name: price, dtype: float64
用柱狀圖展示如下:
bar_plt2=data.groupby(['screen material']).median()['price']
fig,axes=plt.subplots(figsize=(18,8))
axes.bar(bar_plt2.index,bar_plt2.values)
axes.set_title('Median price of handphones of various screen materials')
輸出結果:
Text(0.5, 1.0, 'Median price of handphones of various screen materials')
可以注意到的是,以上價格數值均在千元以上,而我們的數據集中還包含有價格很低廉的功能機,那它們的屏幕又都是什么材料呢?
data[(data['brand']=='NOKIA')|(data['brand']=='Philips')]['screen material'].value_counts()
輸出結果:
TFT 27
IPS 3
TFT LCD(IPS) 1
Name: screen material, dtype: int64
可以看到,諾基亞和飛利浦的手機(大多為功能機)的屏幕材料大多為TFT和IPS。
我們又可以反過來看看又到底是哪些價位的手機在使用TFT和IPS呢?
#繪制屏幕材料為IPS或TFT手機的價格分布圖
hist_plot=data[(data['screen material']=='IPS')|(data['screen material']=='TFT')]['price']#查看所有屏幕材料為IPS或TFT手機的價格
sns.distplot(hist_plot)
plt.title('Price Distribution Plot of Handphones Whose Screen Material is TFT or IPS ')
輸出結果:
Text(0.5, 1.0, 'Price Distribution Plot of Handphones Whose Screen Material is TFT or IPS ')
通過觀察以上分布圖以及進一步在數據集data中查看屏幕材料為TFT或IPS的手機發現,IPS主要用於華為的中低端手機,價格在千元以下,或者1300元左右。其中,價格在200元以下的功能機的屏幕材料均為TFT。 出乎我意料的是,也有部分高端手機使用的是IPS或TFT材料,比如華為的榮耀V20,蘋果的iphone 8,使用的是IPS材料;華為mate20和華為p20使用的均為TFT材料,這些手機的價格都在3500元以上。 通過以上的探索分析我們可以知道,高端智能機和低端功能機所使用的屏幕材料也很可能是一樣的。當然我們也不排除,同樣是TFT或IPS材料,它們內部也可能有區別。
嘗試用機器學習方法預測手機價格
在前面的小節中,我們探索了決定手機價格的幾大因素,手機存儲空間ROM,內存RAM,以及品牌,屏幕材料等都是決定手機價格的關鍵因素。 在這一小節中,我會使用回歸決策樹(Regression Decision Tree)的算法僅僅根據手機的外部特征來預測手機的價格。決策數的特征值僅僅采用了手機的品牌brand、后置攝像頭數量rear camera、以及手機重量weight作為我們的特征(feature),目標(target)當然則是我們的價格price 這樣做的原因一來是因為ROM和RAM存在太多的缺失值。如果選取這兩個值做為特征,那么我們會丟失掉太多訓練數據。 二來是想嘗試在不知道手機具體配置,僅僅通過觀察測量手機外部特征能否較為准確地預測手機價格。 以下數據顯示,如果選用ROM、RAM、和brand作為特征,那么我們只能得到原數據集31%左右的數據用作訓練和測試。
data.dropna(subset=['ROM','RAM','brand','price']).shape[0]/data.shape[0]
輸出結果:
0.30692243536280234
所有列缺失值數據統計:
data.isnull().sum().sort_values(ascending=False)
輸出結果:
CPU freq 998
CPU cores 824
front camera 773
RAM 720
rear camera specs 710
ROM 667
CPU model 479
screen material 381
brand 347
rear camera 249
battery 177
resolution 134
month 115
charging port 99
screen size 85
model 70
weight 66
SIM cards 56
year 6
price 3
comments 0
dtype: int64
品牌brand對價格影響非常明顯,所以雖然缺失值較多,我們也必須考慮這個特征。
考慮到品牌brand是非數值數據,我們選取使用回歸決策樹算法來進行機器學習建模。
從原來的數據集提取我們需要的數據
df=data.loc[:,['price','rear camera','brand','weight']].dropna()
由於回歸決策樹只接受數值型數據(numerical data),我們需要對brand進行獨熱編碼(one-hot encoding)
to_model=pd.get_dummies(df)#對非數值型數據進行獨熱編碼
提取特征值和目標值。 (考慮到各種手機品牌的型號數量畢竟很有限,而且部分品牌數據量較少,我們在這里就沒有划分訓練集和測試集了)
x=to_model.iloc[:,1:].values
y=to_model.iloc[:,0].values
訓練回歸決策數模型
model=DecisionTreeRegressor()
model.fit(x,y)
輸出結果:
DecisionTreeRegressor(criterion='mse', max_depth=None, 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,
presort=False, random_state=None, splitter='best')
檢驗我們的模型對各個品牌的預測准確性。
error_list=[]
for each in df['brand'].value_counts().index:
to_fill='brand_{}'.format(each)
x_data=to_model[to_model[to_fill]==1].iloc[:,1:].values
y_data=to_model[to_model[to_fill]==1].iloc[:,0].values
test_result=model.predict(x_data)
merror=mae(y_data.reshape(len(y_data),1),test_result.flatten())
error=(np.abs(test_result-y_data)/y_data).mean()
print(each,end=' : ')
print(np.round(merror,2),end=', ')
print(str(np.round(error*100,3))+'%')
error_list.append([each,merror,error])
輸出結果:
HUAWEI : 238.55, 15.16%
XIAOMI : 202.0, 12.277%
Apple : 663.28, 8.087%
OPPO : 177.65, 9.582%
vivo : 134.78, 8.747%
Philips : 7.01, 2.841%
MEIZU : 51.79, 3.009%
SAMSUNG : 269.2, 3.24%
K-Touch : 7.23, 4.144%
NOKIA : 12.5, 1.321%
lenovo : 33.33, 3.374%
Meitu : 120.0, 6.141%
smartisan : 0.0, 0.0%
realme : 0.0, 0.0%
nubia : 0.0, 0.0%
360 : 0.0, 0.0%
BlackBerry : 0.0, 0.0%
Coolpad : 0.0, 0.0%
ZTE : 0.0, 0.0%
chilli : 0.0, 0.0%
error_df=pd.DataFrame(error_list,columns=['brand','mean_absolute_error','mean_proportional_error'])
error_df
輸出結果:
以上的 DataFrame error_df 表示該決策樹模型對於每個品牌手機預測的准確性,誤差都均在 15% 以內,這個模型還是相對比較准確的。 實際上這個模型最關鍵的是提取了手機的重量weight這一關鍵信息,因為每個型號的手機重量多少是有些區別的,拿一個稍微精確一點的電子秤便能量出區別,決策數只不過是記住了數據而已。造成預測結果誤差的原因我想多半還是因為不同的賣家對同一型號手機的標價不同吧。
項目總結
雖然沒有詳細地呈現數據采集以及數據清理的過程,但是這兩個步驟確是所花時間最多的步驟。雖然京東的網頁對於爬蟲新手已經十分友好,但是頭一回爬取 javascript 渲染后的價格、評論數據還是頗有挑戰性。數據清理主要難點在於數據大多以自然語言呈現,要找到實際上的缺失值,以及將自然語言轉變為數值(比如評論數 comments,后置攝像頭數量 rear cameras )。除去寫這個 kaggle kernel,這兩個步驟大概花了所有時間的70%。 對於采集到的數據進行分析也不是之前想象到的那么容易,為了發掘更深一層次的信息,對於每一次通過 pandas 函數得到的結果都需要認真地分析結果,思考為什么會有這個結果。 總之,這次項目挑戰收獲還是比較大,也是頭一次自己完成數據的采集,清洗,以及分析的全過程。