3
雷锋网 AI 科技评论按:眨眼间我们就从人工特征、专家系统来到了自动特征、深度学习的人工智能新时代,众多开源测试数据集也大大降低了理论研究的门槛,直接加载数据集就可以开始模型训练或者测试。然而面对实际问题时,收集到的数据往往不是像数据集中那样整理好的,直接用来跑模型会带来各种各样的问题。这时候我们就开始回忆起「特征工程」这一组容易被忽略但解决问题时不可或缺的硬功夫。
数据科学家 Dipanjan Sarkar 近日就发布了两篇长博客介绍了一些基本的特征工程知识和技巧。这篇为上篇,主要介绍连续型数值数据的特征工程处理方法。雷锋网 AI 科技评论全文编译如下。
「推动世界运转的是钱」,不论你是否同意这句话,都不能忽视这个事实。以今天的数字化革命时代而言,更恰当的说法已经成了「推动世界运转的是数据」。确实,无论数据的大小和规模,其已经成为企业、公司和组织的头等资产。任何智能系统不管其复杂度如何都需要由数据来驱动。在任何智能系统的核心模块,我们都有一个或多个基于机器学习、深度学习或统计方法的算法,这些算法在一段时间内以数据为原料收集知识,并提供智能见解。但算法本身非常朴素且不能在原始数据上直接得出结果。因此一个重要的任务就是需要从数据中设计出工程上有意义的特征,即能被这些算法理解和使用的特征。
任何智能系统基本上是由一个端到端的流程组成,从数据原始数据开始,利用数据处理技术来加工、处理并从这些数据中设计出有意义的特征和属性。然后我们通常利用统计模型或机器学习模型在这些特征上建模,如果未来要使用的话,就基于眼前要解决的问题部署模型。一个典型的标准的基于 CRISP-DM(注:跨行业数据挖掘标准流程)工业标准处理模型的机器学习流程描述如下。
直接输入原始数据并在这些数据基础上直接建模很可能是鲁莽的,因为我们很可能不会得到期望的结果或性能,且算法不够智能,不能自动地从原始数据中抽取有意义的特征(虽然有一些某种程度上自动抽取特征的技术,比如深度学习技术,后文我们会再谈到)。
我们的主要关注领域放在数据准备方面,正如上图中所指出的,我们先对数据做一些必要数据加工和处理,然后采用各种方法从原始数据中抽取有意义的属性或特征。
特征工程是构建任何智能系统的必要部分。即使你有了很多新的方法如深度学习和元启发式方法来帮助你自动进行机器学习,但每个问题都是针对特定领域的,且更好的特征(适合问题的)通常是系统性能的决定性因素。特征工程是一门艺术也是一门科学,这就是为什么数据科学家在建模前通常花 70% 的时间用于准备数据。让我们看看数据科学界领域里一些名人关于特征工程的言论。
「特征处理是困难的、耗时的且需要专家知识。『实用化的机器学习』基本上就是特征工程。」
—— 吴恩达
这些基本加强了我们先前提到的观点:数据科学家将近 80% 的时间是用在困难且处理耗时的特征工程上,其过程既需要领域知识又需要数学计算。
「特征工程是将原始数据转化特征的过程,特征要能更好地表示潜在问题并提高预测模型在未知数据上的准确率。」
—— Dr. Jason Brownlee
这让我们了解到特征工程是将数据转换为特征的过程,特征是机器学习模型的输入,从而更高质量的特征有助于提高整体模型的性能。特征的好坏非常地取决于潜在的问题。因此,即使机器学习任务在不同场景中是相同的,比如将邮件分为垃圾邮件或非垃圾邮件,或对手写数字字符进行分类,这两个场景中提取的特征千差万别。
来自华盛顿大学的 Pedro Domingos 教授,在这篇名为《A Few Useful Things to Know about Machine Learning》中告诉我们。
「归根到底,有的机器学习项目成功了, 有的失败了。为何如此不同呢?我们很容易想到,最重要的因素就是使用的特征。」
—— Prof. Pedro Domingos
有可能启发你的最后一句关于特征工程的名言来自有名的 Kaggle 比赛选手 Xavier Conort。你们大部分人都知道 Kaggle 上通常会定期地放一些来自真实世界中的棘手的机器学习问题,一般对所有人开放。
「我们使用的算法对 Kaggle 赛手来说都是非常标准的。…我们花费大部分精力在特征工程上。... 我们也非常小心地丢弃可能使模型过拟合的特征。」
—— Xarvier Conort
一个特征通常是来自原始数据的一种特定表示,它是一个单独的、可度量的属性,通常由数据集中的一列来描述。考虑到一个通用的二维数据集,每个样本的观测值用一行来表示,每种特征用一列来表示,从而每个样本的观测值中的各种特征都有一个具体的值。
这样以来,正如上图中例子所示,每行通常代表一个特征向量,整个特征集包括了所有的观察值形成了二维的特征矩阵,称为特征集。这与代表二维数据的数据框或电子表格相似。机器学习算法通常都是处理这些数值型矩阵或张量,因此大部分特征工程技术都将原始数据转换为一些数值型数来表示,使得它们能更好地被算法理解。
从数据集的角度出发,特征可以分为两种主要的类型。一般地,原始特征是直接从数据集中得到,没有额外的操作或处理。导出特征通常来自于特征工程,即我们从现有数据属性中提取的特征。一个简单的例子是从一个包含出生日期的雇员数据集中创建一个新的「年龄」特征,只需要将当前日期减去出生日期即可。
数据的类型和格式各不相同,包括结构化的和非结构化的数据。在这篇文章中,我们将讨论各种用来处理结构化的连续型数值数据的特征工程策略。所有的这些例子都是我最近一本书中的一部分《Pratical Mahine Learning with Python》,你可以访问这篇文章中使用的相关的数据集和代码,它们放在 GitHub 上。在此着重感谢 Gabriel Moreira ,他在特征工程技术上提供了一些优雅的指针,给了我很大帮助。
数值型数据通常以标量的形式表示数据,描述观测值、记录或者测量值。本文的数值型数据是指连续型数据而不是离散型数据,表示不同类目的数据就是后者。数值型数据也可以用向量来表示,向量的每个值或分量代表一个特征。整数和浮点数是连续型数值数据中最常见也是最常使用的数值型数据类型。即使数值型数据可以直接输入到机器学习模型中,你仍需要在建模前设计与场景、问题和领域相关的特征。因此仍需要特征工程。让我们利用 python 来看看在数值型数据上做特征工程的一些策略。我们首先加载下面一些必要的依赖(通常在 Jupyter botebook 上)。
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as spstats
%matplotlib inline
正如我们先前提到的,根据上下文和数据的格式,原始数值型数据通常可直接输入到机器学习模型中。原始的度量方法通常用数值型变量来直接表示为特征,而不需要任何形式的变换或特征工程。通常这些特征可以表示一些值或总数。让我们加载四个数据集之一的 Pokemon 数据集,该数据集也在 Kaggle 上公布了。
poke_df = pd.read_csv('datasets/Pokemon.csv', encoding='utf-8')
poke_df.head()
Pokemon 是一个大型多媒体游戏,包含了各种口袋妖怪(Pokemon)角色。简而言之,你可以认为他们是带有超能力的动物!这些数据集由这些口袋妖怪角色构成,每个角色带有各种统计信息。
如果你仔细地观察上图中这些数据,你会看到几个代表数值型原始值的属性,它可以被直接使用。下面的这行代码挑出了其中一些重点特征。
poke_df[['HP', 'Attack', 'Defense']].head()
这样,你可以直接将这些属性作为特征,如上图所示。这些特征包括 Pokemon 的 HP(血量),Attack (攻击)和 Defense(防御)状态。事实上,我们也可以基于这些字段计算出一些基本的统计量。
poke_df[['HP', 'Attack', 'Defense']].describe()
数值特征形式的基本描述性统计量
这样你就对特征中的统计量如总数、平均值、标准差和四分位数有了一个很好的印象。
原始度量的另一种形式包括代表频率、总数或特征属性发生次数的特征。让我们看看 millionsong 数据集中的一个例子,其描述了某一歌曲被各种用户收听的总数或频数。
popsong_df = pd.read_csv('datasets/song_views.csv',encoding='utf-8')
popsong_df.head(10)
根据这张截图,显而易见 listen_count 字段可以直接作为基于数值型特征的频数或总数。
基于要解决的问题构建模型时,通常原始频数或总数可能与此不相关。比如如果我要建立一个推荐系统用来推荐歌曲,我只希望知道一个人是否感兴趣或是否听过某歌曲。我不需要知道一首歌被听过的次数,因为我更关心的是一个人所听过的各种各样的歌曲。在这个例子中,二值化的特征比基于计数的特征更合适。我们二值化 listen_count 字段如下。
watched = np.array(popsong_df['listen_count'])
watched[watched >= 1] = 1
popsong_df['watched'] = watched
你也可以使用 scikit-learn 中 preprocessing 模块的 Binarizer 类来执行同样的任务,而不一定使用 numpy 数组。
from sklearn.preprocessing import Binarizer
bn = Binarizer(threshold=0.9)
pd_watched =bn.transform([popsong_df['listen_count']])[0]
popsong_df['pd_watched'] = pd_watched
popsong_df.head(11)
你可以从上面的截图中清楚地看到,两个方法得到了相同的结果。因此我们得到了一个二值化的特征来表示一首歌是否被每个用户听过,并且可以在相关的模型中使用它。
处理连续型数值属性如比例或百分比时,我们通常不需要高精度的原始数值。因此通常有必要将这些高精度的百分比舍入为整数型数值。这些整数可以直接作为原始数值甚至分类型特征(基于离散类的)使用。让我们试着将这个观念应用到一个虚拟数据集上,该数据集描述了库存项和他们的流行度百分比。
items_popularity =pd.read_csv('datasets/item_popularity.csv',encoding='utf-8')
items_popularity['popularity_scale_10'] = np.array(np.round((items_popularity['pop_percent'] * 10)),dtype='int')
items_popularity['popularity_scale_100'] = np.array(np.round((items_popularity['pop_percent'] * 100)),dtype='int')
items_popularity
基于上面的输出,你可能猜到我们试了两种不同的舍入方式。这些特征表明项目流行度的特征现在既有 1-10 的尺度也有 1-100 的尺度。基于这个场景或问题你可以使用这些值同时作为数值型或分类型特征。
高级机器学习模型通常会对作为输入特征变量函数的输出响应建模(离散类别或连续数值)。例如,一个简单的线性回归方程可以表示为
其中输入特征用变量表示为
权重或系数可以分别表示为
目标是预测响应 y.
在这个例子中,仅仅根据单个的、分离的输入特征,这个简单的线性模型描述了输出与输入之间的关系。
然而,在一些真实场景中,有必要试着捕获这些输入特征集一部分的特征变量之间的相关性。上述带有相关特征的线性回归方程的展开式可以简单表示为
此处特征可表示为
表示了相关特征。现在让我们试着在 Pokemon 数据集上设计一些相关特征。
atk_def = poke_df[['Attack', 'Defense']]
atk_def.head()
从输出数据框中,我们可以看到我们有两个数值型(连续的)特征,Attack 和 Defence。现在我们可以利用 scikit-learn 建立二度特征。
pf = PolynomialFeatures(degree=2,
interaction_only=False,include_bias=False)
res = pf.fit_transform(atk_def)
res
Output
------
array([[ 49., 49., 2401., 2401., 2401.],
[ 62., 63., 3844., 3906., 3969.],
[ 82., 83., 6724., 6806., 6889.],
...,
[ 110., 60., 12100., 6600., 3600.],
[ 160., 60., 25600., 9600., 3600.],
[ 110., 120., 12100., 13200., 14400.]])
上面的特征矩阵一共描述了 5 个特征,其中包括新的相关特征。我们可以看到上述矩阵中每个特征的度,如下所示。
pd.DataFrame(pf.powers_, columns=['Attack_degree','Defense_degree'])
基于这个输出,现在我们可以通过每个特征的度知道它实际上代表什么。在此基础上,现在我们可以对每个特征进行命名如下。这仅仅是为了便于理解,你可以给这些特征取更好的、容易使用和简单的名字。
intr_features = pd.DataFrame(res, columns=['Attack','Defense','Attack^2','Attack x Defense','Defense^2'])
intr_features.head(5)
因此上述数据代表了我们原始的特征以及它们的相关特征。
处理原始、连续的数值型特征问题通常会导致这些特征值的分布被破坏。这表明有些值经常出现而另一些值出现非常少。除此之外,另一个问题是这些特征的值的变化范围。比如某个音乐视频的观看总数会非常大(Despacito,说你呢)而一些值会非常小。直接使用这些特征会产生很多问题,反而会影响模型表现。因此出现了处理这些问题的技巧,包括分区间法和变换。
分区间(Bining),也叫做量化,用于将连续型数值特征转换为离散型特征(类别)。可以认为这些离散值或数字是类别或原始的连续型数值被分区间或分组之后的数目。每个不同的区间大小代表某种密度,因此一个特定范围的连续型数值会落在里面。对数据做分区间的具体技巧包括等宽分区间以及自适应分区间。我们使用从 2016 年 FreeCodeCamp 开发者和编码员调查报告中抽取出来的一个子集中的数据,来讨论各种针对编码员和软件开发者的属性。
fcc_survey_df =pd.read_csv('datasets/fcc_2016_coder_survey_subset.csv',encoding='utf-8')
fcc_survey_df[['ID.x', 'EmploymentField', 'Age','Income']].head()
对于每个参加调查的编码员或开发者,ID.x 变量基本上是一个唯一的标识符而其他字段是可自我解释的。
就像名字表明的那样,在等宽分区间方法中,每个区间都是固定宽度的,通常可以预先分析数据进行定义。基于一些领域知识、规则或约束,每个区间有个预先固定的值的范围,只有处于范围内的数值才被分配到该区间。基于数据舍入操作的分区间是一种方式,你可以使用数据舍入操作来对原始值进行分区间,我们前面已经讲过。
现在我们分析编码员调查报告数据集的 Age 特征并看看它的分布。
fig, ax = plt.subplots()
fcc_survey_df['Age'].hist(color='#A9C5D3',edgecolor='black',grid=False)
ax.set_title('Developer Age Histogram', fontsize=12)
ax.set_xlabel('Age', fontsize=12)
ax.set_ylabel('Frequency', fontsize=12)
上面的直方图表明,如预期那样,开发者年龄分布仿佛往左侧倾斜(上年纪的开发者偏少)。现在我们根据下面的模式,将这些原始年龄值分配到特定的区间。
Age Range: Bin
---------------
0 - 9 : 0
10 - 19 : 1
20 - 29 : 2
30 - 39 : 3
40 - 49 : 4
50 - 59 : 5
60 - 69 : 6
... and so on
我们可以简单地使用我们先前学习到的数据舍入部分知识,先将这些原始年龄值除以 10,然后通过 floor 函数对原始年龄数值进行截断。
fcc_survey_df['Age_bin_round'] = np.array(np.floor(np.array(fcc_survey_df['Age']) / 10.))
fcc_survey_df[['ID.x', 'Age','Age_bin_round']].iloc[1071:1076]
你可以看到基于数据舍入操作的每个年龄对应的区间。但是如果我们需要更灵活的操作怎么办?如果我们想基于我们的规则或逻辑,确定或修改区间的宽度怎么办?基于常用范围的分区间方法将帮助我们完成这个。让我们来定义一些通用年龄段位,使用下面的方式来对开发者年龄分区间。
Age Range : Bin
---------------
0 - 15 : 1
16 - 30 : 2
31 - 45 : 3
46 - 60 : 4
61 - 75 : 5
75 - 100 : 6
基于这些常用的分区间方式,我们现在可以对每个开发者年龄值的区间打标签,我们将存储区间的范围和相应的标签。
bin_ranges = [0, 15, 30, 45, 60, 75, 100]
bin_names = [1, 2, 3, 4, 5, 6]
fcc_survey_df['Age_bin_custom_range'] = pd.cut(np.array(fcc_survey_df['Age']),bins=bin_ranges)
fcc_survey_df['Age_bin_custom_label'] = pd.cut(np.array(fcc_survey_df['Age']),bins=bin_ranges, labels=bin_names)
# view the binned features
fcc_survey_df[['ID.x', 'Age', 'Age_bin_round','Age_bin_custom_range','Age_bin_custom_label']].iloc[10a71:1076]
使用等宽分区间的不足之处在于,我们手动决定了区间的值范围,而由于落在某个区间中的数据点或值的数目是不均匀的,因此可能会得到不规则的区间。一些区间中的数据可能会非常的密集,一些区间会非常稀疏甚至是空的!自适应分区间方法是一个更安全的策略,在这些场景中,我们让数据自己说话!这样,我们使用数据分布来决定区间的范围。
基于分位数的分区间方法是自适应分箱方法中一个很好的技巧。量化对于特定值或切点有助于将特定数值域的连续值分布划分为离散的互相挨着的区间。因此 q 分位数有助于将数值属性划分为 q 个相等的部分。关于量化比较流行的例子包括 2 分位数,也叫中值,将数据分布划分为2个相等的区间;4 分位数,也简称分位数,它将数据划分为 4 个相等的区间;以及 10 分位数,也叫十分位数,创建 10 个相等宽度的区间,现在让我们看看开发者数据集的 Income 字段的数据分布。
fig, ax = plt.subplots()
fcc_survey_df['Income'].hist(bins=30, color='#A9C5D3',edgecolor='black',grid=False)
ax.set_title('Developer Income Histogram',fontsize=12)
ax.set_xlabel('Developer Income', fontsize=12)
ax.set_ylabel('Frequency', fontsize=12)
描述开发者收入分布的直方图
上述的分布描述了一个在收入上右歪斜的分布,少数人赚更多的钱,多数人赚更少的钱。让我们基于自适应分箱方式做一个 4-分位数或分位数。我们可以很容易地得到如下的分位数。
quantile_list = [0, .25, .5, .75, 1.]
quantiles =
fcc_survey_df['Income'].quantile(quantile_list)
quantiles
Output
------
0.00 6000.0
0.25 20000.0
0.50 37000.0
0.75 60000.0
1.00 200000.0
Name: Income, dtype: float64
现在让我们在原始的分布直方图中可视化下这些分位数。
fig, ax = plt.subplots()
fcc_survey_df['Income'].hist(bins=30, color='#A9C5D3',edgecolor='black',grid=False)
for quantile in quantiles:
qvl = plt.axvline(quantile, color='r')
ax.legend([qvl], ['Quantiles'], fontsize=10)
ax.set_title('Developer Income Histogram with Quantiles',fontsize=12)
ax.set_xlabel('Developer Income', fontsize=12)
ax.set_ylabel('Frequency', fontsize=12)
上面描述的分布中红色线代表了分位数值和我们潜在的区间。让我们利用这些知识来构建我们基于分区间策略的分位数。
quantile_labels = ['0-25Q', '25-50Q', '50-75Q', '75-100Q']
fcc_survey_df['Income_quantile_range'] = pd.qcut(
fcc_survey_df['Income'],q=quantile_list)
fcc_survey_df['Income_quantile_label'] = pd.qcut(
fcc_survey_df['Income'],q=quantile_list,labels=quantile_labels)
fcc_survey_df[['ID.x', 'Age', 'Income','Income_quantile_range',
'Income_quantile_label']].iloc[4:9]
通过这个例子,你应该对如何做基于分位数的自适应分区间法有了一个很好的认识。一个需要重点记住的是,分区间的结果是离散值类型的分类特征,当你在模型中使用分类数据之前,可能需要额外的特征工程相关步骤。我们将在接下来的部分简要地讲述分类数据的特征工程技巧。
我们讨论下先前简单提到过的数据分布倾斜的负面影响。现在我们可以考虑另一个特征工程技巧,即利用统计或数学变换。我们试试看 Log 变换和 Box-Cox 变换。这两种变换函数都属于幂变换函数簇,通常用来创建单调的数据变换。它们的主要作用在于它能帮助稳定方差,始终保持分布接近于正态分布并使得数据与分布的平均值无关。
log 变换属于幂变换函数簇。该函数用数学表达式表示为
读为以 b 为底 x 的对数等于 y。这可以变换为
表示以b为底指数必须达到多少才等于x。自然对数使用 b=e,e=2.71828,通常叫作欧拉常数。你可以使用通常在十进制系统中使用的 b=10 作为底数。
当应用于倾斜分布时 Log 变换是很有用的,因为他们倾向于拉伸那些落在较低的幅度范围内自变量值的范围,倾向于压缩或减少更高幅度范围内的自变量值的范围。从而使得倾斜分布尽可能的接近正态分布。让我们对先前使用的开发者数据集的 Income 特征上使用log变换。
fcc_survey_df['Income_log'] = np.log((1+fcc_survey_df['Income']))
fcc_survey_df[['ID.x', 'Age', 'Income','Income_log']].iloc[4:9]
Income_log 字段描述了经过 log 变换后的特征。现在让我们来看看字段变换后数据的分布。
基于上面的图,我们可以清楚地看到与先前倾斜分布相比,该分布更加像正态分布或高斯分布。
income_log_mean =np.round(np.mean(fcc_survey_df['Income_log']), 2)
fig, ax = plt.subplots()
fcc_survey_df['Income_log'].hist(bins=30,color='#A9C5D3',edgecolor='black',grid=False)
plt.axvline(income_log_mean, color='r')
ax.set_title('Developer Income Histogram after Log Transform',fontsize=12)
ax.set_xlabel('Developer Income (log scale)',fontsize=12)
ax.set_ylabel('Frequency', fontsize=12)
ax.text(11.5, 450, r'$\mu$='+str(income_log_mean),fontsize=10)
经过log变换后描述开发者收入分布的直方图
Box-Cox 变换是另一个流行的幂变换函数簇中的一个函数。该函数有一个前提条件,即数值型值必须先变换为正数(与 log 变换所要求的一样)。万一出现数值是负的,使用一个常数对数值进行偏移是有帮助的。数学上,Box-Cox 变换函数可以表示如下。
生成的变换后的输出y是输入 x 和变换参数的函数;当 λ=0 时,该变换就是自然对数 log 变换,前面我们已经提到过了。λ 的最佳取值通常由最大似然或最大对数似然确定。现在让我们在开发者数据集的收入特征上应用 Box-Cox 变换。首先我们从数据分布中移除非零值得到最佳的值,结果如下。
income = np.array(fcc_survey_df['Income'])
income_clean = income[~np.isnan(income)]
l, opt_lambda = spstats.boxcox(income_clean)
print('Optimal lambda value:', opt_lambda)
Output
------
Optimal lambda value: 0.117991239456
现在我们得到了最佳的值,让我们在取值为 0 和 λ(最佳取值 λ )时使用 Box-Cox 变换对开发者收入特征进行变换。
fcc_survey_df['Income_boxcox_lambda_0'] = spstats.boxcox((1+fcc_survey_df['Income']),lmbda=0)
fcc_survey_df['Income_boxcox_lambda_opt'] = spstats.boxcox(fcc_survey_df['Income'],lmbda=opt_lambda)
fcc_survey_df[['ID.x', 'Age', 'Income', 'Income_log','Income_boxcox_lambda_0','Income_boxcox_lambda_opt']].iloc[4:9]
变换后的特征在上述数据框中描述了。就像我们期望的那样,Income_log 和 Income_boxcox_lamba_0 具有相同的取值。让我们看看经过最佳λ变换后 Income 特征的分布。
income_boxcox_mean = np.round(np.mean(fcc_survey_df['Income_boxcox_lambda_opt']),2)
fig, ax = plt.subplots()
fcc_survey_df['Income_boxcox_lambda_opt'].hist(bins=30,
color='#A9C5D3',edgecolor='black', grid=False)
plt.axvline(income_boxcox_mean, color='r')
ax.set_title('Developer Income Histogram after Box–Cox Transform',fontsize=12)
ax.set_xlabel('Developer Income (Box–Cox transform)',fontsize=12)
ax.set_ylabel('Frequency', fontsize=12)
ax.text(24, 450, r'$\mu$='+str(income_boxcox_mean),fontsize=10)
经过Box-Cox变换后描述开发者收入分布的直方图
分布看起来更像是正态分布,与我们经过 log 变换后的分布相似。
特征工程是机器学习和数据科学中的一个重要方面,永远都不应该被忽视。虽然我们也有自动的机器学习框架,如 AutoML(但该框架也强调了它需要好的特征才能跑出好的效果!)。特征工程永不过时,即使对于自动化方法,其中也有一部分经常需要根据数据类型、领域和要解决的问题而设计特殊的特征。
这篇文章中我们讨论了在连续型数值数据上特征工程的常用策略。在接下来的部分,我们将讨论处理离散、分类数据的常用策略,在后续章节中会提到非结构化类型数据的处理策略。敬请关注!
这篇文章中使用的所有的代码和数据集都可以从 GitHub 上访问。
代码也以 Jupyter notebook 的形式提供了。
via:Understanding Feature Engineering (Part-1) ,雷锋网 AI 科技评论编译
相关文章:
Kaggle16000份问卷揭示数据科学家平均画像:30岁,硕士学位,年薪36万
雷峰网原创文章,未经授权禁止转载。详情见转载须知。