I've stood next to the machine that makes this company's product for eleven years. I'd never valued the company. The stock is down twenty percent this year, so I did it properly — Wall Street Prep's mechanics, Damodaran's discipline, and the one thing neither of them has: what the numbers look like from the floor.
Updated October 21. I went back and pulled IP's FY2025 10-K for one reason: pensions and leases are where a paper company keeps the debt that "net debt" leaves out. The pensions and the leases came back clean. A footnote didn't: a $487M tax bill from a 2007 timberland sale, due in 2027. Neither did the reserves for old mill sites and asbestos claims, $373M more. All in, the 10-K takes $1.81 a share off the DCF ($40.25 → $38.44) and moves the down-cycle odds the market is pricing from 48% to 44%. Every number below has been rerun from the model; the line-by-line is here.
price$32Oct 15, 2026 · 52-wk $29–50
YTD−20%−23% over 12 months
yield5.8%$1.85/share · ~$980M/yr
net debt$8.5BEV ≈ $25.4B
debt-like, 10-K$957Mafter tax · −$1.81/share
2026 EBITDA guide$3.2–3.4Bwas $3.5–3.7B in January
Damodaran's rule is that every valuation is a story with numbers attached, and the story comes first, because the numbers are only as good as the story they're consistent with. Here's the story as I see it from the plant, before I open a spreadsheet.
the bull case: capacity out, price in
IP closed Savannah (1.0 million tons a year) and Riceboro (430k) in September 2025, after Red River and Orange. The industry took out 3.9 million tons in 2025 — nearly 10% of North American containerboard capacity.
Three price increases in 2026. The index has recognized about $170 a ton on linerboard. The CFO's own sensitivity: every $10 a ton is $90M of EBITDA a year.
80/20 found ~$510M of run-rate cost in 2025 and expects the same in 2026. Thirty-one EMEA closures, 2,800 positions.
They bought the NORPAC mill in Washington for $360M because, in the CEO's words, they were "significantly short paper on the West Coast and shipping it there uneconomically." Anyone who's watched a truck of linerboard cross three states knows what that costs.
the bear case: expensive assets, soft demand
Demand is flat. The trade forecast for 2026 containerboard went from +2% to −2% after February. The bank analysts are "mostly worried about demand."
EMEA (the DS Smith business) lost $80M in Q2 because paper prices rose faster than box prices. That lag is real; I've lived it from the other side.
The 10% capacity cut was cost economics — low performers out — not a demand squeeze. Those mills aren't coming back, which supports price, but the tightness is thinner than the headline.
Paper is capital-hungry. D&A is 8% of sales; capex is running $2.1B a year. The dividend costs $980M and 2025 free cash flow was negative. The 5.8% yield is the market telling you it doesn't believe the dividend is safe.
And one thing that isn't in either column because it happened this week: IP announced in January it would spin off EMEA, and on October 15 it said it had unsolicited bids and was running a sale process in parallel. The DS Smith capital markets day is November 3. So the question "what is the sum of the parts worth" is no longer academic.
narrative to numbers
A DCF is a machine that turns operating assumptions into a price. The machine is standard; the assumptions are everything. So here is 2027 built as a bridge from 2026 guidance, with each piece tied to the story, and with my own judgments marked as judgments.
The 2027 bridge for the North American business, $ millions. Grey is company guidance; orange are my judgments (the model's yellow cells); green is what falls out — and the deliberate step down to mid-cycle.
The judgments, stated plainly so you can disagree with them:
Half the price sticks. Of the $170 a ton the index has recognized, I assume 50% holds at the box level through 2027. Index-linked contracts lag by months; the third round hasn't reached the open market yet; a soft year gives some of it back. Half, net of the $175M already in 2026 guidance, is about $590M of 2027 EBITDA.
Volume up 1%. The market is flat-to-down; IP says it's outperforming by three or four points. I split the difference and assume a 30% contribution margin on the increment, which is what a box plant running through existing mills earns on the next ton.
$300M of cost-out against $250M of inflation. OCC is back to $80–100 a ton from the 2025 lows, freight is up, and 80/20 keeps finding money. Net of each other, it's roughly a wash — which is what a transformation looks like from the P&L two years in.
Then it comes back down. This is the Damodaran part people skip. 2027 is a price-increase year, and a price-increase year is a peak. A cyclical gets valued at mid-cycle, not at the peak. So 2028 onward I use a mid-cycle NA margin of 17% — between Smurfit Westrock's 15% and Packaging Corp's 21% — and the model deliberately shows EBITDA stepping down from 2027 to 2028. If that looks wrong to you, that's the assumption to argue with.
where the debt hides
Net debt is what IP owes its lenders less its cash: $8.5B at the end of June. A paper company owes more than that. It owes pensions to the people who ran its mills, health care it promised them, rent on its railcars and warehouses, the cleanup of mills it no longer runs, and taxes it put off. None of it is in net debt, and all of it gets paid before a shareholder sees a dollar. That's why I went back and pulled the FY2025 10-K. Line by line:
item
what the 10-K says (Dec 31, 2025)
how it's counted
per share
Pension plans in surplus
$486M. The U.S. qualified plan holds $8,482M against $8,116M owed; the UK plan is $112M over.
not counted: a surplus is hard to get back out (counting it adds +$0.70)
$0.00
Pension plans in deficit
$342M, including $218M of U.S. nonqualified plans with no assets behind them
after tax
−$0.49
Retiree medical and postemployment
$133M
after tax
−$0.19
Environmental reserves
$270M, the "estimated probable liability" for old mill sites
after tax
−$0.39
Asbestos claims
$103M, net of insurance
after tax
−$0.15
Brazil tax guarantee to Sylvamo
$48M booked, of a potential $274M
the booked part, in full
−$0.09
Operating leases
$707M
not added: the rent is already inside EBITDA
$0.00
Finance leases
$71M
already inside the debt
$0.00
Tax on the Temple-Inland timber notes
$487M, "settled with the maturity of the notes in 2027"
in full
−$0.92
The timber notes, net of their loans
$2,349M of notes against $2,127M of nonrecourse loans
what's left over is IP's
+$0.42
What the 10-K takes off the value
$957M after tax
−$1.81
After tax at the model's 24% rate, per share on 529.5M shares. Pension and lease balances are at December 31, 2025, the last time IP measured them; debt and cash are from June 30, 2026. Every line is an input in the workbook with its quote from the filing, and two of the twenty-one checks tie them back to the 10-K's own totals.
The pensions came back clean. The U.S. plan holds $8,482M against $8,116M owed, and the 10-K says IP's funded plans are "fully funded on a projected benefit obligation basis." I count the plans in deficit and give no credit for the plans in surplus. The leases came back clean in a different way: under U.S. GAAP the cost of an operating lease is an operating expense, so the rent is already inside the EBITDA the whole model runs on. Adding the $707M liability as debt too would count the same rent twice.
The reserves are what a hundred-year-old paper company carries for a long time: $270M for cleaning up mill sites it no longer runs (Cass Lake, the Kalamazoo River, San Jacinto, Versailles Pond) and $103M for asbestos claims after insurance. They're booked, the cash goes out over years, and adjusted EBITDA leaves them out, so they count: −$0.39 and −$0.15 a share.
The footnote is the part I didn't expect. In 2007 Temple-Inland sold its timberland for notes instead of cash and put off the tax; IP inherited both when it bought Temple-Inland in 2012. The 10-K says it plainly: "$487M deferred tax liability related to the Temple-Inland timber monetization, which will be settled with the maturity of the notes in 2027." That year the notes ($2,349M) pay off the loans taken against them ($2,127M) and leave $222M, and the tax takes $487M. Net, −$0.50 a share, due the same year $2.6B of IP's regular debt matures.
Two things I didn't count, so you can: about $344M of unrecognized tax benefits, which IP can't say when or whether it will pay (−$0.65 a share if all of it were), and the rest of a Brazilian tax assessment IP agreed to share with Sylvamo, the printing-papers business it spun off. IP puts its potential share at about $274M, has booked $48M of it (counted above), and a first-round court ruling went its way; the other $226M would be −$0.43 a share.
All in, the 10-K takes $1.81 a share off the DCF, $40.25 to $38.44, and moves the down-cycle odds the market is pricing from 48% to 44%. My edge narrows from 18 points to 14 points. What the bet is on doesn't change: the discount isn't hidden debt. It's the cycle.
what the machine says
Three independent routes, same inputs, and they land in the same place, which is the first thing worth knowing.
Two honesty checks the model prints on purpose. The terminal value is 82% of the enterprise value — normal for a mature company, and it means the answer is the terminal assumptions, which is why the next section exists. And the Gordon-growth terminal value implies an exit multiple of 8.2×, right between Smurfit Westrock and Packaging Corp, so the growth-and-discount-rate pair isn't quietly telling a story the comps don't.
The two assumptions that decide the number, with the market price for scale:
NA mid-cycle margin ↓ · exit multiple →
6.5×
7.5×
8.5×
9.5×
11×
12×
13%
$22
$25
$27
$29
$33
$35
15% (Smurfit Westrock)
$28
$31
$34
$36
$40
$43
17% (this model)
$34
$37
$40
$43
$47
$50
19%
$40
$44
$47
$50
$54
$57
21% (Packaging Corp)
$47
$50
$53
$57
$62
$65
Grey cells are at or below the $32 market price. Read it as a map of what you'd have to believe.
At $32, the market is paying a Smurfit Westrock multiple for the North American business and giving DS Smith almost nothing.
That's the thing the model made me see. Back out what the price implies and the stock trades at 7.7× 2026 guidance and about 6× the 2027 bridge. The market isn't disagreeing with the arithmetic; it's disagreeing with the story. It assumes 80/20 doesn't earn IP a multiple above SW's, that most of the $170 doesn't stick, and that the dividend gets cut. Those are three versions of one belief: that the cycle turns down before the transformation shows up.
the bet, priced
A point estimate of $38 is not a bet; it's a sentence. A bet needs a distribution. So I ran the same chain twenty thousand times with the decisive assumptions drawn from ranges instead of set to values — how much price sticks, the mid-cycle margin, the exit multiple, the discount rate, volume, capex intensity — plus a 15% noise term on the terminal value for the possibility that the model's structure is simply wrong.
The first version said 90% of outcomes were above $32. That was a lie, and a common one: I'd drawn the assumptions independently, and in a downturn they move together — price doesn't stick, demand falls, margins compress and the multiple shrinks all at once. So the second version draws the cycle first, with a 30% chance of a down-cycle over the horizon, and draws everything else conditional on it.
Twenty thousand values per share. The dashed line is the $32 price; the tails are what the point estimate hides.
P(worth more than $32)62%given a down-cycle: 3% · given not: 87%
median / mean$37 / $3610th pct $16 · 90th pct $54
P(worth less than $16)11%P(worth more than $64): 2%
Then the question that matters: what does the probability of a down-cycle have to be for the bet to be fair?
The whole bet is one number. P(value > $32) as the probability of a containerboard down-cycle over the next three years goes from 10% to 70%.
At $32 the market is pricing a coin flip on a down-cycle. If you think it's 30%, the shares are worth about $37 and you have an edge. If you think it's 44%, you're being paid nothing.
Everything else — the DCF, the comps, the sum of the parts — collapses into that one judgment. Which is the useful thing about doing this properly: it tells you what you're actually betting on, and it isn't the dividend or the spin-off or the price increase. It's the cycle. And the cycle is the thing you can watch: box shipments, the index in October and November, OCC, and what the DS Smith buyers are willing to pay on November 3.
price your own bet
Everything above is my guess. Here is the same machine with the guesses exposed. Set what you believe — how much of the $170 sticks, the mid-cycle margin, what a buyer pays in 2030, the discount rate, and the odds of a down-cycle — and the page prices the bet the way the spreadsheet and the Monte Carlo do, live.
your value / share—
vs $32—
P(value > $32)—
10th / 90th pct—
2027 EBITDA—
On sizing: Kelly on this distribution says a half-Kelly position of about 20% of a portfolio, which is absurd, and the absurdity is the lesson. Kelly assumes you know the distribution. The distribution is the thing you're least sure of — I assigned that 30% myself. Treat Kelly as an upper bound, then divide by the number of things you could be wrong about. For one stock in one cyclical industry, that's a small position or none.
would I
Here's where I've landed, as a person and not as advice. The honest version of this stock is a bet on the containerboard cycle, with a 5.8% yield that may or may not survive, a separation that may or may not unlock value, and a management that has been doing the hard, expensive, unpopular work of closing the mills everyone knew should close. If I want the quality, I'd own Packaging Corp at 12× and sleep. If I want the same multiple with less drama, Smurfit Westrock is right there at 7×. IP is the one where my information is worth something — because the cycle call is the whole bet, and the cycle is what I read every week in box plants.
That's an argument for a small position and a watch list, not for conviction. The model is the thing I'm confident in. The 30% is the thing I'm not.
What to watch, in order of how fast it would change my mind (kept current in the thesis log): Fibre Box Association shipments for Q4 (demand); the Fastmarkets index in October and November (does the third round hold); the Q3 call's price realization number against the $175M that was in guidance; the November 3 DS Smith day and any sale price (the SOTP); OCC above $120 (the cost side). Each one is a row in the model.
what drives it
Before sizing anything, which assumption actually moves the answer? Rank correlation between each simulated input and the value per share, across the twenty thousand runs. The cycle dominates: the mid-cycle margin and the exit multiple top the list, and both are drawn from the cycle regime, which ranks third on its own. The pricing guess matters, but less than the cycle it sits inside.
Spearman rank correlation of each input with value per share. Negative means a higher input lowers the value.
my bet, as instruments
The model values the business; a position pays on the price, and the two can disagree for a long time. So this adds one more assumption — over about a year the price closes 60% of the gap toward the simulated value, with 12% of market noise on top — and maps that onto the four ways I could actually express the view, per 100 shares. Option premiums are placeholders from a reasonable chain; check the live one before doing anything.
Payoff at the horizon for each instrument (lines) over the distribution of the horizon price (bars). The dashed line is today's $32.
instrument (per 100 shares)
capital
E[P&L]
P(profit)
10th
90th
worst
on capital
100 shares at $32
$3,200
$414
65%
(932)
$1,705
(2,109)
+13%
1 call, Jan-2028 $35 (~$3.50)
$350
$26
34%
(350)
$870
(350)
+7%
1 cash-secured put, $28 6-mo (~$1.75)
$2,800
$32
77%
(453)
$175
(1,627)
+1%
1 call spread 35/45 (~$2.25)
$225
$71
39%
(225)
$775
(225)
+32%
The table is the "win rate is a proxy" lesson on my own bet. The cash-secured put wins 77% of the time and has the lowest expected return on capital, because the 23% is where it loses $1,600 against $175 of premium. The call spread has the best return on capital and loses money 61% of the time; in 52% of runs it loses the whole $225. Shares are the boring middle: two-thirds odds, +13% expected, and they pay the dividend while I wait, if it survives. None of this is advice; it's the distribution, priced four ways.
show the code (bet.py, as run)
"""My bet: the value distribution mapped onto the instruments I could actually buy.
The Monte Carlo gives a distribution of what the shares are WORTH. A position pays on
what they TRADE at, and the market can disagree with value for a long time. So the one
extra assumption here is convergence: over the horizon the price moves a fraction c of
the way from $32 toward the simulated value (c = 1 is full convergence, 0 is none).
Option premiums are ILLUSTRATIVE placeholders — check the live chain before trading.
Run: python3 bet.py (writes bet.json for the post's chart)
"""
import io, contextlib, json
import numpy as np
# re-run the simulation to get the value draws (same seed as montecarlo.py)
src = open("montecarlo.py").read().replace('json.dump(out, open("mc.json", "w"), indent=1)', '')
ns = {}
with contextlib.redirect_stdout(io.StringIO()):
exec(src, ns)
value, down, price = ns["value"], ns["down"], ns["price"]
rng = np.random.default_rng(7)
# ---- horizon price: partial convergence toward value, plus noise the model can't see
c = 0.6 # 60% of the gap closes over ~12–15 months
price_T = price + c * (value - price)
MKT_NOISE = 0.12 # lognormal market noise the model can't see
price_T = price_T * np.exp(rng.normal(0, MKT_NOISE, len(value)))
price_T = np.maximum(price_T, 3)
# ---- dividend over the horizon: $1.85 held in the base regime, halved in a down-cycle (cut)
div = np.where(down, 0.925, 1.85)
# ---- instruments (per 100 shares / one contract). Premiums are placeholders.
CALL_K, CALL_PREM = 35.0, 3.50 # Jan-2028 $35 call, illustrative
PUT_K, PUT_PREM = 28.0, 1.75 # 6-month $28 cash-secured put, illustrative
SPR_K1, SPR_K2, SPR_NET = 35.0, 45.0, 2.25 # 35/45 call spread, illustrative net debit
instruments = {
"100 shares at $32": dict(pnl=100 * (price_T - price) + 100 * div, capital=100 * price),
"1 call, Jan-2028 $35 (~$3.50)": dict(pnl=100 * (np.maximum(0, price_T - CALL_K) - CALL_PREM), capital=100 * CALL_PREM),
"1 cash-secured put, $28 6-mo (~$1.75)": dict(pnl=100 * (PUT_PREM - np.maximum(0, PUT_K - price_T)), capital=100 * PUT_K),
"1 call spread 35/45 (~$2.25)": dict(pnl=100 * (np.minimum(SPR_K2 - SPR_K1, np.maximum(0, price_T - SPR_K1)) - SPR_NET), capital=100 * SPR_NET),
}
rows = []
for name, d in instruments.items():
p = d["pnl"]
rows.append(dict(name=name, capital=float(d["capital"]), mean=float(p.mean()), p_profit=float((p > 0).mean()),
p10=float(np.percentile(p, 10)), p50=float(np.percentile(p, 50)), p90=float(np.percentile(p, 90)),
worst=float(p.min()), roc=float(p.mean() / d["capital"]),
mean_down=float(p[down].mean()), mean_base=float(p[~down].mean())))
print(f"{name:40s} capital ${d['capital']:>6,.0f} E[P&L] ${p.mean():>7,.0f} P(profit) {(p>0).mean():5.1%} 10th ${np.percentile(p,10):>7,.0f} 90th ${np.percentile(p,90):>7,.0f} worst ${p.min():>7,.0f} on capital {p.mean()/d['capital']:+.1%}")
# payoff curves for the chart (per 100 shares, before dividends)
grid = np.arange(10, 70.5, 0.5)
curves = {
"shares": (100 * (grid - price)).tolist(),
"call": (100 * (np.maximum(0, grid - CALL_K) - CALL_PREM)).tolist(),
"put": (100 * (PUT_PREM - np.maximum(0, PUT_K - grid))).tolist(),
"spread": (100 * (np.minimum(SPR_K2 - SPR_K1, np.maximum(0, grid - SPR_K1)) - SPR_NET)).tolist(),
}
hist, edges = np.histogram(price_T, bins=np.arange(0, 100, 2.5))
json.dump(dict(convergence=c, noise=MKT_NOISE, spread_wipeout=float((price_T <= SPR_K1).mean()), price=price, grid=grid.tolist(), curves=curves, rows=rows,
priceT=dict(edges=edges.tolist(), counts=hist.tolist(), p10=float(np.percentile(price_T, 10)), p50=float(np.percentile(price_T, 50)), p90=float(np.percentile(price_T, 90))),
premiums=dict(call=CALL_PREM, put=PUT_PREM, spread=SPR_NET)), open("bet.json", "w"))
print(f"\nhorizon price (c={c}): 10th ${np.percentile(price_T,10):.0f} median ${np.percentile(price_T,50):.0f} 90th ${np.percentile(price_T,90):.0f}")
does it tie out
A model that evaluates is not a model that's right. So the numbers are checked the way a reviewer would check them — every identity, every cross-check between methods, every claim against what the company itself said — and the checks run as a script that fails loudly. Twenty-one checks; all pass. The ones that matter most: the 2026 cash flow lands inside the company's own guide, the Gordon terminal value implies an exit multiple the comps support, the Python reproduces the spreadsheet to the dollar, the Monte Carlo's breakeven is where the arithmetic says it should be, and the 10-K's pension lines tie to the filing's own totals.
validation output
PASS market cap = price × shares: $16,944M vs $16,944M
PASS net debt = gross debt − cash: $8,491M
PASS 10-K pension lines tie out: surplus plans − deficit plans = the filing's net, and the U.S. plan's table = its stated surplus: $486M − $342M = $144M (10-K: $144M); U.S. qualified $8,482M − $8,116M = $366M
PASS debt-like items rebuild from the 10-K lines and come off the equity, not the WACC: $957M after tax = $957M from the lines; equity = EV − $8,491M − $957M
PASS 2026E EBITDA = guidance midpoint: $3,300M vs $3,300M guide midpoint
PASS 2026E levered FCF inside the company's Jan guide ($300–500M): $303M (guide given before the April cut; the low end is the honest reading)
PASS dividend cost ≈ $980M: $980M = $1.85 × 529.5M
PASS 2026E levered FCF does NOT cover the dividend: 0.31× coverage
PASS Gordon TV implies an exit multiple between SW and PKG: 8.2x (SW 7.2x, PKG 12.2x)
PASS Exit-multiple TV implies a sane perpetual growth (0–4%): 1.89%
PASS WACC > terminal growth: 7.36% > 2.0%
PASS terminal value share of EV disclosed and in the mature-company band (60–90%): 82%
PASS DCF sheet ≈ Scenarios mid-cycle column (same chain, two layouts): $38.44 vs $38.69
PASS Sensitivity grid (17% × 8.5x) ≈ DCF within the interpolation error: $40.11 vs $38.44
PASS Bear < Mid < Bull: $11 < $39 < $69
PASS 25% of $170/ton moves 2027E EBITDA by 42.5 × $9M: $382.5M vs $382.5M (CFO: $10/ton ≈ $90M)
PASS Python chain reproduces Scenarios!C21 (2027E EBITDA): $4,136M vs $4,136M
PASS P(value > price) falls monotonically as P(down-cycle) rises: 79% → 71% → 62% → 53% → 45% → 37% → 28%
PASS breakeven P(down-cycle) ≈ 50% (market price = expected value): mean at p=0.5: $30.9
PASS MC mean sits between the Bear and Bull point estimates: $35.9
PASS regime split is real: P(>price | down) ≪ P(>price | base): 3% vs 87%
0 failure(s)
show the checks (validate.py)
"""Validation: does the model tie out? Run: python3 validate.py
Each check is a thing a reviewer would ask. A check prints PASS/FAIL with the numbers,
and the script exits non-zero if anything fails, so it can gate a republish.
"""
import json, sys, subprocess
from openpyxl import load_workbook
wb = load_workbook("IP_valuation_model.xlsx", data_only=True)
inp = {r[0].value: r[1].value for r in wb["Inputs"].iter_rows(min_row=5) if r[0].value and r[1].value is not None}
M, D, S, V = wb["Model"], wb["DCF"], wb["Scenarios"], wb["Sensitivity"]
DCF = {} # DCF sheet by label, so a new row can't silently shift what a check reads
for row in D.iter_rows(min_row=1):
if isinstance(row[0].value, str) and row[1].value is not None:
DCF.setdefault(row[0].value.strip(), row[1].value)
mc = json.load(open("mc.json")); pdown = json.load(open("mc_pdown.json"))
fails = 0
def check(name, ok, detail):
global fails
print(f"{'PASS' if ok else 'FAIL'} {name}: {detail}")
fails += 0 if ok else 1
# --- identities and tie-outs to what the company said
price, shares = inp["Share price ($)"], inp["Diluted shares (millions)"]
mcap, netdebt, debtlike = DCF["Market capitalization"], DCF["Net debt (gross debt − cash)"], DCF["Debt-like items from the 10-K, after tax (Inputs)"]
check("market cap = price × shares", abs(mcap - price * shares) < 1, f"${mcap:,.0f}M vs ${price*shares:,.0f}M")
check("net debt = gross debt − cash", abs(netdebt - (inp["Gross debt"] - inp["Cash and temporary investments"])) < 1, f"${netdebt:,.0f}M")
L = lambda prefix: next(v for k, v in inp.items() if k.startswith(prefix))
surplus, deficit = L("Pension plans in surplus"), L("Pension plans in deficit")
check("10-K pension lines tie out: surplus plans − deficit plans = the filing's net, and the U.S. plan's table = its stated surplus",
abs((surplus - deficit) - L("Pensions, all plans")) < 1 and abs(L("Pension, U.S. qualified plan: assets") - L("Pension, U.S. qualified plan: obligation") - 366) < 1,
f"${surplus:,.0f}M − ${deficit:,.0f}M = ${surplus - deficit:,.0f}M (10-K: ${L('Pensions, all plans'):,.0f}M); U.S. qualified ${L('Pension, U.S. qualified plan: assets'):,.0f}M − ${L('Pension, U.S. qualified plan: obligation'):,.0f}M = $366M")
dl = ((deficit + L("Retiree medical") + L("Environmental remediation") + L("Asbestos claims")) * (1 - inp["Cash tax rate"])
- surplus * (1 - inp["Cash tax rate"]) * L("Count the pension surplus") + L("Brazil tax guarantee")
+ L("Deferred tax on the Temple-Inland") - (L("Timber notes held") - L("Nonrecourse loans")))
check("debt-like items rebuild from the 10-K lines and come off the equity, not the WACC", abs(dl - debtlike) < 1 and abs(
DCF["Equity value = EV − net debt − debt-like"] - (DCF["Enterprise value"] - netdebt - debtlike)) < 1,
f"${debtlike:,.0f}M after tax = ${dl:,.0f}M from the lines; equity = EV − ${netdebt:,.0f}M − ${debtlike:,.0f}M")
e26 = M["C14"].value
check("2026E EBITDA = guidance midpoint", abs(e26 - (inp["2026 adj. EBITDA guidance — low"] + inp["2026 adj. EBITDA guidance — high"]) / 2) < 1, f"${e26:,.0f}M vs $3,300M guide midpoint")
lfcf26 = M["C30"].value
check("2026E levered FCF inside the company's Jan guide ($300–500M)", 250 <= lfcf26 <= 550, f"${lfcf26:,.0f}M (guide given before the April cut; the low end is the honest reading)")
div = -M["C31"].value
check("dividend cost ≈ $980M", abs(div - 980) < 20, f"${div:,.0f}M = $1.85 × 529.5M")
check("2026E levered FCF does NOT cover the dividend", lfcf26 < div, f"{lfcf26/div:.2f}× coverage")
# --- terminal value cross-checks (each method implies the other's parameter)
tv_mult = DCF["implied exit multiple of the Gordon TV"]; tv_g = DCF["implied perpetual growth of the exit-multiple TV"]
check("Gordon TV implies an exit multiple between SW and PKG", inp["Smurfit Westrock EV/EBITDA (LTM)"] <= tv_mult <= inp["PKG EV/EBITDA (LTM)"], f"{tv_mult:.1f}x (SW {inp['Smurfit Westrock EV/EBITDA (LTM)']}x, PKG {inp['PKG EV/EBITDA (LTM)']}x)")
check("Exit-multiple TV implies a sane perpetual growth (0–4%)", 0 <= tv_g <= 0.04, f"{tv_g*100:.2f}%")
check("WACC > terminal growth", DCF["WACC"] > inp["Terminal growth rate"], f"{DCF['WACC']*100:.2f}% > {inp['Terminal growth rate']*100:.1f}%")
tv_share = DCF["terminal value as % of EV"]
check("terminal value share of EV disclosed and in the mature-company band (60–90%)", 0.6 <= tv_share <= 0.9, f"{tv_share*100:.0f}%")
# --- three routes agree
dcf_ps, scen_mid_ps, sens_cell = DCF["Value per share ($)"], S["C30"].value, V["D8"].value # sensitivity row 17% × col 8.5x
check("DCF sheet ≈ Scenarios mid-cycle column (same chain, two layouts)", abs(dcf_ps - scen_mid_ps) < 1.0, f"${dcf_ps:.2f} vs ${scen_mid_ps:.2f}")
check("Sensitivity grid (17% × 8.5x) ≈ DCF within the interpolation error", abs(sens_cell - dcf_ps) < 3.0, f"${sens_cell:.2f} vs ${dcf_ps:.2f}")
check("Bear < Mid < Bull", S["B30"].value < S["C30"].value < S["D30"].value, f"${S['B30'].value:.0f} < ${S['C30'].value:.0f} < ${S['D30'].value:.0f}")
# --- the CFO's sensitivity is reproduced
e0, e25 = V["C16"].value, V["C17"].value # 2027E EBITDA at 0% and 25% stick
check("25% of $170/ton moves 2027E EBITDA by 42.5 × $9M", abs((e25 - e0) - 42.5 * 9) < 1, f"${e25-e0:,.1f}M vs ${42.5*9:,.1f}M (CFO: $10/ton ≈ $90M)")
# --- Python reproduces the spreadsheet chain (the Monte Carlo's point estimate equals the mid column)
import importlib.util, io, contextlib
src = open("montecarlo.py").read().replace('json.dump(out, open("mc.json", "w"), indent=1)', '')
ns = {}
with contextlib.redirect_stdout(io.StringIO()):
exec(src, ns)
import numpy as np
# deterministic run of the same chain at the mid inputs
a = dict(stick=0.5, margin=0.17, mult=8.0, wacc=DCF["WACC"], vol=0.01, emea_m=0.11, emea_d=150.0, capex=0.065)
pd_ = a["stick"] * 170 * 9 - 175
rev_na_27 = ns["rev_na_26"] * (1 + a["vol"]) + pd_; rev_emea_27 = ns["rev_emea_26"] * 1.025
na27 = 2425 + pd_ + ns["rev_na_26"] * a["vol"] * 0.30 + (300 - 250) * ns["rev_na_26"] / ns["rev_26"]
emea27 = 950 + a["emea_d"] + (300 - 250) * ns["rev_emea_26"] / ns["rev_26"]
check("Python chain reproduces Scenarios!C21 (2027E EBITDA)", abs((na27 + emea27 - 75) - S["C21"].value) < 1, f"${na27+emea27-75:,.0f}M vs ${S['C21'].value:,.0f}M")
# --- Monte Carlo sanity
check("P(value > price) falls monotonically as P(down-cycle) rises", all(pdown[i]["p_above"] > pdown[i+1]["p_above"] for i in range(len(pdown)-1)), " → ".join(f"{r['p_above']:.0%}" for r in pdown))
check("breakeven P(down-cycle) ≈ 50% (market price = expected value)", any(abs(r["mean"] - 32) < 1.5 and abs(r["p_down"] - 0.5) < 1e-9 for r in pdown), f"mean at p=0.5: ${[r['mean'] for r in pdown if abs(r['p_down']-0.5)<1e-9][0]:.1f}")
check("MC mean sits between the Bear and Bull point estimates", S["B30"].value < mc["mean"] < S["D30"].value, f"${mc['mean']:.1f}")
check("regime split is real: P(>price | down) ≪ P(>price | base)", mc["p_above_price_given_down"] < 0.2 and mc["p_above_price_given_base"] > 0.8, f"{mc['p_above_price_given_down']:.0%} vs {mc['p_above_price_given_base']:.0%}")
print(f"\n{fails} failure(s)")
sys.exit(1 if fails else 0)
What a green run proves and what it doesn't: the arithmetic is consistent and tied to its sources. It cannot prove the 30%, the 17%, or the 8×. Those are the judgment, and the log is where they get tested against time.
the workbook, rendered
The same spreadsheet, so you don't have to open it. Blue is a hardcoded input, yellow is a judgment of mine, everything else is a formula. The provenance column is the point: every number says where it came from.
Inputs — every assumption with its source
assumption
value
provenance
source / note
Market data (as of Oct 15–17, 2026)
Item
Value
Provenance
Source / note
Share price ($)
$32.00
MARKET
Benzinga quote page, last updated Oct 15, 2026 (header shows $31.90; FAQ shows $32.12).
Diluted shares (millions)
529.5
REPORTED
Q2 2026 earnings release: diluted average shares 529.5M.
Dividend per share ($/yr)
$1.85
MARKET
Benzinga: 5.80% yield at $31.90 → $1.85/share; policy maintained through 2026 per Q4 2025 call.
What behaves like debt but isn't in net debt (FY2025 10-K, filed Feb 2026; balances at Dec 31, 2025)
Pension, U.S. qualified plan: assets
$8,482
REPORTED
10-K MD&A: 'U.S. qualified pension $ 8,116 $ 8,482' (benefit obligation, fair value of plan assets).
Pension, U.S. qualified plan: obligation (PBO)
$8,116
REPORTED
Same table. The plan is overfunded: 'a surplus of $366 million'.
Pension, UK qualified plan (DS Smith): surplus
$112
REPORTED
10-K MD&A: 'an overfunded UK qualified pension with a surplus of $112 million.'
Pension, U.S. nonqualified plans: obligation, no assets
$218
REPORTED
10-K MD&A: 'U.S. nonqualified pension 218 —'. Paid as benefits come due ($49M in 2025).
Pensions, all plans: assets above obligations (the filing's net)
$144
REPORTED
10-K MD&A: 'the projected benefit obligation ... was approximately $144 million lower than the fair value of plan assets.' Note 19: U.S. plans +$148M, non-U.S. −$4M.
Pension plans in surplus, total (not counted by default)
$486
REPORTED
10-K Note 19, noncurrent assets: U.S. $367M + non-U.S. $119M = $486M (balance sheet: Overfunded Pension Plan Assets $486M).
Pension plans in deficit, total (counted, after tax)
$342
REPORTED
10-K Note 19, liabilities: U.S. $21M current + $198M noncurrent, non-U.S. $5M current + $118M noncurrent = $342M (balance sheet: Underfunded Pension Benefit Obligations $316M long-term).
Retiree medical and postemployment benefits (counted, after tax)
$133
REPORTED
10-K balance sheet: Postretirement and Postemployment Benefit Obligation $133M. Note 19 calls the other postretirement plans 'not considered material' and gives no table.
Operating lease liabilities (not counted: the lease cost is already inside EBITDA)
$707
REPORTED
10-K Note 10: operating lease liabilities $221M current (other current liabilities) + $486M noncurrent (long-term lease obligations); 2025 operating lease cost $258M. Under U.S. GAAP operating lease cost is an operating expense, so EBITDA is after rent; adding the liability too would count it twice.
10-K Note 10: finance lease liabilities $17M in notes payable and current maturities of long-term debt + $54M in long-term debt.
Environmental remediation reserves, old mill sites (counted, after tax)
$270
REPORTED
10-K Note 14: 'International Paper's estimated probable liability for these environmental matters, totaled approximately $270 million' at Dec 31, 2025 (Cass Lake, Kalamazoo River, San Jacinto, Versailles Pond among them). Remediation is a special item, outside adjusted EBITDA.
Asbestos claims, net of insurance (counted, after tax)
$103
REPORTED
10-K Note 14: 'total recorded liability with respect to pending and future asbestos-related claims was $103 million ... net of insurance recoveries'.
Brazil tax guarantee to Sylvamo, already booked (counted in full)
$48
REPORTED
10-K Note 14: 'The initial fair value estimate and recorded liability as of December 31, 2021 was $48 million and remains this amount at December 31, 2025.'
Deferred tax on the Temple-Inland timber notes, due 2027 (counted)
$487
REPORTED
10-K, contractual obligations: '$487 million deferred tax liability related to the Temple-Inland timber monetization, which will be settled with the maturity of the notes in 2027.'
Timber notes held by the special purpose entities (counted, net of the loans)
$2,349
REPORTED
10-K balance sheet: Long-Term Financial Assets of Variable Interest Entities $2,349M. Note 15: $2.4B face, due 2027, backed by letters of credit from three banks.
Nonrecourse loans against those notes
$2,127
REPORTED
10-K balance sheet: Long-Term Nonrecourse Financial Liabilities of Variable Interest Entities $2,127M. Not in gross debt above; the notes repay them in 2027.
Regular debt maturing in 2027 (context, not added)
Unrecognized tax benefits (not counted: timing and amount uncertain)
$344
REPORTED
10-K contractual obligations footnote: 'approximately $344 million of unrecognized tax benefits'.
Brazil tax matter via the Sylvamo agreement, IP's potential share (counted only the $48M booked)
$274
REPORTED
10-K Risk Factors: 'our potential liability for such assessments would currently be approximately $274 million' (assessments total about $394M, 2007–2015 tax years).
Count the pension surplus too? (1 = yes, 0 = no)
0
JUDGMENT
Default no: a surplus can't be spent without an excise tax or years of contribution holidays. Set 1 to net it against the deficits.
Debt-like items, after tax (from the 10-K lines above)
$957
DERIVED
Pension deficits, retiree medical, environmental and asbestos reserves, after tax; the booked Brazil guarantee; the 2027 timber tax less the equity in the timber structure (notes less loans); minus the pension surplus only if counted. Operating leases excluded.
Latest reported and guided financials
2025 pro forma net sales — Packaging Solutions NA
$15,000
REPORTED
Q4 2025 call: 'over $15B' pro forma for the North American business (rounded).
2025 pro forma net sales — Packaging Solutions EMEA
$8,500
REPORTED
Q4 2025 call: about $8.5B pro forma for EMEA (DS Smith full year).
Q1 2026 call: IP expects pricing to add ~$175M to NA in 2026, mostly in H2.
Share of the YTD index gain that STICKS through 2027 (floor judgment)
50.0%
JUDGMENT
Q1 for Jason: of the $170/ton announced, what holds at the box level in 12 months? Green Markets found no evidence the third round reached the open market (Sept). Default 50%.
NA box volume growth 2027
1.0%
JUDGMENT
Market flat in 2026 (TTOBMA cut containerboard to −2%); IP claims +3–4 pts vs market. Default +1%.
Contribution margin on incremental NA volume
30.0%
JUDGMENT
Incremental box volume through existing mills/plants; typical converter contribution 25–35%.
Incremental 80/20 cost-out 2027 (NA + EMEA)
$300
JUDGMENT
~$510M run-rate benefits in 2025, 'similar' expected in 2026 (Q4 2025 call); EMEA >$200M run-rate from 31 closures. Default $300M for 2027, fading.
Cost inflation 2027 (OCC, energy, labor, freight)
$250
JUDGMENT
OCC back to ~$80–100/ton from 2025 lows (5-yr avg $140); freight up on Middle East; EMEA inflation ~$100M in 2026 guide. Default $250M.
Capex as % of revenue 2028+ (maintenance + modest growth)
6.5%
JUDGMENT
Paper is capex-heavy: D&A ≈ 8% of sales today. 6.5% assumes the transformation capex rolls off. Your 'assets in the ground are expensive to run' point lives here.
D&A as % of revenue 2027+
8.0%
JUDGMENT
2026: $1,965M on ~$24B = 8.2%.
Net working capital investment, % of incremental revenue
10.0%
JUDGMENT
Packaging converters typically carry 8–12% of sales in NWC.
Cash tax rate
24.0%
JUDGMENT
US 21% federal + state, blended with EMEA. Check against the 10-K effective rate.
Discount rate (WACC build)
Risk-free rate (10-yr US Treasury)
4.0%
MARKET
Placeholder near the Oct 2026 10-yr yield — refresh before publishing.
Equity beta
1.1
JUDGMENT
IP 5-yr regression beta has run ~1.0–1.2; Damodaran paper/forest products industry beta ~1.0. Default 1.10.
Equity risk premium
4.5%
JUDGMENT
Damodaran implied ERP has run 4–5% in 2025–26.
Pre-tax cost of debt
5.5%
MARKET
IP senior unsecured notes have traded ~5–6% yield (BBB-range credit). Refresh.
Terminal growth rate
2.0%
JUDGMENT
At or below long-run nominal GDP for a mature, capex-heavy business.
Exit EV/EBITDA multiple (cross-check)
8.0×
JUDGMENT
IP's own 10-yr average ~8–9x; SW 7.2x and PKG 12.2x today.
Peers (Multiples.vc, Oct 15–17, 2026)
PKG EV/EBITDA (LTM)
12.2×
MARKET
Multiples.vc Oct 17, 2026; EV $25B, LTM revenue $9.8B, EBITDA margin 21%.
PKG EBITDA margin (LTM)
21.0%
MARKET
Multiples.vc.
Smurfit Westrock EV/EBITDA (LTM)
7.2×
MARKET
Multiples.vc Oct 15, 2026; EV $35B, LTM revenue $32B, EBITDA margin 15%.
Smurfit Westrock EBITDA margin (LTM)
15.0%
MARKET
Multiples.vc.
Scenario switch
Active scenario (1 = Bear, 2 = Mid-cycle, 3 = Bull) — drives the Scenarios sheet overrides
2
JUDGMENT
The Model sheet uses the Inputs above directly; the Scenarios sheet runs all three side by side.
Model — 2025A to 2030E, the bridge and the cash flow
Fiscal year
2025A
2026E
2027E
2028E
2029E
2030E
Revenue
Net sales — NA
$15,000
$15,287
$16,030
$16,431
$16,842
$17,263
Net sales — EMEA
$8,500
$8,663
$8,879
$9,101
$9,329
$9,562
Net sales — enterprise
$23,500
$23,950
$24,909
$25,532
$26,170
$26,825
growth
1.9%
4.0%
2.5%
2.5%
2.5%
Adjusted EBITDA — the bridge
Adj. EBITDA — NA
$2,300
$2,425
$3,093
$2,793
$2,863
$2,935
Adj. EBITDA — EMEA
$800
$950
$1,118
$1,001
$1,026
$1,052
Corporate / other
(100)
(75)
(75)
(75)
(75)
(75)
Adj. EBITDA — enterprise
$3,000
$3,300
$4,136
$3,719
$3,814
$3,911
margin
12.8%
13.8%
16.6%
14.6%
14.6%
14.6%
NA margin
15.3%
15.9%
19.3%
17.0%
17.0%
17.0%
EMEA margin
9.4%
11.0%
12.6%
11.0%
11.0%
11.0%
From EBITDA to unlevered free cash flow
Depreciation & amortization
(1,867)
(1,965)
(1,993)
(2,043)
(2,094)
(2,146)
EBIT (adjusted)
$1,133
$1,335
$2,143
$1,677
$1,721
$1,766
Cash taxes on EBIT
(272)
(320)
(514)
(402)
(413)
(424)
NOPAT
$861
$1,015
$1,629
$1,274
$1,308
$1,342
Add back D&A
$1,867
$1,965
$1,993
$2,043
$2,094
$2,146
Capital expenditures
(2,000)
(2,100)
(1,900)
(1,660)
(1,701)
(1,744)
Investment in net working capital
(45)
(96)
(62)
(64)
(65)
Separation / transaction costs (cash)
(250)
(125)
$0
$0
$0
Unlevered free cash flow
$728
$585
$1,501
$1,595
$1,636
$1,679
UFCF / revenue
3.1%
2.4%
6.0%
6.2%
6.3%
6.3%
Sanity checks against what the company says
Levered FCF proxy = UFCF − after-tax interest
$303
$1,219
Dividends paid
(980)
(980)
(980)
(980)
(980)
(980)
2025A reported free cash flow (actual)
(159)
dividend coverage by levered FCF
0.3×
1.2×
DCF — WACC, terminal value two ways, value per share
WACC
Market capitalization
$16,944
Net debt (gross debt − cash)
$8,491
Debt-like items from the 10-K, after tax (Inputs)
$957
Weight of equity (market)
66.6%
Weight of debt (market)
33.4%
Cost of equity = rf + β × ERP
8.9%
After-tax cost of debt
4.2%
WACC
7.4%
Cash flows
Fiscal year
2027E
2028E
2029E
2030E
Unlevered free cash flow
$1,501
$1,595
$1,636
$1,679
Years from valuation date (mid-year)
0.7
1.7
2.7
3.7
Discount factor
1.0
0.9
0.8
0.8
Present value of UFCF
$1,428
$1,414
$1,351
$1,291
Terminal value — two ways
Gordon growth: UFCF₂₀₃₀ × (1+g) / (WACC − g)
$31,960
Exit multiple: EBITDA₂₀₃₀ × multiple
$31,292
implied exit multiple of the Gordon TV
8.2×
implied perpetual growth of the exit-multiple TV
1.9%
Terminal value used (average of the two)
$31,626
PV of terminal value
$24,320
Enterprise value → equity value → per share
Sum of PV of 2027–2030 UFCF
$5,483
Enterprise value
$29,803
terminal value as % of EV
81.6%
Equity value = EV − net debt − debt-like
$20,355
Value per share ($)
$38.44
vs. market price
20.1%
implied EV / 2027E EBITDA
7.2×
market EV / 2027E EBITDA (today)
6.1×
market EV / 2026E EBITDA (today)
7.7×
Comps and sum of the parts
Trading comps (LTM, Multiples.vc)
Company
EV/EBITDA
EBITDA margin
Packaging Corp of America (PKG)
12.2×
21.0%
Smurfit Westrock (SW)
7.2×
15.0%
International Paper (IP) — 2026E, at $32
7.7×
13.8%
IP NA segment margin 2026E
15.9%
Sum of the parts — 2028E mid-cycle EBITDA × segment multiple (not the 2027 price-spike year)
Segment
2028E EBITDA
Multiple
Value
Why
Packaging Solutions NA
$2,793
9.0×
$25,139
Between SW (7.2x) and PKG (12.2x): a pure-NA box business with 80/20 margins not yet proven.
Packaging Solutions EMEA (DS Smith)
$1,001
6.5×
$6,507
European packaging peers trade 6–7x; a sale to a strategic could clear higher — that's the Oct 15 news.
Corporate (capitalized)
(75)
9.0×
(675)
Enterprise value (SOTP)
$30,972
Less: net debt + debt-like
(9,448)
Less: separation costs and dis-synergies (one-time)
(375)
1.5 years of the Inputs run-rate; two public companies cost more to run than one.
Equity value
$21,148
Per share ($)
$39.94
vs. $32 market
24.8%
Breakeven: EMEA multiple at which SOTP = market price
2.3×
If a buyer pays more than this for DS Smith, the market is underpricing the sum.
the model, line by line
This is the Scenarios sheet of the workbook, values as computed, so the whole argument fits on one screen: ten levers on top (the yellow cells), everything below them a formula. The DCF sheet does the same chain year by year; the Monte Carlo draws the levers from distributions and runs it twenty thousand times.
the chain
bear
mid-cycle
bull
Price that sticks through 2027 (% of $170/ton)
25.0%
50.0%
80.0%
NA volume growth 2027
-2.0%
1.0%
3.0%
Cost-out 2027
$200
$300
$450
Inflation 2027
$350
$250
$200
EMEA 2027 change
$0
$150
$300
Mid-cycle NA margin 2028+
13.0%
17.0%
20.0%
Mid-cycle EMEA margin 2028+
9.0%
11.0%
13.0%
Capex % of revenue 2028+
7.5%
6.5%
6.0%
WACC
7.9%
7.4%
6.9%
Exit multiple
7.0×
8.0×
9.5×
derived — formulas only from here down
NA revenue 2027E
$15,189
$16,030
$16,795
EMEA revenue 2027E
$8,879
$8,879
$8,879
NA EBITDA 2027E
$2,445
$3,093
$3,771
EMEA EBITDA 2027E
$896
$1,118
$1,340
Enterprise EBITDA 2027E
$3,266
$4,136
$5,037
Enterprise revenue 2030E
$25,919
$26,825
$27,648
Enterprise EBITDA 2030E (mid-cycle)
$2,912
$3,911
$4,785
UFCF 2030E
$704
$1,679
$2,441
UFCF 2027E (approx.)
$919
$1,597
$2,296
PV of 2027–2030 UFCF (linear ramp 2027→2030)
$2,781
$5,613
$8,196
Terminal value (avg of Gordon and exit multiple)
$16,318
$31,626
$48,362
Enterprise value
$15,115
$29,933
$46,034
Equity value
$5,667
$20,484
$36,586
Value per share ($)
$10.70
$38.69
$69.09
vs. $32 market
-66.6%
20.9%
115.9%
Terminal value share of EV
81.6%
81.2%
82.2%
Implied EV / 2027E EBITDA
4.6×
7.2×
9.1×
From IP_valuation_model.xlsx → Scenarios. Levers are judgments; derived rows are arithmetic. The Dictionary sheet in the workbook defines every term.
show the Monte Carlo (montecarlo.py, as run)
"""Monte Carlo over the IP valuation chain (same mechanics as the Scenarios sheet).
The deterministic model gives one number per story. This gives the distribution of
value per share when the four decisive assumptions are uncertain, and answers the
question a bet actually needs answered: P(value > price), the tails, and how big a
position the edge justifies (Kelly).
Everything here mirrors IP_valuation_model.xlsx — same inputs, same bridge, same
terminal mechanics. Run: python3 montecarlo.py (writes mc.json for the post's d3 chart)
"""
import json
import numpy as np
rng = np.random.default_rng(2026)
N = 20000
# ---------------- fixed inputs, read from the workbook's Inputs sheet so the two can't drift ----------------
from openpyxl import load_workbook
I = {r[0].value: r[1].value for r in load_workbook("IP_valuation_model.xlsx", data_only=True)["Inputs"].iter_rows(min_row=5)
if r[0].value and isinstance(r[1].value, (int, float))}
price, shares = float(I["Share price ($)"]), float(I["Diluted shares (millions)"])
net_debt = I["Gross debt"] - I["Cash and temporary investments"] + next(v for k, v in I.items() if "debt-like" in k.lower())
rev_2025_na, rev_2025_emea = float(I["2025 pro forma net sales — Packaging Solutions NA"]), float(I["2025 pro forma net sales — Packaging Solutions EMEA"])
rev_2026_h1 = float(I["H1 2026 net sales"])
na_26, emea_26 = float(I["2026 adj. EBITDA guidance — NA (midpoint)"]), float(I["2026 adj. EBITDA guidance — EMEA (midpoint)"])
corp = (I["2026 adj. EBITDA guidance — low"] + I["2026 adj. EBITDA guidance — high"]) / 2 - na_26 - emea_26
price_index_ytd = float(I["2026 containerboard index increase recognized YTD ($/ton, linerboard)"])
price_sens, price_in_2026 = float(I["EBITDA per $1/ton of price (annualized, NA)"]), float(I["Price benefit already inside 2026 NA guidance"])
contrib_margin = I["Contribution margin on incremental NA volume"]
costout_27, infl_27 = float(I["Incremental 80/20 cost-out 2027 (NA + EMEA)"]), float(I["Cost inflation 2027 (OCC, energy, labor, freight)"])
emea_27_delta = float(I["EMEA EBITDA change 2027 (box-price catch-up + cost-out, net)"])
rev_growth_lt, da_pct = I["Revenue growth 2028–2030 (nominal)"], I["D&A as % of revenue 2027+"]
nwc_pct, tax = I["Net working capital investment, % of incremental revenue"], I["Cash tax rate"]
capex_2027, sep_costs = float(I["Capex 2027"]), float(I["Separation / transaction costs expected 2026–27"])
g = I["Terminal growth rate"]
rev_na_26 = rev_2026_h1 * 2 * rev_2025_na / (rev_2025_na + rev_2025_emea)
rev_emea_26 = rev_2026_h1 * 2 * rev_2025_emea / (rev_2025_na + rev_2025_emea)
rev_26 = rev_na_26 + rev_emea_26
# ---------------- the uncertain assumptions, with a cycle ----------------
# Independent draws understate the tails: in a downturn price does NOT stick, demand falls,
# margins compress and the multiple shrinks together. So first draw the regime, then draw the
# assumptions conditional on it. P(down-cycle over the horizon) is itself a judgment: 30%.
p_down = 0.30
down = rng.random(N) < p_down
# 1. Share of the $170/ton that sticks through 2027. Jason: "more like half, lag is months."
stick = np.where(down, rng.beta(2, 5, N), rng.beta(5, 4, N)) # down: mode ~0.2; base: mode ~0.57
# 2. Mid-cycle NA EBITDA margin: SW 13% floor ... PKG 21% ceiling.
na_margin = np.where(down, rng.triangular(0.11, 0.135, 0.16, N), rng.triangular(0.14, 0.175, 0.21, N))
# 3. Exit EV/EBITDA multiple.
mult = np.where(down, rng.triangular(6.0, 7.0, 8.5, N), rng.triangular(7.0, 8.5, 11.0, N))
# 4. WACC: normal around 7.4%, 50 bp sd, floored well above g.
wacc = np.clip(rng.normal(0.074, 0.005, N), 0.055, 0.10)
# 2027 volume: base +1% +/- 1.5%; down-cycle -2.5% +/- 1.5% (TTOBMA's -2% forecast lives here).
vol = np.where(down, rng.normal(-0.025, 0.015, N), rng.normal(0.01, 0.015, N))
# EMEA mid-cycle margin and the 2027 catch-up also depend on the regime.
emea_margin_mid = np.where(down, rng.triangular(0.08, 0.095, 0.11, N), rng.triangular(0.10, 0.115, 0.13, N))
emea_27_delta = np.where(down, rng.normal(0.0, 75.0, N), rng.normal(150.0, 75.0, N))
# Capex intensity 2028+: the 'assets in the ground' lever.
capex_pct = rng.triangular(0.060, 0.065, 0.078, N)
# Model risk: the structure itself is wrong by some amount we cannot see. Lognormal haircut/uplift on
# the terminal value with 15% sd — Damodaran's margin of safety, made explicit instead of applied at the end.
MODEL_NOISE = 0.15
model_noise = np.exp(rng.normal(0.0, MODEL_NOISE, N))
# ---------------- the chain (vectorized, same as Scenarios sheet) ----------------
price_delta = stick * price_index_ytd * price_sens - price_in_2026
rev_na_27 = rev_na_26 * (1 + vol) + price_delta
rev_emea_27 = rev_emea_26 * (1 + rev_growth_lt)
na_27 = na_26 + price_delta + rev_na_26 * vol * contrib_margin + (costout_27 - infl_27) * rev_na_26 / rev_26
emea_27 = emea_26 + emea_27_delta + (costout_27 - infl_27) * rev_emea_26 / rev_26
ebitda_27 = na_27 + emea_27 + corp
rev_27 = rev_na_27 + rev_emea_27
grow3 = (1 + rev_growth_lt) ** 3
rev_30 = rev_27 * grow3
ebitda_30 = rev_na_27 * grow3 * na_margin + rev_emea_27 * grow3 * emea_margin_mid + corp
ufcf_30 = (ebitda_30 - rev_30 * da_pct) * (1 - tax) + rev_30 * da_pct - rev_30 * capex_pct - rev_30 * rev_growth_lt / (1 + rev_growth_lt) * nwc_pct
ufcf_27 = (ebitda_27 - rev_27 * da_pct) * (1 - tax) + rev_27 * da_pct - capex_2027 - sep_costs * 0.5
pv_interim = (ufcf_27 / (1 + wacc) ** 0.7
+ (ufcf_27 * 2 / 3 + ufcf_30 / 3) / (1 + wacc) ** 1.7
+ (ufcf_27 / 3 + ufcf_30 * 2 / 3) / (1 + wacc) ** 2.7
+ ufcf_30 / (1 + wacc) ** 3.7)
tv = 0.5 * (ufcf_30 * (1 + g) / (wacc - g) + ebitda_30 * mult) * model_noise
ev = pv_interim + tv / (1 + wacc) ** 3.7
value = (ev - net_debt) / shares
# ---------------- what a bet needs to know ----------------
p_above = float((value > price).mean())
pct = {k: float(np.percentile(value, k)) for k in (5, 10, 25, 50, 75, 90, 95)}
ev_value = float(value.mean())
# Treat it as a bet that resolves over ~3 years: return r = value/price - 1 (ignoring the 5.8% dividend,
# which is upside if it survives and a cut if it doesn't). Growth-optimal (Kelly) fraction solves
# max_f E[log(1 + f r)] numerically; we report half of it, which is what practitioners actually size to.
r = value / price - 1
fs = np.linspace(0, 1, 201)
growth = np.array([np.mean(np.log1p(np.clip(f * r, -0.999, None))) for f in fs])
kelly = float(fs[growth.argmax()])
half_kelly = kelly / 2
# what drives it: rank correlation of each input with value
def spearman(a, b):
ra, rb = a.argsort().argsort(), b.argsort().argsort()
return float(np.corrcoef(ra, rb)[0, 1])
drivers = {"down-cycle regime": spearman(down.astype(float), value), "price sticks": spearman(stick, value), "NA mid-cycle margin": spearman(na_margin, value),
"exit multiple": spearman(mult, value), "WACC": spearman(wacc, value),
"2027 volume": spearman(vol, value), "capex intensity": spearman(capex_pct, value), "model noise": spearman(model_noise, value)}
hist, edges = np.histogram(value, bins=np.arange(0, 100, 2.5))
hist_down, _ = np.histogram(value[down], bins=edges) # the same bins, split by regime: the two humps
hist_base, _ = np.histogram(value[~down], bins=edges)
out = {"n": N, "price": price, "p_down": p_down, "p_above_price_given_down": float((value[down] > price).mean()), "p_above_price_given_base": float((value[~down] > price).mean()),
"mean_down": float(value[down].mean()), "mean_base": float(value[~down].mean()), "p_above_price": p_above, "mean": ev_value, "percentiles": pct,
"kelly": kelly, "half_kelly": half_kelly, "drivers": drivers,
"hist": {"edges": edges.tolist(), "counts": hist.tolist(), "down": hist_down.tolist(), "base": hist_base.tolist()},
"median_down": float(np.median(value[down])), "median_base": float(np.median(value[~down])), "share_down": float(down.mean()),
"model_noise_sd": MODEL_NOISE, "p_loss_half": float((value < price * 0.5).mean()), "p_double": float((value > price * 2).mean())}
json.dump(out, open("mc.json", "w"), indent=1)
print(f"P(value > ${price}) = {p_above:.1%} mean ${ev_value:.1f} median ${pct[50]:.1f}")
print("percentiles:", {k: round(v, 1) for k, v in pct.items()})
print(f"P(value < half the price) = {out['p_loss_half']:.1%} P(value > 2x) = {out['p_double']:.1%}")
print(f"Kelly {kelly:.2f} half-Kelly {half_kelly:.2f}")
print(f"given down-cycle ({p_down:.0%}): P(>price) {out['p_above_price_given_down']:.1%}, mean ${out['mean_down']:.1f}; given base: P(>price) {out['p_above_price_given_base']:.1%}, mean ${out['mean_base']:.1f}")
print("drivers (rank corr):", {k: round(v, 2) for k, v in drivers.items()})
The browser panel above uses the same chain with a 3,000-draw simulation, so its percentages differ from the Python run by a point or two.
the model and the code
The spreadsheet is IP_valuation_model.xlsx: 291 live formulas, a Dictionary sheet that explains every term in plant language, every input tagged with its source (the FY2025 10-K, the Q2 2026 release, the Q1 and Q4 calls, Fastmarkets via Packaging Dive, Multiples.vc), and every judgment of mine in yellow so you can overwrite it. The Monte Carlo is montecarlo.py, which mirrors the sheet's scenario chain line for line; build_model.py generates the workbook, validate.py runs the twenty-one tie-out checks, and bet.py maps the distribution onto the instruments. Public information only. Not investment advice; I'm an engineer who builds solvers, not a financial advisor, and I may hold or trade any of these names.
The mechanics are the ones Wall Street Prep teaches — unlevered free cash flow, WACC, a terminal value two ways, comps and sum-of-the-parts. The discipline is Damodaran's: story first, mid-cycle for a cyclical, show what the terminal value carries, and treat the answer as a distribution because that's what it is. The floor is mine.
Written by Jason Fetzer, edited with Claude. Sources: IP Form 10-K for 2025 (filed February 2026); IP Q2 2026 earnings release; Packaging Dive on the Q1 2026 call (Apr 30), the Savannah/Riceboro closures (Aug 2025) and Fastmarkets RISI pricing (Sept 18, 2026); MarketBeat Q4 2025 call summary (Jan 29, 2026); Investing.com on the EMEA sale process (Oct 15, 2026); Multiples.vc for PKG and SW; Benzinga quote (Oct 15, 2026).