library(growkar)
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
library(knitr)
data(yeast_growth_data)High-throughput microbial growth assays are widely used to characterize phenotypic responses to genetic perturbations, environmental conditions, and drug treatments. These assays generate time-resolved measurements across large numbers of samples, analogous to other high-throughput experimental platforms in functional genomics.
While tools exist for transcriptomic and epigenomic data analysis within the Bioconductor ecosystem, there is a relative lack of standardized infrastructure for analyzing growth-based phenotypic data and integrating it with omics datasets.
The growkar package addresses this gap by providing a scalable and reproducible framework for high-throughput microbial phenotyping from growth assays. It uses Bioconductor data structures to represent assay measurements and derived phenotypes, while accepting tidy and wide plate-reader exports as import adapters.
The primary workflow is:
SummarizedExperiment.growkar uses two S4 classes and no S3 classes:
GrowthExperiment is the data container. It is an S4 class that extends SummarizedExperiment, adding a validity method that enforces the canonical growth-assay layout (an od assay, numeric time in rowData()). Because it is a SummarizedExperiment, every Bioconductor method works on it unchanged, and as(x, "SummarizedExperiment") is always available for handing objects to other packages.GrowthFit represents a model result — a logistic or Gompertz fit for one sample. It is not a data container; instances are stored in metadata(ge) beside the experiment they were derived from.The package defines no S3 classes and registers no S3 methods.
Build a GrowthExperiment with the constructor or with standard S4 coercion:
# Constructor (control over column-name resolution):
ge <- GrowthExperiment(yeast_growth_data)
ge
#> class: GrowthExperiment
#> dim: 49 9
#> metadata(1): growkar_schema
#> assays(1): od
#> rownames(49): 0 0.5 ... 23.5 24
#> rowData names(1): time
#> colnames(9): Cg_R1 Cg_R2 ... YPD_R2 YPD_R3
#> colData names(3): sample condition replicate
# Equivalent standard coercion:
ge2 <- as(yeast_growth_data, "GrowthExperiment")
identical(ge, ge2)
#> [1] TRUE
# It is a SummarizedExperiment, so it hands off cleanly to other packages:
se <- as(ge, "SummarizedExperiment")
is(ge, "SummarizedExperiment")
#> [1] TRUETidy tables and wide plate-reader exports are handled purely as import adapters by as_tidy_growth_data(), which resolves vendor column labels and normalizes them to sample, time, and od before the container is built. Once data are in the container, tidy manipulation is delegated to the tidyomics stack rather than reimplemented — see Interoperability with tidyomics below.
Derived summaries are stored in metadata() of the same object, so growth phenotypes travel with the container:
ge <- growth_metrics(ge, method = "rolling_window", average_replicates = TRUE)
S4Vectors::metadata(ge)$growth_metrics
#> # A tibble: 3 × 10
#> sample mu start_time end_time r_squared method n_points degraded note
#> <chr> <dbl> <dbl> <dbl> <dbl> <chr> <int> <lgl> <chr>
#> 1 Cg 0.569 4.5 6.5 1.000 rolling_… 5 FALSE roll…
#> 2 CgFlu 0.404 4.5 6.5 1.000 rolling_… 5 FALSE roll…
#> 3 YPD 0.00128 12.5 14.5 0.500 rolling_… 5 FALSE roll…
#> # ℹ 1 more variable: doubling_time <dbl>This workflow is useful when growth phenotypes need to be carried forward into other Bioconductor analyses or linked to omics-derived sample annotations.
The canonical layout in growkar is:
assay(ge, "od"): OD matrix with timepoints in rows and samples in columnsrowData(ge): timepoint metadatacolData(ge): sample metadatametadata(ge): derived results such as growth metrics, phase windows, and fitsvalidate_growth_experiment() checks that an object follows this layout:
validate_growth_experiment(ge)
#> class: GrowthExperiment
#> dim: 49 9
#> metadata(4): growkar_schema growth_metrics analysis_params
#> growth_metrics_parameters
#> assays(1): od
#> rownames(49): 0 0.5 ... 23.5 24
#> rowData names(1): time
#> colnames(9): Cg_R1 Cg_R2 ... YPD_R2 YPD_R3
#> colData names(3): sample condition replicateHelper accessors make these components easier to inspect:
growth_assay(ge)[1:3, 1:3]
#> Cg_R1 Cg_R2 Cg_R3
#> 0 0.115 0.116 0.117
#> 0.5 0.116 0.118 0.117
#> 1 0.118 0.119 0.118
timepoints(ge)
#> # A tibble: 49 × 1
#> time
#> <dbl>
#> 1 0
#> 2 0.5
#> 3 1
#> 4 1.5
#> 5 2
#> 6 2.5
#> 7 3
#> 8 3.5
#> 9 4
#> 10 4.5
#> # ℹ 39 more rows
sample_data(ge)
#> # A tibble: 9 × 3
#> sample condition replicate
#> <chr> <fct> <fct>
#> 1 Cg_R1 Cg R1
#> 2 Cg_R2 Cg R2
#> 3 Cg_R3 Cg R3
#> 4 CgFlu_R1 CgFlu R1
#> 5 CgFlu_R2 CgFlu R2
#> 6 CgFlu_R3 CgFlu R3
#> 7 YPD_R1 YPD R1
#> 8 YPD_R2 YPD R2
#> 9 YPD_R3 YPD R3as_tidy_growth_data() is the import adapter. It accepts wide plate-reader exports (time in the first column, samples in the remaining columns) as well as long tables, resolves common instrument column labels such as Time [h] and OD600, and infers condition/replicate from suffixed sample names.
tidy_growth <- as_tidy_growth_data(yeast_growth_data)
head(tidy_growth)
#> # A tibble: 6 × 5
#> time sample od condition replicate
#> <dbl> <chr> <dbl> <chr> <chr>
#> 1 0 Cg_R1 0.115 Cg R1
#> 2 0 Cg_R2 0.116 Cg R2
#> 3 0 Cg_R3 0.117 Cg R3
#> 4 0 CgFlu_R1 0.131 CgFlu R1
#> 5 0 CgFlu_R2 0.133 CgFlu R2
#> 6 0 CgFlu_R3 0.132 CgFlu R3The canonical columns are sample, time, and od. Additional metadata such as condition and replicate are carried alongside them.
validate_growth_data(tidy_growth)
#> # A tibble: 441 × 5
#> time sample od condition replicate
#> <dbl> <chr> <dbl> <chr> <chr>
#> 1 0 Cg_R1 0.115 Cg R1
#> 2 0 Cg_R2 0.116 Cg R2
#> 3 0 Cg_R3 0.117 Cg R3
#> 4 0 CgFlu_R1 0.131 CgFlu R1
#> 5 0 CgFlu_R2 0.133 CgFlu R2
#> 6 0 CgFlu_R3 0.132 CgFlu R3
#> 7 0 YPD_R1 0.105 YPD R1
#> 8 0 YPD_R2 0.105 YPD R2
#> 9 0 YPD_R3 0.104 YPD R3
#> 10 0.5 Cg_R1 0.116 Cg R1
#> # ℹ 431 more rowsImported data are then converted into the canonical container:
growkar does not reimplement tidy verbs or a tidy display layer for SummarizedExperiment. Install the optional tidySummarizedExperiment package to filter, mutate, summarize, and plot growkar objects with the standard tidyverse grammar, while the underlying object remains a SummarizedExperiment usable by any other Bioconductor package.
library(tidySummarizedExperiment)
#> Loading required package: SummarizedExperiment
#> Loading required package: MatrixGenerics
#> Loading required package: matrixStats
#>
#> Attaching package: 'matrixStats'
#> The following object is masked from 'package:dplyr':
#>
#> count
#>
#> Attaching package: 'MatrixGenerics'
#> The following objects are masked from 'package:matrixStats':
#>
#> colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse,
#> colCounts, colCummaxs, colCummins, colCumprods, colCumsums,
#> colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs,
#> colMads, colMaxs, colMeans2, colMedians, colMins, colOrderStats,
#> colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds,
#> colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads,
#> colWeightedMeans, colWeightedMedians, colWeightedSds,
#> colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet,
#> rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods,
#> rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
#> rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins,
#> rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks,
#> rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars,
#> rowWeightedMads, rowWeightedMeans, rowWeightedMedians,
#> rowWeightedSds, rowWeightedVars
#> Loading required package: GenomicRanges
#> Loading required package: stats4
#> Loading required package: BiocGenerics
#> Loading required package: generics
#>
#> Attaching package: 'generics'
#> The following object is masked from 'package:dplyr':
#>
#> explain
#> The following objects are masked from 'package:base':
#>
#> as.difftime, as.factor, as.ordered, intersect, is.element, setdiff,
#> setequal, union
#>
#> Attaching package: 'BiocGenerics'
#> The following object is masked from 'package:dplyr':
#>
#> combine
#> The following objects are masked from 'package:stats':
#>
#> IQR, mad, sd, var, xtabs
#> The following object is masked from 'package:utils':
#>
#> data
#> The following objects are masked from 'package:base':
#>
#> Filter, Find, Map, Position, Reduce, anyDuplicated, aperm, append,
#> as.data.frame, basename, cbind, colnames, dirname, do.call,
#> duplicated, eval, evalq, get, grep, grepl, is.unsorted, lapply,
#> mapply, match, mget, order, paste, pmax, pmax.int, pmin, pmin.int,
#> rank, rbind, rownames, sapply, saveRDS, scale, sequence, table,
#> tapply, transform, unique, unsplit, which.max, which.min
#> Loading required package: S4Vectors
#>
#> Attaching package: 'S4Vectors'
#> The following objects are masked from 'package:dplyr':
#>
#> first, rename
#> The following object is masked from 'package:utils':
#>
#> findMatches
#> The following objects are masked from 'package:base':
#>
#> I, expand.grid, unname
#> Loading required package: IRanges
#>
#> Attaching package: 'IRanges'
#> The following objects are masked from 'package:dplyr':
#>
#> collapse, desc, slice
#> Loading required package: Seqinfo
#> Loading required package: Biobase
#> Welcome to Bioconductor
#>
#> Vignettes contain introductory material; view with
#> 'browseVignettes()'. To cite Bioconductor, see
#> 'citation("Biobase")', and for packages 'citation("pkgname")'.
#>
#> Attaching package: 'Biobase'
#> The following object is masked from 'package:MatrixGenerics':
#>
#> rowMedians
#> The following objects are masked from 'package:matrixStats':
#>
#> anyMissing, rowMedians
#> Loading required package: ttservice
#>
#> Attaching package: 'ttservice'
#> The following objects are masked from 'package:dplyr':
#>
#> bind_cols, bind_rows
#> ℹ tidySummarizedExperiment says: By default SummarizedExperiment uses the standard display. For a tidy tibble-style display, run tidy_print_on(remember = TRUE).
#>
#> Attaching package: 'tidySummarizedExperiment'
#> The following object is masked _by_ '.GlobalEnv':
#>
#> se
# The SE prints and behaves as a tibble abstraction, without being converted.
se
#> class: SummarizedExperiment
#> dim: 49 9
#> metadata(1): growkar_schema
#> assays(1): od
#> rownames(49): 0 0.5 ... 23.5 24
#> rowData names(1): time
#> colnames(9): Cg_R1 Cg_R2 ... YPD_R2 YPD_R3
#> colData names(3): sample condition replicate
se |>
filter(condition == "Cg") |>
filter(time <= 6) |>
select(.feature, .sample, od, condition, replicate)
#> class: SummarizedExperiment
#> dim: 13 3
#> metadata(3): growkar_schema latest_filter_scope_report
#> latest_select_scope_report
#> assays(1): od
#> rownames(13): 0 0.5 ... 5.5 6
#> rowData names(0):
#> colnames(3): Cg_R1 Cg_R2 Cg_R3
#> colData names(2): condition replicate
se |>
group_by(condition) |>
summarise(max_od = max(od), .groups = "drop")
#> ℹ tidySummarizedExperiment says: A data frame is returned for independent data analysis.
#> # A tibble: 3 × 2
#> condition max_od
#> <fct> <dbl>
#> 1 Cg 2.02
#> 2 CgFlu 1.97
#> 3 YPD 0.105growkar results stored in metadata(se) are ordinary tibbles, so they slot directly into the same downstream workflow.
Graphing is optional in growkar: ggplot2 and RColorBrewer are declared in Suggests, so the data-structure and analysis layers install without a graphics stack. The plot_*() functions check for these packages at call time and return standard ggplot objects that can be customized further.
plot_growth_curve(
se,
average_replicates = TRUE,
colour_col = "condition",
palette_name = "Dark2"
)Replicate-level faceting remains available when averaging is disabled:
plot_growth_curve(
se,
average_replicates = FALSE,
colour_col = "condition",
facet_col = "replicate",
palette_name = "Dark2"
)metrics <- summarize_growth_metrics(
se,
method = "rolling_window",
average_replicates = TRUE
)
knitr::kable(metrics, digits = 3)| sample | mu | start_time | end_time | r_squared | method | n_points | degraded | note | doubling_time |
|---|---|---|---|---|---|---|---|---|---|
| Cg | 0.569 | 4.5 | 6.5 | 1.0 | rolling_window | 5 | FALSE | rolling_window_ranked | 1.219 |
| CgFlu | 0.404 | 4.5 | 6.5 | 1.0 | rolling_window | 5 | FALSE | rolling_window_ranked | 1.714 |
| YPD | 0.001 | 12.5 | 14.5 | 0.5 | rolling_window | 5 | FALSE | rolling_window_ranked | 539.788 |
For a single sample, the empirical growth-rate estimate can be inspected in more detail:
gr <- compute_growth_rate(se, method = "rolling_window")
#> Warning: Sample `YPD_R1`: Exponential phase detection did not yield a positive
#> growth slope (rolling_window_ranked).
#> Warning: Sample `YPD_R2`: Exponential phase detection did not yield a positive
#> growth slope (rolling_window_ranked).
knitr::kable(head(gr), digits = 3)| sample | mu | start_time | end_time | r_squared | method | n_points | degraded | note |
|---|---|---|---|---|---|---|---|---|
| Cg_R1 | 0.576 | 4.5 | 6.5 | 1.000 | rolling_window | 5 | FALSE | rolling_window_ranked |
| Cg_R2 | 0.560 | 4.5 | 6.5 | 1.000 | rolling_window | 5 | FALSE | rolling_window_ranked |
| Cg_R3 | 0.571 | 4.5 | 6.5 | 0.999 | rolling_window | 5 | FALSE | rolling_window_ranked |
| CgFlu_R1 | 0.403 | 4.5 | 6.5 | 1.000 | rolling_window | 5 | FALSE | rolling_window_ranked |
| CgFlu_R2 | 0.407 | 4.5 | 6.5 | 1.000 | rolling_window | 5 | FALSE | rolling_window_ranked |
| CgFlu_R3 | 0.403 | 4.5 | 6.5 | 1.000 | rolling_window | 5 | FALSE | rolling_window_ranked |
The doubling time helper is useful when you already have growth-rate values:
knitr::kable(
tibble(
sample = gr$sample,
growth_rate = gr$mu,
doubling_time = compute_doubling_time(gr$mu)
),
digits = 3
)| sample | growth_rate | doubling_time |
|---|---|---|
| Cg_R1 | 0.576 | 1.204 |
| Cg_R2 | 0.560 | 1.238 |
| Cg_R3 | 0.571 | 1.214 |
| CgFlu_R1 | 0.403 | 1.719 |
| CgFlu_R2 | 0.407 | 1.702 |
| CgFlu_R3 | 0.403 | 1.721 |
| YPD_R1 | NA | NA |
| YPD_R2 | NA | NA |
| YPD_R3 | 0.004 | 179.350 |
detect_exponential_phase() returns the ranked candidate windows and metadata describing whether the chosen interval required any degraded fallback.
| sample | rank | start_time | end_time | slope | r_squared | n_points | selection_reason | degraded |
|---|---|---|---|---|---|---|---|---|
| Cg_R1 | 1 | 4.5 | 6.5 | 0.576 | 1.000 | 5 | rolling_window_ranked | FALSE |
| Cg_R1 | 2 | 4.0 | 6.0 | 0.551 | 0.997 | 5 | rolling_window_ranked | FALSE |
| Cg_R1 | 3 | 5.0 | 7.0 | 0.551 | 0.997 | 5 | rolling_window_ranked | FALSE |
| Cg_R1 | 4 | 3.5 | 5.5 | 0.490 | 0.990 | 5 | rolling_window_ranked | FALSE |
| Cg_R1 | 5 | 5.5 | 7.5 | 0.490 | 0.991 | 5 | rolling_window_ranked | FALSE |
| Cg_R1 | 6 | 6.0 | 8.0 | 0.414 | 0.987 | 5 | rolling_window_ranked | FALSE |
For a full-curve model-based summary, fit one of the supported parametric models. fit_growth_curve() returns a GrowthFit S4 object.
sample_id <- unique(gr$sample)[1]
fit_input <- as_tidy_growth_data(se) |>
filter(sample == sample_id)
cg_fit <- fit_growth_curve(fit_input, model = "logistic")
cg_fit
#> <GrowthFit> sample=Cg_R1, model=logistic, status=converged, n_points=49
isVirtualClass("GrowthFit")
#> [1] FALSE
extract_params(cg_fit)
#> # A tibble: 1 × 6
#> sample model asymptote r t0 doubling_time_model
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Cg_R1 logistic 2.03 0.777 6.97 0.892
summary(cg_fit)
#> # A tibble: 1 × 9
#> sample model converged status message n_points rss aic bic
#> <chr> <chr> <lgl> <chr> <chr> <int> <dbl> <dbl> <dbl>
#> 1 Cg_R1 logistic TRUE converged <NA> 49 0.0688 -175. -167.GrowthFit supports the standard modelling generics and a small set of accessors, so slots never need to be touched directly:
fit_sample(cg_fit)
#> [1] "Cg_R1"
fit_model(cg_fit)
#> [1] "logistic"
fit_status(cg_fit)
#> [1] "converged"
fit_converged(cg_fit)
#> [1] TRUE
coef(cg_fit)
#> K r t0
#> 2.0271408 0.7772492 6.9688283
head(fitted(cg_fit))
#> [1] 0.008965938 0.013196595 0.019404332 0.028490904 0.041744026 0.060974028
nobs(cg_fit)
#> [1] 49Failed fits remain machine-readable and do not crash downstream helpers:
flat_fit <- fit_growth_curve(
tibble(
sample = "flat",
time = 0:4,
od = rep(0.2, 5)
),
model = "logistic"
)
summary(flat_fit)
#> # A tibble: 1 × 9
#> sample model converged status message n_points rss aic bic
#> <chr> <chr> <lgl> <chr> <chr> <int> <dbl> <dbl> <dbl>
#> 1 flat logistic FALSE flat_curve Model fitting… 5 NA NA NAAcross a plate, fits are stored in metadata() of the same SummarizedExperiment:
se <- fit_growth_models(se, model = "logistic")
S4Vectors::metadata(se)$growth_model_parameters
#> # A tibble: 9 × 6
#> sample model asymptote r t0 doubling_time_model
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Cg_R1 logistic 2.03 0.777 6.97 0.892
#> 2 Cg_R2 logistic 2.01 0.784 6.95 0.884
#> 3 Cg_R3 logistic 2.02 0.784 7.03 0.884
#> 4 CgFlu_R1 logistic 1.98 0.563 7.51 1.23
#> 5 CgFlu_R2 logistic 1.96 0.569 7.40 1.22
#> 6 CgFlu_R3 logistic 1.90 0.589 7.29 1.18
#> 7 YPD_R1 logistic 0.208 0.00000001 0.0423 69314718.
#> 8 YPD_R2 logistic 0.208 0.00000001 0.0423 69314718.
#> 9 YPD_R3 logistic 0.208 0.00000001 812. 69314718.Observed and fitted values can be visualized together:
The supported SE-native interface includes:
as_tidy_growth_data()GrowthExperiment()validate_growth_experiment()compute_growth_rate()summarize_growth_metrics()detect_exponential_phase()fit_growth_curve()GrowthFit object with status and diagnostics rather than throwing a cryptic nls error.sessionInfo()
#> R version 4.6.1 Patched (2026-06-24 r90190)
#> Platform: x86_64-apple-darwin20
#> Running under: macOS Ventura 13.7.8
#>
#> Matrix products: default
#> BLAS: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRblas.0.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.6-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
#>
#> locale:
#> [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#>
#> time zone: America/New_York
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] ggplot2_4.0.3 tidyr_1.3.2
#> [3] tidySummarizedExperiment_1.23.2 ttservice_0.5.3
#> [5] SummarizedExperiment_1.43.0 Biobase_2.73.2
#> [7] GenomicRanges_1.65.3 Seqinfo_1.3.2
#> [9] IRanges_2.47.5 S4Vectors_0.51.9
#> [11] BiocGenerics_0.59.12 generics_0.1.4
#> [13] MatrixGenerics_1.25.0 matrixStats_1.5.0
#> [15] knitr_1.51 dplyr_1.2.1
#> [17] growkar_0.99.2
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 xfun_0.60 bslib_0.12.0
#> [4] htmlwidgets_1.6.4 lattice_0.23-1 vctrs_0.7.3
#> [7] tools_4.6.1 tidyprint_1.1.0 tibble_3.3.1
#> [10] fansi_1.0.7 pkgconfig_2.0.3 Matrix_1.7-6
#> [13] data.table_1.18.6.1 RColorBrewer_1.1-3 S7_0.2.2
#> [16] lifecycle_1.0.5 compiler_4.6.1 farver_2.1.2
#> [19] stringr_1.6.0 htmltools_0.5.9 sass_0.4.10
#> [22] yaml_2.3.12 plotly_4.12.1 pillar_1.11.1
#> [25] jquerylib_0.1.4 ellipsis_0.3.3 DelayedArray_0.39.6
#> [28] cachem_1.1.0 abind_1.4-8 tidyselect_1.2.1
#> [31] digest_0.6.39 stringi_1.8.9 purrr_1.2.2
#> [34] labeling_0.4.3 fastmap_1.2.0 grid_4.6.1
#> [37] cli_3.6.6 SparseArray_1.13.2 magrittr_2.0.5
#> [40] S4Arrays_1.13.0 dichromat_2.0-1 utf8_1.2.6
#> [43] withr_3.0.3 scales_1.4.0 rmarkdown_2.31
#> [46] XVector_0.53.0 httr_1.4.8 otel_0.2.0
#> [49] evaluate_1.0.5 viridisLite_0.4.3 rlang_1.3.0
#> [52] glue_1.8.1 jsonlite_2.0.0 R6_2.6.1