Unverified Commit eb36de58 authored by Utorque's avatar Utorque Committed by GitHub
Browse files

Merge pull request #1 from Utorque/claude/horloml-web-app-011CUbWgr7HjWM2LFKdsZCmc

Review horloML web app functionality
parents 500e71df 1a8e7155
Loading
Loading
Loading
Loading

forecast_app/README.md

0 → 100644
+106 −0
Original line number Diff line number Diff line
# HorloML Supply Chain Forecasting Web App

An educational web application for teaching supply chain forecasting and demand planning.

## Overview

Students analyze 10 years of historical sales data for 3 watch models and predict demand for Year 11. The app evaluates their predictions and shows the financial impact of forecasting errors.

## Features

- **Historical Data Analysis**: View and analyze 10 years of monthly sales data
- **Demand Forecasting**: Predict monthly demand for year 11 (12 months × 3 watches)
- **Financial Impact**: See how prediction errors affect revenue, costs, and profit
- **Performance Scoring**: Get an overall score based on accuracy and financial outcomes
- **Interactive Visualizations**: Charts showing trends, comparisons, and results

## Watch Models

1. **Luxury Classic** ($500) - High-end watches with holiday seasonality
2. **Sport Pro** ($220) - Athletic watches popular in spring/summer
3. **Casual Style** ($120) - Everyday watches with steady demand

## Installation

```bash
# Install dependencies
pip install -r requirements.txt

# Generate the dataset (already done, but can regenerate)
python data_generator.py

# Run the web app
python app.py
```

The app will be available at `http://localhost:5001`

## How It Works

### 1. Data Generation

The `data_generator.py` script creates realistic supply chain data:
- 11 years of monthly data (132 months total)
- Includes seasonal patterns, growth trends, and realistic variance
- Calculates demand, production, inventory, costs, and revenue

### 2. Student Workflow

1. **Home Page**: Learn about the challenge
2. **Historical Data**: Analyze 10 years of sales patterns
3. **Make Predictions**: Enter demand forecasts for year 11
4. **View Results**: See comparison with actual demand and financial impact

### 3. Evaluation Logic

The app evaluates predictions based on:
- **Prediction Accuracy**: How close predictions were to actual demand
- **Financial Impact**:
  - Under-prediction → Stockouts → Lost revenue
  - Over-prediction → Excess inventory → Holding costs
- **Overall Score**: Weighted combination of accuracy, profit, and service level

## Learning Objectives

Students learn:
- How to analyze time series data for patterns
- The importance of considering seasonality in forecasting
- The financial consequences of forecasting errors
- The balance between under-stocking (lost sales) and over-stocking (excess costs)

## File Structure

```
forecast_app/
├── app.py                          # Flask web application
├── data_generator.py               # Dataset generation script
├── requirements.txt                # Python dependencies
├── README.md                       # This file
├── data/                           # Generated datasets
│   ├── supply_chain_data_full.json     # Full 11 years
│   ├── supply_chain_data_training.json # Training data (years 1-10)
│   └── supply_chain_data_test.json     # Test data (year 11)
└── templates/                      # HTML templates
    ├── base.html                   # Base template
    ├── index.html                  # Home page
    ├── historical.html             # Historical data view
    ├── predict.html                # Prediction input
    └── results.html                # Results and evaluation
```

## Customization

To regenerate the dataset with different parameters, edit `data_generator.py`:
- Change `seed` for different random patterns
- Modify `base_demand`, `seasonality_amplitude`, or `trend` for each watch
- Adjust cost structures and pricing

## Technologies Used

- **Backend**: Flask (Python)
- **Frontend**: Bulma CSS, Chart.js
- **Data Generation**: NumPy

## License

Educational use only.

forecast_app/app.py

0 → 100644
+308 −0
Original line number Diff line number Diff line
"""
Supply Chain Forecasting Educational Web App

Students analyze 10 years of historical data and predict year 11.
Results show financial impact of their predictions.
"""

from flask import Flask, render_template, request, session, jsonify, redirect, url_for
import json
import os
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'supply-chain-forecast-secret-key-2024'
app.config['SESSION_TYPE'] = 'filesystem'

# Load dataset
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
TRAINING_DATA_PATH = os.path.join(DATA_DIR, 'supply_chain_data_training.json')
TEST_DATA_PATH = os.path.join(DATA_DIR, 'supply_chain_data_test.json')


def load_training_data():
    """Load the 10-year training dataset"""
    with open(TRAINING_DATA_PATH, 'r') as f:
        return json.load(f)


def load_test_data():
    """Load the year 11 test data (ground truth)"""
    with open(TEST_DATA_PATH, 'r') as f:
        return json.load(f)


def calculate_results(predictions, test_data):
    """
    Calculate financial results comparing predictions to actual data

    Args:
        predictions: Dict of {watch_id: [12 monthly predictions]}
        test_data: List of 12 months of actual data

    Returns:
        Dictionary with detailed results
    """
    watches = load_training_data()['metadata']['watches']

    results = {
        'monthly_comparison': [],
        'watch_performance': {},
        'financial_summary': {
            'total_revenue': 0,
            'total_costs': 0,
            'total_profit': 0,
            'lost_revenue': 0,
            'excess_costs': 0
        },
        'prediction_accuracy': {}
    }

    # Process each month
    for month_idx, month_data in enumerate(test_data):
        month_comparison = {
            'month': month_idx + 1,
            'date': month_data['date'],
            'watches': []
        }

        # Process each watch
        for watch_data in month_data['watches']:
            watch_id = watch_data['watch_id']
            watch = next(w for w in watches if w['id'] == watch_id)

            # Get student's prediction
            predicted_demand = predictions.get(str(watch_id), [0] * 12)[month_idx]
            actual_demand = watch_data['demand']

            # Calculate production based on prediction
            # Student's prediction drives production
            production = int(predicted_demand)

            # Calculate inventory (simple model: start with previous month's end)
            if month_idx == 0:
                inventory_start = 100  # Starting inventory for year 11
            else:
                prev_watch = [w for w in results['monthly_comparison'][month_idx-1]['watches']
                             if w['watch_id'] == watch_id][0]
                inventory_start = prev_watch['inventory_end']

            # Calculate what actually happens
            available = inventory_start + production
            units_sold = min(actual_demand, available)
            inventory_end = available - units_sold
            stockout = max(0, actual_demand - units_sold)

            # Financial calculations
            revenue = units_sold * watch['sell_price']
            production_cost = production * watch['base_cost']
            labor_cost = production * 20.0
            holding_cost = inventory_end * watch['base_cost'] * 0.02

            # Lost revenue from stockouts
            lost_revenue = stockout * watch['sell_price']

            # Excess costs from overproduction
            excess_inventory = max(0, inventory_end - 50)  # 50 is healthy safety stock
            excess_cost = excess_inventory * watch['base_cost'] * 0.05  # 5% waste/obsolescence

            total_costs = production_cost + labor_cost + holding_cost + excess_cost
            profit = revenue - total_costs

            # Prediction error
            error = abs(predicted_demand - actual_demand)
            error_pct = (error / actual_demand * 100) if actual_demand > 0 else 0

            watch_result = {
                'watch_id': watch_id,
                'watch_name': watch['name'],
                'predicted_demand': predicted_demand,
                'actual_demand': actual_demand,
                'production': production,
                'inventory_start': inventory_start,
                'inventory_end': inventory_end,
                'units_sold': units_sold,
                'stockout': stockout,
                'revenue': round(revenue, 2),
                'production_cost': round(production_cost, 2),
                'labor_cost': round(labor_cost, 2),
                'holding_cost': round(holding_cost, 2),
                'excess_cost': round(excess_cost, 2),
                'lost_revenue': round(lost_revenue, 2),
                'total_costs': round(total_costs, 2),
                'profit': round(profit, 2),
                'error': error,
                'error_pct': round(error_pct, 1)
            }

            month_comparison['watches'].append(watch_result)

            # Accumulate totals
            results['financial_summary']['total_revenue'] += revenue
            results['financial_summary']['total_costs'] += total_costs
            results['financial_summary']['total_profit'] += profit
            results['financial_summary']['lost_revenue'] += lost_revenue
            results['financial_summary']['excess_costs'] += excess_cost

            # Accumulate watch-level statistics
            if watch_id not in results['watch_performance']:
                results['watch_performance'][watch_id] = {
                    'watch_name': watch['name'],
                    'total_predicted': 0,
                    'total_actual': 0,
                    'total_sold': 0,
                    'total_stockout': 0,
                    'total_revenue': 0,
                    'total_profit': 0,
                    'errors': []
                }

            perf = results['watch_performance'][watch_id]
            perf['total_predicted'] += predicted_demand
            perf['total_actual'] += actual_demand
            perf['total_sold'] += units_sold
            perf['total_stockout'] += stockout
            perf['total_revenue'] += revenue
            perf['total_profit'] += profit
            perf['errors'].append(error_pct)

        results['monthly_comparison'].append(month_comparison)

    # Round financial summary
    for key in results['financial_summary']:
        results['financial_summary'][key] = round(results['financial_summary'][key], 2)

    # Calculate overall prediction accuracy
    all_errors = []
    for watch_id, perf in results['watch_performance'].items():
        avg_error = sum(perf['errors']) / len(perf['errors'])
        perf['avg_error_pct'] = round(avg_error, 1)
        perf['accuracy'] = round(100 - avg_error, 1)
        all_errors.extend(perf['errors'])

    results['prediction_accuracy'] = {
        'overall_error_pct': round(sum(all_errors) / len(all_errors), 1),
        'overall_accuracy': round(100 - (sum(all_errors) / len(all_errors)), 1)
    }

    # Calculate score (0-100)
    # Based on: accuracy (50%), profit (30%), service level (20%)
    accuracy_score = results['prediction_accuracy']['overall_accuracy'] * 0.5

    # Profit score (normalize to potential max profit)
    potential_profit = results['financial_summary']['total_revenue']  # If perfect predictions
    actual_profit = results['financial_summary']['total_profit']
    profit_score = min(30, (actual_profit / potential_profit * 30)) if potential_profit > 0 else 0

    # Service level score (based on stockouts)
    total_actual = sum(perf['total_actual'] for perf in results['watch_performance'].values())
    total_sold = sum(perf['total_sold'] for perf in results['watch_performance'].values())
    service_level = (total_sold / total_actual * 100) if total_actual > 0 else 0
    service_score = service_level * 0.2

    results['overall_score'] = round(accuracy_score + profit_score + service_score, 1)

    return results


@app.route('/')
def index():
    """Home page"""
    return render_template('index.html')


@app.route('/historical')
def historical():
    """View historical data (10 years)"""
    training_data = load_training_data()

    # Prepare data for visualization
    watches = training_data['metadata']['watches']
    historical_data = training_data['historical_data']

    # Aggregate by year for summary
    yearly_summary = {}
    for month_data in historical_data:
        year = month_data['year']
        if year not in yearly_summary:
            yearly_summary[year] = {
                'year': year,
                'watches': {w['id']: {
                    'name': w['name'],
                    'total_demand': 0,
                    'total_revenue': 0,
                    'total_profit': 0
                } for w in watches}
            }

        for watch_data in month_data['watches']:
            watch_id = watch_data['watch_id']
            yearly_summary[year]['watches'][watch_id]['total_demand'] += watch_data['demand']
            yearly_summary[year]['watches'][watch_id]['total_revenue'] += watch_data['revenue']
            yearly_summary[year]['watches'][watch_id]['total_profit'] += watch_data['profit']

    return render_template('historical.html',
                          watches=watches,
                          historical_data=historical_data,
                          yearly_summary=yearly_summary)


@app.route('/predict', methods=['GET', 'POST'])
def predict():
    """Prediction input page"""
    if request.method == 'POST':
        # Save predictions to session
        predictions = {}
        for watch_id in [1, 2, 3]:
            predictions[str(watch_id)] = []
            for month in range(1, 13):
                key = f'prediction_{watch_id}_{month}'
                value = request.form.get(key, 0)
                predictions[str(watch_id)].append(int(value))

        session['predictions'] = predictions

        # Calculate results
        test_data = load_test_data()
        results = calculate_results(predictions, test_data)
        session['results'] = results

        return redirect(url_for('results'))

    # GET request - show form
    training_data = load_training_data()
    watches = training_data['metadata']['watches']

    # Get last 12 months as reference
    last_year = training_data['historical_data'][-12:]

    return render_template('predict.html',
                          watches=watches,
                          last_year=last_year)


@app.route('/results')
def results():
    """Results page showing comparison and financial impact"""
    if 'results' not in session:
        return redirect(url_for('predict'))

    results = session['results']
    predictions = session['predictions']
    test_data = load_test_data()

    return render_template('results.html',
                          results=results,
                          predictions=predictions,
                          test_data=test_data)


@app.route('/reset')
def reset():
    """Clear session and start over"""
    session.clear()
    return redirect(url_for('index'))


if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5001)
+7845 −0

File added.

Preview size limit exceeded, changes collapsed.

+710 −0

File added.

Preview size limit exceeded, changes collapsed.

+7138 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading