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=