本文转载自公众号:数人之道
若需要广州二手房源结果数据文件及 Jupyter Notebook 源文件,请扫描下方二维码关注『数人之道』公众号,回复 广州二手房 获取。
12 月 20 日,央行授权全国银行间同业拆借中心公布,最新一期的贷款市场报价利率(LPR)为:1 年期 LPR 为 3.8%,较上一期下调 5 个 BP,5 年期以上 LPR 为 4.65%,同上期保持一致。
连续 19 个月“按兵不动”后,1 年期 LPR 报价年内首次下调!但是本次 5 年期 LPR 报价保持不变,因此对于选择浮动利率的按揭购房者而言,没有发生任何实质性的变化。
虽然本次 5 年期 LPR 报价保持不变是源于当前“房住不炒”的房地产调控基调,但买不起的房子还是买不起,一线城市核心地段的房价依然坚挺。那怎样可以获取自己所在城市目前的房价行情?Python 就能帮你!
以笔者目前身在的广州为例,由于一线城市住宅供地需求紧张,每年放出的新盘不多,因此二手房的价格才能更准确、真实地反映当地房价行情,那我们就可以用 Python 爬取互联网上的广州二手房信息来进行分析。这里我们选择链家网的数据进行爬取。
1. 环境准备
1.1 工具
由于我们需要一边运行代码,一边查看结果进行分析,因此我们选择 Anaconda + Jupyter Notebook 进行代码的编写及编译。
Anaconda 是一个包含丰富的科学包及其依赖项的 Python 开源发行版本,是做数据分析的首选,这样就无需再自己使用 pip 手动导入安装各种数据分析需要用到的依赖包。
Jupyter Notebook 是一种 Web 应用,能让用户将说明文本、数学方程、代码和可视化内容全部组合到一个易于共享的文档中。其已成为数据分析、机器学习的必备工具。因为它可以让数据分析师集中精力向用户解释整个分析过程,而不是梳理文档。
安装 Jupyter Notebook 最简单的方法是使用 Anaconda, Anaconda 中附带了 Jupyter Notebook, 能够在默认环境下使用。
1.2 模块
本次房源数据爬取及分析主要应用到的模块(库)包括:
- requests:对网页信息进行爬取
- time:设置每次爬取后的休息时间
- BeautifulSoup:对爬取的页面进行解析,并从中获取数据
- re:进行正则表达式操作
- pandas, numpy:对数据进行处理
- matplotlib:对数据进行可视化分析
- sklearn:对数据进行聚类分析
导入这些相关的模块(库):
import requests
import time
from bs4 import BeautifulSoup
import re
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
注意在 Jupyter Notebook 中需要先执行这部分模块导入才能执行后面的代码块,否则会报找不到依赖包的错误。
2. 构建爬虫,抓取信息
2.1 分析网页
开始爬取数据之前,先对网页的 URL 结构及需要爬取的数据在目标页面中的结构进行观察及分析。
2.1.1. URL 结构分析
链家网的二手房列表页面 URL 结构为:
http://gz.lianjia.com/ershoufang/pg1/
其中gz
表示城市,ershoufang
是频道名称,pg1
是页面码。我们只爬取广州的二手房频道,因此前面的部分不会变化,变化的是后面的页码数字,从 1-100 按 1 单调递增。
于是,我们将 URL 拆分为两部分:
- 固定部分:
http://gz.lianjia.com/ershoufang/pg
- 可变部分:1 - 100
对于可变部分,我们可以用 for 循环生成 1-100 的数字,与前面的固定部分拼接成需要爬取的 URL,再将每个 URL 爬取到的页面信息递归整合保存起来。
2.1.2. 页面结构分析
我们需要爬取的数据信息包括:房屋所处位置、总价、单价、户型、面积、朝向、装修情况、楼层、楼龄、楼型、关注人数等。借助浏览器开发工具的元素查看器,我们可以看到这些数据信息都在页面的 info div 中:
将所需数据所在的 div 做 DOM 拆解如下:
房屋的位置信息在 positionInfo 标签中,房屋的户型、面积、朝向、楼层、楼龄、楼型等属性信息都在 houseInfo 标签中,房屋的价格信息在 priceInfo 标签中,关注度信息在 followInfo 标签中。
2.2 构建爬虫
为了尽量伪装成正常的请求,我们需要在 http 请求中设置一个头部信息,否则很容易被封。头部信息网上有很多现成的可以用,也可以使用 httpwatch 等工具进行查看。
# 设置请求头部信息
headers = {
'Accept':'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding':'gzip, deflate, br',
'Accept-Language':'zh-CN,zh;q=0.8',
'Connection':'keep-alive',
'Referer':'http://www.baidu.com/link?url=_andhfsjjjKRgEWkj7i9cFmYYGsisrnm2A-TN3XZDQXxvGsM9k9ZZSnikW2Yds4s&wd=&eqid=c3435a7d00006bd600000003582bfd1f'
}
使用 for 循环生成 1-100 的数字,转化格式后与前面的 URL 固定部分拼接成需要爬取的 URL,再将爬取到的页面信息递归保存,并设置每两个页面之间的请求间隔时间为 0.5 秒。
# 设置二手房列表页URL固定部分
url = 'http://gz.lianjia.com/ershoufang/pg'# 循环爬取二手房列表页页面信息
for i in range(1, 100):if i == 1:i = str(i)entireURL = (url + i + '/')res = requests.get(url=entireURL, headers=headers)html = res.contentelse:i = str(i)entireURL = (url + i + '/')res = requests.get(url=entireURL, headers=headers)html2 = res.contenthtml = html + html2# 设置每页请求间隔时间time.sleep(0.5)
由于保存的页面信息数据太多,Jupyter Notebook 无法全部输出显示,可以先将需要获取的页面数设置少一点,例如 1-2 页,运行验证查看是否爬取成功:
页面信息已被成功爬取。
2.3 提取信息
页面爬取完成后是无法直接阅读和进行数据提取的,还需要进行页面解析。我们使用 BeautifulSoup 模块对页面进行解析,解析成我们在浏览器中查看源代码看到的样子。
# 对爬取的页面信息进行解析
htmlResolve = BeautifulSoup(html, 'html.parser')
解析完成后,根据页面结构的分析,提取所需要用到的数据。下面我们分别对房源的总价(priceInfo)、单价(unitPrice)、位置(positionInfo)、属性(houseInfo)、关注度(followInfo)五部分数据信息进行提取。
把页面 div 中 class=priceInfo 属性的部分提取出来,并使用 for 循环将其中每个房源的总价格数据存储在数组 tp 中。
# 提取房源总价格信息
price = htmlResolve.find_all("div", attrs={"class": "priceInfo"})
tp = []
for p in price:totalPrice = p.span.stringtp.append(totalPrice)
提取房源单价、位置、属性和关注度信息的方法与提取房源总价格的方法类似。
# 提取房源单价信息
unitPriceInfo = htmlResolve.find_all("div", attrs={"class": "unitPrice"})
upi = []
for up in unitPriceInfo:unitPrice = up.get_text()upi.append(unitPrice)# 提取房源位置信息
positionInfo = htmlResolve.find_all("div", attrs={"class": "positionInfo"})
pi = []
for ps in positionInfo:position = ps.get_text()pi.append(position)# 提取房源户型、面积、朝向等属性信息
houseInfo = htmlResolve.find_all("div", attrs={"class": "houseInfo"})
hi = []
for h in houseInfo:house = h.get_text()hi.append(house)# 提取房源关注度信息
followInfo = htmlResolve.find_all("div", attrs={"class": "followInfo"})
fi = []
for f in followInfo:follow = f.get_text()fi.append(follow)
抽取其中若干个数组,检查提取的信息情况:
看到房源总价数组的前20个数据,结果正常,提取成功。其他数组的检查方法类似。
3. 处理数据,构造特征
3.1 创建数据表
使用 pandas 模块将前面提取到的房源总价、单价、位置、属性和关注度等信息进行汇总,生成 DataFrame 数据表,用于后面进行数据分析。
# 创建数据表
house = pd.DataFrame({"totalprice": tp, "unitprice": upi, "positioninfo": pi, "houseinfo": hi, "followinfo": fi})
创建完成后,可查看数据表的内容,检查数据表的构造情况:
数据表成功构建。
3.2 构造特征
虽然我们已经将提取的信息构造为 DataFrame 结构的数据,但这个数据集表比较粗糙,比如房屋的户型、面积、朝向等属性信息都包含在房源属性信息(houseinfo)这一个字段中,不能直接使用。因此,我们还需要对数据表进行进一步的处理。
根据所需要的数据信息,我们对数据表中的字段进行新的特征构造,以便于下一步进行分析。
从 houseinfo 字段中,新构造出特征:户型、面积、朝向、装修情况、楼层、楼龄、楼型。
从 followinfo 字段中,新构造出特征:关注度。
这里特征构造的方法就是进行分列操作,把原来的大字段切分成最小颗粒度特征的新字段。
由于每个房源属性特征信息之间都是以竖线分隔开来的,因此我们只需要以竖线对属性信息进行分列即可。
# 对房源属性信息进行特征构造
houseinfo_split = pd.DataFrame((x.split('|') for x in house.houseinfo), index=house.index, columns=["huxing","mianji","chaoxiang","zhuangxiu","louceng","louling","louxing","spec"])
使用相同的方法对房源“关注度”特征字段进行构造,注意这里的特征信息分隔符是斜杠,而不是竖线。
# 对房源关注度信息进行特征构造
followinfo_split = pd.DataFrame((y.split('/') for y in house.followinfo), index=house.index, columns=["guanzhu", "fabu"])
构造完成后,查看特征字段构造的结果表进行检查:
将构造的特征字段拼接到原来的数据表后,这样就能在分析中将这些字段与其他数据信息配合使用。
# 将构造的特征字段拼接到原数据表后
house = pd.merge(house, houseinfo_split, right_index=True, left_index=True)
house = pd.merge(house, followinfo_split, right_index=True, left_index=True)
查看拼接后的数据表结果进行检查:
完成拼接后的数据表中包含了原有信息及新构造的特征值。
3.3 数据加工及清洗
在完成特征值构造拼接后的数据表中,部分字段的数据存在数字与中文混合的情况,且数据格式是文本,不能直接使用。
3.3.1. 数据加工
这里的数据加工工作是将数字从字符串中提取出来。可以采用两种方式:一种是跟分列一样的方法,将数字后的字符串作为分隔符进行分列提取;另一种是利用正则表达式的方式进行提取。
使用第一种方式,对以下字段进行数字提取:房源单价。
# 对数值型房源单价信息进行特征构造
unitprice_num_split = pd.DataFrame((z.split('元') for z in house.unitprice), index=house.index, columns=["danjia_num","danjia_danwei"])# 将构造的特征字段拼接到原数据表后
house = pd.merge(house, unitprice_num_split, right_index=True, left_index=True)
使用第二种方式,对以下字段进行数字提取:房源面积、关注度。
# 定义函数,提取字符串中的数字
def get_num(string):return (re.findall("\d+\.?\d*", string)[0])# 提取房源面积信息中的数字
house["mianji_num"] = house["mianji"].apply(get_num)# 提取房源关注度信息中的数字
house["guanzhu_num"] = house["guanzhu"].apply(get_num)
3.3.2. 数据清洗
提取出来的数据在使用前还需要进行清洗,通常的操作包括去除空格和格式转换。
对提取出来的房源单价、面积和关注度数字进行两端空格的去除操作。
# 去除提取的数字字段两端的空格
house["danjia_num"] = house["danjia_num"].map(str.strip)
house["mianji_num"] = house["mianji_num"].map(str.strip)
house["guanzhu_num"] = house["guanzhu_num"].map(str.strip)
将所有数值字段(包括房源总价)转换为 float 格式,以方便后续的分析。
# 将数值字段的格式转换为float
house["danjia_num"] = house["danjia_num"].str.replace(',', '').astype(float)
house["totalprice"] = house["totalprice"].astype(float)
house["mianji_num"] = house["mianji_num"].astype(float)
house["guanzhu_num"] = house["guanzhu_num"].astype(float)
查看进行数据加工及清洗后的数据表结果进行检查:
房源单价、面积、关注度信息中的数字均已提取出来,且房源总价、单价、面积、关注度的数值已正确转换为 float 格式。
4. 分析数据,可视化输出
将爬取到的数据进行信息提取、清洗、加工后,就可以对最终的数据结果进行分析了。
这里对大家都比较关心的房价和房屋面积、关注度的情况进行探索分析,并使用 Matplotlib 模块绘制 2D 图形,对数据进行可视化输出。
4.1 房源面积分布情况
4.1.1. 查看范围
查看爬取的广州在售二手房数据的房源面积范围。
# 查看房源面积数据的范围
house["mianji_num"].min(), house["mianji_num"].max()
得到房源面积范围:(18.3, 535.23).
4.1.2. 数据分组
根据房源的面积范围,对房源面积数据进行分组。这里以 50 为组距,将房源面积分为 11 组,并统计这 11 组中房源的数量。
# 对房源面积数据进行分组
bins = [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550]
group_mianji = ['小于50', '50-100', '100-150', '150-200', '200-250', '250-300', '300-350', '350-400', '400-450', '450-500', '500-550']
house['group_mianji'] = pd.cut(house['mianji_num'], bins, labels=group_mianji)# 按房源面积分组对房源数量进行汇总
group_mianji = house.groupby('group_mianji')['group_mianji'].agg(len)
分组统计的效果相当于以下 SQL 语句:
select group_mianji, count(*) from house group by group_mianji;
可以看到,房源大部分集中在 50-150 平米的范围,符合常理。
4.1.3. 绘制分布图
使用 Matplotlib 模块对按房源面积分组统计的房源数量绘制分布图,过程中需要用到 numpy 模块进行 y轴分组构建。
# 绘制房源面积分布图
plt.rc('font', family='STXihei', size=15)
ygroup = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
plt.barh([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], group_mianji, color='#205bc3', alpha=0.8, align='center', edgecolor='white')
plt.ylabel('面积分组(单位:平米)')
plt.xlabel('数量')
plt.title('房源面积分布图')
plt.legend(['数量'], loc='upper right')
plt.grid(color='#92a1a2', linestyle='--', linewidth=1, axis='y', alpha=0.4)
plt.yticks(ygroup, ('小于50', '50-100', '100-150', '150-200', '200-250', '250-300', '300-350', '350-400', '400-450', '450-500', '500-550'))# 查看绘制的分布图
plt.show()
查看房源面积分布图绘制结果:
处理后的数据进行可视化输出后,对分析就更加事半功倍了。
4.1.4. 数据分析
在抓取的广州在售二手房源数据中,数量最多的是面积范围在 50-100 之间的房源,其次为 100-150 的房源。随着面积增加数量减少;小于 50 的小面积房源也有一定的数量。
4.2 房源总价分布情况
4.2.1. 查看范围
查看爬取的广州在售二手房数据的房源总价格范围。
# 查看房源总价格数据的范围
house["totalprice"].min(), house["totalprice"].max()
得到房源总价格范围:(29.0, 3500.0).
4.2.2. 数据分组
根据房源的总价格范围,对房源总价数据进行分组。这里以 500 为组距,将房源面积分为 7 组,并统计这 7 组中房源的数量。
# 对房源总价格数据进行分组
bins = [0, 500, 1000, 1500, 2000, 2500, 3000, 3500]
group_totalprice = ['小于500', '500-100', '1000-1500', '1500-2000', '2000-2500', '2500-3000', '3000-3500']
house['group_totalprice'] = pd.cut(house['totalprice'], bins, labels=group_totalprice)# 按房源总价格分组对房源数量进行汇总
group_totalprice = house.groupby('group_totalprice')['group_totalprice'].agg(len)
可以看到,房源总价格大部分集中在小于 500 万的范围内。
4.2.3. 绘制分布图
使用 Matplotlib 模块对按房源总价格分组统计的房源数量绘制分布图。
# 绘制房源总价分布图
plt.rc('font', family='STXihei', size=15)
ygroup = np.array([1, 2, 3, 4, 5, 6, 7])
plt.barh([1, 2, 3, 4, 5, 6, 7], group_totalprice, color='#205bc3', alpha=0.8, align='center', edgecolor='white')
plt.ylabel('总价分组(单位:万元)')
plt.xlabel('数量')
plt.title('房源总价分布图')
plt.legend(['数量'], loc='upper right')
plt.grid(color='#92a1a2', linestyle='--', linewidth=1, axis='y', alpha=0.4)
plt.yticks(ygroup, ('小于500', '500-100', '1000-1500', '1500-2000', '2000-2500', '2500-3000', '3000-3500'))# 查看绘制的分布图
plt.show()
查看房源总价分布图绘制结果:
4.2.4. 数据分析
在抓取的广州在售二手房源数据中,数量最多的是总价格小于 500 万的房源,而且是远远多于总价 500 万以上的房源,看来广州是最友好的一线城市没错了,起码在这里买房比其他三个一线城市要容易不少。
这里看到最高的房价在 3500 万,当然,这并不是广州房价的真实上限水平。这是由于链家网只能爬取 100 页的数据,不在页面展示的记录我们无法获取,因此爬取到的并不是完整的全部二手房源数据;另外,顶级豪宅房源也基本不会在公开平台上挂牌。
4.3 房源关注度分布情况
房源关注度分布分析的过程跟房源面积、房源总价的分布分析类似,这里不再展开过程,直接看房源关注度分布图结果:
这里看到由于四套关注度在 1000 以上,甚至有到 5000 关注度的房源,导致分布图十分不平衡,绝大部分房源关注度都在 1000 以下。
需要注意的是,这里的关注度数据无法准确表示房源的热门程度。在实际业务中,热门房源十分抢手,可能刚上线就成交了,由于出售速度快而导致关注度较少。我们暂时忽略掉这些复杂的情况,因此关注度数据仅供参考。
4.4 房源聚类分析
最后,我们使用机器学习库 sklearn 对爬取的广州在售二手房源数据,按总价格、面积和关注度进行聚类分析。将在售的二手房源按总价格、面积和关注度的相似性分在不同的类别中。
# 使用房源总价格、面积和关注度三个字段进行聚类
house_type = np.array(house[['totalprice', 'mianji_num', 'guanzhu_num']])# 设置质心数量参数值为3
cls_house = KMeans(n_clusters=3)# 计算聚类结果
cls_house = cls_house.fit(house_type)
查看每个类别的中心点坐标,并在原数据表中标注所属类别。
# 查看分类结果的中心坐标
cls_house.cluster_centers_# 在原数据表中标注房子所属类别
house['label'] = cls_house.labels_
查看分类结果的中心点坐标:
根据三个类别在总价格、面积和关注度三个点的中心坐标,将在售二手房源分为三个类别:
这个分类跟我们的日常经验有点背道而驰,再看看中心坐标,第三个偏差十分大,我们将数据表导出到 Excel 中查看这个数据。
# 写入Excel
house.to_excel('ershouHousePrice.xls')
完成后,在项目所在目录,可以看到生成了文件名为 ershouHousePrice 的 Excel 文件。打开文件,查看关注度在 1000 以上的数据记录。
都是华景新城和农讲所的二手房源,这个有存在刷数的嫌疑,有兴趣的朋友可以将这部分数据剔除再进行分析,看看结果有什么差异。
但从上述的分析,并结合营销和市场地域特色的角度来看,在广州二手房市场中,总价且面积在中等水平的房源,在地段、位置、交通、医疗、教育、商业等方面的确大部分都会比总价且面积低的房源更优质。一线城市在这些资源更丰富的地区,其虹吸能力比非一线城市是强很多的。另外,广州二手房市场的中等水平比其他一线城市要低。综上所述,总价且面积在中等水平的广州二手房源能吸引更多的用户关注。
5. 结语
正如上文中所说,由于链家网只能爬取 100 页的数据,不在页面展示的记录我们无法爬取,因此这里爬取到的并不是完整的全部二手房源数据,不能作为商用参考。
这篇文章的编写目的,是希望能抛砖引玉,让大家对网站爬虫和数据分析有所了解,并能在此基础上,进行更深层次和更发散的探索,举一反三。例如,既然能获取二手房的信息,就能获取一手房的信息,你要做的就是观察一手房的 URL 及页面结构并进行修改;从不同要素和角度进行房源分析;等等。
本篇所有代码可以到我的 github 或 gitee 中获取:
https://github.com/hugowong88/ershouHouseDA_gz
https://gitee.com/hugowong88/ershou-house-da_gz
★★★★★
实践数据技术,探索数据思维,关注数据行业前沿资讯,数据人之道。扫描下方二维码关注『数人之道』公众号,发掘更多精彩的【数据分析】相关文章。