Jump to content

Module:RiskRange

From RiskiPedia

This module converts two probability values into a human-friendly range string with a common denominator, making it easy to compare the low and high estimates.

Usage

From wikitext (via Template:RiskRange):

{{RiskRange|probability1|probability2}}

Direct module invocation:

{{#invoke:RiskRange|convert|probability1|probability2}}

Parameters

Parameter Description
1 First probability (decimal between 0 and 1)
2 Second probability (decimal between 0 and 1)

The module automatically orders the probabilities so the lower risk appears first and higher risk appears second.

Output Format

The module outputs ranges in the format "N to M in D" where:

  • N = lower numerator (lower risk)
  • M = higher numerator (higher risk)
  • D = common denominator

Denominator Selection

  • If either probability is ≥ 0.1, the denominator is 10 (e.g., "2 to 6 in 10")
  • Otherwise, the denominator is chosen based on the smaller probability, rounded to a "nice" value (10, 20, 50, 100, 200, 500, 1,000, etc.)

Special Cases

Condition Output
Both probabilities ≤ 0 "no chance"
Both probabilities ≥ 1 "100% chance"
Both probabilities ≥ 0.95 "almost certain"
One is 0, other is 1 "anywhere from no chance to certain"
Probabilities within 5% of each other Single value, e.g., "about 5 in 10"
Lower probability ≈ 0 "up to about N in D"
Higher probability ≥ 1 "at least N in D"

Examples

Input Output
{{RiskRange|0.2|0.6}} 2 to 6 in 10
{{RiskRange|0.001|0.01}} 1 to 10 in 1,000
{{RiskRange|0.21|0.6}} 2 to 6 in 10
{{RiskRange|0.33|0.82}} 3 to 8 in 10
{{RiskRange|0.01|0.05}} 1 to 5 in 100
{{RiskRange|0.0001|0.001}} 1 to 10 in 10,000
{{RiskRange|0.5|0.5}} about 5 in 10
{{RiskRange|0|0.5}} up to about 5 in 10

Algorithm

  1. Validate inputs and handle error cases
  2. Order probabilities (lower first)
  3. Check for special cases (zero, one, equal, almost certain)
  4. Select common denominator:
    • Use 10 if higher probability ≥ 0.1
    • Otherwise use nice_denominator(1/lower_probability)
  5. Calculate numerators: N = probability × denominator
  6. Round numerators to integers
  7. If both round to same value, try larger denominator
  8. Format output string

See Also


local p = {}

-- Format an integer with commas as thousands separators
local function format_with_commas(n)
    local s = tostring(math.floor(n))
    local sign, int = s:match("^([%-]?)(%d+)$")
    if not int then return s end
    int = int:reverse():gsub("(%d%d%d)", "%1,")
    int = int:reverse():gsub("^,", "")
    return sign .. int
end

-- Round to a "nice" denominator (power of 10, or 2/5 times a power of 10)
local function nice_denominator(recip)
    if recip <= 1 then return 10 end

    local exponent = math.floor(math.log10(recip))
    local base = 10 ^ exponent

    -- Try nice values: 1, 2, 5, 10 times the base
    local nice_values = { base, base * 2, base * 5, base * 10 }

    for _, val in ipairs(nice_values) do
        if val >= recip then
            return val
        end
    end

    return base * 10
end

-- Convert a probability range to a human-friendly string with common denominator
-- @param frame.args[1] First probability (lower or higher)
-- @param frame.args[2] Second probability (lower or higher)
-- @return A string like "1 to 10 in 1,000" or "2 to 6 in 10"
function p.convert(frame)
    local prob1_str = frame.args[1]
    local prob2_str = frame.args[2]

    -- Validate inputs
    if prob1_str == nil or prob1_str == '' then
        return "Error: First probability not provided"
    end
    if prob2_str == nil or prob2_str == '' then
        return "Error: Second probability not provided"
    end

    local prob1 = tonumber(prob1_str)
    local prob2 = tonumber(prob2_str)

    if not prob1 then
        return "Error: Invalid first probability"
    end
    if not prob2 then
        return "Error: Invalid second probability"
    end

    -- Ensure prob_low <= prob_high (lower risk first, higher risk second)
    local prob_low, prob_high
    if prob1 <= prob2 then
        prob_low, prob_high = prob1, prob2
    else
        prob_low, prob_high = prob2, prob1
    end

    -- Handle special cases
    if prob_low <= 0 and prob_high <= 0 then
        return "no chance"
    end
    if prob_low >= 1 and prob_high >= 1 then
        return "100% chance"
    end
    if prob_low >= 0.95 and prob_high >= 0.95 then
        return "almost certain"
    end
    if prob_low <= 0 and prob_high >= 1 then
        return "anywhere from no chance to certain"
    end

    -- Handle case where probabilities are effectively equal
    local ratio = prob_high / math.max(prob_low, 1e-10)
    if ratio < 1.05 then
        -- They're essentially the same, use single value format
        -- For probabilities >= 0.1, use "N in 10" for consistency
        if prob_low >= 0.1 then
            local n = math.floor(prob_low * 10 + 0.5)
            return "about " .. n .. " in 10"
        else
            local recip = 1 / prob_low
            local denom = nice_denominator(recip)
            local numer = math.floor(prob_low * denom + 0.5)
            if numer < 1 then numer = 1 end
            return "about " .. numer .. " in " .. format_with_commas(denom)
        end
    end

    -- Handle case where low probability is zero or nearly zero
    if prob_low <= 1e-9 then
        -- Just describe the high end
        -- For probabilities >= 0.1, use "N in 10" for consistency
        if prob_high >= 0.1 then
            local n_high = math.floor(prob_high * 10 + 0.5)
            return "up to about " .. n_high .. " in 10"
        else
            local recip_high = 1 / prob_high
            local denom = nice_denominator(recip_high)
            local n_high = math.floor(prob_high * denom + 0.5)
            if n_high < 1 then n_high = 1 end
            return "up to about " .. n_high .. " in " .. format_with_commas(denom)
        end
    end

    -- Handle case where high probability is >= 1
    if prob_high >= 1 then
        -- For probabilities >= 0.1, use "N in 10" for consistency
        if prob_low >= 0.1 then
            local n_low = math.floor(prob_low * 10 + 0.5)
            return "at least " .. n_low .. " in 10"
        else
            local recip_low = 1 / prob_low
            local denom = nice_denominator(recip_low)
            local n_low = math.floor(prob_low * denom + 0.5)
            if n_low < 1 then n_low = 1 end
            return "at least " .. n_low .. " in " .. format_with_commas(denom)
        end
    end

    -- Standard case: both probabilities are in (0, 1)
    -- Prefer denominator of 10 when the higher probability is >= 0.1
    local denom
    if prob_high >= 0.1 then
        denom = 10
    else
        -- Pick denominator based on the smaller probability (larger reciprocal)
        local recip_low = 1 / prob_low
        denom = nice_denominator(recip_low)
    end

    -- Calculate numerators
    local n_low = prob_low * denom
    local n_high = prob_high * denom

    -- Round to integers
    n_low = math.floor(n_low + 0.5)
    n_high = math.floor(n_high + 0.5)

    -- Ensure minimum of 1 for low end
    if n_low < 1 then n_low = 1 end

    -- If they rounded to the same value, adjust
    if n_low == n_high then
        -- Try to differentiate by using a larger denominator
        denom = denom * 10
        n_low = math.floor(prob_low * denom + 0.5)
        n_high = math.floor(prob_high * denom + 0.5)
        if n_low < 1 then n_low = 1 end

        -- If still equal, just return single value
        if n_low == n_high then
            return "about " .. n_low .. " in " .. format_with_commas(denom)
        end
    end

    -- Format output
    return n_low .. " to " .. n_high .. " in " .. format_with_commas(denom)
end

return p