一、选题背景:
二手房市场又称存量房地产市场。随着国家对新建商品房市场调控力度的加大和存量房市场的逐步扩大,进入市场的二手房数量不断增加,住房者根据自己的经济收入的变化逐步单反提高住房的品质。从租赁市场到二手市场到商品房市场,商品房市场还分安置型、实用型、舒适型级别。所以说住房市场形成了阶梯消费模式。二手房市场分析,二手房市场在阶梯中处于承前启后的作用,它关系着一、二、三级市场的联动效应,为整个房地产消费以及经济增长发挥着重要的作用。
二、数据来源:
通过爬虫采集的链家全网北京二手房数据。
scrapy爬取链家
房源信息
确定以上爬取内容后,就开始爬虫部分的工作。首先在item.py文件中定义一个子类,该子类继承了父类scrapy.Item,然后在子类中用scrapy.Field()定义以上信息的字段。如下代码,将所有需要的字段信息都设置好。
import scrapy
class LianjiaSpiderItem(scrapy.Item):
# define the fields for your item here like:
Id = scrapy.Field()
Region = scrapy.Field()
Garden = scrapy.Field()
Layout = scrapy.Field()
Size = scrapy.Field()
Direction = scrapy.Field()
Renovation = scrapy.Field()
Elevator = scrapy.Field()
Floor = scrapy.Field()
Year = scrapy.Field()
Price = scrapy.Field()
District = scrapy.Field()
pass
在spider文件夹下的爬取文件(自定义)中导入所需库,如下代码:
- json:json格式的转换;
- scrapy:scrapy库;
- logging:日志;
- BeautifulSoup:使用bs4提取网页信息;
- table:settings中自设的一个字典;
- LianjiaSpiderItem:字段Field;
# -*- coding:utf-8 -*-
import json
import scrapy
import logging
from bs4 import BeautifulSoup
from lianjia_spider.settings import table
from lianjia_spider.items import LianjiaSpiderItem
具体实现:
def parse(self, response):
item = LianjiaSpiderItem()
soup = BeautifulSoup(response.body, "html.parser")
#获取到所有子列表的信息
house_info_list = soup.find_all(name="li", class_="clear")
# 通过url辨认所在区域
url = response.url
url = url.split(\'/\')
item[\'Region\'] = table[url[-3]]
for info in house_info_list:
item[\'Id\'] = info.a[\'data-housecode\']
house_info = info.find_all(name="div", class_="houseInfo")[0]
house_info = house_info.get_text()
house_info = house_info.replace(\' \', \'\')
house_info = house_info.split(\'/\')
# print(house_info)
try:
item[\'Garden\'] = house_info[0]
item[\'Layout\'] = house_info[1]
item[\'Size\'] = house_info[2]
item[\'Direction\'] = house_info[3]
item[\'Renovation\'] = house_info[4]
if len(house_info) > 5:
item[\'Elevator\'] = house_info[5]
else:
item[\'Elevator\'] = \'\'
except:
print("数据保存错误")
position_info = info.find_all(name=\'div\', class_=\'positionInfo\')[0]
position_info = position_info.get_text()
position_info = position_info.replace(\' \', \'\')
position_info = position_info.split(\'/\')
# print(position_info)
try:
item[\'Floor\'] = position_info[0]
item[\'Year\'] = position_info[1]
item[\'District\'] = position_info[2]
except:
print("数据保存错误")
price_info = info.find_all("div", class_="totalPrice")[0]
item[\'Price\'] = price_info.span.get_text()
yield item
三、实施过程及代码:
首先导入要使用的科学计算包numpy,pandas,可视化matplotlib,seaborn,以及机器学习包sklearn。
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib as mpl
import matplotlib.pyplot as plt
from IPython.display import display
plt.style.use("fivethirtyeight")
sns.set_style({\'font.sans-serif\':[\'simhei\',\'Arial\']})
%matplotlib inline
# 检查Python版本
from sys import version_info
if version_info.major != 3:
raise Exception(\'请使用Python 3 来完成此项目\')
然后导入数据,并进行初步的观察,这些观察包括了解数据特征的缺失值,异常值,以及大概的描述性统计。
# 导入链家二手房数据
lianjia_df = pd.read_csv(\'lianjia.csv\')
display(lianjia_df.head(n=2))
初步观察到一共有11个特征变量,Price 在这里是我们的目标变量。
# 检查缺失值情况
lianjia_df.info()
发现了数据集一共有23677条数据,其中Elevator特征有明显的缺失值
lianjia_df.describe()
上面结果给出了特征值是数值的一些统计值,包括平均数,标准差,中位数,最小值,最大值,25%分位数,75%分位数。
# 添加新特征房屋均价
df = lianjia_df.copy()
df[\'PerPrice\'] = lianjia_df[\'Price\']/lianjia_df[\'Size\']
# 重新摆放列位置
columns = [\'Region\', \'District\', \'Garden\', \'Layout\', \'Floor\', \'Year\', \'Size\', \'Elevator\', \'Direction\', \'Renovation\', \'PerPrice\', \'Price\']
df = pd.DataFrame(df, columns = columns)
# 重新审视数据集
display(df.head(n=2))
我们发现 Id 特征其实没有什么实际意义,所以将其移除。由于房屋单价分析起来比较方便,简单的使用总价/面积就可得到,所以增加一个新的特征 PerPrice(只用于分析,不是预测特征)。
数据可视化分析:
Region特征分析
对于区域特征,我们可以分析不同区域房价和数量的对比。
# 对二手房区域分组对比二手房数量和每平米房价
df_house_count = df.groupby(\'Region\')[\'Price\'].count().sort_values(ascending=False).to_frame().reset_index()
df_house_mean = df.groupby(\'Region\')[\'PerPrice\'].mean().sort_values(ascending=False).to_frame().reset_index()
f, [ax1,ax2,ax3] = plt.subplots(3,1,figsize=(20,15))
sns.barplot(x=\'Region\', y=\'PerPrice\', palette="Blues_d", data=df_house_mean, ax=ax1)
ax1.set_title(\'北京各大区二手房每平米单价对比\',fontsize=15)
ax1.set_xlabel(\'区域\')
ax1.set_ylabel(\'每平米单价\')
sns.barplot(x=\'Region\', y=\'Price\', palette="Greens_d", data=df_house_count, ax=ax2)
ax2.set_title(\'北京各大区二手房数量对比\',fontsize=15)
ax2.set_xlabel(\'区域\')
ax2.set_ylabel(\'数量\')
sns.boxplot(x=\'Region\', y=\'Price\', data=df, ax=ax3)
ax3.set_title(\'北京各大区二手房房屋总价\',fontsize=15)
ax3.set_xlabel(\'区域\')
ax3.set_ylabel(\'房屋总价\')
plt.show()
使用了pandas的网络透视功能 groupby 分组排序。区域特征可视化直接采用 seaborn 完成,颜色使用调色板 palette 参数,颜色渐变,越浅说明越少,反之越多。可以观察到:
-
-
二手房均价:西城区的房价最贵均价大约11万/平,因为西城在二环以里,且是热门学区房的聚集地。其次是东城大约10万/平,然后是海淀大约8.5万/平,其它均低于8万/平。
-
-
二手房房数量:从数量统计上来看,目前二手房市场上比较火热的区域。海淀区和朝阳区二手房数量最多,差不多都接近3000套,毕竟大区,需求量也大。然后是丰台区,近几年正在改造建设,有赶超之势。
-
二手房总价:通过箱型图看到,各大区域房屋总价中位数都都在1000万以下,且房屋总价离散值较高,西城最高达到了6000万,说明房屋价格特征不是理想的正太分布
Size特征分析
f, [ax1,ax2] = plt.subplots(1, 2, figsize=(15, 5))
# 建房时间的分布情况
sns.distplot(df[\'Size\'], bins=20, ax=ax1, color=\'r\')
sns.kdeplot(df[\'Size\'], shade=True, ax=ax1)
# 建房时间和出售价格的关系
sns.regplot(x=\'Size\', y=\'Price\', data=df, ax=ax2)
plt.show()
-
Size 分布:
通过 distplot 和 kdeplot 绘制柱状图观察 Size 特征的分布情况,属于长尾类型的分布,这说明了有很多面积很大且超出正常范围的二手房。
-
Size 与 Price 的关系:
通过 regplot 绘制了 Size 和 Price 之间的散点图,发现 Size 特征基本与Price呈现线性关系,符合基本常识,面积越大,价格越高。但是有两组明显的异常点:1. 面积不到10平米,但是价格超出10000万;2. 一个点面积超过了1000平米,价格很低,需要查看是什么情况
- df.loc[df[\'Size\']< 10]
-
经过查看发现这组数据是别墅,出现异常的原因是由于别墅结构比较特殊(无朝向无电梯),字段定义与二手商品房不太一样导致爬虫爬取数据错位。也因别墅类型二手房不在我们的考虑范围之内,故将其移除再次观察Size分布和Price关系。
- df.loc[df[\'Size\']>1000]
-
经观察这个异常点不是普通的民用二手房,很可能是商用房,所以才有1房间0厅确有如此大超过1000平米的面积,这里选择移除。
- df = df[(df[\'Layout\']!=\'叠拼别墅\')&(df[\'Size\']<1000)]
-
Layout特征分析
- f, ax1= plt.subplots(figsize=(20,20))
sns.countplot(y=\'Layout\', data=df, ax=ax1)
ax1.set_title(\'房屋户型\',fontsize=15)
ax1.set_xlabel(\'数量\')
ax1.set_ylabel(\'户型\')
plt.show() -
Renovation 特征分析
- df[\'Renovation\'].value_counts()
-
精装 11345 简装 8497 其他 3239 毛坯 576
南北 20
Name: Renovation, dtype: int64 - # 去掉错误数据“南北”,因为爬虫过程中一些信息位置为空,导致“Direction”的特征出现在这里,需要清除或替换
df[\'Renovation\'] = df.loc[(df[\'Renovation\'] != \'南北\'), \'Renovation\']
# 画幅设置
f, [ax1,ax2,ax3] = plt.subplots(1, 3, figsize=(20, 5))
sns.countplot(df[\'Renovation\'], ax=ax1)
sns.barplot(x=\'Renovation\', y=\'Price\', data=df, ax=ax2)
sns.boxplot(x=\'Renovation\', y=\'Price\', data=df, ax=ax3)
plt.show() -
观察到,精装修的二手房数量最多,简装其次,也是我们平日常见的。而对于价格来说,毛坯类型却是最高,其次是精装修。
Elevator 特征分析
初探数据的时候,我们发现 Elevator 特征是有大量缺失值的,这对于我们是十分不利的,首先我们先看看有多少缺失值:misn = len(df.loc[(df[\'Elevator\'].isnull()), \'Elevator\'])
print(\'Elevator缺失值数量为:\'+ str(misn))Elevator 缺失值数量为:8237
这么多的缺失值怎么办呢?这个需要根据实际情况考虑,常用的方法有平均值/中位数填补法,直接移除,或者根据其他特征建模预测等。
这里我们考虑填补法,但是有无电梯不是数值,不存在平均值和中位数,怎么填补呢?这里给大家提供一种思路:就是根据楼层 Floor 来判断有无电梯,一般的楼层大于6的都有电梯,而小于等于6层的一般都没有电梯。有了这个标准,那么剩下的就简单了。
# 由于存在个别类型错误,如简装和精装,特征值错位,故需要移除
df[\'Elevator\'] = df.loc[(df[\'Elevator\'] == \'有电梯\')|(df[\'Elevator\'] == \'无电梯\'), \'Elevator\']
# 填补Elevator缺失值
df.loc[(df[\'Floor\']>6)&(df[\'Elevator\'].isnull()), \'Elevator\'] = \'有电梯\'
df.loc[(df[\'Floor\']<=6)&(df[\'Elevator\'].isnull()), \'Elevator\'] = \'无电梯\'
f, [ax1,ax2] = plt.subplots(1, 2, figsize=(20, 10))
sns.countplot(df[\'Elevator\'], ax=ax1)
ax1.set_title(\'有无电梯数量对比\',fontsize=15)
ax1.set_xlabel(\'是否有电梯\')
ax1.set_ylabel(\'数量\')
sns.barplot(x=\'Elevator\', y=\'Price\', data=df, ax=ax2)
ax2.set_title(\'有无电梯房价对比\',fontsize=15)
ax2.set_xlabel(\'是否有电梯\')
ax2.set_ylabel(\'总价\')
plt.show() -
结果观察到,有电梯的二手房数量居多一些,毕竟高层土地利用率比较高,适合北京庞大的人群需要,而高层就需要电梯。相应的,有电梯二手房房价较高,因为电梯前期装修费和后期维护费包含内了(但这个价格比较只是一个平均的概念,比如无电梯的6层豪华小区当然价格更高了)。
Year 特征分析grid = sns.FacetGrid(df, row=\'Elevator\', col=\'Renovation\', palette=\'seismic\',size=4)
grid.map(plt.scatter, \'Year\', \'Price\')
grid.add_legend()在Renovation和Elevator的分类条件下,使用 FaceGrid 分析 Year 特征,观察结果如下:
-
整个二手房房价趋势是随着时间增长而增长的;
-
2000年以后建造的二手房房价相较于2000年以前有很明显的价格上涨;
-
1980年之前几乎不存在有电梯二手房数据,说明1980年之前还没有大面积安装电梯;
-
1980年之前无电梯二手房中,简装二手房占绝大多数,精装反而很少;
Floor 特征分析f, ax1= plt.subplots(figsize=(20,5))
sns.countplot(x=\'Floor\', data=df, ax=ax1)
ax1.set_title(\'房屋户型\',fontsize=15)
ax1.set_xlabel(\'数量\')
ax1.set_ylabel(\'户型\')
plt.show()结论:
-
-
可以看到,6层二手房数量最多,但是单独的楼层特征没有什么意义,因为每个小区住房的总楼层数都不一样,我们需要知道楼层的相对意义。另外,楼层与文化也有很重要联系,比如中国文化七上八下,七层可能受欢迎,房价也贵,而一般也不会有4层或18层。当然,正常情况下中间楼层是比较受欢迎的,价格也高,底层和顶层受欢迎度较低,价格也相对较低。所以楼层是一个非常复杂的特征,对房价影响也比较大。
四、建模过程:
- 特征工程
- 特征工程
3"""
4# 移除结构类型异常值和房屋大小异常值
5df = df[(df[\'Layout\']!=\'叠拼别墅\')&(df[\'Size\']<1000)]
6
7# 去掉错误数据“南北”,因为爬虫过程中一些信息位置为空,导致“Direction”的特征出现在这里,需要清除或替换
8df[\'Renovation\'] = df.loc[(df[\'Renovation\'] != \'南北\'), \'Renovation\']
9
10# 由于存在个别类型错误,如简装和精装,特征值错位,故需要移除
11df[\'Elevator\'] = df.loc[(df[\'Elevator\'] == \'有电梯\')|(df[\'Elevator\'] == \'无电梯\'), \'Elevator\']
12
13# 填补Elevator缺失值
14df.loc[(df[\'Floor\']>6)&(df[\'Elevator\'].isnull()), \'Elevator\'] = \'有电梯\'
15df.loc[(df[\'Floor\']<=6)&(df[\'Elevator\'].isnull()), \'Elevator\'] = \'无电梯\'
16
17# 只考虑“室”和“厅”,将其它少数“房间”和“卫”移除
18df = df.loc[df[\'Layout\'].str.extract(\'^\d(.*?)\d.*?\') == \'室\']
19
20# 提取“室”和“厅”创建新特征
21df[\'Layout_room_num\'] = df[\'Layout\'].str.extract(\'(^\d).*\', expand=False).astype(\'int64\')
22df[\'Layout_hall_num\'] = df[\'Layout\'].str.extract(\'^\d.*?(\d).*\', expand=False).astype(\'int64\')
23
24# 按中位数对“Year”特征进行分箱
25df[\'Year\'] = pd.qcut(df[\'Year\'],8).astype(\'object\')
26
27# 对“Direction”特征
28d_list_one = [\'东\',\'西\',\'南\',\'北\']
29d_list_two = [\'东西\',\'东南\',\'东北\',\'西南\',\'西北\',\'南北\']
30d_list_three = [\'东西南\',\'东西北\',\'东南北\',\'西南北\']
31d_list_four = [\'东西南北\']
32df[\'Direction\'] = df[\'Direction\'].apply(direct_func)
33df = df.loc[(df[\'Direction\']!=\'no\')&(df[\'Direction\']!=\'nan\')]
34
35# 根据已有特征创建新特征
36df[\'Layout_total_num\'] = df[\'Layout_room_num\'] + df[\'Layout_hall_num\']
37df[\'Size_room_ratio\'] = df[\'Size\']/df[\'Layout_total_num\']
38
39# 删除无用特征
40df = df.drop([\'Layout\',\'PerPrice\',\'Garden\'],axis=1)
41
42# 对于object特征进行onehot编码
43df,df_cat = one_hot_encoder(df) - Layout
- # 只考虑“室”和“厅”,将其它少数“房间”和“卫”移除
2df = df.loc[df[\'Layout\'].str.extract(\'^\d(.*?)\d.*?\') == \'室\']
3
4# 提取“室”和“厅”创建新特征
5df[\'Layout_room_num\'] = df[\'Layout\'].str.extract(\'(^\d).*\', expand=False).astype(\'int64\')
6df[\'Layout_hall_num\'] = df[\'Layout\'].str.extract(\'^\d.*?(\d).*\', expand=False).astype(\'int64\') - Year
- 我们还有一个 Year 特征,为建房的年限时间。年限种类很多,分布在1950和2018之间,如果每个不同的 Year 值都作为特征值,我们并不能找出 Year 对 Price 有什么影响,因为年限划分的太细了。因此,我们只有将连续数值型特征 Year 离散化,做分箱处理。
- # 按中位数对“Year”特征进行分箱
df[\'Year\'] = pd.qcut(df[\'Year\'],8).astype(\'object\') -
# 移除结构类型异常值和房屋大小异常值
df = df[(df[\'Layout\'] != \'叠拼别墅\') & (df[\'Size\'] < 1000)]
# 去掉错误数据“南北”,因为爬虫过程中一些信息位置为空,导致“Direction”的特征出现在这里,需要清除或替换
df[\'Renovation\'] = df.loc[(df[\'Renovation\'] != \'南北\'), \'Renovation\']
# 由于存在个别类型错误,如简装和精装,特征值错位,故需要移除
df[\'Elevator\'] = df.loc[(df[\'Elevator\'] == \'有电梯\') | (df[\'Elevator\'] == \'无电梯\'), \'Elevator\']
# 填补Elevator缺失值
df.loc[(df[\'Floor\'] > 6) & (df[\'Elevator\'].isnull()), \'Elevator\'] = \'有电梯\'
df.loc[(df[\'Floor\'] <= 6) & (df[\'Elevator\'].isnull()), \'Elevator\'] = \'无电梯\'
# 只考虑“室”和“厅”,将其它少数“房间”和“卫”移除
df = df.loc[df[\'Layout\'].str.extract(\'^\d(.*?)\d.*?\') == \'室\']
# 提取“室”和“厅”创建新特征
df[\'Layout_room_num\'] = df[\'Layout\'].str.extract(\'(^\d).*\', expand=False).astype(\'int64\')
df[\'Layout_hall_num\'] = df[\'Layout\'].str.extract(\'^\d.*?(\d).*\', expand=False).astype(\'int64\')
# 按中位数对“Year”特征进行分箱
df[\'Year\'] = pd.qcut(df[\'Year\'], 8).astype(\'object\')
# 对“Direction”特征
d_list_one = [\'东\', \'西\', \'南\', \'北\']
d_list_two = [\'东西\', \'东南\', \'东北\', \'西南\', \'西北\', \'南北\']
d_list_three = [\'东西南\', \'东西北\', \'东南北\', \'西南北\']
d_list_four = [\'东西南北\']
df[\'Direction\'] = df[\'Direction\'].apply(direct_func)
df = df.loc[(df[\'Direction\'] != \'no\') & (df[\'Direction\'] != \'nan\')]
# 根据已有特征创建新特征
df[\'Layout_total_num\'] = df[\'Layout_room_num\'] + df[\'Layout_hall_num\']
df[\'Size_room_ratio\'] = df[\'Size\'] / df[\'Layout_total_num\']
# 删除无用特征
df = df.drop([\'Layout\', \'PerPrice\', \'Garden\'], axis=1)
# 对于object特征进行onehot编码
df, df_cat = one_hot_encoder(df)
# 只考虑“室”和“厅”,将其它少数“房间”和“卫”移除
df = df.loc[df[\'Layout\'].str.extract(\'^\d(.*?)\d.*?\') == \'室\']
# 提取“室”和“厅”创建新特征
df[\'Layout_room_num\'] = df[\'Layout\'].str.extract(\'(^\d).*\', expand=False).astype(\'int64\')
df[\'Layout_hall_num\'] = df[\'Layout\'].str.extract(\'^\d.*?(\d).*\', expand=False).astype(\'int64\')
#我们还有一个Year特征,为建房的年限时间。年限种类很多,分布在1950和2018之间,如果每个不同的Year值都作为特征值,我们并不能找出Year对Price有什么影响,因为年限划分的太细了。因此,我们只有将连续数值型特征
#Year离散化,做分箱处理。
# 按中位数对“Year”特征进行分箱
df[\'Year\'] = pd.qcut(df[\'Year\'], 8).astype(\'object\') -
Direction
-
# 对“Direction”特征
d_list_one = [\'东\',\'西\',\'南\',\'北\']
d_list_two = [\'东西\',\'东南\',\'东北\',\'西南\',\'西北\',\'南北\']
d_list_three = [\'东西南\',\'东西北\',\'东南北\',\'西南北\']
d_list_four = [\'东西南北\']
df[\'Direction\'] = df[\'Direction\'].apply(direct_func)
df = df.loc[(df[\'Direction\']!=\'no\')&(df[\'Direction\']!=\'nan\')] -
- 特征相关性
-
下面使用 seaborn 的 heatmap 方法对特征相关性进行可视化。
# data_corr
colormap = plt.cm.RdBu
plt.figure(figsize=(20,20))
# plt.title(\'Pearson Correlation of Features\', y=1.05, size=15)
sns.heatmap(df.corr(),linewidths=0.1,vmax=1.0, square=True, cmap=colormap, linecolor=\'white\', annot=True) -
颜色偏红或者偏蓝都说明相关系数较大,即两个特征对于目标变量的影响程度相似,即存在严重的重复信息,会造成过拟合现象。因此,通过特征相关性分析,我们可以找出哪些特征有严重的重叠信息,然后择优选择。
- 数据建模预测
-
模型策略方法如下:
-
使用Cart决策树的回归模型对二手房房价进行分析预测
-
使用交叉验证方法充分利用数据集进行训练,避免数据划分不均匀的影响。
-
使用GridSearchCV方法优化模型参数
-
使用R2评分方法对模型预测评分
- 数据划分
- # 转换训练测试集格式为数组
features = np.array(features)
prices = np.array(prices)
# 导入sklearn进行训练测试集划分
from sklearn.model_selection import train_test_split
features_train, features_test, prices_train, prices_test = train_test_split(features, prices, test_size=0.2, random_state=0) - 将以上数据划分为训练集和测试集,训练集用于建立模型,测试集用于测试模型预测准确率。使用sklearn的 model_selection 实现以上划分功能。
- 建立模型
- from sklearn.model_selection import KFold
2from sklearn.tree import DecisionTreeRegressor
3from sklearn.metrics import make_scorer
4from sklearn.model_selection import GridSearchCV
5
6# 利用GridSearchCV计算最优解
7def fit_model(X, y):
8 """ 基于输入数据 [X,y],利于网格搜索找到最优的决策树模型"""
9
10 cross_validator = KFold(10, shuffle=True)
11 regressor = DecisionTreeRegressor()
12
13 params = {\'max_depth\':[1,2,3,4,5,6,7,8,9,10]}
14 scoring_fnc = make_scorer(performance_metric)
15 grid = GridSearchCV(estimator = regressor, param_grid = params, scoring = scoring_fnc, cv = cross_validator)
16
17 # 基于输入数据 [X,y],进行网格搜索
18 grid = grid.fit(X, y)
19# print pd.DataFrame(grid.cv_results_)
20 return grid.best_estimator_
21
22# 计算R2分数
23def performance_metric(y_true, y_predict):
24 """计算并返回预测值相比于预测值的分数"""
25 from sklearn.metrics import r2_score
26 score = r2_score(y_true, y_predict)
27
28 return score - 使用了 KFold 方法减缓过拟合,GridSearchCV 方法进行最优参数自动搜查,最后使用R2评分来给模型打分。
- 调参优化模型
- import visuals as vs
2
3# 分析模型
4vs.ModelLearning(features_train, prices_train)
5vs.ModelComplexity(features_train, prices_train)
6
7optimal_reg1 = fit_model(features_train, prices_train)
8
9# 输出最优模型的 \'max_depth\' 参数
10print("最理想模型的参数 \'max_depth\' 是 {} 。".format(optimal_reg1.get_params()[\'max_depth\']))
11
12predicted_value = optimal_reg1.predict(features_test)
13r2 = performance_metric(prices_test, predicted_value)
14
15print("最优模型在测试数据上 R^2 分数 {:,.2f}。".format(r2)) - 由于决策树容易过拟合的问题,我们这里采取观察学习曲线的方法查看决策树深度,并判断模型是否出现了过拟合现象。以下是观察到的学习曲线图形:
-
通过观察,最理想模型的参数"max_depth"是10,此种情况下达到了偏差与方差的最优平衡,最后模型在测试数据上的R2分数,也即二手房房价预测的准确率为:0.81。
-
import scrapy class LianjiaSpiderItem(scrapy.Item): # define the fields for your item here like: Id = scrapy.Field() Region = scrapy.Field() Garden = scrapy.Field() Layout = scrapy.Field() Size = scrapy.Field() Direction = scrapy.Field() Renovation = scrapy.Field() Elevator = scrapy.Field() Floor = scrapy.Field() Year = scrapy.Field() Price = scrapy.Field() District = scrapy.Field() pass -*- coding:utf-8 -*- import json import scrapy import logging from bs4 import BeautifulSoup from lianjia_spider.settings import table from lianjia_spider.items import LianjiaSpiderItem def parse(self, response): item = LianjiaSpiderItem() soup = BeautifulSoup(response.body, "html.parser") # 获取到所有子列表的信息 house_info_list = soup.find_all(name="li", class_="clear") # 通过url辨认所在区域 url = response.url url = url.split(\'/\') item[\'Region\'] = table[url[-3]] for info in house_info_list: item[\'Id\'] = info.a[\'data-housecode\'] house_info = info.find_all(name="div", class_="houseInfo")[0] house_info = house_info.get_text() house_info = house_info.replace(\' \', \'\') house_info = house_info.split(\'/\') # print(house_info) try: item[\'Garden\'] = house_info[0] item[\'Layout\'] = house_info[1] item[\'Size\'] = house_info[2] item[\'Direction\'] = house_info[3] item[\'Renovation\'] = house_info[4] if len(house_info) > 5: item[\'Elevator\'] = house_info[5] else: item[\'Elevator\'] = \'\' except: print("数据保存错误") position_info = info.find_all(name=\'div\', class_=\'positionInfo\')[0] position_info = position_info.get_text() position_info = position_info.replace(\' \', \'\') position_info = position_info.split(\'/\') # print(position_info) try: item[\'Floor\'] = position_info[0] item[\'Year\'] = position_info[1] item[\'District\'] = position_info[2] except: print("数据保存错误") price_info = info.find_all("div", class_="totalPrice")[0] item[\'Price\'] = price_info.span.get_text() yield item #首先导入要使用的科学计算包numpy, pandas, 可视化matplotlib, seaborn, 以及机器学习包sklearn。 import pandas as pd import numpy as np import seaborn as sns import matplotlib as mpl import matplotlib.pyplot as plt from IPython.display import display plt.style.use("fivethirtyeight") sns.set_style({\'font.sans-serif\': [\'simhei\', \'Arial\']}) % matplotlib inline # 检查Python版本 from sys import version_info if version_info.major != 3: raise Exception(\'请使用Python 3 来完成此项目\') #然后导入数据,并进行初步的观察,这些观察包括了解数据特征的缺失值,异常值,以及大概的描述性统计。 # 导入链家二手房数据 lianjia_df = pd.read_csv(\'lianjia.csv\') display(lianjia_df.head(n=2)) # 添加新特征房屋均价 df = lianjia_df.copy() df[\'PerPrice\'] = lianjia_df[\'Price\'] / lianjia_df[\'Size\'] # 重新摆放列位置 columns = [\'Region\', \'District\', \'Garden\', \'Layout\', \'Floor\', \'Year\', \'Size\', \'Elevator\', \'Direction\', \'Renovation\', \'PerPrice\', \'Price\'] df = pd.DataFrame(df, columns=columns) # 重新审视数据集 display(df.head(n=2)) # 对二手房区域分组对比二手房数量和每平米房价 df_house_count = df.groupby(\'Region\')[\'Price\'].count().sort_values(ascending=False).to_frame().reset_index() df_house_mean = df.groupby(\'Region\')[\'PerPrice\'].mean().sort_values(ascending=False).to_frame().reset_index() f, [ax1, ax2, ax3] = plt.subplots(3, 1, figsize=(20, 15)) sns.barplot(x=\'Region\', y=\'PerPrice\', palette="Blues_d", data=df_house_mean, ax=ax1) ax1.set_title(\'北京各大区二手房每平米单价对比\', fontsize=15) ax1.set_xlabel(\'区域\') ax1.set_ylabel(\'每平米单价\') sns.barplot(x=\'Region\', y=\'Price\', palette="Greens_d", data=df_house_count, ax=ax2) ax2.set_title(\'北京各大区二手房数量对比\', fontsize=15) ax2.set_xlabel(\'区域\') ax2.set_ylabel(\'数量\') sns.boxplot(x=\'Region\', y=\'Price\', data=df, ax=ax3) ax3.set_title(\'北京各大区二手房房屋总价\', fontsize=15) ax3.set_xlabel(\'区域\') ax3.set_ylabel(\'房屋总价\') plt.show() f, [ax1, ax2] = plt.subplots(1, 2, figsize=(15, 5)) # 建房时间的分布情况 sns.distplot(df[\'Size\'], bins=20, ax=ax1, color=\'r\') sns.kdeplot(df[\'Size\'], shade=True, ax=ax1) # 建房时间和出售价格的关系 sns.regplot(x=\'Size\', y=\'Price\', data=df, ax=ax2) plt.show() df.loc[df[\'Size\'] > 1000] df = df[(df[\'Layout\'] != \'叠拼别墅\') & (df[\'Size\'] < 1000)] #Layout特征分析 f, ax1 = plt.subplots(figsize=(20, 20)) sns.countplot(y=\'Layout\', data=df, ax=ax1) ax1.set_title(\'房屋户型\', fontsize=15) ax1.set_xlabel(\'数量\') ax1.set_ylabel(\'户型\') plt.show() #Renovation特征分析 df[\'Renovation\'].value_counts() # 去掉错误数据“南北”,因为爬虫过程中一些信息位置为空,导致“Direction”的特征出现在这里,需要清除或替换 df[\'Renovation\'] = df.loc[(df[\'Renovation\'] != \'南北\'), \'Renovation\'] # 画幅设置 f, [ax1, ax2, ax3] = plt.subplots(1, 3, figsize=(20, 5)) sns.countplot(df[\'Renovation\'], ax=ax1) sns.barplot(x=\'Renovation\', y=\'Price\', data=df, ax=ax2) sns.boxplot(x=\'Renovation\', y=\'Price\', data=df, ax=ax3) plt.show() misn = len(df.loc[(df[\'Elevator\'].isnull()), \'Elevator\']) print(\'Elevator缺失值数量为:\' + str(misn)) # 由于存在个别类型错误,如简装和精装,特征值错位,故需要移除 df[\'Elevator\'] = df.loc[(df[\'Elevator\'] == \'有电梯\') | (df[\'Elevator\'] == \'无电梯\'), \'Elevator\'] # 填补Elevator缺失值 df.loc[(df[\'Floor\'] > 6) & (df[\'Elevator\'].isnull()), \'Elevator\'] = \'有电梯\' df.loc[(df[\'Floor\'] <= 6) & (df[\'Elevator\'].isnull()), \'Elevator\'] = \'无电梯\' f, [ax1, ax2] = plt.subplots(1, 2, figsize=(20, 10)) sns.countplot(df[\'Elevator\'], ax=ax1) ax1.set_title(\'有无电梯数量对比\', fontsize=15) ax1.set_xlabel(\'是否有电梯\') ax1.set_ylabel(\'数量\') sns.barplot(x=\'Elevator\', y=\'Price\', data=df, ax=ax2) ax2.set_title(\'有无电梯房价对比\', fontsize=15) ax2.set_xlabel(\'是否有电梯\') ax2.set_ylabel(\'总价\') plt.show() grid = sns.FacetGrid(df, row=\'Elevator\', col=\'Renovation\', palette=\'seismic\', size=4) grid.map(plt.scatter, \'Year\', \'Price\') grid.add_legend() #Floor特征分析 f, ax1 = plt.subplots(figsize=(20, 5)) sns.countplot(x=\'Floor\', data=df, ax=ax1) ax1.set_title(\'房屋户型\', fontsize=15) ax1.set_xlabel(\'数量\') ax1.set_ylabel(\'户型\') plt.show() #建模过程 # 移除结构类型异常值和房屋大小异常值 df = df[(df[\'Layout\'] != \'叠拼别墅\') & (df[\'Size\'] < 1000)] # 去掉错误数据“南北”,因为爬虫过程中一些信息位置为空,导致“Direction”的特征出现在这里,需要清除或替换 df[\'Renovation\'] = df.loc[(df[\'Renovation\'] != \'南北\'), \'Renovation\'] # 由于存在个别类型错误,如简装和精装,特征值错位,故需要移除 df[\'Elevator\'] = df.loc[(df[\'Elevator\'] == \'有电梯\') | (df[\'Elevator\'] == \'无电梯\'), \'Elevator\'] # 填补Elevator缺失值 df.loc[(df[\'Floor\'] > 6) & (df[\'Elevator\'].isnull()), \'Elevator\'] = \'有电梯\' df.loc[(df[\'Floor\'] <= 6) & (df[\'Elevator\'].isnull()), \'Elevator\'] = \'无电梯\' # 只考虑“室”和“厅”,将其它少数“房间”和“卫”移除 df = df.loc[df[\'Layout\'].str.extract(\'^\d(.*?)\d.*?\') == \'室\'] # 提取“室”和“厅”创建新特征 df[\'Layout_room_num\'] = df[\'Layout\'].str.extract(\'(^\d).*\', expand=False).astype(\'int64\') df[\'Layout_hall_num\'] = df[\'Layout\'].str.extract(\'^\d.*?(\d).*\', expand=False).astype(\'int64\') # 按中位数对“Year”特征进行分箱 df[\'Year\'] = pd.qcut(df[\'Year\'], 8).astype(\'object\') # 对“Direction”特征 d_list_one = [\'东\', \'西\', \'南\', \'北\'] d_list_two = [\'东西\', \'东南\', \'东北\', \'西南\', \'西北\', \'南北\'] d_list_three = [\'东西南\', \'东西北\', \'东南北\', \'西南北\'] d_list_four = [\'东西南北\'] df[\'Direction\'] = df[\'Direction\'].apply(direct_func) df = df.loc[(df[\'Direction\'] != \'no\') & (df[\'Direction\'] != \'nan\')] # 根据已有特征创建新特征 df[\'Layout_total_num\'] = df[\'Layout_room_num\'] + df[\'Layout_hall_num\'] df[\'Size_room_ratio\'] = df[\'Size\'] / df[\'Layout_total_num\'] # 删除无用特征 df = df.drop([\'Layout\', \'PerPrice\', \'Garden\'], axis=1) # 对于object特征进行onehot编码 df, df_cat = one_hot_encoder(df) # 只考虑“室”和“厅”,将其它少数“房间”和“卫”移除 df = df.loc[df[\'Layout\'].str.extract(\'^\d(.*?)\d.*?\') == \'室\'] # 提取“室”和“厅”创建新特征 df[\'Layout_room_num\'] = df[\'Layout\'].str.extract(\'(^\d).*\', expand=False).astype(\'int64\') df[\'Layout_hall_num\'] = df[\'Layout\'].str.extract(\'^\d.*?(\d).*\', expand=False).astype(\'int64\') #我们还有一个Year特征,为建房的年限时间。年限种类很多,分布在1950和2018之间,如果每个不同的Year值都作为特征值,我们并不能找出Year对Price有什么影响,因为年限划分的太细了。因此,我们只有将连续数值型特征 #Year离散化,做分箱处理。 # 按中位数对“Year”特征进行分箱 df[\'Year\'] = pd.qcut(df[\'Year\'], 8).astype(\'object\') # 对“Direction”特征 d_list_one = [\'东\', \'西\', \'南\', \'北\'] d_list_two = [\'东西\', \'东南\', \'东北\', \'西南\', \'西北\', \'南北\'] d_list_three = [\'东西南\', \'东西北\', \'东南北\', \'西南北\'] d_list_four = [\'东西南北\'] df[\'Direction\'] = df[\'Direction\'].apply(direct_func) df = df.loc[(df[\'Direction\'] != \'no\') & (df[\'Direction\'] != \'nan\')] # data_corr colormap = plt.cm.RdBu plt.figure(figsize=(20, 20)) # plt.title(\'Pearson Correlation of Features\', y=1.05, size=15) sns.heatmap(df.corr(), linewidths=0.1, vmax=1.0, square=True, cmap=colormap, linecolor=\'white\', annot=True) # 转换训练测试集格式为数组 features = np.array(features) prices = np.array(prices) # 导入sklearn进行训练测试集划分 from sklearn.model_selection import train_test_split features_train, features_test, prices_train, prices_test = train_test_split(features, prices, test_size=0.2,random_state=0) from sklearn.model_selection import KFold from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import make_scorer from sklearn.model_selection import GridSearchCV # 利用GridSearchCV计算最优解 def fit_model(X, y): """ 基于输入数据 [X,y],利于网格搜索找到最优的决策树模型""" cross_validator = KFold(10, shuffle=True) regressor = DecisionTreeRegressor() params = {\'max_depth\': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]} scoring_fnc = make_scorer(performance_metric) grid = GridSearchCV(estimator=regressor, param_grid=params, scoring=scoring_fnc, cv=cross_validator) # 基于输入数据 [X,y],进行网格搜索 grid = grid.fit(X, y) # print pd.DataFrame(grid.cv_results_) return grid.best_estimator_ # 计算R2分数 def performance_metric(y_true, y_predict): """计算并返回预测值相比于预测值的分数""" from sklearn.metrics import r2_score score = r2_score(y_true, y_predict) return score 调参优化模型 import visuals as vs # 分析模型 vs.ModelLearning(features_train, prices_train) vs.ModelComplexity(features_train, prices_train) optimal_reg1 = fit_model(features_train, prices_train) # 输出最优模型的 \'max_depth\' 参数 print("最理想模型的参数 \'max_depth\' 是 {} 。".format(optimal_reg1.get_params()[\'max_depth\'])) predicted_value = optimal_reg1.predict(features_test) r2 = performance_metric(prices_test, predicted_value) print("最优模型在测试数据上 R^2 分数 {:,.2f}。".format(r2))