Every grade on this site is a weighted sum of five published dimensions. You do not have to take that on trust. This page is the whole calculation, and the same code is downloadable as a notebook you can run against the live API.

Download the notebook (Jupyter, nbformat 4.5). It reads one public file and imports nothing from our codebase, because a check that needed our code would not be a check.

Reproduce the Cancellation Friction Index

Cancel Atlas publishes a grade for every company in its index, and claims that anyone can recompute those grades from published weights and cited evidence. This notebook is that claim, written out so you can run it.

It uses one file, api/v1/companies.json, which is public and CORS-open. It imports nothing from the Cancel Atlas codebase. If it needed our code, the claim would be unverifiable.

The dataset is licensed CC BY-SA 4.0.

import json, os, urllib.request

# Point this anywhere. The default is the live public API, so this notebook runs as-is for anyone.
SOURCE = os.environ.get('CANCEL_ATLAS_SOURCE', 'https://www.cancelatlas.com/api/v1/companies.json')

def load(src):
    if src.startswith('http'):
        req = urllib.request.Request(src, headers={'User-Agent': 'reproduce-the-index/1.0'})
        with urllib.request.urlopen(req, timeout=60) as r:
            return json.load(r)
    with open(src, encoding='utf-8') as f:
        return json.load(f)

data = load(SOURCE)
print('methodology', data['methodology_version'], '| companies', data['total_count'])
print('licence    ', data['license'])

The weights are published, not asserted

The five dimensions and their weights ship inside the same file as the scores. Nothing here is hard-coded from the site's prose.

weights = {d['key']: d['weight'] for d in data['policy_dimensions']}
for d in data['policy_dimensions']:
    print(f"  {d['weight']:>3}  {d['key']:<18} {d['label']}")
print('  ---  total', sum(weights.values()))
print()
print('formula:', data['score_formula'])
print('bands  :', data['grade_rule'])

Recompute every score

The formula is a weighted mean of the five 0-100 dimension scores, rounded. Note that a plain unweighted mean gives a different answer: Netflix averages to 85 but weights to 86. The weights are the point.

BANDS = [(85, 'A'), (70, 'B'), (55, 'C'), (40, 'D'), (0, 'F')]

def score_of(subs):
    return round(sum((weights[k] / 100) * v for k, v in subs.items() if k in weights))

def grade_of(score):
    return next(g for lo, g in BANDS if score >= lo)

agree = disagree = 0
problems = []
for c in data['companies']:
    p = c.get('policy') or {}
    subs, published, published_grade = p.get('scores'), p.get('score'), p.get('grade')
    if not subs or published is None:
        problems.append((c['id'], 'no published sub-scores'))
        continue
    mine = score_of(subs)
    if mine != published:
        disagree += 1
        problems.append((c['id'], f'score {published} published, {mine} recomputed'))
    elif grade_of(mine) != published_grade:
        disagree += 1
        problems.append((c['id'], f'grade {published_grade} published, {grade_of(mine)} from bands'))
    else:
        agree += 1

print(f'reproduced exactly : {agree}')
print(f'disagreed          : {disagree}')
for cid, why in problems[:10]:
    print('   ', cid, why)

One company, worked by hand

So you can check the arithmetic yourself rather than trusting the loop.

c = next(x for x in data['companies'] if x['id'] == 'netflix')
subs = c['policy']['scores']
total = 0.0
for k, v in subs.items():
    part = (weights[k] / 100) * v
    total += part
    print(f"  {k:<18} {v:>3} x {weights[k]:>2}% = {part:6.2f}")
print(f"  {'':<18} {'':>3}          {total:6.2f}  -> {round(total)}")
print()
print('published:', c['policy']['score'], c['policy']['grade'])
print('unweighted mean would be:', round(sum(subs.values()) / len(subs)))

Every grade carries its evidence

A score is only meaningful if you can check what it was read from. Each record cites the pages it was graded against, with the date they were read.

for s in c['policy'].get('sources', []):
    print(' -', s.get('title'))
    print('  ', s.get('url'))
print()
print('checked:', c['policy'].get('policy_checked'))
print('tier   :', c['policy'].get('evidence_tier'))

What this does and does not show

It shows the published grades follow from the published weights and the published sub-scores, arithmetically, with no hidden step.

It does not show the sub-scores are the right reading of each company's policy page. That is a judgement made against cited, dated evidence, and the citations above are there so you can dispute any of it. The index grades what a company documents, not what cancelling actually feels like.