Heatmaps, PCA and clustering need a complete data matrix. Label-free data are not complete: a protein is often not observed in some samples, and often not in any sample of one group. While such proteins are frequently below the limit of detection (LOD), complete absence can also reflect true biological on/off expression, which makes them candidates of high biological interest. PCA drops every protein with a missing value, and with it exactly these proteins.
library(prolfqua)
library(ggplot2)
sim <- sim_lfq_data_protein_config(Nprot = 100, weight_missing = 0.5, seed = 1234)
lfq <- LFQData$new(sim$data, sim$config)
wide <- lfq$data_wide(as.matrix = TRUE)$data
empty_group <- lfq$data_long() |>
dplyr::group_by(protein_Id, group_) |>
dplyr::summarise(empty = all(is.na(abundance)), .groups = "drop") |>
dplyr::filter(empty)
c(
proteins = nrow(wide),
complete = sum(stats::complete.cases(wide)),
with_empty_group = dplyr::n_distinct(empty_group$protein_Id)
)## proteins complete with_empty_group
## 100 23 5
The proteins with an empty group:
lfq$data_long() |>
dplyr::filter(protein_Id %in% empty_group$protein_Id) |>
dplyr::select(protein_Id, sampleName, abundance) |>
tidyr::pivot_wider(names_from = sampleName, values_from = abundance) |>
knitr::kable(digits = 1)| protein_Id | A_V1 | A_V2 | A_V3 | A_V4 | B_V1 | B_V2 | B_V3 | B_V4 | Ctrl_V1 | Ctrl_V2 | Ctrl_V3 | Ctrl_V4 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cnCfON~9769 | NA | 15.9 | 14.3 | 14.4 | NA | NA | NA | NA | NA | 15.8 | 17.5 | NA |
| DoWup2~8058 | 17.9 | NA | 18.9 | NA | NA | NA | NA | NA | NA | NA | NA | 17 |
| LMQBev~8107 | NA | 16.0 | NA | NA | NA | 15.8 | 16.0 | 15.8 | NA | NA | NA | NA |
| umjHSH~2090 | NA | NA | NA | NA | NA | NA | 15.8 | 16.5 | NA | NA | 17.4 | NA |
| ZHBkZv~0394 | NA | NA | NA | NA | NA | NA | NA | 13.3 | NA | NA | 12.6 | NA |
To test for differences you do not need to impute anything yourself:
build_contrast_analysis(method = "lm_impute") also
estimates fold changes for proteins with an empty group, refitting them
at the LOD and borrowing their variance from the other proteins. See Comparing
two groups and Comparing
modelling methods on one dataset. Here we reuse the models of that
fit to fill the data matrix.
impute_from_model() fills every missing cell with the
prediction of its protein model and keeps the observed values. The
summary tells which route each protein took:
complete: no missing values, nothing to fillfitted: the model fitted on the observed values
predicts the missing cells from their grouplod_refit: a protein whose fit failed, typically
because a group is empty, is refitted with its missing values set to the
LOD and observed values below the LOD raised to it; the empty group gets
a value at the LODcontrasts <- c(AvsC = "group_A - group_Ctrl", BvsC = "group_B - group_Ctrl")
fa <- build_contrast_analysis(lfq, "~ group_", contrasts, method = "lm_impute")
filled <- impute_from_model(fa$model, lfq)
table(filled$summary$route)##
## complete fitted lod_refit
## 23 70 7
MissingHelpers estimates the LOD from the data. Groups
in which a protein is observed in only one sample, or in the fewest
samples, are the closest the data comes to the detection limit; the LOD
is the median of their mean intensities. Pass lod to
build_contrast_analysis() to use a LOD of your own.
## [1] 15.97769
The proteins with an empty group after imputation; compare with the table above:
filled$lfqdata$data_long() |>
dplyr::filter(protein_Id %in% empty_group$protein_Id) |>
dplyr::select(protein_Id, sampleName, abundance) |>
tidyr::pivot_wider(names_from = sampleName, values_from = abundance) |>
knitr::kable(digits = 1)| protein_Id | A_V1 | A_V2 | A_V3 | A_V4 | B_V1 | B_V2 | B_V3 | B_V4 | Ctrl_V1 | Ctrl_V2 | Ctrl_V3 | Ctrl_V4 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cnCfON~9769 | 16.0 | 15.9 | 14.3 | 14.4 | 16.0 | 16.0 | 16.0 | 16.0 | 16.5 | 15.8 | 17.5 | 16.5 |
| DoWup2~8058 | 17.9 | 17.6 | 18.9 | 17.6 | 16.0 | 16.0 | 16.0 | 16.0 | 16.4 | 16.4 | 16.4 | 17.0 |
| LMQBev~8107 | 16.0 | 16.0 | 16.0 | 16.0 | 16.0 | 15.8 | 16.0 | 15.8 | 16.0 | 16.0 | 16.0 | 16.0 |
| umjHSH~2090 | 16.0 | 16.0 | 16.0 | 16.0 | 16.2 | 16.2 | 15.8 | 16.5 | 16.9 | 16.9 | 17.4 | 16.9 |
| ZHBkZv~0394 | 16.0 | 16.0 | 16.0 | 16.0 | 16.0 | 16.0 | 16.0 | 13.3 | 16.0 | 16.0 | 12.6 | 16.0 |
We cluster the proteins once, on the imputed data, and draw both heatmaps with that row order, so each row shows the same protein before and after.
before <- lfq$data_wide(as.matrix = TRUE)
after <- filled$lfqdata$data_wide(as.matrix = TRUE)
row_order <- rownames(after$data)[hclust(dist(t(scale(t(after$data)))))$order]ht_before <- plot_heatmap(
before$data, before$annotation, lfq$factor_keys(), lfq$sample_name(),
na_fraction = 1, row_order = row_order
)
ht_beforeBefore imputation: missing cells are grey.
ht_after <- plot_heatmap(
after$data, after$annotation, lfq$factor_keys(), lfq$sample_name(),
na_fraction = 1, row_order = row_order
)
ht_afterAfter imputation: every cell holds a value.
Before imputation PCA can only use the proteins without missing values; after imputation it uses all proteins.
n_complete <- sum(filled$summary$route == "complete")
gridExtra::grid.arrange(
lfq$get_Plotter()$pca() +
ggtitle(paste0("Before imputation: ", n_complete, " complete proteins")),
filled$lfqdata$get_Plotter()$pca() +
ggtitle(paste0("After imputation: ", nrow(filled$summary), " proteins")),
ncol = 1
)PCA before (top) and after (bottom) imputation.
After imputation the groups separate more cleanly, in the heatmap and
in the PCA. Part of that is circular: each filled value is the model’s
estimate for its group, so imputation adds the group structure of the
model to the data. The filled values also carry no noise. Use the filled
matrix to look at the data; for tests use the contrasts of
lm_impute, which account for the imputation.
## R version 4.6.2 (2026-10-29)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 26.04.1 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
##
## locale:
## [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
## [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
## [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
## [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
## [9] LC_ADDRESS=C LC_TELEPHONE=C
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] ggplot2_4.0.3 prolfqua_0.99.8 dplyr_1.2.1 BiocStyle_2.41.0
##
## loaded via a namespace (and not attached):
## [1] Rdpack_2.6.6 gridExtra_2.3.1 rlang_1.3.0
## [4] magrittr_2.0.5 clue_0.3-68 GetoptLong_1.1.1
## [7] otel_0.3.0 matrixStats_1.5.0 compiler_4.6.2
## [10] mgcv_1.9-5 png_0.1-9 vctrs_0.7.3
## [13] pkgconfig_2.0.3 shape_1.4.6.1 crayon_1.5.3
## [16] fastmap_1.2.0 backports_1.5.1 labeling_0.4.3
## [19] utf8_1.2.6 promises_1.5.0 rmarkdown_2.32
## [22] nloptr_2.2.1 UpSetR_1.4.1 purrr_1.2.2
## [25] xfun_0.62 glmnet_5.2 jomo_2.7-6
## [28] logistf_1.26.1 cachem_1.1.0 jsonlite_2.0.0
## [31] progress_1.2.3 later_1.4.8 pan_2.0
## [34] prettyunits_1.2.0 broom_1.0.13 parallel_4.6.2
## [37] cluster_2.1.8.3 R6_2.6.1 bslib_0.12.0
## [40] stringi_1.8.9 RColorBrewer_1.1-3 limma_3.99.4
## [43] boot_1.3-32 rpart_4.1.27 jquerylib_0.1.4
## [46] Rcpp_1.1.2 iterators_1.0.14 knitr_1.52
## [49] IRanges_2.47.5 httpuv_1.6.17 Matrix_1.7-6
## [52] splines_4.6.2 nnet_7.3-21 tidyselect_1.2.1
## [55] yaml_2.3.12 doParallel_1.0.17 codetools_0.2-20
## [58] lattice_0.23-1 tibble_3.3.1 plyr_1.8.9
## [61] shiny_1.14.0 withr_3.0.3 S7_0.2.2
## [64] evaluate_1.0.5 survival_3.8-12 circlize_0.4.18
## [67] pillar_1.11.1 BiocManager_1.30.27 mice_3.19.0
## [70] KernSmooth_2.23-27 foreach_1.5.2 stats4_4.6.2
## [73] reformulas_0.4.4 plotly_4.12.1 generics_0.1.4
## [76] hms_1.1.4 S4Vectors_0.51.10 scales_1.4.0
## [79] minqa_1.2.8 xtable_1.8-8 glue_1.8.1
## [82] maketools_1.3.2 tools_4.6.2 sys_3.4.3
## [85] data.table_1.18.6.1 lme4_2.1-0 forcats_1.0.1
## [88] buildtools_1.0.0 grid_4.6.2 tidyr_1.3.2
## [91] rbibutils_2.4.1 crosstalk_1.2.2 colorspace_2.1-4
## [94] nlme_3.1-172 formula.tools_1.7.1 cli_3.6.6
## [97] viridisLite_0.4.3 ComplexHeatmap_2.29.0 gtable_0.3.6
## [100] sass_0.4.10 digest_0.6.39 operator.tools_1.6.3.1
## [103] BiocGenerics_0.59.13 ggrepel_0.9.8 rjson_0.2.23
## [106] htmlwidgets_1.6.4 farver_2.1.2 htmltools_0.5.9
## [109] lifecycle_1.0.5 httr_1.4.9 mime_0.13
## [112] GlobalOptions_0.1.4 mitml_0.4-5 statmod_1.5.2
## [115] MASS_7.3-66