數(shù)據(jù)項目總結(jié) -- 深圳租房數(shù)據(jù)分析!
作者:皮錢超,廈門大學,Datawhale成員
大家好,《上篇》根據(jù)深圳的租房數(shù)據(jù),并從統(tǒng)計分析和可視化的角度進行了分析,受到讀者的歡迎。今天將使用之前的數(shù)據(jù)進行數(shù)據(jù)分析和建模,以及模型的可解釋性探索。本文的主要內(nèi)容包含:

導入庫
導入主要的庫用于:數(shù)據(jù)處理、可視化、建模、特征可解釋性等。

1、數(shù)據(jù)探索
1)導入數(shù)據(jù)

數(shù)據(jù)在Datawhale后臺回復 深圳 可下載
2)數(shù)據(jù)形狀和字段類型

下面是具體的特征解釋:
#?下面是特征屬性
name:小區(qū)名字
layout:幾室?guī)讖d幾衛(wèi)
location:朝向
size:建筑面積
sizeInside:套內(nèi)面積
zhuangxiu:裝修方式
time:小區(qū)建成時間
zone:行政區(qū)
position:行政區(qū)的具體位置;比如寶安壹方中心,龍華民治等
way:出租方式,整租或者合租
#?最終的目標變量y?
money:價格??3)查看數(shù)據(jù)的缺失值情況
使用的是df.isnull().sum()來查看:在time字段中存在6個缺失值

4)缺失值填充
由于缺失量比較少,直接在網(wǎng)上搜索到相應的時間進行了人工填充:先定位缺失值的位置,再進行填充。

2、字段預處理
2.1 name
小區(qū)的姓名name直接刪除
df.drop("name",axis=1,inplace=True)
2.2 layout
layout分成3個具體的屬性:室、廳、衛(wèi)
特殊情況:當layout為"商鋪"時,直接刪除~

使用正則解析出室廳衛(wèi):
df1?=?df["layout"].str.extract(r'(?P\d)室(?P\d)廳(?P\d)衛(wèi)' )
#?合并到原數(shù)據(jù)
df?=?pd.concat([df1,df],axis=1)
#?刪除layout
df.drop("layout",axis=1,inplace=True)
#?layout=商鋪正則解析為NaN,直接刪除
df.dropna(subset=["shi","ting","wei"],inplace=True)
df.head()

2.3 location
不同朝向的房子數(shù)量:
df["location"].value_counts()
朝南?????552
朝南北????284
朝北?????241
朝東南????241
朝西南????174
朝西北????142
朝東北????140
朝東?????132
朝西??????92
朝東西??????2
Name:?location,?dtype:?int64
????
#?小提琴圖
fig?=?px.violin(df,y="money",color="location")
fig.show()

小結(jié):可以看到,在朝南北、朝南、朝北的房子數(shù)量比較多,而且整體的價格分布更為廣泛。
對不同朝向的房子實施硬編碼:
#?自定義的順序:根據(jù)朝向和價格的關(guān)系圖(上面)
location?=?["朝東西","朝東北","朝西","朝西北","朝東","朝西南","朝東南","朝南","朝北","朝南北"]
location_dict?=?{}
for?n,?i?in?enumerate(location):
????location_dict[i]?=?n+1?#?保證序號從1開始
????
df["location"]?=?df["location"].map(location_dict)
df.head()

2.4 size和sizeInside
建筑面積和套內(nèi)面積提取出數(shù)值部分,提供兩種方法:
#?1、通過切割的方式來提取
df["size"]?=?df["size"].apply(lambda?x:?x.split("面積")[1].split("㎡")[0])
#?2、使用正則的方式提取
df["sizeInside"]?=?df["sizeInside"].str.extract(r'面積(?P[\d.]+)' )
df.head()

2.5 zhuangxiu
df["zhuangxiu"].value_counts()
精裝????1172
普裝?????747
豪裝??????62
毛坯??????19
Name:?zhuangxiu,?dtype:?int64
主觀意義上的思路:毛坯的等級最低,豪裝最高。下面實施硬編碼過程:
#?硬編碼
zhuangxiu?=?{"毛坯":1,"普裝":2,?"精裝":3,?"豪裝":4}
zhuangxiu
{'毛坯':?1,?'普裝':?2,?'精裝':?3,?'豪裝':?4}
df["zhuangxiu"]?=?df["zhuangxiu"].map(zhuangxiu)
df.head()

2.6 numberFloor
中低高樓層和價格money之間的關(guān)系
#?提取中低高樓層
df["numberFloor"]?=?df["numberFloor"].apply(lambda?x:?x.split("(")[0])
df.head()


小結(jié):中低高3個樓層在房租上面的影響稍小。直接考慮獨熱編碼的方式:

df?=?(df.join(pd.get_dummies(df["numberFloor"]))
????.rename(columns={"中樓層":"middleFloor",
????????????????????"低樓層":"lowFloor",
????????????????????"高樓層":"highFloor"}))
df.head()

df.drop("numberFloor",axis=1,inplace=True)??#?刪除原字段
2.7 time
房子的建成時間處理:
df["time"].value_counts()
#?部分結(jié)果
2003年建成????133
2005年建成????120
2006年建成????114
2004年建成????111
2010年建成????104
......
2019年????????3???#?人工填充的時間
2022年建成??????2
1983年建成??????1
2003年????????1
2020年????????1
2004年????????1
Name:?time,?dtype:?int64
提取時間年份:
df["time"]?=?df["time"].str.extract(r'(?P)
#?time轉(zhuǎn)成數(shù)值型
df["time"]?=?df["time"].astype("float")
#?建成時間和當前的差距
df["time"]?=?2022?-?df["time"]
df.head()

2.8 zone+position
行政區(qū)域和地理位置的合并處理
df["zone"].value_counts()
龍崗??????548
福田??????532
龍華??????293
南山??????218
寶安??????173
羅湖??????167
光明???????32
坪山???????31
鹽田????????5
大鵬新區(qū)??????1
Name:?zone,?dtype:?int64
#?行政區(qū)和價格的關(guān)系
fig?=?px.violin(df,y="money",color="zone")
fig.show()

#?合并字段
df["zone_position"]?=?df["zone"]?+?"_"?+?df["position"]
df.head()

不同行政區(qū)域不同地理位置下的價格均值統(tǒng)計:
zone_position_mean?=?
(df.groupby("zone_position")["money"].mean()
.reset_index()
.sort_values("money",ascending=False,ignore_index=True))
zone_position_mean

根據(jù)合并的zone_position_mean數(shù)據(jù)框中的money 來進行硬編碼:福田_車公廟 是最高位
zone_position?=?zone_position_mean["zone_position"].tolist()[::-1]
zone_position_dict?=?{}
for?n,?i?in?enumerate(zone_position):
????zone_position_dict[i]?=?n+1?
????
df["zone_position"]?=?df["zone_position"].map(zone_position_dict)
#?刪除原字段
df.drop(["zone","position"],axis=1,inplace=True)
df.head()

2.9 way
出租方式和價格的關(guān)系探索:
fig?=?px.violin(df,y="money",color="way")
fig.show()

從way的取值來看:大部分的房子是愿意整租的,而且押一付一和押二付一最為普遍。提取出“整租”和“合租”兩種方式:
df["way"]?=?df["way"].apply(lambda?x:?x.split("?")[0])
#?編碼
df["way"]?=?df["way"].map({"整租":1,"合租":0})
df
終于:算是得到一份比較符合建模的數(shù)據(jù)

3、樣本不均衡
我們查看way字段下的數(shù)據(jù)情況:明顯way=1的取值情況是遠遠大于way=0。

解決方法:使用SMOTE算法來解決解決way=0過少的問題。

解決之后的way=1和way=0的樣本相同:

字段異常處理
1、填充樣本后發(fā)現(xiàn)某些應該是整數(shù),卻出現(xiàn)了小數(shù),比如:wei和time等

cols?=?["shi","ting","wei","time"]
for?i?in?cols:
????smote_df[i]?=?smote_df[i].apply(lambda?x:?round(x))
2、類型轉(zhuǎn)化

#?轉(zhuǎn)變數(shù)據(jù)類型
smote_df["size"]?=?smote_df["size"].astype(float)??
smote_df["sizeInside"]?=?smote_df["sizeInside"].astype(float)
4、相關(guān)性分析
4.1 相關(guān)系數(shù)
針對上面得到的smote_df數(shù)據(jù)進行下面的相關(guān)性分析:
#?保存了再讀取
#?smote_df.to_csv("data_new.csv",index=False)
df?=?pd.read_csv("data_new.csv")
1、相關(guān)性
能夠直觀看到money因變量和size與sizeInside相關(guān)性很強
corr?=?df.corr()
f,ax=plt.subplots(figsize=(12,6))
sns.heatmap(corr,vmax=0.8,square=True,fmt='.2f',?cmap='PuBu_r')
plt.show()

4.2 相關(guān)系數(shù)排序
單個屬性和money之間的相關(guān)性大小排序,可以看到:size(房子面積)、sizeInsize(套內(nèi)面積)和wei(衛(wèi))的相關(guān)型比較強。
單純地從相關(guān)系數(shù)得到的結(jié)論,還是有待考證~

4.3 自變量和因變量關(guān)系

上面舉出3個字段和money之間的相關(guān)性,在最右側(cè):
- size和sizeInside相對集中的時候,money高頻出現(xiàn)0-25000之間
- 在不同的money取值情況,wei字段的取值集中在1-4之間
5、回歸建模
5.1 特征歸一化
下面是針對數(shù)據(jù)的歸一化過程,采用的MinMaxScaler方法:
X?=?df.drop("money",axis=1)
y?=?df[["money"]]
#?實例化
mm?=?MinMaxScaler()
data?=?mm.fit_transform(X)
#?歸一化后的數(shù)據(jù)
X?=?pd.DataFrame(data,columns=X.columns.tolist())
X.head()

5.2 數(shù)據(jù)集切分
按照訓練集:測試集 = 8:2的比例進行數(shù)據(jù)集的劃分:
from?sklearn.model_selection?import?train_test_split
X_train,?X_test,?y_train,?y_test?=?train_test_split(
??X,?y,?
??test_size=0.2,??#?比例
??random_state=4??#?隨機狀態(tài)
)?????????????????????????????????????????????????
5.3 特征選擇
在前面的工作我們從相關(guān)系數(shù)和因變量的相關(guān)性大小進行了比較,發(fā)現(xiàn):Size、sizeInside和wei是比較相關(guān)的系數(shù)。
下面是從使用mutual_info_classif來查看每個特征的重要性:
from?sklearn.feature_selection?import?mutual_info_classif
imp?=?pd.DataFrame(mutual_info_classif(X,y),
??????????????????index=X.columns)
imp.columns=['importance']
imp.sort_values(by='importance',ascending=False)

我們發(fā)現(xiàn):shi、wei、zhuangxiu、size都是比較重要的特征
5.4 評價指標

引入多種回歸模型:
#?線性回歸
from?sklearn.linear_model?import?LinearRegression
#?決策樹回歸
from?sklearn.tree?import?DecisionTreeRegressor
#?梯度提升回歸,隨機森林回歸
from?sklearn.ensemble?import?GradientBoostingRegressor,RandomForestRegressor
5.5 不同模型效果
重點關(guān)注模型的r2值
1、線性回歸-LinearRegression

2、決策樹回歸-DecisionTreeRegressor

3、隨機森林回歸-RandomForestRegressor

4、梯度提升回歸-GradientBoostingRegressor

通過3種回歸模型的r2得分比較:GradientBoostingRegressor > DecisionTreeRegressor > RandomForestRegressor > LinearRegression
6、模型可解釋分析
為了更好理解機器學習模型的輸出,下面使用SHAP庫來探索調(diào)參后隨機森林模型的可解釋性。通過pip install shap即可安裝

6.1 隨機森林調(diào)參

進行fit擬合之后便得到了最優(yōu)的參數(shù)組合:
rf_random.best_params_
#?結(jié)果
{'n_estimators':?120,?'max_features':?'auto',?'max_depth':?20}
調(diào)參后隨機森林模型的r2系數(shù)略優(yōu)于調(diào)參前:

建立最佳參數(shù)下的模型:
rf_random?=?RandomForestRegressor(
??n_estimators=180,
??max_features="auto",
??max_depth=10)
rf_random.fit(X_train,?y_train)
6.2 計算shap_values
SHAP value最大的優(yōu)勢是SHAP能對于反映出每一個樣本中的特征的影響力,而且還能夠表現(xiàn)出影響的正負性。
在SHAP中進行模型解釋需要先創(chuàng)建一個explainer,SHAP支持很多類型的explainer
#?1、傳入調(diào)優(yōu)模型rf_random創(chuàng)建explainer
explainer?=?shap.TreeExplainer(rf_random)
#?2、計算shap值
shap_values?=?explainer.shap_values(X_test)
shap_values

6.3 均值對比
1、通過模型預測得到的均值求解
#?y的預測值和真實值比較
y_pred?=?rf_random.predict(X_test)
#?預測值
y_test["pred"]?=?y_pred??
#?money部分是真實值
y_test.head()

其中:money-真實值,pred-隨機森林模型的預測值。得到的均值為:
#?真實的平均值
y_test["pred"].mean()
5614.137953764154
2、通過SHAP得到的基準值base_value
#?shap得到的基準值
y_base?=?explainer.expected_value
y_base
array([5487.93357763])
6.4 整體特征重要性
取出每個特征的shap值的絕對值的平均值作為該特征的重要性,得到水平的條形圖:
shap.summary_plot(shap_values,?
??????????????????X_test,?
??????????????????plot_type="bar")

shap.summary_plot(shap_values,?X_test)

可以看到,通過shap值來看:
- size面積、zone_position區(qū)域位置、shi房間數(shù) 的重要性才是最靠前的,這個結(jié)果可以和相關(guān)系數(shù)的大小進行對比
- 前5個特征中,藍點主要還是集中在SHAP值小于0的區(qū)域
6.5 單個樣本的SHAP值
從數(shù)據(jù)中隨機抽查一條樣本查看shap值:
i=18
df0?=?pd.DataFrame()
df0['feature']?=?X_test.columns.tolist()
df0['feature_value']?=?X_test.iloc[i].values
df0['shap_value']?=?shap_values[i]
df0.head(10)

- 第一列是特征名稱
- 第二列是特征的具體數(shù)值
- 第三列是各個特征在該樣本中對應的SHAP值
注意:一個樣本中各特征 SHAP 值的和加上基線值應該等于該樣本的預測值

單條樣本的特征可視化:
#?可視化
shap.initjs()
shap.force_plot(
??explainer.expected_value,?
??shap_values[i],
??X_test.iloc[i])

紅色表示higher部分:表明針對這條數(shù)據(jù),zone_position和size是重要的特征。下面是換了另一個樣本的結(jié)果:

不同的樣本具有不同的重要屬性
6.6 部分依賴圖-Partial Dependence Plot
1、單個變量的影響
shap.dependence_plot('size',?
?????????????????????shap_values,?
?????????????????????X_test,?
?????????????????????interaction_index=None,?
?????????????????????show=False)

通過圖形我們觀察到:size的取值在0.4以下,size的shap都是相對平和。一旦size達到一定值,對房租價格的拉升作用相當明顯。
2、兩個變量的交互式影響
SHAP同樣能夠查看兩個變量的交互式影響,比如size和zone_position。
shap.dependence_plot(
????'size',?
????shap_values,?
????X_test,
????interaction_index='zone_position',??#?指定
????show=False)

當指定了interaction_index 的取值情況,我們不僅能夠觀察到size和房租的關(guān)系,還可以看到size和zone_position之間存在的關(guān)系:size在分界點(大致在0.4)前后,one_position的相應都比較大(紅色),這同時表明zone_position是一個重要性高的特征~
整理不易,點贊三連↓
