Introducción
Un total elevado de visitantes no identifica por sí solo la mejor franja de apertura. Una tasa horaria alta obtenida de un único registro también es una base débil para decidir. Aplicarás una regla explícita de la exposición para determinar qué comparaciones tienen suficientes registros, conservar todos los líderes empatados y comunicar los límites junto a un gráfico real.
Este laboratorio presupone el laboratorio anterior de observaciones horarias o conocimientos equivalentes sobre claves compuestas, registros faltantes, tasas por hora y cobertura, además de Python, pandas y Matplotlib básicos. Este entorno empieza de forma independiente con datos sintéticos originales y un observe.py completo; no necesita archivos del entorno anterior. La duración es una estimación del autor, no un tiempo de finalización medido.
Aplicar una regla explícita de evidencia
En este paso separarás la elegibilidad de la clasificación. En la pestaña WebIDE, abre el terminal inferior o elige Terminal → New Terminal en el menú superior izquierdo. Empieza en el espacio de trabajo preparado y ejecuta el análisis suministrado. Sus dos argumentos son el directorio de entrada y el de salida.
cd /home/labex/project/opening-hours
python3 observe.py data results
cat results/summary.csv
El resumen conserva todas las franjas. morning suma 40, pero su tasa es 10; afternoon y evening tienen tasa 12. short tiene tasa 20 con una sola hora registrada. sparse tiene tres horas registradas, pero cobertura 0.6. quiet se observó realmente con cero visitantes, mientras que la tasa de unlogged es desconocida.
La regla del ejercicio es pública: una franja es elegible solo si tiene al menos tres horas observadas y cobertura de al menos 0.75. La igualdad también cumple: tres observaciones de cuatro horas previstas alcanzan ambos límites. Compara las franjas elegibles mediante visitantes por hora observada. Conserva todas las franjas cuya diferencia absoluta respecto a la tasa elegible máxima esté dentro de 1e-9. Esta pequeña tolerancia trata la representación en coma flotante, no diferencias comerciales relevantes. Si no hay franjas elegibles, aplaza la recomendación. Son reglas de decisión del ejercicio, no umbrales universales de confianza estadística.
Cada franja conserva también una razón. Usa esta prioridad: no_open_hours si las horas previstas son cero; en otro caso, too_few_observations si hay menos de tres horas observadas; después, low_coverage si la cobertura es inferior a 0.75; de lo contrario, eligible. Así se distingue una franja cerrada, una sin registros y otra con suficientes registros pero cobertura insuficiente. Nunca elimines una franja no elegible de la tabla comparativa.
set_index('slot') convierte cada nombre estable de franja en etiqueta de fila. Restar la tasa máxima candidata y aplicar .abs().le(1e-9) conserva todas las tasas dentro de la tolerancia indicada; sorted da un orden de presentación estable a la lista de franjas de la decisión.
Crea recommend.py. Como antes, > sustituye el archivo y el here-document entre comillas guarda literalmente las líneas hasta PY. sys.argv lee la ruta del resumen y el directorio de salida. .ge() significa mayor o igual; las máscaras clasifican columnas completas. None se convierte en JSON null cuando no existe una tasa elegida. JSON es un formato de texto estructurado; json.dumps conserva la lista de identidades, por lo que un empate es una lista y no la fila que casualmente apareció primero.
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
}
Se conservan ambas franjas empatadas. short no es candidata pese a su tasa alta; sparse queda excluida por baja cobertura. quiet sigue siendo elegible con tasa cero: poca actividad no significa datos inválidos. Si todas las tasas elegibles fueran cero, todas esas franjas empatarían en cero y podrían compararse. Si ninguna franja fuera elegible, el resultado correcto sería status: defer, una lista slots vacía y rate: null.
Autocomprobación: abre results/comparison.csv en Explorer y encuentra la razón de cada franja. El contrato del programa es python3 recommend.py SUMMARY_CSV OUTPUT_DIRECTORY. Debe funcionar con otro resumen correctamente generado, sin depender de nombres fijos ni de un ganador fijo.
Mostrar la evidencia junto a la recomendación
En este paso mostrarás juntos los totales y las tasas sin disfrazar de cero las observaciones faltantes. Continúa en el mismo terminal de WebIDE, en /home/labex/project/opening-hours.
El contrato del gráfico es pequeño: dos paneles de barras verticales, identidades de franja en las marcas horizontales y etiquetas verticales Total visitors y Visitors per recorded hour. Muestra las horas observadas y la cobertura de cada franja, incluidas las no elegibles. Un total o una tasa ausente no tiene barra numérica y se etiqueta unknown. Los colores, fuentes, dimensiones y el orden pueden variar. El gráfico debe ser determinista: la misma entrada y el script actual reproducen los mismos píxeles; los metadatos del PNG pueden variar. No incluyas estilos aleatorios ni la hora actual en la imagen.
El bucle utiliza zip para tomar juntos un eje, un nombre de columna y una etiqueta de unidad. summary.iterrows() recorre cada franja y su fila, y enumerate añade la posición numérica de la barra. El formato de f-string :.0% muestra una fracción como porcentaje sin decimales. pd.notna comprueba si la cobertura existe antes de formatearla. annotate coloca texto en una posición de datos; xytext=(0, 5) con textcoords='offset points' lo desplaza cinco puntos de pantalla hacia arriba. ha='center' centra el texto y fontsize=7 mantiene compactas las etiquetas de evidencia. Ninguna de estas opciones visuales cambia el cálculo.
Añade el siguiente bloque una vez a recommend.py; >> conserva el código existente. subplots crea dos ejes. range(len(summary)) proporciona una posición por franja. Usa n=N; cov=P% para las horas observadas y la cobertura como porcentaje entero (cov=n/a si es indefinida). Cada etiqueta indica cuánta evidencia respalda esa posición. El gráfico incluye la tasa real de las franjas no elegibles, pero su razón y la decisión adjunta evitan confundirlas con una recomendación. Las tasas ausentes permanecen ausentes.
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
Abre results/comparison.png en Explorer de WebIDE. Lee juntos los paneles de totales y tasas: el total mayor de la mañana no supera la tasa de la tarde o de la tarde-noche. short muestra claramente n=1, y sparse, cov=60%. quiet tiene un cero real; las tasas de unlogged y closed son desconocidas. Una barra de altura cero no debe sustituir esos valores desconocidos.
cat results/advice.txt
Decision: compare
Slots: afternoon, evening
Evidence: historical description
Effect of extending hours: not established
El consejo utiliza cuatro campos públicos: Decision es compare o defer, Slots contiene ID estables separados por comas o none, Evidence es historical description y Effect of extending hours es not established. El orden de los campos y de las franjas puede variar; puedes añadir explicaciones después. Estos campos estructurados mantienen el consejo coherente con la decisión real y explicitan su interpretación sin depender de un párrafo persuasivo.
La recomendación se limita a las oportunidades históricas observadas y a la regla explícita. No demuestra que cambiar los horarios cause la misma tasa de visitantes. Una siguiente acción útil es recopilar los registros que faltan antes de decidir sobre franjas insuficientemente registradas. advice.txt comunica el resultado; las relaciones horarias, la tabla comparativa, la decisión y los datos realmente graficados aportan la evidencia.
Conserva ambos scripts, los dos CSV originales, hours.csv, summary.csv, comparison.csv, decision.json, el PNG real y el consejo. Si un nombre de archivo es incorrecto, lee el error, localízalo con ls y ejecuta la ruta correcta. Si sustituyes el archivo en vez de añadir contenido, recrea el script del paso 1 y añade este bloque una vez. Volver a ejecutar cualquiera de los programas recalcula sus salidas sin modificar las entradas.
Autocomprobación: los valores del gráfico proceden del resumen actual, están todos los empates y el resultado incluye la limitación. En el desafío cambiarán las identidades y la evidencia de otro centro, manteniendo estas reglas de análisis.
Resumen
Has separado la elegibilidad de la clasificación, conservado todos los líderes empatados y dado una razón a cada franja excluida. Dos gráficos de barras reales muestran el total de visitantes junto a los visitantes por hora registrada, con las observaciones y la cobertura visibles. Las tasas desconocidas siguen siendo desconocidas, y el consejo expresa el límite causal de los datos históricos descriptivos.
Tus scripts pueden recalcular los resultados de otro centro. Un análisis correcto puede aplazar la recomendación, conservar varios empates o comparar una franja realmente tranquila y elegible; no necesita producir un único ganador llamativo.



