跳转到内容

模組:PythonScript/Librarys/Statistics

维基百科,自由的百科全书
-- Module:PythonScript/Librarys/Statistics
-- 純 Lua 實作的 Python 3.10 statistics 標準庫 (延遲載入版,支援 float/int/BigInt/Decimal 與 NormalDist)
local StatisticsMod = {}

local PyClass = require("Module:PythonScript/Class")
local Runtime = require("Module:PythonScript/Runtime")

local S = {
    initialized = false,
    cache = {},
    linreg_cls = nil
}

-- ==========================================
-- 1. 內部型別轉換與高精度四則運算輔助
-- ==========================================
function S.is_none(v)
    return v == nil or (type(v) == "table" and v.__class and v.__class.__name == "NoneType")
end

function S.is_dec(v)
    return type(v) == "table" and v.__type == "instance" and v.__dict and v.__dict._is_decimal == true
end

function S.raise_err(msg)
    S.ensure_init()
    Runtime.raise(S.cache.StatisticsError, msg)
end

function S.to_float(x)
    if type(x) == "number" then return x end
    if type(x) == "boolean" then return x and 1.0 or 0.0 end
    if type(x) == "table" then
        if x.__type == "int" then
            local MathCore = require("Module:PythonScript/Runtime/Cmath")
            return x.value or tonumber(MathCore.BigInt.to_string(x.sign, x.limbs))
        elseif S.is_dec(x) then
            return require("Module:PythonScript/Librarys/Decimal").to_number(x)
        elseif x.__type == "instance" then
            local ok, flt_m = pcall(PyClass.get_attr, x, "__float__")
            if ok and flt_m and flt_m.__type == "bound_method" then
                return require("Module:PythonScript/Evaluator").invoke(flt_m.__func, {}, {}, flt_m.__self)
            end
            if x.__builtin_value ~= nil then return S.to_float(x.__builtin_value) end
        end
    end
    Runtime.raise("TypeError", "can't convert type to float")
end

function S.extract_list(iterable)
    if iterable == nil then Runtime.raise("TypeError", "missing required iterable argument") end
    if type(iterable) == "table" and (iterable.__type == "list" or iterable.__type == "tuple") then
        local res = {}
        for i = 1, #iterable.elements do res[i] = iterable.elements[i] end
        return res
    end
    local lst = Runtime.builtins.list.__call({iterable})
    return lst.elements
end

function S.sort_list(arr)
    local EvalUtils = require("Module:PythonScript/Evaluator/Utils")
    local copy = {}
    for i = 1, #arr do copy[i] = arr[i] end
    table.sort(copy, function(a, b) return EvalUtils.py_cmp(a, b) < 0 end)
    return copy
end

-- 支援保留 Decimal 型別之通用算術
function S.is_exact(v)
    return type(v) == "table" and v.__type == "instance" and v.__dict and (v.__dict._is_decimal or v.__dict._is_fraction)
end

function S.v_add(a, b)
    if S.is_exact(a) or S.is_exact(b) then
        local Evaluator = require("Module:PythonScript/Evaluator")
        local B = require("Module:PythonScript/Runtime").builtins
        if S.is_exact(a) then
            local r = Evaluator.invoke(PyClass.get_attr(a, "__add__"), {b}, {})
            if r ~= B.NotImplemented then return r end
        end
        if S.is_exact(b) then
            local r = Evaluator.invoke(PyClass.get_attr(b, "__radd__"), {a}, {})
            if r ~= B.NotImplemented then return r end
        end
    end
    return S.to_float(a) + S.to_float(b)
end

function S.v_sub(a, b)
    if S.is_exact(a) or S.is_exact(b) then
        local Evaluator = require("Module:PythonScript/Evaluator")
        local B = require("Module:PythonScript/Runtime").builtins
        if S.is_exact(a) then
            local r = Evaluator.invoke(PyClass.get_attr(a, "__sub__"), {b}, {})
            if r ~= B.NotImplemented then return r end
        end
        if S.is_exact(b) then
            local r = Evaluator.invoke(PyClass.get_attr(b, "__rsub__"), {a}, {})
            if r ~= B.NotImplemented then return r end
        end
    end
    return S.to_float(a) - S.to_float(b)
end

function S.v_mul(a, b)
    if S.is_exact(a) or S.is_exact(b) then
        local Evaluator = require("Module:PythonScript/Evaluator")
        local B = require("Module:PythonScript/Runtime").builtins
        if S.is_exact(a) then
            local r = Evaluator.invoke(PyClass.get_attr(a, "__mul__"), {b}, {})
            if r ~= B.NotImplemented then return r end
        end
        if S.is_exact(b) then
            local r = Evaluator.invoke(PyClass.get_attr(b, "__rmul__"), {a}, {})
            if r ~= B.NotImplemented then return r end
        end
    end
    return S.to_float(a) * S.to_float(b)
end

function S.v_div(a, b)
    if S.is_exact(a) or S.is_exact(b) then
        local Evaluator = require("Module:PythonScript/Evaluator")
        local B = require("Module:PythonScript/Runtime").builtins
        if S.is_exact(a) then
            local r = Evaluator.invoke(PyClass.get_attr(a, "__truediv__"), {b}, {})
            if r ~= B.NotImplemented then return r end
        end
        if S.is_exact(b) then
            local r = Evaluator.invoke(PyClass.get_attr(b, "__rtruediv__"), {a}, {})
            if r ~= B.NotImplemented then return r end
        end
    end
    return S.to_float(a) / S.to_float(b)
end

function S.v_sqrt(a)
    if S.is_dec(a) then
        return require("Module:PythonScript/Evaluator").invoke(PyClass.get_attr(a, "sqrt"), {}, {})
    end
    return math.sqrt(S.to_float(a))
end

-- ==========================================
-- 2. 误差函數與常態分佈反函數 (erf & inv_cdf)
-- ==========================================
function S.erf(x)
    local sign = (x < 0) and -1 or 1
    x = math.abs(x)
    local t = 1.0 / (1.0 + 0.3275911 * x)
    local y = 1.0 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * math.exp(-x * x)
    return sign * y
end

-- Acklam 有理逼近法計算標準常態分佈反函數 \Phi^{-1}(p)
function S.std_inv_cdf(p)
    if p <= 0.0 or p >= 1.0 then
        S.raise_err("inv_cdf() requires 0.0 < p < 1.0")
    end
    local a1, a2, a3, a4, a5, a6 = -39.69683028665376, 220.9460984245205, -275.9285104469687, 138.3577518672690, -30.66479806614716, 2.506628277459239
    local b1, b2, b3, b4, b5 = -54.47609879822406, 161.5858368580409, -155.6989798598866, 66.80131188771972, -13.28068155288572
    local c1, c2, c3, c4, c5, c6 = -0.007784894002430293, -0.3223964580411365, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783
    local d1, d2, d3, d4 = 0.007784695709041462, 0.3224671290700398, 2.445134137142996, 3.754408661907416
    local p_low = 0.02425
    local p_high = 1.0 - p_low
    local q, r
    if p < p_low then
        q = math.sqrt(-2.0 * math.log(p))
        return (((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) / ((((d1 * q + d2) * q + d3) * q + d4) * q + 1.0)
    elseif p <= p_high then
        q = p - 0.5
        r = q * q
        return (((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q / (((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1.0)
    else
        q = math.sqrt(-2.0 * math.log(1.0 - p))
        return -(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) / ((((d1 * q + d2) * q + d3) * q + d4) * q + 1.0)
    end
end

-- ==========================================
-- 3. 統計核心演算法實作
-- ==========================================
function S.mean_impl(data)
    local elts = S.extract_list(data)
    local n = #elts
    if n < 1 then S.raise_err("mean requires at least one data point") end
    local total = elts[1]
    for i = 2, n do total = S.v_add(total, elts[i]) end
    return S.v_div(total, n)
end

function S.fmean_impl(data, weights)
    local elts = S.extract_list(data)
    local n = #elts
    if n < 1 then S.raise_err("fmean requires at least one data point") end
    if S.is_none(weights) then
        local sum = 0.0
        for i = 1, n do sum = sum + S.to_float(elts[i]) end
        return sum / n
    else
        local w_elts = S.extract_list(weights)
        if #w_elts ~= n then S.raise_err("allbar weights must be the same length as data") end
        local num, den = 0.0, 0.0
        for i = 1, n do
            local w = S.to_float(w_elts[i])
            num = num + S.to_float(elts[i]) * w
            den = den + w
        end
        if den <= 0.0 then S.raise_err("sum of weights must be greater than zero") end
        return num / den
    end
end

function S.geometric_mean_impl(data)
    local elts = S.extract_list(data)
    local n = #elts
    if n < 1 then S.raise_err("geometric mean requires a non-empty dataset") end
    local log_sum = 0.0
    for i = 1, n do
        local v = S.to_float(elts[i])
        if v ~= v then return 0/0 end
        if v <= 0.0 then S.raise_err("geometric mean requires positive inputs") end
        log_sum = log_sum + math.log(v)
    end
    return math.exp(log_sum / n)
end

function S.harmonic_mean_impl(data, weights)
    local elts = S.extract_list(data)
    local n = #elts
    if n < 1 then S.raise_err("harmonic_mean requires at least one data point") end
    local w_elts = nil
    if not S.is_none(weights) then
        w_elts = S.extract_list(weights)
        if #w_elts ~= n then S.raise_err("Number of weights does not match data size") end
    end
    local sum_w, sum_inv = 0.0, 0.0
    local has_zero = false
    for i = 1, n do
        local x = S.to_float(elts[i])
        if x < 0.0 then S.raise_err("harmonic mean does not support negative values") end
        local w = w_elts and S.to_float(w_elts[i]) or 1.0
        if w < 0.0 then S.raise_err("harmonic mean weights cannot be negative") end
        if w > 0.0 then
            sum_w = sum_w + w
            if x == 0.0 then has_zero = true
            elseif not has_zero then sum_inv = sum_inv + (w / x) end
        end
    end
    if sum_w <= 0.0 then S.raise_err("sum of weights must be greater than zero") end
    if has_zero then return 0 end
    return sum_w / sum_inv
end

function S.median_impl(data)
    local sorted = S.sort_list(S.extract_list(data))
    local n = #sorted
    if n == 0 then S.raise_err("no median for empty data") end
    local mid = math.floor(n / 2)
    if n % 2 == 1 then return sorted[mid + 1] end
    return S.v_div(S.v_add(sorted[mid], sorted[mid + 1]), 2)
end

function S.median_low_impl(data)
    local sorted = S.sort_list(S.extract_list(data))
    local n = #sorted
    if n == 0 then S.raise_err("no median for empty data") end
    return sorted[math.floor((n + 1) / 2)]
end

function S.median_high_impl(data)
    local sorted = S.sort_list(S.extract_list(data))
    local n = #sorted
    if n == 0 then S.raise_err("no median for empty data") end
    return sorted[math.floor(n / 2) + 1]
end

function S.median_grouped_impl(data, interval)
    local sorted = S.sort_list(S.extract_list(data))
    local n = #sorted
    if n == 0 then S.raise_err("no median for empty data") end
    local intv = S.is_none(interval) and 1.0 or S.to_float(interval)
    if n == 1 then return S.to_float(sorted[1]) end
    local x = S.to_float(sorted[math.floor(n / 2) + 1])
    local cf, f = 0, 0
    for i = 1, n do
        local v = S.to_float(sorted[i])
        if v < x then cf = cf + 1
        elseif v == x then f = f + 1 end
    end
    local L = x - intv / 2.0
    return L + intv * (n / 2.0 - cf) / f
end

function S.multimode_impl(data)
    local elts = S.extract_list(data)
    if #elts == 0 then return { __type = "list", elements = {} } end
    local EvalUtils = require("Module:PythonScript/Evaluator/Utils")
    local counts, unique_vals = {}, {}
    local max_c = 0
    for i = 1, #elts do
        local v = elts[i]
        local found_idx = nil
        for j = 1, #unique_vals do
            if EvalUtils.py_eq(unique_vals[j], v) then found_idx = j; break end
        end
        if not found_idx then
            table.insert(unique_vals, v)
            found_idx = #unique_vals
            counts[found_idx] = 0
        end
        local c = counts[found_idx] + 1
        counts[found_idx] = c
        if c > max_c then max_c = c end
    end
    local res = {}
    for j = 1, #unique_vals do
        if counts[j] == max_c then table.insert(res, unique_vals[j]) end
    end
    return { __type = "list", elements = res }
end

function S.mode_impl(data)
    local mm = S.multimode_impl(data)
    if #mm.elements == 0 then S.raise_err("no mode for empty data") end
    return mm.elements[1]
end

function S.quantiles_impl(data, n, method)
    n = S.is_none(n) and 4 or S.to_float(n)
    method = S.is_none(method) and "exclusive" or method
    if n < 1 or math.floor(n) ~= n then S.raise_err("n must be at least 1") end
    local sorted = S.sort_list(S.extract_list(data))
    local ld = #sorted
    if ld < 2 then S.raise_err("must have at least two data points") end
    local res = {}
    if method == "inclusive" then
        local m = ld - 1
        for i = 1, n - 1 do
            local j = math.floor(i * m / n)
            local delta = (i * m) - (j * n)
            local interpolated = (S.to_float(sorted[j + 1]) * (n - delta) + S.to_float(sorted[j + 2]) * delta) / n
            table.insert(res, interpolated)
        end
    elseif method == "exclusive" then
        local m = ld + 1
        for i = 1, n - 1 do
            local j = math.floor(i * m / n)
            if j < 1 then j = 1 elseif j > ld - 1 then j = ld - 1 end
            local delta = (i * m) - (j * n)
            local interpolated = (S.to_float(sorted[j]) * (n - delta) + S.to_float(sorted[j + 1]) * delta) / n
            table.insert(res, interpolated)
        end
    else
        Runtime.raise("ValueError", "unknown method: " .. tostring(method))
    end
    return { __type = "list", elements = res }
end

function S.var_core(data, center, is_sample)
    local elts = S.extract_list(data)
    local n = #elts
    if is_sample and n < 2 then S.raise_err("variance requires at least two data points") end
    if not is_sample and n < 1 then S.raise_err("pvariance requires at least one data point") end
    local m = S.is_none(center) and S.mean_impl(data) or center
    local sum_sq = S.is_dec(m) and S.v_sub(m, m) or 0.0
    for i = 1, n do
        local diff = S.v_sub(elts[i], m)
        sum_sq = S.v_add(sum_sq, S.v_mul(diff, diff))
    end
    return S.v_div(sum_sq, is_sample and (n - 1) or n)
end

function S.covariance_impl(x, y)
    local ex, ey = S.extract_list(x), S.extract_list(y)
    local n = #ex
    if #ey ~= n then S.raise_err("covariance requires that both inputs have same number of data points") end
    if n < 2 then S.raise_err("covariance requires at least two data points") end
    local mx, my = S.fmean_impl(x), S.fmean_impl(y)
    local sxy = 0.0
    for i = 1, n do
        sxy = sxy + (S.to_float(ex[i]) - mx) * (S.to_float(ey[i]) - my)
    end
    return sxy / (n - 1)
end

function S.correlation_impl(x, y)
    local ex, ey = S.extract_list(x), S.extract_list(y)
    local n = #ex
    if #ey ~= n then S.raise_err("correlation requires that both inputs have same number of data points") end
    if n < 2 then S.raise_err("correlation requires at least two data points") end
    local mx, my = S.fmean_impl(x), S.fmean_impl(y)
    local sxx, syy, sxy = 0.0, 0.0, 0.0
    for i = 1, n do
        local dx, dy = S.to_float(ex[i]) - mx, S.to_float(ey[i]) - my
        sxx = sxx + dx * dx
        syy = syy + dy * dy
        sxy = sxy + dx * dy
    end
    if sxx == 0.0 or syy == 0.0 then S.raise_err("at least one of the inputs is constant") end
    local r = sxy / math.sqrt(sxx * syy)
    if r > 1.0 then r = 1.0 elseif r < -1.0 then r = -1.0 end
    return r
end

function S.linear_regression_impl(x, y, proportional)
    local ex, ey = S.extract_list(x), S.extract_list(y)
    local n = #ex
    if #ey ~= n then S.raise_err("linear regression requires that both inputs have same number of data points") end
    if n < 2 then S.raise_err("linear regression requires at least two data points") end
    local sxx, sxy, mx, my = 0.0, 0.0, 0.0, 0.0
    if proportional then
		for i = 1, n do
			local xi, yi = S.to_float(ex[i]), S.to_float(ey[i])
			sxx = sxx + xi * xi
			sxy = sxy + xi * yi
		end
	else
		mx, my = S.fmean_impl(x), S.fmean_impl(y)
		for i = 1, n do
            local dx, dy = S.to_float(ex[i]) - mx, S.to_float(ey[i]) - my
            sxx = sxx + dx * dx
            sxy = sxy + dx * dy
        end
    end
    if sxx == 0.0 then S.raise_err("x is constant") end
    local slope = sxy / sxx
    local intercept = proportional and 0.0 or (my - slope * mx)
    if not S.linreg_cls then
        local col_mod = require("Module:PythonScript/Librarys").get_module("collections")
        local nt_fn = PyClass.get_attr(col_mod, "namedtuple")
        S.linreg_cls = nt_fn.__call({ "LinearRegression", { __type = "list", elements = { "slope", "intercept" } } })
    end
    return require("Module:PythonScript/Evaluator").invoke(S.linreg_cls, { slope, intercept }, {})
end

-- ==========================================
-- 4. 延遲初始化 StatisticsError、NormalDist 與模組字典
-- ==========================================
function S.make_normal_dist(mu, sigma)
    S.ensure_init()
    if sigma < 0.0 then S.raise_err("sigma must be non-negative") end
    local inst = PyClass.make_instance(S.cache.NormalDist)
    inst.__dict._is_normaldist = true
    inst.__dict._mu = mu * 1.0
    inst.__dict._sigma = sigma * 1.0
    return inst
end

function S.is_normal_dist(v)
    return type(v) == "table" and v.__type == "instance" and v.__dict and v.__dict._is_normaldist == true
end

function S.ensure_init()
    if S.initialized then return end
    S.initialized = true

    local B = Runtime.builtins
    local C = S.cache

    C.StatisticsError = PyClass.make_class("StatisticsError", { B.ValueError }, {})
    C.StatisticsError.__dict["__module__"] = "statistics"

    -- NormalDist 類別定義
    local function make_prop(fn)
        return { __type = "property", fget = { __type = "builtin_method", __call = fn } }
    end

    local nd_dict = {
        __init__ = { __type = "builtin_method", __call = function(a, kw)
            local self = a[1]
            local mu = (a[2] ~= nil and a[2]) or (kw and kw.mu) or 0.0
            local sigma = (a[3] ~= nil and a[3]) or (kw and kw.sigma) or 1.0
            mu, sigma = S.to_float(mu), S.to_float(sigma)
            if sigma < 0.0 then S.raise_err("sigma must be non-negative") end
            self.__dict._is_normaldist = true
            self.__dict._mu = mu
            self.__dict._sigma = sigma
            return nil
        end },
        mean = make_prop(function(a) return a[1].__dict._mu end),
        median = make_prop(function(a) return a[1].__dict._mu end),
        mode = make_prop(function(a) return a[1].__dict._mu end),
        stdev = make_prop(function(a) return a[1].__dict._sigma end),
        variance = make_prop(function(a) local s = a[1].__dict._sigma; return s * s end),

        from_samples = { __type = "classmethod", __func = { __type = "builtin_method", __name = "from_samples", __call = function(a)
            local cls, data = a[1], a[2]
            local mu = S.fmean_impl(data)
            local sigma = math.sqrt(S.to_float(S.var_core(data, mu, true)))
            local inst = S.make_normal_dist(mu, sigma)
            inst.__class = cls
            return inst
        end } },

        samples = { __type = "builtin_method", __call = function(a, kw)
            local self, n = a[1], S.to_float(a[2])
            local seed = kw and kw.seed
            local rand_mod = require("Module:PythonScript/Librarys/Random").get_module()
            local Evaluator = require("Module:PythonScript/Evaluator")
            local gauss_fn
            if not S.is_none(seed) then
                local rng = Evaluator.invoke(PyClass.get_attr(rand_mod, "Random"), {seed}, {})
                gauss_fn = PyClass.get_attr(rng, "gauss")
            else
                gauss_fn = PyClass.get_attr(rand_mod, "gauss")
            end
            local mu, sigma = self.__dict._mu, self.__dict._sigma
            local res = {}
            for i = 1, n do
                res[i] = Evaluator.invoke(gauss_fn, {mu, sigma}, {})
            end
            return { __type = "list", elements = res }
        end },

        pdf = { __type = "builtin_method", __call = function(a)
            local self, x = a[1], S.to_float(a[2])
            local mu, sigma = self.__dict._mu, self.__dict._sigma
            if sigma == 0.0 then S.raise_err("pdf() not defined when sigma is zero") end
            local z = (x - mu) / sigma
            return math.exp(-0.5 * z * z) / (sigma * math.sqrt(2.0 * math.pi))
        end },

        cdf = { __type = "builtin_method", __call = function(a)
            local self, x = a[1], S.to_float(a[2])
            local mu, sigma = self.__dict._mu, self.__dict._sigma
            if sigma == 0.0 then S.raise_err("cdf() not defined when sigma is zero") end
            return 0.5 * (1.0 + S.erf((x - mu) / (sigma * math.sqrt(2.0))))
        end },

        inv_cdf = { __type = "builtin_method", __call = function(a)
            local self, p = a[1], S.to_float(a[2])
            return self.__dict._mu + self.__dict._sigma * S.std_inv_cdf(p)
        end },

        quantiles = { __type = "builtin_method", __call = function(a, kw)
            local self = a[1]
            local n = (a[2] ~= nil and a[2]) or (kw and kw.n) or 4
            n = S.to_float(n)
            if n < 1 or math.floor(n) ~= n then S.raise_err("n must be at least 1") end
            local mu, sigma = self.__dict._mu, self.__dict._sigma
            local res = {}
            for i = 1, n - 1 do
                res[i] = mu + sigma * S.std_inv_cdf(i / n)
            end
            return { __type = "list", elements = res }
        end },

        zscore = { __type = "builtin_method", __call = function(a)
            local self, x = a[1], S.to_float(a[2])
            local mu, sigma = self.__dict._mu, self.__dict._sigma
            if sigma == 0.0 then S.raise_err("zscore() not defined when sigma is zero") end
            return (x - mu) / sigma
        end },

        overlap = { __type = "builtin_method", __call = function(a)
            local self, other = a[1], a[2]
            if not S.is_normal_dist(other) then Runtime.raise("TypeError", "Expected another NormalDist instance") end
            local x1, y1 = self.__dict._mu, self.__dict._sigma
            local x2, y2 = other.__dict._mu, other.__dict._sigma
            if y1 == 0.0 or y2 == 0.0 then S.raise_err("overlap() not defined when sigma is zero") end
            if y2 < y1 then x1, y1, x2, y2 = x2, y2, x1, y1 end
            local dv = y1 * y1 - y2 * y2
            local dm = math.abs(x2 - x1)
            if dv == 0.0 then
                return 1.0 - S.erf(dm / (2.0 * y1 * math.sqrt(2.0)))
            end
            local a_val = x1 * y2 * y2 - x2 * y1 * y1
            local b_val = y1 * y2 * math.sqrt(dm * dm + dv * math.log((y1 * y1) / (y2 * y2)))
            local pt1, pt2 = (a_val + b_val) / dv, (a_val - b_val) / dv
            local cdf1 = function(v) return 0.5 * (1.0 + S.erf((v - x1) / (y1 * math.sqrt(2.0)))) end
            local cdf2 = function(v) return 0.5 * (1.0 + S.erf((v - x2) / (y2 * math.sqrt(2.0)))) end
            return 1.0 - (math.abs(cdf2(pt1) - cdf1(pt1)) + math.abs(cdf2(pt2) - cdf1(pt2)))
        end },

        -- 運算子多載
        __add__ = { __type = "builtin_method", __call = function(a)
            local self, other = a[1], a[2]
            if S.is_normal_dist(other) then
                local s1, s2 = self.__dict._sigma, other.__dict._sigma
                return S.make_normal_dist(self.__dict._mu + other.__dict._mu, math.sqrt(s1 * s1 + s2 * s2))
            end
            return S.make_normal_dist(self.__dict._mu + S.to_float(other), self.__dict._sigma)
        end },
        __radd__ = { __type = "builtin_method", __call = function(a)
            return S.make_normal_dist(S.to_float(a[2]) + a[1].__dict._mu, a[1].__dict._sigma)
        end },
        __sub__ = { __type = "builtin_method", __call = function(a)
            local self, other = a[1], a[2]
            if S.is_normal_dist(other) then
                local s1, s2 = self.__dict._sigma, other.__dict._sigma
                return S.make_normal_dist(self.__dict._mu - other.__dict._mu, math.sqrt(s1 * s1 + s2 * s2))
            end
            return S.make_normal_dist(self.__dict._mu - S.to_float(other), self.__dict._sigma)
        end },
        __rsub__ = { __type = "builtin_method", __call = function(a)
            return S.make_normal_dist(S.to_float(a[2]) - a[1].__dict._mu, a[1].__dict._sigma)
        end },
        __mul__ = { __type = "builtin_method", __call = function(a)
            local c = S.to_float(a[2])
            return S.make_normal_dist(a[1].__dict._mu * c, a[1].__dict._sigma * math.abs(c))
        end },
        __truediv__ = { __type = "builtin_method", __call = function(a)
            local c = S.to_float(a[2])
            if c == 0.0 then Runtime.raise("ZeroDivisionError", "NormalDist division by zero") end
            return S.make_normal_dist(a[1].__dict._mu / c, a[1].__dict._sigma / math.abs(c))
        end },
        __pos__ = { __type = "builtin_method", __call = function(a)
            return S.make_normal_dist(a[1].__dict._mu, a[1].__dict._sigma)
        end },
        __neg__ = { __type = "builtin_method", __call = function(a)
            return S.make_normal_dist(-a[1].__dict._mu, a[1].__dict._sigma)
        end },
        __eq__ = { __type = "builtin_method", __call = function(a)
            local self, other = a[1], a[2]
            if not S.is_normal_dist(other) then return false end
            return self.__dict._mu == other.__dict._mu and self.__dict._sigma == other.__dict._sigma
        end },
        __hash__ = { __type = "builtin_method", __call = function(a)
            return B.hash({ { __type = "tuple", elements = { a[1].__dict._mu, a[1].__dict._sigma } } })
        end },
        __repr__ = { __type = "builtin_method", __call = function(a)
            return string.format("NormalDist(mu=%s, sigma=%s)", B.repr({a[1].__dict._mu}), B.repr({a[1].__dict._sigma}))
        end }
    }
    nd_dict.__rmul__ = nd_dict.__mul__
    nd_dict.__str__ = nd_dict.__repr__
    C.NormalDist = PyClass.make_class("NormalDist", {}, nd_dict)
    C.NormalDist.__dict["__module__"] = "statistics"

    -- 模組層級函式包裝
    local function wrap(name, fn)
        C[name] = { __type = "builtin_method", __name = name, __call = fn }
    end

    wrap("mean", function(a) return S.mean_impl(a[1]) end)
    wrap("fmean", function(a, kw) return S.fmean_impl(a[1], (a[2] ~= nil and a[2]) or (kw and kw.weights)) end)
    wrap("geometric_mean", function(a) return S.geometric_mean_impl(a[1]) end)
    wrap("harmonic_mean", function(a, kw) return S.harmonic_mean_impl(a[1], (a[2] ~= nil and a[2]) or (kw and kw.weights)) end)
    wrap("median", function(a) return S.median_impl(a[1]) end)
    wrap("median_low", function(a) return S.median_low_impl(a[1]) end)
    wrap("median_high", function(a) return S.median_high_impl(a[1]) end)
    wrap("median_grouped", function(a, kw) return S.median_grouped_impl(a[1], (a[2] ~= nil and a[2]) or (kw and kw.interval)) end)
    wrap("mode", function(a) return S.mode_impl(a[1]) end)
    wrap("multimode", function(a) return S.multimode_impl(a[1]) end)
    wrap("quantiles", function(a, kw)
        local n = (a[2] ~= nil and a[2]) or (kw and kw.n) or 4
        local method = (a[3] ~= nil and a[3]) or (kw and kw.method) or "exclusive"
        return S.quantiles_impl(a[1], n, method)
    end)
    wrap("pvariance", function(a, kw) return S.var_core(a[1], (a[2] ~= nil and a[2]) or (kw and kw.mu), false) end)
    wrap("pstdev", function(a, kw) return S.v_sqrt(S.var_core(a[1], (a[2] ~= nil and a[2]) or (kw and kw.mu), false)) end)
    wrap("variance", function(a, kw) return S.var_core(a[1], (a[2] ~= nil and a[2]) or (kw and kw.xbar), true) end)
    wrap("stdev", function(a, kw) return S.v_sqrt(S.var_core(a[1], (a[2] ~= nil and a[2]) or (kw and kw.xbar), true)) end)
    wrap("covariance", function(a) return S.covariance_impl(a[1], a[2]) end)
    wrap("correlation", function(a) return S.correlation_impl(a[1], a[2]) end)
    wrap("linear_regression", function(a, kw)
        local prop = (a[3] ~= nil and a[3]) or (kw and kw.proportional) or false
        return S.linear_regression_impl(a[1], a[2], B.bool.__call({prop}))
    end)
end

-- ==========================================
-- 5. 模組匯出介面 (Lazy Loading)
-- ==========================================
function StatisticsMod.get_module()
    local mod_dict = {
        __getattr__ = {
            __type = "builtin_method",
            __call = function(args)
                local self, attr = args[1], args[2]
                S.ensure_init()
                local val = S.cache[attr]
                if val ~= nil then
                    self.__dict[attr] = val
                    return val
                end
                Runtime.raise("AttributeError", "module 'statistics' has no attribute '" .. tostring(attr) .. "'")
            end
        }
    }
    local mod = PyClass.make_instance(PyClass.make_class("module", {}, mod_dict))
    mod.__dict["__name__"] = "statistics"
    mod.__dict["__all__"] = {
        __type = "list",
        elements = {
            "NormalDist", "StatisticsError",
            "fmean", "geometric_mean", "harmonic_mean", "mean",
            "median", "median_grouped", "median_high", "median_low",
            "mode", "multimode", "pstdev", "pvariance", "quantiles",
            "stdev", "variance", "covariance", "correlation", "linear_regression"
        }
    }
    return mod
end

return StatisticsMod