Проект: анализируем свою продуктивность по большим данным
medium

Проект: анализируем свою продуктивность по большим данным

Разбираем скрипт из статьи «Кинжала»

Сегодня у наших друзей в «Кинжале» вышла статья про то, как анализировать свою продуктивность. В статье была ссылка на Google Colab с кодом на Python, где и происходила вся магия анализа. Сегодня мы разберём, что делает и как работает этот код.

Кратко смысл проекта:

  1. Программа ActivityWatch следит за вашей активностью: какие программы у вас открыты и какие там вкладки браузера.
  2. Из программы мы достаём сырой файл с данными о продуктивности.
  3. Этот файл анализируется в заранее написанном Python-скрипте.
  4. Скрипт рисует визуализацию. По ней видно, когда человек работал, а когда отвлекался: 

Что делаем

Сегодня мы разберём, как работает тот визуализирующий скрипт, который превращает сырые данные в наглядную визуализацию. Скрипт лежит в 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()

И что это всё значит?

Понять, что означает график, — это как раз работа аналитика. На первый взгляд кажется, что работе мешает слишком много переключений на общение и развлечения. А ещё видно, что нам нужно поработать над категориями получше — слишком много попало в раздел «Другое».

Код:

Татьяна Цвирова

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Виталий Вебер

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
medium
[anycomment]
Exit mobile version