机器学习特征工程经验总结一

这块内容分为两篇文章,有一点长,但内容很实用,建议耐心的看一下。

机器学习特征工程经验总结一
机器学习特征工程经验总结二

--------------------------------------------

很多人其实非常好奇BAT里机器学习算法工程师平时工作内容是怎样?其实大部分人都是在跑数据,各种map-reduce,hive SQL,数据仓库搬砖,数据清洗、数据清洗、数据清洗,业务分析、分析case、找特征、找特征…而复杂的模型都是极少数的数据科学家在做。例如在阿里,算法工程师要挖掘业务场景,根据业务找出高效的特征,2周内可以完成一次特征迭代,一个月左右可以完成模型的小优化,来提升auc。因此特征很关键,腾讯、阿里他们得模型效果那么好多半归功于特征工程。

特征迭代的意思是把新造的、组合的、修正的特征加入模型中并且优化结果,这就是一次特征迭代。

工作中特征工程的流程一般是以下几点:

1. 数据采集
2. 数据清洗
3. 数据采样
4. 特征处理
5. 特征选择

而这篇总结核心主要在特征处理和特征选择上,特别是特征处理,除了对不同类型特征进行数据处理外,还有如何找到和构建组合特征。

数据采集
数据采集前需要明确采集哪些数据,一般的思路为:哪些数据对最后的结果预测有帮助?数据我们能够采集到吗?线上实时计算的时候获取是否快捷?

比如我要给用户做商品推荐,那我需要采集什么信息呢?
-店家:店铺的评分、店铺类别……
-商品:商品评分、购买人数、颜色、材质、领子形状……
-用户:历史信息(购买商品的最低价最高价)、消费能力、商品停留时间……

数据清洗
数据清洗在工作中也是很重要一步。数据清洗就是要去除脏数据。

那么如何判定脏数据呢?

    简单属性判定:一个人身高3米+的人;一个人一个月买了10w的发卡。
    组合或统计属性判定:号称在米国却ip一直都是大陆的新闻阅读用户?你要判定一个人是否会买篮球鞋,样本中女性用户85%?
    补齐可对应的缺省值:不可信的样本丢掉,缺省值极多的字段考虑不用。

数据采样
采集、清洗过数据以后,正负样本是不均衡的,要进行数据采样。

既然总体中样本比例过低,很自然的思路就是从总体中重新抽样,提高建模样本中正样本的比例。

所谓正样本(positive samples)、负样本(negative samples),对于某一环境下的人脸识别应用来说,比如教室中学生的人脸识别,则教室的墙壁,窗户,身体,衣服等等便属于负样本的范畴。

过采样和欠采样是比较常用的方法,前者是增加正样本的数量,后者是减少负样本的数量。如果总体中正样本的绝对数量过少,可以将所有正样本全部纳入,再抽取部分负样本构建建模样本,这种思路其实就是过采样和欠采样的结合。

如果正样本量都很大,那么可以欠采样,也叫下采样
如果正样本数量不大就用采集更多的方法,比如
oversampling 过采样直接复制,还有smote算法,新生成更多的正样本。

#用index的方法去实现下采样和过采样
import numpy as np
import pandas as pd
 
 #下采样/欠采样
def lower_sample_data(df, percent=1):
    '''
    percent:正样本与负样本的比例,比如0.6,正样本是6,
    负样本是10
    '''
    data1 = df[df['Label'] == 1]  # 将少数正样本
    放在data1
    data0 = df[df['Label'] == 0]  # 将多数负样本
    放在data0
    index = np.random.randint(len(data0),
    size=percent * len(data0))  
    # 随机给定下采样取出样本的序号
    lower_data0 = data0.iloc[list(index)]  
    # 下采样
    return(pd.concat([lower_data0, data1]))

#过采样
def over_sample_data(df, percent=1):
    '''
    percent:正样本与负样本的比例,比如0.6,正样本是6,
    负样本是10
    '''
    data1 = df[df['Label'] == 1]  # 将少数正样本
    放在data1
    data0 = df[df['Label'] == 0]  # 将多数负样本
    放在data0
    lack_percent = percent - (len(data1)/len(data0))
    index = np.random.randint(len(data1),
     size=lack_percent * len(data1))  
     # 随机给定下采样取出样本的序号
    over_data1 = data1.iloc[list(index)]  
    # 过采样
    return(pd.concat([over_data1, data1,data0]))

---------------------


#用imblearn库去实现过采样,下采样和smote

#过采样
from sklearn.datasets import make_classification
from collections import Counter

X, y = make_classification(n_samples=5000,
n_features=2, n_informative=2,
n_redundant=0, n_repeated=0,
n_classes=3,n_clusters_per_class=1,
weights=[0.01, 0.05, 0.94],
class_sep=0.8, random_state=0)

Counter(y)
Out[10]: Counter({0: 64, 1: 262, 2: 4674})
 
from imblearn.over_sampling import RandomOverSampler
 
ros = RandomOverSampler(random_state=0)
X_resampled, y_resampled = ros.fit_sample(X, y)
 
sorted(Counter(y_resampled).items())
Out[13]:
[(0, 4674), (1, 4674), (2, 4674)]

#smote
from imblearn.over_sampling import SMOTE
 
X_resampled_smote, y_resampled_smote =
SMOTE().fit_sample(X, y)
 
sorted(Counter(y_resampled_smote).items())
Out[29]:
[(0, 4674), (1, 4674), (2, 4674)]

#下采样


和上面一样,用 imblearn.under_sampling的库


举一个数据不平衡的应用实战
本次分享的数据集来源于德国某电信行业的客户历史交易数据,该数据集一共包含条4,681记录,19个变量,其中因变量churn为二元变量,yes表示客户流失,no表示客户未流失;剩余的自变量包含客户的是否订购国际长途套餐、语音套餐、短信条数、话费、通话次数等。接下来就利用该数据集,探究非平衡数据转平衡后的效果。

# 导入第三方包
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import model_selection
from sklearn import tree
from sklearn import metrics
from imblearn.over_sampling import SMOTE

# 读取数据
churn = pd.read_excel(r'C:\Users\Administrator\Desktop\Customer_Churn.xlsx')

# 中文乱码的处理
plt.rcParams['font.sans-serif']=['Microsoft YaHei']

# 为确保绘制的饼图为圆形,需执行如下代码
plt.axes(aspect = 'equal')
# 统计交易是否为欺诈的频数
counts = churn.churn.value_counts()

# 绘制饼图
plt.pie(x = counts,labels=pd.Series(counts.index).map({'yes':'流失','no':'未流失'}), autopct='%.2f%%')
# 显示图形
plt.show()


经过可视化发现流失用户仅占到8.3%,相比于未流失用户,还是存在比较大的差异的。可以认为两种类别的客户是失衡的,如果直接对这样的数据建模,可能会导致模型的结果不够准确。不妨先对该数据构建随机森林模型,看看是否存在偏倚的现象。

原始数据表中的state变量和Area_code变量表示用户所属的“州”和地区编码,直观上可能不是影响用户是否流失的重要原因,故将这两个变量从表中删除。除此,用户是否订购国际长途业务international_plan和语音业务voice_mail_plan,属于字符型的二元值,它们是不能直接代入模型的,故需要转换为0-1二元值。

# 数据清洗
# 删除state变量和area_code变量
churn.drop(labels=['state','area_code'], axis = 1, inplace = True)

# 将二元变量international_plan和voice_mail_plan转换为0-1哑变量
churn.international_plan = churn.international_plan.map({'no':0,'yes':1})
churn.voice_mail_plan = churn.voice_mail_plan.map({'no':0,'yes':1})


经过清洗后的干净数据,接下来对该数据集进行拆分,分别构建训练数据集和测试数据集,并利用训练数据集构建分类器,测试数据集检验分类器。

# 用于建模的所有自变量
predictors = churn.columns[:-1]
# 数据拆分为训练集和测试集
X_train,X_test,y_train,y_test =
model_selection.train_test_split(churn[predictors], churn.churn, random_state=12)

# 构建决策树
dt = tree.DecisionTreeClassifier(n_estimators = 300)
dt.fit(X_train,y_train)
# 模型在测试集上的预测
pred = dt.predict(X_test)

# 模型的预测准确率
print(metrics.accuracy_score(y_test, pred))
# 模型评估报告
print(metrics.classification_report(y_test, pred))


如上结果所示,决策树的预测准确率超过93%,其中预测为no的覆盖率recall为97%,但是预测为yes的覆盖率recall却为62%,两者相差甚远,说明分类器确实偏向了样本量多的类别(no)。

# 绘制ROC曲线
# 计算流失用户的概率值,用于生成ROC曲线的数据
y_score =
dt.predict_proba(X_test)[:,1]
fpr,tpr,threshold = metrics.roc_curve(y_test.map({'no':0,'yes':1}), y_score)

# 计算AUC的值
roc_auc = metrics.auc(fpr,tpr)

# 绘制面积图
plt.stackplot(fpr, tpr, color='steelblue', alpha = 0.5, edgecolor = 'black')
# 添加边际线
plt.plot(fpr, tpr, color='black', lw = 1)
# 添加对角线
plt.plot([0,1],[0,1], color = 'red', linestyle = '--')
# 添加文本信息
plt.text(0.5,0.3,'ROC curve (area = %0.3f)' % roc_auc)
# 添加x轴与y轴标签
plt.xlabel('1-Specificity') plt.ylabel('Sensitivity')
# 显示图形
plt.show()


如上图所示,ROC曲线下的面积为0.795,AUC的值小于0.8,故认为模型不太合理。(通常拿AUC与0.8比较,如果大于0.8,则认为模型合理)。接下来,利用SMOTE算法对数据进行处理。

# 对训练数据集作平衡处理
over_samples = SMOTE(random_state=1234)
over_samples_X,over_samples_y =
over_samples.fit_sample(X_train, y_train)

# 重抽样前的类别比例
print(y_train.value_counts()/len(y_train))
# 重抽样后的类别比例
print(pd.Series(over_samples_y).value_counts()/
len(over_samples_y))


如上结果所示,对于训练数据集本身,它的类别比例还是存在较大差异的,但经过SMOTE算法处理后,两个类别就可以达到1:1的平衡状态。下面就可以利用这个平衡数据,重新构建决策树分类器了。

# 基于平衡数据重新构建决策树模型
dt2 = ensemble.DecisionTreeClassifier(n_estimators = 300)
dt2.fit(over_samples_X,over_samples_y)

# 模型在测试集上的预测
pred2 =dt2.predict(np.array(X_test))

# 模型的预测准确率
print(metrics.accuracy_score(y_test, pred2))
# 模型评估报告
print(metrics.classification_report(y_test, pred2))


如上结果所示,利用平衡数据重新建模后,模型的准确率同样很高,为92.6%(相比于原始非平衡数据构建的模型,准确率仅下降1%),但是预测为yes的覆盖率提高了10%,达到72%,这就是平衡带来的好处。

# 计算流失用户的概率值,用于生成ROC曲线的数据
y_score = rf2.predict_proba(np.array(X_test))[:,1]

fpr,tpr,threshold = metrics.roc_curve(y_test.map({'no':0,'yes':1}), y_score)

# 计算AUC的值
roc_auc = metrics.auc(fpr,tpr)

# 绘制面积图
plt.stackplot(fpr, tpr, color='steelblue', alpha = 0.5, edgecolor = 'black')
# 添加边际线
plt.plot(fpr, tpr, color='black', lw = 1)
# 添加对角线
plt.plot([0,1],[0,1], color = 'red', linestyle = '--')
# 添加文本信息
plt.text(0.5,0.3,'ROC curve (area = %0.3f)' % roc_auc)
# 添加x轴与y轴标签
plt.xlabel('1-Specificity') plt.ylabel('Sensitivity')

# 显示图形 电动叉车
plt.show()

这篇是展示平时工作中如何做数据清理和预处理。

一般数据清理和预处理的流程是:

  •     数据加载与粗略查看
  •     处理丢失的数据
  •     处理离群点
  •     数据统计
  •     特征值的合并、连接
  •     数据转换、标准化、归一化
  •     去除常变量

下面会拿热门的铁达尼号等数据做示范:

1.数据加载鱼粗略查看
在pandas读进来数据一个train后,train的格式为DataFrame,调用下面的几个方法就可以大致了解我们得到的数据是什么,有什么特征值,特征值的数据类型是什么,如果是数值那么最大最小值是什么等。

train.head(5)     #显示前5行数据
train.tail(5)     #显示后5行
train.columns    #查看列名
train.info()     #查看各字段的信息
train.shape      #查看数据集行列分布,几行几列
train.describe() #查看数据的大体情况


2.处理丢失数据(缺失值)

2.1 找到丢失的位置
输出每个列丢失值也即值为NaN的数据和,并从多到少排序。

total = train.isnull().sum().sort_values(ascending=False)
print(total)


Cabin          687
Age            177
Embarked         2
Fare             0
Ticket           0
Parch            0
SibSp            0
Sex              0
Name             0
Pclass           0
Survived         0
PassengerId      0

也可以输出百分比:

percent =(train.isnull().sum()/train.isnull().count()).sort_values(ascending=False)
missing_data = pd.concat([total, percent], axis=1, keys=['Total', 'Percent'])
missing_data.head(20)


由此可以看到‘Cabin’的缺失数量最多,‘Embarked’最少。

2.2 缺失值处理
对缺失数据的处理我们有很多方法:

  •     使用可用特征的均值/中位值/众数来填补缺失值(这是从列角度去处理缺失值);
  •     使用特殊值来填补缺失值,如-1;
  •     忽略有缺失值的样本(缺失的数目很多);
  •     使用相似样本的均值添补缺失值(这时从行角度去处理缺失值);
  •     使用另外的机器学习算法预测缺失值。

2.2.1 填补:

#使用出现次数最多的值填补
train['Embarked'] = train['Embarked'].fillna('S')

train.product_type[train.product_type.isnull()]=train.product_type.dropna().mode().values

#使用中位数填补
train['Fare'] = train['Fare'].fillna(train['Fare'].median())

使用平均数填补
train['Age'] = train['Age'].fillna(train['Age'].mean())
train['LotFrontage'].fillna(train['LotFrontage'].mean())


2.2.2 去除缺失值由于缺失数量太多
根据情况可以选择忽略这一特征的列和忽略出现缺失的那几行。
通常后者出现在缺失的行数比较少的情况下。

#去掉一列
train = train.drop(['Cabin'], axis = 1)

#去掉这个特征为空的行
#当然后面可以加上inplace=True表示直接就在内存中替换了
train_new = train.drop(train[train['Embarked'].isnull()].index)

2.3 处理离群点

2.3.1查找离群点
将数据可视化(python中的pyplot),观察异常值,引用HousePrice中的一个例子:

#bivariate analysis saleprice/grlivarea
var = 'GrLivArea'

data = pd.concat([df_train['SalePrice'], df_train[var]], axis=1)

data.plot.scatter(x=var, y='SalePrice', ylim=(0,800000));


可以发现,有四个点在非常偏离的位置,先讨论上面两个偏移位。
在下方无数点形成非常好的线性关系,那这偏移很大的两点是异常值吗?不是,虽然偏离位置很远,但是与下面无数点形成的线性关系还是拟合的。
再看看右下两个值,显然它们偏离得没有道理,删!

2.3.2 离群点处理

删除离群点
 

train.sort_values(by = 'GrLivArea', ascending = False)[:2]
train= train.drop(train[train['Id'] == 1299].index)
train= train.drop(train[train['Id'] == 524].index)


保留偏离值
当然并不是所有的偏离值都需要删除,具体需要在分析之后选择处理方式。这里将偏离值保留下来并不是原封不动保留,而需要做标准化或归一化处理,具体的处理方式会在下篇特征处理说明。

除此之外,还有一种方法去除离群点。
我们还可以采用了另外一种简单有效的方法:
在原始数据上训练 xgboost,用得到的 xgb 模型输出特征的重要性,取最重要的前 20 个特征,统计每个样本在这 20 个特征上的缺失值个数,将缺失值个数大于 10 的样本作为离群点。

2.4 数据统计

#统计某一列中各个元素值出现的次数
train['MSSubClass'].value_counts()   
 
#列出数据的偏斜度
train['MSSubClass'].skew()    

#列出数据的峰度
train['MSSubClass'].kurt()

#计算两个列的相关度
train['LotFrontage'].corr(train['LotArea'])   

#观察两个列的值的二维图
x = 'GrLivArea';y = 'SalePrice'

data = pd.concat([train[y], train[x]], axis=1)

data.plot.scatter(x=x, y=y, ylim=(0,800000));
#这里800000为y的最大值

#计算所有特征值每两个之间的相关系数,并作图表示。
corrmat = train.corr()#得到相关系数
f,ax = plt.subplots(figsize = (12,9))
sns.heatmap(corrmat, vmax = .8, square = True)#热点图

#取出相关性最大的前十个,做出热点图表示
k = 10
#number of variables for heatmap
cols = corrmat.nlargest(k, 'SalePrice')['SalePrice'].index
cm = np.corrcoef(train[cols].values.T)
sns.set(font_scale=1.25)

hm = sns.heatmap(cm, cbar=True, annot=True, square=True, fmt='.2f', annot_kws={'size': 10}, yticklabels=cols.values,xticklabels=cols.values)

plt.show()
#就可以查看到相关性有多大

 

2.5 特征值合并和连接
在合并连接之前,我们需要了解pandas.groupby这个分组方法,因为很多时候我们是在从几个特征值里挖掘一些值来当作新的特征值,这样子我们这个分组的方法就显得尤为重要了,如按照同一个用户进行分组来计算这个用户的行为次数当作新的特征值等等

#按照用户分组---------------------一个特征值
train.groupby('userid',as_index=False)

#按照用户与目的地分组---------------两个特征值
train.groupby(['userid','end_loc'],as_index=False)

#用户、起点、目的地-----------------三个特征值
train.groupby(['userid','start_loc','end_loc'],as_index=False)

#跟MSSubClass进行分组,并求分组后的平均值
train[['MSSubClass', 'LotFrontage']].groupby(['MSSubClass'], as_index=False).mean()

#选取特定的属性的某个值然后进行分类
train[train['date']=='2017-1-2'].groupby(['userid'],as_index=False)  

#获得分组后,统计分组中'end_loc'的数量返回为一列由‘userid’和‘user_count’组成的新的DataFrame
user_count = train.groupby('userid',as_index=False)['end_loc'].agg({'user_count':'count'})

#将获得的新的DataFrame合并到train,更多的merge参数请查阅文档
train= pd.merge(train,user_count,on=['userid'],how='left')
user_eloc_count = train.groupby(['userid','end_loc'],as_index=False)['userid'].agg({'user_eloc_count':'count'})
train= pd.merge(train,user_eloc_count,on=['userid','end_loc'],how='left')


还有许多方法构造新特征,后面会有篇写这个特征处理。

#讲训练数据与测试数据连接起来,以便一起进行数据清洗。
#这里需要注意的是,如果没有后面的ignore_index=True,
那么index的值在连接后的这个新数据中是不连续的,
如果要按照index删除一行数据,可能会发现多删一条。

merge_data=pd.concat([train,test],ignore_index=True)  


2.6 数据转换,标准化和归一化

2.6.1 数据转换

  •     数据类型转化(例如浮点数转为整数)
  •     字符串数值化(序数编码,独热编码)
  •     数值型数据离散化
  •     字符大小写转换(例如_QQ和_qQ,都是同一个字符,一般统一转换成大小写)
  •     空格符处理(例如“中国”和“中国 ”,将空格符去除)
  •     城市名处理(例如“北京”和“北京市”,属于同一个城市,将市去掉)
#浮点型数值转换为整型
train['Age']=train['Age'].astype(int)

#字符串的替换--映射
#将字符串数值化
train['MSZoning']=train['MSZoning'].map({'RL':1,'RM':2,'RR':3,}).astype(int)
train['Embarked'] = train['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)

#一般建议将map拿出来
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}
train['Title'] = train['Title'].map(title_mapping)
train['Title'] = train['Title'].fillna(0)

#将字符串特征列中的内容分别提出来作为新的特征出现,表现为0、1。
train= pd.get_dummies(houseprice)   

#将连续型特征值分块,每一块用数字标识
train.loc[ train['Fare'] <= 7.91, 'Fare'] = 0
train.loc[(train['Fare'] > 7.91) & (train['Fare'] <= 14.454), 'Fare'] = 1
train.loc[(train['Fare'] > 14.454) & (train['Fare'] <= 31), 'Fare']   = 2
train.loc[ train['Fare'] > 31, 'Fare'] = 3
train['Fare'] = train['Fare'].astype(int)


下面这个数值转换是将数值进行log计算,使分布的数值显常态

train['SalePrice'] = np.log(train['SalePrice'])


而有时这样的log不可行,就需要使用log(x+1)来 处理.

train["SalePrice"] = np.log1p(train["SalePrice"])


#将偏斜度大于0.75的数值列log转换,使之尽量符合正态分布。
numeric_feats = train[train.dtypes != 'object'].columns

skewed_feats =
train[numeric_feats].apply(lambda x: skew(x.dropna())) #compute skewness

skewed_feats = skewed_feats[skewed_feats > 0.75]

skewed_feats = skewed_feats.index

all_data[skewed_feats] = np.log1p(all_data[skewed_feats])


 

2.6.2 数据标准化和归一化(Standardization、Normalization)
标准化归一化会在后面说,实际使用时最主要的还是要了解什么时候需要标准化,什么时候用归一化,还需要清楚当前数据适合什么标准化方式等等。
在sklearn.preprocessing 介绍的标准化方式有:

  •     preprocessing.scale()、preprocessing.StandardScaler(),使数据集呈现标准正态分布,即mean = 0,且标准差std = 1。
  •     MinMaxScaler 、MaxAbsScaler,前者使数据集分布在[0,1],后者分布在[-1,1]。这种方式通常在(1) 特征的标准差较小 (2) 可以使稀疏数据集中的0值继续为0,这两种情况下使用。
  •     preprocessing.QuantileTransformer(),将数据映射到[0,1]之间均匀分布,会破坏原数据之间的相关特性。
  •     归一化方式:preprocessing.normalize(),将样本缩放成单位向量,(1)需要使用二次方程,比如点积或者其他核方法计算样本对之间的相似性(2)常用于文本分类和内容聚类的向量空间模型的基础。

2.7 去除常变量
常变量对于因变量来说都一样,没什么几乎大的变化,所以相关性不是很强。可以通过计算每个数值型特征的标准差,剔除部分变化很 小的特征。

比如下表列出的 几个个特征是标准差接近于 0 的,我们剔除了这些特征。

参考:

https://blog.csdn.net/weixin_42736194/article/details/83045400

https://blog.csdn.net/weixin_42736194/article/details/83046959

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值