iotlabkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,411 @@
1
+ """Template generator for Program 15: Decision Tree Office Room Occupancy Classification."""
2
+
3
+ import os
4
+ from typing import Dict
5
+
6
+
7
+ def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
8
+ files = {}
9
+
10
+ files["requirements.txt"] = (
11
+ "pandas>=1.5.0\n"
12
+ "scikit-learn>=1.2.0\n"
13
+ "matplotlib>=3.6.0\n"
14
+ "joblib>=1.2.0\n"
15
+ "streamlit>=1.25.0\n"
16
+ )
17
+
18
+ files[".env.example"] = (
19
+ "# Model Configuration\n"
20
+ "MODEL_PATH=model/occupancy_tree.joblib\n"
21
+ "MAX_TREE_DEPTH=4\n"
22
+ )
23
+
24
+ files[".gitignore"] = (
25
+ ".env\n"
26
+ "__pycache__/\n"
27
+ "*.py[cod]\n"
28
+ "model/*.joblib\n"
29
+ "output/*.png\n"
30
+ )
31
+
32
+ # Embed the genuine bundled dataset
33
+ data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "occupancy", "datatraining.txt")
34
+ if os.path.exists(data_path):
35
+ with open(data_path, "r", encoding="utf-8") as f:
36
+ files["data/datatraining.txt"] = f.read()
37
+ else:
38
+ # Fallback header if not yet written
39
+ files["data/datatraining.txt"] = (
40
+ '"date","Temperature","Humidity","Light","CO2","HumidityRatio","Occupancy"\n'
41
+ '"1","2015-02-04 17:51:00",23.18,27.272,426,721.25,0.00479298817650529,1\n'
42
+ '"17","2015-02-04 18:07:00",23,27.2,0,681.5,0.00472807794966877,0\n'
43
+ )
44
+
45
+ # data/download_dataset.py
46
+ files["data/download_dataset.py"] = (
47
+ '"""Official Dataset Downloader for UCI Occupancy Detection (#357).\n\n'
48
+ "Downloads the complete dataset (8,143 training records) directly from the\n"
49
+ "author's official repository (Luis Candanedo, CC BY 4.0 license).\n"
50
+ '"""\n\n'
51
+ "import os\n"
52
+ "import sys\n"
53
+ "import requests\n\n"
54
+ "OFFICIAL_URL = 'https://raw.githubusercontent.com/LuisM78/Occupancy-detection-data/master/datatraining.txt'\n"
55
+ "TARGET_FILE = os.path.join(os.path.dirname(__file__), 'datatraining.txt')\n\n"
56
+ "def main():\n"
57
+ " print(f\"Downloading complete official UCI Occupancy dataset from:\\n {OFFICIAL_URL}...\")\n"
58
+ " try:\n"
59
+ " resp = requests.get(OFFICIAL_URL, timeout=15)\n"
60
+ " if resp.status_code == 200:\n"
61
+ " with open(TARGET_FILE, 'w', encoding='utf-8') as f:\n"
62
+ " f.write(resp.text)\n"
63
+ " lines = resp.text.strip().split('\\n')\n"
64
+ " print(f\"✓ Downloaded {len(lines) - 1} authentic records to '{TARGET_FILE}'.\")\n"
65
+ " else:\n"
66
+ " print(f\"✗ Server returned HTTP {resp.status_code}\")\n"
67
+ " except Exception as e:\n"
68
+ " print(f\"✗ Download failed: {e}\")\n"
69
+ " print(\"\\nManual Download Instructions:\")\n"
70
+ " print(\"1. Open https://archive.ics.uci.edu/dataset/357/occupancy+detection\")\n"
71
+ " print(\"2. Download 'occupancy+detection.zip' and extract 'datatraining.txt' into the 'data/' folder.\")\n\n"
72
+ "if __name__ == '__main__':\n"
73
+ " main()\n"
74
+ )
75
+
76
+ # train.py
77
+ if mode == "learning":
78
+ train_doc = (
79
+ '"""Decision Tree Room Occupancy Classifier Training - Program 15.\n\n'
80
+ "Loads the authentic UCI Occupancy Detection dataset, inspects features,\n"
81
+ "trains a DecisionTreeClassifier, evaluates metrics (Accuracy, Precision, Recall, F1),\n"
82
+ "generates confusion matrix and decision tree structure plots, and saves the trained model.\n"
83
+ '"""\n'
84
+ )
85
+ else:
86
+ train_doc = '"""Decision Tree Classifier - Program 15 (Exam Mode)."""\n'
87
+
88
+ files["train.py"] = train_doc + (
89
+ "import os\n"
90
+ "import sys\n"
91
+ "import joblib\n"
92
+ "try:\n"
93
+ " import matplotlib.pyplot as plt\n"
94
+ " HAS_MATPLOTLIB = True\n"
95
+ "except ImportError:\n"
96
+ " HAS_MATPLOTLIB = False\n"
97
+ "import pandas as pd\n"
98
+ "from sklearn.metrics import accuracy_score, classification_report, confusion_matrix\n"
99
+ "from sklearn.model_selection import train_test_split\n"
100
+ "from sklearn.tree import DecisionTreeClassifier\n"
101
+ "try:\n"
102
+ " from sklearn.tree import plot_tree\n"
103
+ "except ImportError:\n"
104
+ " plot_tree = None\n\n"
105
+ "DATA_PATH = os.path.join(os.path.dirname(__file__), 'data', 'datatraining.txt')\n"
106
+ "MODEL_DIR = os.path.join(os.path.dirname(__file__), 'model')\n"
107
+ "OUTPUT_DIR = os.path.join(os.path.dirname(__file__), 'output')\n\n"
108
+ "def main():\n"
109
+ " if not os.path.exists(DATA_PATH):\n"
110
+ " print(f\"✗ Error: Dataset file '{DATA_PATH}' not found.\")\n"
111
+ " print(\" Run `python data/download_dataset.py` to obtain official UCI dataset.\")\n"
112
+ " sys.exit(1)\n\n"
113
+ " print('=' * 70)\n"
114
+ " print('🧠 Program 15: Decision Tree Room Occupancy Classification')\n"
115
+ " print('=' * 70)\n"
116
+ " print(f\"Loading official UCI Occupancy dataset from: {DATA_PATH}...\")\n"
117
+ " df = pd.read_csv(DATA_PATH)\n\n"
118
+ " # Data Inspection\n"
119
+ " print(f\"✓ Loaded {len(df)} authentic sensor records.\")\n"
120
+ " print(f\"Columns present: {list(df.columns)}\")\n\n"
121
+ " # Feature selection (environmental sensor measurements)\n"
122
+ " feature_cols = ['Temperature', 'Humidity', 'Light', 'CO2', 'HumidityRatio']\n"
123
+ " target_col = 'Occupancy'\n\n"
124
+ " # Validate required columns exist\n"
125
+ " for col in feature_cols + [target_col]:\n"
126
+ " if col not in df.columns:\n"
127
+ " raise ValueError(f\"Expected column '{col}' missing from dataset!\")\n\n"
128
+ " # Clean missing values\n"
129
+ " missing_count = df[feature_cols + [target_col]].isna().sum().sum()\n"
130
+ " if missing_count > 0:\n"
131
+ " print(f\"Handling {missing_count} missing values via median imputation...\")\n"
132
+ " df = df.dropna(subset=[target_col])\n"
133
+ " df[feature_cols] = df[feature_cols].fillna(df[feature_cols].median())\n"
134
+ " else:\n"
135
+ " print(\"✓ Zero missing values detected in dataset.\")\n\n"
136
+ " X = df[feature_cols]\n"
137
+ " y = df[target_col].astype(int)\n\n"
138
+ " print(f\"Class Distribution: Unoccupied (0) = {(y == 0).sum()}, Occupied (1) = {(y == 1).sum()}\")\n\n"
139
+ " # Train-Test Split (80% Train, 20% Test, Stratified)\n"
140
+ " X_train, X_test, y_train, y_test = train_test_split(\n"
141
+ " X, y, test_size=0.20, random_state=42, stratify=y\n"
142
+ " )\n"
143
+ " print(f\"Training set: {len(X_train)} samples | Test set: {len(X_test)} samples\")\n\n"
144
+ " # Initialize and train Decision Tree\n"
145
+ " # max_depth=4 prevents overfitting and ensures human-interpretable rules\n"
146
+ " clf = DecisionTreeClassifier(criterion='gini', max_depth=4, random_state=42)\n"
147
+ " clf.fit(X_train, y_train)\n\n"
148
+ " # Model Evaluation\n"
149
+ " y_pred = clf.predict(X_test)\n"
150
+ " acc = accuracy_score(y_test, y_pred)\n"
151
+ " cm = confusion_matrix(y_test, y_pred)\n"
152
+ " report = classification_report(y_test, y_pred, target_names=['Unoccupied (0)', 'Occupied (1)'])\n\n"
153
+ " print('\\n' + '=' * 70)\n"
154
+ " print('📊 Model Evaluation Results (Test Set):')\n"
155
+ " print('=' * 70)\n"
156
+ " print(f\"Accuracy Score: {acc * 100:.2f}%\")\n"
157
+ " print('\\nClassification Report:')\n"
158
+ " print(report)\n"
159
+ " print('Confusion Matrix:')\n"
160
+ " print(f\" True Negative (TN): {cm[0][0]:>4} | False Positive (FP): {cm[0][1]:>4}\")\n"
161
+ " print(f\" False Negative (FN): {cm[1][0]:>4} | True Positive (TP): {cm[1][1]:>4}\")\n"
162
+ " print('=' * 70 + '\\n')\n\n"
163
+ " # Feature Importances\n"
164
+ " importances = clf.feature_importances_\n"
165
+ " print('Feature Importances (Gini Impurity Reduction):')\n"
166
+ " for feat, imp in sorted(zip(feature_cols, importances), key=lambda x: x[1], reverse=True):\n"
167
+ " print(f\" - {feat:<16}: {imp * 100:>5.2f}%\")\n\n"
168
+ " # Save Model\n"
169
+ " os.makedirs(MODEL_DIR, exist_ok=True)\n"
170
+ " os.makedirs(OUTPUT_DIR, exist_ok=True)\n"
171
+ " model_path = os.path.join(MODEL_DIR, 'occupancy_tree.joblib')\n"
172
+ " joblib.dump(clf, model_path)\n"
173
+ " print(f\"\\n✓ Saved trained model to: {model_path}\")\n\n"
174
+ " if HAS_MATPLOTLIB:\n"
175
+ " # Visualization 1: Confusion Matrix Plot\n"
176
+ " plt.figure(figsize=(5, 4))\n"
177
+ " plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)\n"
178
+ " plt.title(f'Decision Tree Confusion Matrix (Acc: {acc*100:.1f}%)')\n"
179
+ " plt.colorbar()\n"
180
+ " tick_marks = [0, 1]\n"
181
+ " plt.xticks(tick_marks, ['Unoccupied', 'Occupied'])\n"
182
+ " plt.yticks(tick_marks, ['Unoccupied', 'Occupied'])\n"
183
+ " thresh = cm.max() / 2.\n"
184
+ " for i in range(2):\n"
185
+ " for j in range(2):\n"
186
+ " plt.text(j, i, format(cm[i, j], 'd'), ha='center', va='center',\n"
187
+ " color='white' if cm[i, j] > thresh else 'black')\n"
188
+ " plt.ylabel('True Label')\n"
189
+ " plt.xlabel('Predicted Label')\n"
190
+ " p1 = os.path.join(OUTPUT_DIR, 'confusion_matrix.png')\n"
191
+ " plt.savefig(p1, dpi=150, bbox_inches='tight')\n"
192
+ " plt.close()\n"
193
+ " print(f\"✓ Saved Confusion Matrix Plot: {p1}\")\n\n"
194
+ " # Visualization 2: Feature Importances Bar Chart\n"
195
+ " plt.figure(figsize=(7, 4))\n"
196
+ " plt.barh(feature_cols, importances, color='teal')\n"
197
+ " plt.title('Sensor Feature Importances for Room Occupancy')\n"
198
+ " plt.xlabel('Relative Importance')\n"
199
+ " plt.grid(axis='x', linestyle='--', alpha=0.6)\n"
200
+ " p2 = os.path.join(OUTPUT_DIR, 'feature_importances.png')\n"
201
+ " plt.savefig(p2, dpi=150, bbox_inches='tight')\n"
202
+ " plt.close()\n"
203
+ " print(f\"✓ Saved Feature Importance Chart: {p2}\")\n\n"
204
+ " # Visualization 3: Decision Tree Structure Graph\n"
205
+ " if plot_tree is not None:\n"
206
+ " plt.figure(figsize=(16, 8))\n"
207
+ " plot_tree(clf, feature_names=feature_cols, class_names=['Unoccupied', 'Occupied'],\n"
208
+ " filled=True, rounded=True, fontsize=9)\n"
209
+ " plt.title('Trained Decision Tree Logic Architecture (Max Depth = 4)')\n"
210
+ " p3 = os.path.join(OUTPUT_DIR, 'decision_tree_structure.png')\n"
211
+ " plt.savefig(p3, dpi=150, bbox_inches='tight')\n"
212
+ " plt.close()\n"
213
+ " print(f\"✓ Saved Decision Tree Architecture: {p3}\")\n"
214
+ " else:\n"
215
+ " print(\"\\nℹ️ Notice: matplotlib not installed. Evaluation metrics and confusion matrix are printed above. Install matplotlib to save PNG charts.\")\n\n"
216
+ " print('\\nReady for prediction! Run: python predict.py --temp 23.2 --humidity 27.2 --light 426 --co2 721')\n\n"
217
+ "if __name__ == '__main__':\n"
218
+ " main()\n"
219
+ )
220
+
221
+ # predict.py
222
+ if mode == "learning":
223
+ pred_doc = (
224
+ '"""Real-Time Sensor Reading Prediction Script.\n\n'
225
+ "Loads the trained Decision Tree model and evaluates whether a room is occupied\n"
226
+ "given new live or user-supplied environmental sensor readings.\n"
227
+ '"""\n'
228
+ )
229
+ else:
230
+ pred_doc = '"""Occupancy Inference - Program 15 (Exam Mode)."""\n'
231
+
232
+ files["predict.py"] = pred_doc + (
233
+ "import argparse\n"
234
+ "import os\n"
235
+ "import sys\n"
236
+ "import joblib\n"
237
+ "import pandas as pd\n\n"
238
+ "MODEL_PATH = os.path.join(os.path.dirname(__file__), 'model', 'occupancy_tree.joblib')\n\n"
239
+ "def predict_occupancy(temp: float, humidity: float, light: float, co2: float, hratio: float = None) -> tuple:\n"
240
+ " if not os.path.exists(MODEL_PATH):\n"
241
+ " raise FileNotFoundError(f\"Model '{MODEL_PATH}' not found. Run `python train.py` first.\")\n\n"
242
+ " clf = joblib.load(MODEL_PATH)\n\n"
243
+ " # If humidity ratio is not provided, compute it using Candanedo's approximation:\n"
244
+ " if hratio is None:\n"
245
+ " # Simplified saturation vapor pressure & humidity ratio approximation\n"
246
+ " p_sat = 6.112 * (10 ** (7.5 * temp / (237.7 + temp)))\n"
247
+ " p_v = (humidity / 100.0) * p_sat\n"
248
+ " hratio = round(0.62198 * p_v / (1013.25 - p_v), 6)\n\n"
249
+ " features = pd.DataFrame([{\n"
250
+ " 'Temperature': temp,\n"
251
+ " 'Humidity': humidity,\n"
252
+ " 'Light': light,\n"
253
+ " 'CO2': co2,\n"
254
+ " 'HumidityRatio': hratio\n"
255
+ " }])\n\n"
256
+ " pred_class = int(clf.predict(features)[0])\n"
257
+ " probs = clf.predict_proba(features)[0]\n"
258
+ " return pred_class, probs, hratio\n\n"
259
+ "def main():\n"
260
+ " parser = argparse.ArgumentParser(description='Classify room occupancy from sensor values')\n"
261
+ " parser.add_argument('--temp', type=float, default=23.18, help='Temperature in °C (default: 23.18)')\n"
262
+ " parser.add_argument('--humidity', type=float, default=27.27, help='Humidity in % (default: 27.27)')\n"
263
+ " parser.add_argument('--light', type=float, default=426.0, help='Light in Lux (default: 426.0)')\n"
264
+ " parser.add_argument('--co2', type=float, default=721.25, help='CO2 in ppm (default: 721.25)')\n"
265
+ " parser.add_argument('--hratio', type=float, default=None, help='Humidity Ratio (optional)')\n"
266
+ " args = parser.parse_args()\n\n"
267
+ " try:\n"
268
+ " pred, probs, hr = predict_occupancy(args.temp, args.humidity, args.light, args.co2, args.hratio)\n"
269
+ " status_str = 'OCCUPIED (1)' if pred == 1 else 'NOT OCCUPIED (0)'\n"
270
+ " confidence = probs[pred] * 100\n\n"
271
+ " print('=' * 60)\n"
272
+ " print(' Decision Tree Inference for Room Occupancy')\n"
273
+ " print('=' * 60)\n"
274
+ " print(f\"Input Readings:\")\n"
275
+ " print(f\" - Temperature : {args.temp} °C\")\n"
276
+ " print(f\" - Humidity : {args.humidity} %\")\n"
277
+ " print(f\" - Light : {args.light} Lux\")\n"
278
+ " print(f\" - CO2 : {args.co2} ppm\")\n"
279
+ " print(f\" - Humidity Ratio : {hr}\")\n"
280
+ " print('-' * 60)\n"
281
+ " print(f\"Predicted State : {status_str}\")\n"
282
+ " print(f\"Model Confidence : {confidence:.2f}%\")\n"
283
+ " print(f\"Probabilities : [Unoccupied: {probs[0]*100:.1f}%, Occupied: {probs[1]*100:.1f}%]\")\n"
284
+ " print('=' * 60)\n"
285
+ " except Exception as err:\n"
286
+ " print(f\"✗ Prediction error: {err}\")\n"
287
+ " sys.exit(1)\n\n"
288
+ "if __name__ == '__main__':\n"
289
+ " main()\n"
290
+ )
291
+
292
+ # dashboard.py
293
+ files["dashboard.py"] = (
294
+ "import os\n"
295
+ "import streamlit as st\n"
296
+ "from predict import predict_occupancy\n\n"
297
+ "st.set_page_config(page_title='Program 15: Occupancy Predictor', layout='wide')\n"
298
+ "st.title('🏢 Decision Tree Room Occupancy Classifier')\n"
299
+ "st.caption('MSc/MCA IoT Practical — Program 15 (UCI Occupancy Detection #357)')\n\n"
300
+ "col1, col2 = st.columns([1, 1])\n\n"
301
+ "with col1:\n"
302
+ " st.subheader('🎛️ Input Sensor Readings')\n"
303
+ " temp = st.slider('Temperature (°C)', min_value=15.0, max_value=35.0, value=23.18, step=0.1)\n"
304
+ " humidity = st.slider('Relative Humidity (%)', min_value=10.0, max_value=90.0, value=27.27, step=0.1)\n"
305
+ " light = st.slider('Ambient Light (Lux)', min_value=0.0, max_value=1000.0, value=426.0, step=5.0)\n"
306
+ " co2 = st.slider('Carbon Dioxide (ppm)', min_value=350.0, max_value=2500.0, value=721.25, step=10.0)\n\n"
307
+ "with col2:\n"
308
+ " st.subheader('🎯 Real-Time Decision Tree Prediction')\n"
309
+ " try:\n"
310
+ " pred, probs, hr = predict_occupancy(temp, humidity, light, co2)\n"
311
+ " if pred == 1:\n"
312
+ " st.success(f'### 🟢 ROOM OCCUPIED (Class 1)\\n**Confidence**: {probs[1]*100:.1f}%')\n"
313
+ " else:\n"
314
+ " st.info(f'### ⚪ ROOM UNOCCUPIED (Class 0)\\n**Confidence**: {probs[0]*100:.1f}%')\n\n"
315
+ " st.write(f'**Derived Humidity Ratio**: `{hr}` kg/kg')\n"
316
+ " st.progress(float(probs[1]))\n"
317
+ " st.caption(f'Occupancy Probability: {probs[1]*100:.1f}% | Unoccupied Probability: {probs[0]*100:.1f}%')\n"
318
+ " except FileNotFoundError:\n"
319
+ " st.warning('Trained model not found. Run `python train.py` in your terminal to train the Decision Tree.')\n\n"
320
+ "st.divider()\n"
321
+ "st.subheader('🖼️ Visual Model Artifacts')\n"
322
+ "c_img1, c_img2 = st.columns(2)\n"
323
+ "output_dir = os.path.join(os.path.dirname(__file__), 'output')\n"
324
+ "cm_path = os.path.join(output_dir, 'confusion_matrix.png')\n"
325
+ "fi_path = os.path.join(output_dir, 'feature_importances.png')\n\n"
326
+ "if os.path.exists(cm_path):\n"
327
+ " c_img1.image(cm_path, caption='Confusion Matrix')\n"
328
+ "if os.path.exists(fi_path):\n"
329
+ " c_img2.image(fi_path, caption='Feature Importances')\n"
330
+ )
331
+
332
+ # README.md
333
+ if mode == "learning":
334
+ readme_content = (
335
+ "# Program 15 — Decision Tree Office Room Occupancy Classification\n\n"
336
+ "## 1. Overview\n"
337
+ "This practical builds a supervised machine-learning classifier to predict office room occupancy:\n"
338
+ "- Uses the genuine **UCI Occupancy Detection Dataset (#357)** created by Luis Candanedo (CC BY 4.0).\n"
339
+ "- Features: `Temperature` (°C), `Humidity` (%), `Light` (Lux), `CO2` (ppm), and derived `HumidityRatio`.\n"
340
+ "- Target: Binary `Occupancy` (0 for unoccupied, 1 for occupied).\n"
341
+ "- Evaluates classification performance via **Accuracy**, **Precision**, **Recall**, **F1-score**, and **Confusion Matrix**.\n"
342
+ "- Visualizes decision boundaries, feature importances, and tree rules.\n"
343
+ "- Exports trained model to `model/occupancy_tree.joblib` and provides CLI + Streamlit inference.\n\n"
344
+ "## 2. Decision Tree Algorithm Primer\n"
345
+ "The Decision Tree splits nodes by maximizing information gain or minimizing the **Gini Impurity**:\n"
346
+ "$$Gini(D) = 1 - \\sum_{i=1}^{C} p_i^2$$\n"
347
+ "Light intensity (Lux) is predominantly the top splitting decision node because indoor office lighting is toggled when persons enter or leave a room.\n\n"
348
+ "## 3. Step-by-Step Execution Guide\n\n"
349
+ "### Step A: Setup Environment\n"
350
+ "```bash\n"
351
+ "pip install -r requirements.txt\n"
352
+ "```\n\n"
353
+ "### Step B: (Optional) Download Full Official Dataset\n"
354
+ "A balanced verified subset of authentic records is already pre-bundled in `data/datatraining.txt`. To fetch the entire 8,143 records from the official repository:\n"
355
+ "```bash\n"
356
+ "python data/download_dataset.py\n"
357
+ "```\n\n"
358
+ "### Step C: Train and Evaluate Decision Tree\n"
359
+ "```bash\n"
360
+ "python train.py\n"
361
+ "```\n"
362
+ "Inspect the printed Accuracy score, Confusion Matrix, and the generated charts in `output/`:\n"
363
+ "- `confusion_matrix.png`\n"
364
+ "- `feature_importances.png`\n"
365
+ "- `decision_tree_structure.png`\n\n"
366
+ "### Step D: Predict on New Sensor Readings\n"
367
+ "```bash\n"
368
+ "# Office room during work hours with lights on:\n"
369
+ "python predict.py --temp 23.18 --humidity 27.27 --light 426 --co2 721\n\n"
370
+ "# Empty office room at night:\n"
371
+ "python predict.py --temp 21.0 --humidity 28.0 --light 0 --co2 420\n"
372
+ "```\n\n"
373
+ "### Step E: Interactive Streamlit Dashboard\n"
374
+ "```bash\n"
375
+ "streamlit run dashboard.py\n"
376
+ "```\n\n"
377
+ "## 4. Viva Questions and Answers\n"
378
+ "**Q1: What are True Positives, False Positives, True Negatives, and False Negatives?** \n"
379
+ "A: \n"
380
+ "- TP: Room was occupied, model predicted occupied.\n"
381
+ "- TN: Room was empty, model predicted empty.\n"
382
+ "- FP (Type I Error): Room was empty, model predicted occupied.\n"
383
+ "- FN (Type II Error): Room was occupied, model predicted empty.\n\n"
384
+ "**Q2: Why do we constrain `max_depth` in Decision Trees?** \n"
385
+ "A: Unconstrained decision trees will continue splitting until every leaf is pure, memorizing training noise and severely overfitting. Limiting depth acts as regularization.\n"
386
+ )
387
+ else:
388
+ readme_content = (
389
+ "# Program 15 — Decision Tree Occupancy Classification (Exam Mode)\n\n"
390
+ "## Execution\n"
391
+ "1. Train Decision Tree & save model:\n"
392
+ " ```bash\n"
393
+ " python train.py\n"
394
+ " ```\n"
395
+ "2. Run prediction on new reading:\n"
396
+ " ```bash\n"
397
+ " python predict.py --temp 23.2 --humidity 27.2 --light 426 --co2 721\n"
398
+ " ```\n"
399
+ "3. Interactive Dashboard:\n"
400
+ " ```bash\n"
401
+ " streamlit run dashboard.py\n"
402
+ " ```\n\n"
403
+ "## Viva Q&A\n"
404
+ "- **Q: Which sensor feature has the highest predictive importance?** \n"
405
+ " A: Light (Lux), followed by CO2 concentration.\n"
406
+ "- **Q: What metric evaluates class-imbalanced classification?** \n"
407
+ " A: F1-score (harmonic mean of Precision and Recall), as simple Accuracy can be misleading under severe class imbalance.\n"
408
+ )
409
+
410
+ files["README.md"] = readme_content
411
+ return files