| """ |
| Inject our Signal-Faithful rationales into OpenTSLM's expected CSV schema, so their |
| training pipeline trains on OUR rationales with everything else held fixed. |
| |
| OpenTSLM's HAR loader (har_cot_loader.py) expects, at |
| OpenTSLM/src/data/har_cot/har_cot_{train,val,test}_cot.csv |
| columns: x_axis, y_axis, z_axis, label, rationale |
| where the three *_axis columns are stringified arrays and `rationale` is the CoT the |
| model is trained to produce (OpenTSLMSP.compute_loss teacher-forces on it). |
| |
| This script keeps the original time series + label and swaps in our faithful rationale, |
| matched row-for-row (the splits are produced in the same order). Run the resulting |
| training twice — once with the original CSVs, once with ours — to get the downstream |
| faithful-vs-baseline comparison (paper Section "Downstream HAR Classification"). |
| |
| Usage: |
| python grpo/inject_faithful_data.py \ |
| --orig OpenTSLM/src/data/har_cot/har_cot_train_cot.csv \ |
| --ours /path/to/faithful_har_cot_train.csv \ |
| --out OpenTSLM/src/data/har_cot/har_cot_train_cot.csv \ |
| --ours-rationale-col our_rationale # or 'rationale' |
| [--backup] # save the original alongside as *.orig.csv first |
| |
| Repeat for val/test. For ECG, our faithful CSV uses (sample_id, our_rationale, ...) and |
| ECGQACoTQADataset has its own loader; adapt --ours-rationale-col and the merge key with |
| --key sample_id accordingly. |
| """ |
|
|
| import argparse |
| import os |
| import shutil |
|
|
| import pandas as pd |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--orig", required=True, help="OpenTSLM's original CSV (time series + label)") |
| ap.add_argument("--ours", required=True, help="our faithful CSV containing the rationale") |
| ap.add_argument("--out", required=True, help="destination CSV (OpenTSLM schema)") |
| ap.add_argument("--ours-rationale-col", default="our_rationale", |
| help="rationale column name in --ours (try 'our_rationale' or 'rationale')") |
| ap.add_argument("--key", default=None, |
| help="optional join key present in both CSVs (e.g. sample_id). " |
| "If omitted, rows are matched by position.") |
| ap.add_argument("--backup", action="store_true", help="copy --orig to *.orig.csv first") |
| args = ap.parse_args() |
|
|
| orig = pd.read_csv(args.orig) |
| ours = pd.read_csv(args.ours) |
| print(f"orig: {len(orig)} rows, cols={list(orig.columns)}") |
| print(f"ours: {len(ours)} rows, cols={list(ours.columns)}") |
|
|
| if args.ours_rationale_col not in ours.columns: |
| raise SystemExit( |
| f"--ours-rationale-col '{args.ours_rationale_col}' not in {list(ours.columns)}" |
| ) |
|
|
| merged = orig.copy() |
| if args.key: |
| if args.key not in orig.columns or args.key not in ours.columns: |
| raise SystemExit(f"--key '{args.key}' must be in both CSVs") |
| rmap = dict(zip(ours[args.key], ours[args.ours_rationale_col])) |
| before = len(merged) |
| merged = merged[merged[args.key].isin(rmap)].copy() |
| merged["rationale"] = merged[args.key].map(rmap) |
| print(f"joined on {args.key}: kept {len(merged)}/{before} rows") |
| else: |
| if len(orig) != len(ours): |
| raise SystemExit( |
| f"row count mismatch ({len(orig)} vs {len(ours)}); use --key for a safe join" |
| ) |
| merged["rationale"] = ours[args.ours_rationale_col].values |
|
|
| n_empty = merged["rationale"].isna().sum() |
| if n_empty: |
| print(f"WARNING: {n_empty} rows have no rationale after merge") |
|
|
| if args.backup and os.path.abspath(args.orig) == os.path.abspath(args.out): |
| bak = args.orig.replace(".csv", ".orig.csv") |
| if not os.path.exists(bak): |
| shutil.copy(args.orig, bak) |
| print(f"backed up original -> {bak}") |
|
|
| os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True) |
| merged.to_csv(args.out, index=False) |
| print(f"wrote {len(merged)} rows -> {args.out}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|