-
Notifications
You must be signed in to change notification settings - Fork 29
Add Logistic PCA (LPCA) analysis #500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1e9b218
bb338a8
efd1005
8f89119
3d9a38f
e4b4679
41b64b7
c49362f
7536d8c
63d941d
a44bb50
58ceac3
33f0435
90b5094
20ff901
a16ba19
b063bbd
b1905c0
ec14dc8
1704c47
5b64162
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Jeebjean marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # LPCA (Logistic PCA) wrapper | ||
|
|
||
|
Jeebjean marked this conversation as resolved.
|
||
| Docker image: https://hub.docker.com/r/reedcompbio/lpca | ||
|
|
||
| This wrapper runs [logisticPCA](https://github.com/andland/logisticPCA) | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extra linebreak here |
||
| ([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 | ||
|
Jeebjean marked this conversation as resolved.
|
||
|
|
||
| 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. | ||
|
|
||
| ### `partial_decomp` | ||
|
|
||
| The LPCA wrapper always runs `logisticSVD` with `partial_decomp = TRUE`, which | ||
| uses a truncated (rARPACK-based) decomposition instead of a full one. This is | ||
| hardcoded rather than exposed as a parameter: | ||
|
|
||
| - On small datasets it has no practical effect on the result. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noting that the package documentations suggests that it can slow down decomposition for small datasets. I think it is still fair to say that is harmless, so I don't recommend a change. I'm noting it in case we have future problems.
|
||
| - On large datasets it is required to avoid out-of-memory (OOMKilled) errors | ||
| that occur with the full decomposition. | ||
|
|
||
| Because it is beneficial on large inputs and harmless on small ones, it is | ||
| enabled unconditionally and is not a user-facing configuration option. | ||
|
|
||
| ## Scripts | ||
|
|
||
| The image contains two R scripts under `/app`: | ||
|
|
||
| - `run_lpca.R <input> <output> <k> <m>`: runs logisticPCA with a fixed `m` and | ||
| writes the scores CSV plus a sibling `<output basename>_deviance.txt`. | ||
| - `run_cv.R <input> <output> <k>`: cross-validates `m` over 1..20 and writes a | ||
| CSV with a `best_m` column (plus a `_curve.csv` with the full CV curve). Only | ||
| used when `cv: true`. | ||
|
|
||
| Both read a CSV whose first column holds row labels and whose remaining columns | ||
| are binary (0/1) features, and coerce missing values to 0. | ||
|
|
||
| ## Dependency note | ||
|
|
||
| `logisticPCA` declares `ggplot2` as a hard `Imports` dependency, so building the | ||
| image compiles the ggplot2 stack. The Dockerfile installs the required Debian | ||
| system libraries for that. To build from a source CRAN mirror the `repos` | ||
| argument already points at `https://cran.r-project.org`. | ||
|
|
||
| ## Building and publishing the image | ||
|
|
||
| For the SPRAS default registry to resolve the image, it must be published as | ||
| `docker.io/reedcompbio/lpca:v1`, which requires access to the `reedcompbio` | ||
| Docker Hub organization: | ||
|
|
||
| docker build -t reedcompbio/lpca:v1 docker-wrappers/lpca/ | ||
| docker push reedcompbio/lpca:v1 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # run_cv.R | ||
| # Finds the optimal m for a given k using cross-validation | ||
|
|
||
| args = commandArgs(trailingOnly = TRUE) | ||
| input_file = args[1] | ||
| output_file = args[2] | ||
| k = as.integer(args[3]) | ||
|
|
||
| set.seed(42) | ||
|
|
||
| # Load data | ||
| library(logisticPCA) | ||
| data = read.csv(input_file, row.names = NULL) | ||
| data = data[, -1] | ||
| data_matrix = as.matrix(data) | ||
| data_matrix[is.na(data_matrix)] = 0 | ||
|
|
||
| # Cross-validation over m, fixed k | ||
| cv_result = cv.lpca(data_matrix, ks = k, ms = 1:20) | ||
| best_m = which.min(cv_result) | ||
|
|
||
| cat("Cross-validation done for k =", k, "\n") | ||
| cat("Best m:", best_m, "\n") | ||
|
|
||
| # Save best m | ||
| write.csv(data.frame(k = k, best_m = best_m), output_file, row.names = FALSE) | ||
|
|
||
| # Save full CV curve (all m values and their reconstruction error) | ||
| cv_curve_file = sub("\\.csv$", "_curve.csv", output_file) | ||
| cv_df = data.frame( | ||
| m = 1:20, | ||
| reconstruction_error = as.numeric(cv_result), | ||
| is_best = (1:20) == best_m | ||
| ) | ||
| write.csv(cv_df, cv_curve_file, row.names = FALSE) | ||
| cat("CV curve saved to", cv_curve_file, "\n") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Read command line arguments | ||
| args = commandArgs(trailingOnly = TRUE) | ||
| input_file = args[1] | ||
| output_file = args[2] | ||
| k = as.integer(args[3]) | ||
| m = as.numeric(args[4]) | ||
|
|
||
| # Load data | ||
| library(logisticPCA) | ||
| data = read.csv(input_file, row.names = NULL) | ||
| row_labels = data[, 1] | ||
| data = data[, -1] | ||
| data_matrix = as.matrix(data) | ||
| data_matrix[is.na(data_matrix)] = 0 | ||
|
|
||
| # Run LPCA | ||
| model = logisticPCA(data_matrix, k = k, m = m, partial_decomp = TRUE) | ||
|
Jeebjean marked this conversation as resolved.
|
||
|
|
||
| # Save scores | ||
| scores = model$PCs | ||
| rownames(scores) = row_labels | ||
| write.csv(scores, output_file, row.names = TRUE) | ||
|
|
||
| # Save deviance explained | ||
| deviance_file = sub("\\.csv$", "_deviance.txt", output_file) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm now able to inspect the outputs of LPCA. Does it only write a scalar deviance explained unlike PCA, which provides variance per dimension? Does this account for the LPCA deviance file: PCA variance file: |
||
| writeLines(as.character(model$prop_deviance_expl), deviance_file) | ||
|
|
||
| cat("LPCA done! Scores saved to", output_file, "\n") | ||
| cat("Score dimensions:", nrow(scores), "x", ncol(scores), "\n") | ||
| cat("Proportion of deviance explained:", model$prop_deviance_expl, "\n") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be We may want to create an issue noting this to fix both of them later. |
||
Uh oh!
There was an error while loading. Please reload this page.