Partitioning variation of call structure at social scales in parrots

Statistical analysis

Author
Published

August 1, 2025

Code
# print link to github repo if any
if (file.exists("./.git/config")) {
    config <- readLines("./.git/config")
    url <- grep("url", config, value = TRUE)
    url <- gsub("\\turl = |.git$", "", url)
    cat("\nSource code and data found at [", url, "](", url, ")", sep = "")
}

Source code and data found at https://github.com/maRce10/thick_billed_parrots_vocal_variation

Code
# options to customize chunk outputs
knitr::opts_chunk$set(
    tidy.opts = list(width.cutoff = 65),
    tidy = TRUE,
    message = FALSE
)

options(
    DT.options = list(
        pageLength = 200,
        scrollX = TRUE,
        scrollY = "800px",
        dom = 'Bfrtip',
        buttons = c('copy', 'csv', 'excel')
    )
)

datatable2 <- function(x, ...) datatable(data = x, extensions = 'Buttons', ...)
Code
# ggplot font size
bs <- 12

# custom functions
part_var <- function(model) {
    var_components_pc1 <- as.data.frame(VarCorr(model))
    total_variance <- sum(var_components_pc1$vcov)
    var_components_pc1$proportion <- var_components_pc1$vcov / total_variance
    
    return(var_components_pc1)
}

# plot variance partitioning results
graph_part_var <- function(levels,
                           data,
                           by = NULL,
                           response,
                           full = TRUE,
                           tick.labels = levels) {
    if (!is.null(by)) {
        if (full) {
            by_levels <- c(unique(data[, by]), "full")
        } else {
            by_levels <- unique(data[, by])
        }
    } else
        by_levels <- "full"
    
    # scale response
    if (is.numeric(data[, response])) {
        data[, response] <- scale(data[, response])
    } else {
        stop("Response variable must be numeric")
    }
    
    var_by_list <- lapply(by_levels, function(x) {
        if (x != "full") {
            formula <- paste0(response, " ~ (1 | ", paste(levels, collapse = " / "), ")")
            
            cat(paste0(x, ":", formula), "\n")
            
            dat <- droplevels(data[data[, by] == x, ])
        } else {
            dat <- droplevels(data)
            
            if (!is.null(by)) {
                formula <- paste0(response,
                                  " ~ (1 | ",
                                  paste(levels, collapse = " / "),
                                  ") + (1 | ",
                                  by,
                                  ")")
            } else {
                formula <- paste0(response,
                                  " ~ (1 | ",
                                  paste(levels, collapse = " / "),
                                  ")")
            }
            
            cat("full:", formula, "\n")
            
        }
        
        model <- lmer(
            formula,
            data = dat,
            control = lmerControl(
                optimizer = "bobyqa",
                optCtrl = list(maxfun = 2e5)
            )
        )
        
        var_components <- part_var(model)
        
        print(var_components)
        
        var_components <- var_components[var_components$grp != "Residual", ]
        
        if (!is.null(by)) {
            var_components <- var_components[var_components$grp != by, ]
        }
        var_components$level <- factor(rev(levels), levels = rev(levels))
        
        # sample sizes
        var_components$n <- sapply(var_components$level, function(y)
            length(unique(dat[, names(dat) == y])))
        
        var_components$prop <- as.character(round(var_components$proportion, 2))
        
        var_components$prop[var_components$prop == "0"] <- "< 0.01"
        
        var_components$prop <- paste(var_components$prop,
                                     " (n = ",
                                     var_components$n,
                                     ")",
                                     sep = "")
        
        var_components$by_levels <- x
        
        var_components$perc <- paste(
            round(var_components$proportion * 100, 0),
            "% (n = ",
            var_components$n,
            ")",
            sep = ""
        )
        
        return(var_components)
        
    })
    
    var_by_df <- do.call(rbind, var_by_list)
    
    var_by_df$by_levels <- factor(var_by_df$by_levels, levels = by_levels)
    
    # set X tick labels
    xlabs <- tick.labels
    names(xlabs) <- levels
    
    gg_out <- ggplot(var_by_df, aes(x = level, y = proportion, fill = level)) +
        geom_bar(stat = "identity") +
        theme_classic(base_size = 20) +
        labs(x = "Level", y = "Explained variance (%)", fill = "Factor") +
        facet_wrap(~ by_levels, ncol = 1) +
        theme(axis.text.x = element_text(
            angle = 45,
            vjust = 1,
            hjust = 1
        )) +
        geom_text(aes(label = perc), vjust = -0.5, size = 5) +
        scale_fill_viridis_d(
            option = "G",
            begin = 0.1,
            end = 0.9,
            guide = "none",
            direction = -1
            
        ) +
        scale_y_continuous(labels = label_percent(), limits = c(0, max(var_by_df$proportion) + 0.16)) +
        scale_x_discrete(labels = xlabs)
    
    
    
    # Data for the nested ovals
    oval_data <- data.frame(
        # label = c("A", "B", "C", "D"),
        x0 = c(0, 0, 0, 0, 0),
        # Center x position for all ovals
        y0 = c(-1.7, -0.4, 0.7, 2.2, 3.3),
        # Center y position for all ovals
        b = c(1.5, 3, 4.5, 6, 7.5),
        # Semi-major axis (horizontal radius)
        a = c(2, 4, 6, 8, 10) + 1,
        # Semi-minor axis (vertical radius)
        text.pos = c(-4.7, -2, 1, 3.2, 6) + 3  # Radius for each circle
        
    )
    
    oval_data <- oval_data[seq_along(levels), ]
    oval_data$label <- rev(xlabs)
    
    oval_data$size <- oval_data$a * oval_data$b
    
    # oval_data <- oval_data[oval_data$label %in% c("Site", "Bird"), ]  # Order by y0 for correct layering
    
    # oval_data$label <- factor(oval_data$label, levels = rev(oval_data$label))
    oval_data$label <- factor(oval_data$label, levels = oval_data$label[order(oval_data$size, decreasing = FALSE)])
    oval_data$label.color <-  viridis::mako(
        nrow(oval_data),
        begin = 0.1,
        end = 0.9,
        direction = -1,
        alpha = 1
    )
    oval_data$label.color <- factor(oval_data$label.color, levels = oval_data$label.color[order(oval_data$size, decreasing = TRUE)])
    
    # oval_data <- oval_data[1:4,]
    
    # Plot using ggplot2 and ggforce
    gg_oval <- ggplot(oval_data, group = label) +
        geom_ellipse(aes(
            x0 = x0,
            y0 = y0,
            a = a,
            b = b,
            angle = 0,
            fill = label.color
        ),
        color = "white") +
        geom_text(
            aes(x = x0, y = text.pos, label = label),
            size = 7,
            color = c(rep("black", nrow(oval_data) - 1), "white")
        ) +
        # scale_fill_viridis_d(option = "G", begin = 0.1, end = 0.9, guide = "none", direction = 1) + # Add labels in the center
        scale_fill_identity() +  # Use the colors as specified in the data
        theme_void() +           # Remove background, axes, and grid
        theme(legend.position = "none") +
        coord_fixed()  #+          # Keep aspect ratio fixed for true ovals
    #ylim(c(-12, 6))
    print(gg_oval)
    
    return(gg_out)
}

Purpose

  • Quantify contribution of different social levels of organization in the geographic variation of parrots calls.

Analysis flowchart

flowchart LR
  A(Read data) --> B(Plot acoustic space) 
  B --> C(Regression models<br>to partition variance) 
  C --> D(Plot variance partition)

style A fill:#44015466
style B fill:#3E4A894D
style C fill:#26828E4D
style D fill:#6DCD594D

Load packages

Code
# install/ load packages
sketchy::load_packages(packages = c("knitr", "formatR", "warbleR",
    "ggplot2", "maRce10/PhenotypeSpace", "lme4", "scales", "Rtsne",
    "umap", "ggforce", "DT", "viridis", "readxl"))

1 Thick-billed parrots

1.1 MFCCs

Code
tbp_calls <- readRDS("./data/processed/extended_selection_table_tpb_calls.rds")
tbp_mfcc <- readRDS("./data/processed/mfcc_tpb_calls.rds")

tbp_calls$PC1.mfcc <- NA
tbp_calls$PC2.mfcc <- NA

for (i in unique(tbp_calls$type)) {
    pca <- prcomp(tbp_mfcc[tbp_calls$type == i, -c(1:2)], center = TRUE,
        scale. = TRUE)
    tbp_calls$PC1.mfcc[tbp_calls$type == i] <- pca$x[, 1]
    tbp_calls$PC2.mfcc[tbp_calls$type == i] <- pca$x[, 2]

    # umap_l <- umap(tbp_mfcc[tbp_calls$type == i, -c(1:2)])
    # tbp_calls$PC1.mfcc[tbp_calls$type == i] <- umap_l$layout[,
    # 1] tbp_calls$PC2.mfcc[tbp_calls$type == i] <-
    # umap_l$layout[, 2]
}

1.2 Data

Click to see data

1.3 Partition variance contribution

Code
mod_barks <- lmer(PC1.mfcc ~ (1 | Site/UniqueBird), data = tbp_calls[tbp_calls$type ==
    "barks", ])

graph_part_var(levels = c("Site", "UniqueBird"), data = as.data.frame(tbp_calls),
    by = "type", response = "PC1.mfcc", full = FALSE, tick.labels = c("Site",
        "Individual"))
laughs:PC1.mfcc ~ (1 | Site / UniqueBird) 
              grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site (Intercept) <NA> 0.3422145 0.5849910  0.3674321
2            Site (Intercept) <NA> 0.0000000 0.0000000  0.0000000
3        Residual        <NA> <NA> 0.5891536 0.7675634  0.6325679
barks:PC1.mfcc ~ (1 | Site / UniqueBird) 
              grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site (Intercept) <NA> 0.6301692 0.7938320  0.4744282
2            Site (Intercept) <NA> 0.3966465 0.6297988  0.2986187
3        Residual        <NA> <NA> 0.3014552 0.5490493  0.2269531
Warning: Using the `size` aesthetic in this geom was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` in the `default_aes` field and elsewhere instead.

2 Yellow-naped amazons

2.1 MFCCs

Code
yna_mfcc <- readRDS("./data/processed/mfcc_yna_calls.rds")
yna_calls <- readRDS("./data/processed/extended_selection_table_yna_calls.rds")

2.2 Data

Click to see data

2.2.1 Single PCA including both years

Code
# pca
pca <- prcomp(yna_mfcc[, -c(1:2)], center = TRUE, scale. = TRUE)

yna_calls$PC1.mfcc <- pca$x[, 1]
yna_calls$PC2.mfcc <- pca$x[, 2]

# yna_calls$PC1.mfcc <- NA yna_calls$PC2.mfcc <- NA for (i in
# unique(yna_calls$type)){ pca <- prcomp(yna_mfcc[yna_calls$type
# == i, -c(1:2)], center = TRUE, scale. = TRUE)
# yna_calls$PC1.mfcc[yna_calls$type == i] <- pca$x[, 1]
# yna_calls$PC2.mfcc[yna_calls$type == i] <- pca$x[, 2] # umap_l
# <- umap(yna_mfcc[yna_calls$type == i, -c(1:2)]) #
# yna_calls$PC1.mfcc[yna_calls$type == i] <- umap_l$layout[, 1]
# # yna_calls$PC2.mfcc[yna_calls$type == i] <- umap_l$layout[,
# 2] }

yna_calls <- yna_calls[!is.na(yna_calls$Site), ]

2.2.1.1 Partition variance contribution

Code
graph_part_var(levels = c("Dialect", "Site", "UniqueBird"), data = as.data.frame(yna_calls),
    by = "type", response = "PC1.mfcc", full = TRUE, tick.labels = c("Dialect",
        "Site", "Individual"))
1994:PC1.mfcc ~ (1 | Dialect / Site / UniqueBird) 
                      grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site:Dialect (Intercept) <NA> 0.2085083 0.4566271  0.2700217
2            Site:Dialect (Intercept) <NA> 0.0812762 0.2850898  0.1052540
3                 Dialect (Intercept) <NA> 0.1956841 0.4423619  0.2534141
4                Residual        <NA> <NA> 0.2867222 0.5354645  0.3713101
2005:PC1.mfcc ~ (1 | Dialect / Site / UniqueBird) 
                      grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site:Dialect (Intercept) <NA> 0.4342576 0.6589822  0.4072297
2            Site:Dialect (Intercept) <NA> 0.2354432 0.4852249  0.2207894
3                 Dialect (Intercept) <NA> 0.1658560 0.4072542  0.1555332
4                Residual        <NA> <NA> 0.2308133 0.4804303  0.2164477
full: PC1.mfcc ~ (1 | Dialect / Site / UniqueBird) + (1 | type) 
                      grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site:Dialect (Intercept) <NA> 0.3696672 0.6080026  0.3030462
2            Site:Dialect (Intercept) <NA> 0.1727707 0.4156570  0.1416342
3                 Dialect (Intercept) <NA> 0.1940531 0.4405146  0.1590811
4                    type (Intercept) <NA> 0.2357404 0.4855311  0.1932556
5                Residual        <NA> <NA> 0.2476062 0.4976005  0.2029829

2.2.2 Individual PCA for each year

Code
yna_calls$PC1.mfcc <- NA
yna_calls$PC2.mfcc <- NA

for (i in unique(yna_calls$type)) {
    pca <- prcomp(yna_mfcc[yna_calls$type == i, -c(1:2)], center = TRUE,
        scale. = TRUE)
    yna_calls$PC1.mfcc[yna_calls$type == i] <- pca$x[, 1]
    yna_calls$PC2.mfcc[yna_calls$type == i] <- pca$x[, 2]

    # umap_l <- umap(yna_mfcc[yna_calls$type == i, -c(1:2)])
    # yna_calls$PC1.mfcc[yna_calls$type == i] <- umap_l$layout[,
    # 1] yna_calls$PC2.mfcc[yna_calls$type == i] <-
    # umap_l$layout[, 2]
}

yna_calls <- yna_calls[!is.na(yna_calls$Site), ]

2.2.2.1 Partition variance contribution

Code
graph_part_var(levels = c("Dialect", "Site", "UniqueBird"), data = as.data.frame(yna_calls),
    by = "type", response = "PC1.mfcc", full = TRUE, tick.labels = c("Dialect",
        "Site", "Individual"))
1994:PC1.mfcc ~ (1 | Dialect / Site / UniqueBird) 
                      grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site:Dialect (Intercept) <NA> 0.1116152 0.3340886  0.0839091
2            Site:Dialect (Intercept) <NA> 0.1430501 0.3782196  0.1075410
3                 Dialect (Intercept) <NA> 0.8251574 0.9083817  0.6203298
4                Residual        <NA> <NA> 0.2503687 0.5003686  0.1882201
2005:PC1.mfcc ~ (1 | Dialect / Site / UniqueBird) 
                      grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site:Dialect (Intercept) <NA> 0.4474666 0.6689294  0.4849532
2            Site:Dialect (Intercept) <NA> 0.2768944 0.5262075  0.3000913
3                 Dialect (Intercept) <NA> 0.0000000 0.0000000  0.0000000
4                Residual        <NA> <NA> 0.1983396 0.4453534  0.2149556
full: PC1.mfcc ~ (1 | Dialect / Site / UniqueBird) + (1 | type) 
                      grp        var1 var2      vcov     sdcor proportion
1 UniqueBird:Site:Dialect (Intercept) <NA> 0.3573140 0.5977575  0.3160685
2            Site:Dialect (Intercept) <NA> 0.3951709 0.6286262  0.3495555
3                 Dialect (Intercept) <NA> 0.1640521 0.4050335  0.1451152
4                    type (Intercept) <NA> 0.0000000 0.0000000  0.0000000
5                Residual        <NA> <NA> 0.2139585 0.4625565  0.1892608

3 Monk parakeets

3.1 MFCCs acoustic space by call type

Code
mpa_mfcc <- readRDS("./data/processed/mfcc_mpa_calls.rds")
mpa_calls <- readRDS("./data/processed/monk_parakeet_contactCalls_rangeComparison_extSelTable.RDS")

Sample sizes:

  • Calls per Bird:
Code
ids <- mpa_calls$Bird_ID
ids[is.na(ids)] <- "UNIDENTIFIED"
tbid <- table(ids)

tbid
ids
     INT-UM1     INT-UM10     INT-UM11     INT-UM12     INT-UM13     INT-UM15 
          23            7            1            1            1            1 
    INT-UM16     INT-UM17     INT-UM18     INT-UM19      INT-UM2      INT-UM3 
           9            6            1           12            1            1 
     INT-UM4      INT-UM5      INT-UM6      INT-UM7      INT-UM8      INT-UM9 
           1           21           25           29            1            6 
     NAT-AAT      NAT-RAW      NAT-UM1      NAT-UM2      NAT-UM3      NAT-UM4 
          13            5           26           24            6           18 
     NAT-UM5      NAT-ZW8 UNIDENTIFIED 
           8            9         1340 
Code
mpa_calls$country.region <- paste(mpa_calls$country, mpa_calls$region,
    sep = "-")
  • Birds with more than 1 call: 18
  • Country regions with identified birds: Uruguay-West - United States-Southwest - United States-Southeast

3.2 Data

Data set for those from marked individuals with more than 1 call:

Click to see data
Code
mpa_mfcc <- mpa_mfcc[mpa_calls$Bird_ID %in% names(tbid[tbid > 1]),
    ]
mpa_calls <- mpa_calls[mpa_calls$Bird_ID %in% names(tbid[tbid > 1]),
    ]

# pca
pca <- prcomp(mpa_mfcc[, -c(1:2)], center = TRUE, scale. = TRUE)

mpa_calls$PC1.mfcc <- pca$x[, 1]
mpa_calls$PC2.mfcc <- pca$x[, 2]


# mpa_calls$PC1.mfcc <- NA mpa_calls$PC2.mfcc <- NA for (i in
# unique(mpa_calls$country)){ pca <-
# prcomp(mpa_mfcc[mpa_calls$country == i, -c(1:2)], center =
# TRUE, scale. = TRUE) mpa_calls$PC1.mfcc[mpa_calls$country ==
# i] <- pca$x[, 1] mpa_calls$PC2.mfcc[mpa_calls$country == i] <-
# pca$x[, 2] # umap_l <- umap(mpa_mfcc[mpa_calls$type == i,
# -c(1:2)]) # mpa_calls$PC1.mfcc[mpa_calls$type == i] <-
# umap_l$layout[, 1] # mpa_calls$PC2.mfcc[mpa_calls$type == i]
# <- umap_l$layout[, 2] }

3.3 Partition variance contribution

Code
mpa_calls <- as.data.frame(mpa_calls)

mpa_calls$city_site <- mpa_calls$introduced_city

mpa_calls$city_site[mpa_calls$country == "Uruguay"] <- mpa_calls$site[mpa_calls$country ==
    "Uruguay"]

graph_part_var(levels = c("city_site", "region", "Bird_ID"), data = mpa_calls,
    response = "PC1.mfcc", full = TRUE, by = "range", tick.labels = c("City",
        "Region", "Individual"))
Native:PC1.mfcc ~ (1 | city_site / region / Bird_ID) 
                       grp        var1 var2       vcov     sdcor proportion
1 Bird_ID:region:city_site (Intercept) <NA> 0.23403508 0.4837717 0.49226484
2         region:city_site (Intercept) <NA> 0.02062357 0.1436091 0.04337921
3                city_site (Intercept) <NA> 0.07698449 0.2774608 0.16192769
4                 Residual        <NA> <NA> 0.14378200 0.3791860 0.30242827
Introduced:PC1.mfcc ~ (1 | city_site / region / Bird_ID) 
                       grp        var1 var2      vcov     sdcor proportion
1 Bird_ID:region:city_site (Intercept) <NA> 0.3369851 0.5805042  0.5410605
2         region:city_site (Intercept) <NA> 0.0000000 0.0000000  0.0000000
3                city_site (Intercept) <NA> 0.0000000 0.0000000  0.0000000
4                 Residual        <NA> <NA> 0.2858382 0.5346384  0.4589395
full: PC1.mfcc ~ (1 | city_site / region / Bird_ID) + (1 | range) 
Warning in checkConv(attr(opt, "derivs"), opt$par, ctrl = control$checkConv, : Model is nearly unidentifiable: large eigenvalue ratio
 - Rescale variables?
                       grp        var1 var2       vcov     sdcor proportion
1 Bird_ID:region:city_site (Intercept) <NA> 0.28937387 0.5379348 0.23343631
2         region:city_site (Intercept) <NA> 0.02275861 0.1508596 0.01835925
3                city_site (Intercept) <NA> 0.03278378 0.1810629 0.02644649
4                    range (Intercept) <NA> 0.67125831 0.8193036 0.54150039
5                 Residual        <NA> <NA> 0.22345207 0.4727072 0.18025756

4 Budgerigars

Code
budgie_dat <- read.csv("./data/processed/budgie_tsne_acoustic_space.csv")

metadat <- read_xlsx("./data/processed/budgie_experiment_metadata.xlsx")
metadat$flock <- paste(metadat$Treatment, metadat$`Treatment Room`,
    sep = "-")

budgie_dat$flock <- sapply(budgie_dat$ID, function(x) {
    metadat$flock[metadat$`Bird ID` == x][1]
})

budgie_dat <- budgie_dat[, c("ID", "treatment", "flock", "week", "TSNE1",
    "TSNE2")]

4.1 Data

Click to see data

4.2 Partition variance contribution

Code
budgie_dat_week_1_4 <- budgie_dat[budgie_dat$week %in% c(1, 4), ]

budgie_dat_week_1_4$week <- paste("week", budgie_dat_week_1_4$week,
    sep = " ")

graph_part_var(levels = c("flock", "ID"), data = budgie_dat_week_1_4,
    response = "TSNE1", full = TRUE, by = "week", tick.labels = c("Flock",
        "Individual"))
week 1:TSNE1 ~ (1 | flock / ID) 
       grp        var1 var2      vcov     sdcor proportion
1 ID:flock (Intercept) <NA> 0.3586982 0.5989142  0.3356301
2    flock (Intercept) <NA> 0.0000000 0.0000000  0.0000000
3 Residual        <NA> <NA> 0.7100327 0.8426344  0.6643699
week 4:TSNE1 ~ (1 | flock / ID) 
       grp        var1 var2       vcov     sdcor proportion
1 ID:flock (Intercept) <NA> 0.40827684 0.6389654 0.37821256
2    flock (Intercept) <NA> 0.02903974 0.1704105 0.02690134
3 Residual        <NA> <NA> 0.64217384 0.8013575 0.59488610
full: TSNE1 ~ (1 | flock / ID) + (1 | week) 
       grp        var1 var2        vcov      sdcor  proportion
1 ID:flock (Intercept) <NA> 0.322583389 0.56796425 0.302336148
2    flock (Intercept) <NA> 0.000000000 0.00000000 0.000000000
3     week (Intercept) <NA> 0.006257408 0.07910378 0.005864656
4 Residual        <NA> <NA> 0.738128508 0.85914406 0.691799196

5 Long-billed hermits

Code
gg_var_pc1 <- readRDS("~/Dropbox/Projects/lbh_cultural_evolution_pattern_and_process/output/plots/song_variance_contribution_spectro.RDS")


# gg_var_pc1 <- gg_var_pc1 + scale_fill_manual(values = mako(4, begin = 0.1, end = 0.9, direction = 1, alpha = 0.9)[c(1, 3, 2, 4)]) +  theme(legend.position = "none")


# Data for the nested ovals
oval_data <- data.frame(
    # label = c("A", "B", "C", "D"),
    x0 = c(0, 0, 0, 0),                # Center x position for all ovals
    y0 = c(-1.7, -0.4, 0.7, 2.2),                # Center y position for all ovals
    b = c(1.5, 3, 4.5, 6),             # Semi-major axis (horizontal radius)
    a = c(2, 4, 6, 8) + 1,                 # Semi-minor axis (vertical radius)
    text.pos = c(-4.7, -2, 1, 3.2) + 3,  # Radius for each circle
    label = rev(c("Site", "Lek", "Song neighborhood", "Individual"))  # 
)

oval_data$label <- factor(oval_data$label, levels = (c("Site", "Lek", "Song neighborhood", "Individual")))

 oval_data$label.color <-  viridis::mako(
        nrow(oval_data),
        begin = 0.1,
        end = 0.9,
        direction = -1,
        alpha = 1
    )
    oval_data$label.color <- factor(oval_data$label.color, levels = oval_data$label.color[order(oval_data$b, decreasing = TRUE)])
    

# Plot using ggplot2 and ggforce
oval_plot <- ggplot(oval_data) + 
    geom_ellipse(aes(x0 = x0, y0 = y0, a = a, b = b, angle = 0, fill = label.color), color = "white") +
    geom_text(aes(x = x0, y = text.pos, label = label), size = c(6, 5, 6, 6), color = rev(c( "white",rep("black", 3)))) + 
    scale_fill_viridis_d(option = "G", begin = 0.1, end = 0.9, guide = "none", direction = -1, alpha = 0.6) + # Add labels in the center
    scale_fill_identity() +  # Use the colors as specified in the data
    theme_void() +           # Remove background, axes, and grid
    theme(legend.position = "none") +
    coord_fixed()  #+          # Keep aspect ratio fixed for true ovals
    #ylim(c(-12, 6))

oval_plot

Code
gg_var_pc1 + theme(legend.position = "none") + theme_classic() + labs(x = "Level", y = "Explained variance (%)") +
     scale_x_discrete(labels = c("Individuo" = "Individual", "Vecindario de canto" = "Song neighborhood", "Corral" = "Lek", "Sitio" = "Site")) + facet_wrap(~ feature, nrow = 1, labeller= as_labeller(c("Rasgos espectro-temporales" = "Full")))

Code
# cowplot::plot_grid(gg_var_pc1 +    scale_y_continuous(labels = label_percent(), limits = c(0, 0.7)), oval_plot, nrow = 1, rel_widths = c(1, 0.7))

Takeaways

  • Call type dependent variation in variance explained

Session information

─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.5.0 (2025-04-11)
 os       Ubuntu 22.04.5 LTS
 system   x86_64, linux-gnu
 ui       X11
 language (EN)
 collate  en_US.UTF-8
 ctype    en_US.UTF-8
 tz       America/Costa_Rica
 date     2025-08-01
 pandoc   3.2 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/x86_64/ (via rmarkdown)
 quarto   1.7.31 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package          * version   date (UTC) lib source
 abind              1.4-8     2024-09-12 [1] CRAN (R 4.5.0)
 askpass            1.2.1     2024-10-04 [3] CRAN (R 4.5.0)
 bitops             1.0-9     2024-10-03 [3] CRAN (R 4.5.0)
 boot               1.3-31    2024-08-28 [4] CRAN (R 4.4.2)
 brio               1.1.5     2024-04-24 [3] CRAN (R 4.5.0)
 bslib              0.9.0     2025-01-30 [1] CRAN (R 4.5.0)
 cachem             1.1.0     2024-05-16 [1] CRAN (R 4.5.0)
 cellranger         1.1.0     2016-07-27 [3] CRAN (R 4.0.1)
 class              7.3-23    2025-01-01 [4] CRAN (R 4.4.2)
 classInt           0.4-11    2025-01-08 [1] CRAN (R 4.5.0)
 cli                3.6.5     2025-04-23 [1] CRAN (R 4.5.0)
 cluster            2.1.8.1   2025-03-12 [4] CRAN (R 4.4.3)
 codetools          0.2-20    2024-03-31 [4] CRAN (R 4.5.0)
 crayon             1.5.3     2024-06-20 [1] CRAN (R 4.5.0)
 crosstalk          1.2.1     2023-11-23 [3] CRAN (R 4.5.0)
 curl               6.4.0     2025-06-22 [1] CRAN (R 4.5.0)
 DBI                1.2.3     2024-06-02 [1] CRAN (R 4.5.0)
 deldir             2.0-4     2024-02-28 [1] CRAN (R 4.5.0)
 devtools           2.4.5     2022-10-11 [1] CRAN (R 4.5.0)
 digest             0.6.37    2024-08-19 [1] CRAN (R 4.5.0)
 dplyr              1.1.4     2023-11-17 [1] CRAN (R 4.5.0)
 DT               * 0.33      2024-04-04 [3] CRAN (R 4.5.0)
 dtw                1.23-1    2022-09-19 [1] CRAN (R 4.5.0)
 e1071              1.7-16    2024-09-16 [1] CRAN (R 4.5.0)
 ellipsis           0.3.2     2021-04-29 [3] CRAN (R 4.1.1)
 evaluate           1.0.4     2025-06-18 [1] CRAN (R 4.5.0)
 farver             2.1.2     2024-05-13 [1] CRAN (R 4.5.0)
 fastmap            1.2.0     2024-05-15 [1] CRAN (R 4.5.0)
 fftw               1.0-9     2024-09-20 [3] CRAN (R 4.5.0)
 formatR          * 1.14      2023-01-17 [1] CRAN (R 4.5.0)
 fs                 1.6.6     2025-04-12 [1] CRAN (R 4.5.0)
 generics           0.1.4     2025-05-09 [1] CRAN (R 4.5.0)
 ggforce          * 0.3.3     2021-03-05 [3] CRAN (R 4.1.1)
 ggplot2          * 3.5.2     2025-04-09 [1] CRAN (R 4.5.0)
 glue               1.8.0     2024-09-30 [1] CRAN (R 4.5.0)
 goftest            1.2-3     2021-10-07 [3] CRAN (R 4.1.1)
 gridExtra          2.3       2017-09-09 [1] CRAN (R 4.5.0)
 gtable             0.3.6     2024-10-25 [1] CRAN (R 4.5.0)
 htmltools          0.5.8.1   2024-04-04 [1] CRAN (R 4.5.0)
 htmlwidgets        1.6.4     2023-12-06 [1] RSPM (R 4.5.0)
 httpuv             1.6.16    2025-04-16 [1] RSPM (R 4.5.0)
 httr               1.4.7     2023-08-15 [3] CRAN (R 4.5.0)
 jquerylib          0.1.4     2021-04-26 [1] CRAN (R 4.5.0)
 jsonlite           2.0.0     2025-03-27 [1] CRAN (R 4.5.0)
 KernSmooth         2.23-26   2025-01-01 [4] CRAN (R 4.4.2)
 knitr            * 1.50      2025-03-16 [1] CRAN (R 4.5.0)
 labeling           0.4.3     2023-08-29 [1] CRAN (R 4.5.0)
 later              1.4.2     2025-04-08 [1] RSPM (R 4.5.0)
 lattice            0.22-7    2025-04-02 [4] CRAN (R 4.5.0)
 lifecycle          1.0.4     2023-11-07 [1] CRAN (R 4.5.0)
 lme4             * 1.1-37    2025-03-26 [1] CRAN (R 4.5.0)
 magrittr           2.0.3     2022-03-30 [1] CRAN (R 4.5.0)
 MASS               7.3-65    2025-02-28 [4] CRAN (R 4.4.3)
 Matrix           * 1.7-3     2025-03-11 [4] CRAN (R 4.4.3)
 memoise            2.0.1     2021-11-26 [3] CRAN (R 4.1.2)
 mgcv               1.9-3     2025-04-04 [4] CRAN (R 4.5.0)
 mime               0.13      2025-03-17 [1] CRAN (R 4.5.0)
 miniUI             0.1.2     2025-04-17 [3] CRAN (R 4.5.0)
 minqa              1.2.4     2014-10-09 [3] CRAN (R 4.0.1)
 NatureSounds     * 1.0.5     2025-01-17 [1] CRAN (R 4.5.0)
 nicheROVER         1.1.2     2023-10-13 [1] CRAN (R 4.5.0)
 nlme               3.1-168   2025-03-31 [4] CRAN (R 4.4.3)
 nloptr             2.2.1     2025-03-17 [3] CRAN (R 4.5.0)
 openssl            2.3.3     2025-05-26 [1] CRAN (R 4.5.0)
 packrat            0.9.2     2023-09-05 [1] CRAN (R 4.5.0)
 pbapply            1.7-4     2025-07-20 [1] CRAN (R 4.5.0)
 permute            0.9-8     2025-06-25 [1] CRAN (R 4.5.0)
 PhenotypeSpace   * 0.1.1     2025-07-21 [1] Github (maRce10/PhenotypeSpace@1ee576c)
 pillar             1.11.0    2025-07-04 [1] CRAN (R 4.5.0)
 pkgbuild           1.4.8     2025-05-26 [1] CRAN (R 4.5.0)
 pkgconfig          2.0.3     2019-09-22 [1] CRAN (R 4.5.0)
 pkgload            1.4.0     2024-06-28 [1] CRAN (R 4.5.0)
 png                0.1-8     2022-11-29 [3] CRAN (R 4.5.0)
 polyclip           1.10-7    2024-07-23 [1] CRAN (R 4.5.0)
 profvis            0.4.0     2024-09-20 [1] CRAN (R 4.5.0)
 promises           1.3.3     2025-05-29 [1] RSPM (R 4.5.0)
 proxy              0.4-27    2022-06-09 [1] CRAN (R 4.5.0)
 purrr              1.0.4     2025-02-05 [1] CRAN (R 4.5.0)
 R6                 2.6.1     2025-02-15 [1] CRAN (R 4.5.0)
 raster             3.6-32    2025-03-28 [1] CRAN (R 4.5.0)
 rbibutils          2.3       2024-10-04 [1] CRAN (R 4.5.0)
 RColorBrewer       1.1-3     2022-04-03 [1] CRAN (R 4.5.0)
 Rcpp               1.1.0     2025-07-02 [1] CRAN (R 4.5.0)
 RCurl              1.98-1.17 2025-03-22 [1] CRAN (R 4.5.0)
 Rdpack             2.6.4     2025-04-09 [1] CRAN (R 4.5.0)
 readxl           * 1.4.5     2025-03-07 [3] CRAN (R 4.5.0)
 reformulas         0.4.1     2025-04-30 [1] CRAN (R 4.5.0)
 remotes            2.5.0     2024-03-17 [1] CRAN (R 4.5.0)
 reticulate         1.42.0    2025-03-25 [1] CRAN (R 4.5.0)
 rjson              0.2.23    2024-09-16 [1] CRAN (R 4.5.0)
 rlang              1.1.6     2025-04-11 [1] CRAN (R 4.5.0)
 rmarkdown          2.29      2024-11-04 [1] CRAN (R 4.5.0)
 RSpectra           0.16-0    2019-12-01 [3] CRAN (R 4.0.1)
 rstudioapi         0.17.1    2024-10-22 [1] CRAN (R 4.5.0)
 Rtsne            * 0.17      2023-12-07 [1] CRAN (R 4.5.0)
 sass               0.4.10    2025-04-11 [1] CRAN (R 4.5.0)
 scales           * 1.4.0     2025-04-24 [1] CRAN (R 4.5.0)
 seewave          * 2.2.3     2023-10-19 [1] CRAN (R 4.5.0)
 sessioninfo        1.2.3     2025-02-05 [3] CRAN (R 4.5.0)
 sf                 1.0-21    2025-05-15 [1] CRAN (R 4.5.0)
 shiny              1.10.0    2024-12-14 [1] CRAN (R 4.5.0)
 signal             1.8-1     2024-06-26 [1] CRAN (R 4.5.0)
 sketchy            1.0.5     2025-04-22 [1] CRANs (R 4.5.0)
 sp                 2.2-0     2025-02-01 [1] CRAN (R 4.5.0)
 spatstat.data      3.1-6     2025-03-17 [1] CRAN (R 4.5.0)
 spatstat.explore   3.4-3     2025-05-21 [1] CRAN (R 4.5.0)
 spatstat.geom      3.5-0     2025-07-20 [1] CRAN (R 4.5.0)
 spatstat.random    3.4-1     2025-05-20 [1] CRAN (R 4.5.0)
 spatstat.sparse    3.1-0     2024-06-21 [1] CRAN (R 4.5.0)
 spatstat.univar    3.1-4     2025-07-13 [1] CRAN (R 4.5.0)
 spatstat.utils     3.1-5     2025-07-17 [1] CRAN (R 4.5.0)
 stringi            1.8.7     2025-03-27 [1] CRAN (R 4.5.0)
 stringr            1.5.1     2023-11-14 [1] CRAN (R 4.5.0)
 tensor             1.5.1     2025-06-17 [1] CRAN (R 4.5.0)
 terra              1.8-60    2025-07-21 [1] CRAN (R 4.5.0)
 testthat           3.2.3     2025-01-13 [1] CRAN (R 4.5.0)
 tibble             3.3.0     2025-06-08 [1] RSPM (R 4.5.0)
 tidyselect         1.2.1     2024-03-11 [1] CRAN (R 4.5.0)
 tuneR            * 1.4.7     2024-04-17 [1] CRAN (R 4.5.0)
 tweenr             2.0.3     2024-02-26 [3] CRAN (R 4.5.0)
 umap             * 0.2.10.0  2023-02-01 [1] CRAN (R 4.5.0)
 units              0.8-7     2025-03-11 [1] CRAN (R 4.5.0)
 urlchecker         1.0.1     2021-11-30 [1] CRAN (R 4.5.0)
 usethis            3.1.0     2024-11-26 [3] CRAN (R 4.5.0)
 vctrs              0.6.5     2023-12-01 [1] CRAN (R 4.5.0)
 vegan              2.7-1     2025-06-05 [1] CRAN (R 4.5.0)
 viridis          * 0.6.5     2024-01-29 [1] CRAN (R 4.5.0)
 viridisLite      * 0.4.2     2023-05-02 [1] CRAN (R 4.5.0)
 warbleR          * 1.1.36    2016-04-19 [1] CRAN (R 4.5.0)
 withr              3.0.2     2024-10-28 [1] CRAN (R 4.5.0)
 xaringanExtra      0.8.0     2024-05-19 [1] CRAN (R 4.5.0)
 xfun               0.52      2025-04-02 [1] CRAN (R 4.5.0)
 xtable             1.8-4     2019-04-21 [3] CRAN (R 4.0.1)
 yaml               2.3.10    2024-07-26 [1] CRAN (R 4.5.0)

 [1] /home/m/R/x86_64-pc-linux-gnu-library/4.5
 [2] /usr/local/lib/R/site-library
 [3] /usr/lib/R/site-library
 [4] /usr/lib/R/library
 * ── Packages attached to the search path.

──────────────────────────────────────────────────────────────────────────────