本文是python初学者上手机器学习的学习记录,重点是熟悉整个操作流程。
整个流程包括数据载入,查看数据结构,划分测试集与训练集,数据探索,数据准备,选择和训练模型,交叉验证以及测试集评估算法。
对于第一次上手的新手来说,还是很烦躁的。
csv文件用的是pd.read_csv函数。注意文件路径中的“\”应该再使用一个“\”进行转义,或者直接换成“/”。
import numpy as np
import pandas as pd
data=pd.read_csv('C:\\Users\\43480\\Downloads\\train.csv')
个人理解这是整个流程中重要性最容易被忽略的一个部分。了解了数据性质,才可以在后面的各个流程中选择合适的算法和算法参数。
在这里,需要对于数据的质量做出判断。
data.head()默认是前五行。项目目标是用前面的数据预测最后一列的label。
import matplotlib.pyplot as plt
data.head()
id | income | age | experience_years | is_married | city | region | current_job_years | current_house_years | house_ownership | car_ownership | profession | label | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | train_0 | 8529345 | 44 | 2 | single | 210 | 0 | 2 | 10 | rented | no | 13 | 0 |
1 | train_1 | 7848654 | 55 | 9 | single | 229 | 2 | 9 | 13 | rented | no | 43 | 0 |
2 | train_2 | 8491491 | 61 | 20 | single | 114 | 28 | 8 | 11 | rented | no | 12 | 0 |
3 | train_3 | 8631544 | 69 | 13 | married | 276 | 14 | 13 | 12 | rented | no | 27 | 0 |
4 | train_4 | 6947233 | 62 | 10 | single | 56 | 11 | 10 | 12 | rented | no | 47 | 0 |
data.info()可以看到各个属性的类型与非空值的数量,决定了是否需要独热编码将文本转成数字以及是否需要用数据清理补全空值。
data.describe()可以看到数字量的信息,重点是看各个量的分布情况,会不会是一个很偏的分布或者存在明显的离群点,如果是,有的时候需要有对应的处理。
data.info()
data.describe()
RangeIndex: 168000 entries, 0 to 167999
Data columns (total 13 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 168000 non-null object
1 income 168000 non-null int64
2 age 168000 non-null int64
3 experience_years 168000 non-null int64
4 is_married 168000 non-null object
5 city 168000 non-null int64
6 region 168000 non-null int64
7 current_job_years 168000 non-null int64
8 current_house_years 168000 non-null int64
9 house_ownership 168000 non-null object
10 car_ownership 168000 non-null object
11 profession 168000 non-null int64
12 label 168000 non-null int64
dtypes: int64(9), object(4)
memory usage: 16.7+ MB
income | age | experience_years | city | region | current_job_years | current_house_years | profession | label | |
---|---|---|---|---|---|---|---|---|---|
count | 1.680000e+05 | 168000.000000 | 168000.000000 | 168000.000000 | 168000.000000 | 168000.000000 | 168000.000000 | 168000.000000 | 168000.000000 |
mean | 4.994944e+06 | 49.961577 | 10.088887 | 157.930446 | 13.801554 | 6.339571 | 11.997673 | 25.251054 | 0.123065 |
std | 2.879353e+06 | 17.053195 | 5.998594 | 92.123165 | 9.379915 | 3.647073 | 1.399613 | 14.722342 | 0.328513 |
min | 1.031000e+04 | 21.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 10.000000 | 0.000000 | 0.000000 |
25% | 2.499018e+06 | 35.000000 | 5.000000 | 78.000000 | 6.000000 | 4.000000 | 11.000000 | 13.000000 | 0.000000 |
50% | 4.994848e+06 | 50.000000 | 10.000000 | 157.000000 | 14.000000 | 6.000000 | 12.000000 | 25.000000 | 0.000000 |
75% | 7.475446e+06 | 65.000000 | 15.000000 | 238.000000 | 22.000000 | 9.000000 | 13.000000 | 38.000000 | 0.000000 |
max | 9.999938e+06 | 79.000000 | 20.000000 | 316.000000 | 28.000000 | 14.000000 | 14.000000 | 50.000000 | 1.000000 |
value_counts()方法可以查看object类型对象的信息。
data['label'].value_counts()
0 147325
1 20675
Name: label, dtype: int64
data.hist(bins=100, figsize=(20,15))
.hist()查看数值类型对象的分布,仍然是,主要看数据是否存在出乎意料的分布。
data.hist(bins=100, figsize=(20,15))
array([[,
,
],
[,
,
],
[,
,
]],
dtype=object)
这里只是最简单的用法train_set,test_set=train_test_split(data,test_size=0.2),并没有指定随机种子random_seed和用于处理不平衡数据集的参数satisfy
from sklearn.model_selection import train_test_split
train_set,test_set=train_test_split(data,test_size=0.2)
由于在本例分类问题中,明显看出label的分布很不平衡。因此,有必要在创建测试集时考虑这一点,即stratify=data[‘label’]。注意这里奇怪的设定X和y的方法data[:][data.columns[:-1]]以及data[‘label’](这里的data[‘label’]也可以写成data[:][data.columns[-1]])。
from sklearn.model_selection import train_test_split
train_set,test_set,y_train,y_test=train_test_split(data[:][data.columns[:-1]],data[:][data.columns[-1]],test_size=0.2,stratify=data['label'])
查看训练集和验证集的label分布比例。可以发现二者中label的01比是一样的。
y_train.value_counts()/len(train_set)
0 0.876935
1 0.123065
Name: label, dtype: float64
y_test.value_counts()/len(test_set)
0 0.876935
1 0.123065
Name: label, dtype: float64
理论上来说,从这里开始的所有操作都应该在训练集上进行。数据探索的目的是为了进一步寻找显而易见的数据之间的关系。这里用到的函数是散点图.plot(kind=‘scatter’)和相关系数矩阵.corr()。
这里使用.join是因为需要查看的是各个变量与最后的预测结果label的关系,alpha可以认为是表示数据点颜色深浅的参数。
data_train=train_set.copy()
temp_data=data_train.join(y_train)
temp_data[1:1000].plot(kind="scatter",x="income",y="label",alpha=0.05)
temp_data.corr()
income | age | experience_years | city | region | current_job_years | current_house_years | profession | label | |
---|---|---|---|---|---|---|---|---|---|
income | 1.000000 | 0.000950 | 0.004149 | -0.003351 | -0.001929 | 0.009478 | -0.002693 | 0.004420 | -0.001807 |
age | 0.000950 | 1.000000 | -0.003419 | 0.005144 | -0.005433 | 0.000720 | -0.019908 | -0.012319 | -0.023407 |
experience_years | 0.004149 | -0.003419 | 1.000000 | -0.023026 | 0.000007 | 0.645649 | 0.017978 | 0.001453 | -0.031337 |
city | -0.003351 | 0.005144 | -0.023026 | 1.000000 | -0.035727 | -0.027860 | -0.008931 | 0.018646 | 0.005056 |
region | -0.001929 | -0.005433 | 0.000007 | -0.035727 | 1.000000 | 0.008473 | 0.004185 | 0.001800 | -0.004040 |
current_job_years | 0.009478 | 0.000720 | 0.645649 | -0.027860 | 0.008473 | 1.000000 | 0.003652 | -0.004915 | -0.015477 |
current_house_years | -0.002693 | -0.019908 | 0.017978 | -0.008931 | 0.004185 | 0.003652 | 1.000000 | 0.002531 | -0.006133 |
profession | 0.004420 | -0.012319 | 0.001453 | 0.018646 | 0.001800 | -0.004915 | 0.002531 | 1.000000 | -0.004007 |
label | -0.001807 | -0.023407 | -0.031337 | 0.005056 | -0.004040 | -0.015477 | -0.006133 | -0.004007 | 1.000000 |
这个阶段的目的是把数据处理成机器学习算法能够直接使用的数据。主要包括缺失值处理、文本和分类属性处理和特征缩放。
缺失值处理可以使用dropna()以及fillna()等方法,实际操作中这些方法都集成在SimpleImputer类中。注意如果使用strategy=‘median’,则这个类只能在数值属性上计算。
简单的做法是使用下述的OrdinalEncoder()类,这个类的问题是对于无序类别转换后会在未来的相似度度量时出现问题,因此对于无序类别,一般都采用后面用到的OneHotEncoder()类进行编码转换。
下面这个部分的代码首先划分了数值型属性和文本分类属性,然后对于数值使用SimpleImputer,对于文本分类使用OrdinalEncoder。
from sklearn.preprocessing import OrdinalEncoder
from sklearn.impute import SimpleImputer
imputer=SimpleImputer(strategy='median')
cat=['is_married','house_ownership','car_ownership']
data_cat=data_train[cat]
cat.append('id')
data_num=data_train.drop(cat,axis=1)
cat.remove('id')
ordinal_encoder=OrdinalEncoder()
data_cat_encodered=ordinal_encoder.fit_transform(data_cat)
说实话我不太清楚pipeline的优势在哪里。理论上来说可以让程序简洁易读,但是实际操作的时候,一旦数据处理的结果除了问题,还是需要把pipeline拆开一步一步看里面的问题到底出在哪里的。
只能说再学习再领会了。
也就是标准化,用的是StandardScaler类。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline=Pipeline([
('imputer',SimpleImputer(strategy='median')),
#('attribs_adder',CombinedAttributesAdder()),
('std_scaler',StandardScaler())
])
data_num_tr=num_pipeline.fit_transform(data_num)
from sklearn.preprocessing import OneHotEncoder
cat_encoder=OneHotEncoder()
data_cat_1hot=cat_encoder.fit_transform(data_cat)
ColumnTransformer类在这里的用法是把处理后的数值型和文本型属性连接成一张完整的数据表格。讲道理这个类的参数很奇怪。
from sklearn.compose import ColumnTransformer
num_attribs=data_train.columns.drop(cat).drop('id')#.drop()可以在不影响原对象的情况下生成一个新的对象
cat_attribs=cat.copy()
full_pipeline=ColumnTransformer([
('num',num_pipeline,num_attribs),
('cate',OneHotEncoder(),cat_attribs)
])
data_prepared=full_pipeline.fit_transform(data_train)
到这里数据预处理就做完了,后面就是使用各种算法了。
这里首先使用最简单的最小二乘法试一试。暂时无视最小二乘做分类预测合不合适的问题…
from sklearn.linear_model import LinearRegression
lin_reg=LinearRegression()
data_labels=y_train
lin_reg.fit(data_prepared,data_labels)
LinearRegression()
从训练集中拿出一些数据来看看预测结果。
some_data=data_train.iloc[:100]
some_labels=data_labels.iloc[:100]
some_data_prepared=full_pipeline.transform(some_data)
print('Predictions:', lin_reg.predict(some_data_prepared))
print('labels:',some_labels)
Predictions: [0.13324356 0.06813812 0.11545181 0.11249924 0.08952332 0.1189003
0.105793 0.10971451 0.11217117 0.13776016 0.08611679 0.10936356
0.09428406 0.12818909 0.1342659 0.10783768 0.11572647 0.1403389
0.09718704 0.0999794 0.12628555 0.11398315 0.10149384 0.08406067
0.11320114 0.12833023 0.09347153 0.11769485 0.11962891 0.10712051
0.12628555 0.12824249 0.11003494 0.11316299 0.12756729 0.14403152
0.12031937 0.09963608 0.13869095 0.1473732 0.1410408 0.14761734
0.09777069 0.1433754 0.12514114 0.13670731 0.12614822 0.11314774
0.12529373 0.1254158 0.11951828 0.12356949 0.11419296 0.14549637
0.12572861 0.14506912 0.12781143 0.13658524 0.14636612 0.11413193
0.11112976 0.13087845 0.14978409 0.11518478 0.14868546 0.1241951
0.11139297 0.12134171 0.11556244 0.11551285 0.14125443 0.06076431
0.13763809 0.0622139 0.12202835 0.14395523 0.12439346 0.15021133
0.10580826 0.13664627 0.09584427 0.10268402 0.15408707 0.12779617
0.09299088 0.13241959 0.13728714 0.14119339 0.10634232 0.10967636
0.08873367 0.13383865 0.10382462 0.10637283 0.12781906 0.1294899
0.11076736 0.12068558 0.12443161 0.10746384]
labels: 45247 0
127747 0
43295 0
25850 0
62422 0
..
665 0
101019 0
196 0
58710 0
102191 0
Name: label, Length: 100, dtype: int64
在整个训练集上的预测结果
from sklearn.metrics import mean_squared_error
data_predictions=lin_reg.predict(data_prepared)
lin_mse=mean_squared_error(data_labels,data_predictions)
lin_rmse=np.sqrt(lin_mse)
lin_rmse
0.3279736801232515
使用的是cross_val_score函数。反正书上给出了带负号的理由,那就用neg吧。从lin_rmse_score的结果看,拟合效果不错。
from sklearn.model_selection import cross_val_score
scores=cross_val_score(lin_reg,data_prepared,data_labels,scoring='neg_mean_squared_error',cv=10)
lin_rmse_score=np.sqrt(-scores)
lin_rmse_score
array([0.33027754, 0.32981625, 0.3325474 , 0.33003665, 0.3275183 ,
0.32668944, 0.3253184 , 0.32128751, 0.32691752, 0.32952707])
线性回归是一种很简单的算法,因此就没有搜索超参数的步骤了(因为就没有超参数)。但是不代表其他算法没有…
将测试集的数据经过前面的一套预处理后,再用线性回归算法得出一个预测结果。
X_test_prepared=full_pipeline.transform(test_set)
final_prediction=lin_reg.predict(X_test_prepared)
final_mse=mean_squared_error(y_test,final_prediction)
final_rmse=np.sqrt(final_mse)
final_rmse
0.32780615241094346
通过查看lin_reg.coef_可以发现,获得的参数相当离谱。
lin_reg.coef_
array([-6.22622660e-04, -7.69702530e-03, -1.18704161e-02, 1.52550113e-03,
-8.62294723e-04, 2.80572379e-03, -1.79606626e-03, -1.29128873e-03,
5.58111749e+09, 5.58111749e+09, -1.38026382e+11, -1.38026382e+11,
-1.38026382e+11, 1.11388033e+11, 1.11388033e+11])
最后将得到的预测值与测试集的给定结果y_test作比较,对于最小二乘法的结果给出评价。注意这里的y_test是Series,final_label是list,无法直接比较。
length_test=len(final_prediction)
final_label=[1 if final_prediction[i]>0.5 else 0 for i in range(length_test)]
Y_test=y_test.tolist()
final_percentage=[1 if final_label[i]==Y_test[i] else 0 for i in range(length_test)]
error_rate=1-sum(final_percentage)/length_test
error_rate
0.12306547619047614
注意到0.1230很眼熟,说明最小二乘法进行分类会得到很离谱的结果,最次也应该用个Logistic回归之类的算法。
当然,region和city区间的数据应该按照文本和分类属性做独热编码,实际没有做,对于预测结果也是有很大的影响的(个人可能会直接删掉这两个属性…)。
不过,本文的主要内容是算法运行的整个流程,采用的算法是否合适不是本文重点。对于python初学者来说,当然是算法流程调用函数类对象类型之类的东西更麻烦一些。