Add a /model-info page — show metrics + feature importance
The model is a black box to users. Surface the R^2, MAE, and which features matter most. Trust comes from transparency.
🧮 Code files for this stage
# Add to app/main.py:
@app.get('/model-info', response_class=HTMLResponse)
async def model_info(request: Request):
# Inspect the loaded model — no retraining
model_type = type(model).__name__
# Feature importances (RandomForest) or coefficients (LinearRegression)
if hasattr(model, 'feature_importances_'):
importances = list(zip(FEATURES, model.feature_importances_))
importances.sort(key=lambda x: x[1], reverse=True)
importance_type = 'feature_importances_'
elif hasattr(model, 'coef_'):
importances = list(zip(FEATURES, model.coef_))
importances.sort(key=lambda x: abs(x[1]), reverse=True)
importance_type = 'coefficient'
else:
importances = []
importance_type = 'unknown'
# Re-compute test metrics by loading the data + splitting the same way
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, r2_score
df = pd.read_csv('data/students.csv').drop(columns=['student_id'])
X = df.drop(columns=['final_score'])
y = df['final_score']
_, X_test, _, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
y_pred = model.predict(X_test)
metrics = {
'r2': round(r2_score(y_test, y_pred), 3),
'mae': round(mean_absolute_error(y_test, y_pred), 2),
}
return templates.TemplateResponse('model.html', {
'request': request,
'model_type': model_type,
'metrics': metrics,
'importances': importances,
'importance_type': importance_type,
})
{% extends 'base.html' %}
{% block title %}Model info{% endblock %}
{% block content %}
<div class='card'>
<h1>Model Info</h1>
<p><strong>Type:</strong> {{ model_type }}</p>
<p><strong>R^2:</strong> {{ metrics.r2 }} (1.0 is perfect, 0 is guessing)</p>
<p><strong>MAE:</strong> {{ metrics.mae }} points (average error)</p>
</div>
<div class='card' style='margin-top: 1rem;'>
<h2>Feature importance ({{ importance_type }})</h2>
<p>Which inputs matter most to the model's prediction.</p>
{% for feature, weight in importances %}
<div class='imp-row'>
<span class='imp-name'>{{ feature }}</span>
<div class='imp-bar-bg'>
<div class='imp-bar' style='width: {{ (weight * 100 if weight < 1 else weight) | round(1) }}%;'></div>
</div>
<span class='imp-val'>{{ weight | round(3) }}</span>
</div>
{% endfor %}
</div>
{% endblock %}
/* Feature importance bars */
.imp-row { display: flex; align-items: center; gap: 0.5rem; margin: 0.4rem 0; }
.imp-name { width: 200px; font-family: monospace; font-size: 0.9rem; }
.imp-bar-bg { flex: 1; height: 18px; background: #eee; border-radius: 9px; overflow: hidden; }
.imp-bar { height: 100%; background: #a0c000; transition: width 0.3s; }
.imp-val { width: 60px; text-align: right; font-family: monospace; font-size: 0.85rem; color: #555; }
How it works
hasattr(model, 'feature_importances_') is the trick — RandomForest and most tree-based models expose feature_importances_ (a 0-1 array summing to 1, where bigger = more important). LinearRegression exposes coef_ instead (signed weights — positive means more of that feature INCREASES the prediction, negative means DECREASES). The same /model-info route handles both cases by checking which attribute exists. This is duck typing — we don't care WHAT the model is, we care what it CAN DO.
/model-info returns HTML, not JSON. Use response_class=HTMLResponse. If you also want a JSON version (for dashboards), add a parallel GET /api/model-info with response_model=ModelInfoResponse — but for this project, HTML is enough.
- Retraining the model on every /model-info request — load once at startup, just read its attributes here.
- Showing coefficients without absolute value — a -5 coefficient is MORE important than a +1, but sorting by raw value puts it last.
- Comparing RandomForest importances with LinearRegression coefficients directly — they're on different scales. Compare within one model type.