import pytest import pandas as pd import numpy as np from unittest.mock import Mock from recommendations.Recommendations import Recommendations @pytest.fixture def heat_demand_predictions(): return pd.DataFrame( [ {'id': '614626+0_phase=0', 'predictions': 256.6, 'property_id': '614626', 'recommendation_id': '0_phase=0', 'phase': 0}, {'id': '614626+1_phase=0', 'predictions': 256.6, 'property_id': '614626', 'recommendation_id': '1_phase=0', 'phase': 0}, {'id': '614626+2_phase=0', 'predictions': 256.6, 'property_id': '614626', 'recommendation_id': '2_phase=0', 'phase': 0}, {'id': '614626+3_phase=1', 'predictions': 263.1, 'property_id': '614626', 'recommendation_id': '3_phase=1', 'phase': 1}, {'id': '614626+4_phase=2', 'predictions': 259.0, 'property_id': '614626', 'recommendation_id': '4_phase=2', 'phase': 2}, {'id': '614626+5_phase=3', 'predictions': 250.5, 'property_id': '614626', 'recommendation_id': '5_phase=3', 'phase': 3}, {'id': '614626+6_phase=3', 'predictions': 245.7, 'property_id': '614626', 'recommendation_id': '6_phase=3', 'phase': 3}, {'id': '614626+7_phase=3', 'predictions': 199.7, 'property_id': '614626', 'recommendation_id': '7_phase=3', 'phase': 3}, {'id': '614626+8_phase=4', 'predictions': 250.5, 'property_id': '614626', 'recommendation_id': '8_phase=4', 'phase': 4}, {'id': '614626+9_phase=5', 'predictions': 139.5, 'property_id': '614626', 'recommendation_id': '9_phase=5', 'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 139.5, 'property_id': '614626', 'recommendation_id': '10_phase=5', 'phase': 5}, {'id': '614626+11_phase=5', 'predictions': 139.5, 'property_id': '614626', 'recommendation_id': '11_phase=5', 'phase': 5}, {'id': '614626+12_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '12_phase=5', 'phase': 5}, {'id': '614626+13_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '13_phase=5', 'phase': 5}, {'id': '614626+14_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '14_phase=5', 'phase': 5}, {'id': '614626+15_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '15_phase=5', 'phase': 5}, {'id': '614626+16_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '16_phase=5', 'phase': 5}, {'id': '614626+17_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '17_phase=5', 'phase': 5}, {'id': '614626+18_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '18_phase=5', 'phase': 5}, {'id': '614626+19_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '19_phase=5', 'phase': 5}, {'id': '614626+20_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '20_phase=5', 'phase': 5}, {'id': '614626+21_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '21_phase=5', 'phase': 5}, {'id': '614626+22_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '22_phase=5', 'phase': 5}, {'id': '614626+23_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '23_phase=5', 'phase': 5}, {'id': '614626+24_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '24_phase=5', 'phase': 5}, {'id': '614626+25_phase=5', 'predictions': 102.5, 'property_id': '614626', 'recommendation_id': '25_phase=5', 'phase': 5}, {'id': '614626+26_phase=5', 'predictions': 102.5, 'property_id': '614626', 'recommendation_id': '26_phase=5', 'phase': 5}, {'id': '614626+27_phase=5', 'predictions': 102.5, 'property_id': '614626', 'recommendation_id': '27_phase=5', 'phase': 5}, {'id': '614626+28_phase=5', 'predictions': 102.5, 'property_id': '614626', 'recommendation_id': '28_phase=5', 'phase': 5}, {'id': '614626+29_phase=5', 'predictions': 82.5, 'property_id': '614626', 'recommendation_id': '29_phase=5', 'phase': 5}, {'id': '614626+30_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '30_phase=5', 'phase': 5}, {'id': '614626+31_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '31_phase=5', 'phase': 5}, {'id': '614626+32_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '32_phase=5', 'phase': 5}, {'id': '614626+33_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '33_phase=5', 'phase': 5}, {'id': '614626+34_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '34_phase=5', 'phase': 5}, {'id': '614626+35_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '35_phase=5', 'phase': 5}, {'id': '614626+36_phase=5', 'predictions': 114.3, 'property_id': '614626', 'recommendation_id': '36_phase=5', 'phase': 5}, {'id': '614626+37_phase=5', 'predictions': 169.2, 'property_id': '614626', 'recommendation_id': '37_phase=5', 'phase': 5}, {'id': '614626+38_phase=5', 'predictions': 169.2, 'property_id': '614626', 'recommendation_id': '38_phase=5', 'phase': 5}, {'id': '614626+39_phase=5', 'predictions': 169.2, 'property_id': '614626', 'recommendation_id': '39_phase=5', 'phase': 5}, {'id': '614626+40_phase=5', 'predictions': 155.1, 'property_id': '614626', 'recommendation_id': '40_phase=5', 'phase': 5}, {'id': '614626+41_phase=5', 'predictions': 155.1, 'property_id': '614626', 'recommendation_id': '41_phase=5', 'phase': 5}, {'id': '614626+42_phase=5', 'predictions': 155.1, 'property_id': '614626', 'recommendation_id': '42_phase=5', 'phase': 5}, {'id': '614626+43_phase=5', 'predictions': 155.1, 'property_id': '614626', 'recommendation_id': '43_phase=5', 'phase': 5}, {'id': '614626+44_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '44_phase=5', 'phase': 5}, {'id': '614626+45_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '45_phase=5', 'phase': 5}, {'id': '614626+46_phase=5', 'predictions': 133.6, 'property_id': '614626', 'recommendation_id': '46_phase=5', 'phase': 5}, {'id': '614626+47_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '47_phase=5', 'phase': 5}, {'id': '614626+48_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '48_phase=5', 'phase': 5}, {'id': '614626+49_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '49_phase=5', 'phase': 5}, {'id': '614626+50_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '50_phase=5', 'phase': 5}, {'id': '614626+51_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '51_phase=5', 'phase': 5}, {'id': '614626+52_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '52_phase=5', 'phase': 5}, {'id': '614626+53_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '53_phase=5', 'phase': 5}, {'id': '614626+54_phase=5', 'predictions': 130.0, 'property_id': '614626', 'recommendation_id': '54_phase=5', 'phase': 5}, {'id': '614626+55_phase=5', 'predictions': 182.6, 'property_id': '614626', 'recommendation_id': '55_phase=5', 'phase': 5}, {'id': '614626+56_phase=5', 'predictions': 169.2, 'property_id': '614626', 'recommendation_id': '56_phase=5', 'phase': 5}, {'id': '614626+57_phase=5', 'predictions': 169.2, 'property_id': '614626', 'recommendation_id': '57_phase=5', 'phase': 5} ] ) @pytest.fixture def carbon_predictions(): return pd.DataFrame( [ {'id': '614626+0_phase=0', 'predictions': 2.2, 'property_id': '614626', 'recommendation_id': '0_phase=0', 'phase': 0}, {'id': '614626+1_phase=0', 'predictions': 2.2, 'property_id': '614626', 'recommendation_id': '1_phase=0', 'phase': 0}, {'id': '614626+2_phase=0', 'predictions': 2.2, 'property_id': '614626', 'recommendation_id': '2_phase=0', 'phase': 0}, {'id': '614626+3_phase=1', 'predictions': 2.2, 'property_id': '614626', 'recommendation_id': '3_phase=1', 'phase': 1}, {'id': '614626+4_phase=2', 'predictions': 2.2, 'property_id': '614626', 'recommendation_id': '4_phase=2', 'phase': 2}, {'id': '614626+5_phase=3', 'predictions': 2.1, 'property_id': '614626', 'recommendation_id': '5_phase=3', 'phase': 3}, {'id': '614626+6_phase=3', 'predictions': 2.1, 'property_id': '614626', 'recommendation_id': '6_phase=3', 'phase': 3}, {'id': '614626+7_phase=3', 'predictions': 1.4, 'property_id': '614626', 'recommendation_id': '7_phase=3', 'phase': 3}, {'id': '614626+8_phase=4', 'predictions': 2.1, 'property_id': '614626', 'recommendation_id': '8_phase=4', 'phase': 4}, {'id': '614626+9_phase=5', 'predictions': 1.3, 'property_id': '614626', 'recommendation_id': '9_phase=5', 'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 1.3, 'property_id': '614626', 'recommendation_id': '10_phase=5', 'phase': 5}, {'id': '614626+11_phase=5', 'predictions': 1.3, 'property_id': '614626', 'recommendation_id': '11_phase=5', 'phase': 5}, {'id': '614626+12_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '12_phase=5', 'phase': 5}, {'id': '614626+13_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '13_phase=5', 'phase': 5}, {'id': '614626+14_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '14_phase=5', 'phase': 5}, {'id': '614626+15_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '15_phase=5', 'phase': 5}, {'id': '614626+16_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '16_phase=5', 'phase': 5}, {'id': '614626+17_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '17_phase=5', 'phase': 5}, {'id': '614626+18_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '18_phase=5', 'phase': 5}, {'id': '614626+19_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '19_phase=5', 'phase': 5}, {'id': '614626+20_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '20_phase=5', 'phase': 5}, {'id': '614626+21_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '21_phase=5', 'phase': 5}, {'id': '614626+22_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '22_phase=5', 'phase': 5}, {'id': '614626+23_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '23_phase=5', 'phase': 5}, {'id': '614626+24_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '24_phase=5', 'phase': 5}, {'id': '614626+25_phase=5', 'predictions': 0.9, 'property_id': '614626', 'recommendation_id': '25_phase=5', 'phase': 5}, {'id': '614626+26_phase=5', 'predictions': 0.9, 'property_id': '614626', 'recommendation_id': '26_phase=5', 'phase': 5}, {'id': '614626+27_phase=5', 'predictions': 0.9, 'property_id': '614626', 'recommendation_id': '27_phase=5', 'phase': 5}, {'id': '614626+28_phase=5', 'predictions': 0.9, 'property_id': '614626', 'recommendation_id': '28_phase=5', 'phase': 5}, {'id': '614626+29_phase=5', 'predictions': 0.8, 'property_id': '614626', 'recommendation_id': '29_phase=5', 'phase': 5}, {'id': '614626+30_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '30_phase=5', 'phase': 5}, {'id': '614626+31_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '31_phase=5', 'phase': 5}, {'id': '614626+32_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '32_phase=5', 'phase': 5}, {'id': '614626+33_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '33_phase=5', 'phase': 5}, {'id': '614626+34_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '34_phase=5', 'phase': 5}, {'id': '614626+35_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '35_phase=5', 'phase': 5}, {'id': '614626+36_phase=5', 'predictions': 1.0, 'property_id': '614626', 'recommendation_id': '36_phase=5', 'phase': 5}, {'id': '614626+37_phase=5', 'predictions': 1.5, 'property_id': '614626', 'recommendation_id': '37_phase=5', 'phase': 5}, {'id': '614626+38_phase=5', 'predictions': 1.5, 'property_id': '614626', 'recommendation_id': '38_phase=5', 'phase': 5}, {'id': '614626+39_phase=5', 'predictions': 1.5, 'property_id': '614626', 'recommendation_id': '39_phase=5', 'phase': 5}, {'id': '614626+40_phase=5', 'predictions': 1.4, 'property_id': '614626', 'recommendation_id': '40_phase=5', 'phase': 5}, {'id': '614626+41_phase=5', 'predictions': 1.4, 'property_id': '614626', 'recommendation_id': '41_phase=5', 'phase': 5}, {'id': '614626+42_phase=5', 'predictions': 1.4, 'property_id': '614626', 'recommendation_id': '42_phase=5', 'phase': 5}, {'id': '614626+43_phase=5', 'predictions': 1.4, 'property_id': '614626', 'recommendation_id': '43_phase=5', 'phase': 5}, {'id': '614626+44_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '44_phase=5', 'phase': 5}, {'id': '614626+45_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '45_phase=5', 'phase': 5}, {'id': '614626+46_phase=5', 'predictions': 1.2, 'property_id': '614626', 'recommendation_id': '46_phase=5', 'phase': 5}, {'id': '614626+47_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '47_phase=5', 'phase': 5}, {'id': '614626+48_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '48_phase=5', 'phase': 5}, {'id': '614626+49_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '49_phase=5', 'phase': 5}, {'id': '614626+50_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '50_phase=5', 'phase': 5}, {'id': '614626+51_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '51_phase=5', 'phase': 5}, {'id': '614626+52_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '52_phase=5', 'phase': 5}, {'id': '614626+53_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '53_phase=5', 'phase': 5}, {'id': '614626+54_phase=5', 'predictions': 1.1, 'property_id': '614626', 'recommendation_id': '54_phase=5', 'phase': 5}, {'id': '614626+55_phase=5', 'predictions': 1.6, 'property_id': '614626', 'recommendation_id': '55_phase=5', 'phase': 5}, {'id': '614626+56_phase=5', 'predictions': 1.5, 'property_id': '614626', 'recommendation_id': '56_phase=5', 'phase': 5}, {'id': '614626+57_phase=5', 'predictions': 1.5, 'property_id': '614626', 'recommendation_id': '57_phase=5', 'phase': 5} ] ) @pytest.fixture def property_instance(): return Mock( id=614626, data={ "current-energy-efficiency": 65, "co2-emissions-current": 2.4, "energy-consumption-current": 284, "roof-energy-eff": "Good", "lighting-energy-eff": "Good", }, roof={ "is_loft": True, "insulation_thickness": "250", "is_valid": True, }, lighting={ "low_energy_proportion": 0.5 } ) @pytest.mark.parametrize( "input_data, expected", [ ( [ {"recommendation_id": "a", "phase": 0, "sap_adjustment": 1.7}, {"recommendation_id": "b", "phase": 0, "sap_adjustment": 1.7}, ], [{"recommendation_id": "a", "phase": 0, "sap_adjustment": 1.7}], ), ( [ {"recommendation_id": "a", "phase": 1, "sap_adjustment": 2}, {"recommendation_id": "b", "phase": 2, "sap_adjustment": 3}, ], [ {"recommendation_id": "a", "phase": 1, "sap_adjustment": 2}, {"recommendation_id": "b", "phase": 2, "sap_adjustment": 3}, ], ), ], ) def test_filter_phase_adjustment(input_data, expected): assert Recommendations._filter_phase_adjustment(input_data) == expected @pytest.mark.parametrize( "sap_impact, limit, expected", [ (1.0, -4, True), # positive SAP not allowed (0.0, -4, True), # zero not allowed (-1.0, -4, False), # valid range (-3.9, -4, False), # valid range (-4.0, -4, False), # exact lower bound allowed (-4.1, -4, True), # below lower bound ], ) def test_check_ventilation_out_of_bounds(sap_impact, limit, expected): assert Recommendations._check_ventilation_out_of_bounds( sap_impact, limit ) is expected @pytest.mark.parametrize( "sap_impact, limit, expected", [ (1.2, -4, -1), # positive → capped to -1 (0.0, -4, -1), # zero → capped to -1 (-5.0, -4, -4), # below limit → clamp (-3.0, -4, -3.0), # already valid → unchanged ], ) def test_adjust_ventilation_sap(sap_impact, limit, expected): assert Recommendations._adjust_ventilation_sap( sap_impact, limit ) == expected def test_get_previous_phase_values_starting_phase(property_instance): result = Recommendations._get_previous_phase_values( rec_phase=0, starting_phase=0, impact_summary=[], property_instance=property_instance, ) assert result == { "sap": 65.0, "carbon": 2.4, "heat_demand": 284.0, } def test_get_previous_phase_values_single_rep(property_instance): impact_summary = [ { "phase": 0, "representative": True, "sap": 66, "carbon": 2.2, "heat_demand": 260, } ] result = Recommendations._get_previous_phase_values( rec_phase=1, starting_phase=0, impact_summary=impact_summary, property_instance=property_instance, ) assert result["sap"] == 66 assert result["carbon"] == 2.2 assert result["heat_demand"] == 260 def test_get_previous_phase_values_median(property_instance): impact_summary = [ {"phase": 1, "representative": True, "sap": 70, "carbon": 2.0, "heat_demand": 250}, {"phase": 1, "representative": True, "sap": 74, "carbon": 1.6, "heat_demand": 230}, ] result = Recommendations._get_previous_phase_values( rec_phase=2, starting_phase=0, impact_summary=impact_summary, property_instance=property_instance, ) assert result["sap"] == np.median([70, 74]) assert result["carbon"] == np.median([2.0, 1.6]) assert result["heat_demand"] == np.median([250, 230]) def test_compute_phase_impact_standard(): previous = {"sap": 65, "carbon": 2.4, "heat_demand": 284} current = {"sap": 64, "carbon": 2.6, "heat_demand": 300} impact = Recommendations._compute_phase_impact( rec_type="loft_insulation", previous_phase_values=previous, current_phase_values=current, ) # monotonicity enforced assert impact["sap"] == 0 assert impact["carbon"] == 0 assert impact["heat_demand"] == 0 def test_compute_phase_impact_mechanical_ventilation(): previous = {"sap": 65, "carbon": 2.4, "heat_demand": 284} current = {"sap": 63, "carbon": 2.4, "heat_demand": 284} impact = Recommendations._compute_phase_impact( rec_type="mechanical_ventilation", previous_phase_values=previous, current_phase_values=current, ) assert impact["sap"] == -2 def test_resolve_current_phase_sap_with_adjustments(): rec = {"phase": 3, "survey": False} previous = {"sap": 65} phase_metrics = {"sap_change": 70} adjustments = [ {"phase": 1, "sap_adjustment": 1.5}, {"phase": 2, "sap_adjustment": 2.0}, ] sap = Recommendations._resolve_current_phase_sap( rec=rec, previous_phase_values=previous, phase_energy_efficiency_metrics=phase_metrics, adjustments=adjustments, ) assert sap == 70 - (1.5 + 2.0) def test_validate_recommendation_updates_raises(): rec = { "sap_points": None, "co2_equivalent_savings": None, "heat_demand": None, } with pytest.raises(ValueError): Recommendations._validate_recommendation_updates(rec) def test_calculate_recommendation_impact(property_instance, heat_demand_predictions, carbon_predictions): ####### # Case 3 ####### # Here, the solar impact falls below our threshold and so we expect a solar adjustment to increase the impact # above the minimum threshold all_predictions3 = { "sap_change_predictions": pd.DataFrame( [ {'id': '614626+0_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '0_phase=0', 'phase': 0}, {'id': '614626+1_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '1_phase=0', 'phase': 0}, {'id': '614626+2_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '2_phase=0', 'phase': 0}, {'id': '614626+3_phase=1', 'predictions': 65.3, 'property_id': '614626', 'recommendation_id': '3_phase=1', 'phase': 1}, {'id': '614626+4_phase=2', 'predictions': 66.3, 'property_id': '614626', 'recommendation_id': '4_phase=2', 'phase': 2}, {'id': '614626+5_phase=3', 'predictions': 67.3, 'property_id': '614626', 'recommendation_id': '5_phase=3', 'phase': 3}, {'id': '614626+6_phase=3', 'predictions': 68.1, 'property_id': '614626', 'recommendation_id': '6_phase=3', 'phase': 3}, {'id': '614626+7_phase=3', 'predictions': 70.1, 'property_id': '614626', 'recommendation_id': '7_phase=3', 'phase': 3}, {'id': '614626+8_phase=4', 'predictions': 67.3, 'property_id': '614626', 'recommendation_id': '8_phase=4', 'phase': 4}, {'id': '614626+9_phase=5', 'predictions': 85.3, 'property_id': '614626', 'recommendation_id': '9_phase=5', 'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 85.3, 'property_id': '614626', 'recommendation_id': '10_phase=5', 'phase': 5}, {'id': '614626+11_phase=5', 'predictions': 85.3, 'property_id': '614626', 'recommendation_id': '11_phase=5', 'phase': 5}, {'id': '614626+12_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '12_phase=5', 'phase': 5}, {'id': '614626+13_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '13_phase=5', 'phase': 5}, {'id': '614626+14_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '14_phase=5', 'phase': 5}, {'id': '614626+15_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '15_phase=5', 'phase': 5}, {'id': '614626+16_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '16_phase=5', 'phase': 5}, {'id': '614626+17_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '17_phase=5', 'phase': 5}, {'id': '614626+18_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '18_phase=5', 'phase': 5}, {'id': '614626+19_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '19_phase=5', 'phase': 5}, {'id': '614626+20_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '20_phase=5', 'phase': 5}, {'id': '614626+21_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '21_phase=5', 'phase': 5}, {'id': '614626+22_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '22_phase=5', 'phase': 5}, {'id': '614626+23_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '23_phase=5', 'phase': 5}, {'id': '614626+24_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '24_phase=5', 'phase': 5}, {'id': '614626+25_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '25_phase=5', 'phase': 5}, {'id': '614626+26_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '26_phase=5', 'phase': 5}, {'id': '614626+27_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '27_phase=5', 'phase': 5}, {'id': '614626+28_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '28_phase=5', 'phase': 5}, {'id': '614626+29_phase=5', 'predictions': 83.8, 'property_id': '614626', 'recommendation_id': '29_phase=5', 'phase': 5}, {'id': '614626+30_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '30_phase=5', 'phase': 5}, {'id': '614626+31_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '31_phase=5', 'phase': 5}, {'id': '614626+32_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '32_phase=5', 'phase': 5}, {'id': '614626+33_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '33_phase=5', 'phase': 5}, {'id': '614626+34_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '34_phase=5', 'phase': 5}, {'id': '614626+35_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '35_phase=5', 'phase': 5}, {'id': '614626+36_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '36_phase=5', 'phase': 5}, {'id': '614626+37_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '37_phase=5', 'phase': 5}, {'id': '614626+38_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '38_phase=5', 'phase': 5}, {'id': '614626+39_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '39_phase=5', 'phase': 5}, {'id': '614626+40_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '40_phase=5', 'phase': 5}, {'id': '614626+41_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '41_phase=5', 'phase': 5}, {'id': '614626+42_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '42_phase=5', 'phase': 5}, {'id': '614626+43_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '43_phase=5', 'phase': 5}, {'id': '614626+44_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '44_phase=5', 'phase': 5}, {'id': '614626+45_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '45_phase=5', 'phase': 5}, {'id': '614626+46_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '46_phase=5', 'phase': 5}, {'id': '614626+47_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '47_phase=5', 'phase': 5}, {'id': '614626+48_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '48_phase=5', 'phase': 5}, {'id': '614626+49_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '49_phase=5', 'phase': 5}, {'id': '614626+50_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '50_phase=5', 'phase': 5}, {'id': '614626+51_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '51_phase=5', 'phase': 5}, {'id': '614626+52_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '52_phase=5', 'phase': 5}, {'id': '614626+53_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '53_phase=5', 'phase': 5}, {'id': '614626+54_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '54_phase=5', 'phase': 5}, {'id': '614626+55_phase=5', 'predictions': 79.4, 'property_id': '614626', 'recommendation_id': '55_phase=5', 'phase': 5}, {'id': '614626+56_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '56_phase=5', 'phase': 5}, {'id': '614626+57_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '57_phase=5', 'phase': 5}] ), "heat_demand_predictions": heat_demand_predictions, "carbon_change_predictions": carbon_predictions, "hotwater_kwh_predictions": pd.DataFrame([]), "heating_kwh_predictions": pd.DataFrame([]), } recommendations3 = { 614626: [ [ { 'phase': 0, 'type': 'loft_insulation', 'measure_type': 'loft_insulation', 'sap_points': 0, 'survey': False, 'recommendation_id': '0_phase=0', 'co2_equivalent_savings': np.float64(0.19999999999999973), 'heat_demand': np.float64(27.399999999999977)}, ], [ { 'phase': 1, 'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation', 'sap_points': np.float64(-1.4000000000000057), 'heat_demand': np.float64(-6.5), 'kwh_savings': 0, 'co2_equivalent_savings': np.float64(0.0), 'energy_cost_savings': 0, 'innovation_rate': 0.0, 'recommendation_id': '3_phase=1', 'efficiency': 0} ], [ { 'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting', 'already_installed': False, 'sap_points': 5, 'kwh_savings': 164.25, 'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0), 'total': 10.5, 'contingency': 2.73, 'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True, 'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023)} ], [ { 'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3, 'total': 70, 'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336, 'vat': 11.666666666666664, 'labour_hours': 0.5, 'labour_days': 1, 'sap_points': np.float64(1.0), 'already_installed': False, 'innovation_rate': 0.0, 'recommendation_id': '5_phase=3', 'efficiency': 70, 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5) }, { 'type': 'heating', 'phase': 3, 'measure_type': 'time_temperature_zone_control', 'total': 604.5840000000001, 'contingency': 60.45840000000001, 'contingency_rate': 0.1, 'subtotal': 571.32, 'vat': 33.264, 'labour_hours': 3.08, 'labour_days': np.float64(1.0), 'sap_points': np.float64(1.8), 'already_installed': False, 'recommendation_id': '6_phase=3', 'efficiency': 604.5840000000001, 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(13.300000000000011) }, { 'phase': 3, 'type': 'heating', 'measure_type': 'air_source_heat_pump', 'sap_points': np.float64(3.8), 'already_installed': False, 'total': 17144.924, 'contingency': 4195.5434000000005, 'contingency_rate': 0.35, 'vat': 33.264, 'labour_hours': 83.08, 'labour_days': np.float64(11.0), 'innovation_rate': 0, 'recommendation_id': '7_phase=3', 'efficiency': 17144.924, 'co2_equivalent_savings': np.float64(0.8000000000000003), 'heat_demand': np.float64(59.30000000000001) } ], [ {'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating', 'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0, 'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0, 'labour_days': np.float64(1.0), 'innovation_rate': 0.0, 'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0), 'heat_demand': np.float64(0.0)} ], [ { 'phase': 5, 'type': 'solar_pv', 'measure_type': 'solar_pv', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(16.0), 'already_installed': False, 'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315, 'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48, 'labour_days': 2, 'has_battery': False, 'initial_ac_kwh_per_year': np.float64(4844.465553999999), 'innovation_rate': 0.0, 'recommendation_id': '29_phase=5', 'efficiency': np.float64(368.263125) } ] ] } representative_recommendations3 = { 614626: [ { 'phase': 0, 'type': 'loft_insulation', 'measure_type': 'loft_insulation', 'sap_points': 0, 'already_installed': False, 'total': 1029.0, 'contingency': 102.9, 'contingency_rate': 0.1, 'labour_hours': 8, 'labour_days': 1, 'survey': False, 'innovation_rate': 0.0, 'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587), 'co2_equivalent_savings': np.float64(0.19999999999999973), 'heat_demand': np.float64(27.399999999999977) }, { 'phase': 1, 'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation', 'starting_u_value': None, 'new_u_value': None, 'already_installed': False, 'sap_points': np.float64(-1.4000000000000057), 'heat_demand': np.float64(-6.5), 'kwh_savings': 0, 'co2_equivalent_savings': np.float64(0.0), 'energy_cost_savings': 0, 'total': 560.0, 'labour_hours': 8, 'labour_days': 1.0, 'innovation_rate': 0.0, 'recommendation_id': '3_phase=1', 'efficiency': 0}, { 'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting', 'already_installed': False, 'sap_points': 5, 'kwh_savings': 164.25, 'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0), 'total': 10.5, 'contingency': 2.73, 'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True, 'innovation_rate': 0.0, 'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023) }, { 'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3, 'total': 70, 'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336, 'vat': 11.666666666666664, 'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(1.0), 'already_installed': False, 'innovation_rate': 0.0, 'recommendation_id': '5_phase=3', 'efficiency': 70, 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5) }, { 'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating', 'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0, 'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0, 'labour_days': np.float64(1.0), 'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0), 'heat_demand': np.float64(0.0)}, { 'phase': 5, 'type': 'solar_pv', 'measure_type': 'solar_pv', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(16.0), 'already_installed': False, 'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315, 'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48, 'labour_days': 2, 'has_battery': False, 'innovation_rate': 0.0, 'recommendation_id': '29_phase=5', 'efficiency': np.float64(368.263125) } ] } recommendations_with_impact3, impact_summary3, adjustments3 = ( Recommendations.calculate_recommendation_impact( property_instance=property_instance, all_predictions=all_predictions3, recommendations=recommendations3, representative_recommendations=representative_recommendations3, debug=True ) ) # We expect adjustments for loft insulation, lighting and solar assert adjustments3 == [ {'recommendation_id': '0_phase=0', 'phase': 0, 'sap_adjustment': np.float64(1.7)}, {'recommendation_id': '4_phase=2', 'phase': 2, 'sap_adjustment': np.float64(4.0)}, {'recommendation_id': '29_phase=5', 'phase': 5, 'sap_adjustment': np.float64(-2.5)} ] # Check the impact has slowed through to solar - the final on the impact summary. The 5 # point prediction isn't associated to the prediction from the model so the adjustment # should be df = all_predictions3["sap_change_predictions"] raw_prediction = 83.8 # We expect 1.7 decrease from loft, 4 decrease from lighting, and 2.5 increase from solar # for a total of a 3.2 decrease expected_adjusted_prediction = raw_prediction - 3.2 assert impact_summary3[-1]["sap"] == expected_adjusted_prediction def test_loft_adjustment_flows_to_solar(property_instance, heat_demand_predictions, carbon_predictions): ######################## # Case 1 ######################## # Just an adjustment to loft insulation sap_change_predictions = pd.DataFrame( [ {'id': '614626+0_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '0_phase=0', 'phase': 0}, {'id': '614626+1_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '1_phase=0', 'phase': 0}, {'id': '614626+2_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '2_phase=0', 'phase': 0}, {'id': '614626+3_phase=1', 'predictions': 65.3, 'property_id': '614626', 'recommendation_id': '3_phase=1', 'phase': 1}, {'id': '614626+4_phase=2', 'predictions': 66.3, 'property_id': '614626', 'recommendation_id': '4_phase=2', 'phase': 2}, {'id': '614626+5_phase=3', 'predictions': 67.3, 'property_id': '614626', 'recommendation_id': '5_phase=3', 'phase': 3}, {'id': '614626+6_phase=3', 'predictions': 68.1, 'property_id': '614626', 'recommendation_id': '6_phase=3', 'phase': 3}, {'id': '614626+7_phase=3', 'predictions': 70.1, 'property_id': '614626', 'recommendation_id': '7_phase=3', 'phase': 3}, {'id': '614626+8_phase=4', 'predictions': 67.3, 'property_id': '614626', 'recommendation_id': '8_phase=4', 'phase': 4}, {'id': '614626+9_phase=5', 'predictions': 85.3, 'property_id': '614626', 'recommendation_id': '9_phase=5', 'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 85.3, 'property_id': '614626', 'recommendation_id': '10_phase=5', 'phase': 5}, {'id': '614626+11_phase=5', 'predictions': 85.3, 'property_id': '614626', 'recommendation_id': '11_phase=5', 'phase': 5}, {'id': '614626+12_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '12_phase=5', 'phase': 5}, {'id': '614626+13_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '13_phase=5', 'phase': 5}, {'id': '614626+14_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '14_phase=5', 'phase': 5}, {'id': '614626+15_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '15_phase=5', 'phase': 5}, {'id': '614626+16_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '16_phase=5', 'phase': 5}, {'id': '614626+17_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '17_phase=5', 'phase': 5}, {'id': '614626+18_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '18_phase=5', 'phase': 5}, {'id': '614626+19_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '19_phase=5', 'phase': 5}, {'id': '614626+20_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '20_phase=5', 'phase': 5}, {'id': '614626+21_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '21_phase=5', 'phase': 5}, {'id': '614626+22_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '22_phase=5', 'phase': 5}, {'id': '614626+23_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '23_phase=5', 'phase': 5}, {'id': '614626+24_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '24_phase=5', 'phase': 5}, {'id': '614626+25_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '25_phase=5', 'phase': 5}, {'id': '614626+26_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '26_phase=5', 'phase': 5}, {'id': '614626+27_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '27_phase=5', 'phase': 5}, {'id': '614626+28_phase=5', 'predictions': 86.7, 'property_id': '614626', 'recommendation_id': '28_phase=5', 'phase': 5}, {'id': '614626+29_phase=5', 'predictions': 83.8, 'property_id': '614626', 'recommendation_id': '29_phase=5', 'phase': 5}, {'id': '614626+30_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '30_phase=5', 'phase': 5}, {'id': '614626+31_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '31_phase=5', 'phase': 5}, {'id': '614626+32_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '32_phase=5', 'phase': 5}, {'id': '614626+33_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '33_phase=5', 'phase': 5}, {'id': '614626+34_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '34_phase=5', 'phase': 5}, {'id': '614626+35_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '35_phase=5', 'phase': 5}, {'id': '614626+36_phase=5', 'predictions': 86.4, 'property_id': '614626', 'recommendation_id': '36_phase=5', 'phase': 5}, {'id': '614626+37_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '37_phase=5', 'phase': 5}, {'id': '614626+38_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '38_phase=5', 'phase': 5}, {'id': '614626+39_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '39_phase=5', 'phase': 5}, {'id': '614626+40_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '40_phase=5', 'phase': 5}, {'id': '614626+41_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '41_phase=5', 'phase': 5}, {'id': '614626+42_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '42_phase=5', 'phase': 5}, {'id': '614626+43_phase=5', 'predictions': 83.4, 'property_id': '614626', 'recommendation_id': '43_phase=5', 'phase': 5}, {'id': '614626+44_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '44_phase=5', 'phase': 5}, {'id': '614626+45_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '45_phase=5', 'phase': 5}, {'id': '614626+46_phase=5', 'predictions': 85.5, 'property_id': '614626', 'recommendation_id': '46_phase=5', 'phase': 5}, {'id': '614626+47_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '47_phase=5', 'phase': 5}, {'id': '614626+48_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '48_phase=5', 'phase': 5}, {'id': '614626+49_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '49_phase=5', 'phase': 5}, {'id': '614626+50_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '50_phase=5', 'phase': 5}, {'id': '614626+51_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '51_phase=5', 'phase': 5}, {'id': '614626+52_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '52_phase=5', 'phase': 5}, {'id': '614626+53_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '53_phase=5', 'phase': 5}, {'id': '614626+54_phase=5', 'predictions': 85.4, 'property_id': '614626', 'recommendation_id': '54_phase=5', 'phase': 5}, {'id': '614626+55_phase=5', 'predictions': 79.4, 'property_id': '614626', 'recommendation_id': '55_phase=5', 'phase': 5}, {'id': '614626+56_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '56_phase=5', 'phase': 5}, {'id': '614626+57_phase=5', 'predictions': 81.2, 'property_id': '614626', 'recommendation_id': '57_phase=5', 'phase': 5}] ) all_predictions = { "sap_change_predictions": sap_change_predictions, "heat_demand_predictions": heat_demand_predictions, "carbon_change_predictions": carbon_predictions, "hotwater_kwh_predictions": pd.DataFrame([]), "heating_kwh_predictions": pd.DataFrame([]), } recommendations = { 614626: [ [ { 'phase': 0, 'type': 'loft_insulation', 'measure_type': 'loft_insulation', 'sap_points': 0, 'already_installed': False, 'contingency_rate': 0.1, 'labour_hours': 8, 'labour_days': 1, 'survey': False, 'innovation_rate': 0.0, 'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587), 'co2_equivalent_savings': np.float64(0.19999999999999973), 'heat_demand': np.float64(27.399999999999977)}, ], [ { 'phase': 1, 'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation', 'already_installed': False, 'sap_points': np.float64(-1.4000000000000057), 'heat_demand': np.float64(-6.5), 'kwh_savings': 0, 'co2_equivalent_savings': np.float64(0.0), 'energy_cost_savings': 0, 'total': 560.0, 'labour_hours': 8, 'labour_days': 1.0, 'innovation_rate': 0.0, 'recommendation_id': '3_phase=1', 'efficiency': 0} ], [ {'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting', 'new_u_value': None, 'already_installed': False, 'sap_points': 1, 'kwh_savings': 164.25, 'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0), 'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True, 'innovation_rate': 0.0, 'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023)} ], [ { 'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3, 'total': 70, 'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336, 'vat': 11.666666666666664, 'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(1.0), 'already_installed': False, 'recommendation_id': '5_phase=3', 'efficiency': 70, 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5) }, { 'type': 'heating', 'phase': 3, 'measure_type': 'time_temperature_zone_control', 'total': 604.5840000000001, 'contingency': 60.45840000000001, 'contingency_rate': 0.1, 'subtotal': 571.32, 'vat': 33.264, 'labour_hours': 3.08, 'labour_days': np.float64(1.0), 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(1.8), 'already_installed': False, 'recommendation_id': '6_phase=3', 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(13.300000000000011) }, { 'phase': 3, 'type': 'heating', 'measure_type': 'air_source_heat_pump', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(3.8), 'already_installed': False, 'total': 17144.924, 'contingency': 4195.5434000000005, 'contingency_rate': 0.35, 'vat': 33.264, 'labour_hours': 83.08, 'labour_days': np.float64(11.0), 'innovation_rate': 0, 'recommendation_id': '7_phase=3', 'efficiency': 17144.924, 'co2_equivalent_savings': np.float64(0.8000000000000003), 'heat_demand': np.float64(59.30000000000001) } ], [ {'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0, 'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0, 'labour_days': np.float64(1.0), 'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0), 'heat_demand': np.float64(0.0)} ], [ { 'phase': 5, 'type': 'solar_pv', 'measure_type': 'solar_pv', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(16.0), 'already_installed': False, 'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315, 'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48, 'labour_days': 2, 'has_battery': False, 'initial_ac_kwh_per_year': np.float64(4844.465553999999), 'innovation_rate': 0.0, 'recommendation_id': '29_phase=5', 'efficiency': np.float64(368.263125) } ] ] } representative_recommendations = { 614626: [ { 'phase': 0, 'type': 'loft_insulation', 'measure_type': 'loft_insulation', 'starting_u_value': np.float64(0.17), 'new_u_value': np.float64(0.14), 'sap_points': 0, 'already_installed': False, 'contingency_rate': 0.1, 'labour_hours': 8, 'labour_days': 1, 'survey': False, 'innovation_rate': 0.0, 'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587), 'co2_equivalent_savings': np.float64(0.19999999999999973), 'heat_demand': np.float64(27.399999999999977) }, { 'phase': 1, 'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation', 'starting_u_value': None, 'new_u_value': None, 'already_installed': False, 'sap_points': np.float64(-1.4000000000000057), 'heat_demand': np.float64(-6.5), 'kwh_savings': 0, 'co2_equivalent_savings': np.float64(0.0), 'energy_cost_savings': 0, 'total': 560.0, 'labour_hours': 8, 'labour_days': 1.0, 'recommendation_id': '3_phase=1', 'efficiency': 0}, { 'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting', 'starting_u_value': None, 'new_u_value': None, 'already_installed': False, 'sap_points': 1, 'kwh_savings': 164.25, 'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0), 'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True, 'innovation_rate': 0.0, 'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023) }, { 'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3, 'total': 70, 'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336, 'vat': 11.666666666666664, 'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(1.0), 'already_installed': False, 'recommendation_id': '5_phase=3', 'efficiency': 70, 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5) }, { 'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0, 'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0, 'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0), 'heat_demand': np.float64(0.0)}, { 'phase': 5, 'type': 'solar_pv', 'measure_type': 'solar_pv', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(16.0), 'already_installed': False, 'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315, 'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48, 'labour_days': 2, 'has_battery': False, 'recommendation_id': '29_phase=5', } ] } recommendations_with_impact, impact_summary, adjustments = ( Recommendations.calculate_recommendation_impact( property_instance=property_instance, all_predictions=all_predictions, recommendations=recommendations, representative_recommendations=representative_recommendations, debug=True ) ) # We expect an adjustment to be made for loft insulation, reducing the impact by # 1.7 assert adjustments == [{'recommendation_id': '0_phase=0', 'phase': 0, 'sap_adjustment': np.float64(1.7)}] # We expect that adjustment to flow through to the final recommendation so that the solar recommendation has # a 1.7 sap point reduction in impact final_impact_summary = impact_summary[-1] assert float(final_impact_summary["sap"]) == 82.1 assert float(final_impact_summary["sap_prediction"]) == 83.8 assert final_impact_summary["measure_type"] == "solar_pv" assert recommendations_with_impact[0][0]["sap_points"] == 0 def test_lighting_and_loft_adjustment_combined(property_instance, heat_demand_predictions, carbon_predictions): ######################## # Case 2 ######################## # Example case with both a loft insulation and lighting adjustment # lighting now has a SAP point impact of 5 - the affected recommendation is # recommendation_id=4_phase=2 all_predictions2 = { "sap_change_predictions": pd.DataFrame( [ {'id': '614626+0_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '0_phase=0', 'phase': 0}, {'id': '614626+1_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '1_phase=0', 'phase': 0}, {'id': '614626+2_phase=0', 'predictions': 66.7, 'property_id': '614626', 'recommendation_id': '2_phase=0', 'phase': 0}, {'id': '614626+3_phase=1', 'predictions': 65.3, 'property_id': '614626', 'recommendation_id': '3_phase=1', 'phase': 1}, {'id': '614626+4_phase=2', 'predictions': 71.3, 'property_id': '614626', 'recommendation_id': '4_phase=2', 'phase': 2}, {'id': '614626+5_phase=3', 'predictions': 72.3, 'property_id': '614626', 'recommendation_id': '5_phase=3', 'phase': 3}, {'id': '614626+6_phase=3', 'predictions': 73.1, 'property_id': '614626', 'recommendation_id': '6_phase=3', 'phase': 3}, {'id': '614626+7_phase=3', 'predictions': 75.1, 'property_id': '614626', 'recommendation_id': '7_phase=3', 'phase': 3}, {'id': '614626+8_phase=4', 'predictions': 72.3, 'property_id': '614626', 'recommendation_id': '8_phase=4', 'phase': 4}, {'id': '614626+9_phase=5', 'predictions': 90.3, 'property_id': '614626', 'recommendation_id': '9_phase=5', 'phase': 5}, {'id': '614626+10_phase=5', 'predictions': 85.3, 'property_id': '614626', 'recommendation_id': '10_phase=5', 'phase': 5}, {'id': '614626+11_phase=5', 'predictions': 90.3, 'property_id': '614626', 'recommendation_id': '11_phase=5', 'phase': 5}, {'id': '614626+12_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '12_phase=5', 'phase': 5}, {'id': '614626+13_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '13_phase=5', 'phase': 5}, {'id': '614626+14_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '14_phase=5', 'phase': 5}, {'id': '614626+15_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '15_phase=5', 'phase': 5}, {'id': '614626+16_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '16_phase=5', 'phase': 5}, {'id': '614626+17_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '17_phase=5', 'phase': 5}, {'id': '614626+18_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '18_phase=5', 'phase': 5}, {'id': '614626+19_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '19_phase=5', 'phase': 5}, {'id': '614626+20_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '20_phase=5', 'phase': 5}, {'id': '614626+21_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '21_phase=5', 'phase': 5}, {'id': '614626+22_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '22_phase=5', 'phase': 5}, {'id': '614626+23_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '23_phase=5', 'phase': 5}, {'id': '614626+24_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '24_phase=5', 'phase': 5}, {'id': '614626+25_phase=5', 'predictions': 91.7, 'property_id': '614626', 'recommendation_id': '25_phase=5', 'phase': 5}, {'id': '614626+26_phase=5', 'predictions': 91.7, 'property_id': '614626', 'recommendation_id': '26_phase=5', 'phase': 5}, {'id': '614626+27_phase=5', 'predictions': 91.7, 'property_id': '614626', 'recommendation_id': '27_phase=5', 'phase': 5}, {'id': '614626+28_phase=5', 'predictions': 91.7, 'property_id': '614626', 'recommendation_id': '28_phase=5', 'phase': 5}, {'id': '614626+29_phase=5', 'predictions': 88.8, 'property_id': '614626', 'recommendation_id': '29_phase=5', 'phase': 5}, {'id': '614626+30_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '30_phase=5', 'phase': 5}, {'id': '614626+31_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '31_phase=5', 'phase': 5}, {'id': '614626+32_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '32_phase=5', 'phase': 5}, {'id': '614626+33_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '33_phase=5', 'phase': 5}, {'id': '614626+34_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '34_phase=5', 'phase': 5}, {'id': '614626+35_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '35_phase=5', 'phase': 5}, {'id': '614626+36_phase=5', 'predictions': 91.4, 'property_id': '614626', 'recommendation_id': '36_phase=5', 'phase': 5}, {'id': '614626+37_phase=5', 'predictions': 86.2, 'property_id': '614626', 'recommendation_id': '37_phase=5', 'phase': 5}, {'id': '614626+38_phase=5', 'predictions': 86.2, 'property_id': '614626', 'recommendation_id': '38_phase=5', 'phase': 5}, {'id': '614626+39_phase=5', 'predictions': 86.2, 'property_id': '614626', 'recommendation_id': '39_phase=5', 'phase': 5}, {'id': '614626+40_phase=5', 'predictions': 88.4, 'property_id': '614626', 'recommendation_id': '40_phase=5', 'phase': 5}, {'id': '614626+41_phase=5', 'predictions': 88.4, 'property_id': '614626', 'recommendation_id': '41_phase=5', 'phase': 5}, {'id': '614626+42_phase=5', 'predictions': 88.4, 'property_id': '614626', 'recommendation_id': '42_phase=5', 'phase': 5}, {'id': '614626+43_phase=5', 'predictions': 88.4, 'property_id': '614626', 'recommendation_id': '43_phase=5', 'phase': 5}, {'id': '614626+44_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '44_phase=5', 'phase': 5}, {'id': '614626+45_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '45_phase=5', 'phase': 5}, {'id': '614626+46_phase=5', 'predictions': 90.5, 'property_id': '614626', 'recommendation_id': '46_phase=5', 'phase': 5}, {'id': '614626+47_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '47_phase=5', 'phase': 5}, {'id': '614626+48_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '48_phase=5', 'phase': 5}, {'id': '614626+49_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '49_phase=5', 'phase': 5}, {'id': '614626+50_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '50_phase=5', 'phase': 5}, {'id': '614626+51_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '51_phase=5', 'phase': 5}, {'id': '614626+52_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '52_phase=5', 'phase': 5}, {'id': '614626+53_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '53_phase=5', 'phase': 5}, {'id': '614626+54_phase=5', 'predictions': 90.4, 'property_id': '614626', 'recommendation_id': '54_phase=5', 'phase': 5}, {'id': '614626+55_phase=5', 'predictions': 84.4, 'property_id': '614626', 'recommendation_id': '55_phase=5', 'phase': 5}, {'id': '614626+56_phase=5', 'predictions': 86.2, 'property_id': '614626', 'recommendation_id': '56_phase=5', 'phase': 5}, {'id': '614626+57_phase=5', 'predictions': 86.2, 'property_id': '614626', 'recommendation_id': '57_phase=5', 'phase': 5}] ), "heat_demand_predictions": heat_demand_predictions, "carbon_change_predictions": carbon_predictions, "hotwater_kwh_predictions": pd.DataFrame([]), "heating_kwh_predictions": pd.DataFrame([]), } recommendations2 = { 614626: [ [ { 'phase': 0, 'type': 'loft_insulation', 'measure_type': 'loft_insulation', 'sap_points': 0, 'survey': False, 'innovation_rate': 0.0, 'recommendation_id': '0_phase=0', 'efficiency': np.float64(6052.801176470587), 'co2_equivalent_savings': np.float64(0.19999999999999973), 'heat_demand': np.float64(27.399999999999977)}, ], [ { 'phase': 1, 'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation', 'starting_u_value': None, 'new_u_value': None, 'already_installed': False, 'sap_points': np.float64(-1.4000000000000057), 'heat_demand': np.float64(-6.5), 'kwh_savings': 0, 'co2_equivalent_savings': np.float64(0.0), 'energy_cost_savings': 0, 'total': 560.0, 'labour_hours': 8, 'labour_days': 1.0, 'recommendation_id': '3_phase=1', } ], [ { 'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting', 'new_u_value': None, 'already_installed': False, 'sap_points': 5, 'kwh_savings': 164.25, 'energy_cost_savings': 45.480824999999996, 'co2_equivalent_savings': np.float64(0.0), 'total': 10.5, 'contingency': 2.73, 'contingency_rate': 0.26, 'labour_hours': 1, 'labour_days': 0.125, 'survey': True, 'innovation_rate': 0.0, 'recommendation_id': '4_phase=2', 'efficiency': 10.5, 'heat_demand': np.float64(4.100000000000023)} ], [ { 'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3, 'total': 70, 'contingency': 7.0, 'contingency_rate': 0.1, 'subtotal': 58.333333333333336, 'vat': 11.666666666666664, 'labour_hours': 0.5, 'labour_days': 1, 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(1.0), 'already_installed': False, 'recommendation_id': '5_phase=3', 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(8.5)}, { 'type': 'heating', 'phase': 3, 'measure_type': 'time_temperature_zone_control', 'total': 604.5840000000001, 'contingency': 60.45840000000001, 'contingency_rate': 0.1, 'subtotal': 571.32, 'vat': 33.264, 'labour_hours': 3.08, 'labour_days': np.float64(1.0), 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(1.8), 'already_installed': False, 'innovation_rate': 0.0, 'recommendation_id': '6_phase=3', 'efficiency': 604.5840000000001, 'co2_equivalent_savings': np.float64(0.10000000000000009), 'heat_demand': np.float64(13.300000000000011)}, { 'phase': 3, 'type': 'heating', 'measure_type': 'air_source_heat_pump', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(3.8), 'already_installed': False, 'total': 17144.924, 'contingency': 4195.5434000000005, 'contingency_rate': 0.35, 'vat': 33.264, 'labour_hours': 83.08, 'labour_days': np.float64(11.0), 'innovation_rate': 0, 'recommendation_id': '7_phase=3', 'efficiency': 17144.924, 'co2_equivalent_savings': np.float64(0.8000000000000003), 'heat_demand': np.float64(59.30000000000001)} ], [ { 'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(0.0), 'already_installed': False, 'total': 60.0, 'contingency': 6.0, 'contingency_rate': 0.1, 'subtotal': 50.0, 'vat': 10.0, 'labour_hours': 6.0, 'labour_days': np.float64(1.0), 'innovation_rate': 0.0, 'recommendation_id': '8_phase=4', 'efficiency': 60.0, 'co2_equivalent_savings': np.float64(0.0), 'heat_demand': np.float64(0.0) } ], [ { 'phase': 5, 'type': 'solar_pv', 'measure_type': 'solar_pv', 'starting_u_value': None, 'new_u_value': None, 'sap_points': np.float64(16.0), 'already_installed': False, 'total': 5892.21, 'subtotal': 5892.21, 'contingency': 883.8315, 'contingency_rate': 0.15, 'vat': 0, 'labour_hours': 48, 'labour_days': 2, 'has_battery': False, 'innovation_rate': 0.0, 'recommendation_id': '29_phase=5', 'efficiency': np.float64(368.263125) } ] ] } representative_recommendations2 = { 614626: [ { 'phase': 0, 'type': 'loft_insulation', 'measure_type': 'loft_insulation', 'sap_points': 0, 'survey': False, 'recommendation_id': '0_phase=0', }, { 'phase': 1, 'type': 'mechanical_ventilation', 'measure_type': 'mechanical_ventilation', 'sap_points': np.float64(-1.4000000000000057), 'recommendation_id': '3_phase=1' }, { 'phase': 2, 'type': 'low_energy_lighting', 'measure_type': 'low_energy_lighting', 'sap_points': 5, 'survey': True, 'recommendation_id': '4_phase=2', }, { 'type': 'heating', 'measure_type': 'roomstat_programmer_trvs', 'phase': 3, 'sap_points': np.float64(1.0), 'recommendation_id': '5_phase=3', }, { 'phase': 4, 'type': 'secondary_heating', 'measure_type': 'secondary_heating', 'sap_points': np.float64(0.0), 'recommendation_id': '8_phase=4', }, { 'phase': 5, 'type': 'solar_pv', 'measure_type': 'solar_pv', 'sap_points': np.float64(16.0), 'recommendation_id': '29_phase=5', } ] } recommendations_with_impact2, impact_summary2, adjustments2 = ( Recommendations.calculate_recommendation_impact( property_instance=property_instance, all_predictions=all_predictions2, recommendations=recommendations2, representative_recommendations=representative_recommendations2, debug=True ) ) assert adjustments2 == [ {'recommendation_id': '0_phase=0', 'phase': 0, 'sap_adjustment': np.float64(1.7)}, {'recommendation_id': '4_phase=2', 'phase': 2, 'sap_adjustment': np.float64(4.0)} ]