我们读取一下之前保存的pickle文件并打印出此时的train和test,前面的探索性分析过程我们保留了143个特征,接下来我们还希望在这些特征中提取出一些隐藏的信息。
train = pd.read_pickle('./data/santander-customer-satisfaction/output/train.pkl')
test = pd.read_pickle('./data/santander-customer-satisfaction/output/test.pkl')
X_train = train.copy()
X_test = test.copy()
X_train.shape,X_test.shape
((76020, 144), (75818, 143))
在EDA过程中我们知道了训练集中包含了非常多的0值,那么我们是否可以构造一个特征,他表示每行样本中,所有特征值零或非零出现的次数呢?我们将其命名为no_zeros和no_nonzeros
def add_feature_no_zeros(train=X_train,test = X_test):
#构造新特征,表示每行样本中143个特征取值为0和非零的出现次数
col = [k for k in train.columns if k != 'TARGET']
for df in [train,test]:
df['no_zeros'] = (df.loc[:,col] != 0).sum(axis=1).values
df['no_nonzeros'] = (df.loc[:,col] == 0).sum(axis=1).values
除此之外,在样本的所有特征中,每一种前缀的特征都有其独特的分布规律,每行记录每种关键词的所有特征取值为零的个数。因此我们构造新特征,表示每一行样本中每一种关键词前缀的特征取值为零或者非零的出现次数。
def add_feature_no_zeros_keyword(keyword,train=X_train,test=X_test):
col = [k for k in train.columns if keyword in k]
# for k in col:
for df in [train,test]:
df['no_zeros_'+keyword] = (df.loc[:,col] != 0).sum(axis=1).values
df['no_nonzeros_'+keyword] = (df.loc[:,col] == 0).sum(axis=1).values
add_feature_no_zeros()
keywords = list(f_keywords.keys())
for k in keywords:
add_feature_no_zeros_keyword(k)
此时我们再来查看一下训练集和测试集的shape:
X_train.shape,X_test.shape
((76020, 154), (75818, 153))
这样就新增了10个特征
除此之外,我们注意到imp和saldo前缀特征,他们的取值除了0以外,其他的值是一个右偏分布,而且分布比较零散,那么我们将其均值作为一个新特征。
考虑到一个均值对目标变量应该没有影响,构造新特征-获取唯一值个数处于(50,210]之间的’col’特征中取每一种唯一值的情况下,含imp和saldo前缀特征的均值
def average_col(col,features,train=X_train,test=X_test):
'''
获取'col'特征中每一种唯一值的情况下feature特征的均值,并令其为新特征
'''
for df in [train,test]:
unique_values = df[col].unique()
for feature in features:
#对每一个特征求他在指定特征col的每一个唯一值下的均值
avg_value = []
for value in unique_values:
#对于每一个特征列col,求其每一种唯一值的情况下feature特征的均值
avg = df.loc[df[col] == value,feature].mean()
avg_value.append(avg)
avg_dict = dict(zip(unique_values,avg_value))
new_col = 'avg_'+ col + '_' + feature
df[new_col] = np.zeros(df.shape[0])#新建新特征
for value in unique_values:
df.loc[df[col]==value,new_col] = avg_dict[value]
#含imp和saldo前缀的所有特征,不包括no_zeros_imp和no_zeros_saldo
features = [i for i in X_train.columns if (('imp' in i) | ('saldo' in i)) & ('no_zeros' not in i)]
#唯一值个数处于(50,210]之间的特征列
columns = [i for i in X_train.columns if (X_train[i].nunique() <= 210) & (X_train[i].nunique() > 50)]
len(features),len(columns)
for col in tqdm(columns):
average_col(col,features)
我们再来看看此时的shape:
X_train.shape, X_test.shape
((76020, 952), (75818, 951))
现在我们的特征已经很多了,除了最开始进行了剔除零方差和稀疏特征等一些简单的操作,我们一直在新增信息,这其中肯定包含了很多重复提取的冗余信息。主要体现在两个方面:1、与目标变量相关度非常低。2、特征之间相关性非常高,在回归分析中我们称之为多重共线性。现在我们就开始着手对这些特征进行处理。
这里我们将低相关性阈值设置为0.0001,高相关性阈值设置为0.95,即检索所有特征,如果与目标变量的相关系数小于0.0001,我们就将它剔除,如果特征彼此之间的相关系数大于0.95,我们就保留一个与目标变量TARGET相关系数最高的特征。
def remove_corr_var(train=X_train,test=X_test,
target_threshold=10**-3,within_threshold=0.95):
#删除与目标变量相关性低的特征,删除彼此之间相关性高的特征(保留一个)
initial_feature = train.shape[1]
corr = train.drop('ID',axis=1).corr().abs()
corr_target = pd.DataFrame(corr['TARGET']).sort_values(by='TARGET')
print('corr_target')
print(corr_target)
feat_df = corr_target[corr_target['TARGET']<=target_threshold]
print('有 %i 个特征因为与目标变量TARGET的相关系数绝对值小于 %.3f而被删除' % (feat_df.shape[0],target_threshold))
print('deleting...')
for df in [train,test]:
df.drop(feat_df.index,axis=1,inplace=True)
print('已删除!')
#删除彼此之间相关性高的特征(保留一个与TARGET相关性最高的特征)
corr.sort_values(by='TARGET',ascending=False,inplace=True)#将相关矩阵每一行先按TARGET列降序排列
corr = corr.reindex(columns=corr.index)
corr.drop('TARGET',axis=1,inplace=True)#删除target列
corr.drop('TARGET',axis=0,inplace=True)
corr.drop(feat_df.index,axis=1,inplace=True)#删除feat_df中特征在corr表corr表里的列
corr.drop(feat_df.index,inplace=True)
upper = corr.where(np.triu(np.ones(corr.shape),k=1).astype(np.bool)) # 获取相关矩阵的上三角
column = [col for col in upper.columns if any(upper[col] > within_threshold)] # 获取与特征之一高度相关的所有列
print("有 %i 个特征与另一个特征高度相关且相关系数为 %.3f 及以上而被删除" % (len(column), within_threshold))
print("删除中.........")
for df in [train, test]:
df.drop(column, axis=1, inplace=True)
print("已删除!")
print("特征数从 %i 个变成 %i 个,其中 %i 个特征已被删除" %
(initial_feature, test.shape[1], initial_feature - test.shape[1]))
(具体代码分析过程详见源代码)
至此数据清洗过程完成,将train和test保存为P文件,方便后续调用
解释一下这里为什么是保存X_train,X_test,我们之前是利用copy函数对train和test进行拷贝的。但是这里的拷贝是深度拷贝,就是X-train是train的一个指针,所以我们对train作出的修改一样会同步到X_train去
# 保存为P文件,方便后续调用
X_train.to_pickle('./data/santander-customer-satisfaction/output/X_train.pkl')
X_test.to_pickle('./data/santander-customer-satisfaction/output/X_test.pkl')
# 读取上述P文件
X_train = pd.read_pickle('./data/santander-customer-satisfaction/output/X_train.pkl')
X_test = pd.read_pickle('./data/santander-customer-satisfaction/output/X_test.pkl')
X_train.shape, X_test.shape
现在我们的训练集和测试集的shape为
((76020, 338), (75818, 337))
之前我们分析过,对imp和saldo类的特征,其数据通常不均衡,除去大量的0值以外,剩余的非零值往往呈现出右偏分布 所以这里我们对这两类特征进行对数变换
def apply_log1p(column,train=X_train,test=X_test):
#对数变换列特征
tr = train.copy()
te = test.copy()
for df in [tr,te]:
for col in column:
df.loc[df[col] >= 0, col] = np.log1p(df.loc[df[col] >= 0, col].values)
return tr,te
# 对所有最小值大于等于0的imp和saldo特征进行对数变换(var38在EDA中已经对数化,这里不再操作)
features = [i for i in X_train.columns if (('saldo' in i) | ('imp' in i)) & ((X_train[i].values >= 0).all())]
X_train_1, X_test_1 = apply_log1p(features)
将对数变换后的数据集保存为X_train_l,X_test_l数据集
X_train_1.to_pickle('./data/santander-customer-satisfaction/output/X_train_1.pkl')
X_test_1.to_pickle('./data/santander-customer-satisfaction/output/X_test_1.pkl'))
对于分类问题,通常我们需要对分类型变量进行编码操作,但是唯一值过多的变量会使得数据集过于稀疏,所以我们这里设置一个阈值,唯一值大于1小于11的变量我们才进行编码。
# 选取唯一值的个数(2,10]的特征
cat_col = []
for col in X_train.columns:
if (X_train[col].nunique() <= 10) & (col != 'TARGET') & (X_train[col].nunique() > 2):
cat_col.append(col)
print("有 %i 个特征其唯一值数量(2,10]并使用它们创建独热编码和响应编码变量,同时删除原始特征" % (len(cat_col)))
有 17 个特征其唯一值数量(2,10]并使用它们创建独热编码和响应编码变量,同时删除原始特征
打印出需要编码的变量列表:
['var15',
'num_var4',
'num_var5_0',
'num_var12_0',
'num_var13_largo_0',
'num_var14_0',
'num_var25',
'num_var30',
'num_var40_0',
'num_var41_0',
'num_var42_0',
'num_var42',
'var36',
'num_aport_var13_hace3',
'num_meses_var5_ult3',
'num_meses_var8_ult3',
'num_meses_var39_vig_ult3']
接下来我们定义针对指定列进行one_hot编码的函数:
def one_hot_encoding(col,train=X_train,test=X_test):
#对训练集和测试集中的特征进行独热编码
#一般的训练过程是enc.transform(df).toarray(),就是默认返回稀疏矩阵,然后便于查看所以转换为数组的形式,但是直接sparse=False就可以直接返回数组矩阵而不需要toarray()
#默认情况下,handle_unknown = error,当遇到 transform时遇到fit中没有出现过的特征类别时,会直接报错
#get_features:展示编码后的特征名,默认有个前缀是x0,x1...例如:array(['x0_PHD', 'x0_master', 'x1_A', 'x1_B', 'x1_C'], dtype=object),但是这个x0也可以通过input_features参数指定
#duplicated函数:找出df中的重复值
ohe = OneHotEncoder(sparse=True, handle_unknown='ignore')
ohe.fit(train[col])
feature_names = list(ohe.get_feature_names(input_features=col))
features = list(train.drop(col, axis=1).columns)
features.extend(feature_names)
# train
df = train.copy()
temp = ohe.transform(df[col])
df.drop(col, axis=1, inplace=True)
train = pd.DataFrame(hstack([df.values, temp]).toarray(), columns=features)
train = train.loc[:, ~train.columns.duplicated(keep='first')] # 删除重复行
# test
df = test.copy()
temp = ohe.transform(df[col])
df.drop(col, axis=1, inplace=True)
features.remove('TARGET')
test = pd.DataFrame(hstack([df.values, temp]).toarray(), columns=features)
test = test.loc[:, ~test.columns.duplicated(keep='first')]
return train, test
将其应用于X_train_1数据集并查看这一步:
X_train_ohe, X_test_ohe = one_hot_encoding(cat_col)
X_train_1_ohe, X_test_1_ohe = one_hot_encoding(cat_col, X_train_1, X_test_1)
X_train_ohe.shape, X_test_ohe.shape, X_train_1_ohe.shape, X_test_1_ohe.shape
((76020, 426), (75818, 425), (76020, 426), (75818, 425))
响应编码是一种对分类数据进行矢量化的技术。假设我们有一个名为“grade_category”的分类特征,它具有以下唯一标签 - [‘grades_3_5’,‘grades_prek_2’,‘grades_9_12’,‘grades_6_8’]。 假设我们正在处理目标类标签为 0 和 1 的分类问题
在响应编码中,您必须为我们特征中的每个标签输出概率值,该标签出现在特定的类标签中 例如,grades_prek_2 = [发生在 class_0 的概率,发生在 class 1 的概率]
响应编码的介绍
def response_encoding_return(df, column, target, alpha=5000):
"""
使用带有拉普拉斯平滑的响应编码到分类列column,并在训练、测试、验证数据集中转换相应的列。
此函数用来训练出最优的参数alpha
"""
unique_values = set(df[column].values)#求得所有唯一值
dict_values = {}
for value in unique_values:
total = len(df[df[column] == value])
sum_promoted = len(df[(df[column] == value) & df[target] == 1])
dict_values[value] = np.round((sum_promoted + alpha) / (total + alpha * len(unique_values)), 2)
return dict_values
# 寻找最好的alpha
def find_alpha(seed):
random.seed(seed)
ran_in = random.randint(0, 10) # 随机生成0-9的整数
col = [col for col in cat_col if X_train[col].nunique() > 3][ran_in]
print('Feature: "%s"' % (col))
for alpha in [100, 500, 1000, 2500, 5000, 10000]:
print('for alpha %i:%s' % (alpha, response_encoding_return(X_train, col, "TARGET", alpha=alpha)))
find_alpha(seed=100)
我们分别在两个种子下寻找最优alpha:
== 查看以上两个特征,发现最好的 alpha 为 alpha=100,因为每个类别对此 alpha 具有最高的不同响应编码值, 随着 alpha 的增加,所有唯一值的响应编码值变得相似==
接下来定义响应编码函数:
def response_encoding(df,test_df,column,target='TARGET',alpha=5000):
"""
在这里,我们使用带有拉普拉斯平滑的响应编码到分类列,并在训练、测试、验证数据集中转换相应的列。
在这里,我们将重复每个类别的值 alpha 时间。
"""
feature = column + '_1'
feature_ = column + '_0'
unique_values = set(df[column].values)
dict_values = {} #存储target=1的响应编码值
dict_values_ = {} #存储target=0的响应编码值
for value in unique_values:
total = len(df[df[column] == value])#此类别值在df中的个数
#类别为某‘value’且目标变量为1时在df中的总个数
sum_promoted = len(df[(df[column] == value) & (df[target] == 1)])
sum_unpromoted = total - sum_promoted# 类别为某'vale'值且目标变量取0时在df中的总个数
dict_values[value] = np.round((sum_promoted + alpha) / (total + alpha*len(unique_values)),2)
dict_values[value] = np.round((sum_unpromoted + alpha) / (total + alpha*len(unique_values)),2)
#假定了在某个字段中训练集出现的取值不完整,有些只在测试集中出现,这就是未知类别
dict_values['unknown'] = 0.5#在训练集上观测不到的未知类别将被分配为0.5
dict_values_['unknown'] = 0.5
df[feature] = (df[column].map(dict_values)).values
df[feature_] = (df[column].map(dict_values_)).values
# print('dict_values: ')
# print(dict_values)
# print('dict_values_: ')
# print(dict_values_)
df.drop(column, axis=1, inplace=True)
unique_values_test = set(test_df[column])
#找出亮哥set中的不同元素并赋值为unknown
test_df[column] = test_df[column].apply(lambda x: 'unknown' if x in (unique_values_test-unique_values) else x)
test_df[feature] = (test_df[column].map(dict_values)).values
test_df[feature_] = (test_df[column].map(dict_values_)).values
test_df.drop(column, axis=1, inplace=True)
对特征进行响应编码:
alpha = 100
X_train_re = X_train.copy()
X_test_re = X_test.copy()
X_train_1_re = X_train_1.copy()
X_test_1_re = X_test_1.copy()
for col in tqdm(cat_col):
response_encoding(X_train_re, X_test_re, col, alpha=alpha)
response_encoding(X_train_1_re, X_test_1_re, col, alpha=alpha)
X_train_re.shape, X_test_re.shape, X_train_1_re.shape, X_test_1_re.shape
此时的shape为:
((76020, 354), (75818, 353), (76020, 354), (75818, 353))
def stdzation(train,test):
col = [i for i in train.columns if (i != 'TARGET') & (i != 'ID')]
scaler = StandardScaler()
train[col] = scaler.fit_transform(train[col])
test[col] = scaler.transform(test[col])
datasets = [(X_train, X_test), (X_train_re, X_test_re), (X_train_ohe, X_test_ohe),
(X_train_1, X_test_1), (X_train_1_re, X_test_1_re), (X_train_1_ohe, X_test_1_ohe)]
for train, test in datasets:
stdzation(train, test)
dataset之所以有很多种数据集组合是因为,上面的过程对数据集进行了几种操作:1、对分类变量进行响应编码2、对分类变量进行one-hot编码3、对imp和saldo关键字特征进行对数变换4、对数变换和响应编码的组合5、对数变换和独热编码的组合
保存最终特征工程后的数据集
normal: Normal normal_re: Normal with Response encoding normal_ohe: Normal with One Hot Encoding
log: Log Transformed log_re: Log Transformed with Response encoding log_ohe: Log Transformed with One Hot Encoding
我们查看一下各数据集的特征数:
datasets_labels = ["normal", 'normal_re',
"normal_ohe", "log", 'log_re', "log_ohe"]
print("不同数据集最终的特征数是:")
for i, (train, test) in enumerate(datasets):
print("%s:\t%i" % (datasets_labels[i], test.shape[1]))
不同数据集最终的特征数是:
normal: 337
normal_re: 353
normal_ohe: 425
log: 337
log_re: 353
log_ohe: 425
至此,模型训练前所有的准备工作均已完成,下一节我们将使用不同的模型对测试集上的分类效果进行综合评估。