<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ashleigh Giles</title>
    <description>The latest articles on DEV Community by Ashleigh Giles (@doornumber).</description>
    <link>https://dev.to/doornumber</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4234160%2F88d144ac-097f-424a-9f34-a73ab65e23ea.png</url>
      <title>DEV Community: Ashleigh Giles</title>
      <link>https://dev.to/doornumber</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/doornumber"/>
    <language>en</language>
    <item>
      <title>Joining HM Land Registry Price Paid Data to EPC certificates: address matching, failure modes and a CR5 worked example</title>
      <dc:creator>Ashleigh Giles</dc:creator>
      <pubDate>Mon, 09 Nov 2026 10:36:15 +0000</pubDate>
      <link>https://dev.to/doornumber/joining-hm-land-registry-price-paid-data-to-epc-certificates-address-matching-failure-modes-and-a-nmj</link>
      <guid>https://dev.to/doornumber/joining-hm-land-registry-price-paid-data-to-epc-certificates-address-matching-failure-modes-and-a-nmj</guid>
      <description>&lt;p&gt;If you want to analyse UK house prices by property type, floor area or build era, you have to join two open datasets that share no key: HM Land Registry Price Paid Data and the Energy Performance Certificate register. This post explains what each dataset contains, why the join is hard, the address-matching strategy that works, the failure modes to expect, and what the result looks like for postcode district CR5 (Coulsdon, Chipstead, Old Coulsdon, Hooley and Netherne-on-the-Hill). Figures were calculated on 9 November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each dataset contains
&lt;/h2&gt;

&lt;p&gt;HM Land Registry publishes Price Paid Data under the Open Government Licence v3.0. Each record is one sale and holds the price, the date of transfer, the postcode, the property type (D detached, S semi-detached, T terraced, F flat or maisonette, O other), whether the property is new build, the tenure (F freehold, L leasehold) and the address split into parts: PAON (primary addressable object name, usually the house number or name), SAON (secondary, usually a flat number), street, locality and town. It also carries a record status: A for added, C for changed and D for deleted, so a correct import handles updates and deletions and not only inserts.&lt;/p&gt;

&lt;p&gt;The Energy Performance Certificate register is published by the Ministry of Housing, Communities and Local Government. Each certificate holds the address, the postcode, the property type and built form, the total floor area in square metres, the construction age band, the energy rating from A to G, and a unique property reference number, the UPRN. Certificates exist only for homes that have been built, sold, let or improved since the scheme began, so some addresses have none, and a home can have several certificates over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the join is hard
&lt;/h2&gt;

&lt;p&gt;Price Paid Data has no stable property identifier. The EPC register has a UPRN, but a sale record does not, so the join has to be made on address text. HM Land Registry has said it will add UPRNs to Price Paid Data, which should make joins far easier, but historical data will still need address matching. Meanwhile the two datasets write addresses differently, and the differences are systematic, not random.&lt;/p&gt;

&lt;h2&gt;
  
  
  The matching strategy
&lt;/h2&gt;

&lt;p&gt;A reliable approach works in passes, from strictest to loosest, and stops at the first pass that matches.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Normalise both sides.&lt;/strong&gt; Upper-case everything, remove punctuation, collapse spaces, expand common abbreviations (RD to ROAD, CL to CLOSE), and strip the postcode of spaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match on postcode plus PAON plus SAON plus street.&lt;/strong&gt; This resolves most houses and many flats exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match on postcode plus house number and street&lt;/strong&gt;, ignoring locality. Many addresses differ only in the locality or town line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match flats on postcode plus the flat number and building name.&lt;/strong&gt; In EPC data a flat may be written as FLAT 3, 12 HIGH ROAD, while Price Paid Data holds SAON FLAT 3 and PAON 12, or just 3 and 12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fall back to the closest EPC for the street number&lt;/strong&gt;, flagging the match as low confidence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep a column recording which pass produced each match, so you can report the share of exact matches and exclude fuzzy ones from sensitive statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes to expect
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flats written in different orders.&lt;/strong&gt; FLAT 3, 12 HIGH ROAD in one dataset becomes 3 12 HIGH ROAD in the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;House names instead of numbers.&lt;/strong&gt; Homes called by name, such as a house name with no number, need a match on name and postcode, and some have changed name since the certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing certificates.&lt;/strong&gt; A property that has never been sold, let or improved since certificates began has none. In CR5, 265 of 2,672 addresses, which is 10%, have no construction age band for this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Several certificates for one home.&lt;/strong&gt; Use the most recent lodgement date, but keep the older ones if you want to study improvements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversions and splits.&lt;/strong&gt; A house converted into flats keeps one old certificate and several new ones, and sales before the conversion belong to a different property.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deleted and changed Price Paid records.&lt;/strong&gt; If you ignore status D you will count sales that were removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A CR5 worked example
&lt;/h2&gt;

&lt;p&gt;After joining, CR5 has 2,672 addresses and 1,398 recorded sales between 1 January 2019 and 24 July 2026. The median sold price is £500,000. The join lets you split that by property type and by construction age band.&lt;/p&gt;

&lt;p&gt;By type, the median is £690,000 for detached houses across 413 sales, £530,000 for semi-detached across 412 sales, £442,000 for end-terrace across 73 sales, £422,500 for mid-terrace across 165 sales and £300,000 for flats across 328 sales. By construction age band, 876 addresses, which is 33%, were built between 1930 and 1949, and the median by band is £530,000 for 1930 to 1949 (437 sales), £556,000 for 1950 to 1966 (167 sales), £480,000 for 1900 to 1929 (164 sales) and £425,000 for 2003 to 2006 (83 sales). Flats are 31% of the addresses built from 2003 to 2006 and only 7% of those built from 1930 to 1949, which is why the newest band has a lower median than the 1930s band.&lt;/p&gt;

&lt;p&gt;Without the EPC join you could not produce the second table at all, because Price Paid Data carries no age band. With it, you can explain why price follows built form, and the village pages built on it, such as &lt;a href="https://doornumber.co.uk/area/chipstead" rel="noopener noreferrer"&gt;Chipstead&lt;/a&gt; at £600,000 and &lt;a href="https://doornumber.co.uk/area/hooley" rel="noopener noreferrer"&gt;Hooley&lt;/a&gt; at £423,000, show a clear link between the share of flats and the village median.&lt;/p&gt;

&lt;h2&gt;
  
  
  Statistics pitfalls once joined
&lt;/h2&gt;

&lt;p&gt;Three mistakes recur. The first is mixing property types: compare detached with detached and flats with flats, because composition effects dominate. The neighbouring district &lt;a href="https://doornumber.co.uk/postcode-district/cr8" rel="noopener noreferrer"&gt;CR8&lt;/a&gt; has a lower overall median than CR5 (£455,000 against £500,000) even though detached, semi-detached and flat medians are all higher there, because flats are 44% of its sales against 23% in CR5. The second is ranking streets on tiny samples: a median from three sales moves a long way on one transaction, so require a minimum count and display it. The third is ignoring the publication delay: Price Paid Data is published with a lag, so the latest months are incomplete and a partial year looks like a fall in activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code sketch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^A-Z0-9 ]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bRD\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ROAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\bCL\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLOSE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;ppd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ppd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ppd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ppd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;saon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ppd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;street&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;epc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;epc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;epc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;saon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;epc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;street&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;epc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;epc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lodgement_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;drop_duplicates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;joined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ppd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epc&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;floor_area&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age_band&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uprn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_band&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notna&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matched to an EPC (per cent):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the later passes only on the rows still unmatched, and record the pass number in a column.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to validate the join
&lt;/h2&gt;

&lt;p&gt;A join that runs without errors can still be wrong, so test it. Start with the match rate by pass: if more than a few per cent of rows are matched only in the last pass, your normalisation is missing a pattern. Check property type agreement, because a sale recorded as a flat that joins to an EPC for a detached house is almost certainly a bad match. Compare floor area against price: a sale of a 40 square metre home at £1.5 million signals a mismatch or a data error. Look at the age band by street, because one street of 1930s semi-detached houses should not contain a 2012 band unless there is a new development on it. Finally, count addresses per street against an independent source, such as the street tables for &lt;a href="https://doornumber.co.uk/postcode-district/cr8/streets" rel="noopener noreferrer"&gt;CR8&lt;/a&gt; or &lt;a href="https://doornumber.co.uk/postcode-district/cr3/streets" rel="noopener noreferrer"&gt;CR3&lt;/a&gt;, to confirm you have not dropped or duplicated homes. In CR8 the join covers 3,892 addresses and 2,009 sales, and in CR3 3,853 addresses and 1,838 sales, which gives a sense of the scale a district-level join should reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions the joined data answers
&lt;/h2&gt;

&lt;p&gt;First, how does price vary with build era? Because the EPC register records construction age band, you can show that homes built from 1930 to 1949 sell for a median of £555,000 in CR8 while homes built from 2012 onwards sell for £425,000, with 82% of the newest addresses being flats. Second, how large is the premium for space? With floor area you can compute a price per square metre and compare it with the street median, which helps to separate a genuinely expensive street from one that simply has large houses. Third, which areas look expensive only because of their property mix? Village figures such as &lt;a href="https://doornumber.co.uk/area/woodcote" rel="noopener noreferrer"&gt;Woodcote&lt;/a&gt; at £600,000 and &lt;a href="https://doornumber.co.uk/area/purley" rel="noopener noreferrer"&gt;Purley&lt;/a&gt; at £427,500 need the type and age splits before they can be compared fairly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Price Paid Data include every sale?&lt;/strong&gt; No. It leaves out some transactions, such as non-market transfers, and recent months are filled in over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use the UPRN to join?&lt;/strong&gt; The EPC register has a UPRN, and HM Land Registry has announced that UPRNs will be added to Price Paid Data, which will simplify future joins. For historical sales you still need address matching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often are the datasets updated?&lt;/strong&gt; Price Paid Data is published monthly, and the EPC register is updated as new certificates are lodged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What match rate is realistic?&lt;/strong&gt; In the CR5 example about 90% of addresses have a construction age band. The remainder have no usable certificate or could not be matched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a median a valuation?&lt;/strong&gt; No. A median describes past sales and is not an estimate of what a particular home is worth today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licences and attribution
&lt;/h2&gt;

&lt;p&gt;Price Paid Data and the EPC register are both published under the Open Government Licence, and you should check the terms for the address fields in the EPC data before redistributing them. OpenStreetMap data, if you add places, is under the Open Database Licence and needs the credit "© OpenStreetMap contributors". The data sources and licences used for the examples above are listed on the &lt;a href="https://doornumber.co.uk/data-sources" rel="noopener noreferrer"&gt;data sources and licensing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to see the result
&lt;/h2&gt;

&lt;p&gt;Street-level tables built on this join are published for &lt;a href="https://doornumber.co.uk/postcode-district/cr5/streets" rel="noopener noreferrer"&gt;every street in CR5&lt;/a&gt;, and the &lt;a href="https://doornumber.co.uk/rankings" rel="noopener noreferrer"&gt;house price rankings&lt;/a&gt; compare streets, the biggest and oldest homes and districts by price and energy efficiency. For budgeting around a purchase, the free &lt;a href="https://doornumber.co.uk/tools" rel="noopener noreferrer"&gt;property tools&lt;/a&gt; calculate stamp duty and mortgage payments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Price Paid Data has the price and no attributes; the EPC register has the attributes and no price. Joining them on normalised address text, in passes from strict to loose, gives you a dataset that can answer questions neither source can alone. Expect about ten per cent of addresses to have no certificate, keep a confidence flag on every match, compare like with like, and always show the number of sales behind a median.&lt;/p&gt;

&lt;p&gt;About the author: &lt;a href="https://doornumber.co.uk" rel="noopener noreferrer"&gt;Door Number&lt;/a&gt; is a free property information site for England and Wales that builds its pages from open datasets and credits the source of every figure.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>python</category>
      <category>datascience</category>
      <category>uk</category>
    </item>
    <item>
      <title>Street-level median house prices from HM Land Registry Price Paid Data in Python</title>
      <dc:creator>Ashleigh Giles</dc:creator>
      <pubDate>Sun, 08 Nov 2026 23:05:19 +0000</pubDate>
      <link>https://dev.to/doornumber/street-level-median-house-prices-from-hm-land-registry-price-paid-data-in-python-9h0</link>
      <guid>https://dev.to/doornumber/street-level-median-house-prices-from-hm-land-registry-price-paid-data-in-python-9h0</guid>
      <description>&lt;p&gt;To get street-level median house prices in England and Wales, download HM Land Registry Price Paid Data, filter by postcode prefix, group by street and take the median, keeping only streets with enough sales. This tutorial does it with pandas in under 20 lines, then shows why the result needs a minimum sample size and a property-type split.&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://doornumber.co.uk" rel="noopener noreferrer"&gt;Door Number&lt;/a&gt;, which publishes street-level tables built on this data. Checked on 8 November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the Price Paid Data file look like?
&lt;/h2&gt;

&lt;p&gt;The yearly CSV files (pp-2019.csv to pp-2026.csv) have no header row. Each record has 16 fields, in this order: transaction ID, price, date of transfer, postcode, property type (D detached, S semi-detached, T terraced, F flat or maisonette, O other), old or new build (Y or N), tenure (F freehold, L leasehold), PAON (house number or name), SAON (flat number), street, locality, town or city, district, county, PPD category type (A or B) and record status (A added, C changed, D deleted). The data is published under the Open Government Licence v3.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you calculate the street medians?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postcode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ptype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenure&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;saon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;street&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;locality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;town&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;district&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;county&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pp-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2019&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2027&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_dates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cr5&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CR5 &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;na&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="n"&gt;streets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cr5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;street&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count &amp;gt;= 3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ascending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;streets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the trailing space in &lt;code&gt;startswith("CR5 ")&lt;/code&gt;. Every full UK postcode has a space after its outward code, so this selects exactly the CR5 district and nothing longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you split by property type?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;by_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cr5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ptype&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Splitting by type is not optional. On Door Number's CR5 dataset, the median detached sale since 2019 is £690,000 (413 sales) and the median flat is £300,000 (328 sales). A street of flats and a street of detached houses cannot be ranked together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the result look like?
&lt;/h2&gt;

&lt;p&gt;Door Number's published table, which joins sales to EPC data, puts Caversham Close at the top of CR5 with a median of £1,230,000 from six detached sales. Compare it with your own output, and expect small differences where street names are spelled differently across records. The full table is at &lt;a href="https://doornumber.co.uk/postcode-district/cr5/streets" rel="noopener noreferrer"&gt;every street in CR5 with sold prices&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which pitfalls catch people out?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Records with status D are deletions. If you merge monthly update files, drop them; the complete file already reflects them.&lt;/li&gt;
&lt;li&gt;Price Paid Data excludes some transactions, such as non-market sales, and the latest months are incomplete.&lt;/li&gt;
&lt;li&gt;Street names can be blank for some rural or named properties, so check for NaN before grouping.&lt;/li&gt;
&lt;li&gt;Medians from three sales are fragile. Show the count next to every median, as above.&lt;/li&gt;
&lt;li&gt;To compare postcodes or single homes rather than streets, the same grouping works on the full postcode column. See an example postcode page, &lt;a href="https://doornumber.co.uk/postcode/cr5-3lz" rel="noopener noreferrer"&gt;CR5 3LZ&lt;/a&gt;, or a single property record, &lt;a href="https://doornumber.co.uk/property/14-jennys-way-netherne-on-the-hill-cr5-1rp" rel="noopener noreferrer"&gt;14 Jennys Way, Netherne-on-the-Hill&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where else can you go from here?
&lt;/h2&gt;

&lt;p&gt;Join the sales to the EPC register for floor area and construction age band, or to Census 2021 for area context. HM Land Registry has announced that it will add UPRNs to Price Paid Data from 28 August 2026, which should make that join far easier. Door Number lists every dataset and licence on its &lt;a href="https://doornumber.co.uk/data-sources" rel="noopener noreferrer"&gt;data sources page&lt;/a&gt;, and has free calculators on its &lt;a href="https://doornumber.co.uk/tools" rel="noopener noreferrer"&gt;tools page&lt;/a&gt;. For national context, see &lt;a href="https://doornumber.co.uk/house-prices" rel="noopener noreferrer"&gt;house prices by region&lt;/a&gt; and the &lt;a href="https://doornumber.co.uk/rankings" rel="noopener noreferrer"&gt;national street rankings&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;About the author: Ashleigh Giles runs Door Number, which publishes house prices and local information from open UK data, with sources credited on every page.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opendata</category>
      <category>datascience</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Open UK property data explained: Price Paid, EPC, Census 2021 and OpenStreetMap</title>
      <dc:creator>Ashleigh Giles</dc:creator>
      <pubDate>Sun, 08 Nov 2026 22:32:57 +0000</pubDate>
      <link>https://dev.to/doornumber/open-uk-property-data-explained-price-paid-epc-census-2021-and-openstreetmap-362e</link>
      <guid>https://dev.to/doornumber/open-uk-property-data-explained-price-paid-epc-census-2021-and-openstreetmap-362e</guid>
      <description>&lt;p&gt;If you want to build anything with UK house price data, four free datasets do most of the work: HM Land Registry Price Paid Data, the EPC register, ONS Census 2021 and OpenStreetMap. This post covers what each one contains, how they are licensed, where they join badly, and what a street-level result looks like using postcode district CR5 (Coulsdon, in the London Borough of Croydon) as a worked example.&lt;/p&gt;

&lt;p&gt;I run Door Number, a free property information site built on these datasets. Everything below was checked on 8 November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does each dataset contain?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;Publisher&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Licence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price Paid Data&lt;/td&gt;
&lt;td&gt;HM Land Registry&lt;/td&gt;
&lt;td&gt;Every residential sale in England and Wales: price, date, postcode, property type, new build flag, freehold or leasehold, address parts&lt;/td&gt;
&lt;td&gt;Open Government Licence v3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EPC register&lt;/td&gt;
&lt;td&gt;MHCLG&lt;/td&gt;
&lt;td&gt;Energy certificates: floor area, construction age band, built form, energy rating, UPRN&lt;/td&gt;
&lt;td&gt;Open Government Licence (check the terms on address fields)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Census 2021&lt;/td&gt;
&lt;td&gt;ONS&lt;/td&gt;
&lt;td&gt;Population, households, tenure, travel, by output area and local authority&lt;/td&gt;
&lt;td&gt;Open Government Licence v3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenStreetMap&lt;/td&gt;
&lt;td&gt;OSM contributors&lt;/td&gt;
&lt;td&gt;Roads, buildings, shops, schools, transport&lt;/td&gt;
&lt;td&gt;Open Database Licence (ODbL), credit "© OpenStreetMap contributors"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Price Paid Data records property type as a single letter (D detached, S semi-detached, T terraced, F flat or maisonette, O other), tenure as F or L, and a record status of A (added), C (changed) or D (deleted), so a correct import has to handle updates and deletions, not only inserts. HM Land Registry has announced that it will add UPRNs to Price Paid Data from 28 August 2026, which should make joins to the EPC register far easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where do the datasets join badly?
&lt;/h2&gt;

&lt;p&gt;Price Paid Data has no stable identifier for a property, so matching a sale to an EPC means matching addresses: the PAON (house number or name), SAON (flat), street and postcode. Typical failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flats written "Flat 3, 12 High Road" in one dataset and "3 12 High Road" in another&lt;/li&gt;
&lt;li&gt;House names instead of numbers&lt;/li&gt;
&lt;li&gt;Properties with no EPC at all, because certificates only exist when a property was sold, let or improved&lt;/li&gt;
&lt;li&gt;Several certificates for one property, so you need the most recent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the CR5 dataset (2,672 addresses), 265 (10%) have no construction age band on the EPC register.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you learn once the joins are done?
&lt;/h2&gt;

&lt;p&gt;A worked example from CR5, using sales since 1 January 2019 (1,398 sales):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Median sold price, all property types: £500,000&lt;/li&gt;
&lt;li&gt;Detached £690,000 (413 sales), semi-detached £530,000 (412), flat £300,000 (328)&lt;/li&gt;
&lt;li&gt;33% of addresses (876 of 2,672) were built 1930 to 1949, according to the EPC construction age band&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the median is driven by the mix of property types, comparing two areas by their overall median can mislead. The neighbouring district CR8 (Purley and Kenley) has a lower overall median than CR5 (£455,000 against £500,000) even though detached homes, semis and flats each sell for more there, because flats are 44% of its sales against 23% in CR5.&lt;/p&gt;

&lt;h2&gt;
  
  
  What mistakes should you avoid?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not treat Price Paid Data as complete. It excludes some transactions, such as non-market sales, and recent months are filled in over time.&lt;/li&gt;
&lt;li&gt;Do not rank streets on tiny samples. A median from three sales moves a long way on one transaction; require a minimum count and show it.&lt;/li&gt;
&lt;li&gt;Do not mix property types in one comparison. Group by detached, semi-detached, terraced and flat first.&lt;/li&gt;
&lt;li&gt;Do state the period. A median since 2019 will not match a 12-month figure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;HM Land Registry Price Paid Data, EPC register (MHCLG), ONS Census 2021, OpenStreetMap, all as above. Street-level tables for CR5 are published at &lt;a href="https://doornumber.co.uk/postcode-district/cr5/streets" rel="noopener noreferrer"&gt;doornumber.co.uk/postcode-district/cr5/streets&lt;/a&gt;, and the data sources and licensing page lists every dataset used: &lt;a href="https://doornumber.co.uk/data-sources" rel="noopener noreferrer"&gt;doornumber.co.uk/data-sources&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;About the author: Ashleigh Giles runs &lt;a href="https://doornumber.co.uk" rel="noopener noreferrer"&gt;Door Number&lt;/a&gt;, which publishes house prices and local information from open UK data, with sources credited on every page.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>datascience</category>
      <category>uk</category>
      <category>property</category>
    </item>
  </channel>
</rss>
