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,422 @@
1
+ """Template generator for Program 17: Random Forest Multi-Class Air Quality 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
+ "# Random Forest Model Settings\n"
20
+ "MODEL_PATH=model/air_quality_rf.joblib\n"
21
+ "N_ESTIMATORS=100\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 genuine balanced dataset
33
+ data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "india_air", "city_day_sample.csv")
34
+ if os.path.exists(data_path):
35
+ with open(data_path, "r", encoding="utf-8") as f:
36
+ files["data/city_day_sample.csv"] = f.read()
37
+
38
+ # data/download_dataset.py
39
+ files["data/download_dataset.py"] = (
40
+ '"""Dataset Downloader for Kaggle Indian Air Quality (Rohan Rao, CC0 Public Domain).\n\n'
41
+ "Supports downloading via authenticated Kaggle CLI or mirror URL without hardcoding credentials.\n"
42
+ '"""\n\n'
43
+ "import os\n"
44
+ "import sys\n"
45
+ "import requests\n\n"
46
+ "MIRROR_URL = 'https://raw.githubusercontent.com/vikramforsk2019/Streamlit/master/city_day.csv'\n"
47
+ "TARGET_FILE = os.path.join(os.path.dirname(__file__), 'city_day_sample.csv')\n\n"
48
+ "def main():\n"
49
+ " print('Kaggle Air Quality Data in India (city_day.csv)')\n"
50
+ " print('Dataset URL: https://www.kaggle.com/datasets/rohanrao/air-quality-data-in-india')\n"
51
+ " print('-' * 60)\n"
52
+ " print('Options to obtain full dataset:')\n"
53
+ " print(' Option 1: Using Kaggle CLI (if configured):')\n"
54
+ " print(' kaggle datasets download -d rohanrao/air-quality-data-in-india -f city_day.csv -p data/ --unzip')\n"
55
+ " print('\\n Option 2: Direct public mirror download...')\n\n"
56
+ " try:\n"
57
+ " print(f\"Fetching mirror from {MIRROR_URL}...\")\n"
58
+ " resp = requests.get(MIRROR_URL, timeout=25)\n"
59
+ " if resp.status_code == 200:\n"
60
+ " with open(TARGET_FILE, 'w', encoding='utf-8') as f:\n"
61
+ " f.write(resp.text)\n"
62
+ " lines = resp.text.strip().split('\\n')\n"
63
+ " print(f\"✓ Downloaded full dataset ({len(lines) - 1} rows) to '{TARGET_FILE}'.\")\n"
64
+ " else:\n"
65
+ " print(f\"✗ Server returned HTTP {resp.status_code}\")\n"
66
+ " except Exception as e:\n"
67
+ " print(f\"✗ Mirror download failed: {e}\")\n"
68
+ " print(\"\\nPlease download 'city_day.csv' manually from Kaggle and place it in the 'data/' directory.\")\n\n"
69
+ "if __name__ == '__main__':\n"
70
+ " main()\n"
71
+ )
72
+
73
+ # train.py
74
+ if mode == "learning":
75
+ train_doc = (
76
+ '"""Random Forest Air Quality Classifier Training - Program 17.\n\n'
77
+ "Trains an ensemble Random Forest model on Indian Air Quality data (`city_day.csv`).\n"
78
+ "Predicts the official CPCB Air Quality Index category (Good, Satisfactory, Moderate, Poor, Very Poor, Severe)\n"
79
+ "from 6 criteria pollutants: PM2.5, PM10, NO2, CO, SO2, and O3.\n"
80
+ '"""\n'
81
+ )
82
+ else:
83
+ train_doc = '"""Random Forest Air Quality Classifier - Program 17 (Exam Mode)."""\n'
84
+
85
+ files["train.py"] = train_doc + (
86
+ "import os\n"
87
+ "import sys\n"
88
+ "import joblib\n"
89
+ "try:\n"
90
+ " import matplotlib.pyplot as plt\n"
91
+ " HAS_MATPLOTLIB = True\n"
92
+ "except ImportError:\n"
93
+ " HAS_MATPLOTLIB = False\n"
94
+ "import pandas as pd\n"
95
+ "from sklearn.ensemble import RandomForestClassifier\n"
96
+ "from sklearn.metrics import accuracy_score, classification_report, confusion_matrix\n"
97
+ "from sklearn.model_selection import train_test_split\n\n"
98
+ "DATA_PATH = os.path.join(os.path.dirname(__file__), 'data', 'city_day_sample.csv')\n"
99
+ "MODEL_DIR = os.path.join(os.path.dirname(__file__), 'model')\n"
100
+ "OUTPUT_DIR = os.path.join(os.path.dirname(__file__), 'output')\n\n"
101
+ "# Authoritative Central Pollution Control Board (CPCB) India National Air Quality Index Standard\n"
102
+ "CPCB_CLASSES = ['Good', 'Satisfactory', 'Moderate', 'Poor', 'Very Poor', 'Severe']\n"
103
+ "LABEL_TO_ID = {name: idx for idx, name in enumerate(CPCB_CLASSES)}\n"
104
+ "ID_TO_LABEL = {idx: name for idx, name in enumerate(CPCB_CLASSES)}\n\n"
105
+ "def derive_cpcb_bucket(aqi: float) -> str:\n"
106
+ " \"\"\"Derives official CPCB category from numerical AQI if AQI_Bucket is unpopulated.\"\"\"\n"
107
+ " if pd.isna(aqi):\n"
108
+ " return None\n"
109
+ " if aqi <= 50:\n"
110
+ " return 'Good'\n"
111
+ " elif aqi <= 100:\n"
112
+ " return 'Satisfactory'\n"
113
+ " elif aqi <= 200:\n"
114
+ " return 'Moderate'\n"
115
+ " elif aqi <= 300:\n"
116
+ " return 'Poor'\n"
117
+ " elif aqi <= 400:\n"
118
+ " return 'Very Poor'\n"
119
+ " else:\n"
120
+ " return 'Severe'\n\n"
121
+ "def main():\n"
122
+ " if not os.path.exists(DATA_PATH):\n"
123
+ " print(f\"✗ Error: Dataset '{DATA_PATH}' not found.\")\n"
124
+ " print(\" Run `python data/download_dataset.py` to obtain city_day.csv.\")\n"
125
+ " sys.exit(1)\n\n"
126
+ " print('=' * 75)\n"
127
+ " print('🌲 Program 17: Random Forest Air Quality Classification')\n"
128
+ " print('=' * 75)\n"
129
+ " print(f\"Loading dataset from: {DATA_PATH}...\")\n"
130
+ " df = pd.read_csv(DATA_PATH)\n"
131
+ " print(f\"✓ Loaded {len(df)} records. Inspecting schema...\")\n\n"
132
+ " features = ['PM2.5', 'PM10', 'NO2', 'CO', 'SO2', 'O3']\n"
133
+ " # Check features\n"
134
+ " for f in features:\n"
135
+ " if f not in df.columns:\n"
136
+ " raise ValueError(f\"Required pollutant column '{f}' missing from dataset!\")\n\n"
137
+ " # Verify or derive target label\n"
138
+ " if 'AQI_Bucket' in df.columns:\n"
139
+ " # Fill any missing AQI_Bucket from numerical AQI if available\n"
140
+ " missing_buckets = df['AQI_Bucket'].isna()\n"
141
+ " if missing_buckets.any() and 'AQI' in df.columns:\n"
142
+ " df.loc[missing_buckets, 'AQI_Bucket'] = df.loc[missing_buckets, 'AQI'].apply(derive_cpcb_bucket)\n"
143
+ " elif 'AQI' in df.columns:\n"
144
+ " print(\"Notice: 'AQI_Bucket' missing. Deriving from numerical AQI using official CPCB thresholds...\")\n"
145
+ " df['AQI_Bucket'] = df['AQI'].apply(derive_cpcb_bucket)\n"
146
+ " else:\n"
147
+ " raise ValueError(\"Neither 'AQI_Bucket' nor 'AQI' present in dataset!\")\n\n"
148
+ " # Drop rows without valid CPCB class\n"
149
+ " df = df[df['AQI_Bucket'].isin(CPCB_CLASSES)].copy()\n\n"
150
+ " # Convert features to numeric and impute missing values using median\n"
151
+ " for f in features:\n"
152
+ " df[f] = pd.to_numeric(df[f], errors='coerce')\n"
153
+ " median_val = df[f].median()\n"
154
+ " df[f] = df[f].fillna(median_val)\n\n"
155
+ " X = df[features]\n"
156
+ " y = df['AQI_Bucket'].map(LABEL_TO_ID)\n\n"
157
+ " print('\\nTarget Class Distribution:')\n"
158
+ " counts = df['AQI_Bucket'].value_counts()\n"
159
+ " for c in CPCB_CLASSES:\n"
160
+ " print(f\" - {c:<14}: {counts.get(c, 0):>4} records\")\n\n"
161
+ " # Stratified Train-Test Split (80% Train, 20% Test)\n"
162
+ " X_train, X_test, y_train, y_test = train_test_split(\n"
163
+ " X, y, test_size=0.20, random_state=42, stratify=y\n"
164
+ " )\n"
165
+ " print(f\"\\nTraining set: {len(X_train)} samples | Testing set: {len(X_test)} samples\")\n\n"
166
+ " # Train Random Forest Classifier\n"
167
+ " rf = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42, n_jobs=-1)\n"
168
+ " rf.fit(X_train, y_train)\n\n"
169
+ " # Evaluate on Test Set\n"
170
+ " y_pred = rf.predict(X_test)\n"
171
+ " acc = accuracy_score(y_test, y_pred)\n"
172
+ " cm = confusion_matrix(y_test, y_pred)\n"
173
+ " report = classification_report(y_test, y_pred, target_names=CPCB_CLASSES)\n\n"
174
+ " print('\\n' + '=' * 75)\n"
175
+ " print('📊 Random Forest Classification Results (Test Set):')\n"
176
+ " print('=' * 75)\n"
177
+ " print(f\"Overall Accuracy: {acc * 100:.2f}%\")\n"
178
+ " print('\\nClassification Report:')\n"
179
+ " print(report)\n"
180
+ " print('=' * 75)\n\n"
181
+ " # Feature Importances\n"
182
+ " importances = rf.feature_importances_\n"
183
+ " print('\\nPollutant Feature Importances (Gini Reduction):')\n"
184
+ " for feat, imp in sorted(zip(features, importances), key=lambda x: x[1], reverse=True):\n"
185
+ " print(f\" - {feat:<8}: {imp * 100:>5.2f}%\")\n\n"
186
+ " # Save Model Bundle\n"
187
+ " os.makedirs(MODEL_DIR, exist_ok=True)\n"
188
+ " os.makedirs(OUTPUT_DIR, exist_ok=True)\n"
189
+ " bundle = {\n"
190
+ " 'model': rf,\n"
191
+ " 'features': features,\n"
192
+ " 'classes': CPCB_CLASSES,\n"
193
+ " 'accuracy': acc\n"
194
+ " }\n"
195
+ " model_path = os.path.join(MODEL_DIR, 'air_quality_rf.joblib')\n"
196
+ " joblib.dump(bundle, model_path)\n"
197
+ " print(f\"\\n✓ Saved model bundle to: {model_path}\")\n\n"
198
+ " if HAS_MATPLOTLIB:\n"
199
+ " # Chart 1: Confusion Matrix Heatmap\n"
200
+ " plt.figure(figsize=(7, 6))\n"
201
+ " plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Greens)\n"
202
+ " plt.title(f'Random Forest Confusion Matrix (Accuracy: {acc*100:.1f}%)')\n"
203
+ " plt.colorbar()\n"
204
+ " ticks = range(len(CPCB_CLASSES))\n"
205
+ " plt.xticks(ticks, CPCB_CLASSES, rotation=45, ha='right')\n"
206
+ " plt.yticks(ticks, CPCB_CLASSES)\n"
207
+ " thresh = cm.max() / 2.\n"
208
+ " for i in range(len(CPCB_CLASSES)):\n"
209
+ " for j in range(len(CPCB_CLASSES)):\n"
210
+ " plt.text(j, i, format(cm[i, j], 'd'), ha='center', va='center',\n"
211
+ " color='white' if cm[i, j] > thresh else 'black')\n"
212
+ " plt.ylabel('True CPCB Bucket')\n"
213
+ " plt.xlabel('Predicted CPCB Bucket')\n"
214
+ " plt.tight_layout()\n"
215
+ " p1 = os.path.join(OUTPUT_DIR, 'confusion_matrix.png')\n"
216
+ " plt.savefig(p1, dpi=150, bbox_inches='tight')\n"
217
+ " plt.close()\n"
218
+ " print(f\"✓ Saved Confusion Matrix: {p1}\")\n\n"
219
+ " # Chart 2: Feature Importances Bar Chart\n"
220
+ " plt.figure(figsize=(7, 4))\n"
221
+ " plt.barh(features, importances, color='forestgreen')\n"
222
+ " plt.title('Air Pollutant Importance for AQI Classification')\n"
223
+ " plt.xlabel('Importance')\n"
224
+ " plt.grid(axis='x', linestyle='--', alpha=0.6)\n"
225
+ " p2 = os.path.join(OUTPUT_DIR, 'feature_importances.png')\n"
226
+ " plt.savefig(p2, dpi=150, bbox_inches='tight')\n"
227
+ " plt.close()\n"
228
+ " print(f\"✓ Saved Feature Importance Chart: {p2}\")\n"
229
+ " else:\n"
230
+ " print(\"\\nℹ️ Notice: matplotlib not installed. Evaluation metrics (Accuracy, F1, Confusion Matrix) printed above. Install matplotlib to save PNG charts.\")\n\n"
231
+ " print('\\nReady for inference! Test: python predict.py --pm25 85 --pm10 140 --no2 32 --co 1.2 --so2 15 --o3 45')\n\n"
232
+ "if __name__ == '__main__':\n"
233
+ " main()\n"
234
+ )
235
+
236
+ # predict.py
237
+ if mode == "learning":
238
+ pred_doc = (
239
+ '"""Air Quality Classification Inference - Program 17.\n\n'
240
+ "Loads the trained Random Forest model and classifies a new set of ambient pollutant\n"
241
+ "readings (PM2.5, PM10, NO2, CO, SO2, O3) into one of the 6 official CPCB AQI categories.\n"
242
+ '"""\n'
243
+ )
244
+ else:
245
+ pred_doc = '"""Air Quality Inference - Program 17 (Exam Mode)."""\n'
246
+
247
+ files["predict.py"] = pred_doc + (
248
+ "import argparse\n"
249
+ "import os\n"
250
+ "import sys\n"
251
+ "import joblib\n"
252
+ "import pandas as pd\n\n"
253
+ "MODEL_PATH = os.path.join(os.path.dirname(__file__), 'model', 'air_quality_rf.joblib')\n\n"
254
+ "def main():\n"
255
+ " parser = argparse.ArgumentParser(description='Classify air quality from pollutant readings')\n"
256
+ " parser.add_argument('--pm25', type=float, default=85.0, help='PM2.5 concentration in μg/m³')\n"
257
+ " parser.add_argument('--pm10', type=float, default=140.0, help='PM10 concentration in μg/m³')\n"
258
+ " parser.add_argument('--no2', type=float, default=32.0, help='NO2 concentration in μg/m³')\n"
259
+ " parser.add_argument('--co', type=float, default=1.2, help='CO concentration in mg/m³')\n"
260
+ " parser.add_argument('--so2', type=float, default=15.0, help='SO2 concentration in μg/m³')\n"
261
+ " parser.add_argument('--o3', type=float, default=45.0, help='O3 concentration in μg/m³')\n"
262
+ " args = parser.parse_args()\n\n"
263
+ " if not os.path.exists(MODEL_PATH):\n"
264
+ " print(f\"✗ Error: Model file '{MODEL_PATH}' not found.\")\n"
265
+ " print(\" Run `python train.py` first to train the Random Forest model.\")\n"
266
+ " sys.exit(1)\n\n"
267
+ " bundle = joblib.load(MODEL_PATH)\n"
268
+ " model = bundle['model']\n"
269
+ " features = bundle['features']\n"
270
+ " classes = bundle['classes']\n\n"
271
+ " input_row = pd.DataFrame([{\n"
272
+ " 'PM2.5': args.pm25,\n"
273
+ " 'PM10': args.pm10,\n"
274
+ " 'NO2': args.no2,\n"
275
+ " 'CO': args.co,\n"
276
+ " 'SO2': args.so2,\n"
277
+ " 'O3': args.o3\n"
278
+ " }])[features]\n\n"
279
+ " pred_idx = model.predict(input_row)[0]\n"
280
+ " probs = model.predict_proba(input_row)[0]\n"
281
+ " pred_category = classes[pred_idx]\n"
282
+ " confidence = probs[pred_idx] * 100\n\n"
283
+ " print('=' * 65)\n"
284
+ " print('🌲 Random Forest Air Quality Classification')\n"
285
+ " print('=' * 65)\n"
286
+ " print('Input Pollutant Concentrations:')\n"
287
+ " print(f\" - PM2.5 : {args.pm25:>6.1f} μg/m³\")\n"
288
+ " print(f\" - PM10 : {args.pm10:>6.1f} μg/m³\")\n"
289
+ " print(f\" - NO2 : {args.no2:>6.1f} μg/m³\")\n"
290
+ " print(f\" - CO : {args.co:>6.1f} mg/m³\")\n"
291
+ " print(f\" - SO2 : {args.so2:>6.1f} μg/m³\")\n"
292
+ " print(f\" - O3 : {args.o3:>6.1f} μg/m³\")\n"
293
+ " print('-' * 65)\n"
294
+ " print(f\"Predicted CPCB AQI Bucket : {pred_category.upper()}\")\n"
295
+ " print(f\"Classification Confidence : {confidence:.2f}%\")\n"
296
+ " print('\\nClass Probabilities:')\n"
297
+ " for c, p in zip(classes, probs):\n"
298
+ " print(f\" - {c:<14}: {p * 100:>5.1f}%\")\n"
299
+ " print('=' * 65)\n\n"
300
+ "if __name__ == '__main__':\n"
301
+ " main()\n"
302
+ )
303
+
304
+ # dashboard.py
305
+ files["dashboard.py"] = (
306
+ "import os\n"
307
+ "import joblib\n"
308
+ "import pandas as pd\n"
309
+ "import streamlit as st\n\n"
310
+ "st.set_page_config(page_title='Program 17: Air Quality Classifier', layout='wide')\n"
311
+ "st.title('🌲 Random Forest Air Quality Classification Dashboard')\n"
312
+ "st.caption('MSc/MCA IoT Practical — Program 17 (Indian CPCB AQI Standards)')\n\n"
313
+ "MODEL_PATH = os.path.join(os.path.dirname(__file__), 'model', 'air_quality_rf.joblib')\n\n"
314
+ "if not os.path.exists(MODEL_PATH):\n"
315
+ " st.warning('Trained Random Forest model not found. Run `python train.py` in your terminal.')\n"
316
+ "else:\n"
317
+ " bundle = joblib.load(MODEL_PATH)\n"
318
+ " model = bundle['model']\n"
319
+ " classes = bundle['classes']\n\n"
320
+ " col1, col2 = st.columns([1, 1])\n"
321
+ " with col1:\n"
322
+ " st.subheader('🎛️ Input Pollutant Levels')\n"
323
+ " pm25 = st.slider('PM2.5 (μg/m³)', 0.0, 500.0, 85.0, 1.0)\n"
324
+ " pm10 = st.slider('PM10 (μg/m³)', 0.0, 800.0, 140.0, 5.0)\n"
325
+ " no2 = st.slider('NO2 (μg/m³)', 0.0, 200.0, 32.0, 1.0)\n"
326
+ " co = st.slider('CO (mg/m³)', 0.0, 30.0, 1.2, 0.1)\n"
327
+ " so2 = st.slider('SO2 (μg/m³)', 0.0, 200.0, 15.0, 1.0)\n"
328
+ " o3 = st.slider('O3 (μg/m³)', 0.0, 300.0, 45.0, 1.0)\n\n"
329
+ " with col2:\n"
330
+ " st.subheader('🎯 Predicted Air Quality Category')\n"
331
+ " input_df = pd.DataFrame([{'PM2.5': pm25, 'PM10': pm10, 'NO2': no2, 'CO': co, 'SO2': so2, 'O3': o3}])\n"
332
+ " pred_idx = model.predict(input_df)[0]\n"
333
+ " probs = model.predict_proba(input_df)[0]\n"
334
+ " pred_label = classes[pred_idx]\n\n"
335
+ " color_map = {\n"
336
+ " 'Good': 'green', 'Satisfactory': 'lime',\n"
337
+ " 'Moderate': 'orange', 'Poor': 'red',\n"
338
+ " 'Very Poor': 'purple', 'Severe': 'maroon'\n"
339
+ " }\n"
340
+ " st.markdown(f\"### Status: **:{color_map.get(pred_label, 'blue')}[{pred_label.upper()}]**\")\n"
341
+ " st.caption(f\"Confidence: {probs[pred_idx]*100:.1f}%\")\n\n"
342
+ " prob_df = pd.DataFrame({'Category': classes, 'Probability': probs})\n"
343
+ " st.bar_chart(prob_df.set_index('Category'))\n\n"
344
+ " st.divider()\n"
345
+ " st.subheader('📊 Training Evaluation Artifacts')\n"
346
+ " c_img1, c_img2 = st.columns(2)\n"
347
+ " output_dir = os.path.join(os.path.dirname(__file__), 'output')\n"
348
+ " cm_p = os.path.join(output_dir, 'confusion_matrix.png')\n"
349
+ " fi_p = os.path.join(output_dir, 'feature_importances.png')\n"
350
+ " if os.path.exists(cm_p):\n"
351
+ " c_img1.image(cm_p, caption='Multi-Class Confusion Matrix')\n"
352
+ " if os.path.exists(fi_p):\n"
353
+ " c_img2.image(fi_p, caption='Pollutant Feature Importances')\n"
354
+ )
355
+
356
+ # README.md
357
+ if mode == "learning":
358
+ readme_content = (
359
+ "# Program 17 — Random Forest Multi-Class Air Quality Classification\n\n"
360
+ "## 1. Overview\n"
361
+ "This practical implements an ensemble Random Forest classifier on authentic Indian Air Quality records:\n"
362
+ "- Uses the genuine **Kaggle Air Quality Data in India (`city_day.csv`)** dataset compiled by Rohan Rao (CC0 Public Domain).\n"
363
+ "- Analyzes 6 criteria pollutants: `PM2.5`, `PM10`, `NO2`, `CO`, `SO2`, and `O3`.\n"
364
+ "- Predicts the 6 official **Central Pollution Control Board (CPCB)** AQI buckets:\n"
365
+ " - **Good** (0–50)\n"
366
+ " - **Satisfactory** (51–100)\n"
367
+ " - **Moderate** (101–200)\n"
368
+ " - **Poor** (201–300)\n"
369
+ " - **Very Poor** (301–400)\n"
370
+ " - **Severe** (401–500)\n"
371
+ "- Evaluates model using stratified multi-class Accuracy, Classification Report, and Confusion Matrix.\n"
372
+ "- Ranks feature importances showing which pollutants govern health bucket transitions.\n"
373
+ "- Saves trained model to `model/air_quality_rf.joblib` with CLI and Streamlit inference.\n\n"
374
+ "## 2. Random Forest Ensemble Primer\n"
375
+ "Random Forest constructs $B$ decision trees on bootstrap samples of the training data. For each split, a random subset of $m \\approx \\sqrt{p}$ features is considered. The ensemble prediction is determined by majority vote:\n"
376
+ "$$\\hat{C}_{\\text{RF}}(x) = \\text{majority vote}\\left\\{ \\hat{C}_b(x) \\right\\}_{b=1}^B$$\n"
377
+ "This bagging technique drastically reduces model variance while maintaining low bias.\n\n"
378
+ "## 3. Step-by-Step Execution Guide\n\n"
379
+ "### Step A: Setup Environment\n"
380
+ "```bash\n"
381
+ "pip install -r requirements.txt\n"
382
+ "```\n\n"
383
+ "### Step B: Train Model & Evaluate\n"
384
+ "```bash\n"
385
+ "python train.py\n"
386
+ "```\n"
387
+ "Inspect accuracy score, per-class F1-scores, and generated charts in `output/`:\n"
388
+ "- `confusion_matrix.png`\n"
389
+ "- `feature_importances.png`\n\n"
390
+ "### Step C: Predict Air Quality on New Pollutant Readings\n"
391
+ "```bash\n"
392
+ "# Moderate winter day:\n"
393
+ "python predict.py --pm25 85 --pm10 140 --no2 32 --co 1.2 --so2 15 --o3 45\n\n"
394
+ "# Severe pollution episode:\n"
395
+ "python predict.py --pm25 320 --pm10 480 --no2 110 --co 4.5 --so2 35 --o3 80\n"
396
+ "```\n\n"
397
+ "### Step D: Interactive Dashboard\n"
398
+ "```bash\n"
399
+ "streamlit run dashboard.py\n"
400
+ "```\n\n"
401
+ "## 4. Viva Questions and Answers\n"
402
+ "**Q1: What are the 6 CPCB AQI health categories in India?** \n"
403
+ "A: Good (0-50), Satisfactory (51-100), Moderate (101-200), Poor (201-300), Very Poor (301-400), and Severe (401-500).\n\n"
404
+ "**Q2: What is Bagging (Bootstrap Aggregation) in Random Forests?** \n"
405
+ "A: Each tree is trained on an independently drawn random sample of the training dataset with replacement (bootstrap sample), and random subsets of features are evaluated at each split to decorrelate individual trees.\n"
406
+ )
407
+ else:
408
+ readme_content = (
409
+ "# Program 17 — Random Forest Air Quality Classification (Exam Mode)\n\n"
410
+ "## Execution\n"
411
+ "1. Train Random Forest: `python train.py`\n"
412
+ "2. Predict AQI Category: `python predict.py --pm25 85 --pm10 140 --no2 32 --co 1.2 --so2 15 --o3 45`\n"
413
+ "3. Dashboard: `streamlit run dashboard.py`\n\n"
414
+ "## Viva Q&A\n"
415
+ "- **Q: Why use Random Forest over a single Decision Tree?** \n"
416
+ " A: Random Forest averages predictions across dozens of decorrelated trees, substantially mitigating variance and preventing overfitting.\n"
417
+ "- **Q: What is the primary pollutant driving high Indian AQI values?** \n"
418
+ " A: Fine particulate matter (PM2.5), which typically exhibits the highest feature importance in CPCB classification models.\n"
419
+ )
420
+
421
+ files["README.md"] = readme_content
422
+ return files
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iotlabkit contributors
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.