结合已分享的 python 内容,本篇做一次初级的房价数据分析。该数据源于kaggle,是真实的西雅图房产销售数据,提取码: 2w9v,下载的数据内容如下 :

查看数据
拿到数据以后,一般可在 PyCharm 先用 Pandas 预览数据的大小,特征,本例子是csv文件,使用read_csv函数来读取,读进来之后是DataFrame格式
import numpy as np
import pandas as pd
# 读取房价数据
house_price = pd.read_csv('house_data.csv')
# 查看每一列的计数及数据类型等信息
print(house_price.info())
# DataFrame打印时显示所有列
pd.set_option('display.max_columns', None)
# 查看统计信息
print(house_price.describe())
# 查看前10行
print(house_price.head(10))
因为文章可看性的原因,打印内容较多这里使用省略号代替,预览情况如下 :
price bedrooms ... yr_built yr_renovated
count 4.600000e+03 4600.000000 ... 4600.000000 4600.000000
mean 5.519630e+05 3.400870 ... 1970.786304 808.608261
std 5.638347e+05 0.908848 ... 29.731848 979.414536
min 0.000000e+00 0.000000 ... 1900.000000 0.000000
25% 3.228750e+05 3.000000 ... 1951.000000 0.000000
50% 4.609435e+05 3.000000 ... 1976.000000 0.000000
75% 6.549625e+05 4.000000 ... 1997.000000 1999.000000
max 2.659000e+07 9.000000 ... 2014.000000 2014.000000
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4600 entries, 0 to 4599
Data columns (total 17 columns):
date 4600 non-null object
price 4600 non-null float64
bedrooms 4600 non-null float64
bathrooms 4600 non-null float64
...
statezip 4600 non-null object
country 4600 non-null object
dtypes: float64(4), int64(9), object(4)
数据分类
处理特征前先根据业务知识筛选出有效特征,城市 / 州的邮编 / 国家 中城市划分房价最细,因此排除州和国家;然后进行分类,一般可分为离散型和连续性;离散型又可分为有序型和无序型
特征 | 描述 | 分类 |
---|---|---|
date | 销售日期 | 连续型 |
price | 房屋价格 | 连续型 |
bedrooms / bathrooms | 房间数量 / 洗手间数量 | 连续型 |
sqft_living / sqft_lot | 房屋居住面积 / 占地面积 | 连续型 |
floors | 房屋层数 | 离散有序型 |
waterfront | 是否临海 | 离散无序型 |
view | 被看过房次数 | 离散有序型 |
condition | 房屋评价等级 | 离散有序型 |
sqft_above / sqft_basement | 地上面积 / 地下室面积 | 连续型 |
yr_built / yr_renovated | 建筑时间 / 翻新时间 | 连续型 |
city / zipcode / country | 城市 / 州的邮编 / 国家 | 离散无序型 |
#连续型数据,日期为字符串暂不选入
c_features = ['price', 'sqft_living', 'sqft_lot', 'sqft_above', 'sqft_basement',
'yr_built', 'yr_renovated']
#离散型数据,城市为字符串暂不选入
d_features = ['bedrooms', 'bathrooms', 'floors', 'waterfront', 'view', 'condition']
低方差过滤
若特征的数值变化幅度很小 (连续值方差接近0) 或近乎只有单一值 (离散值多数为同一类),可以认为它对预测的因变量没有影响
# 连续变量的方差,越小越无变化
print(house_price[c_features].std(),'\n')
# 离散变量的类别及数量
for i in d_features:
print(house_price[i].value_counts(),'\n')
因篇幅只列出了局部打印,从结果可看出数据集变化明显,没有需要忽视的特征
price 563834.702547
sqft_living 963.206916
sqft_lot 35884.436145
sqft_above 862.168977
sqft_basement 464.137228
yr_built 29.731848
yr_renovated 979.414536
1.0 2174
2.0 1811
1.5 444
3.0 128
2.5 41
3.5 2
Name: floors, dtype: int64
0 4140
2 205
3 116
4 70
1 69
Name: view, dtype: int64
数据分布
大多数分析方法是建立在数据集符合正态分布的前提;一般情况下,样本超过30,连续型数据就会呈正态分布,否则说明可能有其他未知因素的影响或取样不随机,使预测结果受影响;
通过.describe()
发现该数据集连续型特征存在偏态性,即存在极端值,如max值远大于平均数加上3倍标准差;可以计算偏度、峰度或可视化图表进一步确认;
import seaborn as sns
import matplotlib.pyplot as plt
for i in c_features:
# 计算特征的偏度和峰度,偏度为0、峰值为3符合正态分布
print(house_price[i].skew())
print(house_price[i].kurt()-3)
# 可视化特征数据分布
fig, axes = plt.subplots(2, 4)
fig.set_size_inches(20, 10)
axes = axes.flatten()
for i in n:
sns.distplot(house_price[i], hist=True, kde=True, ax= axes[n.index(i)])
plt.show()

- 建筑时间呈不规则分布,可以离散化后,进行相关性分析;
- 地下室面积、翻新时间呈双峰分布,存在大量 0 值,需分出新特征;
- 房屋价格、居住面积、占地面积、室内面积有长尾,但大部分子集呈正态分布,可处理离群值后进行相关性分析;
# 更新连续型特征
c_features = ['price', 'sqft_living', 'sqft_lot', 'sqft_above', 'sqft_basement']
# 更新离散型特征,新增是否有地下室
d_features = d_features + ['has_basement']
特征清洗
缺失值、离群值等会使某些模型欠拟合或过拟合,通常使用均值代替或删除等方法;上述.describe
查看数据时房价存在 0 值和 连续型数据存在长尾,这些都需要判断如何清洗
# 打印出0值房屋的信息进行观察
free_house = house_price.query('price == 0')
print(free_house.describe())
# 没有发现0值的原因,比如建筑年代久远,面积过小等,所以认为是缺失值
house_price = house_price.query('price > 0')
# 可视化房价箱型图,可展示离群值
p = house_price[['price']].boxplot(return_type='dict')
plt.show()
# flies即为异常值的标签,得到离群值的数组
y = p['fliers'][0].get_ydata()
# 划分出正态分布和离群两部分
p1 = house_price.loc[house_price['price'] > y.min()-1]
p2 = house_price.loc[house_price['price'] < y.min()]
print('P1:\n', p1.describe(), '\n', 'P2:\n', p2.describe())
通过摘要发现离群均价是正常均价的3倍,通常与房价最相关的面积类的数值比也是2~3倍;所以连续型数据未呈正态分布的原因,一定程度与样本非随机有关,如房企有意销售大面积房屋,因此只删除箱型图中严重离群的数值

- 是否有地下室和地下室面积对房价的影响可能是不同的;
- 没有翻新的房屋可以将建筑时间视为最近一次的翻新时间;
y = np.sort(y)
# 排除掉后20%的离群值
house_price = house_price.loc[house_price['price'] < y[-50]]
# 新增特征,有无地下室
house_price['has_basement'] = house_price['sqft_basement'].apply(lambda x: 1 if x > 0 else 0)
# 补充特征,将建筑时间当作未翻新房屋的翻新时间
house_price.loc[house_price['yr_renovated'] == 0, 'yr_renovated'] = house_price.query('yr_renovated == 0')['yr_built']
相关性分析
如果数个特征之间相关性很高,只取最显著的特征加入模型即可;通常连续型特征之间使用皮尔逊系数,但需要符合正态分布;离散有序型特征使用斯皮尔曼系数等
# 皮尔逊系数热力图,下述左图
sns.heatmap(house_price[c_features].corr('pearson'), vmin=0, vmax=1, annot=True)
plt.show()
# 排除了无地下室房屋后的热力图,下述右图
data = house_price[c_features].query('sqft_basement > 0')
sns.heatmap(data.corr('pearson'), vmin=0, vmax=1, annot=True)
plt.show()
可以清楚的发现房价是与居住面积、室内面积强相关,与地下室面积中相关的;在排除无地下室类型的房屋后,可以发现居住面积、室内面积和地下室面积三者是强相关的,因此只留下居住面积

- 可以将某些连续型特征离散化后,合并计算相关性系数,如房价、居住面积;
- 分类的间距需有业务根据,如相隔1~10年内建筑的房屋新旧程度相近等;
- 二值化的离散无序型特征,即只有0和1两种选择,有时也可和有序型合并计算相关性;
# 离散化函数,入参为新特征、需离散化的特征、间距、分类类型和序列位置
def discretization(new_feature, feature, bins, type, id):
if type == 0: #type为0使用qcut
house_price[new_feature] = pd.qcut(house_price[feature],
bins, labels=range(1, bins + 1)).astype(int)
else: #type为1使用cut
house_price[new_feature] = pd.cut(house_price[feature],
bins, labels=range(1, bins + 1)).astype(int)
d_features.insert(id, new_feature) #插入离散值序列时的位置
# 新增四个特征,价格区间、居住面积区间、建筑时间区间、翻新时间区间
discretization('price_rank', 'price', 10, 0, 0)
discretization('sqft_living_rank', 'sqft_living', 10, 0, 1)
discretization('yr_built_rank', 'yr_built', 10, 1, len(d_features))
discretization('yr_renovated_rank', 'yr_renovated', 10, 1, len(d_features))
# 打印斯皮尔曼系数和绘制热力图
print(house_price[d_features].corr('spearman'))
sns.heatmap(house_price[d_features].corr('pearson'), vmin=0, vmax=1, annot=True, annot_kws={"size": 7})
plt.show()
斯皮尔曼系数需对照秩界值表,通常样本超过50时,系数在[0.29, 1]之间,才能证明特征相关;通过热力图可以看出与居住面积相关的特征大概率也与价格有关,且与前者的相关性大于后者;因此只保留居住面积

- 销售日期通常是与房价无直接关系的,数据集中的日期范围也相对较短;
- 城市映射的是不同地区房屋单位面积价格的差异,与房价必然相关;
# 取销售月份
house_price['sale_month'] = house_price['date'].apply(lambda x: x[5])
# 计算每月销售的均价
g = house_price.groupby('sale_month').agg({'price':['mean','count']})
print(g)
# 计算各城市面积均价
g = house_price.groupby('city').agg({'price': 'mean', 'sqft_living': 'mean'})
# 根据均价分类
g['city_rank'] = pd.cut(g.eval('price/sqft_living'), 10, labels=range(1, 10+1)).astype(int)
house_price = pd.merge(house_price, g.city_rank, on='city')
验证相关性
可以使用随机森林直接对特征进行筛选,通常可取重要系数超过0.15的特征;但当特征数量庞大时,还是需要依靠上述的步骤预处理减少无效特征
import pydotplus
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
# 移除重复特征及预测特征
features = c_features+d_features
print(features)
features.remove('sqft_living_rank')
features.remove('price_rank')
features.remove('price')
features.append('city_rank')
# 用于模型的数据集
features = house_price[features]
prices = house_price['price']
# 划分训练集与测试集比为4:1
train_features, test_features, train_prices, test_prices = train_test_split(features, prices, test_size = 0.2, random_state = 42)
# 调用随机森林算法训练模型
rf = RandomForestRegressor(n_estimators=100, random_state=42, max_depth=10)
rf.fit(train_features, train_prices)
#选择其中一个决策树输出dot文件
tree = rf.estimators_[5]
export_graphviz(tree,
out_file = 'tree.dot',
feature_names = list(features.columns),
rounded = True,
precision = 1)
#将dot文件转化为svg图片文件
graph = pydotplus.graph_from_dot_file('tree.dot')
graph.write_svg('price.svg')
# 打印出值的重要系数
importances = rf.feature_importances_
indices = np.argsort(importances)
print(indices)
for i in indices:
print ('feasture:'+list(features.columns)[i]+', importance:', importances[I])
查看重要系数排序,只有居住面积、城市超过了0.15,与之前的分析结果一致

预测模型
本次选择最常用的线形回归模型,因数据集存在部分离群值和某些城市房价样本较少,为了防止过拟合,选取Ridge岭回归
from sklearn.model_selection import train_test_split
from sklearn.linear_model import RidgeCV
# 将城市哑值化,因为各城市的房价系数可能是不同的
features = pd.get_dummies((house_price[['sqft_living', 'city']]))
# 划分训练集与测试集比为4:1
train_features, test_features, train_prices, test_prices = train_test_split(features, prices, test_size = 0.2, random_state = 42)
# 调用岭回归模型训练
lin_reg = RidgeCV()
lin_reg.fit(train_features, train_prices)
# 用测试值预测房价
y_predict = lin_reg.predict(test_features)
# 输出模型分数
print('lin_reg.score: ',lin_reg.score(test_features, test_prices),'\n\n')
# 输出模型的准确率
accuracy_rate = (1-abs(y_predict-test_prices)/test_prices).mean()
print('accuracy_rate: ', accuracy_rate,'\n\n')
# 输出回归系数
print('lin_reg.coef:\n',lin_reg.coef_,'\n')
print('lin_reg.coef: ',lin_reg.intercept_,'\n')
# 实际值的散点图
plt.scatter(house_price['sqft_living'], house_price['price'], color='blue', s=1)
# 将多元线线形映射至一维,预测值的直线图
features = pd.get_dummies((house_price[['sqft_living', 'city']]))
plt.plot(house_price['sqft_living'], lin_reg.predict(features), color='red', linewidth=1)
plt.show()
平均74%的精准率,预测结果一般,可以尝试非线性的模型,或继续优化特征工程

网友评论