2. 全图仅剩一个点:聚合与分组键误用定位

问题现象与最小复现

散点图或折线图只出现一个点,通常不是绘图库坏了,而是进入绘图函数的数据本身只剩一行。常见来源有三类:分组键选错(把唯一标识列当成了分组列)、聚合函数误用(对整表做了一次全局聚合)、索引重置缺失(分组后索引被压平,绘图时按索引对齐导致只剩一个有效点)。下面用一份可复现的教学数据定位问题。

import pandas as pd
import matplotlib.pyplot as plt

# 教学示例数据:3 个样本,每个样本 4 个时间点
df = pd.DataFrame({
    "sample_id": ["S1"]*4 + ["S2"]*4 + ["S3"]*4,
    "time_h":    [0, 1, 2, 3] * 3,
    "signal":    [1.0, 1.8, 2.4, 3.1,
                  0.9, 1.5, 2.0, 2.6,
                  1.2, 2.0, 2.7, 3.4],
})

# 错误写法 A:把唯一标识当分组键
bad_a = df.groupby("sample_id").mean(numeric_only=True)
print(bad_a.shape)   # (3, 2) —— 每个样本被压成一行

# 错误写法 B:全局聚合,整表只剩一行
bad_b = df.agg({"signal": "mean"})
print(bad_b.shape)   # (1,) —— 这就是"一个点"的直接来源

# 错误写法 C:分组后未 reset_index,绘图按索引对齐
bad_c = df.groupby("time_h")["signal"].mean()
print(bad_c.index.name)  # time_h,索引被替换,原 sample_id 丢失

三种写法都会让绘图输入的行数骤减。判断依据很直接:绘图前打印 df.shape 与 df.head(),若行数等于分组数而非原始观测数,就说明数据已被压缩。

原始数据 12 行观测 sample_id × time_h signal 连续值 shape = (12, 3) A 分组键误用 groupby("sample_id").mean() 每样本压成 1 行 → (3, 2) B 全局聚合 df.agg({"signal":"mean"}) 整表压成 1 行 → (1,) C 索引未重置 groupby("time_h")["signal"].mean() 索引替换,绘图对齐失败 绘图输入 行数 = 1 → 单点 散点/折线均失效 修复 保留观测粒度 reset_index() 核对 shape 再绘图 判断依据:绘图前 df.shape 行数应等于原始观测数;若等于分组数或 1,即为聚合/分组键/索引问题

定位与修复

按“先看行数、再看分组键、最后看索引”的顺序排查,能快速锁定问题。修复的核心是保留观测粒度:绘图数据必须是每个时间点一行,而不是每个样本一行。

# 修复 A:分组键改为真正的时间列,并保留样本维度
fixed_a = (df.groupby(["sample_id", "time_h"], as_index=False)
             .agg(signal_mean=("signal", "mean")))
print(fixed_a.shape)   # (12, 3) —— 与原始观测数一致

# 修复 B:若确实需要全局统计,单独计算,不要覆盖绘图数据
global_mean = df["signal"].mean()
print(f"全局均值 = {global_mean:.3f}")   # 仅作参考线,不替代散点

# 修复 C:分组后重置索引,恢复可对齐的列
fixed_c = (df.groupby("time_h", as_index=False)
             .agg(signal_mean=("signal", "mean")))
print(fixed_c.columns.tolist())  # ['time_h', 'signal_mean']

# 绘图验证
fig, ax = plt.subplots(figsize=(6, 4))
for sid, g in df.groupby("sample_id"):
    ax.plot(g["time_h"], g["signal"], marker="o", label=sid)
ax.axhline(global_mean, color="gray", linestyle="--", linewidth=1, label="全局均值")
ax.set_xlabel("time_h")
ax.set_ylabel("signal")
ax.legend()
plt.tight_layout()
plt.show()
修复后应看到 3 条折线、每条 4 个点,共 12 个标记点,并有一条灰色全局均值参考线。若仍只有一个点,继续检查 ax.plot 传入的 x、y 是否被误传为标量。

验证清单

检查项期望值异常含义
绘图数据行数等于原始观测数(本例 12)行数=分组数或 1,说明被聚合压缩
分组键列时间/类别列,非唯一标识用 sample_id 分组会丢失时间维度
索引状态已 reset_index 或 as_index=False索引替换导致绘图对齐失败
绘图标记数与数据行数一致标记数少于行数,检查 x/y 传参

本例的关键结论:单点问题几乎总能在绘图前的数据形状上找到证据。把 shape、分组键和索引状态三处核对清楚,再进入绘图环节,就能避免把数据压缩问题误判为坐标轴或渲染问题。