import argparse
import os
import json
import pandas as pd
import numpy as np
import statsmodels.api as sm
from statsmodels.stats.diagnostic import linear_reset
def validate_for_regression(data, dataset_name):
issues = []
non_numeric_cols = data.select_dtypes(exclude=[np.number]).columns.tolist()
if non_numeric_cols:
issues.append(f"Non-numeric columns detected: {non_numeric_cols}")
missing_counts = data.isnull().sum()
if missing_counts.sum() > 0:
missing_cols = missing_counts[missing_counts > 0].index.tolist()
issues.append(f"Missing values detected in: {missing_cols}")
return issues
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 data, []
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':
encoded, uniques = pd.factorize(data[col])
data[col] = encoded
print(f" {col}: {len(uniques)} unique values -> integer level encoding")
else:
pass
return non_numeric_cols
def main():
parser = argparse.ArgumentParser(
description="RESET Test - Check functional form specification using statsmodels"
)
parser.add_argument(
"--csv",
default="../../datasets/csv/mtcars.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(
"--powers",
default="2,3",
help="Powers to use (e.g., '2,3' for squared and cubed)"
)
parser.add_argument(
"--type",
default="fitted",
choices=["fitted", "regressor", "exog"],
help="Type of terms to add (default: fitted)"
)
args = parser.parse_args()
powers = [int(p.strip()) for p in args.powers.split(",")]
type_map = {
"fitted": "fitted",
"regressor": "exog",
"exog": "exog"
}
sm_type = type_map[args.type]
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)
issues = validate_for_regression(data, dataset_name)
if any("Non-numeric" in issue for issue in issues):
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]
if pd.api.types.is_numeric_dtype(data[response_col]):
predictor_cols = data.columns[1:].tolist()
print(f"Response variable: {response_col} (numeric)")
else:
print(f"INFO: Response variable '{response_col}' is categorical - using one-hot encoding")
data = pd.get_dummies(data, columns=[response_col], drop_first=True)
response_col = data.columns[0]
predictor_cols = data.columns[1:].tolist()
print(f"Predicting '{response_col}' vs all other categories (one-hot encoding)")
X = data[predictor_cols]
X = sm.add_constant(X)
y = data[response_col]
formula_str = f"{response_col} ~ {' + '.join(predictor_cols)}"
try:
model = sm.OLS(y, X).fit()
except Exception as e:
raise ValueError(f"Failed to fit regression model: {e}")
print("\nRESET Test (Python - Custom Implementation)")
print("=" * 50)
print(f"Dataset: {dataset_name}")
print(f"Formula: {formula_str}")
print(f"Powers: {powers}")
print(f"Type: {args.type}")
fitted = model.fittedvalues.values
n = len(y)
k = len(predictor_cols)
p = k + 1
if args.type == "fitted":
Z = np.column_stack([fitted ** power for power in powers])
elif args.type == "regressor" or args.type == "exog":
X_no_intercept = X.values[:, 1:] Z_cols = []
for power in powers:
for col_idx in range(X_no_intercept.shape[1]):
Z_cols.append(X_no_intercept[:, col_idx] ** power)
Z = np.column_stack(Z_cols)
else:
raise ValueError(f"Unsupported type: {args.type}")
q = Z.shape[1]
if n <= p + q:
output = {
"test_name": "RESET Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": formula_str,
"statistic": None,
"p_value": None,
"passed": None,
"skipped": True,
"reason": f"Insufficient degrees of freedom: n={n}, p={p}, q={q}",
"power": powers,
"type": args.type,
"description": "Ramsey's RESET test for functional form misspecification. Tests whether powers of fitted values, regressors, or principal component significantly improve model fit."
}
else:
XZ = np.column_stack([X.values, Z])
try:
model_unrestricted = sm.OLS(y, XZ).fit()
except Exception as e:
if "singular" in str(e).lower() or "multicollinearity" in str(e).lower():
output = {
"test_name": "RESET Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": formula_str,
"statistic": None,
"p_value": None,
"passed": None,
"skipped": True,
"reason": "High multicollinearity detected - cannot reliably perform RESET test",
"power": powers,
"type": args.type,
"description": "Ramsey's RESET test for functional form misspecification. Tests whether powers of fitted values, regressors, or principal component significantly improve model fit."
}
else:
raise
else:
rss_restricted = model.ssr
rss_unrestricted = model_unrestricted.ssr
df1 = q
df2 = n - p - q
f_stat = (df2 / df1) * ((rss_restricted - rss_unrestricted) / rss_unrestricted)
if not np.isfinite(f_stat) or f_stat < 0:
f_stat = 0.0
from scipy import stats
p_value = 1 - stats.f.cdf(f_stat, df1, df2)
passed = p_value > 0.05
print(f"Statistic (F): {f_stat:.6f}")
print(f"p-value: {p_value:.6f}")
print(f"Passed: {passed}")
print()
output = {
"test_name": "RESET Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": formula_str,
"statistic": float(f_stat),
"p_value": float(p_value),
"passed": bool(passed),
"power": powers,
"type": args.type,
"description": "Ramsey's RESET test for functional form misspecification. Tests whether powers of fitted values, regressors, or principal component significantly improve model fit."
}
os.makedirs(args.output_dir, exist_ok=True)
output_file = os.path.join(args.output_dir, f"{dataset_name}_reset.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()