Kaggle ML Courses

Kaggle Courses

Intro to ML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import pandas as pd

# 数据检视
df = pd.read_csv(...)
df.describe()
df.head()
df.tail()
df.custom-columns

# 数据预处理
df = df.dropna()
y = df.target
feature_list = ['col1', 'col2', 'col3']
X = df[feature_list]
X.describe()
X.head()
X.tail()

# 数据建模分析 `sklearn`
from sklearn.tree import DecisionTree
model = DecisionTree(random_state=0)
model.fit(X,y)

# 模型评估调整 `sklearn.metrics`, `sklearn.model_selection`
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error

X_train, X_valid, y_train, y_valid = train_test_split(X, y, random_state = 0)
model = DecisionTree(random_state=0)
model.fit(X_train, y_train)

predictions = model.predict(X_valid)
mean_absolute_error(y_valid, X_valid)

def get_mae(max_leaf_nodes, X_train, y_train, X_valid, y_valid):
model = DecisionTree(max_leaf_nodes=max_leaf_nodes, random_state=0)
model.fit(X_train, y_trian)
prediction_valid = model.predict(X_valid)
return mean_absolute_error(y_valid, prediction_valid)

for max_leaf_nodes in [5, 50, 500, 5000]:
mae = get_mae(max_leaf_nodes, )
print(f"Max leaf nodes: {max_leaf_nodes} \t\t Mean Absolute Error: {mae:.0f}")

# 注:根据评估确定合适模型参数后,最终拟合应使用全部数据训练(训练集+验证集)

# 尝试其他模型
from sklearn.ensemble import RandomForestRegressor
model = RandomForestRegressor(random_state=0)
model.fit(X_train, y_trian)
prediction_valid = model.predict(X_valid)
print(mean_absolute_error(y_valid, prediction_valid))

# 模型预测
df_test = pd.read_csv(...)
X_test = df_test[feature_list]
prediction_test = model.predict(X_test)
output = pd.DataFrame({'Id': df_test.Id, 'target': prediction_test})
output.to_csv('submission.csv', index=False)

Pandas Course

创建、文件I/O、索引/筛选/访问、统计分析、外部函数、分组/排序、缺值处理、重命名、拼接合并

DataFrame
二维数据表,可理解为由{列标签(key): Series列向量(value)}组成的字典。相比Numpy二维数组多了行&列标签,且不同列的数据类型不要求一致(列为Series)。

1
2
3
4
5
6
7
8
9
import pandas as pd

df = pd.DataFrame({'Score': [99, 21], 'Grade': ['A+', 'C']})
df = pd.DataFrame({'Score': [99, 21], 'Grade': ['A+', 'C']},
index = ['Lily', 'Susan'])

pd.DataFrame({'Score': [99]})
pd.DataFrame({'Score': 99}) # Value Error
pd.DataFrame({'Score': 99, 'Grade':['A+', 'A+']})

Series
一维标签向量,相比1d-array多了行标签,数字索引及切片外,还可用标签访问。Series对应DataFrame中的列,有行标签,但无法添加列标签(可赋予整体上的名字name)。

1
2
s = pd.Series([1, 2, 3])
s = pd.Series([99, 21], index=['Lily', 'Susan'], name='Score')

文件数据 I/O

1
2
3
4
df = pd.read_csv("/path/to/csv/file", index_col=0)
# 若文本数据已包含行标签,可由index_col指定行标签所在列
df.head()
df.to_csv("/path/to/save/file")

数据访问筛选
DataFrame可理解为字典{col_label:Series},列可通过字典键值访问df[col_label]。符合语法前提下,列标签会被自动转为DateFrame的属性,可由属性操作符访问df.col。当列标签不符合Python变量命名规则,如为数字或含有空格等特殊字符,则无法转为属性。

1
2
3
4
5
df['Score']   # 列访问(标签)
df.Score # 列访问(属性)
df['Score']['Lily']
df[:1] # 行访问(切片)
df[df['Score']>60] # 行访问(逻辑判断)

虽然上述Python/Numpy操作方便直观,但实际代码更推荐使用经专门优化的pandas方法:.at[], .loc[]基于标签进行访问; .iat[], .iloc[]基于数字指标进行访问。
对比:iloc[]类似Numpy,上手更方便,loc[]则能利用标签名更清晰的传递代码含义;此外,loc[]支持布尔数组/逻辑判断筛选,而iloc[]不支持。 .isin(), .isnull()

1
2
3
4
5
6
7
8
df.loc['Lily']
df.loc[:, 'Score'] # loc[]可以用:,但不可以切片
df.loc[['Lily','Susan'], ['Grade', 'Score']]
df.loc[df['Score']>60]
df.at['Lily', 'Score']
df.iloc[0]
df.iloc[:, 0]
df.iat[0, 0]

注:DataFrame标签也支持切片,但不同于数字索引切片,标签切片会同时包含头和尾

1
2
df['Lily':'Susan']
df.loc['Lily':'Susan']

赋值
DataFrame支持直接将生成器赋值给数据列,而Python/Numpy需预先转换

统计分析

1
2
3
4
.describe()
.mean()
.unique()
.value_counts()

自定义函数

1
2
3
4
5
s.map(func-like)    # 逐元素操作,仅支持单列(Series)
df.applymap(func) # 整个表逐元素操作
df.apply(func, axis=0, args=(), **kwds) # 整个表逐行/列操作
df.pipe(func, *args, **kwargs) # 函数串联
df.agg(func, axis=0, *args, **kwargs), df.transform() # 函数并联

分组与排序
df.groupby()分组函数返回的是一个特殊的DataFrameGroupBy对象,其后通常需要跟数据分析操作或应用其他自定义函数操作,并最终返回普通DataFrame或/Series对象。

1
2
3
4
5
6
7
8
df.groupby('Grade').count()  # 类似df.Grade.value_counts(),但后者返回Series

# 基于多列数据分组时会出现多重索引/层级索引 `MultiIndex`
# df.reset_index() 将索引恢复默认,原索引会被重置为数据列(MultiIndex变为多列数据)

# groupby分组数据默认依然是按索引排序,可由以下函数调整
df.sort_index(axis=0, ascending=True)
df.sort_values(by, axis=0, ascending=True)

缺失值处理

1
2
3
4
5
6
7
8
9
10
# .dtype属性可查看DataFrame数据列/Series的数据类型,.astype()方法可显式转换数据类型
# 注意:字符串数据列的数据类型默认为`object`,此外pandas提供时间序列/类别等额外类型
# 而基于pandas处理Nan的实现,Nan被作为浮点型处理

df.isnull() # Nan判断 `df.notnull()`
df.fillna(value=None, method=None, axis=None) # 填充
df.dropna(axis=0, how='any') # 移除
df.replace(to_replace=None, value=None, regex=False) # 替换
# `df.replace()`可用于应对"Unknown", "Undisclosed", "Invalid"等非Nan的缺失值标识
# to_replace可以是要被替换的值(数字/字符串)或正则表达式,可以是单值或列表

重命名

1
2
3
4
5
6
7
# df.rename(mapper=None, index=None, columns=None, axis=None)
df.rename(columns={'Score': 'Points'})
df.rename(index={'Lily': 'Lucy'})

# df.set_index(<col>) 指定某列数据为行指标,指定多列时生成MultiIndex
# df.reset_index() 将索引恢复默认,原索引会被重置为数据列(MultiIndex变为多列数据)
# df.rename_axis() 修改表头(axis)名,即 行标签所在列 或 列标签所在行 的名字

拼接合并

1
2
3
4
5
6
7
8
9
10
11
12
# pd.concat([df1, df2], axis=0)   最简单,行或列拼接
# df.join(df2, on=None, how='left', lsuffix='', rsuffix='') 列拼接合并
# df.merge() 最复杂的拼接及数据合并,具体参考函数文档,通常用join即可
# df.combine(df2, func, fill_value=None) 逐列合并,相同列根据func保留一个值
# `join`合并时需指定`df2`索引所对齐的列(`on`),默认为索引对索引
df_1 = pd.DataFrame({'Id': [0, 1, 2, 3, 4],
'Score':[90, 89, 40, 98, 80]})
df_2 = pd.DataFrame({'Id': [0, 1, 2, 3, 4],
'Grade':['A', 'B+', 'D', 'A+', 'B']})
df_1.join(df_2, lsuffix='_l', rsuffix='_r') # 按索引对齐(存在列冲突)
df_1.set_index('Id').join(df_2.set_index('Id')) # Id转为索引,并按索引对齐
df_1.join(df_2.set_index('Id'), on='Id') # 按Id对齐:df_2(Id->Index), on='Id'

更多pandas操作介绍可参考pandas快速入门

Intermediate ML

数据缺失、类别变量、Pipeline、交叉验证、XGBoost、数据泄露

缺值处理

  • 忽略存在缺失值的列(或行?) 除非缺值过多否则不推荐,会损失有效信息
  • 缺值填充:sklearn.impute SimpleImputer, KNNImputer
  • 标记填充:填充之外追加对应列标记缺值 MissingIndicator
1
2
3
4
5
6
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy='median')
imputer.fit(X) # 拟合
imputer.transform(X) # 转换(填充)
imputer.fit_transform(X) # 拟合+转换
imputer.inverse_transform(X) # 还原

注意:sklearn默认是配合Numpy数组的,因此返回值默认是Numpy数组。配合pandas使用时,需手动将返回的Numpy数组转换为DataFrame(标签也需手动加回)。

1
2
3
X_imputed = pd.DataFrame(imputer.transform(X),
index = X.index, columns=X.custom-columns)
# 列标签用于标识特征,行标签用于标识记录。

如果模型未涉及标签引用,则标签不加也无所谓,对结果没影响。但若涉及数据表合并操作,则标签对于数据对齐很关键,需小心处理。下面示例中使用MissingIndicator进行数据预处理(标记+填充)时,行标签的添加是必须的。

数据预处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import pandas as pd
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split

# 读入数据
data = pd.read_csv('../input/train.csv', index_col='Id')
data_test = pd.read_csv('../input/test.csv', index_col='Id')

# 忽略训练集中没有y值的数据,拆分X,y
data.dropna(axis=0, subset=['SalePrice'], inplace=True)
y = data.pop('Price')
# y = data.Price; data.drop(['Price'], axis=1, inplace=True)

# 忽略取值为字符串的特征
X = data.select_dtypes(exclude=['object'])
X_test = data_test.select_dtypes(exclude=['object'])

# 拆分训练集与测试集
X_train, X_valid, y_train, y_valid = train_test_split(X, y, \
train_size=0.8, test_size=0.2, random_state=0)

# 特征移除(.dropna()??)
cols_null = X.custom-columns[X.isnull().any()]
X_train_reduced = X_train.drop(columns=cols_null)
X_valid_reduced = X_valid.drop(columns=cols_null)
X_reduced = X.drop(columns=cols_null) # train + valid

# 数据填充(imputation)
# 通常数据填充要比移除特征效果好,效果不好的原因之一可能是填充的值不合理。
imputer = SimpleImputer(strategy='median')
X_train_imputed = pd.DataFrame(imputer.fit_transform(X_train))
X_valid_imputed = pd.DataFrame(imputer.transform(X_valid))
X_imputed = pd.DataFrame(imputer.transform(X)) # train + valid

# 标记+填充
cols_null = X.custom-columns[X.isnull().any()]
X_train_ind = X_train.join(X_train[cols_null].isnull(), rsuffix='_indicator')
X_valid_ind = X_valid.join(X_valid[cols_null].isnull(), rsuffix='_indicator')
X_ind = X.join(X[cols_null], rsuffix='_indicator')
# from sklearn.impute import MissingIndicator
# indicator = MissingIndicator().fit(X)
# transform = lambda df: pd.DataFrame(indicator.transform(df), index=df.index)
# X_train_ind = X_train.join(transform(X_train))
# X_valid_ind = X_valid.join(transform(X_valid))
# X_ind = X.join(transform(X))
imputer = SimpleImputer(strategy='median')
X_train_ind_imputed = pd.DataFrame(imputer.fit_transform(X_train_ind))
X_valid_ind_imputed = pd.DataFrame(imputer.transform(X_valid_ind))
X_ind_imputed = pd.DataFrame(imputer.transform(X_ind))

类别数据

  • 忽略类别变量 除非类别变量与目标无关,否则不推荐
  • 序编码(数字编码):sklearn.preprocessing OrdinalEncoder 编码为整数序号
    只对于有序变量(ordinal variables)有意义,树模型处理有序变量时通常会采用序编码
  • 独热编码:用基向量编码类别,可用于名义变量(nominal variables) OneHotEncoder
    注意,随着类别增加,独热编码所需维度也将增加,计算难度随之上升(维度诅咒)

注1:sklearn中目标值类别(y)的序编码器为LabelEncoder、独热编码器为LabelBinarizer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder
# encoder = OrdinalEncoder()
encoder = OneHotEncoder(sparse=False, handle_unknown='ignore')
# 返回值默认为稀疏矩阵(scipy.sparse.csr.csr_matrix),sparse=False返回Numpy数组

X_categorical = X.select_dtypes('object') # 类别数据(字符串数据)
# cols_categorical = X.custom-columns[X.dtypes=='object']
# X[cols_categorical].nunique().sort_values() # 返回所有类别数据特征的分类数
# cols_low_cardinality = cols_categorical[X[cols_categorical].nunique()<=10]
cols_low_cardinality = X.custom-columns[(X.dtypes=='object') & (X.nunique()<=10)]
X_categorical = X.loc[:, cols_low_cardinality] # 分类数<=10的类别数据特征

# 编码前需进行缺值处理,简单的可使用SimpleImputer的'most_frequent'/'constant'策略
encoder.fit(X_categorical) # 拟合
encoder.transform(X_categorical) # 转换(编码)
encoder.fit_transform(X_categorical) # 拟合+转换
encoder.inverse_transform(X_categorical) # 还原

# X.loc[:, cols_categorical] = encoder.transform(X_categorical)
# One-hot编码时会引入新的列,不能像上面直接赋值,需要合并数据表
code_categorical = pd.DataFrame(encoder.transform(X_categorical),
index = X_categorical.index)
X = X.drop(cols_categorical, axis=1).join(code_categorical)

问题1:独热编码中类别数据的每个分类都将作为独立特征(在DataFrame中引入一列),而最终增加的数据条目还要乘以记录数(行数)。因此当某个类别数据的种类/基数(cardinality)特别多时,将引入巨大的稀疏矩阵。相比之下,序编码只是将原数据条目替换为数字,并不增加数据表维度。因此为了避免维度诅咒,对于种类/基数过多的类别,通常会避免独热编码,而采用序编码或直接忽略。
问题2:数据分为训练集、验证集之后,某些特征可能会出现验证集中存在测试集中未包含的类别,导致编码失败。最简单粗暴的做法是忽略这些类别,此时除了像下面手动筛选特征外,还可简单设置编码器参数handle_unknown='ignore'

1
2
3
4
5
cols_categorical = X.custom-columns[X.dtypes=='object']
cols_choosen = [col for col in cols_categorical if
set(X_valid[col]).issubset(set(X_train[col]))]
# 拟合的时候代入全部数据(训练集+验证集)是否可以?是否存在数据泄露?存在!?
# 如果预先知道所有可能的类别,是该按全部类别还是训练集中存在的类别编码??

问题3:Pandas也支持方便快捷的独热编码(pd.get_dummies()),但建立机器学习模型时仍推荐使用sklearn,原因就在于问题2,具体可参考这里,不过也有一些技巧解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
X_train = pd.get_dummies(X_train)
X_valid = pd.get_dummies(X_valid)
X_train, X_valid = X_train.align(X_valid, join='left', axis=1, fill_value=0)
# 分开编码如何保证编码一致性:独热编码之后值为具体某类的记录只有相应的特征列取1
# 按列左对齐后,将Nan填充为0就可保证编码一致,左表中不含的类别值的编码会变为全0
df_1 = pd.DataFrame({'Score': [90, 89, 59, 98], 'Grade': ['A', 'B+', 'C', 'A+']})
df_2 = pd.DataFrame({'Score': [99, 21], 'Grade': ['A+', 'D']})
df1 = pd.get_dummies(df_1)
df2 = pd.get_dummies(df_2)
df1, df2 = df1.align(df2, join='left', axis=1, fill_value=0)
# 或者可先将类别数据('object'类型)转换为类别确定的'category'类型,再get_dummines
# https://stackoverflow.com/questions/37425961
# https://gist.github.com/psinger/ef4592492dc8edf101130f0bf32f5ff9
cat_types = pd.CategoricalDtype(categories=['A+', 'A', 'B+', 'B', 'C']) # 限定类别
df1 = pd.get_dummies(df_1.astype({'Grade': cat_types}))
df2 = pd.get_dummies(df_2.astype({'Grade': cat_types}))

Pipelines

预处理流程封装(数值数据+类别数据) → 模型定义 → 组装运行fit, predict

  • 基于Pipeline,只需定义预处理所需执行操作(缺值填充、数据编码等),而无需手动执行,训练模型时直接代入原始数据,相关预处理操作会被自动调用执行。
  • 类似的,代入验证集、测试集进行预测时,相关预处理操作同样会自动执行。
  • Pipeline可使代码更清晰,避免遗漏预处理,也便于后续的模型验证及部署。

sklearn.pipeline:Pipeline(串联), FeatureUnion(并联)
sklearn.compose:ColumnTransformer(列选择+并联), TransformedTargetRegressor(y)

  • Pipeline(steps) 串联多个数据变换操作并最终传递给transformer/estimator
  • FeatureUnion(transformer_list) 合并(并联)多个数据变换/特征提取操作
  • ColumnTransformer(transformers, remainder='drop') 对指定的数据列执行各自变换
  • TransformedTargetRegressor() 用于回归问题中目标值(y)的非线性变换

管道(Pipeline)、特征联合(FeatureUnion)及列变换(ColumnTransformer)的首个参数均为元组列表,前两者元组为命名加变换,后者多了列筛选参数(name:str, transformer, cols)
列变换的transformer除了正常变换(支持fit+transform),还可取'drop'/'passthrough',用于指定要移除或保持原样的特征列;而所有元组的cols参数都未涉及的剩余列的操作由reminder指定,同样可取正常变换, 'drop''passthrough',默认会被移除('drop')。
最后列变换为并联操作,返回特征的列顺序由列表中变换的顺序决定,不同变换涉及同一数据列,会返回各自独立变换后的特征,而非串联操作。对于需要串联操作的列变换可先用管道封装,再加入列变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder
from sklearn.ensemble import RandomForestRegressor

cols_cat = X.custom-columns[(X.dtypes=='object') & (X.nunique()<=10)]
cols_num = X.select_dtypes('number').custom-columns
# X.dtypes.isin(['int64', 'float64']) 存在Bug
# https://stackoverflow.com/questions/56485323/
# cols_select = cols_cat.to_list() + cols_num.to_list()
# X_train = X_train[cols_select]
# X_valid = X_valid[cols_select]

# 预处理流程(preprecessor: Pipeline, ColumnTransformer)
numerical_transformer = SimpleImputer(strategy='median') # 数值数据
categorical_transformer = Pipeline(steps=[ # 类别数据
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore')) ])
preprocessor = ColumnTransformer(transformers=[ # 预处理封装
('num', numerical_transformer, cols_num),
('cat', categorical_transformer, cols_cat) ])
# 注意这里cols_num, cols_cat限定了预处理所应用的数据列

# 模型(model)
model = RandomForestRegressor(n_estimators=100, random_state=0)

# 组装(pipeline: Pipeline)
pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('model', model) ])

# 运行
pipeline.fit(X_train, y) # train
pipeline.predict(X_valid) # predict

交叉验证
cross_validation
交叉验证可以提供更准确的模型评估(尤其是超参数较多时),但也增加了时间开销。当数据量大时交叉验证成本较高,同时也不是很必要,因此通常的做法为数据量相对少时采取交叉验证。此外如果交叉验证中每次的结果都很接近,通常意味着单次验证就够了。

1
2
3
4
5
6
from sklearn.model_selection import cross_val_score

scores = cross_val_score(pipeline, X, y, cv=5, scoring='neg_mean_absolute_error')
print("Average MAE score:", -scores.mean())
# scoring参数为模型评价函数,这里为负的MAE
# 取负是因为scikit-learn统一默认值越大模型越好

XGBoost

梯度提升(Gradient Boosting) sklearn.ensemble, XGBoost, LightGBM, CatBoost

History of XGBoost

梯度提升属于集成学习方法,后者原理为基于一定策略融合多个小模型,提升模型整体决策鲁棒性或预测精度。其中模型整合策略主要有平均(Averaging)和提升(Boosting)两种:前者即多个模型民主投票,如基于Bagging(Bootstrap Aggregating)的随机森林(RF)算法;后者是指每个模型都建立在前一模型基础之上,迭代提升。Bagging通过平均增强模型鲁棒性(↓variance),而Boosting是迭代优化Loss(↓bias)。还有一个集成策略是在多个模型之上堆叠(Stacking)一个更高层的模型,并将底层模型输出作为输入,用于最终决策。
具体的,梯度提升中:“提升”大致为先训练一个基础模型做粗略预测,计算预测的Loss,之后以前一模型预测的残差为目标,追加新的模型进行修正,重新计算Loss,再叠加新的模型修正,依次迭代,提升模型预测精度,具体可参考这个视频;而“梯度”是指提升时利用梯度下降算法最小化预测的误差。XGBoost、LightGBM、CatBoost属于梯度提升机(GBM)或者说梯度提升决策树(GBDT)的不同实现,后两者主要在速度上有较明显提升,且都可以自动应对分类特征,其中CatBoost更是以处理类别特征而闻名。

1
2
3
4
5
6
7
8
9
10
11
12
from xgboost import XGBRegressor
model = XGBRegressor(n_estimators=100, learning_rate=0.3, random_state=0)
# model.get_params()查看各当前值(几乎全部是None,不是很理解...)
# n_estimators:树叠加的数量上限(默认100),应与学习率配合调整(欠拟合 v.s. 过拟合)
# learning_rate:学习率,压低后继模型贡献权重,取值区间[0, 1],默认为0.3
# n_jobs:数据量较大时可开启并行,通常取CPU核数(默认为最大进程数??)
model.fit(X_train, y_train, early_stopping_rounds=5, eval_set=[(X_valid, y_valid)])
# early_stopping_rounds:模型验证连续变差指定次数后终止训练,需配合验证集(eval_set)
# 相应的 eval_metric 对回归器默认为均方根误差RMSE(分类器为分类错误率) mae, logloss
# 为避免随机性造成误判,通常取总循环次数(n_estimators)的10%,如10次左右
# 放宽n_estimators的限制,可实现根据模型验证效果自动判定训练终止时机
model.predict(X_valid)

数据泄露

  • 目标信息泄露(Target leakage):预测时使用了当下未知的预测目标直接相关信息
  • 训练数据污染(Train-test contamination):训练、测试集划分前就进行了数据预处理

预测癌症时使用抗癌药物服用信息或就诊医院信息属于目标信息泄露,又比如使用普通K折交叉验证检验时序预测模型(有专门的TimeSeriesSplit)。过早特征化属于测试数据泄露,数据划分前就进行特征提取、转换、降维、选择等操作;又或者数据划分前就进行自助采样(bootstrap)或数据增强,致使训练集与测试集包含相同数据或基于同一数据的变换。在模型验证前缺值填充、特征变换等操作的拟合都只能用于训练集,不能包含验证集(及测试集),验证后、测试前可同时拟合训练集+验证集吗???
此外维基百科中的 group leakage 部分提供了一个较难察觉的实例,当数据非独立同分布,内部存在确定的强关联(但未被留意),此时随机划分测试集与验证集,验证集与测试集间就会存在信息泄露。

注意:对于某些时序预测,如股市价格,若每日价格波动有限(限制涨跌),则当天价格就会是次日价格的很好估计,因此即便价格预测准确率很高,对于交易而言可能也并无帮助。对于后者能准确预测每日涨跌及幅度的模型才是有价值的,因此更合理的预测目标是每日的价格波动,而非价格本身。

Data Visualization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd
pd.plotting.register_matplotlib_converters()
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

data = pd.read_csv(data_filepath, index_col="Date", parse_dates=True)

# 线图用于显示变化趋势,散点图用于发现关联,分布图用于查看统计信息
# plt.figure(figsize=(16,6))
sns.lineplot(data=data) # data.plot() | data.plot.line()
sns.barplot(data=data.col.to_frame().T) # data.col.plot.bar()
sns.heatmap(data=data, annot=True)
sns.scatterplot(x='col1', y='col2', hue='col3', data=data) # hue用于分组
sns.regplot(x='col1', y='col2', data=data) # 散点 + 线性回归拟合
sns.lmplot(x='col1', y='col2', hue='col3', data=data) # 散点 + 分组回归
sns.swarmplot(x='col1', y='col2', data=data) # 分簇散点图
sns.histplot(data, x='col') # data.col.plot.hist()
sns.kdeplot(data.col) # data.col.plot.kde()
sns.jointplot(x='col1', y='col2', data=data, kind='kde') # 联合分布
# sns.set_theme(), sns.set_style()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
import plotly.express as px
# pd.set_option('plotting.backend', 'plotly')

# https://plotly.com/python/plotly-express/
px.line(data) # data.plot() | data.plot.line()
px.bar(data.col) # data.col.plot.bar()
px.imshow(data, text_auto=True)
px.scatter(data, x='col1', y='col2', color='col3') # data.plot.scatter()
px.scatter(data, x='col1', y='col2', trendline='ols')
px.scatter(data, x='col1', y='col2', color='col3', trendline='ols')
px.strip(data, x='col1', y='col2')
px.histogram(data, x='col') # data.col.plot.hist()
px.density_contour(data, x="col1", y="col2",
marginal_x="histogram", marginal_y="histogram")
# px.defaults.template = 'ggplot2' # 'seaborn', 'presentation', ...
# https://plotly.com/python/figure-factories/
# Plotly的Express模块尚不支持KDE plot,需借助旧的Figure Factory模块
# 注意:distplot默认支持多组数据,参数hist_data, group_labels为列表
import plotly.figure_factory as ff
ff.create_distplot(hist_data=[data.col], group_labels=['col'],
show_hist=False, show_rug=False)

Feature Engineering

关键特征识别(互信息)、新特征引入、高基数类别特征处理(目标编码)、聚类特征(K-means)、(PCA)
更具体的可参考scikit-learn文档

特征工程就是通过综合现有特征信息,通过特征的变换或降维等改善模型:

  • 提升模型预测表现
  • 减少数据量/计算量
  • 提升结果可解释性

如体感温度就是综合了温度、湿度、风力等信息的指标,可以更好的传递人们主观的温度感受。而机器学习模型需要学习特征变量与目标变量的依赖关系,当模型不能有效学习的关系时,可以通过特征变换加以调整。比如利用线性模型处理非线性依赖问题时,就可通过特征变换使新的特征与目标呈现线性关系。最后,进行特征工程时,基于原始数据的基准模型有助于我们判断新引入的特征是否有效。

特征工程的常见技术有:

  • 数据清洗:数据缺失、重复、偏置等异常情况
    缺值处理、日期处理、类别编码、异常检测、错误修正
    检测空数值或缺失行
    搜索重复行并将其移除
    基于统计技术,利用专业知识发现离群点
    确定列的分布情况,并剔除没有方差的列
    如果其方差很小,则意味着其识别能力较弱。极端情况下其方差为0,这意味着该属性在所有样本上的值都是恒定的
    当特征值都是离散型变量的时候这种方法才能用,如果是连续型变量,就需要将连续变量离散化之后才能用。
  • 特征编码:
    目标编码、证据权重(WoE)编码、均值编码??? (sklearn.preprocessing)
  • 特征选择:对数据的已有特征进行重要性排序,剔除不重要的特征
    互信息、通过随机森林或 XGBoost区分特征的重要性(sklearn.feature_selection)
  • 特征变换:基于现有特征的组合变换构建新特征
    基础变换(标准化/归一化)、数学变换(e.g.取对数)、特征拆分/合并、离散化(分箱bining)、二值化、(横向)分组聚合、(纵向)二值特征计数(count) (pandas, sklearn.preprocessing)
  • 特征提取:聚类分析
  • 数据降维:将数据投影到低维空间,并保持数据的核心特性(Embedding)
    主成分分析(PCA)、线性判别分析(LDA)、局部线性嵌入(LLE) (sklearn.decomposition, sklearn.lda, sklearn.manifold)
    sklearn.feature_extraction???

特征分割、组合稀疏类

特征选择

  • 过滤式(Filter method):使用模型无关的统计指标对特征排序筛选(Filter)
    评价指标通常为单个特征与预测目标的关联度(univariate scores):如信息增益/互信息(mutual_info_classif, mutual_info_regression)、 Pearson相关系数(r_regression)、卡方检验(chi2)、F检验(f_classif, f_regression)等。而相应的选择策略有简单的指定数量(KBest)/百分比(Percentile)及指定显著性水平(计算p值)的Fpr, Fdr, Fwe等。
  • 封装式(Wrapper method):对学习模型进行封装(Wrapper)用于特征评价筛选
    最简单的是基于不同输入特征下,模型对验证集的预测表现筛选特征。通常会采用贪心搜索,正向逐个添加或反向逐个排除(SequentialFeatureSelector, SFS),反复训练模型并交叉验证,更具体解释可参考西瓜书。此外也可尝试遗传算法等启发式搜索。
    另一种相对高效的做法是基于模型训练后特征的重要性(feature_importances_)或权重(coef_)逐个移除,被称为递归特征消除(Recursive Feature Elimination, RFE),可结合交叉验证(CV)自动确定最佳特征数RFECV。当前特征数为m的模型,k折交叉验证,SFS删除特征需m*k次训练,而RFE只需一次训练(RFECV要k次),但相对的它需要模型提供coef_, feature_importances_或其他特征评价函数。
  • 嵌入式(Embedded method):特征选择直接内嵌(Embedded)在学习模型内
    模型在优化损失的同时自动实现特征筛选,最典型的就是L1正则以及决策树模型。这类学习模型除了直接用于最终预测,也可专门用作特征选择(SelectFromModel),将得到的特征用于训练其他更复杂的模型。

过滤式特征选择不依赖预测模型,通常速度更快,且所选择特征更为通用,避免过拟合,但也可能错过适合模型的最佳特征,降低模型表现。此外,如果特征筛选指标只考虑特征与预测目标间关联,而未考虑特征间关联,可能最终得到的特征存在冗余关联。特征排序除了直接筛选特征,也可结合交叉验证确定最佳特征数,或者作为封装式筛选方法的初步筛选。封装式特征选择依赖于学习模型,能够自动处理特征间的关联,但需要反复训练和检验模型,计算量更大,此外结果也易出现过拟合。大致而言,过滤式、嵌入式、封装式,在计算量上依次增加,但由于针对性的调校,在效果上通常也依次提升。

https://zhuanlan.zhihu.com/p/67475635
https://zhuanlan.zhihu.com/p/68259539

Data Cleaning

Explainability

Time Series

Geography

EDA

SQL