Demographic Analysis

Population composition changes and their impact

Introduction

Between 2018 and 2024, the UK population composition changed significantly due to immigration. Using Office for National Statistics (ONS) provisional long-term international migration data, we can quantify these changes precisely and assess their potential impact on church attendance patterns.

Key immigration flows 2018-2024:

  • Net migration: Cumulative +4.2 million (YE 2019 - YE 2024)
  • Non-EU+ immigration surge: From 346,000 (YE Mar 2019) to 1,122,000 (YE Mar 2024)
  • Ukrainian refugees: Significant arrivals post-February 2022
  • Hong Kong BN(O) visa holders: >163,000 (January 2021 onwards)
  • Other migration: Substantial increases across various visa routes

These groups may have different religious attendance patterns than the existing UK population, which could explain apparent changes in church attendance without any underlying “revival” among existing residents.

ONS Migration Data Analysis

Loading and Preparing ONS Data

Show the code
# Load ONS long-term international migration data
# Skip first 12 rows (metadata + multi-line header), then set column names manually
ons_migration_raw <- read_csv(
  here::here("data/ons/provisional-migration-2024/raw/Table 1-Table 1.csv"),
  skip = 12,
  col_names = c("flow", "period", "all_nationalities", "british", "eu_plus", "non_eu_plus",
                paste0("empty_", 1:8)),  # Extra empty columns in the CSV
  col_types = cols(.default = "c")  # Read everything as character first
)

# Clean and prepare data
ons_migration_clean <- ons_migration_raw %>%
  # Remove empty columns
  select(flow, period, all_nationalities, british, eu_plus, non_eu_plus) %>%
  # Remove empty rows
  filter(!is.na(flow) & flow != "") %>%
  # Remove notes/markers from values and convert to numeric
  mutate(
    all_nationalities = as.numeric(gsub("[^0-9-]", "", all_nationalities)),
    british = as.numeric(gsub("[^0-9-]", "", british)),
    eu_plus = as.numeric(gsub("[^0-9-]", "", eu_plus)),
    non_eu_plus = as.numeric(gsub("[^0-9-]", "", non_eu_plus))
  )

# Filter for survey-relevant period: YE 2018 to YE 2024
migration_period <- ons_migration_clean %>%
  filter(grepl("YE", period)) %>%
  # Create proper year variable
  mutate(
    # Extract 2-digit year suffix (e.g., "YE Jun 12" -> "12")
    year_suffix = as.numeric(str_extract(period, "[0-9]{2}$")),
    # Convert to 4-digit year (18-99 = 2018-2099, 00-17 = 2000-2017)
    year = ifelse(year_suffix >= 18, 2000 + year_suffix, 2000 + year_suffix),
    # Extract quarter/month (e.g., "YE Jun 12" -> "Jun")
    quarter = str_extract(period, "(?<=YE )[A-Za-z]+")
  ) %>%
  # Focus on key periods
  filter(year >= 2018 & year <= 2024)

Cumulative Impact: How Much Did Immigration Change the Population?

Show the code
# Calculate cumulative net migration between survey periods
# 2018 survey: Oct-Nov 2018 (use YE Dec 2018 as proxy)
# 2024 survey: Nov-Dec 2024 (use YE Dec 2024)

survey_period_migration <- net_migration %>%
  filter(
    (year == 2018 & quarter == "Dec") |
    (year == 2024 & quarter == "Dec")
  ) %>%
  select(year, quarter, all_nationalities, british, eu_plus, non_eu_plus)

# Calculate cumulative between periods
cumulative_migration <- net_migration %>%
  filter(
    (year == 2019) |
    (year >= 2020 & year <= 2023) |
    (year == 2024 & quarter %in% c("Mar", "Jun", "Sep", "Dec"))
  ) %>%
  summarise(
    total_net = sum(all_nationalities, na.rm = TRUE) / 1000,
    british_net = sum(british, na.rm = TRUE) / 1000,
    eu_net = sum(eu_plus, na.rm = TRUE) / 1000,
    non_eu_net = sum(non_eu_plus, na.rm = TRUE) / 1000
  )

# UK population estimate (2024)
uk_pop_2024 <- 68.3e6  # millions
england_wales_pop_2024 <- 60.5e6  # millions (used in Bible Society analysis)

# Calculate percentages
pct_of_uk <- (cumulative_migration$total_net * 1000) / uk_pop_2024 * 100
pct_of_ew <- (cumulative_migration$total_net * 1000) / england_wales_pop_2024 * 100

Cumulative Net Migration (2019-2024):

Nationality Group Net Migration % of UK Pop. % of E&W Pop.
Total 7274.0 million 10.7% 12.0%
Non-EU+ 7711.0 million 11.3% 12.7%
EU+ 251.0 million 0.4% 0.4%
British -692.0 thousand -1.01% -1.14%

Key Finding: Between the 2018 and 2024 surveys, net migration added approximately 12.0% of the England & Wales population - roughly 1 in every 8 people.

Immigration Components: Who Arrived?

Show the code
# Filter immigration data
immigration <- migration_period %>%
  filter(flow == "Immigration") %>%
  arrange(year, match(quarter, c("Mar", "Jun", "Sep", "Dec"))) %>%
  mutate(
    period_label = paste0(quarter, " ", year),
    period_order = row_number()
  )

# Create stacked area plot
immigration_long <- immigration %>%
  select(period_order, period_label, british, eu_plus, non_eu_plus) %>%
  pivot_longer(
    cols = c(british, eu_plus, non_eu_plus),
    names_to = "nationality",
    values_to = "count"
  ) %>%
  mutate(
    nationality = factor(
      nationality,
      levels = c("non_eu_plus", "eu_plus", "british"),
      labels = c("Non-EU+", "EU+", "British")
    )
  )

ggplot(immigration_long, aes(x = period_order, y = count / 1000, fill = nationality)) +
  geom_area(alpha = 0.8) +
  # Survey periods
  annotate("segment", x = 2.5, xend = 2.5, y = 0, yend = 1400,
           linetype = "dashed", color = "blue", linewidth = 1) +
  annotate("text", x = 2.5, y = 1450, label = "2018\nSurvey",
           size = 3.5, fontface = "bold", color = "blue") +
  annotate("segment", x = 25.5, xend = 25.5, y = 0, yend = 1400,
           linetype = "dashed", color = "red", linewidth = 1) +
  annotate("text", x = 25.5, y = 1450, label = "2024\nSurvey",
           size = 3.5, fontface = "bold", color = "red") +
  scale_fill_manual(
    values = c("Non-EU+" = "#d7191c", "EU+" = "#fdae61", "British" = "#abd9e9"),
    name = "Nationality Group"
  ) +
  scale_x_continuous(
    breaks = seq(1, nrow(immigration), by = 4),
    labels = immigration$period_label[seq(1, nrow(immigration), by = 4)]
  ) +
  scale_y_continuous(labels = comma_format(suffix = "K")) +
  labs(
    title = "Immigration to UK: Dramatic Shift in Composition",
    subtitle = "Non-EU+ immigration surged whilst EU+ immigration declined post-Brexit",
    x = "Year Ending",
    y = "Immigration (thousands)",
    caption = "Source: ONS Long-term International Migration (provisional estimates)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    plot.subtitle = element_text(size = 11),
    legend.position = "top",
    axis.text.x = element_text(angle = 45, hjust = 1),
    panel.grid.minor = element_blank()
  )

Immigration to UK by nationality group (2018-2024)

The Composition Change Problem

Before vs After Comparison:

Show the code
# Compare immigration composition at survey times
composition_comparison <- immigration %>%
  filter(
    (year == 2018 & quarter == "Dec") |
    (year == 2024 & quarter == "Dec")
  ) %>%
  mutate(
    total_immigration = british + eu_plus + non_eu_plus,
    pct_british = british / total_immigration * 100,
    pct_eu = eu_plus / total_immigration * 100,
    pct_non_eu = non_eu_plus / total_immigration * 100
  ) %>%
  select(year, quarter, total_immigration, pct_british, pct_eu, pct_non_eu)
Period Total Immigration British EU+ Non-EU+
YE Dec 2018 825K 9% 51% 40%
YE Dec 2024 NAK NA% NA% NA%
Change NAK NA pp NA pp NA pp

Critical shift: Non-EU+ immigration increased from 40% to NA% of total immigration between survey periods.

Age Composition of Immigrants: The Smoking Gun

The ONS data includes age breakdowns for Non-EU+ nationals (the dominant immigrant group). This reveals why the Bible Society survey showed the unusual pattern of youngest adults having the largest attendance increase.

Show the code
# Load Table 7 - Age breakdown for Non-EU+ nationals
age_data_raw <- read_csv(
  here::here("data/ons/provisional-migration-2024/raw/Table 7-Table 1.csv"),
  skip = 8,
  col_names = c("period", "sex", "age_band", "immigration", "emigration", "net_migration",
                paste0("empty_", 1:5)),
  col_types = cols(.default = "c"),
  trim_ws = TRUE
)

# Clean age data for YE Dec 2024 (most recent)
age_composition <- age_data_raw %>%
  select(period, sex, age_band, immigration, net_migration) %>%
  # Remove empty rows and filter for Dec 2024, All sexes
  filter(
    !is.na(period),
    grepl("YE Dec 24", period),
    sex == "All",
    !is.na(age_band),
    age_band != "All",
    age_band != ""
  ) %>%
  mutate(
    # Clean up text fields and normalize age bands to handle format changes
    age_band = str_trim(age_band),
    age_band = case_when(
      grepl("Under 16", age_band) ~ "Under 16",
      grepl("16.*24", age_band) ~ "16-24",
      grepl("25.*34", age_band) ~ "25-34",
      grepl("35.*44", age_band) ~ "35-44",
      grepl("45.*54", age_band) ~ "45-54",
      grepl("55.*64", age_band) ~ "55-64",
      grepl("65", age_band) ~ "65+",
      TRUE ~ age_band
    ),
    # Use parse_number for robust numeric conversion from strings with commas etc.
    immigration = parse_number(immigration),
    net_migration = parse_number(net_migration)
  ) %>%
  # Remove any rows with NA values after conversion
  filter(!is.na(immigration), immigration > 0) %>%
  # Calculate percentages
  mutate(
    pct_immigration = immigration / sum(immigration, na.rm = TRUE) * 100,
    pct_net = net_migration / sum(net_migration, na.rm = TRUE) * 100
  ) %>%
  # Order by age for proper display
  mutate(
    age_order = case_when(
      age_band == "Under 16" ~ 1,
      age_band == "16-24" ~ 2,
      age_band == "25-34" ~ 3,
      age_band == "35-44" ~ 4,
      age_band == "45-54" ~ 5,
      age_band == "55-64" ~ 6,
      age_band == "65+" ~ 7,
      TRUE ~ 99
    )
  ) %>%
  arrange(age_order)

# Debug: Check what age bands we have
if (nrow(age_composition) == 0) {
  stop("No age data found! Check the filtering and data cleaning.")
}

# Calculate key statistics
young_adults_pct <- age_composition %>%
  filter(age_band %in% c("16-24", "25-34")) %>%
  summarise(total = sum(pct_immigration)) %>%
  pull(total)

under_35_pct <- age_composition %>%
  filter(age_band %in% c("Under 16", "16-24", "25-34")) %>%
  summarise(total = sum(pct_immigration)) %>%
  pull(total)

# Ensure we have the expected percentage
if (young_adults_pct < 10) {
  warning("Young adults percentage is unexpectedly low: ", young_adults_pct, 
          "%. Check data parsing. Age bands found: ", 
          paste(unique(age_composition$age_band), collapse = ", "))
}

Non-EU+ Immigration by Age (YE December 2024):

Non-EU+ immigration by age group (Year Ending December 2024)
Age Group Immigration % of Total
16-24 246,000 38.3%
25-34 243,000 37.9%
35-44 99,000 15.4%
45-54 34,000 5.3%
55-64 12,000 1.9%
65+ 8,000 1.2%

Key findings:

  • Young adults (16-34): 76% of all Non-EU+ immigrants
  • Under 35: 76% of all Non-EU+ immigrants
  • Over 55: Only 3% of immigrants
Show the code
# Create age pyramid visualization
ggplot(age_composition, aes(x = reorder(age_band, age_order), y = pct_immigration, fill = age_band)) +
  geom_col(alpha = 0.85, width = 0.7) +
  geom_text(aes(label = sprintf("%.1f%%", pct_immigration)), 
            vjust = -0.5, fontface = "bold", size = 4) +
  scale_fill_brewer(palette = "YlOrRd", direction = -1) +
  scale_y_continuous(limits = c(0, 45), breaks = seq(0, 45, 5),
                     expand = expansion(mult = c(0, 0.05))) +
  labs(
    title = "Non-EU+ Immigration Heavily Skewed Toward Young Adults",
    subtitle = sprintf("%.0f%% of Non-EU+ immigrants are aged 16-34", young_adults_pct),
    x = "Age Group",
    y = "Percentage of Non-EU+ Immigration (%)",
    caption = "Source: ONS Table 7 - Long-term international migration by age and sex (YE Dec 2024)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    legend.position = "none",
    plot.title = element_text(face = "bold", size = 14),
    plot.subtitle = element_text(size = 11, color = "#d73027"),
    axis.text.x = element_text(angle = 45, hjust = 1),
    panel.grid.major.x = element_blank()
  )

Age composition of Non-EU+ immigration (YE Dec 2024)

Comparing Immigrant Age Composition to Bible Society Survey Patterns

This age data provides the “smoking gun” evidence that the apparent “revival” is a composition effect:

Bible Society Survey showed: - 18-34 age group: +7.0pp increase (largest of all age groups) - 35-54 age group: +1.0pp increase (smallest increase) - 55+ age group: +2.0pp increase (moderate)

ONS immigration data shows: - 76% of Non-EU+ immigrants are aged 16-34 - Only 21% are aged 35-54 - Only 3% are aged 55+

Show the code
# Create comparison data
# Map ONS age bands to Bible Society categories
immigrant_age_mapped <- tibble(
  age_category = c("18-34", "35-54", "55+"),
  immigrant_pct = c(
    # 16-34 maps roughly to 18-34 (excluding under-16)
    young_adults_pct,
    # 35-54 from ONS bands
    age_composition %>% 
      filter(age_band %in% c("35-44", "45-54")) %>% 
      summarise(total = sum(pct_immigration)) %>% 
      pull(total),
    # 55+ from ONS bands
    age_composition %>% 
      filter(age_band %in% c("55-64", "65+")) %>% 
      summarise(total = sum(pct_immigration)) %>% 
      pull(total)
  ),
  survey_increase = c(7.0, 1.0, 2.0)
)

# Create dual-axis comparison
p1 <- ggplot(immigrant_age_mapped, aes(x = age_category)) +
  geom_col(aes(y = immigrant_pct, fill = "Immigration"), 
           alpha = 0.85, width = 0.4, position = position_nudge(x = -0.2)) +
  geom_col(aes(y = survey_increase * 10, fill = "Survey Increase"), 
           alpha = 0.85, width = 0.4, position = position_nudge(x = 0.2)) +
  geom_text(aes(y = immigrant_pct, label = sprintf("%.0f%%", immigrant_pct)),
            position = position_nudge(x = -0.2), vjust = -0.5, fontface = "bold", size = 4) +
  geom_text(aes(y = survey_increase * 10, label = sprintf("+%.1fpp", survey_increase)),
            position = position_nudge(x = 0.2), vjust = -0.5, fontface = "bold", size = 4) +
  scale_fill_manual(
    values = c("Immigration" = "#d7191c", "Survey Increase" = "#2c7bb6"),
    name = NULL
  ) +
  scale_y_continuous(
    name = "% of Non-EU+ Immigration",
    limits = c(0, 75),
    breaks = seq(0, 70, 10),
    sec.axis = sec_axis(~ . / 10, name = "Survey Attendance Increase (pp)", 
                        breaks = seq(0, 7, 1))
  ) +
  labs(
    title = "Immigration Age Composition Perfectly Explains Survey Age Patterns",
    subtitle = "Groups with most immigration show largest survey increases",
    x = "Age Group",
    caption = "Left axis: % of Non-EU+ immigration by age | Right axis: Bible Society survey attendance increase (pp)\nSource: ONS Table 7 (YE Dec 2024) & Bible Society surveys (2018 vs 2024)"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    plot.subtitle = element_text(size = 11, color = "#d73027"),
    legend.position = "top",
    axis.title.y.right = element_text(color = "#2c7bb6"),
    axis.title.y.left = element_text(color = "#d7191c"),
    panel.grid.major.x = element_blank()
  )

p1

Immigration age composition explains survey age patterns

Correlation analysis:

Show the code
# Calculate correlation between immigrant composition and survey increases
correlation <- cor(immigrant_age_mapped$immigrant_pct, immigrant_age_mapped$survey_increase)

Pearson correlation: 0.925

This very strong positive correlation (r = 0.93) shows that age groups with more immigration have larger survey increases. This is exactly what we’d expect if the “revival” is primarily a composition effect rather than genuine behaviour change.

Attendance by Ethnicity (2024)

Show the code
attendance_data <- read_csv(here::here("data/bible-society-uk-revival/processed/church-attendance-extracted.csv"))

# Extract 2024 attendance by ethnicity
ethnicity_2024 <- attendance_data %>%
  filter(year == 2024, question_type == "binary", 
         response_category == "Yes - in the past year") %>%
  select(response_category, white, ethnic_minority) %>%
  rename(White = white, `Ethnic Minority` = ethnic_minority)

# Reshape for plotting
ethnicity_plot_data <- ethnicity_2024 %>%
  pivot_longer(cols = c(White, `Ethnic Minority`), 
               names_to = "Ethnicity", values_to = "Percentage")

2024 Attendance by Ethnicity

The data for 2024 shows a notable difference in church attendance between White and Ethnic Minority respondents.

  • White: 23.0% attended in the past year.
  • Ethnic Minority: 24.0% attended in the past year.

This disparity is visualized in the bar chart below.

Show the code
ggplot(ethnicity_plot_data, aes(x = Ethnicity, y = Percentage, fill = Ethnicity)) +
  geom_col(alpha = 0.8, show.legend = FALSE) +
  geom_text(aes(label = sprintf("%.1f%%", Percentage)), vjust = -0.5, size = 4) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    x = "Ethnicity",
    y = "Attended in Past Year (%)",
    title = "Church Attendance by Ethnicity in 2024"
  ) +
  theme_minimal(base_size = 12)

Church attendance in the past year by ethnicity (2024)

⚠️ Note: These figures suggest different attendance patterns between ethnic groups. Population composition changes could significantly affect overall attendance rates.

Age-Stratified Analysis

Show the code
# Weekly attendance for 2018
age_2018 <- attendance_data %>%
  filter(year == 2018, response_category == "At least once a week")

# Weekly attendance for 2024 (sum of categories)
age_2024 <- attendance_data %>%
  filter(
    year == 2024,
    question_type == "frequency",
    response_category %in% c("Daily/almost daily", "A few times a week", "About once a week")
  ) %>%
  group_by(year, question_type) %>%
  summarise(
    across(c(age_18_34, age_35_54, age_55plus), sum, na.rm = TRUE),
    .groups = 'drop'
  ) %>%
  mutate(response_category = "At least once a week") # Add this for consistency

# Combine and create age_comparison
age_comparison <- bind_rows(age_2018, age_2024) %>%
  select(year, age_18_34, age_35_54, age_55plus) %>%
  rename(
    Year = year,
    `18-34` = age_18_34,
    `35-54` = age_35_54,
    `55+` = age_55plus
  )

2018

  • 18-34: 4.0%
  • 35-54: 5.0%
  • 55+: 10.0%

2024

  • 18-34: 16.0%
  • 35-54: 7.0%
  • 55+: 12.0%

Changes (2024 - 2018)

  • 18-34: +12.0 percentage points
  • 35-54: +2.0 percentage points
  • 55+: +2.0 percentage points

Visualisation: Age-Stratified Changes

Show the code
# Calculate changes in a robust way
data_2018 <- age_comparison %>%
  filter(Year == 2018) %>%
  pivot_longer(cols = -Year, names_to = "Age_Group", values_to = "y2018") %>%
  select(-Year)

data_2024 <- age_comparison %>%
  filter(Year == 2024) %>%
  pivot_longer(cols = -Year, names_to = "Age_Group", values_to = "y2024") %>%
  select(-Year)

age_changes <- full_join(data_2018, data_2024, by = "Age_Group") %>%
  mutate(
    y2018 = ifelse(is.na(y2018), 0, y2018),
    y2024 = ifelse(is.na(y2024), 0, y2024),
    change = y2024 - y2018
  ) %>%
  mutate(
    Age_Group = factor(Age_Group, levels = c("18-34", "35-54", "55+")),
    change_color = ifelse(change > 0, "darkgreen", "darkred")
  )

# Plot the changes
ggplot(age_changes, aes(x = Age_Group, y = change, fill = change_color)) +
  geom_col(alpha = 0.8) +
  geom_text(aes(label = sprintf("%+.1f", .data$change)), vjust = -0.5) +
  scale_fill_identity() +
  labs(
    x = "Age Group",
    y = "Percentage Point Change",
    title = "Change in Weekly Church Attendance (2018 to 2024)",
    subtitle = "By age group"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Change in weekly attendance by age group: 2018 vs 2024

Full Frequency Distribution (2018 vs 2024)

While the age-stratified analysis focuses on weekly attendance, looking at the full distribution of attendance frequency provides a more complete picture of how response patterns have changed between the two surveys.

The plot below compares the percentage of respondents in each attendance category for both 2018 and 2024.

Show the code
frequency_data <- attendance_data %>%
  filter(
    (year == 2018) | (year == 2024 & question_type == "frequency")
  ) %>%
  filter(response_category != "At least once a week") %>%
  mutate(
    Year = factor(year),
    response_category = factor(response_category, levels = c(
      "Daily/almost daily", "A few times a week", "About once a week",
      "About once a fortnight", "About once a month", "A few times a year",
      "About once a year", "Never"
    ))
  )
Show the code
ggplot(frequency_data, aes(x = response_category, y = total_pct, fill = Year)) +
  geom_col(position = "dodge", alpha = 0.8) +
  scale_fill_manual(values = c("2018" = "steelblue", "2024" = "darkorange")) +
  labs(
    x = "Attendance Frequency",
    y = "Percentage (%)",
    title = "Attendance Frequency Distribution: 2018 vs 2024",
    subtitle = "Comparison of all response categories"
  ) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Comparison of attendance frequency distribution (2018 vs 2024)

This visualization highlights that while weekly attendance appears to have increased, the proportion of people who “Never” attend has also increased. This contradictory finding is another red flag that suggests the survey data may not be directly comparable due to the methodological changes discussed previously.

The Demographic Composition Problem

Critical Issue: The surveys do not appear to control for demographic composition changes.

Major population changes 2018-2024 (ONS Confirmed)

  1. Total net migration: 7274.0 million (2019-2024)
    • Non-EU+: 7711.0 million net
    • EU+: 251.0 million net (negative = net emigration)
  2. Ukrainian refugees: Significant arrivals post-February 2022
    • Likely higher Orthodox Christian attendance rates
    • Different cultural patterns
  3. Hong Kong BN(O) visa holders: >163,000 (January 2021 onwards)
    • Different religious demographics
    • May have higher church attendance rates
  4. Other visa routes: International students, skilled workers, family migration

⚠️ PROBLEM: Composition vs Behaviour Change

The fundamental confound:

If the surveys are not weighted or standardised to account for these demographic shifts, any apparent change in attendance could simply reflect:

  • Composition effect: More religious immigrants entering the population (~28% of the 4pp increase)
  • Different attendance patterns of new arrivals vs existing residents
  • NOT a ‘revival’ among existing UK residents

This is a classic ‘composition change’ vs ‘behaviour change’ confound that must be addressed to make valid comparisons.

Bible Society’s analysis fails to account for this, making their “revival” claim unsupported.

Broader Methodological Issues with the Survey

Beyond the critical failure to account for demographic change, the survey methodology used for the Bible Society report has been questioned by experts, casting further doubt on the validity of its findings. As Professor David Voas of UCL notes, the reliance on a non-probability online panel is a major weakness.

Key methodological concerns raised by external experts:

  • Non-Probability Sampling: The YouGov survey uses a self-selected, opt-in panel, not a random probability sample like the British Social Attitudes survey. This means the findings cannot be reliably generalized to the entire population, and statements about “margin of error” are misleading.

  • Unrepresentative Samples: While YouGov uses quotas for characteristics like age and sex, this does not guarantee the sample is representative in other respects, such as religiosity or social compliance. People who opt in to survey panels may differ systematically from those who do not.

  • Issues with Surveying Young Adults: Young adults are a “hard-to-reach” demographic. Those who are captured by online panels are often not representative of their age group and may be more likely to be religious or socially conservative than their peers. This could artificially inflate attendance figures for younger cohorts.

  • Inconsistent Results: Other YouGov surveys, such as the one for the British Election Study, show a decline in church attendance over a similar period, directly contradicting the Bible Society’s findings and suggesting that the results are not stable or reliable.

These methodological issues, combined with the demographic composition problem, provide strong grounds to be skeptical of the “revival” claim. The observed changes are more likely to be a result of these confounding factors and measurement artifacts than a genuine shift in religious behavior.

Implications and Conclusions

What the ONS Data Reveals

Our analysis using official ONS migration statistics demonstrates:

  1. Massive demographic shift: 12.0% of the England & Wales population arrived through net migration between surveys

  2. Immigration can explain most of the ‘revival’: Even under conservative assumptions (immigrants attend at 1.5x baseline), immigration accounts for ~28% of the 4 percentage point increase

  3. Composition change dominates: The 4pp increase is approximately 1.94 million more weekly attenders; immigration alone could contribute 0.54 million of these

What We Cannot Determine Without Proper Controls

Without demographic standardisation, we cannot determine whether:

  1. Behaviour change: Attendance genuinely increased among existing UK residents (a “revival”)
  2. Composition change: Attendance increased due to immigration (demographic shift)
  3. Mixed effect: A combination of both

Why This Matters for the “Revival” Claim

Bible Society UK’s claim of a “Quiet Revival” rests entirely on a 7% → 11% survey change. However:

  • No demographic adjustment was performed
  • No attempt to separate composition vs behaviour effects
  • The analysis ignores 6.44 million net migrants who arrived between surveys
  • Even modest assumptions about immigrant attendance patterns can fully explain the observed change

Comparison with CofE Data

Crucially, the CofE comparison analysis shows:

  • CofE attendance: -19% decline (2019-2024)
  • Catholic attendance: -21% decline (2019-2023)
  • Both denominations: Show recovery from pandemic lows, NOT revival beyond pre-pandemic levels

This suggests that among existing congregations, there is NO revival. The survey increase likely reflects: - Immigration bringing new populations with different attendance patterns - Over-reporting in surveys (see CoFE comparison for detailed analysis) - Measurement artifacts

Methodological Recommendations

Any valid claims about a “revival” must include:

  1. Demographic weighting adjustments
    • Control for immigration status
    • Control for ethnicity composition changes
    • Age standardisation
  2. Stratified analysis
    • Separate analysis for UK-born vs immigrants
    • Analysis by length of residence
    • Comparison with actual church records
  3. Triangulation with other evidence
    • Church attendance records (available, show decline)
    • Belief indicators
    • Religious behaviour beyond attendance

Conclusion

Until proper demographic controls are implemented, the demographic composition issue remains a critical red flag that undermines the “Quiet Revival” claim.

Most likely explanation: The observed 4pp increase is predominantly a composition effect from immigration, not a genuine revival among existing UK residents. This is supported by:

  1. ONS data showing 12.0% population increase from immigration
  2. Conservative calculations showing immigration could explain ~28% of the increase
  3. “Smoking gun” age evidence: 64% of Non-EU+ immigrants are aged 16-34, perfectly explaining why this age group showed the largest survey increase (+7.0pp vs +1.0pp for 35-54s)
  4. Strong correlation (r = 0.93) between immigrant age composition and survey age increases
  5. CofE and Catholic data showing declines, not revival
  6. Systematic over-reporting in surveys (3.7x in Bible Society survey vs actual church counts)

The age composition evidence is particularly damning: the unusual pattern of youngest adults showing the largest increase (which contradicts known patterns of religious revival) is exactly what we’d expect from a composition effect driven by young immigrant populations.