Hur påverkar bergvärme priset på en bostad?

Vi undersöker hur bergvärme påverkar värdet på ett hus genom att se hur mycket felet i Boolis statistiska värdering skiljer sig mellan hus med bergvärme och hus utan bergvärme. För mer detaljer kring metoden kan du läsa ett tidigare blogginlägg i ämnet på https://jobb.booli.se/blog/posts/18473-statistiska-varderingar-som-matt-pa-forsaljningsresultat. I korthet går metoden ut på att vi gör statistiska värderingar av en stor mängd bostäder och sedan delar upp bostäderna i olika grupper för att se om grupperna skiljer sig åt.

Våra datakällor består av Boolis databas med information om bostäder och värderingar, samt brunnsarkivet från SGU. Brunnsarkivet finns på https://www.sgu.se/grundvatten/brunnar-och-dricksvatten/brunnsarkivet/ och innehåller information om var i landet det finns grävda brunnar och vad brunnarna används till. Vi har gjort antagandet att om en fastighet har en energibrunn så används den brunnen till bergvärme/-kyla.

Vi börjar med att läsa in datan som består av alla försäljningar med tillhörande statistiska värderingar i Sverige mellan 2016-06-01 och 2019-06-01. Sedan matchar vi de försäljningarna till brunnsdatan.

Inläsning av data

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Read residence to sold map and evaluation data
sold2residence = pd.read_csv('data/sold-res-all.csv')
evaluations = pd.read_csv('data/knn_all_ot_77104_20160601-20190601.csv')
evaluations = evaluations[evaluations['objectType'].isin(('Villa', 'Radhus', 'Parhus', 'Kedjehus'))]

# Merge datasets to map residenceId to an evaluation
evals_with_residences = evaluations.merge(sold2residence[['id', 'residenceId', 'areaId', 'areaName']],
                                          left_on='booliId', right_on='id', how='inner')

Vi har data om brunnar med fastighetsbeteckning, kopplat till Boolis bostads-ID, uppdelade i en fil per län. Så vi itererar varje fil och slår samman dem.

In [2]:
# Read well data
from pathlib import Path

partial_dfs = []
pathlist = Path('data/brunnar/').glob('*.csv')
for path in pathlist:
    df = pd.read_csv(str(path))
    partial_dfs.append(df)
wells = pd.concat(partial_dfs, axis=0, sort=False)
energy_wells = wells[wells['ANVANDNING'] == 'ENE'].copy()

Brunnsarkivet kan innehålla information om fler än 1 brunn per fastighet. Vi är intresserade av att undersöka om det funnits en energibrunn på fastigheten vid försäljningstillfället, så vi väljer ut den tidigast borrade brunnen och rensar bort information om senare borrade brunnar.

Informationen om vilket datum brunnarna borrades finns inte alltid, och när det finns så skiftar formatet något. Vi får skriva en funktion för att tolka datumen och göra om dem till datetime-objekt.

In [3]:
# Attempt to parse well drill dates
def parse_drill_date(date):
    parsed = np.nan
    if not np.isnan(date):
        datestring = str(int(date))
        if len(datestring) == 4:
            datestring = '{}0101'.format(datestring)
        elif len(datestring) == 6:
            datestring = '{}01'.format(datestring)
        try:
            parsed = pd.to_datetime(datestring)
        except:
            pass
    return parsed

energy_wells['drillDate'] = energy_wells['BORRDATUM'].apply(parse_drill_date)

När vi har datumen på datetime-format kan vi sortera vår dataframe på borrdatum och välja ut den tidigast borrade energibrunnen för varje fastighet.

In [4]:
# Sort by date
energy_wells = energy_wells.sort_values("drillDate", ascending=True)

# Keep only the oldest well for each cadastral
energy_wells = energy_wells.drop_duplicates(subset="cadastral", keep='first')
In [5]:
# Merge datasets
combined = evals_with_residences.merge(energy_wells, on='residenceId', how='left')
In [6]:
display("Antal försäljningar: {}".format(evaluations['booliId'].count()))
display("Antal fastighetsmatchade försäljningar: {}".format(evals_with_residences['booliId'].count()))
display("Antal brunnar: {}".format(wells['BRUNNS_ID'].count()))
display("Antal energibrunnar: {}".format(energy_wells['BRUNNS_ID'].count()))
display("Antal försäljningar efter brunnsmatchning: {}".format(combined['booliId'].count()))
'Antal försäljningar: 247672'
'Antal fastighetsmatchade försäljningar: 227430'
'Antal brunnar: 661628'
'Antal energibrunnar: 286279'
'Antal försäljningar efter brunnsmatchning: 227430'

Vi tittar kort på vår data för att verifiera att allt gått bra. Vi kan se att vårt ursprungliga dataset med försäljningspriser och värderingar var på ca 248 000 observationer av husförsäljningar. Efter en matchning mot fastighetsdata har vi kvar ca 227 000 obs.

I brunnsarkivet finns data om totalt ca 662 000 brunnar, varav ca 286 000 är energibrunnar.

Efter en matchning mellan fastighetsbeteckningarna i brunnsarkivet och Boolis försäljningsdata har vi kvar våra 227 000 obs, men nu kompletterad med information energibrunnar.

Förbehandling och filtrering

När vi har försäljningsdatum och borrdatum kan vi beräkna en binär feature som säger om det fanns en borrad brunn vid försäljningstillfället. Vi kallar kolumnen drilled_before_sale och använder den för vår analys.

In [7]:
# Cast solddate string to datetime object
combined['soldDate'] = pd.to_datetime(combined['soldDate'], format='%Y-%m-%d')

# Assign binary class to objects that had a well when they were sold
combined['has_energy_well'] = (~combined['BRUNNS_ID'].isna()) & (combined['ANVANDNING'] == 'ENE')
combined['drilled_before_sale'] = combined.apply(lambda x: x.soldDate > x.drillDate, axis=1)

Vi gör en grundläggande outlier-filtrering på försäljningspris och värderingsfel.

In [8]:
# Remove outliers based on soldPrice and estimation error
filtered = combined
quantiles = filtered['soldPrice'].quantile([0.01, 0.99])
filtered = filtered[(filtered['soldPrice'] >= quantiles[0.01]) &
                    (filtered['soldPrice'] <= quantiles[0.99])]
quantiles = filtered['error_percent'].quantile([0.05, 0.95])
filtered = filtered[(filtered['error_percent'] >= quantiles[0.05]) &
                    (filtered['error_percent'] <= quantiles[0.95])]
In [9]:
display(filtered.groupby('drilled_before_sale')['drilled_before_sale'].count())
drilled_before_sale
False    182859
True      17769
Name: drilled_before_sale, dtype: int64

Det ger oss totalt ca 200 000 observationer i vårt dataset av husförsäljningar, varav ca 18 000 hade bergvärme när de såldes.

Analys

När all data är på plats analyserar vi hur feldistributionen för hela populationen ser ut. Sedan undersöker vi om feldistributionerna skiljer sig åt mellan de olika grupperna. Vi vill också undersöka om det finns någon korrelation mellan förekomsten av bergvärme och geografi, tid och annan bostadsdata.

In [10]:
filtered['error_percent'].hist(bins=20)
display("Median absolute error: {} %".format(filtered['abs_error_percent'].median() * 100))
display(filtered['error_percent'].describe())
'Median absolute error: 13.566440662406626 %'
count    200628.000000
mean          0.030159
std           0.237572
min          -0.403403
25%          -0.128409
50%          -0.002232
75%           0.145479
max           0.842180
Name: error_percent, dtype: float64

Eftersom vi tittar på samtliga småhus i hela landet får vi, trots vår outlier-filtrering, ganska långa svansar med stora värderingsfel. Upp mot 80 % övervärdering eller 40 % undervärdering i de värsta fallen. Felkurvan är centrerad kring 3 %, och standardavvikelsen är ca 24 %, vilket innebär att vi i genomsnitt övervärderar med 3 % och att 2/3 av alla värderingar hamnar inom 24 % från rätt försäljningspris. Medianen av absolutvärdet på felet är ca 13,6 %.

Felkurvan är inte normalfördelad, utan ser snarare lognormal ut. Vi tittar vidare på hur felkurvorna ser ut för de olika klasserna.

Varierar felfördelningarna mellan de som har bergvärme och de som inte har?

In [11]:
f = plt.figure(figsize=(16,4))
f.subplots_adjust(hspace=0.4, wspace=0.4)

for i, label in enumerate(filtered['drilled_before_sale'].unique()):
    df = filtered[filtered['drilled_before_sale'] == label]
    ax = f.add_subplot(1, 2, i+1)
    ax.set_title("Har bergvärme = {} [SEK]".format(label))
    df['error_percent'].hist(bins=20)
    display("Har bergvärme = {}".format(label))
    print("Median absolute error: {} %".format(df['abs_error_percent'].median() * 100))
    print(df['error_percent'].describe())
'Har bergvärme = False'
Median absolute error: 13.529714285714286 %
count    182859.000000
mean          0.035826
std           0.238823
min          -0.403403
25%          -0.123103
50%           0.002423
75%           0.151988
max           0.842180
Name: error_percent, dtype: float64
'Har bergvärme = True'
Median absolute error: 13.881500000000003 %
count    17769.000000
mean        -0.028152
std          0.215820
min         -0.403359
25%         -0.176134
50%         -0.053261
75%          0.080416
max          0.840634
Name: error_percent, dtype: float64

Felkurvorna för båda klasser påminner mycket om varandra, båda har det lognormala utseendet. Felkurvan för de obs som har bergvärme är dock förskjuten till vänster, med ett medelfel på ca -3 %. Det innebär att bostäderna i den klassen sålts för mer än vår värdering. Medianen av absolutfelet är i samma storlek och standardavvikelserna är liknande i båda klasser.

Hur är bergvärmebrunnarna utspridda över tid?

In [12]:
from pandas.plotting import register_matplotlib_converters

register_matplotlib_converters()

drilled = filtered[filtered['drillDate'] >= pd.to_datetime('1976-01-01')]
drilled['drillDate'].hist(bins=30)
Out[12]:
<matplotlib.axes._subplots.AxesSubplot at 0x14a57eb10>

Borrdatumen för energibrunnar har spikar vid åren kring 2005 och 2017. Men det är en förhållandevis jämn spridning från början av 2000-talet fram till idag.

Finns det en geografisk komponent i var man har bergvärme?

In [13]:
import plotly.express as px
import plotly.graph_objects as go

px.set_mapbox_access_token(open(".mapboxtoken").read())

filtered['has_well'] = filtered['drilled_before_sale'].astype('str')
sthlm = filtered[filtered['areaName'] == 'Stockholms län']
fig = px.scatter_mapbox(sthlm,
                        lat="lat",
                        lon="lng",
                        color="has_well",
                        color_continuous_scale=px.colors.cyclical.IceFire,
                        size_max=0.3,
                        zoom=8)
fig.update_layout(mapbox_style='light')
fig.show()

För att inte överbelasta mapbox-klienten med markörer begränsar vi oss till Stockholms län och visualiserar hur den geografiska spridningen av bergvärme ser ut i länet.

Vi kan se att det är en ganska jämn spridning av bergvärmebrunnar över länet, men zoomar man in hittar man tydliga kluster där bergvärme helt saknas. T.ex. finns det ingen försäljning av hus med bergvärme i Bro, Brunna eller Kungsängen. Den här typen av lokala avvikelser förekommer här och var i kartan, både i tätbebyggda områden och i glesbygd. Exempel på mindre tätt bebygda områden som helt (eller till största del) saknar bergvärme är Torö i Nynäshamn, Brevikshalvön i Tyresö och Vätö i Norrtälje.

Att det ser ut så här skulle kunna bero på kommunala bestämmelser, då det är den kommunala miljömyndigheten som ger tillstånd för att borra energibrunnar. Det är inte visualiserat här men liknande mönster går att se i hela landet, med isolerade kluster av försäljningar där det inte finns bergvärme alls. Vi kommer inte göra någon djupare undersökning av varför, utan nöjer oss med att konstatera att förekomsten av bergvärme varierar geografiskt, och att det kan påverka tillförlitligheten i vår analys.

Är det vanligare med bergvärme för vissa prisklasser eller storleksklasser?

In [14]:
pearson_corr = filtered[[
    'drilled_before_sale',
    'soldPrice',
    'estimatedPrice',
    'plotArea',
    'livingArea',
    'additionalArea',
    'rooms',
    'constructionYear',
]].corr(method='pearson')

display(pearson_corr['drilled_before_sale'])
drilled_before_sale    1.000000
soldPrice              0.094489
estimatedPrice         0.075222
plotArea               0.004128
livingArea             0.133706
additionalArea         0.156904
rooms                  0.131729
constructionYear      -0.050244
Name: drilled_before_sale, dtype: float64

De parametrar som har ett korrelationsvärde över 0,1 relaterar till husets storlek. Korrelationen mellan bergvärme och tomtstorlek är nära 0.

Möjligen är det något vanligare med bergvärme för större hus, men med så låga korrelationsvärden måste slutsatsen bli att bergvärme är ungefär lika vanligt förekommande för alla tomtstorlekar och husstorlekar.

Är bergvärme värdehöjande?

Nu undersöker vi hur värderingsfelet varierar beroende på om fastigheterna har bergvärme och gör ett statistiskt test för att bedöma tillförlitligheten. Vi tittar också på hur felen varierar baserat på län.

In [15]:
filtered.groupby('drilled_before_sale').agg(
    Antal=('error', len),
    Medianfel_SEK=('error', np.median),
    Medelfel_SEK=('error', np.mean),
    Medianfel_procent=('error_percent', np.median),
    Medelfel_procent=('error_percent', np.mean)
)
Out[15]:
Antal Medianfel_SEK Medelfel_SEK Medianfel_procent Medelfel_procent
drilled_before_sale
False 182859.0 4521.0 -19231.668302 0.002423 0.035826
True 17769.0 -114833.0 -170602.241994 -0.053261 -0.028152
In [16]:
from scipy.stats import ttest_ind

with_well = filtered[filtered['drilled_before_sale'] == True]
without_well = filtered[filtered['drilled_before_sale'] == False]

display(ttest_ind(with_well['error_percent'], without_well['error_percent']))
Ttest_indResult(statistic=-34.371736819340995, pvalue=3.7822185521283983e-258)
In [17]:
filtered.groupby(['areaName', 'drilled_before_sale']).agg(
    Antal=('error', len),
    Medianfel_SEK=('error', np.median),
    Medelfel_SEK=('error', np.mean),
    Medianfel_procent=('error_percent', np.median),
    Medelfel_procent=('error_percent', np.mean)
)
Out[17]:
Antal Medianfel_SEK Medelfel_SEK Medianfel_procent Medelfel_procent
areaName drilled_before_sale
Blekinge län False 3772.0 8478.5 -33687.851007 0.007510 0.048441
True 156.0 -168092.5 -230840.256410 -0.089850 -0.048002
Dalarnas län False 6607.0 -9716.0 -45419.052520 -0.007660 0.035790
True 653.0 -125932.0 -157751.554364 -0.085965 -0.046959
Gotlands län False 1577.0 -28589.0 -59872.670260 -0.013077 0.025756
True 29.0 -313545.0 -359727.448276 -0.157653 -0.066374
Gävleborgs län False 5886.0 690.0 -29116.593952 0.000538 0.043165
True 852.0 -103671.5 -150896.431925 -0.082245 -0.039085
Hallands län False 7201.0 17013.0 2369.769615 0.006418 0.037473
True 582.0 -173406.0 -266757.056701 -0.051960 -0.028324
Jämtlands län False 3064.0 5382.5 -13297.301240 0.005976 0.051301
True 322.0 -128736.0 -183956.934783 -0.094673 -0.053484
Jönköpings län False 6003.0 4585.0 -21231.386473 0.003398 0.042434
True 1203.0 -82269.0 -130668.105569 -0.052765 -0.016836
Kalmar län False 6403.0 13146.0 -16991.364673 0.012257 0.054073
True 631.0 -85100.0 -173904.852615 -0.082693 -0.054306
Kronobergs län False 3645.0 20861.0 2952.136626 0.019089 0.054711
True 501.0 -90415.0 -127262.195609 -0.077848 -0.033450
Norrbottens län False 5710.0 31564.0 22786.942732 0.029288 0.066173
True 221.0 -133095.0 -160479.737557 -0.115340 -0.065623
Skåne län False 28815.0 7580.0 -29569.163526 0.003382 0.031227
True 485.0 -124405.0 -212431.148454 -0.061615 -0.037357
Stockholms län False 28378.0 -11943.0 -17383.171823 -0.002991 0.016273
True 2862.0 -184651.0 -192198.638714 -0.030689 -0.018051
Södermanlands län False 6199.0 -16061.0 -59787.304081 -0.007607 0.024297
True 876.0 -140294.5 -218424.360731 -0.062508 -0.032760
Uppsala län False 6825.0 41869.0 36327.597363 0.017262 0.046644
True 545.0 -88140.0 -153998.664220 -0.033554 -0.023285
Värmlands län False 6082.0 -1311.0 -22081.182341 -0.001092 0.048618
True 827.0 -66483.0 -99895.203144 -0.048419 -0.013406
Västerbottens län False 5202.0 10040.5 -21318.729527 0.008862 0.044800
True 347.0 -108997.0 -170874.100865 -0.079246 -0.046008
Västernorrlands län False 4791.0 3157.0 -17674.000209 0.004622 0.055007
True 604.0 -105441.0 -152214.937086 -0.092608 -0.049330
Västmanlands län False 5366.0 -17368.0 -43765.845322 -0.009578 0.020654
True 442.0 -182232.0 -208021.671946 -0.094344 -0.058582
Västra Götalands län False 28447.0 2632.0 -16493.752944 0.001233 0.034840
True 3563.0 -123176.0 -179746.452708 -0.047649 -0.020748
Örebro län False 5513.0 9372.0 -7664.359151 0.006946 0.046485
True 943.0 -80908.0 -112170.668081 -0.045186 -0.021725
Östergötlands län False 7373.0 9080.0 -21766.493829 0.003931 0.034019
True 1125.0 -115395.0 -153244.000889 -0.050545 -0.016935

Resultat

En jämförelse av alla försäljningar i landet visar att bergvärme är värt ca 115 000 SEK. Ett t-test ger ett p-värde i storleksordning 10-258. Det är en väldigt tydlig indikator på att det finns en skillnad mellan de här två distributionerna.

Grupperat per län kan vi se att siffrorna varierar en del över landet. Gotlands län sticker ut med ca 314 000 SEK undervärdering i medianfel. Men eftersom gruppen av försäljningar med bergvärme bara är 29 hus på Gotland är de siffrorna inte tillförlitliga, utan kan handla om slump.

För övriga 20 län kan vi se återkommande mönster. Om ett hus har bergvärme har det en tendens att överträffa Boolis statistiska värdering vid försäljning. Medianfelet uttryckt i procent varierar mycket, allt från 3 % i Stockholms län till närmre 12 % i Norrbotten. Medianfelet uttryckt i kronor ligger mer konstant. Det tyder på att värdeökningen av att installera bergvärme inte är korrelerad till husets värde, utan till kostnaden av att installationen.

Slutsats och kommentar

Slutsats: Bergvärme är värdehöjande.

Vi kan tydligt se att förekomsten av bergvärme korrelerar med en förhållandevis stor negativ avvikelse från medelfelet för hela populationen. D.v.s. vi värderar hus med bergvärme för lågt, vilket kan tolkas som att bergvärme är värdehöjande.

Exakt hur värdehöjande varierar över landet. Flest kronor får man i Stockholms och Västmanlands län, minst får man i Värmland och Örebro. Sett till snittet i landet får man ca 115 000 SEK mer för sitt hus om det har bergvärme.

Är resultaten tillförlitliga?

Det kan kan tänkas att förekomsten av bergvärme är korrelerat till andra kvaliteter som vi inte tar hänsyn till i våra statistiska värderingar. En hypotes är att bergvärme indikerar att skicket på huset i övrigt är högt, och att det är det goda skicket som är värt 115 000 SEK, inte bara bergvärmen. Den hypotesen är tyvärr svår att testa p.g.a. brist på data. Att borrdatumen för brunnarna är förhållandevis jämnt utspridda över en 20-årsperiod talar emot att bergvärme skulle korrelera till att huset är nyrenoverat. Men det kan finnas skicksvariationer som att t.ex. de mest nedgångna husen inte förekommer i gruppen av hus som har bergvärme, och att det kan ha en påverkan på felstatistiken som gör att analysen inte blir helt tillförlitlig.

Det finns också en geografisk variation i förekomsten av bergvärme, och det finns en möjlighet att bergvärme är vanligare i områden där vi generellt sett undervärderar.

Givet den stora volymen på datan, och de tydliga resultaten i våra tester, vill vi ändå påstå att bergvärme är värdehöjande. Men exakt hur värdehöjande ner på tusenlappen kan vi inte vara helt säkra på.

Är bergvärme värt investeringen?

Att installera bergvärme kostar ca 150 000 SEK innan ROT-avdrag enligt https://www.byggahus.se/varme/vad-kostar-bergvarme. Efter ROT-avdraget hamnar kostnaden på ca 117 000 SEK vilket ju stämmer väldigt väl överrens med siffrorna vi kom fram till i vår analys. Ovanpå det tillkommer installation av ett vattenburet värmesystem inne i huset (om man inte redan har det) som också kan kosta ganska mycket.

Funderar man på att installera bergvärme behöver man såklart göra en kostnadskalkyl utifrån sitt eget hus och sin egna mark. Men har man möjligheten indikerar vår analys att man kan få tillbaks en stor del av sin investering. Bergvärme brukar ju också sänka de löpande uppvärmningskostnaderna man har, och är bättre för klimatet än många andra uppvärmningsmetoder.

In [ ]: