Debugging Split-Sum Mismatches in Royalty Calculations

A split-sum mismatch is the single most common defect an engineer will chase down while working through Troubleshooting Royalty Metadata Failures: writer, publisher, or master shares for a work that add up to 99.98%, 100.4%, or some other value that is not exactly 100%. The gap looks tiny, but at catalog scale it either underpays every contributor on a work by a fraction of a cent per stream — invisible until an auditor totals a reporting_period — or, in the over-100% case, silently overpays one claimant at another’s expense. This guide focuses specifically on detecting these mismatches reliably and fixing the four causes that produce them: floating-point accumulation, controlled-composition caps, missing contributors, and over-100% double claims.

Rounding Strategy Trade-offs

The choice of how shares are stored, summed, and rounded determines whether a mismatch is a real data defect or an artifact of arithmetic. The following compares the common approaches:

Approach Precision behavior Implementation cost Royalty Impact
float addition Accumulates binary rounding error across many small shares (e.g. six writers at 16.67% each) Lowest — native type, no library High risk: sums can drift by fractions of a cent per stream, compounding across millions of plays into real underpayment or overpayment
Decimal with fixed 2-place quantization Exact for currency-like values, but a share like 1/3 must be truncated or rounded before storage Low — standard library decimal module Low risk if a documented remainder-allocation rule (e.g. largest-remainder method) assigns the leftover cent deterministically
Decimal with higher precision (4–6 places) internally, rounded only at payout Preserves exact fractional shares through calculation, rounds once at the final step Medium — requires consistent precision context across all pipeline stages Lowest risk: avoids compounding rounding error across multiple ETL stages, but requires strict schema enforcement so no stage silently truncates
Fixed absolute tolerance (e.g. ±0.005) on the 100% check Tolerant of legitimate sub-cent rounding, but can mask real missing-contributor defects if set too loose Low — one constant to tune Moderate risk: too tight causes false-positive quarantines on every batch; too loose lets genuine underpayment defects pass the gate silently

Prerequisites & Assumptions

The patterns below assume Python 3.11+, pydantic>=2.0 for share validation, and polars>=0.20 for batch-level aggregation. Shares are assumed to be stored as Decimal from the point of ingestion onward — if an upstream system still emits float values, convert via Decimal(str(value)) immediately, never Decimal(value), to avoid importing the source float’s own binary imprecision. Each share row carries iswc, payee_id, right_share_pct, and territory_code, matching the schema enforced across the broader Metadata Taxonomy Best Practices reference.

Implementation

Step 1: Normalize incoming shares to Decimal at ingestion

Never let a float-typed share reach a reconciliation query. Coerce and validate at the pydantic model boundary so a malformed value fails fast with a clear error rather than silently propagating.

python
from decimal import Decimal, InvalidOperation
from pydantic import BaseModel, field_validator

class RightShare(BaseModel):
    iswc: str
    payee_id: str
    right_share_pct: Decimal

    @field_validator("right_share_pct", mode="before")
    @classmethod
    def coerce_decimal(cls, value):
        if isinstance(value, float):
            value = str(value)
        try:
            return Decimal(value)
        except InvalidOperation as exc:
            raise ValueError(f"unparseable share value: {value!r}") from exc

Step 2: Group and sum per work per territory, per right type

Splits are scoped to a right type (mechanical, performance, master) and often to a territory, since some agreements carve out different shares by region. Summing across the wrong scope is itself a common source of false-positive mismatches.

python
import polars as pl

def split_sums(shares: pl.DataFrame) -> pl.DataFrame:
    return (
        shares.group_by(["iswc", "right_type", "territory_code"])
        .agg(pl.col("right_share_pct").sum().alias("total_pct"),
             pl.col("payee_id").n_unique().alias("payee_count"))
    )

Step 3: Apply a tolerance-based assertion, not exact equality

Legitimate rounding from largest-remainder allocation can leave a sum a fraction of a cent from exactly 1.00. Use an explicit tolerance constant rather than either exact equality or an unbounded “close enough” check.

python
from decimal import Decimal

TOLERANCE = Decimal("0.005")  # half a cent, tuned per catalog

def find_mismatches(sums: pl.DataFrame) -> pl.DataFrame:
    return sums.filter(
        (pl.col("total_pct") - Decimal("1.00")).abs() > TOLERANCE
    )

Step 4: Distinguish under-100% from over-100% before routing a fix

A sum below tolerance usually means a missing contributor row; a sum above tolerance usually means a double claim or an unreleased controlled-composition cap. Route each case differently rather than applying one generic “rebalance” fix.

python
def classify(total_pct: Decimal) -> str:
    if total_pct < Decimal("1.00") - TOLERANCE:
        return "under_claimed"      # likely missing contributor
    if total_pct > Decimal("1.00") + TOLERANCE:
        return "over_claimed"       # likely double claim or cap violation
    return "within_tolerance"

Verification & Validation

Before any corrected batch re-enters the payout stream, run an assertion harness that must pass with zero exceptions:

python
def assert_batch_balanced(shares: pl.DataFrame) -> None:
    sums = split_sums(shares)
    mismatches = find_mismatches(sums)
    assert mismatches.height == 0, (
        f"{mismatches.height} work/territory/right_type combinations "
        f"outside tolerance: {mismatches['iswc'].to_list()[:10]}"
    )

Run this harness as a required gate in CI against a fixture catalog with known-good and known-bad splits, and again against every production batch immediately before payout calculation. Log the count of payee_count per work alongside the sum — a work with total_pct near 1.00 but only one payee_id on a composition known to have three writers is a signal worth flagging even when the tolerance check technically passes. Keep a rolling record of how many works fail the harness per batch; a sudden spike almost always indicates a schema change further upstream rather than a batch of coincidentally bad catalog data, and should trip investigation before the batch is quarantined row by row.

Edge Cases & Gotchas

A controlled-composition cap — common in US mechanical licensing, where a compulsory rate caps the payable mechanical royalty regardless of the number of writers — can make a technically correct 100%-summing split look like an underpayment when compared against a naive per-stream rate expectation; validate against the cap explicitly rather than assuming any sub-100%-equivalent payout is a defect. A NULL right_share_pct on a contributor row (as opposed to a missing row entirely) will break a naive sum() silently in some SQL engines by ignoring the null rather than failing, so explicit null checks before aggregation are required. Watch for shares recorded against a stale iswc after a work-code merge — the sum can look correct because it is only summing the shares still attached to the superseded code, missing the ones re-pointed to the canonical work.

The classification logic above assumes contributor roles and share types are modeled consistently to begin with; see Modeling Contributor Roles and Splits for the schema that makes per-territory, per-right-type grouping possible. For the full set of related failure modes and their diagnostics, return to Troubleshooting Royalty Metadata Failures.