abinazebinoy commited on
Commit
4ac80cb
·
1 Parent(s): 45d1eca

feat(phase-34): wire BenfordsAnalyzer + GhostCompanyDetector + ShadowDirectorDetector into risk scorer

Browse files

Three 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.

Files changed (1) hide show
  1. 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