Skip to content

agricola Python API

agricola exports two functions which can be used in place of the recommended command line workflow:

The implementation is organized into focused namespaces:

  • agricola.pipeline contains step orchestration and output writing.
  • agricola.io contains genotype, local-ancestry, and variant access helpers.
  • agricola.models contains ridge and logistic-ridge models.
  • agricola.statistics contains quantitative and binary association kernels.
  • agricola.numerical contains shared preprocessing and linear-algebra helpers.
  • agricola.validation contains input validation and preparation.

agricola.pipeline.step1.step1(datasets, Y, X, phenotypes, train_mask, test_mask, h2_prior, trait_type, loocv=False, B=1000, idx_sample=None, variants=None, level0_dir=None, prune_blocks=True, key=None, memory_mode='standard')

Perform agricola step 1

Parameters:

Name Type Description Default
datasets list[PgenData]

A list of PgenData objects (either single object or one per-chromosome)

required
Y ArrayLike

A (N, P) jax array of phenotypes

required
X ArrayLike | None

A (N, C) jax array of covariates (no intercept)

required
phenotypes list[str]

A list of phenotype names, ordered as the columns of Y

required
train_mask ArrayLike

A (N, K) jax array indicating training set status for each set k in 1, ..., K

required
test_mask ArrayLike

A (N, K) jax array indicating test set status for each set k in 1, ..., K

required
h2_prior ArrayLike

A 1D jax array of prior values for snp heritability

required
trait_type str | TraitType

Either "qt" or "bt"

required
loocv bool

A boolean indicating whether to perform LOOCV instead of standard cross validation. Ignored for trait_type="qt".

False
B int

The number of variants per block

1000
idx_sample ArrayLike | None

An optional (N_sub,) jax array with indices of samples to include

None
variants list[str] | None

A list of variant IDs to include in the analysis. If not provided, all variants are used

None
level0_dir str | None

The directory where level 0 predictions are written

None
prune_blocks bool

Whether to sample variants in a dataset in level 0 so that n_variants (mod B) = 0. This will improve speed with JIT compilations

True
key ArrayLike | None

Optional JAX PRNG key used for level 0 variant subsampling

None

Returns:

Type Description
dict[str, DataFrame]

A dict where keys are chromosomes and values are (N, P) pandas DataFrames of level 1 predictions

Source code in src/agricola/pipeline/step1.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def step1(
    datasets: list[PgenData],
    Y: ArrayLike,
    X: ArrayLike | None,
    phenotypes: list[str],
    train_mask: ArrayLike,
    test_mask: ArrayLike,
    h2_prior: ArrayLike,
    trait_type: str | TraitType,
    loocv: bool = False,
    B: int = 1000,
    idx_sample: ArrayLike | None = None,
    variants: list[str] | None = None,
    level0_dir: str | None = None,
    prune_blocks: bool = True,
    key: ArrayLike | None = None,
    memory_mode: str = "standard",
) -> dict[str, DataFrame]:
    """Perform agricola step 1

    Args:
        datasets: A list of PgenData objects (either single object or one
            per-chromosome)
        Y: A (N, P) jax array of phenotypes
        X: A (N, C) jax array of covariates (no intercept)
        phenotypes: A list of phenotype names, ordered as the columns of Y
        train_mask: A (N, K) jax array indicating training set status for each set k in 1, ..., K
        test_mask: A (N, K) jax array indicating test set status for each set k in 1, ..., K
        h2_prior: A 1D jax array of prior values for snp heritability
        trait_type: Either "qt" or "bt"
        loocv: A boolean indicating whether to perform LOOCV instead of standard
            cross validation. Ignored for trait_type="qt".
        B: The number of variants per block
        idx_sample: An optional (N_sub,) jax array with indices of samples to include
        variants: A list of variant IDs to include in the analysis. If not provided,
            all variants are used
        level0_dir: The directory where level 0 predictions are written
        prune_blocks: Whether to sample variants in a dataset in level 0 so that
            n_variants (mod B) = 0. This will improve speed with JIT compilations
        key: Optional JAX PRNG key used for level 0 variant subsampling

    Returns:
        A dict where keys are chromosomes and values are (N, P) pandas DataFrames
            of level 1 predictions
    """
    directory_context = (
        tempfile.TemporaryDirectory() if level0_dir is None else nullcontext(level0_dir)
    )
    with directory_context as working_dir:
        level0_path = Path(working_dir)
        level0_path.mkdir(parents=True, exist_ok=True)

        level0_files = level0(
            datasets,
            Y,
            X,
            phenotypes,
            train_mask,
            test_mask,
            h2_prior,
            B,
            idx_sample,
            variants,
            str(level0_path),
            prune_blocks,
            key,
            memory_mode,
        )

        return level1(
            level0_files,
            Y,
            X,
            phenotypes,
            train_mask,
            test_mask,
            h2_prior,
            trait_type,
            loocv,
            memory_mode,
        )

agricola.pipeline.step2.step2(datasets, Y, X, step1_predictions, outdir, phenotypes, trait_type=TraitType.QT, test_type=TestType.SCORE, chrom=None, B=1000, min_ac=1, idx_sample=None, variants=None, adjust_lanc=True, impute=False, overwrite=True, partition_phenotype=True, max_rows=None)

Perform agricola step 2

Parameters:

Name Type Description Default
datasets list[LancData]

A list of LancData objects (either single object or one per-chromosome)

required
Y ArrayLike

A (N, P) jax array of outcomes

required
X ArrayLike | None

A (N, C) jax array of covariates

required
step1_predictions dict[str, DataFrame] | None

An optional dict with LOCO linear predictions from step 1. The values are (N, P) NumPy arrays

required
outdir str | Path

Outputs will be written to {output_prefix}_{phenotype}.parquet

required
phenotypes list[str]

A list of phenotype names

required
trait_type str | TraitType

either "qt" or "bt"

QT
test_type str | TestType

Either "score" or "wald"

SCORE
B int

The block size (max number of variants to read at once)

1000
min_ac int

the minimum allele count threshold

1
idx_sample ArrayLike | None

An optional numpy array with ordered indices of samples (in the psam file) to retain

None
variants list[str] | None

An optional list of variant IDs to retain

None
adjust_lanc bool

A boolean indicating whether to adjust tests for local ancestry

True
impute bool

Whether to impute the phenotype. Much faster, but only available for qt traits. If all phenotypes are non-missing, this is ignored.

False
overwrite bool

Whether to overwrite the outdir if it already exists

True
partition_phenotype bool

Whether to partition output parquet files by phenotype

True
max_rows int | None

Max number of rows/variants per phenotype to keep in memory before writing an output file. Defaults to 5000000 / len(phenotypes)

None
Source code in src/agricola/pipeline/step2.py
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
def step2(
    datasets: list[LancData],
    Y: ArrayLike,
    X: ArrayLike | None,
    step1_predictions: dict[str, pd.DataFrame] | None,
    outdir: str | Path,
    phenotypes: list[str],
    trait_type: str | TraitType = TraitType.QT,
    test_type: str | TestType = TestType.SCORE,
    chrom: str | None = None,
    B: int = 1000,
    min_ac: int = 1,
    idx_sample: ArrayLike | None = None,
    variants: list[str] | None = None,
    adjust_lanc: bool = True,
    impute: bool = False,
    overwrite: bool = True,
    partition_phenotype: bool = True,
    max_rows: int | None = None,
) -> None:
    """Perform agricola step 2

    Args:
        datasets: A list of LancData objects (either single object or one
            per-chromosome)
        Y: A (N, P) jax array of outcomes
        X: A (N, C) jax array of covariates
        step1_predictions: An optional dict with LOCO linear predictions from step 1.
            The values are (N, P) NumPy arrays
        outdir: Outputs will be written to {output_prefix}_{phenotype}.parquet
        phenotypes: A list of phenotype names
        trait_type: either "qt" or "bt"
        test_type: Either "score" or "wald"
        B: The block size (max number of variants to read at once)
        min_ac: the minimum allele count threshold
        idx_sample: An optional numpy array with ordered indices of samples (in
            the psam file) to retain
        variants: An optional list of variant IDs to retain
        adjust_lanc: A boolean indicating whether to adjust tests for local ancestry
        impute: Whether to impute the phenotype. Much faster, but only available
            for qt traits. If all phenotypes are non-missing, this is ignored.
        overwrite: Whether to overwrite the outdir if it already exists
        partition_phenotype: Whether to partition output parquet files by phenotype
        max_rows: Max number of rows/variants per phenotype to keep in memory
            before writing an output file. Defaults to 5000000 / len(phenotypes)
    """
    ## Create writer
    outdir_path = Path(outdir)
    if overwrite and outdir_path.exists():
        shutil.rmtree(outdir_path)

    if max_rows:
        max_rows_total = max_rows * len(phenotypes)
    else:
        max_rows_total = 5000000

    with ParquetRotatingWriter(outdir_path, partition_phenotype, max_rows_total) as writer:
        if impute:
            M = jnp.ones(shape=jnp.asarray(Y).shape)
        else:
            M = (~jnp.isnan(jnp.asarray(Y))).astype(float)

        Y, X, step1_predictions_np, idx_sample, test_type_enum, trait_type_enum = (
            validate_step2_inputs(
                datasets,
                Y,
                X,
                phenotypes,
                step1_predictions,
                B,
                idx_sample,
                variants,
                test_type,
                trait_type,
            )
        )

        ## Adjust phenotype for covariates to match step 1
        if trait_type_enum == TraitType.QT:
            Q, _ = jnp.linalg.qr(X, mode="reduced")
            Y = stdize(Y - (Q @ (Q.T @ Y)))
            if (M == 1).all():
                impute = True
        elif impute:
            raise ValueError("impute must be False for binary traits")

        time_total_start = time.perf_counter()
        for dataset in datasets:
            pgen_path = dataset.plink_prefix + ".pgen"
            logger.info("Testing associations for file: %s", pgen_path)

            time_ds_start = time.perf_counter()
            _step2_dataset(
                dataset,
                writer,
                Y,
                M,
                step1_predictions_np,
                X,
                idx_sample,
                phenotypes,
                trait_type_enum,
                test_type_enum,
                chrom,
                B,
                min_ac,
                variants,
                adjust_lanc,
                impute,
            )
            time_ds = str(timedelta(seconds=int(time.perf_counter() - time_ds_start)))
            logger.info("Elapsed time: %s", time_ds)

        time_total = str(timedelta(seconds=int(time.perf_counter() - time_total_start)))
        logger.info("Step 2 completed in: %s", time_total)