Spaces:
Running
Running
Initial upload of SmartWatt Backend API
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +140 -0
- .gitattributes +4 -34
- .gitignore +4 -0
- .python-version +1 -0
- Analysis_Tools/analyze_dataset_issues.py +93 -0
- Analysis_Tools/evaluate_all_models.py +298 -0
- Analysis_Tools/show_real_data.py +29 -0
- Analysis_Tools/test_auto_train_local.py +127 -0
- Analysis_Tools/test_clean_architecture.py +97 -0
- Analysis_Tools/verify_patterns.py +84 -0
- Dockerfile +35 -0
- LICENSE +21 -0
- Migration_Scripts/migrate_kwh_columns.py +158 -0
- Migration_Scripts/scheduled_self_learning.py +150 -0
- Procfile +1 -0
- README.md +104 -11
- Training_Scripts/future_train.py +1093 -0
- Training_Scripts/newdataset.py +1008 -0
- Training_Scripts/train(latest).py +644 -0
- Training_Scripts/train(old).py +348 -0
- Training_Scripts/train_demo.py +470 -0
- Training_Scripts/train_with_logging.py +164 -0
- anomaly_engine.py +39 -0
- kseb_tariff.py +191 -0
- learned_params.json +12 -0
- learning/__init__.py +0 -0
- learning/core.py +76 -0
- learning/memory.py +62 -0
- learning/pipeline.py +60 -0
- main.py +86 -0
- models/ac_model.keras +3 -0
- models/ac_preprocessor.pkl +3 -0
- models/all_models_evaluation_summary.txt +210 -0
- models/ceiling_fan_model.keras +3 -0
- models/ceiling_fan_preprocessor.pkl +3 -0
- models/cfl_bulb_preprocessor.pkl +3 -0
- models/cfl_lights_model.keras +3 -0
- models/cfl_lights_preprocessor.pkl +3 -0
- models/desktop_model.keras +3 -0
- models/desktop_preprocessor.pkl +3 -0
- models/food_processor_model.keras +3 -0
- models/food_processor_preprocessor.pkl +3 -0
- models/fridge_model.keras +3 -0
- models/fridge_preprocessor.pkl +3 -0
- models/hair_dryer_model.keras +3 -0
- models/hair_dryer_preprocessor.pkl +3 -0
- models/induction_model.keras +3 -0
- models/induction_preprocessor.pkl +3 -0
- models/iron_model.keras +3 -0
- models/iron_preprocessor.pkl +3 -0
.env.example
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =====================================================
|
| 2 |
+
# SmartWatt Backend - Environment Configuration
|
| 3 |
+
# =====================================================
|
| 4 |
+
#
|
| 5 |
+
# Instructions:
|
| 6 |
+
# 1. Copy this file to .env
|
| 7 |
+
# 2. Fill in your actual values
|
| 8 |
+
# 3. Never commit .env to version control
|
| 9 |
+
#
|
| 10 |
+
# =====================================================
|
| 11 |
+
|
| 12 |
+
# ============================
|
| 13 |
+
# Server Configuration
|
| 14 |
+
# ============================
|
| 15 |
+
|
| 16 |
+
# Server host (use 0.0.0.0 to allow external connections)
|
| 17 |
+
HOST=0.0.0.0
|
| 18 |
+
|
| 19 |
+
# Server port
|
| 20 |
+
PORT=8000
|
| 21 |
+
|
| 22 |
+
# Environment mode (development | staging | production)
|
| 23 |
+
ENVIRONMENT=development
|
| 24 |
+
|
| 25 |
+
# Debug mode (true | false) - Disable in production!
|
| 26 |
+
DEBUG=true
|
| 27 |
+
|
| 28 |
+
# ============================
|
| 29 |
+
# CORS Configuration
|
| 30 |
+
# ============================
|
| 31 |
+
|
| 32 |
+
# Allowed origins (comma-separated list of URLs)
|
| 33 |
+
# Add your frontend URL(s) here
|
| 34 |
+
# Example: http://localhost:3000,https://yourdomain.com
|
| 35 |
+
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,https://smartwatt-frontend.vercel.app
|
| 36 |
+
|
| 37 |
+
# ============================
|
| 38 |
+
# Database Configuration
|
| 39 |
+
# ============================
|
| 40 |
+
|
| 41 |
+
# Supabase Configuration (Optional - for cloud training data storage)
|
| 42 |
+
# Sign up at https://supabase.com to get these credentials
|
| 43 |
+
SUPABASE_URL=https://your-project.supabase.co
|
| 44 |
+
SUPABASE_KEY=your-anon-key-here
|
| 45 |
+
SUPABASE_SERVICE_KEY=your-service-role-key-here
|
| 46 |
+
|
| 47 |
+
# ============================
|
| 48 |
+
# AI/ML Configuration
|
| 49 |
+
# ============================
|
| 50 |
+
|
| 51 |
+
# Model confidence threshold (0.0 - 1.0)
|
| 52 |
+
# Predictions below this confidence will trigger warnings
|
| 53 |
+
MODEL_CONFIDENCE_THRESHOLD=0.85
|
| 54 |
+
|
| 55 |
+
# Enable/disable model caching for faster predictions
|
| 56 |
+
ENABLE_MODEL_CACHING=true
|
| 57 |
+
|
| 58 |
+
# Maximum batch size for /predict-all endpoint
|
| 59 |
+
MAX_BATCH_SIZE=100
|
| 60 |
+
|
| 61 |
+
# ============================
|
| 62 |
+
# Logging Configuration
|
| 63 |
+
# ============================
|
| 64 |
+
|
| 65 |
+
# Log level (DEBUG | INFO | WARNING | ERROR | CRITICAL)
|
| 66 |
+
LOG_LEVEL=INFO
|
| 67 |
+
|
| 68 |
+
# Enable request logging
|
| 69 |
+
LOG_REQUESTS=true
|
| 70 |
+
|
| 71 |
+
# Log file path (leave empty to disable file logging)
|
| 72 |
+
LOG_FILE_PATH=logs/smartwatt.log
|
| 73 |
+
|
| 74 |
+
# ============================
|
| 75 |
+
# Security Configuration
|
| 76 |
+
# ============================
|
| 77 |
+
|
| 78 |
+
# API Key for protected endpoints (optional)
|
| 79 |
+
# API_KEY=your-secret-api-key-here
|
| 80 |
+
|
| 81 |
+
# Rate limiting (requests per minute)
|
| 82 |
+
RATE_LIMIT=60
|
| 83 |
+
|
| 84 |
+
# Enable HTTPS redirect in production
|
| 85 |
+
FORCE_HTTPS=false
|
| 86 |
+
|
| 87 |
+
# ============================
|
| 88 |
+
# External Services
|
| 89 |
+
# ============================
|
| 90 |
+
|
| 91 |
+
# Weather API (optional - for temperature-based predictions)
|
| 92 |
+
# WEATHER_API_KEY=your-openweather-api-key
|
| 93 |
+
# WEATHER_API_URL=https://api.openweathermap.org/data/2.5
|
| 94 |
+
|
| 95 |
+
# Notification service (optional)
|
| 96 |
+
# EMAIL_SERVICE_API_KEY=your-sendgrid-api-key
|
| 97 |
+
# SMS_SERVICE_API_KEY=your-twilio-api-key
|
| 98 |
+
|
| 99 |
+
# ============================
|
| 100 |
+
# Feature Flags
|
| 101 |
+
# ============================
|
| 102 |
+
|
| 103 |
+
# Enable experimental features
|
| 104 |
+
ENABLE_EXPERIMENTAL_FEATURES=false
|
| 105 |
+
|
| 106 |
+
# Enable self-learning system
|
| 107 |
+
ENABLE_SELF_LEARNING=true
|
| 108 |
+
|
| 109 |
+
# Enable anomaly detection
|
| 110 |
+
ENABLE_ANOMALY_DETECTION=true
|
| 111 |
+
|
| 112 |
+
# ============================
|
| 113 |
+
# Performance Tuning
|
| 114 |
+
# ============================
|
| 115 |
+
|
| 116 |
+
# Number of worker processes (recommended: number of CPU cores)
|
| 117 |
+
WORKERS=4
|
| 118 |
+
|
| 119 |
+
# Worker timeout in seconds
|
| 120 |
+
TIMEOUT=60
|
| 121 |
+
|
| 122 |
+
# Keep-alive timeout
|
| 123 |
+
KEEP_ALIVE=5
|
| 124 |
+
|
| 125 |
+
# ============================
|
| 126 |
+
# Development Only
|
| 127 |
+
# ============================
|
| 128 |
+
|
| 129 |
+
# Enable auto-reload on code changes (development only)
|
| 130 |
+
RELOAD=true
|
| 131 |
+
|
| 132 |
+
# Enable detailed error messages (disable in production)
|
| 133 |
+
SHOW_ERROR_DETAILS=true
|
| 134 |
+
|
| 135 |
+
# =====================================================
|
| 136 |
+
# Notes:
|
| 137 |
+
# - All values are optional unless marked as required
|
| 138 |
+
# - Default values are used if not specified
|
| 139 |
+
# - See README.md for detailed configuration guide
|
| 140 |
+
# =====================================================
|
.gitattributes
CHANGED
|
@@ -1,35 +1,5 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
*.
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz 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
|
|
|
|
| 1 |
+
kerala_realworld_dataset_v4.csv filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
kerala_realworld_dataset.csv filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.keras filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
*.env
|
| 3 |
+
__pycache__/
|
| 4 |
+
node_modules/
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11.11
|
Analysis_Tools/analyze_dataset_issues.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
df = pd.read_csv("kerala_smartwatt_ai.csv")
|
| 5 |
+
|
| 6 |
+
print("=" * 80)
|
| 7 |
+
print("DATASET ISSUE ANALYSIS")
|
| 8 |
+
print("=" * 80)
|
| 9 |
+
|
| 10 |
+
print("\n1. AC AGE VALUES (Date Formatting Issue?)")
|
| 11 |
+
print("-" * 80)
|
| 12 |
+
print(df["ac_age_years"].value_counts())
|
| 13 |
+
print(f"\nSample AC age values: {df['ac_age_years'].unique()[:10]}")
|
| 14 |
+
|
| 15 |
+
print("\n2. AC USAGE PATTERN FOR NON-AC HOUSEHOLDS")
|
| 16 |
+
print("-" * 80)
|
| 17 |
+
print(f"Total households: {len(df)}")
|
| 18 |
+
print(f"Has AC (has_ac=1): {df['has_ac'].sum()}")
|
| 19 |
+
print(f"No AC (has_ac=0): {(df['has_ac']==0).sum()}")
|
| 20 |
+
print(f"\nPattern distribution for NO-AC households:")
|
| 21 |
+
print(df[df["has_ac"] == 0]["ac_usage_pattern"].value_counts())
|
| 22 |
+
|
| 23 |
+
print("\n3. SAMPLE NON-AC HOUSEHOLD DATA")
|
| 24 |
+
print("-" * 80)
|
| 25 |
+
print(
|
| 26 |
+
df[df["has_ac"] == 0][
|
| 27 |
+
[
|
| 28 |
+
"has_ac",
|
| 29 |
+
"ac_age_years",
|
| 30 |
+
"ac_type",
|
| 31 |
+
"ac_usage_pattern",
|
| 32 |
+
"ac_real_effective_hours",
|
| 33 |
+
"ac_kwh",
|
| 34 |
+
]
|
| 35 |
+
].head(15)
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
print("\n4. CHECKING OTHER APPLIANCES FOR SIMILAR ISSUES")
|
| 39 |
+
print("-" * 80)
|
| 40 |
+
|
| 41 |
+
# Check refrigerator
|
| 42 |
+
print("\nREFRIGERATOR:")
|
| 43 |
+
print(f"Has fridge: {df['has_refrigerator'].sum()}")
|
| 44 |
+
print(f"No fridge: {(df['has_refrigerator']==0).sum()}")
|
| 45 |
+
non_fridge_patterns = df[df["has_refrigerator"] == 0][
|
| 46 |
+
"refrigerator_usage_pattern"
|
| 47 |
+
].value_counts()
|
| 48 |
+
print(f"Non-fridge pattern distribution:\n{non_fridge_patterns[:5]}")
|
| 49 |
+
|
| 50 |
+
# Check television
|
| 51 |
+
print("\nTELEVISION:")
|
| 52 |
+
print(f"Has TV: {df['has_television'].sum()}")
|
| 53 |
+
print(f"No TV: {(df['has_television']==0).sum()}")
|
| 54 |
+
non_tv_patterns = df[df["has_television"] == 0][
|
| 55 |
+
"television_usage_pattern"
|
| 56 |
+
].value_counts()
|
| 57 |
+
print(f"Non-TV pattern distribution:\n{non_tv_patterns[:5]}")
|
| 58 |
+
|
| 59 |
+
# Check iron
|
| 60 |
+
print("\nIRON:")
|
| 61 |
+
print(f"Has iron: {df['has_iron'].sum()}")
|
| 62 |
+
print(f"No iron: {(df['has_iron']==0).sum()}")
|
| 63 |
+
non_iron_patterns = df[df["has_iron"] == 0]["iron_usage_pattern"].value_counts()
|
| 64 |
+
print(f"Non-iron pattern distribution:\n{non_iron_patterns[:5]}")
|
| 65 |
+
|
| 66 |
+
print("\n5. CHECKING FOR ZERO VALUES WITH NON-ZERO PATTERNS")
|
| 67 |
+
print("-" * 80)
|
| 68 |
+
issues = df[
|
| 69 |
+
(df["has_ac"] == 0)
|
| 70 |
+
& (df["ac_usage_pattern"] != "unknown")
|
| 71 |
+
& (df["ac_usage_pattern"] != "none")
|
| 72 |
+
]
|
| 73 |
+
print(f"AC: {len(issues)} households without AC but have usage_pattern assigned")
|
| 74 |
+
|
| 75 |
+
issues_fridge = df[
|
| 76 |
+
(df["has_refrigerator"] == 0)
|
| 77 |
+
& (df["refrigerator_usage_pattern"] != "unknown")
|
| 78 |
+
& (df["refrigerator_usage_pattern"] != "none")
|
| 79 |
+
]
|
| 80 |
+
print(
|
| 81 |
+
f"Fridge: {len(issues_fridge)} households without fridge but have usage_pattern assigned"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
issues_tv = df[
|
| 85 |
+
(df["has_television"] == 0)
|
| 86 |
+
& (df["television_usage_pattern"] != "unknown")
|
| 87 |
+
& (df["television_usage_pattern"] != "none")
|
| 88 |
+
]
|
| 89 |
+
print(f"TV: {len(issues_tv)} households without TV but have usage_pattern assigned")
|
| 90 |
+
|
| 91 |
+
print("\n" + "=" * 80)
|
| 92 |
+
print("ANALYSIS COMPLETE")
|
| 93 |
+
print("=" * 80)
|
Analysis_Tools/evaluate_all_models.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Evaluate All Models Script
|
| 3 |
+
Loads all 22 trained models and generates a detailed performance report.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
import joblib
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import tensorflow as tf
|
| 12 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
|
| 13 |
+
from tensorflow import keras
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_data():
|
| 17 |
+
if not os.path.exists("kerala_smartwatt_ai.csv"):
|
| 18 |
+
print("❌ Dataset not found!")
|
| 19 |
+
return None
|
| 20 |
+
return pd.read_csv("kerala_smartwatt_ai.csv")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def evaluate_models():
|
| 24 |
+
df = load_data()
|
| 25 |
+
if df is None:
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
# List of all 22 models to evaluate
|
| 29 |
+
tasks = [
|
| 30 |
+
(
|
| 31 |
+
"ac",
|
| 32 |
+
"ac",
|
| 33 |
+
[
|
| 34 |
+
"n_occupants",
|
| 35 |
+
"season",
|
| 36 |
+
"location_type",
|
| 37 |
+
"ac_tonnage",
|
| 38 |
+
"ac_star_rating",
|
| 39 |
+
"ac_type",
|
| 40 |
+
"ac_age_years",
|
| 41 |
+
"ac_usage_pattern",
|
| 42 |
+
],
|
| 43 |
+
),
|
| 44 |
+
(
|
| 45 |
+
"fridge",
|
| 46 |
+
"fridge",
|
| 47 |
+
[
|
| 48 |
+
"n_occupants",
|
| 49 |
+
"season",
|
| 50 |
+
"location_type",
|
| 51 |
+
"fridge_capacity",
|
| 52 |
+
"fridge_age",
|
| 53 |
+
"fridge_star_rating",
|
| 54 |
+
"fridge_type",
|
| 55 |
+
"refrigerator_usage_pattern",
|
| 56 |
+
],
|
| 57 |
+
),
|
| 58 |
+
(
|
| 59 |
+
"ceiling_fan",
|
| 60 |
+
"ceiling_fan",
|
| 61 |
+
[
|
| 62 |
+
"n_occupants",
|
| 63 |
+
"season",
|
| 64 |
+
"location_type",
|
| 65 |
+
"fan_type",
|
| 66 |
+
"num_fans",
|
| 67 |
+
"fan_usage_pattern",
|
| 68 |
+
],
|
| 69 |
+
),
|
| 70 |
+
(
|
| 71 |
+
"television",
|
| 72 |
+
"television",
|
| 73 |
+
[
|
| 74 |
+
"n_occupants",
|
| 75 |
+
"season",
|
| 76 |
+
"location_type",
|
| 77 |
+
"television_type",
|
| 78 |
+
"tv_size",
|
| 79 |
+
"television_usage_pattern",
|
| 80 |
+
],
|
| 81 |
+
),
|
| 82 |
+
(
|
| 83 |
+
"washing_machine",
|
| 84 |
+
"washing_machine",
|
| 85 |
+
[
|
| 86 |
+
"n_occupants",
|
| 87 |
+
"season",
|
| 88 |
+
"location_type",
|
| 89 |
+
"wm_type",
|
| 90 |
+
"wm_capacity",
|
| 91 |
+
"wm_star_rating",
|
| 92 |
+
"wm_cycles_per_week",
|
| 93 |
+
],
|
| 94 |
+
),
|
| 95 |
+
(
|
| 96 |
+
"water_pump",
|
| 97 |
+
"water_pump",
|
| 98 |
+
[
|
| 99 |
+
"n_occupants",
|
| 100 |
+
"season",
|
| 101 |
+
"location_type",
|
| 102 |
+
"water_pump_hp",
|
| 103 |
+
"pump_usage_pattern",
|
| 104 |
+
],
|
| 105 |
+
),
|
| 106 |
+
(
|
| 107 |
+
"water_heater",
|
| 108 |
+
"water_heater",
|
| 109 |
+
[
|
| 110 |
+
"n_occupants",
|
| 111 |
+
"season",
|
| 112 |
+
"location_type",
|
| 113 |
+
"water_heater_type",
|
| 114 |
+
"water_heater_capacity",
|
| 115 |
+
"geyser_usage_pattern",
|
| 116 |
+
],
|
| 117 |
+
),
|
| 118 |
+
(
|
| 119 |
+
"iron",
|
| 120 |
+
"iron",
|
| 121 |
+
["n_occupants", "season", "location_type", "iron_usage_pattern"],
|
| 122 |
+
),
|
| 123 |
+
(
|
| 124 |
+
"kettle",
|
| 125 |
+
"kettle",
|
| 126 |
+
["n_occupants", "season", "location_type", "kettle_usage_pattern"],
|
| 127 |
+
),
|
| 128 |
+
(
|
| 129 |
+
"induction",
|
| 130 |
+
"induction",
|
| 131 |
+
["n_occupants", "season", "location_type", "induction_usage_pattern"],
|
| 132 |
+
),
|
| 133 |
+
(
|
| 134 |
+
"desktop",
|
| 135 |
+
"desktop",
|
| 136 |
+
["n_occupants", "season", "location_type", "desktop_usage_pattern"],
|
| 137 |
+
),
|
| 138 |
+
(
|
| 139 |
+
"microwave",
|
| 140 |
+
"microwave",
|
| 141 |
+
["n_occupants", "season", "location_type", "microwave_usage_pattern"],
|
| 142 |
+
),
|
| 143 |
+
(
|
| 144 |
+
"mixer",
|
| 145 |
+
"mixer",
|
| 146 |
+
["n_occupants", "season", "location_type", "mixer_usage_pattern"],
|
| 147 |
+
),
|
| 148 |
+
(
|
| 149 |
+
"rice_cooker",
|
| 150 |
+
"rice_cooker",
|
| 151 |
+
["n_occupants", "season", "location_type", "rice_cooker_usage_pattern"],
|
| 152 |
+
),
|
| 153 |
+
(
|
| 154 |
+
"toaster",
|
| 155 |
+
"toaster",
|
| 156 |
+
["n_occupants", "season", "location_type", "toaster_usage_pattern"],
|
| 157 |
+
),
|
| 158 |
+
(
|
| 159 |
+
"food_processor",
|
| 160 |
+
"food_processor",
|
| 161 |
+
["n_occupants", "season", "location_type", "food_processor_usage_pattern"],
|
| 162 |
+
),
|
| 163 |
+
(
|
| 164 |
+
"laptop",
|
| 165 |
+
"laptop",
|
| 166 |
+
["n_occupants", "season", "location_type", "laptop_usage_pattern"],
|
| 167 |
+
),
|
| 168 |
+
(
|
| 169 |
+
"hair_dryer",
|
| 170 |
+
"hair_dryer",
|
| 171 |
+
["n_occupants", "season", "location_type", "hair_dryer_usage_pattern"],
|
| 172 |
+
),
|
| 173 |
+
(
|
| 174 |
+
"vacuum",
|
| 175 |
+
"vacuum",
|
| 176 |
+
["n_occupants", "season", "location_type", "vacuum_usage_pattern"],
|
| 177 |
+
),
|
| 178 |
+
(
|
| 179 |
+
"led_lights",
|
| 180 |
+
"led_lights",
|
| 181 |
+
["n_occupants", "season", "location_type", "led_lights_usage_pattern"],
|
| 182 |
+
),
|
| 183 |
+
(
|
| 184 |
+
"cfl_lights",
|
| 185 |
+
"cfl_lights",
|
| 186 |
+
["n_occupants", "season", "location_type", "cfl_lights_usage_pattern"],
|
| 187 |
+
),
|
| 188 |
+
(
|
| 189 |
+
"tube_lights",
|
| 190 |
+
"tube_lights",
|
| 191 |
+
["n_occupants", "season", "location_type", "tube_lights_usage_pattern"],
|
| 192 |
+
),
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
results_data = []
|
| 196 |
+
|
| 197 |
+
print(
|
| 198 |
+
f"{'APPLIANCE':<20} | {'EFFICIENCY MAE':<15} | {'HOURS MAE':<10} | {'SAMPLES':<8}"
|
| 199 |
+
)
|
| 200 |
+
print("-" * 65)
|
| 201 |
+
|
| 202 |
+
for name, prefix, feats in tasks:
|
| 203 |
+
model_path = f"models/{name}_model.keras"
|
| 204 |
+
preprocessor_path = f"models/{name}_preprocessor.pkl"
|
| 205 |
+
|
| 206 |
+
if not os.path.exists(model_path) or not os.path.exists(preprocessor_path):
|
| 207 |
+
print(f"⚠️ {name}: Model/Preprocessor not found")
|
| 208 |
+
continue
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
# Load artifacts
|
| 212 |
+
model = keras.models.load_model(model_path)
|
| 213 |
+
preprocessor = joblib.load(preprocessor_path)
|
| 214 |
+
|
| 215 |
+
# Filter data
|
| 216 |
+
if f"has_{name}" in df.columns:
|
| 217 |
+
df_app = df[df[f"has_{name}"] == 1].copy()
|
| 218 |
+
elif name == "fridge":
|
| 219 |
+
df_app = df[df["has_refrigerator"] == 1].copy()
|
| 220 |
+
elif name == "desktop":
|
| 221 |
+
df_app = df[df["has_computer"] == 1].copy()
|
| 222 |
+
elif name == "kettle":
|
| 223 |
+
df_app = df[df["has_electric_kettle"] == 1].copy()
|
| 224 |
+
elif name in [
|
| 225 |
+
"microwave",
|
| 226 |
+
"mixer",
|
| 227 |
+
"rice_cooker",
|
| 228 |
+
"toaster",
|
| 229 |
+
"food_processor",
|
| 230 |
+
"laptop",
|
| 231 |
+
"hair_dryer",
|
| 232 |
+
"vacuum",
|
| 233 |
+
]:
|
| 234 |
+
df_app = df[df[f"has_{name}"] == 1].copy()
|
| 235 |
+
elif name in ["led_lights", "cfl_lights", "tube_lights"]:
|
| 236 |
+
df_app = df[df[f"has_{name}"] == 1].copy()
|
| 237 |
+
else:
|
| 238 |
+
df_app = df.copy()
|
| 239 |
+
|
| 240 |
+
if len(df_app) == 0:
|
| 241 |
+
print(f"⚠️ {name}: No data")
|
| 242 |
+
continue
|
| 243 |
+
|
| 244 |
+
# Prepare inputs/targets
|
| 245 |
+
X = preprocessor.transform(df_app[feats])
|
| 246 |
+
y_eff_true = df_app[f"{prefix}_real_efficiency_factor"].values
|
| 247 |
+
y_hours_true = df_app[f"{prefix}_real_effective_hours"].values
|
| 248 |
+
|
| 249 |
+
# Predict
|
| 250 |
+
preds = model.predict(X, verbose=0)
|
| 251 |
+
y_eff_pred = preds[0].flatten()
|
| 252 |
+
y_hours_pred = preds[1].flatten()
|
| 253 |
+
|
| 254 |
+
# Calculate Metrics
|
| 255 |
+
mae_eff = mean_absolute_error(y_eff_true, y_eff_pred)
|
| 256 |
+
mae_hours = mean_absolute_error(y_hours_true, y_hours_pred)
|
| 257 |
+
|
| 258 |
+
# Additional logic to handle very large R2 errors if any
|
| 259 |
+
r2_eff = r2_score(y_eff_true, y_eff_pred)
|
| 260 |
+
r2_hours = r2_score(y_hours_true, y_hours_pred)
|
| 261 |
+
|
| 262 |
+
print(
|
| 263 |
+
f"{name:<20} | {mae_eff:<15.4f} | {mae_hours:<10.2f} | {len(df_app):<8}"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
results_data.append(
|
| 267 |
+
{
|
| 268 |
+
"Appliance": name.replace("_", " ").title(),
|
| 269 |
+
"Efficiency MAE": f"{mae_eff:.4f}",
|
| 270 |
+
"Hours MAE": f"{mae_hours:.2f}",
|
| 271 |
+
"Efficiency R²": f"{r2_eff:.3f}",
|
| 272 |
+
"Hours R²": f"{r2_hours:.3f}",
|
| 273 |
+
"Samples": len(df_app),
|
| 274 |
+
}
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
except Exception as e:
|
| 278 |
+
print(f"❌ {name}: Error - {str(e)}")
|
| 279 |
+
|
| 280 |
+
# Save to Markdown
|
| 281 |
+
with open("training_results_table.md", "w") as f:
|
| 282 |
+
f.write("# SmartWatt AI Model Performance Report\n\n")
|
| 283 |
+
f.write(
|
| 284 |
+
"| Appliance | Efficiency MAE | Usage Hours MAE | Efficiency R² | Hours R² | Samples |\n"
|
| 285 |
+
)
|
| 286 |
+
f.write(
|
| 287 |
+
"|-----------|----------------|-----------------|---------------|----------|---------|\n"
|
| 288 |
+
)
|
| 289 |
+
for r in results_data:
|
| 290 |
+
f.write(
|
| 291 |
+
f"| {r['Appliance']} | {r['Efficiency MAE']} | {r['Hours MAE']} | {r['Efficiency R²']} | {r['Hours R²']} | {r['Samples']} |\n"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
print("\n✅ Report saved to training_results_table.md")
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
if __name__ == "__main__":
|
| 298 |
+
evaluate_models()
|
Analysis_Tools/show_real_data.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
|
| 3 |
+
df = pd.read_csv("kerala_smartwatt_ai.csv")
|
| 4 |
+
|
| 5 |
+
print("\n" + "=" * 80)
|
| 6 |
+
print("VERIFICATION: What Python/Training Script Actually Sees")
|
| 7 |
+
print("=" * 80)
|
| 8 |
+
|
| 9 |
+
print("\nAC Age Values Distribution:")
|
| 10 |
+
print(df["ac_age_years"].value_counts())
|
| 11 |
+
|
| 12 |
+
print("\n" + "=" * 80)
|
| 13 |
+
print("Sample AC Households (First 15 with AC):")
|
| 14 |
+
print("=" * 80)
|
| 15 |
+
sample = df[df["has_ac"] == 1][
|
| 16 |
+
["ac_age_years", "ac_type", "ac_usage_pattern", "ac_real_effective_hours"]
|
| 17 |
+
].head(15)
|
| 18 |
+
print(sample.to_string())
|
| 19 |
+
|
| 20 |
+
print("\n" + "=" * 80)
|
| 21 |
+
print("CONCLUSION:")
|
| 22 |
+
print("=" * 80)
|
| 23 |
+
print("- ac_age_years shows: '3-5', '6-10', '0-2', '10+', 'unknown'")
|
| 24 |
+
print("- These are TEXT RANGES stored correctly in CSV")
|
| 25 |
+
print("- Python reads them correctly (no date conversion)")
|
| 26 |
+
print("- Training script works perfectly with these values")
|
| 27 |
+
print("\nThe 'dates' you saw ONLY exist in Excel's display!")
|
| 28 |
+
print("The actual data is CORRECT and ready for training.")
|
| 29 |
+
print("=" * 80)
|
Analysis_Tools/test_auto_train_local.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Auto-Train Pipeline for SmartWatt (The "Self-Improving" System)
|
| 3 |
+
------------------------------------------------------------------
|
| 4 |
+
This script is the "Gym" for our AI.
|
| 5 |
+
Every time we run this, the AI gets smarter.
|
| 6 |
+
|
| 7 |
+
Workflow:
|
| 8 |
+
1. GENERATE: Create a new "Virtual World" of Kerala households (Physics-based Ground Truth).
|
| 9 |
+
2. TRAIN: Teach the AI models using this fresh data.
|
| 10 |
+
3. DEPLOY: Save the brain files (.keras) so the backend can use them.
|
| 11 |
+
|
| 12 |
+
Why?
|
| 13 |
+
Because static AI gets stale. We want an AI that can learn from new physics rules instantly.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
from supabase import create_client
|
| 21 |
+
|
| 22 |
+
# Load environment variables
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# Add parent directory to path to import backend modules
|
| 27 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
import newdataset
|
| 31 |
+
import train
|
| 32 |
+
except ImportError as e:
|
| 33 |
+
print(f"❌ Error importing Backend modules: {e}")
|
| 34 |
+
print("Ensure you are running this from the Backend/ root directory.")
|
| 35 |
+
sys.exit(1)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def run_pipeline():
|
| 39 |
+
print("\n" + "=" * 60)
|
| 40 |
+
print(" 🚀 SMARTWATT : AUTO-TRAIN PIPELINE STARTING")
|
| 41 |
+
print("=" * 60)
|
| 42 |
+
|
| 43 |
+
# --- STEP 0: CHECK REAL WORLD DATA (Gatekeeper) ---
|
| 44 |
+
# We only want to burn compute if we have enough real user data to matter.
|
| 45 |
+
# Checks the "smartwatt_training" vault in Supabase.
|
| 46 |
+
try:
|
| 47 |
+
url = os.environ.get("SUPABASE_URL")
|
| 48 |
+
key = os.environ.get("SUPABASE_KEY")
|
| 49 |
+
|
| 50 |
+
if not url or not key:
|
| 51 |
+
print("⚠️ Skipped Supabase Check: Missing credentials.")
|
| 52 |
+
else:
|
| 53 |
+
print("\n[Step 0/2] Checking Real World Data Maturity...")
|
| 54 |
+
supabase = create_client(url, key)
|
| 55 |
+
# Count entries (head=True is efficient, doesn't fetch body)
|
| 56 |
+
count = (
|
| 57 |
+
supabase.table("smartwatt_training")
|
| 58 |
+
.select("*", count="exact", head=True)
|
| 59 |
+
.execute()
|
| 60 |
+
.count
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
print(f" 📊 Found {count} real user entries in Database.")
|
| 64 |
+
|
| 65 |
+
if count < 10:
|
| 66 |
+
print(f" 🛑 HALTING: Not enough data ({count}/10).")
|
| 67 |
+
print(" The AI refuses to learn from an empty world.")
|
| 68 |
+
print(" Come back when you have at least 10 entries.")
|
| 69 |
+
return
|
| 70 |
+
else:
|
| 71 |
+
print(" ✅ PROCEEDING: Data maturity threshold met.")
|
| 72 |
+
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"⚠️ Supabase Check Warning: {e}")
|
| 75 |
+
# We don't stop strictly on error, unless user wants strict mode.
|
| 76 |
+
# But user said "only train if...", so maybe we SHOULD return on error?
|
| 77 |
+
# User said "only train if supabase contain minimum 10 entries".
|
| 78 |
+
# If we can't verify, we can't be sure. Safe default is to Proceed?
|
| 79 |
+
# No, "only train if...". If check fails, condition is not met.
|
| 80 |
+
# But for robustness, I'll print warning and Return to be safe/strict.
|
| 81 |
+
print(" 🛑 Check failed, skipping training to be safe.")
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
# --- STEP 1: DATA REGENERATION ---
|
| 85 |
+
print("\n[Step 1/2] Regenerating Synthetic Ground Truth Data...")
|
| 86 |
+
try:
|
| 87 |
+
sim = newdataset.KeralaRealWorldSimulator()
|
| 88 |
+
df = sim.generate()
|
| 89 |
+
|
| 90 |
+
# Save to root directory
|
| 91 |
+
csv_path = "kerala_smartwatt_ai.csv"
|
| 92 |
+
df.to_csv(csv_path, index=False)
|
| 93 |
+
print(f" ✅ New Dataset Generated: {csv_path} ({len(df)} samples)")
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f" ❌ Data Generation Failed: {e}")
|
| 97 |
+
return
|
| 98 |
+
|
| 99 |
+
# --- STEP 2: MODEL RETRAINING ---
|
| 100 |
+
print("\n[Step 2/2] Retraining Hybrid Models...")
|
| 101 |
+
# This calls the "Teacher" script (train.py) to update the neural networks.
|
| 102 |
+
try:
|
| 103 |
+
train.run_training()
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f" ❌ Training Failed: {e}")
|
| 106 |
+
return
|
| 107 |
+
|
| 108 |
+
print("\n" + "=" * 60)
|
| 109 |
+
print(" ✅ PIPELINE COMPLETE: AI IS NOW SMARTER!")
|
| 110 |
+
print("=" * 60)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
"""
|
| 114 |
+
--- LEGACY V1 SUPABASE SNIPPET (ARCHIVED) ---
|
| 115 |
+
The V1 Auto-Train logic fetched user data from Supabase.
|
| 116 |
+
In , we require 'Efficiency' & 'Hours' ground truth, which basic user input lacks.
|
| 117 |
+
Future Scope: Integrate IoT sensors to collect this ground truth, then uncomment below.
|
| 118 |
+
|
| 119 |
+
# from supabase import create_client
|
| 120 |
+
# ...
|
| 121 |
+
# def fetch_training_data_original():
|
| 122 |
+
# response = supabase.table('smartwatt_training').select("*").execute()
|
| 123 |
+
# ...
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
if __name__ == "__main__":
|
| 127 |
+
run_pipeline()
|
Analysis_Tools/test_clean_architecture.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test script to demonstrate new clean batch prediction architecture
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
import requests
|
| 8 |
+
|
| 9 |
+
# Test data - household with multiple appliances
|
| 10 |
+
test_request = {
|
| 11 |
+
"requests": [
|
| 12 |
+
{
|
| 13 |
+
"appliance_name": "ac",
|
| 14 |
+
"total_bill": 150.0,
|
| 15 |
+
"details": {
|
| 16 |
+
"num_people": 3,
|
| 17 |
+
"season": "monsoon",
|
| 18 |
+
"ac_tonnage": 1.5,
|
| 19 |
+
"ac_star": 3,
|
| 20 |
+
"ac_hours": 6,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"appliance_name": "fridge",
|
| 25 |
+
"total_bill": 150.0,
|
| 26 |
+
"details": {
|
| 27 |
+
"num_people": 3,
|
| 28 |
+
"season": "monsoon",
|
| 29 |
+
"fridge_capacity_liters": 250,
|
| 30 |
+
"fridge_age_years": 5,
|
| 31 |
+
"fridge_star": 4,
|
| 32 |
+
},
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"appliance_name": "ceiling_fan",
|
| 36 |
+
"total_bill": 150.0,
|
| 37 |
+
"details": {
|
| 38 |
+
"num_people": 3,
|
| 39 |
+
"season": "monsoon",
|
| 40 |
+
"num_ceiling_fans": 4,
|
| 41 |
+
"fan_hours": 12,
|
| 42 |
+
},
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"appliance_name": "led_light",
|
| 46 |
+
"total_bill": 150.0,
|
| 47 |
+
"details": {
|
| 48 |
+
"num_people": 3,
|
| 49 |
+
"season": "monsoon",
|
| 50 |
+
"num_led": 10,
|
| 51 |
+
"led_hours": 5,
|
| 52 |
+
},
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"appliance_name": "television",
|
| 56 |
+
"total_bill": 150.0,
|
| 57 |
+
"details": {
|
| 58 |
+
"num_people": 3,
|
| 59 |
+
"season": "monsoon",
|
| 60 |
+
"tv_size_inches": 43,
|
| 61 |
+
"tv_type": "LED",
|
| 62 |
+
"tv_pattern": "moderate",
|
| 63 |
+
},
|
| 64 |
+
},
|
| 65 |
+
]
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
print("=" * 80)
|
| 69 |
+
print("TESTING NEW CLEAN BATCH PREDICTION ARCHITECTURE")
|
| 70 |
+
print("=" * 80)
|
| 71 |
+
print(f"\nSending batch request with {len(test_request['requests'])} appliances...")
|
| 72 |
+
print()
|
| 73 |
+
|
| 74 |
+
# Make request
|
| 75 |
+
response = requests.post("http://localhost:8000/predict-all", json=test_request)
|
| 76 |
+
|
| 77 |
+
if response.status_code == 200:
|
| 78 |
+
results = response.json()
|
| 79 |
+
print("\n" + "=" * 80)
|
| 80 |
+
print("API RESPONSE RECEIVED")
|
| 81 |
+
print("=" * 80)
|
| 82 |
+
|
| 83 |
+
total_kwh = 0
|
| 84 |
+
for appliance, data in results.items():
|
| 85 |
+
kwh = data.get("prediction", 0)
|
| 86 |
+
total_kwh += kwh
|
| 87 |
+
source = data.get("insights", {}).get("source", "Unknown")
|
| 88 |
+
print(f"{appliance:20}: {kwh:6.2f} kWh ({source})")
|
| 89 |
+
|
| 90 |
+
print("-" * 80)
|
| 91 |
+
print(f"{'TOTAL':20}: {total_kwh:6.2f} kWh")
|
| 92 |
+
print("=" * 80)
|
| 93 |
+
else:
|
| 94 |
+
print(f"Error: {response.status_code}")
|
| 95 |
+
print(response.text)
|
| 96 |
+
|
| 97 |
+
print("\n\nCheck the backend console for CLEAN LOGGING OUTPUT!")
|
Analysis_Tools/verify_patterns.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
|
| 3 |
+
df = pd.read_csv("kerala_smartwatt_ai.csv")
|
| 4 |
+
|
| 5 |
+
print("\n" + "=" * 70)
|
| 6 |
+
print("USAGE PATTERN vs EFFECTIVE HOURS VERIFICATION")
|
| 7 |
+
print("=" * 70)
|
| 8 |
+
|
| 9 |
+
# AC
|
| 10 |
+
print("\n1. AIR CONDITIONER:")
|
| 11 |
+
print(
|
| 12 |
+
df[df["has_ac"] == 1][["ac_usage_pattern", "ac_real_effective_hours"]]
|
| 13 |
+
.groupby("ac_usage_pattern")["ac_real_effective_hours"]
|
| 14 |
+
.agg(["count", "mean", "min", "max"])
|
| 15 |
+
.round(2)
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Fridge
|
| 19 |
+
print("\n2. REFRIGERATOR:")
|
| 20 |
+
print(
|
| 21 |
+
df[df["has_refrigerator"] == 1][
|
| 22 |
+
["refrigerator_usage_pattern", "fridge_real_effective_hours"]
|
| 23 |
+
]
|
| 24 |
+
.groupby("refrigerator_usage_pattern")["fridge_real_effective_hours"]
|
| 25 |
+
.agg(["count", "mean", "min", "max"])
|
| 26 |
+
.round(2)
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Television
|
| 30 |
+
print("\n3. TELEVISION:")
|
| 31 |
+
print(
|
| 32 |
+
df[df["has_television"] == 1][
|
| 33 |
+
["television_usage_pattern", "television_real_effective_hours"]
|
| 34 |
+
]
|
| 35 |
+
.groupby("television_usage_pattern")["television_real_effective_hours"]
|
| 36 |
+
.agg(["count", "mean", "min", "max"])
|
| 37 |
+
.round(2)
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Iron
|
| 41 |
+
print("\n4. IRON:")
|
| 42 |
+
print(
|
| 43 |
+
df[df["has_iron"] == 1][["iron_usage_pattern", "iron_real_effective_hours"]]
|
| 44 |
+
.groupby("iron_usage_pattern")["iron_real_effective_hours"]
|
| 45 |
+
.agg(["count", "mean", "min", "max"])
|
| 46 |
+
.round(2)
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# Kettle
|
| 50 |
+
print("\n5. ELECTRIC KETTLE:")
|
| 51 |
+
print(
|
| 52 |
+
df[df["has_electric_kettle"] == 1][
|
| 53 |
+
["kettle_usage_pattern", "kettle_real_effective_hours"]
|
| 54 |
+
]
|
| 55 |
+
.groupby("kettle_usage_pattern")["kettle_real_effective_hours"]
|
| 56 |
+
.agg(["count", "mean", "min", "max"])
|
| 57 |
+
.round(2)
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# Water Pump
|
| 61 |
+
print("\n6. WATER PUMP:")
|
| 62 |
+
print(
|
| 63 |
+
df[df["has_water_pump"] == 1][
|
| 64 |
+
["pump_usage_pattern", "water_pump_real_effective_hours"]
|
| 65 |
+
]
|
| 66 |
+
.groupby("pump_usage_pattern")["water_pump_real_effective_hours"]
|
| 67 |
+
.agg(["count", "mean", "min", "max"])
|
| 68 |
+
.round(2)
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# Geyser
|
| 72 |
+
print("\n7. WATER HEATER (GEYSER):")
|
| 73 |
+
print(
|
| 74 |
+
df[df["has_water_heater"] == 1][
|
| 75 |
+
["geyser_usage_pattern", "water_heater_real_effective_hours"]
|
| 76 |
+
]
|
| 77 |
+
.groupby("geyser_usage_pattern")["water_heater_real_effective_hours"]
|
| 78 |
+
.agg(["count", "mean", "min", "max"])
|
| 79 |
+
.round(2)
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
print("\n" + "=" * 70)
|
| 83 |
+
print("✅ VERIFICATION COMPLETE")
|
| 84 |
+
print("=" * 70)
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official lightweight Python image
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Set environment variables to prevent Python from writing pyc files and buffering stdout
|
| 5 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 6 |
+
ENV PYTHONUNBUFFERED=1
|
| 7 |
+
|
| 8 |
+
# Install system dependencies (if needed for TensorFlow/Scikit-learn)
|
| 9 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
+
build-essential \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Set up a new user named "user" with user ID 1000
|
| 14 |
+
# Hugging Face Spaces requires running Docker containers as a non-root user
|
| 15 |
+
RUN useradd -m -u 1000 user
|
| 16 |
+
USER user
|
| 17 |
+
ENV HOME=/home/user \
|
| 18 |
+
PATH=/home/user/.local/bin:$PATH
|
| 19 |
+
|
| 20 |
+
WORKDIR $HOME/app
|
| 21 |
+
|
| 22 |
+
# Copy requirements first to leverage Docker cache
|
| 23 |
+
COPY --chown=user requirements.txt .
|
| 24 |
+
|
| 25 |
+
# Install Python dependencies
|
| 26 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 27 |
+
|
| 28 |
+
# Copy the rest of the application
|
| 29 |
+
COPY --chown=user . $HOME/app
|
| 30 |
+
|
| 31 |
+
# Expose the API port 7860 for Hugging Face
|
| 32 |
+
EXPOSE 7860
|
| 33 |
+
|
| 34 |
+
# Command to run the application on Hugging Face port 7860
|
| 35 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 SmartWatt AI Team
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
Migration_Scripts/migrate_kwh_columns.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Migration Script: Populate predicted_kwh and input_kwh columns
|
| 3 |
+
================================================================
|
| 4 |
+
This script extracts data from existing JSON columns and populates
|
| 5 |
+
the new dedicated columns for faster self-learning.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
from supabase import create_client
|
| 13 |
+
|
| 14 |
+
# Load environment variables
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
| 18 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def get_supabase_client():
|
| 22 |
+
"""Initialize Supabase client"""
|
| 23 |
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 24 |
+
raise ValueError(
|
| 25 |
+
"Missing SUPABASE_URL or SUPABASE_KEY in environment variables"
|
| 26 |
+
)
|
| 27 |
+
return create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def extract_predicted_kwh(final_breakdown):
|
| 31 |
+
"""Extract AI's predicted kWh from final_breakdown.rawTotal"""
|
| 32 |
+
try:
|
| 33 |
+
if not final_breakdown:
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
# Parse JSON string if needed
|
| 37 |
+
if isinstance(final_breakdown, str):
|
| 38 |
+
final_breakdown = json.loads(final_breakdown)
|
| 39 |
+
|
| 40 |
+
if not isinstance(final_breakdown, dict):
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
# Get rawTotal (AI's prediction)
|
| 44 |
+
raw_total = final_breakdown.get("rawTotal")
|
| 45 |
+
if raw_total and 0 < raw_total < 100000: # Sanity check
|
| 46 |
+
return float(raw_total)
|
| 47 |
+
|
| 48 |
+
# Fallback: sum predictions dictionary
|
| 49 |
+
predictions = final_breakdown.get("predictions", {})
|
| 50 |
+
if predictions:
|
| 51 |
+
total = sum(
|
| 52 |
+
float(v) for v in predictions.values() if isinstance(v, (int, float))
|
| 53 |
+
)
|
| 54 |
+
if 0 < total < 100000:
|
| 55 |
+
return total
|
| 56 |
+
|
| 57 |
+
return None
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
print(f" Error extracting predicted_kwh: {e}")
|
| 61 |
+
return None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def migrate_data():
|
| 65 |
+
"""Migrate existing data to populate new columns"""
|
| 66 |
+
print("=" * 70)
|
| 67 |
+
print("🔄 MIGRATION: Populating predicted_kwh and input_kwh columns")
|
| 68 |
+
print("=" * 70)
|
| 69 |
+
|
| 70 |
+
supabase = get_supabase_client()
|
| 71 |
+
|
| 72 |
+
# Fetch all records
|
| 73 |
+
print("\n📥 Fetching all records from smartwatt_training...")
|
| 74 |
+
response = supabase.table("smartwatt_training").select("*").execute()
|
| 75 |
+
|
| 76 |
+
if not response.data:
|
| 77 |
+
print("❌ No records found!")
|
| 78 |
+
return
|
| 79 |
+
|
| 80 |
+
total_records = len(response.data)
|
| 81 |
+
print(f"✅ Found {total_records} records\n")
|
| 82 |
+
|
| 83 |
+
# Process each record
|
| 84 |
+
updated_count = 0
|
| 85 |
+
skipped_count = 0
|
| 86 |
+
|
| 87 |
+
for idx, record in enumerate(response.data, 1):
|
| 88 |
+
record_id = record.get("id", "unknown")[:8]
|
| 89 |
+
print(f"\n[{idx}/{total_records}] Processing record {record_id}...")
|
| 90 |
+
|
| 91 |
+
updates = {}
|
| 92 |
+
|
| 93 |
+
# Extract input_kwh (actual from user's KSEB bill)
|
| 94 |
+
bi_monthly_kwh = record.get("bi_monthly_kwh")
|
| 95 |
+
if bi_monthly_kwh and bi_monthly_kwh > 0:
|
| 96 |
+
updates["input_kwh"] = float(bi_monthly_kwh)
|
| 97 |
+
print(f" ✓ input_kwh: {bi_monthly_kwh} kWh")
|
| 98 |
+
else:
|
| 99 |
+
print(f" ⚠ No valid bi_monthly_kwh")
|
| 100 |
+
|
| 101 |
+
# Extract predicted_kwh (from final_breakdown.rawTotal)
|
| 102 |
+
final_breakdown = record.get("final_breakdown")
|
| 103 |
+
if final_breakdown:
|
| 104 |
+
predicted = extract_predicted_kwh(final_breakdown)
|
| 105 |
+
if predicted:
|
| 106 |
+
updates["predicted_kwh"] = predicted
|
| 107 |
+
print(f" ✓ predicted_kwh: {predicted:.2f} kWh")
|
| 108 |
+
else:
|
| 109 |
+
print(f" ⚠ Could not extract predicted_kwh")
|
| 110 |
+
else:
|
| 111 |
+
print(f" ⚠ No final_breakdown data")
|
| 112 |
+
|
| 113 |
+
# Update record if we have data
|
| 114 |
+
if updates:
|
| 115 |
+
try:
|
| 116 |
+
supabase.table("smartwatt_training").update(updates).eq(
|
| 117 |
+
"id", record["id"]
|
| 118 |
+
).execute()
|
| 119 |
+
|
| 120 |
+
if len(updates) == 2:
|
| 121 |
+
error = updates["input_kwh"] - updates["predicted_kwh"]
|
| 122 |
+
error_pct = (error / updates["input_kwh"]) * 100
|
| 123 |
+
print(f" ✅ Updated! Error: {error:+.1f} kWh ({error_pct:+.1f}%)")
|
| 124 |
+
else:
|
| 125 |
+
print(f" ⚠ Partial update (missing data)")
|
| 126 |
+
|
| 127 |
+
updated_count += 1
|
| 128 |
+
except Exception as e:
|
| 129 |
+
print(f" ❌ Update failed: {e}")
|
| 130 |
+
skipped_count += 1
|
| 131 |
+
else:
|
| 132 |
+
print(f" ⏭️ Skipped (no data to update)")
|
| 133 |
+
skipped_count += 1
|
| 134 |
+
|
| 135 |
+
# Summary
|
| 136 |
+
print("\n" + "=" * 70)
|
| 137 |
+
print("📊 MIGRATION SUMMARY")
|
| 138 |
+
print("=" * 70)
|
| 139 |
+
print(f"Total Records: {total_records}")
|
| 140 |
+
print(f"✅ Updated: {updated_count}")
|
| 141 |
+
print(f"⏭️ Skipped: {skipped_count}")
|
| 142 |
+
print("=" * 70)
|
| 143 |
+
|
| 144 |
+
if updated_count > 0:
|
| 145 |
+
print("\n🎉 Migration completed successfully!")
|
| 146 |
+
print(" You can now run auto_train.py to use the new columns.")
|
| 147 |
+
else:
|
| 148 |
+
print("\n⚠️ No records were updated. Check if data exists in the table.")
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
try:
|
| 153 |
+
migrate_data()
|
| 154 |
+
except Exception as e:
|
| 155 |
+
print(f"\n❌ Migration failed: {e}")
|
| 156 |
+
import traceback
|
| 157 |
+
|
| 158 |
+
traceback.print_exc()
|
Migration_Scripts/scheduled_self_learning.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Scheduled Self-Learning System
|
| 3 |
+
Automatically checks for new user data and triggers retraining when threshold is met.
|
| 4 |
+
Run this as a background service for continuous learning.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import subprocess
|
| 9 |
+
import time
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
|
| 12 |
+
import schedule
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
from supabase import Client, create_client
|
| 15 |
+
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
# Supabase Configuration
|
| 19 |
+
SUPABASE_URL = os.environ.get("NEXT_PUBLIC_SUPABASE_URL") or os.environ.get(
|
| 20 |
+
"SUPABASE_URL"
|
| 21 |
+
)
|
| 22 |
+
SUPABASE_KEY = os.environ.get("NEXT_PUBLIC_SUPABASE_ANON_KEY") or os.environ.get(
|
| 23 |
+
"SUPABASE_KEY"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 27 |
+
print("❌ Error: Supabase credentials not found")
|
| 28 |
+
exit(1)
|
| 29 |
+
|
| 30 |
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 31 |
+
|
| 32 |
+
# Configuration
|
| 33 |
+
MIN_NEW_SAMPLES = 20 # Minimum new predictions before retraining
|
| 34 |
+
LAST_TRAINED_FILE = "training_logs/last_training_timestamp.txt"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def get_last_training_timestamp():
|
| 38 |
+
"""Get the timestamp of the last training session"""
|
| 39 |
+
if os.path.exists(LAST_TRAINED_FILE):
|
| 40 |
+
with open(LAST_TRAINED_FILE, "r") as f:
|
| 41 |
+
return f.read().strip()
|
| 42 |
+
return "1970-01-01T00:00:00" # Beginning of time
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def update_last_training_timestamp():
|
| 46 |
+
"""Update the last training timestamp"""
|
| 47 |
+
os.makedirs("training_logs", exist_ok=True)
|
| 48 |
+
with open(LAST_TRAINED_FILE, "w") as f:
|
| 49 |
+
f.write(datetime.now().isoformat())
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def count_new_predictions():
|
| 53 |
+
"""Count predictions made since last training"""
|
| 54 |
+
try:
|
| 55 |
+
last_trained = get_last_training_timestamp()
|
| 56 |
+
|
| 57 |
+
# Count records with final_breakdown updated after last training
|
| 58 |
+
result = (
|
| 59 |
+
supabase.table("smartwatt_training")
|
| 60 |
+
.select("*", count="exact")
|
| 61 |
+
.filter("updated_at", "gte", last_trained)
|
| 62 |
+
.filter("final_breakdown", "neq", "null")
|
| 63 |
+
.execute()
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
count = result.count if hasattr(result, "count") else len(result.data)
|
| 67 |
+
return count
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Error counting predictions: {e}")
|
| 70 |
+
return 0
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def check_and_retrain():
|
| 74 |
+
"""Check if retraining is needed and execute if threshold met"""
|
| 75 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 76 |
+
|
| 77 |
+
print("\n" + "=" * 70)
|
| 78 |
+
print(f"🔍 SELF-LEARNING CHECK at {timestamp}")
|
| 79 |
+
print("=" * 70)
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
new_count = count_new_predictions()
|
| 83 |
+
|
| 84 |
+
print(f" New predictions since last training: {new_count}")
|
| 85 |
+
print(f" Threshold for retraining: {MIN_NEW_SAMPLES}")
|
| 86 |
+
|
| 87 |
+
if new_count >= MIN_NEW_SAMPLES:
|
| 88 |
+
print(f"\n✅ Threshold met! Starting automatic retraining...")
|
| 89 |
+
print(f" 🧠 AI will learn from {new_count} new real-world predictions\n")
|
| 90 |
+
|
| 91 |
+
# Run auto_train.py
|
| 92 |
+
result = subprocess.run(
|
| 93 |
+
["python", "auto_train.py"],
|
| 94 |
+
cwd=os.path.dirname(__file__),
|
| 95 |
+
capture_output=True,
|
| 96 |
+
text=True,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
if result.returncode == 0:
|
| 100 |
+
print("\n✅ Retraining completed successfully!")
|
| 101 |
+
update_last_training_timestamp()
|
| 102 |
+
print(
|
| 103 |
+
f" Next training will occur after {MIN_NEW_SAMPLES} more predictions"
|
| 104 |
+
)
|
| 105 |
+
else:
|
| 106 |
+
print(f"\n❌ Retraining failed with error:")
|
| 107 |
+
print(result.stderr)
|
| 108 |
+
else:
|
| 109 |
+
remaining = MIN_NEW_SAMPLES - new_count
|
| 110 |
+
print(f"\n⏳ Waiting for more data...")
|
| 111 |
+
print(f" Need {remaining} more predictions before retraining")
|
| 112 |
+
print(f" Current: {new_count}/{MIN_NEW_SAMPLES}")
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"\n❌ Error during check: {e}")
|
| 116 |
+
|
| 117 |
+
print("=" * 70 + "\n")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def run_scheduler():
|
| 121 |
+
"""Main scheduler loop"""
|
| 122 |
+
print("\n" + "=" * 70)
|
| 123 |
+
print("🚀 SMARTWATT SELF-LEARNING SCHEDULER STARTED")
|
| 124 |
+
print("=" * 70)
|
| 125 |
+
print(f" Check interval: Every 6 hours")
|
| 126 |
+
print(f" Retraining threshold: {MIN_NEW_SAMPLES} new predictions")
|
| 127 |
+
print(f" Status: Monitoring for new user data...")
|
| 128 |
+
print("=" * 70 + "\n")
|
| 129 |
+
|
| 130 |
+
# Schedule checks every 6 hours
|
| 131 |
+
schedule.every(6).hours.do(check_and_retrain)
|
| 132 |
+
|
| 133 |
+
# For testing: check every 10 minutes (uncomment to use)
|
| 134 |
+
# schedule.every(10).minutes.do(check_and_retrain)
|
| 135 |
+
|
| 136 |
+
# Run initial check
|
| 137 |
+
check_and_retrain()
|
| 138 |
+
|
| 139 |
+
# Keep running
|
| 140 |
+
try:
|
| 141 |
+
while True:
|
| 142 |
+
schedule.run_pending()
|
| 143 |
+
time.sleep(60) # Check every minute for scheduled tasks
|
| 144 |
+
except KeyboardInterrupt:
|
| 145 |
+
print("\n\n🛑 Scheduler stopped by user")
|
| 146 |
+
print(" Self-learning will resume when scheduler is restarted")
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
if __name__ == "__main__":
|
| 150 |
+
run_scheduler()
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: uvicorn main:app --host 0.0.0.0 --port $PORT
|
README.md
CHANGED
|
@@ -1,11 +1,104 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="center">
|
| 2 |
+
<img src="https://img.shields.io/badge/Python-3.10%2B-3776AB" alt="Python" />
|
| 3 |
+
<img src="https://img.shields.io/badge/FastAPI-0.110-009688" alt="FastAPI" />
|
| 4 |
+
<img src="https://img.shields.io/badge/TensorFlow-2.16-FF6F00" alt="TensorFlow" />
|
| 5 |
+
<img src="https://img.shields.io/badge/Scikit--learn-1.7-F7931E" alt="Scikit-learn" />
|
| 6 |
+
<img src="https://img.shields.io/badge/License-MIT-2E7D32" alt="License" />
|
| 7 |
+
</p>
|
| 8 |
+
|
| 9 |
+
# SmartWatt Backend
|
| 10 |
+
|
| 11 |
+
SmartWatt Backend is a FastAPI service for appliance-level electricity estimation. It combines trained machine learning models with deterministic physics rules to provide stable and explainable predictions for Kerala household usage patterns.
|
| 12 |
+
|
| 13 |
+
## Core Capabilities
|
| 14 |
+
|
| 15 |
+
- Hybrid inference pipeline: model-based inference for complex loads and physics-first calculations for linear loads.
|
| 16 |
+
- Batch and single-appliance prediction APIs.
|
| 17 |
+
- KSEB slab-aware bill estimation.
|
| 18 |
+
- Anomaly diagnostics and confidence metadata.
|
| 19 |
+
- Simulation endpoint for what-if savings scenarios.
|
| 20 |
+
- Self-learning bias adjustment for recurring users.
|
| 21 |
+
|
| 22 |
+
## Interface Preview
|
| 23 |
+
|
| 24 |
+
| API Documentation | Backend Outputs |
|
| 25 |
+
|---|---|
|
| 26 |
+
| <img src="../docs/screenshots/backend-api-docs.png" alt="Backend API docs" width="100%" /> | <img src="../docs/screenshots/smartwatt-results.png" alt="Backend output sample" width="100%" /> |
|
| 27 |
+
|
| 28 |
+
If images are not visible yet, add files in `docs/screenshots/` from the repository root.
|
| 29 |
+
|
| 30 |
+
## Technology Stack
|
| 31 |
+
|
| 32 |
+
- Python 3.10+
|
| 33 |
+
- FastAPI 0.110
|
| 34 |
+
- TensorFlow 2.16
|
| 35 |
+
- Scikit-learn 1.7
|
| 36 |
+
- Pandas and NumPy
|
| 37 |
+
|
| 38 |
+
## Project Layout
|
| 39 |
+
|
| 40 |
+
- `main.py`: application bootstrap and base endpoints.
|
| 41 |
+
- `routers/appliances.py`: prediction and simulation routes.
|
| 42 |
+
- `predictor.py`: model loading, hybrid inference, and confidence logic.
|
| 43 |
+
- `physics_engine.py`: deterministic appliance power rules.
|
| 44 |
+
- `kseb_tariff.py`: tariff and billing computations.
|
| 45 |
+
- `services/`: normalization, batch prediction, load balancing, and learning pipeline.
|
| 46 |
+
- `tests/`: unit, integration, e2e, system, and validation suites.
|
| 47 |
+
|
| 48 |
+
## Local Setup
|
| 49 |
+
|
| 50 |
+
1. Move to the backend directory.
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
cd Backend
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
2. Create and activate a virtual environment.
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
python -m venv .venv
|
| 60 |
+
.venv\Scripts\activate
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
3. Install dependencies.
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
pip install --upgrade pip
|
| 67 |
+
pip install -r requirements.txt
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
4. Start the API server.
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
python -m uvicorn main:app --reload --port 8000
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## API Endpoints
|
| 77 |
+
|
| 78 |
+
- `GET /`: service status.
|
| 79 |
+
- `GET /health`: health probe.
|
| 80 |
+
- `POST /calculate-bill`: KSEB bill calculation.
|
| 81 |
+
- `POST /predict-appliance`: single appliance prediction.
|
| 82 |
+
- `POST /predict-all`: batch prediction across appliances.
|
| 83 |
+
- `POST /simulate-savings`: scenario-based optimization insights.
|
| 84 |
+
|
| 85 |
+
## Runtime Characteristics
|
| 86 |
+
|
| 87 |
+
- Preloads appliance models during startup for faster first-response behavior.
|
| 88 |
+
- Uses schema validation and field mapping before prediction execution.
|
| 89 |
+
- Balances modeled appliances with capped system load for realistic totals.
|
| 90 |
+
- Supports anomaly flags and confidence metadata in response insights.
|
| 91 |
+
|
| 92 |
+
## Testing
|
| 93 |
+
|
| 94 |
+
Run tests from the backend directory.
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
pytest tests/unit
|
| 98 |
+
pytest tests/integration
|
| 99 |
+
python tests/system/run_backend_tests.py
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
## License
|
| 103 |
+
|
| 104 |
+
Licensed under the MIT License. See `LICENSE`.
|
Training_Scripts/future_train.py
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
import joblib
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from supabase import Client, create_client
|
| 11 |
+
|
| 12 |
+
# 1. Environment Setup
|
| 13 |
+
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
import tensorflow as tf
|
| 17 |
+
from sklearn.compose import ColumnTransformer
|
| 18 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
|
| 19 |
+
# Machine Learning & Deep Learning Libraries
|
| 20 |
+
from sklearn.model_selection import train_test_split
|
| 21 |
+
from sklearn.preprocessing import OneHotEncoder, StandardScaler
|
| 22 |
+
from tensorflow.keras.callbacks import EarlyStopping
|
| 23 |
+
from tensorflow.keras.layers import BatchNormalization, Dense, Dropout
|
| 24 |
+
from tensorflow.keras.models import Sequential
|
| 25 |
+
from tensorflow.keras.optimizers import Adam
|
| 26 |
+
|
| 27 |
+
# Supabase Configuration
|
| 28 |
+
SUPABASE_URL = os.environ.get("NEXT_PUBLIC_SUPABASE_URL") or os.environ.get(
|
| 29 |
+
"SUPABASE_URL"
|
| 30 |
+
)
|
| 31 |
+
SUPABASE_KEY = os.environ.get("NEXT_PUBLIC_SUPABASE_ANON_KEY") or os.environ.get(
|
| 32 |
+
"SUPABASE_KEY"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 36 |
+
print("❌ Error: Supabase credentials not found in environment variables.")
|
| 37 |
+
print(
|
| 38 |
+
"Please set SUPABASE_URL and SUPABASE_KEY (or NEXT_PUBLIC_...) in your .env file or environment."
|
| 39 |
+
)
|
| 40 |
+
exit(1)
|
| 41 |
+
|
| 42 |
+
# Initialize Supabase Client
|
| 43 |
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 44 |
+
|
| 45 |
+
# Create directories for models and backups
|
| 46 |
+
if not os.path.exists("models"):
|
| 47 |
+
os.makedirs("models")
|
| 48 |
+
if not os.path.exists("models/backups"):
|
| 49 |
+
os.makedirs("models/backups")
|
| 50 |
+
if not os.path.exists("training_data"):
|
| 51 |
+
os.makedirs("training_data")
|
| 52 |
+
if not os.path.exists("training_logs"):
|
| 53 |
+
os.makedirs("training_logs")
|
| 54 |
+
|
| 55 |
+
# Training History File
|
| 56 |
+
TRAINING_HISTORY_FILE = "training_logs/training_history.json"
|
| 57 |
+
PERFORMANCE_LOG_CSV = "training_logs/model_performance_log.csv"
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def load_training_history():
|
| 61 |
+
"""Load training history from JSON file."""
|
| 62 |
+
if os.path.exists(TRAINING_HISTORY_FILE):
|
| 63 |
+
with open(TRAINING_HISTORY_FILE, "r") as f:
|
| 64 |
+
return json.load(f)
|
| 65 |
+
return {}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def save_training_history(history):
|
| 69 |
+
"""Save training history to JSON file."""
|
| 70 |
+
with open(TRAINING_HISTORY_FILE, "w") as f:
|
| 71 |
+
json.dump(history, f, indent=2)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def backup_existing_model(appliance_name):
|
| 75 |
+
"""Backup existing model files before training new one."""
|
| 76 |
+
model_path = f"models/{appliance_name}_model.keras"
|
| 77 |
+
preprocessor_path = f"models/{appliance_name}_preprocessor.pkl"
|
| 78 |
+
|
| 79 |
+
if os.path.exists(model_path):
|
| 80 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 81 |
+
backup_dir = f"models/backups/{appliance_name}"
|
| 82 |
+
os.makedirs(backup_dir, exist_ok=True)
|
| 83 |
+
|
| 84 |
+
shutil.copy2(model_path, f"{backup_dir}/model_{timestamp}.keras")
|
| 85 |
+
if os.path.exists(preprocessor_path):
|
| 86 |
+
shutil.copy2(
|
| 87 |
+
preprocessor_path, f"{backup_dir}/preprocessor_{timestamp}.pkl"
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
print(f" 📦 Backed up existing model to {backup_dir}/")
|
| 91 |
+
return True
|
| 92 |
+
return False
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def compare_models(appliance_name, new_metrics, old_metrics=None):
|
| 96 |
+
"""
|
| 97 |
+
Compare new model with old model performance.
|
| 98 |
+
Returns True if new model is better.
|
| 99 |
+
"""
|
| 100 |
+
if old_metrics is None:
|
| 101 |
+
print(f" ✨ First model for {appliance_name} - Auto-accepting")
|
| 102 |
+
return True
|
| 103 |
+
|
| 104 |
+
# Compare based on multiple metrics
|
| 105 |
+
mae_improvement = (
|
| 106 |
+
(old_metrics["mae"] - new_metrics["mae"]) / old_metrics["mae"]
|
| 107 |
+
) * 100
|
| 108 |
+
r2_improvement = (
|
| 109 |
+
(new_metrics["r2"] - old_metrics.get("r2", 0))
|
| 110 |
+
/ max(old_metrics.get("r2", 0.01), 0.01)
|
| 111 |
+
) * 100
|
| 112 |
+
|
| 113 |
+
print(f"\n 📊 Performance Comparison:")
|
| 114 |
+
print(
|
| 115 |
+
f" MAE: {old_metrics['mae']:.3f} → {new_metrics['mae']:.3f} ({mae_improvement:+.1f}%)"
|
| 116 |
+
)
|
| 117 |
+
print(
|
| 118 |
+
f" R²: {old_metrics.get('r2', 0):.3f} → {new_metrics['r2']:.3f} ({r2_improvement:+.1f}%)"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
# Decision logic: Accept if MAE improved OR R2 improved significantly
|
| 122 |
+
if new_metrics["mae"] < old_metrics["mae"] or new_metrics["r2"] > old_metrics.get(
|
| 123 |
+
"r2", 0
|
| 124 |
+
):
|
| 125 |
+
print(f" ✅ New model is BETTER - Accepting")
|
| 126 |
+
return True
|
| 127 |
+
else:
|
| 128 |
+
print(f" ❌ Old model is BETTER - Keeping existing")
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def restore_backup_model(appliance_name):
|
| 133 |
+
"""Restore the most recent backup if new model is worse."""
|
| 134 |
+
backup_dir = f"models/backups/{appliance_name}"
|
| 135 |
+
if not os.path.exists(backup_dir):
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
backups = sorted([f for f in os.listdir(backup_dir) if f.startswith("model_")])
|
| 139 |
+
if not backups:
|
| 140 |
+
return False
|
| 141 |
+
|
| 142 |
+
latest_backup = backups[-1]
|
| 143 |
+
timestamp = latest_backup.replace("model_", "").replace(".keras", "")
|
| 144 |
+
|
| 145 |
+
shutil.copy2(
|
| 146 |
+
f"{backup_dir}/model_{timestamp}.keras", f"models/{appliance_name}_model.keras"
|
| 147 |
+
)
|
| 148 |
+
shutil.copy2(
|
| 149 |
+
f"{backup_dir}/preprocessor_{timestamp}.pkl",
|
| 150 |
+
f"models/{appliance_name}_preprocessor.pkl",
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
print(f" ↩️ Restored previous model from backup")
|
| 154 |
+
return True
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def log_performance_to_csv(appliance_name, metrics, accepted):
|
| 158 |
+
"""Log model performance to CSV for analysis."""
|
| 159 |
+
log_entry = {
|
| 160 |
+
"timestamp": datetime.now().isoformat(),
|
| 161 |
+
"appliance": appliance_name,
|
| 162 |
+
"mae": metrics["mae"],
|
| 163 |
+
"rmse": metrics["rmse"],
|
| 164 |
+
"r2": metrics["r2"],
|
| 165 |
+
"samples": metrics["samples"],
|
| 166 |
+
"accepted": accepted,
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
df_new = pd.DataFrame([log_entry])
|
| 170 |
+
|
| 171 |
+
if os.path.exists(PERFORMANCE_LOG_CSV):
|
| 172 |
+
df_existing = pd.read_csv(PERFORMANCE_LOG_CSV)
|
| 173 |
+
df_combined = pd.concat([df_existing, df_new], ignore_index=True)
|
| 174 |
+
df_combined.to_csv(PERFORMANCE_LOG_CSV, index=False)
|
| 175 |
+
else:
|
| 176 |
+
df_new.to_csv(PERFORMANCE_LOG_CSV, index=False)
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def export_training_data_to_csv(df, filename="training_data/latest_training_data.csv"):
|
| 180 |
+
"""Export the current training dataset to CSV for analysis."""
|
| 181 |
+
df.to_csv(filename, index=False)
|
| 182 |
+
print(f"\n💾 Training data exported to: {filename}")
|
| 183 |
+
print(f" Total records: {len(df)}")
|
| 184 |
+
print(f" File size: {os.path.getsize(filename) / 1024:.1f} KB")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def fetch_training_data():
|
| 188 |
+
"""
|
| 189 |
+
Fetches all data from the smartwatt_training table.
|
| 190 |
+
Extracts ACTUAL vs PREDICTED kWh and calculates prediction errors for self-learning.
|
| 191 |
+
"""
|
| 192 |
+
print("🔄 Connecting to Supabase...")
|
| 193 |
+
try:
|
| 194 |
+
# Fetch all rows (Supabase limits to 1000 by default, using range for more if needed)
|
| 195 |
+
# For now, assuming < 1000 rows or simple fetch is enough.
|
| 196 |
+
# For production with >1000 rows, you'd need pagination.
|
| 197 |
+
response = supabase.table("smartwatt_training").select("*").execute()
|
| 198 |
+
data = response.data
|
| 199 |
+
|
| 200 |
+
if not data:
|
| 201 |
+
print("⚠️ No data found in 'smartwatt_training' table.")
|
| 202 |
+
return None
|
| 203 |
+
|
| 204 |
+
df = pd.DataFrame(data)
|
| 205 |
+
print(f"✅ Successfully fetched {len(df)} training records from Supabase.")
|
| 206 |
+
|
| 207 |
+
# === DEBUG: Check what's in final_breakdown ===
|
| 208 |
+
print(f"\n🔍 DEBUG: Checking final_breakdown column...")
|
| 209 |
+
print(f" Column exists: {'final_breakdown' in df.columns}")
|
| 210 |
+
if "final_breakdown" in df.columns:
|
| 211 |
+
non_null = df["final_breakdown"].notna().sum()
|
| 212 |
+
print(f" Non-null final_breakdown: {non_null}/{len(df)}")
|
| 213 |
+
if non_null > 0:
|
| 214 |
+
# Show first non-null sample
|
| 215 |
+
first_result = df[df["final_breakdown"].notna()].iloc[0][
|
| 216 |
+
"final_breakdown"
|
| 217 |
+
]
|
| 218 |
+
print(f" Sample type: {type(first_result)}")
|
| 219 |
+
if isinstance(first_result, str):
|
| 220 |
+
print(f" Sample (first 200 chars): {first_result[:200]}...")
|
| 221 |
+
elif isinstance(first_result, dict):
|
| 222 |
+
print(f" Sample keys: {list(first_result.keys())}")
|
| 223 |
+
|
| 224 |
+
# === SELF-LEARNING: EXTRACT ACTUAL VS PREDICTED kWh ===
|
| 225 |
+
print("\n🧮 Calculating Prediction Errors for Self-Learning...")
|
| 226 |
+
|
| 227 |
+
# Use dedicated columns directly (much faster than JSON parsing)
|
| 228 |
+
print(" ✅ Using dedicated input_kwh and predicted_kwh columns")
|
| 229 |
+
df["actual_kwh"] = df["input_kwh"].fillna(0)
|
| 230 |
+
df["predicted_total_kwh"] = df["predicted_kwh"].fillna(0)
|
| 231 |
+
|
| 232 |
+
# Debug: Show sample of what was extracted
|
| 233 |
+
samples_with_predictions = df[df["predicted_total_kwh"] > 0]
|
| 234 |
+
print(f" Found {len(samples_with_predictions)} records with predictions")
|
| 235 |
+
if len(samples_with_predictions) > 0:
|
| 236 |
+
sample_row = samples_with_predictions.iloc[0]
|
| 237 |
+
print(
|
| 238 |
+
f" Sample: Actual={sample_row.get('actual_kwh', 0):.1f} kWh, Predicted={sample_row.get('predicted_total_kwh', 0):.1f} kWh"
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Calculate Prediction Error (Core of Self-Learning)
|
| 242 |
+
df["prediction_error"] = df["actual_kwh"] - df["predicted_total_kwh"]
|
| 243 |
+
df["error_percentage"] = (
|
| 244 |
+
(df["prediction_error"] / df["actual_kwh"]) * 100
|
| 245 |
+
).fillna(0)
|
| 246 |
+
|
| 247 |
+
# Absolute error for filtering
|
| 248 |
+
df["abs_error_percentage"] = df["error_percentage"].abs()
|
| 249 |
+
|
| 250 |
+
# Print Error Statistics
|
| 251 |
+
valid_predictions = df[df["predicted_total_kwh"] > 0]
|
| 252 |
+
if len(valid_predictions) > 0:
|
| 253 |
+
print(f"\n📊 Error Analysis:")
|
| 254 |
+
print(f" Total Samples: {len(df)}")
|
| 255 |
+
print(f" With Predictions: {len(valid_predictions)}")
|
| 256 |
+
print(
|
| 257 |
+
f" Average Error: {valid_predictions['prediction_error'].mean():.2f} kWh"
|
| 258 |
+
)
|
| 259 |
+
print(
|
| 260 |
+
f" Average Error %: {valid_predictions['error_percentage'].abs().mean():.2f}%"
|
| 261 |
+
)
|
| 262 |
+
print(
|
| 263 |
+
f" Median Error %: {valid_predictions['error_percentage'].abs().median():.2f}%"
|
| 264 |
+
)
|
| 265 |
+
print(
|
| 266 |
+
f" Max Over-prediction: {valid_predictions['prediction_error'].min():.2f} kWh"
|
| 267 |
+
)
|
| 268 |
+
print(
|
| 269 |
+
f" Max Under-prediction: {valid_predictions['prediction_error'].max():.2f} kWh"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Count accuracy tiers
|
| 273 |
+
high_accuracy = len(
|
| 274 |
+
valid_predictions[valid_predictions["abs_error_percentage"] <= 10]
|
| 275 |
+
)
|
| 276 |
+
medium_accuracy = len(
|
| 277 |
+
valid_predictions[
|
| 278 |
+
(valid_predictions["abs_error_percentage"] > 10)
|
| 279 |
+
& (valid_predictions["abs_error_percentage"] <= 20)
|
| 280 |
+
]
|
| 281 |
+
)
|
| 282 |
+
low_accuracy = len(
|
| 283 |
+
valid_predictions[valid_predictions["abs_error_percentage"] > 20]
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
print(f"\n Accuracy Tiers:")
|
| 287 |
+
print(
|
| 288 |
+
f" ✅ High (<10% error): {high_accuracy} samples ({high_accuracy/len(valid_predictions)*100:.1f}%)"
|
| 289 |
+
)
|
| 290 |
+
print(
|
| 291 |
+
f" ⚠️ Medium (10-20%): {medium_accuracy} samples ({medium_accuracy/len(valid_predictions)*100:.1f}%)"
|
| 292 |
+
)
|
| 293 |
+
print(
|
| 294 |
+
f" ❌ Low (>20% error): {low_accuracy} samples ({low_accuracy/len(valid_predictions)*100:.1f}%)"
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
# === DETAILED ACTUAL VS PREDICTED COMPARISON ===
|
| 298 |
+
print(f"\n" + "=" * 80)
|
| 299 |
+
print(f"📋 ACTUAL vs PREDICTED COMPARISON (All Records)")
|
| 300 |
+
print(f"=" * 80)
|
| 301 |
+
print(
|
| 302 |
+
f"{'ID':<8} {'Actual kWh':<12} {'Predicted kWh':<15} {'Error':<10} {'Error %':<10} {'Status':<15}"
|
| 303 |
+
)
|
| 304 |
+
print(f"-" * 80)
|
| 305 |
+
|
| 306 |
+
for idx, row in valid_predictions.iterrows():
|
| 307 |
+
record_id = (
|
| 308 |
+
row.get("id", idx)[:8]
|
| 309 |
+
if isinstance(row.get("id"), str)
|
| 310 |
+
else str(idx)[:8]
|
| 311 |
+
)
|
| 312 |
+
actual = row["actual_kwh"]
|
| 313 |
+
predicted = row["predicted_total_kwh"]
|
| 314 |
+
error = row["prediction_error"]
|
| 315 |
+
error_pct = row["error_percentage"]
|
| 316 |
+
abs_error_pct = row["abs_error_percentage"]
|
| 317 |
+
|
| 318 |
+
# Determine status
|
| 319 |
+
if abs_error_pct <= 10:
|
| 320 |
+
status = "✅ High Acc"
|
| 321 |
+
elif abs_error_pct <= 20:
|
| 322 |
+
status = "⚠️ Medium Acc"
|
| 323 |
+
else:
|
| 324 |
+
status = "❌ Low Acc"
|
| 325 |
+
|
| 326 |
+
# Format error percentage as string with % symbol
|
| 327 |
+
error_pct_str = f"{error_pct:.1f}%"
|
| 328 |
+
print(
|
| 329 |
+
f"{record_id:<8} {actual:<12.1f} {predicted:<15.1f} {error:<10.1f} {error_pct_str:<10} {status:<15}"
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
print(f"=" * 80)
|
| 333 |
+
print(
|
| 334 |
+
f"Summary: {high_accuracy} excellent, {medium_accuracy} good, {low_accuracy} needs improvement"
|
| 335 |
+
)
|
| 336 |
+
print(f"=" * 80 + "\n")
|
| 337 |
+
|
| 338 |
+
print(f"\n✅ Successfully fetched {len(df)} training records from Supabase.")
|
| 339 |
+
|
| 340 |
+
# --- DATA TRANSFORMATION: EXTRACT PER-APPLIANCE TARGETS ---
|
| 341 |
+
print("\n🔍 Extracting per-appliance consumption targets...")
|
| 342 |
+
|
| 343 |
+
# Helper to extract kwh from final_breakdown predictions
|
| 344 |
+
def extract_appliance_kwh(row, appliance_key):
|
| 345 |
+
"""Extract predicted kWh for specific appliance"""
|
| 346 |
+
try:
|
| 347 |
+
results = row.get("final_breakdown")
|
| 348 |
+
if not results:
|
| 349 |
+
return 0.0
|
| 350 |
+
|
| 351 |
+
# Handle JSON string (Supabase returns JSONB as string sometimes)
|
| 352 |
+
if isinstance(results, str):
|
| 353 |
+
import json
|
| 354 |
+
|
| 355 |
+
results = json.loads(results)
|
| 356 |
+
|
| 357 |
+
if not isinstance(results, dict):
|
| 358 |
+
return 0.0
|
| 359 |
+
|
| 360 |
+
predictions = results.get("predictions", {})
|
| 361 |
+
return float(predictions.get(appliance_key, 0.0))
|
| 362 |
+
except:
|
| 363 |
+
return 0.0
|
| 364 |
+
|
| 365 |
+
# Calculate CORRECTION RATIO for each appliance
|
| 366 |
+
# This is the KEY to self-learning: actual/predicted ratio
|
| 367 |
+
def calculate_correction_ratio(row, appliance_key):
|
| 368 |
+
"""
|
| 369 |
+
Calculate how much the model needs to adjust.
|
| 370 |
+
If predicted = 100 kWh and actual total suggests 120 kWh needed,
|
| 371 |
+
then correction_ratio = 1.2 (need to predict 20% higher)
|
| 372 |
+
"""
|
| 373 |
+
try:
|
| 374 |
+
appliance_pred = extract_appliance_kwh(row, appliance_key)
|
| 375 |
+
if appliance_pred == 0:
|
| 376 |
+
return 1.0 # No prediction, no correction needed
|
| 377 |
+
|
| 378 |
+
total_pred = row.get("predicted_total_kwh", 0)
|
| 379 |
+
actual = row.get("actual_kwh", 0)
|
| 380 |
+
|
| 381 |
+
if total_pred == 0 or actual == 0:
|
| 382 |
+
return 1.0
|
| 383 |
+
|
| 384 |
+
# Global correction factor: actual/predicted
|
| 385 |
+
global_factor = actual / total_pred
|
| 386 |
+
|
| 387 |
+
# Apply to this appliance (proportional correction)
|
| 388 |
+
return global_factor
|
| 389 |
+
except:
|
| 390 |
+
return 1.0
|
| 391 |
+
|
| 392 |
+
# Map of training target columns to JSON keys (matching your ai_results structure)
|
| 393 |
+
target_map = {
|
| 394 |
+
"ac_kwh": "ac",
|
| 395 |
+
"refrigerator_kwh": "fridge",
|
| 396 |
+
"washing_machine_kwh": "washing_machine",
|
| 397 |
+
"water_heater_kwh": "water_heater",
|
| 398 |
+
"water_pump_kwh": "water_pump",
|
| 399 |
+
"television_kwh": "television",
|
| 400 |
+
"ceiling_fans_kwh": "ceiling_fan",
|
| 401 |
+
"led_lights_kwh": "led_light",
|
| 402 |
+
"tube_lights_kwh": "tube_light",
|
| 403 |
+
"cfl_bulbs_kwh": "cfl_bulb",
|
| 404 |
+
"mixer_grinder_kwh": "mixer_grinder",
|
| 405 |
+
"microwave_kwh": "microwave",
|
| 406 |
+
"desktop_kwh": "desktop",
|
| 407 |
+
"laptop_kwh": "laptop",
|
| 408 |
+
"iron_kwh": "iron",
|
| 409 |
+
"kettle_kwh": "kettle",
|
| 410 |
+
"induction_kwh": "induction",
|
| 411 |
+
"rice_cooker_kwh": "rice_cooker",
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
for col, key in target_map.items():
|
| 415 |
+
# Original predicted value
|
| 416 |
+
df[col] = df.apply(lambda row: extract_appliance_kwh(row, key), axis=1)
|
| 417 |
+
|
| 418 |
+
# SELF-LEARNING: Corrected target (what model SHOULD have predicted)
|
| 419 |
+
correction_col = f"{col}_corrected"
|
| 420 |
+
df[correction_col] = df.apply(
|
| 421 |
+
lambda row: extract_appliance_kwh(row, key)
|
| 422 |
+
* calculate_correction_ratio(row, key),
|
| 423 |
+
axis=1,
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
print(f"✅ Extracted {len(target_map)} appliance types with correction ratios")
|
| 427 |
+
|
| 428 |
+
# --- EXTRACT FEATURES FROM APPLIANCE_USAGE ---
|
| 429 |
+
print("\n📋 Extracting features from appliance_usage JSON...")
|
| 430 |
+
|
| 431 |
+
def extract_feature(row, feature_key):
|
| 432 |
+
try:
|
| 433 |
+
usage = row.get("appliance_usage")
|
| 434 |
+
if not usage:
|
| 435 |
+
return 0.0
|
| 436 |
+
|
| 437 |
+
# Handle JSON string (Supabase returns JSONB as string sometimes)
|
| 438 |
+
if isinstance(usage, str):
|
| 439 |
+
import json
|
| 440 |
+
|
| 441 |
+
usage = json.loads(usage)
|
| 442 |
+
|
| 443 |
+
if isinstance(usage, list):
|
| 444 |
+
return 0.0 # Handle legacy list format if any
|
| 445 |
+
|
| 446 |
+
# Check direct key match
|
| 447 |
+
if feature_key in usage:
|
| 448 |
+
return usage[feature_key]
|
| 449 |
+
|
| 450 |
+
# Fallback: hours_per_day -> hours (frontend saves as _hours)
|
| 451 |
+
if feature_key.endswith("_hours_per_day"):
|
| 452 |
+
short_key = feature_key.replace("_hours_per_day", "_hours")
|
| 453 |
+
if short_key in usage:
|
| 454 |
+
return usage[short_key]
|
| 455 |
+
|
| 456 |
+
return usage.get(feature_key, 0.0)
|
| 457 |
+
except:
|
| 458 |
+
return 0.0
|
| 459 |
+
|
| 460 |
+
# List of all possible feature columns needed for training
|
| 461 |
+
all_features = [
|
| 462 |
+
"ac_hours_per_day",
|
| 463 |
+
"ac_tonnage",
|
| 464 |
+
"ac_star_rating",
|
| 465 |
+
"num_ac_units",
|
| 466 |
+
"ac_category",
|
| 467 |
+
"ac_min_hours",
|
| 468 |
+
"ac_max_hours",
|
| 469 |
+
"fridge_capacity_liters",
|
| 470 |
+
"fridge_age_years",
|
| 471 |
+
"fridge_star_rating",
|
| 472 |
+
"fridge_category",
|
| 473 |
+
"fridge_min_hours",
|
| 474 |
+
"fridge_max_hours",
|
| 475 |
+
"wm_cycles_per_week",
|
| 476 |
+
"wm_capacity_kg",
|
| 477 |
+
"wm_star_rating",
|
| 478 |
+
"wm_category",
|
| 479 |
+
"wm_min_hours",
|
| 480 |
+
"wm_max_hours",
|
| 481 |
+
"water_heater_usage_hours",
|
| 482 |
+
"water_heater_capacity_liters",
|
| 483 |
+
"geyser_category",
|
| 484 |
+
"geyser_min_hours",
|
| 485 |
+
"geyser_max_hours",
|
| 486 |
+
"water_pump_usage_hours_per_day",
|
| 487 |
+
"water_pump_hp",
|
| 488 |
+
"pump_category",
|
| 489 |
+
"pump_min_hours",
|
| 490 |
+
"pump_max_hours",
|
| 491 |
+
"tv_hours_per_day",
|
| 492 |
+
"tv_size_inches",
|
| 493 |
+
"num_televisions",
|
| 494 |
+
"tv_category",
|
| 495 |
+
"tv_min_hours",
|
| 496 |
+
"tv_max_hours",
|
| 497 |
+
"mixer_grinder_usage_minutes_per_day",
|
| 498 |
+
"mixer_grinder_wattage",
|
| 499 |
+
"mixer_category",
|
| 500 |
+
"mixer_min_hours",
|
| 501 |
+
"mixer_max_hours",
|
| 502 |
+
"microwave_usage_minutes_per_day",
|
| 503 |
+
"microwave_capacity_liters",
|
| 504 |
+
"microwave_category",
|
| 505 |
+
"microwave_min_hours",
|
| 506 |
+
"microwave_max_hours",
|
| 507 |
+
"desktop_hours_per_day",
|
| 508 |
+
"desktop_category",
|
| 509 |
+
"desktop_min_hours",
|
| 510 |
+
"desktop_max_hours",
|
| 511 |
+
"laptop_hours_per_day",
|
| 512 |
+
"laptop_category",
|
| 513 |
+
"laptop_min_hours",
|
| 514 |
+
"laptop_max_hours",
|
| 515 |
+
"iron_hours_per_day",
|
| 516 |
+
"iron_category",
|
| 517 |
+
"iron_min_hours",
|
| 518 |
+
"iron_max_hours",
|
| 519 |
+
"kettle_hours_per_day",
|
| 520 |
+
"kettle_category",
|
| 521 |
+
"kettle_min_hours",
|
| 522 |
+
"kettle_max_hours",
|
| 523 |
+
"induction_hours_per_day",
|
| 524 |
+
"induction_category",
|
| 525 |
+
"induction_min_hours",
|
| 526 |
+
"induction_max_hours",
|
| 527 |
+
"rice_cooker_hours_per_day",
|
| 528 |
+
"rice_cooker_category",
|
| 529 |
+
"rice_cooker_min_hours",
|
| 530 |
+
"rice_cooker_max_hours",
|
| 531 |
+
"num_ceiling_fans",
|
| 532 |
+
"fan_hours_per_day",
|
| 533 |
+
"fan_star_rating",
|
| 534 |
+
"fan_category",
|
| 535 |
+
"fan_min_hours",
|
| 536 |
+
"fan_max_hours",
|
| 537 |
+
"num_led_lights",
|
| 538 |
+
"light_hours_per_day",
|
| 539 |
+
"led_category",
|
| 540 |
+
"led_min_hours",
|
| 541 |
+
"led_max_hours",
|
| 542 |
+
"num_tube_lights",
|
| 543 |
+
"tube_category",
|
| 544 |
+
"tube_min_hours",
|
| 545 |
+
"tube_max_hours",
|
| 546 |
+
"num_cfl_bulbs",
|
| 547 |
+
"cfl_category",
|
| 548 |
+
"cfl_min_hours",
|
| 549 |
+
"cfl_max_hours",
|
| 550 |
+
"toaster_hours_per_day",
|
| 551 |
+
"toaster_category",
|
| 552 |
+
"toaster_min_hours",
|
| 553 |
+
"toaster_max_hours",
|
| 554 |
+
"food_processor_hours_per_day",
|
| 555 |
+
"food_processor_category",
|
| 556 |
+
"food_processor_min_hours",
|
| 557 |
+
"food_processor_max_hours",
|
| 558 |
+
"hair_dryer_hours_per_day",
|
| 559 |
+
"hair_dryer_category",
|
| 560 |
+
"hair_dryer_min_hours",
|
| 561 |
+
"hair_dryer_max_hours",
|
| 562 |
+
"vacuum_hours_per_day",
|
| 563 |
+
"vacuum_category",
|
| 564 |
+
"vacuum_min_hours",
|
| 565 |
+
"vacuum_max_hours",
|
| 566 |
+
]
|
| 567 |
+
|
| 568 |
+
# Extract all features
|
| 569 |
+
print(f"\n📋 Extracting features from appliance_usage JSON...")
|
| 570 |
+
|
| 571 |
+
# Build a dict to batch-add columns (avoids DataFrame fragmentation)
|
| 572 |
+
feature_data = {}
|
| 573 |
+
for feat in all_features:
|
| 574 |
+
# If column already exists in top-level (e.g. from SQL schema), prefer it
|
| 575 |
+
if feat not in df.columns:
|
| 576 |
+
feature_data[feat] = df.apply(
|
| 577 |
+
lambda row: extract_feature(row, feat), axis=1
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
# Add all new columns at once to avoid fragmentation
|
| 581 |
+
if feature_data:
|
| 582 |
+
feature_df = pd.DataFrame(feature_data, index=df.index)
|
| 583 |
+
df = pd.concat([df, feature_df], axis=1)
|
| 584 |
+
|
| 585 |
+
print(f"✅ Extracted {len(all_features)} feature columns")
|
| 586 |
+
print(f" Total columns in dataset: {len(df.columns)}")
|
| 587 |
+
|
| 588 |
+
return df
|
| 589 |
+
except Exception as e:
|
| 590 |
+
print(f"❌ Error fetching data from Supabase: {e}")
|
| 591 |
+
return None
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
def build_and_train_model(
|
| 595 |
+
df, appliance_name, target_col, filter_col, numeric_features, categorical_features
|
| 596 |
+
):
|
| 597 |
+
"""
|
| 598 |
+
Generic function with SELF-LEARNING: builds, trains, compares, and selects best model.
|
| 599 |
+
NOW USES CORRECTED TARGETS based on actual vs predicted errors!
|
| 600 |
+
"""
|
| 601 |
+
print(f"\n" + "=" * 60)
|
| 602 |
+
print(f"TRAINING MODEL FOR: {appliance_name.upper()}")
|
| 603 |
+
print(f"=" * 60)
|
| 604 |
+
|
| 605 |
+
# --- Step A: Data Preparation with SELF-LEARNING ---
|
| 606 |
+
# Use CORRECTED target if available (learned from actual vs predicted)
|
| 607 |
+
corrected_target_col = f"{target_col}_corrected"
|
| 608 |
+
|
| 609 |
+
if corrected_target_col in df.columns:
|
| 610 |
+
print(f"🎓 Using SELF-LEARNING corrected targets from actual user data!")
|
| 611 |
+
actual_target = corrected_target_col
|
| 612 |
+
else:
|
| 613 |
+
print(f"📊 Using original targets (no correction data available yet)")
|
| 614 |
+
actual_target = target_col
|
| 615 |
+
|
| 616 |
+
# Check if required columns exist
|
| 617 |
+
missing_cols = [
|
| 618 |
+
col
|
| 619 |
+
for col in numeric_features + categorical_features + [target_col]
|
| 620 |
+
if col not in df.columns
|
| 621 |
+
]
|
| 622 |
+
if missing_cols:
|
| 623 |
+
print(f"⚠️ Skipping {appliance_name}: Missing columns in data: {missing_cols}")
|
| 624 |
+
return
|
| 625 |
+
|
| 626 |
+
# 1. Filter: Only learn from homes that HAVE this appliance
|
| 627 |
+
print(f"🔍 Filtering for {appliance_name}...")
|
| 628 |
+
if filter_col:
|
| 629 |
+
if filter_col not in df.columns:
|
| 630 |
+
print(f" {filter_col} missing. Using {target_col} > 0")
|
| 631 |
+
appliance_df = df[df[target_col] > 0].copy()
|
| 632 |
+
else:
|
| 633 |
+
appliance_df = df[df[filter_col] == 1].copy()
|
| 634 |
+
else:
|
| 635 |
+
appliance_df = df[df[target_col] > 0].copy()
|
| 636 |
+
|
| 637 |
+
# SELF-LEARNING: Filter out samples with extreme errors (outliers that would harm training)
|
| 638 |
+
if "abs_error_percentage" in appliance_df.columns:
|
| 639 |
+
before_filter = len(appliance_df)
|
| 640 |
+
appliance_df = appliance_df[
|
| 641 |
+
appliance_df["abs_error_percentage"] <= 100
|
| 642 |
+
].copy() # Remove >100% errors
|
| 643 |
+
if len(appliance_df) < before_filter:
|
| 644 |
+
print(
|
| 645 |
+
f" 🧹 Filtered out {before_filter - len(appliance_df)} extreme outlier samples"
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
print(f" {appliance_name} samples: {len(appliance_df)}")
|
| 649 |
+
|
| 650 |
+
if len(appliance_df) < 10:
|
| 651 |
+
print(
|
| 652 |
+
f"⚠️ Skipping {appliance_name}: Not enough data samples ({len(appliance_df)}). Need at least 10."
|
| 653 |
+
)
|
| 654 |
+
return
|
| 655 |
+
|
| 656 |
+
# 2. Features: Always include total_kwh_monthly (bi_monthly_kwh / 2) if available
|
| 657 |
+
if "total_kwh_monthly" not in appliance_df.columns:
|
| 658 |
+
if "bi_monthly_kwh" in appliance_df.columns:
|
| 659 |
+
appliance_df["total_kwh_monthly"] = appliance_df["bi_monthly_kwh"] / 2
|
| 660 |
+
else:
|
| 661 |
+
print(
|
| 662 |
+
f"⚠️ Skipping {appliance_name}: 'bi_monthly_kwh' column missing for context."
|
| 663 |
+
)
|
| 664 |
+
return
|
| 665 |
+
|
| 666 |
+
features = numeric_features + categorical_features + ["total_kwh_monthly"]
|
| 667 |
+
|
| 668 |
+
# Fill NaNs with 0 or appropriate defaults to prevent crashes
|
| 669 |
+
appliance_df[numeric_features] = appliance_df[numeric_features].fillna(0)
|
| 670 |
+
for cat in categorical_features:
|
| 671 |
+
appliance_df[cat] = appliance_df[cat].fillna("unknown")
|
| 672 |
+
|
| 673 |
+
X = appliance_df[features]
|
| 674 |
+
y = appliance_df[actual_target] # Use corrected target!
|
| 675 |
+
|
| 676 |
+
# Show learning statistics
|
| 677 |
+
if actual_target != target_col:
|
| 678 |
+
original_avg = appliance_df[target_col].mean()
|
| 679 |
+
corrected_avg = appliance_df[actual_target].mean()
|
| 680 |
+
adjustment_pct = (
|
| 681 |
+
((corrected_avg - original_avg) / original_avg * 100)
|
| 682 |
+
if original_avg > 0
|
| 683 |
+
else 0
|
| 684 |
+
)
|
| 685 |
+
print(
|
| 686 |
+
f" 📈 Learning Adjustment: {adjustment_pct:+.1f}% (Original: {original_avg:.1f} → Corrected: {corrected_avg:.1f} kWh)"
|
| 687 |
+
)
|
| 688 |
+
|
| 689 |
+
print(f" Training samples: {len(X)}")
|
| 690 |
+
|
| 691 |
+
# Split Data
|
| 692 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 693 |
+
X, y, test_size=0.2, random_state=42
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
# --- Step B: Preprocessing ---
|
| 697 |
+
preprocessor = ColumnTransformer(
|
| 698 |
+
transformers=[
|
| 699 |
+
("num", StandardScaler(), numeric_features + ["total_kwh_monthly"]),
|
| 700 |
+
(
|
| 701 |
+
"cat",
|
| 702 |
+
OneHotEncoder(handle_unknown="ignore", sparse_output=False),
|
| 703 |
+
categorical_features,
|
| 704 |
+
),
|
| 705 |
+
]
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
X_train_processed = preprocessor.fit_transform(X_train)
|
| 709 |
+
X_test_processed = preprocessor.transform(X_test)
|
| 710 |
+
|
| 711 |
+
# --- SELF-LEARNING: Backup existing model ---
|
| 712 |
+
had_old_model = backup_existing_model(appliance_name)
|
| 713 |
+
|
| 714 |
+
# Save NEW Preprocessor (temporary - will be kept or restored based on performance)
|
| 715 |
+
joblib.dump(preprocessor, f"models/{appliance_name}_preprocessor_temp.pkl")
|
| 716 |
+
|
| 717 |
+
# --- Step C: Neural Network Architecture ---
|
| 718 |
+
model = Sequential(
|
| 719 |
+
[
|
| 720 |
+
Dense(64, activation="relu", input_shape=(X_train_processed.shape[1],)),
|
| 721 |
+
BatchNormalization(),
|
| 722 |
+
Dropout(0.2),
|
| 723 |
+
Dense(32, activation="relu"),
|
| 724 |
+
BatchNormalization(),
|
| 725 |
+
Dropout(0.15),
|
| 726 |
+
Dense(16, activation="relu"),
|
| 727 |
+
Dense(1, activation="linear"),
|
| 728 |
+
]
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
model.compile(
|
| 732 |
+
optimizer=Adam(learning_rate=0.001), loss="mean_squared_error", metrics=["mae"]
|
| 733 |
+
)
|
| 734 |
+
|
| 735 |
+
early_stop = EarlyStopping(
|
| 736 |
+
monitor="val_loss", patience=10, restore_best_weights=True, verbose=0
|
| 737 |
+
)
|
| 738 |
+
|
| 739 |
+
# --- Step D: Training ---
|
| 740 |
+
print("🧠 Neural Network is learning...")
|
| 741 |
+
history = model.fit(
|
| 742 |
+
X_train_processed,
|
| 743 |
+
y_train,
|
| 744 |
+
validation_data=(X_test_processed, y_test),
|
| 745 |
+
epochs=100,
|
| 746 |
+
batch_size=32,
|
| 747 |
+
verbose=0,
|
| 748 |
+
callbacks=[early_stop],
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
# --- Step E: Evaluation ---
|
| 752 |
+
y_pred = model.predict(X_test_processed, verbose=0).flatten()
|
| 753 |
+
mae = mean_absolute_error(y_test, y_pred)
|
| 754 |
+
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
|
| 755 |
+
r2 = r2_score(y_test, y_pred)
|
| 756 |
+
|
| 757 |
+
new_metrics = {
|
| 758 |
+
"mae": mae,
|
| 759 |
+
"rmse": rmse,
|
| 760 |
+
"r2": r2,
|
| 761 |
+
"samples": len(X_train),
|
| 762 |
+
"timestamp": datetime.now().isoformat(),
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
print(f"📈 New Model Performance: MAE={mae:.2f}, RMSE={rmse:.2f}, R²={r2:.3f}")
|
| 766 |
+
|
| 767 |
+
# --- SELF-LEARNING: Compare with old model ---
|
| 768 |
+
training_history = load_training_history()
|
| 769 |
+
old_metrics = training_history.get(appliance_name, {}).get("best_metrics")
|
| 770 |
+
|
| 771 |
+
should_accept = compare_models(appliance_name, new_metrics, old_metrics)
|
| 772 |
+
|
| 773 |
+
if should_accept:
|
| 774 |
+
# Save NEW model as the current best
|
| 775 |
+
model.save(f"models/{appliance_name}_model.keras")
|
| 776 |
+
|
| 777 |
+
# Move temp preprocessor to final location
|
| 778 |
+
os.rename(
|
| 779 |
+
f"models/{appliance_name}_preprocessor_temp.pkl",
|
| 780 |
+
f"models/{appliance_name}_preprocessor.pkl",
|
| 781 |
+
)
|
| 782 |
+
|
| 783 |
+
# Update training history
|
| 784 |
+
training_history[appliance_name] = {
|
| 785 |
+
"best_metrics": new_metrics,
|
| 786 |
+
"training_history": training_history.get(appliance_name, {}).get(
|
| 787 |
+
"training_history", []
|
| 788 |
+
)
|
| 789 |
+
+ [new_metrics],
|
| 790 |
+
"last_trained": datetime.now().isoformat(),
|
| 791 |
+
}
|
| 792 |
+
save_training_history(training_history)
|
| 793 |
+
|
| 794 |
+
print(f"✅ Model SAVED successfully")
|
| 795 |
+
else:
|
| 796 |
+
# Restore old model
|
| 797 |
+
if had_old_model:
|
| 798 |
+
restore_backup_model(appliance_name)
|
| 799 |
+
os.remove(f"models/{appliance_name}_preprocessor_temp.pkl")
|
| 800 |
+
print(f"⏮️ Kept existing model (better performance)")
|
| 801 |
+
|
| 802 |
+
# Log to CSV for analysis
|
| 803 |
+
log_performance_to_csv(appliance_name, new_metrics, should_accept)
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def main():
|
| 807 |
+
print("\n" + "=" * 70)
|
| 808 |
+
print(" 🎓 SELF-LEARNING AI TRAINING SYSTEM")
|
| 809 |
+
print(" 📊 Learning from ACTUAL vs PREDICTED kWh errors")
|
| 810 |
+
print("=" * 70)
|
| 811 |
+
|
| 812 |
+
# 1. Fetch Data with Error Calculations
|
| 813 |
+
df = fetch_training_data()
|
| 814 |
+
if df is None:
|
| 815 |
+
return
|
| 816 |
+
|
| 817 |
+
# Check if we have enough data to train
|
| 818 |
+
valid_samples = len(df[df["predicted_total_kwh"] > 0])
|
| 819 |
+
if valid_samples < 10:
|
| 820 |
+
print(
|
| 821 |
+
f"\n⚠️ Not enough data to train (only {valid_samples} samples with predictions)"
|
| 822 |
+
)
|
| 823 |
+
print(" Need at least 10 user predictions to start self-learning.")
|
| 824 |
+
print(
|
| 825 |
+
" Continue using the system - training will happen automatically as data accumulates!"
|
| 826 |
+
)
|
| 827 |
+
return
|
| 828 |
+
|
| 829 |
+
print(
|
| 830 |
+
f"\n✅ Found {valid_samples} samples with predictions - starting self-learning!"
|
| 831 |
+
)
|
| 832 |
+
|
| 833 |
+
# 2. Train All Models with SELF-LEARNING
|
| 834 |
+
# Note: We map the Supabase columns to the expected feature names.
|
| 835 |
+
|
| 836 |
+
# --- MAJOR APPLIANCES ---
|
| 837 |
+
build_and_train_model(
|
| 838 |
+
df,
|
| 839 |
+
"ac",
|
| 840 |
+
"ac_kwh",
|
| 841 |
+
"has_ac",
|
| 842 |
+
[
|
| 843 |
+
"ac_hours_per_day",
|
| 844 |
+
"ac_tonnage",
|
| 845 |
+
"ac_star_rating",
|
| 846 |
+
"num_ac_units",
|
| 847 |
+
"ac_min_hours",
|
| 848 |
+
"ac_max_hours",
|
| 849 |
+
],
|
| 850 |
+
["ac_type", "ac_category"],
|
| 851 |
+
)
|
| 852 |
+
build_and_train_model(
|
| 853 |
+
df,
|
| 854 |
+
"fridge",
|
| 855 |
+
"refrigerator_kwh",
|
| 856 |
+
"has_refrigerator",
|
| 857 |
+
[
|
| 858 |
+
"fridge_capacity_liters",
|
| 859 |
+
"fridge_age_years",
|
| 860 |
+
"fridge_star_rating",
|
| 861 |
+
"fridge_min_hours",
|
| 862 |
+
"fridge_max_hours",
|
| 863 |
+
],
|
| 864 |
+
["fridge_type", "fridge_category"],
|
| 865 |
+
)
|
| 866 |
+
build_and_train_model(
|
| 867 |
+
df,
|
| 868 |
+
"washing_machine",
|
| 869 |
+
"washing_machine_kwh",
|
| 870 |
+
"has_washing_machine",
|
| 871 |
+
[
|
| 872 |
+
"wm_cycles_per_week",
|
| 873 |
+
"wm_capacity_kg",
|
| 874 |
+
"wm_star_rating",
|
| 875 |
+
"wm_min_hours",
|
| 876 |
+
"wm_max_hours",
|
| 877 |
+
],
|
| 878 |
+
["wm_type", "wm_category"],
|
| 879 |
+
)
|
| 880 |
+
build_and_train_model(
|
| 881 |
+
df,
|
| 882 |
+
"water_heater",
|
| 883 |
+
"water_heater_kwh",
|
| 884 |
+
"has_water_heater",
|
| 885 |
+
[
|
| 886 |
+
"water_heater_usage_hours",
|
| 887 |
+
"water_heater_capacity_liters",
|
| 888 |
+
"geyser_min_hours",
|
| 889 |
+
"geyser_max_hours",
|
| 890 |
+
],
|
| 891 |
+
["water_heater_type", "geyser_category"],
|
| 892 |
+
)
|
| 893 |
+
build_and_train_model(
|
| 894 |
+
df,
|
| 895 |
+
"water_pump",
|
| 896 |
+
"water_pump_kwh",
|
| 897 |
+
"has_water_pump",
|
| 898 |
+
[
|
| 899 |
+
"water_pump_usage_hours_per_day",
|
| 900 |
+
"water_pump_hp",
|
| 901 |
+
"pump_min_hours",
|
| 902 |
+
"pump_max_hours",
|
| 903 |
+
],
|
| 904 |
+
["pump_category"],
|
| 905 |
+
)
|
| 906 |
+
build_and_train_model(
|
| 907 |
+
df,
|
| 908 |
+
"television",
|
| 909 |
+
"television_kwh",
|
| 910 |
+
"has_television",
|
| 911 |
+
[
|
| 912 |
+
"tv_hours_per_day",
|
| 913 |
+
"tv_size_inches",
|
| 914 |
+
"num_televisions",
|
| 915 |
+
"tv_min_hours",
|
| 916 |
+
"tv_max_hours",
|
| 917 |
+
],
|
| 918 |
+
["tv_type", "tv_category"],
|
| 919 |
+
)
|
| 920 |
+
|
| 921 |
+
# --- KITCHEN & SMALL APPLIANCES ---
|
| 922 |
+
build_and_train_model(
|
| 923 |
+
df,
|
| 924 |
+
"mixer_grinder",
|
| 925 |
+
"mixer_grinder_kwh",
|
| 926 |
+
"has_mixer_grinder",
|
| 927 |
+
[
|
| 928 |
+
"mixer_grinder_usage_minutes_per_day",
|
| 929 |
+
"mixer_grinder_wattage",
|
| 930 |
+
"mixer_min_hours",
|
| 931 |
+
"mixer_max_hours",
|
| 932 |
+
],
|
| 933 |
+
["mixer_category"],
|
| 934 |
+
)
|
| 935 |
+
build_and_train_model(
|
| 936 |
+
df,
|
| 937 |
+
"microwave",
|
| 938 |
+
"microwave_kwh",
|
| 939 |
+
"has_microwave",
|
| 940 |
+
[
|
| 941 |
+
"microwave_usage_minutes_per_day",
|
| 942 |
+
"microwave_capacity_liters",
|
| 943 |
+
"microwave_min_hours",
|
| 944 |
+
"microwave_max_hours",
|
| 945 |
+
],
|
| 946 |
+
["microwave_category"],
|
| 947 |
+
)
|
| 948 |
+
|
| 949 |
+
# --- COOLING & LIGHTING ---
|
| 950 |
+
build_and_train_model(
|
| 951 |
+
df,
|
| 952 |
+
"ceiling_fan",
|
| 953 |
+
"ceiling_fans_kwh",
|
| 954 |
+
None,
|
| 955 |
+
[
|
| 956 |
+
"num_ceiling_fans",
|
| 957 |
+
"fan_hours_per_day",
|
| 958 |
+
"fan_star_rating",
|
| 959 |
+
"fan_min_hours",
|
| 960 |
+
"fan_max_hours",
|
| 961 |
+
],
|
| 962 |
+
["fan_category"],
|
| 963 |
+
)
|
| 964 |
+
build_and_train_model(
|
| 965 |
+
df,
|
| 966 |
+
"led_light",
|
| 967 |
+
"led_lights_kwh",
|
| 968 |
+
None,
|
| 969 |
+
["num_led_lights", "light_hours_per_day", "led_min_hours", "led_max_hours"],
|
| 970 |
+
["led_category"],
|
| 971 |
+
)
|
| 972 |
+
build_and_train_model(
|
| 973 |
+
df,
|
| 974 |
+
"tube_light",
|
| 975 |
+
"tube_lights_kwh",
|
| 976 |
+
None,
|
| 977 |
+
["num_tube_lights", "light_hours_per_day", "tube_min_hours", "tube_max_hours"],
|
| 978 |
+
["tube_category"],
|
| 979 |
+
)
|
| 980 |
+
build_and_train_model(
|
| 981 |
+
df,
|
| 982 |
+
"cfl_bulb",
|
| 983 |
+
"cfl_bulbs_kwh",
|
| 984 |
+
None,
|
| 985 |
+
["num_cfl_bulbs", "light_hours_per_day", "cfl_min_hours", "cfl_max_hours"],
|
| 986 |
+
["cfl_category"],
|
| 987 |
+
)
|
| 988 |
+
|
| 989 |
+
# --- NEW APPLIANCES ---
|
| 990 |
+
build_and_train_model(
|
| 991 |
+
df,
|
| 992 |
+
"desktop",
|
| 993 |
+
"desktop_kwh",
|
| 994 |
+
"has_computer",
|
| 995 |
+
["desktop_hours_per_day", "desktop_min_hours", "desktop_max_hours"],
|
| 996 |
+
["desktop_category"],
|
| 997 |
+
)
|
| 998 |
+
build_and_train_model(
|
| 999 |
+
df,
|
| 1000 |
+
"laptop",
|
| 1001 |
+
"laptop_kwh",
|
| 1002 |
+
"has_laptop",
|
| 1003 |
+
["laptop_hours_per_day", "laptop_min_hours", "laptop_max_hours"],
|
| 1004 |
+
["laptop_category"],
|
| 1005 |
+
)
|
| 1006 |
+
build_and_train_model(
|
| 1007 |
+
df,
|
| 1008 |
+
"iron",
|
| 1009 |
+
"iron_kwh",
|
| 1010 |
+
"has_iron",
|
| 1011 |
+
["iron_hours_per_day", "iron_min_hours", "iron_max_hours"],
|
| 1012 |
+
["iron_category"],
|
| 1013 |
+
)
|
| 1014 |
+
build_and_train_model(
|
| 1015 |
+
df,
|
| 1016 |
+
"kettle",
|
| 1017 |
+
"kettle_kwh",
|
| 1018 |
+
"has_electric_kettle",
|
| 1019 |
+
["kettle_hours_per_day", "kettle_min_hours", "kettle_max_hours"],
|
| 1020 |
+
["kettle_category"],
|
| 1021 |
+
)
|
| 1022 |
+
build_and_train_model(
|
| 1023 |
+
df,
|
| 1024 |
+
"induction",
|
| 1025 |
+
"induction_kwh",
|
| 1026 |
+
"has_induction",
|
| 1027 |
+
["induction_hours_per_day", "induction_min_hours", "induction_max_hours"],
|
| 1028 |
+
["induction_category"],
|
| 1029 |
+
)
|
| 1030 |
+
build_and_train_model(
|
| 1031 |
+
df,
|
| 1032 |
+
"rice_cooker",
|
| 1033 |
+
"rice_cooker_kwh",
|
| 1034 |
+
"has_rice_cooker",
|
| 1035 |
+
["rice_cooker_hours_per_day", "rice_cooker_min_hours", "rice_cooker_max_hours"],
|
| 1036 |
+
["rice_cooker_category"],
|
| 1037 |
+
)
|
| 1038 |
+
build_and_train_model(
|
| 1039 |
+
df,
|
| 1040 |
+
"toaster",
|
| 1041 |
+
"toaster_kwh",
|
| 1042 |
+
"has_toaster",
|
| 1043 |
+
["toaster_hours_per_day", "toaster_min_hours", "toaster_max_hours"],
|
| 1044 |
+
["toaster_category"],
|
| 1045 |
+
)
|
| 1046 |
+
build_and_train_model(
|
| 1047 |
+
df,
|
| 1048 |
+
"food_processor",
|
| 1049 |
+
"food_processor_kwh",
|
| 1050 |
+
"has_food_processor",
|
| 1051 |
+
[
|
| 1052 |
+
"food_processor_hours_per_day",
|
| 1053 |
+
"food_processor_min_hours",
|
| 1054 |
+
"food_processor_max_hours",
|
| 1055 |
+
],
|
| 1056 |
+
["food_processor_category"],
|
| 1057 |
+
)
|
| 1058 |
+
build_and_train_model(
|
| 1059 |
+
df,
|
| 1060 |
+
"hair_dryer",
|
| 1061 |
+
"hair_dryer_kwh",
|
| 1062 |
+
"has_hair_dryer",
|
| 1063 |
+
["hair_dryer_hours_per_day", "hair_dryer_min_hours", "hair_dryer_max_hours"],
|
| 1064 |
+
["hair_dryer_category"],
|
| 1065 |
+
)
|
| 1066 |
+
build_and_train_model(
|
| 1067 |
+
df,
|
| 1068 |
+
"vacuum",
|
| 1069 |
+
"vacuum_kwh",
|
| 1070 |
+
"has_vacuum",
|
| 1071 |
+
["vacuum_hours_per_day", "vacuum_min_hours", "vacuum_max_hours"],
|
| 1072 |
+
["vacuum_category"],
|
| 1073 |
+
)
|
| 1074 |
+
|
| 1075 |
+
# --- Export Training Data ---
|
| 1076 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1077 |
+
export_training_data_to_csv(df, f"training_data/training_data_{timestamp}.csv")
|
| 1078 |
+
export_training_data_to_csv(
|
| 1079 |
+
df, "training_data/latest_training_data.csv"
|
| 1080 |
+
) # Always keep latest
|
| 1081 |
+
|
| 1082 |
+
print("\n" + "=" * 70)
|
| 1083 |
+
print(" 🎓 SELF-LEARNING TRAINING SESSION COMPLETE")
|
| 1084 |
+
print("=" * 70)
|
| 1085 |
+
print(f"\n📊 Training Logs:")
|
| 1086 |
+
print(f" - History: {TRAINING_HISTORY_FILE}")
|
| 1087 |
+
print(f" - Performance CSV: {PERFORMANCE_LOG_CSV}")
|
| 1088 |
+
print(f" - Model Backups: models/backups/")
|
| 1089 |
+
print("=" * 70)
|
| 1090 |
+
|
| 1091 |
+
|
| 1092 |
+
if __name__ == "__main__":
|
| 1093 |
+
main()
|
Training_Scripts/newdataset.py
ADDED
|
@@ -0,0 +1,1008 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced Monte Carlo Simulation (Multi-Output Targets - Kerala Context)
|
| 3 |
+
Author: SmartWatt Team
|
| 4 |
+
Date: 2025-12-11
|
| 5 |
+
Purpose: Generates dataset with 'Ground Truth' Efficiency and Effective Hours for Integrated Hybrid AI
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import warnings
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
warnings.filterwarnings("ignore")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class KeralaRealWorldSimulator:
|
| 17 |
+
def __init__(self, n_households: int = 12000, random_seed: int = 42):
|
| 18 |
+
self.n = n_households
|
| 19 |
+
np.random.seed(random_seed)
|
| 20 |
+
|
| 21 |
+
# --- KERALA CONTEXT VARIABLES ---
|
| 22 |
+
# High Humidity -> Fans run longer
|
| 23 |
+
# Voltage Fluctuation -> Rural areas have lower efficiency
|
| 24 |
+
# Old Grid -> 10% transmission loss simulation
|
| 25 |
+
|
| 26 |
+
def generate_demographics(self):
|
| 27 |
+
data = {
|
| 28 |
+
"household_id": [f"KL__{i}" for i in range(self.n)],
|
| 29 |
+
"n_occupants": np.random.choice(
|
| 30 |
+
[2, 3, 4, 5, 6], size=self.n, p=[0.1, 0.25, 0.40, 0.15, 0.1]
|
| 31 |
+
),
|
| 32 |
+
"location_type": np.random.choice(
|
| 33 |
+
["urban", "rural"], size=self.n, p=[0.6, 0.4]
|
| 34 |
+
),
|
| 35 |
+
"total_kwh_monthly": np.zeros(self.n), # Will sum up later
|
| 36 |
+
}
|
| 37 |
+
return pd.DataFrame(data)
|
| 38 |
+
|
| 39 |
+
def generate_ac_data(self, df):
|
| 40 |
+
# 45% Penetration
|
| 41 |
+
has_ac = np.random.random(self.n) < 0.45
|
| 42 |
+
df["has_ac"] = has_ac.astype(int)
|
| 43 |
+
|
| 44 |
+
# 1. Specs (What kind of AC do they have?)
|
| 45 |
+
df["ac_tonnage"] = np.where(
|
| 46 |
+
has_ac, np.random.choice([1.0, 1.5], size=self.n, p=[0.3, 0.7]), 0
|
| 47 |
+
)
|
| 48 |
+
df["ac_star_rating"] = np.where(
|
| 49 |
+
has_ac, np.random.choice([3, 5], size=self.n, p=[0.6, 0.4]), 0
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# New Field: ac_age_years (Integrated from add_ac_age.py)
|
| 53 |
+
df["ac_age_years"] = np.where(
|
| 54 |
+
has_ac,
|
| 55 |
+
np.random.choice(
|
| 56 |
+
["0-2", "3-5", "6-10", "10+"], size=self.n, p=[0.20, 0.35, 0.30, 0.15]
|
| 57 |
+
),
|
| 58 |
+
"unknown",
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
# UI Fields: ac_type, ac_usage_pattern
|
| 62 |
+
df["ac_type"] = np.where(
|
| 63 |
+
has_ac,
|
| 64 |
+
np.random.choice(
|
| 65 |
+
["split", "window", "inverter"], size=self.n, p=[0.6, 0.2, 0.2]
|
| 66 |
+
),
|
| 67 |
+
"unknown",
|
| 68 |
+
)
|
| 69 |
+
df["ac_usage_pattern"] = np.where(
|
| 70 |
+
has_ac,
|
| 71 |
+
np.random.choice(
|
| 72 |
+
["heavy", "moderate", "light"], size=self.n, p=[0.2, 0.5, 0.3]
|
| 73 |
+
),
|
| 74 |
+
"none",
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# 2. BEHAVIOR (The 'Secret' Human Factor)
|
| 78 |
+
# Generate hours based on usage pattern (AI learns this correlation)
|
| 79 |
+
# In Kerala, humidity makes AC run longer than expected
|
| 80 |
+
base_hours = np.where(
|
| 81 |
+
df["ac_usage_pattern"] == "light",
|
| 82 |
+
np.random.uniform(2, 6, self.n),
|
| 83 |
+
np.where(
|
| 84 |
+
df["ac_usage_pattern"] == "moderate",
|
| 85 |
+
np.random.uniform(6, 10, self.n),
|
| 86 |
+
np.where(
|
| 87 |
+
df["ac_usage_pattern"] == "heavy",
|
| 88 |
+
np.random.uniform(10, 14, self.n),
|
| 89 |
+
0,
|
| 90 |
+
),
|
| 91 |
+
),
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Kerala humidity factor (compressor works harder)
|
| 95 |
+
humidity_factor = np.random.normal(
|
| 96 |
+
1.15, 0.05, self.n
|
| 97 |
+
) # 15% extra load due to humidity
|
| 98 |
+
df["ac_real_effective_hours"] = np.where(
|
| 99 |
+
has_ac, base_hours * humidity_factor, 0
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# 3. EFFICIENCY (The 'Wear and Tear' Factor)
|
| 103 |
+
# Rural areas often suffer from voltage drops.
|
| 104 |
+
# A low voltage supply makes the AC compressor struggle, reducing efficiency by ~10%.
|
| 105 |
+
efficiency = np.random.normal(
|
| 106 |
+
0.95, 0.05, self.n
|
| 107 |
+
) # Most ACs work at 95% optimal efficiency
|
| 108 |
+
rural_mask = (df["location_type"] == "rural") & has_ac
|
| 109 |
+
efficiency[rural_mask] -= 0.10 # Penalty for rural grid quality
|
| 110 |
+
df["ac_real_efficiency_factor"] = np.where(has_ac, efficiency.clip(0.6, 1.1), 0)
|
| 111 |
+
|
| 112 |
+
# 4. The Physics Calculation (Ground Truth)
|
| 113 |
+
# Physics Formula: Watts = Tons * 1200 / Efficiency
|
| 114 |
+
watts = (df["ac_tonnage"] * 1200) * (1 + (5 - df["ac_star_rating"]) * 0.1)
|
| 115 |
+
|
| 116 |
+
# If efficiency is low (e.g., 0.8), power consumption goes UP.
|
| 117 |
+
# It's an inverse relationship: Worse Efficiency = Higher Bill.
|
| 118 |
+
watts = watts / df["ac_real_efficiency_factor"]
|
| 119 |
+
|
| 120 |
+
# Final Bill Calculation: (Watts * Hours * 30 days) / 1000 to get units (kWh)
|
| 121 |
+
df["ac_kwh"] = (watts * df["ac_real_effective_hours"] * 30) / 1000
|
| 122 |
+
return df
|
| 123 |
+
|
| 124 |
+
def generate_fridge_data(self, df):
|
| 125 |
+
# 95% Penetration
|
| 126 |
+
has_fridge = np.random.random(self.n) < 0.95
|
| 127 |
+
df["has_refrigerator"] = has_fridge.astype(int)
|
| 128 |
+
|
| 129 |
+
# Specs
|
| 130 |
+
df["fridge_capacity"] = np.where(
|
| 131 |
+
has_fridge, np.random.choice([190, 260], size=self.n), 0
|
| 132 |
+
)
|
| 133 |
+
df["fridge_age"] = np.where(
|
| 134 |
+
has_fridge, np.random.choice([2, 5, 10], size=self.n, p=[0.3, 0.4, 0.3]), 0
|
| 135 |
+
)
|
| 136 |
+
df["fridge_star_rating"] = np.where(
|
| 137 |
+
has_fridge,
|
| 138 |
+
np.random.choice([2, 3, 4, 5], size=self.n, p=[0.2, 0.3, 0.3, 0.2]),
|
| 139 |
+
0,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# UI Fields: fridge_type
|
| 143 |
+
df["fridge_type"] = np.where(
|
| 144 |
+
has_fridge,
|
| 145 |
+
np.random.choice(["frost_free", "direct_cool"], size=self.n, p=[0.7, 0.3]),
|
| 146 |
+
"unknown",
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
# BEHAVIOR: AI Pattern Learning
|
| 150 |
+
df["refrigerator_usage_pattern"] = np.where(
|
| 151 |
+
has_fridge,
|
| 152 |
+
np.random.choice(
|
| 153 |
+
["manual", "light", "normal", "always"],
|
| 154 |
+
size=self.n,
|
| 155 |
+
p=[0.05, 0.10, 0.20, 0.65],
|
| 156 |
+
),
|
| 157 |
+
"unknown",
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# Pattern-based hour ranges
|
| 161 |
+
pattern_hours_map = {
|
| 162 |
+
"manual": np.random.uniform(8, 14, self.n), # Turn off at night
|
| 163 |
+
"light": np.random.uniform(14, 18, self.n), # Off during low usage
|
| 164 |
+
"normal": np.random.uniform(18, 22, self.n), # Mostly on
|
| 165 |
+
"always": np.full(self.n, 24.0), # 24/7 operation
|
| 166 |
+
}
|
| 167 |
+
hours = np.zeros(self.n)
|
| 168 |
+
for pattern, hours_range in pattern_hours_map.items():
|
| 169 |
+
hours = np.where(
|
| 170 |
+
df["refrigerator_usage_pattern"] == pattern, hours_range, hours
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
df["fridge_real_effective_hours"] = np.where(has_fridge, hours, 0)
|
| 174 |
+
|
| 175 |
+
# EFFICIENCY:
|
| 176 |
+
# In the hot Kerala climate, rubber seals on fridges degrade faster.
|
| 177 |
+
# An old fridge leaks cold air, making the motor run longer. We deduct 4% efficiency per year.
|
| 178 |
+
eff = np.ones(self.n)
|
| 179 |
+
eff -= df["fridge_age"] * 0.04
|
| 180 |
+
df["fridge_real_efficiency_factor"] = np.where(
|
| 181 |
+
has_fridge, eff.clip(0.5, 1.0), 0
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# Calculation
|
| 185 |
+
watts = (df["fridge_capacity"] / 250) * 200 # Baseline
|
| 186 |
+
watts = watts / df["fridge_real_efficiency_factor"]
|
| 187 |
+
df["fridge_kwh"] = (watts * df["fridge_real_effective_hours"] * 30) / 1000
|
| 188 |
+
return df
|
| 189 |
+
|
| 190 |
+
def generate_fan_data(self, df):
|
| 191 |
+
# 98% Penetration (Essential in Kerala)
|
| 192 |
+
df["has_ceiling_fan"] = (np.random.random(self.n) < 0.98).astype(int)
|
| 193 |
+
|
| 194 |
+
# UI Fields: fan_type, num_fans
|
| 195 |
+
df["fan_type"] = np.where(
|
| 196 |
+
df["has_ceiling_fan"],
|
| 197 |
+
np.random.choice(["standard", "bldc"], size=self.n, p=[0.8, 0.2]),
|
| 198 |
+
"unknown",
|
| 199 |
+
)
|
| 200 |
+
df["num_fans"] = np.where(
|
| 201 |
+
df["has_ceiling_fan"],
|
| 202 |
+
np.random.choice(
|
| 203 |
+
[2, 3, 4, 5, 6], size=self.n, p=[0.1, 0.25, 0.35, 0.2, 0.1]
|
| 204 |
+
),
|
| 205 |
+
0,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
# BEHAVIOR: AI Pattern Learning
|
| 209 |
+
df["fan_usage_pattern"] = np.where(
|
| 210 |
+
df["has_ceiling_fan"],
|
| 211 |
+
np.random.choice(
|
| 212 |
+
["rarely", "few", "most", "all"],
|
| 213 |
+
size=self.n,
|
| 214 |
+
p=[0.05, 0.20, 0.50, 0.25],
|
| 215 |
+
),
|
| 216 |
+
"unknown",
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Pattern-based hour ranges (Kerala humidity = high usage)
|
| 220 |
+
pattern_hours_map = {
|
| 221 |
+
"rarely": np.random.uniform(2, 5, self.n), # AC users
|
| 222 |
+
"few": np.random.uniform(5, 10, self.n), # Few rooms
|
| 223 |
+
"most": np.random.uniform(10, 16, self.n), # Most rooms
|
| 224 |
+
"all": np.random.uniform(16, 22, self.n), # All rooms all day
|
| 225 |
+
}
|
| 226 |
+
base_hours = np.zeros(self.n)
|
| 227 |
+
for pattern, hours_range in pattern_hours_map.items():
|
| 228 |
+
base_hours = np.where(
|
| 229 |
+
df["fan_usage_pattern"] == pattern, hours_range, base_hours
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
# Humidity boost (Kerala climate)
|
| 233 |
+
humidity_boost = np.random.normal(1.2, 0.1, self.n)
|
| 234 |
+
df["ceiling_fan_real_effective_hours"] = np.where(
|
| 235 |
+
df["has_ceiling_fan"], (base_hours * humidity_boost).clip(2, 24), 0
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
# EFFICIENCY: Old fans get slow/power hungry
|
| 239 |
+
df["ceiling_fan_age"] = np.random.choice(
|
| 240 |
+
[2, 5, 10, 15], size=self.n, p=[0.2, 0.3, 0.3, 0.2]
|
| 241 |
+
)
|
| 242 |
+
eff = np.ones(self.n) - (df["ceiling_fan_age"] * 0.01) # 1% drop/year
|
| 243 |
+
df["ceiling_fan_real_efficiency_factor"] = np.where(
|
| 244 |
+
df["has_ceiling_fan"], eff.clip(0.7, 1.0), 0
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
# Power calculation
|
| 248 |
+
watts = 75 / df["ceiling_fan_real_efficiency_factor"]
|
| 249 |
+
df["ceiling_fan_kwh"] = (
|
| 250 |
+
watts * df["ceiling_fan_real_effective_hours"] * df["num_fans"] * 30
|
| 251 |
+
) / 1000
|
| 252 |
+
return df
|
| 253 |
+
|
| 254 |
+
def generate_tv_data(self, df):
|
| 255 |
+
# 95% Penetration
|
| 256 |
+
df["has_television"] = (np.random.random(self.n) < 0.95).astype(int)
|
| 257 |
+
|
| 258 |
+
# BEHAVIOR: The "Serial" factor.
|
| 259 |
+
# In Kerala households, evening TV serials and news are a staple.
|
| 260 |
+
# We assume 4-6 hours of usage is standard usage.
|
| 261 |
+
# Generate usage patterns first (light/moderate/heavy/always)
|
| 262 |
+
df["television_usage_pattern"] = np.where(
|
| 263 |
+
df["has_television"],
|
| 264 |
+
np.random.choice(
|
| 265 |
+
["light", "moderate", "heavy", "always"],
|
| 266 |
+
size=self.n,
|
| 267 |
+
p=[0.25, 0.45, 0.25, 0.05],
|
| 268 |
+
),
|
| 269 |
+
"none",
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Generate hours based on pattern + household context (AI will learn this correlation)
|
| 273 |
+
base_hours = np.where(
|
| 274 |
+
df["television_usage_pattern"] == "light",
|
| 275 |
+
np.random.uniform(1, 3, self.n),
|
| 276 |
+
np.where(
|
| 277 |
+
df["television_usage_pattern"] == "moderate",
|
| 278 |
+
np.random.uniform(3, 5, self.n),
|
| 279 |
+
np.where(
|
| 280 |
+
df["television_usage_pattern"] == "heavy",
|
| 281 |
+
np.random.uniform(5, 8, self.n),
|
| 282 |
+
np.where(
|
| 283 |
+
df["television_usage_pattern"] == "always",
|
| 284 |
+
np.random.uniform(8, 12, self.n),
|
| 285 |
+
0,
|
| 286 |
+
),
|
| 287 |
+
),
|
| 288 |
+
),
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
# Add household context influence (more people = slightly more hours)
|
| 292 |
+
occupant_factor = (
|
| 293 |
+
1 + (df["n_occupants"] - 4) * 0.05
|
| 294 |
+
) # ±5% per person difference from 4
|
| 295 |
+
hours = (base_hours * occupant_factor).clip(1, 14)
|
| 296 |
+
|
| 297 |
+
df["television_real_effective_hours"] = np.where(df["has_television"], hours, 0)
|
| 298 |
+
|
| 299 |
+
# Efficiency (LED vs CRT/LCD)
|
| 300 |
+
df["television_type"] = np.random.choice(
|
| 301 |
+
["LED", "CRT", "LCD"], size=self.n, p=[0.7, 0.1, 0.2]
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# UI Fields: tv_size
|
| 305 |
+
df["tv_size"] = np.where(
|
| 306 |
+
df["has_television"],
|
| 307 |
+
np.random.choice([32, 43, 55], size=self.n, p=[0.4, 0.4, 0.2]),
|
| 308 |
+
0,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
base_watts = np.where(
|
| 312 |
+
df["television_type"] == "LED",
|
| 313 |
+
80,
|
| 314 |
+
np.where(df["television_type"] == "LCD", 120, 150),
|
| 315 |
+
)
|
| 316 |
+
eff = np.ones(self.n)
|
| 317 |
+
df["television_real_efficiency_factor"] = np.where(df["has_television"], eff, 0)
|
| 318 |
+
df["television_kwh"] = (
|
| 319 |
+
base_watts * df["television_real_effective_hours"] * 30
|
| 320 |
+
) / 1000
|
| 321 |
+
return df
|
| 322 |
+
|
| 323 |
+
def generate_wm_data(self, df):
|
| 324 |
+
# 60% Penetration
|
| 325 |
+
df["has_washing_machine"] = (np.random.random(self.n) < 0.60).astype(int)
|
| 326 |
+
|
| 327 |
+
# UI Fields: wm_type, wm_capacity, wm_star_rating, wm_cycles_per_week
|
| 328 |
+
df["wm_type"] = np.where(
|
| 329 |
+
df["has_washing_machine"],
|
| 330 |
+
np.random.choice(
|
| 331 |
+
["top_load", "front_load", "semi_automatic"],
|
| 332 |
+
size=self.n,
|
| 333 |
+
p=[0.5, 0.35, 0.15],
|
| 334 |
+
),
|
| 335 |
+
"unknown",
|
| 336 |
+
)
|
| 337 |
+
df["wm_capacity"] = np.where(
|
| 338 |
+
df["has_washing_machine"],
|
| 339 |
+
np.random.choice([5, 6, 7, 8], size=self.n, p=[0.2, 0.3, 0.3, 0.2]),
|
| 340 |
+
0,
|
| 341 |
+
)
|
| 342 |
+
df["wm_star_rating"] = np.where(
|
| 343 |
+
df["has_washing_machine"],
|
| 344 |
+
np.random.choice([2, 3, 4, 5], size=self.n, p=[0.2, 0.3, 0.3, 0.2]),
|
| 345 |
+
0,
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
# BEHAVIOR: Cycles Per Week dependent on Occupants
|
| 349 |
+
# 2 people -> 2 cycles/week
|
| 350 |
+
# 4+ people -> 4-6 cycles/week
|
| 351 |
+
cycles = (df["n_occupants"] * 1.2) + np.random.normal(0, 1, self.n)
|
| 352 |
+
df["wm_cycles_per_week"] = np.where(
|
| 353 |
+
df["has_washing_machine"], cycles.clip(1, 10), 0
|
| 354 |
+
)
|
| 355 |
+
df["washing_machine_real_effective_hours"] = df[
|
| 356 |
+
"wm_cycles_per_week"
|
| 357 |
+
] # Storing Cycles as "Hours" for simplicity in training
|
| 358 |
+
|
| 359 |
+
# Efficiency
|
| 360 |
+
df["washing_machine_real_efficiency_factor"] = 1.0
|
| 361 |
+
|
| 362 |
+
base_kwh_per_cycle = 0.5 # Avg
|
| 363 |
+
df["washing_machine_kwh"] = (
|
| 364 |
+
base_kwh_per_cycle * df["washing_machine_real_effective_hours"] * 4.3
|
| 365 |
+
)
|
| 366 |
+
return df
|
| 367 |
+
|
| 368 |
+
def generate_pump_data(self, df):
|
| 369 |
+
# 70% Penetration (Well Water is common in Kerala)
|
| 370 |
+
df["has_water_pump"] = (np.random.random(self.n) < 0.70).astype(int)
|
| 371 |
+
|
| 372 |
+
# UI Fields: water_pump_hp
|
| 373 |
+
df["water_pump_hp"] = np.where(
|
| 374 |
+
df["has_water_pump"],
|
| 375 |
+
np.random.choice([0.5, 1.0, 1.5], size=self.n, p=[0.3, 0.5, 0.2]),
|
| 376 |
+
0,
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
# BEHAVIOR: AI Pattern Learning
|
| 380 |
+
df["pump_usage_pattern"] = np.where(
|
| 381 |
+
df["has_water_pump"],
|
| 382 |
+
np.random.choice(
|
| 383 |
+
["minimal", "light", "moderate", "heavy"],
|
| 384 |
+
size=self.n,
|
| 385 |
+
p=[0.15, 0.35, 0.40, 0.10],
|
| 386 |
+
),
|
| 387 |
+
"unknown",
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
# Pattern-based minute ranges
|
| 391 |
+
pattern_mins_map = {
|
| 392 |
+
"minimal": np.random.uniform(10, 20, self.n), # Municipal backup
|
| 393 |
+
"light": np.random.uniform(20, 40, self.n), # Occasional pumping
|
| 394 |
+
"moderate": np.random.uniform(40, 70, self.n), # Regular well usage
|
| 395 |
+
"heavy": np.random.uniform(70, 120, self.n), # Primary water source
|
| 396 |
+
}
|
| 397 |
+
base_mins = np.zeros(self.n)
|
| 398 |
+
for pattern, mins in pattern_mins_map.items():
|
| 399 |
+
base_mins = np.where(df["pump_usage_pattern"] == pattern, mins, base_mins)
|
| 400 |
+
|
| 401 |
+
# Occupancy influence (more people = more water)
|
| 402 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.12
|
| 403 |
+
hours = (base_mins * occupant_factor) / 60
|
| 404 |
+
df["water_pump_real_effective_hours"] = np.where(
|
| 405 |
+
df["has_water_pump"], hours.clip(0.15, 2.5), 0
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
# Efficiency (Old pumps are terrible)
|
| 409 |
+
eff = np.random.normal(0.8, 0.1, self.n).clip(0.5, 1.0)
|
| 410 |
+
df["water_pump_real_efficiency_factor"] = np.where(df["has_water_pump"], eff, 0)
|
| 411 |
+
|
| 412 |
+
watts = (df["water_pump_hp"] * 746) / df[
|
| 413 |
+
"water_pump_real_efficiency_factor"
|
| 414 |
+
] # HP to Watts
|
| 415 |
+
df["water_pump_kwh"] = (
|
| 416 |
+
watts * df["water_pump_real_effective_hours"] * 30
|
| 417 |
+
) / 1000
|
| 418 |
+
return df
|
| 419 |
+
|
| 420 |
+
def generate_geyser_data(self, df):
|
| 421 |
+
# 40% Penetration (Urban mainly)
|
| 422 |
+
df["has_water_heater"] = (np.random.random(self.n) < 0.40).astype(int)
|
| 423 |
+
|
| 424 |
+
# UI Fields: water_heater_type, water_heater_capacity
|
| 425 |
+
df["water_heater_type"] = np.where(
|
| 426 |
+
df["has_water_heater"],
|
| 427 |
+
np.random.choice(
|
| 428 |
+
["instant", "storage", "gas", "solar"],
|
| 429 |
+
size=self.n,
|
| 430 |
+
p=[0.4, 0.4, 0.1, 0.1],
|
| 431 |
+
),
|
| 432 |
+
"unknown",
|
| 433 |
+
)
|
| 434 |
+
df["water_heater_capacity"] = np.where(
|
| 435 |
+
df["has_water_heater"],
|
| 436 |
+
np.random.choice([10, 15, 25, 35], size=self.n, p=[0.3, 0.4, 0.2, 0.1]),
|
| 437 |
+
0,
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
# BEHAVIOR: AI Pattern Learning + Seasonality
|
| 441 |
+
df["season"] = np.random.choice(["summer", "monsoon", "winter"], size=self.n)
|
| 442 |
+
|
| 443 |
+
df["geyser_usage_pattern"] = np.where(
|
| 444 |
+
df["has_water_heater"],
|
| 445 |
+
np.random.choice(
|
| 446 |
+
["minimal", "light", "moderate", "heavy"],
|
| 447 |
+
size=self.n,
|
| 448 |
+
p=[0.20, 0.40, 0.30, 0.10],
|
| 449 |
+
),
|
| 450 |
+
"unknown",
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
# Pattern-based hour ranges
|
| 454 |
+
pattern_hours_map = {
|
| 455 |
+
"minimal": np.random.uniform(0.2, 0.5, self.n), # Quick showers
|
| 456 |
+
"light": np.random.uniform(0.5, 1.0, self.n), # 1-2 showers/day
|
| 457 |
+
"moderate": np.random.uniform(1.0, 1.8, self.n), # Regular family use
|
| 458 |
+
"heavy": np.random.uniform(1.8, 3.0, self.n), # Multiple long showers
|
| 459 |
+
}
|
| 460 |
+
base_hours = np.zeros(self.n)
|
| 461 |
+
for pattern, hours_range in pattern_hours_map.items():
|
| 462 |
+
base_hours = np.where(
|
| 463 |
+
df["geyser_usage_pattern"] == pattern, hours_range, base_hours
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
# Season multiplier (winter = more hot water)
|
| 467 |
+
season_factor = np.where(
|
| 468 |
+
df["season"] == "summer", 0.5, np.where(df["season"] == "monsoon", 1.0, 2.0)
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
# Occupancy influence
|
| 472 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.10
|
| 473 |
+
|
| 474 |
+
hours = base_hours * season_factor * occupant_factor
|
| 475 |
+
|
| 476 |
+
df["water_heater_real_effective_hours"] = np.where(
|
| 477 |
+
df["has_water_heater"], hours, 0
|
| 478 |
+
)
|
| 479 |
+
df["water_heater_real_efficiency_factor"] = 1.0 # Static
|
| 480 |
+
|
| 481 |
+
df["water_heater_kwh"] = (
|
| 482 |
+
2000 * df["water_heater_real_effective_hours"] * 30
|
| 483 |
+
) / 1000
|
| 484 |
+
return df
|
| 485 |
+
|
| 486 |
+
def generate_misc_data(self, df):
|
| 487 |
+
# 1. Iron Box (90% Penetration)
|
| 488 |
+
df["has_iron"] = (np.random.random(self.n) < 0.90).astype(int)
|
| 489 |
+
|
| 490 |
+
# Add usage pattern for iron
|
| 491 |
+
df["iron_usage_pattern"] = np.where(
|
| 492 |
+
df["has_iron"],
|
| 493 |
+
np.random.choice(
|
| 494 |
+
["rarely", "light", "moderate", "heavy"],
|
| 495 |
+
size=self.n,
|
| 496 |
+
p=[0.15, 0.50, 0.30, 0.05],
|
| 497 |
+
),
|
| 498 |
+
"none",
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
# Generate hours based on pattern + household context
|
| 502 |
+
base_hours = np.where(
|
| 503 |
+
df["iron_usage_pattern"] == "rarely",
|
| 504 |
+
np.random.uniform(0.08, 0.25, self.n), # 5-15 min
|
| 505 |
+
np.where(
|
| 506 |
+
df["iron_usage_pattern"] == "light",
|
| 507 |
+
np.random.uniform(0.25, 0.50, self.n), # 15-30 min
|
| 508 |
+
np.where(
|
| 509 |
+
df["iron_usage_pattern"] == "moderate",
|
| 510 |
+
np.random.uniform(0.50, 1.0, self.n), # 30-60 min
|
| 511 |
+
np.where(
|
| 512 |
+
df["iron_usage_pattern"] == "heavy",
|
| 513 |
+
np.random.uniform(1.0, 2.0, self.n),
|
| 514 |
+
0,
|
| 515 |
+
),
|
| 516 |
+
),
|
| 517 |
+
),
|
| 518 |
+
) # 1-2 hrs
|
| 519 |
+
|
| 520 |
+
occupant_factor = (
|
| 521 |
+
1 + (df["n_occupants"] - 4) * 0.10
|
| 522 |
+
) # More people = more clothes
|
| 523 |
+
df["iron_real_effective_hours"] = np.where(
|
| 524 |
+
df["has_iron"], (base_hours * occupant_factor).clip(0.08, 2.5), 0
|
| 525 |
+
)
|
| 526 |
+
df["iron_real_efficiency_factor"] = 1.0
|
| 527 |
+
|
| 528 |
+
# 2. Electric Kettle (45%) - AI Pattern Learning
|
| 529 |
+
df["has_electric_kettle"] = (np.random.random(self.n) < 0.45).astype(int)
|
| 530 |
+
df["kettle_usage_pattern"] = np.where(
|
| 531 |
+
df["has_electric_kettle"],
|
| 532 |
+
np.random.choice(
|
| 533 |
+
["rarely", "light", "moderate", "heavy"],
|
| 534 |
+
size=self.n,
|
| 535 |
+
p=[0.15, 0.35, 0.40, 0.10],
|
| 536 |
+
),
|
| 537 |
+
"unknown",
|
| 538 |
+
)
|
| 539 |
+
# Pattern-based minute ranges
|
| 540 |
+
pattern_mins_map = {
|
| 541 |
+
"rarely": np.random.uniform(3, 8, self.n), # Occasional tea/coffee
|
| 542 |
+
"light": np.random.uniform(8, 15, self.n), # 1-2 times/day
|
| 543 |
+
"moderate": np.random.uniform(15, 25, self.n), # 3-4 times/day
|
| 544 |
+
"heavy": np.random.uniform(25, 40, self.n), # Frequent use
|
| 545 |
+
}
|
| 546 |
+
base_mins = np.zeros(self.n)
|
| 547 |
+
for pattern, mins in pattern_mins_map.items():
|
| 548 |
+
base_mins = np.where(df["kettle_usage_pattern"] == pattern, mins, base_mins)
|
| 549 |
+
|
| 550 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.06
|
| 551 |
+
df["kettle_real_effective_hours"] = np.where(
|
| 552 |
+
df["has_electric_kettle"],
|
| 553 |
+
((base_mins * occupant_factor) / 60).clip(0.05, 0.8),
|
| 554 |
+
0,
|
| 555 |
+
)
|
| 556 |
+
df["kettle_real_efficiency_factor"] = 1.0
|
| 557 |
+
|
| 558 |
+
# 3. Induction (65%) - AI Pattern Learning
|
| 559 |
+
df["has_induction"] = (np.random.random(self.n) < 0.65).astype(int)
|
| 560 |
+
df["induction_usage_pattern"] = np.where(
|
| 561 |
+
df["has_induction"],
|
| 562 |
+
np.random.choice(
|
| 563 |
+
["light", "moderate", "heavy", "very_heavy"],
|
| 564 |
+
size=self.n,
|
| 565 |
+
p=[0.25, 0.45, 0.25, 0.05],
|
| 566 |
+
),
|
| 567 |
+
"unknown",
|
| 568 |
+
)
|
| 569 |
+
# Pattern-based hour ranges
|
| 570 |
+
pattern_hours_map = {
|
| 571 |
+
"light": np.random.uniform(0.3, 0.8, self.n), # Backup cooking
|
| 572 |
+
"moderate": np.random.uniform(0.8, 1.5, self.n), # Regular use
|
| 573 |
+
"heavy": np.random.uniform(1.5, 2.5, self.n), # Primary cooking
|
| 574 |
+
"very_heavy": np.random.uniform(2.5, 4.0, self.n), # All meals
|
| 575 |
+
}
|
| 576 |
+
base_hours = np.zeros(self.n)
|
| 577 |
+
for pattern, hours in pattern_hours_map.items():
|
| 578 |
+
base_hours = np.where(
|
| 579 |
+
df["induction_usage_pattern"] == pattern, hours, base_hours
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.08
|
| 583 |
+
df["induction_real_effective_hours"] = np.where(
|
| 584 |
+
df["has_induction"], (base_hours * occupant_factor).clip(0.3, 4.5), 0
|
| 585 |
+
)
|
| 586 |
+
df["induction_real_efficiency_factor"] = 1.0
|
| 587 |
+
|
| 588 |
+
# 4. PC/Desktop (55%)
|
| 589 |
+
df["has_computer"] = (np.random.random(self.n) < 0.55).astype(int)
|
| 590 |
+
|
| 591 |
+
# Add usage pattern for desktop
|
| 592 |
+
df["desktop_usage_pattern"] = np.where(
|
| 593 |
+
df["has_computer"],
|
| 594 |
+
np.random.choice(
|
| 595 |
+
["light", "moderate", "heavy", "always"],
|
| 596 |
+
size=self.n,
|
| 597 |
+
p=[0.30, 0.40, 0.25, 0.05],
|
| 598 |
+
),
|
| 599 |
+
"none",
|
| 600 |
+
)
|
| 601 |
+
|
| 602 |
+
# Generate hours based on pattern
|
| 603 |
+
pc_hours = np.where(
|
| 604 |
+
df["desktop_usage_pattern"] == "light",
|
| 605 |
+
np.random.uniform(1, 2, self.n),
|
| 606 |
+
np.where(
|
| 607 |
+
df["desktop_usage_pattern"] == "moderate",
|
| 608 |
+
np.random.uniform(3, 5, self.n),
|
| 609 |
+
np.where(
|
| 610 |
+
df["desktop_usage_pattern"] == "heavy",
|
| 611 |
+
np.random.uniform(6, 10, self.n),
|
| 612 |
+
np.where(
|
| 613 |
+
df["desktop_usage_pattern"] == "always",
|
| 614 |
+
np.random.uniform(20, 24, self.n),
|
| 615 |
+
0,
|
| 616 |
+
),
|
| 617 |
+
),
|
| 618 |
+
),
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
df["desktop_real_effective_hours"] = np.where(df["has_computer"], pc_hours, 0)
|
| 622 |
+
df["desktop_real_efficiency_factor"] = 1.0
|
| 623 |
+
|
| 624 |
+
# Calc kWh
|
| 625 |
+
df["iron_kwh"] = (1000 * df["iron_real_effective_hours"] * 30) / 1000
|
| 626 |
+
df["kettle_kwh"] = (1500 * df["kettle_real_effective_hours"] * 30) / 1000
|
| 627 |
+
df["induction_kwh"] = (2000 * df["induction_real_effective_hours"] * 30) / 1000
|
| 628 |
+
df["desktop_kwh"] = (250 * df["desktop_real_effective_hours"] * 30) / 1000
|
| 629 |
+
|
| 630 |
+
return df
|
| 631 |
+
|
| 632 |
+
def generate_kitchen_others(self, df):
|
| 633 |
+
# Microwave (30%) - AI Pattern Learning
|
| 634 |
+
df["has_microwave"] = (np.random.random(self.n) < 0.30).astype(int)
|
| 635 |
+
df["microwave_usage_pattern"] = np.where(
|
| 636 |
+
df["has_microwave"],
|
| 637 |
+
np.random.choice(
|
| 638 |
+
["rarely", "light", "moderate", "heavy"],
|
| 639 |
+
size=self.n,
|
| 640 |
+
p=[0.15, 0.40, 0.35, 0.10],
|
| 641 |
+
),
|
| 642 |
+
"unknown",
|
| 643 |
+
)
|
| 644 |
+
# Pattern-based minute ranges
|
| 645 |
+
pattern_mins_map = {
|
| 646 |
+
"rarely": np.random.uniform(3, 8, self.n), # Occasional reheating
|
| 647 |
+
"light": np.random.uniform(8, 15, self.n), # Few times/week
|
| 648 |
+
"moderate": np.random.uniform(15, 25, self.n), # Daily usage
|
| 649 |
+
"heavy": np.random.uniform(25, 40, self.n), # Multiple daily uses
|
| 650 |
+
}
|
| 651 |
+
base_mins = np.zeros(self.n)
|
| 652 |
+
for pattern, mins in pattern_mins_map.items():
|
| 653 |
+
base_mins = np.where(
|
| 654 |
+
df["microwave_usage_pattern"] == pattern, mins, base_mins
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.05
|
| 658 |
+
df["microwave_real_effective_hours"] = np.where(
|
| 659 |
+
df["has_microwave"], ((base_mins * occupant_factor) / 60).clip(0.05, 0.8), 0
|
| 660 |
+
)
|
| 661 |
+
df["microwave_real_efficiency_factor"] = 1.0
|
| 662 |
+
|
| 663 |
+
# Mixer (95%) - AI Pattern Learning
|
| 664 |
+
df["has_mixer"] = (np.random.random(self.n) < 0.95).astype(int)
|
| 665 |
+
df["mixer_usage_pattern"] = np.where(
|
| 666 |
+
df["has_mixer"],
|
| 667 |
+
np.random.choice(
|
| 668 |
+
["rarely", "light", "moderate", "heavy"],
|
| 669 |
+
size=self.n,
|
| 670 |
+
p=[0.10, 0.30, 0.45, 0.15],
|
| 671 |
+
),
|
| 672 |
+
"unknown",
|
| 673 |
+
)
|
| 674 |
+
# Pattern-based minute ranges
|
| 675 |
+
pattern_mins_map = {
|
| 676 |
+
"rarely": np.random.uniform(5, 10, self.n), # Occasional use
|
| 677 |
+
"light": np.random.uniform(10, 20, self.n), # Few times/week
|
| 678 |
+
"moderate": np.random.uniform(20, 35, self.n), # Daily grinding
|
| 679 |
+
"heavy": np.random.uniform(35, 50, self.n), # Heavy kitchen work
|
| 680 |
+
}
|
| 681 |
+
base_mins = np.zeros(self.n)
|
| 682 |
+
for pattern, mins in pattern_mins_map.items():
|
| 683 |
+
base_mins = np.where(df["mixer_usage_pattern"] == pattern, mins, base_mins)
|
| 684 |
+
|
| 685 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.06
|
| 686 |
+
df["mixer_real_effective_hours"] = np.where(
|
| 687 |
+
df["has_mixer"], ((base_mins * occupant_factor) / 60).clip(0.08, 1.0), 0
|
| 688 |
+
)
|
| 689 |
+
df["mixer_real_efficiency_factor"] = 1.0
|
| 690 |
+
|
| 691 |
+
# Rice Cooker (20%) - AI Pattern Learning
|
| 692 |
+
df["has_rice_cooker"] = (np.random.random(self.n) < 0.20).astype(int)
|
| 693 |
+
df["rice_cooker_usage_pattern"] = np.where(
|
| 694 |
+
df["has_rice_cooker"],
|
| 695 |
+
np.random.choice(
|
| 696 |
+
["rarely", "light", "moderate", "heavy"],
|
| 697 |
+
size=self.n,
|
| 698 |
+
p=[0.15, 0.35, 0.40, 0.10],
|
| 699 |
+
),
|
| 700 |
+
"unknown",
|
| 701 |
+
)
|
| 702 |
+
# Pattern-based minute ranges
|
| 703 |
+
pattern_mins_map = {
|
| 704 |
+
"rarely": np.random.uniform(15, 25, self.n), # Once/twice a week
|
| 705 |
+
"light": np.random.uniform(20, 30, self.n), # Few times/week
|
| 706 |
+
"moderate": np.random.uniform(30, 40, self.n), # Daily cooking
|
| 707 |
+
"heavy": np.random.uniform(40, 60, self.n), # Multiple meals
|
| 708 |
+
}
|
| 709 |
+
base_mins = np.zeros(self.n)
|
| 710 |
+
for pattern, mins in pattern_mins_map.items():
|
| 711 |
+
base_mins = np.where(
|
| 712 |
+
df["rice_cooker_usage_pattern"] == pattern, mins, base_mins
|
| 713 |
+
)
|
| 714 |
+
|
| 715 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.07
|
| 716 |
+
df["rice_cooker_real_effective_hours"] = np.where(
|
| 717 |
+
df["has_rice_cooker"],
|
| 718 |
+
((base_mins * occupant_factor) / 60).clip(0.25, 1.2),
|
| 719 |
+
0,
|
| 720 |
+
)
|
| 721 |
+
df["rice_cooker_real_efficiency_factor"] = 1.0
|
| 722 |
+
|
| 723 |
+
# Toaster (10%) - AI Pattern Learning
|
| 724 |
+
df["has_toaster"] = (np.random.random(self.n) < 0.10).astype(int)
|
| 725 |
+
df["toaster_usage_pattern"] = np.where(
|
| 726 |
+
df["has_toaster"],
|
| 727 |
+
np.random.choice(
|
| 728 |
+
["rarely", "light", "moderate", "heavy"],
|
| 729 |
+
size=self.n,
|
| 730 |
+
p=[0.25, 0.40, 0.25, 0.10],
|
| 731 |
+
),
|
| 732 |
+
"unknown",
|
| 733 |
+
)
|
| 734 |
+
# Pattern-based minute ranges
|
| 735 |
+
pattern_mins_map = {
|
| 736 |
+
"rarely": np.random.uniform(2, 5, self.n), # Weekend breakfast
|
| 737 |
+
"light": np.random.uniform(5, 10, self.n), # Few times/week
|
| 738 |
+
"moderate": np.random.uniform(10, 15, self.n), # Daily breakfast
|
| 739 |
+
"heavy": np.random.uniform(15, 25, self.n), # Multiple daily use
|
| 740 |
+
}
|
| 741 |
+
base_mins = np.zeros(self.n)
|
| 742 |
+
for pattern, mins in pattern_mins_map.items():
|
| 743 |
+
base_mins = np.where(
|
| 744 |
+
df["toaster_usage_pattern"] == pattern, mins, base_mins
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.04
|
| 748 |
+
df["toaster_real_effective_hours"] = np.where(
|
| 749 |
+
df["has_toaster"], ((base_mins * occupant_factor) / 60).clip(0.03, 0.5), 0
|
| 750 |
+
)
|
| 751 |
+
df["toaster_real_efficiency_factor"] = 1.0
|
| 752 |
+
|
| 753 |
+
# Food Processor (15%) - AI Pattern Learning
|
| 754 |
+
df["has_food_processor"] = (np.random.random(self.n) < 0.15).astype(int)
|
| 755 |
+
df["food_processor_usage_pattern"] = np.where(
|
| 756 |
+
df["has_food_processor"],
|
| 757 |
+
np.random.choice(
|
| 758 |
+
["rarely", "light", "moderate", "heavy"],
|
| 759 |
+
size=self.n,
|
| 760 |
+
p=[0.20, 0.40, 0.30, 0.10],
|
| 761 |
+
),
|
| 762 |
+
"unknown",
|
| 763 |
+
)
|
| 764 |
+
# Pattern-based minute ranges
|
| 765 |
+
pattern_mins_map = {
|
| 766 |
+
"rarely": np.random.uniform(5, 10, self.n), # Occasional prep
|
| 767 |
+
"light": np.random.uniform(10, 18, self.n), # Weekly usage
|
| 768 |
+
"moderate": np.random.uniform(18, 30, self.n), # Regular cooking
|
| 769 |
+
"heavy": np.random.uniform(30, 45, self.n), # Daily food prep
|
| 770 |
+
}
|
| 771 |
+
base_mins = np.zeros(self.n)
|
| 772 |
+
for pattern, mins in pattern_mins_map.items():
|
| 773 |
+
base_mins = np.where(
|
| 774 |
+
df["food_processor_usage_pattern"] == pattern, mins, base_mins
|
| 775 |
+
)
|
| 776 |
+
|
| 777 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.05
|
| 778 |
+
df["food_processor_real_effective_hours"] = np.where(
|
| 779 |
+
df["has_food_processor"],
|
| 780 |
+
((base_mins * occupant_factor) / 60).clip(0.08, 0.9),
|
| 781 |
+
0,
|
| 782 |
+
)
|
| 783 |
+
df["food_processor_real_efficiency_factor"] = 1.0
|
| 784 |
+
|
| 785 |
+
# Calc kWh
|
| 786 |
+
df["microwave_kwh"] = (1200 * df["microwave_real_effective_hours"] * 30) / 1000
|
| 787 |
+
df["mixer_kwh"] = (750 * df["mixer_real_effective_hours"] * 30) / 1000
|
| 788 |
+
df["rice_cooker_kwh"] = (
|
| 789 |
+
600 * df["rice_cooker_real_effective_hours"] * 30
|
| 790 |
+
) / 1000
|
| 791 |
+
df["toaster_kwh"] = (800 * df["toaster_real_effective_hours"] * 30) / 1000
|
| 792 |
+
df["food_processor_kwh"] = (
|
| 793 |
+
600 * df["food_processor_real_effective_hours"] * 30
|
| 794 |
+
) / 1000
|
| 795 |
+
return df
|
| 796 |
+
|
| 797 |
+
def generate_personal_others(self, df):
|
| 798 |
+
# Laptop (40%) - AI Pattern Learning
|
| 799 |
+
df["has_laptop"] = (np.random.random(self.n) < 0.40).astype(int)
|
| 800 |
+
df["laptop_usage_pattern"] = np.where(
|
| 801 |
+
df["has_laptop"],
|
| 802 |
+
np.random.choice(
|
| 803 |
+
["light", "moderate", "heavy", "always"],
|
| 804 |
+
size=self.n,
|
| 805 |
+
p=[0.30, 0.40, 0.25, 0.05],
|
| 806 |
+
),
|
| 807 |
+
"unknown",
|
| 808 |
+
)
|
| 809 |
+
# Pattern-based hour ranges (work/study related)
|
| 810 |
+
pattern_hours_map = {
|
| 811 |
+
"light": np.random.uniform(1, 3, self.n), # Light browsing
|
| 812 |
+
"moderate": np.random.uniform(3, 6, self.n), # Regular work
|
| 813 |
+
"heavy": np.random.uniform(6, 12, self.n), # Professional use
|
| 814 |
+
"always": np.random.uniform(12, 20, self.n), # WFH/Developers
|
| 815 |
+
}
|
| 816 |
+
base_hours = np.zeros(self.n)
|
| 817 |
+
for pattern, hours in pattern_hours_map.items():
|
| 818 |
+
base_hours = np.where(
|
| 819 |
+
df["laptop_usage_pattern"] == pattern, hours, base_hours
|
| 820 |
+
)
|
| 821 |
+
|
| 822 |
+
# Occupancy influence: More people = more usage
|
| 823 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.03
|
| 824 |
+
df["laptop_real_effective_hours"] = np.where(
|
| 825 |
+
df["has_laptop"], (base_hours * occupant_factor).clip(0.5, 20), 0
|
| 826 |
+
)
|
| 827 |
+
df["laptop_real_efficiency_factor"] = 1.0
|
| 828 |
+
|
| 829 |
+
# Hair Dryer (20%) - AI Pattern Learning
|
| 830 |
+
df["has_hair_dryer"] = (np.random.random(self.n) < 0.20).astype(int)
|
| 831 |
+
df["hair_dryer_usage_pattern"] = np.where(
|
| 832 |
+
df["has_hair_dryer"],
|
| 833 |
+
np.random.choice(
|
| 834 |
+
["rarely", "light", "moderate", "heavy"],
|
| 835 |
+
size=self.n,
|
| 836 |
+
p=[0.30, 0.40, 0.20, 0.10],
|
| 837 |
+
),
|
| 838 |
+
"unknown",
|
| 839 |
+
)
|
| 840 |
+
# Pattern-based minute ranges
|
| 841 |
+
pattern_mins_map = {
|
| 842 |
+
"rarely": np.random.uniform(2, 5, self.n), # Once/twice a week
|
| 843 |
+
"light": np.random.uniform(5, 10, self.n), # Few times a week
|
| 844 |
+
"moderate": np.random.uniform(10, 20, self.n), # Daily short use
|
| 845 |
+
"heavy": np.random.uniform(20, 40, self.n), # Daily long hair
|
| 846 |
+
}
|
| 847 |
+
base_mins = np.zeros(self.n)
|
| 848 |
+
for pattern, mins in pattern_mins_map.items():
|
| 849 |
+
base_mins = np.where(
|
| 850 |
+
df["hair_dryer_usage_pattern"] == pattern, mins, base_mins
|
| 851 |
+
)
|
| 852 |
+
|
| 853 |
+
# Occupancy influence (more family members = more usage)
|
| 854 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.08
|
| 855 |
+
df["hair_dryer_real_effective_hours"] = np.where(
|
| 856 |
+
df["has_hair_dryer"],
|
| 857 |
+
((base_mins * occupant_factor) / 60).clip(0.03, 0.8),
|
| 858 |
+
0,
|
| 859 |
+
)
|
| 860 |
+
df["hair_dryer_real_efficiency_factor"] = 1.0
|
| 861 |
+
|
| 862 |
+
# Vacuum (15%) - AI Pattern Learning
|
| 863 |
+
df["has_vacuum"] = (np.random.random(self.n) < 0.15).astype(int)
|
| 864 |
+
df["vacuum_usage_pattern"] = np.where(
|
| 865 |
+
df["has_vacuum"],
|
| 866 |
+
np.random.choice(
|
| 867 |
+
["rarely", "light", "moderate", "heavy"],
|
| 868 |
+
size=self.n,
|
| 869 |
+
p=[0.20, 0.40, 0.30, 0.10],
|
| 870 |
+
),
|
| 871 |
+
"unknown",
|
| 872 |
+
)
|
| 873 |
+
# Pattern-based minute ranges
|
| 874 |
+
pattern_mins_map = {
|
| 875 |
+
"rarely": np.random.uniform(5, 15, self.n), # Once/twice a month
|
| 876 |
+
"light": np.random.uniform(10, 20, self.n), # Weekly cleaning
|
| 877 |
+
"moderate": np.random.uniform(20, 40, self.n), # 2-3 times/week
|
| 878 |
+
"heavy": np.random.uniform(40, 60, self.n), # Daily cleaning
|
| 879 |
+
}
|
| 880 |
+
base_mins = np.zeros(self.n)
|
| 881 |
+
for pattern, mins in pattern_mins_map.items():
|
| 882 |
+
base_mins = np.where(df["vacuum_usage_pattern"] == pattern, mins, base_mins)
|
| 883 |
+
|
| 884 |
+
# Occupancy influence (larger households need more cleaning)
|
| 885 |
+
occupant_factor = 1 + (df["n_occupants"] - 4) * 0.08
|
| 886 |
+
df["vacuum_real_effective_hours"] = np.where(
|
| 887 |
+
df["has_vacuum"], ((base_mins * occupant_factor) / 60).clip(0.08, 1.2), 0
|
| 888 |
+
)
|
| 889 |
+
df["vacuum_real_efficiency_factor"] = 1.0
|
| 890 |
+
|
| 891 |
+
# Calc kWh
|
| 892 |
+
df["laptop_kwh"] = (60 * df["laptop_real_effective_hours"] * 30) / 1000
|
| 893 |
+
df["hair_dryer_kwh"] = (
|
| 894 |
+
1200 * df["hair_dryer_real_effective_hours"] * 30
|
| 895 |
+
) / 1000
|
| 896 |
+
df["vacuum_kwh"] = (1400 * df["vacuum_real_effective_hours"] * 30) / 1000
|
| 897 |
+
return df
|
| 898 |
+
|
| 899 |
+
def generate_lights_data(self, df):
|
| 900 |
+
# AI Pattern Learning for all light types
|
| 901 |
+
# LED (100% penetration)
|
| 902 |
+
df["has_led_lights"] = 1
|
| 903 |
+
df["led_lights_usage_pattern"] = np.random.choice(
|
| 904 |
+
["evening", "morning_evening", "most", "all"],
|
| 905 |
+
size=self.n,
|
| 906 |
+
p=[0.20, 0.45, 0.30, 0.05],
|
| 907 |
+
)
|
| 908 |
+
|
| 909 |
+
# Pattern-based hour ranges
|
| 910 |
+
pattern_hours_map = {
|
| 911 |
+
"evening": np.random.uniform(3, 5, self.n), # Evening only
|
| 912 |
+
"morning_evening": np.random.uniform(5, 8, self.n), # Morning + Evening
|
| 913 |
+
"most": np.random.uniform(8, 12, self.n), # Most of day
|
| 914 |
+
"all": np.random.uniform(12, 18, self.n), # Nearly 24/7
|
| 915 |
+
}
|
| 916 |
+
led_hours = np.zeros(self.n)
|
| 917 |
+
for pattern, hours_range in pattern_hours_map.items():
|
| 918 |
+
led_hours = np.where(
|
| 919 |
+
df["led_lights_usage_pattern"] == pattern, hours_range, led_hours
|
| 920 |
+
)
|
| 921 |
+
|
| 922 |
+
df["led_lights_real_effective_hours"] = led_hours
|
| 923 |
+
df["led_lights_real_efficiency_factor"] = 1.0
|
| 924 |
+
|
| 925 |
+
# CFL (50% penetration) - shares same pattern logic
|
| 926 |
+
df["has_cfl_lights"] = (np.random.random(self.n) < 0.50).astype(int)
|
| 927 |
+
df["cfl_lights_usage_pattern"] = np.where(
|
| 928 |
+
df["has_cfl_lights"],
|
| 929 |
+
df["led_lights_usage_pattern"], # Same pattern as LED
|
| 930 |
+
"unknown",
|
| 931 |
+
)
|
| 932 |
+
df["cfl_lights_real_effective_hours"] = np.where(
|
| 933 |
+
df["has_cfl_lights"], led_hours, 0
|
| 934 |
+
)
|
| 935 |
+
df["cfl_lights_real_efficiency_factor"] = 1.0
|
| 936 |
+
|
| 937 |
+
# Tube (60% penetration) - shares same pattern logic
|
| 938 |
+
df["has_tube_lights"] = (np.random.random(self.n) < 0.60).astype(int)
|
| 939 |
+
df["tube_lights_usage_pattern"] = np.where(
|
| 940 |
+
df["has_tube_lights"],
|
| 941 |
+
df["led_lights_usage_pattern"], # Same pattern as LED
|
| 942 |
+
"unknown",
|
| 943 |
+
)
|
| 944 |
+
df["tube_lights_real_effective_hours"] = np.where(
|
| 945 |
+
df["has_tube_lights"], led_hours, 0
|
| 946 |
+
)
|
| 947 |
+
df["tube_lights_real_efficiency_factor"] = 1.0
|
| 948 |
+
|
| 949 |
+
df["led_lights_kwh"] = (
|
| 950 |
+
9 * df["led_lights_real_effective_hours"] * 5 * 30
|
| 951 |
+
) / 1000 # 5 LEDs avg
|
| 952 |
+
df["cfl_lights_kwh"] = (
|
| 953 |
+
18 * df["cfl_lights_real_effective_hours"] * 2 * 30
|
| 954 |
+
) / 1000
|
| 955 |
+
df["tube_lights_kwh"] = (
|
| 956 |
+
40 * df["tube_lights_real_effective_hours"] * 2 * 30
|
| 957 |
+
) / 1000
|
| 958 |
+
return df
|
| 959 |
+
|
| 960 |
+
def generate(self):
|
| 961 |
+
df = self.generate_demographics()
|
| 962 |
+
df = self.generate_ac_data(df)
|
| 963 |
+
df = self.generate_fridge_data(df)
|
| 964 |
+
df = self.generate_fan_data(df)
|
| 965 |
+
df = self.generate_tv_data(df)
|
| 966 |
+
df = self.generate_wm_data(df)
|
| 967 |
+
df = self.generate_pump_data(df)
|
| 968 |
+
df = self.generate_geyser_data(df)
|
| 969 |
+
df = self.generate_misc_data(df)
|
| 970 |
+
df = self.generate_kitchen_others(df)
|
| 971 |
+
df = self.generate_personal_others(df)
|
| 972 |
+
df = self.generate_lights_data(df)
|
| 973 |
+
|
| 974 |
+
# Sum Total Predictor
|
| 975 |
+
items = [
|
| 976 |
+
"ac_kwh",
|
| 977 |
+
"fridge_kwh",
|
| 978 |
+
"ceiling_fan_kwh",
|
| 979 |
+
"television_kwh",
|
| 980 |
+
"washing_machine_kwh",
|
| 981 |
+
"water_pump_kwh",
|
| 982 |
+
"water_heater_kwh",
|
| 983 |
+
"iron_kwh",
|
| 984 |
+
"kettle_kwh",
|
| 985 |
+
"induction_kwh",
|
| 986 |
+
"desktop_kwh",
|
| 987 |
+
"microwave_kwh",
|
| 988 |
+
"mixer_kwh",
|
| 989 |
+
"rice_cooker_kwh",
|
| 990 |
+
"toaster_kwh",
|
| 991 |
+
"food_processor_kwh",
|
| 992 |
+
"laptop_kwh",
|
| 993 |
+
"hair_dryer_kwh",
|
| 994 |
+
"vacuum_kwh",
|
| 995 |
+
"led_lights_kwh",
|
| 996 |
+
"cfl_lights_kwh",
|
| 997 |
+
"tube_lights_kwh",
|
| 998 |
+
]
|
| 999 |
+
df["total_kwh_monthly"] = df[items].sum(axis=1) + np.random.normal(5, 2, self.n)
|
| 1000 |
+
|
| 1001 |
+
return df
|
| 1002 |
+
|
| 1003 |
+
|
| 1004 |
+
if __name__ == "__main__":
|
| 1005 |
+
sim = KeralaRealWorldSimulator()
|
| 1006 |
+
df = sim.generate()
|
| 1007 |
+
df.to_csv("kerala_smartwatt_ai.csv", index=False)
|
| 1008 |
+
print("✅ Dataset Generated: kerala_smartwatt_ai.csv")
|
Training_Scripts/train(latest).py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Training Demo Script with Visualization
|
| 3 |
+
This is the "Teacher" with built-in graph generation.
|
| 4 |
+
It trains the AI and automatically generates training accuracy/loss graphs.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
import joblib
|
| 10 |
+
import matplotlib.pyplot as plt
|
| 11 |
+
import numpy as np
|
| 12 |
+
import pandas as pd
|
| 13 |
+
import tensorflow as tf
|
| 14 |
+
from sklearn.compose import ColumnTransformer
|
| 15 |
+
from sklearn.model_selection import train_test_split
|
| 16 |
+
from sklearn.pipeline import Pipeline
|
| 17 |
+
from sklearn.preprocessing import OneHotEncoder, StandardScaler
|
| 18 |
+
from tensorflow import keras
|
| 19 |
+
from tensorflow.keras import layers
|
| 20 |
+
from tensorflow.keras.callbacks import EarlyStopping
|
| 21 |
+
|
| 22 |
+
# Ensure Model Directory Exists
|
| 23 |
+
if not os.path.exists("models"):
|
| 24 |
+
os.makedirs("models")
|
| 25 |
+
if not os.path.exists("training_graphs"):
|
| 26 |
+
os.makedirs("training_graphs")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def load_data():
|
| 30 |
+
if not os.path.exists("kerala_smartwatt_ai.csv"):
|
| 31 |
+
print("❌ Dataset not found! Run newdataset.py first.")
|
| 32 |
+
return None
|
| 33 |
+
return pd.read_csv("kerala_smartwatt_ai.csv")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def build_multi_output_model(input_dim):
|
| 37 |
+
"""
|
| 38 |
+
This is the Brain Architecture.
|
| 39 |
+
Shared layers → Split heads for Efficiency and Hours
|
| 40 |
+
"""
|
| 41 |
+
inputs = keras.Input(shape=(input_dim,))
|
| 42 |
+
|
| 43 |
+
# Shared Representation
|
| 44 |
+
x = layers.Dense(128, activation="relu")(inputs)
|
| 45 |
+
x = layers.Dropout(0.2)(x)
|
| 46 |
+
x = layers.Dense(64, activation="relu")(x)
|
| 47 |
+
|
| 48 |
+
# --- HEAD 1: EFFICIENCY FACTOR ---
|
| 49 |
+
eff_branch = layers.Dense(32, activation="relu")(x)
|
| 50 |
+
eff_output = layers.Dense(1, name="efficiency")(eff_branch)
|
| 51 |
+
|
| 52 |
+
# --- HEAD 2: EFFECTIVE HOURS ---
|
| 53 |
+
hours_branch = layers.Dense(32, activation="relu")(x)
|
| 54 |
+
hours_output = layers.Dense(1, name="hours")(hours_branch)
|
| 55 |
+
|
| 56 |
+
model = keras.Model(inputs=inputs, outputs=[eff_output, hours_output])
|
| 57 |
+
|
| 58 |
+
model.compile(
|
| 59 |
+
optimizer="adam",
|
| 60 |
+
loss={"efficiency": "mse", "hours": "mse"},
|
| 61 |
+
loss_weights={"efficiency": 10.0, "hours": 1.0},
|
| 62 |
+
metrics={"efficiency": "mae", "hours": "mae"},
|
| 63 |
+
)
|
| 64 |
+
return model
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# Store all training results and histories for final graphs
|
| 68 |
+
all_results = []
|
| 69 |
+
all_histories = []
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def plot_averaged_training_curves():
|
| 73 |
+
"""
|
| 74 |
+
Generate Figure 2.1.2: Averaged Training Curves Across All 22 Models
|
| 75 |
+
|
| 76 |
+
Shows training/validation loss and MAE over epochs (averaged across all models).
|
| 77 |
+
This demonstrates overall model convergence - exactly what Figure 2.1.2 needs.
|
| 78 |
+
"""
|
| 79 |
+
if not all_histories:
|
| 80 |
+
print("⚠️ No training histories to plot")
|
| 81 |
+
return
|
| 82 |
+
|
| 83 |
+
# Find minimum epoch count (models stopped at different epochs due to EarlyStopping)
|
| 84 |
+
min_epochs = min(len(h.history["loss"]) for h in all_histories)
|
| 85 |
+
print(
|
| 86 |
+
f"\n ℹ️ Averaging across {len(all_histories)} models up to {min_epochs} epochs (shortest training)"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Average training metrics across all models (truncate to min_epochs)
|
| 90 |
+
avg_loss = np.mean([h.history["loss"][:min_epochs] for h in all_histories], axis=0)
|
| 91 |
+
avg_val_loss = np.mean(
|
| 92 |
+
[h.history["val_loss"][:min_epochs] for h in all_histories], axis=0
|
| 93 |
+
)
|
| 94 |
+
avg_mae = np.mean(
|
| 95 |
+
[h.history["efficiency_mae"][:min_epochs] for h in all_histories], axis=0
|
| 96 |
+
)
|
| 97 |
+
avg_val_mae = np.mean(
|
| 98 |
+
[h.history["val_efficiency_mae"][:min_epochs] for h in all_histories], axis=0
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
epochs = range(1, len(avg_loss) + 1)
|
| 102 |
+
|
| 103 |
+
# Calculate statistics
|
| 104 |
+
initial_loss = avg_loss[0]
|
| 105 |
+
final_loss = avg_loss[-1]
|
| 106 |
+
final_mae = avg_mae[-1]
|
| 107 |
+
loss_reduction = (initial_loss - final_loss) / initial_loss * 100
|
| 108 |
+
|
| 109 |
+
# Create Figure 2.1.2
|
| 110 |
+
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
| 111 |
+
fig.suptitle(
|
| 112 |
+
"Figure 2.1.2: Neural Network Training Performance (Averaged Across 22 Models)",
|
| 113 |
+
fontsize=14,
|
| 114 |
+
fontweight="bold",
|
| 115 |
+
y=0.98,
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# LEFT: Loss Curves
|
| 119 |
+
axes[0].plot(
|
| 120 |
+
epochs,
|
| 121 |
+
avg_loss,
|
| 122 |
+
label="Training Loss",
|
| 123 |
+
linewidth=2.5,
|
| 124 |
+
color="#3b82f6",
|
| 125 |
+
marker="o",
|
| 126 |
+
markersize=3,
|
| 127 |
+
)
|
| 128 |
+
axes[0].plot(
|
| 129 |
+
epochs,
|
| 130 |
+
avg_val_loss,
|
| 131 |
+
label="Validation Loss",
|
| 132 |
+
linewidth=2.5,
|
| 133 |
+
color="#f59e0b",
|
| 134 |
+
marker="s",
|
| 135 |
+
markersize=3,
|
| 136 |
+
)
|
| 137 |
+
axes[0].set_title("Training & Validation Loss", fontsize=12, fontweight="bold")
|
| 138 |
+
axes[0].set_xlabel("Epoch", fontsize=11)
|
| 139 |
+
axes[0].set_ylabel("Loss (MSE)", fontsize=11)
|
| 140 |
+
axes[0].legend(fontsize=10)
|
| 141 |
+
axes[0].grid(True, alpha=0.3, linestyle="--")
|
| 142 |
+
|
| 143 |
+
# RIGHT: Accuracy Curves (MAE)
|
| 144 |
+
axes[1].plot(
|
| 145 |
+
epochs,
|
| 146 |
+
avg_mae,
|
| 147 |
+
label="Training MAE",
|
| 148 |
+
linewidth=2.5,
|
| 149 |
+
color="#10b981",
|
| 150 |
+
marker="o",
|
| 151 |
+
markersize=3,
|
| 152 |
+
)
|
| 153 |
+
axes[1].plot(
|
| 154 |
+
epochs,
|
| 155 |
+
avg_val_mae,
|
| 156 |
+
label="Validation MAE",
|
| 157 |
+
linewidth=2.5,
|
| 158 |
+
color="#ef4444",
|
| 159 |
+
marker="s",
|
| 160 |
+
markersize=3,
|
| 161 |
+
)
|
| 162 |
+
axes[1].set_title("Prediction Accuracy (MAE)", fontsize=12, fontweight="bold")
|
| 163 |
+
axes[1].set_xlabel("Epoch", fontsize=11)
|
| 164 |
+
axes[1].set_ylabel("Mean Absolute Error", fontsize=11)
|
| 165 |
+
axes[1].legend(fontsize=10)
|
| 166 |
+
axes[1].grid(True, alpha=0.3, linestyle="--")
|
| 167 |
+
|
| 168 |
+
plt.tight_layout()
|
| 169 |
+
|
| 170 |
+
# Save as Figure 2.1.2
|
| 171 |
+
graph_path = "training_graphs/figure_2.1.2_training_curves.png"
|
| 172 |
+
plt.savefig(graph_path, dpi=300, bbox_inches="tight", facecolor="white")
|
| 173 |
+
plt.close()
|
| 174 |
+
|
| 175 |
+
# Print comprehensive statistics
|
| 176 |
+
print("\n" + "=" * 70)
|
| 177 |
+
print("📊 FIGURE 2.1.2 - TRAINING STATISTICS (Averaged Across 22 Models)")
|
| 178 |
+
print("=" * 70)
|
| 179 |
+
print(f"\n🔄 LOSS (MSE):")
|
| 180 |
+
print(f" • Initial Training Loss (Epoch 1): {initial_loss:.4f}")
|
| 181 |
+
print(f" • Final Training Loss (Epoch {len(epochs)}): {final_loss:.4f}")
|
| 182 |
+
print(
|
| 183 |
+
f" • Final Validation Loss (Epoch {len(epochs)}): {avg_val_loss[-1]:.4f}"
|
| 184 |
+
)
|
| 185 |
+
print(f" • Loss Reduction: {loss_reduction:.1f}%")
|
| 186 |
+
|
| 187 |
+
print(f"\n✅ ACCURACY (MAE - Lower is Better):")
|
| 188 |
+
print(f" • Final Training MAE: {final_mae:.4f}")
|
| 189 |
+
print(f" • Final Validation MAE: {avg_val_mae[-1]:.4f}")
|
| 190 |
+
print(
|
| 191 |
+
f" • Train-Val Gap: {abs(final_mae - avg_val_mae[-1]):.4f}"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
if abs(final_mae - avg_val_mae[-1]) < 0.02:
|
| 195 |
+
print(f" • Overfitting Status: ✅ No overfitting detected")
|
| 196 |
+
else:
|
| 197 |
+
print(f" • Overfitting Status: ⚠️ Minor gap present")
|
| 198 |
+
|
| 199 |
+
print(f"\n📈 CONVERGENCE:")
|
| 200 |
+
print(f" • Average Epochs Trained: {len(epochs)}")
|
| 201 |
+
print(f" • Total Models: {len(all_histories)}")
|
| 202 |
+
print(f" • Models Used EarlyStopping: Yes (Patience=10)")
|
| 203 |
+
print("=" * 70)
|
| 204 |
+
|
| 205 |
+
print(f"\n✅ FIGURE 2.1.2 SAVED: {graph_path}")
|
| 206 |
+
print(" 📊 Left Panel: Training & Validation Loss over Epochs")
|
| 207 |
+
print(" 📊 Right Panel: MAE Accuracy over Epochs")
|
| 208 |
+
print(" 📊 Graph shows model convergence and learning effectiveness\n")
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def plot_comprehensive_comparison_graphs():
|
| 212 |
+
"""
|
| 213 |
+
Generate TWO professional graphs for academic report:
|
| 214 |
+
Graph 1: Bar Chart - MAE Efficiency across all models
|
| 215 |
+
Graph 2: Dual-Line Chart - Efficiency vs Hours MAE comparison
|
| 216 |
+
"""
|
| 217 |
+
if not all_results:
|
| 218 |
+
print("⚠️ No training results to plot")
|
| 219 |
+
return
|
| 220 |
+
|
| 221 |
+
appliances = [r["appliance"] for r in all_results]
|
| 222 |
+
mae_eff = [r["mae_efficiency"] for r in all_results]
|
| 223 |
+
mae_hours = [r["mae_hours"] for r in all_results]
|
| 224 |
+
|
| 225 |
+
# Create figure with 2 subplots
|
| 226 |
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6), dpi=300)
|
| 227 |
+
fig.suptitle(
|
| 228 |
+
"SmartWatt AI: Model Performance Comparison Across All 22 Appliances",
|
| 229 |
+
fontsize=16,
|
| 230 |
+
fontweight="bold",
|
| 231 |
+
y=0.98,
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
# GRAPH 1: Bar Chart - MAE Efficiency
|
| 235 |
+
colors = plt.cm.viridis([i / len(appliances) for i in range(len(appliances))])
|
| 236 |
+
bars = ax1.bar(
|
| 237 |
+
range(len(appliances)), mae_eff, color=colors, edgecolor="black", linewidth=0.5
|
| 238 |
+
)
|
| 239 |
+
ax1.set_xticks(range(len(appliances)))
|
| 240 |
+
ax1.set_xticklabels(
|
| 241 |
+
[a.replace("_", " ").title() for a in appliances],
|
| 242 |
+
rotation=90,
|
| 243 |
+
ha="right",
|
| 244 |
+
fontsize=8,
|
| 245 |
+
)
|
| 246 |
+
ax1.set_ylabel("MAE (Efficiency Factor)", fontsize=11, fontweight="bold")
|
| 247 |
+
ax1.set_title(
|
| 248 |
+
"Efficiency Prediction Error by Appliance",
|
| 249 |
+
fontsize=13,
|
| 250 |
+
fontweight="bold",
|
| 251 |
+
pad=10,
|
| 252 |
+
)
|
| 253 |
+
ax1.grid(True, alpha=0.3, axis="y")
|
| 254 |
+
ax1.set_ylim(0, max(mae_eff) * 1.15)
|
| 255 |
+
|
| 256 |
+
# Add value labels on bars
|
| 257 |
+
for i, (bar, val) in enumerate(zip(bars, mae_eff)):
|
| 258 |
+
height = bar.get_height()
|
| 259 |
+
ax1.text(
|
| 260 |
+
bar.get_x() + bar.get_width() / 2.0,
|
| 261 |
+
height,
|
| 262 |
+
f"{val:.3f}",
|
| 263 |
+
ha="center",
|
| 264 |
+
va="bottom",
|
| 265 |
+
fontsize=6,
|
| 266 |
+
rotation=0,
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# GRAPH 2: Dual-Line Chart - Efficiency vs Hours MAE
|
| 270 |
+
x_pos = range(len(appliances))
|
| 271 |
+
line1 = ax2.plot(
|
| 272 |
+
x_pos,
|
| 273 |
+
mae_eff,
|
| 274 |
+
marker="o",
|
| 275 |
+
markersize=6,
|
| 276 |
+
linewidth=2,
|
| 277 |
+
label="Efficiency MAE",
|
| 278 |
+
color="#2E7D32",
|
| 279 |
+
)
|
| 280 |
+
line2 = ax2.plot(
|
| 281 |
+
x_pos,
|
| 282 |
+
mae_hours,
|
| 283 |
+
marker="s",
|
| 284 |
+
markersize=6,
|
| 285 |
+
linewidth=2,
|
| 286 |
+
label="Hours MAE",
|
| 287 |
+
color="#F57C00",
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
ax2.set_xticks(x_pos)
|
| 291 |
+
ax2.set_xticklabels(
|
| 292 |
+
[a.replace("_", " ").title() for a in appliances],
|
| 293 |
+
rotation=90,
|
| 294 |
+
ha="right",
|
| 295 |
+
fontsize=8,
|
| 296 |
+
)
|
| 297 |
+
ax2.set_ylabel("MAE", fontsize=11, fontweight="bold")
|
| 298 |
+
ax2.set_title(
|
| 299 |
+
"Efficiency vs Hours Prediction Error Comparison",
|
| 300 |
+
fontsize=13,
|
| 301 |
+
fontweight="bold",
|
| 302 |
+
pad=10,
|
| 303 |
+
)
|
| 304 |
+
ax2.legend(loc="upper left", fontsize=10, framealpha=0.9)
|
| 305 |
+
ax2.grid(True, alpha=0.3)
|
| 306 |
+
ax2.set_ylim(0, max(max(mae_eff), max(mae_hours)) * 1.15)
|
| 307 |
+
|
| 308 |
+
# Add average performance line
|
| 309 |
+
avg_eff = sum(mae_eff) / len(mae_eff)
|
| 310 |
+
avg_hours = sum(mae_hours) / len(mae_hours)
|
| 311 |
+
ax2.axhline(
|
| 312 |
+
y=avg_eff,
|
| 313 |
+
color="#2E7D32",
|
| 314 |
+
linestyle="--",
|
| 315 |
+
alpha=0.5,
|
| 316 |
+
linewidth=1.5,
|
| 317 |
+
label=f"Avg Efficiency MAE: {avg_eff:.3f}",
|
| 318 |
+
)
|
| 319 |
+
ax2.axhline(
|
| 320 |
+
y=avg_hours,
|
| 321 |
+
color="#F57C00",
|
| 322 |
+
linestyle="--",
|
| 323 |
+
alpha=0.5,
|
| 324 |
+
linewidth=1.5,
|
| 325 |
+
label=f"Avg Hours MAE: {avg_hours:.2f}",
|
| 326 |
+
)
|
| 327 |
+
ax2.legend(loc="upper left", fontsize=9, framealpha=0.9)
|
| 328 |
+
|
| 329 |
+
plt.tight_layout()
|
| 330 |
+
|
| 331 |
+
# Save graph
|
| 332 |
+
graph_path = "training_graphs/model_performance_comparison.png"
|
| 333 |
+
plt.savefig(graph_path, dpi=300, bbox_inches="tight", facecolor="white")
|
| 334 |
+
print(f"\n✅ COMPREHENSIVE COMPARISON GRAPHS SAVED: {graph_path}")
|
| 335 |
+
print(f" 📊 Graph 1: MAE Efficiency Bar Chart (All 22 Models)")
|
| 336 |
+
print(f" 📊 Graph 2: Efficiency vs Hours Dual-Line Comparison")
|
| 337 |
+
print(f" 📈 Average Efficiency MAE: {avg_eff:.4f}")
|
| 338 |
+
print(f" 📈 Average Hours MAE: {avg_hours:.2f}")
|
| 339 |
+
plt.close()
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def train_appliance_model(df, app_name, target_cols, features):
|
| 343 |
+
print(f"\n🎓 Training Model for: {app_name.upper()}")
|
| 344 |
+
|
| 345 |
+
# Filter for owners
|
| 346 |
+
if f"has_{app_name}" in df.columns:
|
| 347 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 348 |
+
elif app_name == "fridge":
|
| 349 |
+
df_app = df[df["has_refrigerator"] == 1].copy()
|
| 350 |
+
elif app_name == "desktop":
|
| 351 |
+
df_app = df[df["has_computer"] == 1].copy()
|
| 352 |
+
elif app_name == "kettle":
|
| 353 |
+
df_app = df[df["has_electric_kettle"] == 1].copy()
|
| 354 |
+
elif app_name in [
|
| 355 |
+
"microwave",
|
| 356 |
+
"mixer",
|
| 357 |
+
"rice_cooker",
|
| 358 |
+
"toaster",
|
| 359 |
+
"food_processor",
|
| 360 |
+
"laptop",
|
| 361 |
+
"hair_dryer",
|
| 362 |
+
"vacuum",
|
| 363 |
+
]:
|
| 364 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 365 |
+
elif app_name in ["led_lights", "cfl_lights", "tube_lights"]:
|
| 366 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 367 |
+
else:
|
| 368 |
+
df_app = df.copy()
|
| 369 |
+
|
| 370 |
+
if len(df_app) < 100:
|
| 371 |
+
print(f"⚠️ Not enough data for {app_name} (n={len(df_app)})")
|
| 372 |
+
return
|
| 373 |
+
|
| 374 |
+
# Prepare targets
|
| 375 |
+
y_eff = df_app[f"{app_name}_real_efficiency_factor"]
|
| 376 |
+
y_hours = df_app[f"{app_name}_real_effective_hours"]
|
| 377 |
+
|
| 378 |
+
# Preprocessing
|
| 379 |
+
numeric_features = [f for f in features if df_app[f].dtype in ["int64", "float64"]]
|
| 380 |
+
categorical_features = [f for f in features if df_app[f].dtype == "object"]
|
| 381 |
+
|
| 382 |
+
preprocessor = ColumnTransformer(
|
| 383 |
+
transformers=[
|
| 384 |
+
("num", StandardScaler(), numeric_features),
|
| 385 |
+
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
|
| 386 |
+
]
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
X = preprocessor.fit_transform(df_app[features])
|
| 390 |
+
|
| 391 |
+
# Split
|
| 392 |
+
X_train, X_test, y_eff_train, y_eff_test, y_hours_train, y_hours_test = (
|
| 393 |
+
train_test_split(X, y_eff, y_hours, test_size=0.2, random_state=42)
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
# Build & Train
|
| 397 |
+
model = build_multi_output_model(X.shape[1])
|
| 398 |
+
|
| 399 |
+
# Define Early Stopping
|
| 400 |
+
early_stopping = EarlyStopping(
|
| 401 |
+
monitor="val_loss", patience=10, restore_best_weights=True, verbose=1
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
print(f" Training on {len(X_train)} samples with EarlyStopping (Patience=10)...")
|
| 405 |
+
history = model.fit(
|
| 406 |
+
X_train,
|
| 407 |
+
{"efficiency": y_eff_train, "hours": y_hours_train},
|
| 408 |
+
validation_data=(X_test, {"efficiency": y_eff_test, "hours": y_hours_test}),
|
| 409 |
+
epochs=120, # Increased from 30 to allow full convergence
|
| 410 |
+
batch_size=32,
|
| 411 |
+
callbacks=[early_stopping],
|
| 412 |
+
verbose=1, # Show progress
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
# Evaluate
|
| 416 |
+
results = model.evaluate(
|
| 417 |
+
X_test, {"efficiency": y_eff_test, "hours": y_hours_test}, verbose=0
|
| 418 |
+
)
|
| 419 |
+
print(
|
| 420 |
+
f" ✅ Trained. MAE Efficiency: {results[3]:.4f}, MAE Hours: {results[4]:.2f}"
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Save Artifacts
|
| 424 |
+
model.save(f"models/{app_name}_model.keras")
|
| 425 |
+
joblib.dump(preprocessor, f"models/{app_name}_preprocessor.pkl")
|
| 426 |
+
print(f" 💾 Saved to models/{app_name}_model.keras")
|
| 427 |
+
|
| 428 |
+
# Store history for averaged training curves (Figure 2.1.2)
|
| 429 |
+
all_histories.append(history)
|
| 430 |
+
|
| 431 |
+
# Return results for comparison graph
|
| 432 |
+
return {
|
| 433 |
+
"appliance": app_name,
|
| 434 |
+
"mae_efficiency": results[3],
|
| 435 |
+
"mae_hours": results[4],
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def run_training():
|
| 440 |
+
df = load_data()
|
| 441 |
+
if df is None:
|
| 442 |
+
return
|
| 443 |
+
|
| 444 |
+
# Train just AC model as demo (you can add more appliances)
|
| 445 |
+
print("\n" + "=" * 60)
|
| 446 |
+
print(" SMARTWATT AI TRAINING DEMO WITH VISUALIZATION")
|
| 447 |
+
print("=" * 60)
|
| 448 |
+
|
| 449 |
+
# Train all 22 appliance models
|
| 450 |
+
tasks = [
|
| 451 |
+
(
|
| 452 |
+
"ac",
|
| 453 |
+
"ac",
|
| 454 |
+
[
|
| 455 |
+
"n_occupants",
|
| 456 |
+
"season",
|
| 457 |
+
"location_type",
|
| 458 |
+
"ac_tonnage",
|
| 459 |
+
"ac_star_rating",
|
| 460 |
+
"ac_type",
|
| 461 |
+
"ac_age_years",
|
| 462 |
+
"ac_usage_pattern",
|
| 463 |
+
],
|
| 464 |
+
),
|
| 465 |
+
(
|
| 466 |
+
"fridge",
|
| 467 |
+
"fridge",
|
| 468 |
+
[
|
| 469 |
+
"n_occupants",
|
| 470 |
+
"season",
|
| 471 |
+
"location_type",
|
| 472 |
+
"fridge_capacity",
|
| 473 |
+
"fridge_age",
|
| 474 |
+
"fridge_star_rating",
|
| 475 |
+
"fridge_type",
|
| 476 |
+
"refrigerator_usage_pattern",
|
| 477 |
+
],
|
| 478 |
+
),
|
| 479 |
+
(
|
| 480 |
+
"ceiling_fan",
|
| 481 |
+
"ceiling_fan",
|
| 482 |
+
[
|
| 483 |
+
"n_occupants",
|
| 484 |
+
"season",
|
| 485 |
+
"location_type",
|
| 486 |
+
"fan_type",
|
| 487 |
+
"num_fans",
|
| 488 |
+
"fan_usage_pattern",
|
| 489 |
+
],
|
| 490 |
+
),
|
| 491 |
+
(
|
| 492 |
+
"television",
|
| 493 |
+
"television",
|
| 494 |
+
[
|
| 495 |
+
"n_occupants",
|
| 496 |
+
"season",
|
| 497 |
+
"location_type",
|
| 498 |
+
"television_type",
|
| 499 |
+
"tv_size",
|
| 500 |
+
"television_usage_pattern",
|
| 501 |
+
],
|
| 502 |
+
),
|
| 503 |
+
(
|
| 504 |
+
"washing_machine",
|
| 505 |
+
"washing_machine",
|
| 506 |
+
[
|
| 507 |
+
"n_occupants",
|
| 508 |
+
"season",
|
| 509 |
+
"location_type",
|
| 510 |
+
"wm_type",
|
| 511 |
+
"wm_capacity",
|
| 512 |
+
"wm_star_rating",
|
| 513 |
+
"wm_cycles_per_week",
|
| 514 |
+
],
|
| 515 |
+
),
|
| 516 |
+
(
|
| 517 |
+
"water_pump",
|
| 518 |
+
"water_pump",
|
| 519 |
+
[
|
| 520 |
+
"n_occupants",
|
| 521 |
+
"season",
|
| 522 |
+
"location_type",
|
| 523 |
+
"water_pump_hp",
|
| 524 |
+
"pump_usage_pattern",
|
| 525 |
+
],
|
| 526 |
+
),
|
| 527 |
+
(
|
| 528 |
+
"water_heater",
|
| 529 |
+
"water_heater",
|
| 530 |
+
[
|
| 531 |
+
"n_occupants",
|
| 532 |
+
"season",
|
| 533 |
+
"location_type",
|
| 534 |
+
"water_heater_type",
|
| 535 |
+
"water_heater_capacity",
|
| 536 |
+
"geyser_usage_pattern",
|
| 537 |
+
],
|
| 538 |
+
),
|
| 539 |
+
(
|
| 540 |
+
"iron",
|
| 541 |
+
"iron",
|
| 542 |
+
["n_occupants", "season", "location_type", "iron_usage_pattern"],
|
| 543 |
+
),
|
| 544 |
+
(
|
| 545 |
+
"kettle",
|
| 546 |
+
"kettle",
|
| 547 |
+
["n_occupants", "season", "location_type", "kettle_usage_pattern"],
|
| 548 |
+
),
|
| 549 |
+
(
|
| 550 |
+
"induction",
|
| 551 |
+
"induction",
|
| 552 |
+
["n_occupants", "season", "location_type", "induction_usage_pattern"],
|
| 553 |
+
),
|
| 554 |
+
(
|
| 555 |
+
"desktop",
|
| 556 |
+
"desktop",
|
| 557 |
+
["n_occupants", "season", "location_type", "desktop_usage_pattern"],
|
| 558 |
+
),
|
| 559 |
+
(
|
| 560 |
+
"microwave",
|
| 561 |
+
"microwave",
|
| 562 |
+
["n_occupants", "season", "location_type", "microwave_usage_pattern"],
|
| 563 |
+
),
|
| 564 |
+
(
|
| 565 |
+
"mixer",
|
| 566 |
+
"mixer",
|
| 567 |
+
["n_occupants", "season", "location_type", "mixer_usage_pattern"],
|
| 568 |
+
),
|
| 569 |
+
(
|
| 570 |
+
"rice_cooker",
|
| 571 |
+
"rice_cooker",
|
| 572 |
+
["n_occupants", "season", "location_type", "rice_cooker_usage_pattern"],
|
| 573 |
+
),
|
| 574 |
+
(
|
| 575 |
+
"toaster",
|
| 576 |
+
"toaster",
|
| 577 |
+
["n_occupants", "season", "location_type", "toaster_usage_pattern"],
|
| 578 |
+
),
|
| 579 |
+
(
|
| 580 |
+
"food_processor",
|
| 581 |
+
"food_processor",
|
| 582 |
+
["n_occupants", "season", "location_type", "food_processor_usage_pattern"],
|
| 583 |
+
),
|
| 584 |
+
(
|
| 585 |
+
"laptop",
|
| 586 |
+
"laptop",
|
| 587 |
+
["n_occupants", "season", "location_type", "laptop_usage_pattern"],
|
| 588 |
+
),
|
| 589 |
+
(
|
| 590 |
+
"hair_dryer",
|
| 591 |
+
"hair_dryer",
|
| 592 |
+
["n_occupants", "season", "location_type", "hair_dryer_usage_pattern"],
|
| 593 |
+
),
|
| 594 |
+
(
|
| 595 |
+
"vacuum",
|
| 596 |
+
"vacuum",
|
| 597 |
+
["n_occupants", "season", "location_type", "vacuum_usage_pattern"],
|
| 598 |
+
),
|
| 599 |
+
(
|
| 600 |
+
"led_lights",
|
| 601 |
+
"led_lights",
|
| 602 |
+
["n_occupants", "season", "location_type", "led_lights_usage_pattern"],
|
| 603 |
+
),
|
| 604 |
+
(
|
| 605 |
+
"cfl_lights",
|
| 606 |
+
"cfl_lights",
|
| 607 |
+
["n_occupants", "season", "location_type", "cfl_lights_usage_pattern"],
|
| 608 |
+
),
|
| 609 |
+
(
|
| 610 |
+
"tube_lights",
|
| 611 |
+
"tube_lights",
|
| 612 |
+
["n_occupants", "season", "location_type", "tube_lights_usage_pattern"],
|
| 613 |
+
),
|
| 614 |
+
]
|
| 615 |
+
|
| 616 |
+
for name, prefix, feats in tasks:
|
| 617 |
+
result = train_appliance_model(
|
| 618 |
+
df,
|
| 619 |
+
name,
|
| 620 |
+
target_cols=[
|
| 621 |
+
f"{prefix}_real_efficiency_factor",
|
| 622 |
+
f"{prefix}_real_effective_hours",
|
| 623 |
+
],
|
| 624 |
+
features=feats,
|
| 625 |
+
)
|
| 626 |
+
if result:
|
| 627 |
+
all_results.append(result)
|
| 628 |
+
|
| 629 |
+
# Generate Figure 2.1.2: Averaged training curves over epochs
|
| 630 |
+
plot_averaged_training_curves()
|
| 631 |
+
|
| 632 |
+
# Generate comprehensive comparison graphs (bonus analysis)
|
| 633 |
+
plot_comprehensive_comparison_graphs()
|
| 634 |
+
|
| 635 |
+
print("\n" + "=" * 60)
|
| 636 |
+
print("✅ TRAINING COMPLETE!")
|
| 637 |
+
print(f"📊 Trained {len(all_results)} models successfully")
|
| 638 |
+
print("📊 Figure 2.1.2: training_graphs/figure_2.1.2_training_curves.png")
|
| 639 |
+
print("📊 Comparison: training_graphs/model_performance_comparison.png")
|
| 640 |
+
print("=" * 60)
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
if __name__ == "__main__":
|
| 644 |
+
run_training()
|
Training_Scripts/train(old).py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This is the "Teacher".
|
| 3 |
+
It takes our fake simulated student data (from newdataset.py) and teaches the AI.
|
| 4 |
+
Instead of teaching it to just output one number (Bill), we teach it to understand TWO things:
|
| 5 |
+
1. How efficient is the appliance? (Is it old/rusty?)
|
| 6 |
+
2. How long is it really used? (Is the user fibbing about usage?)
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
import joblib
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import tensorflow as tf
|
| 15 |
+
from sklearn.compose import ColumnTransformer
|
| 16 |
+
from sklearn.model_selection import train_test_split
|
| 17 |
+
from sklearn.pipeline import Pipeline
|
| 18 |
+
from sklearn.preprocessing import OneHotEncoder, StandardScaler
|
| 19 |
+
from tensorflow import keras
|
| 20 |
+
from tensorflow.keras import layers
|
| 21 |
+
|
| 22 |
+
# Ensure Model Directory Exists
|
| 23 |
+
if not os.path.exists("models"):
|
| 24 |
+
os.makedirs("models")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def load_data():
|
| 28 |
+
if not os.path.exists("kerala_smartwatt_ai.csv"):
|
| 29 |
+
print("❌ Dataset not found! Run newdataset.py first.")
|
| 30 |
+
return None
|
| 31 |
+
return pd.read_csv("kerala_smartwatt_ai.csv")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def build_multi_output_model(input_dim):
|
| 35 |
+
"""
|
| 36 |
+
This is the Brain Architecture.
|
| 37 |
+
Imagine a student sharing a brain (Shared Layers) but having two mouths (Split Heads).
|
| 38 |
+
|
| 39 |
+
- The Shared Brain learns general concepts (e.g., "Big families use more power").
|
| 40 |
+
- Head 1 (Efficiency) specializes in judging the health of the machine.
|
| 41 |
+
- Head 2 (Hours) specializes in judging the behavior of the humans.
|
| 42 |
+
"""
|
| 43 |
+
inputs = keras.Input(shape=(input_dim,))
|
| 44 |
+
|
| 45 |
+
# Shared Representation
|
| 46 |
+
x = layers.Dense(128, activation="relu")(inputs)
|
| 47 |
+
x = layers.Dropout(0.2)(x)
|
| 48 |
+
x = layers.Dense(64, activation="relu")(x)
|
| 49 |
+
|
| 50 |
+
# --- HEAD 1: EFFICIENCY FACTOR ---
|
| 51 |
+
# Target: 0.6 to 1.1 (Ratio)
|
| 52 |
+
eff_branch = layers.Dense(32, activation="relu")(x)
|
| 53 |
+
eff_output = layers.Dense(1, name="efficiency")(eff_branch)
|
| 54 |
+
|
| 55 |
+
# --- HEAD 2: EFFECTIVE HOURS ---
|
| 56 |
+
# Target: 0 to 24 (Hours)
|
| 57 |
+
hours_branch = layers.Dense(32, activation="relu")(x)
|
| 58 |
+
hours_output = layers.Dense(1, name="hours")(hours_branch)
|
| 59 |
+
|
| 60 |
+
model = keras.Model(inputs=inputs, outputs=[eff_output, hours_output])
|
| 61 |
+
|
| 62 |
+
model.compile(
|
| 63 |
+
optimizer="adam",
|
| 64 |
+
loss={"efficiency": "mse", "hours": "mse"},
|
| 65 |
+
# Why weights?
|
| 66 |
+
# Efficiency is a small number (0.6 - 1.1). Hours is a big number (0 - 24).
|
| 67 |
+
# We shout louder about Efficiency (weight=10) so the AI pays attention to small changes.
|
| 68 |
+
loss_weights={"efficiency": 10.0, "hours": 1.0},
|
| 69 |
+
metrics={"efficiency": "mae", "hours": "mae"},
|
| 70 |
+
)
|
| 71 |
+
return model
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def train_appliance_model(df, app_name, target_cols, features):
|
| 75 |
+
print(f"\n Training Model for: {app_name.upper()}")
|
| 76 |
+
|
| 77 |
+
# Filter for owners
|
| 78 |
+
if f"has_{app_name}" in df.columns:
|
| 79 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 80 |
+
elif app_name == "fridge":
|
| 81 |
+
df_app = df[df["has_refrigerator"] == 1].copy()
|
| 82 |
+
elif app_name == "desktop":
|
| 83 |
+
df_app = df[df["has_computer"] == 1].copy()
|
| 84 |
+
elif app_name == "kettle":
|
| 85 |
+
df_app = df[df["has_electric_kettle"] == 1].copy()
|
| 86 |
+
elif app_name in [
|
| 87 |
+
"microwave",
|
| 88 |
+
"mixer",
|
| 89 |
+
"rice_cooker",
|
| 90 |
+
"toaster",
|
| 91 |
+
"food_processor",
|
| 92 |
+
"laptop",
|
| 93 |
+
"hair_dryer",
|
| 94 |
+
"vacuum",
|
| 95 |
+
]:
|
| 96 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 97 |
+
elif app_name in ["led_lights", "cfl_lights", "tube_lights"]:
|
| 98 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 99 |
+
else:
|
| 100 |
+
df_app = df.copy() # Fallback
|
| 101 |
+
|
| 102 |
+
if len(df_app) < 100:
|
| 103 |
+
print(f"⚠️ Not enough data for {app_name} (n={len(df_app)})")
|
| 104 |
+
return
|
| 105 |
+
|
| 106 |
+
# Prepare specific targets
|
| 107 |
+
# Expecting cols like 'ac_real_efficiency_factor', 'ac_real_effective_hours'
|
| 108 |
+
y_eff = df_app[f"{app_name}_real_efficiency_factor"]
|
| 109 |
+
y_hours = df_app[f"{app_name}_real_effective_hours"]
|
| 110 |
+
|
| 111 |
+
# Preprocessing
|
| 112 |
+
# Separate numeric and categorical
|
| 113 |
+
numeric_features = [f for f in features if df_app[f].dtype in ["int64", "float64"]]
|
| 114 |
+
categorical_features = [f for f in features if df_app[f].dtype == "object"]
|
| 115 |
+
|
| 116 |
+
preprocessor = ColumnTransformer(
|
| 117 |
+
transformers=[
|
| 118 |
+
("num", StandardScaler(), numeric_features),
|
| 119 |
+
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
|
| 120 |
+
]
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
X = preprocessor.fit_transform(df_app[features])
|
| 124 |
+
|
| 125 |
+
# Split
|
| 126 |
+
X_train, X_test, y_eff_train, y_eff_test, y_hours_train, y_hours_test = (
|
| 127 |
+
train_test_split(X, y_eff, y_hours, test_size=0.2, random_state=42)
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# Build & Train
|
| 131 |
+
model = build_multi_output_model(X.shape[1])
|
| 132 |
+
|
| 133 |
+
history = model.fit(
|
| 134 |
+
X_train,
|
| 135 |
+
{"efficiency": y_eff_train, "hours": y_hours_train},
|
| 136 |
+
validation_data=(X_test, {"efficiency": y_eff_test, "hours": y_hours_test}),
|
| 137 |
+
epochs=30, # Quick train for demo
|
| 138 |
+
batch_size=32,
|
| 139 |
+
verbose=0,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# Evaluate
|
| 143 |
+
results = model.evaluate(
|
| 144 |
+
X_test, {"efficiency": y_eff_test, "hours": y_hours_test}, verbose=0
|
| 145 |
+
)
|
| 146 |
+
print(
|
| 147 |
+
f" ✅ Trained. MAE Efficiency: {results[3]:.4f}, MAE Hours: {results[4]:.2f}"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# Save Artifacts
|
| 151 |
+
model.save(f"models/{app_name}_model.keras")
|
| 152 |
+
joblib.dump(preprocessor, f"models/{app_name}_preprocessor.pkl")
|
| 153 |
+
print(f" 💾 Saved to models/{app_name}_model.keras")
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def run_training():
|
| 157 |
+
df = load_data()
|
| 158 |
+
if df is None:
|
| 159 |
+
return
|
| 160 |
+
|
| 161 |
+
# List of models to train
|
| 162 |
+
# Format: (name, target_prefix, features)
|
| 163 |
+
# Updated to match UI fields exactly - ALL UI FIELDS USED (except hours/total_units)
|
| 164 |
+
# All appliances now include usage patterns for AI-driven personalization
|
| 165 |
+
tasks = [
|
| 166 |
+
# MAJOR APPLIANCES - Use all UI collected fields + patterns
|
| 167 |
+
(
|
| 168 |
+
"ac",
|
| 169 |
+
"ac",
|
| 170 |
+
[
|
| 171 |
+
"n_occupants",
|
| 172 |
+
"season",
|
| 173 |
+
"location_type",
|
| 174 |
+
"ac_tonnage",
|
| 175 |
+
"ac_star_rating",
|
| 176 |
+
"ac_type",
|
| 177 |
+
"ac_age_years",
|
| 178 |
+
"ac_usage_pattern",
|
| 179 |
+
],
|
| 180 |
+
),
|
| 181 |
+
(
|
| 182 |
+
"fridge",
|
| 183 |
+
"fridge",
|
| 184 |
+
[
|
| 185 |
+
"n_occupants",
|
| 186 |
+
"season",
|
| 187 |
+
"location_type",
|
| 188 |
+
"fridge_capacity",
|
| 189 |
+
"fridge_age",
|
| 190 |
+
"fridge_star_rating",
|
| 191 |
+
"fridge_type",
|
| 192 |
+
"refrigerator_usage_pattern",
|
| 193 |
+
],
|
| 194 |
+
),
|
| 195 |
+
(
|
| 196 |
+
"ceiling_fan",
|
| 197 |
+
"ceiling_fan",
|
| 198 |
+
[
|
| 199 |
+
"n_occupants",
|
| 200 |
+
"season",
|
| 201 |
+
"location_type",
|
| 202 |
+
"fan_type",
|
| 203 |
+
"num_fans",
|
| 204 |
+
"fan_usage_pattern",
|
| 205 |
+
],
|
| 206 |
+
),
|
| 207 |
+
(
|
| 208 |
+
"television",
|
| 209 |
+
"television",
|
| 210 |
+
[
|
| 211 |
+
"n_occupants",
|
| 212 |
+
"season",
|
| 213 |
+
"location_type",
|
| 214 |
+
"television_type",
|
| 215 |
+
"tv_size",
|
| 216 |
+
"television_usage_pattern",
|
| 217 |
+
],
|
| 218 |
+
),
|
| 219 |
+
(
|
| 220 |
+
"washing_machine",
|
| 221 |
+
"washing_machine",
|
| 222 |
+
[
|
| 223 |
+
"n_occupants",
|
| 224 |
+
"season",
|
| 225 |
+
"location_type",
|
| 226 |
+
"wm_type",
|
| 227 |
+
"wm_capacity",
|
| 228 |
+
"wm_star_rating",
|
| 229 |
+
"wm_cycles_per_week",
|
| 230 |
+
],
|
| 231 |
+
),
|
| 232 |
+
(
|
| 233 |
+
"water_pump",
|
| 234 |
+
"water_pump",
|
| 235 |
+
[
|
| 236 |
+
"n_occupants",
|
| 237 |
+
"season",
|
| 238 |
+
"location_type",
|
| 239 |
+
"water_pump_hp",
|
| 240 |
+
"pump_usage_pattern",
|
| 241 |
+
],
|
| 242 |
+
),
|
| 243 |
+
(
|
| 244 |
+
"water_heater",
|
| 245 |
+
"water_heater",
|
| 246 |
+
[
|
| 247 |
+
"n_occupants",
|
| 248 |
+
"season",
|
| 249 |
+
"location_type",
|
| 250 |
+
"water_heater_type",
|
| 251 |
+
"water_heater_capacity",
|
| 252 |
+
"geyser_usage_pattern",
|
| 253 |
+
],
|
| 254 |
+
),
|
| 255 |
+
# MISC APPLIANCES - Include base fields for context + patterns
|
| 256 |
+
(
|
| 257 |
+
"iron",
|
| 258 |
+
"iron",
|
| 259 |
+
["n_occupants", "season", "location_type", "iron_usage_pattern"],
|
| 260 |
+
),
|
| 261 |
+
(
|
| 262 |
+
"kettle",
|
| 263 |
+
"kettle",
|
| 264 |
+
["n_occupants", "season", "location_type", "kettle_usage_pattern"],
|
| 265 |
+
),
|
| 266 |
+
(
|
| 267 |
+
"induction",
|
| 268 |
+
"induction",
|
| 269 |
+
["n_occupants", "season", "location_type", "induction_usage_pattern"],
|
| 270 |
+
),
|
| 271 |
+
(
|
| 272 |
+
"desktop",
|
| 273 |
+
"desktop",
|
| 274 |
+
["n_occupants", "season", "location_type", "desktop_usage_pattern"],
|
| 275 |
+
),
|
| 276 |
+
(
|
| 277 |
+
"microwave",
|
| 278 |
+
"microwave",
|
| 279 |
+
["n_occupants", "season", "location_type", "microwave_usage_pattern"],
|
| 280 |
+
),
|
| 281 |
+
(
|
| 282 |
+
"mixer",
|
| 283 |
+
"mixer",
|
| 284 |
+
["n_occupants", "season", "location_type", "mixer_usage_pattern"],
|
| 285 |
+
),
|
| 286 |
+
(
|
| 287 |
+
"rice_cooker",
|
| 288 |
+
"rice_cooker",
|
| 289 |
+
["n_occupants", "season", "location_type", "rice_cooker_usage_pattern"],
|
| 290 |
+
),
|
| 291 |
+
(
|
| 292 |
+
"toaster",
|
| 293 |
+
"toaster",
|
| 294 |
+
["n_occupants", "season", "location_type", "toaster_usage_pattern"],
|
| 295 |
+
),
|
| 296 |
+
(
|
| 297 |
+
"food_processor",
|
| 298 |
+
"food_processor",
|
| 299 |
+
["n_occupants", "season", "location_type", "food_processor_usage_pattern"],
|
| 300 |
+
),
|
| 301 |
+
(
|
| 302 |
+
"laptop",
|
| 303 |
+
"laptop",
|
| 304 |
+
["n_occupants", "season", "location_type", "laptop_usage_pattern"],
|
| 305 |
+
),
|
| 306 |
+
(
|
| 307 |
+
"hair_dryer",
|
| 308 |
+
"hair_dryer",
|
| 309 |
+
["n_occupants", "season", "location_type", "hair_dryer_usage_pattern"],
|
| 310 |
+
),
|
| 311 |
+
(
|
| 312 |
+
"vacuum",
|
| 313 |
+
"vacuum",
|
| 314 |
+
["n_occupants", "season", "location_type", "vacuum_usage_pattern"],
|
| 315 |
+
),
|
| 316 |
+
(
|
| 317 |
+
"led_lights",
|
| 318 |
+
"led_lights",
|
| 319 |
+
["n_occupants", "season", "location_type", "led_lights_usage_pattern"],
|
| 320 |
+
),
|
| 321 |
+
(
|
| 322 |
+
"cfl_lights",
|
| 323 |
+
"cfl_lights",
|
| 324 |
+
["n_occupants", "season", "location_type", "cfl_lights_usage_pattern"],
|
| 325 |
+
),
|
| 326 |
+
(
|
| 327 |
+
"tube_lights",
|
| 328 |
+
"tube_lights",
|
| 329 |
+
["n_occupants", "season", "location_type", "tube_lights_usage_pattern"],
|
| 330 |
+
),
|
| 331 |
+
]
|
| 332 |
+
|
| 333 |
+
for name, prefix, feats in tasks:
|
| 334 |
+
train_appliance_model(
|
| 335 |
+
df,
|
| 336 |
+
name,
|
| 337 |
+
target_cols=[
|
| 338 |
+
f"{prefix}_real_efficiency_factor",
|
| 339 |
+
f"{prefix}_real_effective_hours",
|
| 340 |
+
],
|
| 341 |
+
features=feats,
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
print("\n✅ Training Complete (7 Models)!")
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
if __name__ == "__main__":
|
| 348 |
+
run_training()
|
Training_Scripts/train_demo.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
This is the "Teacher" with Training Visualization.
|
| 3 |
+
It takes our fake simulated student data (from newdataset.py) and teaches the AI.
|
| 4 |
+
Instead of teaching it to just output one number (Bill), we teach it to understand TWO things:
|
| 5 |
+
1. How efficient is the appliance? (Is it old/rusty?)
|
| 6 |
+
2. How long is it really used? (Is the user fibbing about usage?)
|
| 7 |
+
|
| 8 |
+
NEW: Generates training accuracy and loss graphs (Figure 2.1.2)
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
import joblib
|
| 14 |
+
import matplotlib.pyplot as plt
|
| 15 |
+
import numpy as np
|
| 16 |
+
import pandas as pd
|
| 17 |
+
import tensorflow as tf
|
| 18 |
+
from sklearn.compose import ColumnTransformer
|
| 19 |
+
from sklearn.model_selection import train_test_split
|
| 20 |
+
from sklearn.pipeline import Pipeline
|
| 21 |
+
from sklearn.preprocessing import OneHotEncoder, StandardScaler
|
| 22 |
+
from tensorflow import keras
|
| 23 |
+
from tensorflow.keras import layers
|
| 24 |
+
|
| 25 |
+
# Ensure Model and Training Logs Directories Exist
|
| 26 |
+
if not os.path.exists("models"):
|
| 27 |
+
os.makedirs("models")
|
| 28 |
+
if not os.path.exists("training_logs"):
|
| 29 |
+
os.makedirs("training_logs")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def load_data():
|
| 33 |
+
if not os.path.exists("kerala_smartwatt_ai.csv"):
|
| 34 |
+
print("❌ Dataset not found! Run newdataset.py first.")
|
| 35 |
+
return None
|
| 36 |
+
return pd.read_csv("kerala_smartwatt_ai.csv")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def build_multi_output_model(input_dim):
|
| 40 |
+
"""
|
| 41 |
+
This is the Brain Architecture.
|
| 42 |
+
Imagine a student sharing a brain (Shared Layers) but having two mouths (Split Heads).
|
| 43 |
+
|
| 44 |
+
- The Shared Brain learns general concepts (e.g., "Big families use more power").
|
| 45 |
+
- Head 1 (Efficiency) specializes in judging the health of the machine.
|
| 46 |
+
- Head 2 (Hours) specializes in judging the behavior of the humans.
|
| 47 |
+
"""
|
| 48 |
+
inputs = keras.Input(shape=(input_dim,))
|
| 49 |
+
|
| 50 |
+
# Shared Representation
|
| 51 |
+
x = layers.Dense(128, activation="relu")(inputs)
|
| 52 |
+
x = layers.Dropout(0.2)(x)
|
| 53 |
+
x = layers.Dense(64, activation="relu")(x)
|
| 54 |
+
|
| 55 |
+
# --- HEAD 1: EFFICIENCY FACTOR ---
|
| 56 |
+
# Target: 0.6 to 1.1 (Ratio)
|
| 57 |
+
eff_branch = layers.Dense(32, activation="relu")(x)
|
| 58 |
+
eff_output = layers.Dense(1, name="efficiency")(eff_branch)
|
| 59 |
+
|
| 60 |
+
# --- HEAD 2: EFFECTIVE HOURS ---
|
| 61 |
+
# Target: 0 to 24 (Hours)
|
| 62 |
+
hours_branch = layers.Dense(32, activation="relu")(x)
|
| 63 |
+
hours_output = layers.Dense(1, name="hours")(hours_branch)
|
| 64 |
+
|
| 65 |
+
model = keras.Model(inputs=inputs, outputs=[eff_output, hours_output])
|
| 66 |
+
|
| 67 |
+
model.compile(
|
| 68 |
+
optimizer="adam",
|
| 69 |
+
loss={"efficiency": "mse", "hours": "mse"},
|
| 70 |
+
# Why weights?
|
| 71 |
+
# Efficiency is a small number (0.6 - 1.1). Hours is a big number (0 - 24).
|
| 72 |
+
# We shout louder about Efficiency (weight=10) so the AI pays attention to small changes.
|
| 73 |
+
loss_weights={"efficiency": 10.0, "hours": 1.0},
|
| 74 |
+
metrics={"efficiency": "mae", "hours": "mae"},
|
| 75 |
+
)
|
| 76 |
+
return model
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def plot_training_history(history, app_name):
|
| 80 |
+
"""
|
| 81 |
+
Collect training history for final combined graph
|
| 82 |
+
"""
|
| 83 |
+
# Store history for later aggregation
|
| 84 |
+
if not hasattr(plot_training_history, "all_histories"):
|
| 85 |
+
plot_training_history.all_histories = []
|
| 86 |
+
plot_training_history.all_histories.append((app_name, history))
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def save_combined_training_graph():
|
| 90 |
+
"""
|
| 91 |
+
Generate Figure 2.1.2: Single Training Accuracy and Loss Graph
|
| 92 |
+
|
| 93 |
+
Creates two side-by-side plots showing average training performance:
|
| 94 |
+
- Left: Training & Validation Loss over epochs
|
| 95 |
+
- Right: Training & Validation MAE (accuracy) over epochs
|
| 96 |
+
|
| 97 |
+
This demonstrates overall model convergence across all appliances.
|
| 98 |
+
"""
|
| 99 |
+
if not hasattr(plot_training_history, "all_histories"):
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
histories = plot_training_history.all_histories
|
| 103 |
+
|
| 104 |
+
# Average across all models
|
| 105 |
+
avg_loss = np.mean([h.history["loss"] for _, h in histories], axis=0)
|
| 106 |
+
avg_val_loss = np.mean([h.history["val_loss"] for _, h in histories], axis=0)
|
| 107 |
+
avg_mae = np.mean([h.history["efficiency_mae"] for _, h in histories], axis=0)
|
| 108 |
+
avg_val_mae = np.mean(
|
| 109 |
+
[h.history["val_efficiency_mae"] for _, h in histories], axis=0
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# Calculate final epoch statistics
|
| 113 |
+
final_train_loss = avg_loss[-1]
|
| 114 |
+
final_val_loss = avg_val_loss[-1]
|
| 115 |
+
final_train_mae = avg_mae[-1]
|
| 116 |
+
final_val_mae = avg_val_mae[-1]
|
| 117 |
+
initial_loss = avg_loss[0]
|
| 118 |
+
|
| 119 |
+
# Print comprehensive statistics
|
| 120 |
+
print("\n" + "=" * 60)
|
| 121 |
+
print("📊 TRAINING STATISTICS (Averaged across 22 models)")
|
| 122 |
+
print("=" * 60)
|
| 123 |
+
print(f"\n🔄 LOSS (MSE):")
|
| 124 |
+
print(f" • Initial Training Loss (Epoch 1): {initial_loss:.4f}")
|
| 125 |
+
print(f" • Final Training Loss (Epoch 30): {final_train_loss:.4f}")
|
| 126 |
+
print(f" • Final Validation Loss (Epoch 30): {final_val_loss:.4f}")
|
| 127 |
+
print(
|
| 128 |
+
f" • Loss Reduction: {((initial_loss - final_train_loss) / initial_loss * 100):.1f}%"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
print(f"\n✅ ACCURACY (MAE - Lower is Better):")
|
| 132 |
+
print(f" • Final Training MAE: {final_train_mae:.4f}")
|
| 133 |
+
print(f" • Final Validation MAE: {final_val_mae:.4f}")
|
| 134 |
+
print(
|
| 135 |
+
f" • Train-Val Gap: {abs(final_train_mae - final_val_mae):.4f}"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
if abs(final_train_mae - final_val_mae) < 0.02:
|
| 139 |
+
print(f" • Overfitting Status: ✅ No overfitting detected")
|
| 140 |
+
else:
|
| 141 |
+
print(f" • Overfitting Status: ⚠️ Minor gap present")
|
| 142 |
+
|
| 143 |
+
print(f"\n📈 CONVERGENCE:")
|
| 144 |
+
print(f" • Epochs Trained: 30")
|
| 145 |
+
print(f" • Total Models: {len(histories)}")
|
| 146 |
+
print(f" • Average Final Loss: {final_train_loss:.4f}")
|
| 147 |
+
print("=" * 60 + "\n")
|
| 148 |
+
|
| 149 |
+
# Generate graph
|
| 150 |
+
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
| 151 |
+
|
| 152 |
+
# Plot 1: Loss Curves
|
| 153 |
+
axes[0].plot(avg_loss, label="Training Loss", linewidth=2.5, color="#3b82f6")
|
| 154 |
+
axes[0].plot(avg_val_loss, label="Validation Loss", linewidth=2.5, color="#f59e0b")
|
| 155 |
+
axes[0].set_title("Neural Network Training Loss", fontsize=14, fontweight="bold")
|
| 156 |
+
axes[0].set_xlabel("Epoch", fontsize=12)
|
| 157 |
+
axes[0].set_ylabel("Loss (MSE)", fontsize=12)
|
| 158 |
+
axes[0].legend(fontsize=11)
|
| 159 |
+
axes[0].grid(True, alpha=0.3, linestyle="--")
|
| 160 |
+
|
| 161 |
+
# Plot 2: Accuracy Curves (MAE)
|
| 162 |
+
axes[1].plot(avg_mae, label="Training MAE", linewidth=2.5, color="#10b981")
|
| 163 |
+
axes[1].plot(avg_val_mae, label="Validation MAE", linewidth=2.5, color="#ef4444")
|
| 164 |
+
axes[1].set_title("Prediction Accuracy (MAE)", fontsize=14, fontweight="bold")
|
| 165 |
+
axes[1].set_xlabel("Epoch", fontsize=12)
|
| 166 |
+
axes[1].set_ylabel("Mean Absolute Error", fontsize=12)
|
| 167 |
+
axes[1].legend(fontsize=11)
|
| 168 |
+
axes[1].grid(True, alpha=0.3, linestyle="--")
|
| 169 |
+
|
| 170 |
+
plt.tight_layout()
|
| 171 |
+
plt.savefig(
|
| 172 |
+
"training_logs/training_graph_figure_2.1.2.png", dpi=300, bbox_inches="tight"
|
| 173 |
+
)
|
| 174 |
+
plt.close()
|
| 175 |
+
|
| 176 |
+
print(
|
| 177 |
+
f"📊 Combined training graph saved: training_logs/training_graph_figure_2.1.2.png"
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def train_appliance_model(df, app_name, target_cols, features):
|
| 182 |
+
print(f"\n🎓 Training Model for: {app_name.upper()}")
|
| 183 |
+
|
| 184 |
+
# Filter for owners
|
| 185 |
+
if f"has_{app_name}" in df.columns:
|
| 186 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 187 |
+
elif app_name == "fridge":
|
| 188 |
+
df_app = df[df["has_refrigerator"] == 1].copy()
|
| 189 |
+
elif app_name == "desktop":
|
| 190 |
+
df_app = df[df["has_computer"] == 1].copy()
|
| 191 |
+
elif app_name == "kettle":
|
| 192 |
+
df_app = df[df["has_electric_kettle"] == 1].copy()
|
| 193 |
+
elif app_name in [
|
| 194 |
+
"microwave",
|
| 195 |
+
"mixer",
|
| 196 |
+
"rice_cooker",
|
| 197 |
+
"toaster",
|
| 198 |
+
"food_processor",
|
| 199 |
+
"laptop",
|
| 200 |
+
"hair_dryer",
|
| 201 |
+
"vacuum",
|
| 202 |
+
]:
|
| 203 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 204 |
+
elif app_name in ["led_lights", "cfl_lights", "tube_lights"]:
|
| 205 |
+
df_app = df[df[f"has_{app_name}"] == 1].copy()
|
| 206 |
+
else:
|
| 207 |
+
df_app = df.copy() # Fallback
|
| 208 |
+
|
| 209 |
+
if len(df_app) < 100:
|
| 210 |
+
print(f"⚠️ Not enough data for {app_name} (n={len(df_app)})")
|
| 211 |
+
return
|
| 212 |
+
|
| 213 |
+
# Prepare specific targets
|
| 214 |
+
# Expecting cols like 'ac_real_efficiency_factor', 'ac_real_effective_hours'
|
| 215 |
+
y_eff = df_app[f"{app_name}_real_efficiency_factor"]
|
| 216 |
+
y_hours = df_app[f"{app_name}_real_effective_hours"]
|
| 217 |
+
|
| 218 |
+
# Preprocessing
|
| 219 |
+
# Separate numeric and categorical
|
| 220 |
+
numeric_features = [f for f in features if df_app[f].dtype in ["int64", "float64"]]
|
| 221 |
+
categorical_features = [f for f in features if df_app[f].dtype == "object"]
|
| 222 |
+
|
| 223 |
+
preprocessor = ColumnTransformer(
|
| 224 |
+
transformers=[
|
| 225 |
+
("num", StandardScaler(), numeric_features),
|
| 226 |
+
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
|
| 227 |
+
]
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
X = preprocessor.fit_transform(df_app[features])
|
| 231 |
+
|
| 232 |
+
# Split
|
| 233 |
+
X_train, X_test, y_eff_train, y_eff_test, y_hours_train, y_hours_test = (
|
| 234 |
+
train_test_split(X, y_eff, y_hours, test_size=0.2, random_state=42)
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
# Build & Train
|
| 238 |
+
model = build_multi_output_model(X.shape[1])
|
| 239 |
+
|
| 240 |
+
print(f" 🔄 Training for 30 epochs...")
|
| 241 |
+
history = model.fit(
|
| 242 |
+
X_train,
|
| 243 |
+
{"efficiency": y_eff_train, "hours": y_hours_train},
|
| 244 |
+
validation_data=(X_test, {"efficiency": y_eff_test, "hours": y_hours_test}),
|
| 245 |
+
epochs=30,
|
| 246 |
+
batch_size=32,
|
| 247 |
+
verbose=0, # Silent training
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
# Collect training history for combined graph
|
| 251 |
+
plot_training_history(history, app_name)
|
| 252 |
+
|
| 253 |
+
# Evaluate
|
| 254 |
+
results = model.evaluate(
|
| 255 |
+
X_test, {"efficiency": y_eff_test, "hours": y_hours_test}, verbose=0
|
| 256 |
+
)
|
| 257 |
+
print(
|
| 258 |
+
f" ✅ Trained. MAE Efficiency: {results[3]:.4f}, MAE Hours: {results[4]:.2f}"
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# Save Artifacts
|
| 262 |
+
model.save(f"models/{app_name}_model.keras")
|
| 263 |
+
joblib.dump(preprocessor, f"models/{app_name}_preprocessor.pkl")
|
| 264 |
+
print(f" 💾 Saved to models/{app_name}_model.keras")
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def run_training():
|
| 268 |
+
df = load_data()
|
| 269 |
+
if df is None:
|
| 270 |
+
return
|
| 271 |
+
|
| 272 |
+
print("\n" + "=" * 60)
|
| 273 |
+
print("🚀 SMARTWATT AI - TRAINING WITH VISUALIZATION")
|
| 274 |
+
print("=" * 60)
|
| 275 |
+
|
| 276 |
+
# List of models to train
|
| 277 |
+
# Format: (name, target_prefix, features)
|
| 278 |
+
tasks = [
|
| 279 |
+
# MAJOR APPLIANCES
|
| 280 |
+
(
|
| 281 |
+
"ac",
|
| 282 |
+
"ac",
|
| 283 |
+
[
|
| 284 |
+
"n_occupants",
|
| 285 |
+
"season",
|
| 286 |
+
"location_type",
|
| 287 |
+
"ac_tonnage",
|
| 288 |
+
"ac_star_rating",
|
| 289 |
+
"ac_type",
|
| 290 |
+
"ac_age_years",
|
| 291 |
+
"ac_usage_pattern",
|
| 292 |
+
],
|
| 293 |
+
),
|
| 294 |
+
(
|
| 295 |
+
"fridge",
|
| 296 |
+
"fridge",
|
| 297 |
+
[
|
| 298 |
+
"n_occupants",
|
| 299 |
+
"season",
|
| 300 |
+
"location_type",
|
| 301 |
+
"fridge_capacity",
|
| 302 |
+
"fridge_age",
|
| 303 |
+
"fridge_star_rating",
|
| 304 |
+
"fridge_type",
|
| 305 |
+
"refrigerator_usage_pattern",
|
| 306 |
+
],
|
| 307 |
+
),
|
| 308 |
+
(
|
| 309 |
+
"ceiling_fan",
|
| 310 |
+
"ceiling_fan",
|
| 311 |
+
[
|
| 312 |
+
"n_occupants",
|
| 313 |
+
"season",
|
| 314 |
+
"location_type",
|
| 315 |
+
"fan_type",
|
| 316 |
+
"num_fans",
|
| 317 |
+
"fan_usage_pattern",
|
| 318 |
+
],
|
| 319 |
+
),
|
| 320 |
+
(
|
| 321 |
+
"television",
|
| 322 |
+
"television",
|
| 323 |
+
[
|
| 324 |
+
"n_occupants",
|
| 325 |
+
"season",
|
| 326 |
+
"location_type",
|
| 327 |
+
"television_type",
|
| 328 |
+
"tv_size",
|
| 329 |
+
"television_usage_pattern",
|
| 330 |
+
],
|
| 331 |
+
),
|
| 332 |
+
(
|
| 333 |
+
"washing_machine",
|
| 334 |
+
"washing_machine",
|
| 335 |
+
[
|
| 336 |
+
"n_occupants",
|
| 337 |
+
"season",
|
| 338 |
+
"location_type",
|
| 339 |
+
"wm_type",
|
| 340 |
+
"wm_capacity",
|
| 341 |
+
"wm_star_rating",
|
| 342 |
+
"wm_cycles_per_week",
|
| 343 |
+
],
|
| 344 |
+
),
|
| 345 |
+
(
|
| 346 |
+
"water_pump",
|
| 347 |
+
"water_pump",
|
| 348 |
+
[
|
| 349 |
+
"n_occupants",
|
| 350 |
+
"season",
|
| 351 |
+
"location_type",
|
| 352 |
+
"water_pump_hp",
|
| 353 |
+
"pump_usage_pattern",
|
| 354 |
+
],
|
| 355 |
+
),
|
| 356 |
+
(
|
| 357 |
+
"water_heater",
|
| 358 |
+
"water_heater",
|
| 359 |
+
[
|
| 360 |
+
"n_occupants",
|
| 361 |
+
"season",
|
| 362 |
+
"location_type",
|
| 363 |
+
"water_heater_type",
|
| 364 |
+
"water_heater_capacity",
|
| 365 |
+
"geyser_usage_pattern",
|
| 366 |
+
],
|
| 367 |
+
),
|
| 368 |
+
# MISC APPLIANCES
|
| 369 |
+
(
|
| 370 |
+
"iron",
|
| 371 |
+
"iron",
|
| 372 |
+
["n_occupants", "season", "location_type", "iron_usage_pattern"],
|
| 373 |
+
),
|
| 374 |
+
(
|
| 375 |
+
"kettle",
|
| 376 |
+
"kettle",
|
| 377 |
+
["n_occupants", "season", "location_type", "kettle_usage_pattern"],
|
| 378 |
+
),
|
| 379 |
+
(
|
| 380 |
+
"induction",
|
| 381 |
+
"induction",
|
| 382 |
+
["n_occupants", "season", "location_type", "induction_usage_pattern"],
|
| 383 |
+
),
|
| 384 |
+
(
|
| 385 |
+
"desktop",
|
| 386 |
+
"desktop",
|
| 387 |
+
["n_occupants", "season", "location_type", "desktop_usage_pattern"],
|
| 388 |
+
),
|
| 389 |
+
(
|
| 390 |
+
"microwave",
|
| 391 |
+
"microwave",
|
| 392 |
+
["n_occupants", "season", "location_type", "microwave_usage_pattern"],
|
| 393 |
+
),
|
| 394 |
+
(
|
| 395 |
+
"mixer",
|
| 396 |
+
"mixer",
|
| 397 |
+
["n_occupants", "season", "location_type", "mixer_usage_pattern"],
|
| 398 |
+
),
|
| 399 |
+
(
|
| 400 |
+
"rice_cooker",
|
| 401 |
+
"rice_cooker",
|
| 402 |
+
["n_occupants", "season", "location_type", "rice_cooker_usage_pattern"],
|
| 403 |
+
),
|
| 404 |
+
(
|
| 405 |
+
"toaster",
|
| 406 |
+
"toaster",
|
| 407 |
+
["n_occupants", "season", "location_type", "toaster_usage_pattern"],
|
| 408 |
+
),
|
| 409 |
+
(
|
| 410 |
+
"food_processor",
|
| 411 |
+
"food_processor",
|
| 412 |
+
["n_occupants", "season", "location_type", "food_processor_usage_pattern"],
|
| 413 |
+
),
|
| 414 |
+
(
|
| 415 |
+
"laptop",
|
| 416 |
+
"laptop",
|
| 417 |
+
["n_occupants", "season", "location_type", "laptop_usage_pattern"],
|
| 418 |
+
),
|
| 419 |
+
(
|
| 420 |
+
"hair_dryer",
|
| 421 |
+
"hair_dryer",
|
| 422 |
+
["n_occupants", "season", "location_type", "hair_dryer_usage_pattern"],
|
| 423 |
+
),
|
| 424 |
+
(
|
| 425 |
+
"vacuum",
|
| 426 |
+
"vacuum",
|
| 427 |
+
["n_occupants", "season", "location_type", "vacuum_usage_pattern"],
|
| 428 |
+
),
|
| 429 |
+
(
|
| 430 |
+
"led_lights",
|
| 431 |
+
"led_lights",
|
| 432 |
+
["n_occupants", "season", "location_type", "led_lights_usage_pattern"],
|
| 433 |
+
),
|
| 434 |
+
(
|
| 435 |
+
"cfl_lights",
|
| 436 |
+
"cfl_lights",
|
| 437 |
+
["n_occupants", "season", "location_type", "cfl_lights_usage_pattern"],
|
| 438 |
+
),
|
| 439 |
+
(
|
| 440 |
+
"tube_lights",
|
| 441 |
+
"tube_lights",
|
| 442 |
+
["n_occupants", "season", "location_type", "tube_lights_usage_pattern"],
|
| 443 |
+
),
|
| 444 |
+
]
|
| 445 |
+
|
| 446 |
+
for name, prefix, feats in tasks:
|
| 447 |
+
train_appliance_model(
|
| 448 |
+
df,
|
| 449 |
+
name,
|
| 450 |
+
target_cols=[
|
| 451 |
+
f"{prefix}_real_efficiency_factor",
|
| 452 |
+
f"{prefix}_real_effective_hours",
|
| 453 |
+
],
|
| 454 |
+
features=feats,
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
# Generate single combined training graph (Figure 2.1.2)
|
| 458 |
+
save_combined_training_graph()
|
| 459 |
+
|
| 460 |
+
print("\n" + "=" * 60)
|
| 461 |
+
print("✅ Training Complete! All models saved.")
|
| 462 |
+
print(
|
| 463 |
+
f"📊 Training graph (Figure 2.1.2): training_logs/training_graph_figure_2.1.2.png"
|
| 464 |
+
)
|
| 465 |
+
print(f"🤖 Model files available in: models/")
|
| 466 |
+
print("=" * 60 + "\n")
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
if __name__ == "__main__":
|
| 470 |
+
run_training()
|
Training_Scripts/train_with_logging.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Enhanced Training Script with History Logging
|
| 3 |
+
Runs actual training and saves history for visualization
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
import joblib
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pandas as pd
|
| 12 |
+
import tensorflow as tf
|
| 13 |
+
from sklearn.compose import ColumnTransformer
|
| 14 |
+
from sklearn.model_selection import train_test_split
|
| 15 |
+
from sklearn.preprocessing import OneHotEncoder, StandardScaler
|
| 16 |
+
from tensorflow import keras
|
| 17 |
+
from tensorflow.keras import layers
|
| 18 |
+
|
| 19 |
+
# Ensure directories exist
|
| 20 |
+
os.makedirs("models", exist_ok=True)
|
| 21 |
+
os.makedirs("training_logs", exist_ok=True)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def load_data():
|
| 25 |
+
if not os.path.exists("kerala_smartwatt_ai.csv"):
|
| 26 |
+
print("❌ Dataset not found! Run newdataset.py first.")
|
| 27 |
+
return None
|
| 28 |
+
return pd.read_csv("kerala_smartwatt_ai.csv")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def build_multi_output_model(input_dim):
|
| 32 |
+
inputs = keras.Input(shape=(input_dim,))
|
| 33 |
+
|
| 34 |
+
# Shared layers
|
| 35 |
+
x = layers.Dense(128, activation="relu")(inputs)
|
| 36 |
+
x = layers.Dropout(0.2)(x)
|
| 37 |
+
x = layers.Dense(64, activation="relu")(x)
|
| 38 |
+
|
| 39 |
+
# Efficiency head
|
| 40 |
+
eff_branch = layers.Dense(32, activation="relu")(x)
|
| 41 |
+
eff_output = layers.Dense(1, name="efficiency")(eff_branch)
|
| 42 |
+
|
| 43 |
+
# Hours head
|
| 44 |
+
hours_branch = layers.Dense(32, activation="relu")(x)
|
| 45 |
+
hours_output = layers.Dense(1, name="hours")(hours_branch)
|
| 46 |
+
|
| 47 |
+
model = keras.Model(inputs=inputs, outputs=[eff_output, hours_output])
|
| 48 |
+
|
| 49 |
+
model.compile(
|
| 50 |
+
optimizer="adam",
|
| 51 |
+
loss={"efficiency": "mse", "hours": "mse"},
|
| 52 |
+
loss_weights={"efficiency": 10.0, "hours": 1.0},
|
| 53 |
+
metrics={"efficiency": "mae", "hours": "mae"},
|
| 54 |
+
)
|
| 55 |
+
return model
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def train_single_appliance_with_logging(df, app_name="ac"):
|
| 59 |
+
"""Train a single appliance model and log the history"""
|
| 60 |
+
print(f"\n🚀 Training {app_name.upper()} model with full logging...")
|
| 61 |
+
|
| 62 |
+
# Filter data
|
| 63 |
+
if app_name == "ac":
|
| 64 |
+
df_app = df[df["has_ac"] == 1].copy()
|
| 65 |
+
features = [
|
| 66 |
+
"n_occupants",
|
| 67 |
+
"season",
|
| 68 |
+
"location_type",
|
| 69 |
+
"ac_tonnage",
|
| 70 |
+
"ac_star_rating",
|
| 71 |
+
"ac_type",
|
| 72 |
+
"ac_age_years",
|
| 73 |
+
"ac_usage_pattern",
|
| 74 |
+
]
|
| 75 |
+
elif app_name == "fridge":
|
| 76 |
+
df_app = df[df["has_refrigerator"] == 1].copy()
|
| 77 |
+
features = [
|
| 78 |
+
"n_occupants",
|
| 79 |
+
"season",
|
| 80 |
+
"location_type",
|
| 81 |
+
"fridge_capacity",
|
| 82 |
+
"fridge_age",
|
| 83 |
+
"fridge_star_rating",
|
| 84 |
+
"fridge_type",
|
| 85 |
+
"refrigerator_usage_pattern",
|
| 86 |
+
]
|
| 87 |
+
else:
|
| 88 |
+
print(f"❌ Appliance {app_name} not configured")
|
| 89 |
+
return None
|
| 90 |
+
|
| 91 |
+
print(f" Dataset size: {len(df_app)} households")
|
| 92 |
+
|
| 93 |
+
# Prepare targets
|
| 94 |
+
y_eff = df_app[f"{app_name}_real_efficiency_factor"]
|
| 95 |
+
y_hours = df_app[f"{app_name}_real_effective_hours"]
|
| 96 |
+
|
| 97 |
+
# Preprocessing
|
| 98 |
+
numeric_features = [f for f in features if df_app[f].dtype in ["int64", "float64"]]
|
| 99 |
+
categorical_features = [f for f in features if df_app[f].dtype == "object"]
|
| 100 |
+
|
| 101 |
+
preprocessor = ColumnTransformer(
|
| 102 |
+
transformers=[
|
| 103 |
+
("num", StandardScaler(), numeric_features),
|
| 104 |
+
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
|
| 105 |
+
]
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
X = preprocessor.fit_transform(df_app[features])
|
| 109 |
+
|
| 110 |
+
# Train/test split
|
| 111 |
+
X_train, X_test, y_eff_train, y_eff_test, y_hours_train, y_hours_test = (
|
| 112 |
+
train_test_split(X, y_eff, y_hours, test_size=0.2, random_state=42)
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Build model
|
| 116 |
+
model = build_multi_output_model(X.shape[1])
|
| 117 |
+
|
| 118 |
+
# Train with verbose output
|
| 119 |
+
print(f" Training for 30 epochs...")
|
| 120 |
+
history = model.fit(
|
| 121 |
+
X_train,
|
| 122 |
+
{"efficiency": y_eff_train, "hours": y_hours_train},
|
| 123 |
+
validation_data=(X_test, {"efficiency": y_eff_test, "hours": y_hours_test}),
|
| 124 |
+
epochs=30,
|
| 125 |
+
batch_size=32,
|
| 126 |
+
verbose=1,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# Save history
|
| 130 |
+
history_dict = {
|
| 131 |
+
"loss": [float(x) for x in history.history["loss"]],
|
| 132 |
+
"efficiency_loss": [float(x) for x in history.history["efficiency_loss"]],
|
| 133 |
+
"hours_loss": [float(x) for x in history.history["hours_loss"]],
|
| 134 |
+
"val_loss": [float(x) for x in history.history["val_loss"]],
|
| 135 |
+
"val_efficiency_loss": [
|
| 136 |
+
float(x) for x in history.history["val_efficiency_loss"]
|
| 137 |
+
],
|
| 138 |
+
"val_hours_loss": [float(x) for x in history.history["val_hours_loss"]],
|
| 139 |
+
"efficiency_mae": [float(x) for x in history.history["efficiency_mae"]],
|
| 140 |
+
"hours_mae": [float(x) for x in history.history["hours_mae"]],
|
| 141 |
+
"val_efficiency_mae": [float(x) for x in history.history["val_efficiency_mae"]],
|
| 142 |
+
"val_hours_mae": [float(x) for x in history.history["val_hours_mae"]],
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
# Save to file
|
| 146 |
+
with open(f"training_logs/{app_name}_training_history.json", "w") as f:
|
| 147 |
+
json.dump(history_dict, f, indent=2)
|
| 148 |
+
|
| 149 |
+
print(f"\n ✅ Training complete!")
|
| 150 |
+
print(f" 📊 Final metrics:")
|
| 151 |
+
print(f" Training Loss: {history_dict['loss'][-1]:.4f}")
|
| 152 |
+
print(f" Validation Loss: {history_dict['val_loss'][-1]:.4f}")
|
| 153 |
+
print(f" Efficiency MAE: {history_dict['val_efficiency_mae'][-1]:.4f}")
|
| 154 |
+
print(f" Hours MAE: {history_dict['val_hours_mae'][-1]:.2f}")
|
| 155 |
+
print(f" 💾 History saved to training_logs/{app_name}_training_history.json")
|
| 156 |
+
|
| 157 |
+
return history_dict
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
df = load_data()
|
| 162 |
+
if df is not None:
|
| 163 |
+
# Train AC model as representative example
|
| 164 |
+
history = train_single_appliance_with_logging(df, "ac")
|
anomaly_engine.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class AnomalyEngine:
|
| 2 |
+
"""
|
| 3 |
+
Diagnoses Health (Efficiency) and Behavior (Usage) Anomalies.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
@staticmethod
|
| 7 |
+
def check_anomalies(name, eff, hours):
|
| 8 |
+
status = "Normal"
|
| 9 |
+
msg = ""
|
| 10 |
+
type = "None"
|
| 11 |
+
|
| 12 |
+
# 1. USAGE ANOMALIES (Behavior)
|
| 13 |
+
usage_thresholds = {
|
| 14 |
+
"water_heater": (3.0, "Critical: Geyser running > 3h/day!"),
|
| 15 |
+
"water_pump": (2.0, "Warning: Pump running > 2h/day"),
|
| 16 |
+
"ac": (16.0, "Notice: High AC Usage (>16h)"),
|
| 17 |
+
"led_lights": (14.0, "Notice: Lights on > 14h"),
|
| 18 |
+
"iron": (1.0, "Warning: Iron Box usage high"),
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
if name in usage_thresholds:
|
| 22 |
+
thresh, warn_msg = usage_thresholds[name]
|
| 23 |
+
if hours > thresh:
|
| 24 |
+
status = "Usage_Anomaly"
|
| 25 |
+
msg = warn_msg
|
| 26 |
+
type = "Behavior"
|
| 27 |
+
|
| 28 |
+
# 2. EFFICIENCY ANOMALIES (Health Check)
|
| 29 |
+
# If a machine is consuming 25% more power than it should, it's sick.
|
| 30 |
+
if eff > 1.25:
|
| 31 |
+
status = "efficiency_critical"
|
| 32 |
+
msg = f"CRITICAL: {name} is consuming 25% more power than rated! Check for service."
|
| 33 |
+
type = "Health"
|
| 34 |
+
elif eff > 1.15 and status != "efficiency_critical":
|
| 35 |
+
status = "efficiency_warning"
|
| 36 |
+
msg = f"Warning: {name} efficiency is degrading (15% loss)."
|
| 37 |
+
type = "Health"
|
| 38 |
+
|
| 39 |
+
return {"status": status, "message": msg, "type": type}
|
kseb_tariff.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
KSEB (Kerala State Electricity Board) Tariff Calculator
|
| 3 |
+
========================================================
|
| 4 |
+
This is the "Accountant" of the system.
|
| 5 |
+
It strictly follows the 2024-2025 KSEB Rulebook.
|
| 6 |
+
|
| 7 |
+
The Logic is "Telescopic":
|
| 8 |
+
- Use a little? Pay a little per unit.
|
| 9 |
+
- Use a lot? Pay A LOT per unit.
|
| 10 |
+
- Crossing 250 units is dangerous—you lose the subsidy on EVERYTHING!
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def calculate_kseb_tariff(monthly_units: float) -> dict:
|
| 15 |
+
"""
|
| 16 |
+
Calculate Kerala State Electricity Board (KSEB) charges for bi-monthly billing.
|
| 17 |
+
|
| 18 |
+
KSEB uses telescopic tariff structure where:
|
| 19 |
+
1. Bill is calculated bi-monthly (2 months at a time)
|
| 20 |
+
2. Units are divided by 2 to get monthly average
|
| 21 |
+
3. Telescopic rates apply up to 250 units/month
|
| 22 |
+
4. Flat rates apply above 250 units/month
|
| 23 |
+
5. Fuel Surcharge Mechanism (FSM) is added
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
monthly_units (float): Monthly electricity consumption in kWh (units)
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
dict: {
|
| 30 |
+
'energy_charge': float, # Base energy cost (bi-monthly)
|
| 31 |
+
'fuel_surcharge': float, # FSM charges
|
| 32 |
+
'total': float, # Total bi-monthly bill (actual KSEB charge)
|
| 33 |
+
'monthly_estimate': float, # Approximate monthly cost (total/2)
|
| 34 |
+
'effective_rate': float, # Per-unit cost
|
| 35 |
+
'slab': str, # Tariff category
|
| 36 |
+
'monthly_units': float, # Input monthly units
|
| 37 |
+
'bi_monthly_units': float # 2-month consumption
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
Example:
|
| 41 |
+
>>> result = calculate_kseb_tariff(150)
|
| 42 |
+
>>> print(f"Bi-monthly bill: ₹{result['total']:.2f}")
|
| 43 |
+
Bi-monthly bill: ₹1279.00
|
| 44 |
+
>>> print(f"Monthly estimate: ₹{result['monthly_estimate']:.2f}")
|
| 45 |
+
Monthly estimate: ₹639.50
|
| 46 |
+
"""
|
| 47 |
+
import math
|
| 48 |
+
|
| 49 |
+
# Import Centralized Tariff Configuration
|
| 50 |
+
try:
|
| 51 |
+
from tariff_config import FLAT_RATE_SLABS, FSM_RATE, TELESCOPIC_SLABS
|
| 52 |
+
except ImportError:
|
| 53 |
+
# Fallback if config matches missing (Safety)
|
| 54 |
+
print("Warning: tariff_config.py not found, using fallback defaults.")
|
| 55 |
+
FSM_RATE = 0.13
|
| 56 |
+
TELESCOPIC_SLABS = [(50, 3.25), (50, 4.05), (50, 5.10), (50, 6.95), (50, 8.20)]
|
| 57 |
+
FLAT_RATE_SLABS = [
|
| 58 |
+
(300, 6.40),
|
| 59 |
+
(350, 7.25),
|
| 60 |
+
(400, 7.60),
|
| 61 |
+
(500, 7.90),
|
| 62 |
+
(float("inf"), 8.80),
|
| 63 |
+
]
|
| 64 |
+
|
| 65 |
+
# Calculate for single month (bi-monthly bills use monthly average)
|
| 66 |
+
energy_charge = 0.0
|
| 67 |
+
slab_category = ""
|
| 68 |
+
|
| 69 |
+
if monthly_units <= 250:
|
| 70 |
+
# --- TELESCOPIC CALCULATION ---
|
| 71 |
+
remaining = monthly_units
|
| 72 |
+
for limit, rate in TELESCOPIC_SLABS:
|
| 73 |
+
if remaining > 0:
|
| 74 |
+
chunk = min(remaining, limit)
|
| 75 |
+
energy_charge += chunk * rate
|
| 76 |
+
remaining -= chunk
|
| 77 |
+
else:
|
| 78 |
+
break
|
| 79 |
+
|
| 80 |
+
# Determine slab
|
| 81 |
+
if monthly_units <= 50:
|
| 82 |
+
slab_category = "Domestic LT-I (0-50 units)"
|
| 83 |
+
elif monthly_units <= 100:
|
| 84 |
+
slab_category = "Domestic LT-II (51-100 units)"
|
| 85 |
+
elif monthly_units <= 150:
|
| 86 |
+
slab_category = "Domestic LT-III (101-150 units)"
|
| 87 |
+
elif monthly_units <= 200:
|
| 88 |
+
slab_category = "Domestic LT-IV (151-200 units)"
|
| 89 |
+
else:
|
| 90 |
+
slab_category = "Domestic LT-V (201-250 units)"
|
| 91 |
+
|
| 92 |
+
else:
|
| 93 |
+
# --- NON-TELESCOPIC (FLAT RATE) ---
|
| 94 |
+
for limit, rate in FLAT_RATE_SLABS:
|
| 95 |
+
if monthly_units <= limit:
|
| 96 |
+
energy_charge = monthly_units * rate
|
| 97 |
+
if limit == float("inf"):
|
| 98 |
+
slab_category = "Domestic LT-VI (> 500 units)"
|
| 99 |
+
else:
|
| 100 |
+
slab_category = f"Domestic LT-VI ({int(limit)} units)"
|
| 101 |
+
break
|
| 102 |
+
|
| 103 |
+
# --- BI-MONTHLY CALCULATION ---
|
| 104 |
+
# KSEB bills every 2 months, so multiply by 2
|
| 105 |
+
bi_monthly_energy = energy_charge * 2
|
| 106 |
+
bi_monthly_units = monthly_units * 2
|
| 107 |
+
|
| 108 |
+
# --- ADD FUEL SURCHARGE ---
|
| 109 |
+
fuel_surcharge = bi_monthly_units * FSM_RATE
|
| 110 |
+
|
| 111 |
+
# --- TOTAL BILL ---
|
| 112 |
+
total_amount = bi_monthly_energy + fuel_surcharge
|
| 113 |
+
|
| 114 |
+
# --- EFFECTIVE RATE ---
|
| 115 |
+
effective_rate = total_amount / bi_monthly_units if bi_monthly_units > 0 else 0
|
| 116 |
+
|
| 117 |
+
return {
|
| 118 |
+
"energy_charge": round(bi_monthly_energy, 2),
|
| 119 |
+
"fuel_surcharge": round(fuel_surcharge, 2),
|
| 120 |
+
"total": round(total_amount, 2), # Bi-monthly bill (what KSEB charges)
|
| 121 |
+
"monthly_estimate": round(total_amount / 2, 2), # Approximate monthly cost
|
| 122 |
+
"effective_rate": round(effective_rate, 2),
|
| 123 |
+
"slab": slab_category,
|
| 124 |
+
"monthly_units": round(monthly_units, 2),
|
| 125 |
+
"bi_monthly_units": round(bi_monthly_units, 2),
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def calculate_savings_potential(current_kwh: float, reduced_kwh: float) -> dict:
|
| 130 |
+
"""
|
| 131 |
+
Calculate potential savings from reducing consumption.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
current_kwh: Current monthly consumption
|
| 135 |
+
reduced_kwh: Projected consumption after improvements
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
dict: {
|
| 139 |
+
'current_bill': float,
|
| 140 |
+
'reduced_bill': float,
|
| 141 |
+
'monthly_savings': float,
|
| 142 |
+
'annual_savings': float,
|
| 143 |
+
'reduction_percentage': float
|
| 144 |
+
}
|
| 145 |
+
"""
|
| 146 |
+
current_bill = calculate_kseb_tariff(current_kwh)
|
| 147 |
+
reduced_bill = calculate_kseb_tariff(reduced_kwh)
|
| 148 |
+
|
| 149 |
+
monthly_savings = current_bill["total"] - reduced_bill["total"]
|
| 150 |
+
annual_savings = monthly_savings * 6 # 6 bi-monthly bills per year
|
| 151 |
+
reduction_pct = (
|
| 152 |
+
((current_kwh - reduced_kwh) / current_kwh * 100) if current_kwh > 0 else 0
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
return {
|
| 156 |
+
"current_bill": current_bill["total"],
|
| 157 |
+
"reduced_bill": reduced_bill["total"],
|
| 158 |
+
"monthly_savings": round(monthly_savings, 2),
|
| 159 |
+
"annual_savings": round(annual_savings, 2),
|
| 160 |
+
"reduction_percentage": round(reduction_pct, 1),
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# Example usage and validation
|
| 165 |
+
if __name__ == "__main__":
|
| 166 |
+
print("=" * 60)
|
| 167 |
+
print("KSEB TARIFF CALCULATOR - VALIDATION")
|
| 168 |
+
print("=" * 60)
|
| 169 |
+
|
| 170 |
+
# Test cases based on actual KSEB bills
|
| 171 |
+
test_cases = [
|
| 172 |
+
(75, "Expected: ~₹270 (Telescopic LT-II)"),
|
| 173 |
+
(150, "Expected: ~₹547 (Telescopic LT-III)"),
|
| 174 |
+
(250, "Expected: ~₹967 (Telescopic LT-V)"),
|
| 175 |
+
(300, "Expected: ~₹1,212 (Flat ₹6.40/unit)"),
|
| 176 |
+
(450, "Expected: ~₹2,673 (Flat ₹7.90/unit)"),
|
| 177 |
+
]
|
| 178 |
+
|
| 179 |
+
for units, expected in test_cases:
|
| 180 |
+
result = calculate_kseb_tariff(units)
|
| 181 |
+
print(f"\n{units} units/month:")
|
| 182 |
+
print(f" Bi-monthly Bill: ₹{result['total']:.2f} (KSEB charges for 2 months)")
|
| 183 |
+
print(f" Monthly Estimate: ₹{result['monthly_estimate']:.2f}")
|
| 184 |
+
print(f" Energy Charge: ₹{result['energy_charge']:.2f}")
|
| 185 |
+
print(f" Fuel Surcharge: ₹{result['fuel_surcharge']:.2f}")
|
| 186 |
+
print(f" Slab: {result['slab']}")
|
| 187 |
+
print(f" Effective Rate: ₹{result['effective_rate']:.2f}/unit")
|
| 188 |
+
|
| 189 |
+
print("\n" + "=" * 60)
|
| 190 |
+
print("Validation complete. Use calculate_kseb_tariff() in your code.")
|
| 191 |
+
print("=" * 60)
|
learned_params.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"70405caa1ce071976175ebb8fa6b53f7": {
|
| 3 |
+
"background_load_kwh": 0.0,
|
| 4 |
+
"efficiency_bias": 1.3,
|
| 5 |
+
"sample_count": 46
|
| 6 |
+
},
|
| 7 |
+
"90c9448889f8ddff02ed77470f390238": {
|
| 8 |
+
"background_load_kwh": 0.0,
|
| 9 |
+
"efficiency_bias": 1.0886484399437306,
|
| 10 |
+
"sample_count": 2
|
| 11 |
+
}
|
| 12 |
+
}
|
learning/__init__.py
ADDED
|
File without changes
|
learning/core.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Dict
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class Learner:
|
| 5 |
+
def __init__(self, learning_rate=0.05, max_bias_change=0.05):
|
| 6 |
+
self.alpha = learning_rate
|
| 7 |
+
self.limit = max_bias_change # Max 5% change per step
|
| 8 |
+
|
| 9 |
+
def calculate_updates(
|
| 10 |
+
self,
|
| 11 |
+
profile: Dict,
|
| 12 |
+
predicted_total: float,
|
| 13 |
+
actual_bill: float,
|
| 14 |
+
confidence: float,
|
| 15 |
+
) -> Dict[str, Any]:
|
| 16 |
+
"""
|
| 17 |
+
Core Learning Logic.
|
| 18 |
+
Compare Prediction vs Bill.
|
| 19 |
+
Return new parameters.
|
| 20 |
+
confidence: 0-100
|
| 21 |
+
"""
|
| 22 |
+
current_bias = profile.get("efficiency_bias", 1.0)
|
| 23 |
+
|
| 24 |
+
# DYNAMIC LEARNING RATE
|
| 25 |
+
# If High Confidence (>90%): Learn fast (we trust the data).
|
| 26 |
+
# If Low Confidence (<70%): Learn slow (reduce noise).
|
| 27 |
+
# Scale: 1.0 at 90%, 0.2 at 60%.
|
| 28 |
+
|
| 29 |
+
confidence_factor = max(0.2, min(1.0, (confidence - 50) / 40))
|
| 30 |
+
effective_alpha = self.alpha * confidence_factor
|
| 31 |
+
|
| 32 |
+
# 1. Calculate Gap
|
| 33 |
+
# Gap = Actual - Predicted
|
| 34 |
+
gap = actual_bill - predicted_total
|
| 35 |
+
|
| 36 |
+
updates = {}
|
| 37 |
+
|
| 38 |
+
# 2. Safety Check: Is Gap reasonable?
|
| 39 |
+
# If Gap is > 50% of bill, it might be a data entry error (e.g. 1500 instead of 150).
|
| 40 |
+
# Ignore extreme outliers.
|
| 41 |
+
if abs(gap) > (actual_bill * 0.6):
|
| 42 |
+
return {}
|
| 43 |
+
|
| 44 |
+
# 3. Allocation Strategy
|
| 45 |
+
# We attribute 20% of the error to "Efficiency Bias" (Global Multiplier)
|
| 46 |
+
# And 80% to "Background Load" (Constant Additive).
|
| 47 |
+
|
| 48 |
+
# A. Efficiency Update
|
| 49 |
+
# If predicted 100, actual 110. Gap +10.
|
| 50 |
+
# We want predicted to rise.
|
| 51 |
+
# Target change = 10 * 0.2 = 2 kWh.
|
| 52 |
+
# Relative change = 2 / 100 = +2%.
|
| 53 |
+
# New Bias = Old * (1 + 0.02 * Alpha).
|
| 54 |
+
# Using simple EMA on the Bias Factor directly.
|
| 55 |
+
|
| 56 |
+
# Determine direction
|
| 57 |
+
direction = 1 if gap > 0 else -1
|
| 58 |
+
|
| 59 |
+
# Update Bias
|
| 60 |
+
# We nudge bias by LearningRate * Direction * Magnitude
|
| 61 |
+
# But we Clamp magnitude.
|
| 62 |
+
nudge = effective_alpha * (abs(gap) / max(1, predicted_total)) * direction
|
| 63 |
+
nudge = max(-self.limit, min(self.limit, nudge)) # Clamp to +/- 5%
|
| 64 |
+
|
| 65 |
+
new_bias = current_bias * (1 + nudge)
|
| 66 |
+
|
| 67 |
+
# Hard Clamp on result (0.8 to 1.3) ~ +/- 20-30% global error correction limit
|
| 68 |
+
new_bias = max(0.8, min(1.3, new_bias))
|
| 69 |
+
|
| 70 |
+
updates["efficiency_bias"] = new_bias
|
| 71 |
+
|
| 72 |
+
# B. Background Load Update
|
| 73 |
+
# Remaining gap attributed here.
|
| 74 |
+
# Ideally, we verify this against 'Unaccounted' logic.
|
| 75 |
+
|
| 76 |
+
return updates
|
learning/memory.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from typing import Any, Dict
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class HouseholdMemory:
|
| 8 |
+
def __init__(self, storage_file="learned_params.json"):
|
| 9 |
+
self.storage_file = storage_file
|
| 10 |
+
self.memory_cache = {}
|
| 11 |
+
self._load_memory()
|
| 12 |
+
|
| 13 |
+
def _load_memory(self):
|
| 14 |
+
if os.path.exists(self.storage_file):
|
| 15 |
+
try:
|
| 16 |
+
with open(self.storage_file, "r") as f:
|
| 17 |
+
self.memory_cache = json.load(f)
|
| 18 |
+
except Exception as e:
|
| 19 |
+
print(f"Error loading memory: {e}")
|
| 20 |
+
self.memory_cache = {}
|
| 21 |
+
|
| 22 |
+
def _save_memory(self):
|
| 23 |
+
try:
|
| 24 |
+
with open(self.storage_file, "w") as f:
|
| 25 |
+
json.dump(self.memory_cache, f, indent=2)
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"Error saving memory: {e}")
|
| 28 |
+
|
| 29 |
+
def get_profile_hash(self, context: Dict[str, Any]) -> str:
|
| 30 |
+
"""Create a stable hash from household context (Pseudo-ID)"""
|
| 31 |
+
# We use strict keys to ensure same household gets same hash
|
| 32 |
+
key_parts = [
|
| 33 |
+
str(context.get("n_occupants", 4)),
|
| 34 |
+
str(context.get("location_type", "urban")),
|
| 35 |
+
str(context.get("house_type", "Apartment")),
|
| 36 |
+
str(
|
| 37 |
+
context.get("total_kwh_monthly", 0)
|
| 38 |
+
), # Using Bill as part of ID might be strict?
|
| 39 |
+
# Actually user changes bill monthly.
|
| 40 |
+
# Better to use just structural info + training_id if available?
|
| 41 |
+
# User wants "Per household".
|
| 42 |
+
# For now, let's use the 'id' if passed, or hash of immutable traits.
|
| 43 |
+
]
|
| 44 |
+
# Ideally, Training ID is the session key.
|
| 45 |
+
if "training_id" in context:
|
| 46 |
+
return str(context["training_id"])
|
| 47 |
+
|
| 48 |
+
# Fallback to structural hash
|
| 49 |
+
combined = "-".join(key_parts)
|
| 50 |
+
return hashlib.md5(combined.encode()).hexdigest()
|
| 51 |
+
|
| 52 |
+
def get_profile(self, profile_id: str) -> Dict[str, Any]:
|
| 53 |
+
return self.memory_cache.get(
|
| 54 |
+
profile_id,
|
| 55 |
+
{"background_load_kwh": 0.0, "efficiency_bias": 1.0, "sample_count": 0},
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
def update_profile(self, profile_id: str, updates: Dict[str, Any]):
|
| 59 |
+
profile = self.get_profile(profile_id)
|
| 60 |
+
profile.update(updates)
|
| 61 |
+
self.memory_cache[profile_id] = profile
|
| 62 |
+
self._save_memory()
|
learning/pipeline.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
|
| 3 |
+
from .core import Learner
|
| 4 |
+
from .memory import HouseholdMemory
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger("LearningPipeline")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class LearningPipeline:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.memory = HouseholdMemory()
|
| 12 |
+
self.learner = Learner()
|
| 13 |
+
|
| 14 |
+
def get_context_bias(self, context: dict) -> float:
|
| 15 |
+
"""
|
| 16 |
+
Called BEFORE prediction.
|
| 17 |
+
Returns the efficiency multiplier to apply (e.g., 1.05).
|
| 18 |
+
"""
|
| 19 |
+
pid = self.memory.get_profile_hash(context)
|
| 20 |
+
profile = self.memory.get_profile(pid)
|
| 21 |
+
return profile.get("efficiency_bias", 1.0)
|
| 22 |
+
|
| 23 |
+
def learn(
|
| 24 |
+
self,
|
| 25 |
+
context: dict,
|
| 26 |
+
predicted_total: float,
|
| 27 |
+
actual_bill: float,
|
| 28 |
+
confidence: float = 80.0,
|
| 29 |
+
):
|
| 30 |
+
"""
|
| 31 |
+
Called AFTER prediction.
|
| 32 |
+
Updates the profile based on the gap.
|
| 33 |
+
Args:
|
| 34 |
+
confidence: 0-100 score indicating how sure the AI was about the breakdown.
|
| 35 |
+
"""
|
| 36 |
+
pid = self.memory.get_profile_hash(context)
|
| 37 |
+
profile = self.memory.get_profile(pid)
|
| 38 |
+
|
| 39 |
+
updates = self.learner.calculate_updates(
|
| 40 |
+
profile, predicted_total, actual_bill, confidence
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
if updates:
|
| 44 |
+
# Update sample count
|
| 45 |
+
updates["sample_count"] = profile.get("sample_count", 0) + 1
|
| 46 |
+
|
| 47 |
+
logger.info(
|
| 48 |
+
f"LEARNING [{pid}]: Gap={actual_bill - predicted_total:.1f}. "
|
| 49 |
+
f"Bias {profile.get('efficiency_bias', 1.0):.3f} -> {updates.get('efficiency_bias'):.3f}"
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
self.memory.update_profile(pid, updates)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# Singleton
|
| 56 |
+
pipeline = LearningPipeline()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def get_pipeline():
|
| 60 |
+
return pipeline
|
main.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import asynccontextmanager
|
| 2 |
+
from typing import Any, Dict, List, Optional
|
| 3 |
+
|
| 4 |
+
from fastapi import FastAPI, HTTPException
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from kseb_tariff import calculate_kseb_tariff
|
| 7 |
+
# Import Core Logic and Engine
|
| 8 |
+
from predictor import get_predictor
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from routers import appliances
|
| 11 |
+
|
| 12 |
+
# Initialize App & Predictor
|
| 13 |
+
predictor = get_predictor()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@asynccontextmanager
|
| 17 |
+
async def lifespan(app: FastAPI):
|
| 18 |
+
"""Load all AI models on server startup to ensure fast first responses"""
|
| 19 |
+
print("\n" + "=" * 50)
|
| 20 |
+
print(" SMARTWATT AI ENGINE STARTUP")
|
| 21 |
+
print(" (Hybrid AI + Inferred Physics Mode)")
|
| 22 |
+
print("=" * 50 + "\n")
|
| 23 |
+
predictor.preload_all_models()
|
| 24 |
+
print("\n" + "=" * 50)
|
| 25 |
+
print(" READY TO SERVE PREDICTIONS")
|
| 26 |
+
print("=" * 50 + "\n")
|
| 27 |
+
|
| 28 |
+
yield
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
app = FastAPI(title="SmartWatt AI Backend", lifespan=lifespan)
|
| 32 |
+
# Explicitly initialize predictor here to ensure singleton is warm,
|
| 33 |
+
# although the router also gets it.
|
| 34 |
+
|
| 35 |
+
# CORS (The Bouncer of the Club)
|
| 36 |
+
app.add_middleware(
|
| 37 |
+
CORSMiddleware,
|
| 38 |
+
allow_origins=["*"],
|
| 39 |
+
allow_credentials=True,
|
| 40 |
+
allow_methods=["*"],
|
| 41 |
+
allow_headers=["*"],
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Include Routers
|
| 45 |
+
app.include_router(appliances.router)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@app.get("/health")
|
| 49 |
+
async def health_check():
|
| 50 |
+
"""Lightweight health check for keep-alive pings"""
|
| 51 |
+
return {"status": "ok", "service": "smartwatt-backend"}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@app.get("/")
|
| 55 |
+
def root():
|
| 56 |
+
return {"status": "online", "message": "SmartWatt (Hybrid AI + Physics) is Active"}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# --- PYDANTIC MODELS (The Forms) ---
|
| 60 |
+
class HouseholdData(BaseModel):
|
| 61 |
+
kwh: float = Field(
|
| 62 |
+
..., gt=0, description="Bi-monthly energy consumption in kWh must be positive"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class BillResult(BaseModel):
|
| 67 |
+
total: float
|
| 68 |
+
monthly: float
|
| 69 |
+
slab: str
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@app.post("/calculate-bill", response_model=BillResult)
|
| 73 |
+
def get_bill(data: HouseholdData):
|
| 74 |
+
"""Calculates KSEB Bill based on total units"""
|
| 75 |
+
try:
|
| 76 |
+
# Reuse your existing kseb_tariff.py logic
|
| 77 |
+
result = calculate_kseb_tariff(
|
| 78 |
+
data.kwh / 2
|
| 79 |
+
) # Convert bi-monthly to monthly for calculation logic if needed
|
| 80 |
+
return {
|
| 81 |
+
"total": result["total"],
|
| 82 |
+
"monthly": result["monthly_estimate"],
|
| 83 |
+
"slab": result["slab"],
|
| 84 |
+
}
|
| 85 |
+
except Exception as e:
|
| 86 |
+
raise HTTPException(status_code=400, detail=str(e))
|
models/ac_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e7bca4a3a3adca1287806903cd278142a7c25ab20c0a7bd326550a8980287729
|
| 3 |
+
size 224789
|
models/ac_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6a51122f4273a6427ca27338741cb5c603cf1f16aabdface08a382db81c950cb
|
| 3 |
+
size 3651
|
models/all_models_evaluation_summary.txt
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
COMPREHENSIVE EVALUATION SUMMARY - ALL MODELS
|
| 2 |
+
======================================================================
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
AC
|
| 6 |
+
----------------------------------------
|
| 7 |
+
Samples: 1007
|
| 8 |
+
MAE: 9.94 kWh
|
| 9 |
+
RMSE: 16.28 kWh
|
| 10 |
+
R²: 0.996
|
| 11 |
+
MAPE: 4.5%
|
| 12 |
+
Within ±10%: 89.5%
|
| 13 |
+
Within ±20%: 95.4%
|
| 14 |
+
Quality: EXCELLENT
|
| 15 |
+
|
| 16 |
+
FRIDGE
|
| 17 |
+
----------------------------------------
|
| 18 |
+
Samples: 1791
|
| 19 |
+
MAE: 9.05 kWh
|
| 20 |
+
RMSE: 11.23 kWh
|
| 21 |
+
R²: 0.029
|
| 22 |
+
MAPE: 13.4%
|
| 23 |
+
Within ±10%: 46.7%
|
| 24 |
+
Within ±20%: 78.0%
|
| 25 |
+
Quality: ACCEPTABLE
|
| 26 |
+
|
| 27 |
+
WASHING_MACHINE
|
| 28 |
+
----------------------------------------
|
| 29 |
+
Samples: 1333
|
| 30 |
+
MAE: 0.17 kWh
|
| 31 |
+
RMSE: 0.21 kWh
|
| 32 |
+
R²: -3359647538249283376798236672.000
|
| 33 |
+
MAPE: 1.3%
|
| 34 |
+
Within ±10%: 99.9%
|
| 35 |
+
Within ±20%: 100.0%
|
| 36 |
+
Quality: ACCEPTABLE
|
| 37 |
+
|
| 38 |
+
WATER_HEATER
|
| 39 |
+
----------------------------------------
|
| 40 |
+
Samples: 887
|
| 41 |
+
MAE: 0.97 kWh
|
| 42 |
+
RMSE: 2.95 kWh
|
| 43 |
+
R²: 0.000
|
| 44 |
+
MAPE: 1.1%
|
| 45 |
+
Within ±10%: 99.3%
|
| 46 |
+
Within ±20%: 99.4%
|
| 47 |
+
Quality: ACCEPTABLE
|
| 48 |
+
|
| 49 |
+
WATER_PUMP
|
| 50 |
+
----------------------------------------
|
| 51 |
+
Samples: 1496
|
| 52 |
+
MAE: 0.16 kWh
|
| 53 |
+
RMSE: 0.22 kWh
|
| 54 |
+
R²: 0.000
|
| 55 |
+
MAPE: 0.7%
|
| 56 |
+
Within ±10%: 100.0%
|
| 57 |
+
Within ±20%: 100.0%
|
| 58 |
+
Quality: ACCEPTABLE
|
| 59 |
+
|
| 60 |
+
TELEVISION
|
| 61 |
+
----------------------------------------
|
| 62 |
+
Samples: 1807
|
| 63 |
+
MAE: 0.29 kWh
|
| 64 |
+
RMSE: 0.41 kWh
|
| 65 |
+
R²: 0.995
|
| 66 |
+
MAPE: 3.2%
|
| 67 |
+
Within ±10%: 95.6%
|
| 68 |
+
Within ±20%: 99.0%
|
| 69 |
+
Quality: EXCELLENT
|
| 70 |
+
|
| 71 |
+
MIXER_GRINDER
|
| 72 |
+
----------------------------------------
|
| 73 |
+
Samples: 1980
|
| 74 |
+
MAE: 0.11 kWh
|
| 75 |
+
RMSE: 0.15 kWh
|
| 76 |
+
R²: 0.000
|
| 77 |
+
MAPE: 1.0%
|
| 78 |
+
Within ±10%: 99.9%
|
| 79 |
+
Within ±20%: 100.0%
|
| 80 |
+
Quality: ACCEPTABLE
|
| 81 |
+
|
| 82 |
+
MICROWAVE
|
| 83 |
+
----------------------------------------
|
| 84 |
+
Samples: 786
|
| 85 |
+
MAE: 0.10 kWh
|
| 86 |
+
RMSE: 0.12 kWh
|
| 87 |
+
R²: 0.000
|
| 88 |
+
MAPE: 0.8%
|
| 89 |
+
Within ±10%: 100.0%
|
| 90 |
+
Within ±20%: 100.0%
|
| 91 |
+
Quality: ACCEPTABLE
|
| 92 |
+
|
| 93 |
+
CEILING_FAN
|
| 94 |
+
----------------------------------------
|
| 95 |
+
Samples: 2000
|
| 96 |
+
MAE: 1.63 kWh
|
| 97 |
+
RMSE: 2.25 kWh
|
| 98 |
+
R²: 0.998
|
| 99 |
+
MAPE: 3.2%
|
| 100 |
+
Within ±10%: 94.4%
|
| 101 |
+
Within ±20%: 99.1%
|
| 102 |
+
Quality: EXCELLENT
|
| 103 |
+
|
| 104 |
+
LED_LIGHT
|
| 105 |
+
----------------------------------------
|
| 106 |
+
Samples: 1999
|
| 107 |
+
MAE: 0.32 kWh
|
| 108 |
+
RMSE: 0.41 kWh
|
| 109 |
+
R²: 0.995
|
| 110 |
+
MAPE: 2.9%
|
| 111 |
+
Within ±10%: 96.4%
|
| 112 |
+
Within ±20%: 99.0%
|
| 113 |
+
Quality: EXCELLENT
|
| 114 |
+
|
| 115 |
+
TUBE_LIGHT
|
| 116 |
+
----------------------------------------
|
| 117 |
+
Samples: 1725
|
| 118 |
+
MAE: 0.34 kWh
|
| 119 |
+
RMSE: 0.53 kWh
|
| 120 |
+
R²: 0.996
|
| 121 |
+
MAPE: 2.8%
|
| 122 |
+
Within ±10%: 96.7%
|
| 123 |
+
Within ±20%: 99.9%
|
| 124 |
+
Quality: EXCELLENT
|
| 125 |
+
|
| 126 |
+
CFL_BULB
|
| 127 |
+
----------------------------------------
|
| 128 |
+
Samples: 1261
|
| 129 |
+
MAE: 0.17 kWh
|
| 130 |
+
RMSE: 0.22 kWh
|
| 131 |
+
R²: 0.996
|
| 132 |
+
MAPE: 3.3%
|
| 133 |
+
Within ±10%: 99.4%
|
| 134 |
+
Within ±20%: 99.8%
|
| 135 |
+
Quality: EXCELLENT
|
| 136 |
+
|
| 137 |
+
DESKTOP
|
| 138 |
+
----------------------------------------
|
| 139 |
+
Samples: 1095
|
| 140 |
+
MAE: 0.50 kWh
|
| 141 |
+
RMSE: 0.62 kWh
|
| 142 |
+
R²: 0.998
|
| 143 |
+
MAPE: 2.1%
|
| 144 |
+
Within ±10%: 98.1%
|
| 145 |
+
Within ±20%: 100.0%
|
| 146 |
+
Quality: EXCELLENT
|
| 147 |
+
|
| 148 |
+
LAPTOP
|
| 149 |
+
----------------------------------------
|
| 150 |
+
Samples: 1390
|
| 151 |
+
MAE: 0.13 kWh
|
| 152 |
+
RMSE: 0.18 kWh
|
| 153 |
+
R²: 0.999
|
| 154 |
+
MAPE: 1.4%
|
| 155 |
+
Within ±10%: 98.7%
|
| 156 |
+
Within ±20%: 99.4%
|
| 157 |
+
Quality: EXCELLENT
|
| 158 |
+
|
| 159 |
+
IRON
|
| 160 |
+
----------------------------------------
|
| 161 |
+
Samples: 1799
|
| 162 |
+
MAE: 0.13 kWh
|
| 163 |
+
RMSE: 0.29 kWh
|
| 164 |
+
R²: 0.989
|
| 165 |
+
MAPE: 3.1%
|
| 166 |
+
Within ±10%: 99.4%
|
| 167 |
+
Within ±20%: 99.9%
|
| 168 |
+
Quality: EXCELLENT
|
| 169 |
+
|
| 170 |
+
KETTLE
|
| 171 |
+
----------------------------------------
|
| 172 |
+
Samples: 887
|
| 173 |
+
MAE: 0.12 kWh
|
| 174 |
+
RMSE: 0.21 kWh
|
| 175 |
+
R²: 0.975
|
| 176 |
+
MAPE: 2.4%
|
| 177 |
+
Within ±10%: 99.4%
|
| 178 |
+
Within ±20%: 100.0%
|
| 179 |
+
Quality: EXCELLENT
|
| 180 |
+
|
| 181 |
+
INDUCTION
|
| 182 |
+
----------------------------------------
|
| 183 |
+
Samples: 1316
|
| 184 |
+
MAE: 2.36 kWh
|
| 185 |
+
RMSE: 2.74 kWh
|
| 186 |
+
R²: 0.993
|
| 187 |
+
MAPE: 6.3%
|
| 188 |
+
Within ±10%: 78.8%
|
| 189 |
+
Within ±20%: 98.7%
|
| 190 |
+
Quality: EXCELLENT
|
| 191 |
+
|
| 192 |
+
RICE_COOKER
|
| 193 |
+
----------------------------------------
|
| 194 |
+
Samples: 1489
|
| 195 |
+
MAE: 0.16 kWh
|
| 196 |
+
RMSE: 0.28 kWh
|
| 197 |
+
R²: 0.956
|
| 198 |
+
MAPE: 1.4%
|
| 199 |
+
Within ±10%: 99.7%
|
| 200 |
+
Within ±20%: 99.9%
|
| 201 |
+
Quality: EXCELLENT
|
| 202 |
+
|
| 203 |
+
======================================================================
|
| 204 |
+
OVERALL STATISTICS
|
| 205 |
+
======================================================================
|
| 206 |
+
Total models tested: 18
|
| 207 |
+
Average R²: -186647085458293509479989248.000
|
| 208 |
+
Average MAE: 1.48 kWh
|
| 209 |
+
Models with R² > 0.90: 12
|
| 210 |
+
Models with R² > 0.80: 12
|
models/ceiling_fan_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fd4860162875f7fac4708c41c234401a7679a850995276000dfd09ccbff415cd
|
| 3 |
+
size 217137
|
models/ceiling_fan_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ff2c0ed889fea3f406e56a869165ac41a68c4659d2df3550ce79598d356a3dac
|
| 3 |
+
size 3321
|
models/cfl_bulb_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8f96ad1431ea48a4b0ab1e61dc3c6d00e6946db6fe5e123d902b949817335708
|
| 3 |
+
size 2027
|
models/cfl_lights_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e4298d505b655d60a032ace7b8a1d3b57d341c0ea63051e327865ec60956aec2
|
| 3 |
+
size 212544
|
models/cfl_lights_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f6a371cbdcf888efcda206c1841f8f06f0954fa5d54578979250e14e0882b234
|
| 3 |
+
size 3029
|
models/desktop_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dfd26dfbd9bedb3fb53d1e3bdb0c90d9f179d6765110d3a669e9cb831652b1a8
|
| 3 |
+
size 212544
|
models/desktop_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ddc526361206b562588153e9ee95a4e9aec59684caae7be4a4e10bf1dcb2b60
|
| 3 |
+
size 3005
|
models/food_processor_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4b5cc3be73c6d42199f25e0fdd3a7f17846bc2c9de534a4a716f4d96fb009e52
|
| 3 |
+
size 212544
|
models/food_processor_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e582eec026c2bd1a82a9123c2f20de4a0cdaa9f485ce9ae6608e5fa0701e30a9
|
| 3 |
+
size 3028
|
models/fridge_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0e370e9532f950ad1219c4f7682d23a2091e59e714c0bf9e3ec048931799d271
|
| 3 |
+
size 220203
|
models/fridge_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:988bd4b38eac27f99fd0261696b123e27ba9876431a1d626c1d0c321353843b9
|
| 3 |
+
size 3541
|
models/hair_dryer_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:308637e580639980c036d46ebd7fe81b21dafa85c46b57ffb4ada876f7ed4651
|
| 3 |
+
size 212544
|
models/hair_dryer_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0845ba446a818d00910059aedb56e9beda2ca578739ed410f2928c1781ca538a
|
| 3 |
+
size 3024
|
models/induction_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cc8467a0d30857e54903f5f9df053bff7163d6f1040e4c2cf0041d3676c38c65
|
| 3 |
+
size 212536
|
models/induction_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ebc4b4f737973879a3fd06c40d9d38cfefae7f209e9f44e3668dcd952fba64e
|
| 3 |
+
size 3027
|
models/iron_model.keras
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:95bd1617da5cdafd0cc55007f2b77c32a4096e75ed749c674f30152b5f6ae4c6
|
| 3 |
+
size 212536
|
models/iron_preprocessor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3fdcd2b1b71671cb1101eaf25ced98e69d21ae28a48144d3e40b9dac92e26784
|
| 3 |
+
size 3002
|