65.9K
CodeProject 正在变化。 阅读更多。
Home

掌握 Pandas 的 MultiIndexes:强大的复杂数据分析工具

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2023 年 6 月 19 日

CPOL

12分钟阅读

viewsIcon

5114

如何使用 pandas 中的多重索引,并举例说明监测地表温度变化等实际用例。

引言

Pandas 是 Python 中广泛使用的数据处理库,提供了强大的功能来处理各种类型的数据。其一项值得注意的特性是能够处理 MultiIndexes(也称为分层索引)。在这篇博文中,我们将深入探讨 MultiIndexes 的概念,并探索如何利用它们来处理复杂的多维数据集。

理解 MultiIndexes:分析体育运动表现数据

MultiIndex 是 Pandas 的一种数据结构,允许跨多个维度或级别进行索引和访问数据。它能够为行和列创建分层结构,提供一种组织和分析数据的灵活方式。为了说明这一点,让我们考虑一个场景:您是一位私人教练或教练,在运动员进行体育活动期间监测他们的健康参数。您想在特定的时间间隔内跟踪各种参数,例如心率、跑步配速和步频。

合成健康表现数据

为了处理这类数据,让我们开始编写 Python 代码来模拟健康表现数据,特别是心率和跑步步频。

from __future__ import annotations
from datetime import datetime, timedelta
import numpy as np
import pandas as pd

start = datetime(2023, 6, 8, 14)
end = start + timedelta(hours=1, minutes=40)
timestamps = pd.date_range(start, end, freq=timedelta(minutes=1), inclusive='left')

def get_heart_rate(begin_hr: int, end_hr: int, break_point: int) -> pd.Series[float]:
    noise = np.random.normal(loc=0.0, scale=3, size=100)
    heart_rate = np.concatenate((np.linspace(begin_hr, end_hr, num=break_point), 
                 [end_hr] * (100 - break_point))) + noise
    return pd.Series(data=heart_rate, index=timestamps)

def get_cadence(mean_cadence: int) -> pd.Series[float]:
    noise = np.random.normal(loc=0.0, scale=1, size=100)
    cadence = pd.Series(data=[mean_cadence] * 100 + noise, index=timestamps)
    cadence[::3] = np.NAN
    cadence[1::3] = np.NAN
    return cadence.ffill().fillna(mean_cadence)

提供的代码片段展示了体育活动期间心率和步频的合成数据生成。它首先导入所需的模块,如 datetimenumpypandas

体育活动的持续时间定义为 100 分钟,并利用 **pd.date_range** 函数在每分钟间隔生成一系列时间戳,以覆盖此期间。

get_heart_rate 函数生成合成心率数据,假设心率线性增加到一定水平,然后在活动的其余时间保持恒定水平。引入高斯噪声以增加心率数据的变异性,使其更真实。

类似地,get_cadence 函数生成合成步频数据,假设在整个活动过程中步频相对恒定。添加高斯噪声以创建步频值的变异性,噪声值每三分钟而不是每分钟更新一次,这反映了步频相对于心率的稳定性。

有了数据生成函数后,现在就可以为两位运动员 Bob 和 Alice 创建合成数据了。

bob_hr = get_heart_rate(begin_hr=110, end_hr=160, break_point=20)
alice_hr = get_heart_rate(begin_hr=90, end_hr=140, break_point=50)
bob_cadence = get_cadence(mean_cadence=175)
alice_cadence = get_cadence(mean_cadence=165)

此时,我们拥有 Bob 和 Alice 的心率和步频。让我们使用 matplotlib 绘制它们,以获得更多的数据洞察。

from __future__ import annotations
import matplotlib.dates as mdates
import matplotlib.pyplot as plt

date_formatter = mdates.DateFormatter('%H:%M:%S')  # Customize the date format as needed

fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(111)
ax.xaxis.set_major_formatter(date_formatter)
ax.plot(bob_hr, color="red", label="Heart Rate Bob", marker=".")
ax.plot(alice_hr, color="red", label="Heart Rate Alice", marker="v")
ax.grid()
ax.legend()
ax.set_ylabel("Heart Rate [BPM]")
ax.set_xlabel("Time")

ax_cadence = ax.twinx()
ax_cadence.plot(bob_cadence, color="purple", 
                label="Cadence Bob", marker=".", alpha=0.5)
ax_cadence.plot(alice_cadence, color="purple", 
                label="Cadence Alice", marker="v", alpha=0.5)
ax_cadence.legend()
ax_cadence.set_ylabel("Cadence [SPM]")
ax_cadence.set_ylim(158, 180)

太棒了!对数据的初步分析提供了有趣的观察结果。我们可以轻松区分 Bob 和 Alice 在最高心率和心率增加速率上的差异。此外,Bob 的步频似乎明显高于 Alice。

使用 DataFrame 实现可扩展性

然而,您可能已经注意到,为每个健康参数和运动员使用单独的变量(bob_hralice_hrbob_cadencealice_cadence)的方法是不可扩展的。在现实世界中,如果运动员和健康参数数量庞大,这种方法会很快变得不切实际和繁琐。

为了解决这个问题,我们可以利用 Pandas 的强大功能,通过使用 Pandas DataFrame 来表示多个运动员和健康参数的数据。通过以表格格式组织数据,我们可以轻松地同时管理和分析多个变量。

DataFrame 的每一行可以对应一个特定的时间戳,每一列可以表示特定运动员的健康参数。这种结构允许高效地存储和操作多维数据。

通过使用 DataFrame,我们可以消除对单独变量的需求,并将所有数据存储在一个对象中。这增强了代码的清晰度,简化了数据处理,并提供了对整个数据集更直观的表示。

bob_df = pd.concat([bob_hr.rename("heart_rate"), 
         bob_cadence.rename("cadence")], axis="columns")

这就是 Bob 健康数据的 Dataframe 外观。

  心率 步频
2023-06-08 14:00:00 112.359 175
2023-06-08 14:01:00 107.204 175
2023-06-08 14:02:00 116.617 175.513
2023-06-08 14:03:00 121.151 175.513
2023-06-08 14:04:00 123.27 175.513
2023-06-08 14:05:00 120.901 174.995
2023-06-08 14:06:00 130.24 174.995
2023-06-08 14:07:00 131.15 174.995
2023-06-08 14:08:00 131.402 174.669

引入分层 DataFrame

最后一个 dataframe 已经看起来好多了!但是现在,我们仍然需要为每个运动员创建一个新的 dataframe。这时 Pandas MultiIndex 可以提供帮助。让我们看看如何优雅地将多个运动员和健康参数的数据合并到一个 dataframe 中。

from itertools import product
bob_df = bob_hr.to_frame("value")
bob_df["athlete"] = "Bob"
bob_df["parameter"] = "heart_rate"

values = {
    "Bob": {
        "heart_rate": bob_hr,
        "cadence": bob_cadence,
    },
    "Alice": {
        "heart_rate": alice_hr,
        "cadence": alice_cadence
    }
}

sub_dataframes: list[pd.DataFrame] = []
for athlete, parameter in product(["Bob", "Alice"], ["heart_rate", "cadence"]):
    sub_df = values[athlete][parameter].to_frame("values")
    sub_df["athlete"] = athlete
    sub_df["parameter"] = parameter
    sub_dataframes.append(sub_df)

df = pd.concat(sub_dataframes).set_index(["athlete", "parameter"], append=True)
df.index = df.index.set_names(["timestamps", "athlete", "parameter"])

此代码处理运动员 Bob 和 Alice 的心率和步频数据。它执行以下步骤:

  1. 为 Bob 的心率数据创建一个 DataFrame,并添加运动员和参数的元数据列。
  2. 定义一个字典,其中存储 Bob 和 Alice 的心率和步频数据。
  3. 生成运动员和参数的组合(Bob/Alice 和 heart_rate/cadence)。
  4. 对于每个组合,创建具有相应数据和元数据列的子 dataframe
  5. 将所有子 dataframe 串联成一个单独的 dataframe
  6. 将索引设置为包含时间戳、运动员和参数的级别。这就是实际 MultiIndex 被创建的地方。

这就是分层 dataframe df 的外观。

 
(Timestamp('2023-06-08 14:00:00'), 'Bob', 'heart_rate') 112.359
(Timestamp('2023-06-08 14:01:00'), 'Bob', 'heart_rate') 107.204
(Timestamp('2023-06-08 14:02:00'), 'Bob', 'heart_rate') 116.617
(Timestamp('2023-06-08 14:03:00'), 'Bob', 'heart_rate') 121.151
(Timestamp('2023-06-08 14:04:00'), 'Bob', 'heart_rate') 123.27
(Timestamp('2023-06-08 14:05:00'), 'Bob', 'heart_rate') 120.901
(Timestamp('2023-06-08 14:06:00'), 'Bob', 'heart_rate') 130.24
(Timestamp('2023-06-08 14:07:00'), 'Bob', 'heart_rate') 131.15
(Timestamp('2023-06-08 14:08:00'), 'Bob', 'heart_rate') 131.402

此时,我们得到了一个单独的 dataframe,它包含了任意数量的运动员和健康参数的所有信息。我们现在可以轻松使用 .xs 方法来查询分层 dataframe

df.xs("Bob", level="athlete")  # get all health data for Bob
 
(Timestamp('2023-06-08 14:00:00'), 'heart_rate') 112.359
(Timestamp('2023-06-08 14:01:00'), 'heart_rate') 107.204
(Timestamp('2023-06-08 14:02:00'), 'heart_rate') 116.617
(Timestamp('2023-06-08 14:03:00'), 'heart_rate') 121.151
(Timestamp('2023-06-08 14:04:00'), 'heart_rate') 123.27
df.xs("heart_rate", level="parameter")  *# get all heart rates*
 
(Timestamp('2023-06-08 14:00:00'), 'Bob') 112.359
(Timestamp('2023-06-08 14:01:00'), 'Bob') 107.204
(Timestamp('2023-06-08 14:02:00'), 'Bob') 116.617
(Timestamp('2023-06-08 14:03:00'), 'Bob') 121.151
(Timestamp('2023-06-08 14:04:00'), 'Bob') 123.27
df.xs("Bob", level="athlete").xs
     ("heart_rate", level="parameter")  # get heart_rate data for Bob
时间戳
2023-06-08 14:00:00 112.359
2023-06-08 14:01:00 107.204
2023-06-08 14:02:00 116.617
2023-06-08 14:03:00 121.151
2023-06-08 14:04:00 123.27

用例:地球温度变化

为了展示分层 DataFrame 的强大功能,让我们探讨一个真实而复杂的用例:分析过去几十年来地球表面温度的变化。为此,我们将利用 Kaggle 上提供的 数据集,该数据集总结了 美国国家航空航天局戈达德空间研究所 (NASA-GISS) 分发的全球地表温度变化数据。

检查和转换原始数据

让我们开始读取和检查数据。在深入分析之前,这一步对于更好地理解数据集的结构和内容至关重要。以下是如何使用 Pandas 来完成此操作:

from pathlib import Path

file_path = Path() / "data" / "Environment_Temperature_change_E_All_Data_NOFLAG.csv"
df = pd.read_csv(file_path , encoding='cp1252')
df.describe()

点击图片查看全尺寸

从初步检查中可以明显看出,数据组织在一个 dataframe 中,不同月份和国家有单独的行。然而,不同年份的值分布在 dataframe 的多个列中,这些列以 'Y' 前缀标记。这种格式使得有效读取和可视化数据具有挑战性。为了解决这个问题,我们将把数据转换为更结构化和分层的数据框格式,使我们能够更方便地查询和可视化数据。

from dataclasses import dataclass, field
from datetime import date
from pydantic import BaseModel

MONTHS = {
	"January": 1, 
	"February": 2, 
	"March": 3, 
	"April": 4, 
	"May": 5, 
	"June": 6, 
	"July": 7, 
	"August": 8,
  "September": 9, 
	"October": 10,
  "November": 11, 
	"December": 12
}

class GistempDataElement(BaseModel):
    area: str
    timestamp: date
    value: float

@dataclass
class GistempTransformer:
    temperature_changes: list[GistempDataElement] = field(default_factory=list)
    standard_deviations: list[GistempDataElement] = field(default_factory=list)

    def _process_row(self, row) -> None:
        relevant_elements = ["Temperature change", "Standard Deviation"]
        if (element := row["Element"]) not in relevant_elements or 
           (month := MONTHS.get(row["Months"])) is None:
            return None

        for year, value in row.filter(regex="Y.*").items():
            new_element = GistempDataElement(
                timestamp=date(year=int(year.replace("Y", "")), month=month, day=1),
                area=row["Area"],
                value=value
            )
            if element == "Temperature change":
                self.temperature_changes.append(new_element)
            else:
                self.standard_deviations.append(new_element)

    @property
    def df(self) -> pd.DataFrame:
        temp_changes_df = pd.DataFrame.from_records([elem.dict() 
                          for elem in self.temperature_changes])
        temp_changes = temp_changes_df.set_index
        (["timestamp", "area"]).rename(columns={"value": "temp_change"})

        std_deviations_df = pd.DataFrame.from_records([elem.dict() 
                            for elem in self.standard_deviations])
        std_deviations = std_deviations_df.set_index
        (["timestamp", "area"]).rename(columns={"value": "std_deviation"})

        return pd.concat([temp_changes, std_deviations], axis="columns")

    def process(self):
        environment_data = Path() / "data" / 
        "Environment_Temperature_change_E_All_Data_NOFLAG.csv"
        df = pd.read_csv(environment_data, encoding='cp1252')
        df.apply(self._process_row, axis="columns")

此代码介绍了 GistempTransformer 类,它演示了从 CSV 文件处理温度数据以及创建包含温度变化和标准偏差的分层 DataFrame

GistempTransformer 类定义为 dataclass,包含两个列表 temperature_changesstandard_deviations,用于存储处理后的数据元素。_process_row 方法负责处理输入 DataFrame 的每一行。它会检查相关元素,例如“Temperature change”和“Standard Deviation”,从 Months 列中提取月份,并创建 GistempDataElement 类的实例。然后根据元素类型将这些实例附加到相应的列表中。

df 属性通过合并 temperature_changesstandard_deviations 列表来返回一个 DataFrame。这个分层 DataFrame 具有一个 MultiIndex,其级别代表时间戳和区域,从而提供了数据的结构化组织。

transformer = GistempTransformer()
transformer.process()
df = transformer.df
  温度变化 标准偏差
(datetime.date(1961, 1, 1), 'Afghanistan') 0.777 1.95
(datetime.date(1962, 1, 1), 'Afghanistan') 0.062 1.95
(datetime.date(1963, 1, 1), 'Afghanistan') 2.744 1.95
(datetime.date(1964, 1, 1), 'Afghanistan') -5.232 1.95
(datetime.date(1965, 1, 1), 'Afghanistan') 1.868 1.95

分析气候数据

现在我们已经将所有相关数据整合到一个 dataframe 中,我们可以继续检查和可视化数据。我们的重点是检查每个区域的线性回归线,因为它们提供了对过去几十年温度变化的总体趋势的见解。为了便于可视化,我们将创建一个函数来绘制温度变化及其相应的回归线。

def plot_temperature_changes(areas: list[str]) -> None:
    fig = plt.figure(figsize=(12, 6))
    ax1 = fig.add_subplot(211)
    ax2 = fig.add_subplot(212)

    for area in areas:
        df_country = df[df.index.get_level_values("area") == area].reset_index()
        dates = df_country["timestamp"].map(datetime.toordinal)
        gradient, offset = np.polyfit(dates, df_country.temp_change, deg=1)
        ax1.scatter(df_country.timestamp, df_country.temp_change, label=area, s=5)
        ax2.plot(df_country.timestamp, gradient * dates + offset, label=area)

    ax1.grid()
    ax2.grid()
    ax2.legend()
    ax2.set_ylabel("Regression Lines [°C]")
    ax1.set_ylabel("Temperature change [°C]")

在此函数中,我们在 Pandas MultiIndex 上使用 **get_level_values** 方法,以在不同级别上有效地查询分层 Dataframe 中的数据。让我们使用此函数来可视化不同大陆的温度变化。

plot_temperature_changes
(["Africa", "Antarctica", "Americas", "Asia", "Europe", "Oceania"])

continents

从这张图中,我们可以得出几个关键结论:

  • 所有大陆的回归线都具有正斜率,这表明全球地表温度呈上升趋势。
  • 与其它大陆相比,欧洲的回归线斜率明显更陡,这意味着欧洲的温度升幅更为明显。这一发现与欧洲升温速度快于其它地区的观察结果一致。
  • 导致欧洲温度升幅高于南极洲的具体因素很复杂,需要详细的科学研究。然而,一个促成因素可能是洋流的影响。欧洲受到湾流等暖洋流的影响,这些洋流将热量从热带地区输送到该区域。这些洋流在调节温度方面发挥作用,并可能导致欧洲观察到的相对升温幅度更大。相比之下,南极洲被寒冷的洋流包围,其气候深受南大洋和南极环流的影响,它们阻碍了温暖海水的入侵,从而限制了升温效应。

现在,让我们将分析重点放在欧洲本身,检查欧洲不同地区的温度变化。我们可以通过为每个欧洲地区创建单独的图来做到这一点。

plot_temperature_changes
(["Southern Europe", "Eastern Europe", "Northern Europe", "Western Europe"])

从欧洲不同地区绘制的温度变化来看,我们观察到整个欧洲大陆的总体温度上升幅度相当。虽然不同地区回归线的陡峭程度可能略有差异,例如东欧的回归线比南欧略陡,但在地区之间未观察到显著差异。

受气候变化影响最大和最小的十个国家

现在,让我们将重点转移到识别自 2000 年以来平均温度升幅最大的前 10 个国家。以下是如何检索国家列表的示例:

df[df.index.get_level_values(level="timestamp") > 
date(2000, 1, 1)].groupby("area").mean().sort_values
(by="temp_change",ascending=False).head(10)
区域 温度变化 标准偏差
斯瓦尔巴群岛和扬马延岛 2.61541 2.48572
爱沙尼亚 1.69048 nan
科威特 1.6825 1.12843
白俄罗斯 1.66113 nan
芬兰 1.65906 2.15634
斯洛文尼亚 1.6555 nan
俄罗斯联邦 1.64507 nan
巴林 1.64209 0.937431
东欧 1.62868 0.970377
奥地利 1.62721 1.56392

为了提取自 2000 年以来平均温度升幅最大的前 10 个国家,我们执行以下步骤:

  1. 使用 df.index.get_level_values(level='timestamp') >= date(2000, 1, 1) 过滤 dataframe,使其仅包含年份大于或等于 2000 的行。
  2. 使用 .groupby('area') 按 'Area'(国家)对数据进行分组。
  3. 使用 .mean() 计算每个国家的平均温度变化。
  4. 使用 **.sort_values(by="temp_change",ascending=True).head(10)** 选择平均温度变化最大的前 10 个国家。

这一结果与我们之前的观察一致,证实欧洲比其它大陆经历了更高的温度升幅。

继续我们的分析,现在让我们探讨受温度升幅影响最小的十个国家。我们可以利用与之前相同的方法来提取这些信息。以下是如何检索国家列表的示例:

df[df.index.get_level_values(level="timestamp") > date(2000, 1, 1)].groupby
  ("area").mean().sort_values(by="temp_change",ascending=True).head(10)
区域 温度变化 标准偏差
皮特凯恩群岛 0.157284 0.713095
马绍尔群岛 0.178335 nan
南乔治亚岛和南桑威奇群岛 0.252101 1.11
密克罗尼西亚(联邦) 0.291996 nan
智利 0.297607 0.534071
威克岛 0.306269 nan
诺福克岛 0.410659 0.594073
阿根廷 0.488159 0.91559
津巴布韦 0.493519 0.764067
南极洲 0.527987 1.55841

我们观察到此列表中的大多数国家都是位于南半球的偏远小岛。这一发现进一步支持了我们之前的结论,即与其它地区相比,南部大陆(尤其是 南极洲)受气候变化的影响较小。

夏季和冬季的温度变化

现在,让我们深入进行更复杂的查询,使用分层 dataframe。在本特定用例中,我们的重点是分析冬季和夏季的温度变化。为了本次分析的目的,我们将冬季定义为 12 月、1 月和 2 月,而夏季则包括 6 月、7 月和 8 月。通过利用 Pandas 和分层 dataframe 的强大功能,我们可以轻松可视化欧洲在这些季节的温度变化。以下是完成此任务的示例代码片段:

all_winters = df[df.index.get_level_values(level="timestamp").map
(lambda x: x.month in [12, 1, 2])]
all_summers = df[df.index.get_level_values(level="timestamp").map
(lambda x: x.month in [6, 7, 8])]
winters_europe = all_winters.xs("Europe", level="area").sort_index()
summers_europe = all_summers.xs("Europe", level="area").sort_index()

fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(111)
ax.plot(winters_europe.index, winters_europe.temp_change, 
label="Winters", marker="o", markersize=4)
ax.plot(summers_europe.index, summers_europe.temp_change, 
label="Summers", marker='o', markersize=4)
ax.grid()
ax.legend()
ax.set_ylabel("Temperature Change [°C]")

从这张图中,我们可以观察到冬季的温度变化比夏季的温度变化更不稳定。为了量化这种差异,让我们计算两个季节的温度变化标准差。

pd.concat([winters_europe.std().rename("winters"), 
summers_europe.std().rename("summers")], axis="columns")
  冬季 夏季
温度变化 1.82008 0.696666

结论

总之,精通 Pandas 中的 MultiIndex 为处理复杂的数据分析任务提供了强大的工具。通过利用 MultiIndex,用户可以以灵活直观的方式有效地组织和分析多维数据集。处理行和列的分层结构的能力增强了代码的清晰度,简化了数据处理,并能够同时分析多个变量。无论是跟踪运动员的健康参数还是分析地球随时间的温度变化,理解和利用 Pandas 中的 MultiIndex 都能充分发挥该库处理复杂数据场景的潜力。

您可以在此处找到此帖子中包含的所有代码:https://github.com/GlennViroux/pandas-multi-index-blog

© . All rights reserved.