%E6%AF%95%E8%AE%BE%E6%8C%87%E5%AF%BC/README.md
1 课题背景
首先,爬虫采集链家网所有二手房挂牌数据,并对采集到的数据进行清洗; 然后,对清洗后的数据进行可视化分析二手房网站源码,探索隐藏在大量数据背后的规律; 最后,采用聚类算法对所有二手房数据进行降维分析,根据降维分析的结果对此类房源进行粗略分类,从而汇总所有数据。 通过以上分析,我们可以了解市场上二手房的基本特征以及住房资源的分布情况,这可以帮助我们做出买房的决定。
2 达到疗效
总体数据文件词云
各地区二手房挂牌量折线图
二手房房屋使用水平直方图
二手房基本信息可视化分析
各地区二手房平均总价直方图
各地区二手房总价及总价箱线图
二手房总价最高的前20名
二手房总价及单价热力图
二手房总价热力图
单价大于200万的二手房分布图
二手房施工面积分析
二手房施工面积分布区间直方图
二手房可视化分析
二手房双层比例
从二手房饼图可以看出,2房1厅和2房2厅为标配,占比接近一半。 其中,3房2厅和3房1厅的房源也占了不少房源,其他双层房源的房源占比较小。
二手房家居装修
二手房房源朝向分布
二手房建筑类型占比
3 数据采集
这部分通过网络爬虫程序抓取链家网上所有二手房的数据,收集原始数据作为整个数据分析的基石。
链家网站结构分析
链家网二手房首页界面如下图所示。 首页的黄色框显示了当前正在出售二手房的各个区域的名称。 中间的白色框显示列表总数。 下图绿框所示为二手房。 房源信息缩略图,白框区域包含二手房房源页面的URL地址标签。 图2底部的黄色框显示的是二手房首页的房源页数。
链家网二手房首页截图上半部分:
二手房挂牌信息页面如下图。 我们需要采集的目标数据就在这个页面,包括基本信息、房屋属性和交易属性。 各类信息包含的数据项如下:
1)基本信息:小区名称、面积、总价、单价。
2)房屋属性:房屋类型、楼层、建筑面积、单元结构、户型面积、房屋类型、房屋朝向、建筑结构、装修、扶梯配比、配备扶梯、产权期限。
3)交易属性:挂牌时间、交易权属、上次交易、房屋用途、房龄、产权归属、抵押信息、房屋零配件等。
网络爬虫的关键问题描述
1)问题一:链家网二手房首页最多只显示100页房源数据,因此在采集二手房房源信息页面的URL地址时,会采集不完整,导致仅显示100页房源数据。最终收集到的部分数据。
解决方案:按地区爬取所有二手房数据。 只需 100 个页面即可显示多达 3,000 个套件。 当这个区域有3000多栋房子的时候,就可以直接爬行了。 如果该区域内的房屋超过3000间,则可以将其划分为更小的区域。
2)问题二:如果爬虫程序运行速度太快二手房网站源码,当采集到两三千条数据时就会触发链家网的反爬虫机制,所有请求都会被重定向到链家网的人机识别页面练甲。 抓取失败。
解决方案:①在程序中为每个http请求构造一个标头,并每次改变http请求标头中USER_AGENTS数据项的值,使请求信息看起来像是不同浏览器发出的访问请求。 ② 爬虫程序处理完一个http请求和响应后,随机休眠1-3秒。 2500个请求后,程序休眠20分钟,以控制程序的请求率。
4 数据清洗
爬虫程序收集到的数据无法直接分析。 需要去除一些“脏”数据,纠正一些错误数据,统一所有数据字段的格式,将那些分散的数据组织成统一的结构化数据。
原始数据中需要清洗的主要部分
需要清洗的数据主要部分如下:
1)对齐杂乱记录的数据项
2)清理一些数据项格式
3)缺失值处理
3.2.3 数据清洗结果
数据清洗前的原始数据如下图所示。
清洗后的数据如下图所示,可以看到清洗后的数据正则化了很多。
5 数据聚类分析
现阶段利用聚类算法中的k-means算法对所有二手房数据进行降维。 根据降维结果和经验,对此类住房来源进行了粗略的分类,达到了汇总数据的目的。 在降维过程中,我们选取面积、总价、总价三个数值变量作为样本点的降维属性。
k-means算法原理
基本的
k-Means算法是最常用的聚类算法,它是一种无监督学习算法,目的是将相似的对象分到同一个簇中。 簇中相似的对象越多,聚类效果越好。 该算法不适合处理离散属性,但对于连续属性的降维效果很好。
聚类效果判断标准
使每个样本点与簇刚体之间的误差平方和最小,这是评价k-means算法最终降维效果的评价标准。
算法实现步骤
1)选择k值
2) 创建k个点作为k个簇的起始刚体。
3)估计剩余元素到k簇刚体的距离,并将这些元素合并到距离最小的簇中。
4)根据降维结果,重新估计k个簇的新刚体,即取簇中所有元素在各自维度上的算术平均值。
5) 根据新的刚体重新减少所有单元。
6)重复步骤5,直到降维结果不再改变。
7) 最后输出降维结果。
算法缺点
K-Means算法虽然原理简单,但也有其自身的缺陷:
1)降维前需要给出用于聚类的簇数k值,但很多情况下k值的选择是非常不可预测的。 很多时候,我们并不知道降维之前给定的数据集应该划分成多少类才是最合适的。
2)k-means需要人为确定初始刚体,不同的初始刚体可能会导致截然不同的降维结果,无法保证k-means算法一定会收敛到全局最优解。
3)对异常值敏感。
4)结果不稳定(受输入顺序影响)。
5)时间复杂度较高O(nkt),其中n是对象总数,k是簇数,t是迭代次数。
算法实现 关键问题描述
K值选择说明
根据降维原则:组内差异应该小,组间差异应该大。 我们首先计算每个 SSE(Sum of
平方
误差)值,然后绘制折线图进行比较,并从中选择最优解。 从图中我们可以看出,k值达到5后,SSE逐渐发生变化,因此我们选择5作为k值。
初始K个刚体选择说明
k个刚体的初始选择是随机方法。 按照正态分布从每列的最大值和最小值中随机选择k个刚体。
关于异常值
异常值是非常不寻常且非常特殊的数据点,与整体相去甚远。 由于k-means算法对异常值非常敏感,因此在降维之前应该去除诸如“极大”、“极小”等异常值数据,否则会影响降维结果。 判断异常值的标准是基于上述数据可视化分析过程的散点图和箱线图。 根据散点图和箱线图,需要消除的离散值范围如下:
1)单价:基本都在10万以内,无异常值。
2)总价:基本上都在3000以内,这里需要剔除3000以外的异常值。
3)建筑面积:基本上都集中在500以内,这里需要剔除500以外的异常值。
数据标准化
由于单价单位为亿元,单价单位为元/平方米,建筑面积单位为平方米,因此从数据点估算欧式距离的单位是没有意义的。 同时总价在3000以内,建筑面积在500以内,但总价基本都在2万以上。 在估计距离时,平均价格比单价发挥更大的作用。 价格的作用远小于建筑面积,所以降维的结果是有问题的。 在这种情况下,我们需要对数据进行标准化,即按比例缩放数据,使它们都落入特定的区间内。 去掉数据的单位限制,将其转换为无量纲纯值,从而可以对不同单位或量级的指标进行估计和比较。
我们把总价、总价和面积映射到500,因为面积本身就在500以内,所以不要处理太多。 估算距离时,单价需要减去映射比率0.005,总价需要除以映射比率0.16。 数据标准化前和数据标准化后的降维疗效对比如下: 图32和图33为未进行数据标准化时的降维疗效散点图; 图34和图35是数据标准化后的降维疗效散点图。
数据归一化前总价与建筑面积降维效能散点图:
聚类结果分析
聚类结果如下
1)聚类结果统计信息如下:
2)聚类后总价与建筑面积散点图以及单价与建筑面积散点图。
3)聚类结果组0、1、2、3、4的区域分布图如下。
聚类结果组0的区域分布图如下:
6部分核心代码
# -*- coding: utf-8 -*-
"""
Created on Tue Feb 23 10:09:15 2016
K-means cluster
@author: liudiwei
"""
import numpy as np
class KMeansClassifier():
"this is a k-means classifier"
def __init__(self, k=3, initCent='random', max_iter=5000):
"""构造函数,初始化相关属性"""
self._k = k
self._initCent = initCent#初始中心
self._max_iter = max_iter#最大迭代
#一个m*2的二维矩阵,矩阵第一列存储样本点所属的族的索引值,
#第二列存储该点与所属族的质心的平方误差
self._clusterAssment = None#样本点聚类结结构矩阵
self._labels = None
self._sse = None#SSE(Sum of squared errors)平方误差和
def _calEDist(self, arrA, arrB):
"""
功能:欧拉距离距离计算
输入:两个一维数组
"""
arrA_temp = arrA.copy()
arrB_temp = arrB.copy()
arrA_temp[0] = arrA_temp[0]*0.16
arrA_temp[1] = arrA_temp[1]*0.005
arrB_temp[0] = arrB_temp[0]*0.16
arrB_temp[1] = arrB_temp[1]*0.005
return np.math.sqrt(sum(np.power(arrA_temp - arrB_temp, 2)))
def _calMDist(self, arrA, arrB):
"""
功能:曼哈顿距离距离计算
输入:两个一维数组
"""
return sum(np.abs(arrA-arrB))
def _randCent(self, data_X, k):
"""
功能:随机选取k个质心
输出:centroids #返回一个m*n的质心矩阵
"""
n = data_X.shape[1] - 3 #获取特征值的维数(要删除一个用于标记的id列和经纬度值)
centroids = np.empty((k,n)) #使用numpy生成一个k*n的矩阵,用于存储质心
for j in range(n):
minJ = min(data_X[:,j+1])
rangeJ = max(data_X[:,j+1] - minJ)
#使用flatten拉平嵌套列表(nested list)
centroids[:, j] = (minJ + rangeJ * np.random.rand(k, 1)).flatten()
return centroids
def fit(self, data_X):
"""
输入:一个m*n维的矩阵
"""
if not isinstance(data_X, np.ndarray) or
isinstance(data_X, np.matrixlib.defmatrix.matrix):
try:
data_X = np.asarray(data_X)
except:
raise TypeError("numpy.ndarray resuired for data_X")
m = data_X.shape[0] #获取样本的个数
#一个m*2的二维矩阵,矩阵第一列存储样本点所属的族的编号,
#第二列存储该点与所属族的质心的平方误差
self._clusterAssment = np.zeros((m,2))
#创建k个点,作为起始质心
if self._initCent == 'random':
self._centroids = self._randCent(data_X, self._k)
clusterChanged = True
#循环最大迭代次数
for _ in range(self._max_iter): #使用"_"主要是因为后面没有用到这个值
clusterChanged = False
for i in range(m): #将每个样本点分配到离它最近的质心所属的族
minDist = np.inf #首先将minDist置为一个无穷大的数
minIndex = -1 #将最近质心的下标置为-1
for j in range(self._k): #次迭代用于寻找元素最近的质心
arrA = self._centroids[j,:]
arrB = data_X[i,1:4]
distJI = self._calEDist(arrA, arrB) #计算距离
if distJI < minDist:
minDist = distJI
minIndex = j
if self._clusterAssment[i, 0] != minIndex or self._clusterAssment[i, 1] > minDist**2:
clusterChanged = True
self._clusterAssment[i,:] = minIndex, minDist**2
if not clusterChanged:#若所有样本点所属的族都不改变,则已收敛,结束迭代
break
for i in range(self._k):#更新质心,将每个族中的点的均值作为质心
index_all = self._clusterAssment[:,0] #取出样本所属簇的编号
value = np.nonzero(index_all==i) #取出所有属于第i个簇的索引值
ptsInClust = data_X[value[0]] #取出属于第i个簇的所有样本点
self._centroids[i,:] = np.mean(ptsInClust[:,1:4], axis=0) #计算均值,赋予新的质心
self._labels = self._clusterAssment[:,0]
self._sse = sum(self._clusterAssment[:,1])
def predict(self, X):#根据聚类结果,预测新输入数据所属的族
#类型检查
if not isinstance(X,np.ndarray):
try:
X = np.asarray(X)
except:
raise TypeError("numpy.ndarray required for X")
m = X.shape[0]#m代表样本数量
preds = np.empty((m,))
for i in range(m):#将每个样本点分配到离它最近的质心所属的族
minDist = np.inf
for j in range(self._k):
distJI = self._calEDist(self._centroids[j,:], X[i,:])
if distJI < minDist:
minDist = distJI
preds[i] = j
return preds
class biKMeansClassifier():
"this is a binary k-means classifier"
def __init__(self, k=3):
self._k = k
self._centroids = None
self._clusterAssment = None
self._labels = None
self._sse = None
def _calEDist(self, arrA, arrB):
"""
功能:欧拉距离距离计算
输入:两个一维数组
"""
return np.math.sqrt(sum(np.power(arrA-arrB, 2)))
def fit(self, X):
m = X.shape[0]
self._clusterAssment = np.zeros((m,2))
centroid0 = np.mean(X, axis=0).tolist()
centList =[centroid0]
for j in range(m):#计算每个样本点与质心之间初始的平方误差
self._clusterAssment[j,1] = self._calEDist(np.asarray(centroid0),
X[j,:])**2
while (len(centList) < self._k):
lowestSSE = np.inf
#尝试划分每一族,选取使得误差最小的那个族进行划分
for i in range(len(centList)):
index_all = self._clusterAssment[:,0] #取出样本所属簇的索引值
value = np.nonzero(index_all==i) #取出所有属于第i个簇的索引值
ptsInCurrCluster = X[value[0],:] #取出属于第i个簇的所有样本点
clf = KMeansClassifier(k=2)
clf.fit(ptsInCurrCluster)
#划分该族后,所得到的质心、分配结果及误差矩阵
centroidMat, splitClustAss = clf._centroids, clf._clusterAssment
sseSplit = sum(splitClustAss[:,1])
index_all = self._clusterAssment[:,0]
value = np.nonzero(index_all==i)
sseNotSplit = sum(self._clusterAssment[value[0],1])
if (sseSplit + sseNotSplit) < lowestSSE:
bestCentToSplit = i
bestNewCents = centroidMat
bestClustAss = splitClustAss.copy()
lowestSSE = sseSplit + sseNotSplit
#该族被划分成两个子族后,其中一个子族的索引变为原族的索引
#另一个子族的索引变为len(centList),然后存入centList
bestClustAss[np.nonzero(bestClustAss[:,0]==1)[0],0]=len(centList)
bestClustAss[np.nonzero(bestClustAss[:,0]==0)[0],0]=bestCentToSplit
centList[bestCentToSplit] = bestNewCents[0,:].tolist()
centList.append(bestNewCents[1,:].tolist())
self._clusterAssment[np.nonzero(self._clusterAssment[:,0] ==
bestCentToSplit)[0],:]= bestClustAss
self._labels = self._clusterAssment[:,0]
self._sse = sum(self._clusterAssment[:,1])
self._centroids = np.asarray(centList)
def predict(self, X):#根据聚类结果,预测新输入数据所属的族
#类型检查
if not isinstance(X,np.ndarray):
try:
X = np.asarray(X)
except:
raise TypeError("numpy.ndarray required for X")
m = X.shape[0]#m代表样本数量
preds = np.empty((m,))
for i in range(m):#将每个样本点分配到离它最近的质心所属的族
minDist = np.inf
for j in range(self._k):
distJI = self._calEDist(self._centroids[j,:],X[i,:])
if distJI < minDist:
minDist = distJI
preds[i] = j
return preds
最后7个