过渡期间的舍入误差

在巴伐利亚州,平均成绩达到\(2{,}33\)即可进入\(2{,}34\)则不符合条件。在这个分界点上,两次四舍五入操作的顺序决定了孩子的成绩最终落在哪一边。Günther Felbinger 在一篇博文中探讨了这种四舍五入效应的潜在后果。出于兴趣,我自行计算了模型中所有允许的成绩组合所产生的影响。


我的核心问题是:提前将各科成绩四舍五入到整数,与使用未四舍五入的科目平均分进行计算相比,会导致学校成绩计算方式的差异吗?此外,如果像文中建议的那样,先将各科平均分四舍五入到小数点后一位,结果又会如何变化呢?

根据巴伐利亚州小学规章第6条,平均成绩由德语、数学和地方及通识教育三门科目的成绩计算得出。实施细则规定\(2{,}33\) (含2.33), \(2{,}66\) (含2.66)。在以下计算模型中,每个总平均成绩均保留两位小数,不再进行四舍五入。例如……

\[
\frac{2{,}33+2{,}34+2{,}34}{3}=2{,}3366\ldots \longrightarrow 2{,}33.
\]

这对于分类至关重要: \(2{,}3366\ldots\)不会变成\(2{,}34\) ,而是\(2{,}33\) ,因此仍然足以满足文法学校的要求。

初始条目展示了使用假定的学科平均值\(1{,}6\)\(2{,}6\)\(2{,}6\)进行早期四舍五入的情况。由此,首先得出整数等级\(2\)\(3\)\(3\) 。它们的平均值是

\[
\frac{2+3+3}{3}=2{,}6666\ldots \longrightarrow 2{,}66,
\]

这意味着该学生不符合直接进入文法学校的资格。然而,如果不四舍五入到整数,假设示例中的结果将是……

\[
\frac{1{,}6+2{,}6+2{,}6}{3}=2{,}2666\ldots \longrightarrow 2{,}26.
\]

在第二个例子中,舍入方向相反: \(2{,}4\)\(3{,}4\)\(2{,}4\)分别变为整音符值\(2\)\(3\)\(2\) ,正式值为\(2{,}33\) 。然而,未舍入的值则为\(2{,}73\) 。这些例子清晰地展示了这种效果。但是,它们尚未揭示这种现象发生的频率,也未说明采用不同的舍入方法会产生什么后果。

因此,我的计算使用了清晰的结果空间。每个受试者的均值可以是其中的\(501\)值之一。

\[
1{,}00,\, 1{,}01,\, 1{,}02,\, \ldots, \, 5{,}99, \, 6{,}00
\]

假设。我对德语、数学、地方研究和通识研究的所有有序组合赋予相同的权重。因此,有

\[
501^3=125\,751\,501
\]

案例分析。因此,该模型虽然明确,但却是人为的:它假设受试者的平均值均匀分布,三个受试者相互独立,且步长恰好为百分之一。它并不能反映巴伐利亚小学生实际的分布情况。

对于三个主题均值\(d,m,h\)我们写

\[
A(d,m,h)=\frac{\left\lfloor 100\cdot\frac{d+m+h}{3}\right\rfloor}{100}
\]

总体平均值保留两位小数。现在我们比较三种计算方法。:

\[
\begin{aligned}
A_0 &= A(\operatorname{round}(d),\operatorname{round}(m),\operatorname{round}(h)),\\
A_1 &= A(\operatorname{round}_1(d),\operatorname{round}_1(m),\operatorname{round}_1(h)),\\
A_\mathrm{ref} &= A(d,m,h)。
\end{aligned}
\]

\(A_0\)对应于使用整数等级的计算结果, \(A_1\)对应于保留一位小数的方案,而\(A_\mathrm{ref}\)对应于模型中未四舍五入的参考值。例如,该计算可以直接用Rust实现。:

const SCHOOL_GYMNASIUM: i32 = 0;
const SCHOOL_REALSCHULE: i32 = 1;
const SCHOOL_MITTELSCHULE: i32 = 2;
const FLOAT_TOLERANCE: f64 = 1e-9;

#[derive(Clone, Copy, Default)]
struct Statistic {
    conflict_count: u64,
    total_count: u64,
}

fn determine_school(average: f64) -> i32 {
    if average <= 2.33 {
        return SCHOOL_GYMNASIUM;
    }
    if average <= 2.66 {
        return SCHOOL_REALSCHULE;
    }
    SCHOOL_MITTELSCHULE
}

fn calculate_average(note_deutsch: f64, note_mathe: f64, note_hsu: f64) -> f64 {
    let average = (note_deutsch + note_mathe + note_hsu) / 3.0;
    ((average + FLOAT_TOLERANCE) * 100.0).floor() / 100.0
}

fn round_to_whole_grade(grade: f64) -> f64 {
    grade.round()
}

fn round_to_one_decimal(grade: f64) -> f64 {
    (grade * 10.0).round() / 10.0
}

fn normalize_grade(grade: f64) -> f64 {
    (grade * 100.0).round() / 100.0
}

fn percentage(count: u64, total: u64) -> f64 {
    count as f64 / total as f64 * 100.0
}

fn main() {
    let mut combination_count = 0;
    let mut official_reference_conflicts = 0;
    let mut official_proposal_conflicts = 0;
    let mut proposal_reference_conflicts = 0;
    let mut statistics = [Statistic::default(); 4];

    let mut note_deutsch = 1.0;
    while note_deutsch <= 6.0 {
        let mut note_mathe = 1.0;
        while note_mathe <= 6.0 {
            let mut note_hsu = 1.0;
            while note_hsu <= 6.0 {
                combination_count += 1;

                let official_average = calculate_average(
                    round_to_whole_grade(note_deutsch),
                    round_to_whole_grade(note_mathe),
                    round_to_whole_grade(note_hsu),
                );
                let proposed_average = calculate_average(
                    round_to_one_decimal(note_deutsch),
                    round_to_one_decimal(note_mathe),
                    round_to_one_decimal(note_hsu),
                );
                let reference_average =
                    calculate_average(note_deutsch, note_mathe, note_hsu);
                let official_school = determine_school(official_average);
                let proposed_school = determine_school(proposed_average);
                let reference_school = determine_school(reference_average);

                if official_average == 3.0 {
                    statistics[0].total_count += 1;
                    if reference_school != SCHOOL_MITTELSCHULE {
                        statistics[0].conflict_count += 1;
                    }
                }
                if official_average == 2.66 {
                    statistics[1].total_count += 1;
                    if reference_school == SCHOOL_GYMNASIUM {
                        statistics[1].conflict_count += 1;
                    }
                }
                if official_average == 2.33 {
                    statistics[2].total_count += 1;
                    if reference_school != SCHOOL_GYMNASIUM {
                        statistics[2].conflict_count += 1;
                    }
                }
                if official_average == 2.66 {
                    statistics[3].total_count += 1;
                    if reference_school == SCHOOL_MITTELSCHULE {
                        statistics[3].conflict_count += 1;
                    }
                }

                if official_school != reference_school {
                    official_reference_conflicts += 1;
                }
                if official_school != proposed_school {
                    official_proposal_conflicts += 1;
                }
                if proposed_school != reference_school {
                    proposal_reference_conflicts += 1;
                }

                note_hsu = normalize_grade(note_hsu + 0.01);
            }
            note_mathe = normalize_grade(note_mathe + 0.01);
        }
        note_deutsch = normalize_grade(note_deutsch + 0.01);
    }

    println!(
        "official/reference: {}%",
        percentage(official_reference_conflicts, combination_count)
    );
    println!(
        "official/proposal: {}%",
        percentage(official_proposal_conflicts, combination_count)
    );
    println!(
        "proposal/reference: {}%",
        percentage(proposal_reference_conflicts, combination_count)
    );
    for (index, statistic) in statistics.iter().enumerate() {
        println!(
            "#{}: {}% ({}/{})",
            index + 1,
            percentage(statistic.conflict_count, statistic.total_count),
            statistic.conflict_count,
            statistic.total_count
        );
    }
}

较小的容差 calculate_average 防止出现诸如此类的值。 \(2{,}67\) 由于其二进制浮点数表示形式,它被错误地认为 \(2{,}669999\ldots\) 被切断了。 normalize_grade 此外,它将每个循环值还原到百分之一阶网格。至少存在两种不同的可能平均值。 \(1/300\) 分开;容忍度 \(10^{-9}\) 因此,它的大小足以不改变任何真正的临界案例。

程序已编译并启动。

rustc -O calc.rs && ./calc

该程序遍历所有\(125\,751\,501\)种组合并返回结果。:

比较部分
整数成绩与未四舍五入的科目平均成绩\(10{,}4411\,\%\)
与提案相比,成绩以整数形式呈现,保留一位小数。\(10{,}1521\,\%\)
与未四舍五入的专家平均值相比,保留一位小数的提案\(0{,}6615\,\%\)

对我而言,核心价值是 \(10{,}4411\,\%\): 在超过十分之一的组合中,提前将成绩四舍五入到整数会导致计算出的学校类型与未四舍五入的参考值不同。该比例仅描述了所选模型中的四舍五入效应。

改用保留一位小数的专家平均值会导致所有模型案例中有\(10{,}1521\,\%\)的分类结果与之前的方法不同。然而,第三项比较至关重要:与未四舍五入的参考值相比,保留一位小数的方法仅导致\(0{,}6615\,\%\)案例出现偏差。因此,与参考值的偏差降低了

\[
1-\frac{0{,}6615}{10{,}4411}\approx 93{,}7\,\%.
\]

对我而言,这正是当前计算方法令人质疑的后果:假定的学科特定成绩保持不变;只有四舍五入的时间点会改变计算出的学校类型。虽然使用一位小数进行计算并不能完全消除这种影响,但它在模型中显著降低了这种影响。

然而,这些数据并不能告诉我们究竟有多少孩子受到影响。成绩单上的分数并非一定是机械地四舍五入取平均值,学业成绩既非均匀分布也非相互独立,而且孩子实际选择学校类型也并非仅仅取决于这三个因素。我的计算旨在回答一个更具体的问题:当所有百分位数值组合出现的概率均等时,四舍五入会如何改变计算出的成绩?

最后,与原帖进行比较:原帖中提到的总体比例约为\(10\,\%\)与我的计算结果\(10{,}4411\,\%\)接近。然而,我的计算结果与原帖中提到的四个具体案例(每个案例的\(1/6 \approx 17\,\%\)对于“未上中学( \(3{,}00\) ”、“未上文法学校( \(2{,}33\) )”、“上文法学校( \(2{,}66\) ”和“上中学( \(2{,}66\) \(1{,}4615\,\%\) \(59{,}6096\,\%\) \(1{,}2023\,\%\) \(62{,}9395\,\%\)

这四个值是不同规模参考组的条件概率。如果没有重新定义样本空间\(1/6\) 。因此,我的计算证实了舍入效应,但并未证实这些单独的概率。无论如何,要确定受影响儿童的实际人数,需要真实的年级分布及其相关性。

背部