【问题标题】:How to associate point on a curve with points in an array of objects?如何将曲线上的点与对象数组中的点相关联?
【发布时间】:2021-10-02 16:17:29
【问题描述】:

我有一堆来自网络的名字(名字,姓氏,来自不同国家的人)。一些国家/地区统计了每个姓氏有多少人,如here等一些地方所示。

嗯,那个日本姓氏列表只列出了前 100 名。我还有其他的名单,比如越南人列出了前 20 名,甚至在某些地方也列出了前 50 名或 1000 名。但我有 真实 的名单,最多可达 1000 多个。所以我可能有 2000 个日本姓氏,其中只有 100 个列出了该姓氏的实际人数。

我想做的是构建一个“伪造”类的库,它会根据这些统计数据生成真实的名称。我知道如何在 JavaScript 中pick a random element from a weighted array,所以一旦每个名字都包含“权重”(具有该名字的人数),只需将其插入该算法即可。

我的问题是,我如何才能在没有权重的名称上“完成曲线”?也就是说,假设我们有一个类似指数的曲线,来自 20 或 100 个具有权重的名称。然后我想从剩余的未加权列表中随机选择名称,并给它们一个值,使它们在曲线的剩余尾部有点现实。怎么可能?

例如,这里有一个带有权重的越南名字列表:

Nguyen,38
Tran,11
Le,9.5
Pham,7.1
Huynh,5.1
Phan,4.5
Vu,3.9
Đang,2.1
Bui,2
Do,1.4
Ho,1.3
Ngo,1.3
Duong,1
Ly,0.5

这是一个没有权重的列表:

An
Ân
Bạch
Bành
Bao
Biên
Biện
Cam
Cảnh
Cảnh
Cao
Cái
Cát
Chân
Châu
Chiêm
Chu
Chung
Chử
Cổ
Cù
Cung
Cung
Củng
Cừu
Dịch
Diệp
Doãn
Dũ
Dung
Dư
Dữu
Đái
Đàm
Đào
Đậu
Điền
Đinh
Đoàn
Đồ
Đồng
Đổng
Đường
Giả
Giải
Gia
Giản
Giang
Giáp
Hà
Hạ
Hậ
Hác
Hàn
Hầu
Hình
Hoa
Hoắc
Hoạn
Hồng
Hứa
Hướng
Hy
Kha
Khâu
Khổng
Khuất
Kiều
Kim
Kỳ
Kỷ
La
Lạc
Lai
Lam
Lăng
Lãnh
Lâm
Lận
Lệ
Liên
Liêu
Liễu
Long
Lôi
Lục
Lư
Lữ
Lương
Lưu
Mã
Mạc
Mạch
Mai
Mạnh
Mao
Mẫn
Miêu
Minh
Mông
Ngân
Nghê
Nghiêm
Ngư
Ngưu
Nhạc
Nhan
Nhâm
Nhiếp
Nhiều
Nhung
Ninh
Nông
Ôn
Ổn
Ông
Phí
Phó
Phong
Phòng
Phù
Phùng
Phương
Quách
Quan
Quản
Quang
Quảng
Quế
Quyền
Sài
Sầm
Sử
Tạ
Tào
Tăng
Tân
Tần
Tất
Tề
Thạch
Thai
Thái
Thang
Thành
Thảo
Thân
Thi
Thích
Thiện
Thiệu
Thôi
Thủy
Thư
Thường
Tiền
Tiết
Tiêu
Tiêu
Tô
Tôn
Tôn
Tông
Tống
Trác
Trạch
Trại
Trang
Trầm
Trâu
Trì
Triệu
Trịnh
Trương
Từ
Tư
Tưởng
Úc
Ứng
Vạn
Văn
Vân
Vi
Vĩnh
Vũ
Vũ
Vương
Vưu
Xà
Xầm
Xế
Yên

我想随机化没有权重的列表(很容易做到),然后为每个列表分配一个权重,以便它在某种程度上填充曲线的尾部,所以感觉有点现实。如何才能做到这一点?基本上,我们似乎需要获得初始加权曲线的“曲率”,然后以某种方式用新项目扩展它。它不需要是完美的,但任何可以做的近似都是很酷的。我不是统计/数学人,所以我真的不知道从哪里开始。

我没有想要的确切结果,我只是想要在某种程度上产生曲线尾部的东西。例如,列表的开头可能如下所示:

An,0.5
Ân,0.45
Bạch,0.42
Bành,0.40
Bao,0.39
...

为了尝试直观地展示我的目标,下面的黑框是提供的数据。虚线框会延伸很长一段时间,但在这里我展示了它的开始。虚线框是我们将在曲线中填充的内容,使其适合曲线起点的形状。

▐
▐
▐▐
▐▐
▐▐
▐▐▐
▐▐▐ 
▐▐▐▐ 
▐▐▐▐▐▐ 
▐▐▐▐▐▐▐▐▐▐
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐░░░░
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐░░░░░░░░░░░░░░░░░░░░░
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

所以基本上,曲线的左侧是少数几个最高值。当它向右移动时,它会根据“一些”模式变小。我们只需要大致将模式继续向右,所以它基本上延伸了曲线。

【问题讨论】:

  • 你有想要的结果的例子吗?
  • @NinaScholz 我添加了一个视觉效果来展示我想要达到的效果。

标签: javascript statistics curve-fitting


【解决方案1】:

我不是数学家,所以我只是使用 these equations 将数据拟合到 y=A*x^B 方程,尽管 Wolfram 的 some others 可能更适合您的数据。也许一些关于(sur)名称分布的论文可能会暗示一个更好的等式。

不过,目前的预测似乎还不错:

/** @type {[name: string, weight: number][]} */
const KNOWN_NAMES = [
    ['Nguyen', 38],
    ['Tran', 11],
    ['Le', 9.5],
    ['Pham', 7.1],
    ['Huynh', 5.1],
    ['Phan', 4.5],
    ['Vu', 3.9],
    ['Đang', 2.1],
    ['Bui', 2],
    ['Do', 1.4],
    ['Ho', 1.3],
    ['Ngo', 1.3],
    ['Duong', 1],
    ['Ly', 0.5],
]

/** @type {string[]} */
const UNKNOWN_NAMES = [];
for (let i = 0; i < 20; i++) UNKNOWN_NAMES[i] = `Unknown${i}`;

/**
 * Predicts the A and B for y=A*x^B (y=A*(x**B) in JS notation).
 * Based on https://mathworld.wolfram.com/LeastSquaresFittingPowerLaw.html
 * @param {number[]} data
 * @returns {[A: number, B: number]}
 */
function expCurveFit(data) {
    const n = data.length;
    let sum_ln_xy = 0;
    let sum_ln_x = 0;
    let sum_ln_y = 0;
    let sum_ln_x_pow = 0;
    for (let i = 1; i <= n; i++) {
        const x = i;
        const y = data[x - 1];
        sum_ln_xy += Math.log(x) * Math.log(y);
        sum_ln_x += Math.log(x);
        sum_ln_y += Math.log(y);
        sum_ln_x_pow += Math.log(x) ** 2;
    }
    const b_nom = (n * sum_ln_xy) - (sum_ln_x * sum_ln_y);
    const b_den = (n * sum_ln_x_pow) - (sum_ln_x ** 2);
    const b = b_nom / b_den;
    const a = (sum_ln_y - b * sum_ln_x) / n;
    return [Math.exp(a), b];
}

// Calculating the prediction function
const [A, B] = expCurveFit(KNOWN_NAMES.map(([, w]) => w));
console.log(`Fit: A=${A} B=${B}`);
/** @param {number} index */
const predict = (index) => A * ((index + 1) ** B);

// Show prediction results
console.log('== Known weights ==');
KNOWN_NAMES.forEach(([name, expected], index) => {
    const predicted = predict(index);
    console.log(`- #${index}: ${expected} VS ${predicted} => ${predicted - expected}`);
});
console.log('== Predicted tail ==');
for (let i = 0; i < 10; i++) {
    const index = KNOWN_NAMES.length + i;
    console.log(`- #${index}: ${predict(index)}`);
}
console.log('...');
console.log(`- #${2000}: ${predict(2000)}`);
console.log('...');
console.log(`- #${5000}: ${predict(5000)}`);

// Shuffle UNKNOWN, otherwise if your array is alphabetically sorted, names that
// are alphabetically "higher" will get higher weights. Wouldn't look natural.
// Based on https://stackoverflow.com/a/2450976/14274597
for (let index = UNKNOWN_NAMES.length; index;) {
    const random = Math.floor(Math.random() * index--);
    [UNKNOWN_NAMES[index], UNKNOWN_NAMES[random]] = [UNKNOWN_NAMES[random], UNKNOWN_NAMES[index]];
}

/** @type {[name: string, weight: number][]} */
const NAME_WEIGHTS = [
    // If we want to keep the original weights
    ...KNOWN_NAMES,
    // Now our predicted (offset by the number of KNOWN_WEIGHTS one)
    ...UNKNOWN_NAMES.map((name, i) => [name, predict(i + KNOWN_NAMES.length)]),
];

console.log('== Weighted names ==');
for (const [name, weight] of NAME_WEIGHTS) {
    console.log(`- ${name}${' '.repeat(30 - name.length)} ${weight}`);
}

sn-p 的控制台会切断大量的日志记录,但会显示完整的NAME_WEIGHTS 部分。无论如何都在谈论下面的结果。

对于提供的数据,这是它的预测 VS 原始权重:

- #0: 38 VS 43.69867297773659 => 5.698672977736592
- #1: 11 VS 15.989864279951354 => 4.9898642799513535
- #2: 9.5 VS 8.880479579268258 => -0.6195204207317424
- #3: 7.1 VS 5.850881554721921 => -1.2491184452780786
- #4: 5.1 VS 4.233113801814277 => -0.866886198185723
- #5: 4.5 VS 3.2494731198295947 => -1.2505268801704053
- #6: 3.9 VS 2.5984310535459065 => -1.3015689464540934
- #7: 2.1 VS 2.140907162689773 => 0.04090716268977301
- #8: 2 VS 1.8046982250005454 => -0.19530177499945456
- #9: 1.4 VS 1.548946697010319 => 0.14894669701031904
- #10: 1.3 VS 1.34896041963157 => 0.04896041963157005
- #11: 1.3 VS 1.1890208701279552 => -0.11097912987204483
- #12: 1 VS 1.0586914703306205 => 0.0586914703306205
- #13: 0.5 VS 0.9507968333083715 => 0.4507968333083715

远非完美,但这是一个好的开始。此外,对于已知名称,您可以使用原始权重。不过,预测的尾巴看起来还不错:

- #14: 0.8602568021432264
- #15: 0.7833833989610162
- #16: 0.717440740163343
- #17: 0.6603605491345175
- #18: 0.6105530322360989
- #19: 0.5667780226345167
- #20: 0.5280552551540235
- #21: 0.4936006647141039
- #22: 0.462780366132426
- #23: 0.4350768809172298
- #24: 0.4100639959533769
- #25: 0.38738780313885823
- #26: 0.3667522293417049
- #27: 0.3479078719427935
- #28: 0.33064329762683886
- #29: 0.3147781974794336
- #30: 0.30015795563424275
- #31: 0.2866493047726054
- #32: 0.27413682483865165
- #33: 0.2625201014677103
...
- #2000: 0.000711597758736851
...
- #5000: 0.0001884684445732886

这是重用/预测权重并将它们分配给名称的最终结果:

- Nguyen                         38
- Tran                           11
- Le                             9.5
- Pham                           7.1
- Huynh                          5.1
- Phan                           4.5
- Vu                             3.9
- Đang                           2.1
- Bui                            2
- Do                             1.4
- Ho                             1.3
- Ngo                            1.3
- Duong                          1
- Ly                             0.5
- Unknown9                       0.8602568021432264
- Unknown17                      0.7833833989610162
- Unknown10                      0.717440740163343
- Unknown1                       0.6603605491345175
- Unknown7                       0.6105530322360989
- Unknown16                      0.5667780226345167
- Unknown18                      0.5280552551540235
- Unknown12                      0.4936006647141039
- Unknown14                      0.462780366132426
- Unknown15                      0.4350768809172298
- Unknown6                       0.4100639959533769
- Unknown13                      0.38738780313885823
- Unknown0                       0.3667522293417049
- Unknown2                       0.3479078719427935
- Unknown11                      0.33064329762683886
- Unknown3                       0.3147781974794336
- Unknown19                      0.30015795563424275
- Unknown8                       0.2866493047726054
- Unknown5                       0.27413682483865165
- Unknown4                       0.2625201014677103

请注意,我先将未知名称洗牌,否则您会将最高预测权重分配给Unknown0,将第二高分配给Unknown1,......这会感觉很不自然。例如。如果您使用按字母顺序排序的数组,则以 A 开头的名称将非常常见,而以 Z 开头的名称将是最稀有的。

同样,Ly (0.5) 和 Unknown9 (0.86) 之间的突然跳跃显示了拟合曲线的不准确性,但话说回来,现实主义并不需要完美的名称分布。

【讨论】:

    【解决方案2】:

    我无法添加任何 javascript 代码(我不精通 js),但我可以为您指出更多信息的来源,并提供解决方案的概要。

    基本上,您想要导出的信息的复杂度为 O(C2N)、according to this answer, 或 O(n3) according to this one 但一次你已经找到了推导,答案相对容易。如果您想自己导出它,在 javascript 中,您可以使用 TensorFlow 导出您想要的信息。否则,请使用涉及 WolframAlpha 的 Kelvin Schoofs 解决方案。

    之后,只需将导出的方程代入新的“x”值,就像 Kelvin 所做的那样。

    请注意,由于名称的分布可能会因多种因素而有所不同,因此您可能需要注意这一点。这些因素可以用整数 K 来概括,according to this paper. 如果我理解正确,您的数据的“最佳拟合”将遵循"Yule-Simon" distribution,您可能可以使用它来获得更准确的“创建"数据。

    即:

    f(k;ρ) 可用于建模,例如,在大量文本集合中第 k 个最常用词的相对频率,其中根据 Zipf 定律,与 k 的(通常很小的)幂成反比。

    或者,according to this paper, 名称可以建模为

    P(n)=a⋅n-b⋅e-(n/c)d,其中P(n)表示大小不小于n的姓氏比例,b是幂指数,c是幂律部分的截断大小,d是拉伸指数函数中的拉伸参数。

    另外,如果你有访问权限,this paper might provide more information, 但我无权访问它,所以我实际上无法验证它的实用性。我认为可以概括为,姓氏可以根据Pareto distribution, 建模,在这种情况下,您可以将您的姓名插入该分布。我相信你会使用相应的概率密度函数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-04-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多