Spaces:
Runtime error
Runtime error
| """ | |
| Entity Cluster / Bridge Intelligence β read-only aggregation over the | |
| ALREADY-COMPUTED Louvain community partition and betweenness centrality | |
| scores (both loaded from data/checkpoints/*.pkl at startup, see server.py). | |
| This module introduces no new graph computation: it only re-projects two | |
| existing per-account signals (community_id, betweenness) onto the | |
| Entity/Bank structural layer built by entity_graph_service.py, exactly the | |
| way entity_exposure_service.py re-projects the transaction graph onto an | |
| entity's blast radius. Never touches src/ml/ or src/detectors/. | |
| """ | |
| from __future__ import annotations | |
| from src.entity_graph_service import INSTITUTION_SPREAD_MEDIUM | |
| def attach_cluster_aggregates( | |
| entities_by_id: dict, | |
| accounts_by_number: dict, | |
| louvain_partition: dict, | |
| betweenness_scores: dict, | |
| ) -> None: | |
| """For each entity, derive: | |
| - community_id: the majority-vote Louvain community among its owned | |
| accounts (accounts with no partition entry are skipped). | |
| - community_confidence: fraction of scored accounts that agreed with | |
| the winning community (1.0 for single-account entities). | |
| - bridge_score: the max betweenness centrality among its owned accounts | |
| β a structural "how much does this entity sit between communities" | |
| signal, independent of risk scoring. | |
| Mutates entities_by_id in place. Entities with no matching accounts in | |
| either signal get community_id=None / bridge_score=0.0. | |
| """ | |
| for ent in entities_by_id.values(): | |
| votes: dict[str, int] = {} | |
| best_betweenness = 0.0 | |
| for account_number in ent["account_numbers"]: | |
| community_id = louvain_partition.get(account_number) | |
| if community_id is not None: | |
| votes[community_id] = votes.get(community_id, 0) + 1 | |
| score = betweenness_scores.get(account_number) | |
| if score is not None and score > best_betweenness: | |
| best_betweenness = score | |
| if votes: | |
| winning_community, winning_count = max(votes.items(), key=lambda kv: kv[1]) | |
| # Stringify β Louvain community ids are plain ints; routes/JSON | |
| # treat community_id as an opaque string key throughout. | |
| ent["community_id"] = str(winning_community) | |
| ent["community_confidence"] = round(winning_count / sum(votes.values()), 3) | |
| else: | |
| ent["community_id"] = None | |
| ent["community_confidence"] = 0.0 | |
| ent["bridge_score"] = round(best_betweenness, 6) | |
| def build_clusters_index(entities_by_id: dict) -> dict[str, dict]: | |
| """Group entities by their attached community_id into per-cluster | |
| aggregates β the "Mule Ring" unit surfaced by /network/clusters. | |
| `risk_concentration` is a heuristic (share of member entities flagged | |
| HIGH/CRITICAL by the existing worst-account risk rule), not a verified | |
| fraud rate β there is no reliable ground truth at this granularity. | |
| """ | |
| clusters: dict[str, dict] = {} | |
| for entity_id, ent in entities_by_id.items(): | |
| community_id = ent.get("community_id") | |
| if community_id is None: | |
| continue | |
| cluster = clusters.get(community_id) | |
| if cluster is None: | |
| cluster = { | |
| "community_id": community_id, | |
| "entity_ids": [], | |
| "entity_count": 0, | |
| "account_count": 0, | |
| "bank_ids": set(), | |
| "high_risk_entity_count": 0, | |
| } | |
| clusters[community_id] = cluster | |
| cluster["entity_ids"].append(entity_id) | |
| cluster["entity_count"] += 1 | |
| cluster["account_count"] += len(ent["account_numbers"]) | |
| cluster["bank_ids"].update(ent["bank_ids"]) | |
| if ent.get("risk_tier") in ("HIGH", "CRITICAL"): | |
| cluster["high_risk_entity_count"] += 1 | |
| for cluster in clusters.values(): | |
| cluster["bank_ids"] = sorted(cluster["bank_ids"]) | |
| cluster["bank_count"] = len(cluster["bank_ids"]) | |
| cluster["risk_concentration"] = round( | |
| cluster["high_risk_entity_count"] / cluster["entity_count"], 3 | |
| ) if cluster["entity_count"] else 0.0 | |
| return clusters | |
| def rank_bridge_entities(entities_by_id: dict, limit: int = 50) -> list[dict]: | |
| """Entities that are both structural bridges (high betweenness among | |
| their owned accounts) AND already spread across >= INSTITUTION_SPREAD_MEDIUM | |
| banks β the "Cross-Bank Bridge" leaderboard. Sorted by bridge_score desc. | |
| """ | |
| candidates = [ | |
| ent for ent in entities_by_id.values() | |
| if ent.get("bridge_score", 0.0) > 0.0 and len(ent["bank_ids"]) >= INSTITUTION_SPREAD_MEDIUM | |
| ] | |
| candidates.sort(key=lambda ent: ent["bridge_score"], reverse=True) | |
| return candidates[:limit] | |