Introdução
Um total maior de visitantes não determina sozinho a melhor faixa de funcionamento. Uma taxa horária alta baseada em um único registro também oferece pouca sustentação para decidir. Você aplicará a regra explícita do espaço de exposições, encontrará faixas com registros suficientes, manterá todos os líderes empatados e comunicará os limites junto com um gráfico real.
Este laboratório pressupõe o anterior sobre observações horárias, ou conhecimento equivalente de chaves compostas, valores ausentes, taxas horárias e cobertura, além de noções de Python, pandas e Matplotlib. O ambiente começa de forma independente com os dados sintéticos originais e um observe.py completo. Não são necessários arquivos do ambiente anterior. A duração é uma estimativa do autor, não um tempo de conclusão medido.
Aplique uma regra explícita de suficiência dos dados
Nesta etapa, você separará elegibilidade e classificação. Abra o terminal inferior da WebIDE Tab ou selecione Terminal → New Terminal no menu superior esquerdo. Na pasta preparada, execute a análise de observações fornecida. Os dois argumentos são o diretório de entrada e o de saída.
cd /home/labex/project/opening-hours
python3 observe.py data results
cat results/summary.csv
A síntese mantém todas as faixas. morning tem total 40, mas taxa 10; afternoon e evening têm taxa 12. short tem taxa 20 a partir de uma única hora registrada. sparse tem três horas registradas, mas cobertura 0.6. quiet observou um zero real, enquanto a taxa de unlogged é desconhecida.
A regra do local para o exercício é pública: uma faixa só pode ser comparada se tiver pelo menos 3 horas observadas E cobertura de pelo menos 0.75. A igualdade no limite também conta. Três observações em quatro horas planejadas satisfazem os dois limites. Compare as faixas elegíveis por visitantes por hora observada. Mantenha todas as faixas cuja diferença absoluta em relação à maior taxa elegível seja no máximo 1e-9. Essa pequena tolerância trata a representação de ponto flutuante, não uma diferença relevante para o negócio. Sem faixas elegíveis, adie a recomendação. Esta é uma regra de decisão do exercício, não um limite universal de confiança estatística.
Preserve uma razão para cada faixa. A precedência é no_open_hours se não há horas de abertura planejadas; caso contrário, too_few_observations se há menos de três horas observadas; depois, low_coverage se a cobertura é inferior a 0.75; nos demais casos, eligible. Isso distingue faixas fechadas, faixas sem registros e faixas com registros suficientes, mas cobertura insuficiente. Não remova as faixas inelegíveis da comparação.
set_index('slot') usa o nome estável da faixa como rótulo de linha. Subtrair a maior taxa candidata e aplicar .abs().le(1e-9) preserva todas as taxas dentro da tolerância. sorted dá uma ordem estável à lista de faixas na decisão.
Crie recommend.py. Como antes, > substitui o arquivo e o here-document entre aspas salva o texto literalmente até PY. sys.argv lê o caminho da síntese e o diretório de saída. .ge() significa “maior ou igual”; as máscaras classificam colunas inteiras. Sem taxa selecionada, None vira JSON null. JSON é um formato de texto estruturado. json.dumps preserva a lista de identificadores de faixas: um empate vira uma lista, não uma primeira linha escolhida por acaso.
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
}
As duas faixas empatadas são mantidas. short não é candidata apesar da taxa alta, e sparse é excluída pela cobertura insuficiente. quiet é elegível mesmo com taxa zero: poucos visitantes não tornam os dados inválidos. Se todas as taxas elegíveis forem zero, todas as faixas elegíveis empatam em zero e podem ser comparadas. Se nenhuma for elegível, o resultado correto é status: defer, uma lista slots vazia e rate: null.
Autoverificação: abra results/comparison.csv no Explorer e localize a razão de cada faixa. O contrato do programa é python3 recommend.py SUMMARY_CSV OUTPUT_DIRECTORY. Ele deve funcionar com outra síntese correta, sem depender de nomes fixos de faixas ou de um vencedor fixo.
Mostre os dados ao lado da recomendação
Nesta etapa, você mostrará total e taxa juntos sem disfarçar dados ausentes de zero. Continue no mesmo terminal do WebIDE em /home/labex/project/opening-hours.
O formato mínimo do gráfico tem dois painéis de barras verticais, identificadores das faixas nas marcações horizontais e rótulos verticais Total visitors e Visitors per recorded hour. Mostre as horas observadas e a cobertura de todas as faixas, incluindo as inelegíveis. Se um total ou uma taxa estiver ausente, não desenhe uma barra numérica e indique unknown. Cores, fontes, dimensões e ordem das faixas são livres. O gráfico deve ser determinístico: a mesma entrada reproduz os mesmos pixels com o script atual; os metadados do PNG podem variar. Não inclua estilos aleatórios ou a hora atual na imagem.
O laço usa zip para obter, em conjunto, um eixo, um nome de coluna e um rótulo de unidade. summary.iterrows() percorre cada faixa e linha; enumerate acrescenta a posição numérica da barra. O formato de f-string :.0% exibe uma fração como porcentagem sem casas decimais. pd.notna testa se a cobertura existe antes de formatá-la. annotate coloca texto em uma posição de dados. xytext=(0, 5) com textcoords='offset points' desloca o texto cinco pontos gráficos para cima. ha='center' centraliza o texto; fontsize=7 mantém compactos os rótulos dos dados. Essas opções visuais não mudam os cálculos.
Acrescente o bloco abaixo uma vez a recommend.py. >> preserva o código existente. subplots cria os dois eixos. range(len(summary)) dá uma posição a cada faixa. Use n=N; cov=P% para as horas observadas e a cobertura em porcentagem inteira, e cov=n/a quando a cobertura for indefinida. O rótulo mostra a quantidade de dados naquela posição. As taxas reais das faixas inelegíveis também aparecem, mas as razões e a decisão as distinguem dos candidatos recomendados. Uma taxa ausente continua ausente.
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
Abra results/comparison.png no Explorer do WebIDE. Leia os painéis de total e taxa juntos. Mesmo com maior total de manhã, as taxas horárias da tarde e da noite são maiores. Em short, aparece n=1; em sparse, cov=60%. quiet é um zero verdadeiro, mas as taxas de unlogged e closed são desconhecidas. Não substitua o desconhecido por uma barra com altura zero.
cat results/advice.txt
Decision: compare
Slots: afternoon, evening
Evidence: historical description
Effect of extending hours: not established
A recomendação usa quatro campos públicos: Decision é compare ou defer; Slots contém os IDs estáveis das faixas separados por vírgulas ou none; Evidence é historical description; Effect of extending hours é not established. A ordem dos campos e das faixas é livre, e você pode acrescentar explicações depois. Esses campos curtos alinham a recomendação à decisão real e explicitam seu alcance, sem depender apenas de uma redação convincente.
A recomendação se limita às oportunidades passadas observadas e à regra explícita. Ela não mostra que mudar o horário causará a mesma taxa de visitantes. Coletar os registros ausentes é uma próxima ação útil antes de decidir sobre faixas mal registradas. advice.txt comunica o resultado, mas sua sustentação está nas relações horárias, na comparação, na decisão e nos valores realmente desenhados.
Preserve os dois scripts, os dois CSVs originais, hours.csv, summary.csv, comparison.csv, decision.json, o PNG real e a recomendação. Se errar um nome, leia o erro, encontre o arquivo com ls e execute com o caminho correto. Se substituir em vez de acrescentar, recrie o script da etapa 1 e adicione este bloco uma vez. Ambos recalculam as saídas sem alterar as entradas.
Autoverificação: os valores do gráfico seguem a síntese atual, todos os candidatos empatados estão presentes e o resultado inclui a limitação. No desafio, os IDs das faixas e os dados de outro local mudarão, mas as regras de análise serão as mesmas.
Resumo
Você separou a elegibilidade da classificação, preservou todos os líderes empatados e manteve uma razão para cada faixa excluída. Dois gráficos de barras reais mostram lado a lado os totais de visitantes e visitantes por hora registrada, com o número de observações e a cobertura. Taxas desconhecidas continuam desconhecidas, e a recomendação explicita o limite de inferências causais a partir de dados históricos descritivos.
Seus scripts podem recalcular o resultado para outro local. Uma análise correta pode adiar a recomendação, manter vários candidatos empatados ou comparar faixas elegíveis com poucos visitantes de fato. Não é obrigatório escolher um único vencedor impressionante.



