Jishnupg commited on
Commit
8a50d96
·
1 Parent(s): 28a626c

Initial upload of SmartWatt Backend API

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +140 -0
  2. .gitattributes +4 -34
  3. .gitignore +4 -0
  4. .python-version +1 -0
  5. Analysis_Tools/analyze_dataset_issues.py +93 -0
  6. Analysis_Tools/evaluate_all_models.py +298 -0
  7. Analysis_Tools/show_real_data.py +29 -0
  8. Analysis_Tools/test_auto_train_local.py +127 -0
  9. Analysis_Tools/test_clean_architecture.py +97 -0
  10. Analysis_Tools/verify_patterns.py +84 -0
  11. Dockerfile +35 -0
  12. LICENSE +21 -0
  13. Migration_Scripts/migrate_kwh_columns.py +158 -0
  14. Migration_Scripts/scheduled_self_learning.py +150 -0
  15. Procfile +1 -0
  16. README.md +104 -11
  17. Training_Scripts/future_train.py +1093 -0
  18. Training_Scripts/newdataset.py +1008 -0
  19. Training_Scripts/train(latest).py +644 -0
  20. Training_Scripts/train(old).py +348 -0
  21. Training_Scripts/train_demo.py +470 -0
  22. Training_Scripts/train_with_logging.py +164 -0
  23. anomaly_engine.py +39 -0
  24. kseb_tariff.py +191 -0
  25. learned_params.json +12 -0
  26. learning/__init__.py +0 -0
  27. learning/core.py +76 -0
  28. learning/memory.py +62 -0
  29. learning/pipeline.py +60 -0
  30. main.py +86 -0
  31. models/ac_model.keras +3 -0
  32. models/ac_preprocessor.pkl +3 -0
  33. models/all_models_evaluation_summary.txt +210 -0
  34. models/ceiling_fan_model.keras +3 -0
  35. models/ceiling_fan_preprocessor.pkl +3 -0
  36. models/cfl_bulb_preprocessor.pkl +3 -0
  37. models/cfl_lights_model.keras +3 -0
  38. models/cfl_lights_preprocessor.pkl +3 -0
  39. models/desktop_model.keras +3 -0
  40. models/desktop_preprocessor.pkl +3 -0
  41. models/food_processor_model.keras +3 -0
  42. models/food_processor_preprocessor.pkl +3 -0
  43. models/fridge_model.keras +3 -0
  44. models/fridge_preprocessor.pkl +3 -0
  45. models/hair_dryer_model.keras +3 -0
  46. models/hair_dryer_preprocessor.pkl +3 -0
  47. models/induction_model.keras +3 -0
  48. models/induction_preprocessor.pkl +3 -0
  49. models/iron_model.keras +3 -0
  50. 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
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
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
- *.pt filter=lfs diff=lfs merge=lfs -text
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
- title: Smartwatt Backend
3
- emoji: 🐠
4
- colorFrom: pink
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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