目录
[1. 项目介绍](#1. 项目介绍)
[2. 数据清洗/数据预处理](#2. 数据清洗/数据预处理)
[3. 数据探索性分析EDA Exploratory Data Analysis](#3. 数据探索性分析EDA Exploratory Data Analysis)
[4. 特征工程](#4. 特征工程)
[5. 结论和建议](#5. 结论和建议)
1.项目介绍
1.1项目简介
1.2数分目标
1.3数据来源
1.4字段含义
1.5代码内容
1.6将会看到
1.7涉及技术
2.数据清洗/数据预处理
2.1导入数据
df = pd.read.csv(r' ')
2.2查看各列数据类型
df.shape
df.columns
df.info()
df.dtypes
2.3列名重命名
2.4删除重复值
df = df.drop_duplicates(inplace= True)
2.5缺失值处理
对连续特征使用中位数
对分类特征使用众数
print('DF SET MISSING VALUES:')
print(df.isna().sum())
2.6一致化处理
2.7异常值处理
3.数据探索性分析EDA Exploratory Data Analysis
3.1查看目标特征分布(二元特征用饼图)
# Figure size 设置图尺寸大小
plt.figure(figsize=(6,6))
###
plt.figure(): 调用Matplotlib的figure()函数来创建一个新的图形。
figsize=(6,6): 设置图形的大小为宽度和高度都是6英寸。
###
# Pie plot
train['Transported'].value_counts().plot.pie(explode=[0.1,0.1], autopct='%1.1f%%', shadow=True, textprops={'fontsize':16}).set_title("Target distribution")
###
train: 假设是一个Pandas DataFrame,其中包含了数据集。
['Transported']: 选择DataFrame中的' Transported'列,这一列应该是一个分类变量,例如表示某个事件是否发生的布尔值或分类标签。
.value_counts(): 这是一个Pandas方法,用于计算每个唯一值的出现次数。
.plot.pie(...): 调用Pandas内置的绘图方法来绘制饼图,该方法实际上是调用Matplotlib的绘图功能。
explode=[0.1,0.1]: 将饼图中的每个部分稍微分离,这里有两个部分,每个部分都分离出饼图的半径的10%。即每个部分相对于饼图中心向外移动的距离。
autopct='%1.1f%%': 用于显示每个部分的百分比,格式化为浮点数,保留一位小数。
autopct 参数用于自动添加百分比标签到饼图的每个片段上。
%1.1f%% 是一个格式化字符串,其中 %1.1f 表示一个浮点数,保留一位小数,%% 表示百分号(因为在Python字符串中,% 是一个特殊字符,所以需要用 %% 来表示一个字面的百分号)。
shadow=True: 为饼图添加阴影效果,使其更有立体感。
textprops={'fontsize':16}: 设置饼图上文本的属性,这里是将字体大小设置为16。
.set_title("Target distribution"): 为饼图设置标题,这里标题为"Target distribution",表示这是关于' Transported'列目标变量的分布。
3.2查看连续特征和目标特征的关联分布情况(直方图)
# Figure size
plt.figure(figsize=(10,4))
###
plt.figure(): 调用Matplotlib的figure()函数来创建一个新的图形。
figsize=(10,4): 设置图形的大小为宽度10英寸和高度4英寸。
###
# Histogram
sns.histplot(data=train, x='Age', hue='Transported', binwidth=1, kde=True)
###
sns: 引用Seaborn库的别名,通常在导入Seaborn时使用 import seaborn as sns。
histplot: Seaborn库中的一个函数,用于绘制直方图。
data=train: 指定绘制直方图的数据集,这里假设train是一个Pandas DataFrame。
x='Age': 指定DataFrame中要绘制直方图的列名,这里选择了'Age'列。
hue='Transported': 指定用于分组数据的列名,这里根据'Transported'列的值来分别绘制不同颜色的直方图。
binwidth=1: 设置直方图的每个柱子的宽度为1,即每个年龄区间为1年。
kde=True: 是否在直方图上叠加核密度估计(Kernel Density Estimation)曲线,设置为True表示叠加。
核密度估计(KDE):这是一种统计方法,它通过数据的密度估计来创建一个平滑的曲线,该曲线可以近似地表示数据的概率密度函数。
###
# Aesthetics
plt.title('Age distribution')
plt.xlabel('Age (years)')
###
plt.title('Age distribution'): 设置直方图的标题为"Age distribution"。
plt.xlabel('Age (years)'): 设置x轴的标签为"Age (years)",表示直方图的x轴代表年龄(以年为单位)。
###
# Expenditure features
exp_feats=['RoomService', 'FoodCourt', 'ShoppingMall', 'Spa', 'VRDeck']
###
exp_feats 是一个列表,包含了要分析的五项支出特征:房间服务、美食广场、购物中心、水疗中心和虚拟现实甲板。
###
# Plot expenditure features
fig=plt.figure(figsize=(10,20))
###
fig=plt.figure(figsize=(10,20)) 创建了一个图形对象,并设置了图形的大小为宽10英寸、高20英寸。
###
for i, var_name in enumerate(exp_feats):
# Left plot
ax=fig.add_subplot(5,2,2*i+1)
sns.histplot(data=train, x=var_name, axes=ax, bins=30, kde=False, hue='Transported')
ax.set_title(var_name)
###
在循环内部,首先创建一个子图(左图):
ax=fig.add_subplot(5,2,2*i+1) 在5行2列的网格中添加一个子图,位置是根据循环的迭代次数计算的。例如,如果i是0,那么2*i+1计算结果是1,子图将位于第一行第一列的位置;如果i是1,那么2*i+1计算结果是3,子图将位于第一行第二列的位置;如果i是2,那么2*i+1计算结果是5,子图将位于第二行第一列的位置,以此类推。
sns.histplot 使用Seaborn库的histplot函数绘制直方图,其中x参数指定了要绘制的特征,axes=ax指定了子图的位置,bins=30指定了直方图的柱子数量,kde=False表示不绘制核密度估计线,hue='Transported'表示根据'Transported'列的值对数据进行分组。
ax.set_title(var_name) 设置子图的标题为当前特征名称。
###
# Right plot (truncated)
ax=fig.add_subplot(5,2,2*i+2)
sns.histplot(data=train, x=var_name, axes=ax, bins=30, kde=True, hue='Transported')
plt.ylim([0,100])
ax.set_title(var_name)
###
接着创建另一个子图(右图),这个直方图包含核密度估计线,并且y轴的范围被限制在0到100之间:
ax=fig.add_subplot(5,2,2*i+2) 添加另一个子图,位置紧随左图。
sns.histplot 的使用与左图类似,但这里kde=True表示绘制核密度估计线。
plt.ylim([0,100]) 设置y轴的范围。
ax.set_title(var_name) 同样设置子图的标题。
###
fig.tight_layout() # Improves appearance a bit
plt.show()
###
fig.tight_layout() 调整子图参数,使得子图之间不会重叠,改善整体布局。
plt.show() 显示绘制的图形。
###
3.3查看类别特征和目标特征的关联分布情况
# Categorical features
cat_feats=['HomePlanet', 'CryoSleep', 'Destination', 'VIP']
# Plot categorical features
fig=plt.figure(figsize=(10,16))
for i, var_name in enumerate(cat_feats):
ax=fig.add_subplot(4,1,i+1)
sns.countplot(data=train, x=var_name, axes=ax, hue='Transported')
ax.set_title(var_name)
fig.tight_layout() # Improves appearance a bit
plt.show()
###
cat_feats 是一个列表,包含了要分析的四项分类特征:出发星球('HomePlanet')、低温休眠('CryoSleep')、目的地('Destination')和是否为VIP('VIP')。
fig=plt.figure(figsize=(10,16)) 创建了一个图形对象,并设置了图形的大小为宽10英寸、高16英寸。
for 循环遍历 cat_feats 列表中的每个特征。
在循环内部,首先创建一个子图:
ax=fig.add_subplot(4,1,i+1) 在4行1列的网格中添加一个子图,位置是根据循环的迭代次数计算的(从1开始)。
sns.countplot 使用Seaborn库的countplot函数绘制条形图,其中data=train指定了要分析的数据集,x=var_name指定了要绘制的特征,axes=ax指定了子图的位置,hue='Transported'表示根据'Transported'列的值对数据进行分组。
ax.set_title(var_name) 设置子图的标题为当前特征名称。
fig.tight_layout() 调整子图参数,使得子图之间不会重叠,改善整体布局。
plt.show() 显示绘制的图形。
###
3.4查看定性特征
如乘客ID、船舱编号、乘客姓名等
我们还不能绘制这些数据。我们需要将其转化为更有用的功能。
4.特征工程
即对特征进行处理以使得其能更好建模
4.1缺失值处理
4.11联合测试集和训练集
这将使填充缺失值更容易。我们以后再分开它。
# Labels and features
y=train['Transported'].copy().astype(int)
X=train.drop('Transported', axis=1).copy()
# Concatenate dataframes
data=pd.concat([X, test], axis=0).reset_index(drop=True)
###
y=train['Transported'].copy().astype(int):
train['Transported']:从名为train的数据帧中选择名为'Transported'的列,这个列通常是目标变量或标签,表示乘客是否被传送。
.copy():创建该列的一个副本,以避免后续操作对原始数据帧的更改。
.astype(int):将'Transported'列的数据类型转换为整数类型。这可能是因为'Transported'列是分类数据,通常以字符串形式存储,但在机器学习模型训练时需要将其转换为整数。
X=train.drop('Transported', axis=1).copy():
train.drop('Transported', axis=1):从train数据帧中删除'Transported'列,得到特征矩阵。axis=1表示操作是在列方向上进行的。
.copy():同样地,创建特征矩阵的一个副本。
在这两步之后,y变量包含了所有的标签(是否被传送),而X变量包含了所有的特征数据,不包括标签。
data=pd.concat([X, test], axis=0).reset_index(drop=True):
pd.concat([X, test], axis=0):使用pandas的concat函数将X(训练集的特征)和test(测试集的特征)沿着行的方向(axis=0)合并成一个大的数据帧。这意味着新的数据帧将有X和test的所有行,但列保持不变。
.reset_index(drop=True):重置合并后的数据帧的索引。drop=True参数表示丢弃旧的索引,而不是将它们添加为新列。
###
4.12初步查看缺失值总体情况
# Columns with missing values
na_cols=data.columns[data.isna().any()].tolist()
# Missing values summary
mv=pd.DataFrame(data[na_cols].isna().sum(), columns=['Number_missing'])
mv['Percentage_missing']=np.round(100*mv['Number_missing']/len(data),2)
mv
###
na_cols=data.columns[data.isna().any()].tolist():
data.isna():检查data数据帧中的每个元素是否是缺失值(NaN)。
.any():对每一列应用这个操作,如果一列中有至少一个缺失值,就返回True。
data.columns[...]:使用布尔索引从data的所有列中选择那些至少包含一个缺失值的列。
.tolist():将筛选出的列名转换成一个列表,存储在na_cols变量中。
mv=pd.DataFrame(data[na_cols].isna().sum(), columns=['Number_missing']):
data[na_cols]:选择之前找到的包含缺失值的列。
.isna():再次检查这些列中的缺失值。
.sum():对每一列应用求和操作,计算每列中缺失值的总数。
pd.DataFrame(..., columns=['Number_missing']):将得到的缺失值总数转换成一个数据帧,其中列名为'Number_missing'。
mv['Percentage_missing']=np.round(100*mv['Number_missing']/len(data),2):
100*mv['Number_missing']/len(data):计算每列缺失值的百分比,通过将每列的缺失值数量除以数据帧的总行数(len(data)),然后乘以100。
np.round(..., 2):使用numpy的round函数将计算出的百分比四舍五入到两位小数。
mv['Percentage_missing']:将计算出的百分比存储在新的列'Percentage_missing'中。
###
# Heatmap of missing values
plt.figure(figsize=(12,6))
sns.heatmap(train[na_cols].isna().T, cmap='summer')
plt.title('Heatmap of missing values')
###
plt.figure(figsize=(12,6)):
创建一个新的图形对象,并设置其大小为宽12英寸、高6英寸。
sns.heatmap(train[na_cols].isna().T, cmap='summer'):
train[na_cols]:选择之前找到的包含缺失值的列。
.isna():检查这些列中的缺失值。
.T:转置数据帧,使得每一行代表一个特征,每一列代表一个样本。这是为了在热图中正确显示数据,因为热图默认是按行进行颜色映射的。
sns.heatmap(..., cmap='summer'):使用Seaborn库的heatmap函数绘制热图。cmap='summer'指定了热图的颜色映射方案,这里使用的是'summer'。
plt.title('Heatmap of missing values'):
为热图添加标题,标题内容为'Heatmap of missing values'。
###
通过具体规律填充缺失值
5.建模
5.1数据预处理
5.1.1将数据拆分回训练集和测试集
# Train and test
X=data[data['PassengerId'].isin(train['PassengerId'].values)].copy()
X_test=data[data['PassengerId'].isin(test['PassengerId'].values)].copy()
###
X=data[data['PassengerId'].isin(train['PassengerId'].values)].copy()
data['PassengerId'].isin(train['PassengerId'].values):这是一个布尔索引操作,它检查data数据帧中的'PassengerId'列的每个值是否包含在train数据帧的'PassengerId'列的值中。
data[...]:使用布尔索引选择那些在train数据帧'PassengerId'列中存在的行。
.copy():创建筛选出的行的副本,以避免后续操作对原始数据帧的更改。
X:筛选出的数据帧被赋值给变量X,这将是用于训练模型的特征数据。
X_test=data[data['PassengerId'].isin(test['PassengerId'].values)].copy()
这行代码与上面的类似,但它检查的是data数据帧中的'PassengerId'列的值是否包含在test数据帧的'PassengerId'列的值中。
筛选出的行被赋值给变量X_test,这将是用于评估模型性能的测试特征数据。
###
5.1.2删除不需要的特征
# Drop qualitative/redundant/collinear/high cardinality features
X.drop(['PassengerId', 'Group', 'Group_size', 'Age_group', 'Cabin_number'], axis=1, inplace=True)
X_test.drop(['PassengerId', 'Group', 'Group_size', 'Age_group', 'Cabin_number'], axis=1, inplace=True)
5.1.3对被离群值严重扭曲的特征进行对数变换
在对某些数据特征进行分析和建模时,如果这些特征的分布存在偏斜,特别是存在一些非常大的离群值(即异常值),那么这种偏斜和离群值可能会对模型的性能产生不利影响。为了减少这种影响,可以对这些特征进行对数变换。
对数变换是一种数学操作,它将每个数值转换为该数值的对数。例如,如果你有一个数值x
,你可以用自然对数(以e
为底)或者常用对数(以10
为底)来变换它:
- 自然对数:
ln(x)
或np.log(x)
(在Python中) - 常用对数:
log10(x)
或np.log10(x)
(在Python中)
对数变换为什么有用?
-
减少偏斜:如果数据分布的尾部很长(即存在离群值),对数变换可以使这些长尾分布更加集中,减少偏斜。
-
压缩大数值:对数变换能够将大数值压缩到较小的范围内。例如,1000和10000之间的差距在对数变换后会变小。
-
使数据更适合某些模型:有些机器学习模型假设数据是正态分布的。对数变换可以帮助数据更接近正态分布,从而使模型表现更好。
Plot log transform results
fig=plt.figure(figsize=(12,20))
for i, col in enumerate(['RoomService','FoodCourt','ShoppingMall','Spa','VRDeck','Expenditure']):
#使用enumerate函数遍历列表中的每个元素,该列表包含列名。i是元素的索引,col是元素的值。
plt.subplot(6,2,2i+1)
#创建一个6行2列的子图网格,并选择当前列的原始数据子图位置。2i+1确保原始数据的子图位于左列。
sns.histplot(X[col], binwidth=100)
#使用seaborn库的histplot函数绘制X数据集中col列的直方图,其中binwidth=100指定直方图柱子的宽度为100。
plt.ylim([0,200])#设置y轴的范围为0到200。
plt.title(f'{col} (original)')
#为当前子图设置标题,其中{col}是列名,(original)表示这是原始数据。plt.subplot(6,2,2*i+2) sns.histplot(np.log(1+X[col]), color='C1')
#绘制对数变换后的数据直方图。np.log(1+X[col])是对原始数据加1后取对数的结果,这避免了取对数时0值的问题。color='C1'设置直方图的颜色。
plt.ylim([0,200])
plt.title(f'{col} (log-transform)')fig.tight_layout()#自动调整子图参数,以确保子图之间有适当的空间,避免标签重叠。
plt.show()Apply log transform
for col in ['RoomService','FoodCourt','ShoppingMall','Spa','VRDeck','Expenditure']:
X[col]=np.log(1+X[col])
X_test[col]=np.log(1+X_test[col])
5.1.4编码和缩放
# Indentify numerical and categorical columns
numerical_cols = [cname for cname in X.columns if X[cname].dtype in ['int64', 'float64']]
###
使用列表推导式遍历数据集X的列名。
对于每个列名cname,检查该列的数据类型是否为整数('int64')或浮点数('float64')。
如果数据类型为整数或浮点数,则将该列名添加到numerical_cols列表中。
###
categorical_cols = [cname for cname in X.columns if X[cname].dtype == "object"]
###
使用列表推导式遍历数据集X的列名。
对于每个列名cname,检查该列的数据类型是否为对象类型(通常是字符串或分类数据)。
如果数据类型为对象类型,则将该列名添加到categorical_cols列表中。
###
# Scale numerical data to have mean=0 and variance=1
numerical_transformer = Pipeline(steps=[('scaler', StandardScaler())])
###
创建一个Pipeline对象,用于处理数值列。
Pipeline是一个流水线,可以串联多个转换器。
在这个流水线中,我们使用StandardScaler转换器,它会将数据缩放到均值为0,方差为1。
###
# One-hot encode categorical data
categorical_transformer = Pipeline(steps=[('onehot', OneHotEncoder(drop='if_binary', handle_unknown='ignore',sparse=False))])
###
创建另一个Pipeline对象,用于处理分类列。
OneHotEncoder会将分类数据转换为独热编码(One-Hot Encoded)的数据。
drop='if_binary'参数表示如果列中只有两种值,那么在转换时会删除一个值以避免稀疏性。
handle_unknown='ignore'参数表示如果分类列中有未知值,那么这些值在转换后会被忽略。
sparse=False参数表示输出不会是稀疏矩阵,而是普通的NumPy数组。
###
# Combine preprocessing
ct = ColumnTransformer(
transformers=[
('num', numerical_transformer, numerical_cols),
('cat', categorical_transformer, categorical_cols)],
remainder='passthrough')
###
创建一个ColumnTransformer对象,用于组合数值列和分类列的转换器。
在这个转换器中,我们有两个转换器:一个用于数值列,另一个用于分类列。
remainder='passthrough'参数表示对于列不在numerical_cols和categorical_cols中的列,直接通过转换器。
###
# Apply preprocessing
X = ct.fit_transform(X)
#使用ColumnTransformer对象对数据集X进行拟合和转换。
这会创建一个转换器,并将数据集X转换为新的格式。
X_test = ct.transform(X_test)
#使用ColumnTransformer对象对测试集X_test进行转换。
这会使用之前创建的转换器来转换测试集。
# Print new shape
print('Training set shape:', X.shape)
通过这段代码,我们可以将数据集中的数值列和分类列进行预处理,以便将它们用于机器学习模型。数值列被缩放到均值为0,方差为1,而分类列被转换为独热编码。
5.1.5创建验证集
我们将使用它来选择要使用的模型。
# Train-validation split
X_train, X_valid, y_train, y_valid = train_test_split(X,y,stratify=y,train_size=0.8,test_size=0.2,random_state=0)
###
使用train_test_split函数对数据集X和标签y进行分割。
stratify=y参数确保了分割后训练集和验证集在y(标签)的分布上是相似的,这有助于保持模型的泛化能力。
train_size=0.8参数指定了训练集的比例,即80%的数据用于训练。
test_size=0.2参数指定了验证集的比例,即20%的数据用于验证。
random_state=0参数指定了随机数生成器的种子,确保每次运行代码时都能得到相同的结果。
train_test_split函数返回四个值:X_train、X_valid、y_train和y_valid。
X_train和y_train是用于训练模型的数据和标签。
X_valid和y_valid是用于验证模型的数据和标签。
###
5.2模型选择
-
逻辑回归Logistic Regression:这是一个简单的算法,用于预测一个事件发生的概率。例如,它可以帮助你预测一个人是否患有某种疾病,或者一个邮件是否是垃圾邮件。
-
K-最近邻K-Nearest Neighbors (KNN):这个算法通过查看你数据中的"邻居"来预测一个未知数据的类别。例如,如果你有一个数据集,包含不同地区的人口密度,K-最近邻可以帮助你预测一个新的地区的人口密度。
-
支持向量机Support Vector Machine (SVM):这是一种强大的算法,用于在数据中找到最佳的分界线。例如,它可以帮助你区分狗的照片和猫的照片。
-
随机森林Random Forest (RF):这个算法通过构建多个决策树来预测结果。例如,如果你有一个数据集,包含不同的客户信息,随机森林可以帮助你预测一个新客户是否会购买产品。
-
极端梯度提升Extreme Gradient Boosting (XGBoost):这是一种通过构建决策树来最小化损失函数的算法。例如,如果你有一个数据集,包含不同产品的销售数据,极端梯度提升可以帮助你预测一个新的产品是否会卖得好。
-
轻量级梯度提升机器Light Gradient Boosting Machine(LGBM):这是极端梯度提升的一个轻量级版本,通常更快。
-
分类提升Categorical Boosting (CatBoost):这是一种基于梯度提升的算法,用于处理分类特征。
-
朴素贝叶斯Naive Bayes(NB):这个算法使用贝叶斯定理来预测一个事件的概率。例如,如果你有一个数据集,包含不同类型的邮件,朴素贝叶斯可以帮助你预测一个新邮件的类型。
我们将训练这些模型,并在验证集上对其进行评估,然后选择哪些模型进行下一阶段(交叉验证)。
5.2.1定义分类器
# Classifiers
classifiers = {
"LogisticRegression" : LogisticRegression(random_state=0),
"KNN" : KNeighborsClassifier(),
"SVC" : SVC(random_state=0, probability=True),
"RandomForest" : RandomForestClassifier(random_state=0),
#"XGBoost" : XGBClassifier(random_state=0, use_label_encoder=False, eval_metric='logloss'), # XGBoost takes too long
"LGBM" : LGBMClassifier(random_state=0),
"CatBoost" : CatBoostClassifier(random_state=0, verbose=False),
"NaiveBayes": GaussianNB()
}
# Grids for grid search
LR_grid = {'penalty': ['l1','l2'],
'C': [0.25, 0.5, 0.75, 1, 1.25, 1.5],
'max_iter': [50, 100, 150]}
KNN_grid = {'n_neighbors': [3, 5, 7, 9],
'p': [1, 2]}
SVC_grid = {'C': [0.25, 0.5, 0.75, 1, 1.25, 1.5],
'kernel': ['linear', 'rbf'],
'gamma': ['scale', 'auto']}
RF_grid = {'n_estimators': [50, 100, 150, 200, 250, 300],
'max_depth': [4, 6, 8, 10, 12]}
boosted_grid = {'n_estimators': [50, 100, 150, 200],
'max_depth': [4, 8, 12],
'learning_rate': [0.05, 0.1, 0.15]}
NB_grid={'var_smoothing': [1e-10, 1e-9, 1e-8, 1e-7]}
# Dictionary of all grids
grid = {
"LogisticRegression" : LR_grid,
"KNN" : KNN_grid,
"SVC" : SVC_grid,
"RandomForest" : RF_grid,
"XGBoost" : boosted_grid,
"LGBM" : boosted_grid,
"CatBoost" : boosted_grid,
"NaiveBayes": NB_grid
}
请挨个自学吧。。。。。。。
5.2.2训练和评估模型
使用网格搜索训练模型(但没有交叉验证,因此不会花费太长时间),以大致了解哪些模型是该数据集的最佳模型。
对每个分类器进行网格搜索和交叉验证,找到最佳参数,并计算训练时间。这些信息被存储在valid_scores
数据框中,并且每个分类器的最佳参数被存储在clf_best_params
字典中。
i=0
clf_best_params=classifiers.copy()
valid_scores=pd.DataFrame({'Classifer':classifiers.keys(), 'Validation accuracy': np.zeros(len(classifiers)), 'Training time': np.zeros(len(classifiers))})
for key, classifier in classifiers.items():
start = time.time()
clf = GridSearchCV(estimator=classifier, param_grid=grid[key], n_jobs=-1, cv=None)
# Train and score
clf.fit(X_train, y_train)
valid_scores.iloc[i,1]=clf.score(X_valid, y_valid)
# Save trained model
clf_best_params[key]=clf.best_params_
# Print iteration and training time
stop = time.time()
valid_scores.iloc[i,2]=np.round((stop - start)/60, 2)
print('Model:', key)
print('Training time (mins):', valid_scores.iloc[i,2])
print('')
i+=1
5.3建模