library(tidyverse)
pkgload::load_all()
knitr::opts_chunk$set(max.print = 1000)

summarize_ungroup <- function(.data, ...) {
  summarize(.data, ..., .groups = "drop")
}

Arguments

# From analyze.R: ignore trivial area changes
IGNORE_MUTATIONS <- c(3293, 3432)

# From analyze.R:
# - nontrivial mutation happened in 2004
# - for now we're ignoring all years of 2004 and before
# - 2005 is fine as a baseline
# - we use only abolitions from 2005 or later
START_DATE <- as.Date("2005-01-01")

# Smaller working set, use NULL for all
#CANTON <- "LU"
CANTON <- NULL

Preparation

Base set:

mutations <-
  swc_get_mutations(canton = CANTON) %>%
  filter(mAbolitionDate >= !!START_DATE) %>%
  filter(!(mMutationNumber %in% !!IGNORE_MUTATIONS))

Sanity check: Each mutations maps only to one target commune.

mutations_check <-
  mutations %>%
  distinct(mMutationNumber, mId.y, mShortName.y) %>%
  add_count(mMutationNumber) %>%
  filter(n > 1)

stopifnot(nrow(mutations_check) == 0)


mutations_diff <-
  mutations %>%
  filter(mId.x != mId.y | mShortName.x != mShortName.y)

Summarize mutations:

mutations_diff %>%
  count(mAdmissionMode, mAbolitionMode, mMutationNumber) %>%
  select(-n) %>%
  count(mAbolitionMode, mAdmissionMode)
mutations_diff %>%
  count(mAbolitionMode, mAdmissionMode, mMutationNumber, name = "num_changed_municipalities") %>%
  count(mAbolitionMode, mAdmissionMode, num_changed_municipalities)

Restrict columns, aggregate by year:

mutations_base <-
  mutations_diff %>%
  select(mAdmissionDate, mMutationNumber, mId.x, mShortName.x, mId.y, mShortName.y) %>%
  mutate(year = as.integer(lubridate::year(mAdmissionDate))) %>%
  select(-mAdmissionDate, -mMutationNumber)

mutations_base
nested_mutations <-
  mutations_base %>%
  nest(x = -year)

Prototype for “no mutations”:

mutation_init <-
  mutations_base[0, ] %>%
  select(-year)

All target years:

target_years <- seq2(min(nested_mutations$year - 1L), max(nested_mutations$year))

Processing

All valid year pairs:

year_pairs <-
  crossing(target_year = target_years, year = target_years) %>%
  filter(year < target_year)

year_pairs

The entire mapping table for all years is computed in this pipe. It uses accumulate_mappings() and compact_mapping(), described below.

mapping_per_target_year <-
  year_pairs %>%
  left_join(nested_mutations, by = "year") %>%
  group_by(target_year) %>%
  summarize_ungroup(flat = list(
    accumulate_mappings(c(year, target_year[[1]]), x, mutation_init)
  )) %>%
  mutate(compact = map(flat, compact_mapping))

accumulate_mappings() does the heavy lifting, using purrr::accumulate() in backward direction. For e.g. target year 2007, this returns, in an efficient manner:

accumulate_mappings
function(year, mappings, mapping_init) {
  data <- accumulate(
    mappings, combine_mapping,
    .init = mapping_init,
    .dir = "backward"
  )

  tibble(year, data) %>%
    unnest(data)
}
<environment: namespace:SwissCommunes>

The above function calls combine_mapping(), the purpose of this function is to combine two mapping tables via full_join() and coalesce(). We no longer need sparse matrices:

combine_mapping
function(earlier, later) {
  if (is.null(earlier)) {
    return(later)
  }
  if (is.null(later)) {
    return(earlier)
  }

  earlier_join <-
    earlier %>%
    rename(
      mId = mId.y,
      mShortName = mShortName.y
    )

  later_join <-
    later %>%
    rename(
      mId = mId.x,
      mShortName = mShortName.x
    )

  both <- full_join(earlier_join, later_join, by = c("mId", "mShortName"), suffix = c("", ""))

  out <-
    both %>%
    transmute(
      mId.x = coalesce(mId.x, mId),
      mId.y = coalesce(mId.y, mId),
      mShortName.x = coalesce(mShortName.x, mShortName),
      mShortName.y = coalesce(mShortName.y, mShortName)
    ) %>%
    arrange(mId.x, mId.y)

  dm::check_key(out, mId.x)
  dm::check_key(out, mShortName.x)

  out
}
<bytecode: 0x55c23cf22940>
<environment: namespace:SwissCommunes>

For compaction, the compact_mapping() function splits sequences of years into year_from and year_to pairs.

compact_mapping
function(flat) {
  compact_check <-
    flat %>%
    group_by(mId.x, mShortName.x, mId.y, mShortName.y) %>%
    summarize(is_compact = all(diff(year) == 1)) %>%
    ungroup()

  stopifnot(compact_check$is_compact)

  out <-
    flat %>%
    group_by(mId.x, mShortName.x, mId.y, mShortName.y) %>%
    summarize(year_from = min_safe(year), year_to = max_safe(year)) %>%
    ungroup() %>%
    select(year_from, year_to, everything())

  out
}
<environment: namespace:SwissCommunes>

min_safe() and max_safe() are simple wrappers that account for the zero-length corner case.

min_safe
function(x) {
  if (length(x) == 0) x[NA_integer_] else min(x)
}
<environment: namespace:SwissCommunes>

The overall result is a nested tibble, one sub-tibble per target year with compact and explicit mappings:

mapping_per_target_year %>%
  mutate_at(vars(flat, compact), list(~ map_int(.x, nrow)))

Below are example mappings for three target years:

2006, flat

mapping_per_target_year %>%
  filter(target_year == 2006) %>%
  select(flat) %>%
  unnest(flat)

2006, compact

mapping_per_target_year %>%
  filter(target_year == 2006) %>%
  select(compact) %>%
  unnest(compact)

2014

mapping_per_target_year %>%
  filter(target_year == 2014) %>%
  select(compact) %>%
  unnest(compact)

2020

mapping_per_target_year %>%
  filter(target_year == 2020) %>%
  select(compact) %>%
  unnest(compact)

Final flat table

Unnesting the mapping table without filtering gives the final flat table:

mapping_per_target_year %>%
  select(target_year, compact) %>%
  unnest(compact)
IycgLS0tCiMnIG91dHB1dDogaHRtbF9ub3RlYm9vawojJyAtLS0KCiMtIHNldHVwLCBtZXNzYWdlID0gRkFMU0UsIHJlc3VsdHMgPSAiaGlkZSIKbGlicmFyeSh0aWR5dmVyc2UpCnBrZ2xvYWQ6OmxvYWRfYWxsKCkKa25pdHI6Om9wdHNfY2h1bmskc2V0KG1heC5wcmludCA9IDEwMDApCgpzdW1tYXJpemVfdW5ncm91cCA8LSBmdW5jdGlvbiguZGF0YSwgLi4uKSB7CiAgc3VtbWFyaXplKC5kYXRhLCAuLi4sIC5ncm91cHMgPSAiZHJvcCIpCn0KCiMnICMgQXJndW1lbnRzCgojIEZyb20gYW5hbHl6ZS5SOiBpZ25vcmUgdHJpdmlhbCBhcmVhIGNoYW5nZXMKSUdOT1JFX01VVEFUSU9OUyA8LSBjKDMyOTMsIDM0MzIpCgojIEZyb20gYW5hbHl6ZS5SOgojIC0gbm9udHJpdmlhbCBtdXRhdGlvbiBoYXBwZW5lZCBpbiAyMDA0CiMgLSBmb3Igbm93IHdlJ3JlIGlnbm9yaW5nIGFsbCB5ZWFycyBvZiAyMDA0IGFuZCBiZWZvcmUKIyAtIDIwMDUgaXMgZmluZSBhcyBhIGJhc2VsaW5lCiMgLSB3ZSB1c2Ugb25seSBhYm9saXRpb25zIGZyb20gMjAwNSBvciBsYXRlcgpTVEFSVF9EQVRFIDwtIGFzLkRhdGUoIjIwMDUtMDEtMDEiKQoKIyBTbWFsbGVyIHdvcmtpbmcgc2V0LCB1c2UgTlVMTCBmb3IgYWxsCiNDQU5UT04gPC0gIkxVIgpDQU5UT04gPC0gTlVMTAoKIycgIyBQcmVwYXJhdGlvbgojJwojJyBCYXNlIHNldDoKCm11dGF0aW9ucyA8LQogIHN3Y19nZXRfbXV0YXRpb25zKGNhbnRvbiA9IENBTlRPTikgJT4lCiAgZmlsdGVyKG1BYm9saXRpb25EYXRlID49ICEhU1RBUlRfREFURSkgJT4lCiAgZmlsdGVyKCEobU11dGF0aW9uTnVtYmVyICVpbiUgISFJR05PUkVfTVVUQVRJT05TKSkKCiMnIFNhbml0eSBjaGVjazogRWFjaCBtdXRhdGlvbnMgbWFwcyBvbmx5IHRvIG9uZSB0YXJnZXQgY29tbXVuZS4KCm11dGF0aW9uc19jaGVjayA8LQogIG11dGF0aW9ucyAlPiUKICBkaXN0aW5jdChtTXV0YXRpb25OdW1iZXIsIG1JZC55LCBtU2hvcnROYW1lLnkpICU+JQogIGFkZF9jb3VudChtTXV0YXRpb25OdW1iZXIpICU+JQogIGZpbHRlcihuID4gMSkKCnN0b3BpZm5vdChucm93KG11dGF0aW9uc19jaGVjaykgPT0gMCkKCgptdXRhdGlvbnNfZGlmZiA8LQogIG11dGF0aW9ucyAlPiUKICBmaWx0ZXIobUlkLnggIT0gbUlkLnkgfCBtU2hvcnROYW1lLnggIT0gbVNob3J0TmFtZS55KQoKIycgU3VtbWFyaXplIG11dGF0aW9uczoKCm11dGF0aW9uc19kaWZmICU+JQogIGNvdW50KG1BZG1pc3Npb25Nb2RlLCBtQWJvbGl0aW9uTW9kZSwgbU11dGF0aW9uTnVtYmVyKSAlPiUKICBzZWxlY3QoLW4pICU+JQogIGNvdW50KG1BYm9saXRpb25Nb2RlLCBtQWRtaXNzaW9uTW9kZSkKCm11dGF0aW9uc19kaWZmICU+JQogIGNvdW50KG1BYm9saXRpb25Nb2RlLCBtQWRtaXNzaW9uTW9kZSwgbU11dGF0aW9uTnVtYmVyLCBuYW1lID0gIm51bV9jaGFuZ2VkX211bmljaXBhbGl0aWVzIikgJT4lCiAgY291bnQobUFib2xpdGlvbk1vZGUsIG1BZG1pc3Npb25Nb2RlLCBudW1fY2hhbmdlZF9tdW5pY2lwYWxpdGllcykKCiMnIFJlc3RyaWN0IGNvbHVtbnMsIGFnZ3JlZ2F0ZSBieSB5ZWFyOgoKbXV0YXRpb25zX2Jhc2UgPC0KICBtdXRhdGlvbnNfZGlmZiAlPiUKICBzZWxlY3QobUFkbWlzc2lvbkRhdGUsIG1NdXRhdGlvbk51bWJlciwgbUlkLngsIG1TaG9ydE5hbWUueCwgbUlkLnksIG1TaG9ydE5hbWUueSkgJT4lCiAgbXV0YXRlKHllYXIgPSBhcy5pbnRlZ2VyKGx1YnJpZGF0ZTo6eWVhcihtQWRtaXNzaW9uRGF0ZSkpKSAlPiUKICBzZWxlY3QoLW1BZG1pc3Npb25EYXRlLCAtbU11dGF0aW9uTnVtYmVyKQoKbXV0YXRpb25zX2Jhc2UKCm5lc3RlZF9tdXRhdGlvbnMgPC0KICBtdXRhdGlvbnNfYmFzZSAlPiUKICBuZXN0KHggPSAteWVhcikKCiMnIFByb3RvdHlwZSBmb3IgIm5vIG11dGF0aW9ucyI6CgptdXRhdGlvbl9pbml0IDwtCiAgbXV0YXRpb25zX2Jhc2VbMCwgXSAlPiUKICBzZWxlY3QoLXllYXIpCgojJyBBbGwgdGFyZ2V0IHllYXJzOgoKdGFyZ2V0X3llYXJzIDwtIHNlcTIobWluKG5lc3RlZF9tdXRhdGlvbnMkeWVhciAtIDFMKSwgbWF4KG5lc3RlZF9tdXRhdGlvbnMkeWVhcikpCgojJyAjIFByb2Nlc3NpbmcKIycKIycgQWxsIHZhbGlkIHllYXIgcGFpcnM6Cgp5ZWFyX3BhaXJzIDwtCiAgY3Jvc3NpbmcodGFyZ2V0X3llYXIgPSB0YXJnZXRfeWVhcnMsIHllYXIgPSB0YXJnZXRfeWVhcnMpICU+JQogIGZpbHRlcih5ZWFyIDwgdGFyZ2V0X3llYXIpCgp5ZWFyX3BhaXJzCgojJyBUaGUgZW50aXJlIG1hcHBpbmcgdGFibGUgZm9yIGFsbCB5ZWFycyBpcyBjb21wdXRlZCBpbiB0aGlzIHBpcGUuCiMnIEl0IHVzZXMgYGFjY3VtdWxhdGVfbWFwcGluZ3MoKWAgYW5kIGBjb21wYWN0X21hcHBpbmcoKWAsIGRlc2NyaWJlZCBiZWxvdy4KCm1hcHBpbmdfcGVyX3RhcmdldF95ZWFyIDwtCiAgeWVhcl9wYWlycyAlPiUKICBsZWZ0X2pvaW4obmVzdGVkX211dGF0aW9ucywgYnkgPSAieWVhciIpICU+JQogIGdyb3VwX2J5KHRhcmdldF95ZWFyKSAlPiUKICBzdW1tYXJpemVfdW5ncm91cChmbGF0ID0gbGlzdCgKICAgIGFjY3VtdWxhdGVfbWFwcGluZ3MoYyh5ZWFyLCB0YXJnZXRfeWVhcltbMV1dKSwgeCwgbXV0YXRpb25faW5pdCkKICApKSAlPiUKICBtdXRhdGUoY29tcGFjdCA9IG1hcChmbGF0LCBjb21wYWN0X21hcHBpbmcpKQoKIycgYGFjY3VtdWxhdGVfbWFwcGluZ3MoKWAgZG9lcyB0aGUgaGVhdnkgbGlmdGluZywgdXNpbmcgYHB1cnJyOjphY2N1bXVsYXRlKClgCiMnIGluIGJhY2t3YXJkIGRpcmVjdGlvbi4KIycgRm9yIGUuZy4gdGFyZ2V0IHllYXIgMjAwNywgdGhpcyByZXR1cm5zLCBpbiBhbiBlZmZpY2llbnQgbWFubmVyOgojJwojJyAtIDIwMDUrMjAwNisyMDA3CiMnIC0gMjAwNisyMDA3CiMnIC0gMjAwNwojJyAtIGVtcHR5IHNldAoKYWNjdW11bGF0ZV9tYXBwaW5ncwoKIycgVGhlIGFib3ZlIGZ1bmN0aW9uIGNhbGxzIGBjb21iaW5lX21hcHBpbmcoKWAsIHRoZSBwdXJwb3NlIG9mIHRoaXMgZnVuY3Rpb24KIycgaXMgdG8gY29tYmluZSB0d28gbWFwcGluZyB0YWJsZXMgdmlhIGBmdWxsX2pvaW4oKWAgYW5kIGBjb2FsZXNjZSgpYC4KIycgV2Ugbm8gbG9uZ2VyIG5lZWQgc3BhcnNlIG1hdHJpY2VzOgoKY29tYmluZV9tYXBwaW5nCgojJyBGb3IgY29tcGFjdGlvbiwgdGhlIGBjb21wYWN0X21hcHBpbmcoKWAgZnVuY3Rpb24gc3BsaXRzIHNlcXVlbmNlcyBvZiB5ZWFycwojJyBpbnRvIGB5ZWFyX2Zyb21gIGFuZCBgeWVhcl90b2AgcGFpcnMuCgpjb21wYWN0X21hcHBpbmcKCiMnIGBtaW5fc2FmZSgpYCBhbmQgYG1heF9zYWZlKClgIGFyZSBzaW1wbGUgd3JhcHBlcnMgdGhhdCBhY2NvdW50IGZvcgojJyB0aGUgemVyby1sZW5ndGggY29ybmVyIGNhc2UuCgptaW5fc2FmZQoKIycgVGhlIG92ZXJhbGwgcmVzdWx0IGlzIGEgbmVzdGVkIHRpYmJsZSwgb25lIHN1Yi10aWJibGUgcGVyIHRhcmdldCB5ZWFyCiMnIHdpdGggY29tcGFjdCBhbmQgZXhwbGljaXQgbWFwcGluZ3M6CgptYXBwaW5nX3Blcl90YXJnZXRfeWVhciAlPiUKICBtdXRhdGVfYXQodmFycyhmbGF0LCBjb21wYWN0KSwgbGlzdCh+IG1hcF9pbnQoLngsIG5yb3cpKSkKCiMnIEJlbG93IGFyZSBleGFtcGxlIG1hcHBpbmdzIGZvciB0aHJlZSB0YXJnZXQgeWVhcnM6CiMnCiMnICMjIDIwMDYsIGZsYXQKCm1hcHBpbmdfcGVyX3RhcmdldF95ZWFyICU+JQogIGZpbHRlcih0YXJnZXRfeWVhciA9PSAyMDA2KSAlPiUKICBzZWxlY3QoZmxhdCkgJT4lCiAgdW5uZXN0KGZsYXQpCgojJwojJyAjIyAyMDA2LCBjb21wYWN0CgptYXBwaW5nX3Blcl90YXJnZXRfeWVhciAlPiUKICBmaWx0ZXIodGFyZ2V0X3llYXIgPT0gMjAwNikgJT4lCiAgc2VsZWN0KGNvbXBhY3QpICU+JQogIHVubmVzdChjb21wYWN0KQoKIycgIyMgMjAxNAoKbWFwcGluZ19wZXJfdGFyZ2V0X3llYXIgJT4lCiAgZmlsdGVyKHRhcmdldF95ZWFyID09IDIwMTQpICU+JQogIHNlbGVjdChjb21wYWN0KSAlPiUKICB1bm5lc3QoY29tcGFjdCkKCiMnICMjIDIwMjAKCm1hcHBpbmdfcGVyX3RhcmdldF95ZWFyICU+JQogIGZpbHRlcih0YXJnZXRfeWVhciA9PSAyMDIwKSAlPiUKICBzZWxlY3QoY29tcGFjdCkgJT4lCiAgdW5uZXN0KGNvbXBhY3QpCgojJyAjIEZpbmFsIGZsYXQgdGFibGUKIycKIycgVW5uZXN0aW5nIHRoZSBtYXBwaW5nIHRhYmxlIHdpdGhvdXQgZmlsdGVyaW5nIGdpdmVzIHRoZSBmaW5hbCBmbGF0IHRhYmxlOgoKbWFwcGluZ19wZXJfdGFyZ2V0X3llYXIgJT4lCiAgc2VsZWN0KHRhcmdldF95ZWFyLCBjb21wYWN0KSAlPiUKICB1bm5lc3QoY29tcGFjdCkK