relative-valuation-toolkit

v2026.09.24

Prices a company against peers and the market with multiples — builds PE, PEG, PBV, EV/EBITDA, EV/EBIT, EV/Sales and EV/Invested Capital with the numerator/denominator consistency rule enforced, computes peer medians and quartiles, fits an ordinary-least-squares regression of a multiple on its companion variables for a sector or the whole market, reports predicted versus actual under and over valuation, and derives the intrinsic multiple from DCF fundamentals. Prices a conglomerate division by division with a sum of the parts, and values cross-holdings and minority interests. Bundles Damodaran's regional pricing regressions and multiple distributions. Use for relative valuation, comparable company analysis, trading comps, sector or market multiple regressions, justified multiples, PEG screens, sum-of-the-parts or break-up analysis, conglomerate discounts, cross-holdings and minority interests, or when asked whether a stock is cheap on its multiple.

GitHub
Install command
npx skhub add lyndonkl/relative-valuation-toolkit
Markdown
SKILL.md

Relative valuation toolkit

Relative valuation infers what an asset is worth from what the market pays for similar assets. It is pricing, not valuation. A stock can be cheap against its comparable group and still be badly overpriced in absolute terms — in a bubble the whole group is wrong together. Say "cheap versus these peers", never "cheap".

Practice runs on this method. Roughly 85% of equity research reports and more than half of acquisition valuations rest on multiples, and plenty of discounted cash flow models are relative valuations in disguise, with a terminal value set at 8x year-5 EBITDA.

The arithmetic here is scripted. Your job is choosing the comparable set, choosing the control technique, and deciding what an unexplained residual means.

The script

resources/multiples.py — pure standard library, no installation needed. Every subcommand takes JSON on stdin (or --in FILE) and prints JSON.

python3 resources/multiples.py <subcommand> --example    # show input shape
python3 resources/multiples.py <subcommand> --in payload.json
python3 resources/multiples.py selftest                  # verify the engine
SubcommandTurns thisInto this
multiplesfinancials and a market pricetraded multiples, with inconsistent pairings refused
peer-statsa column of peer multiplesmedian, quartiles, skew, drop-out count, median test
locateone multiplewhere it sits in a bundled market distribution
intrinsicDCF fundamentalsthe justified multiple those fundamentals imply
regressa sector or market samplecoefficients, R-squared, standard errors, t-statistics
predicta fitted or published equationpredicted multiple and the over/under percentage
sum-of-partsdivisions and their multiplesper-division values and shares, the total, the gap to market
cross-holdingsstakes in other firmswhat to add, what to subtract, and at what value

selftest runs 106 checks: worked examples from the source, and algebraic identities a wrong implementation cannot satisfy by accident. Run it after editing anything.

Run the multiple through four tests

Every multiple gets four tests before you trust it. Skipping any one of them is how comparable-company analysis becomes an exercise in confirming what you already believed.

Relative valuation progress:
- [ ] 1. Definitional — settle the numerator, denominator and timing
- [ ] 2. Descriptive — find where the multiple actually sits today
- [ ] 3. Analytical — derive its companion variable from a DCF
- [ ] 4. Application — pick comparables and control for the differences

1. Definitional test

A multiple is a standardized price: what you pay divided by what you get. Two rules.

Consistency. The numerator and denominator must belong to the same claimholders. Equity value goes with equity earnings or equity book value. Firm or enterprise value goes with operating measures. The script refuses the mismatches rather than computing them, so EV/Net Income and Price/EBITDA come back with an explanation instead of a number.

Uniformity. Every variable must be estimated the same way across every firm in the comparable list, accounting rules included. The same name hides many variants, so never borrow a multiple without knowing how it was built.

python3 resources/multiples.py multiples --example | python3 resources/multiples.py multiples

Supply market_cap (or price_per_share and shares_outstanding), debt, cash, minority_interests, and whichever denominators you have. Use the market value of debt from cost-of-capital-toolkit (mv-debt, field market_value_of_debt), not book debt. Take lease- and research-adjusted earnings from financial-statement-normalization, and apply the adjustment to every firm or to none.

The script nets cash out of enterprise value because interest income on cash appears in neither EBITDA nor EBIT. It adds minority interests back because a consolidated but partly owned subsidiary puts all of its EBITDA in the denominator while the parent owns only part of the equity.

Ask for an arbitrary pairing with custom:

"custom": [{"name": "EV/Net Income", "numerator": "enterprise", "denominator": "net_income"}]

Negative denominators are refused, not computed. A PE built on a loss is not a small number or a large one. It carries no ordering at all, so it cannot be ranked against anything. The output says which metric was negative and what to price on instead — a denominator that stays positive, a forward year, or a sector-specific driver. Record the refusal. That firm has now dropped out of the sample, and the drop-out biases every peer statistic computed without it.

Full map of numerators, denominators, timing variants and companion variables: resources/multiple-map.md.

2. Descriptive test

You cannot call a multiple high or low without knowing the distribution it came from.

Multiples are bounded at zero on the left and unbounded on the right, so their cross-sections are heavily right-skewed and the mean sits far above the median. In January 2021 the average US trailing PE was 103.25 against a median of 20.30 — the mean sat above the 90th percentile. Use the median or a percentile band. The mean of a multiple is close to meaningless.

python3 resources/multiples.py peer-stats --example | python3 resources/multiples.py peer-stats

Each firm needs multiple, and optionally companion and risk. Pass universe_size as the number of firms you started with, not the number that survived. The output separates firms_with_a_usable_multiple from firms_dropped and names each dropped firm, because that count is itself a finding. Only about a third of US firms had a usable trailing PE in January 2021, so PE-based conclusions describe the profitable subsample.

Handle outliers with care. They lie almost entirely on the positive side, so trimming them biases the typical multiple downward. Cap or use percentiles instead.

Add a subject and the output runs the median test: cheap needs a below-median multiple with an above-median companion variable and below-median risk. Treat it as a screen. It weighs each variable independently and ignores the size of every gap, so it runs out as soon as firms differ on more than one dimension.

To place a multiple in the wider market rather than in your peer set:

echo '{"table": "us_trailing_pe_2021", "value": 14.2}' | python3 resources/multiples.py locate

Thresholds decay. "Buy below 6x EBITDA" put you in the middle of the US distribution in January 2010 and below the 10th percentile by January 2021. Run locate against regional_ev_ebitda_median_2021 and the same 6x reads as deeply cheap in the US and close to normal in Japan. Same number, opposite verdicts, purely because the distribution moved.

3. Analytical test

Every multiple is a discounted cash flow model in compressed form. Divide the model by the multiple's denominator and the drivers fall out. The one that dominates cross-sectional differences is the companion variable — the thing you have to control for.

MultipleCompanion variable
PEexpected growth, with payout and cost of equity as controls
PEGrisk, payout and the level of growth
PBVreturn on equity
EV/Invested Capitalreturn on invested capital
EV/Salesafter-tax operating margin
P/Salesnet margin
EV/EBITDAreinvestment needs, tax rate, cost of capital
echo '{"multiple": "PBV", "roe": 0.2022, "growth": 0.04, "cost_of_equity": 0.09,
       "actual_multiple": 2.09}' | python3 resources/multiples.py intrinsic

Use an equity model for equity multiples and a firm model for enterprise multiples. Take the discount rate from cost-of-capital-toolkit (wacc, fields cost_of_equity and wacc). Add actual_multiple to get the gap between traded and justified.

Three things the output is telling you:

  • The relationship is never linear. PE rises with growth far faster at low required returns than at high ones, and the drag from risk is proportionately larger for high-growth firms. A risky high-growth firm can deserve a lower PE than a safe low-growth one.
  • Growth has to be paid for. g = (1 - payout) x ROE on the equity side and g = reinvestment rate x ROIC on the enterprise side. The PBV output reports the sustainable growth your inputs imply and flags the gap. When the long and short forms disagree, the inputs are inconsistent and neither answer is usable.
  • A low EV/EBITDA is often deserved. EBITDA sits above the capital spending that keeps the assets running. Raising CapEx from 30% to 50% of EBITDA cuts the justified multiple from 8.24 to 4.24 on otherwise identical inputs. Ryder System at 2.81x against a trucking sector at 5.61x was a truck-leasing firm with an ageing fleet, not a bargain.

PEG does not neutralise growth. Dividing by growth does not strip growth out, because value is non-linear in growth. PEG falls with risk and with the level of growth, so a PEG screen systematically flags the riskiest and fastest-growing names as cheap. If you use it, regress PEG on beta, payout and ln(growth) rather than comparing raw PEGs.

Formulas, symbol definitions and the full intrinsic payload table: resources/multiple-map.md.

4. Application test

Pick the control technique by counting how many fundamentals differ.

Dimensions that differTechnique
None — true twinsDirect comparison. Higher multiple is expensive.
OneTell the story, or use a modified multiple like PEG.
SeveralRegress the multiple on the fundamentals.

The third row is the normal case.

python3 resources/multiples.py regress --example | python3 resources/multiples.py regress
python3 resources/multiples.py predict --example | python3 resources/multiples.py predict

regress fits by the normal equations, solved with Gauss-Jordan elimination and partial pivoting, and returns coefficients, standard errors, t-statistics, R-squared, adjusted R-squared, residuals, and the correlation of every predictor pair. Enter every predictor as a decimal.

predict takes the equation three ways: observations to fit the sample first, equation for your own coefficients, or library for a bundled published regression. Omit subject after fitting and it prices every firm in the sample.

under/over valuation = actual multiple / predicted multiple - 1

Sector or market

A sector regression asks whether the firm is cheap against its peers. A market-wide regression asks whether it is cheap against every listed firm, given its fundamentals. Both are legitimate and they can disagree, because the peer group may itself be mispriced against the market. State which benchmark your verdict uses.

Bundled market and regional equations cover PE, PEG, PBV, EV/EBITDA, EV/Sales and EV/IC for six regions, plus the country PE regression and the S&P 500 earnings-yield regression. Pick the multiple with the highest regional R-squared when several disagree — EV/IC fits run 50-63%, PEG fits 11-33%.

Reading a fit

Check the t-statistics first. Above 2 is real, 1 to 2 is marginal, below 1 is noise. Drop the noise and re-run, even when theory insists the variable belongs. In pricing the market decides what matters, and explanatory power rarely suffers.

Then check R-squared. Below about 15% the sample is not priced on those fundamentals, so report the prediction as weak evidence rather than as an estimate.

Then check the signs against theory. A wrong sign is almost always multicollinearity, not a discovery.

The engine raises four warnings you should read before quoting any prediction:

  • Negative intercept. The equation can hand a weak firm a negative predicted multiple. The engine refuses to report an over/under against one. Re-run with "intercept": false and treat that as an imperfect fix, not a repair.
  • Multicollinearity between growth and risk. High-growth firms pay out less and often carry more risk, so the predictors overlap and individual coefficients flip sign. When this fires, use the prediction and ignore the coefficients.
  • Small sample. A 15 to 30 firm sector cannot support many predictors. Sector coefficients move violently between vintages; re-estimate every period.
  • Pooled markets. Add a region field to your observations. Country PE levels are driven by interest rates, real growth and country risk. Pooling markets attributes those country differences to the firm-level predictors instead.

Fitting details, the full library index, distribution tables and reproduced worked examples: resources/regressions.md.

The lesson of the worked examples

Telebras had one of the two lowest PEs in global telecom. Controlled for growth and emerging-market risk, it predicted at 8.35 and traded at 8.9 — slightly expensive. A low multiple is not the same as cheap, and that is the whole point of step 4.

Sum of the parts: pricing a multi-business firm

A conglomerate is not one comparable set. Price each division against its own sector, add the pieces, then take out what the segments never contained.

python3 resources/multiples.py sum-of-parts --example | python3 resources/multiples.py sum-of-parts
python3 resources/multiples.py cross-holdings --example | python3 resources/multiples.py cross-holdings

Pricing each division

Each division needs a scalar (ebitda, revenues, capital, ebit) and a scalar_value, plus one of three routes to a multiple.

RouteFieldWhat it does
Own multiplemultiplea number you have already settled
Crudepeer_multiplethe sector median, applied as it stands
Refinedregression with fundamentalsthe sector equation evaluated at this division's own returns and margins

A division is not the median firm in its sector. Controlling for fundamentals moved United Technologies from $61.7bn to $74.2bn.

Where segment disclosure stops at operating income, pass normalized_ebit and d_and_a and the script builds EBITDA. Normalise a cyclical segment first — GE's segments run on the 2013-17 average margin applied to 2017 revenues, not on 2017 alone.

Every division comes back with its value and its share_of_sum_of_parts. Those shares add to one, so they show which business the valuation actually rests on.

Every part has to be valued before debt. An equity-level multiple is refused, because the bridge subtracts debt once at the corporate level.

Three traps the engine will not let you walk into

Unallocated corporate overhead. Segment operating income is reported before corporate G&A, so the divisions add to more than the firm is worth. Either allocate the overhead across the divisions before you price them (corporate_costs_already_allocated: true) or pass corporate_expenses and let the script capitalise it:

drag = corporate expenses x (1 - t) x (1 + g) / (company cost of capital - g)

UTC's $408m of overhead is worth -$4,587m, about 6% of the total. The payload has to say which treatment it used. Leaving the field out is refused, because a silent zero overstates every conglomerate.

The conglomerate discount is observed, not derived. Pass market_enterprise_value and the output reports gap_to_market_enterprise_value, which is 1 minus market EV over the sum of the parts. Nothing above it is reduced. UTC's parts priced at $74.2bn against a $52.3bn enterprise value — a 30% gap that is only money if somebody buys the pieces at peer multiples, and may instead be the market's price for bad capital allocation. Passing a conglomerate_discount of your own is refused: it makes the sum of the parts agree with the market by construction.

Growth has to be earned division by division. Give each division its roc and cost_of_capital. One earning at or below its cost of capital comes back with high_growth_years_allowed: 0 and a warning. UTC Fire & Security earned 6.03% against a 6.78% cost of capital, so its whole value is terminal. A sector peer multiple carries the sector's growth with it, so applying one there prices in growth the division never earned.

Cross holdings

Accounting reports the three ownership tiers three different ways, and each one distorts the valuation in its own direction.

StakeConsolidatedTreatment
Minority passive, under 20%Noadd the stake
Minority active, 20-50%Noadd the stake, and strip its equity income out of operating income
Majority, above 50%Yes100% is already inside the value; subtract the minority interest

cross-holdings takes a list of holdings, each with ownership as a decimal. Set consolidated yourself; without it the script infers from the 50% line and says so, and control can attach below 50%.

Subtract the minority interest at market, not at book. Best is subsidiary_equity_value, from which the script takes the fraction the parent does not own. The fallback is minority_interest_book with sector_price_to_book. Book on its own is refused: it runs far too low for a profitable subsidiary. On the worked exercise the book shortcut gives $810m and the intrinsic treatment $725m, on a firm worth under a billion.

Add a minority stake at market where it trades (market_value_of_stake), at your own valuation where you have one (subsidiary_equity_value), and at book_equity times sector_price_to_book where you have neither. The output labels which of the three it used. Inside an intrinsic valuation a traded value imports the market's error into your answer, and the script says so.

Net taxes_due on unrealised gains. They came to $5,017m on Yahoo's stakes, over 10% of its equity value.

Reference data

resources/data/published_regressions.json and resources/data/multiple_distributions.json are tagged with an as_of date and a refresh note. Both are snapshots. Coefficients swing hard: the US price of an extra percentage point of expected growth ranged from 0.41 in January 2012 to 2.62 in January 2003. Distributions move far enough to reverse a verdict. Refresh from https://pages.stern.nyu.edu/~adamodar/ annually, and record which vintage a valuation used.

Two unit conventions live in the regression file. Most entries take decimals; two reproduce statistics-package output in percentage points. Read the units field first.

Handing off

NeedWhere it comes from
Cost of equity, WACC, market value of debtcost-of-capital-toolkit — wacc, mv-debt
Adjusted EBIT, EBITDA, FCFF, invested capital, ROE, ROICfinancial-statement-normalization — normalize, fcff
Intrinsic value to compare a pricing verdict againstdcf-valuation-engine — value
Value of employee options before a per-share multipleoption-valuation-toolkit
Divisional costs of capital for the zero-growth rulecost-of-capital-toolkit — wacc per division
The cross_holdings figure a sum of the parts bridges withcross-holdings in this skill
Cross-artifact consistency before you publishvaluation-consistency-checks — validate

A pricing verdict and a DCF that disagree is a useful result, not a problem. Run dcf-valuation-engine implied to see what assumption the market price already contains, then argue about that assumption rather than about the multiple.

Common failures

SymptomCause
Engine refuses the multipleNumerator and denominator belong to different claimholders, or the denominator is negative — both are real errors
Every firm in the sector looks cheapComparison against a mean rather than a median on a right-skewed distribution
Cheap on the multiple, expensive on the regressionThe low multiple was explained by weak fundamentals all along
Predicted multiple comes back negativeNegative intercept in the fitted equation; re-fit through the origin
Coefficient has the wrong signMulticollinearity among the predictors, not a finding
Verdict flips between vintagesCoefficients re-estimated on a different year; expected, and the reason to date every fit
Low EV/EBITDA that never re-ratesReinvestment needs justify the low multiple; check CapEx/EBITDA
PEG screen keeps surfacing the same namesPEG loads on risk and high growth; it does not neutralise growth
Half the sector has no multipleNegative earnings or book value; switch to a revenue or forward multiple
Multiple looks cheap against another marketDifferent interest rates, growth and country risk; compare within a market first
Sum of the parts refuses to runNo treatment declared for unallocated corporate overhead; allocate it or capitalise it
The parts beat the whole by a suspicious marginCorporate overhead dropped, or one sector multiple applied to every division
A division looks cheap on its sector multipleIt earns below its cost of capital, so the sector's growth does not belong to it
Cross holdings refusedBook minority interest with no sector price-to-book; book is not market value
Equity value too high after consolidationThe minority interest was never subtracted, so shareholders get a subsidiary they part own
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Not specified

Source path

skills/relative-valuation-toolkit

Default branch

main

Latest commit

4acc337

Tree SHA

4f0a83e