
Tutorial 1: Getting your data into ScreenR
Emanuel Michele Soda
Istituto Europeo Oncologia (IEO), Milano, Italyemanuelsoda@gmail.com
Elena Ceccacci
Istituto Europeo Oncologia (IEO), Milano, Italyelena.ceccacci@ieo.it
2026-10-25
Source:vignettes/Tutorial_1_Creating_the_object.Rmd
Tutorial_1_Creating_the_object.RmdAbout this tutorial
This is the first of three tutorials that explain step by step how to use ScreenR:
- Getting your data into ScreenR (this tutorial): which tables you need, how to create the ScreenR object and how to check that ScreenR understood your data correctly.
- Quality control: how to check that the screen worked before looking for hits.
- Finding and validating hits: how to compare treated and control samples, how to combine the statistical methods and how to check the candidate genes.
Everything in ScreenR starts from a ScreenR object, which keeps all the tables of your experiment together. At the end of this tutorial you will know how to build it from your own data.
What ScreenR needs
ScreenR analyses pooled screens with barcoded shRNA (or sgRNA) libraries: each gene is targeted by several barcodes, the barcodes are counted by sequencing in each sample, and a gene whose barcodes disappear (or increase) in the treated samples is a candidate hit. You need three pieces of information.
| What | Content | Required |
|---|---|---|
| Count table | One row per barcode, one column with the barcode identifier and one column per sample with the number of reads | Yes |
| Annotation table | One row per barcode, with the barcode identifier and the gene it targets | Yes |
| Groups | The experimental group of each sample (for example
Time3_TRT), in the same order as the sample columns |
Yes, for the statistical analysis |
| Sample table | The time point (Day) and the Treatment of each sample | Only when the sample names do not follow the
Day_Treatment_Replicate convention (see below) |
A minimal example
The smallest possible input: two genes with three barcodes each, two control and two treated samples at one time point.
toy_counts <- data.frame(
Barcode = paste0("bc", 1:6),
Day3_CTRL_A = c(520, 610, 480, 300, 350, 410),
Day3_CTRL_B = c(500, 590, 505, 320, 330, 395),
Day3_TRT_A = c(130, 160, 120, 310, 345, 400),
Day3_TRT_B = c(150, 140, 110, 290, 360, 420)
)
toy_counts
#> Barcode Day3_CTRL_A Day3_CTRL_B Day3_TRT_A Day3_TRT_B
#> 1 bc1 520 500 130 150
#> 2 bc2 610 590 160 140
#> 3 bc3 480 505 120 110
#> 4 bc4 300 320 310 290
#> 5 bc5 350 330 345 360
#> 6 bc6 410 395 400 420
toy_annotation <- data.frame(
Barcode = paste0("bc", 1:6),
Gene = rep(c("GeneA", "GeneB"), each = 3)
)
toy_annotation
#> Barcode Gene
#> 1 bc1 GeneA
#> 2 bc2 GeneA
#> 3 bc3 GeneA
#> 4 bc4 GeneB
#> 5 bc5 GeneB
#> 6 bc6 GeneBThe groups say which samples are replicates of the same condition. They must be in the same order as the sample columns of the count table.
Now the object can be created:
toy <- create_screenr_object(
table = toy_counts,
annotation = toy_annotation,
groups = toy_groups
)Telling ScreenR what each sample is
Several functions need to know the time point (Day) and the treatment of each sample, for example to compare treated and control samples at Day 3.
The naming convention
By default ScreenR reads them from the sample names, which should have the form
Day_Treatment_Replicate, for example
Day3_TRT_A: time point Day3, treatment
TRT, replicate A.
Always check how the samples were interpreted with
get_sample_table():
get_sample_table(toy)
#> # A tibble: 4 × 3
#> Sample Day Treatment
#> <chr> <chr> <chr>
#> 1 Day3_CTRL_A Day3 CTRL
#> 2 Day3_CTRL_B Day3 CTRL
#> 3 Day3_TRT_A Day3 TRT
#> 4 Day3_TRT_B Day3 TRTWhen the names have another form: the sample table
If your samples are called, for example, S1,
S2, … or ctrl_rep1, the convention does not
apply. ScreenR then gives a message, and you should describe the samples
with a sample table. It can have any column names: tell
ScreenR which column is which with sample_columns.
counts_other_names <- toy_counts
colnames(counts_other_names) <- c("Barcode", "S1", "S2", "S3", "S4")
my_samples <- data.frame(
sample_id = c("S1", "S2", "S3", "S4"),
time = "Day3",
condition = c("CTRL", "CTRL", "TRT", "TRT")
)
my_samples
#> sample_id time condition
#> 1 S1 Day3 CTRL
#> 2 S2 Day3 CTRL
#> 3 S3 Day3 TRT
#> 4 S4 Day3 TRT
toy2 <- create_screenr_object(
table = counts_other_names,
annotation = toy_annotation,
groups = toy_groups,
sample_table = my_samples,
sample_columns = c(
Sample = "sample_id", Day = "time", Treatment = "condition"
)
)
get_sample_table(toy2)
#> # A tibble: 4 × 3
#> Sample Day Treatment
#> <chr> <chr> <chr>
#> 1 S1 Day3 CTRL
#> 2 S2 Day3 CTRL
#> 3 S3 Day3 TRT
#> 4 S4 Day3 TRTThe rows of the sample table can be in any order: ScreenR matches them to the columns of the count table by name. Every sample of the count table must be in the sample table.
Using your own column names
Your tables do not need columns called Barcode and
Gene. Give their names with barcode_column and
gene_column; ScreenR renames them internally and remembers
the original names.
my_counts <- toy_counts
colnames(my_counts)[1] <- "shRNA_id"
my_annotation <- data.frame(
shRNA_id = paste0("bc", 1:6),
symbol = rep(c("GeneA", "GeneB"), each = 3)
)
toy3 <- create_screenr_object(
table = my_counts, annotation = my_annotation, groups = toy_groups,
barcode_column = "shRNA_id", gene_column = "symbol"
)
get_column_names(toy3)
#> Barcode Gene
#> "shRNA_id" "symbol"If you forget to give the name, the error tells you what to do:
create_screenr_object(
table = my_counts, annotation = my_annotation, groups = toy_groups,
barcode_column = "shRNA_id"
)
#> Error:
#> ! The annotation table has no column called 'Gene'. Its columns are: shRNA_id, symbol. Use gene_column to give the name of the column.Automatic checks
When the object is created ScreenR checks the input and either fixes small problems (telling you what it did) or stops with a message explaining what is wrong:
| Situation | What ScreenR does |
|---|---|
Counts stored as text (e.g. "520") |
Converts them to numbers, with a message |
| Non-numeric columns in the count table (e.g. a gene column) | Removes them, with a message: gene information belongs in the annotation |
groups with a different length than the number of
samples |
Error |
| Duplicated barcodes | Warning |
Barcodes of the count table missing from the annotation
(e.g. unmapped reads *) |
Warning |
| Duplicated barcodes in the annotation | Warning; only the first row is kept |
| A column name that does not exist | Error suggesting the most similar column |
For example, a count table read from a file with the counts as text and an extra gene column:
messy_counts <- toy_counts
messy_counts$Day3_CTRL_A <- as.character(messy_counts$Day3_CTRL_A)
messy_counts$Gene <- toy_annotation$Gene
clean <- create_screenr_object(
table = messy_counts, annotation = toy_annotation, groups = toy_groups
)
#> Column 'Day3_CTRL_A' of the count table was stored as text and has been converted to numbers.
#> Column(s) 'Gene' of the count table are not numeric, so they are not samples, and have been removed. Put the information on the barcodes in the annotation table.
get_count_table(clean)
#> ScreenR count table: 6 rows, 5 columns
#> # A tibble: 6 × 5
#> Barcode Day3_CTRL_A Day3_CTRL_B Day3_TRT_A Day3_TRT_B
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 bc1 520 500 130 150
#> 2 bc2 610 590 160 140
#> 3 bc3 480 505 120 110
#> 4 bc4 300 320 310 290
#> 5 bc5 350 330 345 360
#> 6 bc6 410 395 400 420And a groups vector that does not match the samples:
create_screenr_object(
table = toy_counts, annotation = toy_annotation,
groups = factor(c("CTRL", "TRT"))
)
#> Error:
#> ! groups has 2 values but the count table has 4 samples (Day3_CTRL_A, Day3_CTRL_B, Day3_TRT_A, Day3_TRT_B). groups must have one value for each sample.A real dataset
ScreenR includes the data of a real shRNA chemical lethality screen:
about 530 genes with 10 barcodes each, two early time points
(Time1, Time2) and treated (TRT)
and control (CTRL) samples in triplicate at two later time
points (Time3, Time4).
data(count_table)
data(annotation_table)
dim(count_table)
#> [1] 5323 15
head(count_table[, 1:5])
#> Barcode Time_1 Time_2 Time_3_TRT_rep1 Time_3_TRT_rep2
#> 1 10208 3520 4283 2493 3998
#> 2 10502 280 339 140 542
#> 3 10796 1507 1533 1120 1125
#> 4 11089 3797 4151 2296 3857
#> 5 11382 717 644 564 336
#> 6 11675 7330 7593 4927 6211
head(annotation_table)
#> Gene Barcode Gene_ID Sequence Library
#> 1 Gene_380 10208 NA NA NA
#> 2 Gene_380 10502 NA NA NA
#> 3 Gene_380 10796 NA NA NA
#> 4 Gene_380 11089 NA NA NA
#> 5 Gene_380 11382 NA NA NA
#> 6 Gene_380 11675 NA NA NAThe original sample names do not follow the convention, so we rename
the columns. The rows with barcode * count the reads that
did not match any barcode: they are not barcodes and are removed.
counts <- count_table
colnames(counts) <- c(
"Barcode", "Time1", "Time2", "Time3_TRT_A", "Time3_TRT_B", "Time3_TRT_C",
"Time3_CTRL_A", "Time3_CTRL_B", "Time3_CTRL_C",
"Time4_TRT_A", "Time4_TRT_B", "Time4_TRT_C",
"Time4_CTRL_A", "Time4_CTRL_B", "Time4_CTRL_C"
)
counts <- filter(counts, Barcode != "*")The groups: one value per sample, in the order of the columns. The two early time points are grouped together as the starting point of the screen.
groups <- factor(c(
"T1/T2", "T1/T2",
rep("Time3_TRT", 3), rep("Time3_CTRL", 3),
rep("Time4_TRT", 3), rep("Time4_CTRL", 3)
))Create the object and check the samples. Time1 and
Time2 have no treatment part in their name: ScreenR tells
us, and uses the whole name as both Day and Treatment, which is what we
want for these early samples.
object <- create_screenr_object(
table = counts, annotation = annotation_table, groups = groups
)
#> The Day and the Treatment of each sample are taken from the sample names (Day_Treatment_Replicate). For 2 sample(s) (Time1, Time2) they cannot be separated and are both set to the first part of the name. If this is not intended, give them with the sample_table argument. Check them with get_sample_table().
get_sample_table(object)
#> # A tibble: 14 × 3
#> Sample Day Treatment
#> <chr> <chr> <chr>
#> 1 Time1 Time1 Time1
#> 2 Time2 Time2 Time2
#> 3 Time3_TRT_A Time3 TRT
#> 4 Time3_TRT_B Time3 TRT
#> 5 Time3_TRT_C Time3 TRT
#> 6 Time3_CTRL_A Time3 CTRL
#> 7 Time3_CTRL_B Time3 CTRL
#> 8 Time3_CTRL_C Time3 CTRL
#> 9 Time4_TRT_A Time4 TRT
#> 10 Time4_TRT_B Time4 TRT
#> 11 Time4_TRT_C Time4 TRT
#> 12 Time4_CTRL_A Time4 CTRL
#> 13 Time4_CTRL_B Time4 CTRL
#> 14 Time4_CTRL_C Time4 CTRLLooking inside the object
Use the get_ functions to read the tables of the object
and the set_ functions to replace them. The tables are
views of the object: the object stores each piece of information once
(see below), and the tables are built from it when you ask for them.
| Function | Returns |
|---|---|
get_count_table() |
The raw counts |
get_annotation_table() |
The annotation |
get_groups() |
The groups |
get_sample_table() |
Day and Treatment of each sample |
get_column_names() |
The original column names |
get_normalization() |
The normalization: method and effective library sizes |
get_normalized_count_table() |
The normalized counts (after normalize_data()) |
get_data_table() |
The tidy table used by most functions (after
normalize_data()) |
head(get_count_table(object)[, 1:4])
#> ScreenR count table: 5320 rows, 15 columns
#> # A tibble: 6 × 4
#> Barcode Time1 Time2 Time3_TRT_A
#> <chr> <int> <int> <int>
#> 1 10208 3520 4283 2493
#> 2 10502 280 339 140
#> 3 10796 1507 1533 1120
#> 4 11089 3797 4151 2296
#> 5 11382 717 644 564
#> 6 11675 7330 7593 4927
get_groups(object)
#> [1] T1/T2 T1/T2 Time3_TRT Time3_TRT Time3_TRT Time3_CTRL
#> [7] Time3_CTRL Time3_CTRL Time4_TRT Time4_TRT Time4_TRT Time4_CTRL
#> [13] Time4_CTRL Time4_CTRL
#> Levels: T1/T2 Time3_CTRL Time3_TRT Time4_CTRL Time4_TRTThe object is a SummarizedExperiment
The ScreenR object is also a SummarizedExperiment, the standard Bioconductor container for this kind of data, so it can be used directly with the Bioconductor tools that accept one:
| Part | Content |
|---|---|
assay(object, "counts") |
The raw counts: one row per barcode, one column per sample. The row names are the barcode identifiers |
rowData(object) |
The annotation of each barcode (Gene, …) |
colData(object) |
Sample, Day, Treatment and Group of each sample and, after
normalize_data(), its effective library size
(EffectiveLibSize) |
metadata(object)$normalization |
How the counts were normalized (method, control barcodes, …) |
metadata(object)$palettes |
Optional colors of the sample variables, used by all the plots
(create_screenr_object(palettes = ),
set_palettes()) |
These parts are the only storage of the object. The barcode
identifiers are always text and are the row names of the counts, never a
column of counts: numeric identifiers such as 10208 are
stored as "10208". The normalized counts and the data table
are not stored: they are computed from these parts each time, so they
cannot get out of date.
object
#> class: screenr_object
#> dim: 5320 14
#> metadata(1): screenr_layout
#> assays(1): counts
#> rownames(5320): 10208 10502 ... 13876 13995
#> rowData names(5): Barcode Gene Gene_ID Sequence Library
#> colnames(14): Time1 Time2 ... Time4_CTRL_B Time4_CTRL_C
#> colData names(4): Sample Day Treatment Group
#> normalization: none (run normalize_data())
head(SummarizedExperiment::rowData(object))
#> DataFrame with 6 rows and 5 columns
#> Barcode Gene Gene_ID Sequence Library
#> <character> <character> <logical> <logical> <logical>
#> 10208 10208 Gene_380 NA NA NA
#> 10502 10502 Gene_380 NA NA NA
#> 10796 10796 Gene_380 NA NA NA
#> 11089 11089 Gene_380 NA NA NA
#> 11382 11382 Gene_380 NA NA NA
#> 11675 11675 Gene_380 NA NA NA
SummarizedExperiment::colData(object)
#> DataFrame with 14 rows and 4 columns
#> Sample Day Treatment Group
#> <character> <character> <character> <factor>
#> Time1 Time1 Time1 Time1 T1/T2
#> Time2 Time2 Time2 Time2 T1/T2
#> Time3_TRT_A Time3_TRT_A Time3 TRT Time3_TRT
#> Time3_TRT_B Time3_TRT_B Time3 TRT Time3_TRT
#> Time3_TRT_C Time3_TRT_C Time3 TRT Time3_TRT
#> ... ... ... ... ...
#> Time4_TRT_B Time4_TRT_B Time4 TRT Time4_TRT
#> Time4_TRT_C Time4_TRT_C Time4 TRT Time4_TRT
#> Time4_CTRL_A Time4_CTRL_A Time4 CTRL Time4_CTRL
#> Time4_CTRL_B Time4_CTRL_B Time4 CTRL Time4_CTRL
#> Time4_CTRL_C Time4_CTRL_C Time4 CTRL Time4_CTRLLike any SummarizedExperiment, the object can be subset by barcode (rows) and by sample (columns), and all its parts stay consistent:
treated <- object[, object$Treatment == "TRT"]
treated
#> class: screenr_object
#> dim: 5320 6
#> metadata(1): screenr_layout
#> assays(1): counts
#> rownames(5320): 10208 10502 ... 13876 13995
#> rowData names(5): Barcode Gene Gene_ID Sequence Library
#> colnames(6): Time3_TRT_A Time3_TRT_B ... Time4_TRT_B Time4_TRT_C
#> colData names(4): Sample Day Treatment Group
#> normalization: none (run normalize_data())Keeping the object consistent
Because each piece of information is stored once, changes propagate by themselves:
-
Changing the counts
(
set_count_table(), orassay(object, "counts") <- ...) removes the normalization, which no longer describes the counts: runnormalize_data()again. - Normalizing again (for example with another method) changes at once the normalized counts, the data table, the plots and the edgeR/limma models.
-
Changing the annotation
(
set_annotation_table(), orrowData(object) <- ...) changes the gene of each barcode in all the following analyses. -
Subsetting or reordering the object keeps the
barcodes, their annotation and the samples aligned by identifier. A
normalized object keeps the effective library sizes of the retained
samples, computed on all the barcodes; to normalize on the retained
barcodes, call
normalize_data()again after subsetting. - Inconsistent changes are refused: duplicated barcode identifiers, a
Barcodecolumn of the rowData different from the row names, or invalid effective library sizes give an error.
Objects saved with older versions of ScreenR are converted
automatically by all the functions; update_screenr_object()
converts them explicitly. Those versions did not record how the counts
were normalized, so the converted object is not normalized: run
normalize_data() on it.
Preparing the data for the analysis
Three steps prepare the data for all the following analyses.
1. Remove the barcodes with zero reads in every sample. They carry no information.
object <- remove_all_zero_row(object)2. Normalize the counts. Samples are sequenced at
different depths, so the counts are converted to counts per million
(CPM). The default divides by the total number of reads of each sample;
method = "tmm" and method = "control" are
discussed in tutorial 2. The raw counts are not changed: the
normalization is the effective library size of each sample, and the same
normalization is used by the plots, the data table and the statistical
models.
object <- normalize_data(object)
get_normalization(object)$effective_library_size
#> Time1 Time2 Time3_TRT_A Time3_TRT_B Time3_TRT_C Time3_CTRL_A
#> 10671757 9799724 6824877 9107354 9717868 9485195
#> Time3_CTRL_B Time3_CTRL_C Time4_TRT_A Time4_TRT_B Time4_TRT_C Time4_CTRL_A
#> 9016812 9568624 8933915 9883209 12325124 10351136
#> Time4_CTRL_B Time4_CTRL_C
#> 9180192 9315365
head(get_normalized_count_table(object)[, 1:4])
#> ScreenR normalized count table: 5317 rows, 15 columns
#> # A tibble: 6 × 4
#> Barcode Time1 Time2 Time3_TRT_A
#> <chr> <dbl> <dbl> <dbl>
#> 1 10208 330. 437. 365.
#> 2 10502 26.2 34.6 20.5
#> 3 10796 141. 156. 164.
#> 4 11089 356. 424. 336.
#> 5 11382 67.2 65.7 82.6
#> 6 11675 687. 775. 722.3. Look at the data table. This is a long (“tidy”)
table with one row per barcode and sample, and it is used by most of the
plotting and analysis functions. It is computed from the normalized
object when needed (compute_data_table(), needed by older
versions of ScreenR, is no longer necessary).
data_table <- get_data_table(object)
#> ScreenR normalized data table: 74438 rows, 9 columns
head(select(data_table, Barcode, Gene, Sample, Frequency, Day, Treatment))
#> # A tibble: 6 × 6
#> Barcode Gene Sample Frequency Day Treatment
#> <fct> <chr> <fct> <dbl> <chr> <chr>
#> 1 10208 Gene_380 Time1 330. Time1 Time1
#> 2 10208 Gene_380 Time2 437. Time2 Time2
#> 3 10208 Gene_380 Time3_TRT_A 365. Time3 TRT
#> 4 10208 Gene_380 Time3_TRT_B 439. Time3 TRT
#> 5 10208 Gene_380 Time3_TRT_C 311. Time3 TRT
#> 6 10208 Gene_380 Time3_CTRL_A 358. Time3 CTRL| Column | Meaning |
|---|---|
Barcode, Gene
|
The barcode and its gene |
Sample |
The sample |
Frequency |
The normalized count (CPM) |
Day, Treatment
|
From the sample table |
Sequence, Library,
Gene_ID
|
Copied from the annotation, when present |
Checklist
Before moving on to the quality control:
The next tutorial, 2. Quality control, continues from this object.
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> 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=C.UTF-8 LC_NUMERIC=C LC_TIME=C.UTF-8
#> [4] LC_COLLATE=C.UTF-8 LC_MONETARY=C.UTF-8 LC_MESSAGES=C.UTF-8
#> [7] LC_PAPER=C.UTF-8 LC_NAME=C LC_ADDRESS=C
#> [10] LC_TELEPHONE=C LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] dplyr_1.2.1 ScreenR_1.15.16 BiocStyle_2.40.0
#>
#> loaded via a namespace (and not attached):
#> [1] SummarizedExperiment_1.42.0 gtable_0.3.6
#> [3] xfun_0.62 bslib_0.12.0
#> [5] ggplot2_4.0.3 htmlwidgets_1.6.4
#> [7] Biobase_2.72.0 lattice_0.22-9
#> [9] vctrs_0.7.3 tools_4.6.1
#> [11] generics_0.1.4 stats4_4.6.1
#> [13] tibble_3.3.1 pkgconfig_2.0.3
#> [15] Matrix_1.7-5 RColorBrewer_1.1-3
#> [17] S7_0.2.2 desc_1.4.3
#> [19] S4Vectors_0.50.3 lifecycle_1.0.5
#> [21] compiler_4.6.1 farver_2.1.2
#> [23] stringr_1.6.0 textshaping_1.0.5
#> [25] statmod_1.5.2 Seqinfo_1.2.0
#> [27] htmltools_0.5.9 sass_0.4.10
#> [29] yaml_2.3.12 tidyr_1.3.2
#> [31] pillar_1.11.1 pkgdown_2.2.1
#> [33] jquerylib_0.1.4 DelayedArray_0.38.2
#> [35] cachem_1.1.0 limma_3.68.5
#> [37] abind_1.4-8 tidyselect_1.2.1
#> [39] locfit_1.5-9.12 digest_0.6.39
#> [41] stringi_1.8.9 purrr_1.2.2
#> [43] bookdown_0.48 fastmap_1.2.0
#> [45] grid_4.6.1 cli_3.6.6
#> [47] SparseArray_1.12.3 magrittr_2.0.5
#> [49] patchwork_1.3.2 S4Arrays_1.12.1
#> [51] utf8_1.2.6 withr_3.0.3
#> [53] edgeR_4.10.7 scales_1.4.0
#> [55] rmarkdown_2.32 XVector_0.52.0
#> [57] matrixStats_1.5.0 otel_0.2.0
#> [59] ragg_1.5.2 evaluate_1.0.5
#> [61] knitr_1.52 GenomicRanges_1.64.0
#> [63] IRanges_2.46.0 rlang_1.3.0
#> [65] glue_1.8.1 BiocManager_1.30.27
#> [67] BiocGenerics_0.58.1 jsonlite_2.0.0
#> [69] R6_2.6.1 MatrixGenerics_1.24.0
#> [71] systemfonts_1.3.2 fs_2.1.0