目录:
思路概述
1,数据探索
2,特征工程
3,机器学习模型的初步预测和特征工程
4,变量衍生和PCA降维
5,多模型融合预测
赛题介绍详见天池官网:UNiLAB智慧能源系统大数据分析赛 赛道2:不完整电压数据的电力系统稳定性评估,数据注册登录后可下载
思路概述
算法大赛赛题,数据量不大,训练数据为2500*3900,预测数据2536*1950,数据类型均为浮点数,无空缺值,数据友好。拿到数据后,反复阅读赛题,对赛题的理解,可以少走一些弯路。既然都是浮点数,在开赛阶段直接用现有数据套个模型提交试水,果然,效果感人。。。
后转换思路,从分位值和具体数据大小方面“构造”特征进行后续的分析。在特征工程方面,主要从特征和标签(下文的label)的相关性重要性、特征与特征之间的相关性、特征衍生和降维三方面展开。反复、多次修改(美其名曰:迭代),改到后来终于把代码变成了一座si山,连命名都乱了。。。
模型选用方面,一开始是非常看好逻辑回归模型的,但从实际结果看,单一的弱模型 和 “弱+强模型+模型融合”的半监督学习模型,差距还是挺大的。最终的预测模型共两层:第一层使用knn、逻辑回归、随机森林,将拟合结果作为新的“特征”并入原始数据集,作为第二层模型的特征集(类似stacking融合,但此处没有对训练集进行切分);第二层为LightGB模型,拟合出最终的提交结果
1,数据探索
导入需要用到的包、
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split,GridSearchCV,cross_val_score
from sklearn.preprocessing import MinMaxScaler,StandardScaler
from sklearn.metrics import accuracy_score,mean_squared_error
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
import xgboost as xgb
from lightgbm import LGBMClassifier
from scipy import stats
from sklearn.linear_model import RidgeClassifier
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.decomposition import PCA
读入训练数据,并查看基本信息:
同时,可查看数据的最大值、最小值、空缺值等。查看可得: 数据共2500行、3900列,均为浮点数,无空缺值,columns为1-3900;
读取并查看预测集数据可知:预测集和训练集数据类似,均为浮点数,无空缺值,只是列数少一半,行数多36行,为2536*1950的数据。此处不再放代码
读取并查看训练集的label数据:
可见:该赛题为二分类问题,样本分布均匀。
2,特征工程
由数据探索可知,训练数据上了3900个特征,随机抽取两个特征进行比较,两者之间未发现明显的区别(从赛题中可以了解到,这些特征都是电压数据,保持在0-1.3波动,所有特征为同一类型的工业数据),考虑到特征过多,此处没有直接将这些特征入模,而是“构造”新的特征来进行探索分析。
新构造出的特征分三类,第一类是超过某个值的数量比例如电压大于0.6出现的次数比例,第二类是分位指标如0.1分位、0.25分位、0.75分位等,第三类是总体指标如极差、方差、峰度等。构造特征的函数如下:
#数量特征构造,大于某个阈值的电压出现的次数
def n_feature(df):
w = df.shape[1]
a = pd.DataFrame( (df <0.50).sum(axis=1)/w,columns=["_05"])
a1 = pd.DataFrame( (df <0.60).sum(axis=1)/w,columns=["_06"])
a2 = pd.DataFrame( (df <0.70).sum(axis=1)/w,columns=["_07"])
a3 = pd.DataFrame( (df <0.80).sum(axis=1)/w,columns=["_08"])
a4 = pd.DataFrame( (df <0.90).sum(axis=1)/w,columns=["_09"])
a2_a = pd.DataFrame( a2.values-a.values,columns=["n_1"])
a3_a1 = pd.DataFrame( a3.values-a1.values,columns=["n_2"])
n_feature = pd.concat([a,a1,a2,a3,a4,a2_a,a3_a1],axis=1)
return(n_feature)
#分位数据和总体数据
def q_feature(df):
df['f_min'] =df.min(axis=1)
df['f_quan015'] = df.quantile(q = 0.0015 , axis=1)
df['f_quan03'] = df.quantile(q = 0.003 , axis=1)
df['f_quan061'] = df.quantile(q = 0.0061 , axis=1)
df['f_quan12'] = df.quantile(q = 0.012 , axis=1)
df['f_quan1'] = df.quantile(q = 0.02 , axis=1)
df['f_quan2'] = df.quantile(q = 0.04 , axis=1)
df['f_quan22']= df.quantile(q = 0.06, axis=1)
df['f_quan23']= df.quantile(q = 0.08 , axis=1)
df['f_quan3']= df.quantile(q = 0.1 , axis=1)
df['f_quan4']= df.quantile(q = 0.25 , axis=1)
df['f_quan5']= df.quantile(q = 0.50 , axis=1)
df['f_quan6']= df.quantile(q = 0.75 , axis=1)
df['f_quan7']= df.quantile(q = 0.90 , axis=1)
df['f_quan8']= df.quantile(q = 0.95 , axis=1)
df['r015'] =df['f_quan1'] - df['f_quan061']
df['r015_2'] =df['f_quan3'] - df['f_quan061']
df['r03'] =df['f_quan4'] - df['f_quan061']
df['r015_3'] =df['f_quan3'] - df['f_quan1']
df['r03_2'] =df['f_quan5'] - df['f_quan061']
df['r03_3'] =df['f_quan1'] - df['f_quan03']
df['r03_4'] =df['f_quan3'] - df['f_quan03']
df['r1'] =df['f_quan6'] - df['f_quan1']
df['r2'] =df['f_quan6'] - df['f_quan2']
df['r23'] =df['f_quan6'] - df['f_quan23']
df['r3'] =df['f_quan6'] - df['f_quan3']
df['r4'] =df['f_quan6'] - df['f_quan4']
df['f_max'] =df.max(axis=1)
df['f_r'] = df['f_max'] - df['f_min']
df['f_mean']= df.mean(axis =1)
df['f_var'] = df.var( axis=1)
df['f_kurt'] = df.kurt(axis=1)
return(df)
好像。。命名不是很规范哈。。结合下面的步骤和3的步骤,此处经历了N次修改,有些凌乱。。。【手动抹泪】
调用函数,生成特征,并将label拼接入训练数据:
n_feature_ = n_feature(df_train)
df_train = q_feature(df_train)
df_train = pd.concat([df_train,n_feature_
#,nf
,lab
],axis=1)
df_train.head()
同时,此处可在预测集上调用函数,完成预测集特征的构造
构造好特征后,查看特征之间的关系和特征与label之间的关系。由于此处经历多次调整、修改,故:
1,此处仅将筛选好的、最终用于建模的特征进行展示。
#最后筛选出用于建模的17个特征如下:
col_ys =['f_min','f_quan061', 'f_quan1','f_quan2',
'f_quan3','f_quan4','f_quan5', 'f_quan6',
'f_quan7','f_quan8','f_max','f_mean',
'f_quan22','f_quan23', '_07','_08',
'_09']
df_ys = df_d[col_ys]#选择该17个变量的数据
df_ys = df_ys.reset_index(drop=True)#重置索引
df_ys
2,在步骤3用模型进行初步预测时,发现 f_min 这个特征与label相关性巨大:当 f_min >0.65时,label均为1,当f_min <0.3时,label均为-1,因此,在接下来的建模中,f_min特征直接当做“规则”先对数据进行处理,剩余的f_min在0.3-0.65之间的数据使用机器学习模型进行预测。
#f_min大于0.65时,label为1,f_min小于0.30时,label为-1;
def min_select(df):
df_d = df.loc[df["f_min"]<0.65]
df_d = df_d.loc[df["f_min"]>0.30]
#df_d = df_d.iloc[:,df_d.columns != "f_min"]
#df = df.drop(["f_min"],axis=1)
return(df_d)
df_d = min_select(df_train)
df_d.shape[0], df_d["label"].value_counts()
使用f_min“规则”后,2500行的数据剩余517行,label分布存在不均衡问题,在接下的建模中需要注意
查看特征分布:
column = df_ys.columns
fig = plt.figure(figsize=(80,60),dpi=50)
for i in range(35) :
plt.subplot(7,4,i+1)
sns.boxplot(df_ys[column[i]],orient="v",width=0.5 )
plt.ylabel(column[i],fontsize=36)
plt.show()
部分结果:
查看训练集特征和测试机特征的分布情况。如某特征差异比较大,会影响模型在预测集上的表现,应考虑删除。
dist_cols =6
dist_rows = len(test1.columns )
plt.figure(figsize=(4*dist_cols,4*dist_rows ),dpi=100)
i =1
for col in test1.columns:
ax = plt.subplot(dist_rows,dist_cols,i )
ax = sns.kdeplot(df_ys[col],color="red",shade=True )
ax = sns.kdeplot(test1[col],color="blue",shade=True )
ax.set_xlabel(col)
ax.set_ylabel("different")
ax = ax.legend(["df_ys","test1"])
i += 1
plt.show
查看特征之间共线性:
train = df_ys.copy()
scaler = MinMaxScaler() #vif之前先对数据进行去量纲处理
scaler.fit(train)
train_mms = scaler.transform(train)
train_mms = pd.DataFrame(train_mms,columns=train.columns )
vif_list = [variance_inflation_factor(x,i) for i in range(x.shape[1]) ]
vif_list
特征之间的多重共线性还是比较严重的,尝试删除部分特征后进行预测(此处的特征已经是从最初的40个删除为17),但预测结果直接打脸,还不如保留这些特征。。。
查看特征和label分布关系:
#此处df1为df_ys合并label后的数据,倒腾了太多次。。嗯额嗯。。一开始命名还遵守驼峰规则,迭代次数多了就乱了啊哈哈哈
plt.scatter(df1.f_min, df1.f_quan4, #此处可任意更换特征名称查看
c=df1.label,
cmap='rainbow')
查看特征和label之间的线性关系强弱:
pd.set_option('display.max_columns',10)
pd.set_option('display.max_rows',10)
df1 = df_ys
df1["label"]=lab
df_ys_corr = df1.corr()
df_ys_corr
#此处没有画热力图,直接排序哈
df_ys_corr["label"].sort_values(ascending=False )
特征与label之间的非线性关系可通过树模型进行查看
3,机器学习模型的初步预测和特征工程
数据划分:
train = df1.iloc[:,df1.columns != "label"]
lab = df1.iloc[:,df1.columns == "label"]
xtrain,xtest,ytrain,ytest=train_test_split(train,lab,test_size=0.3,random_state=90)
从步骤1可知,特征均为float数据,无任何分类或排序特征,故首先选用逻辑回归模型对数据进行初步的建模并查看相关系数:
sta = StandardScaler()
sta =sta.fit(xtrain)
xtrain_sta = sta.transform(xtrain)
xtest_sta = sta.transform(xtest)
train_sta = sta.transform(train)
lr = LogisticRegression( C= 5,
penalty = 'l2',
class_weight ={-1:2,1:1}
)
lr = lr.fit(train_sta,lab)
lr.score(train_sta,lab),
cross_val_score(lr,train_sta,lab,cv=4), lr.coef_
可用pd.concat将lr.coef_和columns拼接查看,更直观。此处可以结合步骤2,对特征进行筛选
使用随机森林进行预测并查看特征重要性:
rdf = RandomForestClassifier(max_depth= 3
,criterion= "entropy"
,n_estimators=100)
rdf.fit(train,lab)
#rdf.score(xtrain,ytrain), rdf.score(xtest,ytest),
cross_val_score(rdf,train,lab,cv=4), rdf.feature_importances_
使用KNN模型进行预测:
scaler = MinMaxScaler()
scaler.fit(xtrain)
train_mms = scaler.transform(train)
xtrain_mms = scaler.transform(xtrain)
xtest_mms = scaler.transform(xtest)
knn_mms = KNeighborsClassifier(n_neighbors=5
,weights = 'distance'
)
knn_mms.fit(xtrain_mms,ytrain)
#knn_mms.score(xtest_mms,ytest),
cross_val_score(knn_mms,train_mms,lab,cv=4)
使用LightGB模型进行预测:
LGB = LGBMClassifier(objective= 'binary',
metric = 'error',
boosting_type = 'gbdt', #
#num_leaves =32 , #
max_depth =4,
learning_rate = 0.05,
n_estimators = 200,
subsample = 0.7,
colsample_bytree = 0.7,
# subsample_freq =1,
# reg_alpha =0.1,
# reg_lambda =0.1,
#silent =True,
# min_split_gain = 0.1 ,
# min_child_weight = 0.01
)
LGB = LGB.fit(train,lab)
LGB.score(train,lab), cross_val_score(LGB,train,lab,cv=4)
对LGB 的参数进行网格搜索:
#第一次先搜索一部分参数,搜索出来后当成现成参数带入模型,再搜索另一部分参数,以此类推进行参数调优。
#如果一次搜索太多参数,容易把电脑跑死。。。
LGB1 = LGBMClassifier(objective= 'binary',
metric = 'error',
boosting_type = 'gbdt', #
#num_leaves =32 , #
n_estimators = 100,
#max_depth = 1
)
p = {
'max_depth' :[2,3,4,5],
'learning_rate' : [0.05, 0.2, 0.3, 0.4,1],
'subsample' :[0.7 ,0.8,1],
'colsample_bytree': [0.7 ,0.8,1]
}
gri = GridSearchCV(LGB1,p,cv=4)
gri.fit(train_use,lab)
gri.best_score_, gri.best_params_
除LGB 模型外,其他模型均可进行网格搜索。训练好模型后,对预测集进行预测并提交。提交后成绩:逻辑回归≈ LGB > 随机森林 > K值邻近。此时成绩还有很大提升空间
4,特征衍生和PCA降维
直接使用加减乘(数据中有为0和趋近于0的数据,故没有用除法。当然把极值和0删除后也可进行除法衍生)进行暴力衍生:
#变量衍生加法
def _add(df_d):
"""变量衍生:加法"""
feature_add = pd.DataFrame(df_d.index,columns=["_add"])
colum = df_d.columns
for ii in range(0,df_d.shape[1]-1):
df_d.iloc[:,ii]
for i in range(ii+1, df_d.shape[1]):
v =df_d.iloc[:,ii] + df_d.iloc[:,i]
a =colum[ii]
b =colum[i]
c = f'{a}_add_{b}'
feature_add[c] = v
feature_add = feature_add.iloc [:,1:]
feature_add
return(feature_add)
#变量衍生 减法
def _sub(df_d):
"""变量衍生:减法"""
feature_sub = pd.DataFrame(df_d.index,columns=["_sub"])
colum = df_d.columns
for ii in range(0,df_d.shape[1]-1):
df_d.iloc[:,ii]
for i in range(ii+1, df_d.shape[1]):
v =df_d.iloc[:,ii] - df_d.iloc[:,i]
a =colum[ii]
b =colum[i]
c = f'{a}_sub_{b}'
feature_sub[c] = v
feature_sub = feature_sub.iloc [:,1:]
feature_sub
return(feature_sub)
def _mul(df_d):
"""乘法"""
feature_mul = pd.DataFrame(df_d.index,columns=["_mul"])
colum = df_d.columns
for ii in range(0,df_d.shape[1]-1):
df_d.iloc[:,ii]
for i in range(ii+1, df_d.shape[1]):
v =df_d.iloc[:,ii] * df_d.iloc[:,i]
a =colum[ii]
b =colum[i]
c = f'{a}_mul_{b}'
feature_mul[c] = v
feature_mul = feature_mul.iloc [:,1:]
feature_mul
return(feature_mul)
#变量衍生 除法
def _div(df_d):
"""变量衍生:除法"""
feature_div = pd.DataFrame(df_d.index,columns=["_div"])
colum = df_d.columns
for ii in range(0,df_d.shape[1]-1):
df_d.iloc[:,ii]
for i in range(ii+1, df_d.shape[1]):
v =df_d.iloc[:,ii] / df_d.iloc[:,i]
a =colum[ii]
b =colum[i]
c = f'{a}_div_{b}'
feature_div[c] = v
feature_div = feature_div.iloc [:,1:]
feature_div
return(feature_div)
#变量衍生函数:加减乘除的结果输出:
def _ys(df_d):
a = _add(df_d)
b = _sub(df_d)
c = _mul(df_d)
#d = _div(df_d) #除法
feature_ys = pd.concat ([a,b,c,
#d
],axis=1)
feature_ys
return(feature_ys)
train2= _ys(train)
train2
运行结果:
可见,衍生后,新增408个特征。同样的,调用函数,对测试集数据进行特征衍生:
test2 =_ys(test1)
test2
衍生的训练集为train2,测试集为test2。
使用PCA对衍生特征进行降维,提取其中权重较大的特征,并和原来的17个特征进行合并。
#将训练集和测试集进行合并。如果是实际应用,这里好像有信息泄露的风险哈
concat_ = pd.concat([train1,test1],axis=0)
concat_
#PCA之前记得标准化
sta_ys = StandardScaler()
sta_ys =sta_ys.fit(concat_)
concat_sta = sta_ys.transform(concat_)
#test2_sta = sta_ys.transform(test2)
pca = PCA().fit(concat_sta)
#pca.explained_variance_
plt.figure(figsize=[20,5] )
plt.plot(pca.explained_variance_ratio_ )
plt.show()
运行结果:
提取权重前10的特征,并将训练集和预测集重新分开:
#提取权重前10的因子
pca = PCA(n_components=10 ).fit(concat_sta)
concat_sta_10 = pca.transform(concat_sta)
train_pca = pd.DataFrame( concat_sta_10).loc[0:516,:] #训练集
test_pca = pd.DataFrame( concat_sta_10).loc[517:1007,:] #测试集
将PCA得到的特征和原来的17个特征拼接:
#把pca数据和原始数据 拼接回去
train_use = pd.concat([train,train_pca],axis=1)
train_use
test_use = pd.concat([test1,test_pca],axis=1)
test_use
5,多模型融合预测
使用KNN、逻辑回归、随机森林作为模型第一层(可通过网格搜索对参数进行调优),对数据进行拟合,将拟合结果作为新的“特征”并入原始数据集(原始数据集为27个特征,并入后变成30个特征)形成新的数据集,再使用LGB作为模型第二层对新数据集进行拟合,形成最终的预测结果。
模型第一层:模型代码和步骤3类似,此处不再重复。参数调优后,输出dataframe格式的拟合结果,并将拟合结果和原始数据集合并。可先对单个模型进行参数调优,调优后将三个模型和数据合并的代码封装成函数,方便调用:
def model_level1(data):
"""模型第一层,以下需先对单个模型进行参数调优"""
xtrain,xtest,ytrain,ytest=train_test_split(train2,lab,test_size=0.3,random_state=90)
#随机森林
rdf = RandomForestClassifier(max_depth= 2
,criterion= "entropy"
, min_samples_leaf = 2
# ,min_impurity_decrease =0.001
,n_estimators= 100)
rdf.fit(train_use,lab)
#逻辑回归
sta = StandardScaler()
sta =sta.fit(xtrain)
xtrain_sta = sta.transform(xtrain)
train_sta = sta.transform(train)
lr = LogisticRegression( C= 1,
penalty = 'l2',
class_weight ={-1:1.2,1:1} )
lr = lr.fit(train_sta,lab)
#K值邻近
scaler = MinMaxScaler()
scaler.fit(xtrain)
train_mms = scaler.transform(train)
xtrain_mms = scaler.transform(xtrain)
xtest_mms = scaler.transform(xtest)
knn_mms = KNeighborsClassifier(n_neighbors=5
,weights = 'distance'
)
knn_mms.fit(xtrain_mms,ytrain)
#对data进行变准化和归一化,此处容易遗漏
data_sta = sta.transform(data)
data_mms = scaler.transform(data)
#将拟合结果转化成dataframe格式
a = pd.DataFrame(rdf.predict(feature_p), columns=["rdf_pr"] )
b = pd.DataFrame(lr.predict(data_sta), columns=["lr_pr"] )
c = pd.DataFrame(knn_mms.predict(data_mms),columns=["knn_pr"] )
#合并数据,形成新的数据集data_new
data_new = pd.concat([data,a,b,c],axis=1)
return(data_new)
模型第二程:使用LGBMClassifier对新数据集进行拟合,形成最终预测结果。模型代码和步骤3类似,此处不再重复。
提交结果后可知,490条记录中,预测正确480条,预测错误10条,加上f_min大于0.65和小于0.3的数据后,共2536条记录,最终结果:预测正确2526条,预测错误10条