All guides
IntermediateSelf-pacedFree GuideSection 39/95

Mini-projet — Dashboard d'analyse de candidatures

Practise development foundations: programming, web, APIs, data and AI tools. Work through sections and exercises to build your first projects.

The full lessons for this course are currently available in French. Use the section menu to navigate the French course.

💡

Free content, available without an account. Tools and models change: use the current model guide to check their versions. Models and sources

Section 11.2.19 : Mini-projet — Dashboard d'analyse de candidatures

🎯 Objectif pédagogique

Intégrer toutes les compétences de la Semaine 2 dans un mini-projet complet : charger des données, les nettoyer, les analyser et les visualiser dans un dashboard interactif. Vous construirez un Job Search Analytics Dashboard — un outil pratique que Marc utilise vraiment.


Le projet : votre outil d'analyse personnel

Marc a 200+ candidatures dans sa base PostgreSQL. Il veut un dashboard qui répond à ces questions :

  1. →Quel est mon taux de conversion par étape ?
  2. →Quels secteurs/entreprises répondent le mieux ?
  3. →Combien de candidatures par semaine pour un objectif de 3 offres/mois ?
  4. →Quel est le salaire médian par statut ?

Architecture du projet

job-analytics/
├── data/
│   └── applications.csv        # Données source
├── notebooks/
│   └── analysis.ipynb          # Exploration (Jupyter)
├── src/
│   ├── data_loader.py          # Chargement et nettoyage
│   ├── analytics.py            # Fonctions d'analyse
│   └── dashboard.py            # Génération du dashboard
├── output/
│   └── dashboard.html          # Rapport final
├── requirements.txt
└── README.md

Étape 1 : data_loader.py

"""Chargement et nettoyage des données de candidatures."""
import pandas as pd
import numpy as np

def load_applications(filepath="data/applications.csv"):
    """Charge et nettoie les données de candidatures."""
    df = pd.read_csv(filepath)
    
    # Nettoyage
    df['company'] = df['company'].str.strip().str.title()
    df['status'] = df['status'].str.lower().str.strip()
    df['date'] = pd.to_datetime(df['date'], errors='coerce')
    df['salary'] = pd.to_numeric(df['salary'], errors='coerce')
    
    # Supprimer les doublons
    df = df.drop_duplicates(subset=['company', 'position', 'date'])
    
    # Colonnes dérivées
    df['week'] = df['date'].dt.isocalendar().week
    df['month'] = df['date'].dt.to_period('M')
    df['day_of_week'] = df['date'].dt.day_name()
    
    return df

def generate_sample_data(n=100):
    """Génère des données fictives réalistes."""
    np.random.seed(42)
    
    companies = ['Google', 'Meta', 'Stripe', 'Datadog', 'Mistral AI', 'OVH', 
                 'Vercel', 'BNP Paribas', 'Doctolib', 'Alan', 'Qonto', 'Swile',
                 'Deezer', 'Criteo', 'ContentSquare', 'Algolia', 'Ledger']
    positions = ['Frontend Developer', 'Backend Python', 'Full-Stack', 'Data Analyst',
                 'ML Engineer', 'DevOps', 'Product Manager', 'UX Designer']
    statuses = ['applied', 'interview', 'offer', 'rejected', 'no_response']
    
    data = \{
        'company': np.random.choice(companies, n),
        'position': np.random.choice(positions, n),
        'status': np.random.choice(statuses, n, p=[0.35, 0.15, 0.05, 0.20, 0.25]),
        'salary': np.random.normal(65000, 18000, n).astype(int).clip(35000, 120000),
        'date': pd.date_range('2026-01-01', periods=n, freq='2D')[:n]
    \}
    
    df = pd.DataFrame(data)
    df.to_csv('data/applications.csv', index=False)
    return df

Étape 2 : analytics.py

"""Fonctions d'analyse pour le dashboard de candidatures."""
import pandas as pd

def conversion_funnel(df):
    """Calcule le funnel de conversion."""
    total = len(df)
    responded = len(df[df['status'] != 'no_response'])
    interviews = len(df[df['status'].isin(['interview', 'offer'])])
    offers = len(df[df['status'] == 'offer'])
    
    return \{
        'total': total,
        'responded': responded,
        'response_rate': responded / total * 100,
        'interviews': interviews,
        'interview_rate': interviews / total * 100,
        'offers': offers,
        'offer_rate': offers / total * 100
    \}

def weekly_stats(df):
    """Statistiques hebdomadaires."""
    return df.groupby('week').agg(
        applications=('company', 'count'),
        interviews=('status', lambda x: (x == 'interview').sum()),
        offers=('status', lambda x: (x == 'offer').sum()),
        avg_salary=('salary', 'mean')
    ).round(0)

def company_performance(df):
    """Performance par entreprise."""
    return df.groupby('company').agg(
        total=('status', 'count'),
        interviews=('status', lambda x: (x.isin(['interview', 'offer'])).sum()),
        avg_salary=('salary', 'mean')
    ).assign(
        conversion=lambda x: (x['interviews'] / x['total'] * 100).round(1)
    ).sort_values('conversion', ascending=False)

def salary_analysis(df):
    """Analyse des salaires par statut."""
    return df.groupby('status')['salary'].agg(['mean', 'median', 'std', 'count']).round(0)

Étape 3 : dashboard.py

"""Génération du dashboard visuel."""
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from data_loader import load_applications, generate_sample_data
from analytics import conversion_funnel, weekly_stats, company_performance, salary_analysis

def create_dashboard():
    # Charger les données
    try:
        df = load_applications()
    except FileNotFoundError:
        df = generate_sample_data()
    
    funnel = conversion_funnel(df)
    
    # Dashboard 6 graphiques
    fig, axes = plt.subplots(2, 3, figsize=(20, 12))
    fig.suptitle("Job Search Analytics Dashboard — Mars 2026", fontsize=18, fontweight='bold')
    
    # 1. Funnel de conversion
    stages = ['Candidatures', 'Réponses', 'Entretiens', 'Offres']
    values = [funnel['total'], funnel['responded'], funnel['interviews'], funnel['offers']]
    colors = ['#94a3b8', '#3b82f6', '#f59e0b', '#22c55e']
    axes[0,0].barh(stages[::-1], values[::-1], color=colors[::-1])
    for i, (s, v) in enumerate(zip(stages[::-1], values[::-1])):
        axes[0,0].text(v + 1, i, str(v), va='center', fontweight='bold')
    axes[0,0].set_title(f"Funnel : \{funnel['offer_rate']:.1f\}% de conversion")
    
    # 2. Candidatures par semaine
    weekly = df.groupby(df['date'].dt.isocalendar().week).size()
    axes[0,1].plot(weekly.index, weekly.values, marker='o', color='#0891b2', linewidth=2)
    axes[0,1].fill_between(weekly.index, weekly.values, alpha=0.2, color='#0891b2')
    axes[0,1].axhline(weekly.mean(), color='red', linestyle='--', alpha=0.7, label=f'Moyenne: \{weekly.mean():.0f\}/sem')
    axes[0,1].legend()
    axes[0,1].set_title("Rythme de candidature")
    
    # 3. Distribution des salaires
    for status in ['interview', 'offer', 'applied']:
        subset = df[df['status'] == status]['salary'].dropna()
        if len(subset):
            axes[0,2].hist(subset, bins=15, alpha=0.5, label=status)
    axes[0,2].legend()
    axes[0,2].set_title("Distribution des salaires par statut")
    
    # 4. Top entreprises
    top = company_performance(df).head(8)
    axes[1,0].barh(top.index, top['conversion'], color='#0891b2')
    axes[1,0].set_title("Top entreprises (taux conversion %)")
    axes[1,0].set_xlabel("%")
    
    # 5. Répartition par statut
    status_counts = df['status'].value_counts()
    colors_pie = \{'applied':'#3b82f6','interview':'#f59e0b','offer':'#22c55e','rejected':'#ef4444','no_response':'#94a3b8'\}
    axes[1,1].pie(status_counts.values, labels=status_counts.index, 
                  colors=[colors_pie.get(s, '#94a3b8') for s in status_counts.index],
                  autopct='%1.0f%%', startangle=90)
    axes[1,1].set_title("Répartition par statut")
    
    # 6. Salaire moyen par jour de la semaine
    day_salary = df.groupby('day_of_week')['salary'].median()
    day_order = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
    day_salary = day_salary.reindex(day_order).dropna()
    axes[1,2].bar(range(len(day_salary)), day_salary.values, color='#8b5cf6')
    axes[1,2].set_xticks(range(len(day_salary)))
    axes[1,2].set_xticklabels([d[:3] for d in day_salary.index], rotation=45)
    axes[1,2].set_title("Salaire médian par jour de candidature")
    
    plt.tight_layout(rect=[0, 0.03, 1, 0.95])
    
    # Insight en bas
    fig.text(0.5, 0.01, 
             f"💡 Insight : \{funnel['offer_rate']:.1f\}% de conversion | "
             f"Salaire médian entretiens : \{df[df['status']=='interview']['salary'].median():,.0f\}€ | "
             f"Meilleur jour : \{day_salary.idxmax()\}", 
             ha='center', fontsize=11, fontstyle='italic')
    
    plt.savefig('output/dashboard.png', dpi=200, bbox_inches='tight')
    plt.show()
    print("\n✅ Dashboard sauvegardé dans output/dashboard.png")

if __name__ == "__main__":
    create_dashboard()

🏋️ Exercice final — Semaine 2 (45 minutes)

Construisez votre propre dashboard :

  1. →Générez ou récupérez vos données de candidatures (CSV ou PostgreSQL)
  2. →Nettoyez (pipeline complet : types, NaN, doublons, texte)
  3. →Analysez : funnel de conversion, top entreprises, stats salariales
  4. →Visualisez : minimum 4 graphiques avec titres = insights
  5. →Documentez : un Jupyter Notebook avec Markdown entre les sections
  6. →Exportez : dashboard en PNG + notebook en HTML

Bonus : ajoutez une analyse de tendance temporelle et une recommandation chiffrée.

🎉 Semaine 2 terminée !

En une semaine, Marc est passé de "je sais copier un tableau Excel" à :

  • →✅ Requêter des bases SQL (SELECT, JOIN, CRUD)
  • →✅ Configurer un environnement Python pro
  • →✅ Manipuler des données avec NumPy et pandas
  • →✅ Créer des visualisations avec Matplotlib et Seaborn
  • →✅ Scraper le web et charger des données Open Data
  • →✅ Produire un dashboard d'analyse complet

La Semaine 3 appliquera ces compétences à l'automatisation — scripts qui tournent tout seuls, workflows, et intégration IA.

Quick Check

Quel est l'ordre correct pour un projet data complet ?

Quick Check

Pourquoi créer un dashboard plutôt qu'un simple tableau ?

Quick Check

Dans le funnel de conversion (45 candidatures → 12 entretiens → 3 offres), quel est le plus gros goulot d'étranglement ?

Your progress

Progress is self-reported and stored in your browser. The final completion record is not a qualification or an independent assessment.

View progress and completion record

Learn AI, from prompts to agents

15 free guidesQuizzes, diagrams and workshopsNo account needed

One practical AI workflow in your inbox

Methods, resources and new exercises. Free, unsubscribe at any time.