diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml
index 811b8e034..8bebdabdd 100644
--- a/.github/workflows/build-containers.yml
+++ b/.github/workflows/build-containers.yml
@@ -73,3 +73,8 @@ jobs:
# of detected changes.
always_build: true
context: ./
+ build-and-remove-lpca:
+ uses: "./.github/workflows/build-and-remove-template.yml"
+ with:
+ path: docker-wrappers/LPCA
+ container: reedcompbio/lpca
diff --git a/Snakefile b/Snakefile
index 5ad7aa185..c71625db6 100644
--- a/Snakefile
+++ b/Snakefile
@@ -91,6 +91,16 @@ 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))
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}{algorithm}-lpca.png',out_dir=out_dir, sep=SEP,dataset=dataset_labels,algorithm=algorithms_mult_param_combos))
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}{algorithm}-lpca-coordinates.txt',out_dir=out_dir, sep=SEP, dataset=dataset_labels,algorithm=algorithms_mult_param_combos))
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}{algorithm}-lpca-binary-matrix.csv',out_dir=out_dir, sep=SEP, dataset=dataset_labels,algorithm=algorithms_mult_param_combos))
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}lpca.png',out_dir=out_dir, sep=SEP, dataset=dataset_labels))
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}lpca-scores.csv',out_dir=out_dir, sep=SEP, dataset=dataset_labels))
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}lpca-coordinates.txt',out_dir=out_dir, sep=SEP, dataset=dataset_labels))
+ final_input.extend(expand('{out_dir}{sep}{dataset}-ml{sep}lpca-binary-matrix.csv',out_dir=out_dir, sep=SEP, dataset=dataset_labels))
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 +366,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 +414,32 @@ 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_all:
+ input:
+ pathways = expand('{out_dir}{sep}{{dataset}}-{algorithm_params}{sep}pathway.txt', out_dir=out_dir, sep=SEP, algorithm_params=algorithms_with_params)
+ output:
+ lpca_scores = SEP.join([out_dir, '{dataset}-ml', 'lpca-scores.csv']),
+ lpca_png = SEP.join([out_dir, '{dataset}-ml', 'lpca.png']),
+ lpca_coord = SEP.join([out_dir, '{dataset}-ml', 'lpca-coordinates.txt']),
+ lpca_matrix = SEP.join([out_dir, '{dataset}-ml', 'lpca-binary-matrix.csv'])
+ run:
+ from spras.analysis import lpca
+ summary_df = ml.summarize_networks(input.pathways)
+ lpca.run_lpca(
+ summary_df,
+ output.lpca_scores,
+ output.lpca_matrix,
+ k=_config.config.lpca_params.k,
+ m=_config.config.lpca_params.m,
+ cv=_config.config.lpca_params.cv,
+ container_settings=container_settings
+ )
+ lpca.plot_lpca(
+ output.lpca_scores,
+ output.lpca_png,
+ output.lpca_coord
+ )
+
# 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..9f6b00fad 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -272,3 +272,15 @@ 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
+ lpca:
+ # if true, runs LPCA in addition to the existing PCA (both analyses will run in parallel).
+ # Running both helps compare the two methods on the same data.
+ include: false
+ # number of principal components to compute
+ k: 2
+ # fixed value of the logisticPCA tuning parameter m, used when cv is false.
+ # default of 6 was selected based on cross-validation experiments across multiple
+ # SPRAS algorithms (see https://github.com/Jeebjean/lpca-spras for details)
+ m: 6
+ # if true, choose m by cross-validation; if false, use the fixed m above
+ cv: false
diff --git a/docker-wrappers/LPCA/Dockerfile b/docker-wrappers/LPCA/Dockerfile
new file mode 100644
index 000000000..66b27893a
--- /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(c('logisticPCA', 'rARPACK'), repos='https://cran.r-project.org')" \
+ && Rscript -e "library(logisticPCA); library(rARPACK)"
+
+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..6a3a17e85
--- /dev/null
+++ b/docker-wrappers/LPCA/README.md
@@ -0,0 +1,57 @@
+# LPCA (Logistic PCA) wrapper
+
+Docker image: https://hub.docker.com/r/reedcompbio/lpca
+
+This wrapper runs [logisticPCA](https://github.com/andland/logisticPCA)
+
+This wrapper runs [logisticPCA](https://github.com/andland/logisticPCA)
+([Landgraf & Lee, 2020](https://doi.org/10.1016/j.jmva.2020.104668)) 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
+
+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