如何将曲线上的点关联到对象数组中的点?

4
我从网络上收集了许多姓名(不同国家人的名字和姓氏)。有一些国家有关于每个姓氏拥有多少人口的统计数据,如这里所示。
那个日本姓氏列表仅列出前100个。我还有其他列表,例如越南人的前20位,以及其他地方列出的前50或1000位。但是,我有真实的名字列表,其中最多可达1000+个。因此,我可能有2000个日本姓氏,只有100个列出了具有该姓氏的人数。
我想建立一个“faker”库,根据这些统计数据生成逼真的姓名。我知道如何在JavaScript中从加权数组中选择随机元素,因此一旦每个名称的“权重”(拥有该名称的人数)都包含在内,就只需要将其插入该算法中即可。
我的问题是,如何在没有权重的名称上“完成曲线”?也就是说,假设我们从拥有权重的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
...

为了尝试清晰表达我的意思,下面的黑色方框是提供的数据。虚线框会延伸很长一段时间,但我在这里展示其开头。虚线框是我们将填充到曲线中以使其符合曲线开头形状的部分。
▐
▐
▐▐
▐▐
▐▐
▐▐▐
▐▐▐ 
▐▐▐▐ 
▐▐▐▐▐▐ 
▐▐▐▐▐▐▐▐▐▐
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐░░░░
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐░░░░░░░░░░░░░░░░░░░░░
▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐▐░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

基本上,曲线的左侧是一些最高的值。随着向右移动,它会按照“某种”模式变小。我们只需要粗略地将该模式延续到右侧,这样就可以扩展曲线。

你有想要结果的示例吗? - Nina Scholz
@NinaScholz 我添加了一个可视化图表来展示我的意思。 - Lance
2个回答

3
我无法添加任何JavaScript代码(我不精通JS),但我可以为您提供更多信息来源,并提供解决方案的概述。
基本上,您想要推导的信息复杂度为O(C2N) (根据此答案)或O(n3) (根据这一个),但是一旦找到了推导,答案就相对容易了。如果您想在Javascript中自己推导,请使用TensorFlow来推导您想要的信息。否则,使用Kelvin Schoofs的解决方案,其中涉及WolframAlpha。
之后,只需将推导出的方程插入新的“x”值即可,就像Kelvin所做的那样。
请注意,由于名称的分布可能会因许多因素而异,因此您可能需要注意这一点。这些因素可以用整数K来总结,根据此论文。如果我理解正确,您的数据“最佳拟合”将遵循“Yule-Simon”分布,您可以使用它来获得更准确的“创建”数据分布。
即:

f(k; ρ)可以用来模拟例如大量文本中第k个最常见单词的相对频率,根据Zipf定律,它与k的幂函数成反比例关系(通常为小幂)。

或者,根据此论文,可以将名称建模为

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

另外,如果你有权限,这篇文章可能会提供更多信息,但是我没有权限访问它,所以我无法验证其实用性。我认为可以总结为姓氏可以根据Pareto分布进行建模,在这种情况下,你可以将你的姓名插入该分布。我相信你会使用相应的概率密度函数。


2

我不是数学家,所以我只是使用这些方程将数据拟合到一个y=A*x^B的方程中,虽然Wolfram还有其他一些可能更适合您的数据。也许一些关于(姓氏)分布的论文可以提示更好的方程。

尽管如此,当前的预测似乎并不太糟:

/** @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://dev59.com/IXE95IYBdhLWcg3wHqMV#2450976
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}`);
}

< p > Snippet的控制台截断了很多日志,但显示了完整的 NAME_WEIGHTS 部分。无论如何,下面谈论结果。 < p>对于提供的数据,这是它的预测与原始权重的比较:
- #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开头的名称将是最稀有的。
同样,Ly0.5)和Unknown90.86)之间的突然跳跃显示了拟合曲线的不准确性,但是,现实主义并不需要完美的名称分布。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接