alina-portfolio's picture
Update app.py
4f391bf verified
"""
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)