房价数据分析:爬虫、建模到解释全流程

决定房价高低的关键性因素分析

房价数据从采集到建模的详细过程——以宁波为例

作   者:数字567

作者简介:作者本人是CDA数据科学家,同时也是宁波校区的副校长

 

本文共计6000字左右,如果不喜欢技术过程,可以直接阅读文末分析结论。


房价一直以来是全国老百姓热议的话题,总结起来,无论对于刚需者还是投资者,无非关注两方面的问题:(1)房价的发展趋势是跌还是涨?这就需要对房价的时间发展趋势进行分析预判。(2)哪个区域的房子更具投资价值?这个问题可以理解为某个时间节点下的数据挖掘问题。本文探索和回答的是第二个问题,围绕这个目标567做了一件很多人喜闻乐见的事,展示了如何利用爬虫数据进行数据分析的全流程。

需要说明的是,虽然大部分人对于房价的影响因素都有所了解,但是哪些因素是关键的?关键因素对于房价的作用到底是如何促进和抑制的?像这类问题是大部人没法回答的,这也就体现了数据分析的价值所在。因此,本文将回答两方面的问题:(1)影响房价高低的因素中哪些是关键的;(2)这些关键因素是如何体现制约和限制作用的。

1.  数据采集

1.1 数据获取

本人最怕浪费时间,所以直接用现成工具进行采集,共获得3000多条数据(仅用于研究和学习,不用于任何商业目的)。数据导出为csv格式,方便各种工具调用和分析。


1.2空间坐标补全

原始数据最大的问题是没有空间坐标信息,但是不同区域的房价一般存在显著差异,比如限购圈内的房子很可能就比限购圈外贵,所以补充坐标信息,以反映房子所在区位情况是必不可少的一步。

同样,为了节约时间,简单地百度关键词“python获取百度坐标”,熟练地使用复制粘贴技能,并对代码稍作更正,就能实现坐标获取的目的。以下代码定义了一个函数,用于坐标的获取:

import json,urllib,math

# 根据地址获取经纬度

defgetlnglat(address):

    url ='http://api.map.baidu.com/geocoder/v2/'

    output = 'json'

    ak =你的百度密钥  #浏览器端密钥

    address =urllib.parse.quote(address)  #由于本文地址变量为中文,为防止乱码,先用quote进行编码

    uri = url + '?' + 'address=' + address +'&output=' + output + '&ak=' + ak

    try:

        req = urllib.request.urlopen(uri)

        res = req.read().decode()

        temp = json.loads(res)

        #纬度

        lat = temp['result']['location']['lat']

        #经度

        lng = temp['result']['location']['lng']

        #地址查找失败

        ifmath.isclose(lat,39.910925,rel_tol=1e-5):

            lat = None

        if math.isclose(lng, 116.413384,rel_tol=1e-5):

            lng = None

    except Exception as e:

        print(e)

        lng = None

        lat = None

    return lng, lat

有了上面这段代码,我们就可以根据小区名称获取对应的百度坐标了。将小区名称另存为文本,在Python中简单2行代码即可搞定!

f=open('./小区名称.txt').readlines()

xy=[[getlnglat('宁波'+addr.split('\n')[0])[0],getlnglat('宁波'+addr.split('\n')[0])[1]] for addr in f]

with open('result.csv','a') as res:

    for each in xy:

       res.writelines(str(each[0])+','+str(each[1])+'\n')

这样坐标的问题就解决了。


1.3数据清洗

原始数据准备就绪,但是接踵而至的问题很多:(1)数据是不是直接可用?显然不是;(2)哪些数据是重要的,哪些是不重要的?这个判断有利于减少工作量;(3)坐标怎么利用?等等。

1.3.1坐标修正

坐标不对,分析结论和实际情况很可能大相径庭!因此,怀着对百度的不信任,需要人工复查一遍坐标。打开arcgis,将小区可视化在地图上。总体上来看,采集的数据点分布比较符合实际情况,呈现“中心密、郊区疏“的分布,且数量与各区县的经济发展情况基本吻合。但是明显可以看到有些点已经超出了宁波市域范围,需要手动修正。


修正完后,我们可以初步观察下高房价都集中在哪些区域。图中比较亮的连片区域就是高房价的集聚区,事实上就是宁波现在的核心片区。如三江口、东部新城、南部新城等。


均价热力图分布

1.3.2增加经验性决策因素

从采集的原始数据字段来看,数据集并未体现很多我们关心的房价影响因素,这些因素有政策的层面的、区位层面的、以及周边开发层面的等。因此我们需要进一步进行数据加工。


首先,567找到了宁波的限购圈范围,据此在arcgis中标志出小区是否在限购圈内,以此反映限购政策对房价的影响,下图(右)中的蓝色点表示在限购圈内。

其次,为了分析地铁站对房价的影响,需要识别小区对应的最近地铁站点,进而计算小区与地铁站的距离。567使用了arcgis中的空间关联方法实现了此步操作。


最后,为了体现小区的区位(核心区、郊区、城乡结合部等),567假设周边设施种类越多且设施数量越多则区位能级越高。为了补充这个数据,需要采集电子地图上的POI数据,这个有点小麻烦。同样,为了节约时间,567直接花了百来块大洋买了一份。这里567使用的是网格匹配法,将小区和POI均关联至所在网格(1000米X1000米),以网格聚合数据作为小区的POI数量和POI类型数。

POI数据样表
网格匹配

1.3.3数据的初步清洗

数据的初步清洗是为下一步的分析和建模做准备,这与建模阶段的数据清洗有所区别,你可以理解为粗加工和精加工的区别。

因为567超级喜欢强大的powerquery,所以这里使用powerbi进行清洗,主要做了以下工作:

[if !supportLists]Ø  [endif]删除毫无意义的字段:链接、地址、经纪人等;

[if !supportLists]Ø  [endif]规范化数据表达:将带单位的面积、总价、建筑面积、首付等转化成数值类型,建造年份转化为日期等;

[if !supportLists]Ø  [endif]奇葩格式规整:去除数据中的空格、换行符、回车符等。


[if !supportLists]Ø  [endif]统一类别表达:将五花八门的房屋朝向表述,统一规范为东西、南北等。


[if !supportLists]Ø  [endif]去重:去除明显重复的数据,最后剩下的可用数据仅有1300多条。

1.3.4提取彩蛋性决策因素

从采集的数据中,我们发现有很多文本类描述信息,这些信息非规范地表达了很多有用信息,比如学区房、在小区中的位置、采光等。这些信息在567看来就是非常重要的“彩蛋“性因素,能利用起来说不定就能提高模型的准确性和分析价值。


为了从文本信息中获取更多信息,一个简单的思路是对文本信息进行分词和权重计算,据此提炼新的决策因素。针对分词,567直接使用现成的jieba包,百度抄一段代码即可。分词完还需要统计各个词的词频或者权重,这里利用jieba包自带的TF/IDF算法计算了下权重,原理可以自行拓展阅读。


# -*- coding:utf-8 -*-

import jieba

import jieba.analyse as anls #关键词提取

import codecs

import re

from collections import Counter


class WordCounter(object):


   def count_from_file(self, file, top_limit=0):

       with codecs.open(file, 'r', 'utf-8') as f:

           content = f.read()

           content = re.sub(r'\s+', r' ', content)#修饰空格

           content = re.sub(r'\.+', r' ', content)

           return self.count_from_str(content, top_limit=top_limit)


   def count_from_str(self, content, top_limit=0):

       if top_limit <= 0:

           top_limit = 100

       tags = jieba.analyse.extract_tags(content, topK=1000)#基于TF/IDF 计算词的权重


       words = jieba.cut(content)

       counter = Counter()

       for word in words:

           if word in tags:

                counter[word] += 1


       return counter.most_common(top_limit)

if __name__ == '__main__':

   counter = WordCounter()

   result = counter.count_from_file(r'./文本信息.txt',top_limit=100)

   with open('词权重.csv','a') as wf:

       for each in result:

           wf.writelines(each[0]+','+str(each[1])+'\n')



关键词权重分布

最终获得了如下关键词的权重:


结论1:词权重越高,在本数据集合里意味着用词较为频繁,因此意味着区分度可能不是很高。反而权重较小的词比如一表生、东首、车位等,更可能体现房子的区位优势、小区中的位置优势以及性价比优势等。

结论2:根据上述分析,在已有字段基础上,考虑增加采光程度、是否拎包入住、是否学区房、小区内的位置(中庭、东首、边套等)、是否送车位、是否送车棚、得房率高否、是否带露台、是否急卖等字段。


先来解决简单的问题,直接从每条记录的文本中提取关键信息,并转化为字段添加至房价数据表中,代码如下。到这里,567认为已经把公开的、轻易就能获取到的数据整理好了,那么就可以进行初步的探索和分析了。

jieba.load_userdict("关键词词典.txt")#需要定义一个词典来存放关键词

f=codecs.open('./文本信息.txt',encoding='utf-8').readlines()

txts=[x.split('\t')[1].split('\n')[0]for x in f]

ty={"配套是否成熟":['配套','商圈'],

    "采光程度":['采光佳','采光无遮挡','采光全天候','采光好','采光充足','采光无影响','采光温和'],

   "是否拎包入住":['拎包'],

    "是否学区房":['一表生','学籍','读书'],

    "是否有电梯":['电梯'],

    "小区位置":['中庭','东首','边套'],

    "是否送车位":['车位'],

    "是否送车棚":['车棚'],

    "得房率高否":['得房率'],

    "是否带露台":['露台'],

    "是否急卖":['急卖']

   }

#注意,txts中的第一个是字段名称

items=[]

item={}

for txtin txts:

    words=jieba.lcut(txt,cut_all=False)

    item={}

    for each in words:


        for i in range(len(ty.values())):


            vl=list(ty.values())[i]#值

            key=list(ty.keys())[i]#key


            if each in vl:

                item[key]=each

    items.append(item)

df=pd.DataFrame(items).fillna(0)

df.replace({'配套':1,'商圈':1,

           '采光佳':'好','采光无遮挡':'好','采光全天候':'好','采光好':'好','采光充足':'充足','采光无影响':'无影响','采光温和':'无影响',

            '拎包':1,

            '一表生':1,'学籍':1,'读书':1,

            '电梯':1,

            '车位':1,

            '车棚':1,

            '得房率':1,

            '露台':1,

            '急卖':1

           },inplace=True)

df['采光程度']=df['采光程度'].replace(0,'无影响')

df['小区位置']=df['小区位置'].replace(0,'其他')

df.to_csv('增加文本挖掘字段.csv',encoding='gbk')


2.数据探索

2.1异常值

利用箱线图发现,均价和面积存在几个明显的异常值,过于极端的数据直接删除,部分异常值依然保留,留待建模过程中通过其他方法进行处理。


2.2数据探索性分析

为了节约时间,这里聚焦分析这一重点,至于作图规不规范、美不美观,请忽略。

2.2.1目标变量分布

从下图可以看出,均价具有较为明显的“正态”分布特性,因此不需要作进一步处理。并且可以看出,宁波中心城的均价大概在2.5万/平米~3.0万/平米之间。


房价频率分布


2.2.2交叉分析

进一步,我们可以分析不同因素对均价的影响,这是为了建立起建模前的一条底线,即我们在追求模型的精度时,不能罔顾实际情况,这就需要对实际情况有大致了解,也就是交叉分析的意义所在,下面举例说明。

面积:面积与房价有一定关系,但并非咱们经验认为的小面等同于高价,大面积等同于低价,事实上90~140平米之间的房价是整个区段中较为明显的低谷区。这与宁波首套房要缴纳的契税规定有关:

140平方米及以上的非普通住宅、商铺、写字楼,140平方米以下的非家庭唯一住房,缴1.5%;面积90平方米以上140平方米以下的家庭唯一住房,缴0.75%;面积90平方米及以下的家庭唯一住房,缴0.5%。

明明面积越大,纳税越高,为什么低谷出现在90~140平米之间呢?567猜测,卖得起140平米以上住宅的基本都是“富人“,房子的品质肯定不用说了,这点税对于他们来说基本可以忽略。我们可以称140平米以上的区间是”富人游戏圈“。

套型:总体上室/厅比例越高,均价越高,说明市场偏向实用性,一个房子里整太多客厅似乎意味着空间的浪费。另外,这里我们也能得到一个启发,可以构造一个新的字段即室/厅比来解释房价。

是否限购:正如预期,限购区外的房价明显低于限购区内的房价。

与地铁站的距离:从图中可以看出,地铁站对房价的影响还是比较明显的,总体上离地铁站越近,均价越高。

是否学区房:这个字段是我们从文本信息中提炼出来的,可以发现,学区效应还是非常明显的,即学区意味着高房价。

3.建模

经过以上复杂的骚操作,我们终于来到了激动人心的建模环节。由于这一部分代码过于复杂,567只讲思路和结果,代码部分就不予展示了。

3.1数据清洗

这一步数据清洗,就是567前文中提到的精加工,由于数据量不大,且数据并不复杂,因此使用到的方法也相对简单,主要工作内容包括:

[if !supportLists]Ø  [endif]识别变量类型:数值型变量和分类型变量需要分别进行处理;

[if !supportLists]Ø  [endif]缺失值填补:567利用了xgboost进行自动插值;

[if !supportLists]Ø  [endif]异常值处理:567采用的是盖帽法。

3.2特征选择

对于数值型变量,采用相关系数进行统计分析,P值小于0.05的特征予以保留。

对于类别型变量,采用方差检验判断特征的重要性,同样对于P值小于0.05的特征予以保留。

3.3模型比选和调参

567使用了pycaret进行模型的比选,可以发现Catboost和XGboost整体预测效果最好,且两个模型效果较为接近,同时虑到xgb具有良好的兼容性、以及运行时间较短,最终选择xgb进行调参。

567使用了网格搜索法进行超参数调优,调参后模型预测性能明显提升,模型在测试集上的解释能力达到了75%,这个结果还是比较让人满意的。

initial-train XGB: error=312360.108, r2=0.879

initial-test XGB: error=16346142.845, r2=0.755

当然,一些繁杂的过程表诉被省略了,如dummy。最后,关键的一步,我们还需要验证残差的分布,发现残差具有较好的正态分布特性。综上,认为模型还是比较合适的。

残差分布图

3.4模型解释

这里咱们就回到了本次分析的目的,如果你还记得的话,我们需要回答两个问题:(1)影响房价高低的因素中哪些是关键的(2)这些关键因素是如何体现制约和限制作用的。因此,模型解释是建模过程中非常关键的一步,一般模型自带的特征分析功能往往不是非常理想,所以567使用了SHAP分析方法对模型进行解释。

针对第一个问题我们分析了特征的重要性排名,这里567只提下关键因素和重要因素,这些也是建议买房者需要着重考虑的因素。

[if !supportLists]Ø  [endif]关键因素:可以发现在所有可能对房价有影响的因素中,是否限购这种政策类因素是最为显著的,这也就印证了无数的事实,限制意味着爆发!

[if !supportLists]Ø  [endif]重要因素:POI类型数量、POI数量体现了小区周边开发的成熟度,这两个因素对房价影响也较为明显。此外,楼层数、与地铁站距离、面积大小,也是房价高低的考虑因素。


针对第二个问题我们给出了SHAP图,这里567也只捡重要的讲一下。

[if !supportLists]Ø  [endif]图怎么看:SHAP图中每一行代表一个影响因素,横坐标为SHAP值(值越高说明影响越大,正值表示对房价有促进作用,负值表示对房价有抑制作用)。一个点代表一个样本,颜色表示该样本的影响因素值(红色高,蓝色低)。需要注意的是,这里的影响因素是经过dummy处理的,所以是否限购_0,红色的点其实表示的是”是”,蓝色的点表示的是“否“。

[if !supportLists]Ø  [endif]关键影响因素:限购政策对于房价具有绝对影响,限购圈内的房价明显高于限购圈外的房价;

[if !supportLists]Ø  [endif]重要影响因素:(1)周边配套越成熟越能显著推高房价,这里POI类型数量(反映了业态的丰富程度)比POI数量本省更具有明显的正向推动作用;(2)楼层数越高房价越高,这一点不好理解,主要是由于宁波老房子普遍较低,因此楼层越高,在宁波很可能就代表了那些新开的楼盘,新开楼盘的价格普遍较高;(3)离地铁站的距离的作用,在SHAP图中并不明显,所以后面我们单独进行分析;(4)房屋面积方面,90-120平米的房价会是比较尴尬的,这个区间内面积越大,均价越低;(5)4室的房子对房价拉动作用明显,可以理解为4室的户型在市场上较为符合购房者的空间布置需求;(6)房龄越大,价格竟然越高,这是567没有料想到的,但是如果结合周边配套进行考虑,也就能够理解了,一般房子越老,周边配套越成熟,周边说不定就有你心仪的学区房。


为了更加清晰地看到轨道站点远近对于房价的影响,567绘制了距离与SHAP值的关系图,在各种因素的交互影响下和现状轨道线网布局下(3条线),适中的距离对于房价拉动作用更为明显(距离轨道站点1-3公里范围内)。这些小区本身位于高品质或高人气的鄞州公园、万达广场、印象城等附近,生活娱乐便利,同时1-3公里的距离又是共享单车、公共自行车等交通工具接驳的适宜范围,所以地铁站进一步提升了这些小区的交通便利性。离轨道越近的小区价格提升作用不明显似乎比较费解,事实上宁波市轨道交通的线站位选择原则一般尽量避免直穿成熟区,以减少拆迁成本,所以周边的配套往往并不完善。


当然,在这些因素的交互下,是否为学区房这一我们关心的因素并未凸显出来,这很可能是因为,在树模型的作用下,这一因素被房龄等因素所覆盖了。

你可能感兴趣的:(房价数据分析:爬虫、建模到解释全流程)