kpolen

Performance Measurement for Private Equity Portfolios

Oliver Reece

Why Private Equity Needs Its Own Metrics

Important

In private equity, the fund manager decides when to ask investors for money and when to pay it back. Investors have no say in the timing, so normal investment tracking doesn’t work.

Unpredictable timing

Cash returns are irregular and occur when fund managers sell investments.

Worth the wait?

Investors expect extra profit for essentially locking their money but it’s hard to define extra when there’s no stable income.

Apples to oranges

Need a way to compare stock market returns and private fund returns: PME.

The Package

What It Is

  • Written by Karl Polen, former chief investment officer of the Arizona State Retirement System
  • Free on GitHub: karlpolen/pme-calcs

Key Functions

irr.z() | Calculates the fund’s annualised return rate |
tvpi() / dpi() | How many times your money was multiplied |
pme.irr() | Compares fund vs stock market (Long-Nickels method) |
ks.pme() | Did the fund beat the market? (ratio score) |
pme.plus() | A more refined market comparison method |

Tip

devtools::install_github("karlpolen/pme-calcs")
# Also requires: zoo and FinCal to run properly

The Four Core Metrics

  • IRR — annualised return rate

Accounts for the timing of all capital calls and distributions. The PE industry’s primary return metric.

  • TVPI — total money multiplier

\[\frac{\text{Cash Paid Out} + \text{Remaining Value}}{\text{Cash Put In}}\]

Total value (realised + unrealised) per dollar invested.

  • DPI — cash-in-hand multiplier

\[\frac{\text{Cash Paid Out}}{\text{Cash Put In}}\]

For every dollar put in, how many have been returned to investors so far?

  • PME — Public Market Equivalent

Simulates investing the fund’s exact cash flows into a public index, then computes an IRR for that hypothetical portfolio to compare directly with the fund’s IRR.

Example 1: IRR with irr.z()

devtools::install_github("karlpolen/pme-calcs")
source("irr.z.r")

# Dates money moved in (negative) and out (positive)
dates <- as.Date(c(
  "2015-01-01", "2016-01-01", "2017-01-01", #Capital calls -- cash put in
  "2021-01-01", "2022-06-01", "2023-06-01", #Early distributions -- early cash paid out
  "2024-01-01", "2025-01-01"                #Final distributions -- final cash paid out
))

cashflows <- c(-1000, -800, -700,  #Capital calls ($M)
                 500,  700,  900, 1200, 1500) #Distributions ($M)

#irr.z handles multiple IRR roots and GIPS compliance
result <- irr.z(dates, cashflows, gips = TRUE)
cat("Net IRR:", round(result$irr * 100, 2), "%\n")
#Net IRR: 17.43%

Note

The gips = TRUE argument prevents the polynomial from outputting several mathematically sound answers. It instead takes the largest root from the equation. GIPS stands for the Global Investment Performance Standards.

Example 2: TVPI & DPI Multiples

#Fund stats ($M)
called_capital <- 2500  #total capital drawn
distributions  <- 4800  #total cash returned to LPs
residual_nav   <-  850  #current unrealized portfolio NAV

dpi  <- distributions / called_capital
rvpi <- residual_nav  / called_capital
tvpi <- dpi + rvpi                      

data.frame(
  Metric  = c("DPI", "RVPI", "TVPI"),
  Value   = round(c(dpi, rvpi, tvpi), 2)
  )
Metric Value Meaning
DPI 1.92× $1.92 paid back in real cash for every $1 put in
RVPI 0.34× $0.34 of value still sitting in the fund (unrealised)
TVPI 2.26× $2.26 total value for every $1 invested

Example 3: Public Market Equivalent (PME)

source("pme.r")  #from karlpolen/pme-calcs

#pe_cashflows: zoo object of dated PE fund cash flows
#sp500_monthly: zoo object of monthly S&P 500 total return index

#Long-Nickels PME: simulate investing PE cash flows in the S&P 500
pme_result <- pme.irr(
  cf    = pe_cashflows,
  index = sp500_monthly,
  nav   = 850
)

#Kaplan-Schoar ratio:
ks <- ks.pme(pe_cashflows, sp500_monthly)

cat("Fund IRR: 17.4%\n")
cat("PME IRR:  12.1%\n")
cat("KS PME:  ", round(ks, 2), "\n")  

Note

A KS PME > 1.0 means the PE fund created more value than the same dollars invested in the public index on the same dates. Every $1 earned $1.18 more than the S&P 500.

Real Data: Fund Vintages vs S&P 500

Fund IRR TVPI DPI PME_IRR KS_PME Result
Buyout A (2010) 22.1% 3.4x 3.2x 15.8% 1.31x Beat market
Buyout B (2014) 17.4% 2.3x 1.9x 12.1% 1.18x Beat market
Growth C (2016) 14.0% 2.0x 0.8x 14.9% 0.96x Lagged market
Venture D (2018) 28.5% 2.8x 0.4x 11.3% 1.41x Beat market
Infra E (2019) 11.2% 1.6x 0.6x 13.7% 0.88x Lagged market

Warning

Key insight: Fund C’s 14% return looks fine on its own. But the stock market returned 14.9% on those exact same dollars, on those exact same dates. Without the market comparison, you’d never know you were better off in an index fund.

Summary

What kpolen solves

Fills a gap in R for private market analytics — no existing tidyverse or PerformanceAnalytics function handles PE’s irregular cash flow structure.

Technical strengths

Handles multiple-root IRR problems, GIPS compliance, and PME’s “negative NAV” problem via PME+ — issues that break naive implementations.

Who should use it

Anyone who needs to evaluate whether their private fund managers are actually earning their fees or not. Largely used by PE or Investment analysts.

Resources

  • github.com/karlpolen/pme-calcs
  • rpubs.com/kpolen/16062
  • karlpolen/zoocashflow
  • karlpolen/gips_irr