連載的上兩篇文章,小魚為大家介紹了 KNN 算法的原理,并使用 Python 實現(xiàn)了單變量 KNN 模型的租金預測。此外還介紹了訓練集和測試集的劃分,并使用 RMSE 來評估實現(xiàn)的 KNN 模型。
本節(jié),小魚將為大家?guī)矶嘧兞繉崿F(xiàn)的 KNN 模型。
讀取數(shù)據(jù)集
首先,我們讀取數(shù)據(jù)集,并進行洗牌,將 price 列的數(shù)據(jù)類型轉換為 float:
import pandas as pd
features = ['accommodates','bedrooms','bathrooms','beds','price','minimum_nights','maximum_nights','number_of_reviews']
df = pd.read_csv('listings.csv')[features]
df = df.sample(frac=1, random_state=0)
df.price = df.price.str.replace('\$|,','').astype(float)
df.head()
讀取的數(shù)據(jù)集如下:

查看數(shù)據(jù)集的信息:
>> df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 3723 entries, 2645 to 2732
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 accommodates 3723 non-null int64
1 bedrooms 3702 non-null float64
2 bathrooms 3696 non-null float64
3 beds 3712 non-null float64
4 price 3723 non-null float64
5 minimum_nights 3723 non-null int64
6 maximum_nights 3723 non-null int64
7 number_of_reviews 3723 non-null int64
dtypes: float64(4), int64(4)
memory usage: 261.8 KB
通過 Non-Null Count 我們發(fā)現(xiàn)很多列的非空值個數(shù)都小于樣本數(shù) 3723 ,使用 dropna 將包含缺失值的樣本刪除:
>> df.dropna(inplace=True)
>> df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 3671 entries, 2645 to 2732
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 accommodates 3671 non-null int64
1 bedrooms 3671 non-null float64
2 bathrooms 3671 non-null float64
3 beds 3671 non-null float64
4 price 3671 non-null float64
5 minimum_nights 3671 non-null int64
6 maximum_nights 3671 non-null int64
7 number_of_reviews 3671 non-null int64
dtypes: float64(4), int64(4)
memory usage: 258.1 KB
數(shù)據(jù)預處理 - 標準化
我們接下來要進行多變量的 KNN 模型預測,那就須要平等地對待每一個特征,而不能主觀地認為某些特征重要,某些特征不重要。那如何平等地對待每個特征呢?
我們來想一下,KNN 是通過計算距離,選取離自己最近的 K 個樣本,那在計算距離的時候:

如果特征 q1 的值本身就是比較大的數(shù),比如房屋的面積,而特征 q2 的值本身都是比較小的數(shù),比如衛(wèi)生間的數(shù)量,那么在計算距離的時候,是不是 (q1-p1)**2 也要遠大于 (q2-p2)**2 的值呢?
這樣的話,我們在計算距離的時候,最終的距離基本上都是由值比較大的 q1 特征所決定的。
注:在最開始的時候,我們認為數(shù)據(jù)之間的重要層度是一樣的,并不想偏袒哪個特征,所以標準化 / 歸一化是必要的步驟。
為此,數(shù)據(jù)預處理工作中有一步非常重要,就是數(shù)據(jù)的標準化、歸一化。
數(shù)據(jù)的標準化
標準化是指將所有特征都變化為均值為 0,標準差為 1 的分布:

數(shù)據(jù)進行標準化之后,特征的分布將會關于原點對稱,并且特征之的取值范圍都是在一個較小的區(qū)間上浮動的。
數(shù)據(jù)的歸一化
另一種方法叫做歸一化:

特征經(jīng)過上述變換之后,必然會位于 0-1 的區(qū)間,這樣做可以抑制離群值對結果的影響。
下面,我們使用 sklearn 庫為我們提供的數(shù)據(jù)標準化工具 StandardScaler:
>> from sklearn.preprocessing import StandardScaler
>> StandardScaler().fit_transform(df)
array([[-0.09760238, -0.2495011 , -0.43921129, ..., 1.31682367,
-0.01660143, 0.30422279],
[ 1.89848862, 0.94000348, 1.26516995, ..., -0.34142104,
-0.01657476, -0.48257057],
[-0.09760238, -0.2495011 , -0.43921129, ..., -0.06504692,
-0.01657476, 0.03055553],
...,
[-1.09564788, -0.2495011 , 4.67393243, ..., 0.48770132,
-0.01659621, -0.37994535],
[-0.59662513, -0.2495011 , -0.43921129, ..., -0.06504692,
-0.01660623, 10.1220355 ],
[ 2.39751137, 2.12950806, 1.26516995, ..., -0.34142104,
-0.01657476, -0.51677897]])
注:sklearn 庫是最受歡迎的機器學習庫,它為我們提供了非常多實用的機器學習模型、數(shù)據(jù)預處理工具等等。網(wǎng)址附上:https://scikit-learn.org/stable/
經(jīng)過標準化之后,數(shù)據(jù)變得有正有負,并且整體都比較小了。這里,我們需要注意的是 StandardScaler().fit_transform(df) 返回的是 ndarray 數(shù)組結構,我們可以借助賦值的技巧,轉換為 DataFrame:
scaler = StandardScaler()
df[features] = scaler.fit_transform(df[features])
df.head()
數(shù)據(jù)預處理結果:

這個時候,我們可能會有一個疑問?數(shù)據(jù)標準化之后,預測到的價格似乎也是個沒有實際意義的數(shù)字,那還能轉換成本來的價格嗎?
使用 scaler.inverse_transform 即可得到標準化之前的數(shù)值:
>> scaler.inverse_transform(df)[0]
array([ 3., 1., 1., 1., 75., 7., 180., 24.])
使用 Python 實現(xiàn)多變量 KNN
數(shù)據(jù)預處理完成之后,我們就可以將數(shù)據(jù)集劃分為訓練集和測試集,來進行模型的構建了。
下面代碼中的 pridict_price 函數(shù)實現(xiàn)了 KNN 模型,接收一個樣本和選用的用本特征,返回預測的結果值。
from scipy.spatial import distance
norm_train_df = df.copy().iloc[:2792]
norm_test_df = df.copy().iloc[2792:]
def predict_price(my_sample, features):
norm_train_df['distance'] = distance.cdist(
norm_train_df[features],
[my_sample[features]]
)
knn_5 = norm_train_df.sort_values('distance').price.iloc[:5]
return knn_5.mean()
其中距離的計算,使用了 scipy 提供的工具 dictance.cdist ,cdist 的第二個參數(shù)外面加了一層 [] ,這是因為 cdict 的參數(shù)必須是兩個 2 維的數(shù)組或集合。
cols = ['accommodates', 'bathrooms']
norm_test_df['predict_price'] = norm_test_df[cols].apply(
predict_price,
features=cols,
axis=1
)
norm_test_df[['price', 'predict_price']].head()
apply 方法將 predict_price 函數(shù)應用到測試集中的每個樣本上,計算測試集樣本的預測接歌。最終得到的測試集預測價格如下:

最后,通過預測值和真實值之間的 RMSE 來評估上述 KNN 模型:
>> norm_test_df['squared_error'] = (norm_test_df.predict_price - norm_test_df.price) ** 2
>> mse = norm_test_df.squared_error.mean()
>> rmse = mse ** (1/2)
>> rmse
0.8112504245382595
使用 Sklearn 實現(xiàn) KNN
使用 sklearn 庫實現(xiàn) KNN 模型的步驟非常簡單:首先我們此處的任務是預測租金,因此是一個回歸問題,導入 KNeighborsRegressor 創(chuàng)建回歸問題的 KNN 模型。
from sklearn.neighbors import KNeighborsRegressor
knn = KNeighborsRegressor()
訓練模型:分別傳入訓練集的特征數(shù)據(jù)集和預測數(shù)據(jù)
cols = ['accommodates','bedrooms']
knn.fit(norm_train_df[cols], norm_train_df['price'])
使用測試集預測結果:
>> knn.predict(norm_test_df[cols])[:5]
array([ 0.6293575 , 1.03653751, 0.49266135, -0.24026267, -0.42058525])
評估模型:使用 sklearn.metrics 提供的 mean_squared_error 計算均方誤差 SME,開根號就是均方根誤差 RMSE:
>> from sklearn.metrics import mean_squared_error
>> mean_squared_error(norm_test_df['price'], knn.predict(norm_test_df[cols])) ** (1/2)
0.8305992928886629
下面,我們嘗試加入更多的特征來構建 KNN 回歸模型:
>> knn = KNeighborsRegressor()
>> cols = ['accommodates','bedrooms','bathrooms','beds','minimum_nights','maximum_nights','number_of_reviews']
>> knn.fit(norm_train_df[cols], norm_train_df.price)
>> mean_squared_error(norm_test_df['price'], knn.predict(norm_test_df[cols]) )** (1/2)
0.7326399776714614
加入更多特征之后,RMSE 由之前兩個特征時的 0.83 下降到了 0.73,誤差變小了,模型的準確度得到了提高。
KNN 算法的缺陷
K 近鄰算法的簡單之處在于不需要訓練模型,數(shù)據(jù)做好,直接拿來用就行。
但卻有一個致命的缺點,每次來一條數(shù)據(jù),都要遍歷一次訓練集,找到最接近輸入樣本的數(shù)據(jù),當訓練集樣本數(shù)非常龐大的時候,KNN 的速度將會非常慢,這也是實際應用場景中使用受限的原因。