分类目录数据分析

全国烂尾楼停贷通知数据

停贷通知楼盘数据的可视化结果。可惜的是github的数据缺乏时间轴(通知停贷的时间),从而缺乏了从时间维度来看这个数据的方向。每日8点会自动刷新当前最新数据。

可以有以下一些结论:

1、事件波及面较广,目前已涉及全国26个省市,113个城市。

2、从开发商来看:恒大、融创、绿地是三大巨头。恒大和融创印象中在2015年左右特别火,特别是恒大的各种跨界操作,令人眼花缭乱。而万科、保利(或者说是股市常青树?)等一众老牌企业尚未有大规模停贷的状况发生,如果连他们都沦陷了,有点难以想象?

3、从省市看,河南省遥遥领先,湖南、湖北第二梯队,河北、陕西、广西四川等省份不分伯仲。看起来中部省份是重灾区,整体来看省市也不分发达与不发达,通通中招。而舆论最为火爆的郑州市,当前41个停贷盘中,河南本地房企康桥地产独占六席。

4、从城市看,郑州41席、西安21席、武汉16席,是停贷最多的城市。其中郑州停贷所涉及的开发商五花八门,其中竟有2席龙湖的身影(龙湖在重庆的商业地产可以算得上是非常成功的,各大商圈总有天街的身影)。而恒大的停贷楼盘则是在全国各省市全面开花,哪哪儿都有的感觉。

使用Python+R做用户购买路径分析

1、需求:

在用户行为分析中重要的一环是对用户行为路径的分析,如先访问什么再访问什、现购买什么再购买什么。找到用户的访问或购买路径,有助于我们对商业流程的理解,也可以帮助我们改善和优化企业产品。

2、题设:

假设我们是一家家电电商网站,拥有所有客户的购买每种家电的详细记录(包括客户id,购买产品,购买日期)。

客户购买记录,数据全是瞎编的

我们希望通过这个数据分析出社会用户在购买家电先后顺序的行为路径。
这个结果的实际用途,例如客户在装修新房后购置抽油烟机,那么我们很可能希望给他推荐热水器或者空调等。我们总不至于像某东网站一样,客户购买了一个冰箱后还给推荐另一个冰箱吧?

3、结果示例:

希望能够得到类似下图的结果:

结果示例

这是百度echarts的Sankey(桑基图)图示例,我们将通过Python+R实现类似的效果。

4、技术路线:

如标题所述,使用Python进行数据处理,形成R绘图直接可用的数据结构;再使用R绘制Sankey图(之所以不使用Python直接绘图,是因为Python在绘制桑基图方面还不够完善——好吧,是我不会!)。
先来看R绘制桑基图networkD3官方给出的示例代码:

#R code
# Load energy projection data
# Load energy projection data
URL <- paste0(
        "https://cdn.rawgit.com/christophergandrud/networkD3/",
        "master/JSONdata/energy.json")
Energy <- jsonlite::fromJSON(URL)
# Plot
sankeyNetwork(Links = Energy$links, Nodes = Energy$nodes, Source = "source",
             Target = "target", Value = "value", NodeID = "name",
             units = "TWh", fontSize = 12, nodeWidth = 30)

访问上述url指向的地址,可以看到数据文件格式,这是一个json文件,结构如下:

#R networkD3绘制桑基图所需的数据格式
{"nodes":[
{"name":"Agricultural 'waste'"},
{"name":"Bio-conversion"},
……
],
"links":[
{"source":0,"target":1,"value":124.729},
{"source":1,"target":2,"value":0.597},
{"source":1,"target":3,"value":26.862}],
……
]}

数据主要包含两个部分:nodes的每条记录表示桑基图中每个节点位置的名称;links的每条记录表示从某个节点到另一个节点出现的次数。source和target后面所跟的数字顺序和nodes后面的数据顺序一致。如0表示节点名字是Agricultural ‘waste’,1表示节点Bio-conversion,{“source”:0,”target”:1,”value”:124.729}表示从Agricultural ‘waste’到Bio-conversion有124.729大的量。
因此,剩下的工作就是如何将上述分笔交易数据转化成上述符合要求的格式。

5、数据整理:

大致分析一下,我们首先需要找到购买了两笔及以上的客户。然后将这些客户中,每个客户成交商品的顺序进行处理。
如一个客户先后购买了:冰箱——洗衣机——电视机,整理出的关系则是:

source    target    value
冰箱       洗衣机     1
洗衣机     电视机     1

(1)首先,需要将购买有2次记录及以上的所有数据筛选出来备用。

#Python code
import pandas as pd
#读取所有数据
base_data=pd.read_excel('C:/Users/Administrator/Desktop/家电购买记录.xlsx')
#找出购买了两次及以上的用户(通过user_id统计次数再作筛选)
buy_times=base_data.groupby('user_id')['product'].count()
re_buy_userid=buy_times[buy_times>1].reset_index()
#筛选所有购买两次及以上用户的购买记录
rebuy_data=base_data[base_data['user_id'].isin(re_buy_userid['user_id'])]

(2)第二步,分析购买先后次序。思路是对每个客户的购买记录进行分析,形成第5部分开头所示的明细列表,然后再对这个明细列表进行汇总,计算出每种对应关系的汇总结果。

#Python code
#分析流向
source_name=[]
target_name=[]
num=[]
#遍历每个购买两次及以上的用户id
for user_id in re_buy_userid['user_id']:
    #取出每个用户购买记录明细,按时间升序排列,重建索引,以便后面取结果时使用
    each_buy=rebuy_data[rebuy_data['user_id']==user_id].sort_values(by='buytime',ascending=True).reset_index()
    #按顺序对上述明细数据进行遍历,分别取当前记录的product和下一条记录的product,即可构成购买先后关系
    for i in range(len(each_buy)-1):
        each_source=each_buy['product'][i]
        each_target=each_buy['product'][i+1]
        source_name.append(each_source)
        target_name.append(each_target)
        num.append(1)
#对结果进行整理,形成一个表格
st_result=pd.DataFrame([source_name,target_name,num],index=['source_name','target_name','num']).T
#获取统计结果
group_result=st_result.groupby(['source_name','target_name']).count().reset_index().sort_values(by='num',ascending=False)

这里我们得到的结果如下:

客户购买路径数据统计结果

实际上,到这里,基本上已经是我们R所需要的数据了,只需要将其转换成json格式即可。
同时,我们可以得出结论:我们的客户通常情况下买了电视机会接着买空调,买了电饭锅通常会买电磁炉,买了空调通常会买微波炉(千万别拿这个当决策,前面说了数据都是我瞎编的)…
(3)第三步,将上述数据转换成json格式,并存储成文件。

#Python code
#转化json所需格式
#定义names,以确定nodes的name顺序
names=group_result['source_name'].append(group_result['target_name']).drop_duplicates().reset_index(drop=True)
#将确定好的name顺序固定,生成一个对应表,用以对group_result里面的中文补充相应的name顺序代号
names_order=names.reset_index().reset_index().rename(columns={'level_0':'source','index':'target',0:'product'})
#添加source_name对应的name代号
mid_data=pd.merge(group_result,names_order[['source','product']],left_on='source_name',right_on='product',how='left')
#添加target_name对应的name代号
path_data=pd.merge(mid_data,names_order[['target','product']],left_on='target_name',right_on='product',how='left')

#生成nodes
name_list=[]
for i in names:
    each_name={'name':i}
    name_list.append(each_name)
#生成links
source_target=[]
for i in range(len(path_data)):
    each_links={'source':path_data['source'][i],'target':path_data['target'][i],'value':path_data['num'][i]}
    source_target.append(each_links)

#生成json格式
rebuy_dict={"nodes":name_list,"links":source_target}
rebuy_json=str(rebuy_dict).replace("'",'"')

#存储为json格式
f=open("d:/rebuy.json","w",encoding="utf-8")
f.write(str(rebuy_json))
f.close()

(4)第四步,使用R绘图

#R code
library(networkD3)
rebuy <- jsonlite::fromJSON('D:/rebuy.json')
sankeyNetwork(Links = rebuy$links, Nodes = rebuy$nodes, Source = "source",
              Target = "target", Value = "value", NodeID = "name",
              units = "TWh", fontSize = 30, nodeWidth = 30,
              fontFamily = "微软雅黑")

来看结果吧:

客户购买路径图

不能更丑了,已经。

Python获取中国日历

在做项目排班的时候经常会遇到一个问题,就是如何在Excel中标记中国的节假日?
经过在网上找各种API,发现不是要钱就是服务器404,再不然就是没有标记完全(比如只标记了节假日,但调休加班没有)。所以,最后还是背靠度娘,通过度娘提供的数据来自己做一个带中国节假日日历列表。

度娘的接口

https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=2020%E5%B9%B4&resource_id=6018&format=json

可以看到query对应的值就是你要查询的年月对应的日历。这里有个好处就是,返回的数据会包含当年的所有节假日和调休加班日,如下:

image.png

所以,可以在holiday的list中看到当年所有的放假和调休加班(放假对应的status是1,而调休加班则是2),再结合日历本身的周六周日,就可以对当年所有的日期进行标记,最终输出某一天是法定放假还是法定上班了。

以下是,所有代码

以下是对小白的操作

  • 用法就是,复制这些文本
  • 在本地存储为任意一个text文档
  • 将此文档的后缀名由”.txt”修改为”.py”
  • 然后使用python运行此文件
  • 要修改年份,只需要调整最后的year=2020这个代码就可以了

# -*- coding: utf-8 -*-
"""
Created on Mon Jun  8 17:18:15 2020

@author: 西湖味精三月鲜
"""

# 百度获取日历
# https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=2020%E5%B9%B4&resource_id=6018&format=json


import pandas as pd
import requests
import json
import numpy as np

# 生成某一年的日历
def gen_calendar(year=2020):
    # 生成日历列表
    start_date=str(year)+'0101'
    end_date=str(year)+'1231'
    df=pd.DataFrame()
    dt=pd.date_range(start_date, end_date,freq='1D')
    df['date']=dt
    # 计算周几
    df['dayofweek']=df['date'].dt.dayofweek+1
    # 获取法定节假日
    up1='https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query='
    up2='%E5%B9%B4&resource_id=6018&format=json'
    url="".join([up1,str(year),up2])
    r=requests.get(url)
    r_json=json.loads(r.text)
    # 获取放假信息
    h_data=r_json['data'][0]['holiday']
    h_df=pd.DataFrame()
    for i in range(len(h_data)):
        each_d=h_data[i]['list']
        each_df=pd.DataFrame(each_d)
        h_df=h_df.append(each_df)
    # 处理一下数据,去重等等
    h_df.drop_duplicates(inplace=True)
    h_df.reset_index(drop=True,inplace=True)
    h_df['date']=pd.to_datetime(h_df['date'])
    
    # 合并数据
    df2=pd.merge(df,h_df,how='left',on='date')
    df2.fillna(0,inplace=True)
    df2['status']=df2['status'].astype('int')
    # 返回是否假日
    judge=np.where(df2['dayofweek']<6,0,1)+df2['status']
    judge=np.where((judge==2) | (judge==1),'Y','N')
    df2['isholiday']=judge
    
    return df2



in __name__=="__main__":
    year=2020
    df=gen_calendar(year)
    df2.to_csv('{}年节日历表.csv'.format(year),index=False)

以下是懒人入口,2011-2020年的都在这里

  • 只有2011年及之后年份的数据
  • 只有国务院公布了放假安排后,才会有当年的数据,比如现在就没有2021年的
  • dayofweek是星期几
  • status是放假安排的判断,1表示法定放假,2表示法定上班,0没有特殊意义
  • isholiday就是最后的结果了,Y表示是法定节假日,N表示是要上班的日子
  • 下载链接:
    链接:https://pan.baidu.com/s/1M1ehYSAVoLt-WvHlf4EM4Q 提取码:s7wm

Python网站手机号码解密的一种方法

黄页88网站反爬措施对手机号进行了一些加密处理。这里介绍一下怎么解密手机号码的方法。

首先,我们看一下网站显示的手机号是这样的:

显示的手机号

使用ctrl c复制粘贴后变成了这样:

复制后的手机号码

而实际上,用Python爬虫获取到的内容是这样的:

爬虫获取到的数据

经过分析,发现这些数据其实有以下特点:

  1. 每次刷新网页,编码会变
  2. 编码之间其实有关系(实际上就是0-9转换成了16进制)

所以,我们的思路就是:

  1. 确定手机号码开头1对应的是第一个编码
  2. 然后根据第一个编码获取0-9对应的16进制编码
  3. 在通过编码检索出这串编码对应的电话号码

步骤:

  1. 首先爬取到电话号码的代码

code_list='&#x882b7;&#x882bb;&#x882bc;&#x882b9;&#x882b7;&#x882b8;&#x882bc;&#x882bd;&#x882bd;&#x882bd;&#x882b7'

  1. 要转换为16进制,需要把&#替换为0

# 替换&#为0,用于后面直接转换为10进制数
code_list=code_list.replace("&#","0")

In [4]: code_list
Out[4]: '0x882b7;0x882bb;0x882bc;0x882b9;0x882b7;0x882b8;0x882bc;0x882bd;0x882bd;0x882bd;0x882b7'
  1. 上面是个字符串,要对每个编码进行处理,还是需要转换成列表,刚好对应11个号码

# 转换成列表
code_list=code_list.split(';')

In [6]: code_list
Out[6]: 
['0x882b7',
 '0x882bb',
 '0x882bc',
 '0x882b9',
 '0x882b7',
 '0x882b8',
 '0x882bc',
 '0x882bd',
 '0x882bd',
 '0x882bd',
 '0x882b7']
  1. 因为手机号码的第一位是1,所以确定第一位16进制对应的10进制值,就可以知道0-9对应的10进制值是多少

# 确定第一个号码1对应的10进制值
c1=int(code_list[0],base=16)

In [8]: c1
Out[8]: 557751
  1. 知道了1对应的值,就创建0-9对应的10进制值

# 创建0-9对应的10进制值
int_list=range(c1-1,c1+9)

In [10]: int_list
Out[10]: range(557750, 557760)
  1. 然后将上面的数据转换成16进制

# 将其转换为hex
hex_list=[str(hex(i)) for i in int_list]

In [12]: hex_list
Out[12]: 
['0x882b6',
 '0x882b7',
 '0x882b8',
 '0x882b9',
 '0x882ba',
 '0x882bb',
 '0x882bc',
 '0x882bd',
 '0x882be',
 '0x882bf']
  1. 创建0-9的列表,并转换成字符串,方便后面组装成电话号码(这里0-9的顺序和hex_list里面的顺序是一一对应的)

# 创建0-9的数字对应列表
str_list=[str(i) for i in range(0,10)]

In [14]: str_list
Out[14]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
  1. 将hex和0-9的字符串组装成字典,方便后面查询

# 组装成字典方便对应
code_dict=dict(zip(hex_list,str_list))

In [16]: code_dict
Out[16]: 
{'0x882b6': '0',
 '0x882b7': '1',
 '0x882b8': '2',
 '0x882b9': '3',
 '0x882ba': '4',
 '0x882bb': '5',
 '0x882bc': '6',
 '0x882bd': '7',
 '0x882be': '8',
 '0x882bf': '9'}
  1. 最后一步就是通过字典查询并组装成最终的电话号码了(为了避免各种纠纷:这里的电话号码后面几位进行了修改,不是真实的用户电话)

# 见证奇迹的时刻,把电话号码翻译过来
phone="".join([code_dict[p] for p in code_list])

In [18]: phone
Out[18]: '15631267771'

所有代码如下:

code_list='&#x882b7;&#x882bb;&#x882bc;&#x882b9;&#x882b7;&#x882b8;&#x882bc;&#x882bd;&#x882bd;&#x882bd;&#x882b7'
# 替换&#为0,用于后面直接转换为10进制数
code_list=code_list.replace("&#","0")
# 转换成列表
code_list=code_list.split(';')
# 确定第一个号码1对应的10进制值
c1=int(code_list[0],base=16)

# 创建0-9对应的10进制值
int_list=range(c1-1,c1+9)

# 将其转换为hex
hex_list=[str(hex(i)) for i in int_list]

# 创建0-9的数字对应列表
str_list=[str(i) for i in range(0,10)]

# 组装成字典方便对应
code_dict=dict(zip(hex_list,str_list))

# 见证奇迹的时刻,把电话号码翻译过来
phone="".join([code_dict[p] for p in code_list])

上面这个过程最主要的一点是,我们假设编码的第一位是1,其他号码都是通过这个推算来的。但无法避免有些手机号码填错(第一位不是1),或者填的是固定电话的情况,所以无法适用于这些情况。

地图坐标系转换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.只能进行列选择,不能进行行选择。

Python如何一行写完if elif else列表推导式

Python中的if elif else结构通常如下:

if cond1:
    a=1
elif cond2:
    a=2
else:
    a=3

在特殊情况下,我们可能希望把这个条件语句写成一行,如(列表推导式中)。
结论就是,将上述结构改为:

非列表:    结果甲 if 条件甲 else 结果乙 if 条件乙 else 结果丙
列表推导式:    [结果甲 if 条件甲 else 结果乙 if 条件乙 else 结果丙 for xx in list]
列表推导式筛选包含某个值的数据:    [结果甲 for xx in list if 条件甲]

如下例:
希望对一个字符串数据进行判断,获取一个该二手房信息属于哪种类型的房产:判断依据就是,如果字符串中含有别墅则是别墅,如果含有车位即为车位,其他为高层。

a='大鼎世纪滨江 | 车位 | 39.09平米 | 东 | 无电梯'
b='恒大城二期 | 2室2厅 | 71.49平米 | 东南 | 精装 | 有电梯'
c='协信彩云湖1号 | 叠拼别墅 | 5室2厅 | 280.49平米 | 南 | 简装 | 无电梯'
d=['大鼎世纪滨江 | 车位 | 39.09平米 | 东 | 无电梯',
      '恒大城二期 | 2室2厅 | 71.49平米 | 东南 | 精装 | 有电梯',
      '协信彩云湖1号 | 叠拼别墅 | 5室2厅 | 280.49平米 | 南 | 简装 | 无电梯']

#实现代码
tpye="别墅" if "别墅" in a else "车位" if "车位" in a else "高层"

#结果
In [177]: "别墅" if "别墅" in a else "车位" if "车位" in a else "高层"
Out[177]: '车位'

In [178]: "别墅" if "别墅" in b else "车位" if "车位" in b else "高层"
Out[178]: '高层'

In [179]: "别墅" if "别墅" in c else "车位" if "车位" in c else "高层"
Out[179]: '别墅'

#对于列表推导式
In [180]: ["别墅" if "别墅" in x else "车位" if "车位" in x else "高层" for x in d]
Out[180]: ['车位', '高层', '别墅']

#对于列表推导式,筛选只包含某个词的的数据
In [181]: ["别墅" for x in d if "别墅" in x]
Out[181]: ['别墅']

PowerBI 连接HIVE数据库

  1. 由于目前PowerBI没有直接链接HIVE数据库的工具,因此首先需要下载两个驱动(第一个驱动貌似用不上,是适用于微软的hive数据库的),下载后分别安装:
    1. Microsoft® Hive ODBC Driver,下载地址:点击下载


    2. ClouderaHiveODBC6,下载地址:点击下载
  2. 在电脑中搜索ODBC,打开64-bit ODBC Administrator


  3. 选择”系统DSN”选项卡,点击”添加”,选择cloudera ODBC…,点击完成。


  4. 具体配置如下:


  5. 完成点击test,弹出对话框succeed说明配置成功了。
  6. 这时候在PowerBI中,点击获取数据→其他→ODBC,数据源选择刚才配置好的hive数据库


  7. 在弹出对话框中,数据库选项卡中输入用户名和密码,点击连接即可。