Введение
Большая сумма посетителей ещё не определяет лучший интервал работы. Высокая часовая интенсивность по одной записи тоже даёт слабое основание для решения. Вы примените явное правило выставочной площадки, найдёте интервалы с достаточными данными, сохраните всех равных лидеров и сообщите об ограничениях вместе с настоящей диаграммой.
Предполагается предыдущая работа с почасовыми наблюдениями или эквивалентное понимание составных ключей, пропусков, часовой интенсивности и полноты данных, а также основы Python, pandas и Matplotlib. Эта среда запускается независимо с исходными синтетическими данными и полным observe.py. Файлы предыдущей среды не нужны. Продолжительность оценена автором, а не измерена при прохождении.
Примените явное правило достаточности данных
На этом шаге вы отделите допустимость сравнения от ранжирования. Откройте нижний терминал во вкладке WebIDE Tab или выберите Terminal → New Terminal в меню вверху слева. Из подготовленной рабочей папки запустите предоставленный анализ наблюдений. Два аргумента — входной и выходной каталоги.
cd /home/labex/project/opening-hours
python3 observe.py data results
cat results/summary.csv
В сводке сохранены все интервалы. У morning сумма 40, но интенсивность 10; у afternoon и evening интенсивность 12. У short интенсивность 20 всего по одному записанному часу. У sparse три записанных часа, но полнота 0.6. У quiet наблюдался реальный ноль, а интенсивность unlogged неизвестна.
Правило площадки для упражнения открыто: интервал можно сравнивать, только если есть не менее 3 наблюдённых часов И полнота не ниже 0.75. Равенство границе тоже допускается. Три наблюдения из четырёх запланированных часов соответствуют обеим границам. Сравнивайте допустимые интервалы по посетителям за наблюдённый час. Сохраняйте все интервалы с абсолютной разницей от максимальной допустимой интенсивности не больше 1e-9. Этот малый допуск учитывает представление чисел с плавающей точкой, а не значимую для бизнеса разницу. Если допустимых интервалов нет, отложите рекомендацию. Это правило решения для упражнения, а не универсальный порог статистической уверенности.
Сохраняйте причину для каждого интервала. Приоритет: no_open_hours, если запланированных часов работы ноль; иначе too_few_observations, если наблюдённых часов меньше трёх; иначе low_coverage, если полнота ниже 0.75; иначе eligible. Так различаются закрытые интервалы, интервалы без записей и интервалы с достаточным количеством записей, но недостаточной полнотой. Не удаляйте недопустимые интервалы из сравнения.
set_index('slot') делает устойчивое имя интервала меткой строки. Вычитание максимума среди кандидатов и .abs().le(1e-9) сохраняют все значения в заданном допуске. sorted задаёт устойчивый порядок списка интервалов в решении.
Создайте recommend.py. Как и раньше, > заменяет файл, а here-document в кавычках сохраняет текст буквально до PY. sys.argv читает путь сводки и выходной каталог. .ge() означает «больше или равно»; маски классифицируют целые столбцы. Если выбранной интенсивности нет, None превращается в JSON null. JSON — формат структурированного текста. json.dumps сохраняет список идентификаторов интервалов, поэтому равные лидеры представлены списком, а не случайной первой строкой.
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
}
Оба равных интервала сохранены. short не кандидат, несмотря на высокую интенсивность, а sparse исключён из-за неполноты данных. quiet допустим даже при нулевой интенсивности: низкая посещаемость не означает недействительные данные. Если все допустимые показатели нулевые, все допустимые интервалы делят первое место и могут сравниваться. Если ни один не допустим, правильный результат — status: defer, пустой список slots и rate: null.
Самопроверка: откройте results/comparison.csv в Explorer и найдите причину для каждого интервала. Интерфейс программы — python3 recommend.py SUMMARY_CSV OUTPUT_DIRECTORY. Она должна работать с другой корректной сводкой, не полагаясь на фиксированные имена или заранее выбранного победителя.
Покажите данные рядом с рекомендацией
На этом шаге вы покажете сумму и интенсивность вместе, не выдавая пропуски за нули. Продолжайте в том же терминале WebIDE в /home/labex/project/opening-hours.
Минимальный формат диаграммы: две панели с вертикальными столбцами, идентификаторы интервалов в подписях горизонтальных осей и подписи вертикальных осей Total visitors и Visitors per recorded hour. Покажите число наблюдённых часов и полноту каждого интервала, включая недопустимые. Для отсутствующей суммы или интенсивности не рисуйте числовой столбец и укажите unknown. Цвета, шрифты, размеры и порядок интервалов свободны. Диаграмма должна детерминированно воспроизводить те же пиксели текущим скриптом при одинаковом входе; метаданные PNG могут отличаться. Не добавляйте случайный стиль или текущее время на изображение.
Цикл через zip берёт по одной оси, имени столбца и подписи единицы измерения. summary.iterrows() перебирает интервалы и строки, а enumerate добавляет числовую позицию столбца. Формат f-строки :.0% выводит долю как процент без десятичных знаков. pd.notna проверяет наличие полноты перед форматированием. annotate размещает текст в позиции данных. xytext=(0, 5) с textcoords='offset points' сдвигает текст на пять типографских пунктов вверх. ha='center' центрирует текст, а fontsize=7 делает подпись данных компактной. Эти параметры оформления не меняют вычисления.
Один раз добавьте блок в recommend.py: >> сохраняет существующий код. subplots создаёт две оси. range(len(summary)) даёт позицию каждому интервалу. Для числа наблюдённых часов и полноты в целых процентах используйте n=N; cov=P%, а для неопределённой полноты — cov=n/a. Подпись показывает объём данных в этой позиции. Фактические показатели недопустимых интервалов тоже видны, но причины и решение отличают их от рекомендованных кандидатов. Отсутствующая интенсивность остаётся отсутствующей.
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
Откройте results/comparison.png в Explorer WebIDE. Читайте панели суммы и интенсивности вместе. Хотя утренняя сумма больше, часовая интенсивность днём и вечером выше. У short видно n=1, у sparse — cov=60%. quiet — настоящий ноль, а показатели unlogged и closed неизвестны. Не заменяйте неизвестные значения столбцами нулевой высоты.
cat results/advice.txt
Decision: compare
Slots: afternoon, evening
Evidence: historical description
Effect of extending hours: not established
Рекомендация использует четыре открытых поля: Decision — compare или defer; Slots — устойчивые ID интервалов через запятую или none; Evidence — historical description; Effect of extending hours — not established. Порядок полей и интервалов свободный, после полей можно добавить объяснение. Краткая структура согласует рекомендацию с реальным решением и явно указывает область интерпретации, не полагаясь лишь на убедительный текст.
Рекомендация ограничена наблюдёнными прошлыми возможностями и явным правилом. Она не показывает, что изменение часов работы вызовет ту же интенсивность посетителей. Полезный следующий шаг — собрать недостающие записи перед решением по плохо записанным интервалам. advice.txt сообщает результат, но подтверждают его почасовые связи, сравнение, решение и реально построенные значения.
Сохраните оба скрипта, два исходных CSV, hours.csv, summary.csv, comparison.csv, decision.json, настоящий PNG и рекомендацию. Если ошиблись в имени файла, прочитайте ошибку, найдите файл через ls и запустите с правильным путём. Если заменили скрипт вместо дополнения, воссоздайте его из шага 1 и добавьте блок один раз. Оба скрипта при повторном запуске пересчитывают результаты, не изменяя входные данные.
Самопроверка: диаграмма следует текущей сводке, все равные кандидаты сохранены, а результат сопровождается ограничением интерпретации. В задании изменятся идентификаторы интервалов и данные другой площадки, но правила анализа останутся теми же.
Резюме
Вы отделили допустимость сравнения от ранжирования, сохранили всех равных лидеров и причину для каждого исключённого интервала. Две настоящие столбчатые диаграммы показывают рядом суммы посетителей и посетителей за записанный час, вместе с числом наблюдений и полнотой данных. Неизвестные показатели остались неизвестными, а рекомендация явно ограничивает причинные выводы из описательных исторических данных.
Ваши скрипты могут пересчитать результат для другой площадки. Корректный анализ может отложить рекомендацию, сохранить несколько равных кандидатов или сравнить допустимые интервалы с действительно низким числом посетителей. Он не обязан выбирать одного эффектного победителя.



