scPTR / analyses /deep /07_reconstruction.py
bryan7264's picture
Add files using upload-large-folder tool
4e2940e verified
Raw
History Blame Contribute Delete
4.59 kB
#!/usr/bin/env python
"""Reconstruction quality: how well does DeepPTR fit the observed data?
Evaluates NB log-likelihood on held-out cells, compares reconstruction
error per gene and per cell type.
"""
from _common import *
from scptr.deep._distributions import log_nb_positive
OUT = output_dir("07_reconstruction")
def evaluate_reconstruction(adata_dp, model, history, name, cluster_key="clusters"):
"""Evaluate model fit quality."""
print(f"\n{'=' * 60}\n{name.upper()}: Reconstruction quality\n{'=' * 60}")
from scipy.sparse import issparse
s_np = np.asarray(adata_dp.layers["spliced"]).astype(np.float32)
u_np = np.asarray(adata_dp.layers["unspliced"]).astype(np.float32)
s_t = torch.from_numpy(s_np)
u_t = torch.from_numpy(u_np)
l_s = torch.from_numpy(s_np.sum(1).clip(1).astype(np.float32))
l_u = torch.from_numpy(u_np.sum(1).clip(1).astype(np.float32))
model.eval()
with torch.no_grad():
out = model(s_t, u_t, l_s, l_u, kl_weight=1.0)
# Per-gene reconstruction error
ll_s = log_nb_positive(s_t, out["mu_s"], out["theta_s"]).numpy()
ll_u = log_nb_positive(u_t, out["mu_u"], out["theta_u"]).numpy()
ll_per_gene_s = ll_s.mean(axis=0)
ll_per_gene_u = ll_u.mean(axis=0)
ll_per_gene = ll_per_gene_s + ll_per_gene_u
# Per-cell
ll_per_cell = ll_s.sum(axis=1) + ll_u.sum(axis=1)
# Poisson baseline (mu = mean count)
from scipy.stats import poisson
s_mean = s_np.mean(0, keepdims=True).clip(1e-8)
u_mean = u_np.mean(0, keepdims=True).clip(1e-8)
ll_baseline_s = np.mean(poisson.logpmf(s_np.clip(0, 100).astype(int), s_mean), axis=0)
ll_baseline_u = np.mean(poisson.logpmf(u_np.clip(0, 100).astype(int), u_mean), axis=0)
ll_baseline = ll_baseline_s + ll_baseline_u
improvement = ll_per_gene - ll_baseline
n_improved = (improvement > 0).sum()
print(f" Median per-gene NB log-lik: {np.median(ll_per_gene):.4f}")
print(f" Median Poisson baseline: {np.median(ll_baseline):.4f}")
print(f" Genes improved over baseline: {n_improved}/{len(ll_per_gene)}")
print(f" Training loss: {history.train_loss[-1]:.2f}, Val loss: {history.val_loss[-1]:.2f}")
# Per-cell-type reconstruction
ct_results = []
if cluster_key in adata_dp.obs.columns:
print(f"\n Per-cell-type reconstruction:")
for ct in sorted(adata_dp.obs[cluster_key].unique()):
mask = (adata_dp.obs[cluster_key] == ct).values
if mask.sum() < 5:
continue
mean_ll = ll_per_cell[mask].mean()
ct_results.append({"cell_type": str(ct), "n_cells": int(mask.sum()),
"mean_ll": float(mean_ll)})
print(f" {ct}: mean LL = {mean_ll:.2f} (n={mask.sum()})")
results = {
"median_ll_per_gene": float(np.median(ll_per_gene)),
"median_ll_baseline": float(np.median(ll_baseline)),
"n_improved": int(n_improved),
"n_genes": int(len(ll_per_gene)),
"final_train_loss": float(history.train_loss[-1]),
"final_val_loss": float(history.val_loss[-1]),
"per_celltype": ct_results,
}
# Figure
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))
axes[0].scatter(ll_baseline, ll_per_gene, alpha=0.3, s=8, c="steelblue")
lim = [min(ll_baseline.min(), ll_per_gene.min()), max(ll_baseline.max(), ll_per_gene.max())]
axes[0].plot(lim, lim, "k--", alpha=0.3)
axes[0].set_xlabel("Poisson baseline LL")
axes[0].set_ylabel("DeepPTR NB LL")
axes[0].set_title(f"{n_improved}/{len(ll_per_gene)} genes improved")
axes[1].plot(history.train_loss, label="train")
axes[1].plot(history.val_loss, label="val")
axes[1].set_xlabel("Epoch")
axes[1].set_ylabel("Loss")
axes[1].set_title("Training curve")
axes[1].legend()
if ct_results:
cts = [r["cell_type"] for r in ct_results]
lls = [r["mean_ll"] for r in ct_results]
axes[2].barh(cts, lls, color="steelblue", alpha=0.7)
axes[2].set_xlabel("Mean log-likelihood")
axes[2].set_title("Per-cell-type fit")
fig.suptitle(f"{name}: Reconstruction quality", y=1.02)
fig.tight_layout()
save_fig(fig, f"{name}_reconstruction", OUT)
return results
def main():
set_figure_style()
all_results = {}
for name, loader, ck in DATASETS:
adata_dp, model, history = run_deep(loader)
all_results[name] = evaluate_reconstruction(adata_dp, model, history, name, ck)
save_json(all_results, "reconstruction", OUT)
if __name__ == "__main__":
main()