import argparse
import os
import json
import pandas as pd
import numpy as np
import statsmodels.api as sm
from statsmodels.stats.diagnostic import acorr_breusch_godfrey
def convert_categorical_to_numeric(data, dataset_name):
non_numeric_cols = data.select_dtypes(exclude=[np.number]).columns.tolist()
if not non_numeric_cols:
return []
print(f"INFO: Dataset '{dataset_name}' contains non-numeric columns: {non_numeric_cols}")
print(f"Converting categorical variables to numeric representations...")
for col in non_numeric_cols:
if data[col].dtype == 'object':
data[col] = pd.to_numeric(data[col], errors='coerce')
if data[col].isnull().any():
mode_vals = data[col].mode()
if len(mode_vals) > 0:
mode_val = mode_vals.iloc[0]
else:
mode_val = 0
data[col].fillna(mode_val, inplace=True)
print(f" {col}: {len(data[col].unique())} unique values -> integer encoding (missing filled with mode: {mode_val})")
else:
print(f" {col}: {len(data[col].unique())} unique values -> integer encoding")
else:
pass
return non_numeric_cols
def main():
parser = argparse.ArgumentParser(
description="Breusch-Godfrey Test - Check higher-order serial correlation using statsmodels"
)
parser.add_argument(
"--csv",
default="../../datasets/csv/synthetic_autocorrelated.csv",
help="Path to CSV file (first column = response, rest = predictors)"
)
parser.add_argument(
"--output-dir",
default="../../results/python",
help="Path to output directory"
)
parser.add_argument(
"--order",
type=int,
default=1,
help="Order of serial correlation to test (default: 1)"
)
args = parser.parse_args()
if not os.path.exists(args.csv):
raise FileNotFoundError(f"CSV file not found: {args.csv}")
dataset_name = os.path.splitext(os.path.basename(args.csv))[0]
data = pd.read_csv(args.csv)
non_numeric_cols = data.select_dtypes(exclude=[np.number]).columns.tolist()
if non_numeric_cols:
convert_categorical_to_numeric(data, dataset_name)
remaining_non_numeric = data.select_dtypes(exclude=[np.number]).columns.tolist()
if remaining_non_numeric:
raise ValueError(f"Could not convert the following non-numeric columns to numeric: {remaining_non_numeric}")
response_col = data.columns[0]
predictor_cols = data.columns[1:]
X = data[predictor_cols]
X = sm.add_constant(X)
y = data[response_col]
formula_str = f"{response_col} ~ {' + '.join(predictor_cols)}"
model = sm.OLS(y, X).fit()
lm_stat, lm_pval, f_stat, f_pval = acorr_breusch_godfrey(model, nlags=args.order)
n = len(y)
k = len(predictor_cols) + 1 df_lm = args.order df_f_num = args.order df_f_den = n - k - args.order
alpha = 0.05
if lm_pval > alpha:
interpretation = f"No significant serial correlation detected up to order {args.order}"
else:
interpretation = f"Significant serial correlation detected at order <= {args.order}"
print("Breusch-Godfrey Test (Python - statsmodels)")
print("=" * 45)
print(f"Dataset: {dataset_name}")
print(f"Formula: {formula_str}")
print(f"Order: {args.order}")
print()
print("Chi-squared (LM) statistic:", lm_stat)
print(" p-value:", lm_pval)
print(" df:", df_lm)
print()
print("F statistic:", f_stat)
print(" p-value:", f_pval)
print(" df:", f"{df_f_num}, {df_f_den}")
print()
print(f"Interpretation: {interpretation}")
print(f"Passed (p > 0.05): {lm_pval > alpha}")
print()
output = {
"test_name": "Breusch-Godfrey Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": formula_str,
"order": args.order,
"statistic": float(lm_stat),
"p_value": float(lm_pval),
"test_type": "Chisq",
"df": [float(df_lm)],
"f_statistic": float(f_stat),
"f_p_value": float(f_pval),
"f_df": [float(df_f_num), float(df_f_den)],
"passed": bool(lm_pval > alpha),
"interpretation": interpretation,
"description": f"Tests for serial correlation up to order {args.order}. The LM (Chi-squared) statistic is asymptotically distributed as chi-squared with {args.order} degrees of freedom. The F statistic provides a finite-sample correction. Uses statsmodels.stats.diagnostic.acorr_breusch_godfrey."
}
os.makedirs(args.output_dir, exist_ok=True)
output_file = os.path.join(args.output_dir, f"{dataset_name}_breusch_godfrey.json")
with open(output_file, 'w') as f:
json.dump(output, f, indent=2)
print(f"Results saved to: {output_file}")
if __name__ == "__main__":
main()