diff --git a/Snakefile b/Snakefile
index 5ad7aa185..82f208e4f 100644
--- a/Snakefile
+++ b/Snakefile
@@ -91,6 +91,9 @@ def make_final_input(wildcards):
final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}ensemble-pathway.txt',out_dir=out_dir,sep=SEP,dataset=dataset_labels,algorithm_params=algorithms_with_params))
final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}jaccard-matrix.txt',out_dir=out_dir,sep=SEP,dataset=dataset_labels,algorithm_params=algorithms_with_params))
final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}jaccard-heatmap.png',out_dir=out_dir,sep=SEP,dataset=dataset_labels,algorithm_params=algorithms_with_params))
+
+ if _config.config.analysis_include_lpca:
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}{algorithm}-lpca-scores.csv',out_dir=out_dir, sep=SEP, dataset=dataset_labels, algorithm=algorithms_mult_param_combos))
if _config.config.analysis_include_ml_aggregate_algo:
final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}{algorithm}-pca.png',out_dir=out_dir,sep=SEP,dataset=dataset_labels,algorithm=algorithms_mult_param_combos))
@@ -356,6 +359,7 @@ rule ml_analysis:
ml.hac_horizontal(summary_df, output.hac_image_horizontal, output.hac_clusters_horizontal, **hac_params)
ml.pca(summary_df, output.pca_image, output.pca_variance, output.pca_coordinates, **pca_params)
+
# Calculated Jaccard similarity between output pathways for each dataset
rule jaccard_similarity:
input:
@@ -403,6 +407,23 @@ rule ml_analysis_aggregate_algo:
ml.hac_horizontal(summary_df, output.hac_image_horizontal, output.hac_clusters_horizontal, **hac_params)
ml.pca(summary_df, output.pca_image, output.pca_variance, output.pca_coordinates, **pca_params)
+rule lpca_analysis:
+ input:
+ pathways = collect_pathways_per_algo
+ output:
+ lpca_scores = SEP.join([out_dir, '{dataset}-ml', '{algorithm}-lpca-scores.csv'])
+ run:
+ from spras.analysis import lpca
+ lpca.run_lpca(
+ input.pathways,
+ output.lpca_scores,
+ k=_config.config.lpca_params.k,
+ m=_config.config.lpca_params.m,
+ cv=_config.config.lpca_params.cv,
+ transpose=_config.config.lpca_params.transpose,
+ container_settings=container_settings
+ )
+
# Ensemble the output pathways for each dataset per algorithm
rule ensemble_per_algo:
input:
diff --git a/config/config.yaml b/config/config.yaml
index ef51de739..55c8d3afa 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -272,3 +272,19 @@ analysis:
# adds evaluation per algorithm per dataset-goldstandard pair
# evaluation per algorithm will not run unless ml include and ml aggregate_per_algorithm are set to true
aggregate_per_algorithm: true
+ # Logistic PCA (LPCA) of the binary edge-by-run matrix, one embedding per algorithm
+ # only runs for algorithms with multiple parameter combinations chosen
+ lpca:
+ # run the LPCA analysis per algorithm
+ # LPCA needs enough observations to be meaningful
+ include: false
+ # number of principal components to compute
+ k: 2
+ # fixed value of the logisticPCA tuning parameter m, used when cv is false
+ m: 6
+ # if true, choose m by cross-validation; if false, use the fixed m above
+ cv: false
+ # matrix orientation given to LPCA:
+ # false = edges x runs (observations are the edges)
+ # true = runs x edges (observations are the runs, mirroring the classic ml pca analysis)
+ transpose: false
diff --git a/docker-wrappers/lpca/Dockerfile b/docker-wrappers/lpca/Dockerfile
new file mode 100644
index 000000000..69691fabd
--- /dev/null
+++ b/docker-wrappers/lpca/Dockerfile
@@ -0,0 +1,31 @@
+# Logistic PCA (logisticPCA) wrapper for SPRAS.
+# Invoked by spras/analysis/lpca.py, which supplies the full command
+# (Rscript /app/run_lpca.R ... or /app/run_cv.R ...), so no ENTRYPOINT is set.
+
+# Pinned R version for reproducibility. Bump deliberately, not to :latest.
+FROM rocker/r-base:4.4.2
+
+LABEL org.opencontainers.image.source="https://github.com/Reed-CompBio/spras"
+LABEL org.opencontainers.image.description="Logistic PCA (logisticPCA) wrapper for SPRAS"
+
+# System libraries needed to compile ggplot2 (a hard Import of logisticPCA)
+# and its dependency stack from source on Debian.
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ libcurl4-openssl-dev \
+ libssl-dev \
+ libxml2-dev \
+ libfontconfig1-dev \
+ libfreetype6-dev \
+ libpng-dev \
+ libtiff5-dev \
+ libjpeg-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+# logisticPCA is still on CRAN (last published 2016) and pulls in ggplot2.
+RUN Rscript -e "install.packages('logisticPCA', repos='https://cran.r-project.org')" \
+ && Rscript -e "library(logisticPCA)"
+
+COPY run_lpca.R /app/run_lpca.R
+COPY run_cv.R /app/run_cv.R
+
+WORKDIR /app
\ No newline at end of file
diff --git a/docker-wrappers/lpca/README.md b/docker-wrappers/lpca/README.md
new file mode 100644
index 000000000..37dabc71e
--- /dev/null
+++ b/docker-wrappers/lpca/README.md
@@ -0,0 +1,65 @@
+# LPCA (Logistic PCA) wrapper
+
+This wrapper runs [logisticPCA](https://github.com/andland/logisticPCA)
+(Landgraf & Lee, 2020) as a SPRAS analysis step. It reduces the binary
+edge-by-run matrix built from a set of pathway reconstruction outputs to a small
+number of components and reports the proportion of deviance explained.
+
+The analysis is driven by the `analysis.lpca` config block and the
+`lpca_analysis` Snakemake rule, and is implemented in `spras/analysis/lpca.py`.
+
+## Configuration
+
+ analysis:
+ lpca:
+ include: false # run the LPCA analysis per algorithm
+ k: 2 # number of principal components
+ m: 6 # fixed logisticPCA tuning parameter, used when cv is false
+ cv: false # true: choose m by cross-validation; false: use the fixed m
+ transpose: false # false: edges x runs; true: runs x edges (mirrors the ml pca analysis)
+
+LPCA only runs for algorithms with multiple parameter combinations, so that the
+binary matrix has more than one column. It also needs a reasonable number of
+observations to be meaningful; very small inputs (such as the bundled example
+datasets) produce degenerate results, which is why it is disabled by default.
+
+## Scripts
+
+The image contains two R scripts under `/app`:
+
+- `run_lpca.R