Python 数据清理秘籍第二版(三)
原文:
annas-archive.org/md5/2774e54b23314a6bebe51d6caf9cd592译者:飞龙
第五章:使用可视化工具识别意外值
在上一章的多个配方中,我们已初步接触了可视化的内容。我们使用了直方图和 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 matplotlib和pip 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 图、直方图和堆积直方图:
- 导入
pandas、matplotlib和statsmodels库。
此外,加载地表温度和 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)
- 显示一些天气站的温度行。
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
- 显示一些描述性统计信息。
此外,查看偏度和峰度:
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
- 绘制平均温度的直方图。
此外,在整体均值处绘制一条线:
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 年各气象站的平均温度直方图
- 运行 QQ 图,检查分布与正态分布的偏离情况。
注意到大部分温度分布沿着红线分布(如果分布完全正常,所有点都会落在红线上,但尾部明显偏离正常分布):
sm.qqplot(landtemps[['avgtemp']].sort_values(['avgtemp']), line='s')
plt.title("QQ Plot of Average Temperatures")
plt.show()
这产生了以下 QQ 图:
图 5.2:各气象站的平均温度与正态分布的对比图
- 显示每百万的总 COVID-19 病例的偏度和峰度。
这来自 COVID-19 数据框,其中每一行代表一个国家:
covidtotals.total_cases_pm.skew()
0.8349032460009967
covidtotals.total_cases_pm.kurtosis()
-0.4280595203351645
- 做一个 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:按地区分布的各国病例数的堆叠直方图,每百万人的不同病例数水平
- 在同一图形中显示多个直方图。
这允许不同的 x 和 y 坐标轴值。我们需要循环遍历每个坐标轴,并为每个子图选择 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.hist 和 ax.hist 都能经常奏效。
使用箱型图识别连续变量的异常值
箱型图本质上是我们在第四章:使用单个变量识别异常值中所做工作的图形表示,在这一章中我们使用了四分位距(IQR)的概念——即第一个四分位数和第三个四分位数之间的距离——来确定异常值。任何大于(1.5 * IQR)+ 第三个四分位数的值,或小于第一个四分位数 - (1.5 * IQR) 的值,都被认为是异常值。箱型图正是揭示了这一点。
准备工作
我们将使用关于 COVID-19 病例和死亡数据的累计数据,以及国家纵向调查(NLS)数据。你需要安装 Matplotlib 库,以便在你的计算机上运行代码。
如何操作…
我们使用箱线图来展示学术能力评估测试(SAT)分数、工作周数以及 COVID-19 病例和死亡数的形状和分布:
- 加载
pandas和matplotlib库。
另外,加载 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)
- 绘制 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 语言成绩箱线图
-
接下来,显示一些关于工作周数的描述性统计:
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 -
绘制工作周数的箱线图:
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:并排显示的两个变量的箱线图
- 显示 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
-
绘制每百万病例和死亡数的箱线图:
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:并排显示的两个变量的箱线图
- 将箱线图作为单独的子图显示在一张图上。
当变量值差异很大时(例如 COVID-19 病例和死亡数),在一张图上查看多个箱线图会很困难。幸运的是,Matplotlib 允许我们在每张图上创建多个子图,每个子图可以使用不同的x和y轴。我们还可以将数据以千为单位呈现,以提高可读性:
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 病例分布。
-
导入
pandas、matplotlib和seaborn库: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) -
查看每个学位获得水平的工作周数的中位数以及第一和第三四分位数值。
首先,定义一个返回这些值的函数作为 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
- 绘制按最高学位划分的工作周数的箱线图。
使用 Seaborn 库绘制这些箱线图。首先,创建一个子图并命名为myplt。这使得稍后访问子图属性更加方便。使用boxplot的order参数按最高学位排序。注意,对于那些从未获得学位的人群,低端没有异常值或胡须,因为这些人群的 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 和异常值
- 查看按区域划分的每百万人病例的最小值、最大值、中位数,以及第一和第三四分位数值。
使用在步骤 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
- 绘制按区域划分的每百万人病例的箱线图。
由于区域数量较多,需要翻转坐标轴。同时,绘制一个蜂群图,以显示按区域划分的国家数量。蜂群图为每个区域中的每个国家显示一个点。由于极端值,某些 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 和异常值
-
显示每百万人病例的最高值:
highvalue = covidtotals.total_cases_pm.quantile(0.9) highvalue512388.401covidtotals.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 -
重新绘制不包含极端值的箱线图:
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。在第十一章 整理和重塑数据中,我们更多地使用stack和unstack。
使用小提琴图检查分布形状和异常值
小提琴图将直方图和箱线图结合在一张图中。它们显示了 IQR、中位数、须条,以及各个数值范围内观察值的频率。如果没有实际的小提琴图,很难想象这是如何做到的。我们使用与上一个食谱中箱线图相同的数据生成几个小提琴图,以便更容易理解它们的工作原理。
准备工作
我们将使用 NLS 数据。你需要在计算机上安装 Matplotlib 和 Seaborn 才能运行本食谱中的代码。
如何操作…
我们使用小提琴图来查看分布的范围和形态,并将它们显示在同一图表中。然后我们按组进行小提琴图绘制:
-
加载
pandas、matplotlib和seaborn,以及 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) -
绘制 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 和异常值阈值的标签
-
获取工作周数的描述性统计:
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 -
显示 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:显示两个变量分布范围和形态的小提琴图,按组并排展示
- 按性别和婚姻状况绘制工资收入的小提琴图。
首先,创建一个合并的婚姻状况列。指定性别为 x 轴,工资为 y 轴,新的合并婚姻状况列为 hue。hue 参数用于分组,这将与 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:显示两个不同组之间分布范围和形态的小提琴图
-
按最高学历绘制工作周数的小提琴图:
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 的图形差别很大。
另请参阅
将本节中本食谱的提琴图与本章前面的直方图、箱型图和分组箱型图进行比较可能会有所帮助。
使用散点图查看双变量关系
我的感觉是,数据分析师依赖的图表中,散点图是最常见的图表之一,可能只有直方图例外。我们都非常习惯查看可以在二维平面上展示的关系。散点图捕捉了重要的现实世界现象(变量之间的关系),并且对大多数人来说非常直观。这使得它们成为我们可视化工具箱中不可或缺的一部分。
准备工作
本食谱需要Matplotlib和Seaborn。我们将使用landtemps数据集,它提供了 2023 年全球 12,137 个气象站的平均温度数据。
如何操作……
我们在上一章提升了散点图技能,能够可视化更加复杂的关系。我们通过在一张图表中显示多个散点图、创建三维散点图以及显示多条回归线,来展示平均温度、纬度和海拔之间的关系:
-
加载
pandas、numpy、matplotlib和seaborn:import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns landtemps = pd.read_csv("data/landtemps2023avgs.csv") -
运行纬度(
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:按平均温度绘制的纬度散点图
- 用红色显示高海拔点。
创建低海拔和高海拔的数据框。请注意,在每个纬度下,高海拔点通常较低(即温度较低):
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:按平均温度和海拔绘制的纬度散点图
- 查看温度、纬度和海拔的三维图。
看起来在高海拔站点的纬度增加下,温度的下降趋势较为陡峭:
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:按平均温度绘制的纬度和海拔的三维散点图
- 显示纬度与温度的回归线。
使用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:纬度与平均温度的散点图,带有回归线
- 显示低海拔和高海拔车站的回归线。
这次我们使用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:纬度与温度的散点图,带有不同海拔的回归线
-
显示一些位于低海拔和高海拔回归线上的车站。我们可以使用我们在步骤 3中创建的
high和lowDataFrame: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 19low.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
散点图是查看两个变量之间关系的好方法。这些步骤还展示了我们如何为数据的不同子集显示这些关系。
它是如何工作的……
我们只需提供x和y的列名以及一个 DataFrame,就可以运行一个散点图。无需其他更多的操作。我们可以访问与运行直方图和箱线图时相同的图形和坐标轴属性——标题、坐标轴标签、刻度线和标签等。请注意,为了访问像坐标轴标签(而不是图形上的标签)这样的属性,我们使用set_xlabels或set_ylabels,而不是xlabels或ylabels。
3D 图稍微复杂一些。首先,我们将坐标轴的投影设置为3d——plt.axes(projection='3d'),就像我们在步骤 4中做的那样。然后我们可以为每个子图使用scatter3D方法。
由于散点图旨在说明回归变量(x变量)与因变量之间的关系,因此在散点图上看到最小二乘回归线是非常有帮助的。Seaborn 提供了两种方法来做到这一点:regplot和lmplot。我通常使用regplot,因为它资源消耗较少。但有时,我需要lmplot的特性。我们在步骤 6中使用lmplot及其hue属性,为每个海拔水平生成单独的回归线。
在步骤 7中,我们查看一些异常值:那些温度明显高于其所属组的回归线的车站。我们想要调查葡萄牙的LAJES_AB车站和亚美尼亚的YEREVAN车站的数据((high.latabs>38) & (high.avgtemp>=18))。这些车站的平均温度高于根据给定纬度和海拔水平预测的温度。
还有更多……
我们看到了纬度与平均温度之间的预期关系。随着纬度的增加,温度下降。但是,海拔是另一个重要因素。能够同时可视化所有三个变量有助于我们更容易识别异常值。当然,温度的其他影响因素也很重要,比如暖流。遗憾的是,这些数据在当前的数据集中没有。
散点图非常适合可视化两个连续变量之间的关系。通过一些调整,Matplotlib 和 Seaborn 的散点图工具也可以通过增加第三维度(当第三维度为分类变量时,通过颜色的创意使用,或通过改变点的大小)来提供对三个变量之间关系的理解(第四章中使用线性回归识别具有高影响力的数据点的实例展示了这一点)。
另见
这是一本关于可视化的章节,着重于通过可视化识别意外值。但这些图形也迫切需要我们在第四章中进行的多变量分析,在数据子集中的异常值识别。特别是,线性回归分析和对残差的深入分析,对于识别异常值会很有帮助。
使用折线图检查连续变量的趋势
可视化连续变量在规律时间间隔内的值的典型方法是通过折线图,尽管有时对于较少的时间间隔,柱状图也可以使用。在本配方中,我们将使用折线图来展示变量趋势,并检查趋势的突变以及按组别的时间差异。
准备工作
本配方将处理每日的 COVID-19 病例数据。在之前的配方中,我们使用了按国家统计的总数。每日数据提供了每个国家每日新增病例和新增死亡人数,以及我们在其他配方中使用的相同人口统计变量。你需要安装 Matplotlib 才能运行本配方中的代码。
如何操作……
我们使用折线图来可视化每日 COVID-19 病例和死亡趋势。我们按地区创建折线图,并使用堆叠图来更好地理解一个国家如何影响整个地区的病例数量:
-
导入
pandas、matplotlib以及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"]) -
查看几行 COVID-19 每日数据:
coviddaily.sample(2, random_state=1).T628 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 -
按天计算新增病例和死亡人数。
选择 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
- 按天显示新增病例和新增死亡的折线图。
在不同的子图中显示病例和死亡数据:
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 病例和死亡的每日趋势线
-
按天和地区计算新增病例和死亡人数:
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 -
按选定地区显示新增病例的折线图。
遍历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 每日趋势线
- 使用堆叠图来更仔细地检查一个地区的趋势。
查看南美洲是否是由一个国家(巴西)推动了趋势线。为南美洲按天创建一个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轴的数值。到目前为止,这与我们在使用hist、scatterplot、boxplot和violinplot方法时做的基本相同。但由于我们在处理日期数据,这里我们利用 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 matplotlib和pip install seaborn。
如何做...
我们首先展示 COVID-19 数据的部分相关矩阵,并展示一些关键关系的散点图。然后展示相关矩阵的热图,以可视化所有变量之间的相关性:
-
导入
matplotlib和seaborn,并加载 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"]) -
生成相关矩阵。
查看矩阵的一部分:
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
- 显示中位年龄和国内生产总值(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 按百万人口病例并排的散点图
-
生成相关矩阵的热图:
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。
如何实现…
在这个例子中,我们使用括号操作符以及 loc 和 iloc 访问器来选择 Series 值。让我们开始吧:
-
导入所需的
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 访问器可能会是更好的选择。这个例子中展示了这一点。
- 从 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)
- 使用括号运算符选择 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
- 使用
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
- 使用
iloc访问器选择值。
iloc 与 loc 的区别在于,它接受的是行号列表,而不是标签。它的工作方式类似于括号运算符切片。在这一步中,我们传递一个包含 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]将返回personid在135335到151672之间(包括这两个值)的 GPA 值的 Series。
如步骤 5所示,iloc访问器使用的是行位置,而不是索引标签。我们可以传递一个整数列表或使用切片表示法传递一个范围。
显示 pandas Series 的汇总统计数据
pandas 有许多生成汇总统计数据的方法。我们可以分别使用mean、median、max和min方法轻松获得 Series 的平均值、中位数、最大值或最小值。非常方便的describe方法将返回所有这些统计数据,以及其他一些数据。我们还可以使用quantile方法获得 Series 中任意百分位的值。这些方法可以应用于 Series 的所有值,或者仅用于选定的值。接下来的示例中将展示如何使用这些方法。
准备工作
我们将继续使用 NLS 中的总体 GPA 列。
如何操作...
让我们仔细看看整个数据框和选定行的总体 GPA 分布。为此,请按照以下步骤操作:
-
导入
pandas和numpy并加载 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) -
获取一些描述性统计数据:
gpaoverall = nls97.gpaoverall gpaoverall.mean()2.8184077281812145gpaoverall.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: float64gpaoverall.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 -
显示 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: float64gpaoverall.loc[gpaoverall.between(3,3.5)].count()1679gpaoverall.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: float64gpaoverall.loc[gpaoverall>gpaoverall.quantile(0.99)].\ ... agg(['count','min','max'])count 60.00 min 3.98 max 4.17 Name: gpaoverall, dtype: float64 -
测试所有值中的某一条件。
检查 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
- 基于不同列的值,显示 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
-
显示包含分类数据的 Series 的描述性统计和频率:
nls97.maritalstatus.describe()count 6675 unique 5 top Married freq 3068 Name: maritalstatus, dtype: objectnls97.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 中的所有值上都成立。any 和 all 方法对于此操作非常有用。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 方法(如 describe、mean、sum、isnull、all 和 any)之间不会间隔太长时间。
另见
本章我们只是浅尝辄止地介绍了数据聚合的内容。我们将在第九章《聚合时修复脏数据》中更详细地讨论这一点。
更改 Series 值
在数据清理过程中,我们经常需要更改数据 Series 中的值或创建一个新的 Series。我们可以更改 Series 中的所有值,或仅更改部分数据的值。我们之前用来从 Series 获取值的大部分技术都可以用来更新 Series 的值,尽管需要进行一些小的修改。
准备工作
在本道菜谱中,我们将处理 NLS 数据中的总体高中 GPA 列。
如何实现…
我们可以为所有行或选择的行更改 pandas Series 中的值。我们可以通过对其他 Series 执行算术操作和使用汇总统计来更新 Series。让我们来看看这个过程:
-
导入
pandas并加载 NLS 数据:import pandas as pd nls97 = pd.read_csv("data/nls97f.csv", low_memory=False) nls97.set_index("personid", inplace=True) -
基于标量编辑所有值。
将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
- 使用索引标签设置值。
使用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
- 使用运算符在多个 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
- 使用索引标签设置汇总统计值。
使用loc访问器从100061到100292选择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
- 使用位置设置值。
使用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
- 在筛选后设置 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 的loc和iloc访问器(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中,如果childathome或childnotathome缺失,那么操作将返回missing。我们将在下一章的识别和修复缺失值案例中讨论如何处理这种情况。
另请参阅
第三章,测量你的数据,更详细地介绍了loc和iloc访问器的使用,特别是在选择行和选择与组织列的案例中。
有条件地更改 Series 值
更改 Series 值通常比前一个案例所示的更为复杂。我们经常需要根据该行数据中一个或多个其他 Series 的值来设置 Series 值。当我们需要根据其他行的值设置 Series 值时,这会变得更加复杂;比如,某个人的先前值,或者一个子集的平均值。我们将在本案例和下一个案例中处理这些复杂情况。
准备工作
在本案例中,我们将处理土地温度数据和 NLS 数据。
数据说明
土地温度数据集包含了来自全球 12,000 多个站点在 2023 年的平均温度数据(单位:摄氏度),尽管大多数站点位于美国。该原始数据集来自全球历史气候网络集成数据库,已由美国国家海洋和大气管理局(NOAA)提供给公众使用,网址:www.ncei.noaa.gov/products/land-based-station/global-historical-climatology-network-monthly。
如何做……
我们将使用 NumPy 的where和select方法,根据该 Series 的值、其他 Series 的值和汇总统计来赋值。然后我们将使用lambda和apply函数来构建更复杂的赋值标准。我们开始吧:
-
导入
pandas和numpy,然后加载 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") -
使用 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值传递给了groupby的observed属性。这是所有 pandas 版本在 2.1.0 之前的默认值。在后续的 pandas 版本中,groupby的默认observed=True。当observed为True且groupby中包含分类列时,只会显示观察到的值。这不会影响前一步的汇总统计结果。我仅在此处提到它,以提醒你即将发生的默认值变化。在本章其余部分我将忽略它。
- 使用 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
- 使用 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。
- 让我们使用
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值。
- 现在,让我们使用
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
-
使用
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 -
如果我们只需要处理特定的列,并且不需要将它们传递给自定义函数,我们可以使用
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 的where或select语句,或者lambda或apply语句以来,已经有很长一段时间了。在某些时候,我们需要基于一个或多个其他 Series 的值来创建或更新一个 Series。熟练掌握这些技术是个好主意。
每当有一个内置的 pandas 函数能够完成我们的需求时,最好使用它,而不是使用apply。apply的最大优点是它非常通用且灵活,但也正因为如此,它比优化过的函数更占用资源。然而,当我们想要基于现有 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列的值后添加了尾随空格。)
如何实现...
在本食谱中,我们将执行一些常见的字符串评估和清理任务。我们将使用contains、endswith和findall来分别搜索模式、尾随空格和更复杂的模式。
我们还将创建一个处理字符串值的函数,在将值分配给新 Series 之前,使用replace进行更简单的处理。让我们开始吧:
-
导入
pandas和numpy,然后加载 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) -
测试字符串中是否存在某个模式。
使用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
- 处理字符串中的前导或尾随空格。
创建一个永婚状态的 Series。首先,检查maritalstatus的值。注意有两个表示从未结婚的异常值。它们是“Never-married”后有一个额外的空格,而其他“Never-married”的值则没有尾随空格。使用startswith和endswith分别测试是否有前导空格或尾随空格。使用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
-
使用
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. 高中将变为高中。
- 使用
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
- 我们还将展示
r出现的次数。
使用concat将maritalstatus值、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返回不同类型的值。例如,我们可以使用正则表达式返回字符串中的所有数字列表。在接下来的几步中,我们将展示这一过程。
-
使用
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] -
使用
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
- 用替代值替换 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 调用的开始部分进行缺失值检查,正确处理了缺失值。
我们在进行字符串比较时,也需要注意字母的大小写。例如,Probably 和 probably 并不相等。解决这一问题的一种方法是在进行比较时,使用 upper 或 lower 方法,以防大小写的差异没有实际意义。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 病例以来的天数。让我们开始吧:
-
导入
pandas和dateutils中的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) -
显示出生月份和年份的值。
请注意,出生月份有一个缺失值。除此之外,我们将用来创建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
- 使用
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
- 使用
month和年份integers来创建日期时间列。
我们可以将字典传递给 pandas 的to_datetime函数。字典需要包含年、月和日的键。请注意,birthmonth、birthyear和birthdate没有缺失值:
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
- 使用日期时间列计算年龄。
首先,定义一个函数,当给定起始日期和结束日期时,计算年龄。请注意,我们创建了一个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
-
我们可以改用
relativedelta模块来计算年龄。我们只需要执行以下操作:nls97["age2"] = nls97.\ apply(lambda x: relativedelta(rundate, x.birthdate).years, axis=1) -
我们应该确认我们得到的值与步骤 5中的值相同:
(nls97['age']!=nls97['age2']).sum()0nls97.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 -
将字符串列转换为日期时间列。
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
-
显示日期时间列的描述性统计数据:
covidcases.casedate.nunique()214covidcases.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 -
创建一个
timedelta对象来捕捉日期间隔。
对于每一天,计算自报告首例病例以来,每个国家的天数。首先,创建一个 DataFrame,显示每个国家新病例的第一天,然后将其与完整的 COVID-19 病例数据合并。接着,对于每一天,计算从firstcasedate到casedate的天数:
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。在 步骤 3、4 和 8 中,我们处理了一些最常见的问题:缺失值、从整数部分转换日期和从字符串转换日期。birthmonth 和 birthyear 在 NLS 数据中是整数。我们确认这些值是有效的日期月份和年份。如果,举例来说,存在月份值为 0 或 20,则转换为 pandas datetime 将失败。
birthmonth 或 birthyear 的缺失值将导致 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 的行来做到这一点,然后按 location 和 casedate 排序,并保留每个 location 的第一行。接着,我们将 casedate 改名为 firstcasedate,然后将新的 firstcase DataFrame 与 COVID-19 日病例数据合并。
由于 casedate 和 firstcasedate 都是日期时间列,从后者减去前者将得到一个 timedelta 值。这为我们提供了一个 Series,表示每个国家每个报告日期自 new_cases 首次出现后的天数。报告病例日期(casedate)和首次病例日期(firstcasedate)之间的最大持续时间(dayssincefirstcase)是 1491 天,约为 4 年多。这个间隔计算对于我们想要按病毒在一个国家明显存在的时间来追踪趋势,而不是按日期来追踪趋势时非常有用。
另请参见
与其在步骤 10中使用sort_values和drop_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 操作的自然语言指令:
-
我们首先需要从 PandasAI 导入
OpenAI和SmartDataframe模块。我们还需要实例化一个llm对象:import pandas as pd from pandasai.llm.openai import OpenAI from pandasai import SmartDataframe llm = OpenAI(api_token="Your API Token") -
我们加载 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}) -
现在,我们准备好在我们的
SmartDataframe上生成 Series 的汇总统计信息。我们可以请求单个 Series 的平均值,或者多个 Series 的平均值:nls97sdf.chat("Show average of gpaoverall")2.8184077281812128nls97sdf.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 -
我们还可以通过另一个 Series 来汇总 Series 的值,通常是一个分类的 Series:
nls97sdf.chat("Show satmath average by gender")Female Male 0 486.65 516.88 -
我们还可以通过
SmartDataframe的chat方法创建一个新的 Series。我们不需要使用实际的列名。例如,PandasAI 会自动识别我们想要的是childathomeSeries,当我们写下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 -
我们可以使用
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 -
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 -
我们可以对多个同名的列进行计算:
nls97sdf = nls97sdf.chat("set weeksworkedavg to the average for weeksworked columns")
这将计算所有weeksworked00到weeksworked22列的平均值,并将其分配给一个名为weeksworkedavavg的新列。
-
我们可以根据汇总统计轻松地填补缺失的值:
nls97sdf.gpaenglish.describe()count 5,798 mean 273 std 74 min 0 25% 227 50% 284 75% 323 max 418 Name: gpaenglish, dtype: float64nls97sdf = 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 -
我们还可以使用 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).Tlocation 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 -
我们可以通过创建一个
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).Tiso_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 对象。完成这些步骤后,只需向 SmartDataframe 的 chat 方法发送简单的句子,就足以总结 Series 值并创建新的 Series。
PandasAI 擅长从 Series 中生成简单的统计信息。我们甚至不需要精确记住 Series 名称,正如我们在步骤 3中所见。我们可能使用的自然语言往往比传统的 pandas 方法(如 groupby)更直观。在步骤 4中传递给 chat 的按性别显示 satmath 平均值就是一个很好的例子。
对 Series 进行的操作,包括创建新的 Series,也是相当简单的。在步骤 5中,我们通过指示 SmartDataframe 将住在家中的孩子数与不住在家中的孩子数相加,创建了一个表示孩子总数的 Series(childnum)。我们甚至没有提供字面上的 Series 名称,childathome 和 childnotathome。PandasAI 会自动理解我们的意思。
步骤 6 和 7 展示了使用自然语言进行 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 空间,与作者和其他读者进行讨论: