Использование машинного обучения для оценки проекта.

В одной из предыдущих статей мы создали статистическую модель для прогнозирования фактических сроков и стоимости проекта на основе оценок. Мы обсудили, что можем подогнать оценки (как для проектов Agile, так и для проектов Waterfall) к логнормальному распределению, которое гарантирует положительную поддержку. Использование статистического подхода к оценке позволяет нам давать прогнозы с требуемым уровнем достоверности, а также прогнозировать денежные выгоды, затраты и риски, как мы обсуждали в другом посте.

Меня спросили, как модель обобщается на случай, когда оценка дана в виде диапазона. В самом деле, это то, чему нас все учили: давать не одно число, а диапазон. Один из подходов - продолжать использовать нашу статистическую модель и вводить в нее число в середине, среднее из двух значений.

Таким образом, модель может использоваться без модификаций.

У этого подхода есть две проблемы:

  1. Среднее значение максимума и минимума является произвольным. Это уменьшает вдвое предоставляемую информацию. Было бы лучше, чтобы алгоритм изучил, где нам нужно установить переменную x в интервале между низкой и высокой границей.
  2. Предоставляя нам ряд данных, разработчик пытается донести до нас очень важную информацию: степень неопределенности оценок. Правильная модель должна использовать эту информацию.

Чтобы упростить процесс, мы возьмем натуральный логарифм всех оценок и фактических данных. Поскольку мы моделируем оценки с использованием логарифмически нормального распределения, наши новые переменные y, l, h будут логарифмами фактического количества дней, нижних и верхних оценок соответственно. В этом случае мы можем использовать нормальное распределение! Мы будем моделировать y, используя линейную регрессию:

В случае, когда θh и θl равны, мы получаем точно такую ​​же проблему, как мы обсуждали ранее.

Функцию правдоподобия для отдельного фрагмента данных в этом случае можно записать следующим образом (после this).

Как упоминалось ранее, указав диапазон, разработчик хотел сообщить нам о неопределенности оценки. Мы должны включить эту неопределенность в нашу оценку σ. Интуитивно диапазон пропорционален стандартному отклонению, и мы можем узнать коэффициент, моделируя σ как:

Если мы также используем параметр точности τ вместо σ 0:

Тогда наша функция правдоподобия будет:

Априори для τ и θ традиционно являются гамма-распределением и нормальным распределением соответственно:

Здесь α, β, λ - гиперпараметры.

Выбор априора для ζ сложнее. Для выбранного нами вида функции правдоподобия не существует ни одного сопряженного априорного значения. Пока мы можем выбрать нормальное распределение. Нулевое среднее этого распределения означает, что мы априори не доверяем диапазонам (мы знаем, что у многих консультантов диапазон всегда составляет 20% и не передает никакой информации). Высокое среднее априорного распределения означает, что мы уделяем больше внимания предполагаемой степени неопределенности.

Для простоты мы установили среднее значение равным нулю.

Отрицательная лог-апостериорная функция:

В этом блоге я найду параметры, соответствующие максимальной апостериорной оценке. А чтобы не делать ошибок при различении, мы будем использовать TensorFlow. Мы будем следовать этому примеру, чтобы построить наш код.

import numpy as np
import pandas as pd
import tensorflow as tf

Данные здесь представляют собой приблизительное и фактическое количество дней. Мы видим, что разработчик любил добавлять 25% к своей оценке в качестве буфера, однако для некоторых историй он добавил больше буфера, возможно, для обозначения большей неопределенности.

seed=1389
tf.reset_default_graph()
task_data = pd.DataFrame({'low':[4,14,4,3,4,3,4,9,6,27,20,23,11],
                          'high':[5,18,5,4,5,7,5,10,8,30,25,29,14],
                          'actual':[17,8,5,3,5,4,9,9,4,27,16,15,7,]})
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
fig, ax = plt.subplots(figsize=(11.7, 8.27))
task_data['story_id'] = task_data.index
data_for_plot = pd.melt(task_data, id_vars="story_id", var_name="type", value_name="days")
task_data.drop(columns=['story_id'], inplace=True)
sns.barplot(x='story_id', y='days', hue='type', data=data_for_plot,ax=ax);

При определении переменных мы заменяем τ другой переменной ρ:

Это сделано для того, чтобы оптимизатор не выбрал отрицательное значение τ.

#Taking the log of data
log_data = np.log(task_data.values)
N = log_data.shape[0]
#Defining variables
theta_h = tf.Variable(name='theta_h', initial_value=0.5)
theta_l = tf.Variable(name='theta_l', initial_value=0.5)
zeta = tf.Variable(name='zeta', initial_value=0.01)
rho = tf.Variable(name='rho', initial_value=0.01)

Поскольку мы не хотим настраивать слишком много гиперпараметров, мы установим α и β равными единице. Оба параметра λ действуют как параметры регуляризации, поэтому нам придется их настроить.

#Set the hyperparameters
alpha = tf.constant(name='alpha', value=1.0)
beta = tf.constant(name='beta', value=1.0)
lambda1 = tf.constant(name='lambda1', value=1e-4)
lambda2 = tf.constant(name='lambda2', value=1e-4)
def loss(l, h, y):
    return tf.log(1+zeta**2*(h-l)) + \
        rho**2/2/(1+zeta**2*(h-l))**2 * (y - theta_l*l - theta_h*h)**2
cummulative_loss = tf.reduce_sum(list(np.apply_along_axis(lambda x: loss(*x), axis=1, arr=log_data )))
cost = cummulative_loss - (N+1-2*alpha)/2*tf.log(rho**2) + beta*rho**2 + \
rho**2*lambda1/2*(theta_h**2+theta_l**2) + rho**2*lambda2/2*zeta**2
learning_rate = 1e-4
optimizer = tf.train.AdamOptimizer(learning_rate)
train_op = optimizer.minimize(cost)
import math
init = tf.global_variables_initializer()
n_epochs = int(1e5)

with tf.Session() as sess:
    sess.run(init)
    for epoch in range(n_epochs):
        if epoch % 1e4 == 0:
            print("Epoch", epoch, "Cost =", cost.eval())
            print(f'Parameters: {theta_l.eval()}, {theta_h.eval()}, {rho.eval()}, {zeta.eval()}')
        sess.run(train_op)
    best_theta_l = theta_l.eval()
    best_theta_h = theta_h.eval()
    best_sigma = 1/math.sqrt(rho.eval())
Epoch 0 Cost = 55.26268
Parameters: 0.5, 0.5, 0.009999999776482582, 0.009999999776482582
Epoch 10000 Cost = 6.5892615
Parameters: 0.24855799973011017, 0.6630115509033203, 0.6332486271858215, 1.1534561276317736e-35
Epoch 20000 Cost = 1.39517
Parameters: 0.2485545128583908, 0.6630078554153442, 1.3754394054412842, 1.1534561276317736e-35
Epoch 30000 Cost = 1.3396643
Parameters: 0.24855604767799377, 0.6630094647407532, 1.4745615720748901, 1.1534561276317736e-35
Epoch 40000 Cost = 1.3396641
Parameters: 0.24855272471904755, 0.6630063056945801, 1.4745622873306274, 1.1534561276317736e-35
Epoch 50000 Cost = 1.3396646
Parameters: 0.2485586702823639, 0.6630119681358337, 1.4745632410049438, 1.1534561276317736e-35
Epoch 60000 Cost = 1.3396648
Parameters: 0.2485581487417221, 0.6630115509033203, 1.4745649099349976, 1.1534561276317736e-35
Epoch 70000 Cost = 1.3396643
Parameters: 0.2485586702823639, 0.6630122065544128, 1.4745644330978394, 1.1534561276317736e-35
Epoch 80000 Cost = 1.3396643
Parameters: 0.24855820834636688, 0.6630116701126099, 1.4745631217956543, 1.1534561276317736e-35
Epoch 90000 Cost = 1.3396646
Parameters: 0.248562291264534, 0.663015604019165, 1.474563717842102, 1.1534561276317736e-35

Интересно то, что ζ равно нулю. Это означает, что мы не можем доверять оценке неопределенности, которую дают нам разработчики. Это также означает, что мы можем просто использовать логарифмически-нормальное распределение вокруг среднего, заданного изученными параметрами θl и θh. Допустим, тот же разработчик оценил выполнение новой задачи в 10–15 дней. Подставляя его в формулы, мы видим:

mu = best_theta_l*math.log(10)+best_theta_h*math.log(15)
most_likely_prediction = math.exp(mu)    
most_likely_prediction
10.67385532327305

Мы также можем получить 95% -ную уверенность, подставив значения непосредственно в логнормальное распределение:

from scipy.stats import lognorm
distribution = lognorm(s=best_sigma, scale=most_likely_prediction, loc=0)
print(f'95% confidence: {distribution.ppf(0.95)}')
95% confidence: 41.3614192940211

Как мы видим, если мы хотим 95% уверенности, мы должны дать оценку 41 день вместо 11 дней для 50% уверенности. Это очень легко объяснить, если вы видите, что в прошлом разработчик не очень хорошо оценивал задачи.

Вы можете получить доступ к записной книжке с github.