Skip to content

This is one run, published the way it came out of the app. The report below, its figures, its captions and its notes on the data are the agent's own; nothing has been re-run to look better, and the calls that failed are shown where they happened.

The prompt

The prompt, as typed

Which NSW federal seats did Labor win in 2025 by under 6% on two-party-preferred? For each, show the margin, the swing since 2022, how much the boundary changed, median household income, median age and the share of land bushfire-prone. Chart the swing against the margin, and map the tightest seat's booths coloured by Labor two-party share.

Datasetsdivision_profileelections_federal

9 tool calls6s analysis2 datasets

One sentence that crosses four unrelated publishers. The result and the two-party count are the electoral commission's. The swing needs the previous election's result on the same boundaries, which is the part that is never as simple as subtracting. The boundary change is a redistribution, published as geography rather than as a number. And the demography and the hazard share come off the statistical spine and the state hazard mapping, joined to an electorate that is not a statistical area at all.

What the agent did

  1. Loaded expertiseLoaded expertise: elections, electorates & seat analysis.
  2. Loaded expertiseLoaded expertise: report, briefing & editorial writing.
  3. Ran a queryTOOL ERROR: `open/elections_federal/aec-2004-2025/house_tpp_division.parquet` is a lake KEY, not a path this query can open — it resolves to nothing on its own. Use `zn.read('<dataset>', file='<stem>'failed
  4. Ran a cell in the sandboxwrote nsw_labor_under6_seats.csv — 3 rows6.1s
  5. Rendered a tableRendered `data_3` as a table (3 rows) titled "NSW Labor seats won in 2025 by under 6%".
  6. Rendered a chartRendered `data_2` as a scatter chart titled "Swing to Labor against 2025 margin" (3 rows).
  7. Rendered a mapRendered `data_1` as a points map titled "Banks booths, 2025 Labor two-party-preferred share" (56 features). The `diverging` ramp was refused and the sequential ramp used instead: these values do not
  8. Wrote the documentTOOL ERROR: this document is not publishable yet — 1 thing to fix:failed
  9. Wrote the documentRendered a report titled "NSW Labor’s tight federal seats after the 2025 election" embedding 3 blocks: data_3 (table), data_2 (chart), data_1 (map). Their standalone copies from this turn have been re

2 calls failed and the agent recovered within the same run. Failures are shown because they happened.

What it produced

NSW Labor’s tight federal seats after the 2025 election

Labor won three NSW federal seats by under 6% on two-party-preferred in 2025: Banks at 2.39%, Hughes at 3.06% and Gilmore at 5.13%. Banks was the tightest; its booths ranged from 32.7% to 84.6% Labor two-party-preferred among 56 mappable booths.

Key findings

  • Banks was the tightest Labor seat at a 2.39% margin, compared with 3.06% in Hughes and 5.13% in Gilmore.
  • The derived swing to Labor was 5.03 points in Banks, 6.52 points in Hughes and 4.96 points in Gilmore.
  • Banks had the largest boundary-area change at 23.6%, compared with 5.9% in Hughes and a 0.3% decrease in Gilmore.
  • Bushfire-prone land covered 16.9% of Banks, compared with 85.0% of Hughes and 97.0% of Gilmore.
  • Banks had a $1,944 weekly seat average of SA2 median household incomes, compared with $2,270 in Hughes and $1,336 in Gilmore.

Banks was the closest Labor win at 2.39%

Banks was the narrowest of the three seats, with a 2.39% two-party-preferred margin. Hughes followed at 3.06%, while Gilmore was the least marginal at 5.13%. All three satisfy the equivalent Labor two-party-preferred threshold of below 56%, and all are classic Labor–Coalition contests, so the margin and Labor two-party-preferred framing agree. The three-point spread is a tight NSW subset rather than a statewide ranking.

NSW Labor seats won in 2025 by under 6%
SeatMargin (percentage points)Swing to labor since 2022 (percentage points)Boundary change since 2022 (%)Seat average of SA2 median household income (a$/week)Seat average of SA2 median age (years)Bushfire prone land share (%)
Banks2.395.0323.61,94439.316.9
Hughes3.066.525.92,27038.385
Gilmore5.134.96-0.31,33649.397
Three NSW Labor seats won in 2025 with a two-party-preferred margin under 6%; AEC election results and 2021 Census-derived seat profile, 2025.

The swing was largest in Hughes at 6.52 points

The swing to Labor since 2022 was 6.52 points in Hughes, 5.03 points in Banks and 4.96 points in Gilmore. Hughes and Banks also had boundary-area changes above the 5% disclosure threshold: 5.9% and 23.6%, respectively. Gilmore’s area changed by 0.3% in the opposite direction. The boundary change is a measure of altered seat area, not a voter-level measure, so it provides context for the comparison rather than replacing the derived AEC swing.

Swing to Labor against 2025 margin
Seat-level relationship between the 2025 margin and derived swing to Labor since 2022; AEC results, 2022–2025.

The three seats differ sharply in land and demographic profile

Gilmore had the highest bushfire-prone land share at 97.0%, followed by Hughes at 85.0% and Banks at 16.9%. Those are land shares, not population exposure. The seat average of SA2 median household income was $1,336 a week in Gilmore, $1,944 in Banks and $2,270 in Hughes. The corresponding seat averages of SA2 median age were 49.3, 39.3 and 38.3 years. These measures describe the socioeconomic composition associated with the 2025 boundaries, not exact household or person-level medians.

Banks booth support ranged from 32.7% to 84.6% Labor

Banks had 56 mappable booths from 58 listed voting places. Labor’s two-party-preferred share ranged from 32.7% at Picnic Point South to 84.6% at Ultimo, a spread of 51.9 points. The points represent ordinary formal votes at voting places, so they show the geography of recorded booth support rather than a complete division count. The full Banks division result was 52.39% Labor two-party-preferred, while booth results exclude votes that the AEC does not attribute to individual booths.

Banks booths, 2025 Labor two-party-preferred share
Banks voting-place points coloured by Labor two-party-preferred share; AEC ordinary booth results, 2025.

Notes on the data

  • The seat filter uses the 2025 AEC federal boundaries and Labor’s 2025 two-party-preferred result. Swing is derived from the national sign convention rather than blindly reading the stored swing field; positive values mean a swing to Labor. Banks is the tightest seat, so its booth map uses 2025 ordinary-vote booth results: 56 of 58 Banks booths had coordinates. Booth shares are shares of ordinary formal votes at each booth, not the full division result, and booths can sit outside the seat because of pre-poll and out-of-division voting centres.
  • Household income and age are 2021 Census-derived seat averages of SA2 medians interpolated onto the 2025 boundaries; they are not the median household income or median age of every resident in the seat. Bushfire share is the share of mapped land cells classified as bushfire-prone, not the share of residents or properties.
  • Federal election data © Commonwealth of Australia (Australian Electoral Commission), used under the AEC's terms of use.

What it said

How it got there

Every cell the agent ran, in order and in full, because a seat list you cannot audit is a seat list you have to take on trust.

sqlthis cell failed
WITH t AS (
  SELECT * FROM read_parquet('open/elections_federal/aec-2004-2025/house_tpp_division.parquet')
), nat AS (
  SELECT election_year,
         100.0 * SUM(alp_votes) / SUM(total_votes) AS alp_tpp,
         SUM(swing_pct * total_votes) / SUM(total_votes) AS published_vw
  FROM t GROUP BY 1
), anchored AS (
  SELECT election_year,
         alp_tpp - LAG(alp_tpp) OVER (ORDER BY election_year) AS national_swing,
         published_vw
  FROM nat
), factor AS (
  SELECT election_year,
         CASE WHEN national_swing IS NULL THEN NULL
              WHEN SIGN(national_swing) = SIGN(published_vw) THEN 1 ELSE -1 END AS sign_factor
  FROM anchored
), swing_2025 AS (
  SELECT t.division_name,
         CASE WHEN ABS(t.swing_pct) = ABS(t.alp_pct) THEN NULL
              ELSE t.swing_pct * f.sign_factor END AS swing_to_labor
  FROM t JOIN factor f USING (election_year)
  WHERE t.election_year = 2025
)
SELECT d.division_name,
       ROUND(d.margin_pct, 2) AS margin_pct,
       ROUND(s.swing_to_labor, 2) AS swing_to_labor_since_2022,
       ROUND(d.area_change_pct_since_2022, 1) AS boundary_change_pct,
       ROUND(d.median_hh_income_weekly_avg) AS seat_average_of_sa2_median_household_income_weekly,
       ROUND(d.median_age_avg, 1) AS seat_average_of_sa2_median_age,
       ROUND(d.bushfire_cell_share_pct, 1) AS bushfire_prone_land_share_pct,
       d.is_classic,
       d.changed_party_since_2022
FROM read_parquet('open/division_profile/2026-09/division_profile.parquet') d
JOIN swing_2025 s USING (division_name)
WHERE d.state = 'NSW'
  AND d.member_party_ab = 'ALP'
  AND d.margin_pct < 6
ORDER BY d.margin_pct
bashcell 2 of 2
python3 - <<'EOF'
import zn
con=zn.lake_con()
T=zn.read('elections_federal',file='house_tpp_division')
DP=zn.read('division_profile')
con.execute(f'''CREATE OR REPLACE TEMP TABLE swing_2025 AS
WITH t AS (SELECT * FROM {T}), nat AS (
 SELECT election_year, 100.0*SUM(alp_votes)/SUM(total_votes) AS alp_tpp,
 SUM(swing_pct*total_votes)/SUM(total_votes) AS published_vw FROM t GROUP BY 1),
a AS (SELECT election_year, alp_tpp-LAG(alp_tpp) OVER (ORDER BY election_year) AS national_swing, published_vw FROM nat),
f AS (SELECT election_year, CASE WHEN national_swing IS NULL THEN NULL WHEN SIGN(national_swing)=SIGN(published_vw) THEN 1 ELSE -1 END AS sign_factor FROM a)
SELECT t.division_name, CASE WHEN ABS(t.swing_pct)=ABS(t.alp_pct) THEN NULL ELSE t.swing_pct*f.sign_factor END AS swing_to_labor
FROM t JOIN f USING(election_year) WHERE t.election_year=2025''')
seat_sql=f'''SELECT d.division_name AS "Seat", ROUND(d.margin_pct,2) AS "Margin (percentage points)", ROUND(s.swing_to_labor,2) AS "Swing to Labor since 2022 (percentage points)", ROUND(d.area_change_pct_since_2022,1) AS "Boundary change since 2022 (%)", ROUND(d.median_hh_income_weekly_avg) AS "Seat average of SA2 median household income (A$/week)", ROUND(d.median_age_avg,1) AS "Seat average of SA2 median age (years)", ROUND(d.bushfire_cell_share_pct,1) AS "Bushfire-prone land share (%)" FROM {DP} d JOIN swing_2025 s USING(division_name) WHERE d.state='NSW' AND d.member_party_ab='ALP' AND d.margin_pct<6 ORDER BY d.margin_pct'''
zn.write_out(con,'nsw_labor_under6_seats',seat_sql)
# chart data
zn.write_out(con,'nsw_labor_under6_scatter',f'''SELECT d.division_name AS "Seat", d.margin_pct AS margin_pct, s.swing_to_labor AS swing_to_labor FROM {DP} d JOIN swing_2025 s USING(division_name) WHERE d.state='NSW' AND d.member_party_ab='ALP' AND d.margin_pct<6''')
# tightest is Banks, booths
TPP=zn.read('elections_federal',file='house_tpp_polling_place'); PP=zn.read('elections_federal',file='polling_places')
zn.write_out(con,'banks_booths',f'''SELECT t.polling_place AS "Booth", p.premises_suburb AS "Suburb", p.lat, p.lng, ROUND(t.alp_pct,1) AS labor_tpp_share_pct, t.total_votes AS ordinary_formal_votes FROM {TPP} t JOIN {PP} p USING(election_year,division_id,polling_place_id) WHERE t.election_year=2025 AND t.division_name='Banks' AND p.lat IS NOT NULL ORDER BY t.alp_pct DESC''')
print(con.sql(f'''SELECT COUNT(*) AS seats, MIN(margin_pct) AS tightest_margin, MAX(margin_pct) AS widest_margin FROM {DP} WHERE state='NSW' AND member_party_ab='ALP' AND margin_pct<6'''))
print(con.sql(f'''SELECT COUNT(*) AS total_booths, COUNT(*) FILTER(WHERE p.lat IS NOT NULL) AS mappable FROM {TPP} t LEFT JOIN {PP} p USING(election_year,division_id,polling_place_id) WHERE t.election_year=2025 AND t.division_name='Banks' '''))
EOF

What this run does not tell you

The report carries its own notes on the data, reproduced here exactly as the agent wrote them:

  • The seat filter uses the 2025 AEC federal boundaries and Labor’s 2025 two-party-preferred result. Swing is derived from the national sign convention rather than blindly reading the stored swing field; positive values mean a swing to Labor. Banks is the tightest seat, so its booth map uses 2025 ordinary-vote booth results: 56 of 58 Banks booths had coordinates. Booth shares are shares of ordinary formal votes at each booth, not the full division result, and booths can sit outside the seat because of pre-poll and out-of-division voting centres.
  • Household income and age are 2021 Census-derived seat averages of SA2 medians interpolated onto the 2025 boundaries; they are not the median household income or median age of every resident in the seat. Bushfire share is the share of mapped land cells classified as bushfire-prone, not the share of residents or properties.
  • Federal election data © Commonwealth of Australia (Australian Electoral Commission), used under the AEC's terms of use.

Three things to add to them, none of which is specific to this run.

A margin is a count, not a forecast. Two-party-preferred at one election describes what happened at that election on those boundaries. It is the starting point for a projection and it is not one.

A redistribution makes comparison a modelling choice. When boundaries move, the previous result inside the new boundary has to be estimated from booth and polling-place counts, and different estimates are defensible. A swing across a redistribution carries that estimate's uncertainty and rarely says so.

Electorates are not statistical areas. Census counts, incomes and hazard shares are published on the statistical spine, and an electorate cuts across it. Any figure for a seat is an allocation of those smaller areas to it, and the smaller the areas the better the allocation — the method matters more than the number.

Run it on your own seats

The state and the party here are the ones the prompt named. The same query runs over any chamber, any set of divisions or districts, any margin threshold, and any of the demographic and physical layers in the library. If you want to see it against yours, request access — or read how this is used for electorate analysis and by campaigns and party research.

See it run on your portfolio

Zenancy is in private preview with Group 2 reporters and their advisers.

Request access