from app.services.data_loader import get_companies_df from app.models.schemas import WilayaRisk, Flag import numpy as np def generate_risk_commentary(wilaya_data: dict, risk_scores: dict) -> dict: """ Génère des commentaires éditoriaux en arabe basés sur les scores de risque. """ s1, s2, s3 = risk_scores['s1'], risk_scores['s2'], risk_scores['s3'] index = risk_scores['baath_index'] # Defaults level = "LOW" level_ar = "منخفض" color = "emerald" if index >= 70: level = "HIGH" level_ar = "مرتفع" color = "red" elif index >= 40: level = "MEDIUM" level_ar = "متوسط" color = "amber" comments = [] # S1 - Dépendance if s1 > 0.6: # lowered threshold slightly to match prompt logic 0.7 or 0.6 inconsistency # Prompt said > 0.7 but code example used 0.7. Let's stick to prompt code example logic if possible but use safe checks. dominant_groups = [g for g, count in wilaya_data['groups'].items() if g in ['AGRI_NATUREL', 'ENVIRONNEMENT', 'ENERGIE_MINES'] and count / (sum(wilaya_data['groups'].values()) or 1) > 0.3] if dominant_groups: comments.append(f"الولاية تعتمد بشكل كبير على الأنشطة المرتبطة بالموارد العمومية ({', '.join(dominant_groups)})") # S2 - Concentration if s2 > 0.7: if wilaya_data['groups']: top_group = max(wilaya_data['groups'].items(), key=lambda x: x[1])[0] pct = (wilaya_data['groups'][top_group] / (sum(wilaya_data['groups'].values()) or 1)) * 100 comments.append(f"تركيز عالٍ جدا في مجموعة نشاط واحدة ({top_group}: {pct:.0f}%)") elif s2 > 0.5: comments.append("تركيز ملحوظ في عدد محدود من القطاعات") # S3 - Gouvernance if s3 > 0.5: # Prompt threshold was 0.6 in general description but 0.5 in code example for flag. total_types = sum(wilaya_data['types'].values()) or 1 local_pct = (wilaya_data['types'].get('محلية', 0) / total_types) * 100 regional_pct = (wilaya_data['types'].get('جهوية', 0) / total_types) * 100 comments.append(f"اختلال واضح في الحوكمة: {local_pct:.0f}% محلية مقابل {regional_pct:.0f}% جهوية") # Recommendations recommendations = [] if s1 > 0.6: recommendations.append("التحقق من الأراضي الدولية المُسندة (OTD)") recommendations.append("البحث في صفقات التطهير والبيئة (TUNEPS)") if s2 > 0.7: recommendations.append("تحليل الاحتكارات القطاعية المحتملة") if s3 > 0.5: recommendations.append("مراجعة التوازن بين المحلي والجهوي في تركيبة مجالس الإدارة") if index > 70: recommendations.append("يُنصح بتحقيق صحفي معمق على هذه الولاية") return { "level": level, "level_ar": level_ar, "color": color, "comment_ar": " · ".join(comments) if comments else "لا توجد إشارات خطر واضحة في البيانات الحالية", "recommendations": recommendations } def compute_baath_index_v2(wilaya_df): """ Computes Ba7ath Index (0-100) using continuous formula: INDEX = 100 * (0.4 * s1 + 0.4 * s2 + 0.2 * s3) s1: Dependency on public-resource sectors (AGRI, ENV, MINES) s2: Sector concentration (Max share of any group) s3: Governance imbalance (abs(local - regional)) """ if wilaya_df.empty: return 0.0, 0.0, 0.0, 0.0, [] total = len(wilaya_df) flags = [] # --- s1: Resource Dependency --- # Groups: AGRI_NATUREL, ENVIRONNEMENT, ENERGIE_MINES resource_groups = ['AGRI_NATUREL', 'ENVIRONNEMENT', 'ENERGIE_MINES'] resource_count = wilaya_df[wilaya_df['activity_group'].isin(resource_groups)].shape[0] s1 = resource_count / total if total > 0 else 0.0 if s1 > 0.6: flags.append(Flag(code="RESOURCE_DEPENDENT", severity="high", label_ar="اعتماد كبير على الأنشطة المرتبطة بالموارد العمومية")) # --- s2: Sector Concentration --- # Max share of any single group group_counts = wilaya_df['activity_group'].value_counts(normalize=True) s2 = group_counts.max() if not group_counts.empty else 0.0 if s2 > 0.7: flags.append(Flag(code="ULTRA_CONCENTRATION", severity="medium", label_ar="تركيز عالٍ في مجموعة نشاط واحدة")) # --- s3: Governance Imbalance --- # abs(% local - % regional) type_counts = wilaya_df['type'].value_counts(normalize=True) pct_local = type_counts.get('محلية', 0.0) pct_regional = type_counts.get('جهوية', 0.0) s3 = abs(pct_local - pct_regional) if s3 > 0.5: flags.append(Flag(code="GOVERNANCE_IMBALANCE", severity="low", label_ar="اختلال واضح بين الشركات المحلية والجهوية")) # --- Final Score --- # INDEX = 100 * (0.4 * s1 + 0.4 * s2 + 0.2 * s3) raw_index = 100 * (0.4 * s1 + 0.4 * s2 + 0.2 * s3) baath_index = round(min(raw_index, 100), 1) # Return details for commentary details = { 'groups': wilaya_df['activity_group'].value_counts().to_dict(), 'types': wilaya_df['type'].value_counts().to_dict() } return baath_index, round(s1, 2), round(s2, 2), round(s3, 2), flags, details async def get_risk_for_wilaya(wilaya: str): df = await get_companies_df() if df.empty: return None wilaya_df = df[df['wilaya'] == wilaya] if wilaya_df.empty: # Return neutral risk if no companies return WilayaRisk( wilaya=wilaya, baath_index=0, s1=0, s2=0, s3=0, flags=[], level="LOW", level_ar="منخفض", color="emerald", comment_ar="لا توجد بيانات كافية", recommendations=[] ) score, s1, s2, s3, flags, details = compute_baath_index_v2(wilaya_df) # Generate commentary editorial = generate_risk_commentary(details, { 's1': s1, 's2': s2, 's3': s3, 'baath_index': score }) return WilayaRisk( wilaya=wilaya, baath_index=score, s1=s1, s2=s2, s3=s3, flags=flags, **editorial ) async def get_all_risks(): df = await get_companies_df() if df.empty: return [] risks = [] # Collect unique wilayas wilayas = df['wilaya'].unique() for wilaya in wilayas: risk = await get_risk_for_wilaya(wilaya) if risk: risks.append(risk) return sorted(risks, key=lambda x: x.baath_index, reverse=True)