CoolFace
Apppublic

TwinklData/Community_Collections_App

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
column_detection.py272 linesDownload Raw Back to src
1from __future__ import annotations  # harmless on 3.11+, useful on 3.7‑3.102import re3import string4from typing import Sequence, Dict, Tuple, Optional5import pandas as pd6 7 8# ========= HELPER FUNCTIONS ========9 10def _max_or_eps(values, eps: float = 1e-9) -> float:11    """Avoid divide‑by‑zero during normalisation."""12    return max(values) or eps13 14 15def _normalise(value: float, max_value: float) -> float:16    return value / max_value if max_value else 0.017 18# =================== FREEFORM COL =====================   19 20def detect_freeform_col(21    df: pd.DataFrame,22    *,23    length_weight: float = 0.4,24    punct_weight: float = 0.3,25    unique_weight: float = 0.3,26    low_uniqueness_penalty: float = 0.4,27    name_boosts: dict[str, float] | None = None,28    min_score: float = 0.50,29    return_scores: bool = False,30) -> str | None | Tuple[str | None, Dict[str, float]]:31    """32    Guess which *object* column contains free‑text answers or comments.33 34    A good free‑text column tends to be longish, rich in punctuation,35    and fairly unique row‑to‑row.36 37    name_boosts38        e.g. ``{"additional_comment": 3.1, "usage_reason": 0.5}``39        Multiplicative factors applied if the token appears in the header.40    """41    name_boosts = name_boosts or {}42    obj_cols = df.select_dtypes(include=["object"]).columns43 44    # quick exit45    if not obj_cols.size:46        return (None, {}) if return_scores else None47 48    # pre‑compute raw metrics49    raw: Dict[str, dict[str, float]] = {}50    for col in obj_cols:51        ser = df[col].dropna().astype(str)52        if ser.empty:53            continue54        raw[col] = {55            "avg_len": ser.str.len().mean(),56            "avg_punct": ser.apply(lambda s: sum(c in string.punctuation for c in s)).mean(),57            "unique_ratio": ser.nunique() / len(ser),58        }59 60    if not raw:61        return (None, {}) if return_scores else None62 63    # normalisers64    max_len = _max_or_eps([m["avg_len"] for m in raw.values()])65    max_punc = _max_or_eps([m["avg_punct"] for m in raw.values()])66 67    # composite scores68    scores: Dict[str, float] = {}69    for col, m in raw.items():70        score = (71            length_weight * _normalise(m["avg_len"], max_len)72            + punct_weight * _normalise(m["avg_punct"], max_punc)73            + unique_weight * m["unique_ratio"]74        )75 76        # header boosts / penalties77        for token, factor in name_boosts.items():78            if token in col.lower():79                score *= factor80 81        # penalise low uniqueness82        if m["unique_ratio"] < low_uniqueness_penalty:83            score *= 0.584 85        scores[col] = score86 87    best_col, best_score = max(scores.items(), key=lambda kv: kv[1])88    passed = best_score >= min_score89 90    if return_scores:91        return (best_col if passed else None, scores)92    return best_col if passed else None93 94 95# ================= ID COLUMN =================96 97def detect_id_col(df: pd.DataFrame) -> str | None:98    n_rows = len(df)99 100    # 1) Name‐based detection101    name_pattern = re.compile(r'\b(id|identifier|key)\b', re.IGNORECASE)102    for col in df.columns:103        if name_pattern.search(col):104            return col105 106    # 2) Uniqueness detection: columns where every row is unique107    unique_cols = [108        col for col in df.columns109        if df[col].nunique(dropna=False) == n_rows110    ]111    if not unique_cols:112        return None113 114    # 3) Prioritise int cols over object cols when both are unique115    non_unnamed = [c for c in unique_cols if not c.startswith("Unnamed")]116    candidates = non_unnamed or unique_cols117 118    # 4) Prefer integer dtypes among candidates119    for col in candidates:120        if pd.api.types.is_integer_dtype(df[col]):121            return col122 123    # Fallback: return the first candidate124    return candidates[0]125 126 127# ============== SCHOOL TYPE COLUMN =============128 129def detect_school_type_col(130    df: pd.DataFrame,131    *,132    uniqueness_weight: float = 0.3,133    content_match_weight: float = 0.4, # <-- New weight for content134    length_weight: float = 0.2,135    punct_weight: float = 0.1,136    name_boosts: dict[str, float] | None = None,137    value_keywords: set[str] | None = None, # <-- New parameter for keywords138    min_score: float = 0.40,139    high_uniqueness_penalty: float = 0.95,140    return_scores: bool = False,141) -> str | None | Tuple[str | None, Dict[str, float]]:142    """143    Analyzes a DataFrame to find the column that most likely represents a 'school type'.144 145    The function operates on heuristics based on common characteristics of a school-type col:146    1.  **Content Match**: A significant portion of values match known school types (the strongest signal).147    2.  **Low Uniqueness**: Values are often repeated (e.g., 'Primary', 'All-through').148    3.  **Short Text**: Entries are typically brief.149    4.  **Minimal Punctuation**: Values are clean strings, not sentences.150    5.  **Header Keywords**: The column name itself is a strong indicator (e.g., 'School Type').151    """152    # More robust default name boosts153    if name_boosts is None:154        name_boosts = {'school': 3.0, 'type': 2.0}155 156    # Default set of keywords to search for within the column's values157    if value_keywords is None:158        value_keywords = {159            'nursery', 'primary', 'secondary', 'infant', 'junior',160            'college', 'academy', 'independent', 'special', 'pru',161            'all-through', 'middle', 'state', 'educator', 'home'162        }163 164    obj_cols = df.select_dtypes(include=["object"]).columns165    if not obj_cols.size:166        return (None, {}) if return_scores else None167 168    # Pre-compute raw metrics for each object column169    raw_metrics: Dict[str, dict[str, float]] = {}170    for col in obj_cols:171        ser = df[col].dropna().astype(str)172        if ser.empty:173            continue174 175        # --- New Content Match Calculation ---176        unique_values = ser.unique()177        content_match_score = 0.0178        if len(unique_values) > 0:179            match_count = 0180            for val in unique_values:181                # Check if any keyword is a substring of the lowercase value182                if any(keyword in val.lower() for keyword in value_keywords):183                    match_count += 1184            content_match_score = match_count / len(unique_values)185        # --- End of New Calculation ---186 187        raw_metrics[col] = {188            "avg_len": ser.str.len().mean(),189            "avg_punct": ser.apply(lambda s: sum(c in string.punctuation for c in s)).mean(),190            "unique_ratio": ser.nunique() / len(ser) if len(ser) > 0 else 0.0,191            "content_match": content_match_score # Store the new score192        }193 194    if not raw_metrics:195        return (None, {}) if return_scores else None196 197    # Get max values for normalization198    max_len = _max_or_eps([m["avg_len"] for m in raw_metrics.values()])199    max_punc = _max_or_eps([m["avg_punct"] for m in raw_metrics.values()])200 201    # Calculate a final score for each column202    scores: Dict[str, float] = {}203    for col, metrics in raw_metrics.items():204        len_score = 1 - _normalise(metrics["avg_len"], max_len)205        punc_score = 1 - _normalise(metrics["avg_punct"], max_punc)206        uniq_score = 1 - metrics["unique_ratio"]207 208        # --- Updated Final Scoring Formula ---209        score = (210            content_match_weight * metrics["content_match"] # Use the new score directly211            + uniqueness_weight * uniq_score212            + length_weight * len_score213            + punct_weight * punc_score214        )215 216        # Apply boosts for matching header keywords217        for token, factor in name_boosts.items():218            if token in col.lower().strip():219                score *= factor220 221        # Apply penalty for columns that are almost entirely unique222        if metrics["unique_ratio"] > high_uniqueness_penalty:223            score *= 0.1  # Heavy penalty224 225        scores[col] = score226 227    if not scores:228         return (None, {}) if return_scores else None229 230    best_col, best_score = max(scores.items(), key=lambda item: item[1])231    passed = best_score >= min_score232 233    if return_scores:234        return (best_col if passed else None, scores)235    return best_col if passed else None236# =========== USAGE ============237 238def main():239 240    df = pd.read_csv('data/raw/new-application-format-data.csv')241    df.columns = df.columns.str.strip()242 243    print("--- Testing Column Detection Functions ---")244 245    id_col = detect_id_col(df)246    freeform_col, freeform_scores = detect_freeform_col(df, return_scores=True)247    school_type_col, school_type_scores = detect_school_type_col(df, return_scores=True)248 249    print(f"\nDetected ID Column: '{id_col}'")250    print(f"Detected Free-Form Column: '{freeform_col}'")251    print(f"Detected School Type Column: '{school_type_col}'")252    print()253    print("\n--- Free-form Column Scores (Higher is better) ---")254    if freeform_scores:255        sorted_scores = sorted(freeform_scores.items(), key=lambda item: item[1], reverse=True)256        for col, score in sorted_scores:257            print(f"  - {col:<25}: {score:.4f}")258    else:259        print("No object columns found to score for freeform col...")260 261 262    print("\n--- School Type Column Scores (Higher is better) ---")263    if school_type_scores:264        sorted_scores = sorted(school_type_scores.items(), key=lambda item: item[1], reverse=True)265        for col, score in sorted_scores:266            print(f"  - {col:<25}: {score:.4f}")267    else:268        print("No object columns found to score for career.")269 270if __name__ == '__main__':271    main()272