fintech-algorithms
Using a coding agent? Give it the skill: npx skills add IslamBaraka90/Fintech-Algorithms-Library What it does →

Gains, Lift, and Decile Capture

Install and import#

bash
npm install fintech-algorithms
ts
import { gainsLiftAndDecileCapture } from "fintech-algorithms/model-validation-and-backtesting/classification-and-score-validation/gains-lift-and-decile-capture";

Signature#

gainsLiftAndDecileCapture(inputs)

Ranks records by score, cuts the ranking into equal-count buckets, and reports how much of the total event weight each bucket and each cumulative prefix captures, as a share and as a lift over prevalence.

Parameters#

NameTypeNotes
inputs{ records: Array<{ id: string; label: 0 | 1; score: number; weight?: number; score_available_at?: string; label_available_at?: string }>; evaluation_cutoff?: string; buckets?: number }The scored population plus the bucket count. Every record needs a unique nonempty id, a label that is exactly the number 0 or 1, and a finite score; weight defaults to 1 and must be positive. buckets defaults to 10 and must be an integer between 2 and the number of records. If evaluation_cutoff is supplied, any score_available_at or label_available_at on a record is compared against it as a string and must not sort after it.
buckets: integer between 2 and the record count, default 10 · weight: positive, default 1

Returns#

{ buckets: Array<{ bucket: number; rank_start: number; rank_end: number; record_count: number; population_weight: number; event_weight: number; population_share: number; event_capture: number; bucket_lift: number; cumulative_population: number; cumulative_gain: number; cumulative_lift: number }>; top_decile_capture: number; overall_prevalence: number; total_events: number; total_weight: number; bucket_count: number; tie_break: string; state: string }

One entry per bucket, ordered from the highest-scoring, with its rank span, its own capture and lift, and the running cumulative gain and lift. top_decile_capture is the first bucket's event_capture, overall_prevalence the event weight over total weight, tie_break names the ordering rule (score-descending-id-ascending), and state is ranking-evaluated.

Errors#

  • When records is absent, not an array, or empty — throws RangeError
  • When a record id is missing, empty, or repeats an earlier one — throws RangeError
  • When a label is anything other than the number 0 or 1 — throws RangeError
  • When a weight is zero or negative — throws RangeError
  • When buckets is not an integer — throws RangeError
  • When buckets is below 2 or above the record count — throws RangeError
  • When the total event weight is 0, so lift has no denominator — throws RangeError
  • When score_available_at or label_available_at sorts after evaluation_cutoff — throws RangeError

Complexity: time O(n log n), space O(n).

Worked example#

verified This is the worked example published in the article, replayed by the test suite on every run. The output cannot drift.

Input#

inputs
{
  "records": [
    {
      "id": "R01",
      "label": 1,
      "score": 0.95,
      "probability": 0.92,
      "weight": 1,
      "sector": "Banking",
      "country": "Egypt",
      "regime": "Expansion",
      "score_available_at": "2025-01-01T00:00:00Z",
      "label_available_at": "2026-01-01T00:00:00Z"
    },
    {
      "id": "R02",
      "label": 0,
      "score": 0.9,
      "probability": 0.88,
      "weight": 1,
      "sector": "Insurance",
      "country": "Egypt",
      "regime": "Expansion",
      "score_available_at": "2025-01-01T00:00:00Z",
      "label_available_at": "2026-01-01T00:00:00Z"
    },
    {
      "id": "R03",
      "label": 1,
      "score": 0.9,
      "probability": 0.84,
      "weight": 1,
      "sector": "Markets",
      "country": "Saudi Arabia",
      "regime": "Expansion",
      "score_available_at": "2025-01-01T00:00:00Z",
      "label_available_at": "2026-01-01T00:00:00Z"
    }
  ],
  "evaluation_cutoff": "2026-06-30T00:00:00Z",
  "buckets": 10
}

Call#

gainsLiftAndDecileCapture(inputs)

Returns#

object with 8 fields: buckets, top_decile_capture, overall_prevalence, total_events, total_weight, bucket_count, tie_break, state

{
  "buckets": [
    {
      "bucket": 1,
      "rank_start": 1,
      "rank_end": 3,
      "record_count": 3,
      "population_weight": 3,
      "event_weight": 2,
      "population_share": 0.125,
      "event_capture": 0.2,
      "bucket_lift": 1.5999999999999999,
      "cumulative_population": 0.125,
      "cumulative_gain": 0.2,
      "cumulative_lift": 1.6
    },
    {
      "bucket": 2,
      "rank_start": 4,
      "rank_end": 5,
      "record_count": 2,
      "population_weight": 2,
      "event_weight": 1,
      "population_share": 0.08333333333333333,
      "event_capture": 0.1,
      "bucket_lift": 1.2,
      "cumulative_population": 0.20833333333333334,
      "cumulative_gain": 0.3,
      "cumulative_lift": 1.44
    },
    {
      "bucket": 3,
      "rank_start": 6,
      "rank_end": 8,
      "record_count": 3,
      "population_weight": 3,
      "event_weight": 2,
      "population_share": 0.125,
      "event_capture": 0.2,
      "bucket_lift": 1.5999999999999999,
      "cumulative_population": 0.3333333333333333,
      "cumulative_gain": 0.5,
      "cumulative_lift": 1.5
    }
  ],
  "top_decile_capture": 0.2,
  "overall_prevalence": 0.4166666666666667,
  "total_events": 10,
  "total_weight": 24,
  "bucket_count": 10,
  "tie_break": "score-descending-id-ascending",
  "state": "ranking-evaluated"
}

Other exports#

This module also exports rocCurveAndRocAuc, precisionRecallCurveAndPrAuc, brierScore, logLoss, reliabilityDiagramAndExpectedCalibrationError, costSensitiveThresholdOptimization, scoreStabilityAndMigrationMatrix, sliceBasedValidationBySectorCountryAndRegime, rareEventBacktestAndConfidenceBounds, calculate. Every module additionally exports run as an alias of its primary function, and a meta object carrying its catalog id, domain, family, shape and article URL.

Diagrams#

Gains, Lift, and Decile Capture — article hero
Gains, Lift, and Decile Capture — decision boundaries
Gains, Lift, and Decile Capture — method selection
Gains, Lift, and Decile Capture — system map

Calculation flow#

Gains, Lift, and Decile Capture calculation flow
flowchart LR
    S1["Validate identities scores labels buckets and cutoff"]
    S2["Sort by descending score then ascending ID"]
    S3["Assign each zerobased rank to floorrankDn"]
    S4["Count records and events per bucket"]
    S5["Calculate bucket and cumulative shares"]
    S1 --> S2
    S2 --> S3
    S3 --> S4
    S4 --> S5
    S5 --> D{"bucket count and tiebreak must stay fixed"}
    D --> O["top_decile_capture + diagnostics"]
    O --> A["Audit: Bucket record counts sum to n and cumulative gain ends at "]

How it works#

This page states the contract — how to call it correctly. The article explains the concept: why it works, and where it breaks.

Read the article →

References#

The rest of the Classification and Score Validation family#