TidyTuesday
    • About TidyTuesday
    • Datasets
      • 2025
      • 2024
      • 2023
      • 2022
      • 2021
      • 2020
      • 2019
      • 2018
    • Useful links

    On this page

    • IELTS exam results
      • The Data
      • How to Participate
        • PydyTuesday: A Posit collaboration with TidyTuesday
      • Data Dictionary
        • demo_by_first_language.csv
        • demo_by_nationality.csv
        • demo_by_reasons.csv
        • performance_by_first_language.csv
        • performance_by_nationality.csv
      • Cleaning Script

    IELTS exam results

    This week we’re exploring International English Language Testing System (IELTS) Tests statistics. The dataset comes from the IELTS research website and contains mean scores in various aggregations.

    IELTS is proud to offer transparent statistics on our testing system. The following data has been compiled from the scores achieved by various groups of test takers. It helps researchers and teachers understand the performance of the test and how test takers perform in particular countries or regions.

    The IELTS exam consists of 4 parts: Listening, Speaking, Reading and Writing. Each part is scored using a “band” system from 1 to 9, and then the four parts are averaged for an overall score. You can find more information on what each band means in the IELTS website. There are two versions of the exam: Academic and General Training. The dataset provides values for the years 2022, 2023 and 2024.

    Some questions you can answer:

    • Do English speakers consistently get top marks on the English language test?
    • Which parts of the test do test takers find the most difficult? Is it the same for all test takers?
    • Does the reason for taking the test affect the score?
    • Has there been a change in tests scores in the three years for which we have information?

    Thank you to Elio Campitelli for curating this week’s dataset.

    The Data

    # Using R
    # Option 1: tidytuesdayR R package 
    ## install.packages("tidytuesdayR")
    
    tuesdata <- tidytuesdayR::tt_load('2026-08-18')
    ## OR
    tuesdata <- tidytuesdayR::tt_load(2026, week = 33)
    
    demo_by_first_language <- tuesdata$demo_by_first_language
    demo_by_nationality <- tuesdata$demo_by_nationality
    demo_by_reasons <- tuesdata$demo_by_reasons
    performance_by_first_language <- tuesdata$performance_by_first_language
    performance_by_nationality <- tuesdata$performance_by_nationality
    
    # Option 2: Read directly from GitHub
    
    demo_by_first_language <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_first_language.csv')
    demo_by_nationality <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_nationality.csv')
    demo_by_reasons <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_reasons.csv')
    performance_by_first_language <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_first_language.csv')
    performance_by_nationality <- readr::read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_nationality.csv')
    # Using Python
    # Option 1: pydytuesday python library
    ## pip install pydytuesday
    
    import pydytuesday
    
    # Download files from the week, which you can then read in locally
    pydytuesday.get_date('2026-08-18')
    
    # Option 2: Read directly from GitHub and assign to an object
    
    demo_by_first_language = pandas.read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_first_language.csv')
    demo_by_nationality = pandas.read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_nationality.csv')
    demo_by_reasons = pandas.read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_reasons.csv')
    performance_by_first_language = pandas.read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_first_language.csv')
    performance_by_nationality = pandas.read_csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_nationality.csv')
    # Using Julia
    # Option 1: TidierTuesday.jl library
    ## Pkg.add(url="https://github.com/TidierOrg/TidierTuesday.jl")
    
    using TidierTuesday
    
    # Download datasets for the week, and load them as a NamedTuple of DataFrames
    data = tt_load("2026-08-18")
    
    # Option 2: Read directly from GitHub and assign to an object with TidierFiles
    
    demo_by_first_language = read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_first_language.csv")
    demo_by_nationality = read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_nationality.csv")
    demo_by_reasons = read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_reasons.csv")
    performance_by_first_language = read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_first_language.csv")
    performance_by_nationality = read_csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_nationality.csv")
    
    # Option 3: Read directly from Github and assign without Tidier dependencies
    demo_by_first_language = CSV.read("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_first_language.csv", DataFrame)
    demo_by_nationality = CSV.read("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_nationality.csv", DataFrame)
    demo_by_reasons = CSV.read("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/demo_by_reasons.csv", DataFrame)
    performance_by_first_language = CSV.read("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_first_language.csv", DataFrame)
    performance_by_nationality = CSV.read("https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2026/2026-08-18/performance_by_nationality.csv", DataFrame)

    How to Participate

    • Explore the data, watching out for interesting relationships. We would like to emphasize that you should not draw conclusions about causation in the data. There are various moderating variables that affect all data, many of which might not have been captured in these datasets. As such, our suggestion is to use the data provided to practice your data tidying and plotting techniques, and to consider for yourself what nuances might underlie these relationships.
    • Create a visualization, a model, a Quarto report, a shiny app, or some other piece of data-science-related output, using R, Python, or another programming language.
    • Share your output and the code used to generate it on social media with the #TidyTuesday hashtag.
    • Submit your own dataset!

    PydyTuesday: A Posit collaboration with TidyTuesday

    • Exploring the TidyTuesday data in Python? Posit has some extra resources for you! Have you tried making a Quarto dashboard? Find videos and other resources in Posit’s PydyTuesday repo.
    • Share your work with the world using the hashtags #TidyTuesday and #PydyTuesday so that Posit has the chance to highlight your work, too!
    • Deploy or share your work however you want! If you’d like a super easy way to publish your work, give Connect Cloud a try.

    Data Dictionary

    demo_by_first_language.csv

    variable class description
    type character Type of IELTS exam (Academic or General Training).
    language character Native language of the test taker.
    band character Band Score.
    percent double Proportion of test taker of this native language that got this band score.
    year character Year cohort.

    demo_by_nationality.csv

    variable class description
    type character Type of IELTS exam (Academic or General Training).
    nationality character Nationality of the test taker.
    band character Band Score.
    percent double Proportion of test taker of this nationality that got this band score.
    year character Year cohort.

    demo_by_reasons.csv

    variable class description
    type character Type of IELTS exam (Academic or General Training).
    reason character Stated reason to take the exam.
    band character Band Score.
    percent double Proportion of test taker of this reason that got this band score.
    year character Year cohort.

    performance_by_first_language.csv

    variable class description
    type character Type of IELTS exam (Academic or General Training).
    language character Native language of the test taker.
    part character Part of the test (listening, reading, writing, speaking and overall score)
    score double Mean score.
    year character Year cohort.

    performance_by_nationality.csv

    variable class description
    type character Type of IELTS exam (Academic or General Training).
    nationality character Nationality of the test taker.
    part character Part of the test (listening, reading, writing, speaking and overall score)
    score double Mean score.
    year character Year cohort.

    Cleaning Script

    # URL from https://ielts.org/researchers/our-research/test-statistics
    # and internet archive
    sources <- list(
      demographic = list(
        "2024-2025" = "https://ielts.org/cdn/ielts-research-data/ielts-demographic-data-2024-2025.xlsx",
        "2023-2024" = "https://ielts.org/cdn/ielts-research-data/ielts-demographic-data-2023-2024.xlsx",
        "2022-2023" = "https://web.archive.org/web/20240105131824/https://s3.eu-west-2.amazonaws.com/ielts-web-static/production/Research/ielts-demographic-data-2022.xlsx"
      ),
      performance = list(
        "2024-2025" = "https://ielts.org/cdn/ielts-research-data/ielts-test-taker-performance-data-2024-2025.xlsx",
        "2023-2024" = "https://ielts.org/cdn/ielts-research-data/ielts-test-taker-performance-data-2023-2024.xlsx",
        "2022-2023" = "https://web.archive.org/web/20231001091407/https://s3.eu-west-2.amazonaws.com/ielts-web-static/production/Research/ielts-test-taker-performance-data-2022.xlsx"
      )
    )
    
    root_dir <- "data/curated/ielts"
    
    download <- function(type, year) {
      dir <- file.path(root_dir, "data/raw")
      url <- sources[[type]][[year]]
      destfile <- file.path(dir, glue::glue("{year}_{type}.xlsx"))
      if (file.exists(destfile)) {
        return(destfile)
      }
      dir.create(dirname(destfile), recursive = TRUE, showWarnings = FALSE)
      download.file(url, destfile, mode = "wb")
      return(destfile)
    }
    
    read_page_demographics <- function(file, page) {
      bands <- c(
        "<4",
        "4",
        "4.5",
        "5",
        "5.5",
        "6",
        "6.5",
        "7",
        "7.5",
        "8",
        "8.5",
        "9"
      )
    
      data <- readxl::read_excel(file, sheet = page, skip = 3) |>
        na.omit() # last row sometimes is blank
    
      colnames(data)[colnames(data) == "Below 4"] <- "<4"
      colnames(data) <- tolower(colnames(data))
      data[["statistic"]] <- NULL
      id <- colnames(data)[1]
    
      data |>
        tidyr::pivot_longer(cols = -1, names_to = "band", values_to = "percent")
    }
    
    read_page_performance <- function(file, page) {
      cols <- c("listening", "reading", "writing", "speaking", "overall")
    
      data <- readxl::read_excel(file, sheet = page, skip = 2) |>
        na.omit() |> # last row sometimes is blank
        dplyr::select(-2) # remove the "Statistic" column
    
      colnames(data)[2:6] <- cols
      colnames(data) <- tolower(colnames(data))
      id <- colnames(data)[1]
    
      data |>
        tidyr::pivot_longer(cols = -1, names_to = "part", values_to = "score")
    }
    
    prepare <- function(name, pages_groups, read_page) {
      dir <- file.path(root_dir, "data/cleaned")
      dir.create(dir, recursive = TRUE, showWarnings = FALSE)
      out <- list()
      for (group in names(pages_groups)) {
        out[[group]] <- lapply(names(sources[[name]]), \(year) {
          file <- download(name, year)
          lapply(pages_groups[[group]], \(page) read_page(file, page)) |>
            setNames(c("Academic", "General_Training")) |>
            dplyr::bind_rows(.id = "type") |>
            dplyr::mutate(year = year)
        }) |>
          dplyr::bind_rows() 
      }
      return(out)
    }
    
    demo <- prepare(
      "demographic",
      list(
        reasons = 1:2,
        first_language = 3:4,
        nationality = 5:6
      ),
      read_page_demographics
    )
    demo_by_reasons <- demo$reasons
    demo_by_first_language <- demo$first_language
    demo_by_nationality <- demo$nationality
    
    performance <- prepare(
      "performance",
      list(
        nationality = 1:2,
        first_language = 3:4
      ),
      read_page_performance
    )
    
    performance_by_nationality <- performance$nationality
    performance_by_first_language <- performance$first_language