Adjust Estimate According to Assignee
Problem
Estimating issues is a difficult task, especially in Agile methodologies. Since you rarely know who is going to end up with a task, you can only give a rough estimate on how much time it should take your average team member to do it. But then you might have new people in your team, who might require more time to figure out what's going on inside before they make the changes. Moreover, even older team members can take a different amount of time to solve an issue.
Solution
One option is to "customize" the estimate. Let's say we have a task estimated at 1d. Some may need 1d 4h to solve it, some might do it in 6h. If we can keep track of the error margin for each user and average that across the issues he/she has worked on, it would be possible to give a better starting estimate for future tasks. If a user is constantly taking longer than the average by, let's say 20%, then we could anticipate and raise the estimate by 20% for him/her.
To do so, we'll add 2 post-functions:
- Start Progress - when a user starts progress on an issue, we should re-calculate the remaining estimate to conform to his/her velocity. A user's velocity is the factor by which we should multiply the original estimate to give a better approximation.
- Resolve Issue - when an issue is resolved, we'll add the difference between the time actually spent working on the issue and what we calculated when progress started, then we'll include this result in future approximations.
We'll keep track of the parameters by storing them as user properties and we'll hold 2 values:
- user's velocity
- number of iterations - each time we alter the velocity, we increment this value
Each user starts off with a velocity of 1 and 0 iterations.
Step 1 - Starting Progress - recalculating the estimate
When a user starts progress on an issue, we'll take the original estimate, multiply it by the user's velocity and put the result in the remaining estimate. However, one might work on an issue intermittently, so we'll do this only if there was no time logged on the issue.
Tip |
---|
|
You can also do this step (with minor changes) when the issue is re-assigned. To allow some flexibility, we'll write all the necessary functions in a separate file that we can include from other SIL™ scripts. |
Code Block |
---|
title | Start Progress Post Function |
---|
|
include "common_velocity.incl"; // our "auto-estimate library"
updateRemaining(key); |
Step 2 - Resolving an Issue - recalculating the velocity
When an issue is resolved, we need to calculate the approximation error and include it in the velocity to be used for future estimates.
Code Block |
---|
title | Resolve Issue Post Function |
---|
|
include "common_velocity.incl"; // our "auto-estimate library"
adjustVelocity(key); |
Step 3 - Adding the Environment Configuration
We'll use the SIL™ Environment to hold the default velocity (1) and user properties which store the velocity and number of iterations.
Code Block |
---|
|
default.velocity = 1
user.velocity = jjup.autoestimate.user.velocity
velocity.samples = jjup.autoestimate.velocity.samples |
Step 4 - Adding the "library"
As we said earlier, we'll put all the logic in one file which we can include everywhere else we decide to re-estimate (listeners, services, etc). All you need to do is save the file below in your silprograms folder.
Code Block |
---|
title | common_velocity.incl |
---|
|
function getUserVelocity(string user){
if(! userExists(user)){
number DEFAULT_VELOCITY = silEnv("default.velocity");
logPrint("DEBUG", "User " + user + " does not exist. Cannot calculate velocity. Using default " + DEFAULT_VELOCITY);
return DEFAULT_VELOCITY;
}
number velocity = getUserProperty(user, silEnv("user.velocity"));
if(isNull(velocity)){
string DEFAULT_VELOCITY = silEnv("default.velocity");
setUserProperty(user, silEnv("user.velocity"), DEFAULT_VELOCITY);
velocity = DEFAULT_VELOCITY;
}
return velocity;
}
function getNoVelocitySamples(string user){
if(! userExists(user)){
logPrint("DEBUG", "User " + user + " does not exist. Cannot get number of velocity samples");
return 0;
}
number samples = getUserProperty(user, silEnv("velocity.samples"));
if(isNull(samples)){
setUserProperty(user, silEnv("velocity.samples"), 0);
samples = 0;
}
return samples;
}
function setNoSamples(string user, number noSamples){
if(! userExists(user)){
logPrint("DEBUG", "User " + user + " does not exist. Cannot set velocity samples.");
return ;
}
setUserProperty(user, silEnv("velocity.samples"), noSamples);
}
function setUserVelocity(string user, number velocity){
if(! userExists(user)){
logPrint("DEBUG", "User " + user + " does not exist. Cannot set velocity.");
return ;
}
setUserProperty(user, silEnv("user.velocity"), velocity);
}
function getWorkForUser(string user, string issKey){
number [] ids = getWorklogIdsForUser(user, issKey);
interval intvl = "0h";
if(isNull(ids) || size(ids) == 0){
return intvl;
}
for(number wlid in ids){
intvl += getWorklogLoggedHours(wlid);
}
return intvl;
}
function isSoloWork(string user, string issKey){
return %issKey%.spent == getWorkForUser(user, issKey);
}
function adjustVelocity(string issKey){
string user = %issKey%.assignee;
if(!isSoloWork(user, issKey)){
logPrint("DEBUG", "User" + user + " did not work all by himself on issue " + issKey + ". Cannot update velocity");
return;
}
number noSamples = getNoVelocitySamples(user);
number velocity = getUserVelocity(user);
number estimateWithVelocity = %issKey%.originalEstimate["TOMILLIS"] * velocity;
number currentRatio = %issKey%.spent["TOMILLIS"] /estimateWithVelocity;
number newVelo = (velocity * noSamples + currentRatio) / (noSamples + 1);
setUserVelocity(user, newVelo);
setNoSamples(user, noSamples + 1);
logPrint("INFO", "Updated user " + user + " velocity from " + velocity + " to " + newVelo + " based on " + (noSamples+1) + " samples");
}
function updateRemaining(string issKey){
if(%issKey%.spent > "0d" ){
logPrint("DEBUG", "Work already logged. Not altering remaining estimate.");
return;
}
number velocity = getUserVelocity(%issKey%.assignee);
number remainingMillis = %issKey%.originalEstimate["TOMILLIS"];
%issKey%.remaining = remainingMillis * velocity;
} |
Tip |
---|
|
You can further tweak the functions provided to account for other users that might have worked on the issue. |
See also