import numpy as np import pandas as pd import statsmodels.api as sm from scipy.stats import t as tdist, f as fdist, spearmanr import matplotlib.pyplot as plt def print_table(df: pd.DataFrame, title: str, floatfmt="{:,.6f}".format): print("=" * (len(title) + 2)) print(title) print("=" * (len(title) + 2)) df_print = df.copy() for c in df_print.columns: if np.issubdtype(df_print[c].dtype, np.number): df_print[c] = df_print[c].map(lambda v: floatfmt(v)) print(df_print.to_string(index=False)) print() def _add_const(x): return sm.add_constant(np.asarray(x, float)) def table1_base(x, y): x = np.asarray(x, float); y = np.asarray(y, float) df = pd.DataFrame({ "i": np.arange(1, len(x)+1), "x": x, "y": y, "x^2": x**2, "x*y": x*y, "y^2": y**2 }) print_table(df, "Таблиця 1. Базові розрахунки") return df def fit_ols(x, y): X = _add_const(x) model = sm.OLS(y, X).fit() a0, a1 = model.params se0, se1 = model.bse t0, t1 = model.tvalues p0, p1 = model.pvalues r2 = model.rsquared rss = np.sum(model.resid**2) df = int(model.df_resid) df_out = pd.DataFrame([{ "a0": a0, "a1": a1, "SE(a0)": se0, "SE(a1)": se1, "t(a0)": t0, "p(a0)": p0, "t(a1)": t1, "p(a1)": p1, "R2": r2, "RSS": rss, "df": df }]) print_table(df_out, "OLS: параметри та статистики") yhat = model.fittedvalues u = model.resid df2 = pd.DataFrame({ "i": np.arange(1, len(x)+1), "x": x, "y": y, "ŷ": yhat, "u": u, "u^2": u**2, "|u|": np.abs(u) }) print_table(df2, "Таблиця 2. Залишки та прогнози (OLS)") return model, df2 def var_bins(x, vals, nbins=3): order = np.argsort(x) x_sorted = np.asarray(x)[order] v_sorted = np.asarray(vals)[order] groups = np.array_split(np.arange(len(x)), nbins) rows = [] for b, idx in enumerate(groups, start=1): xx = x_sorted[idx] vv = v_sorted[idx] rows.append({"bin": b, "x_min": xx.min(), "x_max": xx.max(), "n": len(idx), "Var(u)": vv.var(ddof=1)}) return pd.DataFrame(rows) def show_bins_ols(x, u): dfbins = var_bins(x, u, 3) print_table(dfbins, "Таблиця 3. Розкид Var(u) у 3 біннах x (OLS)") return dfbins def spearman_test(x, z, alpha=0.05, label="|u|"): rho, p = spearmanr(x, z) n = len(x) if abs(rho) < 1.0: t_stat = rho * np.sqrt((n - 2) / (1 - rho**2)) else: t_stat = np.sign(rho) * np.inf tcrit = tdist.ppf(1 - alpha/2, df=n-2) out = pd.DataFrame([{ "rho": rho, "t_stat": t_stat, "t_crit(α/2)": tcrit, "p_value": p, "reject_H0": bool(p < alpha) }]) print_table( pd.DataFrame({ "i": np.arange(1, n+1), "x": x, f"|{label}|": z, "rank_x": pd.Series(x).rank(method="average"), f"rank_|{label}|": pd.Series(z).rank(method="average"), "(Δrank)^2": (pd.Series(x).rank(method="average") - pd.Series(z).rank(method="average"))**2 }), f"Таблиця 4. Спірмен — ранги x та |{label}|" ) print_table(out, "Підсумок тесту Спірмена") return rho, p def gq_groups(x, y, m=4, c=2): order = np.argsort(x) x_sorted, y_sorted = np.asarray(x)[order], np.asarray(y)[order] left_idx = np.arange(0, m) right_idx = np.arange(len(x_sorted)-m, len(x_sorted)) mid_cut = slice(m, len(x_sorted)-m) if c > 0: right_idx = np.arange(len(x_sorted)-m, len(x_sorted)) Xl = _add_const(x_sorted[left_idx]); yl = y_sorted[left_idx] Xr = _add_const(x_sorted[right_idx]); yr = y_sorted[right_idx] ols_l = sm.OLS(yl, Xl).fit() ols_r = sm.OLS(yr, Xr).fit() left_tbl = pd.DataFrame({ "i": order[left_idx] + 1, "x": x_sorted[left_idx], "y": yl, "ŷ(group)": ols_l.fittedvalues, "u(group)": ols_l.resid, "u^2(group)": ols_l.resid**2 }) right_tbl = pd.DataFrame({ "i": order[right_idx] + 1, "x": x_sorted[right_idx], "y": yr, "ŷ(group)": ols_r.fittedvalues, "u(group)": ols_r.resid, "u^2(group)": ols_r.resid**2 }) return left_tbl.sort_values("i"), right_tbl.sort_values("i"), ols_l, ols_r def gq_summary_compact(left_fit, right_fit, m, alpha=0.05): var_left = left_fit.resid.var(ddof=1) var_right = right_fit.resid.var(ddof=1) F = var_left / var_right df_m1 = m - 1 Fcrit = fdist.ppf(1 - alpha, df_m1, df_m1) reject = bool(F > Fcrit) out = pd.DataFrame([{ "m": m, "c": 2, "df_m1": df_m1, "RSS_left": np.sum(left_fit.resid**2), "RSS_right": np.sum(right_fit.resid**2), "F_stat": F, "Fcrit_df_m1(one-sided)": Fcrit, "reject_H0": reject }]) return out def run_gq(x, y, m=4, c=2, alpha=0.05, prefix=""): left_tbl, right_tbl, ols_l, ols_r = gq_groups(x, y, m, c) print_table(left_tbl, f"Таблиця 5. Група LEFT для тесту ГК (за x↑)") print_table(right_tbl, f"Таблиця 6. Група RIGHT для тесту ГК (за x↑)") compact = gq_summary_compact(ols_l, ols_r, m, alpha) print_table(compact, "Зведена таблиця тесту Голдфелда–Квандта") return compact def glejser_table(x, yhat, uabs, deltas=(1,2,3,0.5,1/3), alpha=0.05, label="|u|"): rows = [] long_tbl = pd.DataFrame({ "i": np.arange(1, len(x)+1), "x": x, "y": y, "x^2": x**2, "ŷ": yhat, "|u|": uabs, "x*|u|": x*uabs }) for delta in deltas: X = _add_const(x**delta) mod = sm.OLS(uabs, X).fit() b0, b1 = mod.params se_b1 = mod.bse[1] t_ratio = abs(b1) / se_b1 if se_b1 > 0 else np.inf tcrit = tdist.ppf(1 - alpha/2, df=len(x)-2) pval = mod.pvalues[1] rows.append({ "δ": delta, "b0": b0, "b1": b1, "SE(b1)": se_b1, "|b1|/SE(b1)": t_ratio, "t_crit(α/2)": tcrit, "p-value": pval, "Sε^2": np.mean(mod.resid**2), "decision": "ВІДХИЛЯЄМО H0" if pval < alpha else "Приймаємо H0" }) long_tbl[f"û({label})_δ={delta}"] = mod.fittedvalues long_tbl[f"({label}-û)_δ={delta}^2"] = (uabs - mod.fittedvalues)**2 short_tbl = pd.DataFrame(rows) print_table(long_tbl, f"Таблиця 7а. Тест Глейзера — покрокова таблиця") print_table(short_tbl, "Таблиця 7b. Тест Глейзера — параметри й рішення по δ") return short_tbl, long_tbl def choose_delta_for_weights(glejser_short, alpha=0.05): ok = glejser_short[glejser_short["p-value"] >= alpha] if len(ok) == 0: best = glejser_short.sort_values("Sε^2").iloc[0] else: best = ok.iloc[ok["b1"].abs().argmin()] return float(best["δ"]), best def wls_fit_with_weights(x, y, delta, glejser_short): row = glejser_short.loc[glejser_short["δ"] == delta].iloc[0] b0, b1 = row["b0"], row["b1"] sigma_hat = b0 + b1 * (x ** delta) mn = sigma_hat.min() if mn <= 0: sigma_hat = sigma_hat + (abs(mn) + 1e-8) w = 1.0 / (sigma_hat ** 2) model = sm.WLS(y, _add_const(x), weights=w).fit() df_out = pd.DataFrame([{ "a0": model.params[0], "a1": model.params[1], "SE(a0)": model.bse[0], "SE(a1)": model.bse[1], "t(a0)": model.tvalues[0], "p(a0)": model.pvalues[0], "t(a1)": model.tvalues[1], "p(a1)": model.pvalues[1], "R2": model.rsquared, "RSS": np.sum(model.resid**2), "df": int(model.df_resid) }]) print_table(df_out, "WLS: параметри та статистики (після усунення)") comp = pd.DataFrame({ "model": ["OLS", "WLS"], "a0": [np.nan, np.nan], "a1": [np.nan, np.nan], "SE(a0)": [np.nan, np.nan], "SE(a1)": [np.nan, np.nan], "t(a0)": [np.nan, np.nan], "p(a0)": [np.nan, np.nan], "t(a1)": [np.nan, np.nan], "p(a1)": [np.nan, np.nan], "R2": [np.nan, np.nan], "RSS": [np.nan, np.nan], "df": [np.nan, np.nan], }) return model, sigma_hat def compare_ols_wls(ols_model, wls_model): comp = pd.DataFrame({ "model": ["OLS", "WLS"], "a0": [ols_model.params[0], wls_model.params[0]], "a1": [ols_model.params[1], wls_model.params[1]], "SE(a0)": [ols_model.bse[0], wls_model.bse[0]], "SE(a1)": [ols_model.bse[1], wls_model.bse[1]], "t(a0)": [ols_model.tvalues[0], wls_model.tvalues[0]], "p(a0)": [ols_model.pvalues[0], wls_model.pvalues[0]], "t(a1)": [ols_model.tvalues[1], wls_model.tvalues[1]], "p(a1)": [ols_model.pvalues[1], wls_model.pvalues[1]], "R2": [ols_model.rsquared, wls_model.rsquared], "RSS": [np.sum(ols_model.resid**2), np.sum(wls_model.resid**2)], "df": [int(ols_model.df_resid), int(wls_model.df_resid)] }) print_table(comp, "Таблиця 8. Порівняння OLS vs WLS") return comp def standardized_residuals(y, yhat, sigma_hat): u = y - yhat rstar = u / sigma_hat return u, rstar def plot_diagnostics_ols(x, y, yhat, u, nbins=3): fig, axes = plt.subplots(2, 2, figsize=(10, 8)) axes = axes.ravel() axes[0].scatter(x, u) axes[0].axhline(0, linewidth=1) axes[0].set_title("OLS: u vs x"); axes[0].set_xlabel("x"); axes[0].set_ylabel("u") axes[1].scatter(yhat, u) axes[1].axhline(0, linewidth=1) axes[1].set_title("OLS: u vs ŷ"); axes[1].set_xlabel("ŷ"); axes[1].set_ylabel("u") axes[2].scatter(x, np.abs(u)) axes[2].set_title("OLS: |u| vs x"); axes[2].set_xlabel("x"); axes[2].set_ylabel("|u|") bins = var_bins(x, u, nbins) axes[3].bar(bins["bin"], bins["Var(u)"]) axes[3].set_title("OLS: Var(u) у біннах x"); axes[3].set_xlabel("бін"); axes[3].set_ylabel("Var") fig.suptitle("Візуальна діагностика — OLS") plt.tight_layout() plt.show() def plot_diagnostics_wls(x, yhat_w, rstar, nbins=3): fig, axes = plt.subplots(2, 2, figsize=(10, 8)) axes = axes.ravel() axes[0].scatter(x, rstar) axes[0].axhline(0, linewidth=1) axes[0].set_title("Після WLS (r*): r* vs x"); axes[0].set_xlabel("x"); axes[0].set_ylabel("r*") axes[1].scatter(yhat_w, rstar) axes[1].axhline(0, linewidth=1) axes[1].set_title("Після WLS (r*): r* vs ŷ"); axes[1].set_xlabel("ŷ"); axes[1].set_ylabel("r*") axes[2].scatter(x, np.abs(rstar)) axes[2].set_title("Після WLS (r*): |r*| vs x"); axes[2].set_xlabel("x"); axes[2].set_ylabel("|r*|") bins = var_bins(x, rstar, nbins) axes[3].bar(bins["bin"], bins["Var(u)"]) axes[3].set_title("Після WLS (r*): Var(r*) у біннах x"); axes[3].set_xlabel("бін"); axes[3].set_ylabel("Var") fig.suptitle("Візуальна діагностика — Після WLS (r*)") plt.tight_layout() plt.show() def run_pipeline(x, y, alpha=0.05, deltas=(1,2,3,0.5,1/3), m=4, c=2, make_plots=True): x = np.asarray(x, float); y = np.asarray(y, float) table1_base(x, y) ols_model, tbl2 = fit_ols(x, y) yhat = ols_model.fittedvalues u = ols_model.resid show_bins_ols(x, u) spearman_test(x, np.abs(u), alpha, label="u") gq_compact_ols = run_gq(x, y, m=m, c=c, alpha=alpha) glejser_short, glejser_long = glejser_table(x, yhat, np.abs(u), deltas, alpha) delta_best, row_best = choose_delta_for_weights(glejser_short, alpha) wls_model, sigma_hat = wls_fit_with_weights(x, y, delta_best, glejser_short) compare_ols_wls(ols_model, wls_model) yhat_w = wls_model.fittedvalues u_w, rstar = standardized_residuals(y, yhat_w, sigma_hat) df_r = pd.DataFrame({ "i": np.arange(1, len(x)+1), "x": x, "y": y, "ŷ(WLS)": yhat_w, "u(WLS)": u_w, "r*(WLS)": rstar, "r*^2(WLS)": rstar**2, "|r*|(WLS)": np.abs(rstar) }) print_table(df_r, "Таблиця 2*. Прогнози та залишки (WLS) з r* = u/σ̂") df_bins_r = var_bins(x, rstar, 3).rename(columns={"Var(u)": "Var(r*)"}) print_table(df_bins_r, "Таблиця 3*. Розкид Var(r*) у 3 біннах x (після WLS)") spearman_rho_r, p_r = spearman_test(x, np.abs(rstar), alpha, label="r*") left_tbl_r, right_tbl_r, fit_l_r, fit_r_r = gq_groups(x, rstar, m, c) left_r = pd.DataFrame({"i": left_tbl_r["i"], "x": left_tbl_r["x"], "r*(WLS)": fit_l_r.resid + fit_l_r.fittedvalues, "r*(WLS)^2": (fit_l_r.resid + fit_l_r.fittedvalues)**2}) right_r = pd.DataFrame({"i": right_tbl_r["i"], "x": right_tbl_r["x"], "r*(WLS)": fit_r_r.resid + fit_r_r.fittedvalues, "r*(WLS)^2": (fit_r_r.resid + fit_r_r.fittedvalues)**2}) print_table(left_r, "Таблиця 5*. LEFT (стандартизовані r*) для тесту ГК") print_table(right_r, "Таблиця 6*. RIGHT (стандартизовані r*) для тесту ГК") gq_compact_r = gq_summary_compact(fit_l_r, fit_r_r, m, alpha) print_table(gq_compact_r, "Зведення тесту Голдфелда–Квандта на r* (F, df=m−1)") glejser_r_short, glejser_r_long = glejser_table( x, yhat_w, np.abs(rstar), deltas, alpha, label="r*" ) homosk_after_wls = ( (p_r >= alpha) and (not bool(gq_compact_r["reject_H0"].iloc[0])) and all(glejser_r_short["p-value"] >= alpha) ) summary_line = f"ПІДСУМОК: Гомоскедастичність після WLS (за r*): {'ТАК' if homosk_after_wls else 'НІ'}; модель ваг за Глейзером: δ={delta_best:g}" print(summary_line) if make_plots: plot_diagnostics_ols(x, y, yhat, u, nbins=3) plot_diagnostics_wls(x, yhat_w, rstar, nbins=3) return { "ols_model": ols_model, "wls_model": wls_model, "sigma_hat": sigma_hat, "glejser_ols": glejser_short, "glejser_wls_r": glejser_r_short, "gq_ols_compact": gq_compact_ols, "gq_r_compact": gq_compact_r, "homosked_after_wls": homosk_after_wls, "delta_best": delta_best } if __name__ == "__main__": x = [0.3, 0.6, 1.1, 0.2, 0.8, 1.6, 1.5, 0.9, 0.9, 0.8] y = [2.8, 4.3, 6.3, 2.9, 5.3, 8.3, 8.0, 5.7, 5.4, 5.2] run_pipeline(x, y, alpha=0.05, deltas=(1,2,3,0.5,1/3), m=4, c=2, make_plots=True)