目录
1. 前言
大学毕业后初入职场,面临新的环境和任务,想必是每个人最为忐忑和激动的阶段。对于大部分的年轻人来说,毕业后回到熟悉的家乡,又或是去大城市寻找更广阔的机会,将是一个举足轻重的决定。而无论怎样抉择,最后我们都会在一个城市落脚,开始新阶段的生活。除了少数非常幸运的人,此时一个非常重要的问题便是,如何在这个城市寻觅一处落脚之地展开这下一阶段的生活,也就是——租房。
房源信息往往是众多且眼花缭乱的,在对城市并不熟悉的前提下,租房想必是一件非常头疼的问题。本项目选取上海作为城市样本,使用Python爬虫功能爬取海量房源数据,对数据进行清洗和分析,然后对房源数据进行聚类分析,最后利用高德地图JS API将房源在地图上可视化,希望能够帮助毕业生轻松定位合适的房源。
2. 数据源
- 房源数据:房源数据来自链家网,所爬取的数据将包括房源小区名、所属辖区与社区、房间面积、朝向、房间格局、租金以及房源对应的网页地址。
- 房源经纬度及周边POI数据:通过高德地图Web服务API接口获取(需要获取相应key)
- 路线规划数据:通过高德地图JS API进行交通路线规划
3. 方法与工具
3.1 房源数据爬取与清洗
打开链家网的网页,能够看到通过不同筛选条件选出的房源信息,为了筛选掉一部分房源,我们首先可以选择目标租金区间。接下来,我们需要获取该筛选条件下每套房源的相关信息:小区名、所属辖区与社区、面积、朝向、格局和租金,并将数据保存为csv文件,方便后续操作。
这里需要用到Python的BeautifulSoup、Requests等库进行数据爬取以及清洗,具体代码如下所示:
from bs4 import BeautifulSoup
import requests
import csv
import time
import math
# 网址
url = "https://sh.lianjia.com/zufang/pg{page}brp3000erp6000/#contentList"
# 初始化页码
page = 0
# 打开rent.csv文件,若无则会新建一个,设置写入模式
csv_file = open("rent_shanghai.csv", "w", encoding="utf-8", newline="")
#创建writer对象
csv_writer = csv.writer(csv_file, delimiter=',')
res_0 = requests.get(url)
res_0.encoding = "utf-8"
# 获取房屋结果个数
house_count = BeautifulSoup(res_0.text, "html.parser").find(class_="content__article").find("span").text
#计算所需爬取的页面数
pages = math.ceil(int(house_count) / 30)
print("已为您找到{}套上海租房, 共需爬取{}个页面\n".format(house_count, pages))
# 循环所有页面
while True:
page += 1
time.sleep(1)
#抓取目标页面
res = requests.get(url.format(page=page))
#设置编码格式
res.encoding = "utf-8"
#创建一个BeautifulSoup对象,获取页面正文
html = BeautifulSoup(res.text, "html.parser")
print("{}. fetch: ".format(page), url.format(page=page))
#获取当前页面的房子信息
house_list = html.find(class_="content__list").find_all(class_="content__list--item")
#房子信息获取
for house in house_list:
# 获取房子具体信息
house_info = house.find(class_="content__list--item--des").text.split("/")
# 获取房子链接
house_title = house_info[0].strip().split("-")[-1]
house_location = house_info[0].strip()
house_area = house_info[1].strip()
# 获取房子租金
house_rent = house.find(class_="content__list--item-price").find("em").text
house_url = "https://sh.lianjia.com" + house.find(class_="content__list--item--title").find(class_="twoline")["href"].strip()
house_orientation = house_info[2].strip()
house_pattern = house_info[3].strip()
# 写入一行数据
csv_writer.writerow([house_title, house_location, house_rent, house_area, house_orientation, house_pattern, house_url])
if page == pages:
break
csv_file.close()
3.2 房源社区周边POI分析
上述代码获取到了房源相关数据并将其保存为了csv文件。接下来,我们要通过高德地图API获取房源所在社区的地理位置以及周边POI,用于之后在地图上的可视化,和房源聚类分析。
首先将csv文件导入,写入DataFrame。代码如下:
# 导入之后所需的所有库
import numpy as np # library to handle data in a vectorized manner
import pandas as pd # library for data analsysis
import json # library to handle JSON files
from geopy.geocoders import Nominatim # convert an address into latitude and longitude values
import requests # library to handle requests
from pandas.io.json import json_normalize # tranform JSON file into a pandas dataframe
# Matplotlib and associated plotting modules
import matplotlib.cm as cm
import matplotlib.colors as colors
# import k-means from clustering stage
from sklearn.cluster import KMeans
#!conda install -c conda-forge folium=0.5.0 --yes # uncomment this line if you haven't completed the Foursquare API lab
import folium # map rendering library
# import Dataset
house_df = pd.read_csv('rent_shanghai_3000_6000.csv', sep=',', encoding='utf-8')
house_df.columns = ['name', 'location', 'rent', 'area', 'orientation', 'pattern', 'url']
house_df.head()
数据如下图所示:
接下来需要对数据进行一些清洗和处理:
# 新增房源所在社区列
house_df['neighborhood'] = house_df['location'].map(lambda x: x.split("-")[0]+x.split("-")[1])
house_df = house_df[['name', 'neighborhood', 'location', 'rent', 'area', 'orientation', 'pattern', 'url']]
house_df.head()
# 获取独特的社区
neighborhoods = list(house_df['neighborhood'].unique())
print('共有{}个不同的社区'.format(len(neighborhoods)))
neighborhoods
由结果可知所选房源所在的社区共有167个。 接下来用高德地图API接口获取社区的地理位置:
# 构造新的Dataframe来储存社区的经纬度的数据
ll = pd.DataFrame(columns = ['Neighborhood', 'Lnglat', 'District', 'Postcode','Level'])
# 获取社区的地理位置,所属辖区,邮编及类型
# 注意:可能会有社区找不到数据,这时需要用不同的关键词搜索,我是找出NAN值,后期手动补入的
for index, neigh in enumerate(neighborhoods):
url = 'https://restapi.amap.com/v3/geocode/geo?key=da07e4fa0e521bd055b621b83ca8ca7f&address={}&city=上海'.format(neigh)
res = requests.get(url)
data = json.loads(res.text)['geocodes']
ll.loc[index, 'neighborhood'] = neigh
print(index, neigh)
try:
lnglat = data[0]['location']
district = data[0]['district']
postcode = data[0]['adcode']
level = data[0]['level']
ll.loc[index, 'lnglat'] = lnglat
ll.loc[index, 'district'] = district
ll.loc[index, 'postcode'] = postcode
ll.loc[index, 'level'] = level
except:
ll.loc[index, 'lnglat'] = np.nan
ll.loc[index, 'district'] = np.nan
ll.loc[index, 'postcode'] = np.nan
ll.loc[index, 'level'] = np.nan
# 将经纬度列分成经度、纬度两列,并转换数据类型
ll['Longitude'] = ll['lnglat'].map(lambda x: x.split(",")[0])
ll['Latitude'] = ll['lnglat'].map(lambda x: x.split(",")[1])
ll.drop(columns='lnglat', axis=1, inplace=True)
ll['Longitude'] = ll['Longitude'].astype('float')
ll['Latitude'] = ll['Latitude'].astype('float')
# 修改列名
ll.columns = ['Neighborhood', 'District', 'Postcode','Level', 'Longitude', 'Latitude']
ll.head()
数据如下图所示:
然后用社区的地理位置,配合高德地图POI搜索,获取社区周围500米内的的POI的个数。此处的POI类型选取是根据我自己对于社区周边POI的喜好选择的,主要选了对我而言比较重要的一些类型和场所:生活服务、餐饮服务、购物服务、体育休闲、医疗服务、风景类。这个可以根据自己的需求灵活选择。关于高德地图的POI分类及相应搜索可以参考:POI分类编码
# 社区周边POI搜索
## 选取高德地图POI搜索的类别和关键词用于查询
data = np.asanyarray([
['医疗保健类','综合医院|专科医院|诊所','','090100|090200|090300',500,0.10,'https://restapi.amap.com/v3/place/around?key=da07e4fa0e521bd055b621b83ca8ca7f&location={},{}&keywords=&types=090100|090200|090300&radius=500&offset=50&page=1&extensions=all'],
['餐饮服务类','中餐厅|外国餐厅|甜品店|咖啡厅','','050000',500, 0.25,'https://restapi.amap.com/v3/place/around?key=da07e4fa0e521bd055b621b83ca8ca7f&location={},{}&keywords=&types=050000&radius=500&offset=50&page=1&extensions=all'],
['生活服务类','物流速递|电讯营业厅','','070500|070600',500,0.15,''],
['购物服务类','商场|超级市场|综合市场|特色商业街','','060100|060400|060700|061000',500,0.20,''],
['体育休闲类_健身','健身房','健身','080100',500,0.15,''],
['体育休闲类_舞蹈','舞蹈室','舞室','140000',500,0.10,''],
['风景名胜类','公园','公园','110101',500, 0.05, '']])
neigh_criteria = pd.DataFrame(columns = ['category','mid_category','keywords','types','radius','weight','url'], data=data)
要搜索的POI类型和其关键词如下图所示,types列是高德地图POI搜索的编码,weight列是我自己根据对各类场所需求分配的权重:
接下来根据以上的信息对各类POI通过高德地图API进行搜索就可以了:
# 对各类别POI经行搜索,把社区周围500米内的POI个数存入category_df
# 注意:可能由于API连接问题没能获取到数据, 我是后期手动搜索,将NAN值替换了。
def count_category(neighborhoods, lng, lat):
category_df = pd.DataFrame(columns=['Neighborhood','医疗','餐饮','生活','购物', '体育_健身','休闲_舞蹈','风景'])
for row in range(len(neighborhoods)):
neigh_name = neighborhoods[row]
neigh_lng = lng[row]
neigh_lat = lat[row]
category_df.loc[row, 'Neighborhood'] = neigh_name
for i, category, kwd, typ, radius in zip(range(neigh_criteria.shape[0]), neigh_criteria['category'], neigh_criteria['keywords'], neigh_criteria['types'],neigh_criteria['radius']):
try:
url = 'https://restapi.amap.com/v3/place/around?key=b1bf72c8378c94762785e807e2b7f15e&location={},{}&keywords={}&types={}&radius={}&offset=50&page=1&extensions=all'.format(neigh_lng, neigh_lat, kwd, typ, radius)
res = requests.get(url).json()
cnt = res['count']
category_df.iloc[row, i+1] = cnt
except:
print('第{}个社区的第{}类场所没有得到数据'.format(row+1, i+1))
category_df.iloc[row,i+1] = np.nan
print(row+1, neigh_name,'提取成功!')
return category_df
category_df = count_category(ll['Neighborhood'], ll['Longitude'], ll['Latitude'])
# 确认category_df没有空值后对数据进一步处理
# 合并健身和舞蹈类为体育类
category_df['体育'] = category_df.apply(lambda x: int(x['体育_健身']) + int(x['休闲_舞蹈']), axis=1)
category_df.drop(columns=['体育_健身','休闲_舞蹈'], axis=1, inplace=True)
category_df.iloc[:,1:] = category_df.iloc[:,1:].astype('float')
注意在数据提取过程中,可能由于API连接问题没能获取到数据,我是通过后期手动搜索,将NAN值替换了。到这里得到的结果如下图所示:
接下来对数据进行清洗,为各类场所对自己的重要程度分配权重,计算其加权平均数。
# 将数据归一化
category_df.iloc[:,1:]= category_df.iloc[:,1:].apply(lambda x: (x-x.min())/(x.max()-x.min()), axis=0)
# 按照自己对社区周边POI要求,分配权重
category_df['Weighted_venues'] = category_df['医疗']*0.1 + 0.25 * category_df['餐饮'] + 0.1*category_df['生活'] + 0.2*category_df['购物'] + 0.1*category_df['风景'] + 0.25*category_df['体育']
这样我们就获得了可以衡量社区周边POI的指标了,数值区间位于[0,1],数值越大说明该社区周边符合期望的场所越多。数据如下图:
将社区POI指标和房源数据合并,并选取其中的房租、面积和POI三列数据进行聚类分析:
# 合并房源和社区的数据
house_merge = house_df.merge(category_df, left_on='neighborhood', right_on='Neighborhood', how='left')
# 选取重要字段,用于对房源聚类分析
house_cluster_data = house_merge[['rent', 'area','Weighted_venues']]
house_cluster_data.loc[:, 'area'] = house_cluster_data.loc[:, 'area'].apply(lambda x: float(x[0:-1]))
# 归一化处理
house_cluster_data= house_cluster_data.apply(lambda x: (x-x.min())/(x.max()-x.min()), axis=0)
归一化处理后得到的数据如下图:
3.3 房源地理数据和KNN聚类分析
接下来,我们将选取房源的租金、面积以及上面得到的房源所在社区的POI指标,对所有的房源数据进行聚类分析:
# KNN聚类分析
k = 6
kmeans = KMeans(n_clusters = k, random_state=0).fit(house_cluster_data)
kmeans.labels_[0:10]
# 插入得到的cluster label
house_merge.insert(1, 'cluster label', kmeans.labels_)
为了能将房源在地图上可视化,我们还需要获取每个房源的地理位置,即经纬度:
# 获取每个房源的具体地理位置
unique_location = list(house_merge['location'].unique())
loc_df = pd.DataFrame(columns=['location','lnglat'])
# 注意:有可能获取不到房源地理位置,我后期把NAN值用手动查找的数据补上了
for index, location in enumerate(unique_location):
url = 'http://restapi.amap.com/v3/geocode/geo?key=b1bf72c8378c94762785e807e2b7f15e&address={}&city=上海'.format(location)
res = requests.get(url).json()
loc_df.loc[index, 'location']=location
if len(res['geocodes'])>0:
lnglat = res['geocodes'][0]['location']
loc_df.loc[index, 'lnglat']=lnglat
elif len(res['geocodes'])==0:
location = location.split('-')[1]+location.split('-')[2]
url = 'http://restapi.amap.com/v3/geocode/geo?key=b1bf72c8378c94762785e807e2b7f15e&address={}&city=上海'.format(location)
res = requests.get(url).json()
if len(res['geocodes'])>0:
lnglat = res['geocodes'][0]['location']
loc_df.loc[index, 'lnglat']=lnglat
else:
loc_df.loc[index, 'lnglat']=np.nan
# 再次将房源地理位置数据和房源分类数据合并
house_data = house_merge[['house', 'neighborhood','Weighted_venues', 'cluster label','rent','area', 'location','url']]
house_merged_data = house_data.merge(loc_df, on='location', how='left')
最后我们得到的数据如下所示,将该数据保存为csv文件用于后面在高德地图上将房源可视化:
3.5 房源地图可视化
使用Folium,我们将上面获得的数据在地图可视化。需要注意的是folium的编码方式是acsii,如果使用中文作为房源标记的标签,需要将文字进行编码格式的转化,具体代码如下:
# 房源可视化
# 建立房源的label
label_list = []
for i in range(house_merged_data.shape[0]):
location = house_merged_data.loc[i, 'location']
cluster = house_merged_data.loc[i, 'cluster label']
rent = house_merged_data.loc[i, 'rent']
area = house_merged_data.loc[i, 'area']
venue = house_merged_data.loc[i, 'Weighted_venues']
label = '{}, cluster{}, {}, {},{:.2}'.format(location, cluster, rent, area, venue)
label_list.append(label)
print(label_list[0:10])
# 创建地图
address = 'Shanghai'
geolocator = Nominatim(user_agent="sh_explorer")
location = geolocator.geocode(address)
latitude = location.latitude
longitude = location.longitude
map_clusters = folium.Map(location=[latitude, longitude], zoom_start=11)
# 为每个cluster分配颜色
x = np.arange(k)
ys = [i + x + (i*x)**2 for i in range(k)]
colors_array = cm.rainbow(np.linspace(0, 1, len(ys)))
rainbow = [colors.rgb2hex(i) for i in colors_array]
# 注意编码格式,Folium采用ascii编码,所以要把utf-8的编码转换
def utf2asc(s):
return str(str(s).encode('ascii', 'xmlcharrefreplace'))[2:-1]
# 在地图上增加Maker
markers_colors = []
for lat, lon, cluster, url, label in zip(house_merged_data['Latitude'], house_merged_data['Longitude'], house_merged_data['cluster label'], house_merged_data['url'], label_list):
heading3 = """<p>{}</p>""".format
label_1 = label
pp = folium.Html('<a href="'+ url +'"target="_blank">'+ utf2asc(label_1) + '</a>', script=True)
label = folium.Popup(pp)
label = folium.Popup(pp)
folium.CircleMarker(
[lat, lon],
radius=5,
popup=label,
color=rainbow[cluster-1],
fill=True,
fill_color=rainbow[cluster-1],
fill_opacity=0.7).add_to(map_clusters)
map_clusters
最后我们就获得了房源的地图,点击房源标记,会弹出房源的具体信息:名称、位置、所属Cluster,租金和面积,点击超链接会跳转到链家网该房源的页面:
4. 分析
选择指定cluster的房源,分析其共有的特征和规律,代码如下:
house_useful_data = house_merge[['house','cluster label', 'Weighted_venues','rent', 'area','location']]
house_useful_data['space'] = house_useful_data['area'].apply(lambda x: float(x.replace('㎡','')))
cluster_1 = house_useful_data[house_merge['cluster label']==0].sort_values(by='Weighted_venues', ascending=False)
cluster_1.describe()
4.1 cluster 1的房源类型
Cluster 1房源的数据如下所示:可以看出cluster 1的房源租金大致位于4000-4700元/每月的区间,属于中低租金;POI指标平均数为0.20,属于中下水平;房屋面积平均59㎡。因此将cluster 1归类为:租金中低、社区便利度中等、面积中等。
4.2 cluster 2的房源类型:
POI中位数0.15,房租中位数5700元,面积中位数72平方米。类型为:租金高、社区便利度低、面积较大。
4.3 cluster 3的房源类型:
POI中位数0.01,房租中位数4250元,面积中位数79平方米。类型为:租金中低、社区便利度极低、面积大。
4.4 cluster 4的房源类型:
POI中位数0.14,房租中位数3500元,面积中位数72平方米。类型为:租金低、社区便利度较低、面积大。
4.5 cluster 5的房源类型:
POI中位数0.4,房租中位数5700元,面积中位数45平方米。类型为:租金高、社区便利度高、面积中等。
4.6 cluster 6的房源类型:
POI中位数0.17,房租中位数5000元,面积中位数66平方米。类型为:租金中下、社区便利度低、面积中上。
5. 结果与讨论
5.1 房源类型总结
总结上面的房源类型,如下面的表格所示。
| 房源类型 | 租金 | 社区 | 面积 |
|---|---|---|---|
| cluster 1 | 4200元/月 租金中低 | 0.20 社区便利度中 | 59㎡ 面积中 |
| cluster 2 | 5700元/月 租金高 | 0.15 社区便利度中低 | 72㎡ 面积大 |
| cluster 3 | 4250元/月 租金中低 | 0.01 社区便利度极低 | 79㎡ 面积大 |
| cluster 4 | 3500元/月 租金低 | 0.14 社区便利度中低 | 72㎡ 面积大 |
| cluster 5 | 5400元/月 租金高 | 0.40 社区便利度高 | 45㎡ 面积小 |
| cluster 6 | 5000元/月 租金中高 | 0.17 社区便利度中低 | 66㎡ 面积中 |
5.2 高德地图JS API房源展示
上述通过Folium展示的房源效果还是比较静态的,而且房源的交通便利程度无法得知,所以接下来我们要进一步地增加信息,便于房源的选择。高德地图JS API提供了许多插件,能够做到更有互动性的数据展示。参考高德地图JS API上的众多示例,可以自己设计不同效果的地图。最后展现的效果如下图所示。在右上角的信息窗体中可手动填入工作地点,然后该地点将会在地图上定位,周围车程1小时内的区域将会被标蓝。
接下来,我们可以将上面处理后的房源csv文件导入,房源将会在地图上被标记出来,在右侧窗体中还可选择相应的房源类型过滤地图上展示的房源类型:
选择相应的房源标记,会弹出该房源的信息窗体,其中有包括房源、社区、社区POI指标、房源类型、租金、位置等信息,以及可以跳转到链家上该房源的超链接:
**现在,我们就可以根据上面总结的cluster类型区别,很方便直观地在地图上根据自己的需求选择合适的房源类型了。**比如:一般我们会希望房源离工作地点更近一些,因此我们只考虑位于地图蓝色区域中的房源。然后,我们再进一步地考虑房源的社区便利度,尽量从cluster 1和cluster 5中选择房子。如果我们还希望在社区便利度合适的情况下,租金能低一点,则应该选择cluster 1类型的房源。在最后被筛选出来地少数房源中,我们可以点开信息窗体一一查看相关信息,进行最后的比较。确定后便可点击房源的链接跳转到租房界面。这样一来,是不是就能很快定位适合我们的房源了呢。
6. 总结
本项目的整个流程为:
graph TD
1.房源数据获取:Python爬虫--> 2.房源社区POI指标计算:高德地图POI搜索 --> 3.房源聚类分析:KNN聚类分析 --> 4.房源可视化:Folium -->5.高德地图交通规划:高德地图API
通过将海量房源分成6个类别,根据类别信息及高德地图API中的交通规划,我们能迅速的锁定小部分的适合我们的房源,大大减轻了租房过程中的繁琐,以及信息分散的问题。希望这篇文章能给到你一些帮助哦。