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))
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))
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.