Создайте сопоставимые почасовые наблюдения

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

Введение

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

Лабораторная работа предполагает основы Python, чтение, фильтрацию и группировку в pandas, а также столбчатые диаграммы Matplotlib. Python и нужные библиотеки уже установлены в WebIDE. Все данные синтетические: одна строка соответствует одному часу, а не личности посетителя. Продолжительность оценена автором с учётом чтения и исправления обычных ошибок. Вы создадите настоящие файлы CSV и PNG, сохранив исходные данные.

Сравните два интервала с одинаковым числом часов наблюдения

На этом шаге вы вручную рассчитаете интенсивность до построения первой диаграммы. Используйте нижний терминал во вкладке WebIDE Tab. Если его нет, откройте меню вверху слева и выберите Terminal → New Terminal. Команды выполняются в этом терминале, а файлы видны в Explorer слева.

Перейдите в подготовленную рабочую папку. cd меняет рабочий каталог только этого терминала, не других терминалов. pwd показывает текущий каталог. Если позднее файл не найдётся, вернитесь сюда и проверьте написание имени через ls.

cd /home/labex/project/opening-hours
pwd
/home/labex/project/opening-hours

cat показывает текстовый файл, не изменяя его. В этом CSV для каждого интервала есть два наблюдения продолжительностью один час.

cat data/equal.csv
slot,visitors
morning,6
morning,10
evening,8
evening,12

Утром за два записанных часа пришли 16 человек: 16 / 2 = 8 посетителей в час. Вечером за два часа — 20: 20 / 2 = 10. При одинаковых знаменателях вечер лидирует и по сумме, и по часовому показателю. Единица интенсивности — «посетители за один час с записью».

Создайте следующий короткий скрипт. Оператор оболочки > заменяет указанный файл. <<'PY' начинает here-document: текст сохраняется буквально до строки, содержащей только PY. Вставьте весь блок, включая последнюю строку. Файл создаётся сразу, сохранять его в редакторе не нужно. agg задаёт имена результирующих столбцов, а size считает часы наблюдений. index=False исключает индекс строк pandas из CSV. Agg позволяет Matplotlib сохранить PNG без отдельного окна.

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

Строки отсортированы по имени интервала. Начальные пробелы в выводе могут немного отличаться. Откройте opening-hours/results/equal.png в Explorer и рассмотрите сохранённую диаграмму. Два вертикальных столбца показывают 10 и 8 посетителей за записанный час. Подписи горизонтальной оси должны сохранять имена интервалов, а подпись вертикальной оси — Visitors per recorded hour. Цвета, шрифты, размер изображения и порядок интервалов можно менять. PNG — ваш настоящий результат, а не готовое эталонное изображение. Диаграмма должна быть детерминированной: при одинаковых входных данных повторный запуск текущего скрипта воспроизводит те же пиксели. Избегайте случайных стилей и отметок текущего времени. Метаданные PNG могут различаться. Цвета, шрифты и размер выбираете вы.

Обычная ошибка в имени файла лишь заставляет Python сообщить об отсутствии файла и остановиться; работа не сбрасывается. Следующий неудачный запуск намеренный.

python3 missing.py

Вы должны увидеть can't open file и No such file or directory. Выведите список файлов рабочей папки и повторите запуск с правильным именем.

ls
python3 equal.py

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

Сохраните каждый запланированный час

На этом шаге вы свяжете число посетителей с соответствующим запланированным часом. Продолжайте в терминале WebIDE в /home/labex/project/opening-hours. Каждая строка плана — один час определённого интервала в определённую дату. open=1 означает запланированную работу, open=0 — закрытие. Пара date и slot образует составной ключ: одного столбца недостаточно для идентификации строки. Формат даты — YYYY-MM-DD, все даты в файлах являются допустимыми календарными датами.

cat data/plan.csv
cat data/counts.csv

Файл подсчётов намеренно имеет другой порядок строк. 0 — настоящее наблюдение, что посетителей не было. Если для запланированного часа работы нет подсчёта, запись отсутствует, а не равна нулю. Для закрытых часов строк подсчёта нет. Каждая запись подсчёта должна соответствовать открытому часу плана. Составные ключи в обоих файлах должны быть полными и уникальными. Не скрывайте неверные ключи удалением дубликатов или выдумыванием подсчётов.

Левое соединение (left merge) сохраняет все строки левой таблицы — плана. Оно ищет подсчёт по двум столбцам ключа, а не по позиции строки. При отсутствии соответствия pandas добавляет пропущенное значение, отображаемое как NaN. Проверяйте полноту ключей до соединения: pandas может сопоставлять отсутствующие ключи друг с другом, но это не идентифицирует реальный час. duplicated(keys) обнаруживает повторяющиеся пары, а validate='one_to_one' требует отклонять связи с дубликатами.

Скрипт использует .loc[condition, column], чтобы присваивать значения только подходящим строкам. .eq(0) и .eq(1) сравнивают каждое значение с нулём или единицей, & объединяет условия, notna() отличает имеющееся значение от пропуска. Сначала применяется маска закрытых часов, затем маска открытых часов с записью, поэтому настоящий ноль тоже считается наблюдением.

Замените подготовленный observe.py следующим полным скриптом. sys.argv[1] и [2] читают два аргумента пути после имени скрипта. Path и / составляют пути к файлам. mkdir(parents=True, exist_ok=True) при необходимости создаёт выходную папку, позволяя анализировать другой входной каталог тем же скриптом. Два входных CSV только читаются и не изменяются.

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

Первая маска помечает закрытые часы. Вторая отмечает открытые часы с подсчётом как наблюдённые, включая настоящий ноль. & поэлементно объединяет два условия, а notna() проверяет наличие значения. valid_hour переводит условие наблюдения в 1 или 0; сумма даёт число пригодных часов наблюдения. Пропущенные подсчёты не заполняются нулями.

python3 observe.py data results
head -n 10 results/hours.csv

head -n 10 показывает заголовок и первые девять строк данных. Сохранённый CSV содержит date,slot,open,visitors,status,valid_hour и отсортирован по составному ключу. Рассмотрите его и в Explorer. Записанный ноль quiet имеет observed и valid_hour=1; пустое число посетителей unlogged — missing и valid_hour=0; пустой подсчёт closed — closed. Пустая ячейка означает неизвестное или неприменимое значение в зависимости от статуса, а не угаданный ноль.

Самопроверка: каждая строка плана встречается в hours.csv ровно один раз, а каждый подсчёт связан со своей датой и интервалом. Если появился ValueError, прочитайте, какая связь недопустима. Сверьте скрипт с этим кодом и неизменёнными входными файлами, исправьте и запустите снова. Открывать терминал заново не нужно.

Рассчитайте часовую интенсивность и полноту записей

На этом шаге вы сгруппируете данные по интервалам, не меняя смысла неизвестных подсчётов. Продолжайте в том же терминале WebIDE и рабочем каталоге.

У evening запланированы четыре часа работы, из которых три записаны с 12 посетителями каждый. Сумма — 36, наблюдённых часов — 3, интенсивность — 36 / 3 = 12, полнота — 3 / 4 = 0.75: записано 75% запланированных часов работы. Пропущенный час не входит в знаменатель интенсивности, но снижает полноту. У morning — 40 посетителей за четыре часа наблюдения: сумма больше, но интенсивность ниже — 10.

У quiet четыре записанных нуля дают сумму 0 и интенсивность 0 при полной регистрации. У unlogged нет наблюдённых часов: полнота равна 0, а сумма и интенсивность остаются пустыми. У closed запланировано 0 часов работы, поэтому полнота и интенсивность не определены и остаются пустыми. Позднее нельзя превращать неопределённые показатели в столбцы нулевой высоты.

Один раз добавьте следующий блок в observe.py. >> дописывает, а не заменяет файл. Используются созданные выше hours и output. sum(min_count=1) возвращает пропущенную сумму, если все значения отсутствуют; обычное суммирование возвращает ноль даже без действительных наблюдений. replace(0, float('nan')) явно заменяет нулевые знаменатели пропуском перед делением. При этом реально наблюдённый нулевой числитель сохраняется.

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,

Числа в CSV могут быть записаны как 36 или 36.0 без изменения результата. Обязательные столбцы и их смысл сохраняются. Наблюдённая интенсивность short самая высокая, но основана лишь на одном часе. У sparse три наблюдения покрывают только 60% запланированного времени. Поэтому перед рекомендацией нужно оценить достаточность данных. В этой работе вы пока не объявляете ни один из них победителем.

Откройте results/summary.csv в Explorer. Сохраните data/plan.csv, data/counts.csv, data/equal.csv и скрипты. Повторно используемый интерфейс — python3 observe.py INPUT_DIRECTORY OUTPUT_DIRECTORY: он читает два CSV из входного каталога и пишет hours.csv и summary.csv в выходной. Эквивалентные вычисления и другой порядок выходных строк допустимы при сохранении всех ключей и числовых связей. Если вы случайно заменили скрипт вместо дополнения, заново создайте его из шага 2, добавьте этот блок один раз и повторите запуск.

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

Резюме

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

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