feat: version-agnostic analysis scripts with shared version helpers
- scripts/bicorder_common.py (new): single source of truth for historical gradient renames (COLUMN_RENAMES), version detection (bicorder_version col → version col → data/<type>_<version>/ dir convention), and training-CSV auto-selection (find_training_csv) - classify_readings.py: auto-select training run by recorded bicorder version (excludes the input itself to avoid circular training), canonicalize old column names, version-mismatch warnings - bicorder_classifier.py / export_model_for_js.py: share renames + dimension loader; instructive error when clustering results are missing; bicorder_version recorded in exported models - compare_analyses.py: CLI (reference + comparison CSVs), rename canonicalization so runs of different versions align on shared gradients, Descriptor dedup (was silently skewing merges); legacy no-arg audit intact - scripts/univariate_analysis.py (new): per-protocol/per-gradient averages, distributions, summary stats; --img publishes the three README summary charts to img/ - sync_readings.sh: defer classifier training to auto-matching; gitignore analysis/.venv and __pycache__
This commit is contained in:
1 parent
a4f16e8e4d
commit
55cbd6cd5d
8 files changed
+591
-109
No files matched your search
@@ -2,13 +2,42 @@
|
||||
"""
|
||||
Compare multiple analysis CSV files to determine which most closely resembles a reference file.
|
||||
Uses Euclidean distance, correlation, and RMSE metrics.
|
||||
|
||||
Readings files are canonicalized to the current bicorder terminology (history of
|
||||
renames in bicorder_common.py), and comparison runs on the gradient columns
|
||||
shared by all files — so any versions can be compared, and renamed gradients
|
||||
remain comparable across version boundaries.
|
||||
|
||||
Usage:
|
||||
# Legacy audit: manual review vs. the three model test runs (as in README)
|
||||
python3 scripts/compare_analyses.py
|
||||
|
||||
# Explicit: reference file first, then any number of comparison files
|
||||
python3 scripts/compare_analyses.py \
|
||||
data/synthetic_1.2.6/readings.csv data/synthetic_1.4.0/readings.csv
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from scipy.stats import pearsonr
|
||||
from pathlib import Path
|
||||
|
||||
from bicorder_common import apply_renames, csv_version
|
||||
|
||||
|
||||
def load_canonical(path):
|
||||
"""Load a readings CSV, canonicalize columns, and coerce gradient values to numeric."""
|
||||
df = pd.read_csv(path, quotechar='"', escapechar='\\', engine='python')
|
||||
df = apply_renames(df)
|
||||
numeric_cols = [col for col in df.columns if
|
||||
col.startswith(('Design_', 'Entanglement_', 'Experience_'))]
|
||||
for col in numeric_cols:
|
||||
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||
return df, numeric_cols
|
||||
|
||||
|
||||
def calculate_euclidean_distance(df1, df2, numeric_cols):
|
||||
"""Calculate Euclidean distance between two dataframes."""
|
||||
distances = []
|
||||
@@ -45,18 +74,23 @@ def calculate_correlation(df1, df2, numeric_cols):
|
||||
def compare_analyses(reference_file, comparison_files):
|
||||
"""Compare multiple analysis files to a reference file."""
|
||||
|
||||
# Read reference file
|
||||
# Read and canonicalize reference file
|
||||
print(f"Reading reference file: {reference_file}")
|
||||
ref_df = pd.read_csv(reference_file, quotechar='"', escapechar='\\', engine='python')
|
||||
# Get numeric columns (all the rating dimensions)
|
||||
numeric_cols = [col for col in ref_df.columns if
|
||||
col.startswith(('Design_', 'Entanglement_', 'Experience_'))]
|
||||
ref_version = csv_version(reference_file)
|
||||
if ref_version:
|
||||
print(f" bicorder version recorded: v{ref_version}")
|
||||
ref_df, ref_numeric = load_canonical(reference_file)
|
||||
|
||||
# Convert numeric columns to numeric type, coercing errors to NaN
|
||||
for col in numeric_cols:
|
||||
ref_df[col] = pd.to_numeric(ref_df[col], errors='coerce')
|
||||
# Repeated Descriptor entries (kept as control cases in the datasets) would
|
||||
# multiply rows in the Descriptor-based merge; keep first occurrence like
|
||||
# the classifier does.
|
||||
if 'Descriptor' in ref_df.columns:
|
||||
before = len(ref_df)
|
||||
ref_df = ref_df.drop_duplicates(subset='Descriptor', keep='first')
|
||||
if len(ref_df) < before:
|
||||
print(f" Deduplicated reference: {before} → {len(ref_df)} rows (kept first of repeated Descriptor)")
|
||||
|
||||
print(f"\nFound {len(numeric_cols)} numeric dimensions to compare")
|
||||
print(f"\nFound {len(ref_numeric)} numeric dimensions in reference file")
|
||||
print(f"Comparing {len(ref_df)} protocols\n")
|
||||
print("="*80)
|
||||
|
||||
@@ -66,12 +100,23 @@ def compare_analyses(reference_file, comparison_files):
|
||||
print(f"\nComparing: {Path(comp_file).name}")
|
||||
print("-"*80)
|
||||
|
||||
# Read comparison file
|
||||
comp_df = pd.read_csv(comp_file, quotechar='"', escapechar='\\', engine='python')
|
||||
# Read and canonicalize comparison file
|
||||
comp_version = csv_version(comp_file)
|
||||
if comp_version:
|
||||
print(f" bicorder version recorded: v{comp_version}")
|
||||
comp_df, comp_numeric = load_canonical(comp_file)
|
||||
if 'Descriptor' in comp_df.columns:
|
||||
before = len(comp_df)
|
||||
comp_df = comp_df.drop_duplicates(subset='Descriptor', keep='first')
|
||||
if len(comp_df) < before:
|
||||
print(f" Deduplicated comparison: {before} → {len(comp_df)} rows (kept first of repeated Descriptor)")
|
||||
|
||||
# Convert numeric columns to numeric type, coercing errors to NaN
|
||||
for col in numeric_cols:
|
||||
comp_df[col] = pd.to_numeric(comp_df[col], errors='coerce')
|
||||
# Restrict to columns shared by both files (post-rename): enables
|
||||
# comparing across bicorder versions when gradients were renamed
|
||||
numeric_cols = [col for col in ref_numeric if col in comp_numeric]
|
||||
missing = [col for col in ref_numeric if col not in comp_numeric]
|
||||
if missing:
|
||||
print(f" Note: {len(missing)} gradient(s) absent here are excluded: {', '.join(missing)}")
|
||||
|
||||
# Ensure same protocols in same order (match by Descriptor)
|
||||
if 'Descriptor' in ref_df.columns and 'Descriptor' in comp_df.columns:
|
||||
@@ -155,32 +200,58 @@ def compare_analyses(reference_file, comparison_files):
|
||||
|
||||
return results
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Define file paths
|
||||
reference_file = "data/synthetic_1.2.6/readings_manual.csv"
|
||||
comparison_files = [
|
||||
def main(argv=None):
|
||||
"""CLI entry point.
|
||||
|
||||
With no arguments, falls back to the legacy audit: the 1.2.6 manual review
|
||||
against the three model test runs (as described in README.md).
|
||||
"""
|
||||
legacy_reference = "data/synthetic_1.2.6/readings_manual.csv"
|
||||
legacy_comparisons = [
|
||||
"data/synthetic_1.2.6/readings_gemma3-12b.csv",
|
||||
"data/synthetic_1.2.6/readings_gpt-oss.csv",
|
||||
"data/synthetic_1.2.6/readings_mistral.csv"
|
||||
"data/synthetic_1.2.6/readings_mistral.csv",
|
||||
]
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv[1:]
|
||||
if argv:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Compare readings CSVs to a reference (Euclidean distance, RMSE, correlation)',
|
||||
epilog="""Example (cross-version):
|
||||
python3 scripts/compare_analyses.py \\
|
||||
data/synthetic_1.4.0/readings.csv data/synthetic_1.2.6/readings.csv
|
||||
""",
|
||||
)
|
||||
parser.add_argument('reference', help='Reference readings CSV')
|
||||
parser.add_argument('comparisons', nargs='+', help='Comparison readings CSVs')
|
||||
args = parser.parse_args(argv)
|
||||
reference_file, comparison_files = args.reference, args.comparisons
|
||||
else:
|
||||
reference_file, comparison_files = legacy_reference, legacy_comparisons
|
||||
|
||||
# Check if files exist
|
||||
if not Path(reference_file).exists():
|
||||
print(f"Error: Reference file '{reference_file}' not found")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
existing = [file for file in comparison_files if Path(file).exists()]
|
||||
for file in comparison_files:
|
||||
if not Path(file).exists():
|
||||
print(f"Warning: Comparison file '{file}' not found, skipping...")
|
||||
comparison_files.remove(file)
|
||||
|
||||
if not comparison_files:
|
||||
if not existing:
|
||||
print("Error: No comparison files found")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
# Run comparison
|
||||
results = compare_analyses(reference_file, comparison_files)
|
||||
results = compare_analyses(reference_file, existing)
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("Analysis complete!")
|
||||
print("="*80)
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user