UK house price changes in real terms
⋅ 13 minute read
Contents
This post is a response to a newspaper article I read this morning. Every month the FT is reporting on how the housing market in the UK is performing as people love to talk about it:
UK house prices rise less than expected in 2025 as growth slows
UK house prices rose by 0.6 per cent in 2025 after a slowdown at the end of the year, according to lender Nationwide. […] Prices fell 0.4 per cent between November and December to an average of £271,068. Both figures were below analysts’ expectations of a 1.2 per cent annual rise and a 0.1 per cent month-on-month expansion. FT Weekend (02.01.2026)
I am asking myself how useful this statistic is for the average reader, especially for a reader who wants to buy or sell a property.
Two issues are immediately obvious:
- The average yearly house price change across all of the UK might not agree with the house price performance in the areas I live in or I want to move to. There are also difference by house type, e.g. flat vs. detached house vs. terraced house. Different types of property might even perform in opposite directions.
- British people often view property, at least partially, as an investment opportunity. However, in those cases it would help to at least report the real (inflation-adjusted) house price change, not just the nominal change. An increase of 0.6% in nominal house prices sounds different when one doesn’t mention that inflation in 2025 was around 3.6% (UK CPI). Meanwhile other forms of investments did quite well. The FTSE 100 nominally returned 21.4% (which to be fair was a positive outlier this year compared to an average of 6% over the last 10 years).
Fortunately, it is not that difficult to compute real house prices more closely to my situation. The raw data is openly available via API:
- The HM Land Registry registers the ownership of property in the UK and provides aggregate monthly data of the average transactions by region and property type.
- The Office for National Statistics (ONS) provides monthly inflation data (CPI) for the UK.
So, in this post I want to compute and visualize how real flat and detached house prices in specific areas performed both nominally and in real terms.
Querying the housing price data
I am using the UK House Price Index of the Land Registry API:
http://landregistry.data.gov.uk/data/ukhpi/region
This allows me to query the monthly average house price value for different regions and property types. Important to note here is that this data is based on the succesful property transactions in a region which we use here as an approximation of the average value of similar properties in the region (that were or weren’t sold).
I am based in London and interested how the housing market performs close to me or areas that I am interested living in. So I will query the data for all London boroughs as well as some South England regions around London: Kent, Surrey, Buckinghamshire, and Oxfordshire.
I then resample the monthly data to quarterly data by taking the mean across the 3 months of a quarter.
I then store all the data in parquet files. You can find them here .
Inspecting the data
Let’s have a look at how the data is stored in the file:
import pandas as pd
import warnings
import matplotlib.pyplot as plt
pd.set_option('display.float_format', lambda x: '%.2f' % x)
warnings.simplefilter('always', category=UserWarning)
Here I am loading the file for the different counties that I am interested in:
df_regions = pd.read_parquet('./data/uk_house_prices.parquet')
df_regions['is_london_borough'] = False
df_regions.sample(5,ignore_index=True)
| Quarter | Region | Price | PropertyType | is_london_borough | |
|---|---|---|---|---|---|
| 0 | 2023Q4 | Surrey | 517233.67 | All | False |
| 1 | 2011Q3 | Kent | 192827.67 | All | False |
| 2 | 2014Q2 | Oxfordshire | 277195.00 | Semi_detached | False |
| 3 | 2013Q4 | London | 415838.00 | Semi_detached | False |
| 4 | 2021Q3 | Buckinghamshire | 431147.67 | All | False |
and here I am loading the file for the London boroughs:
df_boroughs = pd.read_parquet('./data/london_borough_prices.parquet')
df_boroughs['is_london_borough'] = True
df_boroughs.sample(5,ignore_index=True)
| Quarter | Region | Price | PropertyType | is_london_borough | |
|---|---|---|---|---|---|
| 0 | 2020Q4 | Hillingdon | 406610.67 | Terraced | True |
| 1 | 2024Q4 | Redbridge | 490087.67 | All | True |
| 2 | 2017Q2 | Lewisham | 910228.33 | Detached | True |
| 3 | 2011Q3 | Islington | 369223.67 | Flat | True |
| 4 | 2010Q3 | Enfield | 661067.33 | Detached | True |
You can see that the dataset shows the average quarterly housing transaction values by region. The column PropertyType shows what type of property was sold (Flat, Terraced, Detached, Semi_detached).
Computing real house prices
Next we adjust the nominal house prices from the Land Registry for inflation and compute the real house prices in terms of “2025 pounds”. I have queried the Office for National statistics API to get the UK’s quarterly CPI data and stored it in a parquet file. I am loading this data here:
cpi_df = pd.read_parquet('./data/cpi_data.parquet')
cpi_df.tail(5)
| Quarter | CPI | |
|---|---|---|
| 146 | 2024Q3 | 134.10 |
| 147 | 2024Q4 | 135.20 |
| 148 | 2025Q1 | 136.00 |
| 149 | 2025Q2 | 138.50 |
| 150 | 2025Q3 | 139.20 |
With the consumer price index, we can compute the real price of housing. This real price answers the question: “How much was the property worth in last quarter’s pounds?” If you experienced inflation, your money is worth less, and you will need to pay more for the same house in today’s money terms. If you experienced deflation, your money is worth more, and you will need to pay less for a house in today’s terms (all else equal).
The formula to compute the real house price in terms of the current CPI:
real_price_current = nominal_price_t * (CPI_current / CPI_t)
# Merging the dataframes here to do real price calculation in one step
df = pd.concat([df_regions, df_boroughs])
# Get the most recent CPI value
current_cpi = cpi_df['CPI'].iloc[-1]
latest_quarter = cpi_df['Quarter'].iloc[-1]
# Merge CPI df with house prices df
df = df.merge(cpi_df, on='Quarter', how='left')
# For quarters without CPI data yet, use the most recent CPI
df['CPI'] = df['CPI'].fillna(current_cpi)
# Calculate real prices using above formula
df['RealPrice'] = df['Price'] * (current_cpi / df['CPI'])
df.sample(5,ignore_index=True)
| Quarter | Region | Price | PropertyType | is_london_borough | CPI | RealPrice | |
|---|---|---|---|---|---|---|---|
| 0 | 2012Q3 | City Of Westminster | 2138137.67 | Semi_detached | True | 96.10 | 3097073.50 |
| 1 | 2011Q3 | Croydon | 280930.00 | Semi_detached | True | 93.80 | 416902.52 |
| 2 | 2016Q3 | Bromley | 898828.67 | Detached | True | 100.90 | 1240009.42 |
| 3 | 2017Q4 | Ealing | 1160573.67 | Detached | True | 104.60 | 1544472.80 |
| 4 | 2025Q2 | City Of London | 792392.33 | Flat | True | 138.50 | 796397.20 |
As you can see we now have the column RealPrice which gives us the mean house price transation per quarter and region in real terms. Now we can visualize the data to finally see what happened.
Visualizing nominal and real house prices over time
My goal is to plot the nominal and real house prices over time. To keep things visually simpler on the charts I am only selecting a few London boroughs that I am interested in and only look at data since 2015. For London boroughs I am looking at Flats and Terraced houses and for the counties I am only looking at detached houses.
df = df[df['Quarter'] >= '2015Q1'].copy()
SELECTED_BOROUGHS = [
'Ealing',
'Hackney',
'Hammersmith And Fulham',
'Hounslow',
'Islington',
'Richmond Upon Thames',
'Wandsworth',
]
df_boroughs = df[df['Region'].isin(SELECTED_BOROUGHS) & (df['PropertyType'].isin(['Flat', 'Terraced']))].copy()
df_regions = df[(df['is_london_borough'] == False) & (df['PropertyType'] == 'Detached')].copy()
In the file utils.py I added some code to create the plots and tables. You can safely skip this if not interested. Importing them here in the notebook:
from utils import create_table, plot_prices_by_type
Detached Houses in South England Counties
I am first looking at the house price performance of detached houses in five different English counties. I am plotting both the nominal prices (dashed line) and the real prices (solid line).
I am also creating a table with the real price changes vs. 1 year ago, 2 years ago, 5 years ago, 10 years ago.
fig, axes = plt.subplots(1, 1, figsize=(14, 8))
plot_prices_by_type(df_regions, 'Detached', axes, 'Price', 'Detached Houses in South England counties')
You can see that within the last 10 years most counties saw a decent increase in nominal terms and a decline in real house prices for detached properties. The only exception is Kent with a small real increase of 0.9%. London and Surrey both lost around 10% in real terms.
regions = sorted(df['Region'].unique())
gt_regions_flat = create_table(
df_regions, 'Detached',
'South England Counties',
regions
)
gt_regions_flat
| South England Counties | ||||||||||
| Detached properties - Real prices (CPI-adjusted) - Q4 2025 vs 1, 2, 5, 10 years ago | ||||||||||
| 2025 (Current) | 2024 (1y ago) | 2023 (2y ago) | 2020 (5y ago) | 2015 (10y ago) | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Price (£) | % | Price (£) | % | Price (£) | % | Price (£) | % | Price (£) | % | |
| Buckinghamshire | £853,221 | 0.0 | £883,178 | −3.4 | £894,115 | −4.6 | £974,110 | −12.4 | £924,706 | −7.7 |
| Kent | £605,363 | 0.0 | £620,638 | −2.5 | £633,552 | −4.4 | £692,871 | −12.6 | £599,898 | 0.9 |
| London | £1,132,911 | 0.0 | £1,178,785 | −3.9 | £1,179,862 | −4.0 | £1,331,199 | −14.9 | £1,265,328 | −10.5 |
| Oxfordshire | £680,124 | 0.0 | £675,726 | 0.7 | £676,613 | 0.5 | £757,910 | −10.3 | £723,218 | −6.0 |
| Surrey | £975,634 | 0.0 | £1,007,309 | −3.1 | £1,023,827 | −4.7 | £1,118,116 | −12.7 | £1,083,197 | −9.9 |
Flats in London Boroughs
Next, I am looking at flats in London. Again I am plotting nominal and real quarterly prices for selected London boroughs and create a table with the corresponding numbers.
fig, axes = plt.subplots(1, 1, figsize=(14, 8))
plot_prices_by_type(df_boroughs, 'Flat', axes, 'Price', 'Flats in selected London boroughs')
Both the chart and the table show somewhat stable or slightly increasing nominal prices, but drastic reductions in real prices. Both Hammersmith/Fulham and Wandsworth are down about 30% in real terms since 2015. Moreover compared to last year and two years ago we still see price declines in real terms.
regions = sorted(df['Region'].unique())
gt_regions_flat = create_table(
df_boroughs, 'Flat',
'Selected London Boroughs',
regions
)
gt_regions_flat
| Selected London Boroughs | ||||||||||
| Flat properties - Real prices (CPI-adjusted) - Q4 2025 vs 1, 2, 5, 10 years ago | ||||||||||
| 2025 (Current) | 2024 (1y ago) | 2023 (2y ago) | 2020 (5y ago) | 2015 (10y ago) | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Price (£) | % | Price (£) | % | Price (£) | % | Price (£) | % | Price (£) | % | |
| Ealing | £404,408 | 0.0 | £419,269 | −3.5 | £419,733 | −3.7 | £489,258 | −17.3 | £521,670 | −22.5 |
| Hackney | £546,932 | 0.0 | £554,931 | −1.4 | £583,301 | −6.2 | £685,095 | −20.2 | £667,622 | −18.1 |
| Hammersmith And Fulham | £577,801 | 0.0 | £639,856 | −9.7 | £707,815 | −18.4 | £760,674 | −24.0 | £883,098 | −34.6 |
| Hounslow | £367,011 | 0.0 | £375,391 | −2.2 | £373,890 | −1.8 | £441,401 | −16.9 | £419,450 | −12.5 |
| Islington | £566,442 | 0.0 | £599,386 | −5.5 | £623,593 | −9.2 | £725,791 | −22.0 | £771,081 | −26.5 |
| Richmond Upon Thames | £483,018 | 0.0 | £524,730 | −7.9 | £520,433 | −7.2 | £601,897 | −19.8 | £633,108 | −23.7 |
| Wandsworth | £540,034 | 0.0 | £568,240 | −5.0 | £603,492 | −10.5 | £715,552 | −24.5 | £761,426 | −29.1 |
Terraced houses in London Boroughs
Next, I am looking at terraced houses in the same London boroughs. My expectation is that they did better than flats, especially since they were more popular during the COVID pandemic, where people wanted a garden.
fig, axes = plt.subplots(1, 1, figsize=(14, 8))
plot_prices_by_type(df_boroughs, 'Terraced', axes, 'Price', 'Terraced houses in selected London boroughs')
Indeed, terraced houses did better in nominal terms with all of them up. Again in real terms we see price decreases over all considered comparison years (1,2,5,10). However, it is less drastic than for flats in the same locations.
regions = sorted(df['Region'].unique())
gt_regions_flat = create_table(
df_boroughs, 'Terraced',
'Selected London Boroughs',
regions
)
gt_regions_flat
| Selected London Boroughs | ||||||||||
| Terraced properties - Real prices (CPI-adjusted) - Q4 2025 vs 1, 2, 5, 10 years ago | ||||||||||
| 2025 (Current) | 2024 (1y ago) | 2023 (2y ago) | 2020 (5y ago) | 2015 (10y ago) | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Price (£) | % | Price (£) | % | Price (£) | % | Price (£) | % | Price (£) | % | |
| Ealing | £690,513 | 0.0 | £697,849 | −1.1 | £690,002 | 0.1 | £788,622 | −12.4 | £800,316 | −13.7 |
| Hackney | £972,274 | 0.0 | £966,520 | 0.6 | £1,006,975 | −3.4 | £1,158,935 | −16.1 | £1,073,030 | −9.4 |
| Hammersmith And Fulham | £1,111,278 | 0.0 | £1,221,472 | −9.0 | £1,334,009 | −16.7 | £1,392,109 | −20.2 | £1,547,731 | −28.2 |
| Hounslow | £625,089 | 0.0 | £622,316 | 0.4 | £612,967 | 2.0 | £704,956 | −11.3 | £637,839 | −2.0 |
| Islington | £1,124,856 | 0.0 | £1,175,162 | −4.3 | £1,214,208 | −7.4 | £1,380,766 | −18.5 | £1,380,624 | −18.5 |
| Richmond Upon Thames | £890,118 | 0.0 | £935,936 | −4.9 | £918,218 | −3.1 | £1,036,584 | −14.1 | £1,033,227 | −13.9 |
| Wandsworth | £965,788 | 0.0 | £1,000,163 | −3.4 | £1,050,418 | −8.1 | £1,214,261 | −20.5 | £1,219,378 | −20.8 |
Winners and Losers in real terms (last 3 years)
Lastly I want to see how the housing market in the different London boroughs compare to each other. I am looking at the prices 3 years ago across all property types. (At first I wanted to use 5 years, but I wanted to stay clear of the COVID years which might introduce short-term anomalies into the result). I am comparing real prices even though for a relative comparison between boroughs nominal prices would be fine as well (they all experienced the same inflation).
selected = df[(df['is_london_borough'] == True) & (df['Quarter'].isin(['2025Q4','2022Q4'])) & (df['PropertyType'] == 'All')]
df_wide = selected.pivot(index=['Region', 'PropertyType'], columns='Quarter', values='RealPrice').reset_index()
df_wide['%_real_price_change'] = (df_wide['2025Q4'] - df_wide['2022Q4']) / df_wide['2022Q4']
df_wide.sort_values('%_real_price_change', ascending=False,ignore_index=True).head(10)
| Quarter | Region | PropertyType | 2022Q4 | 2025Q4 | %_real_price_change |
|---|---|---|---|---|---|
| 0 | Lewisham | All | 529841.12 | 503542.00 | -0.05 |
| 1 | Havering | All | 479610.47 | 450760.00 | -0.06 |
| 2 | Southwark | All | 647219.21 | 607299.00 | -0.06 |
| 3 | Waltham Forest | All | 569427.61 | 533922.00 | -0.06 |
| 4 | Haringey | All | 697080.71 | 649650.00 | -0.07 |
| 5 | Hackney | All | 681376.13 | 627538.00 | -0.08 |
| 6 | Hounslow | All | 588525.22 | 540799.00 | -0.08 |
| 7 | Sutton | All | 493375.92 | 452726.00 | -0.08 |
| 8 | Hillingdon | All | 526316.99 | 482852.00 | -0.08 |
| 9 | Barking And Dagenham | All | 387895.94 | 354709.00 | -0.09 |
You can see that even the best performing borough Lewisham did saw decreasing real house prices of 5% in the last 3 years. Overall a lot of these top 10 boroughs are further outside of London and generally more affordable areas. Both Hackney and Southwark are more central, but have recently undergone gentrification which might have counteracted the price pressure in other central boroughs.
df_wide.sort_values('%_real_price_change', ascending=False,ignore_index=True).tail(10)
| Quarter | Region | PropertyType | 2022Q4 | 2025Q4 | %_real_price_change |
|---|---|---|---|---|---|
| 23 | Barnet | All | 706765.75 | 606006.00 | -0.14 |
| 24 | Camden | All | 928639.42 | 792454.00 | -0.15 |
| 25 | Croydon | All | 464581.92 | 393479.00 | -0.15 |
| 26 | Lambeth | All | 654875.76 | 550720.00 | -0.16 |
| 27 | Hammersmith And Fulham | All | 883837.60 | 741308.00 | -0.16 |
| 28 | Wandsworth | All | 834257.35 | 695867.00 | -0.17 |
| 29 | Tower Hamlets | All | 595217.88 | 470209.00 | -0.21 |
| 30 | Kensington And Chelsea | All | 1637183.90 | 1194726.00 | -0.27 |
| 31 | City Of Westminster | All | 1273329.16 | 889935.00 | -0.30 |
| 32 | City Of London | All | 993657.65 | 607399.00 | -0.39 |
Some of the worst performing boroughs over the last 3 years are the City of London, Westminster, Kensington & Chelsea, and Hammersmith and Fulham, which are all very expensive ‘prestige’ areas.
Conclusion
This analysis shows that a prospective home buyer or seller only gains limited information from a national-level house price statistic. Instead, I showed that there are, unsurprisingly, vast differences in price developments between property types and locations.
Moreover, in real terms almost none of the considered regions saw increasing house prices over the last 10 years (notable exception: Kent).
Appendix
Since someone asked, here are the real price changes compare to 3 years ago for all London boroughs:
df_wide.sort_values('%_real_price_change', ascending=False,ignore_index=True)
| Quarter | Region | PropertyType | 2022Q4 | 2025Q4 | %_real_price_change |
|---|---|---|---|---|---|
| 0 | Lewisham | All | 529841.12 | 503542.00 | -0.05 |
| 1 | Havering | All | 479610.47 | 450760.00 | -0.06 |
| 2 | Southwark | All | 647219.21 | 607299.00 | -0.06 |
| 3 | Waltham Forest | All | 569427.61 | 533922.00 | -0.06 |
| 4 | Haringey | All | 697080.71 | 649650.00 | -0.07 |
| 5 | Hackney | All | 681376.13 | 627538.00 | -0.08 |
| 6 | Hounslow | All | 588525.22 | 540799.00 | -0.08 |
| 7 | Sutton | All | 493375.92 | 452726.00 | -0.08 |
| 8 | Hillingdon | All | 526316.99 | 482852.00 | -0.08 |
| 9 | Barking And Dagenham | All | 387895.94 | 354709.00 | -0.09 |
| 10 | Merton | All | 677772.16 | 618798.00 | -0.09 |
| 11 | Harrow | All | 590633.55 | 538433.00 | -0.09 |
| 12 | Bromley | All | 581127.96 | 529749.00 | -0.09 |
| 13 | Redbridge | All | 524996.77 | 476764.00 | -0.09 |
| 14 | Richmond Upon Thames | All | 870795.06 | 785086.00 | -0.10 |
| 15 | Enfield | All | 533218.77 | 480183.00 | -0.10 |
| 16 | Bexley | All | 456886.19 | 410002.00 | -0.10 |
| 17 | Islington | All | 757742.76 | 677305.00 | -0.11 |
| 18 | Ealing | All | 645069.13 | 572575.00 | -0.11 |
| 19 | Greenwich | All | 533193.50 | 472599.00 | -0.11 |
| 20 | Brent | All | 639480.63 | 558093.00 | -0.13 |
| 21 | Kingston Upon Thames | All | 657636.32 | 573489.00 | -0.13 |
| 22 | Newham | All | 471479.67 | 405808.00 | -0.14 |
| 23 | Barnet | All | 706765.75 | 606006.00 | -0.14 |
| 24 | Camden | All | 928639.42 | 792454.00 | -0.15 |
| 25 | Croydon | All | 464581.92 | 393479.00 | -0.15 |
| 26 | Lambeth | All | 654875.76 | 550720.00 | -0.16 |
| 27 | Hammersmith And Fulham | All | 883837.60 | 741308.00 | -0.16 |
| 28 | Wandsworth | All | 834257.35 | 695867.00 | -0.17 |
| 29 | Tower Hamlets | All | 595217.88 | 470209.00 | -0.21 |
| 30 | Kensington And Chelsea | All | 1637183.90 | 1194726.00 | -0.27 |
| 31 | City Of Westminster | All | 1273329.16 | 889935.00 | -0.30 |
| 32 | City Of London | All | 993657.65 | 607399.00 | -0.39 |
Jupyter Notebook
You can find the jupyter notebook for this post here .
If you have any thoughts, questions, or feedback about this post, I would love to hear it. Please reach out to me via email.
Tags:#notebook #pandas #economics