第 9 章 数据探索和分析

既然已经花费了一些时间获取和清洗数据,你可以开始做数据分析了!不要对结果有太多期望,这对于你的数据探索过程来说很重要。你的问题可能对于某个答案来说太宽泛,也可能没有结论性的答案。回想一下你在第一节自然科学课程中学到的有关假设和结论的知识。最好用同样的方法来进行数据探索,并且要理解,在数据分析中你可能不会得到一个清晰的结论。

尽管如此,只是去探索数据并发现数据中没有趋势或者趋势不符合预期,这就很有趣。如果一切都如我们所愿,数据处理会变得有些无聊。我们已学会少一点期待,多一点探索。

第 9 章 数据探索和分析 - 图1 当你开始分析和探索数据时,可能会意识到需要更多的或不同种类的数据。在你更深入地定义想要回答的问题,并检验数据告诉你什么的过程中,这是很常见的一种情况,你需要接受。

现在也非常适合回顾一下你最初发现数据集时所提出的问题。你想知道什么?是否还有其他相关的问题有助于你的探索?这些问题可能会指出方向,告诉你在哪能找到故事。即使没有,这些问题也会指引你发现另外一些有趣的问题。即使你不能回答最初的问题,也能够对话题有更深入的了解,并发现新的问题去探索。

在这一章,我们会学习一些新的用于数据探索和分析的 Python 库,并且继续应用我们在前两章学到的清洗数据的知识。我们会学习如何合并数据集,探索数据,得到有关数据集中关系的统计学结论。

9.1 探索数据

在前两章你已经学习了解析和清洗数据,想必已经很熟悉用 Python 来与数据交互了。现在我们要使用 Python 更加深入地探索数据。

首先我们要安装将用到的 Python 库 agatehttp://agate.readthedocs.org),它可以帮助我们发现数据的一些基本特征。agate 是一个数据分析库,由 Christopher Groskopf(https:// github.com/onyxfish)编写。Christopher 是一位拥有高超技术水平的数据记者和 Python 开发者,而 agate 库会帮助我们了解数据。使用 pip 安装这个库:

  1. pip install agate

第 9 章 数据探索和分析 - 图2 这一章中的代码与 agate 1.2.0 版本兼容。因为 agate 是一个相对较新的 Python 库,所以随着库的成熟,其中的一些功能是有可能发生改变的。为确保安装的是指定版本的库,你可以使用 pip 设置版本。对本书来说,你可以使用:pip install agate==1.2.0 来安装 agate。我们同样推荐你测试最新的版本,并随时了解书中用到的库的最新代码变化。

我们想要探索 agate 库的一些特性。为了达到这个目的,我们将会使用从 UNICEF 年报得到的关于童工雇用的数据(http://data.unicef.org/child-protection/child-labour.html)。

9.1.1 导入数据

首先,来看一下我们的第一个数据集——UNICEF 的童工汇总数据。我们下载的数据是一个 Excel 文件,包含全世界的童工雇用率列表。我们可以使用学到的关于 Excel 的知识以及从第 4 章和第 7 章学到的数据清洗技术,将原始数据转化为 agate 库所接受的格式。

第 9 章 数据探索和分析 - 图3 在处理 Excel 表单时,我们推荐你用喜欢的 Excel 查看器打开表单文件。这样我们更容易比对 Python“看到”的结果和我们从表单中看到的结果,这有助于数据的查找和提取。

首先,我们导入可能会用到的 Python 库,并且将 Excel 文件加载到 xlrd 中。

  1. import xlrd
  2. import agate
  3. workbook = xlrd.open_workbook('unicef_oct_2014.xls')
  4. workbook.nsheets
  5. workbook.sheet_names()

现在将 Excel 中的数据加载到变量 workbook 中了。这个工作表包含一个表单,叫作 Child labour。

如果你在 IPython 终端中运行上面这段代码(我们推荐使用 IPython,因为你会看到更多的输出),应该看到下面的输出:

  1. In [6]: workbook.nsheets
  2. Out[6]: 1
  3. In [7]: workbook.sheet_names()
  4. Out[7]: [u'Child labour ']

选择这个表单,这样你可以将其导入 agate 库。根据 agate 库的文档(http://agate.readthedocs.io/en/latest/tutorial.html),agate 库可以使用一个标题列表、一个各数据列的类型列表和一个数据读取器(或可迭代的数据列表)来导入数据。所以,我们需要数据的类型,这样才可以顺利地从表单中导入数据到 agate 库。

  1. sheet = workbook.sheets()[0]
  2. sheet.nrows
  3. sheet.row_values(0)
  4. for r in range(sheet.nrows):
  5. print r, sheet.row(r)

nrows 标识表单中共有多少列。

row_values 允许选取一行数据,并且展示这行的值。上面这种情况下,会展示表单数据的标题,因为标题是 Excel 文件的第一行。

❸ 通过使用 rangefor 遍历每一行数据,我们可以像 Python 一样查看每一行数据。表单的 row 方法会返回数据的一些信息和每行数据的类型。

我们从第 3 章了解到,csv 库接受一个打开的文件,并且将其转化为一个迭代器(iterator)。迭代器是一个可以迭代或遍历的对象,每一次返回其对应的值。在代码中,迭代器在展开数据方面比列表更高效,拥有速度和性能的优势。

第 9 章 数据探索和分析 - 图4 因为我们正在处理的数据集很小,所以可以创建一个列表,用其代替迭代器来传参。大多数需要迭代器的库可以处理任何可迭代对象(如列表)。通过这种方式,我们依然遵从 xldragate 所期待的方式。

首先,让我们获取每一列的标题。在之前的输出中,我们可以看到标题在第 4 行和第 5 行。可以使用 zip 来合并标题行:

  1. title_rows = zip(sheet.row_values(4), sheet.row_values(5))
  2. title_rows

现在可以看到变量 title_rows 的值为:

  1. [('', u'Countries and areas'),
  2. (u'Total (%)', ''),
  3. ('', ''),
  4. (u'Sex (%)', u'Male'),
  5. ('', u'Female'),
  6. (u'Place of residence (%)', u'Urban'),
  7. ('', u'Rural'),
  8. (u'Household wealth quintile (%)', u'Poorest'),
  9. ('', u'Second'),
  10. ('', u'Middle'),
  11. ('', u'Fourth'),
  12. ('', u'Richest'),
  13. (u'Reference Year', ''),
  14. (u'Data Source', '')]

同时使用两行信息(第 4 行和第 5 行),会保留如果我们仅使用其中一行信息丢失的那部分信息。这是个很好的选择,我们还可以再花点时间来改进这一点,但是,对于最初的数据探索来说,这是一个很好的开始。标题数据当前是一个元组列表。我们知道 agate 库希望得到一个元组列表,其中第一个值为标题的字符串,这样我们需要将标题转换成字符串列表。

  1. titles = [t[0] + ' ' + t[1] for t in title_rows]
  2. print titles
  3. titles = [t.strip() for t in titles]

在这段代码里面,我们使用了两个列表生成式。在第一个中,我们把 title_rows 列表传入,它是一个元组列表。在这些元组中,存有 Excel 文件中标题行的内容字符串。

第一个列表生成式使用了元组的两个部分(通过元组索引)来拼接一个字符串。我们将每一个元组的值合并在一起,出于可读性的考虑,使用 ' ' 做间隔。现在我们的标题列表只由字符串组成——原来的元组不见了!我们使得标题变得有一些复杂,因为并不是每一个元组都有两个值。通过添加空格分隔,我们创建了一些以空格为开始的标题,像 'Female'

为了删除起始位置的空格,在第二个迭代器中,我们使用 strip 字符串方法,这个方法会移除字符串最开始和最后的空格。现在标题变量有了整洁的字符串列表,能够很好地在 agate 库中使用。

标题已经准备完毕,现在需要从 Excel 文件中选择要使用的数据行。我们的表单有国家和大洲的数据,让我们首先聚焦于国家的数据。我们想要避免意外地将不同类别的数据混合在一起。通过之前的代码输出,我们知道第 6 行至第 114 行是我们想要使用的。我们会使用 row_values 方法来返回 xlrd 表单对象中这些行中的值。

  1. country_rows = [sheet.row_values(r) for r in range(6, 114)]

现在我们有了标题列表和数据列表,所以只需要定义导入 agate 库中的类型。根据关于定义列的文档(http://agate.readthedocs.org/en/latest/tutorial.html#defining-the-columns),我们有文本、布尔、数字和日期 4 种列类型,同时库作者建议,如果数据的类型不确定,就使用文本类型。这里同样有内置的 TypeTesterhttp://agate.readthedocs.io/en/latest/api/data_types.html),可以用其猜测数据类型。首先,使用一些 xlrd 的内置函数来定义列 :

  1. from xlrd.sheet import ctype_text
  2. import agate
  3. text_type = agate.Text()
  4. number_type = agate.Number()
  5. boolean_type = agate.Boolean()
  6. date_type = agate.Date()
  7. example_row = sheet.row(6)
  8. print example_row
  9. print example_row[0].ctype
  10. print example_row[0].value
  11. print ctype_text

❶ 通过打印来检查这一行的值,我们看到有完好的数据。xlrd 会检查所有的数据,保证不会有空行数据的存在。

❷ 这两行代码中,我们调用 ctypevalue 属性来得到数据行中每一个元素的类型和值属性。

第 9 章 数据探索和分析 - 图5 当使用 IPython 时,通过创建一个你关心的变量的新对象,在末尾添加句号,并敲击 Tab 键,就可以轻松地找到新的方法和属性。这会展开一个属性和方法的列表,以便于你更深入地探索。

❸ 使用 xlrd 库中的 ctype_text 对象,我们可以匹配 ctype 方法返回的整数对象,映射它们到可阅读的字符串。这可以代替手动映射类型。

这几行代码让我们更好地了解了可用于定义类型的工具。ctype 方法和 ctype_text 对象可以用来排序和展示给定样例数据行中的数据类型。

第 9 章 数据探索和分析 - 图6 虽然看起来我们为了用这种方式创建列表做了好多工作,但这些工作提供了可复用的能力,这会在之后节省你的时间。重用这些代码片段会在之后为你节省大量宝贵时间,同时也是编写你自己的代码中非常有趣的一面。

现在我们知道哪些函数可以用来探索 Excel 列的数据类型,所以需要尝试为 agate 库创建一个类型列表。我们需要遍历数据行,使用 ctype 来映射列类型 :

  1. types = []
  2. for v in example_row:
  3. value_type = ctype_text[v.ctype]
  4. if value_type == 'text':
  5. types.append(text_type)
  6. elif value_type == 'number':
  7. types.append(number_type)
  8. elif value_type == 'xldate':
  9. types.append(date_type)
  10. else:
  11. types.append(text_type)

❶ 映射我们在探索每一行数据的 ctype 属性时找到的整数值到 ctype_text 字典中,使它们变得可读。现在 value_type 中保存了数据的列类型字符串(也就是文本、数字等)。

❷ 使用 ifelif 语句以及 == 操作符将 value_typeagate 列类型匹配。之后,代码将相应的类型追加到列表中,继续下一个列类型。

❸ 正如库文档中建议的那样,如果这里没有类型匹配上,我们将文本列类型追加到列表中。

现在我们构建了一个函数来接受一个空列表,遍历所有的列,并且创建一个包含数据集中所有列类型的列表。在运行代码之后,我们就有了需要的类型、标题和数据列表。可以将标题和类型打包在一起,通过运行下面这行代码,将结果导入到 agate 表中:

  1. table = agate.Table(country_rows, titles, types)

当你运行这段代码时,会看到 CastError,同时还有 Can not convert value “-” to Decimal for NumberColumn 这行错误信息。

正像第 7 章和第 8 章提到的那样,学习如何清洗数据是数据处理过程中必要的一部分。编写文档完备的代码会让你在未来节省时间。通过阅读这些错误信息,我们意识到有一些坏数据隐藏在某个数据列中。在表单中的一些地方,数据中出现了 '-',而不是 '',这会被当作空值来处理。我们可以编写一个函数来处理这个问题。

  1. def remove_bad_chars(val):
  2. if val == '-':
  3. return None
  4. return val
  5. cleaned_rows = []
  6. for row in country_rows:
  7. cleaned_row = [remove_bad_chars(rv) for rv in row]
  8. cleaned_rows.append(cleaned_row)

❶ 定义函数来去除坏字符(例如整数列中的 '-' 字符)。

❷ 如果值与 '-' 相等,选择这个值准备替换。

❸ 如果值为 '-',返回 None

❹ 遍历 country_rows 来创建一个新的清洗后的列表,包含合法的数据。

❺ 创建一个 cleaned_rows 列表包含清洗后的数据(通过 append 方法)。

第 9 章 数据探索和分析 - 图7 当我们编写函数来修改值的时候,在主逻辑之外保证一个默认的返回(像示例中那样),以确保永远返回一个值。

使用这个函数,我们可以确保整数列拥有 None 类型,而不是 '-'None 告诉 Python,这是一个空数据,在与其他数字比较分析时忽略这个数据。

因为想要复用清洗和改变类型的代码,所以我们把一些已经编好的代码转变成更加抽象和通用的辅助函数。创建最后的清洗函数时,我们创建了一个新的列表,遍历所有行的数据,对每一行数据做了清洗,并为 agate 表返回了一个新的数据列表。让我们看一下,是否可以使用这些概念并抽象它们。

  1. def get_new_array(old_array, function_to_clean):
  2. new_arr = []
  3. for row in old_array:
  4. cleaned_row = [function_to_clean(rv) for rv in row]
  5. new_arr.append(cleaned_row)
  6. return new_arr
  7. cleaned_rows = get_new_array(country_rows, remove_bad_chars)

❶ 定义函数,让其接受两个参数:老的数据数组和清洗数据的函数。

❷ 用更抽象的名称复用我们的代码。在函数的最后,返回新的清洗后的数组。

❸ 使用 remove_bad_chars 函数作为参数调用这个函数,保存清洗后的结果到 cleaned_rows

现在尝试重新运行代码来创建一个表:

  1. In [10]: table = agate.Table(cleaned_rows, titles, types)
  2. In [11]: table
  3. Out[11]: <agate.table.Table at 0x7f9adc489990>

呜啊!我们有了一个 table 变量,保存着一个 Table 对象。现在可以用 agate 库的函数查看数据了。如果你急切地想知道表看起来是什么样的,使用 print_table 方法快速地查看一下表中的内容:

  1. table.print_table(max_columns=7)

第 9 章 数据探索和分析 - 图8 如果至此你一直使用 IPython,并且想确保可以在下一个会话中继续使用这些变量,就使用 %storehttps://ipython.org/ipython-doc/3/config/extensions/storemagic.html)命令。如果希望保存 table 变量,我们可以简单地输入 %store table。在我们的下一个 IPython 会话中,可以通过输入 %store -r 恢复 table 变量。在分析数据的过程中,这对“保存”你的工作非常有用。

下面,我们会深入查看表数据,并使用一些内置的研究工具。

9.1.2 探索表函数

agate 库提供了许多函数来探索数据。首先,我们尝试一些排序方法(http://agate.readthedocs.org/en/latest/tutorial.html?#sorting-and-slicing)。让我们尝试为表排序。通过对童工雇用率的总百分比的列排序,我们可以看到最过分的国家。我们会使用 limithttp://agate.readthedocs.io/en/latest/api/table.html)函数来查看雇用率最高的 10 个国家。

  1. table.column_names
  2. most_egregious = table.order_by('Total (%)', reverse=True).limit(10)
  3. for r in most_egregious.rows:
  4. print r

❶ 检查列名称,这样我们知道要使用的是什么列。

❷ 链式调用 order_bylimit 方法来创建新的表。因为 order_by 会按从最小到最大来排序,所以我们使用 reverse 参数来让其从大到小排序。

❸ 使用新表的 rows 属性,遍历童工雇用情况最糟糕的 10 个国家。

运行这段代码会返回童工雇用率最高的 10 个国家。在儿童工作百分比方面,非洲国家大量出现在列表前列。这是我们第一个有趣的发现!让我们继续探索。为了探究哪些国家的女童雇用率最高,我们可以再一次使用 order_bylimit 函数。这一次,我们将它们应用到女童百分比数据列上:

  1. most_females = table.order_by('Female', reverse=True).limit(10)
  2. for r in most_females.rows:
  3. print '{}: {}%'.format(r['Countries and areas'], r['Female'])

第 9 章 数据探索和分析 - 图9 当第一次探索数据时,使用 Python 的 format 函数会让输出更易阅读,而不是简单地输出每一行数据。这意味着你可以专注于数据本身,而不是费劲去阅读它们。

我们看到了数据中有一些 None 的百分比数据。这不是我们想要的!我们可以使用 agate 表的 where 方法清除这些数据,像下面的代码一样。这一方法类似于 SQL 中的 WHERE 语句,或者 Python 中的 if 语句。where 创建另外一个只包含符合条件的数据行的表。

  1. female_data = table.where(lambda r: r['Female'] is not None)
  2. most_females = female_data.order_by('Female', reverse=True).limit(10)
  3. for r in most_females.rows:
  4. print '{}: {}%'.format(r['Countries and areas'], r['Female'])

首先创建 female_data 表,其中使用了 Python 的 lambda 函数来保证每一行数据有 Female 列存在。where 函数接受 lambda 函数返回的布尔值,并且只在值为真时将数据分离出来。将只含有女性童工雇用数据的行分离出来后,我们使用相同的排序、截断和格式化技巧,来查看女童雇用率非常高的国家列表。

lambda

Python 的 lambda 函数允许我们编写一个单行函数,并且作为一个参数进行传递。这对于我们在这一节探索中所碰到的情况来说十分有用,在这里,我们希望通过一个简单的函数传递一个值。

在编写 lambda 函数时,像上面例子中那样,我们首先编写 lambda 和我们会传递到函数中的代表参数的变量。在这个例子中,变量是 r。在变量名称之后,我们编写一个冒号(:)。这和我们用 def 定义函数,并且用冒号终结一行是相同的。

在冒号之后给 Python 我们想要 lambda 函数计算的逻辑,这个函数会返回一个值。在这个例子中,返回一个布尔值,告诉我们每行中 Female 这个值是否非 None。不一定要返回布尔值,lambda 可以返回任何类型的值(整型、字符串、列表,等等)。

同样可以在 lambda 函数中使用 if else 语句,基于简单的逻辑返回一个值。在 Python 解释器中尝试下面的代码:

  1. (lambda x: 'Positive' if x >= 1 else 'Zero or Negative')(0)
  2. (lambda x: 'Positive' if x >= 1 else 'Zero or Negative')(4)

❶ 将 lambda 函数放在第一对括号中,将要使用的变量作为 x 放在第二对括号中。这个 lambda 函数检查参数是否等于或大于 1。如果是,返回 Positive,否则返回 Zero or Negative

lambda 函数极为有用,但是也会使代码更加难以阅读。要确保遵守好的编程规则,并且只在清晰明确的情形下使用它们。

在检视数据的时候,我们发现很多国家既拥有高童工雇用率又拥有高女童雇用率。我们看过一些过滤和排序的方法,再来看一些 agate 库内置的统计学函数。假如我们想要找到城市童工雇用率的平均百分比。为此,我们来计算 Place of residence (%) Urban 这列数据的均值:

  1. table.aggregate(agate.Mean('Place of residence (%) Urban'))

这段代码调用了表的 aggregate 方法,使用 agate.Mean() 统计学方法和列名称来返回列的数学均值。你可以通过 agate 文档(http://agate.readthedocs.io/en/latest/cookbook/statistics.html#statistics)来查看其他可以在列上使用的聚合函数。

当运行这段代码时,会收到 NullComputationWarning 异常。从这个异常的名字和过往经验中,你可能猜到了这个异常的意义,这个异常意味着 Place of residence (%) Urban 列中可能有一些空数据。我们可以再次使用 where 方法来聚焦于城市平均值:

  1. has_por = table.where(lambda r: r['Place of residence (%) Urban'] is not None)
  2. has_por.aggregate(agate.Mean('Place of residence (%) Urban'))

你会发现得到了相同的值,这是因为 agate 在背后做了相同的事情(去除空列,计算剩下数据的平均值)。让我们来看看对于居住信息表还可以做什么数学计算。可以看一下 place of residence 列的最小值(Min)、最大值(Max)和均值(Mean)。

假如我们想要找到每行数据中农村童工雇用率大于 50% 的数据。agate 库有一个 find 方法,使用条件语句来找到第一个匹配的数据。让我们尝试用代码解决问题:

  1. first_match = has_por.find(lambda x: x['Rural'] > 50)
  2. first_match['Countries and areas']

返回的那行数据就是第一个匹配到的数据,就像在普通字典中一样,我们可以看到数据的名称。在 agate 库的第一次探索之旅中,最后一步,我们将会使用 compute 方法和 agate.Rand() 统计学方法(http://agate.readthedocs.io/en/latest/cookbook/rank.html),基于另一列创建一个新的排序的列。

第 9 章 数据探索和分析 - 图10 当比较数据集的时候,基于一列数据对整体数据进行排序是一个很好的彻查方式。

为了查看童工雇用率最高国家的排名,我们可以使用 Total (%) 列数据进行排序。在将这个数据集和其他数据集合并之前,我们想要一个清晰可见的排序后列数据,来比较合并后的数据。因为我们想要雇用率更高的国家出现在列表前面,所以需要使用参数 reverse=True 逆序排序(http://agate.readthedocs.io/en/latest/cookbook/rank.html#rank-descending)。

  1. ranked = table.compute([('Total Child Labor Rank',
  2. agate.Rank('Total (%)', reverse=True)), ])
  3. for row in ranked.order_by('Total (%)', reverse=True).limit(20).rows:
  4. print row['Total (%)'], row['Total Child Labor Rank']

如果想用另一种方式来计算排名,可以用逆百分比创建一列数据。相对于使用每个国家雇用童工的百分比数据,我们可以使用普通儿童的占比来进行计算。这会让我们在使用 agate.Rank() 方法时,不需要 reverse 参数:

  1. def reverse_percent(row):
  2. return 100 - row['Total (%)']
  3. ranked = table.compute([('Children not working (%)',
  4. agate.Formula(number_type, reverse_percent)),
  5. ])
  6. ranked = ranked.compute([('Total Child Labor Rank',
  7. agate.Rank('Children not working (%)')),
  8. ])
  9. for row in ranked.order_by('Total (%)', reverse=True).limit(20).rows:
  10. print row['Total (%)'], row['Total Child Labor Rank']

❶ 创建一个新的函数来计算并返回给定数据的逆百分比。

❷ 使用 agate 库的 compute 方法,传递一个列表作为参数,并返回新的数据列。列表中的每一个元素必须是元组对象,而元组的第一个元素包含列名称,第二个元素用来计算新的列。在这里,我们使用 Formula 类,其同样需要一个 agate 类型,同函数一起,创建一个列表值。

❸ 用 Children not working (%) 列的数据来创建有适当排序的 Total Child Labor Rank 列。

可以看到,compute 是一个非常好用的工具,它基于一个数据列(或多个数据列)来计算一个新的数据列。现在我们有了排名,让我们看看是否能够合并一些新数据集到童工数据集里。

9.1.3 联结多个数据集

在研究可以和童工数据联结的数据集时,我们碰到了很多无果而终的情况。我们尝试使用世界银行数据(http://data.worldbank.org/)来比较农业和服务经济数据,但是并没有找到任何好的联系。我们进行了更多的阅读,发现有些人把童工和 HIV 感染率联系了起来。我们观察了这些数据,但是并没有找到明显的总体趋势。顺着这个思路,我们想知道犯罪比率对童工雇用率是否有影响——但是这一次,我们依然没有发现任何关联。1

1查看本书的仓库,可以看到其中的一些探索。

经历这么多失败之后,在仔细考察数据和阅读一些文章时,一个特别的想法突然出现。政府腐败(或政府被认为有可能存在腐败)会不会影响童工雇用率?当我们阅读关于童工雇用的资料时,经常发现与反政府武装、学校和工业相关。如果大众不相信政府,并且必须创建一些未经政府批准的组织,这些都可能是招募这些希望工作和帮忙的人(甚至是儿童)的原因之一。

我们锁定了国际公开腐败感指数(Transparency International's Corruption Perceptions Index)数据集,并决定与 UNICEF 童工数据做比对。首先,我们需要把数据导入到 Python 中。下面代码交代了如何导入数据:

  1. cpi_workbook = xlrd.open_workbook('corruption_perception_index.xls')
  2. cpi_sheet = cpi_workbook.sheets()[0]
  3. for r in range(cpi_sheet.nrows):
  4. print r, cpi_sheet.row_values(r)
  5. cpi_title_rows = zip(cpi_sheet.row_values(1), cpi_sheet.row_values(2))
  6. cpi_titles = [t[0] + ' ' + t[1] for t in cpi_title_rows]
  7. cpi_titles = [t.strip() for t in cpi_titles]
  8. cpi_rows = [cpi_sheet.row_values(r) for r in range(3, cpi_sheet.nrows)]
  9. cpi_types = get_types(cpi_sheet.row(3))

我们再一次使用 xlrd 来导入 Excel 数据,并且复用在之前编写的解析标题和为 agate 库准备数据的代码。但是在你运行最后一行代码(这里调用了一个新的函数,get_types)之前,我们需要编写一些代码来帮助我们定义类型和创建表:

  1. def get_types(example_row):
  2. types = []
  3. for v in example_row:
  4. value_type = ctype_text[v.ctype]
  5. if value_type == 'text':
  6. types.append(text_type)
  7. elif value_type == 'number':
  8. types.append(number_type)
  9. elif value_type == 'xldate':
  10. types.append(date_type)
  11. else:
  12. types.append(text_type)
  13. return types
  14. def get_table(new_arr, types, titles):
  15. try:
  16. table = agate.Table(new_arr, titles, types)
  17. return table
  18. except Exception as e:
  19. print e

我们使用之前编写的相同代码来创建函数 get_types,它接受一行数据,为 agate 库输出一个类型列表。我们同样编写了 get_table 函数,函数中使用了 Python 内置的异常处理。

异常处理

整本书中,我们碰到了很多错误,并在它们发生时进行了处理。现在我们有了更多经验,可以开始预见潜在的错误,并做出适当的决定来处理它们。

代码要明确(特别是异常),这样就能够在代码中说明你预期的错误。这还会确保没有预见到的错误会抛出异常,进入错误处理的逻辑,输出错误日志,停止程序的执行。

当使用 tryexcept 时,我们告诉 Python:“请尝试执行这段程序。如果你碰到了错误,请停止执行前一节代码,执行 except 代码块中的代码。”下面是一个例子:

  1. try:
  2. 1 / 0
  3. except Exception:
  4. print 'oops!'

这个例子使用的是通用的异常类型。通常我们希望捕获特定类型的异常,即认为程序会抛出的异常。例如,如果代码会将字符串转化为数字,我们就知道可能会出现一个 ValueError 异常。可以像下面这段代码一样处理这个情况:

  1. def str_to_int(x):
  2. try:
  3. return int(x)
  4. except ValueError:
  5. print 'Could not convert: %s' % x
  6. return x

❶ 开始 try 代码块,它定义了代码可能抛出异常。try 关键字后面永远跟着一个冒号,并且单独占据一行空间。下面一行或者几行的代码,是一个 Python 的 try 代码块,使用 4 个空格缩进。

❷ 返回传入参数的整数形式。当参数是类似于 1 或者 4.5 的值时,这不会有问题。如果参数的值是 '-' 或者 'foo',这会抛出一个 ValueError 异常。

❸ 开始 except 代码块,定义需要捕获的异常类型。这一行同样使用一个冒号作为结束,指定我们想要捕获一个 ValueError 异常(这样 except 代码块会只捕获 ValueError 异常)。这个代码块和下面的代码只在 try 语句抛出了这行代码中指定的异常时才会执行。

❹ 打印一行信息,告诉关于异常的信息。如果需要更新或改进代码,我们可以使用这段信息。

一般来说,我们想构建简明且明确的 tryexcept 代码块。这会让代码变得易读、可预测又明确。

你可能会问,为什么在之后编写的 get_table 函数中使用了 except Exception ?这是个好问题!我们总是希望明确代码;然而,当你第一次用一个库或数据集进行实验的时候,可能不清楚该预防哪些错误。

为了编写捕获特定异常的代码,你需要预测代码可能会抛出什么类型的异常。有 Python 内置的异常类型,但是也有你不熟悉的特殊库异常。如果你正在使用一个 API 库,作者可能会编写一个 RateExceededException 异常,来告诉你发送了太多的请求。当我们面对一个全新的库时,使用 except Exception 代码块,打印或记录日志会帮助我们更多地了解这些错误。

第 9 章 数据探索和分析 - 图11 当编写 except 代码块时,可以通过在异常后面添加代码 as e(在冒号前)存储异常到一个变量 e。因为打印了包含异常信息的变量 e,所以可以了解到更多关于触发异常的信息。最终我们会用更精确的异常,或者一系列的异常代码块,重新编写 except Exception 块,这样代码会运行得更顺利和可预测。

现在我们有了一个 get_table 函数来跟踪 agate 库的异常,并可考虑如何改进代码。我们可以使用新函数将腐败指数数据导入到 Python 中。尝试运行下面的代码:

  1. cpi_types = get_types(cpi_sheet.row(3))
  2. cpi_table = get_table(cpi_rows, cpi_types, cpi_titles)

大功告成!当你运行这段代码,我们的新函数 get_table 会让你看到抛出的错误,而不是函数完全中断。重复的标题可能意味着 = 标题列表中有一些坏标题。通过运行下面的代码来查看这个问题:

  1. print pci_titles

可以看到有两个 Country Rank 列。通过在电子表格中查看 Excel 数据,我们看见的确有重复的列。为了方便起见,我们不会考虑去除重复数据,但是的确需要处理重复的列名称。我们需要拼接 Duplicate 到其中的一个列名称。下面的代码展示了如何处理重复列名称:

  1. cpi_titles[0] = cpi_titles[0] + ' Duplicate'
  2. cpi_table = get_table(cpi_rows, cpi_types, cpi_titles)

我们将第一个标题替换为 Country Rank Duplicate,并且再次尝试创建新的 pci_table:

  1. cpi_rows = get_new_array(cpi_rows, float_to_str)
  2. cpi_table = get_table(cpi_rows, cpi_types, cpi_titles)

现在我们有了没有任何差错的 cpi_table。我们可以着手将其与童工数据联结起来,查看它们之间有什么样的联系。在 agate 库中,有一个很好用的联结表的方法:joinhttp://agate.readthedocs.io/en/latest/api/table.html#agate.table.Table.join)。join 方法模仿 SQL 中的语义,将两张表通过一个共享键联结到一起。表 9-1 总结了不同的联结方式和对应的功能。

表9-1:表联结

联结方式 功能
左外联结 保留左侧表(或 join 语句中的第一张表)中的所有行数据,使用共享键来绑定右侧表(或 join 中的第二张表)的值。如果右侧表中没有匹配的值,使用空值来填充
右外联结 使用右侧表中的值作为初始的匹配键。如果在第一张表(左侧表)中没有对应的值,这些行会使用空值填充
内联结 只返回两张表中都能够通过共享键匹配到值的数据行
全外联结 保留两张表中的所有数据,当共享键相匹配时,组合两张表中的数据到一行中

如果数据并不是完全匹配,或者不具备一对一关系,同时你在使用外联结,就会有为空值的数据行。当表不匹配时,外联结保持表中的数据存在,并用空值替代缺失的数据。这在因为报告必需而想要保留不匹配的数据时非常有用。

如果想要联结 table_atable_b,同时确保不会丢失任何 table_a 里的数据,那可以编写下面的代码:

  1. joined_table = table_a.join(
  2. table_b, 'table_a_column_name', 'table_b_column_name')

在结果 joined_table 中,我们会得到基于传递的列名称,和 table_a 匹配的所有 table_b 值。如果 table_a 中有值不匹配 table_b,我们会保留这些行,但是它们会在 table_b 列上成为空值。如果 table_b 中有值没有在 table_a 中匹配,它们会被排除在新表之外。选择哪一张表在前面和使用哪一种方式的联结是非常重要的。

我们绝不想要空值存在。我们的问题围绕数据是怎样关联的,为了达到这个目的,我们想要使用内联结。agate 库的 join 方法允许传递 inner=True 参数,这会使函数仅作内联结,只保留匹配的行,不会在联结后有空值行。

我们尝试联结童工数据和新规整后的 cpi_table。当我们查看这两个表时,可以将它们通过国家 / 领土的名称匹配在一起。在 cpi_table 中,我们有 Country/Territory 列,同时,在童工数据中,我们有 Counties and areas 列。为了联结这两张表,运行下面的代码 :

  1. cpi_and_cl = cpi_table.join(ranked, 'Country / Territory',
  2. 'Countries and areas', inner=True)

将匹配行放到新表 cpi_and_cl 中。我们可以通过打印几个值来查看这张表,同时研究新的联结后的列,像下面代码这样:

  1. cpi_and_cl.column_names
  2. for r in cpi_and_cl.order_by('CPI 2013 Score').limit(10).rows:
  3. print '{}: {} - {}%'.format(r['Country / Territory'],
  4. r['CPI 2013 Score'], r['Total (%)'])

当查看列名称时,可以看到现在有了两张表里面的所有的列。对数据进行简单计数返回 93 行。我们不需要所有的数据点(pci_table 有 177 行,ranked 有 108 行),我们想要看到的就是关联在一起的数据。当使用 CPI 得分排序,打印出新的联结的表时,是否注意到一些其他的东西?我们只选择了最高的 10 行数据,但是一些有意思的信息变得清晰:

  1. Afghanistan: 8.0 - 10.3%
  2. Somalia: 8.0 - 49.0%
  3. Iraq: 16.0 - 4.7%
  4. Yemen: 18.0 - 22.7%
  5. Chad: 19.0 - 26.1%
  6. Equatorial Guinea: 19.0 - 27.8%
  7. Guinea-Bissau: 19.0 - 38.0%
  8. Haiti: 19.0 - 24.4%
  9. Cambodia: 20.0 - 18.3%
  10. Burundi: 21.0 - 26.3%

除了伊拉克(Iraq)和阿富汗(Afghanistan)两个国家,当国家有非常低的 CPI 得分(即政府腐败高概率)时,同时有非常高的童工雇用率。使用 agate 库的一些内置方法,我们可以研究数据集中存在的类似关系。

9.1.4 识别相关性

agate 库有一些很好的工具,供你在数据集上做简单的数据分析。这是一个很好的初始工具集——可以从 agate 库的工具开始,之后转向更高级的数据分析库,包括 pandasnumpyscipy,按需选择。

我们想要确定政府腐败和童工雇用率之间是否有关联。我们将使用的第一个工具是简单的皮尔森相关系数(http://onlinestatbook.com/2/describing_bivariate_data/pearson.html)。agate 库基于这个算法开发了 agatestat 库(https://github.com/onyxfish/agatestats)。在这之前,你可以使用 numpy 进行相关性分析。相关系数(例如皮尔森相关系数)告诉我们数据是否关联,以及一个因子是否会影响另一个因子。

如果你还没有安装 numpy,可以通过运行命令 pip install numpy 安装它。之后,使用下面几行代码计算童工雇用率和政府腐败指数之间的相关性:

  1. import numpy
  2. numpy.corrcoef(cpi_and_cl.columns['Total (%)'].values(),
  3. cpi_and_cl.columns['CPI 2013 Score'].values())[0, 1]

我们首先得到了类似于之前曾见到的 CastError 异常的错误。因为 numpy 需要浮点型数据,而不是小数型,所以我们需要将数字转换回浮点数。我们可以使用列表生成式做这个转换:

  1. numpy.corrcoef(
  2. [float(t) for t in cpi_and_cl.columns['Total (%)'].values()],
  3. [float(s) for s in cpi_and_cl.columns['CPI 2013 Score'].values()])[0, 1]

我们的输出显示出一些轻微的负相关:

  1. -0.36024907120356736

第 9 章 数据探索和分析 - 图12 负相关意味着,一个变量增长,另一个变量会减小。正相关意味着两个变量会同时增长或减小。皮尔森相关系数在 -1 到 1 之间波动,0 意味着无相关性,-1 和 1 意味着相关性很强。

我们的结果是 -0.36,意味着弱相关,但是的确有关联。我们可以使用这个结果更加深入地研究这一数据集,搞清楚其中的含义。

9.1.5 找出离群值

随着数据分析的进行,你会想要使用一些其他的统计学方法来解释你的数据。一个入手点就是找出离群值。

第 9 章 数据探索和分析 - 图13 离群值出现在个别的数据明显有别于数据集其他部分的时候。离群值会告诉我们数据的一部分情况。有时候,去掉它们会展现出一个明显的趋势;有些时候,离群值本身会透露出很多信息。

有了 agate 库,找到离群值是很容易的。有两种方法可以做到这一点:一是使用标准差,第二个是使用绝对中位差。如果你有一些统计学知识,并且想要使用其中的一个,尽情去尝试!如果没有相关的统计学基础,在你的数据集上同时使用两种方式来分析偏差,可能会揭露出不同的结果。2

2更多关于绝对中位差和标准差的信息,查看 Matthew Martin 关于为什么我们仍在使用标准差的文章(http://www.separatinghyperplanes.com/2014/04/why-do-statisticians-use-standard.html),以及 Stephen Gorad 关于为什么及何时使用均差的学术文章(http://www.leeds.ac.uk/educol/documents/00003759.htm)。

第 9 章 数据探索和分析 - 图14 如果你已经知道数据的分布,可以采用适当的方式来确定变化值;但是在你第一次探索数据时,尝试使用多种不同的方法来确定数据的分布,并了解数据的组成。

我们将会使用 agate 表的标准差离群值(http://agate.readthedocs.io/en/latest/cookbook/statistics.html#identify-outliers)方法。这个方法返回包含至少 3 个高于或低于平均值的偏差值表。下面是使用 agate 表查看标准差离群值的方法。

第 9 章 数据探索和分析 - 图15 如果你在使用 IPython 处理数据,并且需要安装新的库,可以使用 IPython 的魔法命令 %autoreload,在另外一个终端安装新的库后重新加载你的 Python 环境。尝试执行 %load_ext autoreload,然后执行 %autoreload。没错!你拥有了新的库,却没有丢失任何的进程信息。

首先,需要通过运行命令 pip install agatestat 安装 agatestat 库。然后运行下面的代码:

  1. import agatestats
  2. agatestats.patch()
  3. std_dev_outliers = cpi_and_cl.stdev_outliers(
  4. 'Total (%)', deviations=3, reject=False)
  5. len(std_dev_outliers.rows)
  6. std_dev_outliers = cpi_and_cl.stdev_outliers(
  7. 'Total (%)', deviations=5, reject=False)
  8. len(std_dev_outliers.rows)

❶ 使用童工雇用数据的 Total (%) 列和 agatestats stdev_outliers 方法来查看我们的数据中是否含有容易找到的标准差离群值。我们将这个方法的输出赋值给一个新的表 std_dev_outliers。我们使用参数 reject=false 来告诉函数我们希望看到离群值。如果我们设置 reject 等于 True,将会得到没有离群值的数据。

❷ 查看我们发现了多少行离群值(这张表共有 94 行数据)。

❸ 提高偏差的大小,减少离群值的数量。(deviations=5。)

从输出中看出,我们对数据分布并没有很好的了解。当我们使用 Total (%) 列,尝试使用 3 作为标准差识别离群值时,得到了和当前表完全匹配的一张表。这不是我们想要的结果。当使用 5 作为差值界限时,我们并没有看到数据的变化。这告诉我们数据并不是常规的分布。为了找到数据中真正的偏差,我们需要更深入地探索来确定是否需要重新划分数据,使其变为我们研究的国家的子集。

可以使用平均绝对偏差检查 Total (%) 列数据的偏差:

  1. mad = cpi_and_cl.mad_outliers('Total (%)')
  2. for r in mad.rows:
  3. print r['Country / Territory'], r['Total (%)']

有趣!我们的确找到了一个更小的离群值的子集,但是得到了一个奇怪的结果列表:

  1. Mongolia 10.4
  2. India 11.8
  3. Philippines 11.1

查看这个列表时,我们并没有看到数据中的任何最高值或最低值。这意味着,对于识别离群值来说,数据集并没有遵从正常的统计学规则。

第 9 章 数据探索和分析 - 图16 取决于数据集和数据的分布,这两个方法经常会有效地展示出数据的信息。如果没有的话,就像我们这个数据集,继续搞清楚数据能够告诉我们什么联系和趋势。

在探索了数据的分布和数据分布所展现的趋势后,你会想要探索数据中的分组关系。下面这一节解释了怎样对数据分组。

9.1.6 创建分组

为了进一步研究数据,我们将要创建分组,研究分组之间的关系。agate 库提供了很多不同的工具来创建分组,还有其他一些方法来聚合这些分组,确定分组之间的联系。早些时候,我们的童工数据集中有完好的各大洲数据。让我们尝试从地理角度,按照大洲分组数据,看一下这样是否会揭露一些与政府腐败数据之间的关系或总结出其他结论。

首先,我们要解决怎样拿到大洲数据的问题。在本书的 git 仓库中(https://github.com/jackiekazil/datawrangling),我们提供了一个 .json 文件,其中列举了不同大洲包含的国家。使用这个数据,我们可以添加一列,展示每个国家所属的大洲,以便通过大洲分组。下面是这一过程的代码:

  1. import json
  2. country_json = json.loads(open('earth.json', 'rb').read())
  3. country_dict = {}
  4. for dct in country_json:
  5. country_dict[dct['name']] = dct['parent']
  6. def get_country(country_row):
  7. return country_dict.get(country_row['Country / Territory'].lower())
  8. cpi_and_cl = cpi_and_cl.compute([('continent',
  9. agate.Formula(text_type, get_country)),
  10. ])

❶ 使用 json 库来加载 .json 文件。如果你观察这个文件的话,会看到文件中保存了类型为字典的列表。

❷ 遍历 country_dict,将 country 作为键、continent 作为值填充到字典中。

❸ 创建函数:接受国家作为参数,返回它归属的大洲。这个函数使用了 Python 的字符串方法 lower,将大写字母替换为小写形式。.json 文件包含的都是小写的国家名称。

❹ 使用 get_country 函数创建一个新的列,continent。沿用相同的表名称。

现在我们有了大洲和国家数据。我们需要做一个快速的检查来确保没有遗漏任何东西。为了检查,运行下面的代码:

  1. for r in cpi_and_cl.rows:
  2. print r['Country / Territory'], r['continent']

呃,看起来我们落下了一些数据,因为我们可以在一些国家中看到 None 类型的数据:

  1. Democratic Republic of the Congo None
  2. ...
  3. Equatorial Guinea None
  4. Guinea-Bissau None

最好不要丢失这些数据,让我们看一下为什么这些行数据没有匹配上。我们只想要打印出没有匹配上的行。可以使用 agate 来帮忙找到这些行,运行下面的代码:

  1. no_continent = cpi_and_cl.where(lambda x: x['continent'] is None)
  2. for r in no_continent.rows:
  3. print r['Country / Territory']

你的输出应该类似于下面这样:

  1. Saint Lucia
  2. Bosnia and Herzegovina
  3. Sao Tome and Principe
  4. Trinidad and Tobago
  5. Philippines
  6. Timor-Leste
  7. Democratic Republic of the Congo
  8. Equatorial Guinea
  9. Guinea-Bissau

可以看到,没有大洲数据的国家列表很短。我们建议只修改 earth.json 数据文件,因为这会使在未来使用相同的数据文件关联相同的数据更简单。如果你使用代码来找到异常值并匹配它们,当用新数据重做时会变得复杂,每一次更新数据都需要修改代码。

为了修复 .json 文件中的匹配问题,我们需要搞清楚为什么国家没有被找到。打开 earth.json 文件,找到 no_continent 表中的几个国家。例如:

  1. {
  2. "name": "equatorial Guinea",
  3. "parent": "africa"
  4. },
  5. ....
  6. {
  7. "name": "trinidad & tobago",
  8. "parent": "north america"
  9. },
  10. ...
  11. {
  12. "name": "democratic republic of congo",
  13. "parent": "africa"
  14. },

正如我们在 .json 文件中看到的那样,有一些细微的差别,影响了我们顺利地找到国家归属的大洲。本书的 git 仓库中同样包含了一个名叫 earth-cleaned.json 的文件,这个文件在 earth.json 文件上做出了一些必要的修改,比如添加 the 到刚果(Democratic Republic of the Congo,DRC)条目中,将若干条目中的 & 改为“and”。我们现在可以重新运行本节最初的代码,并且使用新的文件作为 country_json 数据。你需要重新联结表,避免重复的列(使用之前用过的联结两张表的代码)。在重新运行这两部分的代码后,你应该不会得到不匹配的国家了。

让我们尝试分组完整的大洲数据,看一下会发现什么。下面的代码做了这件事:

  1. grp_by_cont = cpi_and_cl.group_by('continent')
  2. print grp_by_cont
  3. for cont, table in grp_by_cont.items():
  4. print cont, len(table.rows)

❶ 使用 agate 库的 group_by 方法,这会返回一个字典,其中键是大洲名称,值是一个新表,包含这个大洲的值。

❷ 遍历返回的字典,看一下每张表有多少行。我们将 items 中的键 / 值对分别赋值给变量 conttable,这样 cont 代表键或者大洲名称,table 代表对应表的值。

❸ 打印我们的数据,来检查分组。我们使用 Python 的 len 函数来计算每一张表中的行数。

运行这段代码,我们得到了下面的输出(注意,你的顺序可能不同):

  1. north america 12
  2. europe 12
  3. south america 10
  4. africa 41
  5. asia 19

我们可以看到,非洲和亚洲的数据要大于其他各大洲。这让我们很感兴趣,但是 group_by 方法对于聚合数据来说并不十分容易。如果想要聚合数据,创建一个汇总列的话,我们需要看一下 agate 库中的聚合方法。

我们注意到 agate 表的 aggregate 方法(http://agate.readthedocs.io/en/latest/cookbook/tatistics.html#aggregate-statistics),这个方法接受一个分好组的表和一系列的聚合操作(例如求和)来基于分组计算一个新的列。

在查看了 aggregate 方法的文档后,我们最感兴趣的是大洲童工数据和腐败感指数相对比的结果。我们想要使用一些统计学方法来把每个分组当作一个整体来看待(使用中位数和平均数),但是同样需要识别出最极端的数据(CPI 得分的最小值,童工雇用率的最大值)。这会给我们一些良好的比较结果:

  1. agg = grp_by_cont.aggregate([('cl_mean', agate.Mean('Total (%)')),
  2. ('cl_max', agate.Max('Total (%)')),
  3. ('cpi_median', agate.Median('CPI 2013 Score')),
  4. ('cpi_min', agate.Min('CPI 2013 Score'))])
  5. agg.print_table()

❶ 在我们分好组的表上调用 aggregate 方法,传递一个元组列表作为参数,其中包括新的列名称和 agate 的聚合方法(利用新的列命名计算新列的值)。我们想要计算童工雇用百分比的平均值和最大值,还有腐败感指数(CPI)得分的中位数和最小值。根据你的问题和数据的不同,你可以使用不同的 agate 方法。

❷ 打印新表,这样我们可以从视觉上直观比较数据。

当运行这段代码后,你会看到下面的结果:

  1. |----------------+-----------------------------+--------+------------+----------|
  2. | continent | cl_mean | cl_max | cpi_median | cpi_min |
  3. |----------------+-----------------------------+--------+------------+----------|
  4. | south america | 12,710000000000000000000000 | 33,5 | 36,0 | 24 |
  5. | north america | 10,333333333333333333333333 | 25,8 | 34,5 | 19 |
  6. | africa | 22,348780487804878048780487 | 49,0 | 30,0 | 8 |
  7. | asia | 9,589473684210526315789473 | 33,9 | 30,0 | 8 |
  8. | europe | 5,625000000000000000000000 | 18,4 | 42,0 | 25 |
  9. |----------------+-----------------------------+--------+------------+----------|

如果想更仔细地查看数据相关的图表,可以使用 agate 表的 print_bars 方法,该方法有一个标签列(这里是 continent)和一个数据列(这里是 cl_max),在 IPython 会话中打印童工雇用数据最大值的图表。输出如下:

  1. In [23]: agg.print_bars('continent', 'cl_max')
  2. continent cl_max
  3. south america 33,5 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  4. north america 25,8 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  5. africa 49,0 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  6. asia 33,9 ▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
  7. europe 18,4 ▓░░░░░░░░░░░░░░░░░░░░
  8. +----------+-----------------+--------------+-------------+
  9. 0,0 12,5 25,0 37,5 50,0

现在我们的大洲数据有了几种易于比较的输出,同时这张图片展示了一些趋势。我们注意到非洲的童工雇用率均值最高,最大值也最高,亚洲和南美洲紧随其后。亚洲和南美洲相对较低的均值意味着这些区域可能存在一个或多个离群值。

我们看到腐败感指数的中位值相对差别不大,欧洲最高(政府腐败感最低)。然而,当我们看最小值(最糟糕的政府腐败感得分)时,可以看到非洲和亚洲再一次获得了最糟糕的得分。

这说明该数据集中有几个故事供我们深入探索。我们能够看到政府腐败感和童工雇用之间有联系(尽管联系很弱)。我们同样可以研究哪些国家和大洲是最糟糕的童工雇用国家和最腐败的政府。我们可以看到非洲有着非常高的童工雇用率和相对更高的政府腐败感。我们知道亚洲和南美洲中存在一两个国家,相对于他们的邻居,在童工雇用率上面引人注目。

我们的聚合探索就到这里了。我们可以继续使用已创建的表来寻找更多信息,进行更深入的探索。

9.1.7 深入探索

agate 库还有其他一些强有力的特性,另外还有一些有趣的数据分析库可以用来在你的数据上进行实验。

第 9 章 数据探索和分析 - 图17 根据你的数据和问题的不同,你可能会发现一些特性和库比其他特性和库更有用,但是我们强烈建议你找到使用不同工具来实验的方式。同数据分析本身一样,这会加深你对 Python 和数据分析库的理解。

agatestat 库有一些有趣的统计方法我们还没有探索过。你可以通过 GitHub 跟踪最新的发布和功能(https://github.com/onyxfish/agatestats)。

除此之外,我们建议你继续探索 numpy。你可以使用 numpy 来计算百分位数值(https://docs.scipy.org/doc/numpy-dev/reference/generated/numpy.percentile.html)。你同样可以更深入地使用 scipy,使用 z 得分统计方法来确定离群值(https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.mstats.zscore.html)。

如果你有时间敏感的数据,numpy 能够计算数据之间列与列的变化,从而探索随时间推移数据的变化(https://docs.scipy.org/doc/numpy/reference/generated/numpy.diff.html)。agate 同样可以计算时间相关数据中列的变化(http://agate.readthedocs.io/en/1.0.0/tutorial.html#computing-new-columns)。不要忘记在组成时间列的时候使用时间类型,因为这样做你能够在时间上做一些有趣的分析(例如随时间变化的百分比变化或一系列时间变化的映射)。

如果你想要通过更多的统计方法来探索数据,可安装 latimes-calculate 库(http://latimes-calculate.readthedocs.io/en/latest/index.html)。这个库有很多计算方法,同样还有一些有趣的地理数据分析工具。如果你获取了一些地理数据,这个库可以提供一些有价值的工具来帮你更好地理解、映射和分析数据。

如果你想要更深入进行数理计算和分析,我们强烈推荐 Wes McKinney 的《利用 Python 进行数据分析》一书。这本书介绍了一些更健壮的数据分析库,包括 pandasnumpyscipy 系列的库等。

花时间用一些方法利用我们之前学到的课程来探索你的数据。现在我们会进一步分析数据,确定一些可以得出结论和分享知识的方式。

9.2 分析数据

如果你已经尝试了 agate 库手册(http://agate.readthedocs.org/en/latest/cookbook.html,用于研究的各种不同的方法和工具的汇总)中的更多示例,就会足够熟悉数据并能开始你的分析。

第 9 章 数据探索和分析 - 图18 数据探索和数据分析之间有哪些不同?当分析数据时,我们提出问题并且尝试使用已有的数据回答这些问题。我们可能会对数据集进行组合和分组,以构建一个统计可用的样本。而在数据探索中,我们只是想要研究数据集的一些趋势和属性,不尝试去回答特定的问题或得出确定的结论。

在一些基本的分析后,我们可以尝试回答在数据探索中发现的一些问题:

  • 为什么在非洲童工雇用的概率更高?

  • 在亚洲和南美洲存在什么样的童工雇用离群值?

  • 腐败感和童工雇用率有什么关系?

对于你的数据集,你会有不同的问题,但是尝试跟随我们的实例,并且找到你想要探索的趋势。任何统计学上的离群值或者聚合趋势都可以将你引向有趣的问题去研究。

对我们的数据来说,最有趣的问题是,在非洲政府腐败感和童工雇用的关系。政府腐败,或者政府腐败感,是否会影响社区保护童工不被雇用的能力?

第 9 章 数据探索和分析 - 图19 根据所使用的数据集和数据探索结果,你可能会有很多感兴趣、想要探索的问题。尝试聚焦于一个具体的问题,并用你的分析来回答它。针对多个具体问题重复这一过程。专注会帮助你找到好的答案,保持你的分析明确清晰。

回答这个问题需要更多的探索和更多的数据集。我们可能希望阅读更多的文章,看一下在这个主题上有哪些研究结果。我们可能还希望访问这一领域的专家。最终,我们可能希望选择非洲的一个特定地区或一系列国家,来更好地评估童工雇用情况。下面这一小节展示了怎么做这件事。

9.2.1 分离和聚焦数据

为了之后的分析,我们首先需要分离出非洲国家的数据,更加充分地探索这一子集的数据。我们已经知道了很多使用 agate 库来过滤数据的方式,所以让我们从这里开始。下面的代码展示了怎样把非洲的数据同其他数据分离开来:

  1. africa_cpi_cl = cpi_and_cl.where(lambda x: x['continent'] == 'africa')
  2. for r in africa_cpi_cl.order_by('Total (%)', reverse=True).rows:
  3. print "{}: {}% - {}".format(r['Country / Territory'], r['Total (%)'],
  4. r['CPI 2013 Score'])
  5. import numpy
  6. print numpy.corrcoef(
  7. [float(t) for t in africa_cpi_cl.columns['Total (%)'].values()],
  8. [float(c) for c in africa_cpi_cl.columns['CPI 2013 Score'].values()])[0, 1]
  9. africa_cpi_cl = africa_cpi_cl.compute([('Africa Child Labor Rank',
  10. agate.Rank('Total (%)', reverse=True)),
  11. ])
  12. africa_cpi_cl = africa_cpi_cl.compute([('Africa CPI Rank',
  13. agate.Rank('CPI 2013 Score')),
  14. ])

❶ 使用表方法 where 来过滤出所属大洲是 africa 的行。

❷ 使用某种格式化方法打印这些行,这样我们可以查看数据并进行彻底检查。我们想要确保只有非洲国家的数据,并且能够看到总童工雇用百分比和 CPI 得分。

❸ 在分离出最感兴趣的数据后,看一下皮尔森相关系数是否变化。

❹ 添加一个新的排序列,来展示我们子集中国家之间的相互排名。

在这个数据子集中,我们计算了新的皮尔森相关系数:

  1. -0.404145695171

我们的皮尔森相关系数下降了,表示相对于全球数据,非洲数据中童工雇用和腐败感之间有着更强的相关性。

现在让我们看一下,是否可以找出一个好故事,找到我们想要去研究的数据点。我们会计算腐败感和童工雇用率的平均值,输出有最高童工雇用率和最糟糕的腐败感指数的国家(即比均值表现更差的国家)。下面是相关的代码:

  1. cl_mean = africa_cpi_cl.aggregate(agate.Mean('Total (%)'))
  2. cpi_mean = africa_cpi_cl.aggregate(agate.Mean('CPI 2013 Score'))
  3. def highest_rates(row):
  4. if row['Total (%)'] > cl_mean and row['CPI 2013 Score'] < cpi_mean:
  5. return True
  6. return False
  7. highest_cpi_cl = africa_cpi_cl.where(lambda x: highest_rates(x))
  8. for r in highest_cpi_cl.rows:
  9. print "{}: {}% - {}".format(r['Country / Territory'], r['Total (%)'],
  10. r['CPI 2013 Score'])

❶ 计算我们最感兴趣的列均值:腐败得分和童工雇用率。

❷ 创建函数来识别有着高童工雇用率和低 CPI 得分(即高腐败)的国家。

❸ 函数 highest_rates 返回 TrueFalse,来选择一行数据。这个 lambda 函数判断国家的童工雇用率和腐败感是否高于均值。

当运行这段代码时,我们看到了一些有趣的输出。特别是下面这些行:

  1. Chad: 26.1% - 19.0
  2. Equatorial Guinea: 27.8% - 19.0
  3. Guinea-Bissau: 38.0% - 19.0
  4. Somalia: 49.0% - 8.0

我们输出了一些与均值离得不太远的位于“中部”的数据,随之是这些有着低 CPI 得分和高童工雇用率的数据。因为我们对为什么这里会有高童工雇用率,以及腐败是怎样影响童工雇用率这些问题感兴趣,所以这些数据对我们来说最为适合。

随着研究的继续,我们想要找出这些国家正在发生着什么。是否有和这些国家年轻人和童工相关的电影或文档?是否有关于这一主题的文章和书籍?是否有我们可以联系的专家或研究者?

当更加深入地观察这些国家,我们看到一些明显的事实:贩卖儿童、性虐待、非法的宗教团体、大量的小摊贩和劳动者需求。这些事实是否与选举权被剥削有关?与民众对政府不信任有关?我们是否可以追踪这些国家的民众和他们的邻里?我们是否能够找到组织或人来缓解这些问题?

随着时间的推移,观察政治和时代变化的影响是很有趣的事情。我们可以详细考察 UNICEF 的数据,或者聚焦于一个国家,利用 UNICEF 的多指标类集调查(https://www.unicef.org/statistics/index_24302.html)数据来理解过去几十年发生的变化。

对于你自己的数据集,你需要确定未来探索时有哪些可能性。你能找到更多的数据以供探索吗?你是否可以访问一些人,或者在一段较长的时间中找到一些趋势?是否有这一主题的书籍、电影或者文章能够给你更多启发?你的分析是未来研究的开始。

9.2.2 你的数据在讲什么

现在我们已经探索和分析了数据,可以开始搞清楚数据在告诉我们什么。正如我们在第一次研究童工数据集时经历的那样,有时数据并没有什么联系,没讲述任何故事,没有任何相关性。发现这一点也没关系!

第 9 章 数据探索和分析 - 图20 有时候,没找到相关性将促使我们继续研究,以找到真正存在的联系。有时候,没找到联系,这件事情本身就是一个发现。

在数据分析中,我们寻找趋势和模式。正如我们在童工雇用数据中看到的那样,大多数时候分析是更深入研究的开始。和数据讲故事一样,增加人们的声音或者其他的角度,都是找到联系和发现问题的绝佳方式。

如果你发现了一些联系,即使是很弱的联系,也可以更深入地挖掘。这些联系会通向更好的问题和更专注的研究。正如我们在童工数据中看到的那样,我们对研究越专注,就越容易看到联系。从宽泛的研究开始是很好的,但是用更加精确的视角来结束研究十分重要。

9.2.3 描述结论

当你已经分析了数据并且理解了数据中的联系,就可以开始确定你能得出什么结论。真正了解你的数据集和主题非常重要,这会对你的想法提供强有力的支持。随着你的数据分析、访谈、研究的完成,你的结论会逐渐形成,你只需要确定用什么方式来向全世界分享这些结论。

第 9 章 数据探索和分析 - 图21 如果你在寻找确切结论的过程中陷入困境,在你的发现中包含开放性的问题是完全可取的。一些大的故事就是从几个简单的问题开始的。

如果你能够阐明主题,指出为了得出一个更全面的结论,需要更多的文档、研究和行动的话,这本身就是一个很重要的信息。正如我们在研究中发现的那样,很难说政府腐败引发了高的童工雇用率,但是我们可以说,这之间有很弱的相关性,并且我们想要研究和分析它们之间关联的方式——特别是在某些非洲国家。

9.2.4 将结论写成文档

当你发现一些结论和更多想研究的问题之后,应该开始将你的工作成果写成文档。作为文档和最终展示成果的一部分,你需要对使用的数据源和分析的数据数量了如指掌。在我们的问题中,我们只研究了大约 90 个数据点,但它们代表了我们想要研究的部分。

你可能会发现你关注的数据集比预期的小。只要你清楚你使用的方法和使用小数据集的原因,就不会把听众和报告引入歧途。在下一章,我们会更深入地探究如何报告发现,将我们的想法和工作写成文档,把我们的想法和世界分享。

9.3 小结

在这一章,我们使用了一些新的 Python 库和技术,探索和分析了我们的数据集。你已经能够导入数据、联结数据、分组数据,并且基于发现创造新的数据集。

现在你可以使用统计学方法来找到离群值,衡量数据之间的相关性。你可以通过分离有趣的分组,并且深入数据探索之中,确定清晰的、可回答的问题来研究。如果你曾经使用过 IPython 和 %store 来保存变量,在下一章,我们会用这个命令做更多的交互。

现在你应该能够:

  • 使用 agate 库评估你的数据;

  • 确定哪些事,如果有的话,在数据中是至关重要的;

  • 找到数据中的入手点或一部分的数据来做深入研究,得到结论;

  • 通过分析和探索数据挑战你的假设。

本章中涉及的新概念和库总结在表 9-2 中。

表9-2:新的Python编程概念和库

概念/库 功能
agate 使数据分析变得简单,能够从 CSV 数据中读取数据,创建供分析的表,运行基本的数据分析函数,在数据集上应用过滤器,洞察数据
xlrd ctypectype_text 对象 当使用 xlrd 分析 Excel 数据时,让你能够轻松地看到数据的类型
isintance 函数 检验 Python 对象的类型。如何类型匹配,结果返回一个布尔值
lambda 函数 Python 中的单行函数,对数据集的简单过滤或解析非常有用。注意不要书写不易阅读和理解的 lambda 函数。如果函数很复杂,尝试用一个小函数来代替 lambda 函数
联结(内联结,外联结,左联结,右联结) 允许你通过一个或多个匹配的域联结两个不同的数据集。根据联结数据方式的不同(内 外和左 右),你会得到不同的数据集。花一些时间思考什么类型的联结更符合你的需求
异常处理 使你能够使用代码预见和处理 Python 异常。明确和清楚的异常捕获永远是更好的,这样你不会捕获过度泛化的异常而漏掉 bug
numpy coerrcoef 使用统计学方法,例如皮尔森相关系数,来确定数据集中的两部分是否有联系
agate mad_outliersstdev_outliers 使用统计学模型和工具,例如标准差或平均偏差,来确定数据集是否有特殊的离群值或不合适的值
agate group_byaggregate 根据特定的属性对数据集分组,通过运行聚合分析,查看在分组间是否有明显的不同之处(或相似之处)

在下一章中,你会学习如何使用可视化和讲故事的工具来在 Web 和其他媒介上分享结论。