New teams get free Claude credits for their trial. Learn more

Does money help win elections? Answered with data from SF's November 2026 election

November 5, 2026•11 min read•Vivek Sah
Does money help win elections? Answered with data from SF's November 2026 election

San Francisco publishes every campaign finance filing as an open dataset, and the Department of Elections publishes results as they're counted. Join the two and you can ask the old question directly: does the side with more money win? The answer turned out to be less interesting than the trip there: sum the raw transactions table and you overstate the money by about 3x.

All results here are preliminary results as of Nov 4 (the 4:00 PM release). Turnout in that release was 206,320 ballots of 535,608 registered (38.5%), and it's still rising as mail ballots arrive.

The data

Everything was loaded into BigQuery (contextflo-datasets.sf_elections) from public sources, plus two small hand-built maps.

TableSourceRowsGrain
campaign_transactionsSF Ethics Commission filings via DataSF, since 2025-01-01105,515One line item on one filing (latest version of each filing, so amendments are resolved)
measure_resultsDepartment of Elections results page50Measure x choice, with pass threshold
contest_resultsThe same release's summary XML157Contest x choice
supervisor_roundsRanked choice round-by-round pages169District x row (candidate or ballot tally) x round
precinct_zipsPrecinct breakdown by district, neighborhood and ZIP749ZIP x precinct
committee_measuresHand-built25Committee x measure x election, with position, and a money_from date for committees reused from an earlier campaign
candidate_committeesHand-built40Committee x candidate

The two maps exist because the filings don't reliably say which measure or candidate a committee is for. The ballot_number and support_oppose_code columns are there, but sparse. So each measure committee was matched to its measure, side and election by reading committee names and checking filings, and each candidate committee was matched to the candidate's name exactly as contest_results spells it (Bunny McFadden is on the ballot as Bunny but her legal name is Chelsea A McFadden; Gary McCoy in D8 is not Guy McCoy in D2).

Cleaning the money, one gotcha at a time

Every row in campaign_transactions is one line on one form. Only some of those forms are money coming in.

Form / scheduleWhat it isCounts as raised?
460 Schedule AMonetary contributionsYes
460 Schedule CIn-kind contributionsYes
460 Schedule IMisc receipts: public financing, refunds, interestYes
F460ALine2, F460CLine2, F460ILine2Unitemized subtotals (gifts under $100)Yes
460 Schedule EPayments madeNo, that's spending
460 Schedule GPayments by a consultant on the committee's behalfNo, already inside E
460 Schedule FUnpaid billsNo
460 Schedule DSummary of support given to othersNo
Form 497 (F497P1)24-hour report of a large late contributionOnly if received after the last 460 period
memo_code = trueInformational, counted elsewhereNo

1. Summing every row overstates the money about 3x

Across the 13 November measure committees, counting only each committee's money for this campaign (gotcha 9), every row summed comes to $79.7M. Money actually raised is $26.7M, a ratio of 2.99x. For the 11 committees with regular filings, it runs from 2.76x to 3.71x per committee.

Take Mayor Lurie's Yes on D, E & F committee. Every row summed: $38.5M. Raised: $13.98M. The extra is:

  • $11.96M of Schedule E payments, which is spending.
  • $7.47M of Schedule G consultant payments. Also spending, and already inside E, so it's counted twice if you add it.
  • $4.32M of Form 497 reports. Of that, $4.21M (37 reports) falls inside periods the committee's regular Form 460 filings already cover, so it repeats contributions that appear there; only $110k (2 reports) is money received after the last filing period.
  • $0.86M of Schedule F unpaid bills.

2. The unitemized lines matter for small-donor candidates

Raised = Schedules A + C + I (non-memo) + the three unitemized subtotal lines + 497s dated after the committee's last 460 period. That reconciles to the Commission's own Summary Totals to the dollar: Manny Yekutiel's committee is $787,554 both ways, and Gary McCoy ($367,465), Theo Ellington ($495,402) and Stephen Sherrill ($731,662 across two committees) match too.

Drop the unitemized lines and small-donor candidates come out low: Yekutiel by $11,321 (1.4%), McCoy by $12,279. For a big-donor committee like Lurie's H committee, the gap was $150. The error lands hardest on exactly the candidates a "money vs grassroots" question is about.

3. Committees rename mid-campaign

Lurie's D/E/F committee first filed as "Clean Up City Hall, Mayor Lurie's Ballot Measure Committee". Under the current name it received $6.22M; under the old one, $7.64M. Filter on the current name and you get $6.2M instead of $13.9M. That's the mistake I made first. Group on filer_nid, never filer_name.

4. June reused the measure letters

Measure letters reset every election, and the June 2, 2026 ballot had its own A through D: an earthquake bond, term limits, a business tax cut and a CEO pay tax. Search committee names for "Yes on C" and you pull in the $6.6M "Yes on C, No on D to Protect San Francisco's Small Businesses" committee and the $3.5M "Yes on D - Stand Up for SF" committee, both about June. That's why committee_measures carries election_date, and every query filters to 2026-11-03.

5. One committee, several measures

One bank account can fund several measures, and the filings don't split it:

CommitteeRaisedMeasures
Lurie's D/E/F committee$13.98MD, E, F
Lurie's H committee$5.41MH, RTM
"Yes on C, No on I"$2.79MC, I

Sum a per-measure money column and you get $62.8M against $26.7M actually raised (2.4x), because Lurie's $14M is counted three times. Either show the full amount per measure and label it as shared, or split it evenly and say so. Never sum across measures.

6. Public financing looks like a donation

SF matches small donations to qualifying candidates with city money. It's booked as a Schedule I receipt from "City and County of San Francisco", so it's in "raised" and in the official totals. Ten supervisor candidates got $1,861,317 between them. Ellington and Yekutiel each got $255,000, about half of what Ellington raised.

If the question is "who funded this candidate", report it separately. And don't filter on the description: one $60,000 payment to DJ Brookter has none, so description LIKE '%public financ%' misses it. Filter on the payer and exclude refunds.

7. A ZIP isn't a neighborhood

Donor rows carry a ZIP. A ZIP spans several precincts and often several neighborhoods, so a plain join to precinct_zips multiplies the money. Split each gift across its ZIP's neighborhoods by each neighborhood's share of the ZIP's precincts. For Lurie's two committees, that puts $4.21M of $18.89M itemized (22%) outside San Francisco, with every SF ZIP mapped.

Two caveats: a company gives from its office ZIP, which inflates downtown (Financial District, South of Market), and the precinct-share split is an approximation.

8. Two result sources disagree slightly

The summary XML and the ranked choice round pages are published on different schedules. In District 4, Alan Wong has 11,296 first choices in the summary and 11,446 in round 1 of the ranked choice page. Use the round pages for ranked choice questions, the summary for everything else, and never mix the two in one calculation.

9. A committee can carry money from an earlier campaign

Committees get reused. "No on G, Save Sunset Dunes" is the same committee (same filer_nid) that ran 2024's "Yes on K, Ocean Beach Park for All". Its filings since 2025 include $792,861 received in 2024 and 2025 for that earlier campaign, against $821,431 in 2026. Count everything and No on G looks twice as well funded as it was. committee_measures carries a money_from date for reused committees, and the queries drop rows dated before it.

Two smaller cases of the same family: a committee with no regular Form 460 filing yet (the DSA committee for Yes on I has only $14,986 of 24-hour reports) drops out of any inner join to the filing periods, and a committee formed late can have no recorded receipts at all (No on B). Both need to stay in the result as rows, not disappear.

What the clean numbers say

Measures

MeasureYes side raisedNo side raisedYes % (SF)Better-funded side ahead?
A Charter changes$452,505$070.51%Yes
B Public bank$392,271$049.96%No
C Housing Trust Fund$4,047,421*$080.27%Yes
D Ballot measure process$13,976,301*$462,33557.54%Yes
E City Administrator$13,976,301*$066.68%Yes
F Executive branch$13,976,301*$062.59%Yes
G Upper Great Highway$166,475$821,43136.43%Yes (no side)
H Muni parcel tax$5,413,977*$072.79%Yes
I Transfer tax changes$711,495$2,792,748*45.26%Yes (no side)
J Foreclosure exemption$237,957$079.47%Yes
RTM Regional transit$5,413,977*$077.91%Yes in SF; decided region-wide

* Includes a shared committee's full amount. $0 means no committee in this data recorded receipts on that side, not that nobody campaigned: Measure B has a No committee with nothing recorded. Yes on I includes $14,986 from a committee that has only filed 24-hour reports. Every local measure needed a simple majority.

The better-funded side is ahead on 10 of 11. But only D, G and I had recorded receipts on both sides among the committees included here, which limits what this comparison can tell us about money's effect.

The exception is Measure B, the public bank. $392k was raised for it, the one committee formed against it has no recorded receipts, and it's trailing by 144 votes (95,222 yes, 95,366 no). It needs 50% + 1, and with mail ballots still being counted it's too close to call. It also split by how people voted: 56.9% yes in person (15,741 to 11,947), 48.8% yes by mail (79,481 to 83,419).

Supervisors

Districts 2, 4, 6, 8 and 10 voted, all by ranked choice. "Spent" is payments made: itemized Schedule E plus the unitemized line for payments under $100, which matches the Ethics Commission's own payments total. "Spent per vote" divides that by first-choice votes.

DistrictCandidateRaisedPublic financingSpentFirst-choice votesSpent per votePreliminary result
2Stephen Sherrill$731,662$252,000$651,27413,952$46.68Leads
4Alan Wong$532,426$252,000$487,72011,296$43.18Leads
4Albert Chow$241,551$176,160$227,6023,073$74.07
6Matt Dorsey$130,689$0$110,9238,396$13.21Leads
8Gary McCoy$367,465$217,524$302,46912,517$24.16Leads (round 3)
8Darshini Patel$169,558$102,417$144,7306,539$22.13
8Manny Yekutiel$787,554$255,000$612,7403,121$196.333rd on first choices
8Michael Nguyen$221,594$133,974$195,9682,767$70.82
10Theo Ellington$495,402$255,000$289,3375,905$49.00Leads (round 9)
10DJ Brookter$199,627$94,200$174,7681,918$91.12
10J.R. Eppler$189,287$123,042$157,6601,533$102.84

Smaller candidates are omitted; the full table comes from the last query below. Brookter's public financing is $94,200 with the payer filter from gotcha 6, and $34,200 with the description filter.

  • The top fundraiser leads in 4 of 5 districts.
  • The exception is District 8. Manny Yekutiel raised the most of any supervisor candidate ($787,554, including $255,000 of public financing) and is in third place on first choices, at $196 of spending per first-choice vote. Gary McCoy raised less than half that and spent $24 per vote.
  • Raising the most and spending the most aren't the same: Sherrill spent the most ($651k vs Yekutiel's $613k).
  • District 6 isn't a "less money wins" story. Dorsey's only opponent raised about $4k.
  • Ranked choice changed no outcome. Every round-1 leader is still ahead after the final round; On the round pages, D8 (McCoy at 49.8% in round 1) and D10 (Ellington at 49.4%, nine rounds) needed later rounds to pass 50%. (The summary release the table uses has McCoy at 50.2% of first choices; that's gotcha 8, two sources on different schedules.)

So, does money win?

In this election, the better-funded side leads almost everywhere. But only three measures had recorded receipts on both sides among the committees in this data, so the 10-of-11 figure says little about money's effect. And there are counterexamples: Measure B had all the recorded money and is trailing, and the best-funded supervisor candidate is in third place. Counterexamples aren't a causal test either; this is a description of one election, not an estimate of what money does.

The more useful lesson for anyone working with this kind of data is that the raw table hands you a number about 3x too big, and the fixes are boring and specific: which form a row is on, which committee it belongs to, and which campaign it was for.

Caveats

  • Preliminary count. Measure B is within 144 votes and can flip. Certification is about a month out.
  • State-filed committees aren't here. Committees that file only with the California Secretary of State are missing from the Ethics Commission data.
  • General purpose PACs aren't attributed. A PAC like the Uber Innovation Ballot Measure Committee (about $23.7M received in filings since January 2025) funds several causes and isn't assigned to any one. "Money per measure" means money raised by the measure committees in committee_measures, not all money spent on the measure. Independent expenditures aren't separated.
  • Spending runs to each committee's latest filing. The last days may be missing or sitting in Schedule F.
  • Candidate "raised" includes public financing.
  • Per-vote figures use first-choice votes, not the final ranked choice round.
  • The committee maps are hand-built. A missed committee changes a total.
  • ZIP to neighborhood is an approximation, and company donors sit at their office ZIP.

The SQL

All BigQuery. The covered CTE finds each committee's last 460 period, so 497s that its regular filings already cover are dropped. It is LEFT JOINed, so a committee with no regular filing keeps its 24-hour reports instead of vanishing from the join. money_from in committee_measures drops a reused committee's money from earlier campaigns.

Money raised per committee

WITH covered AS (
  SELECT filer_nid, MAX(end_date) AS covered_through
  FROM `contextflo-datasets.sf_elections.campaign_transactions`
  WHERE form_type IN ('A', 'C', 'I', 'E')
  GROUP BY filer_nid
)
SELECT t.filer_nid,
  SUM(IF((t.form_type IN ('A', 'C', 'I') AND NOT t.memo_code)
      OR t.form_type IN ('F460ALine2', 'F460CLine2', 'F460ILine2'), t.calculated_amount, 0))
    + SUM(IF(t.form_type = 'F497P1'
      AND t.calculated_date > COALESCE(c.covered_through, DATE '1900-01-01'), t.calculated_amount, 0)) AS amount_raised
FROM `contextflo-datasets.sf_elections.campaign_transactions` t
LEFT JOIN covered c USING (filer_nid)
GROUP BY t.filer_nid

This is all of a committee's money in the loaded filings. For a reused committee, filter rows by its money_from date first, as the next two queries do.

Raised vs every row summed, per November measure committee

WITH committees AS (
  SELECT DISTINCT filer_nid, committee_name, money_from
  FROM `contextflo-datasets.sf_elections.committee_measures`
  WHERE election_date = '2026-11-03'
),
-- This campaign's rows only: a committee reused from an earlier campaign carries a money_from date.
t AS (
  SELECT t.*
  FROM `contextflo-datasets.sf_elections.campaign_transactions` t
  JOIN committees c USING (filer_nid)
  WHERE c.money_from IS NULL OR t.calculated_date >= c.money_from
),
covered AS (
  SELECT filer_nid, MAX(end_date) AS covered_through
  FROM t WHERE form_type IN ('A', 'C', 'I', 'E') GROUP BY filer_nid
)
SELECT c.committee_name,
  ROUND(COALESCE(SUM(t.calculated_amount), 0)) AS every_row_summed,
  ROUND(COALESCE(SUM(IF((t.form_type IN ('A', 'C', 'I') AND NOT t.memo_code)
      OR t.form_type IN ('F460ALine2', 'F460CLine2', 'F460ILine2'), t.calculated_amount, 0)), 0)
    + COALESCE(SUM(IF(t.form_type = 'F497P1'
      AND t.calculated_date > COALESCE(cv.covered_through, DATE '1900-01-01'), t.calculated_amount, 0)), 0)) AS raised,
  ROUND(COALESCE(SUM(IF((t.form_type = 'E' AND NOT t.memo_code) OR t.form_type = 'F460ELine2', t.calculated_amount, 0)), 0)) AS spent
FROM committees c
LEFT JOIN t USING (filer_nid)
LEFT JOIN covered cv USING (filer_nid)
GROUP BY c.committee_name
ORDER BY raised DESC

Money for and against each November measure, with the result

WITH cm AS (
  SELECT *, COUNT(*) OVER (PARTITION BY filer_nid) AS measures_backed
  FROM `contextflo-datasets.sf_elections.committee_measures`
  WHERE election_date = '2026-11-03'
),
committees AS (SELECT DISTINCT filer_nid, money_from FROM cm),
-- This campaign's rows only: a committee reused from an earlier campaign carries a money_from date.
t AS (
  SELECT t.*
  FROM `contextflo-datasets.sf_elections.campaign_transactions` t
  JOIN committees c USING (filer_nid)
  WHERE c.money_from IS NULL OR t.calculated_date >= c.money_from
),
covered AS (
  SELECT filer_nid, MAX(end_date) AS covered_through
  FROM t WHERE form_type IN ('A', 'C', 'I', 'E') GROUP BY filer_nid
),
-- LEFT JOINs keep committees with no regular filing; their 24-hour reports all count as new money.
raised AS (
  SELECT c.filer_nid,
    COALESCE(SUM(IF((t.form_type IN ('A', 'C', 'I') AND NOT t.memo_code)
        OR t.form_type IN ('F460ALine2', 'F460CLine2', 'F460ILine2'), t.calculated_amount, 0)), 0)
      + COALESCE(SUM(IF(t.form_type = 'F497P1'
        AND t.calculated_date > COALESCE(cv.covered_through, DATE '1900-01-01'), t.calculated_amount, 0)), 0) AS raised
  FROM committees c
  LEFT JOIN t USING (filer_nid)
  LEFT JOIN covered cv USING (filer_nid)
  GROUP BY c.filer_nid
),
results AS (
  SELECT measure_code, MAX(IF(choice IN ('YES', 'BONDS - YES'), pct, NULL)) AS yes_pct
  FROM `contextflo-datasets.sf_elections.measure_results`
  GROUP BY measure_code
)
SELECT cm.measure_code,
  ROUND(SUM(IF(cm.position = 'yes', r.raised, 0))) AS yes_side_raised,
  ROUND(SUM(IF(cm.position = 'no', r.raised, 0))) AS no_side_raised,
  COUNTIF(cm.measures_backed > 1) AS shared_committees,
  res.yes_pct
FROM cm
JOIN raised r USING (filer_nid)
JOIN results res USING (measure_code)
GROUP BY cm.measure_code, res.yes_pct
ORDER BY cm.measure_code

Supervisor candidates: money, public financing, spending and votes

WITH t AS (
  SELECT t.*
  FROM `contextflo-datasets.sf_elections.campaign_transactions` t
  JOIN `contextflo-datasets.sf_elections.candidate_committees` cc USING (filer_nid)
),
covered AS (
  SELECT filer_nid, MAX(end_date) AS covered_through
  FROM t WHERE form_type IN ('A', 'C', 'I', 'E') GROUP BY filer_nid
),
-- LEFT JOINs keep candidates whose committee has no regular filing yet.
money AS (
  SELECT cc.contest, cc.candidate,
    COALESCE(SUM(IF((t.form_type IN ('A', 'C', 'I') AND NOT t.memo_code)
        OR t.form_type IN ('F460ALine2', 'F460CLine2', 'F460ILine2'), t.calculated_amount, 0)), 0)
      + COALESCE(SUM(IF(t.form_type = 'F497P1'
        AND t.calculated_date > COALESCE(cv.covered_through, DATE '1900-01-01'), t.calculated_amount, 0)), 0) AS raised,
    COALESCE(SUM(IF(t.form_type = 'I' AND LOWER(t.transaction_last_name) = 'city and county of san francisco'
      AND LOWER(COALESCE(t.transaction_description, '')) NOT LIKE '%refund%', t.calculated_amount, 0)), 0) AS public_financing,
    -- Itemized payments plus the unitemized line (payments under $100); matches the official payments total.
    COALESCE(SUM(IF((t.form_type = 'E' AND NOT t.memo_code) OR t.form_type = 'F460ELine2', t.calculated_amount, 0)), 0) AS spent
  FROM `contextflo-datasets.sf_elections.candidate_committees` cc
  LEFT JOIN t USING (filer_nid)
  LEFT JOIN covered cv USING (filer_nid)
  GROUP BY cc.contest, cc.candidate
)
SELECT m.contest, m.candidate,
  ROUND(m.raised) AS raised,
  ROUND(m.public_financing) AS public_financing,
  ROUND(m.spent) AS spent,
  r.votes AS first_choice_votes,
  ROUND(m.spent / NULLIF(r.votes, 0), 2) AS spent_per_vote
FROM money m
JOIN `contextflo-datasets.sf_elections.contest_results` r
  ON r.contest = m.contest AND r.choice = m.candidate
WHERE m.contest LIKE 'MEMBER, BOARD OF SUPERVISORS%'
ORDER BY m.contest, r.votes DESC

Try it yourself

The cleaned dataset, the definitions above (what counts as "raised", how shared committees are handled) and these reference queries are loaded in the Contextflo playground. No account needed: ask it about any measure or district and check its SQL against the queries here.

FAQ

Did the better-funded side win in San Francisco's November 2026 election? In preliminary results as of Nov 4, the better-funded side is ahead on 10 of 11 measures (local Measures A to J plus the regional transit measure). But only three measures (D, G and I) had recorded receipts on both sides among the committees in this data, which limits what the comparison can say about money's effect.

Why does summing SF campaign finance transactions give the wrong total? The transactions table mixes money received with money spent, consultant payments already counted in spending, unpaid bills, and 24-hour reports that repeat later contributions. Across the 13 November measure committees, summing every row gives $79.7M; money actually raised for this campaign is $26.7M.

Where does the SF campaign finance data come from? SF Ethics Commission filings published on DataSF (Campaign Finance - Transactions), joined to the Department of Elections' preliminary results. Committees that file only with the California Secretary of State are not included.