AI 的瞎咧咧:0️⃣ 问题抽象(数学视角)#

你原始数据是一个样本集合: [ X = {x_1, x_2, \dots, x_n}
]

你只能存一小部分信息
目标不是“精确复原 (X)”,而是构造一个新样本集合: [
\tilde{X} = {\tilde{x}_1, \dots, \tilde{x}_m}
] 使得:

  • 分布形态尽量接近
  • 一阶 / 二阶统计量接近
  • 尾部和边界不炸

1️⃣ 平均值 & 标准差(一阶 / 二阶矩)#

定义#

[
\mu = \frac{1}{n}\sum x_i
] [
\sigma = \sqrt{\frac{1}{n}\sum (x_i - \mu)^2}
]

数学意义#

  • (\mu):位置(location)
  • (\sigma):尺度(scale)

但它们只刻画“中心 + 离散程度”
完全不描述分布形状 👉 所以我们没有用“正态假设直接生成”,而是只把它们当 约束条件 / 噪声尺度

2️⃣ 分位数(Quantiles)——形状的骨架#

定义#

p 分位数 (Q(p)) 满足: [
P(X \le Q(p)) = p
] 你存的是: [
Q(0), Q(0.05), Q(0.25), Q(0.5), Q(0.75), Q(0.95), Q(1)
]

数学本质#

  • 这是在 CDF(累积分布函数) 上打“锚点”
  • 它们是 分布形状的骨架 相比矩:
方法是否稳健
高阶矩❌ 对离群点极敏感
分位数✅ 稳定、鲁棒

3️⃣ 逆变换采样(Inverse Transform Sampling)#

这是整个还原过程的核心数学原理

理论基础#

设:

  • (U \sim \text{Uniform}(0,1))
  • (F(x)) 是原分布的 CDF 则: [
    X = F^{-1}(U)
    ] 服从分布 (F)。

在你这个问题里的变形#

你不知道完整的 (F),
但你知道 若干个点上的 (F^{-1}(p))

p : 0.0  0.05  0.25  0.50  0.75  0.95  1.0
x : min  p05   p25   p50   p75   p95   max

于是你构造一个 分段线性的 (\tilde{F}^{-1}): [
\tilde{F}^{-1}(p) =
\text{linear interpolation between known quantiles}
] 这就是:

np.interp(u, probs, quantiles)

数学意义#

  • 你在 概率空间均匀采样
  • 再映射回 数值空间
  • 保证每个分位区间占对的比例

👉 分布形状被保留下来

4️⃣ 线性插值(Piecewise Linear Approximation)#

数学形式#

对于 (p \in [p_i, p_{i+1}]):

[#

\tilde{F}^{-1}(p) x_i + (x_{i+1}-x_i)\frac{p - p_i}{p_{i+1}-p_i}
] 这是对真实逆 CDF 的一阶近似。

为什么不用更高阶插值?#

方法问题
样条会过冲(overshoot)
多项式不单调
线性单调、稳定、可解释

👉 工程里:宁可钝一点,也不炸


5️⃣ 加噪声(正则化思想)#

插值本身的问题:

会产生“硬折线分布”,点都落在几条线上 于是我们加一个 小扰动项: [
\tilde{x}’ = \tilde{x} + \varepsilon
] [
\varepsilon \sim \mathcal{N}(0, \alpha\sigma)
] 其中: [
\alpha \ll 1 \quad (\text{如 } 0.02)
]

数学解释#

  • 相当于对估计分布做 核平滑
  • 类似 KDE 中的 bandwidth 但我们:
  • 不需要存 kernel
  • 不需要存全样本

6️⃣ 边界裁剪(Support Constraint)#

你存了: [
x_{\min}, x_{\max}
] 于是强制: [
\tilde{x} \in [x_{\min}, x_{\max}]
]

数学意义#

  • 限制分布的 support
  • 防止噪声破坏物理 / 业务边界

7️⃣ 整体等价于什么(高层视角)#

你做的事情 ≈

用有限分位点构造经验分布函数(EDF)的近似 再从这个近似 EDF 中采样。 这在统计学里是完全正统的做法

8️⃣ 为什么它是“接近最优”的#

条件满足
存储极小
不依赖分布假设
鲁棒
可复现
不产生非法值

Show the Code#

def estimate_precision(arr, eps=1e-12):
    """
    估计数据精度(最小步长)
    """
    arr = np.asarray(arr, dtype=float)

    uniq = np.unique(arr)
    if uniq.size < 2:
        return 0.0  # 常数数组

    diffs = np.diff(np.sort(uniq))
    diffs = diffs[diffs > eps]

    if diffs.size == 0:
        return 0.0

    # 用中位数,避免异常
    return float(np.median(diffs))

def summarize_array(arr):
    arr = np.asarray(arr, dtype=float)
    if arr.size == 0:
        raise ValueError("array is empty")

    precision = estimate_precision(arr)

    summary = {
        "n": int(arr.size),
        "mean": float(arr.mean()),
        "std": float(arr.std(ddof=0)),
        "min": float(arr.min()),
        "max": float(arr.max()),
        "p05": float(np.percentile(arr, 5)),
        "p25": float(np.percentile(arr, 25)),
        "p50": float(np.percentile(arr, 50)),
        "p75": float(np.percentile(arr, 75)),
        "p95": float(np.percentile(arr, 95)),
        "precision": precision,
    }
    return summary
    
def restore_array(summary, size=None, random_state=None):
    rng = np.random.default_rng(random_state)

    if size is None:
        size = summary["n"]

    quantiles = np.array([
        summary["min"],
        summary["p05"],
        summary["p25"],
        summary["p50"],
        summary["p75"],
        summary["p95"],
        summary["max"],
    ])

    probs = np.array([0.0, 0.05, 0.25, 0.5, 0.75, 0.95, 1.0])

    u = rng.uniform(0.0, 1.0, size)
    restored = np.interp(u, probs, quantiles)

    # 加轻噪声
    noise_scale = summary["std"] * 0.02
    restored += rng.normal(0, noise_scale, size)

    # 裁剪边界
    restored = np.clip(restored, summary["min"], summary["max"])

    # ★ 精度量化(关键)
    precision = summary.get("precision", 0.0)
    if precision > 0:
        restored = np.round(restored / precision) * precision
    return restored