Метрики команды разработки

Введение

Заказчику задачи в конечном счёте всё равно, какой методологией управления разработкой пользуется команда исполнителей — точная дата получения результата для него важнее.

Чтобы называть эту дату более обоснованно, необходимо понимать, как на самом деле работает команда: сколько поставляет задач, как долго проходит процесс анализа задачи перед взятием в работу, на каких этапах в целом происходит «застревание» задачи.

TL; DR — Вот ссылка на репозиторий с python-кодом, который считает метрики из статьи по jira и рисует графики для наглядности (merge requests are very welcome). 

Ниже — описание метрик и способы их расчёта.

Disclaimer: для расчёта использовались обезличенные данные нескольких компаний с примесью синтетики.

Предлагаемый подход

Прежде всего надо выделить то множество задач, для которого будут рассчитаны метрики.

Также следует определить множество статусов, которые мы будет считать «финальными». Например, задача может быть как выполнена, так и отклонена — оба статуса в определённом смысле «финальные».

Для каждой задачи из выделенного множества нам будут интересны следующие свойства:

  • Текущий статус

  • Дата создания

  • Дата первого изменения статуса (считаем датой начала работы над задачей)

  • Дата последнего перехода в финальный статус (считаем датой окончания работы над задачей)

  • Время нахождения в каждом статусе

  • Время нахождения на каждом из исполнителей

@dataclass
class Issue:
    key: str
    status: str

    created_at: datetime
    first_status_change_at: datetime = None
    last_finish_status_at: datetime = None

    doers_x_periods: dict[str, timedelta] = None
    statuses_x_periods: dict[str, timedelta] = None

    @property
    def is_done(self) -> bool:
        return self.last_finish_status_at is not None

    @property
    def lead_time(self) -> timedelta | None:
        if not self.last_finish_status_at:
            return None

        return self.last_finish_status_at - self.created_at

    @property
    def cycle_time(self) -> timedelta | None:
        if self.first_status_change_at and self.last_finish_status_at:
            return self.last_finish_status_at - self.first_status_change_at
        return None

Метрики

Throughput

Сколько задач делает команда разработки за определённый период времени?

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

from collections import defaultdict
from metrics.entity import Issue


def get_throughput(list_of_issues: list[Issue]) -> dict[str, int]:
	tmp = defaultdict(int)
	for issue in list_of_issues:
	    if issue.is_done:
            key = issue.last_finish_status_at.strftime("%YW%V")  # example: 2024W03
                tmp[key] += 1
    return dict(tmp)

Пример вывода:

{
...
'2022W08': 2,
'2022W09': 8,
'2022W10': 5,
'2022W11': 4,
'2022W12': 9,
'2022W13': 12
...
}

Визуализация с линейной регрессией:

Видно, что delivery rate команды снижается

Видно, что delivery rate команды снижается

Lead Time

Сколько времени требуется, что довести задачу до конца?

Пример кода с установленной гранулярностью расчёта в один день и лимитом в 30 дней:

import typing as t

from metrics.entity import Issue

ONE_HOUR: Final[int] = 60 * 60
ONE_DAY: Final[int] = 24 * ONE_HOUR
CALC_LIMIT: Final[int] = 30

  def get_lead_time(
      list_of_issues: list[Issue],
      timeslot: int = ONE_DAY,
      limit: int = CALC_LIMIT,
  ) -> list[float]:
      res = []
      for issue in list_of_issues:
          if issue.lead_time:
              lead_time = max(1, issue.lead_time.total_seconds() // timeslot)
              lead_time = min(lead_time, limit)
              res.append(lead_time)
      return res

Визуализация без лимита:

Cycle Time

Сколько времени проходит между началом работы над задачей и её завершением?

Пример кода:

import typing as t

from metrics.entity import Issue

ONE_HOUR: Final[int] = 60 * 60
ONE_DAY: Final[int] = 24 * ONE_HOUR
CALC_LIMIT: Final[int] = 30
  
def get_cycle_time(
    self,
    list_of_issues: list[Issue],  
    timeslot: int = ONE_DAY,
    limit: int = CALC_LIMIT,
) -> list[float]:
    res = []
    for issue in self.repo.all():
        if issue.cycle_time:
            cycle_time = max(1, issue.cycle_time.total_seconds() // timeslot)
            cycle_time = min(cycle_time, limit)
            res.append(cycle_time)
    return res

Визуализация с лимитом:

f9c7f75a171ef13d968ed98b5574f0cf.png

Queues

Сколько времени задачи проводят на каждом этапе работы?

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

import typing as t

from metrics.entity import Issue

ONE_HOUR: Final[int] = 60 * 60
ONE_DAY: Final[int] = 24 * ONE_HOUR
CALC_LIMIT: Final[int] = 30
  
def get_queue_time(
    list_of_issues: list[Issue],
    timeslot: int = ONE_DAY,
    limit: int = CALC_LIMIT,
) -> dict[str, list[float]]:
    tmp = defaultdict(list)

    for issue in list_of_issues:
        for status, td in issue.statuses_x_periods.items():
            period_in_status = max(1, td.total_seconds() // timeslot)
            period_in_status = min(period_in_status, limit)
            tmp[status].append(period_in_status)

    return dict(tmp)

Статус

Статус «Анализ задачи»

Статус

Статус «Разработка»

Статус

Статус «Ожидает тестирования»

Статус

Статус «В тестировании»

Если наивно подходить к данным, то для цикла «анализ → разработка → ожидание тестирования → тестирование» наиболее ожидаемое затраченное время 2+2+1+2 = 7 рабочих дней.

Оффтоп: смешно, но конкретно для данной компании этот расчёт оказался верен в ~80% случаев. После этого были приняты меры, чтобы уменьшить время в очередях «Анализ» и «Ожидание тестирования». Теперь укладываются в 5 дней.

Наполнение данными

Теперь, когда готовы все структуры данных и алгоритмы их обработки, можно начать наполнять систему данными. Для примера будем использовать Jira как источник сырых данных.

Общий подход такой — для каждого источника сырых надо определить две составляющие:

  • Откуда брать сырые данные (api, filesystem, db, etc)?

  • Как приводить сырые данные в класс Issue?

Примеры для решения обоих задач есть в репозитории, приводить их здесь, наверное, незачем.

Заключение

После получение статистики и метрик не самой дурной идеей кажется попытка собрать простую ML-модель, чтобы попытаться ответить на два главных (с точки зрения заказчика) вопроса:

Этим и займёмся в следующей статье.

© Habrahabr.ru