Чи все так погано зі світлом — перевіряємо даними і графіками

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Усім привіт, мене звати Євген Ковалевський, і я працюю CPO/CTO в TECHIIA та KOLO. Я вирішив за допомогою звичних мені інструментів і коду перевірити, чи справді така невтішна ситуація зі світлом у моєму районі столиці і наскільки можна довіряти графікам відключення електроенергії.

Русня дістала вже всіх. І їхні обстріли нашої інфраструктури дуже скоро будуть відплачені їм. Але допоки наші воїни працюють над відплатою — під кожним постом про відключення світла набігає цілий натовп наших земляків, які шукають когось винного, окрім нашого безпосереднього ворога. З цієї ж серії — і написання скарг, що сусіду мають вимкнути світло, щоб усе було справедливо.

Дивлячись на це все — виникло бажання перевірити цифрами, а чи дійсно все ТАК погано, і куди це все рухається.

Дисклеймер: В коді є неоптимальні місця, але їх «вичісувати» я не буду, бо вирішив витратити цей час на те, щоб написати цей пост, з надією що комусь може бути корисним або хоча б просто цікавим, як можна звичайний набір однотипних повідомлень в чаті перетворити в корисну аналітику.

Графіки краще за емоції

Завжди любив візуалізувати дані, бо так все стає набагато зрозуміліше. І от, знайшовши чат, в якому є повідомлення про відключення і повернення світла в ЖК «Славутич», і перевіривши, що в нього близька до 100% кореляція з ще парою ЖК на Позняках («Патріотика», «Зарічний» як мінімум) я не втримався, щоб не дістати Jupyter Notebook, Pandas i Numpy. І, поїхало.

Шкода тільки, що повідомлення там є тільки від 10 листопада. Але це вже щось.

Готуємо дані

Для початку — вивантажуємо всі повідомлення з каналу в JSON-файл і розкладаємо його в датасет. На основі повідомлень вирахуємо статус світла.

Надалі у нас 1 — буде відповідати за наявність світла, а 0 — за його відсутність.

import pandas as pd
import numpy as np
#%%
def get_state(text):
    #визначаємо зміну статусу на основі тексту
    if '❌' in text:
        return 0
    elif '✅' in text:
        return 1
    else:
        return None

raw_json = pd.read_json('result.json')
json_df = pd.json_normalize(raw_json['messages'])  #розкладаємо словник messages в стовпчики
json_df.date = pd.to_datetime(json_df.date)
json_df.set_index('date', inplace=True)
json_df['state'] = [get_state(x) for x in json_df.text]  #вираховуємо стейт на основі тексту
json_df = json_df[['state']].dropna(subset = ['state'])  #видаляємо повідомлення які не про зміну стейту
json_df = json_df.groupby(pd.Grouper(level='date', freq='T')).max() #заповнюємо індекс відмітками кожної хвилини
json_df = json_df.fillna(method="ffill")  #заливаємо точки з пустими значеннями найбільш актуальним станом
json_df = json_df.loc['2022-11-11':'2022-12-16']  #прибираємо неповні дні. TODO: зробити динамічним на основі датасету

json_df.head()

І матимемо на виході:

date                  state    
2022-11-11 00:00:00    1.0
2022-11-11 00:01:00    1.0
2022-11-11 00:02:00    1.0
2022-11-11 00:03:00    1.0
2022-11-11 00:04:00    1.0
...

Починаємо малювати

Відразу подивимось, як це виглядає:

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()
json_df.asfreq('T').plot(figsize=(15, 5))

Користі з такої візуалізації доволі мало. Якщо згрупувати за днями — має стати краще.

Для обрахунку статусу при групуванні візьмемо середнє значення статусів і одразу отримаємо кількість світла на день у відсотках, що доволі зручно.

df = json_df.groupby(pd.Grouper(level='date', freq='D')).mean()  #групуємо по дням
fig, ax = plt.subplots(figsize=(15,5))
ax.plot(df, label='% per day')
ax.set_ylim([0,1.1])
ax.legend(loc=2)

Стало краще. Але все одно, ще не дуже зрозуміло, тому настав час діставати магію цього стеку.

Тепер нормально візуалізуємо

Перемалюємо в bar-chart, додамо на кожен день підпис, підправимо легенду, а зверху ще доповнимо лінією тренду і 7-денним центрованим середнім, щоб дивитись, як кожен день на цей тренд впливає.

fig, ax = plt.subplots(figsize=(15,5))
plt.xticks(rotation = 75)
width = 0.7
p1 = ax.bar(df.index, df.state, width)
ax.set_xticks(df.index)
ax.set_title('% часу зі світлом (Патріотика)')
ax.bar_label(p1, labels=[f'{x:.2%}' for x in df.state],rotation=90,fontsize=10,label_type='center', color='white')

#add trend-line and avg
import matplotlib.dates as mdates
x = mdates.date2num(df.index)
y= df['state']
z = np.polyfit(x, df['state'], 1)
p = np.poly1d(z)
trend_plot = ax.plot(x, p(x), "r--", label='Trend')

df['moving_avg'] = df.rolling(7, center=True, min_periods=1).mean().to_numpy()
moving_plot = ax.plot(df.moving_avg, c='g', label='7d moving AVG')

plt.legend()
plt.show()

Оце вже краще. Якщо взяти, що темп обстрілів буде зберігатись і нема якоїсь критичної точки деградації електросистеми, то на середину березня будемо мати 40% наявності світла на день (~9,5 годин).

Але якщо взяти їх інтенсивність (один раз в 13-14 днів), то за три місяці для збереження такого негативного тренду має відбутись 6-7 атак, на що, за оцінками аналітиків, ракет вже не вистачить.

Звісно, ця всі аналітика не враховує зміни погоди і нелінійні деградації мережі, але вже хоч щось.

Додамо графіки відключень

Ця інформація вже корисна, але дуже цікаво і не менш корисно було б подивитись, чи відповідає статус тому, що є в графіках, і наскільки їм можна довіряти в умовах, що останнім часом ми майже весь час в стані аварійних відключень.

Спочатку згенеруємо графік (це графік групи № 3):

#Let's generate outages schedule
def get_schedule_state(d):
    schedule = {'Monday': [[0, 3], [9, 12], [18, 21]],
                'Tuesday': [[3, 6], [12, 15], [21, 23]],
                'Wednesday': [[0,0], [6,9], [15,18]],
                'Thursday': [[0, 3], [9, 12], [18, 21]],
                'Friday': [[3, 6], [12, 15], [21, 23]],
                'Saturday': [[0,0], [6,9], [15,18]],
                'Sunday': [[0, 3], [9, 12], [18, 21]]}
    for periods in schedule[d.day_name()]:
        if periods[0] <= d.hour <= periods[1]:
            return 0
    return 1

json_df['schedule_state'] = [get_schedule_state(x) for x in json_df.index]
json_df['is_scheduled'] = (json_df['schedule_state'] == json_df['state']).astype(int)
df = json_df.groupby(pd.Grouper(level='date', freq='D')).mean() # згрупуємо з новими даними

І відразу доповнимо минулу візуалізацію:

fig, ax = plt.subplots(figsize=(15,5))
plt.xticks(rotation = 75)
width = 0.7
p1 = ax.bar(df.index, df.state, width, label = 'По факту')
p2 = ax.plot(df.schedule_state, color = 'red', label = 'Розклад')
ax.set_xticks(df.index)
plt.legend()
ax.set_title('% часу зі світлом (Патріотика)')
ax.bar_label(p1, labels=[f'{x:.2%}' for x in df.state],rotation=90,fontsize=10,label_type='center', color='white')
plt.show()

Що цікаво, не дивлячись на емоційне відчуття «світла менше чим обіцяли», — менше від заявленого у графіку його було всього пару днів. Схоже на те, що відчуття у нас формуються від відповідності графіку, а не загальної кількості світла. Треба перевіряти.

Відповідність графіку

Для того, щоб оцінити — сформуємо 4 статуси: світло по графіку, світло поза графіком, без світла по графіку і без світла поза графіком.

# Вирахуємо статуси на основі розкладу
json_df['scheduled_on'] =  np.where((json_df['state'] == 1) & (json_df['schedule_state'] == 1), 1, 0)
json_df['unscheduled_on'] =  np.where((json_df['state'] == 1) & (json_df['schedule_state'] == 0), 1, 0)
json_df['scheduled_off'] =  np.where((json_df['state'] == 0) & (json_df['schedule_state'] == 0), 1, 0)
json_df['unscheduled_off'] =  np.where((json_df['state'] == 0) & (json_df['schedule_state'] == 1), 1, 0)
full_states = json_df.groupby(pd.Grouper(level='date', freq='D')).mean()
full_states.mean()  # Виведемо тотали


state              0.686806
schedule_state     0.547454
is_scheduled       0.668711
scheduled_on       0.451485
unscheduled_on     0.235320
scheduled_off      0.217226
unscheduled_off    0.095968

Тобто:

було світло: 68,7%
мало бути за графіком: 54,7%
було ЗА графіком: 45,1%
було НЕ за графіком: 23,5%
не було ЗА графіком: 21,7%
не було НЕ за графіком: 9,6%(!)

Значить, світло було поза графіком в 2 рази частіше, чим навпаки. Гляньмо на це за різними днями:

fig, ax = plt.subplots(figsize=(15,5))
plt.xticks(rotation = 75)
width = 0.7
states = ['Scheduled ON', 'Unscheduled ON','Scheduled OFF', 'Unscheduled OFF']
son = ax.bar(full_states.index, full_states.scheduled_on, width, color='green')
uon = ax.bar(full_states.index, full_states.unscheduled_on, width, bottom=full_states.scheduled_on, color='darkgreen')
soff = ax.bar(full_states.index, full_states.scheduled_off, width,
            bottom=full_states.unscheduled_on+full_states.scheduled_on, color='orange')
usoff = ax.bar(full_states.index, full_states.unscheduled_off, width,
            bottom=full_states.scheduled_off+full_states.unscheduled_on+full_states.scheduled_on, color='red')
ax.set_xticks(full_states.index)
plt.legend(states, bbox_to_anchor=(0,1.02,1,0.2), loc="upper left", mode='expand', ncol=4)
fig.tight_layout()
ax.set_title('% статусів по дням')

plt.show()

По графікам залишилось останнє запитання: наскільки ми взагалі можемо на нього покладатись.

fig, ax = plt.subplots(figsize=(15,5))
plt.xticks(rotation = 75)
width = 0.7
p1 = ax.bar(full_states.index, full_states.is_scheduled, width)
mean = np.nanmean(full_states.is_scheduled)
avg_line = ax.axhline(y=mean, linestyle='--', label=f'Середній: {mean:.2%}', color='blue')
ax.set_xticks(full_states.index)

ax.set_title('% часу з відповідністю графіку')

ax.bar_label(p1, labels=[f'{x:.2%}' for x in full_states.is_scheduled],rotation=90,fontsize=10,label_type='center', color='white')
print(full_states[['state', 'schedule_state']].corr())
plt.legend()
plt.show()

Як бачимо тут у нас доволі напружено все. Середній показник за весь час — 66,87%, а кореляція між фактом і графіком 21,2%.

Нагадаю, що майже весь цей час Київ перебуває в стані аварійних відключень і графіки офіційно не діють. Ну, і наявність світла поза графіком, статистично, також впливає негативно на цей показник.

Коли світла більше

Тоді дізнаймося, коли ж у нас більше шансів зловити світло, щоб зарядити EcoFlow. Для цього зробимо pivot-table де по вертикалі візьмемо години, а по горизонталі — дні тижнів, і намалюємо.

dfh = json_df.groupby(pd.Grouper(level='date', freq='H')).mean()
dfh['wd'] = dfh.index.day_name()
dfh['hour'] = dfh.index.hour
heatmap = pd.pivot_table(dfh, values='state', index=['hour'], columns=['wd'], aggfunc=np.mean)
column_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
heatmap = heatmap.reindex(column_order, axis=1)

import matplotlib as mpl
fig, ax = plt.subplots(figsize=(7,7))
cmap = plt.colormaps['summer_r']
cmap0 = mpl.colors.LinearSegmentedColormap.from_list(
        'red2green', ['red', 'green'])
pcm = ax.pcolormesh(heatmap , cmap=cmap0, edgecolors='k', linewidths=0.2,linestyle='dashed')
ax.set_frame_on(False)
ax.set_yticks(np.arange(heatmap.shape[0]) + 0.5, minor=False)
ax.set_xticks(np.arange(heatmap.shape[1]) + 0.5, minor=False)
ax.invert_yaxis()
ax.xaxis.tick_top()
fig.tight_layout()
fig.colorbar(pcm, ax=ax)
ax.set_xticklabels(column_order, minor=False)
ax.set_yticklabels(heatmap.index, minor=False)
plt.xticks(rotation=90)

ax.grid(False)
plt.show()

Тут висновків можна зробити декілька:

  • слоти відключень таки можна прослідкувати — діагональні червоні лінії;
  • на вихідних світла більше, аніж в будні;
  • середа-пʼятниця з 12 до 19 — часто світла нема, навіть поза графіком.

Думаю, цього достатньо.

Висновок

Можемо підсумувати, що графік — це добре, але покластись на нього, поки що, доволі складно. Ну, і віримо в наших енергетиків-титанів і ЗСУ — скоро стане краще. Та і в цілому — ввести повний блекаут в таких тенденціях і поточних запасах ракет русні буде доволі складно. Ну, а щоб пришвидшити нашу перемогу — завжди можна закинути пару донатів на KOLO чи долучитись до зборів DOU.

Якщо у вас є ідеї, чого тут не вистачило, або ви знайшли помилку — велкам в коментарі.

👍ПодобаєтьсяСподобалось14
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Закінчується зима, відключень стає все менше, уже декілька днів вмикають вуличне освітлення.
Цікаво було б подивитись графік за всю зиму.

Не вистачає в аналітиці часто головного — правильно поставлених запитань.

Вітаю, дякую за статтю, не знаєте як збирається інформація для чату з повідомленнями про відключення?

Конкретно цей не знаю. Але вцілому часто через Home Assistant / AJAX збирають

Вітаю, Євгене .

Вдячний за продемонстровану роботу. Завжди захоплювався людьми , що опанували math plot lib.

Незважаючи на дисклеймер, хотів би попитати наступне:

1) Не вистачає все ж таки вихідного набору сирих даних та повного ноутбуку десь в репозиторії для наочності. Або хоча б вивід самої таблички після кожної трансформації , додаванні колонок — складно уявити скільки всього колонок і яких в результуючому наборі даних;

2) pd.Grouper прикольно, але навіщо? Хіба group by не може працювати з індексом напряму? Навіть якщо він і date time типу. Бо як каже дзен — readability counts.

3) Ваша функція

get_schedule_state(d):

— я думаю тут просто необхідні type hints та doc strings. Особливо зважаючи як ви використовуєте специфічні для дати / часу атрибути і методи hour та day_name(). Або графік відключень винести з тіла функції і передавати окремим аргументом.

До речі чому list comprehension і списки замість генераторів та кортежів ? На невеличкому обсязі даних має бути непомітно, але ж дані мають тенденцію рости.

4) Розрахунок світла за розкладом : судячи з schedule — кожен день крім суботи та середи світла нема 9 годин (9/24 це приблизно 37,5 відсотка від доби) і 6 годин в суботу/середу , що складає 25 відсотків. Отже червона лінія розкладу має бути в діапазоні від 62,5 до 75 відсотків. На графіку вона нижча... Чому?

Дослідження чимось нагадує ІТ у мініатюрі — інструментарій, який міг би видати щось практично корисне простішими засобами (наприклад, вирахувати мінімально необхідну ємність і потужність зарядного пристрою зарядної станції, щоб заряд не впав до нуля, два конкретні числа), видає якісь складні у використанні дані, до того ж які нерепрезентативні і застарівають при наступному маштабному обстрілі інфраструктури.

Головний висновок — про негатив з виключеним світлом напишуть сотня сусідів, а за те що лишню годинку світло не вимкнули ніхто не подякує :)

На годинку подовше буває з різних причин, у мене під час останньої атаки весь день світло було, навіть по графіку не вимикали, а все це тому що електрохарчування більше нікуди було дівати — Київ і більша частина області залишилась без світла

такі графіки знижують тривожність та дають можливість хоч якогось контролю
Дякую автору)

Я дуже радий, що світла в день вам дають стільки, що є час на всіляку х..графіки, а не дві години на добу. Сподіваюся, тернопольчани вас не знайдуть)

Ну я їх підготував з тиждень тому, в великій мірі якраз сидячи офлайн і колупаючи один датасет. Тому дякую за ваші теплі слова і приєднаюсь до ваших сподівань, що уж))

Так класно, шо ми радіємо один за одного 🥰

Інсайт (не Київ, але яка різниця) — сиділи по 16 годин без світла ще три тижні тому при практично постійній подачі у сусідів. Все просто тому, що у нас новіший трансформатор і його можна вимикати віддалено, а до сусідського треба їхати вмикати-вимикати вручну. Зараз всі наскаржились — більш-менш клацають часто, головне що не більше ніж на 8 годин офф, не встигають розряджатись упси на оптиці

Звучит, как будто Вы из Тернополя))) У нас вчера весь офис угорал с того, что там кто-то пожаловался на соседей, что у них всё время свет есть)))

Звучить, ніби Ярослав (і можливо люди з Тернополя) зменшили собі час відключень вдвічи

В телеграм чятіку мого міста таке постійно

— от в сусідів світло є, куди писать комплейн?!

І, судячи з логів, скарги таки пишуться.

Чимось нагадує русню. Ті також своєю метою ставлять не те щоб краще їм жилося а щоб сусіду було гірше

Для випадку нагорі цього треду не зовсім точна аналогія. Точніше, навіть, зовсім не точна.
Там грубо кажучи є дві групи користувачів з приблизно рівним споживанням и можливістю економити на обох групах, але для відключення однієї групи електрику треба підняти дупу, а іншу він може відключити прямо з дивану. И як наслідок у першої групи все добре, а друга група страждає за двох. Тут в випадку, якщо електрик таки підійме дупу, сусіду стане гірше, але і тобі таки стане краще.
Ваша аналогія — то для випадків, коли комусь фізично немає можливості дати більше енергії (тому що все розйобане), сусіди підключені з якогось зовсім іншого джерела, до якого немає можливості швидко підключити тебе, і ти йдеш скаржитись «відключить їх також», знаючи, що їм буде гірше, а для тебе нічого не зміниться.

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

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

И что с этими данными потом делать? Распечатать и на холодильник? Если у меня проблемы с электричеством, то это и так очевидно и мне не нужно для понимания этого строить графики.

«Нашо мені ти синуси і косинуси, додавати-ділити вмію»

У собак есть блохи. Блоха это ...

Підписатись на коментарі