实战五十六:基于机器学习的上海房价预测(完整代码+数据集)
本文旨在深入探讨影响房地产价格的各种因素。其中的数据来源于链家网这一权威房地产信息平台。在应用机器学习技术的过程中引入了多种线性回归模型,并且还引入了一个非线性的预测算法。研究结果表明,在决定房价的因素中最为关键的是房屋面积、地理位置、建造年份以及房屋高度。
问题描述
现在房价持续攀升,在尤其是像上海这样的一线城市更是 sky rocketing prices to an extent that defies imagination. 当人们在购房时往往面临诸多考量,在决定一套房屋的价格时有哪些关键因素起着主导作用?如何能够迅速获取一份对该地区住宅市场价格的大致评估?本文旨在探讨如何利用机器学习技术来构建上海房地产价格预测模型,并通过分析数据来揭示定价的关键要素。
数据收集及处理
数据源选择
经过在网上对几个房价信息网的比较,

安居客房价信息

搜房网房价信息

链家网房价信息
发现不同房地产网站提供的房价信息变化较小。在网络上查阅得知链家网上公布的价格与市场价格基本相符,因此选择了链家网上提供的价格数据作为我们的数据来源。
当前上海市场上的新房供应较为有限,在链家平台仅提供了约400套上海新房的信息数据集规模较小难以支撑有效的机器学习训练因此研究团队选择了利用上海二手住宅的数据来进行建模训练而链家平台提供的上海二手住宅数据量则丰富得多达到数万套完全能够满足机器学习的需求
数据收集
通过编写一个Python程序从链家房地产网站获取房价数据,并被存储到MongoDB数据库中以便后续进行训练。代码具体内容可参考附录部分
数据处理
根据截图内容可知,在处理这些数据时能够使其更加适合用于机器学习模型的训练
首先是房型

可以看到这里可以比较轻松的把房型拆分为几个房间,几个客厅。

针对楼层高度的计算问题,在本例中共有6个楼层高度的数据点。其中位于中间位置的房屋其高度计算方式为6×0.5。而低层与高层房屋的高度则采用相应的系数进行计算:低层采用6×0.2的方式得到其高度值;高层则采用6×0.8的方式进行测量。

对于地址,调用了高德地图的地址查询 API 把它转换成了经纬度信息。
其他的数据信息都比较标准,不需要进行额外的处理。
采用的模型及原因
在模型训练过程中, 将现有的数据集按照 80\% 的比例划分为训练集, 剩余 20\% 的数据则用于测试, 这种做法有助于提升模型性能的评估效果
线性回归模型
线性回归模型是一种基础的房价预测模型,在基于波士顿数据进行机器学习时被采用为基本方法。此外,在实际应用中,默认的做法是为每个特征赋予权重以计算出具体价格
神经网络
当前广泛采用的机器学习方法,在数据训练方面通常表现出色。由于反向传播算法通过不断迭代优化使预测结果逐渐接近真实值。可以通过与线性回归模型进行对比分析来探讨其是否能提供更优的效果。
支持向量机
同样是比较流行的机器学习算法。对于线性模型的训练效果通常情况下会优于线性回归模型。通过这个_model_希望能够进一步提升对线性回归模型的表现。
使用的 python 机器学习库
由于目前 Python 在机器学习领域拥有良好的支持,并且提供了众多可调用的机器学习库可供选择。此外,在我的爬虫程序中也采用了基于机器学习的方法进行开发。因此选择在 Python 的 scikit-learn 库中应用一系列机器学习算法来进行数据训练与预测任务。
Sklearn是一个广泛使用的Python第三方模块,在机器学习领域包含了多种算法。它的官方网站是在GitHub上提供了详细的介绍和源代码。这个库是由Google于2007年发起的一个暑期项目中开发的,并得到了800多位贡献者的合作成果。
建模过程
数据特征分析
总共爬取了 39622 条房源信息,爬取的特征如下:

大多数特征属于 numeric 类型。基于对房价影响因素的分析和了解,我们相信较为关键的特征包括以下几点:房子大小、房子位置以及房子建造年份。
在训练时依次输入的特征如下:房子中的卧室数量、房子中的客厅数量、房屋面积、房屋建设年代、房屋海拔高度、所处大楼的海拔高度、房屋经度坐标、房屋纬度坐标。共有八个特征
调参
在训练的过程中依据训练结果中各特征的重要性高低来进行参数优化剔除非关键的特征后再进行一次完整的模型重新训练直至剩余的所有特征均对结果产生显著影响
结果分析及模型对比
基于训练结果可以看出,在使用线性回归模型、神经网络和SVM算法时所出现的预测值与实际市场价格之间的偏差约为每平方米1.8万元。由此可知该房价的变化呈现出明显的非线性特征。

由上图可见,
其表现优于神经网络,
而神经网络紧随其后,
支持向量机(SVM)表现最差。
观察线性回归的参数设置,
每增加一间卧室和一间起居室,
房屋建造时间越晚,
房价呈现下降趋势。
这一现象明显违背常识,
由此可推断该房价预测模型应属于非线性的范畴
非线性模型建模
非线性决策树
该模型是 sklearn 库中自带的 DecisionTreeRegressor。通过计算得出其预测结果的平均误差约为8500元/平方米。评分值达到0.84分(满分1分)。

结论
从改用非线性模型后的情况来看, 模型运行后的结果呈现出显著提升的趋势。通过分析模型输出的结果权重, 我们可以看出, 房屋面积、建筑年代、房屋高度以及所处地理位置均对房价产生较大影响; 相反, 房间类型以及整栋大楼的高度则对其价格的作用较为有限。进一步观察发现, 房屋经纬度信息在房价预测中具有决定性作用, 这表明上海市区与郊区地区的房价差异十分明显
房价查询界面
先在链家网上随意找一条房源信息

输入查询界面

点击查询进行房价咨询

该系统将展示房子的每平米价格和总价信息。可以看出,在链家网上列出的价格是每平米64960元,在模型中预测得出的结果为每平米61417元,总价差距约40万元。基本上实现了帮助用户了解房价的目的。
改进措施
收集更多数据

数据集在 20000 条以下时的训练学习曲线

数据集在 40000 条以下时的训练学习曲线
从图中可以看到,在训练数据量增加的过程中, 验证集准确率持续提高. 为了进一步优化模型性能, 在后续的数据收集过程中应尽可能多地积累高质量的数据. 目前收集的商品房价格数据数量还不够多, 并且受到部分异常房价信息的影响较大.
寻找更多特征
多种因素会影响房价。例如,在考虑房地产时应关注其周边环境的质量。具体来说,在选择一个房产时应该考察周边区域是否具备完善的交通设施以及附近的大型购物中心是否存在。此外,在评估一个房产的价值时还需要考虑其内部装饰状况如何。如果能在数据集中包含这些特征信息,则有助于提高预测模型的效果。
附录
数据采集
# coding:utf-8 ctrl+/ 注释
import urllib2
from bs4 import BeautifulSoup
import sys
import pymongo
import requests
def geocode(address):
parameters = {'address': address, 'key': '8ac4f59c23c73f503f350494ff9310d3','city':'上海'}
base = 'http://restapi.amap.com/v3/geocode/geo'
try:
response = requests.get(base, parameters,timeout=1)
except:
return {}
answer = response.json()
return answer
reload(sys)
sys.setdefaultencoding("utf-8")
connection = pymongo.MongoClient()
tdb = connection.program
post_info = tdb.house
# 链家网d
def find_data(tmp_url, tmp_district, lists):
count = 0
# 每个区的最大显示页数为100页
for page_Num in range(1, 100):
f_url = tmp_url + tmp_district + "/d" + str(page_Num)
print f_url
# print f_url
f_page = urllib2.urlopen(f_url)
f_soup = BeautifulSoup(f_page, "html.parser")
page_soup = f_soup.find(class_="m-list")
# print page_soup
ul_soup = page_soup.find('ul')
# print ul_soup
li_list = ul_soup.findAll('li')[0:]
# print page_Num
for tr in li_list:
info = tr.findAll(class_="info-row")[0:]
row1 = info[0].find(class_="info-col row1-text").text
row1 = row1.strip()
row1 = row1.replace(' ', '')
# print row1
cut_1 = row1.index('|')
# 几室几厅
house_type = row1[0:cut_1]
#print house_type
cut_2 = row1[cut_1 + 1:].index('平')
# 房屋大小
house_size = float(row1[cut_1 + 1:cut_1 + cut_2 + 1])
#print house_size
try:
cut_3 = row1.index('/')
except ValueError:
continue
cut_4 = row1.index('层')
# 建筑总层高
building_height = float(row1[cut_3 + 1:cut_4])
#print building_height
cut_5 = row1.index('区')
# 房屋层高
house_height = row1[cut_5 - 1:cut_5]
#print house_height
row2 = info[1].find(class_="info-col row2-text")
# 均价
average_price = info[1].find(class_="info-col price-item minor").text.strip()
print average_price
# 位置
location = row2.findAll('a')[0:]
try:
year_1 = row2.text.index('年建')
year = row2.text[year_1 - 4:year_1]
except ValueError:
continue
print year
# 小区
housing_estate = location[0].text
#print housing_estate
# 区县
house_district = location[1].text
#print house_district
count = count + 1
# print count
#
#
#整理出room和parlour数量
room_1=house_type.index('室')
room_number=int(house_type[0:room_1])
parlour_1=house_type.index('厅')
parlour_number=int(house_type[room_1+1:parlour_1])
#判断房子的具体高度
if(house_height=='中'):
house_height_inlist=building_height*0.5
elif(house_height=='高'):
house_height_inlist = building_height * 0.88
elif(house_height == '低'):
house_height_inlist = building_height * 0.23
#整理出具体房价
price_1=average_price.index('价')
price_2=average_price.index('元')
average_price_inlist=float(average_price[price_1+1:price_2])
#计算地址经纬度
address = house_district+housing_estate
print address
house_first_location=geocode(address)
if "geocodes" not in house_first_location.keys():
print "no geocodes"
continue
house_first_location=house_first_location["geocodes"]
#print house_first_location
if(house_first_location==[]):
continue
house_location=house_first_location[0]['location'].encode('utf-8')
print house_location
house_location_1=house_location.index(',')
house_location_longtitude=float(house_location[0:house_location_1])
print house_location_longtitude
house_location_latitude=float(house_location[house_location_1+1:])
print house_location_latitude
#house_location_latitude,house_location_longtitude,
list_use = [room_number,parlour_number, house_size, building_height, house_height_inlist,float(year),house_location_longtitude,house_location_latitude,house_district,address,
average_price_inlist]
#print list_use
lists.extend([list_use])
print "{\"户型\":\"%s\", \"大小\":\"%s\",\"楼高\":\"%s\",\"层高\":\"%s\",\"小区名\":\"%s\",\"市区\":\"%s\",\"均价\":\"%s\"}" % (
house_type, house_size, building_height, house_height, housing_estate, house_district, average_price)
data = {"room_number": room_number, "parlour_number": parlour_number,"house_size":house_size,"building_height": building_height,"house_height_inlist": house_height_inlist,"year": float(year),"house_location_longtitude": house_location_longtitude,"house_location_latitude":house_location_latitude,"house_district":house_district,"address":address,"average_price_inlist": average_price_inlist}
post_info.save(data)
# print "end"
# print "all_end"
def use(district):
lists = []
for i in district:
find_data('http://sh.lianjia.com/ershoufang/', i, lists)
return lists
a = ["pudongxinqu", "minhang", "baoshan", "xuhui", "putuo", "yangpu", "changning", "songjiang", "jiading", "huangpu",
"jingan", "zhabei", "hongkou", "qingpu", "fengxian", "jinshan", "chongming"]
if __name__ == '__main__':
dataset = use(a)
代码解读
数据训练
# coding:utf-8 ctrl+/ 注释
import sys
import pymongo
import numpy as np
import price_mongo
reload(sys)
sys.setdefaultencoding("utf-8")
connection = pymongo.MongoClient()
tdb = connection.program
post_info = tdb.house
lists=[]
count=0
for item in post_info.find():
count+=1
single_item=[item["room_number"],item["parlour_number"],item["house_size"],item["year"],
item["building_height"],item["house_height_inlist"],item["house_location_longtitude"],
item["house_location_latitude"],item["average_price_inlist"]]
lists.extend([single_item])
#print count
from sklearn import metrics
from sklearn import preprocessing
from sklearn.linear_model import LinearRegression
from sklearn.cross_validation import train_test_split
_array = np.array(lists)
x = _array[:, 0:8]
#normalized_X=preprocessing.normalize(x)
y = _array[:, 8]
#normalized_Y=preprocessing.normalize(y)
X_train, X_test, y_train, y_test = train_test_split(x, y,test_size=0.2)
print "训练集大小%d"%(X_train.shape[0])
print "测试集大小%d"%(X_test.shape[0])
#线性回归
clf = LinearRegression()
clf.fit(X_train, y_train)
print "线性回归模型参数:"
print clf.coef_
y_pred = clf.predict(X_test)
print ("线性回归误差:%d元"%(np.sqrt(metrics.mean_squared_error(y_test, y_pred))))
#神经网络
from sklearn.neural_network import MLPRegressor
kmodel = MLPRegressor(learning_rate='adaptive',max_iter=2000).fit(X_train, y_train)
y_kmodel_pred = kmodel.predict(X_test)
print ("神经网络误差:%d元"%np.sqrt(metrics.mean_squared_error(y_test, y_kmodel_pred)))
#svm模型
from sklearn import svm
smodel=svm.LinearSVR()
smodel.fit(X_train, y_train)
y_smodel_pred=smodel.predict(X_test)
print "svm误差:%d元"%np.sqrt(metrics.mean_squared_error(y_test, y_smodel_pred))
#决策树模型
from sklearn.tree import DecisionTreeRegressor
dmodel=DecisionTreeRegressor()
dmodel.fit(X_train, y_train)
y_dmodel_pred=dmodel.predict(X_test)
print "非线性决策树误差:%d元"%np.sqrt(metrics.mean_squared_error(y_test, y_dmodel_pred))
print "非线性决策树误差评分:%f"%dmodel.score(X_test,y_test)
print "各参数权重:"
print dmodel.feature_importances_
# coding:utf-8 ctrl+/ 注释
import wx
app=wx.App()
win =wx.Frame(None,title="房价查询",size=(420,335))
win.Show()
btn=wx.Button(win,label='查询',pos=(315,5),size=(80,25))
house_size_input=wx.TextCtrl(win,pos=(80,5),size=(210,25))
house_size_input_label=wx.StaticText(win,label='房屋大小',pos=(10,10),size=(60,30))
room_number_input=wx.TextCtrl(win,pos=(80,45),size=(210,25))
room_number_input_label=wx.StaticText(win,label='房间数',pos=(10,50),size=(60,30))
parlour_number_input=wx.TextCtrl(win,pos=(80,85),size=(210,25))
parlour_number_input_label=wx.StaticText(win,label='厅数',pos=(10,90),size=(60,30))
house_height_input=wx.TextCtrl(win,pos=(80,125),size=(210,25))
house_height_input_label=wx.StaticText(win,label='层高',pos=(10,130),size=(60,30))
building_height_input=wx.TextCtrl(win,pos=(80,165),size=(210,25))
building_height_input_label=wx.StaticText(win,label='楼高',pos=(10,170),size=(60,30))
year_input=wx.TextCtrl(win,pos=(80,205),size=(210,25))
year_input_label=wx.StaticText(win,label='建造年份',pos=(10,210),size=(60,30))
address_input=wx.TextCtrl(win,pos=(80,245),size=(210,25))
address_input_label=wx.StaticText(win,label='地址',pos=(10,250),size=(60,30))
def search(event):
hz = house_size_input.Value
rn=room_number_input.Value
pn=parlour_number_input.Value
hh=house_height_input.Value
bh=building_height_input.Value
yi=year_input.Value
ai=address_input.Value
location=price_mongo.geocode(ai)["geocodes"][0]['location'].encode('utf-8')
house_location_1=location.index(',')
house_location_longtitude=float(location[0:house_location_1])
house_location_latitude=float(location[house_location_1+1:])
list=[]
list.extend([[hz,rn,pn,hh,bh,yi,house_location_longtitude,house_location_latitude]])
out_price=dmodel.predict(list)
print out_price,float(out_price[0])*float(hz)
#print hz,rn,pn,hh,bh,yi,house_location_longtitude,house_location_latitude
btn.Bind(wx.EVT_BUTTON,search)
app.MainLoop()
代码解读
完整代码:<>
