Library/Fundamental Analysis and Valuation/Relative Valuation/EV/EBITDA Comparable Valuation

D18-F03-A02 / Complete engineering topic

EV/EBITDA Comparable Valuation

Keep the enterprise multiple and equity-value bridge separate so debt, cash, preferred equity, NCI, nonoperating assets, and shares remain auditable.

EV/EBITDA Comparable Valuation maps eligible peer fundamentals to an auditable relative valuationD18 / D18-F03

Keep the enterprise multiple and equity-value bridge separate so debt, cash, preferred equity, NCI, nonoperating assets, and shares remain auditable.

EV/EBITDA Comparable Valuation turns eligible peers into a conditional, auditable value

Figure 1. Synthetic canonical path from declared peer inputs to the selected relative-value output; the estimate is conditional, not intrinsic or predictive.

The decision this tutorial makes visible

EV/EBITDA can compare operating businesses with different financing, but only when enterprise value and EBITDA cover compatible claims and operating scope. Adjusted EBITDA definitions and capital bridges commonly break that symmetry.

The precise question is: What enterprise and equity value follow from a median peer EV/EBITDA multiple and an explicit capital bridge?

A practitioner needs to know what the diagnostic does and does not justify. A builder needs a contract that can be reproduced from the same point-in-time inputs in Python, TypeScript, a visual, and a browser lab.

Intuition before notation

The peer multiple values operations first. Financing and non-common claims are removed only after enterprise value is estimated; nonoperating assets are then added back before dividing among common shares.

The result depends on the declared algorithm scope, input clocks, units, equality and rounding policies, and unsupported-state treatment. Change one of those and the output represents a different decision even when its field name is unchanged.

Scope and nearby methods

The canonical variant uses positive peer and target EBITDA, median peer EV/EBITDA, then bridges implied enterprise value to common equity. It does not apply to financial institutions or silently substitute adjusted EBITDA.

VariantDefinitionBest useMain limitation
Canonical reported-basis EV/EBITDAMedian compatible EV/EBITDA then equity bridgeNonfinancial operating-company compsCapital intensity and lease treatment remain
Adjusted EV/EBITDAUses issuer or analyst adjustmentsNormalized operating comparisonDefinitions vary and can exclude recurring costs
EV/EBITA or EV/EBITRetains more amortization or depreciationAsset-intensive comparisonsDifferent economic denominator

What is sourced, selected, synthetic, and derived

RoleMaterial claimEvidenceBoundary
Sourced factEBITDA and adjusted EBITDA require definition and reconciliation discipline.SEC non-GAAP C&DIsSEC disclosure rules do not standardize all analyst adjustments.
Implementation choiceSelect median peer EV/EBITDA and use a declared equity bridge.Frozen package contractOther bridge conventions exist.
Synthetic teaching inputMedian EV/EBITDA is 8× and target EBITDA is 150.Repository fixtureNot issuer data.
Author-derived calculationImplied EV is 1,200 and implied equity is 900.8×150, then bridgeConditional on scope matching.

The authoritative sources support only the exact facts named in the claim ledger. They do not certify the synthetic numbers in this tutorial. The repository fixture is deliberately invented for auditability, and the displayed output is author-derived under the selected implementation choice.

Formula, symbols, and numerical policy

Plain text
M_i = EV_i / EBITDA_i; implied_EV = median(M_i) × target_EBITDA; implied_equity = implied_EV − net_debt − preferred − NCI + nonoperating_assets; implied_price = implied_equity / diluted_shares
SymbolMeaningUnitPolicy
EVenterprise valuecurrencysame capital-claim scope as EBITDA
EBITDAnormalized operating denominatorcurrency/periodstrictly positive
NDnet debtcurrencydebt less declared cash
NCInoncontrolling interestcurrencyinclude when EBITDA consolidates subsidiaries
Sdiluted sharessharespositive
  • Use IEEE-754 binary64 arithmetic without intermediate rounding.
  • Report ratios to at least four decimals and currency outputs to a declared presentation precision only after calculation.
  • Use the ordinary median; for an even peer count, average the two central values.
  • Keep percentage points distinct from decimals and basis points.

Read the formula in the same order as the algorithm. Validate identity, ordering, units, and supported state first. Apply the selected equality and window rules second. Calculate with unrounded numeric values. Round only at the declared presentation boundary, and preserve null as a diagnostic rather than coercing it to zero.

Build the algorithm

  1. Freeze EV and EBITDA definitions
  2. Validate positive comparable EBITDA
  3. Calculate peer EV/EBITDA and median
  4. Multiply by target EBITDA
  5. Subtract net debt, preferred equity, and NCI; add nonoperating assets
  6. Divide positive common equity by diluted shares

Production-minded operational checklist

  1. Freeze valuation and knowledge-cutoff timestamps.
  2. Reconcile every numerator and denominator to a declared reporting basis.
  3. Apply eligibility rules before aggregation.
  4. Inspect peer dispersion and sensitivity before using a point estimate.
  5. Store the complete peer ledger beside the result.

The checklist is intentionally strict: an explicit rejection is safer than a plausible output built from stale, malformed, or unsupported state.

Worked synthetic example

The canonical fixture is synthetic teaching data, not an observed control event, customer order, or broker execution. Its primary author-derived output, implied_price, is 9.0. The complete input and output are in datasets/canonical-input.json and datasets/expected-output.json.

Synthetic peer multiples are 6× through 10×, so the median is 8×. Target EBITDA of 150 implies EV of 1,200. Subtracting 300 net debt, 20 preferred, and 30 NCI, then adding 50 nonoperating assets, gives 900 equity value and 9.00 per share.

Canonical peer audit table

Synthetic peerDerived EV EBITDA
E16.0000×
E27.0000×
E38.0000×
E49.0000×
E510.0000×
Selected median8.0000×

The table is the calculation ledger—not a market sample. Recalculate it before changing any displayed value.

Counterfactual checkpoint

Bridge boundary. If net debt and other senior claims consume all implied enterprise value, the package returns a null per-share value with a nonpositive-equity diagnostic. The output changes because enterprise value is not automatically common equity value

The structured result retains state and diagnostics in addition to the primary number. That makes the calculation independently reviewable and prevents a partial, null, rejected, or venue-bounded outcome from being mistaken for an unqualified value.

Boundary and counterexample workbook

The playground computes every scenario at 61 deterministic parameter states. The table uses the declared focus step and states whether that focus reproduces the canonical fixture. The full state ledger and compressed transition segments are in datasets/scenario-results.json.

ScenarioReview focusPurposeStatePrimary outputDiagnosticDecision segments
Canonical driver sweepStep 30 · canonical fixtureSynthetic deterministic scenario; every state is recomputed from the reference algorithm.valuation-complete9.0000 implied priceselected 8.0000× · peer range 4.0000× · valuation-complete1
Peer dispersionStep 30 · comparison focusSynthetic deterministic scenario; every state is recomputed from the reference algorithm.valuation-complete10.5000 implied priceselected 9.0000× · peer range 3.5000× · valuation-complete1
Peer valuation levelStep 30 · canonical fixtureSynthetic deterministic scenario; every state is recomputed from the reference algorithm.valuation-complete9.0000 implied priceselected 8.0000× · peer range 4.0000× · valuation-complete1
Current-price comparisonStep 30 · comparison focusSynthetic deterministic scenario; every state is recomputed from the reference algorithm.valuation-complete9.0000 implied priceselected 8.0000× · peer range 4.0000× · valuation-complete1
Denominator edgeStep 30 · comparison focusSynthetic deterministic scenario; every state is recomputed from the reference algorithm.valuation-complete3.4000 implied priceselected 8.0000× · peer range 4.0000× · valuation-complete2
Target fundamental sensitivityStep 30 · comparison focusSynthetic deterministic scenario; every state is recomputed from the reference algorithm.valuation-complete8.1818 implied priceselected 8.0000× · peer range 4.0000× · valuation-complete1
Stress comparisonStep 30 · comparison focusSynthetic deterministic scenario; every state is recomputed from the reference algorithm.valuation-complete3.5000 implied priceselected 8.0000× · peer range 4.0000× · valuation-complete2

These rows are not backtest observations. They are controlled counterexamples that expose how one driver changes the state, output, or reason code while the rest of the contract stays fixed.

Visualize the boundary

Calculation flow

Rendering system map…

The flow is deliberately gated: a failed input, eligibility, unit, or model-design check stops the value bridge instead of silently manufacturing a number.

EV/EBITDA Comparable Valuation annotated teaching map

EV/EBITDA Comparable Valuation calculation, routing, and rejection decisions

Figure 3. Unsupported states are routed or rejected explicitly instead of being converted into a plausible-looking multiple.

Open this SVG at full size, or use the guided playground to compare the seven topic-specific canonical, boundary, policy, and failure scenarios.

The Mermaid flow answers where the selected calculation sits in the processing sequence. The SVG keeps the formula, output, decision boundary, and invariant visible together. The lab lets the reader step through the same structured states without changing the underlying definition.

Implementation walkthrough

The Python and TypeScript references begin with the same validation contract, reject malformed and unsupported state before calculation, preserve declared ordering and rounding policies, and return structured diagnostics rather than one context-free number.

The main implementation branches are:

  • EBITDA ≤ 0 — Reject or route to revenue/asset/DCF methods, because The positive-multiple ordering fails.
  • Consolidated EBITDA includes NCI earnings — Include NCI in the bridge, because Enterprise claims must match operating scope.
  • Implied common equity ≤ 0 — Return null price and diagnostic state, because A negative per-share quote would misstate the bridge.

Neither reference silently fetches data, mutates caller-owned inputs outside the declared engine behavior, guesses hidden state, or substitutes a provider default. Shared JSON fixtures make value, null, state, and reason-code drift visible across languages.

Testing and validation

Definition tests compare every canonical field, reject malformed state, and exercise the material boundary. Family validation recomputes every playground state from the reference function. Independent arithmetic is recorded beside the fixture rather than inferred only from implementation output.

The audit must preserve these invariants:

  • every eligible peer's raw numerator and denominator
  • each derived peer multiple
  • selected aggregation statistic and dispersion
  • target denominator and capital bridge
  • final implied value and comparison with current price

Passing the suite proves selected-convention arithmetic and cross-language parity; it does not prove peer comparability or investment usefulness.

Failure modes and misuse

  • The selected peer statistic transmits peer mispricing and normalization errors into the target estimate.
  • A narrow range does not prove economic comparability; a wide range makes the point estimate fragile.
  • Forward inputs are estimates with provider, vintage, horizon, and revision risk.
  • Relative valuation explains a price conditional on peer pricing; it does not establish intrinsic value or future return.

Debugging order

When a result looks surprising, inspect the state in this order:

  1. Confirm identifiers, scope, side, and decision clock.
  2. Confirm units, ordering, and point-in-time inputs.
  3. Confirm equality, rounding, null, and reset policies.
  4. Recalculate the invariant and declared scenario focus before changing code.

Evidence and historical boundary

Historical decision: not useful. A named company would add filing identity, provider-price, forecast-vintage, adjustment, licensing, and hindsight questions without clarifying the arithmetic better than controlled synthetic peers. The package therefore makes no claim about any real security's fair value or future return.

The primary sources are Damodaran relative valuation, NYU valuation portal, SEC non-GAAP C&DIs. They support the source roles listed in the research ledger, not a redistributable historical observation, a private participant decision, production conformance certification, execution-quality result, profitability claim, or prediction claim.

<!-- D18-F03 additive enhancement: method-selection and continuity -->

Choosing among the five relative-value methods

Use the value basis to choose the method before looking at the output label. The highlighted row is this topic's frozen contract; the other rows are nearby methods, not interchangeable fallbacks.

MethodValue basisCanonical driverRejection or routing boundary
P/E Comparable ValuationEquity value / sharePositive EPSReject nonpositive EPS
EV/EBITDA Comparable ValuationEV to equity bridgePositive EBITDARoute financial firms; reject incompatible EBITDA
Price-to-Book ValuationEquity value / sharePositive common BVPSReject nonpositive book; no silent tangible-book swap
PEG RatioForward P/E / growth pointPositive growth pointsReject zero/negative growth; expose 100 times unit trap
Peer-Multiple RegressionConditional fitted multipleGrowth plus ROE plus leverageReject small/rank-deficient designs; flag extrapolation

Interpretation ladder for EV/EBITDA Comparable Valuation

  1. Ask: What enterprise and equity value follow from a compatible EV/EBITDA median?
  2. Verify the driver: Positive EBITDA.
  3. Preserve the basis: Enterprise value, then an equity bridge.
  4. Stop or route when: Route financial firms and reject incompatible EBITDA definitions.
  5. Carry the result forward only as conditional relative value, with its valuation date, knowledge cutoff, units, peer ledger, and diagnostic state.

EV/EBITDA Comparable Valuation method-selection map showing five non-interchangeable relative-value contracts

Figure 4. The highlighted contract answers this topic's question; the other rows prevent a familiar multiple from being applied to the wrong denominator or value basis.

The companion topic glossary defines governed terms and units. Continue through the family learning flow, then use the embedded playground to test the same boundary under synthetic scenarios.

Summary and next topic

You can now produce an enterprise-value estimate and auditable common-equity bridge. The learning flow is: Normalized financial statements and peer universe → EV/EBITDA Comparable Valuation → Price-to-Book Valuation. Carry the result forward only with its scope, clock, state, and evidence label.

EV/EBITDA Comparable Valuation calculation flow

This flow identifies the selected calculation stages and the structured output.

Rendering system map…

Takeaway: Keep operating valuation and the capital bridge on separate lines; most silent errors occur between them.

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

S1 — Relative Valuation

  • Organization or authors: Aswath Damodaran, New York University Stern School of Business
  • Source type: Authoritative academic lecture material
  • Publication or effective date: Current teaching deck accessed 2026-08-04
  • Version: 91-page relative-valuation deck
  • URL or DOI: https://people.stern.nyu.edu/adamodar/pdfiles/country/relval.pdf
  • Accessed: 2026-08-04
  • Jurisdiction: General corporate valuation
  • Supports: A multiple must be defined consistently, compared across genuinely comparable firms, and controlled for growth, risk, and cash-flow fundamentals.
  • Limitations: Does not prescribe this package's synthetic peers, median selection, or point estimate.

S2 — Valuation: Relative Valuation Resources

  • Organization or authors: Aswath Damodaran, New York University Stern School of Business
  • Source type: Authoritative academic resource index
  • Publication or effective date: Accessed 2026-08-04
  • Version: Current web page
  • URL or DOI: https://pages.stern.nyu.edu/~adamodar/New_Home_Page/valuation/val.htm
  • Accessed: 2026-08-04
  • Jurisdiction: General corporate valuation
  • Supports: P/E, PEG, value/EBITDA, and price/book are distinct relative-valuation multiples with distinct fundamental drivers.
  • Limitations: The page is a resource index, not a standardized data contract.

S3 — Non-GAAP Financial Measures Compliance and Disclosure Interpretations

  • Organization or authors: U.S. Securities and Exchange Commission, Division of Corporation Finance
  • Source type: Official regulatory staff guidance
  • Publication or effective date: Updated through December 13, 2022; accessed 2026-08-04
  • Version: Current web version
  • URL or DOI: https://www.sec.gov/rules-regulations/staff-guidance/corporation-finance-interpretations/non-gaap-financial-measures
  • Accessed: 2026-08-04
  • Jurisdiction: United States SEC reporting
  • Supports: EBITDA and adjusted measures require attention to definition, reconciliation, and prominence; ratios containing non-GAAP components inherit those concerns.
  • Limitations: Does not standardize an analyst's peer EBITDA or enterprise-value bridge outside its regulatory context.

Evidence boundary

Sources establish the valuation concepts, accounting or regulatory boundaries, and research context. They do not verify the synthetic fixture, select these peers, or support a return forecast.

relative-valuation.ts
/** Reference calculations for D18-F03 Relative Valuation. */

type Row = Record<string, unknown>;

function numberValue(name: string, value: unknown, options: {positive?: boolean; nonnegative?: boolean} = {}): number {
  if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be a finite number`);
  if (options.positive && value <= 0) throw new RangeError(`${name} must be greater than zero`);
  if (options.nonnegative && value < 0) throw new RangeError(`${name} must be nonnegative`);
  return value;
}

function record(name: string, value: unknown): Row {
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${name} must be an object`);
  return value as Row;
}

function rows(name: string, value: unknown, minimum = 3): Row[] {
  if (!Array.isArray(value) || value.length < minimum) throw new RangeError(`${name} must contain at least ${minimum} rows`);
  const seen = new Set<string>();
  return value.map((raw, index) => {
    const item = record(`${name}[${index}]`, raw);
    if (typeof item.id !== "string" || !item.id.trim()) throw new RangeError(`${name}[${index}].id must be a nonempty string`);
    if (seen.has(item.id)) throw new RangeError(`${name} ids must be unique`);
    seen.add(item.id);
    return item;
  });
}

function median(values: number[]): number {
  const ordered = [...values].sort((a, b) => a - b);
  const middle = Math.floor(ordered.length / 2);
  return ordered.length % 2 ? ordered[middle] : (ordered[middle - 1] + ordered[middle]) / 2;
}

function summary(values: number[]): Record<string, number> {
  const ordered = [...values].sort((a, b) => a - b);
  return {minimum: ordered[0], median: median(ordered), maximum: ordered.at(-1)!, range: ordered.at(-1)! - ordered[0]};
}

function premiumDiscount(implied: number | null, current: unknown, label = "target.current_price"): number | null {
  if (implied === null || current === undefined || current === null) return null;
  return implied / numberValue(label, current, {positive: true}) - 1;
}

function peComparable(inputs: Row): Row {
  const target = record("target", inputs.target);
  const targetEps = numberValue("target.earnings_per_share", target.earnings_per_share, {positive: true});
  const peerMultiples = rows("peers", inputs.peers).map((peer, index) => {
    const price = numberValue(`peers[${index}].price`, peer.price, {positive: true});
    const eps = numberValue(`peers[${index}].earnings_per_share`, peer.earnings_per_share, {positive: true});
    return {id: peer.id, price, earnings_per_share: eps, pe_ratio: price / eps};
  });
  const multipleSummary = summary(peerMultiples.map(row => row.pe_ratio));
  const selected = multipleSummary.median;
  const implied = selected * targetEps;
  return {model: "median-positive-eps-peer-pe", peer_count: peerMultiples.length, peer_multiples: peerMultiples,
    multiple_summary: multipleSummary, selected_multiple: selected, target_earnings_per_share: targetEps,
    implied_price: implied, premium_discount_to_current: premiumDiscount(implied, target.current_price), state: "valuation-complete"};
}

function evEbitda(inputs: Row): Row {
  const target = record("target", inputs.target);
  const ebitda = numberValue("target.ebitda", target.ebitda, {positive: true});
  const shares = numberValue("target.shares_outstanding", target.shares_outstanding, {positive: true});
  const netDebt = numberValue("target.net_debt", target.net_debt ?? 0);
  const preferred = numberValue("target.preferred_equity", target.preferred_equity ?? 0, {nonnegative: true});
  const nci = numberValue("target.noncontrolling_interest", target.noncontrolling_interest ?? 0, {nonnegative: true});
  const nonoperating = numberValue("target.nonoperating_assets", target.nonoperating_assets ?? 0, {nonnegative: true});
  const peerMultiples = rows("peers", inputs.peers).map((peer, index) => {
    const enterpriseValue = numberValue(`peers[${index}].enterprise_value`, peer.enterprise_value, {positive: true});
    const peerEbitda = numberValue(`peers[${index}].ebitda`, peer.ebitda, {positive: true});
    return {id: peer.id, enterprise_value: enterpriseValue, ebitda: peerEbitda, ev_ebitda: enterpriseValue / peerEbitda};
  });
  const multipleSummary = summary(peerMultiples.map(row => row.ev_ebitda));
  const selected = multipleSummary.median;
  const impliedEnterprise = selected * ebitda;
  const impliedEquity = impliedEnterprise - netDebt - preferred - nci + nonoperating;
  const impliedPrice = impliedEquity > 0 ? impliedEquity / shares : null;
  return {model: "median-positive-ebitda-peer-ev-ebitda", peer_count: peerMultiples.length, peer_multiples: peerMultiples,
    multiple_summary: multipleSummary, selected_multiple: selected, target_ebitda: ebitda,
    implied_enterprise_value: impliedEnterprise, equity_bridge: {net_debt: netDebt, preferred_equity: preferred,
      noncontrolling_interest: nci, nonoperating_assets: nonoperating}, implied_equity_value: impliedEquity,
    shares_outstanding: shares, implied_price: impliedPrice,
    premium_discount_to_current: premiumDiscount(impliedPrice, target.current_price),
    state: impliedPrice === null ? "nonpositive-equity-bridge" : "valuation-complete"};
}

function priceToBook(inputs: Row): Row {
  const target = record("target", inputs.target);
  const targetBvps = numberValue("target.book_value_per_share", target.book_value_per_share, {positive: true});
  const peerMultiples = rows("peers", inputs.peers).map((peer, index) => {
    const price = numberValue(`peers[${index}].price`, peer.price, {positive: true});
    const bvps = numberValue(`peers[${index}].book_value_per_share`, peer.book_value_per_share, {positive: true});
    return {id: peer.id, price, book_value_per_share: bvps, price_to_book: price / bvps};
  });
  const multipleSummary = summary(peerMultiples.map(row => row.price_to_book));
  const selected = multipleSummary.median;
  const implied = selected * targetBvps;
  return {model: "median-positive-book-value-peer-pb", peer_count: peerMultiples.length, peer_multiples: peerMultiples,
    multiple_summary: multipleSummary, selected_multiple: selected, target_book_value_per_share: targetBvps,
    implied_price: implied, premium_discount_to_current: premiumDiscount(implied, target.current_price), state: "valuation-complete"};
}

function peg(inputs: Row): Row {
  const target = record("target", inputs.target);
  const forwardPe = numberValue("target.forward_pe", target.forward_pe, {positive: true});
  const growth = numberValue("target.expected_eps_growth_percent", target.expected_eps_growth_percent, {positive: true});
  const peerRatios = rows("peers", inputs.peers).map((peer, index) => {
    const peerPe = numberValue(`peers[${index}].forward_pe`, peer.forward_pe, {positive: true});
    const peerGrowth = numberValue(`peers[${index}].expected_eps_growth_percent`, peer.expected_eps_growth_percent, {positive: true});
    return {id: peer.id, forward_pe: peerPe, expected_eps_growth_percent: peerGrowth, peg_ratio: peerPe / peerGrowth};
  });
  const pegSummary = summary(peerRatios.map(row => row.peg_ratio));
  const targetPeg = forwardPe / growth;
  const impliedPe = pegSummary.median * growth;
  const forwardEps = target.forward_earnings_per_share === undefined || target.forward_earnings_per_share === null
    ? null : numberValue("target.forward_earnings_per_share", target.forward_earnings_per_share, {positive: true});
  const impliedPrice = forwardEps === null ? null : impliedPe * forwardEps;
  return {model: "forward-pe-divided-by-growth-percentage-points", growth_unit: "percentage-points",
    peer_count: peerRatios.length, peer_ratios: peerRatios, peg_summary: pegSummary, target_forward_pe: forwardPe,
    target_growth_percent: growth, target_peg_ratio: targetPeg, selected_peer_peg: pegSummary.median,
    relative_peg: targetPeg / pegSummary.median, implied_forward_pe: impliedPe,
    forward_earnings_per_share: forwardEps, implied_price: impliedPrice,
    premium_discount_to_current: premiumDiscount(impliedPrice, target.current_price),
    state: impliedPrice === null ? "ratio-only" : "valuation-complete"};
}

function solve(matrix: number[][], vector: number[]): number[] {
  const n = vector.length;
  const augmented = matrix.map((row, index) => [...row, vector[index]]);
  for (let column = 0; column < n; column += 1) {
    let pivot = column;
    for (let row = column + 1; row < n; row += 1) if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column])) pivot = row;
    if (Math.abs(augmented[pivot][column]) <= 1e-12) throw new RangeError("regression design matrix is rank deficient");
    [augmented[column], augmented[pivot]] = [augmented[pivot], augmented[column]];
    const scale = augmented[column][column];
    augmented[column] = augmented[column].map(value => value / scale);
    for (let row = 0; row < n; row += 1) {
      if (row === column) continue;
      const factor = augmented[row][column];
      augmented[row] = augmented[row].map((value, index) => value - factor * augmented[column][index]);
    }
  }
  return augmented.map(row => row.at(-1)!);
}

function features(name: string, row: Row): number[] {
  return [1, numberValue(`${name}.expected_growth_percent`, row.expected_growth_percent) / 100,
    numberValue(`${name}.return_on_equity_percent`, row.return_on_equity_percent) / 100,
    numberValue(`${name}.net_debt_to_ebitda`, row.net_debt_to_ebitda)];
}

function regression(inputs: Row): Row {
  const peers = rows("peers", inputs.peers, 6);
  const target = record("target", inputs.target);
  const xRows: number[][] = [];
  const y: number[] = [];
  const multiples: number[] = [];
  peers.forEach((peer, index) => {
    const multiple = numberValue(`peers[${index}].multiple`, peer.multiple, {positive: true});
    xRows.push(features(`peers[${index}]`, peer)); y.push(Math.log(multiple)); multiples.push(multiple);
  });
  const width = xRows[0].length;
  const xtx = Array.from({length: width}, (_, i) => Array.from({length: width}, (_, j) => xRows.reduce((sum, row) => sum + row[i] * row[j], 0)));
  const xty = Array.from({length: width}, (_, i) => xRows.reduce((sum, row, index) => sum + row[i] * y[index], 0));
  const beta = solve(xtx, xty);
  const fittedLogs = xRows.map(row => row.reduce((sum, value, index) => sum + value * beta[index], 0));
  const mean = y.reduce((a, b) => a + b, 0) / y.length;
  const sse = y.reduce((sum, value, index) => sum + (value - fittedLogs[index]) ** 2, 0);
  const sst = y.reduce((sum, value) => sum + (value - mean) ** 2, 0);
  const rSquared = sst === 0 ? (sse === 0 ? 1 : 0) : 1 - sse / sst;
  const fittedPeers = peers.map((peer, index) => ({id: peer.id, observed_multiple: multiples[index],
    fitted_multiple: Math.exp(fittedLogs[index]), log_residual: y[index] - fittedLogs[index]}));
  const targetX = features("target", target);
  const predictedLog = targetX.reduce((sum, value, index) => sum + value * beta[index], 0);
  const impliedMultiple = Math.exp(predictedLog);
  const metric = numberValue("target.valuation_metric", target.valuation_metric, {positive: true});
  const impliedValue = impliedMultiple * metric;
  return {model: "ols-log-multiple-on-growth-roe-and-leverage", peer_count: peers.length,
    feature_order: ["intercept", "expected_growth_decimal", "return_on_equity_decimal", "net_debt_to_ebitda"],
    coefficients: {intercept: beta[0], expected_growth_decimal: beta[1], return_on_equity_decimal: beta[2], net_debt_to_ebitda: beta[3]},
    r_squared_log_space: rSquared, fitted_peers: fittedPeers, target_predicted_log_multiple: predictedLog,
    implied_multiple: impliedMultiple, target_valuation_metric: metric, implied_value: impliedValue,
    premium_discount_to_current: premiumDiscount(impliedValue, target.current_value, "target.current_value"),
    smearing_correction: "not-applied", state: "valuation-complete"};
}

export function calculate(topicId: string, rawInputs: unknown): Row {
  const inputs = record("inputs", rawInputs);
  if (topicId === "D18-F03-A01") return peComparable(inputs);
  if (topicId === "D18-F03-A02") return evEbitda(inputs);
  if (topicId === "D18-F03-A03") return priceToBook(inputs);
  if (topicId === "D18-F03-A04") return peg(inputs);
  if (topicId === "D18-F03-A05") return regression(inputs);
  throw new RangeError(`unsupported topic_id: ${topicId}`);
}
Full-height labplaygroundOpen full screen