"""Synthetic editorial arithmetic, not a CRM production scoring engine.""" import csv import json from collections import defaultdict from pathlib import Path WEIGHTS = {'demo': 20, 'pricing': 10, 'webinar': 10} CAPS = {'demo': 30, 'pricing': 10, 'webinar': 20} ROOT = Path(__file__).parent def decay(age): if age < 0: raise ValueError('Future events are not valid evidence') return 1 if age <= 30 else .5 if age <= 60 else 0 def score(rows): events = {} for row in rows: key = tuple(row[k] for k in ('account_id','product_id','motion','occurrence_id')) value = row['event_type'], int(row['age_days']) if key in events and events[key] != value: raise ValueError('Conflicting occurrence requires review') events[key] = value grouped = defaultdict(lambda: defaultdict(float)) for key, (kind, age) in events.items(): grouped[key[:3]][kind] += WEIGHTS[kind] * decay(age) return {key: sum(min(value, CAPS[kind]) for kind,value in kinds.items()) for key,kinds in grouped.items()} rows = list(csv.DictReader((ROOT/'scoring-fixture.csv').open())) key = ('A17','P1','new-business') assert sum(WEIGHTS[r['event_type']] for r in rows) == 110 unique = {r['occurrence_id']:r for r in rows} assert len(unique) == 5 assert sum(WEIGHTS[r['event_type']] for r in unique.values()) == 70 assert sum(WEIGHTS[r['event_type']]*decay(int(r['age_days'])) for r in unique.values()) == 60 assert score(rows)[key] == 50 assert score(list(reversed(rows))) == score(rows) assert score(rows+rows) == score(rows) assert [decay(x) for x in (0,30,31,60,61)] == [1,1,.5,.5,0] other = [{**r,'product_id':'P2'} for r in rows] assert score(rows+other) == {key:50,('A17','P2','new-business'):50} try: score(rows+[{**rows[0],'age_days':'6'}]) except ValueError: pass else: raise AssertionError('Conflicting event silently accepted') print(json.dumps({'synthetic':True,'source_rows':7,'unique_occurrences':5, 'raw_points':110,'deduplicated_points':70,'decayed_points':60, 'capped_points':50,'fit':'strong (separate)','role_coverage':'2/3 specified roles (separate)', 'reorder_replay_boundaries_product_separation_conflict_checks':'pass'}, indent=2))