Introduction
Une exposition communautaire a besoin d’une comparaison utile de ses créneaux d’ouverture. Un total de visiteurs plus élevé peut simplement refléter davantage d’occasions de les compter. Vous comparerez d’abord deux créneaux observés pendant la même durée, puis construirez une analyse réutilisable qui conserve les heures ouvertes, les heures enregistrées et ce qui reste inconnu.
Ce laboratoire suppose les bases de Python, la lecture, le filtrage et le regroupement avec pandas, ainsi que les diagrammes en barres Matplotlib. Python et les bibliothèques sont prêts dans WebIDE. Tous les comptages sont synthétiques ; une ligne représente une heure, pas l’identité d’un visiteur. La durée est une estimation de l’auteur incluant la lecture et les erreurs courantes. Vous créerez de véritables fichiers CSV et PNG en préservant toutes les entrées originales.
Comparer deux créneaux observés pendant la même durée
Dans cette étape, vous calculerez un taux à la main avant de dessiner le premier graphique. Dans l’onglet WebIDE, utilisez le terminal du bas. S’il est masqué, ouvrez le menu en haut à gauche et choisissez Terminal → New Terminal. Les commandes s’exécutent dans ce terminal ; les fichiers apparaissent dans Explorer à gauche.
Entrez dans l’espace de travail préparé. cd change le répertoire de travail de ce terminal, sans changer celui des autres terminaux. pwd affiche le répertoire courant. Si une commande ultérieure indique qu’un fichier manque, revenez ici et vérifiez son nom avec ls.
cd /home/labex/project/opening-hours
pwd
/home/labex/project/opening-hours
cat affiche un fichier texte sans le modifier. Ce CSV contient deux observations d’une heure pour chaque créneau.
cat data/equal.csv
slot,visitors
morning,6
morning,10
evening,8
evening,12
Le matin compte 16 visiteurs sur deux heures enregistrées : 16 / 2 = 8 visiteurs par heure. Le soir en compte 20 sur deux heures : 20 / 2 = 10. Les dénominateurs étant égaux, les comparaisons par total et par heure favorisent toutes deux le soir. Un taux conserve son unité : visiteurs par heure enregistrée.
Créez le petit script ci-dessous. Dans le shell, > remplace le fichier nommé. <<'PY' commence un here-document : les lignes sont enregistrées littéralement jusqu’à une ligne contenant seulement PY. Collez le bloc complet, dernière ligne comprise. Le fichier est créé immédiatement ; aucun enregistrement dans l’éditeur n’est nécessaire. agg nomme les colonnes du résultat et size compte les heures observées. index=False évite d’écrire l’index de lignes de pandas dans le CSV. Agg permet à Matplotlib d’enregistrer un PNG sans ouvrir une fenêtre séparée.
cat > equal.py <<'PY'
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
data = pd.read_csv('data/equal.csv')
summary = data.groupby('slot').agg(visitors=('visitors', 'sum'), hours=('visitors', 'size'))
summary['rate'] = summary['visitors'] / summary['hours']
summary.to_csv('results/equal-summary.csv')
summary['rate'].plot.bar(color=['#256b5d', '#b35828'], rot=0)
plt.ylabel('Visitors per recorded hour')
plt.title('Equal recording opportunities: two hours each')
plt.tight_layout()
plt.savefig('results/equal.png', dpi=150)
plt.close()
print(summary)
PY
python3 equal.py
visitors hours rate
slot
evening 20 2 10.0
morning 16 2 8.0
Les lignes sont triées par nom de créneau ; les espaces en début de ligne peuvent varier à l’affichage. Ouvrez opening-hours/results/equal.png dans Explorer pour voir le graphique enregistré. Ses deux barres verticales doivent montrer 10 et 8 visiteurs par heure enregistrée. Gardez les noms de créneaux sur les graduations horizontales et l’étiquette verticale Visitors per recorded hour ; couleurs, polices, dimensions et ordre des créneaux peuvent varier. Le PNG est votre véritable sortie, pas une image cible préparée. Le graphique doit être déterministe : à entrée inchangée, réexécuter le script actuel doit produire les mêmes pixels. Évitez les styles aléatoires et les horodatages. Les métadonnées PNG peuvent varier ; vos choix de couleurs, polices et dimensions restent autorisés.
Pour une erreur courante de nom de fichier, comprenez d’abord que Python signale le fichier absent et s’arrête ; votre travail n’a pas été réinitialisé. L’échec suivant est volontaire :
python3 missing.py
Vous devriez voir can't open file et No such file or directory. Pour récupérer, listez les fichiers de l’espace de travail et relancez le véritable nom de fichier.
ls
python3 equal.py
Autocontrôle : les deux créneaux utilisent deux heures observées. Vous n’avez pas encore supposé que toutes les données futures offriront les mêmes possibilités d’enregistrement.
Conserver chaque heure prévue
Dans cette étape, vous associerez les comptages de visiteurs à leurs heures prévues. Continuez dans le terminal WebIDE à /home/labex/project/opening-hours. Chaque ligne du plan est une heure, à une date et dans un créneau. open=1 signifie ouverture prévue ; open=0, fermeture. La paire date et slot est une clé composée : aucune colonne seule n’identifie une ligne. Les dates utilisent YYYY-MM-DD et toutes celles de ces fichiers sont des dates calendaires valides.
cat data/plan.csv
cat data/counts.csv
Le fichier de comptages est volontairement ordonné autrement. Un comptage de 0 est une véritable observation sans visiteur. L’absence de comptage pour une heure ouverte prévue signifie un relevé manquant, pas zéro visiteur. Les heures fermées n’ont pas de ligne de comptage. Chaque comptage doit correspondre à une ligne ouverte du plan ; les clés composées des deux fichiers doivent être complètes et uniques. Ne supprimez pas de doublons et n’inventez pas de comptages pour masquer une clé invalide.
Une jointure gauche (left merge) conserve chaque ligne de la table de gauche : le plan. Elle recherche les comptages avec les deux colonnes de clé, indépendamment des positions des lignes. Une correspondance absente reçoit la valeur manquante de pandas, affichée NaN. Vérifiez la présence des clés avant la jointure : pandas peut associer entre elles des clés manquantes, ce qui n’identifie pas une véritable heure. duplicated(keys) détecte les paires répétées et validate='one_to_one' impose de rejeter les relations dupliquées.
Le script utilise .loc[condition, column] pour affecter seulement les lignes correspondantes. .eq(0) et .eq(1) comparent chaque valeur à zéro ou un ; & combine les conditions et notna() distingue un comptage présent d’une valeur manquante. Le masque des heures fermées est appliqué d’abord, puis celui des heures ouvertes et enregistrées : un vrai zéro est donc observé.
Remplacez le observe.py préparé par le script complet ci-dessous. sys.argv[1] et [2] lisent les deux arguments de chemin après le nom du script. Un Path associé à / construit un chemin de fichier. mkdir(parents=True, exist_ok=True) crée le répertoire de sortie au besoin, pour que le même programme analyse un autre répertoire d’entrée. Les deux CSV d’entrée restent en lecture seule.
cat > observe.py <<'PY'
import sys
from pathlib import Path
import pandas as pd
source = Path(sys.argv[1])
output = Path(sys.argv[2])
plan = pd.read_csv(source / 'plan.csv')
counts = pd.read_csv(source / 'counts.csv')
keys = ['date', 'slot']
if plan[keys].isna().any().any() or counts[keys].isna().any().any():
raise ValueError('Every date and slot must be present')
if plan.duplicated(keys).any() or counts.duplicated(keys).any():
raise ValueError('Every date/slot key must be unique')
if not plan['open'].isin([0, 1]).all():
raise ValueError('open must be 0 or 1')
if counts['visitors'].isna().any() or (counts['visitors'] < 0).any():
raise ValueError('Recorded counts must be nonnegative')
known = counts.merge(plan, on=keys, how='left', validate='one_to_one')
if known['open'].isna().any() or (known['open'] != 1).any():
raise ValueError('Counts must match an open planned hour')
hours = plan.merge(counts, on=keys, how='left', validate='one_to_one')
hours['status'] = 'missing'
hours.loc[hours['open'].eq(0), 'status'] = 'closed'
hours.loc[hours['open'].eq(1) & hours['visitors'].notna(), 'status'] = 'observed'
hours['valid_hour'] = hours['status'].eq('observed').astype(int)
output.mkdir(parents=True, exist_ok=True)
hours.sort_values(keys).to_csv(output / 'hours.csv', index=False)
print(hours[['date', 'slot', 'visitors', 'status']].head())
PY
Le premier masque marque les heures fermées. Le second marque comme observées les heures ouvertes avec un comptage, y compris un vrai zéro. & combine deux conditions élément par élément et notna() teste la présence d’une valeur. valid_hour transforme la condition d’observation en 1 ou 0 ; sa somme comptera les heures d’observation utilisables. Aucun comptage manquant n’est remplacé par zéro.
python3 observe.py data results
head -n 10 results/hours.csv
head -n 10 affiche l’en-tête et les neuf premières lignes de données. Le CSV enregistré contient date,slot,open,visitors,status,valid_hour et est trié par la clé composée. Examinez-le aussi dans Explorer : quiet possède des zéros enregistrés avec observed et valid_hour=1 ; unlogged possède des comptages vides avec missing et valid_hour=0 ; closed possède des comptages vides avec closed. Selon son état, le vide signifie inconnu ou non applicable, jamais un zéro supposé.
Autocontrôle : chaque ligne du plan apparaît exactement une fois dans hours.csv et chaque comptage reste associé à sa propre date et à son propre créneau. Si un ValueError apparaît, lisez quelle relation est invalide. Comparez votre script avec ce bloc et les fichiers sources intacts ; corrigez le script, puis relancez-le. Il n’est pas nécessaire de rouvrir le terminal.
Calculer les taux et la couverture des relevés
Dans cette étape, vous résumerez chaque créneau sans changer le sens d’un comptage inconnu. Continuez dans le même terminal WebIDE et le même espace de travail.
Pour evening, le plan contient quatre heures ouvertes, dont trois ont chacune un relevé de 12 visiteurs. Le total est 36, les heures observées sont 3, le taux horaire est 36 / 3 = 12 et la couverture est 3 / 4 = 0.75 : 75% des heures ouvertes prévues ont été enregistrées. Les heures manquantes n’entrent pas dans le dénominateur du taux, mais réduisent la couverture. morning a 40 visiteurs en quatre heures observées : son total plus élevé a un taux plus faible, 10.
Pour quiet, quatre zéros enregistrés donnent un total et un taux de 0 avec une couverture complète. Pour unlogged, aucune heure n’est observée : la couverture vaut 0, mais total et taux restent vides. Pour closed, les heures ouvertes prévues valent 0 : couverture et taux sont indéfinis et restent vides. Une valeur indéfinie ne doit pas devenir ensuite une barre de taux de hauteur nulle.
Ajoutez une seule fois le bloc suivant à observe.py. Ici, >> ajoute sans remplacer. Il utilise les variables hours et output créées plus haut. sum(min_count=1) renvoie un total manquant si toutes les valeurs manquent, alors que la somme par défaut renvoie zéro sans observation valide. replace(0, float('nan')) rend explicitement manquants les dénominateurs nuls avant la division. Ces opérations préservent un véritable numérateur observé de zéro.
cat >> observe.py <<'PY'
grouped = hours.groupby('slot')
summary = grouped.agg(planned_hours=('open', 'sum'), observed_hours=('valid_hour', 'sum'))
summary['visitors'] = grouped['visitors'].sum(min_count=1)
summary['coverage'] = summary['observed_hours'] / summary['planned_hours'].replace(0, float('nan'))
summary['rate'] = summary['visitors'] / summary['observed_hours'].replace(0, float('nan'))
summary.to_csv(output / 'summary.csv')
print(summary)
PY
python3 observe.py data results
cat results/summary.csv
slot,planned_hours,observed_hours,visitors,coverage,rate
afternoon,3,3,36.0,1.0,12.0
closed,0,0,,,
evening,4,3,36.0,0.75,12.0
morning,4,4,40.0,1.0,10.0
quiet,4,4,0.0,1.0,0.0
short,4,1,20.0,0.25,20.0
sparse,5,3,27.0,0.6,9.0
unlogged,4,0,,0.0,
La présentation numérique du CSV, comme 36 au lieu de 36.0, peut varier sans changer le résultat. Les colonnes requises et leur sens restent identiques. short possède le taux observé le plus élevé, mais une seule heure observée. sparse a trois observations, mais couvre seulement 60% des heures prévues. Ce sont des raisons d’examiner la qualité des données avant de recommander ; ce laboratoire n’a encore déclaré gagnant aucun de ces créneaux.
Ouvrez results/summary.csv dans Explorer. Conservez data/plan.csv, data/counts.csv, data/equal.csv et vos scripts. Le contrat réutilisable est python3 observe.py INPUT_DIRECTORY OUTPUT_DIRECTORY : les deux CSV sont lus en entrée, et hours.csv ainsi que summary.csv sont écrits en sortie. Des calculs équivalents et un autre ordre de lignes sont autorisés si toutes les clés et relations numériques sont conservées. Si vous avez remplacé le script au lieu d’ajouter le bloc, recréez celui de l’étape 2, ajoutez ce bloc une fois, puis relancez-le.
Autocontrôle : un créneau sans observation et un créneau réellement observé sans visiteur ont des totaux, dénominateurs et taux différents. Le prochain laboratoire utilisera ces distinctions pour décider quand les données suffisent à comparer les créneaux.
Résumé
Vous avez créé un graphique comparant des créneaux observés pendant la même durée, relié les comptages par des clés composées complètes et produit des fichiers CSV par heure et par créneau. Les heures fermées, les relevés manquants et les véritables zéros restent distincts. Les taux utilisent les heures ouvertes enregistrées ; la couverture révèle quelle part des possibilités prévues a réellement été enregistrée.
Conservez l’analyse réutilisable et les données originales. Ces taux historiques descriptifs ne prouvent pas ce qui se passerait après une extension des horaires. Dans le prochain environnement indépendant, vous appliquerez une règle explicite de suffisance des données avant de recommander des créneaux.



