# 函数和表格 通过使用 Python 中已有的函数,我们正在建立一个使用的技术清单,用于识别数据集中的规律和主题。 现在我们将探索Python编程语言的核心功能:函数定义。 我们在本书中已经广泛使用了函数,但从未定义过我们自己的函数。定义一个函数的目的是,给一个计算过程命名,它可能会使用多次。计算中有许多需要重复计算的情况。 例如,我们常常希望对表的列中的每个值执行相同的操作。 ## 定义函数 `double`函数的定义仅仅使一个数值加倍。 ```py # Our first function definition def double(x): """ Double x """ return 2*x ``` 我们通过编写`def`来开始定义任何函数。 下面是这个小函数的其他部分(语法)的细分: 当我们运行上面的单元格时,没有使特定的数字加倍,并且`double`主体中的代码还没有求值。因此,我们的函数类似于一个菜谱。 每次我们遵循菜谱中的指导,我们都需要以食材开始。 每次我们想用我们的函数来使一个数字加倍时,我们需要指定一个数字。 我们可以用和调用其他函数完全相同的方式,来调用`double`。 每次我们这样做的时候,主体中的代码都会执行,参数的值赋给了名称`x`。 ```py double(17) 34 double(-0.6/4) -0.3 ``` 以上两个表达式都是调用表达式。 在第二个里面,计算了表达式`-0.6 / 4`的值,然后将其作为参数`x`传递给`double`函数。 每个调用表达式最终都会执行`double`的主体,但使用不同的`x`值。 `double`的主体只有一行: ```py return 2*x ``` 执行这个`return`语句会完成`double`函数体的执行,并计算调用表达式的值。 `double`的参数可以是任何表达式,只要它的值是一个数字。 例如,它可以是一个名称。 `double`函数不知道或不在意如何计算或存储参数。 它唯一的工作是,使用传递给它的参数的值来执行它自己的主体。 ```py any_name = 42 double(any_name) 84 ``` 参数也可以是任何可以加倍的值。例如,可以将整个数值数组作为参数传递给`double`,结果将是另一个数组。 ```py double(make_array(3, 4, 5)) array([ 6, 8, 10]) ``` 但是,函数内部定义的名称(包括像double的x这样的参数)只存在一小会儿。 它们只在函数被调用的时候被定义,并且只能在函数体内被访问。 我们不能在`double`之外引用`x`。 技术术语是`x`具有局部作用域。 因此,即使我们在上面的单元格中调用了`double`,名称`x`也不能在函数体外识别。 ```py x --------------------------------------------------------------------------- NameError Traceback (most recent call last) in () ----> 1 x NameError: name 'x' is not defined ``` 文档字符串。 虽然`double`比较容易理解,但是很多函数执行复杂的任务,并且没有解释就很难使用。 (你自己也可能已经发现了!)因此,一个组成良好的函数有一个唤起它的行为的名字,以及文档。 在 Python 中,这被称为文档字符串 - 描述了它的行为和对其参数的预期。 文档字符串也可以展示函数的示例调用,其中调用前面是`>>>`。 文档字符串可以是任何字符串,只要它是函数体中的第一个东西。 文档字符串通常在开始和结束处使用三个引号来定义,这允许字符串跨越多行。 第一行通常是函数的完整但简短的描述,而下面的行则为将来的用户提供了进一步的指导。 下面是一个名为`percent`的函数定义,它带有两个参数。定义包括一个文档字符串。 ```py # A function with more than one argument def percent(x, total): """Convert x to a percentage of total. More precisely, this function divides x by total, multiplies the result by 100, and rounds the result to two decimal places. >>> percent(4, 16) 25.0 >>> percent(1, 6) 16.67 """ return round((x/total)*100, 2) percent(33, 200) 16.5 ``` 将上面定义的函数`percent`与下面定义的函数`percents`进行对比。 后者以数组为参数,将数组中的所有数字转换为数组中所有值的百分数。 百分数都四舍五入到两位,这次使用`round`来代替`np.round`,因为参数是一个数组而不是一个数字。 ```py def percents(counts): """Convert the values in array_x to percents out of the total of array_x.""" total = counts.sum() return np.round((counts/total)*100, 2) ``` 函数`percents`返回一个百分数的数组,除了四舍五入之外,它总计是 100。 ```py some_array = make_array(7, 10, 4) percents(some_array) array([ 33.33, 47.62, 19.05]) ``` 理解 Python 执行函数的步骤是有帮助的。 为了方便起见,我们在下面的同一个单元格中放入了函数定义和对这个函数的调用。 ```py def biggest_difference(array_x): """Find the biggest difference in absolute value between two adjacent elements of array_x.""" diffs = np.diff(array_x) absolute_diffs = abs(diffs) return max(absolute_diffs) some_numbers = make_array(2, 4, 5, 6, 4, -1, 1) big_diff = biggest_difference(some_numbers) print("The biggest difference is", big_diff) The biggest difference is 5 ``` 这就是当我们运行单元格时,所发生的事情。 ## 多个参数 可以有多种方式来推广一个表达式或代码块,因此一个函数可以有多个参数,每个参数决定结果的不同方面。 例如,我们以前定义的百分比`percents`,每次都四舍五入到两位。 以下两个参的数定义允许不同调用四舍五入到不同的位数。 ```py def percents(counts, decimal_places): """Convert the values in array_x to percents out of the total of array_x.""" total = counts.sum() return np.round((counts/total)*100, decimal_places) parts = make_array(2, 1, 4) print("Rounded to 1 decimal place: ", percents(parts, 1)) print("Rounded to 2 decimal places:", percents(parts, 2)) print("Rounded to 3 decimal places:", percents(parts, 3)) Rounded to 1 decimal place: [ 28.6 14.3 57.1] Rounded to 2 decimal places: [ 28.57 14.29 57.14] Rounded to 3 decimal places: [ 28.571 14.286 57.143] ``` 这个新定义的灵活性来源于一个小的代价:每次调用该函数时,都必须指定小数位数。默认参数值允许使用可变数量的参数调用函数;在调用表达式中未指定的任何参数都被赋予其默认值,这在`def`语句的第一行中进行了说明。 例如,在`percents`的最终定义中,可选参数`decimal_places`赋为默认值`2`。 ```py def percents(counts, decimal_places=2): """Convert the values in array_x to percents out of the total of array_x.""" total = counts.sum() return np.round((counts/total)*100, decimal_places) parts = make_array(2, 1, 4) print("Rounded to 1 decimal place:", percents(parts, 1)) print("Rounded to the default number of decimal places:", percents(parts)) Rounded to 1 decimal place: [ 28.6 14.3 57.1] Rounded to the default number of decimal places: [ 28.57 14.29 57.14] ``` ## 注:方法 函数通过将参数表达式放入函数名称后面的括号来调用。 任何独立定义的函数都是这样调用的。 你也看到了方法的例子,这些方法就像函数一样,但是用点符号来调用,比如`some_table.sort(some_label)`。 你定义的函数将始终首先使用函数名称,并传入所有参数来调用。 ## 在列上应用函数 我们已经看到很多例子,通过将函数应用于现有列或其他数组,来创建新的表格的列。 所有这些函数都以数组作为参数。 但是我们经常打算,通过一个函数转换列中的条目,它不将数组作为它的函数。 例如,它可能只需要一个数字作为它的参数,就像下面定义的函数`cut_off_at_100`。 ```py def cut_off_at_100(x): """The smaller of x and 100""" return min(x, 100) cut_off_at_100(17) 17 cut_off_at_100(117) 100 cut_off_at_100(100) 100 ``` 如果参数小于或等于 100,函数`cut_off_at_100`只返回它的参数。但是如果参数大于 100,则返回 100。 在我们之前使用人口普查数据的例子中,我们看到变量`AGE`的值为 100,表示“100 岁以上”。 以这种方式将年龄限制在 100 岁,正是`cut_off_at_100`所做的。 为了一次性对很多年龄使用这个函数,我们必须能够引用函数本身,而不用实际调用它。 类似地,我们可能会向厨师展示一个蛋糕的菜谱,并要求她用它来烤 6 个蛋糕。 在这种情况下,我们不会使用这个配方自己烘烤蛋糕, 我们的角色只是把菜谱给厨师。 同样,我们可以要求一个表格,在列中的 6 个不同的数字上调用`cut_off_at_100`。 首先,我们创建了一个表,一列是人,一列是它们的年龄。 例如,`C`是 52 岁。 ```py 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 | ### 应用 要在 100 岁截断年龄,我们将使用一个新的`Table`方法。 `apply`方法在列的每个元素上调用一个函数,形成一个返回值的新数组。 为了指出要调用的函数,只需将其命名(不带引号或括号)。 输入值的列的名称必须是字符串,仍然出现在引号内。 ```py ages.apply(cut_off_at_100, 'Age') array([ 17, 100, 52, 100, 6, 100]) ``` 我们在这里所做的是,将`cut_off_at_100`函数应用于`age`表的`Age`列中的每个值。 输出是函数的相应返回值的数组。 例如,17 还是 17,117 变成了 100,52 还是 52,等等。 此数组的长度与`age`表中原始`Age`列的长度相同,可用作名为`Cut Off Age`的新列中的值,并与现有的`Person`和`Age`列共存。 ```py 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()`是一个空表,`age`是一个表值(因为我们在上面定义)的名称。 在 Python 中,每个函数(包括`cut_off_at_100`)也是一个值。 这有助于再次考虑菜谱。 蛋糕的菜谱是一个真实的东西,不同于蛋糕或配料,你可以给它一个名字,像“阿尼的蛋糕菜谱”。 当我们用`def`语句定义`cut_off_at_100`时,我们实际上做了两件事情:我们创建了一个函数来截断数字 100,我们给它命名为`cut_off_at_100`。 我们可以引用任何函数,通过写下它的名字,而没有实际调用它必需的括号或参数。当我们在上面调用`apply`时,我们做了这个。 当我们自己写下一个函数的名字,作为单元格中的最后一行时,Python 会生成一个函数的文本表示,就像打印一个数字或一个字符串值一样。 ```py cut_off_at_100 ``` 请注意,我们没有使用引号(它只是一段文本)或`cut_off_at_100()`(它是一个函数调用,而且是无效的)。我们只是写下`cut_off_at_100`来引用这个函数。 就像我们可以为其他值定义新名称一样,我们可以为函数定义新名称。 例如,假设我们想把我们的函数称为`cut_off`,而不是`cut_off_at_100`。 我们可以这样写: ```py cut_off = cut_off_at_100 ``` 现在`cut_off`就是函数名称了。它是`cut_off_at_100`的相同函数。所以打印出的值应该相同。 ```py cut_off ``` 让我们看看另一个`apply`的应用。 ### 示例:预测 数据科学经常用来预测未来。 如果我们试图预测特定个体的结果 - 例如,她将如何回应处理方式,或者他是否会购买产品,那么将预测基于其他类似个体的结果是很自然的。 查尔斯·达尔文(Charles Darwin)的堂兄弗朗西斯·高尔顿(Sir Francis Galton)是使用这个思想来基于数值数据进行预测的先驱。 他研究了物理特征是如何传递下来的。 下面的数据是父母和他们的成年子女的身高测量值,由高尔顿仔细收集。 每行对应一个成年子女。 变量是家庭的数字代码,父母的身高(以英寸为单位),“双亲身高”,这是父母双方身高的加权平均值 [1],家庭中子女的数量 ,以及子女的出生次序(第几个),性别和身高。 > [1] 高尔顿在计算男性和女性的平均身高之前,将女性身高乘上 1.08。对于这个的讨论,请查看 [Chance](http://chance.amstat.org/2013/09/1-pagano/),这是一个由美国统计协会出版的杂志。 ```py # Galton's data on heights of parents and their adult children galton = Table.read_table('galton.csv') galton ``` | family | father | mother | midparentHeight | children | childNum | gender | childHeight | | --- | --- | --- | --- | --- | --- | --- | --- | | 1 | 78.5 | 67 | 75.43 | 4 | 1 | male | 73.2 | | 1 | 78.5 | 67 | 75.43 | 4 | 2 | female | 69.2 | | 1 | 78.5 | 67 | 75.43 | 4 | 3 | female | 69 | | 1 | 78.5 | 67 | 75.43 | 4 | 4 | female | 69 | | 2 | 75.5 | 66.5 | 73.66 | 4 | 1 | male | 73.5 | | 2 | 75.5 | 66.5 | 73.66 | 4 | 2 | male | 72.5 | | 2 | 75.5 | 66.5 | 73.66 | 4 | 3 | female | 65.5 | | 2 | 75.5 | 66.5 | 73.66 | 4 | 4 | female | 65.5 | | 3 | 75 | 64 | 72.06 | 2 | 1 | male | 71 | | 3 | 75 | 64 | 72.06 | 2 | 2 | female | 68 | (省略了 924 行) 收集数据的主要原因是,能够预测父母所生的子女的成年身高,其中父母和数据集中的类似。让我们尝试这样做,用双亲的身高作为我们预测的基础变量。 因此双亲的身高是我们的预测性变量。 表格`heights`包含双亲和子女的身高。 两个变量的散点图显示了正相关,正如我们对这些变量的预期。 ```py heights = galton.select(3, 7).relabeled(0, 'MidParent').relabeled(1, 'Child') heights ``` | MidParent | Child | | --- | --- | | 75.43 | 73.2 | | 75.43 | 69.2 | | 75.43 | 69 | | 75.43 | 69 | | 73.66 | 73.5 | | 73.66 | 72.5 | | 73.66 | 65.5 | | 73.66 | 65.5 | | 72.06 | 71 | | 72.06 | 68 | (省略了 924 行) ```py heights.scatter(0) ``` 现在假设高尔顿遇到了新的一对夫妇,与他的数据集类似,并且想知道他们的子女有多高。考虑到双亲身高是 68 英寸,他预测子女身高的一个好方法是什么? 一个合理的方法是基于约 68 英寸的双亲身高对应的所有点,来做预测。预测值等于从这些点计算的子女身高的均值。 假设我们是高尔顿,并执行这个计划。现在我们只是对“68 英寸左右”的含义做一个合理的定义,并用它来处理。在课程的后面,我们将研究这种选择的后果。 我们的“接近”的意思是“在半英寸之内”。下图显示了 67.5 英寸和 68.5 英寸之间的双亲身高对应的所有点。这些都是红色直线之间的点。每一个点都对应一个子女;我们对新夫妇的子女身高的预测是所有子女的平均身高。这由金色的点表示。 忽略代码,仅仅专注于理解到达金色的点的心理过程。 ```py heights.scatter('MidParent') _ = 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, 66.24, color='gold', s=40) ``` 为了准确计算出金色的点的位置,我们首先需要确定直线之间的所有点。 这些点对应于`MidParent`在 67.5 英寸和 68.5 英寸之间的行。 ```py close_to_68 = heights.where('MidParent', are.between(67.5, 68.5)) close_to_68 ``` | MidParent | Child | | --- | --- | | 68.44 | 62 | | 67.94 | 71.2 | | 67.94 | 67 | | 68.33 | 62.5 | | 68.23 | 73 | | 68.23 | 72 | | 68.23 | 69 | | 67.98 | 73 | | 67.98 | 71 | | 67.98 | 71 | (省略了 121 行) 双亲身高为 68 英寸的子女的预测身高,是这些行中子女的平均身高。 这是 66.24 英寸。 ```py close_to_68.column('Child').mean() 66.24045801526718 ``` 我们现在有了一种方法,给定任何数据集中的双亲身高,就可以预测子女的身高。我们可以定义一个函数`predict_child`来实现它。 除了名称的选择之外,函数的主体由上面两个单元格中的代码组成。 ```py def predict_child(mpht): """Predict the height of a child whose parents have a midparent height of mpht. The prediction is the average height of the children whose midparent height is in the range mpht plus or minus 0.5. """ close_points = heights.where('MidParent', are.between(mpht-0.5, mpht + 0.5)) return close_points.column('Child').mean() ``` 给定 68 英寸的双亲身高,函数`predict_child`返回与之前相同的预测(66.24 英寸)。 定义函数的好处在于,我们可以很容易地改变预测变量的值,并得到一个新的预测结果。 ```py predict_child(68) 66.24045801526718 predict_child(74) 70.415789473684214 ``` 这些预测有多好? 我们可以了解它,通过将预测值与我们已有的数据进行比较。 为此,我们首先将函数`predict_child`应用于`Midparent`列,并将结果收入称为`Prediction`的新列中。 ```py # Apply predict_child to all the midparent heights heights_with_predictions = heights.with_column( 'Prediction', heights.apply(predict_child, 'MidParent') ) heights_with_predictions ``` | MidParent | Child | Prediction | | --- | --- | --- | | 75.43 | 73.2 | 70.1 | | 75.43 | 69.2 | 70.1 | | 75.43 | 69 | 70.1 | | 75.43 | 69 | 70.1 | | 73.66 | 73.5 | 70.4158 | | 73.66 | 72.5 | 70.4158 | | 73.66 | 65.5 | 70.4158 | | 73.66 | 65.5 | 70.4158 | | 72.06 | 71 | 68.5025 | | 72.06 | 68 | 68.5025 | (省略了 924 行) 为了查看预测值相对于观察数据的位置,可以使用`MidParent`作为公共水平轴绘制重叠的散点图。 ```py heights_with_predictions.scatter('MidParent') ``` 金色的点的图形称为均值图,因为每个金色的点都是两条直线的中心,就像之前绘制的那样。每个都按照给定的双亲高度,做出了子女高度的预测。例如,散点图显示,对于 72 英寸的双亲高度,子女的预测高度将在 68 英寸和 69 英寸之间,事实上,`predict_child(72)`返回 68.5。 高尔顿的计算和可视化与我们非常相似,除了他没有 Python。他通过散点图绘制了均值图,并注意到它大致沿着直线。这条直线现在被称为回归线,是最常见的预测方法之一。高尔顿的朋友,数学家卡尔·皮尔森(Karl Pearson)用这些分析来形式化关联的概念。 这个例子,就像约翰·斯诺(John Snow)对霍乱死亡的分析一样,说明了现代数据科学的一些基本概念的根源可追溯到一个多世纪之前。高尔顿的方法,比如我们在这里使用的方法,是最近邻预测方法的雏形,现在在不同的环境中有着有效的应用。机器学习的现代领域包括这些方法的自动化,来基于庞大且快速发展的数据集进行预测。 ## 按照单变量分类 数据科学家经常需要根据共有的特征,将个体分成不同的组,然后确定组的一些特征。 例如,在使用高尔顿高度数据的例子中,我们看到根据父母的平均高度对家庭进行分类,然后找出每个小组中子女的平均身高,较为实用。 这部分关于将个体分类到非数值类别。我们从回顾`gourp`的基本用法开始。 ### 计算每个分类的数量 具有单个参数的`group `方法计算列中每个值的数量。 结果中,用于分组的列中的每个唯一值是一行。 这是一个关于冰淇淋圆通的小型数据表。 `group `方法可以用来列出不同的口味,并提供每种口味的计数。 ```py cones = Table().with_columns( 'Flavor', make_array('strawberry', 'chocolate', 'chocolate', 'strawberry', 'chocolate'), 'Price', make_array(3.55, 4.75, 6.55, 5.25, 5.25) ) cones ``` | Flavor | Price | | --- | --- | | strawberry | 3.55 | | chocolate | 4.75 | | chocolate | 6.55 | | strawberry | 5.25 | | chocolate | 5.25 | ```py cones.group('Flavor') ``` | Flavor | count | | --- | --- | | chocolate | 3 | | strawberry | 2 |