将函数应用于列

我们已经见过许多通过将函数应用于现有列或其他数组来创建新表格列的例子。所有这些函数都将数组作为其参数。但经常地,我们想要通过一个不以数组为参数的函数来转换列中的条目。例如,它可能只接受一个数字作为参数,如下面定义的 cut_off_at_100 函数。

[In ]:
from datascience import *
import matplotlib
path_data = '../../../assets/data/'
matplotlib.use('Agg')
%matplotlib inline
import matplotlib.pyplot as plots
plots.style.use('fivethirtyeight')
import numpy as np
[In ]:
def cut_off_at_100(x):
    """The smaller of x and 100"""
    return min(x, 100)
[In ]:
cut_off_at_100(17)
17
[In ]:
cut_off_at_100(117)
100
[In ]:
cut_off_at_100(100)
100

函数 cut_off_at_100 在其参数小于等于100时简单地返回该参数。但如果参数大于100,则返回100。

在我们之前使用人口普查数据的例子中,我们看到变量 AGE 有一个值100,表示“100岁或以上”。以这种方式将年龄截断在100正是 cut_off_at_100 所做的。

要同时在许多年龄上使用此函数,我们必须能够“引用”该函数本身,而不实际调用它。类似地,我们可以向厨师展示一个蛋糕食谱,并请她用它来烘焙6个蛋糕。在这种情况下,我们自己并没有使用食谱烘焙任何蛋糕;我们的角色仅仅是将食谱提供给厨师。类似地,我们可以要求一个表格对一列中的6个不同的数字调用 cut_off_at_100

首先,我们创建表格 ages,其中包含一列人员和一列他们的年龄。例如,人员 C 52岁。

[In ]:
ages = Table().with_columns(
    'Person', make_array('A', 'B', 'C', 'D', 'E', 'F'),
    'Age', make_array(17, 117, 52, 100, 6, 101)
)
ages
Person | Age
A      | 17
B      | 117
C      | 52
D      | 100
E      | 6
F      | 101

apply

为了将每个年龄截断在100,我们将使用一个新的 Table 方法。apply 方法对列的每个元素调用一个函数,形成一个新的返回值数组。要指明调用哪个函数,只需说出它的名字(不带引号或括号)。输入值的列名是一个字符串,仍然必须出现在引号内。

[In ]:
ages.apply(cut_off_at_100, 'Age')
array([ 17, 100,  52, 100,   6, 100])

我们在这里所做的就是将函数 cut_off_at_100“应用”到 ages 表的 Age 列中的每个值。输出是对应函数返回值的数组。例如,17保持为17,117变为100,52保持为52,依此类推。

这个数组与 ages 表的原始 Age 列长度相同,可以作为名为 Cut Off Age 的新列中的值,与现有的 PersonAge 列放在一起。

[In ]:
ages.with_column(
    'Cut Off Age', ages.apply(cut_off_at_100, 'Age')
)
Person | Age  | Cut Off Age
A      | 17   | 17
B      | 117  | 100
C      | 52   | 52
D      | 100  | 100
E      | 6    | 6
F      | 101  | 100

函数作为值

我们已经看到 Python 有多种类型的值。例如,6 是一个数值,"cake" 是一个文本值,Table() 是一个空表,ages 是一个表值的名称(因为我们在上面定义了它)。

在 Python 中,每个函数(包括 cut_off_at_100)也是一个值。再次用食谱来类比会有所帮助。蛋糕食谱是一个真实的东西,区别于蛋糕或原料,你可以给它起一个名字,比如“阿妮的蛋糕食谱”。当我们用 def 语句定义 cut_off_at_100 时,我们实际上做了两件独立的事情:我们创建了一个将数字截断在100的函数,并且我们给它取名为 cut_off_at_100

我们可以通过写函数的名称来引用任何函数,而不需要实际调用它所需的括号或参数。我们在上面调用 apply 时就是这样做的。当我们仅写一个函数的名称作为单元格的最后一行时,Python 会生成该函数的文本表示,就像它会打印出数字或字符串值一样。

[In ]:
cut_off_at_100
<function __main__.cut_off_at_100(x)>

注意我们没有写带引号的 "cut_off_at_100"(那只是一段文本),也没有写 cut_off_at_100()(那是一个函数调用,而且是无效的)。我们只是写了 cut_off_at_100 来引用这个函数。

就像我们可以为其他值定义新名称一样,我们也可以为函数定义新名称。例如,假设我们想将函数称为 cut_off 而不是 cut_off_at_100。我们可以这样写:

[In ]:
cut_off = cut_off_at_100

现在 cut_off 是一个函数的名字。它与 cut_off_at_100 是同一个函数,所以打印出来的值完全相同。

[In ]:
cut_off
<function __main__.cut_off_at_100(x)>

让我们再看一个 apply 的应用。

示例:预测

数据科学经常用于对未来进行预测。如果我们试图预测一个特定个体的结果——例如,她对治疗的反应,或者他是否会购买产品——很自然地会基于其他类似个体的结果来进行预测。

下面的表格改编自一个关于父母及其成年子女身高的历史数据集。每一行对应一个成年子女。变量包括家庭的数字编码、父亲和母亲的身高(英寸)、家庭中的子女人数,以及子女的出生顺序(1=最年长)、性别(仅编码为“male”或“female”)和身高(英寸)。

[In ]:
# Data on heights of parents and their adult children
family_heights = Table.read_table(path_data + 'family_heights.csv').drop(3)
family_heights
family | father | mother | children | childNum | sex    | childHeight
1      | 78.5   | 67     | 4        | 1        | male   | 73.2
1      | 78.5   | 67     | 4        | 2        | female | 69.2
1      | 78.5   | 67     | 4        | 3        | female | 69
1      | 78.5   | 67     | 4        | 4        | female | 69
2      | 75.5   | 66.5   | 4        | 1        | male   | 73.5
2      | 75.5   | 66.5   | 4        | 2        | male   | 72.5
2      | 75.5   | 66.5   | 4        | 3        | female | 65.5
2      | 75.5   | 66.5   | 4        | 4        | female | 65.5
3      | 75     | 64     | 2        | 1        | male   | 71
3      | 75     | 64     | 2        | 2        | female | 68
... (924 rows omitted)

收集数据的一个主要原因是要能够预测出生于与该数据集中相似父母的孩子成年后的身高。让我们尝试这样做,采用父母身高的简单平均值作为我们预测所依据的变量。

这个父母平均身高是我们的“预测变量”。在下面的单元格中,其值存储在数组 parent_averages 中。

表格 heights 仅包含父母平均身高和子女身高。这两个变量的散点图显示了我们预期的正关联。

[In ]:
parent_averages = (family_heights.column('father') + family_heights.column('mother'))/2
heights = Table().with_columns(
    'Parent Average', parent_averages,
    'Child', family_heights.column('childHeight')
)
heights
Parent Average | Child
72.75          | 73.2
72.75          | 69.2
72.75          | 69
72.75          | 69
71             | 73.5
71             | 72.5
71             | 65.5
71             | 65.5
69.5           | 71
69.5           | 68
... (924 rows omitted)
[In ]:
heights.scatter('Parent Average')
Scatter plot with 'Parent Average' on the x-axis and 'Child' on the y-axis. Generally as the x values increase, so do the y-values.

现在假设研究人员遇到了一对新夫妇,与数据集中的人相似,并想知道他们孩子的身高会有多高。考虑到父母平均身高是68英寸,预测孩子身高的好方法是什么?

一个合理的方法是基于所有父母平均身高在68英寸左右的点来进行预测。预测值等于仅从这些点计算出的子女平均身高。

让我们执行这个计划。目前我们只需要对“68英寸左右”做一个合理的定义,然后据此工作。在课程后面,我们将探讨这类选择的后果。

我们将“接近”定义为“在半英寸以内”。下图显示了所有父母平均身高在67.5英寸到68.5英寸之间的点。这些是红色线条之间的条带中的所有点。每个点对应一个孩子;我们对新夫妇孩子身高的预测是条带中所有孩子的平均身高。这由金色点表示。

忽略代码,只专注于理解到达那个金色点的思维过程。

[In ]:
heights.scatter('Parent Average')
plots.plot([67.5, 67.5], [50, 85], color='red', lw=2)
plots.plot([68.5, 68.5], [50, 85], color='red', lw=2)
plots.scatter(68, 67.62, color='gold', s=40);
The same scatter plot as above is shown, but this time the y-axis has been extended from 55-80 as before and now stretches to 50 to 85. Two vertical red lines have been added at 67.5 and 68.5 and a gold point at (68, 67.62).

为了精确计算金色点的位置,我们首先需要识别条带中的所有点。这些对应于 Parent Average 在67.5英寸到68.5英寸之间的行。

[In ]:
close_to_68 = heights.where('Parent Average', are.between(67.5, 68.5))
close_to_68
Parent Average | Child
68             | 74
68             | 70
68             | 68
68             | 67
68             | 67
68             | 66
68             | 63.5
68             | 63
67.5           | 65
68.1           | 62.7
... (175 rows omitted)

父母平均身高为68英寸的孩子的预测身高是这些行中子女的平均身高。即67.62英寸。

[In ]:
np.average(close_to_68.column('Child'))
67.62

现在我们有一种方法,可以根据数据集中任意接近的父母平均身高值来预测孩子的身高。我们可以定义一个函数 predict_child 来完成这项工作。函数体包含上面两个单元格中的代码,除了名称的选择。

[In ]:
def predict_child(p_avg):
    """Predict the height of a child whose parents have a parent average height of p_avg.
    
    The prediction is the average height of the children whose parent average height is
    in the range p_avg plus or minus 0.5.
    """
    
    close_points = heights.where('Parent Average', are.between(p_avg-0.5, p_avg + 0.5))
    return np.average(close_points.column('Child'))                     

给定父母平均身高68英寸,函数 predict_child 返回与我们之前得到的相同的预测值(67.62英寸)。定义函数的好处是我们可以轻松更改预测变量的值并获得新的预测。

[In ]:
predict_child(68)
67.62
[In ]:
predict_child(66)
66.08640776699029

这些预测有多好?我们可以通过将预测与我们已有的数据进行比较来了解这一点。为此,我们首先将函数 predict_child 应用到 Parent Average 身高列,并将结果收集到一个名为 Prediction 的新列中。

[In ]:
# Apply predict_child to all the midparent heights

heights_with_predictions = heights.with_column(
    'Prediction', heights.apply(predict_child, 'Parent Average')
)
[In ]:
heights_with_predictions
Parent Average | Child | Prediction
72.75          | 73.2  | 70.1
72.75          | 69.2  | 70.1
72.75          | 69    | 70.1
72.75          | 69    | 70.1
71             | 73.5  | 70.4158
71             | 72.5  | 70.4158
71             | 65.5  | 70.4158
71             | 65.5  | 70.4158
69.5           | 71    | 68.5025
69.5           | 68    | 68.5025
... (924 rows omitted)

为了查看预测值相对于观测数据的位置,我们可以绘制以 Parent Average 为公共水平轴的叠加散点图。

[In ]:
heights_with_predictions.scatter('Parent Average')
The same scatterplot as before is shown (with the original y-axis 55-80). The points are now either dark blue as before labeled 'Child' or gold 'Prediction.' The prediction datapoints sit in the center of the data and have the same x-values as the original data.

金色点的图被称为“平均图”,因为每个金色点是我们之前绘制的垂直条带的中心。每个点都提供了给定父母平均身高时孩子身高的预测。例如,散点图显示,对于65英寸的父母平均身高,孩子的预测身高略高于65英寸,而 predict_child(65) 的计算结果约为65.84。

[In ]:
predict_child(65)
65.83829787234043

注意平均图大致沿一条直线。这条直线现在被称为“回归线”,是最常见的预测方法之一。我们刚刚做的计算与导致回归方法发展的计算非常相似,使用了相同的数据。

这个例子,就像约翰·斯诺(John Snow)分析霍乱死亡人数的例子一样,展示了现代数据科学的一些基本概念有着悠久的根源。这里使用的方法是“最近邻”预测方法的前身,这些方法现在在不同领域有着强大的应用。现代的“机器学习”领域包括自动化此类方法,以基于庞大且快速演化的数据集进行预测。

[In ]: