Сегодня у наших друзей в «Кинжале» вышла статья про то, как анализировать свою продуктивность. В статье была ссылка на Google Colab с кодом на Python, где и происходила вся магия анализа. Сегодня мы разберём, что делает и как работает этот код.
Кратко смысл проекта:
- Программа ActivityWatch следит за вашей активностью: какие программы у вас открыты и какие там вкладки браузера.
- Из программы мы достаём сырой файл с данными о продуктивности.
- Этот файл анализируется в заранее написанном Python-скрипте.
- Скрипт рисует визуализацию. По ней видно, когда человек работал, а когда отвлекался:
Что делаем
Сегодня мы разберём, как работает тот визуализирующий скрипт, который превращает сырые данные в наглядную визуализацию. Скрипт лежит в Google Colab.
Наша задача — понять, как устроен этот код, почему он работает и что в нём можно докрутить, чтобы было удобнее.
Для проекта мы будем использовать реальные данные одного пользователя, которые экспортированы из программы ActivityWatch в CSV-файл.
Как загрузить файл в Colab
Для работы вы можете использовать собственный файл из ActivityWatch. Нужно дать программе поработать у вас на компе несколько дней, потом зайти в Raw Data и экспортнуть первую строку watcher-window в виде CSV.
Чтобы загрузить этот файл с данными в онлайн-блокнот, слева нажимаем на значок папки и в появившийся раздел перетаскиваем с компьютера нужный файл:
Импортируем библиотеки
Чтобы скрипт заработал, в колабе нужно подключить несколько библиотек. Одна посчитает данные, вторая поможет их обработать, а третья — вывести результат. Все эти библиотеки сразу доступны в Google Colab, поэтому мы просто импортируем их соответствующей командой:
# импортируем нужные библиотеки
# для работы с данными
import pandas as pd
# библиотека для обработки больших массивов с числами
import numpy as np
# для работы со временем
from datetime import timedelta, datetime, time
# для визуализации:
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
# для работы с регулярными выражениями
import re
Указываем цвета для категорий
Нам нужно красить нашу активность в разные цвета на графике. Для этого нужно определить, что это будут за цвета. Названия цветов мы положим в переменные, которые будут означать категории — работу, отдых и т. д.
Вот табличка с названиями цветов для ориентира:
# задаём цвета для категорий
# работа
work = 'tab:cyan'
# развлечения
play = 'tab:pink'
# общение
communicate = 'tab:olive'
# обучение
study = 'tab:blue'
# прочее
other = 'lightgrey'
Обрабатываем переработку
Иногда бывает так, что человек засиживается за задачами и заканчивает работу уже за полночь. Технически это уже новый день, но по факту у него продолжался старый день — он просто немного залез на новый. Чтобы обработать на графике такую ситуацию, мы проверим, зашло ли дело после полуночи на другой день. Точка отсечки — 6 утра: всё, что после, уже считается новым днём.
# переменные для 6 утра и полуночи
six_am = time(6, 0, 0)
midnight = time(0, 0, 0)
# функция, которая считает, сколько секунд прошло с 6 утра
def seconds_from_6_am(dtobject):
# если событие произошло после полуночи и до 6 утра — оставляем его в этом дне, но рисуем после полуночи
if (dtobject > midnight) & (dtobject < six_am):
return dtobject.hour * 3600 + dtobject.minute * 60 + dtobject.second + (dtobject.microsecond / 1000000) + (24 * 3600)
# если рабочий день помещается в календарный
else:
return dtobject.hour * 3600 + dtobject.minute * 60 + dtobject.second + (dtobject.microsecond / 1000000)
То же самое сделаем и для даты: если дело не вылезло за 6 утра — оставляем его в предыдущем дне. Читайте комменты:
# переменные для 6 утра и полуночи
six_am = time(6, 0, 0)
midnight = time(0, 0, 0)
# функция отображения даты задачи
def show_the_date(stampobj):
# если задача выполнялась после полуночи, но до 6 утра — оставляем её в предыдущем дне
if (stampobj.time() > midnight) & (stampobj.time() < six_am):
return stampobj.date() - timedelta(1)
# в противном случае показывается как есть
else:
return stampobj.date()
Загружаем и чистим данные
Теперь загружаем данные, которые выгрузили из ActivityWatch, и сразу чистим их от мусора. Например, нам не нужны события, которые длились 0 секунд (случайное окно) или если это было окно входа в систему. Сразу же для каждого события помечаем время начала, дату и длительность — это поможет построить график. Читайте комменты:
# считываем файл с данными и сразу преобразуем время в нужный формат
data = pd.read_csv('watcher.csv', parse_dates=['timestamp'])
# переводим всё в нужный часовой пояс
data['timestamp'] = data['timestamp'].dt.tz_convert('Europe/Moscow')
# оставляем в таблице всё, кроме экрана входа в систему и событий, которые нисколько не длились
data = data[(data.duration > 0) & (data.app != 'loginwindow')].reset_index(drop=True)
# отмечаем время начала
data['start'] = pd.to_datetime(data['timestamp']).apply(lambda x: seconds_from_6_am(x.time()))
# отмечаем дату начала
data['date'] = pd.to_datetime(data['timestamp']).apply(lambda x: (show_the_date(x)))
# добавляем столбец с подписями вертикальной оси, там будет дата
data['yticks'] = pd.to_datetime(data['timestamp']).apply(lambda x: (f'{x.day} {x.month_name()}'))
Ищем уникальные названия приложений
Чтобы классифицировать, какие приложения у нас для работы, а какие нет, нужно знать названия этих приложений. Чтобы не гадать и ничего не забыть, нам нужно узнать, какие приложения есть в нашем файле с данными. Для этого мы обращаемся к объекту data, берём из него всё, что находится в ячейке app, и ищем там уникальные значения с помощью метода unique(). В результате Python найдёт все названия приложений, но не выведет повторы:
# выводит список уникальных приложений
data['app'].unique()
Распределяем приложения по категориям
Здесь мы сами уже решаем, что считать рабочими приложениями, а что развлекательными. Если вы в Ноушене составляете подборки анекдотов, чтобы почитать по дороге на работу, — это будет развлечение, а не работа. Общего критерия нет, смотрите сами, что куда относить.
# распределяем приложения по категориям
def categorized_apps(app):
# для работы
if app in ['Notion', 'Figma', 'Microsoft PowerPoint', 'Notes',
'Microsoft Excel']:
cat = work
# для общения
elif app in ['Microsoft Outlook', 'Telegram', 'zoom.us']:
cat = communicate
# для обучения
elif app in ['Postman', 'Terminal', 'Code', 'MongoDB Compass 1.38.0',
'Sublime Text']:
cat = study
# всё, что не вошло в предыдущие, — это прочее
else:
cat = other
return cat
Разбираемся с браузером
Хитрость браузера в том, что он может служить как для работы, так и для образования или развлечения. Поэтому определять приложение «Браузер» в нашей визуализации не смысла. Нужно уходить в заголовки страниц.
Дальше в коде нужно раскидать, какие заголовки относятся к развлечению, какие — к работе и т. д. Способ поиска — регулярные выражения: это когда компьютер ищет в тексте какие-то сочетания символов.
# распределяем события по заголовкам браузера
def categorized_titles(text):
# для работы
if re.search('Google Таблицы|Google Документы|Colaboratory', text):
cat = work
# для развлечений
elif re.search('Яндекс Музыка|Youtube|Кинопоиск|kinopoisk', text):
cat = play
# для общения
elif re.search('Google Meet|Mattermost|Пачка|Яндекс Почта|Messenger|mail', text):
cat = communicate
# для обучения
elif re.search('Яндекс Практикум|Yandex Cloud', text):
cat = study
# прочее
else:
cat = other
return cat
Распределяем события по категориям
Теперь мы переберём все события, и если это браузер — смотрим на заголовок, а если нет — то на название программы. Так мы обработаем все события и отнесём каждое к какой-то категории:
# распределяем всё по своим категориям
colors = []
# перебираем все события
for i in range(len(data)):
# если это браузер
if data.loc[i,'app'] in ['Google Chrome', 'Safari']:
# если нет заголовка — отправляем в Прочее
if pd.isna(data.loc[i,'title']):
colors.append(other)
else:
# если есть — смотрим на содержимое заголовка
colors.append(categorized_titles(data.loc[i,'title']))
else:
# если это не браузер, смотрим на название приложения
colors.append(categorized_apps(data.loc[i,'app']))
# присваиваем категории выбранные цвета
data['color'] = colors
Рисуем график
Мы сделали всю подготовительную работу — осталось построить график и посмотреть на результаты анализа. Для этого мы формируем начальную и конечную даты, настраиваем и подписываем оси графика, добавляем легенду. После этого перебираем все события внутри каждого дня и наносим их в виде прямоугольника на график. Чем длиннее событие — тем шире прямоугольник.
Как всё будет готово — отображаем результат на экране и сохраняем картинку в PNG-файл.
Читайте комментарии к коду, чтобы понять, что тут происходит.
# группируем всё по дням
days = data.groupby('date')
# находим начальную и конечную даты
min_day = data['date'].min()
max_day = data['date'].max()
# готовим график к рисованию
fig, ax = plt.subplots(figsize=(10, 10))
# настраиваем оси
ax.set_ylim(min_day, max_day + timedelta(days=1))
ax.set_xlim(21600, 93600)
# добавляем время на ось X
ax.set_xticks([21600, 43200, 64800, 86400, 93600],
labels=['06:00', '12:00', '18:00', '24:00', '02:00'])
ax.set_yticks(list(data['date'].unique()),
labels=list(data['yticks'].unique()))
# добавляем разлиновку
ax.grid(visible=True, color='silver', linewidth=0.2)
# подписываем оси
ax.set_xlabel('Время', fontsize=15)
ax.set_ylabel('День', fontsize=15)
# добавляем легенду на график
legend_elements = [Patch(facecolor=work, label='Работа'),
Patch(facecolor=study, label='Учёба'),
Patch(facecolor=communicate, label='Общение'),
Patch(facecolor=play, label='Развлечения'),
Patch(facecolor=other, label='Другое')]
# располагаем её в левом нижнем углу графика
ax.legend(handles=legend_elements, fontsize='medium', edgecolor='none',
loc='lower left', frameon=False)
# перебираем все события внутри дней
for group in days:
df = group[1]
# отмечаем событие на оси
x_axis = df[['start','duration']].to_numpy()
y_axis = (df['date'].min(), timedelta(days=1))
# подбираем соответствующий цвет
types_colors = df['color'].to_numpy()
# рисуем прямоугольник, который отвечает за событие
ax.broken_barh(x_axis, y_axis, facecolors=types_colors)
# сохраняем результат в png-файл
plt.savefig('my_activity.png', dpi=300)
# показываем то, что получилось
plt.show()
И что это всё значит?
Понять, что означает график, — это как раз работа аналитика. На первый взгляд кажется, что работе мешает слишком много переключений на общение и развлечения. А ещё видно, что нам нужно поработать над категориями получше — слишком много попало в раздел «Другое».