{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ===================== CSIRO Biomass（纵表5标签）EDA =====================\nCSV_PATH = \"/kaggle/input/csiro-biomass/train.csv\"\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\n%matplotlib inline\npd.set_option(\"display.max_columns\", 120)\npd.set_option(\"display.width\", 180)\n\nEXPECTED_TARGETS = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n\n# ----------------- 读取 & 基本检查 -----------------\ndf_long = pd.read_csv(CSV_PATH)\nprint(\"Shape:\", df_long.shape)\ndisplay(df_long.head())\n\nprint(\"\\n===== 列信息 =====\")\nprint(df_long.dtypes)\n\nprint(\"\\n===== 缺失值情况 =====\")\nmissing = df_long.isna().sum().sort_values(ascending=False)\ndisplay(pd.DataFrame({\"missing_count\": missing, \"missing_pct\": (missing/len(df_long)*100).round(2)}))\n\n# 关键列存在性\nrequired_cols = [\"image_path\", \"target_name\", \"target\"]\nfor c in required_cols:\n    assert c in df_long.columns, f\"缺少必须列：{c}\"\n\n# 解析日期（若失败则保留为字符串）\nif \"Sampling_Date\" in df_long.columns:\n    try:\n        df_long[\"Sampling_Date\"] = pd.to_datetime(df_long[\"Sampling_Date\"])\n    except Exception as e:\n        print(\"Sampling_Date 解析为 datetime 失败，按字符串保留。\", e)\n\n# ----------------- 分组一致性与5行齐全性校验 -----------------\nprint(\"\\n===== 以 image_path 分组的行数分布（应为全 5）=====\")\ncnt_per_img = df_long.groupby(\"image_path\")[\"target_name\"].count()\ndisplay(cnt_per_img.describe())\nbad_count = cnt_per_img[cnt_per_img != 5]\nprint(f\"非5行的图片数：{len(bad_count)}\")\nif len(bad_count) > 0:\n    display(bad_count.head())\n\nprint(\"\\n===== 每张图片是否包含5个预期 target_name（应为全 True）=====\")\ndef has_all_targets(g):\n    return set(g[\"target_name\"].unique()) == set(EXPECTED_TARGETS)\nok_mask = df_long.groupby(\"image_path\").apply(has_all_targets)\nprint(\"缺失某些 target_name 的图片数：\", int((~ok_mask).sum()))\nif (~ok_mask).any():\n    display(ok_mask[~ok_mask].head())\n\n# 同图元数据一致性（日期/州/NDVI/高度等）\nmeta_cols = [c for c in [\"Sampling_Date\",\"State\",\"Pre_GSHH_NDVI\",\"Height_Ave_cm\",\"Species\"] if c in df_long.columns]\nprint(\"\\n===== 同一 image_path 的元数据一致性检查 =====\")\ninconsist = {}\nfor c in meta_cols:\n    nunq = df_long.groupby(\"image_path\")[c].nunique(dropna=False)\n    bad = nunq[nunq > 1]\n    inconsist[c] = len(bad)\n    if len(bad) > 0:\n        print(f\"[不一致] {c}：{len(bad)} 张图片的该字段在5行内不一致（仅展示前5）\")\n        display(df_long[df_long[\"image_path\"].isin(bad.index)][[\"image_path\",\"target_name\",c]].head(15))\nif inconsist:\n    print(\"不一致计数：\", inconsist)\n\n# ----------------- 纵表 -> 横表（每图一行，5目标作列）-----------------\nprint(\"\\n===== 纵转横（pivot）=====\")\npivot = df_long.pivot_table(index=\"image_path\",\n                            columns=\"target_name\",\n                            values=\"target\",\n                            aggfunc=\"mean\")  # 若有重复行，用均值消解\n# 保持列顺序\npivot = pivot.reindex(columns=EXPECTED_TARGETS)\n# 合并元数据（取每图第一行）\nmeta = df_long.drop_duplicates(subset=[\"image_path\"]).set_index(\"image_path\")\nmeta = meta[[c for c in meta_cols if c in meta.columns]]\ndf = pivot.join(meta, how=\"left\").reset_index()\n\nprint(\"横表形状：\", df.shape)\ndisplay(df.head())\n\n# 基本数量核对\nn_images = df[\"image_path\"].nunique()\nprint(f\"唯一图片数量（横表行数）：{n_images}\")\n\n# ----------------- 五目标总体统计 & 缺失 -----------------\nprint(\"\\n===== 五目标 describe() =====\")\ndisplay(df[EXPECTED_TARGETS].describe().T)\n\nprint(\"\\n===== 五目标缺失情况 =====\")\nmiss_t = df[EXPECTED_TARGETS].isna().sum().sort_values(ascending=False)\ndisplay(pd.DataFrame({\"missing_count\": miss_t, \"missing_pct\": (miss_t/len(df)*100).round(2)}))\n\n# ----------------- 工具函数 -----------------\ndef fd_bins(x):\n    x = pd.Series(x).dropna().astype(float)\n    if x.size < 2: return 10\n    q1,q3 = np.percentile(x,[25,75]); iqr = q3-q1\n    if iqr <= 0: return min(max(x.nunique(),10),50)\n    bw = 2*iqr*(len(x)**(-1/3))\n    if bw <= 0: return 30\n    return int(np.clip((x.max()-x.min())/bw, 10, 100))\n\ndef outlier_iqr_pct(x):\n    s = pd.Series(x).dropna().astype(float)\n    if s.empty: return np.nan\n    q1,q3 = np.percentile(s,[25,75]); iqr = q3-q1\n    if iqr <= 0: return 0.0\n    lower = q1 - 1.5*iqr; upper = q3 + 1.5*iqr\n    return float(((s<lower)|(s>upper)).mean()*100)\n\n# ----------------- 单目标：分布/箱线图/异常值/对数建议 -----------------\nfrom math import isfinite\n\nper_target_summary = []\nfor col in EXPECTED_TARGETS:\n    print(f\"\\n================ Target: {col} ================\")\n    s = df[col]\n    display(s.describe(percentiles=[.01,.05,.25,.5,.75,.95,.99]).to_frame().T)\n\n    # 异常值\n    iqr_pct = outlier_iqr_pct(s)\n    print(f\"IQR异常值比例: {iqr_pct:.3f}%\")\n\n    # 偏度/峰度\n    s_valid = s.dropna().astype(float)\n    if len(s_valid) > 1:\n        skew = float(s_valid.skew())\n        kurt = float(s_valid.kurtosis())\n    else:\n        skew, kurt = np.nan, np.nan\n    print(f\"Skewness: {skew:.3f} | Kurtosis: {kurt:.3f}\")\n\n    # 直方图 + 箱线图\n    fig = plt.figure(figsize=(12,4))\n    plt.subplot(1,2,1)\n    if s_valid.empty:\n        plt.text(0.5,0.5,\"No data\",ha=\"center\",va=\"center\")\n    else:\n        plt.hist(s_valid, bins=fd_bins(s_valid))\n    plt.title(f\"Histogram: {col}\"); plt.xlabel(col); plt.ylabel(\"Count\")\n\n    plt.subplot(1,2,2)\n    if s_valid.empty:\n        plt.text(0.5,0.5,\"No data\",ha=\"center\",va=\"center\")\n    else:\n        plt.boxplot(s_valid, vert=True, labels=[col], showfliers=True)\n    plt.title(f\"Boxplot: {col}\")\n    plt.tight_layout(); plt.show()\n\n    # 对数变换建议\n    if s_valid.min() > 0 and isfinite(skew) and abs(skew) > 1:\n        print(\"提示：分布明显右偏且>0，尝试 log1p 变换有助于稳态。\")\n        t = np.log1p(s_valid)\n        plt.figure(figsize=(6,4))\n        plt.hist(t, bins=fd_bins(t))\n        plt.title(f\"log1p({col}) histogram\"); plt.xlabel(f\"log1p({col})\"); plt.ylabel(\"Count\")\n        plt.show()\n    elif s_valid.min() <= 0 and isfinite(skew) and abs(skew) > 1:\n        print(\"提示：分布偏斜但包含非正值，可考虑 Yeo-Johnson/稳健缩放。\")\n\n    per_target_summary.append({\"target\": col, \"iqr_outlier_pct\": iqr_pct, \"skew\": skew})\n\nprint(\"\\n===== 单目标概览汇总 =====\")\ndisplay(pd.DataFrame(per_target_summary).sort_values(\"iqr_outlier_pct\", ascending=False))\n\n# ----------------- 五目标之间的相关性 & 散点矩阵 -----------------\ncorr_targets = df[EXPECTED_TARGETS].corr(numeric_only=True)\nplt.figure(figsize=(5,4))\nim = plt.imshow(corr_targets, interpolation=\"nearest\")\nplt.title(\"Correlation heatmap (5 targets)\")\nplt.xticks(range(len(EXPECTED_TARGETS)), EXPECTED_TARGETS, rotation=45)\nplt.yticks(range(len(EXPECTED_TARGETS)), EXPECTED_TARGETS)\nplt.colorbar(im, fraction=0.046, pad=0.04)\nplt.tight_layout(); plt.show()\n\nfrom pandas.plotting import scatter_matrix\n_ = scatter_matrix(df[EXPECTED_TARGETS].dropna(), figsize=(10,10), diagonal=\"hist\")\nplt.suptitle(\"Scatter Matrix of 5 Targets\", y=1.02)\nplt.show()\n\n# ----------------- 与关键自变量关系（NDVI/高度） -----------------\nfor feat in [\"Pre_GSHH_NDVI\", \"Height_Ave_cm\"]:\n    if feat in df.columns:\n        print(f\"\\n===== {feat} 分布 =====\")\n        f = df[feat].dropna().astype(float)\n        plt.figure(figsize=(6,4))\n        if f.empty:\n            plt.text(0.5,0.5,\"No data\",ha=\"center\",va=\"center\")\n        else:\n            plt.hist(f, bins=fd_bins(f))\n        plt.title(f\"Histogram: {feat}\"); plt.xlabel(feat); plt.ylabel(\"Count\")\n        plt.show()\n\n        # feat vs 每个目标\n        for col in EXPECTED_TARGETS:\n            xs = df[feat].astype(float); ys = df[col].astype(float)\n            mask = xs.notna() & ys.notna()\n            if mask.sum() == 0: \n                continue\n            plt.figure(figsize=(5,4))\n            plt.scatter(xs[mask], ys[mask], s=10, alpha=0.6)\n            plt.title(f\"{feat} vs {col}\")\n            plt.xlabel(feat); plt.ylabel(col)\n            plt.show()\n\n# ----------------- 时间/州 聚合可视化（可选） -----------------\nif \"Sampling_Date\" in df.columns and np.issubdtype(df[\"Sampling_Date\"].dtype, np.datetime64):\n    # 每日均值\n    daily = df.groupby(df[\"Sampling_Date\"].dt.date)[EXPECTED_TARGETS].mean()\n    plt.figure(figsize=(10,4))\n    for col in EXPECTED_TARGETS:\n        plt.plot(daily.index, daily[col], label=col)\n    plt.xticks(rotation=45)\n    plt.title(\"Daily mean of targets\"); plt.legend(); plt.tight_layout(); plt.show()\n\nif \"State\" in df.columns:\n    # 各州均值（样本数>=5）\n    grp = df.groupby(\"State\")[EXPECTED_TARGETS].agg([\"mean\",\"count\"])\n    grp = grp[grp.xs(\"count\", level=1, axis=1).min(axis=1) >= 5]\n    print(\"\\n===== 按州的目标均值（展示样本数>=5的州）=====\")\n    display(grp)\n    # 简单柱状（以 Dry_Total_g 为例）\n    if (\"Dry_Total_g\" in EXPECTED_TARGETS) and (not grp.empty):\n        means = grp[(\"Dry_Total_g\",\"mean\")].sort_values(ascending=False).head(10)\n        plt.figure(figsize=(8,4))\n        means.plot(kind=\"bar\")\n        plt.title(\"Top-10 States by mean Dry_Total_g\")\n        plt.ylabel(\"mean Dry_Total_g\"); plt.xlabel(\"State\")\n        plt.tight_layout(); plt.show()\n\n# ----------------- 关系检验：Total 与 组分之和；GDM 与 Green -----------------\nif all(c in df.columns for c in [\"Dry_Total_g\",\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\"]):\n    comp_sum = df[\"Dry_Green_g\"].fillna(0) + df[\"Dry_Dead_g\"].fillna(0) + df[\"Dry_Clover_g\"].fillna(0)\n    resid_total = df[\"Dry_Total_g\"] - comp_sum\n    print(\"\\n===== Dry_Total_g 与 (Green+Dead+Clover) 关系 =====\")\n    display(resid_total.describe(percentiles=[.01,.05,.25,.5,.75,.95,.99]).to_frame(\"residual\"))\n    plt.figure(figsize=(6,4))\n    plt.hist(resid_total.dropna(), bins=fd_bins(resid_total))\n    plt.title(\"Residual: Dry_Total_g - (Green+Dead+Clover)\")\n    plt.xlabel(\"residual\"); plt.ylabel(\"Count\"); plt.show()\n\nif all(c in df.columns for c in [\"GDM_g\",\"Dry_Green_g\"]):\n    plt.figure(figsize=(5,4))\n    mask = df[\"GDM_g\"].notna() & df[\"Dry_Green_g\"].notna()\n    plt.scatter(df.loc[mask,\"Dry_Green_g\"], df.loc[mask,\"GDM_g\"], s=10, alpha=0.6)\n    plt.title(\"Dry_Green_g vs GDM_g\")\n    plt.xlabel(\"Dry_Green_g\"); plt.ylabel(\"GDM_g\"); plt.show()\n\nprint(\"\\n==== 结束：若你需要把横表 df 导出用于后续建模，可： df.to_csv('train_wide.csv', index=False) ====\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-30T05:34:49.453584Z","iopub.execute_input":"2025-10-30T05:34:49.453901Z","iopub.status.idle":"2025-10-30T05:34:57.119206Z","shell.execute_reply.started":"2025-10-30T05:34:49.453877Z","shell.execute_reply":"2025-10-30T05:34:57.118223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Config ====\nCSV_PATH = \"/kaggle/input/csiro-biomass/train.csv\"  # 如果在本地，请改为你的文件路径，例如 \"/mnt/data/train.csv\"\nOUT_DIR = \"/kaggle/working/eda_plots\"  # 若想保存图片，设定目录名；设为 None 则不保存\n\n# Kaggle/本地 Matplotlib 后端设置（通常无需更改）\nimport matplotlib\n%matplotlib inline\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Imports ====\nimport os\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nif OUT_DIR:\n    Path(OUT_DIR).mkdir(parents=True, exist_ok=True)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Load ====\ndf = pd.read_csv(CSV_PATH)\nprint(\"Shape:\", df.shape)\ndf.head(10)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Basic info ====\nmem_mb = df.memory_usage(deep=True).sum() / (1024**2)\nprint(f\"Rows: {len(df)}  |  Cols: {df.shape[1]}  |  Memory: {mem_mb:.3f} MB\")\n\ndtypes = df.dtypes.sort_index()\ndisplay(pd.DataFrame({\"dtype\": dtypes}))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Missingness ====\nmissing = df.isna().sum().sort_values(ascending=False)\nmissing_pct = (missing / len(df) * 100).round(2)\nmissing_df = pd.DataFrame({\"missing_count\": missing, \"missing_pct\": missing_pct})\ndisplay(missing_df)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Column type splits ====\nnumeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()\nlow_card_num_as_cat = [c for c in numeric_cols if df[c].nunique(dropna=True) <= 10]\ncategorical_cols = [c for c in df.columns if c not in numeric_cols]\ncategorical_like = sorted(set(categorical_cols + low_card_num_as_cat))\nnumeric_strict = [c for c in numeric_cols if c not in low_card_num_as_cat]\n\nprint(\"numeric_strict:\", len(numeric_strict))\nprint(\"categorical_like:\", len(categorical_like))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Descriptive statistics ====\nif numeric_cols:\n    display(df[numeric_cols].describe().T)\n\nif categorical_like:\n    desc_cat = pd.DataFrame({\n        \"unique\": [df[c].nunique(dropna=True) for c in categorical_like],\n        \"top\": [df[c].mode(dropna=True).iloc[0] if df[c].dropna().size else np.nan for c in categorical_like],\n        \"freq_of_top\": [df[c].value_counts(dropna=True).iloc[0] if df[c].dropna().size else np.nan for c in categorical_like],\n    }, index=categorical_like).sort_index()\n    display(desc_cat)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Helper functions ====\ndef maybe_save(fig_title):\n    if OUT_DIR:\n        fname = \"\".join(ch if ch.isalnum() or ch in \"-_.\" else \"_\" for ch in fig_title)\n        path = Path(OUT_DIR) / f\"{fname}.png\"\n        plt.savefig(path, bbox_inches=\"tight\", dpi=150)\n    plt.show()\n    plt.close()\n\ndef freedman_diaconis_bins(series):\n    # series must be numeric and dropna'ed\n    s = series.astype(float)\n    if s.size < 2:\n        return 10\n    q1, q3 = np.percentile(s, [25, 75])\n    iqr = q3 - q1\n    if iqr <= 0:\n        return min(s.nunique(), 50)\n    bin_width = 2 * iqr * (len(s) ** (-1/3))\n    if bin_width <= 0:\n        return 30\n    return int(np.clip((s.max() - s.min()) / bin_width, 10, 100))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Histograms for numeric_strict ====\nfor col in numeric_strict:\n    plt.figure()\n    s = df[col].dropna().astype(float)\n    if s.empty:\n        plt.text(0.5, 0.5, f\"No data for {col}\", ha=\"center\")\n        plt.title(f\"Histogram: {col}\")\n    else:\n        bins = freedman_diaconis_bins(s)\n        plt.hist(s, bins=bins)\n        plt.title(f\"Histogram: {col}\")\n        plt.xlabel(col)\n        plt.ylabel(\"Count\")\n    maybe_save(f\"hist_{col}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Boxplots for numeric_strict (univariate) ====\nfor col in numeric_strict:\n    plt.figure()\n    s = df[col].dropna().astype(float)\n    if s.empty:\n        plt.text(0.5, 0.5, f\"No data for {col}\", ha=\"center\")\n        plt.title(f\"Boxplot: {col}\")\n    else:\n        plt.boxplot(s, vert=True, labels=[col], showfliers=True)\n        plt.title(f\"Boxplot: {col}\")\n        plt.ylabel(col)\n    maybe_save(f\"box_{col}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Categorical-like bar charts (Top 20 levels) ====\nfor col in categorical_like:\n    plt.figure()\n    vc = df[col].astype(\"category\").value_counts(dropna=False).head(20)\n    vc.plot(kind=\"bar\")  # uses matplotlib backend\n    plt.title(f\"Top 20 categories: {col}\")\n    plt.xlabel(col)\n    plt.ylabel(\"Count\")\n    maybe_save(f\"bar_{col}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Correlation heatmap for numeric_strict ====\nif len(numeric_strict) >= 2:\n    plt.figure()\n    corr = df[numeric_strict].corr(numeric_only=True)\n    im = plt.imshow(corr, interpolation=\"nearest\")\n    plt.title(\"Correlation heatmap (numeric features)\")\n    plt.xticks(range(len(numeric_strict)), numeric_strict, rotation=90)\n    plt.yticks(range(len(numeric_strict)), numeric_strict)\n    plt.colorbar(im, fraction=0.046, pad=0.04)\n    maybe_save(\"correlation_heatmap\")\nelse:\n    print(\"Not enough numeric columns for correlation heatmap.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Infer target column (heuristic) ====\ndef infer_target_column(df):\n    numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()\n    lowered = [c.lower() for c in df.columns]\n    candidates = []\n    for i, cl in enumerate(lowered):\n        if any(key in cl for key in [\n            \"target\", \"label\", \"biomass\", \"agb\", \"agb_t_ha\", \"agbtha\", \"agb_ha\",\n            \"agb_t_per_ha\", \"agb_per_ha\", \"aboveground\", \"yield\", \"y\", \"biomasstpa\", \"agb_mgha\"\n        ]):\n            candidates.append(df.columns[i])\n    cand_numeric = [c for c in candidates if c in numeric_cols]\n    if cand_numeric:\n        cand_numeric.sort(key=lambda c: df[c].isna().sum())\n        return cand_numeric[0]\n    return None\n\nTARGET = infer_target_column(df)\nprint(\"Inferred target:\", TARGET)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Target distribution ====\nif TARGET is not None and TARGET in df.columns:\n    plt.figure()\n    y = df[TARGET]\n    if y.dtype.kind in \"ifu\":\n        s = y.dropna().astype(float)\n        if s.size:\n            plt.hist(s, bins=30)\n        else:\n            plt.text(0.5, 0.5, \"No target data\", ha=\"center\")\n        plt.xlabel(TARGET)\n        plt.ylabel(\"Count\")\n    else:\n        vc = y.astype(\"category\").value_counts(dropna=False)\n        vc.plot(kind=\"bar\")\n        plt.xlabel(TARGET)\n        plt.ylabel(\"Count\")\n    plt.title(f\"Target distribution: {TARGET}\")\n    maybe_save(f\"target_dist_{TARGET}\")\nelse:\n    print(\"No obvious target inferred. You can set TARGET = 'your_target_column' and rerun related cells.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Numeric predictors vs numeric target: scatter ====\nif TARGET is not None and TARGET in df.columns and df[TARGET].dtype.kind in \"ifu\":\n    for col in [c for c in numeric_strict if c != TARGET]:\n        plt.figure()\n        xs = df[col].astype(float)\n        ys = df[TARGET].astype(float)\n        mask = xs.notna() & ys.notna()\n        if mask.sum() == 0:\n            plt.text(0.5, 0.5, \"No overlapping data\", ha=\"center\")\n        else:\n            plt.scatter(xs[mask], ys[mask], s=10, alpha=0.6)\n        plt.title(f\"{col} vs {TARGET}\")\n        plt.xlabel(col)\n        plt.ylabel(TARGET)\n        maybe_save(f\"scatter_{col}_vs_{TARGET}\")\nelse:\n    print(\"Skip scatter: no numeric target detected.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Categorical-like predictors vs numeric target: boxplots ====\nif TARGET is not None and TARGET in df.columns and df[TARGET].dtype.kind in \"ifu\" and len(categorical_like) > 0:\n    for col in categorical_like:\n        plt.figure()\n        tmp = df[[col, TARGET]].dropna()\n        if tmp.empty:\n            plt.text(0.5, 0.5, \"No data\", ha=\"center\")\n        else:\n            top_levels = tmp[col].value_counts().head(12).index\n            tmp2 = tmp[tmp[col].isin(top_levels)]\n            data = [tmp2.loc[tmp2[col] == lvl, TARGET].values for lvl in top_levels]\n            plt.boxplot(data, labels=[str(l) for l in top_levels], showfliers=False)\n        plt.title(f\"{TARGET} by {col} (top 12 levels)\")\n        plt.xlabel(col)\n        plt.ylabel(TARGET)\n        maybe_save(f\"box_{TARGET}_by_{col}\")\nelse:\n    print(\"Skip categorical vs target boxplots.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== Outlier overview via IQR (numeric_strict) ====\nfrom math import isfinite\n\nrows = []\nfor col in numeric_strict:\n    s = df[col].astype(float).dropna()\n    if s.empty:\n        rows.append((col, np.nan, np.nan, np.nan))\n        continue\n    q1, q3 = np.percentile(s, [25, 75])\n    iqr = q3 - q1\n    if iqr <= 0:\n        rows.append((col, q1, q3, 0.0))\n        continue\n    lower = q1 - 1.5 * iqr\n    upper = q3 + 1.5 * iqr\n    pct_out = ((s < lower) | (s > upper)).mean() * 100.0\n    rows.append((col, round(q1, 4), round(q3, 4), round(pct_out, 3)))\n\noutlier_df = pd.DataFrame(rows, columns=[\"column\", \"Q1\", \"Q3\", \"%_outliers_IQR\"])\ndisplay(outlier_df.sort_values(\"%_outliers_IQR\", ascending=False))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}