""" Gradio web app for Crop Recommendation System (compatible with gradio==3.50.0) - Avoids using Gradio args that differ across versions. - If model.pkl or model_meta.json don't exist, creates a small dummy sklearn pipeline and saves it so the Space will start and you can test the UI. """ import json import joblib import numpy as np import pandas as pd import traceback from pathlib import Path # sklearn imports (requirements pinned above) from sklearn.ensemble import RandomForestClassifier from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split import gradio as gr MODEL_PATH = Path("model.pkl") META_PATH = Path("model_meta.json") # Default label classes for dummy model _DEFAULT_LABELS = ["rice", "maize", "chickpea", "cotton", "wheat"] _DEFAULT_NUMERIC_COLS = ["N", "P", "K", "temperature", "humidity", "ph", "rainfall"] def build_and_save_dummy_model(model_path: Path, meta_path: Path): """ Build a tiny RandomForest pipeline trained on synthetic data and save to disk. This ensures the Space starts even without a user model. """ # create synthetic data (reasonable ranges similar to UI sliders) rng = np.random.RandomState(42) n_samples = 500 # features: N,P,K,temperature,humidity,ph,rainfall X = pd.DataFrame({ "N": rng.randint(0, 141, size=n_samples), "P": rng.randint(5, 146, size=n_samples), "K": rng.randint(5, 206, size=n_samples), "temperature": rng.uniform(8, 44, size=n_samples), "humidity": rng.randint(14, 101, size=n_samples), "ph": rng.uniform(3.5, 10, size=n_samples), "rainfall": rng.uniform(20, 300, size=n_samples) }) # create target with some synthetic (but deterministic) rule-ish mapping # produce labels 0..len(_DEFAULT_LABELS)-1 # Basic heuristic: high rainfall -> rice, high temp -> cotton/maize, etc. y = [] for i, row in X.iterrows(): if row["rainfall"] > 1500: y.append(0) # rice elif row["temperature"] > 30 and row["rainfall"] < 800: y.append(3) # cotton elif row["ph"] < 5.5: y.append(2) # chickpea elif row["N"] > 80: y.append(1) # maize else: y.append(4) # wheat y = np.array(y) # simple pipeline pipeline = Pipeline([ ("scaler", StandardScaler()), ("clf", RandomForestClassifier(n_estimators=50, random_state=42)) ]) pipeline.fit(X, y) # Save model and meta joblib.dump(pipeline, model_path) meta = { "numeric_cols": _DEFAULT_NUMERIC_COLS, # store label_classes as list for predictable indexing "label_classes": _DEFAULT_LABELS } with open(meta_path, "w", encoding="utf-8") as f: json.dump(meta, f, indent=2) return pipeline, meta def load_model_and_meta(): """ Try to load model and meta from disk. If not present, build a dummy model and save it. Returns (model, meta, load_error_message_or_None) """ load_error = None try: if not MODEL_PATH.exists() or not META_PATH.exists(): # build & save dummy model so app can run model, meta = build_and_save_dummy_model(MODEL_PATH, META_PATH) return model, meta, None model = joblib.load(MODEL_PATH) with open(META_PATH, "r", encoding="utf-8") as f: meta = json.load(f) # Validate meta if "numeric_cols" not in meta or "label_classes" not in meta: raise KeyError("model_meta.json must contain 'numeric_cols' and 'label_classes' keys.") return model, meta, None except Exception as e: load_error = f"{type(e).__name__}: {e}\n\n{traceback.format_exc()}" # Create fallback dummy objects (in-memory) so UI can still run and show the error fallback_model, fallback_meta = build_and_save_dummy_model(MODEL_PATH, META_PATH) return fallback_model, fallback_meta, load_error # Load or create model & meta _model, _meta, _load_error = load_model_and_meta() # Normalize label_classes into an indexable list label_classes = _meta.get("label_classes", _DEFAULT_LABELS) if isinstance(label_classes, dict): # If it's a dict mapping strings to labels, convert to list by sorting keys when possible try: # try to convert numeric-string keys to int index order items = sorted(label_classes.items(), key=lambda kv: int(kv[0]) if str(kv[0]).isdigit() else kv[0]) label_list = [v for k, v in items] except Exception: # fallback: just take values label_list = list(label_classes.values()) label_classes = label_list elif isinstance(label_classes, list): label_classes = label_classes else: # other types: force to list of strings label_classes = [str(x) for x in label_classes] numeric_cols = _meta.get("numeric_cols", _DEFAULT_NUMERIC_COLS) def predict_crop(N, P, K, temperature, humidity, ph, rainfall): """ Predict crop recommendation and top-3 with confidences. Returns: (recommended_crop_str, confidence_str, top3_dict) """ if _load_error: # Informative response so UI shows the saved error return "Model load warning", "N/A", {"warning": _load_error} try: input_df = pd.DataFrame({ "N": [N], "P": [P], "K": [K], "temperature": [temperature], "humidity": [humidity], "ph": [ph], "rainfall": [rainfall] }) # Predict pred_enc = _model.predict(input_df)[0] # probabilities: handle models without predict_proba gracefully if hasattr(_model, "predict_proba"): probs = _model.predict_proba(input_df)[0] else: # if pipeline ends with classifier without predict_proba, give uniform small values n_labels = len(label_classes) probs = np.zeros(n_labels) probs[pred_enc] = 1.0 # Map encoded prediction to label name robustly try: recommended_crop = label_classes[int(pred_enc)] except Exception: # fallback: if label_classes contains strings of ints or mapping if str(pred_enc) in label_classes: recommended_crop = str(pred_enc) else: # last resort recommended_crop = str(pred_enc) # Confidence lookup: if pred_enc is valid index try: confidence = probs[int(pred_enc)] except Exception: confidence = float(np.max(probs)) if len(probs) > 0 else 0.0 # Top-3 top_idx = np.argsort(probs)[::-1][:3] top3 = {} for rank, idx in enumerate(top_idx, 1): label = label_classes[idx] if idx < len(label_classes) else f"label_{idx}" top3[f"{rank}. {label}"] = f"{probs[idx]:.2%}" return recommended_crop, f"{confidence:.2%}", top3 except Exception as e: err = f"{type(e).__name__}: {e}\n\n{traceback.format_exc()}" return "Prediction failed", "N/A", {"error": err} # Build Gradio UI (compatible usage) with gr.Blocks(title="Crop Recommendation System") as demo: gr.Markdown("# 🌾 Crop Recommendation System") gr.Markdown("Enter soil and weather parameters to get an AI-powered crop recommendation with confidence scores.") if _load_error: gr.Markdown("**⚠️ Warning: There was an issue loading your provided model. A fallback/dummy model is in use.**") # show a short truncated error message in a code block for diagnosis truncated = _load_error if len(_load_error) < 3000 else _load_error[:3000] + "\n\n...[truncated]" gr.Code(truncated, language="text") with gr.Row(): with gr.Column(scale=1): gr.Markdown("### Soil Parameters") N = gr.Slider(label="Nitrogen (N)", minimum=0, maximum=140, value=90, step=1) P = gr.Slider(label="Phosphorus (P)", minimum=5, maximum=145, value=42, step=1) K = gr.Slider(label="Potassium (K)", minimum=5, maximum=205, value=43, step=1) with gr.Column(scale=1): gr.Markdown("### Weather Parameters") temperature = gr.Slider(label="Temperature (°C)", minimum=8, maximum=44, value=21, step=0.1) humidity = gr.Slider(label="Humidity (%)", minimum=14, maximum=100, value=82, step=1) ph = gr.Slider(label="Soil pH", minimum=3.5, maximum=10, value=6.5, step=0.1) rainfall = gr.Slider(label="Annual Rainfall (mm)", minimum=20, maximum=3000, value=203, step=10) predict_btn = gr.Button("🔍 Get Crop Recommendation") with gr.Row(): with gr.Column(scale=2): recommended = gr.Textbox(label="🌾 Recommended Crop", interactive=False) confidence = gr.Textbox(label="✅ Confidence", interactive=False) with gr.Column(scale=1): # gr.JSON in 3.50.0 does NOT accept interactive param - keep it simple top_3 = gr.JSON(label="📈 Top 3 Recommendations") predict_btn.click( fn=predict_crop, inputs=[N, P, K, temperature, humidity, ph, rainfall], outputs=[recommended, confidence, top_3] ) gr.Markdown(""" --- ### Parameter Ranges (based on training data) - **Nitrogen (N)**: 0-140 kg/ha - **Phosphorus (P)**: 5-145 kg/ha - **Potassium (K)**: 5-205 kg/ha - **Temperature**: 8-44°C - **Humidity**: 14-100% - **pH**: 3.5-10 - **Rainfall**: 20-3000 mm/year """) if __name__ == "__main__": # Launch without sending a theme to avoid version issues in older Gradio demo.launch(share=False)