Section 11.3.16 : Job Tracker — Dashboard de statistiques
🎯 Objectif pédagogique
Construire un dashboard interactif avec des graphiques pour visualiser l'avancement de la recherche d'emploi. Vous serez capable de créer des visualisations avec Plotly dans Streamlit et de transformer des données brutes en insights actionnables.
Les données racontent une histoire
Marc a 30 candidatures dans son Job Tracker. Mais combien ont abouti à un entretien ? Quel est son taux de conversion ? Quelles entreprises répondent le plus vite ? Le dashboard transforme les chiffres bruts en décisions.
Dashboard avec Plotly et Streamlit
# dashboard.py — Statistiques et graphiques
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
from database import get_connection
from datetime import datetime, timedelta
import pandas as pd
def render_dashboard():
"""Page dashboard avec statistiques et graphiques."""
st.title("📊 Dashboard — Statistiques de recherche")
# Charger les données
conn = get_connection()
df = pd.read_sql_query(
"SELECT * FROM applications WHERE archived = 0",
conn
)
events_df = pd.read_sql_query("SELECT * FROM events", conn)
conn.close()
if df.empty:
st.info("Aucune candidature. Ajoutez-en depuis le Kanban !")
return
# === KPIs en haut ===
col1, col2, col3, col4 = st.columns(4)
total = len(df)
interviews = len(df[df['status'] == 'interview'])
offers = len(df[df['status'] == 'offer'])
conversion = (interviews / total * 100) if total > 0 else 0
col1.metric("Total candidatures", total)
col2.metric("Entretiens", interviews, f"\{conversion:.0f\}%")
col3.metric("Offres", offers)
col4.metric("Taux de succès", f"\{(offers/total*100):.0f\}%" if total > 0 else "0%")
st.divider()
# === Graphique 1 : Répartition par statut ===
col_left, col_right = st.columns(2)
with col_left:
status_counts = df['status'].value_counts()
colors = \{
'wishlist': '#9CA3AF', 'applied': '#3B82F6',
'screening': '#F59E0B', 'interview': '#8B5CF6',
'offer': '#10B981', 'rejected': '#EF4444'
\}
fig_status = px.pie(
values=status_counts.values,
names=status_counts.index,
title="Répartition par statut",
color=status_counts.index,
color_discrete_map=colors
)
st.plotly_chart(fig_status, use_container_width=True)
with col_right:
# === Graphique 2 : Candidatures par semaine ===
df['created_date'] = pd.to_datetime(df['created_at']).dt.date
weekly = df.groupby(
pd.to_datetime(df['created_at']).dt.isocalendar().week
).size().reset_index(name='count')
weekly.columns = ['week', 'count']
fig_weekly = px.bar(
weekly, x='week', y='count',
title="Candidatures par semaine",
labels=\{'week': 'Semaine', 'count': 'Nombre'\}
)
fig_weekly.update_traces(marker_color='#0891B2')
st.plotly_chart(fig_weekly, use_container_width=True)
st.divider()
# === Graphique 3 : Funnel de conversion ===
funnel_stages = ['applied', 'screening', 'interview', 'offer']
funnel_values = []
for stage in funnel_stages:
# Compter ceux qui ont atteint au moins ce stade
stage_idx = funnel_stages.index(stage)
count = len(df[df['status'].isin(funnel_stages[stage_idx:])])
funnel_values.append(count)
fig_funnel = go.Figure(go.Funnel(
y=['Applied', 'Screening', 'Interview', 'Offer'],
x=funnel_values,
textinfo="value+percent initial",
marker=dict(color=['#3B82F6', '#F59E0B', '#8B5CF6', '#10B981'])
))
fig_funnel.update_layout(title="Funnel de conversion")
st.plotly_chart(fig_funnel, use_container_width=True)
# === Graphique 4 : Temps moyen par étape ===
st.subheader("⏱️ Temps moyen par étape")
stage_durations = []
for _, row in df.iterrows():
app_events = events_df[events_df['application_id'] == row['id']].sort_values('event_date')
if len(app_events) >= 2:
first = pd.to_datetime(app_events.iloc[0]['event_date'])
last = pd.to_datetime(app_events.iloc[-1]['event_date'])
days = (last - first).days
stage_durations.append(\{
'company': row['company'],
'status': row['status'],
'days': days
\})
if stage_durations:
dur_df = pd.DataFrame(stage_durations)
avg_by_status = dur_df.groupby('status')['days'].mean().round(1)
fig_duration = px.bar(
x=avg_by_status.index,
y=avg_by_status.values,
title="Temps moyen par statut (jours)",
labels=\{'x': 'Statut', 'y': 'Jours'\}
)
st.plotly_chart(fig_duration, use_container_width=True)
# === Tableau détaillé ===
with st.expander("📋 Toutes les candidatures"):
display_df = df[['company', 'position', 'status', 'location', 'salary_min', 'created_at']].copy()
display_df.columns = ['Entreprise', 'Poste', 'Statut', 'Lieu', 'Salaire min', 'Date']
st.dataframe(display_df, use_container_width=True)
Intégration dans l'app principale
# Dans app.py — Ajouter la page Dashboard
from dashboard import render_dashboard
# Dans le routeur de pages :
if page == "📊 Dashboard":
render_dashboard()
Plotly vs Matplotlib dans Streamlit
Plotly est préféré dans Streamlit car les graphiques sont interactifs (zoom, hover, export). Matplotlib produit des images statiques. Pour un dashboard, l'interactivité fait une vraie différence — Marc peut survoler un point pour voir les détails.
🏋️ Exercice pratique (25 minutes)
- →Ajoutez
dashboard.pyà votre Job Tracker - →Installez Plotly :
pip install plotly - →Ajoutez au moins 10 candidatures avec différents statuts
- →Naviguez vers le Dashboard et analysez vos métriques
- →Bonus : ajoutez un graphique de salaire moyen par statut
Quel graphique est le plus utile pour voir le taux de conversion des candidatures ?
Pourquoi utiliser Plotly plutôt que Matplotlib dans un dashboard Streamlit ?