Ideal weight: drop Broca family and Hamwi
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-11-01 18:54:38 +01:00
parent 8957d51432
commit bda76b495d
10 changed files with 35 additions and 154 deletions

View File

@ -41,11 +41,6 @@ const String trefethenNote =
"Trefethen's new BMI = 1.3 x kg / m^2.5: corrects classic BMI, which "
'over-reads tall people and under-reads short ones.';
/// Broca explained, shown under the ideal-weight list.
const String brocaNote =
'Broca = height in cm - 100 (1871); it overshoots, so Broca (modified) '
'takes 10% off it for men and 15% for women.';
/// The lean-mass formula the calorie table switches to with a body fat %.
const String katchMcArdle = 'Katch-McArdle';
@ -150,8 +145,9 @@ double? _perInch(
/// Every ideal-body-weight formula we can evaluate, in kg.
///
/// Only Creff uses age. The BMI healthy range is [healthyRange], shown with
/// BMI; [idealSummary] averages these.
/// Only data-backed ones (Devine, its MetLife refits, Peterson); Broca, its
/// derivatives and Hamwi were dropped. The BMI healthy range is
/// [healthyRange], shown with BMI; [idealSummary] averages these.
List<Formula> idealWeights(Biometrics bio) {
final h = bio.heightCm;
final inches = h / _cmPerInch - _fiveFeetIn;
@ -168,14 +164,6 @@ List<Formula> idealWeights(Biometrics bio) {
'Miller',
_perInch(inches, male ? 56.2 : 53.1, male ? 1.41 : 1.36, defined: ok),
),
Formula(
'Hamwi',
_perInch(inches, male ? 48 : 45.5, male ? 2.7 : 2.2, defined: ok),
),
Formula('Broca', h - 100.0),
Formula('Broca (modified)', (h - 100.0) * (male ? 0.9 : 0.85)),
Formula('Lorentz', h - 100.0 - (h - 150.0) / (male ? 4.0 : 2.0)),
Formula('Creff (age)', (h - 100.0 + bio.ageYears / 10.0) * 0.9),
Formula(
'Peterson (BMI 22)',
2.2 * _targetBmi + 3.5 * _targetBmi * (metres - 1.5),

View File

@ -66,10 +66,9 @@ class IdealWeightCard extends StatelessWidget {
'${oneDecimal(summary.$2)}-${oneDecimal(summary.$3)} kg',
),
],
const BodyNote(brocaNote),
if (undefined)
const BodyNote(
'Devine, Robinson, Miller and Hamwi are undefined below 152.4 cm.',
'Devine, Robinson and Miller are undefined below 152.4 cm.',
),
],
);

View File

@ -2,7 +2,7 @@ name: diet_guard_app
description: "Companion phone app for diet_guard: log meals on the go."
publish_to: 'none'
version: 1.0.122+1
version: 1.0.123+1
environment:
sdk: ^3.12.2

View File

@ -71,7 +71,7 @@ void main() {
expect(find.text('24.7'), findsOneWidget);
expect(find.text('59.9-80.7 kg'), findsOneWidget);
expect(find.text('Average of all'), findsOneWidget);
expect(find.textContaining('Broca = height'), findsOneWidget);
expect(find.textContaining('Broca'), findsNothing);
expect(find.text('1780 kcal'), findsOneWidget);
expect(find.text('n/a -- log a body-fat %'), findsNWidgets(2));
// 5000 steps over baseline x 0.747 m x 0.5 x 80 kg = 149.4 kcal, and

View File

@ -77,7 +77,7 @@ void main() {
children: [BmiCard(bio: _short), IdealWeightCard(bio: _short)],
),
);
expect(find.text('n/a'), findsNWidgets(4));
expect(find.text('n/a'), findsNWidgets(3));
expect(find.textContaining('undefined below 152.4 cm'), findsOneWidget);
expect(find.text('adult categories do not apply under 20'), findsOneWidget);
});

View File

@ -23,7 +23,6 @@ from diet_guard._budget_biometrics import Biometrics, mifflin_st_jeor_bmr
__all__ = [
"BMI_PRIME_NOTE",
"BROCA_NOTE",
"KATCH_MCARDLE",
"TREFETHEN_NOTE",
"BmiReport",
@ -38,7 +37,7 @@ __all__ = [
]
_CM_PER_INCH = 2.54
# The inch-based formulas (Devine, Robinson, Miller, Hamwi) are all
# The inch-based formulas (Devine, Robinson, Miller) are all
# "base + k per inch over five feet"; below five feet they are undefined.
_FIVE_FEET_IN = 60.0
# Adult BMI categories are WHO's and do not apply to anyone still growing.
@ -71,10 +70,6 @@ TREFETHEN_NOTE = (
"over-reads tall people and under-reads short ones."
)
KATCH_MCARDLE = "Katch-McArdle"
BROCA_NOTE = (
"Broca = height in cm - 100 (1871); it overshoots, so Broca (modified) "
"takes 10% off it for men and 15% for women."
)
def half_up(value: float, digits: int = 0) -> float:
@ -140,9 +135,11 @@ def _per_inch(
def ideal_weights(bio: Biometrics) -> tuple[Formula, ...]:
"""Every ideal-body-weight formula we can evaluate, in kg.
Only Creff uses age -- it is the one that answers "ideal weight for my age
and height". The BMI healthy range is :func:`healthy_range`, shown with
BMI rather than here; :func:`ideal_summary` averages these.
Only data-backed ones: Devine and its MetLife-table refits (Robinson,
Miller), and Peterson (NHANES). Broca, its derivatives (modified Broca,
Lorentz, Creff) and Hamwi were dropped as unvalidated rules of thumb.
The BMI healthy range is :func:`healthy_range`, shown with BMI rather than
here; :func:`ideal_summary` averages these.
"""
h = bio.height_cm
inches = h / _CM_PER_INCH - _FIVE_FEET_IN
@ -161,14 +158,6 @@ def ideal_weights(bio: Biometrics) -> tuple[Formula, ...]:
inches, 56.2 if male else 53.1, 1.41 if male else 1.36, defined=ok
),
),
Formula(
"Hamwi",
_per_inch(inches, 48.0 if male else 45.5, 2.7 if male else 2.2, defined=ok),
),
Formula("Broca", h - 100.0),
Formula("Broca (modified)", (h - 100.0) * (0.9 if male else 0.85)),
Formula("Lorentz", h - 100.0 - (h - 150.0) / (4.0 if male else 2.0)),
Formula("Creff (age)", (h - 100.0 + bio.age_years / 10.0) * 0.9),
Formula(
"Peterson (BMI 22)", 2.2 * _TARGET_BMI + 3.5 * _TARGET_BMI * (metres - 1.5)
),

View File

@ -16,7 +16,7 @@ from diet_guard._activity_kcal import (
METHODS_NOTE,
session_kcal,
)
from diet_guard._body_calc import BMI_PRIME_NOTE, BROCA_NOTE, TREFETHEN_NOTE, half_up
from diet_guard._body_calc import BMI_PRIME_NOTE, TREFETHEN_NOTE, half_up
from diet_guard._body_energy import KCAL_PER_KG, MIN_TARGET_KCAL, activity_label
from diet_guard._body_steps import STEP_BASELINE
from diet_guard._body_store import age_on
@ -114,7 +114,6 @@ def ideal_lines(view: BodyView) -> list[str]:
avg, low, high = summary
lines.append(f" {'Average of all':<{_NAME_W}}{_num(avg, 1)} kg")
lines.append(f" {'Ideal range':<{_NAME_W}}{_num(low, 1)}-{_num(high, 1)} kg")
lines.extend(_wrap(BROCA_NOTE, " "))
return lines

View File

@ -18,6 +18,7 @@ from diet_guard._body_calc import (
bmr_formulas,
half_up,
ideal_summary,
ideal_weights,
table_bmr,
)
from diet_guard._body_energy import (
@ -81,6 +82,11 @@ class TestBmr:
def test_ideal_summary_is_none_when_nothing_applies(self) -> None:
assert ideal_summary(()) is None
def test_only_data_backed_ideal_weight_formulas(self) -> None:
# Broca, its derivatives and Hamwi are unvalidated rules of thumb.
names = tuple(f.name for f in ideal_weights(_MAN))
assert names == ("Devine", "Robinson", "Miller", "Peterson (BMI 22)")
class TestGoal:
def test_kg_per_day_signs(self) -> None:

View File

@ -115,8 +115,8 @@ every formula's daily total. Heart-rate input was dropped at the user's call.
## Calculations (`_body_calc.py`, `_body_energy.py`, `_week_plan.py`)
- **BMI** kg/m², BMI Prime, Trefethen; WHO bands, not applied under 20.
- **Ideal weight**: Devine, Robinson, Miller, Hamwi (undefined < 152.4 cm),
Broca, modified Broca, Lorentz, Creff (the one that uses age), Peterson;
- **Ideal weight**: Devine, Robinson, Miller (undefined < 152.4 cm), Peterson;
Broca, modified Broca, Lorentz, Creff and Hamwi were dropped (unvalidated);
then their average and min-max "ideal range". The BMI 18.5-24.9 healthy
range sits with BMI.
- **BMR**: Mifflin-St Jeor, Harris-Benedict (1984), and with a body fat

View File

@ -27,26 +27,6 @@
"Miller",
71.5212598425197
],
[
"Hamwi",
77.33858267716536
],
[
"Broca",
80.0
],
[
"Broca (modified)",
72.0
],
[
"Lorentz",
72.5
],
[
"Creff (age)",
74.7
],
[
"Peterson (BMI 22)",
71.50000000000001
@ -90,9 +70,9 @@
]
],
"ideal_summary": [
74.13307086614174,
72.66476377952756,
71.50000000000001,
80.0
74.99212598425197
],
"healthy_range": [
59.94000000000001,
@ -361,26 +341,6 @@
"Miller",
59.84645669291338
],
[
"Hamwi",
56.413385826771645
],
[
"Broca",
65.0
],
[
"Broca (modified)",
55.25
],
[
"Lorentz",
57.5
],
[
"Creff (age)",
62.550000000000004
],
[
"Peterson (BMI 22)",
59.95
@ -424,9 +384,9 @@
]
],
"ideal_summary": [
58.98359580052493,
55.25,
65.0
58.53474409448818,
56.90944881889762,
59.95
],
"healthy_range": [
50.366249999999994,
@ -695,26 +655,6 @@
"Miller",
null
],
[
"Hamwi",
null
],
[
"Broca",
50.0
],
[
"Broca (modified)",
42.5
],
[
"Lorentz",
50.0
],
[
"Creff (age)",
47.25
],
[
"Peterson (BMI 22)",
48.400000000000006
@ -758,9 +698,9 @@
]
],
"ideal_summary": [
47.63,
42.5,
50.0
48.400000000000006,
48.400000000000006,
48.400000000000006
],
"healthy_range": [
41.625,
@ -1029,26 +969,6 @@
"Miller",
68.74566929133857
],
[
"Hamwi",
72.02362204724409
],
[
"Broca",
75.0
],
[
"Broca (modified)",
67.5
],
[
"Lorentz",
68.75
],
[
"Creff (age)",
69.03
],
[
"Peterson (BMI 22)",
67.65
@ -1092,9 +1012,9 @@
]
],
"ideal_summary": [
69.78548556430445,
67.5,
75.0
68.94143700787401,
67.65,
70.46456692913384
],
"healthy_range": [
56.65625,
@ -1363,26 +1283,6 @@
"Miller",
77.62755905511811
],
[
"Hamwi",
89.03149606299212
],
[
"Broca",
91.0
],
[
"Broca (modified)",
81.9
],
[
"Lorentz",
80.75
],
[
"Creff (age)",
85.59
],
[
"Peterson (BMI 22)",
79.97
@ -1426,9 +1326,9 @@
]
],
"ideal_summary": [
83.52175853018372,
80.85608267716535,
77.62755905511811,
91.0
84.9527559055118
],
"healthy_range": [
67.48985,