Собесов

MediaScope — почасовая динамика телесмотрения с разнесением сессий по интервалам

PythonTime series / segmentationСредняяJunior

Условие

Дан датасет сессий телесмотрения (один респондент = много сессий). Поля:

ResearchDate, RespondentID, Start, Stop, Duration (сек), PackageID (телеканал), Weight (статвес), Title (демография, например Мужчины 45-54).

Нужно построить почасовую динамику смотрения в течение дня. Если сессия попадает в несколько часов (например, 15:45–17:08) — её надо разнести по часам: 15:00–16:00, 16:00–17:00, 17:00–18:00 — пропорционально длительности в каждом часе.

Дополнительно: всесторонний EDA, дашборд в Tableau, выводы.

Решение

Подход

«Размазать» сессию по часам = на каждый час h для сессии (Start, Stop) посчитать пересечение [max(Start, h), min(Stop, h+1h)]. Умножить на Weight. Просуммировать по респондентам и часам.

Реализация

import pandas as pd
import numpy as np
 
df = pd.read_excel('mediascope.xlsx', sheet_name='data')
df['Start'] = pd.to_datetime(df['Start'])
df['Stop']  = pd.to_datetime(df['Stop'])
df['date']  = df['Start'].dt.date
 
# Развернем сессию в часовые куски
def explode_to_hours(row):
    start = row['Start']
    stop  = row['Stop']
    h0    = start.replace(minute=0, second=0, microsecond=0)
    rows  = []
    h     = h0
    while h <= stop:
        seg_start = max(start, h)
        seg_end   = min(stop, h + pd.Timedelta(hours=1))
        if seg_end > seg_start:
            seconds = (seg_end - seg_start).total_seconds()
            rows.append((row['RespondentID'], h.hour, row['Title'],
                         row['Weight'], row['PackageID'], seconds))
        h += pd.Timedelta(hours=1)
    return rows
 
records = [r for _, row in df.iterrows() for r in explode_to_hours(row)]
ex = pd.DataFrame(records,
    columns=['resp_id', 'hour', 'title', 'weight', 'pkg', 'seconds'])
ex['weighted_seconds'] = ex['seconds'] * ex['weight']
 
# Почасовая динамика (минуты на 1 респондента, взвешенные)
hourly = (ex.groupby('hour')
            .apply(lambda g: g['weighted_seconds'].sum() / g['weight'].sum() / 60)
            .reset_index(name='avg_min_per_resp'))

Векторизованный (быстрее) вариант

При больших данных pandas apply(per row) будет медленным. Используйте «развернуть на часовые сегменты» через cross-join с расписанием:

hours = pd.DataFrame({'hour_start': pd.date_range('2021-02-06', '2021-02-07', freq='H')[:-1]})
df['key'] = 1; hours['key'] = 1
joined = df.merge(hours, on='key').drop('key', axis=1)
joined['seg_start'] = joined[['Start','hour_start']].max(axis=1)
joined['seg_end']   = joined[['Stop','hour_start_plus']].min(axis=1)  # +1h
joined = joined[joined['seg_end'] > joined['seg_start']]
joined['seconds'] = (joined['seg_end'] - joined['seg_start']).dt.total_seconds()

Лучше — групповой вариант через intervaltree или прямую формулу.

EDA — что искать

  • Прайм-тайм: пик 19:00–22:00 — типичный для рос. ТВ.
  • Дневной горб для пенсионеров (Женщины 55+).
  • Утренний пик для рабочих сегментов (новости 7–9 утра).
  • Различия по полу/возрасту в час прайма.
  • Доля «zapping» (короткие сессии < 60 сек) — сколько каналов перебирают.

Выводы (пример)

  • 60% всех минут смотрения приходится на 18:00–24:00.
  • Аудитория Мужчины 45-54 самая «вечерняя» — пик 21:00.
  • Аудитория Женщины 55-64 — утренний и дневной пики (10–14:00).

Проверка / интерпретация

  • Сумма seconds после разнесения должна равняться Duration для каждой сессии (sanity).
  • Сумма weighted_seconds за день должна давать осмысленную «средневзвешенную аудиторию».

Подводные камни

  1. Edge: Start == Stop (Duration=0 или 60 сек) — обрабатывать (исключать или не делить).
  2. Сессии через полночь: 19:30 → 02:00 следующего дня — нужно разнести и на следующий день. Цикл while с h <= stop должен продолжаться.
  3. Weight интерпретация: статвес = «представительство» респондента в популяции. Должен умножаться на наблюдение, а не суммироваться отдельно.
  4. Часовой пояс: ResearchDate vs Start.dt.date — могут не совпадать в полночь.
  5. Pivot по респонденту × часу: средняя «минуты на респондента» считается с учётом, что не каждый респондент был активен в каждый час. Делитель — общий sum(weight) или (resp × weight).
  6. PackageID: разнесение по часам нужно сохранить также в разрезе канала, иначе теряете возможность считать рейтинг канала.

Эталонный ответ

Разнесение сессии по часам: для каждого часа h пересечение [max(Start, h), min(Stop, h+1)]. Взвешивание на Weight. Группировка по часу × сегменту. Дашборд: тепловая карта (час × сегмент), линия по часам, разрез по PackageID для top-каналов.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти