Hidden Factor Analysis#

Infers hidden factors - latent technical and biological covariates - from a molecular phenotype matrix.

Overview#

Molecular phenotype matrices carry structure nobody recorded: batch, cell-type composition, unmeasured technical variation. Left in, that structure inflates false positives in the association scan, because a variant that happens to track a batch looks like a QTL. This step estimates those hidden factors from the data and adds them to the known covariates, so the scan can regress them out.

Every method here follows the same shape: residualise the phenotype on the covariates you already have, estimate factors from what is left, then stack the chosen factors back on top of the known covariates. They differ only in how factors are estimated and how many are kept.

When to run it. After known covariates are merged and before the association scan, on the phenotype matrix the scan will use.

Input#

  • --phenoFile: the molecular phenotype matrix in UCSC bed.gz form, one row per feature and one column per sample. Example output/rnaseq/protocol_example.rnaseq.bed.bed.gz.

  • --covFile: the merged known-covariate matrix, an #id header plus one column per sample, as written by covariate_formatting. Example output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz:

    #id  SAMPLE_001  SAMPLE_002  SAMPLE_003
    sex  1           1           1
    age  90.97       80.24       83.9
    PC1  -0.4525795  -0.0450112  0.0704428
    

    The covariate file is optional for PEER but recommended, so that a proper xQTL model is built. BiCV additionally requires an indexed VCF, which the workflow generates for itself.

  • --N: how many hidden factors to keep. 0, the default, lets the method choose: Marchenko_PC keeps components above the Marchenko-Pastur bound and PCA uses the Buja and Eyuboglu permutation. Give a positive number to fix the count instead, which PEER needs.

  • --choose_k_method: Marchenko or the permutation alternative, selecting which rule the shared PCA step applies. It also names the output file.

  • --mean-impute-missing: off by default. When set, missing phenotype values are mean-imputed rather than the feature being dropped.

  • --cwd: the directory outputs are written to. Must be a full path.

Output#

  • {cwd}/{name}.{choose_k_method}_PC.gz - the deliverable: the known covariates with the inferred Hidden_Factor_PC* rows stacked underneath, ready for the association scan. {name} is the phenotype file stem joined to the covariate file stem. Example ...prune.pca.Marchenko_PC.gz has 60 columns and 26 rows, being the header plus 2 known covariates plus the 15 genotype PCs plus the 8 hidden factors this method kept:

    #id                SAMPLE_001            SAMPLE_002            SAMPLE_003
    sex                1                     1                     1
    age                90.97                 80.24                 83.9
    PC1                -0.4525795337766864   -0.04501122972985456  0.07044283190889761
    PC2                0.04906817968399002   -0.1674973221595418   -0.0041635808141029
    ...
    Hidden_Factor_PC8  ...                   ...                   ...
    
  • {cwd}/{name}.residual.bed.gz - the residualised phenotype matrix produced by the shared first step and consumed by whichever method follows, written with a .tbi index.

  • {cwd}/{name}.residual.PEER_MODEL.hd5 - the fitted PEER model, from PEER only.

  • {cwd}/{name}.residual.PEER.gz and {cwd}/{name}.residual.PEER.diag.pdf - the PEER factors and the convergence diagnostic plot.

  • {cwd}/{name}.residual.bed.fake.vcf.gz - the indexed VCF BiCV builds for APEX, an intermediate rather than a result.

  • {cwd}/{name}.residual.BiCV.gz - the BiCV factors.

Minimal Working Example#

Four alternative methods, each a chain that starts from the same residualisation step and then estimates hidden factors its own way. Pick one; they are not run in sequence.

PEER (GTEx-style)#

PEER infers hidden factors using a MOFA-based probabilistic model (Stegle et al.). The number of factors follows GTEx recommendations based on sample size, or can be fixed with --N. This method runs natively here (it uses the mofapy2 Python package), so the example below is fully runnable on the toy data and produces the factor matrix plus a convergence-diagnostic PDF.

Timing: TBD (on toy dataset)

sos run pipeline/covariate_hidden_factor.ipynb PEER \
    --cwd output/covariate \
    --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \
    --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \
    --N 3

PCA with Buja & Eyuboglu permutation#

This is the same PCA workflow as above (PCA), but the number of components to keep is chosen by the Buja & Eyuboglu permutation procedure instead of the Marchenko-Pastur threshold. Set --choose_k_method Buja_Eyuboglu. This route uses jackstraw::permutationPA (B=100 permutations), so it runs a little longer than the Marchenko variant but is fully runnable on the toy data.

Timing: TBD (on toy dataset)

sos run pipeline/covariate_hidden_factor.ipynb PCA \
    --cwd output/covariate \
    --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \
    --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \
    --choose_k_method Marchenko \
    --mean-impute-missing

BiCV factor analysis with APEX#

BiCV chooses the number of hidden factors by bi-cross-validation using the APEX software (Owen et al. 2016). The pipeline computes residuals on the merged covariates, builds the inputs APEX expects (an indexed VCF placeholder and a phenotype matrix), then calls apex factor. The number of factors follows GTEx recommendations or can be fixed with --N.

Timing: TBD (on toy dataset)

sos run pipeline/covariate_hidden_factor.ipynb BiCV \
    --cwd output/covariate \
    --phenoFile output/rnaseq/protocol_example.rnaseq.bed.bed.gz \
    --covFile output/covariate/protocol_example.covariates.protocol_example.genotype.merged.plink_qc.plink_qc.prune.pca.gz \
    --N 3

Command Interface#

sos run pipeline/covariate_hidden_factor.ipynb -h
usage: sos run pipeline/covariate_hidden_factor.ipynb
               [workflow_name | -t targets] [options] [workflow_options]
  workflow_name:        Single or combined workflows defined in this script
  targets:              One or more targets to generate
  options:              Single-hyphen sos parameters (see "sos run -h" for details)
  workflow_options:     Double-hyphen workflow-specific parameters

Workflows:
  Marchenko_PC
  PCA
  PEER
  BiCV

Global Workflow Options:
  --modular-script-dir code/script (as path)
  --cwd output (as path)
                        The output directory for generated files. MUST BE FULL
                        PATH
  --covFile VAL (as path, required)
                        Merged Covariates File
  --phenoFile VAL (as path, required)
                        Path to the input molecular phenotype data.
  --name  f'{phenoFile:bnn}.{covFile:bn}'

  --job-size 1 (as int)
                        For cluster jobs, number commands to run per job
  --walltime 5h
                        Wall clock time expected
  --mem 16G
                        Memory expected
  --numThreads 8 (as int)
                        Number of threads
  --container ''
                        Software container option
  --entrypoint ''

Sections
  *_1:
    Workflow Options:
      --[no-]mean-impute-missing (default to False)
  Marchenko_PC_2, PCA_2:
    Workflow Options:
      --choose-k-method Marchenko
                        Marchenko or Marchenko
      --N 0 (as int)
  PEER_2:
    Workflow Options:
      --N 0 (as int)
                        N PEER factors, If do not specify or specified as 0,
                        default values suggested by GTEx (based on different
                        sample size) will be used
      --iteration 1000 (as int)
                        Default values from PEER software: The number of max
                        iteration
      --tol 0.001 (as float)
                        Prior parameters parameter: Alpha_a = 0.001 parameter:
                        Alpha_b = 0.1 parameter: Eps_a = 0.1 parameter: Eps_b =
                        10.0 Tolarance parameters
      --[no-]r2-tol (default to False)
                        parameter: var_tol = 0.00001 minimum variance explained
                        criteria to drop factors while training
      --convergence-mode fast
                        Convergence mode: Convergence mode for MOFAr "slow",
                        "medium" or "fast", corresponding to 1e-5%, 1e-4% or
                        1e-3% deltaELBO change.
  PEER_3:
  BiCV_2:
  BiCV_3:
    Workflow Options:
      --N 0 (as int)
                        N factors, if not specify, calculated based on sample
                        size according to GTeX
      --iteration 10 (as int)
                        The number of iteration

Workflow implementation#

[global]
parameter: modular_script_dir = path('code/script')  # override with --modular-script-dir
# The output directory for generated files. MUST BE FULL PATH
parameter: cwd = path("output")
# Merged Covariates File
parameter: covFile = path
# Path to the input molecular phenotype data.
parameter: phenoFile = path
parameter: name = f'{phenoFile:bnn}.{covFile:bn}'
# For cluster jobs, number commands to run per job
parameter: job_size = 1
# Wall clock time expected
parameter: walltime = "5h"
# Memory expected
parameter: mem = "16G"
# Number of threads
parameter: numThreads = 8
# Software container option
parameter: container = ""
parameter: entrypoint= ""
[*_1(computing residual on merged covariates)]
parameter: mean_impute_missing = False
input: phenoFile, covFile
output: f'{cwd}/{name}.residual.bed.gz'
task: trunk_workers = 1, trunk_size = job_size, walltime = walltime,  mem = mem, tags = f'{step_name}_{_output:bn}'
bash: expand= "${ }", stderr = f'{_output:nn}.stderr', stdout = f'{_output:nn}.stdout', container = container, entrypoint = entrypoint
    Rscript ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor.R \
        --step compute_residual \
        --cwd "${cwd}" \
        --phenoFile "${phenoFile}" \
        --covFile "${covFile}" \
        --output "${_output}" \
        ${"--mean-impute-missing" if mean_impute_missing else ""} \
        --numThreads ${numThreads}

Principal Components Analysis on molecular phenotype matrix#

[Marchenko_PC_2, PCA_2]
# Marchenko or Marchenko
parameter: choose_k_method = "Marchenko"
parameter: N = 0
output: f'{cwd}/{_input:bnnn}.{choose_k_method}_PC.gz'
task: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'
bash: expand= "${ }", stderr = f'{_output:n}.stderr', stdout = f'{_output:n}.stdout', container = container, entrypoint = entrypoint
    Rscript ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor.R \
        --step Marchenko_PC \
        --cwd "${cwd}" \
        --residFile "${_input}" \
        --covFile "${covFile}" \
        --choose-k-method "${choose_k_method}" \
        --output "${_output}" \
        --N ${N} \
        --numThreads ${numThreads}

PEER Method#

[PEER_2]
# N PEER factors, If do not specify or specified as 0, default values suggested by 
# GTEx (based on different sample size) will be used
parameter: N = 0
# Default values from PEER software:
## The number of max iteration
parameter: iteration = 1000
### Prior parameters
#parameter: Alpha_a = 0.001
#parameter: Alpha_b = 0.1
#parameter: Eps_a = 0.1
#parameter: Eps_b = 10.0
# Tolarance parameters
parameter: tol = 0.001
#parameter: var_tol = 0.00001
# minimum variance explained criteria to drop factors while training
parameter: r2_tol = False
# Convergence mode: Convergence mode for MOFAr "slow", "medium" or "fast", corresponding to 1e-5%, 1e-4% or 1e-3% deltaELBO change.
parameter: convergence_mode = "fast"
# input is the residual file from the first step
output: f'{cwd:a}/{_input:bnn}.PEER_MODEL.hd5'
task: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout', container = container, entrypoint = entrypoint
    python ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor_peer.py \
        --resid-file "${_input}" \
        --model-file "${_output}" \
        --factors-out "${cwd:a}/${_input:bnn}.PEER.factors.tsv" \
        --weights-out "${cwd:a}/${_input:bnn}.PEER.weights.tsv" \
        --variance-out "${cwd:a}/${_input:bnn}.PEER.variance.tsv" \
        --num-factor ${N} \
        --iteration ${iteration} \
        --convergence-mode "${convergence_mode}" \
        --num-threads ${numThreads} \
        --tol ${tol} \
        --r2-tol ${r2_tol}
[PEER_3]
output: f'{cwd:a}/{_input:bnn}.PEER.gz', f'{cwd:a}/{_input:bnn}.PEER.diag.pdf'
task: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint
    Rscript ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor.R \
        --step PEER_extract \
        --cwd "${cwd}" \
        --modelFile "${_input}" \
        --covFile "${covFile}" \
        --numThreads ${numThreads}

Reference#

  • PEER code is adapted from here

  • GTEx recommandation of PEER factors is here

  • Examples by PEER is at github

BiCV Factor Analysis with APEX#

The commands below were constructed and tested against the maintainers’ own reading of the program, without input from the APEX authors, so treat the parameter choices as a starting point rather than as endorsed defaults.

[BiCV_2]
output: f'{cwd:a}/{_input:bn}.fake.vcf.gz'
task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output:bn}'
bash: expand= "${ }", stderr = f'{_output}.stderr', stdout = f'{_output}.stdout', container = container, entrypoint = entrypoint
    Rscript ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor.R \
        --step BiCV_2 \
        --cwd "${cwd}" \
        --residFile "${_input}" \
        --numThreads ${numThreads}
[BiCV_3]
# N factors, if not specify, calculated based on sample size according to GTeX
parameter: N = 0
# The number of iteration
parameter: iteration = 10
input: output_from(1), output_from(2)
output: f'{cwd:a}/{_input[0]:bnn}.BiCV.gz'
task: trunk_workers = 1, walltime = walltime, mem = mem, cores = numThreads, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]}.stderr', stdout = f'{_output[0]}.stdout', container = container, entrypoint = entrypoint
    n_factors_file="${_output[0]:nn}.n_factors.txt"
    Rscript ${modular_script_dir}/data_preprocessing/covariate/covariate_hidden_factor.R \
        --step BiCV_nfactors \
        --residFile "${_input[0]}" \
        --N ${N} \
        --output "$n_factors_file"
    n_factors=$(cat "$n_factors_file")
    out_prefix="${_output[0]:nn}"
    apex factor \
        --out "$out_prefix" \
        --iter ${iteration} \
        --factors "$n_factors" \
        --bed "${_input[0]}" \
        --vcf "${_input[1]}" \
        --threads ${numThreads} \
        --cov "${covFile}"
    mv -f "$out_prefix.cov.gz" "${_output[0]}"