FinObservatory

Housing cycles / methodology

Housing cycles methodology

Everything on /housing is computed at build time from four parquet tables. No figure is typed into a page. This document is the record of what was filtered, what was dropped, and what the result cannot support.

Sources

  • BIS residential property prices (bis.org/statistics/pp.htm), selected series. Quarterly, real and nominal. 57 countries have an ISO3 code and at least 40 quarters of the real index.
  • Laeven and Valencia, Systemic Banking Crises Database, 2025 update. The banking-crisis start and end years used as the events.
  • Jorda-Schularick-Taylor Macrohistory Database (macrohistory.net), 18 advanced economies, 1870 to 2020. Nominal house prices (hpnom), CPI, and the crisisJST flag.
  • Robert Shiller, Online Data (shillerdata.com), US home price index, 1890 onward, real and nominal.

The BIS filter, and why it is not optional

The BIS table stores four different series in one value column. unit takes the values Index, 2010 = 100 and Year-on-year changes, in per cent; value_type takes Nominal and Real. Filtering on one column and not the other averages an index level together with a percent change. Every query in src/lib/housing.ts filters on both, and matches the index unit by prefix so that a BIS rebase away from 2010 = 100 would change the pages rather than empty them:

WHERE iso3 IS NOT NULL AND length(iso3) = 3
  AND unit LIKE 'Index,%'
  AND value_type = 'Real'
  AND freq = 'Q'
  AND value IS NOT NULL

The year-on-year unit has no prefix that can be matched safely, so it is guarded instead: yoyUnit() checks that Year-on-year changes, in per cent is present in the file and throws at build time if it is not, rather than letting a renamed label silently empty every year-on-year cell on the site.

The iso3 condition drops the 4 aggregate series in the same file (Advanced economies [5R], Emerging market economies (aggregate) [4T], Euro area [XM], World [XW]), which have no ISO3 code. They are not countries; they get no page and enter no median.

Where a year-on-year growth rate is shown, it is read from the BIS Year-on-year changes, in per cent rows directly rather than differenced from the index.

Annual aggregation

The event study is annual, so the quarterly real index is collapsed to a year by taking the mean of its four quarters, with HAVING count(*) = 4. A year with fewer than four quarters is dropped entirely rather than averaged over whatever is present. Reporting lags differ: 26 of 57 countries currently report through 2026-Q1, the rest stop earlier (31 at 2025-Q4). The rule is written against the quarter count, never against a quarter label, so it keeps working when the vintage moves.

Sample selection: the completeness rule

An episode enters the event study only if the country’s annual real index exists at both t-5 and t+5, where t is the first year of the Laeven-Valencia banking crisis. This is deliberately strict. Without it, the median at t-5 would be a median over a different set of countries than the median at t+5, and the resulting path would move for reasons that have nothing to do with housing.

Laeven-Valencia dates 63 systemic banking crises across 49 of the 57 BIS countries. 29 of those 63 clear the rule, in 26 countries, between 1977 and 2011. Every one of the 29 is listed on the index page.

What is measured

  • Run-up: the annual real index at t divided by the annual real index at t-5, minus 1.
  • Trough vs t: the lowest annual real index in t through t+5, against the value at t. Zero means real prices never closed a year below the crisis-year level.
  • Peak to trough: the lowest annual real index in t through t+5 against the highest in t-5 through t.
  • Off own real peak (country pages, coverage table): the latest real quarter against the highest real quarter that country ever reported. It is conditional on the sample start: a peak before the index begins is invisible.

The 150-year cross-check

The BIS panel begins in 1947 at the earliest, so no crisis dated before 1952 can have a complete t-5 window. The same event study is therefore re-run on Jorda-Schularick-Taylor, where real house prices are hpnom deflated by cpi and the event is crisisJST = 1. The completeness rule is identical. It returns 54 episodes: 29 dated 1882 to 1939 and 25 dated 1974 to 2008, with none in between. The rule bites hardest early: JST dates 63 banking crises through 1945 and 29 of them have house prices five years either side, against 25 of 25 after 1945. Only the ratio to the crisis year is ever used, so the country-specific base of hpnom never matters.

Shiller

shiller_home_price_monthly is not monthly throughout: 63 of its rows have a null month and are annual observations (1890 to 1952), and 842 are genuinely monthly (1953 to 2023). Long-run growth is computed on annual means over complete years only (twelve monthly rows, or the one annual row of the early segment), which is valid across the whole span and drops the partial final year. The four monthly dates on the index page (2005-12, 2012-02, 2022-05 and the last observation, 2023-01) come from a query that filters to month IS NOT NULL, so it cannot reach outside the monthly segment.

The long-run drawdown on the index page is found, not asserted: a running peak over the annual series returns the deepest fall in the record (1894 to 1921, -47.1%), its peak year, and the first year the index closed back above that peak (1987). No year is written into the query.

Both inputs to the real index are spliced. The nominal index comes from 4 sources (Grebler 1890-1933, Five-CityMedian 1934-1952, PHCPI 1953-1974, S&P/CoreLogic/Case-Shiller 1975-2023) and the CPI deflator from 2 (Warren&Pearson 1890-1912, BLS 1913-2023), and the two seams do not fall in the same years. The real index is exactly the nominal index divided by that CPI and rebased to the first observation (1890 = 100). That is checked at build time, not assumed: reproducing it from the nominal and cpi columns of the same table matches the published real index over all 904 rows, to a maximum absolute error of 0.000000.

The page set

generateStaticParams is driven by the data, not by a country list: a country gets a page if and only if it has at least 40 quarters (10 years) of BIS real index. 57 countries qualify, and the thinnest series among them (Poland) has 64 quarters. The bar still runs on every build, so a country added later with three quarters of data cannot ship an empty page.

Limitations

  • Levels are meaningless across countries. 2010 = 100 is set independently per country. Nothing in this module compares levels across countries.
  • 19 of 29 episodes began in the same five years. They began between 2007 and 2011. The cross-country median is not 29 independent draws. The event-study table splits the median into those 19 and the 10 others.
  • Coverage is uneven. The index starts as early as 1947 and as late as 2010. 34 of the 63 banking crises in these countries have no complete window. 30 of them are in the 23 countries that have a crisis on record but no usable episode (BGR, BRA, CHL, CHN, CZE, EST, HRV, IDN, IND, ISR, LTU, LUX, LVA, MAR, MEX, MKD, PER, PHL, POL, ROU, SVK, SVN, TUR), and every one of those 30 is missing the year five years before the crisis. The remaining 4 sit in countries that do contribute at least one other episode. Every one of them is listed on its country page and marked not measurable, rather than dropped.
  • 8 countries have no crisis at all in Laeven-Valencia (AUS, CAN, HKG, MLT, NZL, SGP, SRB, ZAF). They get a page for their price history and contribute to no median. Absence from a chronology is the chronology’s judgement, not proof of stability.
  • Shiller is one country, its 1890-1952 segment is annual rather than monthly, and both the nominal index and the CPI under it are spliced series.
  • No identification. The crisis dates come from Laeven-Valencia, not from this price data, and nothing here separates cause from effect. This module reports co-timing.
  • Medians over episodes, not countries. A country with two qualifying crises contributes two observations to every median here.

Code

All queries: src/lib/housing.ts. The trap list is in the file header. Pages: src/app/housing/page.tsx, src/app/housing/[iso3]/page.tsx, and this file.