File size: 2,923 Bytes
6a87728
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0bce3b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6a87728
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""Energy-expenditure helpers used by the inference layer.

Mifflin-St Jeor is the standard modern estimator for resting metabolic
rate. We use it to translate the user's profile into a personalized
daily calorie target, which becomes one input to the RAG coach.
"""

from __future__ import annotations

ACTIVITY_FACTORS: dict[str, float] = {
    "sedentary": 1.2,
    "light": 1.375,
    "moderate": 1.55,
    "active": 1.725,
    "very active": 1.9,
}

GOAL_DELTAS_KCAL: dict[str, int] = {
    "lose": -500,
    "maintain": 0,
    "gain": 300,
}

# Empirical training-data bounds for the UCI Obesity Levels dataset.
# Profiles outside these ranges are extrapolations — the classifier should
# not be trusted at face value there.
TRAINING_BMI_RANGE: tuple[float, float] = (13.0, 50.8)
TRAINING_WEIGHT_RANGE_KG: tuple[float, float] = (39.0, 173.0)
TRAINING_HEIGHT_RANGE_CM: tuple[float, float] = (145.0, 198.0)
TRAINING_AGE_RANGE: tuple[int, int] = (14, 61)

# BMI cutoffs used to derive the rule-based "BMI band" class. The upper
# bounds are anchored to WHO bands, with the Overweight split (I/II) and
# the Obesity split (I/II/III) following the UCI dataset's own labeling
# convention so the band matches the classifier's class names.
_BMI_BAND_CUTOFFS: list[tuple[float, str]] = [
    (18.5, "Insufficient_Weight"),
    (25.0, "Normal_Weight"),
    (27.5, "Overweight_Level_I"),
    (30.0, "Overweight_Level_II"),
    (35.0, "Obesity_Type_I"),
    (40.0, "Obesity_Type_II"),
    (float("inf"), "Obesity_Type_III"),
]


def bmi_to_band(bmi: float) -> str:
    """Map a BMI value to the canonical 7-class band label.

    Rule-based, transparent, and immune to training-data skew —
    used as a sanity check against the classifier's prediction.
    """
    for upper, label in _BMI_BAND_CUTOFFS:
        if bmi < upper:
            return label
    return "Obesity_Type_III"


def bmr_mifflin_st_jeor(age: int, weight_kg: float, height_cm: float, sex: str = "male") -> float:
    s = 5 if sex.lower().startswith("m") else -161
    return 10.0 * weight_kg + 6.25 * height_cm - 5.0 * age + s


def tdee(bmr: float, activity_level: str) -> float:
    return bmr * ACTIVITY_FACTORS[activity_level]


def daily_target_kcal(age: int, weight_kg: float, height_cm: float,
                      sex: str, activity_level: str, goal: str) -> float:
    base = tdee(bmr_mifflin_st_jeor(age, weight_kg, height_cm, sex), activity_level)
    return base + GOAL_DELTAS_KCAL[goal]


def macro_targets(target_kcal: float, goal: str) -> dict[str, float]:
    splits = {
        "lose":     (0.35, 0.30, 0.35),
        "maintain": (0.25, 0.30, 0.45),
        "gain":     (0.25, 0.25, 0.50),
    }
    p_pct, f_pct, c_pct = splits[goal]
    return {
        "protein_g": (target_kcal * p_pct) / 4.0,
        "fat_g": (target_kcal * f_pct) / 9.0,
        "carbohydrate_g": (target_kcal * c_pct) / 4.0,
        "sodium_mg": 2000.0,
    }