- 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
110 lines
3.6 KiB
Python
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)
|