The Piotroski F-Score is one of the most elegant tools in quantitative value investing. Developed by Stanford accounting professor Joseph Piotroski in 2000, it distills a company's financial health into a single number between 0 and 9. In this guide, we'll implement it from scratch in Python.
What is the Piotroski F-Score?
The F-Score evaluates 9 binary criteria across three dimensions:
- Profitability (4 points): Is the company making money efficiently?
- Leverage & Liquidity (3 points): Is the balance sheet improving?
- Operating Efficiency (2 points): Are margins and turnover improving?
A score of 8-9 signals strong fundamentals. A score of 0-2 signals financial weakness. In Piotroski's original study, buying high-scoring stocks and shorting low-scoring ones generated 23% annual returns from 1976-1996.
The Implementation
def piotroski_f_score(
roa: float,
roa_prev: float,
ocf: float,
total_assets: float,
leverage: float,
leverage_prev: float,
current_ratio: float,
current_ratio_prev: float,
shares: int,
shares_prev: int,
gross_margin: float,
gross_margin_prev: float,
asset_turnover: float,
asset_turnover_prev: float,
) -> dict:
"""Calculate Piotroski F-Score with detailed breakdown."""
criteria = {}
# --- Profitability (4 criteria) ---
criteria['positive_roa'] = roa > 0
criteria['positive_ocf'] = ocf > 0
criteria['increasing_roa'] = roa > roa_prev
criteria['accruals'] = (ocf / total_assets) > roa if total_assets > 0 else False
# --- Leverage & Liquidity (3 criteria) ---
criteria['decreasing_leverage'] = leverage < leverage_prev
criteria['increasing_current_ratio'] = current_ratio > current_ratio_prev
criteria['no_dilution'] = shares <= shares_prev
# --- Operating Efficiency (2 criteria) ---
criteria['increasing_gross_margin'] = gross_margin > gross_margin_prev
criteria['increasing_asset_turnover'] = asset_turnover > asset_turnover_prev
score = sum(1 for passed in criteria.values() if passed)
return {
'score': score,
'criteria': criteria,
'interpretation': interpret_score(score)
}
def interpret_score(score: int) -> str:
if score >= 8:
return "Strong - Robust financial health"
elif score >= 6:
return "Good - Financially sound"
elif score >= 4:
return "Moderate - Mixed signals"
elif score >= 2:
return "Weak - Multiple red flags"
else:
return "Very Weak - Significant distress indicators"
Using It with Real Data
Here's how you'd use this with financial statement data:
result = piotroski_f_score(
roa=0.08, roa_prev=0.06, # ROA improved
ocf=500_000_000, # Positive cash flow
total_assets=5_000_000_000,
leverage=0.35, leverage_prev=0.40, # Debt decreased
current_ratio=1.8, current_ratio_prev=1.5, # Liquidity improved
shares=1_000_000, shares_prev=1_000_000, # No dilution
gross_margin=0.42, gross_margin_prev=0.38, # Margins improved
asset_turnover=0.65, asset_turnover_prev=0.60 # Efficiency improved
)
print(f"F-Score: {result['score']}/9")
print(f"Assessment: {result['interpretation']}")
print("\nCriteria breakdown:")
for name, passed in result['criteria'].items():
status = "PASS" if passed else "FAIL"
print(f" {name}: {status}")
Output:
F-Score: 9/9
Assessment: Strong - Robust financial health
Criteria breakdown:
positive_roa: PASS
positive_ocf: PASS
increasing_roa: PASS
accruals: PASS
decreasing_leverage: PASS
increasing_current_ratio: PASS
no_dilution: PASS
increasing_gross_margin: PASS
increasing_asset_turnover: PASS
The Accruals Check - The Most Misunderstood Criterion
Criterion 4 (accruals) is the most subtle. It checks whether operating cash flow relative to assets exceeds ROA. Why? Because earnings driven by actual cash flow are higher quality than earnings driven by accounting accruals.
A company can report positive earnings while generating negative cash flow through aggressive revenue recognition, capitalizing expenses, or other accounting choices. The accruals check catches this.
# This company reports profits but cash flow tells a different story
accruals_check = (ocf / total_assets) > roa
# If ROA is 8% but OCF/Assets is only 3%,
# earnings are heavily accrual-based = red flag
Limitations to Keep in Mind
The F-Score isn't perfect:
Backward-looking: It compares this year to last year. A company recovering from a bad year scores well even if it's still mediocre.
Sector-blind: The same thresholds apply to capital-intensive manufacturers and asset-light tech companies. A tech company with high leverage might score poorly on criteria that don't apply.
Cyclical sensitivity: In cyclical downturns, even great companies see declining margins and ROA. The score drops not because the company is weak, but because the entire sector is in a trough.
No growth consideration: Two companies with identical F-Scores of 9 could have vastly different growth prospects.
Combining with Other Metrics
The F-Score works best as a filter, not a standalone signal. Combine it with:
- Altman Z-Score for bankruptcy risk assessment
- Beneish M-Score for earnings manipulation detection
- Valuation metrics (P/E, EV/EBITDA, DCF) to find cheap AND healthy stocks
This "Quality Triple Check" approach - using Piotroski, Altman, and Beneish together - gives you a much more complete picture of financial health than any single metric.
Academic Reference
Piotroski, J.D. (2000). "Value Investing: The Use of Historical Financial Statement Information to Separate Winners from Losers." Journal of Accounting Research, 38, 1-41. DOI: 10.2307/2672906
I'm Javier Sanz, a software engineer and value investor. I build tools for fundamental analysis at ValueMarkers.
Top comments (0)