Python 数值计算(四)
原文:
annas-archive.org/md5/9e81efca12eeaa9a42c1e05702b8c0c0译者:飞龙
第九章:添加交互性并制作动画图表
作为一本通过精心设计的示例讲解 Matplotlib 使用的书籍,我们选择推迟或简化内部机制的讨论。对于那些想要了解 Matplotlib 内部工作原理的读者,建议阅读 Duncan M. McGreggor 的 Mastering matplotlib。在我们 Matplotlib 的学习旅程中,讨论后端已不可避免,这些后端将绘图命令转化为图形。这些后端可以大致分为非交互式或交互式。我们将提供与每种后端类别相关的示例。
Matplotlib 并非一开始就作为动画包设计,因此在某些高级应用中可能会显得有些缓慢。对于以动画为中心的应用,PyGame 是一个非常好的替代方案(www.pygame.org);它支持 OpenGL 和 Direct3D 加速图形,在动画对象时提供极快的速度。然而,Matplotlib 在大多数时候的表现还是可以接受的,我们将指导你如何创建比静态图表更具吸引力的动画。
本章中的示例将基于失业率和按教育水平划分的收入数据(2016 年),这些数据来自data.gov并由美国劳工部劳工统计局整理。以下是本章的大纲:
-
从网站抓取信息
-
非交互式后端
-
交互式后端:Tkinter、Jupyter 和 Plot.ly
-
创建动画图表
-
将动画导出为视频
从网站抓取信息
全球各地的政府或辖区越来越重视开放数据的重要性,开放数据旨在增加公民参与,并为决策提供信息,使政策更加公开接受公众审议。全球一些开放数据计划的示例包括data.gov(美国)、data.gov.uk(英国)和data.gov.hk(香港)。
这些数据门户通常提供应用程序编程接口(API;有关更多详细信息,请参见第七章,在线数据可视化)以便编程访问数据。然而,一些数据集没有 API,因此我们需要使用经典的网页抓取技术从网站提取信息。
BeautifulSoup(www.crummy.com/software/BeautifulSoup/)是一个非常有用的工具包,用于从网站抓取信息。基本上,任何带有 HTML 标签的内容都可以用这个神奇的工具包抓取,从文本、链接、表格和样式,到图片。Scrapy 也是一个很好的网页抓取工具包,但它更像是一个编写强大网络爬虫的框架。因此,如果你只需要从页面获取一个表格,BeautifulSoup 提供了更简单的操作方法。
在本章节中,我们将一直使用 BeautifulSoup 版本 4.6。要安装 BeautifulSoup 4,我们可以再次依赖于 PyPI:
pip install beautifulsoup4
美国教育程度(2016 年)对应的失业率和收入数据可以在www.bls.gov/emp/ep_table_001.htm找到。目前,BeautifulSoup 无法处理 HTML 请求。因此,我们需要使用 urllib.request 或 requests 包来获取网页。在这两个选项中,requests 包可能更容易使用,因为它具有更高级别的 HTTP 客户端接口。如果您的系统上没有安装 requests,可以通过 PyPI 安装它:
pip install requests
在编写网页抓取代码之前,让我们先查看一下网页。如果我们使用 Google Chrome 浏览器访问劳工统计局网站,可以通过右键单击检查与我们需要的表格对应的 HTML 代码:
将显示一个用于代码检查的弹出窗口,允许我们阅读页面上每个元素的代码。
具体来说,我们可以看到列名在<thead>...</thead>部分中定义,而表格内容在<tbody>...</tbody>部分中定义。
为了指示 BeautifulSoup 抓取我们需要的信息,我们需要给它清晰的指示。我们可以右键单击代码检查窗口中的相关部分,并复制格式为 CSS 选择器的唯一标识符。
层叠样式表(CSS)选择器最初是为了将特定于元素的样式应用于网站而设计的。有关更多信息,请访问以下页面:
www.w3schools.com/cssref/css_selectors.asp。
让我们尝试获取thead和tbody的 CSS 选择器,并使用 BeautifulSoup.select() 方法来抓取相应的 HTML 代码:
import requests
from bs4 import BeautifulSoup
# Specify the url
url = "https://www.bls.gov/emp/ep_table_001.htm"
# Query the website and get the html response
response = requests.get(url)
# Parse the returned html using BeautifulSoup
bs = BeautifulSoup(response.text)
# Select the table header by CSS selector
thead = bs.select("#bodytext > table > thead")[0]
# Select the table body by CSS selector
tbody = bs.select("#bodytext > table > tbody")[0]
# Make sure the code works
print(thead)
我们从前面的代码中看到以下输出:
<thead> <tr> <th scope="col"><p align="center" valign="top"><strong>Educational attainment</strong></p></th> <th scope="col"><p align="center" valign="top">Unemployment rate (%)</p></th> <th scope="col"><p align="center" valign="top">Median usual weekly earnings ($)</p></th> </tr> </thead>
接下来,我们将查找<thead>...</thead>中所有<th>...</th>的实例,其中包含每列的名称。我们将构建一个以标题为键的列表字典来存储数据:
# Get the column names
headers = []
# Find all header columns in <thead> as specified by <th> html tags
for col in thead.find_all('th'):
headers.append(col.text.strip())
# Dictionary of lists for storing parsed data
data = {header:[] for header in headers}
最后,我们解析表格主体(<tbody>...</tbody>)中剩余的行(<tr>...</tr>),并将数据转换为 pandas DataFrame:
import pandas as pd
# Parse the rows in table body
for row in tbody.find_all('tr'):
# Find all columns in a row as specified by <th> or <td> html tags
cols = row.find_all(['th','td'])
# enumerate() allows us to loop over an iterable,
# and return each item preceded by a counter
for i, col in enumerate(cols):
# Strip white space around the text
value = col.text.strip()
# Try to convert the columns to float, except the first column
if i > 0:
value = float(value.replace(',','')) # Remove all commas in
# string
# Append the float number to the dict of lists
data[headers[i]].append(value)
# Create a dataframe from the parsed dictionary
df = pd.DataFrame(data)
# Show an excerpt of parsed data
df.head()
| 教育程度 | 中位数周收入(美元) | 失业率(%) | |
|---|---|---|---|
| 0 | 博士学位 | 1664.0 | 1.6 |
| 1 | 专业学位 | 1745.0 | 1.6 |
| 2 | 硕士学位 | 1380.0 | 2.4 |
| 3 | 学士学位 | 1156.0 | 2.7 |
| 4 | 专科学位 | 819.0 | 3.6 |
现在我们已经获取了 HTML 表格并将其格式化为结构化的 pandas DataFrame。
非交互式后端
绘制图形的代码在 Matplotlib 术语中被视为前端。我们在第六章中首次提到了后端,Hello Plotting World!,当时我们在谈论输出格式。实际上,Matplotlib 的后端在支持图形格式方面远不止差异如此简单。后端在幕后处理了许多事情!而这决定了绘图能力的支持。例如,LaTeX 文本布局仅在 AGG、PDF、PGF 和 PS 后端中得到支持。
我们至今一直在使用非交互式后端,包括 AGG、Cairo、GDK、PDF、PGF、PS 和 SVG。大多数这些后端不需要额外的依赖项,但 Cairo 和 GDK 分别需要 Cairo 图形库或 GIMP 绘图工具包才能工作。
非交互式后端可以进一步分为两类——向量和光栅。向量图形通过点、路径和形状来描述图像,这些点、路径和形状是通过数学公式计算得出的。无论缩放与否,向量图形总是显得平滑,其大小通常也远小于对应的光栅图形。PDF、PGF、PS 和 SVG 后端属于“向量”类别。
光栅图形通过有限数量的小色块(像素)来描述图像。因此,如果我们足够放大,我们会开始看到模糊的图像,换句话说,就是像素化。通过增加图像的分辨率或每英寸点数(DPI),我们不太可能观察到像素化。AGG、Cairo 和 GDK 属于这一组后端。下表总结了非交互式后端的主要功能和区别:
| 后端 | 向量/光栅 | 输出格式 |
|---|---|---|
| Agg | 光栅 | PNG |
| Cairo | 向量/光栅 | PDF、PNG、PS 或 SVG |
| 向量 | ||
| PGF | 向量 | PDF 或 PGF |
| PS | 向量 | PS |
| SVG | 向量 | SVG |
| GDK(在 Matplotlib 2.0 中已弃用) | 光栅 | PNG、JPEG 或 TIFF |
通常,我们无需手动选择后端,因为默认选择在大多数任务中都能很好地工作。另一方面,我们可以通过在导入matplotlib.pyplot之前使用matplotlib.use()方法来指定后端:
import matplotlib
matplotlib.use('SVG') # Change to SVG backend
import matplotlib.pyplot as plt
import textwrap # Standard library for text wraping
# Create a figure
fig, ax = plt.subplots(figsize=(6,7))
# Create a list of x ticks positions
ind = range(df.shape[0])
# Plot a bar chart of median usual weekly earnings by educational
# attainments
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
# Set the x-axis label
ax.set_xlabel('Median weekly earnings (USD)')
# Label the x ticks
# The tick labels are a bit too long, let's wrap them in 15-char lines
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
# Give extra margin at the bottom to display the tick labels
fig.subplots_adjust(left=0.3)
# Save the figure in SVG format
plt.savefig("test.svg")
交互式后端
Matplotlib 可以构建更具互动性的图形,这对于读者更具吸引力。有时,图形可能被图形元素所淹没,使得难以分辨单个数据点。在其他情况下,一些数据点可能看起来非常相似,以至于肉眼很难分辨差异。交互式图形可以通过让我们进行放大、缩小、平移和按需探索图形来解决这两种情况。
通过使用交互式后端,Matplotlib 中的图形可以嵌入到图形用户界面(GUI)应用程序中。默认情况下,Matplotlib 支持将 Agg 光栅图形渲染器与多种 GUI 工具包配对,包括 wxWidgets(Wx)、GIMP 工具包(GTK+)、Qt 和 Tkinter(Tk)。由于 Tkinter 是 Python 的事实标准 GUI,它建立在 Tcl/Tk 之上,因此我们可以通过在独立的 Python 脚本中调用plt.show()来创建交互式图形。
基于 Tkinter 的后端
让我们尝试将以下代码复制到一个单独的文本文件中,并命名为chapter6_gui.py。之后,在终端(Mac/Linux)或命令提示符(Windows)中输入python chapter6_gui.py。如果你不确定如何打开终端或命令提示符,请参阅第六章,Hello Plotting World!,以获取更多详情:
import matplotlib
import matplotlib.pyplot as plt
import textwrap # Standard library for text wraping
import requests
import pandas as pd
from bs4 import BeautifulSoup
# Specify the url
url = "https://www.bls.gov/emp/ep_table_001.htm"
# Query the website and get the html response
response = requests.get(url)
# Parse the returned html using BeautifulSoup
bs = BeautifulSoup(response.text)
# Select the table header by CSS selector
thead = bs.select("#bodytext > table > thead")[0]
# Select the table body by CSS selector
tbody = bs.select("#bodytext > table > tbody")[0]
# Get the column names
headers = []
# Find all header columns in <thead> as specified by <th> html tags
for col in thead.find_all('th'):
headers.append(col.text.strip())
# Dictionary of lists for storing parsed data
data = {header:[] for header in headers}
# Parse the rows in table body
for row in tbody.find_all('tr'):
# Find all columns in a row as specified by <th> or <td> html tags
cols = row.find_all(['th','td'])
# enumerate() allows us to loop over an iterable,
# and return each item preceded by a counter
for i, col in enumerate(cols):
# Strip white space around the text
value = col.text.strip()
# Try to convert the columns to float, except the first column
if i > 0:
value = float(value.replace(',','')) # Remove all commas in
# string
# Append the float number to the dict of lists
data[headers[i]].append(value)
# Create a dataframe from the parsed dictionary
df = pd.DataFrame(data)
# Create a figure
fig, ax = plt.subplots(figsize=(6,7))
# Create a list of x ticks positions
ind = range(df.shape[0])
# Plot a bar chart of median usual weekly earnings by educational
# attainments
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
# Set the x-axis label
ax.set_xlabel('Median weekly earnings (USD)')
# Label the x ticks
# The tick labels are a bit too long, let's wrap them in 15-char lines
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
# Give extra margin at the bottom to display the tick labels
fig.subplots_adjust(left=0.3)
# Show the figure in a GUI
plt.show()
我们看到一个类似于下图的弹出窗口。我们可以通过点击底部工具栏上的按钮来平移、缩放选择区域、配置子图边距、保存,并在不同视图之间来回切换。如果将鼠标悬停在图形上,我们还可以在右下角查看精确的坐标。这一功能对于分析彼此接近的数据点非常有用。
接下来,我们将通过在图形上方添加一个单选按钮小部件来扩展应用程序,使我们可以在显示每周收入或失业率之间切换。该单选按钮可以在matplotlib.widgets中找到,我们将把一个数据更新函数附加到按钮的.on_clicked()事件上。你可以将以下代码粘贴到前面代码示例(chapter6_gui.py)中的plt.show()行之前。让我们看看它是如何工作的:
# Import Matplotlib radio button widget
from matplotlib.widgets import RadioButtons
# Create axes for holding the radio selectors.
# supply [left, bottom, width, height] in normalized (0, 1) units
bax = plt.axes([0.3, 0.9, 0.4, 0.1])
radio = RadioButtons(bax, ('Weekly earnings', 'Unemployment rate'))
# Define the function for updating the displayed values
# when the radio button is clicked
def radiofunc(label):
# Select columns from dataframe, and change axis label depending on
# selection
if label == 'Weekly earnings':
data = df["Median usual weekly earnings ($)"]
ax.set_xlabel('Median weekly earnings (USD)')
elif label == 'Unemployment rate':
data = df["Unemployment rate (%)"]
ax.set_xlabel('Unemployment rate (%)')
# Update the bar heights
for i, rect in enumerate(rects):
rect.set_width(data[i])
# Rescale the x-axis range
ax.set_xlim(xmin=0, xmax=data.max()*1.1)
# Redraw the figure
plt.draw()
# Attach radiofunc to the on_clicked event of the radio button
radio.on_clicked(radiofunc)
你将看到图形上方出现一个新的单选框。尝试在两个状态之间切换,看看图形是否会相应更新。完整的代码也可以在我们的代码库中找到,名为chapter6_tkinter.py。
Jupyter Notebook 的交互式后端
在我们结束本节之前,我们将介绍两种书籍中很少涉及的交互式后端。从 Matplotlib 1.4 开始,专门为 Jupyter Notebook 设计了一个交互式后端。为了启用该功能,我们只需在笔记本的开始部分粘贴%matplotlib notebook。我们将改编本章前面的一个示例来使用这个后端:
# Import the interactive backend for Jupyter notebook
%matplotlib notebook
import matplotlib
import matplotlib.pyplot as plt
import textwrap
fig, ax = plt.subplots(figsize=(6,7))
ind = range(df.shape[0])
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
ax.set_xlabel('Median weekly earnings (USD)')
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
fig.subplots_adjust(left=0.3)
# Show the figure using interactive notebook backend
plt.show()
你将看到一个交互式界面,按钮类似于基于 Tkinter 的应用程序:
基于 Plot.ly 的后端
最后,我们将讨论 Plot.ly,这是一个基于 D3.js 的交互式绘图库,支持多种编程语言,包括 Python。由于其强大的数据仪表盘、高性能以及详细的文档,Plot.ly 在在线数据分析领域迅速获得了关注。欲了解更多信息,请访问 Plot.ly 的网站(plot.ly)。
Plot.ly 通过其 Python 绑定,提供了将 Matplotlib 图形轻松转换为在线交互式图表的功能。要安装 Plotly.py,我们可以使用 PyPI:
pip install plotly
让我们通过一个快速示例向你展示如何将 Matplotlib 与 Plot.ly 集成:
import matplotlib.pyplot as plt
import numpy as np
import plotly.plotly as py
from plotly.offline import init_notebook_mode, enable_mpl_offline, iplot_mpl
# Plot offline in Jupyter Notebooks, not required for standalone script
# Note: Must be called before any plotting actions
init_notebook_mode()
# Convert mpl plots to locally hosted HTML documents, not required if you
# are a registered plot.ly user and have a API key
enable_mpl_offline()
# Create two subplots with shared x-axis
fig, axarr = plt.subplots(2, sharex=True)
# The code for generating "df" is skipped for brevity, please refer to the
# "Tkinter-based backend" section for details of generating "df"
ind = np.arange(df.shape[0]) # the x locations for the groups
width = 0.35
# Plot a bar chart of the weekly earnings in the first axes
axarr[0].bar(ind, df["Median usual weekly earnings ($)"], width)
# Plot a bar chart of the unemployment rate in the second axes
axarr[1].bar(ind, df["Unemployment rate (%)"], width)
# Set the ticks and labels
axarr[1].set_xticks(ind)
# Reduce verbosity of labels by removing " degree"
axarr[1].set_xticklabels([value.replace(" degree","") for value in df["Educational attainment"]])
# Offline Interactive plot using plot.ly
# Note: import and use plotly.offline.plot_mpl instead for standalone
# Python scripts
iplot_mpl(fig)
运行前面的 Plot.ly 示例时,可能会看到以下错误消息:
IOPub data rate exceeded. The notebook server will temporarily stop sending output to the client in order to avoid crashing it.
To change this limit, set the config variable
--NotebookApp.iopub_data_rate_limit.
若要绕过此错误,可以通过设置更高的 iopub_data_rate_limit 重新启动 Jupyter Notebook:
jupyter notebook --NotebookApp.iopub_data_rate_limit=1.0e10
你可能还注意到,尽管代码中明确指定了刻度标签,但它们无法正确显示。这个问题也在官方 GitHub 页面上有报告(github.com/plotly/plotly.py/issues/735)。遗憾的是,目前尚未修复这个问题。
我们承认,网上有大量材料描述了如何将 Matplotlib 图形集成到不同的 GUI 应用程序中。由于篇幅限制,我们不会在这里逐一介绍这些后端。对于想要深入了解这些交互式后端的读者,Alexandre Devert 在 matplotlib Plotting Cookbook 中写了一个精彩的章节(第八章,用户界面)。在那本书的 第八章,用户界面 中,Alexandre 还提供了使用 wxWidgets、GTK 和 Pyglet 创建 GUI 应用程序的方案。
创建动画图形
正如本章开头所解释的那样,Matplotlib 最初并不是为了制作动画而设计的,而且有些 GPU 加速的 Python 动画包可能更适合这一任务(例如 PyGame)。然而,由于我们已经熟悉 Matplotlib,因此将现有的图形转化为动画非常简单。
安装 FFmpeg
在开始制作动画之前,我们需要在系统中安装 FFmpeg、avconv、MEncoder 或 ImageMagick。这些额外的依赖项并未与 Matplotlib 一起打包,因此需要单独安装。我们将带你一步步安装 FFmpeg。
对于基于 Debian 的 Linux 用户,可以在终端中输入以下命令来安装 FFmpeg:
sudo apt-get install ffmpeg
在 Ubuntu 14.04 或更早版本中,可能无法安装 FFmpeg。要在 Ubuntu 14.04 上安装 FFmpeg,请按照以下步骤操作:
**sudo add-apt-repository ppa:mc3man/trusty-media**
按 Enter 确认添加仓库。
**还需要注意,在初始设置和某些包升级时,使用 apt-get 时需要执行 sudo apt-get dist-upgrade**
**更多信息:https://launchpad.net/~mc3man/+archive/ubuntu/trusty-media**
**按 [ENTER] 继续或按 ctrl-c 取消添加**
在安装 FFmpeg 之前,更新并升级一些软件包。
**sudo apt-get update**
**sudo apt-get dist-upgrade**
最后,按照正常程序通过 apt-get 安装 FFmpeg:
**sudo apt-get install ffmpeg**
对于 Mac 用户,Homebrew(brew.sh/)是搜索和安装 FFmpeg 包的最简单方法。对于没有 Homebrew 的用户,你可以将以下代码粘贴到终端中进行安装:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
之后,我们可以通过在终端应用中执行以下命令来安装 FFmpeg:
brew install ffmpeg
另外,你也可以通过将二进制文件(evermeet.cx/ffmpeg/)复制到系统路径中(例如,/usr/local/bin)来安装 FFmpeg。读者可以访问以下页面以获取更多细节:www.renevolution.com/ffmpeg/2013/03/16/how-to-install-ffmpeg-on-mac-os-x.html
Windows 用户的安装步骤要复杂一些,因为我们需要自己下载可执行文件,然后将其添加到系统路径中。因此,我们准备了一系列屏幕截图来引导你完成整个过程。
首先,我们需要从 ffmpeg.zeranoe.com/builds/ 获取一个预构建的二进制文件。选择与你的系统相匹配的 CPU 架构,并选择最新版本和静态链接库。
接下来,我们需要将下载的 ZIP 文件解压到 C 盘,路径为 c:\ffmpeg,并将文件夹 c:\ffmpeg\bin 添加到 Path 变量中。为此,请进入控制面板,点击“系统和安全”链接,然后点击“系统”。在系统窗口中,点击左侧的“高级系统设置”链接:
在弹出的系统属性窗口中,点击“环境变量…”按钮:
选择 Path 条目,点击“编辑…”按钮:
在“编辑环境变量”窗口中,创建一个新条目,显示 c:\ffmpeg\bin。点击所有弹出窗口中的“确定”以保存更改。重新启动命令提示符和 Jupyter Notebook,之后你就可以开始使用了。
访问 Wikihow(www.wikihow.com/Install-FFmpeg-on-Windows)获取有关如何在 Windows 7 上安装 FFmpeg 的说明。
创建动画
Matplotlib 提供了两种创建动画的主要接口:TimedAnimation和FuncAnimation。TimedAnimation适用于创建基于时间的动画,而FuncAnimation则可以根据自定义函数创建动画。考虑到FuncAnimation提供了更高的灵活性,我们将在本节中仅探索FuncAnimation的使用。读者可以参考官方文档(matplotlib.org/api/animation_api.html)了解更多关于TimedAnimation的信息。
FuncAnimation通过反复调用一个函数来改变每一帧中 Matplotlib 对象的属性。在以下示例中,我们通过假设年增长率为 5%来模拟中位数周薪的变化。我们将创建一个自定义函数--animate--它返回在每一帧中发生变化的 Matplotlib 艺术对象。这个函数将与其他一些额外参数一起传递给animation.FuncAnimation():
import textwrap
import matplotlib.pyplot as plt
import random
# Matplotlib animation module
from matplotlib import animation
# Used for generating HTML video embed code
from IPython.display import HTML
# Adapted from previous example, codes that are modified are commented
fig, ax = plt.subplots(figsize=(6,7))
ind = range(df.shape[0])
rects = ax.barh(ind, df["Median usual weekly earnings ($)"], height=0.5)
ax.set_xlabel('Median weekly earnings (USD)')
ylabels=[textwrap.fill(label,15) for label in df["Educational attainment"]]
ax.set_yticks(ind)
ax.set_yticklabels(ylabels)
fig.subplots_adjust(left=0.3)
# Change the x-axis range
ax.set_xlim(0,7600)
# Add a text annotation to show the current year
title = ax.text(0.5,1.05, "Median weekly earnings (USD) in 2016",
bbox={'facecolor':'w', 'alpha':0.5, 'pad':5},
transform=ax.transAxes, ha="center")
# Animation related stuff
n=30 #Number of frames
# Function for animating Matplotlib objects
def animate(frame):
# Simulate 5% annual pay rise
data = df["Median usual weekly earnings ($)"] * (1.05 ** frame)
# Update the bar heights
for i, rect in enumerate(rects):
rect.set_width(data[i])
# Update the title
title.set_text("Median weekly earnings (USD) in {}".format(2016+frame))
return rects, title
# Call the animator. Re-draw only the changed parts when blit=True.
# Redraw all elements when blit=False
anim=animation.FuncAnimation(fig, animate, blit=False, frames=n)
# Save the animation in MPEG-4 format
anim.save('test.mp4')
# OR--Embed the video in Jupyter notebook
HTML(anim.to_html5_video())
这是其中一个视频帧的屏幕截图:
在这个示例中,我们将动画输出为 MPEG-4 编码的视频格式。视频也可以以 H.264 编码的视频格式嵌入到 Jupyter Notebook 中。你只需要调用Animation.to_html5_video()方法,并将返回的对象传递给IPython.display.HTML。视频编码和 HTML5 代码生成将在后台自动完成。
总结
在本章中,你通过使用 BeautifulSoup 网页抓取库进一步丰富了获取在线数据的技巧。你成功地学习了创建交互式图形和动画的不同方法。这些技巧将为你在更高级的应用中创建直观且引人入胜的可视化铺平道路。
第十章:选择数据的子集
Series 或 DataFrame 中的每个数据维度都通过一个索引对象进行标记。正是这个索引将 pandas 数据结构与 NumPy 的 n 维数组区分开来。索引为每一行和每一列的数据提供有意义的标签,pandas 用户可以通过使用这些标签来选择数据。此外,pandas 还允许用户通过行列的整数位置来选择数据。这种同时支持标签选择和整数位置选择的功能,使得选择数据子集的语法既强大又令人困惑。
通过标签或整数位置选择数据并非 pandas 独有。Python 字典和列表是内建的数据结构,它们的选择方式正好是这两种方式之一。字典和列表都有明确的使用说明,并且其索引运算符的使用场景有限。字典的键(即标签)必须是不可变对象,例如字符串、整数或元组。列表必须使用整数或切片对象来进行选择。字典只能通过传递键来一次选择一个对象。从某种意义上讲,pandas 将使用整数选择数据的能力(类似于列表)和使用标签选择数据的能力(类似于字典)结合在一起。
在本章中,我们将讨论以下主题:
-
选择 Series 数据
-
选择 DataFrame 的行
-
同时选择 DataFrame 的行和列
-
使用整数和标签选择数据
-
加速标量选择
-
懒惰地切片行
-
按字典序切片
选择 Series 数据
Series 和 DataFrame 是复杂的数据容器,具有多个属性,使用索引运算符可以以不同的方式选择数据。除了索引运算符本身,.iloc 和 .loc 属性也可以使用索引运算符以各自独特的方式进行数据选择。统称这些属性为索引器。
索引术语可能会让人感到困惑。这里使用索引运算符一词,是为了与其他索引器区分开来。它指的是 Series 或 DataFrame 后面的括号 []。例如,给定一个 Series s,你可以通过以下方式选择数据:s[item] 和 s.loc[item]。第一个使用的是索引运算符,第二个使用的是 .loc 索引器。
Series 和 DataFrame 的索引器允许通过整数位置(类似于 Python 列表)和标签(类似于 Python 字典)进行选择。.iloc 索引器仅按整数位置进行选择,使用方式类似于 Python 列表。.loc 索引器仅按索引标签进行选择,类似于 Python 字典的工作方式。
准备就绪
.loc 和 .iloc 都可以与 Series 和 DataFrame 一起使用。这个示例展示了如何使用 .iloc 按整数位置选择 Series 数据,以及如何使用 .loc 按标签选择数据。这些索引器不仅可以接受标量值,还可以接受列表和切片。
如何操作...
- 使用学校数据集,设定机构名称为索引,并使用索引操作符选择单一列作为 Series:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> city = college['CITY']
>>> city.head()
INSTNM
Alabama A & M University Normal
University of Alabama at Birmingham Birmingham
Amridge University Montgomery
University of Alabama in Huntsville Huntsville
Alabama State University Montgomery
Name: CITY, dtype: object
.iloc索引器仅通过整数位置进行选择。传递一个整数给它将返回一个标量值:
>>> city.iloc[3]
Huntsville
- 要选择多个不同的整数位置,将一个列表传递给
.iloc。这将返回一个 Series:
>>> city.iloc[[10,20,30]]
INSTNM
Birmingham Southern College Birmingham
George C Wallace State Community College-Hanceville Hanceville
Judson College Marion
Name: CITY, dtype: object
- 要选择一个等距分布的数据分区,使用切片符号:
>>> city.iloc[4:50:10]
INSTNM
Alabama State University Montgomery
Enterprise State Community College Enterprise
Heritage Christian University Florence
Marion Military Institute Marion
Reid State Technical College Evergreen
Name: CITY, dtype: object
- 现在我们转向
.loc索引器,它仅通过索引标签进行选择。传递一个字符串将返回一个标量值:
>>> city.loc['Heritage Christian University']
Florence
- 要选择多个不连续的标签,使用列表:
>>> np.random.seed(1)
>>> labels = list(np.random.choice(city.index, 4))
>>> labels
['Northwest HVAC/R Training Center',
'California State University-Dominguez Hills',
'Lower Columbia College',
'Southwest Acupuncture College-Boulder']
>>> city.loc[labels]
INSTNM
Northwest HVAC/R Training Center Spokane
California State University-Dominguez Hills Carson
Lower Columbia College Longview
Southwest Acupuncture College-Boulder Boulder
Name: CITY, dtype: object
- 要选择一个等距分布的数据分区,使用切片符号。确保
start和stop值为字符串。可以使用整数来指定切片的步长:
>>> city.loc['Alabama State University':
'Reid State Technical College':10]
INSTNM
Alabama State University Montgomery
Enterprise State Community College Enterprise
Heritage Christian University Florence
Marion Military Institute Marion
Reid State Technical College Evergreen
Name: CITY, dtype: object
工作原理...
Series 中的值通过从 0 开始的整数引用。第 2 步使用 .loc 索引器选择 Series 的第四个元素。第 3 步将一个包含三个整数的列表传递给索引操作符,返回一个选择了这些整数位置的 Series。这个功能是对 Python 列表的一种增强,因为 Python 列表无法以这种方式选择多个不连续的项。
在第 4 步中,使用 start、stop 和 step 指定的切片符号选择 Series 的整个部分。
第 5 步到第 7 步是使用基于标签的索引器 .loc 重复第 2 步到第 4 步的操作。标签必须与索引中的值完全匹配。为了确保标签完全匹配,我们在第 6 步从索引中随机选择四个标签,并将它们存储到一个列表中,然后选择它们的值作为 Series。使用 .loc 索引器的选择总是包含最后一个元素,正如第 7 步所示。
还有更多内容...
当将标量值传递给索引操作符时,如第 2 步和第 5 步所示,将返回一个标量值。当传递一个列表或切片时,如其他步骤所示,将返回一个 Series。这个返回值可能看起来不一致,但如果我们将 Series 看作一个类似字典的对象,它将标签映射到值,那么返回值就有意义了。为了选择一个单独的项目并保留它在 Series 中,传递一个单项列表而不是标量值:
>>> city.iloc[[3]]
INSTNM
University of Alabama in Huntsville Huntsville
Name: CITY, dtype: object
使用 .loc 时需要小心切片符号。如果 start 索引出现在 stop 索引之后,那么会返回一个空的 Series,且不会抛出异常:
>>> city.loc['Reid State Technical College':
'Alabama State University':10]
Series([], Name: CITY, dtype: object)
参见
- Pandas 官方文档关于索引的内容(
bit.ly/2fdtZWu)
选择 DataFrame 行
选择 DataFrame 行的最明确和推荐的方式是使用 .iloc 和 .loc 索引器。它们能够独立且同时选择行或列。
准备工作
这个教程展示了如何使用 .iloc 和 .loc 索引器从 DataFrame 中选择行。
操作步骤...
- 读取学校数据集,并设置索引为机构名称:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college.head()
- 向
.iloc索引器传递一个整数,选择该位置的整行:
>>> college.iloc[60]
CITY Anchorage
STABBR AK
HBCU 0
...
UG25ABV 0.4386
MD_EARN_WNE_P10 42500
GRAD_DEBT_MDN_SUPP 19449.5
Name: University of Alaska Anchorage, Length: 26, dtype: object
- 要获取与前一步相同的行,请将索引标签传递给
.loc索引器:
>>> college.loc['University of Alaska Anchorage']
CITY Anchorage
STABBR AK
HBCU 0
...
UG25ABV 0.4386
MD_EARN_WNE_P10 42500
GRAD_DEBT_MDN_SUPP 19449.5
Name: University of Alaska Anchorage, Length: 26, dtype: object
- 若要选择不连续的行作为 DataFrame,可以将整数列表传递给
.iloc索引器:
>>> college.iloc[[60, 99, 3]]
- 可以通过将精确的机构名称列表传递给
.loc,重新生成第 4 步中的同一 DataFrame:
>>> labels = ['University of Alaska Anchorage',
'International Academy of Hair Design',
'University of Alabama in Huntsville']
>>> college.loc[labels]
- 使用
.iloc的切片表示法选择数据的整个区段:
>>> college.iloc[99:102]
- 切片表示法也适用于
.loc索引器,并且包括最后一个标签:
>>> start = 'International Academy of Hair Design'
>>> stop = 'Mesa Community College'
>>> college.loc[start:stop]
工作原理...
将标量值、标量列表或切片对象传递给 .iloc 或 .loc 索引器,会导致 pandas 扫描索引标签,返回相应的行。如果传递的是单一的标量值,则返回一个 Series。如果传递的是列表或切片对象,则返回一个 DataFrame。
还有更多内容...
在第 5 步中,可以直接从第 4 步返回的 DataFrame 中选择索引标签列表,而无需复制和粘贴:
>>> college.iloc[[60, 99, 3]].index.tolist()
['University of Alaska Anchorage',
'International Academy of Hair Design',
'University of Alabama in Huntsville']
另见
- 请参考 第十二章中的 检查 Index 对象,索引对齐。
同时选择 DataFrame 的行和列
直接使用索引操作符是选择一个或多个列的正确方法。然而,它不允许同时选择行和列。要同时选择行和列,你需要通过逗号分隔有效的行选择和列选择,传递给 .iloc 或 .loc 索引器。
准备工作
选择行和列的通用形式如下所示:
>>> df.iloc[rows, columns]
>>> df.loc[rows, columns]
rows 和 columns 变量可以是标量值、列表、切片对象或布尔序列。
向索引器传递布尔序列的内容,请参阅 第十一章,布尔索引。
在本步骤中,每个步骤展示了使用 .iloc 同时选择行和列,并使用 .loc 完全复现这一操作。
如何操作...
- 读取大学数据集,并将索引设置为机构名称。使用切片表示法选择前 3 行和前 4 列:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college.iloc[:3, :4]
>>> college.loc[:'Amridge University', :'MENONLY']
- 选择两个不同列的所有行:
>>> college.iloc[:, [4,6]].head()
>>> college.loc[:, ['WOMENONLY', 'SATVRMID']].head()
- 选择不连续的行和列:
>>> college.iloc[[100, 200], [7, 15]]
>>> rows = ['GateWay Community College',
'American Baptist Seminary of the West']
>>> columns = ['SATMTMID', 'UGDS_NHPI']
>>> college.loc[rows, columns]
- 选择一个单一的标量值:
>>> college.iloc[5, -4]
>>> college.loc['The University of Alabama', 'PCTFLOAN']
-.401
- 切片行并选择单列:
>>> college.iloc[90:80:-2, 5]
>>> start = 'Empire Beauty School-Flagstaff'
>>> stop = 'Arizona State University-Tempe'
>>> college.loc[start:stop:-2, 'RELAFFIL']
INSTNM
Empire Beauty School-Flagstaff 0
Charles of Italy Beauty College 0
Central Arizona College 0
University of Arizona 0
Arizona State University-Tempe 0
Name: RELAFFIL, dtype: int64
工作原理...
同时选择行和列的一个关键是理解括号中逗号的使用。逗号左边的选择始终基于行索引选择行,逗号右边的选择始终基于列索引选择列。
并不一定需要同时选择行和列。第 2 步展示了如何选择所有行以及部分列。冒号表示一个切片对象,它只是返回该维度的所有值。
还有更多内容...
当选择部分行,同时包含所有列时,不需要在逗号后面使用冒号。如果没有逗号,默认行为是选择所有列。前面的例子就是以这种方式选择行的。你当然可以使用冒号来表示所有列的切片。以下代码是等效的:
>>> college.iloc[:10]
>>> college.iloc[:10, :]
使用整数和标签同时选择数据
.iloc和.loc索引器各自通过整数或标签位置选择数据,但不能同时处理这两种输入类型的组合。在早期版本的 pandas 中,曾经有一个索引器.ix,它可以通过整数和标签位置同时选择数据。尽管这个功能在特定情况下很方便,但它本质上存在歧义,给许多 pandas 用户带来了困惑。因此,.ix索引器已被弃用,应避免使用。
准备工作
在.ix被弃用之前,可以使用college.ix[:5, 'UGDS_WHITE':'UGDS_UNKN']来选择大学数据集中从UGDS_WHITE到UGDS_UNKN的前五行及所有列。现在,无法直接使用.loc或.iloc来做到这一点。以下示例展示了如何查找列的整数位置,然后使用.iloc完成选择。
如何操作...
- 读取大学数据集并将机构名称(
INSTNM)指定为索引:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
- 使用索引方法
get_loc找到所需列的整数位置:
>>> col_start = college.columns.get_loc('UGDS_WHITE')
>>> col_end = college.columns.get_loc('UGDS_UNKN') + 1
>>> col_start, col_end
- 使用
col_start和col_end通过整数位置选择列,使用.iloc:
>>> college.iloc[:5, col_start:col_end]
工作原理...
第 2 步首先通过columns属性检索列的索引。索引有一个get_loc方法,它接受一个索引标签并返回该标签的整数位置。我们找到希望切片的列的起始和结束整数位置。因为.iloc的切片是不包括最后一个项的,所以需要加 1。第 3 步使用切片表示法处理行和列。
还有更多内容...
我们可以进行类似的操作,使.loc也能处理整数和位置的混合。以下示例展示了如何选择第 10 到第 15 行(包含),以及UGDS_WHITE到UGDS_UNKN的列:
>>> row_start = df_college.index[10]
>>> row_end = df_college.index[15]
>>> college.loc[row_start:row_end, 'UGDS_WHITE':'UGDS_UNKN']
使用.ix(已弃用,因此不推荐这样做)执行相同操作的方式如下:
>>> college.ix[10:16, 'UGDS_WHITE':'UGDS_UNKN']
通过将.loc和.iloc链式调用,也能实现相同的结果,但通常不建议链式调用索引器:
>>> college.iloc[10:16].loc[:, 'UGDS_WHITE':'UGDS_UNKN']
提高标量选择的速度
.iloc和.loc索引器都能够从 Series 或 DataFrame 中选择单个元素,即标量值。然而,也存在.iat和.at索引器,它们分别以更快的速度完成相同的任务。像.iloc一样,.iat索引器使用整数位置来进行选择,必须传递两个以逗号分隔的整数。类似于.loc,.at索引器使用标签进行选择,必须传递一个索引和一个列标签,以逗号分隔。
准备工作
如果计算时间至关重要,这个例子是非常有价值的。它展示了在进行标量选择时,.iat和.at相较于.iloc和.loc的性能提升。
如何实现...
- 读取
college成绩单数据集,将机构名称作为索引。将大学名称和列名传递给.loc以选择标量值:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> cn = 'Texas A & M University-College Station'
>>> college.loc[cn, 'UGDS_WHITE']
.661
- 使用
.at实现相同的结果:
>>> college.at[cn, 'UGDS_WHITE']
.661
- 使用
%timeit魔法命令来找出速度差异:
>>> %timeit college.loc[cn, 'UGDS_WHITE']
8.97 µs ± 617 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
>>> %timeit college.at[cn, 'UGDS_WHITE']
6.28 µs ± 214 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
- 查找前述选择的整数位置,然后计时
.iloc和.iat之间的差异:
>>> row_num = college.index.get_loc(cn)
>>> col_num = college.columns.get_loc('UGDS_WHITE')
>>> row_num, col_num
(3765, 10)
>>> %timeit college.iloc[row_num, col_num]
9.74 µs ± 153 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
>>> %timeit college.iat[row_num, col_num]
7.29 µs ± 431 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
它是如何工作的……
标量索引器.iat和.at只接受标量值。如果传递任何其他值,它们将失败。它们在进行标量选择时是.iloc和.loc的替代品。timeit魔法命令可以在前面加上两个百分号时计时整个代码块,加上一个百分号时计时单独的部分。它显示,通过切换到标量索引器,平均节省大约 2.5 微秒。虽然这可能不多,但如果在程序中重复进行标量选择,节省的时间会快速积累。
还有更多……
.iat和.at同样适用于 Series。将一个标量值传递给它们,它们将返回一个标量:
>>> state = college['STABBR'] # Select a Series
>>> state.iat[1000]
'IL'
>>> state.at['Stanford University']
'CA'
懒惰地切片行
本章前面的示例展示了如何使用.iloc和.loc索引器在任一维度中选择 Series 和 DataFrame 的子集。使用单一索引操作符可以实现选择行的快捷方式。这只是一个展示 pandas 附加功能的快捷方式,但索引操作符的主要功能实际上是选择 DataFrame 列。如果你想选择行,最好使用.iloc或.loc,因为它们没有歧义。
准备工作
在这个例子中,我们将切片对象传递给 Series 和 DataFrame 的索引操作符。
如何实现...
- 读取包含机构名称作为索引的大学数据集,然后从索引 10 到 20 选择每隔一行:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college[10:20:2]
- 在 Series 中也有相同的切片操作:
>>> city = college['CITY']
>>> city[10:20:2]
INSTNM
Birmingham Southern College Birmingham
Concordia College Alabama Selma
Enterprise State Community College Enterprise
Faulkner University Montgomery
New Beginning College of Cosmetology Albertville
Name: CITY, dtype: object
- Series 和 DataFrame 也可以通过标签进行切片,只需使用索引操作符:
>>> start = 'Mesa Community College'
>>> stop = 'Spokane Community College'
>>> college[start:stop:1500]
- 这里是使用 Series 的相同标签切片:
>>> city[start:stop:1500]
INSTNM
Mesa Community College Mesa
Hair Academy Inc-New Carrollton New Carrollton
National College of Natural Medicine Portland
Name: CITY, dtype: object
它是如何工作的……
索引操作符的行为会根据传递给它的对象类型而改变。以下伪代码概述了 DataFrame 索引操作符如何处理传递给它的对象:
>>> df[item] # Where `df` is a DataFrame and item is some object
If item is a string then
Find a column name that matches the item exactly
Raise KeyError if there is no match
Return the column as a Series
If item is a list of strings then
Raise KeyError if one or more strings in item don't match columns
Return a DataFrame with just the columns in the list
If item is a slice object then
Works with either integer or string slices
Raise KeyError if label from label slice is not in index
Return all ROWS that are selected by the slice
If item is a list, Series or ndarray of booleans then
Raise ValueError if length of item not equal to length of DataFrame
Use the booleans to return only the rows with True in same location
前述的逻辑涵盖了所有最常见的情况,但并不是一个详尽无遗的列表。对于 Series,逻辑稍微不同,实际上比 DataFrame 更复杂。由于其复杂性,最好避免仅仅在 Series 上使用索引操作符,而是使用明确的.iloc和.loc索引器。
Series 索引操作符的一种可接受使用场景是进行布尔索引。详情请参见第十二章,索引对齐。
我将这一类型的行切片称为懒惰切片,因为它不使用更明确的.iloc或.loc。就我个人而言,每当进行行切片时,我总是使用这些索引器,因为这样做没有任何疑问,能够明确知道自己在做什么。
还有更多内容...
需要注意的是,这种懒惰切片方法只适用于 DataFrame 的行和 Series,不能用于列。此外,也不能同时选择行和列。例如,下面的代码试图选择前十行和两列:
>>> college[:10, ['CITY', 'STABBR']]
TypeError: unhashable type: 'slice'
要以这种方式进行选择,你需要使用.loc或.iloc。这里有一种可能的方式,首先选择所有学院标签,然后使用基于标签的索引器.loc:
>>> first_ten_instnm = college.index[:10]
>>> college.loc[first_ten_instnm, ['CITY', 'STABBR']]
字典顺序切片
.loc索引器通常根据索引的精确字符串标签来选择数据。然而,它也允许你根据索引中值的字典顺序来选择数据。具体来说,.loc允许你使用切片符号按字典顺序选择所有行。这只有在索引已排序的情况下才有效。
准备工作
在这个实例中,你将首先对索引进行排序,然后在.loc索引器内部使用切片符号选择两个字符串之间的所有行。
如何操作...
- 读取学院数据集,并将学院名称设置为索引:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
- 尝试选择所有名称在字典顺序上介于
'Sp'和'Su'之间的学院:
>>> college.loc['Sp':'Su']
KeyError: 'Sp'
- 由于索引未排序,之前的命令会失败。让我们对索引进行排序:
>>> college = college.sort_index()
- 现在,让我们重新运行步骤 2 中的相同命令:
>>> college.loc['Sp':'Su']
它是如何工作的...
.loc的正常行为是根据传入的精确标签来选择数据。当这些标签在索引中找不到时,它会引发KeyError。然而,当索引按字典顺序排序并传入切片时,存在一个特殊例外。在这种情况下,即使start和stop标签不是索引中的精确值,也可以在它们之间进行选择。
还有更多内容...
使用这个方法,选择字母表中两个字母之间的学院非常容易。例如,要选择所有以字母D到S开头的学院,你可以使用college.loc['D':'T']。像这样切片时,最后一个索引仍然包括在内,所以这将返回一个名称恰好为T的学院。
这种类型的切片在索引按相反方向排序时也适用。你可以通过 is_monotonic_increasing 或 is_monotonic_decreasing 属性来判断索引的排序方向。这两个属性中的任何一个必须为 True,才能使字典顺序切片生效。例如,以下代码会按字典顺序将索引从 Z 排序到 A:
>>> college = college.sort_index(ascending=False)
>>> college.index.is_monotonic_decreasing
True
>>> college.loc['E':'B']
Python 会将所有大写字母排在小写字母之前,所有整数排在大写字母之前。
第十一章:布尔索引
从数据集中筛选数据是最常见和基本的操作之一。使用布尔索引,有多种方式可以在 pandas 中筛选(或子集化)数据。布尔索引(也称为布尔选择)可能是一个让人困惑的术语,但在 pandas 中,它指的是通过为每一行提供布尔值(True 或 False)来选择行。这些布尔值通常存储在 Series 或 NumPy 的ndarray中,并通常通过对 DataFrame 中一个或多个列应用布尔条件来创建。我们首先创建布尔 Series,并对其计算统计数据,然后再通过创建更复杂的条件来使用布尔索引,以多种方式筛选数据。
在本章中,我们将讨论以下主题:
-
计算布尔统计数据
-
构造多个布尔条件
-
使用布尔索引筛选
-
使用索引选择复制布尔索引
-
使用唯一且排序的索引进行选择
-
获取股价的视角
-
转换 SQL WHERE 子句
-
确定股市回报的正态性
-
使用 query 方法提高布尔索引的可读性
-
使用
where方法保留 Series -
屏蔽 DataFrame 行
-
使用布尔值、整数位置和标签进行选择
计算布尔统计数据
在首次接触布尔 Series 时,计算其基本汇总统计信息可能会很有帮助。布尔 Series 中的每个值都被评估为 0 或 1,因此所有适用于数值的 Series 方法也适用于布尔值。
准备工作
在这个例子中,我们通过对数据列应用条件来创建布尔 Series,然后从中计算汇总统计信息。
如何操作...
- 读取
movie数据集,将索引设置为电影标题,并查看前几行:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> movie.head()
- 通过使用大于比较运算符与
durationSeries,判断每部电影的时长是否超过两小时:
>>> movie_2_hours = movie['duration'] > 120
>>> movie_2_hours.head(10)
movie_title
Avatar True
Pirates of the Caribbean: At World's End True
Spectre True
The Dark Knight Rises True
Star Wars: Episode VII - The Force Awakens False
John Carter True
Spider-Man 3 True
Tangled False
Avengers: Age of Ultron True
Harry Potter and the Half-Blood Prince True
Name: duration, dtype: bool
- 现在我们可以使用这个 Series 来确定时长超过两小时的电影数量:
>>> movie_2_hours.sum()
1039
- 要找出数据集中时长超过两小时的电影百分比,可以使用
mean方法:
>>> movie_2_hours.mean()
0.2114
- 不幸的是,第 4 步的输出是误导性的。
duration列有一些缺失值。如果你回顾第 1 步的 DataFrame 输出,你会看到最后一行缺少duration的值。第 2 步的布尔条件对此返回了False。我们需要先删除缺失值,然后再评估条件并计算均值:
>>> movie['duration'].dropna().gt(120).mean()
.2112
- 使用
describe方法输出布尔 Series 的一些汇总统计信息:
>>> movie_2_hours.describe()
count 4916
unique 2
top False
freq 3877
Name: duration, dtype: object
它是如何工作的...
大多数 DataFrame 不会像我们的电影数据集那样拥有布尔列。生成布尔系列的最直接方法是将条件应用于其中一列,使用比较运算符。在步骤 2 中,我们使用大于运算符来测试每部电影的时长是否超过两小时(120 分钟)。步骤 3 和 4 计算了布尔系列的两个重要量——其总和和均值。这些方法之所以可行,是因为 Python 将False/True分别视为 0/1。
你可以通过计算布尔系列的均值来证明其表示True值的百分比。为此,可以使用value_counts方法,并将normalize参数设置为True来获取其分布情况:
>>> movie_2_hours.value_counts(normalize=True)
False 0.788649
True 0.211351
Name: duration, dtype: float64
步骤 5 提醒我们注意步骤 4 的错误结果。尽管duration列有缺失值,但布尔条件仍然将所有针对缺失值的比较结果评估为False。删除这些缺失值使我们能够计算出正确的统计量。这可以通过方法链式调用一步完成。
步骤 6 展示了 pandas 如何将布尔列与其处理对象数据类型的方式相似,显示频率信息。这是思考布尔系列的一种自然方式,而不是像处理数值数据时那样显示分位数。
还有更多内容...
可以比较来自同一 DataFrame 的两列,生成布尔系列。例如,我们可以确定电影中演员 1 是否比演员 2 拥有更多的 Facebook 点赞数。为此,我们需要选择这两列,并删除任何包含缺失值的行。然后,我们进行比较并计算均值:
>>> actors = movie[['actor_1_facebook_likes',
'actor_2_facebook_likes']].dropna()
>>> (actors['actor_1_facebook_likes'] >
actors['actor_2_facebook_likes']).mean()
.978
构建多个布尔条件
在 Python 中,布尔表达式使用内建的逻辑运算符and、or和not。这些关键字不能与 pandas 中的布尔索引一起使用,分别被替换为&、|和~。此外,每个表达式必须用括号括起来,否则会抛出错误。
准备就绪
构建一个精确的筛选器来处理你的数据集,可能需要将多个布尔表达式结合起来,提取出一个精确的子集。在这个步骤中,我们构建了多个布尔表达式,并将它们结合起来,找出所有imdb_score大于 8、content_rating为 PG-13 且title_year早于 2000 年或晚于 2009 年的电影。
如何实现...
- 加载电影数据集并将标题设置为索引:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
- 创建一个变量来独立地存储每组标准,作为一个布尔系列:
>>> criteria1 = movie.imdb_score > 8
>>> criteria2 = movie.content_rating == 'PG-13'
>>> criteria3 = ((movie.title_year < 2000) |
(movie.title_year > 2009))
>>> criteria2.head() # all criteria Series look similar
movie_title
Avatar True
Pirates of the Caribbean: At World's End True
Spectre True
The Dark Knight Rises True
Star Wars: Episode VII - The Force Awakens False
Name: content_rating, dtype: bool
- 将所有标准结合成一个布尔系列:
>>> criteria_final = criteria1 & criteria2 & criteria3
>>> criteria_final.head()
movie_title
Avatar False
Pirates of the Caribbean: At World's End False
Spectre False
The Dark Knight Rises True
Star Wars: Episode VII - The Force Awakens False
dtype: bool
工作原理...
Series 中的所有值都可以使用标准的比较运算符(<、>、==、!=、<=、>=)与标量值进行比较。表达式movie.imdb_score > 8将产生一个布尔值 Series,其中所有imdb_score大于 8 的值为True,小于或等于 8 的为False。这个布尔 Series 的索引保持与原始 Series 相同,在这个例子中,是电影的标题。
criteria3变量由两个独立的布尔表达式组成。每个表达式必须用括号括起来才能正常工作。管道字符|用于在两个 Series 的每个值之间创建逻辑or条件。
所有三个条件都需要为True,才能符合食谱的要求。它们通过与符号&结合,形成一个逻辑and条件,适用于每个 Series 值。
还有更多...
pandas 使用不同语法进行逻辑运算符的结果是,运算符的优先级不再相同。比较运算符的优先级高于and、or和not。然而,pandas 的新运算符(按位运算符&、|和~)的优先级高于比较运算符,因此需要使用括号。以下是一个示例,帮助更清楚地理解:
>>> 5 < 10 and 3 > 4
False
在前面的表达式中,5 < 10首先求值,然后是3 < 4,最后求值and。Python 按如下顺序推进表达式:
>>> 5 < 10 and 3 > 4
>>> True and 3 > 4
>>> True and False
>>> False
让我们看看如果criteria3中的表达式写成如下会发生什么:
>>> movie.title_year < 2000 | movie.title_year > 2009
TypeError: cannot compare a dtyped [float64] array with a scalar of type [bool]
由于按位运算符的优先级高于比较运算符,首先会求值2000 | movie.title_year,这是没有意义的并且会抛出错误。因此,需要使用括号来确保运算以正确的顺序进行。
为什么 pandas 不能使用and、or和not?当这些关键字被求值时,Python 试图找到对象整体的真值。由于将整个 Series 视为 True 或 False 没有意义——只有每个元素才有意义——因此 pandas 会抛出错误。
Python 中的许多对象都有布尔表示。例如,除 0 以外的所有整数都被视为True。除空字符串外的所有字符串都为True。所有非空的集合、元组、字典和列表都为True。一个空的 DataFrame 或 Series 不会被评估为 True 或 False,而是抛出错误。通常,要获取 Python 对象的真值,可以将其传递给bool函数。
另见
- Python 运算符优先级(
bit.ly/2vxuqSn)
使用布尔索引进行筛选
对于 Series 和 DataFrame 对象的布尔选择几乎是相同的。两者都是通过将一个布尔值 Series(其索引与被过滤对象相同)传递给索引运算符来实现的。
准备工作
本示例构建了两个复杂且独立的布尔标准,用于选择不同的电影集。第一组电影来自前一个示例,包含 imdb_score 大于 8,content_rating 为 PG-13,以及 title_year 在 2000 年之前或 2009 年之后的电影。第二组电影包含 imdb_score 小于 5,content_rating 为 R,并且 title_year 在 2000 年到 2010 年之间的电影。
如何操作...
- 读取
movie数据集,将索引设置为movie_title,并创建第一组标准:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> crit_a1 = movie.imdb_score > 8
>>> crit_a2 = movie.content_rating == 'PG-13'
>>> crit_a3 = (movie.title_year < 2000) | (movie.title_year > 2009)
>>> final_crit_a = crit_a1 & crit_a2 & crit_a3
- 为第二组电影创建标准:
>>> crit_b1 = movie.imdb_score < 5
>>> crit_b2 = movie.content_rating == 'R'
>>> crit_b3 = ((movie.title_year >= 2000) &
(movie.title_year <= 2010))
>>> final_crit_b = crit_b1 & crit_b2 & crit_b3
- 使用 pandas
or操作符将两个标准结合起来。这将生成一个布尔序列,包含属于任一标准的所有电影:
>>> final_crit_all = final_crit_a | final_crit_b
>>> final_crit_all.head()
movie_title
Avatar False
Pirates of the Caribbean: At World's End False
Spectre False
The Dark Knight Rises True
Star Wars: Episode VII - The Force Awakens False
dtype: bool
- 一旦你有了布尔序列,只需将其传递给索引操作符来过滤数据:
>>> movie[final_crit_all].head()
- 我们已经成功地过滤了数据和数据框的所有列。我们无法轻松地手动检查过滤是否正确。让我们通过
.loc索引器同时过滤行和列:
>>> cols = ['imdb_score', 'content_rating', 'title_year']
>>> movie_filtered = movie.loc[final_crit_all, cols]
>>> movie_filtered.head(10)
它是如何工作的…
在第 1 步和第 2 步中,每组标准都由更简单的布尔表达式构建。并不需要像这里那样为每个布尔表达式创建不同的变量,但这确实使得读取和调试逻辑错误更容易。由于我们需要两组电影,第 3 步使用 pandas 逻辑 or 操作符将它们结合起来。
第 4 步展示了布尔索引的准确语法。你只需将第 3 步中创建的布尔序列直接传递给索引操作符。只有 final_crit_all 中值为 True 的电影会被选择。
布尔索引也可以与 .loc 索引器一起使用,如第 5 步所示,通过同时进行布尔索引和单独列选择。这种精简的数据框更容易手动检查逻辑是否正确实现。
布尔索引不能与 .iloc 索引操作符一起正常工作。如果将布尔序列传递给它,会引发异常。但是,如果传递布尔 ndarray,它将与其他索引器中的行为相同。
还有更多...
如前所述,可以使用一个长的布尔表达式代替多个较短的表达式。为了用一行长代码复制第 1 步中的 final_crit_a 变量,我们可以这样做:
>>> final_crit_a2 = (movie.imdb_score > 8) & \
(movie.content_rating == 'PG-13') & \
((movie.title_year < 2000) |
(movie.title_year > 2009))
>>> final_crit_a2.equals(final_crit_a)
True
另见
-
Pandas 官方文档中的 布尔索引 (
bit.ly/2v1xK77) -
检查 Python 对象的真实性 (
bit.ly/2vn8WXX)
使用索引选择复制布尔索引
通过利用索引,复制特定的布尔选择情况是可能的。通过索引进行选择更加直观,也提高了可读性。
准备工作
在本例中,我们使用college数据集通过布尔索引和索引选择分别选择来自特定州的所有机构,并将它们的性能进行比较。
如何实现...
- 读取
college数据集并使用布尔索引选择德克萨斯州(TX)的所有机构:
>>> college = pd.read_csv('data/college.csv')
>>> college[college['STABBR'] == 'TX'].head()
Pandas 官方文档
- 为了通过索引选择来复制此操作,我们需要将
STABBR列移到索引中。然后我们可以使用基于标签的选择,通过.loc索引器来选择:
>>> college2 = college.set_index('STABBR')
>>> college2.loc['TX'].head()
- 让我们比较两种方法的速度:
>>> %timeit college[college['STABBR'] == 'TX']
1.43 ms ± 53.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit college2.loc['TX']
526 µs ± 6.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
- 布尔索引的时间是索引选择的三倍。由于设置索引并非免费的,因此我们也需要计时这个操作:
>>> %timeit college2 = college.set_index('STABBR')
1.04 ms ± 5.37 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
工作原理...
第一步通过判断哪些数据行的STABBR等于TX来创建布尔 Series。这个 Series 被传递给索引操作符,进而对数据进行子集化。可以通过将相同的列移动到索引中,并使用基于标签的索引选择.loc来复制此过程。通过索引选择的数据比通过布尔选择的数据要快得多。
还有更多内容...
本例只选择一个州。也可以使用布尔索引和索引选择同时选择多个州。让我们选择德克萨斯(TX)、加利福尼亚(CA)和纽约(NY)。对于布尔选择,可以使用isin方法;而对于索引选择,只需将列表传递给.loc:
>>> states = ['TX', 'CA', 'NY']
>>> college[college['STABBR'].isin(states)]
>>> college2.loc[states]
这篇食谱解释的内容远不止这些。Pandas 根据索引是否唯一或已排序来实现索引。有关更多详细信息,请参阅以下食谱。
使用唯一和已排序索引进行选择
当索引唯一或已排序时,索引选择性能会大幅提高。之前的例子使用了一个未排序的索引,该索引包含重复项,这导致选择过程相对较慢。
准备工作
在本食谱中,我们使用college数据集来形成唯一或排序的索引,以提高索引选择的性能。我们将继续将其与布尔索引进行性能比较。
如何实现...
- 读取 college 数据集,创建一个以
STABBR为索引的单独 DataFrame,并检查索引是否已排序:
>>> college = pd.read_csv('data/college.csv')
>>> college2 = college.set_index('STABBR')
>>> college2.index.is_monotonic
False
- 对
college2的索引进行排序,并将其存储为另一个对象:
>>> college3 = college2.sort_index()
>>> college3.index.is_monotonic
True
- 对来自三个数据框架的德克萨斯州(TX)选择进行计时:
>>> %timeit college[college['STABBR'] == 'TX']
1.43 ms ± 53.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit college2.loc['TX']
526 µs ± 6.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit college3.loc['TX']
183 µs ± 3.67 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
- 排序后的索引比布尔选择快近一个数量级。现在我们来看看唯一索引。为此,我们使用机构名称作为索引:
>>> college_unique = college.set_index('INSTNM')
>>> college_unique.index.is_unique
True
- 让我们使用布尔索引选择斯坦福大学:
>>> college[college['INSTNM'] == 'Stanford University']
- 让我们使用索引选择来选择斯坦福大学:
>>> college_unique.loc['Stanford University']
CITY Stanford
STABBR CA
HBCU 0
...
UG25ABV 0.0401
MD_EARN_WNE_P10 86000
GRAD_DEBT_MDN_SUPP 12782
Name: Stanford University, dtype: object
- 它们都产生相同的数据,只是对象不同。让我们对每种方法进行计时:
>>> %timeit college[college['INSTNM'] == 'Stanford University']
1.3 ms ± 56.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit college_unique.loc['Stanford University']
157 µs ± 682 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
工作原理...
当索引未排序且包含重复项时,例如college2,Pandas 需要检查索引中的每个值,以确保做出正确选择。当索引已排序时,如college3,Pandas 利用一种叫做二分查找的算法,极大地提高了性能。
在这部分的食谱中,我们使用一个独特的列作为索引。Pandas 通过哈希表实现唯一索引,这使得选择操作更加高效。无论索引的长度如何,每个索引位置几乎可以在相同的时间内被查找。
还有更多内容...
与索引选择相比,布尔选择提供了更多的灵活性,因为它可以基于任意多个列进行条件筛选。在这个食谱中,我们使用了一个单一的列作为索引。实际上,也可以将多个列连接起来作为索引。例如,在下面的代码中,我们将索引设置为城市和州列的连接:
>>> college.index = college['CITY'] + ', ' + college['STABBR']
>>> college = college.sort_index()
>>> college.head()
从这里开始,我们可以选择来自特定城市和州组合的所有大学,而不使用布尔索引。让我们选择来自Miami, FL的所有大学:
>>> college.loc['Miami, FL'].head()
我们可以将这个复合索引选择的速度与布尔索引进行比较。差异的数量级超过一个:
>>> %%timeit
>>> crit1 = college['CITY'] == 'Miami'
>>> crit2 = college['STABBR'] == 'FL'
>>> college[crit1 & crit2]
2.43 ms ± 80.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit college.loc['Miami, FL']
197 µs ± 8.69 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
参见
- 二分查找算法 (
bit.ly/2wbMq20)
获取股票价格的视角
对于那些购买了长期股票头寸的投资者,显然希望在股票价格接近或达到历史最高点时卖出股票。当然,实际上这非常困难,尤其是当股票价格在其历史中只有一小部分时间位于某个阈值之上时。我们可以使用布尔索引找到股票价格超过或低于某个值的所有时间点。这个练习可能帮助我们理解某只股票的常见交易区间。
准备工作
在这个食谱中,我们研究了 2010 年初至 2017 年中期的施耐德(Schlumberger)股票。我们使用布尔索引提取了这段时间内最低和最高 10%的收盘价序列。然后,我们绘制了所有数据点,并突出显示那些属于上下 10%的数据点。
如何实现...
- 读取施耐德股票数据,将
Date列设置为索引,并将其转换为DatetimeIndex:
>>> slb = pd.read_csv('data/slb_stock.csv', index_col='Date',
parse_dates=['Date'])
>>> slb.head()
- 选择收盘价作为一个序列,并使用
describe方法返回总结统计信息:
>>> slb_close = slb['Close']
>>> slb_summary = slb_close.describe(percentiles=[.1, .9])
>>> slb_summary
count 1895.000000
mean 79.121905
std 11.767802
min 51.750000
10% 64.892000
50% 78.000000
90% 93.248000
max 117.950000
Name: Close, dtype: float64
- 使用布尔选择,选择所有位于上下第十百分位数的收盘价:
>>> upper_10 = slb_summary.loc['90%']
>>> lower_10 = slb_summary.loc['10%']
>>> criteria = (slb_close < lower_10) | (slb_close > upper_10)
>>> slb_top_bottom_10 = slb_close[criteria]
- 将筛选后的结果序列以浅灰色绘制在所有收盘价(黑色)之上。使用
matplotlib库在第十和第九十百分位数位置绘制水平线:
>>> slb_close.plot(color='black', figsize=(12,6))
>>> slb_top_bottom_10.plot(marker='o', style=' ',
ms=4, color='lightgray')
>>> xmin = criteria.index[0]
>>> xmax = criteria.index[-1]
>>> plt.hlines(y=[lower_10, upper_10], xmin=xmin,
xmax=xmax, color='black')
它是如何工作的...
第 2 步中 describe 方法的结果本身是一个 Series,标识性总结统计量作为其索引标签。这个总结 Series 用于存储第十百分位数和第九十百分位数作为独立变量。第 3 步使用布尔索引选择分布的上下第十百分位数中的值。
Series 和 DataFrame 都通过 plot 方法提供直接的绘图功能。第一次调用 plot 方法来自 slb_close Series,它包含了所有的 SLB 收盘价。这是图表中的黑线。来自 slb_filtered 的数据点作为灰色标记直接绘制在收盘价上。style 参数设置为一个空格,这样就不会绘制任何线条。ms 参数设置标记的大小。
Matplotlib 提供了一个方便的函数 hlines,用于绘制水平线。它接受一组 y 值,并从 xmin 到 xmax 绘制这些线。
从我们创建的图表的新视角来看,可以清楚地看到,尽管 SLB 的历史最高股价接近每股 93。
还有更多...
我们可以不再通过在收盘价上绘制红色(或黑色)点来表示上下第十百分位数,而是使用 matplotlib 的 fill_between 函数。这个函数填充两条线之间的所有区域。它接受一个可选的 where 参数,该参数接受一个布尔 Series,告知它确切的填充位置:
>>> slb_close.plot(color='black', figsize=(12,6))
>>> plt.hlines(y=[lower_10, upper_10],
xmin=xmin, xmax=xmax,color='lightgray')
>>> plt.fill_between(x=criteria.index, y1=lower_10,
y2=slb_close.values, color='black')
>>> plt.fill_between(x=criteria.index,y1=lower_10,
y2=slb_close.values, where=slb_close < lower_10,
color='lightgray')
>>> plt.fill_between(x=criteria.index, y1=upper_10,
y2=slb_close.values, where=slb_close > upper_10,
color='lightgray')
翻译 SQL WHERE 子句
许多 pandas 用户有直接从数据库处理数据的背景,使用的是无处不在的 结构化查询语言 (SQL) 。SQL 是一种标准化的语言,用于定义、操作和控制存储在数据库中的数据。SELECT 语句是使用 SQL 选择、过滤、聚合和排序数据的最常用方式。Pandas 可以连接到数据库并向其发送 SQL 语句。
SQL 是数据科学家必须掌握的一种非常重要的语言。世界上大部分的数据存储在需要 SQL 来检索、操作和进行分析的数据库中。SQL 语法相对简单且容易学习。许多公司如 Oracle、Microsoft、IBM 等提供了不同的 SQL 实现。尽管不同的实现之间的语法不兼容,但其核心语法非常相似。
准备工作
在 SQL SELECT 语句中,WHERE 子句是非常常见的,用于过滤数据。这个方法将编写与 SQL 查询等效的 pandas 代码,用于选择员工数据集中的某个子集。
使用此方法并不需要理解任何 SQL 语法。
假设我们被要求找到所有在警察或消防部门工作的女性员工,她们的基础薪资在 8 万到 12 万美元之间。以下 SQL 语句将为我们解答这个查询:
SELECT
UNIQUE_ID,
DEPARTMENT,
GENDER,
BASE_SALARY
FROM
EMPLOYEE
WHERE
DEPARTMENT IN ('Houston Police Department-HPD',
'Houston Fire Department (HFD)') AND
GENDER = 'Female' AND
BASE_SALARY BETWEEN 80000 AND 120000;
如何做...
- 将
employee数据集读入为 DataFrame:
>>> employee = pd.read_csv('data/employee.csv')
- 在筛选数据之前,最好先对每个被筛选的列进行一些手动检查,以了解筛选中将使用的确切值:
>>> employee.DEPARTMENT.value_counts().head()
Houston Police Department-HPD 638
Houston Fire Department (HFD) 384
Public Works & Engineering-PWE 343
Health & Human Services 110
Houston Airport System (HAS) 106
Name: DEPARTMENT, dtype: int64
>>> employee.GENDER.value_counts()
Male 1397
Female 603
>>> employee.BASE_SALARY.describe().astype(int)
count 1886
mean 55767
std 21693
min 24960
25% 40170
50% 54461
75% 66614
max 275000
Name: BASE_SALARY, dtype: int64
- 为每个标准写一个语句。使用
isin方法来测试是否等于多个值中的一个:
>>> depts = ['Houston Police Department-HPD',
'Houston Fire Department (HFD)']
>>> criteria_dept = employee.DEPARTMENT.isin(depts)
>>> criteria_gender = employee.GENDER == 'Female'
>>> criteria_sal = (employee.BASE_SALARY >= 80000) & \
(employee.BASE_SALARY <= 120000)
- 将所有布尔 Series 组合在一起:
>>> criteria_final = (criteria_dept &
criteria_gender &
criteria_sal)
- 使用布尔索引仅选择符合最终标准的行:
>>> select_columns = ['UNIQUE_ID', 'DEPARTMENT',
'GENDER', 'BASE_SALARY']
>>> employee.loc[criteria_final, select_columns].head()
它是如何工作的...
在实际进行筛选之前,你显然需要知道将使用的确切字符串名称。Series 的value_counts方法是获取确切字符串名称和该值出现次数的绝佳方式。
isin方法等同于 SQL 中的IN操作符,接受一个包含所有可能保留值的列表。虽然可以使用一系列OR条件来模拟这个表达式,但效率较低且不够规范。
薪资标准criteria_sal是通过结合两个简单的不等式表达式形成的。所有标准最终通过 pandas 的and运算符&组合在一起,产生一个单一的布尔 Series 作为筛选条件。
还有更多...
对于许多操作,pandas 提供了多种方法来做同一件事。在前面的示例中,薪资标准使用了两个独立的布尔表达式。与 SQL 类似,Series 也有between方法,薪资标准可以等价地写成如下形式:
>>> criteria_sal = employee.BASE_SALARY.between(80000, 120000)
isin的另一个有用应用是提供由其他 pandas 语句自动生成的值序列。这可以避免手动调查以查找需要存储在列表中的确切字符串名称。相反,假设我们要排除出现频率最高的前五个部门的行:
>>> top_5_depts = employee.DEPARTMENT.value_counts().index[:5]
>>> criteria = ~employee.DEPARTMENT.isin(top_5_depts)
>>> employee[criteria]
SQL 的等效查询如下:
SELECT
*
FROM
EMPLOYEE
WHERE
DEPARTMENT not in
(
SELECT
DEPARTMENT
FROM (
SELECT
DEPARTMENT,
COUNT(1) as CT
FROM
EMPLOYEE
GROUP BY
DEPARTMENT
ORDER BY
CT DESC
LIMIT 5
)
);
注意使用 pandas 的非操作符~,它对 Series 的所有布尔值取反。
另见
-
Pandas 官方文档中的
isin方法(bit.ly/2v1GPfQ)和between方法(bit.ly/2wq9YPF) -
请参考第十五章中的连接到 SQL 数据库的食谱,合并 Pandas 对象
-
W3Schools 中的 SQL 基础介绍(
bit.ly/2hsq8Wp) -
SQL 中的 IN 操作符(
bit.ly/2v3H7Bg) -
SQL 中的 BETWEEN 操作符(
bit.ly/2vn5UTP)
确定股市回报的正态性
在基础统计学教材中,正态分布被广泛用于描述许多不同的数据群体。虽然大多数时间,许多随机过程确实看起来像正态分布,但现实生活往往更加复杂。股市回报是一个典型的例子,它的分布看起来可能相当正常,但实际上却可能相差甚远。
准备工作
这份食谱描述了如何获取互联网零售巨头亚马逊的每日股市回报,并非正式地测试它们是否符合正态分布。
如何做...
- 加载亚马逊股票数据并将日期设为索引:
>>> amzn = pd.read_csv('data/amzn_stock.csv', index_col='Date',
parse_dates=['Date'])
>>> amzn.head()
- 通过仅选择收盘价并使用
pct_change方法来计算每日回报率,创建一个 Series:
>>> amzn_daily_return = amzn.Close.pct_change()
>>> amzn_daily_return.head()
Date
2010-01-04 NaN
2010-01-05 0.005900
2010-01-06 -0.018116
2010-01-07 -0.017013
2010-01-08 0.027077
Name: Close, dtype: float64
- 删除缺失值并绘制回报的直方图,以视觉检查分布:
>>> amzn_daily_return = amzn_daily_return.dropna()
>>> amzn_daily_return.hist(bins=20)
- 正态分布大致遵循 68-95-99.7 法则——意味着 68%的数据位于均值 1 个标准差范围内,95%位于 2 个标准差内,99.7%位于 3 个标准差内。我们现在将计算每日回报中有多少百分比落在均值的 1、2 和 3 个标准差范围内。为此,我们需要均值和标准差:
>>> mean = amzn_daily_return.mean()
>>> std = amzn_daily_return.std()
- 计算每个观察值的
z-score的绝对值。z-score是离均值的标准差个数:
>>> abs_z_score = amzn_daily_return.sub(mean).abs().div(std)
- 找出位于 1、2 和 3 个标准差范围内的回报百分比:
>>> pcts = [abs_z_score.lt(i).mean() for i in range(1,4)]
>>> print('{:.3f} fall within 1 standard deviation. '
'{:.3f} within 2 and {:.3f} within 3'.format(*pcts))
0.787 fall within 1 standard deviation. 0.957 within 2 and 0.985 within 3
它是如何工作的...
默认情况下,pct_change Series 方法计算当前元素与前一个元素之间的百分比变化。这将原始的股票收盘价转化为每日百分比回报。返回 Series 的第一个元素是缺失值,因为没有前一个价格。
直方图是总结和可视化一维数值数据的极佳图形。从图中可以明显看出,分布是对称的,但仍然很难判断它是否符合正态分布。有一些正式的统计方法可以用来确定分布的正态性,但我们将简单地找到数据与 68-95-99.7 法则的匹配程度。
第 5 步计算每个观察值离均值的标准差个数,这被称为z-score。这一步使用方法而非符号(-和/)来进行减法和除法。步骤 6 中也使用了小于的方法,而不是符号。
步骤 6 中取均值可能看起来有些奇怪。abs_z_score.lt(1)表达式的结果是一个布尔值的 Series。由于布尔值会被计算为 0 或 1,取该 Series 的均值返回的是True元素所占的百分比,这正是我们所需要的。
现在,我们可以通过将结果数字(78.7-95.7-98.5)与 68-95-99.7 规则进行比较,更容易判断回报的正态性。对于 1 和 3 个标准差,百分比与规则的偏差很大,我们可以得出结论:亚马逊的日股回报不遵循正态分布。
还有更多……
为了自动化此过程,我们可以编写一个函数,接受股票数据并输出日回报的直方图,以及落在均值的 1、2 和 3 个标准差范围内的百分比。以下函数完成了这一功能,并用符号对应的替代方法替换了原有方法:
>>> def test_return_normality(stock_data):
close = stock_data['Close']
daily_return = close.pct_change().dropna()
daily_return.hist(bins=20)
mean = daily_return.mean()
std = daily_return.std()
abs_z_score = abs(daily_return - mean) / std
pcts = [abs_z_score.lt(i).mean() for i in range(1,4)]
print('{:.3f} fall within 1 standard deviation. '
'{:.3f} within 2 and {:.3f} within 3'.format(*pcts))
>>> slb = pd.read_csv('data/slb_stock.csv', index_col='Date',
parse_dates=['Date'])
>>> test_return_normality(slb)
0.742 fall within 1 standard deviation. 0.946 within 2 and 0.986 within 3
另见
- Pandas 官方文档中的
pct_changeSeries 方法 (bit.ly/2wcjmqT)
使用 query 方法改进布尔索引的可读性
布尔索引不一定是最容易阅读或编写的语法,尤其是当使用一行代码写复杂过滤器时。Pandas 提供了一种替代的基于字符串的语法,通过 DataFrame query 方法,可以提供更清晰的表达方式。
query DataFrame 方法是实验性的,功能不如布尔索引强大,不应在生产代码中使用。
准备开始
这个食谱复制了本章之前的食谱,翻译 SQL WHERE 子句,但它改用了 query DataFrame 方法。目标是筛选出来自警察或消防部门的女性员工数据,这些员工的年薪在 8 万到 12 万美元之间。
如何操作……
- 读取员工数据,分配所选部门,并将列导入变量:
>>> employee = pd.read_csv('data/employee.csv')
>>> depts = ['Houston Police Department-HPD',
'Houston Fire Department (HFD)']
>>> select_columns = ['UNIQUE_ID', 'DEPARTMENT',
'GENDER', 'BASE_SALARY']
- 构建查询字符串并执行该方法:
>>> qs = "DEPARTMENT in @depts " \
"and GENDER == 'Female' " \
"and 80000 <= BASE_SALARY <= 120000"
>>> emp_filtered = employee.query(qs)
>>> emp_filtered[select_columns].head()
工作原理……
传递给 query 方法的字符串看起来更像是普通英语,而不是普通的 pandas 代码。可以使用 @ 符号引用 Python 变量,就像 depts 一样。所有的 DataFrame 列名都可以通过直接引用它们的名称来访问,无需内层引号。如果需要字符串,例如 Female,则需要使用内层引号。
query 语法的另一个优点是能够在单个表达式中编写双重不等式,并且它能够理解冗长的逻辑运算符 and、or 和 not,而不是像布尔索引那样使用按位运算符。
还有更多……
我们不需要手动输入部门名称列表,而是可以通过编程的方式创建它。例如,如果我们想找到所有不是前 10 个部门中频率最高的女性员工,可以运行以下代码:
>>> top10_depts = employee.DEPARTMENT.value_counts() \
.index[:10].tolist()
>>> qs = "DEPARTMENT not in @top10_depts and GENDER == 'Female'"
>>> employee_filtered2 = employee.query(qs)
>>> employee_filtered2.head()
另见
- Pandas 官方文档中的
query方法 (bit.ly/2vnlwXk)
使用where方法保留 Series
布尔索引必然会通过删除所有不符合标准的行来过滤数据集。与删除所有这些值不同,使用where方法可以保留它们。where方法保留 Series 或 DataFrame 的大小,并将不满足标准的值设为缺失值,或者用其他值替代。
准备就绪
在这个示例中,我们传递where方法的布尔条件,用于在movie数据集中设置演员 1 的 Facebook 点赞数的上下限。
如何操作...
- 读取
movie数据集,将电影标题设置为索引,并选择actor_1_facebook_likes列中所有非缺失的值:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> fb_likes = movie['actor_1_facebook_likes'].dropna()
>>> fb_likes.head()
movie_title
Avatar 1000.0
Pirates of the Caribbean: At World's End 40000.0
Spectre 11000.0
The Dark Knight Rises 27000.0
Star Wars: Episode VII - The Force Awakens 131.0
Name: actor_1_facebook_likes, dtype: float64
- 让我们使用
describe方法来大致了解分布情况:
>>> fb_likes.describe(percentiles=[.1, .25, .5, .75, .9]) \
.astype(int)
count 4909
mean 6494
std 15106
min 0
10% 240
25% 607
50% 982
75% 11000
90% 18000
max 640000
Name: actor_1_facebook_likes, dtype: int64
- 此外,我们还可以绘制该 Series 的直方图来直观检查分布情况:
>>> fb_likes.hist()
- 这是一个相当糟糕的可视化图形,很难看出分布情况。另一方面,步骤 2 中的摘要统计量似乎告诉我们数据高度偏向右侧,且许多观测值比中位数大一个数量级。让我们创建标准来测试点赞数是否少于 20,000:
>>> criteria_high = fb_likes < 20000
>>> criteria_high.mean().round(2)
.91
- 大约 91%的电影的演员 1 点赞数少于 20,000。现在我们将使用
where方法,它接受布尔条件。默认行为是返回一个与原始 Series 大小相同的 Series,但将所有False位置替换为缺失值:
>>> fb_likes.where(criteria_high).head()
movie_title
Avatar 1000.0
Pirates of the Caribbean: At World's End NaN
Spectre 11000.0
The Dark Knight Rises NaN
Star Wars: Episode VII - The Force Awakens 131.0
Name: actor_1_facebook_likes, dtype: float64
where方法的第二个参数other允许你控制替换的值。让我们将所有缺失的值改为 20,000:
>>> fb_likes.where(criteria_high, other=20000).head()
movie_title
Avatar 1000.0
Pirates of the Caribbean: At World's End 20000.0
Spectre 11000.0
The Dark Knight Rises 20000.0
Star Wars: Episode VII - The Force Awakens 131.0
Name: actor_1_facebook_likes, dtype: float64
- 同样,我们可以创建标准来设定最小点赞数的下限。在这里,我们链式调用另一个
where方法,并将不满足条件的值替换为300:
>>> criteria_low = fb_likes > 300
>>> fb_likes_cap = fb_likes.where(criteria_high, other=20000)\
.where(criteria_low, 300)
>>> fb_likes_cap.head()
movie_title
Avatar 1000.0
Pirates of the Caribbean: At World's End 20000.0
Spectre 11000.0
The Dark Knight Rises 20000.0
Star Wars: Episode VII - The Force Awakens 300.0
Name: actor_1_facebook_likes, dtype: float64
- 原始 Series 和修改后的 Series 的长度相同:
>>> len(fb_likes), len(fb_likes_cap)
(4909, 4909)
- 让我们用修改后的 Series 制作一个直方图。由于数据的范围更加紧凑,它应该能生成一个更好的图形:
>>> fb_likes_cap.hist()
它是如何工作的...
where方法再次保留调用对象的大小和形状,并且在传入的布尔值为True时不会修改值。在步骤 1 中删除缺失值是很重要的,因为where方法最终会在后续步骤中用有效数字替换它们。
步骤 2 中的摘要统计给了我们一些直觉,表明在哪些情况下设置数据上限是有意义的。另一方面,步骤 3 中的直方图似乎将所有数据集中到一个箱子里。数据中有太多离群值,单纯的直方图很难生成好的图形。where方法允许我们为数据设置上限和下限,从而生成一个具有更多可见条形的直方图。
还有更多...
Pandas 实际上有内建的方法 clip、clip_lower 和 clip_upper,可以实现这一操作。clip 方法可以同时设置下限和上限。我们还检查了这个替代方法是否产生完全相同的 Series,结果是一样的:
>>> fb_likes_cap2 = fb_likes.clip(lower=300, upper=20000)
>>> fb_likes_cap2.equals(fb_likes_cap)
True
另见
- Pandas 官方文档关于
where方法的介绍 (bit.ly/2vmW2cv)
遮罩 DataFrame 行
mask 方法执行的操作正好是 where 方法的相反操作。默认情况下,它会在布尔条件为 True 的地方创建缺失值。本质上,它就是在你的数据集中遮罩或覆盖掉某些值。
准备工作
在这个案例中,我们将遮罩所有 2010 年之后制作的电影数据行,然后筛选出所有包含缺失值的行。
如何操作……
- 读取
movie数据集,将电影标题设为索引,并创建筛选条件:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> c1 = movie['title_year'] >= 2010
>>> c2 = movie['title_year'].isnull()
>>> criteria = c1 | c2
- 使用
mask方法在 DataFrame 中将所有 2010 年以后制作的电影行中的值设为缺失值。任何原本title_year列为缺失值的电影也会被遮罩:
>>> movie.mask(criteria).head()
- 注意到前面 DataFrame 中第三、第四和第五行的所有值都缺失了。链式调用
dropna方法删除所有值都缺失的行:
>>> movie_mask = movie.mask(criteria).dropna(how='all')
>>> movie_mask.head()
- 第 3 步的操作其实就是做一个复杂的布尔索引。我们可以检查这两种方法是否产生相同的 DataFrame:
>>> movie_boolean = movie[movie['title_year'] < 2010]
>>> movie_mask.equals(movie_boolean)
False
equals方法告诉我们它们不相等。有些地方出错了。让我们做一些合理性检查,看看它们的形状是否相同:
>>> movie_mask.shape == movie_boolean.shape
True
- 当我们使用前面的
mask方法时,它创建了很多缺失值。缺失值的类型是float,因此任何原本是整数的列现在变成了浮动类型。equals方法会返回False,如果列的数据类型不同,即使它们的值是相同的。让我们检查数据类型的相等性,看看这种情况是否发生:
>>> movie_mask.dtypes == movie_boolean.dtypes
color True
director_name True
num_critic_for_reviews True
duration True
director_facebook_likes True
actor_3_facebook_likes True
actor_2_name True
actor_1_facebook_likes True
gross True
genres True
actor_1_name True
num_voted_users False
cast_total_facebook_likes False
.....
dtype: bool
- 结果发现有几列的类型不相同。Pandas 为这些情况提供了一个替代方案。在其测试模块中,主要供开发者使用,有一个函数
assert_frame_equal,允许你在不检查数据类型是否相等的情况下,检查 Series 和 DataFrame 的内容是否相等:
from pandas.testing import assert_frame_equal
>>> assert_frame_equal(movie_boolean, movie_mask, check_dtype=False)
它是如何工作的……
默认情况下,mask 方法会将缺失值覆盖掉。mask 方法的第一个参数是条件,通常是布尔类型的 Series,例如 criteria。由于 mask 方法是从 DataFrame 调用的,所以在条件为 False 的行中,所有的值都会变为缺失值。第 3 步使用这个被遮罩的 DataFrame 来删除包含所有缺失值的行。第 4 步演示了如何使用布尔索引执行相同的操作。
在数据分析过程中,不断验证结果非常重要。检查 Series 和 DataFrame 是否相等是非常常见的验证方法。我们在第 4 步中的第一次尝试得到了意外的结果。在深入分析之前,一些基本的合理性检查,比如确保行和列的数量相同,或者行列名称相同,是很好的检查方式。
第 6 步比较了两组数据类型的 Series。正是在这里,我们揭示了 DataFrames 不相等的原因。equals 方法检查值和数据类型是否相同。第 7 步中的 assert_frame_equal 函数有许多可用的参数,用于以多种方式测试相等性。请注意,调用 assert_frame_equal 后没有输出。如果两个传递的 DataFrames 相等,该方法返回 None;如果不相等,则抛出错误。
还有更多...
让我们比较掩码和删除缺失行与布尔索引之间的速度差异。在这种情况下,布尔索引速度快了一个数量级:
>>> %timeit movie.mask(criteria).dropna(how='all')
11.2 ms ± 144 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
>>> %timeit movie[movie['title_year'] < 2010]
1.07 ms ± 34.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
另见
-
Pandas 官方文档关于
assert_frame_equal的说明 (bit.ly/2u5H5Yl) -
Python 官方文档关于
assert语句的说明 (bit.ly/2v1YKmY)
使用布尔值、整数位置和标签进行选择
第十章,选择数据的子集,涵盖了通过 .iloc 和 .loc 索引器选择不同数据子集的多种方法。这两种索引器可以通过整数位置或标签同时选择行和列。这两种索引器还可以通过布尔索引进行数据选择,尽管布尔值既不是整数,也不是标签。
准备工作
在本例中,我们将通过布尔索引同时过滤 .iloc 和 .loc 索引器的行和列。
如何操作...
- 读取电影数据集,设置标题为索引,然后创建一个布尔 Series,匹配所有内容评级为
G且 IMDB 评分小于4的电影:
>>> movie = pd.read_csv('data/movie.csv', index_col='movie_title')
>>> c1 = movie['content_rating'] == 'G'
>>> c2 = movie['imdb_score'] < 4
>>> criteria = c1 & c2
- 我们首先将这些标准传递给
.loc索引器以过滤行:
>>> movie_loc = movie.loc[criteria]
>>> movie_loc.head()
- 让我们检查一下这个 DataFrame 是否与直接通过索引操作符生成的 DataFrame 完全相等:
>>> movie_loc.equals(movie[criteria])
True
- 现在让我们尝试使用
.iloc索引器进行相同的布尔索引:
>>> movie_iloc = movie.iloc[criteria]
ValueError: iLocation based boolean indexing cannot use an indexable as a mask
- 事实证明,我们不能直接使用布尔值的 Series,因为存在索引问题。然而,我们可以使用布尔值的 ndarray。要提取该数组,可以使用
values属性:
>>> movie_iloc = movie.iloc[criteria.values]
>>> movie_iloc.equals(movie_loc)
True
- 虽然不太常见,但可以使用布尔索引选择特定的列。在这里,我们选择所有数据类型为 64 位整数的列:
>>> criteria_col = movie.dtypes == np.int64
>>> criteria_col.head()
color False
director_name False
num_critic_for_reviews False
duration False
director_facebook_likes False
dtype: bool
>>> movie.loc[:, criteria_col].head()
- 由于
criteria_col是一个 Series,它总是有一个索引,因此你必须使用底层的 ndarray 才能与.iloc一起使用。以下操作产生与第 6 步相同的结果。
>>> movie.iloc[:, criteria_col.values].head()
- 可以使用布尔序列来选择行,并同时使用整数或标签选择列。记住,行和列选择之间需要用逗号隔开。让我们保持行标准,选择
content_rating、imdb_score、title_year和gross:
>>> cols = ['content_rating', 'imdb_score', 'title_year', 'gross']
>>> movie.loc[criteria, cols].sort_values('imdb_score')
- 可以通过
.iloc重复相同的操作,但你需要获取所有列的整数位置:
>>> col_index = [movie.columns.get_loc(col) for col in cols]
>>> col_index
[20, 24, 22, 8]
>>> movie.iloc[criteria.values, col_index]
它是如何工作的…
布尔索引可以通过.iloc和.loc索引器实现,但有一个注意点:.iloc不能传递 Series,而必须传递底层的 ndarray。让我们来看一下criteria序列的底层一维 ndarray:
>>> a = criteria.values
>>> a[:5]
array([False, False, False, False, False], dtype=bool)
>>> len(a), len(criteria)
(4916, 4916)
数组的长度与 Series 的长度相同,而 Series 的长度与电影 DataFrame 的长度相同。布尔数组的整数位置与 DataFrame 的整数位置对齐,过滤过程按预期发生。这些数组也可以与.loc操作符一起使用,但它们对于.iloc是必需的。
第 6 步和第 7 步展示了如何按列过滤,而不是按行过滤。冒号:表示选择所有行,冒号后的逗号分隔了行和列的选择。其实,还有一种更简单的方法可以选择具有整数数据类型的列,那就是通过select_dtypes方法。
第 8 步和第 9 步展示了一种非常常见且有用的方法,可以同时对行和列进行布尔索引。你只需在行和列选择之间放置一个逗号。第 9 步使用列表推导式遍历所有所需的列名,通过get_loc方法找到它们的整数位置。
还有更多…
实际上,可以将布尔数组和列表传递给 Series 对象,这些数组和列表的长度与正在进行索引操作的 DataFrame 不相等。我们通过选择第一行和第三行,以及第一列和第四列来看看这个例子:
>>> movie.loc[[True, False, True], [True, False, False, True]]
两个布尔列表的长度与它们索引的轴的长度不同。列表中没有明确给出布尔值的其余行和列会被丢弃。
另见
- 请参考第十章的选择具有整数和标签的数据方法,选择数据的子集
第十二章:索引对齐
当多个 Series 或 DataFrame 以某种方式组合时,数据的每个维度都会在每个轴上自动对齐,然后才会进行任何计算。对于未熟悉者来说,这种默默进行的自动对齐可能会引起极大的困惑,但它为高级用户提供了巨大的灵活性。本章深入探讨了索引对象,并展示了一系列利用其自动对齐功能的示例。
本章将涵盖以下主题:
-
检查索引对象
-
生成笛卡尔积
-
扩展索引
-
填充具有不等索引的值
-
从不同的 DataFrame 中追加列
-
突出显示每列的最大值
-
使用方法链复制
idxmax -
查找最常见的最大值
检查索引对象
Series 和 DataFrame 的每个轴都有一个 Index 对象,用于标记值。索引对象有许多不同类型,但它们都具有相同的公共行为。除特殊的 MultiIndex 外,所有 Index 对象都是一维数据结构,结合了 Python 集合和 NumPy ndarray 的功能和实现。
准备好
在这个示例中,我们将检查大学数据集的列索引,并探讨其许多功能。
如何实现...
- 读取大学数据集,将列索引分配给一个变量,并输出它:
>>> college = pd.read_csv('data/college.csv')
>>> columns = college.columns
>>> columns
Index(['INSTNM', 'CITY', 'STABBR', 'HBCU', ...], dtype='object')
- 使用
values属性访问底层的 NumPy 数组:
>>> columns.values
array(['INSTNM', 'CITY', 'STABBR', 'HBCU', ...], dtype=object)
- 通过标量、列表或切片根据整数位置选择索引中的项:
>>> columns[5]
'WOMENONLY'
>>> columns[[1,8,10]]
Index(['CITY', 'SATMTMID', 'UGDS'], dtype='object')
>>> columns[-7:-4]
Index(['PPTUG_EF', 'CURROPER', 'PCTPELL'], dtype='object')
- 索引与 Series 和 DataFrame 共享许多相同的方法:
>>> columns.min(), columns.max(), columns.isnull().sum()
('CITY', 'WOMENONLY', 0)
- 直接在
Index对象上使用基本的算术和比较操作符:
>>> columns + '_A'
Index(['INSTNM_A', 'CITY_A', 'STABBR_A', 'HBCU_A', ...], dtype='object')
>>> columns > 'G'
array([ True, False, True, True, ...], dtype=bool)
- 尝试在创建后直接更改索引值会失败。索引是不可变对象:
>>> columns[1] = 'city'
TypeError: Index does not support mutable operations
它是如何工作的...
正如你从许多索引对象操作中看到的,它似乎与 Series 和ndarrays有很多相似之处。最大的区别之一出现在第 6 步。索引是不可变的,一旦创建,其值不能更改。
还有更多...
索引支持集合操作:并集、交集、差集和对称差集:
>>> c1 = columns[:4]
>>> c1
Index(['INSTNM', 'CITY', 'STABBR', 'HBCU'], dtype='object')
>>> c2 = columns[2:6]
>>> c2
Index(['STABBR', 'HBCU', 'MENONLY'], dtype='object')
>>> c1.union(c2) # or `c1 | c2`
Index(['CITY', 'HBCU', 'INSTNM', 'MENONLY', 'RELAFFIL', 'STABBR'], dtype='object')
>>> c1.symmetric_difference(c2) # or `c1 ^ c2`
Index(['CITY', 'INSTNM', 'MENONLY'], dtype='object')
索引与 Python 集合共享一些相同的操作。索引在另一个重要方面与 Python 集合相似。它们(通常)使用哈希表实现,这使得从 DataFrame 中选择行或列时具有极快的访问速度。由于它们是使用哈希表实现的,索引对象的值需要是不可变的,如字符串、整数或元组,就像 Python 字典中的键一样。
索引支持重复值,如果某个索引中有重复项,那么哈希表将无法再使用
用于其实现,且对象访问速度变得更慢。
参见
- Pandas 官方文档中的
Index(bit.ly/2upfgtr)
生成笛卡尔积
每当两个 Series 或 DataFrame 与另一个 Series 或 DataFrame 进行操作时,每个对象的索引(包括行索引和列索引)首先会进行对齐,才会开始任何操作。这个索引对齐是默默发生的,对于新接触 pandas 的人来说可能会感到很惊讶。除非索引完全相同,否则这种对齐总是会在索引之间创建一个笛卡尔积。
笛卡尔积是一个数学术语,通常出现在集合论中。两个集合的笛卡尔积是两个集合中所有可能的元素对的组合。例如,标准扑克牌中的 52 张牌表示的是 13 个点数(A,2,3,...,Q,K)与四种花色之间的笛卡尔积。
准备工作
产生笛卡尔积并不总是预期的结果,但了解它是如何发生的以及何时发生,能帮助避免不期望的后果。在这个示例中,两个具有重叠但不同索引的 Series 被相加,产生了一个令人惊讶的结果。
如何实现...
按照以下步骤创建一个笛卡尔积:
- 构造两个具有不同索引但包含一些相同值的 Series:
>>> s1 = pd.Series(index=list('aaab'), data=np.arange(4))
>>> s1
a 0
a 1
a 2
b 3
dtype: int64
>>> s2 = pd.Series(index=list('cababb'), data=np.arange(6))
>>> s2
c 0
a 1
b 2
a 3
b 4
b 5
dtype: int64
- 将这两个 Series 相加,产生一个笛卡尔积:
>>> s1 + s2
a 1.0
a 3.0
a 2.0
a 4.0
a 3.0
a 5.0
b 5.0
b 7.0
b 8.0
c NaN
dtype: float64
它是如何工作的...
每个 Series 都是通过类构造函数创建的,该构造函数接受各种各样的输入,其中最简单的输入是每个参数 index 和 data 的值序列。
数学上的笛卡尔积与对两个 pandas 对象进行操作的结果略有不同。在 s1 中的每个 a 标签都会与 s2 中的每个 a 标签配对。这种配对在结果的 Series 中产生了六个 a 标签,三个 b 标签和一个 c 标签。笛卡尔积发生在所有相同的索引标签之间。
由于标签 c 仅存在于 Series s2 中,pandas 默认将其值设为缺失,因为在 s1 中没有与之对齐的标签。每当一个索引标签只属于一个对象时,pandas 会将其默认设置为缺失值。遗憾的是,这导致 Series 的数据类型从整数转换为浮点数,因为 NumPy 的缺失值对象 np.nan 只适用于浮点数,而不适用于整数。Series 和 DataFrame 列必须具有同质的数值数据类型,因此每个值都被转换为浮点数。对于这个小数据集来说差异不大,但对于较大的数据集,这可能会带来显著的内存影响。
还有更多...
上述示例的例外情况发生在索引包含完全相同的元素并且顺序相同的情况下。当发生这种情况时,不会进行笛卡尔积,索引会根据它们的位置对齐。在这里需要注意的是,每个元素都根据位置精确对齐,并且数据类型保持为整数:
>>> s1 = pd.Series(index=list('aaabb'), data=np.arange(5))
>>> s2 = pd.Series(index=list('aaabb'), data=np.arange(5))
>>> s1 + s2
a 0
a 2
a 4
b 6
b 8
dtype: int64
如果索引的元素相同,但顺序在两个 Series 之间不同,则会发生笛卡尔积。让我们改变 s2 中索引的顺序,并重新运行相同的操作:
>>> s1 = pd.Series(index=list('aaabb'), data=np.arange(5))
>>> s2 = pd.Series(index=list('bbaaa'), data=np.arange(5))
>>> s1 + s2
a 2
a 3
a 4
a 3
a 4
a 5
a 4
a 5
a 6
b 3
b 4
b 4
b 5
dtype: int64
很有趣的是,pandas 对相同操作有两个截然不同的结果。如果笛卡尔积是 pandas 的唯一选择,那么像将 DataFrame 列相加这样简单的操作也会导致返回元素数量的爆炸。
在这个示例中,每个系列的元素数量不同。通常,在 Python 和其他语言中,当操作的维度包含不同数量的元素时,不允许进行操作。但 Pandas 通过首先对齐索引,然后再执行操作,允许这种情况发生。
爆炸索引
之前的示例通过两个小系列的加法操作演示了索引不相等时的情况。当处理更大数据时,这个问题可能会产生荒谬的错误结果。
准备就绪
在这个示例中,我们添加了两个较大的系列,它们的索引有少量独特的值,但顺序不同。结果会导致索引中的值数爆炸。
如何做...
- 读取员工数据并将索引设置为种族列:
>>> employee = pd.read_csv('data/employee.csv', index_col='RACE')
>>> employee.head()
- 选择
BASE_SALARY列作为两个不同的系列。检查此操作是否确实创建了两个新对象:
>>> salary1 = employee['BASE_SALARY']
>>> salary2 = employee['BASE_SALARY']
>>> salary1 is salary2
True
salary1和salary2变量实际上引用的是同一个对象。这意味着对其中一个的任何修改都会影响另一个。为了确保获得数据的全新副本,使用copy方法:
>>> salary1 = employee['BASE_SALARY'].copy()
>>> salary2 = employee['BASE_SALARY'].copy()
>>> salary1 is salary2
False
- 让我们通过对其中一个系列进行排序来改变其索引的顺序:
>>> salary1 = salary1.sort_index()
>>> salary1.head()
RACE
American Indian or Alaskan Native 78355.0
American Indian or Alaskan Native 81239.0
American Indian or Alaskan Native 60347.0
American Indian or Alaskan Native 68299.0
American Indian or Alaskan Native 26125.0
Name: BASE_SALARY, dtype: float64
>>> salary2.head()
RACE
Hispanic/Latino 121862.0
Hispanic/Latino 26125.0
White 45279.0
White 63166.0
White 56347.0
Name: BASE_SALARY, dtype: float64
- 让我们将这两个
salary系列加在一起:
>>> salary_add = salary1 + salary2
>>> salary_add.head()
RACE
American Indian or Alaskan Native 138702.0
American Indian or Alaskan Native 156710.0
American Indian or Alaskan Native 176891.0
American Indian or Alaskan Native 159594.0
American Indian or Alaskan Native 127734.0
Name: BASE_SALARY, dtype: float64
- 操作成功完成。让我们再创建一个
salary1系列,并将其加到自身,然后输出每个系列的长度。我们将索引从 2,000 个值爆炸到超过 100 万个值:
>>> salary_add1 = salary1 + salary1
>>> len(salary1), len(salary2), len(salary_add), len(salary_add1)
(2000, 2000, 1175424, 2000)
它是如何工作的...
第 2 步乍一看好像创建了两个独特的对象,但实际上它创建了一个单独的对象,通过两个不同的变量名引用。表达式employee['BASE_SALARY']技术上创建了一个视图,而不是一个全新的副本。通过is运算符可以验证这一点。
在 pandas 中,视图不是新对象,而只是另一个对象的引用,通常是 DataFrame 的某个子集。这个共享对象可能会引发许多问题。
为了确保两个变量引用完全不同的对象,我们使用copy系列方法,并再次通过is运算符验证它们是不同的对象。第 4 步使用sort_index方法按照种族排序系列。第 5 步将这些不同的系列加在一起,产生某些结果。仅通过查看头部,仍然不清楚已产生了什么。
第 6 步将salary1与自身相加,展示两种不同 Series 相加的比较。所有 Series 的长度都已输出,我们清楚地看到series_add的值已经爆炸性地增长到超过一百万个值。由于索引不完全相同,每个唯一的索引值之间进行了笛卡尔积。这个例子极大地展示了在合并多个 Series 或 DataFrame 时,索引的影响有多大。
还有更多内容...
我们可以通过一点数学计算来验证salary_add的值的数量。由于所有相同索引值之间进行了笛卡尔积,我们可以将它们各自的计数的平方相加。即使是缺失的索引值,也会与自己进行笛卡尔积:
>>> index_vc = salary1.index.value_counts(dropna=False)
>>> index_vc
Black or African American 700
White 665
Hispanic/Latino 480
Asian/Pacific Islander 107
NaN 35
American Indian or Alaskan Native 11
Others 2
Name: RACE, dtype: int64
>>> index_vc.pow(2).sum()
1175424
使用不等索引填充值
当使用加法运算符将两个 Series 相加时,如果其中一个索引标签在另一个 Series 中不存在,结果值总是缺失的。Pandas 提供了add方法,可以选择填充缺失值。
准备工作
在这个例子中,我们使用add方法的fill_value参数将来自baseball数据集的多个 Series 相加,确保结果中没有缺失值,即使这些 Series 的索引不相等。
如何操作...
- 读取三个
baseball数据集,并将索引设置为playerID:
>>> baseball_14 = pd.read_csv('data/baseball14.csv',
index_col='playerID')
>>> baseball_15 = pd.read_csv('data/baseball15.csv',
index_col='playerID')
>>> baseball_16 = pd.read_csv('data/baseball16.csv',
index_col='playerID')
>>> baseball_14.head()
- 使用
difference索引方法来查找哪些索引标签出现在baseball_14中但不在baseball_15中,反之亦然:
>>> baseball_14.index.difference(baseball_15.index)
Index(['corpoca01', 'dominma01', 'fowlede01', 'grossro01',
'guzmaje01', 'hoeslj01', 'krausma01', 'preslal01',
'singljo02'], dtype='object', name='playerID')
>>> baseball_14.index.difference(baseball_16.index)
Index(['congeha01', 'correca01', 'gattiev01', 'gomezca01',
'lowrije01', 'rasmuco01', 'tuckepr01', 'valbulu01'],
dtype='object', name='playerID')
- 每个索引中都有相当多的独特球员。让我们找出每个球员在三年期间的总击球数。
H列包含击球次数:
>>> hits_14 = baseball_14['H']
>>> hits_15 = baseball_15['H']
>>> hits_16 = baseball_16['H']
>>> hits_14.head()
playerID
altuvjo01 225
cartech02 115
castrja01 103
corpoca01 40
dominma01 121
Name: H, dtype: int64
- 首先使用加法运算符将两个 Series 相加:
>>> (hits_14 + hits_15).head()
playerID
altuvjo01 425.0
cartech02 193.0
castrja01 174.0
congeha01 NaN
corpoca01 NaN
Name: H, dtype: float64
- 即使球员
congeha01和corpoca01在 2015 年有击球记录,他们的结果仍然是缺失的。让我们使用add方法及其参数fill_value来避免缺失值:
>>> hits_14.add(hits_15, fill_value=0).head()
playerID
altuvjo01 425.0
cartech02 193.0
castrja01 174.0
congeha01 46.0
corpoca01 40.0
Name: H, dtype: float64
- 我们通过再次链式调用
add方法将 2016 年的击球数据加到一起:
>>> hits_total = hits_14.add(hits_15, fill_value=0) \
.add(hits_16, fill_value=0)
>>> hits_total.head()
playerID
altuvjo01 641.0
bregmal01 53.0
cartech02 193.0
castrja01 243.0
congeha01 46.0
Name: H, dtype: float64
- 检查结果中是否存在缺失值:
>>> hits_total.hasnans
False
它是如何工作的...
add方法与加法运算符类似,但通过提供fill_value参数来代替不匹配的索引,从而提供更多灵活性。在这个问题中,将不匹配的索引值默认设置为 0 是有意义的,但你也可以使用其他任意数字。
有时候,每个 Series 中包含的索引标签对应着缺失值。在这个特定的例子中,当两个 Series 相加时,无论是否使用了fill_value参数,索引标签依然会对应缺失值。为了说明这一点,看看下面的例子,其中索引标签a在每个 Series 中都对应着缺失值:
>>> s = pd.Series(index=['a', 'b', 'c', 'd'],
data=[np.nan, 3, np.nan, 1])
>>> s
a NaN
b 3.0
c NaN
d 1.0
dtype: float64
>>> s1 = pd.Series(index=['a', 'b', 'c'], data=[np.nan, 6, 10])
>>> s1
a NaN
b 6.0
c 10.0
dtype: float64
>>> s.add(s1, fill_value=5)
a NaN
b 9.0
c 15.0
d 6.0
dtype: float64
还有更多内容...
本例展示了如何仅使用单一索引将多个 Series 相加。将 DataFrame 相加也是完全可行的。将 DataFrame 相加时,会先对齐索引和列,然后进行计算,并且对于不匹配的索引会生成缺失值。让我们从 2014 年的棒球数据集中选择几个列开始。
>>> df_14 = baseball_14[['G','AB', 'R', 'H']]
>>> df_14.head()
让我们从 2015 年的棒球数据集中也选择一些相同的列和一些不同的列:
>>> df_15 = baseball_15[['AB', 'R', 'H', 'HR']]
>>> df_15.head()
将两个 DataFrame 相加时,当行或列标签无法对齐时,会创建缺失值。使用 style 属性访问 highlight_null 方法,可以轻松查看缺失值的位置:
>>> (df_14 + df_15).head(10).style.highlight_null('yellow')
只有在两个 DataFrame 中都出现的 playerID 行才不会是缺失的。同样,AB、H 和 R 列是两个 DataFrame 中唯一出现的列。即使我们使用 add 方法并指定了 fill_value 参数,我们仍然会有缺失值。这是因为某些行和列的组合在输入数据中从未存在过。例如,playerID congeha01 与列 G 的交集。他只出现在没有 G 列的 2015 数据集中。因此,没有为其填充值:
>>> df_14.add(df_15, fill_value=0).head(10) \
.style.highlight_null('yellow')
从不同 DataFrame 中追加列
所有 DataFrame 都可以向自己添加新列。然而,像往常一样,每当一个 DataFrame 从另一个 DataFrame 或 Series 添加新列时,索引会先对齐,然后才会创建新列。
准备工作
本例使用 employee 数据集,在其中添加一个包含该员工所在部门最大薪水的新列。
如何操作...
- 导入
employee数据并在一个新 DataFrame 中选择DEPARTMENT和BASE_SALARY列:
>>> employee = pd.read_csv('data/employee.csv')
>>> dept_sal = employee[['DEPARTMENT', 'BASE_SALARY']]
- 按照每个部门的薪水对这个较小的 DataFrame 进行排序:
>>> dept_sal = dept_sal.sort_values(['DEPARTMENT', 'BASE_SALARY'],
ascending=[True, False])
- 使用
drop_duplicates方法保留每个DEPARTMENT的第一行:
>>> max_dept_sal = dept_sal.drop_duplicates(subset='DEPARTMENT')
>>> max_dept_sal.head()
- 将
DEPARTMENT列放入每个 DataFrame 的索引中:
>>> max_dept_sal = max_dept_sal.set_index('DEPARTMENT')
>>> employee = employee.set_index('DEPARTMENT')
- 现在索引已经包含匹配的值,我们可以向
employeeDataFrame 添加一个新列:
>>> employee['MAX_DEPT_SALARY'] = max_dept_sal['BASE_SALARY']
>>> employee.head()
- 我们可以使用
query方法验证结果,检查是否存在BASE_SALARY大于MAX_DEPT_SALARY的行:
>>> employee.query('BASE_SALARY > MAX_DEPT_SALARY')
它是如何工作的...
第 2 步和第 3 步找到每个部门的最大薪水。为了使自动索引对齐正常工作,我们将每个 DataFrame 的索引设置为部门。第 5 步之所以有效,是因为左侧 DataFrame employee 的每一行索引与右侧 DataFrame max_dept_sal 中的唯一一个索引对齐。如果 max_dept_sal 的索引中有重复的部门,则该操作将失败。
比如,假设我们在等号右侧使用一个有重复索引值的 DataFrame,会发生什么情况。我们使用 sample DataFrame 方法随机选择十行,且不进行替换:
>>> np.random.seed(1234)
>>> random_salary = dept_sal.sample(n=10).set_index('DEPARTMENT')
>>> random_salary
注意索引中有多个重复的部门。当我们尝试创建一个新列时,系统会提示一个错误,告知我们存在重复项。employee DataFrame 中至少有一个索引标签与 random_salary 中的两个或多个索引标签发生了连接:
>>> employee['RANDOM_SALARY'] = random_salary['BASE_SALARY']
ValueError: cannot reindex from a duplicate axis
还有更多……
不是所有等号左侧的索引都需要匹配,但最多只能有一个匹配项。如果左侧的 DataFrame 索引没有对应的对齐项,结果值将会缺失。我们来创建一个示例,展示这种情况的发生。我们将只使用 max_dept_sal Series 的前三行来创建一个新列:
>>> employee['MAX_SALARY2'] = max_dept_sal['BASE_SALARY'].head(3)
>>> employee.MAX_SALARY2.value_counts()
140416.0 29
100000.0 11
64251.0 5
Name: MAX_SALARY2, dtype: int64
>>> employee.MAX_SALARY2.isnull().mean()
.9775
操作成功完成,但只填充了三个部门的薪资数据。其他没有出现在 max_dept_sal Series 前三行中的部门,结果显示为缺失值。
突出显示每一列中的最大值
college 数据集包含许多数值型列,用于描述每所学校的不同指标。很多人对在某些指标上表现最佳的学校感兴趣。
准备工作
这个方案会发现每个数值型列的最大值,并对 DataFrame 进行样式设置,以突出显示信息,方便用户查看。
如何做……
- 使用机构名称作为索引来读取 college 数据集:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college.dtypes
CITY object
STABBR object
HBCU float64
MENONLY float64
...
PCTFLOAN float64
UG25ABV float64
MD_EARN_WNE_P10 object
GRAD_DEBT_MDN_SUPP object
Length: 26, dtype: object
- 除了
CITY和STABBR列外,所有其他列似乎都是数值型的。从前一步检查数据类型时,意外发现MD_EARN_WNE_P10和GRAD_DEBT_MDN_SUPP列的数据类型为对象,而非数值型。为了更好地了解这些列的值是什么,让我们检查它们的第一个值:
>>> college.MD_EARN_WNE_P10.iloc[0]
'30300'
>>> college.GRAD_DEBT_MDN_SUPP.iloc[0]
'33888'
- 这些值是字符串格式,但我们希望它们是数值型的。这意味着 Series 中可能会出现其他非数值字符。检查的一个方法是将这些列按降序排序,并检查前几行:
>>> college.MD_EARN_WNE_P10.sort_values(ascending=False).head()
INSTNM
Sharon Regional Health System School of Nursing PrivacySuppressed
Northcoast Medical Training Academy PrivacySuppressed
Success Schools PrivacySuppressed
Louisiana Culinary Institute PrivacySuppressed
Bais Medrash Toras Chesed PrivacySuppressed
Name: MD_EARN_WNE_P10, dtype: object
- 问题似乎出在一些学校对于这两列数据存在隐私问题。为了强制将这些列转换为数值型,可以使用 pandas 的
to_numeric函数:
>>> cols = ['MD_EARN_WNE_P10', 'GRAD_DEBT_MDN_SUPP']
>>> for col in cols:
college[col] = pd.to_numeric(college[col], errors='coerce')
>>> college.dtypes.loc[cols]
MD_EARN_WNE_P10 float64
GRAD_DEBT_MDN_SUPP float64
dtype: object
- 使用
select_dtypes方法筛选出只有数值型的列。这样可以排除STABBR和CITY列,因为在这个问题中,这些列没有最大值的意义:
>>> college_n = college.select_dtypes(include=[np.number])
>>> college_n.head()
- 利用数据字典,有几列仅包含二进制(0/1)值,这些列提供不了有用信息。为了程序化地找到这些列,我们可以创建布尔 Series,并通过
nunique方法查找所有具有两个唯一值的列:
>>> criteria = college_n.nunique() == 2
>>> criteria.head()
HBCU True
MENONLY True
WOMENONLY True
RELAFFIL True
SATVRMID False
dtype: bool
- 将这个布尔系列传递给列索引对象的索引运算符,并创建一个二进制列的列表:
>>> binary_cols = college_n.columns[criteria].tolist()
>>> binary_cols
['HBCU', 'MENONLY', 'WOMENONLY', 'RELAFFIL', 'DISTANCEONLY', 'CURROPER']
- 使用
drop方法移除二进制列:
>>> college_n2 = college_n.drop(labels=binary_cols, axis='columns')
>>> college_n2.head()
- 使用
idxmax方法查找每列最大值的索引标签:
>>> max_cols = college_n2.idxmax()
>>> max_cols
SATVRMID California Institute of Technology
SATMTMID California Institute of Technology
UGDS University of Phoenix-Arizona
UGDS_WHITE Mr Leon's School of Hair Design-Moscow
...
PCTFLOAN ABC Beauty College Inc
UG25ABV Dongguk University-Los Angeles
MD_EARN_WNE_P10 Medical College of Wisconsin
GRAD_DEBT_MDN_SUPP Southwest University of Visual Arts-Tucson
Length: 18, dtype: object
- 对
max_cols系列调用unique方法。这将返回一个包含唯一列名的ndarray:
>>> unique_max_cols = max_cols.unique()
>>> unique_max_cols[:5]
array(['California Institute of Technology',
'University of Phoenix-Arizona',
"Mr Leon's School of Hair Design-Moscow",
'Velvatex College of Beauty Culture',
'Thunderbird School of Global Management'], dtype=object)
- 使用
max_cols的值来选择只有最大值的学校的行,然后使用style属性突出显示这些值:
>>> college_n2.loc[unique_max_cols].style.highlight_max()
它是如何工作的...
idxmax方法非常强大,当索引有意义的标签时,它变得非常有用。没想到MD_EARN_WNE_P10和GRAD_DEBT_MDN_SUPP都是object数据类型。在导入时,如果列中至少包含一个字符串,Pandas 会将所有数字值强制转换为字符串。
通过检查第 2 步中的特定列值,我们清楚地看到这些列中包含了字符串。在第 3 步中,我们按降序排序,因为数字字符会首先出现。这将所有字母值提升到 Series 的顶部。我们发现了导致问题的PrivacySuppressed字符串。Pandas 能够使用to_numeric函数强制将仅包含数字字符的所有字符串转换为实际的数字数据类型。为了覆盖to_numeric在遇到无法转换的字符串时抛出错误的默认行为,必须将coerce传递给errors参数。这会强制所有非数字字符的字符串变为缺失值(np.nan)。
有几个列没有有用或有意义的最大值。它们在第 4 步到第 6 步中被移除。select_dtypes对于包含许多列的宽数据框非常有用。
在第 7 步中,idxmax遍历所有列,查找每列的最大值索引。它将结果输出为一个 Series。既有最高的 SAT 数学成绩,也有最高的 SAT 语文成绩的学校是加利福尼亚理工学院。东国大学洛杉矶校区的学生人数超过 25 岁的是最多的。
尽管idxmax提供的信息很有用,但它并没有给出相应的最大值。为此,我们从max_cols系列中的值中收集所有唯一的学校名称。
最后,在第 8 步中,我们使用.loc索引器根据索引标签选择行,索引标签在第一步中作为学校名称创建。这只筛选出具有最大值的学校。数据框有一个实验性的style属性,它本身有一些方法可以改变显示数据框的外观。突出显示最大值使结果更加清晰。
还有更多...
默认情况下,highlight_max方法会高亮显示每列的最大值。我们可以使用axis参数来高亮显示每行的最大值。在这里,我们只选择college数据集中的种族百分比列,并突出显示每个学校的最高百分比种族:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds = college.filter(like='UGDS_').head()
>>> college_ugds.style.highlight_max(axis='columns')
尝试在大型 DataFrame 上应用样式可能会导致 Jupyter 崩溃,这就是为什么样式只应用于 DataFrame 的头部。
参见
- Pandas 官方文档关于 DataFrame 样式的说明(
bit.ly/2hsZkVK)
使用方法链模拟idxmax
尝试自己实现一个内置的 DataFrame 方法是一个很好的练习。这种类型的模仿可以帮助你更深入地理解一些通常不会接触到的其他 pandas 方法。idxmax是一个挑战性很大的方法,仅使用本书中讲解过的方法来实现它比较困难。
准备工作
这个方法逐步链式调用基本方法,最终找到所有包含最大列值的行索引值。
如何实现……
- 加载
college数据集,并执行与前一个方法相同的操作,以获取我们感兴趣的数值列:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> cols = ['MD_EARN_WNE_P10', 'GRAD_DEBT_MDN_SUPP']
>>> for col in cols:
college[col] = pd.to_numeric(college[col], errors='coerce')
>>> college_n = college.select_dtypes(include=[np.number])
>>> criteria = college_n.nunique() == 2
>>> binary_cols = college_n.columns[criteria].tolist()
>>> college_n = college_n.drop(labels=binary_cols, axis='columns')
- 使用
max方法找到每列的最大值:
>>> college_n.max().head()
SATVRMID 765.0
SATMTMID 785.0
UGDS 151558.0
UGDS_WHITE 1.0
UGDS_BLACK 1.0
dtype: float64
- 使用
eq方法测试 DataFrame 中的每个值与其列的max值是否相等。默认情况下,eq方法会将 DataFrame 的列与传递的 Series 索引的标签对齐:
>>> college_n.eq(college_n.max()).head()
- 这个 DataFrame 中所有至少包含一个
True值的行必须包含一个列最大值。我们可以使用any方法来查找所有包含至少一个True值的行:
>>> has_row_max = college_n.eq(college_n.max()).any(axis='columns')
>>> has_row_max.head()
INSTNM
Alabama A & M University False
University of Alabama at Birmingham False
Amridge University False
University of Alabama in Huntsville False
Alabama State University False
dtype: bool
- 只有 18 列,这意味着
has_row_max中最多只有 18 个True值。让我们查找实际有多少个:
>>> college_n.shape
(7535, 18)
>>> has_row_max.sum()
401
- 这有点出乎意料,但事实证明,有些列有很多行等于最大值。这在许多百分比列中很常见,这些列的最大值为 1。
idxmax返回最大值的第一次出现。让我们退后一步,去掉any方法,查看步骤 3 的输出。我们改用cumsum方法来累积所有的True值。展示的是前后三行数据:
>>> college_n.eq(college_n.max()).cumsum()
- 有些列只有一个唯一的最大值,比如
SATVRMID和SATMTMID,而像UGDS_WHITE这样的列有很多最大值。109 所学校的本科生全部是白人。如果我们再链式调用一次cumsum方法,值 1 将在每列中只出现一次,并且它会是最大值的第一次出现:
>>> college_n.eq(college_n.max()).cumsum().cumsum()
- 现在我们可以使用
eq方法测试每个值是否等于 1,然后使用any方法查找包含至少一个True值的行:
>>> has_row_max2 = college_n.eq(college_n.max()) \
.cumsum() \
.cumsum() \
.eq(1) \
.any(axis='columns')
>>> has_row_max2.head()
INSTNM
Alabama A & M University False
University of Alabama at Birmingham False
Amridge University False
University of Alabama in Huntsville False
Alabama State University False
dtype: bool
- 测试
has_row_max2是否没有超过列数的True值:
>>> has_row_max2.sum()
16
- 我们需要所有
has_row_max2为True的机构。我们可以简单地在系列上使用布尔索引:
>>> idxmax_cols = has_row_max2[has_row_max2].index
>>> idxmax_cols
Index(['Thunderbird School of Global Management',
'Southwest University of Visual Arts-Tucson',
'ABC Beauty College Inc',
'Velvatex College of Beauty Culture',
'California Institute of Technology',
'Le Cordon Bleu College of Culinary Arts-San Francisco',
'MTI Business College Inc', 'Dongguk University-Los Angeles',
'Mr Leon's School of Hair Design-Moscow',
'Haskell Indian Nations University', 'LIU Brentwood',
'Medical College of Wisconsin', 'Palau Community College',
'California University of Management and Sciences',
'Cosmopolitan Beauty and Tech School',
'University of Phoenix-Arizona'], dtype='object', name='INSTNM')
- 这 16 个机构是至少包含一个列的首次最大值发生的索引。我们可以检查它们是否与通过
idxmax方法找到的相同:
>>> set(college_n.idxmax().unique()) == set(idxmax_cols)
True
它是如何工作的...
第一步通过将两列转换为数字并去除二进制列,复制了之前配方中的工作。在第二步中,我们找到每列的最大值。需要注意的是,pandas 会默默地丢弃它无法计算最大值的列。如果发生这种情况,那么第三步仍然会完成,但对于每个没有最大值的列,都会产生False值。
第四步使用any方法扫描每一行,寻找至少一个True值。任何包含至少一个True值的行都表示该列有一个最大值。在第五步中,我们将结果布尔系列相加,确定有多少行包含最大值。有些出乎意料的是,行数远远多于列数。第六步解释了为什么会发生这种情况。我们对第三步的输出进行累计求和,检测每列的最大值等于多少行的总数。
许多学院的学生群体中 100%属于单一种族。这是多行最大值出现的最大原因。如你所见,SAT 分数列和本科生人数列只有一行最大值,但多个种族列则有最大值的平局。
我们的目标是找到具有最大值的第一行。我们需要再次进行累计求和,这样每列只有一行等于 1。第 8 步将代码格式化为每行一个方法,并且如同第 4 步那样运行any方法。如果此步骤成功,那么我们应该没有比列数更多的True值。第 9 步验证了这一点。
为了验证我们找到的列与之前列中的idxmax相同,我们在has_row_max2上使用布尔选择。由于列的顺序会不同,因此我们将列名序列转换为集合,集合本身是无序的,用来比较相等性。
还有更多...
通过将索引操作符与匿名函数链式调用,实际上可以在一行代码中完成这个配方。这一小技巧去除了第 10 步的需要。我们可以对比直接使用idxmax方法和我们手动实现的时间差:
>>> %timeit college_n.idxmax().values
1.12 ms ± 28.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit college_n.eq(college_n.max()) \
.cumsum() \
.cumsum() \
.eq(1) \
.any(axis='columns') \
[lambda x: x].index
5.35 ms ± 55.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
不幸的是,我们的努力比内置的idxmax pandas 方法慢五倍,但无论其性能回退如何,许多富有创意和实用的解决方案都使用类似cumsum的累计方法与布尔系列一起,沿着轴找到连续的序列或特定的模式。
查找最常见的最大值
该大学数据集包含超过 7,500 所大学的八种不同种族的本科生人口比例。我们可以尝试找出每所学校本科生中种族比例最高的种族,并找到这个结果在整个数据集中的分布。我们可以回答一个问题,例如,有多少比例的学校白人学生比例高于其他任何种族?
准备就绪
在这个例子中,我们使用idxmax方法找到每所学校本科生中种族比例最高的种族,然后查找这些最大值的分布。
如何操作…
- 读取大学数据集并仅选择那些包含本科生种族比例信息的列:
>>> college = pd.read_csv('data/college.csv', index_col='INSTNM')
>>> college_ugds = college.filter(like='UGDS_')
>>> college_ugds.head()
- 使用
idxmax方法获取每行中种族比例最高的列名称:
>>> highest_percentage_race = college_ugds.idxmax(axis='columns')
>>> highest_percentage_race.head()
INSTNM
Alabama A & M University UGDS_BLACK
University of Alabama at Birmingham UGDS_WHITE
Amridge University UGDS_BLACK
University of Alabama in Huntsville UGDS_WHITE
Alabama State University UGDS_BLACK
dtype: object
- 使用
value_counts方法返回最大出现次数的分布:
>>> highest_percentage_race.value_counts(normalize=True)
UGDS_WHITE 0.670352
UGDS_BLACK 0.151586
UGDS_HISP 0.129473
UGDS_UNKN 0.023422
UGDS_ASIAN 0.012074
UGDS_AIAN 0.006110
UGDS_NRA 0.004073
UGDS_NHPI 0.001746
UGDS_2MOR 0.001164
dtype: float64
它是如何工作的……
这个例子的关键在于认识到这些列都代表相同的信息单位。我们可以相互比较这些列,这通常不是常见的情况。例如,直接比较 SAT 语文成绩和本科生人口比例是没有意义的。由于数据是以这种方式结构化的,我们可以对每一行数据应用idxmax方法来找到最大值所在的列。我们需要使用axis参数来修改其默认行为。
第 2 步完成了此操作并返回了一个 Series,现在我们可以简单地应用value_counts方法来返回分布。我们将True传递给normalize参数,因为我们关心的是分布(相对频率),而不是原始计数。
还有更多……
我们可能还想进一步探索并回答这个问题:对于那些黑人学生比例高于任何其他种族的学校,它们的第二高种族比例的分布是什么?
>>> college_black = college_ugds[highest_percentage_race == 'UGDS_BLACK']
>>> college_black = college_black.drop('UGDS_BLACK', axis='columns')
>>> college_black.idxmax(axis='columns').value_counts(normalize=True)
UGDS_WHITE 0.661228
UGDS_HISP 0.230326
UGDS_UNKN 0.071977
UGDS_NRA 0.018234
UGDS_ASIAN 0.009597
UGDS_2MOR 0.006718
UGDS_AIAN 0.000960
UGDS_NHPI 0.000960
dtype: float64
在应用本例中的相同方法之前,我们需要删除UGDS_BLACK列。有趣的是,似乎这些黑人学生比例较高的学校也倾向于有更高的西班牙裔学生比例。