Skip to content

Win Alignment Scoring

Methodology Disclosure — Read This First

The score produced by score_win_probability() is a self-assessment alignment score, not a win probability. See Methodology for a full disclosure of what this tool models, what it does not model, and the limitations of scoring against published criteria.


Overview

score_win_probability() evaluates your application against the CDFI Fund's published CY 2024-2025 Review Process criteria. It returns a WinProbabilityScore with:

  • Section scores for Business Strategy (0–50) and Community Outcomes (0–50)
  • Priority Points bonus (0–10)
  • An aggregate base score (0–100) and aggregate with priority (0–110)
  • A tier classification: Not Qualified, Highly Qualified, or Top Tier
  • Gating notes explaining any section minimums not met
  • Phase 2 flags for qualitative factors outside the scope of this tool

For the canonical scoring framework — sub-score formulas, threshold tables, and methodology disclosure — see Methodology.


Basic usage

from nmtcapp import Application, CDEProfile, Pipeline

app = Application(cde=CDEProfile.sample(), requested_allocation=65_000_000)
app.add_pipeline(Pipeline.from_csv("pipeline.csv"))

score = app.score_win_probability()
print(score.summary())

score_win_probability() runs analyze() internally (or reuses the cached result if analyze() was already called). The CDEProfile.extra dict is automatically passed as cde_attributes, so any CDE-level scoring inputs in your YAML file are picked up without extra code.


Reading the score output

score = app.score_win_probability()

# Tier and aggregate
print(score.tier)                           # "Not Qualified" | "Highly Qualified" | "Top Tier"
print(score.aggregate_base_score)           # 0–100 (Business Strategy + Community Outcomes)
print(score.aggregate_with_priority)        # 0–110 (includes Priority Points bonus)

# Section totals
print(score.business_strategy["section_total"])    # 0–50
print(score.community_outcomes["section_total"])   # 0–50
print(score.priority_points["section_total"])      # 0–10

# Section scores normalized to 0–100 (for charts and comparisons)
print(score.dimensional_scores)
# {
#   "business_strategy":  86.0,   # 43/50 × 100
#   "community_outcomes": 94.0,   # 47/50 × 100
#   "priority_points":    90.0,   # 9/10 × 100
# }

# Gating notes (non-empty only when a section minimum is not met)
for note in score.tier_gating_notes:
    print(note)

# Phase 2 flags (informational — not scored by this tool)
print(score.phase2_flags)

# Backward-compatible fields (maintained for existing callers)
print(score.composite_score)    # float alias for aggregate_base_score
print(score.competitive_tier)   # legacy string: "strong" | "competitive" | "weak"

# JSON-safe dict
data = score.to_dict()

Tier classification

The CDFI Fund uses section minimums to gate applications into the "Highly Qualified" pool that advances to Phase 2 review. It publishes one gate, in the first two rows. The third row is this tool's own label and is marked as such.

Tier Aggregate Base Score Section Minimums Phase 2? Whose threshold
Not Qualified < 85, or either section < 40 No CDFI Fund
Highly Qualified 85–94 Both sections ≥ 40/50 Yes CDFI Fund
Top Tier ≥ 95 Both sections ≥ 45/50 Yes — same pool This tool

"Top Tier" is not a CDFI Fund tier. The Review Process (p.3, Step 2) publishes the Highly Qualified gate — 40 per section and an aggregate base score of 85 — and nothing above it; "Top Tier" returns zero hits across the Allocation Application (142 pp.), the Review Process (7 pp.) and the CY 2024-2025 NOAA (10 pp.). The 95/45 cut points are an unsourced house heuristic, and an application in this row is in the same Highly Qualified pool as the row above it, not a further one. See Methodology.

The section minimums are gating, not just weighted. An application with 92 aggregate but 38 in Community Outcomes is Not Qualified — it does not advance to Phase 2 regardless of the aggregate.

if score.tier == "Not Qualified":
    print("Does not advance to Phase 2.")
    for note in score.tier_gating_notes:
        print(f"  Gap: {note}")
elif score.tier == "Highly Qualified":
    print("Advances to Phase 2. Award depends on Phase 2 outcome and pool ranking.")
elif score.tier == "Top Tier":
    print("High probability of Phase 2 advancement.")

Providing CDE-level attributes

Pipeline data alone is not sufficient to score all sub-criteria. Business Strategy and Community Accountability require CDE-level inputs (product terms, track record, governance) that are not derivable from a project CSV.

There are two ways to supply these inputs:

Place scoring inputs under any key in your cde_profile.yaml. They are loaded into CDEProfile.extra and passed to the scoring model automatically:

# cde_profile.yaml — scoring inputs (any top-level keys not in CDEProfile fields
# flow into CDEProfile.extra and are forwarded to WinProbabilityModel)
products_below_market_pct: 0.55
products_flexible_indicia_count: 6
prior_award_count: 4
years_in_operation: 8
has_own_capital_at_risk: true
pipeline_pct_identified: 0.92
track_record_pipeline_alignment_pct: 0.85
track_record_deployment_pct: 0.94
has_third_party_validation: true
lic_board_representation_pct: 0.44
has_community_engagement_track_record: true
unrelated_entities_pct: 0.95
dbc_focus_years: 6
dbc_dollar_volume_pct: 0.78

Then load and score normally — no extra code needed:

cde = CDEProfile.from_yaml("cde_profile.yaml")
app = Application(cde=cde, requested_allocation=65_000_000)
app.add_pipeline(pipeline)
score = app.score_win_probability()   # cde.extra passed automatically

See nmtcapp/templates/cde_profile_sample.yaml and nmtcapp/templates/pipeline_template.xlsx for the full field list.

Option B — inline dict

Pass cde_attributes directly to WinProbabilityModel().score() when calling the model layer directly:

from nmtcapp.intelligence.win_probability import WinProbabilityModel
from nmtcapp.intelligence.pipeline_analyzer import PipelineAnalyzer

result = PipelineAnalyzer().analyze(pipeline)
score = WinProbabilityModel().score(
    result,
    requested_allocation=65_000_000,
    cde_attributes={
        "products_below_market_pct": 0.55,
        "prior_award_count": 4,
        "years_in_operation": 8,
        # ... other CDE-level inputs
    }
)

Graceful degradation

When CDE-level inputs are absent, each affected sub-score defaults to 0 rather than raising an error. This means:

  • Missing products_below_market_pct and products_flexible_indicia_count → Product Flexibility scores 0/10
  • Missing track record fields → Track Record Strength and Alignment score 0/25
  • Missing governance fields → Community Accountability scores 0/10

The aggregate score will be lower than the true CDE score, but the tool will not crash. The score.tier_gating_notes will explain which section minimums were not met.

Pipeline-derived fallbacks: Some CDE-level inputs can be inferred from pipeline flags when not supplied directly:

CDE attribute Pipeline fallback
products_below_market_pct Fraction of QEI where is_below_market_rate = True
unrelated_entities_pct Fraction of QEI where is_unrelated_entity = True
pct_persistent_poverty From distress_analysis (requires is_persistent_poverty flag)
pct_us_territories From distress_analysis (requires is_us_territory flag)

Using the score to prioritize improvements

The section breakdown is the most actionable output. To identify where to focus:

bs = score.business_strategy
co = score.community_outcomes
pp = score.priority_points

print(f"Business Strategy:   {bs['section_total']}/50  (HQ minimum: 40)")
print(f"Community Outcomes:  {co['section_total']}/50  (HQ minimum: 40)")
print(f"Priority Points:     {pp['section_total']}/10")

# Sub-score drill-down: find the largest gaps
sub_scores = {
    "Product Flexibility (BS)":     (bs.get("product_flexibility", 0),    10),
    "Pipeline Credibility (BS)":    (bs.get("pipeline_credibility", 0),   15),
    "Track Record Strength (BS)":   (bs.get("track_record_strength", 0),  15),
    "Track Record Alignment (BS)":  (bs.get("track_record_alignment", 0), 10),
    "Higher Distress (CO)":         (co.get("higher_distress_targeting", 0), 15),
    "Deep Distress (CO)":           (co.get("deep_distress_commitment", 0),  10),
    "Special Targeting (CO)":       (co.get("special_targeting", 0),          5),
    "Outcomes Quality (CO)":        (co.get("community_outcomes_quality", 0), 10),
    "Accountability (CO)":          (co.get("community_accountability", 0),   10),
}
for name, (actual, max_pts) in sorted(sub_scores.items(), key=lambda x: x[1][0] / x[1][1]):
    gap = max_pts - actual
    print(f"  {name:<35} {actual:4.1f}/{max_pts}  (gap: {gap:.1f})")

Then use app.recommendations() for specific, quantified actions per gap. Each recommendation includes a citation field pointing to the relevant CDFI Fund Review Process section.


Combining with recommendations and optimization

# Get recommendations tied to the current score
recs = app.recommendations()
print(recs.summary())

for r in recs.recommendations:
    if r.priority in ("critical", "high"):
        print(f"[{r.priority.upper()}] {r.category}: {r.finding}")
        print(f"  Action:   {r.action}")
        print(f"  Estimate: {r.quantified_improvement}")
        if r.citation:
            print(f"  Citation: {r.citation}")

# Optimize project subset to maximize competitive alignment
from nmtcapp.optimizer import OptimizationConstraints
result = app.optimize_pipeline(
    constraints=OptimizationConstraints(max_total_qei=65_000_000, min_states=5)
)
print(f"Score: {result.alignment_score_before*100:.0f}{result.alignment_score_after*100:.0f}")

Phase 2 flags

score.phase2_flags reports qualitative factors that cannot be scored from pipeline data. They are informational only — they do not affect the Phase 1 aggregate score.

for flag, value in score.phase2_flags.items():
    print(f"{flag}: {value}")

The keys, and what each one is:

Key Type Source
non_metro_commitment_pct float or None The CDE's own declaration, passed through from cde_attributes["non_metro_commitment_pct"] — its answer to Question 22(c). None if it declared nothing. Nothing computes this.
non_metro_pipeline_qei_pct float Measured. The share of pipeline QEI in verified Non-Metropolitan Counties, as a percentage. A characterisation of the pipeline, not an answer to Question 22 — see Methodology.
non_metro_undetermined_qei_pct float Measured. The share of pipeline QEI whose county status could not be determined. Not metropolitan.
favorable_fee_structure bool or None From cde_attributes["has_favorable_fee_structure"].
prior_reporting_compliance_risk bool From cde_attributes["has_prior_reporting_issues"]; defaults to False.

Corrected in 1.4.0. This section described the dict as "boolean flags" and named four keys — non_metro_commitment_risk, fee_structure_risk, prior_reporting_compliance_risk and capitalization_risk. Three of the four have never existed in any release, and the dict has never been all booleans. The example above no longer prints a ⚠️/✓ verdict either, because two of these values are percentages and one is a None-able declaration, none of which is truthy-testable as a risk.

non_metro_meets_minimum was removed in the same round: it reported rural_pct >= 0.20, and there is no 20% threshold an individual Applicant must clear. See Methodology.