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 " "Trefethen's new BMI = 1.3 x kg / m^2.5: corrects classic BMI, which "
'over-reads tall people and under-reads short ones.'; '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 %. /// The lean-mass formula the calorie table switches to with a body fat %.
const String katchMcArdle = 'Katch-McArdle'; const String katchMcArdle = 'Katch-McArdle';
@ -150,8 +145,9 @@ double? _perInch(
/// Every ideal-body-weight formula we can evaluate, in kg. /// Every ideal-body-weight formula we can evaluate, in kg.
/// ///
/// Only Creff uses age. The BMI healthy range is [healthyRange], shown with /// Only data-backed ones (Devine, its MetLife refits, Peterson); Broca, its
/// BMI; [idealSummary] averages these. /// derivatives and Hamwi were dropped. The BMI healthy range is
/// [healthyRange], shown with BMI; [idealSummary] averages these.
List<Formula> idealWeights(Biometrics bio) { List<Formula> idealWeights(Biometrics bio) {
final h = bio.heightCm; final h = bio.heightCm;
final inches = h / _cmPerInch - _fiveFeetIn; final inches = h / _cmPerInch - _fiveFeetIn;
@ -168,14 +164,6 @@ List<Formula> idealWeights(Biometrics bio) {
'Miller', 'Miller',
_perInch(inches, male ? 56.2 : 53.1, male ? 1.41 : 1.36, defined: ok), _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( Formula(
'Peterson (BMI 22)', 'Peterson (BMI 22)',
2.2 * _targetBmi + 3.5 * _targetBmi * (metres - 1.5), 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', '${oneDecimal(summary.$2)}-${oneDecimal(summary.$3)} kg',
), ),
], ],
const BodyNote(brocaNote),
if (undefined) if (undefined)
const BodyNote( 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." description: "Companion phone app for diet_guard: log meals on the go."
publish_to: 'none' publish_to: 'none'
version: 1.0.122+1 version: 1.0.123+1
environment: environment:
sdk: ^3.12.2 sdk: ^3.12.2

View File

@ -71,7 +71,7 @@ void main() {
expect(find.text('24.7'), findsOneWidget); expect(find.text('24.7'), findsOneWidget);
expect(find.text('59.9-80.7 kg'), findsOneWidget); expect(find.text('59.9-80.7 kg'), findsOneWidget);
expect(find.text('Average of all'), 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('1780 kcal'), findsOneWidget);
expect(find.text('n/a -- log a body-fat %'), findsNWidgets(2)); 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 // 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)], 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.textContaining('undefined below 152.4 cm'), findsOneWidget);
expect(find.text('adult categories do not apply under 20'), 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__ = [ __all__ = [
"BMI_PRIME_NOTE", "BMI_PRIME_NOTE",
"BROCA_NOTE",
"KATCH_MCARDLE", "KATCH_MCARDLE",
"TREFETHEN_NOTE", "TREFETHEN_NOTE",
"BmiReport", "BmiReport",
@ -38,7 +37,7 @@ __all__ = [
] ]
_CM_PER_INCH = 2.54 _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. # "base + k per inch over five feet"; below five feet they are undefined.
_FIVE_FEET_IN = 60.0 _FIVE_FEET_IN = 60.0
# Adult BMI categories are WHO's and do not apply to anyone still growing. # 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." "over-reads tall people and under-reads short ones."
) )
KATCH_MCARDLE = "Katch-McArdle" 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: def half_up(value: float, digits: int = 0) -> float:
@ -140,9 +135,11 @@ def _per_inch(
def ideal_weights(bio: Biometrics) -> tuple[Formula, ...]: def ideal_weights(bio: Biometrics) -> tuple[Formula, ...]:
"""Every ideal-body-weight formula we can evaluate, in kg. """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 Only data-backed ones: Devine and its MetLife-table refits (Robinson,
and height". The BMI healthy range is :func:`healthy_range`, shown with Miller), and Peterson (NHANES). Broca, its derivatives (modified Broca,
BMI rather than here; :func:`ideal_summary` averages these. 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 h = bio.height_cm
inches = h / _CM_PER_INCH - _FIVE_FEET_IN 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 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( Formula(
"Peterson (BMI 22)", 2.2 * _TARGET_BMI + 3.5 * _TARGET_BMI * (metres - 1.5) "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, METHODS_NOTE,
session_kcal, 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_energy import KCAL_PER_KG, MIN_TARGET_KCAL, activity_label
from diet_guard._body_steps import STEP_BASELINE from diet_guard._body_steps import STEP_BASELINE
from diet_guard._body_store import age_on from diet_guard._body_store import age_on
@ -114,7 +114,6 @@ def ideal_lines(view: BodyView) -> list[str]:
avg, low, high = summary avg, low, high = summary
lines.append(f" {'Average of all':<{_NAME_W}}{_num(avg, 1)} kg") 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.append(f" {'Ideal range':<{_NAME_W}}{_num(low, 1)}-{_num(high, 1)} kg")
lines.extend(_wrap(BROCA_NOTE, " "))
return lines return lines

View File

@ -18,6 +18,7 @@ from diet_guard._body_calc import (
bmr_formulas, bmr_formulas,
half_up, half_up,
ideal_summary, ideal_summary,
ideal_weights,
table_bmr, table_bmr,
) )
from diet_guard._body_energy import ( from diet_guard._body_energy import (
@ -81,6 +82,11 @@ class TestBmr:
def test_ideal_summary_is_none_when_nothing_applies(self) -> None: def test_ideal_summary_is_none_when_nothing_applies(self) -> None:
assert ideal_summary(()) is 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: class TestGoal:
def test_kg_per_day_signs(self) -> None: 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`) ## Calculations (`_body_calc.py`, `_body_energy.py`, `_week_plan.py`)
- **BMI** kg/m², BMI Prime, Trefethen; WHO bands, not applied under 20. - **BMI** kg/m², BMI Prime, Trefethen; WHO bands, not applied under 20.
- **Ideal weight**: Devine, Robinson, Miller, Hamwi (undefined < 152.4 cm), - **Ideal weight**: Devine, Robinson, Miller (undefined < 152.4 cm), Peterson;
Broca, modified Broca, Lorentz, Creff (the one that uses age), 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 then their average and min-max "ideal range". The BMI 18.5-24.9 healthy
range sits with BMI. range sits with BMI.
- **BMR**: Mifflin-St Jeor, Harris-Benedict (1984), and with a body fat - **BMR**: Mifflin-St Jeor, Harris-Benedict (1984), and with a body fat

View File

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