Satyam311's picture
Clean deploy: numpy vector search, binaries excluded
d4ec2fa
Raw History Blame Contribute Delete
3.98 kB
"""
Ticket 3 — Clean & Split Dataset.
Produces train/val/test CSVs (70/15/15) from `reports` LEFT JOIN `gold_labels`.
Split is stratified on a combined key so that:
- reports with an ATA-chapter label are distributed proportionally across splits
- reports with only a severity label are distributed proportionally across splits
- reports with neither label (~49%) are kept too (useful for the semantic-search /
embeddings ticket later, just not for supervised training) and bucketed together
- a single asrs_report_id can never appear in more than one split (checked at the end)
"""
import argparse
import sqlite3
from pathlib import Path
import pandas as pd
from sklearn.model_selection import train_test_split
RANDOM_SEED = 42
def stratify_key(row):
if pd.notna(row["ata_chapter_label"]):
return f"ATA_{row['ata_chapter_label']}"
if pd.notna(row["severity_label"]):
return f"SEV_{row['severity_label']}"
return "UNLABELED"
def load_joined(db_path: str) -> pd.DataFrame:
conn = sqlite3.connect(db_path)
df = pd.read_sql_query(
"""
SELECT r.asrs_report_id, r.narrative_text, r.report_date, r.aircraft_type,
r.phase_of_flight, g.ata_chapter_label, g.severity_label
FROM reports r
LEFT JOIN gold_labels g ON r.asrs_report_id = g.asrs_report_id
""",
conn,
)
conn.close()
return df
def split(db_path: str, out_dir: str):
df = load_joined(db_path)
df["strat_key"] = df.apply(stratify_key, axis=1)
# Any class in strat_key with fewer than 2 members can't be split at all -> route to UNLABELED-like
# bucket instead of crashing. (Not expected given our smallest real class is 11, but defensive.)
counts = df["strat_key"].value_counts()
too_small = counts[counts < 2].index.tolist()
if too_small:
print(f"WARNING: merging {len(too_small)} tiny strat groups into UNLABELED: {too_small}")
df.loc[df["strat_key"].isin(too_small), "strat_key"] = "UNLABELED"
train_df, temp_df = train_test_split(
df, test_size=0.30, stratify=df["strat_key"], random_state=RANDOM_SEED
)
val_df, test_df = train_test_split(
temp_df, test_size=0.50, stratify=temp_df["strat_key"], random_state=RANDOM_SEED
)
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
cols = ["asrs_report_id", "narrative_text", "report_date", "aircraft_type",
"phase_of_flight", "ata_chapter_label", "severity_label"]
train_df[cols].to_csv(out / "train.csv", index=False)
val_df[cols].to_csv(out / "val.csv", index=False)
test_df[cols].to_csv(out / "test.csv", index=False)
# ---- Verification ----
print(f"Train: {len(train_df)} Val: {len(val_df)} Test: {len(test_df)} Total: {len(df)}")
ids_train = set(train_df["asrs_report_id"])
ids_val = set(val_df["asrs_report_id"])
ids_test = set(test_df["asrs_report_id"])
overlap = (ids_train & ids_val) | (ids_train & ids_test) | (ids_val & ids_test)
print(f"Cross-split ID overlap (must be 0): {len(overlap)}")
print()
print("ATA-chapter label proportions (train / val / test):")
for split_name, split_df in [("train", train_df), ("val", val_df), ("test", test_df)]:
labeled = split_df["ata_chapter_label"].notna().sum()
print(f" {split_name}: {labeled} labeled ({labeled/len(split_df):.1%} of split)")
print()
print("Severity label proportions (train / val / test):")
for split_name, split_df in [("train", train_df), ("val", val_df), ("test", test_df)]:
labeled = split_df["severity_label"].notna().sum()
print(f" {split_name}: {labeled} labeled ({labeled/len(split_df):.1%} of split)")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--db", default="data/processed/reports.db")
parser.add_argument("--out", default="data/processed")
args = parser.parse_args()
split(args.db, args.out)