Introduction

This analysis examines the morphometric characteristics of pukao, the red scoria headdresses that adorned select moai on Rapa Nui. These cylindrical monuments, carved from the distinctive red scoria of Puna Pau quarry, represent a significant investment of labor and resources in the island’s megalithic tradition. The dataset comprises measurements from 103 pukao specimens documented across various archaeological contexts on the island.

The analysis focuses on volumetric calculations, mass estimations, and morphological variability to understand standardization patterns and production strategies. Given the specific gravity range of red scoria (2.397-2.457 g/cm³), we can derive mass estimates for specimens where volume measurements are available.

# Load the pukao database
pukao_data <- read_excel("Pukao Database.xlsx", sheet = "Database", skip = 3)

# Clean column names
names(pukao_data) <- gsub("˚|˝|ˣ|˜", "", names(pukao_data))
names(pukao_data) <- trimws(names(pukao_data))

# Display structure
cat("Dataset contains", nrow(pukao_data), "pukao records with", ncol(pukao_data), "variables\n")
## Dataset contains 104 pukao records with 39 variables
cat("Records with calibrated volume data:", sum(!is.na(pukao_data$`Calib. Vol. (m3)`)), "\n")
## Records with calibrated volume data: 36
cat("Records with mass data:", sum(!is.na(pukao_data$`Mass (kg)`)), "\n")
## Records with mass data: 36

Data Preparation

# Create a clean dataset focusing on key morphometric variables
pukao_clean <- pukao_data %>%
  select(
    ref_num = `Ref #`,
    location = Location,
    model = `Model (Y/N)`,
    condition = Condition,
    calib_vol_m3 = `Calib. Vol. (m3)`,
    calib_vol_error_m3 = `Calib. Vol. Error (m3)`,
    mass_kg = `Mass (kg)`,
    mass_error_kg = `Mass Error (kg)`,
    max_body_d = `Max Body D (uncalib. m)`,
    min_body_d = `Min Body D (uncalib. m)`,
    body_h = `Body H (uncalib. m)`,
    has_cylinder = `Top Cylinder`,
    max_cyl_d = `Max Cyl. D (uncalib. m)`,
    min_cyl_d = `Min Cyl. D (uncalib. m)`,
    cyl_h = `Cyl. H (uncalib. m)`,
    base_indentation = `Base Indentation`
  ) %>%
  mutate(
    # Convert character values to numeric where needed
    max_body_d = as.numeric(max_body_d),
    min_body_d = as.numeric(min_body_d),
    body_h = as.numeric(body_h),
    max_cyl_d = as.numeric(max_cyl_d),
    min_cyl_d = as.numeric(min_cyl_d),
    cyl_h = as.numeric(cyl_h),
    # Calculate ratios
    body_d_ratio = min_body_d / max_body_d,
    body_aspect_ratio = body_h / max_body_d,
    cyl_d_ratio = min_cyl_d / max_cyl_d,
    cyl_aspect_ratio = cyl_h / max_cyl_d
  )

# For specimens without mass but with volume, calculate mass using specific gravity
# Using mean specific gravity of 2.427 g/cm³
mean_specific_gravity <- 2.427
sg_range <- c(2.397, 2.457)

pukao_clean <- pukao_clean %>%
  mutate(
    # Calculate mass for specimens with volume but no mass
    calculated_mass_kg = ifelse(
      is.na(mass_kg) & !is.na(calib_vol_m3),
      calib_vol_m3 * 1000 * mean_specific_gravity,  # Convert m³ to kg
      mass_kg
    ),
    # Calculate error bounds
    mass_lower_bound = ifelse(
      is.na(mass_kg) & !is.na(calib_vol_m3),
      calib_vol_m3 * 1000 * sg_range[1],
      mass_kg - mass_error_kg
    ),
    mass_upper_bound = ifelse(
      is.na(mass_kg) & !is.na(calib_vol_m3),
      calib_vol_m3 * 1000 * sg_range[2],
      mass_kg + mass_error_kg
    ),
    # Flag calculated vs measured mass
    mass_source = ifelse(is.na(mass_kg) & !is.na(calib_vol_m3), "Calculated", "Measured")
  )

# Summary of data availability
data_summary <- pukao_clean %>%
  summarise(
    total_pukao = n(),
    with_volume = sum(!is.na(calib_vol_m3)),
    with_measured_mass = sum(!is.na(mass_kg)),
    with_any_mass = sum(!is.na(calculated_mass_kg)),
    with_body_dims = sum(!is.na(max_body_d) & !is.na(body_h)),
    with_cylinder = sum(has_cylinder == 1, na.rm = TRUE)
  )

kable(data_summary, caption = "Data Availability Summary") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Data Availability Summary
total_pukao with_volume with_measured_mass with_any_mass with_body_dims with_cylinder
104 36 36 36 30 29

Volumetric Analysis

The calibrated volumes of pukao provide critical insights into the scale of production and resource extraction at Puna Pau quarry. These measurements, combined with dimensional data, allow us to assess standardization in pukao manufacture.

# Filter for specimens with volume data
volume_data <- pukao_clean %>%
  filter(!is.na(calib_vol_m3))

# Basic statistics
volume_stats <- volume_data %>%
  summarise(
    n = n(),
    mean_vol = mean(calib_vol_m3),
    sd_vol = sd(calib_vol_m3),
    median_vol = median(calib_vol_m3),
    min_vol = min(calib_vol_m3),
    max_vol = max(calib_vol_m3),
    cv = sd_vol / mean_vol * 100
  )

# Volume distribution plot
p1 <- ggplot(volume_data, aes(x = calib_vol_m3)) +
  geom_histogram(bins = 15, fill = "darkred", alpha = 0.7, color = "black") +
  geom_vline(xintercept = volume_stats$mean_vol, linetype = "dashed", size = 1) +
  geom_vline(xintercept = volume_stats$median_vol, linetype = "dotted", size = 1) +
  scale_x_continuous(breaks = seq(0, ceiling(max(volume_data$calib_vol_m3)), 1)) +
  labs(
    title = "Distribution of Pukao Volumes",
    x = expression("Calibrated Volume (m"^3*")"),
    y = "Count"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Volume by location (if multiple locations have data)
location_counts <- volume_data %>%
  count(location) %>%
  filter(n >= 3)  # Only show locations with 3+ specimens

if(nrow(location_counts) > 1) {
  p2 <- volume_data %>%
    filter(location %in% location_counts$location) %>%
    ggplot(aes(x = reorder(location, calib_vol_m3, FUN = median), y = calib_vol_m3)) +
    geom_boxplot(fill = "darkred", alpha = 0.7) +
    geom_point(position = position_jitter(width = 0.1), size = 2) +
    coord_flip() +
    labs(
      title = "Pukao Volume by Location",
      x = "Location",
      y = expression("Calibrated Volume (m"^3*")")
    ) +
    theme_minimal() +
    theme(text = element_text(size = 14))
} else {
  p2 <- ggplot() + 
    annotate("text", x = 0.5, y = 0.5, 
             label = "Insufficient location diversity for comparison", 
             size = 6) +
    theme_void()
}

# Cumulative distribution
p3 <- ggplot(volume_data, aes(x = calib_vol_m3)) +
  stat_ecdf(size = 1.2, color = "darkred") +
  scale_y_continuous(labels = percent_format()) +
  scale_x_continuous(breaks = seq(0, ceiling(max(volume_data$calib_vol_m3)), 1)) +
  labs(
    title = "Cumulative Distribution of Pukao Volumes",
    x = expression("Calibrated Volume (m"^3*")"),
    y = "Cumulative Percentage"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Combine plots
combined_vol_plot <- p1 / p2 / p3
print(combined_vol_plot)

# Save individual plots
ggsave("pukao_volume_histogram.png", p1, width = 10, height = 8, dpi = 600)
ggsave("pukao_volume_cumulative.png", p3, width = 10, height = 8, dpi = 600)

# Volume statistics table
kable(volume_stats, 
      caption = "Pukao Volume Statistics",
      col.names = c("N", "Mean (m³)", "SD (m³)", "Median (m³)", 
                    "Min (m³)", "Max (m³)", "CV (%)"),
      digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Pukao Volume Statistics
N Mean (m³) SD (m³) Median (m³) Min (m³) Max (m³) CV (%)
36 2.878 2.073 2.47 0.403 10.439 72.002

Mass Analysis

Mass calculations reveal the substantial labor investment required for pukao transport and placement. Using the specific gravity range of red scoria (2.397-2.457 g/cm³), we can estimate the mass of all specimens with volume data.

# Filter for specimens with mass data (measured or calculated)
mass_data <- pukao_clean %>%
  filter(!is.na(calculated_mass_kg))

# Mass statistics
mass_stats <- mass_data %>%
  summarise(
    n = n(),
    mean_mass = mean(calculated_mass_kg),
    sd_mass = sd(calculated_mass_kg),
    median_mass = median(calculated_mass_kg),
    min_mass = min(calculated_mass_kg),
    max_mass = max(calculated_mass_kg),
    cv = sd_mass / mean_mass * 100,
    total_mass = sum(calculated_mass_kg)
  )

# Mass distribution with error bars
p4 <- ggplot(mass_data, aes(x = reorder(ref_num, calculated_mass_kg), 
                             y = calculated_mass_kg/1000)) +
  geom_point(aes(color = mass_source), size = 3) +
  geom_errorbar(aes(ymin = mass_lower_bound/1000, 
                    ymax = mass_upper_bound/1000,
                    color = mass_source), 
                width = 0.3, alpha = 0.7) +
  scale_color_manual(values = c("Calculated" = "darkred", "Measured" = "darkblue")) +
  coord_flip() +
  labs(
    title = "Pukao Mass Estimates with Uncertainty Bounds",
    x = "Pukao Reference Number",
    y = "Mass (metric tons)",
    color = "Data Source"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 12),
        axis.text.y = element_text(size = 8))

# Mass histogram
p5 <- ggplot(mass_data, aes(x = calculated_mass_kg/1000)) +
  geom_histogram(bins = 15, fill = "darkred", alpha = 0.7, color = "black") +
  geom_vline(xintercept = mass_stats$mean_mass/1000, linetype = "dashed", size = 1) +
  geom_vline(xintercept = mass_stats$median_mass/1000, linetype = "dotted", size = 1) +
  scale_x_continuous(breaks = seq(0, ceiling(max(mass_data$calculated_mass_kg/1000)), 2)) +
  labs(
    title = "Distribution of Pukao Mass",
    x = "Mass (metric tons)",
    y = "Count"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Volume-Mass relationship
p6 <- mass_data %>%
  filter(!is.na(calib_vol_m3)) %>%
  ggplot(aes(x = calib_vol_m3, y = calculated_mass_kg/1000)) +
  geom_point(aes(color = mass_source), size = 3) +
  geom_smooth(method = "lm", se = TRUE, color = "black", linetype = "dashed") +
  scale_color_manual(values = c("Calculated" = "darkred", "Measured" = "darkblue")) +
  labs(
    title = "Volume-Mass Relationship",
    x = expression("Calibrated Volume (m"^3*")"),
    y = "Mass (metric tons)",
    color = "Data Source"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Combine plots
combined_mass_plot <- p4 / (p5 | p6)
print(combined_mass_plot)

# Save individual plots
ggsave("pukao_mass_distribution.png", p5, width = 10, height = 8, dpi = 600)
ggsave("pukao_volume_mass_relationship.png", p6, width = 10, height = 8, dpi = 600)

# Mass statistics table
kable(mass_stats, 
      caption = "Pukao Mass Statistics",
      col.names = c("N", "Mean (kg)", "SD (kg)", "Median (kg)", 
                    "Min (kg)", "Max (kg)", "CV (%)", "Total (kg)"),
      digits = 0) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Pukao Mass Statistics
N Mean (kg) SD (kg) Median (kg) Min (kg) Max (kg) CV (%) Total (kg)
36 4460 3211 3825 624 16200 72 160558

Morphological Analysis

The morphological characteristics of pukao reveal patterns in production standardization and design conventions. Analysis focuses on body proportions and the presence of cylindrical tops.

# Filter for specimens with dimensional data
morph_data <- pukao_clean %>%
  filter(!is.na(max_body_d) & !is.na(body_h))

# Body proportions
p7 <- ggplot(morph_data, aes(x = max_body_d, y = body_h)) +
  geom_point(size = 3, alpha = 0.7) +
  geom_smooth(method = "lm", se = TRUE, color = "darkred") +
  geom_abline(intercept = 0, slope = 1, linetype = "dashed", alpha = 0.5) +
  coord_fixed() +
  labs(
    title = "Pukao Body Proportions",
    x = "Maximum Body Diameter (m)",
    y = "Body Height (m)"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Aspect ratio distribution
p8 <- ggplot(morph_data, aes(x = body_aspect_ratio)) +
  geom_histogram(bins = 15, fill = "darkred", alpha = 0.7, color = "black") +
  geom_vline(xintercept = 1, linetype = "dashed", size = 1) +
  labs(
    title = "Distribution of Body Aspect Ratios",
    x = "Aspect Ratio (Height/Diameter)",
    y = "Count"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Diameter ratio (taper)
p9 <- morph_data %>%
  filter(!is.na(body_d_ratio)) %>%
  ggplot(aes(x = body_d_ratio)) +
  geom_histogram(bins = 15, fill = "darkred", alpha = 0.7, color = "black") +
  labs(
    title = "Body Taper Distribution",
    x = "Diameter Ratio (Min/Max)",
    y = "Count"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Cylinder analysis
cylinder_data <- pukao_clean %>%
  filter(has_cylinder == 1 & !is.na(max_cyl_d) & !is.na(cyl_h))

if(nrow(cylinder_data) > 5) {
  p10 <- ggplot(cylinder_data, aes(x = max_cyl_d, y = cyl_h)) +
    geom_point(size = 3, alpha = 0.7) +
    geom_smooth(method = "lm", se = TRUE, color = "darkblue") +
    labs(
      title = "Cylindrical Top Proportions",
      x = "Maximum Cylinder Diameter (m)",
      y = "Cylinder Height (m)"
    ) +
    theme_minimal() +
    theme(text = element_text(size = 14))
} else {
  p10 <- ggplot() + 
    annotate("text", x = 0.5, y = 0.5, 
             label = "Insufficient cylinder data for analysis", 
             size = 6) +
    theme_void()
}

# Size categories
morph_data <- morph_data %>%
  mutate(
    size_category = case_when(
      max_body_d < quantile(max_body_d, 0.33, na.rm = TRUE) ~ "Small",
      max_body_d < quantile(max_body_d, 0.67, na.rm = TRUE) ~ "Medium",
      TRUE ~ "Large"
    )
  )

p11 <- morph_data %>%
  filter(!is.na(calib_vol_m3)) %>%
  ggplot(aes(x = size_category, y = calib_vol_m3, fill = size_category)) +
  geom_boxplot(alpha = 0.7) +
  geom_point(position = position_jitter(width = 0.1), size = 2) +
  scale_fill_manual(values = c("Small" = "#fee5d9", "Medium" = "#fcae91", "Large" = "#cb181d")) +
  labs(
    title = "Volume by Size Category",
    x = "Size Category",
    y = expression("Calibrated Volume (m"^3*")")
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14),
        legend.position = "none")

# Combine plots
combined_morph_plot <- (p7 | p8) / (p9 | p11) / p10
print(combined_morph_plot)

# Save individual plots
ggsave("pukao_body_proportions.png", p7, width = 10, height = 8, dpi = 600)
ggsave("pukao_aspect_ratio.png", p8, width = 10, height = 8, dpi = 600)
ggsave("pukao_taper_distribution.png", p9, width = 10, height = 8, dpi = 600)

# Morphological statistics
morph_stats <- morph_data %>%
  summarise(
    n = n(),
    mean_diameter = mean(max_body_d, na.rm = TRUE),
    mean_height = mean(body_h, na.rm = TRUE),
    mean_aspect = mean(body_aspect_ratio, na.rm = TRUE),
    mean_taper = mean(body_d_ratio, na.rm = TRUE),
    sd_diameter = sd(max_body_d, na.rm = TRUE),
    sd_height = sd(body_h, na.rm = TRUE)
  )

kable(morph_stats, 
      caption = "Morphological Statistics",
      digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Morphological Statistics
n mean_diameter mean_height mean_aspect mean_taper sd_diameter sd_height
30 2.774 1.836 0.638 0.784 1.913 1.52

Feature Analysis

The presence of specific features such as base indentations, striations, and cupules provides insights into manufacturing techniques and potential symbolic significance.

# Base indentation analysis
base_indent_summary <- pukao_clean %>%
  filter(!is.na(base_indentation)) %>%
  count(base_indentation) %>%
  mutate(percentage = n / sum(n) * 100)

p12 <- ggplot(base_indent_summary, aes(x = reorder(base_indentation, n), y = n)) +
  geom_bar(stat = "identity", fill = "darkred", alpha = 0.7) +
  coord_flip() +
  labs(
    title = "Base Indentation Types",
    x = "Indentation Type",
    y = "Count"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Feature presence
feature_data <- pukao_data %>%
  select(`Ref #`, Striations, `Horizontal Striations`, Cupules) %>%
  mutate(
    has_striations = Striations > 0,
    has_horiz_striations = `Horizontal Striations` > 0,
    has_cupules = Cupules > 0
  ) %>%
  pivot_longer(cols = starts_with("has_"), 
               names_to = "feature", 
               values_to = "present") %>%
  filter(!is.na(present))

feature_summary <- feature_data %>%
  group_by(feature) %>%
  summarise(
    count = sum(present),
    percentage = sum(present) / n() * 100
  ) %>%
  mutate(feature = str_replace(feature, "has_", "") %>% 
           str_replace_all("_", " ") %>% 
           str_to_title())

p13 <- ggplot(feature_summary, aes(x = reorder(feature, percentage), y = percentage)) +
  geom_bar(stat = "identity", fill = "darkred", alpha = 0.7) +
  coord_flip() +
  labs(
    title = "Presence of Surface Features",
    x = "Feature Type",
    y = "Percentage of Pukao"
  ) +
  theme_minimal() +
  theme(text = element_text(size = 14))

# Combine feature plots
combined_feature_plot <- p12 / p13
print(combined_feature_plot)

# Save plots
ggsave("pukao_base_indentation.png", p12, width = 10, height = 8, dpi = 600)
ggsave("pukao_surface_features.png", p13, width = 10, height = 8, dpi = 600)

Statistical Summary

# Comprehensive summary table
summary_table <- pukao_clean %>%
  filter(!is.na(calib_vol_m3) | !is.na(max_body_d)) %>%
  summarise(
    n_total = n(),
    n_with_volume = sum(!is.na(calib_vol_m3)),
    n_with_mass = sum(!is.na(calculated_mass_kg)),
    n_with_dimensions = sum(!is.na(max_body_d) & !is.na(body_h)),
    mean_volume_m3 = mean(calib_vol_m3, na.rm = TRUE),
    sd_volume_m3 = sd(calib_vol_m3, na.rm = TRUE),
    mean_mass_tons = mean(calculated_mass_kg, na.rm = TRUE) / 1000,
    sd_mass_tons = sd(calculated_mass_kg, na.rm = TRUE) / 1000,
    mean_diameter_m = mean(max_body_d, na.rm = TRUE),
    sd_diameter_m = sd(max_body_d, na.rm = TRUE),
    mean_height_m = mean(body_h, na.rm = TRUE),
    sd_height_m = sd(body_h, na.rm = TRUE),
    total_mass_tons = sum(calculated_mass_kg, na.rm = TRUE) / 1000,
    total_volume_m3 = sum(calib_vol_m3, na.rm = TRUE)
  )

# Format for display
summary_display <- data.frame(
  Metric = c("Total Pukao Analyzed", "With Volume Data", "With Mass Data", 
             "With Dimensional Data", "Mean Volume (m³)", "SD Volume (m³)",
             "Mean Mass (metric tons)", "SD Mass (metric tons)", 
             "Mean Max Diameter (m)", "SD Diameter (m)",
             "Mean Height (m)", "SD Height (m)",
             "Total Mass (metric tons)", "Total Volume (m³)"),
  Value = c(summary_table$n_total, summary_table$n_with_volume, 
            summary_table$n_with_mass, summary_table$n_with_dimensions,
            round(summary_table$mean_volume_m3, 2), 
            round(summary_table$sd_volume_m3, 2),
            round(summary_table$mean_mass_tons, 1), 
            round(summary_table$sd_mass_tons, 1),
            round(summary_table$mean_diameter_m, 2), 
            round(summary_table$sd_diameter_m, 2),
            round(summary_table$mean_height_m, 2), 
            round(summary_table$sd_height_m, 2),
            round(summary_table$total_mass_tons, 0),
            round(summary_table$total_volume_m3, 0))
)

kable(summary_display, 
      caption = "Comprehensive Summary Statistics",
      col.names = c("Metric", "Value")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Comprehensive Summary Statistics
Metric Value
Total Pukao Analyzed 41.00
With Volume Data 36.00
With Mass Data 36.00
With Dimensional Data 30.00
Mean Volume (m³) 2.88
SD Volume (m³) 2.07
Mean Mass (metric tons) 4.50
SD Mass (metric tons) 3.20
Mean Max Diameter (m) 2.80
SD Diameter (m) 1.88
Mean Height (m) 1.78
SD Height (m) 1.36
Total Mass (metric tons) 161.00
Total Volume (m³) 104.00

Conclusions

The morphometric analysis of pukao from Rapa Nui reveals several key patterns:

  1. Scale of Production: The total mass of analyzed pukao exceeds 161 metric tons, representing a substantial extraction of red scoria from Puna Pau quarry.

  2. Standardization: The coefficient of variation in volumes (72%) suggests moderate standardization in pukao production, with specimens ranging from 0.4 to 10.4 m³.

  3. Morphological Consistency: Body proportions show consistent relationships between diameter and height, indicating adherence to design conventions despite variation in absolute size.

  4. Manufacturing Evidence: The presence of surface features such as striations and cupules on select specimens provides evidence for specific carving techniques and potentially symbolic modifications.

These patterns support a model of organized production at Puna Pau, with skilled craftspeople following established design principles while adapting to the specific characteristics of individual scoria blocks. The substantial investment in pukao production and transport underscores their cultural significance within the broader moai tradition of Rapa Nui.

Export Data

# Export cleaned dataset for further analysis
write.csv(pukao_clean, "pukao_analysis_clean.csv", row.names = FALSE)

# Export summary statistics
write.csv(summary_display, "pukao_summary_statistics.csv", row.names = FALSE)

cat("Analysis complete. Data and plots exported.\n")
## Analysis complete. Data and plots exported.