偶在网上看到一篇数据处理文章关于“11种特征选择策略的实践”。原文出处不详,可自行百度。
于是下载了文中提到的一个automobile.csv文件,根据文章内容对11种策略进行实践,写下此文。
万万没想到,只打算用一天两三个小时做完的测试,结果用了居然一个星期。原文中贴出来的代码也有一些未完善的地方,算法也有一些差异,只好一行一行代码作分析再一点点慢慢调试,最终贴出来的代码核心思想没变但是跟原版略有差别。
坦言,对python这门语言真的是又爱又恨。爱它的方便也恨它的方便。什么东西都可以调用库,用一条语句就能解决了在C里面一大串一大串代码才能实现的功能;但是恨也在此,只是会调用方法并不能深刻了解它的精髓---算法如果不尝试去肢解它、不拿笔和纸一点点验算一遍的话了解再多也只是皮毛。还得是扎扎实实从C这种底层的高级语言开始才是王道呀。
文件的git地址:
https://raw.githubusercontent.com/pycaret/pycaret/master/datasets/automobile.csv
文件的本地下载地址:
https://download.csdn.net/download/tizi666666/87418827
基于该文件,写代码完成这11种策略的实践,同时也会对代码做出解读。各部分仅贴出关键代码,文末会有全代码,代码中一些用于调试的功能打上了注释符号。
零、前序
导入文件到pd中,由于我把文件放在根目录的DataFile下,所以导入文件如下,并做一些预处理。
filepath = os.getcwd() + '\\DataFile\\' + 'automobile.csv'
df = pd.read_csv(filepath)
df = df.replace('?', np.NaN)
df = df.dropna(axis=0)
此处需要特别强调的是,df.dropna(axis=0)这个语句中的参数axis,在一些系统中,会用0来代表行1代表列,有些则相反。所以执行这一句语句后必须要把df的shape显示出来,看看数据的行、列是否还是符合自己要求的,否则会错误删除了数据列或者行。
数据预处理
有些行列需要提前做一些变换(我是一边做一边补充这部分代码的,实际情况中应该提前做好规划)
#预处理
def init():
global df
df = df.replace('?', np.NaN)
df = df.dropna(axis=0)
# print(df.dtypes)
df['bore'] = df['bore'].astype('float')
return None
一、删除未使用的列
这项不言而喻,在数据运算过程中,一些诸如日期、班次、设备名称等信息,基于先见可以预想得到,有些数据是不会被使用或者不适用,可以删除。特别的是有些文件会有索引,从1到N做排列,这类数据如果计算熵值的话会非常的大---当然如果计算增益比的话他们就会“显现出来”。所以这类数据,一眼就看穿肯定没用的,就应该删除了。
二、删除具有缺失值的列
具有缺失值的列,文中提到说这是不可忍受的。个人理解这句话不准确,因为有一些算法其实对缺失值并不敏感。但是在处理过程中对他们进行一些删除、均值填补,或者其他的方式填补还是有必要的。删除不是唯一的方法,也不是最好的方法,但是大多数情况下而言是有效的,具体情况需要具体再分析。
def t2():
print(df.isnull().sum())
return None
通过上述方法,查询哪些项空值较多,再对其进行删除或者是其他处理。
三、不相关的特性
3.1数值变量
文中给出的指引是,使用一个叫corr()函数。先贴上代码,如下
df.corr(numeric_only=True).loc['price'].plot(kind='barh', figsize=(13, 8))
plot.show()
原文在corr的参数中没使用numeric_only=True,我给它补上了,否则会有警告。这项的意思是,只使用数字做运算。因为字符串是无法做运算的(当然,后文会有针对字符变量的,需要转换成one-hot编码)。
关于这个函数,运算速度极快,快得我都忍不住去了解一下这玩意是啥来的,能这么快算出来肯定是不难的。才发现,这个函数只是用来计算数据与数据之间的线性相关性---但是不包过一些次方项或者log函数类运算。有些特征可能并不是线性相关,如果是一些其他关系型函数的话,可能就会被“误删除”,所以对这种方式个人表示有所保留。
文中也提供了一种同样的方法,通过阈值来判断。由于我定义在函数中,所以把变量global了,使用者在调用的过程中也应注意这个问题,避免数据没有被修改。代码如下:
global df
corr = abs(df.corr(numeric_only=True).loc['price'])
corr = corr[corr < 0.2]
cols_to_drop = corr.index.to_list()
df = df.drop(cols_to_drop, axis=1)
运行结果如下:
3.2分类变量
使用箱线图。通过下列代码,绘制关于“fuel-type”这一列的价格数据。
sns.boxplot(y='price', x='fuel-type', data=df)
plt.show()
绘制结果如下图所示:
箱子的中间一条线,是数据的中位数。箱子的上下限,分别是数据的上四分位数和下四分位数。这意味着箱子包含了50%的数据。因此,箱子的宽度在一定程度上反映了数据的波动程度。上下方的点指的是偏离的情况。
文中提到“柴油车的中位价高于汽油车。这意味着这个分类变量可以解释汽车价格,所以应放弃它。可以像这样单独检查每个分类列。”不过呢,它给出来的这个结论,我也不是太理解,主要是不清楚这个“应放弃它”这几个字是怎么理解,是要删除这一列,还是删除这一列中关于柴油车的数据?当然,此处不深究,因为这个数据集仅用于数据处理,不落实到分析实例中。
另外一点,做箱线图之前,有可能还需要对数据做对数变换,否则图像会被压缩。这个实验我没做过,也不清楚什么情况,mark下来当备注吧。另,网上一些对“箱线图”的解答,都把中位数理解成了平均数,这是严重不对的,具体可查定义此处不表。
四、低方差特征
这项在我的sk学习笔记中有提到过,sk中也有对应的sklearn.feature_selection.VarianceThreshold这个API可以实现。
此文中通过以下代码查看
print(df.select_dtypes(include=np.number).var().astype('str'))
print(df['bore'].describe())
显示效果:
可见,“bore”的方差极低。但是!查看一下“bore”的数据可见
该数据项的极差比较小,所以方差也就比较小了。此处,我不太苟同这种做法。因为有些时候你不知道数据是不是已经经过了标准化处理等方法,如果是一些无量纲化的数据的话,那么可能会出现大家方差差异不大甚至是标准化处理后均值为0方差为1,此时的这种对比并无意义。在实际作业中,我们的数据多数来源于一些数采系统,数据的标签含义是什么有可能我们都没深究过,更不知道数据在系统内部是否已经经过了一些无量纲化处理,所以我几乎不用低方差的方法去删数据。此为后话,有待进一步研究。
五、多重共线性
查看两个变量的线性关系。
5.1数值变量
用heatmat热图的方法查看
sns.set(rc={'figure.figsize': (16, 10)})
sns.heatmap(df.corr(), annot=True, linewidths=.5, center=0, cbar=False, cmap="PiYG")
plt.show()
设定阈值比如0.8,然后人工挑选出这些列对其进行删除。
df = df.drop(['length', 'width', 'curb-weight', 'engine-size', 'city-mpg'], axis=1)
5.2分类变量
取两个列,建立交叉表(即代码中的crosstab),计算卡方差。代码如下:
df_cat = df[['fuel-type', 'body-style']]
crosstab = pd.crosstab(df_cat['fuel-type'], df_cat['body-style'])
print(chi2_contingency(crosstab))
这是一种非参检验,通常数据都要求符合正态分布,但是有些时候观察数据并不一定符合这个要求。所以这个参数,只能说是用于参考的。
运行结果如上图所示,分别是卡方值、P值、自由度、预期频率数组。
以上图为例,卡方值为7.89,自由度为4,如果设置显著性水平为0.05,那么查表对应的临界值为9.488,示例中的卡方值7.89<9.488,那么否定了零假设,所以他们之间是不独立的。那么确认,这两个参数是存在关联的。按照原文逻辑,可以删掉其中一个参数。
六、特征系数
接着,文中使用了一段代码,进行了一个逻辑回归运算,如下:
# get dummies for categorical features
df = pd.get_dummies(df, drop_first=True)
X = df.drop('price', axis=1)
y = df['price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.fit_transform(X_test)
X_train = pd.DataFrame(X_train, columns=X.columns.to_list())
X_test = pd.DataFrame(X_test, columns=X.columns.to_list())
model = LinearRegression() # fit
model.fit(X_train, y_train)
训练完毕后,在未进行任何调优和设置的情况下,原文就对模型进行分析。我这里抱怀疑的态度吧。
# feature coefficients
coeffs = model.coef_
# visualizing coefficients
index = X_train.columns.tolist()
pd.DataFrame(coeffs, index=index, columns=['coeff']).sort_values(by='coeff').plot(kind='barh', figsize=(4, 10))
plt.show()
展示的结果如下图:
由于采用了hot-one编码,然后很多数据我没有也不知道原文做了什么处理,所以显得纵坐标比较多,显示很乱。当然这也没关系,在实际情况中,我们也很有可能会遇到这些问题的---列太多显示不全。我写了一段代码,使这些数据以列表的形式显示,这样我们看起来虽然没太直观,但是会更有利于我们做筛选工作。代码如下:
dfcoeffs = pd.DataFrame(coeffs.reshape(1,-1),columns=X.columns.to_list()).T
dfcoeffs = dfcoeffs.sort_values(by=df1.columns.tolist())
print(dfcoeffs.astype('str'))
代码量不多,就三行,目的是为了把上图中的数据以表格的形式显示,并且做了排序。
结果是很清晰的,截取部分如图所示,有些列的贡献为0,说明这些特征我们是可以滤掉的。此处滤掉的标准不详,自行拟定。
七、P值
引入了statsmodels库中提供的方法,输出相关系数和P值。
ols = sm.OLS(y, X).fit()
print(ols.summary())
运行的结果参照网上找来的一张图,细节不表。
八、方差膨胀因子(VIF)
文中提到:
方差膨胀因子 (VIF) 是衡量多重共线性的另一种方法。它被测量为整体模型方差与每个独立特征的方差的比率。一个特征的高 VIF 表明它与一个或多个其他特征相关。根据经验:
VIF = 1 表示无相关性
VIF = 1-5 中等相关性
VIF >5 高相关
vif_df = pd.DataFrame(vif, index=index, columns=['vif']).sort_values(by='vif', ascending=False)
print(vif_df[vif_df['vif'] < 10])
运行结果如下图所示:
九、基于特征重要性选择
在数据集上实现一个随机森林模型并过滤一些特征,代码如下:
model = RandomForestClassifier(n_estimators=200, random_state=0)
model.fit(X,y)
importances = model.feature_importances_
cols = X.columns
pd.DataFrame(importances, cols, columns=['importance']).sort_values(by='importance', ascending=True).plot(kind='barh', figsize=(4, 10))
plt.show()
得到如下图所示。同样由于数据未处理过的原因,横坐标比较乱。
feature_importance,即特征重要性。这个参数之前在Adaboost中有简单了解过,还没试过对它进行实践。
# calculate standard deviation of feature importances
std = np.std([i.feature_importances_ for i in model.estimators_], axis=0)
# visualization
feat_with_importance = pd.Series(importances, X.columns)
fig, ax = plt.subplots(figsize=(12, 5))
feat_with_importance.plot.bar(yerr=std, ax=ax)
ax.set_title("Feature importances")
ax.set_ylabel("Mean decrease in impurity")
plt.show()
观察每一棵树的误差图
根据每个特征的重要性来进行一些删除。
十、使用 Scikit Learn 自动选择特征
10.1基于卡方的技术
直接使用sk里面的库,保留特征
#保留10个特征
X_best = SelectKBest(chi2, k=10).fit_transform(X, y)
#保留75%特征
X_top = SelectPercentile(chi2, percentile=75).fit_transform(X, y)
print(X_best)
print(X_top)
10.2基于正则化
这个方法采用的是L2正则化,引入惩罚系数。文中例子使用的是支持向量机的,L1正则化使用的是LASSO,而L2使用的Ridge会更常用一些,因为L1使用的是绝对值和,L2使用的是平方和,相比L2更好一些,此处不表,具体可参照我的机器学习笔记中关于线性回归的过拟合与欠拟合相关。
model = LinearSVC(penalty='l2', C=0.002, dual=False)
model.fit(X, y)
# select features using the meta transformer
selector = SelectFromModel(estimator=model, prefit=True)
X_new = selector.transform(X)
print(X.shape[1])
print(X_new.shape[1])
# names of selected features
feature_names = np.array(X.columns)
print(feature_names[selector.get_support()])
虽然原文提到了使用L2,但是代码里它又使用了L1。。。代码已改回L2。
在这个例子中,包括超参数C,估计也是随便指定的一个数,也没做任何调优。这是个展示的例子,在实际工作中,大概也还是需要花多一些时间在这方面的处理上。
从我们的显示结果可见,数据X是有136列的,经过了训练后,系统自动为我们判定出了35个。这为我们提供了很好的参考。
10.3基于序贯法
原文:序贯法是一种经典的统计技术。在这种情况下一次添加/删除一个功能并检查模型性能,直到它针对需求进行优化。序贯法有两种变体。前向选择技术从 0 特征开始,然后添加一个最大程度地减少错误的特征;然后添加另一个特征,依此类推。向后选择在相反的方向上起作用。模型从包含的所有特征开始并计算误差;然后它消除了一个可以进一步减少误差的特征。重复该过程,直到保留所需数量的特征。
网上关于SequentialFeatureSelector的资料几乎没有,我也尝试着跟着代码去跑一遍,由于y的特征只有一个,跑起来一直有告警,跑得很慢。。。无奈之下减少了一些参数。看了一下代码,就是构建一个随机森林的estimator,然后用SequentialFeatureSelector导入这个预估器进行训练
model = RandomForestClassifier(n_estimators=1, random_state=0)
selector = SequentialFeatureSelector(estimator=model, n_features_to_select=2, direction='backward', cv=2)
selector.fit_transform(X, y)
feature_names = np.array(X.columns)
print(feature_names[selector.get_support()])
跑出来了一大堆的“UserWarning: The least populated class in y has only 1 members, which is less than n_splits=2. warnings.warn”后得出如下图所示的结果。
十一、主成分分析 (PCA)
终于来到了这个重头戏的东西啦!PCA在我们平时作分析的过程中是非常常用的,但是我通常也只是观察数据再做出选择,很少去绘图。这的demo帮我们直接绘图出来了,值得mark下来以后可以用!
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
pca = PCA()
pca.fit(X_scaled)
evr = pca.explained_variance_ratio_
plt.figure(figsize=(12, 5))
plt.plot(range(0, len(evr)), evr.cumsum(), marker="o", linestyle="--")
plt.xlabel("Number of components")
plt.ylabel("Cumulative explained variance")
plt.show()
这么直观的显示,简直不要太爽了!
有了这张图,我们就可以知道,大概在60的时候,已经可以解释将近100%的数据了---也就是说,在将近140的数据中,除了这60,剩余的80都是“没太大意义的”,我们就可以大刀阔斧地去删除。也不用我们去指定删除哪些个数据,在PCA中它就有方法可以为我们删除,只需要调整n_components参数就可以保留我们需要的比例或者特征个数。
个人实践小结
在分析过程中,首先如果对数据有初步的了解,能对了解的数据做出适当的判断,手动删除一些数据,当然是最好的。但是在实际工作中,其实我们会发现,做算法的和做业务的,大家是完全剥离的。算法工程师没那个功夫也不可能有那个功夫去认真细致地了解数据的具体含义。这就需要提供数据的人能在自己有限的知识体系范围内对数据做出合理的判断。这种判断,不是要你说出“这个数据有用/没用”,如果能准确描述出这种东西,根本就不需要数据分析这件事情了。更重要的是要基于工作经验出发,阐述数据的合理性。举个例子来说,我们不需要驾驶员告诉我们,“车速”这项指标在我们这个应用中有用或者没用、该不该删除掉,但是我们需要驾驶员告诉我们,“车速达到500km/h”这是个不合理的现象---这种不合理,之于算法工程师而言,他们可能并不知道的。
基础知识还是应该要扎实一点。python里有许多的库使用起来确实很方便,但是如果对这些库的应用仅仅只停留在了解,而没有去深挖它具体实现的步骤,很容易埋下了“地雷”,导致误删了一些数据
超参的调节果然是个细活,需要太多的耐心了!
数据的预处理,是每一个模型训练前最重要的缓解,做好这一步的工作,胜过对后续模型超参的调整工作,也能为后续模型超参的调整减少非常多的工作量。
正确看待过拟合问题。这一点一直是我的一个毛病,很多参量不舍得舍弃。应该设定一些规则并把它固话下来。
多写写代码、多做分析,是有好处的。这一次实践让我获得的最大收获居然是一个小小的事情---先标准化还是先划分数据集。以往我一直的经验,都是先把所有的数据都标准化,然后再划分数据集,这次看到别人写的代码是先划分数据集的,然后标准化的时候还使用了两次fit_transform,我一开始还特别纳闷,因为第二次一般也不会再做fit了,这里又fit了一次肯定会出问题啊,所以一直对代码和运算结果保持怀疑态度。等后面慢慢研究慢慢摸索才发现,人家的思路和我的不一样,而且这种做法还更佳!所以还是回答第二点处,要夯实好底层的基础,理解透彻各项工作的原理,这才是重点。学得多真不如学的精。
PLT果然是个好东西!
最后,贴一个整理后的完整的代码,仅供参考,与原文代码有出入,有些调试完毕后就被我注释掉了,可以自行打开。
数据集处于上传审核状态,审核通过后再更新。
import os
import matplotlib.pyplot as plot
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns
from scipy.stats import chi2_contingency
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import statsmodels.api as sm
from sklearn.svm import LinearSVC
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.feature_selection import (SelectKBest, chi2, SelectPercentile, SelectFromModel, SequentialFeatureSelector)
pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
filepath = os.getcwd() + '\\DataFile\\' + 'automobile.csv'
df = pd.read_csv(filepath)
# 预处理
def init():
global df
df = df.replace('?', np.NaN)
df = df.dropna(axis=0)
# print(df.dtypes)
df['bore'] = df['bore'].astype('float')
return None
# 删除未使用的列
# 如果知道某个特定列将不会被使用,随时将其删除。
def t1():
return None
# 删除具有缺失值的列
# 缺失值在机器学习中是不可接受的,因此我们会采用不同的策略来清理缺失数据(例如插补)。但是如果列中缺少大量数据,那么完全删除它是非常好的方法。
def t2():
# print(df.shape)
global df
# print(df.isnull().sum())
df = df.drop('normalized-losses', axis=1)
return None
# 不相关的特征
# 数值变量-如果一个特征没有表现出相关性,它就是一个主要的消除目标。可以分别测试数值和分类特征的相关性。
def t31():
global df
# df.corr(numeric_only=True).loc['price'].plot(kind='barh', figsize=(13, 8))
# plot.show()
corr = abs(df.corr(numeric_only=True).loc['price'])
corr = corr[corr < 0.2]
cols_to_drop = corr.index.to_list()
df = df.drop(cols_to_drop, axis=1)
# print(df)
return None
# 分类变量-使用箱线图查找目标和分类特征之间的相关性
def t32():
sns.boxplot(y='price', x='fuel-type', data=df)
plt.show()
return None
# 低方差特征
def t4():
print(df.select_dtypes(include=np.number).var().astype('str'))
print(df['bore'].describe())
return None
# 多重共线性
# 数值变量
def t51():
global df
# sns.set(rc={'figure.figsize': (16, 10)})
# sns.heatmap(df.corr(), annot=True, linewidths=.5, center=0, cbar=False, cmap="PiYG")
# plt.show()
# 手动删除具有 0.80 共线性阈值的特征。
df = df.drop(['length', 'width', 'curb-weight', 'engine-size', 'city-mpg'], axis=1)
return None
# 分类变量
def t52():
global df
df_cat = df[['fuel-type', 'body-style']]
crosstab = pd.crosstab(df_cat['fuel-type'], df_cat['body-style'])
# print(chi2_contingency(crosstab))
df = df.drop(['body-style'], axis=1)
return None
# 特征系数、P值、VIF
def t6():
global df
df = df.dropna()
df = pd.get_dummies(df, drop_first=True)
X = df.drop('price', axis=1)
y = df['price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.fit_transform(X_test)
# convert back to dataframe
X_train = pd.DataFrame(X_train, columns=X.columns.to_list())
X_test = pd.DataFrame(X_test, columns=X.columns.to_list())
# instantiate model
model = LinearRegression()
model.fit(X_train, y_train)
# feature coefficients
coeffs = model.coef_
# visualizing coefficients
index = X_train.columns.tolist()
# pd.DataFrame(coeffs, index=index, columns=['coeff']).sort_values(by='coeff').plot(kind='barh', figsize=(4, 10))
# plt.show()
# dfcoeffs = pd.DataFrame(coeffs.reshape(1, -1), columns=X.columns.to_list()).T
# dfcoeffs = dfcoeffs.sort_values(by=df1.columns.tolist())
# print(dfcoeffs.astype('str'))
ols = sm.OLS(y, X).fit()
# print(ols.summary())
# calculate VIF
vif = pd.Series([variance_inflation_factor(X.values, i) for i in range(X.shape[1])], index=X.columns)
# display VIFs in a table
vif_df = pd.DataFrame(vif, index=index, columns=['vif']).sort_values(by='vif', ascending=False)
print(vif_df[vif_df['vif'] < 10])
return None
# 特征重要性选择
def t7():
global df
df = pd.get_dummies(df, drop_first=True)
X = df.drop('price', axis=1)
y = df['price']
model = RandomForestClassifier(n_estimators=200, random_state=0)
model.fit(X, y)
importances = model.feature_importances_
# visualization
cols = X.columns
# pd.DataFrame(importances, cols, columns=['importance']).sort_values(by='importance', ascending=True).plot(kind='barh', figsize=(4, 10))
# plt.show()
# calculate standard deviation of feature importances
std = np.std([i.feature_importances_ for i in model.estimators_], axis=0)
# visualization
feat_with_importance = pd.Series(importances, X.columns)
fig, ax = plt.subplots(figsize=(12, 5))
feat_with_importance.plot.bar(yerr=std, ax=ax)
ax.set_title("Feature importances")
ax.set_ylabel("Mean decrease in impurity")
plt.show()
return None
# 基于卡方的技术、正则化
def t8():
global df
df = pd.get_dummies(df, drop_first=True)
X = df.drop('price', axis=1)
y = df['price']
X_best = SelectKBest(chi2, k=10).fit_transform(X, y)
X_top = SelectPercentile(chi2, percentile=75).fit_transform(X, y)
# print(X_best)
# print(X_top)
model = LinearSVC(penalty='l2', C=0.002, dual=False)
model.fit(X, y)
# select features using the meta transformer
selector = SelectFromModel(estimator=model, prefit=True)
X_new = selector.transform(X)
print(X.shape[1])
print(X_new.shape[1])
# names of selected features
feature_names = np.array(X.columns)
print(feature_names[selector.get_support()])
return None
# 基于序贯法
def t9():
global df
df = pd.get_dummies(df, drop_first=True)
X = df.drop('price', axis=1)
y = df['price']
# instantiate model
model = RandomForestClassifier(n_estimators=1, random_state=0)
# select features
selector = SequentialFeatureSelector(estimator=model, n_features_to_select=2, direction='backward', cv=2)
selector.fit_transform(X, y)
# check names of features selected
feature_names = np.array(X.columns)
print(feature_names[selector.get_support()])
return None
# PCA
def t10():
global df
df = pd.get_dummies(df, drop_first=True)
X = df.drop('price', axis=1)
y = df['price']
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# fit PCA to data
pca = PCA()
pca.fit(X_scaled)
evr = pca.explained_variance_ratio_
# visualizing the variance explained by each principal components
plt.figure(figsize=(12, 5))
plt.plot(range(0, len(evr)), evr.cumsum(), marker="o", linestyle="--")
plt.xlabel("Number of components")
plt.ylabel("Cumulative explained variance")
plt.show()
return None
if __name__ == '__main__':
init()
t2()
t31()
t51()
t52()
# t6()
# t7()
# t8()
# t9()
t10()