Рекомендуйте часы работы на основе наблюдений

PythonBeginner
Практиковаться сейчас

Введение

Большая сумма посетителей ещё не определяет лучший интервал работы. Высокая часовая интенсивность по одной записи тоже даёт слабое основание для решения. Вы примените явное правило выставочной площадки, найдёте интервалы с достаточными данными, сохраните всех равных лидеров и сообщите об ограничениях вместе с настоящей диаграммой.

Предполагается предыдущая работа с почасовыми наблюдениями или эквивалентное понимание составных ключей, пропусков, часовой интенсивности и полноты данных, а также основы 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 и добавьте блок один раз. Оба скрипта при повторном запуске пересчитывают результаты, не изменяя входные данные.

Самопроверка: диаграмма следует текущей сводке, все равные кандидаты сохранены, а результат сопровождается ограничением интерпретации. В задании изменятся идентификаторы интервалов и данные другой площадки, но правила анализа останутся теми же.

Резюме

Вы отделили допустимость сравнения от ранжирования, сохранили всех равных лидеров и причину для каждого исключённого интервала. Две настоящие столбчатые диаграммы показывают рядом суммы посетителей и посетителей за записанный час, вместе с числом наблюдений и полнотой данных. Неизвестные показатели остались неизвестными, а рекомендация явно ограничивает причинные выводы из описательных исторических данных.

Ваши скрипты могут пересчитать результат для другой площадки. Корректный анализ может отложить рекомендацию, сохранить несколько равных кандидатов или сравнить допустимые интервалы с действительно низким числом посетителей. Он не обязан выбирать одного эффектного победителя.