autocause 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autocause/__init__.py +10 -0
- autocause/cli.py +607 -0
- autocause/config/defaults.json +244 -0
- autocause/core/__init__.py +6 -0
- autocause/core/causal_audit_adapter.py +176 -0
- autocause/core/causal_strength.py +573 -0
- autocause/core/ci_sensitivity.py +177 -0
- autocause/core/decision.py +418 -0
- autocause/core/diagnostics.py +895 -0
- autocause/core/distribution_tests.py +601 -0
- autocause/core/ensemble_scoring.py +277 -0
- autocause/core/evaluation_extras.py +247 -0
- autocause/core/experiment_tracker.py +499 -0
- autocause/core/falsification.py +505 -0
- autocause/core/graph_metrics.py +391 -0
- autocause/core/guards.py +75 -0
- autocause/core/icp_stability.py +738 -0
- autocause/core/io.py +460 -0
- autocause/core/lag_analysis.py +389 -0
- autocause/core/lagbands.py +366 -0
- autocause/core/methods/__init__.py +66 -0
- autocause/core/methods/consensus.py +775 -0
- autocause/core/methods/correlation.py +938 -0
- autocause/core/methods/granger.py +800 -0
- autocause/core/methods/lpcmci.py +190 -0
- autocause/core/methods/predictive_baseline.py +199 -0
- autocause/core/methods/tigramite_pcmci.py +1159 -0
- autocause/core/methods/transfer_entropy.py +518 -0
- autocause/core/methods/varlingam.py +207 -0
- autocause/core/methods/visualize_results.py +239 -0
- autocause/core/multiple_testing.py +66 -0
- autocause/core/output/__init__.py +5 -0
- autocause/core/output/edge_output.py +386 -0
- autocause/core/plot_cache.py +400 -0
- autocause/core/power_analysis.py +222 -0
- autocause/core/preprocessing.py +823 -0
- autocause/core/qc.py +315 -0
- autocause/core/robust_correlation.py +515 -0
- autocause/core/run_workflow.py +2593 -0
- autocause/core/sample_size_adequacy.py +494 -0
- autocause/core/sensitivity.py +609 -0
- autocause/core/stats.py +367 -0
- autocause/core/surrogate_inference.py +28 -0
- autocause/core/tau_max_estimation.py +828 -0
- autocause/core/tiered_consensus.py +423 -0
- autocause/core/validate.py +505 -0
- autocause/core/validation/__init__.py +71 -0
- autocause/core/validation/bootstrap.py +196 -0
- autocause/core/validation/cross_site.py +153 -0
- autocause/core/validation/effects.py +141 -0
- autocause/core/validation/link_classifier.py +107 -0
- autocause/core/validation/robustness.py +285 -0
- autocause/core/validation/stability.py +153 -0
- autocause/core/validation/surrogates.py +192 -0
- autocause/demo_interactive_summary.py +165 -0
- autocause/plots/__init__.py +67 -0
- autocause/plots/causal_analysis.py +914 -0
- autocause/plots/comparison.py +206 -0
- autocause/plots/correlation.py +534 -0
- autocause/plots/correlation_plots.py +650 -0
- autocause/plots/graphs.py +486 -0
- autocause/plots/interactive.py +760 -0
- autocause/plots/lags.py +217 -0
- autocause/plots/maps.py +211 -0
- autocause/plots/pvalues.py +180 -0
- autocause/reporting/__init__.py +40 -0
- autocause/reporting/causal_summary.py +483 -0
- autocause/reporting/summarize.py +350 -0
- autocause-0.2.1.dist-info/METADATA +232 -0
- autocause-0.2.1.dist-info/RECORD +73 -0
- autocause-0.2.1.dist-info/WHEEL +5 -0
- autocause-0.2.1.dist-info/licenses/LICENSE +661 -0
- autocause-0.2.1.dist-info/top_level.txt +1 -0
autocause/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework package for causal discovery in time series.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.1"
|
|
6
|
+
__author__ = "Marco Ruiz, Miguel Arana-Catania, David R. Ardila, Rodrigo Ventura"
|
|
7
|
+
|
|
8
|
+
from . import core, plots, reporting, config
|
|
9
|
+
|
|
10
|
+
__all__ = ["core", "plots", "reporting", "config"]
|
autocause/cli.py
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-Line Interface for Causal Discovery Framework
|
|
3
|
+
|
|
4
|
+
Provides easy-to-use CLI for running causal analysis pipelines,
|
|
5
|
+
generating visualizations, and producing reports.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m framework.cli run --input data.csv --methods granger te pcmci consensus
|
|
9
|
+
python -m framework.cli plot --results results.csv --output plots/
|
|
10
|
+
python -m framework.cli report --experiment_name "Exp1" --results_dir results/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import logging
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import sys
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Mitigate long Numba JIT compile stalls (e.g., from optional tigramite/numba paths)
|
|
25
|
+
# Set before any potential imports of libraries that use numba.
|
|
26
|
+
os.environ.setdefault("NUMBA_DISABLE_JIT", "1")
|
|
27
|
+
os.environ.setdefault("NUMBA_LOG_LEVEL", "WARNING")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def setup_logging(verbose: bool = False) -> None:
|
|
31
|
+
"""Configure logging."""
|
|
32
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
33
|
+
logging.basicConfig(
|
|
34
|
+
level=level,
|
|
35
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_command(args) -> int:
|
|
40
|
+
"""Execute causal discovery run."""
|
|
41
|
+
logger.info(f"Running causal analysis on: {args.input}")
|
|
42
|
+
|
|
43
|
+
# Import here to avoid circular imports
|
|
44
|
+
from autocause.core.io import load_timeseries_csv
|
|
45
|
+
from autocause.core.qc import generate_qc_report
|
|
46
|
+
from autocause.core.methods import (
|
|
47
|
+
granger,
|
|
48
|
+
transfer_entropy,
|
|
49
|
+
tigramite_pcmci,
|
|
50
|
+
consensus,
|
|
51
|
+
)
|
|
52
|
+
from autocause.core.decision import decide_and_run as auto_decide_and_run
|
|
53
|
+
|
|
54
|
+
# Load data
|
|
55
|
+
try:
|
|
56
|
+
df, _meta = load_timeseries_csv(args.input)
|
|
57
|
+
logger.info(f"Loaded data: {df.shape}")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Failed to load data: {e}")
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
# Quality control
|
|
63
|
+
qc_report = generate_qc_report(df)
|
|
64
|
+
logger.info(f"QC Report: {qc_report}")
|
|
65
|
+
|
|
66
|
+
# Get variable pairs
|
|
67
|
+
if args.pairs:
|
|
68
|
+
pairs = json.loads(args.pairs)
|
|
69
|
+
else:
|
|
70
|
+
# All combinations
|
|
71
|
+
vars_list = list(df.columns)
|
|
72
|
+
pairs = [
|
|
73
|
+
(vars_list[i], vars_list[j])
|
|
74
|
+
for i in range(len(vars_list))
|
|
75
|
+
for j in range(len(vars_list))
|
|
76
|
+
if i != j
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
logger.info(f"Testing {len(pairs)} variable pairs")
|
|
80
|
+
|
|
81
|
+
results_dict = {}
|
|
82
|
+
|
|
83
|
+
# Run selected methods
|
|
84
|
+
if "granger" in args.methods:
|
|
85
|
+
logger.info("Running Granger causality...")
|
|
86
|
+
try:
|
|
87
|
+
results_dict["granger"] = granger.batch_granger_causality(
|
|
88
|
+
df, pairs, maxlag=args.maxlag, alpha=args.alpha
|
|
89
|
+
)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"Granger failed: {e}")
|
|
92
|
+
|
|
93
|
+
if "te" in args.methods:
|
|
94
|
+
logger.info("Running Transfer Entropy...")
|
|
95
|
+
try:
|
|
96
|
+
results_dict["transfer_entropy"] = transfer_entropy.batch_transfer_entropy(
|
|
97
|
+
df, pairs, delays=[1, 2, 3], alpha=args.alpha, method="discrete"
|
|
98
|
+
)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Transfer Entropy failed: {e}")
|
|
101
|
+
|
|
102
|
+
if "pcmci" in args.methods:
|
|
103
|
+
logger.info("Running PCMCI+...")
|
|
104
|
+
try:
|
|
105
|
+
results_dict["pcmci"] = tigramite_pcmci.batch_pcmci(
|
|
106
|
+
df, pairs, tau_max=args.maxlag, alpha=args.alpha
|
|
107
|
+
)
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(f"PCMCI+ failed: {e}")
|
|
110
|
+
|
|
111
|
+
if "auto" in args.methods:
|
|
112
|
+
logger.info("Running AUTO decision engine...")
|
|
113
|
+
try:
|
|
114
|
+
auto_df = auto_decide_and_run(
|
|
115
|
+
df, pairs, window_days=args.maxlag * 5, alpha=args.alpha
|
|
116
|
+
)
|
|
117
|
+
results_dict["auto"] = auto_df
|
|
118
|
+
# Also split by method for downstream compatibility
|
|
119
|
+
if (
|
|
120
|
+
auto_df is not None
|
|
121
|
+
and not auto_df.empty
|
|
122
|
+
and "method" in auto_df.columns
|
|
123
|
+
):
|
|
124
|
+
g_df = auto_df[auto_df["method"] == "Granger"]
|
|
125
|
+
te_df = auto_df[auto_df["method"] == "TransferEntropy"]
|
|
126
|
+
pc_df = auto_df[auto_df["method"] == "PCMCI+"]
|
|
127
|
+
if not g_df.empty:
|
|
128
|
+
results_dict["granger"] = g_df
|
|
129
|
+
if not te_df.empty:
|
|
130
|
+
results_dict["transfer_entropy"] = te_df
|
|
131
|
+
if not pc_df.empty:
|
|
132
|
+
results_dict["pcmci"] = pc_df
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"AUTO failed: {e}")
|
|
135
|
+
|
|
136
|
+
# Save results
|
|
137
|
+
output_dir = Path(args.output) if args.output else Path("results")
|
|
138
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
# Consensus if requested
|
|
141
|
+
if "consensus" in args.methods and len(results_dict) > 1:
|
|
142
|
+
logger.info("Computing consensus...")
|
|
143
|
+
try:
|
|
144
|
+
consensus_result = consensus.merge_method_results(
|
|
145
|
+
results_dict.get("granger"),
|
|
146
|
+
results_dict.get("transfer_entropy"),
|
|
147
|
+
results_dict.get("pcmci"),
|
|
148
|
+
min_votes=getattr(args, "min_votes", 2),
|
|
149
|
+
lag_tolerance_steps=getattr(args, "lag_tolerance_steps", 1),
|
|
150
|
+
sampling_days=getattr(args, "sampling_days", 1),
|
|
151
|
+
alpha=args.alpha,
|
|
152
|
+
output_dir=str(output_dir),
|
|
153
|
+
)
|
|
154
|
+
results_dict["consensus"] = consensus_result["consensus_edges"]
|
|
155
|
+
logger.info(
|
|
156
|
+
f"✅ Consensus: {len(consensus_result['consensus_edges'])} edges "
|
|
157
|
+
f"(saved CSV, GraphML, report)"
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Consensus failed: {e}")
|
|
161
|
+
|
|
162
|
+
# Save results
|
|
163
|
+
|
|
164
|
+
for method, results in results_dict.items():
|
|
165
|
+
if results is not None:
|
|
166
|
+
output_file = output_dir / f"{method}.csv"
|
|
167
|
+
results.to_csv(output_file, index=False)
|
|
168
|
+
logger.info(f"Saved: {output_file}")
|
|
169
|
+
|
|
170
|
+
logger.info(f"✅ Analysis complete. Results saved to: {output_dir}")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def plot_command(args) -> int:
|
|
175
|
+
"""Generate visualizations."""
|
|
176
|
+
logger.info(f"Generating plots from: {args.results}")
|
|
177
|
+
|
|
178
|
+
from autocause.plots import (
|
|
179
|
+
plot_pvalue_distribution,
|
|
180
|
+
plot_lag_histogram,
|
|
181
|
+
plot_causal_graph,
|
|
182
|
+
plot_method_comparison,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
output_dir = Path(args.output) if args.output else Path("figures")
|
|
186
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
|
|
188
|
+
# Load results
|
|
189
|
+
methods = ["granger", "transfer_entropy", "pcmci", "consensus"]
|
|
190
|
+
results_dict = {}
|
|
191
|
+
|
|
192
|
+
for method in methods:
|
|
193
|
+
result_file = (
|
|
194
|
+
Path(args.results) / f"{method}.csv"
|
|
195
|
+
if Path(args.results).is_dir()
|
|
196
|
+
else Path(args.results)
|
|
197
|
+
)
|
|
198
|
+
if result_file.exists():
|
|
199
|
+
results_dict[method] = pd.read_csv(result_file)
|
|
200
|
+
logger.info(f"Loaded: {result_file}")
|
|
201
|
+
|
|
202
|
+
if not results_dict:
|
|
203
|
+
logger.warning("No result files found")
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
# Generate plots
|
|
207
|
+
logger.info("Generating p-value distributions...")
|
|
208
|
+
for method, df in results_dict.items():
|
|
209
|
+
plot_pvalue_distribution(
|
|
210
|
+
df, method=method.upper(), output_path=output_dir / f"pvalues_{method}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
logger.info("Generating lag histograms...")
|
|
214
|
+
for method, df in results_dict.items():
|
|
215
|
+
lag_col = (
|
|
216
|
+
"delay" if "delay" in df.columns else "lag" if "lag" in df.columns else None
|
|
217
|
+
)
|
|
218
|
+
if lag_col:
|
|
219
|
+
plot_lag_histogram(
|
|
220
|
+
df,
|
|
221
|
+
method=method.upper(),
|
|
222
|
+
lag_column=lag_col,
|
|
223
|
+
output_path=output_dir / f"lags_{method}",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
logger.info("Generating causal graphs...")
|
|
227
|
+
for method, df in results_dict.items():
|
|
228
|
+
if "source" in df.columns and "target" in df.columns:
|
|
229
|
+
plot_causal_graph(
|
|
230
|
+
df, method=method.upper(), output_path=output_dir / f"graph_{method}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
logger.info("Generating method comparison...")
|
|
234
|
+
plot_method_comparison(results_dict, output_path=output_dir / "method_comparison")
|
|
235
|
+
|
|
236
|
+
logger.info(f"✅ Plots saved to: {output_dir}")
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def diagnose_command(args) -> int:
|
|
241
|
+
"""Run diagnostic checks on data or results."""
|
|
242
|
+
logger.info(f"Running diagnostics: {args.experiment_name}")
|
|
243
|
+
|
|
244
|
+
from autocause.core.io import load_csv
|
|
245
|
+
from autocause.core.diagnostics import (
|
|
246
|
+
Diagnostics,
|
|
247
|
+
permutation_negative_control,
|
|
248
|
+
reverse_direction_check,
|
|
249
|
+
method_divergence_alert,
|
|
250
|
+
)
|
|
251
|
+
from autocause.plots import (
|
|
252
|
+
plot_pvalue_distribution,
|
|
253
|
+
plot_lag_histogram,
|
|
254
|
+
plot_method_comparison,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
output_dir = Path(args.output) if args.output else Path("diagnostics")
|
|
258
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
|
|
260
|
+
# === Load results ===
|
|
261
|
+
results_dict = {}
|
|
262
|
+
if args.results:
|
|
263
|
+
results_path = Path(args.results)
|
|
264
|
+
if results_path.is_dir():
|
|
265
|
+
for csv_file in results_path.glob("*.csv"):
|
|
266
|
+
method = csv_file.stem
|
|
267
|
+
results_dict[method] = pd.read_csv(csv_file)
|
|
268
|
+
logger.info(f"Loaded: {csv_file}")
|
|
269
|
+
elif results_path.is_file():
|
|
270
|
+
results_dict["method"] = pd.read_csv(results_path)
|
|
271
|
+
logger.info(f"Loaded: {results_path}")
|
|
272
|
+
|
|
273
|
+
if not results_dict:
|
|
274
|
+
logger.warning("No result files found")
|
|
275
|
+
return 1
|
|
276
|
+
|
|
277
|
+
# === Run standard diagnostics ===
|
|
278
|
+
logger.info("\n" + "=" * 70)
|
|
279
|
+
logger.info("RUNNING STANDARD DIAGNOSTICS")
|
|
280
|
+
logger.info("=" * 70)
|
|
281
|
+
|
|
282
|
+
all_edges = pd.concat(results_dict.values(), ignore_index=True)
|
|
283
|
+
diag = Diagnostics(all_edges, cadence_days=args.cadence_days)
|
|
284
|
+
diag.run_all()
|
|
285
|
+
|
|
286
|
+
# Save standard diagnostics
|
|
287
|
+
diag.save_results(output_dir)
|
|
288
|
+
|
|
289
|
+
# === Generate plots ===
|
|
290
|
+
if args.plots:
|
|
291
|
+
logger.info("\n" + "=" * 70)
|
|
292
|
+
logger.info("GENERATING DIAGNOSTIC PLOTS")
|
|
293
|
+
logger.info("=" * 70)
|
|
294
|
+
|
|
295
|
+
figures_dir = output_dir / "figures"
|
|
296
|
+
figures_dir.mkdir(exist_ok=True)
|
|
297
|
+
|
|
298
|
+
# P-value distributions per method
|
|
299
|
+
for method, df in results_dict.items():
|
|
300
|
+
if "p_value" in df.columns:
|
|
301
|
+
try:
|
|
302
|
+
plot_pvalue_distribution(
|
|
303
|
+
df,
|
|
304
|
+
method=method.upper(),
|
|
305
|
+
output_path=figures_dir / f"pvalues_{method}",
|
|
306
|
+
)
|
|
307
|
+
logger.info(f" ✓ P-value distribution: {method}")
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.warning(f" ✗ P-value plot failed for {method}: {e}")
|
|
310
|
+
|
|
311
|
+
# Lag distributions per method
|
|
312
|
+
for method, df in results_dict.items():
|
|
313
|
+
if "lag_days" in df.columns or "lag_steps" in df.columns:
|
|
314
|
+
try:
|
|
315
|
+
lag_col = "lag_days" if "lag_days" in df.columns else "lag_steps"
|
|
316
|
+
plot_lag_histogram(
|
|
317
|
+
df,
|
|
318
|
+
method=method.upper(),
|
|
319
|
+
lag_column=lag_col,
|
|
320
|
+
output_path=figures_dir / f"lags_{method}",
|
|
321
|
+
)
|
|
322
|
+
logger.info(f" ✓ Lag histogram: {method}")
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.warning(f" ✗ Lag plot failed for {method}: {e}")
|
|
325
|
+
|
|
326
|
+
# Method comparison
|
|
327
|
+
try:
|
|
328
|
+
plot_method_comparison(
|
|
329
|
+
results_dict, output_path=figures_dir / "method_comparison"
|
|
330
|
+
)
|
|
331
|
+
logger.info(" ✓ Method comparison")
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logger.warning(f" ✗ Method comparison failed: {e}")
|
|
334
|
+
|
|
335
|
+
# === Run advanced diagnostics ===
|
|
336
|
+
if args.advanced:
|
|
337
|
+
logger.info("\n" + "=" * 70)
|
|
338
|
+
logger.info("RUNNING ADVANCED DIAGNOSTICS")
|
|
339
|
+
logger.info("=" * 70)
|
|
340
|
+
|
|
341
|
+
advanced_results = {}
|
|
342
|
+
|
|
343
|
+
# Permutation negative control
|
|
344
|
+
if args.permutation and args.input:
|
|
345
|
+
logger.info("\n>>> Permutation Negative Control")
|
|
346
|
+
try:
|
|
347
|
+
df = load_csv(args.input)
|
|
348
|
+
# Get pairs from results
|
|
349
|
+
pairs = []
|
|
350
|
+
for _, row in all_edges.iterrows():
|
|
351
|
+
if "source" in row and "target" in row:
|
|
352
|
+
pairs.append((row["source"], row["target"]))
|
|
353
|
+
pairs = list(set(pairs)) # unique pairs
|
|
354
|
+
|
|
355
|
+
for method in results_dict.keys():
|
|
356
|
+
perm_result = permutation_negative_control(
|
|
357
|
+
df=df,
|
|
358
|
+
pairs=pairs,
|
|
359
|
+
method_name=method,
|
|
360
|
+
n_permutations=args.n_permutations,
|
|
361
|
+
alpha=args.alpha,
|
|
362
|
+
tolerance=args.tolerance,
|
|
363
|
+
maxlag=args.maxlag,
|
|
364
|
+
)
|
|
365
|
+
advanced_results[f"permutation_{method}"] = perm_result
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Permutation control failed: {e}")
|
|
369
|
+
|
|
370
|
+
# Reverse-direction check
|
|
371
|
+
if args.reverse and args.input:
|
|
372
|
+
logger.info("\n>>> Reverse-Direction Check")
|
|
373
|
+
try:
|
|
374
|
+
df = load_csv(args.input)
|
|
375
|
+
|
|
376
|
+
for method, edges_df in results_dict.items():
|
|
377
|
+
# Filter to significant edges only
|
|
378
|
+
if "is_significant" in edges_df.columns:
|
|
379
|
+
sig_edges = edges_df[edges_df["is_significant"] == 1]
|
|
380
|
+
elif "q_value" in edges_df.columns:
|
|
381
|
+
sig_edges = edges_df[edges_df["q_value"] < args.alpha]
|
|
382
|
+
elif "p_value" in edges_df.columns:
|
|
383
|
+
sig_edges = edges_df[edges_df["p_value"] < args.alpha]
|
|
384
|
+
else:
|
|
385
|
+
sig_edges = edges_df
|
|
386
|
+
|
|
387
|
+
if not sig_edges.empty:
|
|
388
|
+
reverse_result = reverse_direction_check(
|
|
389
|
+
df=df,
|
|
390
|
+
detected_edges=sig_edges,
|
|
391
|
+
method_name=method,
|
|
392
|
+
alpha=args.alpha,
|
|
393
|
+
tolerance=args.tolerance,
|
|
394
|
+
maxlag=args.maxlag,
|
|
395
|
+
)
|
|
396
|
+
advanced_results[f"reverse_{method}"] = reverse_result
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
logger.error(f"Reverse-direction check failed: {e}")
|
|
400
|
+
|
|
401
|
+
# Method divergence alert
|
|
402
|
+
if args.divergence:
|
|
403
|
+
logger.info("\n>>> Method Divergence Alert")
|
|
404
|
+
try:
|
|
405
|
+
divergence_result = method_divergence_alert(
|
|
406
|
+
results_dict=results_dict,
|
|
407
|
+
alpha=args.alpha,
|
|
408
|
+
divergence_threshold=args.divergence_threshold,
|
|
409
|
+
)
|
|
410
|
+
advanced_results["method_divergence"] = divergence_result
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"Method divergence check failed: {e}")
|
|
413
|
+
|
|
414
|
+
# Save advanced diagnostics
|
|
415
|
+
if advanced_results:
|
|
416
|
+
import json
|
|
417
|
+
|
|
418
|
+
advanced_path = output_dir / "advanced_diagnostics.json"
|
|
419
|
+
with open(advanced_path, "w", encoding="utf-8") as f:
|
|
420
|
+
json.dump(advanced_results, f, indent=2, default=str)
|
|
421
|
+
logger.info(f"\n✅ Advanced diagnostics saved to: {advanced_path}")
|
|
422
|
+
|
|
423
|
+
logger.info(f"\n✅ Diagnostics complete. Results saved to: {output_dir}")
|
|
424
|
+
return 0
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def report_command(args) -> int:
|
|
428
|
+
"""Generate analysis reports."""
|
|
429
|
+
logger.info(f"Generating report for: {args.experiment_name}")
|
|
430
|
+
|
|
431
|
+
from autocause.reporting import (
|
|
432
|
+
generate_summary_report,
|
|
433
|
+
compare_with_baseline,
|
|
434
|
+
generate_latex_table,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
output_dir = Path(args.output) if args.output else Path("reports")
|
|
438
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
439
|
+
|
|
440
|
+
# Load results
|
|
441
|
+
results_dict = {}
|
|
442
|
+
if args.results:
|
|
443
|
+
results_path = Path(args.results)
|
|
444
|
+
for csv_file in results_path.glob("*.csv"):
|
|
445
|
+
method = csv_file.stem
|
|
446
|
+
results_dict[method] = pd.read_csv(csv_file)
|
|
447
|
+
logger.info(f"Loaded: {csv_file}")
|
|
448
|
+
|
|
449
|
+
if not results_dict:
|
|
450
|
+
logger.warning("No result files found")
|
|
451
|
+
return 1
|
|
452
|
+
|
|
453
|
+
# Generate report
|
|
454
|
+
logger.info("Generating summary report...")
|
|
455
|
+
generate_summary_report(
|
|
456
|
+
results_dict,
|
|
457
|
+
experiment_name=args.experiment_name,
|
|
458
|
+
output_path=output_dir / "summary.txt",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Generate LaTeX table
|
|
462
|
+
logger.info("Generating LaTeX table...")
|
|
463
|
+
comparison_df = compare_with_baseline(results_dict, args.experiment_name)
|
|
464
|
+
generate_latex_table(comparison_df, output_path=output_dir / "results_table.tex")
|
|
465
|
+
|
|
466
|
+
logger.info(f"✅ Reports saved to: {output_dir}")
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def main() -> int:
|
|
471
|
+
"""Main CLI entry point."""
|
|
472
|
+
parser = argparse.ArgumentParser(
|
|
473
|
+
description="Causal Discovery Framework CLI",
|
|
474
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
475
|
+
epilog="""
|
|
476
|
+
Examples:
|
|
477
|
+
# Run analysis
|
|
478
|
+
python -m framework.cli run --input data.csv --methods granger te pcmci consensus
|
|
479
|
+
|
|
480
|
+
# Generate plots
|
|
481
|
+
python -m framework.cli plot --results results/ --output figures/
|
|
482
|
+
|
|
483
|
+
# Generate report
|
|
484
|
+
python -m framework.cli report --experiment_name "Exp1" --results results/ --output reports/
|
|
485
|
+
""",
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Global arguments
|
|
489
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose logging")
|
|
490
|
+
|
|
491
|
+
# Subcommands
|
|
492
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
493
|
+
|
|
494
|
+
# RUN command
|
|
495
|
+
run_parser = subparsers.add_parser("run", help="Run causal analysis")
|
|
496
|
+
run_parser.add_argument("--input", required=True, help="Input CSV file")
|
|
497
|
+
run_parser.add_argument(
|
|
498
|
+
"--methods",
|
|
499
|
+
nargs="+",
|
|
500
|
+
default=["granger"],
|
|
501
|
+
choices=["granger", "te", "pcmci", "consensus", "auto"],
|
|
502
|
+
help="Methods to run",
|
|
503
|
+
)
|
|
504
|
+
run_parser.add_argument("--maxlag", type=int, default=12, help="Maximum lag")
|
|
505
|
+
run_parser.add_argument(
|
|
506
|
+
"--alpha", type=float, default=0.05, help="Significance level"
|
|
507
|
+
)
|
|
508
|
+
run_parser.add_argument("--pairs", help="JSON string of variable pairs")
|
|
509
|
+
run_parser.add_argument("--output", default="results", help="Output directory")
|
|
510
|
+
run_parser.set_defaults(func=run_command)
|
|
511
|
+
|
|
512
|
+
# PLOT command
|
|
513
|
+
plot_parser = subparsers.add_parser("plot", help="Generate visualizations")
|
|
514
|
+
plot_parser.add_argument(
|
|
515
|
+
"--results", required=True, help="Results directory or CSV file"
|
|
516
|
+
)
|
|
517
|
+
plot_parser.add_argument("--output", default="figures", help="Output directory")
|
|
518
|
+
plot_parser.set_defaults(func=plot_command)
|
|
519
|
+
|
|
520
|
+
# REPORT command
|
|
521
|
+
report_parser = subparsers.add_parser("report", help="Generate reports")
|
|
522
|
+
report_parser.add_argument(
|
|
523
|
+
"--experiment_name", required=True, help="Experiment name"
|
|
524
|
+
)
|
|
525
|
+
report_parser.add_argument("--results", help="Results directory")
|
|
526
|
+
report_parser.add_argument("--output", default="reports", help="Output directory")
|
|
527
|
+
report_parser.set_defaults(func=report_command)
|
|
528
|
+
|
|
529
|
+
# DIAGNOSE command
|
|
530
|
+
diagnose_parser = subparsers.add_parser(
|
|
531
|
+
"diagnose", help="Run diagnostic checks on results"
|
|
532
|
+
)
|
|
533
|
+
diagnose_parser.add_argument(
|
|
534
|
+
"--experiment_name", required=True, help="Experiment name"
|
|
535
|
+
)
|
|
536
|
+
diagnose_parser.add_argument(
|
|
537
|
+
"--results", required=True, help="Results directory or CSV file"
|
|
538
|
+
)
|
|
539
|
+
diagnose_parser.add_argument(
|
|
540
|
+
"--input", help="Input data CSV (required for permutation/reverse checks)"
|
|
541
|
+
)
|
|
542
|
+
diagnose_parser.add_argument(
|
|
543
|
+
"--output", default="diagnostics", help="Output directory"
|
|
544
|
+
)
|
|
545
|
+
diagnose_parser.add_argument(
|
|
546
|
+
"--cadence_days", type=int, default=5, help="Days per timestep"
|
|
547
|
+
)
|
|
548
|
+
diagnose_parser.add_argument(
|
|
549
|
+
"--alpha", type=float, default=0.05, help="Significance level"
|
|
550
|
+
)
|
|
551
|
+
diagnose_parser.add_argument(
|
|
552
|
+
"--plots", action="store_true", help="Generate diagnostic plots"
|
|
553
|
+
)
|
|
554
|
+
diagnose_parser.add_argument(
|
|
555
|
+
"--advanced", action="store_true", help="Run advanced diagnostics"
|
|
556
|
+
)
|
|
557
|
+
diagnose_parser.add_argument(
|
|
558
|
+
"--permutation",
|
|
559
|
+
action="store_true",
|
|
560
|
+
help="Run permutation negative control (requires --input)",
|
|
561
|
+
)
|
|
562
|
+
diagnose_parser.add_argument(
|
|
563
|
+
"--reverse",
|
|
564
|
+
action="store_true",
|
|
565
|
+
help="Run reverse-direction check (requires --input)",
|
|
566
|
+
)
|
|
567
|
+
diagnose_parser.add_argument(
|
|
568
|
+
"--divergence",
|
|
569
|
+
action="store_true",
|
|
570
|
+
help="Check for method divergence",
|
|
571
|
+
)
|
|
572
|
+
diagnose_parser.add_argument(
|
|
573
|
+
"--n_permutations", type=int, default=100, help="Number of permutations"
|
|
574
|
+
)
|
|
575
|
+
diagnose_parser.add_argument(
|
|
576
|
+
"--tolerance", type=float, default=0.03, help="Tolerance for FDR deviation"
|
|
577
|
+
)
|
|
578
|
+
diagnose_parser.add_argument(
|
|
579
|
+
"--divergence_threshold",
|
|
580
|
+
type=float,
|
|
581
|
+
default=0.20,
|
|
582
|
+
help="Relative divergence threshold",
|
|
583
|
+
)
|
|
584
|
+
diagnose_parser.add_argument(
|
|
585
|
+
"--maxlag", type=int, default=12, help="Maximum lag for methods"
|
|
586
|
+
)
|
|
587
|
+
diagnose_parser.set_defaults(func=diagnose_command)
|
|
588
|
+
|
|
589
|
+
# Parse arguments
|
|
590
|
+
args = parser.parse_args()
|
|
591
|
+
|
|
592
|
+
setup_logging(args.verbose)
|
|
593
|
+
|
|
594
|
+
if not args.command:
|
|
595
|
+
parser.print_help()
|
|
596
|
+
return 0
|
|
597
|
+
|
|
598
|
+
# Execute command
|
|
599
|
+
try:
|
|
600
|
+
return args.func(args)
|
|
601
|
+
except Exception as e:
|
|
602
|
+
logger.error(f"Error: {e}", exc_info=args.verbose)
|
|
603
|
+
return 1
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
if __name__ == "__main__":
|
|
607
|
+
sys.exit(main())
|