@dra2020/district-analytics 4.0.0 → 4.1.0

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/dist/cli.js CHANGED
@@ -89432,7 +89432,7 @@ function reduceCSplits(CxD, districtTotals) {
89432
89432
  for (let i = 1; i < nD; i++) {
89433
89433
  let split_total = CxDreducedC[i][j];
89434
89434
  if (split_total > 0) {
89435
- if (areRoughlyEqual(split_total, districtTotals[i - 1])) {
89435
+ if (U.areRoughlyEqual(split_total, districtTotals[i - 1], S.EQUAL_TOLERANCE)) {
89436
89436
  CxDreducedC[0][j] += split_total;
89437
89437
  CxDreducedC[i][j] = 0;
89438
89438
  }
@@ -89455,7 +89455,7 @@ function reduceDSplits(CxD, countyTotals) {
89455
89455
  for (let j = 1; j < nC; j++) {
89456
89456
  let split_total = CxDreducedD[i][j];
89457
89457
  if (split_total > 0) {
89458
- if (areRoughlyEqual(split_total, countyTotals[j - 1])) {
89458
+ if (U.areRoughlyEqual(split_total, countyTotals[j - 1], S.EQUAL_TOLERANCE)) {
89459
89459
  CxDreducedD[i][0] += split_total;
89460
89460
  CxDreducedD[i][j] = 0;
89461
89461
  }
@@ -89519,13 +89519,6 @@ function calcDistrictFractions(CxD, districtTotals) {
89519
89519
  return g;
89520
89520
  }
89521
89521
  exports.calcDistrictFractions = calcDistrictFractions;
89522
- // Deal with decimal census "counts" due to disagg/re-agg
89523
- function areRoughlyEqual(x, y) {
89524
- let delta = Math.abs(x - y);
89525
- let result = (delta < S.EQUAL_TOLERANCE) ? true : false;
89526
- return result;
89527
- }
89528
- exports.areRoughlyEqual = areRoughlyEqual;
89529
89522
  function splitScore(splits) {
89530
89523
  let e;
89531
89524
  if (splits.length > 0) {
@@ -89800,7 +89793,7 @@ exports.scorePolsbyPopper = scorePolsbyPopper;
89800
89793
  /*! exports provided: partisan, minority, traditionalPrinciples, default */
89801
89794
  /***/ (function(module) {
89802
89795
 
89803
- module.exports = JSON.parse("{\"partisan\":{\"bias\":{\"range\":[0,0.2],\"weight\":[50,80]},\"impact\":{\"weight\":[50,0],\"threshold\":4},\"competitiveness\":{\"overall\":{\"range\":[0,0.67],\"weight\":25},\"marginal\":{\"range\":[0,0.85],\"weight\":75},\"range\":[0.45,0.55],\"distribution\":[0.25,0.75],\"weight\":[0,20]},\"weight\":[100,80]},\"minority\":{\"range\":[0.37,0.5],\"distribution\":[0.25,0.75],\"shift\":0.12,\"bonus\":20},\"traditionalPrinciples\":{\"compactness\":{\"reock\":{\"range\":[0.25,0.5],\"weight\":50},\"polsby\":{\"range\":[0.1,0.5],\"weight\":50},\"weight\":[0,50]},\"splitting\":{\"county\":{\"range\":[1,1.71],\"allowableSplitsMultiplier\":1.5,\"weight\":50},\"district\":{\"range\":[1,1.5],\"weight\":50},\"weight\":[0,50]},\"popdev\":{\"range\":[[0.0075,0.002],[0.1,-1]],\"weight\":[0,0]},\"weight\":[0,20]}}");
89796
+ module.exports = JSON.parse("{\"partisan\":{\"bias\":{\"range\":[0,0.2],\"weight\":[50,80]},\"impact\":{\"weight\":[50,0],\"threshold\":4},\"competitiveness\":{\"overall\":{\"range\":[0,0.67],\"weight\":25},\"marginal\":{\"range\":[0,0.85],\"weight\":75},\"range\":[0.45,0.55],\"distribution\":[0.25,0.75],\"weight\":[0,20]},\"bonus\":2,\"weight\":[100,80]},\"minority\":{\"range\":[0.37,0.5],\"distribution\":[0.25,0.75],\"shift\":0.12,\"bonus\":20},\"traditionalPrinciples\":{\"compactness\":{\"reock\":{\"range\":[0.25,0.5],\"weight\":50},\"polsby\":{\"range\":[0.1,0.5],\"weight\":50},\"weight\":[0,50]},\"splitting\":{\"county\":{\"range\":[1,1.71],\"allowableSplitsMultiplier\":1.5,\"weight\":50},\"district\":{\"range\":[1,1.5],\"weight\":50},\"weight\":[0,50]},\"popdev\":{\"range\":[[0.0075,0.002],[0.1,-1]],\"weight\":[0,0]},\"weight\":[0,20]}}");
89804
89797
 
89805
89798
  /***/ }),
89806
89799
 
@@ -89842,6 +89835,11 @@ function impactWeight(context, overridesJSON) {
89842
89835
  return iW;
89843
89836
  }
89844
89837
  exports.impactWeight = impactWeight;
89838
+ function winnerBonus(overridesJSON) {
89839
+ const bonus = config_json_1.default.partisan.bonus;
89840
+ return bonus;
89841
+ }
89842
+ exports.winnerBonus = winnerBonus;
89845
89843
  // The maximum # of unearned seats that scores positively
89846
89844
  function unearnedThreshold(overridesJSON) {
89847
89845
  const threshold = config_json_1.default.partisan.impact.threshold;
@@ -90156,12 +90154,13 @@ function evalMinorityOpportunity(p, bLog = false) {
90156
90154
  }
90157
90155
  }
90158
90156
  // Sum the # of level 1 (37–50%) and level 2 (> 50%) opportunity districts - ignore total minority
90159
- let l1 = 0;
90160
- let l2 = 0;
90161
- bucketsByDemo.slice(1).forEach(function (buckets) {
90162
- l1 += buckets[0 /* Level1 */];
90163
- l2 += buckets[1 /* Level2 */];
90164
- });
90157
+ // let l1: number = 0;
90158
+ // let l2: number = 0;
90159
+ // bucketsByDemo.slice(1).forEach(function (buckets: number[]): void
90160
+ // {
90161
+ // l1 += buckets[T.OpportunityBucket.Level1];
90162
+ // l2 += buckets[T.OpportunityBucket.Level2]
90163
+ // });
90165
90164
  // Sum the # of opportunity districts - ignore total minority
90166
90165
  const oD = U.sumArray(opptyByDemo.slice(1));
90167
90166
  // Sum the # of proportion districts - ignore total minority
@@ -90173,8 +90172,8 @@ function evalMinorityOpportunity(p, bLog = false) {
90173
90172
  averageDVf: averageDVf,
90174
90173
  bucketsByDemographic: bucketsByDemo
90175
90174
  },
90176
- nOpportunity1: l1,
90177
- nOpportunity2: l2,
90175
+ // nOpportunity1: l1,
90176
+ // nOpportunity2: l2,
90178
90177
  nProportional: pD,
90179
90178
  opportunityDistricts: oD,
90180
90179
  score: score
@@ -90193,17 +90192,22 @@ function calcDistrictsByDemo(MfArray, N) {
90193
90192
  return districtsByDemo;
90194
90193
  }
90195
90194
  exports.calcDistrictsByDemo = calcDistrictsByDemo;
90195
+ // NOTE - Shift minority proportions up, so 37% minority scores like 49% share,
90196
+ // but use the uncompressed seat probability distribution. This makes a 37%
90197
+ // district have a 40% chance of winning, a 42% district have an 84% chance,
90198
+ // and a 50% district have a 99% chance. Below 37% has no chance.
90199
+ //
90196
90200
  function estMinorityOpportunity(Mf) {
90201
+ // NOTE - Switch to compress the probability distribution
90202
+ const bCompress = false;
90203
+ const dist = bCompress ? C.minorityOpportunityDistribution() : [0.0, 1.0];
90204
+ const range = C.minorityOpportunityRange();
90197
90205
  const _normalizer = new normalize_1.Normalizer(Mf);
90198
- // NOTE - Shift proportions, so 37% minority scores like 49% partisan
90199
90206
  const shift = C.minorityShift();
90200
90207
  _normalizer.wipNum += shift;
90201
- const range = C.minorityOpportunityDistribution();
90202
- const distBeg = range[C.BEG];
90203
- const distEnd = range[C.END];
90204
- _normalizer.clip(distBeg, distEnd);
90205
- _normalizer.unitize(distBeg, distEnd);
90206
- const oppty = partisan_1.estSeatProbability(_normalizer.wipNum, range);
90208
+ _normalizer.clip(dist[C.BEG], dist[C.END]);
90209
+ _normalizer.unitize(dist[C.BEG], dist[C.END]);
90210
+ const oppty = (Mf < range[C.BEG]) ? 0.0 : partisan_1.estSeatProbability(_normalizer.wipNum, dist);
90207
90211
  return oppty;
90208
90212
  }
90209
90213
  exports.estMinorityOpportunity = estMinorityOpportunity;
@@ -90350,20 +90354,36 @@ const normalize_1 = __webpack_require__(/*! ./normalize */ "./src/normalize.ts")
90350
90354
  // NOTE - I'm passing T.VfArray's into everything. District indices = array indices.
90351
90355
  // NOTE - I do not (cannot) assume that the values are sorted.
90352
90356
  // SCORE BIAS & COMPETITIVENESS
90353
- /* SCORECARD FIELDS
90357
+ /* SCORECARD FIELDS:
90354
90358
 
90355
- * ^S# [bestS] = the Democratic seats closest to proportional
90356
- * ^S% [bestSf] = the corresponding Democratic seat share
90359
+ * ^S# [propS] = the Democratic seats closest to proportional
90360
+ * ^S% [propSf] = the corresponding Democratic seat share
90357
90361
  * S! [fptpS] = the estimated number of Democratic seats using first past the post
90358
- * S# [probableS] = the estimated Democratic seats, using seat probabilities
90359
- * S% [probableSf] = the estimated Democratic seat share fraction, calculated as S# / N
90362
+ * S# [estS] = the estimated Democratic seats, using seat probabilities
90363
+ * S% [estSf] = the estimated Democratic seat share fraction, calculated as S# / N
90364
+
90365
+ * Bs50 [s50V] = Seat bias as a fraction of N
90366
+ * Bv50 [v50S] = Votes bias as a fraction
90367
+ * decl [decl] = Declination
90368
+ * gSym [gSym] = Global symmetry
90369
+
90370
+ * EG [EG] = Efficiency gap as a fraction
90371
+ * sVg [BsGf] = Seats bias @ <V> (geometric)
90372
+ * prop [prop] = Disproportionality
90373
+ * MM [mMs] = Mean – median difference using statewide Vf
90374
+ * TO [tOf] = Turnout bias
90375
+ * MM' [mMd] = Mean – median difference using average district v
90376
+ * LO [LO] = Lopsided outcomes
90377
+
90360
90378
  * B% [bias]= the bias calculated as S% – ^S%
90361
90379
  * B$ [bias.score] = the bias score normalized [0–100]
90362
90380
 
90363
90381
  * UE# [unearnedS] = the number of unearned seats (R = positive; D = negative)
90364
90382
  * I$ [impact.score] -- the unearned seats normalized [0–100] as impact
90365
90383
 
90366
- * R# [r] = the responsiveness at V%
90384
+ * R [bigR] = Overall responsiveness
90385
+ * r [littleR] = The point responsiveness at V%
90386
+ * MIR [MIR] = Minimal inverse responsiveness
90367
90387
  * rD [rD] = the estimated # of responsive districts (using probabilities)
90368
90388
  * rD% [rDf] = the estimated # of responsive districts, as a fraction of N
90369
90389
 
@@ -90379,23 +90399,42 @@ const normalize_1 = __webpack_require__(/*! ./normalize */ "./src/normalize.ts")
90379
90399
  * >P$ [score2] = the combined partisan score used to compare plans *within* a state, along with traditional districting principles
90380
90400
 
90381
90401
  */
90382
- function scorePartisan(Vf, VfArray, bAll = false, bConstrained = true) {
90402
+ function scorePartisan(Vf, VfArray, options) {
90403
+ const bAlternateMetrics = options.alternates;
90404
+ const bConstrained = options.constrained;
90405
+ const shift = options.shift;
90383
90406
  const N = VfArray.length;
90384
- const bestS = bestSeats(N, Vf);
90385
- const bestSf = bestSeatShare(bestS, N);
90386
- const fptpS = bAll ? estFPTPSeats(VfArray) : undefined;
90407
+ const propS = propSeats(N, Vf);
90408
+ const propSf = propSeatShare(propS, N);
90409
+ const fptpS = bAlternateMetrics ? estFPTPSeats(VfArray) : undefined;
90387
90410
  const range = bConstrained ? C.competitiveDistribution() : undefined;
90388
- const probableS = estprobableSeats(VfArray, range);
90389
- const probableSf = estprobableSeatShare(probableS, N);
90390
- const bias = estbias(probableSf, bestSf);
90391
- const biasScore = scorebias(bias, Vf, probableSf, N);
90392
- const unearnedS = estunearnedSeats(bestS, probableS);
90393
- const impactScore = scoreImpact(unearnedS);
90394
- const bInferSV = (bAll || (!bConstrained));
90395
- const inferredSVpoints = bInferSV ? inferSVpoints(Vf, VfArray, range) : undefined;
90396
- const R = bInferSV ? estResponsiveness(Vf, inferredSVpoints) : undefined;
90397
- const rD = bInferSV ? estResponsiveDistricts(VfArray) : undefined;
90398
- const rDf = bInferSV ? estResponsiveDistrictsShare(rD, N) : undefined;
90411
+ const estS = estSeats(VfArray, range);
90412
+ const estSf = estSeatShare(estS, N);
90413
+ const bias = estBias(estSf, propSf);
90414
+ const biasScore = scorebias(bias, Vf, estSf);
90415
+ const unearnedS = estUnearnedSeats(propS, estS);
90416
+ const impactScore = scoreImpact(unearnedS, Vf, N);
90417
+ // Calculate additional alternate metrics for reference
90418
+ // NOTE - Use the uncompressed seat probability function
90419
+ const inferredSVpoints = bAlternateMetrics ? inferSVpoints(Vf, VfArray, shift) : undefined;
90420
+ const TOf = bAlternateMetrics ? calcTurnoutBias(Vf, VfArray) : undefined;
90421
+ const Bs50 = bAlternateMetrics ? estPartisanBias(inferredSVpoints, N) : undefined;
90422
+ const Bs50f = (!(Bs50 === undefined)) ? U.trim(Bs50 / N) : undefined;
90423
+ const Bv50f = bAlternateMetrics ? estVotesBias(inferredSVpoints, N) : undefined;
90424
+ const decl = bAlternateMetrics ? calcDeclination(VfArray) : undefined;
90425
+ const gSym = bAlternateMetrics ? calcGlobalSymmetry(inferredSVpoints, Bs50f) : undefined;
90426
+ const EG = bAlternateMetrics ? calcEfficiencyGap(Vf, estSf) : undefined;
90427
+ const BsGf = bAlternateMetrics ? estGeometricSeatsBias(Vf, inferredSVpoints) : undefined;
90428
+ const prop = bAlternateMetrics ? calcDisproportionality(Vf, estSf) : undefined;
90429
+ const mMs = bAlternateMetrics ? estMeanMedianDifference(VfArray, Vf) : undefined;
90430
+ const mMd = bAlternateMetrics ? estMeanMedianDifference(VfArray) : undefined;
90431
+ const LO = bAlternateMetrics ? calcLopsidedOutcomes(VfArray) : undefined;
90432
+ // Calculate alternate responsiveness metrics for reference
90433
+ const bigR = bAlternateMetrics ? calcBigR(Vf, estSf) : undefined;
90434
+ const littleR = bAlternateMetrics ? estResponsiveness(Vf, inferredSVpoints) : undefined;
90435
+ const MIR = bAlternateMetrics ? calcMinimalInverseResponsiveness(Vf, littleR) : undefined;
90436
+ const rD = (!bConstrained || bAlternateMetrics) ? estResponsiveDistricts(VfArray) : undefined;
90437
+ const rDf = bAlternateMetrics ? estResponsiveDistrictsShare(rD, N) : undefined;
90399
90438
  const Cn = countCompetitiveDistricts(VfArray);
90400
90439
  const cD = bConstrained ? estCompetitiveDistricts(VfArray) : rD;
90401
90440
  const cDf = estCompetitiveDistrictsShare(cD, N);
@@ -90403,12 +90442,11 @@ function scorePartisan(Vf, VfArray, bAll = false, bConstrained = true) {
90403
90442
  const Md = estMarginalCompetitiveDistricts(Mrange, VfArray);
90404
90443
  const Mdf = estMarginalCompetitiveShare(Md, Mrange);
90405
90444
  const competitivenessScore = scoreCompetitiveness(Mdf, cDf);
90406
- const biasScoring = {
90407
- bestS: bestS,
90408
- bestSf: bestSf,
90409
- fptpS: fptpS,
90410
- probableS: probableS,
90411
- probableSf: probableSf,
90445
+ let biasScoring = {
90446
+ propS: propS,
90447
+ propSf: propSf,
90448
+ estS: estS,
90449
+ estSf: estSf,
90412
90450
  bias: bias,
90413
90451
  score: biasScore
90414
90452
  };
@@ -90417,9 +90455,6 @@ function scorePartisan(Vf, VfArray, bAll = false, bConstrained = true) {
90417
90455
  score: impactScore
90418
90456
  };
90419
90457
  let competitiveScoring = {
90420
- r: R,
90421
- rD: rD,
90422
- rDf: rDf,
90423
90458
  c: Cn,
90424
90459
  cD: cD,
90425
90460
  cDf: cDf,
@@ -90428,8 +90463,22 @@ function scorePartisan(Vf, VfArray, bAll = false, bConstrained = true) {
90428
90463
  mDf: Mdf,
90429
90464
  score: competitivenessScore
90430
90465
  };
90431
- if (bAll) {
90432
- competitiveScoring.r = R;
90466
+ if (bAlternateMetrics) {
90467
+ biasScoring.tOf = TOf;
90468
+ biasScoring.fptpS = fptpS;
90469
+ biasScoring.s50V = Bs50f;
90470
+ biasScoring.v50S = Bv50f;
90471
+ biasScoring.decl = decl;
90472
+ biasScoring.gSym = gSym;
90473
+ biasScoring.eG = EG;
90474
+ biasScoring.sVg = BsGf;
90475
+ biasScoring.prop = prop;
90476
+ biasScoring.mMs = mMs;
90477
+ biasScoring.mMd = mMd;
90478
+ biasScoring.lO = LO;
90479
+ competitiveScoring.bigR = bigR;
90480
+ competitiveScoring.littleR = littleR;
90481
+ competitiveScoring.mIR = MIR;
90433
90482
  competitiveScoring.rD = rD;
90434
90483
  competitiveScoring.rDf = rDf;
90435
90484
  }
@@ -90458,35 +90507,49 @@ function weightPartisan(bS, iS, cS, context) {
90458
90507
  return score;
90459
90508
  }
90460
90509
  exports.weightPartisan = weightPartisan;
90461
- function printPartisanScorecardHeader() {
90462
- console.log('XX, Name, N, 1/N%, V%, ^S#, ^S%, S!, S#, S%, B%, B$, UE#, I$, R#, Rd, Rd%, C#, Cd, Cd%, beg, end, Md, Md%, C$, <P$, >P$');
90463
- }
90464
- exports.printPartisanScorecardHeader = printPartisanScorecardHeader;
90465
- function printPartisanScorecardRow(xx, name, N, Vf, s) {
90466
- console.log('%s, %s, %i, %f, %f, %i, %f, %i, %f, %f, %f, %i, %f, %i, %f, %f, %f, %i, %f, %f, %i, %i, %f, %f, %i, %i, %i', xx, name, N, U.trim(1 / N), Vf, s.bias.bestS, s.bias.bestSf, s.bias.fptpS, s.bias.probableS, s.bias.probableSf, s.bias.bias, s.bias.score, s.impact.unearnedS, s.impact.score, s.competitiveness.r, s.competitiveness.rD, s.competitiveness.rDf, s.competitiveness.c, s.competitiveness.cD, s.competitiveness.cDf, s.competitiveness.mRange[C.BEG], s.competitiveness.mRange[C.END], s.competitiveness.mD, s.competitiveness.mDf, s.competitiveness.score, s.score, s.score2);
90510
+ function extraBonus(Vf) {
90511
+ const okExtra = (0.5 - Vf) * (C.winnerBonus() - 1.0);
90512
+ return U.trim(okExtra);
90467
90513
  }
90468
- exports.printPartisanScorecardRow = printPartisanScorecardRow;
90469
- function scorebias(rawbias, Vf, Sf, N) {
90514
+ exports.extraBonus = extraBonus;
90515
+ function scorebias(rawBias, Vf, Sf) {
90470
90516
  if (isAntimajoritarian(Vf, Sf)) {
90471
90517
  return 0;
90472
90518
  }
90473
90519
  else {
90474
- const _normalizer = new normalize_1.Normalizer(rawbias);
90520
+ // Adjust bias to incorporate an acceptable winner's bonus based on Vf
90521
+ const extra = extraBonus(Vf);
90522
+ const adjusted = adjustBias(Vf, rawBias, extra);
90523
+ // Then normalize
90524
+ const _normalizer = new normalize_1.Normalizer(adjusted);
90475
90525
  const worst = C.biasRange()[C.BEG];
90476
90526
  const best = C.biasRange()[C.END];
90527
+ _normalizer.positive();
90477
90528
  _normalizer.clip(worst, best);
90478
90529
  _normalizer.unitize(worst, best);
90479
90530
  _normalizer.invert();
90480
- // _normalizer.decay();
90481
90531
  _normalizer.rescale();
90482
90532
  const score = _normalizer.normalizedNum;
90483
90533
  return score;
90484
90534
  }
90485
90535
  }
90486
90536
  exports.scorebias = scorebias;
90537
+ // Adjust bias to account for a winner's bonus
90538
+ function adjustBias(Vf, bias, extra) {
90539
+ if (Vf > 0.5)
90540
+ return Math.min(bias - extra, 0);
90541
+ else
90542
+ return Math.max(bias - extra, 0);
90543
+ }
90544
+ exports.adjustBias = adjustBias;
90487
90545
  // Normalize unearned seats
90488
- function scoreImpact(rawUE) {
90489
- const _normalizer = new normalize_1.Normalizer(rawUE);
90546
+ function scoreImpact(rawUE, Vf, N) {
90547
+ // Adjust impact to incorporate an acceptable winner's bonus based on Vf
90548
+ const extra = extraBonus(Vf);
90549
+ const adjustedBias = adjustBias(Vf, rawUE / N, extra);
90550
+ const adjustedImpact = adjustedBias * N;
90551
+ // Then normalize
90552
+ const _normalizer = new normalize_1.Normalizer(adjustedImpact);
90490
90553
  const worst = C.unearnedThreshold();
90491
90554
  const best = 0.0;
90492
90555
  _normalizer.positive();
@@ -90538,11 +90601,11 @@ const { erf } = __webpack_require__(/*! mathjs */ "./node_modules/mathjs/main/es
90538
90601
  // Estimate the probability of a seat win for district, given a Vf
90539
90602
  function estSeatProbability(Vf, range) {
90540
90603
  if (range) {
90604
+ // If a range is provided, it defines end points of a compressed probability
90605
+ // distribution. These *aren't* the points where races start or stop being
90606
+ // contested, just the end points of a distribution that yields the desired
90607
+ // probabilities in the typical competitive range [45-55%].
90541
90608
  const _normalizer = new normalize_1.Normalizer(Vf);
90542
- // The end points of a compressed probability distribution
90543
- // NOTE - These aren't the points where races start or stop being contested,
90544
- // just the end points of a distribution that yields the desired behavior
90545
- // in the typical competitive range [45-55%].
90546
90609
  const distBeg = range[C.BEG];
90547
90610
  const distEnd = range[C.END];
90548
90611
  _normalizer.clip(distBeg, distEnd);
@@ -90550,6 +90613,7 @@ function estSeatProbability(Vf, range) {
90550
90613
  return seatProbabilityFn(_normalizer.wipNum);
90551
90614
  }
90552
90615
  else {
90616
+ // Otherwise, use the full probability distribution.
90553
90617
  return seatProbabilityFn(Vf);
90554
90618
  }
90555
90619
  }
@@ -90564,19 +90628,27 @@ function estDistrictResponsiveness(Vf) {
90564
90628
  return U.trim(1.0 - 4.0 * Math.pow((estSeatProbability(Vf) - 0.5), 2));
90565
90629
  }
90566
90630
  exports.estDistrictResponsiveness = estDistrictResponsiveness;
90567
- function inferSVpoints(Vf, VfArray, range) {
90631
+ function inferSVpoints(Vf, VfArray, shift, range) {
90568
90632
  const nDistricts = VfArray.length;
90569
90633
  let SVpoints = [];
90570
90634
  for (let shiftedVf of shiftRange()) {
90571
- const shiftedVPI = shiftDistricts(Vf, VfArray, shiftedVf);
90572
- const shiftedSf = estprobableSeats(shiftedVPI, range) / nDistricts;
90635
+ const shiftedVPI = shiftDistricts(Vf, VfArray, shiftedVf, shift);
90636
+ const shiftedSf = estSeats(shiftedVPI, range) / nDistricts;
90573
90637
  SVpoints.push({ v: shiftedVf, s: shiftedSf });
90638
+ // TODO - Why can't I trim these? Why does that only break the Hypotheticals?!?
90639
+ // SVpoints.push({v: U.trim(Number(shiftedVf)), s: shiftedSf});
90574
90640
  }
90575
90641
  return SVpoints;
90576
90642
  }
90577
90643
  exports.inferSVpoints = inferSVpoints;
90578
- // Shift districts *proportionally*
90579
- function shiftDistricts(Vf, VfArray, shiftedVf) {
90644
+ function shiftDistricts(Vf, VfArray, shiftedVf, shift) {
90645
+ if (shift == 0 /* Proportional */)
90646
+ return shiftProportionally(Vf, VfArray, shiftedVf);
90647
+ else
90648
+ return shiftUniformly(Vf, VfArray, shiftedVf);
90649
+ }
90650
+ // Shift districts proportionally
90651
+ function shiftProportionally(Vf, VfArray, shiftedVf) {
90580
90652
  let shiftedVfArray;
90581
90653
  if (shiftedVf < Vf) {
90582
90654
  // Shift down: D's to R's
@@ -90594,40 +90666,48 @@ function shiftDistricts(Vf, VfArray, shiftedVf) {
90594
90666
  }
90595
90667
  return shiftedVfArray;
90596
90668
  }
90597
- // Generate v's from 0.25 to 0.75 in 0.005 (1/2%) increments
90669
+ // Shift districts uniformly
90670
+ function shiftUniformly(Vf, VfArray, shiftedVf) {
90671
+ const shift = shiftedVf - Vf;
90672
+ const shiftedVfArray = VfArray.map((v => v + shift));
90673
+ return shiftedVfArray;
90674
+ }
90675
+ // Generate a range of v's in 1/2% increments
90598
90676
  function shiftRange() {
90599
- const range = [];
90600
- const lower = 25 / 100;
90601
- const upper = 75 / 100;
90677
+ const range = [0.25, 0.75];
90678
+ const axisRange = [];
90602
90679
  const step = (1 / 100) / 2;
90603
- for (let v = lower; v <= upper + S.EPSILON; v += step) {
90604
- range.push(v);
90680
+ for (let v = range[0]; v <= range[1] + S.EPSILON; v += step) {
90681
+ axisRange.push(v);
90605
90682
  }
90606
- return range;
90683
+ return axisRange;
90607
90684
  }
90608
- // ESTIMATE BIAS ("FAIR")
90685
+ // ESTIMATE BIAS
90686
+ //
90687
+ // NOTE: By convention, '+' = R bias; '-' = D bias.
90688
+ //
90609
90689
  // ^S# - The # of Democratic seats closest to proportional @ statewide Vf
90610
90690
  // The "expected number of seats" from http://bit.ly/2Fcuf4q
90611
- function bestSeats(N, Vf) {
90691
+ function propSeats(N, Vf) {
90612
90692
  return Math.round((N * Vf) - S.EPSILON);
90613
90693
  }
90614
- exports.bestSeats = bestSeats;
90694
+ exports.propSeats = propSeats;
90615
90695
  // ^S% - The corresponding Democratic seat share
90616
- function bestSeatShare(bestS, N) {
90696
+ function propSeatShare(bestS, N) {
90617
90697
  return U.trim(bestS / N);
90618
90698
  }
90619
- exports.bestSeatShare = bestSeatShare;
90620
- // S# - The estimated # of Democratic seats @ statewide Vf, using seat probabilities
90621
- function estprobableSeats(VfArray, range) {
90699
+ exports.propSeatShare = propSeatShare;
90700
+ // S# - The estimated # of Democratic seats, using seat probabilities
90701
+ function estSeats(VfArray, range) {
90622
90702
  // Python: sum([est_seat_probability(vpi) for vpi in vpi_by_district])
90623
90703
  return U.trim(U.sumArray(VfArray.map(v => estSeatProbability(v, range))));
90624
90704
  }
90625
- exports.estprobableSeats = estprobableSeats;
90626
- // S% - The estimated Democratic seat share fraction @ statewide Vf
90627
- function estprobableSeatShare(probableS, N) {
90628
- return U.trim(probableS / N);
90705
+ exports.estSeats = estSeats;
90706
+ // S% - The estimated Democratic seat share fraction
90707
+ function estSeatShare(estS, N) {
90708
+ return U.trim(estS / N);
90629
90709
  }
90630
- exports.estprobableSeatShare = estprobableSeatShare;
90710
+ exports.estSeatShare = estSeatShare;
90631
90711
  // F# - The estimated number of Democratic seats using first past the post
90632
90712
  function estFPTPSeats(VfArray) {
90633
90713
  // Python: sum([1.0 for vpi in vpi_by_district if (vpi > 0.5)])
@@ -90641,34 +90721,33 @@ function estFPTPSeats(VfArray) {
90641
90721
  }));
90642
90722
  }
90643
90723
  exports.estFPTPSeats = estFPTPSeats;
90644
- // B% - Tthe bias calculated as S% ^S%
90645
- function estbias(estSeatShare, bestSeatShare) {
90646
- return U.trim(Math.abs(estSeatShare - bestSeatShare));
90724
+ // B% - The bias calculated as ^S% S%
90725
+ function estBias(estSf, propSf) {
90726
+ return U.trim(propSf - estSf);
90647
90727
  }
90648
- exports.estbias = estbias;
90728
+ exports.estBias = estBias;
90649
90729
  // UE# - The estimated # of unearned seats
90650
90730
  // UE_# from http://bit.ly/2Fcuf4q
90651
- function estunearnedSeats(best, probable) {
90652
- // NOTE - + values = unearned R seats; – values = unearned D seats
90653
- return U.trim(best - probable);
90731
+ function estUnearnedSeats(proportional, probable) {
90732
+ return U.trim(proportional - probable);
90654
90733
  }
90655
- exports.estunearnedSeats = estunearnedSeats;
90734
+ exports.estUnearnedSeats = estUnearnedSeats;
90656
90735
  // ESTIMATE RESPONSIVENESS ("COMPETITIVE")
90657
90736
  // R# - Estimate responsiveness at the statewide vote share
90658
90737
  function estResponsiveness(Vf, inferredSVpoints) {
90659
- // Python:
90660
- // V1, S1 = lower_bracket(sv_curve_pts, statewide_vote_share, VOTE_SHARE)
90661
- // V2, S2 = upper_bracket(sv_curve_pts, statewide_vote_share, VOTE_SHARE)
90662
- // R = ((S2 - S1) / total_seats) / (V2 - V1)
90663
- // Note - Seat values are already fractions [0.0–1.0] here.
90664
- const lowerPt = findLowerBracket(Vf, inferredSVpoints);
90665
- const upperPt = findUpperBracket(Vf, inferredSVpoints);
90666
- const R = ((upperPt.s - lowerPt.s) / (upperPt.v - lowerPt.v));
90667
- return U.trim(R);
90738
+ let R = undefined;
90739
+ // NOTE - Seat values are already fractions [0.0–1.0] here.
90740
+ const lowerPt = findBracketingLowerVf(Vf, inferredSVpoints);
90741
+ const upperPt = findBracketingUpperVf(Vf, inferredSVpoints);
90742
+ if (!(U.areRoughlyEqual((upperPt.v - lowerPt.v), 0, S.EPSILON))) {
90743
+ R = ((upperPt.s - lowerPt.s) / (upperPt.v - lowerPt.v));
90744
+ R = U.trim(R);
90745
+ }
90746
+ return R;
90668
90747
  }
90669
90748
  exports.estResponsiveness = estResponsiveness;
90670
90749
  // Find the S(V) point that brackets a Vf value on the lower end
90671
- function findLowerBracket(Vf, inferredSVpoints) {
90750
+ function findBracketingLowerVf(Vf, inferredSVpoints) {
90672
90751
  let lowerPt = inferredSVpoints[0];
90673
90752
  let smallerPoints = [];
90674
90753
  for (let pt of inferredSVpoints) {
@@ -90684,7 +90763,7 @@ function findLowerBracket(Vf, inferredSVpoints) {
90684
90763
  return lowerPt;
90685
90764
  }
90686
90765
  // Find the S(V) point that brackets a Vf value on the upper end
90687
- function findUpperBracket(Vf, inferredSVpoints) {
90766
+ function findBracketingUpperVf(Vf, inferredSVpoints) {
90688
90767
  let upperPt = inferredSVpoints[-1];
90689
90768
  for (let pt of inferredSVpoints) {
90690
90769
  if (pt.v >= Vf) {
@@ -90695,6 +90774,35 @@ function findUpperBracket(Vf, inferredSVpoints) {
90695
90774
  }
90696
90775
  return upperPt;
90697
90776
  }
90777
+ // The corresponding functions via the Sf y-axis (vs. Vf x-axis)
90778
+ // Find the S(V) point that brackets a Sf value on the lower end
90779
+ function findBracketingLowerSf(Sf, inferredSVpoints) {
90780
+ let lowerPt = inferredSVpoints[0];
90781
+ let smallerPoints = [];
90782
+ for (let pt of inferredSVpoints) {
90783
+ if (pt.s <= Sf) {
90784
+ smallerPoints.push(pt);
90785
+ }
90786
+ else {
90787
+ break;
90788
+ }
90789
+ }
90790
+ // The last smaller point
90791
+ lowerPt = smallerPoints.slice(-1)[0];
90792
+ return lowerPt;
90793
+ }
90794
+ // Find the S(V) point that brackets a Sf value on the upper end
90795
+ function findBracketingUpperSf(Sf, inferredSVpoints) {
90796
+ let upperPt = inferredSVpoints[-1];
90797
+ for (let pt of inferredSVpoints) {
90798
+ if (pt.s >= Sf) {
90799
+ // The first bigger point
90800
+ upperPt = { v: pt.v, s: pt.s };
90801
+ break;
90802
+ }
90803
+ }
90804
+ return upperPt;
90805
+ }
90698
90806
  // rD - Estimate the number of responsive districts, given a set of Vf's
90699
90807
  function estResponsiveDistricts(VfArray) {
90700
90808
  // Python: sum([est_district_responsiveness(vpi) for vpi in vpi_by_district])
@@ -90764,7 +90872,7 @@ function estMarginalCompetitiveShare(Md, Mrange) {
90764
90872
  }
90765
90873
  exports.estMarginalCompetitiveShare = estMarginalCompetitiveShare;
90766
90874
  function findMarginalDistricts(Vf, VfArray, N) {
90767
- const bestS = bestSeats(N, Vf);
90875
+ const bestS = propSeats(N, Vf);
90768
90876
  const fptpS = estFPTPSeats(VfArray);
90769
90877
  // Find the marginal districts IDs (indexed 1–N)
90770
90878
  let minId;
@@ -90785,6 +90893,328 @@ function findMarginalDistricts(Vf, VfArray, N) {
90785
90893
  return [minId, maxId];
90786
90894
  }
90787
90895
  exports.findMarginalDistricts = findMarginalDistricts;
90896
+ // ADVANCED/ALTERNATE METRICS FOR REFERENCE
90897
+ function calcTurnoutBias(statewideVf, VfArray) {
90898
+ const districtAvg = U.avgArray(VfArray);
90899
+ const turnoutBias = statewideVf - districtAvg;
90900
+ return U.trim(turnoutBias);
90901
+ }
90902
+ exports.calcTurnoutBias = calcTurnoutBias;
90903
+ // PARTISAN BIAS - I'm using John Nagle's simple seat bias below, which is what
90904
+ // PlanScore is doing:
90905
+ //
90906
+ // "Partisan bias is the difference between each party’s seat share and 50 %
90907
+ // in a hypothetical, perfectly tied election.For example, if a party would
90908
+ // win 55 % of a plan’s districts if it received 50 % of the statewide vote,
90909
+ // then the plan would have a bias of 5 % in this party’s favor.To calculate
90910
+ // partisan bias, the observed vote share in each district is shifted by the
90911
+ // amount necessary to simulate a tied statewide election.Each party’s seat
90912
+ // share in this hypothetical election is then determined. The difference
90913
+ // between each party’s seat share and 50 % is partisan bias."
90914
+ //
90915
+ // This is *not* King's & others' geometric partisan bias metric per se.
90916
+ // That is below.
90917
+ function estPartisanBias(inferredSVpoints, nDistricts) {
90918
+ return estSeatBias(inferredSVpoints, nDistricts);
90919
+ }
90920
+ exports.estPartisanBias = estPartisanBias;
90921
+ // SEATS BIAS -- John Nagle's simple seat bias @ 50% (alpha), a fractional # of seats.
90922
+ function estSeatBias(inferredSVpoints, nDistricts) {
90923
+ const half = 0.5;
90924
+ const tolerance = 0.001;
90925
+ let dSeats = 0;
90926
+ for (let pt of inferredSVpoints) {
90927
+ if (U.areRoughlyEqual(pt.v, half, tolerance)) {
90928
+ dSeats = pt.s * nDistricts;
90929
+ break;
90930
+ }
90931
+ }
90932
+ const rSeats = nDistricts - dSeats;
90933
+ const Bs = (rSeats - dSeats) / 2.0;
90934
+ // NOTE - That is the same as (N/2) - S(0.5).
90935
+ // const BsAlt = (nDistricts / 2.0) - dSeats;
90936
+ return U.trim(Bs);
90937
+ }
90938
+ exports.estSeatBias = estSeatBias;
90939
+ // VOTES BIAS -- John Nagle's simple vote bias @ 50% (alpha2), a percentage.
90940
+ function estVotesBias(inferredSVpoints, nDistricts) {
90941
+ let extraVf = 0.0;
90942
+ // Interpolate the extra Vf required @ Sf = 0.5
90943
+ const lowerPt = findBracketingLowerSf(0.5, inferredSVpoints);
90944
+ const upperPt = findBracketingUpperSf(0.5, inferredSVpoints);
90945
+ if ((upperPt.s - lowerPt.s) != 0) {
90946
+ const ratio = (upperPt.v - lowerPt.v) / (upperPt.s - lowerPt.s);
90947
+ const deltaS = 0.5 - lowerPt.s;
90948
+ extraVf = lowerPt.v + (ratio * deltaS) - 0.5;
90949
+ }
90950
+ extraVf = U.trim(extraVf);
90951
+ return extraVf;
90952
+ }
90953
+ exports.estVotesBias = estVotesBias;
90954
+ // GEOMETRIC SEATS BIAS (@ V = statewide vote share)
90955
+ function estGeometricSeatsBias(Vf, inferredSVpoints) {
90956
+ const bgsSVpoints = inferGeometricSeatsBiasPoints(inferredSVpoints);
90957
+ // Interpolate the seat fraction @ Vf
90958
+ const lowerPt = findBracketingLowerVf(Vf, bgsSVpoints);
90959
+ const upperPt = findBracketingUpperVf(Vf, bgsSVpoints);
90960
+ const ratio = (upperPt.s - lowerPt.s) / (upperPt.v - lowerPt.v);
90961
+ const deltaV = Vf - lowerPt.v;
90962
+ const deltaS = ratio * deltaV;
90963
+ const BsGf = lowerPt.s + deltaS;
90964
+ return U.trim(BsGf);
90965
+ }
90966
+ exports.estGeometricSeatsBias = estGeometricSeatsBias;
90967
+ function inferGeometricSeatsBiasPoints(inferredSVpoints) {
90968
+ const nPoints = inferredSVpoints.length;
90969
+ const inverseSVpoints = invertSVPoints(inferredSVpoints);
90970
+ let bgsSVpoints = [];
90971
+ for (let i = 0; i < nPoints; i++) {
90972
+ const Vf = inferredSVpoints[i].v;
90973
+ const sD = inferredSVpoints[i].s;
90974
+ const sR = inverseSVpoints[i].s;
90975
+ const BsGf = 0.5 * (sR - sD);
90976
+ bgsSVpoints.push({ v: Vf, s: BsGf });
90977
+ }
90978
+ return bgsSVpoints;
90979
+ }
90980
+ exports.inferGeometricSeatsBiasPoints = inferGeometricSeatsBiasPoints;
90981
+ function invertSVPoints(inferredSVpoints) {
90982
+ let invertedSVpoints = [];
90983
+ for (let pt of inferredSVpoints) {
90984
+ const Vd = pt.v;
90985
+ const Sd = pt.s;
90986
+ const Vr = U.trim(1.0 - Vd);
90987
+ const Sr = 1.0 - Sd;
90988
+ invertedSVpoints.push({ v: Vr, s: Sr });
90989
+ }
90990
+ invertedSVpoints.sort(function (a, b) {
90991
+ return a.v - b.v;
90992
+ });
90993
+ return invertedSVpoints;
90994
+ }
90995
+ exports.invertSVPoints = invertSVPoints;
90996
+ // EFFICIENCY GAP -- note the formulation used. Also, to accommodate turnout bias,
90997
+ // we would need to have D & R votes, not just shares.
90998
+ function calcEfficiencyGap(Vf, Sf, shareType = 0 /* Democratic */) {
90999
+ let efficiencyGap;
91000
+ if (shareType == 1 /* Republican */) {
91001
+ // NOTE - This is the common formulation:
91002
+ //
91003
+ // EG = (Sf – 0.5) – (2 × (Vf – 0.5))
91004
+ //
91005
+ // in which it is implied that '-' = R bias; '+' = D bias.
91006
+ efficiencyGap = U.trim((Sf - 0.5) - (2.0 * (Vf - 0.5)));
91007
+ }
91008
+ else {
91009
+ // NOTE - This is the alternate formulation in which '+' = R bias; '-' = D bias,
91010
+ // which is consistent with all our other metrics.
91011
+ efficiencyGap = U.trim((2.0 * (Vf - 0.5)) - (Sf - 0.5));
91012
+ }
91013
+ return U.trim(efficiencyGap);
91014
+ }
91015
+ exports.calcEfficiencyGap = calcEfficiencyGap;
91016
+ // MEAN–MEDIAN DIFFERENCE
91017
+ //
91018
+ // From PlanScore.org: "The mean-median difference is a party’s median vote share
91019
+ // minus its mean vote share, across all of a plan’s districts. For example, if
91020
+ // a party has a median vote share of 45 % and a mean vote share of 50 %, then
91021
+ // the plan has a mean - median difference of 5 % against this party. When the
91022
+ // mean and the median diverge significantly, the district distribution is skewed
91023
+ // in favor of one party and against its opponent. Conversely, when the mean and
91024
+ // the median are close, the district distribution is more symmetric."
91025
+ //
91026
+ // From Princeton Gerrymandering Project: "The mean-median difference is calculated
91027
+ // by subtracting the average vote share of either party across all districts from
91028
+ // the median vote share of the same party across all districts. A negative mean -
91029
+ // median difference indicates that the examined party has an advantage; a positive
91030
+ // difference indicates that the examined party is disadvantaged."
91031
+ //
91032
+ // So:
91033
+ // * With D VPI, '+' = R bias; '-' = D bias <<< We're using this convention.
91034
+ // * With R VPI, '-' = R bias; '+' = D bias.
91035
+ function estMeanMedianDifference(VfArray, Vf) {
91036
+ const meanVf = Vf ? Vf : U.avgArray(VfArray);
91037
+ const medianVf = U.medianArray(VfArray);
91038
+ // NOTE - Switched order to get the signs correct
91039
+ const difference = meanVf - medianVf;
91040
+ // const difference: number = medianVf - meanVf;
91041
+ return U.trim(difference);
91042
+ }
91043
+ exports.estMeanMedianDifference = estMeanMedianDifference;
91044
+ // HELPERS FOR DECLINATION & LOPSIDED OUTCOMES
91045
+ // Key r(v) points, defined in Fig. 19:
91046
+ // * VfArray are Democratic seat shares (by convention).
91047
+ // * But the x-axis of r(v) graphs us Republican seat share.
91048
+ // * So, you have to invert the D/R axis for Vb; and
91049
+ // * Invert the D/R probabilities for Va.
91050
+ function keyRVpoints(VfArray) {
91051
+ const nDistricts = VfArray.length;
91052
+ const estS = estSeats(VfArray);
91053
+ const Sb = estSeatShare(estS, nDistricts);
91054
+ // TODO - Understand why the corresponding V to Sb is always @ 0.5.
91055
+ // John Nagle: "This is the dividing vote for party A vs party B wins defined
91056
+ // by Warrington.My modification just puts fractions of districts to the each side."
91057
+ const Rb = Sb / 2;
91058
+ const Ra = (1 + Sb) / 2;
91059
+ const Vb = 1.0 - (U.sumArray(VfArray.map(v => estSeatProbability(v) * v))) / estS;
91060
+ const Va = (U.sumArray(VfArray.map(v => estSeatProbability(1 - v) * (1 - v)))) / (nDistricts - estS);
91061
+ const keyPoints = {
91062
+ Sb: Sb,
91063
+ Ra: Ra,
91064
+ Rb: Rb,
91065
+ Va: Va,
91066
+ Vb: Vb
91067
+ };
91068
+ return keyPoints;
91069
+ }
91070
+ exports.keyRVpoints = keyRVpoints;
91071
+ function isASweep(Sf, nDistricts) {
91072
+ const oneDistrict = 1 / nDistricts;
91073
+ const bSweep = ((Sf > (1 - oneDistrict)) || (Sf < oneDistrict)) ? true : false;
91074
+ return bSweep;
91075
+ }
91076
+ exports.isASweep = isASweep;
91077
+ function radiansToDegrees(radians) {
91078
+ const degrees = radians * (180 / Math.PI);
91079
+ return degrees;
91080
+ }
91081
+ exports.radiansToDegrees = radiansToDegrees;
91082
+ // DECLINATION
91083
+ //
91084
+ // Declination is calculated using the key r(v) points, defined in Fig. 19.
91085
+ // Note that district vote shares are D shares, so party A = Rep & B = Dem.
91086
+ function calcDeclination(VfArray) {
91087
+ const { Sb, Ra, Rb, Va, Vb } = keyRVpoints(VfArray);
91088
+ const bSweep = isASweep(Sb, VfArray.length);
91089
+ const bTooFewDistricts = (VfArray.length < 5) ? true : false;
91090
+ const bVaAt50 = (U.areRoughlyEqual((Va - 0.5), 0.0, S.EPSILON)) ? true : false;
91091
+ const bVbAt50 = (U.areRoughlyEqual((0.5 - Vb), 0.0, S.EPSILON)) ? true : false;
91092
+ let decl;
91093
+ if (bSweep || bTooFewDistricts || bVaAt50 || bVbAt50) {
91094
+ decl = undefined;
91095
+ }
91096
+ else {
91097
+ const lTan = (Sb - Rb) / (0.5 - Vb);
91098
+ const rTan = (Ra - Sb) / (Va - 0.5);
91099
+ const lAngle = radiansToDegrees(Math.atan(lTan));
91100
+ const rAngle = radiansToDegrees(Math.atan(rTan));
91101
+ decl = rAngle - lAngle;
91102
+ decl = U.trim(decl);
91103
+ }
91104
+ return decl;
91105
+ }
91106
+ exports.calcDeclination = calcDeclination;
91107
+ // LOPSIDED OUTCOMES
91108
+ //
91109
+ // This is a measure of packing bias is:
91110
+ //
91111
+ // LO = (1⁄2 - vB) - (vA – 1⁄2) Eq. 5.4.1 on P. 26
91112
+ //
91113
+ // "The ideal for this measure is that the excess vote share for districts
91114
+ // won by party A averaged over those districts equals the excess vote share
91115
+ // for districts won by party B averaged over those districts.
91116
+ // A positive value of LO indicates greater packing of party B voters and,
91117
+ // therefore, indicates a bias in favor of party A."
91118
+ function calcLopsidedOutcomes(VfArray) {
91119
+ const { Sb, Ra, Rb, Va, Vb } = keyRVpoints(VfArray);
91120
+ const bSweep = isASweep(Sb, VfArray.length);
91121
+ let LO;
91122
+ if (bSweep) {
91123
+ LO = undefined;
91124
+ }
91125
+ else {
91126
+ LO = (0.5 - Vb) - (Va - 0.5);
91127
+ LO = U.trim(LO);
91128
+ }
91129
+ return LO;
91130
+ }
91131
+ exports.calcLopsidedOutcomes = calcLopsidedOutcomes;
91132
+ // TODO - Add unit tests <<< Depends on S(V)
91133
+ // GLOBAL SYMMETRY - Fig. 17 in Section 5.1
91134
+ function calcGlobalSymmetry(inferredSVpoints, S50V) {
91135
+ const invertedSVpoints = invertSVPoints(inferredSVpoints);
91136
+ let gSym = 0.0;
91137
+ for (let i in inferredSVpoints) {
91138
+ gSym += Math.abs(inferredSVpoints[i].s - invertedSVpoints[i].s) / 2;
91139
+ }
91140
+ const sign = (S50V < 0) ? -1 : 1;
91141
+ gSym *= sign;
91142
+ return U.trim(gSym);
91143
+ }
91144
+ exports.calcGlobalSymmetry = calcGlobalSymmetry;
91145
+ // RAW DISPROPORTIONALITY
91146
+ //
91147
+ // gamma = Sf – Vf : Eq.C.1.1 on P. 42
91148
+ function calcDisproportionality(Vf, Sf) {
91149
+ const gamma = Vf - Sf;
91150
+ // const gamma = Sf - Vf;
91151
+ return U.trim(gamma);
91152
+ }
91153
+ exports.calcDisproportionality = calcDisproportionality;
91154
+ // TODO - Add unit tests <<< Depends on S(V)
91155
+ // BIG 'R': Defined in Footnote 22 on P. 10
91156
+ function calcBigR(Vf, Sf) {
91157
+ let bigR = undefined;
91158
+ if (!(U.areRoughlyEqual(Vf, 0.5, S.EPSILON))) {
91159
+ bigR = (Sf - 0.5) / (Vf - 0.5);
91160
+ bigR = U.trim(bigR);
91161
+ }
91162
+ return bigR;
91163
+ }
91164
+ exports.calcBigR = calcBigR;
91165
+ // TODO - Add unit tests <<< Depends on S(V)
91166
+ // MINIMAL INVERSE RESPONSIVENESS
91167
+ //
91168
+ // zeta = (1 / r) - (1 / r_sub_max) : Eq. 5.2.1
91169
+ //
91170
+ // where r_sub_max = 10 or 20 for balanced and unbalanced states, respectively.
91171
+ function calcMinimalInverseResponsiveness(Vf, r) {
91172
+ let MIR = undefined;
91173
+ if (!(U.areRoughlyEqual(r, 0, S.EPSILON))) {
91174
+ const bBalanced = isBalanced(Vf);
91175
+ const ideal = bBalanced ? 0.1 : 0.2;
91176
+ MIR = (1 / r) - ideal;
91177
+ MIR = U.trim(MIR);
91178
+ }
91179
+ return MIR;
91180
+ }
91181
+ exports.calcMinimalInverseResponsiveness = calcMinimalInverseResponsiveness;
91182
+ function isBalanced(Vf) {
91183
+ const [lower, upper] = C.competitiveRange();
91184
+ const bBalanced = ((Vf > upper) || (Vf < lower)) ? false : true;
91185
+ return bBalanced;
91186
+ }
91187
+ // HELPERS
91188
+ function printPartisanScorecardHeader() {
91189
+ console.log('XX, Name, N, V%, ^S#, S#, B%, B$, UE#, I$, C#, Cd, Md, C$, <P$');
91190
+ }
91191
+ exports.printPartisanScorecardHeader = printPartisanScorecardHeader;
91192
+ function printPartisanScorecardRow(xx, name, N, Vf, s) {
91193
+ console.log('%s, %s, %i, %f, %i, %f, %f, %i, %f, %i, %i, %f, %f, %i, %i', xx, // 1
91194
+ name, // 2
91195
+ N, // 3
91196
+ Vf, // 4
91197
+ s.bias.propS, // 5
91198
+ s.bias.estS, // 6
91199
+ s.bias.bias, // 7
91200
+ s.bias.score, s.impact.unearnedS, // 9
91201
+ s.impact.score, s.competitiveness.c, // 11
91202
+ s.competitiveness.cD, s.competitiveness.mD, s.competitiveness.score, s.score // 15
91203
+ );
91204
+ }
91205
+ exports.printPartisanScorecardRow = printPartisanScorecardRow;
91206
+ // Generate partisan details (Table 1)
91207
+ function printPartisanDetailsHeader() {
91208
+ console.log('XX, <V>, S(<V>), S50V, V50S, Decl, B_G, EG, Beta, l-gamma, mM, TO, mM\', LO, R, r, Zeta');
91209
+ }
91210
+ exports.printPartisanDetailsHeader = printPartisanDetailsHeader;
91211
+ function printPartisanDetailsRow(xx, name, N, Vf, s) {
91212
+ console.log('%s, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f, %f', xx, Vf, s.bias.estSf, s.bias.s50V, s.bias.v50S, s.bias.decl, s.bias.gSym, s.bias.eG, s.bias.sVg, // Beta
91213
+ s.bias.prop, // Lower-gamma
91214
+ s.bias.mMs, s.bias.tOf, s.bias.mMd, s.bias.lO, s.competitiveness.bigR, s.competitiveness.littleR, s.competitiveness.mIR // Zeta
91215
+ );
91216
+ }
91217
+ exports.printPartisanDetailsRow = printPartisanDetailsRow;
90788
91218
 
90789
91219
 
90790
91220
  /***/ }),
@@ -90809,7 +91239,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
90809
91239
  return result;
90810
91240
  };
90811
91241
  Object.defineProperty(exports, "__esModule", { value: true });
90812
- const U = __importStar(__webpack_require__(/*! ./utils */ "./src/utils.ts"));
90813
91242
  const S = __importStar(__webpack_require__(/*! ./settings */ "./src/settings.ts"));
90814
91243
  const C = __importStar(__webpack_require__(/*! ./config */ "./src/config.ts"));
90815
91244
  const compact_1 = __webpack_require__(/*! ./compact */ "./src/compact.ts");
@@ -90852,7 +91281,12 @@ function scorePlan(p, overridesJSON) {
90852
91281
  populationDeviation: pdS
90853
91282
  };
90854
91283
  // PARTISAN ("fair") subcategories - bias, impact, & competitiveness (plus lots of supporting measures)
90855
- const pS = partisan_1.scorePartisan(p.partisanProfile.statewideVf, p.partisanProfile.vfArray);
91284
+ const options = {
91285
+ alternates: true,
91286
+ constrained: true,
91287
+ shift: 0 /* Proportional */
91288
+ };
91289
+ const pS = partisan_1.scorePartisan(p.partisanProfile.statewideVf, p.partisanProfile.vfArray, options);
90856
91290
  // Combine the partisan/partisan & traditional principles/best scores into an overall
90857
91291
  // score for comparing plans w/in a state
90858
91292
  let score = weightOverall(pS.score2, tpS.score, 1 /* WithinAState */);
@@ -90905,11 +91339,26 @@ function mixinMinorityBonus(score, minorityBonus) {
90905
91339
  }
90906
91340
  exports.mixinMinorityBonus = mixinMinorityBonus;
90907
91341
  function printScorecardHeader() {
90908
- console.log('XX, Name, N, V%, ^S#, S#, B%, B$, UE#, I$, C#, Cd, Md, C$, <P$, >P$, Rc, Rc$, Pc, Pc$, G$, Cs, Cs$, Ds, Ds$, S$, Eq, Eq$, T$, O1, O2, Od, Pd, M$, $$$');
91342
+ console.log('XX, Name, N, V%, ^S#, S#, B%, B$, UE#, I$, C#, Cd, Md, C$, >P$, Rc, Rc$, Pc, Pc$, G$, Cs, Cs$, Ds, Ds$, S$, Eq, Eq$, T$, Od, M$, $$$');
90909
91343
  }
90910
91344
  exports.printScorecardHeader = printScorecardHeader;
90911
91345
  function printScorecardRow(xx, name, N, Vf, s) {
90912
- console.log('%s, %s, %i, %f, %i, %f, %f, %i, %f, %i, %i, %f, %f, %i, %i, %i, %f, %i, %f, %i, %i, %f, %i, %f, %i, %i, %f, %i, %i, %i, %i, %f, %i, %i, %i', xx, name, N, U.trim(Vf), s.partisan.bias.bestS, s.partisan.bias.probableS, s.partisan.bias.bias, s.partisan.bias.score, s.partisan.impact.unearnedS, s.partisan.impact.score, s.partisan.competitiveness.c, s.partisan.competitiveness.cD, s.partisan.competitiveness.mD, s.partisan.competitiveness.score, s.partisan.score, s.partisan.score2, s.traditionalPrinciples.compactness.reock.raw, s.traditionalPrinciples.compactness.reock.normalized, s.traditionalPrinciples.compactness.polsby.raw, s.traditionalPrinciples.compactness.polsby.normalized, s.traditionalPrinciples.compactness.score, s.traditionalPrinciples.splitting.county.raw, s.traditionalPrinciples.splitting.county.normalized, s.traditionalPrinciples.splitting.district.raw, s.traditionalPrinciples.splitting.district.normalized, s.traditionalPrinciples.splitting.score, s.traditionalPrinciples.populationDeviation.raw, s.traditionalPrinciples.populationDeviation.normalized, s.traditionalPrinciples.score, s.minority.nOpportunity1, s.minority.nOpportunity2, s.minority.opportunityDistricts, s.minority.nProportional, s.minority.score, s.score);
91346
+ console.log('%s, %s, %i, %f, %i, %f, %f, %i, %f, %i, %i, %f, %f, %i, %i, %f, %i, %f, %i, %i, %f, %i, %f, %i, %i, %f, %i, %i, %f, %i, %i', xx, // 1
91347
+ name, // 2
91348
+ N, // 3
91349
+ Vf, // 4
91350
+ s.partisan.bias.propS, // 5
91351
+ s.partisan.bias.estS, // 6
91352
+ s.partisan.bias.bias, // 7
91353
+ s.partisan.bias.score, s.partisan.impact.unearnedS, // 9
91354
+ s.partisan.impact.score, s.partisan.competitiveness.c, // 11
91355
+ s.partisan.competitiveness.cD, s.partisan.competitiveness.mD, s.partisan.competitiveness.score, s.partisan.score2, // 15
91356
+ s.traditionalPrinciples.compactness.reock.raw, s.traditionalPrinciples.compactness.reock.normalized, s.traditionalPrinciples.compactness.polsby.raw, s.traditionalPrinciples.compactness.polsby.normalized, s.traditionalPrinciples.compactness.score, s.traditionalPrinciples.splitting.county.raw, s.traditionalPrinciples.splitting.county.normalized, s.traditionalPrinciples.splitting.district.raw, s.traditionalPrinciples.splitting.district.normalized, s.traditionalPrinciples.splitting.score, s.traditionalPrinciples.populationDeviation.raw, s.traditionalPrinciples.populationDeviation.normalized, s.traditionalPrinciples.score,
91357
+ // s.minority.nOpportunity1,
91358
+ // s.minority.nOpportunity2,
91359
+ s.minority.opportunityDistricts,
91360
+ // s.minority.nProportional,
91361
+ s.minority.score, s.score);
90913
91362
  }
90914
91363
  exports.printScorecardRow = printScorecardRow;
90915
91364
 
@@ -90966,6 +91415,66 @@ const sample_profile_json_1 = __importDefault(__webpack_require__(/*! ../testdat
90966
91415
  exports.sampleProfile = sample_profile_json_1.default;
90967
91416
  const sample_scorecard_json_1 = __importDefault(__webpack_require__(/*! ../testdata/samples/sample-scorecard.json */ "./testdata/samples/sample-scorecard.json"));
90968
91417
  exports.sampleScorecard = sample_scorecard_json_1.default;
91418
+ exports.Metrics = {
91419
+ // PARTISAN
91420
+ // Bias metrics
91421
+ Vf: { label: "Statewide D vote share", abbr: "V" },
91422
+ estS: { label: "Probable D seats", abbr: "S_V" },
91423
+ estSf: { label: "Probable D seat share", units: 1 /* Percentage */, abbr: "S%" },
91424
+ propS: { label: "D seats closest to proportional", abbr: "^S" },
91425
+ propSf: { label: "D seat share closest to proportional", units: 1 /* Percentage */, abbr: "^S%" },
91426
+ fptpS: { label: "Probable FPTP D seats", abbr: "S!" },
91427
+ bias: { label: "Effective bias", units: 1 /* Percentage */, abbr: "B%", description: "Calculated as S% – ^S%" },
91428
+ biasScore: { label: "Bias score", abbr: "B$", description: "Bias score normalized [0–100]" },
91429
+ // Additional bias metrics
91430
+ s50v: { label: "Seats bias", units: 1 /* Percentage */, abbr: "BS_50", symbol: "\u{03B1}" },
91431
+ v50s: { label: "Votes bias", units: 1 /* Percentage */, abbr: "BV_50", symbol: "\u{03B1}2" },
91432
+ decl: { label: "Declination", units: 2 /* Degrees */, abbr: "decl", symbol: "\u{03B4}" },
91433
+ gSym: { label: "Global symmetry", abbr: "GS", symbol: "\u{0393}" },
91434
+ EG: { label: "Efficiency gap", units: 1 /* Percentage */, abbr: "EG", symbol: "\u{03B3}2" },
91435
+ BsGf: { label: "Partisan bias", units: 1 /* Percentage */, abbr: "BS_V", symbol: "\u{03D0}" },
91436
+ prop: { label: "Disproprtionality", units: 1 /* Percentage */, abbr: "prop", symbol: "\u{03B3}" },
91437
+ MM: { label: "Mean–median", abbr: "MM" },
91438
+ LO: { label: "Lopsided outcomes", abbr: "LO" },
91439
+ // Impact metrics
91440
+ unearnedS: { label: "Unearned seats", abbr: "UE" },
91441
+ impactScore: { label: "Impact score", abbr: "I$", description: "Impact score normalized to [0–100]" },
91442
+ // Competitiveness/responsiveness metrics
91443
+ c: { label: "Simple competitive districts", abbr: "C", description: "Count of districts in the range [45–55%]" },
91444
+ cD: { label: "Probable competitive districts", abbr: "Cd", description: "Probable competitive districts, using probabilities" },
91445
+ cDf: { label: "Probable competitive districts share", units: 1 /* Percentage */, abbr: "Cdf" },
91446
+ mD: { label: "Competitive marginal districts", abbr: "Md", description: "Probable competitive marginal districts, using probabilities" },
91447
+ mDf: { label: "Competitive marginal districts share", units: 1 /* Percentage */, abbr: "Mdf" },
91448
+ competitivenessScore: { label: "Competitiveness score", abbr: "C$", description: "Competitiveness score normalized to [0–100]" },
91449
+ // Additional competitiveness/responsiveness metrics
91450
+ rD: { label: "Responsive districts", abbr: "Rd" },
91451
+ rDf: { label: "Responsive districts %", abbr: "Rd%" },
91452
+ bigR: { label: "Overall responsiveness", abbr: "R" /*, symbol: "\u{}" */ },
91453
+ littleR: { label: "Point responsiveness", abbr: "r", symbol: "\u{03C1}" },
91454
+ MIR: { label: "Minimal inverse responsiveness", abbr: "MIR", symbol: "\u{03B6}" },
91455
+ partisanScore: { label: "Partisan score", abbr: "P$" },
91456
+ // * <P$ [score] = the combined partisan score used to compare plans *across* states
91457
+ // * >P$ [score2] = the combined partisan score used to compare plans *within* a state, along with traditional districting principles
91458
+ // MINORITY
91459
+ oD: { label: "Opportunity districts", abbr: "Od" },
91460
+ minorityBonus: { label: "Minority bonus", abbr: "M$" },
91461
+ // TRADITIONAL DISTRICTING PRINCIPLES
91462
+ reockRaw: { label: "Reock", abbr: "Rc" },
91463
+ reockNormalized: { label: "Reock", abbr: "Rc$", description: "Reock normalized to [0–100]" },
91464
+ polsbyRaw: { label: "Polsby-Popper", abbr: "Pc" },
91465
+ polsbyNormalized: { label: "Polsby-Popper", abbr: "Pc$", description: "Polsby-Popper normalized to [0–100]" },
91466
+ compactnessScore: { label: "Compactness score", abbr: "G$" },
91467
+ countySplittingRaw: { label: "County splitting", abbr: "Cs" },
91468
+ countySplittingNormalized: { label: "County splitting", abbr: "Cs$", description: "County splitting normalized to [0–100]" },
91469
+ districtSplittingRaw: { label: "District splitting", abbr: "Ds" },
91470
+ districtSplittingNormalized: { label: "District splitting", abbr: "Ds$", description: "District splitting normalized to [0–100]" },
91471
+ splittingScore: { label: "Splitting score", abbr: "S$" },
91472
+ populationDeviationRaw: { label: "Population deviation", abbr: "Eq" },
91473
+ populationDeviationNormalized: { label: "County splitting", abbr: "Eq$", description: "Population deviation normalized to [0–100]" },
91474
+ traditionalPrinciplesScore: { label: "Traditional districting principles score", abbr: "T$", description: "Traditional principles score normalized to [0–100]" },
91475
+ // OVERALL SCORE
91476
+ score: { label: "Overall score", abbr: "$$$" }
91477
+ };
90969
91478
 
90970
91479
 
90971
91480
  /***/ }),
@@ -91008,11 +91517,31 @@ function maxArray(arr) {
91008
91517
  return Math.max(...arr);
91009
91518
  }
91010
91519
  exports.maxArray = maxArray;
91520
+ // Modified from https://jsfiddle.net/Lucky500/3sy5au0c/
91521
+ function medianArray(arr) {
91522
+ if (arr.length === 0)
91523
+ return 0;
91524
+ arr.sort(function (a, b) {
91525
+ return a - b;
91526
+ });
91527
+ var half = Math.floor(arr.length / 2);
91528
+ if (arr.length % 2)
91529
+ return arr[half];
91530
+ return (arr[half - 1] + arr[half]) / 2.0;
91531
+ }
91532
+ exports.medianArray = medianArray;
91011
91533
  function initArray(n, value) {
91012
91534
  return Array.from(Array(n), () => value);
91013
91535
  }
91014
91536
  exports.initArray = initArray;
91015
91537
  // MISCELLANEOUS
91538
+ // Deal with decimal census "counts" due to disagg/re-agg
91539
+ function areRoughlyEqual(x, y, tolerance) {
91540
+ let delta = Math.abs(x - y);
91541
+ let result = (delta < tolerance) ? true : false;
91542
+ return result;
91543
+ }
91544
+ exports.areRoughlyEqual = areRoughlyEqual;
91016
91545
  // Round a fractional number [0-1] to the desired level of PRECISION.
91017
91546
  function trim(fullFraction, digits = undefined) {
91018
91547
  if (digits == 0) {
@@ -91064,7 +91593,7 @@ module.exports = JSON.parse("{\"state\":\"NC\",\"planName\":\"NC 116th Congressi
91064
91593
  /*! exports provided: score, partisan, minority, traditionalPrinciples, default */
91065
91594
  /***/ (function(module) {
91066
91595
 
91067
- module.exports = JSON.parse("{\"score\":5,\"partisan\":{\"bias\":{\"bestS\":7,\"bestSf\":0.5385,\"probableS\":4.1925,\"probableSf\":0.3225,\"bias\":0.216,\"score\":0},\"impact\":{\"unearnedS\":2.8075,\"score\":0},\"competitiveness\":{\"c\":6,\"cD\":0.7266,\"cDf\":0.0559,\"mRange\":[5,11],\"mD\":0.7134,\"mDf\":0.1019,\"score\":10},\"score\":3,\"score2\":2},\"minority\":{\"report\":{\"bucketsByDemographic\":[[10,14],[3,0],[1,9],[0,0],[0,0],[0,0]],\"averageDVf\":0.6737588682747838},\"nOpportunity1\":4,\"nOpportunity2\":9,\"nProportional\":18,\"opportunityDistricts\":12.9424,\"score\":14},\"traditionalPrinciples\":{\"score\":18,\"compactness\":{\"score\":35,\"reock\":{\"raw\":0.3373,\"normalized\":35,\"notes\":{}},\"polsby\":{\"raw\":0.2418,\"normalized\":35,\"notes\":{}}},\"splitting\":{\"score\":0,\"county\":{\"raw\":1.1474,\"normalized\":0,\"notes\":{}},\"district\":{\"raw\":1.4839,\"normalized\":3,\"notes\":{}}},\"populationDeviation\":{\"raw\":0.016,\"normalized\":0,\"notes\":{\"maxDeviation\":11693}}}}");
91596
+ module.exports = JSON.parse("{\"score\":5,\"partisan\":{\"bias\":{\"propS\":7,\"propSf\":0.5385,\"estS\":4.1925,\"estSf\":0.3225,\"bias\":0.216,\"tOf\":-0.0023,\"fptpS\":3,\"s50V\":0.2172,\"v50S\":0.045,\"decl\":36.5164,\"gSym\":6.6602,\"eG\":0.2846,\"sVg\":0.2098,\"prop\":0.2695,\"mMd\":0.0593,\"mMs\":0.057,\"lO\":0.1106,\"score\":0},\"impact\":{\"unearnedS\":2.8075,\"score\":0},\"competitiveness\":{\"bigR\":-16.926,\"littleR\":4.3523,\"mIR\":0.1298,\"rD\":4.0207,\"rDf\":0.3093,\"c\":6,\"cD\":0.7266,\"cDf\":0.0559,\"mRange\":[5,11],\"mD\":0.7134,\"mDf\":0.1019,\"score\":10},\"score\":3,\"score2\":2},\"minority\":{\"report\":{\"bucketsByDemographic\":[[10,14],[3,0],[1,9],[0,0],[0,0],[0,0]],\"averageDVf\":0.6737588682747838},\"nProportional\":18,\"opportunityDistricts\":12.9424,\"score\":14},\"traditionalPrinciples\":{\"score\":18,\"compactness\":{\"score\":35,\"reock\":{\"raw\":0.3373,\"normalized\":35,\"notes\":{}},\"polsby\":{\"raw\":0.2418,\"normalized\":35,\"notes\":{}}},\"splitting\":{\"score\":0,\"county\":{\"raw\":1.1474,\"normalized\":0,\"notes\":{}},\"district\":{\"raw\":1.4839,\"normalized\":3,\"notes\":{}}},\"populationDeviation\":{\"raw\":0.016,\"normalized\":0,\"notes\":{\"maxDeviation\":11693}}}}");
91068
91597
 
91069
91598
  /***/ })
91070
91599