Monday, November 2, 2026

Overbuilt and over-extended

Even a reasonable and prudent municipality that budgeted for optimal asset management would have been wrecked by material costs increases in concrete and asphalt. Most munis have been neither prudent or planful and have operated on a 'pay as you go' ad-hoc basis, without any kind of asset management plan, and are hence overbuilt and over-extended.

The feds don't require asset management plans from DOT's because they like process. They require it because they are on the hook (politically) for quite a lot of spending, and they want to make sure that what their money buys gets taken care of. Of course, that obligation has mainly been honored in the breach[3], through willfully ignoring aforesaid plans, which then required the FAST/MAPS act requirements, which essentially says "you have to spend at least enough money on maintenance that pavement condition doesn't get worse". [1]

But for most munis, from an economic competitiveness perspective, road quality is just one among a plethora of factors. Terrible roads but amazing schools is a reasonable trade-off some places elect to make. 

The real questions for most munis is "Do you really need that lane there? Sure, it's nice at 8a on a Monday morning, but is it worth the million bucks every ten years[2] to have it for those ten minutes?".  

#----------------------------------------------------------------------------------------------------------------------

[1] West Virginia an interesting case. Lots of roads there are narrow, built entirely without any shoulder whatsoever-- you leave the pavement, and you are going over the edge. But it does mean, in terms of square meters of pavement that requires maintenance, WV is doing ok. A wider should would make things safer (so many car crashes in WV) but that would also mean far fewer paved roads. A 2' should on every 8' lane would mean 25% fewer paved lane miles, with an accompanied reduction in roadway access. 

[2] "Why do we need to pay for it--we already built it" is crankery. Everything is always constantly breaking down. We just don't notice it until it's too late, because the breakdown is exponential. Roads, human health, house roofs. It's all fine until it's not, and then it's an emergency, and emergencies are costly to fix. 

[3] 'Fix-it-first' is a half-measure; politics means money goes to where it shows--filling pot-holes and patching cracks, while the very foundation of the road is washed away due to a clogged drainpipe. 

Wednesday, October 28, 2026

Against Zoning

 The role of racism (explicit and implicit) in zoning is undeniable. The problems modern zoning code 'solves' are rarely the ones that provide its legal justification. Zoning is inherently exclusionary, which has purpose in excluding incompatible uses, but the definition of which has been ratcheted up over times to sometimes absurd levels: a 'you shouldn't be able to see your neighbor house' sort of thing. Functionally, a lot of zoning code is written to pre-empt nuisance, and the level of pre-emption of expected/supposed nuisance can be extreme. Most zoning fixes are designed in response to solve new problems: how big the problem would actually have been beyond a single circumstance becomes moot. So it's easy for cities to become pre-emptively over-regulated. And as staff changes over, it's easy for the institutional memory of the basis for a zoning tweak to become lost, and zoning continues to operate on a 'not broke don't fix it' basis, despite the tremendous forgone benefits.

Wednesday, October 21, 2026

"We prefer zoning that blends rather than contrasts"

The market reality is that land prices are now so high that no one can afford to build anything that 'blends'. Much high density is a precondition for new housing. Without new housing, the cost of existing housing rises. California has finally reached the point where the state leaders recognized that the cost of housing was impairing statewide economic growth and have started pre-empting local zoning. Utah (a deep red state) has some to a similar conclusion, but a different solution: places that don't permit growth don't get any infrastructure investment.

Friday, October 16, 2026

Density Profiles of Cities

 Playing around with my WeAloneOnEarth code, testing it against cities* I've lived in, started getting some interesting results:

Suggesting Phoenix is more like Salt Lake than Raleigh is counter-intuitive: aren't both Raleigh and Phoenix both sprawling sunbelt cities? Adding more cities makes it stranger:


Suggests that in some fundamental way, a surprising array of cities in the west are similar, and very different from places in the sprawling south. Adding more major southern cities only reinforces things:


Compare and contrast on some extreme cases is interesting: LA is its own thing, as different from Phoenix as Phoenix is from Atlanta.  
But all show a distinct 'peak' showing their median density, which is radically different from Boston or NYC.
Seems worthwhile to parameterize the logistic-style curves in the first chart. The result is a pretty good fit, with the interesting implication that NYC is in fact lacking in high density development, and there are a lot of tracts near 10^4 when they should be nearer 10^5. 


Altogether, these two charts together suggest that we can describe every city in American using two numbers: a median population, and a k-value. Hypothetically, Cleveland would probably have a peak at 10^.37, with a K value of 3.5. In actuality, Cleveland peaks (minimally), but has a much higher K value than expected (k=4.79) such that it is much more like Atlanta(!) than Chicago and seems to be missing high-density tracts in the 10^(3.5-4) order of magnitude. 


Next step may be to map this - if I was to arbitrarily declare 10^3 (1000 persons**/square kilometer) to be transit suitable densities, what would each of these cities look like, and how would that align to their transit networks.

#--------------------------------------------------------------------------------------------------------------------

*The term city is used casually here; the data is for census urbanized areas. A city as a legal and territorial jurisdiction isn't relevant in this context. 

**At some point I'll need to include employment into these estimates--employment concentration is critical for effective fixed guideway transit, probably more important than population density. 














Thursday, October 15, 2026

A Portrait of a City as a Squiggly Line

 I have loved the graphics from "A Portrait of the City as a Squiggly Line" from the from the "We Alone on Earth" blog for the past 15 years (!), and have long aspired to recreate it. With some extensive help from Claude, I have done so (R Code at the bottom). 

The original (based on 2000 census data)

My recreation (2023 ACS data):

Changes: Atlanta given its own color. 

I suspect that the creator of the original did some additional (visual) smoothing for the second chart. 

R code follows:

#-------------------------------------------------------------------------------

# Population density experienced by residents, compared across eight 2020 Census Urban Areas:

#   Los Angeles--Long Beach--Anaheim, CA     San Francisco--Oakland, CA
#   Chicago, IL--IN                          New York--Jersey City--Newark, NY--NJ
#   Boston, MA--NH                           Seattle--Tacoma, WA
#   Las Vegas--Henderson--Paradise, NV       Atlanta, GA
#
# Universe: census tracts whose interior point falls inside the urban area
# boundary. Whole tracts are used (their full population and land area), so
# tracts straddling the edge are in or out as a unit.
#
# Density = tract population / tract LAND area (people per km²).
#
# Two charts:
#   1. Share of residents living at AT LEAST each density (log x-axis).
#      Read: "x% of the urban area's people live in tracts at least this dense."
#   2. Number of people living in tracts within +/-15% of each density.
#      Read: "this many people live at about this density."
#
# Both are computed directly from tracts -- no smoothing.
#
# Population: most recent ACS 5-year (tries newer vintages first, falls back).
# Land area:  TIGER/Line cartographic boundary tracts (ALAND) for the same year.
# Urban areas: TIGER/Line 2020 Urban Areas (2020 criteria).
#
# Requires: tidycensus, tigris, sf, dplyr, ggplot2, scales, patchwork
#   install.packages(c("tidycensus", "tigris", "sf", "dplyr", "ggplot2", "scales", "patchwork"))
# One-time API key setup (free key: https://api.census.gov/data/key_signup.html):
#   tidycensus::census_api_key("YOUR_KEY", install = TRUE); readRenviron("~/.Renviron")
# ---------------------------------------------------------------------------
suppressPackageStartupMessages({
  library(tidycensus)
  library(tigris)
  library(sf)
  library(dplyr)
  library(ggplot2)
  library(scales)
})
options(tigris_use_cache = TRUE)
# --- settings --------------------------------------------------------------
# One row per urban area. `ua_name` must match the Census name exactly --
# note the double hyphen in multi-city names. If a name is off, the script
# stops and suggests close matches. `state` = the state(s) whose tracts to
# search; for a UA crossing a state line, add a second row with the same
# label and ua_name and the other state.
areas <- data.frame(
  label   = c("Los Angeles", "Bay Area",
              "Chicago", "Chicago",
              "New York", "New York",
              "Boston", "Boston",
              "Seattle", "Las Vegas", "Atlanta"),
  ua_name = c("Los Angeles--Long Beach--Anaheim, CA", "San Francisco--Oakland, CA",
              "Chicago, IL--IN", "Chicago, IL--IN",
              "New York--Jersey City--Newark, NY--NJ", "New York--Jersey City--Newark, NY--NJ",
              "Boston, MA--NH", "Boston, MA--NH",
              "Seattle--Tacoma, WA", "Las Vegas--Henderson--Paradise, NV", "Atlanta, GA"),
  state   = c("CA", "CA",
              "IL", "IN",
              "NY", "NJ",
              "MA", "NH",
              "WA", "NV", "GA"),
  stringsAsFactors = FALSE
)
ua_year <- 2023      # fallback TIGER vintage if the 2020-criteria download fails
# Line colors, keyed by label -- matplotlib's classic cycle (b, g, r, c, m,
# y, k), matching the reference figure, except Atlanta, which is pale blue
# here instead of repeating Los Angeles's blue.
line_cols <- c("Los Angeles" = "#0000FF",
               "Bay Area"    = "#008000",
               "Chicago"     = "#FF0000",
               "New York"    = "#00BFBF",
               "Boston"      = "#BF00BF",
               "Seattle"     = "#BFBF00",
               "Las Vegas"   = "#000000",
               "Atlanta"     = "#7FB2E5")   # pale blue, to separate it from LA's blue
x_range    <- c(10^2.5, 1e5)  # x-axis limits for both charts, people/km².
# NULL = fit the data.
chart1_xmin <- 10^2.5  # chart 1 x-axis starts here (~316 people/km²)
chart2_xmin <- 10^2.5  # chart 2 x-axis starts here (~316 people/km²)
window_pct <- 15     # chart 2: count people within +/- this % of each density
chart2_log <- TRUE   # chart 2 x-axis on a log scale (FALSE = linear)
grid_n     <- 400    # chart 2: number of density points evaluated
panel_wh   <- 2      # each chart's plot area is this many times wider than tall
years_to_try <- 2025:2022   # newest first; first one that answers wins
out_png  <- "density_profiles.png"            # both charts stacked, one figure
out_png1 <- "density_share_at_least.png"      # chart 1 alone
out_png2 <- "density_people_within_window.png"  # chart 2 alone
# --- download --------------------------------------------------------------
if (!nzchar(Sys.getenv("CENSUS_API_KEY")))
  stop("No Census API key found. Run once:\n",
       '  tidycensus::census_api_key("YOUR_KEY", install = TRUE)\n',
       '  readRenviron("~/.Renviron")\n',
       "Free key: https://api.census.gov/data/key_signup.html", call. = FALSE)
states <- unique(areas$state)
# Statewide tract pulls; the urban area filter happens spatially below.
pull_pop <- function(year) {
  bind_rows(lapply(states, function(s)
    get_acs(geography = "tract", variables = c(pop = "B01003_001"),
            state = s, survey = "acs5", year = year, cache_table = TRUE)))
}
acs <- NULL
errs <- character(0)
for (yr in years_to_try) {
  res <- tryCatch(pull_pop(yr), error = function(e) e)
  if (inherits(res, "error")) {
    errs[as.character(yr)] <- conditionMessage(res)
    message(sprintf("  %d: %s", yr, conditionMessage(res)))
    next
  }
  if (nrow(res) > 0) { acs <- res; acs_year <- yr; break }
}
if (is.null(acs)) stop("Every year failed. Errors:\n",
                       paste0("  ", names(errs), ": ", errs, collapse = "\n"),
                       call. = FALSE)
message(sprintf("Using ACS %d-%d 5-year estimates", acs_year - 4, acs_year))
# --- urban area boundaries -------------------------------------------------
# tigris only builds a valid download URL for the 2020-criteria urban areas
# as the full TIGER/Line file with year = 2020 + criteria = "2020" (no
# cartographic-boundary version). It's a one-time ~100 MB download, cached
# afterwards. The second attempt is a fallback for other tigris versions.
ua_attempts <- list(
  function() urban_areas(year = 2020, criteria = "2020", progress_bar = FALSE),
  function() urban_areas(year = ua_year, progress_bar = FALSE)
)
ua_all <- NULL
for (f in ua_attempts) {
  ua_all <- tryCatch(f(), error = function(e) { message("  urban areas: ", conditionMessage(e)); NULL })
  if (!is.null(ua_all)) break
}
if (is.null(ua_all)) stop("Could not download urban area boundaries (see messages above).",
                          call. = FALSE)
name_col <- grep("^NAME", names(ua_all), value = TRUE)[1]   # NAME20 in 2020-based files
missing <- setdiff(areas$ua_name, ua_all[[name_col]])
if (length(missing) > 0) {
  hints <- vapply(missing, function(m) {
    key <- sub("[-, ].*$", "", m)                        # first word of the name
    hits <- grep(key, ua_all[[name_col]], value = TRUE, ignore.case = TRUE)
    paste0("  '", m, "' -- did you mean: ",
           if (length(hits)) paste(head(hits, 5), collapse = " | ") else "(no match)")
  }, character(1))
  stop("Urban area name(s) not found:\n", paste(hints, collapse = "\n"), call. = FALSE)
}
# Equal-area projection so interior points and joins are planar-correct.
ua <- ua_all[ua_all[[name_col]] %in% areas$ua_name, name_col]
names(ua)[names(ua) == name_col] <- "ua_name"
ua <- st_transform(ua, 5070)
# --- tracts: land area + urban area assignment ----------------------------
# Same vintage as the ACS so tract GEOIDs match. ALAND excludes water.
tr <- do.call(rbind, lapply(states, function(s)
  tracts(state = s, year = acs_year, cb = TRUE, progress_bar = FALSE)[, c("GEOID", "ALAND")])) |>
  st_transform(5070)
# A tract belongs to an urban area if its interior point (always inside the
# polygon, unlike a centroid) falls within the boundary.
tract_ua <- suppressWarnings(st_point_on_surface(tr)) |>
  st_join(ua, join = st_within, left = FALSE) |>
  st_drop_geometry() |>
  inner_join(distinct(areas, ua_name, area = label), by = "ua_name") |>
  select(GEOID, ALAND, area)
df <- acs |>
  inner_join(tract_ua, by = "GEOID") |>
  transmute(area, GEOID, pop = estimate,
            land_km2 = as.numeric(ALAND) / 1e6,
            density  = pop / land_km2)
n_in_ua <- nrow(df)
df <- df |> filter(!is.na(pop), pop > 0, land_km2 > 0)   # log axis needs density > 0
if (n_in_ua > nrow(df))
  message(sprintf("  %d urban-area tracts dropped (no population or no land area)",
                  n_in_ua - nrow(df)))
# --- summaries -------------------------------------------------------------
# Population-weighted median: half of residents live at lower density, half
# at higher. On chart 1 it's where each curve crosses 50%.
wmedian <- function(x, w) { o <- order(x); x[o][which(cumsum(w[o]) / sum(w) >= 0.5)[1]] }
stats <- df |>
  group_by(area) |>
  # med must come before pop: summarise() evaluates in order, and pop = sum(pop)
  # would otherwise replace the per-tract weights with a single total.
  summarise(tracts = n(),
            med = wmedian(density, pop),
            pop = sum(pop), .groups = "drop")
print(stats)
# Plot order and colors follow `areas` and `line_cols`.
lvl <- unique(areas$label)
pal <- line_cols[lvl]
if (anyNA(pal))
  stop("No color in line_cols for: ", paste(lvl[is.na(pal)], collapse = ", "), call. = FALSE)
df <- df |> mutate(area = factor(area, levels = lvl))
src <- sprintf("Census tracts, ACS %d-%d 5-year estimates; land area from TIGER/Line",
               acs_year - 4, acs_year)
# --- shared styling: matplotlib "classic" look -----------------------------
# White panel, full black box, ticks pointing inward, no gridlines, thin lines.
base_theme <- theme_bw(base_size = 12) +
  theme(panel.grid        = element_blank(),
        panel.border      = element_rect(colour = "black", fill = NA, linewidth = 0.6),
        axis.ticks        = element_line(colour = "black", linewidth = 0.4),
        axis.ticks.length = unit(-4, "pt"),                # inward ticks
        axis.text         = element_text(colour = "black", size = 11),
        axis.text.x       = element_text(margin = margin(t = 7)),
        axis.text.y       = element_text(margin = margin(r = 7)),
        axis.title        = element_text(size = 11),
        plot.margin       = margin(6, 14, 6, 6),
        aspect.ratio      = 1 / panel_wh)          # panel height = width / panel_wh
x_lim <- if (is.null(x_range)) range(df$density) else x_range
log_x <- scale_x_log10(labels = label_log(), breaks = 10^(0:6), expand = c(0, 0))
# Minor log ticks, drawn inside the panel along the bottom like matplotlib's.
log_ticks <- annotation_logticks(sides = "b", outside = FALSE, colour = "black",
                                 short = unit(2, "pt"), mid = unit(3, "pt"),
                                 long = unit(4, "pt"), linewidth = 0.3)
line_w <- 0.95  # line thickness for both charts
# --- chart 1: share of residents living at AT LEAST each density ----------
# For each tract (sorted by density), the share of the urban area's population
# in tracts at that density or higher. Starts at 100% and steps down.
cum <- df |>
  group_by(area) |>
  arrange(density, .by_group = TRUE) |>
  mutate(share_at_least = 100 * rev(cumsum(rev(pop))) / sum(pop)) |>
  ungroup()
p1 <- ggplot(cum, aes(x = density, y = share_at_least, colour = area)) +
  geom_step(direction = "vh", linewidth = line_w) +
  scale_colour_manual(values = pal, name = NULL) +
  log_x + log_ticks +
  scale_y_continuous(breaks = seq(0, 100, 20), expand = c(0, 0)) +
  # Zoom only: tracts below chart1_xmin still count toward the shares, so the
  # curves enter at below 100% if some residents live at lower density.
  coord_cartesian(xlim = c(chart1_xmin, x_lim[2]), ylim = c(0, 100)) +
  labs(x = "People per sq. km.", y = "Percentage living at at least that density") +
  base_theme +
  # Boxed legend inside the top-right corner.
  theme(legend.position      = c(0.99, 0.98),
        legend.justification = c(1, 1),
        legend.background    = element_rect(colour = "black", fill = "white", linewidth = 0.4),
        legend.key           = element_blank(),
        legend.key.width     = unit(2.4, "lines"),
        legend.text          = element_text(size = 13),
        legend.margin        = margin(4, 8, 4, 4))
# --- chart 2: people living within +/- window_pct of each density ---------
# Evaluate a grid of densities; at each one, sum the population of tracts
# whose density is within +/- window_pct of it. The window is relative, so
# it widens as density rises (at 15%: +/-150 at 1,000; +/-1,500 at 10,000).
rng    <- x_lim
rng[1] <- chart2_xmin        # no need to evaluate densities left of the axis
grid <- if (chart2_log) 10^seq(log10(rng[1]), log10(rng[2]), length.out = grid_n) else
  seq(rng[1], rng[2], length.out = grid_n)
f <- window_pct / 100
win <- df |>
  group_by(area) |>
  group_modify(function(d, ...) {
    inside <- outer(d$density, grid, function(x, g) abs(x - g) <= f * g)
    data.frame(density = grid, people = colSums(inside * d$pop))
  }) |>
  ungroup()
p2 <- ggplot(win, aes(x = density, y = people, colour = area)) +
  geom_line(linewidth = line_w) +
  scale_colour_manual(values = pal, guide = "none") +     # legend lives on chart 1
  {if (chart2_log) log_x else scale_x_continuous(labels = label_comma(), expand = c(0, 0))} +
  {if (chart2_log) log_ticks} +
  scale_y_continuous(labels = label_number(big.mark = ""), breaks = breaks_extended(8),
                     expand = expansion(mult = c(0, 0.02))) +
  coord_cartesian(xlim = rng) +
  labs(x = "People per sq. km.",
       y = sprintf("People living within %.1f%% of that density", window_pct)) +
  base_theme
# --- output ----------------------------------------------------------------
fig_title <- "Density profiles of cities"
if (requireNamespace("patchwork", quietly = TRUE)) {
  # Stack the two charts in one figure, like the reference.
  fig <- patchwork::wrap_plots(p1, p2, ncol = 1) +
    patchwork::plot_annotation(
      title   = fig_title,
      caption = src,
      theme   = theme(plot.title   = element_text(hjust = 0.5, size = 15),
                      plot.caption = element_text(colour = "grey40", size = 8)))
  print(fig)
  ggsave(out_png, fig, width = 10, height = 10.5, dpi = 300, bg = "white")
  message("Saved ", out_png)
} else {
  message("Install patchwork for the stacked figure: install.packages(\"patchwork\")")
  print(p1); print(p2)
}
ggsave(out_png1, p1 + ggtitle(fig_title) + theme(plot.title = element_text(hjust = 0.5)),
       width = 10, height = 5.8, dpi = 300, bg = "white")
ggsave(out_png2, p2 + ggtitle(fig_title) + theme(plot.title = element_text(hjust = 0.5)),
       width = 10, height = 5.8, dpi = 300, bg = "white")
message("Saved ", out_png1, " and ", out_png2)
# ---------------------------------------------------------------------------
# Notes
# * Chart 1 is the complementary cumulative distribution: every curve starts
#   at 100% on the left and falls to 0 at the urban area's densest tract. A curve
#   further right means more residents live at higher density.
# * Chart 2 is raw counts, so bigger urban areas sit higher (New York's
#   ~20 million people dwarf Las Vegas's ~2.2 million). For shape
#   comparison, divide `people` by each urban area's total population.
# * Multi-state urban areas (Chicago, New York, Boston) have one row per
#   state in `areas`; all their tracts land in the same curve.
# * Chart 2 is jagged by design: each tract enters and leaves the window as a
#   block of people. Raise window_pct (e.g. 15) for a smoother line.
# * Edge tracts: a tract whose interior point is inside the urban area counts
#   in full, including any rural land it holds outside the boundary, so the
#   low-density tail is slightly overstated. To check the boundary match, map
#   it: plot(st_geometry(ua)); plot(st_geometry(tr[tr$GEOID %in% df$GEOID, ]), add = TRUE)
# * People per mi² = people per km² x 2.59.
# ---------------------------------------------------------------------------


Change in Mode Share

Changing mode share or diverting auto trips to transit isn't the point. Getting people where they need to go is. Measure total trips, compare automobile delay with and without the transit improvement. Compare the cost of transit with the cost of freeway improvements needed to achieve similar outcomes in the next 30 years. Past a certain point, the math says "build transit", and once you are there, the game is just optimizing land use near transit.  

Wednesday, October 14, 2026

Access has economic value. Mobility has zero economic value.

"Mobility has inherent economic value and induced demand is made up of real people, making real choices they couldn't make before."

Access has economic value. Mobility has zero economic value. If I double both the speed and the distance of a trip, mobility has doubled, but access hasn't changed, and there has been zero economic benefit.

"That is just wrong. If people can travel longer distances at the same generalised cost, then they have more opportunities for where to live and work. This has value for them. That is an economic benefit".

In terms of short-term equilibrium, absolutely. However, mid-term (months) transportation behavior is characterized by the 'Triple Convergence' where people shift from other routes, other modes, and other trip-times, until the whole travel network reaches equilibrium at a near-identical travel distance. In the long-term (decades), any actual increases in mobility spur additional development at the periphery, until congestion again reaches equilibrium. In a long-term planning context, the argument is that is that we're better off increasing access to opportunities by clustering things in central locations, at higher densities, rather than relying on mobility improvements create through spending trillions building/maintaining limited access highway systems. Research shows the intuitive conclusion that the easier it is to drive miles, the more miles people drive. So when I'm talking about the lack of value, I'm talking about specific trips: if I double the distance I travel to buy food, but travel at twice the speed, I get no benefit, but I've doubled the public cost of in terms of traffic and pollution.


Wednesday, October 7, 2026

Homeowner cartels and urban political economics

The hermeneutical principle is "The Purpose of a System is What it Does". The economic principle is that people respond to incentives. 

A cartel is an association of suppliers with the purpose of maintaining prices at a high level. The only suppliers of land (they aren't making any more of it) are existing landowners, and for most municipal jurisdictions, that means the owner occupiers of single-family detached homes.

'Zoning is a conspiracy to protect property values'. Anyone who owns a house has a vested interest in preventing prices from decreasing, and from encouraging them to increase to a higher level. A mortgage locks in both the minimum acceptable level of price and the minimum rate of appreciation* (the mortgage rate). 

Prices are the equilibrium between supply and demand, so maintaining prices means ensuring supply grows no faster than demand. Where supply is elastic, and new housing can be readily supplied, home prices don't appreciate. The less readily equivalent housing can be supplied; the less elastic supply is. Locations being unique and space being finite, new housing is less readily-equivalent to existing housing, simply because it is more distant. The more distant something is, the less equivalent it is--even equally accessible locations offer access to different things. 

In a stagnant metropolitan region, housing supply matches housing demand and the price level is stable*. It is only in growing regions that the value of homes appreciates. It is the value of location that appreciates, not the decaying bundle of sticks and tar atop it. And the value of a location is proportional to the transportation costs of that location--central locations are always more valuable. Hence, assumptions of housing appreciation rely on the production of new housing in inferior locations.

So the homeowner cartel to maintain the value of their asset, they must maintain the value of their location but preventing additional supply.  Conceptually, doubling the number of houses available halves the premium of the location, simply because that premium is divided over more houses. (Empirically, the relationship is non-linear, because doubling the houses halves the land per house and the premium for a location isn't a 1:1 relationship to the number of houses).

Municipal policies depend on voter preferences, and all voters must be residents. When a new suburban municipality incorporates, land values don't support apartments, so all housing (and hence all voters) are home-owner households, are part of the cartel. Voters elect people who restrict supply, restricted supply fuels appreciation, and the cartel works for its members. 

The cartel can even permit some rental housing, as renters tend to be younger or poorer and hence less politically engaged, so there is minimal political conflict - the cabal remains hegemonic, and the only conflict is between developers (who want to build/sell apartments) and the homeowner cabal of voters, who don't want competitors for parking, views or public amenities. It's a very stable equilibrium, as the stock dominates and the financial incentives for density are low enough that developers stick to the low-hanging fruit. 

But inevitably, as the value of a location appreciates, so do the rents to live in that location, for both apartments and single-family rentals. Which, when it happens faster than wage growth, displaces existing renters to worse housing (more remote/lower quality). Still an equilibrium, but a less stable one, as fewer voters are members of the cartel. Higher rents are also destabilizing, as the support the development of new 'luxury' apartments, occupied by older, higher-income renters more likely to be politically active, and indifferent to homeowner cartel concerns about appreciation but sensitive to displacement. Worse yet, there is a substantial latent constituency of voters (otherwise politically marginal) that can be organized -- renters threatened by displacement by rising rents. 

To buy off this constituency, the classic homeowner cartel tactic is to offer rent control: buying off political support through subsidy. It has the added advantage of blunting the development of apartments by capping rent and hence expected return on investment. Converting existing rental housing into 'permanently affordable housing' aids and abets this, by preventing existing rentals from being refurbished into 'luxury apartments' which might house politically active renters. 

When homeowners cry "Manhattanization", it's not just a matter of density--it's the danger of a renter dominated polity indifferent or hostile to the political hegemony that underlies the homeowner cabal and continued property appreciation. 

Maintaining the political consensus to limit supply to guarantee appreciation requires restraining the political power of both renters and landlords. The first is achieved by ensuring renters are politically marginal: poor, young and characterized by short tenure. Over time, traditional rent control sabotages the second and third characteristics, as renters age in place. Contrast housing vouchers, available only to the needy, ensuring that renter residents are always poor (and hence politically disengaged). Zoning unpleasant locations (noxious, noisy, smelly or dangerous) such as near freeways or along arterial roadways supports rapid turnover. 

Homeowners don't disparage developers by accident: developers aren't part of the cartel. The mortgage rate they can support is set by the appreciation in rents, rather than directly by the appreciation in location as it is for the homeowner cartel. A municipality dominated by a homeowner cartel will be hostile to developers, and active in using regulation to constrain with unreasonable requirements for parking, facade articulation, and public improvements, while also delaying them with conditional approvals and design review. The explicit goal is both to ensure cartel members aren't negatively affected by new development, but the outcome is to limit the supply of new housing.

The real threat to the homeowner cartel is when developers make common cause with politically engaged renters to advocate for additional rental housing at the state level. Developers can supply the money for political advocacy, while politically engaged renters can rally an even larger latent constituency. At a municipal level, only resident renters can vote, and so only those in danger of displacement are willing to organize. But at the extra-municipal level, that constituency consists not just those threatened by displacement but anyone who has ever been displaced by rising rents and is hence sympathetic to existing renters. 

Another classic cartel tactic is to permit new development, but to try to ensure new entrants are aligned with the cartel by permitting only for-sale condos intended for owner-occupiers. 

The aims of the homeowner cartel are to:

  • * limit new supply
  • * marginalize renters 
  • * ensure any new housing is owner occupied

The tactics of the homeowner cartel are to:

  • * use regulation (zoning, lot sizes) to restrict supply
  • * use zoning to limit apartments to noxious locations
  • * use rent-control / affordability mandates to buy off renters and make new development unattractive
  • * convert existing market rental housing into income restricted housing
  • * prioritize apartments with politically marginal renters (young, poor, high-turnover)
  • * permit condos, but not apartments, despite identical physical form

*Interesting corollary is that if a mortgage sets the required appreciation rate, and the appreciation rate is zero (ie, in a stagnant metropolitan area), it implies lower future values, and hence lower present values, in the same way in which appreciation at a rate greater than the mortgage rate implies higher future values, and hence higher present values. Hence, in any stagnant metropolitan areas, the higher the interest rate, the greater the rate of depreciation in local housing prices. The magnitude of the effect is proportional to the number of people with new mortgages - older mortgages were financed (or refinanced) at lower rates. 


 








Friday, October 2, 2026

AI has broken the job market

I don't mean bad for job seekers. I mean functioning badly as a market--a mechanism of matching buyers and sellers, mediated via prices. 

A combination of AI and Workday makes applying for jobs badly very easy, which generates a lot of marginal applicants that still require review by HR. But the sheer number of candidates overwhelm HR folks, who fall back on easy-but-bad heuristics to filter out people they don't expect to be suitable. Degree completion (testing grit and intelligence) is a reasonable heuristic for junior staff. But it is an increasingly bad one for more senior roles, where experience is far more important. But Workday doesn't provide an easy tool to filter for "20 years of experience in X". In my field, professional organization credentials/certifications play that role - EIT/PE and AICP/FAICP are reliable indicators of seniority and capability, and (crucially) allows folks in HR to filter using a single uniform term.

By many metrics, modern job markets do much better on an aggregation basis--bringing together a lot of buyers and sellers in one place. Which is overwhelming. Given too much to choose from, analysis paralysis and decision fatigue are taking over. Further, HR folks are (like teachers) suffering from AI, which has undermined their traditional evaluation mechanisms. In the past, word processors and spell check eliminated the ability to use typing ability and/or spelling as a filtering heuristic. AI has destroyed the utility of the traditional cover letter and resume--a decent quality version of either can be produced with almost no effort by the applicant. 

Further, a bigger market makes 'product comparison' (who is actually good at what / who is good to work for) much more difficult [1]. And when information asymmetry prevails, a 'market for lemons' emerges: any hire might be a lemon, nobody wants to overpay, so salary offers are lowballs, so none of the 'peaches' are willing to switch jobs, reinforcing the dynamic [2]. And so the job market locks up, which is terrible for both buyers and sellers. 

Price comparison for labor is always difficult - people are non-fungible, and the smaller the organization, the less standard the role they perform, and the more the functions they undertake resemble the job description [3]. So there is a huge push to commoditize labor competencies using things like certificates and boot camps. It is, at best, a partial solution--degradation of any credential is inevitable. For-profit diploma mills emerged in response to the value of a college degree as a credential [4]. And any bootcamps [5] that relies on customers through-put has every incentive to maximize volume [6] and customer satisfaction, regardless of how that undermines the credentials of past bootcamps. In turn, the need for HR folks to evaluate the quality of bootcamps and mini-credentials is itself exhausting. 

The long-term solution for ghost-written resumes/cover letters has long been interviews, which can't be conducted at scale. So to proxy that, more and more applications have required mini-essays, but which are subject to the same 'grading problems' as resumes and cover letters - AI generates a volume of text that outstrips human capacity to evaluate, and so HR departments increasingly outsource cognitive labor at to AI [7]. AI is both fallible and a black box [8] so unknown errors abound. Further, evaluative AI capacity is poor - AI is trained, not educated. It is reliant on its training [10] and does not get better over time [11], regardless of how many resumes it reviews. So the AI HR has available to it is hardly better than keyword searches, with all the associated flaws. 

There is also the issue of deception. A certain amount of exaggeration is expected on a resume, but in addition to the market for lemons, it can generate a Red Queen's Race--people lie more and more simply to be considered. Anecdotally, in New York, the norm for bar-service is to claim an order of magnitude great experience - months for weeks, weeks for days. And in the context of things like Workfront, clearing (arbitrary) thresholds for applications to be considered by a human, deceiving the AI filter becomes a necessity. 

#----------------------------------------------------------------------------

[1] Remote work magnifies this. There may be 5-10 employers (half of which are marginal) within a reasonable commute of home. But remote work requires evaluating employers all over the world. (Which has engendered entirely new varieties of employment scams). Evaluation is difficult, and expensive in time and cognitive capacity. 

[2] The first thing medieval guilds (and trade fairs) do is mandate standard quality. Not because they like quality (they'd sell a lemon if they could get away from it) but because suspicion of low-quality drives down prices for everyone.

[3] Efforts to replace truck drivers with AV have foundered thus - much of what truck drivers do is not driving trucks but rather acting as a 'resolution agent' for immediate logistical problems (breakdowns, missing cargo, etc). 

[4] Elite institutions (i.e. Yale) resist degradation by being exclusive, following a quality-not-quantity approach to education. Even if they admit a moron, that moron will become an educated moron. 

[5] Awkwardly, the push to raise college graduation rates did not help: Trying to provide 'good value for money' for the public by reducing the drop-out rate seems attractive, but faced with a need to increase a statistic, the easiest way to achieve it is to lower standards.   "When a measure becomes a target, it ceases to be a good measure".  (Goodharts law). Reduction in graduation rates are merely a lagging indicator of a failure during the admissions process - people unlikely to be successful were admitted for not good reasons: a reasoning error conflating a mediating variable (college education) between grit/IQ and success as a causal variable, a cognitive error recognized by politely ignored because it enabled Type A academic Deans to build empires. 

[6] Likewise, the MBA approach of reducing costs by reducing quality until volumes suffer doesn't work. In a market, the presence of competitors denies unlimited degradation. For a monopsonic public provider of higher education, there is no limit to the degradation of value (as nutrition, art history and English majors can attest). 

[7] In the future, most work consists of evaluating the quality of AI outputs and "prompt engineering". This will drive both an explosion in the demand for such services, but also dramatic increase in the cost of non-AI services (Baumol's cost disease). A hairdresser who can evaluate AI outputs chooses alternative, more lucrative employment, and the price for the remaining hairdressers rises to reflect the reduced competition. 

[8] Asked to explain its reasoning, an AI makes up a post-hoc explanation that fits the facts and deludes itself into thinking that was always what it thought. A distinction that can only be ascertained by lawyerly questioning or the parent of a precocious child. But, like the child, AI is getting better at an uncertain rate, with both greater cognitive capacity, but also greater meta-textual awareness - AI is getting better at assessing the qualities of its lies and the person asking the question, and so is getting better at being more deceptive. Arguably, this recapitulates the development of human cognition, where humans then developed a counter-strategy: to punish deception with viciousness disproportionate to its immediate harmfulness. 

[9] AI isn't educated, but trained, and is reliant on its training to make inferences. The quality of that training data is degrading exponentially, as an ever-larger volume of web-scrapable material consists of AI produced content, and ever more of the good stuff is secured behind paywalls and access denied by robots.txt. (Evidence that AI is semi-legally harvesting user documents has been a long-term suspicion and the recent Navier-Stokes scandal practically validates the suspicion).  

[10] It is not by accident that Google is now buying books by the load and scanning them all - it's a source of training data web-scraping can't match. Even the most dated Windows 3.1 guide represents substantially better training data than web-scraped AI generated content, because it was written, edited, reviewed and published in a context where publication (printing, warehousing, distribution, sales) represented a substantial and risky investment.

[11] In the not-too-distant future, AI training data will include everything ever written before AI, and there will emerge and industry of people whose role is to provide AI with training data by reviewing the quality of its responses and suggesting improvements, based on expert judgment. AI owners are already trying to crowdsource this specific form of labor, which will in turn fall victim to the market-for-lemons problem in which they only people who respond are cranks or malicious. 


















Wednesday, September 30, 2026

Household type and transportation housing cost equilibrium

When apartments are in short supply, landlords outbid families for single family detached homes with yards, even while the premium offered for the yard is less. Three incomes beat two, even with substantial income differences. A family can only afford to outbid a roommate household where their transportation costs are lower: two cars beat three, even with substantial transportation differences. So new family housing gets produced where transportation costs are proportionally higher--at peripheral locations. Happily, this is also where regulation is least and hence supply most elastic. 

But for households that want/need lower transportation costs (and are willing/able to pay a premium for it), supply is much less elastic, as the land is already built up and adding new housing is more difficult. So to maintain affordability, households are forced to compromise on quality, accepting older and ore depreciated units. 

Depreciated housing is only acceptable on a temporary basis--as your income rises, you either have to be able to move up or fix up. And a fix-up is only worth doing if you can capture the value of that investment by enjoying it long-term. Hence, family households are forced into home ownership as a means to obtain decent housing in a decent location. 



What makes a market

 In the most basic village-produce-market sense, a market should enable:

  1. Buyer and sellers to congregate 
  2. Product comparison
  3. Competitive Pricing

Lacking any of those, and you don't have a market. 

There have to be multiple buyers and sellers. Monopoly/monopsony are classic market failures--a single seller can rise prices and lower quality without limit, while a single buyer can lower wages and prices without limit. Congregation has to take place in both place and time. One buyer per day is just a series of monopolists. Likewise, an entire convention center full of stores of unlike products is not a market--it's just collection of monopolists [1]. And prices have to be competitive--a group who has come together to set prices is called a cartel [2]. In the early medieval ages [3], there is an immediate return of 'market days' (for perishable commodities like foodstuff) and 'trade fairs' (for industrial commodities like wool) [4]. 


[1] Retail centers are deliberately set up this way, so that there are never two of the same type of store/restaurant/service, lest competition between the two drive down prices. 

[2] Unless it's a cartel to set the price of labor, in which case we call it a union. <grin>. But almost all unions emerge in a response to a monopsony on the purchase of labor--people won't suffer the costs (union dues, lost income) easily. 

[3] As soon as it becomes reasonable safe to travel and commerce returns. Crime (Banditry and piracy) drops to a reasonable level. 

[4] Places without which are reliant on peddlers, door-to-door salesmen who are crazily inefficient, but able to function because they can extract high prices by being a mobile monopoly. They are also afflicted by 'buyers' and 'agents' why are mobile monopsonists. 



Friday, September 25, 2026

An imagined conversation about livestreaming the TRB conference (and reviewing for it)

TRB Attendees: "We want to be able to attend remotely, watching presentations and attending meetings.

TRB: "We want people to come and attend the conference in person".

TRB Non-Attendees: "The cost of attendance (transportation, hotel, opportunity cost of time) doesn't justify coming in person; remote meetings and webinars are a norm, and there is no reason you can't do them. Some dude with a laptop livestreamed what he attended, so it's technically feasible". 

TRB: "We will no longer permit livestreaming, so there is more reason to come in person!"

TRB Non-Attendees: "Why are you doing this?"

TRB: "We signed a lease at the conference center, so we are locked in for the next ten years, and we need enough people to come to pay for it!". 

TRB Non-Attendees & TRB Attendees: :|

TRB: "Also, the conference center Wi-Fi is too weak..."

TRB Non-Attendees: That seems like a conference-center problem. Are they having trouble supplying running water too? Can you not afford a cellular connection for four hours a day for five days? 

TRB: "Also, the point of the conference is networking -- TRB is funded by gas-tax research funding, so we have to demonstrate value to DOT staff (who attend for free)". 

TRB Attendees: "So we pay $800 in case DOT staff might want to talk to us?"

TRB: "And see cutting edge research presented in poster and panel sessions. And if an academic, present your own research. And if a DOT, present your own innovations. Oh, and sell things to the attendees, and sell things to people selling things to the other attendees.  TRB relies a lot on conference sponsor funding, and sponsorship amounts are proportional to the size of our in-person audience--there is a reluctance to pay the same amount for sponsorship when there are only half as many attendees". 

TRB Non-Attendees: "How about a half-price ticket, since we're not receiving a lot of the benefits of attendance?"

TRB: "Calibrating how much that ticket should cost, given its propensity to cannibalize our other revenue (tickets, sponsorships, political influence) just doesn't seem worth the risk". 

TRB Non-Attendees: "Well, that means I'm not attending".

TRB: "That is a risk we can take -- we can quantify dollars needed in a way we can't quantify people who didn't attend. Attendance is already down post-COVID, so we can't risk it shrinking more".

TRB Non-Attendees: "The value of the alternatives (webinars) got better, so that's not unexpected".

TRB: "TRB committees, stop sponsoring webinars--we can't have people getting TRB benefits if they aren't coming to TRB". 

TRB Committees: "But wait, I thought we were supposed to popularize and distribute research?"

TRB: "Yes, but not like that". 

TRB Non-Attendees & TRB Attendees: :|

...

TRB: "We are having troubling finding sufficient reviewers for our papers!"

TRB Non-Attendees & TRB Attendees: :|

TRB Reviewers: "Seems like a bit of a waste, to do this review for something that's only going to be presented at a conference a few people can pay to attend, or a journal only a few people can pay to access. Why am I donating time to this again?"

TRB: "We are having trouble articulating a value proposition as well and are relying on academic norms of unpaid labor for peer review. That norm is collapsing and with it the number of peer reviewers and hence the quality of peer review- papers are only getting single reviewers". 

TRB Reviewers: "Have you heard of a 'desk reject'? All the main academic journals do it; TRB's commitment to review everything is actually quite strange". 

TRB: "Again, funded by gas tax money for research--TRB is an applied research organization. The thing your average DOT official submits aren't anything close to academic quality, but they don't need to be, because our intended end-users are DOT staff. They can be highly contextual and very niche. 

TRB Reviewers: "Sounds like your model has been highjacked by academics using it to provide guaranteed and timely feedback on academic articles that aren't useful to your end-users". 

TRB: "Yes, but it's so valuable for supporting early career researchers!"

TRB Reviewers: "Didn't you say your end user was a DOT staff member?"

TRB: "Everyone who gets accepted to present (panel or poster) has to register for TRB (albeit at reduced student price) and that does wonders for our bottom line and attendance numbers!"

/cynicism