4. 对数坐标下零值与负值导致的绘图中断

问题定位:对数轴为什么“画不出来”

对数坐标的数学定义域是 x > 0。当数据列中出现 0 或负值时,不同绘图库的处理方式并不一致:有的在坐标变换阶段直接抛出异常(如 ValueError: Data has no positive values, and therefore cannot be log-scaled),有的把非正值静默映射为 NaN 从而“丢点”,还有的只保留正值但把整条折线断开。因此排障的第一步不是急着改数据,而是先确认是报错还是丢点,再确认非正值的数量与位置。

下面用一段可复现的示例数据来演示。假设我们有一组随时间变化的浓度测量值,其中混入了 0 和负值(仪器下限截断或背景扣除后的结果):

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 示例数据:含 0 与负值
t = np.arange(1, 13)
conc = np.array([5.2, 3.8, 2.1, 0.0, 0.9, -0.4, 0.6, 0.35, 0.12, 0.0, 0.05, 0.02])
df = pd.DataFrame({"t": t, "conc": conc})

print("非正值数量:", (df["conc"] <= 0).sum())
print("非正值位置:", df.loc[df["conc"] <= 0, "t"].tolist())
# 非正值数量: 3
# 非正值位置: [4, 6, 10]

直接调用 ax.set_yscale("log") 时,Matplotlib 会忽略非正值并在控制台给出警告,折线在对应位置断开;若整列都非正,则直接报错。定位阶段应始终先打印非正值的数量、索引和原始值,而不是只看图。

对数轴非正值:三种处理路径与数值—位置一致性 原始序列 conc(t=1…12) 5.23.82.1 0.0 0.9 -0.4 0.60.350.12 0.0 0.050.02 方案 A:过滤非正值 df[df.conc > 0] 保留 9 个点,删除 t=4,6,10 折线在缺口处断开 数值不变,位置真实 适合:非正值确为无效/缺失 验证:log10(0.9)=-0.046 与 y 轴刻度位置一致 方案 B:伪对数 symlog ax.set_yscale("symlog", linthresh=0.1) 保留全部 12 个点 |x|<0.1 区间线性,其余对数 0 与负值可显示 适合:需保留零/负值语义 验证:0 落在 y=0 线性段 -0.4 与 0.4 对称分布 方案 C:分段坐标 正值段 log + 非正段线性 两段各自映射,拼接显示 需自定义变换函数 适合:量级跨度大且含负值 注意:段间刻度需标注 验证:两段边界值连续 刻度标签不重叠

三种方案的实现与选择依据

方案 A(过滤)最稳妥,前提是你能确认非正值属于无效数据。实现时用布尔掩码过滤,并保留原始索引以便追溯:

mask = df["conc"] > 0
df_pos = df[mask].copy()
ax.plot(df_pos["t"], df_pos["conc"], marker="o")
ax.set_yscale("log")
# 缺口处折线断开,符合“数据缺失”的语义

方案 B(伪对数 symlog)适合零和负值本身有物理意义(如背景扣除后的净信号)。linthresh 决定线性区宽度,应取略大于最小正值的量级,避免线性区吞掉有效数据:

ax.plot(df["t"], df["conc"], marker="o")
ax.set_yscale("symlog", linthresh=0.1)
ax.axhline(0, color="gray", lw=0.8, ls="--")

方案 C(分段坐标)在需要同时展示大正值与负值时使用,通常通过自定义 FuncScale 或双轴实现,成本最高,仅在 A、B 都不满足语义时采用。

选择顺序建议:先判断非正值是否为无效数据 → 是则用 A;否则判断零/负值是否有意义 → 有意义且量级跨度不大用 B;跨度极大且必须同图对比再用 C。

验证:变换后数值与视觉位置是否一致

无论选哪种方案,都要做一次“数值—位置”核对,防止坐标变换悄悄改变了数据含义。对纯对数轴,取两个已知点计算其在轴上的相对位置:

import numpy as np
# 对数轴上,值 v 的位置比例 = (log10(v) - log10(ymin)) / (log10(ymax) - log10(ymin))
ymin, ymax = 0.01, 10
for v in [0.02, 0.12, 5.2]:
    frac = (np.log10(v) - np.log10(ymin)) / (np.log10(ymax) - np.log10(ymin))
    print(f"v={v:>5} 位置比例={frac:.3f}")
# v= 0.02 位置比例=0.101
# v= 0.12 位置比例=0.360
# v= 5.2  位置比例=0.905

把这三个比例与图中数据点的实际像素位置对照,若偏差超过 1~2 像素,说明存在额外的坐标变换或数据被意外修改。对 symlog,则要额外确认 0 恰好落在 y=0 的线性段中心,且 -0.4 与 0.4 关于零线对称。

常见陷阱:过滤后忘记重置索引,导致后续按位置取数错位;或对已过滤数据仍用原始 t 做插值,产生虚假的连续折线。过滤后应显式 reset_index(drop=True) 或始终按 t 列对齐。

排障检查清单

检查项判断依据处理动作
是否报错控制台出现 log 相关 ValueError确认整列是否全为非正
非正值数量与位置(df[col] <= 0).sum()记录索引,判断是否无效数据
丢点还是断线点数少于原始行数检查是否被静默转 NaN
方案语义匹配零/负值是否有物理意义A / B / C 三选一
数值—位置一致性log10 比例 vs 像素位置偏差大则排查额外变换