网站搜索

使用Python进行数据分析


数据分析是一个广泛的术语,涵盖了多种技术,使您能够揭示原始数据中可能存在的任何见解和关系。正如您所料,Python 很适合数据分析。一旦 Python 分析了您的数据,您就可以使用您的发现来做出良好的业务决策、改进程序,甚至根据您的发现做出明智的预测。

在本教程中,您将:

  • 了解对健全的数据分析工作流程的需求
  • 了解数据分析工作流程的不同阶段
  • 了解如何使用 Python 进行数据分析

在开始之前,您应该熟悉 Jupyter Notebook,这是一种流行的数据分析工具。另外,JupyterLab 将为您提供增强的笔记本电脑体验。您可能还想了解 pandas DataFrame 如何存储其数据。了解 DataFrame 和 pandas Series 之间的区别也将很有用。

在本教程中,您将使用名为 james_bond_data.csv 的文件。这是免费詹姆斯邦德电影数据集的修改版本。 james_bond_data.csv 文件包含原始数据的子集,并更改了一些记录以使其适合本教程。您可以在可下载的材料中找到它。获得数据文件后,您就可以开始第一个数据分析任务了。

了解数据分析工作流程的需求

数据分析是一个非常流行的领域,可能涉及执行许多不同复杂性的不同任务。您执行哪些具体分析步骤取决于您正在分析哪个数据集以及您希望收集哪些信息。为了克服这些范围和复杂性问题,您需要在执行分析时采取战略方法。这就是数据分析工作流程可以为您提供帮助的地方。

数据分析工作流程是一个为您的分析团队在分析数据时提供一组步骤的过程。每个步骤的实施将根据分析的性质而有所不同,但遵循商定的工作流程可以让每个参与人员都知道需要发生什么并了解项目的进展情况。

使用工作流程还有助于确保您的分析方法面向未来。通过遵循定义的步骤集,您的努力就会变得系统化,从而最大限度地减少您犯错误或错过某些内容的可能性。此外,当您仔细记录您的工作时,您可以根据未来可用的数据重新应用您的程序。因此,数据分析工作流程还提供可重复性和可扩展性。

没有适合每种分析的单一数据工作流程,也没有针对其中使用的程序的通用术语。为了为本教程的其余部分提供结构,下图说明了大多数工作流程中常见的阶段:

实线箭头显示了标准数据分析工作流程,您将通过该工作流程了解每个阶段发生的情况。虚线箭头表示您可能需要多次执行某些单独步骤,具体取决于分析的成功与否。事实上,如果您的第一次分析揭示了一些需要进一步关注的有趣内容,您甚至可能必须重复整个过程。

现在您已经了解了数据分析工作流程的需求,您将完成其步骤并对电影数据进行分析。您将分析的电影都与英国特工邦德有关……詹姆斯·邦德。

设定你的目标

数据分析的第一个工作流程步骤是仔细但明确地定义您的目标。对于您和您的分析团队来说,明确你们到底想要实现什么目标至关重要。这一步不涉及任何编程,但同样重要,因为如果不了解您想去的地方,您就不可能到达那里。

数据分析的目标会根据您分析的内容而有所不同。您的团队领导可能想知道为什么新产品没有售出,或者您的政府可能想了解有关新药临床测试的信息。您甚至可能会被要求根据特定金融工具的过去结果提出投资建议。无论如何,您仍然必须明确自己的目标。这些定义了您的范围。

在本教程中,您将通过前面提到的詹姆斯·邦德电影数据集获得一些乐趣,从而获得数据分析的经验。你的目标是什么? 现在注意,007

  • 烂番茄的评分和 IMDb 的评分有什么关系吗?
  • 通过分析电影的长度是否可以得到一些见解?
  • 詹姆斯·邦德杀死的敌人数量和杀死他们的电影的用户评分之间有关系吗?

现在你已经了解了你的任务,是时候进入现场看看你能发现什么情报了。

获取您的数据

确定目标后,下一步就是考虑实现这些目标需要哪些数据。希望这些数据很容易获得,但您可能需要努力工作才能获得它。您可能需要从组织内的数据存储系统中提取它或收集调查数据。无论如何,您都需要以某种方式获取数据。

在这种情况下,你很幸运。当您的老板向您介绍您的目标时,他们还向您提供了 james_bond_data.csv 文件中的数据。您现在必须花一些时间来熟悉您面前的内容。在简报期间,您对该文件的内容做了一些注释:

Release

电影上映日期

Movie

电影的标题

Bond

饰演主角的演员

Bond_Car_MFG

詹姆斯·邦德汽车的制造商

US_Gross

这部电影的美国总收入

World_Gross

电影的全球总收入

Budget ($ 000s)

电影预算,千美元

Film_Length

电影的上映时间

Avg_User_IMDB

IMDb 的平均用户评分

Avg_User_Rtn_Tom

烂番茄的平均用户评分

Martinis

邦德在电影中喝了多少马提尼酒

正如您所看到的,您拥有各种各样的数据。您不需要所有这些来实现您的目标,但您可以稍后再考虑这一点。现在,您将集中精力从文件中获取数据并将其放入 Python 中进行清理和分析。

另请记住,保留原始文件被认为是最佳实践,以备将来需要。因此,您决定使用数据的清理版本创建第二个数据文件。这也将简化您的任务可能导致的任何未来分析。

从 CSV 文件读取数据

您可以获取多种文件格式的数据。最常见的文件之一是逗号分隔值 (CSV) 文件。这是一个文本文件,每段数据用逗号分隔。第一行通常是定义文件内容的标题行,后续行包含实际数据。 CSV 文件已使用多年,并且仍然很受欢迎,因为多个数据存储程序都使用它们。

由于 james_bond_data.csv 是一个文本文件,因此您可以在任何文本编辑器中打开它。下面的屏幕截图显示了它在记事本中打开:

正如您所看到的,CSV 文件读起来并不愉快。幸运的是,您很少需要阅读它们的原始形式。

当您需要分析数据时,Python 的 pandas 库是一个流行的选择。要在 Jupyter Notebook 中安装 pandas,请添加新的代码单元并输入 !python -m pip install pandas。当您运行单元时,您将安装该库。如果您在命令行中工作,则可以使用相同的命令,只是不带感叹号 (!)。

安装 pandas 后,您现在可以使用它将数据文件读取到 pandas DataFrame 中。下面的代码将为您完成此操作:

In [1]: import pandas as pd
   ...:
   ...: james_bond_data = pd.read_csv("james_bond_data.csv").convert_dtypes()

首先,将 pandas 库导入到您的程序中。标准做法是将 pandas 别名为 pd 以便代码用作参考。接下来,您使用 read_csv() 函数将数据文件读入名为 james_bond_data 的 DataFrame 中。这不仅会读取您的文件,还会负责从数据中整理标题并为每条记录建立索引。

虽然单独使用 pd.read_csv() 即可,但您也可以使用 .convert_dtypes()。这种良好的做法允许 pandas 优化它在 DataFrame 中使用的数据类型。

假设您的 CSV 文件包含一列缺少值的整数。默认情况下,这些将被分配numpy.NaN浮点常量。这会强制 pandas 为该列分配 float64 数据类型。然后,为了一致性,列中的所有整数都将转换为浮点数。

这些浮点值可能会导致后续计算结果中出现其他不需要的浮点数。同样,如果原始数字是年龄,那么将它们转换为浮点数可能不是您想要的。

您使用 .convert_dtypes() 意味着列将被分配一种扩展数据类型。任何 int 类型的整数列现在都将成为新的 Int64 类型。发生这种情况是因为 pandas.NA 表示原始缺失值并且可以作为 Int64 读取。同样,文本列变成 string 类型,而不是更通用的 object 类型。顺便说一句,浮点数成为新的 Float64 扩展类型,且 F 大写。

创建 DataFrame 后,您决定快速查看它以确保读取按照您的预期进行。执行此操作的快速方法是使用 .head()。默认情况下,此函数将显示前 5 条记录,但您可以自定义 .head(),通过向其传递整数来显示您喜欢的任何数字。在这里,您决定查看默认的五个记录:

In [2]: james_bond_data.head()
Out[2]:
           Release                  Movie          Bond  Bond_Car_MFG  \
0       June, 1962                 Dr. No  Sean Connery       Sunbeam
1     August, 1963  From Russia with Love  Sean Connery       Bentley
2        May, 1964             Goldfinger  Sean Connery  Aston Martin
3  September, 1965            Thunderball  Sean Connery  Aston Martin
4   November, 1967    You Only Live Twice  Sean Connery        Toyota

          US_Gross        World_Gross  Budget ($ 000s)  Film_Length  \
0   $16,067,035.00     $59,567,035.00        $1,000.00     110 mins
1   $24,800,000.00     $78,900,000.00        $2,000.00     115 mins
2   $51,100,000.00    $124,900,000.00        $3,000.00     110 mins
3   $63,600,000.00    $141,200,000.00        $9,000.00     130 mins
4   $43,100,000.00    $111,600,000.00        $9,500.00     117 mins

   Avg_User_IMDB  Avg_User_Rtn_Tom  Martinis  Kills_Bond
0            7.3               7.7         2           4
1            7.5               8.0         0          11
2            7.8               8.4         1           9
3            7.0               6.8         0          20
4            6.9               6.3         1          21

[5 rows x 12 columns]

您现在有一个 pandas DataFrame,其中包含记录及其标题以及左侧的数字索引。如果您使用的是 Jupyter Notebook,那么输出将如下所示:

如您所见,Jupyter Notebook 的输出更具可读性。但是,两者都比您开始使用的 CSV 文件好得多。

从其他来源读取数据

尽管 CSV 是一种流行的数据文件格式,但它并不是特别好。缺乏格式标准化意味着某些 CSV 文件包含多个页眉和页脚行,而其他文件则两者都不包含。此外,缺乏定义的日期格式以及在数据内和数据之间使用不同的分隔符和定界符可能会导致读取数据时出现问题。

幸运的是,pandas 允许您读取许多其他格式,例如 JSON 和 Excel。它还提供网络抓取功能,允许您从网站读取表格。一种特别有趣且相对较新的格式是用于处理批量数据的面向列的 Apache Parquet 文件格式。由于其压缩能力,Parquet 文件在与云存储系统一起使用时也具有成本效益。

虽然读取基本 CSV 文件的能力足以进行此分析,但下载部分提供了一些替代文件格式,其中包含与 james_bond_data.csv 相同的数据。每个文件都被命名为james_bond_data,并带有特定于文件的扩展名。为什么不看看您是否能够像处理 CSV 文件一样将它们读入 DataFrame 中呢?

如果您想要额外的挑战,请尝试按出版顺序从维基百科中抓取书籍表。如果你成功了,那么你将获得一些有价值的知识,M 会对你非常满意

要解决这些挑战,请展开以下可折叠部分:

要读取 JSON 文件,请使用 pd.read_json()

In [1]: import pandas as pd
   ...:
   ...: james_bond_data = pd.read_json("james_bond_data.json").convert_dtypes()

如您所见,您只需指定要读取的 JSON 文件即可。如果需要,您还可以指定一些有趣的格式和数据转换选项。文档页面会告诉您更多信息。

在此之前,您必须安装 openpyxl 库。您可以在 Jupyter Notebook 中使用命令 !python -m pip install openpyxl 或在终端使用 python -m pip install openpyxl 。要读取 Excel 文件,请使用 .read_excel()

In [1]: import pandas as pd
   ...:
   ...: james_bond_data = pd.read_excel("james_bond_data.xlsx").convert_dtypes()

和以前一样,您只需要指定文件名。如果您要读取多个工作表之一,则还必须使用 sheet_name 参数指定工作表名称。文档页面会告诉您更多信息。

在此之前,您必须安装一个序列化引擎,例如 pyarrow。为此,您可以在 Jupyter Notebook 中使用命令 !python -m pip install pyarrow 或在终端使用 python -m pip install pyarrow 。要读取 parquet 文件,请使用 .read_parquet()

In [1]: import pandas as pd
   ...:
   ...: james_bond_data = pd.read_parquet(
   ...:     "james_bond_data.parquet"
   ...: ).convert_dtypes()

和以前一样,您只需要指定文件名。文档页面将告诉您更多信息,包括如何使用替代序列化引擎。

在此之前,您必须安装 lxml 库以允许您读取 HTML 文件。为此,您可以在 Jupyter Notebook 中使用命令 !python -m pip install lxml 或在终端使用 python -m pip install lxml 。要读取或抓取 HTML 表格,请使用 read_html()

In [1]: import pandas as pd
   ...:
   ...: james_bond_tables = pd.read_html(
   ...:     "https://en.wikipedia.org/wiki/List_of_James_Bond_novels_and_short_stories"
   ...: )
   ...: james_bond_data = james_bond_tables[1].convert_dtypes()

这次,您传递了要抓取的网站的 URL。 read_html() 函数将返回网页上的表格列表。在此示例中,您感兴趣的位置位于列表索引 1 处,但找到您想要的位置可能需要一定量的试验和错误。文档页面会告诉您更多信息。

现在您已经有了数据,您可能认为是时候深入研究并开始分析了。虽然这很诱人,但你现在还做不到。这是因为您的数据可能尚不可分析。在下一步中,您将解决这个问题。

使用 Python 清理数据

数据分析工作流程的数据清理阶段通常是耗时最长的阶段,特别是当需要分析大量数据时。在此阶段,您必须检查数据,以确保其不存在格式不当、不正确、重复或不完整的数据。除非您有高质量的数据可供分析,否则您的 Python 数据分析代码极不可能返回高质量的结果。

虽然您必须在分析之前检查并重新检查数据以解决尽可能多的问题,但您还必须接受分析期间可能会出现其他问题。这就是为什么您之前看到的图表中的数据清理和分析阶段之间可能存在迭代的原因。

清理数据的传统方法是单独应用 pandas 方法,直到数据被清理为止。虽然这有效,但这意味着您创建了一组中间 DataFrame 版本,每个版本都应用了单独的修复。然而,这会给未来的清理带来再现性问题,因为您必须严格按照顺序重新应用每个修复。

更好的方法是使用一段代码重复更新内存中的相同 DataFrame 来清理数据。编写数据清理代码时,应该以增量方式构建它,并在编写每个增量后对其进行测试。然后,一旦您编写了足够多的内容来完全清理数据,您将拥有一个高度可重用的脚本,用于清理您将来可能需要分析的任何数据。这就是您将在此处采用的方法。

创建有意义的列名称

当您从某些系统中提取数据时,列名称可能没有您想要的那么有意义。确保 DataFrame 中的列被合理命名是一个很好的做法。为了保持它们在代码中的可读性,您应该采用使用全部小写字符的 Python 变量命名约定,多个单词之间用下划线分隔。这会强制您的分析代码使用这些名称,从而使其更具可读性。

要重命名 DataFrame 中的列,请使用 .rename()。您向其传递一个 Python 字典,其键是原始列名称,其值是替换名称:

In [3]: new_column_names = {
   ...:     "Release": "release_date",
   ...:     "Movie": "movie_title",
   ...:     "Bond": "bond_actor",
   ...:     "Bond_Car_MFG": "car_manufacturer",
   ...:     "US_Gross": "income_usa",
   ...:     "World_Gross": "income_world",
   ...:     "Budget ($ 000s)": "movie_budget",
   ...:     "Film_Length": "film_length",
   ...:     "Avg_User_IMDB": "imdb",
   ...:     "Avg_User_Rtn_Tom": "rotten_tomatoes",
   ...:     "Martinis": "martinis_consumed",
   ...:     "Kills_Bond": "bond_kills",
   ...: }
   ...:
   ...: data = james_bond_data.rename(columns=new_column_names)

在上面的代码中,您已将每个列名称替换为更 Pythonic 的名称。这将返回一个使用 data 变量引用的新 DataFrame,而不是 james_bond_data 引用的原始 DataFrame。 从现在开始,您将使用 data DataFrame。

与数据清理的所有阶段一样,测试您的代码是否按预期工作非常重要:

In [4]: data.columns
Out[4]:
Index(['release_date', 'movie_title', 'bond_actor', 'car_manufacturer',
       'income_usa', 'income_world', 'movie_budget', 'film_length',
       'imdb', 'rotten_tomatoes', 'martinis_consumed', 'bond_kills'],
      dtype='object')

要快速查看 DataFrame 中的列标签,请使用 DataFrame 的 .columns 属性。如您所见,您已成功重命名列。现在您已准备好继续并清理实际数据本身。

处理缺失数据

作为起点,您可以快速检查数据中是否缺少任何内容。 DataFrame 的 .info() 方法允许您快速执行此操作:

In [5]: data.info()
Out[5]:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 27 entries, 0 to 26
Data columns (total 12 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   release_date            27 non-null     string
 1   movie_title             27 non-null     string
 2   bond_actor              27 non-null     string
 3   car_manufacturer        27 non-null     string
 4   income_usa              27 non-null     string
 5   income_world            27 non-null     string
 6   movie_budget            27 non-null     string
 7   film_length             27 non-null     string
 8   imdb                    26 non-null     Float64
 9   rotten_tomatoes         26 non-null     Float64
 10  martinis_consumed       27 non-null     Int64
 11  bond_kills              27 non-null     Int64
dtypes: Float64(2), Int64(2), string(8)
memory usage: 2.8 KB

当此方法运行时,您会看到 DataFrame 的非常简洁的摘要。 .info() 方法显示缺少数据。输出顶部附近的 RangeIndex 行告诉您已将 27 行数据读入 DataFrame。但是,imdbrotten_tomatoes 列各仅包含 26 个非空值。这些列中的每一列都有一条缺失数据。

您可能还注意到某些数据列的数据类型不正确。首先,您将专注于修复丢失的数据。之后您将处理数据类型问题。

在修复这些列之前,您需要查看它们。下面的代码将向您展示它们:

In [6]: data.loc[data.isna().any(axis="columns")]
Out[6]:
   release_date           movie_title   bond_actor car_manufacturer  \
10  April, 1977  The Spy Who Loved Me  Roger Moore            Lotus

         income_usa       income_world  movie_budget  film_length  \
10   $46,800,000.00    $185,400,000.00    $14,000.00     125 mins

    imdb  rotten_tomatoes  martinis_consumed  bond_kills
10  <NA>             <NA>                  1          31

[1 rows x 12 columns]

要查找缺少数据的行,您可以使用 DataFrame 的 .isna() 方法。这将分析 data DataFrame 并返回第二个大小相同的布尔 DataFrame,其中包含 TrueFalse 值,具体取决于是否data DataFrame 中的相应值是否包含

一旦你有了第二个布尔数据帧,你就可以使用它的 .any(axis="columns") 方法返回一个 pandas Series,它将包含 True 其中第二个中的行DataFrame 有一个 True 值,如果没有,则为 False 值。此系列中的 True 值表示包含缺失数据的行,而 False 值表示不存在缺失数据的行。

此时,您有一个布尔值系列。要查看行本身,您可以使用 DataFrame 的 .loc 属性。虽然您通常使用 .loc 通过标签访问行和列的子集,但您也可以将布尔系列传递给它并返回一个仅包含与 True 对应的行的 DataFrame代码> 系列中的条目。这些是缺少数据的行。

如果将所有这些放在一起,则会得到 data.loc[data.isna().any(axis="columns")]。如您所见,输出仅显示一行包含两个 值。

当您第一次看到只出现一行时,您可能会感到有点震动,但现在您不再激动,因为您明白原因了。

您的目标之一是生成将来可以重用的代码。前面的代码实际上仅用于查找重复项,不会成为最终生产代码的一部分。如果您在 Jupyter Notebook 中工作,那么您可能会想包含这样的代码。虽然如果你想记录你所做的一切,这是必要的,但你最终会得到一个凌乱的笔记本,这会分散其他人阅读的注意力。

如果您在 JupyterLab 中的笔记本中工作,那么一个好的工作流程策略是在 JupyterLab 中针对您的笔记本打开一个新控制台,并在该控制台中运行测试和探索性代码。您可以将任何能够提供所需结果的代码复制到 Jupyter 笔记本中,并且可以丢弃任何不符合您预期或不需要的代码。

要向笔记本添加新控制台,请右键单击正在运行的笔记本上的任意位置,然后从出现的弹出菜单中选择笔记本的新控制台。您的笔记本下方将出现一个新控制台。在控制台中输入您想要试验的任何代码,然后点击 Shift<span>+Enter 来运行它。您将看到结果显示在代码上方,以便您决定是否保留它。

分析完成后,您应该从头开始重置并重新测试整个 Jupyter Notebook。为此,请从菜单中选择内核重新启动内核清除所有单元的输出。这将重置笔记本的内核,删除之前结果的所有痕迹。然后,您可以按顺序重新运行代码单元以验证一切是否正常工作。

可下载内容中提供了两个 Jupyter 笔记本,您可以通过单击下面的链接获取:

data_analysis_results.ipynb 笔记本包含用于清理和分析数据的可重用代码版本,而 data_analysis_findings.ipynb 笔记本包含用于获得这些数据的过程的日志。最终结果。

您可以使用其他 Python 环境完成本教程,但强烈建议使用 JupyterLab 中的 Jupyter Notebook。

要修复这些错误,您需要更新data DataFrame。正如您之前了解到的,您将在 data 变量引用的 DataFrame 中临时构建所有更改,然后在全部完成后将它们写入磁盘。您现在将添加一些代码来修复您发现的那些 值。

经过一番研究后,您发现缺失值分别为 7.16.8。下面的代码将正确更新每个缺失值:

In [7]: data = james_bond_data.rename(columns=new_column_names).combine_first(
   ...:     pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:)

在这里,您选择使用 Python 字典定义 DataFrame。字典的键定义其列标题,而其值定义数据。每个值都包含一个嵌套字典。该嵌套字典的键提供行索引,而值提供更新。数据框看起来像这样:

In [8]: pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
Out[8]:
    imdb  rotten_tomatoes
10   7.1              6.8

然后,当您调用 .combine_first() 并向其传递此 DataFrame 时, 行中的 imdbrotten_tomatoes 列中的两个缺失值10 分别替换为 7.16.8。请记住,您尚未更新原始 james_bond_data DataFrame。您仅更改了 data 变量引用的 DataFrame。

你现在必须测试你的努力。继续运行 data[data.isna().any(axis="columns")] 以确保没有返回任何行。您应该看到一个空的 DataFrame。

现在您将修复无效的数据类型。如果没有这一点,对数据的数值分析即使不是不可能,也是毫无意义的。首先,您将修复货币列。

处理财务专栏

您之前运行的 data.info() 代码还向您揭示了一个更微妙的问题。 venue_usavenue_worldmovie_budgetfilm_length 列的数据类型均为 string。但是,这些都应该是数字类型,因为字符串对于计算用处不大。同样,包含发布日期的 release 列也是一个 string。这应该是一个日期类型。

首先,您需要查看每列中的一些数据以了解问题所在:

In [9]: data[
   ...:     ["income_usa", "income_world", "movie_budget", "film_length"]
   ...: ].head()
Out[9]:
        income_usa     income_world  movie_budget  film_length
0   $16,067,035.00   $59,567,035.00     $1,000.00     110 mins
1   $24,800,000.00   $78,900,000.00     $2,000.00     115 mins
2   $51,100,000.00  $124,900,000.00     $3,000.00     110 mins
3   $63,600,000.00  $141,200,000.00     $9,000.00     130 mins
4   $43,100,000.00  $111,600,000.00     $9,500.00     117 mins

要访问多个列,请将列名称列表传递到 DataFrame 的 [] 运算符中。虽然您也可以使用 data.loc[],但单独使用 data[] 更干净。任一选项都会返回一个包含这些列中所有数据的 DataFrame。为了使事情易于管理,您可以使用 .head() 方法将输出限制为前五个记录。

如您所见,三个财务列均包含美元符号和逗号分隔符,而 film_length 列包含 "mins"。您需要删除所有这些才能在分析中使用剩余的数字。这些附加字符就是数据类型被误解为字符串的原因。

尽管您可以替换整个 DataFrame 中的 $ 符号,但这可能会在您不想删除的地方将其删除。如果一次删除一列会更安全。为此,您可以充分利用 DataFrame 的 .assign() 方法。这可以向 DataFrame 添加新列,或用更新的值替换现有列。

首先,假设您想要替换正在创建的 data DataFrame 的 venue_usa 列中的 $ 符号。第 6 行到第 12 行中的附加代码实现了此目的:

In [10]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:     )
   ...: )

要更正 venue_usa 列,您可以将其新数据定义为 pandas Series,并将其传递到 data DataFrame 的 .assign() 方法。然后,此方法将用新系列覆盖现有列或创建包含它的新列。您将要更新或创建的列的名称定义为引用新数据系列的命名参数。在本例中,您将传递一个名为 venue_usa 的参数。

最好使用 lambda 函数创建新系列。本示例中使用的 lambda 函数接受 data DataFrame 作为其参数,然后使用 .replace() 方法删除 $ 和逗号venue_usa 列中每个值的分隔符。最后,它将当前类型为 string 的剩余数字转换为 Float64

要实际删除 $ 符号和逗号,请将正则表达式 [$,] 传递到 .replace() 中。通过将这两个字符括在 [] 中,您就指定要删除这两个字符的所有实例。然后将它们的替换定义为 ""。您还可以将 regex 参数设置为 True 以允许将 [$,] 解释为正则表达式。

lambda 函数的结果是一个不带 $ 或逗号分隔符的 Series。然后,您将此系列分配给变量 venue_usa。这会导致 .assign() 方法使用清理后的更新覆盖现有 venue_usa 列的数据。

再看一下上面的代码,您就会看到这一切是如何组合在一起的。您向 .assign() 传递一个名为 venue_usa 的参数,该参数引用一个 lambda 函数,该函数计算包含更新内容的 Series。您将 lambda 生成的 Series 分配给名为 venue_usa 的参数,该参数告诉 .assign() 使用新的值更新现有的 venue_usa 列价值观。

现在继续运行此代码以从 venue_usa 列中删除有问题的字符。不要忘记测试您的工作并验证您是否已进行替换。另外,请记住验证 venue_usa 的数据类型确实是 Float64

当然,您需要处理的不仅仅是 venue_usa 列。您还需要对 venue_worldmovie_budget 列执行相同的操作。您还可以使用相同的 .assign() 方法来实现此目的。您可以使用它来创建和分配任意数量的列。您只需将它们作为单独的命名参数传递即可。

为什么不继续看看是否可以编写从 venue_worldmovie_budget 列中删除相同的两个字符的代码?和以前一样,不要忘记验证您的代码是否按预期工作,但请记住检查正确的列!

一旦您尝试解决这些列的剩余问题,您可以揭示以下解决方案:

在下面的代码中,您使用了之前的代码,但添加了 lambda 来删除剩余的 "$" 和分隔符字符串:

In [11]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:     )
   ...: )

第 12 行处理 venue_world 数据,第 17 行处理 movie_budget 数据。正如您所看到的,所有三个 lambda 的工作方式都是相同的。

完成这些更正后,请记住使用 data.info() 测试您的代码。您会看到财务数据不再是字符串类型,而是Float64数字。要查看实际的更改,您可以使用data.head()

现在更正了货币列数据类型,您可以修复其余的无效类型。

更正无效数据类型

接下来,您必须从胶片长度值中删除 "mins" 字符串,然后将该列转换为整数类型。这将允许您分析这些值。要删除有问题的 "mins" 文本,您决定使用 pandas 的 .str.removesuffix() 系列方法。这允许您删除从 film_length 列右侧传递给它的字符串。然后,您可以使用 .astype("Int64") 来处理数据类型。

使用上述信息,继续查看是否可以使用 lambda 更新 film_length 列,并将其作为另一个参数添加到 .assign() 方法中。

您可以透露以下解决方案:

在下面的代码中,您使用了之前的代码,但添加了一个 lambda 来删除从第 22 行开始的 "mins" 字符串:

In [12]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         film_length=lambda data: (
   ...:             data["film_length"]
   ...:             .str.removesuffix("mins")
   ...:             .astype("Int64")
   ...:         ),
   ...:     )
   ...: )

如您所见,lambda 在第 24 行使用 .removesuffix() 来更新 film_length 列,方法是根据原始 film_length 中的数据生成新的 Series 列,但减去每个值末尾的 "mins" 字符串。为了确保您可以将列的数据用作数字,请使用 .astype("Int64")

和以前一样,使用您之前使用的 .info().head() 方法测试您的代码。您应该会看到 film_length 列现在具有更有用的 Int64 数据类型,并且您已删除了 “mins”

除了财务数据的问题之外,您还注意到 release_date 列被视为字符串。要将其数据转换为datetime格式,您可以使用pd.to_datetime()

要使用 to_datetime(),请将系列 data["release_date"] 传递给其中,不要忘记指定格式字符串以允许正确解释日期值。这里每个日期的形式都是 June, 1962,因此在您的代码中,您使用 %B 后跟逗号和空格来表示月份名称的位置,然后%Y 表示四位数年份。

您还可以借此机会在 DataFrame 中创建一个名为 release_year 的新列,用于存储更新的 data["release_date"] 列数据的年份部分。访问该值的代码是data["release_date"].dt.year。您认为将每年分开可能对未来的分析甚至未来的 DataFrame 索引有用。

使用上述信息,继续查看是否可以将 release_date 列更新为正确的类型,并创建一个新的 release_year 列,其中包含每部电影的上映年份。和以前一样,您可以使用 .assign() 和 lambda 来实现这两个目的,并且再次像以前一样,记住测试您的努力。

您可以透露以下解决方案:

在下面的代码中,您使用之前的代码并添加了 lambda 来更新 release_date 列的数据类型并创建一个包含发布年份的新列:

In [13]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         film_length=lambda data: (
   ...:             data["film_length"]
   ...:             .str.removesuffix("mins")
   ...:             .astype("Int64")
   ...:         ),
   ...:         release_date=lambda data: pd.to_datetime(
   ...:             data["release_date"], format="%B, %Y"
   ...:         ),
   ...:         release_year=lambda data: (
   ...:             data["release_date"]
   ...:             .dt.year
   ...:             .astype("Int64")
   ...:         ),
   ...:     )
   ...: )

如您所见,第 27 行分配给 release_date 的 lambda 更新了 release_date 列,而第 30 行的 lambda 创建了新的 release_year 列包含 release_date 列中日期的 year 部分。

一如既往,不要忘记测试你的努力。

现在您已经解决了这些最初的问题,您可以重新运行 data.info() 以验证您是否已解决所有最初的问题:

In [14]: data.info()
Out[14]:
<class 'pandas.core.frame.DataFrame'>
Index: 27 entries, 0 to 26
Data columns (total 13 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   bond_actor              27 non-null     string
 1   bond_kills              27 non-null     Int64
 2   car_manufacturer        27 non-null     string
 3   film_length             27 non-null     Int64
 4   income_usa              27 non-null     Float64
 5   income_world            27 non-null     Float64
 6   imdb                    27 non-null     Float64
 7   martinis_consumed       27 non-null     Int64
 8   movie_budget            27 non-null     Float64
 9   movie_title             27 non-null     string
 10  release_date            27 non-null     datetime64[ns]
 11  rotten_tomatoes         27 non-null     Float64
 12  release_year            27 non-null     Int64
dtypes: Float64(5), Int64(3), datetime64[ns](1), string(3)
memory usage: 2.8 KB

正如您所看到的,原来的二十七个条目现在都包含了数据。 release_date 列采用 datetime64 格式,三个收入和 film_length 列均采用数字类型。 DataFrame 中甚至还有一个新的 release_year 列。当然,这种检查并不是真正必要的,因为像所有优秀的秘密特工一样,您在编写代码时已经检查了代码。

您可能还注意到列顺序已更改。发生这种情况是由于您之前使用了 combine_first()。在此分析中,列顺序并不重要,因为您永远不需要显示 DataFrame。如有必要,您可以使用方括号指定列顺序,如 data[["column_1", ...]] 中。

至此,您已确保数据中没有丢失任何内容,并且类型全部正确。接下来,您将注意力转向实际数据本身。

修复数据不一致的问题

在之前更新 movie_budget 列标签时,您可能已经注意到,与其他财务列相比,其数字显得较小。原因是它的数据以千为单位,而其他列都是实际数字。您决定对此采取一些措施,因为如果将此数据与您处理的其他金融专栏进行比较,可能会导致问题。

您可能会想编写另一个 lambda 并使用 movie_budget 参数将其传递到 .assign() 中。不幸的是,这不起作用,因为您不能在同一个函数中两次使用相同的参数。您可以重新访问 movie_budget 参数并添加将其结果乘以 1000 的功能,或者您可以根据 movie_budget 列值创建另一列。或者,您可以创建单独的 .assign() 调用。

这些选项中的每一个都可以,但乘以现有值可能是最简单的。继续看看您是否可以将之前的 movie_budget lambda 的结果乘以 1000

您可以透露以下解决方案:

下面的代码与早期版本类似。要乘以 lambda 结果,请使用乘法:

In [15]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:             * 1000
   ...:         ),
   ...:         film_length=lambda data: (
   ...:             data["film_length"]
   ...:             .str.removesuffix("mins")
   ...:             .astype("Int64")
   ...:         ),
   ...:         release_date=lambda data: pd.to_datetime(
   ...:             data["release_date"], format="%B, %Y"
   ...:         ),
   ...:         release_year=lambda data: (
   ...:             data["release_date"]
   ...:             .dt.year
   ...:             .astype("Int64")
   ...:         ),
   ...:     )
   ...: )

从第 17 行开始的 lambda 固定了货币值,并且您已在第 21 行对其进行了调整,将这些值乘以 1000。所有财务栏现在都采用相同的单位,从而可以进行比较。

您可以使用之前使用过的技术来查看 movie_budget 中的值并确认您已正确调整它们。

现在您已经解决了一些格式问题,是时候继续进行其他检查了。

纠正拼写错误

最困难的数据清理任务之一是检查拼写错误,因为它们可能出现在任何地方。因此,您通常直到分析后期才会遇到它们,实际上,您可能根本不会注意到它们。

在本练习中,您将查找扮演邦德的演员姓名和汽车制造商名称中的拼写错误。这相对简单,因为这两列都包含来自一组有限允许值的数据项:

In [16]: data["bond_actor"].value_counts()
Out[16]:
bond_actor
Roger Moore       7
Sean Connery      5
Daniel Craig      5
Pierce Brosnan    4
Timothy Dalton    3
George Lazenby    1
Shawn Connery     1
Roger MOORE       1
Name: count, dtype: Int64

.value_counts() 方法允许您快速获取 pandas Series 中每个元素的计数。在这里,您可以使用它来帮助您查找 bond_actor 列中可能存在的拼写错误。正如您所看到的,肖恩·康纳利 (Sean Connery) 的一个实例和罗杰·摩尔 (Roger Moore) 的一个实例包含拼写错误。

要通过字符串替换来修复这些问题,请使用 pandas 数据系列的 .str.replace() 方法。在最简单的形式中,您只需向其传递原始字符串和要替换它的字符串。在这种情况下,您可以通过链接对 .str.replace() 的两个调用来同时替换这两个拼写错误。

使用上述信息,继续查看是否可以更正 bond_actor 列中的拼写错误。和以前一样,您可以使用 lambda 来实现此目的。

您可以透露以下解决方案:

在更新的代码中,您修复了演员的姓名:

In [17]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:             * 1000
   ...:         ),
   ...:         film_length=lambda data: (
   ...:             data["film_length"]
   ...:             .str.removesuffix("mins")
   ...:             .astype("Int64")
   ...:         ),
   ...:         release_date=lambda data: pd.to_datetime(
   ...:             data["release_date"], format="%B, %Y"
   ...:         ),
   ...:         release_year=lambda data: (
   ...:             data["release_date"]
   ...:             .dt.year
   ...:             .astype("Int64")
   ...:         ),
   ...:         bond_actor=lambda data: (
   ...:             data["bond_actor"]
   ...:             .str.replace("Shawn", "Sean")
   ...:             .str.replace("MOORE", "Moore")
   ...:         ),
   ...:     )
   ...: )

如您所见,第 36 行的新 lambda 更新了 Bond_actor 列中的两个拼写错误。第一个 .str.replace()Shawn 的所有实例更改为 Sean,而第二个则修复 MOORE > 实例。

您可以通过再次重新运行 .value_counts() 方法来测试是否已进行这些更改。

作为练习,为什么不分析一下汽车制造商的名称,看看是否能发现任何拼写错误?如果有,请使用上面显示的技术来修复它们。

您可以透露以下解决方案:

再次使用 value_counts() 分析 car_manufacturer 列:

In [18]: data["car_manufacturer"].value_counts()
Out[18]:
car_manufacturer
Aston Martin    8
AMC             3
Rolls Royce     3
Lotus           2
BMW             2
Astin Martin    2
Sunbeam         1
Bentley         1
Toyota          1
Mercury         1
Ford            1
Citroen         1
Bajaj           1
Name: count, dtype: Int64

这次,有两个针对名为 Astin Martin 的汽车的恶意条目。这些是不正确的,需要修复:

In [19]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:             * 1000
   ...:         ),
   ...:         film_length=lambda data: (
   ...:             data["film_length"]
   ...:             .str.removesuffix("mins")
   ...:             .astype("Int64")
   ...:         ),
   ...:         release_date=lambda data: pd.to_datetime(
   ...:             data["release_date"], format="%B, %Y"
   ...:         ),
   ...:         release_year=lambda data: (
   ...:             data["release_date"]
   ...:             .dt.year
   ...:             .astype("Int64")
   ...:         ),
   ...:         bond_actor=lambda data: (
   ...:             data["bond_actor"]
   ...:             .str.replace("Shawn", "Sean")
   ...:             .str.replace("MOORE", "Moore")
   ...:         ),
   ...:         car_manufacturer=lambda data: (
   ...:             data["car_manufacturer"].str.replace("Astin", "Aston")
   ...:         ),
   ...:     )
   ...: )

要修复拼写错误,您可以使用与之前相同的技术,只是这次将 car_manufacturer 列中的 "Astin" 替换为 "Aston" 。第 41 行的 lambda 实现了这一点。

当然,在继续之前,您应该针对您的数据重新运行 .value_counts() 方法以验证您的更新。

修复了拼写错误后,接下来您将看看是否可以找到任何可疑的数据。

检查无效异常值

您将执行的下一个检查是验证数值数据是否在正确的范围内。这再次需要仔细考虑,因为任何大或小的数据点都可能是真正的异常值,因此您可能需要重新检查您的来源。但有些可能确实是不正确的。

在此示例中,您将调查邦德在每部电影中消耗的马提尼酒以及每部电影的长度,以确保它们的值在合理的范围内。您可以通过多种方法分析数值数据以检查异常值。一种快速的方法是使用 .describe() 方法:

In [20]: data[["film_length", "martinis_consumed"]].describe()
Out[20]:
       film_length  martinis_consumed
count    27.000000               27.0
mean    168.222222            0.62963
std     206.572083           1.547905
min     106.000000               -6.0
25%     123.000000                0.0
50%     130.000000                1.0
75%     133.000000                1.0
max    1200.000000                3.0

当您在 pandas Series 或 DataFrame 上使用 .describe() 时,它会为您提供一组与 Series 或 DataFrame 的数值相关的统计度量。正如您所看到的,.describe() 为您提供了一系列与您调用它的 DataFrame 的两列中的每一列相关的统计数据。这些也揭示了一些可能的错误。

查看 film_length 列,四分位数显示大多数电影的长度约为 130 分钟,但平均值接近 170 分钟。平均值已被最大值(高达 1200 分钟)所扭曲。

根据分析的性质,您可能需要重新检查源以查明该最大值是否确实不正确。在这种情况下,一部持续二十个小时的电影显然表明存在拼写错误。验证原始数据集后,您发现 120 是正确的值。

再看看邦德在每部电影中喝的马提尼酒的数量,-6 的最低数字根本没有意义。和以前一样,您重新检查源并发现这应该是 6。

您可以使用前面介绍的 .replace() 方法修复这两个错误。例如,data["martinis_consumed"].replace(-6, 6) 将更新马提尼数字,您可以在电影持续时间中使用类似的技术。和以前一样,您可以在 .assign() 中使用 lambda 来完成这两项操作,所以为什么不尝试一下呢?

您可以在下面透露更新的清理代码,包括这些最新添加的内容:

现在您已经添加了两个额外的 lambda:

In [21]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:             * 1000
   ...:         ),
   ...:         film_length=lambda data: (
   ...:             data["film_length"]
   ...:             .str.removesuffix("mins")
   ...:             .astype("Int64")
   ...:             .replace(1200, 120)
   ...:         ),
   ...:         release_date=lambda data: pd.to_datetime(
   ...:             data["release_date"], format="%B, %Y"
   ...:         ),
   ...:         release_year=lambda data: (
   ...:             data["release_date"]
   ...:             .dt.year
   ...:             .astype("Int64")
   ...:         ),
   ...:         bond_actor=lambda data: (
   ...:             data["bond_actor"]
   ...:             .str.replace("Shawn", "Sean")
   ...:             .str.replace("MOORE", "Moore")
   ...:         ),
   ...:         car_manufacturer=lambda data: (
   ...:             data["car_manufacturer"].str.replace("Astin", "Aston")
   ...:         ),
   ...:         martinis_consumed=lambda data: (
   ...:             data["martinis_consumed"].replace(-6, 6)
   ...:         ),
   ...:     )
   ...: )

之前,您使用 lambda 从 film_length 列条目中删除 "mins" 字符串。因此,您无法在同一个 .assign() 中创建单独的 lambda 来替换不正确的胶片长度,因为这样做意味着将第二个参数传递到 .assign() 与此同名。这当然是非法的。

然而,有一个替代解决方案需要一些横向思考。您可以创建一个单独的 .assign() 方法,但将同一列的所有更改保留在同一个 .assign() 中可能更具可读性。

为了执行替换,您从第 23 行开始调整了现有 lambda,将无效的 1200 替换为 120。您在第 45 行使用新的 lambda 修复了马提尼酒列,并将 -6 替换为 6

与以往一样,您应该使用 describe() 方法再次测试这些更新。您现在应该会看到最大 film_length 和最小 martinis_consumed 列的合理值。

您的数据几乎已被清理。还有一件事需要检查和解决,那就是喝太多伏特加马提尼酒可能会让你看到重影。

删除重复数据

您要检查的最后一个问题是是否有任何数据行重复。通常最好将此步骤留到最后,因为之前的更改可能会导致出现重复数据。当您修复数据中的字符串时,最常发生这种情况,因为通常是同一字符串的不同变体首先导致出现不需要的重复。

检测重复项的最简单方法是使用 DataFrame 的 .duplicated() 方法:

In [22]: data.loc[data.duplicated(keep=False)]
Out[22]:
        bond_actor  bond_kills car_manufacturer  film_length  \
8      Roger Moore           1              AMC          125
9      Roger Moore           1              AMC          125
15  Timothy Dalton          13      Rolls Royce          130
16  Timothy Dalton          13      Rolls Royce          130

    income_usa  income_world  imdb  martinis_consumed  \
8   21000000.0    97600000.0   6.7                  0
9   21000000.0    97600000.0   6.7                  0
15  51185000.0   191200000.0   6.7                  2
16  51185000.0   191200000.0   6.7                  2

    movie_budget                  movie_title release_date  \
8      7000000.0  The Man with the Golden Gun   1974-07-01
9      7000000.0  The Man with the Golden Gun   1974-07-01
15    40000000.0         The Living Daylights   1987-05-01
16    40000000.0         The Living Daylights   1987-05-01

    rotten_tomatoes  release_year
8               5.1          1974
9               5.1          1974
15              6.3          1987
16              6.3          1987

[4 rows x 13 columns]

通过设置 keep=False.duplicate() 方法将返回一个布尔系列,其中重复行标记为 True。正如您之前所看到的,当您将此布尔系列传递到 data.loc[] 时,会向您显示重复的 DataFrame 行。在您的数据中,两行已重复。因此,下一步是删除每一行的一个实例。

要消除重复行,请在正在构建的 data DataFrame 上调用 .drop_duplicates() 方法。顾名思义,此方法将遍历 DataFrame 并删除它找到的所有重复行,只留下一个。要按顺序重新索引 DataFrame,请设置 ignore_index=True

看看您是否能找出在代码中插入 .drop_duplicates() 的位置。您不使用 lambda,但在对 .assign() 的调用完成后,重复项将被删除。测试您的努力以确保您确实删除了重复项。

您可以透露以下解决方案:

在更新的代码中,您删除了重复的行:

In [23]: data = (
   ...:     james_bond_data.rename(columns=new_column_names)
   ...:     .combine_first(
   ...:         pd.DataFrame({"imdb": {10: 7.1}, "rotten_tomatoes": {10: 6.8}})
   ...:     )
   ...:     .assign(
   ...:         income_usa=lambda data: (
   ...:             data["income_usa"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         income_world=lambda data: (
   ...:             data["income_world"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:         ),
   ...:         movie_budget=lambda data: (
   ...:             data["movie_budget"]
   ...:             .replace("[$,]", "", regex=True)
   ...:             .astype("Float64")
   ...:             * 1000
   ...:         ),
   ...:         film_length=lambda data: (
   ...:             data["film_length"]
   ...:             .str.removesuffix("mins")
   ...:             .astype("Int64")
   ...:             .replace(1200, 120)
   ...:         ),
   ...:         release_date=lambda data: pd.to_datetime(
   ...:             data["release_date"], format="%B, %Y"
   ...:         ),
   ...:         release_year=lambda data: (
   ...:             data["release_date"]
   ...:             .dt.year
   ...:             .astype("Int64")
   ...:         ),
   ...:         bond_actor=lambda data: (
   ...:             data["bond_actor"]
   ...:             .str.replace("Shawn", "Sean")
   ...:             .str.replace("MOORE", "Moore")
   ...:         ),
   ...:         car_manufacturer=lambda data: (
   ...:             data["car_manufacturer"].str.replace("Astin", "Aston")
   ...:         ),
   ...:         martinis_consumed=lambda data: (
   ...:             data["martinis_consumed"].replace(-6, 6)
   ...:         ),
   ...:     )
   ...:     .drop_duplicates(ignore_index=True)
   ...: )

正如您所看到的,在 .assign() 方法完成调整和创建列之后,您已将 .drop_duplicates() 放置在第 49 行。

如果重新运行 data.loc[data.duplicated(keep=False)],它不会返回任何行。现在每一行都是唯一的。

您现在已经成功识别了数据中的多个缺陷,并使用了各种技术来清理它们。请记住,如果您的分析突出了新的缺陷,那么您可能需要再次重新审视清理阶段。在这种情况下,这是没有必要的。

适当清理数据后,您可能会想跳进去开始分析。但在开始之前,不要忘记您还有另一项非常重要的任务要做!

存储清理后的数据

作为培训的一部分,您已经了解到应该将清理后的 DataFrame 保存到新文件中。其他分析师可以使用它来省去再次清理相同问题的麻烦,但您也允许访问原始文件,以防他们需要参考。 .to_csv() 方法允许您执行此良好实践:

In [36]: data.to_csv("james_bond_data_cleansed.csv", index=False)

您将清理后的 DataFrame 写入名为 james_bond_data_cleansed.csv 的 CSV 文件。通过设置 index=False,您不会编写索引,而只会编写纯数据。该文件将对未来的分析师有用。

在继续之前,花点时间反思一下到目前为止您所取得的成就。您已经清理了数据,使其结构健全,没有任何丢失、没有重复、没有无效的数据类型或异常值。您还消除了相似数据值之间的拼写错误和不一致之处。

到目前为止,您所做的巨大努力不仅使您能够自信地分析数据,而且通过突出显示这些问题,您也可以重新访问数据源并修复这些问题。事实上,如果您强调了获取原始数据的过程中的缺陷,也许可以防止将来再次出现类似的问题。

数据清理确实值得投入时间和精力,并且您已经达到了一个重要的里程碑。现在您已经整理并存储了数据,是时候继续执行任务的主要部分了。是时候开始实现你的目标了。

使用 Python 执行数据分析

数据分析是一个很大的话题,需要大量的学习才能掌握。然而,有四种主要的分析类型:

  • 描述性分析使用以前的数据来解释过去发生的事情。常见的例子包括识别销售趋势或客户的行为。

  • 诊断分析将事情更进一步,并试图找出为什么会发生这些事件。例如,为什么会出现销售趋势?您的客户究竟为何这么做?

  • 预测分析建立在先前分析的基础上,并使用技术来尝试预测未来可能发生的情况。例如,您预计未来的销售趋势会怎样?或者您希望客户下一步做什么?

  • 规范性分析采用早期分析类型发现的所有内容,并使用该信息制定未来策略。例如,您可能希望实施措施来防止销售趋势预测下降或防止客户在其他地方购买。

在本教程中,您将使用 Python 对 james_bond_data_cleansed.csv 数据文件执行一些描述性分析技术,以回答老板之前提出的问题。是时候深入了解一下你能找到什么了。

您在本教程开始时看到的工作流程图中的分析阶段的目的是让您处理清理后的数据并从中提取对其他感兴趣方有用的见解和关系。尽管其他人可能会对你的结论感兴趣,但如果你对如何得出这些结论提出质疑,你就有源数据来支持你的主张。

要完成本教程的其余部分,您需要安装 matplotlibscikit-learn 库。您可以使用 python -m pip install matplotlib scikit-learn 来完成此操作,但如果您在 Jupyter 中使用它,请不要忘记在其前面添加前缀!笔记本。

在分析过程中,您将绘制一些数据图。为此,您将使用 Matplotlib 库的绘图功能。

此外,您将执行回归分析,因此您需要使用 scikit-learn 库中的一些工具。

执行回归分析

您的数据包含来自烂番茄和 IMDb 的评论。您的第一个目标是查明烂番茄的评分与 IMDb 的评分之间是否存在关系。为此,您将使用回归分析来查看两个评级集是否相关。

执行回归分析时,一个好的第一步是绘制您正在分析的两组数据的散点图。该图的形状为您提供了关于它们之间是否存在任何关系的快速视觉线索,如果存在,是否是线性、二次或指数关系。

下面的代码设置您最终生成两个评级集的散点图:

In [1]: import pandas as pd
   ...: import matplotlib.pyplot as plt
   ...:
   ...: data = pd.read_csv("james_bond_data_cleansed.csv").convert_dtypes()

首先,导入 pandas 库,以便将闪亮的新 james_bond_data_cleansed.csv 读取到 DataFrame 中。您还导入 matplotlib.pyplot 库,您将使用它来创建实际的散点图。

然后,您可以使用以下代码实际创建散点图:

In [2]: fig, ax = plt.subplots()
   ...: ax.scatter(data["imdb"], data["rotten_tomatoes"])
   ...: ax.set_title("Scatter Plot of Ratings")
   ...: ax.set_xlabel("Average IMDb Rating")
   ...: ax.set_ylabel("Average Rotten Tomatoes Rating")
   ...: fig.show()

调用 subplots() 函数会设置一个基础架构,允许您将一个或多个绘图添加到同一图中。这不会让你担心,因为你只有一个,但它的功能值得研究。

要创建初始散点图,请将水平系列指定为数据的 imdb 列,将垂直系列指定为 rotten_tomatoes 列。这里的顺序是任意的,因为您感兴趣的是它们之间的关系。

为了帮助读者理解您的绘图,接下来为您的绘图指定一个标题,然后为两个轴提供合理的标签。可能需要使用 fig.show() 代码来显示您的绘图,该代码在 Jupyter Notebook 中是可选的。

在 Jupyter Notebooks 中,您的绘图应如下所示:

散点图显示从左到右向上的明显斜率。这意味着当一组评级增加时,另一组评级也会增加。为了更深入地挖掘并找到一种数学关系,使您能够根据一组来估计另一组,您需要执行回归分析。这意味着您需要按如下方式扩展之前的代码:

In [3]: from sklearn.linear_model import LinearRegression
   ...:
   ...: x = data.loc[:, ["imdb"]]
   ...: y = data.loc[:, "rotten_tomatoes"]

首先,导入LinearRegression。正如您很快就会看到的,您将需要它来执行线性回归计算。然后,您创建一个 pandas DataFrame 和一个 pandas Series。您的 x 是一个包含 imdb 列数据的 DataFrame,而 y 是一个包含 rotten_tomatoes 列数据的系列数据。您可能会在多个功能上进行回归,这就是为什么 x 被定义为具有列列表的 DataFrame 的原因。

现在您已拥有执行线性回归计算所需的一切:

In [4]: model = LinearRegression()
   ...: model.fit(x, y)
   ...:
   ...: r_squared = f"R-Squared: {model.score(x, y):.2f}"
   ...: best_fit = f"y = {model.coef_[0]:.4f}x{model.intercept_:+.4f}"
   ...: y_pred = model.predict(x)

首先,您创建一个 LinearRegression 实例,并使用 .fit() 将两个数据集传递给它。这将为您执行实际的计算。默认情况下,它使用普通最小二乘法 (OLS) 来执行此操作。

创建并填充 LinearRegression 实例后,其 .score() 方法将计算 R 平方(或确定系数)值。这可以衡量最佳拟合线与实际值的接近程度。在您的分析中,R 平方值 0.79 表示最佳拟合线与实际值之间的准确度为 79%。您将其转换为名为 r_squared 的字符串,以便稍后进行绘图。您对整洁度值进行四舍五入。

要构建最佳拟合直线方程的字符串,您可以使用 LinearRegression 对象的 .coef_ 属性来获取其梯度及其 .intercept_ 属性来查找 y 截距。该方程存储在名为 best_fit 的变量中,以便您稍后可以绘制它。

要获取模型针对每个给定 x 值预测的各种 y 坐标,您可以使用模型的 .predict() 方法并将其传递给x 值。您将这些值存储在名为 y_pred 的变量中,以便稍后绘制线条。

最后,生成散点图:

In [5]: fig, ax = plt.subplots()
   ...: ax.scatter(x, y)
   ...: ax.plot(x, y_pred, color="red")
   ...: ax.text(7.25, 5.5, r_squared, fontsize=10)
   ...: ax.text(7.25, 7, best_fit, fontsize=10)
   ...: ax.set_title("Scatter Plot of Ratings")
   ...: ax.set_xlabel("Average IMDb Rating")
   ...: ax.set_ylabel("Average Rotten Tomatoes Rating")
   ...: fig.show()

前三行将最佳拟合线添加到散点图上。 text() 函数将 r_squaredbest_fit 放置在传递给它的坐标处,而 .plot() > 方法将最佳拟合线(红色)添加到散点图中。和以前一样,Jupyter Notebook 中不需要 fig.show()

所有这一切的 Jupyter Notebook 结果如下所示:

现在您已经完成了回归分析,您可以使用其方程来预测一个评级与另一个评级,准确度约为 79%。

研究统计分布

您的数据包括有关每部不同邦德电影的放映时间的信息。您的第二个目标要求您找出是否可以通过分析电影的长度来收集任何见解。为此,您将创建电影时间的条形图,并查看它是否揭示了任何有趣的内容:

In [6]: fig, ax = plt.subplots()
   ...: length = data["film_length"].value_counts(bins=7).sort_index()
   ...: length.plot.bar(
   ...:     ax=ax,
   ...:     title="Film Length Distribution",
   ...:     xlabel="Time Range (mins)",
   ...:     ylabel="Count",
   ...: )
   ...: fig.show()

这次,您使用 pandas 的绘图功能创建条形图。虽然它们不像 Matplotlib 那样广泛,但它们确实使用了 Matplotlib 的一些底层功能。您创建一个由数据 Film_Length 列中的数据组成的系列。然后,您可以使用 .value_counts() 创建一个包含每部电影长度计数的系列。最后,通过传入 bins=7 将它们分为七个范围。

创建系列后,您可以使用 .plot.bar() 快速绘制它。这允许您为绘图定义标题和轴标签,如图所示。结果图揭示了一个非常常见的统计分布:

从图中可以看出,电影长度类似于正态分布。平均电影时长在 122 分钟到 130 分钟之间,大约两个小时多一点。

请注意,在 Jupyter Notebook 中,fig, ax=plt.subplots()fig.show() 代码都不是必需的。某些环境可能需要它才能显示绘图。

如果您愿意,您可以找到更具体的统计值:

In [7]: data["film_length"].agg(["min", "max", "mean", "std"])
Out[7]:
min     106.000000
max     163.000000
mean    128.280000
std      12.940634
Name: film_length, dtype: float64

每个 pandas 数据系列都有一个有用的 .agg() 方法,允许您传入函数列表。然后将其中的每一个应用于系列中的数据。正如您所看到的,平均值确实在 122 到 130 分钟范围内。标准差很小,这意味着电影时间范围内没有太大的差异。最短和最长分别为 106 分钟和 163 分钟。

找不到任何关系

在这个最终分析中,您被要求调查电影的用户评分与邦德在其中实现的击杀数之间是否存在任何关系。

您决定按照与分析两个不同评级集之间的关系时类似的思路进行操作。您从散点图开始:

In [8]: fig, ax = plt.subplots()
   ...: ax.scatter(data["imdb"], data["bond_kills"])
   ...: ax.set_title("Scatter Plot of Kills vs Ratings")
   ...: ax.set_xlabel("Average IMDb Rating")
   ...: ax.set_ylabel("Kills by Bond")
   ...: fig.show()

该代码实际上与您在之前的散点图中使用的代码相同。您决定在分析中使用 IMDb 数据,但您也可以使用烂番茄数据。您已经确定两者之间存在密切关系,因此您选择哪个并不重要。

这次,当您绘制散点图时,它看起来像这样:

正如您所看到的,散点图显示数据是随机分布的。这表明电影收视率和邦德杀戮数量之间没有关系。无论受害者是被撞到瓦尔特 PPK 的错误一侧、被吸出飞机,还是被抛入太空,邦德影迷似乎并不太关心邦德消灭了多少坏人。

在分析数据时,重要的是要认识到您可能并不总能找到有用的东西。事实上,在进行数据分析时必须避免的陷阱之一是在分析数据之前将自己的偏见引入数据中,然后用它来证明你先入为主的结论。有时根本没有什么可得出结论的。

此时,您对自己的发现感到满意。是时候将它们传达给你的老板了。

传达您的发现

一旦数据建模完成并且您从中获得了有用的信息,下一阶段就是将您的发现传达给其他感兴趣的各方。毕竟,它们不仅仅仅供您参考。您可以使用报告或演示文稿来完成此操作。在陈述结论之前,您可能会讨论您的数据源和分析方法。结论背后有数据和方法论,这赋予了他们权威。

您可能会发现,一旦您展示了您的发现,就会出现需要未来分析的问题。再次,您可能需要设定额外的目标并完成整个工作流程来解决这些新问题。回顾一下该图,您会发现数据分析工作流程可能具有循环和迭代的性质。

在某些情况下,您可以重复使用您的分析方法。如果是这样,您可以考虑编写一些脚本来读取数据的未来版本,清理数据,并以与您刚才相同的方式分析数据。这将使未来的结果与您的结果进行比较,并增加您的工作的可扩展性。通过在未来重复您的分析,您可以监控您的原始发现,看看它们在未来数据面前的表现如何。

或者,您可能会发现方法中的缺陷,并且需要以不同的方式重新分析数据。同样,工作流程图也指出了这种可能性。

解决异常

在分析数据集时,您可能已经注意到其中一部詹姆斯·邦德电影丢失了。回头看看你是否能猜出是哪一个。你可以在下面揭晓答案,但不要偷看!另外,如果您运行 data["bond_actor"].value_counts(),您可能会惊讶地发现肖恩·康纳利 (Sean Connery) 只扮演过 6 次邦德,而罗杰·摩尔 (Roger Moore) 扮演过 7 次。或者他做到了?

您在本教程中使用的数据集不包括“Never Say Never Again”。这部电影不被认为是詹姆斯·邦德系列的正式组成部分。然而,肖恩·康纳利确实出演了该片的主角。因此从技术上来说,康纳利和摩尔都曾扮演过邦德007次。

就这样,你的使命完成了。 M非常高兴。作为奖励,他指示 Q 给你一支可以变成直升机的笔。始终是追踪未来数据进行分析的便捷工具。

结论

您现在已经获得了使用数据分析工作流程来分析一些数据并从您的发现中得出结论的经验。您了解数据分析工作流程的主要阶段以及遵循这些阶段的原因。当您将来学习更高级的分析技术时,您仍然可以使用在这里学到的关键技能来确保您未来的数据分析项目彻底、高效地进展。

在本教程中,您学习了:

  • 数据分析工作流程的重要性
  • 数据分析工作流程中主要阶段的目的
  • 清理数据的常用技术
  • 如何使用一些常见的数据分析方法来实现目标
  • 如何以图形方式显示数据分析结果

您应该考虑学习更多的数据分析技术并使用它们练习您的技能。如果您对此处使用的詹姆斯·邦德数据进行了任何进一步的分析,请随时在下面的评论部分分享您有趣的发现。事实上,尝试找到一些令人震惊的东西与我们分享。令人震惊。