存档2019年9月25日

地图坐标系转换API

一、国内的常用坐标系

1、WGS-84坐标系:地心坐标系,GPS原始坐标体系

在中国,任何一个地图产品都不允许使用GPS坐标,据说是为了保密。

2、GCJ-02 坐标系:国测局坐标,火星坐标系

1)国测局02年发布的坐标体系,它是一种对经纬度数据的加密算法,即加入随机的偏差。

2)互联网地图在国内必须至少使用GCJ-02进行首次加密,不允许直接使用WGS-84坐标下的地理数据,同时任何坐标系均不可转换为WGS-84坐标。

3)是国内最广泛使用的坐标体系,高德、腾讯、Google中国地图都使用它。

3、CGCS2000坐标系:国家大地坐标系

该坐标系是通过中国GPS 连续运行基准站、 空间大地控制网以及天文大地网与空间地网联合平差建立的地心大地坐标系统。

4、BD-09坐标系

百度中国地图所采用的坐标系,由GCJ-02进行进一步的偏移算法得到。

5、搜狗坐标系

搜狗地图所采用的坐标系,由GCJ-02进行进一步的偏移算法得到。

6、图吧坐标系

图吧地图所采用的坐标系,由GCJ-02进行进一步的偏移算法得到。

以上原文地址:https://blog.csdn.net/m0_37738114/article/details/80452485

二、常用地图使用的坐标系

百度地图————BD09————在GCJ-02坐标系基础上再次加密(境外为WGS-84)
高德地图————GCJ-02———由WGS-84加密而来
google地图———GCJ-02———由WGS-84加密而来(境外为WGS-84)
腾讯地图————GCJ-02———由WGS-84加密而来
bing地图————WGS-84———原始坐标

三、常用地图坐标系转换API

  1. 其他坐标系转高德坐标:官方链接

API:
      https://restapi.amap.com/v3/assistant/coordinate/convert?
      locations=116.481499,39.990475&coordsys=gps&output=xml&key=<用户的key>
参数:
      coordsys:gps;mapbar;baidu;autonavi(不进行转换)
      output:JSON,XML
  1. 其他坐标系转百度坐标:官方链接

API:
      http://api.map.baidu.com/geoconv/v1/?
      coords=114.21892734521,29.575429778924&from=1&to=5&ak=你的密钥
参数:
from:源坐标类型
1:GPS设备获取的角度坐标,WGS84坐标;
2:GPS获取的米制坐标、sogou地图所用坐标;
3:google地图、soso地图、aliyun地图、mapabc地图和amap地图所用坐标,国测局(GCJ02)坐标;
4:3中列表地图坐标对应的米制坐标;
5:百度地图采用的经纬度坐标;
6:百度地图采用的米制坐标;
7:mapbar地图坐标;
8:51地图坐标

to:目标坐标类型:
3:国测局(GCJ02)坐标;
4:3中对应的米制坐标;
5:bd09ll(百度经纬度坐标);
6:bd09mc(百度米制经纬度坐标)

四、应用场景

先介绍2款通过地名获取地图坐标的api:

百度:
http://api.map.baidu.com/geocoder?address=解放碑&output=json&key=你的密钥&city=重庆

高德:
https://restapi.amap.com/v3/geocode/geo?address=解放碑&output=json&key=你的密钥&city=重庆

使用过程中,个人感觉高德获取的结果更完整一些。通过高德获取的坐标系,可以直接用于Excel的三维地图、PowerBI地图(直观上看没什么偏移量,具体差异是否有差异还需要考证)。
但如果使用Python,地理数据可视化较好的一个选择是pyecharts,echarts的坐标又基于百度,所以又要采用百度的坐标系。
所以,才需要上面这些转化。不过都没有这些地图坐标转WGS84的。

使用Scrapy创建爬虫和常用命令

本文主要记录Scrapy的常用命令,用于备忘。适用于Windows平台。
例如,我们要爬取这个网站:https://www.tudinet.com/market-252-0-0-0/ 重庆地区的土地转让信息。

整体流程如下:

1、使用scrapy startproject cq_land命令创建项目
2、修改settings.py,使爬虫生效(ITEM_PIPELINES、 USER_AGENT 等)
3、修改items.py,用于存储爬取回来的数据
4、使用scrapy genspider tudinet tudinet.com 命令,创建爬虫文件,用于爬取网页内容
5、编写上一步生成的tudinet.py爬虫文件,完成网页内容解析
6、修改pipelines.py文件,对获取到的信息进行整理,完成存储
1、万事第一步:创建工程

首先在cmd或powershell窗口,CD到想要创建项目的目录,然后输入以下命令,创建了一个名为cq_land的项目。

scrapy startproject cq_land
PS C:\WINDOWS\system32> e:
PS E:\> cd E:\web_data
PS E:\web_data> scrapy startproject cq_land
New Scrapy project 'cq_land', using template directory 'c:\\programdata\\anaconda3\\lib\\site-packages\\scrapy\\templates\\project', created in:
    E:\web_data\cq_land

You can start your first spider with:
    cd cq_land
    scrapy genspider example example.com
PS E:\web_data>

这样就生成了一个cq_land的文件目录(完成后先不要关闭终端窗口,后面第4步还会用到)。接下来,我们主要针对items.py、settings.py、pipelines.py和spiders文件夹进行修改。

2、修改settings.py,使爬虫生效

将settings.py中,ITEM_PIPELINES 附近的注释去掉,修改为:

修改前:
#ITEM_PIPELINES = {
#    'cq_land.pipelines.CqLandPipeline': 300,
#}

修改后:
ITEM_PIPELINES = {
    'cq_land.pipelines.CqLandPipeline': 300,
}

有些网站可能需要设置USER_AGENT,所以,加上USER_AGENT防止一般的网站反爬。

USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"
3、修改items.py,用于存储爬取回来的数据

土地出让信息

观察数据结构,主要信息有很多字段,以获取标题和推出时间为例,定义两个item项目(items里面定义的内容,可以理解为定义了一个命名为item的字典,每个定义的项目最为键值对存储在item字典中——键值对存储的内容可以是任何Python对象[常用的字符串、列表等]),修改items.py文件内容如下:

class CqLandItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    title = scrapy.Field() # 出让地标题
    list_time = scrapy.Field() # 推出时间
4、创建爬虫文件,用于爬取网页内容

这时候返回到cmd或powershell终端,cd进入cq_land目录,然后创建名为tudinet的爬虫文件,用于爬取土流网的数据。

PS E:\web_data> cd cq_land
PS E:\web_data\cq_land> scrapy genspider tudinet tudinet.com
Created spider 'tudinet' using template 'basic' in module:
  cq_land.spiders.tudinet
PS E:\web_data\cq_land>

完成后,将在spider文件夹下产生一个叫tudinet.py的文件。这个文件就是定义爬虫怎么解析网页的文件,解析的内容怎么存储到刚才定义好的item中。

5、编写tudinet.py文件,完成网页内容解析

首先需要将刚才定义好的item内容import进来,然后修改start_urls为我们要爬取的网页(这里我们只爬取一个网页作为示例)。然后在parse函数中定义处理过程,并返回结果,代码如下:

import scrapy
from cq_land.items import CqLandItem

class TudinetSpider(scrapy.Spider):
    name = 'tudinet'
    allowed_domains = ['tudinet.com']
    start_urls = ['https://www.tudinet.com/market-252-0-0-0']

    def parse(self, response):
        item=CqLandItem()
        item['title']=response.xpath("//div[@class='land-l-bt']/text()").extract()
        item['list_time']=response.xpath("//div[@class='land-l-cont']/dl/dd/p[1]/text()").extract()
        
        return item

以上是针对单个网页,但实际上我们爬虫多半是需要针对整个网站的所有土地转让信息进行爬取的,因此,我们根据网站翻页的变化,一共有100页可以供我们爬取。因此,我们可以在开头对start_urls进行重新定义,用以爬取这100个页面。

这里有2种处理方式,第一种处理方式,就是直接把这100个url作为列表放到start_urls 中。其他的就不用改动了。

第二种处理方式,就是重新定义start_requests函数。实际上定义这个函数就是把start_urls列表里面的地址用函数生成,然后再通过callback参数设置回调函数,让parse函数来处理Request产生的结果。

import scrapy
from cq_land.items import CqLandItem
from scrapy.http import Request

class TudinetSpider(scrapy.Spider):
    name = 'tudinet'
    allowed_domains = ['tudinet.com']
    # start_urls = ['https://www.tudinet.com/market-252-0-0-0']
    
    def start_requests(self):
        init_url='https://www.tudinet.com/market-252-0-0-0/list-pg'
        for i in range(1,101):
            yield Request("".join([init_url,str(i),'.html']),callback=self.parse)

    def parse(self, response):
        item=CqLandItem()
        item['title']=response.xpath("//div[@class='land-l-bt']/text()").extract()
        item['list_time']=response.xpath("//div[@class='land-l-cont']/dl/dd/p[1]/text()").extract()
        
        return item

实际上,对于有多个层次的网页,例如某些论坛,有很多文章列表。我们首先需要访问首页获取页面总数,然后遍历所有页面获取每个帖子的url,最后通过访问每个帖子的地址获取文章的详细信息。那么我们的爬虫文件结构如基本如下:

class abc_Spider(scrapy.Spider):
    name='abc'
    allowed+domains=['abc.com']
    start_urls=['论坛的首页']
    # 获取总页数
    def parse(self,response):
        pages=response.xpath("//xxxx//").extract()
        # 这里省略了将pages由字符转数字的过程
        for i in range(1,int(pages)):
            yield Request("".join(['xx.abc.com',str(i),"xxx"]),callback=self.get_detail_urls)
    # 获取所有文章详情页的页面url
    def get_detail_urls(self,response):
        detail_urls=response.xpath("//xxxx//").extract()
        for url in detail_urls:
            yield Request(url,callback=self.parse_content)
    # 对详情页内容进行解析,并返回结果
    def parse_content(self,response):
        item=abcitem()
        item['xx']=response.xpath("//xxxx//").extract()
        return item
6、修改pipelines.py,对获取到的信息进行整理,完成存储

将pipelines.py修改为如下内容,爬取的内容将会存储到cq_land.csv文件中。

import pandas as pd

class CqLandPipeline(object):        
    def process_item(self, item, spider):
        title=item['title']
        list_time=item['list_time']
        data=pd.DataFrame([title,list_time],index=['标题','推出时间']).T
        data.to_csv('cq_land.csv',index=False,encoding='gb2312')
        return item

需要注意的是,上述存储方式适用于单个网页的爬取。如果是多个网页,需要在data.to_csv中添加参数,mode=’a’,表示以追加的方式添加数据,同时应注意数据的列标题问题。另外,也可以使用数据库等方式在这里将数据直接存储到数据库。

7、使用scrapy crawl tudinet 运行爬虫

完成上述文件编辑后,返回cmd或powershell终端,运行:scrapy crawl tudinet

PS E:\web_data\cq_land> scrapy crawl tudinet
2019-04-15 19:35:34 [scrapy.utils.log] INFO: Scrapy 1.5.1 started (bot: cq_land)
2019-04-15 19:35:34 [scrapy.utils.log] INFO: Versions:
……
……

没有意外的话,上述代码会产生一个cq_land.csv文件,打开文件如果内容正确就说明爬虫编写成功了。
当然,scrapy crawl tudinet还可以带参数,用于显示日志的级别:

CRITICAL - 严重错误(critical)
ERROR - 一般错误(regular errors)
WARNING - 警告信息(warning messages)
INFO - 一般信息(informational messages)
DEBUG - 调试信息(debugging messages)

可以使用以下方法按需要显示日志
# 完全不输出日志
scrapy crawl tudinet --nolog
# 按默认输入日志
scrapy crawl tudinet -L DEBUG

以上就是scrapy爬虫的基本形式。
通常情况下,对于不熟悉scrapy的情形下,可能对scrapy产生的内容不了解,不知道哪里出了问题,这里可能需要用到scrapy的另一命令,scrapy shell url。这个命令会直接爬取url的内容,然后在终端中,通过ipython终端的方式产生交互,用户可以使用response.xpath() 等方法测试返回结果。如response.url就是请求的url。

PS E:\web_data\cq_land> scrapy shell https://www.tudinet.com/market-252-0-0-0
2019-04-15 20:21:03 [scrapy.utils.log] INFO: Scrapy 1.5.1 started (bot: cq_land)
2019-04-15 20:21:03 [scrapy.utils.log] INFO: Versions: lxml 4.2.1.0, libxml2 2.9.8, cssselect 1.0.3, parsel 1.5.1, w3lib 1.19.0, Twisted 18.9.0, Python 3.6.5 |Anaconda, Inc.| (default, Mar 29 2018, 13:32:41) [MSC v.1900 64 bit (AMD64)], pyOpenSSL 18.0.0 (OpenSSL 1.0.2o  27 Mar 2018), cryptography 2.2.2, Platform Windows-10-10.0.17763-SP0
……
……
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x000001DAF8807358>
[s]   item       {}
[s]   request    <GET https://www.tudinet.com/market-252-0-0-0>
[s]   response   <200 https://www.tudinet.com/market-252-0-0-0>
[s]   settings   <scrapy.settings.Settings object at 0x000001DAF9B70898>
[s]   spider     <TudinetSpider 'tudinet' at 0x1daf9532f28>
[s] Useful shortcuts:
[s]   fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s]   fetch(req)                  Fetch a scrapy.Request and update local objects
[s]   shelp()           Shell help (print this help)
[s]   view(response)    View response in a browser
In [1]:
附:scrapy的命令列表

全局的命令有:

startproject :创建项目(常用必须)
genspider :创建爬虫(常用必须)
settings :获取当前的配置信息,通过scrapy settings -h可以获取这个命令的所有帮助信息
runspider :未创建项目的情况下,运行一个编写在Python文件中的spider
shell : 在终端窗口请求一个网址,可用于探索爬取获得的内容(常用)
fetch :过scrapy downloader 讲网页的源代码下载下来并显示出来
view :将网页document内容下载下来,并且在浏览器显示出来
version :查看版本信息,并查看依赖库的信息

项目命令有:

crawl :运行爬虫(常用必须)
check : 检查代码是否有错误
list :列出所有可用爬虫
edit :edit 在命令行下编辑spider ### 不建议运行
parse 
bench

使用sklearn的回归模型进行二手房估值(工程化过程)

本文主要内容

背景部分:数据背景和需求
第一部分:建模过程和需要解决的问题
第二部分:数据预处理,解决训练和提交估值数据的预处理
第三部分:模型训练和持久化
第四部分:使用训练好的模型进行估值
第五部分:提供数据接口(此部分待完善)

开篇之前,这里有一篇从入门到精通做二手房估值模型的文章:用机器学习自制二手房估价模型。可以了解一下数据获取的方式,线性回归的基本概念等。
这篇文章流程虽完整,但实际上还是没有完成工程化(训练数据和预测数据同时预处理,没有考虑新数据传来的情况),本文将完善此过程。
这里爬虫部分就不再介绍,同时为了方便,也不再爬取地理位置对应的地铁站、学校、医院数量等。提取了部分用于建模的字段如下(不要在意字段命名):

二手房数据

文档链接:[重庆二手房数据](https://pan.baidu.com/s/1gHPyoJhYSFxb3dcspiz0aQ 提取码: 5ib9 )
文档中有list_price(挂牌价)、city(城市)、building(小区)等数据,暂不纳入模型的变量(挂牌价对于新数据其实是不存在的;city目前只有重庆;building太多,需要用到卡方分箱的方法处理,暂时删除),因此直接删除。其余字段解释如下:

deal_price:成交价(万元)
deal_date:成交日期
district:区
sub_district:区块
room_type:房屋类型(高层、别墅、洋房、车位)
house_rooms:几室
house_halls:几厅
house_kitchen:几厨
house_toilets:几卫
size:建筑面积㎡
house_stru:室内结构(跃层、平层等)
build_year:建筑年份
fixture:装修情况
build_stru:建筑结构
lift_family:梯户比
property_year:产权年限
lift:是否有电梯
house_type:产权属性(商品房、经济适用房等)

第一部分:建模过程和需要解决的问题

1.1 数据预处理:将数据处理成适合建模的数据

  • 变量筛选
  • 缺失值填充
  • 虚拟变量构建
  • 异常值处理
  • 相关性和多重共线性检查
  • 附:需要考虑如何对需要预测数据的预处理

1.2 模型训练、调参和可视化:选定建模方法进行模型训练
由于我们需要预测的成交价是一个连续型变量,因此模型需要选择回归类的模型进行训练。回归模型有以下几类:
(1)线性回归(Linear Regression)
(2)逻辑回归(Logistic Regression)
(3)多项式回归(Polynomial Regression)
(4)逐步回归(Stepwise Regression)
(5)岭回归(Ridge Regression)
(6)套索回归(Lasso Regression)
(7)弹性回归(ElasticNet Regression)
详细介绍回归分析的文章:你应该掌握的 7 种回归模型!

1.3 模型效果评估:评估模型的效果
在sklearn中包含四种评价尺度,分别为mean_squared_error、mean_absolute_error、explained_variance_score 和 r2_score。

y:即目标值
y_:即预测值
y-:即目标值的均值
m:样本量

(1) 均方误差(mean-squared-error)
    MSE:Σ(y-y_)^2/m
    RMSE:sqrt(MSE)
(2) 平均绝对值误差(mean_absolute_error)
    MAE:Σ|y-y_|/m
(3) 可释方差得分(explained_variance_score) 
    EVS:1-var{y-y_}/var{y}
(4) 中值绝对误差(Median absolute error)
    MedAE=meadian(|y1,y_1|,...,|yn-y_n|)
(5) R2 决定系数(拟合优度):模型越好:r2→1,模型越差:r2→0
    RSS=Σ(y-y_)^2
    ESS=Σ(y-y-)^2
    R2=1-RSS/TSS

1.4 模型持久化和预测:如何将训练的模型应用于新的数据评估?

  • 可以使用sklearn的持久化工具joblib.dump和joblib.load进行训练模型的存储和读取,用以新数据的预测。

第二部分:数据预处理,解决训练和提交估值数据的预处理

2.1 字段分析
回到数据中,我们对数据字段进行以下分类:
目标字段:deal_price
数值型字段:house_rooms、house_halls、house_kitchen、house_toilets、size
需要转化为数值型的字段:lift_family
日期型数据:deal_date,build_year
字符型字段:除上述字段外的其他字段。

字段预处理方法如下:
(1)对于梯户比(lift_family),要读取文本中的电梯数和户数,从而计算梯户比(电梯数/户数),没有数据的,则返回0。
(2)对于数值型字段:填充缺失值为0(此方法过于粗暴,需要在后续参数调整过程进行优化)
(3)对日期型字段:需要处理成日期到当前时间的时间差
(4)对于字符型字段:通常采用onehot编码进行处理。缺失值归为一类,填充为“miss”。
通过以上的简单处理,就得到了我们准备好的可供训练或者预测的数据。

数据预处理程序构建思路
(1)我们可以定义一个pre_process类,将上述四种类型的数据预处理方法封装到该类中。
(2)由于梯户比处理方法是独立生成中文-数字对照表,因此可以将该函数独立于pre_process类,然后在pre_process中引用该方法处理的结果进行转换。
数据预处理文件DataProcess.py整体结构如下:

DataProcess.py
# 生成1000以内的汉字和数字对应表
def chnum_to_num(output=0):
      ...
# 数据预处理类
class pre_process():
    # 定义初始化函数,用于辨别类被用于训练还是预测
    def __init__(self,method='train'):
        self.method=method
    # 处理梯户比
    def lift_proecess(self,data):
        ...
    # 处理日期
    def date_proecess(self,data):
        ...
    # 处理字符型:onehot处理
    def onehot(self,data):
        ...

2.2 定义梯户比处理函数
梯户比数据:“两梯八户”。这里的梯和户都是中文,需要定义一个函数进行转换。好在数据整体比较规范,除了零到十的汉字外,只有一个特例:两(和二相同)。从常识上判断,我们只需生成1-999范围内的中文-数字对照表就可以。

# 生成1000以内的汉字和数字对应表
def chnum_to_num(output=0):
    ch_num=['零','一','二','三','四','五','六','七','八','九']
    for hundred in range(10):
        # 如果百位是0,则百位为空,否则返回百位数
        if hundred==0:
            hundred_ch=""
        else:
            hundred_ch=ch_num[hundred]+"百"
        # 循环十位
        for ten in range(10):
            # 如果百位和十位都是0,则直接跳过,列表中已包含数据
            if hundred==0 and ten==0:
                continue
            # 百位为0,十位为1的情况
            elif hundred==0 and ten==1:
                ten_ch='十'
            # 百位为不为零,十位为零的情况
            elif ten==0:
                ten_ch="零"
            else:
                ten_ch=ch_num[ten]+"十"
            # 循环个位
            for one in range(10):
                # 整百的情况
                if hundred>0 and ten==0 and one==0:
                    ch_num.append("".join([hundred_ch]))
                # 个位是0
                elif one==0:
                    ch_num.append("".join([hundred_ch,ten_ch]))
                else:
                    one_ch=ch_num[one]
                    ch_num.append("".join([hundred_ch,ten_ch,one_ch]))
    num=list(range(0,1000))
    num_dict=dict(zip(ch_num,num))
    # 添加“两”
    num_dict['两']=2
    # 使用joblib存储文件
    if output==0:
        joblib.dump(num_dict,path+'templet/chnum_to_num.pkl')
    else:
        return num_dict

2.3 pre_process类中的梯户比处理函数
通过chnum_to_num产生的中文和数字对照文件chnum_to_num.pkl,将lift_family列转换为梯户比数值数据。

    # 处理梯户比(将梯户数转化为梯户比数据)
    def lift_proecess(self,data):
        num_dict=joblib.load(path+'templet/chnum_to_num.pkl')
        # 获取梯和户的汉字
        data['ti_ch']=data['lift_family'].map(lambda x: re.findall('(.*)梯',x)[0] if len(re.findall('(.*)梯',x))>0 else None)
        data['hu_ch']=data['lift_family'].map(lambda x: re.findall('梯(.*)户',x)[0] if len(re.findall('梯(.*)户',x))>0 else None)
        data['ti']=data['ti_ch'].map(lambda x: num_dict[x] if x is not None else None)
        data['hu']=data['hu_ch'].map(lambda x: num_dict[x] if x is not None else None)
        data['tihu_rate']=data['ti']/data['hu']
        # 填充缺失值为0
        data['tihu_rate'].fillna(0,inplace=True)
        # 删除多余字段
        data.drop(['lift_family','hu_ch','ti_ch','ti','hu'],axis=1,inplace=True)
        return data

2.4 pre_process类中的日期数据处理
数据中包含两个日期相关的字段。
deal_date:即成交日期。房产的价格实际上是和时间高度相关的。这里计算成交日期和当前日期之间的月份数,用以反映时间变量。要预测数据时,默认为要预测当天的数据,也可以输入一个历史日期,来预测历史上这样的房产售价会是多少。
build_year:建筑的年份,通常年份越久远房产估值会更低。所以,计算建筑年份和当前年份的年数差异。对缺失数据通过同小区的建筑年度均值填充,没有同小区数据则使用全部数据的均值填充。

    # 将日期数据转化成为距离当前日期的时间段
    def date_proecess(self,data):
        ###### 成交日期/或需要预测的日期距离当前日期的月份
        data['deal_delta']=datetime.now()-data['deal_date']
        data['deal_month']=data['deal_delta'].apply(lambda x: x.days/30)
        ###### 建筑年份,计算建筑距离现在有多少年
        data['build_year']=date.today().year-data['build_year']
        # 填充缺失值
        # 方法:按小区计算平均建筑年代,如果缺失数据没有数据,则用全部数据的均值
        build_avg_year=data.groupby('building')['build_year'].mean()
        avg_year=data['build_year'].mean()
        build_avg_year.fillna(avg_year,inplace=True)
        build_avg_year=pd.DataFrame(build_avg_year)
         # 生成辅助列
        year_data=pd.merge(data[['building','build_year']],build_avg_year,how='left',left_on='building',right_index=True)
        # 将填充缺失值
        data['build_year'].fillna(year_data['build_year_y'],inplace=True)
        # 删除生成的过程数据
        data.drop(['deal_date','deal_delta'],axis=1,inplace=True)
        return data

2.5 pre_process类中的字符型数据处理
为简便起见,我们对剩余的字符型数据的缺失值,使用一个单独类“miss”进行填充。
如果是训练过程,则需要分别存储所有字段的所有值信息。
接下来的onehot编码过程就需要读取上述存储的数据,对每个字段进行编码。这里不采用pandas自带的onehot编码函数pd.get_dummies的原因是这样处理是一次性的,不方便后期新数据的处理。

    # onehot处理
    def onehot(self,data):
        data.drop(['building'],axis=1,inplace=True)
        var_list=['district','sub_district','room_type','house_stru','fixture','build_stru','property_year','lift','house_type']
        # 填充缺失值为“miss”,作为单独一类
        for var in var_list:
            data[var].fillna('miss',inplace=True)
        # 如果是训练数据,则先要讲所有onehot的字典存储下来(存储后再调用来进行onehot编码),如果不是则直接调用训练时所存储的数据进行编码
        if self.method=='train':
            for var in var_list:
                var_values=list(set(data[var]))
                joblib.dump(var_values,path+'templet/{}.pkl'.format(var))
                # 另一种方法,le_class.classes_
                # le_class = preprocessing.LabelEncoder()
                # le_class.fit(data[var])
        # 读取每一个编码,进行onehot处理
        for var in var_list:
            print("    ...进行onehot编码,当前变量:{}".format(var))
            var_values=joblib.load(path+'templet/{}.pkl'.format(var))
            var_cols=[var+"_"+str(j) for j in range(len(var_values))]
            # 生成样本量*变量值数量的全0矩阵
            temp_data=pd.DataFrame(data=np.zeros((len(data),len(var_values))),columns=var_cols)
            # 完成onehot处理,在合适的位置填充1
            for i in temp_data.index:
                temp_data.iloc[i,var_values.index(data[var][i])]=1
            # 将完成了onehot编码的数据合并到DATA中
            data=pd.merge(data,temp_data,left_index=True,right_index=True)
        # 删除已经完成了onehot编码的数据
        data.drop(['district','sub_district','room_type','house_stru','fixture','build_stru','property_year','lift','house_type'],axis=1,inplace=True)
        return data

以上过程就基本完成了数据的预处理函数的定义,可以通过依次调用上述函数,完成整体的数据预处理。

第三部分:模型训练和持久化

这一分部将生成一个train.py,这个文件主要完成模型训练和模型持久化。

3.1 使用第二部分定义的函数进行数据预处理
首先我们import上面写好的文件:

import fuction_tools.DataProcess as dp

我们再定义一个data_preprcess函数,集成所有数据预处理函数进行数据预处理。这里,除了第二部分要做的处理外,我们还有一些卧室、客厅数量等数值型数据,需要填充缺失值,缺失值均填充为0。

def data_preprcess(data,method='train'):
    '''
    data: 需要进行数据预处理的数据,pd.DataFrame格式
    method: 如果是train,则表示是训练过程,onehot步骤会存储onehot编码码表
    retrun: 返回预处理完成后的数据
    '''
    # 数据预处理部分
    print("正在进行训练模型的预处理:")
    pre_p=dp.pre_process(method=method)
    # 处理梯户比
    print("  训练模型预处理:处理梯户比数据")
    data=pre_p.lift_proecess(data)
    # 处理日期
    print("  训练模型预处理:处理日期数据")
    data=pre_p.date_proecess(data)
    # 进行onhot编码
    print("  训练模型预处理:onhot编码")
    data=pre_p.onehot(data)
    # 填充室厅卫厨的缺失值为0
    print("  训练模型预处理:填充室厅卫厨的缺失值为0")
    varlist=['house_rooms','house_halls','house_kitchen','house_toilets']
    for var in varlist:
        data[var].fillna(0,inplace=True)
    # data.to_csv('abc.csv',encoding='gb2312')
    return data

通过data_preprcess函数,即可以将我们输入的数据转换成为可以用于模型训练的成品数据。

3.2 使用随机森林回归进行训练
这里参考了开篇提及的文章中所使用的方法。使用joblib.dump保存训练结果。

# (1)使用随机森林回归法,进行模型训练并保存训练结果
def rdf_train_data(data):
    # start_time=datetime.now()
    # 打乱数据顺序
    data=data.reindex(np.random.permutation(data.index))
    Y_train=data['deal_price']
    X_train=data.drop(['deal_price'],axis=1)
    
    # 调用scikit-learn的网格搜索,传入参数选择范围,并且制定随机森林回归算法,cv = 5表示5折交叉验证
    param_grid = {"n_estimators":[5,10,50,100,200,500],"max_depth":[5,10,50,100,200,500]}
    grid_search = GridSearchCV(RandomForestRegressor(),param_grid,cv = 5)
    
    # 让模型对训练集和结果进行拟合
    grid_search.fit(X_train,Y_train)
    print("随机森林回归模型的拟合优度R^2为:"+str(np.around(grid_search.best_score_,4)))
    # 存储训练的模型
    joblib.dump(grid_search,path+'models/grid_search.pkl')
    # end_time=datetime.now()
    # user_minte=round((end_time-start_time).seconds/60,1)
    # print("共用时:"+str(user_minte)+"分钟")

3.3 使用线性回归、岭回归等模型进行训练
定义三种模型方法,按照用户的输入,进行训练并输出模型结果。

# (2)使用线性回归、岭回归等模型进行训练
def linear_train_data(data,t_method='Ridge'):
    '''
    data: 要训练的数据,来源于data_preprcess
    method: {Ridge,Lasso,LinearRession},三种训练模型,默认为Ridge岭回归
    '''
    # 定义训练方法
    methods={'LinearRession':linear_model.LinearRegression(fit_intercept=True),
             'Lasso':linear_model.Lasso(),
             'Ridge':linear_model.Ridge()}
    linear=methods[t_method]
    # 划分训练集和测试集
    Y=data['deal_price']
    X=data.drop(['deal_price'],axis=1)
    # X_train,X_test,Y_train,Y_test=train_test_split(X,Y,test_size=0.1,random_state=100)
    linear.fit(X,Y)
    joblib.dump(linear,path+'models/{}.pkl'.format(t_method))
    # 使用模型进行预测
    print(t_method+"模型的拟合优度R^2为:"+str(np.around(linear.score(X,Y),4)))

上述过程定义了4种模型进行训练,并保存了模型结果。

第四部分:使用训练好的模型进行估值

4.1 定义一个统一的预测接口,方便调用各种方法进行预测
通过定义sh_predict函数,后期数据预测调用该函数就可以对新数据进行预测,并输出预测结果。

# 定义预测方法函数
def sh_predict(data,p_predict='grid_search'):
    '''
    data:要预测的数据(已完成数据预处理),需要和训练模型的基本数据字段相同,可以参考dataprocess.py,有相关介绍
    p_predict:{grid_search,Ridge,Lasso,LinearRession},四种训练模型,默认为grid_search随机森林回归
    '''
    linear=joblib.load(path+'models/{}.pkl'.format(p_predict))
    predict_y=linear.predict(data)
    return predict_y

4.2 进行预测和输出各种模型的预测结果
上面定义好了各种函数,这里读取数据,调用函数进行预处理,调用函数进行预测,存储结果数据就可以。

    path='F:/second_hs/'
    # 读取数据
    print("读取数据...")
    p_data=pd.read_excel(path+"predict_house.xlsx",
                           parse_dates=['deal_date'])
    r_data=p_data.copy()
    # 进行数据预处理
    pp_data=data_preprcess(p_data,method='predict')
    pp_data.drop('deal_price',inplace=True,axis=1)
    # 进行预测
    for mthd in ['Ridge','Lasso','LinearRession','grid_search']:
        print("使用:{}方法进行预测。".format(mthd))
        predict_y=sh_predict(pp_data,p_predict=mthd)
        r_data[mthd]=predict_y
    # 导出数据
    r_data.to_csv(path+'predict_result.csv',index=False)
    print("预测完成,请查看文件。")

结果评估方面就暂时不展示了。由于这里的数据预处理过程比较粗略,同时对数据的相关性、多重共线性等未作处理,模型效果实际上并不是很理想。这些都是要在后期模型优化需要做的工作。

第五部分:提供数据接口(此部分待完善)

这部分就暂时不写了,主要是提供网页接口,提供面向用户的二手房估值功能。

以上,由于水平有限,可能会有一些问题。欢迎讨论!

Pandas DataFrame的loc、iloc、ix和at/iat浅析

前段时间看Think Python里面有句话记忆犹新,大概意思是:有时候Python让我们感到困惑,是因为实现一个效果的方法太多,而不是太少。

确实如此,Pandas的DataFrame数据选取就存在这样的问题。本来理解列表索引(了解列表索引请参考:一张图弄懂python索引和切片)就已经很困难了,DataFrame还带这么多方法。

废话少说,直接上结果。

1、loc:通过标签选取数据,即通过index和columns的值进行选取。loc方法有两个参数,按顺序控制行列选取。

#示例数据集
df=pd.DataFrame(np.arange(12).reshape(4,3),columns=list('abc'),index=list('defg'))

df
Out[189]: 
   a   b   c
d  0   1   2
e  3   4   5
f  6   7   8
g  9  10  11

#直接索引行
df.loc['d']
Out[190]: 
a    0
b    1
c    2
Name: d, dtype: int32

#索引多行
df.loc[['d','e']]
Out[191]: 
   a  b  c
d  0  1  2
e  3  4  5

#索引多列
df.loc[:,:'b']
Out[193]: 
   a   b
d  0   1
e  3   4
f  6   7
g  9  10

#如果索引的标签不在index或columns范围则会报错,a标签在列中,loc的第一个参数为行索引。
df.loc['a']
Traceback (most recent call last):
……
KeyError: 'the label [a] is not in the [index]'

2、iloc:通过行号选取数据,即通过数据所在的自然行列数为选取数据。iloc方法也有两个参数,按顺序控制行列选取。

注意:行号和索引有所差异,进行筛选后的数据行号会根据新的DataFrame变化,而索引不会发生变化。

df
Out[196]: 
   a   b   c
d  0   1   2
e  3   4   5
f  6   7   8
g  9  10  11

#选取一行
df.iloc[0]
Out[197]: 
a    0
b    1
c    2
Name: d, dtype: int32

#选取多行
df.iloc[0:2]
Out[198]: 
   a  b  c
d  0  1  2
e  3  4  5

#选取一列或多列
df.iloc[:,2:3]
Out[199]: 
    c
d   2
e   5
f   8
g  11

3、ix:混合索引,同时通过标签和行号选取数据。ix方法也有两个参数,按顺序控制行列选取。

注意:ix的两个参数中,每个参数在索引时必须保持只使用标签或行号进行数据选取,否则会返回一部分控制结果。

df
Out[200]: 
   a   b   c
d  0   1   2
e  3   4   5
f  6   7   8
g  9  10  11

#选取一行
df.ix[1]
Out[201]: 
a    3
b    4
c    5
Name: e, dtype: int32

#错误的混合索引(想选取第一行和e行)
df.ix[[0,'e']]
Out[202]: 
     a    b    c
0  NaN  NaN  NaN
e  3.0  4.0  5.0

#选取区域(e行的前两列)
df.ix['e':,:2]
Out[203]: 
   a   b
e  3   4
f  6   7
g  9  10

4、at/iat:通过标签或行号获取某个数值的具体位置。

df
Out[204]: 
   a   b   c
d  0   1   2
e  3   4   5
f  6   7   8
g  9  10  11

#获取第2行,第3列位置的数据
df.iat[1,2]
Out[205]: 5

#获取f行,a列位置的数据
df.at['f','a']
Out[206]: 6

5、直接索引 df[]

df
Out[208]: 
   a   b   c
d  0   1   2
e  3   4   5
f  6   7   8
g  9  10  11

#选取行
df[0:3]
Out[209]: 
   a  b  c
d  0  1  2
e  3  4  5
f  6  7  8

#选取列
df['a']
Out[210]: 
d    0
e    3
f    6
g    9
Name: a, dtype: int32

#选取多列
df[['a','c']]
Out[211]: 
   a   c
d  0   2
e  3   5
f  6   8
g  9  11

#行号和区间索引只能用于行(预想选取C列的数据,
#但这里选取除了df的所有数据,区间索引只能用于行,
#因defg均>c,所以所有行均被选取出来)
df['c':]
Out[212]: 
   a   b   c
d  0   1   2
e  3   4   5
f  6   7   8
g  9  10  11
df['f':]
Out[213]: 
   a   b   c
f  6   7   8
g  9  10  11

#df.选取列
df.a
Out[214]: 
d    0
e    3
f    6
g    9
Name: a, dtype: int32
#不能使用df.选择行
df.f
Traceback (most recent call last):
  File "<ipython-input-215-6438703abe20>", line 1, in <module>
    df.f
  File "C:\ProgramData\Anaconda3\lib\site-packages\pandas\core\generic.py", line 2744, in __getattr__
    return object.__getattribute__(self, name)
AttributeError: 'DataFrame' object has no attribute 'f'

6、总结

1).loc,.iloc,.ix,只加第一个参数如.loc([1,2]),.iloc([2:3]),.ix[2]…则进行的是行选择
2).loc,.at,选列是只能是列名,不能是position
3).iloc,.iat,选列是只能是position,不能是列名
4)df[]只能进行行选择,或列选择,不能同时进行列选择,列选择只能是列名。行号和区间选择只能进行行选择。当index和columns标签值存在重复时,通过标签选择会优先返回行数据。df.只能进行列选择,不能进行行选择。