Upload folder using huggingface_hub
Browse files- app.py +118 -0
- dashboard.html +140 -0
- train.py +68 -0
app.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, jsonify, render_template
|
| 2 |
+
import joblib, pandas as pd, numpy as np
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
import psycopg2
|
| 5 |
+
from psycopg2 import pool
|
| 6 |
+
|
| 7 |
+
app = Flask(_name_)
|
| 8 |
+
|
| 9 |
+
# ---------- Load model bundle ----------
|
| 10 |
+
bundle = joblib.load('gas_danger_model.pkl')
|
| 11 |
+
model = bundle['model']
|
| 12 |
+
FEATURES = bundle['features'] # ['Hour', 'Weekday', 'Month', 'Afternoon']
|
| 13 |
+
|
| 14 |
+
# ---------- PostgreSQL connection pool ----------
|
| 15 |
+
PG_POOL = pool.SimpleConnectionPool(
|
| 16 |
+
1, 5,
|
| 17 |
+
user="postgres.czytuqnowdogaowfnfhe",
|
| 18 |
+
password="235711",
|
| 19 |
+
host="aws-0-eu-central-1.pooler.supabase.com",
|
| 20 |
+
port="5432",
|
| 21 |
+
dbname="postgres"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# ---------- Per‑gas safety limits ----------
|
| 25 |
+
SAFETY_THRESHOLDS = {
|
| 26 |
+
'alcohol': 1000, 'NH3': 25, 'CO': 35, 'CO2': 5000,
|
| 27 |
+
'Toluene': 100, 'acetone': 750, 'lpg': 1000, 'smoke': 1.0
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
# ---------- Helper to fetch latest row ----------
|
| 31 |
+
def fetch_latest_reading():
|
| 32 |
+
"""
|
| 33 |
+
Returns a dict with keys exactly matching the ones used elsewhere:
|
| 34 |
+
CO2, NH3, alcohol, Toluene, acetone, lpg, CO, smoke, timestamp
|
| 35 |
+
"""
|
| 36 |
+
sql = """
|
| 37 |
+
SELECT co2,
|
| 38 |
+
nh3,
|
| 39 |
+
alcohol,
|
| 40 |
+
toluene,
|
| 41 |
+
acetone,
|
| 42 |
+
lpg,
|
| 43 |
+
co,
|
| 44 |
+
smoke,
|
| 45 |
+
"timestamp"
|
| 46 |
+
FROM gas_data
|
| 47 |
+
ORDER BY "timestamp" DESC
|
| 48 |
+
LIMIT 1;
|
| 49 |
+
"""
|
| 50 |
+
conn = PG_POOL.getconn()
|
| 51 |
+
try:
|
| 52 |
+
with conn.cursor() as cur:
|
| 53 |
+
cur.execute(sql)
|
| 54 |
+
row = cur.fetchone()
|
| 55 |
+
if row is None:
|
| 56 |
+
return None
|
| 57 |
+
raw_keys = ['co2','nh3','alcohol','toluene','acetone',
|
| 58 |
+
'lpg','co','smoke','timestamp']
|
| 59 |
+
raw = dict(zip(raw_keys, row))
|
| 60 |
+
# Map to mixed‑case keys expected elsewhere
|
| 61 |
+
return {
|
| 62 |
+
'CO2': raw['co2'],
|
| 63 |
+
'NH3': raw['nh3'],
|
| 64 |
+
'alcohol': raw['alcohol'],
|
| 65 |
+
'Toluene': raw['toluene'],
|
| 66 |
+
'acetone': raw['acetone'],
|
| 67 |
+
'lpg': raw['lpg'],
|
| 68 |
+
'CO': raw['co'],
|
| 69 |
+
'smoke': raw['smoke'],
|
| 70 |
+
'timestamp':raw['timestamp']
|
| 71 |
+
}
|
| 72 |
+
finally:
|
| 73 |
+
PG_POOL.putconn(conn)
|
| 74 |
+
|
| 75 |
+
# ---------- Routes ----------
|
| 76 |
+
@app.route('/api/predict', methods=['GET'])
|
| 77 |
+
def predict():
|
| 78 |
+
current = fetch_latest_reading()
|
| 79 |
+
if current is None:
|
| 80 |
+
return jsonify({'error': 'No rows in gas_data table'}), 404
|
| 81 |
+
|
| 82 |
+
ts = current['timestamp']
|
| 83 |
+
now = ts if ts else datetime.now()
|
| 84 |
+
|
| 85 |
+
# Add time‑based features
|
| 86 |
+
current.update({
|
| 87 |
+
'Hour': now.hour,
|
| 88 |
+
'Weekday': now.weekday(),
|
| 89 |
+
'Month': now.month,
|
| 90 |
+
'Afternoon': int(12 <= now.hour <= 15)
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
# Build feature vector in training order
|
| 94 |
+
X = pd.DataFrame([[current.get(f, np.nan) for f in FEATURES]], columns=FEATURES)
|
| 95 |
+
prob = float(model.predict_proba(X)[0, 1])
|
| 96 |
+
|
| 97 |
+
# Per‑gas alert block
|
| 98 |
+
alerts = {
|
| 99 |
+
g: {
|
| 100 |
+
'value': current[g],
|
| 101 |
+
'threshold': thr,
|
| 102 |
+
'status': 'danger' if current[g] > thr else 'safe'
|
| 103 |
+
} for g, thr in SAFETY_THRESHOLDS.items()
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return jsonify({
|
| 107 |
+
'prediction': prob,
|
| 108 |
+
'alerts': alerts,
|
| 109 |
+
'overall_status': 'danger' if prob > 0.4 else 'safe',
|
| 110 |
+
'timestamp': now.isoformat()
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
@app.route('/')
|
| 114 |
+
def dashboard():
|
| 115 |
+
return render_template('dashboard.html') # supply your own template
|
| 116 |
+
|
| 117 |
+
if _name_ == '_main_':
|
| 118 |
+
app.run(debug=True, port=5000)
|
dashboard.html
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<title>Gas Danger Monitoring</title>
|
| 5 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 6 |
+
<style>
|
| 7 |
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
| 8 |
+
.dashboard { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
|
| 9 |
+
.card { background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 15px; }
|
| 10 |
+
.danger { background: #ffcccc; border-left: 5px solid red; }
|
| 11 |
+
.safe { background: #ccffcc; border-left: 5px solid green; }
|
| 12 |
+
.gauge { height: 20px; background: #ddd; border-radius: 10px; margin: 5px 0; }
|
| 13 |
+
.gauge-fill { height: 100%; border-radius: 10px; }
|
| 14 |
+
</style>
|
| 15 |
+
</head>
|
| 16 |
+
<body>
|
| 17 |
+
<h1>Factory Gas Monitoring</h1>
|
| 18 |
+
<div class="dashboard">
|
| 19 |
+
<div>
|
| 20 |
+
<div class="card" id="status-card">
|
| 21 |
+
<h2>System Status: <span id="overall-status">Loading...</span></h2>
|
| 22 |
+
<p>Danger Probability: <span id="danger-prob">-</span></p>
|
| 23 |
+
<div class="gauge">
|
| 24 |
+
<div class="gauge-fill" id="danger-gauge"></div>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="card">
|
| 29 |
+
<h2>Gas Levels vs Safety Thresholds</h2>
|
| 30 |
+
<canvas id="gasChart" height="300"></canvas>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div>
|
| 35 |
+
<div class="card">
|
| 36 |
+
<h2>Alerts</h2>
|
| 37 |
+
<div id="alerts-container"></div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<div class="card">
|
| 41 |
+
<h2>Key Metrics</h2>
|
| 42 |
+
<div id="metrics">
|
| 43 |
+
<p>Last Updated: <span id="timestamp">-</span></p>
|
| 44 |
+
<p>Top Risk Factor: <span id="top-risk">CO_rolling_12h</span></p>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<script>
|
| 51 |
+
let gasChart;
|
| 52 |
+
|
| 53 |
+
// Fetch data every 5 seconds
|
| 54 |
+
function updateData() {
|
| 55 |
+
fetch('/api/predict')
|
| 56 |
+
.then(response => response.json())
|
| 57 |
+
.then(data => {
|
| 58 |
+
// Update overall status
|
| 59 |
+
document.getElementById('overall-status').textContent =
|
| 60 |
+
data.overall_status.toUpperCase();
|
| 61 |
+
document.getElementById('overall-status').className = data.overall_status;
|
| 62 |
+
|
| 63 |
+
// Update danger probability
|
| 64 |
+
const dangerPercent = (data.prediction * 100).toFixed(1);
|
| 65 |
+
document.getElementById('danger-prob').textContent = ${dangerPercent}%;
|
| 66 |
+
document.getElementById('danger-gauge').style.width = ${dangerPercent}%;
|
| 67 |
+
document.getElementById('danger-gauge').style.backgroundColor =
|
| 68 |
+
data.prediction > 0.4 ? 'red' : 'green';
|
| 69 |
+
|
| 70 |
+
// Update timestamp
|
| 71 |
+
document.getElementById('timestamp').textContent =
|
| 72 |
+
new Date(data.timestamp).toLocaleString();
|
| 73 |
+
|
| 74 |
+
// Update alerts
|
| 75 |
+
const alertsContainer = document.getElementById('alerts-container');
|
| 76 |
+
alertsContainer.innerHTML = '';
|
| 77 |
+
for (const [gas, info] of Object.entries(data.alerts)) {
|
| 78 |
+
const alertDiv = document.createElement('div');
|
| 79 |
+
alertDiv.className = card ${info.status};
|
| 80 |
+
alertDiv.innerHTML = `
|
| 81 |
+
<h3>${gas}</h3>
|
| 82 |
+
<p>${info.value.toFixed(2)} ppm (Threshold: ${info.threshold} ppm)</p>
|
| 83 |
+
<p>Status: <strong>${info.status.toUpperCase()}</strong></p>
|
| 84 |
+
`;
|
| 85 |
+
alertsContainer.appendChild(alertDiv);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Update chart
|
| 89 |
+
updateChart(data.alerts);
|
| 90 |
+
});
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
function updateChart(alerts) {
|
| 94 |
+
const gases = Object.keys(alerts);
|
| 95 |
+
const values = gases.map(gas => alerts[gas].value);
|
| 96 |
+
const thresholds = gases.map(gas => alerts[gas].threshold);
|
| 97 |
+
|
| 98 |
+
if (gasChart) {
|
| 99 |
+
gasChart.data.datasets[0].data = values;
|
| 100 |
+
gasChart.data.datasets[1].data = thresholds;
|
| 101 |
+
gasChart.update();
|
| 102 |
+
} else {
|
| 103 |
+
const ctx = document.getElementById('gasChart').getContext('2d');
|
| 104 |
+
gasChart = new Chart(ctx, {
|
| 105 |
+
type: 'bar',
|
| 106 |
+
data: {
|
| 107 |
+
labels: gases,
|
| 108 |
+
datasets: [
|
| 109 |
+
{
|
| 110 |
+
label: 'Current Level (ppm)',
|
| 111 |
+
data: values,
|
| 112 |
+
backgroundColor: values.map((v, i) =>
|
| 113 |
+
v > thresholds[i] ? 'rgba(255, 99, 132, 0.7)' : 'rgba(54, 162, 235, 0.7)'
|
| 114 |
+
)
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
label: 'Safety Threshold',
|
| 118 |
+
data: thresholds,
|
| 119 |
+
type: 'line',
|
| 120 |
+
borderColor: 'rgba(255, 159, 64, 1)',
|
| 121 |
+
borderWidth: 2,
|
| 122 |
+
fill: false
|
| 123 |
+
}
|
| 124 |
+
]
|
| 125 |
+
},
|
| 126 |
+
options: {
|
| 127 |
+
scales: {
|
| 128 |
+
y: { beginAtZero: true, title: { display: true, text: 'Concentration (ppm)' } }
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
});
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Initial load
|
| 136 |
+
updateData();
|
| 137 |
+
setInterval(updateData, 5000); // Refresh every 5 seconds
|
| 138 |
+
</script>
|
| 139 |
+
</body>
|
| 140 |
+
</html>
|
train.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import joblib
|
| 4 |
+
from sklearn.pipeline import make_pipeline
|
| 5 |
+
from sklearn.preprocessing import StandardScaler
|
| 6 |
+
from xgboost import XGBClassifier
|
| 7 |
+
from sklearn.metrics import classification_report
|
| 8 |
+
|
| 9 |
+
# ---------------- Load & preprocess ----------------
|
| 10 |
+
df = pd.read_csv('../city_day_clean_dated.csv', parse_dates=['Date']).sort_values('Date')
|
| 11 |
+
|
| 12 |
+
# Fill missing hours
|
| 13 |
+
full = pd.DataFrame(pd.date_range(df['Date'].min(), df['Date'].max(), freq='h'), # 'H' → 'h'
|
| 14 |
+
columns=['DateTime'])
|
| 15 |
+
df = full.merge(df, left_on=full['DateTime'].dt.date,
|
| 16 |
+
right_on=df['Date'].dt.date, how='left').ffill()
|
| 17 |
+
df['Date'] = df['DateTime']
|
| 18 |
+
|
| 19 |
+
# Filter numeric gas columns only (avoid 'DateTime', etc.)
|
| 20 |
+
gas_cols = ['alcohol', 'NH3', 'CO', 'CO2', 'Toluene', 'acetone', 'lpg', 'smoke']
|
| 21 |
+
|
| 22 |
+
# Create danger flags
|
| 23 |
+
danger_thresholds = {col: np.percentile(df[col].dropna(), 95) for col in gas_cols}
|
| 24 |
+
for col in gas_cols:
|
| 25 |
+
df[f'{col}_danger'] = (df[col] > danger_thresholds[col]).astype(int)
|
| 26 |
+
|
| 27 |
+
df['Danger'] = df[[f'{col}_danger' for col in gas_cols]].max(axis=1)
|
| 28 |
+
|
| 29 |
+
# Time features
|
| 30 |
+
df['Hour'] = df['DateTime'].dt.hour
|
| 31 |
+
df['Weekday'] = df['DateTime'].dt.weekday
|
| 32 |
+
df['Month'] = df['DateTime'].dt.month
|
| 33 |
+
df['Afternoon'] = ((df['Hour'] >= 12) & (df['Hour'] <= 15)).astype(int)
|
| 34 |
+
|
| 35 |
+
# Simple demo features
|
| 36 |
+
features = ['Hour', 'Weekday', 'Month', 'Afternoon']
|
| 37 |
+
target = 'Danger'
|
| 38 |
+
|
| 39 |
+
train = df[df['DateTime'] < '2020-01-01']
|
| 40 |
+
test = df[df['DateTime'] >= '2020-01-01']
|
| 41 |
+
|
| 42 |
+
X_train, y_train = train[features], train[target]
|
| 43 |
+
X_test, y_test = test[features], test[target]
|
| 44 |
+
|
| 45 |
+
# ---------------- Build model ----------------
|
| 46 |
+
model = make_pipeline(
|
| 47 |
+
StandardScaler(),
|
| 48 |
+
XGBClassifier(
|
| 49 |
+
n_estimators=200,
|
| 50 |
+
max_depth=5,
|
| 51 |
+
learning_rate=0.1,
|
| 52 |
+
subsample=0.8,
|
| 53 |
+
colsample_bytree=0.8,
|
| 54 |
+
scale_pos_weight=10,
|
| 55 |
+
random_state=42,
|
| 56 |
+
eval_metric='logloss'
|
| 57 |
+
)
|
| 58 |
+
)
|
| 59 |
+
model.fit(X_train, y_train)
|
| 60 |
+
|
| 61 |
+
print('Training report:')
|
| 62 |
+
print(classification_report(y_train, model.predict(X_train)))
|
| 63 |
+
print('\nTest report:')
|
| 64 |
+
print(classification_report(y_test, model.predict(X_test)))
|
| 65 |
+
|
| 66 |
+
# ---------------- Save bundle ----------------
|
| 67 |
+
joblib.dump({'model': model, 'features': features}, 'gas_danger_model.pkl')
|
| 68 |
+
print('✔️ Model saved to gas_danger_model.pkl')
|