本Task中,我们的任务是使用进阶的机器学习模型lightgbm解决本次问题,以达到更好的预测效果。
你可以从中学会
使用数据集绘制柱状图和折线图,
使用时间序列数据构建历史平移特征和窗口统计特征,
使用lightgbm模型进行训练并预测。
特征工程是参与机器学习竞赛的重要环节,可以通过观察数据并结合专业背景知识改善特征或者构建新的特征。
机器学习中的一个经典理论是:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限。
进阶代码
# 导入模块
!pip install lightgbm
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.metrics import mean_squared_log_error, mean_absolute_error, mean_squared_error
import tqdm
import sys
import os
import gc
import argparse
import warnings
from lightgbm import log_evaluation, early_stopping
warnings.filterwarnings('ignore')
#读取数据
train = pd.read_csv('./data/data283931/train.csv')
test = pd.read_csv('./data/data283931/test.csv')
import matplotlib.pyplot as plt
# 不同type类型对应target的柱状图
type_target_df = train.groupby('type')['target'].mean().reset_index()
plt.figure(figsize=(8, 4))
plt.bar(type_target_df['type'], type_target_df['target'], color=['blue', 'green'])
plt.xlabel('Type')
plt.ylabel('Average Target Value')
plt.title('Bar Chart of Target by Type')
plt.show()
# 不同type类型对应target的折线图
specific_id_df = train[train['id'] == '00037f39cf']
plt.figure(figsize=(10, 5))
plt.plot(specific_id_df['dt'], specific_id_df['target'], marker='o', linestyle='-')
plt.xlabel('DateTime')
plt.ylabel('Target Value')
plt.title("Line Chart of Target for ID '00037f39cf'")
plt.show()
# 合并训练数据和测试数据,并进行排序
data = pd.concat([test, train], axis=0, ignore_index=True)
data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
# 历史平移
for i in range(10,30):
data[f'last{i}_target'] = data.groupby(['id'])['target'].shift(i)
# 窗口统计
data[f'win3_mean_target'] = (data['last10_target'] + data['last11_target'] + data['last12_target']) / 3
# 进行数据切分
train = data[data.target.notnull()].reset_index(drop=True)
test = data[data.target.isnull()].reset_index(drop=True)
# 确定输入特征
train_cols = [f for f in data.columns if f not in ['id','target']]
def time_model(lgb, train_df, test_df, cols):
# 训练集和验证集切分
trn_x, trn_y = train_df[train_df.dt>=31][cols], train_df[train_df.dt>=31]['target']
val_x, val_y = train_df[train_df.dt<=30][cols], train_df[train_df.dt<=30]['target']
# 构建模型输入数据
train_matrix = lgb.Dataset(trn_x, label=trn_y)
valid_matrix = lgb.Dataset(val_x, label=val_y)
# lightgbm参数
lgb_params = {
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': 'mse',
'min_child_weight': 5,
'num_leaves': 2 ** 5,
'lambda_l2': 10,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.05,
'seed': 2024,
'nthread' : 16,
'verbose' : -1,
}
# 训练模型
callbacks = [log_evaluation(period=500), early_stopping(stopping_rounds=500)]
model = lgb.train(lgb_params, train_matrix, 50000, valid_sets=[train_matrix, valid_matrix], categorical_feature=[], callbacks=callbacks)
# 验证集和测试集结果预测
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_df[cols], num_iteration=model.best_iteration)
# 离线分数评估
score = mean_squared_error(val_pred, val_y)
print(score)
return val_pred, test_pred
lgb_oof, lgb_test = time_model(lgb, train, test, train_cols)
# 保存结果文件到本地
test['target'] = lgb_test
test[['id','dt','target']].to_csv('submit.csv', index=None)
代码解析
1、导入模块
!pip install lightgbm
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.metrics import mean_squared_log_error, mean_absolute_error, mean_squared_error
import tqdm
import sys
import os
import gc
import argparse
import warnings
from lightgbm import log_evaluation, early_stopping
warnings.filterwarnings('ignore')
重点使用的库:
numpy
(np
) 和pandas
(pd
) 是Python中用于数值运算和数据处理的标准库。lightgbm
(lgb
) 是一个梯度提升框架,用于训练和预测你的机器学习模型。mean_squared_log_error
、mean_absolute_error
和mean_squared_error
是从sklearn.metrics
中导入的评估模型性能的指标。tqdm
通常用于显示循环或长时间操作的进度条。sys
、os
和gc
是Python的标准库,用于处理系统特定参数和垃圾回收。argparse
用于解析命令行参数,可以增加脚本的灵活性。warnings
用于管理Python的警告信息,filterwarnings('ignore')
用于忽略这些警告。
2、探索性数据分析(EDA)
(1)数据准备阶段
train = pd.read_csv('./data/data283931/train.csv')
test = pd.read_csv('./data/data283931/test.csv')
数据简单介绍:
-
id为房屋id,
-
dt为日标识,训练数据dt最小为11,不同id对应序列长度不同;
-
type为房屋类型,通常而言不同类型的房屋整体消耗存在比较大的差异;
-
target为实际电力消耗,也是我们的本次比赛的预测目标。
(2)绘制不同type类型对应target的柱状图
import matplotlib.pyplot as plt
type_target_df = train.groupby('type')['target'].mean().reset_index()
plt.figure(figsize=(8, 4))
plt.bar(type_target_df['type'], type_target_df['target'], color=['blue', 'green'])
plt.xlabel('Type')
plt.ylabel('Average Target Value')
plt.title('Bar Chart of Target by Type')
plt.show()
train.groupby('type')['target'].mean()
对训练数据集train
按照type
列进行分组,计算每个type
类型对应的target
列的平均值。reset_index()
将分组后的结果重新设置索引,使得type
列变为普通的列,方便后续绘图使用。- 使用
plt.bar()
函数绘制柱状图。type_target_df['type']
是 x 轴的数据,表示不同的type
类型;type_target_df['target']
是 y 轴的数据,表示对应type
类型的平均target
值。 color=['blue', 'green']
指定柱子的颜色,分别对应不同的type
类型。plt.xlabel()
设置 x 轴的标签为'Type'
。plt.ylabel()
设置 y 轴的标签为'Average Target Value'
。plt.title()
设置图表的标题为'Bar Chart of Target by Type'
。plt.show()
显示绘制好的图表。
(3)绘制id为00037f39cf的按dt为序列关于target的折线图
specific_id_df = train[train['id'] == '00037f39cf']
plt.figure(figsize=(10, 5))
plt.plot(specific_id_df['dt'], specific_id_df['target'], marker='o', linestyle='-')
plt.xlabel('DateTime')
plt.ylabel('Target Value')
plt.title("Line Chart of Target for ID '00037f39cf'")
plt.show()
train[train['id'] == '00037f39cf']
用于从训练数据集train
中筛选出id
列值为'00037f39cf'
的所有行数据,这样就得到了一个名为specific_id_df
的数据框,其中包含特定id
的所有数据。- 使用
plt.plot()
函数绘制线图。specific_id_df['dt']
是 x 轴的数据,表示时间(或日期时间);specific_id_df['target']
是 y 轴的数据,表示对应时间点的target
值。 marker='o'
表示用圆圈标记数据点,linestyle='-'
表示用实线连接数据点。plt.xlabel()
设置 x 轴的标签为'DateTime'
,表示日期时间。plt.ylabel()
设置 y 轴的标签为'Target Value'
,表示目标值。plt.title()
设置图表的标题为"Line Chart of Target for ID '00037f39cf'"
,说明这张图展示了特定id
的target
值随时间的变化情况。plt.show()
显示绘制好的线图。
3、特征工程
(1)合并训练数据和测试数据,并进行排序
data = pd.concat([test, train], axis=0, ignore_index=True)
data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
pd.concat([test, train], axis=0, ignore_index=True)
将训练数据集train
和测试数据集test
沿着行(axis=0)方向进行合并,ignore_index=True
用于重新设置索引。data.sort_values(['id','dt'], ascending=False)
按照id
和dt
列进行降序排序。reset_index(drop=True)
重新设置索引,丢弃原来的索引并重置为新的连续整数索引。
(2)历史平移
for i in range(10,30):
data[f'last{i}_target'] = data.groupby(['id'])['target'].shift(i)
- 通过循环,对每个
id
分组后,对target
列进行历史平移。例如,data.groupby(['id'])['target'].shift(10)
表示将每个id
对应的target
列向上平移 10 行,并创建新的特征列last10_target
。
(3)窗口统计
data[f'win3_mean_target'] = (data['last10_target'] + data['last11_target'] + data['last12_target']) / 3
- 创建新的特征列
win3_mean_target
,计算每个样本的last10_target
、last11_target
和last12_target
的均值。
(4)进行数据切分
train = data[data.target.notnull()].reset_index(drop=True)
test = data[data.target.isnull()].reset_index(drop=True)
- 将处理后的数据集
data
根据target
列是否为空切分为训练集train
和测试集test
。train
中包含了target
列不为空的样本,test
中包含了target
列为空的样本。
(5)确定输入特征
train_cols = [f for f in data.columns if f not in ['id','target']]
- 确定用于训练模型的特征列,排除了
id
和target
列之外的所有列。
4、模型训练与测试集预测
(1)函数 time_model
def time_model(lgb, train_df, test_df, cols):
# 训练集和验证集切分
trn_x, trn_y = train_df[train_df.dt>=31][cols], train_df[train_df.dt>=31]['target']
val_x, val_y = train_df[train_df.dt<=30][cols], train_df[train_df.dt<=30]['target']
train_df
是训练数据集,test_df
是测试数据集。cols
是用于训练模型的特征列。train_df[train_df.dt>=31]
选择训练集中日期大于等于31的数据,用于训练模型。train_df[train_df.dt<=30]
选择训练集中日期小于等于30的数据,用作验证集。
# 构建模型输入数据
train_matrix = lgb.Dataset(trn_x, label=trn_y)
valid_matrix = lgb.Dataset(val_x, label=val_y)
- 使用
lgb.Dataset
将训练集和验证集数据转换为 LightGBM 的数据格式。
# lightgbm参数
lgb_params = {
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': 'mse',
'min_child_weight': 5,
'num_leaves': 2 ** 5,
'lambda_l2': 10,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.05,
'seed': 2024,
'nthread' : 16,
'verbose' : -1,
}
- 设置 LightGBM 模型的参数
lgb_params
,包括 boosting 类型、目标函数、评估指标、树的最小子节点权重、叶子节点数、正则化项、特征采样比例、Bagging 参数、学习率等。
# 训练模型
callbacks = [log_evaluation(period=500), early_stopping(stopping_rounds=500)]
model = lgb.train(lgb_params, train_matrix, 50000, valid_sets=[train_matrix, valid_matrix], categorical_feature=[], callbacks=callbacks)
使用 lgb.train
函数训练 LightGBM 模型:
lgb_params
是模型的参数。train_matrix
是训练数据集。50000
是最大训练轮数。valid_sets=[train_matrix, valid_matrix]
指定验证集。callbacks
是回调函数列表,用于控制模型训练过程,例如日志记录和早停。
# 验证集和测试集结果预测
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_df[cols], num_iteration=model.best_iteration)
- 使用训练好的模型预测验证集
val_x
和测试集test_df
的cols
特征列。
# 离线分数评估
score = mean_squared_error(val_pred, val_y)
print(score)
return val_pred, test_pred
- 使用均方误差(MSE)评估模型在验证集上的预测效果,并打印输出。
(3)结果保存
lgb_oof, lgb_test = time_model(lgb, train, test, train_cols)
# 保存结果文件到本地
test['target'] = lgb_test
test[['id','dt','target']].to_csv('submit.csv', index=None)
- 调用
time_model
函数,传入 LightGBM 模型lgb
、训练集train
、测试集test
和特征列train_cols
,得到验证集的预测结果lgb_oof
和测试集的预测结果lgb_test
。 - 将测试集的预测结果
lgb_test
合并到test
数据框中,并保存包含id
、dt
和target
列的结果文件submit.csv
。
运行中曾出现的错误
1、FileNotFoundError: [Errno 2] No such file or directory: './data/train.csv'
这个错误表明你的代码试图从一个不存在或者没有包含目标文件的目录中读取 CSV 文件(
train.csv
)。以下是解决此问题的步骤:
检查文件路径:确认相对于当前工作目录的目录结构和文件路径(
./data/train.csv
)是否正确。./
表示当前脚本或者笔记本所在的目录。确保data
目录存在并且包含train.csv
文件。示例:
├── your_script.py └── data ├── train.csv └── test.csv
绝对路径:可以尝试使用文件的绝对路径来代替相对路径(
./data/train.csv
),这样可以明确指定文件的位置,不受当前工作目录的影响。示例(将
<absolute_path>
替换为实际的绝对路径):train = pd.read_csv('<absolute_path>/data/train.csv') test = pd.read_csv('<absolute_path>/data/test.csv')
确认文件存在:再次确认
data
目录中确实存在train.csv
文件(以及test.csv
文件,如果有的话)。如果文件丢失或者文件名不正确,需要在代码中更新文件名。调试工具:可以使用IDE提供的调试工具或者添加打印语句,输出当前工作目录(
os.getcwd()
)和文件是否存在(os.path.exists('./data/train.csv')
),以便进一步诊断问题。权限:确保你有从指定目录读取文件的必要权限。
替代加载方法:如果你在Jupyter笔记本环境或者类似的环境中工作,可以考虑使用文件浏览器直接导航到文件并直接加载,而不依赖于相对路径。
一旦找到并修正了文件路径问题,
FileNotFoundError
错误应该就能解决,你就可以成功读取你的 CSV 文件了。
2、ModuleNotFoundError: No module named 'lightgbm'
这个错误提示表明 Python 解释器找不到
lightgbm
模块,这通常意味着你的 Python 环境中未安装lightgbm
库。lightgbm
是一个流行的梯度提升库,用于处理包含表格数据的机器学习任务。要解决这个问题,你可以使用
pip
来安装lightgbm
库。具体步骤如下:
使用命令行安装: 打开命令提示符或终端,并执行以下命令:
pip install lightgbm
在 Jupyter Notebook 中安装: 如果你在 Jupyter Notebook 中运行代码,可以在单元格中使用感叹号 (
!
) 来执行 pip 命令:!pip install lightgbm
安装完成后,你就可以正常导入
lightgbm
库了。示例导入语句如下:import lightgbm as lgb
安装成功后,你可以继续使用
lightgbm
库的功能,例如构建和训练梯度提升模型,处理机器学习任务中的数据。
3、TypeError: train() got an unexpected keyword argument 'verbose_eval'
这个错误表明
lightgbm.train()
函数不支持参数verbose_eval
,因此导致了TypeError
异常。在
lightgbm
库中,verbose_eval
和early_stopping_rounds
参数是在低版本中使用的,verbose_eval
用于指定每隔多少轮输出一次信息,early_stopping_rounds
指如果验证集的误差在500次迭代内没有降低,则停止迭代。解决方法:
修改 LightGBM
版本号为3.3.0:pip uninstall lightgbm
pip install lightgbm==3.3.0
卸载已安装
lightgbm
库,并安装能正确使用参数的3.3.0版本库使用 callbacks参数替代:
#修正后的代码示例 from lightgbm import log_evaluation, early_stopping callbacks = [log_evaluation(period=500), early_stopping(stopping_rounds=500)] model = lgb.train(lgb_params, train_matrix, 50000, valid_sets=[train_matrix, valid_matrix], categorical_feature=[], callbacks=callbacks)
注意:
确保
lightgbm
库的版本和文档中的示例代码一致,有时参数名称和支持的参数可能会根据不同版本有所变化。建议查阅官方文档或者库的最新文档以获取正确的参数信息和用法示例。
4、IndentationError: unexpected indent
这个错误提示表明代码中存在意外的缩进错误,通常是由于代码块中的缩进不一致或者不正确导致的问题。
在 Python 中,缩进是非常重要的,它用来表示代码块的开始和结束。如果缩进错误,Python 解释器会抛出
IndentationError
异常。解决方法:
检查缩进:
确保代码块中的所有行都有一致的缩进。在 Python 中通常使用四个空格作为标准的缩进量,不推荐使用制表符进行缩进,因为制表符的宽度可能会有所不同。修复缩进错误:
检查报错行(例如val_pred = model.predict(val_x, num_iteration=model.best_iteration)
)以及其前面的几行代码,确保它们在同一层级的缩进。示例代码:
# 正确的缩进示例 def example_function(): for i in range(10): if i < 5: print(i) else: print(i * 2)
在这个示例中,
for
循环和if-else
语句中的代码块都有正确的缩进,每个级别的缩进都是四个空格。
GBDT
GBDT(Gradient Boosting Decision Tree,梯度提升决策树)是一种集成学习算法,通过集成多棵决策树来完成分类和回归任务。它是由多位学者在1999年提出的一种机器学习算法,旨在通过迭代地训练弱学习器(决策树),逐步提升整体模型的预测能力。
1、主要特点和原理
- 集成方法:GBDT属于集成学习的一种,其核心思想是将多个弱分类器(决策树)集成为一个强分类器,通过组合多个模型的预测结果来提升整体的准确性和泛化能力。
- 基于决策树的弱学习器:GBDT中的基础学习器通常是决策树,每棵决策树都是一个弱学习器,它们串行地构建,每一棵树都试图纠正前一棵树的残差(即预测误差)。
- 损失函数优化:GBDT采用梯度下降法优化损失函数,通过最小化损失函数的负梯度方向来进行迭代优化。在每一轮迭代中,模型在当前模型的负梯度方向上拟合一个新的弱学习器。
- 强大的预测能力:GBDT在处理结构化数据和非线性关系时表现优秀,能够自动处理特征间的复杂交互关系,并且在数据质量较高的情况下,通常能够取得很好的预测效果。
- 常用的实现框架:常见的实现框架包括XGBoost(eXtreme Gradient Boosting)、LightGBM(Light Gradient Boosting Machine)、CatBoost等,它们在性能、效率和功能上有所不同,但基本的原理和思想是相似的。
2、工作流程
- 初始化:初始时,模型用一个常数来拟合训练集的目标值(例如均值)。
- 迭代训练:对于每一轮迭代,计算当前模型的负梯度方向(即残差),用一个新的决策树去拟合这个残差。
- 加权累加:将每棵树的预测结果以一定的权重加权累加,得到最终的预测结果。
- 正则化:通过控制树的深度、叶子节点数等超参数,避免模型过拟合,提升泛化能力。
3、优缺点
- 优点:GBDT能够处理非线性关系、特征间交互复杂的数据,通常表现出色在结构化数据上,且不需要对数据进行特别的预处理,对异常值和缺失值有一定的鲁棒性。
- 缺点:相对于线性模型,GBDT需要更多的计算资源和时间来训练,模型的解释性相对较弱,在数据噪声较大或特征维度较高时容易过拟合。
LightGBM
LightGBM(Light Gradient Boosting Machine)是一个基于梯度提升框架的高效机器学习算法。它由微软开发,专为大规模数据集和高效训练设计。
1、主要特点和优势
-
基于梯度提升框架:LightGBM同样采用了梯度提升算法(Gradient Boosting),但在实现上进行了优化,特别是在处理大规模数据和高效训练方面有显著优势。
-
直方图优化:LightGBM使用了基于直方图的算法来加速训练过程。它将数据按特征的直方图(Histogram)进行分割,降低了内存消耗和计算复杂度,同时提升了训练速度。
-
并行训练:LightGBM支持并行训练,能够利用多线程来加速模型训练过程,尤其适合于多核处理器和分布式计算环境。
-
高效处理大规模数据:LightGBM适用于处理大规模数据集,能够在较短的时间内训练出较为精确的模型。它的内存使用效率和计算效率都比传统的基于树的算法(如XGBoost)更高。
-
支持类别特征:LightGBM能够直接处理类别特征,不需要进行独热编码等预处理,可以显著简化特征工程的流程。
-
高度灵活的参数调优:LightGBM具有丰富的参数选项,可以进行灵活的调优,以适应不同的数据和任务需求。参数的优化能够进一步提升模型的性能和泛化能力。
2、工作原理
-
LightGBM基于树的学习算法,通过迭代训练多棵决策树来逐步提升模型的预测能力。每棵树都试图纠正前一棵树的残差(即预测误差),通过加权累加多棵树的预测结果得到最终的预测结果。
-
LightGBM的训练过程中,使用梯度提升算法来最小化损失函数的负梯度,从而进行模型参数的优化。它采用了Leaf-wise生长策略,即每次从当前所有叶子中选择分裂增益最大的叶子进行分裂,以达到更快的收敛速度。
3、使用场景
- LightGBM适用于各种分类和回归任务,特别是在结构化数据和高维稀疏数据上表现出色。它在处理特征交互和非线性关系时具有优势,常用于金融风控、推荐系统、搜索引擎排序等领域。
特征工程具体分析
1、历史平移特征
历史平移特征指的是将当前时刻的某个变量的值移动(平移)到过去的时间点作为特征。在时间序列问题中,这种特征通常用于捕捉数据的自相关性和滞后效应,即当前时刻的变量值可能受到前几个时间点的影响。
优点
- 简单有效:易于实现和计算。
- 捕捉趋势:能够捕捉到时间序列数据中的趋势和周期性。
- 保留历史信息:不丢失历史数据的信息,可以帮助模型更好地理解数据的动态变化。
缺点
- 信息陈旧:有时候历史数据对当前预测不再具有有效的信息。
- 过度依赖过去:可能导致模型忽略其他更重要的特征或者因素。
2、窗口统计特征
窗口统计特征指的是在一个滑动窗口内计算某个变量的统计特征,例如均值、标准差、最大值、最小值等。这种特征可以帮助模型理解时间序列数据在不同时间窗口内的分布和变化。
优点
- 动态适应:能够随着时间变化而动态调整,反映出数据在不同时间段的变化趋势。
- 丰富信息:提供了更多统计特征,可以帮助模型更好地理解数据的分布和变化。
- 减少噪声:通过平滑数据,减少了由于数据波动带来的噪声影响。
缺点
- 计算复杂度:需要额外的计算资源来计算每个时间窗口内的统计特征。
- 窗口大小选择:窗口大小的选择可能影响特征的效果,需要进行调优。
3、差分特征
差分特征指的是当前时刻与前一个时刻(或者更远的时刻)之间的差值。它可以用来衡量时间序列数据在连续时间点上的变化程度,帮助模型捕捉数据的变化趋势和周期性。
差分操作的方法:
一阶差分(First-order Difference):
表示当前时刻与前一个时刻的差值。
二阶差分(Second-order Difference):
表示一阶差分的差分,用于更进一步地平稳化数据。
季节性差分: 对于具有季节性的时间序列数据,可以进行季节性差分(如一年的差分)来去除季节性趋势。
优点
-
平稳性处理:差分操作可以帮助将非平稳时间序列数据转化为平稳数据,使其更适合应用于统计建模中,尤其是基于平稳时间序列模型(如ARIMA)的预测和分析。
-
捕捉变化趋势:差分特征能够有效地捕捉数据在连续时间点上的变化趋势和波动,有助于理解数据的动态变化过程。
-
去除季节性影响:对具有明显季节性变化的时间序列数据,差分操作可以去除季节性影响,从而使得模型更专注于数据的趋势和非季节性因素。
缺点
-
信息损失:高阶差分(如二阶差分)可能会导致数据的信息损失,因为差分操作会减少数据的原始信息量,特别是在数据量本身不足或者噪声较大时,可能会引入不必要的波动。
-
依赖选择:差分操作的效果依赖于正确选择差分的阶数。选择过高的阶数可能会导致过度平稳化,丢失有用信息;而选择过低的阶数则可能无法达到期望的平稳效果。
-
增加复杂性:对于包含多个季节性因素或长期趋势的复杂时间序列数据,差分操作可能需要多次迭代和尝试,增加了模型建立和调优的复杂性。
-
不适用于所有数据:并非所有时间序列数据都适合进行差分操作。例如,已经是平稳时间序列的数据或者本身没有明显趋势和季节性的数据,进行差分操作可能没有明显的效果甚至是不必要的。
hahaha都看到这里了,要是觉得有用的话就辛苦动动小手点个赞吧!