""" Phase 1 — Data preparation for the Support Integrity Auditor. Responsibilities ---------------- 1. Load the raw CRM ticket CSV. 2. Extract the *real issue sentence* from each description (the dataset pads a genuine leading sentence with random faker words — see README §Data). 3. Build the text the classifier sees + structured metadata features. 4. Create a FIXED, stratified train/test split (held-out evaluation set) that is independent of any pseudo-label, so labels can never leak into the split. Run: python3 src/data_prep.py Out: artifacts/data/processed.parquet (+ a printed summary) """ from __future__ import annotations import os, re, sys import pandas as pd # make `from src import config` work when run as a plain script sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from src import config as C from sklearn.model_selection import train_test_split # --------------------------------------------------------------------------- # # Text extraction # --------------------------------------------------------------------------- # _GREETING = re.compile(r"^\s*(hi|hello|hey|dear)\b[^,.:;!?]*[,:]?\s*", re.IGNORECASE) _FIRST_SENT = re.compile(r"(.+?[.?!])(?:\s|$)") def extract_leading_sentence(desc: str) -> str: """Strip the greeting and return the first sentence (the genuine issue). The trailing faker words form a second 'sentence'; we deliberately drop them because raw keyword counts over the filler are misleading and are exactly the surface an adversarial ticket would attack. """ t = str(desc).strip() t = _GREETING.sub("", t, count=1) m = _FIRST_SENT.search(t) lead = (m.group(1) if m else t).strip() return lead or t def build_model_text(row: pd.Series) -> str: """Text fed to DeBERTa: assigned priority + structured tags + the real issue. Including the *assigned priority* is intentional and correct — the task is to judge whether THAT priority disagrees with the ticket's content. The inferred severity / mismatch label is never shown to the model. """ tier = C.domain_tier(row[C.COL_EMAIL]) return ( f"priority: {row[C.COL_PRIORITY]} | " f"channel: {row[C.COL_CHANNEL]} | " f"category: {row[C.COL_CATEGORY]} | " f"tier: {tier} | " f"{str(row[C.COL_SUBJECT]).strip()}. {row['lead_sentence']}" ) # --------------------------------------------------------------------------- # # Pipeline # --------------------------------------------------------------------------- # def load_raw() -> pd.DataFrame: df = pd.read_csv(C.RAW_CSV) df.columns = [c.strip() for c in df.columns] return df def build_features(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() df[C.COL_RES_HRS] = pd.to_numeric(df[C.COL_RES_HRS], errors="coerce") df[C.COL_SAT] = pd.to_numeric(df[C.COL_SAT], errors="coerce") df["lead_sentence"] = df[C.COL_DESC].map(extract_leading_sentence) df["domain_tier"] = df[C.COL_EMAIL].map(C.domain_tier) df["priority_score"] = df[C.COL_PRIORITY].map(C.PRIORITY_TO_SCORE) df["category_prior"] = df[C.COL_CATEGORY].map(C.CATEGORY_SEVERITY_PRIOR) df["model_text"] = df.apply(build_model_text, axis=1) return df def make_split(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() idx_train, idx_test = train_test_split( df.index, test_size=C.TRAIN["test_size"], random_state=C.SEED, stratify=df[C.COL_PRIORITY], # stable strata, independent of labels ) df["split"] = "train" df.loc[idx_test, "split"] = "test" return df def main() -> pd.DataFrame: df = load_raw() print(f"[load] {len(df):,} rows x {df.shape[1]} cols") df = build_features(df) df = make_split(df) out = C.PROC_DIR / "processed.parquet" df.to_parquet(out, index=False) # summary print(f"[split] train={int((df['split']=='train').sum()):,} " f"test={int((df['split']=='test').sum()):,}") print("[priority dist]") print(df[C.COL_PRIORITY].value_counts(normalize=True).round(3).to_string()) print("[domain tier dist]") print(df["domain_tier"].value_counts(normalize=True).round(3).to_string()) print("\n[sample lead-sentence extraction]") for _, r in df.head(4).iterrows(): print(f" RAW : {r[C.COL_DESC][:90]}") print(f" LEAD: {r['lead_sentence']}") print("\n[sample model_text]") print(" " + df.iloc[1]["model_text"]) print(f"\n[saved] {out}") return df if __name__ == "__main__": main()