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%
10.9 KB · 185 lines python
Raw Blame History
1"""Metrics: pure formulas (reproducible, coverage-normalised) and the DB flow (metrics_current / metric_series / company_daily / global_daily)."""2from __future__ import annotations34from datetime import UTC, date, datetime, timedelta56import pytest7from factories import intel_db  # noqa: F401 — registers the fixture89from companyatlas.services.metrics import (10    activity_score,11    ai_adoption,12    anomaly_score,13    corporate_change_index,14    hiring_momentum,15    historical_coverage,16    saturate,17)18from companyatlas.taxonomy import CCI_FORMULA_VERSION, METRICS_FORMULA_VERSION, Metric1920# ------------------------------------------------------------------------------------------------------------ pure212223def test_activity_score_coverage_normalisation():24    changes = [(1.0, "meaningful")] * 1025    small = activity_score(changes, [], active_sensors=2)26    big = activity_score(changes, [], active_sensors=32)27    assert small and big28    assert small.value > big.value                                  # same raw activity spread over 16× more sensors → lower density29    assert small.inputs["active_sensors"] == 2 and small.formula_version == METRICS_FORMULA_VERSION30    assert 0 < big.value < small.value <= 100313233def test_activity_score_decay_and_weights():34    fresh = activity_score([(0.0, "critical")], [], 1)35    old = activity_score([(30.0, "critical")], [], 1)36    minor_kind = activity_score([(0.0, "meaningful")], [], 1)37    assert fresh.value > old.value > 038    assert fresh.value > minor_kind.value39    assert activity_score([], [], 0) is None                         # no sensors, no activity → no row404142def test_hiring_momentum_requires_listings():43    assert hiring_momentum(10, 2, window=30) is None                 # < 3 listings at the reference point44    m = hiring_momentum(12, 8, window=30, extra={"jobs_new_30d": 5})45    assert m.metric == Metric.HIRING_MOMENTUM_30D and m.value == 50.0 and m.inputs["jobs_new_30d"] == 546    assert hiring_momentum(5, 10, window=7).value == -50.0474849def test_ai_adoption_renormalises_over_available_inputs():50    jobs_only = ai_adoption(ai_open=5, open_jobs=10, ai_events_90d=0, keyword_hits=0, has_text_inputs=False)51    assert jobs_only.value == pytest.approx(100 * (0.5 * 1.0 + 0.25 * 0.0) / 0.75, abs=0.01)52    none = ai_adoption(ai_open=None, open_jobs=None, ai_events_90d=0, keyword_hits=0, has_text_inputs=False)53    assert none is None54    text = ai_adoption(ai_open=None, open_jobs=None, ai_events_90d=3, keyword_hits=5, has_text_inputs=True)55    assert 0 < text.value < 100 and set(text.inputs["weights_used"]) == {"events", "keywords"}565758def test_cci_renormalises_and_maps_momentum():59    full = corporate_change_index({Metric.HIRING_MOMENTUM_30D: 50.0, Metric.PRODUCT_VELOCITY: 80.0, Metric.GEO_EXPANSION: 20.0, Metric.LEADERSHIP_ACTIVITY: 0.0,60                                   Metric.DEVELOPER_MOMENTUM: 40.0, Metric.COMMUNICATION_ACTIVITY: 60.0, Metric.PRICING_ACTIVITY: 10.0})61    expected = 0.25 * 75 + 0.20 * 80 + 0.15 * 20 + 0.15 * 0 + 0.10 * 40 + 0.10 * 60 + 0.05 * 1062    assert full.value == pytest.approx(expected, abs=0.01) and full.formula_version == CCI_FORMULA_VERSION63    partial = corporate_change_index({Metric.PRODUCT_VELOCITY: 80.0, Metric.PRICING_ACTIVITY: 20.0})64    assert partial.value == pytest.approx((0.20 * 80 + 0.05 * 20) / 0.25, abs=0.01)65    assert partial.inputs["weight_coverage"] == 0.25 and partial.confidence < full.confidence66    assert corporate_change_index({}) is None676869def test_anomaly_and_coverage():70    assert anomaly_score(10, 2.0, 1.0, samples=2) is None71    z = anomaly_score(10, 2.0, 1.0, samples=8)72    assert z.value == 8.073    flat = anomaly_score(3, 0.0, 0.0, samples=8)74    assert flat.value == 6.0                                          # stddev floor 0.575    cov = historical_coverage(observed=50, expected=100.0, days_with_obs=15, days_since_first=30, surfaces=4)76    assert cov.value == pytest.approx(100 * (0.5 * 0.5 + 0.3 * 0.5 + 0.2 * 0.5), abs=0.01)77    assert historical_coverage(observed=0, expected=0.0, days_with_obs=0, days_since_first=0, surfaces=0) is None78    assert saturate(6.0, 6.0) == pytest.approx(0.632, abs=0.001)798081# ------------------------------------------------------------------------------------------------------------ database828384@pytest.mark.usefixtures("intel_db")85async def test_company_metrics_end_to_end():86    from factories import (87        CONNECTOR_ATS,88        cleanup,89        make_change,90        make_company,91        make_event,92        make_job,93        make_location,94        make_observation,95        make_sensor,96    )9798    from companyatlas.db import fetch_all, transaction99    from companyatlas.services.metrics import compute_company_metrics100101    try:102        async with transaction() as conn:103            co = await make_company(conn, first_observed_days_ago=40)104            board = await make_sensor(conn, co, "jobs_board", connector_id=CONNECTOR_ATS, created_days_ago=40)105            await make_sensor(conn, co, "pricing", created_days_ago=40)106            await make_sensor(conn, co, "docs", created_days_ago=40)107            await make_sensor(conn, co, "locations", created_days_ago=40)108            # jobs: 8 open now, 4 of them existed 30 days ago (+ 1 removed since) → momentum_30d = (8 − 5) / 5 = +60 %109            for i in range(4):110                await make_job(conn, co, title=f"Engineer {i}", first_seen_days_ago=45, country=None, sensor_id=board["id"])111            await make_job(conn, co, title="Old role", first_seen_days_ago=45, removed_days_ago=10, country=None, sensor_id=board["id"])112            for i in range(4):113                await make_job(conn, co, title=f"ML Engineer {i}", first_seen_days_ago=5, is_ai=True, country="JP", sensor_id=board["id"])114            await make_location(conn, co, name="Tokyo office", country="JP", city="Tokyo", first_seen_days_ago=3)115            await make_event(conn, co, subtype="NEW_PRODUCT", importance=0.7, days_ago=2)116            await make_event(conn, co, subtype="DOC_CHANGE", importance=0.4, days_ago=3)117            await make_event(conn, co, subtype="PRICE_INCREASE", importance=0.8, days_ago=4)118            await make_event(conn, co, subtype="COUNTRY_EXPANSION", importance=0.8, days_ago=3)119            await make_event(conn, co, subtype="AI_HIRING", importance=0.5, days_ago=5, tags=["hiring", "ai"])120            await make_change(conn, board, significance=0.6, detected_at=datetime.now(UTC) - timedelta(days=1), status="processed")121            for d in range(0, 40, 2):122                await make_observation(conn, board, days_ago=d)123        stats = await compute_company_metrics([co["id"]])124        assert stats["companies"] == 1125        async with transaction() as conn:126            rows = {r["metric"]: r for r in await fetch_all(conn, "select * from metrics_current where company_id = :c", c=co["id"])}127            series = await fetch_all(conn, "select metric from metric_series where company_id = :c and day = current_date", c=co["id"])128        assert set(series and [r["metric"] for r in series]) == set(rows)129        assert rows["open_jobs"]["value"] == 8130        assert rows["hiring_momentum_30d"]["value"] == pytest.approx(60.0)131        assert rows["hiring_momentum_30d"]["inputs"]["jobs_new_30d"] == 4 and rows["hiring_momentum_30d"]["confidence"] == pytest.approx(0.9, abs=0.01)132        assert rows["hiring_momentum_7d"]["value"] == pytest.approx(100.0)  # 4 open 7 days ago (removed role already gone), 4 new since133        assert "hiring_momentum_90d" not in rows                          # nothing was open 90 days ago → no fabricated momentum134        assert rows["ai_adoption"]["value"] > 40 and rows["ai_adoption"]["inputs"]["ai_open"] == 4135        assert rows["geo_expansion"]["inputs"]["new_countries_90d"] == ["JP"] and rows["geo_expansion"]["value"] > 0136        assert rows["product_velocity"]["value"] > 0 and rows["pricing_activity"]["value"] > 0 and rows["developer_momentum"]["value"] > 0137        assert "leadership_activity" not in rows                          # no leadership sensor and no leadership events → no row138        cci = rows["corporate_change_index"]139        assert cci["formula_version"] == CCI_FORMULA_VERSION and 0 < cci["value"] <= 100 and "leadership_activity" not in cci["inputs"]["components"]140        assert rows["activity_score"]["value"] > 0 and rows["activity_score"]["inputs"]["active_sensors"] == 4141        assert 0 < rows["historical_coverage"]["value"] <= 100142        assert "anomaly_score" not in rows                                # no baseline yet143    finally:144        await cleanup()145146147@pytest.mark.usefixtures("intel_db")148async def test_daily_aggregates_and_activity_index():149    from factories import cleanup, make_change, make_company, make_event, make_observation, make_sensor150151    from companyatlas.db import fetch_all, fetch_one, transaction152    from companyatlas.services.metrics import compute_daily153154    day0 = date(2001, 1, 10)                                          # far in the past: never collides with live data, cleaned by factories155    try:156        async with transaction() as conn:157            a = await make_company(conn, country="CA", industries=["fintech"])158            b = await make_company(conn, country="US", industries=["retail"])159            sa = await make_sensor(conn, a, "homepage")160            sb = await make_sensor(conn, b, "homepage")161            for i in range(7):162                d = datetime(2001, 1, 4 + i, 12, tzinfo=UTC)163                for s in (sa, sb):164                    await make_observation(conn, s, days_ago=(datetime.now(UTC) - d).total_seconds() / 86400)165                if i < 6:166                    await make_change(conn, sa, significance=0.6, detected_at=d, status="processed")167            # day0: burst on company a168            for _ in range(5):169                await make_change(conn, sa, significance=0.7, detected_at=datetime(2001, 1, 10, 13, tzinfo=UTC), status="processed")170            await make_event(conn, a, subtype="PRICE_INCREASE", days_ago=(datetime.now(UTC) - datetime(2001, 1, 10, 14, tzinfo=UTC)).total_seconds() / 86400)171        for i in range(6):172            await compute_daily(date(2001, 1, 4 + i))173        result = await compute_daily(day0)174        assert result["companies"] == 2 and result["meaningful_changes"] == 5 and result["events"] == 1175        assert result["baseline_days"] == 6176        assert result["activity_index"] == pytest.approx(500.0)     # 5 changes / 2 sensors vs baseline 1 change / 2 sensors → ×100177        async with transaction() as conn:178            g = await fetch_one(conn, "select * from global_daily where day = :d", d=day0)179            assert g["sensors_active"] == 2 and g["companies_active"] == 2 and g["events_by_type"] == {"PRICING": 1}180            assert g["by_country"]["CA"]["meaningful_changes"] == 5 and g["by_industry"]["fintech"]["events"] == 1181            cd = {r["company_id"]: r for r in await fetch_all(conn, "select * from company_daily where day = :d", d=day0)}182            assert cd[a["id"]]["meaningful_changes"] == 5 and cd[b["id"]]["meaningful_changes"] == 0 and cd[b["id"]]["observations"] == 1183    finally:184        await cleanup()185