Spaces:
Sleeping
Sleeping
Upload health_grade_predictor.py
#135
by Danielescp - opened
- health_grade_predictor.py +257 -0
health_grade_predictor.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Restaurant Health Grade Predictor
|
| 3 |
+
----------------------------------
|
| 4 |
+
A Gradio app that predicts health inspection grades (A/B/C)
|
| 5 |
+
using a placeholder Random Forest model trained on synthetic data.
|
| 6 |
+
|
| 7 |
+
Requirements:
|
| 8 |
+
pip install gradio scikit-learn matplotlib numpy pandas
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import matplotlib.pyplot as plt
|
| 15 |
+
import matplotlib.patches as mpatches
|
| 16 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 17 |
+
from sklearn.preprocessing import LabelEncoder
|
| 18 |
+
import warnings
|
| 19 |
+
|
| 20 |
+
warnings.filterwarnings("ignore")
|
| 21 |
+
|
| 22 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
# 1. Build a placeholder Random Forest model on synthetic data
|
| 24 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
+
|
| 26 |
+
CUISINE_TYPES = [
|
| 27 |
+
"American", "Chinese", "Italian", "Mexican", "Japanese",
|
| 28 |
+
"Indian", "Thai", "Mediterranean", "French", "Korean",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
VIOLATION_CODES = [
|
| 32 |
+
"No Violation",
|
| 33 |
+
"02A - No food safety certificate",
|
| 34 |
+
"04L - Evidence of mice or rats",
|
| 35 |
+
"06C - Food not protected",
|
| 36 |
+
"08A - Facility not sanitized",
|
| 37 |
+
"10B - Plumbing not properly installed",
|
| 38 |
+
"15L - Workers not using proper hygiene",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
GRADE_LABELS = ["A", "B", "C"]
|
| 42 |
+
|
| 43 |
+
# Encode categorical features
|
| 44 |
+
cuisine_enc = LabelEncoder().fit(CUISINE_TYPES)
|
| 45 |
+
violation_enc = LabelEncoder().fit(VIOLATION_CODES)
|
| 46 |
+
|
| 47 |
+
def encode_inputs(cuisine: str, violation: str, score: float) -> np.ndarray:
|
| 48 |
+
c = cuisine_enc.transform([cuisine])[0]
|
| 49 |
+
v = violation_enc.transform([violation])[0]
|
| 50 |
+
return np.array([[c, v, score]])
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def generate_synthetic_data(n: int = 2000, seed: int = 42) -> tuple:
|
| 54 |
+
rng = np.random.default_rng(seed)
|
| 55 |
+
cuisines = rng.integers(0, len(CUISINE_TYPES), n)
|
| 56 |
+
violations = rng.integers(0, len(VIOLATION_CODES), n)
|
| 57 |
+
scores = rng.uniform(0, 100, n)
|
| 58 |
+
|
| 59 |
+
# Grade logic: score drives grade; violations add noise
|
| 60 |
+
grades = []
|
| 61 |
+
for i in range(n):
|
| 62 |
+
base = scores[i]
|
| 63 |
+
penalty = violations[i] * 3 # higher code β worse grade
|
| 64 |
+
effective = base - penalty
|
| 65 |
+
if effective >= 60:
|
| 66 |
+
grades.append(0) # A
|
| 67 |
+
elif effective >= 40:
|
| 68 |
+
grades.append(1) # B
|
| 69 |
+
else:
|
| 70 |
+
grades.append(2) # C
|
| 71 |
+
|
| 72 |
+
X = np.column_stack([cuisines, violations, scores])
|
| 73 |
+
y = np.array(grades)
|
| 74 |
+
return X, y
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
print("Training placeholder Random Forest model β¦")
|
| 78 |
+
X_train, y_train = generate_synthetic_data()
|
| 79 |
+
model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
|
| 80 |
+
model.fit(X_train, y_train)
|
| 81 |
+
print("Model ready β")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 85 |
+
# 2. Prediction + chart function
|
| 86 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 87 |
+
|
| 88 |
+
GRADE_COLORS = {
|
| 89 |
+
"A": "#2ECC71", # green
|
| 90 |
+
"B": "#F39C12", # amber
|
| 91 |
+
"C": "#E74C3C", # red
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
def predict_grade(cuisine: str, violation: str, score: float):
|
| 95 |
+
"""Run inference and return a grade label and a probability bar chart."""
|
| 96 |
+
X = encode_inputs(cuisine, violation, score)
|
| 97 |
+
proba = model.predict_proba(X)[0] # shape (3,)
|
| 98 |
+
pred_idx = int(np.argmax(proba))
|
| 99 |
+
grade = GRADE_LABELS[pred_idx]
|
| 100 |
+
confidence = proba[pred_idx] * 100
|
| 101 |
+
|
| 102 |
+
# ββ build the bar chart ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 103 |
+
fig, ax = plt.subplots(figsize=(6, 3.5))
|
| 104 |
+
fig.patch.set_facecolor("#1A1A2E")
|
| 105 |
+
ax.set_facecolor("#16213E")
|
| 106 |
+
|
| 107 |
+
bar_colors = [GRADE_COLORS[g] for g in GRADE_LABELS]
|
| 108 |
+
bars = ax.bar(
|
| 109 |
+
GRADE_LABELS,
|
| 110 |
+
proba * 100,
|
| 111 |
+
color=bar_colors,
|
| 112 |
+
width=0.5,
|
| 113 |
+
edgecolor="none",
|
| 114 |
+
zorder=3,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
# highlight the predicted grade with a glow border
|
| 118 |
+
pred_bar = bars[pred_idx]
|
| 119 |
+
pred_bar.set_linewidth(2.5)
|
| 120 |
+
pred_bar.set_edgecolor("white")
|
| 121 |
+
|
| 122 |
+
# value labels on bars
|
| 123 |
+
for bar, p in zip(bars, proba * 100):
|
| 124 |
+
ax.text(
|
| 125 |
+
bar.get_x() + bar.get_width() / 2,
|
| 126 |
+
bar.get_height() + 1.5,
|
| 127 |
+
f"{p:.1f}%",
|
| 128 |
+
ha="center", va="bottom",
|
| 129 |
+
color="white", fontsize=11, fontweight="bold",
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
ax.set_ylim(0, 110)
|
| 133 |
+
ax.set_xlabel("Predicted Grade", color="#AAAACC", fontsize=11, labelpad=8)
|
| 134 |
+
ax.set_ylabel("Probability (%)", color="#AAAACC", fontsize=11, labelpad=8)
|
| 135 |
+
ax.set_title(
|
| 136 |
+
f"Model Confidence β Predicted Grade: {grade} ({confidence:.1f}%)",
|
| 137 |
+
color="white", fontsize=13, fontweight="bold", pad=12,
|
| 138 |
+
)
|
| 139 |
+
ax.tick_params(colors="white", labelsize=12)
|
| 140 |
+
for spine in ax.spines.values():
|
| 141 |
+
spine.set_visible(False)
|
| 142 |
+
ax.yaxis.grid(True, color="#2A2A4A", linewidth=0.8, zorder=0)
|
| 143 |
+
ax.set_axisbelow(True)
|
| 144 |
+
|
| 145 |
+
plt.tight_layout()
|
| 146 |
+
|
| 147 |
+
# ββ compose the text output βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 148 |
+
emoji = {"A": "π’", "B": "π‘", "C": "π΄"}[grade]
|
| 149 |
+
summary = (
|
| 150 |
+
f"{emoji} Predicted Health Grade: **{grade}**\n\n"
|
| 151 |
+
f"Confidence: {confidence:.1f}%\n\n"
|
| 152 |
+
f"---\n"
|
| 153 |
+
f"| Input | Value |\n"
|
| 154 |
+
f"|---|---|\n"
|
| 155 |
+
f"| Cuisine | {cuisine} |\n"
|
| 156 |
+
f"| Violation | {violation} |\n"
|
| 157 |
+
f"| Inspection Score | {score:.1f} |\n\n"
|
| 158 |
+
f"*Note: This uses a placeholder Random Forest model trained on "
|
| 159 |
+
f"synthetic data. Replace `generate_synthetic_data()` and re-train "
|
| 160 |
+
f"with real inspection records for production use.*"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
return summary, fig
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 167 |
+
# 3. Gradio UI
|
| 168 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 169 |
+
|
| 170 |
+
DESCRIPTION = """
|
| 171 |
+
## π½οΈ Restaurant Health Grade Predictor
|
| 172 |
+
|
| 173 |
+
Enter inspection details below to get a predicted **A / B / C** health grade
|
| 174 |
+
and a probability breakdown from the Random Forest model.
|
| 175 |
+
"""
|
| 176 |
+
|
| 177 |
+
with gr.Blocks(
|
| 178 |
+
title="Health Grade Predictor",
|
| 179 |
+
theme=gr.themes.Soft(
|
| 180 |
+
primary_hue="violet",
|
| 181 |
+
secondary_hue="slate",
|
| 182 |
+
neutral_hue="slate",
|
| 183 |
+
),
|
| 184 |
+
css="""
|
| 185 |
+
.predict-btn { font-size: 1.1rem !important; padding: 0.7rem !important; }
|
| 186 |
+
#grade-output .prose { font-size: 1.05rem !important; }
|
| 187 |
+
""",
|
| 188 |
+
) as demo:
|
| 189 |
+
|
| 190 |
+
gr.Markdown(DESCRIPTION)
|
| 191 |
+
|
| 192 |
+
with gr.Row():
|
| 193 |
+
with gr.Column(scale=1):
|
| 194 |
+
cuisine_input = gr.Dropdown(
|
| 195 |
+
choices=CUISINE_TYPES,
|
| 196 |
+
value="American",
|
| 197 |
+
label="π Cuisine Type",
|
| 198 |
+
)
|
| 199 |
+
violation_input = gr.Dropdown(
|
| 200 |
+
choices=VIOLATION_CODES,
|
| 201 |
+
value="No Violation",
|
| 202 |
+
label="β οΈ Violation Code",
|
| 203 |
+
)
|
| 204 |
+
score_input = gr.Slider(
|
| 205 |
+
minimum=0,
|
| 206 |
+
maximum=100,
|
| 207 |
+
value=85,
|
| 208 |
+
step=0.5,
|
| 209 |
+
label="π Inspection Score (0 = worst, 100 = best)",
|
| 210 |
+
)
|
| 211 |
+
predict_btn = gr.Button(
|
| 212 |
+
"π Predict Grade",
|
| 213 |
+
variant="primary",
|
| 214 |
+
elem_classes="predict-btn",
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
with gr.Column(scale=2):
|
| 218 |
+
grade_output = gr.Markdown(
|
| 219 |
+
value="*Fill in the inputs and click **Predict Grade**.*",
|
| 220 |
+
elem_id="grade-output",
|
| 221 |
+
)
|
| 222 |
+
chart_output = gr.Plot(label="Grade Probability Distribution")
|
| 223 |
+
|
| 224 |
+
predict_btn.click(
|
| 225 |
+
fn=predict_grade,
|
| 226 |
+
inputs=[cuisine_input, violation_input, score_input],
|
| 227 |
+
outputs=[grade_output, chart_output],
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
gr.Examples(
|
| 231 |
+
examples=[
|
| 232 |
+
["Italian", "No Violation", 95],
|
| 233 |
+
["Chinese", "04L - Evidence of mice or rats", 55],
|
| 234 |
+
["Mexican", "08A - Facility not sanitized", 40],
|
| 235 |
+
["Japanese", "02A - No food safety certificate",72],
|
| 236 |
+
["Mediterranean","15L - Workers not using proper hygiene", 30],
|
| 237 |
+
],
|
| 238 |
+
inputs=[cuisine_input, violation_input, score_input],
|
| 239 |
+
outputs=[grade_output, chart_output],
|
| 240 |
+
fn=predict_grade,
|
| 241 |
+
cache_examples=True,
|
| 242 |
+
label="π Quick Examples",
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
gr.Markdown(
|
| 246 |
+
"""
|
| 247 |
+
---
|
| 248 |
+
**How grades work (synthetic rules used for training)**
|
| 249 |
+
`Effective Score = Inspection Score β (Violation Code Index Γ 3)`
|
| 250 |
+
β’ **A** β Effective β₯ 60 | **B** β 40β59 | **C** β < 40
|
| 251 |
+
|
| 252 |
+
Replace `generate_synthetic_data()` with a real labelled dataset to make this production-ready.
|
| 253 |
+
"""
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
demo.launch(share=False)
|