Spaces:
Sleeping
Sleeping
| """Tests for the numeric block.""" | |
| from __future__ import annotations | |
| import pandas as pd | |
| import pytest | |
| from src.numeric.obesity import ( | |
| OBESITY_LEVELS, | |
| apply_favc_override, | |
| derive_high_caloric_meal, | |
| ) | |
| from src.numeric.profile import ( | |
| bmi_to_band, | |
| bmr_mifflin_st_jeor, | |
| daily_target_kcal, | |
| macro_targets, | |
| tdee, | |
| ) | |
| from src.numeric.model import predict | |
| def _profile(**overrides): | |
| """Build a complete profile dict with sensible defaults for tests.""" | |
| base = { | |
| "age": 30, | |
| "weight_kg": 75, | |
| "height_cm": 175, | |
| "Gender": "Male", | |
| "family_history_with_overweight": "no", | |
| "FAVC": "no", | |
| "FCVC": 2.0, | |
| "NCP": 3.0, | |
| "CAEC": "Sometimes", | |
| "SMOKE": "no", | |
| "CH2O": 2.0, | |
| "SCC": "no", | |
| "FAF": 1.0, | |
| "TUE": 1.0, | |
| "CALC": "no", | |
| "MTRANS": "Public_Transportation", | |
| "activity_level": "moderate", | |
| "goal": "maintain", | |
| } | |
| base.update(overrides) | |
| return base | |
| def test_bmr_known_values(): | |
| # Mifflin-St Jeor for a 30-year-old, 75kg, 178cm male: | |
| # 10*75 + 6.25*178 - 5*30 + 5 = 1717.5 kcal | |
| assert bmr_mifflin_st_jeor(30, 75, 178, "male") == pytest.approx(1717.5, abs=0.5) | |
| def test_tdee_scales_with_activity(): | |
| bmr = 1700 | |
| assert tdee(bmr, "sedentary") < tdee(bmr, "moderate") < tdee(bmr, "very active") | |
| def test_target_respects_goal(): | |
| args = (30, 75, 178, "male", "moderate") | |
| lose = daily_target_kcal(*args, "lose") | |
| maintain = daily_target_kcal(*args, "maintain") | |
| gain = daily_target_kcal(*args, "gain") | |
| assert lose < maintain < gain | |
| assert maintain - lose == 500 | |
| assert gain - maintain == 300 | |
| def test_macro_targets_sum_to_target_kcal_within_rounding(): | |
| target = 2500.0 | |
| m = macro_targets(target, "maintain") | |
| total = m["protein_g"] * 4 + m["fat_g"] * 9 + m["carbohydrate_g"] * 4 | |
| assert total == pytest.approx(target, rel=0.01) | |
| def test_high_caloric_meal_threshold(): | |
| high = {"calories_kcal": 850, "fat_g": 40} | |
| low = {"calories_kcal": 400, "fat_g": 15} | |
| borderline = {"calories_kcal": 750, "fat_g": 20} | |
| assert derive_high_caloric_meal(high) is True | |
| assert derive_high_caloric_meal(low) is False | |
| assert derive_high_caloric_meal(borderline) is False | |
| def test_favc_override_flips_columns(): | |
| row = pd.DataFrame([{"FAVC_yes": 0, "FAVC_no": 1, "Age": 30}]) | |
| out = apply_favc_override(row, {"calories_kcal": 900, "fat_g": 35}) | |
| assert int(out["FAVC_yes"].iloc[0]) == 1 | |
| assert int(out["FAVC_no"].iloc[0]) == 0 | |
| def test_favc_override_noop_for_low_calorie_meal(): | |
| row = pd.DataFrame([{"FAVC_yes": 0, "FAVC_no": 1}]) | |
| out = apply_favc_override(row, {"calories_kcal": 400, "fat_g": 10}) | |
| assert int(out["FAVC_yes"].iloc[0]) == 0 | |
| def test_predict_returns_expected_shape_without_models(): | |
| """When models are absent, predict() must return a coherent fallback.""" | |
| out = predict(_profile(weight_kg=75, height_cm=178), nutrition=None) | |
| assert out["obesity_class"] in OBESITY_LEVELS | |
| assert out["daily_target_kcal"] > 0 | |
| assert out["predicted_bmi"] > 0 | |
| assert "models" in out | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BMI-band rule (transparent, training-data-independent) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_bmi_to_band_covers_full_severity_range(bmi, expected): | |
| assert bmi_to_band(bmi) == expected | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Predict shape: every call must populate the anomaly fields | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_predict_always_returns_bmi_band_and_anomaly_fields(): | |
| out = predict(_profile(), nutrition=None) | |
| assert out["bmi_band"] in OBESITY_LEVELS | |
| assert "anomaly_flags" in out | |
| assert isinstance(out["anomaly_flags"], list) | |
| assert "ood_blended" in out | |
| assert "bmi_raw" in out | |
| def test_predict_probabilities_sum_to_one(): | |
| out = predict(_profile(), nutrition=None) | |
| total = sum(out["obesity_probabilities"].values()) | |
| assert total == pytest.approx(1.0, abs=1e-3) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Realistic profiles β model should land on (or near) the BMI band | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_predict_matches_bmi_band_on_realistic_profiles(label, profile_kwargs, expected_band): | |
| out = predict(_profile(**profile_kwargs), nutrition=None) | |
| assert out["bmi_band"] == expected_band, f"{label}: bmi_band wrong" | |
| # Final class within 1 step of the BMI band β the classifier is allowed | |
| # to land on an adjacent severity bucket for borderline BMIs. | |
| gap = abs(OBESITY_LEVELS.index(out["obesity_class"]) - OBESITY_LEVELS.index(expected_band)) | |
| assert gap <= 1, ( | |
| f"{label}: predicted {out['obesity_class']} is {gap} classes " | |
| f"from expected band {expected_band}" | |
| ) | |
| # Most probability mass should sit on the band or an adjacent bucket. | |
| top_two = sorted(out["obesity_probabilities"].items(), key=lambda kv: -kv[1])[:2] | |
| assert sum(p for _, p in top_two) > 0.7, f"{label}: top-2 mass under 70%" | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Anomaly handling β out-of-distribution / rare-conditional inputs | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_anomaly_extreme_overweight_male_routes_to_type_iii(): | |
| """Male BMI 44.2 β Type_III training data is 99.7% female, so the | |
| raw classifier predicts Type_II. The BMI-band override must correct | |
| this and the final class must be Type_III.""" | |
| out = predict(_profile(weight_kg=140, height_cm=178, Gender="Male"), nutrition=None) | |
| assert out["bmi_band"] == "Obesity_Type_III" | |
| assert out["obesity_class"] == "Obesity_Type_III" | |
| assert out["ood_blended"] is True | |
| # A flag mentioning the female-bias must be visible to the user. | |
| assert any("female" in f.lower() for f in out["anomaly_flags"]) | |
| assert out["obesity_probabilities"]["Obesity_Type_III"] > 0.5 | |
| def test_anomaly_weight_above_training_range_is_flagged_and_blended(): | |
| out = predict(_profile(weight_kg=220, height_cm=178), nutrition=None) | |
| assert out["ood_blended"] is True | |
| assert out["obesity_class"] == "Obesity_Type_III" | |
| assert any("trained range" in f.lower() for f in out["anomaly_flags"]) | |
| # Despite the model wanting Type_II, the blended final class must | |
| # be at the top of the severity scale. | |
| assert out["obesity_probabilities"]["Obesity_Type_III"] > 0.5 | |
| def test_anomaly_weight_below_training_range_is_flagged(): | |
| out = predict(_profile(weight_kg=35, height_cm=178), nutrition=None) | |
| assert out["ood_blended"] is True | |
| assert out["obesity_class"] == "Insufficient_Weight" | |
| assert any("below the trained range" in f.lower() for f in out["anomaly_flags"]) | |
| def test_anomaly_bmi_below_training_range_is_flagged(): | |
| # Tall and very light β BMI ~10.1, below the trained 13.0 floor. | |
| out = predict(_profile(weight_kg=38, height_cm=195), nutrition=None) | |
| assert out["bmi_band"] == "Insufficient_Weight" | |
| assert any("bmi" in f.lower() and "outside" in f.lower() for f in out["anomaly_flags"]) | |
| def test_anomaly_age_outside_range_is_flagged(): | |
| out = predict(_profile(age=80), nutrition=None) | |
| assert any("age" in f.lower() and "outside" in f.lower() for f in out["anomaly_flags"]) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # In-distribution behavior β model should NOT be overridden when it | |
| # is operating inside the trained envelope. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_in_distribution_borderline_is_not_blended(): | |
| """BMI 26.8 is right on the Overweight I/II boundary. Adjacent-class | |
| disagreement is acceptable here β no OOD blend, no flags about | |
| training range.""" | |
| out = predict(_profile(weight_kg=85, height_cm=178), nutrition=None) | |
| assert out["ood_blended"] is False | |
| # No range-violation flags for in-distribution input. | |
| assert not any("range" in f.lower() for f in out["anomaly_flags"]) | |
| def test_in_distribution_female_type_iii_is_not_blended(): | |
| """Female Type_III is well-represented (323 examples) β the model | |
| should handle this without needing the BMI-band override.""" | |
| out = predict(_profile(weight_kg=110, height_cm=165, Gender="Female"), nutrition=None) | |
| assert out["bmi_band"] == "Obesity_Type_III" | |
| assert out["obesity_class"] == "Obesity_Type_III" | |
| assert out["ood_blended"] is False | |