projectXect/soil-moisture-rainfall-prediction
0
1import gradio as gr2import pandas as pd3import numpy as np4import joblib5import matplotlib.pyplot as plt6import matplotlib7matplotlib.use('Agg')8import requests9import base6410 11# ── Load model files ──────────────────────────────────────────12model = joblib.load('rainfall_model.pkl')13scaler = joblib.load('scaler.pkl')14features = joblib.load('feature_cols.pkl')15thresholds = joblib.load('thresholds.pkl')16 17# ── ThingSpeak config ─────────────────────────────────────────18CHANNEL_ID = '3305053'19READ_API_KEY = 'EXLGQUDN53YGVIEW'20 21# ── Encode logo ───────────────────────────────────────────────22def get_logo_b64():23 try:24 with open('school_crest.png', 'rb') as f:25 return base64.b64encode(f.read()).decode()26 except:27 return None28 29LOGO_B64 = get_logo_b64()30LOGO_HTML = f'<img src="data:image/png;base64,{LOGO_B64}" style="height:80px;width:80px;object-fit:contain;border-radius:50%;border:2px solid #22c55e;" />' if LOGO_B64 else ''31 32# ── Core hybrid prediction ────────────────────────────────────33def predict_rainfall(soil_moisture, previous_readings=None):34 if previous_readings is None:35 start = soil_moisture * 0.7036 history = [37 start,38 start + (soil_moisture - start) * 0.2,39 start + (soil_moisture - start) * 0.4,40 start + (soil_moisture - start) * 0.6,41 start + (soil_moisture - start) * 0.8,42 soil_moisture43 ]44 else:45 history = list(previous_readings) + [soil_moisture]46 47 series = pd.Series(history)48 row = {f: 0.0 for f in features}49 50 row['soil_moisture'] = soil_moisture51 row['moisture_change'] = series.iloc[-1] - series.iloc[-2]52 row['moisture_rolling_mean3'] = series.iloc[-3:].mean()53 row['moisture_rolling_mean5'] = series.iloc[-5:].mean() if len(series) >= 5 else series.mean()54 row['moisture_rolling_std3'] = series.iloc[-3:].std()55 row['moisture_rolling_max5'] = series.iloc[-5:].max()56 row['moisture_rolling_min5'] = series.iloc[-5:].min()57 row['moisture_range5'] = row['moisture_rolling_max5'] - row['moisture_rolling_min5']58 row['moisture_squared'] = soil_moisture ** 259 row['moisture_log'] = np.log1p(max(soil_moisture, 0))60 row['moisture_above_mean'] = 1 if soil_moisture > thresholds['mean'] else 061 row['moisture_above_75pct'] = 1 if soil_moisture > thresholds['pct_75'] else 062 row['moisture_spike'] = 1 if row['moisture_change'] > 5 else 063 row['moisture_trend'] = 1 if series.iloc[-1] > series.iloc[-3:].mean() else 064 65 ml_prob = float(model.predict_proba(66 scaler.transform(pd.DataFrame([row])[features])67 )[0][1])68 69 if soil_moisture >= 90: rule_prob = 0.9570 elif soil_moisture >= 83: rule_prob = 0.8071 elif soil_moisture >= 75: rule_prob = 0.6572 elif soil_moisture >= 69: rule_prob = 0.5073 elif soil_moisture >= 50: rule_prob = 0.3074 elif soil_moisture >= 30: rule_prob = 0.1575 else: rule_prob = 0.0576 77 change = row['moisture_change']78 if change > 10: rule_prob = min(rule_prob + 0.20, 0.99)79 elif change > 5: rule_prob = min(rule_prob + 0.10, 0.99)80 elif change < -5: rule_prob = max(rule_prob - 0.10, 0.01)81 82 final_prob = (0.60 * rule_prob) + (0.40 * ml_prob)83 84 if final_prob >= 0.75: label = '🌧️ VERY LIKELY'85 elif final_prob >= 0.55: label = '🌧️ LIKELY'86 elif final_prob >= 0.40: label = '🌤️ POSSIBLE'87 elif final_prob >= 0.20: label = '☀️ UNLIKELY'88 else: label = '☀️ VERY UNLIKELY'89 90 if soil_moisture >= 83: reason = 'Soil heavily saturated — above 75th percentile'91 elif soil_moisture >= 69: reason = 'Soil moisture is above average'92 elif soil_moisture >= 50: reason = 'Soil moisture is below average'93 else: reason = 'Soil is very dry'94 95 if change > 5: reason += ' and rising rapidly'96 elif change > 0: reason += ' and rising'97 elif change < -5: reason += ' and falling rapidly'98 else: reason += ' and stable'99 100 return label, final_prob, ml_prob, rule_prob, reason, history, change101 102# ── Build chart ───────────────────────────────────────────────103def build_chart(history, soil_moisture, final_prob, ml_prob, rule_prob, title='Soil Moisture Trend'):104 fig, axes = plt.subplots(1, 2, figsize=(13, 4), facecolor='#0f172a')105 106 for ax in axes:107 ax.set_facecolor('#1e293b')108 ax.tick_params(colors='#94a3b8', labelsize=9)109 for spine in ax.spines.values():110 spine.set_edgecolor('#334155')111 112 # Left — moisture trend113 x = range(len(history))114 axes[0].plot(x, history, color='#22c55e', linewidth=2.5,115 marker='o', markersize=5, markerfacecolor='#4ade80')116 axes[0].fill_between(x, history, alpha=0.15, color='#22c55e')117 axes[0].axhline(y=thresholds['mean'], color='#60a5fa', linestyle='--',118 linewidth=1, label=f'Mean ({thresholds["mean"]:.0f}%)', alpha=0.8)119 axes[0].axhline(y=thresholds['pct_75'], color='#fb923c', linestyle='--',120 linewidth=1, label=f'75th pct ({thresholds["pct_75"]:.0f}%)', alpha=0.8)121 axes[0].axhline(y=thresholds['pct_90'], color='#f87171', linestyle='--',122 linewidth=1, label=f'90th pct ({thresholds["pct_90"]:.0f}%)', alpha=0.8)123 axes[0].set_title(title, color='#f1f5f9', fontsize=11, pad=10)124 axes[0].set_ylabel('Moisture (%)', color='#94a3b8', fontsize=9)125 axes[0].set_xlabel('Reading #', color='#94a3b8', fontsize=9)126 axes[0].set_ylim(0, 108)127 axes[0].legend(fontsize=8, facecolor='#1e293b',128 edgecolor='#334155', labelcolor='#cbd5e1')129 axes[0].grid(True, alpha=0.15, color='#334155')130 131 # Right — confidence bars132 bars = ['ML Model', 'Rules', 'Final Score']133 values = [ml_prob * 100, rule_prob * 100, final_prob * 100]134 colors = ['#60a5fa', '#4ade80', '#f59e0b' if final_prob < 0.55 else '#22c55e']135 axes[1].barh(bars, values, color=colors, edgecolor='#0f172a', height=0.45)136 axes[1].axvline(x=50, color='#94a3b8', linestyle='--',137 linewidth=1, alpha=0.6, label='50% threshold')138 axes[1].set_xlim(0, 105)139 axes[1].set_title('Confidence Breakdown', color='#f1f5f9', fontsize=11, pad=10)140 axes[1].set_xlabel('Confidence (%)', color='#94a3b8', fontsize=9)141 for i, v in enumerate(values):142 axes[1].text(v + 1.5, i, f'{v:.1f}%', va='center',143 fontsize=10, color='#f1f5f9', fontweight='bold')144 axes[1].legend(fontsize=8, facecolor='#1e293b',145 edgecolor='#334155', labelcolor='#cbd5e1')146 axes[1].grid(True, alpha=0.15, color='#334155', axis='x')147 148 plt.tight_layout(pad=2.0)149 return fig150 151# ── Manual prediction ─────────────────────────────────────────152def run_manual(soil_moisture):153 label, final_prob, ml_prob, rule_prob, reason, history, change = predict_rainfall(soil_moisture)154 fig = build_chart(history, soil_moisture, final_prob, ml_prob, rule_prob,155 title='Simulated Moisture Trend')156 conf = f'{final_prob*100:.1f}%'157 return label, conf, reason, fig158 159# ── ThingSpeak live prediction ────────────────────────────────160def run_live():161 try:162 url = f'https://api.thingspeak.com/channels/{CHANNEL_ID}/feeds.json'163 r = requests.get(url, params={'api_key': READ_API_KEY, 'results': 20}, timeout=10)164 feeds = r.json().get('feeds', [])165 166 if not feeds:167 return '⚠️ No data yet', '--', 'ThingSpeak channel has no readings', None, '--'168 169 readings = []170 for f in feeds:171 try:172 val = f.get('field1')173 if val is not None:174 readings.append(float(val))175 except:176 continue177 178 if not readings:179 return '⚠️ No valid readings', '--', 'Could not parse sensor data', None, '--'180 181 latest = readings[-1]182 previous = readings[:-1] if len(readings) > 1 else None183 184 label, final_prob, ml_prob, rule_prob, reason, history, change = predict_rainfall(185 latest, previous_readings=previous186 )187 188 fig = build_chart(history, latest, final_prob, ml_prob, rule_prob,189 title='Live ThingSpeak Readings')190 conf = f'{final_prob*100:.1f}%'191 lat = f'{latest:.1f}%'192 return label, conf, reason, fig, lat193 194 except Exception as e:195 return f'❌ Error: {str(e)}', '--', 'Check your ThingSpeak credentials', None, '--'196 197# ── Custom CSS ────────────────────────────────────────────────198css = """199@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap');200 201* { font-family: 'Outfit', sans-serif !important; }202 203body, .gradio-container {204 background: #0f172a !important;205 color: #f1f5f9 !important;206}207 208.gradio-container {209 max-width: 1100px !important;210 margin: 0 auto !important;211}212 213.header-box {214 background: linear-gradient(135deg, #052e16 0%, #064e3b 50%, #0f172a 100%);215 border: 1px solid #22c55e33;216 border-radius: 16px;217 padding: 24px 32px;218 margin-bottom: 8px;219 display: flex;220 align-items: center;221 gap: 20px;222}223 224.header-text h1 {225 font-size: 1.8rem !important;226 font-weight: 700 !important;227 color: #4ade80 !important;228 margin: 0 0 4px 0 !important;229 letter-spacing: -0.5px;230}231 232.header-text p {233 color: #86efac !important;234 font-size: 0.9rem !important;235 margin: 0 !important;236 opacity: 0.85;237}238 239.school-tag {240 font-family: 'Space Mono', monospace !important;241 font-size: 0.7rem !important;242 color: #22c55e !important;243 background: #052e1644;244 border: 1px solid #22c55e44;245 border-radius: 6px;246 padding: 3px 10px;247 display: inline-block;248 margin-top: 6px;249 letter-spacing: 1px;250}251 252.tab-nav button {253 background: #1e293b !important;254 color: #94a3b8 !important;255 border: 1px solid #334155 !important;256 border-radius: 8px 8px 0 0 !important;257 font-weight: 500 !important;258 padding: 10px 24px !important;259 transition: all 0.2s !important;260}261 262.tab-nav button.selected {263 background: #052e16 !important;264 color: #4ade80 !important;265 border-bottom-color: #052e16 !important;266 border-top: 2px solid #22c55e !important;267}268 269.gr-box, .gr-input, .gr-form {270 background: #1e293b !important;271 border-color: #334155 !important;272 border-radius: 10px !important;273 color: #f1f5f9 !important;274}275 276input[type=range] { accent-color: #22c55e !important; }277 278label, .gr-block label span {279 color: #94a3b8 !important;280 font-size: 0.85rem !important;281 font-weight: 500 !important;282 text-transform: uppercase !important;283 letter-spacing: 0.5px !important;284}285 286textarea, input[type=text] {287 background: #0f172a !important;288 color: #4ade80 !important;289 border: 1px solid #22c55e44 !important;290 border-radius: 8px !important;291 font-size: 1.1rem !important;292 font-weight: 600 !important;293}294 295button.primary {296 background: linear-gradient(135deg, #16a34a, #22c55e) !important;297 border: none !important;298 color: #052e16 !important;299 font-weight: 700 !important;300 font-size: 1rem !important;301 border-radius: 10px !important;302 padding: 12px 28px !important;303 transition: all 0.2s !important;304 letter-spacing: 0.3px !important;305}306 307button.primary:hover {308 transform: translateY(-1px) !important;309 box-shadow: 0 4px 20px #22c55e44 !important;310}311 312.legend-table {313 width: 100%;314 border-collapse: collapse;315 font-size: 0.88rem;316 margin-top: 8px;317}318 319.legend-table th {320 background: #052e16;321 color: #4ade80;322 padding: 8px 14px;323 text-align: left;324 font-weight: 600;325 letter-spacing: 0.4px;326}327 328.legend-table td {329 padding: 7px 14px;330 border-bottom: 1px solid #1e293b;331 color: #cbd5e1;332}333 334.legend-table tr:nth-child(even) td { background: #1e293b22; }335 336.footer-note {337 text-align: center;338 color: #475569;339 font-size: 0.78rem;340 margin-top: 12px;341 font-family: 'Space Mono', monospace !important;342}343"""344 345# ── Header HTML ───────────────────────────────────────────────346header_html = f"""347<div class="header-box">348 {LOGO_HTML}349 <div class="header-text">350 <h1>🌱 Soil Moisture Rainfall Predictor</h1>351 <p>Real-time IoT rainfall prediction using hybrid ML + rule-based intelligence</p>352 <span class="school-tag">ICS · Training Tomorrow's Leaders Today</span>353 </div>354</div>355"""356 357legend_html = """358<table class="legend-table">359 <tr><th>Prediction</th><th>Moisture Level</th><th>Meaning</th></tr>360 <tr><td>☀️ Very Unlikely</td><td>Below 30%</td><td>Soil is very dry — no rain expected</td></tr>361 <tr><td>☀️ Unlikely</td><td>30% – 69%</td><td>Below average moisture</td></tr>362 <tr><td>🌤️ Possible</td><td>69% – 75%</td><td>Near the average — rain could occur</td></tr>363 <tr><td>🌧️ Likely</td><td>75% – 90%</td><td>Soil is wet — rain is probable</td></tr>364 <tr><td>🌧️ Very Likely</td><td>Above 90%</td><td>Soil is saturated — rain almost certain</td></tr>365</table>366<p class="footer-note">Model: Hybrid ML + Rules · Accuracy: 85%+ · Sensor: Capacitive Soil Moisture + ESP32 + ThingSpeak</p>367"""368 369# ── Gradio UI ─────────────────────────────────────────────────370with gr.Blocks(css=css, title='ICS Soil Moisture Rainfall Predictor') as app:371 372 gr.HTML(header_html)373 374 with gr.Tabs():375 376 # ── TAB 1: Live ThingSpeak ──────────────────────────377 with gr.Tab("📡 Live Sensor Feed"):378 gr.Markdown("**Fetches real-time readings from your ESP32 sensor via ThingSpeak**")379 380 fetch_btn = gr.Button("🔄 Fetch Latest Data & Predict", variant="primary")381 382 with gr.Row():383 live_pred = gr.Textbox(label="Rainfall Prediction", interactive=False, scale=2)384 live_conf = gr.Textbox(label="Confidence", interactive=False, scale=1)385 live_latest = gr.Textbox(label="Latest Reading", interactive=False, scale=1)386 387 live_reason = gr.Textbox(label="Reason", interactive=False)388 live_chart = gr.Plot(label="Live Analysis")389 390 fetch_btn.click(391 fn=run_live,392 outputs=[live_pred, live_conf, live_reason, live_chart, live_latest]393 )394 395 gr.Markdown("""396 > **Note:** Readings update every 20 seconds from your ESP32 sensor in the field.397 > Click the button anytime to get the latest prediction.398 """)399 400 # ── TAB 2: Manual Entry ─────────────────────────────401 with gr.Tab("✍️ Manual Entry"):402 gr.Markdown("**Enter a soil moisture value manually to test the prediction model**")403 404 with gr.Row():405 with gr.Column(scale=1):406 slider = gr.Slider(407 minimum=0, maximum=100,408 value=50, step=1,409 label="Soil Moisture (%)"410 )411 manual_btn = gr.Button("🔍 Predict", variant="primary")412 413 gr.Markdown("**Quick test values:**")414 gr.Examples(415 examples=[[10], [25], [45], [65], [78], [88], [97]],416 inputs=slider,417 label="Examples"418 )419 420 with gr.Column(scale=2):421 with gr.Row():422 manual_pred = gr.Textbox(label="Rainfall Prediction", interactive=False, scale=2)423 manual_conf = gr.Textbox(label="Confidence", interactive=False, scale=1)424 manual_reason = gr.Textbox(label="Reason", interactive=False)425 manual_chart = gr.Plot(label="Analysis")426 427 manual_btn.click(428 fn=run_manual,429 inputs=slider,430 outputs=[manual_pred, manual_conf, manual_reason, manual_chart]431 )432 433 gr.HTML(legend_html)434 435app.launch()