Learning Curves

Learning curves model how efficiency or completion rate evolves over time as a team gains experience. In project management they are used to:

Sigmoidal Models

Sigmoidal (S-shaped) functions capture the three phases of learning: slow start, rapid improvement, and plateau. The PRA package provides three model types:

Model Formula Parameters
Logistic K / (1 + exp(−r(t − t₀))) K = ceiling; r = growth rate; t₀ = inflection time
Pearl K / (1 + exp(−r(t − t₀))) Same as logistic (identical functional form)
Gompertz A · exp(−b · exp(−c · t)) A = ceiling; c = growth rate; b = initial suppression

Parameter interpretation (Logistic / Pearl): - K — the maximum achievable value (e.g., 100% completion) - r — how fast the S-curve rises; larger r = steeper transition - t₀ — the time at which the outcome is at its midpoint (inflection point)

Example: Fitting a Logistic Model

library(PRA)

We have weekly completion percentage data for a construction deliverable over 9 weeks.

data <- data.frame(
  time       = 1:9,
  completion = c(5, 15, 40, 60, 70, 75, 80, 85, 90)
)

Fit the Model

fit <- fit_sigmoidal(data, "time", "completion", "logistic")

Assess Fit Quality

Use summary() to examine the fitted coefficients, their standard errors, and the residual standard error — a measure of how closely the model matches the observed data.

summary(fit)
#> 
#> Formula: y ~ logistic(x, K, r, t0)
#> 
#> Parameters:
#>    Estimate Std. Error t value Pr(>|t|)    
#> K   84.3515     2.5142  33.550 4.67e-08 ***
#> r    1.0356     0.1380   7.505 0.000289 ***
#> t0   3.2520     0.1459  22.284 5.34e-07 ***
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> 
#> Residual standard error: 4.148 on 6 degrees of freedom
#> 
#> Number of iterations to convergence: 10 
#> Achieved convergence tolerance: 1.49e-08

A small residual standard error (relative to the response scale) indicates a good fit. Coefficient t values with |t| > 2 are statistically meaningful.

Plot with Confidence Bands

plot_sigmoidal() plots the data, fitted curve, and optional confidence bounds in a single call.

plot_sigmoidal(
  fit, data, "time", "completion", "logistic",
  conf_level = 0.95,
  main = "Logistic Learning Curve - Completion Forecast",
  xlab = "Week",
  ylab = "Completion (%)"
)

The shaded region is the 95% confidence band — the range within which the true fitted curve is likely to lie. Wider bands at the tails reflect greater uncertainty in extrapolated predictions.

Predict Future Completion

Use predict_sigmoidal() to generate numeric forecasts, including confidence bounds for specific future time points.

future_times <- seq(1, 12, length.out = 100)
predictions <- predict_sigmoidal(fit, future_times, "logistic", conf_level = 0.95)

knitr::kable(
  tail(round(predictions, 1), 5),
  caption = "Predicted completion (final 5 points)",
  row.names = FALSE
)
Predicted completion (final 5 points)
x pred lwr upr
11.6 84.3 78.2 90.5
11.7 84.3 78.2 90.5
11.8 84.3 78.2 90.5
11.9 84.3 78.2 90.5
12.0 84.3 78.2 90.5

Comparing All Three Model Types

Different model types can produce different forecasts, especially in the tails. It is good practice to fit multiple models and compare their goodness-of-fit.

fit_logistic <- fit_sigmoidal(data, "time", "completion", "logistic")
fit_pearl <- fit_sigmoidal(data, "time", "completion", "pearl")
fit_gompertz <- fit_sigmoidal(data, "time", "completion", "gompertz")
# Residual standard errors for comparison
rse <- function(fit) summary(fit)$sigma

comparison <- data.frame(
  Model = c("Logistic", "Pearl", "Gompertz"),
  Residual_StdError = round(c(rse(fit_logistic), rse(fit_pearl), rse(fit_gompertz)), 3)
)
knitr::kable(comparison, caption = "Model Fit Comparison (lower RSE = better fit)")
Model Fit Comparison (lower RSE = better fit)
Model Residual_StdError
Logistic 4.148
Pearl 4.148
Gompertz 2.887

Now plot all three fits side by side:

x_seq <- seq(1, 12, length.out = 200)

pred_log <- predict_sigmoidal(fit_logistic, x_seq, "logistic")
pred_prl <- predict_sigmoidal(fit_pearl, x_seq, "pearl")
pred_gom <- predict_sigmoidal(fit_gompertz, x_seq, "gompertz")

# Base plot with observed data
plot(data$time, data$completion,
  pch = 16, xlim = c(1, 12), ylim = c(0, 105),
  main = "Learning Curve: Model Comparison",
  xlab = "Week", ylab = "Completion (%)"
)
lines(pred_log$x, pred_log$pred, col = "steelblue", lwd = 2)
lines(pred_prl$x, pred_prl$pred, col = "tomato", lwd = 2, lty = 2)
lines(pred_gom$x, pred_gom$pred, col = "darkgreen", lwd = 2, lty = 3)
legend("bottomright",
  legend = c("Logistic", "Pearl", "Gompertz"),
  col = c("steelblue", "tomato", "darkgreen"),
  lty = c(1, 2, 3), lwd = 2, bty = "n"
)

Note: Logistic and Pearl share the same functional form and will produce identical fits on the same data. Gompertz has a different shape, its inflection point occurs earlier and the curve is asymmetric, making it better suited for processes that accelerate quickly early on.

Summary

The sigmoidal workflow in PRA:

  1. fit_sigmoidal() — fit a model to observed time-completion data
  2. summary(fit) — inspect coefficient estimates and goodness-of-fit
  3. predict_sigmoidal() — generate numeric forecasts with optional confidence bounds
  4. plot_sigmoidal() — visualize the fit and confidence band

Choose the model type based on the shape of your data and theoretical expectations about the learning process.