Skip to content

Commit e1749e6

Browse files
authored
Merge pull request #1560 from PolicyEngine/codex/fix-1378
Add OBR forecast importer
2 parents d740f63 + 57522f0 commit e1749e6

4 files changed

Lines changed: 833 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added an OBR detailed forecast table importer script for updating economic forecast values in `yoy_growth.yaml`.
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
from io import BytesIO
2+
from zipfile import ZipFile
3+
4+
from policyengine_uk.utils.import_obr_forecasts import (
5+
build_efo_href,
6+
extract_annual_series_from_xlsx,
7+
infer_forecast_start_year,
8+
infer_release,
9+
update_yoy_growth_yaml,
10+
)
11+
12+
13+
def make_inline_cell(ref: str, value: str) -> str:
14+
return f'<c r="{ref}" t="inlineStr"><is><t>{value}</t></is></c>'
15+
16+
17+
def make_number_cell(ref: str, value: float) -> str:
18+
return f'<c r="{ref}"><v>{value}</v></c>'
19+
20+
21+
def make_sheet(rows: dict[int, list[str]]) -> bytes:
22+
row_xml = []
23+
for row_num in sorted(rows):
24+
row_xml.append(f'<row r="{row_num}">{"".join(rows[row_num])}</row>')
25+
xml = (
26+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
27+
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
28+
f"<sheetData>{''.join(row_xml)}</sheetData>"
29+
"</worksheet>"
30+
)
31+
return xml.encode()
32+
33+
34+
def make_test_xlsx() -> bytes:
35+
workbook_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
36+
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
37+
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
38+
<sheets>
39+
<sheet name="1.6" sheetId="1" r:id="rId1"/>
40+
<sheet name="1.7" sheetId="2" r:id="rId2"/>
41+
<sheet name="1.16" sheetId="3" r:id="rId3"/>
42+
</sheets>
43+
</workbook>
44+
"""
45+
rels_xml = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
46+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
47+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
48+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>
49+
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet3.xml"/>
50+
</Relationships>
51+
"""
52+
53+
sheet_16 = make_sheet(
54+
{
55+
3: [make_inline_cell("Q3", "Average weekly earnings growth (per cent)")],
56+
97: [
57+
make_inline_cell("B97", "2025"),
58+
make_number_cell("Q97", 5.17),
59+
],
60+
98: [
61+
make_inline_cell("B98", "2026"),
62+
make_number_cell("Q98", 3.33),
63+
],
64+
}
65+
)
66+
sheet_17 = make_sheet(
67+
{
68+
3: [
69+
make_inline_cell("C3", "RPI"),
70+
make_inline_cell("E3", "CPI"),
71+
make_inline_cell("F3", "CPIH"),
72+
make_inline_cell(
73+
"H3",
74+
"Mortgage interest payments on dwellings (per cent change on a year earlier)",
75+
),
76+
make_inline_cell(
77+
"I3",
78+
"Actual rents for housing (per cent change on a year earlier)",
79+
),
80+
],
81+
98: [
82+
make_inline_cell("B98", "2025"),
83+
make_number_cell("C98", 4.33),
84+
make_number_cell("E98", 3.45),
85+
make_number_cell("F98", 3.88),
86+
make_number_cell("H98", 9.52),
87+
make_number_cell("I98", 5.42),
88+
],
89+
99: [
90+
make_inline_cell("B99", "2026"),
91+
make_number_cell("C99", 3.71),
92+
make_number_cell("E99", 2.48),
93+
make_number_cell("F99", 2.55),
94+
make_number_cell("H99", 7.97),
95+
make_number_cell("I99", 3.34),
96+
],
97+
}
98+
)
99+
sheet_116 = make_sheet(
100+
{
101+
3: [
102+
make_inline_cell(
103+
"D3",
104+
"House price index (per cent change on a year earlier)",
105+
)
106+
],
107+
97: [
108+
make_inline_cell("B97", "2025"),
109+
make_number_cell("D97", 2.80),
110+
],
111+
98: [
112+
make_inline_cell("B98", "2026"),
113+
make_number_cell("D98", 2.40),
114+
],
115+
}
116+
)
117+
118+
buffer = BytesIO()
119+
with ZipFile(buffer, "w") as archive:
120+
archive.writestr("xl/workbook.xml", workbook_xml)
121+
archive.writestr("xl/_rels/workbook.xml.rels", rels_xml)
122+
archive.writestr("xl/worksheets/sheet1.xml", sheet_16)
123+
archive.writestr("xl/worksheets/sheet2.xml", sheet_17)
124+
archive.writestr("xl/worksheets/sheet3.xml", sheet_116)
125+
return buffer.getvalue()
126+
127+
128+
def test_extract_annual_series_from_xlsx():
129+
series = extract_annual_series_from_xlsx(make_test_xlsx())
130+
131+
assert series["average_earnings"] == {2025: 0.0517, 2026: 0.0333}
132+
assert series["rpi"] == {2025: 0.0433, 2026: 0.0371}
133+
assert series["consumer_price_index"] == {2025: 0.0345, 2026: 0.0248}
134+
assert series["cpih"] == {2025: 0.0388, 2026: 0.0255}
135+
assert series["mortgage_interest"] == {2025: 0.0952, 2026: 0.0797}
136+
assert series["rent"] == {2025: 0.0542, 2026: 0.0334}
137+
assert series["house_prices"] == {2025: 0.028, 2026: 0.024}
138+
139+
140+
def test_release_inference_helpers():
141+
assert infer_release("Economy_Detailed_forecast_tables_November_2025.xlsx") == (
142+
"November",
143+
2025,
144+
)
145+
assert infer_forecast_start_year("March", 2026) == 2025
146+
assert build_efo_href("March", 2026) == (
147+
"https://obr.uk/efo/economic-and-fiscal-outlook-march-2026/"
148+
)
149+
150+
151+
def test_update_yoy_growth_yaml_updates_forecast_window_only(tmp_path):
152+
yaml_path = tmp_path / "yoy_growth.yaml"
153+
yaml_path.write_text("""obr:
154+
rpi:
155+
values:
156+
2024-01-01: 0.0300
157+
2025-01-01: 0.0000
158+
2026-01-01: 0.0000
159+
2031-01-01: 0.0230
160+
metadata:
161+
reference:
162+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.7)
163+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
164+
average_earnings:
165+
values:
166+
2024-01-01: 0.0400
167+
2025-01-01: 0.0000
168+
2026-01-01: 0.0000
169+
2031-01-01: 0.0383
170+
metadata:
171+
reference:
172+
- title: Old
173+
href: https://example.com/old
174+
consumer_price_index:
175+
values:
176+
2024-01-01: 0.0200
177+
2025-01-01: 0.0000
178+
2026-01-01: 0.0000
179+
2031-01-01: 0.0200
180+
metadata:
181+
reference:
182+
- title: Old
183+
href: https://example.com/old
184+
cpih:
185+
values:
186+
2024-01-01: 0.0200
187+
2025-01-01: 0.0000
188+
2026-01-01: 0.0000
189+
2031-01-01: 0.0230
190+
metadata:
191+
reference:
192+
- title: Old
193+
href: https://example.com/old
194+
house_prices:
195+
values:
196+
2024-01-01: 0.0100
197+
2025-01-01: 0.0000
198+
2026-01-01: 0.0000
199+
2031-01-01: 0.0200
200+
metadata:
201+
reference:
202+
- title: Old
203+
href: https://example.com/old
204+
mortgage_interest:
205+
values:
206+
2024-01-01: 0.1000
207+
2025-01-01: 0.0000
208+
2026-01-01: 0.0000
209+
2031-01-01: 0.0300
210+
metadata:
211+
reference:
212+
- title: Old
213+
href: https://example.com/old
214+
rent:
215+
values:
216+
2024-01-01: 0.0500
217+
2025-01-01: 0.0000
218+
2026-01-01: 0.0000
219+
2031-01-01: 0.0200
220+
metadata:
221+
reference:
222+
- title: Old
223+
href: https://example.com/old
224+
""")
225+
226+
update_yoy_growth_yaml(
227+
yaml_path=yaml_path,
228+
series_values=extract_annual_series_from_xlsx(make_test_xlsx()),
229+
month="March",
230+
year=2026,
231+
forecast_start_year=2025,
232+
forecast_years=2,
233+
)
234+
235+
content = yaml_path.read_text()
236+
assert "2024-01-01: 0.0300" in content
237+
assert "2025-01-01: 0.0433" in content
238+
assert "2026-01-01: 0.0371" in content
239+
assert "2031-01-01: 0.0230" in content
240+
assert "2025-01-01: 0.0280" in content
241+
assert "2026-01-01: 0.0240" in content
242+
assert (
243+
"OBR EFO March 2026 (detailed forecast tables, economy, Table 1.16)" in content
244+
)
245+
assert "https://obr.uk/efo/economic-and-fiscal-outlook-march-2026/" in content
246+
247+
248+
def test_update_yoy_growth_yaml_keeps_existing_values_when_obr_has_blank_years(
249+
tmp_path,
250+
):
251+
yaml_path = tmp_path / "yoy_growth.yaml"
252+
yaml_path.write_text("""obr:
253+
mortgage_interest:
254+
values:
255+
2025-01-01: 0.0000
256+
2026-01-01: 0.0000
257+
2027-01-01: 0.0553
258+
metadata:
259+
reference:
260+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.7)
261+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
262+
rpi:
263+
values:
264+
2025-01-01: 0.0000
265+
2026-01-01: 0.0000
266+
2027-01-01: 0.0000
267+
metadata:
268+
reference:
269+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.7)
270+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
271+
average_earnings:
272+
values:
273+
2025-01-01: 0.0000
274+
2026-01-01: 0.0000
275+
2027-01-01: 0.0000
276+
metadata:
277+
reference:
278+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.6)
279+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
280+
consumer_price_index:
281+
values:
282+
2025-01-01: 0.0000
283+
2026-01-01: 0.0000
284+
2027-01-01: 0.0000
285+
metadata:
286+
reference:
287+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.7)
288+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
289+
cpih:
290+
values:
291+
2025-01-01: 0.0000
292+
2026-01-01: 0.0000
293+
2027-01-01: 0.0000
294+
metadata:
295+
reference:
296+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.7)
297+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
298+
house_prices:
299+
values:
300+
2025-01-01: 0.0000
301+
2026-01-01: 0.0000
302+
2027-01-01: 0.0000
303+
metadata:
304+
reference:
305+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.16)
306+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
307+
rent:
308+
values:
309+
2025-01-01: 0.0000
310+
2026-01-01: 0.0000
311+
2027-01-01: 0.0000
312+
metadata:
313+
reference:
314+
- title: OBR EFO November 2025 (detailed forecast tables, economy, Table 1.7)
315+
href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/
316+
""")
317+
318+
update_yoy_growth_yaml(
319+
yaml_path=yaml_path,
320+
series_values=extract_annual_series_from_xlsx(make_test_xlsx()),
321+
month="March",
322+
year=2026,
323+
forecast_start_year=2025,
324+
forecast_years=3,
325+
)
326+
327+
content = yaml_path.read_text()
328+
assert "2025-01-01: 0.0952" in content
329+
assert "2026-01-01: 0.0797" in content
330+
assert "2027-01-01: 0.0553" in content

0 commit comments

Comments
 (0)