Household Disaggregation and Auto Ownership
Household Disaggregation
Modeled vs. Observed
Code
hh_data = transpose(hhdis_data);
// Sorted unique values for facet dimensions (HHSIZE = cols, WRKS = rows)
hh_hhsize_vals = Array.from(new Set(hh_data.map(d => d.HHSIZE))).sort((a, b) => a - b)
hh_wrks_vals = Array.from(new Set(hh_data.map(d => d.WRKS))).sort((a, b) => a - b)
inc_list = ["Total", "<$50k", "$50k–$100k", "$100k–$150k", ">$150k"]Code
Code
viewof vXAxis = Inputs.select(["Modeled", "PUMS", "HTS"],
{ label: "X Axis:", value: "PUMS" })
viewof vYAxis = Inputs.select(
["Modeled", "PUMS", "HTS"].filter(d => d !== vXAxis),
{
label: "Y Axis:",
value: ["Modeled", "PUMS", "HTS"].find(d => d !== vXAxis)
}
)
viewof vDisplay = Inputs.radio(["Households", "Percentages"],
{ label: "Display:", value: "Households" })Code
hh_filtered_base = hh_data.filter(d =>
(vINC === "Total" || d.INC === vINC)
)
// Aggregate by geo × HHSIZE × WRKS
hh_agg_facet = {
const grouped = d3.groups(hh_filtered_base, d => d[vGeoLevel], d => d.HHSIZE, d => d.WRKS);
const rows = [];
for (const [geo_id, bySize] of grouped) {
for (const [hhsize, byWrks] of bySize) {
for (const [wrks, records] of byWrks) {
const mod = d3.sum(records, d => d.Modeled_Count);
const pums = d3.sum(records, d => d.Observed_Count);
const hts = d3.sum(records, d => d.HTS_Count);
rows.push({
geo_id, HHSIZE: hhsize, WRKS: wrks,
Modeled_Count: mod, Observed_Count: pums, HTS_Count: hts,
Mod_vs_PUMS: pums > 0 ? (mod - pums) / pums : 0,
Mod_vs_HTS: hts > 0 ? (mod - hts) / hts : 0,
PUMS_vs_Mod: mod > 0 ? (pums - mod) / mod : 0,
HTS_vs_Mod: mod > 0 ? (hts - mod) / mod : 0,
PUMS_vs_HTS: hts > 0 ? (pums - hts) / hts : 0,
HTS_vs_PUMS: pums > 0 ? (hts - pums) / pums : 0,
});
}
}
}
// Normalise each source by its own grand total across all geo × size × workers
const totMod = d3.sum(rows, d => d.Modeled_Count);
const totPums = d3.sum(rows, d => d.Observed_Count);
const totHts = d3.sum(rows, d => d.HTS_Count);
return rows.map(r => ({
...r,
Modeled_Pct: totMod > 0 ? r.Modeled_Count / totMod : 0,
Observed_Pct: totPums > 0 ? r.Observed_Count / totPums : 0,
HTS_Pct: totHts > 0 ? r.HTS_Count / totHts : 0,
}));
}
hh_xy_cfg = {
const fm = {
"PUMS": { field: "Observed_Count", pctField: "Observed_Pct", label: "Observed (PUMS)", color: "steelblue" },
"HTS": { field: "HTS_Count", pctField: "HTS_Pct", label: "Observed (HTS)", color: "#2ca02c" },
"Modeled": { field: "Modeled_Count", pctField: "Modeled_Pct", label: "Modeled", color: "orange" }
};
const errMap = {
"PUMS|Modeled": "Mod_vs_PUMS", "PUMS|HTS": "HTS_vs_PUMS",
"HTS|Modeled": "Mod_vs_HTS", "HTS|PUMS": "PUMS_vs_HTS",
"Modeled|PUMS": "PUMS_vs_Mod", "Modeled|HTS": "HTS_vs_Mod"
};
return { xCfg: fm[vXAxis], yCfg: fm[vYAxis], errField: errMap[`${vXAxis}|${vYAxis}`] };
}
hh_vf = vDisplay === "Percentages"
? { xf: hh_xy_cfg.xCfg.pctField, yf: hh_xy_cfg.yCfg.pctField,
fmt: d3.format(".1%"), tickFmt: d3.format(".0%"), axSuffix: " (%)" }
: { xf: hh_xy_cfg.xCfg.field, yf: hh_xy_cfg.yCfg.field,
fmt: d3.format(",.0f"), tickFmt: undefined, axSuffix: "" }
hh_chart_max = d3.max(hh_agg_facet, d => Math.max(d[hh_vf.xf], d[hh_vf.yf])) * 1.05Code
// 2-D faceted scatter: columns = HH Size, rows = Workers
Plot.plot({
grid: true,
width: 900,
height: 140 * hh_wrks_vals.length + 40,
marginRight: 60,
marginLeft: 50,
fx: { label: "HH Size", tickFormat: d => `Size ${d}`, padding: 0.1 },
fy: { label: "Workers", tickFormat: d => `${d} Worker${d !== 1 ? "s" : ""}`, padding: 0.1 },
x: {
label: `${hh_xy_cfg.xCfg.label}${hh_vf.axSuffix} →`,
domain: [0, hh_chart_max],
tickFormat: hh_vf.tickFmt
},
y: {
label: `↑ ${hh_xy_cfg.yCfg.label}${hh_vf.axSuffix}`,
domain: [0, hh_chart_max],
tickFormat: hh_vf.tickFmt
},
caption: html`<b>${hh_xy_cfg.yCfg.label} vs. ${hh_xy_cfg.xCfg.label}</b> — columns: HH Size | rows: Workers`,
marks: [
// 45-degree equal reference line in every facet panel
Plot.link(
hh_hhsize_vals.flatMap(s => hh_wrks_vals.map(w => ({ s, w }))),
{
x1: 0, y1: 0,
x2: hh_chart_max, y2: hh_chart_max,
fx: "s", fy: "w",
stroke: "gray", strokeOpacity: 0.7, strokeWidth: 1.5
}
),
Plot.dot(hh_agg_facet, {
x: hh_vf.xf, y: hh_vf.yf,
fx: "HHSIZE", fy: "WRKS",
r: 3.5, fill: hh_xy_cfg.yCfg.color, fillOpacity: 0.6,
tip: true,
title: d =>
`${vGeoLevel}: ${d.geo_id}\nHH Size: ${d.HHSIZE} Workers: ${d.WRKS}\n` +
`${hh_xy_cfg.xCfg.label}: ${hh_vf.fmt(d[hh_vf.xf])}\n` +
`${hh_xy_cfg.yCfg.label}: ${hh_vf.fmt(d[hh_vf.yf])}`
}),
Plot.linearRegressionY(hh_agg_facet, {
x: hh_vf.xf, y: hh_vf.yf,
fx: "HHSIZE", fy: "WRKS",
stroke: hh_xy_cfg.yCfg.color, strokeDasharray: "4 4"
})
]
})Code
{
if (!document.getElementById("leaflet-css")) {
const link = document.createElement("link");
link.id = "leaflet-css";
link.rel = "stylesheet";
link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
document.head.appendChild(link);
}
const L = await require("leaflet@1.9.4");
const container = html`<div style="height:560px; width:100%; border-radius:6px; overflow:hidden;"></div>`;
yield container;
const map = L.map(container, {zoomControl: true});
L.tileLayer(
"https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
{
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors ' +
'© <a href="https://carto.com/attributions">CARTO</a>',
subdomains: "abcd",
maxZoom: 19,
}
).addTo(map);
// Aggregate map data across all HHSIZE × WRKS combinations for selected income filter
const mapAgg = d3.rollup(
hh_filtered_base,
v => {
const mod = d3.sum(v, d => d.Modeled_Count);
const pums = d3.sum(v, d => d.Observed_Count);
const hts = d3.sum(v, d => d.HTS_Count);
return {
Modeled_Count: mod, Observed_Count: pums, HTS_Count: hts,
Mod_vs_PUMS: pums > 0 ? (mod - pums) / pums : null,
Mod_vs_HTS: hts > 0 ? (mod - hts) / hts : null,
PUMS_vs_Mod: mod > 0 ? (pums - mod) / mod : null,
HTS_vs_Mod: mod > 0 ? (hts - mod) / mod : null,
PUMS_vs_HTS: hts > 0 ? (pums - hts) / hts : null,
HTS_vs_PUMS: pums > 0 ? (hts - pums) / pums : null,
};
},
d => d[vGeoLevel]
);
const geo_lookup = {
"CO_FIPS": { geo: geo_county_hh, prop: "CO_FIPS" },
"PUMA": { geo: geo_puma_hh, prop: "PUMA" }
};
const { geo: base_geo, prop: name_prop } = geo_lookup[vGeoLevel];
const features = base_geo.features.map(f => {
const key = f.properties[name_prop];
const row = mapAgg.get(+key) || mapAgg.get(String(key))
|| { Modeled_Count: 0, Observed_Count: 0, HTS_Count: 0,
Mod_vs_PUMS: null, Mod_vs_HTS: null,
PUMS_vs_Mod: null, HTS_vs_Mod: null,
PUMS_vs_HTS: null, HTS_vs_PUMS: null };
return { ...f, properties: { ...f.properties, ...row } };
});
const map_features_hh = { type: "FeatureCollection", features };
const colorScale = d3.scaleDiverging(d3.interpolateRdBu).domain([0.5, 0, -0.5]);
function getColor(pct) {
if (pct === null || pct === undefined) return "#cccccc";
return colorScale(Math.max(-0.5, Math.min(0.5, pct)));
}
let geojsonLayer = L.geoJSON(map_features_hh, {
style: feature => ({
fillColor: getColor(feature.properties[hh_xy_cfg.errField]),
fillOpacity: 0.72,
color: "#ffffff",
weight: 1,
}),
onEachFeature: (feature, layer) => {
const p = feature.properties;
const err = p[hh_xy_cfg.errField];
const noData = err === null || err === undefined;
layer.bindPopup(
`<b>${vGeoLevel} ${p[name_prop]}</b><br>` +
`<i style="font-size:10px;color:#666">${hh_xy_cfg.yCfg.label} vs. ${hh_xy_cfg.xCfg.label}</i><br>` +
`Pct Error: ${noData ? "N/A" : (err * 100).toFixed(1) + "%"}<br>` +
`Modeled: ${d3.format(",.0f")(p.Modeled_Count)}<br>` +
`Observed (PUMS): ${d3.format(",.0f")(p.Observed_Count)}<br>` +
`Observed (HTS): ${d3.format(",.0f")(p.HTS_Count)}`
);
layer.on("mouseover", () => layer.setStyle({fillOpacity: 0.95, weight: 2}));
layer.on("mouseout", () => geojsonLayer.resetStyle(layer));
}
}).addTo(map);
if (geojsonLayer.getBounds().isValid()) {
map.fitBounds(geojsonLayer.getBounds(), {padding: [10, 10]});
}
const resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
map.invalidateSize();
if (geojsonLayer.getBounds().isValid()) {
map.fitBounds(geojsonLayer.getBounds(), {padding: [10, 10]});
}
}, 100);
});
resizeObserver.observe(container);
invalidation.then(() => { map.remove(); resizeObserver.disconnect(); });
const legend = L.control({position: "bottomright"});
legend.onAdd = () => {
const stops = [-0.5, -0.25, 0, 0.25, 0.5];
const labels = ["≤ −50%", "−25%", "0%", "+25%", "≥ +50%"];
const swatches = stops.map((v, i) =>
`<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">` +
`<span style="display:inline-block;width:14px;height:14px;border-radius:2px;flex-shrink:0;` +
`background:${getColor(v)};border:1px solid rgba(0,0,0,0.15);"></span>` +
`<span style="font-size:11px;">${labels[i]}</span>` +
`</div>`
).join("");
const div = L.DomUtil.create("div");
div.innerHTML =
`<div style="background:white;padding:10px 12px;border-radius:6px;` +
`box-shadow:0 1px 5px rgba(0,0,0,0.25);line-height:1.5;">` +
`<div style="font-size:12px;font-weight:600;margin-bottom:4px;">Percent Error</div>` +
`<div style="font-size:10px;color:#666;margin-bottom:6px;">` +
`Red = Underpredict | Blue = Overpredict` +
`</div>` +
swatches +
`</div>`;
return div;
};
legend.addTo(map);
}Code
hh_cnt_fmt = vDisplay === "Percentages" ? d3.format(".1%") : d3.format(",.0f")
viewof hh_summary_table = Inputs.table(hh_agg_facet, {
columns: [
"geo_id", "HHSIZE", "WRKS",
vDisplay === "Percentages" ? "Modeled_Pct" : "Modeled_Count",
vDisplay === "Percentages" ? "Observed_Pct" : "Observed_Count",
vDisplay === "Percentages" ? "HTS_Pct" : "HTS_Count",
"Mod_vs_PUMS", "Mod_vs_HTS", "PUMS_vs_HTS"
],
header: {
geo_id: "Geo ID",
HHSIZE: "HH Size",
WRKS: "Workers",
Modeled_Count: "Modeled", Modeled_Pct: "Modeled (%)",
Observed_Count: "PUMS", Observed_Pct: "PUMS (%)",
HTS_Count: "HTS", HTS_Pct: "HTS (%)",
Mod_vs_PUMS: "Mod vs PUMS",
Mod_vs_HTS: "Mod vs HTS",
PUMS_vs_HTS: "PUMS vs HTS"
},
format: {
Modeled_Count: hh_cnt_fmt, Modeled_Pct: hh_cnt_fmt,
Observed_Count: hh_cnt_fmt, Observed_Pct: hh_cnt_fmt,
HTS_Count: hh_cnt_fmt, HTS_Pct: hh_cnt_fmt,
Mod_vs_PUMS: d3.format("+.1%"),
Mod_vs_HTS: d3.format("+.1%"),
PUMS_vs_HTS: d3.format("+.1%")
},
sort: "Mod_vs_PUMS",
reverse: true,
rows: 15,
width: "100%"
})