Python-数据清理秘籍第二版-三-

32 阅读1小时+

Python 数据清理秘籍第二版(三)

原文:annas-archive.org/md5/2774e54b23314a6bebe51d6caf9cd592

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:使用可视化工具识别意外值

在上一章的多个配方中,我们已初步接触了可视化的内容。我们使用了直方图和 QQ 图来检查单一变量的分布,使用散点图查看两个变量之间的关系。但这仅仅是我们在 Matplotlib 和 Seaborn 库中丰富的可视化工具的冰山一角。熟悉这些工具及其似乎无穷无尽的功能,可以帮助我们发现通过标准描述性统计无法轻易察觉的模式和异常。

例如,箱形图是一个非常适合可视化超出特定范围的值的工具。这些工具可以通过分组箱形图或小提琴图进行扩展,允许我们比较不同数据子集之间的分布。我们还可以用散点图做更多的探索,而不仅仅是上一章所做的那样,甚至可以获得一些关于多变量关系的见解。直方图如果将多个直方图绘制在同一图表中,或者创建堆叠直方图时,也能提供更多的见解。本章将深入探讨这些功能。

本章的配方具体展示了以下内容:

  • 使用直方图检查连续变量的分布

  • 使用箱形图识别连续变量的异常值

  • 使用分组箱形图发现特定组中的意外值

  • 使用小提琴图同时检查分布形态和异常值

  • 使用散点图查看双变量关系

  • 使用折线图检查连续变量的趋势

  • 基于相关矩阵生成热图

技术要求

完成本章中的配方,你将需要 Pandas、Numpy、Matplotlib 和 Seaborn。我使用的是pandas 2.1.4,但代码在pandas 1.5.3或更高版本上也能运行。

本章中的代码可以从本书的 GitHub 仓库下载,https://github.com/PacktPublishing/Python-Data-Cleaning-Cookbook-Second-Edition

使用直方图检查连续变量的分布

统计学家用来理解单个变量分布的首选可视化工具是直方图。直方图将一个连续变量绘制在X轴上,分箱由研究者确定,频率分布则显示在Y轴上。

直方图提供了一个清晰且有意义的分布形态图示,包括集中趋势、偏度(对称性)、超额峰度(相对较胖的尾部)和分散程度。这对于统计检验至关重要,因为许多检验会假设变量的分布。此外,我们对数据值的预期应该以对分布形态的理解为指导。例如,来自正态分布的 90^(th)百分位值与来自均匀分布的值有着非常不同的含义。

我要求初学统计学的学生做的第一项任务之一是从一个小样本手动构建直方图。下一节课我们将学习箱型图。直方图和箱型图一起为后续分析提供了坚实的基础。在我的数据科学工作中,我尽量记得在数据导入和清理之后,尽快为所有感兴趣的连续变量构建直方图和箱型图。在本食谱中,我们将创建直方图,接下来的两个食谱中我们将创建箱型图。

准备工作

我们将使用Matplotlib库生成直方图。在 Matplotlib 中,一些任务可以快速且直接地完成,直方图就是其中之一。在本章中,我们将根据哪个工具更容易生成所需的图形,来在 Matplotlib 和基于 Matplotlib 的 Seaborn 之间切换。

我们还将使用statsmodels库。你可以使用pip install matplotlibpip install statsmodels通过 pip 安装 Matplotlib 和 statsmodels。

在本食谱中,我们将处理地球表面温度和 COVID-19 病例的数据。地表温度数据框包含每个天气站的一行数据。COVID-19 数据框包含每个国家的一行数据,其中包括总病例数和人口统计信息。

注意

地表温度数据框包含来自全球超过 12,000 个站点的 2023 年平均温度读数(单位:°C),尽管大多数站点位于美国。原始数据来自全球历史气候网络(GHCN)集成数据库。美国国家海洋和大气管理局提供该数据供公众使用,网址为www.ncei.noaa.gov/products/land-based-station/global-historical-climatology-network-monthly

我们的世界数据提供了供公众使用的 COVID 数据,网址为ourworldindata.org/covid-cases。本食谱中使用的数据是 2024 年 3 月 3 日下载的。

如何操作……

我们仔细查看了 2023 年各天气站的地表温度分布,以及每个国家每百万人的 COVID-19 总病例数。我们首先进行一些描述性统计,然后绘制 QQ 图、直方图和堆积直方图:

  1. 导入pandasmatplotlibstatsmodels库。

此外,加载地表温度和 COVID-19 病例的数据:

import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
landtemps = pd.read_csv("data/landtemps2023avgs.csv")
covidtotals = pd.read_csv("data/covidtotals.csv", parse_dates=["lastdate"])
covidtotals.set_index("iso_code", inplace=True) 
  1. 显示一些天气站的温度行。

latabs列是纬度的值,不包括北方或南方指示符;因此,位于大约 30 度北纬的埃及开罗和位于大约 30 度南纬的巴西阿雷格里港有相同的值:

landtemps[['station','country','latabs',
...   'elevation','avgtemp']].\
...   sample(10, random_state=1) 
 station        country  \
11924         WOLF_POINT_29_ENE  United States  
10671             LITTLE_GRASSY  United States  
10278  FLOWERY_TRAIL_WASHINGTON  United States  
8436                ROCKSPRINGS  United States  
1715               PETERBOROUGH         Canada  
5650          TRACY_PUMPING_PLT  United States  
335              NEPTUNE_ISLAND      Australia  
372                     EUDUNDA      Australia  
2987                  KOZHIKODE          India  
7588                      TRYON  United States  
       latabs  elevation  avgtemp 
11924      48        636        6 
10671      37      1,859       10 
10278      48        792        8 
8436       30        726       20 
1715       44        191        8 
5650       38         19       18 
335        35         32       16 
372        34        415       16 
2987       11          5       30 
7588       35        366       16 
  1. 显示一些描述性统计信息。

此外,查看偏度和峰度:

landtemps.describe() 
 latabs  elevation  avgtemp
count  12,137     12,137   12,137
mean       40        598       12
std        13        775        8
min         0       -350      -57
25%        35         78        6
50%        41        271       11
75%        47        824       17
max        90      9,999       34 
landtemps.avgtemp.skew() 
-0.3856060165979757 
landtemps.avgtemp.kurtosis() 
2.7939884544586033 
  1. 绘制平均温度的直方图。

此外,在整体均值处绘制一条线:

plt.hist(landtemps.avgtemp)
plt.axvline(landtemps.avgtemp.mean(), color='red', linestyle='dashed', linewidth=1)
plt.title("Histogram of Average Temperatures (Celsius)")
plt.xlabel("Average Temperature")
plt.ylabel("Frequency")
plt.show() 

这将产生以下直方图:

图 5.1:2019 年各气象站的平均温度直方图

  1. 运行 QQ 图,检查分布与正态分布的偏离情况。

注意到大部分温度分布沿着红线分布(如果分布完全正常,所有点都会落在红线上,但尾部明显偏离正常分布):

sm.qqplot(landtemps[['avgtemp']].sort_values(['avgtemp']), line='s')
plt.title("QQ Plot of Average Temperatures")
plt.show() 

这产生了以下 QQ 图:

图 5.2:各气象站的平均温度与正态分布的对比图

  1. 显示每百万的总 COVID-19 病例的偏度和峰度。

这来自 COVID-19 数据框,其中每一行代表一个国家:

covidtotals.total_cases_pm.skew() 
0.8349032460009967 
covidtotals.total_cases_pm.kurtosis() 
-0.4280595203351645 
  1. 做一个 COVID-19 病例数据的堆叠直方图。

从四个地区选择数据。(堆叠直方图如果类别超过这个数目会显得很杂乱。)定义一个 getcases 函数,返回一个地区内各国的 total_cases_pm Series。将这些 Series 传递给 hist 方法([getcases(k) for k in showregions])以创建堆叠直方图。注意,西欧与其他地区的差异,几乎所有每百万病例数超过 500,000 的国家都位于该地区:

showregions = ['Oceania / Aus','East Asia','Southern Africa',
...   'Western Europe']
def getcases(regiondesc):
...   return covidtotals.loc[covidtotals.\
...     region==regiondesc,
...     'total_cases_pm']
...
plt.hist([getcases(k) for k in showregions],\
...   color=['blue','mediumslateblue','plum','mediumvioletred'],\
...   label=showregions,\
...   stacked=True)
plt.title("Stacked Histogram of Cases Per Million for Selected Regions")
plt.xlabel("Cases Per Million")
plt.ylabel("Frequency")
plt.legend()
plt.show() 

这产生了以下堆叠直方图:

图 5.3:按地区分布的各国病例数的堆叠直方图,每百万人的不同病例数水平

  1. 在同一图形中显示多个直方图。

这允许不同的 xy 坐标轴值。我们需要循环遍历每个坐标轴,并为每个子图选择 showregions 中的一个不同地区:

fig, axes = plt.subplots(2, 2)
fig.suptitle("Histograms of COVID-19 Cases Per Million by Selected Regions")
axes = axes.ravel()
for j, ax in enumerate(axes):
...   ax.hist(covidtotals.loc[covidtotals.region==showregions[j]].\
...     total_cases_pm, bins=7)
...   ax.set_title(showregions[j], fontsize=10)
...   for tick in ax.get_xticklabels():
...     tick.set_rotation(45)
...
plt.tight_layout()
fig.subplots_adjust(top=0.88)
plt.show() 

这产生了以下直方图:

图 5.4:按每百万病例不同水平,按地区分布的国家数量的直方图

之前的步骤演示了如何通过直方图和 QQ 图来可视化连续变量的分布。

它是如何工作的……

步骤 4 显示了显示直方图是多么容易。这可以通过将一个 Series 传递给 Matplotlib 的 pyplot 模块中的 hist 方法来完成。(我们为 Matplotlib 使用了 plt 的别名。)我们也可以传递任何 ndarray,甚至是数据 Series 的列表。

我们还可以很好地访问图形及其坐标轴的属性。我们可以设置每个坐标轴的标签、刻度标记和刻度标签。我们还可以指定图例的内容及其外观。在本章中,我们将经常利用这一功能。

我们将多个 Series 传递给 步骤 7 中的 hist 方法,以生成堆叠直方图。每个 Series 是一个地区内各国的 total_cases_pm(每百万人的病例数)值。为了获得每个地区的 Series,我们为 showregions 中的每个项调用 getcases 函数。我们为每个 Series 选择颜色,而不是让系统自动分配颜色。我们还使用 showregions 列表选择图例的标签。

步骤 8中,我们首先指定要创建四个子图,分为两行两列。这就是 plt.subplots(2, 2) 的返回结果,它返回了图形和四个坐标轴。我们通过 for j, ax in enumerate(axes) 循环遍历坐标轴。在每次循环中,我们从 showregions 中选择一个不同的区域来绘制直方图。在每个坐标轴内,我们循环遍历刻度标签并改变其旋转角度。我们还调整子图的起始位置,以腾出足够的空间放置图形标题。请注意,在这种情况下,我们需要使用 suptitle 来添加标题,使用 title 只会将标题添加到子图中。

还有更多...

陆地温度数据并不是完全正态分布的,正如步骤 3-5中的直方图以及偏度和峰度测量所显示的那样。它偏向左侧(偏度为 -0.39),且尾部接近正态分布(峰度为 2.79,接近 3)。虽然存在一些极端值,但与数据集的整体规模相比,它们并不多。虽然数据分布不是完美的钟形曲线,但陆地温度数据框比 COVID-19 病例数据要容易处理得多。

COVID-19 的 每百万病例 变量的偏度和峰度显示它与正态分布相距较远。偏度为 0.83,峰度为 -0.43,表明该分布具有一定的正偏态,并且尾部比正态分布更窄。这在直方图中也得到了体现,即使我们按地区查看数据。大多数地区有许多国家的每百万病例数非常低,只有少数几个国家病例数很高。本章中的使用分组箱型图揭示某一特定组中的异常值示例显示几乎每个地区都有异常值。

如果你完成了本章中的所有示例,并且你对 Matplotlib 和 Seaborn 相对较为陌生,你会发现这两个库要么是灵活实用,要么是灵活得让人困惑。甚至很难选择一种策略并坚持下去,因为你可能需要以某种特定的方式设置图形和坐标轴才能获得你想要的可视化效果。在处理这些示例时,记住两件事是很有帮助的:第一,通常你需要创建一个图形和一个或多个子图;第二,主要的绘图函数通常是类似的,因此 plt.histax.hist 都能经常奏效。

使用箱型图识别连续变量的异常值

箱型图本质上是我们在第四章:使用单个变量识别异常值中所做工作的图形表示,在这一章中我们使用了四分位距IQR)的概念——即第一个四分位数和第三个四分位数之间的距离——来确定异常值。任何大于(1.5 * IQR)+ 第三个四分位数的值,或小于第一个四分位数 - (1.5 * IQR) 的值,都被认为是异常值。箱型图正是揭示了这一点。

准备工作

我们将使用关于 COVID-19 病例和死亡数据的累计数据,以及国家纵向调查NLS)数据。你需要安装 Matplotlib 库,以便在你的计算机上运行代码。

如何操作…

我们使用箱线图来展示学术能力评估测试SAT)分数、工作周数以及 COVID-19 病例和死亡数的形状和分布:

  1. 加载pandasmatplotlib库。

另外,加载 NLS 和 COVID-19 数据:

import pandas as pd
import matplotlib.pyplot as plt
nls97 = pd.read_csv("data/nls97f.csv", low_memory=False)
nls97.set_index("personid", inplace=True)
covidtotals = pd.read_csv("data/covidtotals.csv", parse_dates=["lastdate"])
covidtotals.set_index("iso_code", inplace=True) 
  1. 绘制 SAT 语言成绩的箱线图。

首先生成一些描述性统计。boxplot方法生成一个矩形,表示四分位数间距(IQR),即第一和第三四分位数之间的值。须根从该矩形延伸到 1.5 倍的 IQR。任何超过须根范围的值(我们标记为异常值阈值)都被视为异常值(我们使用annotate标记第一和第三四分位点、中位数以及异常值阈值):

nls97.satverbal.describe() 
count	1,406
mean	500
std	112
min	14
25%	430
50%	500
75%	570
max	800
Name: satverbal, dtype: float64 
plt.boxplot(nls97.satverbal.dropna(), labels=['SAT Verbal'])
plt.annotate('outlier threshold', xy=(1.05,780), xytext=(1.15,780), size=7, arrowprops=dict(facecolor='black', headwidth=2, width=0.5, shrink=0.02))
plt.annotate('3rd quartile', xy=(1.08,570), xytext=(1.15,570), size=7, arrowprops=dict(facecolor='black', headwidth=2, width=0.5, shrink=0.02))
plt.annotate('median', xy=(1.08,500), xytext=(1.15,500), size=7, arrowprops=dict(facecolor='black', headwidth=2, width=0.5, shrink=0.02))
plt.annotate('1st quartile', xy=(1.08,430), xytext=(1.15,430), size=7, arrowprops=dict(facecolor='black', headwidth=2, width=0.5, shrink=0.02))
plt.annotate('outlier threshold', xy=(1.05,220), xytext=(1.15,220), size=7, arrowprops=dict(facecolor='black', headwidth=2, width=0.5, shrink=0.02))
plt.title("Boxplot of SAT Verbal Score")
plt.show() 

这将生成以下箱线图:

图 5.5:带有四分位数范围和异常值标签的 SAT 语言成绩箱线图

  1. 接下来,显示一些关于工作周数的描述性统计:

    weeksworked = nls97.loc[:, ['highestdegree',
      'weeksworked20','weeksworked21']]
    weeksworked.describe() 
    
     weeksworked20  weeksworked21
    count          6,971          6,627
    mean              38             36
    std               21             18
    min                0              0
    25%               21             35
    50%               52             43
    75%               52             50
    max               52             52 
    
  2. 绘制工作周数的箱线图:

    plt.boxplot([weeksworked.weeksworked20.dropna(),
      weeksworked.weeksworked21.dropna()],
      labels=['Weeks Worked 2020','Weeks Worked 2021'])
    plt.title("Boxplots of Weeks Worked")
    plt.tight_layout()
    plt.show() 
    

这将生成以下箱线图:

图 5.6:并排显示的两个变量的箱线图

  1. 显示 COVID-19 数据的一些描述性统计。

创建一个标签列表(totvarslabels),供后续步骤使用:

totvars = ['total_cases','total_deaths',
...   'total_cases_pm','total_deaths_pm']
totvarslabels = ['cases','deaths',
...   'cases per million','deaths per million']
covidtotalsonly = covidtotals[totvars]
covidtotalsonly.describe() 
 total_cases  total_deaths  total_cases_pm  total_deaths_pm
count          231           231             231              231
mean     3,351,599        30,214         206,178            1,262
std     11,483,212       104,779         203,858            1,315
min              4             0             354                0
25%         25,672           178          21,822              141
50%        191,496         1,937         133,946              827
75%      1,294,286        14,150         345,690            1,998
max    103,436,829     1,127,152         763,475            6,508 
  1. 绘制每百万病例和死亡数的箱线图:

    fig, ax = plt.subplots()
    plt.title("Boxplots of COVID-19 Cases and Deaths Per Million")
    ax.boxplot([covidtotalsonly.total_cases_pm,covidtotalsonly.total_deaths_pm],\
    ...   labels=['cases per million','deaths per million'])
    plt.tight_layout()
    plt.show() 
    

这将生成以下箱线图:

图 5.7:并排显示的两个变量的箱线图

  1. 将箱线图作为单独的子图显示在一张图上。

当变量值差异很大时(例如 COVID-19 病例和死亡数),在一张图上查看多个箱线图会很困难。幸运的是,Matplotlib 允许我们在每张图上创建多个子图,每个子图可以使用不同的xy轴。我们还可以将数据以千为单位呈现,以提高可读性:

fig, axes = plt.subplots(2, 2)
fig.suptitle("Boxplots of COVID-19 Cases and Deaths in Thousands")
axes = axes.ravel()
for j, ax in enumerate(axes):
  ax.boxplot(covidtotalsonly.iloc[:, j]/1000, labels=[totvarslabels[j]])
plt.tight_layout()
fig.subplots_adjust(top=0.9)
plt.show() 

这将生成以下箱线图:

图 5.8:具有不同 y 轴的箱线图

箱线图是一种相对简单但极其有用的方式,用于查看变量的分布情况。它们使得在一个图形中同时可视化数据的分布、集中趋势和异常值变得非常容易。

它是如何工作的…

使用 Matplotlib 创建箱线图相当简单,正如步骤 2所示。只需将一个系列传递给 pyplot(我们使用plt别名)即可。我们调用 pyplot 的show方法来显示图形。这一步也展示了如何使用annotate向图形添加文本和符号。我们在步骤 4中通过将多个系列传递给 pyplot 来展示多个箱线图。

当尺度差异很大时(例如 COVID-19 结果数据:病例数、死亡数、每百万病例数和每百万死亡数),在单一图形中显示多个箱形图可能会很困难。步骤 7展示了处理这一问题的方法之一。我们可以在一张图上创建多个子图。首先,我们指定希望有四个子图,分布在两列两行。这就是plt.subplots(2, 2)所得到的结果,它返回一个图形和四个坐标轴。然后,我们可以遍历这些坐标轴,在每个上调用boxplot。挺巧妙的!

然而,由于某些极端值的存在,仍然很难看到病例和死亡数的四分位距(IQR)。在下一个示例中,我们去除一些极端值,以便更好地可视化剩余数据。

还有更多内容...

步骤 2中的 SAT 语文分数的箱形图表明该数据呈现相对正态分布。中位数接近 IQR 的中心位置。这并不令人惊讶,因为我们所做的描述性统计显示均值和中位数的值相同。然而,较低端的异常值空间远大于上端。(实际上,SAT 语文分数非常低似乎不可信,应该进行检查。)

步骤 4中的 2020 年和 2021 年工作的周数的箱形图显示了与 SAT 分数相比,变量分布差异非常大。中位数远高于均值,这表明存在负偏态。此外,注意到 2020 年值的分布上端没有胡须或异常值,因为中位数处于最大值附近或等于最大值。

另见

一些箱形图表明我们正在检查的数据并非呈正态分布。第四章中的识别异常值示例涵盖了一些正态分布检验。它还展示了如何更仔细地查看超出异常值阈值的值:即箱形图中的圆点。

使用分组箱形图发现特定组中的意外值

在前面的示例中,我们看到箱形图是检查连续变量分布的一个很好的工具。当我们想查看不同部分的数据集是否具有不同的分布时,箱形图也很有用,例如不同年龄组的薪资、按婚姻状况划分的子女数量,或不同哺乳动物物种的胎儿大小。分组箱形图是通过类别查看变量分布差异的一个方便直观的方式。

准备工作

我们将使用 NLS 和 COVID-19 病例数据。你需要在计算机上安装 Matplotlib 和 Seaborn 库,以便运行本示例中的代码。

如何操作...

我们生成了按最高学位获得情况分类的工作周数的描述性统计数据。然后,我们使用分组箱形图来可视化按学位类别划分的工作周数分布以及按地区划分的 COVID-19 病例分布。

  1. 导入pandasmatplotlibseaborn库:

    import pandas as pd
    import matplotlib.pyplot as plt
    import seaborn as sns
    nls97 = pd.read_csv("data/nls97f.csv", low_memory=False)
    nls97.set_index("personid", inplace=True)
    covidtotals = pd.read_csv("data/covidtotals.csv", 
                               parse_dates=["lastdate"])
    covidtotals.set_index("iso_code", inplace=True) 
    
  2. 查看每个学位获得水平的工作周数的中位数以及第一和第三四分位数值。

首先,定义一个返回这些值的函数作为 Series,然后使用apply对每个组调用该函数:

def gettots(x):
...   out = {}
...   out['min'] = x.min()
...   out['qr1'] = x.quantile(0.25)
...   out['med'] = x.median()
...   out['qr3'] = x.quantile(0.75)
...   out['max'] = x.max()
...   out['count'] = x.count()
...   return pd.Series(out)
...
nls97.groupby(['highestdegree'])['weeksworked21'].\
  apply(gettots).unstack() 
 min  qr1  med  qr3  max  count
highestdegree                                 
0\. None            0    0   39   49   52    487
1\. GED             0    7   42   50   52    853
2\. High School     0   27   42   50   52  2,529
3\. Associates      0   38   43   49   52    614
4\. Bachelors       0   40   43   50   52  1,344
5\. Masters         0   41   45   52   52    614
6\. PhD             0   41   44   49   52     59
7\. Professional    0   41   45   51   52    105 
  1. 绘制按最高学位划分的工作周数的箱线图。

使用 Seaborn 库绘制这些箱线图。首先,创建一个子图并命名为myplt。这使得稍后访问子图属性更加方便。使用boxplotorder参数按最高学位排序。注意,对于那些从未获得学位的人群,低端没有异常值或胡须,因为这些人群的 IQR 几乎覆盖了所有的值范围。25%分位数的值为 0:

myplt = \
  sns.boxplot(x='highestdegree',y='weeksworked21',
  data=nls97,
  order=sorted(nls97.highestdegree.dropna().unique()))
myplt.set_title("Boxplots of Weeks Worked by Highest Degree")
myplt.set_xlabel('Highest Degree Attained')
myplt.set_ylabel('Weeks Worked 2021')
myplt.set_xticklabels(myplt.get_xticklabels(), rotation=60, horizontalalignment='right')
plt.tight_layout()
plt.show() 

这将产生以下的箱线图:

图 5.9:按最高学位划分的工作周数的箱线图,包含 IQR 和异常值

  1. 查看按区域划分的每百万人病例的最小值、最大值、中位数,以及第一和第三四分位数值。

使用在步骤 2中定义的gettots函数:

covidtotals.groupby(['region'])['total_cases_pm'].\
  apply(gettots).unstack() 
 min     qr1     med     qr3     max  count
region                                                       
Caribbean   2,979 128,448 237,966 390,758 626,793  26
Central Af    434   2,888   4,232   9,948  29,614  11
Central Am  2,319  38,585  70,070 206,306 237,539   7
Central As  1,787   7,146  45,454  79,795 162,356   6
East Africa   660   2,018   4,062  71,435 507,765  15
East Asia   8,295  26,930  69,661 285,173 763,475  15
East. Eu  104,252 166,930 223,685 459,646 760,161  21
North Afr   4,649   6,058  34,141  74,463  93,343   5
North Am   60,412 108,218 214,958 374,862 582,158   4
Oceania/Aus 4,620  75,769 259,196 356,829 508,709  24
South Am   19,529 101,490 133,367 259,942 505,919  14
South As    5,630  11,959  31,772  80,128 473,167   9
South. Af   4,370  15,832  40,011  67,775 401,037  10
West Af       363   1,407   2,961   4,783 108,695  16
West Asia     354  78,447 123,483 192,995 512,388  16
West. Eu   32,178 289,756 465,940 587,523 750,727  32 
  1. 绘制按区域划分的每百万人病例的箱线图。

由于区域数量较多,需要翻转坐标轴。同时,绘制一个蜂群图,以显示按区域划分的国家数量。蜂群图为每个区域中的每个国家显示一个点。由于极端值,某些 IQR(四分位距)难以观察:

sns.boxplot(x='total_cases_pm', y='region', data=covidtotals)
sns.swarmplot(y="region", x="total_cases_pm", data=covidtotals, size=2, color=".3", linewidth=0)
plt.title("Boxplots of Total Cases Per Million by Region")
plt.xlabel("Cases Per Million")
plt.ylabel("Region")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show() 

这将产生以下的箱线图:

图 5.10:按区域划分的每百万人病例的箱线图和蜂群图,包含 IQR 和异常值

  1. 显示每百万人病例的最高值:

    highvalue = covidtotals.total_cases_pm.quantile(0.9)
    highvalue 
    
    512388.401 
    
    covidtotals.loc[covidtotals.total_cases_pm>=highvalue,\
      ['location','total_cases_pm']] 
    
     location  total_cases_pm
    iso_code                                          
    AND                         Andorra       601,367.7
    AUT                         Austria       680,262.6
    BRN                          Brunei       763,475.4
    CYP                          Cyprus       760,161.5
    DNK                         Denmark       583,624.9
    FRO                  Faeroe Islands       652,484.1
    FRA                          France       603,427.6
    GIB                       Gibraltar       628,882.7
    GRC                          Greece       540,380.1
    GLP                      Guadeloupe       513,528.3
    GGY                        Guernsey       557,817.1
    ISL                         Iceland       562,822.0
    ISR                          Israel       512,388.4
    JEY                          Jersey       599,218.4
    LVA                          Latvia       528,300.3
    LIE                   Liechtenstein       548,113.3
    LUX                      Luxembourg       603,439.5
    MTQ                      Martinique       626,793.1
    PRT                        Portugal       549,320.5
    SPM       Saint Pierre and Miquelon       582,158.0
    SMR                      San Marino       750,727.2
    SGP                       Singapore       531,183.8
    SVN                        Slovenia       639,407.7
    KOR                     South Korea       667,207.1 
    
  2. 重新绘制不包含极端值的箱线图:

    sns.boxplot(x='total_cases_pm', y='region', data=covidtotals.loc[covidtotals.total_cases_pm<highvalue])
    sns.swarmplot(y="region", x="total_cases_pm", data=covidtotals.loc[covidtotals.total_cases_pm<highvalue], size=3, color=".3", linewidth=0)
    plt.title("Total Cases Without Extreme Values")
    plt.xlabel("Cases Per Million")
    plt.ylabel("Region")
    plt.tight_layout()
    plt.show() 
    

这将产生以下的箱线图:

图 5.11:按区域划分的每百万人病例的箱线图(不包含极端值)

这些分组箱线图展示了按人口调整后的病例分布在各个区域之间的变化情况。

工作原理...

我们在本配方中使用 Seaborn 库来创建图形。我们也可以使用 Matplotlib。实际上,Seaborn 是基于 Matplotlib 构建的,扩展了 Matplotlib 的某些功能,并使一些操作变得更加简单。与 Matplotlib 的默认设置相比,它有时能生成更加美观的图形。

在创建包含多个箱线图的图形之前,最好先准备一些描述性信息。在步骤 2中,我们获取每个学位获得水平的第一个和第三个四分位数值,以及中位数。我们通过首先创建一个名为gettots的函数来实现,该函数返回一个包含这些值的序列。我们通过以下语句将gettots应用于数据框中的每个组:

nls97.groupby(['highestdegree'])['weeksworked21'].apply(gettots).unstack() 

groupby方法创建一个包含分组信息的数据框,并将其传递给apply函数。然后,gettots为每个组计算汇总值。unstack方法将返回的行进行重塑,将每组中的多行(每个汇总统计量一行)转换为每组一行,并为每个汇总统计量创建列。

Step 3中,我们为每个学位层次生成一个箱线图。当我们使用 Seaborn 的boxplot方法时,通常不需要为创建的子图对象命名。但在这一步中,我们将其命名为myplt,以便稍后可以轻松更改属性,例如刻度标签。我们使用set_xticklabels旋转x轴上的标签,以避免它们重叠。

Step 5中,我们翻转箱线图的轴,因为区域的层级比连续变量每百万病例数的刻度多。为此,我们将total_cases_pm作为第一个参数的值,而不是第二个。我们还做了一个 swarm plot,以便了解每个地区的观察数量(国家)。

极端值有时会使箱线图难以查看。箱线图显示异常值和四分位数间距(IQR),但当异常值是第三或第一四分位值的数倍时,IQR 矩形将非常小而难以查看。在Step 7中,我们删除所有大于或等于 512,388 的total_cases_pm值。这改善了可视化细节的呈现。

还有更多内容…

Step 3中的教育程度的周工作箱线图显示了工作周数的高变异性,这在单变量分析中并不明显。教育程度越低,工作周数的波动就越大。2021 年,持有高中以下学历的个体的工作周数存在相当大的变异性,而拥有大学学位的个体的变异性非常小。

这对于我们理解以工作周数衡量的异常值是非常相关的。例如,一个拥有大学学位的人工作了 20 周,算是一个异常值,但如果他们只有高中以下文凭,则不会被视为异常值。

每百万人口病例数的箱线图也邀请我们更加灵活地思考什么是异常值。例如,在北非的每百万人口病例数的异常值在整个数据集中并不算是高异常值。北非的最大值实际上低于西欧的第一四分位数值。

当我看箱线图时,我首先注意到的是中位数在 IQR 中的位置。如果中位数与中心完全不接近,我知道我在处理的不是正态分布的变量。它还让我对偏斜的方向有了很好的感觉。如果它靠近 IQR 的底部,意味着中位数比第三四分位数接近第一四分位数,那么存在正偏斜。比较东欧和西欧的箱线图。大量低值和少量高值使东欧的中位数接近西欧的第一四分位数值。

另请参见

第九章 解决混乱数据聚合中,我们更多地使用groupby。在第十一章 整理和重塑数据中,我们更多地使用stackunstack

使用小提琴图检查分布形状和异常值

小提琴图将直方图和箱线图结合在一张图中。它们显示了 IQR、中位数、须条,以及各个数值范围内观察值的频率。如果没有实际的小提琴图,很难想象这是如何做到的。我们使用与上一个食谱中箱线图相同的数据生成几个小提琴图,以便更容易理解它们的工作原理。

准备工作

我们将使用 NLS 数据。你需要在计算机上安装 Matplotlib 和 Seaborn 才能运行本食谱中的代码。

如何操作…

我们使用小提琴图来查看分布的范围和形态,并将它们显示在同一图表中。然后我们按组进行小提琴图绘制:

  1. 加载 pandasmatplotlibseaborn,以及 NLS 数据:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    nls97 = pd.read_csv("data/nls97f.csv", low_memory=False)
    nls97.set_index("personid", inplace=True) 
    
  2. 绘制 SAT verbal 分数的小提琴图:

    sns.violinplot(y=nls97.satverbal, color="wheat", orient="v")
    plt.title("Violin Plot of SAT Verbal Score")
    plt.ylabel("SAT Verbal")
    plt.text(0.08, 780, 'outlier threshold', horizontalalignment='center', size='x-small')
    plt.text(0.065, nls97.satverbal.quantile(0.75), '3rd quartile', horizontalalignment='center', size='x-small')
    plt.text(0.05, nls97.satverbal.median(), 'Median', horizontalalignment='center', size='x-small')
    plt.text(0.065, nls97.satverbal.quantile(0.25), '1st quartile', horizontalalignment='center', size='x-small')
    plt.text(0.08, 210, 'outlier threshold', horizontalalignment='center', size='x-small')
    plt.text(-0.4, 500, 'frequency', horizontalalignment='center', size='x-small')
    plt.show() 
    

这将产生以下小提琴图:

图 5.12:SAT verbal 分数的小提琴图,带有 IQR 和异常值阈值的标签

  1. 获取工作周数的描述性统计:

    nls97.loc[:, ['weeksworked20','weeksworked21']].describe() 
    
     weeksworked20  weeksworked21
    count          6,971          6,627
    mean              38             36
    std               21             18
    min                0              0
    25%               21             35
    50%               52             43
    75%               52             50
    max               52             52 
    
  2. 显示 2020 年和 2021 年的工作周数。

使用更面向对象的方法,以便更容易访问某些轴的属性。请注意,weeksworked 分布是双峰的,分布的顶部和底部都有峰值。另外,注意 2020 年和 2021 年的 IQR 非常不同:

myplt = sns.violinplot(data=nls97.loc[:, ['weeksworked20','weeksworked21']])
myplt.set_title("Violin Plots of Weeks Worked")
myplt.set_xticklabels(["Weeks Worked 2020","Weeks Worked 2021"])
plt.show() 

这将产生以下小提琴图:

图 5.13:显示两个变量分布范围和形态的小提琴图,按组并排展示

  1. 按性别和婚姻状况绘制工资收入的小提琴图。

首先,创建一个合并的婚姻状况列。指定性别为 x 轴,工资为 y 轴,新的合并婚姻状况列为 huehue 参数用于分组,这将与 x 轴已使用的任何分组一起添加。我们还指定 scale="count",以便根据每个类别中的观察值数量生成大小不同的小提琴图:

nls97["maritalstatuscollapsed"] = \
  nls97.maritalstatus.replace(['Married',
   'Never-married','Divorced','Separated',
   'Widowed'],\
  ['Married','Never Married','Not Married',
   'Not Married','Not Married'])
sns.violinplot(x="gender", y="wageincome20", hue="maritalstatuscollapsed",
  data=nls97, scale="count")
plt.title("Violin Plots of Wage Income by Gender and Marital Status")
plt.xlabel('Gender')
plt.ylabel('Wage Income 2020')
plt.legend(title="", loc="upper center", framealpha=0, fontsize=8)
plt.tight_layout()
plt.show() 

这将产生以下小提琴图:

图 5.14:显示两个不同组之间分布范围和形态的小提琴图

  1. 按最高学历绘制工作周数的小提琴图:

    nls97 = nls97.sort_values(['highestdegree'])
    myplt = sns.violinplot(x='highestdegree',y='weeksworked21', data=nls97)
    myplt.set_xticklabels(myplt.get_xticklabels(), rotation=60, horizontalalignment='right')
    myplt.set_title("Violin Plots of Weeks Worked by Highest Degree")
    myplt.set_xlabel('Highest Degree Attained')
    myplt.set_ylabel('Weeks Worked 2021')
    plt.tight_layout()
    plt.show() 
    

这将产生以下小提琴图:

图 5.15:按组显示分布范围和形态的小提琴图

这些步骤展示了小提琴图如何告诉我们 DataFrame 中连续变量的分布情况,以及它们在不同组之间可能的变化。

工作原理…

类似于箱型图,小提琴图显示了中位数、第一四分位数、第三四分位数和胡须。它们还显示了变量值的相对频率。(当小提琴图垂直显示时,相对频率就是某一点的宽度。)第 2 步中生成的小提琴图及其相关注释提供了一个很好的示例。从小提琴图中我们可以看出,SAT 语文成绩的分布与正态分布没有显著差异,除了低端的极端值外。最大隆起(最大宽度)出现在中位数处,从那里对称地下降。中位数与第一和第三四分位数的距离大致相等。

我们可以通过将一个或多个数据系列传递给violinplot方法来在 Seaborn 中创建小提琴图。我们也可以传递整个 DataFrame 中的一列或多列。在第 4 步中,我们这么做是因为我们希望绘制多个连续变量。

有时我们需要稍微调整图例,以使其既具有信息性,又不显得突兀。在第 5 步中,我们使用了以下命令来移除图例标题(因为从数值中已经很清楚),将图例放置在图形的最佳位置,并使框体透明(framealpha=0):

plt.legend(title="", loc="upper center", framealpha=0, fontsize=8) 

还有更多…

一旦你掌握了小提琴图,你就会欣赏到它在一张图上呈现的海量信息。我们可以了解分布的形状、中心趋势和分散程度。我们也可以轻松地展示不同数据子集的这些信息。

2020 年工作周数的分布与 2021 年工作周数的分布差异足够大,以至于让细心的分析师停下来思考。2020 年的四分位距(IQR)为 31(从 21 到 52),而 2021 年为 15(从 35 到 50)。(2020 年的工作周数分布可能受到疫情的影响。)

在检查第 5 步中生成的小提琴图时,揭示了一个关于工资收入分布的不寻常现象。已婚男性的收入在分布的顶部出现了集中现象,已婚女性也有类似的现象。这对于工资收入分布来说是相当不寻常的。事实证明,工资收入有一个上限为$380,288。这是我们在未来包括工资收入的分析中必须考虑的一个因素。

不同性别和婚姻状况的收入分布形状相似,都在中位数下方略有隆起,并具有延伸的正尾。四分位距的长度相对相似。然而,已婚男性的分布明显高于(或在选择的方向上偏右)其他组。

按学历划分的工作周数的提琴图显示出不同组别间的分布差异,正如我们在上一节的箱型图中发现的那样。更清晰的一点是,低学历人群的分布呈双峰态。在没有大学学位的人群中,工作周数较少的集中在低周数(例如工作 5 周或更少),而没有高中文凭的人群在 2021 年工作 5 周或更少的可能性几乎和工作 50 周或更多的可能性一样。

在本食谱中,我们仅使用了 Seaborn 来生成提琴图。Matplotlib 也可以生成提琴图,但 Matplotlib 中提琴图的默认图形与 Seaborn 的图形差别很大。

另请参阅

将本节中本食谱的提琴图与本章前面的直方图、箱型图和分组箱型图进行比较可能会有所帮助。

使用散点图查看双变量关系

我的感觉是,数据分析师依赖的图表中,散点图是最常见的图表之一,可能只有直方图例外。我们都非常习惯查看可以在二维平面上展示的关系。散点图捕捉了重要的现实世界现象(变量之间的关系),并且对大多数人来说非常直观。这使得它们成为我们可视化工具箱中不可或缺的一部分。

准备工作

本食谱需要MatplotlibSeaborn。我们将使用landtemps数据集,它提供了 2023 年全球 12,137 个气象站的平均温度数据。

如何操作……

我们在上一章提升了散点图技能,能够可视化更加复杂的关系。我们通过在一张图表中显示多个散点图、创建三维散点图以及显示多条回归线,来展示平均温度、纬度和海拔之间的关系:

  1. 加载pandasnumpymatplotlibseaborn

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    landtemps = pd.read_csv("data/landtemps2023avgs.csv") 
    
  2. 运行纬度(latabs)与平均温度的散点图:

    plt.scatter(x="latabs", y="avgtemp", data=landtemps)
    plt.xlabel("Latitude (N or S)")
    plt.ylabel("Average Temperature (Celsius)")
    plt.yticks(np.arange(-60, 40, step=20))
    plt.title("Latitude and Average Temperature in 2023")
    plt.show() 
    

结果如下散点图:

图 5.16:按平均温度绘制的纬度散点图

  1. 用红色显示高海拔点。

创建低海拔和高海拔的数据框。请注意,在每个纬度下,高海拔点通常较低(即温度较低):

low, high = landtemps.loc[landtemps.elevation<=1000], landtemps.loc[landtemps.elevation>1000]
plt.scatter(x="latabs", y="avgtemp", c="blue", data=low)
plt.scatter(x="latabs", y="avgtemp", c="red", data=high)
plt.legend(('low elevation', 'high elevation'))
plt.xlabel("Latitude (N or S)")
plt.ylabel("Average Temperature (Celsius)")
plt.title("Latitude and Average Temperature in 2023")
plt.show() 

结果如下散点图:

图 5.17:按平均温度和海拔绘制的纬度散点图

  1. 查看温度、纬度和海拔的三维图。

看起来在高海拔站点的纬度增加下,温度的下降趋势较为陡峭:

fig = plt.figure()
plt.suptitle("Latitude, Temperature, and Elevation in 2023")
ax = plt.axes(projection='3d')
ax.set_xlabel("Elevation")
ax.set_ylabel("Latitude")
ax.set_zlabel("Avg Temp")
ax.scatter3D(low.elevation, low.latabs, low.avgtemp, label="low elevation", c="blue")
ax.scatter3D(high.elevation, high.latabs, high.avgtemp, label="high elevation", c="red")
ax.legend()
plt.show() 

结果如下散点图:

图 5.18:按平均温度绘制的纬度和海拔的三维散点图

  1. 显示纬度与温度的回归线。

使用regplot获取回归线:

sns.regplot(x="latabs", y="avgtemp", color="blue", data=landtemps)
plt.title("Latitude and Average Temperature in 2023")
plt.xlabel("Latitude (N or S)")
plt.ylabel("Average Temperature")
plt.show() 

结果如下散点图:

图 5.19:纬度与平均温度的散点图,带有回归线

  1. 显示低海拔和高海拔车站的回归线。

这次我们使用lmplot,而不是regplot。这两种方法有类似的功能。不出所料,高海拔车站的回归线看起来具有较低的截距(即线与y轴的交点)以及更陡峭的负斜率:

landtemps['elevation'] = np.where(landtemps.elevation<=1000,'low','high')
sns.lmplot(x="latabs", y="avgtemp", hue="elevation", palette=dict(low="blue", high="red"),  facet_kws=dict(legend_out=False), data=landtemps)
plt.xlabel("Latitude (N or S)")
plt.ylabel("Average Temperature")
plt.yticks(np.arange(-60, 40, step=20))
plt.title("Latitude and Average Temperature in 2023")
plt.show() 

这将产生以下的散点图:

图 5.20:纬度与温度的散点图,带有不同海拔的回归线

  1. 显示一些位于低海拔和高海拔回归线上的车站。我们可以使用我们在步骤 3中创建的highlow DataFrame:

    high.loc[(high.latabs>38) & \
    ...   (high.avgtemp>=18),
    ...   ['station','country','latabs',
    ...   'elevation','avgtemp']] 
    
     station   country  latabs  elevation  avgtemp
    82     YEREVAN   Armenia      40      1,113       19
    3968  LAJES_AB  Portugal      39      1,016       19 
    
    low.loc[(low.latabs>47) & \
    ...   (low.avgtemp>=14),
    ...   ['station','country','latabs',
    ...   'elevation','avgtemp']] 
    
     station        country  latabs  elevation  avgtemp
    1026    COURTENAY_PUNTLEDGE         Canada      50         40       16
    1055    HOWE_SOUNDPAM_ROCKS         Canada      49          5       14
    1318  FORESTBURG_PLANT_SITE         Canada      52        663       18
    2573        POINTE_DU_TALUT         France      47         43       14
    2574      NANTES_ATLANTIQUE         France      47         27       14
    4449           USTORDYNSKIJ         Russia      53        526       17
    6810   WALKER_AH_GWAH_CHING  United States      47        430       20
    7050     MEDICINE_LAKE_3_SE  United States      48        592       16
    8736                 QUINCY  United States      47        392       14
    9914       WINDIGO_MICHIGAN  United States      48        213       16 
    

散点图是查看两个变量之间关系的好方法。这些步骤还展示了我们如何为数据的不同子集显示这些关系。

它是如何工作的……

我们只需提供xy的列名以及一个 DataFrame,就可以运行一个散点图。无需其他更多的操作。我们可以访问与运行直方图和箱线图时相同的图形和坐标轴属性——标题、坐标轴标签、刻度线和标签等。请注意,为了访问像坐标轴标签(而不是图形上的标签)这样的属性,我们使用set_xlabelsset_ylabels,而不是xlabelsylabels

3D 图稍微复杂一些。首先,我们将坐标轴的投影设置为3d——plt.axes(projection='3d'),就像我们在步骤 4中做的那样。然后我们可以为每个子图使用scatter3D方法。

由于散点图旨在说明回归变量(x变量)与因变量之间的关系,因此在散点图上看到最小二乘回归线是非常有帮助的。Seaborn 提供了两种方法来做到这一点:regplotlmplot。我通常使用regplot,因为它资源消耗较少。但有时,我需要lmplot的特性。我们在步骤 6中使用lmplot及其hue属性,为每个海拔水平生成单独的回归线。

步骤 7中,我们查看一些异常值:那些温度明显高于其所属组的回归线的车站。我们想要调查葡萄牙的LAJES_AB车站和亚美尼亚的YEREVAN车站的数据((high.latabs>38) & (high.avgtemp>=18))。这些车站的平均温度高于根据给定纬度和海拔水平预测的温度。

还有更多……

我们看到了纬度与平均温度之间的预期关系。随着纬度的增加,温度下降。但是,海拔是另一个重要因素。能够同时可视化所有三个变量有助于我们更容易识别异常值。当然,温度的其他影响因素也很重要,比如暖流。遗憾的是,这些数据在当前的数据集中没有。

散点图非常适合可视化两个连续变量之间的关系。通过一些调整,Matplotlib 和 Seaborn 的散点图工具也可以通过增加第三维度(当第三维度为分类变量时,通过颜色的创意使用,或通过改变点的大小)来提供对三个变量之间关系的理解(第四章使用线性回归识别具有高影响力的数据点的实例展示了这一点)。

另见

这是一本关于可视化的章节,着重于通过可视化识别意外值。但这些图形也迫切需要我们在第四章中进行的多变量分析,在数据子集中的异常值识别。特别是,线性回归分析和对残差的深入分析,对于识别异常值会很有帮助。

使用折线图检查连续变量的趋势

可视化连续变量在规律时间间隔内的值的典型方法是通过折线图,尽管有时对于较少的时间间隔,柱状图也可以使用。在本配方中,我们将使用折线图来展示变量趋势,并检查趋势的突变以及按组别的时间差异。

准备工作

本配方将处理每日的 COVID-19 病例数据。在之前的配方中,我们使用了按国家统计的总数。每日数据提供了每个国家每日新增病例和新增死亡人数,以及我们在其他配方中使用的相同人口统计变量。你需要安装 Matplotlib 才能运行本配方中的代码。

如何操作……

我们使用折线图来可视化每日 COVID-19 病例和死亡趋势。我们按地区创建折线图,并使用堆叠图来更好地理解一个国家如何影响整个地区的病例数量:

  1. 导入pandasmatplotlib以及matplotlib.dates和日期格式化工具:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib.dates as mdates
    from matplotlib.dates import DateFormatter
    coviddaily = pd.read_csv("data/coviddaily.csv", parse_dates=["casedate"]) 
    
  2. 查看几行 COVID-19 每日数据:

    coviddaily.sample(2, random_state=1).T 
    
     628             26980
    iso_code                    AND             PRT
    casedate             2020-03-15      2022-12-04
    location                Andorra        Portugal
    continent                Europe          Europe
    new_cases                     1           3,963
    new_deaths                    0              69
    population                79843        10270857
    pop_density                 164             112
    median_age                  NaN              46
    gdp_per_capita              NaN          27,937
    hosp_beds                   NaN               3
    vac_per_hund                NaN             NaN
    aged_65_older               NaN              22
    life_expectancy              84              82
    hum_dev_ind                   1               1
    region           Western Europe  Western Europe 
    
  3. 按天计算新增病例和死亡人数。

选择 2023 年 7 月 1 日到 2024 年 3 月 3 日之间的日期,然后使用groupby汇总每一天所有国家的病例和死亡数据:

coviddailytotals = \
  coviddaily.loc[coviddaily.casedate.\
    between('2023-07-01','2024-03-03')].\
  groupby(['casedate'])[['new_cases','new_deaths']].\
  sum().\
  reset_index() 
coviddailytotals.sample(7, random_state=1)
     casedate  new_cases  new_deaths
27 2024-01-07    181,487       1,353
3  2023-07-23    254,984         596
22 2023-12-03    282,319       1,535
18 2023-11-05    158,346       1,162
23 2023-12-10    333,155       1,658
17 2023-10-29    144,325         905
21 2023-11-26    238,282       1,287 
  1. 按天显示新增病例和新增死亡的折线图。

在不同的子图中显示病例和死亡数据:

fig = plt.figure()
plt.suptitle("New COVID-19 Cases and Deaths By Day Worldwide 2023-2024")
ax1 = plt.subplot(2,1,1)
ax1.plot(coviddailytotals.casedate, coviddailytotals.new_cases)
ax1.xaxis.set_major_formatter(DateFormatter("%b"))
ax1.set_xlabel("New Cases")
ax2 = plt.subplot(2,1,2)
ax2.plot(coviddailytotals.casedate, coviddailytotals.new_deaths)
ax2.xaxis.set_major_formatter(DateFormatter("%b"))
ax2.set_xlabel("New Deaths")
plt.tight_layout()
fig.subplots_adjust(top=0.88)
plt.show() 

这将产生以下的折线图:

图 5.21:全球 COVID-19 病例和死亡的每日趋势线

  1. 按天和地区计算新增病例和死亡人数:

    regiontotals = \
      coviddaily.loc[coviddaily.casedate.\
        between('2023-07-01','2024-03-03')].\
      groupby(['casedate','region'])\
        [['new_cases','new_deaths']].\
      sum().\
      reset_index()
    regiontotals.sample(7, random_state=1) 
    
     casedate         region  new_cases  new_deaths
    110 2023-08-13      West Asia      2,313          25
    147 2023-09-03   Central Asia        600           7
    494 2024-02-04  Oceania / Aus     12,594          38
    325 2023-11-19      East Asia     20,088          15
    189 2023-09-17    West Africa         85           0
    218 2023-10-01  South America      4,203          54
    469 2024-01-21  Oceania / Aus     17,503         129 
    
  2. 按选定地区显示新增病例的折线图。

遍历showregions中的各个地区。为每个地区绘制按天计算的new_cases总数的折线图。使用gca方法获取x轴并设置日期格式:

showregions = ['East Asia','Southern Africa',
...   'North America','Western Europe']
for j in range(len(showregions)):
...   rt = regiontotals.loc[regiontotals.\
...     region==showregions[j],
...     ['casedate','new_cases']]
...   plt.plot(rt.casedate, rt.new_cases,
...     label=showregions[j])
plt.title("New COVID-19 Cases By Day and Region in 2023-2024")
plt.gca().get_xaxis().set_major_formatter(DateFormatter("%b"))
plt.ylabel("New Cases")
plt.legend()
plt.show() 

这将产生以下的折线图:

图 5.22:按地区显示的 COVID-19 每日趋势线

  1. 使用堆叠图来更仔细地检查一个地区的趋势。

查看南美洲是否是由一个国家(巴西)推动了趋势线。为南美洲按天创建一个new_cases的 DataFrame(sa)。然后,将巴西的new_cases系列添加到sa DataFrame 中。接着,在sa DataFrame 中为南美洲的病例创建一个新的 Series,去除巴西的病例(sacasesnobr):

sa = \
  coviddaily.loc[(coviddaily.casedate.\
    between('2023-01-01','2023-10-31')) & \
    (coviddaily.region=='South America'),
    ['casedate','new_cases']].\
  groupby(['casedate'])\
    [['new_cases']].\
  sum().\
  reset_index().\
  rename(columns={'new_cases':'sacases'})
br = coviddaily.loc[(coviddaily.\
  location=='Brazil') & \
  (coviddaily.casedate. \
  between('2023-01-01','2023-10-31')),
  ['casedate','new_cases']].\
  rename(columns={'new_cases':'brcases'})
sa = pd.merge(sa, br, left_on=['casedate'], right_on=['casedate'], how="left")
sa.fillna({"sacases": 0},
  inplace=True)
sa['sacasesnobr'] = sa.sacases-sa.brcases
fig = plt.figure()
ax = plt.subplot()
ax.stackplot(sa.casedate, sa.sacases, sa.sacasesnobr, labels=['Brazil','Other South America'])
ax.xaxis.set_major_formatter(DateFormatter("%m-%d"))
plt.title("New COVID-19 Cases in South America in 2023")
plt.tight_layout()
plt.legend(loc="upper left")
plt.show() 

这将生成以下堆叠图:

图 5.23 – 巴西及南美洲其他地区每日病例的堆叠趋势

这些步骤展示了如何使用折线图查看变量随时间的变化趋势,并且如何在一张图上显示不同组的趋势。

它是如何工作的……

在绘制折线图之前,我们需要对每日 COVID-19 数据进行一些处理。在步骤 3中,我们使用groupby来汇总每个国家每天的新病例和死亡病例。在步骤 5中,我们使用groupby来汇总每个地区和每天的病例和死亡人数。

步骤 4中,我们使用plt.subplot(2,1,1)设置了第一个子图。这将为我们提供一个包含两行一列的图形。第三个参数中的1表示这个子图将是第一个,也就是最上面的子图。我们可以传入一个日期数据序列以及y轴的数值。到目前为止,这与我们在使用histscatterplotboxplotviolinplot方法时做的基本相同。但由于我们在处理日期数据,这里我们利用 Matplotlib 的日期格式化工具,并通过xaxis.set_major_formatter(DateFormatter("%b"))来设置只显示月份。由于我们在使用子图,我们使用set_xlabel而不是xlabel来指示我们想要的X轴标签。

我们在步骤 6中为四个选定的地区展示了折线图。我们通过对每个想要绘制的地区调用plot来实现这一点。我们本可以对所有地区进行绘制,但那样图形将过于复杂,难以查看。

步骤 7中,我们需要做一些额外的处理,将巴西的新病例从南美洲的病例中分离出来。完成这一操作后,我们可以绘制一个堆叠图,将南美洲的病例(不包括巴西)和巴西的病例分别显示。这张图表明,2023 年南美洲的新病例趋势主要受到巴西趋势的影响。

还有更多内容……

步骤 6中生成的图表揭示了一些潜在的数据问题。2023 年 4 月,东亚地区出现了一个不寻常的峰值。检查这些总数是否存在数据收集错误非常重要。

很难忽视不同地区趋势的差异。当然,这些差异背后有实际的原因。不同的曲线反映了我们所知道的不同国家和地区传播速度的现实情况。然而,值得探索趋势线方向或斜率的任何重大变化,以确保我们能确认数据的准确性。

另见

我们将在第九章更详细地介绍groupby,在第十章像在步骤 7中那样合并数据时,解决数据问题。

基于相关矩阵生成热图

两个变量之间的相关性是衡量它们一起移动程度的指标。相关系数为 1 意味着两个变量完全正相关。一个变量增大时,另一个也增大。值为-1 意味着它们完全负相关。一个变量增大时,另一个减小。相关系数为 1 或-1 很少发生,但大于 0.5 或小于-0.5 的相关可能仍具有意义。有几种测试可以告诉我们这种关系是否具有统计学意义(如皮尔逊、斯皮尔曼和肯德尔)。因为这是关于可视化的章节,我们将专注于查看重要的相关性。

准备工作

运行此配方中的代码需要安装 Matplotlib 和 Seaborn。两者都可以使用pip安装,命令为pip install matplotlibpip install seaborn

如何做...

我们首先展示 COVID-19 数据的部分相关矩阵,并展示一些关键关系的散点图。然后展示相关矩阵的热图,以可视化所有变量之间的相关性:

  1. 导入matplotlibseaborn,并加载 COVID-19 总数据:

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    covidtotals = pd.read_csv("data/covidtotals.csv", parse_dates=["lastdate"]) 
    
  2. 生成相关矩阵。

查看矩阵的一部分:

corr = covidtotals.corr(numeric_only=True)
corr[['total_cases','total_deaths',
  'total_cases_pm','total_deaths_pm']] 
 total_cases  total_deaths  \
total_cases             1.00          0.76  
total_deaths            0.76          1.00  
total_cases_pm          0.10          0.01  
total_deaths_pm         0.15          0.27  
population              0.70          0.47  
pop_density            -0.03         -0.04  
median_age              0.29          0.19  
gdp_per_capita          0.19          0.13  
hosp_beds               0.21          0.05  
vac_per_hund            0.02         -0.07  
aged_65_older           0.29          0.19  
life_expectancy         0.19          0.11  
hum_dev_ind             0.26          0.21  
                 total_cases_pm  total_deaths_pm 
total_cases                0.10             0.15 
total_deaths               0.01             0.27 
total_cases_pm             1.00             0.44 
total_deaths_pm            0.44             1.00 
population                -0.13            -0.07 
pop_density                0.19             0.02 
median_age                 0.74             0.69 
gdp_per_capita             0.66             0.29 
hosp_beds                  0.48             0.39 
vac_per_hund               0.24            -0.07 
aged_65_older              0.72             0.68 
life_expectancy            0.69             0.49 
hum_dev_ind                0.76             0.60 
  1. 显示中位年龄和国内生产总值(GDP)每人的散点图按百万人口病例。

指示我们希望子图共享y轴值,使用sharey=True

fig, axes = plt.subplots(1,2, sharey=True)
sns.regplot(x="median_age", y="total_cases_pm", data=covidtotals, ax=axes[0])
sns.regplot(x="gdp_per_capita", y="total_cases_pm", data=covidtotals, ax=axes[1])
axes[0].set_xlabel("Median Age")
axes[0].set_ylabel("Cases Per Million")
axes[1].set_xlabel("GDP Per Capita")
axes[1].set_ylabel("")
plt.suptitle("Scatter Plots of Age and GDP with Cases Per Million")
plt.tight_layout()
fig.subplots_adjust(top=0.92)
plt.show() 

这导致以下散点图:

图 5.24:中位年龄和 GDP 按百万人口病例并排的散点图

  1. 生成相关矩阵的热图:

    sns.heatmap(corr, xticklabels=corr.columns, yticklabels=corr.columns, cmap="coolwarm")
    plt.title('Heat Map of Correlation Matrix')
    plt.tight_layout()
    plt.show() 
    

这导致以下热图:

图 5.25:COVID-19 数据的热图,最强相关性显示为红色和桃色

热图是可视化我们 DataFrame 中所有关键变量如何相互相关的好方法。

它是如何工作的...

DataFrame 的corr方法生成所有数值变量与其他数值变量的相关系数。我们在步骤 2中显示了部分矩阵。在步骤 3中,我们制作了中位年龄按百万人口病例和 GDP 每人按百万人口病例的散点图。这些图表显示了当相关系数为 0.74 时(中位年龄与百万人口病例)和当相关系数为 0.66 时(GDP 每人与百万人口病例)。年龄较高或 GDP 较高的国家 tend to have higher cases per million of population.

热图提供了我们在步骤 2中创建的相关矩阵的可视化。所有红色方块表示相关系数为 1.0(即变量与自身的相关性)。桃色矩形表示显著的正相关,如中位数年龄、人均 GDP、人类发展指数和百万案例数之间的相关性。深色矩形表示强负相关关系,例如每十万人接种疫苗与每百万人死亡数之间的关系。

还有更多……

在进行探索性分析或统计建模时,我发现随时拥有一个相关矩阵或热图非常有帮助。当我能够牢记这些双变量关系时,我能更好地理解数据。

另见

我们将在第四章中更详细地讨论检查两个变量关系的工具,具体是识别双变量关系中的异常值和意外值这一部分,在数据子集中的异常值识别

总结

直方图、箱线图、散点图、小提琴图、折线图和热图都是理解变量分布的基本工具。散点图、小提琴图和热图还可以帮助我们更好地理解变量之间的关系,无论它们是连续的还是分类的。在本章中,我们使用了所有这些工具来创建可视化。在下一章中,我们将探讨如何在 pandas 中创建新的 Series,或修改现有 Series 中的值。

留下评论!

喜欢这本书吗?通过在亚马逊上留下评论,帮助像你一样的读者。扫描下面的二维码,获取你选择的免费电子书。

第六章:使用 Series 操作清理和探索数据

我们可以将本书前几章的配方视为本质上是诊断性的。我们导入了一些原始数据,然后生成了关于关键变量的描述性统计数据。这使我们对这些变量的值分布情况有了一个大致的了解,并帮助我们识别出异常值和意外值。然后,我们检查了变量之间的关系,以寻找模式,以及这些模式的偏差,包括逻辑上的不一致性。简而言之,到目前为止,我们的主要目标是弄清楚数据到底发生了什么。

然而,在数据探索和清理项目开始不久后,我们通常需要更改一些变量在某些观察中的初始值。例如,我们可能需要创建一个基于一个或多个其他列值的新列,或者我们可能想要改变某些值,这些值可能在某个范围内,比如小于 0,或者超过某个阈值,可能需要将它们设置为均值,或者设置为缺失。幸运的是,pandas Series 对象提供了大量用于操作数据值的方法。

本章的配方展示了如何使用 pandas 方法来更新 Series 的值,一旦我们确定了需要做什么。理想情况下,我们需要花时间仔细检查数据,在操作变量值之前。我们应该首先有集中趋势的度量、分布形状和范围的指示、相关性和可视化,然后再更新变量的值,或者基于它们创建新的变量。在更新变量值之前,我们还应该对异常值和缺失值有一个清晰的认识,了解它们如何影响汇总统计数据,并对填补新值或其他调整的初步计划有所准备。

完成这些工作后,我们就可以开始进行一些数据清理任务了。这些任务通常涉及直接操作 pandas Series 对象,无论是修改现有 Series 的值,还是创建一个新的 Series。这通常涉及条件性地更改值,仅更改满足特定标准的值,或者基于该 Series 的现有值或另一个 Series 的值,分配多个可能的值。

我们分配这些值的方式在很大程度上取决于 Series 的数据类型,无论是要更改的 Series 还是标准 Series。查询和清理字符串数据与处理日期或数值数据的任务有很大不同。对于字符串数据,我们通常需要评估某个字符串片段是否具有某个值,去除字符串中的一些无意义字符,或将其转换为数值或日期值。对于日期数据,我们可能需要查找无效的或超出范围的日期,甚至计算日期间隔。

幸运的是,pandas Series 提供了大量用于操作字符串、数值和日期值的工具。在本章中,我们将探讨一些最有用的工具。具体来说,我们将涵盖以下几个例子:

  • 从 pandas Series 中获取值

  • 显示 pandas Series 的汇总统计信息

  • 更改 Series 值

  • 有条件地更改 Series 值

  • 评估和清理字符串 Series 数据

  • 处理日期

  • 使用 OpenAI 进行 Series 操作

让我们开始吧!

技术要求

你需要 pandas、NumPy 和 Matplotlib 来完成本章中的例子。我使用的是 pandas 2.1.4,但该代码也可以在 pandas 1.5.3 或更高版本上运行。

本章节中的代码可以从本书的 GitHub 仓库下载,github.com/PacktPublishing/Python-Data-Cleaning-Cookbook-Second-Edition

从 pandas Series 中获取值

pandas Series 是一种一维的类数组结构,它采用 NumPy 数据类型。每个 Series 也有一个索引,这是数据标签的数组。如果在创建 Series 时没有指定索引,它将使用默认的 0 到 N-1 的索引。

创建 pandas Series 的方式有很多种,包括从列表、字典、NumPy 数组或标量中创建。在数据清洗工作中,我们最常通过选择 DataFrame 的列来访问数据 Series,使用属性访问(dataframename.columname)或括号符号(dataframename['columnname'])。属性访问不能用来设置 Series 的值,但括号符号可以用于所有 Series 操作。

在本例中,我们将探讨从 pandas Series 中获取值的几种方法。这些技术与我们在 第三章《数据度量》中介绍的从 pandas DataFrame 获取行的方法非常相似。

做好准备

在本例中,我们将使用 国家纵向调查 (NLS) 的数据,主要是关于每个受访者的高中 平均绩点 (GPA) 数据。

数据说明

国家青少年纵向调查由美国劳工统计局进行。该调查始于 1997 年,调查对象为 1980 年至 1985 年间出生的一群人,并且每年都会进行跟踪调查,直到 2023 年。调查数据可供公众使用,网址:nlsinfo.org

如何实现…

在这个例子中,我们使用括号操作符以及 lociloc 访问器来选择 Series 值。让我们开始吧:

  1. 导入所需的pandas和 NLS 数据:

    import pandas as pd
    nls97 = pd.read_csv("data/nls97f.csv", low_memory=False)
    nls97.set_index("personid", inplace=True) 
    

注意

是否使用括号运算符、loc 访问器或 iloc 访问器,主要是个人偏好问题。通常,当你知道要访问的行的索引标签时,使用 loc 访问器更方便。而当通过绝对位置引用行更为简便时,括号运算符或 iloc 访问器可能会是更好的选择。这个例子中展示了这一点。

  1. 从 GPA 总体列创建一个 Series。

使用 head 显示前几个值及其对应的索引标签。head 默认显示的值数量是 5。Series 的索引与 DataFrame 的索引相同,即 personid

gpaoverall = nls97.gpaoverall
type(gpaoverall) 
pandas.core.series.Series 
gpaoverall.head() 
personid
135335   3.09
999406   2.17
151672    NaN
750699   2.53
781297   2.43
Name: gpaoverall, dtype: float64 
gpaoverall.index 
Index([135335, 999406, 151672, 750699, 781297, 613800,
       403743, 474817, 530234, 351406,
       ...
       290800, 209909, 756325, 543646, 411195, 505861,
       368078, 215605, 643085, 713757],
      dtype='int64', name='personid', length=8984) 
  1. 使用括号运算符选择 GPA 值。

使用切片创建一个 Series,包含从第一个值到第五个值的所有值。注意我们得到了与 第 2 步head 方法相同的值。在 gpaoverall[:5] 中不包含冒号左边的值意味着它将从开头开始。gpaoverall[0:5] 将返回相同的结果。同样,gpaoverall[-5:] 显示的是从第五个到最后一个位置的值。这与 gpaoverall.tail() 返回的结果相同:

gpaoverall[:5] 
135335   3.09
999406   2.17
151672    NaN
750699   2.53
781297   2.43
Name: gpaoverall, dtype: float64 
gpaoverall.tail() 
personid
505861    NaN
368078    NaN
215605   3.22
643085   2.30
713757    NaN
Name: gpaoverall, dtype: float64 
gpaoverall[-5:] 
personid
505861    NaN
368078    NaN
215605   3.22
643085   2.30
713757    NaN
Name: gpaoverall, dtype: float64 
  1. 使用 loc 访问器选择值。

我们将一个索引标签(在此案例中为 personid 的值)传递给 loc 访问器以返回一个标量。如果我们传递一个索引标签列表,无论是一个还是多个,我们将得到一个 Series。我们甚至可以传递一个通过冒号分隔的范围。这里我们将使用 gpaoverall.loc[135335:151672]

gpaoverall.loc[135335]
3.09 
gpaoverall.loc[[135335]] 
personid
135335   3.09
Name: gpaoverall, dtype: float64 
gpaoverall.loc[[135335,999406,151672]] 
personid
135335   3.09
999406   2.17
151672    NaN
Name: gpaoverall, dtype: float64 
gpaoverall.loc[135335:151672] 
personid
135335   3.09
999406   2.17
151672    NaN
Name: gpaoverall, dtype: float64 
  1. 使用 iloc 访问器选择值。

ilocloc 的区别在于,它接受的是行号列表,而不是标签。它的工作方式类似于括号运算符切片。在这一步中,我们传递一个包含 0 的单项列表。然后,我们传递一个包含五个元素的列表 [0,1,2,3,4],以返回一个包含前五个值的 Series。如果我们传递 [:5] 给访问器,也会得到相同的结果:

gpaoverall.iloc[[0]] 
personid
135335   3.09
Name: gpaoverall, dtype: float64 
gpaoverall.iloc[[0,1,2,3,4]] 
personid
135335   3.09
999406   2.17
151672    NaN
750699   2.53
781297   2.43
Name: gpaoverall, dtype: float64 
gpaoverall.iloc[:5] 
personid
135335   3.09
999406   2.17
151672    NaN
750699   2.53
781297   2.43
Name: gpaoverall, dtype: float64 
gpaoverall.iloc[-5:] 
personid
505861    NaN
368078    NaN
215605   3.22
643085   2.30
713757    NaN
Name: gpaoverall, dtype: float64 

访问 pandas Series 值的这些方法——括号运算符、loc 访问器和 iloc 访问器——都有许多使用场景,特别是 loc 访问器。

它是如何工作的...

第 3 步 中,我们使用 [] 括号运算符执行了类似标准 Python 的切片操作来创建一个 Series。这个运算符允许我们根据位置轻松选择数据,方法是使用列表或通过切片符号表示的值范围。该符号的形式为 [start:end:step],如果没有提供 step,则假定 step 为 1。当 start 使用负数时,它表示从原始 Series 末尾开始的行数。

步骤 4中使用的loc访问器通过索引标签选择数据。由于personid是 Series 的索引,我们可以将一个或多个personid值的列表传递给loc访问器,以获取具有这些标签及相关 GPA 值的 Series。我们还可以将一个标签范围传递给访问器,它将返回一个包含从冒号左侧到右侧(包括)的索引标签的 GPA 值的 Series。例如,gpaoverall.loc[135335:151672]将返回personid135335151672之间(包括这两个值)的 GPA 值的 Series。

步骤 5所示,iloc访问器使用的是行位置,而不是索引标签。我们可以传递一个整数列表或使用切片表示法传递一个范围。

显示 pandas Series 的汇总统计数据

pandas 有许多生成汇总统计数据的方法。我们可以分别使用meanmedianmaxmin方法轻松获得 Series 的平均值、中位数、最大值或最小值。非常方便的describe方法将返回所有这些统计数据,以及其他一些数据。我们还可以使用quantile方法获得 Series 中任意百分位的值。这些方法可以应用于 Series 的所有值,或者仅用于选定的值。接下来的示例中将展示如何使用这些方法。

准备工作

我们将继续使用 NLS 中的总体 GPA 列。

如何操作...

让我们仔细看看整个数据框和选定行的总体 GPA 分布。为此,请按照以下步骤操作:

  1. 导入pandasnumpy并加载 NLS 数据:

    import pandas as pd
    import numpy as np
    nls97 = pd.read_csv("data/nls97f.csv",
    low_memory=False)
    nls97.set_index("personid", inplace=True) 
    
  2. 获取一些描述性统计数据:

    gpaoverall = nls97.gpaoverall
    gpaoverall.mean() 
    
    2.8184077281812145 
    
    gpaoverall.describe() 
    
    count	6,004.00
    mean	2.82
    std	0.62
    min	0.10
    25%	2.43
    50%	2.86
    75%	3.26
    max	4.17
    Name: gpaoverall, dtype: float64 
    
    gpaoverall.quantile(np.arange(0.1,1.1,0.1)) 
    
    0.10	2.02
    0.20	2.31
    0.30	2.52
    0.40	2.70
    0.50	2.86
    0.60	3.01
    0.70	3.17
    0.80	3.36
    0.90	3.60
    1.00	4.17
    Name: gpaoverall, dtype: float64 
    
  3. 显示 Series 子集的描述性统计数据:

    gpaoverall.loc[gpaoverall.between(3,3.5)].head(5) 
    
    personid
    135335   3.09
    370417   3.41
    684388   3.00
    984178   3.15
    730045   3.44
    Name: gpaoverall, dtype: float64 
    
    gpaoverall.loc[gpaoverall.between(3,3.5)].count() 
    
    1679 
    
    gpaoverall.loc[(gpaoverall<2) | (gpaoverall>4)].sample(5, random_state=10) 
    
    personid
    382527   1.66
    436086   1.86
    556245   4.02
    563504   1.94
    397487   1.84
    Name: gpaoverall, dtype: float64 
    
    gpaoverall.loc[gpaoverall>gpaoverall.quantile(0.99)].\
    ...   agg(['count','min','max']) 
    
    count     60.00
    min       3.98
    max       4.17
    Name: gpaoverall, dtype: float64 
    
  4. 测试所有值中的某一条件。

检查 GPA 值是否超过 4,并确保所有值都大于或等于 0。(我们通常期望 GPA 在 0 到 4 之间。)还要统计缺失值的数量:

(gpaoverall>4).any() # any person has GPA greater than 4 
True 
(gpaoverall>=0).all() # all people have GPA greater than or equal 0 
False 
(gpaoverall>=0).sum() # of people with GPA greater than or equal 0 
6004 
(gpaoverall==0).sum() # of people with GPA equal to 0 
0 
gpaoverall.isnull().sum() # of people with missing value for GPA 
2980 
  1. 基于不同列的值,显示 Series 的子集描述性统计数据。

显示 2020 年工资收入高于第 75 百分位的个人以及低于第 25 百分位的个人的高中平均 GPA:

nls97.loc[nls97.wageincome20 > nls97.wageincome20.quantile(0.75),'gpaoverall'].mean() 
3.0672837022132797 
nls97.loc[nls97.wageincome20 < nls97.wageincome20.quantile(0.25),'gpaoverall'].mean() 
2.6852676399026763 
  1. 显示包含分类数据的 Series 的描述性统计和频率:

    nls97.maritalstatus.describe() 
    
    count      6675
    unique     5
    top        Married
    freq       3068
    Name: maritalstatus, dtype: object 
    
    nls97.maritalstatus.value_counts() 
    
    Married           3068
    Never-married     2767
    Divorced           669
    Separated          148
    Widowed             23
    Name: maritalstatus, dtype: int64 
    

一旦我们有了 Series,我们可以使用 pandas 的多种工具来计算该 Series 的描述性统计数据。

它是如何工作的……

Series 的describe方法非常有用,因为它能很好地展示连续变量的集中趋势和分布情况。查看每个十分位的值通常也很有帮助。我们在步骤 2中通过将 0.1 到 1.0 之间的值列表传递给 Series 的quantile方法来获得这些信息。

我们可以在 Series 的子集上使用这些方法。在第 3 步中,我们获得了 GPA 值在 3 到 3.5 之间的计数。我们还可以根据与汇总统计量的关系来选择值;例如,gpaoverall>gpaoverall.quantile(0.99) 选择 GPA 值大于第 99^(百分位)的值。然后,我们通过方法链将结果 Series 传递给 agg 方法,返回多个汇总统计量(agg(['count','min','max']))。

有时,我们需要测试某个条件是否在 Series 中的所有值上都成立。anyall 方法对于此操作非常有用。any 当 Series 中至少有一个值满足条件时返回 True(例如,(gpaoverall>4).any())。all 当 Series 中所有值都满足条件时返回 True。当我们将测试条件与 sum 链接时((gpaoverall>=0).sum()),我们可以得到所有 True 值的计数,因为 pandas 在执行数值操作时将 True 视为 1。

(gpaoverall>4) 是一种简写方式,用于创建一个与 gpaoverall 具有相同索引的布尔 Series。当 gpaoverall 大于 4 时,其值为 True,否则为 False

(gpaoverall>4) 
personid
135335    False
999406    False
151672    False
750699    False
781297    False
505861    False
368078    False
215605    False
643085    False
713757    False
Name: gpaoverall, Length: 8984, dtype: bool 

我们有时需要为通过另一个 Series 过滤后的 Series 生成汇总统计数据。在第 5 步中,我们通过计算工资收入高于第三四分位数的个体的平均高中 GPA,以及工资收入低于第一四分位数的个体的平均高中 GPA,来完成这项工作。

describe 方法对于连续变量(如 gpaoverall)最为有用,但在与分类变量(如 maritalstatus)一起使用时也能提供有价值的信息(见第 6 步)。它返回非缺失值的计数、不同值的数量、最常出现的类别以及该类别的频率。

然而,在处理分类数据时,value_counts 方法更为常用。它提供了 Series 中每个类别的频率。

还有更多……

使用 Series 是 pandas 数据清理任务中的基础,数据分析师很快就会发现,本篇中使用的工具已成为他们日常数据清理工作流的一部分。通常,从初始数据导入阶段到使用 Series 方法(如 describemeansumisnullallany)之间不会间隔太长时间。

另见

本章我们只是浅尝辄止地介绍了数据聚合的内容。我们将在第九章《聚合时修复脏数据》中更详细地讨论这一点。

更改 Series 值

在数据清理过程中,我们经常需要更改数据 Series 中的值或创建一个新的 Series。我们可以更改 Series 中的所有值,或仅更改部分数据的值。我们之前用来从 Series 获取值的大部分技术都可以用来更新 Series 的值,尽管需要进行一些小的修改。

准备工作

在本道菜谱中,我们将处理 NLS 数据中的总体高中 GPA 列。

如何实现…

我们可以为所有行或选择的行更改 pandas Series 中的值。我们可以通过对其他 Series 执行算术操作和使用汇总统计来更新 Series。让我们来看看这个过程:

  1. 导入pandas并加载 NLS 数据:

    import pandas as pd
    nls97 = pd.read_csv("data/nls97f.csv",
    low_memory=False)
    nls97.set_index("personid", inplace=True) 
    
  2. 基于标量编辑所有值。

gpaoverall乘以 100:

nls97.gpaoverall.head() 
personid
135335   3.09
999406   2.17
151672    NaN
750699   2.53
781297   2.82
Name: gpaoverall, dtype: float64 
gpaoverall100 = nls97['gpaoverall'] * 100
gpaoverall100.head() 
personid
135335   309.00
999406   217.00
151672      NaN
750699   253.00
781297   243.00
Name: gpaoverall, dtype: float64 
  1. 使用索引标签设置值。

使用loc访问器通过索引标签指定要更改的值:

nls97.loc[[135335], 'gpaoverall'] = 3
nls97.loc[[999406,151672,750699],'gpaoverall'] = 0
nls97.gpaoverall.head() 
personid
135335   3.00
999406   0.00
151672   0.00
750699   0.00
781297   2.43
Name: gpaoverall, dtype: float64 
  1. 使用运算符在多个 Series 之间设置值。

使用+运算符计算孩子的数量,这个数量是住在家里的孩子和不住在家里的孩子的总和:

nls97['childnum'] = nls97.childathome + nls97.childnotathome
nls97.childnum.value_counts().sort_index() 
0.00		23
1.00		1364
2.00		1729
3.00		1020
4.00		420
5.00		149
6.00		55
7.00		21
8.00		7
9.00		1
12.00		2
Name: childnum, dtype: int64 
  1. 使用索引标签设置汇总统计值。

使用loc访问器从100061100292选择personid

nls97.loc[135335:781297,'gpaoverall'] = nls97.gpaoverall.mean()
nls97.gpaoverall.head() 
personid
135335   2.82
999406   2.82
151672   2.82
750699   2.82
781297   2.82
Name: gpaoverall, dtype: float64 
  1. 使用位置设置值。

使用iloc访问器按位置选择。可以使用整数或切片表示法(start:end:step)放在逗号左边,指示应该更改的行。逗号右边使用整数来选择列。gpaoverall列在第 16 个位置(由于列索引是从零开始的,所以是第 15 个位置):

nls97.iloc[0, 15] = 2
nls97.iloc[1:4, 15] = 1
nls97.gpaoverall.head() 
personid
135335   2.00
999406   1.00
151672   1.00
750699   1.00
781297   2.43
Name: gpaoverall, dtype: float64 
  1. 在筛选后设置 GPA 值。

将所有超过4的 GPA 值更改为4

nls97.gpaoverall.nlargest() 
personid
312410     4.17
639701     4.11
850001     4.10
279096     4.08
620216     4.07
Name: gpaoverall, dtype: float64 
nls97.loc[nls97.gpaoverall>4, 'gpaoverall'] = 4
nls97.gpaoverall.nlargest() 
personid
588585   4.00
864742   4.00
566248   4.00
990608   4.00
919755   4.00
Name: gpaoverall, dtype: float64 

前面的步骤展示了如何使用标量、算术操作和汇总统计值更新 Series 中的值。

它是如何工作的…

首先需要注意的是,在步骤 2中,pandas 将标量乘法进行了向量化。它知道我们想将标量应用于所有行。实质上,nls97['gpaoverall'] * 100创建了一个临时 Series,所有值都设置为 100,且拥有与gpaoverall Series 相同的索引。然后,它将gpaoverall与这个 100 值的 Series 相乘。这就是所谓的广播。

我们可以运用本章第一道菜谱中学到的许多内容,比如如何从 Series 中获取值,来选择特定的值进行更新。这里的主要区别是,我们使用 DataFrame 的lociloc访问器(nls97.loc),而不是 Series 的访问器(nls97.gpaoverall.loc)。这样做是为了避免令人头疼的SettingwithCopyWarning,该警告提示我们在 DataFrame 的副本上设置值。nls97.gpaoverall.loc[[135335]] = 3会触发这个警告,而nls97.loc[[135335], 'gpaoverall'] = 3则不会。

步骤 4中,我们看到了 pandas 如何处理两个或多个 Series 之间的数值操作。加法、减法、乘法和除法等操作就像我们在标准 Python 中对标量进行的操作,只不过是向量化的。(这得益于 pandas 的索引对齐功能。请记住,同一个 DataFrame 中的 Series 会有相同的索引。)如果你熟悉 NumPy,那么你已经有了对这个过程的良好理解。

还有更多内容…

注意到nls97.loc[[135335], 'gpaoverall']返回一个 Series,而nls97.loc[[135335], ['gpaoverall']]返回一个 DataFrame,这是很有用的:

type(nls97.loc[[135335], 'gpaoverall']) 
<class 'pandas.core.series.Series'> 
type(nls97.loc[[135335], ['gpaoverall']]) 
<class 'pandas.core.frame.DataFrame'> 

如果loc访问器的第二个参数是字符串,它将返回一个 Series。如果它是一个列表,即使列表只包含一个项,它也会返回一个 DataFrame。

对于我们在本案例中讨论的任何操作,记得关注 pandas 如何处理缺失值。例如,在步骤 4中,如果childathomechildnotathome缺失,那么操作将返回missing。我们将在下一章的识别和修复缺失值案例中讨论如何处理这种情况。

另请参阅

第三章测量你的数据,更详细地介绍了lociloc访问器的使用,特别是在选择行选择与组织列的案例中。

有条件地更改 Series 值

更改 Series 值通常比前一个案例所示的更为复杂。我们经常需要根据该行数据中一个或多个其他 Series 的值来设置 Series 值。当我们需要根据其他行的值设置 Series 值时,这会变得更加复杂;比如,某个人的先前值,或者一个子集的平均值。我们将在本案例和下一个案例中处理这些复杂情况。

准备工作

在本案例中,我们将处理土地温度数据和 NLS 数据。

数据说明

土地温度数据集包含了来自全球 12,000 多个站点在 2023 年的平均温度数据(单位:摄氏度),尽管大多数站点位于美国。该原始数据集来自全球历史气候网络集成数据库,已由美国国家海洋和大气管理局(NOAA)提供给公众使用,网址:www.ncei.noaa.gov/products/land-based-station/global-historical-climatology-network-monthly

如何做……

我们将使用 NumPy 的whereselect方法,根据该 Series 的值、其他 Series 的值和汇总统计来赋值。然后我们将使用lambdaapply函数来构建更复杂的赋值标准。我们开始吧:

  1. 导入pandasnumpy,然后加载 NLS 和土地温度数据:

    import pandas as pd
    import numpy as np
    nls97 = pd.read_csv("data/nls97f.csv", low_memory=False)
    nls97.set_index("personid", inplace=True)
    landtemps = pd.read_csv("data/landtemps2023avgs.csv") 
    
  2. 使用 NumPy 的where函数创建一个包含两个值的分类 Series。

我们来快速检查一下elevation值的分布情况:

landtemps.elevation.quantile(np.arange(0.2,1.1,0.2)) 
0.2      47.9
0.4     190.5
0.6     395.0
0.8   1,080.0
1.0   9,999.0
Name: elevation, dtype: float64 
landtemps['elevation_group'] = np.where(landtemps.elevation>landtemps.elevation.quantile(0.8),'High','Low')
landtemps.elevation_group = landtemps.elevation_group.astype('category')
landtemps.groupby(['elevation_group'],
  observed=False)['elevation'].\
  agg(['count','min','max']) 
 count     min    max
elevation_group                  
High              2428   1,080    9,999
Low               9709    -350    1,080 

注意

你可能已经注意到,我们将False值传递给了groupbyobserved属性。这是所有 pandas 版本在 2.1.0 之前的默认值。在后续的 pandas 版本中,groupby的默认observed=True。当observedTruegroupby中包含分类列时,只会显示观察到的值。这不会影响前一步的汇总统计结果。我仅在此处提到它,以提醒你即将发生的默认值变化。在本章其余部分我将忽略它。

  1. 使用 NumPy 的where方法创建一个包含三个值的分类 Series。

将 80^(th)百分位以上的值设置为'High',介于中位数和 80^(th)百分位之间的值设置为'Medium',剩余的值设置为'Low'

landtemps['elevation_group'] = \
  np.where(landtemps.elevation>
    landtemps.elevation.quantile(0.8),'High',
    np.where(landtemps.elevation>landtemps.elevation.\
      median(),'Medium','Low'))
landtemps.elevation_group = landtemps.elevation_group.astype('category')
landtemps.groupby(['elevation_group'])['elevation'].\
  agg(['count','min','max']) 
 count     min     max
elevation_group                        
High              2428    1,080   9,999
Low               6072     -350     271
Medium            3637      271   1,080 
  1. 使用 NumPy 的select方法来评估一系列条件。

设置一组测试条件,并为结果设置另一个列表。我们希望 GPA 低于 2 且没有学位的个人归为一个类别,GPA 较高但没有学位的个人归为第二个类别,拥有学位但 GPA 较低的个人归为第三个类别,剩余的个人归为第四个类别:

test = [(nls97.gpaoverall<2) &
  (nls97.highestdegree=='0\. None'),
   nls97.highestdegree=='0\. None',
   nls97.gpaoverall<2]
result = ['1\. Low GPA/No Dip','2\. No Diploma',
 '3\. Low GPA']
nls97['hsachieve'] = np.select(test, result, '4\. Did Okay')
nls97[['hsachieve','gpaoverall','highestdegree']].\
  sample(7, random_state=6) 
 hsachieve    gpaoverall     highestdegree
personid                                              
102951     1\. Low GPA/No Dip           1.4           0\. None
583984           4\. Did Okay           3.3    2\. High School
116430           4\. Did Okay           NaN     3\. Associates
859586           4\. Did Okay           2.3    2\. High School
288527           4\. Did Okay           2.7      4\. Bachelors
161698           4\. Did Okay           3.4      4\. Bachelors
943703         2\. No Diploma           NaN           0\. None 
nls97.hsachieve.value_counts().sort_index() 
hsachieve
1\. Low GPA/No Dip              90
2\. No Diploma                 787
3\. Low GPA                    464
4\. Did Okay                  7643
Name: count, dtype: int64 

虽然 NumPy 的select方法在相对简单的条件赋值中非常方便,但当赋值操作较为复杂时,它可能会变得难以使用。在这种情况下,我们可以使用自定义函数,而不是使用select

  1. 让我们使用apply和自定义函数来执行与前一步相同的 Series 值赋值操作。我们创建一个名为gethsachieve的函数,包含将值分配给新变量hsachieve2的逻辑。我们将此函数传递给apply并指定axis=1,以便将该函数应用于所有行。

我们将在下一步中使用相同的技术来处理一个更复杂的赋值操作,该操作基于更多的列和条件。

def gethsachieve(row):
  if (row.gpaoverall<2 and row.highestdegree=="0\. None"):
    hsachieve2 = "1\. Low GPA/No Dip"
  elif (row.highestdegree=="0\. None"):
    hsachieve2 = "2\. No Diploma"
  elif (row.gpaoverall<2):
    hsachieve2 = "3\. Low GPA"
  else:
    hsachieve2 = '4\. Did Okay'
  return hsachieve2
nls97['hsachieve2'] = nls97.apply(gethsachieve,axis=1)
nls97.groupby(['hsachieve','hsachieve2']).size() 
 hsachieve          hsachieve2      
1\. Low GPA/No Dip  1\. Low GPA/No Dip      90
2\. No Diploma      2\. No Diploma         787
3\. Low GPA         3\. Low GPA            464
4\. Did Okay        4\. Did Okay          7643
dtype: int64 

请注意,在这一步中,我们得到了与前一步中hsachieve相同的hsachieve2值。

  1. 现在,让我们使用apply和自定义函数进行更复杂的计算,该计算基于多个变量的值。

以下的getsleepdeprivedreason函数创建一个变量,用于根据调查对象可能因为什么原因导致每晚睡眠时间少于 6 小时来对其进行分类。我们根据 NLS 调查中关于受访者的就业状态、与受访者同住的孩子数、工资收入和最高完成的学业年级等信息来进行分类:

def getsleepdeprivedreason(row):
  sleepdeprivedreason = "Unknown"
  if (row.nightlyhrssleep>=6):
    sleepdeprivedreason = "Not Sleep Deprived"
  elif (row.nightlyhrssleep>0):
    if (row.weeksworked20+row.weeksworked21 < 80):
      if (row.childathome>2):
        sleepdeprivedreason = "Child Rearing"
      else:
        sleepdeprivedreason = "Other Reasons"
    else:
      if (row.wageincome20>=62000 or row.highestgradecompleted>=16):
        sleepdeprivedreason = "Work Pressure"
      else:
        sleepdeprivedreason = "Income Pressure"
  else:
    sleepdeprivedreason = "Unknown"
  return sleepdeprivedreason 
  1. 使用apply来对所有行运行该函数:

    nls97['sleepdeprivedreason'] = nls97.apply(getsleepdeprivedreason, axis=1)
    nls97.sleepdeprivedreason = nls97.sleepdeprivedreason.astype('category')
    nls97.sleepdeprivedreason.value_counts() 
    
    sleepdeprivedreason
    Not Sleep Deprived    5595
    Unknown               2286
    Income Pressure        453
    Work Pressure          324
    Other Reasons          254
    Child Rearing           72
    Name: count, dtype: int64 
    
  2. 如果我们只需要处理特定的列,并且不需要将它们传递给自定义函数,我们可以使用lambda函数与transform。让我们通过使用lambda在一个语句中测试多个列来尝试这个方法。

colenr列记录了每个人在每年 2 月和 10 月的入学状态。我们想要测试是否有任何一列大学入学状态的值为3. 4 年制大学。使用filter创建一个包含colenr列的 DataFrame。然后,使用transform调用一个 lambda 函数,测试每个colenr列的第一个字符。(我们只需查看第一个字符,判断它是否为 3。)接着将其传递给any,评估是否有任何(一个或多个)列的第一个字符为 3。(由于空间限制,我们只显示 2000 年至 2004 年之间的大学入学状态,但我们会检查 1997 年到 2022 年之间所有大学入学状态列的值。)这可以通过以下代码看到:

nls97.loc[[999406,750699],
  'colenrfeb00':'colenroct04'].T 
personid                 999406                750699
colenrfeb00     1\. Not enrolled       1\. Not enrolled
colenroct00   3\. 4-year college       1\. Not enrolled
colenrfeb01   3\. 4-year college       1\. Not enrolled
colenroct01   2\. 2-year college       1\. Not enrolled
colenrfeb02     1\. Not enrolled     2\. 2-year college
colenroct02   3\. 4-year college       1\. Not enrolled
colenrfeb03   3\. 4-year college       1\. Not enrolled
colenroct03   3\. 4-year college       1\. Not enrolled
colenrfeb04   3\. 4-year college       1\. Not enrolled
colenroct04   3\. 4-year college       1\. Not enrolled 
nls97['baenrollment'] = nls97.filter(like="colenr").\
...   transform(lambda x: x.str[0:1]=='3').\
...   any(axis=1)
nls97.loc[[999406,750699], ['baenrollment']].T 
personid      999406  750699
baenrollment    True   False 
nls97.baenrollment.value_counts() 
baenrollment
False    4987
True     3997
Name: count, dtype: int64 

上述步骤展示了几种我们可以用来有条件地设置 Series 值的技巧。

它是如何工作的……

如果你曾在 SQL 或 Microsoft Excel 中使用过if-then-else语句,那么 NumPy 的where对你应该是熟悉的。它的形式是where(测试条件,True时的表达式,False时的表达式)。在第 2 步中,我们测试了每行的海拔值是否大于 80^(百分位数)的值。如果为True,则返回'High';否则返回'Low'。这是一个基本的if-then-else结构。

有时,我们需要将一个测试嵌套在另一个测试中。在第 3 步中,我们为海拔创建了三个组:高,中和低。我们在False部分(第二个逗号之后)没有使用简单的语句,而是使用了另一个where语句。这将它从else语句变成了else if语句。它的形式是where(测试条件,True时的语句,where(测试条件,True时的语句,False时的语句))。

当然,可以添加更多嵌套的where语句,但并不推荐这样做。当我们需要评估一个稍微复杂一些的测试时,NumPy 的select方法非常有用。在第 4 步中,我们将测试的列表以及该测试的结果列表传递给了select。我们还为没有任何测试为True的情况提供了一个默认值4. Did Okay。当多个测试为True时,会使用第一个为True的测试。

一旦逻辑变得更加复杂,我们可以使用apply。DataFrame 的apply方法可以通过指定axis=1将 DataFrame 的每一行传递给一个函数。第 5 步演示了如何使用apply和用户定义的函数复现与第 4 步相同的逻辑。

第 6 步第 7 步中,我们创建了一个 Series,基于工作周数、与受访者同住的子女数量、工资收入和最高学历来分类缺乏睡眠的原因。如果受访者在 2020 年和 2021 年大部分时间没有工作,且有两个以上的孩子与其同住,则sleepdeprivedreason被设置为“育儿”。如果受访者在 2020 年和 2021 年大部分时间没有工作,且有两个或更少的孩子与其同住,则sleepdeprivedreason被设置为“其他原因”。如果受访者在 2020 年和 2021 年大部分时间有工作,则如果他们有高薪或完成了四年的大学学业,sleepdeprivedreason为“工作压力”,否则为“收入压力”。当然,这些分类有些人为,但它们确实展示了如何通过函数基于其他 Series 之间的复杂关系来创建 Series。

第 8 步中,我们使用了transform调用一个 lambda 函数,测试每个大学入学值的第一个字符是否是 3。但首先,我们使用filter方法从 DataFrame 中选择所有的大学入学列。我们本可以将lambda函数与apply搭配使用以实现相同的结果,但transform通常更高效。

你可能注意到,我们在第 2 步第 3 步中创建的新 Series 的数据类型被更改为category。这个新 Series 最初是object数据类型。我们通过将类型更改为category来减少内存使用。

我们在第 2 步中使用了另一个非常有用的方法,虽然是有点偶然的。landtemps.groupby(['elevation_group'])创建了一个 DataFrame 的groupby对象,我们将其传递给一个聚合(agg)函数。这样我们就可以获得每个elevation_group的计数、最小值和最大值,从而验证我们的分组分类是否按预期工作。

还有更多……

自从我有一个数据清理项目没有涉及 NumPy 的whereselect语句,或者lambdaapply语句以来,已经有很长一段时间了。在某些时候,我们需要基于一个或多个其他 Series 的值来创建或更新一个 Series。熟练掌握这些技术是个好主意。

每当有一个内置的 pandas 函数能够完成我们的需求时,最好使用它,而不是使用applyapply的最大优点是它非常通用且灵活,但也正因为如此,它比优化过的函数更占用资源。然而,当我们想要基于现有 Series 之间复杂的关系创建一个 Series 时,它是一个很好的工具。

执行第 6 步第 7 步的另一种方式是将一个 lambda 函数添加到apply中。这会产生相同的结果:

def getsleepdeprivedreason(childathome, nightlyhrssleep, wageincome, weeksworked20, weeksworked21, highestgradecompleted):
...   sleepdeprivedreason = "Unknown"
...   if (nightlyhrssleep>=6):
...     sleepdeprivedreason = "Not Sleep Deprived"
...   elif (nightlyhrssleep>0):
...     if (weeksworked16+weeksworked17 < 80):
...       if (childathome>2):
...         sleepdeprivedreason = "Child Rearing"
...       else:
...         sleepdeprivedreason = "Other Reasons"
...     else:
...       if (wageincome>=62000 or highestgradecompleted>=16):
...         sleepdeprivedreason = "Work Pressure"
...       else:
...         sleepdeprivedreason = "Income Pressure"
...   else:
...     sleepdeprivedreason = "Unknown"
...   return sleepdeprivedreason
...
nls97['sleepdeprivedreason'] = nls97.apply(lambda x: getsleepdeprivedreason(x.childathome, x.nightlyhrssleep, x.wageincome, x.weeksworked16, x.weeksworked17, x.highestgradecompleted), axis=1) 

这种方法的一个优点是,它更清晰地显示了哪些 Series 参与了计算。

另请参见

我们将在第九章《聚合时修复杂乱数据》中详细讲解 DataFrame 的groupby对象。我们在第三章《了解你的数据》中已经探讨了多种选择 DataFrame 列的技术,包括filter

评估和清理字符串 Series 数据

Python 和 pandas 中有许多字符串清理方法。这是件好事。由于存储在字符串中的数据种类繁多,因此在进行字符串评估和操作时,拥有广泛的工具非常重要:当按位置选择字符串片段时,当检查字符串是否包含某个模式时,当拆分字符串时,当测试字符串长度时,当连接两个或更多字符串时,当改变字符串大小写时,等等。在本食谱中,我们将探索一些最常用于字符串评估和清理的方法。

准备工作

在本食谱中,我们将使用 NLS 数据。(实际上,NLS 数据对于这个食谱来说有点过于干净。为了演示如何处理带有尾随空格的字符串,我在maritalstatus列的值后添加了尾随空格。)

如何实现...

在本食谱中,我们将执行一些常见的字符串评估和清理任务。我们将使用containsendswithfindall来分别搜索模式、尾随空格和更复杂的模式。

我们还将创建一个处理字符串值的函数,在将值分配给新 Series 之前,使用replace进行更简单的处理。让我们开始吧:

  1. 导入pandasnumpy,然后加载 NLS 数据:

    import pandas as pd
    import numpy as np
    nls97 = pd.read_csv("data/nls97ca.csv", low_memory=False)
    nls97.set_index("personid", inplace=True) 
    
  2. 测试字符串中是否存在某个模式。

使用contains来检查govprovidejobs(政府应该提供就业)响应中的“绝对不”与“可能不”值。在where调用中,首先处理缺失值,确保它们不会出现在第一个else分支中(即第二个逗号后的部分):

nls97.govprovidejobs.value_counts() 
2\. Probably           617
3\. Probably not       462
1\. Definitely         454
4\. Definitely not     300
Name: govprovidejobs, dtype: int64 
nls97['govprovidejobsdefprob'] = \
  np.where(nls97.govprovidejobs.isnull(),
    np.nan,
      np.where(nls97.govprovidejobs.str.\
      contains("not"),"No","Yes"))
pd.crosstab(nls97.govprovidejobs, nls97.govprovidejobsdefprob) 
govprovidejobsdefprob       No       Yes
govprovidejobs                          
1\. Definitely                0       454
2\. Probably	                0       617
3\. Probably not            462         0
4\. Definitely not          300         0 
  1. 处理字符串中的前导或尾随空格。

创建一个永婚状态的 Series。首先,检查maritalstatus的值。注意有两个表示从未结婚的异常值。它们是“Never-married”后有一个额外的空格,而其他“Never-married”的值则没有尾随空格。使用startswithendswith分别测试是否有前导空格或尾随空格。使用strip去除尾随空格后再测试永婚状态。strip去除前导和尾随空格(lstrip去除前导空格,rstrip去除尾随空格,所以在这个例子中,rstrip也能起作用):

nls97.maritalstatus.value_counts() 
Married           3066
Never-married     2764
Divorced           663
Separated          154
Widowed             23
Never-married        2
Name: count, dtype: int64 
nls97.maritalstatus.str.startswith(' ').any() 
False 
nls97.maritalstatus.str.endswith(' ').any() 
True 
nls97['evermarried'] = \
  np.where(nls97.maritalstatus.isnull(),np.nan,
    np.where(nls97.maritalstatus.str.\
      strip()=="Never-married","No","Yes"))
pd.crosstab(nls97.maritalstatus, nls97.evermarried) 
evermarried        No    Yes
maritalstatus             
Divorced            0    663
Married             0   3066
Never-married    2764      0
Never-married       2      0
Separated           0    154
Widowed             0     23 
  1. 使用isin将字符串值与值列表进行比较:

    nls97['receivedba'] = \
      np.where(nls97.highestdegree.isnull(),np.nan,
        np.where(nls97.highestdegree.str[0:1].\
          isin(['4','5','6','7']),"Yes","No"))
    pd.crosstab(nls97.highestdegree, nls97.receivedba) 
    
    receivedba             No      Yes
    highestdegree                  
    0\. None               953        0
    1\. GED               1146        0
    2\. High School       3667        0
    3\. Associates         737        0
    4\. Bachelors            0     1673
    5\. Masters              0      603
    6\. PhD                  0       54
    7\. Professional         0      120 
    

我们有时需要找出字符串中特定字符的位置。这有时是因为我们需要获取该点之前或之后的文本,或者以不同方式处理这些文本。让我们用之前处理过的“最高学历”列来尝试。我们将创建一个新列,该列不包含数字前缀。例如,2. 高中将变为高中

  1. 使用find获取highestdegree值中句点的位置,并提取该位置后的文本。

在此之前,我们将99. Unknown分配给缺失值。虽然这不是必要的,但它帮助我们明确处理所有值(包括缺失值)的方式,同时增加了有用的复杂性。完成后,前导数字可以是 1 位或 2 位数字。

接下来,我们创建一个 lambda 函数onlytext,它将用于识别我们想要的文本的位置,然后利用它提取该文本。然后,我们使用highestdegree Series 的transform方法调用onlytext函数:

nls97.fillna({"highestdegree":"99\. Unknown"},
  inplace=True)
onlytext = lambda x: x[x.find(".") + 2:]
highestdegreenonum = nls97.highestdegree.\
  astype(str).transform(onlytext)
highestdegreenonum.value_counts(dropna=False).\
  sort_index() 
highestdegree
Associates       737
Bachelors       1673
GED             1146
High School     3667
Masters          603
None             953
PhD               54
Professional     120
Unknown           31
Name: count, dtype: int64 

你可能注意到,在句点和我们想要的文本开始之间有一个空格。为了处理这一点,onlytext函数会从句点后的两个空格处开始提取文本。

注意

为了实现我们想要的结果,我们并不需要给 lambda 函数命名。我们本可以直接在transform方法中输入 lambda 函数。然而,由于 NLS 数据中有多个列具有相似的前缀,创建一个可重用的函数来处理其他列是一个不错的选择。

有时我们需要查找字符串中某个特定值或某种类型的值(比如数字)出现的所有位置。pandas 的findall函数可以用来返回字符串中一个或多个匹配的值。它会返回一个包含满足给定条件的字符串片段的列表。在深入更复杂的例子之前,我们先做一个简单的示范。

使用findall计算每个maritalstatus值中r出现的次数,展示前几行数据。首先,展示maritalstatus的值,然后展示每个值对应的findall返回的列表:

nls97.maritalstatus.head() 
personid
100061           Married
100139           Married
100284     Never-married
100292               NaN
100583           Married
Name: maritalstatus, dtype: object 
nls97.maritalstatus.head().str.findall("r") 
personid
100061       [r, r]
100139       [r, r]
100284    [r, r, r]
100292          NaN
100583       [r, r]
Name: maritalstatus, dtype: object 
  1. 我们还将展示r出现的次数。

使用concatmaritalstatus值、findall返回的列表和列表的长度显示在同一行:

pd.concat([nls97.maritalstatus.head(),
   nls97.maritalstatus.head().str.findall("r"),
   nls97.maritalstatus.head().str.findall("r").\
       str.len()],
   axis=1) 
 maritalstatus    maritalstatus   maritalstatus
personid                                            
100061             Married           [r, r]               2
100139             Married           [r, r]               2
100284       Never-married        [r, r, r]               3
100292                 NaN              NaN             NaN
100583             Married            [r, r]              2 

我们也可以使用findall返回不同类型的值。例如,我们可以使用正则表达式返回字符串中的所有数字列表。在接下来的几步中,我们将展示这一过程。

  1. 使用findall创建一个包含所有数字的列表,该列表来源于weeklyhrstv(每周花费的电视观看时间)字符串。传递给findall"\d+"正则表达式表示我们只想要数字:

    pd.concat([nls97.weeklyhrstv.head(),\
    ...   nls97.weeklyhrstv.str.findall("\d+").head()], axis=1) 
    
     weeklyhrstv             weeklyhrstv
    personid                                      
    100061     11 to 20 hours a week      [11, 20]
    100139     3 to 10 hours a week        [3, 10]
    100284     11 to 20 hours a week      [11, 20]
    100292     NaN                             NaN
    100583     3 to 10 hours a week        [3, 10] 
    
  2. 使用findall创建的列表,从weeklyhrstv文本中创建一个数值 Series。

我们来定义一个函数,它为每个weeklyhrstv值提取findall创建的列表中的最后一个元素。getnum函数还会调整该数字,使其更接近这两个数字的中点,当存在多个数字时。然后我们使用apply调用这个函数,将findall为每个值创建的列表传递给它。crosstab显示新的weeklyhrstvnum列达到了我们的预期效果:

def getnum(numlist):
...   highval = 0
...   if (type(numlist) is list):
...     lastval = int(numlist[-1])
...     if (numlist[0]=='40'):
...       highval = 45
...     elif (lastval==2):
...       highval = 1
...     else:
...       highval = lastval - 5
...   else:
...     highval = np.nan
...   return highval
...
nls97['weeklyhrstvnum'] = nls97.weeklyhrstv.str.\
...   findall("\d+").apply(getnum)
nls97[['weeklyhrstvnum','weeklyhrstv']].head(7) 
 weeklyhrstvnum                 weeklyhrstv
personid                                           
100061                15       11 to 20 hours a week
100139                 5        3 to 10 hours a week
100284                15       11 to 20 hours a week
100292               NaN                         NaN
100583                 5        3 to 10 hours a week
100833                 5        3 to 10 hours a week
100931                 1  Less than 2 hours per week 
pd.crosstab(nls97.weeklyhrstv, nls97.weeklyhrstvnum) 
weeklyhrstvnum                1       5      15     25     35      45
weeklyhrstv                                                 
11 to 20 hours a week         0       0    1145      0      0       0
21 to 30 hours a week         0       0       0    299      0       0
3 to 10 hours a week          0    3625       0      0      0       0
31 to 40 hours a week         0       0       0      0    116       0
Less than 2 hrs.           1350       0       0      0      0       0
More than 40 hrs.             0       0       0      0      0     176 
  1. 用替代值替换 Series 中的值。

weeklyhrscomputer(每周在计算机上花费的时间)Series 目前的值排序不太理想。我们可以通过将这些值替换为表示顺序的字母来解决此问题。我们将首先创建一个包含旧值的列表,以及一个包含新值的列表。然后,使用 Series 的 replace 方法将旧值替换为新值。每当 replace 在旧值列表中找到一个值时,它会将其替换为新值列表中相同位置的值:

comphrsold = ['Less than 1 hour a week',
  '1 to 3 hours a week','4 to 6 hours a week',
  '7 to 9 hours a week','10 hours or more a week']
comphrsnew = ['A. Less than 1 hour a week',
  'B. 1 to 3 hours a week','C. 4 to 6 hours a week',
  'D. 7 to 9 hours a week','E. 10 hours or more a week']
nls97.weeklyhrscomputer.value_counts().sort_index() 
1 to 3 hours a week         733
10 hours or more a week    3669
4 to 6 hours a week         726
7 to 9 hours a week         368
Less than 1 hour a week     296
Name: weeklyhrscomputer, dtype: int64 
nls97.weeklyhrscomputer.replace(comphrsold, comphrsnew, inplace=True)
nls97.weeklyhrscomputer.value_counts().sort_index() 
A. Less than 1 hour a week     296
B. 1 to 3 hours a week         733
C. 4 to 6 hours a week         726
D. 7 to 9 hours a week         368
E. 10 hours or more a week    3669
Name: weeklyhrscomputer, dtype: int64 

本食谱中的步骤演示了我们在 pandas 中可以执行的一些常见字符串评估和操作任务。

工作原理……

我们经常需要检查一个字符串,以查看其中是否存在某种模式。我们可以使用字符串的 contains 方法来实现这一点。如果我们确切知道期望的模式的位置,可以使用标准的切片符号 [start:stop:step] 来选择从起始位置到结束位置减一的文本。(step 的默认值为 1。)例如,在步骤 4 中,我们使用 nls97.highestdegree.str[0:1] 获取了 highestdegree 的第一个字符。然后,我们使用 isin 来测试第一个字符串是否出现在一个值列表中。 (isin 适用于字符数据和数值数据。)

有时,我们需要从字符串中提取多个满足条件的值。在这种情况下,findall 非常有用,因为它会返回一个满足条件的所有值的列表。它还可以与正则表达式配合使用,当我们寻找的内容比字面值更为通用时。在步骤 8步骤 9 中,我们在寻找任何数字。

还有更多……

在根据另一个 Series 的值创建 Series 时,处理缺失值时需要特别小心。缺失值可能会满足 where 调用中的 else 条件,而这并非我们的意图。在步骤 2步骤 3步骤 4 中,我们确保通过在 where 调用的开始部分进行缺失值检查,正确处理了缺失值。

我们在进行字符串比较时,也需要注意字母的大小写。例如,Probablyprobably 并不相等。解决这一问题的一种方法是在进行比较时,使用 upperlower 方法,以防大小写的差异没有实际意义。upper("Probably") == upper("PROBABLY") 实际上是 True

处理日期

处理日期通常并不简单。数据分析师需要成功地解析日期值,识别无效或超出范围的日期,填补缺失的日期,并计算时间间隔。在这些步骤中,每个环节都会遇到意想不到的挑战,但一旦我们成功解析了日期值并获得了 pandas 中的 datetime 值,就算是迈出了成功的一步。在本食谱中,我们将首先解析日期值,然后再处理接下来的其他挑战。

准备工作

在本食谱中,我们将处理 NLS 和 COVID-19 每日病例数据。COVID-19 每日数据包含每个国家每天的报告数据。(NLS 数据实际上对于这个目的来说过于干净。为了说明如何处理缺失的日期值,我将其中一个出生月份的值设置为缺失。)

数据说明

我们的《全球数据》提供了 COVID-19 的公共数据,链接:ourworldindata.org/covid-cases。本食谱中使用的数据是于 2024 年 3 月 3 日下载的。

如何操作…

在这个食谱中,我们将把数字数据转换为日期时间数据,首先通过确认数据中是否包含有效的日期值,然后使用fillna来替换缺失的日期。接下来,我们将计算一些日期间隔;也就是说,计算 NLS 数据中受访者的年龄,以及 COVID-19 每日数据中自首例 COVID-19 病例以来的天数。让我们开始吧:

  1. 导入pandasdateutils中的relativedelta模块,然后加载 NLS 和 COVID-19 每日病例数据:

    import pandas as pd
    from dateutil.relativedelta import relativedelta
    covidcases = pd.read_csv("data/covidcases.csv")
    nls97 = pd.read_csv("data/nls97c.csv")
    nls97.set_index("personid", inplace=True) 
    
  2. 显示出生月份和年份的值。

请注意,出生月份有一个缺失值。除此之外,我们将用来创建birthdate序列的数据看起来相当干净:

nls97[['birthmonth','birthyear']].isnull().sum() 
birthmonth    1
birthyear     0
dtype: int64 
nls97.birthmonth.value_counts(dropna=False).\
  sort_index() 
birthmonth
1      815
2      693
3      760
4      659
5      689
6      720
7      762
8      782
9      839
10     765
11     763
12     736
NaN      1
Name: count, dtype: int64 
nls97.birthyear.value_counts().sort_index() 
1980     1691
1981     1874
1982     1841
1983     1807
1984     1771
Name: birthyear, dtype: int64 
  1. 使用fillna方法为缺失的出生月份设置值。

birthmonth的平均值(四舍五入为最接近的整数)传递给fillna。这将用birthmonth的平均值替换缺失的birthmonth值。请注意,现在又有一个人将birthmonth的值设为 6:

nls97.fillna({"birthmonth":\
 int(nls97.birthmonth.mean())}, inplace=True)
nls97.birthmonth.value_counts(dropna=False).\
  sort_index() 
birthmonth
1     815
2     693
3     760
4     659
5     689
6     721
7     762
8     782
9     839
10    765
11    763
12    736
Name: count, dtype: int64 
  1. 使用month和年份integers来创建日期时间列。

我们可以将字典传递给 pandas 的to_datetime函数。字典需要包含年、月和日的键。请注意,birthmonthbirthyearbirthdate没有缺失值:

nls97['birthdate'] = pd.to_datetime(dict(year=nls97.birthyear, month=nls97.birthmonth, day=15))
nls97[['birthmonth','birthyear','birthdate']].head() 
 birthmonth     birthyear             birthdate
personid                                                 
100061     5              1980                 1980-05-15
100139     9              1983                 1983-09-15
100284     11             1984                 1984-11-15
100292     4              1982                 1982-04-15
100583     6              1980                 1980-06-15 
nls97[['birthmonth','birthyear','birthdate']].isnull().sum() 
birthmonth    0
birthyear     0
birthdate     0
dtype: int64 
  1. 使用日期时间列计算年龄。

首先,定义一个函数,当给定起始日期和结束日期时,计算年龄。请注意,我们创建了一个Timestamp对象rundate,并将其赋值为2024-03-01,以用作年龄计算的结束日期:

def calcage(startdate, enddate):
...   age = enddate.year - startdate.year
...   if (enddate.month<startdate.month or (enddate.month==startdate.month and enddate.day<startdate.day)):
...     age = age -1
...   return age
...
rundate = pd.to_datetime('2024-03-01')
nls97["age"] = nls97.apply(lambda x: calcage(x.birthdate, rundate), axis=1)
nls97.loc[100061:100583, ['age','birthdate']] 
 age     birthdate
personid                   
100061     43    1980-05-15
100139     40    1983-09-15
100284     39    1984-11-15
100292     41    1982-04-15
100583     43    1980-06-15 
  1. 我们可以改用relativedelta模块来计算年龄。我们只需要执行以下操作:

    nls97["age2"] = nls97.\
      apply(lambda x: relativedelta(rundate,
        x.birthdate).years,
        axis=1) 
    
  2. 我们应该确认我们得到的值与步骤 5中的值相同:

    (nls97['age']!=nls97['age2']).sum() 
    
    0 
    
    nls97.groupby(['age','age2']).size() 
    
    age  age2
    39   39      1463
    40   40      1795
    41   41      1868
    42   42      1874
    43   43      1690
    44   44       294
    dtype: int64 
    
  3. 将字符串列转换为日期时间列。

casedate列是object数据类型,而不是datetime数据类型:

covidcases.iloc[:, 0:6].dtypes 
iso_code        object
continent       object
location        object
casedate        object
total_cases	   float64
new_cases       float64
dtype: object 
covidcases.iloc[:, 0:6].sample(2, random_state=1).T 
 628         26980
iso_code            AND           PRT
casedate     2020-03-15    2022-12-04
continent        Europe        Europe
location        Andorra      Portugal
total_cases           2     5,541,211
new_cases             1         3,963 
covidcases['casedate'] = pd.to_datetime(covidcases.casedate, format='%Y-%m-%d')
covidcases.iloc[:, 0:6].dtypes 
iso_code             object
continent            object
location             object
casedate     datetime64[ns]
total_cases         float64
new_cases           float64
dtype: object 
  1. 显示日期时间列的描述性统计数据:

    covidcases.casedate.nunique() 
    
    214 
    
    covidcases.casedate.describe() 
    
    count                            36501
    mean     2021-12-16 05:41:07.954302720
    min                2020-01-05 00:00:00
    25%                2021-01-31 00:00:00
    50%                2021-12-12 00:00:00
    75%                2022-10-09 00:00:00
    max                2024-02-04 00:00:00
    Name: casedate, dtype: object 
    
  2. 创建一个timedelta对象来捕捉日期间隔。

对于每一天,计算自报告首例病例以来,每个国家的天数。首先,创建一个 DataFrame,显示每个国家新病例的第一天,然后将其与完整的 COVID-19 病例数据合并。接着,对于每一天,计算从firstcasedatecasedate的天数:

firstcase = covidcases.loc[covidcases.new_cases>0,['location','casedate']].\
...   sort_values(['location','casedate']).\
...   drop_duplicates(['location'], keep='first').\
...   rename(columns={'casedate':'firstcasedate'})
covidcases = pd.merge(covidcases, firstcase, left_on=['location'], right_on=['location'], how="left")
covidcases['dayssincefirstcase'] = covidcases.casedate - covidcases.firstcasedate
covidcases.dayssincefirstcase.describe() 
count                          36501
mean     637 days 01:36:55.862579112
std      378 days 15:34:06.667833980
min                  0 days 00:00:00
25%                315 days 00:00:00
50%                623 days 00:00:00
75%                931 days 00:00:00
max               1491 days 00:00:00
Name: dayssincefirstcase, dtype: object 

本食谱展示了如何解析日期值并创建日期时间序列,以及如何计算时间间隔。

如何操作…

在 pandas 中处理日期时,第一项任务是将其正确转换为 pandas datetime Series。在 步骤 348 中,我们处理了一些最常见的问题:缺失值、从整数部分转换日期和从字符串转换日期。birthmonthbirthyear 在 NLS 数据中是整数。我们确认这些值是有效的日期月份和年份。如果,举例来说,存在月份值为 0 或 20,则转换为 pandas datetime 将失败。

birthmonthbirthyear 的缺失值将导致 birthdate 缺失。我们使用 fillna 填充了 birthmonth 的缺失值,将其分配为 birthmonth 的平均值。在 步骤 5 中,我们使用新的 birthdate 列计算了每个人截至 2024 年 3 月 1 日的年龄。我们创建的 calcage 函数会根据出生日期晚于 3 月 1 日的个体进行调整。

数据分析师通常会收到包含日期字符串的文件。当发生这种情况时,to_datetime 函数是分析师的得力助手。它通常足够智能,能够自动推断出字符串日期数据的格式,而无需我们明确指定格式。然而,在 步骤 8 中,我们告诉 to_datetime 使用 %Y-%m-%d 格式处理我们的数据。

步骤 9 告诉我们有 214 个独特的日期报告了 COVID-19 病例。第一次报告的日期是 2020 年 1 月 5 日,最后一次报告的日期是 2024 年 2 月 4 日。

步骤 10 中的前两条语句涉及了一些技巧(排序和去重),我们将在 第九章《汇总时修复杂乱数据》和 第十章《合并 DataFrame 时处理数据问题》中详细探讨。这里你只需要理解目标:创建一个按 location(国家)每行数据表示的 DataFrame,并记录首次报告的 COVID-19 病例日期。我们通过仅选择全数据中 new_cases 大于 0 的行来做到这一点,然后按 locationcasedate 排序,并保留每个 location 的第一行。接着,我们将 casedate 改名为 firstcasedate,然后将新的 firstcase DataFrame 与 COVID-19 日病例数据合并。

由于 casedatefirstcasedate 都是日期时间列,从后者减去前者将得到一个 timedelta 值。这为我们提供了一个 Series,表示每个国家每个报告日期自 new_cases 首次出现后的天数。报告病例日期(casedate)和首次病例日期(firstcasedate)之间的最大持续时间(dayssincefirstcase)是 1491 天,约为 4 年多。这个间隔计算对于我们想要按病毒在一个国家明显存在的时间来追踪趋势,而不是按日期来追踪趋势时非常有用。

另请参见

与其在步骤 10中使用sort_valuesdrop_duplicates,我们也可以使用groupby来实现类似的结果。在第九章中,我们将深入探索groupby在聚合时修复杂乱数据。我们还在步骤 10中做了一个合并。第十章合并 DataFrame 时解决数据问题,将专门讨论这个主题。

使用 OpenAI 进行 Series 操作

本章之前食谱中演示的许多 Series 操作可以借助 AI 工具完成,包括通过 PandasAI 与 OpenAI 的大型语言模型一起使用。在这个食谱中,我们研究如何使用 PandasAI 查询 Series 的值,创建新的 Series,有条件地设置 Series 的值,并对 DataFrame 进行一些基础的重塑。

准备就绪

在这个食谱中,我们将再次使用 NLS 和 COVID-19 每日数据。我们还将使用 PandasAI,它可以通过pip install pandasai安装。你还需要从openai.com获取一个令牌,以便向 OpenAI API 发送请求。

如何操作...

以下步骤创建一个 PandasAI SmartDataframe对象,然后使用该对象的聊天方法提交一系列 Series 操作的自然语言指令:

  1. 我们首先需要从 PandasAI 导入OpenAISmartDataframe模块。我们还需要实例化一个llm对象:

    import pandas as pd
    from pandasai.llm.openai import OpenAI
    from pandasai import SmartDataframe
    llm = OpenAI(api_token="Your API Token") 
    
  2. 我们加载 NLS 和 COVID-19 数据并创建一个SmartDataframe对象。我们传入llm对象以及一个 pandas DataFrame:

    covidcases = pd.read_csv("data/covidcases.csv")
    nls97 = pd.read_csv("data/nls97f.csv")
    nls97.set_index("personid", inplace=True)
    nls97sdf = SmartDataframe(nls97, config={"llm": llm}) 
    
  3. 现在,我们准备好在我们的SmartDataframe上生成 Series 的汇总统计信息。我们可以请求单个 Series 的平均值,或者多个 Series 的平均值:

    nls97sdf.chat("Show average of gpaoverall") 
    
    2.8184077281812128 
    
    nls97sdf.chat("Show average for each weeks worked column") 
    
     Average Weeks Worked
                      0
    weeksworked00 26.42
    weeksworked01 29.78
    weeksworked02 31.83
    weeksworked03 33.51
    weeksworked04 35.10
    weeksworked05 37.34
    weeksworked06 38.44
    weeksworked07 39.29
    weeksworked08 39.33
    weeksworked09 37.51
    weeksworked10 37.12
    weeksworked11 38.06
    weeksworked12 38.15
    weeksworked13 38.79
    weeksworked14 38.73
    weeksworked15 39.67
    weeksworked16 40.19
    weeksworked17 40.37
    weeksworked18 40.01
    weeksworked19 41.22
    weeksworked20 38.35
    weeksworked21 36.17
    weeksworked22 11.43 
    
  4. 我们还可以通过另一个 Series 来汇总 Series 的值,通常是一个分类的 Series:

    nls97sdf.chat("Show satmath average by gender") 
    
     Female   Male
    0  486.65 516.88 
    
  5. 我们还可以通过SmartDataframechat方法创建一个新的 Series。我们不需要使用实际的列名。例如,PandasAI 会自动识别我们想要的是childathome Series,当我们写下child at home时:

    nls97sdf = nls97sdf.chat("Set childnum to child at home plus child not at home")
    nls97sdf[['childnum','childathome','childnotathome']].\
      sample(5, random_state=1) 
    
     childnum  childathome  childnotathome
    personid                                      
    211230        2.00         2.00            0.00
    990746        3.00         3.00            0.00
    308169        3.00         1.00            2.00
    798458         NaN          NaN             NaN
    312009         NaN          NaN             NaN 
    
  6. 我们可以使用chat方法有条件地创建 Series 值:

    nls97sdf = nls97sdf.chat("evermarried is 'No' when maritalstatus is 'Never-married', else 'Yes'")
    nls97sdf.groupby(['evermarried','maritalstatus']).size() 
    
    evermarried  maritalstatus
    No           Never-married    2767
    Yes          Divorced          669
                 Married          3068
                 Separated         148
                 Widowed            23
    dtype: int64 
    
  7. PandasAI 对你在这里使用的语言非常灵活。例如,以下内容提供了与步骤 6相同的结果:

    nls97sdf = nls97sdf.chat("if maritalstatus is 'Never-married' set evermarried2 to 'No', otherwise 'Yes'")
    nls97sdf.groupby(['evermarried2','maritalstatus']).size() 
    
    evermarried2  maritalstatus
    No            Never-married    2767
    Yes           Divorced          669
                  Married          3068
                  Separated         148
                  Widowed            23
    dtype: int64 
    
  8. 我们可以对多个同名的列进行计算:

    nls97sdf = nls97sdf.chat("set weeksworkedavg to the average for weeksworked columns") 
    

这将计算所有weeksworked00weeksworked22列的平均值,并将其分配给一个名为weeksworkedavavg的新列。

  1. 我们可以根据汇总统计轻松地填补缺失的值:

    nls97sdf.gpaenglish.describe() 
    
    count   5,798
    mean      273
    std        74
    min         0
    25%       227
    50%       284
    75%       323
    max       418
    Name: gpaenglish, dtype: float64 
    
    nls97sdf = nls97sdf.chat("set missing gpaenglish to the average")
    nls97sdf.gpaenglish.describe() 
    
    count   8,984
    mean      273
    std        59
    min         0
    25%       264
    50%       273
    75%       298
    max       418
    Name: gpaenglish, dtype: float64 
    
  2. 我们还可以使用 PandasAI 进行一些重塑,类似于我们在之前的食谱中所做的。回顾一下,我们处理了 COVID-19 病例数据,并希望获取每个国家的第一行数据。让我们首先以传统方式做一个简化版本:

    firstcase = covidcases.\
      sort_values(['location','casedate']).\
      drop_duplicates(['location'], keep='first')
    firstcase.set_index('location', inplace=True)
    firstcase.shape 
    
    (231, 67) 
    
    firstcase[['iso_code','continent','casedate',
      'total_cases','new_cases']].head(2).T 
    
    location       Afghanistan        Albania
    iso_code               AFG            ALB
    continent             Asia         Europe
    casedate        2020-03-01     2020-03-15
    total_cases           1.00          33.00
    new_cases             1.00          33.00 
    
  3. 我们可以通过创建一个SmartDataframe并使用chat方法来获得相同的结果。这里使用的自然语言非常简单,显示每个国家的第一个 casedate、location 和其他值

    covidcasessdf = SmartDataframe(covidcases, config={"llm": llm})
    firstcasesdf = covidcasessdf.chat("Show first casedate and location and other values for each country.")
    firstcasesdf.shape 
    
    (231, 7) 
    
    firstcasesdf[['location','continent','casedate',
      'total_cases','new_cases']].head(2).T 
    
    iso_code                  ABW             AFG
    location                Aruba     Afghanistan
    continent       North America            Asia
    casedate           2020-03-22      2020-03-01
    total_cases              5.00            1.00
    new_cases                5.00            1.00 
    

请注意,PandasAI 会智能地选择需要获取的列。我们只获取我们需要的列,而不是所有列。我们也可以直接将我们想要的列名传递给chat

这就是一点 PandasAI 和 OpenAI 的魔力!通过传递一句相当普通的句子给chat方法,就完成了所有的工作。

它是如何工作的…

使用 PandasAI 时,大部分工作其实就是导入相关库,并实例化大型语言模型和 SmartDataframe 对象。完成这些步骤后,只需向 SmartDataframechat 方法发送简单的句子,就足以总结 Series 值并创建新的 Series。

PandasAI 擅长从 Series 中生成简单的统计信息。我们甚至不需要精确记住 Series 名称,正如我们在步骤 3中所见。我们可能使用的自然语言往往比传统的 pandas 方法(如 groupby)更直观。在步骤 4中传递给 chat按性别显示 satmath 平均值就是一个很好的例子。

对 Series 进行的操作,包括创建新的 Series,也是相当简单的。在步骤 5中,我们通过指示 SmartDataframe 将住在家中的孩子数与不住在家中的孩子数相加,创建了一个表示孩子总数的 Series(childnum)。我们甚至没有提供字面上的 Series 名称,childathomechildnotathome。PandasAI 会自动理解我们的意思。

步骤 67 展示了使用自然语言进行 Series 操作的灵活性。如果我们在步骤 6中将evermarried 为 ‘No’ 当 maritalstatus 为 ‘Never-married’,否则为 ‘Yes’传递给chat,或者在步骤 7中将*如果 maritalstatus 为 ‘Never-married’,则将 evermarried2 设置为 ‘No’,否则为 ‘Yes’*传递给chat,我们都会得到相同的结果。

我们还可以通过简单的自然语言指令对 DataFrame 进行较为广泛的重塑,正如在步骤 11中所示。我们将and other values添加到指令中,以获取除了casedate之外的列。PandasAI 还会自动识别出location作为索引是有意义的。

还有更多…

鉴于 PandasAI 工具仍然非常新,数据科学家们现在才开始弄清楚如何将这些工具最佳地集成到我们的数据清理和分析工作流程中。PandasAI 有两个明显的应用场景:1)检查我们以传统方式进行的 Series 操作的准确性;2)在 pandas 或 NumPy 工具不够直观时(如 pandas 的 groupby 或 NumPy 的 where 函数),以更直观的方式进行 Series 操作。

PandasAI 还可以用于构建交互式界面来查询数据存储,如数据仪表盘。我们可以使用 AI 工具帮助终端用户更有效地查询组织数据。正如我们在第三章《衡量你的数据》中所看到的,PandasAI 在快速创建可视化方面也非常出色。

另请参见

第九章聚合数据时修复混乱数据中,我们将进行更多的数据聚合操作,包括跨行聚合数据和重新采样日期和时间数据。

总结

本章探讨了多种 pandas Series 方法,用于探索和处理不同类型的数据:数值、字符串和日期。我们学习了如何从 Series 中获取值以及如何生成摘要统计信息。我们还了解了如何更新 Series 中的值,以及如何针对数据子集或根据条件进行更新。我们还探讨了处理字符串或日期 Series 时的特定挑战,以及如何使用 Series 方法来应对这些挑战。最后,我们看到如何利用 PandasAI 来探索和修改 Series。在下一章,我们将探索如何识别和修复缺失值。

加入我们的 Discord 社区

加入我们社区的 Discord 空间,与作者和其他读者进行讨论:

discord.gg/p8uSgEAETX