UK house price changes in real terms

⋅ 13 minute read


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:

  1. 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.
  2. 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:

  1. 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.
  2. 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')

png

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')

png

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')

png

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  

Related: