import csv,json from pathlib import Path from fractions import Fraction from collections import defaultdict p=Path(__file__).resolve().parent def read(n): with (p/n).open() as f:return list(csv.DictReader(f)) s=read('industry-step-up-2026.csv');division=[r for r in s if r['row_type']=='industry_division'];unknown=next(r for r in s if r['row_type']=='unknown_reconciliation');national=next(r for r in s if r['row_type']=='national_total') assert len(division)==19 for r in s: assert int(r['end_june_2025'])-int(r['start_june_2024'])==int(r['net_2024_25']) assert int(r['end_june_2026'])-int(r['end_june_2025'])==int(r['net_2025_26']) assert int(r['net_2025_26'])-int(r['net_2024_25'])==int(r['change_in_annual_net_gain']) flows={r['period']:r for r in read('national-entries-exits-2022-2026.csv')} denominator=int(flows['2025-26']['net_change'])-int(flows['2024-25']['net_change']) assert int(national['change_in_annual_net_gain'])==denominator assert sum(int(r['change_in_annual_net_gain']) for r in division)+int(unknown['change_in_annual_net_gain'])==denominator pair=sum(int(r['change_in_annual_net_gain']) for r in division if r['industry'] in ['Construction','Professional, Scientific and Technical Services']) m=read('employment-movement-2026.csv');non=next(r for r in m if r['category']=='Non-employing') for r in m: assert int(r['entries'])-int(r['exits'])==int(r['entry_exit_balance']) assert int(r['entry_exit_balance'])+int(r['net_surviving_movement'])==int(r['net_change']) assert sum(int(r['net_surviving_movement']) for r in m)==0 assert sum(int(r['net_change']) for r in m)==int(flows['2025-26']['net_change']) movement_share=100*int(non['net_surviving_movement'])/int(non['net_change']) raw=read('council-industry-comparison-inputs.csv');bycode=defaultdict(list) for r in raw: if r['industry']!='ALL':bycode[r['code']].append(r) summary=[];published={r['lga_code']:r for r in read('transport-comparison-2024-2025.csv')} for minimum in [1000,5000,10000]: totals=[r for r in raw if r['industry']=='ALL' and int(r['count_2024'])>=minimum] totals.sort(key=lambda r:(-Fraction(int(r['count_2025'])-int(r['count_2024']),int(r['count_2024'])),r['code'])) flags=[];inclusive=[] for rank,r in enumerate(totals,1): sectors=bycode[r['code']];best=max(int(t['net_2024_25']) for t in sectors);leaders=[t['industry'] for t in sectors if int(t['net_2024_25'])==best];sole=leaders==['I'];inc='I' in leaders;flags.append(sole);inclusive.append(inc) if minimum==5000: q=published[r['code']];assert int(q['rank'])==rank;assert (q['transport_sole_leader']=='True')==sole;assert (q['transport_joint_or_sole_leader']=='True')==inc;assert int(q['largest_division_net_change'])==best summary.append({'minimum':minimum,'eligible':len(totals),'top10_sole':sum(flags[:10]),'other_sole':sum(flags[10:]),'other_including_ties':sum(inclusive[10:])}) assert [(r['eligible'],r['top10_sole'],r['other_sole']) for r in summary]==[(301,9,25),(135,8,14),(81,7,8)] print(json.dumps({'step_up_pair':pair,'step_up_denominator':denominator,'unknown_step':int(unknown['change_in_annual_net_gain']),'step_up_share_pct':100*pair/denominator,'movement_share_nonemploying_pct':movement_share,'council_sensitivity':summary},indent=2))