数据介绍
该项目是Data Castle上的美国King County房价预测训练赛,用到的数据取自于kaggle datasets,由@harlfoxem提供并分享,但是只选取了其中的子集,并对数据做了一些预处理使数据更加符合回归分析比赛的要求。
数据主要包括5月至5月美国King County的房屋销售价格以及房屋的基本信息。 数据分为训练数据和测试数据,分别保存在kc_train.csv和kc_test.csv两个文件中。 其中训练数据主要包括10000条记录,14个字段,主要字段说明如下:
第一列“销售日期”("SaleDate"):5月到5月房屋出售时的日期第二列“销售价格”("SalePrice"):房屋交易价格,单位为美元,是目标预测值第三列“卧室数”("BedroomNum"):房屋中的卧室数目第四列“浴室数”("BathroomNum"):房屋中的浴室数目第五列“房屋面积”("LivingArea"):房屋里的生活面积第六列“停车面积”("ParkingArea"):停车坪的面积第七列“楼层数”("Floor"):房屋的楼层数第八列“房屋评分”("Rating"):King County房屋评分系统对房屋的总体评分第九列“建筑面积”("BuildingArea"):除了地下室之外的房屋建筑面积第十列“地下室面积”("BasementArea"):地下室的面积第十一列“建筑年份”("BuildingYear"):房屋建成的年份第十二列“修复年份”("RepairYear"):房屋上次修复的年份第十三列"纬度"("Latitude"):房屋所在纬度第十四列“经度”("Longitude"):房屋所在经度
测试数据主要包括3000条记录,13个字段,跟训练数据的不同是测试数据并不包括房屋销售价格,需要通过由训练数据所建立的模型以及所给的测试数据,得出测试数据相应的房屋销售价格预测值。
# 导入数据分析需要的包import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport seaborn as sns%matplotlib inlineplt.style.use('ggplot')
分别导入训练数据和测试数据,并按照数据介绍中的内容给每一列加上列名
trainDf = pd.read_csv('kc_train.csv', header = None,
names = ['SaleDate', 'SalePrice', 'BedroomNum', 'BathroomNum', 'LivingArea',
'ParkingArea', 'Floor', 'Rating', 'BuildingArea', 'BasementArea',
'BuildingYear', 'RepairYear', 'Latitude', 'Longitude'])
testDf = pd.read_csv('kc_test.csv', header = None,
names = ['SaleDate', 'BedroomNum', 'BathroomNum', 'LivingArea',
'ParkingArea', 'Floor', 'Rating', 'BuildingArea', 'BasementArea',
'BuildingYear', 'RepairYear', 'Latitude', 'Longitude'])
将训练集和测试集合并,便于对数据进行统一处理
fullDf = trainDf.append(testDf, ignore_index = True )
查看数据信息
trainDf.info()
以下为输出结果
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10000 entries, 0 to 9999
Data columns (total 14 columns):
SaleDate 10000 non-null int64
SalePrice 10000 non-null int64
BedroomNum 10000 non-null int64
BathroomNum 10000 non-null float64
LivingArea 10000 non-null int64
ParkingArea 10000 non-null int64
Floor 10000 non-null float64
Rating 10000 non-null int64
BuildingArea 10000 non-null int64
BasementArea 10000 non-null int64
BuildingYear 10000 non-null int64
RepairYear 10000 non-null int64
Latitude 10000 non-null float64
Longitude 10000 non-null float64
dtypes: float64(4), int64(10)
memory usage: 1.1 MB
训练集一共有10000条数据,且数据集中没有缺失值。可以看到数据类型全是数值型。
下面对某些数据进行处理:将销售日期转换为日期格式,并提取出年份和月份;将建筑年份分区,转化为分类变量。
# 将销售日期转换为日期格式,并提取出年份和月份fullDf['SaleDate_year'] = pd.to_datetime(fullDf['SaleDate'], format='%Y%m%d').dt.yearfullDf['SaleDate_month'] = pd.to_datetime(fullDf['SaleDate'], format='%Y%m%d').dt.month
建筑年份从1900年到,每隔分一组
temp_list = [i for i in range(1900, )]
bins = temp_list[::15]
fullDf['BuildingYearBins'] = pd.cut(fullDf['BuildingYear'], bins=bins)
使用pandas的get_dummies方法进行one-hot编码,得到虚拟变量,添加前缀‘Year’
BuildingYearBinsDf = pd.get_dummies(fullDf['BuildingYearBins'], prefix='Year')
添加虚拟变量到数据集中
fullDf = pd.concat([fullDf, BuildingYearBinsDf], axis=1)
将建筑年份和销售日期两列删除
fullDf.drop(['BuildingYear', 'SaleDate'], axis=1, inplace=True)
查看房价的分布情况
%matplotlib notebook
trainDf['SalePrice'].hist(bins=30)
# 查看房价数据的描述统计量trainDf.SalePrice.describe()# 以下为输出结果count 1.000000e+04mean5.428749e+05std3.729258e+05min7.500000e+0425%3.225000e+0550%4.507000e+0575%6.450000e+05max6.885000e+06Name: SalePrice, dtype: float64
可以看到房价呈现明显的右偏态分布,大部分房价位于0-100万美元的区间,但房价最高接近700万美元。从该列的描述统计量中也能发现,房价平均值大于房价中位数,表现出右偏态的特征。
由于要分析影响房价的因素,所以作出每个变量与房价之间的关系图
plotDf = fullDf.iloc[0:10000, :] # 提取出总数据集的前10000行,即训练集的内容,用该DataFrame作图sns.barplot(x='SaleDate_year', y='SalePrice', data=plotDf)
sns.barplot(x='SaleDate_month', y='SalePrice', data=plotDf)
和的房屋销售价格比较接近。每个月的房价都会有小幅波动,4、5、6、10、11月的房价处在较高水平。因此将销售月份这一变量转化为虚拟变量加入总数据集中,删除原来的销售月份列。由于销售年份与房价相关性不大,将该列也删去。
SaleDateMonthDf = pd.get_dummies(fullDf['SaleDate_month'], prefix='Month')fullDf = pd.concat([fullDf, SaleDateMonthDf], axis=1)fullDf.drop(['SaleDate_year', 'SaleDate_month'], axis=1, inplace=True)
sns.boxplot(x='BedroomNum', y='SalePrice', data=plotDf)
sns.boxplot(x='BathroomNum', y='SalePrice', data=plotDf)
plt.scatter(plotDf['LivingArea'], plotDf['SalePrice'])
卧室数、浴室数为离散型变量,用箱形图;房屋面积为连续型变量,用散点图。可以看出这三者与房价均成正相关,原因也很好理解,这三个变量越大意味着房子越大,越大的房子一般都会越贵。
sns.scatterplot(x='ParkingArea', y='SalePrice', data=plotDf)
sns.boxplot(x='Floor', y='SalePrice', data=plotDf)
停车面积和楼层数与房屋价格的相关性不大。从图中可以看出有很多房屋停车面积很小但是价格却很贵。而房屋楼层高可能意味着每一层的面积小,比如同样都是300平方米的房子,三层楼的占地只要100平方米,而一层楼的占地就要300平方米,很多情况下是后者的地价更贵。将这两个变量从数据集中删除。
fullDf.drop(['ParkingArea', 'Floor'], axis=1, inplace=True)
sns.boxplot(x='Rating', y='SalePrice', data=plotDf)
sns.scatterplot(x='BuildingArea', y='SalePrice', data=plotDf)
从图中可以看出房屋评分对房价起着相当重要的作用,基本上评分越高的房子房价也越贵。建筑面积这个变量与房屋面积类似,建筑面积越大一般都会导致房价越高。
sns.scatterplot(x='BasementArea', y='SalePrice', data=plotDf)
可以看到地下室面积这一列数据中有很多值为0,新建一个分类变量‘HasBasement’:如果BasementArea为0则该变量为0,否则该变量为1
fullDf['HasBasement'] = 0fullDf.at[fullDf['BasementArea'] > 0, 'HasBasement'] = 1
sns.barplot(x='BuildingYearBins', y='SalePrice', data=plotDf)
sns.boxplot(x='RepairYear', y='SalePrice', data=plotDf)
可以看到年代很久远的房子和最近的新房子售价较高,说明建筑年份与房价有一定的相关性。年代久远的房子可能是因为其历史价值而被追捧,新房子的价格高也说明了相比旧房子人们更喜欢新建的房子。而修复年份与房价的相关性不大,很多房子没有修复,但是它们的价格差距很大,修复过的房子其价格也没有明显的规律,因此将修复年份列从数据集中删除。建筑年份已转化为虚拟变量,将建筑年份列也删除。
fullDf.drop(['RepairYear', 'BuildingYearBins'], axis=1, inplace=True)
sns.scatterplot(x='Longitude', y='Latitude', hue='SalePrice', data=plotDf)
将房屋的经纬度作为坐标点画出散点图,并用颜色表示房价的高低。
利用matplotlib的Basemap模块可以在地图上根据经纬度画出点
from mpl_toolkits.basemap import Basemap
Bm = Basemap(llcrnrlon=-122.6, llcrnrlat=47.1, urcrnrlon=-121.5, urcrnrlat=47.8, resolution='h')
Basemap前四个参数是画出的经纬度的范围,最后一个参数是分辨率
Bm.drawmapboundary(fill_color='aqua') #将地图填充为蓝色
Bm.drawcoastlines() # 画出海岸线
lons = plotDf.Longitude
lats = plotDf.Latitude
x, y = Bm(lons, lats) # x、y为房屋的经纬度
Bm.scatter(x, y, marker='o', color='m') #用散点图画出每个房屋的位置
图中黑线是陆地和海洋的分隔线,黑线中间的部位是海洋河流,黑线外边的区域是陆地。可以看到在陆地上样本点的分布很密集。
分别选出房价最高和最低的十个点,在地图上画出它们的位置。
max_lons = plotDf.sort_values(by='SalePrice', ascending=False).Longitude[:10]max_lats = plotDf.sort_values(by='SalePrice', ascending=False).Latitude[:10]min_lons = plotDf.sort_values(by='SalePrice', ascending=False).Longitude[-10:]min_lats = plotDf.sort_values(by='SalePrice', ascending=False).Latitude[-10:]
Bm1 = Basemap(llcrnrlon=-122.6, llcrnrlat=47.1, urcrnrlon=-121.5, urcrnrlat=47.8, resolution='h')
Bm1.drawmapboundary(fill_color='aqua')
Bm1.drawcoastlines()
x1, y1 = Bm1(max_lons, max_lats)
Bm1.scatter(x1, y1, marker='o', color='m')
Bm2 = Basemap(llcrnrlon=-122.6, llcrnrlat=47.1, urcrnrlon=-121.5, urcrnrlat=47.8, resolution='h')Bm2.drawmapboundary(fill_color='aqua')Bm2.drawcoastlines()x2, y2 = Bm2(min_lons, min_lats)Bm2.scatter(x2, y2, marker='o', color='m')
可以看到房价最高的十个点基本都分布在湖边,而房价最低的十个点基本都分布在内陆,这也验证了seaborn画出的散点图,房价高的点基本上都位于水畔,风景好的地方房价自然也高
特征选择
根据数据可视化的结果,与房价比较相关的几个特征为卧室数、浴室数、房屋面积、房屋评价、销售月份、建筑年份。
下面使用几种不同的方法来选取我们训练模型时需要的特征
1. 相关系数
求出每两个特征之间的相关系数,并用热力图画出。由于建筑年份和销售月份是虚拟变量,不便于单独拿出来和其他特征进行相关性比较,也会使热力图显得很乱,这里没有显示这两个特征与其他特征的相关系数。
sns.heatmap(fullDf.iloc[0:10000, [0,1,2,3,4,5,6,7,8,29]].corr(), annot = True, vmin = 0, vmax = 1)
可以看到与房价的相关系数在0.1以上的几个特征为:卧室数、浴室数、房屋面积、建筑面积、房屋评价、地下室面积、是否有地下室。
2. 迭代特征选择
假设最后选择的特征集合为X',所有特征的集合为X,辅助特征集合为X”。
初始化X’=空集,X”=X;对于X”中的所有特征,每次取出一个进行训练,选出一轮中得分最高的那个特征加入X',同时在X"中删除该特征;重复第二步,直到新加入任何特征模型性能都无提升
# 引入线性回归模型和交叉检验from sklearn import linear_modelfrom sklearn.model_selection import cross_val_score
lm = linear_model.LinearRegression()
df = fullDf[0:10000]
features = ['BasementArea', 'BathroomNum', 'BedroomNum', 'BuildingArea',
'Latitude', 'LivingArea', 'Longitude', 'Rating',
'Year_(1900, 1915]', 'Year_(1915, 1930]', 'Year_(1930, 1945]',
'Year_(1945, 1960]', 'Year_(1960, 1975]', 'Year_(1975, 1990]',
'Year_(1990, ]', 'Year_(, ]', 'Month_1', 'Month_2',
'Month_3', 'Month_4', 'Month_5', 'Month_6', 'Month_7', 'Month_8',
'Month_9', 'Month_10', 'Month_11', 'Month_12', 'HasBasement']
y = df['SalePrice']
selected_features = []
rest_features = features[:]
best_score = -1e+12
'''
交叉检验的评分标准选择'neg_mean_squared_error',即均方误差,在这里为负数,绝对值越小表示误差越小,
因此初始的best_score设置为一个绝对值很大的负值
'''
while len(rest_features)>0:
temp_best_i = ''
temp_best_score = -1e+12
for feature_i in rest_features:
temp_features = selected_features + [feature_i]
X = df[temp_features]
scores = cross_val_score(lm, X, y, cv=5, scoring='neg_mean_squared_error')
score = np.mean(scores)
if score > temp_best_score:
temp_best_score = score
temp_best_i = feature_i
print("select",temp_best_i,"acc:",temp_best_score)
if temp_best_score > best_score:
best_score = temp_best_score
selected_features += [temp_best_i]
rest_features.remove(temp_best_i)
else:
break
print("best feature set: ",selected_features,"score: ",best_score)
以下为输出结果
select LivingArea acc: -71349166475.62779
select Latitude acc: -61308736701.538574
select Rating acc: -57228772728.23087
select Longitude acc: -55217175788.554344
select Year_(1900, 1915] acc: -54266373901.291626
select Year_(1915, 1930] acc: -53265361117.21611
select Year_(1930, 1945] acc: -52314470279.37959
select Year_(1945, 1960] acc: -51128635898.9767
select BedroomNum acc: -50497821841.750175
select Year_(1960, 1975] acc: -49859660488.61521
select BathroomNum acc: -49271438161.992
select Month_4 acc: -49167258638.85094
select Month_3 acc: -49102030213.97478
select Year_(1990, ] acc: -49067174888.75468
select Month_12 acc: -49049580993.40038
select Year_(, ] acc: -49034235274.66737
select Year_(1975, 1990] acc: -48798669332.794945
select HasBasement acc: -48793266204.60174
select Month_7 acc: -48789312424.92695
select Month_9 acc: -48788246493.10238
select Month_10 acc: -48793653843.01216
best feature set: ['LivingArea', 'Latitude', 'Rating', 'Longitude', 'Year_(1900, 1915]', 'Year_(1915, 1930]', 'Year_(1930, 1945]', 'Year_(1945, 1960]', 'BedroomNum', 'Year_(1960, 1975]', 'BathroomNum', 'Month_4', 'Month_3', 'Year_(1990, ]', 'Month_12', 'Year_(, ]', 'Year_(1975, 1990]', 'HasBasement', 'Month_7', 'Month_9'] score: -48788246493.10238
迭代法选择出的特征为房屋面积、经纬度、房屋评价、建筑年份、卧室数、浴室数、销售月份、是否有地下室。
3. 随机森林特征选择法——Gini Importance
使用Gini指数表示节点的纯度,Gini指数越大纯度越低。然后计算每个节点的Gini指数 - 子节点的Gini指数之和,记为Gini decrease。最后将所有树上相同特征节点的Gini decrease加权的和记为Gini importance,该数值会在0-1之间,该数值越大即代表该节点(特征)重要性越大。
from sklearn.model_selection import train_test_splitfrom sklearn.ensemble import RandomForestRegressor
X为数据集的特征,y为数据集的标签
X = fullDf[:][0:10000]
X.drop('SalePrice', axis=1, inplace=True)
y = trainDf['SalePrice']
将数据集分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
rfReg = RandomForestRegressor(n_estimators=50)
rfReg.fit(X_train, y_train)
combine_lists = lambda item: [item[0], item[1]]
feature_importances = list(map(combine_lists, zip(X_train.columns, rfReg.feature_importances_)))
feature_importances = pd.DataFrame(feature_importances, columns=['feature', 'importance']).sort_values(by='importance', ascending=False)
feature_importances #输出每个特征的Gini Importance,并按从大到小的顺序排序
根据以上三种特征选择的结果,最后选取地下室面积、浴室数、卧室数、建筑面积、经纬度、房屋面积、房屋评价、建筑年份、销售月份这几个特征,删除是否有地下室的特征。
fullDf.drop('HasBasement', axis=1, inplace=True)
算法选择
选择线性回归算法和随机森林算法,评价指标选择决定系数(R Squared)
from sklearn.metrics import r2_score
X = fullDf[:][0:10000]
X.drop('SalePrice', axis=1, inplace=True)
y = trainDf['SalePrice']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
rfReg = RandomForestRegressor(n_estimators=50)
lReg = linear_model.LinearRegression()
rfReg.fit(X_train, y_train)
lReg.fit(X_train, y_train)
print('RandomForest_R2: {}nLinearRegression_R2: {}'.format(r2_score(y_test, rfReg.predict(X_test)),
r2_score(y_test, lReg.predict(X_test))))
以下为输出结果
RandomForest_R2: 0.8329728730385538
LinearRegression_R2: 0.6319265123160838
从两种模型的决定系数可以看出,随机森林算法比线性回归算法的效果要好。因此选择随机森林算法对测试集进行预测。
房价预测
使用随机森林算法对测试集进行预测。由于选择的某些特征量纲不一样,它们的单位不在一个数量级上,可能会导致有的特征被忽略,为了消除量纲的影响,对特征进行归一化。另外,由于房屋价格的分布呈右偏态,对其取对数,使其分布接近正态分布。
from sklearn.preprocessing import MinMaxScaler
tempDf = fullDf[:]
tempDf.drop('SalePrice', axis=1, inplace=True) # tempDf为测试集和训练集中不包含房价的所有特征
minmax_scaler = MinMaxScaler()
minmax_scaler.fit(tempDf)
X = minmax_scaler.transform(tempDf)
X = pd.DataFrame(X, columns=tempDf.columns) # X为测试集和训练集归一化后的所有特征
new_X = X[:][0:10000] # new_X为训练集的所有特征
y = np.log(trainDf['SalePrice']) # y为取对数后的房价
predict_X = X[:][10000:] # predict_X为测试集的所有特征
rfReg = RandomForestRegressor(n_estimators=50)
rfReg.fit(new_X, y)
predict_y = rfReg.predict(predict_X)
predict_y = np.exp(predict_y) # 将房价由对数形式还原
predictDf = pd.DataFrame(predict_y, columns=['price'])
predictDf.to_csv('predict.csv', index = False) # 将结果转化成规定的形式输出到csv文件中