kiwi/scripts/pipeline/estimate_recipe_nutrition.py
pyr0ball c18bfec8f5
Some checks are pending
CI / Backend (Python) (push) Waiting to run
CI / Frontend (Vue) (push) Waiting to run
Mirror / mirror (push) Waiting to run
fix(lint): add ruff config, fix all lint errors for GitHub CI
- pyproject.toml: add [tool.ruff] config suppressing E402/W293 globally,
  F841/E741/E702 in tests, E741 in scripts
- inventory.py: split semicolon import (E702)
- recipes.py: fix logger -> log (F821 undefined name)
- shopping.py: rename l -> lnk in list comprehension (E741)
- format_conversion.py: noqa F841 on CUDA flag (used as future hook)
- backfill_keywords.py: rename done -> _done (F841)
- ingest_purplecarrot.py: drop == True comparison (E712)
- Auto-fix: I001 import sorting, F401 unused imports across all files
2026-07-06 02:49:02 -07:00

110 lines
3.6 KiB
Python

"""
Estimate macro nutrition for recipes that have no direct data.
For each recipe where sugar_g / carbs_g / fiber_g / calories are NULL,
look up the matched ingredient_profiles and average their per-100g values,
then scale by a rough 150g-per-ingredient portion assumption.
Mark such rows with nutrition_estimated=1 so the UI can display a disclaimer.
Recipes with food.com direct data (nutrition_estimated=0 and values set) are untouched.
Usage:
conda run -n job-seeker python scripts/pipeline/estimate_recipe_nutrition.py \
--db /path/to/kiwi.db
"""
from __future__ import annotations
import argparse
import json
import sqlite3
from pathlib import Path
# Rough grams per ingredient when no quantity data is available.
_GRAMS_PER_INGREDIENT = 150.0
def estimate(db_path: Path) -> None:
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
# Load ingredient_profiles macro data into memory for fast lookup.
profile_macros: dict[str, dict[str, float]] = {}
for row in conn.execute(
"SELECT name, calories_per_100g, carbs_g_per_100g, fiber_g_per_100g, sugar_g_per_100g "
"FROM ingredient_profiles"
):
name, cal, carbs, fiber, sugar = row
if name:
profile_macros[name] = {
"calories": float(cal or 0),
"carbs": float(carbs or 0),
"fiber": float(fiber or 0),
"sugar": float(sugar or 0),
}
# Select recipes with no direct nutrition data.
rows = conn.execute(
"SELECT id, ingredient_names FROM recipes "
"WHERE sugar_g IS NULL AND carbs_g IS NULL AND fiber_g IS NULL"
).fetchall()
updated = 0
batch: list[tuple] = []
for recipe_id, ingredient_names_json in rows:
try:
names: list[str] = json.loads(ingredient_names_json or "[]")
except Exception:
names = []
matched = [profile_macros[n] for n in names if n in profile_macros]
if not matched:
continue
# Average per-100g macros across matched ingredients,
# then multiply by assumed portion weight per ingredient.
n = len(matched)
portion_factor = _GRAMS_PER_INGREDIENT / 100.0
total_cal = sum(m["calories"] for m in matched) / n * portion_factor * n
total_carbs = sum(m["carbs"] for m in matched) / n * portion_factor * n
total_fiber = sum(m["fiber"] for m in matched) / n * portion_factor * n
total_sugar = sum(m["sugar"] for m in matched) / n * portion_factor * n
batch.append((
round(total_cal, 1) or None,
round(total_carbs, 2) or None,
round(total_fiber, 2) or None,
round(total_sugar, 2) or None,
recipe_id,
))
if len(batch) >= 5000:
conn.executemany(
"UPDATE recipes SET calories=?, carbs_g=?, fiber_g=?, sugar_g=?, "
"nutrition_estimated=1 WHERE id=?",
batch,
)
conn.commit()
updated += len(batch)
print(f" {updated} recipes estimated...")
batch = []
if batch:
conn.executemany(
"UPDATE recipes SET calories=?, carbs_g=?, fiber_g=?, sugar_g=?, "
"nutrition_estimated=1 WHERE id=?",
batch,
)
conn.commit()
updated += len(batch)
conn.close()
print(f"Total: {updated} recipes received estimated nutrition")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--db", required=True, type=Path)
args = parser.parse_args()
estimate(args.db)