SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
8.6 KB

# Scoring — metrics, indices, baselines, signals, trends

Owner: services/metrics.py (metrics-v1, CCI cci-v1), services/signals.py (signals-v1), services/trends.py (trends-v1). Parameters live in taxonomy.METRIC_PARAMS / taxonomy.CCI_WEIGHTS / config.Settings; bump the formula version whenever they change (spec §177). Every metrics_current row stores value, confidence, inputs (the numbers the formula saw), formula_version, computed_at; metric_series keeps one point per company/metric/day. No inputs → no row (never 0-as-measured).

Schedules: metrics-hourly (CA_METRICS_CRON, default 7 * * * *) for companies with activity in the last CA_METRICS_ACTIVE_WINDOW_DAYS (90); daily-aggregates (CA_DAILY_CRON, 20 0 * * *) = catch-up of missed days + all-company recompute; signals hourly; trends at :25. CLI: catlas metrics [--all] [--company slug], catlas daily [--day|--catch-up], catlas signals, catlas trends.

Notation: sat(x, k) = 1 − e^(−x/k) (0–1, x=k → 0.63); decay(age) = e^(−age/τ); ages in days.

# Company metrics (metrics_current)

Metric Formula Inputs stored Emitted when
activity_score (0–100) raw = Σ_changes w(kind)·decay(age, τ=10) + Σ_events importance·decay(age, τ) over 30 d, w = meaningful 1 / major 2 / critical 3; density = raw / active_sensors^0.5 (coverage normalisation); score = 100·log1p(density)/log1p(6) capped at 100 changes_30d, events_30d, raw_changes, raw_events, active_sensors, density, τ, exponent, D_max ≥ 1 active sensor or any activity
open_jobs count of jobs.status='open' jobs_new_30d, jobs_removed_30d, remote_ratio, by_country, by_department, ai_open company has a careers/jobs_board sensor or any job rows
hiring_momentum_{7,30,90}d (%) (open_now − open_then)/open_then × 100, open_then reconstructed from first_seen_at ≤ t−N and (removed_at is null or > t−N) open_now, open_then, window (+ 30 d extras) open_then ≥ 3 (hiring_min_listings) — no % from 0 or 2 listings
ai_adoption (0–100) components: jobs min(1, ai_open/open × 2) (w 0.5), events sat(ai_events_90d, 3) (w 0.25: AI_HIRING/AI_LAUNCH/tag ai), keywords sat(hits, 5) (w 0.25: AI_KEYWORDS in product names, 90 d news titles, latest snapshot titles); weights renormalised over available components ai_open, open_jobs, ai_events_90d, keyword_hits, components, weights_used any component has inputs
product_velocity 100·sat(Σ importance·decay(age, 30), 6) over 90 d PRODUCT + changelog/docs/API/SDK subtypes events_90d, surfaces products/changelog/docs/api/developer/services sensor or a PRODUCT event
developer_momentum 100·sat(Σ DEVELOPER importance·decay + 0.5·meaningful dev-surface changes 30 d, 6) events_90d, meaningful_changes_30d, surfaces docs/api/developer/changelog sensor or DEVELOPER event
communication_activity 100·sat(max(news_30d, comm_events_30d) + 0.5·min(·), 8) news_items_30d, events_30d newsroom/blog/feed/IR/research sensor or news rows
pricing_activity 100·sat(Σ PRICING importance·decay + 0.25·pricing changes 30 d, 3) events_90d, meaningful_changes_30d pricing sensor or PRICING event
leadership_activity 100·sat(Σ LEADERSHIP importance·decay, 3) events_90d leadership sensor or LEADERSHIP event
geo_expansion `100·sat(new_locations_90d + 3· new countries + Σ COUNTRY_EXPANSION importance, 4)`; new countries = countries whose first location or first job appeared within 90 d
corporate_change_index (cci-v1) Σ w_i·c_i / Σ w_i over available components with CCI_WEIGHTS (hiring 0.25 · product 0.20 · geo 0.15 · leadership 0.15 · developer 0.10 · communication 0.10 · pricing 0.05); hiring momentum (%) mapped to 50 + clamp(m, −100, 100)/2; confidence 0.4 + 0.6·weight_coverage components, weights, weight_coverage ≥ 1 component
anomaly_score (z) (this_week_meaningful − mean) / max(stddev, 0.5) against baselines.meaningful_changes_weekly this_week, mean, stddev, samples baseline with ≥ 4 samples
historical_coverage (0–100) 100·(0.5·min(1, observed_ok/expected) + 0.3·continuity + 0.2·min(1, surfaces/8)); expected = Σ_sensors age_s / current_interval_s; continuity = days_with_observations / days_since_first_observed observed, expected, days_with_obs, days_since_first, surfaces first_observed_at set and ≥ 1 sensor

Confidence conventions: activity min(0.95, 0.5 + 0.05·sensors); hiring 0.9 with a structured jobs_board sensor else 0.75; AI adoption 0.6 (inferred from public signals only); saturating scores 0.7; coverage 0.8.

# Daily aggregates

compute_daily(day) (UTC day, idempotent upserts):

  • company_daily — observations, changes, meaningful_changes, events (+ events_by_type), jobs_open / jobs_new / jobs_removed / jobs_ai_open at end of day (reconstructed from first_seen_at / removed_at), news_items, sensors_active. Only companies with activity that day get a row.
  • global_daily — sums + companies_active / sensors_active (distinct in observations that day; falls back to active sensors when no observation rows exist), events_by_type, jobs_open, by_country / by_industry ({companies, meaningful_changes, events} per key) and the Global Corporate Activity Index: ratio = meaningful_changes / sensors_active; baseline = mean(ratio) over the trailing 28 days with sensors; activity_index = ratio / baseline × 100 (baseline = 100). Null when no baseline exists yet (never fabricated).
  • baselines — per company, weekly buckets of company_daily over CA_BASELINE_WINDOW_DAYS (56): meaningful_changes_weekly, jobs_new_weekly, news_weekly → population mean / stddev / samples. Requires ≥ 2 observed weeks. Consumed by anomaly_score, HIRING_SURGE and signals.
  • compute_daily_catch_up() fills every day between the last computed day (or the first observation/change/event) and yesterday.

Coverage normalisation (spec §146): activity uses per-sensor density, the global index divides by active sensors and compares to a trailing baseline, trends divide mentions by active companies, and the CCI renormalises over available components — growth of the sensor network alone must not move any index.

# Signals (signals, signals-v1)

Labelled as signals, with strength 0–1, confidence, explanation, evidence (event ids + metric values), window_days, expires_at (CA_SIGNALS_TTL_DAYS = 14). One active row per (company, kind): re-detection updates it, disappearance sets status='expired'.

kind trigger
hiring_surge hiring_momentum_30d ≥ +30 % with ≥ 10 open, or a HIRING_SURGE event in the window
hiring_freeze hiring_momentum_30d ≤ −30 % with ≤ 1 new listing, or a HIRING_FREEZE_SIGNAL event — worded "listings no longer visible"
launch_buildup within 14 d ≥ 3 of {product, docs/API, changelog, careers, messaging} categories including product, or ≥ 4 categories — "Possible launch preparation signal"
expansion COUNTRY_EXPANSION event, or new countries (locations or job countries, 90 d) with location events / new jobs
pricing_migration ≥ 2 PRICING events in 30 d, or a tier added and a tier removed
developer_push ≥ 3 DEVELOPER events in 30 d or developer_momentum ≥ 60
enterprise_repositioning new contact-sales tier(s) in 30 d, or ≥ 2 events mentioning enterprise / SSO / audit logs / contact sales
ai_acceleration ≥ 2 AI-tagged events in 30 d, or ≥ 25 % of ≥ 4 new listings are AI-related
abnormal_activity anomaly_score ≥ CA_ANOMALY_Z (2.5)

Scope aggregates (scope = industry | country): ≥ 3 companies with the same active kind → one aggregate signal per (scope, key, kind) with the company slugs as evidence; aggregates expire when the support drops below the threshold.

Terms = lowercase 1–3-grams from event titles (prefix like "News release:" removed) and news titles of the day, company names removed, stopwords excluded at n-gram edges, tokens ≥ 3 chars (unigrams ≥ 4). A term is kept for a day only when ≥ CA_TRENDS_MIN_COMPANIES (3) distinct companies used it (mentions, companies). Momentum for a window W ∈ {7, 30, 90}: rate = mentions / mean(companies_active) over the window, momentum = (rate_now − rate_prev) / max(rate_prev, ε); snapshots are stored in settings_kv['trends:momentum:<W>d'] for the API.