Last active
November 18, 2025 21:17
-
-
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 ( | |
| "bufio" | |
| "cmp" | |
| "encoding/csv" | |
| "flag" | |
| "fmt" | |
| "maps" | |
| "os" | |
| "path/filepath" | |
| "slices" | |
| "strconv" | |
| "strings" | |
| csvutil "github.com/earthboundkid/csv/v2" | |
| ) | |
| const ElectionDay = "11/04/2025" | |
| func main() { | |
| app := parse() | |
| if err := app.run(); err != nil { | |
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | |
| os.Exit(1) | |
| } | |
| } | |
| type mode int | |
| const ( | |
| filterMode mode = iota + 1 | |
| crunchMode | |
| idMode | |
| ) | |
| type appEnv struct { | |
| mode | |
| in, out string | |
| } | |
| func parse() *appEnv { | |
| flag.CommandLine.Init("voterreg", flag.ExitOnError) | |
| flag.Parse() | |
| var app appEnv | |
| if flag.NArg() < 1 { | |
| return &app | |
| } | |
| switch flag.Arg(0) { | |
| case "filter": | |
| app.mode = filterMode | |
| case "crunch": | |
| app.mode = crunchMode | |
| case "id": | |
| app.mode = idMode | |
| } | |
| fl := flag.NewFlagSet("voterreg", flag.ExitOnError) | |
| fl.StringVar(&app.in, "in", "voters.csv", "`file path` for input") | |
| fl.StringVar(&app.out, "out", ".", "`directory path` for output") | |
| fl.Parse(flag.Args()[1:]) | |
| return &app | |
| } | |
| func (app *appEnv) run() error { | |
| switch app.mode { | |
| case crunchMode: | |
| return app.counted() | |
| case filterMode: | |
| return app.filter() | |
| case idMode: | |
| return app.addID() | |
| default: | |
| return fmt.Errorf("invalid mode. Must be crunch, filter, or id") | |
| } | |
| } | |
| type Counts struct { | |
| TotalReg int | |
| Turnout int | |
| MaleReg int | |
| MaleTurnout int | |
| FemaleReg int | |
| FemaleTurnout int | |
| RepublicanReg int | |
| RepublicanTurnout int | |
| DemocraticReg int | |
| DemocraticTurnout int | |
| OtherReg int | |
| OtherTurnout int | |
| } | |
| type CrunchInputRow 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"` | |
| PrecinctCode string `csv:"Precinct code"` | |
| Ward string `csv:"Ward"` | |
| } | |
| func (row CrunchInputRow) CountyPrecinct() string { | |
| if row.County == "PHILADELPHIA" { | |
| return "ZPrecinct " + row.County + "-" + strings.TrimPrefix(row.Ward, "WD") | |
| } | |
| return "ZPrecinct " + row.County + "-" + row.PrecinctCode | |
| } | |
| 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 CrunchInputRow | |
| for range csvutil.Scan(csvutil.Options{ | |
| Reader: f, | |
| }, &row) { | |
| pennCounts.increment(row) | |
| // Prefix names of counties so they sort together | |
| 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) | |
| setDefault(counts, row.CountyPrecinct()).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(outputHeader); err != nil { | |
| return err | |
| } | |
| if err := w.Write(pennCounts.toOutputRow("Pennsylvania")); err != nil { | |
| return err | |
| } | |
| for _, k := range slices.Sorted(maps.Keys(counts)) { | |
| // Strip sorting prefix, if present | |
| area := strings.TrimPrefix(k, "County ") | |
| area = strings.TrimPrefix(area, "ZPrecinct ") | |
| if err := w.Write(counts[k].toOutputRow(area)); err != nil { | |
| return err | |
| } | |
| } | |
| w.Flush() | |
| return w.Error() | |
| } | |
| func (c *Counts) increment(row CrunchInputRow) { | |
| c.TotalReg++ | |
| turnout := row.LastVoted == ElectionDay | |
| if turnout { | |
| c.Turnout++ | |
| } | |
| if row.Gender == "M" { | |
| c.MaleReg++ | |
| } | |
| if row.Gender == "M" && turnout { | |
| c.MaleTurnout++ | |
| } | |
| if row.Gender == "F" { | |
| c.FemaleReg++ | |
| } | |
| if row.Gender == "F" && turnout { | |
| c.FemaleTurnout++ | |
| } | |
| if row.Party == "R" { | |
| c.RepublicanReg++ | |
| if turnout { | |
| c.RepublicanTurnout++ | |
| } | |
| } | |
| if row.Party == "D" { | |
| c.DemocraticReg++ | |
| if turnout { | |
| c.DemocraticTurnout++ | |
| } | |
| } | |
| if row.Party != "R" && row.Party != "D" { | |
| c.OtherReg++ | |
| if turnout { | |
| c.OtherTurnout++ | |
| } | |
| } | |
| } | |
| var outputHeader = []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", | |
| } | |
| func (c *Counts) toOutputRow(area string) []string { | |
| return []string{ | |
| // 0: "Area", | |
| 0: area, | |
| // 1: "Total registered", | |
| 1: strconv.Itoa(c.TotalReg), | |
| // 2: "Recent voter turnout", | |
| 2: strconv.Itoa(c.Turnout), | |
| // 3: "Turnout percentage", | |
| 3: pct(c.Turnout, c.TotalReg), | |
| // 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.MaleReg), | |
| 5: strconv.Itoa(c.MaleTurnout), | |
| 6: pct(c.MaleTurnout, c.MaleReg), | |
| 7: pct(c.MaleReg, c.TotalReg), | |
| 8: pct(c.MaleTurnout, c.Turnout), | |
| 9: performance(c.MaleReg, c.MaleTurnout, c.TotalReg, 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.FemaleReg), | |
| 11: strconv.Itoa(c.FemaleTurnout), | |
| 12: pct(c.FemaleTurnout, c.FemaleReg), | |
| 13: pct(c.FemaleReg, c.TotalReg), | |
| 14: pct(c.FemaleTurnout, c.Turnout), | |
| 15: performance(c.FemaleReg, c.FemaleTurnout, c.TotalReg, 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.RepublicanReg), | |
| 17: strconv.Itoa(c.RepublicanTurnout), | |
| 18: pct(c.RepublicanTurnout, c.RepublicanReg), | |
| 19: pct(c.RepublicanReg, c.TotalReg), | |
| 20: pct(c.RepublicanTurnout, c.Turnout), | |
| 21: performance(c.RepublicanReg, c.RepublicanTurnout, c.TotalReg, 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.DemocraticReg), | |
| 23: strconv.Itoa(c.DemocraticTurnout), | |
| 24: pct(c.DemocraticTurnout, c.DemocraticReg), | |
| 25: pct(c.DemocraticReg, c.TotalReg), | |
| 26: pct(c.DemocraticTurnout, c.Turnout), | |
| 27: performance(c.DemocraticReg, c.DemocraticTurnout, c.TotalReg, 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.OtherReg), | |
| 29: strconv.Itoa(c.OtherTurnout), | |
| 30: pct(c.OtherTurnout, c.OtherReg), | |
| 31: pct(c.OtherReg, c.TotalReg), | |
| 32: pct(c.OtherTurnout, c.Turnout), | |
| 33: performance(c.OtherReg, c.OtherTurnout, c.TotalReg, 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 | |
| } | |
| func printOnce(s string, m map[string]int) { | |
| m[s]++ | |
| if n := m[s]; n > 1 { | |
| return | |
| } | |
| fmt.Println(s) | |
| } | |
| func printSorted[K cmp.Ordered, V any](m map[K]V) { | |
| for _, k := range slices.Sorted(maps.Keys(m)) { | |
| fmt.Println(k, m[k]) | |
| } | |
| } | |
| func (app *appEnv) filter() (err error) { | |
| f, err := os.Open(app.in) | |
| if err != nil { | |
| return err | |
| } | |
| defer f.Close() | |
| out, err := os.Create(filepath.Join(app.out, "filtered.csv")) | |
| if err != nil { | |
| return err | |
| } | |
| defer func() { | |
| err = cmp.Or(err, out.Close()) | |
| }() | |
| type InputRow struct { | |
| Title string `csv:"Title"` | |
| First string `csv:"First"` | |
| Middle string `csv:"Middle"` | |
| Last string `csv:"Last"` | |
| Suffix string `csv:"Suffix"` | |
| Gender string `csv:"Gender"` | |
| DOB string `csv:"DOB"` | |
| FirstRegistered string `csv:"First registered"` | |
| LastVoted string `csv:"Last voted"` | |
| StatusLastUpdated string `csv:"Status last updated"` | |
| Status string `csv:"Status"` | |
| Party string `csv:"Party affiliation"` | |
| HouseNo string `csv:"House number"` | |
| HouseSuffix string `csv:"House number suffix"` | |
| Street string `csv:"Street name"` | |
| Apartment string `csv:"Apartment number"` | |
| City string `csv:"City"` | |
| State string `csv:"State"` | |
| ZIP string `csv:"ZIP"` | |
| PrecinctCode string `csv:"Precinct code"` | |
| Ward string `csv:"Ward"` | |
| HouseDistrict string `csv:"State House District"` | |
| SenateDistrict string `csv:"State Senate District"` | |
| CongressionalDistrict string `csv:"Congressional District"` | |
| Mailing1 string `csv:"Mailing address 1"` | |
| Mailing2 string `csv:"Mailing address 2"` | |
| Home string `csv:"Home phone"` | |
| Registration string `csv:"Registration method"` | |
| County string `csv:"County"` | |
| } | |
| w := csv.NewWriter(out) | |
| if err := w.Write([]string{ | |
| "Row", | |
| "Gender", | |
| "DOB", | |
| "First registered", | |
| "Last voted", | |
| "Party", | |
| "ZIP", | |
| "State House District", | |
| "State Senate District", | |
| "Congressional District", | |
| "Precinct code", | |
| "Ward", | |
| "County", | |
| }); err != nil { | |
| return err | |
| } | |
| n := 0 | |
| var row InputRow | |
| for range csvutil.Scan(csvutil.Options{ | |
| Reader: f, | |
| }, &row) { | |
| if row.Status != "A" { | |
| continue | |
| } | |
| n++ | |
| if err := w.Write([]string{ | |
| strconv.Itoa(n), | |
| row.Gender, | |
| row.DOB, | |
| row.FirstRegistered, | |
| row.LastVoted, | |
| row.Party, | |
| row.ZIP, | |
| row.HouseDistrict, | |
| row.SenateDistrict, | |
| row.CongressionalDistrict, | |
| row.PrecinctCode, | |
| row.Ward, | |
| row.County, | |
| }); err != nil { | |
| return err | |
| } | |
| } | |
| w.Flush() | |
| if err := w.Error(); err != nil { | |
| return err | |
| } | |
| fmt.Println("rows:", n) | |
| return nil | |
| } | |
| func (app *appEnv) addID() error { | |
| // Open the input file | |
| in, err := os.Open(app.in) | |
| if err != nil { | |
| return err | |
| } | |
| defer in.Close() | |
| out, err := os.Create(filepath.Join(app.out, "voter-id.csv")) | |
| if err != nil { | |
| return err | |
| } | |
| defer out.Close() | |
| s := bufio.NewScanner(in) | |
| w := bufio.NewWriter(out) | |
| n := 0 | |
| for s.Scan() { | |
| line := s.Text() | |
| if n == 0 { | |
| _, err := w.WriteString("rowid,") | |
| if err != nil { | |
| return err | |
| } | |
| } else { | |
| _, err := fmt.Fprintf(w, "%d,", n) | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| _, err := w.WriteString(line + "\n") | |
| if err != nil { | |
| return err | |
| } | |
| n++ | |
| } | |
| if err := s.Err(); err != nil { | |
| return err | |
| } | |
| return w.Flush() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment