Introduction

This notebook documents the data transformation and processing stage of the housing affordability analysis pipeline. Its purpose is to convert raw, heterogeneous source data into a clean, consistent, and analysis-ready format suitable for exploratory analysis and modeling.

The transformation process begins with the raw datasets generated during the ingestion stage, including Zillow Home Value Index (ZHVI) time series, macroeconomic indicators from the Federal Reserve Economic Data (FRED), and median household income estimates from the American Community Survey (ACS). These sources differ in temporal resolution, geographic definitions, and data structure, requiring systematic alignment and standardization before analysis.

Key transformation steps performed in this notebook include reshaping wide time-series data into tidy long formats, harmonizing date conventions across sources, standardizing metropolitan region identifiers, adjusting prices for inflation, and reconciling annual income data with monthly housing and financial indicators. Where necessary, transparent imputation rules are applied to ensure temporal continuity while preserving the underlying structure of the data.

This notebook is deliberately procedural rather than analytical. No exploratory data analysis or modeling is conducted here. Instead, each step focuses on producing well-defined intermediate datasets and a final processed panel that serves as the sole input to downstream analysis. Validation checks are included to confirm expected coverage, missingness, and variable ranges prior to saving outputs.

All processed datasets produced by this notebook are written to the data/processed/ directory and are tracked in version control. By separating ingestion, transformation, and analysis into distinct stages, the pipeline improves reproducibility, clarity, and maintainability, allowing analytical results to be traced back to their original data sources with minimal ambiguity.

Load Datasets and Frequency Harmonization

library(tidyverse)
library(lubridate)

dir.create("data/processed", recursive = TRUE, showWarnings = FALSE)

#---Mortgage rate: Week -> monthly average ---

mortgage_m <- read_csv("./data/raw/fred_MORTGAGE30US_20000101.csv") %>%
  mutate(date = floor_date(date,"month")) %>%
  group_by(date) %>%
  summarise(mortgage_30y = mean(MORTGAGE30US, NA.RM = TRUE), .groups = "drop")

# --- CPI already monthly ---

cpi_m <- read_csv("./data/raw/fred_CPIAUCSL_20000101.csv") %>%
  rename(cpi = CPIAUCSL)

#---Save ---
write_csv(mortgage_m, "./data/processed/mortgage_monthly.csv")
write_csv(cpi_m, "./data/processed/cpi_monthly.csv")

Income -> Monthly Panel

library(tidyverse)
library(lubridate)

# Load annual income (2010–2023)
income_annual <- read_csv(
  "./data/raw/acs5_median_hh_income_metro_2010_2023.csv",
  show_col_types = FALSE
)

# Load zhvi to get the monthly date span
zhvi <- read_csv(
  "./data/processed/zhvvi_metro_monthly.csv",
  show_col_types = FALSE
)

# Define the global monthly date sequence (month-start)
date_seq <- seq.Date(
  from = min(zhvi$date),
  to   = max(zhvi$date),
  by   = "month"
)

# Normalizer so Census & Zillow names can match later
normalize_region <- function(x) {
  x %>%
    stringr::str_remove(" Metropolitan Statistical Area$") %>%
    stringr::str_remove(" Metro Area$") %>%
    stringr::str_remove(" Micro Area$") %>%
    stringr::str_trim()
}

# Convert annual -> monthly grid, fill down
income_monthly <- income_annual %>%
  mutate(
    region_norm = normalize_region(region),
    date = ymd(paste0(year, "-01-01"))
  ) %>%
  select(region_norm, date, income_median_hh) %>%
  group_by(region_norm) %>%
  tidyr::complete(date = date_seq) %>%
  tidyr::fill(income_median_hh, .direction = "down") %>%
  ungroup() %>%
  arrange(region_norm, date)

# Verify
income_monthly %>%
  summarise(
    n_regions = n_distinct(region_norm),
    start_date = min(date),
    end_date = max(date),
    pct_missing = mean(is.na(income_median_hh))
  ) %>%
  print()
# A tibble: 1 × 4
  n_regions start_date end_date   pct_missing
      <int> <date>     <date>           <dbl>
1      1218 2000-01-01 2025-12-01       0.444
# Save
#dir.create("./data/processed", recursive = TRUE, showWarnings = FALSE)
readr::write_csv(income_monthly, "./data/processed/income_metro_monthly.csv")

Zillow Data processing

library(tidyverse)
library(lubridate)

zhvi_raw <- read_csv("./data/raw/Metro_zhvi_uc_sfrcondo_tier_0.33_0.67_sm_sa_month.csv")

#Columns are RegionID, SizeRank, RegionName, RegionType, StateName
date_cols <- names(zhvi_raw) %>%
  str_subset("^\\d{4}-\\d{2}-\\d{2}$")
#print(date_cols)
stopifnot(length(date_cols) > 0)

#---Wide -> Long ---

zhvi_metro <- zhvi_raw %>%
  pivot_longer(
    cols      =  all_of(date_cols),
    names_to  =  "date",
    values_to =  "zhvi",
    values_drop_na = TRUE
  ) %>%
  mutate(
    date = ymd(date),
    region = str_trim(RegionName)
  ) %>%
  select(
    region,
    date,
    zhvi
  ) %>%
  arrange(region, date)
#head(zhvi_metro,100)
#---Sanity checks ---
stopifnot(all(!is.na(zhvi_metro$region)))
stopifnot(all(!is.na(zhvi_metro$date)))
stopifnot(all(!is.na(zhvi_metro$zhvi)))

zhvi_metro <- zhvi_metro %>%
  mutate(date = floor_date(date, "month"))

zhvi_metro %>%
  summarise(
    n_regions = n_distinct(region),
    start_date = min(date),
    end_date = max(date)
  ) %>%
  print()
# A tibble: 1 × 3
  n_regions start_date end_date  
      <int> <date>     <date>    
1       895 2000-01-01 2025-12-01
write_csv(
  zhvi_metro, "./data/processed/zhvvi_metro_monthly.csv"
)

CSUSHPISA dataset

cs_m <- readr::read_csv("./data/raw/fred_CSUSHPISA_20000101.csv") %>%
  mutate(date = ymd(date)) %>%
  rename(case_shiller_us = CSUSHPISA) %>%
  arrange(date)

readr::write_csv(cs_m, "./data/processed/case_shiller_us_monthly.csv")

Panel Construction

library(tidyverse)
library(lubridate)

# --- Load processed datasets ---

zhvi <- read_csv("./data/processed/zhvvi_metro_monthly.csv", show_col_types = FALSE)
mortgage <- read_csv("./data/processed/mortgage_monthly.csv", show_col_types = FALSE)
cpi <- read_csv("./data/processed/cpi_monthly.csv", show_col_types = FALSE)
income <- read_csv("./data/processed/income_metro_annual.csv", show_col_types = FALSE)

#---Define global monthly date sequence --
date_seq <- seq.Date(
  from = min(zhvi$date),
  to = max(zhvi$date),
  by = "month"
)

#--- Expand annual income to monthly (forward-fill) ---

income_m <- income %>%
  mutate(date = floor_date(date, "year")) %>%
  group_by(region) %>%
  tidyr::complete(date = date_seq) %>%
  fill(income_median_hh, .direction = "updown") %>%
  ungroup()


#---Build unified metro-month panel ---

housing_panel <- zhvi %>%
  left_join(mortgage, by = "date") %>%
  left_join(cpi, by = "date") %>%
  left_join(income_m, by = c("region", "date"))

#---checks ---

housing_panel %>%
  summarise(
    n_regions = n_distinct(region),
    start_date = min(date),
    end_date = max(date),
    pct_missing_zhvi = mean(is.na(zhvi)),
    pct_missing_income = mean(is.na(income_median_hh))
  ) %>%
  print()
# A tibble: 1 × 5
  n_regions start_date end_date   pct_missing_zhvi pct_missing_income
      <int> <date>     <date>                <dbl>              <dbl>
1       895 2000-01-01 2025-12-01                0                  1
write_csv(
  housing_panel,
  "./data/processed/housing_panel_metro_monthly.csv"
)

Panel Integrity Check

housing_panel %>%
  summarise(
    n_regions = n_distinct(region),
    n_rows = n(),
    start_date = min(date),
    end_date = max(date),
    pct_missing_zhvi = mean(is.na(zhvi)),
    pct_missing_income = mean(is.na(income_median_hh)),
    pct_missing_mortgage = mean(is.na(mortgage_30y)),
    pct_missing_cpi = mean(is.na(cpi))
  ) %>%
  print()
# A tibble: 1 × 8
  n_regions n_rows start_date end_date   pct_missing_zhvi pct_missing_income pct_missing_mortgage pct_missing_cpi
      <int>  <int> <date>     <date>                <dbl>              <dbl>                <dbl>           <dbl>
1       895 229974 2000-01-01 2025-12-01                0                  1                    0         0.00389

Normalization function for region

normalize_region <- function(x){
  x %>%
    stringr::str_remove(" Metropolitain Statistical Area$") %>%
    stringr::str_remove(" Metro Area$") %>%
    stringr::str_remove(" Micro Area$") %>%
    stringr::str_trim()
}

Apply normalization

zhvi <- zhvi %>%
  mutate(region_norm = normalize_region(region))

income_m <- income_m %>%
  mutate(region_norm = normalize_region(region))

Rebuild the panel

housing_panel <- zhvi %>%
  left_join(mortgage, by = "date") %>%
  left_join(cpi, by = "date") %>%
  left_join(
    income_m,
    by = c("region_norm" = "region_norm", "date"="date")
  )

Check

housing_panel %>%
  summarise(
    pct_missing_income = mean(is.na(income_median_hh))
  ) %>% print()
# A tibble: 1 × 1
  pct_missing_income
               <dbl>
1              0.207
head(housing_panel, 200)

Save the full panel

write_csv(
  housing_panel,
  "./data/processed/housing_panel_metro_monthly.csv"
)

Filter to metros with income coverage

housing_panel_model <- housing_panel %>%
  group_by(region_norm) %>%
  filter(any(!is.na(income_median_hh))) %>%
  ungroup()

Recheck

housing_panel_model %>%
  summarise(pct_missing_income = mean(is.na(income_median_hh)))

Approximately 20% of Zillow-defined metro areas do not have corresponding ACS income estimates due to geographic definition differences. I restrict income-based analyses to metros with observed ACS coverage.

Save model ready panel

readr::write_csv(
  housing_panel_model,
  "./data/processed/housing_panel_model_metro_monthly.csv"
)

message("Saved: data/processed/housing_panel_model_metro_monthly.csv")
LS0tDQp0aXRsZTogIlIgTm90ZWJvb2siDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQojIEludHJvZHVjdGlvbg0KDQpUaGlzIG5vdGVib29rIGRvY3VtZW50cyB0aGUgKipkYXRhIHRyYW5zZm9ybWF0aW9uIGFuZCBwcm9jZXNzaW5nIHN0YWdlKiogb2YgdGhlIGhvdXNpbmcgYWZmb3JkYWJpbGl0eSBhbmFseXNpcyBwaXBlbGluZS4gSXRzIHB1cnBvc2UgaXMgdG8gY29udmVydCByYXcsIGhldGVyb2dlbmVvdXMgc291cmNlIGRhdGEgaW50byBhIGNsZWFuLCBjb25zaXN0ZW50LCBhbmQgYW5hbHlzaXMtcmVhZHkgZm9ybWF0IHN1aXRhYmxlIGZvciBleHBsb3JhdG9yeSBhbmFseXNpcyBhbmQgbW9kZWxpbmcuDQoNClRoZSB0cmFuc2Zvcm1hdGlvbiBwcm9jZXNzIGJlZ2lucyB3aXRoIHRoZSByYXcgZGF0YXNldHMgZ2VuZXJhdGVkIGR1cmluZyB0aGUgaW5nZXN0aW9uIHN0YWdlLCBpbmNsdWRpbmcgWmlsbG93IEhvbWUgVmFsdWUgSW5kZXggKFpIVkkpIHRpbWUgc2VyaWVzLCBtYWNyb2Vjb25vbWljIGluZGljYXRvcnMgZnJvbSB0aGUgRmVkZXJhbCBSZXNlcnZlIEVjb25vbWljIERhdGEgKEZSRUQpLCBhbmQgbWVkaWFuIGhvdXNlaG9sZCBpbmNvbWUgZXN0aW1hdGVzIGZyb20gdGhlIEFtZXJpY2FuIENvbW11bml0eSBTdXJ2ZXkgKEFDUykuIFRoZXNlIHNvdXJjZXMgZGlmZmVyIGluIHRlbXBvcmFsIHJlc29sdXRpb24sIGdlb2dyYXBoaWMgZGVmaW5pdGlvbnMsIGFuZCBkYXRhIHN0cnVjdHVyZSwgcmVxdWlyaW5nIHN5c3RlbWF0aWMgYWxpZ25tZW50IGFuZCBzdGFuZGFyZGl6YXRpb24gYmVmb3JlIGFuYWx5c2lzLg0KDQpLZXkgdHJhbnNmb3JtYXRpb24gc3RlcHMgcGVyZm9ybWVkIGluIHRoaXMgbm90ZWJvb2sgaW5jbHVkZSByZXNoYXBpbmcgd2lkZSB0aW1lLXNlcmllcyBkYXRhIGludG8gdGlkeSBsb25nIGZvcm1hdHMsIGhhcm1vbml6aW5nIGRhdGUgY29udmVudGlvbnMgYWNyb3NzIHNvdXJjZXMsIHN0YW5kYXJkaXppbmcgbWV0cm9wb2xpdGFuIHJlZ2lvbiBpZGVudGlmaWVycywgYWRqdXN0aW5nIHByaWNlcyBmb3IgaW5mbGF0aW9uLCBhbmQgcmVjb25jaWxpbmcgYW5udWFsIGluY29tZSBkYXRhIHdpdGggbW9udGhseSBob3VzaW5nIGFuZCBmaW5hbmNpYWwgaW5kaWNhdG9ycy4gV2hlcmUgbmVjZXNzYXJ5LCB0cmFuc3BhcmVudCBpbXB1dGF0aW9uIHJ1bGVzIGFyZSBhcHBsaWVkIHRvIGVuc3VyZSB0ZW1wb3JhbCBjb250aW51aXR5IHdoaWxlIHByZXNlcnZpbmcgdGhlIHVuZGVybHlpbmcgc3RydWN0dXJlIG9mIHRoZSBkYXRhLg0KDQpUaGlzIG5vdGVib29rIGlzIGRlbGliZXJhdGVseSAqKnByb2NlZHVyYWwgcmF0aGVyIHRoYW4gYW5hbHl0aWNhbCoqLiBObyBleHBsb3JhdG9yeSBkYXRhIGFuYWx5c2lzIG9yIG1vZGVsaW5nIGlzIGNvbmR1Y3RlZCBoZXJlLiBJbnN0ZWFkLCBlYWNoIHN0ZXAgZm9jdXNlcyBvbiBwcm9kdWNpbmcgd2VsbC1kZWZpbmVkIGludGVybWVkaWF0ZSBkYXRhc2V0cyBhbmQgYSBmaW5hbCBwcm9jZXNzZWQgcGFuZWwgdGhhdCBzZXJ2ZXMgYXMgdGhlIHNvbGUgaW5wdXQgdG8gZG93bnN0cmVhbSBhbmFseXNpcy4gVmFsaWRhdGlvbiBjaGVja3MgYXJlIGluY2x1ZGVkIHRvIGNvbmZpcm0gZXhwZWN0ZWQgY292ZXJhZ2UsIG1pc3NpbmduZXNzLCBhbmQgdmFyaWFibGUgcmFuZ2VzIHByaW9yIHRvIHNhdmluZyBvdXRwdXRzLg0KDQpBbGwgcHJvY2Vzc2VkIGRhdGFzZXRzIHByb2R1Y2VkIGJ5IHRoaXMgbm90ZWJvb2sgYXJlIHdyaXR0ZW4gdG8gdGhlIGBkYXRhL3Byb2Nlc3NlZC9gIGRpcmVjdG9yeSBhbmQgYXJlIHRyYWNrZWQgaW4gdmVyc2lvbiBjb250cm9sLiBCeSBzZXBhcmF0aW5nIGluZ2VzdGlvbiwgdHJhbnNmb3JtYXRpb24sIGFuZCBhbmFseXNpcyBpbnRvIGRpc3RpbmN0IHN0YWdlcywgdGhlIHBpcGVsaW5lIGltcHJvdmVzIHJlcHJvZHVjaWJpbGl0eSwgY2xhcml0eSwgYW5kIG1haW50YWluYWJpbGl0eSwgYWxsb3dpbmcgYW5hbHl0aWNhbCByZXN1bHRzIHRvIGJlIHRyYWNlZCBiYWNrIHRvIHRoZWlyIG9yaWdpbmFsIGRhdGEgc291cmNlcyB3aXRoIG1pbmltYWwgYW1iaWd1aXR5Lg0KDQojIyBMb2FkIERhdGFzZXRzIGFuZCBGcmVxdWVuY3kgSGFybW9uaXphdGlvbg0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeShsdWJyaWRhdGUpDQoNCmRpci5jcmVhdGUoImRhdGEvcHJvY2Vzc2VkIiwgcmVjdXJzaXZlID0gVFJVRSwgc2hvd1dhcm5pbmdzID0gRkFMU0UpDQoNCiMtLS1Nb3J0Z2FnZSByYXRlOiBXZWVrIC0+IG1vbnRobHkgYXZlcmFnZSAtLS0NCg0KbW9ydGdhZ2VfbSA8LSByZWFkX2NzdigiLi9kYXRhL3Jhdy9mcmVkX01PUlRHQUdFMzBVU18yMDAwMDEwMS5jc3YiKSAlPiUNCiAgbXV0YXRlKGRhdGUgPSBmbG9vcl9kYXRlKGRhdGUsIm1vbnRoIikpICU+JQ0KICBncm91cF9ieShkYXRlKSAlPiUNCiAgc3VtbWFyaXNlKG1vcnRnYWdlXzMweSA9IG1lYW4oTU9SVEdBR0UzMFVTLCBOQS5STSA9IFRSVUUpLCAuZ3JvdXBzID0gImRyb3AiKQ0KDQojIC0tLSBDUEkgYWxyZWFkeSBtb250aGx5IC0tLQ0KDQpjcGlfbSA8LSByZWFkX2NzdigiLi9kYXRhL3Jhdy9mcmVkX0NQSUFVQ1NMXzIwMDAwMTAxLmNzdiIpICU+JQ0KICByZW5hbWUoY3BpID0gQ1BJQVVDU0wpDQoNCiMtLS1TYXZlIC0tLQ0Kd3JpdGVfY3N2KG1vcnRnYWdlX20sICIuL2RhdGEvcHJvY2Vzc2VkL21vcnRnYWdlX21vbnRobHkuY3N2IikNCndyaXRlX2NzdihjcGlfbSwgIi4vZGF0YS9wcm9jZXNzZWQvY3BpX21vbnRobHkuY3N2IikNCmBgYA0KDQojIyBJbmNvbWUgLVw+IE1vbnRobHkgUGFuZWwNCg0KYGBge3J9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkobHVicmlkYXRlKQ0KDQojIExvYWQgYW5udWFsIGluY29tZSAoMjAxMOKAkzIwMjMpDQppbmNvbWVfYW5udWFsIDwtIHJlYWRfY3N2KA0KICAiLi9kYXRhL3Jhdy9hY3M1X21lZGlhbl9oaF9pbmNvbWVfbWV0cm9fMjAxMF8yMDIzLmNzdiIsDQogIHNob3dfY29sX3R5cGVzID0gRkFMU0UNCikNCg0KIyBMb2FkIHpodmkgdG8gZ2V0IHRoZSBtb250aGx5IGRhdGUgc3Bhbg0Kemh2aSA8LSByZWFkX2NzdigNCiAgIi4vZGF0YS9wcm9jZXNzZWQvemh2dmlfbWV0cm9fbW9udGhseS5jc3YiLA0KICBzaG93X2NvbF90eXBlcyA9IEZBTFNFDQopDQoNCiMgRGVmaW5lIHRoZSBnbG9iYWwgbW9udGhseSBkYXRlIHNlcXVlbmNlIChtb250aC1zdGFydCkNCmRhdGVfc2VxIDwtIHNlcS5EYXRlKA0KICBmcm9tID0gbWluKHpodmkkZGF0ZSksDQogIHRvICAgPSBtYXgoemh2aSRkYXRlKSwNCiAgYnkgICA9ICJtb250aCINCikNCg0KIyBOb3JtYWxpemVyIHNvIENlbnN1cyAmIFppbGxvdyBuYW1lcyBjYW4gbWF0Y2ggbGF0ZXINCm5vcm1hbGl6ZV9yZWdpb24gPC0gZnVuY3Rpb24oeCkgew0KICB4ICU+JQ0KICAgIHN0cmluZ3I6OnN0cl9yZW1vdmUoIiBNZXRyb3BvbGl0YW4gU3RhdGlzdGljYWwgQXJlYSQiKSAlPiUNCiAgICBzdHJpbmdyOjpzdHJfcmVtb3ZlKCIgTWV0cm8gQXJlYSQiKSAlPiUNCiAgICBzdHJpbmdyOjpzdHJfcmVtb3ZlKCIgTWljcm8gQXJlYSQiKSAlPiUNCiAgICBzdHJpbmdyOjpzdHJfdHJpbSgpDQp9DQoNCiMgQ29udmVydCBhbm51YWwgLT4gbW9udGhseSBncmlkLCBmaWxsIGRvd24NCmluY29tZV9tb250aGx5IDwtIGluY29tZV9hbm51YWwgJT4lDQogIG11dGF0ZSgNCiAgICByZWdpb25fbm9ybSA9IG5vcm1hbGl6ZV9yZWdpb24ocmVnaW9uKSwNCiAgICBkYXRlID0geW1kKHBhc3RlMCh5ZWFyLCAiLTAxLTAxIikpDQogICkgJT4lDQogIHNlbGVjdChyZWdpb25fbm9ybSwgZGF0ZSwgaW5jb21lX21lZGlhbl9oaCkgJT4lDQogIGdyb3VwX2J5KHJlZ2lvbl9ub3JtKSAlPiUNCiAgdGlkeXI6OmNvbXBsZXRlKGRhdGUgPSBkYXRlX3NlcSkgJT4lDQogIHRpZHlyOjpmaWxsKGluY29tZV9tZWRpYW5faGgsIC5kaXJlY3Rpb24gPSAiZG93biIpICU+JQ0KICB1bmdyb3VwKCkgJT4lDQogIGFycmFuZ2UocmVnaW9uX25vcm0sIGRhdGUpDQoNCiMgVmVyaWZ5DQppbmNvbWVfbW9udGhseSAlPiUNCiAgc3VtbWFyaXNlKA0KICAgIG5fcmVnaW9ucyA9IG5fZGlzdGluY3QocmVnaW9uX25vcm0pLA0KICAgIHN0YXJ0X2RhdGUgPSBtaW4oZGF0ZSksDQogICAgZW5kX2RhdGUgPSBtYXgoZGF0ZSksDQogICAgcGN0X21pc3NpbmcgPSBtZWFuKGlzLm5hKGluY29tZV9tZWRpYW5faGgpKQ0KICApICU+JQ0KICBwcmludCgpDQoNCiMgU2F2ZQ0KI2Rpci5jcmVhdGUoIi4vZGF0YS9wcm9jZXNzZWQiLCByZWN1cnNpdmUgPSBUUlVFLCBzaG93V2FybmluZ3MgPSBGQUxTRSkNCnJlYWRyOjp3cml0ZV9jc3YoaW5jb21lX21vbnRobHksICIuL2RhdGEvcHJvY2Vzc2VkL2luY29tZV9tZXRyb19tb250aGx5LmNzdiIpDQoNCg0KYGBgDQoNCiMjIFppbGxvdyBEYXRhIHByb2Nlc3NpbmcNCg0KYGBge3J9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkobHVicmlkYXRlKQ0KDQp6aHZpX3JhdyA8LSByZWFkX2NzdigiLi9kYXRhL3Jhdy9NZXRyb196aHZpX3VjX3NmcmNvbmRvX3RpZXJfMC4zM18wLjY3X3NtX3NhX21vbnRoLmNzdiIpDQoNCiNDb2x1bW5zIGFyZSBSZWdpb25JRCwgU2l6ZVJhbmssIFJlZ2lvbk5hbWUsIFJlZ2lvblR5cGUsIFN0YXRlTmFtZQ0KZGF0ZV9jb2xzIDwtIG5hbWVzKHpodmlfcmF3KSAlPiUNCiAgc3RyX3N1YnNldCgiXlxcZHs0fS1cXGR7Mn0tXFxkezJ9JCIpDQojcHJpbnQoZGF0ZV9jb2xzKQ0Kc3RvcGlmbm90KGxlbmd0aChkYXRlX2NvbHMpID4gMCkNCg0KIy0tLVdpZGUgLT4gTG9uZyAtLS0NCg0Kemh2aV9tZXRybyA8LSB6aHZpX3JhdyAlPiUNCiAgcGl2b3RfbG9uZ2VyKA0KICAgIGNvbHMgICAgICA9ICBhbGxfb2YoZGF0ZV9jb2xzKSwNCiAgICBuYW1lc190byAgPSAgImRhdGUiLA0KICAgIHZhbHVlc190byA9ICAiemh2aSIsDQogICAgdmFsdWVzX2Ryb3BfbmEgPSBUUlVFDQogICkgJT4lDQogIG11dGF0ZSgNCiAgICBkYXRlID0geW1kKGRhdGUpLA0KICAgIHJlZ2lvbiA9IHN0cl90cmltKFJlZ2lvbk5hbWUpDQogICkgJT4lDQogIHNlbGVjdCgNCiAgICByZWdpb24sDQogICAgZGF0ZSwNCiAgICB6aHZpDQogICkgJT4lDQogIGFycmFuZ2UocmVnaW9uLCBkYXRlKQ0KI2hlYWQoemh2aV9tZXRybywxMDApDQojLS0tU2FuaXR5IGNoZWNrcyAtLS0NCnN0b3BpZm5vdChhbGwoIWlzLm5hKHpodmlfbWV0cm8kcmVnaW9uKSkpDQpzdG9waWZub3QoYWxsKCFpcy5uYSh6aHZpX21ldHJvJGRhdGUpKSkNCnN0b3BpZm5vdChhbGwoIWlzLm5hKHpodmlfbWV0cm8kemh2aSkpKQ0KDQp6aHZpX21ldHJvIDwtIHpodmlfbWV0cm8gJT4lDQogIG11dGF0ZShkYXRlID0gZmxvb3JfZGF0ZShkYXRlLCAibW9udGgiKSkNCg0Kemh2aV9tZXRybyAlPiUNCiAgc3VtbWFyaXNlKA0KICAgIG5fcmVnaW9ucyA9IG5fZGlzdGluY3QocmVnaW9uKSwNCiAgICBzdGFydF9kYXRlID0gbWluKGRhdGUpLA0KICAgIGVuZF9kYXRlID0gbWF4KGRhdGUpDQogICkgJT4lDQogIHByaW50KCkNCg0Kd3JpdGVfY3N2KA0KICB6aHZpX21ldHJvLCAiLi9kYXRhL3Byb2Nlc3NlZC96aHZ2aV9tZXRyb19tb250aGx5LmNzdiINCikNCmBgYA0KDQojIyMgQ1NVU0hQSVNBIGRhdGFzZXQNCg0KYGBge3J9DQpjc19tIDwtIHJlYWRyOjpyZWFkX2NzdigiLi9kYXRhL3Jhdy9mcmVkX0NTVVNIUElTQV8yMDAwMDEwMS5jc3YiKSAlPiUNCiAgbXV0YXRlKGRhdGUgPSB5bWQoZGF0ZSkpICU+JQ0KICByZW5hbWUoY2FzZV9zaGlsbGVyX3VzID0gQ1NVU0hQSVNBKSAlPiUNCiAgYXJyYW5nZShkYXRlKQ0KDQpyZWFkcjo6d3JpdGVfY3N2KGNzX20sICIuL2RhdGEvcHJvY2Vzc2VkL2Nhc2Vfc2hpbGxlcl91c19tb250aGx5LmNzdiIpDQpgYGANCg0KIyMgUGFuZWwgQ29uc3RydWN0aW9uDQoNCmBgYHtyfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGx1YnJpZGF0ZSkNCg0KIyAtLS0gTG9hZCBwcm9jZXNzZWQgZGF0YXNldHMgLS0tDQoNCnpodmkgPC0gcmVhZF9jc3YoIi4vZGF0YS9wcm9jZXNzZWQvemh2dmlfbWV0cm9fbW9udGhseS5jc3YiLCBzaG93X2NvbF90eXBlcyA9IEZBTFNFKQ0KbW9ydGdhZ2UgPC0gcmVhZF9jc3YoIi4vZGF0YS9wcm9jZXNzZWQvbW9ydGdhZ2VfbW9udGhseS5jc3YiLCBzaG93X2NvbF90eXBlcyA9IEZBTFNFKQ0KY3BpIDwtIHJlYWRfY3N2KCIuL2RhdGEvcHJvY2Vzc2VkL2NwaV9tb250aGx5LmNzdiIsIHNob3dfY29sX3R5cGVzID0gRkFMU0UpDQppbmNvbWUgPC0gcmVhZF9jc3YoIi4vZGF0YS9wcm9jZXNzZWQvaW5jb21lX21ldHJvX2FubnVhbC5jc3YiLCBzaG93X2NvbF90eXBlcyA9IEZBTFNFKQ0KDQojLS0tRGVmaW5lIGdsb2JhbCBtb250aGx5IGRhdGUgc2VxdWVuY2UgLS0NCmRhdGVfc2VxIDwtIHNlcS5EYXRlKA0KICBmcm9tID0gbWluKHpodmkkZGF0ZSksDQogIHRvID0gbWF4KHpodmkkZGF0ZSksDQogIGJ5ID0gIm1vbnRoIg0KKQ0KDQojLS0tIEV4cGFuZCBhbm51YWwgaW5jb21lIHRvIG1vbnRobHkgKGZvcndhcmQtZmlsbCkgLS0tDQoNCmluY29tZV9tIDwtIGluY29tZSAlPiUNCiAgbXV0YXRlKGRhdGUgPSBmbG9vcl9kYXRlKGRhdGUsICJ5ZWFyIikpICU+JQ0KICBncm91cF9ieShyZWdpb24pICU+JQ0KICB0aWR5cjo6Y29tcGxldGUoZGF0ZSA9IGRhdGVfc2VxKSAlPiUNCiAgZmlsbChpbmNvbWVfbWVkaWFuX2hoLCAuZGlyZWN0aW9uID0gInVwZG93biIpICU+JQ0KICB1bmdyb3VwKCkNCg0KDQojLS0tQnVpbGQgdW5pZmllZCBtZXRyby1tb250aCBwYW5lbCAtLS0NCg0KaG91c2luZ19wYW5lbCA8LSB6aHZpICU+JQ0KICBsZWZ0X2pvaW4obW9ydGdhZ2UsIGJ5ID0gImRhdGUiKSAlPiUNCiAgbGVmdF9qb2luKGNwaSwgYnkgPSAiZGF0ZSIpICU+JQ0KICBsZWZ0X2pvaW4oaW5jb21lX20sIGJ5ID0gYygicmVnaW9uIiwgImRhdGUiKSkNCg0KIy0tLWNoZWNrcyAtLS0NCg0KaG91c2luZ19wYW5lbCAlPiUNCiAgc3VtbWFyaXNlKA0KICAgIG5fcmVnaW9ucyA9IG5fZGlzdGluY3QocmVnaW9uKSwNCiAgICBzdGFydF9kYXRlID0gbWluKGRhdGUpLA0KICAgIGVuZF9kYXRlID0gbWF4KGRhdGUpLA0KICAgIHBjdF9taXNzaW5nX3podmkgPSBtZWFuKGlzLm5hKHpodmkpKSwNCiAgICBwY3RfbWlzc2luZ19pbmNvbWUgPSBtZWFuKGlzLm5hKGluY29tZV9tZWRpYW5faGgpKQ0KICApICU+JQ0KICBwcmludCgpDQoNCg0KYGBgDQoNCiMjIFBhbmVsIEludGVncml0eSBDaGVjaw0KDQpgYGB7cn0NCmhvdXNpbmdfcGFuZWwgJT4lDQogIHN1bW1hcmlzZSgNCiAgICBuX3JlZ2lvbnMgPSBuX2Rpc3RpbmN0KHJlZ2lvbiksDQogICAgbl9yb3dzID0gbigpLA0KICAgIHN0YXJ0X2RhdGUgPSBtaW4oZGF0ZSksDQogICAgZW5kX2RhdGUgPSBtYXgoZGF0ZSksDQogICAgcGN0X21pc3Npbmdfemh2aSA9IG1lYW4oaXMubmEoemh2aSkpLA0KICAgIHBjdF9taXNzaW5nX2luY29tZSA9IG1lYW4oaXMubmEoaW5jb21lX21lZGlhbl9oaCkpLA0KICAgIHBjdF9taXNzaW5nX21vcnRnYWdlID0gbWVhbihpcy5uYShtb3J0Z2FnZV8zMHkpKSwNCiAgICBwY3RfbWlzc2luZ19jcGkgPSBtZWFuKGlzLm5hKGNwaSkpDQogICkgJT4lDQogIHByaW50KCkNCg0KYGBgDQoNCiMjIyBOb3JtYWxpemF0aW9uIGZ1bmN0aW9uIGZvciByZWdpb24NCg0KYGBge3J9DQpub3JtYWxpemVfcmVnaW9uIDwtIGZ1bmN0aW9uKHgpew0KICB4ICU+JQ0KICAgIHN0cmluZ3I6OnN0cl9yZW1vdmUoIiBNZXRyb3BvbGl0YWluIFN0YXRpc3RpY2FsIEFyZWEkIikgJT4lDQogICAgc3RyaW5ncjo6c3RyX3JlbW92ZSgiIE1ldHJvIEFyZWEkIikgJT4lDQogICAgc3RyaW5ncjo6c3RyX3JlbW92ZSgiIE1pY3JvIEFyZWEkIikgJT4lDQogICAgc3RyaW5ncjo6c3RyX3RyaW0oKQ0KfQ0KYGBgDQoNCiMjIyBBcHBseSBub3JtYWxpemF0aW9uDQoNCmBgYHtyfQ0Kemh2aSA8LSB6aHZpICU+JQ0KICBtdXRhdGUocmVnaW9uX25vcm0gPSBub3JtYWxpemVfcmVnaW9uKHJlZ2lvbikpDQoNCmluY29tZV9tIDwtIGluY29tZV9tICU+JQ0KICBtdXRhdGUocmVnaW9uX25vcm0gPSBub3JtYWxpemVfcmVnaW9uKHJlZ2lvbikpDQoNCg0KYGBgDQoNCiMjIyBSZWJ1aWxkIHRoZSBwYW5lbA0KDQpgYGB7cn0NCmhvdXNpbmdfcGFuZWwgPC0gemh2aSAlPiUNCiAgbGVmdF9qb2luKG1vcnRnYWdlLCBieSA9ICJkYXRlIikgJT4lDQogIGxlZnRfam9pbihjcGksIGJ5ID0gImRhdGUiKSAlPiUNCiAgbGVmdF9qb2luKA0KICAgIGluY29tZV9tLA0KICAgIGJ5ID0gYygicmVnaW9uX25vcm0iID0gInJlZ2lvbl9ub3JtIiwgImRhdGUiPSJkYXRlIikNCiAgKQ0KYGBgDQoNCiMjIyBDaGVjaw0KDQpgYGB7cn0NCmhvdXNpbmdfcGFuZWwgJT4lDQogIHN1bW1hcmlzZSgNCiAgICBwY3RfbWlzc2luZ19pbmNvbWUgPSBtZWFuKGlzLm5hKGluY29tZV9tZWRpYW5faGgpKQ0KICApICU+JSBwcmludCgpDQpgYGANCg0KYGBge3J9DQpoZWFkKGhvdXNpbmdfcGFuZWwsIDIwMCkNCmBgYA0KDQojIyBTYXZlIHRoZSBmdWxsIHBhbmVsDQoNCmBgYHtyfQ0Kd3JpdGVfY3N2KA0KICBob3VzaW5nX3BhbmVsLA0KICAiLi9kYXRhL3Byb2Nlc3NlZC9ob3VzaW5nX3BhbmVsX21ldHJvX21vbnRobHkuY3N2Ig0KKQ0KYGBgDQoNCiMjIEZpbHRlciB0byBtZXRyb3Mgd2l0aCBpbmNvbWUgY292ZXJhZ2UNCg0KYGBge3J9DQpob3VzaW5nX3BhbmVsX21vZGVsIDwtIGhvdXNpbmdfcGFuZWwgJT4lDQogIGdyb3VwX2J5KHJlZ2lvbl9ub3JtKSAlPiUNCiAgZmlsdGVyKGFueSghaXMubmEoaW5jb21lX21lZGlhbl9oaCkpKSAlPiUNCiAgdW5ncm91cCgpDQpgYGANCg0KIyMjIFJlY2hlY2sNCg0KYGBge3J9DQpob3VzaW5nX3BhbmVsX21vZGVsICU+JQ0KICBzdW1tYXJpc2UocGN0X21pc3NpbmdfaW5jb21lID0gbWVhbihpcy5uYShpbmNvbWVfbWVkaWFuX2hoKSkpDQpgYGANCg0KQXBwcm94aW1hdGVseSAyMCUgb2YgWmlsbG93LWRlZmluZWQgbWV0cm8gYXJlYXMgZG8gbm90IGhhdmUgY29ycmVzcG9uZGluZyBBQ1MgaW5jb21lIGVzdGltYXRlcyBkdWUgdG8gZ2VvZ3JhcGhpYyBkZWZpbml0aW9uIGRpZmZlcmVuY2VzLiBJIHJlc3RyaWN0IGluY29tZS1iYXNlZCBhbmFseXNlcyB0byBtZXRyb3Mgd2l0aCBvYnNlcnZlZCBBQ1MgY292ZXJhZ2UuDQoNCiMjIyBTYXZlIG1vZGVsIHJlYWR5IHBhbmVsDQoNCmBgYHtyfQ0KcmVhZHI6OndyaXRlX2NzdigNCiAgaG91c2luZ19wYW5lbF9tb2RlbCwNCiAgIi4vZGF0YS9wcm9jZXNzZWQvaG91c2luZ19wYW5lbF9tb2RlbF9tZXRyb19tb250aGx5LmNzdiINCikNCg0KbWVzc2FnZSgiU2F2ZWQ6IGRhdGEvcHJvY2Vzc2VkL2hvdXNpbmdfcGFuZWxfbW9kZWxfbWV0cm9fbW9udGhseS5jc3YiKQ0KYGBgDQo=