Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明
Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明
目录
Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明
一、简单介绍
二、单变量非线性变换
三、自动化特征选择
四、利用专家知识
附录
一、参考文献
一、简单介绍
Python是一种跨平台的计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越多被用于独立的、大型项目的开发。Python是一种解释型脚本语言,可以应用于以下领域: Web 和 Internet开发、科学计算和统计、人工智能、教育、桌面界面开发、软件开发、后端开发、网络爬虫。
Python 机器学习是利用 Python 编程语言中的各种工具和库来实现机器学习算法和技术的过程。Python 是一种功能强大且易于学习和使用的编程语言,因此成为了机器学习领域的首选语言之一。Python 提供了丰富的机器学习库,如Scikit-learn、TensorFlow、Keras、PyTorch等,这些库包含了许多常用的机器学习算法和深度学习框架,使得开发者能够快速实现、测试和部署各种机器学习模型。
Python 机器学习涵盖了许多任务和技术,包括但不限于:
- 监督学习:包括分类、回归等任务。
- 无监督学习:如聚类、降维等。
- 半监督学习:结合了有监督和无监督学习的技术。
- 强化学习:通过与环境的交互学习来优化决策策略。
- 深度学习:利用深度神经网络进行学习和预测。
通过 Python 进行机器学习,开发者可以利用其丰富的工具和库来处理数据、构建模型、评估模型性能,并将模型部署到实际应用中。Python 的易用性和庞大的社区支持使得机器学习在各个领域都得到了广泛的应用和发展。
二、单变量非线性变换
在机器学习中的特征工程中,单变量非线性变换是一种将单个特征应用非线性函数的技术,以便提高模型性能或满足模型假设。这些变换有助于处理特征与目标变量之间的非线性关系。常见的单变量非线性变换包括对数变换、平方根变换、平方变换、指数变换等。
下面是一些常用的单变量非线性变换的示例:
1)示例:对数变换
对数变换常用于将具有右偏分布的数据拉近正态分布。它在处理正数数据时特别有用。
import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split# 加载加州房价数据集 california = fetch_california_housing() X_train, X_test, y_train, y_test = train_test_split(california.data, california.target, random_state=0)# 选择一个特征进行对数变换 feature = X_train[:, 0] # 'MedInc' 特征 log_feature = np.log(feature + 1) # 加1以避免对数0的情况# 绘制原始特征和对数变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(log_feature, bins=30) plt.title("Log-transformed Feature") plt.show()
2)示例:平方根变换
平方根变换用于减弱特征中较大的数值的影响。它在处理计数数据时特别有用。
sqrt_feature = np.sqrt(feature)# 绘制原始特征和平方根变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(sqrt_feature, bins=30) plt.title("Square-root-transformed Feature") plt.show()
3)示例:平方变换
平方变换用于增加特征的非线性性。它在处理特征与目标之间具有二次关系的数据时有用。
square_feature = np.square(feature)# 绘制原始特征和平方变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(square_feature, bins=30) plt.title("Square-transformed Feature") plt.show()
4)示例:指数变换
指数变换用于增强特征中较小的数值的影响。
exp_feature = np.exp(feature)# 绘制原始特征和指数变换后的特征 plt.figure(figsize=(12, 6)) plt.subplot(1, 2, 1) plt.hist(feature, bins=30) plt.title("Original Feature") plt.subplot(1, 2, 2) plt.hist(exp_feature, bins=30) plt.title("Exponential-transformed Feature") plt.show()
5)将非线性变换应用到所有特征
如果你想对数据集中的所有特征进行非线性变换,可以使用
FunctionTransformer
。from sklearn.preprocessing import FunctionTransformer# 定义对数变换的函数 log_transformer = FunctionTransformer(np.log1p, validate=True)# 应用对数变换到所有特征 X_train_log = log_transformer.transform(X_train) X_test_log = log_transformer.transform(X_test)print("Original shape:", X_train.shape) print("Log-transformed shape:", X_train_log.shape)
这些非线性变换可以帮助捕捉特征和目标变量之间的复杂关系,从而提高模型的性能。选择适当的变换方法取决于数据的分布和模型的需求。
添加特征的平方或立方可以改进线性回归模型。其他变换通常也对变换某些特征有用,特别是应用数学函数,比如 log
、exp
或 sin
。虽然基于树的模型只关注特征的顺序,但线性模型和神经网络依赖于每个特征的尺度和分布。如果在特征和目标之间存在非线性关系,那么建模就变得非常困难,特别是对于回归问题。log
和 exp
函数可以帮助调节数据的相对比例,从而改进线性模型或神经网络的学习效果。我们之前对内存价格数据应用过这种函数。在处理具有周期性模式的数据时,sin
和 cos
函数非常有用。
大部分模型都在每个特征(在回归问题中还包括目标值)大致遵循高斯分布时表现最好,也就是说,每个特征的直方图应该具有类似于熟悉的“钟形曲线”的形状。使用诸如 log
和 exp
之类的变换并不稀奇,但却是实现这一点的简单又有效的方法。在一种特别常见的情况下,这样的变换非常有用,就是处理整数计数数据时。计数数据是指类似“用户 A 多长时间登录一次?”这样的特征。计数不可能取负值,并且通常遵循特定的统计模式。下面我们使用一个模拟的计数数据集,其性质与在自然状态下能找到的数据集类似。特征全都是整数值,而响应是连续的:
import numpy as nprnd = np.random.RandomState(0)
X_org = rnd.normal(size=(1000, 3))
w = rnd.normal(size=3)X = rnd.poisson(10 * np.exp(X_org))
y = np.dot(X_org, w)
我们来看一下第一个特征的前 10 个元素。它们都是正整数,但除此之外很难找出特定的模式。
如果我们计算每个值的出现次数,那么数值的分布将变得更清楚:
print("Number of feature appearances:\n{}".format(np.bincount(X[:, 0])))
Number of feature appearances: [28 38 68 48 61 59 45 56 37 40 35 34 36 26 23 26 27 21 23 23 18 21 10 917 9 7 14 12 7 3 8 4 5 5 3 4 2 4 1 1 3 2 5 3 8 2 52 1 2 3 3 2 2 3 3 0 1 2 1 0 0 3 1 0 0 0 1 3 0 10 2 0 1 1 0 0 0 0 1 0 0 2 2 0 1 1 0 0 0 0 1 1 00 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 01 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1]
数字 2 似乎是最常见的,共出现了 68 次(bincount
始终从 0 开始),更大数字的出现次数快速下降。但也有一些很大的数字,比如 134 出现了 2 次 (这里 134 实际的出现次数是 0,但 84 和 85 的出现次数是 2,作者想要表达的意思是没错的) 。我们在图 4-7 中将计数可视化。
bins = np.bincount(X[:, 0])
plt.bar(range(len(bins)), bins, color='b')
plt.ylabel("Number of appearances")
plt.xlabel("Value")plt.tight_layout()
plt.savefig('Images/04UnivariateNonlinearTransformation-01.png', bbox_inches='tight')
plt.show()

特征 X[:, 1]
和 X[:, 2]
具有类似的性质。这种类型的数值分布(许多较小的值和一些非常大的值)在实践中非常常见(这是泊松分布,对计数数据相当重要)。 但大多数线性模型无法很好地处理这种数据。我们尝试拟合一个岭回归模型:
from sklearn.linear_model import Ridge
from sklearn.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
score = Ridge().fit(X_train, y_train).score(X_test, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.622
你可以从相对较小的 分数中看出,
Ridge
无法真正捕捉到 X
和 y
之间的关系。不过应用对数变换可能有用。由于数据取值中包括 0(对数在 0 处没有定义),所以我们不能直接应用 log
,而是要计算 log(X + 1)
:
X_train_log = np.log(X_train + 1)
X_test_log = np.log(X_test + 1)
变换之后,数据分布的不对称性变小,也不再有非常大的异常值(见图 4-8):
plt.hist(X_train_log[:, 0], bins=25, color='b')
plt.ylabel("Number of appearances")
plt.xlabel("Value")plt.tight_layout()
plt.savefig('Images/04UnivariateNonlinearTransformation-02.png', bbox_inches='tight')
plt.show()

在新数据上构建一个岭回归模型,可以得到更好的拟合:
score = Ridge().fit(X_train_log, y_train).score(X_test_log, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.875
为数据集和模型的所有组合寻找最佳变换,这在某种程度上是一门艺术。在这个例子中,所有特征都具有相同的性质,这在实践中是非常少见的情况。通常来说,只有一部分特征应该进行变换,有时每个特征的变换方式也各不相同。前面提到过,对基于树的模型而言,这种变换并不重要,但对线性模型来说可能至关重要。对回归的目标变量 y
进行变换有时也是一个好主意。尝试预测计数(比如订单数量)是一项相当常见的任务,而且使用 log(y + 1)
变换也往往有用。(这是对泊松分布非常粗略的近似,而从概率的角度来看,这是正确的解决方法)
从前面的例子中可以看出,分箱、多项式和交互项都对模型在给定数据集上的性能有很大影响,对于复杂度较低的模型更是这样,比如线性模型和朴素贝叶斯模型。与之相反,基于树的模型通常能够自己发现重要的交互项,大多数情况下不需要显式地变换数据。其他模型,比如 SVM、最近邻和神经网络,有时可能会从使用分箱、交互项或多项式中受益,但其效果通常不如线性模型那么明显。
三、自动化特征选择
自动化特征选择是机器学习中用于提高模型性能和减少模型复杂性的重要步骤。通过自动化特征选择,我们可以选择对模型预测有显著影响的特征,去除冗余或不相关的特征,从而提高模型的泛化能力和训练效率。
有了这么多种创建新特征的方法,你可能会想要增大数据的维度,使其远大于原始特征的数量。但是,添加更多特征会使所有模型变得更加复杂,从而增大过拟合的可能性。在添加新特征或处理一般的高维数据集时,最好将特征的数量减少到只包含最有用的那些特征,并删除其余特征。这样会得到泛化能力更好、更简单的模型。但你如何判断每个特征的作用有多大呢?有三种基本的策略:单变量统计 (univariate statistics)、基于模型的选择 (model-based selection)和迭代选择 (iterative selection)。我们将详细讨论这三种策略。所有这些方法都是监督方法,即它们需要目标值来拟合模型。这也就是说,我们需要将数据划分为训练集和测试集,并只在训练集上拟合特征选择。
1、单变量统计
在单变量统计中,我们计算每个特征和目标值之间的关系是否存在统计显著性,然后选择具有最高置信度的特征。对于分类问题,这也被称为方差分析 (analysis of variance,ANOVA)。这些测试的一个关键性质就是它们是单变量的 (univariate),即它们只单独考虑每个特征。因此,如果一个特征只有在与另一个特征合并时才具有信息量,那么这个特征将被舍弃。单变量测试的计算速度通常很快,并且不需要构建模型。另一方面,它们完全独立于你可能想要在特征选择之后应用的模型。
想要在 scikit-learn
中使用单变量特征选择,你需要选择一项测试——对分类问题通常是 f_classif
(默认值),对回归问题通常是 f_regression
——然后基于测试中确定的 p 值来选择一种舍弃特征的方法。所有舍弃参数的方法都使用阈值来舍弃所有 p 值过大的特征(意味着它们不可能与目标值相关)。计算阈值的方法各有不同,最简单的是 SelectKBest
和 SelectPercentile
,前者选择固定数量的 k 个特征,后者选择固定百分比的特征。我们将分类的特征选择应用于 cancer
数据集。为了使任务更难一点,我们将向数据中添加一些没有信息量的噪声特征。我们期望特征选择能能够识别没有信息量的特征并删除它们:
from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectPercentile
from sklearn.model_selection import train_test_split
import numpy as npcancer = load_breast_cancer()# 获得确定性的随机数
rng = np.random.RandomState(42)
noise = rng.normal(size=(len(cancer.data), 50))
# 向数据中添加噪声特征
# 前30个特征来自数据集,后50个是噪声
X_w_noise = np.hstack([cancer.data, noise])X_train, X_test, y_train, y_test = train_test_split(X_w_noise, cancer.target, random_state=0, test_size=.5)
# 使用f_classif(默认值)和SelectPercentile来选择50%的特征
select = SelectPercentile(percentile=50)
select.fit(X_train, y_train)
# 对训练集进行变换
X_train_selected = select.transform(X_train)print("X_train.shape: {}".format(X_train.shape))
print("X_train_selected.shape: {}".format(X_train_selected.shape))
X_train.shape: (284, 80) X_train_selected.shape: (284, 40)
如你所见,特征的数量从 80 减少到 40(原始特征数量的 50%)。我们可以用 get_support
方法来查看哪些特征被选中,它会返回所选特征的布尔遮罩(mask)(其可视化见图 4-9):
import matplotlib.pyplot as pltmask = select.get_support()
print(mask)
# 将遮罩可视化——黑色为True,白色为False
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("Sample index")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-01.png', bbox_inches='tight')
plt.show()
[ True True True True True True True True True False True FalseTrue True True True True True False False True True True TrueTrue True True True True True False False False True False TrueFalse False True False False False False True False False True FalseFalse True False True False False False False False False True FalseTrue False False False False True False True False False False FalseTrue True False True False False False False]

你可以从遮罩的可视化中看出,大多数所选择的特征都是原始特征,并且大多数噪声特征都已被删除。但原始特征的还原并不完美。我们来比较 Logistic 回归在所有特征上的性能与仅使用所选特征的性能:
from sklearn.linear_model import LogisticRegression# 对测试数据进行变换
X_test_selected = select.transform(X_test)lr = LogisticRegression()
lr.fit(X_train, y_train)
print("Score with all features: {:.3f}".format(lr.score(X_test, y_test)))
lr.fit(X_train_selected, y_train)
print("Score with only selected features: {:.3f}".format(lr.score(X_test_selected, y_test)))
在这个例子中,删除噪声特征可以提高性能,即使丢失了某些原始特征。这是一个非常简单的假想示例,在真实数据上的结果要更加复杂。不过,如果特征量太大以至于无法构建模型,或者你怀疑许多特征完全没有信息量,那么单变量特征选择还是非常有用的。
Score with all features: 0.919 Score with only selected features: 0.919
2、基于模型的特征选择
基于模型的特征选择使用一个监督机器学习模型来判断每个特征的重要性,并且仅保留最重要的特征。用于特征选择的监督模型不需要与用于最终监督建模的模型相同。特征选择模型需要为每个特征提供某种重要性度量,以便用这个度量对特征进行排序。决策树和基于决策树的模型提供了 feature_importances_
属性,可以直接编码每个特征的重要性。线性模型系数的绝对值也可以用于表示特征重要性。正如我们在之前所见,L1 惩罚的线性模型学到的是稀疏系数,它只用到了特征的一个很小的子集。这可以被视为模型本身的一种特征选择形式,但也可以用作另一个模型选择特征的预处理步骤。与单变量选择不同,基于模型的选择同时考虑所有特征,因此可以获取交互项(如果模型能够获取它们的话)。要想使用基于模型的特征选择,我们需要使用 SelectFromModel
变换器:
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
select = SelectFromModel(RandomForestClassifier(n_estimators=100, random_state=42),threshold="median")
SelectFromModel
类选出重要性度量(由监督模型提供)大于给定阈值的所有特征。为了得到可以与单变量特征选择进行对比的结果,我们使用中位数作为阈值,这样就可以选择一半特征。我们用包含 100 棵树的随机森林分类器来计算特征重要性。这是一个相当复杂的模型,也比单变量测试要强大得多。下面我们来实际拟合模型:
select.fit(X_train, y_train)
X_train_l1 = select.transform(X_train)
print("X_train.shape: {}".format(X_train.shape))
print("X_train_l1.shape: {}".format(X_train_l1.shape))
X_train.shape: (284, 80) X_train_l1.shape: (284, 40)
我们可以再次查看选中的特征(见图 4-10):
mask = select.get_support()
# 将遮罩可视化——黑色为True,白色为False
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("Sample index")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-02.png', bbox_inches='tight')
plt.show()

这次,除了两个原始特征,其他原始特征都被选中。由于我们指定选择 40 个特征,所以也选择了一些噪声特征。我们来看一下其性能:
X_test_l1 = select.transform(X_test)
score = LogisticRegression().fit(X_train_l1, y_train).score(X_test_l1, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.930
利用更好的特征选择,性能也得到了提高。
3、迭代特征选择
在单变量测试中,我们没有使用模型,而在基于模型的选择中,我们使用了单个模型来选择特征。在迭代特征选择中,将会构建一系列模型,每个模型都使用不同数量的特征。有两种基本方法:开始时没有特征,然后逐个添加特征,直到满足某个终止条件;或者从所有特征开始,然后逐个删除特征,直到满足某个终止条件。由于构建了一系列模型,所以这些方法的计算成本要比前面讨论过的方法更高。其中一种特殊方法是递归特征消除 (recursive feature elimination,RFE),它从所有特征开始构建模型,并根据模型舍弃最不重要的特征,然后使用除被舍弃特征之外的所有特征来构建一个新模型,如此继续,直到仅剩下预设数量的特征。为了让这种方法能够运行,用于选择的模型需要提供某种确定特征重要性的方法,正如基于模型的选择所做的那样。下面我们使用之前用过的同一个随机森林模型,得到的结果如图 4-11 所示:
from sklearn.feature_selection import RFE
select = RFE(RandomForestClassifier(n_estimators=100, random_state=42),n_features_to_select=40)
select.fit(X_train, y_train)
# 将选中的特征可视化:
mask = select.get_support()
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("Sample index")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-03.png', bbox_inches='tight')
plt.show()

与单变量选择和基于模型的选择相比,迭代特征选择的结果更好,但仍然漏掉了一个特征。运行上述代码需要的时间也比基于模型的选择长得多,因为对一个随机森林模型训练了 40 次,每运行一次删除一个特征。我们来测试一下使用 RFE 做特征选择时 Logistic 回归模型的精度:
X_train_rfe= select.transform(X_train)
X_test_rfe= select.transform(X_test)score = LogisticRegression().fit(X_train_rfe, y_train).score(X_test_rfe, y_test)
print("Test score: {:.3f}".format(score))
Test score: 0.930
我们还可以利用在 RFE 内使用的模型来进行预测。这仅使用被选中的特征集:
print("Test score: {:.3f}".format(select.score(X_test, y_test)))
Test score: 0.951
这里,在 RFE 内部使用的随机森林的性能,与在所选特征上训练一个 Logistic 回归模型得到的性能相同。换句话说,只要我们选择了正确的特征,线性模型的表现就与随机森林一样好。
如果你不确定何时选择使用哪些特征作为机器学习算法的输入,那么自动化特征选择可能特别有用。它还有助于减少所需要的特征数量,加快预测速度,或允许可解释性更强的模型。在大多数现实情况下,使用特征选择不太可能大幅提升性能,但它仍是特征工程工具箱中一个非常有价值的工具。
四、利用专家知识
对于特定应用来说,在特征工程中通常可以利用专家知识 (expert knowledge)。虽然在许多情况下,机器学习的目的是避免创建一组专家设计的规则,但这并不意味着应该舍弃该应用或该领域的先验知识。通常来说,领域专家可以帮助找出有用的特征,其信息量比数据原始表示要大得多。想象一下,你在一家旅行社工作,想要预测机票价格。假设你有价格以及日期、航空公司、出发地和目的地的记录。机器学习模型可能从这些记录中构建一个相当不错的模型,但可能无法学到机票价格中的某些重要因素。例如,在度假高峰月份和假日期间,机票价格通常更高。虽然某些假日的日期是固定的(比如圣诞节),其影响可以从日期中学到,但其他假日的日期可能取决于月相(比如光明节和复活节),或者由官方规定(比如学校放假)。如果每个航班都只使用公历记录日期,则无法从数据中学到这些事件。但添加一个特征是很简单的,其中编码了一个航班在公休假日或学校假期的之前、之中还是之后。利用这种方法可以将关于任务属性的先验知识编码到特征中,以辅助机器学习算法。添加一个特征并不会强制机器学习算法使用它,即使最终发现假日信息不包含关于机票价格的信息,用这一信息来扩充数据也不会有什么害处。
下面我们来看一个利用专家知识的特例——虽然在这个例子中,对这些专家知识更正确的叫法应该是“常识”。任务是预测在 Andreas 家门口的自行车出租。
在纽约,Citi Bike 运营着一个带有付费系统的自行车租赁站网络。这些站点遍布整个城市,提供了一种方便的交通方式。自行车出租数据以匿名形式公开(Citi Bike System Data | Citi Bike NYC ),并用各种方法进行了分析。我们想要解决的任务是,对于给定的日期和时间,预测有多少人将会在 Andreas 的家门口租一辆自行车——这样他就知道是否还有自行车留给他。
我们首先将这个站点 2015 年 8 月的数据加载为一个 pandas
数据框。我们将数据重新采样为每 3 小时一个数据,以得到每一天的主要趋势:
import mglearncitibike = mglearn.datasets.load_citibike()
print("Citi Bike data:\n{}".format(citibike.head()))
Citi Bike data: starttime 2015-08-01 00:00:00 3 2015-08-01 03:00:00 0 2015-08-01 06:00:00 9 2015-08-01 09:00:00 41 2015-08-01 12:00:00 39 Freq: 3h, Name: one, dtype: int64
下面这个示例给出了整个月租车数量的可视化(图 4-12):
import pandas as pdplt.figure(figsize=(10, 3))
xticks = pd.date_range(start=citibike.index.min(), end=citibike.index.max(),freq='D')
plt.xticks(xticks, xticks.strftime("%a %m-%d"), rotation=90, ha="left")
plt.plot(citibike, linewidth=1)
plt.xlabel("Date")
plt.ylabel("Rentals")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-04.png', bbox_inches='tight')
plt.show()

观察此数据,我们可以清楚地区分每 24 小时中的白天和夜间。工作日和周末的模式似乎也有很大不同。在对这种时间序列上的预测任务进行评估时,我们通常希望从过去学习 并预测未来 。也就是说,在划分训练集和测试集的时候,我们希望使用某个特定日期之前的所有数据作为训练集,该日期之后的所有数据作为测试集。这是我们通常使用时间序列预测的方式:已知过去所有的出租数据,我们认为明天会发生什么?我们将使用前 184 个数据点(对应前 23 天)作为训练集,剩余的 64 个数据点(对应剩余的 8 天)作为测试集。
在我们的预测任务中,我们使用的唯一特征就是某一租车数量对应的日期和时间。因此输入特征是日期和时间,比如 2015-08-01 00:00:00
,而输出是在接下来 3 小时内的租车数量(根据我们的 DataFrame
,在这个例子中是 3)。
在计算机上存储日期的常用方式是使用 POSIX 时间(这有些令人意外),它是从 1970 年 1 月 1 日 00:00:00(也就是 Unix 时间的起点)起至现在的总秒数。首先,我们可以尝试使用这个单一整数特征作为数据表示:
# 提取目标值(租车数量)
y = citibike.values# 将时间转换为 POSIX 时间(即时间戳)
X = citibike.index
X = pd.to_datetime(X) # 确保索引是日期时间格式
X = X.view('int64') // 10**9 # 转换为秒时间戳并转换为整数# 转换为二维数组
X = X.reshape(-1, 1)print(X[:5])
print(y[:5])
我们首先定义一个函数,它可以将数据划分为训练集和测试集,构建模型并将结果可视化:
# 使用前184个数据点用于训练,剩余的数据点用于测试
n_train = 184# 对给定特征集上的回归进行评估和作图的函数
def eval_on_features(features, target, regressor):# 将给定特征划分为训练集和测试集X_train, X_test = features[:n_train], features[n_train:]# 同样划分目标数组y_train, y_test = target[:n_train], target[n_train:]regressor.fit(X_train, y_train)print("Test-set R^2: {:.2f}".format(regressor.score(X_test, y_test)))y_pred = regressor.predict(X_test)y_pred_train = regressor.predict(X_train)plt.figure(figsize=(10, 3))plt.xticks(range(0, len(X), 8), xticks.strftime("%a %m-%d"), rotation=90,ha="left")plt.plot(range(n_train), y_train, label="train")plt.plot(range(n_train, len(y_test) + n_train), y_test, '-', label="test")plt.plot(range(n_train), y_pred_train, '--', label="prediction train")plt.plot(range(n_train, len(y_test) + n_train), y_pred, '--',label="prediction test")plt.legend(loc=(1.01, 0))plt.xlabel("Date")plt.ylabel("Rentals")
我们之前看到,随机森林需要很少的数据预处理,因此它似乎很适合作为第一个模型。我们使用 POSIX 时间特征 X
,并将随机森林回归传入我们的 eval_on_features
函数。结果如图 4-13 所示。
from sklearn.ensemble import RandomForestRegressor
regressor = RandomForestRegressor(n_estimators=100, random_state=0)
plt.figure()
eval_on_features(X, y, regressor)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-05.png', bbox_inches='tight')
plt.show()
Test-set R^2: -0.04

在训练集上的预测结果相当好,这符合随机森林通常的表现。但对于测试集来说,预测结果是一条常数直线。R2 为 -0.04,说明我们什么都没有学到。发生了什么?
问题在于特征和随机森林的组合。测试集中 POSIX 时间特征的值超出了训练集中特征取值的范围:测试集中数据点的时间戳要晚于训练集中的所有数据点。树以及随机森林无法外推 (extrapolate)到训练集之外的特征范围。结果就是模型只能预测训练集中最近数据点的目标值,即最后一次观测到数据的时间。
显然,我们可以做得更好。这就是我们的“专家知识”的用武之地。通过观察训练数据中的租车数量图像,我们发现两个因素似乎非常重要:一天内的时间与一周的星期几。因此我们来添加这两个特征。我们从 POSIX 时间中学不到任何东西,所以删掉这个特征。首先,我们仅使用每天的时刻。如图 4-14 所示,现在的预测结果对一周内的每天都具有相同的模式:
# 获取小时信息并转换为二维数组
X_hour = citibike.index.hour.to_numpy().reshape(-1, 1)
eval_on_features(X_hour, y, regressor)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-06.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.60

已经好多了,但预测结果显然没有抓住每周的模式。下面我们还添加一周的星期几作为特征(见图 4-15):
X_hour_week = np.hstack([citibike.index.dayofweek.to_numpy().reshape(-1, 1),citibike.index.hour.to_numpy().reshape(-1, 1)])
eval_on_features(X_hour_week, y, regressor)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-07.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.84

现在我们的模型通过考虑一周的星期几和一天内的时间捕捉到了周期性的行为。它的 R2 为 0.84,预测性能相当好。模型学到的内容可能是 8 月前 23 天中星期几与时刻每种组合的平均租车数量。这实际上不需要像随机森林这样复杂的模型,所以我们尝试一个更简单的模型——LinearRegression
(见图 4-16):
from sklearn.linear_model import LinearRegression
eval_on_features(X_hour_week, y, LinearRegression())plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-08.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.13

LinearRegression
的效果差得多,而且周期性模式看起来很奇怪。其原因在于我们用整数编码一周的星期几和一天内的时间,它们被解释为连续变量。因此,线性模型只能学到关于每天时间的线性函数——它学到的是,时间越晚,租车数量越多。但实际模式比这要复杂得多。我们可以通过将整数解释为分类变量(用 OneHotEncoder
进行变换)来获取这种模式(见图 4-17):
from sklearn.preprocessing import OneHotEncoder
from sklearn.linear_model import Ridgeenc = OneHotEncoder()
X_hour_week_onehot = enc.fit_transform(X_hour_week).toarray()eval_on_features(X_hour_week_onehot, y, Ridge())plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-09.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.62

它给出了比连续特征编码好得多的匹配。现在线性模型为一周内的每天都学到了一个系数,为一天内的每个时刻都学到了一个系数。也就是说,一周七天共享“一天内每个时刻”的模式。
利用交互特征,我们可以让模型为星期几和时刻的每一种组合学到一个系数(见图 4-18):
from sklearn.preprocessing import PolynomialFeaturespoly_transformer = PolynomialFeatures(degree=2, interaction_only=True,include_bias=False)
X_hour_week_onehot_poly = poly_transformer.fit_transform(X_hour_week_onehot)
lr = Ridge()
eval_on_features(X_hour_week_onehot_poly, y, lr)plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-10.png', bbox_inches='tight')
plt.show()
Test-set R^2: 0.85

这一变换最终得到一个性能与随机森林类似的模型。这个模型的一大优点是,可以很清楚地看到学到的内容:对每个星期几和时刻的交互项学到了一个系数。我们可以将模型学到的系数作图,而这对于随机森林来说是不可能的。
首先,为时刻和星期几特征创建特征名称:
hour = ["%02d:00" % i for i in range(0, 24, 3)]
day = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
features = day + hour
然后,利用 get_feature_names
方法对 PolynomialFeatures
提取的所有交互特征进行命名,并仅保留系数不为零的那些特征:
features_poly = poly_transformer.get_feature_names_out(features)
features_nonzero = np.array(features_poly)[lr.coef_ != 0]
coef_nonzero = lr.coef_[lr.coef_ != 0]
下面将线性模型学到的系数可视化,如图 4-19 所示:
plt.figure(figsize=(15, 2))
plt.plot(coef_nonzero, 'o')
plt.xticks(np.arange(len(coef_nonzero)), features_nonzero, rotation=90)
plt.xlabel("Feature name")
plt.ylabel("Feature magnitude")plt.tight_layout()
plt.savefig('Images/05AutomatedFeatureSelection-11.png', bbox_inches='tight')
plt.show()

附录
一、参考文献
参考文献:[德] Andreas C. Müller [美] Sarah Guido 《Python Machine Learning Basics Tutorial》
相关文章:

Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明
Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明 目录 Python 机器学习 基础 之 数据表示与特征工程 【单变量非线性变换 / 自动化特征选择/利用专家知识】的简单说明 一、简单介绍 二、单变量非线性变换 三、自…...

uniapp-自定义navigationBar
封装导航栏自定义组件 创建 nav-bar.vue <script setup>import {onReady} from dcloudio/uni-appimport {ref} from vue;const propsdefineProps([navBackgroundColor])const statusBarHeight ref()const navHeight ref()onReady(() > {uni.getSystemInfo({success…...

多式联运奇迹:探索 GPT-4o 的尖端功能
取得的显着进展的DigiOps与人工智能已经标志着重要的里程碑,随着时间的推移塑造了人工智能系统的能力。从早期基于规则系统的出现机器学习和深入学习,人工智能已经发展得更加先进和通用。 生成式预训练 Transformer (GPT) by OpenAI 已特别值得注意。每…...

前端 CSS 经典:好看的标题动画
前言:好看的标题动画实现。 效果: <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><…...

Vue项目打包优化(element+echarts+vue使用cdn)
如何打包查看所有资源大小? 使用插件:webpack-bundle-analyzer 效果图: 安装webpack-bundle-analyzer 第一步,终端执行 npm instatll webpack-bundle-analyzer --save-dev第二步,vue.config.js配置 module.export…...

【ARM 嵌入式 C 入门及渐进 6.1 -- ARMv8 C 内嵌汇编写系统寄存器的函数实现】
请阅读【嵌入式开发学习必备专栏】 文章目录 ARMv8 C 内嵌汇编写系统寄存器 ARMv8 C 内嵌汇编写系统寄存器 在ARMv8架构下,使用C语言结合内嵌汇编实现将一个值写入特定系统寄存器的函数可以按照下面的方法进行。 下面这个示例展示了如何将一个uint64_t类型的值写入…...

ESP32基础应用之使用手机浏览器作为客户端与ESP32作为服务器进行通信
文章目录 1 准备2 移植2.1 softAP工程移植到simple工程中2.2 移植注意事项 3 验证 1 准备 参考工程 Espressif\frameworks\esp-idf-v5.2.1\examples\wifi\getting_started\softAP softAP工程演示将ESP32作为AP,即热点,使手机等终端可以连接参考工程 Esp…...

【课后练习分享】Java用户注册界面设计和求三角形面积的图形界面程序
目录 java编程题(每日一练): 问题一的答案代码如下: 问题一的运行截图如下: 问题二的答案代码如下: 问题二的运行截图如下: java编程题(每日一练): 1.…...

三维空间坐标系变换(旋转平移)
在探究三维空间下的变换前,首先研究二位空间,因为比较直观,再推广到三维空间。 首先应该清楚的一点是:旋转、平移对于坐标系下的点以及坐标系本身而言都是相对的(运动的相对性)。 例如: X O Y …...

OC笔记之foundation框架
OC学习笔记(三) 文章目录 OC学习笔记(三)常用Foundation框架结构体NSRangeNSRange结构体的定义定义 NSRange 的方法打印Range的相关信息NSRange的实际运用查找子字符串返回NSRange结构体 NSPointNSRect NSStringNSString的创建NSS…...

Docker部署springboot包并联通MySQL
Docker部署jar 实现功能 部署springboot下发布的jar包不同docker容器之间通信(如MySQL访问、Redis访问)多个jar包部署 参考文献 Just a moment… Just a moment… https://www.jb51.net/article/279449.htm springboot配置 这里使用多yaml配置文件&…...

多帧激光点云基于标定参数进行融合拼接
1、前言 在三维视觉技术蓬勃发展的今天,点云作为捕获和表示三维环境的基础数据形式,扮演着至关重要的角色。点云融合拼接技术,作为连接孤立点云片段、构建连续、全面三维场景的核心过程,对于自动驾驶、机器人导航、三维建模以及地…...

python数据类型之字符串
目录 1.字符串概念和注意事项 2.字符串内置函数 3.字符串的索引、切片和遍历 4.字符串运算符 5.字符串常用方法 性质判断 开头结尾判断 是否存在某个子串 大小写等格式转化 子串替换 删除两端空白字符 格式化字符串 分割与合并 6.字符串模板 7.exec 函数 8.字符…...

Vue3实战笔记(38)—粒子特效终章
文章目录 前言一、怎样使用官方提供的特效二、海葵特效总结 前言 官方还有很多漂亮的特效,但是vue3只有一个demo,例如我前面实现的两个页面就耗费了一些时间,今天记录一下tsparticles官方内置的几个特效的使用方法,一般这几个就足…...

晶体振荡器
一、晶振与晶体区别 晶振是有源晶振的简称,又叫振荡器,英文名称是oscillator,内部有时钟电路,只需供电便可产生振荡信号;晶体是无源晶振的简称,也叫谐振器,英文名称是crystal,是无极…...

单词可交互的弧形文本
在一个项目中,要求把少儿读本做成电子教材呈现出来,电子书的排版要求跟纸质书一致。其中,英语书有个需求:书中有些不规则排版的文本(如下图所示),当随书音频播放时,被读到的文本要求…...

Linux——进程信号(一)
1.信号入门 1.1生活中的信号 什么是信号? 结合实际红绿灯、闹钟、游戏中的"!"等等这些都是信号。 以红绿灯为例子: 一看到红绿灯我们就知道:红灯停、绿灯行;我们不仅知道它是一个红绿灯而且知道当其出现不同的状况…...

centos9 stream在线安装NVIDIA驱动(rockylinux9.4也成功安装nvidia驱动)
Install NVIDIA Drivers on CentOS Stream 9(rockylinux9.4成功) 主板为技嘉mz72-hb2 显卡为4090 一.Disable Secure Boot From the BIOS 二.Enabling the EPEL Repository on CentOS Stream 9 1.update the DNF package repository cache sudo dnf …...

springmvc不同格式的参数解析
参数解析 application/x-www-form-urlencoded格式 这种格式就是传统的表单提交格式,就是一个个的键值对,会进行url编码,使用springmvc接收时使用RequestParam来进行接收,与传入的字段一一对应,此时使用的参数处理器是R…...

Unity3D让BoxCollider根据子物体生成自适应大小
系列文章目录 unity工具 文章目录 系列文章目录unity工具 👉前言👉一、编辑器添加👉二、代码动态添加的方法(第一种)👉三、代码动态添加的方法(第二种)👉四、重新设置模型的中心点👉壁纸分享👉…...

WSL 2 installation is incomplete.
使用的wsl2版本很旧,因此需要手动更新。 https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi...

Servlet的request对象
request对象的继承关系 1.HttpServletRequest接口继承了ServletRequest接口,对其父接口进行了扩展,可以处理满足所有http协议的请求 2.HttpServletRequest和ServletRequest都是接口,不能创建对象,因此在tomcat底层定义实现类并创…...

蓝桥杯-合并数列
小明发现有很多方案可以把一个很大的正整数拆成若干正整数的和。他采取了其中两种方案,分别将它们列为两个数组 {a1, a2, …, an} 和 {b1, b2, …, bm}。两个数组的和相同。 定义一次合并操作可以将某数组内相邻的两个数合并为一个新数,新数的值是原来两…...

《web应用技术》第9次课后作业
一、将前面的代码继续完善功能 1、采用XML映射文件的形式来映射sql语句; 2、采用动态sql语句的方式,实现条件查询的分页。 二、学习git的使用。 1、每个小组将自己的项目上传到gitee,学会协作开发; 2、学会从gitee上拉取项目…...

FRAUDARCatchSync算法简介
参考:https://blog.51cto.com/u_15127663/2778705 1. 背景 Fraudar 要解决的问题是:找出社交网络中最善于伪装的虚假用户簇。虚假用户会通过增加和正常用户的联系来进行伪装,而这些伪装(边)会形成一个很密集的子网络,可以通过定义…...

刷题之将有序数组转换成二叉搜索树(leetcode)
将有序数组转换成二叉搜索树 正常递归,中序遍历 递归经常会把自己绕晕,还是得画图分析 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(null…...

K-means聚类模型教程(个人总结版)
K-means聚类是一种广泛应用于数据挖掘和数据分析的无监督学习算法。它通过将数据点分成K个簇(cluster),使得同一簇内的数据点之间的相似度最大,不同簇之间的相似度最小。本文将详细介绍K-means聚类算法的背景、基本原理、具体实现…...

android怎么告诉系统不要回收
在Android中,如果你想告诉系统不要回收你的应用程序,可以通过设置Activity的属性来实现。你可以设置android:configChanges属性,指定在哪些配置更改时不重新创建Activity。 例如,如果你想指示系统在屏幕方向更改时不要重新创建Ac…...

【FAQ】HarmonyOS SDK 闭源开放能力 —IAP Kit(2)
1.问题描述: 应用内支付IAP Kit和Payment Kit的区别以及适用场景? 解决方案: IAP Kit是四方支付,仅支持在线虚拟商品,如会员,游戏钻石等,双框架支持全球,目前单框架暂时只支持国内…...

ubuntu设置root开机登录,允许root用户ssh远程登录
ubuntu与centos系统不同,默认root开机不能登录。 1、输入一下命令创建root密码,根据提示输入新密码 sudo passwd root 2、打开gdm-autologin文件,将auth required pam_succeed_if.so user ! root quiet_success这行注释掉,这行就…...