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_harvey_collier
def validate_for_regression(data, dataset_name):
issues = []
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}")
if len(data.columns) > 2: X = data.select_dtypes(include=[np.number])
if X.shape[1] > 0: corr_matrix = X.corr()
max_corr = corr_matrix.abs().values
np.fill_diagonal(max_corr, np.nan) max_abs_corr = np.nanmax(max_corr)
if max_abs_corr > 0.95:
issues.append(f"High multicollinearity detected (max correlation: {max_abs_corr:.3f})")
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:
data[col], uniques = pd.factorize(data[col])
print(f" {col}: {len(uniques)} unique values -> integer level encoding")
return non_numeric_cols
def main():
parser = argparse.ArgumentParser(
description="Harvey-Collier Test - Check functional form 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"
)
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)
issues = validate_for_regression(data, dataset_name)
non_numeric_cols = data.select_dtypes(exclude=[np.number]).columns.tolist()
if non_numeric_cols:
print(f"INFO: Dataset '{dataset_name}' contains non-numeric columns: {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}")
if any("multicollinearity" in issue.lower() for issue in issues):
multicollinearity_issues = [i for i in issues if "multicollinearity" in i.lower()]
print(f"WARNING: {multicollinearity_issues[0]}")
print("Harvey-Collier test is sensitive to multicollinearity. The test will attempt to run but may return NaN values.")
if issues:
remaining_issues = [i for i in issues if not any(keyword in i.lower() for keyword in ["non-numeric", "multicollinearity"])]
if remaining_issues:
raise ValueError(f"Data validation failed: {remaining_issues}")
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)}"
try:
model = sm.OLS(y, X).fit()
except Exception as e:
output = {
"test_name": "Harvey-Collier Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": None,
"statistic": None,
"p_value": None,
"passed": None,
"skipped": True,
"reason": f"Failed to fit regression model: {e}",
"description": "Tests for functional form misspecification by examining whether recursive residuals exhibit a linear trend. Uses statsmodels.stats.diagnostic.linear_harvey_collier."
}
write_output(output, args.output_dir, dataset_name, "harvey_collier")
return
try:
hc_result = linear_harvey_collier(model)
except Exception as e:
if "singular" in str(e).lower() or "multicollinearity" in str(e).lower() or "skip" in str(e).lower():
output = {
"test_name": "Harvey-Collier Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": formula_str,
"statistic": None,
"p_value": None,
"passed": None,
"skipped": True,
"reason": "High multicollinearity detected - Harvey-Collier test requires full-rank design matrix. Consider using VIF to diagnose multicollinearity first.",
"description": "Tests for functional form misspecification by examining whether recursive residuals exhibit a linear trend. Uses statsmodels.stats.diagnostic.linear_harvey_collier."
}
write_output(output, args.output_dir, dataset_name, "harvey_collier")
return
else:
raise
print("Harvey-Collier Test (Python - statsmodels)")
print("=" * 45)
print(f"Dataset: {dataset_name}")
print(f"Formula: {formula_str}")
print(f"Statistic (t): {hc_result[0]}")
print(f"p-value: {hc_result[1]}")
print(f"Passed: {hc_result[1] > 0.05}")
print()
statistic = hc_result[0]
p_value = hc_result[1]
if np.isnan(statistic) or np.isnan(p_value):
output = {
"test_name": "Harvey-Collier Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": formula_str,
"statistic": None,
"p_value": None,
"passed": None,
"skipped": True,
"reason": "Test returned NaN values - likely due to multicollinearity in the data",
"description": "Tests for functional form misspecification by examining whether recursive residuals exhibit a linear trend. Uses statsmodels.stats.diagnostic.linear_harvey_collier."
}
else:
output = {
"test_name": "Harvey-Collier Test (Python - statsmodels)",
"dataset": dataset_name,
"formula": formula_str,
"statistic": float(statistic),
"p_value": float(p_value),
"passed": bool(p_value > 0.05),
"skipped": False,
"description": "Tests for functional form misspecification by examining whether recursive residuals exhibit a linear trend. Uses statsmodels.stats.diagnostic.linear_harvey_collier."
}
write_output(output, args.output_dir, dataset_name, "harvey_collier")
def write_output(output, output_dir, dataset_name, test_name):
os.makedirs(output_dir, exist_ok=True)
output_file = os.path.join(output_dir, f"{dataset_name}_{test_name}.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()