数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。
特征工程是利用数据领域的相关知识来创建能够使机器学习算法达到最佳性能的特征的过程。
特征工程流程:这些过程不是必须全部要有,需要根据业务需求和数据格式特点,适宜调整!
目录
特征工程
1. 数据理解EDA
1.1 数据简略观测
1.2 数据统计
1.3 数据正态性检验
1.4 绘图
2. 特征清洗
2.1 特征分类不平衡
2.2 缺失值处理
2.3 异常值处理
2.4 数据转换
2.5 数据分桶
2.6 一人多次
3. 特征构造(特征生成)
4. 特征选择
4.1 Filter(过滤式),单变量特征选择
4.2 Wrapper(包装法)
4.3 Embedded(嵌入法)
5. 特征降维
5.1 PCA
5.2 LDA
6. 直接预测
这一步最重要的是形成分类变量名列表和连续变量名列表。这样做的好处:
1)方便查看分类变量数据分布。分类变量正负样本比例全是1或95%是1的,没意义,可以删去;连续变量缺失率大于50%和数值分布范围
2)方便后面的相关性检测。分类变量用卡方检验;连续变量用t检验或方差分析。
print('----------------全体变量数据统计描述----------------------')
# 统计全变量体系各变量的平均数、上下四分位数、缺失率
feature_list=[]
mean_list=[]
up_quarter_list=[]
down_quarter_list=[]
miss_list=[]
for i in df_model.columns:
data = df_model[i]
stat_result = pd.DataFrame(data.describe())
# print(stat_result)
mean_value=stat_result.loc['mean',i]
up_quarter=stat_result.loc['25%',i]
down_quarter=stat_result.loc['75%',i]
num=stat_result.loc['count',i]
miss_rate=1-num/df_model.shape[0]
miss_rate="%.2f%%" % (miss_rate * 100) # 百分数输出
feature_list.append(i)
mean_list.append(round(mean_value,2))
up_quarter_list.append(round(up_quarter,2))
down_quarter_list.append(round(down_quarter,2))
miss_list.append(miss_rate)
df_stat=pd.DataFrame({'特征':feature_list,'平均值':mean_list,'上四分位':up_quarter_list,'下四分位':down_quarter_list,'缺失率':miss_list})
df_stat=df_stat.reset_index(drop=True)
writer=pd.ExcelWriter(project_path+'/data/v2.0/df_全体变量数据统计.xlsx')
df_stat.to_excel(writer)
writer.save()
数据正态性检验,是为了方便相关性分析和显著性分析。当样本量巨大时,可以近似认为数据符合正态分布,不用做正态性检验。
分类变量正负样本分类不平衡,少类别提供信息太少,没有学会如何判别少数类。
过采样是针对minority样本,欠采样是针对majority样本;而综合采样是既对minority样本,又对majority样本,同时进行操作的方法
例:
原始数据(Original):未经过任何采样处理(1831X21)每条数据有21个特征。其中正例176个(9.6122%),反例1655个(90.3878%)
欠采样(Undersampling):从反例中随机选择176个数据,与正例合并(352X21)
过采样(Oversampling):从正例中反复抽取并生成1655个数据(势必会重复),并与反例合并(3310X21)
SMOTE:也是一种过采样方法。SMOTE通过找到正例中数据的近邻,来合成新的1655-176=1479个“新正例”,并与原始数据合并(3310X21)
欠采样
from imblearn.under_sampling import TomekLinks
X_train = train_df.drop(['id', 'type'], axis=1)
y = train_df['label']
tl = TomekLinks()
X_us, y_us = tl.fit_sample(X_train, y)
print(X_us.groupby(['label']).size())
# label
# 0 36069
# 1 2757
SMOTE
from imblearn.over_sampling import SMOTE
smote = SMOTE(k_neighbors=5, random_state=42)
X_res, y_res = smote.fit_resample(X_train, y)
X_res.groupby(['label']).size()
# label
# 0 37243
# 1 37243
ADASYN
from imblearn.over_sampling import ADASYN
adasyn = ADASYN(n_neighbors=5, random_state=42)
X_res, y_res = adasyn.fit_resample(X_train, y)
X_res.groupby(['label']).size()
# label
# 0 37243
# 1 36690
综合采样
from imblearn.combine import SMOTETomek
smote_tomek = SMOTETomek(random_state=0)
X_res, y_res = smote_tomek.fit_sample(X_train, y)
X_res.groupby(['label']).size()
# label
# 0 36260
# 1 36260
结果:
1)过采样(右上)只是单纯的重复了正例,因此会过分强调已有的正例。如果其中部分点标记错误或者是噪音,那么错误也容易被成倍的放大。因此最大的风险就是对正例过拟合
2)欠采样(左下)抛弃了大部分反例数据,从而弱化了中间部分反例的影响,可能会造成偏差很大的模型。当然,如果数据不平衡但两个类别基数都很大,或许影响不大。数据总是宝贵的,抛弃数据是很奢侈的,因此另一种常见的做法是反复做欠采样,生成1655/176=9
3)SMOTE(右下)可以看出和过采样(右上)有了明显的不同,因为不单纯是重复正例了,而是在局部区域通过K-近邻生成了新的正例。
# 使用随机森林对缺失值进行插补
import pandas as pd
pd.set_option('mode.chained_assignment', None)
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV
def missing_value_interpolation(df,missing_list=[]):
df = df.reset_index(drop=True)
# 提取存在缺失值的列名
if not missing_list:
for i in df.columns:
if df[i].isnull().sum() > 0:
missing_list.append(i)
missing_list_copy = missing_list.copy()
# 用该列未缺失的值训练随机森林,然后用训练好的rf预测缺失值
for i in range(len(missing_list)):
name=missing_list[0]
df_missing = df[missing_list_copy]
# 将其他列的缺失值用0表示。
missing_list.remove(name)
for j in missing_list:
df_missing[j]=df_missing[j].astype('str').apply(lambda x: 0 if x=='nan' else x)
df_missing_is = df_missing[df_missing[name].isnull()]
df_missing_not = df_missing[df_missing[name].notnull()]
y = df_missing_not[name]
x = df_missing_not.drop([name],axis=1)
# 列出参数列表
tree_grid_parameter = {'n_estimators': list((10, 50, 100, 150, 200))}
# 进行参数的搜索组合
grid = GridSearchCV(RandomForestRegressor(),param_grid=tree_grid_parameter,cv=3)
#rfr=RandomForestRegressor(random_state=0,n_estimators=100,n_jobs=-1)
#根据已有数据去拟合随机森林模型
grid.fit(x, y)
rfr = RandomForestRegressor(n_estimators=grid.best_params_['n_estimators'])
rfr.fit(x, y)
#预测缺失值
predict = rfr.predict(df_missing_is.drop([name],axis=1))
#填补缺失值
df.loc[df[name].isnull(),name] = predict
return df
# 过滤异常值,大于正常值超过100倍!
def filter_exce_value(df,feature):
# 过滤文字!!!!!!!!!!!!!!!!!!!!!!!!!!!
df=df[df[feature].str.contains('\d')]
# 过滤异常大值!!!!!!!!!!!!!!!!!!!!!!!!!!
median_value=df[feature].median()
df[feature]=df[feature].apply(lambda x: x if abs(float(x)) < (100 * abs(median_value)) else np.nan)
df=df[df[feature].notnull()]
return df
一般是用于连续变量不满足正态分布的时候
最重要的一点:如果对因变量进行数据转换,要记得对模型预测结果进行恢复!
Box-Cox变换通过对因变量进行变换,使得变换过的向量与回归自变量具有线性相依关系,误差也服从正态分布.误差各分量是等方差且相互独立。Box-Cox变换兼顾了变量在时间序列维度上的回归特性,所以也可以用于时间序列方面的预测。
from scipy.stats import boxcox
boxcox_transformed_data = boxcox(original_data)
在一些情况下(P值正态化处理,所以优先使用BOX-COX转换,但是当P值>0.003时两种方法均可,优先考虑普通的平方变换
for col in continuous_list:
df_final_10_1[col] = df_final_10_1[col].apply(lambda x: np.log(x) if x > 0 else np.nan if x!=x else 0)
minmax = MinMaxScaler()
num_data_minmax = minmax.fit_transform(num_data)
num_data_minmax = pd.DataFrame(num_data_minmax, columns=num_data.columns, index=num_data.index)
"""类别特征某些需要独热编码一下"""
hot_features = ['bodyType', 'fuelType', 'gearbox', 'notRepairedDamage']
cat_data_hot = pd.get_dummies(cat_data, columns=hot_features)
医学数据挖掘里用处不大
在特征构造的时候,需要借助一些业务知识(比如医学中的BMI、肌酐转化率),遵循的一般原则就是需要发挥想象力,尽可能多的创造特征,不用先考虑哪些特征可能好,可能不好,先弥补这个广度。
医学数据挖掘一般不需要考虑数值、类别和时间特征。
filter--主要对应单变量特征选择;wrapper--主要对应多个特征选择。
特征选择原因:对于一个特定的学习算法来说,哪一个特征是有效的是未知的。因此,需要从所有特征中选择出对于学习算法有益的相关特征。而且在实际应用中,经常会出现维度灾难问题。如果只选择所有特征中的部分特征构建模型,那么可以大大减少学习算法的运行时间,也可以增加模型的可解释性
特征选择原则:获取尽可能小的特征子集,不显著降低分类精度、不影响分类分布以及特征子集应具有稳定、适应性强等特点
filter按照发散性或相关性对各个特征进行评分,设定阈值或者待选择阈值的个数,选择特征。
优点:运行速度快,是一种非常流行的特征选择方法。
缺点:无法提供反馈,特征选择的标准/规范的制定是在特征搜索算法中完成,学习算法无法向特征搜索算法传递对特征的需求。另外,可能处理某个特征时由于任意原因表示该特征不重要,但是该特征与其他特征结合起来则可能变得很重要
https://note.youdao.com/s/9HR1GEQG
https://note.youdao.com/s/aTVlqmDy
独立样本t检验和Mann-Whitney U test
discrete_list = ['gender']
continuous_list = [x for x in df_model.columns if x not in discrete_list]
# 高低剂量组利伐沙班服药前后WBC显著性检验
from scipy.stats import kstest,shapiro
import scipy.stats as st
from scipy.stats import chi2_contingency
##检验是否正态
def norm_test(data):
if len(data) > 30:
norm, p = kstest(data, 'norm')
else:
norm, p = shapiro(data)
#print(t,p)
if p>=0.05:
return True
else:
return False
def test2(data_b, data_p):
if norm_test(data_b) and norm_test(data_p):
x = 1
y = '独立样本T检验'
t, p = st.ttest_ind(list(data_b),list(data_p), nan_policy='omit')
else:
x = 0
y = 'Mann-Whitney U检验'
t,p = st.mannwhitneyu(list(data_b),list(data_p))
return x,y,t,p
def sig_test(df_high,df_low,list):
field_list=[]
y_list=[]
t_list=[]
p_list=[]
result_list=[]
high_mean_list=[]
low_mean_list=[]
# high_num_list=[]
# high_rate_list=[]
# low_num_list=[]
# low_rate_list=[]
for i in range(len(list)):
field=list[i]
df_high_nt=df_high[df_high[field].notnull()]
data_high=df_high_nt[field]
high_mean=round(data_high.mean(),2)
# high_num=df_high_nt.shape[0]
# all_num=df_high.shape[0] + df_low.shape[0]
# high_rate = "%.2f%%" % (round(high_num/all_num) * 100)
df_low_nt=df_low[df_low[field].notnull()]
data_low=df_low_nt[field]
low_mean=round(data_low.mean(),2)
# low_num=df_low_nt.shape[0]
# low_rate="%.2f%%" % (round(low_num/all_num) * 100)
if data_high.shape[0] >= 10 and data_low.shape[0]>=10:
x,y,t,p = test2(data_high, data_low)
if p <=0.05:
sig='显著'
else:
sig='不显著'
field_list.append(field)
y_list.append(y)
t_list.append(t)
p_list.append(p)
result_list.append(sig)
high_mean_list.append(high_mean)
low_mean_list.append(low_mean)
df_result=pd.DataFrame({'特征':field_list,
'高剂量均值':high_mean_list,
'低剂量均值':low_mean_list,
'检验指标':y_list,
't值':t_list,
'p值':p_list,
'显著性结果':result_list})
return df_result
# 住院时长到用药时长的显著性检验
df_inp_time=sig_test(df_lfsb_high,df_lfsb_low,['住院时长'])
df_inp_time=df_inp_time.reset_index(drop=True)
writer=pd.ExcelWriter(project_path+r'/data/result/df_高低剂量组住院时长显著性检验.xlsx')
df_inp_time.to_excel(writer)
writer.save()
卡方检验
## 卡方检验
print('----------------------卡方检验-------------------------')
from scipy.stats import chi2_contingency
r1 = []
r2 = []
tran_test['MPA类药物'] = tran_test['MPA类药物'].astype('str')
for i in range(len(np.unique(tran_test['MPA类药物']))):
r1.append(
tran_test[(tran_test['group'] == 0) & (tran_test['MPA类药物'] == np.unique(tran_test['MPA类药物'])[i])].shape[0])
r2.append(
tran_test[(tran_test['group'] == 1) & (tran_test['MPA类药物'] == np.unique(tran_test['MPA类药物'])[i])].shape[0])
abcd = np.array([r1, r2])
print(abcd)
result = chi2_contingency(abcd)
print(result)
tran_x_1 = tran_x_1.drop(['group'], axis=1)
test_x_1 = test_x_1.drop(['group'], axis=1)
print(tran_x_1.columns)
pearsonr检验
from scipy import stats
r, p = stats.pearsonr(x,y)
spearmanr检验
# 连续变量,spearmanr相关性检验(统计量r);
print('--------------------------计算连续变量的spearmanr相关性系数---------------------------------')
from scipy import stats
t_list = []
p_list = []
q_list = []
for i in continuous_list:
# 删除连续变量中的<、>号
tdm_7_other_filter[i] = tdm_7_other_filter[i].astype('str').apply(lambda x: re.sub(r'<|>', '',x))
x= tdm_7_other_filter[tdm_7_other_filter[i].astype('float').notnull()][i]
y= tdm_7_other_filter[tdm_7_other_filter[i].astype('float').notnull()]['test_result']
t, p = stats.spearmanr(x,y)
t = round(t, 2)
p = round(p, 3)
q = '斯皮尔曼'
# print(i, t, p)
t_list.append(t)
p_list.append(p)
q_list.append(q)
df_spearmanr= pd.DataFrame(data={'连续检测指标': continuous_list,
't值': t_list,
'p值': p_list,
'方法': q_list})
df_spearmanr_1 = df_spearmanr[df_spearmanr['p值'] <= 0.05]
df_spearmanr_2 = df_spearmanr[df_spearmanr['p值'] >= 0.05] # 显著性不成立
df_spearmanr = pd.concat([df_spearmanr_1,df_spearmanr_2], axis=0)
df_spearmanr=df_spearmanr.sort_values(by=['p值'],ascending=True)
df_spearmanr = df_spearmanr.reset_index()
del df_spearmanr['index']
writer = pd.ExcelWriter(project_path + '/result/df_12_其他检测指标连续变量的spearmanr相关性检测.xlsx')
df_spearmanr.to_excel(writer)
writer.save()
根据目标函数(通常是预测效果评分)作为评价函数,每次选择若干特征,排除若干特征。
主要方法:递归特征消除算法。
优点:对特征进行搜索时围绕学习算法展开的,对特征选择的标准/规范是在学习算法的需求中展开的,能够考虑学习算法所属的任意学习偏差,从而确定最佳子特征,真正关注的是学习问题本身。由于每次尝试针对特定子集时必须运行学习算法,所以能够关注到学习算法的学习偏差/归纳偏差,因此封装能够发挥巨大的作用。
缺点:运行速度远慢于过滤算法,实际应用用封装方法没有过滤方法流行。
# 判断文件路径是否存在,如果不存在则创建该路径
def mkdir(path):
folder = os.path.exists(path)
if not folder: # 判断是否存在文件夹如果不存在则创建为文件夹
os.makedirs(path) # makedirs 创建文件时如果路径不存在会创建这个路径
df = pd.read_excel(project_path+'/data/v2.0/建模用数据集(未插补)20210525-3.xlsx')
if 'Unnamed: 0' in df.columns:
df = df.drop(['Unnamed: 0'], axis=1)
continuous_list = [
'年龄', '身高(cm)', '体重(kg)', 'BMI', '他克莫司频次', '他克莫司单次剂量', '他克莫司日剂量',
'C反应蛋白_检测结果', '丙氨酸氨基转移酶_检测结果', '中性粒细胞总数_检测结果', '低密度脂蛋白胆固醇_检测结果',
'凝血酶原时间比率_检测结果', '天门冬氨酸氨基转移酶_检测结果', '尿素_检测结果', '尿酸_检测结果',
'平均RBC血红蛋白浓度_检测结果', '平均红细胞体积_检测结果', '平均红细胞血红蛋白量_检测结果', '平均血小板容积_检测结果',
'总胆固醇_检测结果', '总胆红素_检测结果', '总蛋白_检测结果', '极低密度脂蛋白胆固醇_检测结果', '活化部分凝血活酶时间_检测结果',
'淋巴细胞总数_检测结果', '球蛋白_检测结果', '甘油三酯_检测结果', '白/球比值_检测结果', '白细胞计数_检测结果',
'白蛋白_检测结果', '直接胆红素_检测结果', '红细胞比积测定_检测结果', '肌酐_检测结果', '葡萄糖_检测结果',
'血小板计数_检测结果', '血浆D-二聚体测定_检测结果', '血红蛋白测定_检测结果', '转氨酶比值_检测结果', '间接胆红素_检测结果',
'非高密度脂蛋白胆固醇_检测结果', '高密度脂蛋白胆固醇_检测结果', '乳酸脱氢酶_检测结果', '心型肌酸激酶_检测结果',
'肌酸激酶_检测结果', '尿白细胞(仪器定量)_检测结果', '尿红细胞(仪器定量)_检测结果', 'TDM检测结果'
]
#连续变量取log
df_final_10_1 = df.copy()
#df_final_11_1 = df_final_11.copy()
for col in continuous_list:
df_final_10_1[col] = df_final_10_1[col].apply(lambda x: np.log(x) if x > 0 else np.nan if x!=x else 0)
def model_xy(model):
x = model[model.columns[2:-1]]
y = model['TDM检测结果']
return x, y
col=['身高(cm)', '他克莫司日剂量', '其他免疫抑制剂', '低密度脂蛋白胆固醇_检测结果', '平均红细胞体积_检测结果', '平均红细胞血红蛋白量_检测结果',
'白细胞计数_检测结果', '直接胆红素_检测结果', '红细胞比积测定_检测结果']
df_model_4 = df_final_10_1.copy()
x4, y4 = model_xy(df_model_4)
all_all_results = []
for j in range(1,52):
for xy in [[x4, y4]]:
train_x, test_x, train_y, test_y = train_test_split(xy[0],xy[1],test_size=0.2,random_state=78)
# 津源xgboost模型
sfs = SFS(xgb.XGBRegressor(max_depth=5,
learning_rate=0.01,
n_estimators=500,
min_child_weight=0.5,
eta=0.1,
gamma=0.5,
reg_lambda=10,
subsample=0.5,
colsample_bytree=0.8,
nthread=4,
scale_pos_weight=1),
k_features=j,
forward=True,
floating=False,
verbose=2,
scoring='r2',
cv=3)
sfs = sfs.fit(train_x, train_y)
# 逐步向前筛选结果,包括特征个数,最优特征组合及其r2
sfs_result = sfs.subsets_
print(sfs_result)
df_sfs = pd.DataFrame(sfs_result)
# DataFrame转置
df_sfs_T=pd.DataFrame(df_sfs.values.T,index=df_sfs.columns,columns=df_sfs.index)
df_sfs_T=df_sfs_T.reset_index(drop=True)
# 保存逐步向前筛选结果
r2_list=list(df_sfs_T['avg_score'])
feature_list=list(df_sfs_T['feature_names'])
# 根据逐步向前测试结果筛选最优特征组合
r2_max=max(r2_list)
print(r2_max)
r2_max_index=r2_list.index(r2_max)
df_feature_select=df_sfs_T.iloc[r2_max_index:r2_max_index+1,:]
all_all_results.append(df_feature_select)
df_feature_select=all_all_results[0]
for j in range(1,len(all_all_results)):
df_feature_select=pd.concat([df_feature_select,all_all_results[j]],axis=0)
df_feature_select=df_feature_select.reset_index(drop=True)
# 保存模型测试和测试结果到本地文件
writer = pd.ExcelWriter(project_path + '/data/v2.0/df_逐步向前特征测试结果.xlsx')
df_feature_select.to_excel(writer)
writer.save()
用model进行训练,得到各个特征的权值系数,根据系数从大到小选择特征。
特征选择完成后,还能基于特征选择完成的特征和模型训练出的超参数,再次训练优化。
主要思想:在模型既定的情况下学习出对提高模型准确性最好的特征。也就是在确定模型的过程中,挑选出那些对模型的训练有重要意义的特征。
# 重要性评分
import catboost,xgboost
model_boost=xgboost.XGBRegressor()
model_boost.fit(tran_x,tran_y)
importance = model_boost.feature_importances_
print(tran_x.columns)
print(importance)
df_importance= pd.DataFrame(data={'特征':tran_x.columns,'重要性评分':importance})
df_importance['重要性评分']=df_importance['重要性评分'].apply(lambda x: round(x,3))
df_importance=df_importance.sort_values(['重要性评分'],ascending=False)
df_importance=df_importance.reset_index(drop=True)
writer = pd.ExcelWriter(project_path + '/result/df_19_模型重要性评分.xlsx')
df_importance.to_excel(writer)
writer.save()
L1正则化将系数w的l1范数作为惩罚项加到损失函数上。Lasso能够挑出一些优质特征,同时让其他特征的系数趋于0。当如需要减少特征数的时候它很有用,但是对于数据理解来说不是很好用。
L2正则化对于特征选择来说一种稳定的模型,不像L1正则化那样,系数会因为细微的数据变化而波动。所以L2正则化和L1正则化提供的价值是不同的,L2正则化对于特征理解来说更加有用:表示能力强的特征对应的系数是非零。
boosting体系用于基因分析挖掘
SVM体系(kernel函数进行更改),适用于缺失值和异常值存在的情况
DeepLearning,aml-net,tabnet,TabTransformer