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.
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.
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.
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.
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:
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.
Related
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.