library(HTAN)
library(dplyr)
library(ggplot2)
con <- htan_con("r7")Package: HTAN 0.99.1 (source).
This vignette describes HTAN Phase 1 as of Release 7, the final Phase 1 release: who the participants are, where their samples came from, which assays were run on them, at which data levels the results were released, and how much clinical annotation comes with them. Read it before you plan an analysis, to judge what the data can and cannot support.
Every table and figure below, and nearly every count in the text, is computed from the Release 7 metadata snapshot. The data model vignette explains the tables used here, and its warnings about duplicates, comma-separated lists and inconsistent spelling apply throughout.
We open one connection to the snapshot and use it for everything below.
library(HTAN)
library(dplyr)
library(ggplot2)
con <- htan_con("r7")Most of what follows works from three tables. files has one row per file from htan_files(), plus a coarse modality. file_participants links files to participants through id_provenance, one row per file and participant, so a pooled file counts once for every participant in it. imaging_types gives the imaging technology of each Level 2 image. We need it because Component does not record the technology: every multiplexed image, H&E slide and MERFISH run is ImagingLevel2.
imaging_types <- htan_tbl("imaging_level2_metadata", con) |>
select(HTAN_Data_File_ID, Imaging_Assay_Type) |>
collect()
modality_of <- function(assay, imaging_type) {
case_when(
assay %in% c("ScRNA-seq", "ScATAC-seq", "ScDNA-seq") ~
"Single-cell sequencing",
grepl("Visium|Xenium|GeoMx|Slide-seq", assay) |
imaging_type == "MERFISH" | assay == "ExSeqMinimal" ~
"Spatial transcriptomics",
imaging_type == "H&E" ~ "H&E",
assay %in% c("Imaging", "SRRSImaging") ~ "Multiplexed imaging",
grepl("^Bulk|HI-C", assay) ~ "Bulk sequencing",
assay == "ElectronMicroscopy" ~ "Electron microscopy",
assay %in% c("MassSpectrometry", "RPPA") ~
"Proteomics and metabolomics",
.default = "Other"
)
}
files <- htan_files(con = con) |>
select(HTAN_Data_File_ID, HTAN_Center, Component, assay, level,
extension, File_Size, access, n_participants) |>
collect() |>
left_join(imaging_types, by = "HTAN_Data_File_ID") |>
mutate(modality = modality_of(assay, Imaging_Assay_Type))
file_participants <- htan_tbl("id_provenance", con) |>
distinct(HTAN_Data_File_ID, HTAN_Participant_ID) |>
collect() |>
inner_join(select(files, HTAN_Data_File_ID, HTAN_Center, assay, level,
modality, Imaging_Assay_Type), by = "HTAN_Data_File_ID")
dim(files)
#> [1] 221688 11
dim(file_participants)
#> [1] 245119 7A Level 3 or 4 imaging file has the imaging type of the Level 2 image it was computed from, and the join above does not trace that link. So the modality is exact for Level 2 images and approximate for derived imaging files, which fall under “Multiplexed imaging” unless their assay says otherwise. Participant counts below use every level.
Release 7 lists 221,688 files totalling 333.7 TB. They were derived from 10,895 assayed biospecimens and 2,416 participants. The clinical demographics table has records for 2,797 participants and the biospecimen table lists 13,095 biospecimens. Both include participants and samples with no released files, so to count what has data, count from id_provenance, as we do here. (The DCC’s 2025 description of Phase 1 counted 2,042 participants and 8,425 biospecimens, before Release 7 added data from another 422 participants.)
By access tier:
size_by_tier |>
mutate(share_of_bytes = pct(bytes / sum(bytes)), size = tb(bytes)) |>
select(access, files, size, share_of_bytes) |>
knitr::kable(format.args = list(big.mark = ","))| access | files | size | share_of_bytes |
|---|---|---|---|
| controlled | 42,897 | 241.9 TB | 73% |
| open-gc | 24,271 | 38.6 TB | 12% |
| open-synapse | 154,520 | 53.2 TB | 16% |
Open files on Synapse, the processed data most analyses start from, are 70% of the files but only 16% of the bytes. Most of the volume is raw sequencing (controlled) and Level 2 images in the CRDC General Commons.
centers <- file_participants |>
group_by(HTAN_Center) |>
summarise(participants = n_distinct(HTAN_Participant_ID)) |>
left_join(
files |>
group_by(HTAN_Center) |>
summarise(files = n(),
size_tb = round(sum(File_Size, na.rm = TRUE) / 1e12, 2),
assays = n_distinct(assay)),
by = "HTAN_Center") |>
arrange(desc(participants))
knitr::kable(centers, format.args = list(big.mark = ","))| HTAN_Center | participants | files | size_tb | assays |
|---|---|---|---|---|
| HTAN Duke | 838 | 7,035 | 16.11 | 4 |
| HTAN WUSTL | 286 | 7,176 | 48.07 | 8 |
| HTAN BU | 228 | 5,319 | 13.12 | 5 |
| HTAN HTAPP | 203 | 41,626 | 65.27 | 9 |
| HTAN Vanderbilt | 192 | 3,340 | 22.74 | 5 |
| HTAN HMS | 160 | 1,690 | 20.95 | 2 |
| HTAN MSK | 160 | 5,381 | 9.77 | 2 |
| HTAN DFCI | 133 | 17,560 | 16.79 | 3 |
| HTAN CHOP | 70 | 4,739 | 44.65 | 4 |
| HTAN TNP - TMA | 52 | 4,281 | 2.18 | 2 |
| HTAN Stanford | 41 | 6,321 | 55.40 | 10 |
| HTAN OHSU | 34 | 116,657 | 11.50 | 10 |
| HTAN SRRS | 19 | 262 | 0.71 | 3 |
| HTAN TNP SARDANA | 2 | 301 | 6.44 | 1 |
The centers differ by orders of magnitude, and which center looks largest depends on what you count. HTAN OHSU has the most files (116,657), 95% of them electron microscopy, which produces one file per imaged plane or tile, but only 34 participants. HTAN Duke has by far the most participants (838), 783 of them with a diagnosis of ductal carcinoma in situ. File counts say more about how a center packaged its data than about how much biology it covers. To compare scope, count participants or biospecimens.
ggplot(centers, aes(participants, reorder(HTAN_Center, participants))) +
geom_col(fill = blue, width = 0.7) +
geom_text(aes(label = fmt(participants)), hjust = -0.15, size = 3.2,
colour = "grey25") +
scale_x_continuous(expand = expansion(mult = c(0, 0.12))) +
labs(x = "Participants", y = NULL)The diagnosis table records each participant’s tissue or organ of origin in ICD-O-3 site terms (“Upper-outer quadrant of breast”, “Sigmoid colon”). The snapshot’s tissueOrOrganOfOrigin_to_wholeOrgan_mapping table collapses those to whole organs. Not every term in Release 7 is in the mapping (“Upper lobe lung” and several soft-tissue sites are missing), so we match terms case-insensitively, assign the unmapped ones by keyword and keep anything left as it is.
organ_map <- htan_tbl("tissueOrOrganOfOrigin_to_wholeOrgan_mapping", con) |>
collect() |>
mutate(key = tolower(Tissue_or_Organ_of_Origin)) |>
distinct(key, .keep_all = TRUE)
dx <- htan_tbl("clinical_tier1_diagnosis", con) |>
select(HTAN_Participant_ID, HTAN_Center, Tissue_or_Organ_of_Origin,
Primary_Diagnosis) |>
collect() |>
mutate(key = tolower(Tissue_or_Organ_of_Origin)) |>
left_join(select(organ_map, key, Whole_Organ), by = "key") |>
mutate(organ = case_when(
!is.na(Whole_Organ) ~ Whole_Organ,
grepl("lung", key) ~ "Lung",
grepl("^skin", key) ~ "Skin",
grepl("connective|soft tissue|paraspinal", key) ~ "Soft Tissue",
grepl("bone", key) ~ "Bone",
.default = Tissue_or_Organ_of_Origin),
organ = if_else(tolower(organ) %in% c("not reported", "unknown",
"unknown primary site"), "Not reported", organ))
with_files <- unique(file_participants$HTAN_Participant_ID)
organ_center <- dx |>
filter(HTAN_Participant_ID %in% with_files) |>
distinct(HTAN_Participant_ID, organ, HTAN_Center)
organs <- organ_center |>
distinct(HTAN_Participant_ID, organ) |>
count(organ, sort = TRUE)
main_center <- organ_center |>
count(organ, HTAN_Center) |>
group_by(organ) |>
summarise(main_center = HTAN_Center[which.max(n)],
main_center_share = pct(max(n) / sum(n)))
organs |>
left_join(main_center, by = "organ") |>
head(15)
#> # A tibble: 15 × 4
#> organ n main_center main_center_share
#> <chr> <int> <chr> <chr>
#> 1 Breast 1083 HTAN Duke 76%
#> 2 Lung 336 HTAN BU 54%
#> 3 Colorectal 194 HTAN HMS 52%
#> 4 Not reported 191 HTAN Vanderbilt 92%
#> 5 Other and Ill-defined Sites 83 HTAN HMS 53%
#> 6 Skin 73 HTAN DFCI 60%
#> 7 Pancreas 64 HTAN WUSTL 95%
#> 8 Bone Marrow 62 HTAN CHOP 48%
#> 9 Adrenal Gland 34 HTAN HTAPP 85%
#> 10 Ovary 27 HTAN HTAPP 67%
#> 11 Head and Neck 25 HTAN WUSTL 84%
#> 12 Brain 24 HTAN HTAPP 83%
#> 13 Cervix 20 HTAN WUSTL 100%
#> 14 Kidney 15 HTAN WUSTL 87%
#> 15 Soft Tissue 13 HTAN HTAPP 92%organs |>
slice_head(n = 15) |>
ggplot(aes(n, reorder(organ, n))) +
geom_col(fill = blue, width = 0.7) +
geom_text(aes(label = fmt(n)), hjust = -0.15, size = 3.2,
colour = "grey25") +
scale_x_continuous(expand = expansion(mult = c(0, 0.12))) +
labs(x = "Participants", y = NULL)Breast dominates, and the main_center column shows why: most breast participants come from one center’s cohort. Lung, colorectal, skin (melanoma) and pancreas follow. The ranking says more about which centers enrolled large cohorts than about the burden of each cancer.
“Not reported” is large. Many participants, especially in the pre-cancer atlases, have no usable organ in the diagnosis table, and their organ has to be read from the biospecimen table (Site_of_Resection_or_Biopsy) instead. A participant with several diagnosis records can appear under more than one organ.
The most common primary diagnoses, again among participants with data:
dx |>
filter(HTAN_Participant_ID %in% with_files) |>
distinct(HTAN_Participant_ID, Primary_Diagnosis) |>
count(Primary_Diagnosis, sort = TRUE) |>
slice_head(n = 20) |>
knitr::kable(format.args = list(big.mark = ","))| Primary_Diagnosis | n |
|---|---|
| Ductal carcinoma in situ NOS | 787 |
| Not Reported | 311 |
| Adenocarcinoma NOS | 276 |
| Carcinoma NOS | 105 |
| Ductal carcinoma NOS | 103 |
| Malignant melanoma NOS | 90 |
| Neuroblastoma NOS | 56 |
| Squamous cell carcinoma NOS | 49 |
| High-grade serous carcinoma | 43 |
| Small cell neuroendocrine carcinoma | 43 |
| Pancreatobiliary-type carcinoma | 34 |
| Infiltrating duct carcinoma NOS | 32 |
| Adenocarcinoma pancreatobiliary type | 28 |
| Multiple myeloma | 28 |
| Adenocarcinoma metastatic NOS | 25 |
| Acute lymphoblastic leukemia precursor cell type | 22 |
| Melanoma NOS | 20 |
| Adenocarcinoma intestinal type | 19 |
| Central neuroblastoma | 18 |
| Glioma malignant | 16 |
HTAN’s emphasis on transitions shows here. Ductal carcinoma in situ, a non-invasive breast lesion, is the most common single diagnosis. Precursor states such as familial adenomatous polyposis appear further down the list, alongside metastatic and recurrent cancers. The NOS (“not otherwise specified”) terms are ICD-O-3 morphology names.
File counts by assay and data level:
assay_levels <- files |>
mutate(level = if_else(is.na(level), "none", paste("Level", level))) |>
count(modality, assay, level) |>
tidyr::pivot_wider(names_from = level, values_from = n,
values_fill = 0) |>
select(modality, assay, `Level 1`, `Level 2`, `Level 3`, `Level 4`,
none) |>
arrange(modality, assay)
knitr::kable(assay_levels, format.args = list(big.mark = ","))| modality | assay | Level 1 | Level 2 | Level 3 | Level 4 | none |
|---|---|---|---|---|---|---|
| Bulk sequencing | BulkMethylation-seq | 80 | 51 | 0 | 0 | 0 |
| Bulk sequencing | BulkRNA-seq | 4,134 | 1,922 | 640 | 0 | 0 |
| Bulk sequencing | BulkWES | 7,033 | 1,613 | 1,014 | 0 | 0 |
| Bulk sequencing | HI-C-seq | 66 | 34 | 2 | 0 | 0 |
| Electron microscopy | ElectronMicroscopy | 74,252 | 36,073 | 32 | 41 | 0 |
| H&E | Imaging | 0 | 2,332 | 0 | 0 | 0 |
| Multiplexed imaging | Imaging | 16 | 6,175 | 4,628 | 3,539 | 0 |
| Multiplexed imaging | SRRSImaging | 0 | 143 | 0 | 0 | 0 |
| Other | OtherAssay | 0 | 0 | 0 | 0 | 514 |
| Proteomics and metabolomics | MassSpectrometry | 1,187 | 0 | 28 | 28 | 0 |
| Proteomics and metabolomics | RPPA | 0 | 26 | 26 | 26 | 0 |
| Single-cell sequencing | ScATAC-seq | 2,548 | 344 | 1,006 | 218 | 0 |
| Single-cell sequencing | ScDNA-seq | 1,774 | 1,271 | 0 | 0 | 0 |
| Single-cell sequencing | ScRNA-seq | 15,201 | 5,755 | 3,705 | 857 | 0 |
| Spatial transcriptomics | 10XGenomicsXeniumISSExperiment | 0 | 0 | 0 | 0 | 33 |
| Spatial transcriptomics | 10xVisiumSpatialTranscriptomics-AuxiliaryFiles | 0 | 0 | 0 | 0 | 465 |
| Spatial transcriptomics | 10xVisiumSpatialTranscriptomics-RNA-seq | 794 | 223 | 1,264 | 175 | 0 |
| Spatial transcriptomics | ExSeqMinimal | 0 | 0 | 0 | 0 | 21,153 |
| Spatial transcriptomics | Imaging | 0 | 18,572 | 0 | 0 | 0 |
| Spatial transcriptomics | NanoStringGeoMxDSPSpatialTranscriptomics | 239 | 0 | 53 | 0 | 0 |
| Spatial transcriptomics | Slide-seq | 7 | 47 | 329 | 0 | 0 |
Sequencing assays have all four levels, but Levels 1 and 2 (reads) are controlled, and many datasets stop at Level 3 (count matrices). Level 4 single-cell objects exist for a subset of datasets.
Imaging starts at Level 2. Raw tiles (Level 1) were rarely submitted; the released image is the stitched OME-TIFF. Segmentation (Level 3) and cell tables (Level 4) exist for a fraction of images.
Some assays have no levels at all. Xenium experiments are single bundles, Visium auxiliary files and “Other Assay” files sit outside the scheme, and ExSeq uses a minimal metadata template.
Counting participants rather than files, center by modality:
heat <- file_participants |>
distinct(HTAN_Center, modality, HTAN_Participant_ID) |>
count(HTAN_Center, modality) |>
mutate(HTAN_Center = factor(HTAN_Center,
levels = rev(centers$HTAN_Center)))
ggplot(heat, aes(modality, HTAN_Center, fill = n)) +
geom_tile(colour = "white", linewidth = 0.6) +
geom_text(aes(label = n, colour = log10(n) > 0.55 * log10(max(n))),
size = 3) +
scale_fill_gradient(low = "#e3eefb", high = "#0d3d75", trans = "log10",
name = "Participants") +
scale_colour_manual(values = c(`TRUE` = "white", `FALSE` = "grey15"),
guide = "none") +
labs(x = NULL, y = NULL) +
theme(axis.text.x = element_text(angle = 35, hjust = 1),
panel.grid = element_blank())Rows are ordered by participant count. Single-cell sequencing and multiplexed imaging are common across the network: 11 of the 14 centers have both. Spatial transcriptomics is less widespread: 7 centers have it, led by HTAN WUSTL, HTAN TNP - TMA, HTAN HTAPP. Electron microscopy is OHSU’s alone, and proteomics and metabolomics cover only 20 participants. The darkest tiles are bulk sequencing and H&E at Duke. Its large breast cohort was characterised mainly by bulk RNA and exome sequencing and histology, with single-cell sequencing for only a few participants.
Level 2 images by imaging technology, with the participants and centers behind each:
imaging_participants <- file_participants |>
filter(!is.na(Imaging_Assay_Type)) |>
group_by(Imaging_Assay_Type) |>
summarise(participants = n_distinct(HTAN_Participant_ID),
centers = paste(sort(unique(sub("HTAN ", "", HTAN_Center))),
collapse = ", ")) |>
arrange(desc(participants))
imaging_files <- count(imaging_types, Imaging_Assay_Type, name = "images")
imaging_participants |>
left_join(imaging_files, by = "Imaging_Assay_Type") |>
select(Imaging_Assay_Type, images, participants, centers) |>
knitr::kable(format.args = list(big.mark = ","))| Imaging_Assay_Type | images | participants | centers |
|---|---|---|---|
| H&E | 2,332 | 1,113 | BU, Duke, HMS, HTAPP, Stanford, TNP SARDANA, Vanderbilt, WUSTL |
| t-CyCIF | 1,063 | 177 | HMS, OHSU, TNP - TMA, TNP SARDANA |
| MIBI | 300 | 168 | Duke, HTAPP, MSK |
| MxIF | 1,747 | 131 | BU, DFCI, MSK, Vanderbilt |
| CODEX | 1,415 | 102 | CHOP, DFCI, HTAPP, Stanford, TNP SARDANA, Vanderbilt, WUSTL |
| CyCIF | 717 | 102 | HMS, OHSU, TNP - TMA, TNP SARDANA |
| GeoMX-DSP | 103 | 101 | OHSU, TNP - TMA, Vanderbilt |
| mIHC | 644 | 78 | OHSU, TNP - TMA, TNP SARDANA |
| RareCyte Orion | 75 | 74 | HMS |
| MERFISH | 18,572 | 47 | DFCI, HTAPP |
| IMC | 104 | 27 | BU, WUSTL |
| SABER | 6 | 2 | TNP SARDANA |
| Not Applicable | 1 | 1 | BU |
H&E is the most widely shared modality. It is a common reference against which images from different technologies can be registered and compared. The multiplexed technologies (cyclic immunofluorescence in its CyCIF, t-CyCIF and MxIF forms, CODEX, mIHC, MIBI, IMC and Orion) measure tens of protein markers per cell in intact tissue. Their Level 4 cell tables are what read_htan() turns into SpatialExperiment objects. MERFISH is listed under imaging because it is image-based, but it measures RNA, so we count it as spatial transcriptomics.
Much of HTAN’s value is in participants profiled more than one way. We give each participant the set of modalities, out of five, that they have data for:
combos <- file_participants |>
filter(modality %in% c("Single-cell sequencing", "Spatial transcriptomics",
"Multiplexed imaging", "H&E", "Bulk sequencing")) |>
distinct(HTAN_Participant_ID, modality) |>
group_by(HTAN_Participant_ID) |>
summarise(combo = paste(sort(modality), collapse = " + ")) |>
count(combo, sort = TRUE)
head(combos, 12)
#> # A tibble: 12 × 2
#> combo n
#> <chr> <int>
#> 1 Bulk sequencing + H&E 574
#> 2 Bulk sequencing + Single-cell sequencing 417
#> 3 Single-cell sequencing 248
#> 4 H&E 227
#> 5 Bulk sequencing 143
#> 6 Multiplexed imaging 135
#> 7 Bulk sequencing + H&E + Multiplexed imaging 117
#> 8 H&E + Multiplexed imaging 104
#> 9 Bulk sequencing + Multiplexed imaging + Single-cell sequencing + Spati… 79
#> 10 Bulk sequencing + Single-cell sequencing + Spatial transcriptomics 67
#> 11 Multiplexed imaging + Spatial transcriptomics 58
#> 12 Bulk sequencing + Multiplexed imaging + Single-cell sequencing 52combos |>
slice_head(n = 12) |>
ggplot(aes(n, reorder(combo, n))) +
geom_col(fill = blue, width = 0.7) +
geom_text(aes(label = fmt(n)), hjust = -0.15, size = 3.2,
colour = "grey25") +
scale_x_continuous(expand = expansion(mult = c(0, 0.12))) +
labs(x = "Participants", y = NULL)986 participants have single-cell sequencing, 274 spatial transcriptomics and 668 multiplexed imaging. 156 have both single-cell and spatial transcriptomics, and 222 have both single-cell sequencing and multiplexed imaging. In these participants, cell types defined from dissociated cells can be located in tissue. 783 participants (32% of those with any of these five modalities) were profiled with only one, so cross-modal analyses draw on a much smaller set than the headline participant count.
Open Level 3 and 4 files are the starting point for most reanalyses. By modality and file type:
files |>
filter(access == "open-synapse", level %in% c(3, 4)) |>
count(modality, level, extension) |>
group_by(modality, level) |>
slice_max(n, n = 4, with_ties = FALSE) |>
ungroup() |>
arrange(modality, level, desc(n)) |>
knitr::kable(format.args = list(big.mark = ","))| modality | level | extension | n |
|---|---|---|---|
| Bulk sequencing | 3 | .rds | 525 |
| Bulk sequencing | 3 | .vcf | 452 |
| Bulk sequencing | 3 | .seg | 238 |
| Bulk sequencing | 3 | .vcf.gz | 228 |
| Electron microscopy | 3 | .am | 29 |
| Electron microscopy | 3 | .mask | 1 |
| Electron microscopy | 3 | .model | 1 |
| Electron microscopy | 3 | .tif | 1 |
| Electron microscopy | 4 | .avi | 31 |
| Electron microscopy | 4 | .mp4 | 9 |
| Electron microscopy | 4 | .mpg | 1 |
| Multiplexed imaging | 3 | .tif | 2,857 |
| Multiplexed imaging | 3 | .png | 1,573 |
| Multiplexed imaging | 3 | .tiff | 174 |
| Multiplexed imaging | 3 | .csv | 24 |
| Multiplexed imaging | 4 | .csv | 3,520 |
| Multiplexed imaging | 4 | .h5ad | 13 |
| Multiplexed imaging | 4 | .txt | 4 |
| Multiplexed imaging | 4 | 1 | |
| Proteomics and metabolomics | 3 | .txt | 33 |
| Proteomics and metabolomics | 3 | .csv | 21 |
| Proteomics and metabolomics | 4 | .txt | 54 |
| Single-cell sequencing | 3 | .tsv.gz | 2,158 |
| Single-cell sequencing | 3 | .mtx.gz | 946 |
| Single-cell sequencing | 3 | .csv | 861 |
| Single-cell sequencing | 3 | .mtx | 307 |
| Single-cell sequencing | 4 | .rds | 647 |
| Single-cell sequencing | 4 | .csv | 223 |
| Single-cell sequencing | 4 | .txt | 76 |
| Single-cell sequencing | 4 | .h5ad | 70 |
| Spatial transcriptomics | 3 | .tsv.gz | 542 |
| Spatial transcriptomics | 3 | .mtx.gz | 318 |
| Spatial transcriptomics | 3 | .csv | 274 |
| Spatial transcriptomics | 3 | .txt | 188 |
| Spatial transcriptomics | 4 | .rds | 132 |
| Spatial transcriptomics | 4 | .h5ad | 43 |
Single-cell Level 4 objects come mostly as .rds (R objects, often Seurat) and .h5ad (AnnData), with some delimited tables; Level 3 is count matrices in 10x, MatrixMarket and delimited formats. Imaging Level 4 is almost entirely CSV cell tables, and Level 3 is segmentation masks. read_htan() covers the .rds, .h5ad, 10x .h5, Space Ranger and CSV cases.
File sizes span several orders of magnitude. That matters when you plan downloads.
files |>
filter(access == "open-synapse", level %in% c(3, 4), File_Size > 0,
modality %in% c("Single-cell sequencing", "Spatial transcriptomics",
"Multiplexed imaging", "Bulk sequencing")) |>
ggplot(aes(File_Size, modality)) +
geom_boxplot(outlier.size = 0.4, outlier.alpha = 0.3, fill = "#e3eefb",
colour = blue, width = 0.6) +
facet_wrap(~ paste("Level", level)) +
scale_x_log10(labels = scales::label_bytes(units = "auto_si")) +
labs(x = "File size", y = NULL)files |>
filter(access == "open-synapse", level %in% c(3, 4), File_Size > 0,
modality %in% c("Single-cell sequencing", "Spatial transcriptomics",
"Multiplexed imaging", "Bulk sequencing")) |>
group_by(modality, level) |>
summarise(files = n(),
median = scales::label_bytes(units = "auto_si")(median(File_Size)),
largest = scales::label_bytes(units = "auto_si")(max(File_Size)),
.groups = "drop")
#> # A tibble: 7 × 5
#> modality level files median largest
#> <chr> <int> <int> <chr> <chr>
#> 1 Bulk sequencing 3 1656 3 MB 432 MB
#> 2 Multiplexed imaging 3 4626 2 MB 8 GB
#> 3 Multiplexed imaging 4 3539 4 MB 18 GB
#> 4 Single-cell sequencing 3 4711 18 MB 5 GB
#> 5 Single-cell sequencing 4 1075 389 MB 40 GB
#> 6 Spatial transcriptomics 3 1646 294 kB 124 MB
#> 7 Spatial transcriptomics 4 175 158 MB 365 MBLevel 3 spatial transcriptomics files are small because each Space Ranger output is split into its pieces (barcodes, features, matrix, positions). Level 4 single-cell objects are the largest, since many combine several samples into one object. htan_download() refuses downloads above 20 GB in total unless you raise max_bytes.
Biospecimens by tumour tissue type, with the various missing-value codes folded into “Not reported”:
norm <- function(x) {
x <- trimws(x)
x[is.na(x) | tolower(x) %in% c("not reported", "not recorded",
"unknown", "not applicable", "")] <- "Not reported"
paste0(toupper(substr(x, 1, 1)), substr(x, 2, nchar(x)))
}
bios |>
mutate(Tumor_Tissue_Type = norm(Tumor_Tissue_Type)) |>
count(Tumor_Tissue_Type, sort = TRUE) |>
knitr::kable(format.args = list(big.mark = ","))| Tumor_Tissue_Type | n |
|---|---|
| Primary | 5,612 |
| Metastatic | 2,340 |
| Not Otherwise Specified | 2,259 |
| Premalignant | 894 |
| Normal | 399 |
| Atypia - hyperplasia | 343 |
| Normal adjacent | 343 |
| Not reported | 273 |
| Premalignant - in situ | 237 |
| Recurrent | 230 |
| Additional Primary | 101 |
| Post therapy neoadjuvant | 58 |
| Local recurrence | 13 |
| Normal distant | 3 |
Primary tumour is the largest category (43% of biospecimens). Metastatic, premalignant and atypical tissue together are a large share (premalignant and atypia alone are 11%), and that is the material the study of transitions needs. Normal and normal-adjacent tissue are 6%, so comparisons against normal tissue are possible only for some atlases.
By preservation method:
bios |>
mutate(Preservation_Method = norm(Preservation_Method)) |>
count(Preservation_Method, sort = TRUE) |>
slice_head(n = 8) |>
knitr::kable(format.args = list(big.mark = ","))| Preservation_Method | n |
|---|---|
| Formalin fixed paraffin embedded - FFPE | 6,013 |
| Not reported | 1,926 |
| Snap Frozen | 1,255 |
| Fresh | 1,081 |
| OCT | 915 |
| Frozen | 832 |
| Liquid Nitrogen | 351 |
| Negative 80 Deg C | 234 |
Formalin-fixed paraffin-embedded (FFPE) tissue is the majority. FFPE suits imaging and some spatial methods; fresh or frozen tissue is needed for most single-cell sequencing. So a biospecimen’s preservation method largely predicts which assays could have been run on it.
Tier 1 clinical data are spread over seven tables. Here are their sizes and the number of participants each covers, with the tier 2 table alongside:
clin <- c("clinical_tier1_demographics", "clinical_tier1_diagnosis",
"clinical_tier1_therapy", "clinical_tier1_followup",
"clinical_tier1_exposure", "clinical_tier1_familyhistory",
"clinical_tier1_moleculartest", "clinical_tier2")
bind_rows(lapply(clin, function(t) {
htan_tbl(t, con) |>
summarise(rows = n(), participants = n_distinct(HTAN_Participant_ID)) |>
collect() |>
mutate(table = t, .before = 1)
})) |>
knitr::kable(format.args = list(big.mark = ","))| table | rows | participants |
|---|---|---|
| clinical_tier1_demographics | 2,805 | 2,797 |
| clinical_tier1_diagnosis | 2,300 | 2,273 |
| clinical_tier1_therapy | 3,337 | 1,930 |
| clinical_tier1_followup | 2,167 | 1,861 |
| clinical_tier1_exposure | 2,004 | 2,001 |
| clinical_tier1_familyhistory | 2,284 | 1,932 |
| clinical_tier1_moleculartest | 4,700 | 1,005 |
| clinical_tier2 | 359 | 206 |
The tier 3 tables add disease-specific fields for a few hundred participants:
htan_tables(con) |>
filter(grepl("tier3", name)) |>
select(name, rows)
#> # A tibble: 5 × 2
#> name rows
#> <chr> <dbl>
#> 1 clinical_tier3_brain 82
#> 2 clinical_tier3_breast 129
#> 3 clinical_tier3_colorectal 304
#> 4 clinical_tier3_lung 77
#> 5 clinical_tier3_melanoma 61A column can exist and still be mostly empty. For each diagnosis field we compute the share of records with an informative value: not missing, and not “Not Reported”, “unknown”, “Not Applicable” or a similar code.
uninformative <- c("", "not reported", "not recorded", "unknown",
"not applicable", "not available", "na", "unknown primary site")
completeness <- function(table) {
x <- htan_tbl(table, con) |> collect()
x <- x[setdiff(names(x), c("Component", "Id", "entityId", "Manifest_Id",
"Manifest_Version", "HTAN_Center", "HTAN_Participant_ID"))]
informative <- vapply(x, function(v) {
v <- tolower(trimws(as.character(v)))
mean(!is.na(v) & !v %in% uninformative)
}, numeric(1))
tibble::tibble(table = table, field = names(informative),
informative = informative)
}
dx_complete <- completeness("clinical_tier1_diagnosis") |>
arrange(desc(informative))
head(dx_complete, 10)
#> # A tibble: 10 × 3
#> table field informative
#> <chr> <chr> <dbl>
#> 1 clinical_tier1_diagnosis Age_at_Diagnosis 0.971
#> 2 clinical_tier1_diagnosis Tissue_or_Organ_of_Origin 0.915
#> 3 clinical_tier1_diagnosis Days_to_Last_Follow_up 0.887
#> 4 clinical_tier1_diagnosis Days_to_Last_Known_Disease_Status 0.887
#> 5 clinical_tier1_diagnosis Primary_Diagnosis 0.864
#> 6 clinical_tier1_diagnosis Site_of_Resection_or_Biopsy 0.853
#> 7 clinical_tier1_diagnosis Last_Known_Disease_Status 0.761
#> 8 clinical_tier1_diagnosis Progression_or_Recurrence 0.7
#> 9 clinical_tier1_diagnosis Year_of_Diagnosis 0.576
#> 10 clinical_tier1_diagnosis Tumor_Grade 0.564dx_complete |>
slice_head(n = 30) |>
ggplot(aes(informative, reorder(field, informative))) +
geom_col(fill = blue, width = 0.7) +
scale_x_continuous(labels = scales::label_percent(),
limits = c(0, 1), expand = expansion(mult = c(0, 0.02))) +
labs(x = "Records with an informative value", y = NULL)Of the 86 diagnosis fields, 11 are informative in at least half the records. Primary diagnosis, site, age and the main staging fields are usable for most participants. The long tail of disease-specific fields (Gleason patterns, neuroblastoma classifications, Breslow thickness) is filled only where it applies, so it is mostly empty overall. Before you stratify an analysis by a clinical variable, compute its completeness in the subset you will analyse rather than in the whole release.
Gender, vital status and race, one row per participant with data:
demo_clean <- demo |>
filter(HTAN_Participant_ID %in% with_files) |>
distinct(HTAN_Participant_ID, .keep_all = TRUE) |>
mutate(across(c(Gender, Race, Ethnicity, Vital_Status), norm))
count(demo_clean, Gender, sort = TRUE)
#> # A tibble: 3 × 2
#> Gender n
#> <chr> <int>
#> 1 Female 1734
#> 2 Male 524
#> 3 Not reported 94
count(demo_clean, Vital_Status, sort = TRUE)
#> # A tibble: 3 × 2
#> Vital_Status n
#> <chr> <int>
#> 1 Alive 1389
#> 2 Dead 483
#> 3 Not reported 480
count(demo_clean, Race, sort = TRUE)
#> # A tibble: 6 × 2
#> Race n
#> <chr> <int>
#> 1 White 1611
#> 2 Not reported 347
#> 3 Black or african american 316
#> 4 Asian 53
#> 5 Other 19
#> 6 Not allowed to collect 6Most participants are female, because of the breast atlases. Vital status is known for 80% of participants with data, but outcome analyses also need follow-up times, whose completeness varies by atlas; check it in your subset.
Age at diagnosis is recorded in days since birth, and two things about the values matter before you use them. First, 0 is a placeholder, not an age (see the data model vignette). Second, ages are modified for privacy: the demographics field Age_Is_Obfuscated says whether a participant’s age “has been modified for compliance reasons”, and the most common value is exactly 6,573 days (18 years), which looks like a floor applied to younger participants.
ages <- htan_tbl("clinical_tier1_diagnosis", con) |>
select(HTAN_Participant_ID, HTAN_Center, Age_at_Diagnosis) |>
collect() |>
filter(HTAN_Participant_ID %in% with_files) |>
distinct(HTAN_Participant_ID, .keep_all = TRUE) |>
mutate(age_days = suppressWarnings(as.numeric(Age_at_Diagnosis)))
ages |>
summarise(
participants = n(),
missing_or_code = sum(is.na(age_days)),
zero = sum(age_days == 0, na.rm = TRUE),
at_18_years = sum(age_days == 6573, na.rm = TRUE)
)
#> # A tibble: 1 × 4
#> participants missing_or_code zero at_18_years
#> <int> <int> <int> <int>
#> 1 2273 66 287 318
count(demo_clean, Age_Is_Obfuscated)
#> # A tibble: 3 × 2
#> Age_Is_Obfuscated n
#> <chr> <int>
#> 1 False 146
#> 2 True 1746
#> 3 <NA> 460
ages |>
filter(age_days == 6573) |>
count(HTAN_Center, sort = TRUE)
#> # A tibble: 10 × 2
#> HTAN_Center n
#> <chr> <int>
#> 1 HTAN HTAPP 82
#> 2 HTAN DFCI 61
#> 3 HTAN CHOP 48
#> 4 HTAN HMS 44
#> 5 HTAN WUSTL 37
#> 6 HTAN MSK 29
#> 7 HTAN Stanford 9
#> 8 HTAN BU 6
#> 9 HTAN TNP - TMA 1
#> 10 HTAN TNP SARDANA 1ages |>
filter(age_days > 0) |>
ggplot(aes(age_days / 365.25)) +
geom_histogram(binwidth = 2, boundary = 0, fill = blue,
colour = "white") +
labs(x = "Age at diagnosis (years)", y = "Participants")1,920 participants have a non-zero age, with a median of 53 years. The spike at 18 includes the CHOP pediatric participants, so HTAN’s age data cannot separate children from young adults. Read every age as approximate.
HTAN set out to sample tumours across time. The phase1_longitudinal_data table puts each participant’s biopsies, diagnoses, treatments and follow-up on a single timeline (days from the index date):
long <- htan_tbl("phase1_longitudinal_data", con) |> collect()
count(long, Event_Type, sort = TRUE)
#> # A tibble: 13 × 2
#> Event_Type n
#> <chr> <int>
#> 1 Biopsy or Resection 1731
#> 2 Diagnosis 1492
#> 3 Last Followup_from_followup 1321
#> 4 Vital Status 803
#> 5 Recurrence 571
#> 6 Recurrence_from_followup 542
#> 7 Therapy 265
#> 8 Molecular Test 238
#> 9 Last Follow Up 217
#> 10 Last Known Disease Status 182
#> 11 Progression_from_followup 160
#> 12 Progression 57
#> 13 Progression Free 41
biopsies <- long |>
filter(Event_Type == "Biopsy or Resection") |>
group_by(HTAN_Participant_ID) |>
summarise(timepoints = n_distinct(Start))
table(timepoints = pmin(biopsies$timepoints, 4))
#> timepoints
#> 1
#> 17310 participants have biopsies or resections on two or more different days (the 4 row is four or more). They are the material for before-and-after comparisons. The rest were sampled on a single day, sometimes at several sites or as several sections; that supports comparisons across space but not across time.
.rds, .h5ad, 10x and CSV files that read_htan() can read.The analysis vignette takes one participant from this atlas through download and analysis.
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=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 dplyr_1.2.1 HTAN_0.99.1
#>
#> loaded via a namespace (and not attached):
#> [1] utf8_1.2.6 generics_0.1.4 tidyr_1.3.2
#> [4] RSQLite_3.53.3 digest_0.6.39 magrittr_2.0.5
#> [7] evaluate_1.0.5 grid_4.6.1 RColorBrewer_1.1-3
#> [10] fastmap_1.2.0 blob_1.3.0 jsonlite_2.0.0
#> [13] DBI_1.3.0 purrr_1.2.2 scales_1.4.0
#> [16] httr2_1.3.0 duckdb_1.5.6 cli_3.6.6
#> [19] rlang_1.3.0 dbplyr_2.6.0 bit64_4.8.6
#> [22] withr_3.0.3 cachem_1.1.0 yaml_2.3.12
#> [25] otel_0.2.0 tools_4.6.1 memoise_2.0.1
#> [28] filelock_1.0.3 curl_8.1.0 buildtools_1.0.0
#> [31] vctrs_0.7.3 R6_2.6.1 BiocFileCache_3.3.0
#> [34] lifecycle_1.0.5 bit_4.6.0 pkgconfig_2.0.3
#> [37] pillar_1.11.1 gtable_0.3.6 glue_1.8.1
#> [40] xfun_0.62 tibble_3.3.1 tidyselect_1.2.1
#> [43] sys_3.4.3 knitr_1.52 farver_2.1.2
#> [46] htmltools_0.5.9 rmarkdown_2.32 maketools_1.3.2
#> [49] labeling_0.4.3 compiler_4.6.1 S7_0.2.2