1. Genre
2. Thématiques de recherche : la priorité sera donnée à une représentation équilibrée des différentes thématiques de recherche, en s’appuyant sur les catégories du Conseil National des Universités (CNU). Cela garantit que toutes les disciplines, des sciences exactes aux sciences humaines et sociales, sont équitablement représentées.
3. Profil professionnel : les candidat·e·s, titulaires ou en cours de doctorat, seront sélectionné·e·s pour inclure un équilibre entre le secteur public et le secteur privé, ainsi que des personnes ayant poursuivi une carrière dans la recherche ou ayant exploré d’autres secteurs.
4. Diversité géographique : la sélection prendra en compte la diversité géographique en s’appuyant sur la région d’origine, la localisation de l’école doctorale et la région d’emploi des candidat·e·s.
5. Expérience professionnelle : l’objectif sera de refléter la diversité des niveaux d’expérience professionnelle, que ce soit dans la recherche ou dans d’autres secteurs.
Ce bloc importe les bibliothèques nécessaires au fonctionnement de l'outil
pandas
(pd) : manipulation de données tabulaires, filtres, agrégations, lectures/écritures de fichiers, etcio
: gestion des flux d’entrée/sortie en mémoire (lire un fichier téléversé par exemple).numpy
(np) : calcul numérique, génération de valeurs aléatoirescollections.Counter
: comptage rapide de la fréquence d’élémentsCe bloc importe les bibliothèques nécessaires au fonctionnement de l'outil
math
: fonctions mathématiquesrandom
: génération de valeurs et tirages aléatoirespulp
: modélisation et résolution de problèmes d’optimisation linéaire ou entièresklearn.preprocessing.LabelEncoder
: encodage numérique de catégories textuelles (pour traitement algorithmique)matplotlib.pyplot
: création de graphiquesmatplotlib.gridspec
: gestion des sous-graphesmatplotlib.colors
: manipulation des couleursseaborn
: visualisation esthétiqueipywidgets
: création de widgets interactifs (sliders, menus, boutons)ipywidgets.X
:Layout
: style et disposition des widgetsinteract
: interface interactive pour une fonctionIntSlider
: curseur pour sélectionner un entierIPython.display.X
:display
: affichage d’objets (DataFrames, widgets, HTML)clear_output
: effacement dynamique affichagetqdm.notebook
: barre de progression interactiveCode
1# Librairies nécessaires
2import pandas as pd
3import io
4import numpy as np
5from collections import Counter
6import random
7from sklearn.preprocessing import LabelEncoder
8import matplotlib.pyplot as plt
9from matplotlib import gridspec
10import seaborn as sns
11import ipywidgets as widgets
12from ipywidgets import Layout
13from IPython.display import display, clear_output
14from tqdm.notebook import tqdm
Ce bloc lit les réponses à l'appel à participation depuis le CSV déjà stocké dans le dossier spécifié.
Code
1# Lecture du fichier avec en-tête correct (3e ligne)
2df = pd.read_csv("../data/appel_participation.csv", header=2)
Ce bloc permet de réaliser un premier filtrage basé sur le nombre de disponibilités aux week-ends de la Convention.
Un tableau synthétique permettant de mesurer l'ampleur du filtrage est affiché et modifié dynamiquement selon le filtrage retenu.
À ce stade, 180 candidatures sont disponibles au moins 3 week-ends.
Code
1# Nettoyage des noms de colonnes
2df.columns = (
3 df.columns
4 .str.strip()
5 .str.replace('\n', ' ', regex=False)
6 .str.replace('\r', '', regex=False)
7 .str.replace(' +', ' ', regex=True)
8)
9
10# Colonnes de disponibilité détectées automatiquement
11dispo_cols = [col for col in df.columns if any(mois in col.lower() for mois in ["octobre", "novembre", "décembre", "janvier"])]
12
13print("Colonnes de disponibilité détectées :")
14for col in dispo_cols:
15 print("-", col)
16
17# Ajout d'une colonne avec le nombre total de WE disponibles
18df["Nb_dispos"] = df[dispo_cols].apply(lambda row: sum("oui" in str(val).lower() for val in row), axis=1)
19
20# Slider interactif pour définir le seuil
21seuil_widget = widgets.IntSlider(
22 value=4,
23 min=1,
24 max=len(dispo_cols),
25 step=1,
26 description='WE dispos:',
27 continuous_update=False,
28 layout=Layout(width='500px')
29)
30
31# Zone d’affichage dynamique
32output = widgets.Output()
33
34# Fonction de filtrage + affichage dynamique
35def update_table(_change):
36 seuil = seuil_widget.value
37 df_filtre = df[df["Nb_dispos"] >= seuil].reset_index(drop=True)
38
39 with output:
40 clear_output(wait=True)
41 print(f"🎯 {len(df_filtre)} participant·es ont au moins {seuil} week-ends de disponibilité :")
42 display(df_filtre[["Séquentiel", "SID", "Nb_dispos"] + dispo_cols])
43
44# Lier l’interaction au slider
45seuil_widget.observe(update_table, names='value')
46
47# Afficher les widgets
48display(seuil_widget, output)
49
50# Initialiser l'affichage
51update_table(None)
À partir du filtrage, ce bloc extrait et structure les informations nécessaires au tirage au sort :
Code
1# On part du DataFrame filtré à l’étape précédente
2# On peut sauvegarder df_filtre depuis le bloc[Convention scientifique IESF] Proposition d'intervention 1 dans une variable globale
3df_selection = df[df["Nb_dispos"] >= seuil_widget.value].copy()
4
5# Genre – regroupement en 3 catégories simples
6genre_cols = ["Une femme", "Un homme", "Autre", "Je préfère ne pas le dire"]
7def get_genre(row):
8 if row["Une femme"] == "X":
9 return "Femme"
10 elif row["Un homme"] == "X":
11 return "Homme"
12 elif row["Autre"] == "X":
13 return "Autre"
14 else:
15 return "Non spécifié"
16df_selection["Genre"] = df_selection.apply(get_genre, axis=1)
17
18# Thématiques CNU – booléen par thématique
19# On extrait dynamiquement les colonnes entre les deux libellés connus
20theme_start = "Droit privé et sciences criminelles"
21theme_end = "Sciences biologiques pharmaceutiques"
22theme_mask = df.columns.to_list()
23start_idx = theme_mask.index(theme_start)
24end_idx = theme_mask.index(theme_end)
25theme_cols = theme_mask[start_idx:end_idx + 1]
26
27# Création d'une colonne 'Thématiques' avec la liste des thèmes sélectionnés
28df_selection["Thématiques"] = df_selection[theme_cols].apply(
29 lambda row: [col for col in theme_cols if str(row[col]).strip().upper() == "X"],
30 axis=1
31)
32
33# Profil professionnel – en simplifiant la colonne "Dans quel secteur travaillez-vous ?"
34df_selection["Secteur"] = df_selection["Dans quel secteur travaillez-vous ?"].fillna("Non spécifié")
35
36# Diversité géographique – regrouper 3 colonnes
37geo_cols = {
38 "Département origine": "Quel est le département dans lequel vous avez grandi ?",
39 "Département thèse": "Quel est le département de votre école doctorale ?",
40 "Département emploi": "Quel est votre département d'emploi ?"
41}
42for col_nom, source in geo_cols.items():
43 df_selection[col_nom] = df_selection[source].fillna("Non précisé").astype(str).str.strip()
44
45# Expérience – année de soutenance (ou prévue)
46df_selection["Année soutenance"] = df_selection[
47 "En quelle année avez-vous soutenu votre thèse ou prévoyez-vous de la soutenir si vous êtes encore doctorant(e) ?"
48].replace(" ", np.nan).fillna("Non précisé")
49
50# Aperçu rapide des variables extraites
51df_selection_small = df_selection[
52 ["Séquentiel", "SID", "Genre", "Thématiques", "Secteur"] +
53 list(geo_cols.keys()) + ["Année soutenance"]
54]
55
56print(f"🎯 Données des {len(df_selection_small)} participant·es sélectionné·es pour le tirage au sort:")
57display(df_selection_small.head(10))
Ce bloc trace un ensemble de figures permettant de visualiser la répartition des candidat·es pré-sélectionné·es pour le tirage au sort :
Code
1# Fonction pour couper les labels trop longs
2def truncate_label(label, max_char=50):
3 return label if len(label) <= max_char else label[:max_char - 3] + "..."
4
5# Style et couleurs
6palette = sns.color_palette("colorblind")
7sns.set_theme(style="whitegrid")
8alpha_bg = 0.9 # plus visible maintenant que le panel n'est plus présent
9colors = palette[:10]
10max_char = 50
11
12# Préparation de la figure
13fig = plt.figure(figsize=(18, 26), facecolor='white')
14fig.suptitle("Diversité pré-sélection", fontsize=18, weight='bold')
15
16# Grille personnalisée
17gs = gridspec.GridSpec(5, 4, height_ratios=[1, 1, 1, 1, 1], hspace=0.8, wspace=0.5)
18
19# Petits graphiques (ligne 0)
20ax1 = fig.add_subplot(gs[0, 0]) # Genre
21ax2 = fig.add_subplot(gs[0, 1]) # Nb thématiques
22ax3 = fig.add_subplot(gs[0, 2]) # Secteur
23ax4 = fig.add_subplot(gs[0, 3]) # Année soutenance
24
25# Grands graphiques (colonnes 1 et 3, lignes 1 à 4)
26ax5 = fig.add_subplot(gs[1:3, 1]) # Thématiques
27ax6 = fig.add_subplot(gs[1:3, 3]) # Département origine
28ax7 = fig.add_subplot(gs[3:5, 1]) # Département thèse
29ax8 = fig.add_subplot(gs[3:5, 3]) # Département emploi
30
31# 1. Genre
32sns.countplot(data=df_selection, x="Genre", ax=ax1, color=colors[0], alpha=alpha_bg)
33ax1.set_title("Répartition par genre")
34ax1.tick_params(axis='x', rotation=45)
35ax1.set(xlabel=None, ylabel="Décompte")
36
37# 2. Nombre de thématiques cochées
38df_selection["Nb_thématiques"] = df_selection["Thématiques"].apply(len)
39bins = range(1, df_selection["Nb_thématiques"].max() + 2)
40sns.histplot(df_selection["Nb_thématiques"], bins=bins, ax=ax2, color=colors[1], alpha=alpha_bg)
41ax2.set_title("Nombre de thématiques cochées par candidat·e")
42ax2.set(xlabel="Nombre de thématiques", ylabel="Décompte")
43
44# 3. Secteur
45sns.countplot(data=df_selection, x="Secteur", ax=ax3, color=colors[2], alpha=alpha_bg)
46ax3.set_title("Répartition par secteur d'activité")
47ax3.tick_params(axis='x', rotation=45)
48ax3.set(xlabel=None, ylabel="Décompte")
49
50# 4. Année de soutenance
51try:
52 df_years = df_selection["Année soutenance"].dropna().astype(int).astype(str)
53 all_years = sorted(df_years.unique())
54 sns.countplot(x=df_years, order=all_years, ax=ax4, color=colors[3], alpha=alpha_bg)
55 ax4.set_title("Répartition par année de soutenance")
56 ax4.tick_params(axis='x', rotation=45)
57 interval = 5
58 tick_positions = np.arange(len(all_years))
59 tick_labels = [year if i % interval == 0 else '' for i, year in enumerate(all_years)]
60 ax4.set_xticks(tick_positions)
61 ax4.set_xticklabels(tick_labels, rotation=45)
62 ax4.set(xlabel=None, ylabel="Décompte")
63except Exception as e:
64 ax4.axis("off")
65 ax4.text(0.5, 0.5, f"Année de soutenance non exploitable\n({str(e)})", ha='center', va='center')
66
67# 5. Thématiques
68all_thematiques = list(set(t for sub in df_selection["Thématiques"] for t in sub))
69df_counts = Counter([t for sub in df_selection["Thématiques"] for t in sub])
70all_thematiques_sorted = sorted(all_thematiques, key=lambda x: df_counts.get(x, 0), reverse=True)
71trunc_labels = [truncate_label(l, max_char) for l in all_thematiques_sorted]
72df_vals = [df_counts.get(l, 0) for l in all_thematiques_sorted]
73ax5.barh(trunc_labels, df_vals, color=colors[4], alpha=alpha_bg)
74ax5.set_title("Thématiques CNU sélectionnées")
75ax5.invert_yaxis()
76ax5.set_xlabel("Nombre de candidat·es")
77ax5.set(ylabel=None)
78
79# 6. Département origine
80all_depts = df_selection["Département origine"].dropna().unique()
81df_vals_dept = df_selection["Département origine"].value_counts()
82all_depts_sorted = df_vals_dept.index.tolist()
83ax6.barh(all_depts_sorted, df_vals_dept.values, color=colors[7], alpha=alpha_bg)
84ax6.set_title("Départements d'origine")
85ax6.invert_yaxis()
86ax6.set_xlabel("Nombre de candidat·es")
87ax6.set(ylabel=None)
88
89# 7. Département thèse
90all_th = df_selection["Département thèse"].dropna().unique()
91df_vals_th = df_selection["Département thèse"].value_counts()
92all_th_sorted = df_vals_th.index.tolist()
93ax7.barh(all_th_sorted, df_vals_th.values, color=colors[8], alpha=alpha_bg)
94ax7.set_title("Département thèse")
95ax7.invert_yaxis()
96ax7.set_xlabel("Nombre de candidat·es")
97ax7.set(ylabel=None)
98
99# 8. Département emploi
100all_emp = df_selection["Département emploi"].dropna().unique()
101df_vals_emp = df_selection["Département emploi"].value_counts()
102all_emp_sorted = df_vals_emp.index.tolist()
103ax8.barh(all_emp_sorted, df_vals_emp.values, color=colors[9], alpha=alpha_bg)
104ax8.set_title("Département emploi")
105ax8.invert_yaxis()
106ax8.set_xlabel("Nombre de candidat·es")
107ax8.set(ylabel=None)
108
109# Bordures noires
110for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8]:
111 for spine in ax.spines.values():
112 spine.set_edgecolor('black')
113 spine.set_linewidth(1.2)
114
115plt.show()
Ce bloc permet de lancer un grand nombre de tirages aléatoires de panels et de garder celui qui maximise un score de diversité pondéré sélectionné.
Structure générale :
N_TIRS
, TAILLE_PANEL
, geo_mode
(géographie simplifiée ou complète) et les pondérations des différents critères ; un bouton run_button
(lancer la sélection) ; un output pour capturer l’affichage.LabelEncoder
) construits à partir de df_selection
(pré-sélection à partir du nombre de week-ends disponibles) pour convertir les catégories en codes numériques (secteur, départements, année).diversite_score
: calcule un score de diversité pour un DataFrame (tableau de données) de candidat·es selon les cinq critères mentionnés précédemment (genre, thématiques, secteur, géographie, expérience) puis combine ces scores pondérés.lancer_tirage
(attachée au bouton) :N_TIRS
tirages aléatoires de taille TAILLE_PANEL
diversite_score
pour chaque tiragebest_panel
Code
1## Définition des Widgets pour l'interactivité
2n_tirs_widget = widgets.IntSlider(value=5000, min=100, max=10000, step=100, description="🎲 Tirages")
3panel_size_widget = widgets.IntSlider(value=50, min=10, max=100, step=1, description="👥 Panel")
4
5geo_mode_widget = widgets.Dropdown(
6 options=["simplifiée", "complète"],
7 value="simplifiée",
8 description="🌍 Géographie"
9)
10
11weights_widgets = {
12 "genre": widgets.IntSlider(value=5, min=0, max=10, description="Genre"),
13 "thématiques": widgets.IntSlider(value=4, min=0, max=10, description="Thématiques"),
14 "secteur": widgets.IntSlider(value=3, min=0, max=10, description="Secteur"),
15 "géographie": widgets.IntSlider(value=2, min=0, max=10, description="Géographie"),
16 "expérience": widgets.IntSlider(value=1, min=0, max=10, description="Expérience")
17}
18
19run_button = widgets.Button(description="🚀 Lancer le tirage", button_style='success')
20
21output = widgets.Output()
22
23## Encodage
24# On fait un fit de plusieurs LabelEncoder sur df_selection (les classes uniques sont mémorisées).
25# fillna("Inconnu") garantit qu’il n’y ait pas de NaN au fit.
26# Remarques :
27# Si une colonne indiquée n’existe pas, .fit(...) lèvera une erreur — vérifie la présence des colonnes.
28# Ces encodeurs permettent ensuite de transformer une sous-sélection (tirage) en codes entiers et compter facilement les catégories (avec set(...)).
29
30le_secteur = LabelEncoder().fit(df_selection["Secteur"].fillna("Inconnu"))
31le_origine = LabelEncoder().fit(df_selection["Département origine"].fillna("Inconnu"))
32le_these = LabelEncoder().fit(df_selection["Année soutenance"].fillna(""))
33
34# Encodages pour mode "complet"
35le_geo_origine = LabelEncoder().fit(df_selection["Département origine"].fillna("Inconnu"))
36le_geo_these = LabelEncoder().fit(df_selection["Département thèse"].fillna("Inconnu"))
37le_geo_emploi = LabelEncoder().fit(df_selection["Département emploi"].fillna("Inconnu"))
38
39## Fonction de score
40
41def diversite_score(candidats: pd.DataFrame, weights: dict, geo_mode: str) -> float:
42# Entrées : candidats (DataFrame du tirage), weights (dict des pondérations), geo_mode (string).
43# Retour : un score numérique (plus élevé → panel jugé « plus diversifié » selon la combinaison choisie).
44
45 # 1. Genre
46 # Récupérer la proportion par modalité
47 genre_counts = candidats["Genre"].value_counts(normalize=True)
48 # Proportion catégorie "Une femme", 0 si absente
49 p_femme = genre_counts.get("Une femme", 0)
50 # Score de 1 si proportion femme = 0.5 (parité), 0 si un seul genre
51 genre_score = 1 - abs(p_femme - 0.5) * 2
52
53 # 2. Thématiques
54 # Aplatissement listes de thématiques pour chaque candidat·e
55 all_thematiques = [t for sublist in candidats["Thématiques"] for t in sublist]
56 # Nb thématiques distinctes dans le tirage
57 nb_thematiques_uniques = len(set(all_thematiques))
58 # Ratio mesurant la diversité
59 thematiques_score = nb_thematiques_uniques / len(all_thematiques)
60
61 # 3. Secteur
62 # Transforme le secteur en code
63 secteur_codes = le_secteur.transform(candidats["Secteur"].fillna("Inconnu"))
64 # Compte combien de codes différents sont présents
65 # Normalise par le nombre total de classes connues (public/privé/other)
66 secteur_score = len(set(secteur_codes)) / len(le_secteur.classes_)
67
68 # 4. Géographie
69 # Simplifiée : Département origine normalisé par le nb de classes
70 if geo_mode == "simplifiée":
71 geo_codes = le_origine.transform(candidats["Département origine"].fillna("Inconnu"))
72 geo_score = len(set(geo_codes)) / len(le_origine.classes_)
73 # 3 scores calculés (origine / thèse / emploi), chacun normalisé, puis moyenne simple
74 else: # complète
75 geo_origine_codes = le_geo_origine.transform(candidats["Département origine"].fillna("Inconnu"))
76 geo_these_codes = le_geo_these.transform(candidats["Département thèse"].fillna("Inconnu"))
77 geo_emploi_codes = le_geo_emploi.transform(candidats["Département emploi"].fillna("Inconnu"))
78 geo_score = (
79 len(set(geo_origine_codes)) / len(le_geo_origine.classes_) +
80 len(set(geo_these_codes)) / len(le_geo_these.classes_) +
81 len(set(geo_emploi_codes)) / len(le_geo_emploi.classes_)
82 ) / 3
83
84 # 5. Expérience
85 # Transforme l’année de soutenance en code
86 annee_codes = le_these.transform(candidats["Année soutenance"].fillna(""))
87 # Compte combien de codes différents sont présents
88 # Normalise par le nombre total de classes connues (total d'années différentes)
89 exp_score = len(set(annee_codes)) / len(le_these.classes_)
90
91 # Score total pondéré
92 # Des poids plus élevés favorisent le critère correspondant
93 total = (
94 genre_score * weights["genre"] +
95 thematiques_score * weights["thématiques"] +
96 secteur_score * weights["secteur"] +
97 geo_score * weights["géographie"] +
98 exp_score * weights["expérience"]
99 )
100 return total
101
102## Fonction de tirage
103def lancer_tirage(b):
104 global best_panel # <-- permet d'utiliser best_panel partout
105 with output:
106 clear_output()
107 print("🔁 Démarrage du tirage optimisé...")
108
109 # Récupération de N_TIRS, TAILLE_PANEL, geo_mode.
110 N_TIRS = n_tirs_widget.value
111 TAILLE_PANEL = panel_size_widget.value
112 geo_mode = geo_mode_widget.value
113 # Dictionnaire d’entiers créé à partir des valeurs actuelles des sliders
114 weights = {k: w.value for k, w in weights_widgets.items()}
115
116 # Intitialisations
117 # best_score initialisé bas pour être battu
118 best_score = -1
119 # Stockage du meilleur tirage
120 best_panel = None
121 # barre de progression pour visualiser l’avancement
122 pbar = tqdm(total=N_TIRS)
123
124 # À chaque itération, on prélève au hasard TAILLE_PANEL lignes de df_selection
125 # Variation de l’aléa par random_state=random.randint(...)
126 # Reproduction possible si on fixe la seed du module random
127 for _ in range(N_TIRS):
128 tirage = df_selection.sample(n=TAILLE_PANEL, random_state=random.randint(0, 999999))
129 # On calcule diversite_score sur tirage
130 score = diversite_score(tirage, weights, geo_mode)
131 # Si score meilleur que précédemment, on remplace best_panel et best_score
132 if score > best_score:
133 best_score = score
134 best_panel = tirage
135 pbar.update(1)
136 pbar.close()
137
138 print(f"🎯 Score optimal obtenu : {best_score:.3f}")
139
140 # # Aperçu rapide des variables extraites
141 # best_panel_small = best_panel[
142 # ["Séquentiel", "SID", "Genre", "Thématiques", "Secteur"] +
143 # list(geo_cols.keys()) + ["Année soutenance"]
144 # ]
145 # display(best_panel_small.reset_index(drop=True))
146 # display(best_panel)
147
148# Lier bouton et tirage
149run_button.on_click(lancer_tirage)
150
151# Interface
152ui = widgets.VBox([
153 widgets.HBox([n_tirs_widget, panel_size_widget]),
154 geo_mode_widget,
155 widgets.Label("⚖️ Pondérations :"),
156 widgets.HBox([weights_widgets["genre"], weights_widgets["thématiques"]]),
157 widgets.HBox([weights_widgets["secteur"], weights_widgets["géographie"], weights_widgets["expérience"]]),
158 run_button,
159 output
160])
161
162display(ui)
Ce bloc trace un ensemble de figures permettant de visualiser la répartition des personnes tirées au sort :
Code
1# Fonction pour couper les labels trop longs
2def truncate_label(label, max_char=50):
3 return label if len(label) <= max_char else label[:max_char - 3] + "..."
4
5# Style et couleurs
6palette = sns.color_palette("colorblind")
7sns.set_theme(style="whitegrid")
8alpha_bg = 0.9 # plus visible maintenant que le panel n'est plus présent
9colors = palette[:10]
10max_char = 50
11
12# Préparation de la figure
13fig = plt.figure(figsize=(18, 26), facecolor='white')
14fig.suptitle("Diversité après tirage au sort", fontsize=18, weight='bold')
15
16# Grille personnalisée
17gs = gridspec.GridSpec(5, 4, height_ratios=[1, 1, 1, 1, 1], hspace=0.8, wspace=0.5)
18
19# Petits graphiques (ligne 0)
20ax1 = fig.add_subplot(gs[0, 0]) # Genre
21ax2 = fig.add_subplot(gs[0, 1]) # Nb thématiques
22ax3 = fig.add_subplot(gs[0, 2]) # Secteur
23ax4 = fig.add_subplot(gs[0, 3]) # Année soutenance
24
25# Grands graphiques (colonnes 1 et 3, lignes 1 à 4)
26ax5 = fig.add_subplot(gs[1:3, 1]) # Thématiques
27ax6 = fig.add_subplot(gs[1:3, 3]) # Département origine
28ax7 = fig.add_subplot(gs[3:5, 1]) # Département thèse
29ax8 = fig.add_subplot(gs[3:5, 3]) # Département emploi
30
31# 1. Genre
32sns.countplot(data=best_panel, x="Genre", ax=ax1, color=colors[0], alpha=alpha_bg)
33ax1.set_title("Répartition par genre")
34ax1.tick_params(axis='x', rotation=45)
35ax1.set(xlabel=None, ylabel="Décompte")
36
37# 2. Nombre de thématiques cochées
38best_panel["Nb_thématiques"] = best_panel["Thématiques"].apply(len)
39bins = range(1, best_panel["Nb_thématiques"].max() + 2)
40sns.histplot(best_panel["Nb_thématiques"], bins=bins, ax=ax2, color=colors[1], alpha=alpha_bg)
41ax2.set_title("Nombre de thématiques cochées par candidat·e")
42ax2.set(xlabel="Nombre de thématiques", ylabel="Décompte")
43
44# 3. Secteur
45sns.countplot(data=best_panel, x="Secteur", ax=ax3, color=colors[2], alpha=alpha_bg)
46ax3.set_title("Répartition par secteur d'activité")
47ax3.tick_params(axis='x', rotation=45)
48ax3.set(xlabel=None, ylabel="Décompte")
49
50# 4. Année de soutenance
51try:
52 df_years = best_panel["Année soutenance"].dropna().astype(int).astype(str)
53 all_years = sorted(df_years.unique())
54 sns.countplot(x=df_years, order=all_years, ax=ax4, color=colors[3], alpha=alpha_bg)
55 ax4.set_title("Répartition par année de soutenance")
56 ax4.tick_params(axis='x', rotation=45)
57 interval = 5
58 tick_positions = np.arange(len(all_years))
59 tick_labels = [year if i % interval == 0 else '' for i, year in enumerate(all_years)]
60 ax4.set_xticks(tick_positions)
61 ax4.set_xticklabels(tick_labels, rotation=45)
62 ax4.set(xlabel=None, ylabel="Décompte")
63except Exception as e:
64 ax4.axis("off")
65 ax4.text(0.5, 0.5, f"Année de soutenance non exploitable\n({str(e)})", ha='center', va='center')
66
67# 5. Thématiques
68all_thematiques = list(set(t for sub in best_panel["Thématiques"] for t in sub))
69df_counts = Counter([t for sub in best_panel["Thématiques"] for t in sub])
70all_thematiques_sorted = sorted(all_thematiques, key=lambda x: df_counts.get(x, 0), reverse=True)
71trunc_labels = [truncate_label(l, max_char) for l in all_thematiques_sorted]
72df_vals = [df_counts.get(l, 0) for l in all_thematiques_sorted]
73ax5.barh(trunc_labels, df_vals, color=colors[4], alpha=alpha_bg)
74ax5.set_title("Thématiques CNU sélectionnées")
75ax5.invert_yaxis()
76ax5.set_xlabel("Nombre de candidat·es")
77ax5.set(ylabel=None)
78
79# 6. Département origine
80all_depts = best_panel["Département origine"].dropna().unique()
81df_vals_dept = best_panel["Département origine"].value_counts()
82all_depts_sorted = df_vals_dept.index.tolist()
83ax6.barh(all_depts_sorted, df_vals_dept.values, color=colors[7], alpha=alpha_bg)
84ax6.set_title("Départements d'origine")
85ax6.invert_yaxis()
86ax6.set_xlabel("Nombre de candidat·es")
87ax6.set(ylabel=None)
88
89# 7. Département thèse
90all_th = best_panel["Département thèse"].dropna().unique()
91df_vals_th = best_panel["Département thèse"].value_counts()
92all_th_sorted = df_vals_th.index.tolist()
93ax7.barh(all_th_sorted, df_vals_th.values, color=colors[8], alpha=alpha_bg)
94ax7.set_title("Département thèse")
95ax7.invert_yaxis()
96ax7.set_xlabel("Nombre de candidat·es")
97ax7.set(ylabel=None)
98
99# 8. Département emploi
100all_emp = best_panel["Département emploi"].dropna().unique()
101df_vals_emp = best_panel["Département emploi"].value_counts()
102all_emp_sorted = df_vals_emp.index.tolist()
103ax8.barh(all_emp_sorted, df_vals_emp.values, color=colors[9], alpha=alpha_bg)
104ax8.set_title("Département emploi")
105ax8.invert_yaxis()
106ax8.set_xlabel("Nombre de candidat·es")
107ax8.set(ylabel=None)
108
109# Bordures noires
110for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8]:
111 for spine in ax.spines.values():
112 spine.set_edgecolor('black')
113 spine.set_linewidth(1.2)
114
115plt.show()
Il peut être utile de visualiser sur un même graphique la répartition avant et après tirage au sort. Ce bloc trace un ensemble de figures permettant de visualiser les différences entre population pré-sélectionnée et population tirée au sort :
Code
1# Fonction pour couper les labels trop longs
2def truncate_label(label, max_char=50):
3 return label if len(label) <= max_char else label[:max_char - 3] + "..."
4
5# Style et couleurs
6palette = sns.color_palette("colorblind")
7sns.set_theme(style="whitegrid")
8alpha_bg = 0.3
9alpha_fg = 0.9
10colors = palette[:10]
11max_char = 50
12
13# Préparation de la figure
14fig = plt.figure(figsize=(18, 26), facecolor='white')
15fig.suptitle("Comparaison entre candidat·es et panel tiré au sort", fontsize=18, weight='bold')
16
17# Grille personnalisée
18gs = gridspec.GridSpec(5, 4, height_ratios=[1, 1, 1, 1, 1], hspace=0.8, wspace=0.5)
19
20# Petits graphiques (ligne 0)
21ax1 = fig.add_subplot(gs[0, 0]) # Genre
22ax2 = fig.add_subplot(gs[0, 1]) # Nb thématiques
23ax3 = fig.add_subplot(gs[0, 2]) # Secteur
24ax4 = fig.add_subplot(gs[0, 3]) # Année soutenance
25
26# Grands graphiques (colonnes 1 et 3, lignes 1 à 4)
27ax5 = fig.add_subplot(gs[1:3, 1]) # Thématiques
28ax6 = fig.add_subplot(gs[1:3, 3]) # Département origine
29ax7 = fig.add_subplot(gs[3:5, 1]) # Département thèse
30ax8 = fig.add_subplot(gs[3:5, 3]) # Département emploi
31
32labels_shared = ["Ensemble des candidat·es", "Panel sélectionné"]
33
34# 1. Genre
35sns.countplot(data=df_selection, x="Genre", ax=ax1, color=colors[0], alpha=alpha_bg)
36sns.countplot(data=best_panel, x="Genre", ax=ax1, color=colors[0], alpha=alpha_fg)
37ax1.set_title("Répartition par genre")
38ax1.tick_params(axis='x', rotation=45)
39ax1.set(xlabel=None)
40ax1.set(ylabel="Décompte")
41
42# 2. Nombre de thématiques cochées
43df_selection["Nb_thématiques"] = df_selection["Thématiques"].apply(len)
44best_panel["Nb_thématiques"] = best_panel["Thématiques"].apply(len)
45bins = range(1, max(df_selection["Nb_thématiques"].max(), best_panel["Nb_thématiques"].max()) + 2)
46sns.histplot(df_selection["Nb_thématiques"], bins=bins, ax=ax2, color=colors[1], alpha=alpha_bg)
47sns.histplot(best_panel["Nb_thématiques"], bins=bins, ax=ax2, color=colors[1], alpha=alpha_fg)
48ax2.set_title("Nombre de thématiques cochées par candidat·e")
49ax2.set(xlabel="Nombre de thématiques")
50ax2.set(ylabel="Décompte")
51
52# 3. Secteur
53sns.countplot(data=df_selection, x="Secteur", ax=ax3, color=colors[2], alpha=alpha_bg)
54sns.countplot(data=best_panel, x="Secteur", ax=ax3, color=colors[2], alpha=alpha_fg)
55ax3.set_title("Répartition par secteur d'activité")
56ax3.tick_params(axis='x', rotation=45)
57ax3.set(xlabel=None)
58ax3.set(ylabel="Décompte")
59
60# 4. Année de soutenance
61try:
62 df_years = df_selection["Année soutenance"].dropna().astype(int).astype(str)
63 bp_years = best_panel["Année soutenance"].dropna().astype(int).astype(str)
64 all_years = sorted(set(df_years.unique()) | set(bp_years.unique()))
65 sns.countplot(x=df_years, order=all_years, ax=ax4, color=colors[3], alpha=alpha_bg)
66 sns.countplot(x=bp_years, order=all_years, ax=ax4, color=colors[3], alpha=alpha_fg)
67 ax4.set_title("Répartition par année de soutenance")
68 ax4.tick_params(axis='x', rotation=45)
69 interval = 5
70 tick_positions = np.arange(len(all_years))
71 tick_labels = [year if i % interval == 0 else '' for i, year in enumerate(all_years)]
72 ax4.set_xticks(tick_positions)
73 ax4.set_xticklabels(tick_labels, rotation=45)
74 ax4.set(xlabel=None)
75 ax4.set(ylabel="Décompte")
76except Exception as e:
77 ax4.axis("off")
78 ax4.text(0.5, 0.5, f"Année de soutenance non exploitable\n({str(e)})", ha='center', va='center')
79
80# 5. Toutes les thématiques (avec labels tronqués), triées par importance panel
81all_thematiques = list(set(t for sub in df_selection["Thématiques"] for t in sub) | set(t for sub in best_panel["Thématiques"] for t in sub))
82df_counts = Counter([t for sub in df_selection["Thématiques"] for t in sub])
83panel_counts = Counter([t for sub in best_panel["Thématiques"] for t in sub])
84all_thematiques_sorted = sorted(all_thematiques, key=lambda x: panel_counts.get(x, 0), reverse=True)
85trunc_labels = [truncate_label(l, max_char) for l in all_thematiques_sorted]
86df_vals = [df_counts.get(l, 0) for l in all_thematiques_sorted]
87panel_vals = [panel_counts.get(l, 0) for l in all_thematiques_sorted]
88ax5.barh(trunc_labels, df_vals, color=colors[4], alpha=alpha_bg)
89ax5.barh(trunc_labels, panel_vals, color=colors[4], alpha=alpha_fg)
90ax5.set_title("Thématiques CNU sélectionnées")
91ax5.invert_yaxis() # pour garder le label le plus important en haut
92ax5.set_xlabel("Nombre de candidat·es")
93ax5.set(ylabel=None)
94
95# 6. Tous les départements d'origine, triés par importance panel
96all_depts = list(set(df_selection["Département origine"].dropna()) | set(best_panel["Département origine"].dropna()))
97panel_counts_depts = best_panel["Département origine"].value_counts()
98all_depts_sorted = sorted(all_depts, key=lambda x: panel_counts_depts.get(x, 0), reverse=True)
99df_vals_dept = df_selection["Département origine"].value_counts().reindex(all_depts_sorted).fillna(0)
100panel_vals_dept = best_panel["Département origine"].value_counts().reindex(all_depts_sorted).fillna(0)
101ax6.barh(all_depts_sorted, df_vals_dept.values, color=colors[7], alpha=alpha_bg)
102ax6.barh(all_depts_sorted, panel_vals_dept.values, color=colors[7], alpha=alpha_fg)
103ax6.set_title("Départements d'origine")
104ax6.invert_yaxis()
105ax6.set_xlabel("Nombre de candidat·es")
106ax6.set(ylabel=None)
107
108# 7. Département thèse, triés par importance panel
109all_th = list(set(df_selection["Département thèse"].dropna()) | set(best_panel["Département thèse"].dropna()))
110panel_counts_th = best_panel["Département thèse"].value_counts()
111all_th_sorted = sorted(all_th, key=lambda x: panel_counts_th.get(x, 0), reverse=True)
112df_vals_th = df_selection["Département thèse"].value_counts().reindex(all_th_sorted).fillna(0)
113panel_vals_th = best_panel["Département thèse"].value_counts().reindex(all_th_sorted).fillna(0)
114ax7.barh(all_th_sorted, df_vals_th.values, color=colors[8], alpha=alpha_bg)
115ax7.barh(all_th_sorted, panel_vals_th.values, color=colors[8], alpha=alpha_fg)
116ax7.set_title("Département thèse")
117ax7.invert_yaxis()
118ax7.set_xlabel("Nombre de candidat·es")
119ax7.set(ylabel=None)
120
121# 8. Département emploi, triés par importance panel
122all_emp = list(set(df_selection["Département emploi"].dropna()) | set(best_panel["Département emploi"].dropna()))
123panel_counts_emp = best_panel["Département emploi"].value_counts()
124all_emp_sorted = sorted(all_emp, key=lambda x: panel_counts_emp.get(x, 0), reverse=True)
125df_vals_emp = df_selection["Département emploi"].value_counts().reindex(all_emp_sorted).fillna(0)
126panel_vals_emp = best_panel["Département emploi"].value_counts().reindex(all_emp_sorted).fillna(0)
127ax8.barh(all_emp_sorted, df_vals_emp.values, color=colors[9], alpha=alpha_bg)
128ax8.barh(all_emp_sorted, panel_vals_emp.values, color=colors[9], alpha=alpha_fg)
129ax8.set_title("Département emploi")
130ax8.invert_yaxis()
131ax8.set_xlabel("Nombre de candidat·es")
132ax8.set(ylabel=None)
133
134# Bordures noires
135for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8]:
136 for spine in ax.spines.values():
137 spine.set_edgecolor('black')
138 spine.set_linewidth(1.2)
139
140# Légende partagée
141fig.legend(labels_shared, loc='upper left', fontsize=11, frameon=True, fancybox=True)
142
143plt.show()
Ce bloc sauvegarde la solution retenue dans un fichier CSV.
Code
1df_draw = pd.DataFrame(best_panel)
2df_draw.to_csv("../data/panel_choisi.csv", index=False, header=True)