|
16 | 16 | "- **regularizedvi_nb** (tutorial): NB mode, prior on $\\sqrt{\\theta}$ (pushes $\\theta$ small)\n", |
17 | 17 | "\n", |
18 | 18 | "The Exponential(rate=3) prior has:\n", |
19 | | - "- GammaPoisson direction: $E[1/\\sqrt{\\theta}] = 1/3$ → $\\theta$ centered ~9\n", |
20 | | - "- NB direction: $E[\\sqrt{\\theta}] = 1/3$ → $\\theta$ centered ~1/9\n", |
| 19 | + "- GammaPoisson direction: $E[1/\\sqrt{\\theta}] = 1/3$ \u2192 $\\theta$ centered ~9\n", |
| 20 | + "- NB direction: $E[\\sqrt{\\theta}] = 1/3$ \u2192 $\\theta$ centered ~1/9\n", |
21 | 21 | "\n", |
22 | 22 | "Key question: do the unconstrained scVI models learn theta values that are very different from both 1/9 and 9?" |
23 | 23 | ] |
|
64 | 64 | " (\"scvi_custom\", \"scvi_custom\", \"NB, 512/128, custom train, no prior\"),\n", |
65 | 65 | " (\"scvi_custom_default_train\", \"scvi_custom_default_train\", \"NB, 512/128, default train, no prior\"),\n", |
66 | 66 | " (\"scvi_custom_zinb\", \"scvi_custom_zinb\", \"ZINB, 512/128, custom train, no prior\"),\n", |
67 | | - " (\"regularizedvi_gamma_poisson\", \"regularizedvi_gamma_poisson\", \"GammaPoisson, 512/128, prior→Poisson\"),\n", |
68 | | - " (\"regularizedvi_nb\", \"regularizedvi_nb\", \"NB, 512/128, prior→overdispersed\"),\n", |
69 | | - " (\"regularizedvi_gp_small\", \"regularizedvi_gamma_poisson_small\", \"GammaPoisson, 128/10, prior→Poisson\"),\n", |
70 | | - " (\"regularizedvi_gp_medium\", \"regularizedvi_gamma_poisson_medium\", \"GammaPoisson, 256/30, prior→Poisson\"),\n", |
| 67 | + " (\"regularizedvi_gamma_poisson\", \"regularizedvi_gamma_poisson\", \"GammaPoisson, 512/128, prior\u2192Poisson\"),\n", |
| 68 | + " (\"regularizedvi_nb\", \"regularizedvi_nb\", \"NB, 512/128, prior\u2192overdispersed\"),\n", |
| 69 | + " (\"regularizedvi_gp_small\", \"regularizedvi_gamma_poisson_small\", \"GammaPoisson, 128/10, prior\u2192Poisson\"),\n", |
| 70 | + " (\"regularizedvi_gp_medium\", \"regularizedvi_gamma_poisson_medium\", \"GammaPoisson, 256/30, prior\u2192Poisson\"),\n", |
71 | 71 | " (\n", |
72 | 72 | " \"regularizedvi_gp_small_default\",\n", |
73 | 73 | " \"regularizedvi_gamma_poisson_small_default_train\",\n", |
74 | | - " \"GammaPoisson, 128/10, default train, prior→Poisson\",\n", |
| 74 | + " \"GammaPoisson, 128/10, default train, prior\u2192Poisson\",\n", |
75 | 75 | " ),\n", |
76 | 76 | "]\n", |
77 | 77 | "\n", |
|
112 | 112 | "id": "hist-theta", |
113 | 113 | "metadata": {}, |
114 | 114 | "outputs": [], |
115 | | - "source": "fig, axes = plt.subplots(1, len(models), figsize=(4 * len(models), 4), sharey=True)\nif len(models) == 1:\n axes = [axes]\n\nbins = np.linspace(-3, 3, 80) # log10 scale: 0.001 to 1000\n\nfor ax, (name, data) in zip(axes, models.items(), strict=False):\n log_theta = np.log10(data[\"theta\"])\n ax.hist(log_theta, bins=bins, alpha=0.7, edgecolor=\"black\", linewidth=0.3)\n ax.axvline(np.log10(1 / 9), color=\"red\", linestyle=\"--\", alpha=0.7, label=\"θ=1/9 (NB prior)\")\n ax.axvline(np.log10(9), color=\"blue\", linestyle=\"--\", alpha=0.7, label=\"θ=9 (GP prior)\")\n ax.axvline(\n np.log10(np.median(data[\"theta\"])),\n color=\"green\",\n linestyle=\"-\",\n alpha=0.9,\n label=f\"median={np.median(data['theta']):.1f}\",\n )\n ax.set_xlabel(\"log₁₀(θ)\")\n ax.set_title(name, fontsize=9)\n ax.legend(fontsize=7)\n\naxes[0].set_ylabel(\"Gene count\")\nfig.suptitle(\"Per-gene inverse dispersion θ = exp(px_r)\", fontsize=12)\nplt.tight_layout()\nplt.show()" |
| 115 | + "source": "fig, axes = plt.subplots(1, len(models), figsize=(4 * len(models), 4), sharey=True)\nif len(models) == 1:\n axes = [axes]\n\nbins = np.linspace(-3, 3, 80) # log10 scale: 0.001 to 1000\n\nfor ax, (name, data) in zip(axes, models.items(), strict=False):\n log_theta = np.log10(data[\"theta\"])\n ax.hist(log_theta, bins=bins, alpha=0.7, edgecolor=\"black\", linewidth=0.3)\n ax.axvline(np.log10(1 / 9), color=\"red\", linestyle=\"--\", alpha=0.7, label=\"\u03b8=1/9 (NB prior)\")\n ax.axvline(np.log10(9), color=\"blue\", linestyle=\"--\", alpha=0.7, label=\"\u03b8=9 (GP prior)\")\n ax.axvline(\n np.log10(np.median(data[\"theta\"])),\n color=\"green\",\n linestyle=\"-\",\n alpha=0.9,\n label=f\"median={np.median(data['theta']):.1f}\",\n )\n ax.set_xlabel(\"log\u2081\u2080(\u03b8)\")\n ax.set_title(name, fontsize=9)\n ax.legend(fontsize=7)\n\naxes[0].set_ylabel(\"Gene count\")\nfig.suptitle(\"Per-gene inverse dispersion \u03b8 = exp(px_r)\", fontsize=12)\nplt.tight_layout()\nplt.show()" |
| 116 | + }, |
| 117 | + { |
| 118 | + "cell_type": "markdown", |
| 119 | + "id": "3d6kr3mrx8p", |
| 120 | + "source": "## 2b. Overlaid ECDF \u2014 all models on same axes\n\nThe empirical CDF of $\\log_{10}(\\theta)$ reveals the full shape of each distribution and makes tail differences immediately visible.", |
| 121 | + "metadata": {} |
| 122 | + }, |
| 123 | + { |
| 124 | + "cell_type": "code", |
| 125 | + "id": "bbavnahmdh7", |
| 126 | + "source": "fig, ax = plt.subplots(figsize=(10, 6))\n\nfor name, data in models.items():\n log_theta = np.sort(np.log10(data[\"theta\"]))\n ecdf = np.arange(1, len(log_theta) + 1) / len(log_theta)\n ax.plot(log_theta, ecdf, label=f\"{name} ({data['desc']})\", linewidth=1.5)\n\nax.axvline(np.log10(1 / 9), color=\"red\", linestyle=\"--\", alpha=0.5, label=\"\u03b8=1/9\")\nax.axvline(np.log10(9), color=\"blue\", linestyle=\"--\", alpha=0.5, label=\"\u03b8=9\")\nax.set_xlabel(\"log\u2081\u2080(\u03b8)\")\nax.set_ylabel(\"Cumulative fraction of genes\")\nax.set_title(\"ECDF of per-gene \u03b8 across all models\")\nax.legend(fontsize=7, loc=\"upper left\")\nax.set_xlim(-3, 3)\nax.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.show()", |
| 127 | + "metadata": {}, |
| 128 | + "execution_count": null, |
| 129 | + "outputs": [] |
| 130 | + }, |
| 131 | + { |
| 132 | + "cell_type": "markdown", |
| 133 | + "id": "68q53gvczvm", |
| 134 | + "source": "## 2c. Quantile table and extreme-value fractions\n\nPercentiles reveal how spread the distribution is. Extreme-value fractions show what fraction of genes have very low \u03b8 (extreme overdispersion) or very high \u03b8 (near Poisson).", |
| 135 | + "metadata": {} |
| 136 | + }, |
| 137 | + { |
| 138 | + "cell_type": "code", |
| 139 | + "id": "f8yks65q64w", |
| 140 | + "source": "quantiles = [1, 5, 25, 50, 75, 95, 99]\nheader = f\"{'Model':<35}\" + \"\".join(f\"{'p' + str(q):>8}\" for q in quantiles)\nprint(\"=== Theta quantiles ===\")\nprint(header)\nprint(\"-\" * len(header))\nfor name, data in models.items():\n vals = \"\".join(f\"{np.percentile(data['theta'], q):>8.2f}\" for q in quantiles)\n print(f\"{name:<35}{vals}\")\n\nprint()\n\n# Extreme-value fractions\nthresholds = [0.01, 0.1, 1, 10, 100, 1000]\nheader2 = f\"{'Model':<35}\" + \"\".join(f\"{'<' + str(t):>8}\" for t in thresholds)\nprint(\"=== Fraction of genes with \u03b8 below threshold ===\")\nprint(header2)\nprint(\"-\" * len(header2))\nfor name, data in models.items():\n theta = data[\"theta\"]\n fracs = \"\".join(f\"{(theta < t).mean():>8.3f}\" for t in thresholds)\n print(f\"{name:<35}{fracs}\")\n\nprint()\n\n# IQR and range as spread measure\nprint(\"=== Distribution spread (log10 scale) ===\")\nprint(f\"{'Model':<35}{'IQR':>8}{'90% range':>12}{'98% range':>12}{'full range':>12}\")\nprint(\"-\" * 79)\nfor name, data in models.items():\n lt = np.log10(data[\"theta\"])\n iqr = np.percentile(lt, 75) - np.percentile(lt, 25)\n r90 = np.percentile(lt, 95) - np.percentile(lt, 5)\n r98 = np.percentile(lt, 99) - np.percentile(lt, 1)\n full = lt.max() - lt.min()\n print(f\"{name:<35}{iqr:>8.2f}{r90:>12.2f}{r98:>12.2f}{full:>12.2f}\")", |
| 141 | + "metadata": {}, |
| 142 | + "execution_count": null, |
| 143 | + "outputs": [] |
| 144 | + }, |
| 145 | + { |
| 146 | + "cell_type": "markdown", |
| 147 | + "id": "vzxfo0slei", |
| 148 | + "source": "## 2d. Overlaid histograms (density)\n\nSame data as Section 2 but overlaid on a single plot for direct visual comparison of distribution shapes.", |
| 149 | + "metadata": {} |
| 150 | + }, |
| 151 | + { |
| 152 | + "cell_type": "code", |
| 153 | + "id": "e2rwc55svjq", |
| 154 | + "source": "fig, ax = plt.subplots(figsize=(10, 5))\nbins = np.linspace(-3, 3, 80)\n\nfor name, data in models.items():\n log_theta = np.log10(data[\"theta\"])\n ax.hist(log_theta, bins=bins, alpha=0.3, density=True, label=name)\n\nax.axvline(np.log10(1 / 9), color=\"red\", linestyle=\"--\", alpha=0.7, label=\"\u03b8=1/9\")\nax.axvline(np.log10(9), color=\"blue\", linestyle=\"--\", alpha=0.7, label=\"\u03b8=9\")\nax.set_xlabel(\"log\u2081\u2080(\u03b8)\")\nax.set_ylabel(\"Density\")\nax.set_title(\"Overlaid density of per-gene \u03b8\")\nax.legend(fontsize=7)\nplt.tight_layout()\nplt.show()", |
| 155 | + "metadata": {}, |
| 156 | + "execution_count": null, |
| 157 | + "outputs": [] |
116 | 158 | }, |
117 | 159 | { |
118 | 160 | "cell_type": "markdown", |
|
130 | 172 | "id": "scatter-theta", |
131 | 173 | "metadata": {}, |
132 | 174 | "outputs": [], |
133 | | - "source": "# Compare each model against scvi_default as baseline (if available)\nbaseline = \"scvi_default\"\nif baseline in models:\n others = [k for k in models if k != baseline]\n n = len(others)\n fig, axes = plt.subplots(1, n, figsize=(4 * n, 4))\n if n == 1:\n axes = [axes]\n\n for ax, other_name in zip(axes, others, strict=False):\n x = np.log10(models[baseline][\"theta\"])\n y = np.log10(models[other_name][\"theta\"])\n # Ensure same number of genes\n n_genes = min(len(x), len(y))\n ax.hist2d(x[:n_genes], y[:n_genes], bins=80, cmap=\"viridis\", range=[[-3, 3], [-3, 3]])\n ax.plot([-3, 3], [-3, 3], \"r--\", alpha=0.5)\n ax.set_xlabel(f\"log₁₀(θ) — {baseline}\")\n ax.set_ylabel(f\"log₁₀(θ) — {other_name}\")\n ax.set_title(other_name, fontsize=9)\n\n fig.suptitle(f\"Per-gene θ: each model vs {baseline}\", fontsize=12)\n plt.tight_layout()\n plt.show()\nelse:\n print(f\"Baseline model '{baseline}' not found\")" |
| 175 | + "source": "# Compare each model against scvi_default as baseline (if available)\nbaseline = \"scvi_default\"\nif baseline in models:\n others = [k for k in models if k != baseline]\n n = len(others)\n fig, axes = plt.subplots(1, n, figsize=(4 * n, 4))\n if n == 1:\n axes = [axes]\n\n for ax, other_name in zip(axes, others, strict=False):\n x = np.log10(models[baseline][\"theta\"])\n y = np.log10(models[other_name][\"theta\"])\n # Ensure same number of genes\n n_genes = min(len(x), len(y))\n ax.hist2d(x[:n_genes], y[:n_genes], bins=80, cmap=\"viridis\", range=[[-3, 3], [-3, 3]])\n ax.plot([-3, 3], [-3, 3], \"r--\", alpha=0.5)\n ax.set_xlabel(f\"log\u2081\u2080(\u03b8) \u2014 {baseline}\")\n ax.set_ylabel(f\"log\u2081\u2080(\u03b8) \u2014 {other_name}\")\n ax.set_title(other_name, fontsize=9)\n\n fig.suptitle(f\"Per-gene \u03b8: each model vs {baseline}\", fontsize=12)\n plt.tight_layout()\n plt.show()\nelse:\n print(f\"Baseline model '{baseline}' not found\")" |
134 | 176 | }, |
135 | 177 | { |
136 | 178 | "cell_type": "markdown", |
|
151 | 193 | "source": [ |
152 | 194 | "mu_typical = 5.0 # typical gene expression\n", |
153 | 195 | "\n", |
154 | | - "print(f\"Variance = μ + μ²/θ = {mu_typical} + {mu_typical**2}/θ\")\n", |
155 | | - "print(f\"{'Model':<35} {'Median θ':>10} {'Var(x)':>10} {'Var/μ':>10} {'Poisson Var':>12}\")\n", |
| 196 | + "print(f\"Variance = \u03bc + \u03bc\u00b2/\u03b8 = {mu_typical} + {mu_typical**2}/\u03b8\")\n", |
| 197 | + "print(f\"{'Model':<35} {'Median \u03b8':>10} {'Var(x)':>10} {'Var/\u03bc':>10} {'Poisson Var':>12}\")\n", |
156 | 198 | "print(\"-\" * 80)\n", |
157 | 199 | "for name, data in models.items():\n", |
158 | 200 | " med_theta = np.median(data[\"theta\"])\n", |
|
185 | 227 | " print(f\" CV across batches: median={np.median(cv):.3f}, mean={cv.mean():.3f}, max={cv.max():.3f}\")\n", |
186 | 228 | "\n", |
187 | 229 | " plt.hist(cv, bins=50, alpha=0.7, edgecolor=\"black\", linewidth=0.3)\n", |
188 | | - " plt.xlabel(\"CV of θ across batches\")\n", |
| 230 | + " plt.xlabel(\"CV of \u03b8 across batches\")\n", |
189 | 231 | " plt.ylabel(\"Gene count\")\n", |
190 | | - " plt.title(f\"{name}: per-gene variation of θ across {theta_full.shape[1]} batches\")\n", |
| 232 | + " plt.title(f\"{name}: per-gene variation of \u03b8 across {theta_full.shape[1]} batches\")\n", |
191 | 233 | " plt.show()" |
192 | 234 | ] |
193 | 235 | }, |
|
198 | 240 | "source": [ |
199 | 241 | "## Summary\n", |
200 | 242 | "\n", |
201 | | - "| Model | Prior | Median θ | Interpretation |\n", |
| 243 | + "| Model | Prior | Median \u03b8 | Interpretation |\n", |
202 | 244 | "|-------|-------|----------|----------------|\n", |
203 | 245 | "| scvi_default | None | ~1.3 | Moderate overdispersion |\n", |
204 | 246 | "| scvi_custom | None | ~2.4 | Moderate overdispersion |\n", |
205 | | - "| regularizedvi_nb | Exp on √θ (→small) | ? | θ→1/9 expected |\n", |
206 | | - "| regularizedvi_gamma_poisson | Exp on 1/√θ (→large) | ~53 | Near Poisson |\n", |
| 247 | + "| regularizedvi_nb | Exp on \u221a\u03b8 (\u2192small) | ? | \u03b8\u21921/9 expected |\n", |
| 248 | + "| regularizedvi_gamma_poisson | Exp on 1/\u221a\u03b8 (\u2192large) | ~53 | Near Poisson |\n", |
207 | 249 | "\n", |
208 | | - "Both prior directions constrain θ via √ transform, but the unconstrained models land at θ~1-2, which is far from both 1/9 and 9. This suggests the prior's main effect is constraining θ to a definite range rather than the specific direction mattering." |
| 250 | + "Both prior directions constrain \u03b8 via \u221a transform, but the unconstrained models land at \u03b8~1-2, which is far from both 1/9 and 9. This suggests the prior's main effect is constraining \u03b8 to a definite range rather than the specific direction mattering." |
209 | 251 | ] |
210 | 252 | } |
211 | 253 | ], |
|
0 commit comments