使用k-NN预测房价
目录
绪论
本文的目的是展示如何轻松地使用Scikit Learn(一个流行的Python机器学习库)提供的k-NN回归器和分类器来预测英国的房价,并基于其位置。这是一个直观的、教学性很强的问题,公共数据集很容易获得。
作为奖励,我们还包括一个分类用例,在这个用例中,我们试图根据房子的位置和价格来预测房子的类型(Flat, Terraced, etc.)。
进口
底层库应该首先使用pip3 install <library> 来安装。
1import pandas as pd
2import numpy as np
3import matplotlib
4import matplotlib.colors as colors
5import matplotlib.pyplot as plt
6
7from sklearn.model_selection import train_test_split
8from sklearn.neighbors import KNeighborsClassifier
9from sklearn.neighbors import KNeighborsRegressor
10from sklearn.model_selection import GridSearchCV
数据集
土地注册处的 "售出 "价格 2021年
我们的主要数据集是土地注册处2021年的 "已售 "数据集,其中包含该年的房屋交易,包括每个房产的地址、类型和价格。
我们只对价格、邮编、房产类型(D-独立式、S-半独立式、F-公寓、T-平房、O-其他)以及是否为新建房产(Y/N)感兴趣。我们对逐月和逐年的趋势都不感兴趣。
1house_prices_df = pd.read_csv("pp-2021.csv",
2 usecols=[1,3,4,5],
3 names=["price","postcode","type","new"])
4house_prices_df.head()
| 价格 | 邮政编码 | 类型 | 新建 | |
|---|---|---|---|---|
| 0 | 246000 | NN5 6SQ | S | N |
| 1 | 238000 | NN4 6LS | T | N |
| 2 | 320000 | NN29 7SF | D | N |
| 3 | 560000 | NN11 3TD | S | N |
| 4 | 265000 | NN5 6AH | S | N |
邮政编码到经度和纬度
下一个数据集是邮编与地理坐标的映射,其形式为(邮编,纬度,经度)图元。
1postcodes_df = pd.read_csv("ukpostcodes.csv",usecols=[1,2,3])
2postcodes_df.head()
| 邮政编码 | 纬度 | 经度 | |
|---|---|---|---|
| 0 | AB10 1XG | 57.144165 | -2.114848 |
| 1 | AB10 6RN | 57.137880 | -2.121487 |
| 2 | AB10 7JB | 57.124274 | -2.127190 |
| 3 | AB11 5QN | 57.142701 | -2.093295 |
| 4 | AB11 6UL | 57.137547 | -2.112233 |
我们最后的数据框架,简单地命名为df ,与每个交易的邮编和经纬度坐标相匹配,并将价格除以1000,这样我们就可以用'ks'(千)来推理,这就更容易。
1df = house_prices_df.merge(postcodes_df,how='inner',left_on='postcode',right_on='postcode')
2df['price'] = df['price'] / 1000
3df.head()
| 价格 | 邮政编码 | 类型 | 新 | 纬度 | 经度 | |
|---|---|---|---|---|---|---|
| 0 | 246.0 | NN5 6SQ | S | N | 52.253869 | -0.946142 |
| 1 | 215.0 | NN5 6SQ | S | N | 52.253869 | -0.946142 |
| 2 | 238.0 | NN4 6LS | T | N | 52.198000 | -0.880000 |
| 3 | 311.0 | NN4 6LS | T | N | 52.198000 | -0.880000 |
| 4 | 305.0 | NN4 6LS | D | N | 52.198000 | -0.880000 |
1df['price'].describe()
count 757488.000000
mean 379.990774
std 1972.340933
min 0.001000
25% 171.000000
50% 270.000000
75% 420.000000
max 932540.400000
Name: price, dtype: float64
视觉化
对我们的数据集有一个直观的认识是很有帮助的,我们将其可视化为一个热图,这证明了,正如预期的那样,大伦敦和曼彻斯特等地区的房价相对于英格兰其他地区要高。为了减少噪音,我们使用了一个色标来突出20万到 10万的价格,这样我们就不会因为廉价或昂贵的房子(分别低于20万和高于10万的房子)而导致可视化的扭曲。
1def visualise(df, vmin, vmax):
2 x = df['longitude']
3 y = df['latitude']
4 c = df['price']
5
6 plt.rcParams['figure.figsize'] = [5, 6]
7 plt.rcParams['figure.dpi'] = 100
8
9 plt.scatter(x, y, s=0.01, c=c.sort_values(), cmap='plasma_r',
10 norm=colors.Normalize(vmin=vmin,vmax=vmax), alpha=0.8)
11 plt.colorbar()
12 plt.show()
13
14visualise(df, 200, 1000)
按地点预测房价
第一个目标是创建一个函数,如下所示。
位置 -> 价格
在现实世界中(例如,Rightmove和Zoopla价格估算器的工作方式),我们将包括房屋号码、房间数量等,再加上 "趋势 "方面的考虑,进行计算。
因此,我们基本上是把整个房屋地址归结为它的地理位置,但不仅仅是这样。房子不仅仅是'房子',它们是公寓、独立屋等等。因此,我们将进一步减少我们的搜索空间,只专注于联排别墅。因此,为了简单起见,假设房价预测只适用于排屋。
1# Terraced houses only (i.e., Townhouses in the US)
2prices_df = df[(df['type'] == 'T')]
3# Obtain the average price paid in each postcode
4prices_df = prices_df.groupby(["postcode","latitude","longitude"], as_index=False)["price"].mean()
5prices_df.head()
| 邮政编码 | 纬度 | 经度 | 价格 | |
|---|---|---|---|---|
| 0 | AL1 1AS | 51.749072 | -0.335471 | 608.75 |
| 1 | 辽宁沈阳 | 51.747109 | -0.336542 | 505.00 |
| 2 | 辽宁省 | 51.749497 | -0.336918 | 720.00 |
| 3 | 辽宁沈阳 | 51.749318 | -0.339532 | 775.00 |
| 4 | AL1 1PA | 51.748662 | -0.334472 | 785.00 |
使用k-NN
我们的第一个模型是k-NNN(k-NN)。特别是来自SciKit-Learn的KNeighborsRegressor ,鉴于价格是连续的值。
让我们先把数据集分成训练集和测试集。
1X = prices_df[['latitude','longitude']]
2y = prices_df['price']
3
4X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
k-NN并不是放手不管的。我们需要假设什么是最理想的邻居数量(超参数k),以及每个邻居的距离是否应该均匀,换句话说,超参数的权重。一种方法是用蛮力,也就是说,通过尝试不同的值来找出答案,如下所示。
1uniform = []
2distance = []
3r = range (1,21,2)
4
5for k in r:
6
7 # Euclidan, 'straight' distance
8 model = KNeighborsRegressor(n_neighbors = k, weights='uniform')
9 model.fit(X_train.values, y_train.values)
10 uniform.append(model.score(X_test.values,y_test.values))
11
12 # Distance is inversely proportional (to lessen the weight of outliers)
13 model = KNeighborsRegressor(n_neighbors = k, weights='distance')
14 model.fit(X_train.values, y_train.values)
15 distance.append(model.score(X_test.values,y_test.values))
16
17uniform = np.array(uniform)
18distance = np.array(distance)
19
20plt.rcParams['figure.figsize'] = [10, 3]
21plt.rcParams['figure.dpi'] = 100 # 200 e.g. is really fine, but slower
22plt.plot(r,uniform,label='uniform',color='blue')
23plt.plot(r,distance,label='distance',color='red')
24plt.legend()
25plt.gca().set_xticks(r)
26plt.show()
结果表明,使用超参数k = 15 ,和weight='distance' 将提供最大的准确性。
1pd.DataFrame({"k" : r, "uniform" : uniform, "distance" : distance})
| k | 均匀 | 距离 | |
|---|---|---|---|
| 0 | 1 | 0.557452 | 0.557452 |
| 1 | 3 | 0.608900 | 0.616880 |
| 2 | 5 | 0.626724 | 0.630256 |
| 3 | 7 | 0.655916 | 0.651094 |
| 4 | 9 | 0.656827 | 0.656976 |
| 5 | 11 | 0.661121 | 0.663031 |
| 6 | 13 | 0.657153 | 0.663963 |
| 7 | 15 | 0.660301 | 0.667635 |
| 8 | 17 | 0.658013 | 0.667308 |
| 9 | 19 | 0.654204 | 0.666773 |
不过,这个过程很繁琐,而且对训练数据集做了严格的假设。进一步清洗训练数据集的方法是通过一个叫做交叉验证的过程。GridSearchCV 函数结合了定义分割的数量和通过各种超参数进行迭代的能力。
在我们的例子中,我们没有定义上面看到的迂回循环,而是简单地指定我们想要尝试的数值范围。
1params = {'n_neighbors':range(1,21,2),'weights':['uniform','distance']}
下一步是将k-NN实例、我们的参数(param)束和分割数(cv=5)传递给GridSearchCV 。
1model = GridSearchCV(KNeighborsRegressor(), params, cv=5)
2model.fit(X_train.values,y_train.values)
3model.best_params_
{'n_neighbors': 19, 'weights': 'uniform'}
以上,GridSearchCV ,建议weight = uniform ,就像我们的蛮力过程一样,但k = 19 ,而不是k = 15 。这是因为交叉验证过程做了许多不同轮的采样,不像我们的刚性循环。
1model.score(X_test.values,y_test.values)
0.6542038862362233
我们现在定义我们的位置->价格函数如下。
1def price(description,lat,lon):
2 features = [[lat,lon]]
3 print("{:30s} -> {:5.0f}k ".format(description,float(model.predict(features))))
4
5# Examples
6price('Oxford Circus, London', 51.515276, -0.142038)
7price('Harrods (B. Road), London', 51.499814, -0.163366)
8price('Peak District, National Park', 53.328508, -1.783416)
Oxford Circus, London -> 5531k
Harrods (B. Road), London -> 3956k
Peak District, National Park -> 270k
由位置和价格决定的房屋类型
在上一节中,我们观察了使用k-NN回归器来预测房价的情况。现在让我们使用相同的数据集来处理一个分类问题。我们的目标是根据位置和价格预测房屋类型(独立式、半独立式、平房式等),具体如下。
(位置,价格)->类型
让我们首先创建一个数据集,在这个数据集中,我们可以得到给定位置的每种房屋类型的平均价格。
1
2types_df = df.groupby(["latitude","longitude","type"], as_index=False)["price"].mean()
3types_df.head()
| 纬度 | 经度 | 类型 | 价格 | |
|---|---|---|---|---|
| 0 | 49.912272 | -6.300022 | T | 625.0 |
| 1 | 49.913984 | -6.309107 | S | 475.0 |
| 2 | 49.914130 | -6.312967 | O | 440.0 |
| 3 | 49.914392 | -6.315107 | F | 280.0 |
| 4 | 49.914397 | -6.311555 | F | 255.0 |
接下来,我们将发现最理想的邻居数和权重,使用GridSearchCV 。
1params = {'n_neighbors':range(1,21,2),'weights':['uniform','distance']}
2
3X = types_df[['latitude','longitude','price']]
4y = types_df['type']
5
6X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 0)
7
8model = GridSearchCV(KNeighborsClassifier(), params, cv=5)
9model.fit(X_train.values,y_train.values)
10model.best_params_
{'n_neighbors': 19, 'weights': 'distance'}
得分没有价格预测那么好,但仍有50%的得分,对于第一次尝试来说,这还算不错,没有以任何方式对数据集进行处理。
1model.score(X_test.values,y_test.values)
0.5336038203296112
最后,我们声明一个函数,可以看到预测的实际效果。
1ht = { 'F' : 'Flat', 'T' : 'Terraced', 'S' : 'Semi', 'D' : 'Detached', 'O' : 'Other'}
2
3def house_type(description,lat,lon,price):
4 features = [[lat,lon,price]]
5 print("{:30s} {:5.0f}k -> {}".format(description,price,ht[(model.predict(features)[0])]))
6
7house_type('Oxford Circus, London', 51.515276, -0.142038, 500)
8house_type('Harrods (B. Road), London', 51.499814, -0.163366, 5500)
9house_type('Peak District, National Park', 53.328508, -1.783416, 100)
Oxford Circus, London 500k -> Flat
Harrods (B. Road), London 5500k -> Other
Peak District, National Park 100k -> Terraced
结论
Scikit Learn在预测连续值和类别方面做得很好,分别使用了KNeighborsRegressor() ,和KNeighborsClassifier() ,对于一个关键特征是地理坐标的用例,其中,"距离 "的概念是非常直观的。
使用所有可用的土地注册处的记录,(而不是只有2021年的数据集)将有助于预测 "未来的价格",以及分析新的有趣的模式。例如,公寓和排屋往往在连续的房屋号码方面形成集群。基于门牌号和位置(不含价格)可以实现对房产类型的有效预测,而不一定要有特定门牌号/邮编对的 "售出 "记录。
在你离开之前
🤘 订阅我的100%无垃圾邮件的时事通讯!
电子邮件。