diff --git a/README.md b/README.md index 32aaeab..e5265a2 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ make migrate ```json { - "product_name": "ginger_powder" + "product_SKU": "SKU001", + "days" : 30 } ``` @@ -87,13 +88,26 @@ make migrate ```json { - "product": "ginger_powder", - "current_stock": 120, - "forecasted_demand_next_30_days": 240, - "stock_shortfall": 120, - "daily_predictions": [ - { "date": "2025-05-02", "predicted": 7.3 }, - ... + "product_SKU": "SKU001", + "current_stock": 10000, + "average_forecasted_demand": 14608.27, + "maximum_forecast": 16425.71, + "minimum_forecast": 12764.46, + "stock_shortfall": 4608.27, + "daily_predictions": [ + { + "date": "2025-05-03", + "predicted": 593.5, + "lower_bound": 530.5, + "upper_bound": 651.77 + }, + { + "date": "2025-05-04", + "predicted": 635.67, + "lower_bound": 575.94, + "upper_bound": 697.32 + }, + ... ] } ``` @@ -115,7 +129,7 @@ make migrate The service calls the external warehouse API: ```REST -GET http://warehouse-service/api/stock// +GET http://warehouse-service/api/stock// โ†’ Expected response: { "current_stock": 120 } ``` @@ -136,10 +150,4 @@ def prepare_features(df): return df ``` ---- - -## ๐Ÿงผ Linting and Best Practices - -- Keep views thin โ€” logic should stay in `forecast_runner.py` -- Avoid public access to the `.joblib` model files -- Use DRF (Django REST Framework) for extensible APIs +--- \ No newline at end of file diff --git a/forecast_system/forecaster/feature_pipelines/ginger_powder.py b/forecast_system/forecaster/feature_pipelines/ginger_powder.py deleted file mode 100644 index e69de29..0000000 diff --git a/forecast_system/forecaster/feature_pipelines/pipeline.py b/forecast_system/forecaster/feature_pipelines/pipeline.py new file mode 100644 index 0000000..f741d2e --- /dev/null +++ b/forecast_system/forecaster/feature_pipelines/pipeline.py @@ -0,0 +1,19 @@ +import pandas as pd + +def get_season(date): + month = date.month + if month in [12, 1, 2]: + return 'Winter' + elif month in [3, 4, 5]: + return 'Spring' + elif month in [6, 7, 8]: + return 'Summer' + else: + return 'Fall' + +def prepare_features(df: pd.DataFrame) -> pd.DataFrame: + df['is_weekend'] = df['ds'].dt.dayofweek.isin([5, 6]).astype(int) + df['season'] = df['ds'].apply(get_season) + season_dummies = pd.get_dummies(df['season']) + df = pd.concat([df, season_dummies], axis=1) + return df \ No newline at end of file diff --git a/forecast_system/forecaster/forecast_runner.py b/forecast_system/forecaster/forecast_runner.py index 935bb05..fe60305 100644 --- a/forecast_system/forecaster/forecast_runner.py +++ b/forecast_system/forecaster/forecast_runner.py @@ -1,28 +1,48 @@ -# import joblib -# import importlib -# import pandas as pd -# from .utils.warehouse_api import get_current_stock - -# def forecast_for_product(product_name: str): -# # Load model and feature pipeline -# model = joblib.load(f"forecaster/models/{product_name}.joblib") -# features = importlib.import_module(f"forecaster.feature_pipelines.{product_name}") - -# # Generate base DataFrame with dates -# df = pd.DataFrame({'ds': pd.date_range(start=pd.Timestamp.today(), periods=30)}) - -# df = features.prepare_features(df) -# forecast = model.predict(df) - -# # Get current stock -# stock = get_current_stock(product_name) -# total_forecast = forecast['yhat'].sum() -# shortage = max(0, total_forecast - stock) - -# return { -# "product": product_name, -# "current_stock": stock, -# "forecasted_demand_next_30_days": round(total_forecast, 2), -# "stock_shortfall": round(shortage, 2), -# "daily_predictions": forecast[['ds', 'yhat']].rename(columns={'ds': 'date', 'yhat': 'predicted'}).to_dict('records') -# } +import joblib +import importlib +import pandas as pd +import os +from .utils.warehouse_api import get_current_stock + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +MODEL_DIR = os.path.join(BASE_DIR, 'models') + + +def forecast_for_product(product_SKU: str, days: int = 30) -> dict: + model_path = os.path.join(MODEL_DIR, f"{product_SKU}.joblib") + if not os.path.isfile(model_path): + return {"error": f"Model for '{product_SKU}' not found."} + + try: + model = joblib.load(model_path) + pipeline = importlib.import_module(f'forecaster.feature_pipelines.pipeline') + + future_dates = pd.DataFrame({'ds': pd.date_range(start=pd.Timestamp.today(), periods=days)}) + features = pipeline.prepare_features(future_dates.copy()) + forecast = model.predict(features) + + current_stock = get_current_stock(product_SKU) + total_forecast = round(forecast['yhat'].sum(), 2) + + total_maximum = round(forecast['yhat_upper'].sum(), 2) + total_minimum = round(forecast['yhat_lower'].sum(), 2) + stock_shortfall = max(0, round(total_forecast - current_stock, 2)) + + # Convert dates to string format YYYY-MM-DD + forecast_copy = forecast.copy() + forecast_copy['ds'] = forecast_copy['ds'].dt.strftime('%Y-%m-%d') + + return { + "product_SKU": product_SKU, + "current_stock": current_stock, + "average_forecasted_demand": total_forecast, + "maximum_forecast": total_maximum, + "minimum_forecast": total_minimum, + "stock_shortfall": stock_shortfall, + "daily_predictions": forecast_copy[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].rename(columns={ + 'ds': 'date', 'yhat': 'predicted', 'yhat_lower': 'lower_bound', 'yhat_upper': 'upper_bound' + }).round({'predicted': 2, 'lower_bound': 2, 'upper_bound': 2}).to_dict(orient='records') + } + + except Exception as e: + return {"error": f"Failed to forecast for '{product_SKU}': {str(e)}"} diff --git a/forecast_system/forecaster/models/SKU001.joblib b/forecast_system/forecaster/models/SKU001.joblib new file mode 100644 index 0000000..70220db Binary files /dev/null and b/forecast_system/forecaster/models/SKU001.joblib differ diff --git a/forecast_system/forecaster/models/SKU002.joblib b/forecast_system/forecaster/models/SKU002.joblib new file mode 100644 index 0000000..41df6bb Binary files /dev/null and b/forecast_system/forecaster/models/SKU002.joblib differ diff --git a/forecast_system/forecaster/models/SKU003.joblib b/forecast_system/forecaster/models/SKU003.joblib new file mode 100644 index 0000000..526a8a7 Binary files /dev/null and b/forecast_system/forecaster/models/SKU003.joblib differ diff --git a/forecast_system/forecaster/models/SKU004.joblib b/forecast_system/forecaster/models/SKU004.joblib new file mode 100644 index 0000000..f5eb370 Binary files /dev/null and b/forecast_system/forecaster/models/SKU004.joblib differ diff --git a/forecast_system/forecaster/models/SKU005.joblib b/forecast_system/forecaster/models/SKU005.joblib new file mode 100644 index 0000000..e9ae900 Binary files /dev/null and b/forecast_system/forecaster/models/SKU005.joblib differ diff --git a/forecast_system/forecaster/models/SKU006.joblib b/forecast_system/forecaster/models/SKU006.joblib new file mode 100644 index 0000000..e66ca86 Binary files /dev/null and b/forecast_system/forecaster/models/SKU006.joblib differ diff --git a/forecast_system/forecaster/models/SKU007.joblib b/forecast_system/forecaster/models/SKU007.joblib new file mode 100644 index 0000000..63b7603 Binary files /dev/null and b/forecast_system/forecaster/models/SKU007.joblib differ diff --git a/forecast_system/forecaster/models/SKU008.joblib b/forecast_system/forecaster/models/SKU008.joblib new file mode 100644 index 0000000..80b614d Binary files /dev/null and b/forecast_system/forecaster/models/SKU008.joblib differ diff --git a/forecast_system/forecaster/models/SKU009.joblib b/forecast_system/forecaster/models/SKU009.joblib new file mode 100644 index 0000000..4e580f9 Binary files /dev/null and b/forecast_system/forecaster/models/SKU009.joblib differ diff --git a/forecast_system/forecaster/models/SKU010.joblib b/forecast_system/forecaster/models/SKU010.joblib new file mode 100644 index 0000000..bdba00b Binary files /dev/null and b/forecast_system/forecaster/models/SKU010.joblib differ diff --git a/forecast_system/forecaster/models/ginger_powder.joblib b/forecast_system/forecaster/models/ginger_powder.joblib deleted file mode 100644 index e69de29..0000000 diff --git a/forecast_system/forecaster/urls.py b/forecast_system/forecaster/urls.py index 5abd773..961e0fe 100644 --- a/forecast_system/forecaster/urls.py +++ b/forecast_system/forecaster/urls.py @@ -3,4 +3,5 @@ urlpatterns = [ path('', views.root_view), + path('forecast/', views.forecast_view), ] \ No newline at end of file diff --git a/forecast_system/forecaster/utils/warehouse_api.py b/forecast_system/forecaster/utils/warehouse_api.py index 3fa1961..f207c98 100644 --- a/forecast_system/forecaster/utils/warehouse_api.py +++ b/forecast_system/forecaster/utils/warehouse_api.py @@ -1,7 +1,28 @@ # import requests -# def get_current_stock(product_name: str) -> int: -# res = requests.get(f"http://warehouse-service/api/stock/{product_name}/") -# if res.status_code == 200: -# return res.json().get('current_stock', 0) +# WAREHOUSE_API_BASE = "http://localhost:8001/api" + +# def get_current_stock(product_SKU: str) -> int: +# try: +# response = requests.get(f"{WAREHOUSE_API_BASE}/stock/{product_SKU}/") +# if response.status_code == 200: +# return response.json().get("current_stock", 0) +# except Exception: +# pass # return 0 + +def get_current_stock(product_SKU: str) -> int: + """Return hardcoded stock values for different products.""" + stock_data = { + "SKU001": 10000, + "SKU002": 5000050, + "SKU003": 750000, + "SKU004": 500998, + "SKU005": 42000, + "SKU006": 585000, + "SKU007": 270000, + "SKU008": 45500, + "SKU009": 900000, + "SKU010": 4000000, + } + return stock_data.get(product_SKU, 0) \ No newline at end of file diff --git a/forecast_system/forecaster/views.py b/forecast_system/forecaster/views.py index 6b90b65..3087c1c 100644 --- a/forecast_system/forecaster/views.py +++ b/forecast_system/forecaster/views.py @@ -7,3 +7,17 @@ def root_view(request): return Response({"message": "Forecasting Service API"}, status=status.HTTP_200_OK) + +from .forecast_runner import forecast_for_product + +@api_view(['POST']) +def forecast_view(request): + product_SKU = request.data.get("product_SKU") + days = int(request.data.get("days", 30)) # Default to 30 if not provided + + if not product_SKU: + return Response({"error": "Missing product_SKU"}, status=400) + + result = forecast_for_product(product_SKU, days=days) + return Response(result, status=200 if 'error' not in result else 500) +