Spaces:
Sleeping
Sleeping
| """ | |
| Interactive Grocery List Recommender (ML-Powered) | |
| Uses best-performing Random Forest model trained on real Pakistan food prices. | |
| """ | |
| import json | |
| from pathlib import Path | |
| import joblib | |
| import numpy as np | |
| import pandas as pd | |
| # Import recommendation layer for ML-based ranking | |
| from recommendation_layer import ( | |
| compute_recommendation_scores, | |
| recommend_products, | |
| get_recommendation_summary | |
| ) | |
| # Paths | |
| BASE_DIR = Path(__file__).resolve().parent | |
| PAKISTAN_PRICES_PATH = BASE_DIR / "Pakistan_Food_Prices_2025.csv" | |
| GROCERY_CATALOG_PATH = BASE_DIR / "Grocery_data (1).csv" | |
| MODELS_DIR = BASE_DIR / "models" | |
| # Global model and encoders (loaded once) | |
| DATASET = None | |
| MODEL = None | |
| def _patch_column_transformer(model): | |
| """Ensure ColumnTransformer has expected attrs across sklearn versions.""" | |
| try: | |
| pre = getattr(model, "named_steps", {}).get("pre") | |
| if pre is not None and not hasattr(pre, "_name_to_fitted_passthrough"): | |
| pre._name_to_fitted_passthrough = {} | |
| except Exception: | |
| pass | |
| def _build_catalog_from_pakistan_prices(df: pd.DataFrame) -> pd.DataFrame: | |
| """Aggregate Pakistan price dataset into an item catalog with median prices.""" | |
| # Clean columns | |
| df = df.copy() | |
| df['Item'] = df['Item'].astype(str).str.strip() | |
| df['Category'] = df['Category'].astype(str).str.strip() | |
| df['Price_per_Kg'] = pd.to_numeric(df['Price_per_Kg'], errors='coerce') | |
| df = df.dropna(subset=['Item', 'Category', 'Price_per_Kg']) | |
| # Aggregate median price per Item + Category | |
| agg = ( | |
| df.groupby(['Item', 'Category'], as_index=False)['Price_per_Kg'] | |
| .median() | |
| .rename(columns={'Price_per_Kg': 'price'}) | |
| ) | |
| return agg | |
| def load_dataset(): | |
| """Load real datasets and prepare an item catalog for recommendations.""" | |
| global DATASET | |
| if not PAKISTAN_PRICES_PATH.exists(): | |
| raise FileNotFoundError(f"Dataset not found: {PAKISTAN_PRICES_PATH}") | |
| pak_df = pd.read_csv(PAKISTAN_PRICES_PATH) | |
| catalog_df = _build_catalog_from_pakistan_prices(pak_df) | |
| # Optional: merge with grocery catalog when available (future enhancement) | |
| DATASET = catalog_df.drop_duplicates(subset=['Item']).reset_index(drop=True) | |
| print(f"[OK] Dataset loaded: {len(DATASET)} items from Pakistan price data") | |
| def load_models(): | |
| """Load unified diet compatibility model trained on real data.""" | |
| global MODEL | |
| path = MODELS_DIR / "diet_unified_model.joblib" | |
| if not path.exists(): | |
| raise FileNotFoundError(f"Model not found: {path}. Please run: python train_model.py") | |
| MODEL = joblib.load(path) | |
| _patch_column_transformer(MODEL) | |
| print("[OK] Unified diet model loaded") | |
| def predict_diet_compatibility(target_diet: str): | |
| """Compute recommendation scores using ML diet probability blended with price.""" | |
| df = DATASET.copy() | |
| # Pipeline expects columns: Item, Category, price, DietType | |
| diet = 'Normal' if target_diet == 'All' else target_diet | |
| df = df.copy() | |
| df['DietType'] = diet | |
| # Lasso returns continuous predictions; clip to [0, 1] for probability interpretation | |
| model_pred = MODEL.predict(df[["Item", "Category", "price", "DietType"]]) | |
| model_proba = np.clip(model_pred, 0, 1) | |
| scored_df = compute_recommendation_scores(df[["Item", "Category", "price"]], diet_type=diet, ml_diet_proba=model_proba) | |
| return scored_df | |
| def generate_grocery_list(budget: float, family_size: int, diet_type: str) -> dict: | |
| """Generate grocery list with ML-based ranking and separation of purchased vs recommended | |
| Returns: | |
| Dictionary containing: | |
| - purchased_items: Items selected within budget (what user should buy) | |
| - recommended_items: High-scoring items not purchased (suggestions for consideration) | |
| - budget_summary: Cost details | |
| Why this separation: | |
| - Purchased items = actionable shopping list | |
| - Recommended items = ML-powered suggestions to help users discover alternatives | |
| - Keeps output focused while providing intelligent recommendations on demand | |
| """ | |
| # Get all products with ML-based recommendation scores | |
| products_scored = predict_diet_compatibility(diet_type) | |
| # Filter by recommendation score threshold (0.2 = reasonable confidence) | |
| products_filtered = products_scored[products_scored['recommendation_score'] > 0.2].copy() | |
| if len(products_filtered) == 0: | |
| # Fallback: use top 20% by recommendation score | |
| threshold = products_scored['recommendation_score'].quantile(0.80) | |
| products_filtered = products_scored[products_scored['recommendation_score'] >= threshold].copy() | |
| # Use recommendation layer to rank and select items within budget | |
| purchased_items, total_cost = recommend_products(products_filtered, budget, family_size) | |
| # Get purchased product names for filtering | |
| purchased_names = {item['product'] for item in purchased_items} | |
| # Determine the product name column in dataframe | |
| name_col = 'Item' if 'Item' in products_filtered.columns else 'product' | |
| # Get recommended items (high-scoring but NOT purchased) | |
| # These are suggestions the user may consider | |
| recommended_items = products_filtered[ | |
| ~products_filtered[name_col].isin(purchased_names) | |
| ].copy() | |
| # Sort recommended items by recommendation score (best first) | |
| recommended_items = recommended_items.sort_values( | |
| by='recommendation_score', | |
| ascending=False | |
| ).head(15) # Limit to top 15 recommendations | |
| # Format recommended items for display | |
| recommended_list = [] | |
| for _, row in recommended_items.iterrows(): | |
| recommended_list.append({ | |
| 'product': row.get('Item', row.get('product')), | |
| 'category': row.get('Category', row.get('category')), | |
| 'price': float(row['price']), | |
| 'recommendation_score': float(row['recommendation_score']), | |
| 'diet_suitability': float(row.get('diet_score', np.nan)), | |
| 'price_affordability': float(row['price_score']) | |
| }) | |
| # Calculate budget summary | |
| remaining = budget - total_cost | |
| result = { | |
| 'budget_pkr': float(budget), | |
| 'family_size': int(family_size), | |
| 'diet_type': diet_type, | |
| 'total_cost': round(total_cost, 2), | |
| 'remaining': round(remaining, 2), | |
| 'purchased_items': purchased_items, | |
| 'recommended_items': recommended_list | |
| } | |
| return result | |
| def get_recommended_items(result: dict) -> list: | |
| """Extract recommended items from result dictionary | |
| These are ML-ranked products that scored well but weren't purchased | |
| due to budget constraints. Useful for: | |
| - Discovering alternatives | |
| - Planning future purchases | |
| - Understanding what the ML model considers suitable | |
| """ | |
| return result.get('recommended_items', []) | |
| def display_results(result: dict): | |
| """Display purchased grocery list (actionable shopping list) | |
| This is what the user should actually buy - clean and focused. | |
| Recommendations are shown separately only on request. | |
| """ | |
| print("\n" + "="*60) | |
| print(" 🛒 FINAL GROCERY LIST (Items to Buy)") | |
| print("="*60) | |
| print(f"\n📋 Budget Details:") | |
| print(f" Total Budget: PKR {result['budget_pkr']:,.2f}") | |
| print(f" Family Size: {result['family_size']} members") | |
| print(f" Diet Type: {result['diet_type']}") | |
| print(f" Total Cost: PKR {result['total_cost']:,.2f}") | |
| print(f" Remaining: PKR {result['remaining']:,.2f}") | |
| # Get recommendation summary statistics | |
| purchased = result['purchased_items'] | |
| if purchased: | |
| rec_scores = [item['recommendation_score'] for item in purchased] | |
| avg_score = np.mean(rec_scores) | |
| print(f"\n📊 Selection Quality:") | |
| print(f" Items Selected: {len(purchased)}") | |
| print(f" Avg ML Score: {avg_score:.2%}") | |
| # Display items by category | |
| print(f"\n🛍️ Shopping List:") | |
| print("-" * 60) | |
| # Group by category | |
| from collections import defaultdict | |
| by_category = defaultdict(list) | |
| for item in purchased: | |
| by_category[item['category']].append(item) | |
| # Sort categories for consistent display | |
| for category in sorted(by_category.keys()): | |
| items = by_category[category] | |
| cat_total = sum(item['total_cost'] for item in items) | |
| print(f"\n{category.upper()} (PKR {cat_total:,.2f})") | |
| for item in items: | |
| print(f" • {item['product']:<35} PKR {item['total_cost']:>8,.2f}") | |
| print("\n" + "="*60) | |
| def show_recommended_items(result: dict): | |
| """Display ML-ranked recommendations (items user may consider) | |
| These are high-scoring products NOT purchased due to budget constraints. | |
| Shown only when user explicitly requests them. | |
| Why show these: | |
| - Help users discover alternatives | |
| - Understand what ML considers suitable for their diet | |
| - Plan future purchases or substitutions | |
| """ | |
| recommended = result.get('recommended_items', []) | |
| if not recommended: | |
| print("\n💡 No additional recommendations available at this time.") | |
| return | |
| print("\n" + "="*60) | |
| print(" 💡 RECOMMENDED ITEMS (You May Consider)") | |
| print("="*60) | |
| print("\nThese are high-quality alternatives suggested by ML:") | |
| print(f"Total: {len(recommended)} items\n") | |
| # Display with detailed scoring | |
| print(f"{'Product':<35} {'Price':>10} {'ML Score':>10} {'Details':>20}") | |
| print("-" * 80) | |
| for item in recommended: | |
| product = item['product'][:33] # Truncate if too long | |
| price = item['price'] | |
| score = item['recommendation_score'] | |
| diet_suit = item['diet_suitability'] | |
| price_afford = item['price_affordability'] | |
| # Create details string | |
| details = f"D:{diet_suit:.0%} P:{price_afford:.0%}" | |
| print(f"{product:<35} PKR {price:>7,.2f} {score:>9.1%} {details:>20}") | |
| print("\n" + "="*60) | |
| print("Legend: ML Score = Overall recommendation strength") | |
| print(" D = Diet Suitability (60% weight)") | |
| print(" P = Price Affordability (40% weight)") | |
| print("="*60) | |
| def display_json(result: dict): | |
| """Display results as JSON""" | |
| print("\n" + "="*80) | |
| print("JSON OUTPUT") | |
| print("="*80) | |
| # Create simplified output for JSON | |
| json_output = { | |
| 'budget_pkr': result['budget_pkr'], | |
| 'family_size': result['family_size'], | |
| 'diet_type': result['diet_type'], | |
| 'total_cost': result['total_cost'], | |
| 'remaining': result['remaining'], | |
| 'purchased_items': result['purchased_items'], | |
| 'recommended_items': result['recommended_items'] | |
| } | |
| print(json.dumps(json_output, indent=2)) | |
| print("="*80) | |
| def prompt_budget(default: float = 5000) -> float: | |
| """Prompt user for budget in PKR""" | |
| while True: | |
| try: | |
| print() | |
| response = input(f">>> Enter your budget in PKR [default: {default}]: ").strip() | |
| if not response: | |
| print(f"[Selected] Budget: {default} PKR") | |
| return float(default) | |
| budget = float(response) | |
| print(f"[Selected] Budget: {budget} PKR") | |
| return budget | |
| except ValueError: | |
| print("[ERROR] Invalid input. Please enter a valid number.") | |
| def prompt_family_size(default: int = 3) -> int: | |
| """Prompt user for family size""" | |
| while True: | |
| try: | |
| print() | |
| response = input(f">>> Enter family size (number of people) [default: {default}]: ").strip() | |
| if not response: | |
| print(f"[Selected] Family Size: {default} people") | |
| return int(default) | |
| size = int(response) | |
| if size < 1: | |
| print("[ERROR] Family size must be at least 1.") | |
| continue | |
| print(f"[Selected] Family Size: {size} people") | |
| return size | |
| except ValueError: | |
| print("[ERROR] Invalid input. Please enter a valid number.") | |
| def prompt_diet_type(default: str = "Normal") -> str: | |
| """Prompt user for diet type""" | |
| valid_diets = ["Normal", "Diabetic", "Keto", "All", "Vegetarian"] | |
| print() | |
| print("Available diet types:") | |
| for i, diet in enumerate(valid_diets, 1): | |
| print(f" {i}. {diet}") | |
| while True: | |
| print() | |
| response = input(f">>> Select diet type (enter name or number) [default: {default}]: ").strip() | |
| if not response: | |
| print(f"[Selected] Diet Type: {default}") | |
| return default | |
| # Try numeric selection | |
| try: | |
| choice = int(response) | |
| if 1 <= choice <= len(valid_diets): | |
| selected = valid_diets[choice - 1] | |
| print(f"[Selected] Diet Type: {selected}") | |
| return selected | |
| else: | |
| print(f"[ERROR] Please enter a number between 1 and {len(valid_diets)}.") | |
| continue | |
| except ValueError: | |
| pass | |
| # Try text selection (case-insensitive) | |
| selected = response.capitalize() | |
| if selected in valid_diets: | |
| print(f"[Selected] Diet Type: {selected}") | |
| return selected | |
| print(f"[ERROR] Invalid diet. Please choose: {', '.join(valid_diets)}") | |
| def main(): | |
| print("\n" + "="*80) | |
| print("[GROCERY LIST RECOMMENDER]") | |
| print("Module: AI-powered recommendations using Random Forest (Real Dataset)") | |
| print("="*80) | |
| print("\nThis system recommends items based on:") | |
| print(" * Your budget") | |
| print(" * Family size") | |
| print(" * Diet type") | |
| # Load model and data | |
| print("\n[Loading real datasets...]") | |
| try: | |
| load_dataset() | |
| load_models() | |
| except FileNotFoundError as e: | |
| print(f"\nERROR: {e}") | |
| return | |
| # Get user input | |
| print("\n" + "="*80) | |
| print("STEP 1: ENTER YOUR PREFERENCES") | |
| print("="*80) | |
| budget = prompt_budget() | |
| family_size = prompt_family_size() | |
| diet_type = prompt_diet_type() | |
| # Generate grocery list with ML-based recommendations | |
| print("\n" + "="*80) | |
| print("STEP 2: GENERATING GROCERY LIST") | |
| print("="*80) | |
| print("\n[Using ML model to rank products and select optimal items...]") | |
| result = generate_grocery_list( | |
| budget=budget, | |
| family_size=family_size, | |
| diet_type=diet_type | |
| ) | |
| # Display purchased items (always shown) | |
| print("\n" + "="*80) | |
| print("STEP 3: YOUR GROCERY LIST") | |
| print("="*80) | |
| display_results(result) | |
| # Ask if user wants to see recommendations (shown only on request) | |
| print("\n" + "="*80) | |
| print("STEP 4: ADDITIONAL OPTIONS") | |
| print("="*80) | |
| response = input("\nDo you want to see recommended items? (yes/no) [no]: ").strip().lower() | |
| if response in ['yes', 'y']: | |
| show_recommended_items(result) | |
| # Ask if user wants JSON output | |
| response = input("\nDo you want to export JSON? (yes/no) [no]: ").strip().lower() | |
| if response in ['yes', 'y']: | |
| display_json(result) | |
| # Ask if user wants to generate another list | |
| response = input("\nGenerate another list? (yes/no) [no]: ").strip().lower() | |
| if response in ['yes', 'y']: | |
| main() | |
| else: | |
| print("\nThank you for using ML Grocery Recommender!") | |
| print("="*80 + "\n") | |
| if __name__ == "__main__": | |
| main() | |