Skip to main content
The risk engine lives in app/risk.py. It takes a Business object and returns a score from 0–100, a risk level label, and a breakdown of every component that contributed to the final score.
The engine is deliberately simulated. It uses deterministic industry and country modifiers combined with controlled randomness (random.uniform). This makes it statistically testable — high-risk inputs reliably trend toward higher scores — while still producing varied results across multiple evaluations of the same business.

Algorithm

The score is built from four components added together, then clamped to the 0–100 range:

Step 1 — Random base score

base_score = random.uniform(10, 70)
Every evaluation starts with a random float between 10 and 70. This represents general business risk before any specific modifiers are applied.

Step 2 — Industry modifier

industry_modifier = 0.0
industry = (business.industry or "").lower()
if industry in HIGH_RISK_INDUSTRIES:
    industry_modifier = random.uniform(15, 30)
elif industry in LOW_RISK_INDUSTRIES:
    industry_modifier = random.uniform(-20, -5)
The industry field is lowercased before matching. Unrecognised industries produce a modifier of 0.0.
CategoryIndustriesModifier range
High riskcrypto, gambling, weapons, tobacco+15 to +30
Low riskhealthcare, education, agriculture-20 to -5
NeutralAll others0.0

Step 3 — Country modifier

country_modifier = 0.0
country = (business.country or "").lower()
if country in HIGH_RISK_COUNTRIES:
    country_modifier = random.uniform(15, 25)
Sanctioned or high-risk countries add between 15 and 25 points. The recognised values are: north korea, iran, syria, russia. All other countries produce a modifier of 0.0.

Step 4 — Noise

noise = random.uniform(-5, 5)
A small random perturbation of ±5 is added to prevent identical scores when all other inputs are the same.

Step 5 — Final score

final_score = max(0.0, min(100.0, base_score + industry_modifier + country_modifier + noise))
final_score = round(final_score, 1)
The sum of all components is clamped to [0.0, 100.0] and rounded to one decimal place.

Risk levels

LevelScore range
low< 25
medium25< 50
high50< 75
critical>= 75
if final_score < 25:
    risk_level = "low"
elif final_score < 50:
    risk_level = "medium"
elif final_score < 75:
    risk_level = "high"
else:
    risk_level = "critical"

Output

evaluate_risk returns a plain dict with three keys:
return {
    "risk_score": final_score,
    "risk_level": risk_level,
    "factors": json.dumps(factors),
}
The factors value is a JSON string. When parsed, it contains the individual component values:
{
  "base_score": 42.3,
  "industry_modifier": 22.7,
  "country_modifier": 0.0,
  "noise": -1.4,
  "final_score": 63.6
}
This breakdown is stored in the risk_evaluations.factors column and displayed on the business detail page so you can see exactly what drove the score.

Full source

"""Simulated risk evaluation engine for businesses."""

import json
import random

from app.models import Business

# Industries considered higher risk
HIGH_RISK_INDUSTRIES = {"crypto", "gambling", "weapons", "tobacco"}
LOW_RISK_INDUSTRIES = {"healthcare", "education", "agriculture"}

# Countries with elevated risk modifiers
HIGH_RISK_COUNTRIES = {"north korea", "iran", "syria", "russia"}


def evaluate_risk(business: Business) -> dict:
    """Simulate a risk evaluation based on industry, country, and randomness.

    Returns a dict with risk_score (0-100), risk_level, and factors breakdown.
    """
    base_score = random.uniform(10, 70)

    industry_modifier = 0.0
    industry = (business.industry or "").lower()
    if industry in HIGH_RISK_INDUSTRIES:
        industry_modifier = random.uniform(15, 30)
    elif industry in LOW_RISK_INDUSTRIES:
        industry_modifier = random.uniform(-20, -5)

    country_modifier = 0.0
    country = (business.country or "").lower()
    if country in HIGH_RISK_COUNTRIES:
        country_modifier = random.uniform(15, 25)

    noise = random.uniform(-5, 5)
    final_score = max(0.0, min(100.0, base_score + industry_modifier + country_modifier + noise))
    final_score = round(final_score, 1)

    if final_score < 25:
        risk_level = "low"
    elif final_score < 50:
        risk_level = "medium"
    elif final_score < 75:
        risk_level = "high"
    else:
        risk_level = "critical"

    factors = {
        "base_score": round(base_score, 1),
        "industry_modifier": round(industry_modifier, 1),
        "country_modifier": round(country_modifier, 1),
        "noise": round(noise, 1),
        "final_score": final_score,
    }

    return {
        "risk_score": final_score,
        "risk_level": risk_level,
        "factors": json.dumps(factors),
    }

Architecture overview

How the request flow, HTMX partials, and design decisions fit together

Data model

Where risk scores and factor breakdowns are stored in SQLite

Build docs developers (and LLMs) love