@dra2020/district-analytics 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +5 -0
- package/dist/_api.d.ts +27 -0
- package/dist/_data.d.ts +130 -0
- package/dist/analyze.d.ts +4 -0
- package/dist/cli.js +12041 -0
- package/dist/cli.js.map +1 -0
- package/dist/cohesive.d.ts +4 -0
- package/dist/compact.d.ts +5 -0
- package/dist/constants.d.ts +6 -0
- package/dist/district-analytics.js.map +1 -0
- package/dist/equal.d.ts +4 -0
- package/dist/geofeature.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/minority.d.ts +3 -0
- package/dist/political.d.ts +8 -0
- package/dist/preprocess.d.ts +2 -0
- package/dist/report.d.ts +15 -0
- package/dist/settings.d.ts +5 -0
- package/dist/test/_cli.d.ts +5 -0
- package/dist/types.d.ts +110 -0
- package/dist/utils.d.ts +28 -0
- package/dist/valid.d.ts +8 -0
- package/jestconfig.json +14 -0
- package/main.js +4 -0
- package/package.json +4 -9
- package/src/_api.ts +120 -0
- package/src/_data.ts +572 -0
- package/src/analyze.ts +92 -0
- package/src/cohesive.ts +156 -0
- package/src/compact.ts +208 -0
- package/src/constants.ts +371 -0
- package/src/equal.ts +75 -0
- package/src/geofeature.ts +138 -0
- package/src/index.ts +6 -0
- package/src/minority.ts +70 -0
- package/src/political.ts +114 -0
- package/src/preprocess.ts +132 -0
- package/src/report.ts +997 -0
- package/src/settings.ts +20 -0
- package/src/types.ts +185 -0
- package/src/utils.ts +245 -0
- package/src/valid.ts +275 -0
- package/tsconfig.json +25 -0
- package/tslint.json +3 -0
- package/types/polygon-clipping/index.d.ts +1 -0
- package/webpack.config.js +73 -0
package/src/cohesive.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//
|
|
2
|
+
// "COHESIVE" - We're naming this category which is about county splitting.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as T from './types'
|
|
6
|
+
import * as U from './utils';
|
|
7
|
+
import * as S from './settings';
|
|
8
|
+
|
|
9
|
+
import * as D from './_data'
|
|
10
|
+
import { AnalyticsSession } from './_api';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export function doCountySplits(s: AnalyticsSession): T.TestEntry {
|
|
14
|
+
let test = s.getTest(T.Test.CountySplits) as T.TestEntry;
|
|
15
|
+
|
|
16
|
+
// THE THREE VALUES TO DETERMINE FOR A PLAN
|
|
17
|
+
let unexpectedSplits: number = 0;
|
|
18
|
+
let unexpectedAffected: number = 0;
|
|
19
|
+
let countiesSplitUnexpectedly = [];
|
|
20
|
+
|
|
21
|
+
// FIRST, ANALYZE THE COUNTY SPLITING FOR THE PLAN
|
|
22
|
+
|
|
23
|
+
// Pivot census totals into county-district "splits"
|
|
24
|
+
let countiesByDistrict = s.districts.statistics[D.DistrictField.CountySplits];
|
|
25
|
+
// countiesByDistrict = countiesByDistrict.slice(1, -1);
|
|
26
|
+
|
|
27
|
+
// Find the single-county districts, i.e., districts NOT split across counties.
|
|
28
|
+
// Ignore the dummy unassigned 0 and N+1 summary "districts."
|
|
29
|
+
let singleCountyDistricts = [];
|
|
30
|
+
for (let d = 1; d <= s.state.nDistricts; d++) {
|
|
31
|
+
// See if there's only one county partition
|
|
32
|
+
let nCountiesInDistrict = 0;
|
|
33
|
+
for (let c = 0; c < s.counties.nCounties; c++) {
|
|
34
|
+
if (countiesByDistrict[d][c] > 0) {
|
|
35
|
+
nCountiesInDistrict += 1;
|
|
36
|
+
if (nCountiesInDistrict > 1) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// If so, save the district
|
|
42
|
+
if (nCountiesInDistrict == 1) {
|
|
43
|
+
singleCountyDistricts.push(d);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Process the splits/partitions in the plan:
|
|
48
|
+
// - Count the total # of partitions,
|
|
49
|
+
// - Find the counties split across districts, and
|
|
50
|
+
// - Accumulate the number people affected (except when single-county districts)
|
|
51
|
+
let nPartitionsOverall: number = 0;
|
|
52
|
+
let splitCounties = new Set(); // The counties that are split across districts
|
|
53
|
+
let totalAffected: number = 0; // The total population affected by those splits
|
|
54
|
+
|
|
55
|
+
for (let c = 0; c < s.counties.nCounties; c++) {
|
|
56
|
+
let nCountyParts = 0;
|
|
57
|
+
let subtotal = 0;
|
|
58
|
+
|
|
59
|
+
for (let d = 1; d <= s.state.nDistricts; d++) {
|
|
60
|
+
if (countiesByDistrict[d][c] > 0) {
|
|
61
|
+
nPartitionsOverall += 1;
|
|
62
|
+
nCountyParts += 1;
|
|
63
|
+
if (!(U.arrayContains(singleCountyDistricts, d))) {
|
|
64
|
+
subtotal += countiesByDistrict[d][c];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (nCountyParts > 1) {
|
|
69
|
+
splitCounties.add(c);
|
|
70
|
+
totalAffected += subtotal;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Convert county ordinals to FIPS codes
|
|
75
|
+
let splitCountiesFIPS = U.getSelectObjectKeys(s.counties.index, [...splitCounties]);
|
|
76
|
+
|
|
77
|
+
// THEN TAKE ACCOUNT OF THE COUNTY SPLITTING THAT IS EXPECTED (REQUIRED)
|
|
78
|
+
|
|
79
|
+
// Compute the total number of splits this way, in case any counties are split
|
|
80
|
+
// more than once. I.e., it's not just len(all_counties_split).
|
|
81
|
+
let nSplits = nPartitionsOverall - s.counties.nCounties;
|
|
82
|
+
|
|
83
|
+
// Determine the number of *unexpected* splits. NOTE: Prevent negative numbers,
|
|
84
|
+
// in case you have a plan the *doesn't* split counties that would have to be
|
|
85
|
+
// split due to their size.
|
|
86
|
+
unexpectedSplits = Math.max(0, nSplits - s.state.expectedSplits);
|
|
87
|
+
|
|
88
|
+
// Subtract off the total population that *has* to be affected by splits,
|
|
89
|
+
// because their counties are too big. NOTE: Again, prevent negative numbers,
|
|
90
|
+
// in case you have a plan the *doesn't* split counties that would have to be
|
|
91
|
+
// split due to their size.
|
|
92
|
+
unexpectedAffected = U.trim(Math.max(0, totalAffected - s.state.expectedAffected) / s.state.totalPop);
|
|
93
|
+
|
|
94
|
+
// Find the counties that are split *unexpectedly*. From all the counties that
|
|
95
|
+
// are split, remove those that *have* to be split, because they are bigger than
|
|
96
|
+
// a district.
|
|
97
|
+
let countiesSplitUnexpectedlyFIPS = [];
|
|
98
|
+
for (let fips of splitCountiesFIPS) {
|
|
99
|
+
if (!(U.arrayContains(s.state.tooBigFIPS, fips))) {
|
|
100
|
+
countiesSplitUnexpectedlyFIPS.push(fips);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ... and convert the FIPS codes to county names.
|
|
105
|
+
for (let fips of countiesSplitUnexpectedlyFIPS) {
|
|
106
|
+
countiesSplitUnexpectedly.push(s.counties.nameFromFIPS(fips));
|
|
107
|
+
}
|
|
108
|
+
countiesSplitUnexpectedly = countiesSplitUnexpectedly.sort();
|
|
109
|
+
|
|
110
|
+
// Cache the results in the test
|
|
111
|
+
test['score'] = unexpectedAffected; // TODO - Use Moon's complexity metric here
|
|
112
|
+
test['details'] = {
|
|
113
|
+
'unexpectedSplits': unexpectedSplits,
|
|
114
|
+
'unexpectedAffected': unexpectedAffected,
|
|
115
|
+
'countiesSplitUnexpectedly': countiesSplitUnexpectedly
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return test;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
// 2 - THE COMPLEXITY ANALYTIC NEEDS THE FOLLOWING DATA:
|
|
123
|
+
//
|
|
124
|
+
// If a map is already in simplified (mixed) form, the complexity analytic needs
|
|
125
|
+
// two pieces of data:
|
|
126
|
+
// - The counts of features by summary level--i.e., the numbers of counties, tracts,
|
|
127
|
+
// block groups, and blocks in a state; and
|
|
128
|
+
// - The map -- So it can count the features by summary level in the map,
|
|
129
|
+
// as well as the number of BG’s that are split.
|
|
130
|
+
//
|
|
131
|
+
// TODO - Where would the state counts come from? Preprocessed and passed in, or
|
|
132
|
+
// done in a one-time initialization call (which would require a full set of
|
|
133
|
+
// block geo_id's for the state).
|
|
134
|
+
//
|
|
135
|
+
// However, if a map is not yet (fully) simplified, then determining the
|
|
136
|
+
// complexity of a map also requires a preprocessed summary level hierarchy, so
|
|
137
|
+
// you can get the child features (e.g., tracts) of a parent feature (e.g.,
|
|
138
|
+
// a county).
|
|
139
|
+
//
|
|
140
|
+
// NOTE - I have script for producing this hierarchy which we could repurpose.
|
|
141
|
+
//
|
|
142
|
+
// TODO - For mixed map processing--specfically to find the neighbors of a feature
|
|
143
|
+
// that are actually in the map (as opposed to just neighbors at the same
|
|
144
|
+
// summary level in the static graph)--you need a special hierarchy that
|
|
145
|
+
// distinguishes between the 'interior' and 'edge children of a feature.
|
|
146
|
+
//
|
|
147
|
+
// NOTE - The script noted above does this.
|
|
148
|
+
|
|
149
|
+
export function doPlanComplexity(s: AnalyticsSession): T.TestEntry {
|
|
150
|
+
let test = s.getTest(T.Test.Complexity) as T.TestEntry;
|
|
151
|
+
|
|
152
|
+
console.log("TODO - Calculating plan complexity ...");
|
|
153
|
+
|
|
154
|
+
return test;
|
|
155
|
+
}
|
|
156
|
+
|
package/src/compact.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
//
|
|
2
|
+
// COMPACT
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { gfArea, gfPerimeter, gfDiameter } from './geofeature';
|
|
6
|
+
|
|
7
|
+
import * as T from './types';
|
|
8
|
+
import * as U from './utils';
|
|
9
|
+
import * as S from './settings';
|
|
10
|
+
|
|
11
|
+
import { AnalyticsSession } from './_api';
|
|
12
|
+
|
|
13
|
+
// Measures of compactness compare district shapes to various ideally compact
|
|
14
|
+
// benchmarks, such as circles. All else equal, more compact districts are better.
|
|
15
|
+
//
|
|
16
|
+
// There are four popular measures of compactness. They either focus on how
|
|
17
|
+
// dispersed or how indented a shapes are.
|
|
18
|
+
//
|
|
19
|
+
// These first two measures are the most important:
|
|
20
|
+
//
|
|
21
|
+
// Reock is the primary measure of the dispersion of district shapes, calculated
|
|
22
|
+
// as “the area of the district to the area of the minimum spanning circle that
|
|
23
|
+
// can enclose the district.”
|
|
24
|
+
//
|
|
25
|
+
// R = A / A(Minimum Bounding Circle)
|
|
26
|
+
// R = A / (π * (D / 2)^2)
|
|
27
|
+
//
|
|
28
|
+
// R = 4A / πD^2
|
|
29
|
+
//
|
|
30
|
+
// where A is the area of the district and D is the diameter of the minimum
|
|
31
|
+
// bounding circle.
|
|
32
|
+
//
|
|
33
|
+
// Polsby-Popper is the primary measure of the indendentation of district shapes,
|
|
34
|
+
// calculated as the “the ratio of the area of the district to the area of a circle
|
|
35
|
+
// whose circumference is equal to the perimeter of the district.”
|
|
36
|
+
//
|
|
37
|
+
// PP = A / A(C)
|
|
38
|
+
//
|
|
39
|
+
// where C is that circle. In other words:
|
|
40
|
+
//
|
|
41
|
+
// P = 2πRc and A(C) = π(P / 2π)^2
|
|
42
|
+
//
|
|
43
|
+
// where P is the perimeter of the district and Rc is the radius of the circle.
|
|
44
|
+
//
|
|
45
|
+
// Hence, the measure simplifies to:
|
|
46
|
+
//
|
|
47
|
+
// PP = 4π * (A / P^2)
|
|
48
|
+
//
|
|
49
|
+
// I propose that we use these two, normalize them, and weight equally to determine
|
|
50
|
+
// our compactness value.
|
|
51
|
+
//
|
|
52
|
+
// These second two measures may be used to complement the primary ones above:
|
|
53
|
+
//
|
|
54
|
+
// Convex Hull is a secondary measure of the dispersion of district shapes, calculated
|
|
55
|
+
// as “the ratio of the district area to the area of the minimum convex bounding
|
|
56
|
+
// polygon (also known as a convex hull) enclosing the district.”
|
|
57
|
+
//
|
|
58
|
+
// CH = A / A(Convex Hull)
|
|
59
|
+
//
|
|
60
|
+
// where a convex hull is the minimum perimeter that encloses all points in a shape,
|
|
61
|
+
// basically the shortest unstretched rubber band that fits around the shape.
|
|
62
|
+
//
|
|
63
|
+
// Schwartzberg is a secondary measure of the degree of indentation of district
|
|
64
|
+
// shapes, calculated as “the ratio of the perimeter of the district to the circumference
|
|
65
|
+
// of a circle whose area is equal to the area of the district.”
|
|
66
|
+
//
|
|
67
|
+
// S = 1 / (P / C)
|
|
68
|
+
//
|
|
69
|
+
// where P is the perimeter of the district and C is the circumference of the circle.
|
|
70
|
+
// The radius of the circle is:
|
|
71
|
+
//
|
|
72
|
+
// Rc = SQRT(A / π)
|
|
73
|
+
//
|
|
74
|
+
// So, the circumference of the circle is:
|
|
75
|
+
//
|
|
76
|
+
// C = 2πRc or C = 2π * SQRT(A / π)
|
|
77
|
+
//
|
|
78
|
+
// Hence:
|
|
79
|
+
//
|
|
80
|
+
// S = 1 (P / 2π * SQRT(A / π))
|
|
81
|
+
//
|
|
82
|
+
// S = (2π * SQRT(A / π)) / P
|
|
83
|
+
//
|
|
84
|
+
// All these measures produce values between 0 and 1, with 0 being the least compact
|
|
85
|
+
// and 1 being the most compact. Sometimes these values are multiplied by 100 to
|
|
86
|
+
// give values between 0 and 100.
|
|
87
|
+
//
|
|
88
|
+
// For each measure, the compactness of a set of Congressional districts is the
|
|
89
|
+
// average of that measure for all the districts.
|
|
90
|
+
//
|
|
91
|
+
|
|
92
|
+
// Calculate Reock compactness:
|
|
93
|
+
// reock = (4 * a) / (math.pi * d**2)
|
|
94
|
+
// NOTE - Depends on extractDistrictProperties running first
|
|
95
|
+
export function doReock(s: AnalyticsSession, bLog: boolean = false): T.TestEntry {
|
|
96
|
+
let test = s.getTest(T.Test.Reock) as T.TestEntry;
|
|
97
|
+
|
|
98
|
+
// Calculate Reock scores by district
|
|
99
|
+
let scores: number[] = [];
|
|
100
|
+
|
|
101
|
+
for (let districtID = 1; districtID <= s.state.nDistricts; districtID++) {
|
|
102
|
+
let districtProps = s.districts.getGeoProperties(districtID);
|
|
103
|
+
let a = districtProps[T.DistrictShapeProperty.Area];
|
|
104
|
+
let d = districtProps[T.DistrictShapeProperty.Diameter];
|
|
105
|
+
|
|
106
|
+
let reock = (4 * a) / (Math.PI * d ** 2);
|
|
107
|
+
|
|
108
|
+
// Save each district score
|
|
109
|
+
scores.push(reock);
|
|
110
|
+
|
|
111
|
+
// Echo the results by district
|
|
112
|
+
if (bLog) console.log("Reock for district", districtID, "=", reock);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Populate the test entry
|
|
116
|
+
let averageReock = U.avgArray(scores);
|
|
117
|
+
|
|
118
|
+
test['score'] = U.trim(averageReock);
|
|
119
|
+
test['details'] = {}; // TODO - Any details?
|
|
120
|
+
|
|
121
|
+
return test;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Calculate Polsby-Popper compactness measures:
|
|
125
|
+
// polsby_popper = (4 * math.pi) * (a / p**2)
|
|
126
|
+
// NOTE - Depends on extractDistrictProperties running first
|
|
127
|
+
export function doPolsbyPopper(s: AnalyticsSession, bLog: boolean = false): T.TestEntry {
|
|
128
|
+
let test = s.getTest(T.Test.PolsbyPopper) as T.TestEntry;
|
|
129
|
+
|
|
130
|
+
// Calculate Polsby-Popper scores by district
|
|
131
|
+
let scores: number[] = [];
|
|
132
|
+
|
|
133
|
+
for (let districtID = 1; districtID <= s.state.nDistricts; districtID++) {
|
|
134
|
+
let districtProps = s.districts.getGeoProperties(districtID);
|
|
135
|
+
let a = districtProps[T.DistrictShapeProperty.Area];
|
|
136
|
+
let p = districtProps[T.DistrictShapeProperty.Perimeter];
|
|
137
|
+
|
|
138
|
+
let polsbyPopper = (4 * Math.PI) * (a / p ** 2);
|
|
139
|
+
|
|
140
|
+
// Save each district score
|
|
141
|
+
scores.push(polsbyPopper);
|
|
142
|
+
|
|
143
|
+
// Echo the results by district
|
|
144
|
+
if (bLog) console.log("Polsby-Popper for district", districtID, "=", polsbyPopper);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Populate the test entry
|
|
148
|
+
let averagePolsbyPopper = U.avgArray(scores);
|
|
149
|
+
|
|
150
|
+
test['score'] = U.trim(averagePolsbyPopper);
|
|
151
|
+
test['details'] = {}; // TODO - Any details?
|
|
152
|
+
|
|
153
|
+
return test;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// HELPER TO EXTRACT PROPERTIES OF DISTRICT SHAPES
|
|
158
|
+
|
|
159
|
+
export function extractDistrictProperties(s: AnalyticsSession, bLog: boolean = false): void {
|
|
160
|
+
for (let i = 1; i <= s.state.nDistricts; i++) {
|
|
161
|
+
let j = i - 1; // TODO - Terry: How do you get away w/o this?!?
|
|
162
|
+
let poly = s.districts.getShape(j);
|
|
163
|
+
|
|
164
|
+
// TODO - Bundle these calls?
|
|
165
|
+
let area: number = gfArea(poly);
|
|
166
|
+
let perimeter: number = gfPerimeter(poly);
|
|
167
|
+
let diameter: number = gfDiameter(poly);
|
|
168
|
+
|
|
169
|
+
let props: T.DistrictShapeProperties = [0, 0, 0]; // TODO - Terry?!?
|
|
170
|
+
props[T.DistrictShapeProperty.Area] = area;
|
|
171
|
+
props[T.DistrictShapeProperty.Diameter] = diameter;
|
|
172
|
+
props[T.DistrictShapeProperty.Perimeter] = perimeter;
|
|
173
|
+
|
|
174
|
+
s.districts.setGeoProperties(i, props);
|
|
175
|
+
|
|
176
|
+
if (bLog) console.log("District", i, "A =", area, "P =", perimeter, "D =", diameter);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
// SAVE THESE NOTES, IN CASE WE NEED TO REWORK HOW WE PERFORM THESE CALCS.
|
|
182
|
+
// THEY REFLECT HOW I DID THEM IN PYTHON.
|
|
183
|
+
//
|
|
184
|
+
// THE COMPACTNESS ANALYTICS NEED THE FOLLOWING DATA,
|
|
185
|
+
// IN ADDITION TO THE MAP (IDEALLY, GEO_IDS INDEXED BY DISTRICT_ID)
|
|
186
|
+
//
|
|
187
|
+
// Shapes by geo_id
|
|
188
|
+
//
|
|
189
|
+
// If we need/want to speed compactness calculations up, we'll need
|
|
190
|
+
// to calculate the perimeters and diameters (and areas) of districts implicitly,
|
|
191
|
+
// which will require identifying district boundaries initially and updating
|
|
192
|
+
// them incrementally as districts are (re)assigned.
|
|
193
|
+
//
|
|
194
|
+
// A district's boundary info is the set/list of features that constitute the
|
|
195
|
+
// district's border along with each boundary feature's neighbors. Hence, this
|
|
196
|
+
// requires a contiguity graph with the lengths of shared edges between features
|
|
197
|
+
// precomputed.
|
|
198
|
+
//
|
|
199
|
+
// NOTE - I can write up (if not implement) the logic for determining what shapes
|
|
200
|
+
// constitute a district's boundary. There are a few nuances.
|
|
201
|
+
//
|
|
202
|
+
// If we have to optimize like this when we generalize to mixed maps, the
|
|
203
|
+
// determination of "neighbors in the map" and the length of shared borders
|
|
204
|
+
// (for determining a district perimeters) becomes more complicated and dynamic.
|
|
205
|
+
//
|
|
206
|
+
// NOTE - Again, I can write up (if not implement) the logic for both of these.
|
|
207
|
+
// They are a bit tricky and require special preprocessing of the summary level
|
|
208
|
+
// hierarchy (which I also have a script for that we can repurpose).
|