Recommander des créneaux selon les observations

PythonBeginner
Pratiquer maintenant

Introduction

Un total élevé de visiteurs n’identifie pas à lui seul le meilleur créneau d’ouverture. Un taux horaire élevé issu d’un seul relevé constitue aussi une base fragile pour décider. Vous appliquerez une règle explicite de l’exposition pour déterminer quelles comparaisons reposent sur assez de relevés, conserver tous les premiers ex æquo et communiquer les limites avec un véritable graphique.

Ce laboratoire suppose le précédent laboratoire d’observations horaires ou des connaissances équivalentes sur les clés composées, relevés manquants, taux horaires et couverture, ainsi que les bases de Python, pandas et Matplotlib. Cet environnement démarre indépendamment avec les données synthétiques originales et un observe.py complet ; aucun fichier de l’environnement précédent n’est nécessaire. La durée est une estimation de l’auteur, pas une durée mesurée.

Appliquer une règle explicite de suffisance des données

Dans cette étape, vous séparerez l’admissibilité du classement. Dans l’onglet WebIDE, ouvrez le terminal inférieur ou choisissez Terminal → New Terminal dans le menu en haut à gauche. Partez de l’espace de travail préparé et exécutez l’analyse fournie. Ses deux arguments sont les répertoires d’entrée et de sortie.

cd /home/labex/project/opening-hours
python3 observe.py data results
cat results/summary.csv

Le résumé conserve chaque créneau. morning totalise 40, mais son taux vaut 10 ; afternoon et evening ont chacun un taux de 12. short a un taux de 20 pour une seule heure enregistrée. sparse compte trois heures enregistrées, mais une couverture de 0.6. quiet a véritablement été observé à zéro, tandis que le taux de unlogged est inconnu.

La règle d’exercice est publique : un créneau est admissible uniquement avec au moins trois heures observées et une couverture d’au moins 0.75. L’égalité satisfait la règle : trois observations sur quatre heures prévues atteignent les deux limites. Comparez les créneaux admissibles par visiteurs par heure observée. Conservez tous ceux dont l’écart absolu au taux admissible maximal est inférieur ou égal à 1e-9. Cette petite tolérance traite la représentation flottante, pas des différences métier significatives. Sans créneau admissible, différez la recommandation. Il s’agit de règles de décision d’exercice, pas de seuils universels de confiance statistique.

Chaque créneau garde aussi une raison. Appliquez cette priorité : no_open_hours si les heures prévues sont nulles ; sinon too_few_observations si les heures observées sont inférieures à trois ; sinon low_coverage sous 0.75 ; sinon eligible. Cela distingue un créneau fermé, un créneau sans relevé et un créneau avec assez de relevés mais une couverture insuffisante. Ne retirez jamais un créneau non admissible du tableau comparatif.

set_index('slot') fait du nom stable de créneau l’étiquette de ligne. La soustraction du taux candidat maximal suivie de .abs().le(1e-9) conserve tous les taux dans la tolérance indiquée ; sorted donne un ordre d’affichage stable à la liste de créneaux de la décision.

Créez recommend.py. Comme auparavant, > remplace le fichier et le here-document entre guillemets sauvegarde littéralement les lignes jusqu’à PY. sys.argv lit le chemin du résumé et le répertoire de sortie. .ge() signifie supérieur ou égal ; les masques classent des colonnes entières. None devient JSON null lorsqu’aucun taux n’est choisi. JSON est un format texte structuré ; json.dumps conserve la liste des identités de créneau. Un ex æquo est donc une liste, pas la ligne qui s’est trouvée en premier.

cat > recommend.py <<'PY'
import sys
import json
from pathlib import Path
import pandas as pd

source = Path(sys.argv[1])
output = Path(sys.argv[2])
summary = pd.read_csv(source).set_index('slot')
summary['eligible'] = summary['observed_hours'].ge(3) & summary['coverage'].ge(0.75)
summary['reason'] = 'eligible'
summary.loc[summary['coverage'].lt(0.75), 'reason'] = 'low_coverage'
summary.loc[summary['observed_hours'].lt(3), 'reason'] = 'too_few_observations'
summary.loc[summary['planned_hours'].eq(0), 'reason'] = 'no_open_hours'
candidates = summary.loc[summary['eligible']]
best = float(candidates['rate'].max()) if len(candidates) else None
winners = sorted(candidates.index[(candidates['rate'] - best).abs().le(1e-9)].tolist()) if best is not None else []
decision = {'status': 'compare' if winners else 'defer', 'slots': winners, 'rate': best}
output.mkdir(parents=True, exist_ok=True)
summary.to_csv(output / 'comparison.csv')
(output / 'decision.json').write_text(json.dumps(decision, indent=2) + '\n')
print(summary[['observed_hours', 'coverage', 'rate', 'eligible', 'reason']])
print(json.dumps(decision))
PY
python3 recommend.py results/summary.csv results
cat results/decision.json
{
  "status": "compare",
  "slots": [
    "afternoon",
    "evening"
  ],
  "rate": 12.0
}

Les deux créneaux ex æquo sont conservés. short n’est pas candidat malgré son taux élevé ; sparse est exclu pour faible couverture. quiet reste admissible avec un taux nul : une faible activité n’invalide pas les données. Si tous les taux admissibles étaient nuls, tous les créneaux admissibles seraient ex æquo à zéro et resteraient comparables. Si chaque créneau était non admissible, le résultat correct serait status: defer, une liste slots vide et rate: null.

Autocontrôle : ouvrez results/comparison.csv dans Explorer et trouvez la raison de chaque créneau. Le contrat du programme est python3 recommend.py SUMMARY_CSV OUTPUT_DIRECTORY. Il doit fonctionner avec un autre résumé correct, sans dépendre de noms de créneaux ou d’un gagnant figés.

Présenter les données à côté de la recommandation

Dans cette étape, vous rendrez visibles totaux et taux ensemble sans déguiser les observations manquantes en zéros. Continuez dans le même terminal WebIDE à /home/labex/project/opening-hours.

Le contrat du graphique est volontairement limité : deux panneaux de barres verticales, les identités de créneau sur les graduations horizontales et les étiquettes verticales Total visitors et Visitors per recorded hour. Affichez le nombre d’heures observées et la couverture de chaque créneau, même non admissible. Un total ou taux manquant n’a pas de barre numérique et porte l’étiquette unknown. Couleurs, polices, dimensions et ordre peuvent varier. Le graphique doit être déterministe : entrée inchangée et script actuel reproduisent les mêmes pixels ; les métadonnées PNG peuvent varier. N’ajoutez pas de style aléatoire ni l’heure actuelle dans l’image.

La boucle utilise zip pour prendre ensemble un axe, un nom de colonne et une étiquette d’unité. summary.iterrows() parcourt chaque créneau et sa ligne ; enumerate ajoute sa position numérique de barre. Le format f-string :.0% affiche une fraction comme pourcentage sans décimale. pd.notna vérifie la présence de la couverture avant de la formater. annotate place du texte à une position de données ; xytext=(0, 5) avec textcoords='offset points' le déplace de cinq points d’affichage vers le haut. ha='center' centre le texte et fontsize=7 garde les étiquettes compactes. Aucun de ces choix d’affichage ne change le calcul.

Ajoutez une seule fois le bloc suivant à recommend.py ; >> préserve le code existant. subplots crée deux axes. range(len(summary)) fournit une position de barre par créneau. Utilisez n=N; cov=P% pour les heures observées et la couverture en pourcentage entier (cov=n/a si elle est indéfinie). Chaque étiquette indique combien de données soutiennent la position. Le graphique inclut le taux réel d’un créneau non admissible, mais sa raison et la décision jointe empêchent de le confondre avec une recommandation. Les taux manquants restent manquants.

cat >> recommend.py <<'PY'

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(14, 6))
x = list(range(len(summary)))
for ax, column, label in zip(axes, ['visitors', 'rate'], ['Total visitors', 'Visitors per recorded hour']):
    bars = ax.bar(x, summary[column], color='#26705e')
    ax.set_xticks(x, summary.index, rotation=40, ha='right')
    ax.set_ylabel(label)
    ax.set_ylim(bottom=0)
    ax.grid(axis='y', alpha=0.2)
    for n, (slot, row) in enumerate(summary.iterrows()):
        coverage = f"{row['coverage']:.0%}" if pd.notna(row['coverage']) else 'n/a'
        note = f"n={int(row['observed_hours'])}; cov={coverage}"
        value = row[column]
        if pd.isna(value):
            note = 'unknown\n' + note
        ax.annotate(note, (n, 0 if pd.isna(value) else value), xytext=(0, 5), textcoords='offset points', ha='center', fontsize=7)
fig.suptitle('Opening opportunities: totals and observed-hour rates')
fig.tight_layout(rect=(0, 0.04, 1, 0.93))
fig.savefig(output / 'comparison.png', dpi=150)
plt.close(fig)
choice = ', '.join(winners) if winners else 'none'
advice = f"Decision: {decision['status']}\nSlots: {choice}\n"
advice += 'Evidence: historical description\n'
advice += 'Effect of extending hours: not established\n'
(output / 'advice.txt').write_text(advice)
print(advice)
PY
python3 recommend.py results/summary.csv results

Ouvrez results/comparison.png dans Explorer de WebIDE. Lisez ensemble les panneaux des totaux et des taux : le total supérieur du matin ne surpasse pas les taux de l’après-midi ou du soir. short montre clairement n=1 et sparse, cov=60%. quiet est un vrai zéro ; les taux de unlogged et closed sont inconnus. Une barre à zéro ne doit pas remplacer ces valeurs inconnues.

cat results/advice.txt
Decision: compare
Slots: afternoon, evening
Evidence: historical description
Effect of extending hours: not established

Le conseil utilise quatre champs publics : Decision vaut compare ou defer, Slots contient les ID stables séparés par des virgules ou none, Evidence vaut historical description et Effect of extending hours vaut not established. L’ordre des champs et des créneaux peut varier ; vous pouvez ajouter une explication ensuite. Ces petits champs structurés maintiennent le conseil cohérent avec la décision réelle et précisent son interprétation sans s’appuyer seulement sur un paragraphe convaincant.

La recommandation se limite aux possibilités historiques observées et à la règle explicite. Elle ne démontre pas qu’un changement d’horaires causera le même taux de visiteurs. Une suite utile consiste à compléter les relevés manquants avant de décider pour les créneaux peu enregistrés. advice.txt communique le résultat ; les relations horaires, le tableau comparatif, la décision et les données réellement tracées en constituent les preuves.

Conservez les deux scripts, les deux CSV d’entrée originaux, hours.csv, summary.csv, comparison.csv, decision.json, le vrai PNG et le conseil. Si un nom de fichier est erroné, lisez l’erreur, localisez le fichier avec ls, puis relancez avec le bon chemin. Si vous avez remplacé au lieu d’ajouter, recréez le script de l’étape 1 et ajoutez ce bloc une fois. Réexécuter l’un des programmes recalcule ses sorties sans modifier les entrées.

Autocontrôle : les valeurs du graphique viennent du résumé actuel, chaque ex æquo est présent et la limitation accompagne le résultat. Dans le défi, un autre lieu changera les identités et les observations sans changer ces règles d’analyse.

Résumé

Vous avez séparé l’admissibilité du classement, conservé tous les premiers ex æquo et donné une raison à chaque créneau exclu. Deux véritables diagrammes en barres montrent les visiteurs totaux à côté des visiteurs par heure enregistrée, avec le nombre d’observations et la couverture visibles. Les taux inconnus restent inconnus, et le conseil précise la limite causale des données historiques descriptives.

Vos scripts peuvent recalculer les résultats d’un autre lieu. Une analyse correcte peut différer la recommandation, conserver plusieurs ex æquo ou comparer un créneau réellement calme mais admissible ; elle n’a pas besoin de désigner un seul gagnant spectaculaire.