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)