From 28db3710f58cea79b12c4a6599a13ee1d8274f48 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:16:40 -0400 Subject: [PATCH 001/144] Create predict_template.R --- Templates/predict_template.R | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Templates/predict_template.R diff --git a/Templates/predict_template.R b/Templates/predict_template.R new file mode 100644 index 0000000..1eb3e27 --- /dev/null +++ b/Templates/predict_template.R @@ -0,0 +1,40 @@ +# load libraries and arguments + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +query_path = args[1] +model_path = args[2] +pred_path = args[3] +threads = as.numeric(args[4]) + +# path for other outputs (depends on tools) +out_path = dirname(pred_path) + +#--------------- Data ------------------- + +# read query matrix +message('@ READ QUERY') +query = data.table::fread(query_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# load model +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +# CODE FOR ANY DATA MANIPULATION NEEDED BEFORE PREDICTION HERE + +#----------- Predict -------- + +# CODE FOR PREDICTING HERE +message('@ PREDICT LABELS') + + +message('@ DONE') + +# write prediction +data.table::fwrite(, file = pred_path) + +#---------------------------------------- From bab78e3f47d5462b43dc6b0c2bb71bae553970ab Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:17:48 -0400 Subject: [PATCH 002/144] Create train_templat.R --- Templates/train_templat.R | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Templates/train_templat.R diff --git a/Templates/train_templat.R b/Templates/train_templat.R new file mode 100644 index 0000000..fb44e8c --- /dev/null +++ b/Templates/train_templat.R @@ -0,0 +1,46 @@ +# load libraries and arguments + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +model_path = args[3] +threads = as.numeric(args[4]) + +# path for other outputs (depends on tools) +out_path = dirname(model_path) + +#--------------- Data ------------------- + +# read reference matrix +message('@ READ REF') +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# read reference labels +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# CODE FOR ANY DATA MANIPULATION NEEDED BEFORE TRAINING HERE + + +#------------- Train ------------- + +# CODE FOR TRAINING HERE + +# save trained model +message('@ SAVE MODEL') +save(, file = model_path) +message('@ DONE') + +#--------------------------------------------- From 7f2529e934b645db0be9eb954dca4b391f8a12a8 Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Mon, 31 Jul 2023 16:49:28 -0400 Subject: [PATCH 003/144] scClassify (#25) * Update README.md * Update README based on feedback * Move unwanted tools to scripts archive * Comment out rules for unwanted tools * Remove duplicated line * Add train and predict scripts for scClassify * Allow parallelization in scClassify train & test * Add new scClassify scripts to snakefile * Correct typo in variable name * Output tree produced in scClassify training * Streamline code from PR feedback * Add scClassify documentation to README * Add comments for clarity * Update README.md details --- .gitignore | 4 + README.md | 57 ++-- Scripts/{ => archive}/run_CHETAH.R | 0 Scripts/{ => archive}/run_scmapcell.R | 0 Scripts/{ => archive}/run_scmapcluster.R | 0 Scripts/run_SingleR.R | 1 - Scripts/scClassify/predict_scClassify.R | 79 ++++++ Scripts/scClassify/train_scClassify.R | 92 +++++++ snakefile | 318 +++++++++-------------- 9 files changed, 328 insertions(+), 223 deletions(-) create mode 100644 .gitignore rename Scripts/{ => archive}/run_CHETAH.R (100%) rename Scripts/{ => archive}/run_scmapcell.R (100%) rename Scripts/{ => archive}/run_scmapcluster.R (100%) create mode 100644 Scripts/scClassify/predict_scClassify.R create mode 100644 Scripts/scClassify/train_scClassify.R diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a384c66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.Rhistory +.RData +out_test/ +.snakemake/ \ No newline at end of file diff --git a/README.md b/README.md index 6fd72e8..dc6d0e3 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,41 @@ # scCoAnnotate -# Summary +scRNA-seq based prediction of cell-types using an automated Snakemake pipeline to produce a consensus of several prediction tools. -scRNA-seq based prediction of cell-types using a fast and efficient Snakemake pipeline to increase automation and reduce the need to run several scripts and experiments. The pipeline allows the user to select what single-cell annotation tools they want to run on a selected reference to annotate a list of query datasets. It then outputs a consensus of the predictions across tools selected. This pipeline trains classifiers on genes common to the reference and all query datasets. +# Summary -The pipeline also features parallelization options to exploit the computational resources available. +The pipeline allows the user to select what single-cell annotation tools they want to run on a selected reference to annotate a list of query datasets. It then outputs a consensus of the predictions across tools selected. This pipeline trains classifiers on genes common to the reference and all query datasets. The pipeline also features parallelization options to exploit the computational resources available. # Installation and Dependencies -Install [Snakemake](https://snakemake.readthedocs.io/en/stable/) in your linux environment. +* Install or load [R](https://www.r-project.org/) version 4.2.2 and Python version 3.6.5. -You need to have have [R](https://www.r-project.org/) Version 4.0.5 and Python 3.6.5. +* Install [Snakemake](https://snakemake.readthedocs.io/en/stable/) version 5.32.0 in your linux environment. ```bash $ conda activate base $ mamba create -c conda-forge -c bioconda -n snakemake snakemake ``` - - -You need to also install all the dependancies for the tools you plan on using. You have to copy everything present in this repository and not break paths because it would disrupt the dependancies. One note is to change the paths in run_ACTINN.py to match your own directories when you clone the repository. The paths are in lines 44,45,49. - - - -Current version of snakemake is snakemake/5.32.0 +* Install all the dependencies for the tools you plan on using. See [the list of dependencies below](#Required-packages) + +* Clone this repository into your directory of choice # Quickstart -Using snakemake is straight forward and simple. The rules and processes are arranged as per this rule graph: - -Rule preprocess gets the common genes and creates temporary reference and query datasets based ob the common genes. Rule concat appends all predictions into one tab seperate file (prediction_summary.tsv) and gets the consensus prediction - +The processes of the snakemake pipeline are arranged as per this rule graph: ![dag](https://user-images.githubusercontent.com/59002771/191146873-5c680bbd-d11c-418c-ae96-7662ee7f99ed.png) +Rule preprocess gets the common genes and creates temporary reference and query datasets based ob the common genes. Rule concat appends all predictions into one tab seperate file (`prediction_summary.tsv`) and gets the consensus prediction. - -You need to set everything up in a config file and then run the following command: +After preparing your config file, you can run the pipeline with the following command: ```bash snakemake --use-conda --configfile config.yml --cores 3 ``` -## Config File: +## Config file template ```yaml # target directory output_dir: @@ -79,7 +72,7 @@ consensus: ``` -### An Example Config is attached +### Example config file ```yaml # target directory @@ -124,7 +117,6 @@ consensus: An example of the submission file is also available in this repository and is called submit.sh. This is for TORQUE schedulers. - ``` bash #!/usr/bin/bash #PBS -N scCoAnnotate @@ -152,7 +144,7 @@ module load python/3.6.5 snakemake --use-conda --configfile config.yml --cores 3 ``` -# Tools Available +# Available tools 1. [ACTINN](https://github.com/mafeiyang/ACTINN) 2. [SciBet](https://github.com/PaulingLiu/scibet) @@ -166,11 +158,9 @@ snakemake --use-conda --configfile config.yml --cores 3 11. [scPred](https://github.com/powellgenomicslab/scPred) 12. [scmap (cell and cluster)](https://bioconductor.org/packages/release/bioc/html/scmap.html) +# Required packages - -# Packages Required: - -## Python Specific Libraries: +## Python Libraries: ``` tensorboard==1.7.0 @@ -190,6 +180,8 @@ scHPL==0.0.2 ## R Libraries: ``` +glue +data.table scPred_1.9.2 SingleCellExperiment_1.12.0 SummarizedExperiment_1.20.0 @@ -231,7 +223,18 @@ rule {rulename}: "&> {log}" ``` - The tool script you add must generate outputs that match the output of the rule.. + The tool script you add must generate outputs that match the output of the rule. + +# scClassify + +Detailed documentation for scClassify train and predict scripts, written July 2023 by Bhavyaa Chandarana + +* scCoAnnotate input reference and query have cells as the rows, genes as columns. scClassify (and the Seurat function used for normalization, see below) requires genes on the rows and cells on the columns. Therefore, I used `WGCNA::transposeBigData()` (function optimized for large sparse matrices) to transpose the inputs before normalization and training/prediction. + +* scClassify documentation defines "log-transformed" data as "size-factor normalized" data ([source](https://www.bioconductor.org/packages/devel/bioc/vignettes/scClassify/inst/doc/scClassify.html#2_Setting_up_the_data)). Function documentation for both `train_scClassify()` and `predict_scClassify()` specify that reference and query data must be "log-transformed" ([source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) Therefore, I am normalizing both query and reference with `Seurat::NormalizeData()` (default parameters), which performs log normalization with scale factor 10000 ([source](https://satijalab.org/seurat/reference/normalizedata)) +* scClassify train and predict functions `train_scClassify()` and `predict_scClassify()` both allow parallelization with package `BiocParallel`. If greater than one thread was requested by the user, I turn parallelization mode on with parallel = TRUE, and set the `BiocParallel` parameter to `BiocParallel::MulticoreParam()` with workers equal to number of requested threads (based on code in [this issue](https://github.com/SydneyBioX/scClassify/issues/14)) Otherwise I have set parallelization to FALSE and the `BiocParallel` parameter to `BiocParallel::SerialParam()` (which is the default value of the parameter in the functions - [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)). +* scClassify train function `train_scClassify()` can either return a list output for the model, or an R object of class `scClassifyTrainModel`, based on boolean argument `returnList`. Both types work as input for prediction with `predict_scClassify()`. However, in order to use `scClassify::cellTypeTree()` to extract and output the tree produced by scClassify during training, the input must be the R object of class `scClassifyTrainModel`. Therefore, I have chosen to set `returnList` in `train_scClassify()` to FALSE (default: TRUE), and use the resulting object for `cellTypeTree()`. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) +* `scClassify::plotCellTypeTree()` produces a ggplot object. Therefore, I am using `ggplot2::ggsave()` to save it as a png file. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) \ No newline at end of file diff --git a/Scripts/run_CHETAH.R b/Scripts/archive/run_CHETAH.R similarity index 100% rename from Scripts/run_CHETAH.R rename to Scripts/archive/run_CHETAH.R diff --git a/Scripts/run_scmapcell.R b/Scripts/archive/run_scmapcell.R similarity index 100% rename from Scripts/run_scmapcell.R rename to Scripts/archive/run_scmapcell.R diff --git a/Scripts/run_scmapcluster.R b/Scripts/archive/run_scmapcluster.R similarity index 100% rename from Scripts/run_scmapcluster.R rename to Scripts/archive/run_scmapcluster.R diff --git a/Scripts/run_SingleR.R b/Scripts/run_SingleR.R index 640fc10..d273c09 100644 --- a/Scripts/run_SingleR.R +++ b/Scripts/run_SingleR.R @@ -71,7 +71,6 @@ run_SingleR <- function(RefPath, LabelsPath, QueryPaths, OutputDirs){ # tidy up prediction dataframe Pred_Labels_SingleR <- as.vector(pred$labels) Pred_Labels_SingleR <- data.frame(SingleR =Pred_Labels_SingleR,row.names = cellnames) - Test_Time_SingleR <- as.numeric(difftime(end_time,start_time,units = 'secs')) # Create SingleR subdir in target dir dir.create(file.path(OutputDir, "SingleR"), showWarnings = FALSE) diff --git a/Scripts/scClassify/predict_scClassify.R b/Scripts/scClassify/predict_scClassify.R new file mode 100644 index 0000000..75539be --- /dev/null +++ b/Scripts/scClassify/predict_scClassify.R @@ -0,0 +1,79 @@ +# scClassify prediction script +# Bhavyaa Chandarana, July 2023 + +# load libraries and arguments +library(data.table) +library(Seurat) +library(WGCNA) +library(scClassify) +library(dplyr) +library(tibble) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +query_path = args[1] +model_path = args[2] +pred_path = args[3] +threads = as.numeric(args[4]) + +# path for other outputs (depends on tools) +out_path = dirname(model_path) + +#--------------- Data ------------------- + +# read query matrix +message('@ READ QUERY') +query <- data.table::fread(query_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# load model +# object name "scClassify" +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +# transpose (put cells in columns) for Seurat normalization and scClassify, normalize +query <- query %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() + +#----------- Predict scClassify -------- + +# specify parallelization configuration depending on number of threads +if(threads > 1){ + + bpparam <- BiocParallel::MulticoreParam(workers = threads) + parallel <- TRUE + +} else { + + bpparam <- BiocParallel::SerialParam() + parallel <- FALSE + +} + +# run prediction +message('@ PREDICTING QUERY') +pred <- predict_scClassify( + exprsMat_test = as.matrix(query), + trainRes = scClassify, + parallel = parallel, + BPPARAM = bpparam +) + +# extract predictions table and format +message('@ FORMATTING PREDICTIONS') +pred_table <- pred$pearson_WKNN_limma$predRes %>% as.data.frame() %>% rownames_to_column() +colnames(pred_table)[1] <- "cell" +colnames(pred_table)[2] <- "scClassify" + +# write prediction +message('@ SAVE PREDICTIONS') +data.table::fwrite(pred_table, file = pred_path, + row.names = F, + col.names = T, + sep = ",", + nThread = threads) +message('@ DONE') + +#---------------------------------------- diff --git a/Scripts/scClassify/train_scClassify.R b/Scripts/scClassify/train_scClassify.R new file mode 100644 index 0000000..237a06c --- /dev/null +++ b/Scripts/scClassify/train_scClassify.R @@ -0,0 +1,92 @@ +# scClassify training script +# Bhavyaa Chandarana, July 2023 + +# load libraries and arguments +library(data.table) +library(Seurat) +library(WGCNA) +library(scClassify) +library(dplyr) +library(tibble) +library(ggplot2) +library(glue) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +model_path = args[3] +threads = as.numeric(args[4]) + +# path for other outputs (depends on tools) +out_path = dirname(model_path) + +#--------------- Data ------------------- + +# read reference matrix +message('@ READ REF') +ref <- data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') + +# read reference labels +labels <- data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# check if cell names are in the same order in labels and ref +order <- all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# transpose (put cells in columns) for Seurat normalization and scClassify, normalize +ref <- ref %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() + +#------------- Train scClassify ------------- + +# specify parallelization configuration depending on number of threads +if(threads > 1){ + + bpparam <- BiocParallel::MulticoreParam(workers = threads) + parallel <- TRUE + +} else { + + bpparam <- BiocParallel::SerialParam() + parallel <- FALSE + +} + +# train scClassify +message('@ TRAINING MODEL') +scClassify <- train_scClassify( + exprsMat_train = as.matrix(ref), + cellTypes_train = labels$label, + parallel = parallel, + BPPARAM = bpparam, + returnList = FALSE # so that cell type tree can be extracted +) + +# save trained model +message('@ SAVE MODEL') +save(scClassify, file = model_path) +message('@ DONE') + +#------------- Other outputs ---------------- + +# save cell type tree produced in training +message('@ SAVE TREE') +tree <- cellTypeTree(scClassify) +save(tree, file = glue("{out_path}/scClassify_tree.Rda")) +message('@ DONE') + +# save plot of same cell type tree +message('@ SAVE TREE PLOT') +tree_plot <- plotCellTypeTree(tree) +ggsave(tree_plot, file = glue("{out_path}/scClassify_tree.png")) +message('@ DONE') + +#--------------------------------------------- \ No newline at end of file diff --git a/snakefile b/snakefile index 6c2d8e0..da5a019 100644 --- a/snakefile +++ b/snakefile @@ -1,192 +1,140 @@ # import libraries -report: "report/workflow.rst" import os # Get the names of query samples from the paths given in the query section of the config samples = [os.path.basename(os.path.dirname(query_path)) for query_path in config['query_datasets']] -# Get the names of tools to run from the list provided in the config -tools = config['tools_to_run'] - # Create subdirectories for each query sample for sample in samples: path = "{output_dir}/{sample}".format(output_dir = config["output_dir"], sample = sample) if not os.path.exists(path): os.mkdir(path) -# create the prediction output list as an expand() output -output = expand( - "{output_dir}/{sample}/{tool}/{tool}_pred.csv", - tool = config['tools_to_run'], - output_dir = config["output_dir"], - sample = samples) - -# loop to only mark tools that havent been run on all samples to avoid rerunning -to_run = [] -# loop over all tools -for tool in tools: - alreadyrun = True - for sample in samples: - # check if the prediction output of the tool is present for all query datasets - path = "{output_dir}/{sample}/{tool}/{tool}_pred.csv".format(output_dir = config["output_dir"], sample = sample, tool = tool) - # if file is not present in all, mark it for running - if not os.path.isfile(path): - alreadyrun = False - if alreadyrun == False: - to_run.append(tool) - -# config file to check if certain genes required by the user are present inb the list of common genes between reference and queries -genes_required = config['genes_required'] - -# create benchmarking directory is user chooses to perform benchmarking on the reference -benchmarking_dir = "{output_dir}/benchmarking".format(output_dir = config["output_dir"]) -if not os.path.exists(benchmarking_dir) and bool(config['benchmark']): - os.mkdir(benchmarking_dir) - -# create the benchmarking config file and save to benchmarking subdirectory -if bool(config['benchmark']): - outF = open("{dir}/benchmark.yml".format(dir = benchmarking_dir), "w") - outF.write("output_dir: {output_dir}\n".format(output_dir = benchmarking_dir )) - outF.write("datafile: {reference} \n".format(reference = config['training_reference'])) - outF.write("labfile: {labels}\n".format(labels = config['reference_annotations'])) - outF.write("rejection: \"True\"\n") - outF.write("column: 2\n") - outF.write("metrics: \n") - outF.write(" - N1\n") - outF.write(" - F2\n") - outF.write("tools_to_run:\n") - outF.write(" - Correlation\n") - outF.write(" - SciBet\n") - outF.close() - -# invoke the benchmarking subworkflow (saved in the benchmarking folder in scCoAnnotate dir) -subworkflow Benchmark: - workdir: - "Benchmarking/" - snakefile: - "Benchmarking/Snakefile" - configfile: - "{benchmarking_dir}/benchmark.yml".format(benchmarking_dir = benchmarking_dir) - - - -# use rule all to dictate the final output """ One rule that directs the DAG which is represented in the rulegraph """ rule all: input: - output = Benchmark("{benchmarking_dir}/evaluation/finished.csv".format(benchmarking_dir = benchmarking_dir)) - if bool(config['benchmark']) else [], - summary = expand( - "{output_dir}/{sample}/Prediction_Summary.tsv", - output_dir=config["output_dir"], - sample = samples), - predictions = report(expand( - "{output_dir}/{sample}/{tool}/{tool}_pred.csv", - tool=to_run, - output_dir=config["output_dir"], - sample = samples)), - plot_and_embeddings = expand("{output_dir}/{sample}/figures/{tools}.png", - sample = samples, - output_dir = config['output_dir'], tools = config['tools_to_run']) if bool(config['plots']) else [] - + expand(config["output_dir"] + "/{sample}/Prediction_Summary.tsv", + sample = samples), + expand(config["output_dir"] + "/{sample}/{tool}/{tool}_pred.csv", + tool = config['tools_to_run'], + sample = samples) """ - rule that gets the Consensus and concat all predictions +rule that gets the gets the interesction in genes between samples and reference +It outputs temporary reference and query datasets based on the common genes """ -rule plot: +rule preprocess: input: - summary = expand( - "{output_dir}/{sample}/Prediction_Summary.tsv", - output_dir = config["output_dir"], - sample = samples), - query = expand("{query}", query=config['query_datasets']), - output_dir = expand("{output_dir}/{sample}", sample = samples, output_dir=config['output_dir']) + reference = config['training_reference'], + query = config['query_datasets'] output: - expand("{output_dir}/{sample}/figures/{tools}.png", - sample = samples, - output_dir = config['output_dir'], tools = config['tools_to_run']) - - log: expand("{output_dir}/{sample}/plots.log", output_dir=config["output_dir"], sample = samples) + reference = config['output_dir'] + "/expression.csv", + query = expand(config['output_dir'] + "/{sample}/expression.csv", sample = samples) + params: + check_genes = bool(config['check_genes']), + genes_required = config['genes_required'] + log: config['output_dir'] + "/preprocess.log" + priority: 50 shell: - "Rscript Scripts/plot_preds.R " + "Rscript {workflow.basedir}/Scripts/preprocess.R " + "--ref {input.reference} " "--query {input.query} " - "--output_dir {input.output_dir} " - "&> {log}" - + "--output_dir {config[output_dir]} " + "--check_genes {params.check_genes} " + "--genes_required {params.genes_required} " + "&> {log} " """ - rule that gets the Consensus +rule that gets the Consensus """ rule concat: input: - results = expand("{output_dir}/{sample}/{tool}/{tool}_pred.csv", - tool=to_run, - output_dir=config["output_dir"], - sample = samples), - sample = expand("{output_dir}/{sample}", - output_dir=config["output_dir"], - sample = samples) - params: - consensus = config.get("consensus") + results = expand(config["output_dir"] + "/{sample}/{tool}/{tool}_pred.csv", + tool=config['tools_to_run'], + sample = samples), + sample = expand(config["output_dir"] + "/{sample}", + sample = samples) output: - report(expand("{output_dir}/{sample}/Prediction_Summary.tsv", - sample = samples, - output_dir = config['output_dir']), caption = "report/summaries.rst", category = "Predictions") - log: expand("{output_dir}/{sample}/Gatherpreds.log", output_dir=config["output_dir"], sample = samples) + expand(config["output_dir"] + "/{sample}/Prediction_Summary.tsv", + sample = samples) + log: + expand(config["output_dir"] + "/{sample}/Gatherpreds.log", sample = samples) shell: - "python3 Scripts/Gather_Preds.py " + "python3 {workflow.basedir}/Scripts/Gather_Preds.py " "-i {input.sample} " - "-c {params.consensus} " + "-c {config[consensus]} " "-k {input.sample} " "&> {log}" """ - rule that gets the gets the interesction in genes between samples and reference - It outputs temporary reference and query datasets based on the common genes +Rules for R based tools. """ -rule preprocess: +rule train_scClassify: input: - reference = config['training_reference'], - query = expand("{query}", query=config['query_datasets']), - output_dir = config['output_dir'], + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] output: - reference_result = temp("{output_dir}/expression.csv".format(output_dir = config['output_dir'])), - query = temp(expand("{output_dir}/{sample}/expression.csv", output_dir=config["output_dir"], - sample = samples)) + model = config['output_dir'] + "/scClassify/scClassify_model.Rda" params: - check_genes = bool(config['check_genes']), - genes_required = genes_required - log: "{output_dir}/preprocess.log".format(output_dir=config['output_dir']) - priority: 50 + basedir = {workflow.basedir} + log: + config['output_dir'] + "/scClassify/scClassify.log" + benchmark: + config['output_dir'] + "/scClassify/scClassify_train_benchmark.txt" + threads: 1 + resources: shell: - "Rscript Scripts/preprocess.R " - "--ref {input.reference} " - "--query {input.query} " - "--output_dir {input.output_dir} " - "--check_genes {params.check_genes} " - "--genes_required {params.genes_required} " - "&> {log}" + """ + Rscript {params.basedir}/Scripts/scClassify/train_scClassify.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_scClassify: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/scClassify/scClassify_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/scClassify/scClassify_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/scClassify/scClassify.log" + benchmark: + config['output_dir'] + "/{sample}/scClassify/scClassify_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scClassify/predict_scClassify.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ -""" -Rules for R based tools. -""" +#--------------------------------------------------------------------------- + rule correlation: input: reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), labfile = config['reference_annotations'], query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) - output: pred = expand("{output_dir}/{sample}/correlation/correlation_pred.csv", sample = samples,output_dir=config["output_dir"]), query_time = expand("{output_dir}/{sample}/correlation/correlation_query_time.csv",sample = samples,output_dir=config["output_dir"]), training_time = expand("{output_dir}/{sample}/correlation/correlation_training_time.csv",sample = samples,output_dir=config["output_dir"]) log: expand("{output_dir}/{sample}/correlation/correlation.log", sample = samples,output_dir=config["output_dir"]) shell: - "Rscript Scripts/run_correlation.R " + "Rscript {workflow.basedir}/Scripts/run_correlation.R " "--ref {input.reference} " "--labs {input.labfile} " "--query {input.query} " @@ -212,26 +160,6 @@ rule SciBet: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" - -rule scClassify: - input: - reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), - labfile = config['reference_annotations'], - query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), - output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) - - output: - pred = expand("{output_dir}/{sample}/scClassify/scClassify_pred.csv", sample = samples,output_dir=config["output_dir"]), - query_time = expand("{output_dir}/{sample}/scClassify/scClassify_query_time.csv",sample = samples,output_dir=config["output_dir"]), - training_time = expand("{output_dir}/{sample}/scClassify/scClassify_training_time.csv",sample = samples,output_dir=config["output_dir"]) - log: expand("{output_dir}/{sample}/scClassify/scClassify.log", sample = samples,output_dir=config["output_dir"]) - shell: - "Rscript Scripts/run_scClassify.R " - "--ref {input.reference} " - "--labs {input.labfile} " - "--query {input.query} " - "--output_dir {input.output_dir} " - "&> {log}" rule SingleCellNet: input: @@ -251,47 +179,6 @@ rule SingleCellNet: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" - -rule scPred: - input: - reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), - labfile = config['reference_annotations'], - query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), - output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) - - output: - pred = expand("{output_dir}/{sample}/scPred/scPred_pred.csv", sample = samples,output_dir=config["output_dir"]), - query_time = expand("{output_dir}/{sample}/scPred/scPred_query_time.csv",sample = samples,output_dir=config["output_dir"]), - training_time = expand("{output_dir}/{sample}/scPred/scPred_training_time.csv",sample = samples,output_dir=config["output_dir"]) - log: expand("{output_dir}/{sample}/scPred/scPred.log", sample = samples,output_dir=config["output_dir"]) - shell: - "Rscript Scripts/run_scPred.R " - "--ref {input.reference} " - "--labs {input.labfile} " - "--query {input.query} " - "--output_dir {input.output_dir} " - "&> {log}" - -rule SingleR: - input: - reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), - labfile = config['reference_annotations'], - query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), - output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) - - output: - pred = expand("{output_dir}/{sample}/SingleR/SingleR_pred.csv", sample = samples,output_dir=config["output_dir"]), - query_time = expand("{output_dir}/{sample}/SingleR/SingleR_query_time.csv",sample = samples,output_dir=config["output_dir"]), - training_time = expand("{output_dir}/{sample}/SingleR/SingleR_training_time.csv",sample = samples,output_dir=config["output_dir"]) - - log: expand("{output_dir}/{sample}/SingleR/SingleR.log", sample = samples,output_dir=config["output_dir"]) - shell: - "Rscript Scripts/run_SingleR.R " - "--ref {input.reference} " - "--labs {input.labfile} " - "--query {input.query} " - "--output_dir {input.output_dir} " - "&> {log}" rule CHETAH: input: @@ -418,3 +305,44 @@ rule scHPL: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" + +rule scPred: + input: + reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), + labfile = config['reference_annotations'], + query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), + output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) + + output: + pred = expand("{output_dir}/{sample}/scPred/scPred_pred.csv", sample = samples,output_dir=config["output_dir"]), + query_time = expand("{output_dir}/{sample}/scPred/scPred_query_time.csv",sample = samples,output_dir=config["output_dir"]), + training_time = expand("{output_dir}/{sample}/scPred/scPred_training_time.csv",sample = samples,output_dir=config["output_dir"]) + log: expand("{output_dir}/{sample}/scPred/scPred.log", sample = samples,output_dir=config["output_dir"]) + shell: + "Rscript Scripts/run_scPred.R " + "--ref {input.reference} " + "--labs {input.labfile} " + "--query {input.query} " + "--output_dir {input.output_dir} " + "&> {log}" + +rule SingleR: + input: + reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), + labfile = config['reference_annotations'], + query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), + output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) + + output: + pred = expand("{output_dir}/{sample}/SingleR/SingleR_pred.csv", sample = samples,output_dir=config["output_dir"]), + query_time = expand("{output_dir}/{sample}/SingleR/SingleR_query_time.csv",sample = samples,output_dir=config["output_dir"]), + training_time = expand("{output_dir}/{sample}/SingleR/SingleR_training_time.csv",sample = samples,output_dir=config["output_dir"]) + + log: expand("{output_dir}/{sample}/SingleR/SingleR.log", sample = samples,output_dir=config["output_dir"]) + shell: + "Rscript Scripts/run_SingleR.R " + "--ref {input.reference} " + "--labs {input.labfile} " + "--query {input.query} " + "--output_dir {input.output_dir} " + "&> {log}" \ No newline at end of file From 1031310b6c56d1e74e139cdbc97734ed89fadfd5 Mon Sep 17 00:00:00 2001 From: Tom <64378638+tvegawaichman@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:11:43 -0400 Subject: [PATCH 004/144] Scibet, scHPL and SVM (#29) * Added the SciBet training * Generalize * SciBet train and predict * Added some comments * rownames in predict to cell column * Added Other Outputs like gene markers for training and matrix prob to output * Training and prediction script for scHPL * . * Modified to get the dirname of the model output directory * I forgot to add the os library :( * Move the packages to the top in training, and remove obs_names_make_unique in order to not remove duplicated cells * Add the fwrite specifications * Remove var_unique * Modified and repaired some bugs * Comment * Added glue library * For the scHPL I fixed some problems with names, and for the SVM, I have two different codes for training, since it uses two diff packages if the SVM is lineal, the linealSVM is faster for bigger datasets, and the SVM allows diff kernels, the predict is the same for both since it load the model and run the predictions * gitignore --- .gitignore | 4 +- Scripts/SVC/predict_linealSVM.py | 78 +++++++++++++ Scripts/SVC/train_SVM.py | 101 +++++++++++++++++ Scripts/SVC/train_linealSVM.py | 97 +++++++++++++++++ Scripts/SciBet/predict_SciBet.R | 77 +++++++++++++ Scripts/SciBet/train_SciBet.R | 104 ++++++++++++++++++ Scripts/scHPL/predict_scHPL.py | 72 ++++++++++++ Scripts/scHPL/train_scHPL.py | 181 +++++++++++++++++++++++++++++++ 8 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 Scripts/SVC/predict_linealSVM.py create mode 100644 Scripts/SVC/train_SVM.py create mode 100644 Scripts/SVC/train_linealSVM.py create mode 100644 Scripts/SciBet/predict_SciBet.R create mode 100644 Scripts/SciBet/train_SciBet.R create mode 100644 Scripts/scHPL/predict_scHPL.py create mode 100644 Scripts/scHPL/train_scHPL.py diff --git a/.gitignore b/.gitignore index a384c66..0b66dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .Rhistory .RData out_test/ -.snakemake/ \ No newline at end of file +.snakemake/ +config.yml +test/ diff --git a/Scripts/SVC/predict_linealSVM.py b/Scripts/SVC/predict_linealSVM.py new file mode 100644 index 0000000..a64af07 --- /dev/null +++ b/Scripts/SVC/predict_linealSVM.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jul 24 18:37:59 2023 + +@author: tomas.vega +""" +## Python 3.11.2 +#--------------- Libraries ------------------- +import numpy as np +import pandas as pd +from sklearn.svm import SVC +from sklearn.calibration import CalibratedClassifierCV +import anndata as ad +import sys +import scanpy as sc +import pickle +import os +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +sample_path = str(sys.args[1]) +model_path = str(sys.args[2]) +out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.args[3])) +threshold = float(sys.args[4]) +threads = int(sys.argv[5]) +#--------------- Data ------------------------- +print('@ READ QUERY') +query = pd.read_csv(sample_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +print('@ DONE') + +# load model +print('@ LOAD MODEL') +SVM_model = pickle.load(open(model_path, 'rb')) +print('@ DONE') +### If the trained model was generated with a diff number of threads and the +## prediction is done with other number +SVM_model.n_jobs = threads +## We don't need to calculate again the predict_proba +#pred = SVM_model.predict(query.X) +pred_proba = SVM_model.predict_proba(query.X) +df = pd.DataFrame(pred_proba,index = query.obs_names,columns = SVM_model.classes_) +# Function to get the column name and maximum value for each row +def get_max_column_and_value(row): + pred_label = row.idxmax() + proba_label = row.max() + return pred_label, proba_label + +# Create a new column 'max_column' with the column name containing the maximum value for each row +df['pred_label'], df['proba_label'] = zip(*df.apply(get_max_column_and_value, axis=1)) + +#mm1 = pred.tolist() +#mm = df.max_column.tolist() +# mm1 == mm True +## It's the same + +# Create a new column 'unknown_max_column' to store 'max_column' as 'unknown' if 'max_value' is lower than the threshold +df['pred_label_reject'] = df.apply(lambda row: 'Unknown' if row['proba_label'] < threshold else row['pred_label'], axis=1) + +print('@ WRITTING PREDICTIONS') +pred_df = pd.DataFrame({'cell': df.index, 'SVM': df.pred_label_reject}) +pred_df.to_csv(out_path) +print('@ DONE') + +#------------- Other outputs -------------- +### Save the prob matrix +print('@ WRITTING PROB MATRIX ') +filename = out_other_path + '/prob_matrix.csv' +df.to_csv(filename, + index=True) #True because we want to conserve the rownames (cells) +print('@ DONE ') diff --git a/Scripts/SVC/train_SVM.py b/Scripts/SVC/train_SVM.py new file mode 100644 index 0000000..e6f8ee2 --- /dev/null +++ b/Scripts/SVC/train_SVM.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jul 24 18:37:59 2023 + +@author: tomas.vega +""" +## Python 3.11.2 +#--------------- Libraries ------------------- +import numpy as np +import pandas as pd +from sklearn.svm import SVC +from sklearn.calibration import CalibratedClassifierCV +import anndata as ad +import sys +import scanpy as sc +import pickle +import os +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +ref_path = str(sys.argv[1]) +lab_path = str(sys.argv[2]) +out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.argv[4])) +classifier = str(sys.argv[5]) +threads = int(sys.argv[6]) + +#--------------- Data ------------------------- +# read the data +ref = pd.read_csv(ref_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +labels = pd.read_csv(lab_path, + index_col = 0, + sep=',') + +# check if cell names are in the same order in labels and ref +order = all(labels.index == ref.index) +# throw error if order is not the same +if not order: + sys.exit("@ Order of cells in reference and labels do not match") + +adata = ad.AnnData(X = ref, + obs = dict(obs_names=ref.index.astype(str)), + var = dict(var_names=ref.columns.astype(str)) + ) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(adata, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(adata) + +## Transform labels to array + +label = np.array(labels['label']) + +#------------- Train SVC ------------- +##kernel could be ‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’ +#When the constructor option probability is set to True, class membership +#probability estimates (from the methods predict_proba and predict_log_proba) +#are enabled. +svm_model = SVC(kernel=classifier, + probability=True) + +calibrated_model = CalibratedClassifierCV(svm_model, + n_jobs = threads) +calibrated_model.fit(adata.X, label) + + +#Save the model to disk +print('@ SAVE MODEL') +pickle.dump(calibrated_model, open(out_path, 'wb')) +print('@ DONE') + +#------------- Other outputs -------------- +### Plot the tree +filepath = out_other_path + '/train_parameters.csv' +print('@ WRITE TRAIN PARAMETERS ') +params_table = calibrated_model.get_params() +params_table = pd.DataFrame.from_dict(params_table, + orient= 'index') +params_table.to_csv(filepath) +print('@ DONE') + +## I cannot get the features per class, since: +# there is attribute coef_ for SVM classifier but it only works for SVM with +# linear kernel. For other kernels it is not possible because data are transformed +# by kernel method to another space, which is not related to input space +# (https://stackoverflow.com/questions/21260691/how-to-obtain-features-weights) +# I cannot get the scores since I use the CalibratedClassifierCV too, +#and it doens't have the score attribute. + diff --git a/Scripts/SVC/train_linealSVM.py b/Scripts/SVC/train_linealSVM.py new file mode 100644 index 0000000..08d00cb --- /dev/null +++ b/Scripts/SVC/train_linealSVM.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jul 24 18:37:59 2023 + +@author: tomas.vega +""" +## Python 3.11.2 +#--------------- Libraries ------------------- +import numpy as np +import pandas as pd +from sklearn.svm import LinearSVC +from sklearn.calibration import CalibratedClassifierCV +import anndata as ad +import sys +import scanpy as sc +import pickle +import os +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +ref_path = str(sys.argv[1]) +lab_path = str(sys.argv[2]) +out_path = str(sys.argv[3]) +threads = int(sys.argv[4]) + +#--------------- Data ------------------------- +# read the data +ref = pd.read_csv(ref_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies +#ref = ref.T +labels = pd.read_csv(lab_path, + index_col = 0, + sep=',') + +# check if cell names are in the same order in labels and ref +order = all(labels.index == ref.index) +# throw error if order is not the same +if not order: + sys.exit("@ Order of cells in reference and labels do not match") + +adata = ad.AnnData(X = ref, + obs = dict(obs_names=ref.index.astype(str)), + var = dict(var_names=ref.columns.astype(str)) +) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(adata, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(adata) + +## Transform labels to array + +label = np.array(labels['label']) + +#------------- Train SVM Lineal ------------- +##kernel could be ‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’ +#When the constructor option probability is set to True, class membershipsq +#probability estimates (from the methods predict_proba and predict_log_proba) +#are enabled. +svm_model = LinearSVC() +# Calibrate the model using 5-fold cross validation +calibrated_model = CalibratedClassifierCV(svm_model, + n_jobs = threads) #Default +calibrated_model.fit(adata.X, label) + + +#Save the model to disk +print('@ SAVE MODEL') +pickle.dump(calibrated_model, open(out_path, 'wb')) +print('@ DONE') + +#------------- Other outputs -------------- +### Plot the tree +filepath = out_other_path + '/train_parameters.csv' +print('@ WRITE TRAIN PARAMETERS ') +params_table = calibrated_model.get_params() +params_table = pd.DataFrame.from_dict(params_table, + orient= 'index') +params_table.to_csv(filepath) +print('@ DONE') + +## I cannot get the features per class, since: +# there is attribute coef_ for SVM classifier but it only works for SVM with +# linear kernel. For other kernels it is not possible because data are transformed +# by kernel method to another space, which is not related to input space +# (https://stackoverflow.com/questions/21260691/how-to-obtain-features-weights) +# I cannot get the scores since I use the CalibratedClassifierCV too, +#and it doens't have the score attribute. diff --git a/Scripts/SciBet/predict_SciBet.R b/Scripts/SciBet/predict_SciBet.R new file mode 100644 index 0000000..16948e5 --- /dev/null +++ b/Scripts/SciBet/predict_SciBet.R @@ -0,0 +1,77 @@ +# load libraries and arguments +library(data.table) +library(scibet) +library(WGCNA) +library(tidyverse) +library(Seurat) +library(glue) +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +query_path = args[1] +model_path = args[2] +pred_path = args[3] +threads = as.numeric(args[4]) +# path for other outputs (depends on tools) +out_path = dirname(pred_path) + +#--------------- Data ------------------- + +# read query matrix and transpose +message('@ READ QUERY') +### The matrix of the references is transpose since it needs to be normalize +### with Seurat that expect a genes x cell. +query <- data.table::fread(query_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') %>% + WGCNA::transposeBigData() + +message('@ DONE') + +# load model +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +### The matrix here is transposed since SciBet expect a cell x gene matrix. +query <- Seurat::NormalizeData(query) %>% as.data.frame() %>% WGCNA::transposeBigData() + + +#----------- Predict SciBet -------- + +pred <- Scibet_model(query, + result = 'list') + +pred_labels <- data.frame(SciBet = pred, + cell = rownames(query)) +message('@ WRITTING PREDICTIONS') +data.table::fwrite(pred_labels, + file = pred_path, + row.names = F, + col.names = T, + sep = ",", + nThread = threads) +message('@ DONE') +#---------------------------------------- + + +#------------- Other outputs -------------- +### I tested and the results are the same when you run the same model twice, +### so I run it again to obtain the prob matrix. +pred_matrix <- Scibet_model(query, + result = 'table') +rownames(pred_matrix) <- rownames(query) +pred_matrix <- pred_matrix %>% as.data.frame() %>% tibble::rownames_to_column(" ") + +message('@ SAVE PRED MATRIX') +data.table::fwrite(pred_matrix, + file = glue('{out_path}/Query_probabilities_matrix.csv'), + row.names = F, + col.names = T, + sep = ",", + nThread = threads +) +message('@ DONE') +#---------------------------------------- \ No newline at end of file diff --git a/Scripts/SciBet/train_SciBet.R b/Scripts/SciBet/train_SciBet.R new file mode 100644 index 0000000..87e134d --- /dev/null +++ b/Scripts/SciBet/train_SciBet.R @@ -0,0 +1,104 @@ +# load libraries and arguments +library(data.table) +library(scibet) +library(WGCNA) +library(tidyverse) +library(Seurat) +library(glue) +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +model_path = args[3] +threads = as.numeric(args[4]) +out_path = dirname(model_path) +#--------------- Data ------------------- + +# read reference matrix and transpose +message('@ READ REF') +### The matrix of the references is transpose since it needs to be normalize +### with Seurat that expect a genes x cell. +ref <- data.table::fread(ref_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') %>% + WGCNA::transposeBigData() + +message('@ DONE') + +# read reference labels +labels <- data.table::fread(lab_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(colnames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +### The matrix here is transposed since SciBet expect a cell x gene matrix and +### converted to data.frame since labels need to be add. +ref <- Seurat::NormalizeData(ref) %>% as.data.frame() %>% WGCNA::transposeBigData() + +ref$label <- labels$label + +#------------- Train SciBet ------------- + +Scibet_model <- scibet::Learn(expr = ref) + +# save trained model +message('@ SAVE MODEL') + +save(Scibet_model, + file = model_path) + +message('@ DONE') + +#------------- Other outputs -------------- + +### Transforming the model into a matrix +Scibet_matrix <- scibet::ExportModel(Scibet_model) %>% as.data.frame() %>% tibble::rownames_to_column(" ") + +message('@ SAVE MODEL MATRIX') +data.table::fwrite(Scibet_matrix, + file = glue('{out_path}/model_matrix.csv'), + row.names = F, + col.names = T, + sep = ",", + nThread = threads +) +message('@ DONE') + +### Taking the selected genes used in the model +selected_genes <- Scibet_matrix[,1,drop=T] +### Plotting in a marker heatmap +message('@ PLOTTING MARKER HEATMAP MATRIX') +pdf(glue('{out_path}/selected_markers_per_label.pdf', + width = 12, + height = 12) +) +marker_plot <- scibet::Marker_heatmap(expr = ref, + gene = selected_genes) +plot(marker_plot) +dev.off() +message('@ DONE') + +df_markers <- marker_plot %>% .$data %>% filter(group == cell_type) %>% select(c('gene','cell_type','zscore')) +# write df_markers +message('@ SAVE MARKERS') +data.table::fwrite(df_markers, + file = glue('{out_path}/selected_markers_per_label.csv'), + row.names = F, + col.names = T, + sep = ",", + nThread = threads +) +message('@ DONE') +#---------------------------------------- \ No newline at end of file diff --git a/Scripts/scHPL/predict_scHPL.py b/Scripts/scHPL/predict_scHPL.py new file mode 100644 index 0000000..1090cce --- /dev/null +++ b/Scripts/scHPL/predict_scHPL.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Jul 25 17:52:32 2023 + +@author: tomas.vega +""" +## Python 3.11.2 +#--------------- Libraries ------------------- +import pandas as pd +from scHPL import predict +import anndata as ad +import sys +import os +import scanpy as sc +import pickle +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +sample_path = str(sys.args[1]) +model_path = str(sys.args[2]) +out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.args[3])) +threshold = float(sys.args[4]) +#threads = as.numeric(args[4]) + +#--------------- Data ------------------- + +# read query matrix +print('@ READ QUERY') +query = pd.read_csv(sample_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +print('@ DONE') + +# load model +print('@ LOAD MODEL') +scHPL_model = pickle.load(open(model_path, 'rb')) +print('@ DONE') + +### Query preprocessing + +query = ad.AnnData(X = query, + obs = dict(obs_names=query.index.astype(str)), + var = dict(var_names=query.columns.astype(str)) + ) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(query, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(query) + +#----------- Predict scHPL -------- + +pred = predict.predict_labels(testdata= query.X, + tree = scHPL_model, + threshold = threshold) + +print('@ WRITTING PREDICTIONS') + +pred_labels = pd.DataFrame({'cell': query.obs_names, 'scHPL': pred}) +pred_labels.to_csv(out_path) +print('@ DONE') +#---------------------------------------- diff --git a/Scripts/scHPL/train_scHPL.py b/Scripts/scHPL/train_scHPL.py new file mode 100644 index 0000000..9a84feb --- /dev/null +++ b/Scripts/scHPL/train_scHPL.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jul 24 18:37:59 2023 + +@author: tomas.vega +""" +## Python 3.11.2 +#--------------- Libraries ------------------- +import numpy as np +import pandas as pd +from scHPL import train, learn, utils, TreeNode, _print_node, _count_nodes +from matplotlib import pyplot as plt +import matplotlib.lines as mlines +import anndata as ad +import sys +import scanpy as sc +import pickle +import os +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +ref_path = str(sys.argv[1]) +lab_path = str(sys.argv[2]) +out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.argv[3])) +classifier = str(sys.argv[5]) +dimred = bool(sys.argv[6]) + +#--------------- Data ------------------------- +# read the data +ref = pd.read_csv(ref_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies +#ref = ref.T +labels = pd.read_csv(lab_path, + index_col = 0, + sep=',') + +# check if cell names are in the same order in labels and ref +order = all(labels.index == ref.index) +# throw error if order is not the same +if not order: + sys.exit("@ Order of cells in reference and labels do not match") + +adata = ad.AnnData(X = ref, + obs = dict(obs_names=ref.index.astype(str)), + var = dict(var_names=ref.columns.astype(str)) + ) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(adata, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(adata) + +### A tree could be use an input, in that case the read_tree can be used. +### The tree format is Newick formatted file +#tree = utils.read_tree(treePath) + +## They provided this solution here: https://github.com/lcmmichielsen/scHPL/issues/7 +tree = utils.create_tree('root') +tree = learn._construct_tree(tree, labels) + + +#------------- Train scHPL ------------- +##classifier could be svm, svm_occ, knn +#- the linear SVM when your integrated data still has a lot of +#dimensions (e.g. when you have used Seurat to integrate the datasets) +#- the kNN when your integrated data has less, 10-50, dimensions +#(e.g. when you have used scVI or Harmony to integrate the datasets) +#- the one-class SVM when your main focus is to find unseen cell +#populations. A downside of the one-class SVM, however, is that the +#classification performance drops. +tree = train.train_tree(adata.X, + labels.label, + tree, + classifier = classifier, + dimred = dimred, + useRE = True, + FN = 0.5) + +#Save the model to disk +print('@ SAVE MODEL') +pickle.dump(tree, open(out_path, 'wb')) +print('@ DONE') + + +#------------- Other outputs -------------- +### Plot the tree + +from scHPL.utils import +from matplotlib import pyplot as plt +import matplotlib.lines as mlines +import numpy as np + +#I'm using this method since they provided it here: +#https://github.com/lcmmichielsen/scHPL/issues/5 +def _print_node(node, hor, ver_steps, fig, new_nodes): + global ver + # Add horizontal line + x, y = ([np.max([0.05, hor-0.045]), hor], [ver, ver]) + line = mlines.Line2D(x,y, lw=1) + fig.add_artist(line) + + # Add textbox + if np.isin(node.name[0], new_nodes): + txt = r"$\bf{" + node.name[0] + "}$" + else: + txt = node.name[0] + + for n in node.name: + if(n != node.name[0]): + if np.isin(n, new_nodes): + txt = txt + ' & ' + r"$\bf{" + n + "}$" + else: + txt = txt + ' & ' + n + + fig.text(hor,ver, txt, size=10, + ha = 'left', va='center', + bbox = dict(boxstyle='round', fc='w', ec='k')) + + # Continue with child nodes + hor = hor+0.05 + ver_line_start = ver + ver_line_end = ver + + for i in node.descendants: + ver = ver-ver_steps + ver_line_end = ver + _print_node(i, hor, ver_steps, fig, new_nodes) + + # Add vertical line + x, y = ([np.max([0.05, hor-0.045]), np.max([0.05, hor-0.045])], + [ver_line_start, ver_line_end]) + line = mlines.Line2D(x,y, lw=1) + fig.add_artist(line) + +def print_tree(filepath, + tree: TreeNode, + new_nodes: list = []): + '''Print the tree + Parameters + ---------- + tree : TreeNode + Tree to print + new_nodes : List = [] + Nodes recently added to the tree, these are printed in bold + + Returns + ------- + None. + ''' + + global ver + ver = 0.93 + + count = _count_nodes(tree) + ver_steps = 0.9/count + plot_height = count*0.3 + fig = plt.figure(figsize=(6,plot_height)) # This size is hard coded + ax = plt.subplot(111) + + _print_node(tree[0], hor=0.05, ver_steps=ver_steps, fig=fig, + new_nodes = new_nodes) + + plt.axis('off') + plt.savefig(filepath, dpi=1000) + plt.close() + +print('@ PLOTTING') +filepath = out_other_path + '/tree.png' +print_tree(filepath=filepath, + tree = tree) +print('@ DONE') From e6400152e0161b0b56ba28bd2d9d58597f47594a Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Mon, 17 Jul 2023 19:05:16 -0400 Subject: [PATCH 005/144] Initial commit for split training and prediction with SingleR. Training script functional. Rebase with dev --- .gitignore | 1 - Scripts/SingleR/predict_SingleR.R | 0 Scripts/SingleR/train_SingleR.R | 53 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 Scripts/SingleR/predict_SingleR.R create mode 100644 Scripts/SingleR/train_SingleR.R diff --git a/.gitignore b/.gitignore index 0b66dc9..29c9865 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .Rhistory .RData -out_test/ .snakemake/ config.yml test/ diff --git a/Scripts/SingleR/predict_SingleR.R b/Scripts/SingleR/predict_SingleR.R new file mode 100644 index 0000000..e69de29 diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R new file mode 100644 index 0000000..03e4a95 --- /dev/null +++ b/Scripts/SingleR/train_SingleR.R @@ -0,0 +1,53 @@ +# load libraries +library(tidyverse) +library(SingleR) +library(SingleCellExperiment) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +#--------------- Data -------------- + +# read reference matrix and transpose +message('@ READ REF') +ref = data.table::fread(ref_path, data.table=F, header=T, nThread=threads) %>% + as.data.frame() %>% + column_to_rownames('V1') %>% + t() +message('@ DONE') + +# Make SingleCellExperiment object +ref = SingleCellExperiment(assays = list(counts = ref)) + +# Log normalize reference +message('@ NORMALIZE REF') +ref = scuttle::logNormCounts(ref) +message('@ DONE') + +# Read Reference labels +labels = data.table::fread(lab_path) + +# check if cell names are in the same order in labels and ref +order = all(labels$cell == colnames(ref)) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +#------------- SingleR ------------- + +# Train SingleR +message('@ TRAINING MODEL') +singler = trainSingleR(ref, labels=labels$label, num.threads = threads) +message('@ DONE') + +# save trained model +message('@ SAVE MODEL') +save(singler, file = paste0(out_path, '/model_SingleR.Rda')) +message('@ DONE') + +#----------------------------------- From 51c1081352f5433d08e3eb9d0f8007f0d3226913 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Mon, 17 Jul 2023 19:41:48 -0400 Subject: [PATCH 006/144] code clean --- Scripts/SingleR/train_SingleR.R | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index 03e4a95..8e61a7b 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -3,6 +3,8 @@ library(tidyverse) library(SingleR) library(SingleCellExperiment) +set.seed(1234) + args = commandArgs(trailingOnly = TRUE) ref_path = args[1] lab_path = args[2] @@ -13,8 +15,7 @@ threads = as.numeric(args[4]) # read reference matrix and transpose message('@ READ REF') -ref = data.table::fread(ref_path, data.table=F, header=T, nThread=threads) %>% - as.data.frame() %>% +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% column_to_rownames('V1') %>% t() message('@ DONE') @@ -28,10 +29,11 @@ ref = scuttle::logNormCounts(ref) message('@ DONE') # Read Reference labels -labels = data.table::fread(lab_path) +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') # check if cell names are in the same order in labels and ref -order = all(labels$cell == colnames(ref)) +order = all(rownames(labels) == colnames(ref)) # throw error if order is not the same if(!order){ From ddb65fb3adb27f9d29fad0507c882bf59a9468fa Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Mon, 17 Jul 2023 20:24:31 -0400 Subject: [PATCH 007/144] predict_SingelR.R done. Needs to be tested. --- Scripts/SingleR/predict_SingleR.R | 57 +++++++++++++++++++++++++++++++ Scripts/SingleR/train_SingleR.R | 16 ++++----- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/Scripts/SingleR/predict_SingleR.R b/Scripts/SingleR/predict_SingleR.R index e69de29..14467ed 100644 --- a/Scripts/SingleR/predict_SingleR.R +++ b/Scripts/SingleR/predict_SingleR.R @@ -0,0 +1,57 @@ +# load libraries +library(tidyverse) +library(SingleR) +library(SingleCellExperiment) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +sample_path = args[1] +model_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +#--------------- Data ------------------- + +# read query matrix and transpose +message('@ READ QUERY') +query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F, nrow = 500) %>% + column_to_rownames('V1') %>% + t() +message('@ DONE') + +query[1:5, 1:5] + +# load model +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +print(singler) + +#----------- Predict SingleR ------------ + +# Make SingleCellExperiment object +query = SingleCellExperiment(assays = list(counts = query)) + +# Log normalize query +message('@ NORMALIZE QUERY') +query = scuttle::logNormCounts(query) +message('@ DONE') +print(query) + +# predict labels +message('@ PREDICT LABELS') +pred = classifySingleR(query, singler, assay.type = "logcounts") +message('@ DONE') + +pred_labs = data.frame(SingleR = pred$labels, + row.names = rownames(pred)) + +print(pred_labs) + +# write prediction +data.table::fwrite(pred_labs, file = paste0(out_path, 'SingleR_pred.csv')) + +#---------------------------------------- + diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index 8e61a7b..e97edb7 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -11,7 +11,7 @@ lab_path = args[2] out_path = args[3] threads = as.numeric(args[4]) -#--------------- Data -------------- +#--------------- Data ------------------- # read reference matrix and transpose message('@ READ REF') @@ -20,15 +20,15 @@ ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% t() message('@ DONE') -# Make SingleCellExperiment object +# make SingleCellExperiment object ref = SingleCellExperiment(assays = list(counts = ref)) -# Log normalize reference +# log normalize reference message('@ NORMALIZE REF') ref = scuttle::logNormCounts(ref) message('@ DONE') -# Read Reference labels +# read reference labels labels = data.table::fread(lab_path, header=T, data.table=F) %>% column_to_rownames('V1') @@ -40,11 +40,11 @@ if(!order){ stop("@ Order of cells in reference and labels do not match") } -#------------- SingleR ------------- +#------------- Train SingleR ------------- -# Train SingleR +# train SingleR message('@ TRAINING MODEL') -singler = trainSingleR(ref, labels=labels$label, num.threads = threads) +singler = trainSingleR(ref, labels=labels$label, num.threads = threads, assay.type = "logcounts") message('@ DONE') # save trained model @@ -52,4 +52,4 @@ message('@ SAVE MODEL') save(singler, file = paste0(out_path, '/model_SingleR.Rda')) message('@ DONE') -#----------------------------------- +#---------------------------------------- From d36c219202301af8b1c52227f4376c062af5163b Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Mon, 17 Jul 2023 20:25:40 -0400 Subject: [PATCH 008/144] fixed path --- Scripts/SingleR/predict_SingleR.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/SingleR/predict_SingleR.R b/Scripts/SingleR/predict_SingleR.R index 14467ed..a1a16e1 100644 --- a/Scripts/SingleR/predict_SingleR.R +++ b/Scripts/SingleR/predict_SingleR.R @@ -51,7 +51,7 @@ pred_labs = data.frame(SingleR = pred$labels, print(pred_labs) # write prediction -data.table::fwrite(pred_labs, file = paste0(out_path, 'SingleR_pred.csv')) +data.table::fwrite(pred_labs, file = paste0(out_path, '/SingleR_pred.csv')) #---------------------------------------- From f5bf74003cf1ff076bc772e0845dca19c8ef2f49 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Tue, 18 Jul 2023 09:56:21 -0400 Subject: [PATCH 009/144] updated files to match template --- Scripts/SingleR/predict_SingleR.R | 20 +++++++++----------- Scripts/SingleR/train_SingleR.R | 21 ++++++++++----------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/Scripts/SingleR/predict_SingleR.R b/Scripts/SingleR/predict_SingleR.R index a1a16e1..990c3a0 100644 --- a/Scripts/SingleR/predict_SingleR.R +++ b/Scripts/SingleR/predict_SingleR.R @@ -16,37 +16,35 @@ threads = as.numeric(args[4]) # read query matrix and transpose message('@ READ QUERY') query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F, nrow = 500) %>% - column_to_rownames('V1') %>% - t() + column_to_rownames('V1') message('@ DONE') -query[1:5, 1:5] - # load model message('@ LOAD MODEL') load(model_path) message('@ DONE') -print(singler) - -#----------- Predict SingleR ------------ - -# Make SingleCellExperiment object +# Make SingleCellExperiment object from query query = SingleCellExperiment(assays = list(counts = query)) -# Log normalize query +# Log normalize query counts message('@ NORMALIZE QUERY') query = scuttle::logNormCounts(query) message('@ DONE') print(query) + +#----------- Predict SingleR ------------ + # predict labels message('@ PREDICT LABELS') pred = classifySingleR(query, singler, assay.type = "logcounts") message('@ DONE') +save(pred, file = paste0(out_path, '/SingleR_pred.Rda')) + pred_labs = data.frame(SingleR = pred$labels, - row.names = rownames(pred)) + row.names = rownames(pred)) print(pred_labs) diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index e97edb7..83805ae 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -16,16 +16,7 @@ threads = as.numeric(args[4]) # read reference matrix and transpose message('@ READ REF') ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% - column_to_rownames('V1') %>% - t() -message('@ DONE') - -# make SingleCellExperiment object -ref = SingleCellExperiment(assays = list(counts = ref)) - -# log normalize reference -message('@ NORMALIZE REF') -ref = scuttle::logNormCounts(ref) + column_to_rownames('V1') message('@ DONE') # read reference labels @@ -33,13 +24,21 @@ labels = data.table::fread(lab_path, header=T, data.table=F) %>% column_to_rownames('V1') # check if cell names are in the same order in labels and ref -order = all(rownames(labels) == colnames(ref)) +order = all(rownames(labels..) == rownames(ref..)) # throw error if order is not the same if(!order){ stop("@ Order of cells in reference and labels do not match") } +# make SingleCellExperiment object +ref = SingleCellExperiment(assays = list(counts = t(ref))) + +# log normalize reference +message('@ NORMALIZE REF') +ref = scuttle::logNormCounts(ref) +message('@ DONE') + #------------- Train SingleR ------------- # train SingleR From 7209982a6fff51a8da505bfbdd2d6040c71a73e1 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Tue, 18 Jul 2023 18:21:20 -0400 Subject: [PATCH 010/144] tested versions of SingleR scripts and Snakefile updated to split SingleR rule into training and predicting. Everything working on test data. --- Scripts/SingleR/predict_SingleR.R | 18 +++----- Scripts/SingleR/train_SingleR.R | 11 ++--- Scripts/preprocess.R | 1 + config.yml | 34 +++++++------- snakefile | 75 ++++++++++++------------------- 5 files changed, 57 insertions(+), 82 deletions(-) diff --git a/Scripts/SingleR/predict_SingleR.R b/Scripts/SingleR/predict_SingleR.R index 990c3a0..e345fcc 100644 --- a/Scripts/SingleR/predict_SingleR.R +++ b/Scripts/SingleR/predict_SingleR.R @@ -2,6 +2,7 @@ library(tidyverse) library(SingleR) library(SingleCellExperiment) +library(WGCNA) set.seed(1234) @@ -15,7 +16,7 @@ threads = as.numeric(args[4]) # read query matrix and transpose message('@ READ QUERY') -query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F, nrow = 500) %>% +query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F) %>% column_to_rownames('V1') message('@ DONE') @@ -24,15 +25,14 @@ message('@ LOAD MODEL') load(model_path) message('@ DONE') -# Make SingleCellExperiment object from query +# Make SingleCellExperiment object from query (transpose query first) +query = transposeBigData(query, blocksize = 10000) query = SingleCellExperiment(assays = list(counts = query)) # Log normalize query counts message('@ NORMALIZE QUERY') query = scuttle::logNormCounts(query) message('@ DONE') -print(query) - #----------- Predict SingleR ------------ @@ -41,15 +41,11 @@ message('@ PREDICT LABELS') pred = classifySingleR(query, singler, assay.type = "logcounts") message('@ DONE') -save(pred, file = paste0(out_path, '/SingleR_pred.Rda')) - -pred_labs = data.frame(SingleR = pred$labels, - row.names = rownames(pred)) - -print(pred_labs) +pred_labs = data.frame(cell = rownames(pred), + SingleR = pred$labels) # write prediction -data.table::fwrite(pred_labs, file = paste0(out_path, '/SingleR_pred.csv')) +data.table::fwrite(pred_labs, file = out_path) #---------------------------------------- diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index 83805ae..c187cd6 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -2,6 +2,7 @@ library(tidyverse) library(SingleR) library(SingleCellExperiment) +library(WGCNA) set.seed(1234) @@ -12,7 +13,6 @@ out_path = args[3] threads = as.numeric(args[4]) #--------------- Data ------------------- - # read reference matrix and transpose message('@ READ REF') ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% @@ -24,15 +24,16 @@ labels = data.table::fread(lab_path, header=T, data.table=F) %>% column_to_rownames('V1') # check if cell names are in the same order in labels and ref -order = all(rownames(labels..) == rownames(ref..)) +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) # throw error if order is not the same if(!order){ stop("@ Order of cells in reference and labels do not match") } -# make SingleCellExperiment object -ref = SingleCellExperiment(assays = list(counts = t(ref))) +# make SingleCellExperiment object (transpose ref first) +ref = transposeBigData(ref, blocksize = 10000) +ref = SingleCellExperiment(assays = list(counts = ref)) # log normalize reference message('@ NORMALIZE REF') @@ -48,7 +49,7 @@ message('@ DONE') # save trained model message('@ SAVE MODEL') -save(singler, file = paste0(out_path, '/model_SingleR.Rda')) +save(singler, file = out_path) message('@ DONE') #---------------------------------------- diff --git a/Scripts/preprocess.R b/Scripts/preprocess.R index 46b87f9..7215be4 100644 --- a/Scripts/preprocess.R +++ b/Scripts/preprocess.R @@ -87,6 +87,7 @@ options.names <- sapply(listoptions,function(x){ # Set variables containing command line argument values names(options.args) <- unlist(options.names) ref <- unlist(options.args['ref']) +print(ref) test <- unlist(options.args['query']) output_dir <- unlist(options.args['output_dir' ]) check_genes <- unlist(options.args['check_genes' ]) diff --git a/config.yml b/config.yml index 523acea..eb233d2 100644 --- a/config.yml +++ b/config.yml @@ -1,37 +1,33 @@ # target directory -output_dir: /project/kleinman/hussein.lakkis/from_hydra/test +output_dir: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev/out + # path to reference to train classifiers on (cell x gene raw counts) -training_reference: /project/kleinman/hussein.lakkis/from_hydra/2022_01_10-Datasets/Cortex/p0/expression.csv +training_reference: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/expression.csv + # path to annotations for the reference (csv file with cellname and label headers) -reference_annotations: /project/kleinman/hussein.lakkis/from_hydra/2022_01_10-Datasets/Cortex/p0/labels.csv +reference_annotations: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/labels.csv + # path to query datasets (cell x gene raw counts) query_datasets: - - /project/kleinman/hussein.lakkis/from_hydra/Collab/HGG_Selin_Revision/test/BT2016062/expression.csv - - /project/kleinman/hussein.lakkis/from_hydra/Collab/HGG_Selin_Revision/test/P-1694_S-1694_multiome/expression.csv - - /project/kleinman/hussein.lakkis/from_hydra/Collab/HGG_Selin_Revision/test/P-1701_S-1701_multiome/expression.csv + - /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p3/expression.csv + - /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p0/expression.csv + # step to check if some genes are kept in the common gene space between ref and query check_genes: False -# path for the genes required genes_required: Null + # rejection option for SVM rejection: True + # classifiers to run tools_to_run: - - ACTINN - - scHPL - - scClassify - - correlation - - scmapcluster - - scPred - - SingleCellNet - - SVM_reject - SingleR - - CHETAH - - scmapcell - - SciBet + - correlation + # benchmark tools on reference benchmark: False -plots: True + +plots: False consensus: - all diff --git a/snakefile b/snakefile index da5a019..bf4d89f 100644 --- a/snakefile +++ b/snakefile @@ -71,53 +71,7 @@ rule concat: """ Rules for R based tools. """ -rule train_scClassify: - input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] - output: - model = config['output_dir'] + "/scClassify/scClassify_model.Rda" - params: - basedir = {workflow.basedir} - log: - config['output_dir'] + "/scClassify/scClassify.log" - benchmark: - config['output_dir'] + "/scClassify/scClassify_train_benchmark.txt" - threads: 1 - resources: - shell: - """ - Rscript {params.basedir}/Scripts/scClassify/train_scClassify.R \ - {input.reference} \ - {input.labfile} \ - {output.model} \ - {threads} \ - &> {log} - """ -rule predict_scClassify: - input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/scClassify/scClassify_model.Rda" - output: - pred = config['output_dir'] + "/{sample}/scClassify/scClassify_pred.csv" - params: - basedir = {workflow.basedir} - log: - config['output_dir'] + "/{sample}/scClassify/scClassify.log" - benchmark: - config['output_dir'] + "/{sample}/scClassify/scClassify_predict_benchmark.txt" - threads: 1 - resources: - shell: - """ - Rscript {params.basedir}/Scripts/scClassify/predict_scClassify.R \ - {input.query} \ - {input.model} \ - {output.pred} \ - {threads} \ - &> {log} - """ #--------------------------------------------------------------------------- @@ -179,6 +133,29 @@ rule SingleCellNet: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" +<<<<<<< HEAD +======= + +rule scPred: + input: + reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), + labfile = config['reference_annotations'], + query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), + output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) + + output: + pred = expand("{output_dir}/{sample}/scPred/scPred_pred.csv", sample = samples,output_dir=config["output_dir"]), + query_time = expand("{output_dir}/{sample}/scPred/scPred_query_time.csv",sample = samples,output_dir=config["output_dir"]), + training_time = expand("{output_dir}/{sample}/scPred/scPred_training_time.csv",sample = samples,output_dir=config["output_dir"]) + log: expand("{output_dir}/{sample}/scPred/scPred.log", sample = samples,output_dir=config["output_dir"]) + shell: + "Rscript Scripts/run_scPred.R " + "--ref {input.reference} " + "--labs {input.labfile} " + "--query {input.query} " + "--output_dir {input.output_dir} " + "&> {log}" +>>>>>>> 64bc0da (tested versions of SingleR scripts and Snakefile updated to split SingleR rule into training and predicting. Everything working on test data.) rule CHETAH: input: @@ -304,6 +281,7 @@ rule scHPL: "--labs {input.labfile} " "--query {input.query} " "--output_dir {input.output_dir} " +<<<<<<< HEAD "&> {log}" rule scPred: @@ -345,4 +323,7 @@ rule SingleR: "--labs {input.labfile} " "--query {input.query} " "--output_dir {input.output_dir} " - "&> {log}" \ No newline at end of file + "&> {log}" +======= + "&> {log}" +>>>>>>> 64bc0da (tested versions of SingleR scripts and Snakefile updated to split SingleR rule into training and predicting. Everything working on test data.) From a3bc4c70e5d3781de71b9e3e240c2549ab393c9f Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Tue, 18 Jul 2023 19:53:41 -0400 Subject: [PATCH 011/144] updated workdir paramter for SingelR rules --- snakefile | 57 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/snakefile b/snakefile index bf4d89f..f9ba00f 100644 --- a/snakefile +++ b/snakefile @@ -70,12 +70,58 @@ rule concat: """ Rules for R based tools. -""" - #--------------------------------------------------------------------------- +rule train_SingleR: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/SingleR/SingleR_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/SingleR/SingleR.log" + benchmark: + config['output_dir'] + "/SingleR/SingleR_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SingleR/train_SingleR.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_SingleR: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/SingleR/SingleR_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/SingleR/SingleR_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/SingleR/SingleR.log" + benchmark: + config['output_dir'] + "/{sample}/SingleR/SingleR_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SingleR/predict_SingleR.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + rule correlation: input: reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), @@ -133,8 +179,6 @@ rule SingleCellNet: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" -<<<<<<< HEAD -======= rule scPred: input: @@ -155,7 +199,6 @@ rule scPred: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" ->>>>>>> 64bc0da (tested versions of SingleR scripts and Snakefile updated to split SingleR rule into training and predicting. Everything working on test data.) rule CHETAH: input: @@ -281,7 +324,6 @@ rule scHPL: "--labs {input.labfile} " "--query {input.query} " "--output_dir {input.output_dir} " -<<<<<<< HEAD "&> {log}" rule scPred: @@ -324,6 +366,3 @@ rule SingleR: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" -======= - "&> {log}" ->>>>>>> 64bc0da (tested versions of SingleR scripts and Snakefile updated to split SingleR rule into training and predicting. Everything working on test data.) From 013164d1c2f8a1f00dc2dbb595830f018e324a36 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Wed, 19 Jul 2023 11:30:49 -0400 Subject: [PATCH 012/144] sep scripts for training and predicting with scPred. Normalization updated to match tutorial. snakefile updated with split rules. --- Scripts/scPred/predict_scPred.R | 43 ++++++++++++++++++++ Scripts/scPred/train_scPred.R | 67 +++++++++++++++++++++++++++++++ snakefile | 70 +++++++++++++++++++++++---------- 3 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 Scripts/scPred/predict_scPred.R create mode 100644 Scripts/scPred/train_scPred.R diff --git a/Scripts/scPred/predict_scPred.R b/Scripts/scPred/predict_scPred.R new file mode 100644 index 0000000..ac606f5 --- /dev/null +++ b/Scripts/scPred/predict_scPred.R @@ -0,0 +1,43 @@ +# load libraries and arguments + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +query_path = args[1] +model_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +#--------------- Data ------------------- + +# read query matrix +message('@ READ QUERY') +query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# load model +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +# transpose and create seurat object +query = t(query) +query = CreateSeuratObject(query, row.names = colnames(query)) + +# normalize query +query = query %>% + NormalizeData() + +#----------- Predict scPred ------------- + +# predict cells +query = scPredict(query, scpred) + +pred_labs = data.frame(cell = rownames(query), + scPred = query$scpred) + +# write prediction +data.table::fwrite(pred_labs, file = out_path) + +#---------------------------------------- \ No newline at end of file diff --git a/Scripts/scPred/train_scPred.R b/Scripts/scPred/train_scPred.R new file mode 100644 index 0000000..fdb941b --- /dev/null +++ b/Scripts/scPred/train_scPred.R @@ -0,0 +1,67 @@ +# load libraries and arguments +library(scPred) +library(Seurat) +library(tidyverse) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +#--------------- Data ------------------- + +# read reference matrix +message('@ READ REF') +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# read reference labels +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# transpose and create seurat object with the labels as meta data +ref = t(ref) +ref = CreateSeuratObject(ref, row.names = colnames(ref), meta.data = labels) + +# normalize reference +ref = ref %>% + NormalizeData() %>% + FindVariableFeatures() %>% + ScaleData() %>% + RunPCA() %>% + RunUMAP(dims = 1:30) + +# create scPred object stored in the @misc slot of reference +ref = getFeatureSpace(ref, "label") + +#------------- Train scPred ------------- + +# train model +scpred = trainModel(ref) + +# print model info +get_scpred(scpred) + +# Plot prob (implement extra output later) +#pdf('plot_prob') +#plot_probabilities(scpred) +#dev.off() + +# save trained model +message('@ SAVE MODEL') +save(scpred, file = out_path) +message('@ DONE') + +#--------------------------------------------- \ No newline at end of file diff --git a/snakefile b/snakefile index f9ba00f..53e2761 100644 --- a/snakefile +++ b/snakefile @@ -122,6 +122,56 @@ rule predict_SingleR: &> {log} """ +rule train_scPred: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/scPred/scPred_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/scPred/scPred.log" + benchmark: + config['output_dir'] + "/scPred/scPred_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scPred/train_scPred.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_scPred: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/scPred/scPred_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/scPred/scPred_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/scPred/scPred.log" + benchmark: + config['output_dir'] + "/{sample}/scPred/scPred_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scPred/predict_scPred.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#--------------------------------------------------------------------------- + rule correlation: input: reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), @@ -179,26 +229,6 @@ rule SingleCellNet: "--query {input.query} " "--output_dir {input.output_dir} " "&> {log}" - -rule scPred: - input: - reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), - labfile = config['reference_annotations'], - query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), - output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) - - output: - pred = expand("{output_dir}/{sample}/scPred/scPred_pred.csv", sample = samples,output_dir=config["output_dir"]), - query_time = expand("{output_dir}/{sample}/scPred/scPred_query_time.csv",sample = samples,output_dir=config["output_dir"]), - training_time = expand("{output_dir}/{sample}/scPred/scPred_training_time.csv",sample = samples,output_dir=config["output_dir"]) - log: expand("{output_dir}/{sample}/scPred/scPred.log", sample = samples,output_dir=config["output_dir"]) - shell: - "Rscript Scripts/run_scPred.R " - "--ref {input.reference} " - "--labs {input.labfile} " - "--query {input.query} " - "--output_dir {input.output_dir} " - "&> {log}" rule CHETAH: input: From 646166e4f14f9b5aefc6c390d8998e4ab8b98906 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Wed, 19 Jul 2023 11:42:31 -0400 Subject: [PATCH 013/144] updated prediction_scPred script with correct column names for seurat object --- Scripts/scPred/predict_scPred.R | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Scripts/scPred/predict_scPred.R b/Scripts/scPred/predict_scPred.R index ac606f5..74a7b3b 100644 --- a/Scripts/scPred/predict_scPred.R +++ b/Scripts/scPred/predict_scPred.R @@ -1,4 +1,7 @@ # load libraries and arguments +library(scPred) +library(Seurat) +library(tidyverse) set.seed(1234) @@ -12,7 +15,7 @@ threads = as.numeric(args[4]) # read query matrix message('@ READ QUERY') -query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F) %>% +query = data.table::fread(query_path, nThread=threads, header=T, data.table=F) %>% column_to_rownames('V1') message('@ DONE') @@ -32,10 +35,15 @@ query = query %>% #----------- Predict scPred ------------- # predict cells +message('@ PREDICT') query = scPredict(query, scpred) +message('@ DONE') + +head(colnames(query)) +head(query$scpred_prediction) -pred_labs = data.frame(cell = rownames(query), - scPred = query$scpred) +pred_labs = data.frame(cell = colnames(query), + scPred = query$scpred_prediction) # write prediction data.table::fwrite(pred_labs, file = out_path) From df33bf0d56f06820f2e4aadcd49d0534696593ce Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 19 Jul 2023 11:51:27 -0400 Subject: [PATCH 014/144] Update predict_scPred.R --- Scripts/scPred/predict_scPred.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/scPred/predict_scPred.R b/Scripts/scPred/predict_scPred.R index 74a7b3b..a4195a0 100644 --- a/Scripts/scPred/predict_scPred.R +++ b/Scripts/scPred/predict_scPred.R @@ -35,7 +35,7 @@ query = query %>% #----------- Predict scPred ------------- # predict cells -message('@ PREDICT') +message('@ PREDICT LABELS') query = scPredict(query, scpred) message('@ DONE') @@ -48,4 +48,4 @@ pred_labs = data.frame(cell = colnames(query), # write prediction data.table::fwrite(pred_labs, file = out_path) -#---------------------------------------- \ No newline at end of file +#---------------------------------------- From 5e77b1e50c90566c11629991c7428dc6581f80c4 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Wed, 19 Jul 2023 17:10:47 -0400 Subject: [PATCH 015/144] updated scPred_training script to output a qc plot and to run in paralell. Also made model an argument --- Scripts/scPred/train_scPred.R | 20 +++++++++++++------- snakefile | 6 +++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Scripts/scPred/train_scPred.R b/Scripts/scPred/train_scPred.R index fdb941b..9ce37af 100644 --- a/Scripts/scPred/train_scPred.R +++ b/Scripts/scPred/train_scPred.R @@ -2,6 +2,7 @@ library(scPred) library(Seurat) library(tidyverse) +library(doParallel) set.seed(1234) @@ -10,6 +11,8 @@ ref_path = args[1] lab_path = args[2] out_path = args[3] threads = as.numeric(args[4]) +model_dir = args[5] +model = args[6] #--------------- Data ------------------- @@ -48,20 +51,23 @@ ref = getFeatureSpace(ref, "label") #------------- Train scPred ------------- -# train model -scpred = trainModel(ref) +# train model (parallelized) +cl = makePSOCKcluster(threads) +registerDoParallel(cl) +scpred = trainModel(ref, model = model, allowParallel = T) +stopCluster(cl) # print model info get_scpred(scpred) -# Plot prob (implement extra output later) -#pdf('plot_prob') -#plot_probabilities(scpred) -#dev.off() +# Plot prob +pdf(paste0(model_dir, 'qc_plots.pdf'), width=10, height=10) +plot_probabilities(scpred) +dev.off() # save trained model message('@ SAVE MODEL') save(scpred, file = out_path) message('@ DONE') -#--------------------------------------------- \ No newline at end of file +#--------------------------------------------- diff --git a/snakefile b/snakefile index 53e2761..02ff62f 100644 --- a/snakefile +++ b/snakefile @@ -129,7 +129,9 @@ rule train_scPred: output: model = config['output_dir'] + "/scPred/scPred_model.Rda" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir}, + modeldir = config['output_dir'] + "/scPred/", + model = "svmRadial" log: config['output_dir'] + "/scPred/scPred.log" benchmark: @@ -143,6 +145,8 @@ rule train_scPred: {input.labfile} \ {output.model} \ {threads} \ + {params.modeldir} \ + {params.model} \ &> {log} """ From 88e2eb5631f6c5626ef7d0cfcc3b18a44eec6a62 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Wed, 19 Jul 2023 18:55:58 -0400 Subject: [PATCH 016/144] updated method for marker selection in SingleR to method appropriate for single cell reference --- Scripts/SingleR/train_SingleR.R | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index c187cd6..c15a8f3 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -2,7 +2,6 @@ library(tidyverse) library(SingleR) library(SingleCellExperiment) -library(WGCNA) set.seed(1234) @@ -32,7 +31,7 @@ if(!order){ } # make SingleCellExperiment object (transpose ref first) -ref = transposeBigData(ref, blocksize = 10000) +ref = t(ref) ref = SingleCellExperiment(assays = list(counts = ref)) # log normalize reference @@ -44,7 +43,12 @@ message('@ DONE') # train SingleR message('@ TRAINING MODEL') -singler = trainSingleR(ref, labels=labels$label, num.threads = threads, assay.type = "logcounts") +singler = trainSingleR(ref, + labels=labels$label, + num.threads = threads, + assay.type = "logcounts", + de.method = "wilcox", + de.n = 50) message('@ DONE') # save trained model From 60dd348bf7f548ab511ba2111d39ddfd406fcdc0 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:02:32 -0400 Subject: [PATCH 017/144] removed extra output pram from scPred --- snakefile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/snakefile b/snakefile index 02ff62f..728d8bb 100644 --- a/snakefile +++ b/snakefile @@ -127,10 +127,9 @@ rule train_scPred: reference = config['output_dir'] + "/expression.csv", labfile = config['reference_annotations'] output: - model = config['output_dir'] + "/scPred/scPred_model.Rda" + model_type = config['output_dir'] + "/scPred/scPred_model.Rda" params: basedir = {workflow.basedir}, - modeldir = config['output_dir'] + "/scPred/", model = "svmRadial" log: config['output_dir'] + "/scPred/scPred.log" @@ -145,8 +144,7 @@ rule train_scPred: {input.labfile} \ {output.model} \ {threads} \ - {params.modeldir} \ - {params.model} \ + {params.model_type} \ &> {log} """ From d9c1c3324b0773141749c50ef78e40895a39d197 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:03:54 -0400 Subject: [PATCH 018/144] changed out_path name to pred_path --- Scripts/SingleR/predict_SingleR.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/SingleR/predict_SingleR.R b/Scripts/SingleR/predict_SingleR.R index e345fcc..234d136 100644 --- a/Scripts/SingleR/predict_SingleR.R +++ b/Scripts/SingleR/predict_SingleR.R @@ -9,7 +9,7 @@ set.seed(1234) args = commandArgs(trailingOnly = TRUE) sample_path = args[1] model_path = args[2] -out_path = args[3] +pred_path = args[3] threads = as.numeric(args[4]) #--------------- Data ------------------- @@ -45,7 +45,7 @@ pred_labs = data.frame(cell = rownames(pred), SingleR = pred$labels) # write prediction -data.table::fwrite(pred_labs, file = out_path) +data.table::fwrite(pred_labs, file = pred_path) #---------------------------------------- From 63d5a298804db6ad14ef8d087e74b72e0f0bf8c8 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:05:35 -0400 Subject: [PATCH 019/144] changed out_path to model_path --- Scripts/SingleR/train_SingleR.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index c15a8f3..a6c1804 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -8,7 +8,7 @@ set.seed(1234) args = commandArgs(trailingOnly = TRUE) ref_path = args[1] lab_path = args[2] -out_path = args[3] +model_path = args[3] threads = as.numeric(args[4]) #--------------- Data ------------------- @@ -53,7 +53,7 @@ message('@ DONE') # save trained model message('@ SAVE MODEL') -save(singler, file = out_path) +save(singler, file = model_path) message('@ DONE') #---------------------------------------- From d39b16a41284c72bacb59a9e61c185b8ba284443 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:10:10 -0400 Subject: [PATCH 020/144] chnaged out_path to model_path --- Scripts/scPred/train_scPred.R | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Scripts/scPred/train_scPred.R b/Scripts/scPred/train_scPred.R index 9ce37af..cb3631b 100644 --- a/Scripts/scPred/train_scPred.R +++ b/Scripts/scPred/train_scPred.R @@ -9,10 +9,12 @@ set.seed(1234) args = commandArgs(trailingOnly = TRUE) ref_path = args[1] lab_path = args[2] -out_path = args[3] +model_path = args[3] threads = as.numeric(args[4]) -model_dir = args[5] -model = args[6] +model_type = args[5] + +# path for other outputs (depends on tools) +out_path = dirname(model_path) #--------------- Data ------------------- @@ -54,20 +56,20 @@ ref = getFeatureSpace(ref, "label") # train model (parallelized) cl = makePSOCKcluster(threads) registerDoParallel(cl) -scpred = trainModel(ref, model = model, allowParallel = T) +scpred = trainModel(ref, model = model_type, allowParallel = T) stopCluster(cl) # print model info get_scpred(scpred) # Plot prob -pdf(paste0(model_dir, 'qc_plots.pdf'), width=10, height=10) +pdf(paste0(out_path, 'qc_plots.pdf'), width=10, height=10) plot_probabilities(scpred) dev.off() # save trained model message('@ SAVE MODEL') -save(scpred, file = out_path) +save(scpred, file = model_path) message('@ DONE') #--------------------------------------------- From e3a2133670ca803ddbfd686a4630e6694424987a Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:10:50 -0400 Subject: [PATCH 021/144] chnaged out_path to pred_path --- Scripts/scPred/predict_scPred.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/scPred/predict_scPred.R b/Scripts/scPred/predict_scPred.R index a4195a0..c3cf2b9 100644 --- a/Scripts/scPred/predict_scPred.R +++ b/Scripts/scPred/predict_scPred.R @@ -8,7 +8,7 @@ set.seed(1234) args = commandArgs(trailingOnly = TRUE) query_path = args[1] model_path = args[2] -out_path = args[3] +pred_path = args[3] threads = as.numeric(args[4]) #--------------- Data ------------------- @@ -46,6 +46,6 @@ pred_labs = data.frame(cell = colnames(query), scPred = query$scpred_prediction) # write prediction -data.table::fwrite(pred_labs, file = out_path) +data.table::fwrite(pred_labs, file = pred_path) #---------------------------------------- From c86f7710a257a0c9e3d30d29591c85b1eabed319 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:27:15 -0400 Subject: [PATCH 022/144] Updated README to include information about parameters and normalization for each tool --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dc6d0e3..34152d7 100644 --- a/README.md +++ b/README.md @@ -237,4 +237,26 @@ Detailed documentation for scClassify train and predict scripts, written July 20 * scClassify train function `train_scClassify()` can either return a list output for the model, or an R object of class `scClassifyTrainModel`, based on boolean argument `returnList`. Both types work as input for prediction with `predict_scClassify()`. However, in order to use `scClassify::cellTypeTree()` to extract and output the tree produced by scClassify during training, the input must be the R object of class `scClassifyTrainModel`. Therefore, I have chosen to set `returnList` in `train_scClassify()` to FALSE (default: TRUE), and use the resulting object for `cellTypeTree()`. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) -* `scClassify::plotCellTypeTree()` produces a ggplot object. Therefore, I am using `ggplot2::ggsave()` to save it as a png file. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) \ No newline at end of file +* `scClassify::plotCellTypeTree()` produces a ggplot object. Therefore, I am using `ggplot2::ggsave()` to save it as a png file. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) + +# scPred + +Both reference and query is normaluzed using `Seurat::NormalizeData()`. +Needs computed PCA space. Dimesions set to 1:30 according to tutorial. +Default model `SVMradial`. Option to switch model should be set up in snakemake. + +Normalization and parameters based on this tutorial: +https://powellgenomicslab.github.io/scPred/articles/introduction.html + +# SingleR + +Both reference and query is normaluzed using `scuttle::logNormCounts()`. Both reference and query is converted to SingleCellExperiment objects before normalization. + +Deviation from default parameters: +* `de.method = de.method="wilcox"` +Method for generating marker genes for each class in reference. Wilcox is recomended when single cell data is used as reference +* `de.n = 50` +Number of marker genes to use for each class. Default is `de.n = 10`. Seemed to low so chnaged to 50 (arbitrary). Should be tested with benchmarking at some point (?) + +Normalization and parameters based on this tutorial: +http://www.bioconductor.org/packages/devel/bioc/vignettes/SingleR/inst/doc/SingleR.html#3_Using_single-cell_references From 6efa42a25e4556ff7b97348c2b46a6e818aae4f3 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 26 Jul 2023 13:35:17 -0400 Subject: [PATCH 023/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34152d7..af0c4e4 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,7 @@ Detailed documentation for scClassify train and predict scripts, written July 20 # scPred Both reference and query is normaluzed using `Seurat::NormalizeData()`. -Needs computed PCA space. Dimesions set to 1:30 according to tutorial. +Needs computed PCA space. Dims set to 1:30 according to tutorial. Default model `SVMradial`. Option to switch model should be set up in snakemake. Normalization and parameters based on this tutorial: From 1e56b8a5226c7cc98df708d338c598c72fa03d0d Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Mon, 31 Jul 2023 17:57:29 -0400 Subject: [PATCH 024/144] updated README with default parameter --- README.md | 2 -- Scripts/SingleR/train_SingleR.R | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index af0c4e4..d8de222 100644 --- a/README.md +++ b/README.md @@ -255,8 +255,6 @@ Both reference and query is normaluzed using `scuttle::logNormCounts()`. Both re Deviation from default parameters: * `de.method = de.method="wilcox"` Method for generating marker genes for each class in reference. Wilcox is recomended when single cell data is used as reference -* `de.n = 50` -Number of marker genes to use for each class. Default is `de.n = 10`. Seemed to low so chnaged to 50 (arbitrary). Should be tested with benchmarking at some point (?) Normalization and parameters based on this tutorial: http://www.bioconductor.org/packages/devel/bioc/vignettes/SingleR/inst/doc/SingleR.html#3_Using_single-cell_references diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index a6c1804..f21127d 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -48,7 +48,7 @@ singler = trainSingleR(ref, num.threads = threads, assay.type = "logcounts", de.method = "wilcox", - de.n = 50) + de.n = 10) message('@ DONE') # save trained model From 0569aeb2c1569f1f59bef63f748ec795f3509cdc Mon Sep 17 00:00:00 2001 From: rlopezgutierrez Date: Tue, 1 Aug 2023 12:35:18 -0400 Subject: [PATCH 025/144] singleCellNet (#32) * training and predicting scripts separated for singleCellNet * Merged singleCellNet readme info into dev branch * Changed some singleCellNet parameters to default values --------- Co-authored-by: Rodrigo Lopez Gutierrez Co-authored-by: Rodrigo Lopez Gutierrez --- README.md | 17 ++++- Scripts/singleCellNet/predict_singleCellNet.R | 52 ++++++++++++++ Scripts/singleCellNet/train_singleCellNet.R | 71 +++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 Scripts/singleCellNet/predict_singleCellNet.R create mode 100644 Scripts/singleCellNet/train_singleCellNet.R diff --git a/README.md b/README.md index d8de222..6e487d9 100644 --- a/README.md +++ b/README.md @@ -256,5 +256,20 @@ Deviation from default parameters: * `de.method = de.method="wilcox"` Method for generating marker genes for each class in reference. Wilcox is recomended when single cell data is used as reference -Normalization and parameters based on this tutorial: +Normalization and parameters based on this tutorial: http://www.bioconductor.org/packages/devel/bioc/vignettes/SingleR/inst/doc/SingleR.html#3_Using_single-cell_references + +# singleCellNet + +Documentation written by: Rodrigo Lopez Gutierrez +Date written: 2023-08-01 + +Input for `singleCellNet` is raw counts for both reference and query. The reference is normalized within the `scn_train()` function. The query is currently not normalized. In the tutorial example they used raw query data. Furthermore, according to the tutorial, the classification step is robust to the normalization and transformation steps of the query data sets. They claim that one can even directly use raw data to query and still obtains accurate classification. This could be tested in the future with our data to see if normalized queries perform better. + +Normal parameters were used in both the training and prediction functions, with the expection of the following parameters: +* In `scn_train()`, we used parameter `nTrees = 500` compared to the default `nTrees = 1000`. This parameter changes the number of trees for the random forest classifier. The value selected is based on Hussein's thesis and is changed to improve the speed of `singleCellNet`. It is mentioned that additional training parameters may need to be adjusted depending on the quality of the reference data. Additionally, tutorial mentions that classifier performance may increase if the values for `nTopGenes` and `nTopGenePairs` are increased. +* In `scn_predict()`, we used parameter `nrand = 0` compared to the default `nrand = 50`. This parameter changes the number of randomized single cell RNA-seq profiles which serve as positive controls that should be mostly classified as `rand` (unknown) category. If left at default value, then this would generate extra cells that might complicate downstream consolidation of the consensus predictions for each cell. Therefore, the selected value is used to avoid complication. + +singleCellNet workflow was generated following the tutorial below: +https://pcahan1.github.io/singleCellNet/ + diff --git a/Scripts/singleCellNet/predict_singleCellNet.R b/Scripts/singleCellNet/predict_singleCellNet.R new file mode 100644 index 0000000..8e5a694 --- /dev/null +++ b/Scripts/singleCellNet/predict_singleCellNet.R @@ -0,0 +1,52 @@ +library(tidyverse) +library(Seurat) +library(singleCellNet) +library(tidyverse) +library(data.table) +library(WGCNA) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +sample_path = args[1] +model_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +#--------------- Data ------------------- + +# read query matrix and transpose +message('@ READ QUERY') +query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# Get cell names +cellnames = row.names(query) + +# load model +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +# Transpose query +query = transposeBigData(query, blocksize = 10000) + +#----------- Predict singleCellNet ------------ + +# predict labels +message('@ PREDICT LABELS') +# Default nrand = 50, Hussein had used nrand = 0 +crPBMC = scn_predict(class_info[['cnProc']], query, nrand = 0) +message('@ DONE') + +# classify cells +stQuery = assign_cate(classRes = crPBMC, sampTab = data.frame(row.names = cellnames), cThresh = 0.5) + +pred_labs = data.frame(cell = rownames(stQuery), + singleCellNet = stQuery$category) + +# write prediction +data.table::fwrite(pred_labs, file = out_path) + +#---------------------------------------- diff --git a/Scripts/singleCellNet/train_singleCellNet.R b/Scripts/singleCellNet/train_singleCellNet.R new file mode 100644 index 0000000..b95129c --- /dev/null +++ b/Scripts/singleCellNet/train_singleCellNet.R @@ -0,0 +1,71 @@ +# load libraries +library(tidyverse) +library(data.table) +library(Seurat) +library(singleCellNet) +library(WGCNA) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +#--------------- Data ------------------- +# read reference matrix and transpose +message('@ READ REF') +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# read reference labels +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# make Seurat object (transpose ref first) +ref = transposeBigData(ref, blocksize = 10000) +seurat = CreateSeuratObject(counts = ref, meta.data = labels) + +#------------- Train singleCellNet ------------- + +# Split reference data for training and assessment +# Default ncells = 50, Hussein had used ncells = 80 in scCoAnnotate V1 +stList = splitCommon(sampTab = seurat@meta.data, ncells = 50, dLevel = "label") +# Get the downsampled list +stTrain = stList[[1]] +# Get corresponding +expTrain = as.matrix(GetAssayData(seurat))[,row.names(stTrain)] + +# Train singleCellNet +# Default uses nTopGenes = 10, nTrees = 1000 +# Hussein had used nTopGenes = 12, nTrees = 350 in scCoAnnotate V1 and nTrees = 500 in his thesis +message('@ TRAINING MODEL') +class_info = scn_train(stTrain = stTrain, + expTrain = expTrain, + nTopGenes = 10, + nRand = 70, + nTrees = 500, + nTopGenePairs = 25, + dLevel = "label") +message('@ DONE') + +# save trained model +message('@ SAVE MODEL') +save(class_info, file = out_path) +message('@ DONE') + +#---------------------------------------- + + + + From 95953879a15acc53b9f70adcbb6114842871db21 Mon Sep 17 00:00:00 2001 From: rlopezgutierrez Date: Wed, 2 Aug 2023 17:05:52 -0400 Subject: [PATCH 026/144] Split "training" and prediction scripts for Correlation tool (#36) * Generated training script for correlation method which generates a mean expression matrix for each label in the reference * Generated prediction script for Correlation tool * Add Correlation tool information in the README --------- Co-authored-by: Rodrigo Lopez Gutierrez --- README.md | 17 ++++ Scripts/Correlation/predict_Correlation.R | 98 +++++++++++++++++++++++ Scripts/Correlation/train_Correlation.R | 61 ++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 Scripts/Correlation/predict_Correlation.R create mode 100644 Scripts/Correlation/train_Correlation.R diff --git a/README.md b/README.md index 6e487d9..66734a2 100644 --- a/README.md +++ b/README.md @@ -273,3 +273,20 @@ Normal parameters were used in both the training and prediction functions, with singleCellNet workflow was generated following the tutorial below: https://pcahan1.github.io/singleCellNet/ +# Correlation + +Documentation written by: Rodrigo Lopez Gutierrez +Date written: 2023-08-02 + +The Correlation tool runs a correlation-based cell type prediction on a sample of interest, given the mean gene expression per label for a reference. +The function to label by Spearman correlation was originally generated by Selin Jessa and Marie Coutlier +Path to original file: `/lustre06/project/6004736/sjessa/from_narval/HGG-oncohistones/stable/code/scripts/predict_celltype_cor.R` + +Input for `Correlation` is raw counts for both reference and query. Both the reference and the query are normalized using `Seurat::NormalizeData()`. + +Training script generates a matrix with the mean gene expression for each label in the reference. +Prediction script calculates a correlation between each cell in the query and each label in mean gene expression matrix generated in the training script. Then we assign each cell the most highly correlated label. +* `label_correlation()` function has a parameter `threshold_common_genes` which sets the percentage of query dataset genes required to be in the reference dataset in order to proceed. This parameter is currently not utilized as the preprocessing done in the beginning of the snakefile is extracting only the common genes between the reference and the queries. + +Currently only outputting a table with each cell, the most highly correlated label, and the corresponding correlation score for that label. In the future we could export the full correlation matrix, if necessary. + diff --git a/Scripts/Correlation/predict_Correlation.R b/Scripts/Correlation/predict_Correlation.R new file mode 100644 index 0000000..a046126 --- /dev/null +++ b/Scripts/Correlation/predict_Correlation.R @@ -0,0 +1,98 @@ +# loads needed libraries +library(tidyverse) +library(data.table) +library(Seurat) +library(WGCNA) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +sample_path = args[1] +model_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +#--------------- Data ------------------- + +# read query matrix and transpose +message('@ READ QUERY') +query = data.table::fread(sample_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# load model +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +# Transpose query +query = transposeBigData(query, blocksize = 10000) +seurat_query = CreateSeuratObject(counts = query) + +# Normalize seurat using default "LogNormalize" method +seurat_query = NormalizeData(seurat_query) + +# Get the normalized expression matrix from the query +query_mat = GetAssayData(seurat_query) + +# ------------ Load Correlation Function -------------- +# Function to label by Spearman correlation generated by Selin Jessa and Marie Coutlier +# Path to original file: /lustre06/project/6004736/sjessa/from_narval/HGG-oncohistones/stable/code/scripts/predict_celltype_cor.R + + +label_correlation <- function(test_expr_mat, + ref_expr_mat, + threshold_common_genes = 0.5) { + + rownames(test_expr_mat) <- toupper(rownames(test_expr_mat)) + rownames(ref_expr_mat) <- toupper(rownames(ref_expr_mat)) + + # Testing how many genes are in common and stopping if not enough + common_genes <- intersect(rownames(test_expr_mat), rownames(ref_expr_mat)) + prop_common <- length(common_genes) / length(rownames(test_expr_mat)) + message("@@ ", round(prop_common*100, digits = 2), "% of test dataset genes are in the reference dataset") + + if (prop_common < threshold_common_genes) stop("Proportion of common genes below threshold.") + + # Reducing matrices to common subset + mat1 <- as.matrix(test_expr_mat[common_genes, ]) + mat2 <- as.matrix(ref_expr_mat[common_genes, ]) + + # sanity check + nrow(mat1) == nrow(mat2) + + # Computing correlations + cor_matrix <- cor(mat1, mat2, method = "spearman", use = "complete.obs") + + # Getting the best one + cor_label <- as.data.frame(cor_matrix) %>% + mutate("cell" = rownames(cor_matrix)) %>% + gather("celltype", "correlation", -cell) %>% + group_by(cell) %>% + top_n(1, correlation) %>% + dplyr::select(cell, celltype, correlation) %>% + arrange(cell) + + # Returning the results + return(cor_label) + +} + +#----------- Predict Correlation ------------ + +# predict labels +message('@ PREDICT LABELS') +# Hussein set the threshold_common_genes = 0.3 +# This does not affect current prediction as preprocessing is already subsetting common genes between reference and query +# Therefore, 100% of query dataset genes should be in the reference dataset +# However, this is something that could be explored in the future +predicted = as.data.frame(label_correlation(query_mat,ref_mean_mat,0.3)) +message('@ DONE') + +# Rename columns +colnames(predicted) <- c("cell", "Correlation","Correlation_score") + +# write prediction +data.table::fwrite(predicted, file = out_path) + +#---------------------------------------- diff --git a/Scripts/Correlation/train_Correlation.R b/Scripts/Correlation/train_Correlation.R new file mode 100644 index 0000000..8cc0b17 --- /dev/null +++ b/Scripts/Correlation/train_Correlation.R @@ -0,0 +1,61 @@ +# loads needed libraries +library(tidyverse) +library(data.table) +library(Seurat) +library(WGCNA) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) + +# Script modified from Hussein Lakkis's run_correlation.R script + +#--------------- Data ------------------- +# read reference matrix and transpose +message('@ READ REF') +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# read reference labels +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') + + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# make Seurat object (transpose ref first) +ref = transposeBigData(ref, blocksize = 10000) +seurat = CreateSeuratObject(counts = ref, meta.data = labels) + +# Normalize seurat using default "LogNormalize" method +seurat = NormalizeData(seurat) + +#------------- Generate Mean Expression Matrix ------------- + +# Set labels as identity classes +Idents(seurat) = "label" +# Returns averaged expression values for each identity class +# Function uses slot = "data" (normalized data) as default +seurat = AverageExpression(seurat, return.seurat = TRUE) + +# get mean expression matrix +ref_mean_mat = GetAssayData(seurat) + +# save mean expression matrix +message('@ SAVE MODEL') +save(ref_mean_mat, file = out_path) +message('@ DONE') + +#---------------------------------------- + From b13fab9f99ae53ae9b047d275a42565f60369fc9 Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Fri, 4 Aug 2023 16:12:31 -0400 Subject: [PATCH 027/144] scLearn (#35) * Fix file name typo * Add scLearn train & test scripts + docs * Simplify list naming code - PR feedback * Update README.md * Removed the QC filtering step and replaced with a Seurat normalization, added more outputs for prediction and query --------- Co-authored-by: Tom <64378638+tvegawaichman@users.noreply.github.com> Co-authored-by: Tom --- README.md | 29 +++++++ Scripts/scLearn/predict_scLearn.R | 71 ++++++++++++++++ Scripts/scLearn/train_scLearn.R | 80 +++++++++++++++++++ .../{train_templat.R => train_template.R} | 0 4 files changed, 180 insertions(+) create mode 100644 Scripts/scLearn/predict_scLearn.R create mode 100644 Scripts/scLearn/train_scLearn.R rename Templates/{train_templat.R => train_template.R} (100%) diff --git a/README.md b/README.md index 66734a2..b89ac12 100644 --- a/README.md +++ b/README.md @@ -290,3 +290,32 @@ Prediction script calculates a correlation between each cell in the query and ea Currently only outputting a table with each cell, the most highly correlated label, and the corresponding correlation score for that label. In the future we could export the full correlation matrix, if necessary. +# scLearn + +Detailed documentation for scLearn train and predict scripts, written August 2023 by Bhavyaa Chandarana. Added information Tomas Vega Waichman in 2023-08-04. + +Preprocessing performed in the same way as scLearn documentation tutorial [source](https://github.com/bm2-lab/scLearn#tutorial) for `Single-label single cell assignment` + +* scCoAnnotate input reference and query have cells as the rows, genes as columns. scLearn requires genes on the rows and cells on the columns. Therefore, I used `WGCNA::transposeBigData()` (function optimized for large sparse matrices) to transpose the inputs before normalization and training/prediction. + +* In order to avoid cell filtering, the reference and query matrix were normalized using Seurat::NormalizeData since the authors make the logNormalization in this way but manually (using a scale.factor = 10000 and then log(ref + 1)). Because of this the arguments of 'species' is not used and this allows to use this methods in order species different to the Human and Mouse. + +* Used default value `10` for argument `bootstrap_times` in training function. According to tool documentation, this can be increased to improve accuracy for unassigned cells(?) but increase train time. + +* Default parameters were used for tool prediction + +* Added some outputs, for prediction added a table with the selected genes for the model. In prediction added and output with the whole data.frame with the probabilities for each cell. + +# singleCellNet + +Documentation written by: Rodrigo Lopez Gutierrez +Date written: 2023-08-01 + +Input for `singleCellNet` is raw counts for both reference and query. The reference is normalized within the `scn_train()` function. The query is currently not normalized. In the tutorial example they used raw query data. Furthermore, according to the tutorial, the classification step is robust to the normalization and transformation steps of the query data sets. They claim that one can even directly use raw data to query and still obtains accurate classification. This could be tested in the future with our data to see if normalized queries perform better. + +Normal parameters were used in both the training and prediction functions, with the expection of the following parameters: +* In `scn_train()`, we used parameter `nTrees = 500` compared to the default `nTrees = 1000`. This parameter changes the number of trees for the random forest classifier. The value selected is based on Hussein's thesis and is changed to improve the speed of `singleCellNet`. It is mentioned that additional training parameters may need to be adjusted depending on the quality of the reference data. Additionally, tutorial mentions that classifier performance may increase if the values for `nTopGenes` and `nTopGenePairs` are increased. +* In `scn_predict()`, we used parameter `nrand = 0` compared to the default `nrand = 50`. This parameter changes the number of randomized single cell RNA-seq profiles which serve as positive controls that should be mostly classified as `rand` (unknown) category. If left at default value, then this would generate extra cells that might complicate downstream consolidation of the consensus predictions for each cell. Therefore, the selected value is used to avoid complication. + +singleCellNet workflow was generated following the tutorial below: +https://pcahan1.github.io/singleCellNet/ diff --git a/Scripts/scLearn/predict_scLearn.R b/Scripts/scLearn/predict_scLearn.R new file mode 100644 index 0000000..8dfe62e --- /dev/null +++ b/Scripts/scLearn/predict_scLearn.R @@ -0,0 +1,71 @@ +# load libraries and arguments +library(tidyverse) +library(M3Drop) +library(scLearn) +library(glue) +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +query_path = args[1] +model_path = args[2] +pred_path = args[3] +threads = as.numeric(args[4]) +# species = args[5] # species="Hs" for homo sapiens or species="Mm" for mus musculus. + +# path for other outputs (depends on tools) +out_path = dirname(pred_path) + +#--------------- Data ------------------- + +# read query matrix +message('@ READ QUERY') +query = data.table::fread(query_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# load model +message('@ LOAD MODEL') +load(model_path) +message('@ DONE') + +# transpose query data so so cells are on columns +# Normalizing with Seurat, in order to not filter any cell. +query <- query %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() + +#----------- Predict scLearn -------- + +# CODE FOR PREDICTING HERE +message('@ PREDICT LABELS') + +# From documentation: "Assignment with trained model above. To get a less strict result for "unassigned" cells, you can decrease "diff" and "vote_rate". If you are sure that the cell type of query cells must be in the reference dataset, you can set "threshold_use" as FALSE. It means you don't want to use the thresholds learned by scLearn." +scLearn_predict_result <- scLearn_cell_assignment(scLearn_model_learning_result = scLearn, + expression_profile_query = query, + diff = 0.05, + threshold_use = TRUE, + vote_rate = 0.6) + +message('@ DONE') + + +pred_labs = data.frame(cell = scLearn_predict_result$Query_cell_id, + scLearn = scLearn_predict_result$Predict_cell_type) + +# write prediction +message('@ WRITE PREDICTIONS') +data.table::fwrite(pred_labs, + file = pred_path, + row.names = F, + col.names = T, + sep = ",", + nThread = threads) +message('@ DONE') +#---------------------------------------- +### Add the entire output with the Scores in Additional_information +message('@ WRITE TABLE OUTPUT WITH ALL PROBABILITIES') +data.table::fwrite(scLearn_predict_result, + file = glue('{out_path}/table_with_probabilities.csv'), + row.names = F, + col.names = T, + sep = ",", + nThread = threads) +message('@ DONE') \ No newline at end of file diff --git a/Scripts/scLearn/train_scLearn.R b/Scripts/scLearn/train_scLearn.R new file mode 100644 index 0000000..afaa463 --- /dev/null +++ b/Scripts/scLearn/train_scLearn.R @@ -0,0 +1,80 @@ +# load libraries and arguments +library(tidyverse) +library(M3Drop) +library(scLearn) +library(Seurat) +library(glue) +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +model_path = args[3] +threads = as.numeric(args[4]) +#species = args[5] # species="Hs" for homo sapiens or species="Mm" for mus musculus. + +# path for other outputs (depends on tools) +out_path = dirname(model_path) + +#--------------- Data ------------------- + +# read reference matrix +message('@ READ REF') +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# read reference labels +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# change labels format to named list (names = cell IDs, values = labels) +labels <- setNames(nm=rownames(labels), + object = labels$label) + + + +# Preprocessing +# transpose reference so cells are on columns +## Because we don't want to filter any cell, we will do only the normalization as they do it. +### They do manually the Seurat normalization. +# ref <-apply(ref,2,function(x){x/(sum(x)/10000)}) +# ref <- log(ref+1) + +ref <- ref %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() + +### Select genes +high_varGene_names <- Feature_selection_M3Drop(expression_profile = ref, + log_normalized = T #True because it was previously normalized + ) +#------------- Train scLearn ------------- + +# From documentation: "training the model. To improve the accuracy for "unassigned" cell, you can increase "bootstrap_times", but it will takes longer time. The default value of "bootstrap_times" is 10." +scLearn <- scLearn_model_learning(high_varGene_names = high_varGene_names, + expression_profile = as.matrix(ref), #It need a matrix not a sparse matrix. + sample_information_cellType = labels, + bootstrap_times = 10 #Default + ) + +# save trained model +message('@ SAVE MODEL') +save(scLearn, + file = model_path) +message('@ DONE') + +#--------------------------------------------- + +data.table::fwrite(data.frame(Selected_genes = scLearn$high_varGene_names), + file = glue('{out_path}/selected_genes.csv'), + row.names = F, + col.names = T, + sep = ",", + nThread = threads) diff --git a/Templates/train_templat.R b/Templates/train_template.R similarity index 100% rename from Templates/train_templat.R rename to Templates/train_template.R From 44098160819c309a6e0e319a2095643a621af1cc Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:50:08 -0400 Subject: [PATCH 028/144] Dev alva unify snakemake (#37) * moved old files to archive and added scClassify and SciBet to snakefile. Made new snakefile to avoid merge conflicts: snakemake.wf * minor updates to fix versions * rename report file * updated file * updated snakemake workflow to run more tools * added SVC to snakefile * added SVC and singleCellNet to snakefile * snakemake benchmark updated to include all tools. report for bechmark updated with tabs * updated benchamr snakefile to include final tools. made exmaple.config with everything needed to run current versions of snakefile.wf and snakefile.benchmark * fixed typos in snakefile.benchmark * added interactive plots to prediction notebook * finilized interactive plots in prediction report * updated benchmarking and prediction workflow with rules for correlation. works for test data * changed gather_predictions script to R. archived old scripts * workflow tested with module on narval. cleaned up repository. updated snakemake to run new consensus script * ACTINN included in snakemak.wf (not updated to run samples in paralell or split training prediction). Updated calculate consesnsus to output Unsure and No Consensus as separate categories * updated snakefile with scLearn rules * removed extra file * added all tools to benchmark snakefile. updated prediction report with seed and to save color for all classes * changed snakemake.wf to snakemake.annotate --- .../graph.benchmark-checkpoint.pdf | Bin 0 -> 8019 bytes .../2022-09-08T133151.890209.snakemake.log | 8 - .../2022-09-08T133238.758946.snakemake.log | 10 - .../2022-09-08T141158.777553.snakemake.log | 10 - .../2022-09-08T141421.312624.snakemake.log | 10 - .../2022-09-08T141503.422813.snakemake.log | 10 - .../2022-09-08T141620.098535.snakemake.log | 10 - .../2022-09-08T141718.523238.snakemake.log | 10 - .../2022-09-08T141852.782530.snakemake.log | 10 - .../2022-09-08T142121.740084.snakemake.log | 10 - .../2022-09-08T142322.175430.snakemake.log | 10 - .../2022-09-08T142344.721772.snakemake.log | 10 - .../2022-09-08T142356.762010.snakemake.log | 10 - .../2022-09-08T142404.703381.snakemake.log | 10 - .../2022-09-08T142436.949686.snakemake.log | 7 - .../2022-09-08T142557.810546.snakemake.log | 10 - .../2022-09-08T143045.014696.snakemake.log | 10 - .../2022-09-08T143846.878355.snakemake.log | 10 - .../2022-09-08T143915.339453.snakemake.log | 13 - .../2022-09-08T144001.049537.snakemake.log | 13 - .../2022-09-08T144242.060011.snakemake.log | 13 - .../2022-09-08T144432.308860.snakemake.log | 16 - .../2022-09-08T144609.966021.snakemake.log | 10 - .../2022-09-08T144635.458546.snakemake.log | 10 - .../2022-09-08T144749.295035.snakemake.log | 10 - .../2022-09-08T144812.039084.snakemake.log | 16 - .../2022-09-08T144829.502825.snakemake.log | 16 - .../2022-09-08T145000.697165.snakemake.log | 13 - .../2022-09-08T145117.724678.snakemake.log | 14 - .../2022-09-08T145147.458529.snakemake.log | 14 - .../2022-09-08T145333.876334.snakemake.log | 8 - .../2022-09-08T145358.279667.snakemake.log | 8 - .../2022-09-08T150009.566381.snakemake.log | 13 - .../2022-09-08T151241.745673.snakemake.log | 13 - .../2022-09-08T151446.425062.snakemake.log | 13 - .../2022-09-08T151659.197199.snakemake.log | 13 - .../2022-09-08T152841.441632.snakemake.log | 13 - .../2022-09-08T153008.691643.snakemake.log | 13 - .../2022-09-08T153018.042895.snakemake.log | 24 - .../2022-09-08T153714.016757.snakemake.log | 22 - Notebooks/annotate_report.Rmd | 354 +++++++++ Notebooks/benchmark_report.Rmd | 239 ++++++ .../actinn_format.py | 0 .../actinn_predict.py | 0 .../{predict_linealSVM.py => predict_SVM.py} | 31 +- Scripts/SVC/train_SVM.py | 6 +- ...{train_linealSVM.py => train_linearSVM.py} | 1 + Scripts/SciBet/predict_SciBet.R | 5 +- Scripts/SciBet/train_SciBet.R | 8 +- Scripts/SingleR/predict_SingleR.R | 2 +- Scripts/SingleR/train_SingleR.R | 4 +- .../archive/Benchmarking}/LICENSE | 0 .../Benchmarking}/Scripts/Cross_Validation.R | 0 .../Benchmarking}/Scripts/check_preds.R | 0 .../Benchmarking}/Scripts/complexity/F2.py | 0 .../Benchmarking}/Scripts/complexity/N1.py | 0 .../Benchmarking}/Scripts/complexity/prep.R | 0 .../archive/Benchmarking}/Scripts/evaluate.R | 0 .../Benchmarking}/Scripts/run_CHETAH.R | 0 .../Benchmarking}/Scripts/run_Correlation.R | 0 .../archive/Benchmarking}/Scripts/run_SVM.py | 0 .../Scripts/run_SVM_rejection.py | 0 .../Benchmarking}/Scripts/run_SciBet.R | 0 .../Benchmarking}/Scripts/run_SingleR.R | 0 .../Benchmarking}/Scripts/run_scClassify.R | 0 .../Benchmarking}/Scripts/run_scHPL.py | 0 .../Benchmarking}/Scripts/run_scPred.R | 0 .../archive/Benchmarking}/Scripts/run_scmap.R | 0 .../Benchmarking}/Scripts/run_scmapcell.R | 0 .../Benchmarking}/Scripts/run_scmapcluster.R | 0 .../Benchmarking}/Scripts/run_scmaptotal.R | 0 .../Benchmarking}/Scripts/run_singleCellNet.R | 0 .../archive/Benchmarking}/Snakefile | 0 .../archive/Benchmarking}/dag.pdf | Bin .../archive/Benchmarking}/requirements.txt | 0 Scripts/{ => archive}/Gather_Preds.py | 2 +- Scripts/{ => archive}/plot_preds.R | 0 Scripts/{ => archive}/run_SVM_reject.py | 0 Scripts/{ => archive}/run_SciBet.R | 0 Scripts/{ => archive}/run_SingleCellNet.R | 0 Scripts/{ => archive}/run_SingleR.R | 0 Scripts/{ => archive}/run_correlation.R | 0 Scripts/{ => archive}/run_scClassify.R | 0 Scripts/{ => archive}/run_scHPL.py | 0 Scripts/{ => archive}/run_scPred.R | 0 snakefile => Scripts/archive/snakefile | 0 .../style/theme_min_SelinJessa.R | 0 Scripts/benchmark/subset_folds.R | 81 +++ Scripts/calculate_consensus.R | 63 ++ Scripts/preprocess_v2.R | 49 ++ Scripts/run_ACTINN.py | 12 +- Scripts/scClassify/predict_scClassify.R | 7 +- Scripts/scClassify/train_scClassify.R | 14 +- Scripts/scHPL/predict_scHPL.py | 10 +- Scripts/scHPL/train_scHPL.py | 13 +- .../train_scLearn-checkpoint.R | 80 ++ Scripts/scPred/predict_scPred.R | 5 +- Scripts/scPred/train_scPred.R | 7 +- config.yml => example.config.yml | 28 +- example.submit.sh | 22 + rulegraph.pdf | Bin 10957 -> 14616 bytes setup.R | 3 - snakefile.annotate | 681 ++++++++++++++++++ snakefile.benchmark | 612 ++++++++++++++++ submit.sh | 22 - 105 files changed, 2267 insertions(+), 557 deletions(-) create mode 100644 .ipynb_checkpoints/graph.benchmark-checkpoint.pdf delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T133151.890209.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T133238.758946.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T141158.777553.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T141421.312624.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T141503.422813.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T141620.098535.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T141718.523238.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T141852.782530.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T142121.740084.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T142322.175430.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T142344.721772.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T142356.762010.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T142404.703381.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T142436.949686.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T142557.810546.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T143045.014696.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T143846.878355.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T143915.339453.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144001.049537.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144242.060011.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144432.308860.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144609.966021.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144635.458546.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144749.295035.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144812.039084.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T144829.502825.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T145000.697165.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T145117.724678.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T145147.458529.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T145333.876334.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T145358.279667.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T150009.566381.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T151241.745673.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T151446.425062.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T151659.197199.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T152841.441632.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T153008.691643.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T153018.042895.snakemake.log delete mode 100644 Benchmarking/.snakemake/log/2022-09-08T153714.016757.snakemake.log create mode 100644 Notebooks/annotate_report.Rmd create mode 100644 Notebooks/benchmark_report.Rmd rename Scripts/{ACTINN_scripts => ACTINN}/actinn_format.py (100%) rename Scripts/{ACTINN_scripts => ACTINN}/actinn_predict.py (100%) rename Scripts/SVC/{predict_linealSVM.py => predict_SVM.py} (74%) rename Scripts/SVC/{train_linealSVM.py => train_linearSVM.py} (98%) rename {Benchmarking => Scripts/archive/Benchmarking}/LICENSE (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/Cross_Validation.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/check_preds.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/complexity/F2.py (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/complexity/N1.py (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/complexity/prep.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/evaluate.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_CHETAH.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_Correlation.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_SVM.py (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_SVM_rejection.py (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_SciBet.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_SingleR.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_scClassify.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_scHPL.py (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_scPred.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_scmap.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_scmapcell.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_scmapcluster.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_scmaptotal.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Scripts/run_singleCellNet.R (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/Snakefile (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/dag.pdf (100%) rename {Benchmarking => Scripts/archive/Benchmarking}/requirements.txt (100%) rename Scripts/{ => archive}/Gather_Preds.py (99%) rename Scripts/{ => archive}/plot_preds.R (100%) rename Scripts/{ => archive}/run_SVM_reject.py (100%) rename Scripts/{ => archive}/run_SciBet.R (100%) rename Scripts/{ => archive}/run_SingleCellNet.R (100%) rename Scripts/{ => archive}/run_SingleR.R (100%) rename Scripts/{ => archive}/run_correlation.R (100%) rename Scripts/{ => archive}/run_scClassify.R (100%) rename Scripts/{ => archive}/run_scHPL.py (100%) rename Scripts/{ => archive}/run_scPred.R (100%) rename snakefile => Scripts/archive/snakefile (100%) rename Scripts/{ => archive}/style/theme_min_SelinJessa.R (100%) create mode 100644 Scripts/benchmark/subset_folds.R create mode 100644 Scripts/calculate_consensus.R create mode 100644 Scripts/preprocess_v2.R create mode 100644 Scripts/scLearn/.ipynb_checkpoints/train_scLearn-checkpoint.R rename config.yml => example.config.yml (81%) create mode 100644 example.submit.sh delete mode 100644 setup.R create mode 100644 snakefile.annotate create mode 100644 snakefile.benchmark delete mode 100644 submit.sh diff --git a/.ipynb_checkpoints/graph.benchmark-checkpoint.pdf b/.ipynb_checkpoints/graph.benchmark-checkpoint.pdf new file mode 100644 index 0000000000000000000000000000000000000000..34679187a89b376b9291d6982e4ea3e8a13430d0 GIT binary patch literal 8019 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 wAqlx6oLo9iPWVM9% + column_to_rownames('cellname') %>% + mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label, 'No Consensus'), ., 'Unsure'))) %>% + return() +} + +plot_tool_correlation_heatmap = function(seurat, tools){ + + mat = query@meta.data %>% + select(all_of(tools)) %>% + rownames_to_column('cell') %>% + pivot_longer(!cell) %>% + mutate(value = factor(value)) %>% + mutate(value = as.numeric(value)) %>% + pivot_wider(names_from = name, values_from = value) %>% + column_to_rownames('cell') %>% + cor() + + mat[is.na(mat)] = 0 + + col_fun = circlize::colorRamp2(c(-1, 0, 1), c("#2274A5", "beige", "#F75C03")) + + count = query@meta.data %>% + select(all_of(tools)) %>% + rownames_to_column('cell') %>% + pivot_longer(!cell) %>% + filter(value %in% c('Unsure', 'No Consensus')) %>% + dplyr::count(name, .drop = F) %>% + mutate(freq = round(n/nrow(query@meta.data)*100)) %>% + select(!n) %>% + column_to_rownames('name') + + count[setdiff(names(seurat@meta.data %>% select(tools)), rownames(count)),] = 0 + count = count[order(match(rownames(count), colnames(mat))), , drop = FALSE] + + ha = columnAnnotation('% Unsure/No Consensus' = anno_barplot(count, border = F, gp = gpar(fill = '#596475', col = '#596475'))) + + h = ComplexHeatmap::Heatmap(mat, + name = 'Correlation', + col = col_fun, + width = ncol(mat)*unit(7, "mm"), + height = nrow(mat)*unit(7, "mm"), + rect_gp = gpar(col = "white", lwd = 2), + top_annotation = ha, + show_column_dend = F) + return(h) +} + +create_color_pal = function(class, mb = 'Juarez'){ + pal = sample(met.brewer(mb, length(class))) + names(pal) = class + pal['Unsure'] = 'lightgrey' + pal['No Consensus'] = 'grey' + return(pal) +} + +plot_bar_largest_group = function(seurat, meta_column = '', palet = pal, fr = 0.1){ + +df = seurat@meta.data %>% + count(seurat_clusters, .data[[meta_column]]) %>% + group_by(seurat_clusters) %>% + mutate(`%` = (n / sum(n))) %>% + mutate(meta = ifelse(`%` < fr, NA, .data[[meta_column]])) + +pal = pal[unique(na.omit(df$meta))] + +df = df %>% + mutate(meta = factor(meta, levels = c(NA, names(pal)), exclude = NULL)) + +p1 = df %>% + ggplot(aes(x = seurat_clusters, y = `%`, fill = meta, text = sprintf(" %s
%s ", + meta, + scales::percent(`%`, scale = 100, accuracy = .1)))) + + geom_bar(stat = 'identity', position="fill") + + scale_fill_manual(values = pal, na.value = 'white', name = '') + + scale_y_continuous(expand = c(0,0), labels = scales::percent_format(scale = 100, accuracy = 1)) + + coord_flip() + + theme_bw() + + theme(text = element_text(size = 10), + axis.title = element_blank(), + axis.line = element_line(size = 0.5), + panel.border = element_blank(), + panel.grid = element_blank(), + strip.background = element_blank(), + aspect.ratio = 0.5) + + p2 = ggplotly(p1, tooltip = c('text')) + + return(p2) +} + +plot_percentage_predicted_consensus_class = function(seurat, tools){ + col_fun = circlize::colorRamp2(c(0, 50, 100), c("#2274A5", "beige", "#F75C03")) + +x = seurat@meta.data %>% + select(c('Consensus', tools)) %>% + group_by(Consensus) %>% + pivot_longer(!c('Consensus')) %>% + group_by(Consensus, name) %>% + count(value, .drop = F) %>% + mutate(freq = n/sum(n)*100) %>% + filter(Consensus == value) %>% + select(name, freq, Consensus) %>% + pivot_wider(values_from = 'freq', names_from = 'Consensus',values_fill = 0) %>% + column_to_rownames('name') + + h = ComplexHeatmap::Heatmap(x, + name = '%', + col = col_fun, + width = ncol(x)*unit(4, "mm"), + height = nrow(x)*unit(4, "mm"), + rect_gp = gpar(col = "white", lwd = 2), row_names_side = 'left', + show_row_dend = F, column_names_gp = gpar(size = 7)) + + return(h) +} + +color_class_seurat = function(seurat, meta_column, pal){ + list = list() + pal['Unsure'] = 'red' + pal['No Consensus'] = 'red' + Idents(seurat) = meta_column + class = (table(query@meta.data[[meta_column]]) %>% as.data.frame() %>% filter(Freq > 20))$Var + + for(c in class){ + lab = names(Idents(seurat)[Idents(seurat) == c]) + p = DimPlot(seurat, cells.highlight = lab, cols = 'lightgrey', cols.highlight = pal[c], pt.size = 1) + umap_theme + ggtitle(c) + list[[c]] = p + } + + return(list) +} + +feature_plot_seurat = function(seurat, genes){ + list = list() + + genes = genes[genes %in% rownames(seurat@assays$RNA)] + for(g in genes){ + p = FeaturePlot(seurat, features = g, cols = c("#F2EFC7", "#BC412B"), order = T) + umap_theme + ggtitle(g) + list[[g]] = p + } + return(list) +} + +umap_plotly = function(seurat, meta_column, pal){ + + p1 = cbind(seurat@reductions$umap@cell.embeddings, seurat@meta.data) %>% + slice(sample(1:n())) %>% + ggplot(aes(UMAP_1, UMAP_2, color = .data[[meta_column]], text = .data[[meta_column]])) + + geom_point(alpha = 0.8) + + scale_color_manual(values = pal) + + theme_bw() + umap_theme + theme(legend.position = 'right') + + p2 = ggplotly(plot = p1, tooltip = c('text')) %>% layout(autosize = F, width = 550, height = 450) + + return(p2) +} + +umap_theme = theme(aspect.ratio = 1, + text = element_text(size = 10), + axis.title = element_blank(), + axis.text = element_blank(), + axis.ticks = element_blank(), + axis.line = element_blank(), + panel.border = element_rect(colour = "grey", fill=NA, size=0.5), + panel.grid.major = element_blank(), + panel.grid.minor = element_blank(), + panel.background = element_blank(), + legend.position = "none") +``` + + +```{r} +# read labels from refrence (used to harmonize 'usure' call) +ref_labels = data.table::fread(params$ref_anno, header = T) %>% column_to_rownames('V1') + +# read prediction summary for each reference +list = list() +for(r in refs){ + list[[r]] = data.table::fread(paste0(params$pred_path, '/', params$sample, '/Prediction_Summary.tsv')) %>% + harmonize_unsure(., ref_labels) +} + +# read expression matrix for sample +query = data.table::fread(paste0(params$pred_path, '/', params$sample, '/expression.csv'), + nThread=threads, + header=T, + data.table=F) %>% + column_to_rownames('V1') +``` + +```{r} +# create seurat object from expression matrix +query = t(query) +query = CreateSeuratObject(query, row.names = colnames(query)) + +query = query %>% + NormalizeData() %>% + FindVariableFeatures() %>% + ScaleData() %>% + RunPCA() %>% + FindNeighbors(dims = 1:30) %>% + FindClusters(resolution = 0.5) %>% + RunUMAP(dims = 1:30) +``` + +```{r fig.width=8,fig.height=8,echo=FALSE,message=FALSE,results="asis"} +cat("\n") + +# create reference pal +pal = create_color_pal(ref_labels$label) +save(pal, file = paste0(params$pred_path, '/', params$sample, '/report/class_pal.Rda')) + +for(r in refs) { + + query = AddMetaData(query, list[[r]]) + + cat(" \n#", r, "{.tabset} \n") + + cat(" \n## Sample \n") + + cat("

Clusters

") + + p = umap_plotly(query, 'seurat_clusters', unname(pal)) + print(htmltools::tagList(p)) + + cat("\n") + + cat("

Expression selected genes

") + + if(!length(marker_genes) == 0){ + l = feature_plot_seurat(query, marker_genes) + if(length(marker_genes) < 9){ + cowplot::plot_grid(plotlist = l[1:9], ncol = 3) %>% print() + }else{ + for(i in seq(from = 1, by = 9, length.out = round(length(marker_genes)/9))){ + cowplot::plot_grid(plotlist = l[i:(i+8)], ncol = 3) %>% print() + } + } + } + + cat("\n") + + cat(" \n## Prediction QC \n") + + cat("

Correlation between tools

") + + h = plot_tool_correlation_heatmap(query, tools = tools) + draw(h) + + cat("

Percentage overlap between tools and consensus

") + + h = plot_percentage_predicted_consensus_class(query, tools = tools) + draw(h) + + cat("\n") + + cat(" \n## Prediction {.tabset} \n") + + for(t in tools){ + cat(" \n### ", t , " \n") + + cat("

Top class per cluster

") + + p = plot_bar_largest_group(query, t, fr = 0.1) + print(htmltools::tagList(p)) + + cat("

UMAP

") + + cat("\n") + + p = umap_plotly(query, t, pal) + print(htmltools::tagList(p)) + + cat("\n") + + cat("

UMAP per class

") + + l = color_class_seurat(query, t, pal) + if(length(l)< 9){ + cowplot::plot_grid(plotlist = l[1:9], ncol = 3) %>% print() + }else{ + for(i in seq(from = 1, by = 9, length.out = round(length(l)/9))){ + cowplot::plot_grid(plotlist = l[i:(i+8)], ncol = 3) %>% print() + } + } + + cat("\n") + } +} +``` + +# Report Info + +## Parameters + +```{r echo=FALSE,message=FALSE,results="asis"} +for(p in names(params)){ + cat(" \n -",p,": ", params[[p]], " \n") +} +``` + +## Session + +```{r} +sessionInfo() +``` diff --git a/Notebooks/benchmark_report.Rmd b/Notebooks/benchmark_report.Rmd new file mode 100644 index 0000000..17edeb8 --- /dev/null +++ b/Notebooks/benchmark_report.Rmd @@ -0,0 +1,239 @@ +--- +title: "scCoAnnotate - Benchmarking" +output: + html_document: + df_print: paged + theme: flatly + toc: yes + toc_float: yes + toc_depth: 1 + code_folding: hide +params: + tools: '' + ref_name: '' + pred_path: '' + fold: '' +--- + +```{r, echo=FALSE} +knitr::opts_chunk$set(message = FALSE, warning=FALSE) +``` + +```{r} +set.seed(1234) +library(tidyverse) +library(caret) +library(ComplexHeatmap) +``` + +```{r} +# Function to read true and predicted labels +# Returns a list object with 'pred' and 'true' +read_results = function(path, tool, fold){ + + # set up path to predicted and true labels + pred_path = paste0(path, '/fold', fold, '/', tool, '/', tool, '_pred.csv') + true_path = paste0(path, '/fold', fold, '/test_labels.csv') + + list = list() + list$true = data.table::fread(true_path, header = T) %>% + column_to_rownames('V1') %>% + mutate(label = factor(label, ordered = TRUE)) + + list$pred = data.table::fread(pred_path, header = T) %>% + column_to_rownames('cell') %>% + mutate(label = .data[[tool]], + label = ifelse(!label %in% list$true$label, NA, label), + label = factor(label, ordered = TRUE)) + + return(list) +} + +# Plot confusion matrix as a heatmap +plot_cm = function(cm_table){ + col_fun = circlize::colorRamp2(c(range(cm_table)[1], + range(cm_table)[2]/2, + range(cm_table)[2]), + c("#5C80BC", "#F2EFC7", "#FF595E")) + + h = Heatmap(cm_table, + name = 'Counts', + col = col_fun, + width = ncol(cm_table)*unit(2, "mm"), + height = nrow(cm_table)*unit(2, "mm"), + cluster_rows = F, + cluster_columns = F, + row_names_gp = gpar(fontsize = 7), + column_names_gp = gpar(fontsize = 7), + column_title = 'True Class', + row_title = 'Predicted Class') + + return(h) +} + +# Plot class stat per fold (F1 etc) as barplot +plot_stat = function(cm_byclass, stat){ +p = cm_byclass %>% + as.data.frame() %>% + rownames_to_column('class') %>% + separate(class, into = c(NA, 'class'), sep = ': ') %>% + ggplot(aes(reorder(class, -.data[[stat]]), .data[[stat]])) + + geom_bar(stat = 'identity', col = 'white', fill = 'lightgrey') + + theme_bw() + + theme(text = element_text(size = 10), + axis.title.x = element_blank(), + axis.line = element_line(size = 0.5), + panel.border = element_blank(), + panel.grid.major = element_blank(), + panel.grid.minor = element_blank(), + axis.text.x = element_text(angle = 45, + vjust = 1, + hjust=1), + aspect.ratio = 0.5) + + scale_y_continuous(expand = c(0, 0)) + + geom_hline(yintercept = c(1, 0.5), linetype = 'dotted', color = 'red') + +return(p) +} + +# plot F1 accross folds for each class as a boxplot +plot_stat_boxplot = function(list, tool, stat){ + +df = lapply(list[[tool]], get_stat, stat = stat) + +bind_rows(df) %>% + ggplot(aes(reorder(class, -.data[[stat]], mean), .data[[stat]])) + + geom_boxplot() + + theme_bw() + + theme(text = element_text(size = 10), + axis.title.x = element_blank(), + axis.line = element_line(size = 0.5), + panel.border = element_blank(), + panel.grid.major = element_blank(), + panel.grid.minor = element_blank(), + axis.text.x = element_text(angle = 45, + vjust = 1, + hjust=1), + aspect.ratio = 0.5) + + scale_y_continuous(expand = c(0, 0)) + + geom_hline(yintercept = c(1, 0.5), linetype = 'dotted', color = 'red') +} + +# plot average stat for all tools +plot_mean_tool = function(list, stat){ + +df = lapply(list, function(x){lapply(x, get_stat, stat = stat) %>% bind_rows()}) + +df = bind_rows(df) %>% + group_by(class, tool) %>% + mutate(mean = mean(.data[[stat]])) %>% + distinct(class, tool, mean) %>% + pivot_wider(names_from = 'class', values_from = mean) %>% + column_to_rownames('tool') + +df[is.na(df)] = 0 + +col_fun = circlize::colorRamp2(c(0, + range(df)[2]/2, + range(df)[2]), + c("#3B5B91", "#F2EFC7", "#CC0007")) + +h = Heatmap(df, + name = paste('Mean ', stat), + col = col_fun, + width = ncol(df)*unit(4, "mm"), + height = nrow(df)*unit(6, "mm"), + row_names_side = 'left', + row_names_gp = gpar(fontsize = 12), + show_column_dend = F, + show_row_dend = F) + +return(h) +} + +#--------- HELPER FUNCTIONS ---------------- + +# gets stat for each fold and returns data frame +get_stat = function(x, stat){ + x$byClass %>% + as.data.frame() %>% + rownames_to_column('class') %>% + separate(class, into = c(NA, 'class'), sep = ': ') %>% + select(class, .data[[stat]]) %>% + mutate(fold = x$fold, + tool = x$tool) +} +#------------------------------------------- +``` + +```{r} +tools = strsplit(params$tools, split = ' ')[[1]] +fold = as.numeric(params$fold) +``` + +```{r} +# Read prediction and true labels for each tool and each fold and calculate confusion matrix and stats +# Save everything in a list object with hierarchy TOOL > FOLD > STATS +list = list() +for(t in tools) { + for(n in 1:fold){ + tmp = read_results(params$pred_path, t, n) + list[[t]][[n]] = confusionMatrix(data = tmp$pred$label, reference = tmp$true$label, mode = 'everything') + list[[t]][[n]]$fold = paste0('fold', n) + list[[t]][[n]]$tool = t + } +} +``` + +```{r fig.width=10,echo=FALSE,message=FALSE,results="asis"} +cat(" \n#", params$ref_name , "{.tabset} \n") + +cat(" \n## Summary \n") +cat("

Average F1 score per tool and class

") + +plot_mean_tool(list, 'F1') + +cat("\n") + +for(t in tools) { + cat(" \n##", t, "{.tabset} \n") + + print(plot_stat_boxplot(list, t, 'F1')) + + cat("\n") + + for(n in 1:fold){ + cat(" \n###", paste0('Fold ', n), " \n") + + cat("

Confusion Matrix

") + + draw(plot_cm(list[[t]][[n]]$table)) + + cat("

F1

") + + print(plot_stat(list[[t]][[n]]$byClass, 'F1')) + + cat("\n") + } +} +``` + +# Report Info + +## Parameters +```{r echo=FALSE,message=FALSE,results="asis"} +for(p in names(params)){ + cat(" \n -",p,": ", params[[p]], " \n") +} +``` + + +## Session + +```{r} +sessionInfo() +``` + + + + diff --git a/Scripts/ACTINN_scripts/actinn_format.py b/Scripts/ACTINN/actinn_format.py similarity index 100% rename from Scripts/ACTINN_scripts/actinn_format.py rename to Scripts/ACTINN/actinn_format.py diff --git a/Scripts/ACTINN_scripts/actinn_predict.py b/Scripts/ACTINN/actinn_predict.py similarity index 100% rename from Scripts/ACTINN_scripts/actinn_predict.py rename to Scripts/ACTINN/actinn_predict.py diff --git a/Scripts/SVC/predict_linealSVM.py b/Scripts/SVC/predict_SVM.py similarity index 74% rename from Scripts/SVC/predict_linealSVM.py rename to Scripts/SVC/predict_SVM.py index a64af07..72b501a 100644 --- a/Scripts/SVC/predict_linealSVM.py +++ b/Scripts/SVC/predict_SVM.py @@ -21,12 +21,14 @@ random.seed(123456) #--------------- Parameters ------------------- -sample_path = str(sys.args[1]) -model_path = str(sys.args[2]) +sample_path = str(sys.argv[1]) +model_path = str(sys.argv[2]) out_path = str(sys.argv[3]) -out_other_path = os.path.dirname(str(sys.args[3])) -threshold = float(sys.args[4]) +out_other_path = os.path.dirname(str(sys.argv[3])) +threshold = float(sys.argv[4]) threads = int(sys.argv[5]) +tool_name = str(sys.argv[6]) + #--------------- Data ------------------------- print('@ READ QUERY') query = pd.read_csv(sample_path, @@ -36,10 +38,27 @@ print('@ DONE') +query = ad.AnnData(X = query, + obs = dict(obs_names=query.index.astype(str)), + var = dict(var_names=query.columns.astype(str)) +) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(query, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(query) + + # load model print('@ LOAD MODEL') SVM_model = pickle.load(open(model_path, 'rb')) print('@ DONE') + + ### If the trained model was generated with a diff number of threads and the ## prediction is done with other number SVM_model.n_jobs = threads @@ -65,8 +84,8 @@ def get_max_column_and_value(row): df['pred_label_reject'] = df.apply(lambda row: 'Unknown' if row['proba_label'] < threshold else row['pred_label'], axis=1) print('@ WRITTING PREDICTIONS') -pred_df = pd.DataFrame({'cell': df.index, 'SVM': df.pred_label_reject}) -pred_df.to_csv(out_path) +pred_df = pd.DataFrame({'cell': df.index, tool_name: df.pred_label_reject}) +pred_df.to_csv(out_path, index = False) print('@ DONE') #------------- Other outputs -------------- diff --git a/Scripts/SVC/train_SVM.py b/Scripts/SVC/train_SVM.py index e6f8ee2..369c352 100644 --- a/Scripts/SVC/train_SVM.py +++ b/Scripts/SVC/train_SVM.py @@ -24,9 +24,9 @@ ref_path = str(sys.argv[1]) lab_path = str(sys.argv[2]) out_path = str(sys.argv[3]) -out_other_path = os.path.dirname(str(sys.argv[4])) -classifier = str(sys.argv[5]) -threads = int(sys.argv[6]) +out_other_path = os.path.dirname(str(sys.argv[3])) +classifier = str(sys.argv[4]) +threads = int(sys.argv[5]) #--------------- Data ------------------------- # read the data diff --git a/Scripts/SVC/train_linealSVM.py b/Scripts/SVC/train_linearSVM.py similarity index 98% rename from Scripts/SVC/train_linealSVM.py rename to Scripts/SVC/train_linearSVM.py index 08d00cb..30a215a 100644 --- a/Scripts/SVC/train_linealSVM.py +++ b/Scripts/SVC/train_linearSVM.py @@ -24,6 +24,7 @@ ref_path = str(sys.argv[1]) lab_path = str(sys.argv[2]) out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.argv[3])) threads = int(sys.argv[4]) #--------------- Data ------------------------- diff --git a/Scripts/SciBet/predict_SciBet.R b/Scripts/SciBet/predict_SciBet.R index 16948e5..9ddd3a7 100644 --- a/Scripts/SciBet/predict_SciBet.R +++ b/Scripts/SciBet/predict_SciBet.R @@ -44,8 +44,9 @@ query <- Seurat::NormalizeData(query) %>% as.data.frame() %>% WGCNA::transposeBi pred <- Scibet_model(query, result = 'list') -pred_labels <- data.frame(SciBet = pred, - cell = rownames(query)) +pred_labels <- data.frame(cell = rownames(query), + SciBet = pred) + message('@ WRITTING PREDICTIONS') data.table::fwrite(pred_labels, file = pred_path, diff --git a/Scripts/SciBet/train_SciBet.R b/Scripts/SciBet/train_SciBet.R index 87e134d..77b9131 100644 --- a/Scripts/SciBet/train_SciBet.R +++ b/Scripts/SciBet/train_SciBet.R @@ -1,8 +1,8 @@ # load libraries and arguments library(data.table) library(scibet) -library(WGCNA) library(tidyverse) +library(WGCNA) library(Seurat) library(glue) set.seed(1234) @@ -24,7 +24,7 @@ ref <- data.table::fread(ref_path, header=T, nThread=threads) %>% column_to_rownames('V1') %>% - WGCNA::transposeBigData() + transposeBigData() message('@ DONE') @@ -45,7 +45,7 @@ if(!order){ ### The matrix here is transposed since SciBet expect a cell x gene matrix and ### converted to data.frame since labels need to be add. -ref <- Seurat::NormalizeData(ref) %>% as.data.frame() %>% WGCNA::transposeBigData() +ref <- NormalizeData(ref) %>% as.data.frame() %>% transposeBigData() ref$label <- labels$label @@ -101,4 +101,4 @@ data.table::fwrite(df_markers, nThread = threads ) message('@ DONE') -#---------------------------------------- \ No newline at end of file +#---------------------------------------- diff --git a/Scripts/SingleR/predict_SingleR.R b/Scripts/SingleR/predict_SingleR.R index 234d136..58d4765 100644 --- a/Scripts/SingleR/predict_SingleR.R +++ b/Scripts/SingleR/predict_SingleR.R @@ -26,7 +26,7 @@ load(model_path) message('@ DONE') # Make SingleCellExperiment object from query (transpose query first) -query = transposeBigData(query, blocksize = 10000) +query = transposeBigData(query) query = SingleCellExperiment(assays = list(counts = query)) # Log normalize query counts diff --git a/Scripts/SingleR/train_SingleR.R b/Scripts/SingleR/train_SingleR.R index f21127d..74b46b5 100644 --- a/Scripts/SingleR/train_SingleR.R +++ b/Scripts/SingleR/train_SingleR.R @@ -2,6 +2,7 @@ library(tidyverse) library(SingleR) library(SingleCellExperiment) +library(WGCNA) set.seed(1234) @@ -31,7 +32,7 @@ if(!order){ } # make SingleCellExperiment object (transpose ref first) -ref = t(ref) +ref = transposeBigData(ref) ref = SingleCellExperiment(assays = list(counts = ref)) # log normalize reference @@ -45,7 +46,6 @@ message('@ DONE') message('@ TRAINING MODEL') singler = trainSingleR(ref, labels=labels$label, - num.threads = threads, assay.type = "logcounts", de.method = "wilcox", de.n = 10) diff --git a/Benchmarking/LICENSE b/Scripts/archive/Benchmarking/LICENSE similarity index 100% rename from Benchmarking/LICENSE rename to Scripts/archive/Benchmarking/LICENSE diff --git a/Benchmarking/Scripts/Cross_Validation.R b/Scripts/archive/Benchmarking/Scripts/Cross_Validation.R similarity index 100% rename from Benchmarking/Scripts/Cross_Validation.R rename to Scripts/archive/Benchmarking/Scripts/Cross_Validation.R diff --git a/Benchmarking/Scripts/check_preds.R b/Scripts/archive/Benchmarking/Scripts/check_preds.R similarity index 100% rename from Benchmarking/Scripts/check_preds.R rename to Scripts/archive/Benchmarking/Scripts/check_preds.R diff --git a/Benchmarking/Scripts/complexity/F2.py b/Scripts/archive/Benchmarking/Scripts/complexity/F2.py similarity index 100% rename from Benchmarking/Scripts/complexity/F2.py rename to Scripts/archive/Benchmarking/Scripts/complexity/F2.py diff --git a/Benchmarking/Scripts/complexity/N1.py b/Scripts/archive/Benchmarking/Scripts/complexity/N1.py similarity index 100% rename from Benchmarking/Scripts/complexity/N1.py rename to Scripts/archive/Benchmarking/Scripts/complexity/N1.py diff --git a/Benchmarking/Scripts/complexity/prep.R b/Scripts/archive/Benchmarking/Scripts/complexity/prep.R similarity index 100% rename from Benchmarking/Scripts/complexity/prep.R rename to Scripts/archive/Benchmarking/Scripts/complexity/prep.R diff --git a/Benchmarking/Scripts/evaluate.R b/Scripts/archive/Benchmarking/Scripts/evaluate.R similarity index 100% rename from Benchmarking/Scripts/evaluate.R rename to Scripts/archive/Benchmarking/Scripts/evaluate.R diff --git a/Benchmarking/Scripts/run_CHETAH.R b/Scripts/archive/Benchmarking/Scripts/run_CHETAH.R similarity index 100% rename from Benchmarking/Scripts/run_CHETAH.R rename to Scripts/archive/Benchmarking/Scripts/run_CHETAH.R diff --git a/Benchmarking/Scripts/run_Correlation.R b/Scripts/archive/Benchmarking/Scripts/run_Correlation.R similarity index 100% rename from Benchmarking/Scripts/run_Correlation.R rename to Scripts/archive/Benchmarking/Scripts/run_Correlation.R diff --git a/Benchmarking/Scripts/run_SVM.py b/Scripts/archive/Benchmarking/Scripts/run_SVM.py similarity index 100% rename from Benchmarking/Scripts/run_SVM.py rename to Scripts/archive/Benchmarking/Scripts/run_SVM.py diff --git a/Benchmarking/Scripts/run_SVM_rejection.py b/Scripts/archive/Benchmarking/Scripts/run_SVM_rejection.py similarity index 100% rename from Benchmarking/Scripts/run_SVM_rejection.py rename to Scripts/archive/Benchmarking/Scripts/run_SVM_rejection.py diff --git a/Benchmarking/Scripts/run_SciBet.R b/Scripts/archive/Benchmarking/Scripts/run_SciBet.R similarity index 100% rename from Benchmarking/Scripts/run_SciBet.R rename to Scripts/archive/Benchmarking/Scripts/run_SciBet.R diff --git a/Benchmarking/Scripts/run_SingleR.R b/Scripts/archive/Benchmarking/Scripts/run_SingleR.R similarity index 100% rename from Benchmarking/Scripts/run_SingleR.R rename to Scripts/archive/Benchmarking/Scripts/run_SingleR.R diff --git a/Benchmarking/Scripts/run_scClassify.R b/Scripts/archive/Benchmarking/Scripts/run_scClassify.R similarity index 100% rename from Benchmarking/Scripts/run_scClassify.R rename to Scripts/archive/Benchmarking/Scripts/run_scClassify.R diff --git a/Benchmarking/Scripts/run_scHPL.py b/Scripts/archive/Benchmarking/Scripts/run_scHPL.py similarity index 100% rename from Benchmarking/Scripts/run_scHPL.py rename to Scripts/archive/Benchmarking/Scripts/run_scHPL.py diff --git a/Benchmarking/Scripts/run_scPred.R b/Scripts/archive/Benchmarking/Scripts/run_scPred.R similarity index 100% rename from Benchmarking/Scripts/run_scPred.R rename to Scripts/archive/Benchmarking/Scripts/run_scPred.R diff --git a/Benchmarking/Scripts/run_scmap.R b/Scripts/archive/Benchmarking/Scripts/run_scmap.R similarity index 100% rename from Benchmarking/Scripts/run_scmap.R rename to Scripts/archive/Benchmarking/Scripts/run_scmap.R diff --git a/Benchmarking/Scripts/run_scmapcell.R b/Scripts/archive/Benchmarking/Scripts/run_scmapcell.R similarity index 100% rename from Benchmarking/Scripts/run_scmapcell.R rename to Scripts/archive/Benchmarking/Scripts/run_scmapcell.R diff --git a/Benchmarking/Scripts/run_scmapcluster.R b/Scripts/archive/Benchmarking/Scripts/run_scmapcluster.R similarity index 100% rename from Benchmarking/Scripts/run_scmapcluster.R rename to Scripts/archive/Benchmarking/Scripts/run_scmapcluster.R diff --git a/Benchmarking/Scripts/run_scmaptotal.R b/Scripts/archive/Benchmarking/Scripts/run_scmaptotal.R similarity index 100% rename from Benchmarking/Scripts/run_scmaptotal.R rename to Scripts/archive/Benchmarking/Scripts/run_scmaptotal.R diff --git a/Benchmarking/Scripts/run_singleCellNet.R b/Scripts/archive/Benchmarking/Scripts/run_singleCellNet.R similarity index 100% rename from Benchmarking/Scripts/run_singleCellNet.R rename to Scripts/archive/Benchmarking/Scripts/run_singleCellNet.R diff --git a/Benchmarking/Snakefile b/Scripts/archive/Benchmarking/Snakefile similarity index 100% rename from Benchmarking/Snakefile rename to Scripts/archive/Benchmarking/Snakefile diff --git a/Benchmarking/dag.pdf b/Scripts/archive/Benchmarking/dag.pdf similarity index 100% rename from Benchmarking/dag.pdf rename to Scripts/archive/Benchmarking/dag.pdf diff --git a/Benchmarking/requirements.txt b/Scripts/archive/Benchmarking/requirements.txt similarity index 100% rename from Benchmarking/requirements.txt rename to Scripts/archive/Benchmarking/requirements.txt diff --git a/Scripts/Gather_Preds.py b/Scripts/archive/Gather_Preds.py similarity index 99% rename from Scripts/Gather_Preds.py rename to Scripts/archive/Gather_Preds.py index e56f423..8e47698 100644 --- a/Scripts/Gather_Preds.py +++ b/Scripts/archive/Gather_Preds.py @@ -61,7 +61,7 @@ def run_concat(Results_Folder_Path, tools_for_consensus): for path in paths[1:]: current = pd.read_csv(path, index_col = 0) result = pd.concat([result, current], axis = 1) - + # case 1: get consensus of all tools if tools_for_consensus[0] == 'all': mode = result.select_dtypes(exclude=['float64']).mode(axis = 1) diff --git a/Scripts/plot_preds.R b/Scripts/archive/plot_preds.R similarity index 100% rename from Scripts/plot_preds.R rename to Scripts/archive/plot_preds.R diff --git a/Scripts/run_SVM_reject.py b/Scripts/archive/run_SVM_reject.py similarity index 100% rename from Scripts/run_SVM_reject.py rename to Scripts/archive/run_SVM_reject.py diff --git a/Scripts/run_SciBet.R b/Scripts/archive/run_SciBet.R similarity index 100% rename from Scripts/run_SciBet.R rename to Scripts/archive/run_SciBet.R diff --git a/Scripts/run_SingleCellNet.R b/Scripts/archive/run_SingleCellNet.R similarity index 100% rename from Scripts/run_SingleCellNet.R rename to Scripts/archive/run_SingleCellNet.R diff --git a/Scripts/run_SingleR.R b/Scripts/archive/run_SingleR.R similarity index 100% rename from Scripts/run_SingleR.R rename to Scripts/archive/run_SingleR.R diff --git a/Scripts/run_correlation.R b/Scripts/archive/run_correlation.R similarity index 100% rename from Scripts/run_correlation.R rename to Scripts/archive/run_correlation.R diff --git a/Scripts/run_scClassify.R b/Scripts/archive/run_scClassify.R similarity index 100% rename from Scripts/run_scClassify.R rename to Scripts/archive/run_scClassify.R diff --git a/Scripts/run_scHPL.py b/Scripts/archive/run_scHPL.py similarity index 100% rename from Scripts/run_scHPL.py rename to Scripts/archive/run_scHPL.py diff --git a/Scripts/run_scPred.R b/Scripts/archive/run_scPred.R similarity index 100% rename from Scripts/run_scPred.R rename to Scripts/archive/run_scPred.R diff --git a/snakefile b/Scripts/archive/snakefile similarity index 100% rename from snakefile rename to Scripts/archive/snakefile diff --git a/Scripts/style/theme_min_SelinJessa.R b/Scripts/archive/style/theme_min_SelinJessa.R similarity index 100% rename from Scripts/style/theme_min_SelinJessa.R rename to Scripts/archive/style/theme_min_SelinJessa.R diff --git a/Scripts/benchmark/subset_folds.R b/Scripts/benchmark/subset_folds.R new file mode 100644 index 0000000..aa442c3 --- /dev/null +++ b/Scripts/benchmark/subset_folds.R @@ -0,0 +1,81 @@ +# load libraries and arguments +library(rBayesianOptimization) +library(tidyverse) + +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +out_path = args[3] +threads = as.numeric(args[4]) +n_folds = as.numeric(args[5]) + +#--------------- Data ------------------- + +# read reference matrix +message('@ READ REF') +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# read reference labels +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +ref[1:10,1:10] +head(labels) + +# create n folds +folds = KFold(labels$label, + nfolds = n_folds, + stratified = T, + seed = 1234) +head(folds) + +# Loop through folds and save training and testing data sets +for (i in 1:n_folds){ + message(paste0('@ SAVING FOLD ', i)) + + print(head(folds[[i]])) + + # subset test fold + message('subset test fold') + test = ref[folds[[i]], ,drop=F] + test = test %>% rownames_to_column("cell") + colnames(test)[1] = "" + + # subset true test labels + message('subset true test labels') + test_labels = labels[folds[[i]], ,drop=F] + test_labels = test_labels %>% rownames_to_column("cell") + colnames(test_labels)[1] = "" + + # subset training data + message('subset true test labels') + train = ref[-folds[[i]], ,drop=F] + train = train %>% rownames_to_column("cell") + colnames(train)[1] = "" + + # subset labels for training data + message('@ subset labels for training data') + train_labels = labels[-folds[[i]], ,drop=F] + train_labels = train_labels %>% rownames_to_column("cell") + colnames(train_labels)[1] = "" + + # save csv files + data.table::fwrite(test, paste0(out_path, '/fold', i, '/test.csv')) + data.table::fwrite(test_labels, paste0(out_path, '/fold', i, '/test_labels.csv')) + data.table::fwrite(train, paste0(out_path, '/fold', i, '/train.csv')) + data.table::fwrite(train_labels, paste0(out_path, '/fold', i, '/train_labels.csv')) +} + + diff --git a/Scripts/calculate_consensus.R b/Scripts/calculate_consensus.R new file mode 100644 index 0000000..205e0e4 --- /dev/null +++ b/Scripts/calculate_consensus.R @@ -0,0 +1,63 @@ + +library(tidyverse) + +args = commandArgs(trailingOnly = TRUE) +pred_path = args[1] +summary_path = args[2] +tools = strsplit(args[3], split = ' ')[[1]] +consensus_tools = strsplit(args[4], split = ' ')[[1]] +ref_lab = args[5] + +print(tools) +print(consensus_tools) + +if(consensus_tools[1] == 'all'){ + consensus_tools = tools +} + +print(consensus_tools) + +harmonize_unsure = function(pred, ref_labels){ + pred %>% + column_to_rownames('cellname') %>% + mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label), ., 'Unsure'))) %>% + rownames_to_column('cellname') %>% + return() +} + +getmode = function(v) { + uniqv = unique(v) + matches = tabulate(match(v, uniqv)) + max_match = max(matches) + + ties = ifelse(length(which(matches == max_match)) > 1, T, F) + + if (max_match == 1) { + return("No Consensus") + } else if (ties) { + return("No Consensus") + } else { + return(uniqv[which.max(matches)]) + } +} + +ref_labels = data.table::fread(ref_lab, header = T) %>% column_to_rownames('V1') + +files =list.files(pred_path, pattern = 'pred.csv', recursive = T, full.names = T) + +l = list() +for(f in files){ + l[[basename(dirname(f))]] = data.table::fread(f) +} + +consensus = l %>% reduce(left_join, by = "cell") %>% rename('cellname' = 'cell') +rm(l) + +tmp = harmonize_unsure(consensus, ref_labels) +tmp = tmp %>% select(all_of(consensus_tools)) +consensus$Consensus = apply(tmp, 1, getmode) +rm(tmp) + +data.table::fwrite(consensus, summary_path, sep = '\t') + + diff --git a/Scripts/preprocess_v2.R b/Scripts/preprocess_v2.R new file mode 100644 index 0000000..2e6dcc6 --- /dev/null +++ b/Scripts/preprocess_v2.R @@ -0,0 +1,49 @@ + +library(tidyverse) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +query_paths = args[2] +query_paths = strsplit(query_paths, split = ' ')[[1]] +out_path = args[3] + +paths = c(ref_path, query_paths) +print(paths) + +l = list() +for(i in 1:length(paths)){ + tmp = data.table::fread(paths[i], header = T) %>% column_to_rownames('V1') + l[[i]] = tmp +} + +# get genes for each data frame (colnames) +genes = lapply(l, function(x){(colnames(x))}) + +# reduce set of genes to the intersect +genes = Reduce(intersect, genes) + +# save common genes +data.table::fwrite(data.frame('common_genes' = genes)) + +# filter expression data to the intersect +for(i in 1:length(l)){ + tmp = l[[i]][,genes] + + # calculate fraction of genes kept + fr = (ncol(tmp)/ncol(l[[i]])) + + # throw error if fraction is less than 0.5 + if(fr < 0.5){ + stop(paste(" ", + "@ Number of genes after filtering is less than 50% of original data for sample:", + paths[i], + sep="\n")) + } + + # save filtered data + if(i == 1){ + data.table::fwrite(tmp, ) + }else{ + data.table::fwrite(tmp) + } +} diff --git a/Scripts/run_ACTINN.py b/Scripts/run_ACTINN.py index 774ae73..dc1f976 100644 --- a/Scripts/run_ACTINN.py +++ b/Scripts/run_ACTINN.py @@ -52,7 +52,7 @@ def run_ACTINN(RefPath, LabelsPath, QueryPaths, OutputDirs): tm.sleep(60) # RUN ACTINN formatting - os.system("python ../Scripts/ACTINN_scripts/actinn_format.py -i train.csv -o train -f csv") + os.system("python /lustre06/project/6004736/alvann/from_narval/DEV/scCoAnnotate-dev/Scripts/ACTINN/actinn_format.py -i train.csv -o train -f csv") i = 0 for Query in QueryPaths: @@ -66,24 +66,24 @@ def run_ACTINN(RefPath, LabelsPath, QueryPaths, OutputDirs): Query = np.log1p(Query) Query = Query.transpose() Query.to_csv("Query.csv") - os.system("python ../Scripts/ACTINN_scripts/actinn_format.py -i Query.csv -o Query -f csv") + os.system("python /lustre06/project/6004736/alvann/from_narval/DEV/scCoAnnotate-dev/Scripts/ACTINN/actinn_format.py -i Query.csv -o Query -f csv") # measure total time start = tm.time() # execute the actinn prediction file - os.system("python ../Scripts/ACTINN_scripts/actinn_predict.py -trs train.h5 -trl train_lab.csv -ts Query.h5 -op False ") + os.system("python /lustre06/project/6004736/alvann/from_narval/DEV/scCoAnnotate-dev/Scripts/ACTINN/actinn_predict.py -trs train.h5 -trl train_lab.csv -ts Query.h5 -op False ") tot.append(tm.time()-start) tm.sleep(60) # read predictions and probabilities - predlabels = pd.read_csv('{tmp}/predicted_label.txt'.format(tmp = tmp_path),header=0,index_col=0, sep='\t') + predlabels = pd.read_csv('{tmp}/predicted_label.txt'.format(tmp = tmp_path),sep='\t') - pred = pd.DataFrame({"ACTINN":predlabels['celltype']}, index = predlabels.index) + pred = pd.DataFrame({"cell":predlabels["cellname"], "ACTINN":predlabels["celltype"]}) tot_time = pd.DataFrame(tot) # output the files os.chdir(path) - pred.to_csv("ACTINN_pred.csv", index = True) + pred.to_csv("ACTINN_pred.csv", index = False) tot_time.to_csv("ACTINN_training_time.csv", index = False) tot_time.to_csv("ACTINN_query_time.csv", index = False) i = i+1 diff --git a/Scripts/scClassify/predict_scClassify.R b/Scripts/scClassify/predict_scClassify.R index 75539be..51d8a49 100644 --- a/Scripts/scClassify/predict_scClassify.R +++ b/Scripts/scClassify/predict_scClassify.R @@ -4,10 +4,9 @@ # load libraries and arguments library(data.table) library(Seurat) -library(WGCNA) library(scClassify) -library(dplyr) -library(tibble) +library(tidyverse) +library(WGCNA) set.seed(1234) @@ -35,7 +34,7 @@ load(model_path) message('@ DONE') # transpose (put cells in columns) for Seurat normalization and scClassify, normalize -query <- query %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() +query <- query %>% transposeBigData() %>% Seurat::NormalizeData() #----------- Predict scClassify -------- diff --git a/Scripts/scClassify/train_scClassify.R b/Scripts/scClassify/train_scClassify.R index 237a06c..9c5b6c3 100644 --- a/Scripts/scClassify/train_scClassify.R +++ b/Scripts/scClassify/train_scClassify.R @@ -4,12 +4,11 @@ # load libraries and arguments library(data.table) library(Seurat) -library(WGCNA) library(scClassify) -library(dplyr) -library(tibble) +library(tidyverse) library(ggplot2) library(glue) +library(WGCNA) set.seed(1234) @@ -26,8 +25,9 @@ out_path = dirname(model_path) # read reference matrix message('@ READ REF') -ref <- data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% - column_to_rownames('V1') +ref <- data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames("V1") +message('@ DONE') # read reference labels labels <- data.table::fread(lab_path, header=T, data.table=F) %>% @@ -43,7 +43,7 @@ if(!order){ } # transpose (put cells in columns) for Seurat normalization and scClassify, normalize -ref <- ref %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() +ref <- ref %>% transposeBigData() %>% Seurat::NormalizeData() #------------- Train scClassify ------------- @@ -89,4 +89,4 @@ tree_plot <- plotCellTypeTree(tree) ggsave(tree_plot, file = glue("{out_path}/scClassify_tree.png")) message('@ DONE') -#--------------------------------------------- \ No newline at end of file +#--------------------------------------------- diff --git a/Scripts/scHPL/predict_scHPL.py b/Scripts/scHPL/predict_scHPL.py index 1090cce..0065f02 100644 --- a/Scripts/scHPL/predict_scHPL.py +++ b/Scripts/scHPL/predict_scHPL.py @@ -19,11 +19,11 @@ random.seed(123456) #--------------- Parameters ------------------- -sample_path = str(sys.args[1]) -model_path = str(sys.args[2]) +sample_path = str(sys.argv[1]) +model_path = str(sys.argv[2]) out_path = str(sys.argv[3]) -out_other_path = os.path.dirname(str(sys.args[3])) -threshold = float(sys.args[4]) +out_other_path = os.path.dirname(str(sys.argv[3])) +threshold = float(sys.argv[4]) #threads = as.numeric(args[4]) #--------------- Data ------------------- @@ -67,6 +67,6 @@ print('@ WRITTING PREDICTIONS') pred_labels = pd.DataFrame({'cell': query.obs_names, 'scHPL': pred}) -pred_labels.to_csv(out_path) +pred_labels.to_csv(out_path, index = False) print('@ DONE') #---------------------------------------- diff --git a/Scripts/scHPL/train_scHPL.py b/Scripts/scHPL/train_scHPL.py index 9a84feb..37a7d39 100644 --- a/Scripts/scHPL/train_scHPL.py +++ b/Scripts/scHPL/train_scHPL.py @@ -9,7 +9,8 @@ #--------------- Libraries ------------------- import numpy as np import pandas as pd -from scHPL import train, learn, utils, TreeNode, _print_node, _count_nodes +from scHPL import learn, train, utils +from scHPL.utils import TreeNode, _print_node, _count_nodes from matplotlib import pyplot as plt import matplotlib.lines as mlines import anndata as ad @@ -18,6 +19,7 @@ import pickle import os import random + ### Set seed random.seed(123456) @@ -26,8 +28,8 @@ lab_path = str(sys.argv[2]) out_path = str(sys.argv[3]) out_other_path = os.path.dirname(str(sys.argv[3])) -classifier = str(sys.argv[5]) -dimred = bool(sys.argv[6]) +classifier = str(sys.argv[4]) +dimred = bool(sys.argv[5]) #--------------- Data ------------------------- # read the data @@ -95,11 +97,6 @@ #------------- Other outputs -------------- ### Plot the tree -from scHPL.utils import -from matplotlib import pyplot as plt -import matplotlib.lines as mlines -import numpy as np - #I'm using this method since they provided it here: #https://github.com/lcmmichielsen/scHPL/issues/5 def _print_node(node, hor, ver_steps, fig, new_nodes): diff --git a/Scripts/scLearn/.ipynb_checkpoints/train_scLearn-checkpoint.R b/Scripts/scLearn/.ipynb_checkpoints/train_scLearn-checkpoint.R new file mode 100644 index 0000000..afaa463 --- /dev/null +++ b/Scripts/scLearn/.ipynb_checkpoints/train_scLearn-checkpoint.R @@ -0,0 +1,80 @@ +# load libraries and arguments +library(tidyverse) +library(M3Drop) +library(scLearn) +library(Seurat) +library(glue) +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +model_path = args[3] +threads = as.numeric(args[4]) +#species = args[5] # species="Hs" for homo sapiens or species="Mm" for mus musculus. + +# path for other outputs (depends on tools) +out_path = dirname(model_path) + +#--------------- Data ------------------- + +# read reference matrix +message('@ READ REF') +ref = data.table::fread(ref_path, nThread=threads, header=T, data.table=F) %>% + column_to_rownames('V1') +message('@ DONE') + +# read reference labels +labels = data.table::fread(lab_path, header=T, data.table=F) %>% + column_to_rownames('V1') + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(rownames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# change labels format to named list (names = cell IDs, values = labels) +labels <- setNames(nm=rownames(labels), + object = labels$label) + + + +# Preprocessing +# transpose reference so cells are on columns +## Because we don't want to filter any cell, we will do only the normalization as they do it. +### They do manually the Seurat normalization. +# ref <-apply(ref,2,function(x){x/(sum(x)/10000)}) +# ref <- log(ref+1) + +ref <- ref %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() + +### Select genes +high_varGene_names <- Feature_selection_M3Drop(expression_profile = ref, + log_normalized = T #True because it was previously normalized + ) +#------------- Train scLearn ------------- + +# From documentation: "training the model. To improve the accuracy for "unassigned" cell, you can increase "bootstrap_times", but it will takes longer time. The default value of "bootstrap_times" is 10." +scLearn <- scLearn_model_learning(high_varGene_names = high_varGene_names, + expression_profile = as.matrix(ref), #It need a matrix not a sparse matrix. + sample_information_cellType = labels, + bootstrap_times = 10 #Default + ) + +# save trained model +message('@ SAVE MODEL') +save(scLearn, + file = model_path) +message('@ DONE') + +#--------------------------------------------- + +data.table::fwrite(data.frame(Selected_genes = scLearn$high_varGene_names), + file = glue('{out_path}/selected_genes.csv'), + row.names = F, + col.names = T, + sep = ",", + nThread = threads) diff --git a/Scripts/scPred/predict_scPred.R b/Scripts/scPred/predict_scPred.R index c3cf2b9..a0f7b43 100644 --- a/Scripts/scPred/predict_scPred.R +++ b/Scripts/scPred/predict_scPred.R @@ -2,6 +2,7 @@ library(scPred) library(Seurat) library(tidyverse) +library(WGCNA) set.seed(1234) @@ -25,8 +26,8 @@ load(model_path) message('@ DONE') # transpose and create seurat object -query = t(query) -query = CreateSeuratObject(query, row.names = colnames(query)) +query = transposeBigData(query) +query = CreateSeuratObject(query) # normalize query query = query %>% diff --git a/Scripts/scPred/train_scPred.R b/Scripts/scPred/train_scPred.R index cb3631b..7b91c3c 100644 --- a/Scripts/scPred/train_scPred.R +++ b/Scripts/scPred/train_scPred.R @@ -3,6 +3,7 @@ library(scPred) library(Seurat) library(tidyverse) library(doParallel) +library(WGCNA) set.seed(1234) @@ -37,8 +38,8 @@ if(!order){ } # transpose and create seurat object with the labels as meta data -ref = t(ref) -ref = CreateSeuratObject(ref, row.names = colnames(ref), meta.data = labels) +ref = transposeBigData(ref) +ref = CreateSeuratObject(ref, meta.data = labels) # normalize reference ref = ref %>% @@ -63,7 +64,7 @@ stopCluster(cl) get_scpred(scpred) # Plot prob -pdf(paste0(out_path, 'qc_plots.pdf'), width=10, height=10) +pdf(paste0(out_path, '/qc_plots.pdf'), width=10, height=10) plot_probabilities(scpred) dev.off() diff --git a/config.yml b/example.config.yml similarity index 81% rename from config.yml rename to example.config.yml index eb233d2..ff2d7c9 100644 --- a/config.yml +++ b/example.config.yml @@ -1,5 +1,8 @@ # target directory -output_dir: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev/out +output_dir: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev/out/ + +references: 'Braindex' +marker_genes: 'PDGFRA OLIG2 PTPRC' # path to reference to train classifiers on (cell x gene raw counts) training_reference: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/expression.csv @@ -16,24 +19,19 @@ query_datasets: check_genes: False genes_required: Null -# rejection option for SVM -rejection: True # classifiers to run tools_to_run: + - scPred - SingleR - - correlation + - scClassify + - SciBet + - singleCellNet + - scHPL + - SVMlinear + - Correlation # benchmark tools on reference -benchmark: False - -plots: False -consensus: - - all - - - - - - +benchmark: + n_folds: 3 diff --git a/example.submit.sh b/example.submit.sh new file mode 100644 index 0000000..d598d1a --- /dev/null +++ b/example.submit.sh @@ -0,0 +1,22 @@ +#!/bin/sh +#SBATCH --job-name=scCoAnnotate +#SBATCH --account=rrg-kleinman +#SBATCH --output=logs/%x.out +#SBATCH --error=logs/%x.err +#SBATCH --ntasks=1 +#SBATCH --cpus-per-task= +#SBATCH --time= +#SBATCH --mem-per-cpu= + +module load scCoAnnotate/2.0 + +# path to snakefile and config +snakefile="" +config="" + +# unlock directory incase of previous errors +snakemake -s ${snakefile} --configfile ${config} --unlock + +# run workflow +snakemake -s ${snakefile} --configfile ${config} --cores 2 + diff --git a/rulegraph.pdf b/rulegraph.pdf index d17bc7e55d9aef05ae4000550b0ed2baa2812a97..2dee3a0379ded8223822d1b0969c0e2f35c39527 100644 GIT binary patch delta 13410 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 Ap#T5? delta 9669 zcmZX3byOTpmp8%PUBci_W`Kdg-7P?H2=4CA1Sfca;FX!O z&~}dv(&Ou=St?ypD?6LqPGh&21?qHDg7N5W%`m8gfRLEe@ukSKC4>ql>LZUqn45! zAReG~0gK!k%>k`IGdH?!#_?*&O}@{4+ul^_N6teIhZhgi{X#umz!tFb+<-VW_YkPxg0PhPhGiv!W{5J)Xs#4&?T2*qX4j7-pkLC zg-c$W3^sl{ajJ0~On&|RK20@3Pm@)Ea9q4^<4#$pzsI5M@jFgYqvh4gVc{11f_?Ocan3B*vJEH#D;i7!X2m9=UD41P4a-j9YZHc5Bw zFJ+Xkt{1c6*JtiyfR#?(g$?*Y65?$19o2YQ)LaJ?;Sfntu8>_f?e}p7d#*fI0x}w?%{2JuWX*ybg+Xo=l$_lxjSP259NVzhc1Abcic9|QS`M23EENAvr~ zTQK@hAUTKmVh>;;hGdGWZKsdX2_`ez;aOn!L{`{SsC`Olv@?hB(ZkE zo3M2XF<(!gi?>bXFrLTF1ZDd?CclVCGh!Ui)RUk`=f)wywy;B z9t_TJRSPdtCdAtlWFxg}GDSH9e_m^zqWq5?Hs<=n7T&+TvvEC}ZA);AXSQ*(QToC( z&G5yMhOJ|Uj9k;2Ls1V#?0rv_Zb7zG$9@MFe0*cwbI|)x2r>_vJJTd+Qi6UdPrwB} z)XEb&s4Gje)p%0lQ@wS?l&sLH*mnxGW_=TTXX$19{YhjTpDOK1WS$h^wT!}}HOfeW zB3cl+_BgHES+yTaHAO-Z6qQ?0@X2gm@uyu52Qd$6H*r|uxZZK8ky=^%NP~kUt69eS z#q;!6OAhCW2vUpCKZBZ-@d?yL_PZHSfo}HG7r1HSIi@g!j6erqWSU zk*>_|8O+JORNTi^ZldE{G;;|raBL`4*$NTY;%As?E|}j&0y2HS7*>eaf6`F!%Co?C zvEN0xTMhAaj;icN4HoMkNMS;N(#oc=bI{O#u@u{{rOyw)zoN}=plf(O+Vu@!?)H{= z^cl#+6?Z(*_ztF1E93jdl>hy13be-&HS^I~fJlTE8P$9lMoLU1Rg8*UJVT+f;75#@ zu#zq(xb0AH3xyiJc7`v7TCy7>I{pKf(P;*@x3l-IxGg5bmCfWmFHsJGq@n zK5QSb$UCK_SJF^atRdbqgv_E5;Y6tbFKB-QRDMsyQl-2_9*9=pEy4k~?&2^~h%kXo z@{60#a}%9WaMyENg0mt2sb;9}oTg-}i55xzA(3WgbKH4+4?C3l_wOMit)Ck!wcBOM zAoNs2lUUe=G8@cc*-T+Z=yWbYWHV%DA|p$Am1{&|$kF&GD9Wj!s&S4CMP2VjdzywJ zU%{YripiM60gU=vW_M&V`srx2#U%>#P4s)22#q~>Qn6DG^jM#!5R>k4V}x#;Ca2`- z&2z4^D5sfaK(?+#{_|SyAhCTnsV+hRujS`B?m0WZAiKe26&*}`w#utlXG~-Ao zLoM|)S)t|BU&vXxJHNl_-*o+uz8Y@IQA@Wl`Y4I~i%{6m*lZ$y$kn$1M_}C-H|5%K zc0sXih2i~64im8Z9?Dyk_r|>YQDb|&Lq&1`%2@)kaKqXaN19$xs_jrG0N5GkvDI zJq4XN)1NlVt^y7a6D%JlGKm>3xuc!PfL0&PouXAwy5?&IX@-jwWpLbnrh1?;c{J<8 zmQ8gpL6$+1qXj|s;it{2FNXF;laKE>kpbI9$kr}^EPVT$SLA}yWj|aFe5&w5$q(=@ zb?D{CaTn}VP-XZJ-hvTBz4MjkEV0txCBipGKIvL7!R4dMnDV#$-e;S(p+!q+j5hcl zA*9x1Bu16LjpwlX{{Ff zYeCSyULr)zcI{9|-3~DjSH5d;_hQ5Ma#KaXR(z<&{rK4?6WFuGMYW~Ar^@dAcZ-@0 zrBAWb;ADvdS>onRivklR){Uf51n$j5kR1>G5R6VDVNs|Rkc;mFXP%kG(+oCA339ZV+zJ6e5Dg{^xhciR$ec0*2u{I2k0z>5lzD1}!a}3OBl*V!0r^R{kDkS(RRA?*xzZ zi(A(tKvSVFjquK`{iNSb*MDnMS4OUcY~#{AumV?*yrr}-^HygqX#vSsOs(VAdGJw>-zze>^U+IL{*--s z1?87ATe z{Z;JqIn`C!{0b18D$C)8EoLnC1tuQHYr9HRi%{zu3>9p#Jd7yF5SASUz{*I=Of3Qb zDLgzJ81NU&SqUt^F=i5JF8-GGmdJ%y7vwMWTm`o8mM#ZEf zeavs6>*9R#@}g^5mc&A#sZ+b1Ts!RDmUTWbe)or-Ux=S-IrDVz&YXcT?`iuL#lL=i z`~C?JZ5s^_bu}bQ%)!Xm$lB`Y>Y*r@+2i}Kp=JRmeQP^wUF)vfvUz^}^N)!ici6A+ zz2x!aR`at|9VEpQuePsFFZf4d^391eM&ZdOJyBPc+wQIZdFuQ?uc_Mf zsIoLHmT!pibkW7a+8T+Cm;7fxb+7E5k^qdd%m}==Srxo?4tSZez)AQ6bVsr!vEC^} zF=k*kAqD|9;@Y89YQ0gP=z@BY7ZLTP^5dY!z}-*| z8E-Lv*CUV@{~V3E03A4De5J=I*Gmo$mSqlS3gXDuW8?XrKFqc|$D!+`)z~V~qQ^2n zOkNAJ13f8g1eIi`nG6uctERQ*+H2lEH8tJZjZm@IziRsqyS}_^K?~K0+J(Hfrs$ni zYa$$aP?E-}Qxa|wy4{i9?HI>{V7;GBCeSS{y3@TKD5&mC=(Y67fGZ!KRb`*?bcbjJykc>oHwe=O}RLy zO-FQ^rSifyx*Udn10`xp-8(&wt#na?v?24^0M)fi=>IlC)@32u3yEvWKxu+NrySkV z7Q903b3V*&K62vb1z9#P)Mw#LzV1GgiUZibyE8!0?S7$R>Jqs2kvazCUC%vD0U-!26mh?9F#$Ajgz^W}*ou}O_ZOU7%w@NdOJGF9{07-V1}_VTHO3cq z*Kc=-&XZ0O(Asu&RDY*;p>6+ATn(isfjK@n09cZ+2!1gn)U)Z1 zdJ8|<>DrmvaT~04ZO?k~a@MR$)esKnnpd8@R|YCjI=e0oOCRmCX*K`!UxTx8pP45* z(Y~30vgx~z+1=P91DG%*ahTdjeFOBi&8aml_ zX|wT+b7!*A+0DziN;k@gNXqLg#V2oLzSz0xmB@26Dgfo2gR#D%#VeXjbRnwhaz!vL zeIIVZQJX^BlHa!3UXOK%+(-D>ftm-aDav}DYo1z#T;O7enNBn6H2M{cml96jDtZDB zC7CjD#5ycMq$(KNrSP%)g*74svU_@NC)5nLpW+|m1Ox00Dl6C6R|O9TzJ6|X_c-r- z;y}BcINhr5W4JqA^TptS#Jf&WEuViX3C3m7ScKa{oc)o~!8K#_po_T~l;TvuRv~k20 zO-CddX7b08o<@{hbqry`BoO&ysd>Z9$SA$~Hx*;UJ;Gx5IuP@L7@LJI#XKY&`g3ai zHkm1LR?LfD z>6G>IwJ~(cu{z*XlpTrr!0fCN=n}W|tDMSmnapg(Cv*QOIAIO0BQl4)*OrnB)0QBNYSLIY$b{@nye<0P13%ixOlE>JbS(#at~RwsNLq=*4h4 zOf8{bsG%mbfx*kxBGk?GGny3a7-|->bk-qx%p)ZYT0CZqWVtW#P{oht71~48X1u!j zpQ@vW2#cj)!Y??SVZCWHso`e0BI?E#qgx(}bl<83#ow-q(sxRz+n42zvrw@NBHLBF zYGGJc(L2!f#`$<8G>@IZwikl#oNx4feG8nk#1_VB?q%O*9;{yMP+%qa%l^zi6zgMk zY#!z<_I5V%4jV$a9LHvk-xMNUyHkVMq2$gcs)ZA#C9_-|XAp(= zeGHqpzVPY@p9X2kj&~EA23I{jWQ>?pn=;1yBKl{DoVCliMBaBiGjV~ai!@DKGH5qiwIhA*{C^!*A$ z+>^aU|7v^D1Z^ieDw3s(rcJmM8?*)C8oManxD;UwWRN#6cuB8<&XPP0g*V@n#EkxW9hN^KgcQi@1S zC7{t_rLTOSVlBscZw%+yV)er-)T+)a*Xo-WlWx%L3Dn9JQAI&QYHcV!P-M(hUW<3w zS3Z&FtFxF(7z)&OeB%r=F86g9U{Mp{xyW=h5Y}xFL}bNgR$6&Um_tL2Ff6d>bxWgC z?`{Wl_wZxxLmKDPKo0ACgB0flzJ@E7ZiVG`-bI*CHC$Z-&8s>;Mss(R^0Ga4rmT_* z&=DK{f|^7YJl~47%6yphx)9j;#G&<>5x()j_GZOK+#%IX8UP^D*MHrdR8-4Wonl8Y zSsQgpLM7?3m)_CL5#1W0?f$6d$W7DdRzs?W?Lm+h8Qw0YMjH}}2`^o$o{=2*jJ?F5 zh??az70#AmYi(M_uVO3Q9V+yWws8F z`N$r>!E$oD$7M4G#X0+0I%S-_px4>TAm0Ait84~Q@)q9b)5+J1aHw z8)#U2roQ+omgl92h4h2NbJd;Wg4ZK1KLX5bJx?kzx|j!v zmRSfj9f<5S`P-9}-263e4~}+KzY;?%)}Sj>6w(ITjkAYV-d8&e^9^mngEk@W{eKne zc*}<76H4|%*5+;Dv2|q{GBz~6a4hI>;C&+R~P=p|IJdPRJ7KsP`o%^zNsvt-b-WOCstEksBOFJS9a#PFI8T; zK>FVP#`Ut;CJ*LwK*>au6s>J;;mD$Ou1)|!6}jrrE}fhIIw-{0=z479tjq8dfIU8c z@snc9%j^>;IPyL@MjpB^z?5aHEUcW#Q&~Lxz=mwSm{}6Ol&ohHn{)R< zcKYYZn5&<+?wg%)`e9kCJLRuuTK-1|pEr+k;1biKLt+!jWpEi}srBe41|x~Qhe_o4 zE;gBkF*m?)Oeq2&QE>*ru z-9Z96YYgj5NIQ{NRtGfW$;!i$SH4oT{r%SziC33wgQ9Pnp)k+nd)=Zf!Ho+5tfbQm z-mJdN3xZMV=?BFM!a7ls!7Zq%OO76M!dYG#-V2v)o)2biHhp3i)gfk;bPh?Dgf=N= zx;E$GEc|*m4Md5VpWL zLvmX+*a$VnAKK`Vr-lwy8fJnsT_i1N`&++k8#8Fz0a#(RRA`1cCHc@w9#X$V+`4dA zo-+;;*TZ*>;itj!&foZ=`}~I|!uM7fR%J=Qi@@b0;YYh3jVEo(#m`tqhfN>J@J2pO zBOl>Tks4y|b=nSn@3db)OVxt2vC7M7lcRhSyj`J%%JP+)+2oyXa$$zLGJgAzP&zo8 zgShdf=widFI81t(=1~fYe|{O8y1~mknZCG98S!56_9UQ7&?r(MpscJ5#hEp`A*}yY zG}|inME>c@0PLT-X!2AYoM?9`D$+l|M$co1vb)yGdvr3bi4LNuOSL-VC=E``GVgl$Sx3pp4<`iIDKy*`!&L8sq zBwRgIEQzHPmm*z93M9;)rC~O$5|?1Oj>;$RpNzNvV2GY>JCU1qC>Tp`&%7lXu`SX` zPU&LG5cs2-Eu8hVKVI~czvLpe*Kd2kmLtCuWBoDi7oInSren41yOI6T*w`g~DL~X+ zzEnPa$cFuJ+g*;dDH#AA=`7djyDGP?2obU+i6k1cxI{bk6hdpqe+cOqce%h)dMQC1 zM|qhE$Y$+~#oCmH70w$M)(af92)o>)^K>W9dITh8<2;D6zJ+z%BeXGG=m-3=xyUSl ze~alTfcNExA}ON?wCU~lZ(o$k*y2AFbnw8x$MgKOaSR)aEWlbJxA+z`93w2=b`^97Za!9=LdsB_jS9JpqZ1jQ>05;N6%q+5T?Dpngj|)9MeemQL zsyoAmDr_{>COkHYH!up%3Kst8PBSI*uBg5rm;aY?D`hoVaCT!phZ=om(zZ{Nhkl%4V%o`+1sndJ=Qzy z`w-V52}@bC4aco#*@*yq?$G~u3Mr@9uQMs^;Dv?KTzvlUrn6z|hn+1*Xn~T^2!Ea{ z_O*iqHWERJ2%@+#2$g-6R?h{0oh7#<*g8zv|Ja6!a%{Q+?k zLKI_0N=X*&!RItGfGi|sm`ea7u^|^4NR$UXx=6v-x)hKcC^6;6Icoo#(SkD6 zu&txbFQdhQ|L(B#Qb_3}(_*(3JW_334nKm?ouOMIAicq+F)~(lbpjlzG9FjpJTEjV zAo1BTl=7e)XY4yW#9UhqJH%9wzrCzk2F`{t39%@nNM{oyAkd>Ipi6%^Jao(XMZ9{N zBEn3IZ{kbb_YGkizOT-tUX)loggC%*vX@vgDFT;RHj~<9f^LEdD3%i>l~Qe~8#0)(JP(KVL$TVAAGL6P`d6@L(_)m_WuM10A>T2B7zy3xvcj=gOst zP06X`6*liw2&C2rAzC2WnA{9XH##<=~8P~Q=l^VW~wM0Xd z=Pj|vJV{y>Gz^idSIi$Bq$ni5#Va4TY%KWje60)FqG218*W272RI>2Jhxynd8R>LE zj68XGhUt`l+HMJ@Jm{vJo+udA#0sP=Rni z2Se)g1LIQFNse0?-oa^mt@SvQg+-eizFn(gu32hmPv|1{d5T9*%pwjAC%wJt9vAK2 z1AV`4Ol4kjNpc>`9Kj*MF#+fp<1qeJ=8C4VATKTA-axe`S)jrnU@jd0!Wg5;+h$(I z6#AF``M}DYEm=*eI0CjQ(&ib#*sW5Q723`6Qg(aLix{JOT$>Uh@^98|^7f{=&q}k2 zD$IPO+7(hoLW;ewCoB-retbq4)vH8EXyK%U1#>2>azYq&)HEfv#aSfGtc^6>)Qs$1 zPyqnI2Pp@8S1BVqD_buB<9|^Jl3cWSARq@2z{bVR0RZvvasogcoE!<>TvYTNkbhkg zW-caX_O1Xn4o>jDfVh#PjG2{%rR#r5JGfji{?~j7l3-d`5Kn>)I2_6i;R0|&xB=X} z5CHJMZyxa9lK@_>e@9?24*>Fa=D!C5f5+TF&VNyE&c80uKl*?C|Kt0gFBteQ1LFA0 zfVp`8QGvk!y#Km@01h6Wf1iINY5*_K-&CBOe;b6%20%_u;NSfJ2Lthf6CQbJp`0L|zvcXIl9w0s zp9V0;f9gQo-2dSPLH;i6|K#KTTlIfnTz^{zr27m1|4KnXuKy5%fM8CZgieS88W7A2 MM5UvXRF*>hf8o(hkpKVy diff --git a/setup.R b/setup.R deleted file mode 100644 index c56102d..0000000 --- a/setup.R +++ /dev/null @@ -1,3 +0,0 @@ -list.of.packages <- c("ggplot2", "scibet", "Seurat", "singleCellNet", "viridis", "ggsci", "SingleR", "dplyr") -new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])] -if(length(new.packages)) install.packages(new.packages) diff --git a/snakefile.annotate b/snakefile.annotate new file mode 100644 index 0000000..73687ba --- /dev/null +++ b/snakefile.annotate @@ -0,0 +1,681 @@ +# import libraries +import os +from datetime import datetime + +# Get the names of query samples from the paths given in the query section of the config +samples = [os.path.basename(os.path.dirname(query_path)) for query_path in config['query_datasets']] + +now = datetime.now() +dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") + +# Create subdirectories for each query sample +for sample in samples: + path = "{output_dir}/{sample}".format(output_dir = config["output_dir"], sample = sample) + if not os.path.exists(path): + os.mkdir(path) + +""" +One rule that directs the DAG which is represented in the rulegraph +""" +rule all: + input: + expand(config["output_dir"] + "/{sample}/Prediction_Summary.tsv", + sample = samples), + expand(config['output_dir'] + '/{sample}/report/{sample}.prediction_report.' + dt_string + '.html', + sample = samples) + +""" +rule that gets the gets the interesction in genes between samples and reference +It outputs temporary reference and query datasets based on the common genes +""" +rule preprocess: + input: + reference = config['training_reference'], + query = config['query_datasets'] + output: + reference = config['output_dir'] + "/expression.csv", + query = expand(config['output_dir'] + "/{sample}/expression.csv", sample = samples) + params: + check_genes = bool(config['check_genes']), + genes_required = config['genes_required'] + log: config['output_dir'] + "/preprocess.log" + priority: 50 + shell: + "Rscript {workflow.basedir}/Scripts/preprocess.R " + "--ref {input.reference} " + "--query {input.query} " + "--output_dir {config[output_dir]} " + "--check_genes {params.check_genes} " + "--genes_required {params.genes_required} " + "&> {log} " + +#---------------------------------------------------- +# Consensus +#---------------------------------------------------- + +rule consensus: + input: + results = expand(config["output_dir"] + "/{{sample}}/{tool}/{tool}_pred.csv", + tool=config['tools_to_run']), + sample = config["output_dir"] + "/{sample}" + output: + prediction_summary = config["output_dir"] + "/{sample}/Prediction_Summary.tsv" + log: + config["output_dir"] + "/{sample}/Gatherpreds.log" + params: + basedir = {workflow.basedir}, + tools = config['tools_to_run'], + conesnsus_tools = config['consensus_tools'], + labfile = config['reference_annotations'] + shell: + """ + Rscript {params.basedir}/Scripts/calculate_consensus.R \ + {input.sample} \ + {output.prediction_summary} \ + "{params.tools}" \ + "{params.conesnsus_tools}" \ + {params.labfile} \ + &> {log} + """ + +#---------------------------------------------------- +# Knit Report +#---------------------------------------------------- + +rule knit_report: + input: + pred = config['output_dir'] + "/{sample}/Prediction_Summary.tsv" + output: + report_path = config['output_dir'] + '/{sample}/report/{sample}.prediction_report.' + dt_string + '.html' + log: + config['output_dir'] + "/{sample}/report/report.log" + params: + basedir = {workflow.basedir}, + pred_path = config['output_dir'], + sample = "{sample}", + tools = config['tools_to_run'], + consensus = config['consensus_tools'], + refs = config['references'], + ref_anno = config['reference_annotations'], + marker_genes = config['marker_genes'] + threads: 1 + resources: + shell: + """ + Rscript -e "rmarkdown::render( + '{params.basedir}/Notebooks/annotate_report.Rmd', + params = list(tools = '{params.tools}', + consensus = '{params.consensus}', + refs = '{params.refs}', + ref_anno = '{params.ref_anno}', + pred_path = '{params.pred_path}', + sample = '{params.sample}', + marker_genes = '{params.marker_genes}', + threads = '{threads}'), + output_file = '{output.report_path}')" \ + &> {log} + """ + +#---------------------------------------------------- +# SingleR +#---------------------------------------------------- + +rule train_SingleR: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/SingleR/SingleR_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/SingleR/SingleR.log" + benchmark: + config['output_dir'] + "/SingleR/SingleR_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SingleR/train_SingleR.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_SingleR: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/SingleR/SingleR_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/SingleR/SingleR_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/SingleR/SingleR.log" + benchmark: + config['output_dir'] + "/{sample}/SingleR/SingleR_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SingleR/predict_SingleR.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scPred +#---------------------------------------------------- + +rule train_scPred: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/scPred/scPred_model.Rda" + params: + basedir = {workflow.basedir}, + model_type = "svmRadial", + log: + config['output_dir'] + "/scPred/scPred.log" + benchmark: + config['output_dir'] + "/scPred/scPred_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scPred/train_scPred.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + {params.model_type} \ + &> {log} + """ + +rule predict_scPred: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/scPred/scPred_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/scPred/scPred_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/scPred/scPred.log" + benchmark: + config['output_dir'] + "/{sample}/scPred/scPred_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scPred/predict_scPred.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ +#---------------------------------------------------- +# scClassify +#---------------------------------------------------- + +rule train_scClassify: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/scClassify/scClassify_model.Rda" + params: + basedir = {workflow.basedir}, + log: + config['output_dir'] + "/scClassify/scClassify.log" + benchmark: + config['output_dir'] + "/scClassify/scClassify_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scClassify/train_scClassify.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_scClassify: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/scClassify/scClassify_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/scClassify/scClassify_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/scClassify/scClassify.log" + benchmark: + config['output_dir'] + "/{sample}/scClassify/scClassify_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scClassify/predict_scClassify.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# SciBet +#---------------------------------------------------- + +rule train_SciBet: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/SciBet/SciBet_model.Rda" + params: + basedir = {workflow.basedir}, + log: + config['output_dir'] + "/SciBet/SciBet.log" + benchmark: + config['output_dir'] + "/SciBet/SciBet_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SciBet/train_SciBet.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_SciBet: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/SciBet/SciBet_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/SciBet/SciBet_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/SciBet/SciBet.log" + benchmark: + config['output_dir'] + "/{sample}/SciBet/SciBet_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SciBet/predict_SciBet.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scHPL +#---------------------------------------------------- + +rule train_scHPL: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/scHPL/scHPL_model.Rda" + params: + basedir = {workflow.basedir}, + classifier = 'svm', + dimred = 'False' + log: + config['output_dir'] + "/scHPL/scHPL.log" + benchmark: + config['output_dir'] + "/scHPL/scHPL_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/scHPL/train_scHPL.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {params.classifier} \ + {params.dimred} \ + &> {log} + """ + +rule predict_scHPL: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/scHPL/scHPL_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/scHPL/scHPL_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5 + log: + config['output_dir'] + "/{sample}/scHPL/scHPL.log" + benchmark: + config['output_dir'] + "/{sample}/scHPL/scHPL_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/scHPL/predict_scHPL.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + &> {log} + """ + +#---------------------------------------------------- +# SVM Linear +#---------------------------------------------------- + +rule train_SVMlinear: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/SVMlinear/SVMlinear_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/SVMlinear/SVMlinear.log" + benchmark: + config['output_dir'] + "/SVMlinear/SVMlinear_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/train_linearSVM.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_SVMlinear: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/SVMlinear/SVMlinear_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/SVMlinear/SVMlinear_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0, + tool_name = 'SVMlinear' + log: + config['output_dir'] + "/{sample}/SVMlinear/SVMlinear.log" + benchmark: + config['output_dir'] + "/{sample}/SVMlinear/SVMlinear_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/predict_SVM.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + {threads} \ + {params.tool_name} \ + &> {log} + """ + +#---------------------------------------------------- +# SVC +#---------------------------------------------------- + +rule train_SVC: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/SVCrbf/SVCrbf_model.Rda" + params: + basedir = {workflow.basedir}, + classifier = 'rbf' + log: + config['output_dir'] + "/SVCrbf/SVCrbf.log" + benchmark: + config['output_dir'] + "/SVCrbf/SVCrbf_train_benchmark.txt" + threads: 10 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/train_SVM.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {params.classifier} \ + {threads} \ + &> {log} + """ + +rule predict_SVC: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/SVCrbf/SVCrbf_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/SVCrbf/SVCrbf_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5, + tool_name = 'rbf' + log: + config['output_dir'] + "/{sample}/SVCrbf/SVCrbf.log" + benchmark: + config['output_dir'] + "/{sample}/SVCrbf/SVCrbf_predict_benchmark.txt" + threads: 10 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/predict_SVM.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + {threads} \ + {params.tool_name} \ + &> {log} + """ + +#---------------------------------------------------- +# singleCellNet +#---------------------------------------------------- + +rule train_singleCellNet: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/singleCellNet/singleCellNet_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/singleCellNet/singleCellNet.log" + benchmark: + config['output_dir'] + "/singleCellNet/singleCellNet_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/singleCellNet/train_singleCellNet.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_singleCellNet: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/singleCellNet/singleCellNet_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/singleCellNet/singleCellNet_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/singleCellNet/singleCellNet.log" + benchmark: + config['output_dir'] + "/{sample}/singleCellNet/singleCellNet_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/singleCellNet/predict_singleCellNet.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# Correlation +#---------------------------------------------------- + +rule train_Correlation: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/Correlation/Correlation_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/Correlation/Correlation.log" + benchmark: + config['output_dir'] + "/Correlation/Correlation_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/Correlation/train_Correlation.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_Correlation: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/Correlation/Correlation_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/Correlation/Correlation_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/Correlation/Correlation.log" + benchmark: + config['output_dir'] + "/{sample}/Correlation/Correlation_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/Correlation/predict_Correlation.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scLearn +#---------------------------------------------------- + +rule train_scLearn: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'] + output: + model = config['output_dir'] + "/scLearn/scLearn_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/scLearn/scLearn.log" + benchmark: + config['output_dir'] + "/scLearn/scLearn_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scLearn/train_scLearn.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_scLearn: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/scLearn/scLearn_model.Rda" + output: + pred = config['output_dir'] + "/{sample}/scLearn/scLearn_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/scLearn/scLearn.log" + benchmark: + config['output_dir'] + "/{sample}/scLearn/scLearn_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scLearn/predict_scLearn.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# ACTINN +#---------------------------------------------------- + +rule ACTINN: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'], + query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), + output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) + output: + pred = expand("{output_dir}/{sample}/ACTINN/ACTINN_pred.csv", sample = samples,output_dir=config["output_dir"]), + query_time = expand("{output_dir}/{sample}/ACTINN/ACTINN_query_time.csv",sample = samples,output_dir=config["output_dir"]), + training_time = expand("{output_dir}/{sample}/ACTINN/ACTINN_training_time.csv",sample = samples,output_dir=config["output_dir"]) + log: expand("{output_dir}/{sample}/ACTINN/ACTINN.log", sample = samples,output_dir=config["output_dir"]) + params: + basedir = {workflow.basedir} + shell: + "python3 {params.basedir}/Scripts/run_ACTINN.py " + "--ref {input.reference} " + "--labs {input.labfile} " + "--query {input.query} " + "--output_dir {input.output_dir} " + "&> {log}" + +#---------------------------------------------------- +# The End +#---------------------------------------------------- diff --git a/snakefile.benchmark b/snakefile.benchmark new file mode 100644 index 0000000..3a49fa3 --- /dev/null +++ b/snakefile.benchmark @@ -0,0 +1,612 @@ +# import libraries +import os + +n_folds = list(range(1, config['benchmark']['n_folds']+1)) + +""" +One rule that directs the DAG which is represented in the rulegraph +""" +rule all: + input: report_path = config['output_dir'] + '/report/benchmark_report.html' + +#---------------------------------------------------- +# Subset Folds +#---------------------------------------------------- + +rule subset_folds: + input: + reference = config['training_reference'], + labfile = config['reference_annotations'] + output: + training_data = expand(config['output_dir'] + "/fold{folds_index}/train.csv", folds_index = n_folds), + labfile = expand(config['output_dir'] + "/fold{folds_index}/train_labels.csv", folds_index = n_folds), + test_data = expand(config['output_dir'] + "/fold{folds_index}/test.csv", folds_index = n_folds) + log: + config['output_dir'] + "/subset_folds.log" + params: + basedir = {workflow.basedir}, + outdir = config['output_dir'], + n_folds = config['benchmark']['n_folds'] + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/benchmark/subset_folds.R \ + {input.reference} \ + {input.labfile} \ + {params.outdir} \ + {threads} \ + {params.n_folds} \ + &> {log} + """ + +#---------------------------------------------------- +# Knit Report +#---------------------------------------------------- + +rule knit_report: + input: + pred = expand(config['output_dir'] + "/fold{folds_index}/{tool}/{tool}_pred.csv", + folds_index = n_folds, + tool = config['tools_to_run']) + output: + report_path = config['output_dir'] + '/report/benchmark_report.html' + log: + config['output_dir'] + "/report/report.log" + params: + basedir = {workflow.basedir}, + pred_path = config['output_dir'], + n_folds = config['benchmark']['n_folds'], + tools = config['tools_to_run'] + threads: 1 + resources: + shell: + """ + Rscript -e "rmarkdown::render( + '{params.basedir}/Notebooks/benchmark_report.Rmd', + params = list(tools = '{params.tools}', + ref_name = '', + pred_path = '{params.pred_path}', + fold = '{params.n_folds}'), + output_file = '{output.report_path}')" \ + &> {log} + """ + +#---------------------------------------------------- +# scPred +#---------------------------------------------------- + +rule train_scPred: + input: + reference = config['output_dir'] + "/fold{fold}/train.csv", + labfile = config['output_dir'] + "/fold{fold}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{fold}/scPred/scPred_model.Rda" + params: + basedir = {workflow.basedir}, + scPred_model = "svmRadial" + log: + config['output_dir'] + "/fold{fold}/scPred/scPred.log" + benchmark: + config['output_dir'] + "/fold{fold}/scPred/scPred_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scPred/train_scPred.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + {params.scPred_model} \ + &> {log} + """ + +rule predict_scPred: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/scPred/scPred_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/scPred/scPred_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/scPred/scPred.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scPred/scPred_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scPred/predict_scPred.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# SingelR +#---------------------------------------------------- + +rule train_SingleR: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_model.Rda" + params: + basedir = {workflow.basedir}, + log: + config['output_dir'] + "/fold{folds_index}/SingleR/SingleR.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SingleR/train_SingleR.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_SingleR: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/SingleR/SingleR.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SingleR/predict_SingleR.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scClassify +#---------------------------------------------------- + +rule train_scClassify: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_model.Rda" + params: + basedir = {workflow.basedir}, + log: + config['output_dir'] + "/fold{folds_index}/scClassify/scClassify.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scClassify/train_scClassify.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_scClassify: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/scClassify/scClassify.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scClassify/predict_scClassify.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# SciBet +#---------------------------------------------------- + +rule train_SciBet: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_model.Rda" + params: + basedir = {workflow.basedir}, + log: + config['output_dir'] + "/fold{folds_index}/SciBet/SciBet.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SciBet/train_SciBet.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_SciBet: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/SciBet/SciBet.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/SciBet/predict_SciBet.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# SVM Linear +#---------------------------------------------------- + +rule train_SVMlinear: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/train_linearSVM.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_SVMlinear: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5, + tool_name = 'SVMlinear' + log: + config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/predict_SVM.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + {threads} \ + {params.tool_name} \ + &> {log} + """ + +#---------------------------------------------------- +# SVC +#---------------------------------------------------- + +rule train_SVC: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/SVC/SVC_model.Rda" + params: + basedir = {workflow.basedir}, + classifier = 'rbf' + log: + config['output_dir'] + "/fold{folds_index}/SVC/SVC.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SVC/SVC_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/train_SVM.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {params.classifier} \ + {threads} \ + &> {log} + """ + +rule predict_SVC: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/SVC/SVC_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/SVC/SVC_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5, + tool_name = 'rbf' + log: + config['output_dir'] + "/fold{folds_index}/SVC/SVC.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/SVC/SVC_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/SVC/predict_SVM.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + {threads} \ + {params.tool_name} \ + &> {log} + """ + +#---------------------------------------------------- +# singleCellNet +#---------------------------------------------------- + +rule train_singleCellNet: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_model.Rda" + params: + basedir = {workflow.basedir}, + log: + config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/singleCellNet/train_singleCellNet.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_singleCellNet: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/singleCellNet/predict_singleCellNet.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# Correlation +#---------------------------------------------------- + +rule train_Correlation: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_model.Rda" + params: + basedir = {workflow.basedir}, + log: + config['output_dir'] + "/fold{folds_index}/Correlation/Correlation.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/Correlation/train_Correlation.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ +rule predict_Correlation: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/Correlation/Correlation.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/Correlation/predict_Correlation.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scLearn +#---------------------------------------------------- + +rule train_scLearn: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_model.Rda" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/scLearn/scLearn.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scLearn/train_scLearn.R \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + &> {log} + """ + +rule predict_scLearn: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/scLearn/scLearn.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + Rscript {params.basedir}/Scripts/scLearn/predict_scLearn.R \ + {input.query} \ + {input.model} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scHPL +#---------------------------------------------------- + +rule train_scHPL: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_model.Rda" + params: + basedir = {workflow.basedir}, + classifier = 'svm', + dimred = 'False' + log: + config['output_dir'] + "/fold{folds_index}/scHPL/scHPL.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_train_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/scHPL/train_scHPL.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {params.classifier} \ + {params.dimred} \ + &> {log} + """ + +rule predict_scHPL: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_model.Rda" + output: + pred = config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5 + log: + config['output_dir'] + "/fold{folds_index}/scHPL/scHPL.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/scHPL/predict_scHPL.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + &> {log} + """ + +#---------------------------------------------------- +# The End +#---------------------------------------------------- diff --git a/submit.sh b/submit.sh deleted file mode 100644 index b1f5940..0000000 --- a/submit.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/bash -#PBS -N Test_Submission -#PBS -o logs/err.txt -#PBS -e logs/out.txt -#PBS -l walltime=20:00:00 -#PBS -l nodes=1:ppn=3 -#PBS -l mem=125G -#PBS -l vmem=125G - - -cd /scCoAnnotate/ -mkdir -p logs - -#conda init bash -module load miniconda/3.8 -source ~/.conda_init.sh -module load snakemake/5.32.0 -module load hydra/R/4.0.5 -module load python/3.6.5_new - -snakemake --use-conda --configfile config.yml --cores 2 - From 7e430c1845124f6d6bc9d3c05818b986cf8df4c8 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:51:37 -0400 Subject: [PATCH 029/144] Update example.config.yml --- example.config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example.config.yml b/example.config.yml index ff2d7c9..77f3cf1 100644 --- a/example.config.yml +++ b/example.config.yml @@ -31,6 +31,9 @@ tools_to_run: - SVMlinear - Correlation +consensus_tools: + - all + # benchmark tools on reference benchmark: n_folds: 3 From 1e762487402c312ddcd6077f7be51b50d7d7cb81 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 7 Aug 2023 14:55:00 -0400 Subject: [PATCH 030/144] Update example.config.yml --- example.config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/example.config.yml b/example.config.yml index 77f3cf1..cee4df6 100644 --- a/example.config.yml +++ b/example.config.yml @@ -29,6 +29,7 @@ tools_to_run: - singleCellNet - scHPL - SVMlinear + - scLearn - Correlation consensus_tools: From a105e24be555f309b1622ec3bc6717a1a25a0ae8 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Mon, 7 Aug 2023 14:57:21 -0400 Subject: [PATCH 031/144] removed hidden ipynb folder --- .gitignore | 1 + .../graph.benchmark-checkpoint.pdf | Bin 8019 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 .ipynb_checkpoints/graph.benchmark-checkpoint.pdf diff --git a/.gitignore b/.gitignore index 29c9865..f51901a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .Rhistory .RData .snakemake/ +.ipynb_checkpoints config.yml test/ diff --git a/.ipynb_checkpoints/graph.benchmark-checkpoint.pdf b/.ipynb_checkpoints/graph.benchmark-checkpoint.pdf deleted file mode 100644 index 34679187a89b376b9291d6982e4ea3e8a13430d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8019 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 wAqlx6oLo9iPWVM9 Date: Mon, 7 Aug 2023 15:51:50 -0400 Subject: [PATCH 032/144] Update README.md --- README.md | 280 ++++++++++++++++-------------------------------------- 1 file changed, 84 insertions(+), 196 deletions(-) diff --git a/README.md b/README.md index b89ac12..f29c745 100644 --- a/README.md +++ b/README.md @@ -1,231 +1,119 @@ # scCoAnnotate -scRNA-seq based prediction of cell-types using an automated Snakemake pipeline to produce a consensus of several prediction tools. - # Summary -The pipeline allows the user to select what single-cell annotation tools they want to run on a selected reference to annotate a list of query datasets. It then outputs a consensus of the predictions across tools selected. This pipeline trains classifiers on genes common to the reference and all query datasets. The pipeline also features parallelization options to exploit the computational resources available. +Reference based prediction of cell-types using a fast and efficient Snakemake pipeline to increase automation and reduce the need to run several scripts and experiments. The pipeline allows the user to select what single-cell annotation tools they want to run on a selected reference to annotate a list of query datasets. It then outputs a consensus of the predictions across tools selected. This pipeline trains classifiers on genes common to the reference and all query datasets. + # Installation and Dependencies -* Install or load [R](https://www.r-project.org/) version 4.2.2 and Python version 3.6.5. +Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. -* Install [Snakemake](https://snakemake.readthedocs.io/en/stable/) version 5.32.0 in your linux environment. +**R packages CRAN** +- Seurat +- tidyverse +- MetBrewer +- plotly +- caret +- Matrix -```bash -$ conda activate base -$ mamba create -c conda-forge -c bioconda -n snakemake snakemake +``` +install.package() ``` -* Install all the dependencies for the tools you plan on using. See [the list of dependencies below](#Required-packages) - -* Clone this repository into your directory of choice +Older version of Matrix package had to be installed for Seurat to work: https://github.com/satijalab/seurat/issues/6746 -# Quickstart +``` +devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r-project.org") +``` -The processes of the snakemake pipeline are arranged as per this rule graph: +**R packages bioconductor** +- SingleCellExperiment +- SummarizedExperiment +- ComplexHeatmap +- WGCNA +- SingleR +- scClassify +- scuttle +- scran +- M3Drop -![dag](https://user-images.githubusercontent.com/59002771/191146873-5c680bbd-d11c-418c-ae96-7662ee7f99ed.png) +``` +BiocManager::install() +``` -Rule preprocess gets the common genes and creates temporary reference and query datasets based ob the common genes. Rule concat appends all predictions into one tab seperate file (`prediction_summary.tsv`) and gets the consensus prediction. +**R packages github** +- singleCellNet (pcahan1/singleCellNet) +- scPred (powellgenomicslab/scPred) +- scibet (PaulingLiu/scibet) +- scLearn (bm2-lab/scLearn) + +``` +devtools::install_github() +``` -After preparing your config file, you can run the pipeline with the following command: +**Python modules** +- numpy +- pandas +- scHPL +- sklearn +- anndata +- matplotlib +- scanpy +- datetime +- tensorflow +- tables +- snakemake -```bash -snakemake --use-conda --configfile config.yml --cores 3 +``` +pip install ``` -## Config file template -```yaml -# target directory -output_dir: -# path to reference to train classifiers on (cell x gene raw counts) -training_reference: -# path to annotations for the reference (csv file with cellname and label headers) -reference_annotations: -# path to query datasets (cell x gene raw counts) -query_datasets: - - - - - . - . -# step to check if required genes are kept between query and reference -check_genes: False -# path for the genes required -genes_required: Null -# rejection option for SVM -rejection: True -# classifiers to run -tools_to_run: - - - - - . - . -# benchmark tools on reference -benchmark: False -plots: True -consensus: - - - - - . +# Quickstart -``` +UPDATE +## Config File: -### Example config file +```yaml +# UPDATE +``` + +### An Example Config is attached ```yaml -# target directory -output_dir: /project/kleinman/hussein.lakkis/from_hydra/test -# path to reference to train classifiers on (cell x gene raw counts) -training_reference: /project/kleinman/hussein.lakkis/from_hydra/2022_01_10-Datasets/Cortex/p0/expression.csv -# path to annotations for the reference (csv file with cellname and label headers) -reference_annotations: /project/kleinman/hussein.lakkis/from_hydra/2022_01_10-Datasets/Cortex/p0/labels.csv -# path to query datasets (cell x gene raw counts) -query_datasets: - - /project/kleinman/hussein.lakkis/from_hydra/Collab/HGG_Selin_Revision/test/BT2016062/expression.csv - - /project/kleinman/hussein.lakkis/from_hydra/Collab/HGG_Selin_Revision/test/P-1694_S-1694_multiome/expression.csv - - /project/kleinman/hussein.lakkis/from_hydra/Collab/HGG_Selin_Revision/test/P-1701_S-1701_multiome/expression.csv -# step to check if required genes are kept between query and reference -check_genes: False -# path for the genes required -genes_required: Null -# rejection option for SVM -rejection: True -# classifiers to run -tools_to_run: - - ACTINN - - scHPL - - scClassify - - correlation - - scmapcluster - - scPred - - SingleCellNet - - SVM_reject - - SingleR - - CHETAH - - scmapcell - - SciBet -# benchmark tools on reference -benchmark: False -plots: True -consensus: - - all +# UPDATE ``` ## Submission File: An example of the submission file is also available in this repository and is called submit.sh. This is for TORQUE schedulers. + ``` bash -#!/usr/bin/bash -#PBS -N scCoAnnotate -#PBS -o logs/err.txt -#PBS -e logs/out.txt -#PBS -l walltime=20:00:00 -#PBS -l nodes=1:ppn=3 -#PBS -l mem=125G -#PBS -l vmem=125G - -# you need to do this step - -cd {root directory of the pipeline} -mkdir -p logs - -# set up the environment -#conda init bash -module load miniconda/3.8 -source ~/.conda_init.sh -module load snakemake/5.32.0 -module load hydra/R/4.0.5 -module load python/3.6.5 - -# Run the snakemake -snakemake --use-conda --configfile config.yml --cores 3 +# UPDATE ``` -# Available tools - -1. [ACTINN](https://github.com/mafeiyang/ACTINN) -2. [SciBet](https://github.com/PaulingLiu/scibet) -4. [Spearman Correlation](https://statistics.laerd.com/statistical-guides/spearmans-rank-order-correlation-statistical-guide.php) -5. [SVM](https://scikit-learn.org/stable/modules/svm.html) -6. SVM Rejection -7. [SingleR](https://bioconductor.org/packages/release/bioc/html/SingleR.html) -8. [SingleCellNet](https://github.com/pcahan1/singleCellNet) -9. [CHETAH](https://www.bioconductor.org/packages/release/bioc/html/CHETAH.html) -10. [scHPL](https://github.com/lcmmichielsen/scHPL) -11. [scPred](https://github.com/powellgenomicslab/scPred) -12. [scmap (cell and cluster)](https://bioconductor.org/packages/release/bioc/html/scmap.html) - -# Required packages - -## Python Libraries: - -``` -tensorboard==1.7.0 -tensorboard-data-server==0.6.1 -tensorboard-plugin-wit==1.8.0 -tensorflow==1.7.0 -tensorflow-estimator==2.5.0 -sklearn==0.0 -scikit-learn==0.24.1 -pandas==1.1.5 -numpy==1.19.5 -numpy-groupies==0.9.13 -numpydoc==1.1.0 -scHPL==0.0.2 -``` +# Tools Available + - scPred + - SingleR + - scClassify + - SciBet + - singleCellNet + - scHPL + - SVMlinear + - scLearn + - Correlation -## R Libraries: +# Adding New Tools: +``` R +# UPDATE ``` -glue -data.table -scPred_1.9.2 -SingleCellExperiment_1.12.0 -SummarizedExperiment_1.20.0 -CHETAH_1.6.0 -scmap_1.12.0 -singleCellNet == 0.1.0 -scibet == 1.0 -SingleR == 1.4.1 -Seurat == 4.0.3 -dplyr == 1.0.7 -tidyr == 1.1.3 -viridis == 0.6.1 -ggsci == 2.9 -tidyverse == 1.3.1 -``` -# Adding New Tools: -to add new tools, you have to add this template to the the snakefile as such: +# Tools -``` R -rule {rulename}: - input: - reference = "{output_dir}/expression.csv".format(output_dir =config['output_dir']), - labfile = config['reference_annotations'], - query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), - output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) - - output: - pred = expand("{output_dir}/{sample}/{rulename}/{rulename}_pred.csv", sample = samples,output_dir=config["output_dir"]), - query_time = expand("{output_dir}/{sample}/{rulename}/{rulename}_query_time.csv",sample = samples,output_dir=config["output_dir"]), - training_time = expand("{output_dir}/{sample}/{rulename}/{rulename}_training_time.csv",sample = samples,output_dir=config["output_dir"]) - log: expand("{output_dir}/{sample}/{rulename}/{rulename}.log", sample = samples,output_dir=config["output_dir"]) - shell: - "Rscript Scripts/run_{rulename}.R " - "--ref {input.reference} " - "--labs {input.labfile} " - "--query {input.query} " - "--output_dir {input.output_dir} " - "&> {log}" - - ``` - The tool script you add must generate outputs that match the output of the rule. - -# scClassify +## scClassify Detailed documentation for scClassify train and predict scripts, written July 2023 by Bhavyaa Chandarana @@ -239,7 +127,7 @@ Detailed documentation for scClassify train and predict scripts, written July 20 * `scClassify::plotCellTypeTree()` produces a ggplot object. Therefore, I am using `ggplot2::ggsave()` to save it as a png file. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) -# scPred +## scPred Both reference and query is normaluzed using `Seurat::NormalizeData()`. Needs computed PCA space. Dims set to 1:30 according to tutorial. @@ -248,7 +136,7 @@ Default model `SVMradial`. Option to switch model should be set up in snakemake. Normalization and parameters based on this tutorial: https://powellgenomicslab.github.io/scPred/articles/introduction.html -# SingleR +## SingleR Both reference and query is normaluzed using `scuttle::logNormCounts()`. Both reference and query is converted to SingleCellExperiment objects before normalization. @@ -259,7 +147,7 @@ Method for generating marker genes for each class in reference. Wilcox is recome Normalization and parameters based on this tutorial: http://www.bioconductor.org/packages/devel/bioc/vignettes/SingleR/inst/doc/SingleR.html#3_Using_single-cell_references -# singleCellNet +## singleCellNet Documentation written by: Rodrigo Lopez Gutierrez Date written: 2023-08-01 @@ -273,7 +161,7 @@ Normal parameters were used in both the training and prediction functions, with singleCellNet workflow was generated following the tutorial below: https://pcahan1.github.io/singleCellNet/ -# Correlation +## Correlation Documentation written by: Rodrigo Lopez Gutierrez Date written: 2023-08-02 @@ -290,7 +178,7 @@ Prediction script calculates a correlation between each cell in the query and ea Currently only outputting a table with each cell, the most highly correlated label, and the corresponding correlation score for that label. In the future we could export the full correlation matrix, if necessary. -# scLearn +## scLearn Detailed documentation for scLearn train and predict scripts, written August 2023 by Bhavyaa Chandarana. Added information Tomas Vega Waichman in 2023-08-04. @@ -306,7 +194,7 @@ Preprocessing performed in the same way as scLearn documentation tutorial [sourc * Added some outputs, for prediction added a table with the selected genes for the model. In prediction added and output with the whole data.frame with the probabilities for each cell. -# singleCellNet +## singleCellNet Documentation written by: Rodrigo Lopez Gutierrez Date written: 2023-08-01 From b07a7282144c5edfe487a9dbb5e5d9d73bfb85e1 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 7 Aug 2023 16:00:09 -0400 Subject: [PATCH 033/144] Update README.md --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f29c745..0e9de97 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ devtools::install_github() pip install ``` -# Quickstart +# Quickstart UPDATE @@ -79,19 +79,18 @@ UPDATE # UPDATE ``` -### An Example Config is attached +## Submission File -```yaml -# UPDATE -``` - -## Submission File: +### Annotation -An example of the submission file is also available in this repository and is called submit.sh. This is for TORQUE schedulers. +```bash +# UPDATE +``` +### Benchmark -``` bash -# UPDATE +```bash +# UPDATE ``` # Tools Available From fb01659f96f14613d0cae23c3846c39769cf022c Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:06:12 -0400 Subject: [PATCH 034/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0e9de97..029011d 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ UPDATE ## Submission File -### Annotation +### Annotate ```bash # UPDATE From 2860a8b06b3ad061790027062f5389daa7fae36b Mon Sep 17 00:00:00 2001 From: Tom <64378638+tvegawaichman@users.noreply.github.com> Date: Tue, 8 Aug 2023 18:09:29 -0400 Subject: [PATCH 035/144] Dev tom tangram (#38) * tool: tangram * Tangram docummentation * Tangram docummentation --- README.md | 15 ++++ Scripts/Tangram/run_Tangram.py | 123 +++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 Scripts/Tangram/run_Tangram.py diff --git a/README.md b/README.md index 029011d..95334eb 100644 --- a/README.md +++ b/README.md @@ -206,3 +206,18 @@ Normal parameters were used in both the training and prediction functions, with singleCellNet workflow was generated following the tutorial below: https://pcahan1.github.io/singleCellNet/ + +## Tangram + +Documentation written by: Tomas Vega Waichman +Date written: 2023-08-08 +Tangram maps a single cell that is used as a reference to a spatial dataset. It cannot be separated into training and test sets. +It is necessary to explore whether parallelization is possible. +* The spatial dataset needs to be in a .h5ad format with the .X matrix normalized and log-transformed. +* The mode could be set to `cells` if you want to map cells to spots, and the output matrix will be cell x spot probabilities. Alternatively, set it to `clusters` if the goal is to map whole clusters to the spatial data. +* The output is the highest scored cell type for each spot, determined by the cell type projection (using the `tg.project_cell_annotations` function from the Tangram package). +* Other outputs include: a score matrix for spot vs label, a cell x spot probability matrix, and the Tangram output map object in .h5ad format containing all the relevant information. +* It runs using the whole transcriptome, none gene markers are selected. +* All parameters are the default. +The Tangram workflow was generated following the tutorial provided below: +https://tangram-sc.readthedocs.io/en/latest/tutorial_sq_link.html diff --git a/Scripts/Tangram/run_Tangram.py b/Scripts/Tangram/run_Tangram.py new file mode 100644 index 0000000..e420ba3 --- /dev/null +++ b/Scripts/Tangram/run_Tangram.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Tue Aug 8 17:11:20 2023 + +@author: tomas.vega +""" +## Python 3.11.2 +#--------------- Libraries ------------------- +import tangram as tg +import anndata as ad +import scanpy as sc +import numpy as np +import pandas as pd +import sys +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +ref_path = str(sys.argv[1]) +lab_path = str(sys.argv[2]) +out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.argv[3])) +sp_dataset = str(sys.argv[4]) +mode = str(sys.argv[5]) +#if str(sys.argv[5]) != 'None': +# cluster_label = str(sys.argv[5]) +#else: +# cluster_label = None + +#--------------- Data ------------------------- +# read the data +ref = pd.read_csv(ref_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +labels = pd.read_csv(lab_path, + index_col = 0, + sep=',') + +# check if cell names are in the same order in labels and ref +order = all(labels.index == ref.index) +# throw error if order is not the same +if not order: + sys.exit("@ Order of cells in reference and labels do not match") + +ad_sc = ad.AnnData(X = ref, + obs = dict(obs_names=ref.index.astype(str), + label = labels['label']), + var = dict(var_names=ref.columns.astype(str)) + ) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(ad_sc, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(ad_sc) + +# read spatial query +ad_sp = sc.read_h5ad(sp_dataset) + +### Use only the same genes in the spatial and the query +tg.pp_adatas(ad_sc, ad_sp, genes=None) + +if mode == 'clusters': + ad_map = tg.map_cells_to_space( + ad_sc, + ad_sp, + mode=mode, + cluster_label='label') +else: + ad_map = tg.map_cells_to_space( + ad_sc, + ad_sp, + mode=mode) + + +tg.project_cell_annotations(ad_map, ad_sp, annotation='label') +df = ad_sp.obsm['tangram_ct_pred'].copy() + +# Function to get the column name and maximum value for each row +def get_max_column_and_value(row): + pred_label = row.idxmax() + proba_label = row.max() + return pred_label, proba_label + +# Create a new column 'max_column' with the column name containing the maximum value for each row +df['pred_label'], df['score_label'] = zip(*df.apply(get_max_column_and_value, axis=1)) +# Create a new column 'unknown_max_column' to store 'max_column' as 'unknown' if 'max_value' is lower than the threshold +#df['pred_label_reject'] = df.apply(lambda row: 'Unknown' if row['proba_label'] < threshold else row['pred_label'], axis=1) + +print('@ WRITTING PREDICTIONS') +pred_df = pd.DataFrame({'spot': df.index, 'Tangram': df.pred_label}) +pred_df.to_csv(out_path, index = False) +print('@ DONE') + +#------------- Other outputs -------------- +### Save transfered score matrix +print('@ WRITTING TRANSFERED SCORE MATRIX ') +filename = out_other_path + '/label_score_matrix.csv' +df.to_csv(filename, + index=True) #True because we want to conserve the rownames (cells) +print('@ DONE ') + +#### Save map object that contains the cell x spot prob +print('@ WRITTING PROB MATRIX ') +filename = out_other_path + '/Tangram_mapped_object.h5ad' +ad_map.write_h5ad(filename= outputFile_map, + compression='gzip') +print('@ DONE ') + +#### Save cell x spot prob matrix +print('@ WRITTING PROB MATRIX ') +filename = out_other_path + '/prob_matrix.h5ad' +prob_matrix = pd.DataFrame(ad_map.X,index = ad_map.obs_names,columns = ad_map.var_names) +prob_matrix.to_csv(filename, + index=True) #True because we want to conserve the rownames (cells) +print('@ DONE ') From 1c7869f2b2a2757a7ee7a4ae1ae3e53864bec542 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:22:46 -0400 Subject: [PATCH 036/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95334eb..5528aec 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. install.package() ``` -Older version of Matrix package had to be installed for Seurat to work: https://github.com/satijalab/seurat/issues/6746 +Older version of Matrix package needs to be installed for Seurat to work: https://github.com/satijalab/seurat/issues/6746 ``` devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r-project.org") From a7fc1608c98017b585f1b47eb4c0e382c0a41ffe Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:45:42 -0400 Subject: [PATCH 037/144] Update README.md --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 5528aec..5f4ea0a 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,18 @@ Normal parameters were used in both the training and prediction functions, with singleCellNet workflow was generated following the tutorial below: https://pcahan1.github.io/singleCellNet/ +## ACTINN + +Documentation written by: Alva Annett +Date written: 2023-08-08 + +ACTINN code based on actinn_format.py and actinn_predict.py originally found here: https://github.com/mafeiyang/ACTINN + +ACTINN has been spit into testing and predicting. To do this filtering of outlier genes based on expression across query and reference samples had to be removed. The rest of the code has not been changed from the original ACTINN implementation, just rearanged and some parts related to processing multiple samples at the same time removed. + +ACTINN is run with default parameters from original implementation. +Normalization is based on original implementation and paper (cells scaled to total expression value, times 10 000, log2(x+1) normalized) + ## Tangram Documentation written by: Tomas Vega Waichman @@ -221,3 +233,5 @@ It is necessary to explore whether parallelization is possible. * All parameters are the default. The Tangram workflow was generated following the tutorial provided below: https://tangram-sc.readthedocs.io/en/latest/tutorial_sq_link.html + + From 4588200d55fdfb2ebd9777fc92c2614f7f0ddd12 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:49:52 -0400 Subject: [PATCH 038/144] Update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f4ea0a..717f67f 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,9 @@ UPDATE # UPDATE ``` -# Tools Available +# Tools Available + +Single cell RNA reference + single RNA cell query - scPred - SingleR - scClassify @@ -101,9 +103,14 @@ UPDATE - singleCellNet - scHPL - SVMlinear + - SVC - scLearn + - ACTINN - Correlation +Single cell RNA reference + spatial RNA cell query + - Tangram + # Adding New Tools: ``` R From d8dcb36ff616bf119c9a14df4fa4146a2bcfef5f Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:51:43 -0400 Subject: [PATCH 039/144] Update README.md --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 717f67f..7868542 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,8 @@ UPDATE # Tools Available -Single cell RNA reference + single RNA cell query +**Single cell RNA reference + single RNA cell query** + ``` - scPred - SingleR - scClassify @@ -107,10 +108,13 @@ Single cell RNA reference + single RNA cell query - scLearn - ACTINN - Correlation +``` -Single cell RNA reference + spatial RNA cell query - - Tangram +**Single cell RNA reference + spatial RNA cell query** +``` + - Tangram +``` # Adding New Tools: ``` R From 18eddfdd053e4c9df2e8be07b4f003d59ea34e5a Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:53:31 -0400 Subject: [PATCH 040/144] Update README.md --- README.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7868542..16ca1e6 100644 --- a/README.md +++ b/README.md @@ -96,24 +96,25 @@ UPDATE # Tools Available **Single cell RNA reference + single RNA cell query** - ``` - - scPred - - SingleR - - scClassify - - SciBet - - singleCellNet - - scHPL - - SVMlinear - - SVC - - scLearn - - ACTINN - - Correlation + +```ymal +- scPred +- SingleR +- scClassify +- SciBet +- singleCellNet +- scHPL +- SVMlinear +- SVC +- scLearn +- ACTINN +- Correlation ``` **Single cell RNA reference + spatial RNA cell query** -``` - - Tangram +```yaml +- Tangram ``` # Adding New Tools: From 26a1f097e3e17d6ba61e62b844cb4f2c6eef2fcf Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:55:28 -0400 Subject: [PATCH 041/144] Update README.md --- README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 16ca1e6..7a1b724 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,15 @@ Reference based prediction of cell-types using a fast and efficient Snakemake pi Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. **R packages CRAN** + +```ymal - Seurat - tidyverse - MetBrewer - plotly - caret - Matrix +``` ``` install.package() @@ -28,6 +31,8 @@ devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r ``` **R packages bioconductor** + +```ymal - SingleCellExperiment - SummarizedExperiment - ComplexHeatmap @@ -37,22 +42,26 @@ devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r - scuttle - scran - M3Drop - +``` ``` BiocManager::install() ``` -**R packages github** +**R packages github +```ymal - singleCellNet (pcahan1/singleCellNet) - scPred (powellgenomicslab/scPred) - scibet (PaulingLiu/scibet) - scLearn (bm2-lab/scLearn) +``` ``` devtools::install_github() ``` **Python modules** + +```ymal - numpy - pandas - scHPL @@ -64,6 +73,7 @@ devtools::install_github() - tensorflow - tables - snakemake +``` ``` pip install @@ -95,7 +105,7 @@ UPDATE # Tools Available -**Single cell RNA reference + single RNA cell query** +**Single cell RNA reference + single cellRNA query** ```ymal - scPred @@ -112,7 +122,7 @@ UPDATE ``` -**Single cell RNA reference + spatial RNA cell query** +**Single cell RNA reference + spatial RNA query** ```yaml - Tangram ``` From 6d20678ef0bb16d97bc52ce9079ba5039df6681e Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:56:10 -0400 Subject: [PATCH 042/144] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7a1b724..a82037f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. **R packages CRAN** -```ymal +``` - Seurat - tidyverse - MetBrewer @@ -32,7 +32,7 @@ devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r **R packages bioconductor** -```ymal +``` - SingleCellExperiment - SummarizedExperiment - ComplexHeatmap @@ -48,13 +48,12 @@ BiocManager::install() ``` **R packages github -```ymal +``` - singleCellNet (pcahan1/singleCellNet) - scPred (powellgenomicslab/scPred) - scibet (PaulingLiu/scibet) - scLearn (bm2-lab/scLearn) ``` - ``` devtools::install_github() ``` From 100a3ad774bd193bdf28608fd124357509b611ae Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:56:54 -0400 Subject: [PATCH 043/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a82037f..98345dd 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. ``` ``` -install.package() +install.packages() ``` Older version of Matrix package needs to be installed for Seurat to work: https://github.com/satijalab/seurat/issues/6746 From 25811e0fd7a322115392722adcdb4bc2980c3fdc Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 8 Aug 2023 21:57:40 -0400 Subject: [PATCH 044/144] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98345dd..25e6b0d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r BiocManager::install() ``` -**R packages github +**R packages github** + ``` - singleCellNet (pcahan1/singleCellNet) - scPred (powellgenomicslab/scPred) @@ -73,7 +74,6 @@ devtools::install_github() - tables - snakemake ``` - ``` pip install ``` From 62b6b3376c26bb7a537732b45efb3ac82eff95ab Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 9 Aug 2023 08:23:59 -0400 Subject: [PATCH 045/144] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 25e6b0d..9163b9f 100644 --- a/README.md +++ b/README.md @@ -120,11 +120,15 @@ UPDATE - Correlation ``` - **Single cell RNA reference + spatial RNA query** ```yaml - Tangram ``` + +# Resources + +TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM + # Adding New Tools: ``` R From afedf9542e941253cf21af395756b5c277719c75 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:34:29 -0400 Subject: [PATCH 046/144] Update README.md --- README.md | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9163b9f..997cd14 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,10 @@ Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. **R packages CRAN** -``` -- Seurat -- tidyverse -- MetBrewer -- plotly -- caret -- Matrix -``` +```R +pkg = c("Seurat", "tidyverse", "MetBrewer", "plotly", "caret", "Matrix") -``` -install.packages() +install.packages(pkg) ``` Older version of Matrix package needs to be installed for Seurat to work: https://github.com/satijalab/seurat/issues/6746 @@ -33,18 +26,9 @@ devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r **R packages bioconductor** ``` -- SingleCellExperiment -- SummarizedExperiment -- ComplexHeatmap -- WGCNA -- SingleR -- scClassify -- scuttle -- scran -- M3Drop -``` -``` -BiocManager::install() +pkg = c("SingleCellExperiment", "SummarizedExperiment", "ComplexHeatmap", "WGCNA", "SingleR", "scClassify", "scuttle", "scran", "M3Drop") + +BiocManager::install(pkg) ``` **R packages github** From 9a987e2b509a310460735899d38fd1109aebd647 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:48:20 -0400 Subject: [PATCH 047/144] Update README.md --- README.md | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 997cd14..9ed915b 100644 --- a/README.md +++ b/README.md @@ -12,54 +12,54 @@ Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. **R packages CRAN** ```R -pkg = c("Seurat", "tidyverse", "MetBrewer", "plotly", "caret", "Matrix") +pkg = c("Seurat", + "tidyverse", + "MetBrewer", + "plotly", + "caret", + "Matrix") install.packages(pkg) ``` Older version of Matrix package needs to be installed for Seurat to work: https://github.com/satijalab/seurat/issues/6746 -``` +```R devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r-project.org") ``` **R packages bioconductor** -``` -pkg = c("SingleCellExperiment", "SummarizedExperiment", "ComplexHeatmap", "WGCNA", "SingleR", "scClassify", "scuttle", "scran", "M3Drop") +```R +pkg = c("SingleCellExperiment", + "SummarizedExperiment", + "ComplexHeatmap", + "WGCNA", + "SingleR", + "scClassify", + "scuttle", + "scran", + "M3Drop") BiocManager::install(pkg) ``` **R packages github** -``` -- singleCellNet (pcahan1/singleCellNet) -- scPred (powellgenomicslab/scPred) -- scibet (PaulingLiu/scibet) -- scLearn (bm2-lab/scLearn) -``` -``` -devtools::install_github() + +```R +pkg = c("pcahan1/singleCellNet", + "powellgenomicslab/scPred", + "PaulingLiu/scibet", + "bm2-lab/scLearn") + +devtools::install_github(pkg) ``` **Python modules** -```ymal -- numpy -- pandas -- scHPL -- sklearn -- anndata -- matplotlib -- scanpy -- datetime -- tensorflow -- tables -- snakemake -``` -``` -pip install +```bash +pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensorflow tables snakemake ``` # Quickstart From 49b290ebbcfa233a41c4c01fac47687b7ee7f336 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:52:54 -0400 Subject: [PATCH 048/144] Update README.md --- README.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9ed915b..32bbaee 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ Detailed documentation for scClassify train and predict scripts, written July 20 * `scClassify::plotCellTypeTree()` produces a ggplot object. Therefore, I am using `ggplot2::ggsave()` to save it as a png file. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) ## scPred +Documentation written by: Alva Annett +Date written: July 2023 Both reference and query is normaluzed using `Seurat::NormalizeData()`. Needs computed PCA space. Dims set to 1:30 according to tutorial. @@ -145,6 +147,8 @@ Normalization and parameters based on this tutorial: https://powellgenomicslab.github.io/scPred/articles/introduction.html ## SingleR +Documentation written by: Alva Annett +Date written: July 2023 Both reference and query is normaluzed using `scuttle::logNormCounts()`. Both reference and query is converted to SingleCellExperiment objects before normalization. @@ -171,8 +175,8 @@ https://pcahan1.github.io/singleCellNet/ ## Correlation -Documentation written by: Rodrigo Lopez Gutierrez -Date written: 2023-08-02 +Documentation written by: Rodrigo Lopez Gutierrez +Date written: 2023-08-02 The Correlation tool runs a correlation-based cell type prediction on a sample of interest, given the mean gene expression per label for a reference. The function to label by Spearman correlation was originally generated by Selin Jessa and Marie Coutlier @@ -188,7 +192,8 @@ Currently only outputting a table with each cell, the most highly correlated lab ## scLearn -Detailed documentation for scLearn train and predict scripts, written August 2023 by Bhavyaa Chandarana. Added information Tomas Vega Waichman in 2023-08-04. +Detailed documentation for scLearn train and predict scripts, written August 2023 by Bhavyaa Chandarana. +Added information Tomas Vega Waichman in 2023-08-04. Preprocessing performed in the same way as scLearn documentation tutorial [source](https://github.com/bm2-lab/scLearn#tutorial) for `Single-label single cell assignment` @@ -204,8 +209,8 @@ Preprocessing performed in the same way as scLearn documentation tutorial [sourc ## singleCellNet -Documentation written by: Rodrigo Lopez Gutierrez -Date written: 2023-08-01 +Documentation written by: Rodrigo Lopez Gutierrez +Date written: 2023-08-01 Input for `singleCellNet` is raw counts for both reference and query. The reference is normalized within the `scn_train()` function. The query is currently not normalized. In the tutorial example they used raw query data. Furthermore, according to the tutorial, the classification step is robust to the normalization and transformation steps of the query data sets. They claim that one can even directly use raw data to query and still obtains accurate classification. This could be tested in the future with our data to see if normalized queries perform better. @@ -218,8 +223,8 @@ https://pcahan1.github.io/singleCellNet/ ## ACTINN -Documentation written by: Alva Annett -Date written: 2023-08-08 +Documentation written by: Alva Annett +Date written: 2023-08-08 ACTINN code based on actinn_format.py and actinn_predict.py originally found here: https://github.com/mafeiyang/ACTINN @@ -230,8 +235,9 @@ Normalization is based on original implementation and paper (cells scaled to tot ## Tangram -Documentation written by: Tomas Vega Waichman -Date written: 2023-08-08 +Documentation written by: Tomas Vega Waichman +Date written: 2023-08-08 + Tangram maps a single cell that is used as a reference to a spatial dataset. It cannot be separated into training and test sets. It is necessary to explore whether parallelization is possible. * The spatial dataset needs to be in a .h5ad format with the .X matrix normalized and log-transformed. From 96d30f3f8e6f5e9802aad6c5797ed10dee8d3389 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:55:12 -0400 Subject: [PATCH 049/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 32bbaee..8ba488e 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ UPDATE # Tools Available -**Single cell RNA reference + single cellRNA query** +**Single cell RNA reference + single cell RNA query** ```ymal - scPred From fe05177130ae742fc7ad20f2cd4e7235879e0e6b Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 9 Aug 2023 11:04:19 -0400 Subject: [PATCH 050/144] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8ba488e..4f92654 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,11 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor # Quickstart -UPDATE +1. Clone repository and install dependencies +2. Prepare reference +3. Prepare query samples +4. Prepare config file +5. Prepare submission script (HPC) ## Config File: From 9dee688e9ba333d2d16e11bab8beaab029fd7028 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 9 Aug 2023 19:40:14 -0400 Subject: [PATCH 051/144] Dev alva update preproc (#41) * updated preprocessing script * updated snakefile to use new prerpcessing script * code updated. tested with small data set --- Scripts/archive/preprocess.R | 99 +++++++++++++++++++++++++++++ Scripts/preprocess.R | 120 ++++++++++------------------------- Scripts/preprocess_v2.R | 49 -------------- snakefile.annotate | 44 ++++++------- 4 files changed, 154 insertions(+), 158 deletions(-) create mode 100644 Scripts/archive/preprocess.R delete mode 100644 Scripts/preprocess_v2.R diff --git a/Scripts/archive/preprocess.R b/Scripts/archive/preprocess.R new file mode 100644 index 0000000..7215be4 --- /dev/null +++ b/Scripts/archive/preprocess.R @@ -0,0 +1,99 @@ +library(glue) +library(data.table) + +preprocess_data<-function(RefPath, QueryPaths, OutputDir, check_genes = FALSE, genes_required = NULL){ + " + run preproccessing, normalization, and writing down of matrices with common genes + Wrapper script to run preproccessing, normalization, and writing down of matrices with common genes between query and reference datasets + outputs lists of true and predicted cell labels as csv files, as well as computation time. + + Parameters + ---------- + RefPath : Data file path (.csv), cells-genes matrix with cell unique barcodes + as row names and gene names as column names. + LabelsPath : Cell population annotations (per cell) file path (.csv). + QueryPaths : List of Query datasets paths + OutputDir : Output directory defining the path of the exported files. + genes_required: path to file containing required genes in common feature space with column name genes_required + " + + if(check_genes){ + genes_required <- as.vector(read.csv(genes_required$genes_required))} + + message("@Reading reference") + # read data and labels + # Read the reference expression matrix + Data <- fread(RefPath,data.table=FALSE) + message("@Done reading reference") + + row.names(Data) <- Data$V1 + Data <- Data[, 2:ncol(Data)] + colnames(Data) <- toupper(colnames(Data)) + + # set the genes of the reference to common_genes + common_genes <- colnames(Data) + + # loop over all query datasets and get common genes with all + for(query in QueryPaths){ + message("reading Test") + # Read Query + Test <- fread(query,data.table=FALSE) + row.names(Test) <- Test$V1 + Test <- Test[, 2:ncol(Test)] + # Map gene names to upper + colnames(Test) <- toupper(colnames(Test)) + # subset based on common genes + common_genes<- as.vector(intersect(common_genes, colnames(Test))) + } + + # check if the common genes + if(check_genes){ + if(!(genes_required %in% common_genes)){ + warning("Not all genes required in Gene list are found in the common genes between reference and query") + }} + + # loop over query datasets and subset them to common genes and write it down in each query output dir + for(query in QueryPaths){ + # Read Query + Test <- fread(query,data.table=FALSE) + row.names(Test) <- Test$V1 + Test <- Test[, 2:ncol(Test)] + colnames(Test) <- toupper(colnames(Test)) + Test <- Test[,common_genes] + + # write down query expression matrices + dir.create(file.path(OutputDir, basename(dirname(query))), showWarnings = FALSE) + setwd(paste(OutputDir, basename(dirname(query)),sep= '/')) + fwrite(Test, "expression.csv",row.names=T) + } + Data <- Data[,common_genes ] + fwrite(Data, glue("{OutputDir}/expression.csv"),row.names=T) + fwrite(as.data.frame(common_genes), glue("{OutputDir}/common_genes.csv")) +} + +# Get Command line arguments +args <- commandArgs(trailingOnly = TRUE) +# Split the arguments to form lists +arguments <- paste(unlist(args),collapse=' ') +listoptions <- unlist(strsplit(arguments,'--'))[-1] +# Get individual argument names +options.args <- sapply(listoptions,function(x){ + unlist(strsplit(x, ' '))[-1] + }) +options.names <- sapply(listoptions,function(x){ + option <- unlist(strsplit(x, ' '))[1] +}) + +# Set variables containing command line argument values +names(options.args) <- unlist(options.names) +ref <- unlist(options.args['ref']) +print(ref) +test <- unlist(options.args['query']) +output_dir <- unlist(options.args['output_dir' ]) +check_genes <- unlist(options.args['check_genes' ]) +genes_required <- unlist(options.args['genes_required' ]) + + +preprocess_data(ref, test, output_dir, check_genes, genes_required) + + diff --git a/Scripts/preprocess.R b/Scripts/preprocess.R index 7215be4..96bea51 100644 --- a/Scripts/preprocess.R +++ b/Scripts/preprocess.R @@ -1,99 +1,45 @@ -library(glue) -library(data.table) -preprocess_data<-function(RefPath, QueryPaths, OutputDir, check_genes = FALSE, genes_required = NULL){ - " - run preproccessing, normalization, and writing down of matrices with common genes - Wrapper script to run preproccessing, normalization, and writing down of matrices with common genes between query and reference datasets - outputs lists of true and predicted cell labels as csv files, as well as computation time. - - Parameters - ---------- - RefPath : Data file path (.csv), cells-genes matrix with cell unique barcodes - as row names and gene names as column names. - LabelsPath : Cell population annotations (per cell) file path (.csv). - QueryPaths : List of Query datasets paths - OutputDir : Output directory defining the path of the exported files. - genes_required: path to file containing required genes in common feature space with column name genes_required - " +library(tidyverse) - if(check_genes){ - genes_required <- as.vector(read.csv(genes_required$genes_required))} +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +query_paths = strsplit(args[2], split = ' ')[[1]] +out_path = args[3] - message("@Reading reference") - # read data and labels - # Read the reference expression matrix - Data <- fread(RefPath,data.table=FALSE) - message("@Done reading reference") +l = list() - row.names(Data) <- Data$V1 - Data <- Data[, 2:ncol(Data)] - colnames(Data) <- toupper(colnames(Data)) +# read reference +l[['ref']] = data.table::fread(ref_path, header = T) %>% column_to_rownames('V1') - # set the genes of the reference to common_genes - common_genes <- colnames(Data) - - # loop over all query datasets and get common genes with all - for(query in QueryPaths){ - message("reading Test") - # Read Query - Test <- fread(query,data.table=FALSE) - row.names(Test) <- Test$V1 - Test <- Test[, 2:ncol(Test)] - # Map gene names to upper - colnames(Test) <- toupper(colnames(Test)) - # subset based on common genes - common_genes<- as.vector(intersect(common_genes, colnames(Test))) - } - - # check if the common genes - if(check_genes){ - if(!(genes_required %in% common_genes)){ - warning("Not all genes required in Gene list are found in the common genes between reference and query") - }} - - # loop over query datasets and subset them to common genes and write it down in each query output dir - for(query in QueryPaths){ - # Read Query - Test <- fread(query,data.table=FALSE) - row.names(Test) <- Test$V1 - Test <- Test[, 2:ncol(Test)] - colnames(Test) <- toupper(colnames(Test)) - Test <- Test[,common_genes] - - # write down query expression matrices - dir.create(file.path(OutputDir, basename(dirname(query))), showWarnings = FALSE) - setwd(paste(OutputDir, basename(dirname(query)),sep= '/')) - fwrite(Test, "expression.csv",row.names=T) - } - Data <- Data[,common_genes ] - fwrite(Data, glue("{OutputDir}/expression.csv"),row.names=T) - fwrite(as.data.frame(common_genes), glue("{OutputDir}/common_genes.csv")) +# read query +for(p in query_paths){ + tmp = data.table::fread(p, header = T) %>% column_to_rownames('V1') + query = basename(dirname(p)) + l[[query]] = tmp } -# Get Command line arguments -args <- commandArgs(trailingOnly = TRUE) -# Split the arguments to form lists -arguments <- paste(unlist(args),collapse=' ') -listoptions <- unlist(strsplit(arguments,'--'))[-1] -# Get individual argument names -options.args <- sapply(listoptions,function(x){ - unlist(strsplit(x, ' '))[-1] - }) -options.names <- sapply(listoptions,function(x){ - option <- unlist(strsplit(x, ' '))[1] -}) +# get genes for each data frame (colnames) +genes = lapply(l, function(x){(colnames(x))}) -# Set variables containing command line argument values -names(options.args) <- unlist(options.names) -ref <- unlist(options.args['ref']) -print(ref) -test <- unlist(options.args['query']) -output_dir <- unlist(options.args['output_dir' ]) -check_genes <- unlist(options.args['check_genes' ]) -genes_required <- unlist(options.args['genes_required' ]) +# reduce set of genes to the intersect +genes = Reduce(intersect, genes) +# save common genes +data.table::fwrite(data.frame('common_genes' = genes), paste0(out_path, '/common_genes.csv')) -preprocess_data(ref, test, output_dir, check_genes, genes_required) +# filter each data set for common genes +l = lapply(l, function(x){x[,genes]}) +# save reference +tmp = l[['ref']] %>% rownames_to_column() +colnames(tmp)[1] = " " +data.table::fwrite(tmp, file = paste0(out_path, '/expression.csv'), sep = ',') +# save query +query_names = names(l)[!names(l) == 'ref'] +for(q in query_names){ + print(q) + tmp = l[[q]] %>% rownames_to_column() + colnames(tmp)[1] = " " + data.table::fwrite(tmp, file = paste0(out_path, '/', q, '/expression.csv'), sep = ',') +} diff --git a/Scripts/preprocess_v2.R b/Scripts/preprocess_v2.R deleted file mode 100644 index 2e6dcc6..0000000 --- a/Scripts/preprocess_v2.R +++ /dev/null @@ -1,49 +0,0 @@ - -library(tidyverse) - -args = commandArgs(trailingOnly = TRUE) -ref_path = args[1] -query_paths = args[2] -query_paths = strsplit(query_paths, split = ' ')[[1]] -out_path = args[3] - -paths = c(ref_path, query_paths) -print(paths) - -l = list() -for(i in 1:length(paths)){ - tmp = data.table::fread(paths[i], header = T) %>% column_to_rownames('V1') - l[[i]] = tmp -} - -# get genes for each data frame (colnames) -genes = lapply(l, function(x){(colnames(x))}) - -# reduce set of genes to the intersect -genes = Reduce(intersect, genes) - -# save common genes -data.table::fwrite(data.frame('common_genes' = genes)) - -# filter expression data to the intersect -for(i in 1:length(l)){ - tmp = l[[i]][,genes] - - # calculate fraction of genes kept - fr = (ncol(tmp)/ncol(l[[i]])) - - # throw error if fraction is less than 0.5 - if(fr < 0.5){ - stop(paste(" ", - "@ Number of genes after filtering is less than 50% of original data for sample:", - paths[i], - sep="\n")) - } - - # save filtered data - if(i == 1){ - data.table::fwrite(tmp, ) - }else{ - data.table::fwrite(tmp) - } -} diff --git a/snakefile.annotate b/snakefile.annotate index 73687ba..4b5dd01 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -1,3 +1,8 @@ + +#---------------------------------------------------- +# Setup +#---------------------------------------------------- + # import libraries import os from datetime import datetime @@ -8,15 +13,10 @@ samples = [os.path.basename(os.path.dirname(query_path)) for query_path in confi now = datetime.now() dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") -# Create subdirectories for each query sample -for sample in samples: - path = "{output_dir}/{sample}".format(output_dir = config["output_dir"], sample = sample) - if not os.path.exists(path): - os.mkdir(path) +#---------------------------------------------------- +# Final rule all +#---------------------------------------------------- -""" -One rule that directs the DAG which is represented in the rulegraph -""" rule all: input: expand(config["output_dir"] + "/{sample}/Prediction_Summary.tsv", @@ -24,10 +24,11 @@ rule all: expand(config['output_dir'] + '/{sample}/report/{sample}.prediction_report.' + dt_string + '.html', sample = samples) -""" -rule that gets the gets the interesction in genes between samples and reference -It outputs temporary reference and query datasets based on the common genes -""" + +#---------------------------------------------------- +# Preprocess +#---------------------------------------------------- + rule preprocess: input: reference = config['training_reference'], @@ -36,19 +37,18 @@ rule preprocess: reference = config['output_dir'] + "/expression.csv", query = expand(config['output_dir'] + "/{sample}/expression.csv", sample = samples) params: - check_genes = bool(config['check_genes']), - genes_required = config['genes_required'] + basedir = {workflow.basedir}, + out_path = config['output_dir'] log: config['output_dir'] + "/preprocess.log" priority: 50 shell: - "Rscript {workflow.basedir}/Scripts/preprocess.R " - "--ref {input.reference} " - "--query {input.query} " - "--output_dir {config[output_dir]} " - "--check_genes {params.check_genes} " - "--genes_required {params.genes_required} " - "&> {log} " - + """ + Rscript {params.basedir}/Scripts/preprocess.R \ + {input.reference} \ + "{input.query}" \ + {params.out_path} \ + &> {log} + """ #---------------------------------------------------- # Consensus #---------------------------------------------------- From eb0ae35b73792005db3dd0044e34d8b301f53cec Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Thu, 10 Aug 2023 10:03:47 -0400 Subject: [PATCH 052/144] Update README.md --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4f92654..d186313 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,19 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor 4. Prepare config file 5. Prepare submission script (HPC) -## Config File: +## 1. Clone repository and install dependencies + +## 2. Prepare reference + +## 3. Prepare query + +## 4. Prepare config file ```yaml # UPDATE ``` -## Submission File +## 5. Prepare submission script ### Annotate From 9b700d78950fcc917df658de5b5656889008daed Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Thu, 10 Aug 2023 10:06:12 -0400 Subject: [PATCH 053/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d186313..202842b 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ Date written: 2023-08-08 ACTINN code based on actinn_format.py and actinn_predict.py originally found here: https://github.com/mafeiyang/ACTINN -ACTINN has been spit into testing and predicting. To do this filtering of outlier genes based on expression across query and reference samples had to be removed. The rest of the code has not been changed from the original ACTINN implementation, just rearanged and some parts related to processing multiple samples at the same time removed. +ACTINN has been spit into testing and predicting. To do this, filtering of outlier genes based on expression across all query samples and reference had to be removed. The rest of the code has not been changed from the original ACTINN implementation, just rearanged and some parts related to processing multiple samples at the same time removed. ACTINN is run with default parameters from original implementation. Normalization is based on original implementation and paper (cells scaled to total expression value, times 10 000, log2(x+1) normalized) From a921a1e9dfc5c7d045eab4ea544d1918850ed263 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Thu, 10 Aug 2023 15:42:12 -0400 Subject: [PATCH 054/144] Dev alva actinn (#39) * ACTINN split into esting and training. Annotate and Benchmark workflows updated with new rules for ACTINN * updated snakefile --- Scripts/ACTINN/actinn_format.py | 62 ---- Scripts/ACTINN/predict_ACTINN.py | 303 ++++++++++++++++++ .../{actinn_predict.py => train_ACTINN.py} | 101 +++--- Scripts/{ => archive}/run_ACTINN.py | 0 snakefile.annotate | 64 ++-- snakefile.benchmark | 57 +++- 6 files changed, 456 insertions(+), 131 deletions(-) delete mode 100644 Scripts/ACTINN/actinn_format.py create mode 100644 Scripts/ACTINN/predict_ACTINN.py rename Scripts/ACTINN/{actinn_predict.py => train_ACTINN.py} (81%) rename Scripts/{ => archive}/run_ACTINN.py (100%) diff --git a/Scripts/ACTINN/actinn_format.py b/Scripts/ACTINN/actinn_format.py deleted file mode 100644 index f0dac91..0000000 --- a/Scripts/ACTINN/actinn_format.py +++ /dev/null @@ -1,62 +0,0 @@ -import numpy as np -import pandas as pd -import scipy.io -import os -import argparse - -def get_parser(parser=None): - if parser == None: - parser = argparse.ArgumentParser() - parser.add_argument("-i", "--input", type=str, help="Path to the input file or the 10X directory.") - parser.add_argument("-o", "--output", type=str, help="Prefix of the output file.") - parser.add_argument("-f", "--format", type=str, help="Format of the input file (10X_V2, 10X_V3, txt, csv).") - return parser - -if __name__ == '__main__': - parser = get_parser() - args = parser.parse_args() - if args.format == "10X_V2": - path = args.input - if path[-1] != "/": - path += "/" - new = scipy.io.mmread(os.path.join(path, "matrix.mtx")) - genes = list(pd.read_csv(path+"genes.tsv", header=None, sep='\t')[1]) - barcodes = list(pd.read_csv(path+"barcodes.tsv", header=None)[0]) - new = pd.DataFrame(np.array(new.todense()), index=genes, columns=barcodes) - new.fillna(0, inplace=True) - uniq_index = np.unique(new.index, return_index=True)[1] - new = new.iloc[uniq_index,] - new = new.loc[new.sum(axis=1)>0, :] - print("Dimension of the matrix after removing all-zero rows:", new.shape) - new.to_hdf(args.output+".h5", key="dge", mode="w", complevel=3) - - if args.format == "10X_V3": - path = args.input - if path[-1] != "/": - path += "/" - new = scipy.io.mmread(os.path.join(path, "matrix.mtx.gz")) - genes = list(pd.read_csv(path+"features.tsv.gz", header=None, sep='\t')[1]) - barcodes = list(pd.read_csv(path+"barcodes.tsv.gz", header=None)[0]) - new = pd.DataFrame(np.array(new.todense()), index=genes, columns=barcodes) - new.fillna(0, inplace=True) - uniq_index = np.unique(new.index, return_index=True)[1] - new = new.iloc[uniq_index,] - new = new.loc[new.sum(axis=1)>0, :] - print("Dimension of the matrix after removing all-zero rows:", new.shape) - new.to_hdf(args.output+".h5", key="dge", mode="w", complevel=3) - - if args.format == "csv": - new = pd.read_csv(args.input, index_col=0) - uniq_index = np.unique(new.index, return_index=True)[1] - new = new.iloc[uniq_index,] - new = new.loc[new.sum(axis=1)>0, :] - print("Dimension of the matrix after removing all-zero rows:", new.shape) - new.to_hdf(args.output+".h5", key="dge", mode="w", complevel=3) - - if args.format == "txt": - new = pd.read_csv(args.input, index_col=0, sep="\t") - uniq_index = np.unique(new.index, return_index=True)[1] - new = new.iloc[uniq_index,] - new = new.loc[new.sum(axis=1)>0, :] - print("Dimension of the matrix after removing all-zero rows:", new.shape) - new.to_hdf(args.output+".h5", key="dge", mode="w", complevel=3) diff --git a/Scripts/ACTINN/predict_ACTINN.py b/Scripts/ACTINN/predict_ACTINN.py new file mode 100644 index 0000000..864fb3d --- /dev/null +++ b/Scripts/ACTINN/predict_ACTINN.py @@ -0,0 +1,303 @@ +import numpy as np +import pandas as pd +import sys +import math +import collections +import tensorflow as tf +tf.compat.v1.disable_eager_execution() +import argparse +import timeit +run_time = timeit.default_timer() +from tensorflow.python.framework import ops +import pickle +import os + + +def get_parser(parser=None): + if parser == None: + parser = argparse.ArgumentParser() + parser.add_argument("-ts", "--test_set", type=str, help="Training set file path.") + parser.add_argument("-mp", "--model_path", type=str, help="Model file path.") + parser.add_argument("-pp", "--pred_path", type=str, help="Output prediction file path.") + return parser + + +# Get common genes, normalize and scale the sets +def scale_sets(sets): + # input -- set to be scaled + # output -- scaled set + sets = sets.to_numpy() + sets = np.divide(sets, np.sum(sets, axis=0, keepdims=True)) * 10000 + sets = np.log2(sets+1) + return sets + + + +# Turn labels into matrix +def one_hot_matrix(labels, C): + # input -- labels (true labels of the sets), C (# types) + # output -- one hot matrix with shape (# types, # samples) + C = tf.constant(C, name = "C") + one_hot_matrix = tf.one_hot(labels, C, axis = 0) + sess = tf.compat.v1.Session() + one_hot = sess.run(one_hot_matrix) + sess.close() + return one_hot + + + +# Make types to labels dictionary +def type_to_label_dict(types): + # input -- types + # output -- type_to_label dictionary + type_to_label_dict = {} + all_type = list(set(types)) + for i in range(len(all_type)): + type_to_label_dict[all_type[i]] = i + return type_to_label_dict + + + +# Convert types to labels +def convert_type_to_label(types, type_to_label_dict): + # input -- list of types, and type_to_label dictionary + # output -- list of labels + types = list(types) + labels = list() + for type in types: + labels.append(type_to_label_dict[type]) + return labels + + + +# Function to create placeholders +def create_placeholders(n_x, n_y): + X = tf.compat.v1.placeholder(tf.float32, shape = (n_x, None)) + Y = tf.compat.v1.placeholder(tf.float32, shape = (n_y, None)) + return X, Y + + + +# Initialize parameters +def initialize_parameters(nf, ln1, ln2, ln3, nt): + # input -- nf (# of features), ln1 (# nodes in layer1), ln2 (# nodes in layer2), nt (# types) + # output -- a dictionary of tensors containing W1, b1, W2, b2, W3, b3 + tf.compat.v1.set_random_seed(3) # set seed to make the results consistant + W1 = tf.compat.v1.get_variable("W1", [ln1, nf], initializer = tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform", seed = 3)) + b1 = tf.compat.v1.get_variable("b1", [ln1, 1], initializer = tf.compat.v1.zeros_initializer()) + W2 = tf.compat.v1.get_variable("W2", [ln2, ln1], initializer = tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform", seed = 3)) + b2 = tf.compat.v1.get_variable("b2", [ln2, 1], initializer = tf.compat.v1.zeros_initializer()) + W3 = tf.compat.v1.get_variable("W3", [ln3, ln2], initializer = tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform", seed = 3)) + b3 = tf.compat.v1.get_variable("b3", [ln3, 1], initializer = tf.compat.v1.zeros_initializer()) + W4 = tf.compat.v1.get_variable("W4", [nt, ln3], initializer = tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform", seed = 3)) + b4 = tf.compat.v1.get_variable("b4", [nt, 1], initializer = tf.compat.v1.zeros_initializer()) + parameters = {"W1": W1, "b1": b1, "W2": W2, "b2": b2, "W3": W3, "b3": b3, "W4": W4, "b4": b4} + return parameters + + + +# Forward propagation function +def forward_propagation(X, parameters): + # function model: LINEAR -> RELU -> LINEAR -> RELU -> LINEAR -> SOFTMAX + # input -- dataset with shape (# features, # sample), parameters "W1", "b1", "W2", "b2", "W3", "b3" + # output -- the output of the last linear unit + W1 = parameters['W1'] + b1 = parameters['b1'] + W2 = parameters['W2'] + b2 = parameters['b2'] + W3 = parameters['W3'] + b3 = parameters['b3'] + W4 = parameters['W4'] + b4 = parameters['b4'] + # forward calculations + Z1 = tf.add(tf.matmul(W1, X), b1) + A1 = tf.nn.relu(Z1) + Z2 = tf.add(tf.matmul(W2, A1), b2) + A2 = tf.nn.relu(Z2) + Z3 = tf.add(tf.matmul(W3, A2), b3) + A3 = tf.nn.relu(Z3) + Z4 = tf.add(tf.matmul(W4, A3), b4) + return Z4 + + + +# Compute cost +def compute_cost(Z4, Y, parameters, lambd=0.01): + # input -- Z3 (output of forward propagation with shape (# types, # samples)), Y (true labels, same shape as Z3) + # output -- tensor of teh cost function + logits = tf.transpose(a=Z4) + labels = tf.transpose(a=Y) + cost = tf.reduce_mean(input_tensor=tf.nn.softmax_cross_entropy_with_logits(logits = logits, labels = labels)) + \ + (tf.nn.l2_loss(parameters["W1"]) + tf.nn.l2_loss(parameters["W2"]) + tf.nn.l2_loss(parameters["W3"]) + tf.nn.l2_loss(parameters["W4"])) * lambd + return cost + + + +# Get the mini batches +def random_mini_batches(X, Y, mini_batch_size=32, seed=1): + # input -- X (training set), Y (true labels) + # output -- mini batches + ns = X.shape[1] + mini_batches = [] + np.random.seed(seed) + # shuffle (X, Y) + permutation = list(np.random.permutation(ns)) + shuffled_X = X[:, permutation] + shuffled_Y = Y[:, permutation] + # partition (shuffled_X, shuffled_Y), minus the end case. + num_complete_minibatches = int(math.floor(ns/mini_batch_size)) # number of mini batches of size mini_batch_size in your partitionning + for k in range(0, num_complete_minibatches): + mini_batch_X = shuffled_X[:, k * mini_batch_size : k * mini_batch_size + mini_batch_size] + mini_batch_Y = shuffled_Y[:, k * mini_batch_size : k * mini_batch_size + mini_batch_size] + mini_batch = (mini_batch_X, mini_batch_Y) + mini_batches.append(mini_batch) + # handling the end case (last mini-batch < mini_batch_size) + if ns % mini_batch_size != 0: + mini_batch_X = shuffled_X[:, num_complete_minibatches * mini_batch_size : ns] + mini_batch_Y = shuffled_Y[:, num_complete_minibatches * mini_batch_size : ns] + mini_batch = (mini_batch_X, mini_batch_Y) + mini_batches.append(mini_batch) + return mini_batches + + + +# Forward propagation for prediction +def forward_propagation_for_predict(X, parameters): + # input -- X (dataset used to make prediction), papameters after training + # output -- the output of the last linear unit + W1 = parameters['W1'] + b1 = parameters['b1'] + W2 = parameters['W2'] + b2 = parameters['b2'] + W3 = parameters['W3'] + b3 = parameters['b3'] + W4 = parameters['W4'] + b4 = parameters['b4'] + Z1 = tf.add(tf.matmul(W1, X), b1) + A1 = tf.nn.relu(Z1) + Z2 = tf.add(tf.matmul(W2, A1), b2) + A2 = tf.nn.relu(Z2) + Z3 = tf.add(tf.matmul(W3, A2), b3) + A3 = tf.nn.relu(Z3) + Z4 = tf.add(tf.matmul(W4, A3), b4) + return Z4 + + + +# Predict function +def predict(X, parameters): + # input -- X (dataset used to make prediction), papameters after training + # output -- prediction + W1 = tf.convert_to_tensor(value=parameters["W1"]) + b1 = tf.convert_to_tensor(value=parameters["b1"]) + W2 = tf.convert_to_tensor(value=parameters["W2"]) + b2 = tf.convert_to_tensor(value=parameters["b2"]) + W3 = tf.convert_to_tensor(value=parameters["W3"]) + b3 = tf.convert_to_tensor(value=parameters["b3"]) + W4 = tf.convert_to_tensor(value=parameters["W4"]) + b4 = tf.convert_to_tensor(value=parameters["b4"]) + params = {"W1": W1, "b1": b1, "W2": W2, "b2": b2, "W3": W3, "b3": b3, "W4": W4, "b4": b4} + x = tf.compat.v1.placeholder("float") + z4 = forward_propagation_for_predict(x, params) + p = tf.argmax(input=z4) + sess = tf.compat.v1.Session() + prediction = sess.run(p, feed_dict = {x: X}) + return prediction + + + +# Build the model +def model(X_train, Y_train, X_test, starting_learning_rate = 0.0001, num_epochs = 1500, minibatch_size = 128, print_cost = True): + # input -- X_train (training set), Y_train(training labels), X_test (test set), Y_test (test labels), + # output -- trained parameters + ops.reset_default_graph() # to be able to rerun the model without overwriting tf variables + tf.compat.v1.set_random_seed(3) + seed = 3 + (nf, ns) = X_train.shape + nt = Y_train.shape[0] + costs = [] + # create placeholders of shape (nf, nt) + X, Y = create_placeholders(nf, nt) + # initialize parameters + parameters = initialize_parameters(nf=nf, ln1=100, ln2=50, ln3=25, nt=nt) + # forward propagation: build the forward propagation in the tensorflow graph + Z4 = forward_propagation(X, parameters) + # cost function: add cost function to tensorflow graph + cost = compute_cost(Z4, Y, parameters, 0.005) + # Use learning rate decay + global_step = tf.Variable(0, trainable=False) + learning_rate = tf.compat.v1.train.exponential_decay(starting_learning_rate, global_step, 1000, 0.95, staircase=True) + # backpropagation: define the tensorflow optimizer, AdamOptimizer is used. + optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate=learning_rate) + trainer = optimizer.minimize(cost, global_step=global_step) + # initialize all the variables + init = tf.compat.v1.global_variables_initializer() + # start the session to compute the tensorflow graph + with tf.compat.v1.Session() as sess: + # run the initialization + sess.run(init) + # do the training loop + for epoch in range(num_epochs): + epoch_cost = 0. + num_minibatches = int(ns / minibatch_size) + seed = seed + 1 + minibatches = random_mini_batches(X_train, Y_train, minibatch_size, seed) + for minibatch in minibatches: + # select a minibatch + (minibatch_X, minibatch_Y) = minibatch + # run the session to execute the "optimizer" and the "cost", the feedict contains a minibatch for (X,Y). + _ , minibatch_cost = sess.run([trainer, cost], feed_dict={X: minibatch_X, Y: minibatch_Y}) + epoch_cost += minibatch_cost / num_minibatches + # print the cost every epoch + if print_cost == True and (epoch+1) % 5 == 0: + print ("Cost after epoch %i: %f" % (epoch+1, epoch_cost)) + costs.append(epoch_cost) + parameters = sess.run(parameters) + print ("Parameters have been trained!") + # calculate the correct predictions + correct_prediction = tf.equal(tf.argmax(input=Z4), tf.argmax(input=Y)) + # calculate accuracy on the test set + accuracy = tf.reduce_mean(input_tensor=tf.cast(correct_prediction, "float")) + print ("Train Accuracy:", accuracy.eval({X: X_train, Y: Y_train})) + return parameters + + + +if __name__ == '__main__': + parser = get_parser() + args = parser.parse_args() + + # load model + print('@ LOAD MODEL') + model = pickle.load(open(args.model_path, 'rb')) + + dict_path = os.path.dirname(os.path.abspath(args.model_path)) + label_to_type_dict = pickle.load(open(dict_path + '/label_to_type_dict.pkl', 'rb')) + + print('@ DONE') + + # Read query + query = pd.read_csv(args.test_set, index_col=0) + + # transpose query + query = query.transpose() + + # save barcodes + barcode = list(query.columns) + + print("Dimension of the matrix after removing all-zero rows:", query.shape) + + # noramlize query + query = scale_sets(query) + + # predict + test_predict = predict(query, model) + predicted_label = [] + + for i in range(len(test_predict)): + predicted_label.append(label_to_type_dict[test_predict[i]]) + + predicted_label = pd.DataFrame({"cell":barcode, "ACTINN":predicted_label}) + predicted_label.to_csv(args.pred_path, sep=",", index=False) + +print("Run time:", timeit.default_timer() - run_time) diff --git a/Scripts/ACTINN/actinn_predict.py b/Scripts/ACTINN/train_ACTINN.py similarity index 81% rename from Scripts/ACTINN/actinn_predict.py rename to Scripts/ACTINN/train_ACTINN.py index f838792..fe57cd1 100644 --- a/Scripts/ACTINN/actinn_predict.py +++ b/Scripts/ACTINN/train_ACTINN.py @@ -9,14 +9,15 @@ import timeit run_time = timeit.default_timer() from tensorflow.python.framework import ops - +import pickle +import os def get_parser(parser=None): if parser == None: - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser() parser.add_argument("-trs", "--train_set", type=str, help="Training set file path.") parser.add_argument("-trl", "--train_label", type=str, help="Training label file path.") - parser.add_argument("-ts", "--test_set", type=str, help="Training set file path.") + parser.add_argument("-mp", "--model_path", type=str, help="Output model path.") parser.add_argument("-lr", "--learning_rate", type=float, help="Learning rate (default: 0.0001)", default=0.0001) parser.add_argument("-ne", "--num_epochs", type=int, help="Number of epochs (default: 50)", default=50) parser.add_argument("-ms", "--minibatch_size", type=int, help="Minibatch size (default: 128)", default=128) @@ -28,29 +29,14 @@ def get_parser(parser=None): # Get common genes, normalize and scale the sets def scale_sets(sets): - # input -- a list of all the sets to be scaled - # output -- scaled sets - common_genes = set(sets[0].index) - for i in range(1, len(sets)): - common_genes = set.intersection(set(sets[i].index),common_genes) - common_genes = sorted(list(common_genes)) - sep_point = [0] - for i in range(len(sets)): - sets[i] = sets[i].loc[common_genes,] - sep_point.append(sets[i].shape[1]) - total_set = np.array(pd.concat(sets, axis=1, sort=False), dtype=np.float32) - total_set = np.divide(total_set, np.sum(total_set, axis=0, keepdims=True)) * 10000 - total_set = np.log2(total_set+1) - expr = np.sum(total_set, axis=1) - total_set = total_set[np.logical_and(expr >= np.percentile(expr, 1), expr <= np.percentile(expr, 99)),] - cv = np.std(total_set, axis=1) / np.mean(total_set, axis=1) - total_set = total_set[np.logical_and(cv >= np.percentile(cv, 1), cv <= np.percentile(cv, 99)),] - for i in range(len(sets)): - sets[i] = total_set[:, sum(sep_point[:(i+1)]):sum(sep_point[:(i+2)])] + # input -- set to be scaled + # output -- scaled set + sets = sets.to_numpy() + sets = np.divide(sets, np.sum(sets, axis=0, keepdims=True)) * 10000 + sets = np.log2(sets+1) return sets - # Turn labels into matrix def one_hot_matrix(labels, C): # input -- labels (true labels of the sets), C (# types) @@ -225,7 +211,7 @@ def predict(X, parameters): # Build the model -def model(X_train, Y_train, X_test, starting_learning_rate = 0.0001, num_epochs = 1500, minibatch_size = 128, print_cost = True): +def model(X_train, Y_train, starting_learning_rate = 0.0001, num_epochs = 1500, minibatch_size = 128, print_cost = True): # input -- X_train (training set), Y_train(training labels), X_test (test set), Y_test (test labels), # output -- trained parameters ops.reset_default_graph() # to be able to rerun the model without overwriting tf variables @@ -284,30 +270,51 @@ def model(X_train, Y_train, X_test, starting_learning_rate = 0.0001, num_epochs if __name__ == '__main__': parser = get_parser() args = parser.parse_args() - # Read in the files - train_set = pd.read_hdf(args.train_set, key="dge") - train_set.index = [s.upper() for s in train_set.index] - train_set = train_set.loc[~train_set.index.duplicated(keep='first')] - train_label = pd.read_csv(args.train_label, header=None, sep="\t") - test_set = pd.read_hdf(args.test_set, key="dge") - test_set.index = [s.upper() for s in test_set.index] - test_set = test_set.loc[~test_set.index.duplicated(keep='first')] - barcode = list(test_set.columns) - nt = len(set(train_label.iloc[:,1])) - train_set, test_set = scale_sets([train_set, test_set]) - type_to_label_dict = type_to_label_dict(train_label.iloc[:,1]) + + # read reference + ref = pd.read_csv(args.train_set, index_col=0, header=0) + print("Dimension of reference:", ref.shape) + + # read reference labels + labels = pd.read_csv(args.train_label, index_col=0, header=0, sep=',') + print("Dimension of labels:", labels.shape) + nt = len(set(labels.iloc[:,0])) + + print(ref.head()) + print(labels.head()) + + # check if cell names are in the same order in labels and ref + order = all(labels.index == ref.index) + + # throw error if order is not the same + if not order: + sys.exit("@ Order of cells in reference and labels do not match") + + # transpose reference + ref = ref.transpose() + + # noramlize reference + ref = scale_sets(ref) + + # prepare dictionary from reference labels + type_to_label_dict = type_to_label_dict(labels.iloc[:,0]) label_to_type_dict = {v: k for k, v in type_to_label_dict.items()} print("Cell Types in training set:", type_to_label_dict) - print("# Trainng cells:", train_label.shape[0]) - train_label = convert_type_to_label(train_label.iloc[:,1], type_to_label_dict) - train_label = one_hot_matrix(train_label, nt) - parameters = model(train_set, train_label, test_set, \ - args.learning_rate, args.num_epochs, args.minibatch_size, args.print_cost) - test_predict = predict(test_set, parameters) - predicted_label = [] + print("# Trainng cells:", labels.shape[0]) + + labels = convert_type_to_label(labels.iloc[:,0], type_to_label_dict) + labels = one_hot_matrix(labels, nt) + + # train model + model = model(ref, labels, args.learning_rate, args.num_epochs, args.minibatch_size, args.print_cost) + + print('@ SAVE MODEL') + pickle.dump(model, open(args.model_path, 'wb')) + print('@ DONE') + + # save label_to_type_dict + out_path = os.path.dirname(os.path.abspath(args.model_path)) + pickle.dump(label_to_type_dict, open(out_path + '/label_to_type_dict.pkl', 'wb')) + - for i in range(len(test_predict)): - predicted_label.append(label_to_type_dict[test_predict[i]]) - predicted_label = pd.DataFrame({"cellname":barcode, "celltype":predicted_label}) - predicted_label.to_csv("predicted_label.txt", sep="\t", index=False) print("Run time:", timeit.default_timer() - run_time) diff --git a/Scripts/run_ACTINN.py b/Scripts/archive/run_ACTINN.py similarity index 100% rename from Scripts/run_ACTINN.py rename to Scripts/archive/run_ACTINN.py diff --git a/snakefile.annotate b/snakefile.annotate index 4b5dd01..39cb53a 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -334,7 +334,7 @@ rule train_scHPL: reference = config['output_dir'] + "/expression.csv", labfile = config['reference_annotations'] output: - model = config['output_dir'] + "/scHPL/scHPL_model.Rda" + model = config['output_dir'] + "/scHPL/scHPL_model.pkl" params: basedir = {workflow.basedir}, classifier = 'svm', @@ -359,7 +359,7 @@ rule train_scHPL: rule predict_scHPL: input: query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/scHPL/scHPL_model.Rda" + model = config['output_dir'] + "/scHPL/scHPL_model.pkl" output: pred = config['output_dir'] + "/{sample}/scHPL/scHPL_pred.csv" params: @@ -390,7 +390,7 @@ rule train_SVMlinear: reference = config['output_dir'] + "/expression.csv", labfile = config['reference_annotations'] output: - model = config['output_dir'] + "/SVMlinear/SVMlinear_model.Rda" + model = config['output_dir'] + "/SVMlinear/SVMlinear_model.pkl" params: basedir = {workflow.basedir} log: @@ -412,7 +412,7 @@ rule train_SVMlinear: rule predict_SVMlinear: input: query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/SVMlinear/SVMlinear_model.Rda" + model = config['output_dir'] + "/SVMlinear/SVMlinear_model.pkl" output: pred = config['output_dir'] + "/{sample}/SVMlinear/SVMlinear_pred.csv" params: @@ -638,7 +638,7 @@ rule predict_scLearn: log: config['output_dir'] + "/{sample}/scLearn/scLearn.log" benchmark: - config['output_dir'] + "/{sample}/scLearn/scLearn_predict_benchmark.txt" + config['output_dir'] + "/{sample}/scLearn/scLearn_train_benchmark.txt" threads: 1 resources: shell: @@ -655,26 +655,50 @@ rule predict_scLearn: # ACTINN #---------------------------------------------------- -rule ACTINN: +rule train_ACTINN: input: reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'], - query = expand("{output_dir}/{sample}/expression.csv",sample = samples,output_dir=config['output_dir']), - output_dir = expand("{output_dir}/{sample}",sample = samples,output_dir=config['output_dir']) + labfile = config['reference_annotations'] output: - pred = expand("{output_dir}/{sample}/ACTINN/ACTINN_pred.csv", sample = samples,output_dir=config["output_dir"]), - query_time = expand("{output_dir}/{sample}/ACTINN/ACTINN_query_time.csv",sample = samples,output_dir=config["output_dir"]), - training_time = expand("{output_dir}/{sample}/ACTINN/ACTINN_training_time.csv",sample = samples,output_dir=config["output_dir"]) - log: expand("{output_dir}/{sample}/ACTINN/ACTINN.log", sample = samples,output_dir=config["output_dir"]) + model = config['output_dir'] + "/ACTINN/ACTINN_model.pkl" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} + log: + config['output_dir'] + "/ACTINN/ACTINN.log" + benchmark: + config['output_dir'] + "/ACTINN/ACTINN_predict_benchmark.txt" + shell: + """ + python {params.basedir}/Scripts/ACTINN/train_ACTINN.py \ + -trs {input.reference} \ + -trl {input.labfile} \ + -mp {output.model} \ + &> {log} + """ + +rule predict_ACTINN: + input: + query = config['output_dir'] + "/{sample}/expression.csv", + model = config['output_dir'] + "/ACTINN/ACTINN_model.pkl" + output: + pred = config['output_dir'] + "/{sample}/ACTINN/ACTINN_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/ACTINN/ACTINN.log" + benchmark: + config['output_dir'] + "/{sample}/ACTINN/ACTINN_predict_benchmark.txt" + threads: 1 + resources: shell: - "python3 {params.basedir}/Scripts/run_ACTINN.py " - "--ref {input.reference} " - "--labs {input.labfile} " - "--query {input.query} " - "--output_dir {input.output_dir} " - "&> {log}" + """ + python {params.basedir}/Scripts/ACTINN/predict_ACTINN.py \ + -ts {input.query} \ + -mp {input.model} \ + -pp {output.pred} \ + &> {log} + + """ #---------------------------------------------------- # The End diff --git a/snakefile.benchmark b/snakefile.benchmark index 3a49fa3..abf253a 100644 --- a/snakefile.benchmark +++ b/snakefile.benchmark @@ -1,13 +1,17 @@ # import libraries import os +from datetime import datetime n_folds = list(range(1, config['benchmark']['n_folds']+1)) +now = datetime.now() +dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") + """ One rule that directs the DAG which is represented in the rulegraph """ rule all: - input: report_path = config['output_dir'] + '/report/benchmark_report.html' + input: report_path = config['output_dir'] + '/report/' + dt_string + '.benchmark_report.html' #---------------------------------------------------- # Subset Folds @@ -50,7 +54,7 @@ rule knit_report: folds_index = n_folds, tool = config['tools_to_run']) output: - report_path = config['output_dir'] + '/report/benchmark_report.html' + report_path = config['output_dir'] + '/report/' + dt_string + '.benchmark_report.html' log: config['output_dir'] + "/report/report.log" params: @@ -607,6 +611,55 @@ rule predict_scHPL: &> {log} """ +#---------------------------------------------------- +# ACTINN +#---------------------------------------------------- + +rule train_ACTINN: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_model.pkl" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_predict_benchmark.txt" + shell: + """ + python {params.basedir}/Scripts/ACTINN/train_ACTINN.py \ + -trs {input.reference} \ + -trl {input.labfile} \ + -mp {output.model} \ + &> {log} + """ + +rule predict_ACTINN: + input: + query = config['output_dir'] + "/fold{folds_index}/test.csv", + model = config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_model.pkl" + output: + pred = config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_predict_benchmark.txt" + threads: 1 + resources: + shell: + """ + python {params.basedir}/Scripts/ACTINN/predict_ACTINN.py \ + -ts {input.query} \ + -mp {input.model} \ + -pp {output.pred} \ + &> {log} + + """ + #---------------------------------------------------- # The End #---------------------------------------------------- From 68df9201463bbbfa22e1fd899d9adac0326ce34e Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Sun, 13 Aug 2023 11:50:17 -0400 Subject: [PATCH 055/144] Notebook updated with warnings for tools with high percentage unsure (#43) --- .gitignore | 2 ++ Notebooks/annotate_report.Rmd | 64 +++++++++++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f51901a..cf48696 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ .ipynb_checkpoints config.yml test/ +Notebooks/annotate_report_cache/ +Notebooks/benchmark_report_cache/ diff --git a/Notebooks/annotate_report.Rmd b/Notebooks/annotate_report.Rmd index f91526f..17d7cb9 100644 --- a/Notebooks/annotate_report.Rmd +++ b/Notebooks/annotate_report.Rmd @@ -20,16 +20,16 @@ params: --- ```{r setup, knitr_options, echo=F} -knitr::opts_chunk$set(message = FALSE, warning=FALSE, cache = T) -set.seed(12345) +knitr::opts_chunk$set(message = FALSE, warning=FALSE) ``` -```{r fig.show='hide'} +```{r fig.show='hide', include=F} library(tidyverse) library(ComplexHeatmap) library(Seurat) library(MetBrewer) library(plotly) +library(kableExtra) #empty plotly plot to make sure the other plotly plots get printed later plotly_empty() @@ -203,6 +203,41 @@ umap_plotly = function(seurat, meta_column, pal){ return(p2) } +calculate_percentage_unsure = function(pred, order){ + warn = pred %>% + select(order) %>% + pivot_longer(order) %>% + mutate(value = factor(value)) %>% + group_by(name) %>% + count(value, .drop = F) %>% + mutate(frac = n/sum(n)*100) %>% + filter(!(!name == 'Consensus' & value == 'No Consensus')) %>% + filter(value %in% c('No Consensus', 'Unsure')) %>% + mutate(warn = case_when(frac >= 70 ~ 'HIGH', + frac < 70 & frac > 30 ~ 'MEDIUM', + frac <= 30 ~ 'LOW')) + + warn = data.frame(TOOL = warn$name, + LABEL = warn$value, + PERCENTAGE = warn$frac, + FLAG = warn$warn) %>% + mutate(TOOL = factor(TOOL, levels = order)) + + warn = warn[order(warn$TOOL),] + + warn$FLAG = cell_spec(warn$FLAG, + bold = T, + background = case_when(warn$FLAG == 'HIGH' ~ "red", + warn$FLAG == 'MEDIUM' ~ "yellow", + warn$FLAG == 'LOW' ~ "green")) + + warn$TOOL = cell_spec(warn$TOOL, + bold = ifelse(warn$TOOL == 'Consensus', T, F), + background = ifelse(warn$TOOL == 'Consensus', 'black', 'white'), + color = ifelse(warn$TOOL == 'Consensus', 'white', 'black')) + return(warn) +} + umap_theme = theme(aspect.ratio = 1, text = element_text(size = 10), axis.title = element_blank(), @@ -217,7 +252,7 @@ umap_theme = theme(aspect.ratio = 1, ``` -```{r} +```{r, results='hide'} # read labels from refrence (used to harmonize 'usure' call) ref_labels = data.table::fread(params$ref_anno, header = T) %>% column_to_rownames('V1') @@ -226,6 +261,7 @@ list = list() for(r in refs){ list[[r]] = data.table::fread(paste0(params$pred_path, '/', params$sample, '/Prediction_Summary.tsv')) %>% harmonize_unsure(., ref_labels) + } # read expression matrix for sample @@ -234,10 +270,14 @@ query = data.table::fread(paste0(params$pred_path, '/', params$sample, '/express header=T, data.table=F) %>% column_to_rownames('V1') + + + ``` -```{r} +```{r, results='hide'} # create seurat object from expression matrix +set.seed(12345) query = t(query) query = CreateSeuratObject(query, row.names = colnames(query)) @@ -252,13 +292,14 @@ query = query %>% ``` ```{r fig.width=8,fig.height=8,echo=FALSE,message=FALSE,results="asis"} +set.seed(12345) cat("\n") # create reference pal pal = create_color_pal(ref_labels$label) save(pal, file = paste0(params$pred_path, '/', params$sample, '/report/class_pal.Rda')) -for(r in refs) { +for(r in refs){ query = AddMetaData(query, list[[r]]) @@ -290,11 +331,22 @@ for(r in refs) { cat(" \n## Prediction QC \n") + cat("

Percentage Unsure

") + + calculate_percentage_unsure(list[[r]], order = tools) %>% + kbl(escape = FALSE, row.names = F) %>% + kable_styling(position = "center") %>% + print() + + cat("\n") + cat("

Correlation between tools

") h = plot_tool_correlation_heatmap(query, tools = tools) draw(h) + cat("\n") + cat("

Percentage overlap between tools and consensus

") h = plot_percentage_predicted_consensus_class(query, tools = tools) From 8456b791108c75562d7f6d6f95118a29cdc0c080 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Sun, 13 Aug 2023 12:55:44 -0400 Subject: [PATCH 056/144] chnaged order of save model and plot qc --- Scripts/scPred/train_scPred.R | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Scripts/scPred/train_scPred.R b/Scripts/scPred/train_scPred.R index 7b91c3c..f5cae9c 100644 --- a/Scripts/scPred/train_scPred.R +++ b/Scripts/scPred/train_scPred.R @@ -63,14 +63,14 @@ stopCluster(cl) # print model info get_scpred(scpred) +# save trained model +message('@ SAVE MODEL') +save(scpred, file = model_path) +message('@ DONE') + # Plot prob pdf(paste0(out_path, '/qc_plots.pdf'), width=10, height=10) plot_probabilities(scpred) dev.off() -# save trained model -message('@ SAVE MODEL') -save(scpred, file = model_path) -message('@ DONE') - #--------------------------------------------- From 147fbfbf941c85108f37b3270bccd2794b0e599e Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Sun, 13 Aug 2023 17:07:34 -0400 Subject: [PATCH 057/144] updated benchmarking workflow to include consensus as a tool in report (#44) --- Notebooks/benchmark_report.Rmd | 83 ++++++++++++++++++---------------- snakefile.benchmark | 33 +++++++++++++- 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/Notebooks/benchmark_report.Rmd b/Notebooks/benchmark_report.Rmd index 17edeb8..c27b1b5 100644 --- a/Notebooks/benchmark_report.Rmd +++ b/Notebooks/benchmark_report.Rmd @@ -27,26 +27,13 @@ library(ComplexHeatmap) ``` ```{r} -# Function to read true and predicted labels -# Returns a list object with 'pred' and 'true' -read_results = function(path, tool, fold){ - - # set up path to predicted and true labels - pred_path = paste0(path, '/fold', fold, '/', tool, '/', tool, '_pred.csv') - true_path = paste0(path, '/fold', fold, '/test_labels.csv') - - list = list() - list$true = data.table::fread(true_path, header = T) %>% - column_to_rownames('V1') %>% - mutate(label = factor(label, ordered = TRUE)) - - list$pred = data.table::fread(pred_path, header = T) %>% - column_to_rownames('cell') %>% - mutate(label = .data[[tool]], - label = ifelse(!label %in% list$true$label, NA, label), - label = factor(label, ordered = TRUE)) - - return(list) +get_pred = function(pred, tool, true){ + pred %>% + select(tool) %>% + mutate(label = .data[[tool]], + label = ifelse(!label %in% true$label, NA, label), + label = factor(label, ordered = TRUE)) %>% + return() } # Plot confusion matrix as a heatmap @@ -115,12 +102,13 @@ bind_rows(df) %>% vjust = 1, hjust=1), aspect.ratio = 0.5) + - scale_y_continuous(expand = c(0, 0)) + + scale_y_continuous(limits = c(0, 1), + expand = c(0, 0)) + geom_hline(yintercept = c(1, 0.5), linetype = 'dotted', color = 'red') } # plot average stat for all tools -plot_mean_tool = function(list, stat){ +plot_mean_tool = function(list, stat, tools){ df = lapply(list, function(x){lapply(x, get_stat, stat = stat) %>% bind_rows()}) @@ -137,16 +125,21 @@ col_fun = circlize::colorRamp2(c(0, range(df)[2]/2, range(df)[2]), c("#3B5B91", "#F2EFC7", "#CC0007")) - + +split = c('Consensus', rep('tools', length(tools)-1)) + h = Heatmap(df, - name = paste('Mean ', stat), - col = col_fun, - width = ncol(df)*unit(4, "mm"), - height = nrow(df)*unit(6, "mm"), - row_names_side = 'left', - row_names_gp = gpar(fontsize = 12), - show_column_dend = F, - show_row_dend = F) + name = paste('Mean ', stat), + col = col_fun, + width = ncol(df)*unit(4, "mm"), + height = nrow(df)*unit(6, "mm"), + row_names_side = 'left', + row_names_gp = gpar(fontsize = 12), + show_column_dend = F, + show_row_dend = F, + row_split = split, + cluster_row_slices = F, + row_title = NULL) return(h) } @@ -167,7 +160,7 @@ get_stat = function(x, stat){ ``` ```{r} -tools = strsplit(params$tools, split = ' ')[[1]] +tools = c('Consensus', strsplit(params$tools, split = ' ')[[1]]) fold = as.numeric(params$fold) ``` @@ -175,12 +168,24 @@ fold = as.numeric(params$fold) # Read prediction and true labels for each tool and each fold and calculate confusion matrix and stats # Save everything in a list object with hierarchy TOOL > FOLD > STATS list = list() -for(t in tools) { - for(n in 1:fold){ - tmp = read_results(params$pred_path, t, n) - list[[t]][[n]] = confusionMatrix(data = tmp$pred$label, reference = tmp$true$label, mode = 'everything') - list[[t]][[n]]$fold = paste0('fold', n) - list[[t]][[n]]$tool = t +for(n in 1:fold){ + + # read tru lables + true = data.table::fread(paste0(params$pred_path, '/fold', n, '/test_labels.csv'), header = T) %>% + column_to_rownames('V1') %>% + mutate(label = factor(label, ordered = TRUE)) + + # read prediction summary for fold + pred = data.table::fread(paste0(params$pred_path, '/fold', n, '/Prediction_Summary.tsv'), header = T)%>% + column_to_rownames('cellname') + + for(t in tools){ + + tmp = get_pred(pred, t, true) + + list[[t]][[n]] = confusionMatrix(data = tmp$label, reference = true$label, mode = 'everything') + list[[t]][[n]]$fold = paste0('fold', n) + list[[t]][[n]]$tool = t } } ``` @@ -191,7 +196,7 @@ cat(" \n#", params$ref_name , "{.tabset} \n") cat(" \n## Summary \n") cat("

Average F1 score per tool and class

") -plot_mean_tool(list, 'F1') +plot_mean_tool(list, 'F1', tools) cat("\n") diff --git a/snakefile.benchmark b/snakefile.benchmark index abf253a..24310bc 100644 --- a/snakefile.benchmark +++ b/snakefile.benchmark @@ -52,7 +52,9 @@ rule knit_report: input: pred = expand(config['output_dir'] + "/fold{folds_index}/{tool}/{tool}_pred.csv", folds_index = n_folds, - tool = config['tools_to_run']) + tool = config['tools_to_run']), + consensus = expand(config["output_dir"] + "/fold{folds_index}/Prediction_Summary.tsv", + folds_index = n_folds) output: report_path = config['output_dir'] + '/report/' + dt_string + '.benchmark_report.html' log: @@ -76,6 +78,35 @@ rule knit_report: &> {log} """ +#---------------------------------------------------- +# Consensus +#---------------------------------------------------- + +rule consensus: + input: + results = expand(config["output_dir"] + "/fold{{folds_index}}/{tool}/{tool}_pred.csv", + tool=config['tools_to_run']), + sample = config["output_dir"] + "/fold{folds_index}" + output: + prediction_summary = config["output_dir"] + "/fold{folds_index}/Prediction_Summary.tsv" + log: + config["output_dir"] + "/fold{folds_index}/Gatherpreds.log" + params: + basedir = {workflow.basedir}, + tools = config['tools_to_run'], + conesnsus_tools = config['consensus_tools'], + labfile = config['reference_annotations'] + shell: + """ + Rscript {params.basedir}/Scripts/calculate_consensus.R \ + {input.sample} \ + {output.prediction_summary} \ + "{params.tools}" \ + "{params.conesnsus_tools}" \ + {params.labfile} \ + &> {log} + """ + #---------------------------------------------------- # scPred #---------------------------------------------------- From 39f9aee37db74c2db0a0c4aad02bd966c3f59f9b Mon Sep 17 00:00:00 2001 From: Tom <64378638+tvegawaichman@users.noreply.github.com> Date: Sun, 13 Aug 2023 17:33:59 -0400 Subject: [PATCH 058/144] tool: scAnnotate, scID (#42) * tool: scAnnotate, scID * changed typos --- README.md | 31 ++++++++ Scripts/scAnnotate/run_scAnnotate.R | 98 ++++++++++++++++++++++++++ Scripts/scID/run_scID.R | 105 ++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 Scripts/scAnnotate/run_scAnnotate.R create mode 100644 Scripts/scID/run_scID.R diff --git a/README.md b/README.md index 202842b..af8cdda 100644 --- a/README.md +++ b/README.md @@ -259,4 +259,35 @@ It is necessary to explore whether parallelization is possible. The Tangram workflow was generated following the tutorial provided below: https://tangram-sc.readthedocs.io/en/latest/tutorial_sq_link.html +## scAnnotate +Documentation written by: Tomas Vega Waichman +Date written: 2023-08-11 + +scAnnotate cannot be separated between training and test. +Genes in references and query should match. +It allows to do the normalization inside their function using the parameter `lognormalized = F` but I normalized in the same way as they do on their script (using the NormalizeData function from the Seurat package, via the “LogNormalize” method and a scale factor of 10,000) since if we changed the input it would be easier to change. That's why I use `lognormalized = T`. +scAnnotate has two separate workflows with different batch effect removal steps based on the size of the training data. The `correction ="auto"` allows to automatically detect the needed for the dataset. They suggest using Seurat for dataset with at most one rare cell population +(at most one cell population less than 100 cells) and using Harmony for dataset with at least two rare cell populations (at least two cell populations less than 100 cells). +The `threshold` value goes between 0-1 and the cell with lower probability than the threshold are assing as "unassigned" +The scAnnotate workflow was generated following the tutorial provided below: +* https://cran.r-project.org/web/packages/scAnnotate/vignettes/Introduction.html + +## scAnnotate +Documentation written by: Tomas Vega Waichman +Date written: 2023-08-12 + +scID has some installing isues: + * Needs gdal/3.5.1 to install it. + * MAST is needed… if you are not able to install it use this approach: +``` +wget https://bioconductor.org/packages/release/bioc/src/contrib/MAST_1.26.0.tar.gz +R CMD INSTALL MAST_1.26.0.tar.gz +``` +scID cannot be separated between training and test. +I used their `scID:::counts_to_cpm(counts_gem = query)` function that they provided (hidden, code in their github). Could be replaced with any normalization without log-transformation (they said this in the tutorial below: Any library-depth normalization (e.g. TPM, CPM) is compatible with scID, but not log-transformed data.) +* All parameters are the default except the normalization that is set in F since I normalized outside the function. But there exist some parameters that would be nice to explore as the `estimate_weights_from_target`. +* It's very slow (takes ~ 2hs for the 5k cells query and 5k cell reference), but we have to test if it's related with the number of labels (number of comparison) or the size of the dataset. +The scID workflow was generated following the tutorials provided below: +* https://github.com/BatadaLab/scID/blob/master/vignettes/Mapping_example.md +* https://github.com/BatadaLab/scID diff --git a/Scripts/scAnnotate/run_scAnnotate.R b/Scripts/scAnnotate/run_scAnnotate.R new file mode 100644 index 0000000..cdf0709 --- /dev/null +++ b/Scripts/scAnnotate/run_scAnnotate.R @@ -0,0 +1,98 @@ +# load libraries and arguments +library(data.table) +library(WGCNA) +library(tidyverse) +library(Seurat) +library(glue) +library(scAnnotate) +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +query_path = args[3] +pred_path = args[4] +threads = as.numeric(args[5]) +threshold = as.numeric(args[6]) +# path for other outputs (depends on tools) +out_path = dirname(pred_path) + +#--------------- Data ------------------- + +# read reference matrix and transpose +message('@ READ REF') +### The matrix of the references is transpose since it needs to be normalize +### with Seurat that expect a genes x cell. +ref <- data.table::fread(ref_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') %>% + transposeBigData() + +message('@ DONE') + +# read reference labels +labels <- data.table::fread(lab_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') + + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(colnames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# read query matrix and transpose +message('@ READ QUERY') +### The matrix of the references is transpose since it needs to be normalize +### with Seurat that expect a genes x cell. +query <- data.table::fread(query_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') %>% + WGCNA::transposeBigData() + +message('@ DONE') + +### Query and References should have the same gene features +common.genes <- intersect(rownames(query),rownames(ref)) +query <- query[common.genes,] +ref <- ref[common.genes,] +### Prepare the reference +## Normalization +ref <- NormalizeData(ref) %>% as.data.frame() %>% transposeBigData() +## The label should be in the first column +ref <- cbind(labels,ref) + +## Prepare the query +## Normalization +query <- NormalizeData(query) %>% as.data.frame() %>% transposeBigData() + +#------------- Train + Predict scAnnotate ------------- +## Auto to automaticly define the correction method needed. +pred <- scAnnotate(train=ref, + test=query, + distribution="normal", #Default + correction ="auto", + screening = "wilcox", + threshold=threshold, + lognormalized=TRUE) + +pred_labels <- data.frame(cell = rownames(query), + scAnnotate = pred) + +message('@ WRITTING PREDICTIONS') +data.table::fwrite(pred_labels, + file = pred_path, + row.names = F, + col.names = T, + sep = ",", + nThread = threads) +message('@ DONE') diff --git a/Scripts/scID/run_scID.R b/Scripts/scID/run_scID.R new file mode 100644 index 0000000..19df68e --- /dev/null +++ b/Scripts/scID/run_scID.R @@ -0,0 +1,105 @@ +# load libraries and arguments +library(data.table) +library(WGCNA) +library(tidyverse) +library(glue) +library(Seurat) +library(scID) +library(MAST) +set.seed(1234) + +args = commandArgs(trailingOnly = TRUE) +ref_path = args[1] +lab_path = args[2] +query_path = args[3] +pred_path = args[4] +threads = as.numeric(args[5]) +# path for other outputs (depends on tools) +out_path = dirname(pred_path) + + +#--------------- Data ------------------- + +# read reference matrix and transpose +message('@ READ REF') +### The matrix of the references is transpose since it needs to be normalize +### with Seurat that expect a genes x cell. +ref <- data.table::fread(ref_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') %>% + transposeBigData() + +message('@ DONE') + +# read reference labels +labels <- data.table::fread(lab_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') + + +# check if cell names are in the same order in labels and ref +order = all(as.character(rownames(labels)) == as.character(colnames(ref))) + +# throw error if order is not the same +if(!order){ + stop("@ Order of cells in reference and labels do not match") +} + +# read query matrix and transpose +message('@ READ QUERY') +### The matrix of the references is transpose since it needs to be normalize +### with Seurat that expect a genes x cell. +query <- data.table::fread(query_path, + data.table=F, + header=T, + nThread=threads) %>% + column_to_rownames('V1') %>% + WGCNA::transposeBigData() + +message('@ DONE') + +### Prepare the reference +## Normalization +ref <- scID:::counts_to_cpm(counts_gem = ref) + +## Labels as named vector +label <- setNames(object = labels$label,nm = rownames(labels)) +## Prepare the query +## Normalization +query <- scID:::counts_to_cpm(counts_gem = query) + + +#----------- Train + Predict scID -------- +pred <- scid_multiclass(target_gem = query, + reference_gem = ref, + reference_clusters = label, + logFC = 0.5, #Default + only_pos = FALSE, #Default + normalize_reference = FALSE, #I already normalized the reference + estimate_weights_from_target = FALSE #Default + ) + +pred_labels <- data.frame(cell = names(pred$labels), + scID = as.character(pred$labels) + ) + +message('@ WRITTING PREDICTIONS') +data.table::fwrite(pred_labels, + file = pred_path, + row.names = F, + col.names = T, + sep = ",", + nThread = threads) +message('@ DONE') + +#------------- Other outputs -------------- +#Save the entire output +message('@ SAVE scID OUTPUT') +save(pred, + file = glue('{out_path}/scID_output.Rdata') + ) +message('@ DONE') \ No newline at end of file From 2ed5e91494903a40329d76c7fbdc6faef3ce1eef Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:11:43 -0400 Subject: [PATCH 059/144] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af8cdda..1fe18d2 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,12 @@ BiocManager::install(pkg) **R packages github** - ```R pkg = c("pcahan1/singleCellNet", "powellgenomicslab/scPred", "PaulingLiu/scibet", - "bm2-lab/scLearn") + "bm2-lab/scLearn", + "BatadaLab/scID") devtools::install_github(pkg) ``` From c74f7ccb60558dbbcb6055acab51bf9faf873912 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:23:51 -0400 Subject: [PATCH 060/144] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1fe18d2..eb74cfa 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ pkg = c("Seurat", "MetBrewer", "plotly", "caret", - "Matrix") + "Matrix", + "scAnnotate") install.packages(pkg) ``` @@ -273,7 +274,7 @@ The `threshold` value goes between 0-1 and the cell with lower probability than The scAnnotate workflow was generated following the tutorial provided below: * https://cran.r-project.org/web/packages/scAnnotate/vignettes/Introduction.html -## scAnnotate +## scID Documentation written by: Tomas Vega Waichman Date written: 2023-08-12 From af0ef2d0631d7d352333d4c1e4b8397306028a55 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Sun, 13 Aug 2023 21:00:59 -0400 Subject: [PATCH 061/144] updated both snakefiles with rules for scID and scAnnotate --- Scripts/scID/run_scID.R | 3 +-- snakefile.annotate | 58 +++++++++++++++++++++++++++++++++++++++++ snakefile.benchmark | 58 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/Scripts/scID/run_scID.R b/Scripts/scID/run_scID.R index 19df68e..3259fd4 100644 --- a/Scripts/scID/run_scID.R +++ b/Scripts/scID/run_scID.R @@ -17,7 +17,6 @@ threads = as.numeric(args[5]) # path for other outputs (depends on tools) out_path = dirname(pred_path) - #--------------- Data ------------------- # read reference matrix and transpose @@ -102,4 +101,4 @@ message('@ SAVE scID OUTPUT') save(pred, file = glue('{out_path}/scID_output.Rdata') ) -message('@ DONE') \ No newline at end of file +message('@ DONE') diff --git a/snakefile.annotate b/snakefile.annotate index 39cb53a..c2855b0 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -700,6 +700,64 @@ rule predict_ACTINN: """ +#---------------------------------------------------- +# scID +#---------------------------------------------------- + +rule run_scID: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'], + query = config['output_dir'] + "/{sample}/expression.csv" + output: + pred = config['output_dir'] + "/{sample}/scID/scID_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/{sample}/scID/scID.log" + benchmark: + config['output_dir'] + "/{sample}/scID/scID_predict_benchmark.txt" + shell: + """ + Rscript {params.basedir}/Scripts/scID/run_scID.R \ + {input.reference} \ + {input.labfile} \ + {input.query} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scAnnotate +#---------------------------------------------------- + +rule run_scAnnotate: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'], + query = config['output_dir'] + "/{sample}/expression.csv" + output: + pred = config['output_dir'] + "/{sample}/scAnnotate/scAnnotate_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5 + log: + config['output_dir'] + "/{sample}/scAnnotate/scAnnotate.log" + benchmark: + config['output_dir'] + "/{sample}/scAnnotate/scAnnotate_predict_benchmark.txt" + shell: + """ + Rscript {params.basedir}/Scripts/scAnnotate/run_scAnnotate.R \ + {input.reference} \ + {input.labfile} \ + {input.query} \ + {output.pred} \ + {threads} \ + {params.threshold} \ + &> {log} + """ + #---------------------------------------------------- # The End #---------------------------------------------------- diff --git a/snakefile.benchmark b/snakefile.benchmark index 24310bc..f9f8683 100644 --- a/snakefile.benchmark +++ b/snakefile.benchmark @@ -691,6 +691,64 @@ rule predict_ACTINN: """ +#---------------------------------------------------- +# scID +#---------------------------------------------------- + +rule run_scID: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv", + query = config['output_dir'] + "/fold{folds_index}/test.csv" + output: + pred = config['output_dir'] + "/fold{folds_index}/scID/scID_pred.csv" + params: + basedir = {workflow.basedir} + log: + config['output_dir'] + "/fold{folds_index}/scID/scID.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scID/scID_predict_benchmark.txt" + shell: + """ + Rscript {params.basedir}/Scripts/scID/run_scID.R \ + {input.reference} \ + {input.labfile} \ + {input.query} \ + {output.pred} \ + {threads} \ + &> {log} + """ + +#---------------------------------------------------- +# scAnnotate +#---------------------------------------------------- + +rule run_scAnnotate: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv", + query = config['output_dir'] + "/fold{folds_index}/test.csv" + output: + pred = config['output_dir'] + "/fold{folds_index}/scAnnotate/scAnnotate_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5 + log: + config['output_dir'] + "/fold{folds_index}/scAnnotate/scAnnotate.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scAnnotate/scAnnotate_predict_benchmark.txt" + shell: + """ + Rscript {params.basedir}/Scripts/scAnnotate/run_scAnnotate.R \ + {input.reference} \ + {input.labfile} \ + {input.query} \ + {output.pred} \ + {threads} \ + {params.threshold} \ + &> {log} + """ + #---------------------------------------------------- # The End #---------------------------------------------------- From 5fc69d3b07a995999273560bc3a73439abeed384 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:03:23 -0400 Subject: [PATCH 062/144] Update README.md --- README.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index eb74cfa..650dde7 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,13 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor 4. Prepare config file 5. Prepare submission script (HPC) -## 1. Clone repository and install dependencies +### 1. Clone repository and install dependencies -## 2. Prepare reference +### 2. Prepare reference -## 3. Prepare query +### 3. Prepare query -## 4. Prepare config file +### 4. Prepare config file ```yaml # UPDATE @@ -85,13 +85,13 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor ## 5. Prepare submission script -### Annotate +#### Annotate ```bash # UPDATE ``` -### Benchmark +#### Benchmark ```bash # UPDATE @@ -126,10 +126,32 @@ TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM # Adding New Tools: -``` R +``` # UPDATE ``` +# Tips and Tricks + +- Dryrun before submitting job +```bash +snakemake -s ${snakefile} --configfile ${config} -n +``` + +- Unlock working directory before running (in case previous run crashed) by adding this to your script +```bash +snakemake -s ${snakefile} --configfile ${config} --unlock +``` + +- Add `--rerun-incomplete` if snakemake finds incomplete files from a previous run that was not successfully removed +```bash +snakemake -s ${snakefile} --configfile ${config} --rerun-incomplete +``` + +- Update time stamp on files to avoid reruning rules if code has changed +```bash +snakemake -s ${snakefile} --configfile ${config} -c1 -R $(snakemake -s ${snakefile} --configfile ${config} -c1 --list-code-changes) --touch +``` + # Tools ## scClassify From da23405a74cb417fdc6ac8f79e25faa859d11334 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:08:19 -0400 Subject: [PATCH 063/144] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 650dde7..a9dbc85 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,11 @@ snakemake -s ${snakefile} --configfile ${config} --rerun-incomplete snakemake -s ${snakefile} --configfile ${config} -c1 -R $(snakemake -s ${snakefile} --configfile ${config} -c1 --list-code-changes) --touch ``` +- Generate a report with information about the snakemake workflow +```bash +snakemake -s ${snakefile} --configfile ${config} --report ${report} +``` + # Tools ## scClassify From ada3a1f4f20dc1f2f422eed049451bae6bcf1fbe Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Mon, 14 Aug 2023 10:09:41 -0400 Subject: [PATCH 064/144] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a9dbc85..5c0c190 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ pkg = c("SingleCellExperiment", "scClassify", "scuttle", "scran", - "M3Drop") + "M3Drop", + "scAnnotate") BiocManager::install(pkg) ``` @@ -113,6 +114,8 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor - scLearn - ACTINN - Correlation +- scID +- scAnnotate ``` **Single cell RNA reference + spatial RNA query** From c32ced5d12f0d3d03cb9770d1f6aa307358230cb Mon Sep 17 00:00:00 2001 From: Tom <64378638+tvegawaichman@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:45:53 -0400 Subject: [PATCH 065/144] Dev tom sc nym (#46) * tool: scAnnotate, scID * changed typos * tool: scNym * added the date on README --- README.md | 19 +++++ Scripts/Tangram/run_Tangram.py | 4 +- Scripts/scID/run_scID.R | 1 + Scripts/scNym/run_scNym.py | 124 +++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 Scripts/scNym/run_scNym.py diff --git a/README.md b/README.md index 5c0c190..331279e 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,7 @@ The `threshold` value goes between 0-1 and the cell with lower probability than The scAnnotate workflow was generated following the tutorial provided below: * https://cran.r-project.org/web/packages/scAnnotate/vignettes/Introduction.html + ## scID Documentation written by: Tomas Vega Waichman Date written: 2023-08-12 @@ -322,3 +323,21 @@ I used their `scID:::counts_to_cpm(counts_gem = query)` function that they provi The scID workflow was generated following the tutorials provided below: * https://github.com/BatadaLab/scID/blob/master/vignettes/Mapping_example.md * https://github.com/BatadaLab/scID + +## scNym +Documentation written by: Tomas Vega Waichman +Date written: 2023-08-14 +scNym takes advantage of the query to train the model, so... even if we are able to separated, it needs the query to train the model. +* Query and training are concatenate in the same object and Any cell with the annotation "Unlabeled" will be treated as part of the target dataset and used for semi-supervised and adversarial training. It uses part of the query dataset to train the model. +* Data inputs for scNym should be log(CPM + 1) normalized counts, where CPM is Counts Per Million and log is the natural logarithm. +* They added the step of filtering no expressed genes so I added it but I ignored the step of filtering cells. +* Threashold to assing cells lower than that value as “Unknown”. +* It needs more research in multi-domain. +* whole_df_output.csv has the entire dataframe output with the score for the query test (mark as label == “Unlabeled”). +* I used the configuration as `new_identity_discovery` since: This configuration is useful for experiments where new cell type discoveries may occur. It uses +pseudolabel thresholding to avoid the assumption above. If new cell +types are present in the target data, they correctly receive low +confidence scores. + +The scNym workflow was generated following the tutorials provided below: +* https://github.com/calico/scnym/tree/master diff --git a/Scripts/Tangram/run_Tangram.py b/Scripts/Tangram/run_Tangram.py index e420ba3..cc6a75f 100644 --- a/Scripts/Tangram/run_Tangram.py +++ b/Scripts/Tangram/run_Tangram.py @@ -108,9 +108,9 @@ def get_max_column_and_value(row): print('@ DONE ') #### Save map object that contains the cell x spot prob -print('@ WRITTING PROB MATRIX ') +print('@ WRITTING OUTPUT OBJECT ') filename = out_other_path + '/Tangram_mapped_object.h5ad' -ad_map.write_h5ad(filename= outputFile_map, +ad_map.write_h5ad(filename= filename, compression='gzip') print('@ DONE ') diff --git a/Scripts/scID/run_scID.R b/Scripts/scID/run_scID.R index 3259fd4..2ecf901 100644 --- a/Scripts/scID/run_scID.R +++ b/Scripts/scID/run_scID.R @@ -101,4 +101,5 @@ message('@ SAVE scID OUTPUT') save(pred, file = glue('{out_path}/scID_output.Rdata') ) + message('@ DONE') diff --git a/Scripts/scNym/run_scNym.py b/Scripts/scNym/run_scNym.py new file mode 100644 index 0000000..2ef1ec6 --- /dev/null +++ b/Scripts/scNym/run_scNym.py @@ -0,0 +1,124 @@ +#--------------- Libraries ------------------- +import pandas as pd +from scnym.api import scnym_api +import anndata as ad +import sys +import scanpy as sc +import os +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +ref_path = str(sys.argv[1]) +lab_path = str(sys.argv[2]) +out_path = str(sys.argv[3]) +sample_path = str(sys.argv[4]) +threshold = float(sys.argv[5]) +out_other_path = os.path.dirname(str(sys.argv[3])) + +#--------------- Data ------------------------- +# read the data +ref = pd.read_csv(ref_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +labels = pd.read_csv(lab_path, + index_col = 0, + sep=',') + +# check if cell names are in the same order in labels and ref +order = all(labels.index == ref.index) +# throw error if order is not the same +if not order: + sys.exit("@ Order of cells in reference and labels do not match") + +print('@ READ QUERY') +query = pd.read_csv(sample_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +print('@ DONE') + +#------------- Preparing input ------------- +## Normalizing reference +ad_ref = ad.AnnData(X = ref, + obs = dict(obs_names=ref.index.astype(str), + label = labels['label']), + var = dict(var_names=ref.columns.astype(str)) + ) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization. +sc.pp.normalize_total(ad_ref, target_sum=1e6) +#Logarithmize the data: +sc.pp.log1p(ad_ref) +sc.pp.filter_genes(ad_ref, min_cells=10) + +## Normalizing query +ad_query = ad.AnnData(X = query, + obs = dict(obs_names=query.index.astype(str)), + var = dict(var_names=query.columns.astype(str)) +) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +sc.pp.normalize_total(ad_query, target_sum=1e6) +#Logarithmize the data: +sc.pp.log1p(ad_query) +sc.pp.filter_genes(ad_query, min_cells=10) +# Any cell with the annotation "Unlabeled" will be treated as part of the target +# dataset and used for semi-supervised and adversarial training. +ad_query.obs["label"] = "Unlabeled" + + +#------------- Train scNym ------------- +## Contatenate +adata = ad_ref.concatenate(ad_query) +file_model = out_other_path + '/model +scnym_api( + adata=adata, + task='train', + groupby='label', + out_path=file_model, + config='new_identity_discovery', +) + +file_pred = out_other_path + '/predict +scnym_api( + adata=adata, + task='predict', + key_added='scNym', + trained_model=file_model, + out_path=file_pred, + config='new_identity_discovery', +) + +adata.obs['pred_label_reject'] = adata.obs.apply(lambda row: 'Unknown' if row['scNym_confidence'] < threshold else row['scNym'], axis=1) +## Get only the query +df = adata.obs[adata.obs["label"] == "Unlabeled"] +print('@ WRITTING PREDICTIONS') +pred_df = pd.DataFrame({'cell': df.index, "scNym": df.pred_label_reject}) +pred_df.to_csv(out_path, index = False) +print('@ DONE') + +#------------- Other outputs -------------- +### Save data.frame output +print('@ WRITTING DATA FRAME OUTPUT ') +filename = out_other_path + '/whole_df_output.csv' +df.to_csv(filename, + index=True) #True because we want to conserve the rownames (cells) +print('@ DONE ') + +#### Save map object that contains the cell x spot prob +print('@ WRITTING OUTPUT OBJECT ') +filename = out_other_path + '/scNym_output_object.h5ad' +adata.write_h5ad(filename= filename, + compression='gzip') +print('@ DONE ') From 42a72323ee83f4a319526f3ef507f6aae57cbaaf Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Mon, 14 Aug 2023 18:37:03 -0400 Subject: [PATCH 066/144] Reference mouse to human gene conversion (#47) * Convert ref genes mm to hg if config file specifies * Add PR feedback * Debug gene conversion function output --- README.md | 5 ++++- Scripts/preprocess.R | 46 ++++++++++++++++++++++++++++++++++++++++++++ example.config.yml | 2 ++ snakefile.annotate | 9 ++++++--- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 331279e..dacd98c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,10 @@ pkg = c("SingleCellExperiment", "scuttle", "scran", "M3Drop", - "scAnnotate") + "scAnnotate", + "Orthology.eg.db", + "org.Mm.eg.db", + "org.Hg.eg.db") BiocManager::install(pkg) ``` diff --git a/Scripts/preprocess.R b/Scripts/preprocess.R index 96bea51..3467d58 100644 --- a/Scripts/preprocess.R +++ b/Scripts/preprocess.R @@ -1,10 +1,12 @@ library(tidyverse) +library(WGCNA) args = commandArgs(trailingOnly = TRUE) ref_path = args[1] query_paths = strsplit(args[2], split = ' ')[[1]] out_path = args[3] +convert_genes = as.logical(args[4]) l = list() @@ -18,6 +20,50 @@ for(p in query_paths){ l[[query]] = tmp } +# if specified by user, convert reference gene names from mouse to human +if(convert_genes){ + + # include functions and libraries for conversion + library(Orthology.eg.db) + library(org.Mm.eg.db) + library(org.Hs.eg.db) + library(WGCNA) + mapfun = function(mousegenes){ + gns = mapIds(org.Mm.eg.db, mousegenes, "ENTREZID", "SYMBOL") + mapped = AnnotationDbi::select(Orthology.eg.db, gns, "Homo_sapiens","Mus_musculus") + naind = is.na(mapped$Homo_sapiens) + hsymb = mapIds(org.Hs.eg.db, as.character(mapped$Homo_sapiens[!naind]), "SYMBOL", "ENTREZID") + out = data.frame(Mouse_symbol = mousegenes, mapped, Human_symbol = NA) + out$Human_symbol[!naind] = hsymb + return(out) + } + + # convert + hg = mapfun(colnames(l[['ref']])) %>% dplyr::select(Mouse_symbol, Human_symbol) + + # output list of mouse genes that were not converted + not_converted = hg %>% filter(is.na(Human_symbol)) %>% .$Mouse_symbol + data.table::fwrite(as.list(not_converted), file = paste0(out_path, '/genes_not_converted.csv'), sep = ',') + + # throw error if more than threshold % genes not converted + threshold = 0.5 + if(length(not_converted) > threshold*length(colnames(l[['ref']]))){ + stop(paste0("@ More than ",threshold*100,"% of mouse genes in reference could not be converted to human")) + } + + # modify reference matrix to contain converted genes + + l[['ref']] = l[['ref']] %>% + transposeBigData() %>% + rownames_to_column('Mouse_symbol') %>% + inner_join(hg %>% filter(!is.na(Human_symbol)), + by = 'Mouse_symbol') %>% + dplyr::select(-Mouse_symbol) %>% + column_to_rownames('Human_symbol') %>% + transposeBigData() + +} + # get genes for each data frame (colnames) genes = lapply(l, function(x){(colnames(x))}) diff --git a/example.config.yml b/example.config.yml index cee4df6..7d1ab53 100644 --- a/example.config.yml +++ b/example.config.yml @@ -19,6 +19,8 @@ query_datasets: check_genes: False genes_required: Null +# convert mouse genes of reference to human genes +convert_ref_mm_to_hg: False # classifiers to run tools_to_run: diff --git a/snakefile.annotate b/snakefile.annotate index c2855b0..0cfc461 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -38,7 +38,8 @@ rule preprocess: query = expand(config['output_dir'] + "/{sample}/expression.csv", sample = samples) params: basedir = {workflow.basedir}, - out_path = config['output_dir'] + out_path = config['output_dir'], + convert_genes = config['convert_ref_mm_to_hg'] log: config['output_dir'] + "/preprocess.log" priority: 50 shell: @@ -47,8 +48,10 @@ rule preprocess: {input.reference} \ "{input.query}" \ {params.out_path} \ + {params.convert_genes} \ &> {log} """ + #---------------------------------------------------- # Consensus #---------------------------------------------------- @@ -65,7 +68,7 @@ rule consensus: params: basedir = {workflow.basedir}, tools = config['tools_to_run'], - conesnsus_tools = config['consensus_tools'], + consensus_tools = config['consensus_tools'], labfile = config['reference_annotations'] shell: """ @@ -73,7 +76,7 @@ rule consensus: {input.sample} \ {output.prediction_summary} \ "{params.tools}" \ - "{params.conesnsus_tools}" \ + "{params.consensus_tools}" \ {params.labfile} \ &> {log} """ From f5d9b255c4f1fd25c1db5b96bd7e8657a78ec1e5 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 14 Aug 2023 18:45:47 -0400 Subject: [PATCH 067/144] fixed some errors in scNym --- Scripts/scNym/run_scNym.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Scripts/scNym/run_scNym.py b/Scripts/scNym/run_scNym.py index 2ef1ec6..f709fea 100644 --- a/Scripts/scNym/run_scNym.py +++ b/Scripts/scNym/run_scNym.py @@ -81,7 +81,7 @@ #------------- Train scNym ------------- ## Contatenate adata = ad_ref.concatenate(ad_query) -file_model = out_other_path + '/model +file_model = out_other_path + '/model' scnym_api( adata=adata, task='train', @@ -90,7 +90,7 @@ config='new_identity_discovery', ) -file_pred = out_other_path + '/predict +file_pred = out_other_path + '/predict' scnym_api( adata=adata, task='predict', @@ -101,10 +101,15 @@ ) adata.obs['pred_label_reject'] = adata.obs.apply(lambda row: 'Unknown' if row['scNym_confidence'] < threshold else row['scNym'], axis=1) +def remove_batch(cell_id, batch): + return cell_id.replace(f'-{batch}', '') + +adata.obs['cell_id'] = adata.obs.index.map(lambda cell_id: remove_batch(cell_id, adata.obs.loc[cell_id, 'batch'])) + ## Get only the query df = adata.obs[adata.obs["label"] == "Unlabeled"] print('@ WRITTING PREDICTIONS') -pred_df = pd.DataFrame({'cell': df.index, "scNym": df.pred_label_reject}) +pred_df = pd.DataFrame({'cell': df.cell_id, "scNym": df.pred_label_reject}) pred_df.to_csv(out_path, index = False) print('@ DONE') From 27dfc294cb241f36b9efe2ab91faae152bb23746 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Mon, 14 Aug 2023 19:14:50 -0400 Subject: [PATCH 068/144] updated snakefiles with rules for scNYm --- Scripts/scNym/run_scNym.py | 17 +++++++++++------ snakefile.annotate | 37 +++++++++++++++++++++++++++++++++---- snakefile.benchmark | 29 +++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/Scripts/scNym/run_scNym.py b/Scripts/scNym/run_scNym.py index 2ef1ec6..2548065 100644 --- a/Scripts/scNym/run_scNym.py +++ b/Scripts/scNym/run_scNym.py @@ -12,10 +12,10 @@ #--------------- Parameters ------------------- ref_path = str(sys.argv[1]) lab_path = str(sys.argv[2]) -out_path = str(sys.argv[3]) -sample_path = str(sys.argv[4]) +sample_path = str(sys.argv[3]) +out_path = str(sys.argv[4]) threshold = float(sys.argv[5]) -out_other_path = os.path.dirname(str(sys.argv[3])) +out_other_path = os.path.dirname(str(sys.argv[4])) #--------------- Data ------------------------- # read the data @@ -81,7 +81,7 @@ #------------- Train scNym ------------- ## Contatenate adata = ad_ref.concatenate(ad_query) -file_model = out_other_path + '/model +file_model = out_other_path + '/model' scnym_api( adata=adata, task='train', @@ -90,7 +90,7 @@ config='new_identity_discovery', ) -file_pred = out_other_path + '/predict +file_pred = out_other_path + '/predict' scnym_api( adata=adata, task='predict', @@ -101,10 +101,15 @@ ) adata.obs['pred_label_reject'] = adata.obs.apply(lambda row: 'Unknown' if row['scNym_confidence'] < threshold else row['scNym'], axis=1) +def remove_batch(cell_id, batch): + return cell_id.replace(f'-{batch}', '') + +adata.obs['cell_id'] = adata.obs.index.map(lambda cell_id: remove_batch(cell_id, adata.obs.loc[cell_id, 'batch'])) + ## Get only the query df = adata.obs[adata.obs["label"] == "Unlabeled"] print('@ WRITTING PREDICTIONS') -pred_df = pd.DataFrame({'cell': df.index, "scNym": df.pred_label_reject}) +pred_df = pd.DataFrame({'cell': df.cell_id, "scNym": df.pred_label_reject}) pred_df.to_csv(out_path, index = False) print('@ DONE') diff --git a/snakefile.annotate b/snakefile.annotate index c2855b0..0c1aaec 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -56,8 +56,7 @@ rule preprocess: rule consensus: input: results = expand(config["output_dir"] + "/{{sample}}/{tool}/{tool}_pred.csv", - tool=config['tools_to_run']), - sample = config["output_dir"] + "/{sample}" + tool=config['tools_to_run']) output: prediction_summary = config["output_dir"] + "/{sample}/Prediction_Summary.tsv" log: @@ -66,11 +65,12 @@ rule consensus: basedir = {workflow.basedir}, tools = config['tools_to_run'], conesnsus_tools = config['consensus_tools'], - labfile = config['reference_annotations'] + labfile = config['reference_annotations'], + sample = config["output_dir"] + "/{sample}" shell: """ Rscript {params.basedir}/Scripts/calculate_consensus.R \ - {input.sample} \ + {params.sample} \ {output.prediction_summary} \ "{params.tools}" \ "{params.conesnsus_tools}" \ @@ -758,6 +758,35 @@ rule run_scAnnotate: &> {log} """ +#---------------------------------------------------- +# scNym +#---------------------------------------------------- + +rule run_scNym: + input: + reference = config['output_dir'] + "/expression.csv", + labfile = config['reference_annotations'], + query = config['output_dir'] + "/{sample}/expression.csv" + output: + pred = config['output_dir'] + "/{sample}/scNym/scNym_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5 + log: + config['output_dir'] + "/{sample}/scNym/scNym.log" + benchmark: + config['output_dir'] + "/{sample}/scNym/scNym_predict_benchmark.txt" + shell: + """ + python {params.basedir}/Scripts/scNym/run_scNym.py \ + {input.reference} \ + {input.labfile} \ + {input.query} \ + {output.pred} \ + {params.threshold} \ + &> {log} + """ + #---------------------------------------------------- # The End #---------------------------------------------------- diff --git a/snakefile.benchmark b/snakefile.benchmark index f9f8683..d79d650 100644 --- a/snakefile.benchmark +++ b/snakefile.benchmark @@ -749,6 +749,35 @@ rule run_scAnnotate: &> {log} """ +#---------------------------------------------------- +# scNym +#---------------------------------------------------- + +rule run_scNym: + input: + reference = config['output_dir'] + "/fold{folds_index}/train.csv", + labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv", + query = config['output_dir'] + "/fold{folds_index}/test.csv" + output: + pred = config['output_dir'] + "/fold{folds_index}/scNym/scNym_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = 0.5 + log: + config['output_dir'] + "/fold{folds_index}/scNym/scNym.log" + benchmark: + config['output_dir'] + "/fold{folds_index}/scNym/scNym_predict_benchmark.txt" + shell: + """ + python {params.basedir}/Scripts/scNym/run_scNym.py \ + {input.reference} \ + {input.labfile} \ + {input.query} \ + {output.pred} \ + {params.threshold} \ + &> {log} + """ + #---------------------------------------------------- # The End #---------------------------------------------------- From c050f0e89eba942e38a6a7d22be007a6ec3c469b Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Wed, 16 Aug 2023 12:53:32 -0400 Subject: [PATCH 069/144] Dev bhavyaa issue 51 (#52) * Throw error for low % genes in common gene set * Fix typo in snakefile consensus rule --- Scripts/preprocess.R | 18 +++++++++++++----- snakefile.annotate | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Scripts/preprocess.R b/Scripts/preprocess.R index 3467d58..a5aa499 100644 --- a/Scripts/preprocess.R +++ b/Scripts/preprocess.R @@ -1,6 +1,5 @@ library(tidyverse) -library(WGCNA) args = commandArgs(trailingOnly = TRUE) ref_path = args[1] @@ -28,6 +27,7 @@ if(convert_genes){ library(org.Mm.eg.db) library(org.Hs.eg.db) library(WGCNA) + mapfun = function(mousegenes){ gns = mapIds(org.Mm.eg.db, mousegenes, "ENTREZID", "SYMBOL") mapped = AnnotationDbi::select(Orthology.eg.db, gns, "Homo_sapiens","Mus_musculus") @@ -52,7 +52,6 @@ if(convert_genes){ } # modify reference matrix to contain converted genes - l[['ref']] = l[['ref']] %>% transposeBigData() %>% rownames_to_column('Mouse_symbol') %>% @@ -68,13 +67,22 @@ if(convert_genes){ genes = lapply(l, function(x){(colnames(x))}) # reduce set of genes to the intersect -genes = Reduce(intersect, genes) +common_genes = Reduce(intersect,genes) + +# throw error if number of common genes below % threshold of genes in any of provided datasets (ref or query) +threshold = 0.5 +if(any(length(common_genes) < threshold*length(genes))){ + frac = lapply(genes, function(x){length(common_genes)/length(x)}) + names(frac) = names(l) + print(frac) + stop(paste0("@ In at least one provided dataset (ref or query), less than ",threshold*100,"% of genes appear in common gene set. See above for the fraction of genes from each dataset appearing in common gene set (note: samples with few genes will have higher fractions)")) +} # save common genes -data.table::fwrite(data.frame('common_genes' = genes), paste0(out_path, '/common_genes.csv')) +data.table::fwrite(data.frame('common_genes' = common_genes), paste0(out_path, '/common_genes.csv')) # filter each data set for common genes -l = lapply(l, function(x){x[,genes]}) +l = lapply(l, function(x){x[,common_genes]}) # save reference tmp = l[['ref']] %>% rownames_to_column() diff --git a/snakefile.annotate b/snakefile.annotate index 55a8bf5..7c7c022 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -67,7 +67,7 @@ rule consensus: params: basedir = {workflow.basedir}, tools = config['tools_to_run'], - conesnsus_tools = config['consensus_tools'], + consensus_tools = config['consensus_tools'], labfile = config['reference_annotations'], sample = config["output_dir"] + "/{sample}" shell: From bf4dfa66ec88237ad9b29a97bcd46c162e2e8ce0 Mon Sep 17 00:00:00 2001 From: Tom <64378638+tvegawaichman@users.noreply.github.com> Date: Wed, 16 Aug 2023 14:33:04 -0400 Subject: [PATCH 070/144] tool: CellTypist (#53) * tool: CellTypist * documentation: CellTypist * add PR feedback --- README.md | 31 ++++++++ Scripts/CellTypist/predict_CellTypist.py | 94 ++++++++++++++++++++++++ Scripts/CellTypist/train_CellTypist.py | 88 ++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 Scripts/CellTypist/predict_CellTypist.py create mode 100644 Scripts/CellTypist/train_CellTypist.py diff --git a/README.md b/README.md index dacd98c..6bc04e2 100644 --- a/README.md +++ b/README.md @@ -344,3 +344,34 @@ confidence scores. The scNym workflow was generated following the tutorials provided below: * https://github.com/calico/scnym/tree/master + +## CellTypist +Documentation written by: Tomas Vega Waichman +Date written: 2023-08-16 + +CellTypist allows to separate between training and reference. +It Allows parallelization. +It has their own pre-trained models. +CellTypist requires a logarithmised and normalised expression matrix stored in the `AnnData` (log1p normalised expression to 10,000 counts per cell) [link](https://github.com/Teichlab/celltypist#supplemental-guidance-generate-a-custom-model) +Training: +* I use `check_expression = True` to check that the expression is okay. +* celltypist.train has the option `(feature_selection = True)` in order to do a feature_selection, but it is not implemented. +* The output is the model and and from the model we get the top markers for each cell type using the function `model.extract_top_markers()`. A table with the top 10 genes per cell-type is returned too (top10_model_markers_per_celltype.csv). +Predicting: +* "By default, CellTypist will only do the prediction jobs to infer the identities of input cells, which renders the prediction of each cell independent. To combine the cell type predictions with the cell-cell transcriptomic relationships, CellTypist offers a majority voting approach based on the idea that similar cell subtypes are more likely to form a (sub)cluster regardless of their individual prediction outcomes. To turn on the majority voting classifier in addition to the CellTypist predictions, pass in `majority_voting = True`. +If `majority_voting = True` all the predict column will be the majority_voting results otherwise it use the predicted_labels where each query cell gets its inferred label by choosing the most probable cell type among all possible cell types in the given model." [link](https://celltypist.readthedocs.io/en/latest/notebook/celltypist_tutorial_ml.html) +* majority_voting parameter should be specified in the configfile. +* I use the multilabel prediction… since we want to know if a cell cannot be classified very clearly… Description: For the built-in models, we have collected a large number of cell types; yet, the presence of unexpected (e.g., low-quality or novel cell types) and ambiguous cell states (e.g., doublets) in the query data is beyond the prediction that CellTypist can achieve with a 'find-a-best-match' mode. To overcome this, CellTypist provides the option of multi-label cell type classification, which assigns 0 (i.e., unassigned), 1, or >=2 cell type labels to each query cell. It allows the use of a `threshold` to label cells that are below that probability as "Unnasigned". It allows to have intermediate labels as combination in the format of `celltype1|celltype2`. +* Output: 4 .csv, the prediction for each cell (depending if we choose majority_voting or not will be the majority_voting or not), + * decision_matrix.csv : Decision matrix with the decision score of each cell belonging to a given cell type. + * probability_matrix.csv: Probability matrix representing the probability each cell belongs to a given cell type (transformed from decision matrix by the sigmoid function). + * predicted_labels.csv: The prediction for each cell, if majority_voting was true it has the information of the majority_voting labels AND the predicted_labels. + * Generates some embedding plots. + * An .h5ad object that has all the previous information (with the embeddings too) in a h5ad object. + +The CellTypist workflow was generated following the tutorials provided below: +Training: +* https://celltypist.readthedocs.io/en/latest/celltypist.train.html +* https://github.com/Teichlab/celltypist#supplemental-guidance-generate-a-custom-model +Predicting: +* https://celltypist.readthedocs.io/en/latest/notebook/celltypist_tutorial_ml.html diff --git a/Scripts/CellTypist/predict_CellTypist.py b/Scripts/CellTypist/predict_CellTypist.py new file mode 100644 index 0000000..410ae1e --- /dev/null +++ b/Scripts/CellTypist/predict_CellTypist.py @@ -0,0 +1,94 @@ +#--------------- Libraries ------------------- +import numpy as np +import pandas as pd +import celltypist +import anndata as ad +import sys +import scanpy as sc +import pickle +import os +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +sample_path = str(sys.argv[1]) +model_path = str(sys.argv[2]) +out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.argv[3])) +threshold = float(sys.argv[4]) +threads = int(sys.argv[5]) +majority_voting = bool(sys.argv[6]) + +#--------------- Data ------------------------- +print('@ READ QUERY') +query = pd.read_csv(sample_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +print('@ DONE') + +query = ad.AnnData(X = query, + obs = dict(obs_names=query.index.astype(str)), + var = dict(var_names=query.columns.astype(str)) +) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(query, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(query) + + +# load model +print('@ LOAD MODEL')) +CellTypist_model = pickle.load(open(model_path, 'rb')) +print('@ DONE') + +### If the trained model was generated with a diff number of threads and the +## prediction is done with other number +CellTypist_model.classifier.n_jobs = threads + +#------------- Predict CellTypist ------------- + +predictions = celltypist.annotate(query, + model = CellTypist_model, + majority_voting = majority_voting, + mode = 'prob match', + p_thres = threshold) + +## If the majority voting is true, return that result +if majority_voting: + pred_df = pd.DataFrame({'cell': predictions.predicted_labels.index, + 'CellTypist': predictions.predicted_labels.majority_voting}) +else: + pred_df = pd.DataFrame({'cell': predictions.predicted_labels.index, + "CellTypist": predictions.predicted_labels.predicted_labels}) + +print('@ WRITTING PREDICTIONS') +pred_df.to_csv(out_path, index = False) +print('@ DONE') + +#------------- Other outputs -------------- +## Save the outputs as .csv +print('@ WRITTING CSV OUTPUTS') +predictions.to_table(out_other_path) +print('@ DONE') +## Save the output plots +print('@ GENERATING OUTPUT PLOTS') +prediction.to_plot(out_other_path) +print('@ DONE') + +### Save the prob matrix +print('@ WRITTING OUTPUT OBJECT') +filename = out_other_path + '/CellTypist_output_object.h5ad' +adata = predictions.to_adata(insert_prob = True) +adata.write_h5ad(filename= filename, + compression='gzip') +print('@ DONE ') + + diff --git a/Scripts/CellTypist/train_CellTypist.py b/Scripts/CellTypist/train_CellTypist.py new file mode 100644 index 0000000..0e22a21 --- /dev/null +++ b/Scripts/CellTypist/train_CellTypist.py @@ -0,0 +1,88 @@ +## Python 3.11.2 +#--------------- Libraries ------------------- +import numpy as np +import pandas as pd +import celltypist +import anndata as ad +import sys +import scanpy as sc +import pickle +import os +import random +### Set seed +random.seed(123456) + +#--------------- Parameters ------------------- +ref_path = str(sys.argv[1]) +lab_path = str(sys.argv[2]) +out_path = str(sys.argv[3]) +out_other_path = os.path.dirname(str(sys.argv[3])) +threads = int(sys.argv[4]) +feature_selection = bool(sys.argv[5]) +#--------------- Data ------------------------- +# read the data +ref = pd.read_csv(ref_path, + index_col=0, + sep=',', + engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies + +labels = pd.read_csv(lab_path, + index_col = 0, + sep=',') + +# check if cell names are in the same order in labels and ref +order = all(labels.index == ref.index) +# throw error if order is not the same +if not order: + sys.exit("@ Order of cells in reference and labels do not match") + +adata = ad.AnnData(X = ref, + obs = dict(obs_names=ref.index.astype(str), + label = labels['label']), + var = dict(var_names=ref.columns.astype(str)) + ) + +## Now I normalize the matrix with scanpy: +#Normalize each cell by total counts over all genes, +#so that every cell has the same total count after normalization. +#If choosing `target_sum=1e6`, this is CPM normalization +#1e4 similar as Seurat +sc.pp.normalize_total(adata, target_sum=1e4) +#Logarithmize the data: +sc.pp.log1p(adata) + +#------------- Train CellTypist ------------- + +model = celltypist.train(adata, + labels = "label", + transpose_input = False, + check_expression = True, + feature_selection = feature_selection, + n_jobs = threads) + + +#Save the model to disk +print('@ SAVE MODEL') +new_model.write(out_path) +print('@ DONE') + +#------------- Other outputs -------------- +#We can extract the top markers, I get the top 10 for each cell-type applying the +# function extract_top_markers +dataframes = [] +for cell_type in CellTypist_model.cell_types: + top_markers = CellTypist_model.extract_top_markers(cell_type, 10) + + # Create a DataFrame for the current cell type's top markers + df = pd.DataFrame(top_markers, columns=['Marker']) + df['CellType'] = cell_type # Add a column to store the cell type + + # Append the DataFrame to the list + dataframes.append(df) + +# Concatenate all DataFrames into a single DataFrame +markers_df = pd.concat(dataframes, ignore_index=True) + +filename = out_other_path + "/top10_model_markers_per_celltype.csv" +markers_df.to_csv(filename, + index=False) From 6a84c3034665e44971306ef9641cf66c8a5cef34 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 29 Aug 2023 09:27:30 -0400 Subject: [PATCH 071/144] Dev alva update config (#58) * resolved conflict * workflow takes multiple references in the same config * final version functional * updated to take config with default parameters for all tools * switched output folder structure * updated benchmark workflow to use default config --- Config/config.default.yml | 52 ++++ Notebooks/annotate_report.Rmd | 51 ++-- Report/workflow.rst | 3 + Scripts/ACTINN/train_ACTINN.py | 1 - Scripts/preprocess.R | 20 +- example.config.yml | 1 + snakefile.annotate | 423 ++++++++++++++++++--------------- snakefile.benchmark | 414 +++++++++++++++++--------------- 8 files changed, 550 insertions(+), 415 deletions(-) create mode 100644 Config/config.default.yml create mode 100644 Report/workflow.rst diff --git a/Config/config.default.yml b/Config/config.default.yml new file mode 100644 index 0000000..0496ed1 --- /dev/null +++ b/Config/config.default.yml @@ -0,0 +1,52 @@ + +SingleR: + threads: 1 + +Correlation: + threads: 1 + +scPred: + threads: 1 + classifier: 'svmRadial' + +scClassify: + threads: 1 + +SciBet: + threads: 1 + +singleCellNet: + threads: 1 + +scHPL: + threads: 1 + classifier: 'svm' + dimred: 'False' + threshold: 0.5 + +SVMlinear: + threads: 1 + threshold: 0 + classifier: 'SVMlinear' + +SVC: + threads: 1 + classifier: 'rbf' + threshold: 0.5 + +ACTINN: + threads: 1 + +scLearn: + threads: 1 + +scID: + threads: 1 + +scAnnotate: + threads: 1 + threshold: 0.5 + +scNym: + threads: 1 + threshold: 0.5 diff --git a/Notebooks/annotate_report.Rmd b/Notebooks/annotate_report.Rmd index 17d7cb9..f352458 100644 --- a/Notebooks/annotate_report.Rmd +++ b/Notebooks/annotate_report.Rmd @@ -10,13 +10,13 @@ output: code_folding: hide params: refs: '' - ref_anno: '' tools: '' consensus: '' - pred_path: '' + output_dir: '' sample: '' threads: '' marker_genes: '' + query: '' --- ```{r setup, knitr_options, echo=F} @@ -101,7 +101,7 @@ create_color_pal = function(class, mb = 'Juarez'){ return(pal) } -plot_bar_largest_group = function(seurat, meta_column = '', palet = pal, fr = 0.1){ +plot_bar_largest_group = function(seurat, meta_column = '', pal = pal, fr = 0.1){ df = seurat@meta.data %>% count(seurat_clusters, .data[[meta_column]]) %>% @@ -131,7 +131,7 @@ p1 = df %>% strip.background = element_blank(), aspect.ratio = 0.5) - p2 = ggplotly(p1, tooltip = c('text')) + p2 = ggplotly(p1, tooltip = c('text')) %>% toWebGL() return(p2) } @@ -198,7 +198,7 @@ umap_plotly = function(seurat, meta_column, pal){ scale_color_manual(values = pal) + theme_bw() + umap_theme + theme(legend.position = 'right') - p2 = ggplotly(plot = p1, tooltip = c('text')) %>% layout(autosize = F, width = 550, height = 450) + p2 = ggplotly(plot = p1, tooltip = c('text')) %>% layout(autosize = F, width = 550, height = 450) %>% toWebGL() return(p2) } @@ -233,8 +233,8 @@ calculate_percentage_unsure = function(pred, order){ warn$TOOL = cell_spec(warn$TOOL, bold = ifelse(warn$TOOL == 'Consensus', T, F), - background = ifelse(warn$TOOL == 'Consensus', 'black', 'white'), - color = ifelse(warn$TOOL == 'Consensus', 'white', 'black')) + background = ifelse(warn$TOOL == 'Consensus', 'black', 'white'), + color = ifelse(warn$TOOL == 'Consensus', 'white', 'black')) return(warn) } @@ -252,27 +252,28 @@ umap_theme = theme(aspect.ratio = 1, ``` -```{r, results='hide'} -# read labels from refrence (used to harmonize 'usure' call) -ref_labels = data.table::fread(params$ref_anno, header = T) %>% column_to_rownames('V1') - +```{r} # read prediction summary for each reference list = list() + for(r in refs){ - list[[r]] = data.table::fread(paste0(params$pred_path, '/', params$sample, '/Prediction_Summary.tsv')) %>% - harmonize_unsure(., ref_labels) + list[[r]]$lab = data.table::fread(paste0(params$output_dir, '/model/', r, '/labels.csv'), header = T) + list[[r]]$pred = data.table::fread(paste0(params$output_dir, '/', params$sample, '/', r, '/Prediction_Summary.tsv')) %>% + harmonize_unsure(., list[[r]]$lab) + + # create reference pal + list[[r]]$pal = create_color_pal(list[[r]]$lab$label) + + #save(list[[r]]$pal, file = paste0(params$output_dir, '/model/', r, '/class_pal.Rda')) } # read expression matrix for sample -query = data.table::fread(paste0(params$pred_path, '/', params$sample, '/expression.csv'), +query = data.table::fread(paste0(params$query), nThread=threads, header=T, data.table=F) %>% column_to_rownames('V1') - - - ``` ```{r, results='hide'} @@ -295,13 +296,9 @@ query = query %>% set.seed(12345) cat("\n") -# create reference pal -pal = create_color_pal(ref_labels$label) -save(pal, file = paste0(params$pred_path, '/', params$sample, '/report/class_pal.Rda')) - for(r in refs){ - query = AddMetaData(query, list[[r]]) + query = AddMetaData(query, list[[r]]$pred) cat(" \n#", r, "{.tabset} \n") @@ -309,7 +306,7 @@ for(r in refs){ cat("

Clusters

") - p = umap_plotly(query, 'seurat_clusters', unname(pal)) + p = umap_plotly(query, 'seurat_clusters', unname(list[[r]]$pal)) print(htmltools::tagList(p)) cat("\n") @@ -333,7 +330,7 @@ for(r in refs){ cat("

Percentage Unsure

") - calculate_percentage_unsure(list[[r]], order = tools) %>% + calculate_percentage_unsure(list[[r]]$pred, order = tools) %>% kbl(escape = FALSE, row.names = F) %>% kable_styling(position = "center") %>% print() @@ -361,21 +358,21 @@ for(r in refs){ cat("

Top class per cluster

") - p = plot_bar_largest_group(query, t, fr = 0.1) + p = plot_bar_largest_group(query, t, fr = 0.1, pal = list[[r]]$pal) print(htmltools::tagList(p)) cat("

UMAP

") cat("\n") - p = umap_plotly(query, t, pal) + p = umap_plotly(query, t, list[[r]]$pal) print(htmltools::tagList(p)) cat("\n") cat("

UMAP per class

") - l = color_class_seurat(query, t, pal) + l = color_class_seurat(query, t, list[[r]]$pal) if(length(l)< 9){ cowplot::plot_grid(plotlist = l[1:9], ncol = 3) %>% print() }else{ diff --git a/Report/workflow.rst b/Report/workflow.rst new file mode 100644 index 0000000..b28b04f --- /dev/null +++ b/Report/workflow.rst @@ -0,0 +1,3 @@ + + + diff --git a/Scripts/ACTINN/train_ACTINN.py b/Scripts/ACTINN/train_ACTINN.py index fe57cd1..9270d89 100644 --- a/Scripts/ACTINN/train_ACTINN.py +++ b/Scripts/ACTINN/train_ACTINN.py @@ -266,7 +266,6 @@ def model(X_train, Y_train, starting_learning_rate = 0.0001, num_epochs = 1500, return parameters - if __name__ == '__main__': parser = get_parser() args = parser.parse_args() diff --git a/Scripts/preprocess.R b/Scripts/preprocess.R index a5aa499..8adbe43 100644 --- a/Scripts/preprocess.R +++ b/Scripts/preprocess.R @@ -4,8 +4,10 @@ library(tidyverse) args = commandArgs(trailingOnly = TRUE) ref_path = args[1] query_paths = strsplit(args[2], split = ' ')[[1]] -out_path = args[3] +out = args[3] convert_genes = as.logical(args[4]) +lab_path = args[5] +reference_name = args[6] l = list() @@ -43,7 +45,7 @@ if(convert_genes){ # output list of mouse genes that were not converted not_converted = hg %>% filter(is.na(Human_symbol)) %>% .$Mouse_symbol - data.table::fwrite(as.list(not_converted), file = paste0(out_path, '/genes_not_converted.csv'), sep = ',') + data.table::fwrite(as.list(not_converted), file = paste0(reference_out, '/genes_not_converted.csv'), sep = ',') # throw error if more than threshold % genes not converted threshold = 0.5 @@ -79,7 +81,7 @@ if(any(length(common_genes) < threshold*length(genes))){ } # save common genes -data.table::fwrite(data.frame('common_genes' = common_genes), paste0(out_path, '/common_genes.csv')) +data.table::fwrite(data.frame('common_genes' = genes), paste0(out, '/model/', reference_name, '/common_genes.csv')) # filter each data set for common genes l = lapply(l, function(x){x[,common_genes]}) @@ -87,7 +89,7 @@ l = lapply(l, function(x){x[,common_genes]}) # save reference tmp = l[['ref']] %>% rownames_to_column() colnames(tmp)[1] = " " -data.table::fwrite(tmp, file = paste0(out_path, '/expression.csv'), sep = ',') +data.table::fwrite(tmp, file = paste0(out, '/model/', reference_name, '/expression.csv'), sep = ',') # save query query_names = names(l)[!names(l) == 'ref'] @@ -95,5 +97,13 @@ for(q in query_names){ print(q) tmp = l[[q]] %>% rownames_to_column() colnames(tmp)[1] = " " - data.table::fwrite(tmp, file = paste0(out_path, '/', q, '/expression.csv'), sep = ',') + data.table::fwrite(tmp, file = paste0(out, '/', q, '/', reference_name, '/expression.csv'), sep = ',') } + +# save unique labels (for downstream report color pal) +lab = data.table::fread(lab_path, header = T) %>% column_to_rownames('V1') +lab = data.frame(label = unique(lab$label)) +data.table::fwrite(lab, file = paste0(out, '/model/', reference_name, '/labels.csv'), sep = ',') + + + diff --git a/example.config.yml b/example.config.yml index 7d1ab53..1588000 100644 --- a/example.config.yml +++ b/example.config.yml @@ -25,6 +25,7 @@ convert_ref_mm_to_hg: False # classifiers to run tools_to_run: - scPred + - model: ['SVMradial', 'glm'] - SingleR - scClassify - SciBet diff --git a/snakefile.annotate b/snakefile.annotate index 7c7c022..90f9f29 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -3,12 +3,14 @@ # Setup #---------------------------------------------------- +configfile: workflow.basedir + "/Config/config.default.yml" + # import libraries import os from datetime import datetime # Get the names of query samples from the paths given in the query section of the config -samples = [os.path.basename(os.path.dirname(query_path)) for query_path in config['query_datasets']] +samples = config['query_datasets'] now = datetime.now() dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") @@ -19,36 +21,40 @@ dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") rule all: input: - expand(config["output_dir"] + "/{sample}/Prediction_Summary.tsv", - sample = samples), + expand(config["output_dir"] + "/{sample}/{reference}/Prediction_Summary.tsv", + sample = samples, + reference = config['references']), expand(config['output_dir'] + '/{sample}/report/{sample}.prediction_report.' + dt_string + '.html', sample = samples) - #---------------------------------------------------- # Preprocess #---------------------------------------------------- rule preprocess: input: - reference = config['training_reference'], - query = config['query_datasets'] + reference = lambda wildcards:config['references'][wildcards.reference]['expression'], + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'], + query = config['query_datasets'].values() output: - reference = config['output_dir'] + "/expression.csv", - query = expand(config['output_dir'] + "/{sample}/expression.csv", sample = samples) + reference = config['output_dir'] + "/model/{reference}/expression.csv", + query = expand(config['output_dir'] + "/{sample}/{{reference}}/expression.csv", sample = samples) + log: + config['output_dir'] + "/model/{reference}/preprocess.log" params: basedir = {workflow.basedir}, - out_path = config['output_dir'], - convert_genes = config['convert_ref_mm_to_hg'] - log: config['output_dir'] + "/preprocess.log" - priority: 50 + out = config['output_dir'], + convert_genes = config['convert_ref_mm_to_hg'], + reference_name = "{reference}" shell: """ Rscript {params.basedir}/Scripts/preprocess.R \ {input.reference} \ "{input.query}" \ - {params.out_path} \ + {params.out} \ {params.convert_genes} \ + {input.labfile} \ + {params.reference_name} \ &> {log} """ @@ -58,18 +64,18 @@ rule preprocess: rule consensus: input: - results = expand(config["output_dir"] + "/{{sample}}/{tool}/{tool}_pred.csv", + results = expand(config["output_dir"] + "/{{sample}}/{{reference}}/{tool}/{tool}_pred.csv", tool=config['tools_to_run']) output: - prediction_summary = config["output_dir"] + "/{sample}/Prediction_Summary.tsv" + prediction_summary = config["output_dir"] + "/{sample}/{reference}/Prediction_Summary.tsv" log: - config["output_dir"] + "/{sample}/Gatherpreds.log" + config["output_dir"] + "/{sample}/{reference}/Gatherpreds.log" params: basedir = {workflow.basedir}, tools = config['tools_to_run'], consensus_tools = config['consensus_tools'], - labfile = config['reference_annotations'], - sample = config["output_dir"] + "/{sample}" + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'], + sample = config["output_dir"] + "/{sample}/{reference}/" shell: """ Rscript {params.basedir}/Scripts/calculate_consensus.R \ @@ -87,19 +93,19 @@ rule consensus: rule knit_report: input: - pred = config['output_dir'] + "/{sample}/Prediction_Summary.tsv" + pred = expand(config['output_dir'] + "/{{sample}}/{reference}/Prediction_Summary.tsv", reference = config['references']), + query = lambda wildcards:config['query_datasets'][wildcards.sample] output: report_path = config['output_dir'] + '/{sample}/report/{sample}.prediction_report.' + dt_string + '.html' log: config['output_dir'] + "/{sample}/report/report.log" params: basedir = {workflow.basedir}, - pred_path = config['output_dir'], + output_dir = config['output_dir'], sample = "{sample}", tools = config['tools_to_run'], consensus = config['consensus_tools'], - refs = config['references'], - ref_anno = config['reference_annotations'], + refs = list(config['references'].keys()), marker_genes = config['marker_genes'] threads: 1 resources: @@ -110,11 +116,11 @@ rule knit_report: params = list(tools = '{params.tools}', consensus = '{params.consensus}', refs = '{params.refs}', - ref_anno = '{params.ref_anno}', - pred_path = '{params.pred_path}', + output_dir = '{params.output_dir}', sample = '{params.sample}', marker_genes = '{params.marker_genes}', - threads = '{threads}'), + threads = '{threads}', + query = '{input.query}'), output_file = '{output.report_path}')" \ &> {log} """ @@ -125,17 +131,18 @@ rule knit_report: rule train_SingleR: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/SingleR/SingleR_model.Rda" + model = config['output_dir'] + "/model/{reference}/SingleR/SingleR_model.Rda" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} log: - config['output_dir'] + "/SingleR/SingleR.log" + config['output_dir'] + "/model/{reference}/SingleR/SingleR.log" benchmark: - config['output_dir'] + "/SingleR/SingleR_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/SingleR/SingleR_train_benchmark.txt" + threads: + config['SingleR']['threads'] resources: shell: """ @@ -149,17 +156,18 @@ rule train_SingleR: rule predict_SingleR: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/SingleR/SingleR_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/SingleR/SingleR_model.Rda" output: - pred = config['output_dir'] + "/{sample}/SingleR/SingleR_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/SingleR/SingleR_pred.csv" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/SingleR/SingleR.log" + config['output_dir'] + "/{sample}/{reference}/SingleR/SingleR.log" benchmark: - config['output_dir'] + "/{sample}/SingleR/SingleR_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/SingleR/SingleR_predict_benchmark.txt" + threads: + config['SingleR']['threads'] resources: shell: """ @@ -177,18 +185,19 @@ rule predict_SingleR: rule train_scPred: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/scPred/scPred_model.Rda" + model = config['output_dir'] + "/model/{reference}/scPred/scPred_model.Rda" params: basedir = {workflow.basedir}, - model_type = "svmRadial", + classifier = config['scPred']['classifier'], log: - config['output_dir'] + "/scPred/scPred.log" + config['output_dir'] + "/model/{reference}/scPred/scPred.log" benchmark: - config['output_dir'] + "/scPred/scPred_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/scPred/scPred_train_benchmark.txt" + threads: + config['scPred']['threads'] resources: shell: """ @@ -197,23 +206,24 @@ rule train_scPred: {input.labfile} \ {output.model} \ {threads} \ - {params.model_type} \ + {params.classifier} \ &> {log} """ rule predict_scPred: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/scPred/scPred_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/scPred/scPred_model.Rda" output: - pred = config['output_dir'] + "/{sample}/scPred/scPred_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/scPred/scPred_pred.csv" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/scPred/scPred.log" + config['output_dir'] + "/{sample}/{reference}/scPred/scPred.log" benchmark: - config['output_dir'] + "/{sample}/scPred/scPred_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/scPred/scPred_predict_benchmark.txt" + threads: + config['scPred']['threads'] resources: shell: """ @@ -230,17 +240,18 @@ rule predict_scPred: rule train_scClassify: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/scClassify/scClassify_model.Rda" + model = config['output_dir'] + "/model/{reference}/scClassify/scClassify_model.Rda" params: basedir = {workflow.basedir}, log: - config['output_dir'] + "/scClassify/scClassify.log" + config['output_dir'] + "/model/{reference}/scClassify/scClassify.log" benchmark: - config['output_dir'] + "/scClassify/scClassify_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/scClassify/scClassify_train_benchmark.txt" + threads: + config['scClassify']['threads'] resources: shell: """ @@ -254,17 +265,18 @@ rule train_scClassify: rule predict_scClassify: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/scClassify/scClassify_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/scClassify/scClassify_model.Rda" output: - pred = config['output_dir'] + "/{sample}/scClassify/scClassify_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/scClassify/scClassify_pred.csv" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/scClassify/scClassify.log" + config['output_dir'] + "/{sample}/{reference}/scClassify/scClassify.log" benchmark: - config['output_dir'] + "/{sample}/scClassify/scClassify_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/scClassify/scClassify_predict_benchmark.txt" + threads: + config['scClassify']['threads'] resources: shell: """ @@ -282,17 +294,18 @@ rule predict_scClassify: rule train_SciBet: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/SciBet/SciBet_model.Rda" + model = config['output_dir'] + "/model/{reference}/SciBet/SciBet_model.Rda" params: basedir = {workflow.basedir}, log: - config['output_dir'] + "/SciBet/SciBet.log" + config['output_dir'] + "/model/{reference}/SciBet/SciBet.log" benchmark: - config['output_dir'] + "/SciBet/SciBet_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/SciBet/SciBet_train_benchmark.txt" + threads: + config['SciBet']['threads'] resources: shell: """ @@ -306,17 +319,18 @@ rule train_SciBet: rule predict_SciBet: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/SciBet/SciBet_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/SciBet/SciBet_model.Rda" output: - pred = config['output_dir'] + "/{sample}/SciBet/SciBet_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/SciBet/SciBet_pred.csv" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/SciBet/SciBet.log" + config['output_dir'] + "/{sample}/{reference}/SciBet/SciBet.log" benchmark: - config['output_dir'] + "/{sample}/SciBet/SciBet_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/SciBet/SciBet_predict_benchmark.txt" + threads: + config['SciBet']['threads'] resources: shell: """ @@ -334,19 +348,20 @@ rule predict_SciBet: rule train_scHPL: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/scHPL/scHPL_model.pkl" + model = config['output_dir'] + "/model/{reference}/scHPL/scHPL_model.pkl" params: basedir = {workflow.basedir}, - classifier = 'svm', - dimred = 'False' + classifier = config['scHPL']['classifier'], + dimred = config['scHPL']['dimred'] log: - config['output_dir'] + "/scHPL/scHPL.log" + config['output_dir'] + "/model/{reference}/scHPL/scHPL.log" benchmark: - config['output_dir'] + "/scHPL/scHPL_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/scHPL/scHPL_train_benchmark.txt" + threads: + config['scHPL']['threads'] resources: shell: """ @@ -361,18 +376,19 @@ rule train_scHPL: rule predict_scHPL: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/scHPL/scHPL_model.pkl" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/scHPL/scHPL_model.pkl" output: - pred = config['output_dir'] + "/{sample}/scHPL/scHPL_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/scHPL/scHPL_pred.csv" params: - basedir = {workflow.basedir}, - threshold = 0.5 + basedir = {workflow.basedir}, + threshold = config['scHPL']['threshold'] log: - config['output_dir'] + "/{sample}/scHPL/scHPL.log" + config['output_dir'] + "/{sample}/{reference}/scHPL/scHPL.log" benchmark: - config['output_dir'] + "/{sample}/scHPL/scHPL_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/scHPL/scHPL_predict_benchmark.txt" + threads: + config['scHPL']['threads'] resources: shell: """ @@ -390,17 +406,18 @@ rule predict_scHPL: rule train_SVMlinear: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/SVMlinear/SVMlinear_model.pkl" + model = config['output_dir'] + "/model/{reference}/SVMlinear/SVMlinear_model.pkl" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/SVMlinear/SVMlinear.log" + config['output_dir'] + "/model/{reference}/SVMlinear/SVMlinear.log" benchmark: - config['output_dir'] + "/SVMlinear/SVMlinear_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/SVMlinear/SVMlinear_train_benchmark.txt" + threads: + config['SVMlinear']['threads'] resources: shell: """ @@ -414,19 +431,20 @@ rule train_SVMlinear: rule predict_SVMlinear: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/SVMlinear/SVMlinear_model.pkl" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/SVMlinear/SVMlinear_model.pkl" output: - pred = config['output_dir'] + "/{sample}/SVMlinear/SVMlinear_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/SVMlinear/SVMlinear_pred.csv" params: basedir = {workflow.basedir}, - threshold = 0, - tool_name = 'SVMlinear' + threshold = config['SVMlinear']['threshold'], + tool_name = config['SVMlinear']['classifier'] log: - config['output_dir'] + "/{sample}/SVMlinear/SVMlinear.log" + config['output_dir'] + "/{sample}/{reference}/SVMlinear/SVMlinear.log" benchmark: - config['output_dir'] + "/{sample}/SVMlinear/SVMlinear_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/SVMlinear/SVMlinear_predict_benchmark.txt" + threads: + config['SVMlinear']['threads'] resources: shell: """ @@ -446,18 +464,19 @@ rule predict_SVMlinear: rule train_SVC: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/SVCrbf/SVCrbf_model.Rda" + model = config['output_dir'] + "/model/{reference}/SVCrbf/SVCrbf_model.Rda" params: basedir = {workflow.basedir}, - classifier = 'rbf' + classifier = config['SVC']['classifier'] log: - config['output_dir'] + "/SVCrbf/SVCrbf.log" + config['output_dir'] + "/model/{reference}/SVCrbf/SVCrbf.log" benchmark: - config['output_dir'] + "/SVCrbf/SVCrbf_train_benchmark.txt" - threads: 10 + config['output_dir'] + "/model/{reference}/SVCrbf/SVCrbf_train_benchmark.txt" + threads: + config['SVC']['threads'] resources: shell: """ @@ -472,19 +491,20 @@ rule train_SVC: rule predict_SVC: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/SVCrbf/SVCrbf_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/{reference}/SVCrbf/SVCrbf_model.Rda" output: - pred = config['output_dir'] + "/{sample}/SVCrbf/SVCrbf_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/SVCrbf/SVCrbf_pred.csv" params: basedir = {workflow.basedir}, - threshold = 0.5, - tool_name = 'rbf' + threshold = config['SVC']['threshold'], + tool_name = config['SVC']['classifier'] log: - config['output_dir'] + "/{sample}/SVCrbf/SVCrbf.log" + config['output_dir'] + "/{sample}/{reference}/SVCrbf/SVCrbf.log" benchmark: - config['output_dir'] + "/{sample}/SVCrbf/SVCrbf_predict_benchmark.txt" - threads: 10 + config['output_dir'] + "/{sample}/{reference}/SVCrbf/SVCrbf_predict_benchmark.txt" + threads: + config['SVC']['threads'] resources: shell: """ @@ -504,17 +524,18 @@ rule predict_SVC: rule train_singleCellNet: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/singleCellNet/singleCellNet_model.Rda" + model = config['output_dir'] + "/model/{reference}/singleCellNet/singleCellNet_model.Rda" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/singleCellNet/singleCellNet.log" + config['output_dir'] + "/model/{reference}/singleCellNet/singleCellNet.log" benchmark: - config['output_dir'] + "/singleCellNet/singleCellNet_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/singleCellNet/singleCellNet_train_benchmark.txt" + threads: + config['singleCellNet']['threads'] resources: shell: """ @@ -528,17 +549,18 @@ rule train_singleCellNet: rule predict_singleCellNet: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/singleCellNet/singleCellNet_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/singleCellNet/singleCellNet_model.Rda" output: - pred = config['output_dir'] + "/{sample}/singleCellNet/singleCellNet_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/singleCellNet/singleCellNet_pred.csv" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/singleCellNet/singleCellNet.log" + config['output_dir'] + "/{sample}/{reference}/singleCellNet/singleCellNet.log" benchmark: - config['output_dir'] + "/{sample}/singleCellNet/singleCellNet_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/singleCellNet/singleCellNet_predict_benchmark.txt" + threads: + config['singleCellNet']['threads'] resources: shell: """ @@ -556,17 +578,18 @@ rule predict_singleCellNet: rule train_Correlation: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/Correlation/Correlation_model.Rda" + model = config['output_dir'] + "/model/{reference}/Correlation/Correlation_model.Rda" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/Correlation/Correlation.log" + config['output_dir'] + "/model/{reference}/Correlation/Correlation.log" benchmark: - config['output_dir'] + "/Correlation/Correlation_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/Correlation/Correlation_train_benchmark.txt" + threads: + config['Correlation']['threads'] resources: shell: """ @@ -580,17 +603,18 @@ rule train_Correlation: rule predict_Correlation: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/Correlation/Correlation_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/Correlation/Correlation_model.Rda" output: - pred = config['output_dir'] + "/{sample}/Correlation/Correlation_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/Correlation/Correlation_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/Correlation/Correlation.log" + config['output_dir'] + "/{sample}/{reference}/Correlation/Correlation.log" benchmark: - config['output_dir'] + "/{sample}/Correlation/Correlation_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/Correlation/Correlation_predict_benchmark.txt" + threads: + config['Correlation']['threads'] resources: shell: """ @@ -608,17 +632,18 @@ rule predict_Correlation: rule train_scLearn: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/scLearn/scLearn_model.Rda" + model = config['output_dir'] + "/model/{reference}/scLearn/scLearn_model.Rda" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/scLearn/scLearn.log" + config['output_dir'] + "/model/{reference}/scLearn/scLearn.log" benchmark: - config['output_dir'] + "/scLearn/scLearn_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/model/{reference}/scLearn/scLearn_train_benchmark.txt" + threads: + config['scLearn']['threads'] resources: shell: """ @@ -632,17 +657,18 @@ rule train_scLearn: rule predict_scLearn: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/scLearn/scLearn_model.Rda" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/scLearn/scLearn_model.Rda" output: - pred = config['output_dir'] + "/{sample}/scLearn/scLearn_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/scLearn/scLearn_pred.csv" params: - basedir = {workflow.basedir} + basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/scLearn/scLearn.log" + config['output_dir'] + "/{sample}/{reference}/scLearn/scLearn.log" benchmark: - config['output_dir'] + "/{sample}/scLearn/scLearn_train_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/scLearn/scLearn_train_benchmark.txt" + threads: + config['scLearn']['threads'] resources: shell: """ @@ -660,16 +686,18 @@ rule predict_scLearn: rule train_ACTINN: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'] + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/ACTINN/ACTINN_model.pkl" + model = config['output_dir'] + "/model/{reference}/ACTINN/ACTINN_model.pkl" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/ACTINN/ACTINN.log" + config['output_dir'] + "/model/{reference}/ACTINN/ACTINN.log" benchmark: - config['output_dir'] + "/ACTINN/ACTINN_predict_benchmark.txt" + config['output_dir'] + "/model/{reference}/ACTINN/ACTINN_predict_benchmark.txt" + threads: + config['ACTINN']['threads'] shell: """ python {params.basedir}/Scripts/ACTINN/train_ACTINN.py \ @@ -681,17 +709,18 @@ rule train_ACTINN: rule predict_ACTINN: input: - query = config['output_dir'] + "/{sample}/expression.csv", - model = config['output_dir'] + "/ACTINN/ACTINN_model.pkl" + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/ACTINN/ACTINN_model.pkl" output: - pred = config['output_dir'] + "/{sample}/ACTINN/ACTINN_pred.csv" + pred = config['output_dir'] + "/{reference}/{sample}/ACTINN/ACTINN_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/ACTINN/ACTINN.log" + config['output_dir'] + "/{sample}/{reference}/ACTINN/ACTINN.log" benchmark: - config['output_dir'] + "/{sample}/ACTINN/ACTINN_predict_benchmark.txt" - threads: 1 + config['output_dir'] + "/{sample}/{reference}/ACTINN/ACTINN_predict_benchmark.txt" + threads: + config['ACTINN']['threads'] resources: shell: """ @@ -709,17 +738,19 @@ rule predict_ACTINN: rule run_scID: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'], - query = config['output_dir'] + "/{sample}/expression.csv" + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'], + query = config['output_dir'] + "/{sample}/{reference}/expression.csv" output: - pred = config['output_dir'] + "/{sample}/scID/scID_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/scID/scID_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/{sample}/scID/scID.log" + config['output_dir'] + "/{sample}/{reference}/scID/scID.log" benchmark: - config['output_dir'] + "/{sample}/scID/scID_predict_benchmark.txt" + config['output_dir'] + "/{sample}/{reference}/scID/scID_predict_benchmark.txt" + threads: + config['scID']['threads'] shell: """ Rscript {params.basedir}/Scripts/scID/run_scID.R \ @@ -737,18 +768,20 @@ rule run_scID: rule run_scAnnotate: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'], - query = config['output_dir'] + "/{sample}/expression.csv" + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'], + query = config['output_dir'] + "/{sample}/{reference}/expression.csv" output: - pred = config['output_dir'] + "/{sample}/scAnnotate/scAnnotate_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/scAnnotate/scAnnotate_pred.csv" params: basedir = {workflow.basedir}, - threshold = 0.5 + threshold = config['scAnnotate']['threshold'] log: - config['output_dir'] + "/{sample}/scAnnotate/scAnnotate.log" + config['output_dir'] + "/{sample}/{reference}/scAnnotate/scAnnotate.log" benchmark: - config['output_dir'] + "/{sample}/scAnnotate/scAnnotate_predict_benchmark.txt" + config['output_dir'] + "/{sample}/{reference}/scAnnotate/scAnnotate_predict_benchmark.txt" + threads: + config['scAnnotate']['threads'] shell: """ Rscript {params.basedir}/Scripts/scAnnotate/run_scAnnotate.R \ @@ -767,18 +800,20 @@ rule run_scAnnotate: rule run_scNym: input: - reference = config['output_dir'] + "/expression.csv", - labfile = config['reference_annotations'], - query = config['output_dir'] + "/{sample}/expression.csv" + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'], + query = config['output_dir'] + "/{sample}/{reference}/expression.csv" output: - pred = config['output_dir'] + "/{sample}/scNym/scNym_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/scNym/scNym_pred.csv" params: basedir = {workflow.basedir}, - threshold = 0.5 + threshold = config['scNym']['threshold'] log: - config['output_dir'] + "/{sample}/scNym/scNym.log" + config['output_dir'] + "/{sample}/{reference}/scNym/scNym.log" benchmark: - config['output_dir'] + "/{sample}/scNym/scNym_predict_benchmark.txt" + config['output_dir'] + "/{sample}/{reference}/scNym/scNym_predict_benchmark.txt" + threads: + config['scNym']['threads'] shell: """ python {params.basedir}/Scripts/scNym/run_scNym.py \ diff --git a/snakefile.benchmark b/snakefile.benchmark index d79d650..52c2489 100644 --- a/snakefile.benchmark +++ b/snakefile.benchmark @@ -1,3 +1,10 @@ + +#---------------------------------------------------- +# Setup +#---------------------------------------------------- + +configfile: workflow.basedir + "/Config/config.default.yml" + # import libraries import os from datetime import datetime @@ -7,11 +14,14 @@ n_folds = list(range(1, config['benchmark']['n_folds']+1)) now = datetime.now() dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") -""" -One rule that directs the DAG which is represented in the rulegraph -""" +#---------------------------------------------------- +# Final rule all +#---------------------------------------------------- + rule all: - input: report_path = config['output_dir'] + '/report/' + dt_string + '.benchmark_report.html' + input: + expand(config['output_dir_benchmark'] + "/{reference}/report/" + dt_string + '.benchmark_report.html', + reference = config['references'].keys()) #---------------------------------------------------- # Subset Folds @@ -19,17 +29,17 @@ rule all: rule subset_folds: input: - reference = config['training_reference'], - labfile = config['reference_annotations'] + reference = lambda wildcards:config['references'][wildcards.reference]['expression'], + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - training_data = expand(config['output_dir'] + "/fold{folds_index}/train.csv", folds_index = n_folds), - labfile = expand(config['output_dir'] + "/fold{folds_index}/train_labels.csv", folds_index = n_folds), - test_data = expand(config['output_dir'] + "/fold{folds_index}/test.csv", folds_index = n_folds) + training_data = expand(config['output_dir_benchmark'] + "/{{reference}}/fold{folds_index}/train.csv", folds_index = n_folds), + labfile = expand(config['output_dir_benchmark'] + "/{{reference}}/fold{folds_index}/train_labels.csv", folds_index = n_folds), + test_data = expand(config['output_dir_benchmark'] + "/{{reference}}/fold{folds_index}/test.csv", folds_index = n_folds) log: - config['output_dir'] + "/subset_folds.log" + config['output_dir_benchmark'] + "/{reference}/subset_folds.log" params: basedir = {workflow.basedir}, - outdir = config['output_dir'], + outdir = config['output_dir_benchmark'] + "/{reference}", n_folds = config['benchmark']['n_folds'] threads: 1 resources: @@ -50,28 +60,28 @@ rule subset_folds: rule knit_report: input: - pred = expand(config['output_dir'] + "/fold{folds_index}/{tool}/{tool}_pred.csv", + pred = expand(config['output_dir_benchmark'] + "/{{reference}}/fold{folds_index}/{tool}/{tool}_pred.csv", folds_index = n_folds, tool = config['tools_to_run']), - consensus = expand(config["output_dir"] + "/fold{folds_index}/Prediction_Summary.tsv", + consensus = expand(config['output_dir_benchmark'] + "/{{reference}}/fold{folds_index}/Prediction_Summary.tsv", folds_index = n_folds) output: - report_path = config['output_dir'] + '/report/' + dt_string + '.benchmark_report.html' + report_path = config['output_dir_benchmark'] + "/{reference}/report/" + dt_string + '.benchmark_report.html' log: - config['output_dir'] + "/report/report.log" + config['output_dir_benchmark'] + "/{reference}/report/report.log" params: basedir = {workflow.basedir}, - pred_path = config['output_dir'], + pred_path = config['output_dir_benchmark'] + "/{reference}", n_folds = config['benchmark']['n_folds'], - tools = config['tools_to_run'] - threads: 1 + tools = config['tools_to_run'], + ref_name = "{reference}" resources: shell: """ Rscript -e "rmarkdown::render( '{params.basedir}/Notebooks/benchmark_report.Rmd', params = list(tools = '{params.tools}', - ref_name = '', + ref_name = '{params.ref_name}', pred_path = '{params.pred_path}', fold = '{params.n_folds}'), output_file = '{output.report_path}')" \ @@ -84,22 +94,22 @@ rule knit_report: rule consensus: input: - results = expand(config["output_dir"] + "/fold{{folds_index}}/{tool}/{tool}_pred.csv", - tool=config['tools_to_run']), - sample = config["output_dir"] + "/fold{folds_index}" + results = expand(config['output_dir_benchmark'] + "/{{reference}}/fold{{folds_index}}/{tool}/{tool}_pred.csv", + tool=config['tools_to_run']) output: - prediction_summary = config["output_dir"] + "/fold{folds_index}/Prediction_Summary.tsv" + prediction_summary = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Prediction_Summary.tsv" log: - config["output_dir"] + "/fold{folds_index}/Gatherpreds.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Gatherpreds.log" params: basedir = {workflow.basedir}, tools = config['tools_to_run'], conesnsus_tools = config['consensus_tools'], - labfile = config['reference_annotations'] + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'], + sample = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}" shell: """ Rscript {params.basedir}/Scripts/calculate_consensus.R \ - {input.sample} \ + {params.sample} \ {output.prediction_summary} \ "{params.tools}" \ "{params.conesnsus_tools}" \ @@ -113,18 +123,19 @@ rule consensus: rule train_scPred: input: - reference = config['output_dir'] + "/fold{fold}/train.csv", - labfile = config['output_dir'] + "/fold{fold}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{fold}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{fold}/train_labels.csv" output: - model = config['output_dir'] + "/fold{fold}/scPred/scPred_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{fold}/scPred/scPred_model.Rda" params: basedir = {workflow.basedir}, - scPred_model = "svmRadial" + classifier = config['scPred']['classifier'] log: - config['output_dir'] + "/fold{fold}/scPred/scPred.log" + config['output_dir_benchmark'] + "/{reference}/fold{fold}/scPred/scPred.log" benchmark: - config['output_dir'] + "/fold{fold}/scPred/scPred_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{fold}/scPred/scPred_train_benchmark.txt" + threads: + config['scPred']['threads'] resources: shell: """ @@ -133,23 +144,24 @@ rule train_scPred: {input.labfile} \ {output.model} \ {threads} \ - {params.scPred_model} \ + {params.classifier} \ &> {log} """ rule predict_scPred: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/scPred/scPred_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scPred/scPred_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/scPred/scPred_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scPred/scPred_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/scPred/scPred.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scPred/scPred.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scPred/scPred_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scPred/scPred_predict_benchmark.txt" + threads: + config['scPred']['threads'] resources: shell: """ @@ -162,22 +174,23 @@ rule predict_scPred: """ #---------------------------------------------------- -# SingelR +# SingleR #---------------------------------------------------- rule train_SingleR: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SingleR/SingleR_model.Rda" params: basedir = {workflow.basedir}, log: - config['output_dir'] + "/fold{folds_index}/SingleR/SingleR.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SingleR/SingleR.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SingleR/SingleR_train_benchmark.txt" + threads: + config['SingleR']['threads'] resources: shell: """ @@ -191,17 +204,18 @@ rule train_SingleR: rule predict_SingleR: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SingleR/SingleR_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SingleR/SingleR_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/SingleR/SingleR.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SingleR/SingleR.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SingleR/SingleR_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SingleR/SingleR_predict_benchmark.txt" + threads: + config['SingleR']['threads'] resources: shell: """ @@ -219,17 +233,18 @@ rule predict_SingleR: rule train_scClassify: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scClassify/scClassify_model.Rda" params: basedir = {workflow.basedir}, log: - config['output_dir'] + "/fold{folds_index}/scClassify/scClassify.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scClassify/scClassify.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scClassify/scClassify_train_benchmark.txt" + threads: + config['scClassify']['threads'] resources: shell: """ @@ -243,17 +258,18 @@ rule train_scClassify: rule predict_scClassify: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scClassify/scClassify_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scClassify/scClassify_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/scClassify/scClassify.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scClassify/scClassify.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scClassify/scClassify_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scClassify/scClassify_predict_benchmark.txt" + threads: + config['scClassify']['threads'] resources: shell: """ @@ -271,17 +287,18 @@ rule predict_scClassify: rule train_SciBet: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SciBet/SciBet_model.Rda" params: basedir = {workflow.basedir}, log: - config['output_dir'] + "/fold{folds_index}/SciBet/SciBet.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SciBet/SciBet.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SciBet/SciBet_train_benchmark.txt" + threads: + config['SciBet']['threads'] resources: shell: """ @@ -295,17 +312,18 @@ rule train_SciBet: rule predict_SciBet: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SciBet/SciBet_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SciBet/SciBet_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/SciBet/SciBet.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SciBet/SciBet.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SciBet/SciBet_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SciBet/SciBet_predict_benchmark.txt" + threads: + config['SciBet']['threads'] resources: shell: """ @@ -323,17 +341,18 @@ rule predict_SciBet: rule train_SVMlinear: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVMlinear/SVMlinear_model.Rda" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVMlinear/SVMlinear.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVMlinear/SVMlinear_train_benchmark.txt" + threads: + config['SVMlinear']['threads'] resources: shell: """ @@ -347,19 +366,20 @@ rule train_SVMlinear: rule predict_SVMlinear: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVMlinear/SVMlinear_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVMlinear/SVMlinear_pred.csv" params: basedir = {workflow.basedir}, - threshold = 0.5, - tool_name = 'SVMlinear' + threshold = config['SVMlinear']['threshold'], + tool_name = config['SVMlinear']['classifier'] log: - config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVMlinear/SVMlinear.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SVMlinear/SVMlinear_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVMlinear/SVMlinear_predict_benchmark.txt" + threads: + config['SVMlinear']['threads'] resources: shell: """ @@ -379,18 +399,18 @@ rule predict_SVMlinear: rule train_SVC: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/SVC/SVC_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_model.Rda" params: - basedir = {workflow.basedir}, - classifier = 'rbf' + basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/SVC/SVC.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SVC/SVC_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_train_benchmark.txt" + threads: + config['SVC']['threads'] resources: shell: """ @@ -405,19 +425,20 @@ rule train_SVC: rule predict_SVC: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/SVC/SVC_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/SVC/SVC_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_pred.csv" params: basedir = {workflow.basedir}, - threshold = 0.5, - tool_name = 'rbf' + threshold = config['SVC']['threshold'], + tool_name = config['SVC']['classifier'] log: - config['output_dir'] + "/fold{folds_index}/SVC/SVC.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC.log" benchmark: - config['output_dir'] + "/fold{folds_index}/SVC/SVC_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_predict_benchmark.txt" + threads: + config['SVC']['threads'] resources: shell: """ @@ -437,17 +458,18 @@ rule predict_SVC: rule train_singleCellNet: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/singleCellNet/singleCellNet_model.Rda" params: basedir = {workflow.basedir}, log: - config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/singleCellNet/singleCellNet.log" benchmark: - config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/singleCellNet/singleCellNet_train_benchmark.txt" + threads: + config['singleCellNet']['threads'] resources: shell: """ @@ -461,17 +483,18 @@ rule train_singleCellNet: rule predict_singleCellNet: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/singleCellNet/singleCellNet_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/singleCellNet/singleCellNet_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/singleCellNet/singleCellNet.log" benchmark: - config['output_dir'] + "/fold{folds_index}/singleCellNet/singleCellNet_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/singleCellNet/singleCellNet_predict_benchmark.txt" + threads: + config['singleCellNet']['threads'] resources: shell: """ @@ -489,17 +512,18 @@ rule predict_singleCellNet: rule train_Correlation: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Correlation/Correlation_model.Rda" params: basedir = {workflow.basedir}, log: - config['output_dir'] + "/fold{folds_index}/Correlation/Correlation.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Correlation/Correlation.log" benchmark: - config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Correlation/Correlation_train_benchmark.txt" + threads: + config['Correlation']['threads'] resources: shell: """ @@ -512,17 +536,18 @@ rule train_Correlation: """ rule predict_Correlation: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Correlation/Correlation_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Correlation/Correlation_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/Correlation/Correlation.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Correlation/Correlation.log" benchmark: - config['output_dir'] + "/fold{folds_index}/Correlation/Correlation_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/Correlation/Correlation_predict_benchmark.txt" + threads: + config['Correlation']['threads'] resources: shell: """ @@ -540,17 +565,18 @@ rule predict_Correlation: rule train_scLearn: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scLearn/scLearn_model.Rda" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/scLearn/scLearn.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scLearn/scLearn.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scLearn/scLearn_train_benchmark.txt" + threads: + config['scLearn']['threads'] resources: shell: """ @@ -564,17 +590,18 @@ rule train_scLearn: rule predict_scLearn: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scLearn/scLearn_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scLearn/scLearn_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/scLearn/scLearn.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scLearn/scLearn.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scLearn/scLearn_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scLearn/scLearn_predict_benchmark.txt" + threads: + config['scLearn']['threads'] resources: shell: """ @@ -592,19 +619,20 @@ rule predict_scLearn: rule train_scHPL: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scHPL/scHPL_model.Rda" params: basedir = {workflow.basedir}, - classifier = 'svm', - dimred = 'False' + classifier = config['scHPL']['classifier'], + dimred = config['scHPL']['dimred'] log: - config['output_dir'] + "/fold{folds_index}/scHPL/scHPL.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scHPL/scHPL.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_train_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scHPL/scHPL_train_benchmark.txt" + threads: + config['scHPL']['threads'] resources: shell: """ @@ -619,18 +647,19 @@ rule train_scHPL: rule predict_scHPL: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_model.Rda" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scHPL/scHPL_model.Rda" output: - pred = config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scHPL/scHPL_pred.csv" params: - basedir = {workflow.basedir}, - threshold = 0.5 + basedir = {workflow.basedir}, + threshold = config['scHPL']['threshold'] log: - config['output_dir'] + "/fold{folds_index}/scHPL/scHPL.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scHPL/scHPL.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scHPL/scHPL_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scHPL/scHPL_predict_benchmark.txt" + threads: + config['scHPL']['threads'] resources: shell: """ @@ -648,16 +677,18 @@ rule predict_scHPL: rule train_ACTINN: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_model.pkl" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/ACTINN/ACTINN_model.pkl" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/ACTINN/ACTINN.log" benchmark: - config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_predict_benchmark.txt" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/ACTINN/ACTINN_predict_benchmark.txt" + threads: + config['ACTINN']['threads'] shell: """ python {params.basedir}/Scripts/ACTINN/train_ACTINN.py \ @@ -669,17 +700,18 @@ rule train_ACTINN: rule predict_ACTINN: input: - query = config['output_dir'] + "/fold{folds_index}/test.csv", - model = config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_model.pkl" + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/ACTINN/ACTINN_model.pkl" output: - pred = config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/ACTINN/ACTINN_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/ACTINN/ACTINN.log" benchmark: - config['output_dir'] + "/fold{folds_index}/ACTINN/ACTINN_predict_benchmark.txt" - threads: 1 + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/ACTINN/ACTINN_predict_benchmark.txt" + threads: + config['ACTINN']['threads'] resources: shell: """ @@ -697,17 +729,19 @@ rule predict_ACTINN: rule run_scID: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv", - query = config['output_dir'] + "/fold{folds_index}/test.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv", + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv" output: - pred = config['output_dir'] + "/fold{folds_index}/scID/scID_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scID/scID_pred.csv" params: basedir = {workflow.basedir} log: - config['output_dir'] + "/fold{folds_index}/scID/scID.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scID/scID.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scID/scID_predict_benchmark.txt" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scID/scID_benchmark.txt" + threads: + config['scID']['threads'] shell: """ Rscript {params.basedir}/Scripts/scID/run_scID.R \ @@ -725,18 +759,20 @@ rule run_scID: rule run_scAnnotate: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv", - query = config['output_dir'] + "/fold{folds_index}/test.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv", + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv" output: - pred = config['output_dir'] + "/fold{folds_index}/scAnnotate/scAnnotate_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scAnnotate/scAnnotate_pred.csv" params: basedir = {workflow.basedir}, threshold = 0.5 log: - config['output_dir'] + "/fold{folds_index}/scAnnotate/scAnnotate.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scAnnotate/scAnnotate.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scAnnotate/scAnnotate_predict_benchmark.txt" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scAnnotate/scAnnotate_benchmark.txt" + threads: + config['scAnnotate']['threads'] shell: """ Rscript {params.basedir}/Scripts/scAnnotate/run_scAnnotate.R \ @@ -755,18 +791,20 @@ rule run_scAnnotate: rule run_scNym: input: - reference = config['output_dir'] + "/fold{folds_index}/train.csv", - labfile = config['output_dir'] + "/fold{folds_index}/train_labels.csv", - query = config['output_dir'] + "/fold{folds_index}/test.csv" + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv", + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv" output: - pred = config['output_dir'] + "/fold{folds_index}/scNym/scNym_pred.csv" + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scNym/scNym_pred.csv" params: basedir = {workflow.basedir}, threshold = 0.5 log: - config['output_dir'] + "/fold{folds_index}/scNym/scNym.log" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scNym/scNym.log" benchmark: - config['output_dir'] + "/fold{folds_index}/scNym/scNym_predict_benchmark.txt" + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/scNym/scNym_benchmark.txt" + threads: + config['scNym']['threads'] shell: """ python {params.basedir}/Scripts/scNym/run_scNym.py \ From 0fe792bd5c92d8b6f1c7dfda03a926ad301f09fe Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:17:57 -0400 Subject: [PATCH 072/144] rules for celltypeist in both snakefiles, fixed typos in celltypist code (#60) --- Config/config.default.yml | 6 ++ Scripts/CellTypist/predict_CellTypist.py | 10 +-- Scripts/CellTypist/train_CellTypist.py | 6 +- snakefile.annotate | 78 ++++++++++++++++++++---- snakefile.benchmark | 64 ++++++++++++++++++- 5 files changed, 143 insertions(+), 21 deletions(-) diff --git a/Config/config.default.yml b/Config/config.default.yml index 0496ed1..788747d 100644 --- a/Config/config.default.yml +++ b/Config/config.default.yml @@ -50,3 +50,9 @@ scAnnotate: scNym: threads: 1 threshold: 0.5 + +CellTypist: + threads: 1 + feature_selection: 'True' + majority_voting: 'True' + threshold: 0.5 diff --git a/Scripts/CellTypist/predict_CellTypist.py b/Scripts/CellTypist/predict_CellTypist.py index 410ae1e..5a1ec89 100644 --- a/Scripts/CellTypist/predict_CellTypist.py +++ b/Scripts/CellTypist/predict_CellTypist.py @@ -45,18 +45,18 @@ # load model -print('@ LOAD MODEL')) -CellTypist_model = pickle.load(open(model_path, 'rb')) +print('@ LOAD MODEL') +model = pickle.load(open(model_path, 'rb')) print('@ DONE') ### If the trained model was generated with a diff number of threads and the ## prediction is done with other number -CellTypist_model.classifier.n_jobs = threads +model["Model"].n_jobs = threads #------------- Predict CellTypist ------------- predictions = celltypist.annotate(query, - model = CellTypist_model, + model = model_path, majority_voting = majority_voting, mode = 'prob match', p_thres = threshold) @@ -80,7 +80,7 @@ print('@ DONE') ## Save the output plots print('@ GENERATING OUTPUT PLOTS') -prediction.to_plot(out_other_path) +predictions.to_plots(out_other_path) print('@ DONE') ### Save the prob matrix diff --git a/Scripts/CellTypist/train_CellTypist.py b/Scripts/CellTypist/train_CellTypist.py index 0e22a21..8474c74 100644 --- a/Scripts/CellTypist/train_CellTypist.py +++ b/Scripts/CellTypist/train_CellTypist.py @@ -63,15 +63,15 @@ #Save the model to disk print('@ SAVE MODEL') -new_model.write(out_path) +model.write(out_path) print('@ DONE') #------------- Other outputs -------------- #We can extract the top markers, I get the top 10 for each cell-type applying the # function extract_top_markers dataframes = [] -for cell_type in CellTypist_model.cell_types: - top_markers = CellTypist_model.extract_top_markers(cell_type, 10) +for cell_type in model.cell_types: + top_markers = model.extract_top_markers(cell_type, 10) # Create a DataFrame for the current cell type's top markers df = pd.DataFrame(top_markers, columns=['Marker']) diff --git a/snakefile.annotate b/snakefile.annotate index 90f9f29..8b16644 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -21,9 +21,6 @@ dt_string = now.strftime("%Y-%m-%d_%H-%M-%S") rule all: input: - expand(config["output_dir"] + "/{sample}/{reference}/Prediction_Summary.tsv", - sample = samples, - reference = config['references']), expand(config['output_dir'] + '/{sample}/report/{sample}.prediction_report.' + dt_string + '.html', sample = samples) @@ -467,14 +464,14 @@ rule train_SVC: reference = config['output_dir'] + "/model/{reference}/expression.csv", labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] output: - model = config['output_dir'] + "/model/{reference}/SVCrbf/SVCrbf_model.Rda" + model = config['output_dir'] + "/model/{reference}/SVC" + config['SVC']['classifier'] + "/SVC" + config['SVC']['classifier'] + "_model.pkl" params: basedir = {workflow.basedir}, classifier = config['SVC']['classifier'] log: - config['output_dir'] + "/model/{reference}/SVCrbf/SVCrbf.log" + config['output_dir'] + "/model/{reference}/SVC" + config['SVC']['classifier'] + "/SVC" + config['SVC']['classifier'] + ".log" benchmark: - config['output_dir'] + "/model/{reference}/SVCrbf/SVCrbf_train_benchmark.txt" + config['output_dir'] + "/model/{reference}/SVC" + config['SVC']['classifier'] + "/SVC" + config['SVC']['classifier'] + "_train_benchmark.txt" threads: config['SVC']['threads'] resources: @@ -492,17 +489,17 @@ rule train_SVC: rule predict_SVC: input: query = config['output_dir'] + "/{sample}/{reference}/expression.csv", - model = config['output_dir'] + "/{reference}/SVCrbf/SVCrbf_model.Rda" + model = config['output_dir'] + "/model/{reference}/SVC" + config['SVC']['classifier'] + "/SVC" + config['SVC']['classifier'] + "_model.pkl" output: - pred = config['output_dir'] + "/{sample}/{reference}/SVCrbf/SVCrbf_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/SVC" + config['SVC']['classifier'] + "/SVC" + config['SVC']['classifier'] + "_pred.csv" params: basedir = {workflow.basedir}, threshold = config['SVC']['threshold'], tool_name = config['SVC']['classifier'] log: - config['output_dir'] + "/{sample}/{reference}/SVCrbf/SVCrbf.log" + config['output_dir'] + "/{sample}/{reference}/SVC" + config['SVC']['classifier'] + "/SVC" + config['SVC']['classifier'] + ".log" benchmark: - config['output_dir'] + "/{sample}/{reference}/SVCrbf/SVCrbf_predict_benchmark.txt" + config['output_dir'] + "/{sample}/{reference}/SVC" + config['SVC']['classifier'] + "/SVC" + config['SVC']['classifier'] + "_predict_benchmark.txt" threads: config['SVC']['threads'] resources: @@ -729,7 +726,6 @@ rule predict_ACTINN: -mp {input.model} \ -pp {output.pred} \ &> {log} - """ #---------------------------------------------------- @@ -825,6 +821,66 @@ rule run_scNym: &> {log} """ +#---------------------------------------------------- +# CellTypist +#---------------------------------------------------- + +rule train_CellTypist: + input: + reference = config['output_dir'] + "/model/{reference}/expression.csv", + labfile = lambda wildcards:config['references'][wildcards.reference]['labels'] + output: + model = config['output_dir'] + "/model/{reference}/CellTypist/CellTypist_model.pkl" + params: + basedir = {workflow.basedir}, + feature_selection = config['CellTypist']['feature_selection'] + log: + config['output_dir'] + "/model/{reference}/CellTypist/CellTypist.log" + benchmark: + config['output_dir'] + "/model/{reference}/CellTypist/CellTypist_train_benchmark.txt" + threads: + config['CellTypist']['threads'] + resources: + shell: + """ + python {params.basedir}/Scripts/CellTypist/train_CellTypist.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + {params.feature_selection} \ + &> {log} + """ + +rule predict_CellTypist: + input: + query = config['output_dir'] + "/{sample}/{reference}/expression.csv", + model = config['output_dir'] + "/model/{reference}/CellTypist/CellTypist_model.pkl" + output: + pred = config['output_dir'] + "/{sample}/{reference}/CellTypist/CellTypist_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = config['CellTypist']['threshold'], + majority_voting = config['CellTypist']['majority_voting'] + log: + config['output_dir'] + "/{sample}/{reference}/CellTypist/CellTypist.log" + benchmark: + config['output_dir'] + "/{sample}/{reference}/CellTypist/CellTypist_predict_benchmark.txt" + threads: + config['CellTypist']['threads'] + resources: + shell: + """ + python {params.basedir}/Scripts/CellTypist/predict_CellTypist.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + {threads} \ + {params.majority_voting} \ + &> {log} + """ + #---------------------------------------------------- # The End #---------------------------------------------------- diff --git a/snakefile.benchmark b/snakefile.benchmark index 52c2489..8144e24 100644 --- a/snakefile.benchmark +++ b/snakefile.benchmark @@ -402,7 +402,7 @@ rule train_SVC: reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" output: - model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_model.pkl" params: basedir = {workflow.basedir} log: @@ -426,7 +426,7 @@ rule train_SVC: rule predict_SVC: input: query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", - model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_model.Rda" + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_model.pkl" output: pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/SVC/SVC_pred.csv" params: @@ -816,6 +816,66 @@ rule run_scNym: &> {log} """ +#---------------------------------------------------- +# CellTypist +#---------------------------------------------------- + +rule train_CellTypist: + input: + reference = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train.csv", + labfile = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/train_labels.csv" + output: + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/CellTypist/CellTypist_model.pkl" + params: + basedir = {workflow.basedir}, + feature_selection = config['CellTypist']['feature_selection'] + log: + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/CellTypist/CellTypist.log" + benchmark: + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/CellTypist/CellTypist_train_benchmark.txt" + threads: + config['CellTypist']['threads'] + resources: + shell: + """ + python {params.basedir}/Scripts/CellTypist/train_CellTypist.py \ + {input.reference} \ + {input.labfile} \ + {output.model} \ + {threads} \ + {params.feature_selection} \ + &> {log} + """ + +rule predict_CellTypist: + input: + query = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/test.csv", + model = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/CellTypist/CellTypist_model.pkl" + output: + pred = config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/CellTypist/CellTypist_pred.csv" + params: + basedir = {workflow.basedir}, + threshold = config['CellTypist']['threshold'], + majority_voting = config['CellTypist']['majority_voting'] + log: + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/CellTypist/CellTypist.log" + benchmark: + config['output_dir_benchmark'] + "/{reference}/fold{folds_index}/CellTypist/CellTypist_predict_benchmark.txt" + threads: + config['CellTypist']['threads'] + resources: + shell: + """ + python {params.basedir}/Scripts/CellTypist/predict_CellTypist.py \ + {input.query} \ + {input.model} \ + {output.pred} \ + {params.threshold} \ + {threads} \ + {params.majority_voting} \ + &> {log} + """ + #---------------------------------------------------- # The End #---------------------------------------------------- From c3fb773a6adf6f480b87a48df7d6fc91151352e1 Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Thu, 31 Aug 2023 15:24:08 -0400 Subject: [PATCH 073/144] Update README.md #59 --- README.md | 236 +++++++++++++++++++++++++++++------------------------- 1 file changed, 125 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 6bc04e2..bed4012 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,17 @@ # Summary -Reference based prediction of cell-types using a fast and efficient Snakemake pipeline to increase automation and reduce the need to run several scripts and experiments. The pipeline allows the user to select what single-cell annotation tools they want to run on a selected reference to annotate a list of query datasets. It then outputs a consensus of the predictions across tools selected. This pipeline trains classifiers on genes common to the reference and all query datasets. +Snakemake pipeline for consensus prediction of cell types in single-cell RNA sequencing (scRNA-seq) data. The pipeline allows users to run up to 15 different reference-based annotation tools (statistical models and machine learning approaches) to predict cell type labels of multiple scRNA-seq samples. It then outputs a consensus of the predictions, which has been found to have increased accuracy in benchmarking experiments compared to the individual predictions alone, by combining the strengths of the different approaches. +The pipeline is automated and running it does not require prior knowledge of machine learning. It also features parallelization options to exploit available computational resources for maximal efficiency. This pipeline trains classifiers on genes common to the reference and all query datasets. # Installation and Dependencies -Tested with [R](https://www.r-project.org/) Version 4.2.2 and Python 3.11.2. +This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. -**R packages CRAN** +It has been tested with [R](https://www.r-project.org/) version 4.2.2 and Python version 3.11.2. + +## R packages - CRAN ```R pkg = c("Seurat", @@ -29,7 +32,7 @@ Older version of Matrix package needs to be installed for Seurat to work: https: devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r-project.org") ``` -**R packages bioconductor** +## R packages - Bioconductor ```R pkg = c("SingleCellExperiment", @@ -49,7 +52,7 @@ pkg = c("SingleCellExperiment", BiocManager::install(pkg) ``` -**R packages github** +## R packages - Github ```R pkg = c("pcahan1/singleCellNet", @@ -61,7 +64,7 @@ pkg = c("pcahan1/singleCellNet", devtools::install_github(pkg) ``` -**Python modules** +## Python modules ```bash pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensorflow tables snakemake @@ -69,17 +72,17 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor # Quickstart -1. Clone repository and install dependencies -2. Prepare reference -3. Prepare query samples -4. Prepare config file -5. Prepare submission script (HPC) +1. [Clone repository and install dependencies](#clone-repository-and-install-dependencies) +2. [Prepare reference](#prepare-reference) +3. [Prepare query samples](#prepare-query-samples) +4. [Prepare config file](#prepare-config-file) +5. [Prepare HPC submission script](#prepare-hpc-submission-script) ### 1. Clone repository and install dependencies ### 2. Prepare reference -### 3. Prepare query +### 3. Prepare query samples ### 4. Prepare config file @@ -87,7 +90,7 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor # UPDATE ``` -## 5. Prepare submission script +## 5. Prepare HPC submission script #### Annotate @@ -101,11 +104,11 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor # UPDATE ``` -# Tools Available +# Available tools -**Single cell RNA reference + single cell RNA query** +## Single cell RNA reference + single cell RNA query -```ymal +```yaml - scPred - SingleR - scClassify @@ -121,7 +124,7 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor - scAnnotate ``` -**Single cell RNA reference + spatial RNA query** +## Single cell RNA reference + spatial RNA query ```yaml - Tangram ``` @@ -130,15 +133,15 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM -# Adding New Tools: +# Adding new tools: ``` # UPDATE ``` -# Tips and Tricks +# Snakemake Tips and Tricks -- Dryrun before submitting job +- Dryrun snakemake pipeline before submitting job ```bash snakemake -s ${snakefile} --configfile ${config} -n ``` @@ -153,7 +156,7 @@ snakemake -s ${snakefile} --configfile ${config} --unlock snakemake -s ${snakefile} --configfile ${config} --rerun-incomplete ``` -- Update time stamp on files to avoid reruning rules if code has changed +- Update time stamp on files to avoid rerunning rules if code has changed ```bash snakemake -s ${snakefile} --configfile ${config} -c1 -R $(snakemake -s ${snakefile} --configfile ${config} -c1 --list-code-changes) --touch ``` @@ -163,11 +166,15 @@ snakemake -s ${snakefile} --configfile ${config} -c1 -R $(snakemake -s ${snakefi snakemake -s ${snakefile} --configfile ${config} --report ${report} ``` -# Tools +# Detailed documentation on tool wrapper scripts ## scClassify -Detailed documentation for scClassify train and predict scripts, written July 2023 by Bhavyaa Chandarana +Documentation written by: Bhavyaa Chandarana +Date written: 2023-07 + +scClassify workflow was generated using the tutorial below: +https://www.bioconductor.org/packages/release/bioc/vignettes/scClassify/inst/doc/scClassify.html * scCoAnnotate input reference and query have cells as the rows, genes as columns. scClassify (and the Seurat function used for normalization, see below) requires genes on the rows and cells on the columns. Therefore, I used `WGCNA::transposeBigData()` (function optimized for large sparse matrices) to transpose the inputs before normalization and training/prediction. @@ -180,51 +187,52 @@ Detailed documentation for scClassify train and predict scripts, written July 20 * `scClassify::plotCellTypeTree()` produces a ggplot object. Therefore, I am using `ggplot2::ggsave()` to save it as a png file. (Function documentation [source](https://www.bioconductor.org/packages/release/bioc/manuals/scClassify/man/scClassify.pdf)) ## scPred -Documentation written by: Alva Annett -Date written: July 2023 -Both reference and query is normaluzed using `Seurat::NormalizeData()`. -Needs computed PCA space. Dims set to 1:30 according to tutorial. -Default model `SVMradial`. Option to switch model should be set up in snakemake. +Documentation written by: Alva Annett +Date written: 2023-07 Normalization and parameters based on this tutorial: https://powellgenomicslab.github.io/scPred/articles/introduction.html -## SingleR -Documentation written by: Alva Annett -Date written: July 2023 +* Both reference and query is normalized using `Seurat::NormalizeData()`. +Needs computed PCA space. Dims set to 1:30 according to tutorial. + +* Default model `SVMradial`. Option to switch model should be set up in snakemake. -Both reference and query is normaluzed using `scuttle::logNormCounts()`. Both reference and query is converted to SingleCellExperiment objects before normalization. +## SingleR -Deviation from default parameters: -* `de.method = de.method="wilcox"` -Method for generating marker genes for each class in reference. Wilcox is recomended when single cell data is used as reference +Documentation written by: Alva Annett +Date written: 2023-07 Normalization and parameters based on this tutorial: http://www.bioconductor.org/packages/devel/bioc/vignettes/SingleR/inst/doc/SingleR.html#3_Using_single-cell_references +* Both reference and query is normalized using `scuttle::logNormCounts()`. Both reference and query is converted to SingleCellExperiment objects before normalization. + +* Deviation from default parameters: `de.method = de.method="wilcox"` Method for generating marker genes for each class in reference. Wilcox is recomended when single cell data is used as reference + ## singleCellNet Documentation written by: Rodrigo Lopez Gutierrez Date written: 2023-08-01 +singleCellNet workflow was generated following the tutorial below: +https://pcahan1.github.io/singleCellNet/ + Input for `singleCellNet` is raw counts for both reference and query. The reference is normalized within the `scn_train()` function. The query is currently not normalized. In the tutorial example they used raw query data. Furthermore, according to the tutorial, the classification step is robust to the normalization and transformation steps of the query data sets. They claim that one can even directly use raw data to query and still obtains accurate classification. This could be tested in the future with our data to see if normalized queries perform better. Normal parameters were used in both the training and prediction functions, with the expection of the following parameters: * In `scn_train()`, we used parameter `nTrees = 500` compared to the default `nTrees = 1000`. This parameter changes the number of trees for the random forest classifier. The value selected is based on Hussein's thesis and is changed to improve the speed of `singleCellNet`. It is mentioned that additional training parameters may need to be adjusted depending on the quality of the reference data. Additionally, tutorial mentions that classifier performance may increase if the values for `nTopGenes` and `nTopGenePairs` are increased. * In `scn_predict()`, we used parameter `nrand = 0` compared to the default `nrand = 50`. This parameter changes the number of randomized single cell RNA-seq profiles which serve as positive controls that should be mostly classified as `rand` (unknown) category. If left at default value, then this would generate extra cells that might complicate downstream consolidation of the consensus predictions for each cell. Therefore, the selected value is used to avoid complication. -singleCellNet workflow was generated following the tutorial below: -https://pcahan1.github.io/singleCellNet/ - ## Correlation Documentation written by: Rodrigo Lopez Gutierrez Date written: 2023-08-02 The Correlation tool runs a correlation-based cell type prediction on a sample of interest, given the mean gene expression per label for a reference. -The function to label by Spearman correlation was originally generated by Selin Jessa and Marie Coutlier -Path to original file: `/lustre06/project/6004736/sjessa/from_narval/HGG-oncohistones/stable/code/scripts/predict_celltype_cor.R` +The function to label by Spearman correlation was originally generated by Selin Jessa and Marie Coutelier. +Path to original file on Narval compute cluster: `/lustre06/project/6004736/sjessa/from_narval/HGG-oncohistones/stable/code/scripts/predict_celltype_cor.R` Input for `Correlation` is raw counts for both reference and query. Both the reference and the query are normalized using `Seurat::NormalizeData()`. @@ -236,25 +244,28 @@ Currently only outputting a table with each cell, the most highly correlated lab ## scLearn -Detailed documentation for scLearn train and predict scripts, written August 2023 by Bhavyaa Chandarana. -Added information Tomas Vega Waichman in 2023-08-04. +Documentation written by: Bhavyaa Chandarana, updated by Tomas Vega Waichman +Date written: 2023-08-04 -Preprocessing performed in the same way as scLearn documentation tutorial [source](https://github.com/bm2-lab/scLearn#tutorial) for `Single-label single cell assignment` +scLearn workflow was generated using the following tutorial: https://github.com/bm2-lab/scLearn#single-label-single-cell-assignment * scCoAnnotate input reference and query have cells as the rows, genes as columns. scLearn requires genes on the rows and cells on the columns. Therefore, I used `WGCNA::transposeBigData()` (function optimized for large sparse matrices) to transpose the inputs before normalization and training/prediction. -* In order to avoid cell filtering, the reference and query matrix were normalized using Seurat::NormalizeData since the authors make the logNormalization in this way but manually (using a scale.factor = 10000 and then log(ref + 1)). Because of this the arguments of 'species' is not used and this allows to use this methods in order species different to the Human and Mouse. +* In order to avoid cell filtering, the reference and query matrix were normalized using Seurat::NormalizeData. The authors original log normalized in this way in this way but with a custom function (using a scale.factor = 10000 and then log(ref + 1)). Because of this, the scLearn function argument for `species` is not used. This allows us to use this method with species other than human or mouse (only two arguments accepted) * Used default value `10` for argument `bootstrap_times` in training function. According to tool documentation, this can be increased to improve accuracy for unassigned cells(?) but increase train time. * Default parameters were used for tool prediction -* Added some outputs, for prediction added a table with the selected genes for the model. In prediction added and output with the whole data.frame with the probabilities for each cell. +* Added some outputs: for training, added a table with the genes selected for the model. For prediction, added an output with the whole data frame containing the probabilities for each cell. ## singleCellNet Documentation written by: Rodrigo Lopez Gutierrez -Date written: 2023-08-01 +Date written: 2023-08-01 + +singleCellNet workflow was generated following the tutorial below: +https://pcahan1.github.io/singleCellNet/ Input for `singleCellNet` is raw counts for both reference and query. The reference is normalized within the `scn_train()` function. The query is currently not normalized. In the tutorial example they used raw query data. Furthermore, according to the tutorial, the classification step is robust to the normalization and transformation steps of the query data sets. They claim that one can even directly use raw data to query and still obtains accurate classification. This could be tested in the future with our data to see if normalized queries perform better. @@ -262,116 +273,119 @@ Normal parameters were used in both the training and prediction functions, with * In `scn_train()`, we used parameter `nTrees = 500` compared to the default `nTrees = 1000`. This parameter changes the number of trees for the random forest classifier. The value selected is based on Hussein's thesis and is changed to improve the speed of `singleCellNet`. It is mentioned that additional training parameters may need to be adjusted depending on the quality of the reference data. Additionally, tutorial mentions that classifier performance may increase if the values for `nTopGenes` and `nTopGenePairs` are increased. * In `scn_predict()`, we used parameter `nrand = 0` compared to the default `nrand = 50`. This parameter changes the number of randomized single cell RNA-seq profiles which serve as positive controls that should be mostly classified as `rand` (unknown) category. If left at default value, then this would generate extra cells that might complicate downstream consolidation of the consensus predictions for each cell. Therefore, the selected value is used to avoid complication. -singleCellNet workflow was generated following the tutorial below: -https://pcahan1.github.io/singleCellNet/ - ## ACTINN Documentation written by: Alva Annett Date written: 2023-08-08 -ACTINN code based on actinn_format.py and actinn_predict.py originally found here: https://github.com/mafeiyang/ACTINN +ACTINN code is based on `actinn_format.py` and `actinn_predict.py` originally found here: https://github.com/mafeiyang/ACTINN -ACTINN has been spit into testing and predicting. To do this, filtering of outlier genes based on expression across all query samples and reference had to be removed. The rest of the code has not been changed from the original ACTINN implementation, just rearanged and some parts related to processing multiple samples at the same time removed. +* ACTINN has been split into testing and predicting. To do this, filtering of outlier genes based on expression across all query samples and reference had to be removed. The rest of the code has not been changed from the original ACTINN implementation, other than rearrangements and removal of some parts related to processing multiple samples at the same time. -ACTINN is run with default parameters from original implementation. -Normalization is based on original implementation and paper (cells scaled to total expression value, times 10 000, log2(x+1) normalized) +* ACTINN is run with default parameters from original implementation. Normalization is based on original implementation and paper (cells scaled to total expression value, times 10 000, log2(x+1) normalized) ## Tangram Documentation written by: Tomas Vega Waichman Date written: 2023-08-08 -Tangram maps a single cell that is used as a reference to a spatial dataset. It cannot be separated into training and test sets. +The Tangram workflow was generated following the tutorial provided below: +https://tangram-sc.readthedocs.io/en/latest/tutorial_sq_link.html + +Tangram maps cells of a single cell reference to a spatial dataset. It cannot be separated into training and test steps. It is necessary to explore whether parallelization is possible. -* The spatial dataset needs to be in a .h5ad format with the .X matrix normalized and log-transformed. + +* The spatial dataset needs to be in a `.h5ad` format with the `.X` matrix normalized and log-transformed. * The mode could be set to `cells` if you want to map cells to spots, and the output matrix will be cell x spot probabilities. Alternatively, set it to `clusters` if the goal is to map whole clusters to the spatial data. -* The output is the highest scored cell type for each spot, determined by the cell type projection (using the `tg.project_cell_annotations` function from the Tangram package). -* Other outputs include: a score matrix for spot vs label, a cell x spot probability matrix, and the Tangram output map object in .h5ad format containing all the relevant information. -* It runs using the whole transcriptome, none gene markers are selected. +* The output is the highest scoring cell type for each spot, determined by the cell type projection (using the `tg.project_cell_annotations` function from the Tangram package). +* Other outputs include: a score matrix for spot vs label, a cell x spot probability matrix, and the Tangram output map object in `.h5ad` format containing all the relevant information. +* It runs using the whole transcriptome, no gene markers are selected. * All parameters are the default. -The Tangram workflow was generated following the tutorial provided below: -https://tangram-sc.readthedocs.io/en/latest/tutorial_sq_link.html ## scAnnotate -Documentation written by: Tomas Vega Waichman -Date written: 2023-08-11 -scAnnotate cannot be separated between training and test. -Genes in references and query should match. -It allows to do the normalization inside their function using the parameter `lognormalized = F` but I normalized in the same way as they do on their script (using the NormalizeData function from the Seurat package, via the “LogNormalize” method and a scale factor of 10,000) since if we changed the input it would be easier to change. That's why I use `lognormalized = T`. -scAnnotate has two separate workflows with different batch effect removal steps based on the size of the training data. The `correction ="auto"` allows to automatically detect the needed for the dataset. They suggest using Seurat for dataset with at most one rare cell population -(at most one cell population less than 100 cells) and using Harmony for dataset with at least two rare cell populations (at least two cell populations less than 100 cells). -The `threshold` value goes between 0-1 and the cell with lower probability than the threshold are assing as "unassigned" +Documentation written by: Tomas Vega Waichman +Date written: 2023-08-11 The scAnnotate workflow was generated following the tutorial provided below: -* https://cran.r-project.org/web/packages/scAnnotate/vignettes/Introduction.html +https://cran.r-project.org/web/packages/scAnnotate/vignettes/Introduction.html +* Training and test steps of scAnnotate cannot be separated. +* Genes in references and query should match. +* The tool allows normalization inside their function using the parameter `lognormalized = F`. I normalized in the same way as they do on their script, but using the NormalizeData function from the Seurat package, via the “LogNormalize” method and a scale factor of 10,000. This is to allow the script to be easier to modify in the future (e.g. in case we allow an option for pre-normalized data). Since the data is normalized already by Seurat I set `lognormalized = T`. +* scAnnotate has two separate workflows with different batch effect removal steps based on the size of the training data. The `correction ="auto"` allows to automatically detect the needed for the dataset. They suggest using Seurat for dataset with at most one rare cell population (at most one cell population less than 100 cells) and using Harmony for dataset with at least two rare cell populations (at least two cell populations less than 100 cells). +* The `threshold` value goes between 0-1 and the cell with lower probability than the threshold are set to "unassigned" ## scID + Documentation written by: Tomas Vega Waichman -Date written: 2023-08-12 +Date written: 2023-08-12 + +The scID workflow was generated following the tutorials provided below: +* https://github.com/BatadaLab/scID/blob/master/vignettes/Mapping_example.md +* https://github.com/BatadaLab/scID -scID has some installing isues: - * Needs gdal/3.5.1 to install it. - * MAST is needed… if you are not able to install it use this approach: +scID has some issues for installation: + * Needs module `gdal/3.5.1` + * MAST is needed. If you are not able to install it, use this approach: ``` wget https://bioconductor.org/packages/release/bioc/src/contrib/MAST_1.26.0.tar.gz R CMD INSTALL MAST_1.26.0.tar.gz ``` -scID cannot be separated between training and test. -I used their `scID:::counts_to_cpm(counts_gem = query)` function that they provided (hidden, code in their github). Could be replaced with any normalization without log-transformation (they said this in the tutorial below: Any library-depth normalization (e.g. TPM, CPM) is compatible with scID, but not log-transformed data.) + +* Training and test steps of scID cannot be separated. +* I used their `scID:::counts_to_cpm(counts_gem = query)` function that they provided (hidden, code in their github). Could be replaced with any normalization without log-transformation (they said this in the tutorial below: Any library-depth normalization (e.g. TPM, CPM) is compatible with scID, but not log-transformed data.) * All parameters are the default except the normalization that is set in F since I normalized outside the function. But there exist some parameters that would be nice to explore as the `estimate_weights_from_target`. * It's very slow (takes ~ 2hs for the 5k cells query and 5k cell reference), but we have to test if it's related with the number of labels (number of comparison) or the size of the dataset. -The scID workflow was generated following the tutorials provided below: -* https://github.com/BatadaLab/scID/blob/master/vignettes/Mapping_example.md -* https://github.com/BatadaLab/scID ## scNym + Documentation written by: Tomas Vega Waichman -Date written: 2023-08-14 -scNym takes advantage of the query to train the model, so... even if we are able to separated, it needs the query to train the model. -* Query and training are concatenate in the same object and Any cell with the annotation "Unlabeled" will be treated as part of the target dataset and used for semi-supervised and adversarial training. It uses part of the query dataset to train the model. +Date written: 2023-08-14 + +The scNym workflow was generated following the tutorial provided below: +https://github.com/calico/scnym/tree/master + +scNym takes advantage of the query to train the model, so the training and test steps should not be separated. + +* Query and training are concatenated into the same object. Any cell with the annotation "Unlabeled" will be treated as part of the target dataset and used for semi-supervised and adversarial training. It uses part of the query dataset to train the model. * Data inputs for scNym should be log(CPM + 1) normalized counts, where CPM is Counts Per Million and log is the natural logarithm. -* They added the step of filtering no expressed genes so I added it but I ignored the step of filtering cells. -* Threashold to assing cells lower than that value as “Unknown”. +* They added the step of filtering genes that are not expressed, so I added it, but I ignored the step of filtering cells. +* This tool uses a threshold to assign labels to cells, and cells not passing this threshold have value “Unknown”. * It needs more research in multi-domain. -* whole_df_output.csv has the entire dataframe output with the score for the query test (mark as label == “Unlabeled”). -* I used the configuration as `new_identity_discovery` since: This configuration is useful for experiments where new cell type discoveries may occur. It uses -pseudolabel thresholding to avoid the assumption above. If new cell -types are present in the target data, they correctly receive low -confidence scores. - -The scNym workflow was generated following the tutorials provided below: -* https://github.com/calico/scnym/tree/master +* Additional output: `whole_df_output.csv` has the entire dataframe output with the score for the query test (mark as label == “Unlabeled”). +* I used the configuration as `new_identity_discovery` since: "This configuration is useful for experiments where new cell type discoveries may occur. It uses pseudolabel thresholding to avoid the assumption above. If new cell types are present in the target data, they correctly receive low +confidence scores." ## CellTypist + Documentation written by: Tomas Vega Waichman Date written: 2023-08-16 -CellTypist allows to separate between training and reference. -It Allows parallelization. -It has their own pre-trained models. -CellTypist requires a logarithmised and normalised expression matrix stored in the `AnnData` (log1p normalised expression to 10,000 counts per cell) [link](https://github.com/Teichlab/celltypist#supplemental-guidance-generate-a-custom-model) -Training: -* I use `check_expression = True` to check that the expression is okay. -* celltypist.train has the option `(feature_selection = True)` in order to do a feature_selection, but it is not implemented. -* The output is the model and and from the model we get the top markers for each cell type using the function `model.extract_top_markers()`. A table with the top 10 genes per cell-type is returned too (top10_model_markers_per_celltype.csv). -Predicting: -* "By default, CellTypist will only do the prediction jobs to infer the identities of input cells, which renders the prediction of each cell independent. To combine the cell type predictions with the cell-cell transcriptomic relationships, CellTypist offers a majority voting approach based on the idea that similar cell subtypes are more likely to form a (sub)cluster regardless of their individual prediction outcomes. To turn on the majority voting classifier in addition to the CellTypist predictions, pass in `majority_voting = True`. -If `majority_voting = True` all the predict column will be the majority_voting results otherwise it use the predicted_labels where each query cell gets its inferred label by choosing the most probable cell type among all possible cell types in the given model." [link](https://celltypist.readthedocs.io/en/latest/notebook/celltypist_tutorial_ml.html) -* majority_voting parameter should be specified in the configfile. -* I use the multilabel prediction… since we want to know if a cell cannot be classified very clearly… Description: For the built-in models, we have collected a large number of cell types; yet, the presence of unexpected (e.g., low-quality or novel cell types) and ambiguous cell states (e.g., doublets) in the query data is beyond the prediction that CellTypist can achieve with a 'find-a-best-match' mode. To overcome this, CellTypist provides the option of multi-label cell type classification, which assigns 0 (i.e., unassigned), 1, or >=2 cell type labels to each query cell. It allows the use of a `threshold` to label cells that are below that probability as "Unnasigned". It allows to have intermediate labels as combination in the format of `celltype1|celltype2`. -* Output: 4 .csv, the prediction for each cell (depending if we choose majority_voting or not will be the majority_voting or not), - * decision_matrix.csv : Decision matrix with the decision score of each cell belonging to a given cell type. - * probability_matrix.csv: Probability matrix representing the probability each cell belongs to a given cell type (transformed from decision matrix by the sigmoid function). - * predicted_labels.csv: The prediction for each cell, if majority_voting was true it has the information of the majority_voting labels AND the predicted_labels. - * Generates some embedding plots. - * An .h5ad object that has all the previous information (with the embeddings too) in a h5ad object. - The CellTypist workflow was generated following the tutorials provided below: Training: * https://celltypist.readthedocs.io/en/latest/celltypist.train.html * https://github.com/Teichlab/celltypist#supplemental-guidance-generate-a-custom-model Predicting: * https://celltypist.readthedocs.io/en/latest/notebook/celltypist_tutorial_ml.html + +CellTypist allows separation between training and reference, and allows parallelization. +They provide their own pre-trained models. +CellTypist requires a logarithmised and normalised expression matrix stored in the `AnnData` (log1p normalised expression to 10,000 counts per cell) [link](https://github.com/Teichlab/celltypist#supplemental-guidance-generate-a-custom-model) + +Training: +* I use `check_expression = True` to check that the expression is okay. +* `celltypist.train` has the option `(feature_selection = True)` in order to do a feature_selection, but it is not implemented. +* The output is the model and and from the model we get the top markers for each cell type using the function `model.extract_top_markers()`. A table with the top 10 genes per cell-type is returned too (top10_model_markers_per_celltype.csv). + +Predicting: +* From tutorial: "By default, CellTypist will only do the prediction jobs to infer the identities of input cells, which renders the prediction of each cell independent. To combine the cell type predictions with the cell-cell transcriptomic relationships, CellTypist offers a majority voting approach based on the idea that similar cell subtypes are more likely to form a (sub)cluster regardless of their individual prediction outcomes. To turn on the majority voting classifier in addition to the CellTypist predictions, pass in `majority_voting = True`. If `majority_voting = True` all the predict column will be the majority_voting results otherwise it use the predicted_labels where each query cell gets its inferred label by choosing the most probable cell type among all possible cell types in the given model." [link](https://celltypist.readthedocs.io/en/latest/notebook/celltypist_tutorial_ml.html) +* `majority_voting parameter` should be specified in the configfile. +* I use the multilabel prediction, since we want to know if a cell cannot be classified very clearly… Description: "For the built-in models, we have collected a large number of cell types; yet, the presence of unexpected (e.g., low-quality or novel cell types) and ambiguous cell states (e.g., doublets) in the query data is beyond the prediction that CellTypist can achieve with a 'find-a-best-match' mode. To overcome this, CellTypist provides the option of multi-label cell type classification, which assigns 0 (i.e., unassigned), 1, or >=2 cell type labels to each query cell. It allows the use of a `threshold` to label cells that are below that probability as "Unnasigned". It allows to have intermediate labels as combination in the format of `celltype1|celltype2`." + +* Output: 4 `.csv`, the prediction for each cell (depending if we choose majority_voting or not will be the majority_voting or not), + * `decision_matrix.csv`: Decision matrix with the decision score of each cell belonging to a given cell type. + * `probability_matrix.csv`: Probability matrix representing the probability each cell belongs to a given cell type (transformed from decision matrix by the sigmoid function). + * `predicted_labels.csv`: The prediction for each cell, if majority_voting was true it has the information of the majority_voting labels AND the predicted_labels. + * Generates some embedding plots. + * An `.h5ad` object that has all the previous information (with the embeddings too) in a `.h5ad` object. From acf6acb77845493a63f2bed6b2cb0e6d483b65eb Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Thu, 31 Aug 2023 15:27:09 -0400 Subject: [PATCH 074/144] Fix links within README.md #59 --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bed4012..c47c0be 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,13 @@ devtools::install_github(pkg) pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensorflow tables snakemake ``` -# Quickstart +# Quickstart tutorial -1. [Clone repository and install dependencies](#clone-repository-and-install-dependencies) -2. [Prepare reference](#prepare-reference) -3. [Prepare query samples](#prepare-query-samples) -4. [Prepare config file](#prepare-config-file) -5. [Prepare HPC submission script](#prepare-hpc-submission-script) +1. [Clone repository and install dependencies](#1-clone-repository-and-install-dependencies) +2. [Prepare reference](#2-prepare-reference) +3. [Prepare query samples](#3-prepare-query-samples) +4. [Prepare config file](#4-prepare-config-file) +5. [Prepare HPC submission script](#5-prepare-hpc-submission-script) ### 1. Clone repository and install dependencies From 2b1e685829a8349f948d9761997494e9c93f123c Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Thu, 31 Aug 2023 15:27:48 -0400 Subject: [PATCH 075/144] Update heading README.md #59 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c47c0be..10058a7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor # UPDATE ``` -## 5. Prepare HPC submission script +### 5. Prepare HPC submission script #### Annotate From 4f7e7d5250615302978884e51710ae174c27c831 Mon Sep 17 00:00:00 2001 From: Alva Annett Date: Thu, 31 Aug 2023 15:46:37 -0400 Subject: [PATCH 076/144] pdated rule graph --- rulegraph.annotation.pdf | Bin 0 -> 16849 bytes rulegraph.pdf | Bin 14616 -> 0 bytes snakefile.annotate | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 rulegraph.annotation.pdf delete mode 100644 rulegraph.pdf diff --git a/rulegraph.annotation.pdf b/rulegraph.annotation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dd47b88bee7ec4d4e9152819a6398c1865d8b77a GIT binary patch literal 16849 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 fr?Gt=_}`}F;AjYO{Hq@)y+7f1R(j&Oex diff --git a/snakefile.annotate b/snakefile.annotate index 8b16644..705c33c 100644 --- a/snakefile.annotate +++ b/snakefile.annotate @@ -709,7 +709,7 @@ rule predict_ACTINN: query = config['output_dir'] + "/{sample}/{reference}/expression.csv", model = config['output_dir'] + "/model/{reference}/ACTINN/ACTINN_model.pkl" output: - pred = config['output_dir'] + "/{reference}/{sample}/ACTINN/ACTINN_pred.csv" + pred = config['output_dir'] + "/{sample}/{reference}/ACTINN/ACTINN_pred.csv" params: basedir = {workflow.basedir} log: From 197d0b8abc74882cfc3d63cba852643df48f1edb Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:48:30 -0400 Subject: [PATCH 077/144] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 10058a7..a421147 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Snakemake pipeline for consensus prediction of cell types in single-cell RNA seq The pipeline is automated and running it does not require prior knowledge of machine learning. It also features parallelization options to exploit available computational resources for maximal efficiency. This pipeline trains classifiers on genes common to the reference and all query datasets. +[Rule Graph](rulegraph.annotation.pdf) + # Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. From 71cdcd8e43705e8e8d999b8affc134ff3f19ac09 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:49:02 -0400 Subject: [PATCH 078/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a421147..344f55d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Snakemake pipeline for consensus prediction of cell types in single-cell RNA seq The pipeline is automated and running it does not require prior knowledge of machine learning. It also features parallelization options to exploit available computational resources for maximal efficiency. This pipeline trains classifiers on genes common to the reference and all query datasets. -[Rule Graph](rulegraph.annotation.pdf) +[Annotation Workflow](rulegraph.annotation.pdf) # Installation and Dependencies From 79464766549c244d3f4b7b6b6408b22c1fb390c6 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:50:33 -0400 Subject: [PATCH 079/144] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 344f55d..8ea6772 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor - Correlation - scID - scAnnotate +- scNym +- CellTypist ``` ## Single cell RNA reference + spatial RNA query From fc8ca5703cedb5fa4841ee1e2b2a8f944f5a2fc9 Mon Sep 17 00:00:00 2001 From: Bhavyaa Chandarana Date: Fri, 1 Sep 2023 13:21:35 -0400 Subject: [PATCH 080/144] Update README.md - add devtools BiocManager install --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 8ea6772..f4934d3 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,18 @@ install.packages(pkg) Older version of Matrix package needs to be installed for Seurat to work: https://github.com/satijalab/seurat/issues/6746 ```R +if (!require("devtools", quietly = TRUE)) + install.packages("devtools") + devtools::install_version("Matrix", version = "1.5.3", repos = "http://cran.us.r-project.org") ``` ## R packages - Bioconductor ```R +if (!require("BiocManager", quietly = TRUE)) + install.packages("BiocManager") + pkg = c("SingleCellExperiment", "SummarizedExperiment", "ComplexHeatmap", @@ -57,6 +63,9 @@ BiocManager::install(pkg) ## R packages - Github ```R +if (!require("devtools", quietly = TRUE)) + install.packages("devtools") + pkg = c("pcahan1/singleCellNet", "powellgenomicslab/scPred", "PaulingLiu/scibet", From 74779d0a3fc086dcd32b0b88e5e67f32d00557ee Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 13:51:49 -0400 Subject: [PATCH 081/144] Update example.config.yml --- example.config.yml | 48 ++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/example.config.yml b/example.config.yml index 1588000..032deb6 100644 --- a/example.config.yml +++ b/example.config.yml @@ -1,44 +1,42 @@ -# target directory -output_dir: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev/out/ - -references: 'Braindex' -marker_genes: 'PDGFRA OLIG2 PTPRC' +# target directory +output_dir: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev +output_dir_benchmark: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev/benchmark # path to reference to train classifiers on (cell x gene raw counts) -training_reference: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/expression.csv - -# path to annotations for the reference (csv file with cellname and label headers) -reference_annotations: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/labels.csv +references: + braindex1: + expression: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/expression.csv + labels: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/labels.csv + braindex2: + expression: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/expression.csv + labels: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/ref/labels.csv # path to query datasets (cell x gene raw counts) query_datasets: - - /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p3/expression.csv - - /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p0/expression.csv - -# step to check if some genes are kept in the common gene space between ref and query -check_genes: False -genes_required: Null - -# convert mouse genes of reference to human genes -convert_ref_mm_to_hg: False + ct_p3: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p3/expression.csv + ct_p0: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p0/expression.csv # classifiers to run tools_to_run: - scPred - - model: ['SVMradial', 'glm'] - SingleR - scClassify - SciBet - singleCellNet - - scHPL + - scHPL - SVMlinear - - scLearn - Correlation + - scLearn + - ACTINN + - scID + - scAnnotate + - scNym + - CellTypist consensus_tools: - - all - -# benchmark tools on reference + - all + +# Benchmark parameters benchmark: - n_folds: 3 + n_folds: 10 From 0d82c35ebe4790e596bdf4c7822821a4af1110ad Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 13:57:26 -0400 Subject: [PATCH 082/144] Update example.config.yml --- example.config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example.config.yml b/example.config.yml index 032deb6..992b8a3 100644 --- a/example.config.yml +++ b/example.config.yml @@ -1,3 +1,5 @@ +# Example config for scCoAnnotate annotation and benchmarking workflow + # target directory output_dir: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev output_dir_benchmark: /lustre06/project/6004736/alvann/from_narval/DEV/test-scCoAnnotate-dev/benchmark @@ -16,6 +18,8 @@ query_datasets: ct_p3: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p3/expression.csv ct_p0: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p0/expression.csv + + # classifiers to run tools_to_run: - scPred From 2515023546f121a5a4918a1df3cd86c18153c284 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 13:58:22 -0400 Subject: [PATCH 083/144] Update example.config.yml --- example.config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example.config.yml b/example.config.yml index 992b8a3..379fa7c 100644 --- a/example.config.yml +++ b/example.config.yml @@ -18,7 +18,8 @@ query_datasets: ct_p3: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p3/expression.csv ct_p0: /lustre06/project/6004736/alvann/from_narval/DEV/reference-dev/Braindex_20210710/samples/ct_p0/expression.csv - +# convert gene symbols in reference from mouse to human +convert_ref_mm_to_hg: False # classifiers to run tools_to_run: From 16bed3fcda3c174c700de404078d0695a8725253 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:08:24 -0400 Subject: [PATCH 084/144] Update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f4934d3..0d19122 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # scCoAnnotate + # Summary Snakemake pipeline for consensus prediction of cell types in single-cell RNA sequencing (scRNA-seq) data. The pipeline allows users to run up to 15 different reference-based annotation tools (statistical models and machine learning approaches) to predict cell type labels of multiple scRNA-seq samples. It then outputs a consensus of the predictions, which has been found to have increased accuracy in benchmarking experiments compared to the individual predictions alone, by combining the strengths of the different approaches. @@ -8,7 +9,7 @@ The pipeline is automated and running it does not require prior knowledge of mac [Annotation Workflow](rulegraph.annotation.pdf) -# Installation and Dependencies +# :gear: Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. @@ -81,7 +82,7 @@ devtools::install_github(pkg) pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensorflow tables snakemake ``` -# Quickstart tutorial +# :running_woman: Quickstart tutorial 1. [Clone repository and install dependencies](#1-clone-repository-and-install-dependencies) 2. [Prepare reference](#2-prepare-reference) @@ -115,7 +116,7 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor # UPDATE ``` -# Available tools +# :hammer_and_wrench: Available tools ## Single cell RNA reference + single cell RNA query @@ -146,13 +147,13 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM -# Adding new tools: +# Adding new tools ``` # UPDATE ``` -# Snakemake Tips and Tricks +# :sanke: Snakemake Tips and Tricks - Dryrun snakemake pipeline before submitting job ```bash From dde6b4943052b3d7446a1003caf54e872aae4d09 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:46:28 -0400 Subject: [PATCH 085/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d19122..93c6f08 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM # UPDATE ``` -# :sanke: Snakemake Tips and Tricks +# 🐍 Snakemake Tips and Tricks - Dryrun snakemake pipeline before submitting job ```bash From 808445c9a9c455f487c49c2337dd764d97b193d4 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:02:00 -0400 Subject: [PATCH 086/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 93c6f08..9c72c6a 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ snakemake -s ${snakefile} --configfile ${config} -c1 -R $(snakemake -s ${snakefi snakemake -s ${snakefile} --configfile ${config} --report ${report} ``` -# Detailed documentation on tool wrapper scripts +# :female_detective: Detailed documentation on tool wrapper scripts ## scClassify From c391f3724f144505eb178ec653fffb714e0c48f7 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:17:38 -0400 Subject: [PATCH 087/144] Update README.md --- README.md | 69 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 9c72c6a..e5e0290 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,40 @@ The pipeline is automated and running it does not require prior knowledge of mac [Annotation Workflow](rulegraph.annotation.pdf) +# :running_woman: Quickstart tutorial + +1. [Clone repository and install dependencies](#1-clone-repository-and-install-dependencies) +2. [Prepare reference](#2-prepare-reference) +3. [Prepare query samples](#3-prepare-query-samples) +4. [Prepare config file](#4-prepare-config-file) +5. [Prepare HPC submission script](#5-prepare-hpc-submission-script) + +### 1. Clone repository and install dependencies + +### 2. Prepare reference + +### 3. Prepare query samples + +### 4. Prepare config file + +```yaml +# UPDATE +``` + +### 5. Prepare HPC submission script + +#### Annotate + +```bash +# UPDATE +``` + +#### Benchmark + +```bash +# UPDATE +``` + # :gear: Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. @@ -82,40 +116,6 @@ devtools::install_github(pkg) pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensorflow tables snakemake ``` -# :running_woman: Quickstart tutorial - -1. [Clone repository and install dependencies](#1-clone-repository-and-install-dependencies) -2. [Prepare reference](#2-prepare-reference) -3. [Prepare query samples](#3-prepare-query-samples) -4. [Prepare config file](#4-prepare-config-file) -5. [Prepare HPC submission script](#5-prepare-hpc-submission-script) - -### 1. Clone repository and install dependencies - -### 2. Prepare reference - -### 3. Prepare query samples - -### 4. Prepare config file - -```yaml -# UPDATE -``` - -### 5. Prepare HPC submission script - -#### Annotate - -```bash -# UPDATE -``` - -#### Benchmark - -```bash -# UPDATE -``` - # :hammer_and_wrench: Available tools ## Single cell RNA reference + single cell RNA query @@ -143,6 +143,7 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor - Tangram ``` + # Resources TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM From 63770b5792078f5ebd9ef3a63555bbe4ae628f3c Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:34:14 -0400 Subject: [PATCH 088/144] Update README.md --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index e5e0290..01ac63f 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,45 @@ The pipeline is automated and running it does not require prior knowledge of mac ### 1. Clone repository and install dependencies +This step is only nessesary if you are not part of the Kleinman group! + +Clone git repository in appropriate location: + +```bash +git clone https://github.com/fungenomics/scCoAnnotate.git +``` +Install R packages and python modules as specified in [Installation and Dependencies](#gear-installation-and-dependencies) + +If you are part of the Kleinman group you only need to load the module on Narval or Hydra: + +```bash +module load scCoAnnotate/2.0 +``` + ### 2. Prepare reference +The input format for the references is a **cell x gene matrix** (.csv) and a **cell x label matrix** (.csv). + +Both the **cell x gene matrix** and **cell x label matrix** need the first column to be the cell names in the same order with an empty column name. + +**cell x gene matrix** +```bash +'',gene1,gene2,gene3 +cell1,1,24,30 +cell2,54,20,61 +cell3,0,12,0 +cell4,1,13,17 +``` + + **cell x label matrix** +```bash +'',label +cell1,label1 +cell2,label1 +cell3,label3 +cell4,label2 +``` + ### 3. Prepare query samples ### 4. Prepare config file From 9c95dc34cddfca92914188b3cdb6adafbb500b2a Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:48:46 -0400 Subject: [PATCH 089/144] Update README.md --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 01ac63f..cf22fa7 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,9 @@ module load scCoAnnotate/2.0 ### 2. Prepare reference -The input format for the references is a **cell x gene matrix** (.csv) and a **cell x label matrix** (.csv). +The input format for the references is a **cell x gene matrix** (.csv) of raw counts and a **cell x label matrix** (.csv). -Both the **cell x gene matrix** and **cell x label matrix** need the first column to be the cell names in the same order with an empty column name. +Both the **cell x gene matrix** and **cell x label matrix** need the first column to be the cell names in matching order with an empty column name. **cell x gene matrix** ```bash @@ -60,10 +60,59 @@ cell4,label2 ### 3. Prepare query samples +The input format for the query samples is a **cell x gene matrix** (.csv) of raw counts. + +The first column needs to be the cell names with an empty column name. + +**cell x gene matrix** +```bash +'',gene1,gene2,gene3 +cell1,27,1,34 +cell2,0,12,56 +cell3,0,17,12 +cell4,54,20,61 +``` + ### 4. Prepare config file +For each set of query samples a config file needs to be prepared with information about the samples, the reference, the tools you want to run and how to calculate the consensus. + +See [Example Config](example.config.yml) + ```yaml -# UPDATE +# target directory +output_dir: +output_dir_benchmark: + +# path to reference to train classifiers on (cell x gene raw counts) +references: + : + expression: + labels: + : + expression: + labels: + +# path to query datasets (cell x gene raw counts) +query_datasets: + : + : + +# convert gene symbols in reference from mouse to human +convert_ref_mm_to_hg: False + +# classifiers to run +tools_to_run: + - tool1 + - tool2 + +# consensus method +consensus_tools: + - all + +# benchmark parameters +benchmark: + n_folds: ``` ### 5. Prepare HPC submission script From 51fd3a42be87f7e5cb75beb954b5b561ad9fbcaa Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:56:26 -0400 Subject: [PATCH 090/144] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf22fa7..b3f6fc4 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,13 @@ cell4,54,20,61 For each set of query samples a config file needs to be prepared with information about the samples, the reference, the tools you want to run and how to calculate the consensus. +Multiple references can be specified with a unique **reference name**. + +Full list of available tools can be found here: [Available tools](#hammer-and-wrench-available-tools) +Make sure that the names of the selected tools have the same capitalization and format as this list. + +The consensus method selected in **consensus_tools** can either be 'all' (which uses all the tools in **tools_to_run**) or a list of tools to include. + See [Example Config](example.config.yml) ```yaml @@ -229,7 +236,6 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor - Tangram ``` - # Resources TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM From 34f0d292092f1b7cb35edc9f02ab8bad2ca7f558 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 15:59:42 -0400 Subject: [PATCH 091/144] Update example.submit.sh --- example.submit.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/example.submit.sh b/example.submit.sh index d598d1a..19dbb73 100644 --- a/example.submit.sh +++ b/example.submit.sh @@ -4,15 +4,15 @@ #SBATCH --output=logs/%x.out #SBATCH --error=logs/%x.err #SBATCH --ntasks=1 -#SBATCH --cpus-per-task= -#SBATCH --time= -#SBATCH --mem-per-cpu= +#SBATCH --cpus-per-task=5 +#SBATCH --time=24:00:00 +#SBATCH --mem-per-cpu=60GB module load scCoAnnotate/2.0 # path to snakefile and config -snakefile="" -config="" +snakefile= +config= # unlock directory incase of previous errors snakemake -s ${snakefile} --configfile ${config} --unlock From 10a54a24dd48325ac78b343e044433da05cd6a93 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:00:38 -0400 Subject: [PATCH 092/144] Update example.submit.sh --- example.submit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.submit.sh b/example.submit.sh index 19dbb73..d9e14ac 100644 --- a/example.submit.sh +++ b/example.submit.sh @@ -18,5 +18,5 @@ config= snakemake -s ${snakefile} --configfile ${config} --unlock # run workflow -snakemake -s ${snakefile} --configfile ${config} --cores 2 +snakemake -s ${snakefile} --configfile ${config} --cores 5 From fc5cb11117e3595706d7394362c9e60545216e71 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:10:33 -0400 Subject: [PATCH 093/144] Update README.md --- README.md | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b3f6fc4..a2b1701 100644 --- a/README.md +++ b/README.md @@ -124,18 +124,38 @@ benchmark: ### 5. Prepare HPC submission script -#### Annotate +To run the snakemake pipeline on a HPC a submission script needs to be prepared + +See: [Example Bash Script](example.submit.sh) ```bash -# UPDATE -``` +#!/bin/sh +#SBATCH --job-name=scCoAnnotate +#SBATCH --account=rrg-kleinman +#SBATCH --output=logs/%x.out +#SBATCH --error=logs/%x.err +#SBATCH --ntasks=1 +#SBATCH --cpus-per-task=5 +#SBATCH --time=24:00:00 +#SBATCH --mem-per-cpu=60GB -#### Benchmark +module load scCoAnnotate/2.0 -```bash -# UPDATE +# path to snakefile and config +snakefile= +config= + +# unlock directory incase of previous errors +snakemake -s ${snakefile} --configfile ${config} --unlock + +# run workflow +snakemake -s ${snakefile} --configfile ${config} --cores 5 ``` +Depending on if you want to run the annotation workflow or the benchmarking workflow the snakefile needs to be path to either [snakefile.annotate](snakefile.annotate) or [snakefile.benchmark](snakefile.benchmark) + +OBS!! Make sure that the number of cores requested match the number of cores in the snakemake command for optimal use of resources + # :gear: Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. @@ -232,15 +252,16 @@ pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensor ``` ## Single cell RNA reference + spatial RNA query + ```yaml - Tangram ``` -# Resources +# :floppy_disk: Resources -TABLE WITH TOOL, N CELLS, N LABELS, TIME, MEM +Add table with resource usage for different sice references and queries -# Adding new tools +# :woman_mechanic: Adding new tools ``` # UPDATE From 4f2376c710987887b6950bd7acba6b52ecda6126 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:32:14 -0400 Subject: [PATCH 094/144] Update README.md --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/README.md b/README.md index a2b1701..ba69f88 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,86 @@ Depending on if you want to run the annotation workflow or the benchmarking work OBS!! Make sure that the number of cores requested match the number of cores in the snakemake command for optimal use of resources +# Chaning default parameters + +The pipeline uses a default config file to specify tool parameters as well as cluster options. For full list of parameters you can change see: [Default Config](Config/config.default.yml) + +To over ride these values you can either add a corresponding section in your config file or copy the whole default config to your run folder, change the values and add it as an extra config in the submission script. The second option may be preferable if you are changing many of the default parameters. + +The order of overwriting parameters are as follows: +1. Config specified in the snakefile (in this case the default config) +2. Config specified as snakemake argument with `--configfile` (in the order they are added) + +## Option 1: Add corresponding section to your own config file + +Case: You want to change the probbability cut off threshold from 0.5 to 0.25 for **scHPL** + +This section is found in the default config: + +```ymal +scHPL: + threads: 1 + classifier: 'svm' + dimred: 'False' + threshold: 0.5 +``` + +Create a corresponding section in your config and change the threshold value to 0.25: + +```yaml +# target directory +output_dir: +output_dir_benchmark: + +# path to reference to train classifiers on (cell x gene raw counts) +references: + : + expression: + labels: + : + expression: + labels: + +# path to query datasets (cell x gene raw counts) +query_datasets: + : + : + +# convert gene symbols in reference from mouse to human +convert_ref_mm_to_hg: False + +# classifiers to run +tools_to_run: + - tool1 + - tool2 + +# consensus method +consensus_tools: + - all + +# benchmark parameters +benchmark: + n_folds: + +# additional parameters +scHPL: + threshold: 0.25 +``` + +## Option 2: Copy the whole default config and provide it as an additional config file in the submission + +In this case your submission script would look like this: + +```bash +# path to snakefile and config +snakefile= +config= +extra_config= + +# run workflow +snakemake -s ${snakefile} --configfile ${config} ${extra_config} --cores 5 +``` + # :gear: Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. From 205144b273bfb8970a290c6a37bb754829fe44b3 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:39:43 -0400 Subject: [PATCH 095/144] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba69f88..7612394 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ benchmark: n_folds: ``` +How + ### 5. Prepare HPC submission script To run the snakemake pipeline on a HPC a submission script needs to be prepared @@ -156,7 +158,7 @@ Depending on if you want to run the annotation workflow or the benchmarking work OBS!! Make sure that the number of cores requested match the number of cores in the snakemake command for optimal use of resources -# Chaning default parameters +## Changing default parameters The pipeline uses a default config file to specify tool parameters as well as cluster options. For full list of parameters you can change see: [Default Config](Config/config.default.yml) From bc278b17101539df517a5526aedeca727d0c8b1f Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:55:30 -0400 Subject: [PATCH 096/144] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7612394..48e1b0b 100644 --- a/README.md +++ b/README.md @@ -122,13 +122,13 @@ benchmark: n_folds: ``` -How +See [Example Bash Script](example.submit.sh) ### 5. Prepare HPC submission script To run the snakemake pipeline on a HPC a submission script needs to be prepared -See: [Example Bash Script](example.submit.sh) +See: [Example Bash Script](## changing-default-parameters ) ```bash #!/bin/sh @@ -160,7 +160,7 @@ OBS!! Make sure that the number of cores requested match the number of cores in ## Changing default parameters -The pipeline uses a default config file to specify tool parameters as well as cluster options. For full list of parameters you can change see: [Default Config](Config/config.default.yml) +The pipeline uses a default config file in addition to the user defined one to specify tool parameters as well as cluster options. For full list of parameters you can change see: [Default Config](Config/config.default.yml) To over ride these values you can either add a corresponding section in your config file or copy the whole default config to your run folder, change the values and add it as an extra config in the submission script. The second option may be preferable if you are changing many of the default parameters. From 58fbe7885c5c8e93683e0f13c2d49aa3d227f437 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:59:40 -0400 Subject: [PATCH 097/144] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48e1b0b..d00bf4b 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ See [Example Bash Script](example.submit.sh) To run the snakemake pipeline on a HPC a submission script needs to be prepared -See: [Example Bash Script](## changing-default-parameters ) +See: [Changing Default Parameters](## changing-default-parameters) ```bash #!/bin/sh @@ -158,7 +158,7 @@ Depending on if you want to run the annotation workflow or the benchmarking work OBS!! Make sure that the number of cores requested match the number of cores in the snakemake command for optimal use of resources -## Changing default parameters +## Changing Default Parameters The pipeline uses a default config file in addition to the user defined one to specify tool parameters as well as cluster options. For full list of parameters you can change see: [Default Config](Config/config.default.yml) @@ -167,6 +167,7 @@ To over ride these values you can either add a corresponding section in your con The order of overwriting parameters are as follows: 1. Config specified in the snakefile (in this case the default config) 2. Config specified as snakemake argument with `--configfile` (in the order they are added) +3. Parameters specified directly in snakemake argument with `--config` ## Option 1: Add corresponding section to your own config file From 3aca15c40cdf930bd0be9a1ce9bfe3b9c34f27da Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:04:13 -0400 Subject: [PATCH 098/144] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d00bf4b..c025f33 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ snakemake -s ${snakefile} --configfile ${config} --cores 5 Depending on if you want to run the annotation workflow or the benchmarking workflow the snakefile needs to be path to either [snakefile.annotate](snakefile.annotate) or [snakefile.benchmark](snakefile.benchmark) -OBS!! Make sure that the number of cores requested match the number of cores in the snakemake command for optimal use of resources +**OBS!!** Make sure that the number of cores requested match the number of cores in the snakemake command for optimal use of resources ## Changing Default Parameters @@ -309,7 +309,7 @@ devtools::install_github(pkg) ## Python modules ```bash -pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensorflow tables snakemake +pip install numpy pandas scHPL sklearn anndata matplotlib scanpy datetime tensorflow tables celltypist snakemake ``` # :hammer_and_wrench: Available tools From e9ff46ae32351cbbd2b2a9ef3f6776c32c8a02cf Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:04:22 -0400 Subject: [PATCH 099/144] Update README.md From daa8ebb2765adf67d41e15989205837cd3f5ea95 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:06:08 -0400 Subject: [PATCH 100/144] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c025f33..b6f6d72 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,6 @@ snakemake -s ${snakefile} --configfile ${config} --unlock # run workflow snakemake -s ${snakefile} --configfile ${config} --cores 5 ``` - Depending on if you want to run the annotation workflow or the benchmarking workflow the snakefile needs to be path to either [snakefile.annotate](snakefile.annotate) or [snakefile.benchmark](snakefile.benchmark) **OBS!!** Make sure that the number of cores requested match the number of cores in the snakemake command for optimal use of resources @@ -171,7 +170,7 @@ The order of overwriting parameters are as follows: ## Option 1: Add corresponding section to your own config file -Case: You want to change the probbability cut off threshold from 0.5 to 0.25 for **scHPL** +**Case:** You want to change the probbability cut off threshold from 0.5 to 0.25 for **scHPL** This section is found in the default config: @@ -225,7 +224,7 @@ scHPL: threshold: 0.25 ``` -## Option 2: Copy the whole default config and provide it as an additional config file in the submission +## Option 2: Copy the whole default config and add it as an extra config file in the snakemake command In this case your submission script would look like this: From 163ff0f364b42c966f73602486679d6329a90955 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:06:51 -0400 Subject: [PATCH 101/144] Add files via upload --- scCoAnnotate_workflow.drawio.png | Bin 0 -> 75628 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 scCoAnnotate_workflow.drawio.png diff --git a/scCoAnnotate_workflow.drawio.png b/scCoAnnotate_workflow.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..3eb6a54267177afdef50e77d8d81a35459ff1756 GIT binary patch literal 75628 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 E0sc{U0ssI2 literal 0 HcmV?d00001 From 6cbb7cdf7a7fb041a026fcf6c12a13d8e2f3dc58 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:16:21 -0400 Subject: [PATCH 102/144] Add files via upload --- scCoAnnotate_workflow.drawio.png | Bin 75628 -> 38919 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scCoAnnotate_workflow.drawio.png b/scCoAnnotate_workflow.drawio.png index 3eb6a54267177afdef50e77d8d81a35459ff1756..c140253fa544d1a5f2c2db9ee0b346cfb8120f35 100644 GIT binary patch literal 38919 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 E0sc{U0ssI2 From f75b63657a46d1e4ccdac4b4ea8c7e9601196031 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:34:50 -0400 Subject: [PATCH 103/144] Add files via upload --- scCoAnnotate_workflow.drawio.png | Bin 38919 -> 83494 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scCoAnnotate_workflow.drawio.png b/scCoAnnotate_workflow.drawio.png index c140253fa544d1a5f2c2db9ee0b346cfb8120f35..efe0c4f4526a96a6838ee670a2800bd9e68eaa7f 100644 GIT binary patch literal 83494 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 Date: Tue, 5 Sep 2023 17:55:05 -0400 Subject: [PATCH 104/144] Add files via upload --- scCoAnnotate_workflow.drawio.png | Bin 83494 -> 84133 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/scCoAnnotate_workflow.drawio.png b/scCoAnnotate_workflow.drawio.png index efe0c4f4526a96a6838ee670a2800bd9e68eaa7f..1770ed487c76a1087ff8b374323a270022479448 100644 GIT binary patch delta 52047 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 delta 50327 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 Date: Tue, 5 Sep 2023 17:55:22 -0400 Subject: [PATCH 105/144] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b6f6d72..85b2907 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# scCoAnnotate - +# scCoAnnotate + +See the snakemake rule graph for a more detailed description of the annotation workflow: [Annotation Workflow](rulegraph.annotation.pdf) # :running_woman: Quickstart tutorial From 0584a9e1583c02922706d769e2801f876db0a942 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:56:12 -0400 Subject: [PATCH 106/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85b2907..2807d66 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scCoAnnotate # Summary From a05424a26fca262ce354fb636589e15cc2972aa0 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:56:31 -0400 Subject: [PATCH 107/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2807d66..032210b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The pipeline is automated and running it does not require prior knowledge of mac Two different workflows can be run as part of scCoAnnotate. The annotation workflow takes both a references data set and query samples with the aim of annotating the query samples. The benchmarking workflow takes only the reference and preforms a M fold cross validation. - + See the snakemake rule graph for a more detailed description of the annotation workflow: [Annotation Workflow](rulegraph.annotation.pdf) From 5fd9393de69e12fc161c6a77d8f67f6f926cfae0 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 17:58:03 -0400 Subject: [PATCH 108/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 032210b..2807d66 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The pipeline is automated and running it does not require prior knowledge of mac Two different workflows can be run as part of scCoAnnotate. The annotation workflow takes both a references data set and query samples with the aim of annotating the query samples. The benchmarking workflow takes only the reference and preforms a M fold cross validation. - + See the snakemake rule graph for a more detailed description of the annotation workflow: [Annotation Workflow](rulegraph.annotation.pdf) From 34024e3d8187719a13449c1f58d412eb62e90251 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:01:38 -0400 Subject: [PATCH 109/144] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 2807d66..7360427 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ The pipeline is automated and running it does not require prior knowledge of mac Two different workflows can be run as part of scCoAnnotate. The annotation workflow takes both a references data set and query samples with the aim of annotating the query samples. The benchmarking workflow takes only the reference and preforms a M fold cross validation. - - See the snakemake rule graph for a more detailed description of the annotation workflow: [Annotation Workflow](rulegraph.annotation.pdf) From fc83a59bfea05b0808763dbfa0929213bb82b8a8 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:02:03 -0400 Subject: [PATCH 110/144] Delete scCoAnnotate_workflow.drawio.png --- scCoAnnotate_workflow.drawio.png | Bin 84133 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 scCoAnnotate_workflow.drawio.png diff --git a/scCoAnnotate_workflow.drawio.png b/scCoAnnotate_workflow.drawio.png deleted file mode 100644 index 1770ed487c76a1087ff8b374323a270022479448..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84133 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 z 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 Date: Tue, 5 Sep 2023 18:03:09 -0400 Subject: [PATCH 111/144] Add files via upload --- scCoAnnotate_workflow.drawio.png | Bin 0 -> 84133 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 scCoAnnotate_workflow.drawio.png diff --git a/scCoAnnotate_workflow.drawio.png b/scCoAnnotate_workflow.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..1770ed487c76a1087ff8b374323a270022479448 GIT binary patch literal 84133 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 z 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 Date: Tue, 5 Sep 2023 18:04:27 -0400 Subject: [PATCH 112/144] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7360427..e30964d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ The pipeline is automated and running it does not require prior knowledge of mac Two different workflows can be run as part of scCoAnnotate. The annotation workflow takes both a references data set and query samples with the aim of annotating the query samples. The benchmarking workflow takes only the reference and preforms a M fold cross validation. + + See the snakemake rule graph for a more detailed description of the annotation workflow: [Annotation Workflow](rulegraph.annotation.pdf) From 9dfc9056fe0fbf93bf51f5cc7d5a7ca2810f92f0 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:09:02 -0400 Subject: [PATCH 113/144] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e30964d..87b625d 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ The order of overwriting parameters are as follows: 2. Config specified as snakemake argument with `--configfile` (in the order they are added) 3. Parameters specified directly in snakemake argument with `--config` -## Option 1: Add corresponding section to your own config file +### Option 1: Add corresponding section to your own config file **Case:** You want to change the probbability cut off threshold from 0.5 to 0.25 for **scHPL** @@ -228,7 +228,7 @@ scHPL: threshold: 0.25 ``` -## Option 2: Copy the whole default config and add it as an extra config file in the snakemake command +### Option 2: Copy the whole default config and add it as an extra config file in the snakemake command In this case your submission script would look like this: From 3619fc0e4c599928cb309d11bd85c654b050b3e1 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:10:10 -0400 Subject: [PATCH 114/144] Update README.md From c5a5a5a45bcd5a2c0fd36eb75e02191b2e034de3 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:12:46 -0400 Subject: [PATCH 115/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87b625d..8a8777a 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ cell4,54,20,61 For each set of query samples a config file needs to be prepared with information about the samples, the reference, the tools you want to run and how to calculate the consensus. -Multiple references can be specified with a unique **reference name**. +Multiple references can be specified with an unique **reference name**. Full list of available tools can be found here: [Available tools](#hammer-and-wrench-available-tools) Make sure that the names of the selected tools have the same capitalization and format as this list. From edd2438ec56fc9620e52818c1a69def54f0d6357 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:13:24 -0400 Subject: [PATCH 116/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a8777a..9219412 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ For each set of query samples a config file needs to be prepared with informatio Multiple references can be specified with an unique **reference name**. -Full list of available tools can be found here: [Available tools](#hammer-and-wrench-available-tools) +Full list of available tools can be found here: [Available tools](#hammer-and-wrench-available-tools) Make sure that the names of the selected tools have the same capitalization and format as this list. The consensus method selected in **consensus_tools** can either be 'all' (which uses all the tools in **tools_to_run**) or a list of tools to include. From 490f584a9a56d332dbebb67584ed7c24fceacf5e Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:15:09 -0400 Subject: [PATCH 117/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9219412..3b507b1 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ See [Example Bash Script](example.submit.sh) To run the snakemake pipeline on a HPC a submission script needs to be prepared -See: [Changing Default Parameters](## changing-default-parameters) +See: [Changing Default Parameters](##changing-default-parameters) ```bash #!/bin/sh From 10f0a9f4add29a501be65acd6b25c92e79d71b07 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:16:07 -0400 Subject: [PATCH 118/144] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3b507b1..04fd812 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Make sure that the names of the selected tools have the same capitalization and The consensus method selected in **consensus_tools** can either be 'all' (which uses all the tools in **tools_to_run**) or a list of tools to include. -See [Example Config](example.config.yml) +See: [Example Config](example.config.yml) ```yaml # target directory @@ -125,14 +125,13 @@ consensus_tools: benchmark: n_folds: ``` - -See [Example Bash Script](example.submit.sh) +See: [Changing Default Parameters](##changing-default-parameters) ### 5. Prepare HPC submission script To run the snakemake pipeline on a HPC a submission script needs to be prepared -See: [Changing Default Parameters](##changing-default-parameters) +See: [Example Bash Script](example.submit.sh) ```bash #!/bin/sh From 1bf3d14f962658f840b69ecd42206c8eb6d5cd8f Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:12:18 -0400 Subject: [PATCH 119/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 04fd812..272e07d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ See the snakemake rule graph for a more detailed description of the annotation w ### 1. Clone repository and install dependencies -This step is only nessesary if you are not part of the Kleinman group! +This step is not nessesary if you are part of the Kleinman group! Clone git repository in appropriate location: From ef191f873c30c249add85792ebe578986181e020 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:15:15 -0400 Subject: [PATCH 120/144] Update train_CellTypist.py --- Scripts/CellTypist/train_CellTypist.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Scripts/CellTypist/train_CellTypist.py b/Scripts/CellTypist/train_CellTypist.py index 8474c74..2fa355c 100644 --- a/Scripts/CellTypist/train_CellTypist.py +++ b/Scripts/CellTypist/train_CellTypist.py @@ -9,17 +9,21 @@ import pickle import os import random + ### Set seed random.seed(123456) #--------------- Parameters ------------------- + ref_path = str(sys.argv[1]) lab_path = str(sys.argv[2]) out_path = str(sys.argv[3]) out_other_path = os.path.dirname(str(sys.argv[3])) threads = int(sys.argv[4]) feature_selection = bool(sys.argv[5]) + #--------------- Data ------------------------- + # read the data ref = pd.read_csv(ref_path, index_col=0, @@ -32,22 +36,25 @@ # check if cell names are in the same order in labels and ref order = all(labels.index == ref.index) + # throw error if order is not the same if not order: sys.exit("@ Order of cells in reference and labels do not match") +# create AnnData object adata = ad.AnnData(X = ref, obs = dict(obs_names=ref.index.astype(str), label = labels['label']), var = dict(var_names=ref.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# normalize the matrix with scanpy: +# normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(adata, target_sum=1e4) + #Logarithmize the data: sc.pp.log1p(adata) @@ -67,7 +74,8 @@ print('@ DONE') #------------- Other outputs -------------- -#We can extract the top markers, I get the top 10 for each cell-type applying the + +# We can extract the top markers, I get the top 10 for each cell-type applying the # function extract_top_markers dataframes = [] for cell_type in model.cell_types: From ac8da3caef320431e5acb876c2859e35dc7bc125 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:16:05 -0400 Subject: [PATCH 121/144] Update predict_CellTypist.py --- Scripts/CellTypist/predict_CellTypist.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Scripts/CellTypist/predict_CellTypist.py b/Scripts/CellTypist/predict_CellTypist.py index 5a1ec89..07c3b78 100644 --- a/Scripts/CellTypist/predict_CellTypist.py +++ b/Scripts/CellTypist/predict_CellTypist.py @@ -8,10 +8,12 @@ import pickle import os import random + ### Set seed random.seed(123456) #--------------- Parameters ------------------- + sample_path = str(sys.argv[1]) model_path = str(sys.argv[2]) out_path = str(sys.argv[3]) @@ -21,6 +23,7 @@ majority_voting = bool(sys.argv[6]) #--------------- Data ------------------------- + print('@ READ QUERY') query = pd.read_csv(sample_path, index_col=0, @@ -34,12 +37,13 @@ var = dict(var_names=query.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(query, target_sum=1e4) + #Logarithmize the data: sc.pp.log1p(query) @@ -74,10 +78,12 @@ print('@ DONE') #------------- Other outputs -------------- + ## Save the outputs as .csv print('@ WRITTING CSV OUTPUTS') predictions.to_table(out_other_path) print('@ DONE') + ## Save the output plots print('@ GENERATING OUTPUT PLOTS') predictions.to_plots(out_other_path) From 13c04a0a005626f9a70c53db8f3ccaf66bb10e00 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:19:01 -0400 Subject: [PATCH 122/144] Update predict_SVM.py --- Scripts/SVC/predict_SVM.py | 44 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Scripts/SVC/predict_SVM.py b/Scripts/SVC/predict_SVM.py index 72b501a..5996bc1 100644 --- a/Scripts/SVC/predict_SVM.py +++ b/Scripts/SVC/predict_SVM.py @@ -17,10 +17,18 @@ import pickle import os import random -### Set seed + +# Set seed random.seed(123456) +# Function to get the column name and maximum value for each row +def get_max_column_and_value(row): + pred_label = row.idxmax() + proba_label = row.max() + return pred_label, proba_label + #--------------- Parameters ------------------- + sample_path = str(sys.argv[1]) model_path = str(sys.argv[2]) out_path = str(sys.argv[3]) @@ -30,6 +38,7 @@ tool_name = str(sys.argv[6]) #--------------- Data ------------------------- + print('@ READ QUERY') query = pd.read_csv(sample_path, index_col=0, @@ -43,43 +52,33 @@ var = dict(var_names=query.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(query, target_sum=1e4) + #Logarithmize the data: sc.pp.log1p(query) - # load model print('@ LOAD MODEL') SVM_model = pickle.load(open(model_path, 'rb')) print('@ DONE') -### If the trained model was generated with a diff number of threads and the -## prediction is done with other number +# If the trained model was generated with a diff number of threads and the +# prediction is done with other number SVM_model.n_jobs = threads -## We don't need to calculate again the predict_proba -#pred = SVM_model.predict(query.X) + +# Calculate predicted probability pred_proba = SVM_model.predict_proba(query.X) df = pd.DataFrame(pred_proba,index = query.obs_names,columns = SVM_model.classes_) -# Function to get the column name and maximum value for each row -def get_max_column_and_value(row): - pred_label = row.idxmax() - proba_label = row.max() - return pred_label, proba_label # Create a new column 'max_column' with the column name containing the maximum value for each row df['pred_label'], df['proba_label'] = zip(*df.apply(get_max_column_and_value, axis=1)) -#mm1 = pred.tolist() -#mm = df.max_column.tolist() -# mm1 == mm True -## It's the same - # Create a new column 'unknown_max_column' to store 'max_column' as 'unknown' if 'max_value' is lower than the threshold df['pred_label_reject'] = df.apply(lambda row: 'Unknown' if row['proba_label'] < threshold else row['pred_label'], axis=1) @@ -89,7 +88,8 @@ def get_max_column_and_value(row): print('@ DONE') #------------- Other outputs -------------- -### Save the prob matrix + +# Save the prob matrix print('@ WRITTING PROB MATRIX ') filename = out_other_path + '/prob_matrix.csv' df.to_csv(filename, From ac30121811b9f01c8e957163fb38905b109e22f4 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:21:02 -0400 Subject: [PATCH 123/144] Update train_SVM.py --- Scripts/SVC/train_SVM.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/Scripts/SVC/train_SVM.py b/Scripts/SVC/train_SVM.py index 369c352..7d62e02 100644 --- a/Scripts/SVC/train_SVM.py +++ b/Scripts/SVC/train_SVM.py @@ -5,7 +5,8 @@ @author: tomas.vega """ -## Python 3.11.2 +# Python 3.11.2 + #--------------- Libraries ------------------- import numpy as np import pandas as pd @@ -17,7 +18,8 @@ import pickle import os import random -### Set seed + +# Set seed random.seed(123456) #--------------- Parameters ------------------- @@ -41,6 +43,7 @@ # check if cell names are in the same order in labels and ref order = all(labels.index == ref.index) + # throw error if order is not the same if not order: sys.exit("@ Order of cells in reference and labels do not match") @@ -50,24 +53,24 @@ var = dict(var_names=ref.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(adata, target_sum=1e4) -#Logarithmize the data: -sc.pp.log1p(adata) -## Transform labels to array +# Logarithmize the data: +sc.pp.log1p(adata) +# Transform labels to array label = np.array(labels['label']) #------------- Train SVC ------------- -##kernel could be ‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’ -#When the constructor option probability is set to True, class membership -#probability estimates (from the methods predict_proba and predict_log_proba) -#are enabled. +# kernel could be ‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’ +# When the constructor option probability is set to True, class membership +# probability estimates (from the methods predict_proba and predict_log_proba) +# are enabled. svm_model = SVC(kernel=classifier, probability=True) @@ -76,13 +79,13 @@ calibrated_model.fit(adata.X, label) -#Save the model to disk +# Save the model to disk print('@ SAVE MODEL') pickle.dump(calibrated_model, open(out_path, 'wb')) print('@ DONE') #------------- Other outputs -------------- -### Plot the tree +# Plot the tree filepath = out_other_path + '/train_parameters.csv' print('@ WRITE TRAIN PARAMETERS ') params_table = calibrated_model.get_params() @@ -91,11 +94,11 @@ params_table.to_csv(filepath) print('@ DONE') -## I cannot get the features per class, since: +# I cannot get the features per class, since: # there is attribute coef_ for SVM classifier but it only works for SVM with # linear kernel. For other kernels it is not possible because data are transformed # by kernel method to another space, which is not related to input space # (https://stackoverflow.com/questions/21260691/how-to-obtain-features-weights) # I cannot get the scores since I use the CalibratedClassifierCV too, -#and it doens't have the score attribute. +# and it doens't have the score attribute. From 1673bee53f64d2e5b938c7d77423f982bb5ebc22 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:22:08 -0400 Subject: [PATCH 124/144] Update train_linearSVM.py --- Scripts/SVC/train_linearSVM.py | 36 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Scripts/SVC/train_linearSVM.py b/Scripts/SVC/train_linearSVM.py index 30a215a..73e2231 100644 --- a/Scripts/SVC/train_linearSVM.py +++ b/Scripts/SVC/train_linearSVM.py @@ -6,6 +6,7 @@ @author: tomas.vega """ ## Python 3.11.2 + #--------------- Libraries ------------------- import numpy as np import pandas as pd @@ -17,7 +18,8 @@ import pickle import os import random -### Set seed + +# Set seed random.seed(123456) #--------------- Parameters ------------------- @@ -33,13 +35,14 @@ index_col=0, sep=',', engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies -#ref = ref.T + labels = pd.read_csv(lab_path, index_col = 0, sep=',') # check if cell names are in the same order in labels and ref order = all(labels.index == ref.index) + # throw error if order is not the same if not order: sys.exit("@ Order of cells in reference and labels do not match") @@ -49,13 +52,14 @@ var = dict(var_names=ref.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(adata, target_sum=1e4) -#Logarithmize the data: + +# Logarithmize the data: sc.pp.log1p(adata) ## Transform labels to array @@ -63,24 +67,24 @@ label = np.array(labels['label']) #------------- Train SVM Lineal ------------- -##kernel could be ‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’ -#When the constructor option probability is set to True, class membershipsq -#probability estimates (from the methods predict_proba and predict_log_proba) -#are enabled. +# kernel could be ‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’ +# When the constructor option probability is set to True, class membershipsq +# probability estimates (from the methods predict_proba and predict_log_proba) +# are enabled. svm_model = LinearSVC() + # Calibrate the model using 5-fold cross validation calibrated_model = CalibratedClassifierCV(svm_model, n_jobs = threads) #Default calibrated_model.fit(adata.X, label) - #Save the model to disk print('@ SAVE MODEL') pickle.dump(calibrated_model, open(out_path, 'wb')) print('@ DONE') #------------- Other outputs -------------- -### Plot the tree +# Plot the tree filepath = out_other_path + '/train_parameters.csv' print('@ WRITE TRAIN PARAMETERS ') params_table = calibrated_model.get_params() @@ -89,10 +93,10 @@ params_table.to_csv(filepath) print('@ DONE') -## I cannot get the features per class, since: +# I cannot get the features per class, since: # there is attribute coef_ for SVM classifier but it only works for SVM with # linear kernel. For other kernels it is not possible because data are transformed # by kernel method to another space, which is not related to input space # (https://stackoverflow.com/questions/21260691/how-to-obtain-features-weights) # I cannot get the scores since I use the CalibratedClassifierCV too, -#and it doens't have the score attribute. +# and it doens't have the score attribute. From 7adeaf9c4a60e5579af9362512d8d111d473eb76 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:27:52 -0400 Subject: [PATCH 125/144] Update predict_SciBet.R --- Scripts/SciBet/predict_SciBet.R | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Scripts/SciBet/predict_SciBet.R b/Scripts/SciBet/predict_SciBet.R index 9ddd3a7..54b8d4d 100644 --- a/Scripts/SciBet/predict_SciBet.R +++ b/Scripts/SciBet/predict_SciBet.R @@ -5,8 +5,10 @@ library(WGCNA) library(tidyverse) library(Seurat) library(glue) + set.seed(1234) +#---------- Parameters ------------------- args = commandArgs(trailingOnly = TRUE) query_path = args[1] model_path = args[2] @@ -19,8 +21,9 @@ out_path = dirname(pred_path) # read query matrix and transpose message('@ READ QUERY') -### The matrix of the references is transpose since it needs to be normalize -### with Seurat that expect a genes x cell. + +# The matrix of the references is transpose since it needs to be normalize +# with Seurat that expect a genes x cell. query <- data.table::fread(query_path, data.table=F, header=T, @@ -35,10 +38,9 @@ message('@ LOAD MODEL') load(model_path) message('@ DONE') -### The matrix here is transposed since SciBet expect a cell x gene matrix. +# The matrix here is transposed since SciBet expect a cell x gene matrix. query <- Seurat::NormalizeData(query) %>% as.data.frame() %>% WGCNA::transposeBigData() - #----------- Predict SciBet -------- pred <- Scibet_model(query, @@ -55,12 +57,11 @@ data.table::fwrite(pred_labels, sep = ",", nThread = threads) message('@ DONE') -#---------------------------------------- - #------------- Other outputs -------------- -### I tested and the results are the same when you run the same model twice, -### so I run it again to obtain the prob matrix. + +# I tested and the results are the same when you run the same model twice, +# so I run it again to obtain the prob matrix. pred_matrix <- Scibet_model(query, result = 'table') rownames(pred_matrix) <- rownames(query) @@ -75,4 +76,4 @@ data.table::fwrite(pred_matrix, nThread = threads ) message('@ DONE') -#---------------------------------------- \ No newline at end of file +#---------------------------------------- From a57fd2c805ae1c0c72ed396226b4b46f7f391802 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:28:51 -0400 Subject: [PATCH 126/144] Update train_SciBet.R --- Scripts/SciBet/train_SciBet.R | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Scripts/SciBet/train_SciBet.R b/Scripts/SciBet/train_SciBet.R index 77b9131..c004d04 100644 --- a/Scripts/SciBet/train_SciBet.R +++ b/Scripts/SciBet/train_SciBet.R @@ -5,20 +5,24 @@ library(tidyverse) library(WGCNA) library(Seurat) library(glue) + set.seed(1234) +#---------- Parameters ------------------- args = commandArgs(trailingOnly = TRUE) ref_path = args[1] lab_path = args[2] model_path = args[3] threads = as.numeric(args[4]) out_path = dirname(model_path) + #--------------- Data ------------------- # read reference matrix and transpose message('@ READ REF') -### The matrix of the references is transpose since it needs to be normalize -### with Seurat that expect a genes x cell. + +# The matrix of the references is transpose since it needs to be normalize +# with Seurat that expect a genes x cell. ref <- data.table::fread(ref_path, data.table=F, header=T, @@ -43,10 +47,9 @@ if(!order){ stop("@ Order of cells in reference and labels do not match") } -### The matrix here is transposed since SciBet expect a cell x gene matrix and -### converted to data.frame since labels need to be add. +# The matrix here is transposed since SciBet expect a cell x gene matrix and +# converted to data.frame since labels need to be add. ref <- NormalizeData(ref) %>% as.data.frame() %>% transposeBigData() - ref$label <- labels$label #------------- Train SciBet ------------- @@ -63,7 +66,7 @@ message('@ DONE') #------------- Other outputs -------------- -### Transforming the model into a matrix +# Transforming the model into a matrix Scibet_matrix <- scibet::ExportModel(Scibet_model) %>% as.data.frame() %>% tibble::rownames_to_column(" ") message('@ SAVE MODEL MATRIX') @@ -76,9 +79,10 @@ data.table::fwrite(Scibet_matrix, ) message('@ DONE') -### Taking the selected genes used in the model +# Taking the selected genes used in the model selected_genes <- Scibet_matrix[,1,drop=T] -### Plotting in a marker heatmap + +# Plotting in a marker heatmap message('@ PLOTTING MARKER HEATMAP MATRIX') pdf(glue('{out_path}/selected_markers_per_label.pdf', width = 12, @@ -91,6 +95,7 @@ dev.off() message('@ DONE') df_markers <- marker_plot %>% .$data %>% filter(group == cell_type) %>% select(c('gene','cell_type','zscore')) + # write df_markers message('@ SAVE MARKERS') data.table::fwrite(df_markers, From 1e4a051ced504dfbe1b99d2ea06e18d8c7a9828b Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:30:25 -0400 Subject: [PATCH 127/144] Update run_Tangram.py --- Scripts/Tangram/run_Tangram.py | 39 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/Scripts/Tangram/run_Tangram.py b/Scripts/Tangram/run_Tangram.py index cc6a75f..9b04f1f 100644 --- a/Scripts/Tangram/run_Tangram.py +++ b/Scripts/Tangram/run_Tangram.py @@ -14,7 +14,8 @@ import pandas as pd import sys import random -### Set seed + +# Set seed random.seed(123456) #--------------- Parameters ------------------- @@ -24,12 +25,9 @@ out_other_path = os.path.dirname(str(sys.argv[3])) sp_dataset = str(sys.argv[4]) mode = str(sys.argv[5]) -#if str(sys.argv[5]) != 'None': -# cluster_label = str(sys.argv[5]) -#else: -# cluster_label = None #--------------- Data ------------------------- + # read the data ref = pd.read_csv(ref_path, index_col=0, @@ -42,6 +40,7 @@ # check if cell names are in the same order in labels and ref order = all(labels.index == ref.index) + # throw error if order is not the same if not order: sys.exit("@ Order of cells in reference and labels do not match") @@ -52,19 +51,20 @@ var = dict(var_names=ref.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(ad_sc, target_sum=1e4) -#Logarithmize the data: + +# Logarithmize the data: sc.pp.log1p(ad_sc) # read spatial query ad_sp = sc.read_h5ad(sp_dataset) -### Use only the same genes in the spatial and the query +# Use only the same genes in the spatial and the query tg.pp_adatas(ad_sc, ad_sp, genes=None) if mode == 'clusters': @@ -91,8 +91,6 @@ def get_max_column_and_value(row): # Create a new column 'max_column' with the column name containing the maximum value for each row df['pred_label'], df['score_label'] = zip(*df.apply(get_max_column_and_value, axis=1)) -# Create a new column 'unknown_max_column' to store 'max_column' as 'unknown' if 'max_value' is lower than the threshold -#df['pred_label_reject'] = df.apply(lambda row: 'Unknown' if row['proba_label'] < threshold else row['pred_label'], axis=1) print('@ WRITTING PREDICTIONS') pred_df = pd.DataFrame({'spot': df.index, 'Tangram': df.pred_label}) @@ -100,24 +98,31 @@ def get_max_column_and_value(row): print('@ DONE') #------------- Other outputs -------------- -### Save transfered score matrix + +# Save transfered score matrix print('@ WRITTING TRANSFERED SCORE MATRIX ') + filename = out_other_path + '/label_score_matrix.csv' df.to_csv(filename, index=True) #True because we want to conserve the rownames (cells) + print('@ DONE ') -#### Save map object that contains the cell x spot prob +# Save map object that contains the cell x spot prob print('@ WRITTING OUTPUT OBJECT ') + filename = out_other_path + '/Tangram_mapped_object.h5ad' ad_map.write_h5ad(filename= filename, compression='gzip') + print('@ DONE ') -#### Save cell x spot prob matrix +# Save cell x spot prob matrix print('@ WRITTING PROB MATRIX ') + filename = out_other_path + '/prob_matrix.h5ad' prob_matrix = pd.DataFrame(ad_map.X,index = ad_map.obs_names,columns = ad_map.var_names) prob_matrix.to_csv(filename, index=True) #True because we want to conserve the rownames (cells) + print('@ DONE ') From 877f6950396b949435d14fa3e42077dacf483e72 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:31:31 -0400 Subject: [PATCH 128/144] Update run_scAnnotate.R --- Scripts/scAnnotate/run_scAnnotate.R | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Scripts/scAnnotate/run_scAnnotate.R b/Scripts/scAnnotate/run_scAnnotate.R index cdf0709..377ab88 100644 --- a/Scripts/scAnnotate/run_scAnnotate.R +++ b/Scripts/scAnnotate/run_scAnnotate.R @@ -7,6 +7,7 @@ library(glue) library(scAnnotate) set.seed(1234) +#---------- Parameters ------------------- args = commandArgs(trailingOnly = TRUE) ref_path = args[1] lab_path = args[2] @@ -14,6 +15,7 @@ query_path = args[3] pred_path = args[4] threads = as.numeric(args[5]) threshold = as.numeric(args[6]) + # path for other outputs (depends on tools) out_path = dirname(pred_path) @@ -21,8 +23,9 @@ out_path = dirname(pred_path) # read reference matrix and transpose message('@ READ REF') -### The matrix of the references is transpose since it needs to be normalize -### with Seurat that expect a genes x cell. + +# The matrix of the references is transpose since it needs to be normalize +# with Seurat that expect a genes x cell. ref <- data.table::fread(ref_path, data.table=F, header=T, @@ -50,8 +53,9 @@ if(!order){ # read query matrix and transpose message('@ READ QUERY') -### The matrix of the references is transpose since it needs to be normalize -### with Seurat that expect a genes x cell. + +# The matrix of the references is transpose since it needs to be normalize +# with Seurat that expect a genes x cell. query <- data.table::fread(query_path, data.table=F, header=T, @@ -61,22 +65,23 @@ query <- data.table::fread(query_path, message('@ DONE') -### Query and References should have the same gene features +# Query and References should have the same gene features common.genes <- intersect(rownames(query),rownames(ref)) query <- query[common.genes,] ref <- ref[common.genes,] -### Prepare the reference -## Normalization + +# Prepare the reference: Normalization ref <- NormalizeData(ref) %>% as.data.frame() %>% transposeBigData() -## The label should be in the first column + +# The label should be in the first column ref <- cbind(labels,ref) -## Prepare the query -## Normalization +# Prepare the query: Normalization query <- NormalizeData(query) %>% as.data.frame() %>% transposeBigData() #------------- Train + Predict scAnnotate ------------- -## Auto to automaticly define the correction method needed. + +# Auto to automaticly define the correction method needed. pred <- scAnnotate(train=ref, test=query, distribution="normal", #Default From c5f3c1b43ff3f1f00e2120f53ff2f70d3553a599 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:32:11 -0400 Subject: [PATCH 129/144] Update predict_scHPL.py --- Scripts/scHPL/predict_scHPL.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Scripts/scHPL/predict_scHPL.py b/Scripts/scHPL/predict_scHPL.py index 0065f02..c01e972 100644 --- a/Scripts/scHPL/predict_scHPL.py +++ b/Scripts/scHPL/predict_scHPL.py @@ -15,7 +15,8 @@ import scanpy as sc import pickle import random -### Set seed + +# Set seed random.seed(123456) #--------------- Parameters ------------------- @@ -42,20 +43,20 @@ scHPL_model = pickle.load(open(model_path, 'rb')) print('@ DONE') -### Query preprocessing - +# Query preprocessing query = ad.AnnData(X = query, obs = dict(obs_names=query.index.astype(str)), var = dict(var_names=query.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(query, target_sum=1e4) -#Logarithmize the data: + +# Logarithmize the data: sc.pp.log1p(query) #----------- Predict scHPL -------- From 423d01dfdcd29f741e26cd0e0a385e8ab2f6c33a Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:33:19 -0400 Subject: [PATCH 130/144] Update train_scHPL.py --- Scripts/scHPL/train_scHPL.py | 52 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Scripts/scHPL/train_scHPL.py b/Scripts/scHPL/train_scHPL.py index 37a7d39..4d053d8 100644 --- a/Scripts/scHPL/train_scHPL.py +++ b/Scripts/scHPL/train_scHPL.py @@ -20,7 +20,7 @@ import os import random -### Set seed +# Set seed random.seed(123456) #--------------- Parameters ------------------- @@ -37,13 +37,14 @@ index_col=0, sep=',', engine='c') ## could be pyarrow to make it faster but it needs to be install on the module and has many problems with dependencies -#ref = ref.T + labels = pd.read_csv(lab_path, index_col = 0, sep=',') # check if cell names are in the same order in labels and ref order = all(labels.index == ref.index) + # throw error if order is not the same if not order: sys.exit("@ Order of cells in reference and labels do not match") @@ -53,33 +54,33 @@ var = dict(var_names=ref.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization -#1e4 similar as Seurat +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization +# 1e4 similar as Seurat sc.pp.normalize_total(adata, target_sum=1e4) -#Logarithmize the data: + +# Logarithmize the data: sc.pp.log1p(adata) -### A tree could be use an input, in that case the read_tree can be used. -### The tree format is Newick formatted file -#tree = utils.read_tree(treePath) +# A tree could be use an input, in that case the read_tree can be used. +# The tree format is Newick formatted file +# tree = utils.read_tree(treePath) -## They provided this solution here: https://github.com/lcmmichielsen/scHPL/issues/7 +# They provided this solution here: https://github.com/lcmmichielsen/scHPL/issues/7 tree = utils.create_tree('root') tree = learn._construct_tree(tree, labels) - #------------- Train scHPL ------------- -##classifier could be svm, svm_occ, knn -#- the linear SVM when your integrated data still has a lot of -#dimensions (e.g. when you have used Seurat to integrate the datasets) -#- the kNN when your integrated data has less, 10-50, dimensions -#(e.g. when you have used scVI or Harmony to integrate the datasets) -#- the one-class SVM when your main focus is to find unseen cell -#populations. A downside of the one-class SVM, however, is that the -#classification performance drops. +# classifier could be svm, svm_occ, knn +# - the linear SVM when your integrated data still has a lot of +# dimensions (e.g. when you have used Seurat to integrate the datasets) +# - the kNN when your integrated data has less, 10-50, dimensions +# (e.g. when you have used scVI or Harmony to integrate the datasets) +# - the one-class SVM when your main focus is to find unseen cell +# populations. A downside of the one-class SVM, however, is that the +# classification performance drops. tree = train.train_tree(adata.X, labels.label, tree, @@ -88,17 +89,16 @@ useRE = True, FN = 0.5) -#Save the model to disk +# Save the model to disk print('@ SAVE MODEL') pickle.dump(tree, open(out_path, 'wb')) print('@ DONE') #------------- Other outputs -------------- -### Plot the tree - -#I'm using this method since they provided it here: -#https://github.com/lcmmichielsen/scHPL/issues/5 +# Plot the tree +# I'm using this method since they provided it here: +# https://github.com/lcmmichielsen/scHPL/issues/5 def _print_node(node, hor, ver_steps, fig, new_nodes): global ver # Add horizontal line From b745edc07366420c52c10e45e46ea2d724240aba Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:34:16 -0400 Subject: [PATCH 131/144] Update run_scID.R --- Scripts/scID/run_scID.R | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Scripts/scID/run_scID.R b/Scripts/scID/run_scID.R index 2ecf901..4bc63d5 100644 --- a/Scripts/scID/run_scID.R +++ b/Scripts/scID/run_scID.R @@ -8,12 +8,14 @@ library(scID) library(MAST) set.seed(1234) +#---------- Parameters ------------------- args = commandArgs(trailingOnly = TRUE) ref_path = args[1] lab_path = args[2] query_path = args[3] pred_path = args[4] threads = as.numeric(args[5]) + # path for other outputs (depends on tools) out_path = dirname(pred_path) @@ -21,8 +23,9 @@ out_path = dirname(pred_path) # read reference matrix and transpose message('@ READ REF') -### The matrix of the references is transpose since it needs to be normalize -### with Seurat that expect a genes x cell. + +# The matrix of the references is transpose since it needs to be normalize +# with Seurat that expect a genes x cell. ref <- data.table::fread(ref_path, data.table=F, header=T, @@ -50,8 +53,9 @@ if(!order){ # read query matrix and transpose message('@ READ QUERY') -### The matrix of the references is transpose since it needs to be normalize -### with Seurat that expect a genes x cell. + +# The matrix of the references is transpose since it needs to be normalize +# with Seurat that expect a genes x cell. query <- data.table::fread(query_path, data.table=F, header=T, @@ -61,14 +65,13 @@ query <- data.table::fread(query_path, message('@ DONE') -### Prepare the reference -## Normalization +# Prepare the reference: Normalization ref <- scID:::counts_to_cpm(counts_gem = ref) -## Labels as named vector +# Labels as named vector label <- setNames(object = labels$label,nm = rownames(labels)) -## Prepare the query -## Normalization + +# Prepare the query: Normalization query <- scID:::counts_to_cpm(counts_gem = query) @@ -96,7 +99,7 @@ data.table::fwrite(pred_labels, message('@ DONE') #------------- Other outputs -------------- -#Save the entire output +# Save the entire output message('@ SAVE scID OUTPUT') save(pred, file = glue('{out_path}/scID_output.Rdata') From 42c0d47ea9fa857117ffe0d847fcba3cf3b77112 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:34:55 -0400 Subject: [PATCH 132/144] Update predict_scLearn.R --- Scripts/scLearn/predict_scLearn.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Scripts/scLearn/predict_scLearn.R b/Scripts/scLearn/predict_scLearn.R index 8dfe62e..2c839af 100644 --- a/Scripts/scLearn/predict_scLearn.R +++ b/Scripts/scLearn/predict_scLearn.R @@ -5,12 +5,12 @@ library(scLearn) library(glue) set.seed(1234) +#---------- Parameters ------------------- args = commandArgs(trailingOnly = TRUE) query_path = args[1] model_path = args[2] pred_path = args[3] threads = as.numeric(args[4]) -# species = args[5] # species="Hs" for homo sapiens or species="Mm" for mus musculus. # path for other outputs (depends on tools) out_path = dirname(pred_path) @@ -60,7 +60,8 @@ data.table::fwrite(pred_labs, nThread = threads) message('@ DONE') #---------------------------------------- -### Add the entire output with the Scores in Additional_information + +# Add the entire output with the Scores in Additional_information message('@ WRITE TABLE OUTPUT WITH ALL PROBABILITIES') data.table::fwrite(scLearn_predict_result, file = glue('{out_path}/table_with_probabilities.csv'), @@ -68,4 +69,4 @@ data.table::fwrite(scLearn_predict_result, col.names = T, sep = ",", nThread = threads) -message('@ DONE') \ No newline at end of file +message('@ DONE') From 66c5a9fbe8abe2fa71568f60b96aedfd3aa94137 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:35:34 -0400 Subject: [PATCH 133/144] Update train_scLearn.R --- Scripts/scLearn/train_scLearn.R | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Scripts/scLearn/train_scLearn.R b/Scripts/scLearn/train_scLearn.R index afaa463..2d19c93 100644 --- a/Scripts/scLearn/train_scLearn.R +++ b/Scripts/scLearn/train_scLearn.R @@ -6,12 +6,12 @@ library(Seurat) library(glue) set.seed(1234) +#---------- Parameters ------------------- args = commandArgs(trailingOnly = TRUE) ref_path = args[1] lab_path = args[2] model_path = args[3] threads = as.numeric(args[4]) -#species = args[5] # species="Hs" for homo sapiens or species="Mm" for mus musculus. # path for other outputs (depends on tools) out_path = dirname(model_path) @@ -44,14 +44,10 @@ labels <- setNames(nm=rownames(labels), # Preprocessing # transpose reference so cells are on columns -## Because we don't want to filter any cell, we will do only the normalization as they do it. -### They do manually the Seurat normalization. -# ref <-apply(ref,2,function(x){x/(sum(x)/10000)}) -# ref <- log(ref+1) - +# Because we don't want to filter any cell, we will do only the normalization as they do it. ref <- ref %>% WGCNA::transposeBigData() %>% Seurat::NormalizeData() -### Select genes +# Select genes high_varGene_names <- Feature_selection_M3Drop(expression_profile = ref, log_normalized = T #True because it was previously normalized ) From 48bd2913bb6806cc790ebd59a6ff21ef9f515230 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:37:55 -0400 Subject: [PATCH 134/144] Update run_scNym.py --- Scripts/scNym/run_scNym.py | 42 +++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/Scripts/scNym/run_scNym.py b/Scripts/scNym/run_scNym.py index 2548065..d34690f 100644 --- a/Scripts/scNym/run_scNym.py +++ b/Scripts/scNym/run_scNym.py @@ -6,7 +6,8 @@ import scanpy as sc import os import random -### Set seed + +# Set seed random.seed(123456) #--------------- Parameters ------------------- @@ -30,6 +31,7 @@ # check if cell names are in the same order in labels and ref order = all(labels.index == ref.index) + # throw error if order is not the same if not order: sys.exit("@ Order of cells in reference and labels do not match") @@ -43,43 +45,48 @@ print('@ DONE') #------------- Preparing input ------------- -## Normalizing reference + +#Normalizing reference ad_ref = ad.AnnData(X = ref, obs = dict(obs_names=ref.index.astype(str), label = labels['label']), var = dict(var_names=ref.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization. +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization. sc.pp.normalize_total(ad_ref, target_sum=1e6) -#Logarithmize the data: + +# Logarithmize the data: sc.pp.log1p(ad_ref) sc.pp.filter_genes(ad_ref, min_cells=10) -## Normalizing query +# Normalizing query ad_query = ad.AnnData(X = query, obs = dict(obs_names=query.index.astype(str)), var = dict(var_names=query.columns.astype(str)) ) -## Now I normalize the matrix with scanpy: -#Normalize each cell by total counts over all genes, -#so that every cell has the same total count after normalization. -#If choosing `target_sum=1e6`, this is CPM normalization +# Now I normalize the matrix with scanpy: +# Normalize each cell by total counts over all genes, +# so that every cell has the same total count after normalization. +# If choosing `target_sum=1e6`, this is CPM normalization sc.pp.normalize_total(ad_query, target_sum=1e6) -#Logarithmize the data: + +# Logarithmize the data: sc.pp.log1p(ad_query) sc.pp.filter_genes(ad_query, min_cells=10) + # Any cell with the annotation "Unlabeled" will be treated as part of the target # dataset and used for semi-supervised and adversarial training. ad_query.obs["label"] = "Unlabeled" #------------- Train scNym ------------- -## Contatenate + +# Contatenate adata = ad_ref.concatenate(ad_query) file_model = out_other_path + '/model' scnym_api( @@ -106,7 +113,7 @@ def remove_batch(cell_id, batch): adata.obs['cell_id'] = adata.obs.index.map(lambda cell_id: remove_batch(cell_id, adata.obs.loc[cell_id, 'batch'])) -## Get only the query +# Get only the query df = adata.obs[adata.obs["label"] == "Unlabeled"] print('@ WRITTING PREDICTIONS') pred_df = pd.DataFrame({'cell': df.cell_id, "scNym": df.pred_label_reject}) @@ -114,14 +121,15 @@ def remove_batch(cell_id, batch): print('@ DONE') #------------- Other outputs -------------- -### Save data.frame output + +# Save data.frame output print('@ WRITTING DATA FRAME OUTPUT ') filename = out_other_path + '/whole_df_output.csv' df.to_csv(filename, index=True) #True because we want to conserve the rownames (cells) print('@ DONE ') -#### Save map object that contains the cell x spot prob +# Save map object that contains the cell x spot prob print('@ WRITTING OUTPUT OBJECT ') filename = out_other_path + '/scNym_output_object.h5ad' adata.write_h5ad(filename= filename, From 40e0453e062049928b197745503360ff74815fd4 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:13:55 -0400 Subject: [PATCH 135/144] Update README.md --- README.md | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 272e07d..b52f9e2 100644 --- a/README.md +++ b/README.md @@ -348,13 +348,36 @@ Add table with resource usage for different sice references and queries # :woman_mechanic: Adding new tools -``` -# UPDATE -``` +1. Identify new tool + +2. Read documentation. Try to find this information: +- Can tool be split into training and prediction step? +- What normalization does the tool expect for the reference and query? +- Can the tool be paralellized? How? +- Does the tool expect the exact same set of features in the reference and the query? +- Are there extra outputs from the training and prediction that may be usefull to save (qc plots, probbability/p-value associated with each prediction etc..)? +- Are there parameters in the training and prediction that can be changed? What are the defult values? + +3. Open issue and create a branch from dev with the tool name + your name + +4. Write scripts (check Templats folder for how to structure scripts: [Templats](Templats)) +- The scripts should take the reference expression/labels and query expression .csv files as specified in [Prepare reference](#2-prepare-reference) and [Prepare query samples](#3-prepare-query-samples) +- The scripts should take any additional parameters you want to be able to change as input + +5. Update the snakefiles with rules for the new tool (ask Alva if you need help) + +6. Update README +- Write detailed documentation about the tool in the section: [Detailed documentation on tool wrapper scripts](female-detective-detailed-documentation-on-tool-wrapper-scripts) +- Detailed documentation should include information from step 2 and if you changed any default parameters/normalization etc.. Links to papers and tutorials used to create the scripts can be put here. +- Update other sections of the readme such as the [Installation and Dependencies](gear-installation-and-dependencies) and [Available tools](hammer-and-wrench-available-tools) + +7. Create pull request from your branch to dev and request reviewer + +8. Make sure module on Narval/Hydra gets updated with the new tool packages # 🐍 Snakemake Tips and Tricks -- Dryrun snakemake pipeline before submitting job +- Dryrun snakemake pipeline before submitting job ```bash snakemake -s ${snakefile} --configfile ${config} -n ``` From 732bf34dc08503253aad22d3e89afd86b8c701bf Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:35:49 -0400 Subject: [PATCH 136/144] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b52f9e2..048ded4 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,10 @@ extra_config= snakemake -s ${snakefile} --configfile ${config} ${extra_config} --cores 5 ``` +# Outputs + +ADD DESCRIPTION OF OUTPUTS + # :gear: Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. @@ -373,7 +377,7 @@ Add table with resource usage for different sice references and queries 7. Create pull request from your branch to dev and request reviewer -8. Make sure module on Narval/Hydra gets updated with the new tool packages +8. Make sure module on Narval/Hydra gets updated with necessary packages # 🐍 Snakemake Tips and Tricks From bfcbfa88d9433d715d326fd45a46db4c76280450 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:37:28 -0400 Subject: [PATCH 137/144] Update calculate_consensus.R --- Scripts/calculate_consensus.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/calculate_consensus.R b/Scripts/calculate_consensus.R index 205e0e4..480f7aa 100644 --- a/Scripts/calculate_consensus.R +++ b/Scripts/calculate_consensus.R @@ -20,7 +20,7 @@ print(consensus_tools) harmonize_unsure = function(pred, ref_labels){ pred %>% column_to_rownames('cellname') %>% - mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label), ., 'Unsure'))) %>% + mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label), ., 'Unresolved'))) %>% rownames_to_column('cellname') %>% return() } From 8130267bf8572af72e2d935f8cc487b7127117d7 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:40:08 -0400 Subject: [PATCH 138/144] Update annotate_report.Rmd Chnaged Unsure to Unresolved --- Notebooks/annotate_report.Rmd | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Notebooks/annotate_report.Rmd b/Notebooks/annotate_report.Rmd index f352458..f72ffe0 100644 --- a/Notebooks/annotate_report.Rmd +++ b/Notebooks/annotate_report.Rmd @@ -43,11 +43,11 @@ marker_genes = strsplit(params$marker_genes, split = ' ')[[1]] ```{r} # function converts everything that is not in reference labels -# to 'Unsure' -harmonize_unsure = function(pred, ref_labels){ +# to 'Unresolved' +harmonize_unresolved = function(pred, ref_labels){ pred %>% column_to_rownames('cellname') %>% - mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label, 'No Consensus'), ., 'Unsure'))) %>% + mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label, 'No Consensus'), ., 'Unresolved'))) %>% return() } @@ -71,7 +71,7 @@ plot_tool_correlation_heatmap = function(seurat, tools){ select(all_of(tools)) %>% rownames_to_column('cell') %>% pivot_longer(!cell) %>% - filter(value %in% c('Unsure', 'No Consensus')) %>% + filter(value %in% c('Unresolved', 'No Consensus')) %>% dplyr::count(name, .drop = F) %>% mutate(freq = round(n/nrow(query@meta.data)*100)) %>% select(!n) %>% @@ -80,7 +80,7 @@ plot_tool_correlation_heatmap = function(seurat, tools){ count[setdiff(names(seurat@meta.data %>% select(tools)), rownames(count)),] = 0 count = count[order(match(rownames(count), colnames(mat))), , drop = FALSE] - ha = columnAnnotation('% Unsure/No Consensus' = anno_barplot(count, border = F, gp = gpar(fill = '#596475', col = '#596475'))) + ha = columnAnnotation('% Unresolved/No Consensus' = anno_barplot(count, border = F, gp = gpar(fill = '#596475', col = '#596475'))) h = ComplexHeatmap::Heatmap(mat, name = 'Correlation', @@ -96,7 +96,7 @@ plot_tool_correlation_heatmap = function(seurat, tools){ create_color_pal = function(class, mb = 'Juarez'){ pal = sample(met.brewer(mb, length(class))) names(pal) = class - pal['Unsure'] = 'lightgrey' + pal['Unresolved'] = 'lightgrey' pal['No Consensus'] = 'grey' return(pal) } From 11cc856863bf27e40bc21951129706c3db5d0c32 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:41:46 -0400 Subject: [PATCH 139/144] Update annotate_report.Rmd --- Notebooks/annotate_report.Rmd | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Notebooks/annotate_report.Rmd b/Notebooks/annotate_report.Rmd index f72ffe0..d44e134 100644 --- a/Notebooks/annotate_report.Rmd +++ b/Notebooks/annotate_report.Rmd @@ -42,15 +42,6 @@ marker_genes = strsplit(params$marker_genes, split = ' ')[[1]] ``` ```{r} -# function converts everything that is not in reference labels -# to 'Unresolved' -harmonize_unresolved = function(pred, ref_labels){ - pred %>% - column_to_rownames('cellname') %>% - mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label, 'No Consensus'), ., 'Unresolved'))) %>% - return() -} - plot_tool_correlation_heatmap = function(seurat, tools){ mat = query@meta.data %>% From 847a6aa2a0052f23cd165f46f9837289c286af88 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:42:27 -0400 Subject: [PATCH 140/144] Update calculate_consensus.R --- Scripts/calculate_consensus.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Scripts/calculate_consensus.R b/Scripts/calculate_consensus.R index 480f7aa..31e5133 100644 --- a/Scripts/calculate_consensus.R +++ b/Scripts/calculate_consensus.R @@ -17,7 +17,7 @@ if(consensus_tools[1] == 'all'){ print(consensus_tools) -harmonize_unsure = function(pred, ref_labels){ +harmonize_unresolved = function(pred, ref_labels){ pred %>% column_to_rownames('cellname') %>% mutate(across(where(is.character), ~ifelse(. %in% c(ref_labels$label), ., 'Unresolved'))) %>% @@ -53,7 +53,7 @@ for(f in files){ consensus = l %>% reduce(left_join, by = "cell") %>% rename('cellname' = 'cell') rm(l) -tmp = harmonize_unsure(consensus, ref_labels) +tmp = harmonize_unresolved(consensus, ref_labels) tmp = tmp %>% select(all_of(consensus_tools)) consensus$Consensus = apply(tmp, 1, getmode) rm(tmp) From a7a4bf418b8f0cd47685291ccd051a69ccce2e78 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:47:48 -0400 Subject: [PATCH 141/144] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 048ded4..2b5fb21 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # scCoAnnotate -# Summary - Snakemake pipeline for consensus prediction of cell types in single-cell RNA sequencing (scRNA-seq) data. The pipeline allows users to run up to 15 different reference-based annotation tools (statistical models and machine learning approaches) to predict cell type labels of multiple scRNA-seq samples. It then outputs a consensus of the predictions, which has been found to have increased accuracy in benchmarking experiments compared to the individual predictions alone, by combining the strengths of the different approaches. The pipeline is automated and running it does not require prior knowledge of machine learning. It also features parallelization options to exploit available computational resources for maximal efficiency. This pipeline trains classifiers on genes common to the reference and all query datasets. @@ -245,7 +243,7 @@ snakemake -s ${snakefile} --configfile ${config} ${extra_config} --cores 5 ADD DESCRIPTION OF OUTPUTS -# :gear: Installation and Dependencies +# :package: Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. From e5525a263f6d131505e39ae0eb33923ea9e71331 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:48:41 -0400 Subject: [PATCH 142/144] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b5fb21..65536b6 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ snakemake -s ${snakefile} --configfile ${config} ${extra_config} --cores 5 ADD DESCRIPTION OF OUTPUTS -# :package: Installation and Dependencies +# :gear: Installation and Dependencies This tool has been designed and tested for use on a high-performance computing cluster (HPC) with a SLURM workload manager. From 9272fc4170a87cac295d407c3292b0998a3c51c5 Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:53:49 -0400 Subject: [PATCH 143/144] Update README.md --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 65536b6..e925afb 100644 --- a/README.md +++ b/README.md @@ -350,9 +350,9 @@ Add table with resource usage for different sice references and queries # :woman_mechanic: Adding new tools -1. Identify new tool +**1. Identify new tool** -2. Read documentation. Try to find this information: +**2. Read documentation. Try to find this information:** - Can tool be split into training and prediction step? - What normalization does the tool expect for the reference and query? - Can the tool be paralellized? How? @@ -360,22 +360,26 @@ Add table with resource usage for different sice references and queries - Are there extra outputs from the training and prediction that may be usefull to save (qc plots, probbability/p-value associated with each prediction etc..)? - Are there parameters in the training and prediction that can be changed? What are the defult values? -3. Open issue and create a branch from dev with the tool name + your name +**3. Open issue and create a branch from dev with the tool name + your name** -4. Write scripts (check Templats folder for how to structure scripts: [Templats](Templats)) +**4. Write scripts (check Templats folder for how to structure scripts: [Templats](Templats))** - The scripts should take the reference expression/labels and query expression .csv files as specified in [Prepare reference](#2-prepare-reference) and [Prepare query samples](#3-prepare-query-samples) - The scripts should take any additional parameters you want to be able to change as input -5. Update the snakefiles with rules for the new tool (ask Alva if you need help) +**5. Update the snakefiles with rules for the new tool (ask Alva if you need help)** -6. Update README +**6. Update README** - Write detailed documentation about the tool in the section: [Detailed documentation on tool wrapper scripts](female-detective-detailed-documentation-on-tool-wrapper-scripts) - Detailed documentation should include information from step 2 and if you changed any default parameters/normalization etc.. Links to papers and tutorials used to create the scripts can be put here. - Update other sections of the readme such as the [Installation and Dependencies](gear-installation-and-dependencies) and [Available tools](hammer-and-wrench-available-tools) -7. Create pull request from your branch to dev and request reviewer +**7. Create pull request from your branch to dev and request reviewer** -8. Make sure module on Narval/Hydra gets updated with necessary packages +**8. When pull request is approved merge with dev** +- Rebase with dev +- Squash + merge + +**9. Make sure module on Narval/Hydra gets updated with necessary packages** # 🐍 Snakemake Tips and Tricks From 6e5ea35fb4df28d5f3236cbbf8a9aaf904866a4f Mon Sep 17 00:00:00 2001 From: alvaannett <46521652+alvaannett@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:14:16 -0400 Subject: [PATCH 144/144] Update README.md --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e925afb..9066f90 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,27 @@ snakemake -s ${snakefile} --configfile ${config} ${extra_config} --cores 5 # Outputs -ADD DESCRIPTION OF OUTPUTS +## Output folder structure + +In each output folder there will be one folder per sample as well as a folder for the models. In each sample folder and in the model folder there will be subfolders for each reference specified in the config file. Each sample folder will also contain a reports folder with a `.html` report with the prediction results. Each refrence folder contains sub folders for the individual tools model and prediction results. + +``` +out/ +├── sample1 +│   ├── reference1 +│   ├── reference2 +│   └── report +├── sample1 +│   ├── reference1 +│   ├── reference2 +│   └── report +└── model + ├── reference1 + └── reference2 +``` + +## Output files + # :gear: Installation and Dependencies