Advertisement

从数据爬取到决策树建模——预测北京二手房房价

阅读量:

一、项目背景

北京地区的房价一直是广大家庭所关注的焦点话题。本次研究项目的主要目标是深入剖析北京市二手住宅市场价格规律。基于Scrapy框架的技术支持,本项目采用了系统性的方式获取链家网站上详尽的二手房房源数据。对采集到的信息进行了基础性的统计分析,并通过可视化工具构建相应的数据展示模块。运用决策树算法模型对未来的市场走势进行了科学预测。最后阶段,则重点考察了训练过程中的学习曲线变化情况,并评估模型是否陷入了过拟合的风险状态。(仅供参考)

二、爬取数据

链家网站的二手房房源信息展示如下:

在这里插入图片描述

共有77049条房源信息,并非全部公开展示。根据现有设置,默认情况下仅提供前100页的数据结果,默认每页包含30条记录。本数据集未启用反爬机制,默认情况下即可进行数据抓取。本文主要关注并提取了标有红色方框包围的11个字段信息:包括:房源详细描述(如价格走势)、地理位置信息(如周边商圈)、空间布局规划(如功能分区)、面积大小(如平面面积)、朝向方位(如南北向)、室内装修状况(如材料选择)、所处楼层信息以及建筑建造时间等详细参数,并对上述各项进行了深入解析与数据分析

在这里插入图片描述

如下是具体的爬虫代码:
item.py

复制代码
    import scrapy
    
    class LianjiaspiderprojectItem(scrapy.Item):
    # define the fields for your item here like:
    Description = scrapy.Field() #房源描述
    Location = scrapy.Field() #房源所在位置
    Layout = scrapy.Field() #房源布局
    Size = scrapy.Field() #房源面积
    Direction = scrapy.Field() #房源朝向
    Renovation = scrapy.Field() #房源装修情况
    Floorinfo = scrapy.Field() #房源所在楼层信息
    Year = scrapy.Field() #房源建造年份
    Type = scrapy.Field() #房源类型
    Price = scrapy.Field() #房源总价
    unitPrice = scrapy.Field() #房源单价
    
    pass

lianjia.py

复制代码
    import scrapy
    from lxml import etree
    from LianjiaSpiderProject.items import LianjiaspiderprojectItem
    
    class LianjiaSpider(scrapy.Spider):
    name = 'lianjia'
    #allowed_domains = ['www.xxx.com']
    start_urls = ['https://bj.lianjia.com/ershoufang/pg1/']
    
    initial_url = "https://bj.lianjia.com/ershoufang/pg"
    current_page = 2
    
    def parse(self, response):
        #获取第一页中所有房源信息所在的标签,其中每页包括30条房源信息,即30条li标签
        sell_list = response.xpath('//ul[@class="sellListContent"]/li')
        #对30条li标签进行解析获取相应的房源信息
        for sell in sell_list:
            Houseinfo = sell.xpath('./div[1]/div[@class="address"]/div//text()').extract()[0]
    
            if len(Houseinfo.split(' | ')) == 7:
                Layout = Houseinfo.split(' | ')[0]
                Size = Houseinfo.split(' | ')[1]
                Direction = Houseinfo.split(' | ')[2]
                Renovation = Houseinfo.split(' | ')[3]
                Floorinfo = Houseinfo.split(' | ')[4]
                Year = Houseinfo.split(' | ')[5]
                Type = Houseinfo.split(' | ')[6]
            else:
                break
    
            Description = sell.xpath('./div[1]/div[@class="title"]/a/text()').extract()[0]
            Location = sell.xpath('./div[1]/div[@class="flood"]//text()').extract()
            Location_new = "".join([x.strip() for x in Location if len(x.strip()) > 0])  # 去除列表中的空格和空字符串,并将其拼接成一个字符串
            Price = sell.xpath('./div[1]/div[@class="priceInfo"]/div[1]//text()').extract()
            Price_new = "".join(Price)
            unitPrice = sell.xpath('./div[1]/div[@class="priceInfo"]/div[2]//text()')[0].extract()
    
            #将爬取的数据与item文件里面的数据对应起来
            item = LianjiaspiderprojectItem()
    
            item['Description'] = Description
            item['Location'] = Location_new
            item['Layout'] = Layout
            item['Size'] = Size
            item['Direction'] = Direction
            item['Renovation'] = Renovation
            item['Floorinfo'] = Floorinfo
            item['Year'] = Year
            item['Type'] = Type
            item['Price'] = Price_new
            item['unitPrice'] = unitPrice
    
            yield item
    
        #链家只展示了100页的内容,抓完100页就停止爬虫
        #组装下一页要抓取的网址
        if self.current_page != 101:
            new_url = self.initial_url + str(self.current_page) + '/'
            print('starting scrapy url:', new_url)
            yield scrapy.Request(new_url, callback=self.parse)
    
            self.current_page += 1
        else:
            print('scrapy done')
    
        pass

pipelines.py

复制代码
    from itemadapter import ItemAdapter
    import csv
    
    class LianjiaspiderprojectPipeline(object):
    fp = None
    index = 0
    #该方法只在爬虫开始的时候被调用一次
    def open_spider(self, spider):
        print('开始爬虫......')
        self.fp = open('./lianjia.csv', 'a', encoding='utf-8')
    
    def process_item(self, item, spider):
        Description = item['Description']
        Location = item['Location']
        Layout = item['Layout']
        Size = item['Size']
        Direction = item['Direction']
        Renovation = item['Renovation']
        Floorinfo = item['Floorinfo']
        Year = item['Year']
        Type = item['Type']
        Price = item['Price']
        unitPrice = item['unitPrice']
    
        if self.index == 0:
            columnnames = "房源描述,位置,布局,面积,朝向,装修情况,楼层,建造年份,类型,总计,单价"
            self.fp.write(columnnames+'\n')
    
            self.index = 1
        self.fp.write("{},{},{},{},{},{},{},{},{},{},{}\n".format(Description, Location, Layout, Size, Direction, Renovation, Floorinfo, Year, Type, Price, unitPrice))
    
        return item
    
    def close_spider(self, spider):
        print('爬虫结束!')
        self.fp.close()
    
    pass

三、导入爬取的数据

复制代码
    #导入相关库
    import warnings
    warnings.filterwarnings('ignore')
    import numpy as np
    import pandas as pd
    from IPython.display import display
    import matplotlib.pyplot as plt
    %matplotlib inline
    import seaborn as sns
    sns.set(style='darkgrid',context='notebook',font_scale=1.5) # 设置背景
    plt.rcParams['font.sans-serif']=['SimHei']
    plt.rcParams['axes.unicode_minus']=False #处理中文和坐标负号显示
    
    #导入链家网站二手房数据集
    lianjia = pd.read_csv('lianjia.csv')
    display(lianjia.head())
    
    Direction	District	Elevator	Floor	Garden	Id	Layout	Price	Region	Renovation	Size	Year
    0	东西	灯市口	NaN	6	锡拉胡同21号院	101102647043	3室1厅	780.0	东城	精装	75.0	1988
    1	南北	东单	无电梯	6	东华门大街	101102650978	2室1厅	705.0	东城	精装	60.0	1988
    2	南西	崇文门	有电梯	16	新世界中心	101102672743	3室1厅	1400.0	东城	其他	210.0	1996
    3	南	崇文门	NaN	7	兴隆都市馨园	101102577410	1室1厅	420.0	东城	精装	39.0	2004
    4	南	陶然亭	有电梯	19	中海紫御公馆	101102574696	2室2厅	998.0	东城	精装	90.0	2010
    
    lianjia.info()
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 23677 entries, 0 to 23676
    Data columns (total 12 columns):
     #   Column      Non-Null Count  Dtype  
    ---  ------      --------------  -----  
     0   Direction   23677 non-null  object 
     1   District    23677 non-null  object 
     2   Elevator    15440 non-null  object 
     3   Floor       23677 non-null  int64  
     4   Garden      23677 non-null  object 
     5   Id          23677 non-null  int64  
     6   Layout      23677 non-null  object 
     7   Price       23677 non-null  float64
     8   Region      23677 non-null  object 
     9   Renovation  23677 non-null  object 
     10  Size        23677 non-null  float64
     11  Year        23677 non-null  int64  
    dtypes: float64(2), int64(3), object(7)
    memory usage: 2.2+ MB
    
    lianjia.describe()
    Floor	Id	Price	Size	Year
    count	23677.000000	2.367700e+04	23677.000000	23677.000000	23677.000000
    mean	12.765088	1.011024e+11	610.668319	99.149301	2001.326519
    std	7.643932	5.652477e+05	411.452107	50.988838	9.001996
    min	1.000000	1.010886e+11	60.000000	2.000000	1950.000000
    25%	6.000000	1.011022e+11	365.000000	66.000000	1997.000000
    50%	11.000000	1.011025e+11	499.000000	88.000000	2003.000000
    75%	18.000000	1.011027e+11	717.000000	118.000000	2007.000000
    max	57.000000	1.011028e+11	6000.000000	1019.000000	2017.000000

观察结果表明:该数据集中共有23677条记录(样本),涉及共11个特征字段(指标)。其中price字段是我们关注的目标变量(因变量)。elevator字段存在缺失记录(缺失值)。此外,在统计指标中发现size字段的最大面积达到约1019平方米(m²),最小仅2平方米(m²)。这种极端差异在现实场景中不太可能发生(合理推测),因此这可能是一个异常观测点(outlier),会对模型性能产生显著影响(影响)。需要注意的是,“异常观测点”这一结论目前还处于初步阶段(阶段判断),尚未得到验证

复制代码
    #添加新特征-房屋均价
    df = lianjia.copy()
    df['PerPrice'] = lianjia['Price']/lianjia['Size']
    
    #调整各个特征的排列顺序,其中'Id'特征实际意义不大,故将其移除
    columns = ['Region', 'District', 'Garden', 'Layout', 'Floor', 'Year', 'Size', \
           'Elevator', 'Direction', 'Renovation', 'PerPrice', 'Price']
    df = pd.DataFrame(df, columns = columns)
    
    #重新审视数据
    display(df.head())
    
    Region	District	Garden	Layout	Floor	Year	Size	Elevator	Direction	Renovation	PerPrice	Price
    0	东城	灯市口	锡拉胡同21号院	3室1厅	6	1988	75.0	NaN	东西	精装	10.400000	780.0
    1	东城	东单	东华门大街	2室1厅	6	1988	60.0	无电梯	南北	精装	11.750000	705.0
    2	东城	崇文门	新世界中心	3室1厅	16	1996	210.0	有电梯	南西	其他	6.666667	1400.0
    3	东城	崇文门	兴隆都市馨园	1室1厅	7	2004	39.0	NaN	南	精装	10.769231	420.0
    4	东城	陶然亭	中海紫御公馆	2室2厅	19	2010	90.0	有电梯	南	精装	11.088889	998.0

四、数据可视化分析

Region特征分析

复制代码
    #对Region进行分组,研究不同区域的二手房数量、单价和总价情况
    df_house_count = df.groupby('Region').count()['Price'].sort_values(ascending=False).reset_index().rename({'Price':'Count'},axis=1)
    df_house_mean = df.groupby('Region').mean()['PerPrice'].sort_values(ascending=False).reset_index().rename({'PerPrice':'MeanPrice'},axis=1)
    
    plt.figure(figsize=(20,30))
    plt.subplot((311))
    sns.barplot(x='Region', y='Count', palette='Blues_d', data=df_house_count)
    plt.title('北京各区域二手房数量对比', fontsize=15)
    plt.xlabel('区域')
    plt.ylabel('数量')
    
    plt.subplot((312))
    sns.barplot(x='Region', y='MeanPrice', palette='Greens_d', data=df_house_mean)
    plt.title('北京各区域二手房单价对比', fontsize=15)
    plt.xlabel('区域')
    plt.ylabel('每平米单价')
    
    plt.subplot((313))
    sns.boxplot(x='Region', y='Price', palette='Blues_d', data=df)
    plt.title('北京各区域二手房房价对比', fontsize=15)
    plt.xlabel('区域')
    plt.ylabel('房总价')
    
    plt.show()
在这里插入图片描述
  • 二手房数量方面数据显示:海淀区与朝阳区并列第一,在售二手房约为3千套左右。随后是丰台区,在过去几年里正快速推进改建工程,并呈现出赶超的趋势。
  • 在二手房价格方面统计结果表明:西城区房价最高约为1.1万元/平方米的原因在于其位于二环核心地段并且集中了众多优质教育资源房型产品。紧随其后的是东城区售价约1.0万元/平方米之后依次为海淀区售价约850元/平方米其余区域则多在8万元以下徘徊。
  • 关于二手房总价数据呈现中位数普遍低于一千万元的特点其中间值普遍低于一千万元同时各个地区的售价呈现较高的离散程度值得注意的是西城某楼盘售价高达六千万远超其他区域水平这反映出该地区房价分布并非典型的正态分布特征。

Size特征分析

复制代码
    plt.figure(figsize=(20,20))
    plt.subplot((211))
    #二手房面积的分布情况
    sns.distplot(df['Size'], bins=20, color='r', kde=True, kde_kws={'color':'steelblue','shade':True,'linewidth':6})
    #二手房价格与面积的关系
    plt.subplot((212))
    sns.regplot(x='Size', y='Price', data=df)
    
    plt.show()
在这里插入图片描述
  • Size的分布情况:通过分析直方图(distplot)和核密度估计图(KDE),二手房面积呈现出长尾型的分布特征,在这种情况下存在大量超出常规范围的房屋面积。
  • Size与Price之间的关系:Price与Size之间呈现出明显的线性关联,在大多数情况下面积越大则价格越高这一现象符合市场规律。然而需要特别关注两种异常情况:一是房屋面积小于10平方米却价格超过1千万;二是房屋面积超过千平方米却价格偏低的情况需要进一步核实具体情况。
复制代码
    df.loc[df['Size']<10]
    df.loc[df['Size']>1000]

*通过检查发现:这一异常点并非普通的普通住宅,并且很可能属于商业用途的房产。因为仅有1间卧室且无卫生间,并且其面积超过约93平方英尺(即约8.6平方米),因此决定将其从项目中剔除。

复制代码
    #移除异常数据
    df = df[(df['Layout']!='叠拼别墅') & (df['Size']<1000)]
    
    #重新绘制Size分布、Size与Price之间的关系
    plt.figure(figsize=(20,20))
    plt.subplot((211))
    #二手房面积的分布情况
    sns.distplot(df['Size'], bins=20, color='r', kde=True, kde_kws={'color':'steelblue','shade':True,'linewidth':6})
    #二手房价格与面积的关系
    plt.subplot((212))
    sns.regplot(x='Size', y='Price', data=df)
    
    plt.show()
在这里插入图片描述

Layout特征分析

复制代码
    plt.figure(figsize=(20,20))
    sns.countplot(y='Layout', data=df)
    plt.title('户型', fontsize=15)
    plt.xlabel('数量')
    plt.ylabel('户型')
    plt.show()
在这里插入图片描述

通过细致观察可以看出:户型类别繁多,在主流居住需求中占据主导地位的是2室1厅的住宅 layouts. 在深入分析 layouts 的特征后发现,在分类体系中存在命名方式不统一的问题. 例如有些 layout 被称作2室1厅 有些则被称作2房间1卫 这样的差异性容易造成混淆. 因此在进行机器学习建模前必须对数据特征进行规范化处理

Renovation特征分析

复制代码
    df['Renovation'].value_counts()
    精装    11345
    简装     8496
    其他     3239
    毛坯      576
    Name: Renovation, dtype: int64
    
    #绘制Renovation各分类的数量情况
    plt.figure(figsize=(15,30))
    plt.subplot((311))
    sns.countplot(df['Renovation'])
    
    #绘制Renovation与Price之间的关系
    plt.subplot((312))
    sns.barplot(x='Renovation', y='Price', data=df)
    plt.subplot((313))
    sns.boxplot(x='Renovation', y='Price', data=df)
    plt.show()
在这里插入图片描述

通过观察分析得出:精装修二手房的数量占比最大;其后是简约装修类房产;同时发现毛坯类型的房屋价格最高;其后是精装修类型的.

Elevator特征分析

复制代码
    df['Elevator'].value_counts()
    有电梯    9341
    无电梯    6078
    Name: Elevator, dtype: int64
    
    #查看缺失值
    df.loc[df['Elevator'].isnull()].shape
    (8237, 12)

本研究针对Elevator特征的8237条缺失记录进行了处理。研究思路表明,在本研究中采用填补法对缺失值进行填充,并基于经验规律构建了填充模型:即当楼层超过6层时具备电梯;而当楼层低于或等于6层时通常不具备电梯。

复制代码
    #填补缺失值
    df.loc[(df['Floor']>6)&(df['Elevator'].isnull()), 'Elevator'] = '有电梯'
    df.loc[(df['Floor']<=6)&(df['Elevator'].isnull()), 'Elevator'] = '无电梯'
    
    #绘制Elevator不同类别的数量
    plt.figure(figsize=(20,15))
    plt.subplot((121))
    sns.countplot(df['Elevator'])
    plt.title('有无电梯数量对比',fontsize=15)
    plt.xlabel('是否有电梯',fontsize=15)
    plt.ylabel('数量',fontsize=15)
    
    #绘制Elevator类别与Price的关系
    plt.subplot((122))
    sns.barplot(x='Elevator', y='Price', data=df)
    plt.title('有无电梯房价对比',fontsize=15)
    plt.xlabel('是否有电梯',fontsize=15)
    plt.ylabel('房价',fontsize=15)
    plt.show()
在这里插入图片描述

通过观察分析发现:拥有电梯的二手房数量较多,在城市中尤其是像北京这样的大城市里人口规模庞大对居住需求也有很高的要求因此为了满足这些需求就必须要有相应的电梯系统支持。与此同时在拥有电梯的情况下房价通常会比较高

Year特征分析

复制代码
    #绘制Elevator和Renovation不同的分类情况下,Year与Price的关系
    grid = sns.FacetGrid(df, row='Elevator', col='Renovation', palette='pal', size=5) #size可调节图形间距
    grid.map(plt.scatter, 'Year', 'Price')
    grid.add_legend()
    plt.show()
在这里插入图片描述

经过观察发现:

  • 随着房龄的增长,二手房的价格逐渐下降;
    • 相比20世纪90年代,近年来新建二手房价格呈现出明显上涨趋势;
    • 直至1980年前,基本看不到带电梯的新建住宅,这表明在此之前小区普遍未安装电梯;
    • 即使是在没有电梯的情况下,大多数二手房都是简约风格,精装修的比例相对较低。

Floor特征分析

复制代码
    #绘制不同楼层的数量关系
    plt.figure(figsize=(20,8))
    sns.countplot(x='Floor', data=df)
    plt.title('二手房楼层',fontsize=15)
    plt.xlabel('楼层')
    plt.ylabel('数量')
    plt.show()
在这里插入图片描述

在调查过程中发现:6层的二手房数量最多;然而单个楼层本身并没有太多意义(因为各个小区所拥有的总楼层数存在差异)。通常情况下来说,在多数情况下中间楼层往往受到较高程度的关注(价格较高),而底层和顶层则不太受关注,并且其价格相对较低。

四、特征工程

机器学习之前需要对数据进一步处理,具体如下:

Layout特征

复制代码
    #只保留'xx室xx厅'数据,将其它少数房间与卫移除
    df = df.loc[df['Layout'].str.extract('^\d(.*?)\d.*?', expand=False)=='室']
    
    #提取'室'和'厅'创建新特征
    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')
    
    df['Layout'].value_counts()
    2室1厅    9485
    3室1厅    3999
    3室2厅    2765
    1室1厅    2681
    2室2厅    1671
    4室2厅     930
    1室0厅     499
    4室1厅     295
    5室2厅     200
    4室3厅      96
    5室3厅      75
    1室2厅      67
    6室2厅      59
    2室0厅      50
    3室3厅      43
    6室3厅      29
    3室0厅      29
    5室1厅      27
    7室3厅       7
    7室2厅       6
    2室3厅       5
    8室3厅       4
    4室4厅       4
    5室4厅       4
    6室4厅       4
    8室2厅       3
    4室0厅       3
    6室0厅       2
    9室3厅       1
    9室2厅       1
    7室1厅       1
    8室5厅       1
    5室0厅       1
    8室4厅       1
    6室5厅       1
    1室3厅       1
    9室1厅       1
    6室1厅       1
    Name: Layout, dtype: int64

Year特征

复制代码
    #对Year特征进行分箱,等频分段(按年限,Year划分太细,故将连续型数值Year特征离散化)
    df['Year'] = pd.qcut(df['Year'],8).astype('object')
    
    df['Year'].value_counts()
    (2000.0, 2003.0]      3705
    (2004.0, 2007.0]      3369
    (1990.0, 1997.0]      3110
    (1949.999, 1990.0]    3024
    (1997.0, 2000.0]      2829
    (2010.0, 2017.0]      2687
    (2007.0, 2010.0]      2571
    (2003.0, 2004.0]      1757
    Name: Year, dtype: int64

Direction特征

复制代码
    df['Direction'].value_counts()
    南北        11367
    南          2726
    东西         1388
    东南         1311
    西南         1094
    东           843
    西           802
    西北          733
    东北          645
    北           484
    东南北         465
    南西北         370
    南西          158
    东西北         139
    东南西         133
    西南北         124
    东南西北         90
    西南东北         23
    南东北          19
    东南西南         15
    东南南          13
    东东南          10
    西东北          10
    西南西北         10
    东西南           9
    南西南           9
    东南东北          7
    西南西           5
    东南南北          5
    南西东北          3
    东西南北          3
    西西南           2
    东东北           2
    东北东北          2
    南西西北          2
    西北东北          2
    西西北           2
    南西北北          2
    南东            2
    南西南北          2
    西北北           2
    南西南西          2
    西南西北东北        1
    南北东           1
    东南南西北         1
    东西北东北         1
    东南西北北         1
    东南西北东北        1
    东西北北          1
    北南            1
    东西东北          1
    西南西北北         1
    南北西           1
    东南西南东北        1
    东南北西          1
    东南西南北         1
    东东南南          1
    南北东北          1
    北西            1
    Name: Direction, dtype: int64

建立一个函数用于对Direction中的特征值进行去重处理,并去除一些不符合逻辑的组合结果。例如,在方向序列中出现的类似情况如'西南北'与'南西北'等。

复制代码
    list_one_num = ['东','西','南','北']
    list_two_num = ['东西','东南','东北','西南','西北','南北']
    list_three_num = ['东西南','东西北','东南北','西南北']
    list_four_num = ['东西南北']
    
    def direct_func(x):
    if not isinstance(x, str):
        raise TypeError
    
    x = x.strip()
    x_len = len(x)
    x_list = pd.unique([y for y in x])
    if x_len != len(x_list):
        return 'no'
    
    if (x_len==2)&(x not in list_two_num):
        m0=x[0]
        m1=x[1]
        return m1+m0
    elif (x_len==3)&(x not in list_three_num):
        for n in list_three_num:
            if (x_list[0] in n)&(x_list[1] in n)&(x_list[2] in n):
                return n
    elif (x_len==4)&(x not in list_four_num):
        return list_four_num[0]
    else:
        return x
    
    df['Direction'] = df['Direction'].apply(direct_func)
    df = df.loc[(df['Direction']!='no')&(df['Direction']!='nan')]
复制代码
    df['Direction'].value_counts()
    南北      11368
    南        2726
    东西       1388
    东南       1313
    西南       1252
    东         843
    西         802
    西北        734
    东北        645
    西南北       495
    东南北       485
    北         484
    东西北       149
    东西南       142
    东西南北      120
    Name: Direction, dtype: int64

创建新特征

复制代码
    #每个房间的平均面积
    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)
    display(df.head())
    
    Region	District	Floor	Year	Size	Elevator	Direction	Renovation	Price	Layout_room_num	Layout_hall_num	Layout_total_num	Size_room_ratio
    0	东城	灯市口	6	(1949.999, 1990.0]	75.0	无电梯	东西	精装	780.0	3	1	4	18.75
    1	东城	东单	6	(1949.999, 1990.0]	60.0	无电梯	南北	精装	705.0	2	1	3	20.00
    2	东城	崇文门	16	(1990.0, 1997.0]	210.0	有电梯	西南	其他	1400.0	3	1	4	52.50
    3	东城	崇文门	7	(2003.0, 2004.0]	39.0	有电梯	南	精装	420.0	1	1	2	19.50
    4	东城	陶然亭	19	(2007.0, 2010.0]	90.0	有电梯	南	精装	998.0	2	2	4	22.50

对object类型的特征进行One-hot编码

复制代码
    #定义函数,One-hot编码
    def one_hot_encoder(df, nan_as_category=True):
    original_columns = list(df.columns)
    categorical_columns = [col for col in df.columns if df[col].dtype=='object']
    df = pd.get_dummies(df, columns=categorical_columns, dummy_na=nan_as_category)
    new_columns = [c for c in df.columns if c not in original_columns]
    
    return df, new_columns
    
    df, df_cat = one_hot_encoder(df)

特征相关性

复制代码
    colormap = plt.cm.RdBu
    plt.figure(figsize=(30,20))
    sns.heatmap(df.corr(), linewidths=0.1, vmax=1.0, square=True, cmap=colormap, linecolor='white', annot=True)
在这里插入图片描述

颜色呈现明显红色或蓝色均表明两者间的关联程度较高;即这两个特征对目标变量的作用强度基本相当;这提示我们应关注存在明显的冗余信息;进而可依据这些分析结果筛选出具有独特意义的特征;以避免模型过拟合的问题。

五、决策树算法预测房价

采用基于Cart决策树的回归模型用于二手房价格数据的预测。
通过交叉验证方法最大化利用训练数据量,并尽量避免因数据划分不均而导致的信息损失。
采用网格搜索技术来优化模型参数设置。
借助学习曲线工具分析模型是否出现过拟合现象。

复制代码
    prices = df['Price'] #目标值
    features = df.drop(['Price'], axis=1) #特征值
    
    prices = np.array(prices)
    features = np.array(features)
    
    #建立决策树回归模型
    from sklearn.model_selection import train_test_split
    from sklearn.model_selection import KFold
    from sklearn.tree import DecisionTreeRegressor
    from sklearn.model_selection import GridSearchCV
    
    #利用GridSearchCV计算最优解
    def fit_model(features, prices):    
    features_train, features_test, prices_train, prices_test = train_test_split(features, prices, test_size=0.2, random_state=22)
    
    corss_validation = KFold(10, shuffle=True)
    regressor = DecisionTreeRegressor()
    params = {'max_depth':[1,2,3,4,5,6,7,8,9,10]}
    grid = GridSearchCV(estimator=regressor, param_grid=params, cv=corss_validation)
    
    grid.fit(features_train, prices_train)
    
    print('预测的准确率为:',grid.score(features_test, prices_test))
    print('选择的决策树深度为{}'.format(grid.best_estimator_))
    print('交叉验证中最好的结果为{}'.format(grid.best_score_))  
    
    return None
    
    fit_model(features, prices)
复制代码
    # 可视化模拟学习曲线,观察是否出现过拟合问题。
    from sklearn.model_selection import learning_curve, validation_curve
    from sklearn.model_selection import ShuffleSplit
    
    #绘制Learning_curve曲线
    def ModelLearning(X, y):
    cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
    train_sizes = np.rint(np.linspace(1, X.shape[0]*0.8-1, 10)).astype(int)
    #print(train_sizes)
    
    fig = plt.figure(figsize=(30,20))
    
    for k, depth in enumerate([1,3,6,10]):
        regressor = DecisionTreeRegressor(max_depth=depth)
        
        sizes, train_scores, test_scores = learning_curve(regressor, X, y, cv=cv, train_sizes=train_sizes)
    
        train_std = np.std(train_scores, axis=1)
        train_mean = np.mean(train_scores, axis=1)
        valid_std = np.std(test_scores, axis=1)
        valid_mean = np.mean(test_scores, axis=1)
    
        ax = fig.add_subplot(2, 2, k+1)
        ax.plot(sizes, train_mean, 'o-', color='r', linewidth=6, markersize=15, label='Training Score')
        ax.plot(sizes, valid_mean, 'o-', color='g', linewidth=6, markersize=15, label='Validation score')
        ax.fill_between(sizes, train_mean - train_std, train_mean + train_std, alpha=0.15, color='r')
        ax.fill_between(sizes, valid_mean - valid_std, valid_mean + valid_std, alpha=0.15, color='g')
    
        ax.set_title('max_depth={}'.format(depth), fontsize=30)
        ax.set_xlabel('训练集数量', fontsize=30)
        ax.set_ylabel('score', fontsize=30)
        ax.set_xlim([0, X.shape[0]*0.8])
        ax.set_ylim([-0.05, 1.05])
    
    ax.legend(bbox_to_anchor=(1.05,2.05), loc='upper right', fontsize=30) 
    fig.show()
    
    #绘制Validation_curve曲线
    def ModelComplexity(X, y):
    cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
    max_depth = np.arange(1,11)
    
    train_scores, valid_scores = validation_curve(DecisionTreeRegressor(), X, y, param_name='max_depth', param_range=max_depth, cv=cv)
    
    train_std = np.std(train_scores, axis=1)
    train_mean = np.mean(train_scores, axis=1)
    valid_std = np.std(valid_scores, axis=1)
    valid_mean = np.mean(valid_scores, axis=1)
    
    plt.figure(figsize=(8,6))
    plt.plot(max_depth, train_mean, 'o-', color='r', label='Training Score', linewidth=3, markersize=6)
    plt.plot(max_depth, valid_mean, 'o-', color='g', label='Validation Score', linewidth=3, markersize=6)
    plt.fill_between(max_depth, train_mean - train_std, train_mean + train_std, alpha=0.15, color='r')
    plt.fill_between(max_depth, valid_mean - valid_std, valid_mean + valid_std, alpha=0.15, color='g')
    
    plt.legend(loc='lower right')
    plt.xlabel('max_depth')
    plt.ylabel('Score')
    plt.ylim([-0.05, 1.05])
    plt.show()
    
    ModelLearning(features_train, prices_train)    
    ModelComplexity(features_train, prices_train)
在这里插入图片描述
在这里插入图片描述

预测模型的准确度值为: 0.831288503230392
所选决策树的最大深度参数设置为DecisionTreeRegressor(max_depth=10)
交叉验证的最佳表现值为: 0.7887002299501087

  • 由以上曲线可见,在决策深度接近10时(偏差与方法达到了均衡状态),此时对应的模型即为最优模型。
  • 本模型的准确率高达83%,大致能对二手房价进行预测。(参考价值仅为83%)

全部评论 (0)

还没有任何评论哟~