回归问题的常见处理方式

Kaggle中的入门竞赛Houseprice竞赛是一个经典的回归问题,下面将以其中的特征工程代码演示一下回归问题中的常见套路。

1. 缺失值处理

缺失值处理通常有如下几种方式:

  • 以特定值填充,有些NAN值具有特殊意义
  • 使用该特征的均值或中位数进行填充,适用于数值型特征
  • 使用该特征的众数进行填充,适用于分类型或离散型特征
  • 参考同类特征进行填充,如Houseprice中可以参考同处一个Neighborhood的特征的数值分布进行缺失值填充
  • 直接删除,适用于缺失值过多,且该特征方差过小的情况
# 区域因素
    data['MSZoning'] = data['MSZoning'].fillna(data['MSZoning'].mode().iloc[0])
        # 交通地形因素
    for f in ['Street','Alley','LandContour','LandSlope','Condition1','Condition2']:
        data[f] = data[f].fillna(data[f].mode().iloc[0])
    data['LotFrontage'] = data['LotFrontage'].fillna(data['LotFrontage'].mean())
        # 房屋总体特征
    for f in ['MasVnrType','MasVnrArea','Exterior1st','Exterior2nd','Functional']:
        data[f] = data[f].fillna(data[f].mode().iloc[0])
        # 房屋内部配置
    for f in ['BsmtQual','BsmtCond','BsmtFinSF1','BsmtFinSF2','BsmtFullBath','BsmtUnfSF','BsmtHalfBath',
            'GarageQual','GarageCond','PoolQC','KitchenQual','GarageArea','GarageCars']:
        data[f] = data[f].fillna(0)
    for f in ['BsmtExposure','BsmtFinType1','BsmtFinType2','GarageType','GarageYrBlt','GarageFinish','Fence','MiscFeature']:
        data[f] = data[f].fillna('None')
    data['TotalBsmtSF'] = data['TotalBsmtSF'].fillna(data['TotalBsmtSF'].mean())
    data['Electrical'] = data['Electrical'].fillna(data['Electrical'].mode().iloc[0])
    data['FireplaceQu'] = data['FireplaceQu'].fillna(0.0)
        # 销售信息
    data['SaleType'] = data['SaleType'].fillna(data['SaleType'].mode().iloc[0])
        # 其他
    data['Utilities'] = data['Utilities'].fillna(data['Utilities'].mode().iloc[0])

2. 添加新特征

利用现有的特征,添加新特征,这是机器学习项目中最具创造性的步骤,特征工程决定了最终得分的上限,能否找到项目中的Golden Feature是项目成败的关键。

这个步骤主要依靠对于特定业务的了解。

套路的话主要是对特征的组合或者添加多次项转化成多项式回归。

我曾见过一个很生猛的套路:对任意两列特征做加减乘运算,生成新的特征,然后再进行筛选,如果你的电脑性能够强,对项目业务又不太熟悉,不妨尝试一下这种方法,参考代码

这个项目中我只添加了三个特征,效果尚可(我曾按国内房产评估方法为每个test样本添加了可比实例价格,效果不好)。

data['Remodeled'] = (data['YearBuilt'] != data['YearRemodAdd']) * 1
    data['Age'] = data['YrSold'] - data['YearBuilt'] + 1
    data['TotalSF'] = data['TotalBsmtSF'] + data['1stFlrSF'] + data['2ndFlrSF']

3. 特征处理

连续数值型特征

对数值型特征的处理方式很简单,主要是对偏态分布的数据进行标准化处理,对于偏度大于某个阈值的特征转为正态分布或者取对数处理(如果觉得设定偏度阈值太麻烦了,可以直接对所有数值型特征进行处理)。

numeric_feats = data.drop(['AVG_PRICE','SalePrice'],axis=1).dtypes[(data.dtypes != 'object') & (data.dtypes != 'datetime64[ns]')].index # 获取数值列
    skewed_feats = data[numeric_feats].apply(lambda x: skew(x))
    skewed_feats = skewed_feats[skewed_feats > 0.75]
    skewed_feats = skewed_feats.index
    std = StandardScaler()
    data[skewed_feats] = std.fit_transform(data[skewed_feats])

在分类和关联分析问题中,还会有连续变量离散化的操作。

分类型或离散型特征

字符型的分类特征无法直接带入回归模型中运算,需要进行数值化,然而进行数值化之后,模型会考虑各数值之间的距离:比如把红黄绿三种颜色编号为123,那么模型会认为红色和黄色之间的距离比红色和绿色之间的距离近,从而导致模型偏差。

通常会采用的方式是对特征进行独热编码,可以通过sklearn中的OneHotEncoder()和pandas中的get_dummies()实现。

4. 特征筛选

特征筛选的筛选主要有两类方式,一种我称之为统计筛选,另一种是模型筛选

统计筛选

  • 方差选择法
  • 相关系数法
  • 卡方检验法
  • 互信息法

这些方法中,方差选择法是单独计算每个特征的方差,选择方差高于阈值的特征。其他三种方法是采用不同的手段计算特征与因变量(预测目标)之间的相关性来筛选特征。

模型筛选

模型筛选常见的也有两种方式:

  • 使用模型中的特征重要性进行排序
  • 逐步添加或减少特征,如果模型得到改善则保留更改

其实两种方式差不多,只是方法1中的特征重要性只考虑单特征对模型的影响,而方法2中考虑的是不同特征组合的模型效果,在方法2中,本地cv验证方法的选取非常重要。我采用的是第二种方法,代码如下:

def backward_cv(train_data,clf = RidgeCV(alphas=[1e-6,1e-5,1e-4,1e-3,1e-2,1e-1,1])):
    '''
    逐步删除特征,运算时间较长,clf尽量选择简单模型
    '''
    x = train_data.drop(['SalePrice','AVG_PRICE','Id'],axis=1)
    y = np.log(train_data['AVG_PRICE'])
    best_score = check(x,y)
    dropped_col = []
    for col in tqdm(x.columns):
        score = check(x.drop(col,axis=1),y)
        if score <= best_score:
            x = x.drop(col,axis=1)
            best_score = score
            print(score)
            dropped_col.append(col)
        else:
            pass
    print(x.shape,best_score)
    return x.columns

def forward_cv(train_data,clf = RidgeCV(alphas=[1e-6,1e-5,1e-4,1e-3,1e-2,1e-1,1])):
    '''
    逐步增加特征
    '''
    x = train_data.drop(['SalePrice','AVG_PRICE','Id'],axis=1)
    y = np.log(train_data['AVG_PRICE'])
    best_score = np.inf
    # 先寻找最好的单特征
    best_col = ""
    for col in tqdm(x.columns):
        score = check(x[col].reshape(-1,1),y)
        if score < best_score:
            best_score = score
            best_col = col
            print(score)
    x_new = pd.DataFrame({
        best_col:x[best_col]
    })
    print('===best_col==',best_col)
    for col in tqdm(x.drop(best_col,axis=1).columns):
        x_new[col] = x[col] # 这一列莫名其妙地加到了行上面
        score = check(x_new,y)
        if score < best_score:
            best_score = score
            print(col,score)
        elif len(x_new.shape) > 1:
            x_new = x_new.drop(col,axis=1)
    return x_new.columns

降维

  • PCA
  • LDA

降维通常是用来减少特征中的线性相关量,控制模型中的维度,通常使用与模型中特征量过大,又不好删除的情况(不确定哪些因素对模型没有用)。这个方法我暂时没有用到。

5. 模型调参

很多模型中都有超参数,就是那种不确定会对模型影响不明确的因素。sklearn提供了两种调参方式,分别是网格搜索GridSearchCV()和随机搜索RandomizedSearchCV()。GridSearchCV效果更稳定,RandomizedSearchCV就有点看人品了,效果好的时候比GridSearchCV好,差的时候会很差。

下面是我用的调参参数

rid = search_model(Ridge(),x,y,params = {
        'alpha': [1e-6,1e-5,1e-4,1e-3,1e-2,1e-1,1],
        'fit_intercept': [True,False],
        'normalize': [True,False],
    })

    las = search_model(Lasso(),x,y,params = {
        'alpha':[1e-6,1e-5,1e-4,1e-3,1e-2,1e-1,1],
        'fit_intercept': [True,False],
        'normalize': [True,False],
        'max_iter':[100,300,500]
    })

    xg = search_model(XGBRegressor(),x,y,params = {
        'learning_rate':[0.1],
        'max_depth':[2],
        'n_estimators':[500],
        'reg_alpha':[0.2,0.3,0.4,0.5,0.6],
        'reg_lambda':[0.2,0.3,0.4,0.5,0.6,0.7]
    })

    rf = search_model(RandomForestRegressor(),x,y,params={
        'n_estimators':[300,500,800],
        'max_features':[0.5,'sqrt',0.8],
        'min_samples_leaf':[2,3,4],
        'n_jobs':[-1],
        'max_depth':[3,5,7,9,11]
    })

    krr = search_model(KernelRidge(),x,y,params={
        'alpha':[1e-4,1e-3,1e-2,1e-1,1e0,1e1],
        'kernel':['linear','polynomial','rbf'],
        'degree':[2,3,4],
    })

    gbd = search_model(GradientBoostingRegressor(),x,y,params = {
        'loss':['ls', 'lad', 'huber', 'quantile'],
        'learning_rate':[1e-4,1e-3,1e-2,1e-1],
        'n_estimators':[100,200,400],
        'criterion':['mse'],
        'max_features':['sqrt','log2']
    })

6. 模型融合

模型融合的目的是提高模型的泛化能力,通常会采用得分相近、但是原理相差较大的几个模型进行融合,比如回归模型中可以用Rdige/Lasso回归 + 随机森林 + xgboost 这样的组合方式。

组合方式也有多种:

Average

最简单的融合方式,就是把多个线性模型的结果进行线性组合。如果在分类问题中可以使用类似的Voting方法,这种简单又有效的方法当然要尝试一下:

def voting_predict(models,test,weights='auto'):
    '''表决结果'''
    if weights == 'auto':
        weights = [1/len(models) for i in range(len(models))]
    weights = np.array(weights).reshape(-1,1)
    predictions = np.zeros((test.shape[0],len(models)))
    for i,m in enumerate(models):
        yp = m.predict(test.drop('Id',axis=1))
        # predictions.append(yp)
        predictions[:,i] = yp
    return np.squeeze(np.dot(predictions,weights))

Bagging

多次从总样本中有放回地抽取样本,通过得到的子样本建立多个子模型,然后使用Average将这些子模型进行融合。随机森林算法就是衍生于bagging算法

Boosting

多次迭代训练,每次训练完之后,将预测效果较差的样本的权重加大,然后再对训练出来的子模型结果进行加权的线性组合(与Average类似),sklearn中提供了Adaboost和GBDT函数,可以直接调用。

Stacking

Stacking是比较难描述的算法,原理如下图所示:

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

</figure>

在Python中没有现成的模块可用,需要自己写:

class stack_model:
    '''使用KFold的方式将数据集划分为5个部分,使用每个basemodel训练五次,
    再预测五次,合并得到一个predict_price,作为mergemodel中的自变量'''
    def __init__(self,base_models,merge_model,n_folds = 5):
        self.base_models = base_models
        self.merge_model = merge_model
        self.n_folds = n_folds

    def fit(self,x,y):
        self.fitted_models = [list() for x in self.base_models] # 用于存储训练之后的模型
        kfold = KFold(n_splits = self.n_folds,shuffle = True)
        out_of_fold_predictions = np.zeros((x.shape[0],len(self.base_models)))
        for i,model in enumerate(self.base_models):
            for train_index,valid_index in kfold.split(x,y):
                instance = clone(model)
                instance.fit(x.iloc[train_index],y.iloc[train_index])
                self.fitted_models[i].append(instance)
                y_pred = instance.predict(x.iloc[valid_index])
                out_of_fold_predictions[valid_index,i] = y_pred
        self.merge_model.fit(out_of_fold_predictions,y)
        return self

    def predict(self,x):
        merge_features = np.column_stack([
            np.column_stack([
                model.predict(x) for model in models
            ]).mean(axis=1) for models in self.fitted_models
        ])
        return self.merge_model.predict(merge_features)

整个思路就是这样,现在模型还在调整中,如果效果比较好的话,我会把源代码分享出来。

最后,对机器学习感兴趣的可以加交流群:


机器学习-菜鸡互啄
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容