Commit ·
1dfcad5
1
Parent(s): 0e56e07
initial commit
Browse files- .gitattributes +1 -0
- Dockerfile +12 -0
- app.py +31 -0
- modules/__init__.py +0 -0
- modules/__pycache__/__init__.cpython-312.pyc +0 -0
- modules/__pycache__/__init__.cpython-38.pyc +0 -0
- modules/__pycache__/campaign_analysis.cpython-312.pyc +0 -0
- modules/__pycache__/campaign_analysis.cpython-38.pyc +0 -0
- modules/__pycache__/inventory_optimization.cpython-312.pyc +0 -0
- modules/__pycache__/inventory_optimization.cpython-38.pyc +0 -0
- modules/__pycache__/machine_failure.cpython-38.pyc +0 -0
- modules/__pycache__/pricing_intelligence.cpython-312.pyc +0 -0
- modules/__pycache__/pricing_intelligence.cpython-38.pyc +0 -0
- modules/__pycache__/sales_perdiction.cpython-312.pyc +0 -0
- modules/__pycache__/sales_perdiction.cpython-38.pyc +0 -0
- modules/__pycache__/supply_failure.cpython-38.pyc +0 -0
- modules/__pycache__/underperformance.cpython-312.pyc +0 -0
- modules/__pycache__/underperformance.cpython-38.pyc +0 -0
- modules/campaign_analysis.py +162 -0
- modules/inventory_optimization.py +7 -0
- modules/machine_failure.py +376 -0
- modules/pricing_intelligence.py +7 -0
- modules/sales_perdiction.py +246 -0
- modules/supply_failure.py +363 -0
- modules/underperformance.py +7 -0
- requirements.txt +11 -0
- static/images/campaign.jpg +0 -0
- static/images/cover.jpeg +0 -0
- static/images/inventory.png +3 -0
- static/images/machine.jpeg +0 -0
- static/images/pricing.jpg +0 -0
- static/images/sales prediction.png +3 -0
- static/images/supply.jpeg +0 -0
- static/images/underperforming.png +3 -0
- static/index.js +690 -0
- static/styles.css +99 -0
- templates/base.html +38 -0
- templates/campaign_analysis.html +105 -0
- templates/index.html +104 -0
- templates/inventory_optimization.html +11 -0
- templates/machine_failure.html +101 -0
- templates/pricing_intelligence.html +11 -0
- templates/sales_prediction.html +147 -0
- templates/supply_failure.html +99 -0
- templates/underperformance.html +11 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
|
| 12 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template
|
| 2 |
+
from modules.sales_perdiction import sales_prediction_bp
|
| 3 |
+
from modules.campaign_analysis import campaign_analysis_bp
|
| 4 |
+
from modules.inventory_optimization import inventory_optimization_bp
|
| 5 |
+
from modules.pricing_intelligence import pricing_intelligence_bp
|
| 6 |
+
from modules.underperformance import underperformance_bp
|
| 7 |
+
from modules.machine_failure import machine_failure_bp
|
| 8 |
+
from modules.supply_failure import supply_failure_bp
|
| 9 |
+
|
| 10 |
+
app = Flask(__name__)
|
| 11 |
+
app.secret_key = "secret_key"
|
| 12 |
+
app.config['SESSION_TYPE'] = 'filesystem'
|
| 13 |
+
|
| 14 |
+
app.register_blueprint(sales_prediction_bp)
|
| 15 |
+
app.register_blueprint(campaign_analysis_bp)
|
| 16 |
+
app.register_blueprint(inventory_optimization_bp)
|
| 17 |
+
app.register_blueprint(pricing_intelligence_bp)
|
| 18 |
+
app.register_blueprint(underperformance_bp)
|
| 19 |
+
app.register_blueprint(machine_failure_bp)
|
| 20 |
+
app.register_blueprint(supply_failure_bp)
|
| 21 |
+
|
| 22 |
+
@app.route('/')
|
| 23 |
+
def index():
|
| 24 |
+
return render_template('index.html', title="Home - Predictive Analytics Tool")
|
| 25 |
+
|
| 26 |
+
# if __name__ == '__main__':
|
| 27 |
+
# app.run(debug=True)
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
port = 7860
|
| 31 |
+
app.run(debug=True, host="0.0.0.0", port=port)
|
modules/__init__.py
ADDED
|
File without changes
|
modules/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (180 Bytes). View file
|
|
|
modules/__pycache__/__init__.cpython-38.pyc
ADDED
|
Binary file (164 Bytes). View file
|
|
|
modules/__pycache__/campaign_analysis.cpython-312.pyc
ADDED
|
Binary file (654 Bytes). View file
|
|
|
modules/__pycache__/campaign_analysis.cpython-38.pyc
ADDED
|
Binary file (5.8 kB). View file
|
|
|
modules/__pycache__/inventory_optimization.cpython-312.pyc
ADDED
|
Binary file (686 Bytes). View file
|
|
|
modules/__pycache__/inventory_optimization.cpython-38.pyc
ADDED
|
Binary file (562 Bytes). View file
|
|
|
modules/__pycache__/machine_failure.cpython-38.pyc
ADDED
|
Binary file (10.5 kB). View file
|
|
|
modules/__pycache__/pricing_intelligence.cpython-312.pyc
ADDED
|
Binary file (672 Bytes). View file
|
|
|
modules/__pycache__/pricing_intelligence.cpython-38.pyc
ADDED
|
Binary file (548 Bytes). View file
|
|
|
modules/__pycache__/sales_perdiction.cpython-312.pyc
ADDED
|
Binary file (14.1 kB). View file
|
|
|
modules/__pycache__/sales_perdiction.cpython-38.pyc
ADDED
|
Binary file (7.03 kB). View file
|
|
|
modules/__pycache__/supply_failure.cpython-38.pyc
ADDED
|
Binary file (10.3 kB). View file
|
|
|
modules/__pycache__/underperformance.cpython-312.pyc
ADDED
|
Binary file (666 Bytes). View file
|
|
|
modules/__pycache__/underperformance.cpython-38.pyc
ADDED
|
Binary file (542 Bytes). View file
|
|
|
modules/campaign_analysis.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, request, flash, redirect, url_for
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
campaign_analysis_bp = Blueprint('campaign_analysis', __name__, url_prefix='/campaign')
|
| 7 |
+
|
| 8 |
+
_current_sales_df = None
|
| 9 |
+
_current_campaign_df = None
|
| 10 |
+
|
| 11 |
+
def normalize(s):
|
| 12 |
+
return str(s).strip().lower()
|
| 13 |
+
|
| 14 |
+
# OPTIONAL: channel mapping between campaign and sales if desired
|
| 15 |
+
CHANNEL_MAP = {
|
| 16 |
+
'whatsapp': 'online',
|
| 17 |
+
'email': 'online',
|
| 18 |
+
'mobile app': 'online',
|
| 19 |
+
'tv': 'offline',
|
| 20 |
+
# add more as needed
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@campaign_analysis_bp.route('/', methods=['GET'])
|
| 24 |
+
def show_campaign_analysis():
|
| 25 |
+
return render_template('campaign_analysis.html', title="Campaign Analysis Engine")
|
| 26 |
+
|
| 27 |
+
@campaign_analysis_bp.route('/upload', methods=['POST'])
|
| 28 |
+
def upload_files():
|
| 29 |
+
global _current_sales_df, _current_campaign_df
|
| 30 |
+
|
| 31 |
+
sales_file = request.files.get('sales_file')
|
| 32 |
+
campaign_file = request.files.get('campaign_file')
|
| 33 |
+
if not sales_file or not campaign_file:
|
| 34 |
+
flash('Please upload both sales and campaign CSV files.')
|
| 35 |
+
return redirect(url_for('campaign_analysis.show_campaign_analysis'))
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
_current_sales_df = pd.read_csv(sales_file, parse_dates=['date'], dayfirst=True)
|
| 39 |
+
_current_campaign_df = pd.read_csv(campaign_file, parse_dates=['start_date', 'end_date'], dayfirst=True)
|
| 40 |
+
except Exception as e:
|
| 41 |
+
flash(f'Error reading CSVs: {e}')
|
| 42 |
+
return redirect(url_for('campaign_analysis.show_campaign_analysis'))
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
analysis = analyze_campaigns(_current_sales_df, _current_campaign_df)
|
| 46 |
+
plot_data = make_plot_data(analysis['campaigns'])
|
| 47 |
+
except Exception as e:
|
| 48 |
+
flash(f'Error during analysis: {e}')
|
| 49 |
+
return redirect(url_for('campaign_analysis.show_campaign_analysis'))
|
| 50 |
+
|
| 51 |
+
return render_template('campaign_analysis.html',
|
| 52 |
+
title="Campaign Analysis Engine",
|
| 53 |
+
campaigns=analysis['campaigns'],
|
| 54 |
+
summary=analysis['summary'],
|
| 55 |
+
columns=_current_campaign_df.columns.tolist(),
|
| 56 |
+
plot_data=plot_data # <--- DO NOT json.dumps here!
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
def make_plot_data(campaigns):
|
| 60 |
+
# For bar and line charts
|
| 61 |
+
names = [c['name'] if c['name'] else c['id'] for c in campaigns]
|
| 62 |
+
roi = [c['roi'] if c['roi']==c['roi'] else 0 for c in campaigns]
|
| 63 |
+
uplift = [c['uplift_pct'] if c['uplift_pct']==c['uplift_pct'] else 0 for c in campaigns]
|
| 64 |
+
revenue = [c['total_revenue'] for c in campaigns]
|
| 65 |
+
start_dates = [str(c['start_date']) for c in campaigns]
|
| 66 |
+
types = [c['name'] for c in campaigns]
|
| 67 |
+
regions = [c['region'] for c in campaigns]
|
| 68 |
+
|
| 69 |
+
# Pie chart of types
|
| 70 |
+
from collections import Counter
|
| 71 |
+
type_counts = Counter(types)
|
| 72 |
+
region_counts = Counter(regions)
|
| 73 |
+
|
| 74 |
+
return {
|
| 75 |
+
"bar_uplift": {"x": names, "y": uplift},
|
| 76 |
+
"bar_roi": {"x": names, "y": roi},
|
| 77 |
+
"line_revenue": {"x": start_dates, "y": revenue, "names": names},
|
| 78 |
+
"pie_type": {"labels": list(type_counts.keys()), "values": list(type_counts.values())},
|
| 79 |
+
"pie_region": {"labels": list(region_counts.keys()), "values": list(region_counts.values())}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
def map_campaign_channel_to_sales(campaign_channel):
|
| 83 |
+
# Normalize and map campaign channel to sales channel if needed
|
| 84 |
+
ch = normalize(campaign_channel)
|
| 85 |
+
return CHANNEL_MAP.get(ch, ch)
|
| 86 |
+
|
| 87 |
+
def analyze_campaigns(sales_df, campaign_df):
|
| 88 |
+
"""
|
| 89 |
+
For each campaign, compute total units/revenue during the campaign period,
|
| 90 |
+
estimate uplift vs. pre-campaign, and simple ROI.
|
| 91 |
+
"""
|
| 92 |
+
sales_df['channel'] = sales_df['channel'].astype(str)
|
| 93 |
+
campaign_results = []
|
| 94 |
+
for _, camp in campaign_df.iterrows():
|
| 95 |
+
# Split target_skus and iterate
|
| 96 |
+
target_skus = [sku.strip() for sku in str(camp['target_skus']).split(',')]
|
| 97 |
+
region = camp['region']
|
| 98 |
+
# Map campaign channel to sales channel if needed
|
| 99 |
+
campaign_channel = camp['channel']
|
| 100 |
+
mapped_channel = map_campaign_channel_to_sales(campaign_channel)
|
| 101 |
+
start_date = camp['start_date']
|
| 102 |
+
end_date = camp['end_date']
|
| 103 |
+
|
| 104 |
+
# Filter sales for this campaign
|
| 105 |
+
sales_mask = (
|
| 106 |
+
sales_df['sku'].isin(target_skus)
|
| 107 |
+
& (sales_df['region'] == region)
|
| 108 |
+
& (sales_df['channel'].str.lower() == mapped_channel)
|
| 109 |
+
& (sales_df['date'] >= start_date)
|
| 110 |
+
& (sales_df['date'] <= end_date)
|
| 111 |
+
)
|
| 112 |
+
sales_during = sales_df[sales_mask]
|
| 113 |
+
total_units = int(sales_during['sales_units'].sum())
|
| 114 |
+
total_revenue = float(sales_during['sales_revenue'].sum())
|
| 115 |
+
|
| 116 |
+
# Uplift: compare average daily units in campaign vs. 14 days prior
|
| 117 |
+
pre_mask = (
|
| 118 |
+
sales_df['sku'].isin(target_skus)
|
| 119 |
+
& (sales_df['region'] == region)
|
| 120 |
+
& (sales_df['channel'].str.lower() == mapped_channel)
|
| 121 |
+
& (sales_df['date'] >= (start_date - pd.Timedelta(days=14)))
|
| 122 |
+
& (sales_df['date'] < start_date)
|
| 123 |
+
)
|
| 124 |
+
sales_pre = sales_df[pre_mask]
|
| 125 |
+
days_campaign = (end_date - start_date).days + 1
|
| 126 |
+
days_pre = (sales_pre['date'].max() - sales_pre['date'].min()).days + 1 if not sales_pre.empty else 1
|
| 127 |
+
avg_daily_campaign = total_units / days_campaign if days_campaign > 0 else 0
|
| 128 |
+
avg_daily_pre = sales_pre['sales_units'].sum() / days_pre if days_pre > 0 and not sales_pre.empty else 0
|
| 129 |
+
uplift_pct = ((avg_daily_campaign - avg_daily_pre) / avg_daily_pre * 100) if avg_daily_pre > 0 else np.nan
|
| 130 |
+
|
| 131 |
+
# ROI: (Incremental revenue - budget) / budget
|
| 132 |
+
budget = camp.get('budget', np.nan)
|
| 133 |
+
try:
|
| 134 |
+
budget = float(budget)
|
| 135 |
+
except Exception:
|
| 136 |
+
budget = np.nan
|
| 137 |
+
incremental_revenue = total_revenue - sales_pre['sales_revenue'].sum() if not sales_pre.empty else total_revenue
|
| 138 |
+
roi = ((incremental_revenue - budget) / budget * 100) if pd.notna(budget) and budget > 0 else np.nan
|
| 139 |
+
|
| 140 |
+
campaign_results.append({
|
| 141 |
+
'id': camp.get('campaign_id', ''),
|
| 142 |
+
'name': camp.get('type', ''),
|
| 143 |
+
'sku': ','.join(target_skus),
|
| 144 |
+
'region': region,
|
| 145 |
+
'channel': campaign_channel,
|
| 146 |
+
'start_date': start_date.date() if not pd.isnull(start_date) else '',
|
| 147 |
+
'end_date': end_date.date() if not pd.isnull(end_date) else '',
|
| 148 |
+
'budget': budget if not pd.isnull(budget) else '-',
|
| 149 |
+
'total_units': total_units,
|
| 150 |
+
'total_revenue': total_revenue,
|
| 151 |
+
'uplift_pct': uplift_pct,
|
| 152 |
+
'roi': roi
|
| 153 |
+
})
|
| 154 |
+
|
| 155 |
+
# Simple summary for display
|
| 156 |
+
summary = {
|
| 157 |
+
'total_campaigns': len(campaign_results),
|
| 158 |
+
'total_revenue': float(np.nansum([c['total_revenue'] for c in campaign_results])),
|
| 159 |
+
'avg_uplift_pct': float(np.nanmean([c['uplift_pct'] for c in campaign_results if not np.isnan(c['uplift_pct'])])) if campaign_results else 0,
|
| 160 |
+
'avg_roi': float(np.nanmean([c['roi'] for c in campaign_results if not np.isnan(c['roi'])])) if campaign_results else 0,
|
| 161 |
+
}
|
| 162 |
+
return {'campaigns': campaign_results, 'summary': summary}
|
modules/inventory_optimization.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template
|
| 2 |
+
|
| 3 |
+
inventory_optimization_bp = Blueprint('inventory_optimization', __name__, url_prefix='/inventory')
|
| 4 |
+
|
| 5 |
+
@inventory_optimization_bp.route('/')
|
| 6 |
+
def show_inventory_optimization():
|
| 7 |
+
return render_template('inventory_optimization.html', title="Inventory Optimization")
|
modules/machine_failure.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.utils
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import joblib
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from sklearn.model_selection import train_test_split
|
| 11 |
+
from sklearn.preprocessing import StandardScaler, LabelEncoder
|
| 12 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 13 |
+
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
|
| 14 |
+
import random
|
| 15 |
+
|
| 16 |
+
machine_failure_bp = Blueprint('machine_failure', __name__, url_prefix='/predict/machine_failure')
|
| 17 |
+
|
| 18 |
+
UPLOAD_FOLDER = 'temp_uploads'
|
| 19 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 20 |
+
|
| 21 |
+
def get_current_df():
|
| 22 |
+
try:
|
| 23 |
+
csv_path = session.get('machine_csv_path')
|
| 24 |
+
print(f"Debug - CSV Path from session: {csv_path}")
|
| 25 |
+
|
| 26 |
+
if csv_path and os.path.exists(csv_path):
|
| 27 |
+
print(f"Debug - File exists at path: {csv_path}")
|
| 28 |
+
return pd.read_csv(csv_path)
|
| 29 |
+
|
| 30 |
+
print("Debug - No valid CSV path found")
|
| 31 |
+
return None
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"Debug - Error in get_current_df: {str(e)}")
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
def get_summary_stats(df):
|
| 37 |
+
return {
|
| 38 |
+
'total_rows': len(df),
|
| 39 |
+
'total_columns': len(df.columns),
|
| 40 |
+
'columns': list(df.columns),
|
| 41 |
+
'numeric_columns': list(df.select_dtypes(include=[np.number]).columns),
|
| 42 |
+
'categorical_columns': list(df.select_dtypes(exclude=[np.number]).columns),
|
| 43 |
+
'missing_values': df.isnull().sum().to_dict()
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def preprocess_data(df, for_prediction=False, label_encoders=None):
|
| 47 |
+
"""Preprocess the dataframe for machine learning
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
df (pd.DataFrame): The input DataFrame.
|
| 51 |
+
for_prediction (bool): True if preprocessing for a single prediction, False for training.
|
| 52 |
+
label_encoders (dict): Dictionary of pre-fitted LabelEncoders for single prediction.
|
| 53 |
+
"""
|
| 54 |
+
df_processed = df.copy()
|
| 55 |
+
|
| 56 |
+
categorical_columns = []
|
| 57 |
+
numerical_columns = []
|
| 58 |
+
|
| 59 |
+
# Dynamically determine column types based on the current DataFrame
|
| 60 |
+
for column in df_processed.columns:
|
| 61 |
+
if column in ['timestamp', 'maintenance_timestamp']:
|
| 62 |
+
continue
|
| 63 |
+
|
| 64 |
+
# Check if column is numeric or can be converted to numeric (after dropping NaNs for check)
|
| 65 |
+
if pd.api.types.is_numeric_dtype(df_processed[column]):
|
| 66 |
+
numerical_columns.append(column)
|
| 67 |
+
else:
|
| 68 |
+
try:
|
| 69 |
+
# Attempt to convert to numeric, if successful, it's numeric
|
| 70 |
+
if pd.to_numeric(df_processed[column].dropna()).notna().all():
|
| 71 |
+
numerical_columns.append(column)
|
| 72 |
+
else:
|
| 73 |
+
categorical_columns.append(column)
|
| 74 |
+
except ValueError:
|
| 75 |
+
categorical_columns.append(column)
|
| 76 |
+
|
| 77 |
+
# Handle timestamps
|
| 78 |
+
for time_col in ['timestamp', 'maintenance_timestamp']:
|
| 79 |
+
if time_col in df_processed.columns:
|
| 80 |
+
# Convert to datetime, coercing errors
|
| 81 |
+
df_processed[time_col] = pd.to_datetime(df_processed[time_col], errors='coerce')
|
| 82 |
+
|
| 83 |
+
# Extract features only if the column contains valid datetime values
|
| 84 |
+
if not df_processed[time_col].isnull().all():
|
| 85 |
+
df_processed[f'{time_col}_hour'] = df_processed[time_col].dt.hour.fillna(0) # Fill NaN with 0 if time part is missing
|
| 86 |
+
df_processed[f'{time_col}_day'] = df_processed[time_col].dt.day.fillna(0)
|
| 87 |
+
df_processed[f'{time_col}_month'] = df_processed[time_col].dt.month.fillna(0)
|
| 88 |
+
else:
|
| 89 |
+
df_processed[f'{time_col}_hour'] = 0
|
| 90 |
+
df_processed[f'{time_col}_day'] = 0
|
| 91 |
+
df_processed[f'{time_col}_month'] = 0
|
| 92 |
+
|
| 93 |
+
df_processed = df_processed.drop(columns=[time_col])
|
| 94 |
+
|
| 95 |
+
# Encode categorical variables
|
| 96 |
+
current_label_encoders = {}
|
| 97 |
+
if not for_prediction: # During training, fit and save encoders
|
| 98 |
+
for col in categorical_columns:
|
| 99 |
+
if col in df_processed.columns:
|
| 100 |
+
le = LabelEncoder()
|
| 101 |
+
df_processed[col] = le.fit_transform(df_processed[col].astype(str))
|
| 102 |
+
current_label_encoders[col] = le
|
| 103 |
+
else:
|
| 104 |
+
for col, le in label_encoders.items():
|
| 105 |
+
if col in df_processed.columns:
|
| 106 |
+
# Use a lambda function to handle unseen labels: map to -1 or a default if not in classes
|
| 107 |
+
df_processed[col] = df_processed[col].astype(str).apply(
|
| 108 |
+
lambda x: le.transform([x])[0] if x in le.classes_ else -1
|
| 109 |
+
)
|
| 110 |
+
return df_processed, current_label_encoders
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@machine_failure_bp.route('/', methods=['GET'])
|
| 114 |
+
def show_machine_failure():
|
| 115 |
+
return render_template('machine_failure.html', title="Machine Failure Prediction")
|
| 116 |
+
|
| 117 |
+
@machine_failure_bp.route('/upload', methods=['POST'])
|
| 118 |
+
def upload_file():
|
| 119 |
+
if 'machine_file' not in request.files:
|
| 120 |
+
flash('No file selected')
|
| 121 |
+
return redirect(url_for('machine_failure.show_machine_failure'))
|
| 122 |
+
|
| 123 |
+
file = request.files['machine_file']
|
| 124 |
+
if file.filename == '':
|
| 125 |
+
flash('No file selected')
|
| 126 |
+
return redirect(url_for('machine_failure.show_machine_failure'))
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 130 |
+
safe_filename = f"machine_data_{timestamp}.csv"
|
| 131 |
+
file_path = os.path.join(UPLOAD_FOLDER, safe_filename)
|
| 132 |
+
|
| 133 |
+
file.save(file_path)
|
| 134 |
+
session['machine_csv_path'] = file_path
|
| 135 |
+
print(f"Debug - Saved file to: {file_path}")
|
| 136 |
+
|
| 137 |
+
df = pd.read_csv(file_path)
|
| 138 |
+
preview_data = df.head().to_dict('records')
|
| 139 |
+
summary_stats = get_summary_stats(df)
|
| 140 |
+
|
| 141 |
+
session['original_columns'] = df.columns.tolist()
|
| 142 |
+
|
| 143 |
+
return render_template('machine_failure.html',
|
| 144 |
+
title="Machine Failure Prediction",
|
| 145 |
+
preview_data=preview_data,
|
| 146 |
+
columns=df.columns.tolist(),
|
| 147 |
+
summary_stats=summary_stats)
|
| 148 |
+
|
| 149 |
+
except Exception as e:
|
| 150 |
+
print(f"Debug - Upload error: {str(e)}")
|
| 151 |
+
flash(f'Error processing file: {str(e)}')
|
| 152 |
+
return redirect(url_for('machine_failure.show_machine_failure'))
|
| 153 |
+
|
| 154 |
+
@machine_failure_bp.route('/run_prediction', methods=['POST'])
|
| 155 |
+
def run_prediction():
|
| 156 |
+
try:
|
| 157 |
+
df = get_current_df()
|
| 158 |
+
if df is None:
|
| 159 |
+
return jsonify({'success': False, 'error': 'No data available. Please upload a CSV file first.'})
|
| 160 |
+
|
| 161 |
+
target_col = request.form.get('target_col')
|
| 162 |
+
if not target_col:
|
| 163 |
+
return jsonify({'success': False, 'error': 'Target column not selected.'})
|
| 164 |
+
|
| 165 |
+
# Preprocess the data for training
|
| 166 |
+
df_processed, label_encoders = preprocess_data(df.copy(), for_prediction=False)
|
| 167 |
+
|
| 168 |
+
encoders_path = os.path.join(UPLOAD_FOLDER, f'encoders_{datetime.now().strftime("%Y%m%d_%H%M%S")}.joblib')
|
| 169 |
+
joblib.dump(label_encoders, encoders_path)
|
| 170 |
+
session['encoders_path'] = encoders_path
|
| 171 |
+
|
| 172 |
+
# Prepare features and target
|
| 173 |
+
if target_col not in df_processed.columns:
|
| 174 |
+
return jsonify({'success': False, 'error': f"Target column '{target_col}' not found after preprocessing. Check if it was dropped or transformed incorrectly."})
|
| 175 |
+
|
| 176 |
+
X = df_processed.drop(columns=[target_col])
|
| 177 |
+
y = df_processed[target_col]
|
| 178 |
+
|
| 179 |
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 180 |
+
scaler = StandardScaler()
|
| 181 |
+
X_train_scaled = scaler.fit_transform(X_train)
|
| 182 |
+
X_test_scaled = scaler.transform(X_test)
|
| 183 |
+
|
| 184 |
+
clf = RandomForestClassifier(random_state=42)
|
| 185 |
+
clf.fit(X_train_scaled, y_train)
|
| 186 |
+
y_pred = clf.predict(X_test_scaled)
|
| 187 |
+
|
| 188 |
+
importances = clf.feature_importances_
|
| 189 |
+
feature_names = X.columns
|
| 190 |
+
feature_importance = sorted(
|
| 191 |
+
zip(feature_names, importances),
|
| 192 |
+
key=lambda x: x[1],
|
| 193 |
+
reverse=True
|
| 194 |
+
)[:5]
|
| 195 |
+
|
| 196 |
+
top_features = [{'feature': f, 'importance': float(imp)} for f, imp in feature_importance]
|
| 197 |
+
|
| 198 |
+
session['feature_names'] = X.columns.tolist()
|
| 199 |
+
session['target_column_name'] = target_col
|
| 200 |
+
|
| 201 |
+
metrics = {
|
| 202 |
+
'Accuracy': accuracy_score(y_test, y_pred),
|
| 203 |
+
'Precision': precision_score(y_test, y_pred, average='weighted', zero_division=0),
|
| 204 |
+
'Recall': recall_score(y_test, y_pred, average='weighted', zero_division=0),
|
| 205 |
+
'F1 Score': f1_score(y_test, y_pred, average='weighted', zero_division=0)
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
model_path = os.path.join(UPLOAD_FOLDER, f'model_{datetime.now().strftime("%Y%m%d_%H%M%S")}.joblib')
|
| 209 |
+
scaler_path = os.path.join(UPLOAD_FOLDER, f'scaler_{datetime.now().strftime("%Y%m%d_%H%M%S")}.joblib')
|
| 210 |
+
|
| 211 |
+
joblib.dump(clf, model_path)
|
| 212 |
+
joblib.dump(scaler, scaler_path)
|
| 213 |
+
|
| 214 |
+
session['model_path'] = model_path
|
| 215 |
+
session['scaler_path'] = scaler_path
|
| 216 |
+
|
| 217 |
+
return jsonify({
|
| 218 |
+
'success': True,
|
| 219 |
+
'metrics': metrics,
|
| 220 |
+
'top_features': top_features,
|
| 221 |
+
})
|
| 222 |
+
|
| 223 |
+
except Exception as e:
|
| 224 |
+
print(f"Error in run_prediction: {e}")
|
| 225 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 226 |
+
|
| 227 |
+
@machine_failure_bp.route('/get_form_data', methods=['GET'])
|
| 228 |
+
def get_form_data():
|
| 229 |
+
try:
|
| 230 |
+
df = get_current_df()
|
| 231 |
+
if df is None:
|
| 232 |
+
return jsonify({'success': False, 'error': 'No data available. Please upload a file first.'})
|
| 233 |
+
|
| 234 |
+
target_col = session.get('target_column_name')
|
| 235 |
+
if not target_col:
|
| 236 |
+
return jsonify({'success': False, 'error': 'Target column not found in session. Please run prediction first.'})
|
| 237 |
+
|
| 238 |
+
exclude_cols = ['error_severity', 'downtime_minutes', 'failure_type', target_col]
|
| 239 |
+
|
| 240 |
+
form_fields = []
|
| 241 |
+
for col in df.columns:
|
| 242 |
+
if col.lower() in [ec.lower() for ec in exclude_cols]:
|
| 243 |
+
continue
|
| 244 |
+
|
| 245 |
+
default_value = None
|
| 246 |
+
if not df[col].dropna().empty:
|
| 247 |
+
if pd.api.types.is_numeric_dtype(df[col]):
|
| 248 |
+
min_val = df[col].min()
|
| 249 |
+
max_val = df[col].max()
|
| 250 |
+
if pd.isna(min_val) or pd.isna(max_val):
|
| 251 |
+
default_value = 0.0
|
| 252 |
+
else:
|
| 253 |
+
default_value = round(random.uniform(float(min_val), float(max_val)), 2)
|
| 254 |
+
elif col in ['timestamp', 'maintenance_timestamp']:
|
| 255 |
+
sample_date = random.choice(df[col].dropna().tolist())
|
| 256 |
+
try:
|
| 257 |
+
parsed_date = pd.to_datetime(sample_date)
|
| 258 |
+
if pd.isna(parsed_date):
|
| 259 |
+
default_value = "YYYY-MM-DD HH:MM:SS" # Fallback for invalid dates
|
| 260 |
+
else:
|
| 261 |
+
default_value = parsed_date.strftime('%Y-%m-%d %H:%M:%S')
|
| 262 |
+
except Exception:
|
| 263 |
+
default_value = "YYYY-MM-DD HH:MM:SS"
|
| 264 |
+
else:
|
| 265 |
+
unique_vals_str = [str(x) for x in df[col].dropna().unique()]
|
| 266 |
+
if unique_vals_str:
|
| 267 |
+
default_value = random.choice(unique_vals_str)
|
| 268 |
+
else:
|
| 269 |
+
default_value = ""
|
| 270 |
+
|
| 271 |
+
if pd.api.types.is_numeric_dtype(df[col]):
|
| 272 |
+
form_fields.append({
|
| 273 |
+
'name': col,
|
| 274 |
+
'type': 'number',
|
| 275 |
+
'default_value': default_value
|
| 276 |
+
})
|
| 277 |
+
elif col in ['timestamp', 'maintenance_timestamp']:
|
| 278 |
+
form_fields.append({
|
| 279 |
+
'name': col,
|
| 280 |
+
'type': 'text',
|
| 281 |
+
'placeholder': 'YYYY-MM-DD HH:MM:SS (optional)',
|
| 282 |
+
'default_value': default_value
|
| 283 |
+
})
|
| 284 |
+
else:
|
| 285 |
+
unique_values = [str(x) for x in df[col].dropna().unique().tolist()]
|
| 286 |
+
form_fields.append({
|
| 287 |
+
'name': col,
|
| 288 |
+
'type': 'select',
|
| 289 |
+
'options': unique_values,
|
| 290 |
+
'default_value': default_value
|
| 291 |
+
})
|
| 292 |
+
|
| 293 |
+
return jsonify({'success': True, 'form_fields': form_fields})
|
| 294 |
+
|
| 295 |
+
except Exception as e:
|
| 296 |
+
print(f"Error in get_form_data: {e}")
|
| 297 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
@machine_failure_bp.route('/predict_single', methods=['POST'])
|
| 301 |
+
def predict_single():
|
| 302 |
+
try:
|
| 303 |
+
model_path = session.get('model_path')
|
| 304 |
+
scaler_path = session.get('scaler_path')
|
| 305 |
+
encoders_path = session.get('encoders_path')
|
| 306 |
+
feature_names = session.get('feature_names')
|
| 307 |
+
target_col = session.get('target_column_name')
|
| 308 |
+
|
| 309 |
+
if not all([model_path, scaler_path, encoders_path, feature_names, target_col]):
|
| 310 |
+
return jsonify({'success': False, 'error': 'Model or preprocessing artifacts not found. Please train a model first.'})
|
| 311 |
+
|
| 312 |
+
model = joblib.load(model_path)
|
| 313 |
+
scaler = joblib.load(scaler_path)
|
| 314 |
+
label_encoders = joblib.load(encoders_path)
|
| 315 |
+
|
| 316 |
+
input_data = request.json
|
| 317 |
+
if not input_data:
|
| 318 |
+
return jsonify({'success': False, 'error': 'No input data provided.'})
|
| 319 |
+
|
| 320 |
+
original_uploaded_columns = session.get('original_columns')
|
| 321 |
+
|
| 322 |
+
if not original_uploaded_columns:
|
| 323 |
+
return jsonify({'success': False, 'error': 'Original dataset column names not found in session. Please upload a file.'})
|
| 324 |
+
|
| 325 |
+
full_input_df = pd.DataFrame(columns=original_uploaded_columns)
|
| 326 |
+
|
| 327 |
+
single_row_input_df = pd.DataFrame([input_data])
|
| 328 |
+
|
| 329 |
+
for col in original_uploaded_columns:
|
| 330 |
+
if col in single_row_input_df.columns:
|
| 331 |
+
full_input_df.loc[0, col] = single_row_input_df.loc[0, col]
|
| 332 |
+
else:
|
| 333 |
+
full_input_df.loc[0, col] = np.nan
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
preprocessed_input_df, _ = preprocess_data(full_input_df.copy(), for_prediction=True, label_encoders=label_encoders)
|
| 337 |
+
|
| 338 |
+
final_input_features = pd.DataFrame(columns=feature_names)
|
| 339 |
+
|
| 340 |
+
for col in feature_names:
|
| 341 |
+
if col in preprocessed_input_df.columns:
|
| 342 |
+
final_input_features[col] = pd.to_numeric(preprocessed_input_df[col], errors='coerce').values
|
| 343 |
+
else:
|
| 344 |
+
final_input_features[col] = 0.0
|
| 345 |
+
|
| 346 |
+
final_input_features = final_input_features.fillna(0.0)
|
| 347 |
+
|
| 348 |
+
input_scaled = scaler.transform(final_input_features)
|
| 349 |
+
|
| 350 |
+
prediction_value = model.predict(input_scaled)[0]
|
| 351 |
+
|
| 352 |
+
prediction_display = prediction_value
|
| 353 |
+
if target_col in label_encoders:
|
| 354 |
+
if isinstance(prediction_value, (int, np.integer)) and prediction_value < len(label_encoders[target_col].classes_):
|
| 355 |
+
prediction_display = str(label_encoders[target_col].inverse_transform([prediction_value])[0])
|
| 356 |
+
else:
|
| 357 |
+
prediction_display = str(prediction_value) + " (Unknown Class)"
|
| 358 |
+
else:
|
| 359 |
+
if isinstance(prediction_value, np.number):
|
| 360 |
+
prediction_display = float(prediction_value)
|
| 361 |
+
else:
|
| 362 |
+
prediction_display = prediction_value
|
| 363 |
+
|
| 364 |
+
probability = None
|
| 365 |
+
if hasattr(model, 'predict_proba'):
|
| 366 |
+
probability = model.predict_proba(input_scaled)[0].tolist()
|
| 367 |
+
probability = [float(p) for p in probability]
|
| 368 |
+
|
| 369 |
+
return jsonify({
|
| 370 |
+
'success': True,
|
| 371 |
+
'prediction': prediction_display,
|
| 372 |
+
'probability': probability
|
| 373 |
+
})
|
| 374 |
+
except Exception as e:
|
| 375 |
+
print(f"Error in predict_single: {e}")
|
| 376 |
+
return jsonify({'success': False, 'error': str(e)})
|
modules/pricing_intelligence.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template
|
| 2 |
+
|
| 3 |
+
pricing_intelligence_bp = Blueprint('pricing_intelligence', __name__, url_prefix='/pricing')
|
| 4 |
+
|
| 5 |
+
@pricing_intelligence_bp.route('/')
|
| 6 |
+
def show_pricing_intelligence():
|
| 7 |
+
return render_template('pricing_intelligence.html', title="Pricing Intelligence")
|
modules/sales_perdiction.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.utils
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
sales_prediction_bp = Blueprint('sales_prediction', __name__, url_prefix='/forecast/sales')
|
| 9 |
+
|
| 10 |
+
_current_df = None
|
| 11 |
+
|
| 12 |
+
def get_summary_stats(df):
|
| 13 |
+
return {
|
| 14 |
+
'total_rows': len(df),
|
| 15 |
+
'total_columns': len(df.columns),
|
| 16 |
+
'columns': list(df.columns),
|
| 17 |
+
'numeric_columns': list(df.select_dtypes(include=[np.number]).columns),
|
| 18 |
+
'categorical_columns': list(df.select_dtypes(exclude=[np.number]).columns),
|
| 19 |
+
'missing_values': df.isnull().sum().to_dict()
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
@sales_prediction_bp.route('/', methods=['GET'])
|
| 23 |
+
def show_sales_prediction():
|
| 24 |
+
return render_template('sales_prediction.html', title="Sales Prediction Engine")
|
| 25 |
+
|
| 26 |
+
@sales_prediction_bp.route('/upload', methods=['POST'])
|
| 27 |
+
def upload_file():
|
| 28 |
+
if 'sales_file' not in request.files:
|
| 29 |
+
flash('No file selected')
|
| 30 |
+
return redirect(url_for('sales_prediction.show_sales_prediction'))
|
| 31 |
+
|
| 32 |
+
file = request.files['sales_file']
|
| 33 |
+
if file.filename == '':
|
| 34 |
+
flash('No file selected')
|
| 35 |
+
return redirect(url_for('sales_prediction.show_sales_prediction'))
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
global _current_df
|
| 39 |
+
_current_df = pd.read_csv(file)
|
| 40 |
+
preview_data = _current_df.head().to_dict('records')
|
| 41 |
+
summary_stats = get_summary_stats(_current_df)
|
| 42 |
+
return render_template('sales_prediction.html',
|
| 43 |
+
title="Sales Prediction Engine",
|
| 44 |
+
preview_data=preview_data,
|
| 45 |
+
columns=_current_df.columns.tolist(),
|
| 46 |
+
summary_stats=summary_stats)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
flash(f'Error processing file: {str(e)}')
|
| 49 |
+
return redirect(url_for('sales_prediction.show_sales_prediction'))
|
| 50 |
+
|
| 51 |
+
@sales_prediction_bp.route('/plot', methods=['POST'])
|
| 52 |
+
def create_plot():
|
| 53 |
+
try:
|
| 54 |
+
global _current_df
|
| 55 |
+
if _current_df is None:
|
| 56 |
+
return jsonify(success=False,
|
| 57 |
+
error="Please upload the CSV first.")
|
| 58 |
+
|
| 59 |
+
column = request.form.get('column')
|
| 60 |
+
plot_type = request.form.get('plot_type')
|
| 61 |
+
NUMERIC = {'sales_units', 'sales_revenue', 'price'}
|
| 62 |
+
CATEGORICAL = {'sku', 'category', 'region', 'channel', 'brand'}
|
| 63 |
+
|
| 64 |
+
if column not in NUMERIC.union(CATEGORICAL):
|
| 65 |
+
return jsonify(success=False,
|
| 66 |
+
error=f"Column '{column}' not supported.")
|
| 67 |
+
|
| 68 |
+
if column in NUMERIC:
|
| 69 |
+
_current_df[column] = pd.to_numeric(_current_df[column],
|
| 70 |
+
errors='coerce')
|
| 71 |
+
|
| 72 |
+
df = _current_df.dropna(subset=[column])
|
| 73 |
+
if plot_type == 'histogram' and column in NUMERIC:
|
| 74 |
+
fig = px.histogram(df, x=column, nbins=40,
|
| 75 |
+
title=f'Distribution of {column}')
|
| 76 |
+
|
| 77 |
+
elif plot_type == 'box' and column in NUMERIC:
|
| 78 |
+
fig = px.box(df, y=column,
|
| 79 |
+
title=f'Box-plot of {column}')
|
| 80 |
+
|
| 81 |
+
elif plot_type == 'bar' and column in CATEGORICAL:
|
| 82 |
+
agg_df = (df.groupby(column, as_index=False)
|
| 83 |
+
['sales_units'].sum()
|
| 84 |
+
.sort_values('sales_units', ascending=False)
|
| 85 |
+
.head(25))
|
| 86 |
+
|
| 87 |
+
fig = px.bar(agg_df, x=column, y='sales_units',
|
| 88 |
+
title=f'{column} – total units sold',
|
| 89 |
+
labels={'sales_units': 'Units sold'})
|
| 90 |
+
|
| 91 |
+
fig.update_layout(xaxis_categoryorder='total descending')
|
| 92 |
+
|
| 93 |
+
else:
|
| 94 |
+
return jsonify(success=False,
|
| 95 |
+
error=f"'{plot_type}' not allowed for '{column}'")
|
| 96 |
+
|
| 97 |
+
plot_json = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
|
| 98 |
+
return jsonify(success=True, plot=plot_json)
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return jsonify(success=False, error=str(e))
|
| 102 |
+
|
| 103 |
+
@sales_prediction_bp.route('/fix_nulls', methods=['POST'])
|
| 104 |
+
def fix_nulls():
|
| 105 |
+
try:
|
| 106 |
+
global _current_df
|
| 107 |
+
if _current_df is None:
|
| 108 |
+
return jsonify(success=False, error="No data available. Please upload a CSV file first.")
|
| 109 |
+
column = request.form.get('column')
|
| 110 |
+
method = request.form.get('method')
|
| 111 |
+
if column not in _current_df.columns:
|
| 112 |
+
return jsonify(success=False, error=f"Column '{column}' not found")
|
| 113 |
+
nulls_before = int(_current_df[column].isnull().sum())
|
| 114 |
+
if nulls_before == 0:
|
| 115 |
+
return jsonify(success=False, error=f"No nulls to fix in '{column}'")
|
| 116 |
+
if method == 'drop':
|
| 117 |
+
_current_df = _current_df.dropna(subset=[column])
|
| 118 |
+
elif method == 'mean':
|
| 119 |
+
_current_df[column] = _current_df[column].fillna(_current_df[column].mean())
|
| 120 |
+
elif method == 'median':
|
| 121 |
+
_current_df[column] = _current_df[column].fillna(_current_df[column].median())
|
| 122 |
+
elif method == 'mode':
|
| 123 |
+
_current_df[column] = _current_df[column].fillna(_current_df[column].mode()[0])
|
| 124 |
+
else:
|
| 125 |
+
return jsonify(success=False, error="Invalid method")
|
| 126 |
+
nulls_after = int(_current_df[column].isnull().sum())
|
| 127 |
+
summary_stats = get_summary_stats(_current_df)
|
| 128 |
+
preview_data = _current_df.head().to_dict('records')
|
| 129 |
+
columns = list(_current_df.columns)
|
| 130 |
+
return jsonify(
|
| 131 |
+
success=True,
|
| 132 |
+
message=f"Fixed {nulls_before - nulls_after} nulls in '{column}' using {method}.",
|
| 133 |
+
summary_stats=summary_stats,
|
| 134 |
+
preview_data=preview_data,
|
| 135 |
+
columns=columns
|
| 136 |
+
)
|
| 137 |
+
except Exception as e:
|
| 138 |
+
return jsonify(success=False, error=str(e))
|
| 139 |
+
|
| 140 |
+
@sales_prediction_bp.route('/run_forecast', methods=['POST'])
|
| 141 |
+
def run_forecast():
|
| 142 |
+
try:
|
| 143 |
+
global _current_df
|
| 144 |
+
if _current_df is None:
|
| 145 |
+
return jsonify(success=False, error="No data available. Please upload a CSV file first.")
|
| 146 |
+
model = request.form.get('model')
|
| 147 |
+
date_col = request.form.get('date_col')
|
| 148 |
+
target_col = request.form.get('target_col')
|
| 149 |
+
horizon = int(request.form.get('horizon', 30))
|
| 150 |
+
|
| 151 |
+
if date_col not in _current_df.columns or target_col not in _current_df.columns:
|
| 152 |
+
return jsonify(success=False, error="Invalid column selection.")
|
| 153 |
+
|
| 154 |
+
df = _current_df.copy()
|
| 155 |
+
df = df[[date_col, target_col]].dropna()
|
| 156 |
+
# Try to parse date with dayfirst and fallback to default
|
| 157 |
+
try:
|
| 158 |
+
df[date_col] = pd.to_datetime(df[date_col], dayfirst=True, errors='coerce')
|
| 159 |
+
except Exception:
|
| 160 |
+
df[date_col] = pd.to_datetime(df[date_col], errors='coerce')
|
| 161 |
+
df = df.dropna(subset=[date_col])
|
| 162 |
+
df = df.sort_values(date_col)
|
| 163 |
+
df = df.groupby(date_col)[target_col].sum().reset_index()
|
| 164 |
+
|
| 165 |
+
# Only forecast if enough data
|
| 166 |
+
if len(df) < horizon + 5:
|
| 167 |
+
return jsonify(success=False, error="Not enough data for the selected forecast horizon.")
|
| 168 |
+
|
| 169 |
+
train = df.iloc[:-horizon]
|
| 170 |
+
test = df.iloc[-horizon:]
|
| 171 |
+
|
| 172 |
+
forecast = []
|
| 173 |
+
conf_int = []
|
| 174 |
+
trend = []
|
| 175 |
+
seasonality = []
|
| 176 |
+
metrics = {}
|
| 177 |
+
|
| 178 |
+
if model == "ARIMA":
|
| 179 |
+
try:
|
| 180 |
+
from statsmodels.tsa.arima.model import ARIMA
|
| 181 |
+
ts = train.set_index(date_col)[target_col]
|
| 182 |
+
arima = ARIMA(ts, order=(1,1,1)).fit()
|
| 183 |
+
pred = arima.get_forecast(steps=horizon)
|
| 184 |
+
forecast = pred.predicted_mean.values.tolist()
|
| 185 |
+
conf_int = pred.conf_int().values.tolist()
|
| 186 |
+
trend = [np.mean(ts)] * horizon
|
| 187 |
+
seasonality = [0] * horizon
|
| 188 |
+
y_true = test[target_col].values
|
| 189 |
+
y_pred = forecast
|
| 190 |
+
metrics = {
|
| 191 |
+
"MAPE": float(np.mean(np.abs((y_true - y_pred) / y_true)) * 100),
|
| 192 |
+
"RMSE": float(np.sqrt(np.mean((y_true - y_pred) ** 2))),
|
| 193 |
+
"R2": float(1 - np.sum((y_true - y_pred) ** 2) / np.sum((y_true - np.mean(y_true)) ** 2))
|
| 194 |
+
}
|
| 195 |
+
except Exception as e:
|
| 196 |
+
return jsonify(success=False, error=f"ARIMA error: {e}")
|
| 197 |
+
|
| 198 |
+
elif model == "Prophet":
|
| 199 |
+
try:
|
| 200 |
+
from prophet import Prophet
|
| 201 |
+
prophet_df = train.rename(columns={date_col: "ds", target_col: "y"})
|
| 202 |
+
m = Prophet()
|
| 203 |
+
m.fit(prophet_df)
|
| 204 |
+
future = m.make_future_dataframe(periods=horizon)
|
| 205 |
+
forecast_df = m.predict(future)
|
| 206 |
+
forecast = forecast_df['yhat'][-horizon:].values.tolist()
|
| 207 |
+
conf_int = list(zip(forecast_df['yhat_lower'][-horizon:].values, forecast_df['yhat_upper'][-horizon:].values))
|
| 208 |
+
trend = forecast_df['trend'][-horizon:].values.tolist()
|
| 209 |
+
seasonality = (forecast_df['seasonal'][-horizon:].values.tolist() if 'seasonal' in forecast_df else [0]*horizon)
|
| 210 |
+
y_true = test[target_col].values
|
| 211 |
+
y_pred = forecast
|
| 212 |
+
metrics = {
|
| 213 |
+
"MAPE": float(np.mean(np.abs((y_true - y_pred) / y_true)) * 100),
|
| 214 |
+
"RMSE": float(np.sqrt(np.mean((y_true - y_pred) ** 2))),
|
| 215 |
+
"R2": float(1 - np.sum((y_true - y_pred) ** 2) / np.sum((y_true - np.mean(y_true)) ** 2))
|
| 216 |
+
}
|
| 217 |
+
except Exception as e:
|
| 218 |
+
return jsonify(success=False, error=f"Prophet error: {e}")
|
| 219 |
+
|
| 220 |
+
elif model in ["Random Forest", "XGBoost", "LSTM"]:
|
| 221 |
+
y_true = test[target_col].values
|
| 222 |
+
y_pred = [np.mean(train[target_col])] * horizon
|
| 223 |
+
forecast = y_pred
|
| 224 |
+
conf_int = [[y-5, y+5] for y in y_pred]
|
| 225 |
+
trend = [np.mean(train[target_col])] * horizon
|
| 226 |
+
seasonality = [0] * horizon
|
| 227 |
+
metrics = {
|
| 228 |
+
"MAPE": float(np.mean(np.abs((y_true - y_pred) / y_true)) * 100),
|
| 229 |
+
"RMSE": float(np.sqrt(np.mean((y_true - y_pred) ** 2))),
|
| 230 |
+
"R2": float(1 - np.sum((y_true - y_pred) ** 2) / np.sum((y_true - np.mean(y_true)) ** 2))
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
forecast_dates = test[date_col].dt.strftime('%Y-%m-%d').tolist()
|
| 234 |
+
|
| 235 |
+
return jsonify(
|
| 236 |
+
success=True,
|
| 237 |
+
forecast=forecast,
|
| 238 |
+
conf_int=conf_int,
|
| 239 |
+
trend=trend,
|
| 240 |
+
seasonality=seasonality,
|
| 241 |
+
metrics=metrics,
|
| 242 |
+
dates=forecast_dates,
|
| 243 |
+
model=model
|
| 244 |
+
)
|
| 245 |
+
except Exception as e:
|
| 246 |
+
return jsonify(success=False, error=str(e))
|
modules/supply_failure.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import plotly.utils
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import joblib
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from sklearn.model_selection import train_test_split
|
| 11 |
+
from sklearn.preprocessing import StandardScaler, LabelEncoder
|
| 12 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 13 |
+
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
|
| 14 |
+
import random
|
| 15 |
+
|
| 16 |
+
supply_failure_bp = Blueprint('supply_failure', __name__, url_prefix='/predict/supply_failure')
|
| 17 |
+
|
| 18 |
+
UPLOAD_FOLDER = 'temp_uploads'
|
| 19 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 20 |
+
|
| 21 |
+
def get_current_df_supply():
|
| 22 |
+
try:
|
| 23 |
+
csv_path = session.get('supply_csv_path')
|
| 24 |
+
if csv_path and os.path.exists(csv_path):
|
| 25 |
+
return pd.read_csv(csv_path)
|
| 26 |
+
return None
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"Error in get_current_df_supply: {str(e)}")
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
def get_summary_stats_supply(df):
|
| 32 |
+
return {
|
| 33 |
+
'total_rows': len(df),
|
| 34 |
+
'total_columns': len(df.columns),
|
| 35 |
+
'columns': list(df.columns),
|
| 36 |
+
'numeric_columns': list(df.select_dtypes(include=[np.number]).columns),
|
| 37 |
+
'categorical_columns': list(df.select_dtypes(exclude=[np.number]).columns),
|
| 38 |
+
'missing_values': df.isnull().sum().to_dict()
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
def preprocess_data_supply(df, for_prediction=False, label_encoders=None):
|
| 42 |
+
df_processed = df.copy()
|
| 43 |
+
|
| 44 |
+
# Identify date columns based on known names
|
| 45 |
+
date_cols = ['order_date', 'promised_delivery_date', 'actual_delivery_date']
|
| 46 |
+
|
| 47 |
+
# Process date columns: extract features and drop original
|
| 48 |
+
for col in date_cols:
|
| 49 |
+
if col in df_processed.columns:
|
| 50 |
+
# Convert to datetime, coercing errors to NaT
|
| 51 |
+
df_processed[col] = pd.to_datetime(df_processed[col], errors='coerce')
|
| 52 |
+
|
| 53 |
+
# Extract features only if there are valid datetime values
|
| 54 |
+
if not df_processed[col].isnull().all():
|
| 55 |
+
df_processed[f'{col}_day_of_week'] = df_processed[col].dt.dayofweek.fillna(-1) # -1 for NaN dates
|
| 56 |
+
df_processed[f'{col}_month'] = df_processed[col].dt.month.fillna(-1)
|
| 57 |
+
df_processed[f'{col}_year'] = df_processed[col].dt.year.fillna(-1)
|
| 58 |
+
df_processed[f'{col}_day'] = df_processed[col].dt.day.fillna(-1)
|
| 59 |
+
else: # If all dates are NaT, add dummy columns filled with -1
|
| 60 |
+
df_processed[f'{col}_day_of_week'] = -1
|
| 61 |
+
df_processed[f'{col}_month'] = -1
|
| 62 |
+
df_processed[f'{col}_year'] = -1
|
| 63 |
+
df_processed[f'{col}_day'] = -1
|
| 64 |
+
df_processed = df_processed.drop(columns=[col])
|
| 65 |
+
|
| 66 |
+
# Identify numerical and categorical columns after date processing
|
| 67 |
+
categorical_columns = []
|
| 68 |
+
numerical_columns = []
|
| 69 |
+
|
| 70 |
+
for column in df_processed.columns:
|
| 71 |
+
if pd.api.types.is_numeric_dtype(df_processed[column]):
|
| 72 |
+
numerical_columns.append(column)
|
| 73 |
+
else:
|
| 74 |
+
try:
|
| 75 |
+
# Attempt to convert to numeric, if successful, it's numeric
|
| 76 |
+
if pd.to_numeric(df_processed[column].dropna()).notna().all():
|
| 77 |
+
numerical_columns.append(column)
|
| 78 |
+
else:
|
| 79 |
+
categorical_columns.append(column)
|
| 80 |
+
except ValueError:
|
| 81 |
+
categorical_columns.append(column)
|
| 82 |
+
|
| 83 |
+
# Encode categorical variables
|
| 84 |
+
current_label_encoders = {}
|
| 85 |
+
if not for_prediction: # During training, fit and save encoders
|
| 86 |
+
for col in categorical_columns:
|
| 87 |
+
if col in df_processed.columns:
|
| 88 |
+
le = LabelEncoder()
|
| 89 |
+
df_processed[col] = le.fit_transform(df_processed[col].astype(str).fillna('missing_value'))
|
| 90 |
+
current_label_encoders[col] = le
|
| 91 |
+
else: # For prediction, use provided encoders
|
| 92 |
+
for col, le in label_encoders.items():
|
| 93 |
+
if col in df_processed.columns:
|
| 94 |
+
df_processed[col] = df_processed[col].astype(str).apply(
|
| 95 |
+
lambda x: le.transform([x])[0] if x in le.classes_ else -1
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# Ensure numerical columns are truly numeric and fill any NaNs
|
| 99 |
+
for col in numerical_columns:
|
| 100 |
+
if col in df_processed.columns:
|
| 101 |
+
df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce').fillna(0) # Fill numerical NaNs with 0
|
| 102 |
+
|
| 103 |
+
return df_processed, current_label_encoders
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@supply_failure_bp.route('/', methods=['GET'])
|
| 107 |
+
def show_supply_failure():
|
| 108 |
+
return render_template('supply_failure.html', title="Supply Failure Prediction")
|
| 109 |
+
|
| 110 |
+
@supply_failure_bp.route('/upload', methods=['POST'])
|
| 111 |
+
def upload_file_supply():
|
| 112 |
+
if 'supply_file' not in request.files:
|
| 113 |
+
flash('No file selected')
|
| 114 |
+
return redirect(url_for('supply_failure.show_supply_failure'))
|
| 115 |
+
|
| 116 |
+
file = request.files['supply_file']
|
| 117 |
+
if file.filename == '':
|
| 118 |
+
flash('No file selected')
|
| 119 |
+
return redirect(url_for('supply_failure.show_supply_failure'))
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 123 |
+
safe_filename = f"supply_data_{timestamp}.csv"
|
| 124 |
+
file_path = os.path.join(UPLOAD_FOLDER, safe_filename)
|
| 125 |
+
|
| 126 |
+
file.save(file_path)
|
| 127 |
+
session['supply_csv_path'] = file_path
|
| 128 |
+
|
| 129 |
+
df = pd.read_csv(file_path)
|
| 130 |
+
preview_data = df.head().to_dict('records')
|
| 131 |
+
summary_stats = get_summary_stats_supply(df)
|
| 132 |
+
session['original_columns_supply'] = df.columns.tolist()
|
| 133 |
+
|
| 134 |
+
return render_template('supply_failure.html',
|
| 135 |
+
title="Supply Failure Prediction",
|
| 136 |
+
preview_data=preview_data,
|
| 137 |
+
columns=df.columns.tolist(),
|
| 138 |
+
summary_stats=summary_stats)
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
flash(f'Error processing file: {str(e)}')
|
| 142 |
+
return redirect(url_for('supply_failure.show_supply_failure'))
|
| 143 |
+
|
| 144 |
+
@supply_failure_bp.route('/run_prediction', methods=['POST'])
|
| 145 |
+
def run_prediction_supply():
|
| 146 |
+
try:
|
| 147 |
+
df = get_current_df_supply()
|
| 148 |
+
if df is None:
|
| 149 |
+
return jsonify({'success': False, 'error': 'No data available. Please upload a CSV file first.'})
|
| 150 |
+
|
| 151 |
+
target_col = 'failure_flag' # Fixed target column as per definition
|
| 152 |
+
|
| 153 |
+
df_processed, label_encoders = preprocess_data_supply(df.copy(), for_prediction=False)
|
| 154 |
+
|
| 155 |
+
encoders_path = os.path.join(UPLOAD_FOLDER, f'supply_encoders_{datetime.now().strftime("%Y%m%d_%H%M%S")}.joblib')
|
| 156 |
+
joblib.dump(label_encoders, encoders_path)
|
| 157 |
+
session['supply_encoders_path'] = encoders_path
|
| 158 |
+
|
| 159 |
+
if target_col not in df_processed.columns:
|
| 160 |
+
return jsonify({'success': False, 'error': f"Target column '{target_col}' not found after preprocessing. Check if it was dropped or transformed incorrectly."})
|
| 161 |
+
|
| 162 |
+
X = df_processed.drop(columns=[target_col])
|
| 163 |
+
y = df_processed[target_col]
|
| 164 |
+
|
| 165 |
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 166 |
+
scaler = StandardScaler()
|
| 167 |
+
X_train_scaled = scaler.fit_transform(X_train)
|
| 168 |
+
X_test_scaled = scaler.transform(X_test)
|
| 169 |
+
|
| 170 |
+
clf = RandomForestClassifier(random_state=42)
|
| 171 |
+
clf.fit(X_train_scaled, y_train)
|
| 172 |
+
y_pred = clf.predict(X_test_scaled)
|
| 173 |
+
|
| 174 |
+
importances = clf.feature_importances_
|
| 175 |
+
feature_names = X.columns
|
| 176 |
+
feature_importance = sorted(
|
| 177 |
+
zip(feature_names, importances),
|
| 178 |
+
key=lambda x: x[1],
|
| 179 |
+
reverse=True
|
| 180 |
+
)[:5]
|
| 181 |
+
|
| 182 |
+
top_features = [{'feature': f, 'importance': float(imp)} for f, imp in feature_importance]
|
| 183 |
+
|
| 184 |
+
session['supply_feature_names'] = X.columns.tolist()
|
| 185 |
+
session['supply_target_column_name'] = target_col
|
| 186 |
+
|
| 187 |
+
metrics = {
|
| 188 |
+
'Accuracy': accuracy_score(y_test, y_pred),
|
| 189 |
+
'Precision': precision_score(y_test, y_pred, average='weighted', zero_division=0),
|
| 190 |
+
'Recall': recall_score(y_test, y_pred, average='weighted', zero_division=0),
|
| 191 |
+
'F1 Score': f1_score(y_test, y_pred, average='weighted', zero_division=0)
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
model_path = os.path.join(UPLOAD_FOLDER, f'supply_model_{datetime.now().strftime("%Y%m%d_%H%M%S")}.joblib')
|
| 195 |
+
scaler_path = os.path.join(UPLOAD_FOLDER, f'supply_scaler_{datetime.now().strftime("%Y%m%d_%H%M%S")}.joblib')
|
| 196 |
+
|
| 197 |
+
joblib.dump(clf, model_path)
|
| 198 |
+
joblib.dump(scaler, scaler_path)
|
| 199 |
+
|
| 200 |
+
session['supply_model_path'] = model_path
|
| 201 |
+
session['supply_scaler_path'] = scaler_path
|
| 202 |
+
|
| 203 |
+
return jsonify({
|
| 204 |
+
'success': True,
|
| 205 |
+
'metrics': metrics,
|
| 206 |
+
'top_features': top_features,
|
| 207 |
+
})
|
| 208 |
+
|
| 209 |
+
except Exception as e:
|
| 210 |
+
print(f"Error in run_prediction_supply: {e}")
|
| 211 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 212 |
+
|
| 213 |
+
@supply_failure_bp.route('/get_form_data', methods=['GET'])
|
| 214 |
+
def get_form_data_supply():
|
| 215 |
+
try:
|
| 216 |
+
df = get_current_df_supply()
|
| 217 |
+
if df is None:
|
| 218 |
+
return jsonify({'success': False, 'error': 'No data available. Please upload a file first.'})
|
| 219 |
+
|
| 220 |
+
target_col = 'failure_flag' # Fixed target column for supply chain
|
| 221 |
+
if not target_col: # Should not happen if fixed, but good for robustness
|
| 222 |
+
return jsonify({'success': False, 'error': 'Target column not found in session. Please run prediction first.'})
|
| 223 |
+
|
| 224 |
+
# Columns to exclude from the form as requested by the user
|
| 225 |
+
exclude_cols = ['delivery_delay_days', 'delivered_quantity', 'return_reason', 'delivery_status', 'failure_type', target_col, 'order_id', 'component_id', 'po_approval_delay_days', 'customs_clearance_days', 'actual_delivery_date']
|
| 226 |
+
|
| 227 |
+
form_fields = []
|
| 228 |
+
for col in df.columns:
|
| 229 |
+
if col.lower() in [ec.lower() for ec in exclude_cols]:
|
| 230 |
+
continue
|
| 231 |
+
|
| 232 |
+
default_value = None
|
| 233 |
+
if not df[col].dropna().empty:
|
| 234 |
+
if pd.api.types.is_numeric_dtype(df[col]):
|
| 235 |
+
min_val = df[col].min()
|
| 236 |
+
max_val = df[col].max()
|
| 237 |
+
if pd.isna(min_val) or pd.isna(max_val):
|
| 238 |
+
default_value = 0.0
|
| 239 |
+
else:
|
| 240 |
+
default_value = round(random.uniform(float(min_val), float(max_val)), 2)
|
| 241 |
+
elif col in ['order_date', 'promised_delivery_date', 'actual_delivery_date']:
|
| 242 |
+
sample_date = random.choice(df[col].dropna().tolist())
|
| 243 |
+
try:
|
| 244 |
+
parsed_date = pd.to_datetime(sample_date)
|
| 245 |
+
if pd.isna(parsed_date):
|
| 246 |
+
default_value = "YYYY-MM-DD HH:MM:SS"
|
| 247 |
+
else:
|
| 248 |
+
default_value = parsed_date.strftime('%Y-%m-%d %H:%M:%S')
|
| 249 |
+
except Exception:
|
| 250 |
+
default_value = "YYYY-MM-DD HH:MM:SS"
|
| 251 |
+
else: # Categorical or other types
|
| 252 |
+
unique_vals_str = [str(x) for x in df[col].dropna().unique()]
|
| 253 |
+
if unique_vals_str:
|
| 254 |
+
default_value = random.choice(unique_vals_str)
|
| 255 |
+
else:
|
| 256 |
+
default_value = ""
|
| 257 |
+
|
| 258 |
+
if pd.api.types.is_numeric_dtype(df[col]):
|
| 259 |
+
form_fields.append({
|
| 260 |
+
'name': col,
|
| 261 |
+
'type': 'number',
|
| 262 |
+
'default_value': default_value
|
| 263 |
+
})
|
| 264 |
+
elif col in ['order_date', 'promised_delivery_date', 'actual_delivery_date']:
|
| 265 |
+
form_fields.append({
|
| 266 |
+
'name': col,
|
| 267 |
+
'type': 'text',
|
| 268 |
+
'placeholder': 'YYYY-MM-DD HH:MM:SS',
|
| 269 |
+
'default_value': default_value
|
| 270 |
+
})
|
| 271 |
+
else: # Categorical
|
| 272 |
+
unique_values = [str(x) for x in df[col].dropna().unique().tolist()]
|
| 273 |
+
form_fields.append({
|
| 274 |
+
'name': col,
|
| 275 |
+
'type': 'select',
|
| 276 |
+
'options': unique_values,
|
| 277 |
+
'default_value': default_value
|
| 278 |
+
})
|
| 279 |
+
|
| 280 |
+
return jsonify({'success': True, 'form_fields': form_fields})
|
| 281 |
+
|
| 282 |
+
except Exception as e:
|
| 283 |
+
print(f"Error in get_form_data_supply: {e}")
|
| 284 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
@supply_failure_bp.route('/predict_single', methods=['POST'])
|
| 288 |
+
def predict_single_supply():
|
| 289 |
+
try:
|
| 290 |
+
model_path = session.get('supply_model_path')
|
| 291 |
+
scaler_path = session.get('supply_scaler_path')
|
| 292 |
+
encoders_path = session.get('supply_encoders_path')
|
| 293 |
+
feature_names = session.get('supply_feature_names')
|
| 294 |
+
target_col = session.get('supply_target_column_name')
|
| 295 |
+
original_uploaded_columns = session.get('original_columns_supply')
|
| 296 |
+
|
| 297 |
+
if not all([model_path, scaler_path, encoders_path, feature_names, target_col, original_uploaded_columns]):
|
| 298 |
+
return jsonify({'success': False, 'error': 'Model or preprocessing artifacts not found for supply chain. Please train a model first.'})
|
| 299 |
+
|
| 300 |
+
model = joblib.load(model_path)
|
| 301 |
+
scaler = joblib.load(scaler_path)
|
| 302 |
+
label_encoders = joblib.load(encoders_path)
|
| 303 |
+
|
| 304 |
+
input_data = request.json
|
| 305 |
+
if not input_data:
|
| 306 |
+
return jsonify({'success': False, 'error': 'No input data provided.'})
|
| 307 |
+
|
| 308 |
+
full_input_df = pd.DataFrame(columns=original_uploaded_columns)
|
| 309 |
+
single_row_input_df = pd.DataFrame([input_data])
|
| 310 |
+
|
| 311 |
+
for col in original_uploaded_columns:
|
| 312 |
+
if col in single_row_input_df.columns:
|
| 313 |
+
full_input_df.loc[0, col] = single_row_input_df.loc[0, col]
|
| 314 |
+
else:
|
| 315 |
+
full_input_df.loc[0, col] = np.nan
|
| 316 |
+
|
| 317 |
+
preprocessed_input_df, _ = preprocess_data_supply(full_input_df.copy(), for_prediction=True, label_encoders=label_encoders)
|
| 318 |
+
|
| 319 |
+
final_input_features = pd.DataFrame(columns=feature_names)
|
| 320 |
+
|
| 321 |
+
for col in feature_names:
|
| 322 |
+
if col in preprocessed_input_df.columns:
|
| 323 |
+
final_input_features[col] = pd.to_numeric(preprocessed_input_df[col], errors='coerce').values
|
| 324 |
+
else:
|
| 325 |
+
final_input_features[col] = 0.0
|
| 326 |
+
|
| 327 |
+
final_input_features = final_input_features.fillna(0.0)
|
| 328 |
+
|
| 329 |
+
input_scaled = scaler.transform(final_input_features)
|
| 330 |
+
|
| 331 |
+
prediction_value = model.predict(input_scaled)[0]
|
| 332 |
+
|
| 333 |
+
# Convert prediction_value to standard Python int/float/str
|
| 334 |
+
prediction_display = prediction_value
|
| 335 |
+
if target_col in label_encoders and prediction_value in label_encoders[target_col].classes_: # Check if target was encoded and value is in classes
|
| 336 |
+
prediction_display = str(label_encoders[target_col].inverse_transform([prediction_value])[0])
|
| 337 |
+
else:
|
| 338 |
+
if isinstance(prediction_value, np.number):
|
| 339 |
+
prediction_display = float(prediction_value)
|
| 340 |
+
else:
|
| 341 |
+
prediction_display = prediction_value # Keep as is if not np.number
|
| 342 |
+
|
| 343 |
+
# Convert 0/1 to "No Failure"/"Failure" based on the definition for failure_flag
|
| 344 |
+
if prediction_display == 0 or prediction_display == "0":
|
| 345 |
+
user_friendly_prediction = "Delivery Successful"
|
| 346 |
+
elif prediction_display == 1 or prediction_display == "1":
|
| 347 |
+
user_friendly_prediction = "Delivery Failed"
|
| 348 |
+
else:
|
| 349 |
+
user_friendly_prediction = str(prediction_display) # Fallback if target is something else
|
| 350 |
+
|
| 351 |
+
probability = None
|
| 352 |
+
if hasattr(model, 'predict_proba'):
|
| 353 |
+
probability = model.predict_proba(input_scaled)[0].tolist()
|
| 354 |
+
probability = [float(p) for p in probability]
|
| 355 |
+
|
| 356 |
+
return jsonify({
|
| 357 |
+
'success': True,
|
| 358 |
+
'prediction': user_friendly_prediction,
|
| 359 |
+
'probability': probability
|
| 360 |
+
})
|
| 361 |
+
except Exception as e:
|
| 362 |
+
print(f"Error in predict_single_supply: {e}")
|
| 363 |
+
return jsonify({'success': False, 'error': str(e)})
|
modules/underperformance.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Blueprint, render_template
|
| 2 |
+
|
| 3 |
+
underperformance_bp = Blueprint('underperformance', __name__, url_prefix='/underperformance')
|
| 4 |
+
|
| 5 |
+
@underperformance_bp.route('/')
|
| 6 |
+
def show_underperformance():
|
| 7 |
+
return render_template('underperformance.html', title="Underperformance Detector")
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
jinja2
|
| 3 |
+
scikit-learn
|
| 4 |
+
xgboost
|
| 5 |
+
pandas
|
| 6 |
+
numpy
|
| 7 |
+
flask-session
|
| 8 |
+
flask-cors
|
| 9 |
+
plotly
|
| 10 |
+
matplotlib
|
| 11 |
+
huggingface_hub[CLI]
|
static/images/campaign.jpg
ADDED
|
static/images/cover.jpeg
ADDED
|
static/images/inventory.png
ADDED
|
Git LFS Details
|
static/images/machine.jpeg
ADDED
|
static/images/pricing.jpg
ADDED
|
static/images/sales prediction.png
ADDED
|
Git LFS Details
|
static/images/supply.jpeg
ADDED
|
static/images/underperforming.png
ADDED
|
Git LFS Details
|
static/index.js
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 2 |
+
console.log('Landing page loaded.');
|
| 3 |
+
|
| 4 |
+
// Animate elements on scroll using Intersection Observer
|
| 5 |
+
const animatedElements = document.querySelectorAll('.animate-on-scroll');
|
| 6 |
+
const observer = new IntersectionObserver(entries => {
|
| 7 |
+
entries.forEach(entry => {
|
| 8 |
+
if (entry.isIntersecting) {
|
| 9 |
+
entry.target.classList.add('animate-fadeIn');
|
| 10 |
+
observer.unobserve(entry.target);
|
| 11 |
+
}
|
| 12 |
+
});
|
| 13 |
+
}, { threshold: 0.2 });
|
| 14 |
+
|
| 15 |
+
animatedElements.forEach(el => observer.observe(el));
|
| 16 |
+
|
| 17 |
+
// Plotly initial plot (if present)
|
| 18 |
+
const initialPlotDiv = document.getElementById('initialPlot');
|
| 19 |
+
if (initialPlotDiv) {
|
| 20 |
+
const plotData = JSON.parse(initialPlotDiv.dataset.plot);
|
| 21 |
+
Plotly.newPlot('plotDiv', plotData.data, plotData.layout);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Initialize toast notifications
|
| 25 |
+
if (!document.getElementById('toast-container')) {
|
| 26 |
+
const toastContainer = document.createElement('div');
|
| 27 |
+
toastContainer.id = 'toast-container';
|
| 28 |
+
toastContainer.className = 'fixed top-4 right-4 z-50';
|
| 29 |
+
document.body.appendChild(toastContainer);
|
| 30 |
+
}
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
function showToast(message, type = 'success') {
|
| 34 |
+
const toast = document.createElement('div');
|
| 35 |
+
toast.className = `p-4 mb-4 rounded shadow-lg ${type === 'success' ? 'bg-green-500' : 'bg-red-500'} text-white`;
|
| 36 |
+
toast.textContent = message;
|
| 37 |
+
|
| 38 |
+
const container = document.getElementById('toast-container');
|
| 39 |
+
container.appendChild(toast);
|
| 40 |
+
|
| 41 |
+
setTimeout(() => {
|
| 42 |
+
toast.remove();
|
| 43 |
+
}, 3000);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
window.createPlot = function() {
|
| 47 |
+
const column = document.getElementById('plotColumn').value;
|
| 48 |
+
const plotType = document.getElementById('plotType').value;
|
| 49 |
+
|
| 50 |
+
fetch('/forecast/sales/plot', {
|
| 51 |
+
method: 'POST',
|
| 52 |
+
headers: {
|
| 53 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
| 54 |
+
},
|
| 55 |
+
body: `column=${column}&plot_type=${plotType}`
|
| 56 |
+
})
|
| 57 |
+
.then(response => response.json())
|
| 58 |
+
.then(data => {
|
| 59 |
+
if (data.success) {
|
| 60 |
+
const plotData = JSON.parse(data.plot);
|
| 61 |
+
Plotly.newPlot('plotDiv', plotData.data, plotData.layout);
|
| 62 |
+
showToast(`Generated ${plotType} for ${column}`);
|
| 63 |
+
} else {
|
| 64 |
+
showToast(data.error, 'error');
|
| 65 |
+
}
|
| 66 |
+
})
|
| 67 |
+
.catch(error => {
|
| 68 |
+
console.error('Error:', error);
|
| 69 |
+
showToast(error.message, 'error');
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
window.fixNulls = function(column) {
|
| 74 |
+
const method = document.getElementById('method-' + column).value;
|
| 75 |
+
fetch('/forecast/sales/fix_nulls', {
|
| 76 |
+
method: 'POST',
|
| 77 |
+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
| 78 |
+
body: `column=${column}&method=${method}`
|
| 79 |
+
})
|
| 80 |
+
.then(response => response.json())
|
| 81 |
+
.then(data => {
|
| 82 |
+
const msgDiv = document.getElementById('null-fix-message');
|
| 83 |
+
if (data.success) {
|
| 84 |
+
msgDiv.textContent = data.message;
|
| 85 |
+
msgDiv.classList.remove('text-red-700');
|
| 86 |
+
msgDiv.classList.add('text-green-700');
|
| 87 |
+
showToast(data.message);
|
| 88 |
+
|
| 89 |
+
// Update summary stats
|
| 90 |
+
updateSummaryStats(data.summary_stats);
|
| 91 |
+
|
| 92 |
+
// Update preview table
|
| 93 |
+
updatePreviewTable(data.columns, data.preview_data);
|
| 94 |
+
|
| 95 |
+
// Update nulls UI
|
| 96 |
+
updateNullsUI(data.summary_stats.missing_values);
|
| 97 |
+
|
| 98 |
+
} else {
|
| 99 |
+
msgDiv.textContent = data.error;
|
| 100 |
+
msgDiv.classList.remove('text-green-700');
|
| 101 |
+
msgDiv.classList.add('text-red-700');
|
| 102 |
+
showToast(data.error, 'error');
|
| 103 |
+
}
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// Helper to update summary stats
|
| 108 |
+
function updateSummaryStats(stats) {
|
| 109 |
+
document.querySelectorAll('.summary-total-rows').forEach(el => el.textContent = stats.total_rows);
|
| 110 |
+
document.querySelectorAll('.summary-total-cols').forEach(el => el.textContent = stats.total_columns);
|
| 111 |
+
document.querySelectorAll('.summary-numeric-cols').forEach(el => el.textContent = stats.numeric_columns.join(', '));
|
| 112 |
+
document.querySelectorAll('.summary-categorical-cols').forEach(el => el.textContent = stats.categorical_columns.join(', '));
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Helper to update preview table
|
| 116 |
+
function updatePreviewTable(columns, previewData) {
|
| 117 |
+
const table = document.getElementById('preview-table');
|
| 118 |
+
if (!table) return;
|
| 119 |
+
// Update header
|
| 120 |
+
let thead = table.querySelector('thead');
|
| 121 |
+
let tbody = table.querySelector('tbody');
|
| 122 |
+
thead.innerHTML = '<tr>' + columns.map(col => `<th class="px-4 py-2 bg-gray-100">${col}</th>`).join('') + '</tr>';
|
| 123 |
+
// Update body
|
| 124 |
+
tbody.innerHTML = previewData.map(row =>
|
| 125 |
+
'<tr>' + columns.map(col => `<td class="border px-4 py-2">${row[col]}</td>`).join('') + '</tr>'
|
| 126 |
+
).join('');
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Helper to update nulls UI
|
| 130 |
+
function updateNullsUI(missingValues) {
|
| 131 |
+
const nullList = document.getElementById('null-list');
|
| 132 |
+
if (!nullList) return;
|
| 133 |
+
let html = '';
|
| 134 |
+
let hasNulls = false;
|
| 135 |
+
for (const [col, count] of Object.entries(missingValues)) {
|
| 136 |
+
if (count > 0) {
|
| 137 |
+
hasNulls = true;
|
| 138 |
+
html += `
|
| 139 |
+
<li>
|
| 140 |
+
<span>${col}: ${count} missing values</span>
|
| 141 |
+
<select id="method-${col}" class="border rounded p-1 mx-2">
|
| 142 |
+
<option value="drop">Drop Rows</option>
|
| 143 |
+
<option value="mean">Fill Mean</option>
|
| 144 |
+
<option value="median">Fill Median</option>
|
| 145 |
+
<option value="mode">Fill Mode</option>
|
| 146 |
+
</select>
|
| 147 |
+
<button class="btn-learn-more" onclick="fixNulls('${col}')">Fix</button>
|
| 148 |
+
</li>
|
| 149 |
+
`;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
nullList.innerHTML = html;
|
| 153 |
+
// If no nulls left, show a message
|
| 154 |
+
if (!hasNulls) {
|
| 155 |
+
nullList.innerHTML = '<li class="text-green-700 font-semibold">No missing values remaining!</li>';
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
window.runForecast = function() {
|
| 159 |
+
const dateCol = document.getElementById('forecastDateCol').value;
|
| 160 |
+
const targetCol = document.getElementById('forecastTargetCol').value;
|
| 161 |
+
const model = document.getElementById('forecastModel').value;
|
| 162 |
+
const horizon = document.getElementById('forecastHorizon').value;
|
| 163 |
+
const metricsDiv = document.getElementById('forecast-metrics');
|
| 164 |
+
const plotDiv = document.getElementById('forecast-plot');
|
| 165 |
+
const errorDiv = document.getElementById('forecast-error');
|
| 166 |
+
metricsDiv.innerHTML = '';
|
| 167 |
+
plotDiv.innerHTML = '';
|
| 168 |
+
errorDiv.textContent = '';
|
| 169 |
+
|
| 170 |
+
fetch('/forecast/sales/run_forecast', {
|
| 171 |
+
method: 'POST',
|
| 172 |
+
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
| 173 |
+
body: `date_col=${dateCol}&target_col=${targetCol}&model=${model}&horizon=${horizon}`
|
| 174 |
+
})
|
| 175 |
+
.then(response => response.json())
|
| 176 |
+
.then(data => {
|
| 177 |
+
if (data.success) {
|
| 178 |
+
// Show metrics
|
| 179 |
+
metricsDiv.innerHTML = `
|
| 180 |
+
<div class="mb-2">
|
| 181 |
+
<strong>Model:</strong> ${data.model}
|
| 182 |
+
</div>
|
| 183 |
+
<div class="mb-2">
|
| 184 |
+
<strong>MAPE:</strong> ${data.metrics.MAPE.toFixed(2)}%
|
| 185 |
+
<strong class="ml-4">RMSE:</strong> ${data.metrics.RMSE.toFixed(2)}
|
| 186 |
+
<strong class="ml-4">R²:</strong> ${data.metrics.R2.toFixed(3)}
|
| 187 |
+
</div>
|
| 188 |
+
`;
|
| 189 |
+
// Plot forecast with confidence intervals
|
| 190 |
+
const trace = {
|
| 191 |
+
x: data.dates,
|
| 192 |
+
y: data.forecast,
|
| 193 |
+
mode: 'lines+markers',
|
| 194 |
+
name: 'Forecast'
|
| 195 |
+
};
|
| 196 |
+
const lower = data.conf_int.map(ci => ci[0]);
|
| 197 |
+
const upper = data.conf_int.map(ci => ci[1]);
|
| 198 |
+
const ciTrace = {
|
| 199 |
+
x: [...data.dates, ...data.dates.slice().reverse()],
|
| 200 |
+
y: [...upper, ...lower.reverse()],
|
| 201 |
+
fill: 'toself',
|
| 202 |
+
fillcolor: 'rgba(0,100,80,0.2)',
|
| 203 |
+
line: {color: 'transparent'},
|
| 204 |
+
name: 'Confidence Interval',
|
| 205 |
+
showlegend: true,
|
| 206 |
+
type: 'scatter'
|
| 207 |
+
};
|
| 208 |
+
const layout = {
|
| 209 |
+
title: 'Forecasted Sales',
|
| 210 |
+
xaxis: {title: 'Date'},
|
| 211 |
+
yaxis: {title: 'Forecast'},
|
| 212 |
+
showlegend: true
|
| 213 |
+
};
|
| 214 |
+
Plotly.newPlot(plotDiv, [trace, ciTrace], layout);
|
| 215 |
+
showToast('Forecast generated!');
|
| 216 |
+
} else {
|
| 217 |
+
errorDiv.textContent = data.error;
|
| 218 |
+
showToast(data.error, 'error');
|
| 219 |
+
}
|
| 220 |
+
})
|
| 221 |
+
.catch(error => {
|
| 222 |
+
errorDiv.textContent = error.message;
|
| 223 |
+
showToast(error.message, 'error');
|
| 224 |
+
});
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Machine Failure Prediction
|
| 228 |
+
|
| 229 |
+
function runPrediction() {
|
| 230 |
+
// Show loading state
|
| 231 |
+
const metricsDiv = document.getElementById('predict-metrics');
|
| 232 |
+
const errorDiv = document.getElementById('predict-error');
|
| 233 |
+
const singlePredictionSection = document.getElementById('single-prediction-section');
|
| 234 |
+
|
| 235 |
+
metricsDiv.innerHTML = '<p>Running prediction...</p>';
|
| 236 |
+
errorDiv.textContent = '';
|
| 237 |
+
singlePredictionSection.classList.add('hidden'); // Hide form until model is ready
|
| 238 |
+
|
| 239 |
+
// Get selected values
|
| 240 |
+
const targetCol = document.getElementById('predictTargetCol').value;
|
| 241 |
+
const model = document.getElementById('predictModel').value; // Model is currently fixed to RF in backend, but UI allows selection
|
| 242 |
+
|
| 243 |
+
// Create form data
|
| 244 |
+
const formData = new FormData();
|
| 245 |
+
formData.append('target_col', targetCol);
|
| 246 |
+
formData.append('model', model);
|
| 247 |
+
|
| 248 |
+
// Make API call
|
| 249 |
+
fetch('/predict/machine_failure/run_prediction', {
|
| 250 |
+
method: 'POST',
|
| 251 |
+
body: formData
|
| 252 |
+
})
|
| 253 |
+
.then(response => response.json())
|
| 254 |
+
.then(data => {
|
| 255 |
+
if (data.success) {
|
| 256 |
+
// Display metrics in a grid
|
| 257 |
+
let metricsHtml =
|
| 258 |
+
'<div class="grid grid-cols-2 md:grid-cols-4 gap-4">';
|
| 259 |
+
for (const [key, value] of Object.entries(data.metrics)) {
|
| 260 |
+
metricsHtml += `
|
| 261 |
+
<div class="p-4 bg-blue-50 rounded">
|
| 262 |
+
<p class="font-semibold">${key}</p>
|
| 263 |
+
<p class="text-xl">${value.toFixed(4)}</p>
|
| 264 |
+
</div>
|
| 265 |
+
`;
|
| 266 |
+
}
|
| 267 |
+
metricsHtml += "</div>";
|
| 268 |
+
metricsDiv.innerHTML = metricsHtml;
|
| 269 |
+
|
| 270 |
+
// --- Feature Importance ---
|
| 271 |
+
if (data.top_features && Array.isArray(data.top_features)) {
|
| 272 |
+
const fiDiv = document.getElementById("feature-importance");
|
| 273 |
+
const fiList = document.getElementById("feature-importance-list");
|
| 274 |
+
fiDiv.classList.remove("hidden");
|
| 275 |
+
fiList.innerHTML = `
|
| 276 |
+
<table class="min-w-full table-auto">
|
| 277 |
+
<thead>
|
| 278 |
+
<tr>
|
| 279 |
+
<th class="px-4 py-2 bg-gray-100">Feature</th>
|
| 280 |
+
<th class="px-4 py-2 bg-gray-100">Importance</th>
|
| 281 |
+
</tr>
|
| 282 |
+
</thead>
|
| 283 |
+
<tbody>
|
| 284 |
+
${data.top_features
|
| 285 |
+
.map(
|
| 286 |
+
(f) =>
|
| 287 |
+
`<tr>
|
| 288 |
+
<td class="border px-4 py-2">${
|
| 289 |
+
f.feature
|
| 290 |
+
}</td>
|
| 291 |
+
<td class="border px-4 py-2">${f.importance.toFixed(
|
| 292 |
+
4
|
| 293 |
+
)}</td>
|
| 294 |
+
</tr>`
|
| 295 |
+
)
|
| 296 |
+
.join("")}
|
| 297 |
+
</tbody>
|
| 298 |
+
</table>
|
| 299 |
+
`;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
showToast("Model trained successfully");
|
| 303 |
+
|
| 304 |
+
// Now that the model is trained, fetch form data and display the single prediction form
|
| 305 |
+
fetchSinglePredictionForm();
|
| 306 |
+
} else {
|
| 307 |
+
errorDiv.textContent = data.error || 'An error occurred';
|
| 308 |
+
showToast(data.error || 'An error occurred', 'error');
|
| 309 |
+
}
|
| 310 |
+
})
|
| 311 |
+
.catch(error => {
|
| 312 |
+
errorDiv.textContent = 'Error: ' + error.message;
|
| 313 |
+
showToast('Error: ' + error.message, 'error');
|
| 314 |
+
});
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
window.runPrediction = runPrediction;
|
| 318 |
+
|
| 319 |
+
// Function to fetch data for and generate the single prediction form
|
| 320 |
+
function fetchSinglePredictionForm() {
|
| 321 |
+
fetch('/predict/machine_failure/get_form_data')
|
| 322 |
+
.then(response => response.json())
|
| 323 |
+
.then(data => {
|
| 324 |
+
if (data.success) {
|
| 325 |
+
generatePredictionForm(data.form_fields);
|
| 326 |
+
document.getElementById('single-prediction-section').classList.remove('hidden');
|
| 327 |
+
} else {
|
| 328 |
+
showToast(data.error, 'error');
|
| 329 |
+
document.getElementById('single-prediction-error').textContent = data.error;
|
| 330 |
+
}
|
| 331 |
+
})
|
| 332 |
+
.catch(error => {
|
| 333 |
+
console.error('Error fetching form data:', error);
|
| 334 |
+
showToast('Error fetching form data: ' + error.message, 'error');
|
| 335 |
+
document.getElementById('single-prediction-error').textContent = 'Error fetching form data: ' + error.message;
|
| 336 |
+
});
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
// Function to dynamically generate the prediction form
|
| 340 |
+
function generatePredictionForm(formFields) {
|
| 341 |
+
const formContainer = document.getElementById('single-prediction-form');
|
| 342 |
+
formContainer.innerHTML = ''; // Clear previous fields
|
| 343 |
+
|
| 344 |
+
formFields.forEach(field => {
|
| 345 |
+
const div = document.createElement('div');
|
| 346 |
+
div.className = 'mb-4';
|
| 347 |
+
|
| 348 |
+
const label = document.createElement('label');
|
| 349 |
+
label.className = 'block text-gray-700 text-sm font-bold mb-2';
|
| 350 |
+
label.textContent = field.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); // Capitalize and replace underscores
|
| 351 |
+
|
| 352 |
+
div.appendChild(label);
|
| 353 |
+
|
| 354 |
+
if (field.type === 'select') {
|
| 355 |
+
const select = document.createElement('select');
|
| 356 |
+
select.id = `input-${field.name}`;
|
| 357 |
+
select.name = field.name;
|
| 358 |
+
select.className = 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline';
|
| 359 |
+
|
| 360 |
+
field.options.forEach(option => {
|
| 361 |
+
const optionElement = document.createElement('option');
|
| 362 |
+
optionElement.value = option;
|
| 363 |
+
optionElement.textContent = option;
|
| 364 |
+
// Set default selected option
|
| 365 |
+
if (field.default_value !== undefined && String(field.default_value) === String(option)) { // Compare as strings
|
| 366 |
+
optionElement.selected = true;
|
| 367 |
+
}
|
| 368 |
+
select.appendChild(optionElement);
|
| 369 |
+
});
|
| 370 |
+
div.appendChild(select);
|
| 371 |
+
} else if (field.type === 'number') {
|
| 372 |
+
const input = document.createElement('input');
|
| 373 |
+
input.id = `input-${field.name}`;
|
| 374 |
+
input.name = field.name;
|
| 375 |
+
input.type = 'number';
|
| 376 |
+
input.className = 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline';
|
| 377 |
+
input.placeholder = `Enter ${field.name.replace(/_/g, ' ')}`;
|
| 378 |
+
if (field.default_value !== undefined) {
|
| 379 |
+
input.value = field.default_value;
|
| 380 |
+
}
|
| 381 |
+
div.appendChild(input);
|
| 382 |
+
} else { // Default to text for other types, including timestamps
|
| 383 |
+
const input = document.createElement('input');
|
| 384 |
+
input.id = `input-${field.name}`;
|
| 385 |
+
input.name = field.name;
|
| 386 |
+
input.type = 'text';
|
| 387 |
+
input.className = 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline';
|
| 388 |
+
input.placeholder = field.placeholder || `Enter ${field.name.replace(/_/g, ' ')}`;
|
| 389 |
+
if (field.default_value !== undefined) {
|
| 390 |
+
input.value = field.default_value;
|
| 391 |
+
}
|
| 392 |
+
div.appendChild(input);
|
| 393 |
+
}
|
| 394 |
+
formContainer.appendChild(div);
|
| 395 |
+
});
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
// Function to handle single instance prediction submission
|
| 399 |
+
window.predictSingleInstance = function() {
|
| 400 |
+
const form = document.getElementById('single-prediction-form');
|
| 401 |
+
const formData = {};
|
| 402 |
+
const inputs = form.querySelectorAll('input, select');
|
| 403 |
+
|
| 404 |
+
inputs.forEach(input => {
|
| 405 |
+
formData[input.name] = input.value;
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
const resultDiv = document.getElementById('single-prediction-result');
|
| 409 |
+
const predictionOutput = document.getElementById('prediction-output');
|
| 410 |
+
const probabilityOutput = document.getElementById('probability-output');
|
| 411 |
+
const errorDiv = document.getElementById('single-prediction-error');
|
| 412 |
+
|
| 413 |
+
resultDiv.classList.add('hidden');
|
| 414 |
+
errorDiv.textContent = '';
|
| 415 |
+
predictionOutput.textContent = 'Predicting...';
|
| 416 |
+
probabilityOutput.innerHTML = '';
|
| 417 |
+
|
| 418 |
+
fetch('/predict/machine_failure/predict_single', {
|
| 419 |
+
method: 'POST',
|
| 420 |
+
headers: {
|
| 421 |
+
'Content-Type': 'application/json',
|
| 422 |
+
},
|
| 423 |
+
body: JSON.stringify(formData)
|
| 424 |
+
})
|
| 425 |
+
.then(response => response.json())
|
| 426 |
+
.then(data => {
|
| 427 |
+
if (data.success) {
|
| 428 |
+
let displayPrediction = 'Unknown';
|
| 429 |
+
if (data.prediction === 0 || data.prediction === '0') {
|
| 430 |
+
displayPrediction = 'No Failure';
|
| 431 |
+
} else if (data.prediction === 1 || data.prediction === '1') {
|
| 432 |
+
displayPrediction = 'Failure';
|
| 433 |
+
} else {
|
| 434 |
+
displayPrediction = data.prediction; // Fallback for other values if backend sends different strings
|
| 435 |
+
}
|
| 436 |
+
predictionOutput.textContent = displayPrediction;
|
| 437 |
+
|
| 438 |
+
if (data.probability && Array.isArray(data.probability)) {
|
| 439 |
+
if (data.probability.length === 2) { // Binary classification
|
| 440 |
+
const probNoFailure = data.probability[0];
|
| 441 |
+
const probFailure = data.probability[1];
|
| 442 |
+
probabilityOutput.innerHTML = `Probability of No Failure: ${(probNoFailure * 100).toFixed(2)}%<br>
|
| 443 |
+
Probability of Failure: ${(probFailure * 100).toFixed(2)}%`;
|
| 444 |
+
} else { // Multi-class classification
|
| 445 |
+
probabilityOutput.innerHTML = 'Probabilities: ' + data.probability.map((p, i) => `Class ${i}: ${(p * 100).toFixed(2)}%`).join(', ');
|
| 446 |
+
}
|
| 447 |
+
} else {
|
| 448 |
+
probabilityOutput.innerHTML = 'Probability: N/A (not a classification model)';
|
| 449 |
+
}
|
| 450 |
+
resultDiv.classList.remove('hidden');
|
| 451 |
+
showToast('Single prediction successful!');
|
| 452 |
+
} else {
|
| 453 |
+
errorDiv.textContent = data.error || 'An error occurred during single prediction.';
|
| 454 |
+
showToast(data.error || 'An error occurred', 'error');
|
| 455 |
+
}
|
| 456 |
+
})
|
| 457 |
+
.catch(error => {
|
| 458 |
+
console.error('Error during single prediction:', error);
|
| 459 |
+
errorDiv.textContent = 'Error: ' + error.message;
|
| 460 |
+
showToast('Error: ' + error.message, 'error');
|
| 461 |
+
});
|
| 462 |
+
};
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
// Supply Failure Prediction (New functions similar to Machine Failure)
|
| 466 |
+
|
| 467 |
+
window.runSupplyPrediction = function() {
|
| 468 |
+
const metricsDiv = document.getElementById('predict-metrics-supply');
|
| 469 |
+
const errorDiv = document.getElementById('predict-error-supply');
|
| 470 |
+
const singlePredictionSection = document.getElementById('single-prediction-section-supply');
|
| 471 |
+
|
| 472 |
+
metricsDiv.innerHTML = '<p>Running prediction...</p>';
|
| 473 |
+
errorDiv.textContent = '';
|
| 474 |
+
singlePredictionSection.classList.add('hidden');
|
| 475 |
+
|
| 476 |
+
// Target column is fixed to 'failure_flag'
|
| 477 |
+
const targetCol = 'failure_flag';
|
| 478 |
+
const model = document.getElementById('predictModelSupply').value;
|
| 479 |
+
|
| 480 |
+
const formData = new FormData();
|
| 481 |
+
formData.append('target_col', targetCol);
|
| 482 |
+
formData.append('model', model);
|
| 483 |
+
|
| 484 |
+
fetch('/predict/supply_failure/run_prediction', {
|
| 485 |
+
method: 'POST',
|
| 486 |
+
body: formData
|
| 487 |
+
})
|
| 488 |
+
.then(response => response.json())
|
| 489 |
+
.then(data => {
|
| 490 |
+
if (data.success) {
|
| 491 |
+
let metricsHtml =
|
| 492 |
+
'<div class="grid grid-cols-2 md:grid-cols-4 gap-4">';
|
| 493 |
+
for (const [key, value] of Object.entries(data.metrics)) {
|
| 494 |
+
metricsHtml += `
|
| 495 |
+
<div class="p-4 bg-blue-50 rounded">
|
| 496 |
+
<p class="font-semibold">${key}</p>
|
| 497 |
+
<p class="text-xl">${value.toFixed(4)}</p>
|
| 498 |
+
</div>
|
| 499 |
+
`;
|
| 500 |
+
}
|
| 501 |
+
metricsHtml += "</div>";
|
| 502 |
+
metricsDiv.innerHTML = metricsHtml;
|
| 503 |
+
|
| 504 |
+
// --- Feature Importance ---
|
| 505 |
+
if (data.top_features && Array.isArray(data.top_features)) {
|
| 506 |
+
const fiDiv = document.getElementById("feature-importance");
|
| 507 |
+
const fiList = document.getElementById("feature-importance-list");
|
| 508 |
+
fiDiv.classList.remove("hidden");
|
| 509 |
+
fiList.innerHTML = `
|
| 510 |
+
<table class="min-w-full table-auto">
|
| 511 |
+
<thead>
|
| 512 |
+
<tr>
|
| 513 |
+
<th class="px-4 py-2 bg-gray-100">Feature</th>
|
| 514 |
+
<th class="px-4 py-2 bg-gray-100">Importance</th>
|
| 515 |
+
</tr>
|
| 516 |
+
</thead>
|
| 517 |
+
<tbody>
|
| 518 |
+
${data.top_features
|
| 519 |
+
.map(
|
| 520 |
+
(f) =>
|
| 521 |
+
`<tr>
|
| 522 |
+
<td class="border px-4 py-2">${
|
| 523 |
+
f.feature
|
| 524 |
+
}</td>
|
| 525 |
+
<td class="border px-4 py-2">${f.importance.toFixed(
|
| 526 |
+
4
|
| 527 |
+
)}</td>
|
| 528 |
+
</tr>`
|
| 529 |
+
)
|
| 530 |
+
.join("")}
|
| 531 |
+
</tbody>
|
| 532 |
+
</table>
|
| 533 |
+
`;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
showToast("Supply Model trained successfully");
|
| 537 |
+
fetchSupplySinglePredictionForm();
|
| 538 |
+
} else {
|
| 539 |
+
errorDiv.textContent = data.error || 'An error occurred';
|
| 540 |
+
showToast(data.error || 'An error occurred', 'error');
|
| 541 |
+
}
|
| 542 |
+
})
|
| 543 |
+
.catch(error => {
|
| 544 |
+
errorDiv.textContent = 'Error: ' + error.message;
|
| 545 |
+
showToast('Error: ' + error.message, 'error');
|
| 546 |
+
});
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
function fetchSupplySinglePredictionForm() {
|
| 550 |
+
fetch('/predict/supply_failure/get_form_data')
|
| 551 |
+
.then(response => response.json())
|
| 552 |
+
.then(data => {
|
| 553 |
+
if (data.success) {
|
| 554 |
+
generateSupplyPredictionForm(data.form_fields);
|
| 555 |
+
document.getElementById('single-prediction-section-supply').classList.remove('hidden');
|
| 556 |
+
} else {
|
| 557 |
+
showToast(data.error, 'error');
|
| 558 |
+
document.getElementById('single-prediction-error-supply').textContent = data.error;
|
| 559 |
+
}
|
| 560 |
+
})
|
| 561 |
+
.catch(error => {
|
| 562 |
+
console.error('Error fetching supply form data:', error);
|
| 563 |
+
showToast('Error fetching supply form data: ' + error.message, 'error');
|
| 564 |
+
document.getElementById('single-prediction-error-supply').textContent = 'Error fetching supply form data: ' + error.message;
|
| 565 |
+
});
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
function generateSupplyPredictionForm(formFields) {
|
| 569 |
+
const formContainer = document.getElementById('single-prediction-form-supply');
|
| 570 |
+
formContainer.innerHTML = '';
|
| 571 |
+
|
| 572 |
+
formFields.forEach(field => {
|
| 573 |
+
const div = document.createElement('div');
|
| 574 |
+
div.className = 'mb-4';
|
| 575 |
+
|
| 576 |
+
const label = document.createElement('label');
|
| 577 |
+
label.className = 'block text-gray-700 text-sm font-bold mb-2';
|
| 578 |
+
label.textContent = field.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 579 |
+
|
| 580 |
+
div.appendChild(label);
|
| 581 |
+
|
| 582 |
+
if (field.type === 'select') {
|
| 583 |
+
const select = document.createElement('select');
|
| 584 |
+
select.id = `input-supply-${field.name}`;
|
| 585 |
+
select.name = field.name;
|
| 586 |
+
select.className = 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline';
|
| 587 |
+
|
| 588 |
+
field.options.forEach(option => {
|
| 589 |
+
const optionElement = document.createElement('option');
|
| 590 |
+
optionElement.value = option;
|
| 591 |
+
optionElement.textContent = option;
|
| 592 |
+
if (field.default_value !== undefined && String(field.default_value) === String(option)) {
|
| 593 |
+
optionElement.selected = true;
|
| 594 |
+
}
|
| 595 |
+
select.appendChild(optionElement);
|
| 596 |
+
});
|
| 597 |
+
div.appendChild(select);
|
| 598 |
+
} else if (field.type === 'number') {
|
| 599 |
+
const input = document.createElement('input');
|
| 600 |
+
input.id = `input-supply-${field.name}`;
|
| 601 |
+
input.name = field.name;
|
| 602 |
+
input.type = 'number';
|
| 603 |
+
input.className = 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline';
|
| 604 |
+
input.placeholder = `Enter ${field.name.replace(/_/g, ' ')}`;
|
| 605 |
+
if (field.default_value !== undefined) {
|
| 606 |
+
input.value = field.default_value;
|
| 607 |
+
}
|
| 608 |
+
div.appendChild(input);
|
| 609 |
+
} else { // Default to text for other types, including timestamps
|
| 610 |
+
const input = document.createElement('input');
|
| 611 |
+
input.id = `input-supply-${field.name}`;
|
| 612 |
+
input.name = field.name;
|
| 613 |
+
input.type = 'text';
|
| 614 |
+
input.className = 'shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline';
|
| 615 |
+
input.placeholder = field.placeholder || `Enter ${field.name.replace(/_/g, ' ')}`;
|
| 616 |
+
if (field.default_value !== undefined) {
|
| 617 |
+
input.value = field.default_value;
|
| 618 |
+
}
|
| 619 |
+
div.appendChild(input);
|
| 620 |
+
}
|
| 621 |
+
formContainer.appendChild(div);
|
| 622 |
+
});
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
window.predictSupplySingleInstance = function() {
|
| 626 |
+
const form = document.getElementById('single-prediction-form-supply');
|
| 627 |
+
const formData = {};
|
| 628 |
+
const inputs = form.querySelectorAll('input, select');
|
| 629 |
+
|
| 630 |
+
inputs.forEach(input => {
|
| 631 |
+
formData[input.name] = input.value;
|
| 632 |
+
});
|
| 633 |
+
|
| 634 |
+
const resultDiv = document.getElementById('single-prediction-result-supply');
|
| 635 |
+
const predictionOutput = document.getElementById('prediction-output-supply');
|
| 636 |
+
const probabilityOutput = document.getElementById('probability-output-supply');
|
| 637 |
+
const errorDiv = document.getElementById('single-prediction-error-supply');
|
| 638 |
+
|
| 639 |
+
resultDiv.classList.add('hidden');
|
| 640 |
+
errorDiv.textContent = '';
|
| 641 |
+
predictionOutput.textContent = 'Predicting...';
|
| 642 |
+
probabilityOutput.innerHTML = '';
|
| 643 |
+
|
| 644 |
+
fetch('/predict/supply_failure/predict_single', {
|
| 645 |
+
method: 'POST',
|
| 646 |
+
headers: {
|
| 647 |
+
'Content-Type': 'application/json',
|
| 648 |
+
},
|
| 649 |
+
body: JSON.stringify(formData)
|
| 650 |
+
})
|
| 651 |
+
.then(response => response.json())
|
| 652 |
+
.then(data => {
|
| 653 |
+
if (data.success) {
|
| 654 |
+
// User-friendly mapping for Supply Failure
|
| 655 |
+
let displayPrediction = 'Unknown';
|
| 656 |
+
if (data.prediction === "Delivery Successful") { // Backend sends "Delivery Successful" or "Delivery Failed"
|
| 657 |
+
displayPrediction = 'Delivery Successful';
|
| 658 |
+
} else if (data.prediction === "Delivery Failed") {
|
| 659 |
+
displayPrediction = 'Delivery Failed';
|
| 660 |
+
} else {
|
| 661 |
+
displayPrediction = data.prediction; // Fallback
|
| 662 |
+
}
|
| 663 |
+
predictionOutput.textContent = displayPrediction;
|
| 664 |
+
|
| 665 |
+
if (data.probability && Array.isArray(data.probability)) {
|
| 666 |
+
if (data.probability.length === 2) {
|
| 667 |
+
// Assuming data.probability[0] corresponds to 'Delivery Successful' and data.probability[1] to 'Delivery Failed'
|
| 668 |
+
const probSuccessful = data.probability[0];
|
| 669 |
+
const probFailed = data.probability[1];
|
| 670 |
+
probabilityOutput.innerHTML = `Probability of Delivery Successful: ${(probSuccessful * 100).toFixed(2)}%<br>
|
| 671 |
+
Probability of Delivery Failed: ${(probFailed * 100).toFixed(2)}%`;
|
| 672 |
+
} else {
|
| 673 |
+
probabilityOutput.innerHTML = 'Probabilities: ' + data.probability.map((p, i) => `Class ${i}: ${(p * 100).toFixed(2)}%`).join(', ');
|
| 674 |
+
}
|
| 675 |
+
} else {
|
| 676 |
+
probabilityOutput.innerHTML = 'Probability: N/A (not a classification model)';
|
| 677 |
+
}
|
| 678 |
+
resultDiv.classList.remove('hidden');
|
| 679 |
+
showToast('Single prediction successful!');
|
| 680 |
+
} else {
|
| 681 |
+
errorDiv.textContent = data.error || 'An error occurred during single prediction.';
|
| 682 |
+
showToast(data.error || 'An error occurred', 'error');
|
| 683 |
+
}
|
| 684 |
+
})
|
| 685 |
+
.catch(error => {
|
| 686 |
+
console.error('Error during single prediction:', error);
|
| 687 |
+
errorDiv.textContent = 'Error: ' + error.message;
|
| 688 |
+
showToast('Error: ' + error.message, 'error');
|
| 689 |
+
});
|
| 690 |
+
};
|
static/styles.css
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Base typography and container */
|
| 2 |
+
body {
|
| 3 |
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
/* Container max width override */
|
| 7 |
+
.container {
|
| 8 |
+
max-width: 1200px;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/* Custom keyframes for fade in */
|
| 12 |
+
@keyframes fadeIn {
|
| 13 |
+
from {
|
| 14 |
+
opacity: 0;
|
| 15 |
+
transform: translateY(20px);
|
| 16 |
+
}
|
| 17 |
+
to {
|
| 18 |
+
opacity: 1;
|
| 19 |
+
transform: translateY(0);
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/* Animation helper */
|
| 24 |
+
.animate-fadeIn {
|
| 25 |
+
animation: fadeIn 1s ease-out forwards;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Hero section background and overlay */
|
| 29 |
+
.hero-bg {
|
| 30 |
+
background-image: url('https://source.unsplash.com/1600x900/?business,analytics');
|
| 31 |
+
background-size: cover;
|
| 32 |
+
background-position: center;
|
| 33 |
+
position: relative;
|
| 34 |
+
height: 80vh;
|
| 35 |
+
}
|
| 36 |
+
.hero-overlay {
|
| 37 |
+
position: absolute;
|
| 38 |
+
top: 0;
|
| 39 |
+
left: 0;
|
| 40 |
+
right: 0;
|
| 41 |
+
bottom: 0;
|
| 42 |
+
background: rgba(0, 0, 0, 0.5);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* Button styles */
|
| 46 |
+
.btn-learn-more {
|
| 47 |
+
display: inline-block;
|
| 48 |
+
padding: 0.75rem 1.5rem;
|
| 49 |
+
font-size: 0.875rem;
|
| 50 |
+
font-weight: 600;
|
| 51 |
+
border-radius: 0.5rem;
|
| 52 |
+
transition: background-color 0.3s ease;
|
| 53 |
+
background-color: #3b82f6; /* Tailwind blue-500 */
|
| 54 |
+
color: #ffffff;
|
| 55 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 56 |
+
}
|
| 57 |
+
.btn-learn-more:hover {
|
| 58 |
+
background-color: #2563eb; /* Tailwind blue-600 */
|
| 59 |
+
}
|
| 60 |
+
.btn-custom {
|
| 61 |
+
transition: transform 0.3s ease;
|
| 62 |
+
}
|
| 63 |
+
.btn-custom:hover {
|
| 64 |
+
transform: scale(1.05);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/* ----- Module Base Styles (Reusable for all modules) ----- */
|
| 68 |
+
.module {
|
| 69 |
+
padding: 1rem;
|
| 70 |
+
}
|
| 71 |
+
.module-title {
|
| 72 |
+
font-size: 1.875rem; /* 30px equivalent */
|
| 73 |
+
font-weight: 700;
|
| 74 |
+
color: #1f2937; /* Tailwind gray-800 */
|
| 75 |
+
}
|
| 76 |
+
.module-intro {
|
| 77 |
+
font-size: 1.125rem;
|
| 78 |
+
color: #4b5563; /* Tailwind gray-600 */
|
| 79 |
+
}
|
| 80 |
+
.module-section {
|
| 81 |
+
background-color: #ffffff;
|
| 82 |
+
padding: 1.5rem;
|
| 83 |
+
border-radius: 0.5rem;
|
| 84 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 85 |
+
margin-bottom: 1.5rem;
|
| 86 |
+
}
|
| 87 |
+
.module-section-header {
|
| 88 |
+
font-size: 1.5rem; /* 24px equivalent */
|
| 89 |
+
font-weight: 700;
|
| 90 |
+
color: #374151; /* Tailwind gray-700 */
|
| 91 |
+
margin-bottom: 0.5rem;
|
| 92 |
+
}
|
| 93 |
+
.module-section-content {
|
| 94 |
+
font-size: 1rem;
|
| 95 |
+
color: #4b5563;
|
| 96 |
+
}
|
| 97 |
+
.module-list li {
|
| 98 |
+
margin-bottom: 0.5rem;
|
| 99 |
+
}
|
templates/base.html
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{{ title | default("Predictive Analytics Tool") }}</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
| 9 |
+
<script src="{{ url_for('static', filename='index.js') }}" defer></script>
|
| 10 |
+
</head>
|
| 11 |
+
<body class="bg-gray-100 text-gray-900">
|
| 12 |
+
<header class="bg-white shadow">
|
| 13 |
+
<div class="container mx-auto px-4 py-6 flex justify-between items-center">
|
| 14 |
+
<div class="text-2xl font-bold">Predictive Analytics Tool</div>
|
| 15 |
+
<nav>
|
| 16 |
+
<ul class="flex space-x-4">
|
| 17 |
+
<li><a href="/" class="hover:text-blue-500 transition">Home</a></li>
|
| 18 |
+
<li><a href="/forecast/sales" class="hover:text-blue-500 transition">Sales Prediction</a></li>
|
| 19 |
+
<li><a href="/campaign" class="hover:text-blue-500 transition">Campaign Analysis</a></li>
|
| 20 |
+
<li><a href="/inventory" class="hover:text-blue-500 transition">Inventory Optimization</a></li>
|
| 21 |
+
<li><a href="/pricing" class="hover:text-blue-500 transition">Pricing Intelligence</a></li>
|
| 22 |
+
<li><a href="/underperformance" class="hover:text-blue-500 transition">Underperformance</a></li>
|
| 23 |
+
<li><a href="/predict/machine_failure" class="hover:text-blue-500 transition">Machine Failure</a></li>
|
| 24 |
+
<li><a href="/predict/supply_failure" class="hover:text-blue-500 transition">Supply Failure</a></li>
|
| 25 |
+
</ul>
|
| 26 |
+
</nav>
|
| 27 |
+
</div>
|
| 28 |
+
</header>
|
| 29 |
+
<main>
|
| 30 |
+
{% block content %}{% endblock %}
|
| 31 |
+
</main>
|
| 32 |
+
<footer class="bg-white border-t">
|
| 33 |
+
<div class="container mx-auto px-4 py-4 text-center text-sm">
|
| 34 |
+
© 2025 Predictive Analytics Tool. All rights reserved.
|
| 35 |
+
</div>
|
| 36 |
+
</footer>
|
| 37 |
+
</body>
|
| 38 |
+
</html>
|
templates/campaign_analysis.html
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<section class="container mx-auto py-8">
|
| 4 |
+
<h1 class="text-3xl font-bold mb-4">Campaign Analysis</h1>
|
| 5 |
+
<p class="mb-6">Analyze the performance of your promotions and campaigns to calculate uplift, ROI, and customer engagement.</p>
|
| 6 |
+
<form method="POST" action="{{ url_for('campaign_analysis.upload_files') }}" enctype="multipart/form-data" class="mb-8 bg-white p-6 rounded shadow flex flex-col gap-4">
|
| 7 |
+
<div class="flex flex-col gap-2">
|
| 8 |
+
<label class="font-semibold">Sales Data CSV</label>
|
| 9 |
+
<input type="file" name="sales_file" accept=".csv" required class="file:border file:rounded file:px-3 file:py-1">
|
| 10 |
+
</div>
|
| 11 |
+
<div class="flex flex-col gap-2">
|
| 12 |
+
<label class="font-semibold">Campaign Data CSV</label>
|
| 13 |
+
<input type="file" name="campaign_file" accept=".csv" required class="file:border file:rounded file:px-3 file:py-1">
|
| 14 |
+
</div>
|
| 15 |
+
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition">Analyze Campaigns</button>
|
| 16 |
+
</form>
|
| 17 |
+
|
| 18 |
+
{% if summary %}
|
| 19 |
+
<div class="bg-white p-6 rounded shadow mb-8">
|
| 20 |
+
<h2 class="text-xl font-bold mb-2">Summary</h2>
|
| 21 |
+
<ul class="list-disc pl-5">
|
| 22 |
+
<li><b>Total Campaigns:</b> {{ summary.total_campaigns }}</li>
|
| 23 |
+
<li><b>Total Campaign Revenue:</b> ${{ "%.2f"|format(summary.total_revenue) }}</li>
|
| 24 |
+
<li><b>Average Campaign Uplift:</b> {{ "%.2f"|format(summary.avg_uplift_pct) }}%</li>
|
| 25 |
+
<li><b>Average ROI:</b> {{ "%.2f"|format(summary.avg_roi) }}%</li>
|
| 26 |
+
</ul>
|
| 27 |
+
</div>
|
| 28 |
+
{% endif %}
|
| 29 |
+
|
| 30 |
+
{% if plot_data %}
|
| 31 |
+
<div class="bg-white p-6 rounded shadow mb-8">
|
| 32 |
+
<h2 class="text-xl font-bold mb-4">Campaign Visualizations</h2>
|
| 33 |
+
<div class="flex flex-col gap-8 w-full">
|
| 34 |
+
<div class="w-full">
|
| 35 |
+
<h3 class="font-semibold mb-2 text-blue-600">Uplift % by Campaign</h3>
|
| 36 |
+
<div id="uplift_chart" class="w-full" style="min-height:350px;"></div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="w-full">
|
| 39 |
+
<h3 class="font-semibold mb-2 text-blue-600">ROI % by Campaign</h3>
|
| 40 |
+
<div id="roi_chart" class="w-full" style="min-height:350px;"></div>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="w-full">
|
| 43 |
+
<h3 class="font-semibold mb-2 text-blue-600">Campaign Revenue Over Time</h3>
|
| 44 |
+
<div id="revenue_chart" class="w-full" style="min-height:350px;"></div>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="w-full">
|
| 47 |
+
<h3 class="font-semibold mb-2 text-blue-600">Campaign Types</h3>
|
| 48 |
+
<div id="type_pie" class="w-full" style="min-height:350px;"></div>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="w-full">
|
| 51 |
+
<h3 class="font-semibold mb-2 text-blue-600">Top Regions</h3>
|
| 52 |
+
<div id="region_pie" class="w-full" style="min-height:350px;"></div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
<script src="https://cdn.plot.ly/plotly-2.31.1.min.js"></script>
|
| 57 |
+
<script>
|
| 58 |
+
const plots = {{ plot_data|tojson }};
|
| 59 |
+
if (plots && plots.bar_uplift && plots.bar_uplift.x && plots.bar_uplift.x.length) {
|
| 60 |
+
Plotly.newPlot('uplift_chart', [{
|
| 61 |
+
x: plots.bar_uplift.x,
|
| 62 |
+
y: plots.bar_uplift.y,
|
| 63 |
+
type: 'bar',
|
| 64 |
+
marker: {color: '#3b82f6'},
|
| 65 |
+
text: plots.bar_uplift.y.map(v=>v.toFixed(2)+'%')
|
| 66 |
+
}], {title: 'Uplift % by Campaign', margin: {t:30}, width: 900, height: 350});
|
| 67 |
+
}
|
| 68 |
+
if (plots && plots.bar_roi && plots.bar_roi.x && plots.bar_roi.x.length) {
|
| 69 |
+
Plotly.newPlot('roi_chart', [{
|
| 70 |
+
x: plots.bar_roi.x,
|
| 71 |
+
y: plots.bar_roi.y,
|
| 72 |
+
type: 'bar',
|
| 73 |
+
marker: {color: '#10b981'},
|
| 74 |
+
text: plots.bar_roi.y.map(v=>v.toFixed(2)+'%')
|
| 75 |
+
}], {title: 'ROI % by Campaign', margin: {t:30}, width: 900, height: 350});
|
| 76 |
+
}
|
| 77 |
+
if (plots && plots.line_revenue && plots.line_revenue.x && plots.line_revenue.x.length) {
|
| 78 |
+
Plotly.newPlot('revenue_chart', [{
|
| 79 |
+
x: plots.line_revenue.x,
|
| 80 |
+
y: plots.line_revenue.y,
|
| 81 |
+
type: 'scatter',
|
| 82 |
+
mode: 'lines+markers',
|
| 83 |
+
marker: {color: '#6366f1'},
|
| 84 |
+
text: plots.line_revenue.names
|
| 85 |
+
}], {title: 'Revenue by Campaign Start Date', margin: {t:30}, width: 900, height: 350});
|
| 86 |
+
}
|
| 87 |
+
if (plots && plots.pie_type && plots.pie_type.labels && plots.pie_type.labels.length) {
|
| 88 |
+
Plotly.newPlot('type_pie', [{
|
| 89 |
+
labels: plots.pie_type.labels,
|
| 90 |
+
values: plots.pie_type.values,
|
| 91 |
+
type: 'pie'
|
| 92 |
+
}], {title: 'Campaign Type Distribution', margin: {t:30}, width: 900, height: 350});
|
| 93 |
+
}
|
| 94 |
+
if (plots && plots.pie_region && plots.pie_region.labels && plots.pie_region.labels.length) {
|
| 95 |
+
Plotly.newPlot('region_pie', [{
|
| 96 |
+
labels: plots.pie_region.labels,
|
| 97 |
+
values: plots.pie_region.values,
|
| 98 |
+
type: 'pie'
|
| 99 |
+
}], {title: 'Top Regions', margin: {t:30}, width: 900, height: 350});
|
| 100 |
+
}
|
| 101 |
+
</script>
|
| 102 |
+
{% endif %}
|
| 103 |
+
|
| 104 |
+
</section>
|
| 105 |
+
{% endblock %}
|
templates/index.html
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<!-- Hero section -->
|
| 4 |
+
<section class="hero-bg">
|
| 5 |
+
<div class="hero-overlay flex flex-col justify-center items-center">
|
| 6 |
+
<h1 class="text-5xl text-white font-bold mb-4 animate-on-scroll">Transform Your Data</h1>
|
| 7 |
+
<p class="mb-6 text-xl text-white animate-on-scroll">Empower your sales & inventory decisions with advanced predictive analytics.</p>
|
| 8 |
+
<a href="/forecast/sales" class="btn-custom bg-blue-500 text-white px-8 py-3 rounded-full shadow-lg hover:bg-blue-600 transition animate-on-scroll">
|
| 9 |
+
Get Started
|
| 10 |
+
</a>
|
| 11 |
+
</div>
|
| 12 |
+
</section>
|
| 13 |
+
|
| 14 |
+
<!-- Features Section -->
|
| 15 |
+
<section id="features" class="py-16 bg-gray-50">
|
| 16 |
+
<div class="container mx-auto px-4">
|
| 17 |
+
<!-- Sales Prediction -->
|
| 18 |
+
<div class="flex flex-col md:flex-row items-center my-12 animate-on-scroll">
|
| 19 |
+
<div class="md:w-1/2 md:pr-8">
|
| 20 |
+
<h2 class="text-3xl font-bold mb-4">Sales Prediction Engine</h2>
|
| 21 |
+
<p class="mb-4">Forecast daily sales with high accuracy using state-of-the-art machine learning and time-series models.</p>
|
| 22 |
+
<a href="/forecast/sales" class="text-blue-500 hover:underline">Learn More</a>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="md:w-1/2">
|
| 25 |
+
<img src="static\images\sales prediction.png" alt="Sales Prediction" class="w-full rounded-lg shadow-lg">
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<!-- Campaign Analysis -->
|
| 30 |
+
<div class="flex flex-col md:flex-row-reverse items-center my-12 animate-on-scroll">
|
| 31 |
+
<div class="md:w-1/2 md:pl-8">
|
| 32 |
+
<h2 class="text-3xl font-bold mb-4">Campaign Analysis</h2>
|
| 33 |
+
<p class="mb-4">Analyze campaign performance to optimize ROI and boost customer engagement through comprehensive insights.</p>
|
| 34 |
+
<a href="/campaign" class="text-blue-500 hover:underline">Learn More</a>
|
| 35 |
+
</div>
|
| 36 |
+
<div class="md:w-1/2">
|
| 37 |
+
<img src="static\images\campaign.jpg" alt="Campaign Analysis" class="w-full rounded-lg shadow-lg">
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<!-- Inventory Optimization -->
|
| 42 |
+
<div class="flex flex-col md:flex-row items-center my-12 animate-on-scroll">
|
| 43 |
+
<div class="md:w-1/2 md:pr-8">
|
| 44 |
+
<h2 class="text-3xl font-bold mb-4">Inventory Optimization</h2>
|
| 45 |
+
<p class="mb-4">Utilize demand forecasting and sophisticated optimization algorithms to reduce surplus and prevent stockouts.</p>
|
| 46 |
+
<a href="/inventory" class="text-blue-500 hover:underline">Learn More</a>
|
| 47 |
+
</div>
|
| 48 |
+
<div class="md:w-1/2">
|
| 49 |
+
<img src="static\images\inventory.png" alt="Inventory Optimization" class="w-full rounded-lg shadow-lg">
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<!-- Pricing Intelligence -->
|
| 54 |
+
<div class="flex flex-col md:flex-row-reverse items-center my-12 animate-on-scroll">
|
| 55 |
+
<div class="md:w-1/2 md:pl-8">
|
| 56 |
+
<h2 class="text-3xl font-bold mb-4">Pricing Intelligence</h2>
|
| 57 |
+
<p class="mb-4">Optimize pricing strategies with advanced conversion analytics and price elasticity models for maximum revenue.</p>
|
| 58 |
+
<a href="/pricing" class="text-blue-500 hover:underline">Learn More</a>
|
| 59 |
+
</div>
|
| 60 |
+
<div class="md:w-1/2">
|
| 61 |
+
<img src="static\images\pricing.jpg" alt="Pricing Intelligence" class="w-full rounded-lg shadow-lg">
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<!-- Underperformance Detector -->
|
| 66 |
+
<div class="flex flex-col md:flex-row items-center my-12 animate-on-scroll">
|
| 67 |
+
<div class="md:w-1/2 md:pr-8">
|
| 68 |
+
<h2 class="text-3xl font-bold mb-4">Underperformance Detector</h2>
|
| 69 |
+
<p class="mb-4">Identify low-performing segments and uncover upsell opportunities through advanced analytics insights.</p>
|
| 70 |
+
<a href="/underperformance" class="text-blue-500 hover:underline">Learn More</a>
|
| 71 |
+
</div>
|
| 72 |
+
<div class="md:w-1/2">
|
| 73 |
+
<img src="static\images\underperforming.png" alt="Underperformance Detector" class="w-full rounded-lg shadow-lg">
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<!-- Machine Failure -->
|
| 78 |
+
<div class="flex flex-col md:flex-row-reverse items-center my-12 animate-on-scroll">
|
| 79 |
+
<div class="md:w-1/2 md:pl-8">
|
| 80 |
+
<h2 class="text-3xl font-bold mb-4">Machine Failure Predictor</h2>
|
| 81 |
+
<p class="mb-4">Predict machine failure before breakdown.</p>
|
| 82 |
+
<a href="/predict/machine_failure" class="text-blue-500 hover:underline">Learn More</a>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="md:w-1/2">
|
| 85 |
+
<img src="static\images\machine.jpeg" alt="Underperformance Detector" class="w-full rounded-lg shadow-lg">
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<!-- Supply Failure -->
|
| 90 |
+
<div class="flex flex-col md:flex-row items-center my-12 animate-on-scroll">
|
| 91 |
+
<div class="md:w-1/2 md:pr-8">
|
| 92 |
+
<h2 class="text-3xl font-bold mb-4">Supply Chain Failure Predictor</h2>
|
| 93 |
+
<p class="mb-4">Predict supply chain failure before breakdown.</p>
|
| 94 |
+
<a href="/predict/supply_failure" class="text-blue-500 hover:underline">Learn More</a>
|
| 95 |
+
</div>
|
| 96 |
+
<div class="md:w-1/2">
|
| 97 |
+
<img src="static\images\supply.jpeg" alt="Underperformance Detector" class="w-full rounded-lg shadow-lg">
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
</div>
|
| 103 |
+
</section>
|
| 104 |
+
{% endblock %}
|
templates/inventory_optimization.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<section>
|
| 4 |
+
<h1 class="text-3xl font-bold mb-4">Inventory Optimization</h1>
|
| 5 |
+
<p class="mb-6">Minimize stockouts and reduce surplus by leveraging demand forecasting and optimization algorithms.</p>
|
| 6 |
+
<div class="bg-white p-6 rounded shadow">
|
| 7 |
+
<!-- Future dashboard elements for inventory planning -->
|
| 8 |
+
<p>[Inventory optimization dashboard components here...]</p>
|
| 9 |
+
</div>
|
| 10 |
+
</section>
|
| 11 |
+
{% endblock %}
|
templates/machine_failure.html
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<section class="py-8">
|
| 4 |
+
<h1 class="text-3xl font-bold mb-4">Machine Failure Prediction</h1>
|
| 5 |
+
|
| 6 |
+
<div class="bg-gray-50 p-6 rounded shadow mb-6">
|
| 7 |
+
<h2 class="text-2xl font-semibold mb-2">Upload Machine Failure Data (CSV)</h2>
|
| 8 |
+
<form action="{{ url_for('machine_failure.upload_file') }}" method="POST" enctype="multipart/form-data">
|
| 9 |
+
<input type="file" name="machine_file" accept=".csv" class="mb-4 block">
|
| 10 |
+
<button type="submit" class="btn-learn-more">Upload</button>
|
| 11 |
+
</form>
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
{% if preview_data %}
|
| 15 |
+
<div class="bg-white p-6 rounded shadow mb-6">
|
| 16 |
+
<h2 class="text-2xl font-semibold mb-4">Data Preview (First 5 Rows)</h2>
|
| 17 |
+
<div class="overflow-x-auto">
|
| 18 |
+
<table id="preview-table" class="min-w-full table-auto">
|
| 19 |
+
<thead>
|
| 20 |
+
<tr>
|
| 21 |
+
{% for column in columns %}
|
| 22 |
+
<th class="px-4 py-2 bg-gray-100">{{ column }}</th>
|
| 23 |
+
{% endfor %}
|
| 24 |
+
</tr>
|
| 25 |
+
</thead>
|
| 26 |
+
<tbody>
|
| 27 |
+
{% for row in preview_data %}
|
| 28 |
+
<tr>
|
| 29 |
+
{% for column in columns %}
|
| 30 |
+
<td class="border px-4 py-2">{{ row[column] }}</td>
|
| 31 |
+
{% endfor %}
|
| 32 |
+
</tr>
|
| 33 |
+
{% endfor %}
|
| 34 |
+
</tbody>
|
| 35 |
+
</table>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div class="bg-white p-6 rounded shadow">
|
| 40 |
+
<h2 class="text-2xl font-semibold mb-4">Summary Statistics</h2>
|
| 41 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 42 |
+
<div class="p-4 bg-blue-50 rounded">
|
| 43 |
+
<p><strong>Total Rows:</strong> <span class="summary-total-rows">{{ summary_stats.total_rows }}</span></p>
|
| 44 |
+
<p><strong>Total Columns:</strong> <span class="summary-total-cols">{{ summary_stats.total_columns }}</span></p>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="p-4 bg-green-50 rounded">
|
| 47 |
+
<p><strong>Numeric Columns:</strong> <span class="summary-numeric-cols">{{ summary_stats.numeric_columns|join(', ') }}</span></p>
|
| 48 |
+
<p><strong>Categorical Columns:</strong> <span class="summary-categorical-cols">{{ summary_stats.categorical_columns|join(', ') }}</span></p>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<div id="feature-importance" class="bg-white p-6 rounded shadow mt-6 hidden">
|
| 54 |
+
<h2 class="text-2xl font-semibold mb-4">Top 5 Feature Importances</h2>
|
| 55 |
+
<div id="feature-importance-list"></div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="bg-white p-6 rounded shadow mt-6">
|
| 59 |
+
<h2 class="text-2xl font-semibold mb-4">Failure Prediction Metrics</h2>
|
| 60 |
+
<div class="flex flex-wrap gap-4 mb-4">
|
| 61 |
+
<div>
|
| 62 |
+
<label class="block font-semibold mb-1">Target Column</label>
|
| 63 |
+
<select id="predictTargetCol" class="border rounded p-2">
|
| 64 |
+
{% for column in columns %}
|
| 65 |
+
<option value="{{ column }}">{{ column }}</option>
|
| 66 |
+
{% endfor %}
|
| 67 |
+
</select>
|
| 68 |
+
</div>
|
| 69 |
+
<div>
|
| 70 |
+
<label class="block font-semibold mb-1">Model</label>
|
| 71 |
+
<select id="predictModel" class="border rounded p-2">
|
| 72 |
+
<option value="Random Forest">Random Forest</option>
|
| 73 |
+
<option value="XGBoost">XGBoost</option>
|
| 74 |
+
<option value="Logistic Regression">Logistic Regression</option>
|
| 75 |
+
</select>
|
| 76 |
+
</div>
|
| 77 |
+
<div class="flex items-end">
|
| 78 |
+
<button onclick="runPrediction()" class="btn-learn-more">Run Prediction</button>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
<div id="predict-metrics" class="mb-4"></div>
|
| 82 |
+
<div id="predict-error" class="text-red-600 font-semibold mt-2"></div>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
<div id="single-prediction-section" class="bg-white p-6 rounded shadow mt-6 hidden">
|
| 86 |
+
<h2 class="text-2xl font-semibold mb-4">Predict for a Single Instance</h2>
|
| 87 |
+
<form id="single-prediction-form" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
|
| 88 |
+
</form>
|
| 89 |
+
<button onclick="predictSingleInstance()" class="btn-learn-more">Predict</button>
|
| 90 |
+
<div id="single-prediction-result" class="mt-4 p-4 bg-blue-100 rounded hidden">
|
| 91 |
+
<p class="font-semibold text-lg">Prediction: <span id="prediction-output" class="font-bold text-blue-800"></span></p>
|
| 92 |
+
<div id="probability-output" class="mt-2 text-sm"></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div id="single-prediction-error" class="text-red-600 font-semibold mt-2"></div>
|
| 95 |
+
</div>
|
| 96 |
+
{% endif %}
|
| 97 |
+
|
| 98 |
+
</section>
|
| 99 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 100 |
+
<script src="{{ url_for('static', filename='index.js') }}"></script>
|
| 101 |
+
{% endblock %}
|
templates/pricing_intelligence.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<section>
|
| 4 |
+
<h1 class="text-3xl font-bold mb-4">Pricing Intelligence</h1>
|
| 5 |
+
<p class="mb-6">Discover optimal pricing strategies based on price elasticity and conversion analysis to maximize revenue.</p>
|
| 6 |
+
<div class="bg-white p-6 rounded shadow">
|
| 7 |
+
<!-- Future pricing analytics components -->
|
| 8 |
+
<p>[Pricing intelligence dashboard components here...]</p>
|
| 9 |
+
</div>
|
| 10 |
+
</section>
|
| 11 |
+
{% endblock %}
|
templates/sales_prediction.html
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<section class="py-8">
|
| 4 |
+
<h1 class="text-3xl font-bold mb-4">Sales Prediction Engine</h1>
|
| 5 |
+
|
| 6 |
+
<!-- File Upload Form -->
|
| 7 |
+
<div class="bg-gray-50 p-6 rounded shadow mb-6">
|
| 8 |
+
<h2 class="text-2xl font-semibold mb-2">Upload Sales Data (CSV)</h2>
|
| 9 |
+
<form action="{{ url_for('sales_prediction.upload_file') }}" method="POST" enctype="multipart/form-data">
|
| 10 |
+
<input type="file" name="sales_file" accept=".csv" class="mb-4 block">
|
| 11 |
+
<button type="submit" class="btn-learn-more">Upload</button>
|
| 12 |
+
</form>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
{% if preview_data %}
|
| 16 |
+
<!-- Data Preview -->
|
| 17 |
+
<div class="bg-white p-6 rounded shadow mb-6">
|
| 18 |
+
<h2 class="text-2xl font-semibold mb-4">Data Preview (First 5 Rows)</h2>
|
| 19 |
+
<div class="overflow-x-auto">
|
| 20 |
+
<table id="preview-table" class="min-w-full table-auto">
|
| 21 |
+
<thead>
|
| 22 |
+
<tr>
|
| 23 |
+
{% for column in columns %}
|
| 24 |
+
<th class="px-4 py-2 bg-gray-100">{{ column }}</th>
|
| 25 |
+
{% endfor %}
|
| 26 |
+
</tr>
|
| 27 |
+
</thead>
|
| 28 |
+
<tbody>
|
| 29 |
+
{% for row in preview_data %}
|
| 30 |
+
<tr>
|
| 31 |
+
{% for column in columns %}
|
| 32 |
+
<td class="border px-4 py-2">{{ row[column] }}</td>
|
| 33 |
+
{% endfor %}
|
| 34 |
+
</tr>
|
| 35 |
+
{% endfor %}
|
| 36 |
+
</tbody>
|
| 37 |
+
</table>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<!-- Summary Statistics -->
|
| 42 |
+
<div class="bg-white p-6 rounded shadow">
|
| 43 |
+
<h2 class="text-2xl font-semibold mb-4">Summary Statistics</h2>
|
| 44 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 45 |
+
<div class="p-4 bg-blue-50 rounded">
|
| 46 |
+
<p><strong>Total Rows:</strong> <span class="summary-total-rows">{{ summary_stats.total_rows }}</span></p>
|
| 47 |
+
<p><strong>Total Columns:</strong> <span class="summary-total-cols">{{ summary_stats.total_columns }}</span></p>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="p-4 bg-green-50 rounded">
|
| 50 |
+
<p><strong>Numeric Columns:</strong> <span class="summary-numeric-cols">{{ summary_stats.numeric_columns|join(', ') }}</span></p>
|
| 51 |
+
<p><strong>Categorical Columns:</strong> <span class="summary-categorical-cols">{{ summary_stats.categorical_columns|join(', ') }}</span></p>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<!-- Missing Values Summary & No-code Fix UI -->
|
| 56 |
+
{% set null_columns = [] %}
|
| 57 |
+
{% for column, count in summary_stats.missing_values.items() %}
|
| 58 |
+
{% if count > 0 %}
|
| 59 |
+
{% set _ = null_columns.append((column, count)) %}
|
| 60 |
+
{% endif %}
|
| 61 |
+
{% endfor %}
|
| 62 |
+
<div class="mt-4 p-4 bg-yellow-50 rounded">
|
| 63 |
+
<h3 class="font-semibold mb-2">Missing Values</h3>
|
| 64 |
+
<ul id="null-list">
|
| 65 |
+
{% for column, count in null_columns %}
|
| 66 |
+
<li>
|
| 67 |
+
<span>{{ column }}: {{ count }} missing values</span>
|
| 68 |
+
<select id="method-{{ column }}" class="border rounded p-1 mx-2">
|
| 69 |
+
<option value="drop">Drop Rows</option>
|
| 70 |
+
<option value="mean">Fill Mean</option>
|
| 71 |
+
<option value="median">Fill Median</option>
|
| 72 |
+
<option value="mode">Fill Mode</option>
|
| 73 |
+
</select>
|
| 74 |
+
<button class="btn-learn-more" onclick="fixNulls('{{ column }}')">Fix</button>
|
| 75 |
+
</li>
|
| 76 |
+
{% endfor %}
|
| 77 |
+
</ul>
|
| 78 |
+
<div id="null-fix-message" class="mt-2 text-green-700 font-semibold"></div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<!-- Data Visualization -->
|
| 83 |
+
<div class="bg-white p-6 rounded shadow mt-6">
|
| 84 |
+
<h2 class="text-2xl font-semibold mb-4">Data Visualization</h2>
|
| 85 |
+
<div class="flex gap-4 mb-4">
|
| 86 |
+
<select id="plotColumn" class="border rounded p-2">
|
| 87 |
+
{% for column in columns %}
|
| 88 |
+
<option value="{{ column }}">{{ column }}</option>
|
| 89 |
+
{% endfor %}
|
| 90 |
+
</select>
|
| 91 |
+
<select id="plotType" class="border rounded p-2">
|
| 92 |
+
<option value="histogram">Histogram (Numeric)</option>
|
| 93 |
+
<option value="box">Box Plot (Numeric)</option>
|
| 94 |
+
<option value="bar">Bar Plot (Categorical)</option>
|
| 95 |
+
</select>
|
| 96 |
+
<button onclick="createPlot()" class="btn-learn-more">Generate Plot</button>
|
| 97 |
+
</div>
|
| 98 |
+
<div id="plotDiv" class="w-full h-96"></div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<!-- Forecasting -->
|
| 102 |
+
<div class="bg-white p-6 rounded shadow mt-6">
|
| 103 |
+
<h2 class="text-2xl font-semibold mb-4">Forecasting</h2>
|
| 104 |
+
<div class="flex flex-wrap gap-4 mb-4">
|
| 105 |
+
<div>
|
| 106 |
+
<label class="block font-semibold mb-1">Date Column</label>
|
| 107 |
+
<select id="forecastDateCol" class="border rounded p-2">
|
| 108 |
+
{% for column in columns %}
|
| 109 |
+
<option value="{{ column }}">{{ column }}</option>
|
| 110 |
+
{% endfor %}
|
| 111 |
+
</select>
|
| 112 |
+
</div>
|
| 113 |
+
<div>
|
| 114 |
+
<label class="block font-semibold mb-1">Target Column</label>
|
| 115 |
+
<select id="forecastTargetCol" class="border rounded p-2">
|
| 116 |
+
{% for column in columns %}
|
| 117 |
+
<option value="{{ column }}">{{ column }}</option>
|
| 118 |
+
{% endfor %}
|
| 119 |
+
</select>
|
| 120 |
+
</div>
|
| 121 |
+
<div>
|
| 122 |
+
<label class="block font-semibold mb-1">Model</label>
|
| 123 |
+
<select id="forecastModel" class="border rounded p-2">
|
| 124 |
+
<option value="ARIMA">ARIMA</option>
|
| 125 |
+
<option value="Prophet">Prophet</option>
|
| 126 |
+
<option value="Random Forest">Random Forest</option>
|
| 127 |
+
<option value="XGBoost">XGBoost</option>
|
| 128 |
+
<option value="LSTM">LSTM</option>
|
| 129 |
+
</select>
|
| 130 |
+
</div>
|
| 131 |
+
<div>
|
| 132 |
+
<label class="block font-semibold mb-1">Forecast Horizon</label>
|
| 133 |
+
<input id="forecastHorizon" type="number" value="30" min="1" max="365" class="border rounded p-2 w-20">
|
| 134 |
+
</div>
|
| 135 |
+
<div class="flex items-end">
|
| 136 |
+
<button onclick="runForecast()" class="btn-learn-more">Run Forecast</button>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
<div id="forecast-metrics" class="mb-4"></div>
|
| 140 |
+
<div id="forecast-plot" class="w-full h-96"></div>
|
| 141 |
+
<div id="forecast-error" class="text-red-600 font-semibold mt-2"></div>
|
| 142 |
+
</div>
|
| 143 |
+
{% endif %}
|
| 144 |
+
</section>
|
| 145 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 146 |
+
<script src="{{ url_for('static', filename='index.js') }}"></script>
|
| 147 |
+
{% endblock %}
|
templates/supply_failure.html
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<section class="py-8">
|
| 4 |
+
<h1 class="text-3xl font-bold mb-4">Supply Failure Prediction</h1>
|
| 5 |
+
|
| 6 |
+
<div class="bg-gray-50 p-6 rounded shadow mb-6">
|
| 7 |
+
<h2 class="text-2xl font-semibold mb-2">Upload Supply Chain Data (CSV)</h2>
|
| 8 |
+
<form action="{{ url_for('supply_failure.upload_file_supply') }}" method="POST" enctype="multipart/form-data">
|
| 9 |
+
<input type="file" name="supply_file" accept=".csv" class="mb-4 block">
|
| 10 |
+
<button type="submit" class="btn-learn-more">Upload</button>
|
| 11 |
+
</form>
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
{% if preview_data %}
|
| 15 |
+
<div class="bg-white p-6 rounded shadow mb-6">
|
| 16 |
+
<h2 class="text-2xl font-semibold mb-4">Data Preview (First 5 Rows)</h2>
|
| 17 |
+
<div class="overflow-x-auto">
|
| 18 |
+
<table id="preview-table" class="min-w-full table-auto">
|
| 19 |
+
<thead>
|
| 20 |
+
<tr>
|
| 21 |
+
{% for column in columns %}
|
| 22 |
+
<th class="px-4 py-2 bg-gray-100">{{ column }}</th>
|
| 23 |
+
{% endfor %}
|
| 24 |
+
</tr>
|
| 25 |
+
</thead>
|
| 26 |
+
<tbody>
|
| 27 |
+
{% for row in preview_data %}
|
| 28 |
+
<tr>
|
| 29 |
+
{% for column in columns %}
|
| 30 |
+
<td class="border px-4 py-2">{{ row[column] }}</td>
|
| 31 |
+
{% endfor %}
|
| 32 |
+
</tr>
|
| 33 |
+
{% endfor %}
|
| 34 |
+
</tbody>
|
| 35 |
+
</table>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div class="bg-white p-6 rounded shadow">
|
| 40 |
+
<h2 class="text-2xl font-semibold mb-4">Summary Statistics</h2>
|
| 41 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 42 |
+
<div class="p-4 bg-blue-50 rounded">
|
| 43 |
+
<p><strong>Total Rows:</strong> <span class="summary-total-rows">{{ summary_stats.total_rows }}</span></p>
|
| 44 |
+
<p><strong>Total Columns:</strong> <span class="summary-total-cols">{{ summary_stats.total_columns }}</span></p>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="p-4 bg-green-50 rounded">
|
| 47 |
+
<p><strong>Numeric Columns:</strong> <span class="summary-numeric-cols">{{ summary_stats.numeric_columns|join(', ') }}</span></p>
|
| 48 |
+
<p><strong>Categorical Columns:</strong> <span class="summary-categorical-cols">{{ summary_stats.categorical_columns|join(', ') }}</span></p>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<div id="feature-importance" class="bg-white p-6 rounded shadow mt-6 hidden">
|
| 54 |
+
<h2 class="text-2xl font-semibold mb-4">Top 5 Feature Importances</h2>
|
| 55 |
+
<div id="feature-importance-list"></div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="bg-white p-6 rounded shadow mt-6">
|
| 59 |
+
<h2 class="text-2xl font-semibold mb-4">Supply Failure Prediction Metrics</h2>
|
| 60 |
+
<div class="flex flex-wrap gap-4 mb-4">
|
| 61 |
+
<div>
|
| 62 |
+
<label class="block font-semibold mb-1">Target Column</label>
|
| 63 |
+
<select id="predictTargetColSupply" class="border rounded p-2" disabled>
|
| 64 |
+
<option value="failure_flag">failure_flag</option>
|
| 65 |
+
</select>
|
| 66 |
+
</div>
|
| 67 |
+
<div>
|
| 68 |
+
<label class="block font-semibold mb-1">Model</label>
|
| 69 |
+
<select id="predictModelSupply" class="border rounded p-2">
|
| 70 |
+
<option value="Random Forest">Random Forest</option>
|
| 71 |
+
<option value="XGBoost">XGBoost</option>
|
| 72 |
+
<option value="Logistic Regression">Logistic Regression</option>
|
| 73 |
+
</select>
|
| 74 |
+
</div>
|
| 75 |
+
<div class="flex items-end">
|
| 76 |
+
<button onclick="runSupplyPrediction()" class="btn-learn-more">Run Prediction</button>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
<div id="predict-metrics-supply" class="mb-4"></div>
|
| 80 |
+
<div id="predict-error-supply" class="text-red-600 font-semibold mt-2"></div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div id="single-prediction-section-supply" class="bg-white p-6 rounded shadow mt-6 hidden">
|
| 84 |
+
<h2 class="text-2xl font-semibold mb-4">Predict for a Single Supply Instance</h2>
|
| 85 |
+
<form id="single-prediction-form-supply" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
|
| 86 |
+
</form>
|
| 87 |
+
<button onclick="predictSupplySingleInstance()" class="btn-learn-more">Predict</button>
|
| 88 |
+
<div id="single-prediction-result-supply" class="mt-4 p-4 bg-blue-100 rounded hidden">
|
| 89 |
+
<p class="font-semibold text-lg">Prediction: <span id="prediction-output-supply" class="font-bold text-blue-800"></span></p>
|
| 90 |
+
<div id="probability-output-supply" class="mt-2 text-sm"></div>
|
| 91 |
+
</div>
|
| 92 |
+
<div id="single-prediction-error-supply" class="text-red-600 font-semibold mt-2"></div>
|
| 93 |
+
</div>
|
| 94 |
+
{% endif %}
|
| 95 |
+
|
| 96 |
+
</section>
|
| 97 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 98 |
+
<script src="{{ url_for('static', filename='index.js') }}"></script>
|
| 99 |
+
{% endblock %}
|
templates/underperformance.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% block content %}
|
| 3 |
+
<section>
|
| 4 |
+
<h1 class="text-3xl font-bold mb-4">Underperformance & Upsell Detector</h1>
|
| 5 |
+
<p class="mb-6">Identify products and segments that are underperforming along with upsell opportunities using advanced analytics.</p>
|
| 6 |
+
<div class="bg-white p-6 rounded shadow">
|
| 7 |
+
<!-- Future dashboard view for underperformance -->
|
| 8 |
+
<p>[Underperformance analysis components here...]</p>
|
| 9 |
+
</div>
|
| 10 |
+
</section>
|
| 11 |
+
{% endblock %}
|