Source code for usda_fdc.analysis.analysis

"""
Food nutrient analysis functionality.
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any, Union, Tuple

from ..models import Food, Nutrient
from .dri import DriType, Gender, get_dri

[docs]@dataclass class NutrientValue: """ Represents a nutrient value with additional analysis information. """ nutrient: Nutrient amount: float unit: str dri: Optional[float] = None dri_percent: Optional[float] = None dri_type: Optional[DriType] = None
[docs]@dataclass class NutrientAnalysis: """ Analysis of a food's nutrient content. """ food: Food serving_size: float # in grams nutrients: Dict[str, NutrientValue] = field(default_factory=dict) calories_per_serving: float = 0.0 protein_per_serving: float = 0.0 carbs_per_serving: float = 0.0 fat_per_serving: float = 0.0 macronutrient_distribution: Dict[str, float] = field(default_factory=dict)
[docs] def get_nutrient(self, nutrient_id: str) -> Optional[NutrientValue]: """ Get a nutrient value by ID. Args: nutrient_id: The nutrient ID or name. Returns: The nutrient value, or None if not found. """ return self.nutrients.get(nutrient_id.lower())
def _get_nutrient_id(nutrient: Nutrient) -> str: """ Get a standardized nutrient ID from a nutrient. Args: nutrient: The nutrient object. Returns: A standardized nutrient ID. """ # Map common nutrient names to standardized IDs name_map = { "protein": "protein", "total lipid (fat)": "fat", "fatty acids, total saturated": "saturated_fat", "carbohydrate, by difference": "carbs", "fiber, total dietary": "fiber", "sugars, total including nlea": "sugar", "calcium, ca": "calcium", "iron, fe": "iron", "sodium, na": "sodium", "vitamin c, total ascorbic acid": "vitamin_c", "vitamin a, iu": "vitamin_a", "cholesterol": "cholesterol", "potassium, k": "potassium", "energy": "calories" } # Try to match by name name_lower = nutrient.name.lower() for key, value in name_map.items(): if key in name_lower: return value # If no match, use a simplified version of the name return name_lower.replace(",", "").replace(" ", "_")
[docs]def analyze_food( food: Food, serving_size: float = 100.0, dri_type: DriType = DriType.RDA, gender: Gender = Gender.MALE, age: int = 30 ) -> NutrientAnalysis: """ Analyze the nutrient content of a food. Args: food: The food to analyze. serving_size: The serving size in grams. dri_type: The type of DRI to use for comparison. gender: The gender to use for DRI values. age: The age to use for DRI values. Returns: A NutrientAnalysis object. """ # Create analysis object analysis = NutrientAnalysis( food=food, serving_size=serving_size ) # Process nutrients for nutrient in food.nutrients: # Get standardized nutrient ID nutrient_id = _get_nutrient_id(nutrient) # Calculate amount for the serving size amount = nutrient.amount * (serving_size / 100.0) # Get DRI value if available dri_value = get_dri(nutrient_id, dri_type, gender, age) # Calculate DRI percentage if DRI is available dri_percent = None if dri_value is not None and dri_value > 0: dri_percent = (amount / dri_value) * 100.0 # Create nutrient value nutrient_value = NutrientValue( nutrient=nutrient, amount=amount, unit=nutrient.unit_name, dri=dri_value, dri_percent=dri_percent, dri_type=dri_type ) # Add to nutrients dictionary analysis.nutrients[nutrient_id] = nutrient_value # Track macronutrients if nutrient_id == "protein": analysis.protein_per_serving = amount elif nutrient_id == "fat": analysis.fat_per_serving = amount elif nutrient_id == "carbs": analysis.carbs_per_serving = amount elif nutrient_id == "calories": analysis.calories_per_serving = amount # Calculate macronutrient distribution total_calories = 0.0 # Protein: 4 calories per gram protein_calories = analysis.protein_per_serving * 4.0 total_calories += protein_calories # Carbs: 4 calories per gram carb_calories = analysis.carbs_per_serving * 4.0 total_calories += carb_calories # Fat: 9 calories per gram fat_calories = analysis.fat_per_serving * 9.0 total_calories += fat_calories # If we don't have macronutrient data, use the calories value if total_calories == 0.0 and analysis.calories_per_serving > 0: total_calories = analysis.calories_per_serving # Calculate percentages if total_calories > 0: analysis.macronutrient_distribution = { "protein": (protein_calories / total_calories) * 100.0 if protein_calories > 0 else 0.0, "carbs": (carb_calories / total_calories) * 100.0 if carb_calories > 0 else 0.0, "fat": (fat_calories / total_calories) * 100.0 if fat_calories > 0 else 0.0 } return analysis
[docs]def analyze_foods( foods: List[Food], serving_sizes: Optional[List[float]] = None, dri_type: DriType = DriType.RDA, gender: Gender = Gender.MALE, age: int = 30 ) -> List[NutrientAnalysis]: """ Analyze the nutrient content of multiple foods. Args: foods: The foods to analyze. serving_sizes: The serving sizes in grams (one per food). dri_type: The type of DRI to use for comparison. gender: The gender to use for DRI values. age: The age to use for DRI values. Returns: A list of NutrientAnalysis objects. """ # Use default serving size if not provided if serving_sizes is None: serving_sizes = [100.0] * len(foods) # Ensure serving_sizes matches foods length if len(serving_sizes) != len(foods): raise ValueError("Number of serving sizes must match number of foods") # Analyze each food return [ analyze_food(food, serving_size, dri_type, gender, age) for food, serving_size in zip(foods, serving_sizes) ]
[docs]def compare_foods( foods: List[Food], nutrient_ids: Optional[List[str]] = None, serving_sizes: Optional[List[float]] = None, dri_type: DriType = DriType.RDA, gender: Gender = Gender.MALE, age: int = 30 ) -> Dict[str, List[Tuple[str, float, str]]]: """ Compare the nutrient content of multiple foods. Args: foods: The foods to compare. nutrient_ids: The nutrient IDs to compare. serving_sizes: The serving sizes in grams (one per food). dri_type: The type of DRI to use for comparison. gender: The gender to use for DRI values. age: The age to use for DRI values. Returns: A dictionary mapping nutrient IDs to lists of (food_name, amount, unit) tuples. """ # Analyze foods analyses = analyze_foods(foods, serving_sizes, dri_type, gender, age) # If no nutrient IDs provided, use common ones if nutrient_ids is None: nutrient_ids = ["protein", "fat", "carbs", "fiber", "vitamin_c", "calcium", "iron"] # Initialize result dictionary result: Dict[str, List[Tuple[str, float, str]]] = { nutrient_id: [] for nutrient_id in nutrient_ids } # Compare nutrients for analysis in analyses: for nutrient_id in nutrient_ids: nutrient_value = analysis.get_nutrient(nutrient_id) if nutrient_value: result[nutrient_id].append(( analysis.food.description, nutrient_value.amount, nutrient_value.unit )) return result