Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,35 @@ make migrate

```json
{
"product_name": "ginger_powder"
"product_SKU": "SKU001",
"days" : 30
}
```

### Response

```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
},
...
]
}
```
Expand All @@ -115,7 +129,7 @@ make migrate
The service calls the external warehouse API:

```REST
GET http://warehouse-service/api/stock/<product_name>/
GET http://warehouse-service/api/stock/<product_SKU>/
→ Expected response: { "current_stock": 120 }
```

Expand All @@ -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
---
Empty file.
19 changes: 19 additions & 0 deletions forecast_system/forecaster/feature_pipelines/pipeline.py
Original file line number Diff line number Diff line change
@@ -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
76 changes: 48 additions & 28 deletions forecast_system/forecaster/forecast_runner.py
Original file line number Diff line number Diff line change
@@ -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)}"}
Binary file added forecast_system/forecaster/models/SKU001.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU002.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU003.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU004.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU005.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU006.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU007.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU008.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU009.joblib
Binary file not shown.
Binary file added forecast_system/forecaster/models/SKU010.joblib
Binary file not shown.
Empty file.
1 change: 1 addition & 0 deletions forecast_system/forecaster/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

urlpatterns = [
path('', views.root_view),
path('forecast/', views.forecast_view),
]
29 changes: 25 additions & 4 deletions forecast_system/forecaster/utils/warehouse_api.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions forecast_system/forecaster/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)