分类目录机器学习

使用sklearn的回归模型进行二手房估值(工程化过程)

本文主要内容

背景部分:数据背景和需求
第一部分:建模过程和需要解决的问题
第二部分:数据预处理,解决训练和提交估值数据的预处理
第三部分:模型训练和持久化
第四部分:使用训练好的模型进行估值
第五部分:提供数据接口(此部分待完善)

开篇之前,这里有一篇从入门到精通做二手房估值模型的文章:用机器学习自制二手房估价模型。可以了解一下数据获取的方式,线性回归的基本概念等。
这篇文章流程虽完整,但实际上还是没有完成工程化(训练数据和预测数据同时预处理,没有考虑新数据传来的情况),本文将完善此过程。
这里爬虫部分就不再介绍,同时为了方便,也不再爬取地理位置对应的地铁站、学校、医院数量等。提取了部分用于建模的字段如下(不要在意字段命名):

二手房数据

文档链接:[重庆二手房数据](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("预测完成,请查看文件。")

结果评估方面就暂时不展示了。由于这里的数据预处理过程比较粗略,同时对数据的相关性、多重共线性等未作处理,模型效果实际上并不是很理想。这些都是要在后期模型优化需要做的工作。

第五部分:提供数据接口(此部分待完善)

这部分就暂时不写了,主要是提供网页接口,提供面向用户的二手房估值功能。

以上,由于水平有限,可能会有一些问题。欢迎讨论!

Python产品组合售卖策略算法设计

假设我们有ABCDEF共6个产品售卖,利润率分别如下表。假设我们的客户统一都是只有50的资产,那么我们在保证每个客户贡献有35%的利润率的情况下,可以给客户哪些产品组合(即限定客户只能买某些产品多少件,以避免客户购买利润率低的产品过多导致单个客户利润率不足)?

首先我们考虑上述限定条件的数学逻辑,以上条件符合以下限定条件:

Xi表示每个产品的价格。

pi表示购买的产品数量。

ri表示每个产品的利润率。

这是一个线性规划的问题,但我们先不考虑用运筹学的方法来解决问题。而通过手写算法来实现。

1、考虑目标利润35%,那么产品组合方案中必定存在利润率在35%或以上的产品。因此将产品数据分为2组:35%及以上为一组,35%以下为第二组。

2、计算单个产品在50元以内可以卖多少个,并把个数和总价信息分别补充到一组和二组中,形成单个产品在50元价格内的可购买数量列表。

3、通过逐一添加一组的数据和二组的数据组合,判断其是否满足前面提到的限定条件,如果满足则返回当前的产品组合。保存并存储即可。

将数据整理如下并存储在data工作表中:

使用pandas将数据读取为data,设定客户资产balance和利润率目标rate:

定义一个函数data_cls,完成1和2的操作。返回的结果u_data和o_data分别表示小于和大于等于rate目标利润率的条件下,在帐户资产为balance的条件下可以购买的所有单个产品的数量和总价组合信息。Max_n是50块可以买到的最多多少个产品,以此来确定我们最后组合产品的数量最大为多少时,循环迭代停止。

完成了单个产品的组合分类,接下来就是对每个分类的组合进行判断。定义一个判断函数cal_comb来计算组合策略是否符合预期结果。

然后就是进行穷举计算了:

以上就利用最笨的办法完成了对产品组合售卖策略的算法设计。逻辑上没问题,计算上老费力了。我的老爷机根本算不出来,想想根本58取25的组合,百亿不止的数据集。

百度网盘,文件下载:

链接: https://pan.baidu.com/s/1qkLMVY3UTN5yglUHGuuMpg

密码: 568t