Last active
October 22, 2025 17:20
-
-
Save earthboundkid/ccb3fb4d4440f8c5261833bfd4a6f2fe to your computer and use it in GitHub Desktop.
Crunch voter turnout information from registration data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "cmp" | |
| "encoding/csv" | |
| "flag" | |
| "fmt" | |
| "maps" | |
| "os" | |
| "path/filepath" | |
| "slices" | |
| "strconv" | |
| "strings" | |
| csvutil "github.com/earthboundkid/csv/v2" | |
| ) | |
| func main() { | |
| app := parse() | |
| if err := app.run(); err != nil { | |
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | |
| os.Exit(1) | |
| } | |
| } | |
| type appEnv struct { | |
| in, out string | |
| } | |
| func parse() *appEnv { | |
| var app appEnv | |
| flag.StringVar(&app.in, "input", "voters.csv", "path` for input") | |
| flag.StringVar(&app.out, "out", ".", "`directory` for output") | |
| flag.CommandLine.Init("crunch", flag.ExitOnError) | |
| flag.Parse() | |
| return &app | |
| } | |
| func (app *appEnv) run() error { | |
| return app.counted() | |
| } | |
| type Counts struct { | |
| Total int | |
| Turnout int | |
| Male int | |
| MaleTurnout int | |
| Female int | |
| FemaleTurnout int | |
| Republican int | |
| RepublicanTurnout int | |
| Democratic int | |
| DemocraticTurnout int | |
| Other int | |
| OtherTurnout int | |
| } | |
| type InputRow struct { | |
| Gender string `csv:"Gender"` | |
| DOB string `csv:"DOB"` | |
| FirstRegistered string `csv:"First registered"` | |
| LastVoted string `csv:"Last voted"` | |
| Party string `csv:"Party"` | |
| ZIP string `csv:"ZIP"` | |
| HouseDistrict string `csv:"State House District"` | |
| SenateDistrict string `csv:"State Senate District"` | |
| CongressionalDistrict string `csv:"Congressional District"` | |
| County string `csv:"County"` | |
| } | |
| func (app *appEnv) counted() error { | |
| f, err := os.Open(app.in) | |
| if err != nil { | |
| return err | |
| } | |
| defer f.Close() | |
| counts := make(map[string]*Counts) | |
| var pennCounts Counts | |
| var row InputRow | |
| for range csvutil.Scan(csvutil.Options{ | |
| Reader: f, | |
| }, &row) { | |
| pennCounts.increment(row) | |
| county := "County " + row.County | |
| setDefault(counts, county).increment(row) | |
| setDefault(counts, row.CongressionalDistrict).increment(row) | |
| setDefault(counts, row.HouseDistrict).increment(row) | |
| setDefault(counts, row.SenateDistrict).increment(row) | |
| } | |
| out, err := os.Create(filepath.Join(app.out, "counts.csv")) | |
| if err != nil { | |
| return err | |
| } | |
| defer func() { | |
| err = cmp.Or(err, out.Close()) | |
| }() | |
| w := csv.NewWriter(out) | |
| if err := w.Write([]string{ | |
| 0: "Area", | |
| 1: "Total registered", | |
| 2: "Recent voter turnout", | |
| 3: "Turnout percentage", | |
| 4: "Males registered", | |
| 5: "Male turnout", | |
| 6: "Male turnout percentage", | |
| 7: "Male percentage of registration", | |
| 8: "Male percentage of total turnout", | |
| 9: "Male turnout performance", | |
| 10: "Females registered", | |
| 11: "Female turnout", | |
| 12: "Female turnout percentage", | |
| 13: "Female percentage of registration", | |
| 14: "Female percentage of total turnout", | |
| 15: "Female turnout performance", | |
| 16: "Republicans registered", | |
| 17: "Republican turnout", | |
| 18: "Republican turnout percentage", | |
| 19: "Republican percentage of registration", | |
| 20: "Republican percentage of total turnout", | |
| 21: "Republican turnout performance", | |
| 22: "Democrats registered", | |
| 23: "Democratic turnout", | |
| 24: "Democratic turnout percentage", | |
| 25: "Democratic percentage of registration", | |
| 26: "Democratic percentage of total turnout", | |
| 27: "Democratic turnout performance", | |
| 28: "Other party registered", | |
| 29: "Other party turnout", | |
| 30: "Other party turnout percentage", | |
| 31: "Other party percentage of registration", | |
| 32: "Other party percentage of total turnout", | |
| 33: "Other party turnout performance", | |
| }); err != nil { | |
| return err | |
| } | |
| if err := w.Write(pennCounts.toOutputRow("Pennsylvania")); err != nil { | |
| return err | |
| } | |
| for _, k := range slices.Sorted(maps.Keys(counts)) { | |
| if err := w.Write(counts[k].toOutputRow(k)); err != nil { | |
| return err | |
| } | |
| } | |
| w.Flush() | |
| return w.Error() | |
| } | |
| func (c *Counts) increment(row InputRow) { | |
| c.Total++ | |
| turnout := row.LastVoted == "05/20/2025" | |
| if turnout { | |
| c.Turnout++ | |
| } | |
| if row.Gender == "M" { | |
| c.Male++ | |
| } | |
| if row.Gender == "M" && turnout { | |
| c.MaleTurnout++ | |
| } | |
| if row.Gender == "F" { | |
| c.Female++ | |
| } | |
| if row.Gender == "F" && turnout { | |
| c.FemaleTurnout++ | |
| } | |
| if row.Party == "R" { | |
| c.Republican++ | |
| if turnout { | |
| c.RepublicanTurnout++ | |
| } | |
| } | |
| if row.Party == "D" { | |
| c.Democratic++ | |
| if turnout { | |
| c.DemocraticTurnout++ | |
| } | |
| } | |
| if row.Party != "R" && row.Party != "D" { | |
| c.Other++ | |
| if turnout { | |
| c.OtherTurnout++ | |
| } | |
| } | |
| } | |
| func (c *Counts) toOutputRow(area string) []string { | |
| return []string{ | |
| // 0: "Area", | |
| 0: strings.TrimPrefix(area, "County "), | |
| // 1: "Total registered", | |
| 1: strconv.Itoa(c.Total), | |
| // 2: "Recent voter turnout", | |
| 2: strconv.Itoa(c.Turnout), | |
| // 3: "Turnout percentage", | |
| 3: pct(c.Turnout, c.Total), | |
| // 4: "Males registered", | |
| // 5: "Male turnout", | |
| // 6: "Male turnout percentage", | |
| // 7: "Male percentage of registration", | |
| // 8: "Male percentage of total turnout", | |
| // 9: "Male turnout performance", | |
| 4: strconv.Itoa(c.Male), | |
| 5: strconv.Itoa(c.MaleTurnout), | |
| 6: pct(c.MaleTurnout, c.Male), | |
| 7: pct(c.Male, c.Total), | |
| 8: pct(c.MaleTurnout, c.Turnout), | |
| 9: performance(c.Male, c.MaleTurnout, c.Total, c.Turnout), | |
| // 10: "Females registered", | |
| // 11: "Female turnout", | |
| // 12: "Female turnout percentage", | |
| // 13: "Female percentage of registration", | |
| // 14: "Female percentage of total turnout", | |
| // 15: "Female turnout performance", | |
| 10: strconv.Itoa(c.Female), | |
| 11: strconv.Itoa(c.FemaleTurnout), | |
| 12: pct(c.FemaleTurnout, c.Female), | |
| 13: pct(c.Female, c.Total), | |
| 14: pct(c.FemaleTurnout, c.Turnout), | |
| 15: performance(c.Female, c.FemaleTurnout, c.Total, c.Turnout), | |
| // 16: "Republicans registered", | |
| // 17: "Republican turnout", | |
| // 18: "Republican turnout percentage", | |
| // 19: "Republican percentage of registration", | |
| // 20: "Republican percentage of total turnout", | |
| // 21: "Republican turnout performance", | |
| 16: strconv.Itoa(c.Republican), | |
| 17: strconv.Itoa(c.RepublicanTurnout), | |
| 18: pct(c.RepublicanTurnout, c.Republican), | |
| 19: pct(c.Republican, c.Total), | |
| 20: pct(c.RepublicanTurnout, c.Turnout), | |
| 21: performance(c.Republican, c.RepublicanTurnout, c.Total, c.Turnout), | |
| // 22: "Democrats registered", | |
| // 23: "Democratic turnout", | |
| // 24: "Democratic turnout percentage", | |
| // 25: "Democratic percentage of registration", | |
| // 26: "Democratic percentage of total turnout", | |
| // 27: "Democratic turnout performance", | |
| 22: strconv.Itoa(c.Democratic), | |
| 23: strconv.Itoa(c.DemocraticTurnout), | |
| 24: pct(c.DemocraticTurnout, c.Democratic), | |
| 25: pct(c.Democratic, c.Total), | |
| 26: pct(c.DemocraticTurnout, c.Turnout), | |
| 27: performance(c.Democratic, c.DemocraticTurnout, c.Total, c.Turnout), | |
| // 28: "Other party registered", | |
| // 29: "Other party turnout", | |
| // 30: "Other party turnout percentage", | |
| // 31: "Other party percentage of registration", | |
| // 32: "Other party percentage of total turnout", | |
| // 33: "Other party turnout performance", | |
| 28: strconv.Itoa(c.Other), | |
| 29: strconv.Itoa(c.OtherTurnout), | |
| 30: pct(c.OtherTurnout, c.Other), | |
| 31: pct(c.Other, c.Total), | |
| 32: pct(c.OtherTurnout, c.Turnout), | |
| 33: performance(c.Other, c.OtherTurnout, c.Total, c.Turnout), | |
| } | |
| } | |
| // pct returns a string representing num divided by denom as a percentage | |
| func pct(num, denom int) string { | |
| return fmt.Sprintf("%.1f%%", 100*float64(num)/float64(denom)) | |
| } | |
| // performance returns a percentage represening how much more or less a group turned out versus their percentage of registrants | |
| func performance(groupReg, groupTurnout, total, turnout int) string { | |
| g, gt, r, t := float64(groupReg), float64(groupTurnout), float64(total), float64(turnout) | |
| perf := (gt * r / g / t) - 1 | |
| return fmt.Sprintf("%.1f%%", 100*perf) | |
| } | |
| // setDefault is like Python's dict.setdefault. | |
| // It adds a map entry for key if one doesn't exist | |
| // and returns the existing entry if it does. | |
| func setDefault[M ~map[K]*V, K comparable, V any](m M, k K) *V { | |
| if v := m[k]; v != nil { | |
| return v | |
| } | |
| v := new(V) | |
| m[k] = v | |
| return v | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment