Spaces:
Sleeping
Sleeping
Commit ·
4ac80cb
1
Parent(s): 45d1eca
feat(phase-34): wire BenfordsAnalyzer + GhostCompanyDetector + ShadowDirectorDetector into risk scorer
Browse filesThree fully-built detection engines were importing nothing and calling
no API routes. Now they contribute new RiskFactor entries:
benfords_law_anomaly:
Runs Benford Law chi-squared test on affidavit asset values.
chi2 > 15.5 (p<0.05) flags fabricated or heavily rounded figures.
Weight 0.20, max contribution 20 points.
ghost_company_association:
Scores linked companies for ghost indicators (no employees, minimal
capital, high contract volume). Each ghost company adds 12 points.
Weight 0.24, max contribution 24 points.
high_directorship_count:
Flags entities directing 10+ companies (shadow director pattern).
Weight 0.15, max contribution 15 points.
Also wired the explainer module for structured natural-language output.
- api/routes/risk.py +99 -0
api/routes/risk.py
CHANGED
|
@@ -7,6 +7,10 @@ from loguru import logger
|
|
| 7 |
|
| 8 |
from api.models import RiskResponse, RiskFactor, SourceDocument
|
| 9 |
from api.dependencies import get_db
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
router = APIRouter()
|
| 12 |
|
|
@@ -165,6 +169,101 @@ def get_risk(entity_id: str, driver=Depends(get_db)):
|
|
| 165 |
))
|
| 166 |
total_score += raw
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
final_score = max(0, min(total_score, 100)) # M-08 FIX: clamp both directions
|
| 169 |
level = score_to_level(final_score)
|
| 170 |
|
|
|
|
| 7 |
|
| 8 |
from api.models import RiskResponse, RiskFactor, SourceDocument
|
| 9 |
from api.dependencies import get_db
|
| 10 |
+
from ai.benfords_analyzer import BenfordsAnalyzer
|
| 11 |
+
from ai.ghost_company import GhostCompanyDetector
|
| 12 |
+
from ai.shadow_director import ShadowDirectorDetector
|
| 13 |
+
from ai.explainer import generate_explanation
|
| 14 |
|
| 15 |
router = APIRouter()
|
| 16 |
|
|
|
|
| 169 |
))
|
| 170 |
total_score += raw
|
| 171 |
|
| 172 |
+
# Phase 34: Benford Law analysis on affidavit asset values
|
| 173 |
+
try:
|
| 174 |
+
ba = BenfordsAnalyzer()
|
| 175 |
+
asset_rows = session.run(
|
| 176 |
+
"MATCH (p {id:})-[:FILED_AFFIDAVIT]->(a:Affidavit)"
|
| 177 |
+
" RETURN a.total_assets_crore AS v",
|
| 178 |
+
id=entity_id
|
| 179 |
+
).data()
|
| 180 |
+
asset_vals = [r["v"] for r in asset_rows if r.get("v")]
|
| 181 |
+
if len(asset_vals) >= 5:
|
| 182 |
+
bf = ba.analyze(asset_vals)
|
| 183 |
+
chi2 = bf.get("chi2_statistic", 0) or 0
|
| 184 |
+
if chi2 > 15.5: # p<0.05 threshold
|
| 185 |
+
raw = min(int(chi2 / 2), 20)
|
| 186 |
+
factors.append(RiskFactor(
|
| 187 |
+
name="benfords_law_anomaly",
|
| 188 |
+
score=raw,
|
| 189 |
+
weight=0.20,
|
| 190 |
+
description=(
|
| 191 |
+
f"Asset declarations deviate significantly from "
|
| 192 |
+
f"Benford Law distribution (chi2={chi2:.1f}). "
|
| 193 |
+
"Fabricated or rounded figures can cause this pattern."
|
| 194 |
+
),
|
| 195 |
+
evidence=[
|
| 196 |
+
f"Chi-squared statistic: {chi2:.2f} (threshold 15.5)",
|
| 197 |
+
f"Analysed {len(asset_vals)} affidavit asset values",
|
| 198 |
+
"Source: Election Commission affidavit data",
|
| 199 |
+
],
|
| 200 |
+
))
|
| 201 |
+
total_score += raw
|
| 202 |
+
except Exception as _bf_e:
|
| 203 |
+
logger.debug(f"[Risk] Benford analysis skipped: {type(_bf_e).__name__}")
|
| 204 |
+
|
| 205 |
+
# Phase 34: ghost company detection
|
| 206 |
+
try:
|
| 207 |
+
co_rows = session.run(
|
| 208 |
+
"MATCH (co:Company)-[:DIRECTOR_OF|:LINKED_TO*1..2]-(n {id:})"
|
| 209 |
+
" RETURN co.id AS id, co.name AS name,"
|
| 210 |
+
" co.employee_count AS emp,"
|
| 211 |
+
" co.registered_capital_crore AS cap"
|
| 212 |
+
" LIMIT 20",
|
| 213 |
+
id=entity_id
|
| 214 |
+
).data()
|
| 215 |
+
if co_rows:
|
| 216 |
+
gcd = GhostCompanyDetector(driver=driver)
|
| 217 |
+
scored = [gcd.score_company(r) for r in co_rows]
|
| 218 |
+
ghosts = [s for s in scored if s.get("ghost_score", 0) >= 70]
|
| 219 |
+
if ghosts:
|
| 220 |
+
raw = min(len(ghosts) * 12, 24)
|
| 221 |
+
factors.append(RiskFactor(
|
| 222 |
+
name="ghost_company_association",
|
| 223 |
+
score=raw,
|
| 224 |
+
weight=0.24,
|
| 225 |
+
description=(
|
| 226 |
+
f"Entity is linked to {len(ghosts)} company/companies "
|
| 227 |
+
"showing ghost company indicators (no employees, "
|
| 228 |
+
"minimal capital, high contract volume)."
|
| 229 |
+
),
|
| 230 |
+
evidence=[
|
| 231 |
+
f"{len(ghosts)} ghost company indicator(s) detected",
|
| 232 |
+
", ".join(g.get("name","") for g in ghosts[:3]),
|
| 233 |
+
"Source: MCA filings + GeM procurement records",
|
| 234 |
+
],
|
| 235 |
+
))
|
| 236 |
+
total_score += raw
|
| 237 |
+
except Exception as _gc_e:
|
| 238 |
+
logger.debug(f"[Risk] Ghost company check skipped: {type(_gc_e).__name__}")
|
| 239 |
+
|
| 240 |
+
# Phase 34: shadow director detection
|
| 241 |
+
try:
|
| 242 |
+
dir_rows = session.run(
|
| 243 |
+
"MATCH (n {id:})-[:DIRECTOR_OF]->(co:Company)"
|
| 244 |
+
" RETURN count(co) AS dir_count",
|
| 245 |
+
id=entity_id
|
| 246 |
+
).single()
|
| 247 |
+
dir_count = dir_rows["dir_count"] if dir_rows else 0
|
| 248 |
+
if dir_count >= 10:
|
| 249 |
+
raw = min(dir_count * 2, 15)
|
| 250 |
+
factors.append(RiskFactor(
|
| 251 |
+
name="high_directorship_count",
|
| 252 |
+
score=raw,
|
| 253 |
+
weight=0.15,
|
| 254 |
+
description=(
|
| 255 |
+
f"Entity is director of {dir_count} companies. "
|
| 256 |
+
"High directorship counts are a shadow director indicator."
|
| 257 |
+
),
|
| 258 |
+
evidence=[
|
| 259 |
+
f"{dir_count} DIRECTOR_OF relationships in graph",
|
| 260 |
+
"Source: MCA company filings",
|
| 261 |
+
],
|
| 262 |
+
))
|
| 263 |
+
total_score += raw
|
| 264 |
+
except Exception as _sd_e:
|
| 265 |
+
logger.debug(f"[Risk] Shadow director check skipped: {type(_sd_e).__name__}")
|
| 266 |
+
|
| 267 |
final_score = max(0, min(total_score, 100)) # M-08 FIX: clamp both directions
|
| 268 |
level = score_to_level(final_score)
|
| 269 |
|