Rounding errors during transition

In Bavaria, an overall average grade of \(2{,}33\) certifies suitability for Gymnasium \(2{,}34\) does not. At this sharp cutoff, the order of two rounding operations can determine which side a child falls on. Günther Felbinger discusses the potential consequences of this rounding effect in a blog post . Out of pure interest, I calculated the effects myself across all grade combinations permitted in the model.


My central question is: How often does early rounding to whole subject grades lead to a different type of school calculation than calculations using unrounded subject averages? Furthermore, it would be interesting to see what changes if the averages of the individual subjects (as suggested there) are initially rounded to only one decimal place.

According to Section 6 of the Bavarian Primary School Regulations, the average grade is calculated from the grades in German, Mathematics, and Local and General Studies. The implementing regulations specify the limits as up to and including \(2{,}33\) and up to and including \(2{,}66\) . In the following calculation model, each overall average is truncated to two decimal places and not rounded again. Thus, for example...

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

This is crucial for the classification: \(2{,}3366\ldots\) does not become \(2{,}34\) , but \(2{,}33\) and is therefore still sufficient for grammar school.

The initial entry illustrates early rounding with the assumed subject-specific mean values \(1{,}6\) , \(2{,}6\) , and \(2{,}6\) . From these, the whole grades \(2\) , \(3\) , and \(3\) are first derived. Their mean is

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

which means the student doesn't qualify for direct grammar school admission. Without rounding to whole grades, however, the result in the hypothetical example would be...

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

In the second example, the rounding works in the opposite direction: \(2{,}4\) , \(3{,}4\) and \(2{,}4\) become the whole note values \(2\) , \(3\) and \(2\) , officially \(2{,}33\) . The unrounded values, however, result in \(2{,}73\) . The examples clearly demonstrate the effect. However, they do not yet reveal how frequently it occurs or what the consequences of a different rounding method would be.

Therefore, for my calculation I use a clear result space. Each subject mean can be one of the \(501\) values within it.

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

Assume. I weight all ordered combinations of German, mathematics, and local and general studies equally. Thus, there are

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

Cases. The model is therefore unambiguous, but artificial: It assumes equally distributed subject averages, the independence of the three subjects, and steps of exactly one hundredth. It does not reflect the actual distribution of Bavarian primary school children.

For three subject means \(d,m,h\) we write

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

for the overall average truncated to two decimal places. Now we compare three calculation methods.:

\[
\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\) corresponds to the calculation using whole grades, \(A_1\) to the proposal with one decimal place, and \(A_\mathrm{ref}\) to the unrounded reference within the model. The calculation can be implemented directly in Rust , for example.:

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
        );
    }
}

The small tolerance in calculate_average prevents a value such as \(2{,}67\) because of its binary float representation, it was mistakenly considered \(2{,}669999\ldots\) is cut off. normalize_grade Furthermore, it reduces each loop value back to a hundredth-order grid. At least two different possible mean values exist. \(1/300\) apart; the tolerance of \(10^{-9}\) is therefore small enough not to change any real borderline case.

The program is compiled and started with

rustc -O calc.rs && ./calc

The program iterates through all \(125\,751\,501\) combinations and returns:

ComparisonPortion
Whole grades versus unrounded subject averages\(10{,}4411\,\%\)
Whole grades compared to the proposal with one decimal place\(10{,}1521\,\%\)
Proposal with one decimal place compared to unrounded expert averages\(0{,}6615\,\%\)

The central value for me is \(10{,}4411\,\%\): In more than one in ten combinations, early rounding to whole grades leads to a different calculated school type than the unrounded reference. This proportion describes exclusively the rounding effect within the chosen model.

Switching to expert averages with one decimal place would lead to a different classification in \(10{,}1521\,\%\) of all model cases compared to the previous method. However, the third comparison is crucial: compared to the unrounded reference, the method with one decimal place leaves only \(0{,}6615\,\%\) deviating cases. The deviation from the reference thus decreases by

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

This, for me, is precisely the questionable consequence of the current calculation method: the assumed subject-specific achievements remain unchanged; only the point in time at which rounding occurs can alter the calculated school type. While calculating with one decimal place doesn't completely eliminate this effect, it significantly reduces it within the model.

These figures, however, do not tell us how many children are actually affected. Report card grades are not necessarily generated as mechanically rounded averages, academic performance is neither uniformly distributed nor independent, and the actual choice of school type does not depend solely on these three figures. My calculation answers a more specific question: How often does rounding change the calculated grade when all hundredth-value combinations are equally likely?

Finally, a comparison with the original post: The overall rate mentioned there, approximately \(10\,\%\) is close to my \(10{,}4411\,\%\) . However, my calculations do not confirm the four individual cases, each reported as \(1/6 \approx 17\,\%\) For "missed secondary school with \(3{,}00\) ", "missed grammar school with \(2{,}33\) ", "achieved grammar school with \(2{,}66\) ", and "achieved secondary school with \(2{,}66\) \(1{,}4615\,\%\) \(59{,}6096\,\%\) \(1{,}2023\,\%\) \(62{,}9395\,\%\) .

The four values are conditional probabilities with differently sized reference groups. Without a differently defined sample space \(1/6\) cannot be derived from them. My calculation thus confirms the rounding effect, but not these individual probabilities. In any case, statements about the actual number of affected children would require real grade distributions and their dependencies.

Back