Introducción
Una exposición comunitaria necesita una comparación útil de sus franjas de apertura. Un mayor total de visitantes puede significar simplemente más oportunidades de registrar visitas. Primero compararás dos franjas con iguales oportunidades de observación y después construirás un análisis reutilizable que conserve qué horas estuvieron abiertas, cuáles se registraron y cuáles siguen siendo desconocidas.
Este laboratorio presupone Python básico, lectura, filtrado y agrupación con pandas y gráficos de barras con Matplotlib. Python y las bibliotecas están preparados en WebIDE. Todos los recuentos son sintéticos; una fila representa una hora, no la identidad de un visitante. La duración es una estimación del autor que incluye lectura y errores habituales. Crearás archivos CSV y PNG reales, conservando todas las entradas originales.
Comparar dos franjas con igual tiempo observado
En este paso calcularás una tasa a mano antes de dibujar el primer gráfico. En la pestaña WebIDE, utiliza el terminal inferior. Si está oculto, abre el menú superior izquierdo y elige Terminal → New Terminal. Los comandos se ejecutan en ese terminal; los archivos aparecen en Explorer, a la izquierda.
Entra en el espacio de trabajo preparado. cd cambia el directorio de trabajo de este terminal, sin cambiar otros terminales. pwd muestra el directorio actual. Si un comando posterior indica que falta un archivo, vuelve aquí y comprueba su nombre con ls.
cd /home/labex/project/opening-hours
pwd
/home/labex/project/opening-hours
cat muestra un archivo de texto sin modificarlo. Este CSV contiene dos observaciones de una hora por franja.
cat data/equal.csv
slot,visitors
morning,6
morning,10
evening,8
evening,12
La mañana suma 16 visitantes en dos horas registradas: 16 / 2 = 8 visitantes por hora. La tarde-noche suma 20 en dos horas: 20 / 2 = 10. Los denominadores son iguales, por lo que tanto el total como la comparación por hora favorecen la tarde-noche. Una tasa mantiene su unidad: visitantes por hora registrada.
Crea el siguiente script. En el shell, > sustituye el archivo indicado. <<'PY' inicia un here-document: las líneas se guardan literalmente hasta una línea que contenga solo PY. Pega el bloque completo, incluida su última línea. Esto crea el archivo inmediatamente; no hay que guardarlo en un editor. agg da nombre a las columnas del resultado y size cuenta las horas observadas. index=False evita escribir el índice de filas de pandas en el CSV. Agg permite guardar un PNG con Matplotlib sin abrir una ventana independiente.
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
Las filas se ordenan por nombre de franja; los espacios iniciales de la visualización pueden variar. Abre opening-hours/results/equal.png en Explorer para ver el gráfico guardado. Sus dos barras verticales deben mostrar 10 y 8 visitantes por hora registrada. Mantén cada nombre de franja en su marca horizontal y la etiqueta vertical Visitors per recorded hour; puedes cambiar colores, fuentes, tamaño y orden de las franjas. El PNG es tu salida real, no una imagen objetivo preparada. El gráfico debe ser determinista: con la misma entrada, volver a ejecutar el script actual debe producir los mismos píxeles. Evita estilos aleatorios y marcas de tiempo. Los metadatos del PNG pueden variar; puedes seguir eligiendo colores, fuentes y tamaño.
Ante un error habitual de nombre de archivo, entiende primero que Python informa de que falta el archivo y se detiene; no ha reiniciado tu trabajo. El siguiente fallo es intencional:
python3 missing.py
Debes ver can't open file y No such file or directory. Recupérate mostrando los archivos del espacio de trabajo y ejecutando de nuevo el nombre correcto.
ls
python3 equal.py
Autocomprobación: ambas franjas usan dos horas observadas. Todavía no has supuesto que todos los datos futuros tendrán las mismas oportunidades de registro.
Conservar cada hora prevista
En este paso relacionarás los recuentos de visitantes con sus horas previstas. Continúa en el terminal de WebIDE, en /home/labex/project/opening-hours. Cada fila del plan representa una hora, en una fecha y una franja. open=1 significa apertura prevista; open=0, cierre. El par date y slot es una clave compuesta: ninguna de las columnas identifica por sí sola una fila. Las fechas usan YYYY-MM-DD y todas las del archivo son fechas válidas del calendario.
cat data/plan.csv
cat data/counts.csv
El archivo de recuentos está deliberadamente en otro orden. Un recuento de 0 es una observación real sin visitantes. La ausencia de un recuento para una hora de apertura prevista significa falta de registro, no cero visitantes. Las horas cerradas no tienen fila de recuento. Cada recuento debe corresponder a una fila abierta del plan; ambos archivos deben tener claves compuestas completas y únicas. No elimines duplicados ni inventes recuentos para ocultar una clave inválida.
Una combinación izquierda (left merge) conserva todas las filas de la tabla izquierda: el plan. Busca los recuentos mediante ambas columnas de la clave, independientemente de sus posiciones. Las filas sin coincidencia reciben el valor ausente de pandas, mostrado como NaN. Comprueba que las claves estén completas antes de combinar: pandas puede hacer coincidir claves ausentes entre sí, lo que no identifica una hora real. duplicated(keys) detecta pares repetidos y validate='one_to_one' exige rechazar relaciones duplicadas.
El script usa .loc[condition, column] para asignar valores solo a las filas que cumplen la condición. .eq(0) y .eq(1) comparan cada valor con cero o uno; & combina condiciones y notna() distingue un recuento presente de un valor ausente. Primero se aplica la máscara de cierre y después la de apertura con registro, de modo que un cero real cuenta como observado.
Sustituye el observe.py preparado por el siguiente script completo. sys.argv[1] y [2] leen los dos argumentos de ruta después del nombre del script. Un Path unido con / construye una ruta de archivo. mkdir(parents=True, exist_ok=True) crea el directorio de salida cuando es necesario, para que el mismo programa analice otro directorio de entrada. Los dos CSV de entrada permanecen en modo de solo lectura.
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
La primera máscara marca las horas cerradas. La segunda marca como observadas las horas abiertas que tienen recuento, incluido el cero real. & combina dos condiciones elemento a elemento y notna() pregunta si existe un valor. valid_hour convierte la condición de observación en 1 o 0; su suma contará las horas de observación utilizables. Ningún recuento faltante se rellena con cero.
python3 observe.py data results
head -n 10 results/hours.csv
head -n 10 muestra la cabecera y las primeras nueve filas de datos. El CSV guardado incluye date,slot,open,visitors,status,valid_hour y se ordena por la clave compuesta. Examínalo también en Explorer: quiet contiene ceros registrados con observed y valid_hour=1; unlogged tiene recuentos vacíos con missing y valid_hour=0; closed tiene recuentos vacíos con closed. Según su estado, el espacio vacío significa desconocido o no aplicable, nunca un cero supuesto.
Autocomprobación: cada fila del plan aparece exactamente una vez en hours.csv, y cada recuento permanece asociado a su propia fecha y franja. Si aparece ValueError, lee qué relación es inválida. Compara tu script con este bloque y con los archivos originales intactos; corrige el script y vuelve a ejecutarlo. No necesitas reabrir el terminal.
Calcular tasas y cobertura de los registros
En este paso resumirás cada franja sin cambiar lo que significa un recuento desconocido. Continúa en el mismo terminal de WebIDE y espacio de trabajo.
Para evening, el plan contiene cuatro horas abiertas y hay tres registros de 12 visitantes cada uno. El total es 36, las horas observadas son 3, la tasa por hora es 36 / 3 = 12 y la cobertura es 3 / 4 = 0.75: se registró el 75% de las horas abiertas previstas. Las horas faltantes no entran en el denominador de la tasa, pero reducen la cobertura. morning tiene 40 visitantes en cuatro horas observadas: su total mayor tiene una tasa menor, 10.
En quiet, cuatro ceros registrados producen total 0 y tasa 0 con cobertura completa. En unlogged no se observó ninguna hora: la cobertura es 0, pero el total y la tasa permanecen vacíos. En closed hay 0 horas abiertas previstas: tanto la cobertura como la tasa son indefinidas y permanecen vacías. Un valor indefinido no debe convertirse después en una barra de tasa de altura cero.
Añade el siguiente bloque una vez al final de observe.py. Aquí >> añade contenido, no sustituye el archivo. Usa las variables hours y output creadas antes. sum(min_count=1) devuelve un total ausente cuando todos los valores faltan, mientras que la suma predeterminada devuelve cero cuando no hay observaciones válidas. replace(0, float('nan')) convierte explícitamente los denominadores cero en valores ausentes antes de dividir. Estas operaciones conservan un numerador realmente observado de cero.
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,
El formato numérico del CSV puede variar, por ejemplo 36 frente a 36.0, sin cambiar el resultado. Las columnas requeridas y sus significados siguen siendo los mismos. short tiene la tasa observada más alta, pero solo una hora observada. sparse tiene tres observaciones, pero cubre únicamente el 60% de sus horas previstas. Son motivos para valorar la calidad de la evidencia antes de recomendar; este laboratorio todavía no ha declarado ganadora ninguna de esas franjas.
Abre results/summary.csv en Explorer. Conserva data/plan.csv, data/counts.csv, data/equal.csv y tus scripts. El contrato reutilizable es python3 observe.py INPUT_DIRECTORY OUTPUT_DIRECTORY: ambos CSV se leen de la entrada y tanto hours.csv como summary.csv se escriben en la salida. Se permiten cálculos equivalentes y otro orden de filas si conservan todas las claves y relaciones numéricas. Si sustituyes por accidente el script al añadir el bloque, recrea el script del paso 2, añade este bloque una sola vez y vuelve a ejecutarlo.
Autocomprobación: una franja sin observaciones y otra observada con cero visitantes tienen distintos totales, denominadores y tasas. El siguiente laboratorio utilizará estas diferencias para decidir cuándo hay evidencia suficiente para comparar franjas.
Resumen
Has creado un gráfico con iguales oportunidades de observación, relacionado los recuentos mediante claves compuestas completas y producido archivos CSV por hora y por franja. Las horas cerradas, los registros faltantes y los ceros reales siguen diferenciados. Las tasas usan horas abiertas registradas; la cobertura muestra qué proporción de las oportunidades previstas se registró realmente.
Conserva el análisis reutilizable y los datos originales. Estas tasas históricas descriptivas no demuestran qué sucedería al ampliar el horario. En el siguiente entorno independiente aplicarás una regla explícita de evidencia antes de formular una recomendación.



