When to Use
Use this skill in sustainable quantitative investing, multi-factor portfolio construction, and ESG risk overlays, when you hold ESG assessments from more than one vendor for the same issuer and need a single defensible number plus an explicit measure of how much the vendors disagree.
ESG ratings from different providers measure different things. Berg, Kölbel & Rigobon (2022, Review of Finance 26(6), 1315–1344) report pairwise correlations between six major raters ranging from 0.38 to 0.71, and decompose the divergence into measurement (56%), scope (38%) and weight (6%). Because scope alone accounts for well over a third of it, the disagreement between vendors is itself information — a name the vendors cannot agree on is a name whose ESG score should not carry conviction in either direction.
This module normalizes each vendor's native scale to $[0.0, 1.0]$ using that vendor's own published band structure, computes the consensus $\bar{S}$ and the dispersion $\sigma_{\text{esg}}$, gates directional signals on both vendor coverage and vendor agreement, and carries rule-based exclusion flags through to the audit report.
When NOT to Use
- As a standalone trading signal. This is a research feature generator. It has no return model, no position sizing, and no calibration. The $0.75$ / $0.30$ / $0.25$ thresholds are illustrative defaults, not empirically validated constants.
- To build a cross-sectional ESG factor from nominal vendor scales. The three vendors use incompatible reference frames (see Prerequisites). Rescaling nominal scales makes them arithmetically averageable, not conceptually comparable. For a cross-sectional factor, rank each vendor's raw scores within your universe on a common date and feed those percentiles in via
msci_rating_map/ pre-normalized inputs. This engine scores one issuer at a time and cannot compute universe percentiles for you. - As a compliance screen.
EXCLUDED_SECTORreports exclusions you have already determined elsewhere. The engine evaluates no revenue thresholds, no treaty lists, and no UNGC/OECD violation status. A high consensus ESG score is not evidence that an issuer passes an exclusion screen — the two are independent tests. - On a single vendor. With one opinion the dispersion is zero by construction; "the vendors agree" is unfalsifiable. The engine returns
INSUFFICIENT_VENDOR_COVERAGErather than a signal. - On a mixed-vintage vendor panel in a backtest without a point-in-time store. See the look-ahead pitfall below.
Prerequisites
- Security identifier (
ticker) and raw vendor scores in vendor-native units — the engine does the normalization, so do not pre-scale. - Awareness that the three inputs are benchmarked differently, which is the scope component of the divergence above:
- MSCI ESG Ratings — seven letter bands
AAA…CCC, derived from a 0–10 final Industry-Adjusted Score split into seven equal 1.4286-wide bands. MSCI states these are "explicitly intended to be interpreted relative to a company's industry peers." - Morningstar Sustainalytics ESG Risk Rating — unmanaged-risk score where 0 is best. Published bands: Negligible 0–9.99, Low 10–19.99, Medium 20–29.99, High 30–39.99, Severe 40 and higher. Sustainalytics states "These risk categories are absolute, i.e. comparable across sectors."
- LSEG ESG Scores (formerly Refinitiv / ASSET4) — 0–100 where 100 is best, benchmarked against the TRBC industry group for environmental and social categories and against the country of incorporation for governance.
- MSCI ESG Ratings — seven letter bands
- Rule-based exclusion status determined upstream, passed as
has_controversial_weaponsand/orexclusion_reasons. - A timezone-aware
as_ofvintage stamp if the output feeds a backtest. Naive datetimes are rejected. - Python 3.10+ standard library only. No third-party dependency.
Workflow
- Vendor Score Normalization — each vendor is mapped to $[0.0, 1.0]$ using its own published band structure, not a nominal min/max:
- MSCI: the letter identifies an interval of the 0–10 Industry-Adjusted Score, so the point estimate is the band mid-point, $(2k+1)/14$ for $k=0$ (
CCC) to $6$ (AAA) —CCC$=0.0714$,BBB$=0.5$,AAA$=0.9286$. Mapping the letters to band end-points ($\text{AAA}=1.0$, $\text{CCC}=0.0$) overstates both tails by ~7 points and mechanically inflates dispersion against continuously-scored vendors. - Sustainalytics: $1.0 - \frac{\min(\text{Risk},,40)}{40}$. The rescale is bounded at the Severe-band floor, not at 100. Severe Risk is open-ended above 40, so 100 is a nominal bound nobody observes; dividing by it would map a Severe issuer scoring 45 to $0.55$ — above the mid-point of the normalized scale — and no realistic issuer could ever reach the laggard band. Override with
sustainalytics_severe_thresholdif your universe justifies a different floor. - LSEG/Refinitiv: $\frac{\text{Score}}{100}$ — already reported on the target scale.
- An unrecognised MSCI token (
'A+','AAAA'), a NaN, an infinity, or an out-of-range score raises rather than returningNoneor clipping.Nonemeans "this vendor does not cover this issuer" and nothing else.
- MSCI: the letter identifies an interval of the 0–10 Industry-Adjusted Score, so the point estimate is the band mid-point, $(2k+1)/14$ for $k=0$ (
- Consensus & Dispersion Calculation over the $K$ covering vendors:
- $\bar{S} = \frac{1}{K}\sum S_k$; $\quad \sigma_{\text{esg}} = \sqrt{\frac{1}{K}\sum (S_k - \bar{S})^2}$ (population form, deviations taken about the unrounded mean).
- Both are
Nonewhen undefined — $\bar{S}$ with no coverage, $\sigma_{\text{esg}}$ with a single vendor. They are never reported as $0.0$, because a downstream factor ranker reads a zero consensus as the worst ESG name in the universe rather than as "not measured".
- Signal Classification (precedence order, first match wins):
EXCLUDED_SECTOR— any rule-based exclusion was already failed. Overrides every consensus score; the normalized scores are still reported so the audit trail survives.INSUFFICIENT_VENDOR_COVERAGE— fewer thanmin_vendors_for_conviction(default 2) vendors cover the issuer.NEUTRAL_HIGH_DISAGREEMENT— $\sigma_{\text{esg}} > 0.25$. Applied symmetrically: vendors that cannot agree support a laggard call no more than they support a leader call.BULLISH_ESG_LEADER— $\bar{S} \ge 0.75$.BEARISH_ESG_LAGGARD— $\bar{S} \le 0.30$.NEUTRAL— otherwise.
- Threshold Calibration — re-estimate all four thresholds per universe (region, sector, market cap, vendor panel) on out-of-sample data before trading them.
- Audit Report Generation — output
EsgSignalAuditReport, carrying the normalized per-vendor scores,vendor_count,exclusion_reasonsand theas_ofvintage.
Full procedure: see
references/workflows.md. Standards and regulatory touchpoints: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Naively Averaging Un-Normalized Ratings: Averaging an MSCI letter with a Sustainalytics risk number is not just a scale error — the two run in opposite directions, so the arithmetic silently rewards the riskiest names.
- Rescaling by a Nominal Maximum Nobody Observes: Sustainalytics' theoretical ceiling is 100, but its Severe band starts at 40 and is open-ended. Dividing by 100 compresses the entire realised distribution into roughly $[0.55, 1.0]$, biases every consensus upward, and inflates dispersion against vendors that do use their full range.
- Mapping Ordinal Letters to Band End-Points:
AAAdoes not mean "a perfect 10". It means the Industry-Adjusted Score fell somewhere in $[8.571, 10.0]$. Assigning it $1.0$ asserts the top of that interval for every AAA-rated issuer. - Averaging Across Incompatible Reference Frames: MSCI is industry-relative, Sustainalytics is explicitly absolute, LSEG benchmarks governance against the country of incorporation. A consensus across the three is a blend of three different questions; Berg et al. attribute 38% of total divergence to this scope effect. State it as a documented limitation rather than assuming it away.
- Treating a Single Vendor as Corroboration: One score yields $\sigma_{\text{esg}} = 0$ by construction. Reporting that as "low vendor disagreement" claims agreement that was never observed.
- Gating Only the Bullish Side on Disagreement: If the leader signal requires vendor agreement but the laggard signal does not, an issuer rated
CCCby one vendor and 80/100 by another emits a confident short-side ESG signal off maximal disagreement. - Silent NaN and Out-of-Range Propagation: A NaN in a 0–100 field clips to a boundary under
max(0, min(100, x)), so a corrupt feed becomes a perfect score rather than an error. Reject non-finite and out-of-range values at the vendor boundary. - Fabricating a Zero for Missing Data: Reporting
consensus = 0.0for an uncovered or excluded issuer places it at the bottom of any cross-sectional ESG rank. Uncovered is not the same as bad. - Look-Ahead Bias from Retroactively Rewritten ESG History: ESG vendors restate their own past. Berg, Fabisik & Sautner (ECGI Finance Working Paper 708/2020) document that Refinitiv ESG rewrote historical scores such that median overall scores in the rewritten data were 18% lower than in the original vintage (E −44%, S −16%, G −7%), that firms' ESG quantile classifications changed as a result, and that the ESG–return relationship present in the rewritten data is absent in the initial data. They conclude the rewriting is "an ongoing rather than a one-off phenomenon." Backtest against the vintage that was actually observable on the trading date; stamp every record with
as_of. - Survivorship Bias in Vendor Coverage: Today's vendor universe omits issuers that were delisted, acquired, or simply not yet covered. Backtesting on it overstates coverage and biases the sample toward large, well-disclosed issuers.
- Methodology-Version Breaks Inside a Backtest Window: Vendors periodically replace the model, not just the data. LSEG's ESG Scores methodology effective 1 July 2026 describes an "absolute assessment" model, where earlier Refinitiv documentation described percentile-rank scoring. A series spanning such a change is two different variables sharing one column name.
Verification
- Instantiate
EsgDataSignalEngine. Submit MSCI'AAA'($0.9286$), Sustainalytics8.0($0.8$), LSEG/Refinitiv85.0($0.85$): consensus $0.8595$, dispersion $0.0529$, signalBULLISH_ESG_LEADER. - Submit conflicting ratings — MSCI
'AAA'($0.9286$) against Sustainalytics30.0($0.25$): consensus $0.5893$, dispersion $0.3393$, signalNEUTRAL_HIGH_DISAGREEMENT. - Confirm the disagreement gate is symmetric: MSCI
'CCC'($0.0714$), Sustainalytics40.0($0.0$), LSEG/Refinitiv80.0($0.8$) gives consensus $0.2905$ — inside the laggard band — but dispersion $0.3615$, so the emitted signal isNEUTRAL_HIGH_DISAGREEMENT, notBEARISH_ESG_LAGGARD. - Confirm a single vendor returns
INSUFFICIENT_VENDOR_COVERAGEwithvendor_disagreement_dispersion is None, and that an uncovered issuer returnsconsensus_esg_score is Nonerather than $0.0$. - Confirm
normalize_refinitiv(float('nan'))andnormalize_refinitiv(500.0)both raiseValueError, and thatnormalize_msci('A+')raisesValueErrorrather than returningNone. - Confirm
has_controversial_weapons=TrueyieldsEXCLUDED_SECTOReven against anAAA/ 95 panel, while the normalized per-vendor scores remain populated in the report. - Run
python -m unittest discover -s skills/esg-data-signal-research-and-vendor-comparison/scripts.