R Data Exploration Best Practices¶
Objective: Master senior-level R data exploration patterns for production systems. When you need to understand complex datasets, when you want to discover patterns and insights, when you need enterprise-grade exploration patternsโthese best practices become your weapon of choice.
Core Principles¶
- Systematic Approach: Follow a structured exploration methodology
- Visualization First: Use visualizations to understand data patterns
- Statistical Summary: Complement visuals with statistical summaries
- Data Quality: Assess and document data quality issues
- Documentation: Document findings and insights
Data Overview and Structure¶
Initial Data Inspection¶
# R/01-data-exploration.R
#' Comprehensive data overview
#'
#' @param data Data frame to explore
#' @return List containing data overview information
data_overview <- function(data) {
overview <- list(
dimensions = dim(data),
column_names = names(data),
data_types = sapply(data, class),
missing_values = colSums(is.na(data)),
unique_values = sapply(data, function(x) length(unique(x))),
memory_usage = object.size(data)
)
# Add summary statistics for numeric columns
numeric_cols <- sapply(data, is.numeric)
if (sum(numeric_cols) > 0) {
overview$numeric_summary <- summary(data[, numeric_cols, drop = FALSE])
}
# Add frequency tables for categorical columns
categorical_cols <- sapply(data, function(x) is.character(x) || is.factor(x))
if (sum(categorical_cols) > 0) {
overview$categorical_summary <- lapply(data[, categorical_cols, drop = FALSE], table)
}
return(overview)
}
#' Print data overview in a formatted way
#'
#' @param overview Data overview list
print_data_overview <- function(overview) {
cat("Data Overview\n")
cat("=============\n")
cat("Dimensions:", overview$dimensions[1], "rows x", overview$dimensions[2], "columns\n")
cat("Memory usage:", format(overview$memory_usage, units = "MB"), "\n\n")
cat("Column Information\n")
cat("------------------\n")
for (i in seq_along(overview$column_names)) {
col_name <- overview$column_names[i]
col_type <- overview$data_types[i]
missing_count <- overview$missing_values[i]
unique_count <- overview$unique_values[i]
cat(sprintf("%-20s %-10s Missing: %-5d Unique: %-5d\n",
col_name, col_type, missing_count, unique_count))
}
if (!is.null(overview$numeric_summary)) {
cat("\nNumeric Summary\n")
cat("---------------\n")
print(overview$numeric_summary)
}
if (!is.null(overview$categorical_summary)) {
cat("\nCategorical Summary\n")
cat("-------------------\n")
for (i in seq_along(overview$categorical_summary)) {
cat("\n", names(overview$categorical_summary)[i], ":\n")
print(overview$categorical_summary[[i]])
}
}
}
Data Quality Assessment¶
# R/01-data-exploration.R (continued)
#' Assess data quality
#'
#' @param data Data frame to assess
#' @return Data quality report
assess_data_quality <- function(data) {
quality_report <- list(
completeness = assess_completeness(data),
consistency = assess_consistency(data),
accuracy = assess_accuracy(data),
validity = assess_validity(data)
)
return(quality_report)
}
#' Assess data completeness
#'
#' @param data Data frame
#' @return Completeness metrics
assess_completeness <- function(data) {
total_cells <- nrow(data) * ncol(data)
missing_cells <- sum(is.na(data))
completeness <- list(
total_cells = total_cells,
missing_cells = missing_cells,
completeness_rate = (total_cells - missing_cells) / total_cells,
columns_with_missing = names(data)[colSums(is.na(data)) > 0],
rows_with_missing = sum(rowSums(is.na(data)) > 0),
missing_patterns = identify_missing_patterns(data)
)
return(completeness)
}
#' Identify missing value patterns
#'
#' @param data Data frame
#' @return Missing value patterns
identify_missing_patterns <- function(data) {
missing_matrix <- is.na(data)
# Find columns that are always missing together
missing_correlations <- cor(missing_matrix)
missing_correlations[is.na(missing_correlations)] <- 0
# Find rows with similar missing patterns
missing_patterns <- table(apply(missing_matrix, 1, paste, collapse = ""))
return(list(
missing_correlations = missing_correlations,
missing_patterns = missing_patterns
))
}
#' Assess data consistency
#'
#' @param data Data frame
#' @return Consistency metrics
assess_consistency <- function(data) {
consistency <- list(
duplicate_rows = sum(duplicated(data)),
inconsistent_categories = identify_inconsistent_categories(data),
outlier_columns = identify_outlier_columns(data)
)
return(consistency)
}
#' Identify inconsistent categories
#'
#' @param data Data frame
#' @return Inconsistent categories
identify_inconsistent_categories <- function(data) {
categorical_cols <- sapply(data, function(x) is.character(x) || is.factor(x))
inconsistencies <- list()
for (col in names(data)[categorical_cols]) {
values <- data[[col]]
unique_values <- unique(values[!is.na(values)])
# Check for case inconsistencies
case_insensitive <- tolower(unique_values)
if (length(unique_values) != length(unique(case_insensitive))) {
inconsistencies[[col]] <- "Case inconsistencies detected"
}
# Check for whitespace inconsistencies
trimmed_values <- trimws(unique_values)
if (length(unique_values) != length(unique(trimmed_values))) {
inconsistencies[[col]] <- c(inconsistencies[[col]], "Whitespace inconsistencies detected")
}
}
return(inconsistencies)
}
#' Identify columns with outliers
#'
#' @param data Data frame
#' @return Outlier information
identify_outlier_columns <- function(data) {
numeric_cols <- sapply(data, is.numeric)
outlier_info <- list()
for (col in names(data)[numeric_cols]) {
values <- data[[col]][!is.na(data[[col]])]
if (length(values) > 0) {
Q1 <- quantile(values, 0.25)
Q3 <- quantile(values, 0.75)
IQR <- Q3 - Q1
lower_bound <- Q1 - 1.5 * IQR
upper_bound <- Q3 + 1.5 * IQR
outliers <- values[values < lower_bound | values > upper_bound]
if (length(outliers) > 0) {
outlier_info[[col]] <- list(
count = length(outliers),
percentage = length(outliers) / length(values) * 100,
values = outliers
)
}
}
}
return(outlier_info)
}
Statistical Exploration¶
Descriptive Statistics¶
# R/02-statistical-exploration.R
#' Comprehensive descriptive statistics
#'
#' @param data Data frame
#' @param group_by Column to group by (optional)
#' @return Descriptive statistics
descriptive_statistics <- function(data, group_by = NULL) {
numeric_cols <- sapply(data, is.numeric)
if (sum(numeric_cols) == 0) {
return(NULL)
}
numeric_data <- data[, numeric_cols, drop = FALSE]
if (is.null(group_by)) {
stats <- calculate_basic_stats(numeric_data)
} else {
stats <- calculate_grouped_stats(numeric_data, data[[group_by]])
}
return(stats)
}
#' Calculate basic statistics
#'
#' @param data Numeric data frame
#' @return Basic statistics
calculate_basic_stats <- function(data) {
stats <- data.frame(
variable = names(data),
n = sapply(data, function(x) sum(!is.na(x))),
mean = sapply(data, mean, na.rm = TRUE),
median = sapply(data, median, na.rm = TRUE),
sd = sapply(data, sd, na.rm = TRUE),
min = sapply(data, min, na.rm = TRUE),
max = sapply(data, max, na.rm = TRUE),
q25 = sapply(data, quantile, 0.25, na.rm = TRUE),
q75 = sapply(data, quantile, 0.75, na.rm = TRUE),
skewness = sapply(data, calculate_skewness),
kurtosis = sapply(data, calculate_kurtosis),
stringsAsFactors = FALSE
)
return(stats)
}
#' Calculate grouped statistics
#'
#' @param data Numeric data frame
#' @param group_by Grouping variable
#' @return Grouped statistics
calculate_grouped_stats <- function(data, group_by) {
groups <- unique(group_by[!is.na(group_by)])
stats_list <- list()
for (group in groups) {
group_data <- data[group_by == group, , drop = FALSE]
stats_list[[as.character(group)]] <- calculate_basic_stats(group_data)
}
return(stats_list)
}
#' Calculate skewness
#'
#' @param x Numeric vector
#' @return Skewness value
calculate_skewness <- function(x) {
x <- x[!is.na(x)]
if (length(x) < 3) return(NA)
mean_x <- mean(x)
sd_x <- sd(x)
if (sd_x == 0) return(0)
skewness <- sum(((x - mean_x) / sd_x)^3) / length(x)
return(skewness)
}
#' Calculate kurtosis
#'
#' @param x Numeric vector
#' @return Kurtosis value
calculate_kurtosis <- function(x) {
x <- x[!is.na(x)]
if (length(x) < 4) return(NA)
mean_x <- mean(x)
sd_x <- sd(x)
if (sd_x == 0) return(0)
kurtosis <- sum(((x - mean_x) / sd_x)^4) / length(x) - 3
return(kurtosis)
}
Correlation Analysis¶
# R/02-statistical-exploration.R (continued)
#' Comprehensive correlation analysis
#'
#' @param data Data frame
#' @param method Correlation method
#' @return Correlation analysis results
correlation_analysis <- function(data, method = "pearson") {
numeric_cols <- sapply(data, is.numeric)
if (sum(numeric_cols) < 2) {
return(NULL)
}
numeric_data <- data[, numeric_cols, drop = FALSE]
# Calculate correlation matrix
cor_matrix <- cor(numeric_data, use = "complete.obs", method = method)
# Find high correlations
high_correlations <- find_high_correlations(cor_matrix)
# Calculate partial correlations
partial_correlations <- calculate_partial_correlations(numeric_data)
# Test correlation significance
significance_tests <- test_correlation_significance(numeric_data, method)
results <- list(
correlation_matrix = cor_matrix,
high_correlations = high_correlations,
partial_correlations = partial_correlations,
significance_tests = significance_tests
)
return(results)
}
#' Find high correlations
#'
#' @param cor_matrix Correlation matrix
#' @param threshold Correlation threshold
#' @return High correlation pairs
find_high_correlations <- function(cor_matrix, threshold = 0.7) {
# Remove diagonal and get upper triangle
cor_matrix[upper.tri(cor_matrix, diag = TRUE)] <- NA
# Find high correlations
high_cor <- which(abs(cor_matrix) > threshold, arr.ind = TRUE)
if (nrow(high_cor) == 0) {
return(data.frame())
}
results <- data.frame(
variable1 = rownames(cor_matrix)[high_cor[, 1]],
variable2 = colnames(cor_matrix)[high_cor[, 2]],
correlation = cor_matrix[high_cor],
stringsAsFactors = FALSE
)
# Sort by absolute correlation
results <- results[order(abs(results$correlation), decreasing = TRUE), ]
return(results)
}
#' Calculate partial correlations
#'
#' @param data Numeric data frame
#' @return Partial correlation matrix
calculate_partial_correlations <- function(data) {
if (ncol(data) < 3) {
return(NULL)
}
# Remove rows with missing values
complete_data <- data[complete.cases(data), ]
if (nrow(complete_data) < 3) {
return(NULL)
}
# Calculate partial correlations
partial_cor <- corpcor::cor2pcor(cor(complete_data))
rownames(partial_cor) <- colnames(partial_cor) <- names(complete_data)
return(partial_cor)
}
#' Test correlation significance
#'
#' @param data Numeric data frame
#' @param method Correlation method
#' @return Significance test results
test_correlation_significance <- function(data, method = "pearson") {
n_vars <- ncol(data)
results <- data.frame(
variable1 = character(0),
variable2 = character(0),
correlation = numeric(0),
p_value = numeric(0),
significant = logical(0),
stringsAsFactors = FALSE
)
for (i in 1:(n_vars - 1)) {
for (j in (i + 1):n_vars) {
var1 <- names(data)[i]
var2 <- names(data)[j]
# Remove missing values
complete_cases <- complete.cases(data[[var1]], data[[var2]])
x <- data[[var1]][complete_cases]
y <- data[[var2]][complete_cases]
if (length(x) > 2) {
test_result <- cor.test(x, y, method = method)
results <- rbind(results, data.frame(
variable1 = var1,
variable2 = var2,
correlation = test_result$estimate,
p_value = test_result$p.value,
significant = test_result$p.value < 0.05,
stringsAsFactors = FALSE
))
}
}
}
return(results)
}
Visualization for Exploration¶
Distribution Visualizations¶
# R/03-visualization-exploration.R
#' Create comprehensive distribution plots
#'
#' @param data Data frame
#' @param variables Variables to plot
#' @return List of distribution plots
create_distribution_plots <- function(data, variables = NULL) {
if (is.null(variables)) {
numeric_cols <- sapply(data, is.numeric)
variables <- names(data)[numeric_cols]
}
plots <- list()
for (var in variables) {
if (var %in% names(data)) {
plots[[var]] <- create_single_distribution_plot(data, var)
}
}
return(plots)
}
#' Create single distribution plot
#'
#' @param data Data frame
#' @param variable Variable to plot
#' @return Distribution plot
create_single_distribution_plot <- function(data, variable) {
p <- ggplot2::ggplot(data, aes_string(x = variable)) +
ggplot2::geom_histogram(aes(y = ..density..), bins = 30, alpha = 0.7, fill = "steelblue") +
ggplot2::geom_density(alpha = 0.5, color = "red", size = 1) +
ggplot2::geom_vline(aes_string(xintercept = paste0("mean(", variable, ", na.rm = TRUE)")),
color = "green", linetype = "dashed", size = 1) +
ggplot2::geom_vline(aes_string(xintercept = paste0("median(", variable, ", na.rm = TRUE)")),
color = "orange", linetype = "dashed", size = 1) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = paste("Distribution of", variable),
x = variable,
y = "Density"
)
return(p)
}
#' Create box plots for multiple variables
#'
#' @param data Data frame
#' @param variables Variables to plot
#' @return Box plot
create_box_plots <- function(data, variables) {
# Reshape data for box plot
plot_data <- data[, variables, drop = FALSE]
plot_data$id <- 1:nrow(plot_data)
plot_data_long <- tidyr::pivot_longer(plot_data, cols = all_of(variables),
names_to = "variable", values_to = "value")
p <- ggplot2::ggplot(plot_data_long, aes(x = variable, y = value)) +
ggplot2::geom_boxplot(aes(fill = variable), alpha = 0.7) +
ggplot2::geom_jitter(alpha = 0.3, width = 0.2) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "Box Plots of Variables",
x = "Variable",
y = "Value"
) +
ggplot2::theme(legend.position = "none")
return(p)
}
Relationship Visualizations¶
# R/03-visualization-exploration.R (continued)
#' Create scatter plot matrix
#'
#' @param data Data frame
#' @param variables Variables to include
#' @return Scatter plot matrix
create_scatter_plot_matrix <- function(data, variables) {
numeric_data <- data[, variables, drop = FALSE]
# Create pairs plot
p <- GGally::ggpairs(numeric_data,
lower = list(continuous = GGally::wrap("points", alpha = 0.3)),
upper = list(continuous = GGally::wrap("cor", size = 3)),
diag = list(continuous = GGally::wrap("densityDiag", alpha = 0.7)))
return(p)
}
#' Create correlation heatmap
#'
#' @param cor_matrix Correlation matrix
#' @return Correlation heatmap
create_correlation_heatmap <- function(cor_matrix) {
# Convert to long format
cor_data <- reshape2::melt(cor_matrix)
p <- ggplot2::ggplot(cor_data, aes(x = Var1, y = Var2, fill = value)) +
ggplot2::geom_tile() +
ggplot2::scale_fill_gradient2(low = "blue", high = "red", mid = "white",
midpoint = 0, limit = c(-1, 1)) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "Correlation Heatmap",
x = "",
y = "",
fill = "Correlation"
) +
ggplot2::theme(axis.text.x = element_text(angle = 45, hjust = 1))
return(p)
}
#' Create time series plots
#'
#' @param data Data frame
#' @param time_var Time variable
#' @param value_vars Value variables
#' @return Time series plots
create_time_series_plots <- function(data, time_var, value_vars) {
plots <- list()
for (var in value_vars) {
p <- ggplot2::ggplot(data, aes_string(x = time_var, y = var)) +
ggplot2::geom_line() +
ggplot2::geom_point(alpha = 0.5) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = paste("Time Series of", var),
x = time_var,
y = var
)
plots[[var]] <- p
}
return(plots)
}
Advanced Exploration Techniques¶
Dimensionality Reduction¶
# R/04-advanced-exploration.R
#' Perform PCA analysis
#'
#' @param data Data frame
#' @param variables Variables to include
#' @return PCA results
perform_pca_analysis <- function(data, variables) {
numeric_data <- data[, variables, drop = FALSE]
# Remove rows with missing values
complete_data <- numeric_data[complete.cases(numeric_data), ]
if (nrow(complete_data) < 2) {
return(NULL)
}
# Perform PCA
pca_result <- prcomp(complete_data, scale = TRUE)
# Calculate variance explained
variance_explained <- pca_result$sdev^2 / sum(pca_result$sdev^2)
cumulative_variance <- cumsum(variance_explained)
# Create results
results <- list(
pca_result = pca_result,
variance_explained = variance_explained,
cumulative_variance = cumulative_variance,
loadings = pca_result$rotation,
scores = pca_result$x
)
return(results)
}
#' Create PCA visualization
#'
#' @param pca_results PCA results
#' @param data Original data
#' @return PCA plots
create_pca_visualization <- function(pca_results, data) {
plots <- list()
# Scree plot
scree_data <- data.frame(
component = 1:length(pca_results$variance_explained),
variance_explained = pca_results$variance_explained,
cumulative_variance = pca_results$cumulative_variance
)
plots$scree <- ggplot2::ggplot(scree_data, aes(x = component, y = variance_explained)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "PCA Scree Plot",
x = "Principal Component",
y = "Variance Explained"
)
# Biplot
biplot_data <- data.frame(
PC1 = pca_results$scores[, 1],
PC2 = pca_results$scores[, 2]
)
plots$biplot <- ggplot2::ggplot(biplot_data, aes(x = PC1, y = PC2)) +
ggplot2::geom_point(alpha = 0.5) +
ggplot2::theme_minimal() +
ggplot2::labs(
title = "PCA Biplot",
x = "PC1",
y = "PC2"
)
return(plots)
}
Clustering Analysis¶
# R/04-advanced-exploration.R (continued)
#' Perform clustering analysis
#'
#' @param data Data frame
#' @param variables Variables to include
#' @param method Clustering method
#' @return Clustering results
perform_clustering_analysis <- function(data, variables, method = "kmeans") {
numeric_data <- data[, variables, drop = FALSE]
# Remove rows with missing values
complete_data <- numeric_data[complete.cases(numeric_data), ]
if (nrow(complete_data) < 2) {
return(NULL)
}
# Scale data
scaled_data <- scale(complete_data)
if (method == "kmeans") {
# Determine optimal number of clusters
optimal_k <- determine_optimal_clusters(scaled_data)
# Perform k-means clustering
cluster_result <- kmeans(scaled_data, centers = optimal_k, nstart = 25)
results <- list(
method = "kmeans",
optimal_k = optimal_k,
clusters = cluster_result$cluster,
centers = cluster_result$centers,
withinss = cluster_result$withinss,
totss = cluster_result$totss,
betweenss = cluster_result$betweenss
)
} else if (method == "hierarchical") {
# Perform hierarchical clustering
dist_matrix <- dist(scaled_data)
hclust_result <- hclust(dist_matrix)
# Cut tree to get clusters
optimal_k <- determine_optimal_clusters(scaled_data)
clusters <- cutree(hclust_result, k = optimal_k)
results <- list(
method = "hierarchical",
optimal_k = optimal_k,
clusters = clusters,
hclust_result = hclust_result
)
}
return(results)
}
#' Determine optimal number of clusters
#'
#' @param data Scaled data
#' @return Optimal number of clusters
determine_optimal_clusters <- function(data) {
max_k <- min(10, nrow(data) - 1)
if (max_k < 2) {
return(2)
}
# Calculate within-cluster sum of squares for different k
wss <- numeric(max_k)
for (k in 1:max_k) {
wss[k] <- sum(kmeans(data, centers = k, nstart = 25)$withinss)
}
# Find elbow point
diffs <- diff(wss)
optimal_k <- which.max(diffs) + 1
return(optimal_k)
}
TL;DR Runbook¶
Quick Start¶
# 1. Data overview
overview <- data_overview(data)
print_data_overview(overview)
# 2. Data quality assessment
quality <- assess_data_quality(data)
print(quality)
# 3. Descriptive statistics
stats <- descriptive_statistics(data)
print(stats)
# 4. Correlation analysis
correlations <- correlation_analysis(data)
print(correlations$high_correlations)
# 5. Visualizations
dist_plots <- create_distribution_plots(data)
cor_heatmap <- create_correlation_heatmap(correlations$correlation_matrix)
Essential Patterns¶
# Systematic exploration
explore_data <- function(data) {
# Overview
overview <- data_overview(data)
# Quality
quality <- assess_data_quality(data)
# Statistics
stats <- descriptive_statistics(data)
# Correlations
correlations <- correlation_analysis(data)
# Visualizations
plots <- create_distribution_plots(data)
return(list(
overview = overview,
quality = quality,
statistics = stats,
correlations = correlations,
plots = plots
))
}
This guide provides the complete machinery for comprehensive data exploration in R. Each pattern includes implementation examples, visualization strategies, and real-world usage patterns for enterprise deployment.