@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.
@@ -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).