银行贷款违约风险预测
**项目简介:**比赛由 Kaggle 举办,要求选手依据客户的信用卡信息,分期付款信息,信用局信息等预测客户贷款是否会违约。一共有8个数据集,包括1个主训练集,1个测试集和6个辅助信息表,主训练集特征主要有用户的个人属性,包括用户的性别,职业,是否有车,是否有房,房子面积等基本信息,辅助信息表包括用户的历史申请信息,历史账户余额信息,分期付款信息,信用卡信息,信用局信息和在信用局上的额度信息。
主训练集探索:
目的:
1、了解数据的缺失值情况、异常值情况,以便做对应的数据清洗
2、了解一下违约贷款和正常贷款用户画像的区别,加深对业务的理解,为我们后面的数据分析展开打基础
1、缺失值探索
# 读取训练集和测试集数据
app_train=pd.read_csv('./application_train.csv')
app_test = pd.read_csv('./application_test.csv')#定义缺失值检测函数
def missing_values_table(df):mis_val = df.isnull().sum() #对缺失值进行统计mis_val_percent = 100 * df.isnull().sum() / len(df) #缺失值占比mis_val_table = pd.concat([mis_val, mis_val_percent], axis=1) #mis_val_table_ren_columns = mis_val_table.rename(columns = {0 : 'Missing Values', 1 : '% of Total Values'})# renamemis_val_table_ren_columns =mis_val_table_ren_columns[mis_val_table_ren_columns.iloc[:,1] != 0].sort_values('% of Total Values', ascending=False).round(1) #对缺失值占比进行排序return mis_val_table_ren_columns #返回缺失值列表missing_values_table(app_train)
数据一共有122列,有67列存在缺失情况,最高缺失值的列缺失度为69.9%,可以发现前面的几列特征缺失度的都是一样的,并且它们都是属于房屋信息,根据这个规律我们可以猜测用户缺失房屋信息可能是因为某种特定原因导致的,而不是随机缺失,这点我们会在后面的特征工程用上。
2、异常值探索
查看用户年龄的数据分布情况(因为数据中,年龄的数值是负数,反映的是申请贷款前,这个用户活了多少天,所以这里我除了负365做了下处理),发现数据的分布还是比较正常的,最大年龄69岁,最小年龄20岁,没有很异常的数字
查看用户的工作时间分布情况发现(同样工作时间也是负数,所以我除了负365),最小值是-1000年,这里的-1000年明显是一个异常数据,没有人的工作时间是负数的,这可能是个异常值
看一下用户受工作时间的数据分布情况,发现所有的异常值都是一个值,365243,对于这个异常值我的理解是它可能是代表缺失值,所以我的选择是将这个异常值用空值去替换,这样可以保留这个信息,又抹去了异常值,替换之后我们再看一下工作时间的分布情况,正常了很多
3、违约用户画像探索
这部分分析的目标主要是查看违约用户和非违约用户的特征分布情况,目标是对违约用户的画像建立一个基本的了解,为后续特征工程打下基础。比如数据集里面有很多字段,包括性别、年龄、工作时间等等,那么是男性更容易违约还是女性?是年龄大的人更容易违约还是年龄小的人?查看这些数据,可以帮助我们对数据有更好的理解。
# 绘图函数,通过图形可以直观地看到数据的分布情况
def plot_stats(feature,label_rotation=False,horizontal_layout=True):temp = app_train[feature].value_counts()df1 = pd.DataFrame({feature: temp.index,'Number of contracts': temp.values})# Calculate the percentage of target=1 per category valuecat_perc = app_train[[feature, 'TARGET']].groupby([feature],as_index=False).mean()cat_perc.sort_values(by='TARGET', ascending=False, inplace=True)if(horizontal_layout):fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12,6))else:fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(12,14))sns.set_color_codes("pastel")s = sns.barplot(ax=ax1, x = feature, y="Number of contracts",data=df1)if(label_rotation):s.set_xticklabels(s.get_xticklabels(),rotation=90)s = sns.barplot(ax=ax2, x = feature, y='TARGET', order=cat_perc[feature], data=cat_perc)if(label_rotation):s.set_xticklabels(s.get_xticklabels(),rotation=90)plt.ylabel('Percent of target with value 1 [%]', fontsize=10)plt.tick_params(axis='both', which='major', labelsize=10)plt.show()
- 首先来看一下男性和女性用户的违约率情况,发现男性用户违约率更高,男性用户违约率为11%,女性为7%,男性用户违约率稍高
- 再来看一下违约用户和正常用户的年龄分布情况,因为年龄是连续型变量,和性别不同,所以我们使用分布图去看年龄的分布情况,通过数据分布我们可以看到,违约用户年轻用户分布更多,所以我们可以推断的结论是用户年龄越小,违约的可能性越大
- 可以对用户的年龄进行分捅,进一步观察不同年龄段用户的违约概率,发现确实是用户年龄越小,违约的可能性越高
- 再来看一下不同贷款类型的违约率情况,对于现金贷款和流动资金循坏贷款,现金贷款的违约率更高
- 看下用户有没有房和车对违约率的影响,发现没有车和房的人违约率更高,但相差并不是很大
- 从家庭情况看,申请的用户大多已经结婚,单身和世俗结婚的违约率较高,寡居的违约率最低(civil marriage世俗结婚是西方婚姻的一种,和宗教婚姻相反)
- 看一下子女信息,大部分申请者没有孩子或孩子在3个以下,孩子越多的家庭违约率越高,发现对于有9、11个孩子的家庭违约率达到了100%(和样本少也有关系)
- 根据申请者的收入类型区分,可以发现休产假和没有工作的人违约率较高,在35%以上,对于这两类人群放款需较为谨慎
- 从职业来看,越相对收入较低、不稳定的职业违约率越高,比如低廉劳动力、司机、理发师,而像会计、高科技员工等具有稳定高收入的职业违约率就较低
- 贷款申请人受教育程度大多为中学,学历越低越容易违约
3、特征工程
接下来我们通过对特征的一些理解,尝试做出一些新的特征
- CREDIT_INCOME_PERCENT: 贷款金额/客户收入,预期是这个比值越大,说明贷款金额大于用户的收入,用户违约的可能性就越大
- ANNUITY_INCOME_PERCENT: 贷款的每年还款金额/客户收入,逻辑与上面一致
- CREDIT_TERM: 贷款的每年还款金额/贷款金额,贷款的还款周期,猜测还款周期短的贷款,用户的短期压力可能会比较大,违约概率高
- DAYS_EMPLOYED_PERCENT: 用户工作时间/用户年龄
- INCOME_PER_CHILD:客户收入/孩子数量,客户的收入平均到每个孩子身上,同样的收入,如果这个人的家庭很大,孩子很多,那么他的负担可能比较重,违约的可能性可能更高
- HAS_HOUSE_INFORMATION : 大家还记得我们在处理缺失值时提到的规律吗, 这里我们根据客户是否有缺失房屋信息设计一个二分类特征,如果未缺失的话是1,缺失的是0
app_train_domain = app_train.copy()
app_test_domain = app_test.copy()app_train_domain['CREDIT_INCOME_PERCENT'] = app_train_domain['AMT_CREDIT'] / app_train_domain['AMT_INCOME_TOTAL']
app_train_domain['ANNUITY_INCOME_PERCENT'] = app_train_domain['AMT_ANNUITY'] / app_train_domain['AMT_INCOME_TOTAL']
app_train_domain['CREDIT_TERM'] = app_train_domain['AMT_ANNUITY'] / app_train_domain['AMT_CREDIT']
app_train_domain['DAYS_EMPLOYED_PERCENT'] = app_train_domain['DAYS_EMPLOYED'] / app_train_domain['DAYS_BIRTH']
app_train_domain['INCOME_PER_CHILD'] = app_train_domain['AMT_INCOME_TOTAL'] / app_train_domain['CNT_CHILDREN']
app_train_domain['HAS_HOUSE_INFORMATION'] = app_train_domain['COMMONAREA_MEDI'].apply(lambda x:1 if x>0 else 0)
对设计出来的连续性特征查看它们在违约用户和非违约用户中的分布情况,可以发现除CREDIT_TERM这个特征外,其他的特征区分度似乎都不是很明显
- 再来看一下通过缺失值设计的这个特征,通过下图我们可以看到,缺失房屋信息的用户违约概率要明显高于未缺失用户,这在我们模型的预测中可以算是一个比较有效的特征了
- 对测试集做同样的处理
4、建模预测
最后,利用现有的主数据集先进行一次建模预测,模型的话选择LGB模型,速度快,效果好
# 创建实例
model = lgb.LGBMClassifier(n_estimators=1000, objective = 'binary', class_weight = 'balanced', learning_rate = 0.05, reg_alpha = 0.1, reg_lambda = 0.1, subsample = 0.8, n_jobs = -1, random_state = 50)# 训练模型
model.fit(train_features, train_labels, eval_metric = 'auc',eval_set = [(valid_features, valid_labels), (train_features, train_labels)],eval_names = ['valid', 'train'], categorical_feature = cat_indices,early_stopping_rounds = 100, verbose = 200)
- 通过lgb自带的函数查看特征的重要性
def plot_feature_importances(df):# 按照重要性排序df = df.sort_values('importance', ascending = False).reset_index()# 对重要性特征进行标准化df['importance_normalized'] = df['importance'] / df['importance'].sum()# 作图展示plt.figure(figsize = (10, 6))ax = plt.subplot()# 取重要性排名前15名进行展示ax.barh(list(reversed(list(df.index[:15]))), df['importance_normalized'].head(15), align = 'center', edgecolor = 'k')# 设置yticks和labelsax.set_yticks(list(reversed(list(df.index[:15]))))ax.set_yticklabels(df['feature'].head(15))plt.xlabel('Normalized Importance'); plt.title('Feature Importances')plt.show()return df
fi_sorted = plot_feature_importances(fi)
5、利用其他数据集信息
首先是信用局信息,数据集中的每一行代表的是主训练集中的申请人曾经在其他金融机构申请的贷款信息,可以看到数据集中同样有一列是“SK_ID_CURR’,和主训练集中的列一致,可以通过这一列去把辅助训练集和主训练集做left join,但需要注意的一点是,一个SK_ID_CURR可能会对应多个SK_ID_BUREAU,即一个申请人如果在其他金融机构曾经有多条贷款信息的话,这里就会有多条记录,因为模型训练每个申请人在数据集中只能有一条记录,所以说不能直接把辅助训练集去和主训练集join,一般来说需要去计算一些统计特征(groupby操作)
# 读取信用局数据
bureau = pd.read_csv('./bureau.csv')
bureau.head()
#针对每个贷款申请人计算他们在其他金融机构历史上的贷款数量
previous_loan_counts = bureau.groupby('SK_ID_CURR', as_index=False)['SK_ID_BUREAU'].count().rename(columns = {'SK_ID_BUREAU': 'previous_loan_counts'})
previous_loan_counts.head()
- 然后我们再把计算出来的统计特征和主训练集做left join,可以看到我们的统计特征就出现在了最后一列
app_train = app_train.merge(previous_loan_counts, on = 'SK_ID_CURR', how = 'left')# 用0填充空值
app_train['previous_loan_counts'] = app_train['previous_loan_counts'].fillna(0)
app_train.head()
- 在做出新特征后,我们往往还需要检验新特征是否对预测有区分度,不是所有的新特征都是有用的,有些没有用的特征加到数据集里反而会降低预测值,通过查看违约和非违约用户previous_loan_counts的统计属性发现,虽然非违约用户的平均贷款申请数量要略多于违约用户,但差异很小,所以其实很难判断这个特征对预测是否是有用的,我们可以尝试在做一些更多的特征
- 定义一个查看分布的函数,以后再做出新特征时,我们可以用这个函数快速查看新的特征在违约用户和非违约用户中的分布情况
def kde_target(var_name, df):# 计算新变量和目标之间的相关性corr = df['TARGET'].corr(df[var_name])# 计算违约和不违约用户的中位数avg_repaid = df.ix[df['TARGET'] == 0, var_name].median()avg_not_repaid = df.ix[df['TARGET'] == 1, var_name].median()plt.figure(figsize = (12, 6))# 作图展示分布情况sns.kdeplot(df.ix[df['TARGET'] == 0, var_name], label = 'TARGET == 0')sns.kdeplot(df.ix[df['TARGET'] == 1, var_name], label = 'TARGET == 1')plt.xlabel(var_name); plt.ylabel('Density'); plt.title('%s Distribution' % var_name)plt.legend()
kde_target('previous_loan_counts', app_train)
连续型变量特征提取
- 对于连续型变量,可以采用计算它们的统计值来作为特征
# 定义函数完成特征提取
def agg_numeric(df, group_var, df_name):# 移除id变量for col in df:if col != group_var and 'SK_ID' in col:df = df.drop(columns = col)group_ids = df[group_var]numeric_df = df.select_dtypes('number')numeric_df[group_var] = group_ids# 通过groupby和agg函数计算出统计特征agg = numeric_df.groupby(group_var).agg(['count', 'mean', 'max', 'min', 'sum']).reset_index()columns = [group_var]# 列名重命名for var in agg.columns.levels[0]:if var != group_var:for stat in agg.columns.levels[1][:-1]:columns.append('%s_%s_%s' % (df_name, var, stat))agg.columns = columnsreturn aggbureau_agg_new = agg_numeric(bureau.drop(columns = ['SK_ID_BUREAU']), group_var = 'SK_ID_CURR', df_name = 'bureau')
# 提取的特征加入到主训练集中
app_train = app_train.merge(bureau_agg_new , on = 'SK_ID_CURR', how = 'left')
bureau_agg_new.head()
离散型变量特征提取
对于离散型变量不能计算它们的统计特征,但可以计算离散型特征中每个取值的个数,通过这种方式来获取到一些信息
比如:原始数据是这样的
最终我们希望达到的效果是:
除此之外,我们还可以计算每个用户的每个值的个数在整体中的比例:
# 同样设置一个函数来提取离散变量的特征
def count_categorical(df, group_var, df_name):# 首先先把数据集中的离散特征变成哑变量categorical = pd.get_dummies(df.select_dtypes('object'))categorical[group_var] = df[group_var]# 通过groupby和agg函数对特征进行统计categorical = categorical.groupby(group_var).agg(['sum', 'mean'])column_names = []# 重命名列for var in categorical.columns.levels[0]:for stat in ['count', 'count_norm']:column_names.append('%s_%s_%s' % (df_name, var, stat))categorical.columns = column_namesreturn categoricalbureau_counts = count_categorical(bureau, group_var = 'SK_ID_CURR', df_name = 'bureau')
#做好的特征可以合并到主训练集中
app_train = app_train.merge(bureau_counts , left_on = 'SK_ID_CURR', right_index = True, how = 'left')
app_train.head()
整合所有数据集
- 重新读取一遍数据集,把数据集还原到初始状态
app_train=pd.read_csv('./application_train.csv')
app_test = pd.read_csv('./application_test.csv')
bureau = pd.read_csv('./bureau.csv')
previous_application = pd.read_csv('./previous_application.csv')
- 把之前对主训练集做的特征重新加入到数据集
app_train['CREDIT_INCOME_PERCENT'] = app_train['AMT_CREDIT'] / app_train['AMT_INCOME_TOTAL']
app_train['ANNUITY_INCOME_PERCENT'] = app_train['AMT_ANNUITY'] / app_train['AMT_INCOME_TOTAL']
app_train['CREDIT_TERM'] = app_train['AMT_ANNUITY'] / app_train['AMT_CREDIT']
app_train['DAYS_EMPLOYED_PERCENT'] = app_train['DAYS_EMPLOYED'] / app_train['DAYS_BIRTH']
app_train['INCOME_PER_CHILD'] = app_train['AMT_INCOME_TOTAL'] / app_train['CNT_CHILDREN']
app_train['HAS_HOUSE_INFORMATION'] = app_train['COMMONAREA_MEDI'].apply(lambda x:1 if x>0 else 0)app_test['CREDIT_INCOME_PERCENT'] = app_test['AMT_CREDIT'] / app_test['AMT_INCOME_TOTAL']
app_test['ANNUITY_INCOME_PERCENT'] = app_test['AMT_ANNUITY'] / app_test['AMT_INCOME_TOTAL']
app_test['CREDIT_TERM'] = app_test['AMT_ANNUITY'] / app_test['AMT_CREDIT']
app_test['DAYS_EMPLOYED_PERCENT'] = app_test['DAYS_EMPLOYED'] / app_test['DAYS_BIRTH']
app_test['INCOME_PER_CHILD'] = app_test['AMT_INCOME_TOTAL'] / app_test['CNT_CHILDREN']
app_test['HAS_HOUSE_INFORMATION'] = app_test['COMMONAREA_MEDI'].apply(lambda x:1 if x>0 else 0)
- 两个函数完成之前对信用局数据中连续变量和离散变量的特征提取
bureau_counts = count_categorical(bureau, group_var = 'SK_ID_CURR', df_name = 'bureau')
bureau_agg_new = agg_numeric(bureau.drop(columns = ['SK_ID_BUREAU']), group_var = 'SK_ID_CURR', df_name = 'bureau')
bureau_agg_new.head()
- 给训练集和测试集增加信用局相关特征
app_train = app_train.merge(bureau_counts, on = 'SK_ID_CURR', how = 'left')
app_train = app_train.merge(bureau_agg_new, on = 'SK_ID_CURR', how = 'left')app_test = app_test.merge(bureau_counts, on = 'SK_ID_CURR', how = 'left')
app_test = app_test.merge(bureau_agg_new, on = 'SK_ID_CURR', how = 'left')
- 对于历史贷款信息数据,做同样的操作
previous_appication_counts = count_categorical(previous_application, group_var = 'SK_ID_CURR', df_name = 'previous_application')
previous_appication_agg_new = agg_numeric(previous_application, group_var = 'SK_ID_CURR', df_name = 'previous_application')app_train = app_train.merge(previous_appication_counts, on = 'SK_ID_CURR', how = 'left')
app_train = app_train.merge(previous_appication_agg_new, on = 'SK_ID_CURR', how = 'left')
app_test = app_test.merge(previous_appication_counts, on = 'SK_ID_CURR', how = 'left')
app_test = app_test.merge(previous_appication_agg_new, on = 'SK_ID_CURR', how = 'left')print(app_train.shape)
print(app_test.shape)
特征筛选
在之前的一系列的特征工程中,我们给训练集和测试集增加了很多新的特征,特征也膨胀到了600多列,在最后建模之前,还需要对这些加入的特征再做一次筛选,排除一些具有共线性的特征以提高模型的效果,我们可以计算变量与变量之间的相关系数,来快速移除一些相关性过高的变量,这里可以定义一个阈值是0.8,即移除每一对相关性大于0.8的变量中的其中一个变量
corrs = app_train.corr()# 设置阈值
threshold = 0.8
above_threshold_vars = {}
for col in corrs:above_threshold_vars[col] = list(corrs.index[corrs[col] > threshold])# Track columns to remove and columns already examined
cols_to_remove = []
cols_seen = []
cols_to_remove_pair = []# Iterate through columns and correlated columns
for key, value in above_threshold_vars.items():# Keep track of columns already examinedcols_seen.append(key)for x in value:if x == key:nextelse:# Only want to remove one in a pairif x not in cols_seen:cols_to_remove.append(x)cols_to_remove_pair.append(key)cols_to_remove = list(set(cols_to_remove))
print('Number of columns to remove: ', len(cols_to_remove))
# 在结果中可以看到,我们一共要移除189列具有高相关性的变量
#训练集和测试集都移除对应的列,把列数降低到426
train_corrs_removed = app_train.drop(columns = cols_to_remove)
test_corrs_removed = app_test.drop(columns = cols_to_remove)print('Training Corrs Removed Shape: ', train_corrs_removed.shape)
print('Testing Corrs Removed Shape: ', test_corrs_removed.shape)
建模预测
用之前建立好的模型再进行一次建模预测,到此,就完成了这个项目
submission, fi, metrics = model(train_corrs_removed, test_corrs_removed)
fi_sorted = plot_feature_importances(fi)
银行贷款违约风险预测相关推荐
- 【机器学习】银行贷款违约预测
使用二分类逻辑回归识别贷款违约风险 为了说明逻辑回归的应用场景,这里引入一个案例,该案例有关银行贷款违约,我们使用二分类逻辑回归来评估信用风险,如果您是银行的贷款人员,那么您希望能够识别那些指示可能违 ...
- python数据分析实战之信用卡违约风险预测
文章目录 1.明确需求和目的 2. 数据收集 3.数据预处理 3.1 数据整合 3.1.1 加载相关库和数据集 3.1.2 主要数据集概览 3.2 数据清洗 3.2.1 多余列的删除 3.2.2 数据 ...
- 信贷违约风险预测(二)简单的数据探索
之前已经简单介绍了数据,客户的违约风险的预测是一个监督学习的任务,主要是对客户进行分类,就是哪些人可以获得贷款,哪些不可以,每个申请者可能会违约的概率在0~1之间,0:表示申请者能及时还款,1:申请者 ...
- Home Credit Default Risk 违约风险预测,kaggle比赛,初级篇,LB 0.749
Home Credit Default Risk 结论 背景知识 数据集 数据分析 平衡度 数据缺失 数据类型 离群值 填充缺失值 建模 Logistic Regression LightGBM Fe ...
- 信贷违约风险预测(四)训练模型
之前给予application_train.csv和application_test.csv,做了简单的特征工程:得到了下面几个文件:(对recode后的数据做了三种不同的特征处理) Polynomi ...
- Python大数据综合应用 :零基础入门机器学习、深度学习算法原理与案例
机器学习.深度学习算法原理与案例实现暨Python大数据综合应用高级研修班 一.课程简介 课程强调动手操作:内容以代码落地为主,以理论讲解为根,以公式推导为辅.共4天8节,讲解机器学习和深度学习的模型 ...
- uci数据集中的缺失数据_从uci早期糖尿病风险预测数据集中创建分类器
uci数据集中的缺失数据 To begin we must first go and download the dataset from the UCI dataset repository. The ...
- 【Ryo】SPSS Modeler:贝叶斯网络在预测银行信贷风险中的应用
对银行信贷来说,如何量化客户违约的可能性,对潜在的风险进行预测是管理决策层关注的重中之重.面对复杂的信息结构和庞大的人群数据,运用贝叶斯网络能够理清相关影响因素的关联关系,是现在提高信贷违约风险预测正 ...
- 中小微企业的信贷决策
本短文以(20数学建模C-中小微企业的信贷决策_木下瞳的博客-CSDN博客)提供的思路编写,详情思路可见吗木下瞳. 基于客户价值对中小微企业信贷决策研究 摘要 中小微企业目前在我国社会企业市场占比规模 ...
- 《scikit-learn》SVM(二)数据不均衡
我们继续学习一些其他的细节 一:样本均衡问题 我们来看看在SVM中样本不均衡的情况 比如两个样本集合的数目严重不对等,我们希望模型更能识别出少数样本,比如银行贷款,预测某人会不会抵赖,我们更希望能预测 ...
最新文章
- 石英晶体振荡器的结构
- 《30天吃掉那只 TensorFlow2.0 》(附下载)
- 微信小程序图片上传到服务器再自动替换,微信小程序批量上传图片到服务器,并实现预览,删除功能...
- 团队项目个人进展——Day05
- 异步爬虫(爬取小说30秒12MB!)Python实现
- QJson生成文件和解析文件
- graphql-yoga的安装步骤
- folders默认配置 shell_更改windows默认的User Shell Folders
- VSTS TFS 强制删除签出锁定项 解除 锁定
- python给定一个整数n、判断n是否为素数_输入一个大于3的整数n,判断它是否为素数...
- POJ-2151 Check the difficulty of problems 概率DP
- Azure Synapse Analytics简介第1部分:什么是Azure Synapse Analytics?
- gitolite安装及配置
- 富文本编辑器粘贴图片
- Funcode学习笔记:写一个维护性高、扩展性强的框架【By Myself】【C++】
- 极客星球 | Unity3D插件模板化探索
- 关于adsl上网的问题
- 坦克大战小游戏——新手练习用的
- whois查询的不同结果是什么意思?
- 二十五、Java中的网络编程
热门文章
- moocpython答案_中国大学moocPython编程基础题目及答案
- 魔兽争霸3冰封王座 对战初始化被禁止
- 从校园到职场 - 谈谈艺多不压身
- 利用kNN算法对iris数据集进行分类,本人也做了修改使得代码可实现
- 常见的Web服务器、应用服务器(Apache、tomcat、jetty、Nginx)简介及优缺点总结
- html制作3d动画效果,【分享】HTML5的Canvas制作3D动画效果分享
- unity 裙子摆动_【Unity Shader】摇摆的小草——顶点动画
- wordpress头像被墙_如何在WordPress中添加新的默认头像
- (爱斯维尔期刊:遇到问题已解决) 使用elsarticle-harv的style引文格式时报错 mand\Nat@force@numbers{}\NAT@force@numbers
- 如何在电脑上录制游戏视频画面