CHAPTER 26/ 45
Quality
Through line: Quality is not a single number. It has several accounting dimensions: profitability, leverage, the cash content of earnings, and stability. For each dimension, ask what the numerator is, what the denominator is, whether both come from the same financial-statement version, and whether the market knew them at that point in time. Prerequisites: Chapter 12 (financial statements), Chapter 23 (cross-sectional research), and Chapter 24 (value and point-in-time fundamentals). After this chapter, you should be able to decompose ROE into net margin, turnover, and the equity multiplier, and explain why rising ROE does not necessarily mean better operations; distinguish ending from average denominators and handle negative or near-zero equity; calculate gross profit to assets, operating profitability to equity, accrual ratios, and earnings stability; construct ratios from one financial-statement version; explain financial-sector conventions; and interpret the published RMW factor return while recognizing that it does not test an individual-stock quality signal.
1. Chapter goals
Readers should be able to answer: Is a company “high quality” because it truly earns more, or because it borrows more? Does a number in an earnings ratio represent cash, and was it public at the time?
- State the numerator, denominator, unit, direction, and relevant industries for ROE, ROA, GP/A, OP, leverage, the accrual ratio, and earnings stability.
- Use the DuPont decomposition
to derive the effect of leverage, , and use two companies to show how “operating improvement” and “more leverage” can produce similar ROE changes. - Explain the choice between average and ending balances, why negative or near-zero equity makes ROE meaningless, and why both year-end equity balances must be positive before averaging them.
- Apply the “same financial-statement version” rule: numerator and denominator must come from the same filing; a restated numerator cannot be divided by a pre-restatement denominator.
- Reproduce with a constructed panel that net-income measures mix in accruals; most cross-sectional ROE variance comes from leverage; near-zero equity can severely distort Pearson IC; Spearman is more robust to extreme magnitudes, but ranks and sample filtering can still change it; and the Pearson composite identity for two uncorrelated standardized dimensions, then compare it in scale with a constructed rank IC.
- Read published monthly RMW returns and report the mean,
-statistic, subperiods, drawdown, and correlations; explain that this describes an aggregate factor return and does not test any stock-level quality signal.
Completion criterion: Every quality signal includes a field table specifying each field’s reporting period, available_at, ingested_at, unit, and industries where it is undefined.
2. Why Quant needs this
A value factor asks, “Is the price cheap relative to fundamentals?” A quality factor asks, “Are the fundamentals themselves good?” Together, they provide more information available in advance for identifying value traps: a company that is cheap but unprofitable, highly leveraged, or supported by accruals may rank well on value and poorly on quality. Chapter 28 combines the two, but every quality metric must stand on its own before combination.
Quality metrics have the same difficulty as value metrics, but it is less visible. The numerator comes from the income statement (a flow over a period), while the denominator comes from the balance sheet (a stock at a point in time); their time conventions naturally differ. The denominator is often equity close to zero or even negative. Profit and liabilities mean different things in financial firms. Net income includes amounts unsupported by cash. Financial statements may be restated. Chapter 24’s point-in-time selection rules apply directly, with one additional rule: the numerator and denominator of a ratio must come from the same version.
The chapter proceeds as follows: define the measures (Section 5); derive why leverage changes ROE (6.1); address denominators (6.2), measures that do not directly contain a financing multiplier (6.3), accruals (6.4), estimation error in stability (6.5), same-version selection (6.6), and financial firms (6.7); quantify how standardized dimensions combine (6.8); and finish with a constructed panel and published RMW returns (Sections 10–11).
3. Intuition
Last year, two companies both raised ROE from 15% to 20%. Company A cut costs and sold more effectively, so it earned more profit per dollar of sales. Company B borrowed money to repurchase shares, shrinking its equity; the same profit divided by less equity raised ROE. Both ROEs rose, but A improved its operations while B took on more risk. Decomposing ROE into “margin × turnover × leverage” reveals the difference.
Another common confusion is profit versus cash. Net income is an accounting number. Part of it comes from changes in receivables, inventory, and accrued expenses and does not correspond to cash received. If the profit-cash difference comes from accruals that later reverse, it may be followed by lower earnings. Investment, seasonal working capital, and accounting classifications can also cause a difference; its direction must be tested. Subtracting net cash from operating activities from net income, then dividing by average assets, gives the accrual ratio. It measures the gap between accounting earnings and operating cash flow; it is not “cash” itself.
A third issue is timing. A financial statement may be released weeks after the fiscal year ends, and its reported values may later be revised. For every number in a quality signal, ask: who could see it at the signal month-end?
Simplifying boundary: the constructed panel in this chapter represents these phenomena with a minimal structure (one latent quality factor, one accrual tendency, and random leverage). Real-company accounting is much more complex.
3.1 One available filing, worked through completely
4. Mathematical foundations
Notation. For company
Average balances. Profit earned during a fiscal year corresponds to average assets during that year. A common calculation is
DuPont identity. When
The three factors are net margin, asset turnover, and the equity multiplier (also called financial leverage).
Precision of the sample standard deviation. For
Combining two signals. Let standardized signals
Frequency convention: accounting quantities are annual; signals and returns are monthly. signal_time is month-end, and the signal uses the most recent fiscal year that was available.
5. Formal definitions
Return on Equity (ROE).
Return on Assets (ROA).
Gross Profitability (GP/A).
Operating Profitability (OP). Fama and French’s (2015, note to Table 8) definition is
Leverage. This chapter uses both
Accrual ratio.
Earnings stability. The sample standard deviation of the most recent
Same-version rule. The numerator and denominator used to form a ratio must come from the same financial statement and version. After a restatement is released, recalculate using all values from the restated filing; before it is released, continue to use the original values.
RMW (Robust Minus Weak). A published profitability-factor spread:
Convention for using “quality.” In this chapter, “quality” refers only to the calculable accounting dimensions above, not unmeasured concepts such as management or a moat. Different studies combine dimensions differently; this chapter does not call any one combination “the standard definition of a quality factor.”
6. Derivations
6.1 DuPont decomposition and the effect of leverage on ROE
Consider a company with debt
The identity used is
The cost of leverage: the same
6.2 Denominators: average, ending, and near-zero equity
Sensitivity of ROE to the denominator.
Negative equity. When
Average-balance trap. Ending equity is average_balance(..., floor=0.0)).
Near-zero equity. Even positive equity may need a floor; for example, require equity to exceed 5% of assets (the floor parameter). The threshold is a research choice: write it into the plan and report the fraction excluded.
Ending versus average balances. Profit is a flow accumulated over the fiscal year, so average balances match it better. Ending balances refer to fiscal year-end and are simpler to use; that reference date does not mean they have been publicly released or locally ingested. Both ending and average conventions must wait until the same filing satisfies the available_at/ingested_at cutoff. The two are similar for companies with stable growth, but can differ substantially when equity changes rapidly (Section 8.3:
6.3 Why gross profit to assets does not directly contain a financing multiplier
Net income
6.4 Accruals: the gap between earnings and cash
Net cash from operating activities
6.5 Estimation error in earnings stability
Measure stability with the sample standard deviation
6.6 Same-version rule: a false change caused by a restatement
Suppose a company originally reports latest_available_values; all numerators and denominators must come from that row. This includes the prior year-end balance (the comparative balance in the same 10-K), which is why assets_prev and equity_prev are stored in the same row.
6.7 Financial firms
Banks and insurers have balance sheets dominated by financial liabilities. Interest is both a cost and operating revenue, and they do not have cost of goods sold in the same sense; the numerators for GP/A and OP are undefined, as are EV and EBITDA (Chapter 24). A bank’s equity multiplier is often around 10, versus 1 to 2 for a nonfinancial company, so the same ROE has a different meaning in the two industries. Treatment: set GP/A and OP to NaN for financial firms (Novy-Marx’s author manuscript also excludes them); rank ROE and ROA within industry; report coverage for each metric. In the constructed panel, the median bank equity multiplier is about 11.2, versus about 1.5 for nonfinancial companies.
6.8 Combining two standardized dimensions: the arithmetic of correlation
The following is an identity for standardized Pearson variables on the same paired sample. Uncorrelated does not mean independent; the specified correlation matrix must be valid, and the composite variance must be nonzero. Let
7. Financial interpretation
Quality has multiple dimensions. Profitability levels (GP/A, OP, ROA, ROE) ask “how much does the company earn?” Leverage asks “how much does it rely on borrowing?” Accruals ask “how much cash is in earnings?” Stability asks “how volatile are earnings?” Dimensions can point in opposite directions: a highly leveraged company can have high ROE but be fragile; a company with high accruals can report high earnings but little operating cash. Before compressing them into one “quality score,” know what each dimension measures.
ROE mixes in leverage. In the constructed panel, cross-sectional variance of ROE is 5.6 times that of ROA (0.0233 versus 0.0042). The mean leverage
Net-income measures contain accruals. In the constructed panel, profit
The relation between quality and value is an empirical question. Fama and French (2015, Section 7) find that value stocks tend to have weaker profitability unconditionally; after controlling for other factors, value stocks behave like robustly profitable stocks. In the constructed panel, quality and value are independent by design, so it cannot answer this question. Published RMW and HML returns have correlation 0.095 (Section 11).
Economic meaning of RMW. Published RMW averages 0.25% per month (
Availability timing. As in Chapter 24, Novy-Marx’s author manuscript starts using a fiscal year’s accounting data at the end of June in the next calendar year; Fama–French’s RMW also forms in July of year
7.1 Why profitability might relate to next-period returns
High profitability does not automatically imply high stock returns: if the price already fully reflects those cash flows, a buyer gets no free excess return. A risk hypothesis might ask whether highly profitable companies have lower risk and therefore lower required returns, or how different quality dimensions behave in bad states. A mispricing hypothesis might ask whether investors underweight earnings persistence or underreact to differences between cash and accruals. A proxy hypothesis might ask whether a measure stands in for industry, size, asset measurement, or leverage. Each question requires observations and tests designed in advance; the word “quality” alone does not imply a positive premium.
The accrual gap especially requires economic judgment. A growing company may increase receivables, temporarily making operating cash flow lower than profit; it may also be recognizing revenue aggressively. The same accrual ratio does not identify the cause. The course’s constructed panel builds a negative effect of accrual tendency on returns into its generator to demonstrate how the signal behaves under that assumption; it does not decide the mechanism for real companies. The original Sloan and Piotroski papers have not been read here, so no empirical results from them are added. RMW is a published portfolio spread and cannot substitute for single-stock GP/A or OP fields; it also does not test the accounting quality in this example.
8. Numerical worked examples: calculate each step
All numbers are generated by hand_examples() and independently checked using a different implementation in qa/a5/claude/recompute/ch26_recompute.py. The companies are constructed for teaching.
8.1 Same increase in ROE: operating improvement versus more leverage
This section uses a simplified DuPont calculation with ending balances, unlike P4’s average balances in Section 3.1. Amounts are in millions of dollars. Last year, Companies A and B were identical: net income 60, sales 1000, total assets 600, and equity 400. ROE
This year, Company A’s net income rises to 90 and everything else stays fixed: margin 9%, turnover 1.667, multiplier 1.5, and ROE
8.2 Effect of leverage on ROE
Let
8.3 Denominator: ending, average, and near-zero equity
Let
| Company | Beginning equity | Ending equity | Ending ROE | Average equity | Average ROE | If ending equity is below 2% of assets |
|---|---|---|---|---|---|---|
| Stable | 400 | 400 | 10.0% | 400 | 10.0% | Keep |
| Shrinking equity | 500 | 100 | 40.0% | 300 | 13.3% | Keep |
| Near-zero equity | 300 | 2 | 2000% | 151 | 26.5% | Set ending ROE to NaN ( |
| Negative at year-end | 100 | NaN | 35 (unguarded) | 114.3% (unguarded) | Require both balances positive first: NaN |
Interpretation: ending and average values differ most for companies whose equity changes rapidly. Averaging can hide negative ending equity by turning it positive (and reporting ROE of 114%). With a threshold, near-zero equity becomes NaN; report the fraction excluded.
8.4 Gross profit, operating profitability, and accruals
A company in this simplified example has identical beginning and ending assets, so 800 is also same-version average assets; the accrual example likewise uses 500 as average assets. Sales are 1000, cost of goods sold 600, SG&A 200, interest 20, total assets 800, and book equity 400. GP/A
8.5 False change from mixing versions
Originally,
8.6 Estimation error in stability
For a five-year standard deviation of ROA, relative standard error is
8.7 Combining two dimensions
Let
9. Python implementation
Field definitions are in the new module quality.py. Same-version fundamental selection reuses Chapter 24’s latest_available_values, which can retrieve multiple columns at once with the entire row from one version. Ranking and IC reuse Chapter 23’s factor_research.py.
| Function | Input | Output and boundary |
|---|---|---|
average_balance |
Beginning and ending balances | floor, either balance at or below it gives NaN (for equity use floor=0.0). |
guarded_ratio |
Numerator, denominator, floor (number or table) |
Preserves the numerator’s sign; denominator at or below floor gives NaN. |
dupont |
Net income, sales, assets, equity | Net margin, turnover, equity multiplier, and ROE (product of the first three); margin requires positive sales; turnover requires positive sales/assets; the multiplier requires positive assets/equity; net income retains its sign; ROE requires positive sales, assets, and equity. |
gross_profit_to_assets |
Revenue, cost of goods sold, assets | |
operating_profitability |
Revenue, cost of goods sold, SG&A, interest, book equity | Fama–French (2015) OP; any of four missing numerator items gives NaN; equity must be positive. |
accrual_ratio |
Net income, net cash from operating activities, assets | |
leverage_ratio |
Debt, assets | |
earnings_stability |
Annual-ratio history (rows are fiscal years) | Sample standard deviation over the latest min_years gives NaN. |
roe_from_leverage |
Default offline entry point (run from the I3 reader-package root):
.venv/bin/python run_notebook.py notebooks/ch26_quality.ipynb
The default is RUN_FRENCH=False. The stability example uses 100,000 worlds and seed 11 (the body’s historical example uses 200,000). The composite example uses 400,000 observations and resets seed 5 for each quality_world and quality_study default to seed 20261107. The runner also writes an execution copy and status under runs/execution_<stem>_<uuid>/. The notebook’s English-language layer and package-wide paths are assembled in BIL23. The following historical full CLI and test references are not the default offline command:
.venv/bin/python code/ch26_quality.py
.venv/bin/python -m pytest tests/test_ch26_quality.py tests/test_ch26_workflow.py -q
By default, this builds only a chapter-level RUN_DIR and does not write CSV, PNG, or JSON files or generate a historical figure library.
Frequency: accounting measures are annual; signals and returns are monthly. signal_time is the final instant of month-end (UTC). Version selection follows Chapter 24, Section 6.4, using latest_available_values. Each filing contains both the current and prior-year balance sheets, so average balances come from the same filing. CLI seed: 20261107.
Code segment one: DuPont decomposition and leverage effect (Sections 8.1 and 8.2).
import pandas as pd
from quant_course.quality import dupont, roe_from_leverage
idx = pd.period_range("2026-06", periods=1, freq="M")
mk = lambda v: pd.DataFrame([[v]], index=idx, columns=["f"])
before = dupont(mk(60.0), mk(1000.0), mk(600.0), mk(400.0))
ops = dupont(mk(90.0), mk(1000.0), mk(600.0), mk(400.0)) # operations improve: margin 6% -> 9%
lev = dupont(mk(56.25), mk(1000.0), mk(600.0), mk(300.0)) # buy back 100 of equity with debt; after-tax interest 3.75
r = lambda d, k: float(d[k].iloc[0, 0])
assert abs(r(before, "roe") - 0.15) < 1e-12 and abs(r(ops, "roe") - 0.225) < 1e-12 and abs(r(lev, "roe") - 0.1875) < 1e-12
assert r(lev, "net_margin") < r(before, "net_margin") and r(lev, "equity_multiplier") > r(before, "equity_multiplier") # ROE rose although margin fell
assert abs(roe_from_leverage(0.08, 0.03, 1.0) - 0.13) < 1e-12 and abs((16 - 0.03 * 100) / 100 - 0.13) < 1e-12 # balance-sheet check
print(round(r(ops, "roe"), 4), round(r(lev, "roe"), 4))
Code segment two: denominator rules (Section 8.3).
import numpy as np
import pandas as pd
from quant_course.quality import average_balance, guarded_ratio
idx = pd.period_range("2026-06", periods=1, freq="M")
cols = ["steady", "shrinking", "tiny", "negative"]
ni = pd.DataFrame([[40.0] * 4], index=idx, columns=cols)
end = pd.DataFrame([[400.0, 100.0, 2.0, -30.0]], index=idx, columns=cols)
prev = pd.DataFrame([[400.0, 500.0, 300.0, 100.0]], index=idx, columns=cols)
assets = pd.DataFrame([[1000.0] * 4], index=idx, columns=cols)
roe_end = guarded_ratio(ni, end).iloc[0]
roe_avg_unguarded = guarded_ratio(ni, average_balance(end, prev)).iloc[0]
roe_avg = guarded_ratio(ni, average_balance(end, prev, floor=0.0)).iloc[0]
roe_floor = guarded_ratio(ni, end, floor=0.02 * assets).iloc[0]
assert abs(roe_end["tiny"] - 20.0) < 1e-12 and np.isnan(roe_end["negative"]) # 2000%, and no ratio for negative equity
assert abs(roe_avg_unguarded["negative"] - 40 / 35) < 1e-12 # the average hides the negative balance: 114%
assert np.isnan(roe_avg["negative"]) and abs(roe_avg["shrinking"] - 40 / 300) < 1e-12
assert np.isnan(roe_floor["tiny"]) and abs(roe_floor["shrinking"] - 0.4) < 1e-12 # 2 < 2% of assets (20): dropped
print(pd.DataFrame({"ending": roe_end, "average, unguarded": roe_avg_unguarded, "average, both > 0": roe_avg, "ending, 2% floor": roe_floor}).round(3))
Code segment three: GP/A, OP, accruals, and leverage (Section 8.4).
import pandas as pd
from quant_course.quality import accrual_ratio, gross_profit_to_assets, operating_profitability
idx = pd.period_range("2026-06", periods=1, freq="M")
mk = lambda *v: pd.DataFrame([list(v)], index=idx, columns=["low_debt", "high_debt"][: len(v)])
rev, cogs, sga = mk(1000.0, 1000.0), mk(600.0, 600.0), mk(200.0, 200.0)
interest, assets, equity = mk(20.0, 60.0), mk(800.0, 800.0), mk(400.0, 400.0)
gpa = gross_profit_to_assets(rev, cogs, assets).iloc[0]
op = operating_profitability(rev, cogs, sga, interest, equity).iloc[0]
assert (gpa == 0.5).all() # GP/A ignores interest
assert abs(op["low_debt"] - 0.45) < 1e-12 and abs(op["high_debt"] - 0.35) < 1e-12 # OP falls when interest rises
acc = accrual_ratio(mk(50.0, 50.0), mk(80.0, 20.0), mk(500.0, 500.0)).iloc[0]
assert abs(acc["low_debt"] + 0.06) < 1e-12 and abs(acc["high_debt"] - 0.06) < 1e-12 # cash-backed vs not (columns reused as labels)
print(gpa.to_dict(), op.round(3).to_dict(), acc.round(3).to_dict())
Code segment four: same-version rule and restatement (Section 8.5).
import pandas as pd
from quant_course.factor_signals import latest_available_values
from quant_course.quality import guarded_ratio
versions = pd.DataFrame({
"security_id": ["S1", "S1"], "period_end": pd.to_datetime(["2022-12-31", "2022-12-31"]),
"available_at": pd.to_datetime(["2023-03-15 16:05", "2023-11-20 16:05"], utc=True),
"ingested_at": pd.to_datetime(["2023-03-15 16:12", "2023-11-20 16:12"], utc=True),
"net_income": [40.0, 28.0], "equity": [400.0, 350.0]}) # restated NI and equity
months = pd.period_range("2023-10", "2023-11", freq="M")
pit = latest_available_values(versions, months, ["net_income", "equity"])
same_version = guarded_ratio(pit["net_income"], pit["equity"])["S1"]
old_equity = latest_available_values(versions, months[:1], ["equity"])["equity"]["S1"].iloc[0] # the equity known in October, kept after the restatement
mixed_nov = pit["net_income"]["S1"].iloc[1] / old_equity
assert abs(same_version.iloc[0] - 0.10) < 1e-12 and abs(same_version.iloc[1] - 0.08) < 1e-12
assert abs(mixed_nov - 0.07) < 1e-12 # new numerator over the old denominator: a false -3 pp change
print(same_version.round(3).to_dict(), round(mixed_nov, 3))
Code segment five: estimation error in earnings stability and probability of a rank reversal (Sections 6.5 and 8.6).
import numpy as np
rng = np.random.default_rng(11)
worlds = 200_000
from scipy import stats
for n in (5, 10):
expected = float(stats.f.sf((0.06 / 0.04) ** 2, n - 1, n - 1)) # exact: s1^2/s2^2 = (sigma1/sigma2)^2 F(n-1, n-1); 0.226 and 0.121
s1 = rng.normal(0.0, 0.04, (worlds, n)).std(axis=1, ddof=1) # true sigma 4%
s2 = rng.normal(0.0, 0.06, (worlds, n)).std(axis=1, ddof=1) # true sigma 6% (50% higher)
reversed_share = float((s1 > s2).mean()) # sample ranking opposite to the truth
assert abs(reversed_share - expected) < 0.01 # the lognormal approximation (0.209, 0.112) is a little low
print(n, round(reversed_share, 3), round(1 / np.sqrt(2 * (n - 1)), 3))
Code segment six: IC after combining two uncorrelated signals (Sections 6.8 and 8.7).
import numpy as np
rng = np.random.default_rng(5)
rho1, rho2, n = 0.0241, 0.0418, 600_000
def composite_ic(r12: float) -> float:
"""Correlation of z1 + z2 with y when corr(z1, y) = rho1, corr(z2, y) = rho2 and corr(z1, z2) = r12."""
cov = np.array([[1.0, r12, rho1], [r12, 1.0, rho2], [rho1, rho2, 1.0]])
z1, z2, y = rng.multivariate_normal(np.zeros(3), cov, size=n).T
return float(np.corrcoef(z1 + z2, y)[0, 1])
zero, half = composite_ic(0.0), composite_ic(0.5)
assert abs(zero - (rho1 + rho2) / np.sqrt(2)) < 0.004 and abs(half - (rho1 + rho2) / np.sqrt(2 * 1.5)) < 0.004
assert half < zero # a correlated second signal shrinks the gain
print(round(zero, 4), round(half, 4))
10. Reading the figures
Figure 26-1: Leverage, ROE, and near-zero equity. Left: with an after-tax debt cost of 3% (dashed line), three curves show ROE against

Figure 26-2: IC for different quality measures. Constructed panel with 300 nonfinancial companies and 132 months, using fixed seed 20261107. Bars show mean Spearman IC (vertical lines are approximately plus or minus two standard errors), in order: ROE (ending equity), ROE (average equity), ROE (average equity with equity above 5% of assets), ROA, GP/A, OP,

Figure 26-3: Published RMW. Left: compounded cumulative RMW monthly spread (log scale, starting at 1, July 1963 through August 2026). Right: rolling 120-month mean RMW (percent per month). Interpretation: a slow long-run rise, positive most of the time, with a drawdown around 2000 (maximum drawdown about 42%, occurring in 2000-02). This is a published aggregate spread, before costs and financing.

11. Historical market case
Data and limitations. Use the French five-factor (2×3) monthly data frozen on 2026-10-05: Ken French Data Library: F-F Research Data 5 Factors 2x3. The processed CSV has SHA-256 833bd925b49651428781c789c13a6ac7454d4676449e6d119782050046a57eea and 758 months (July 1963 through August 2026), in decimal monthly spread returns. Publicly downloadable does not mean freely redistributable. RMW construction is described in Section 5 and the source notes: six value-weighted portfolios are formed by size and operating profitability using fiscal year
Descriptive results.
| Period | Months | Mean (% per month) | Standard deviation (% per month) | |
|---|---|---|---|---|
| 1963-07 through 1999-12 | 438 | 0.173 | 2.26 | 1.60 |
| 2000-01 through 2026-08 | 320 | 0.359 | 2.16 | 2.98 |
| 2007-01 through 2020-12 | 168 | 0.234 | 1.90 | 1.59 |
| Full sample | 758 | 0.251 | 3.03 | 2.29 |
Maximum drawdown of compound returns is 41.8%, occurring in 2000-02. Correlation with market excess return is
What can and cannot be said. We can say that the published profitability spread averaged positive returns over 1963–2026; mean returns are positive in both subperiods, while standard deviations differ by nearly a factor of two; RMW is negatively correlated with SMB and nearly uncorrelated with HML. We cannot say: (1) that any one accounting metric (GP/A, OP, ROE, or accruals) is the better quality measure, since RMW uses only operating profitability; (2) that high-OP individual stocks earn higher returns, since RMW is the arithmetic difference of six value-weighted portfolios; (3) what the quality–value relation is for individual stocks: Fama–French’s (2015, Section 7) unconditional and conditional descriptions apply to their sample, and this chapter has no individual-stock test; or (4) that the strategy is tradable, has capacity, or is profitable after costs.
What is missing for stock-level quality research. We need every version and available_at for each company-year’s revenue, cost of goods sold, SG&A, interest, net income, operating cash flow, assets, equity, and debt; historical stock universes and delistings; historical industry classifications; prices and market capitalizations. The SEC Financial Statement Data Sets listed in Chapter 24 provide as-filed XBRL values and acceptance timestamps from 2009 onward, but mappings from tags (Extensible Business Reporting Language, XBRL, labels) to accounting concepts and coverage/consistency of items such as cost of goods sold require separate review; the datasets do not contain prices or delisting returns. No such data were downloaded for this chapter. The Project 4 preparation-layer gap table lists these items.
12. Common mistakes
Mistake 1: interpreting higher ROE as operating improvement. Section 8.1: Company B’s margin falls while its ROE rises. Fix: always report the three DuPont factors, or use GP/A, which does not directly deduct interest, in a comparison holding operations and assets fixed; report leverage as well. ROA uses net income after interest and still contains financing information.
Mistake 2: averaging away negative equity. Section 8.3: ending equity
Mistake 3: allowing near-zero equity into the ranking. In the constructed panel, Pearson IC for unguarded ending-equity ROE is 0.0017 (
import numpy as np
import pandas as pd
from quant_course.factor_research import information_coefficient
from quant_course.quality import guarded_ratio
rng = np.random.default_rng(8)
n = 300
quality = rng.standard_normal(n)
equity = np.exp(rng.normal(5.0, 0.3, n)); equity[:6] = [1.0, 0.5, 2.0, 0.8, 1.5, 3.0] # six near-zero equity firms
income = 40 + 15 * quality + rng.normal(0, 5, n)
ret = 0.01 * quality + 0.03 * rng.standard_normal(n)
idx = pd.period_range("2026-06", periods=1, freq="M")
frame = lambda v: pd.DataFrame([v], index=idx, columns=range(n))
raw = guarded_ratio(frame(income), frame(equity)).iloc[0].to_numpy() # no floor: ROE of ~4000% for the tiny firms
guard = guarded_ratio(frame(income), frame(equity), floor=frame(np.full(n, 20.0))).iloc[0].to_numpy()
p_raw, p_guard = information_coefficient(raw, ret, method="pearson"), information_coefficient(guard, ret, method="pearson")
s_raw = information_coefficient(raw, ret, method="spearman")
assert p_guard > p_raw + 0.1 and abs(s_raw - information_coefficient(guard, ret, method="spearman")) < 0.1
print(round(p_raw, 3), round(p_guard, 3), round(s_raw, 3))
Mistake 4: mixing versions. Section 8.5: using a new numerator over an old denominator gives a 3-percentage-point decline instead of the true 2-point decline, an extra 1-point drop. Fix: select the whole row at once.
Mistake 5: calculating GP/A or OP for financial firms. Banks have no cost of goods sold in this definition; replacing revenue with interest income and cost with interest expense produces a different measure. Fix: set these two metrics to NaN for financial firms, rank ROE and ROA within industry, and report coverage. In the constructed panel, all bank GP/A values are NaN (CLI assertion).
Mistake 6: treating net income as cash. Section 8.4: two companies with the same net income have operating cash flow of 80 and 20; this does not identify, transaction by transaction, which profit was received in cash. Fix: calculate the accrual ratio separately and read net-income measures alongside accruals.
Mistake 7: ranking stability from only a few years. Sections 6.5 and 8.6: the relative error of a five-year standard deviation is 35%; the probability that sample ranks reverse for two companies whose true standard deviations differ by 30% is about 30%. When the true difference is 50% (26-E06), the exact reversal probabilities are about 23% for 5 years and 12% for 10 years. Fix: require enough years, reduce the weight, or use a longer window.
Mistake 8: tuning composite weights on the test period. The composite assumptions in Sections 8.7 and 6.8 set equal weights in advance. Choosing weights based on test-period IC inflates the composite IC through post hoc selection (Chapter 22). Fix: fix weights in the plan or estimate them only in the training period.
13. Quantitative research application
Quality field contract (Project 4 signal layer).
| Field | Source | Time fields | Unit | Missingness and convention |
|---|---|---|---|---|
revenue, cogs, sga, interest |
Income statement | period_end, available_at, ingested_at |
Reporting currency | cogs is missing for financial firms; set GP/A and OP to NaN. |
net_income, cfo (cash flow from operations) |
Income statement, cash-flow statement | Same as above | Reporting currency | Must be from the same version as assets. |
assets, assets_prev, equity, equity_prev, debt |
Balance sheet (current-year and prior-year comparative balances) | Same as above | Reporting currency | Current P4 average equity first requires both year-end equity values positive, then average equity above 5% of average assets; average assets follow the interface convention. |
gp_a, neg_accrual, roe_avg |
Current P4 signal calculation | signal_time |
Decimal | Calculate from one same-version row; OP, ROA, and leverage are separate teaching measures. |
industry |
Versioned classification | available_at |
Label | Used for within-industry ranking. |
Research workflow (constructed panel). Freeze a plan: the hypothesis that companies with higher gross profit to assets and a lower accrual ratio on the same date have higher next-month returns; the signal is the equal-weighted average of the same-date percentiles of those two dimensions; the primary statistic is the
Constraints for Project 4. (1) Retrieve an entire row at once with latest_available_values for every ratio. (2) Report coverage and exclusions for each metric. (3) Handle financial firms separately. (4) Report net-income measures together with accruals and leverage. (5) Require enough years for stability. (6) Fix composite weights in advance; multiple dimensions enter Chapter 28’s factor-correlation and crowding analysis. (7) Record every variant in Chapter 22’s ledger.
13.1 Metric mapping and coverage: first ask “who is still rankable?”
The current P4 quality fields are only gp_a, neg_accrual, and roe_avg, with formulas corresponding to 44.4444%, +3.3333%, and 25.7143% in Section 3.1. The current seven fields do not include OP. The standalone quality.operating_profitability function is available for teaching; the existence of this function does not mean P4 has integrated OP. The paper’s gross profits-to-assets, the course’s average-asset GP/A, and French’s equity-denominator OP are three distinct definitions; do not substitute one for another. RMW is a portfolio return formed under French’s convention, not company-level OP input.
P4’s actual roe_avg guard operates in two steps: average_balance(equity, equity_prev, floor=0.0) first excludes any record with a nonpositive year-end equity balance; then guarded_ratio(..., floor=0.05 * assets_avg) excludes average equity that is not strictly greater than 5% of average assets. A value equal to the threshold is excluded. This is a prespecified interface threshold, not a universal definition of ROE. The 2% threshold in Section 8.3 and the 5% ending-equity threshold in 26-E03 are separate teaching-policy examples and must not be described as the P4 rule.
Consider four companies with average assets of 1000 and net income of 40, and beginning/ending equity as follows: A (80,120), B (10,30), C (120,-20), and D (40,60). Company A has average equity 100, greater than 50, so ROE of 40% is valid. B has average equity 20, at or below 50, and is excluded. C is excluded first because ending equity is nonpositive (its unguarded average is exactly 50). D’s average equity is 50, equal to the threshold, and is excluded. Of four companies with complete inputs, only one passes: valid ROE coverage is 25%, with 75% lost. Accruals or GP/A may still be available according to their own fields. Missingness is not a zero quality score. Report coverage by size and industry and each failure reason, since filtering may systematically change the universe.
Raw quality fields enter the unified winsorization, industry/size residualization, standardization, and synthesis flow in Chapter 24, Section 13.1. Missing GP/A for financial firms changes which dimensions are available; renormalizing within style does not restore missing economic evidence. A residualized quality score and the final weight projection by industry are separate stages. Distinguish older A6 weights outside the support from C13’s limited correction: listed-support projection, gross restoration to 2, and the 60-name threshold are accepted; execution and drift residuals are reported separately. See the current-interface entry in Chapter 24, Section 13.1. Real eligibility, borrowing, fees, capacity, and a complete real-data P4 remain unfinished; old outputs have not thereby been rerun.
14. Chapter summary
Quality comprises several accounting dimensions. ROE is the product of net margin, turnover, and leverage; leverage can raise ROE without improving operations:
15. Exercises
26-E01 (by hand). A company’s first-year net income is 36, sales 600, total assets 400, and equity 150. In year two, net income is 39.6, sales 660, total assets 440, and equity 110 (it borrowed to repurchase shares). Calculate net margin, turnover, equity multiplier, and ROE for both years; explain the source of the ROE change.
26-E02 (by hand). Operating return
26-E03 (by hand). Net income is 24 and assets are 800. Three companies have (beginning equity, ending equity): (200, 180), (150, 6), and (80,
26-E04 (by hand). Revenue is 2400, cost of goods sold 1500, SG&A 500, interest 100, total assets 2000 (same-version beginning and ending assets are both 2000, so average assets are also 2000), and book equity is 800. Find GP/A and OP. If interest is 300, what are the two values? With net income 120, operating cash flow 60, and average assets 1000, what is the accrual ratio? What if operating cash flow is 180?
26-E05 (by hand). Originally, net income is 55 and equity is 500; after restatement, net income is 44 and equity is 440. Find both same-version ROEs, the value using the new numerator over the old denominator, and the false change caused by mixing versions.
26-E06 (calculation). What is the relative standard error of a standard deviation estimated over 5 years and over 10 years? If two companies’ true standard deviations are 4% and 6%, what are the approximate probabilities that their sample rankings reverse using 5-year and 10-year samples?
26-E07 (calculation). Two standardized signals have correlations
26-E08 (concept and calculation). A bank has ROA of 1% and an equity multiplier of 11; a software company has ROA of 8% and an equity multiplier of 1.5. Calculate both ROEs and explain why ranking them by ROE across the full market is inappropriate. What alternative would you use?
26-N01 (filing version and coverage). Recalculate the five ratios in Section 3.1 and label the sign, denominator, and annual unit; explain why February values are unavailable and why French OP does not switch to average equity. Use the four companies in Section 13.1 to check P4’s ROE threshold and coverage loss, and explain why GP/A and RMW cannot be substituted for each other.
16. Programming exercises
26-P01. Use dupont to reproduce 26-E01 and assert that the entire ROE change comes from the equity multiplier (net margin and turnover are unchanged). Acceptance criterion: assertions for all three factors pass.
26-P02. Construct a version table with one restatement, use latest_available_values to retrieve numerator and denominator together, and assert the ROEs before and after restatement as well as that “new numerator over old denominator” differs from both. Acceptance criterion: all three assertions pass.
26-P03. Simulate two signals with correlation 0.5 and correlations 0.03 and 0.02 with the label. Calculate the IC of their equally weighted composite and compare it with
17. Challenge problems
26-C01 (derivation). Extend Section 6.1. Let pretax operating profit (earnings before interest and taxes, EBIT) over assets be
26-C02 (open-ended). For a Project 4 quality signal, write a data-gap table and frozen plan. For every field, state whether it exists in the course’s current data, a source where real data could be obtained, its time fields, and missing-data rules. Explain which conclusions cannot be written without these data. List the three checks required before combining quality and value: correlation, coverage, and timing. Grading: gap table 30, frozen plan 30, claims that cannot be made 20, pre-combination checks 20.
18. Complete solutions and reference implementations
26-E01. Year one: net margin
26-E02.
26-E03. Ending ROEs:
26-E04. GP/A
26-E05. Same version: original
26-E06. Relative standard error
26-E07. If uncorrelated:
26-E08. Bank ROE
26-P01 reference implementation
import pandas as pd
from quant_course.quality import dupont
idx = pd.period_range("2026-06", periods=1, freq="M")
mk = lambda v: pd.DataFrame([[v]], index=idx, columns=["f"])
y1 = dupont(mk(36.0), mk(600.0), mk(400.0), mk(150.0))
y2 = dupont(mk(39.6), mk(660.0), mk(440.0), mk(110.0))
g = lambda d, k: float(d[k].iloc[0, 0])
assert abs(g(y1, "roe") - 0.24) < 1e-12 and abs(g(y2, "roe") - 0.36) < 1e-12
assert abs(g(y1, "net_margin") - g(y2, "net_margin")) < 1e-12 and abs(g(y1, "asset_turnover") - g(y2, "asset_turnover")) < 1e-12
assert g(y2, "equity_multiplier") > g(y1, "equity_multiplier") # all of the ROE change is leverage
26-P02 reference implementation
import pandas as pd
from quant_course.factor_signals import latest_available_values
from quant_course.quality import guarded_ratio
versions = pd.DataFrame({
"security_id": ["S1", "S1"], "period_end": pd.to_datetime(["2022-12-31", "2022-12-31"]),
"available_at": pd.to_datetime(["2023-03-15 16:00", "2023-09-10 16:00"], utc=True),
"ingested_at": pd.to_datetime(["2023-03-15 16:10", "2023-09-10 16:10"], utc=True),
"net_income": [55.0, 44.0], "equity": [500.0, 440.0]})
months = pd.period_range("2023-08", "2023-09", freq="M")
pit = latest_available_values(versions, months, ["net_income", "equity"])
roe = guarded_ratio(pit["net_income"], pit["equity"])["S1"]
old_equity = pit["equity"]["S1"].iloc[0]
mixed = pit["net_income"]["S1"].iloc[1] / old_equity
assert abs(roe.iloc[0] - 0.11) < 1e-12 and abs(roe.iloc[1] - 0.10) < 1e-12
assert abs(mixed - 0.088) < 1e-12 and mixed != roe.iloc[0] and mixed != roe.iloc[1]
26-P03 reference implementation
import numpy as np
rng = np.random.default_rng(6)
n, r12, rho1, rho2 = 600_000, 0.5, 0.03, 0.02
cov = np.array([[1.0, r12, rho1], [r12, 1.0, rho2], [rho1, rho2, 1.0]]) # corr(z1,y)=rho1, corr(z2,y)=rho2, corr(z1,z2)=r12
z1, z2, y = rng.multivariate_normal(np.zeros(3), cov, size=n).T
sim = np.corrcoef(z1 + z2, y)[0, 1]
assert abs(sim - (rho1 + rho2) / np.sqrt(2 * (1 + r12))) < 0.005
print(round(sim, 4), round(0.05 / np.sqrt(3.0), 4))
26-C01. Assume a common tax rate, deductible interest, and a tax shield usable in the current period; hold asset/debt and operating-return/interest-rate conventions fixed. Losses and deferred tax shields require separate modeling. Pretax operating profit is
using
26-C02 (grading guidance and reference). In the course’s current data, French RMW is an aggregate spread; constructed companies in valuation.py have very few fields; and there are no versioned stock-level revenue, cost of goods sold, SG&A, interest, or operating-cash-flow data, nor prices, historical universes, or industry history. The SEC Financial Statement Data Sets can supply as-filed values and filed and accepted timestamps from 2009 onward, but XBRL tag mapping for items such as cost of goods sold, industry classifications, prices, and delisting returns must be sourced separately (and the data’s access restrictions followed; see Chapter 24). Claims that cannot be made: returns to a stock-level quality factor, its correlation with value, or its after-cost performance. Three checks before combination: same-date rank correlation of quality and value metrics (which determines composite gain), coverage and exclusions for each metric (which determine sample selection), and each field’s available_at and lag rule (which determine look-ahead). Grading follows the original four criteria.
26-N01 complete solution. Average assets are 900 and average equity is 350. GP/A is 44.4444%; ROE is 25.7143%; the raw accrual ratio is -3.3333%, so its negative is +3.3333%; debt to ending assets is 30%. French OP is also
19. Further reading
- Must Know: Fama–French (2015), note to Table 8 and Section 7; read the OP definition and the unconditional relation between value and profitability.
- Must Know: Ken French database five-factor description page; read how RMW is formed and its sample requirements.
- Should Know: Novy-Marx, The Other Side of Value, June 2012 author manuscript; read the introduction and data description to understand the asset denominator and exclusion of financial firms. The journal version was not read.
- Should Know: Chapter 12 of this course (financial statements), definitions of net income, cash flow, and accruals.
- Advanced: Original papers by Sloan (1996) on accruals and Piotroski (2000) on fundamental scoring; check empirical findings on accruals and stability measures. Not read for this chapter.
- Optional: “Selected Financial Data” in any public 10-K; practice finding each field’s reporting period and comparative balances.
20. References
Citation keys and scope of reading are listed in source_notes.md.
- CH26_FF5_PAGE: Kenneth R. French Data Library, “Description of Fama/French 5 Factors (2x3).”
- CH26_FF2015: Fama, E. F., and French, K. R. (2015). Journal of Financial Economics 116(1), note to Table 8 (printed p. 14), Section 7 (pp. 12–13).
- CH26_NOVYMARX2012: Novy-Marx, R. “The Other Side of Value: The Gross Profitability Premium,” June 2012 author manuscript; journal version, JFE 108(1), not read.
- CH24_SEC_FSDS: SEC Financial Statement Data Sets description (see Chapter 24 source notes).
- FRENCH: Frozen French five-factor snapshot (
data/manifests/french_factors_2026-10-05.json).