Skip to content

Commit 3c33220

Browse files
author
Tom Pearson
authored
Add clamping (#21)
* add new test data * add clamping and tests to index-core * add clamping and tests to the utils * docs
1 parent 7bf0178 commit 3c33220

File tree

9 files changed

+64
-57
lines changed

9 files changed

+64
-57
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ There will often be ancillary imformation associated with each entity, things li
3939
---
4040
## API
4141

42-
### indexCore(_indicators:Array_, _entities:Array_, [_indexMax:Number=100_],[_allowOverwrite:Boolean=true_])
42+
### indexCore(_indicators:Array_, _entities:Array_, [_indexMax:Number=100_],[_allowOverwrite:Boolean=true_],[_clamp=false_])
4343

4444
indexCore constructs and calculates an index it returns an `index` object allowing access to results of the calculation for all the entities at a give indicator level, the structure of the index as a whole and methods for adjusting indicator values and weightings.
4545

@@ -72,6 +72,7 @@ __indexMax__, optional. is the maximum value for the index score (minimum is alw
7272

7373
__allowOverwrite__, optional. By default this is set to true ensuring that all calculated values _will_ be calculated by the module. If set to false, calculated values that are supplied in the `entities` sheet will take precedence. Most of the time you don't want to do this but it might be a handy escape hatch.
7474

75+
__clamp__, optional. By default this is set to false. Indicator values above the max or below the min values for that indicator will not be constrained therefor normalised values may lie outside the expected range. Set to true if you want you values constrained.
7576

7677
### indexCore.__adjustValue(_entityName:String_, _indicatorID:String_, _value:Number_)__
7778
A function that allows the user to adjust an entity's (_entityName_) indicator (_indicatorID_) score to a specified _value_. Calculated values for that entity are re-calculated using the new value. If the function is called without _value_ the indicator is reset to it's inital value. If the function is called without _indicatorId_ or _value_ all indicators on the entity are reset to their initial values. As a convenience the function returns an object with all the recalculated values.

data/simple-index-set/entities.csv

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Brass: Birmingham,,8,8,3,7,3,6,7,7,4,10,50,100
55
Treasure Island,,7,5,3,7,1.5,8,8,8,6,10,40,20
66
Tigris and Eurphrates,,8,5,4,6,1,7,3,6,1,5,35,100
77
The Crew,,8,4,7,6,.5,7,3,6,0,0,13,30
8-
Twilight Imperium,,6,7,4,4,7,8,8,6,1,-5,90,10
8+
Twilight Imperium,,6,7,4,4,7,8,8,6,1,-5,90,10
9+
Chinatown,,7,5,7,20,1,8,8,7,4,0,45,90

data/simple-index-set/indicators.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ id,indicatorName,min,max,invert,diverging,weighting,type,
55
1.1,thinkyness,0,10,,,1,,
66
1.2,rules complexity,0,10,,,1,,
77
1.3,mechanical familiarity,0,10,,,1,,
8-
1.4 ,rule book quality,0,10,,,1,,
8+
1.4,rule book quality,0,10,,,1,,
99
1.5,play length,0.5,8,,,1,,
1010
1.6,fun,0,10,,,1,,
1111
2.1,theme integration,0,10,,,1,,

index.js

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,17 @@
1-
import { clone } from './src/utils.js';
21
import {csvParse} from 'd3';
32
import fs from 'fs';
43
import indexCore from './src/index-core.js';
54

6-
const waterRootDir = 'data/wateroptimisation';
5+
const simpleRootDir = 'data/simple-index-set';
76

8-
const waterIndicators = csvParse(fs.readFileSync(`${waterRootDir}/indicators.csv`, 'utf-8'));
9-
const waterEntities = csvParse(fs.readFileSync(`${waterRootDir}/entities.csv`, 'utf-8'));
7+
const simpleIndicators = csvParse(fs.readFileSync(`${simpleRootDir}/indicators.csv`, 'utf-8'));
8+
const simpleEntities = csvParse(fs.readFileSync(`${simpleRootDir}/entities.csv`, 'utf-8'));
109

11-
// const waterOptimisationIndex = indexCore(waterIndicators, waterEntities);
10+
const simpleIndex = indexCore(simpleIndicators, simpleEntities, 100 ,true, true);
11+
const simpleIndexUnrestricted = indexCore(simpleIndicators, simpleEntities);
1212

13-
const inclusiveIternetRootDir = 'data/inclusiveinternet/2021';
13+
//console.log( simpleIndex.getEntity('Chinatown') )
14+
console.log( simpleIndex.getEntity('Chinatown').value, 'vs', simpleIndexUnrestricted.getEntity('Chinatown').value );
15+
console.log( simpleIndex.getEntity('Chinatown')['1'], 'vs', simpleIndexUnrestricted.getEntity('Chinatown')['1'] )
16+
console.log( simpleIndex.getEntity('Chinatown')['1.4'], 'vs', simpleIndexUnrestricted.getEntity('Chinatown')['1.4'] )
1417

15-
const inclusiveInternetIndicators = csvParse(fs.readFileSync(`${inclusiveIternetRootDir}/indicators.csv`, 'utf-8'));
16-
const inclusiveInternetEntities = csvParse(fs.readFileSync(`${inclusiveIternetRootDir}/entities.csv`, 'utf-8'));
17-
const inclusiveInternetIndex = indexCore(inclusiveInternetIndicators, inclusiveInternetEntities);
18-
19-
20-
// console.log('value', inclusiveInternetIndex.indexedData['Singapore']['value'])
21-
// console.log('1', inclusiveInternetIndex.indexedData['Singapore']['1'])
22-
// console.log('1.2', inclusiveInternetIndex.indexedData['Singapore']['1.2'])
23-
// console.log('1.2.1', inclusiveInternetIndex.getEntityIndicator('Singapore','1.2.1'))
24-
25-
console.log(inclusiveInternetIndex.adjustValue('Singapore','1.2.1',50));
26-
27-
// console.log('adjusted')
28-
// console.log('value', inclusiveInternetIndex.indexedData['Singapore']['value'])
29-
// console.log('1', inclusiveInternetIndex.indexedData['Singapore']['1'])
30-
// console.log('1.2', inclusiveInternetIndex.indexedData['Singapore']['1.2'])
31-
// console.log('1.2.1', inclusiveInternetIndex.getEntityIndicator('Singapore','1.2.1'))
32-
33-
// const simpleRootDir = 'data/simple-index-set';
34-
35-
// const simpleIndicators = csvParse(fs.readFileSync(`${simpleRootDir}/indicators.csv`, 'utf-8'));
36-
// const simpleEntities = csvParse(fs.readFileSync(`${simpleRootDir}/entities.csv`, 'utf-8'));
37-
38-
// const simpleIndex = indexCore(simpleIndicators, simpleEntities);
39-
// console.log(simpleIndex.indexedData['Monopoly'].value)
40-
// console.log(simpleIndex.indexedData['Monopoly']['1'])
41-
// console.log(simpleIndex.indexedData['Monopoly']['1.1'])
42-
// simpleIndex.adjustValue('Monopoly','1.1',10);
43-
// console.log('---');
44-
// console.log(simpleIndex.indexedData['Monopoly'].value)
45-
// console.log(simpleIndex.indexedData['Monopoly']['1'])
46-
// console.log(simpleIndex.indexedData['Monopoly']['1.1'])

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@economist/index-core",
3-
"version": "1.5.3",
3+
"version": "1.6.0",
44
"description": "",
55
"main": "src/index-core.js",
66
"type": "module",

src/index-core.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { calculateWeightedMean, clone, normalise } from './utils.js';
22

33
const indicatorIdTest = /^([\w]\.)*\w{1}$/;
44

5-
function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100, allowOverwrite = true) {
5+
// TODO: the last 3 args, (indexMax, allowOverwrite, clamp) should proabbly be an options object
6+
function indexCore(
7+
indicatorsData = [],
8+
entitiesData = [],
9+
indexMax = 100,
10+
allowOverwrite = true,
11+
clamp = false,
12+
) {
613
if (indicatorsData.length === 0 || entitiesData.length === 0) return {};
714
const indicatorLookup = Object.fromEntries(
815
indicatorsData
@@ -56,7 +63,7 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100, allow
5663
if (!normalised) {
5764
return acc + Number(v[indicatorID]);
5865
}
59-
return acc + normalise(Number(v[indicatorID]), indicatorRange, indexMax);
66+
return acc + normalise(Number(v[indicatorID]), indicatorRange, indexMax, clamp);
6067
}, 0);
6168
return sum / length;
6269
}
@@ -102,18 +109,18 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100, allow
102109
function indexEntity(entity, calculationList, overwrite = allowOverwrite) {
103110
const newEntity = clone(entity);
104111
calculationList.forEach((indicatorID) => {
105-
if ((newEntity[indicatorID] && overwrite === true)
106-
|| !newEntity[indicatorID]) {
112+
if ((newEntity[indicatorID] && overwrite === true) || !newEntity[indicatorID]) {
107113
// get the required component indicators to calculate the parent value
108114
// this is a bit brittle maybe?
115+
109116
const componentIndicators = indicatorsData
110117
.filter((indicator) => (indicator.id.indexOf(indicatorID) === 0
111118
&& indicator.id.length === indicatorID.length + 2))
112-
.filter((indicator) => !excludeIndicator(indicator))
119+
.filter((indicator) => excludeIndicator(indicator) === false)
113120
.map((indicator) => formatIndicator(indicator, newEntity, indexMax));
114121
// calculate the weighted mean of the component indicators on the newEntity
115122
// assign that value to the newEntity
116-
newEntity[indicatorID] = calculateWeightedMean(componentIndicators, indexMax);
123+
newEntity[indicatorID] = calculateWeightedMean(componentIndicators, indexMax, clamp);
117124
} else {
118125
console.warn(`retaining existing value for ${newEntity.name} - ${indicatorID} : ${Number(entity[indicatorID])}`);
119126
newEntity[indicatorID] = Number(entity[indicatorID]);
@@ -124,7 +131,7 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100, allow
124131
.filter((indicator) => String(indicator.id).match(indicatorIdTest) && indicator.id.split('.').length === 1)
125132
.map((indicator) => formatIndicator(indicator, newEntity, indexMax));
126133

127-
newEntity.value = calculateWeightedMean(pillarIndicators, indexMax);
134+
newEntity.value = calculateWeightedMean(pillarIndicators, indexMax, clamp);
128135
if (!newEntity.user) {
129136
newEntity.user = {};
130137
}

src/utils.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@ export function clone(o) {
22
return JSON.parse(JSON.stringify(o));
33
}
44

5-
export function normalise(value, range = [0, 100], normaliseTo = 100) {
6-
return ((value - range[0]) / (range[1] - range[0])) * normaliseTo;
5+
export function clamper(range, value) { // restrict a value to between the vales of a tuple
6+
return Math.min(Math.max(value, range[0]), range[1]);
77
}
88

9-
export function calculateWeightedMean(weightedValues, normaliseTo = 100) {
9+
export function normalise(value, range = [0, 100], normaliseTo = 100, clamp = false) {
10+
let x = value;
11+
if (clamp) {
12+
x = clamper(range, value);
13+
}
14+
return ((x - range[0]) / (range[1] - range[0])) * normaliseTo;
15+
}
16+
17+
export function calculateWeightedMean(weightedValues, normaliseTo = 100, clamp = false) {
1018
let weightedSum = 0;
1119
let cumulativeWeight = 0;
1220
for (let i = 0; i < weightedValues.length; i += 1) {
1321
const indicator = weightedValues[i];
14-
const normalisedValue = normalise(indicator.value, indicator.range, normaliseTo);
22+
const normalisedValue = normalise(indicator.value, indicator.range, normaliseTo, clamp);
1523
const weightedValue = indicator.invert
1624
? ((normaliseTo - normalisedValue) * indicator.weight)
1725
: (normalisedValue * indicator.weight);

test/index-core.test.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ test('adjust indicator', ()=>{
2626
const adjustedValue = simpleIndex.indexedData['Monopoly'].value;
2727
simpleIndex.adjustValue('Monopoly');
2828
const resetValue = simpleIndex.indexedData['Monopoly'].value;
29-
expect(originalValue.toFixed(3)).toBe('49.118')
30-
expect(adjustedValue.toFixed(3)).toBe('53.118')
29+
expect(originalValue.toFixed(3)).toBe('50.118')
30+
expect(adjustedValue.toFixed(3)).toBe('53.451')
3131
expect(originalValue).toBe(resetValue);
3232
})
3333

@@ -37,7 +37,7 @@ test('reset individual indicator', ()=>{
3737
simpleIndex.adjustValue('Monopoly','1.2',1);
3838
simpleIndex.adjustValue('Monopoly','1.2');
3939
const resetValue = simpleIndex.indexedData['Monopoly'].value;
40-
expect(resetValue.toFixed(3)).toBe('53.118')
40+
expect(resetValue.toFixed(3)).toBe('53.451')
4141
})
4242

4343
test('getIndicatorMean index-core', ()=>{
@@ -72,7 +72,7 @@ test('diverging indicator index-core', ()=>{
7272

7373
test('indicator overide index-core', ()=>{
7474
const simpleIndexOverwrite = indexCore(simpleIndicators, simpleEntities, undefined, true);
75-
expect(simpleIndexOverwrite.indexedData['Catan']['1']).toBe(46.66666666666667);
75+
expect(simpleIndexOverwrite.indexedData['Catan']['1'].toFixed(3)).toBe('52.222');
7676
expect(simpleIndexOverwrite.indexedData['Twilight Imperium']['2.3']).toBe(70);
7777

7878
const simpleIndex = indexCore(simpleIndicators, simpleEntities, undefined, false);
@@ -92,4 +92,12 @@ test('check the return value from adjustValue', ()=>{
9292
expect(adjustedObject['1.2']).toBe(3.142);
9393
expect(adjustedObject['user']).toBe(undefined);
9494
expect(adjustedObject['data']).toBe(undefined);
95+
})
96+
97+
test('test for clamped values', ()=>{
98+
const clampedIndex = indexCore(simpleIndicators, simpleEntities, 100, true, true);
99+
const unrestrictedIndex = indexCore(simpleIndicators, simpleEntities);
100+
expect(clampedIndex.getEntity('Chinatown')['1'].toFixed(3)).toBe('62.778');
101+
expect(unrestrictedIndex.getEntity('Chinatown')['1'].toFixed(3)).toBe('79.444');
102+
95103
})

test/utils.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
calculateWeightedMean,
3+
clamper,
34
normalise
45
} from '../src/utils';
56

@@ -59,6 +60,16 @@ test('simple weighted mean', ()=>{
5960

6061
test('normalise', ()=>{
6162
const normA = normalise(32,[0,64],100);
63+
const normB = normalise(100,[0,64],100, true);
64+
const normC = normalise(96,[0,64],100);
6265
expect(normA).toBe(50);
66+
expect(normB).toBe(100);
67+
expect(normC).toBe(150);
6368
});
6469

70+
test('clamper',()=>{
71+
expect(clamper([0,100],300)).toBe(100);
72+
expect(clamper([0,100],-50)).toBe(0);
73+
expect(clamper([0,100],50)).toBe(50);
74+
})
75+

0 commit comments

Comments
 (0)