Введение
Местной выставочной площадке нужно корректно сравнить часы работы. Большая сумма посетителей может объясняться лишь большим числом возможностей для наблюдения. Сначала вы сравните два интервала с одинаковой продолжительностью наблюдений, затем создадите повторно используемый анализ, который различает часы работы по плану, часы с записями и всё ещё неизвестные значения.
Лабораторная работа предполагает основы 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, добавьте этот блок один раз и повторите запуск.
Самопроверка: интервал без наблюдений и интервал с действительно наблюдённым нулём имеют разные суммы, знаменатели и интенсивности. В следующей работе вы используете это различие, чтобы решить, достаточно ли данных для сравнения.
Резюме
Вы построили диаграмму для равных возможностей наблюдения, сопоставили подсчёты по полному составному ключу и создали почасовую таблицу и сводку по интервалам. Закрытые часы, пропущенные записи и реальные нули остались различимыми. Интенсивность использует часы работы с записями, а полнота данных показывает, какая доля запланированных возможностей была записана.
Сохраните повторно используемый анализ и исходные данные. Эти описательные показатели за прошлый период не доказывают, что произойдёт при продлении часов работы. В следующей независимой среде вы примените явное правило достаточности данных перед выдачей рекомендации.



