How to Describe Data in R

R Published Updated 4 mins read By Leon Wei
How to Describe Data in R cover image

Quick summary

Summarize this blog with AI

Introduction

A useful data description states what one row represents, the variable types and units, missingness, distribution and relevant groups. This lesson uses ggplot2’s supplied diamonds dataset and aggregates repeated categorical combinations before drawing a heatmap. Description of this dataset is not a claim about current diamond prices or a representative market sample.

Tested with R 4.5.1, dplyr 1.1.4 and ggplot2 4.0.0. Run the code blocks in order. Install the named packages before starting; inputs are supplied below.

Identify the rows and variables first

The dataset documentation describes 53,940 round-cut diamonds, with price in US dollars, carat weight, ordered quality categories and depth percentage. Keep numerical identifiers or category codes out of numeric summaries unless their magnitude has meaning.

library(dplyr)
 library(ggplot2)
 dat <- ggplot2::diamonds
 print(dim(dat))
 print(vapply(dat, function(x) paste(class(x), collapse = "/"), character(1)))
 missing <- vapply(dat, function(x) sum(is.na(x)), integer(1))
 print(missing)
 stopifnot(nrow(dat) == 53940, ncol(dat) == 10, is.ordered(dat$cut),
           is.numeric(dat$price), sum(missing) == 0)

The supplied dataset has 10 columns and no NA cells. That does not certify data quality: impossible values, duplicates, measurement errors and nonrepresentative sampling are separate checks. Zero dimensions in some rows, for example, warrant investigation rather than an automatic zero-to-missing conversion across all fields.

Summarize numeric data with explicit missingness policy

Report observed count, mean, median and spread rather than a mean alone. R’s sd() is the sample SD; IQR() is the difference between the 75th and 25th sample quantiles. A safe empty-group policy avoids converting an unknown mean into zero.

describe_numeric <- function(x) {
   if (!is.numeric(x) || !is.null(dim(x)) || any(is.infinite(x)))
     stop("Need a numeric vector without infinite values")
   seen <- x[!is.na(x)]
   c(rows = length(x), observed = length(seen), missing = sum(is.na(x)),
     mean = if (length(seen)) mean(seen) else NA_real_,
     median = if (length(seen)) median(seen) else NA_real_,
     sample_sd = if (length(seen) > 1L) sd(seen) else NA_real_,
     IQR = if (length(seen)) IQR(seen) else NA_real_)
 }
 print(round(describe_numeric(dat$price), 2))
 print(table(dat$cut, useNA = "ifany"))
 stopifnot(describe_numeric(dat$price)["observed"] == nrow(dat),
           sum(table(dat$cut)) == nrow(dat),
           is.na(describe_numeric(numeric())["mean"]),
           is.na(describe_numeric(c(NA_real_, NA_real_))["mean"]),
           is.na(describe_numeric(5)["sample_sd"]))

Prices have mean 3932.80 and median 2401 dollars in this dataset, indicating a right-skewed distribution. The most frequent cut is Ideal with 21,551 observations. For statistical modes, specify tie handling; base mode() reports storage information rather than frequency.

Show the full distribution rather than only averages

This histogram uses fixed 1,000-dollar bins and counts. Its axis is price, not row number. It shows the concentration of lower prices and the long right tail. A normality test is not required to report these descriptive statistics.

p_distribution <- ggplot(dat, aes(price)) +
   geom_histogram(breaks = seq(0, 19000, 1000), fill = "#679fb5", color = "white") +
   labs(title = "Price distribution in the supplied dataset", x = "Price (US dollars)",
        y = "Number of diamonds") + theme_minimal(base_size = 12)
 print(p_distribution)
 stopifnot(sum(ggplot_build(p_distribution)$data[[1]]$count) == nrow(dat))
Diamond price distributionCounts in 1000-dollar intervals show a long right tail. 0 5000 10000 15000 0 5000 10000 15000 Price (US dollars) Number of diamonds Price distribution in the supplied dataset
The supplied dataset is descriptive, not current-market pricing.

Aggregate to one row per heatmap cell

Many diamonds share a cut/clarity pair. Drawing one tile per original row would overplot those cells, with later rows hiding earlier ones. First calculate one mean depth percentage and a count per pair, then pass the resulting table to geom_tile(). The number of observations differs across cells, so keep counts with the summary.

cells <- dat |>
   group_by(cut, clarity) |>
   summarise(n = n(), mean_depth = mean(depth), .groups = "drop")
 stopifnot(nrow(cells) == 40, !anyDuplicated(cells[c("cut", "clarity")]),
           sum(cells$n) == nrow(dat))
 print(head(cells))
 p_cells <- ggplot(cells, aes(cut, clarity, fill = mean_depth)) +
   geom_tile(color = "white") +
   geom_text(aes(label = sprintf("%.1f", mean_depth)), size = 3) +
   scale_fill_gradient(low = "#edf4f7", high = "#287391") +
   labs(title = "Mean depth by cut and clarity", x = "Cut", y = "Clarity",
        fill = "Mean depth (%)", caption = "One aggregated value per cell; group sizes differ.") +
   theme_minimal(base_size = 12)
 print(p_cells)
 stopifnot(nrow(ggplot_build(p_cells)$data[[1]]) == nrow(cells))
Mean depth by cut and clarityForty categorical cells each show an aggregated mean depth percentage. 65.7 64.4 63.9 63.6 62.9 62.8 60.4 60.1 62.1 62.2 62.7 62.4 62.0 62.3 62.1 61.4 61.8 61.9 61.9 61.8 61.7 61.7 61.7 61.4 61.2 61.2 61.3 61.3 61.3 61.3 61.3 61.0 61.6 61.7 61.7 61.7 61.7 61.7 61.7 61.6 I1 SI2 SI1 VS2 VS1 VVS2 VVS1 IF Fair Good Very Good Premium Ideal Cut Clarity Mean depth (%) 61 62 63 64 65 Mean depth by cut and clarity One aggregated value per cell; group sizes differ.
The plotted data contain exactly one row per cut/clarity combination.

The heatmap has 40 tiles, not 53,940 overlapping ones. Mean depth by group is a descriptive comparison, not a causal effect of a quality label. An absent group would need an explicit missing-cell representation, not a fabricated zero depth.

Keep association separate from causation

Carat and price are paired numeric variables, so a sample correlation can summarize their linear association. The coefficient does not adjust for cut, clarity or other attributes and does not identify a causal effect. For prediction, reserve evaluation data rather than choosing a model by training fit alone.

paired <- dat |> filter(is.finite(carat), is.finite(price))
 r <- cor(paired$carat, paired$price)
 cat("Paired rows:", nrow(paired), "Pearson r:", round(r, 4), "
")
 stopifnot(nrow(paired) == 53940, r > 0, r < 1)
 missing_demo <- c(0, 10, NA_real_)
 stopifnot(describe_numeric(missing_demo)["mean"] == 5,
           mean(replace(missing_demo, is.na(missing_demo), 0)) != 5)

The Pearson coefficient is approximately 0.9216 for these observations. The final check illustrates why replacing an unknown with zero changes a summary. Preserve raw data and write down the inclusion rules, units, denominator and aggregation grain alongside each reported statistic.

Primary references

Interview Prep

Begin Your SQL, Python, and R Journey

Master 230 interview-style coding questions and build the data skills needed for analyst, scientist, and engineering roles.

Related Articles

All Articles
Data Normalization in R cover image
r Oct 17, 2026

Data Normalization in R

Learn how to normalize data in R with comprehensive tutorials, code samples, and best practices for beginners.

How to Transpose Data in R cover image
r May 6, 2024

How to Transpose Data in R

Dive into the essentials of transposing data in R with this comprehensive guide. Perfect for beginners aiming to enhance their R programming ski…

How to Sort an R Data Frame cover image
r May 1, 2024

How to Sort an R Data Frame

Learn how to effectively sort R data frames with this comprehensive guide for beginners, featuring detailed R code examples.