Чи все так погано зі світлом — перевіряємо даними і графіками
Підписуйтеся на 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, додамо на кожен день підпис, підправимо легенду, а зверху ще доповнимо лінією тренду і
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 годин).
Але якщо взяти їх інтенсивність (один раз в
Звісно, ця всі аналітика не враховує зміни погоди і нелінійні деградації мережі, але вже хоч щось.
Додамо графіки відключень
Ця інформація вже корисна, але дуже цікаво і не менш корисно було б подивитись, чи відповідає статус тому, що є в графіках, і наскільки їм можна довіряти в умовах, що останнім часом ми майже весь час в стані аварійних відключень.
Спочатку згенеруємо графік (це графік групи № 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.
Якщо у вас є ідеї, чого тут не вистачило, або ви знайшли помилку — велкам в коментарі.
24 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів