Skip to content

Feat/bootstrapping - #5

Merged
Kirscher merged 6 commits into
mainfrom
feat/bootstrapping
Feb 2, 2026
Merged

Kirscher merged 6 commits into
mainfrom
feat/bootstrapping

Conversation

@Kirscher

@Kirscher Kirscher commented Feb 1, 2026

Copy link
Copy Markdown
Owner

No description provided.

- Add bootstrap.py with state-of-the-art bootstrap resampling
- Implement BCa (Bias-Corrected and Accelerated) confidence intervals
- Support multiple bootstrap methods: bca, percentile, basic
- Add CLI for analyzing metric CSV files
- Support mean, median, std, min, max, iqr statistics
- Add comprehensive test suite (19 tests)
- Update requirements with scipy, pandas, tabulate dependencies
- Export bootstrap functions from package __init__.py

Usage:
  python -m ensemble_metrics.bootstrap --metrics-dir ./metrics_all
  python -m ensemble_metrics.bootstrap --metric-file ./ace.csv --ci-level 0.99
- Add compare_methods() for statistical comparison of two ensembling strategies
- Implement paired bootstrap test for mean differences
- Compute Cohen's d effect size with interpretation
- Add p-values and significance indicators
- Create compare.py CLI for easy command-line comparisons
- Support markdown, LaTeX, and plain text output formats
- Export comparison functions from package

Usage:
  python -m src.ensemble_metrics.compare \
      --method-a ./baseline/metrics \
      --method-b ./proposed/metrics \
      --name-a 'Baseline' --name-b 'Proposed'

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83a6757614

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +228 to +231
# Filter out summary rows (mean, std, etc.)
summary_keywords = ["mean", "std", "median", "min", "max", "sum", "count"]
mask = ~df["case_id"].str.lower().isin(summary_keywords)
df_cases = df[mask].copy()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Coerce case_id to string before .str.lower filtering

The summary-row filter assumes case_id is a string and calls .str.lower(). If a metrics CSV uses numeric case IDs (common in some datasets), pd.read_csv will infer an integer dtype and this line raises “Can only use .str accessor with string values,” which aborts all bootstrap workflows for such files. Converting case_id to string before .str (e.g., astype(str)) avoids the crash while still filtering summary rows.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds bootstrap-based confidence intervals and paired method comparison utilities (with CLIs) to the ensemble_metrics package.

Changes:

  • Introduces a bootstrap resampling module supporting percentile/basic/BCa CIs plus metric CSV aggregation and table formatting.
  • Adds a CLI for paired bootstrap comparisons between two metric directories and renders summary tables.
  • Adds pytest coverage for the bootstrap workflow and updates public exports + dependencies.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/ensemble_metrics/bootstrap.py Implements bootstrap CIs, directory/file processing, and paired method comparison utilities (plus a CLI entrypoint).
src/ensemble_metrics/compare.py Adds a dedicated CLI for comparing two methods’ metric outputs.
src/ensemble_metrics/__init__.py Re-exports bootstrap and comparison APIs from the top-level package.
tests/test_bootstrap.py Adds tests for bootstrap statistics, CSV loading, directory processing, formatting, and a basic integration workflow.
requirements.txt Adds dependencies needed for the new functionality (SciPy/Pandas/Tabulate).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ensemble_metrics/bootstrap.py Outdated
(difference, ci_lower, ci_upper, p_value)
"""
if random_state is not None:
np.random.seed(random_state)

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

paired_bootstrap_test also sets the global NumPy RNG with np.random.seed(random_state), which has the same global-state/concurrency issues as in bootstrap_statistic. Use a local generator scoped to the function to keep randomness isolated and reproducible.

Suggested change
np.random.seed(random_state)
rng = np.random.default_rng(random_state)
else:
rng = np.random.default_rng()

Copilot uses AI. Check for mistakes.
Comment thread src/ensemble_metrics/compare.py Outdated

Statistical Tests Performed:
- Paired bootstrap test for mean difference
- 95% BCa confidence interval for the difference

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI help text states that it computes a “95% BCa confidence interval for the difference”, but paired_bootstrap_test currently uses a simple percentile interval (no BCa adjustment). Either update the help text to reflect the implemented method or implement a BCa interval for paired differences so the CLI documentation matches behavior.

Suggested change
- 95% BCa confidence interval for the difference
- 95% percentile bootstrap confidence interval for the difference

Copilot uses AI. Check for mistakes.
Comment thread src/ensemble_metrics/bootstrap.py Outdated
numerator = z0 + z_alpha
denominator = 1 - a * (z0 + z_alpha)
if np.abs(denominator) < 1e-10:
return z_alpha # Fall back to unadjusted

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _bca_interval.adjusted_percentile, the fallback path returns z_alpha (a z-score), but the caller treats the return value as a probability/percentile in [0, 1]. This causes p_lower/p_upper to be clipped to 0.001/0.999 and can yield incorrect or overly extreme BCa intervals when denominator is near zero. Return the unadjusted percentile (i.e., a probability) instead of the z-score in the fallback branch.

Suggested change
return z_alpha # Fall back to unadjusted
# Fall back to the unadjusted percentile (probability), not the z-score
return stats.norm.cdf(z_alpha)

Copilot uses AI. Check for mistakes.

# Filter out summary rows (mean, std, etc.)
summary_keywords = ["mean", "std", "median", "min", "max", "sum", "count"]
mask = ~df["case_id"].str.lower().isin(summary_keywords)

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_metric_csv assumes case_id is a string column (df["case_id"].str.lower()), which will raise if the column contains non-strings/NaNs. Consider normalizing first (e.g., astype(str) and/or fillna("")) before calling .str.lower() so CSVs with numeric or missing case IDs fail gracefully or still load.

Suggested change
mask = ~df["case_id"].str.lower().isin(summary_keywords)
case_id_normalized = df["case_id"].astype(str).str.lower()
mask = ~case_id_normalized.isin(summary_keywords)

Copilot uses AI. Check for mistakes.
Comment thread src/ensemble_metrics/compare.py Outdated
Comment on lines +8 to +13
import pandas as pd

from .bootstrap import (
compare_methods,
format_comparison_table,
)

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pandas is imported but not used in this module. Removing the unused import will avoid lint warnings and slightly reduce import overhead.

Copilot uses AI. Check for mistakes.
Comment thread tests/test_bootstrap.py Outdated
Comment on lines +4 to +7
import os
import tempfile
from pathlib import Path

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports (os, tempfile, Path) are unused in this test module. Removing them will avoid lint warnings and keep the test focused on the relevant dependencies.

Suggested change
import os
import tempfile
from pathlib import Path

Copilot uses AI. Check for mistakes.
Comment on lines +560 to +569
def compare_methods(
metrics_dir_a: str,
metrics_dir_b: str,
method_a_name: str = "Method A",
method_b_name: str = "Method B",
n_bootstrap: int = 10000,
ci_level: float = 0.95,
random_state: Optional[int] = None,
metric_pattern: str = "*.csv"
) -> pd.DataFrame:

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comparison workflow (paired_bootstrap_test / compare_methods / format_comparison_table) is not covered by tests (no references found under tests/). Adding unit/integration tests for at least one happy-path comparison (matching case_ids, known direction of effect, and expected columns in the output DataFrame) would help prevent regressions in the statistical logic and output schema.

Copilot uses AI. Check for mistakes.
Comment thread src/ensemble_metrics/bootstrap.py Outdated
Comment on lines +69 to +71
if random_state is not None:
np.random.seed(random_state)

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bootstrap_statistic seeds the global NumPy RNG via np.random.seed(random_state), which mutates global state and can make results from other code non-reproducible (and is problematic under concurrency). Prefer using a local RNG (e.g., np.random.default_rng(random_state)) and draw samples from that generator instead of the global np.random module.

Copilot uses AI. Check for mistakes.
import argparse
import os
import glob
from typing import Dict, List, Optional, Tuple, Union

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'Dict' is not used.
Import of 'Union' is not used.

Suggested change
from typing import Dict, List, Optional, Tuple, Union
from typing import List, Optional, Tuple

Copilot uses AI. Check for mistakes.
- Use local RNG (np.random.default_rng) instead of global np.random.seed
  for thread safety and isolated reproducibility
- Fix BCa fallback to return probability instead of z-score in
  _bca_interval.adjusted_percentile
- Handle non-string case_id values in load_metric_csv by converting
  to string before filtering
- Update compare.py CLI help text to correctly state 'percentile'
  confidence interval (not BCa)
- Remove unused imports: pandas from compare.py, os/tempfile/Path
  from test_bootstrap.py

This comment was marked as outdated.

- Update .gitignore to prevent re-committing

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 6 changed files in this pull request and generated 14 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ensemble_metrics/bootstrap.py
Comment on lines +562 to +571
def compare_methods(
metrics_dir_a: str,
metrics_dir_b: str,
method_a_name: str = "Method A",
method_b_name: str = "Method B",
n_bootstrap: int = 10000,
ci_level: float = 0.95,
random_state: Optional[int] = None,
metric_pattern: str = "*.csv"
) -> pd.DataFrame:

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New comparison functionality (paired_bootstrap_test, cohens_d, compare_methods, format_comparison_table) is not covered by tests in tests/test_bootstrap.py. Adding unit tests for at least: (1) deterministic behavior under fixed seed, (2) case alignment by case_id, and (3) expected p-value/CI behavior on a simple synthetic dataset would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +35
python -m ensemble_metrics.compare \\
--method-a ./results_baseline/metrics_all \\
--method-b ./results_new/metrics_all \\
--name-a "Baseline" --name-b "New Method"

# With specific confidence level and output
python -m ensemble_metrics.compare \\
--method-a ./method1/metrics \\
--method-b ./method2/metrics \\
--ci-level 0.99 \\
--output comparison_results.csv

# LaTeX output for papers
python -m ensemble_metrics.compare \\

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The epilog examples use python -m ensemble_metrics.compare, but this repo’s other CLI usage (e.g., src/ensemble_metrics/README.md) uses python -m src.ensemble_metrics.... Please align the module path in the help text with the actual package/module layout (or add packaging/entrypoints so ensemble_metrics is importable directly).

Suggested change
python -m ensemble_metrics.compare \\
--method-a ./results_baseline/metrics_all \\
--method-b ./results_new/metrics_all \\
--name-a "Baseline" --name-b "New Method"
# With specific confidence level and output
python -m ensemble_metrics.compare \\
--method-a ./method1/metrics \\
--method-b ./method2/metrics \\
--ci-level 0.99 \\
--output comparison_results.csv
# LaTeX output for papers
python -m ensemble_metrics.compare \\
python -m src.ensemble_metrics.compare \\
--method-a ./results_baseline/metrics_all \\
--method-b ./results_new/metrics_all \\
--name-a "Baseline" --name-b "New Method"
# With specific confidence level and output
python -m src.ensemble_metrics.compare \\
--method-a ./method1/metrics \\
--method-b ./method2/metrics \\
--ci-level 0.99 \\
--output comparison_results.csv
# LaTeX output for papers
python -m src.ensemble_metrics.compare \\

Copilot uses AI. Check for mistakes.
Comment on lines +427 to +432
# Pivot for better readability
pivot_df = df_formatted.pivot(
index="metric",
columns="statistic",
values="value_with_ci" if "value_with_ci" in df_formatted.columns else "point_estimate"
)

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format_results_table uses DataFrame.pivot(...), which raises if there are duplicate (metric, statistic) pairs. Since statistics parsing doesn’t deduplicate and callers can pass duplicates, this can fail at runtime. Consider validating uniqueness up-front (and erroring clearly) or using pivot_table(..., aggfunc='first') if duplicates should be tolerated.

Copilot uses AI. Check for mistakes.
if len(data_a) != len(data_b):
raise ValueError(f"Arrays must have same length: {len(data_a)} vs {len(data_b)}")

n = len(data_a)

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

paired_bootstrap_test doesn’t guard against n < 2, n_bootstrap <= 0, or invalid ci_level. With n == 0 the resampling call will fail, and with small/zero n_bootstrap you can get divide-by-zero or invalid percentiles. Add input validation similar to bootstrap_statistic (and include the observed sizes in the error message).

Suggested change
n = len(data_a)
n = len(data_a)
if n < 2:
raise ValueError(f"paired_bootstrap_test requires at least 2 paired samples; got n={n}")
if n_bootstrap <= 0:
raise ValueError(
f"paired_bootstrap_test requires n_bootstrap > 0; got n_bootstrap={n_bootstrap} "
f"for n={n}"
)
if not (0 < ci_level < 1):
raise ValueError(
f"ci_level must be in (0, 1); got ci_level={ci_level} for n={n}, "
f"n_bootstrap={n_bootstrap}"
)

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +172
jackknife_stats = np.zeros(n)
for i in range(n):
jackknife_sample = np.delete(data, i)
jackknife_stats[i] = statistic_func(jackknife_sample)

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_bca_interval assumes statistic_func can be computed on each jackknife sample of size n-1. With the default statistics=['mean','median','std'] and method='bca', small datasets can break: for example, std uses ddof=1, and when n==2 each jackknife sample has length 1 so statistic_func returns NaN, which then propagates to NaN percentiles and raises in np.percentile. Consider validating minimum n for BCa (or for specific statistics), and/or handling exceptions/NaNs in jackknife stats with a fallback (e.g., a=0 or falling back to percentile CI).

Copilot uses AI. Check for mistakes.
import argparse
import os
import glob
from typing import Dict, List, Optional, Tuple, Union

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dict and Union are imported from typing but not used anywhere in this module. Removing unused imports will keep the file cleaner and avoids lint failures if you run a linter.

Suggested change
from typing import Dict, List, Optional, Tuple, Union
from typing import List, Optional, Tuple

Copilot uses AI. Check for mistakes.
Comment on lines +372 to +373
f"ci_{int(r.ci_level*100)}_lower": r.ci_lower,
f"ci_{int(r.ci_level*100)}_upper": r.ci_upper,

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bootstrap_metrics_directory names CI columns using int(r.ci_level * 100), which truncates non-integer confidence levels (e.g., 0.975 -> 97) and can mislabel results. Consider rounding (e.g., round(...)) or encoding the CI level more precisely in the column name to avoid ambiguity.

Suggested change
f"ci_{int(r.ci_level*100)}_lower": r.ci_lower,
f"ci_{int(r.ci_level*100)}_upper": r.ci_upper,
f"ci_{int(round(r.ci_level * 100))}_lower": r.ci_lower,
f"ci_{int(round(r.ci_level * 100))}_upper": r.ci_upper,

Copilot uses AI. Check for mistakes.
Comment thread src/ensemble_metrics/bootstrap.py Outdated
Comment on lines +223 to +233
df = pd.read_csv(filepath)

# Get metric name from columns (second column typically)
metric_name = [col for col in df.columns if col != "case_id"][0]

# Filter out summary rows (mean, std, etc.)
# Convert case_id to string to handle numeric or mixed types
summary_keywords = ["mean", "std", "median", "min", "max", "sum", "count"]
df["case_id"] = df["case_id"].fillna("").astype(str)
mask = ~df["case_id"].str.lower().isin(summary_keywords)
df_cases = df[mask].copy()

Copilot AI Feb 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_metric_csv assumes a case_id column exists and that there is at least one other column ([...][0]). If the CSV schema differs, this will raise a KeyError/IndexError with a confusing stack trace. Consider validating the expected columns up-front (presence of case_id and exactly one metric column, or explicitly choosing the metric column) and raising a clear ValueError when the schema is unexpected.

Copilot uses AI. Check for mistakes.
Kirscher and others added 2 commits February 1, 2026 23:06
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Kirscher
Kirscher merged commit aad4c36 into main Feb 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants