导读:
分类问题是机器学习应用中的常见问题,而二分类问题是其中的典型,例如垃圾邮件的识别。本文基于UCI机器学习数据库中的银行营销数据集,从对数据集进行探索,数据预处理和特征工程,到学习模型的评估与选择,较为完整的展示了解决分类问题的大致流程。文中包含了一些常见问题的处理方式,例如缺失值的处理、非数值属性如何编码、如何使用过抽样和欠抽样的方法解决分类问题中正负样本不均衡的问题等等。
作者:llhthinker
欢迎转载,请保留原文链接:http://www.cnblogs.com/llhthinker/p/7101572.html
1. 数据集选取与问题定义
本次实验选取UCI机器学习库中的银行营销数据集(Bank Marketing Data Set: http://archive.ics.uci.edu/ml/datasets/Bank+Marketing )[Moro et al., 2014]. 。这些数据与葡萄牙银行机构的直接营销活动有关。这些直接营销活动是以电话为基础的。通常来说,银行机构的客服人员至少需要联系一次客户来得知客户是否将认购银行的产品(定期存款)。因此,与该数据集对应的任务是分类任务,而分类目标是预测客户是(yes)否(no)认购定期存款(变量y)。
数据集包含四个csv文件:
1) bank-additional-full.csv: 包含所有的样例(41188个)和所有的特征输入(20个),根据时间排序(从2008年5月到2010年9月);
2) bank-additional.csv: 从1)中随机选出10%的样例(4119个);
3) bank-full.csv: 包含所有的样例(41188个)和17个特征输入,根据时间排序。(该数据集是更老的版本,特征输入较少);
4) bank.csv: 从3)中随机选出10%的样例4119个)。
提供小的数据集(bank-additional.csv和bank.csv)是为了能够快速测试一些计算代价较大的机器学习算法(例如SVM)。本次实验将选取较新的数据集,即包含20个特征量的1)和2)。
2. 认识数据
2.1 数据集输入变量与输出变量
数据集的输入变量是20个特征量,分为数值变量(numeric)和分类(categorical)变量。具体描述见数据集网站http://archive.ics.uci.edu/ml/datasets/Bank+Marketing。
输出变量为y,即客户是否已经认购定期存款(binary: "yes", "no")。
2.2 原始数据分析
首先载入数据,
然后使用info()函数和describe()函数查看数据集的基本信息。
3. 数据预处理与特征工程
3.1 缺失值处理
从2.2节给出的数据集基本信息可以看出,数值型变量(int64和float64)没有缺失。非数值型变量可能存在unknown值。使用如下代码查看字符型变量unknown值的个数。
缺失值处理通常有如下的方法:
- 对于unknown值数量较少的变量,包括job和marital,删除这些变量是缺失值(unknown)的行;
- 如果预计该变量对于学习模型效果影响不大,可以对unknown值赋众数,这里认为变量都对学习模型有较大影响,不采取此法;
- 可以使用数据完整的行作为训练集,以此来预测缺失值,变量housing,loan,education和default的缺失值采取此法。由于sklearn的模型只能处理数值变量,需要先将分类变量数值化,然后进行预测。本次实验使用随机森林预测缺失值,代码如下:
def fill_unknown(data, bin_attrs, cate_attrs, numeric_attrs):
# fill_attrs = ['education', 'default', 'housing', 'loan']
fill_attrs = []
for i in bin_attrs+cate_attrs:
if data[data[i] == 'unknown']['y'].count() < 500:
# delete col containing unknown
data = data[data[i] != 'unknown']
else:
fill_attrs.append(i) data = encode_cate_attrs(data, cate_attrs)
data = encode_bin_attrs(data, bin_attrs)
data = trans_num_attrs(data, numeric_attrs)
data['y'] = data['y'].map({'no': 0, 'yes': 1}).astype(int)
for i in fill_attrs:
test_data = data[data[i] == 'unknown']
testX = test_data.drop(fill_attrs, axis=1)
train_data = data[data[i] != 'unknown']
trainY = train_data[i]
trainX = train_data.drop(fill_attrs, axis=1)
test_data[i] = train_predict_unknown(trainX, trainY, testX)
data = pd.concat([train_data, test_data]) return data
3.2 分类变量数值化
为了能使分类变量参与模型计算,我们需要将分类变量数值化,也就是编码。分类变量又可以分为二项分类变量、有序分类变量和无序分类变量。不同种类的分类变量编码方式也有区别。
3.2.1 二分类变量编码
根据上文的输入变量描述,可以认为变量default 、housing 和loan 为二分类变量,对其进行0,1编码。代码如下:
def encode_bin_attrs(data, bin_attrs):
for i in bin_attrs:
data.loc[data[i] == 'no', i] = 0
data.loc[data[i] == 'yes', i] = 1
return data
3.2.2 有序分类变量编码
根据上文的输入变量描述,可以认为变量education是有序分类变量,影响大小排序为"illiterate", "basic.4y", "basic.6y", "basic.9y", "high.school", "professional.course", "university.degree", 变量影响由小到大的顺序编码为1、2、3、...,。代码如下:
def encode_edu_attrs(data):
values = ["illiterate", "basic.4y", "basic.6y", "basic.9y",
"high.school", "professional.course", "university.degree"]
levels = range(1,len(values)+1)
dict_levels = dict(zip(values, levels))
for v in values:
data.loc[data['education'] == v, 'education'] = dict_levels[v]
return data
3.2.3 无序分类变量编码
根据上文的输入变量描述,可以认为变量job,marital,contact,month,day_of_week为无序分类变量。需要说明的是,虽然变量month和day_of_week从时间角度是有序的,但是对于目标变量而言是无序的。对于无序分类变量,可以利用哑变量(dummy variables)进行编码。一般的,n个分类需要设置n-1个哑变量。例如,变量marital分为divorced、married、single,使用两个哑变量V1和V2来编码。
marital |
V1 |
V2 |
divorced |
0 |
0 |
married |
1 |
0 |
single |
0 |
1 |
Python的pandas包提供生成哑变量的函数,故代码如下:
def encode_cate_attrs(data, cate_attrs):
data = encode_edu_attrs(data)
cate_attrs.remove('education')
for i in cate_attrs:
dummies_df = pd.get_dummies(data[i])
dummies_df = dummies_df.rename(columns=lambda x: i+'_'+str(x))
data = pd.concat([data,dummies_df],axis=1)
data = data.drop(i, axis=1)
return data
3.3 数值特征预处理
3.3.1 连续型特征离散化
将连续型特征离散化的一个好处是可以有效地克服数据中隐藏的缺陷: 使模型结果更加稳定。例如,数据中的极端值是影响模型效果的一个重要因素。极端值导致模型参数过高或过低,或导致模型被虚假现象"迷惑",把原来不存在的关系作为重要模式来学习。而离散化,尤其是等距离散,可以有效地减弱极端值和异常值的影响。
通过观察2.2节的原始数据集的统计信息,可以看出变量duration的最大值为4918,而75%分位数为319,远小于最大值,而且该变量的标准差为259,相对也比较大。因此对变量duration进行离散化。具体地,使用pandas.qcut()函数来离散化连续数据,它使用分位数对数据进行划分(分箱: bining),可以得到大小基本相等的箱子(bin),以区间形式表示。然后使用pandas.factorize()函数将区间转为数值。
data[bining_attr] = pd.qcut(data[bining_attr], bining_num)
data[bining_attr] = pd.factorize(data[bining_attr])[0]+1
3.3.3 规范化
由于不同变量常常使用不同的度量单位,从数值上看它们相差很大,容易使基于距离度量的学习模型更容易受数值较大的变量影响。数据规范化就是将数据压缩到一个范围内,从而使得所有变量的单位影响一致。
for i in numeric_attrs:
scaler = preprocessing.StandardScaler()
data[i] = scaler.fit_transform(data[i])
3.3.4 持久化预处理后的数据
由于需要训练模型预测unknown值,预处理过程的时间代价比较大。因此将预处理后的数据持久化,保存到文件中,之后的学习模型直接读取文件数据进行训练预测,无须再预处理。
def preprocess_data():
input_data_path = "../data/bank-additional/bank-additional-full.csv"
processed_data_path = '../processed_data/bank-additional-full.csv'
print("Loading data...")
data = pd.read_csv(input_data_path, sep=';')
print("Preprocessing data...")
numeric_attrs = ['age', 'duration', 'campaign', 'pdays', 'previous',
'emp.var.rate', 'cons.price.idx', 'cons.conf.idx',
'euribor3m', 'nr.employed',]
bin_attrs = ['default', 'housing', 'loan']
cate_attrs = ['poutcome', 'education', 'job', 'marital',
'contact', 'month','day_of_week'] data = shuffle(data)
data = fill_unknown(data, bin_attrs, cate_attrs, numeric_attrs)
data.to_csv(processed_data_path, index=False)
需要注意的是,由于原始数据是有序的(以时间为序),读取原始数据后,需要将其随机打乱,变成无序数据集。这里使用sklearn.utils包中的shuffle()函数进行打乱。
一些情况下原始数据维度非常高,维度越高,数据在每个特征维度上的分布就越稀疏,这对机器学习算法基本都是灾难性(维度灾难)。当我们又没有办法挑选出有效的特征时,需要使用PCA等算法来降低数据维度,使得数据可以用于统计学习的算法。但是,如果能够挑选出少而精的特征了,那么PCA等降维算法没有很大必要。在本次实验中,数据集中的特征已经比较有代表性而且并不过多,所以应该不需要降维(实验证明降维确实没有帮助)。关于降维的介绍可以参考之前写的这个博客。
总之,数据预处理对于训练机器学习算法非常重要,正所谓“garbage in, garbage out”。
4. 模型的训练与评估
4.1 划分数据集
首先,需要将处理好的数据集划分为3部分,分别是训练集(train set)、交叉检验集(Cross validation set)和测试集(test set)。(另见博客学习模型的评估和选择)。训练集是用于训练模型。交叉检验集用来进行模型的选择,包括选择不同的模型或者同一模型的不同参数,即选择在交叉检验集上的测试结果最优的模型。测试集用于检测最终选择的最优模型的质量。通常,可以按照6:2:2的比例划分,代码如下:
def split_data(data):
data_len = data['y'].count()
split1 = int(data_len*0.6)
split2 = int(data_len*0.8)
train_data = data[:split1]
cv_data = data[split1:split2]
test_data = data[split2:] return train_data, cv_data, test_data
4.2 训练集重采样
对导入的数据集按如下方式进行简单统计可以发现,正样本(y=1)的数量远小于负样本(y=0)的数量,近似等于负样本数量的1/8。
在分类模型中,这种数据不平衡问题会使得学习模型倾向于把样本分为多数类,但是,我们常常更关心少数类的预测情况。在本次分类问题中,分类目标是预测客户是(yes:1)否(no:0)认购定期存款(变量y)。显然,我们更关心有哪些客户认购定期存款。为减弱数据不均衡问题带来的不利影响,在数据层面有两种较简单的方法:过抽样和欠抽样。
- 过抽样: 抽样处理不平衡数据的最常用方法,基本思想就是通过改变训练数据的分布来消除或减小数据的不平衡。过抽样方法通过增加少数类样本来提高少数类的分类性能 ,最简单的办法是简单复制少数类样本,缺点是可能导致过拟合,没有给少数类增加任何新的信息,泛化能力弱。改进的过抽样方法通过在少数类中加入随机高斯噪声或产生新的合成样本等方法。
- 欠抽样: 欠抽样方法通过减少多数类样本来提高少数类的分类性能,最简单的方法是通过随机地去掉一些多数类样本来减小多数类的规模,缺点是会丢失多数类的一些重要信息,不能够充分利用已有的信息。
在本次实验中,采用Smote算法[Chawla et al., 2002]增加新的样本进行过抽样;采用随机地去掉一些多数类样本的方法进行欠抽样。Smote算法的基本思想是对于少数类中每一个样本x,以欧氏距离为标准计算它到少数类样本集中所有样本的距离,得到其k近邻。然后根据样本不平衡比例设置一个采样比例以确定采样倍率N,对于每一个少数类样本x,从其k近邻中随机选择若干个样本,构建新的样本。针对本实验的数据,为防止新生成的数据噪声过大,新的样本只有数值型变量真正是新生成的,其他变量和原样本一致。重采样的代码如下:
def resample_train_data(train_data, n, frac):
numeric_attrs = ['age', 'duration', 'campaign', 'pdays', 'previous',
'emp.var.rate', 'cons.price.idx', 'cons.conf.idx',
'euribor3m', 'nr.employed',]
#numeric_attrs = train_data.drop('y',axis=1).columns
pos_train_data_original = train_data[train_data['y'] == 1]
pos_train_data = train_data[train_data['y'] == 1]
new_count = n * pos_train_data['y'].count()
neg_train_data = train_data[train_data['y'] == 0].sample(frac=frac)
train_list = []
if n != 0:
pos_train_X = pos_train_data[numeric_attrs]
pos_train_X2 = pd.concat([pos_train_data.drop(numeric_attrs, axis=1)] * n)
pos_train_X2.index = range(new_count) s = smote.Smote(pos_train_X.values, N=n, k=3)
pos_train_X = s.over_sampling()
pos_train_X = pd.DataFrame(pos_train_X, columns=numeric_attrs,
index=range(new_count))
pos_train_data = pd.concat([pos_train_X, pos_train_X2], axis=1)
pos_train_data = pd.DataFrame(pos_train_data, columns=pos_train_data_original.columns)
train_list = [pos_train_data, neg_train_data, pos_train_data_original]
else:
train_list = [neg_train_data, pos_train_data_original]
print("Size of positive train data: {} * {}".format(pos_train_data_original['y'].count(), n+1))
print("Size of negative train data: {} * {}".format(neg_train_data['y'].count(), frac))
train_data = pd.concat(train_list, axis=0)
return shuffle(train_data)
4.3 模型的训练与评估
常用的分类模型包括感知机,SVM,朴素贝叶斯,决策树,logistic回归,随机森林等等。本次实验选择logistic回归和随机森林在训练集上进行训练,在交叉检验集上进行评估,随机森林的表现更优,所以最终选择随机森林模型在测试集上进行测试。
对于不同的任务,评价一个模型的优劣可能不同。正如4.2节中所言,实验选取的数据集是不平衡的,数据集中负样本0值占数据集总比例高达88.7%,如果我们的模型"预测"所有的目标变量值都为0,那么准确度(Accuracy)应该在88.7%左右。但是,显然,这种"预测"没有意义。所以,我们更倾向于能够预测出正样本(y=1)的模型。因此,实验中将正样本的f1-score作为评价模型优劣的标准(也可以用其他类似的评价指标如AUC)。训练与评估的代码如下:
def train_evaluate(train_data, test_data, classifier, n=1, frac=1.0, threshold = 0.5):
train_data = resample_train_data(train_data, n, frac)
train_X = train_data.drop('y',axis=1)
train_y = train_data['y']
test_X = test_data.drop('y', axis=1)
test_y = test_data['y'] classifier = classifier.fit(train_X, train_y)
prodict_prob_y = classifier.predict_proba(test_X)[:,1]
report = classification_report(test_y, prodict_prob_y > threshold,
target_names = ['no', 'yes'])
prodict_y = (prodict_prob_y > threshold).astype(int)
accuracy = np.mean(test_y.values == prodict_y)
print("Accuracy: {}".format(accuracy))
print(report)
fpr, tpr, thresholds = metrics.roc_curve(test_y, prodict_prob_y)
precision, recall, thresholds = metrics.precision_recall_curve(test_y, prodict_prob_y)
test_auc = metrics.auc(fpr, tpr)
plot_pr(test_auc, precision, recall, "yes") return prodict_y
利用训练评估函数可以进行模型的选择,分别选择Logistic回归模型和随机森林模型,并对其分别调整各自参数的取值,最终选择f1-score最高的随机森林模型。具体地,当将n_estimators设置为400,对正样本进行7倍的过抽样(n=7),不对负样本进行负抽样(frac=1.0),正样本分类的阈值为0.40(threshold),即当预测某样本属于正样本的概率大于0.4时,就将该样本分类为正样本。
forest = RandomForestClassifier(n_estimators=400, oob_score=True)
prodict_y = train_evaluate(train_data, test_data, forest, n=7, frac=1, threshold=0.40)
该模型在交叉检验集上的评估结果如下:
precision-recall曲线如下:
最后,将该模型应用于测试集,测试结果如下:
precision-recall曲线如下:
5. 展望
还可以考虑以下几个方面以提高F1得分:
- 更细致的特征选择,如派生属性;
- 采用更好的方法解决数据不平衡问题,如代价敏感学习方法;
- 更细致的调参;
- 尝试其他分类模型如神经网络;
完整代码:见github
参考资料:
[Moro et al., 2014] S. Moro, P. Cortez and P. Rita. A Data-Driven Approach to Predict the Success of Bank Telemarketing. Decision Support Systems, Elsevier, 62:22-31, June 2014
[Chawla et al., 2002] N. V. Chawla, L. O. Hall, K. W. Bowyer, and W. P. Kegelmeyer. SMOTE: SMOTE: Synthetic Minority Over-sampling Technique. Journal of Artificial Intelligence Research, 16:321–357, 2002.
http://blog.csdn.net/dream2009gd/article/details/35569343