Earned Value Management (EVM) is a project management technique that integrates scope, schedule, and cost to measure project performance objectively. By comparing planned work against actual work accomplished and actual cost incurred, EVM provides early warning of cost overruns and schedule delays.
| Metric | Formula | Interpretation |
|---|---|---|
| PV – Planned Value | BAC × Planned% | Budget authorized for scheduled work |
| EV – Earned Value | BAC × Actual% | Budget for work actually completed |
| AC – Actual Cost | Σ period costs | Total cost incurred to date |
| CV – Cost Variance | EV − AC | > 0: under budget; < 0: over budget |
| SV – Schedule Variance | EV − PV | > 0: ahead of schedule; < 0: behind schedule |
| CPI – Cost Performance Index | EV / AC | > 1: efficient; = 1: on target; < 1: inefficient |
| SPI – Schedule Performance Index | EV / PV | > 1: ahead; = 1: on target; < 1: behind |
| EAC – Estimate at Completion | See below | Forecast of total project cost |
| ETC – Estimate to Complete | EAC − AC | Remaining cost to finish the project |
| VAC – Variance at Completion | BAC − EAC | Expected budget surplus (positive) or overrun (negative) |
| TCPI – To-Complete Performance Index | (BAC−EV) / (BAC−AC) | Efficiency required on remaining work |
We will track a project with a total budget of $500,000 over a 5-period schedule.
PV is the authorized budget for work planned through the current period.
pv_val <- pv(bac, schedule, time_period)
cat("Planned Value (PV): $", format(pv_val, big.mark = ","), "\n")Planned Value (PV): $ 250,000
The project was planned to be 50% complete by period 3, so PV = $250,000.
EV reflects the budget value of work actually completed.
actual_per_complete <- 0.40
ev_val <- ev(bac, actual_per_complete)
cat("Earned Value (EV): $", format(ev_val, big.mark = ","), "\n")Earned Value (EV): $ 2e+05
Only 40% of work is done despite 50% being planned — we are behind schedule.
AC is the cumulative cost incurred. Use
cumulative = FALSE when passing individual period
costs.
period_costs <- c(45000, 110000, 135000)
ac_val <- ac(period_costs, time_period, cumulative = FALSE)
cat("Actual Cost (AC): $", format(ac_val, big.mark = ","), "\n")Actual Cost (AC): $ 290,000
sv_val <- sv(ev_val, pv_val)
cv_val <- cv(ev_val, ac_val)
spi_val <- spi(ev_val, pv_val)
cpi_val <- cpi(ev_val, ac_val)
cat("Schedule Variance (SV): $", format(sv_val, big.mark = ","), "\n")Schedule Variance (SV): $ -50,000
Cost Variance (CV): $ -90,000
Schedule Performance Index (SPI): 0.8
Cost Performance Index (CPI): 0.69
Interpretation: SPI < 1 means we are behind schedule (only earning 80 cents of planned value per dollar of schedule). CPI < 1 means we are over budget (earning only 69 cents of value per dollar spent).
EAC forecasts the total cost at project completion. Three methods are available, each making a different assumption about future performance:
| Method | Formula | When to use |
|---|---|---|
| Typical | BAC / CPI | Current cost inefficiency is expected to continue |
| Atypical | AC + (BAC − EV) | Cost overrun was a one-time event; future work will proceed at planned rate |
| Combined | AC + (BAC − EV) / (CPI × SPI) | Both cost and schedule performance will influence future costs |
eac_typical <- eac(bac, method = "typical", cpi = cpi_val)
eac_atypical <- eac(bac, method = "atypical", ac = ac_val, ev = ev_val)
eac_combined <- eac(bac,
method = "combined", cpi = cpi_val, ac = ac_val,
ev = ev_val, spi = spi_val
)
cat("EAC (typical): $", format(round(eac_typical), big.mark = ","), "\n")EAC (typical): $ 725,000
EAC (atypical): $ 590,000
EAC (combined): $ 833,750
The typical method gives the most conservative (highest cost) estimate because it assumes the current CPI persists. The atypical method is the most optimistic, assuming past overruns won’t recur.
eac_table <- data.frame(
Method = c("Typical", "Atypical", "Combined"),
EAC = c(round(eac_typical), round(eac_atypical), round(eac_combined)),
Overrun = c(
round(eac_typical - bac),
round(eac_atypical - bac),
round(eac_combined - bac)
),
Assumption = c(
"Current CPI continues",
"Future work at planned rate",
"CPI and SPI both factor in"
)
)
knitr::kable(eac_table,
format.args = list(big.mark = ","),
caption = "EAC Comparison by Method"
)| Method | EAC | Overrun | Assumption |
|---|---|---|---|
| Typical | 725,000 | 225,000 | Current CPI continues |
| Atypical | 590,000 | 90,000 | Future work at planned rate |
| Combined | 833,750 | 333,750 | CPI and SPI both factor in |
etc_val <- etc(bac, ev_val, cpi_val)
vac_val <- vac(bac, eac_typical)
# TCPI to meet original BAC
tcpi_bac <- tcpi(bac, ev_val, ac_val, target = "bac")
# TCPI to meet revised EAC (typical)
tcpi_eac <- tcpi(bac, ev_val, ac_val, target = "eac", eac = eac_typical)
cat("Estimate to Complete (ETC): $", format(round(etc_val), big.mark = ","), "\n")Estimate to Complete (ETC): $ 435,000
Variance at Completion (VAC): $ -225,000
TCPI (to meet BAC): 1.429
TCPI (to meet EAC): 0.69
Interpretation: TCPI > 1 means the team must work more efficiently than they have been to meet the target. A TCPI of 1.43 to meet BAC means the remaining work must be done at 143% efficiency — significantly better than the current CPI of 0.69. Using the EAC target is more realistic and shows whether the revised budget is achievable.
The chart below shows cumulative PV, AC, and EV over time, with horizontal reference lines for BAC and EAC.
time_periods <- c(1, 2, 3)
actual_pct <- c(0.08, 0.22, 0.40)
p_costs <- c(45000, 110000, 135000)
pv_vals <- sapply(time_periods, function(t) pv(bac, schedule, t))
ac_vals <- cumsum(p_costs)
ev_vals <- sapply(actual_pct, function(a) ev(bac, a))
trend_data <- data.frame(
Period = time_periods,
PV = pv_vals,
AC = ac_vals,
EV = ev_vals
)
p <- ggplot2::ggplot(trend_data, ggplot2::aes(x = Period)) +
ggplot2::geom_line(ggplot2::aes(y = PV, color = "Planned Value (PV)"), linewidth = 1.2) +
ggplot2::geom_line(ggplot2::aes(y = AC, color = "Actual Cost (AC)"), linewidth = 1.2) +
ggplot2::geom_line(ggplot2::aes(y = EV, color = "Earned Value (EV)"), linewidth = 1.2) +
ggplot2::geom_point(ggplot2::aes(y = PV, color = "Planned Value (PV)"), size = 3) +
ggplot2::geom_point(ggplot2::aes(y = AC, color = "Actual Cost (AC)"), size = 3) +
ggplot2::geom_point(ggplot2::aes(y = EV, color = "Earned Value (EV)"), size = 3) +
ggplot2::geom_hline(yintercept = bac, linetype = "dashed", color = "black", linewidth = 0.8) +
ggplot2::geom_hline(yintercept = eac_typical, linetype = "dotted", color = "darkred", linewidth = 0.8) +
ggplot2::annotate("text", x = 2.8, y = bac + 12000, label = "BAC = $500K", size = 3.5) +
ggplot2::annotate("text", x = 2.8, y = eac_typical + 12000, label = "EAC = $725K", size = 3.5) +
ggplot2::scale_color_manual(values = c(
"Planned Value (PV)" = "steelblue",
"Actual Cost (AC)" = "tomato",
"Earned Value (EV)" = "forestgreen"
)) +
ggplot2::scale_y_continuous(labels = scales::label_dollar(scale = 1e-3, suffix = "K")) +
ggplot2::labs(
title = "Earned Value Management - Performance Trend",
x = "Time Period",
y = "Value",
color = NULL
) +
ggplot2::theme_minimal() +
ggplot2::theme(legend.position = "bottom")
print(p)Reading the chart: The gap between PV and EV (both measured on the same vertical axis) shows the schedule gap, EV is below PV, confirming we are behind. The gap between AC and EV shows the cost overrun, we have spent more than the value we have earned.