fixed dynamic TDEE
This commit is contained in:
@@ -9,12 +9,20 @@ from typing import Any
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.models import BodyMetric, FoodLog, StepLog, WaterLog
|
||||
from app.db.models import BodyMetric, FitnessProfile, FoodLog, StepLog, WaterLog, WorkoutLog
|
||||
from app.fitness.activity_budget import estimate_workout_active_kcal
|
||||
from app.fitness.calculators import (
|
||||
EXPECTED_LOOKBACK_DAYS,
|
||||
compute_daily_targets,
|
||||
compute_expected_targets,
|
||||
)
|
||||
|
||||
METRIC_DEFS: dict[str, dict[str, str]] = {
|
||||
"weight_kg": {"label": "Вес", "unit": "кг"},
|
||||
"body_fat_pct": {"label": "Жир", "unit": "%"},
|
||||
"calories": {"label": "Калории", "unit": "ккал/день"},
|
||||
"tdee": {"label": "TDEE факт", "unit": "ккал/день"},
|
||||
"tdee_expected": {"label": "TDEE план", "unit": "ккал/день"},
|
||||
"protein_g": {"label": "Белок", "unit": "г/день"},
|
||||
"water_l": {"label": "Вода", "unit": "л/день"},
|
||||
"steps": {"label": "Шаги", "unit": "шаг/день"},
|
||||
@@ -55,6 +63,106 @@ def _last(values: list[tuple[date, float]]) -> float | None:
|
||||
return values[-1][1]
|
||||
|
||||
|
||||
def _profile_for_charts(row: FitnessProfile | None) -> dict[str, float | int | str | None] | None:
|
||||
if row is None:
|
||||
return None
|
||||
return {
|
||||
"sex": row.sex,
|
||||
"age": row.age,
|
||||
"height_cm": row.height_cm,
|
||||
"weight_kg": row.weight_kg,
|
||||
"goal": row.goal,
|
||||
"neat_base_kcal": row.neat_base_kcal,
|
||||
"activity_level": row.activity_level,
|
||||
"weekly_workouts": row.weekly_workouts,
|
||||
"baseline_steps": row.baseline_steps,
|
||||
"baseline_workout_kcal": row.baseline_workout_kcal,
|
||||
}
|
||||
|
||||
|
||||
def _load_activity_maps(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
range_start: datetime,
|
||||
range_end: datetime,
|
||||
weight_kg: float,
|
||||
) -> tuple[dict[date, int], dict[date, float]]:
|
||||
steps_by_day: dict[date, int] = defaultdict(int)
|
||||
workout_kcal_by_day: dict[date, float] = defaultdict(float)
|
||||
|
||||
steps_rows = db.scalars(
|
||||
select(StepLog).where(
|
||||
StepLog.user_id == user_id,
|
||||
StepLog.logged_at >= range_start,
|
||||
StepLog.logged_at <= range_end,
|
||||
)
|
||||
).all()
|
||||
for row in steps_rows:
|
||||
steps_by_day[row.logged_at.date()] += row.steps
|
||||
|
||||
workouts_rows = db.scalars(
|
||||
select(WorkoutLog).where(
|
||||
WorkoutLog.user_id == user_id,
|
||||
WorkoutLog.logged_at >= range_start,
|
||||
WorkoutLog.logged_at <= range_end,
|
||||
)
|
||||
).all()
|
||||
for row in workouts_rows:
|
||||
d = row.logged_at.date()
|
||||
workout_kcal_by_day[d] += estimate_workout_active_kcal(
|
||||
{
|
||||
"title": row.title,
|
||||
"duration_min": row.duration_min,
|
||||
"active_calories": row.active_calories,
|
||||
},
|
||||
weight_kg=weight_kg,
|
||||
)
|
||||
return steps_by_day, workout_kcal_by_day
|
||||
|
||||
|
||||
def _activity_history_before(
|
||||
day: date,
|
||||
steps_by_day: dict[date, int],
|
||||
workout_kcal_by_day: dict[date, float],
|
||||
*,
|
||||
days: int = EXPECTED_LOOKBACK_DAYS,
|
||||
) -> list[dict[str, float | int]]:
|
||||
history: list[dict[str, float | int]] = []
|
||||
start = day - timedelta(days=days)
|
||||
cursor = start
|
||||
while cursor < day:
|
||||
history.append(
|
||||
{
|
||||
"steps": steps_by_day.get(cursor, 0),
|
||||
"workout_kcal": workout_kcal_by_day.get(cursor, 0.0),
|
||||
}
|
||||
)
|
||||
cursor += timedelta(days=1)
|
||||
return history
|
||||
|
||||
|
||||
def _tdee_actual_for_day(
|
||||
profile: dict[str, float | int | str | None],
|
||||
steps_by_day: dict[date, int],
|
||||
workout_kcal_by_day: dict[date, float],
|
||||
day: date,
|
||||
) -> float:
|
||||
steps = steps_by_day.get(day, 0)
|
||||
workout_kcal = workout_kcal_by_day.get(day, 0.0)
|
||||
workouts = [{"active_calories": workout_kcal}] if workout_kcal > 0 else []
|
||||
return float(compute_daily_targets(profile, steps_total=steps, workouts=workouts)["tdee"])
|
||||
|
||||
|
||||
def _tdee_expected_for_day(
|
||||
profile: dict[str, float | int | str | None],
|
||||
steps_by_day: dict[date, int],
|
||||
workout_kcal_by_day: dict[date, float],
|
||||
day: date,
|
||||
) -> float:
|
||||
history = _activity_history_before(day, steps_by_day, workout_kcal_by_day)
|
||||
return float(compute_expected_targets(profile, history=history)["tdee"])
|
||||
|
||||
|
||||
def build_fitness_charts(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
@@ -71,6 +179,25 @@ def build_fitness_charts(
|
||||
range_start = datetime.combine(first_week_start, datetime.min.time(), tzinfo=timezone.utc)
|
||||
range_end = datetime.combine(end, datetime.max.time(), tzinfo=timezone.utc)
|
||||
|
||||
profile_row = db.scalar(
|
||||
select(FitnessProfile).where(FitnessProfile.user_id == user_id).limit(1)
|
||||
)
|
||||
profile = _profile_for_charts(profile_row)
|
||||
weight_kg = float(profile["weight_kg"]) if profile else 70.0
|
||||
|
||||
activity_start = datetime.combine(
|
||||
first_week_start - timedelta(days=EXPECTED_LOOKBACK_DAYS),
|
||||
datetime.min.time(),
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
steps_by_day, workout_kcal_by_day = _load_activity_maps(
|
||||
db,
|
||||
user_id,
|
||||
activity_start,
|
||||
range_end,
|
||||
weight_kg,
|
||||
)
|
||||
|
||||
daily: dict[date, dict[str, float]] = defaultdict(lambda: {
|
||||
"calories": 0.0,
|
||||
"protein_g": 0.0,
|
||||
@@ -180,6 +307,14 @@ def build_fitness_charts(
|
||||
week_daily_values.append(totals["water_ml"] / 1000.0)
|
||||
elif metric == "steps" and totals and "steps" in flags:
|
||||
week_daily_values.append(totals["steps"])
|
||||
elif metric == "tdee" and profile is not None and day_cursor <= end:
|
||||
week_daily_values.append(
|
||||
_tdee_actual_for_day(profile, steps_by_day, workout_kcal_by_day, day_cursor)
|
||||
)
|
||||
elif metric == "tdee_expected" and profile is not None and day_cursor <= end:
|
||||
week_daily_values.append(
|
||||
_tdee_expected_for_day(profile, steps_by_day, workout_kcal_by_day, day_cursor)
|
||||
)
|
||||
day_cursor += timedelta(days=1)
|
||||
|
||||
value: float | None
|
||||
@@ -253,6 +388,9 @@ def build_fitness_charts(
|
||||
end,
|
||||
trend=trend,
|
||||
lookback_days=min(30, max(days_with_data, 7)),
|
||||
profile=profile,
|
||||
steps_by_day=steps_by_day,
|
||||
workout_kcal_by_day=workout_kcal_by_day,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -276,6 +414,9 @@ def _build_daily_series(
|
||||
*,
|
||||
trend: bool,
|
||||
lookback_days: int,
|
||||
profile: dict[str, float | int | str | None] | None = None,
|
||||
steps_by_day: dict[date, int] | None = None,
|
||||
workout_kcal_by_day: dict[date, float] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
start = end - timedelta(days=lookback_days - 1)
|
||||
day_points: list[date] = []
|
||||
@@ -314,6 +455,17 @@ def _build_daily_series(
|
||||
elif key == "steps" and totals and "steps" in flags:
|
||||
value = totals["steps"]
|
||||
has_data = True
|
||||
elif key == "tdee" and profile is not None and steps_by_day is not None and workout_kcal_by_day is not None:
|
||||
value = _tdee_actual_for_day(profile, steps_by_day, workout_kcal_by_day, d)
|
||||
has_data = True
|
||||
elif (
|
||||
key == "tdee_expected"
|
||||
and profile is not None
|
||||
and steps_by_day is not None
|
||||
and workout_kcal_by_day is not None
|
||||
):
|
||||
value = _tdee_expected_for_day(profile, steps_by_day, workout_kcal_by_day, d)
|
||||
has_data = True
|
||||
|
||||
points.append(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user