
本文介绍一种轻量、可扩展的方法,将任意一维函数(如手绘分布曲线)转化为高效采样的随机数生成器,核心采用加权轮盘选择法,兼顾精度与性能,适用于数据可视化中的合成数据生成。
在数据可视化开发中,仅依赖 Math.random() 或标准正态分布(如 d3.randomNormal())往往难以体现设计意图——比如模拟用户停留时长的长尾分布、UI 元素点击热区的双峰密度,或艺术化生成符合手绘轮廓的样本点。此时,我们需要一个能“忠实服从用户定义分布”的随机数生成器:给定一个归一化(或未归一化)的概率密度函数 f(x)(例如从 Observable 图形编辑器导出的 distribution(x)),输出满足该分布形状的随机值 x。
上述需求本质上是从连续(或近似连续)概率密度函数中进行逆变换采样(Inverse Transform Sampling)的替代方案。但由于 f(x) 通常无解析反函数、也不保证可积归一化,直接使用 CDF 逆变换不现实。因此,更实用的工程解法是:离散化 + 加权轮盘选择(Weighted Roulette Wheel Selection)。
其核心思想是:
- 将定义域 [0, 1](或任意区间)划分为 n 个等宽子区间;
- 在每个区间中点(或左端点)计算 f(x_i) 得到权重 w_i;
- 将权重累加为总和 W,再生成 [0, W) 内的均匀随机数 r;
- 顺序累减权重,首个使 r ≤ 累计权重 的索引即为选中区间,返回对应 x_i。
以下是优化后的实现(兼容 D3 v6+,无需外部依赖):
立即学习“Java免费学习笔记(深入)”;
/**
* 创建一个服从自定义密度函数的随机数生成器
* @param {Function} fn - 输入 x ∈ [0,1],输出非负密度值 y ≥ 0
* @param {number} [nbVal=50] - 离散化粒度(越大越精确,但开销略增)
* @returns {Function} 返回一个无参函数,每次调用生成一个符合分布的随机数
*/
const createDistributor = (fn, nbVal = 50) => {
// 步骤1:离散化采样,生成权重数组
const weights = Array.from({ length: nbVal }, (_, i) => {
const x = i / (nbVal - 1); // 覆盖 [0, 1] 全区间(含端点)
return Math.max(0, fn(x)); // 防止负值干扰
});
// 步骤2:预计算累计权重(提升多次调用性能)
const cumulativeWeights = [];
let sum = 0;
for (let w of weights) {
sum += w;
cumulativeWeights.push(sum);
}
const totalWeight = sum;
// 步骤3:闭包内实现高效轮盘选择
return () => {
if (totalWeight <= 0) throw new Error('Distribution function returned zero or negative total weight');
const r = Math.random() * totalWeight;
// 二分查找加速(O(log n) 替代 O(n) 线性扫描)
let left = 0, right = cumulativeWeights.length;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (cumulativeWeights[mid] < r) {
left = mid + 1;
} else {
right = mid;
}
}
// 映射回原始 x 值(线性插值可进一步提升精度)
return left / (nbVal - 1);
};
};
// ✅ 使用示例:模拟“U型”分布(两端高、中间低)
const uShaped = x => 1 - 0.8 * Math.sin(Math.PI * x);
const sampleU = createDistributor(uShaped, 100);
console.log(sampleU()); // e.g., 0.023, 0.987, 0.011...⚠️ 关键注意事项:
- 定义域对齐:本实现默认 x ∈ [0, 1]。若需其他区间(如 [a, b]),请先对输入函数做变量替换:fnScaled = x => fn(a + x*(b-a)),并在返回时还原 return a + (left/(nbVal-1))*(b-a);
- 非负性保障:fn(x) 应始终 ≥ 0;若原始函数含负值,务必用 Math.max(0, fn(x)) 截断,否则破坏概率意义;
- 精度与性能权衡:nbVal = 50 对多数可视化场景已足够;100–200 可应对高保真需求;超过 500 通常收益递减;
- 归一化非必需:算法仅依赖权重相对比例,fn(x) 无需预先积分归一化(极大降低前端计算负担);
- 进阶优化方向:对高频调用场景,可预生成 LUT(查找表);对严格统计需求,建议结合拒绝采样(Rejection Sampling)或使用专业库(如 mathjs.random.distribution)。
总结而言,该方法以极简代码实现了从“草图式分布”到“可用随机源”的跨越——它不追求理论最优,而聚焦于开发者直觉、调试友好性与运行时效率的平衡,正是现代前端数据可视化工作流中不可或缺的“胶水工具”。










