{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "38967358-1993-4d2a-ba1e-bfe4f484cdea",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Period: 2020 - 2024\n",
      "Rows: 1827\n",
      "Unique dates: 1827\n",
      "Date range: 2020-01-01 to 2024-12-31\n",
      "\\nSaved outputs:\n",
      " - Figure1_Boxplot_peak_vs_rest_bw.tif\n",
      " - Figure2_Histograms_raw_log_bw.tif\n",
      " - Figure3_Season_duration_start_end_bw.tif\n",
      " - Figure4_Start_DOY_vs_thermal_indices_bw.tif\n",
      " - Figure5_Spearman_correlation_bw.tif\n",
      " - Figure6_Scree_plot_bw.tif\n",
      " - Figure7_PCA_biplot_bw.tif\n",
      " - Figure8_Varimax_rotation_bw.tif\n",
      " - NB_full_summary.txt\n",
      " - NB_season_summary.txt\n",
      " - Table1_Descriptive_statistics.csv\n",
      " - Table2_Season_timing.csv\n",
      " - Table3_Start_DOY_vs_thermal_indices.csv\n",
      " - Table4_ZINB_results.csv\n",
      " - Table5_PCA_summary.csv\n",
      " - Varimax_rotated_loadings.csv\n",
      " - ZINB_summary.txt\n"
     ]
    }
   ],
   "source": [
    "#!/usr/bin/env python3\n",
    "# -*- coding: utf-8 -*-\n",
    "\"\"\"\n",
    "All-in-one analysis script for:\n",
    "Ambrosia pollen season dynamics and meteorological drivers in Zagreb (2020–2024)\n",
    "\n",
    "Outputs:\n",
    "- Black-and-white journal-style Figures 1–8\n",
    "- Table 1: Descriptive statistics\n",
    "- Table 2: Season timing and totals\n",
    "- Table 3: Start_DOY vs preseason thermal indices\n",
    "- Table 4: ZINB coefficients\n",
    "- Table 5: PCA summary\n",
    "\n",
    "Input file expected:\n",
    "    MasterExcel_CLEAN_ENG.csv\n",
    "\n",
    "Designed for Springer Aerobiologia (black-and-white friendly output).\n",
    "\"\"\"\n",
    "\n",
    "import os\n",
    "import warnings\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib.ticker import MaxNLocator\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.decomposition import PCA, FactorAnalysis\n",
    "import seaborn as sns\n",
    "import statsmodels.api as sm\n",
    "from statsmodels.discrete.count_model import NegativeBinomialP, ZeroInflatedNegativeBinomialP\n",
    "\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "INPUT_FILE = \"MasterExcel_CLEAN_ENG.csv\"\n",
    "OUTDIR = \"Aerobiologia_bw_outputs\"\n",
    "os.makedirs(OUTDIR, exist_ok=True)\n",
    "\n",
    "plt.rcParams.update({\n",
    "    \"figure.dpi\": 120,\n",
    "    \"savefig.dpi\": 600,\n",
    "    \"font.size\": 10,\n",
    "    \"axes.titlesize\": 11,\n",
    "    \"axes.labelsize\": 10,\n",
    "    \"xtick.labelsize\": 9,\n",
    "    \"ytick.labelsize\": 9,\n",
    "    \"legend.fontsize\": 9,\n",
    "    \"axes.linewidth\": 0.8,\n",
    "    \"lines.linewidth\": 1.2,\n",
    "    \"patch.linewidth\": 0.8,\n",
    "    \"font.family\": \"DejaVu Sans\",\n",
    "})\n",
    "\n",
    "BW = {\n",
    "    \"black\": \"black\",\n",
    "    \"dark\": \"0.25\",\n",
    "    \"mid\": \"0.50\",\n",
    "    \"light\": \"0.75\",\n",
    "    \"verylight\": \"0.90\"\n",
    "}\n",
    "\n",
    "def savefig(name):\n",
    "    plt.tight_layout()\n",
    "    plt.savefig(os.path.join(OUTDIR, name), bbox_inches=\"tight\")\n",
    "    plt.close()\n",
    "\n",
    "def exp_or_nan(x):\n",
    "    try:\n",
    "        return np.exp(x)\n",
    "    except Exception:\n",
    "        return np.nan\n",
    "\n",
    "def safe_p(p):\n",
    "    if pd.isna(p):\n",
    "        return np.nan\n",
    "    if p < 0.001:\n",
    "        return \"<0.001\"\n",
    "    return round(float(p), 3)\n",
    "\n",
    "def varimax(Phi, gamma=1.0, q=100, tol=1e-6):\n",
    "    p, k = Phi.shape\n",
    "    R = np.eye(k)\n",
    "    d = 0\n",
    "    for _ in range(q):\n",
    "        d_old = d\n",
    "        Lambda = np.dot(Phi, R)\n",
    "        u, s, vh = np.linalg.svd(\n",
    "            np.dot(\n",
    "                Phi.T,\n",
    "                np.asarray(Lambda) ** 3 -\n",
    "                (gamma / p) * np.dot(Lambda, np.diag(np.diag(Lambda.T @ Lambda)))\n",
    "            )\n",
    "        )\n",
    "        R = np.dot(u, vh)\n",
    "        d = np.sum(s)\n",
    "        if d_old != 0 and d / d_old < 1 + tol:\n",
    "            break\n",
    "    return np.dot(Phi, R), R\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 1) LOAD DATA\n",
    "# =========================================================\n",
    "df = pd.read_csv(INPUT_FILE)\n",
    "df.columns = df.columns.astype(str).str.strip()\n",
    "\n",
    "COLS_EN = {\n",
    "    \"Godina\": \"Year\",\n",
    "    \"Mjesec\": \"Month\",\n",
    "    \"Dan\": \"Day\",\n",
    "    \"Temp\": \"Temperature\",\n",
    "    \"Oborina\": \"Precipitation\",\n",
    "    \"Vlaga\": \"Relative humidity\",\n",
    "    \"Tlak\": \"Pressure\",\n",
    "    \"Brzina_vjetra\": \"Wind speed\",\n",
    "    \"Datum\": \"Date\",\n",
    "    \"Broj zrnaca Breza\": \"Birch pollen\",\n",
    "    \"Broj zrnaca Ambrozija\": \"Ragweed pollen\",\n",
    "    \"Broj zrnaca Trave\": \"Grass pollen\",\n",
    "}\n",
    "df = df.rename(columns=COLS_EN)\n",
    "\n",
    "required_cols = [\n",
    "    \"Date\", \"Year\", \"Month\", \"Temperature\", \"Precipitation\",\n",
    "    \"Relative humidity\", \"Wind speed\", \"Ragweed pollen\"\n",
    "]\n",
    "missing = [c for c in required_cols if c not in df.columns]\n",
    "if missing:\n",
    "    raise ValueError(f\"Missing required columns: {missing}\")\n",
    "\n",
    "df[\"Date\"] = pd.to_datetime(df[\"Date\"], errors=\"coerce\")\n",
    "if df[\"Date\"].isna().any():\n",
    "    raise ValueError(\"Date parsing produced NA values. Check the Date column.\")\n",
    "\n",
    "df = df[(df[\"Year\"] >= 2020) & (df[\"Year\"] <= 2024)].copy()\n",
    "df = df.sort_values(\"Date\").reset_index(drop=True)\n",
    "\n",
    "df[\"Month\"] = pd.to_numeric(df[\"Month\"], errors=\"coerce\")\n",
    "df.loc[df[\"Month\"].isna(), \"Month\"] = df.loc[df[\"Month\"].isna(), \"Date\"].dt.month\n",
    "\n",
    "df[\"log_ragweed\"] = np.log1p(df[\"Ragweed pollen\"])\n",
    "df[\"Season_group\"] = np.where(df[\"Month\"].isin([8, 9]), \"August–September\", \"Rest of year\")\n",
    "\n",
    "print(\"Period:\", int(df[\"Year\"].min()), \"-\", int(df[\"Year\"].max()))\n",
    "print(\"Rows:\", df.shape[0])\n",
    "print(\"Unique dates:\", df[\"Date\"].nunique())\n",
    "print(\"Date range:\", df[\"Date\"].min().date(), \"to\", df[\"Date\"].max().date())\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 2) TABLE 1 — DESCRIPTIVE STATISTICS\n",
    "# =========================================================\n",
    "table1 = pd.DataFrame([{\n",
    "    \"N_days\": int(df.shape[0]),\n",
    "    \"N_zero_days\": int((df[\"Ragweed pollen\"] == 0).sum()),\n",
    "    \"Mean\": round(df[\"Ragweed pollen\"].mean(), 2),\n",
    "    \"Median\": round(df[\"Ragweed pollen\"].median(), 2),\n",
    "    \"Std\": round(df[\"Ragweed pollen\"].std(), 2),\n",
    "    \"Min\": round(df[\"Ragweed pollen\"].min(), 2),\n",
    "    \"P25\": round(df[\"Ragweed pollen\"].quantile(0.25), 2),\n",
    "    \"P75\": round(df[\"Ragweed pollen\"].quantile(0.75), 2),\n",
    "    \"P95\": round(df[\"Ragweed pollen\"].quantile(0.95), 2),\n",
    "    \"Max\": round(df[\"Ragweed pollen\"].max(), 2),\n",
    "}])\n",
    "table1.to_csv(os.path.join(OUTDIR, \"Table1_Descriptive_statistics.csv\"), index=False)\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 3) FIGURE 1 — BOXPLOT (B/W)\n",
    "# =========================================================\n",
    "plt.figure(figsize=(6.4, 5.2))\n",
    "sns.boxplot(\n",
    "    data=df,\n",
    "    x=\"Season_group\",\n",
    "    y=\"Ragweed pollen\",\n",
    "    color=\"white\",\n",
    "    width=0.5,\n",
    "    showcaps=True,\n",
    "    boxprops=dict(edgecolor=\"black\", facecolor=\"white\"),\n",
    "    whiskerprops=dict(color=\"black\"),\n",
    "    capprops=dict(color=\"black\"),\n",
    "    medianprops=dict(color=\"black\", linewidth=1.2),\n",
    "    flierprops=dict(marker='o', markerfacecolor='white', markeredgecolor='black',\n",
    "                    markersize=3, linestyle='none'),\n",
    ")\n",
    "plt.yscale(\"log\")\n",
    "plt.xlabel(\"\")\n",
    "plt.ylabel(\"Ragweed pollen (grains m$^{-3}$, log scale)\")\n",
    "plt.title(\"Peak flowering period vs. rest of the year\")\n",
    "savefig(\"Figure1_Boxplot_peak_vs_rest_bw.tif\")\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 4) FIGURE 2 — HISTOGRAMS (B/W)\n",
    "# =========================================================\n",
    "fig, axes = plt.subplots(1, 2, figsize=(10, 4.3))\n",
    "\n",
    "axes[0].hist(df[\"Ragweed pollen\"].dropna(), bins=40, color=\"white\", edgecolor=\"black\")\n",
    "axes[0].set_title(\"(a) Daily values\")\n",
    "axes[0].set_xlabel(\"Ragweed pollen (grains m$^{-3}$)\")\n",
    "axes[0].set_ylabel(\"Frequency\")\n",
    "\n",
    "axes[1].hist(df[\"log_ragweed\"].dropna(), bins=40, color=BW[\"light\"], edgecolor=\"black\")\n",
    "axes[1].set_title(\"(b) log(1 + x)\")\n",
    "axes[1].set_xlabel(\"log(Ragweed pollen + 1)\")\n",
    "axes[1].set_ylabel(\"Frequency\")\n",
    "\n",
    "fig.suptitle(\"Histograms of ragweed pollen concentrations (2020–2024)\", y=1.02)\n",
    "plt.tight_layout()\n",
    "plt.savefig(os.path.join(OUTDIR, \"Figure2_Histograms_raw_log_bw.tif\"), bbox_inches=\"tight\", dpi=600)\n",
    "plt.close()\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 5) SEASON METRICS — TABLES 2 AND 3\n",
    "# =========================================================\n",
    "season_rows = []\n",
    "for year, g in df.groupby(\"Year\"):\n",
    "    g = g.sort_values(\"Date\").copy()\n",
    "    total = g[\"Ragweed pollen\"].sum()\n",
    "    if total <= 0:\n",
    "        continue\n",
    "\n",
    "    g[\"cum_sum\"] = g[\"Ragweed pollen\"].cumsum()\n",
    "    g[\"cum_pct\"] = g[\"cum_sum\"] / total * 100\n",
    "\n",
    "    start_row = g[g[\"cum_pct\"] >= 5].iloc[0]\n",
    "    end_row = g[g[\"cum_pct\"] >= 95].iloc[0]\n",
    "\n",
    "    start_date = start_row[\"Date\"]\n",
    "    end_date = end_row[\"Date\"]\n",
    "\n",
    "    g_prev = g[g[\"Date\"] < start_date].copy()\n",
    "    if not g_prev.empty:\n",
    "        g_prev[\"gdd10\"] = np.maximum(g_prev[\"Temperature\"] - 10, 0)\n",
    "        gdd10_prev = g_prev[\"gdd10\"].sum()\n",
    "    else:\n",
    "        gdd10_prev = np.nan\n",
    "\n",
    "    t30_window = df[(df[\"Date\"] >= start_date - pd.Timedelta(days=30)) & (df[\"Date\"] < start_date)]\n",
    "    t30_prev = t30_window[\"Temperature\"].mean()\n",
    "\n",
    "    season_rows.append({\n",
    "        \"Year\": int(year),\n",
    "        \"Start\": start_date.date().isoformat(),\n",
    "        \"End\": end_date.date().isoformat(),\n",
    "        \"Duration\": int((end_date - start_date).days),\n",
    "        \"Ambrosia pollen count/m3\": int(total),\n",
    "        \"Start_DOY\": int(start_date.dayofyear),\n",
    "        \"End_DOY\": int(end_date.dayofyear),\n",
    "        \"GDD10_prev\": round(gdd10_prev, 1),\n",
    "        \"MeanT30_prev\": round(t30_prev, 2)\n",
    "    })\n",
    "\n",
    "season_df = pd.DataFrame(season_rows).sort_values(\"Year\")\n",
    "\n",
    "table2 = season_df[[\"Year\", \"Start\", \"End\", \"Duration\", \"Ambrosia pollen count/m3\"]].copy()\n",
    "table2.to_csv(os.path.join(OUTDIR, \"Table2_Season_timing.csv\"), index=False)\n",
    "\n",
    "table3 = season_df[[\"Year\", \"Start_DOY\", \"GDD10_prev\", \"MeanT30_prev\"]].copy()\n",
    "table3.to_csv(os.path.join(OUTDIR, \"Table3_Start_DOY_vs_thermal_indices.csv\"), index=False)\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 6) FIGURE 3 — SEASON CHARACTERISTICS (B/W)\n",
    "# =========================================================\n",
    "fig, axes = plt.subplots(1, 2, figsize=(10.2, 4.4))\n",
    "\n",
    "axes[0].bar(season_df[\"Year\"].astype(str), season_df[\"Duration\"],\n",
    "            color=\"white\", edgecolor=\"black\", hatch=\"///\")\n",
    "axes[0].set_title(\"(a) Season duration\")\n",
    "axes[0].set_xlabel(\"Year\")\n",
    "axes[0].set_ylabel(\"Duration (days)\")\n",
    "\n",
    "axes[1].plot(season_df[\"Year\"], season_df[\"Start_DOY\"],\n",
    "             marker=\"o\", linestyle=\"-\", color=\"black\", label=\"Start\")\n",
    "axes[1].plot(season_df[\"Year\"], season_df[\"End_DOY\"],\n",
    "             marker=\"s\", linestyle=\"--\", color=BW[\"dark\"], label=\"End\")\n",
    "axes[1].set_title(\"(b) Start and end of season\")\n",
    "axes[1].set_xlabel(\"Year\")\n",
    "axes[1].set_ylabel(\"Day of year (DOY)\")\n",
    "axes[1].legend(frameon=False)\n",
    "\n",
    "fig.suptitle(\"Season characteristics of ragweed pollen (2020–2024)\", y=1.02)\n",
    "plt.tight_layout()\n",
    "plt.savefig(os.path.join(OUTDIR, \"Figure3_Season_duration_start_end_bw.tif\"), bbox_inches=\"tight\", dpi=600)\n",
    "plt.close()\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 7) FIGURE 4 — START_DOY VS THERMAL INDICES (B/W)\n",
    "# =========================================================\n",
    "fig, axes = plt.subplots(1, 2, figsize=(10.2, 4.4))\n",
    "\n",
    "axes[0].scatter(season_df[\"GDD10_prev\"], season_df[\"Start_DOY\"],\n",
    "                s=40, facecolors=\"white\", edgecolors=\"black\")\n",
    "for _, row in season_df.iterrows():\n",
    "    axes[0].text(row[\"GDD10_prev\"], row[\"Start_DOY\"], str(int(row[\"Year\"])),\n",
    "                 fontsize=8, ha=\"left\", va=\"bottom\")\n",
    "axes[0].set_xlabel(\"GDD10 before season onset\")\n",
    "axes[0].set_ylabel(\"Start of season (DOY)\")\n",
    "axes[0].set_title(\"(a) GDD10\")\n",
    "\n",
    "axes[1].scatter(season_df[\"MeanT30_prev\"], season_df[\"Start_DOY\"],\n",
    "                s=40, facecolors=\"white\", edgecolors=\"black\")\n",
    "for _, row in season_df.iterrows():\n",
    "    axes[1].text(row[\"MeanT30_prev\"], row[\"Start_DOY\"], str(int(row[\"Year\"])),\n",
    "                 fontsize=8, ha=\"left\", va=\"bottom\")\n",
    "axes[1].set_xlabel(\"Mean temperature in previous 30 days (°C)\")\n",
    "axes[1].set_ylabel(\"Start of season (DOY)\")\n",
    "axes[1].set_title(\"(b) T-30\")\n",
    "\n",
    "fig.suptitle(\"Start-of-season DOY versus preseason thermal indices\", y=1.02)\n",
    "plt.tight_layout()\n",
    "plt.savefig(os.path.join(OUTDIR, \"Figure4_Start_DOY_vs_thermal_indices_bw.tif\"), bbox_inches=\"tight\", dpi=600)\n",
    "plt.close()\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 8) FIGURE 5 — SPEARMAN CORRELATION MATRIX (GRAYSCALE)\n",
    "# =========================================================\n",
    "corr_vars = [\"Ragweed pollen\", \"Temperature\", \"Relative humidity\", \"Precipitation\", \"Wind speed\"]\n",
    "corr = df[corr_vars].corr(method=\"spearman\")\n",
    "\n",
    "labels = {\n",
    "    \"Ragweed pollen\": \"Ragweed pollen\",\n",
    "    \"Temperature\": \"Temperature\",\n",
    "    \"Relative humidity\": \"Humidity\",\n",
    "    \"Precipitation\": \"Precipitation\",\n",
    "    \"Wind speed\": \"Wind speed\",\n",
    "}\n",
    "corr_plot = corr.rename(index=labels, columns=labels)\n",
    "\n",
    "plt.figure(figsize=(6.8, 5.8))\n",
    "sns.heatmap(\n",
    "    corr_plot,\n",
    "    annot=True,\n",
    "    fmt=\".2f\",\n",
    "    cmap=\"Greys\",\n",
    "    vmin=-1,\n",
    "    vmax=1,\n",
    "    square=True,\n",
    "    linewidths=0.6,\n",
    "    linecolor=\"white\",\n",
    "    cbar_kws={\"label\": \"Spearman correlation\", \"shrink\": 0.9},\n",
    "    annot_kws={\"size\": 9, \"color\": \"black\"}\n",
    ")\n",
    "plt.title(\"Spearman correlation matrix\", pad=10)\n",
    "plt.xticks(rotation=30, ha=\"right\")\n",
    "plt.yticks(rotation=0)\n",
    "savefig(\"Figure5_Spearman_correlation_bw.tif\")\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 9) NB + ZINB MODELS\n",
    "# =========================================================\n",
    "df_model = df[[\n",
    "    \"Date\", \"Month\", \"Ragweed pollen\", \"Temperature\",\n",
    "    \"Relative humidity\", \"Precipitation\", \"Wind speed\"\n",
    "]].copy()\n",
    "\n",
    "doy = df_model[\"Date\"].dt.dayofyear\n",
    "df_model[\"sin_doy\"] = np.sin(2 * np.pi * doy / 365)\n",
    "df_model[\"cos_doy\"] = np.cos(2 * np.pi * doy / 365)\n",
    "df_model[\"Season\"] = df_model[\"Date\"].dt.month.isin([8, 9]).astype(int)\n",
    "df_model[\"pollen_lag1\"] = df_model[\"Ragweed pollen\"].shift(1)\n",
    "df_model = df_model.dropna().copy()\n",
    "\n",
    "for src, dst in [\n",
    "    (\"Temperature\", \"Temperature_z\"),\n",
    "    (\"Relative humidity\", \"Humidity_z\"),\n",
    "    (\"Precipitation\", \"Precipitation_z\"),\n",
    "    (\"pollen_lag1\", \"Lag1_z\"),\n",
    "]:\n",
    "    df_model[dst] = (df_model[src] - df_model[src].mean()) / df_model[src].std()\n",
    "\n",
    "# Full-year NB\n",
    "vars_nb_full = [\"Temperature_z\", \"Humidity_z\", \"Precipitation_z\", \"Wind speed\",\n",
    "                \"sin_doy\", \"cos_doy\", \"Season\"]\n",
    "X_nb_full = sm.add_constant(df_model[vars_nb_full])\n",
    "y_nb_full = df_model[\"Ragweed pollen\"].astype(int)\n",
    "nb_full = NegativeBinomialP(endog=y_nb_full, exog=X_nb_full).fit(disp=False, maxiter=200)\n",
    "\n",
    "# Season-restricted NB\n",
    "df_season_nb = df_model[df_model[\"Season\"] == 1].copy()\n",
    "vars_nb_season = [\"Temperature_z\", \"Humidity_z\", \"Precipitation_z\", \"Wind speed\"]\n",
    "X_nb_season = sm.add_constant(df_season_nb[vars_nb_season])\n",
    "y_nb_season = df_season_nb[\"Ragweed pollen\"].astype(int)\n",
    "nb_season = NegativeBinomialP(endog=y_nb_season, exog=X_nb_season).fit(disp=False, maxiter=200)\n",
    "\n",
    "# Final ZINB\n",
    "vars_count = [\"Temperature_z\", \"Lag1_z\", \"sin_doy\", \"cos_doy\", \"Season\"]\n",
    "vars_infl = [\"Temperature_z\", \"Lag1_z\"]\n",
    "\n",
    "X = sm.add_constant(df_model[vars_count])\n",
    "Z = sm.add_constant(df_model[vars_infl])\n",
    "y = df_model[\"Ragweed pollen\"].astype(int)\n",
    "\n",
    "zinb = ZeroInflatedNegativeBinomialP(\n",
    "    endog=y,\n",
    "    exog=X,\n",
    "    exog_infl=Z,\n",
    "    inflation=\"logit\"\n",
    ").fit(maxiter=200, method=\"lbfgs\", disp=False)\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 10) TABLE 4 — ZINB COEFFICIENTS\n",
    "# =========================================================\n",
    "params = zinb.params\n",
    "bse = zinb.bse\n",
    "pvals = zinb.pvalues\n",
    "conf = zinb.conf_int()\n",
    "conf.columns = [\"CI_low_coef\", \"CI_high_coef\"]\n",
    "\n",
    "rows = []\n",
    "for term in params.index:\n",
    "    coef = params[term]\n",
    "    se = bse[term]\n",
    "    p = pvals[term]\n",
    "    low = conf.loc[term, \"CI_low_coef\"]\n",
    "    high = conf.loc[term, \"CI_high_coef\"]\n",
    "\n",
    "    if term == \"alpha\":\n",
    "        rows.append({\n",
    "            \"Predictor\": \"α (dispersion)\",\n",
    "            \"Model component\": \"Overdispersion parameter\",\n",
    "            \"Coefficient\": round(coef, 3),\n",
    "            \"SE\": round(se, 3),\n",
    "            \"IRR/OR\": \"–\",\n",
    "            \"95% CI\": f\"{low:.3f} – {high:.3f}\",\n",
    "            \"p-value\": safe_p(p)\n",
    "        })\n",
    "        continue\n",
    "\n",
    "    if term.startswith(\"inflate_\"):\n",
    "        component = \"Zero inflation (logit)\"\n",
    "        predictor = term.replace(\"inflate_\", \"\")\n",
    "        predictor = {\n",
    "            \"const\": \"Intercept\",\n",
    "            \"Temperature_z\": \"Temperature (z)\",\n",
    "            \"Lag1_z\": \"Lagged pollen (t−1)\",\n",
    "        }.get(predictor, predictor)\n",
    "\n",
    "        ratio = exp_or_nan(coef)\n",
    "        low_r = exp_or_nan(low)\n",
    "        high_r = exp_or_nan(high)\n",
    "        ci_txt = f\"{low_r:.3f} – {high_r:.3f}\"\n",
    "\n",
    "    else:\n",
    "        component = \"Count model (NB)\"\n",
    "        predictor = {\n",
    "            \"const\": \"Intercept\",\n",
    "            \"Temperature_z\": \"Temperature (z)\",\n",
    "            \"Lag1_z\": \"Lagged pollen (t−1)\",\n",
    "            \"sin_doy\": \"sin(DOY)\",\n",
    "            \"cos_doy\": \"cos(DOY)\",\n",
    "            \"Season\": \"Season (Aug–Sep)\"\n",
    "        }.get(term, term)\n",
    "\n",
    "        if predictor in [\"Intercept\", \"sin(DOY)\"]:\n",
    "            ratio = np.nan\n",
    "            ci_txt = f\"{low:.3f} – {high:.3f}\"\n",
    "        else:\n",
    "            ratio = exp_or_nan(coef)\n",
    "            low_r = exp_or_nan(low)\n",
    "            high_r = exp_or_nan(high)\n",
    "            ci_txt = f\"{low_r:.3f} – {high_r:.3f}\"\n",
    "\n",
    "    ratio_txt = \"–\" if pd.isna(ratio) else f\"{ratio:.3f}\"\n",
    "    rows.append({\n",
    "        \"Predictor\": predictor,\n",
    "        \"Model component\": component,\n",
    "        \"Coefficient\": round(coef, 3),\n",
    "        \"SE\": round(se, 3),\n",
    "        \"IRR/OR\": ratio_txt,\n",
    "        \"95% CI\": ci_txt,\n",
    "        \"p-value\": safe_p(p)\n",
    "    })\n",
    "\n",
    "table4 = pd.DataFrame(rows)\n",
    "table4.to_csv(os.path.join(OUTDIR, \"Table4_ZINB_results.csv\"), index=False)\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 11) AUG–SEP PCA / EFA\n",
    "# =========================================================\n",
    "df_mult = df[df[\"Month\"].isin([8, 9])].copy()\n",
    "vars_mult = [\"Temperature\", \"Relative humidity\", \"Precipitation\", \"Wind speed\", \"log_ragweed\"]\n",
    "X_mult = df_mult[vars_mult].dropna().copy()\n",
    "X_scaled = StandardScaler().fit_transform(X_mult)\n",
    "\n",
    "plot_names = {\n",
    "    \"Temperature\": \"Temperature\",\n",
    "    \"Relative humidity\": \"Humidity\",\n",
    "    \"Precipitation\": \"Precipitation\",\n",
    "    \"Wind speed\": \"Wind speed\",\n",
    "    \"log_ragweed\": \"log(Ragweed pollen + 1)\"\n",
    "}\n",
    "\n",
    "# Table 5\n",
    "pca_full = PCA()\n",
    "pca_full.fit(X_scaled)\n",
    "eig = pca_full.explained_variance_\n",
    "var_pct = pca_full.explained_variance_ratio_ * 100\n",
    "cum_pct = np.cumsum(var_pct)\n",
    "\n",
    "table5 = pd.DataFrame({\n",
    "    \"PC\": [f\"PC{i}\" for i in range(1, len(eig)+1)],\n",
    "    \"Eigenvalue\": np.round(eig, 2),\n",
    "    \"%Var\": np.round(var_pct, 1),\n",
    "    \"Cum%Var\": np.round(cum_pct, 1)\n",
    "})\n",
    "table5.to_csv(os.path.join(OUTDIR, \"Table5_PCA_summary.csv\"), index=False)\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 12) FIGURE 6 — SCREE PLOT\n",
    "# =========================================================\n",
    "components = np.arange(1, len(eig) + 1)\n",
    "\n",
    "plt.figure(figsize=(6.4, 5.0))\n",
    "plt.plot(components, eig, marker=\"o\", color=\"black\", linestyle=\"-\")\n",
    "plt.axhline(1, linestyle=\"--\", color=BW[\"dark\"], linewidth=1.0)\n",
    "plt.xlabel(\"Principal component\")\n",
    "plt.ylabel(\"Eigenvalue\")\n",
    "plt.title(\"Scree plot with Kaiser criterion\")\n",
    "plt.xticks(components)\n",
    "plt.gca().xaxis.set_major_locator(MaxNLocator(integer=True))\n",
    "savefig(\"Figure6_Scree_plot_bw.tif\")\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 13) FIGURE 7 — PCA BIPLOT\n",
    "# =========================================================\n",
    "pca = PCA(n_components=2)\n",
    "scores = pca.fit_transform(X_scaled)\n",
    "raw_loadings = pca.components_.T\n",
    "\n",
    "pc1_var = pca.explained_variance_ratio_[0] * 100\n",
    "pc2_var = pca.explained_variance_ratio_[1] * 100\n",
    "\n",
    "temp_idx = vars_mult.index(\"Temperature\")\n",
    "if raw_loadings[temp_idx, 0] < 0:\n",
    "    scores[:, 0] *= -1\n",
    "    raw_loadings[:, 0] *= -1\n",
    "\n",
    "plt.figure(figsize=(7.0, 5.6))\n",
    "plt.scatter(scores[:, 0], scores[:, 1], s=20, alpha=0.35,\n",
    "            facecolors=\"white\", edgecolors=\"black\")\n",
    "plt.axhline(0, linewidth=0.8, color=BW[\"dark\"])\n",
    "plt.axvline(0, linewidth=0.8, color=BW[\"dark\"])\n",
    "\n",
    "arrow_scale = 2.8\n",
    "for i, var in enumerate(vars_mult):\n",
    "    x_arrow = raw_loadings[i, 0] * arrow_scale\n",
    "    y_arrow = raw_loadings[i, 1] * arrow_scale\n",
    "    plt.arrow(\n",
    "        0, 0, x_arrow, y_arrow,\n",
    "        color=\"black\", linewidth=1.0,\n",
    "        head_width=0.06, head_length=0.09,\n",
    "        length_includes_head=True\n",
    "    )\n",
    "    x_text = x_arrow * 1.10\n",
    "    y_text = y_arrow * 1.10\n",
    "    ha = \"left\" if x_text >= 0 else \"right\"\n",
    "    va = \"bottom\" if y_text >= 0 else \"top\"\n",
    "    plt.text(x_text, y_text, plot_names[var], ha=ha, va=va, fontsize=9)\n",
    "\n",
    "plt.xlabel(f\"PC1 ({pc1_var:.1f}% variance)\")\n",
    "plt.ylabel(f\"PC2 ({pc2_var:.1f}% variance)\")\n",
    "plt.title(\"PCA biplot (PC1 vs PC2) – August–September\")\n",
    "savefig(\"Figure7_PCA_biplot_bw.tif\")\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 14) FIGURE 8 — VARIMAX\n",
    "# =========================================================\n",
    "fa = FactorAnalysis(n_components=2, random_state=42)\n",
    "fa.fit(X_scaled)\n",
    "\n",
    "raw_loadings_fa = fa.components_.T\n",
    "rotated_loadings, _ = varimax(raw_loadings_fa)\n",
    "\n",
    "rot_df = pd.DataFrame(\n",
    "    rotated_loadings,\n",
    "    index=[plot_names[v] for v in vars_mult],\n",
    "    columns=[\"Factor 1\", \"Factor 2\"]\n",
    ")\n",
    "rot_df.to_csv(os.path.join(OUTDIR, \"Varimax_rotated_loadings.csv\"), index=True)\n",
    "\n",
    "plt.figure(figsize=(6.4, 5.6))\n",
    "plt.scatter(rot_df[\"Factor 1\"], rot_df[\"Factor 2\"], s=30,\n",
    "            facecolors=\"white\", edgecolors=\"black\")\n",
    "plt.axhline(0, linewidth=0.8, color=BW[\"dark\"])\n",
    "plt.axvline(0, linewidth=0.8, color=BW[\"dark\"])\n",
    "\n",
    "for label, row in rot_df.iterrows():\n",
    "    x, y = row[\"Factor 1\"], row[\"Factor 2\"]\n",
    "    ha = \"left\" if x >= 0 else \"right\"\n",
    "    va = \"bottom\" if y >= 0 else \"top\"\n",
    "    xt, yt = x * 1.06, y * 1.06\n",
    "    if label == \"Wind speed\":\n",
    "        yt -= 0.04\n",
    "    plt.text(xt, yt, label, ha=ha, va=va, fontsize=9)\n",
    "\n",
    "plt.xlabel(\"Factor 1\")\n",
    "plt.ylabel(\"Factor 2\")\n",
    "plt.title(\"Varimax rotation (two-factor solution)\")\n",
    "savefig(\"Figure8_Varimax_rotation_bw.tif\")\n",
    "\n",
    "\n",
    "# =========================================================\n",
    "# 15) SAVE MODEL SUMMARIES\n",
    "# =========================================================\n",
    "with open(os.path.join(OUTDIR, \"NB_full_summary.txt\"), \"w\", encoding=\"utf-8\") as f:\n",
    "    f.write(nb_full.summary().as_text())\n",
    "\n",
    "with open(os.path.join(OUTDIR, \"NB_season_summary.txt\"), \"w\", encoding=\"utf-8\") as f:\n",
    "    f.write(nb_season.summary().as_text())\n",
    "\n",
    "with open(os.path.join(OUTDIR, \"ZINB_summary.txt\"), \"w\", encoding=\"utf-8\") as f:\n",
    "    f.write(zinb.summary().as_text())\n",
    "\n",
    "print(\"\\\\nSaved outputs:\")\n",
    "for fn in sorted(os.listdir(OUTDIR)):\n",
    "    print(\" -\", fn)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "37daa5b3-3d3b-43fa-ac21-3d3964315ec7",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
