本文主要内容
背景部分:数据背景和需求
第一部分:建模过程和需要解决的问题
第二部分:数据预处理,解决训练和提交估值数据的预处理
第三部分:模型训练和持久化
第四部分:使用训练好的模型进行估值
第五部分:提供数据接口(此部分待完善)
开篇之前,这里有一篇从入门到精通做二手房估值模型的文章:用机器学习自制二手房估价模型。可以了解一下数据获取的方式,线性回归的基本概念等。
这篇文章流程虽完整,但实际上还是没有完成工程化(训练数据和预测数据同时预处理,没有考虑新数据传来的情况),本文将完善此过程。
这里爬虫部分就不再介绍,同时为了方便,也不再爬取地理位置对应的地铁站、学校、医院数量等。提取了部分用于建模的字段如下(不要在意字段命名):
文档链接:[重庆二手房数据](https://pan.baidu.com/s/1gHPyoJhYSFxb3dcspiz0aQ 提取码: 5ib9 )
文档中有list_price(挂牌价)、city(城市)、building(小区)等数据,暂不纳入模型的变量(挂牌价对于新数据其实是不存在的;city目前只有重庆;building太多,需要用到卡方分箱的方法处理,暂时删除),因此直接删除。其余字段解释如下:
deal_price:成交价(万元)
deal_date:成交日期
district:区
sub_district:区块
room_type:房屋类型(高层、别墅、洋房、车位)
house_rooms:几室
house_halls:几厅
house_kitchen:几厨
house_toilets:几卫
size:建筑面积㎡
house_stru:室内结构(跃层、平层等)
build_year:建筑年份
fixture:装修情况
build_stru:建筑结构
lift_family:梯户比
property_year:产权年限
lift:是否有电梯
house_type:产权属性(商品房、经济适用房等)
第一部分:建模过程和需要解决的问题
1.1 数据预处理:将数据处理成适合建模的数据
- 变量筛选
- 缺失值填充
- 虚拟变量构建
- 异常值处理
- 相关性和多重共线性检查
- 附:需要考虑如何对需要预测数据的预处理
1.2 模型训练、调参和可视化:选定建模方法进行模型训练
由于我们需要预测的成交价是一个连续型变量,因此模型需要选择回归类的模型进行训练。回归模型有以下几类:
(1)线性回归(Linear Regression)
(2)逻辑回归(Logistic Regression)
(3)多项式回归(Polynomial Regression)
(4)逐步回归(Stepwise Regression)
(5)岭回归(Ridge Regression)
(6)套索回归(Lasso Regression)
(7)弹性回归(ElasticNet Regression)
详细介绍回归分析的文章:你应该掌握的 7 种回归模型!
1.3 模型效果评估:评估模型的效果
在sklearn中包含四种评价尺度,分别为mean_squared_error、mean_absolute_error、explained_variance_score 和 r2_score。
y:即目标值
y_:即预测值
y-:即目标值的均值
m:样本量
(1) 均方误差(mean-squared-error)
MSE:Σ(y-y_)^2/m
RMSE:sqrt(MSE)
(2) 平均绝对值误差(mean_absolute_error)
MAE:Σ|y-y_|/m
(3) 可释方差得分(explained_variance_score)
EVS:1-var{y-y_}/var{y}
(4) 中值绝对误差(Median absolute error)
MedAE=meadian(|y1,y_1|,...,|yn-y_n|)
(5) R2 决定系数(拟合优度):模型越好:r2→1,模型越差:r2→0
RSS=Σ(y-y_)^2
ESS=Σ(y-y-)^2
R2=1-RSS/TSS
1.4 模型持久化和预测:如何将训练的模型应用于新的数据评估?
- 可以使用sklearn的持久化工具joblib.dump和joblib.load进行训练模型的存储和读取,用以新数据的预测。
第二部分:数据预处理,解决训练和提交估值数据的预处理
2.1 字段分析
回到数据中,我们对数据字段进行以下分类:
目标字段:deal_price
数值型字段:house_rooms、house_halls、house_kitchen、house_toilets、size
需要转化为数值型的字段:lift_family
日期型数据:deal_date,build_year
字符型字段:除上述字段外的其他字段。
字段预处理方法如下:
(1)对于梯户比(lift_family),要读取文本中的电梯数和户数,从而计算梯户比(电梯数/户数),没有数据的,则返回0。
(2)对于数值型字段:填充缺失值为0(此方法过于粗暴,需要在后续参数调整过程进行优化)
(3)对日期型字段:需要处理成日期到当前时间的时间差
(4)对于字符型字段:通常采用onehot编码进行处理。缺失值归为一类,填充为“miss”。
通过以上的简单处理,就得到了我们准备好的可供训练或者预测的数据。
数据预处理程序构建思路
(1)我们可以定义一个pre_process类,将上述四种类型的数据预处理方法封装到该类中。
(2)由于梯户比处理方法是独立生成中文-数字对照表,因此可以将该函数独立于pre_process类,然后在pre_process中引用该方法处理的结果进行转换。
数据预处理文件DataProcess.py整体结构如下:
DataProcess.py
# 生成1000以内的汉字和数字对应表
def chnum_to_num(output=0):
...
# 数据预处理类
class pre_process():
# 定义初始化函数,用于辨别类被用于训练还是预测
def __init__(self,method='train'):
self.method=method
# 处理梯户比
def lift_proecess(self,data):
...
# 处理日期
def date_proecess(self,data):
...
# 处理字符型:onehot处理
def onehot(self,data):
...
2.2 定义梯户比处理函数
梯户比数据:“两梯八户”。这里的梯和户都是中文,需要定义一个函数进行转换。好在数据整体比较规范,除了零到十的汉字外,只有一个特例:两(和二相同)。从常识上判断,我们只需生成1-999范围内的中文-数字对照表就可以。
# 生成1000以内的汉字和数字对应表
def chnum_to_num(output=0):
ch_num=['零','一','二','三','四','五','六','七','八','九']
for hundred in range(10):
# 如果百位是0,则百位为空,否则返回百位数
if hundred==0:
hundred_ch=""
else:
hundred_ch=ch_num[hundred]+"百"
# 循环十位
for ten in range(10):
# 如果百位和十位都是0,则直接跳过,列表中已包含数据
if hundred==0 and ten==0:
continue
# 百位为0,十位为1的情况
elif hundred==0 and ten==1:
ten_ch='十'
# 百位为不为零,十位为零的情况
elif ten==0:
ten_ch="零"
else:
ten_ch=ch_num[ten]+"十"
# 循环个位
for one in range(10):
# 整百的情况
if hundred>0 and ten==0 and one==0:
ch_num.append("".join([hundred_ch]))
# 个位是0
elif one==0:
ch_num.append("".join([hundred_ch,ten_ch]))
else:
one_ch=ch_num[one]
ch_num.append("".join([hundred_ch,ten_ch,one_ch]))
num=list(range(0,1000))
num_dict=dict(zip(ch_num,num))
# 添加“两”
num_dict['两']=2
# 使用joblib存储文件
if output==0:
joblib.dump(num_dict,path+'templet/chnum_to_num.pkl')
else:
return num_dict
2.3 pre_process类中的梯户比处理函数
通过chnum_to_num产生的中文和数字对照文件chnum_to_num.pkl,将lift_family列转换为梯户比数值数据。
# 处理梯户比(将梯户数转化为梯户比数据)
def lift_proecess(self,data):
num_dict=joblib.load(path+'templet/chnum_to_num.pkl')
# 获取梯和户的汉字
data['ti_ch']=data['lift_family'].map(lambda x: re.findall('(.*)梯',x)[0] if len(re.findall('(.*)梯',x))>0 else None)
data['hu_ch']=data['lift_family'].map(lambda x: re.findall('梯(.*)户',x)[0] if len(re.findall('梯(.*)户',x))>0 else None)
data['ti']=data['ti_ch'].map(lambda x: num_dict[x] if x is not None else None)
data['hu']=data['hu_ch'].map(lambda x: num_dict[x] if x is not None else None)
data['tihu_rate']=data['ti']/data['hu']
# 填充缺失值为0
data['tihu_rate'].fillna(0,inplace=True)
# 删除多余字段
data.drop(['lift_family','hu_ch','ti_ch','ti','hu'],axis=1,inplace=True)
return data
2.4 pre_process类中的日期数据处理
数据中包含两个日期相关的字段。
deal_date:即成交日期。房产的价格实际上是和时间高度相关的。这里计算成交日期和当前日期之间的月份数,用以反映时间变量。要预测数据时,默认为要预测当天的数据,也可以输入一个历史日期,来预测历史上这样的房产售价会是多少。
build_year:建筑的年份,通常年份越久远房产估值会更低。所以,计算建筑年份和当前年份的年数差异。对缺失数据通过同小区的建筑年度均值填充,没有同小区数据则使用全部数据的均值填充。
# 将日期数据转化成为距离当前日期的时间段
def date_proecess(self,data):
###### 成交日期/或需要预测的日期距离当前日期的月份
data['deal_delta']=datetime.now()-data['deal_date']
data['deal_month']=data['deal_delta'].apply(lambda x: x.days/30)
###### 建筑年份,计算建筑距离现在有多少年
data['build_year']=date.today().year-data['build_year']
# 填充缺失值
# 方法:按小区计算平均建筑年代,如果缺失数据没有数据,则用全部数据的均值
build_avg_year=data.groupby('building')['build_year'].mean()
avg_year=data['build_year'].mean()
build_avg_year.fillna(avg_year,inplace=True)
build_avg_year=pd.DataFrame(build_avg_year)
# 生成辅助列
year_data=pd.merge(data[['building','build_year']],build_avg_year,how='left',left_on='building',right_index=True)
# 将填充缺失值
data['build_year'].fillna(year_data['build_year_y'],inplace=True)
# 删除生成的过程数据
data.drop(['deal_date','deal_delta'],axis=1,inplace=True)
return data
2.5 pre_process类中的字符型数据处理
为简便起见,我们对剩余的字符型数据的缺失值,使用一个单独类“miss”进行填充。
如果是训练过程,则需要分别存储所有字段的所有值信息。
接下来的onehot编码过程就需要读取上述存储的数据,对每个字段进行编码。这里不采用pandas自带的onehot编码函数pd.get_dummies的原因是这样处理是一次性的,不方便后期新数据的处理。
# onehot处理
def onehot(self,data):
data.drop(['building'],axis=1,inplace=True)
var_list=['district','sub_district','room_type','house_stru','fixture','build_stru','property_year','lift','house_type']
# 填充缺失值为“miss”,作为单独一类
for var in var_list:
data[var].fillna('miss',inplace=True)
# 如果是训练数据,则先要讲所有onehot的字典存储下来(存储后再调用来进行onehot编码),如果不是则直接调用训练时所存储的数据进行编码
if self.method=='train':
for var in var_list:
var_values=list(set(data[var]))
joblib.dump(var_values,path+'templet/{}.pkl'.format(var))
# 另一种方法,le_class.classes_
# le_class = preprocessing.LabelEncoder()
# le_class.fit(data[var])
# 读取每一个编码,进行onehot处理
for var in var_list:
print(" ...进行onehot编码,当前变量:{}".format(var))
var_values=joblib.load(path+'templet/{}.pkl'.format(var))
var_cols=[var+"_"+str(j) for j in range(len(var_values))]
# 生成样本量*变量值数量的全0矩阵
temp_data=pd.DataFrame(data=np.zeros((len(data),len(var_values))),columns=var_cols)
# 完成onehot处理,在合适的位置填充1
for i in temp_data.index:
temp_data.iloc[i,var_values.index(data[var][i])]=1
# 将完成了onehot编码的数据合并到DATA中
data=pd.merge(data,temp_data,left_index=True,right_index=True)
# 删除已经完成了onehot编码的数据
data.drop(['district','sub_district','room_type','house_stru','fixture','build_stru','property_year','lift','house_type'],axis=1,inplace=True)
return data
以上过程就基本完成了数据的预处理函数的定义,可以通过依次调用上述函数,完成整体的数据预处理。
第三部分:模型训练和持久化
这一分部将生成一个train.py,这个文件主要完成模型训练和模型持久化。
3.1 使用第二部分定义的函数进行数据预处理
首先我们import上面写好的文件:
import fuction_tools.DataProcess as dp
我们再定义一个data_preprcess函数,集成所有数据预处理函数进行数据预处理。这里,除了第二部分要做的处理外,我们还有一些卧室、客厅数量等数值型数据,需要填充缺失值,缺失值均填充为0。
def data_preprcess(data,method='train'):
'''
data: 需要进行数据预处理的数据,pd.DataFrame格式
method: 如果是train,则表示是训练过程,onehot步骤会存储onehot编码码表
retrun: 返回预处理完成后的数据
'''
# 数据预处理部分
print("正在进行训练模型的预处理:")
pre_p=dp.pre_process(method=method)
# 处理梯户比
print(" 训练模型预处理:处理梯户比数据")
data=pre_p.lift_proecess(data)
# 处理日期
print(" 训练模型预处理:处理日期数据")
data=pre_p.date_proecess(data)
# 进行onhot编码
print(" 训练模型预处理:onhot编码")
data=pre_p.onehot(data)
# 填充室厅卫厨的缺失值为0
print(" 训练模型预处理:填充室厅卫厨的缺失值为0")
varlist=['house_rooms','house_halls','house_kitchen','house_toilets']
for var in varlist:
data[var].fillna(0,inplace=True)
# data.to_csv('abc.csv',encoding='gb2312')
return data
通过data_preprcess函数,即可以将我们输入的数据转换成为可以用于模型训练的成品数据。
3.2 使用随机森林回归进行训练
这里参考了开篇提及的文章中所使用的方法。使用joblib.dump保存训练结果。
# (1)使用随机森林回归法,进行模型训练并保存训练结果
def rdf_train_data(data):
# start_time=datetime.now()
# 打乱数据顺序
data=data.reindex(np.random.permutation(data.index))
Y_train=data['deal_price']
X_train=data.drop(['deal_price'],axis=1)
# 调用scikit-learn的网格搜索,传入参数选择范围,并且制定随机森林回归算法,cv = 5表示5折交叉验证
param_grid = {"n_estimators":[5,10,50,100,200,500],"max_depth":[5,10,50,100,200,500]}
grid_search = GridSearchCV(RandomForestRegressor(),param_grid,cv = 5)
# 让模型对训练集和结果进行拟合
grid_search.fit(X_train,Y_train)
print("随机森林回归模型的拟合优度R^2为:"+str(np.around(grid_search.best_score_,4)))
# 存储训练的模型
joblib.dump(grid_search,path+'models/grid_search.pkl')
# end_time=datetime.now()
# user_minte=round((end_time-start_time).seconds/60,1)
# print("共用时:"+str(user_minte)+"分钟")
3.3 使用线性回归、岭回归等模型进行训练
定义三种模型方法,按照用户的输入,进行训练并输出模型结果。
# (2)使用线性回归、岭回归等模型进行训练
def linear_train_data(data,t_method='Ridge'):
'''
data: 要训练的数据,来源于data_preprcess
method: {Ridge,Lasso,LinearRession},三种训练模型,默认为Ridge岭回归
'''
# 定义训练方法
methods={'LinearRession':linear_model.LinearRegression(fit_intercept=True),
'Lasso':linear_model.Lasso(),
'Ridge':linear_model.Ridge()}
linear=methods[t_method]
# 划分训练集和测试集
Y=data['deal_price']
X=data.drop(['deal_price'],axis=1)
# X_train,X_test,Y_train,Y_test=train_test_split(X,Y,test_size=0.1,random_state=100)
linear.fit(X,Y)
joblib.dump(linear,path+'models/{}.pkl'.format(t_method))
# 使用模型进行预测
print(t_method+"模型的拟合优度R^2为:"+str(np.around(linear.score(X,Y),4)))
上述过程定义了4种模型进行训练,并保存了模型结果。
第四部分:使用训练好的模型进行估值
4.1 定义一个统一的预测接口,方便调用各种方法进行预测
通过定义sh_predict函数,后期数据预测调用该函数就可以对新数据进行预测,并输出预测结果。
# 定义预测方法函数
def sh_predict(data,p_predict='grid_search'):
'''
data:要预测的数据(已完成数据预处理),需要和训练模型的基本数据字段相同,可以参考dataprocess.py,有相关介绍
p_predict:{grid_search,Ridge,Lasso,LinearRession},四种训练模型,默认为grid_search随机森林回归
'''
linear=joblib.load(path+'models/{}.pkl'.format(p_predict))
predict_y=linear.predict(data)
return predict_y
4.2 进行预测和输出各种模型的预测结果
上面定义好了各种函数,这里读取数据,调用函数进行预处理,调用函数进行预测,存储结果数据就可以。
path='F:/second_hs/'
# 读取数据
print("读取数据...")
p_data=pd.read_excel(path+"predict_house.xlsx",
parse_dates=['deal_date'])
r_data=p_data.copy()
# 进行数据预处理
pp_data=data_preprcess(p_data,method='predict')
pp_data.drop('deal_price',inplace=True,axis=1)
# 进行预测
for mthd in ['Ridge','Lasso','LinearRession','grid_search']:
print("使用:{}方法进行预测。".format(mthd))
predict_y=sh_predict(pp_data,p_predict=mthd)
r_data[mthd]=predict_y
# 导出数据
r_data.to_csv(path+'predict_result.csv',index=False)
print("预测完成,请查看文件。")
结果评估方面就暂时不展示了。由于这里的数据预处理过程比较粗略,同时对数据的相关性、多重共线性等未作处理,模型效果实际上并不是很理想。这些都是要在后期模型优化需要做的工作。
第五部分:提供数据接口(此部分待完善)
这部分就暂时不写了,主要是提供网页接口,提供面向用户的二手房估值功能。
以上,由于水平有限,可能会有一些问题。欢迎讨论!