CoolFace
Apppublic

projectXect/soil-moisture-rainfall-prediction

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
app.py435 linesDownload Raw Back to root
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()