DEV Community

Javier
Javier

Posted on • Originally published at valuemarkers.com

Implementing the Piotroski F-Score in Python: A Complete Guide for Quantitative Investors

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"
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Limitations to Keep in Mind

The F-Score isn't perfect:

  1. Backward-looking: It compares this year to last year. A company recovering from a bad year scores well even if it's still mediocre.

  2. 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.

  3. 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.

  4. 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)