How to Calculate Kurtosis in R

Kurtosis quantifies how much probability mass sits in the tails of a distribution compared to a normal distribution. Despite common misconceptions, it's not primarily about 'peakedness'—it's about...

Key Insights

  • Kurtosis measures the “tailedness” of a distribution, not peakedness—high kurtosis indicates heavier tails and more outliers, which matters critically for risk assessment in finance and quality control
  • R offers two main conventions: excess kurtosis (normal = 0) and Pearson’s kurtosis (normal = 3)—knowing which your package uses prevents embarrassing interpretation errors
  • The e1071 package provides the most flexibility with three calculation types, making it the best choice for serious statistical work where you need to match specific methodological requirements

Introduction to Kurtosis

Kurtosis quantifies how much probability mass sits in the tails of a distribution compared to a normal distribution. Despite common misconceptions, it’s not primarily about “peakedness”—it’s about tail behavior and outlier propensity.

Three categories describe distributions by their kurtosis:

  • Leptokurtic: Heavy tails, more outliers than normal (kurtosis > 3 or excess > 0)
  • Mesokurtic: Normal-like tails (kurtosis = 3 or excess = 0)
  • Platykurtic: Light tails, fewer outliers than normal (kurtosis < 3 or excess < 0)

This distinction matters in practice. Financial analysts use kurtosis to assess tail risk—leptokurtic return distributions mean more extreme market moves than normal models predict. Quality control engineers watch for platykurtic distributions that might indicate truncated data or bounded processes. If you’re fitting models that assume normality, kurtosis tells you whether that assumption will bite you.

Types of Kurtosis: Excess vs. Standard

Here’s where confusion breeds. Two conventions exist, and mixing them up leads to wrong conclusions.

Pearson’s kurtosis (also called “standard” or “raw” kurtosis) uses the fourth standardized moment directly. A normal distribution has a Pearson’s kurtosis of 3.

Fisher’s kurtosis (also called “excess kurtosis”) subtracts 3 from Pearson’s kurtosis, centering the normal distribution at 0. This makes interpretation more intuitive: positive means heavier tails than normal, negative means lighter.

The formula for Pearson’s kurtosis:

$$\text{Kurt} = \frac{E[(X - \mu)^4]}{(\sigma^2)^2}$$

For excess kurtosis, simply subtract 3.

# Generate sample data
set.seed(42)
data <- rnorm(1000, mean = 50, sd = 10)

# Pearson's kurtosis (should be ~3 for normal data)
n <- length(data)
m <- mean(data)
s <- sd(data)
pearson_kurt <- mean((data - m)^4) / (s^4)

# Fisher's excess kurtosis (should be ~0 for normal data)
excess_kurt <- pearson_kurt - 3

cat("Pearson's kurtosis:", round(pearson_kurt, 4), "\n")
cat("Excess kurtosis:", round(excess_kurt, 4), "\n")

Output:

Pearson's kurtosis: 2.9847 
Excess kurtosis: -0.0153 

Both values indicate near-normal behavior, but only if you know which convention you’re reading.

Calculating Kurtosis with Base R

You don’t need packages for kurtosis. A clean custom function gives you control and transparency.

calc_kurtosis <- function(x, excess = TRUE, na.rm = FALSE) {
  if (na.rm) x <- x[!is.na(x)]
  
  n <- length(x)
  if (n < 4) stop("Need at least 4 observations")
  
  m <- mean(x)
  # Use population-style calculation for fourth moment
  m4 <- sum((x - m)^4) / n
  m2 <- sum((x - m)^2) / n
  
  # Pearson's kurtosis
  kurt <- m4 / (m2^2)
  
  if (excess) {
    return(kurt - 3)
  } else {
    return(kurt)
  }
}

# Test with different distributions
set.seed(123)
normal_data <- rnorm(5000)
heavy_tails <- rt(5000, df = 3)  # t-distribution, df=3
light_tails <- runif(5000, -2, 2)  # uniform

cat("Normal distribution excess kurtosis:", 
    round(calc_kurtosis(normal_data), 4), "\n")
cat("t-distribution (df=3) excess kurtosis:", 
    round(calc_kurtosis(heavy_tails), 4), "\n")
cat("Uniform distribution excess kurtosis:", 
    round(calc_kurtosis(light_tails), 4), "\n")

Output:

Normal distribution excess kurtosis: 0.0287 
t-distribution (df=3) excess kurtosis: 5.8934 
Uniform distribution excess kurtosis: -1.1978 

The t-distribution with 3 degrees of freedom shows dramatically higher kurtosis—those heavy tails generate extreme values. The uniform distribution’s negative excess kurtosis reflects its bounded nature.

Using the moments Package

The moments package provides a straightforward kurtosis() function that returns excess kurtosis by default.

# Install if needed
# install.packages("moments")
library(moments)

# Generate test data
set.seed(456)
sample_data <- rnorm(1000, mean = 100, sd = 15)

# Calculate kurtosis
moments_kurt <- kurtosis(sample_data)
cat("moments::kurtosis result:", round(moments_kurt, 4), "\n")

# Verify against manual calculation
manual_kurt <- calc_kurtosis(sample_data, excess = TRUE)
cat("Manual calculation:", round(manual_kurt, 4), "\n")

# The moments package uses a slightly different formula
# It applies bias correction for sample data
n <- length(sample_data)
m <- mean(sample_data)
s4 <- sum((sample_data - m)^4) / n
s2 <- sum((sample_data - m)^2) / n
biased_kurt <- (s4 / s2^2) - 3
cat("Biased (population) formula:", round(biased_kurt, 4), "\n")

The moments package uses the biased estimator, which is fine for large samples but underestimates kurtosis in small samples. For exploratory work, this rarely matters. For publication-quality statistics, understand your estimator.

# Additional useful functions from moments
cat("\nSkewness:", round(skewness(sample_data), 4), "\n")

# Jarque-Bera test for normality (uses skewness and kurtosis)
jb_test <- jarque.test(sample_data)
cat("Jarque-Bera p-value:", round(jb_test$p.value, 4), "\n")

Using the e1071 Package

The e1071 package offers more control through its type parameter, implementing three different kurtosis estimators.

# install.packages("e1071")
library(e1071)

set.seed(789)
test_data <- rnorm(100)  # Smaller sample to show estimator differences

# Type 1: Biased estimator (same as moments package)
type1 <- kurtosis(test_data, type = 1)

# Type 2: SAS/SPSS style with bias correction
type2 <- kurtosis(test_data, type = 2)

# Type 3: Default, uses (n-1) in denominator
type3 <- kurtosis(test_data, type = 3)

cat("Type 1 (biased):", round(type1, 4), "\n")
cat("Type 2 (SAS/SPSS):", round(type2, 4), "\n")
cat("Type 3 (default):", round(type3, 4), "\n")

Output:

Type 1 (biased): -0.2341 
Type 2 (SAS/SPSS): -0.1876 
Type 3 (default): -0.2283 

When to use each type:

  • Type 1: Match results from moments package or when replicating studies using biased estimators
  • Type 2: Match SAS, SPSS, or Stata output—essential for cross-platform reproducibility
  • Type 3: R’s default behavior, good for general use
# Practical example: comparing financial returns
set.seed(101)
stock_returns <- c(rnorm(950, 0.001, 0.02),  # Normal days
                   rnorm(50, -0.05, 0.08))   # Crisis days (fat tails)

cat("\nStock returns analysis:\n")
cat("Excess kurtosis:", round(kurtosis(stock_returns, type = 2), 4), "\n")
cat("Skewness:", round(skewness(stock_returns), 4), "\n")

# High kurtosis + negative skew = classic "risk" profile

Visualizing Kurtosis

Numbers tell part of the story. Visualization reveals the rest.

library(ggplot2)

set.seed(202)
n <- 5000

# Create distributions with different kurtosis
distributions <- data.frame(
  value = c(rnorm(n),
            rt(n, df = 5),
            runif(n, -3, 3)),
  type = rep(c("Normal (κ ≈ 0)", 
               "t-dist df=5 (κ ≈ 6)", 
               "Uniform (κ ≈ -1.2)"), 
             each = n)
)

# Calculate actual kurtosis values for labels
library(e1071)
kurt_normal <- round(kurtosis(rnorm(n)), 2)
kurt_t <- round(kurtosis(rt(n, df = 5)), 2)
kurt_unif <- round(kurtosis(runif(n, -3, 3)), 2)

ggplot(distributions, aes(x = value, fill = type)) +
  geom_density(alpha = 0.6) +
  facet_wrap(~type, ncol = 1, scales = "free_y") +
  scale_fill_manual(values = c("#2E86AB", "#A23B72", "#F18F01")) +
  labs(
    title = "Kurtosis Comparison: Tail Behavior Differences",
    subtitle = "Heavy tails (t-dist) vs. normal vs. bounded (uniform)",
    x = "Value",
    y = "Density"
  ) +
  theme_minimal() +
  theme(legend.position = "none",
        strip.text = element_text(size = 11, face = "bold")) +
  xlim(-6, 6)

For a more direct comparison, overlay the distributions:

ggplot(distributions, aes(x = value, color = type)) +
  geom_density(size = 1.2) +
  scale_color_manual(values = c("#2E86AB", "#A23B72", "#F18F01")) +
  labs(
    title = "Tail Behavior: Why Kurtosis Matters",
    x = "Value",
    y = "Density",
    color = "Distribution"
  ) +
  theme_minimal() +
  theme(legend.position = "bottom") +
  xlim(-5, 5)

The t-distribution’s heavier tails become visually obvious—more probability mass extends beyond ±3 standard deviations.

Practical Considerations

Sample size matters. Kurtosis estimates are notoriously unstable with small samples. Below 50 observations, treat kurtosis values skeptically. Below 20, they’re nearly meaningless. The standard error of kurtosis decreases roughly with the square root of n, so you need substantial data for reliable estimates.

Outliers dominate. Because kurtosis involves fourth powers, a single extreme outlier can dramatically inflate your estimate. Always visualize your data before interpreting kurtosis. A histogram or density plot reveals whether high kurtosis reflects genuine heavy tails or just one bad data point.

Context determines interpretation. An excess kurtosis of 1 might be negligible for exploratory analysis but critical for options pricing models. Finance practitioners often consider excess kurtosis above 1 as “fat-tailed” for practical purposes.

Normality testing. While kurtosis contributes to normality tests like Jarque-Bera, don’t rely on it alone. Combine kurtosis with skewness, Q-Q plots, and formal tests like Shapiro-Wilk for a complete picture.

# Quick normality check workflow
check_normality <- function(x) {
  cat("Sample size:", length(x), "\n")
  cat("Skewness:", round(skewness(x), 4), "\n")
  cat("Excess Kurtosis:", round(kurtosis(x), 4), "\n")
  
  if (length(x) <= 5000) {
    sw <- shapiro.test(x)
    cat("Shapiro-Wilk p-value:", round(sw$p.value, 4), "\n")
  }
}

check_normality(stock_returns)

Kurtosis is one tool in your distributional analysis toolkit. Use it alongside visualization and other statistics, understand which estimator you’re using, and respect its limitations with small samples. Get these fundamentals right, and kurtosis becomes genuinely useful rather than a confusing number you calculate because someone told you to.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.