graphstrike / server /scoring.py
Pandago's picture
Upload folder using huggingface_hub
50f71a7 verified
"""Pure-math risk scoring engine for the Fake Gang Detection environment.
All functions are stateless — no imports from other project modules.
Implements the formulas from formulas.md exactly.
"""
from __future__ import annotations
import math
def compute_node_risk(photo_reuse: float, bio_template: float) -> float:
"""Content-based risk: stolen photos + copy-paste bios."""
return round(0.60 * photo_reuse + 0.40 * bio_template, 4)
def compute_behavior_risk(account_age_days: int, post_hour_cluster_score: float) -> float:
"""Temporal risk: recently created + posting in the gang's time window."""
age_norm = min(1.0, account_age_days / 365.0)
return round(0.55 * (1.0 - age_norm) + 0.45 * post_hour_cluster_score, 4)
def compute_graph_risk(
flagged_neighbor_ratio: float,
mutual_follow_rate: float,
avg_neighbor_photo_reuse: float,
) -> float:
"""Structural risk: embedded in a flagged cluster + inflated mutual follows."""
return round(
0.45 * flagged_neighbor_ratio
+ 0.35 * mutual_follow_rate
+ 0.20 * avg_neighbor_photo_reuse,
4,
)
def compute_hub_legitimacy(
follower_count: int,
following_count: int,
account_age_days: int,
suspicious_mutual_ratio: float,
) -> float:
"""Celebrity/hub discount: large established accounts are unlikely to be fakes.
High value → high legitimacy → subtract from fake_risk.
"""
F_MAX = 1_000_000
followers_norm = min(1.0, math.log1p(follower_count) / math.log1p(F_MAX))
follow_ratio_norm = min(1.0, (following_count / max(follower_count, 1)) / 5.0)
age_norm = min(1.0, account_age_days / 365.0)
return round(
0.45 * followers_norm
+ 0.25 * (1.0 - follow_ratio_norm)
+ 0.20 * age_norm
+ 0.10 * (1.0 - suspicious_mutual_ratio),
4,
)
def compute_fake_risk(
node_risk: float,
behavior_risk: float,
graph_risk: float,
hub_legitimacy: float,
) -> float:
"""Composite fake risk score in [0.0, 1.0].
Graph risk carries the most weight (0.45) because structural signals
are hardest to fake at scale. Hub legitimacy discounts celebrities.
"""
raw = (
0.30 * node_risk
+ 0.25 * behavior_risk
+ 0.45 * graph_risk
- 0.25 * hub_legitimacy
)
return round(max(0.0, min(1.0, raw)), 4)
def classify_risk(fake_risk: float) -> str:
"""Map a fake_risk score to an account status string."""
if fake_risk < 0.35:
return "normal"
if fake_risk < 0.60:
return "suspect"
return "confirmed_fake"
def grader_score(tp: int, fp: int, fn: int, steps_used: int, max_steps: int) -> float:
"""Normalised [0.0, 1.0] submission score used by /grader endpoint.
Win condition (recall >= 0.8 AND precision >= 0.7):
score = 0.55 + 0.20*recall + 0.15*precision + 0.10*efficiency
Otherwise (partial credit):
score = 0.30*recall + 0.10*precision
"""
recall = tp / 10.0
precision = tp / max(tp + fp, 1)
efficiency = max(0.0, (max_steps - steps_used) / max_steps)
if recall >= 0.8 and precision >= 0.7:
score = 0.55 + 0.20 * recall + 0.15 * precision + 0.10 * efficiency
else:
score = 0.30 * recall + 0.10 * precision
return round(max(0.0, min(1.0, score)), 4)