Fit and audit a log-multiple OLS model with a frozen feature order, rank checks, residual diagnostics, and a target-value bridge.
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
A median treats peer differences as noise. Regression makes a different promise: explain cross-sectional multiple differences with declared fundamentals. That adds model risk, small-sample risk, functional-form risk, and extrapolation risk that must remain visible.
The precise question is: What multiple is warranted by a peer regression after controlling explicitly for growth, profitability, and leverage?
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
Logging keeps the fitted multiple positive and interprets coefficients as changes in log multiple. OLS describes conditional peer pricing; it does not prove that a feature causes valuation or that the relationship will persist.
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 model fits ln(multiple) by ordinary least squares on an intercept, expected growth decimal, ROE decimal, and net-debt/EBITDA. It exponentiates the target fitted log multiple without a smearing correction. Robust, weighted, nonlinear, regularized, and causal models are outside this contract.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Canonical log-OLS | Unweighted OLS on log multiple | Transparent conditional peer model | Outliers and small samples |
| Robust or weighted regression | Changes loss or observation weights | Heteroskedastic or outlier-prone peers | Different estimator |
| Regularized or nonlinear model | Adds shrinkage or nonlinearities | Larger feature sets | Harder interpretation and leakage risk |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| Sourced fact | Valuation-theory-guided warranted multiples can improve peer selection in the cited samples. | Bhojraj and Lee (2002) | Sample results do not guarantee this model. |
| Implementation choice | Fit unweighted OLS in log-multiple space with three drivers and no smearing correction. | Frozen package contract | Not a universal regression specification. |
| Synthetic teaching input | Eight peer rows follow a controlled noisy log-linear relation. | Repository fixture | Not empirical evidence. |
| Author-derived calculation | Coefficients, R², residuals, and target multiple come from the frozen normal-equation solve. | Independent fixture and parity tests | Numerical fit does not prove causality or forecast value. |
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
ln(M_i) = β0 + βg g_i + βr ROE_i + βl leverage_i + ε_i; M_target = exp(x_target β); implied_value = M_target × target_metric
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| M_i | observed peer multiple | turns | strictly positive |
| g_i | expected growth | decimal | percentage input divided by 100 |
| ROE_i | return on equity | decimal | percentage input divided by 100 |
| L_i | net debt/EBITDA | turns | finite |
| β | OLS coefficient vector | mixed | feature order frozen |
| ε_i | log residual | log points | descriptive, not causal |
- 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
- Freeze peer universe, multiple, feature definitions, and knowledge cutoff
- Require at least six complete peers and positive multiples
- Convert growth and ROE points to decimals
- Build X with an intercept and solve (X'X)β=X'y
- Calculate fitted logs, residuals, and log-space R²
- Score the target, exponentiate, and multiply by the compatible target metric
Production-minded operational checklist
- Freeze valuation and knowledge-cutoff timestamps.
- Reconcile every numerator and denominator to a declared reporting basis.
- Apply eligibility rules before aggregation.
- Inspect peer dispersion and sensitivity before using a point estimate.
- 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_multiple, is 6.209198064103. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
Eight synthetic peers are generated around a known log-linear relation with small residuals. The implementation re-estimates the coefficients from the rows, publishes every fitted peer and residual, scores the target, and multiplies the positive implied multiple by a target valuation metric of 5.
Canonical peer audit table
| Synthetic peer | Observed | Fitted | Log residual |
|---|---|---|---|
| R1 | 3.6693× | 3.7970× | -0.0342 |
| R2 | 4.6043× | 4.4574× | 0.0324 |
| R3 | 5.1243× | 5.2327× | -0.0209 |
| R4 | 6.1780× | 6.1428× | 0.0057 |
| R5 | 6.5470× | 6.2763× | 0.0422 |
| R6 | 7.4038× | 7.6519× | -0.0330 |
| R7 | 5.9895× | 5.8909× | 0.0166 |
| R8 | 7.9566× | 8.0276× | -0.0089 |
| Target | 6.2092× |
The table is the calculation ledger—not a market sample. Recalculate it before changing any displayed value.
Counterfactual checkpoint
Identification boundary. If one feature becomes an exact linear combination of the others, the package rejects the regression instead of returning unstable coefficients. The output changes because the design no longer identifies a unique coefficient vector
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.
| Scenario | Review focus | Purpose | State | Primary output | Diagnostic | Decision segments |
|---|---|---|---|---|---|---|
| Canonical driver sweep | Step 30 · canonical fixture | Synthetic deterministic scenario; every state is recomputed from the reference algorithm. | valuation-complete | 6.2092× implied multiple | log-space R² 0.9871 · 8 peers · no smearing | 1 |
| Peer dispersion | Step 30 · comparison focus | Synthetic deterministic scenario; every state is recomputed from the reference algorithm. | valuation-complete | 6.3097× implied multiple | log-space R² 0.9871 · 8 peers · no smearing | 1 |
| Peer valuation level | Step 30 · comparison focus | Synthetic deterministic scenario; every state is recomputed from the reference algorithm. | valuation-complete | 5.8121× implied multiple | log-space R² 0.9871 · 8 peers · no smearing | 1 |
| Current-price comparison | Step 30 · comparison focus | Synthetic deterministic scenario; every state is recomputed from the reference algorithm. | valuation-complete | 6.2092× implied multiple | log-space R² 0.9871 · 8 peers · no smearing | 1 |
| Denominator edge | Step 30 · comparison focus | Synthetic deterministic scenario; every state is recomputed from the reference algorithm. | valuation-complete | 6.1834× implied multiple | log-space R² 0.6434 · 8 peers · no smearing | 1 |
| Target fundamental sensitivity | Step 30 · comparison focus | Synthetic deterministic scenario; every state is recomputed from the reference algorithm. | valuation-complete | 6.9126× implied multiple | log-space R² 0.8536 · 8 peers · no smearing | 1 |
| Stress comparison | Step 30 · canonical fixture | Synthetic deterministic scenario; every state is recomputed from the reference algorithm. | valuation-complete | 6.2092× implied multiple | log-space R² 0.9871 · 8 peers · no smearing | 1 |
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
The flow is deliberately gated: a failed input, eligibility, unit, or model-design check stops the value bridge instead of silently manufacturing a number.
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:
- Design is rank deficient — Reject, because Coefficients are not uniquely identified.
- Target lies outside peer feature range — Flag extrapolation before use, because Fit quality inside the sample does not validate the target region.
- R² is high — Treat as in-sample description only, because It is not causality or out-of-sample accuracy.
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:
- peer universe and feature definitions
- design matrix in frozen feature order
- log observed and fitted multiples
- coefficient vector, residuals, SSE, SST, and R²
- target feature vector, fitted log multiple, exponentiation, and value bridge
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.
- Normal equations are intentionally transparent but less numerically stable than QR or SVD for ill-conditioned production designs.
- Exponentiating a fitted log without a smearing correction estimates the conditional geometric scale, not the conditional arithmetic mean.
Debugging order
When a result looks surprising, inspect the state in this order:
- Confirm identifiers, scope, side, and decision clock.
- Confirm units, ordering, and point-in-time inputs.
- Confirm equality, rounding, null, and reset policies.
- 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, Bhojraj and Lee (2002), Alford (1992). 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.
| Method | Value basis | Canonical driver | Rejection or routing boundary |
|---|---|---|---|
| P/E Comparable Valuation | Equity value / share | Positive EPS | Reject nonpositive EPS |
| EV/EBITDA Comparable Valuation | EV to equity bridge | Positive EBITDA | Route financial firms; reject incompatible EBITDA |
| Price-to-Book Valuation | Equity value / share | Positive common BVPS | Reject nonpositive book; no silent tangible-book swap |
| PEG Ratio | Forward P/E / growth point | Positive growth points | Reject zero/negative growth; expose 100 times unit trap |
| Peer-Multiple Regression | Conditional fitted multiple | Growth plus ROE plus leverage | Reject small/rank-deficient designs; flag extrapolation |
Interpretation ladder for Peer-Multiple Regression
- Ask: What multiple is warranted by an explicit peer model after controlling for declared fundamentals?
- Verify the driver: Growth, ROE, and leverage.
- Preserve the basis: Conditional fitted multiple.
- Stop or route when: Reject small or rank-deficient designs and flag extrapolation.
- Carry the result forward only as conditional relative value, with its valuation date, knowledge cutoff, units, peer ledger, and diagnostic state.
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 a conditional warranted multiple with model diagnostics. The learning flow is: Normalized financial statements and peer universe → Peer-Multiple Regression → Quality and Distress Analysis. Carry the result forward only with its scope, clock, state, and evidence label.
Rendered from the canonical Mermaid sources linked by this article.
Peer-Multiple Regression calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: Regression trades a single peer median for an explicit conditional model; the added precision is conditional on design, sample, and feature definitions.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
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 — Who Is My Peer? A Valuation-Based Approach to the Selection of Comparable Firms
- Organization or authors: Sanjeev Bhojraj and Charles M. C. Lee
- Source type: Original peer-reviewed research
- Publication or effective date: 2002
- Version: Journal of Accounting Research 40(2), 407-439
- URL or DOI: https://doi.org/10.1111/1475-679X.00054
- Accessed: 2026-08-04
- Jurisdiction: Empirical listed-equity sample
- Supports: Valuation-theory-guided warranted multiples can inform peer selection and were tested against future multiples in the paper's samples.
- Limitations: Does not make this tutorial regression causal, predictive, or universally accurate.
S3 — The Effect of the Set of Comparable Firms on the Accuracy of the Price-Earnings Valuation Method
- Organization or authors: Andrew W. Alford
- Source type: Original peer-reviewed research
- Publication or effective date: 1992
- Version: Journal of Accounting Research 30(1), 94-108
- URL or DOI: https://doi.org/10.2307/2491093
- Accessed: 2026-08-04
- Jurisdiction: Empirical listed-equity sample
- Supports: Comparable-firm selection affects P/E valuation accuracy and can be studied by industry, risk, and earnings growth.
- Limitations: Does not guarantee accuracy for a new sample or endorse the package's synthetic peer set.
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.
Full dependency-light reference implementations in both supported languages.
/** 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}`);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.