Skip to content

Commit 99aa014

Browse files
author
Tom Pearson
authored
Add ability to exclude indicators from calculation (#10)
* exclude indicators based on a predicate function * version bump * docs
1 parent 33471e6 commit 99aa014

File tree

5 files changed

+85
-26
lines changed

5 files changed

+85
-26
lines changed

README.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,43 @@ Take a look at the data source descriptions for a fuller description of the prop
6666

6767
__indexMax__, optional. is the maximum value for the index score (minimum is always 0) by thefault this is 100, other typical values are 1 and 10.
6868

69-
### indexCore.__indexedData__:Object
70-
An object where each property is an _entity_ in the index and that entity has properties for each of the properties in the indicators spreadsheet _and_ any calculated values.
71-
### indexCore.__indexStructure__:Object
72-
An object representing the structure of the index
73-
### indexCore.__adjustWeight(_indicatorID:String_, _weight:Number_)__
74-
A function that allows the user adjust the weighting of an indicator to give it a greater or lesser importance in theindex as whole. The index is recalculated after calling this function.
7569
### indexCore.__adjustValue(_entityName:String_, _indicatorID:String_, _value:Number_)__
7670
A function that allows the user to adjust an entity's indicator score within the index the index is recalculated using the new value. The index is recalculated after calling this function.
7771

7872
_NOTE: whislt the old value is retained there's currently no way to reset it._
73+
74+
### indexCore.__adjustWeight(_indicatorID:String_, _weight:Number_)__
75+
A function that allows the user adjust the weighting of an indicator to give it a greater or lesser importance in theindex as whole. The index is recalculated after calling this function.
76+
77+
### indexCore.__filterIndicators([_exclude:function_])__
78+
Allows the user to specify a predicate which excludes certain indicators from the calculation e.g.
79+
```js
80+
// if the indicator includes "b" in its ID then ignore it
81+
indexCore.filterIndicators(indicator => String(indicator.id).indexOf('b')>0 )
82+
```
83+
84+
You can clear the exclusion by calling the same function with no arguments
85+
```js
86+
// if the indicator includes "b" in its then id ignore it
87+
indexCore.filterIndicators()
88+
```
89+
90+
### indexCore.__getEntity([indicatorId:String])__:Object
91+
92+
### indexCore.__getEntities()__:Array
93+
7994
### indexCore.__getIndexMean([indicatorId:String], _[normalise:Boolean=false]_)__:Number
95+
96+
### indexCore.__getIndicator()__:Object
97+
98+
### indexCore.__getIndicatorLookup()__:Object
99+
100+
### indexCore.__indexedData__:Object
101+
An object where each property is an _entity_ in the index and that entity has properties for each of the properties in the indicators spreadsheet _and_ any calculated values.
102+
103+
### indexCore.__indexStructure__:Object
104+
An object representing the structure of the index
105+
r
80106
Returns the mean value for a given indicator accross all entities in the index which posses that value. Note that if an indicator does not posses that value it's excluded from the calculation and a sane result should be returned. Providing no _indicatorId_ will result in the mean of the top level index scores being returned. The second argument determines whether the result will be expressed as normalised (which is the default) or in the raw quantities of the indicator. i.e. if an indicator represents the number of hours of sunlight it could be better to express it un-normalised as in the mean number of hours rather than normalised to between e.g. 1-100.
81107

82108
---

index.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
1+
import { clone } from './src/utils.js';
12
import {csvParse} from 'd3';
23
import fs from 'fs';
34
import indexCore from './src/index-core.js';
45

56
const waterRootDir = 'data/wateroptimisation';
6-
const educationRootDir = 'data/education';
77

88
const waterIndicators = csvParse(fs.readFileSync(`${waterRootDir}/indicators.csv`, 'utf-8'));
99
const waterEntities = csvParse(fs.readFileSync(`${waterRootDir}/entities.csv`, 'utf-8'));
1010

11-
const educationIndicators = csvParse(fs.readFileSync(`${educationRootDir}/indicators.csv`, 'utf-8'));
12-
const educationEntities = csvParse(fs.readFileSync(`${educationRootDir}/entities.csv`, 'utf-8'));
13-
1411
const waterOptimisationIndex = indexCore(waterIndicators, waterEntities);
1512

1613

17-
const educationIndex = indexCore(educationIndicators, educationEntities);
18-
1914
// console.log(waterOptimisationIndex.indexedData['Abu Dhabi']['1']);
2015
// console.log(waterOptimisationIndex.indexedData['Abu Dhabi'].value);
2116
// console.log(waterOptimisationIndex.indexStructure);
2217
// console.log(waterOptimisationIndex.getIndexMean('1.1'))
2318
// console.log(waterOptimisationIndex.getIndexMean('2.1.1'))
2419
// console.log(waterOptimisationIndex.getIndexMean())
2520

26-
console.log(educationIndex.indexStructure);
27-
console.log(educationIndex.indexedData);
21+
// indicator 3.4.4 in the water index has .a and .b sub indicators
22+
// for this indicator abu dhabi has different values for a and b
23+
const before = JSON.stringify(waterOptimisationIndex.getEntity('Abu Dhabi'), null, ' ')
24+
delete before.data;
25+
waterOptimisationIndex.filterIndicators(indicator=>{
26+
return String(indicator.id).indexOf('b')>0; // if the indicator includes "b" in it's id ignore it
27+
})
28+
const after = JSON.stringify(waterOptimisationIndex.getEntity('Abu Dhabi'), null, ' ')
29+
delete after.data; // just for neater output (note data is the original data for an entity used to calculate the index)
30+
31+
console.log('BEFORE', before);
32+
console.log('AFTER', after);

package.json

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

src/index-core.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100) {
1010
);
1111
const indexedData = {};
1212
let indexStructure = {};
13+
let excludeIndicator = () => false; // by default no valid indicators are excluded
1314

1415
function getEntity(entityName) {
1516
return indexedData[entityName];
16-
// return entitiesData.find((d) => d.name === entityName);
1717
}
1818

1919
function getEntities() {
@@ -74,13 +74,14 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100) {
7474

7575
function indexEntity(entity, calculationList) {
7676
const newEntity = clone(entity);
77-
7877
calculationList.forEach((indicatorID) => {
7978
if (newEntity[indicatorID]) { console.log('overwriting', indicatorID); }
80-
// get the required component indicators tocalculate the parent value
79+
// get the required component indicators to calculate the parent value
80+
// this is a bit brittle maybe?
8181
const componentIndicators = indicatorsData
8282
.filter((indicator) => (indicator.id.indexOf(indicatorID) === 0
8383
&& indicator.id.length === indicatorID.length + 2))
84+
.filter((indicator) => !excludeIndicator(indicator))
8485
.map((indicator) => formatIndicator(indicator, newEntity, indexMax));
8586
// calculate the weighted mean of the component indicators on the newEntity
8687
// assign that value to the newEntity
@@ -103,6 +104,8 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100) {
103104
}
104105
if (!e.user) e.user = {};
105106
e.user[indicatorID] = value;
107+
//note the re-indexed value for the entity is not stored
108+
//only the adjustment the re-indexed value is returned to the caller
106109
return indexEntity(Object.assign(clone(e), e.user));
107110
}
108111

@@ -133,12 +136,17 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100) {
133136
return tree;
134137
}
135138

136-
function calculateIndex(exclude = () => false) {
137-
const onlyIdIndicators = indicatorsData.filter((i) => i.id.match(indicatorIdTest));
139+
function calculateIndex() {
140+
const onlyIdIndicators = indicatorsData
141+
.filter((i) =>{
142+
const isIndicator = i.id.match(indicatorIdTest)
143+
const isExcluded = excludeIndicator(i);
144+
return isIndicator && !isExcluded;
145+
});
138146
// get a list of the values we need to calculate
139147
// in order of deepest in the heirachy to to shallowist
140148
const calculationList = onlyIdIndicators
141-
.filter((i) => i.type === 'calculated' && !exclude(i))
149+
.filter((i) => (i.type === 'calculated' && !excludeIndicator(i)))
142150
.map((i) => i.id)
143151
.sort((i1, i2) => (i2.split('.').length - i1.split('.').length));
144152

@@ -147,6 +155,7 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100) {
147155
entitiesData.forEach((entity) => {
148156
const indexedEntity = indexEntity(entity, calculationList);
149157
indexedEntity.data = entity;
158+
indexedEntity.ts = new Date();
150159
indexedData[entity.name] = indexedEntity;
151160
});
152161
}
@@ -158,18 +167,24 @@ function indexCore(indicatorsData = [], entitiesData = [], indexMax = 100) {
158167
calculateIndex();
159168
}
160169

170+
function filterIndicators(exclude = ()=>false){
171+
excludeIndicator = exclude;
172+
calculateIndex();
173+
}
174+
161175
calculateIndex();
162176

163177
return {
164178
adjustValue,
165179
adjustWeight,
166-
indexedData,
167-
indexStructure,
168-
getIndexMean,
180+
filterIndicators,
169181
getEntity,
182+
getEntities,
183+
getIndexMean,
170184
getIndicator,
171185
getIndicatorLookup,
172-
getEntities,
186+
indexedData,
187+
indexStructure,
173188
};
174189
}
175190

test/index-core.test.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,20 @@ test('getIndicatorMean index-core', ()=>{
2020
expect(waterOptimisationIndex.getIndexMean('1.2').toFixed(1)).toBe('87.9');
2121
expect(waterOptimisationIndex.getIndexMean('2.2').toFixed(1)).toBe('74.9');
2222
expect(waterOptimisationIndex.getIndexMean('2.1.1').toFixed(1)).toBe('89.8');
23+
expect(waterOptimisationIndex.getIndexMean('3.1.1').toFixed(1)).toBe('38.2');
2324
expect(waterOptimisationIndex.getIndexMean('2').toFixed(1)).toBe('74.7');
2425
expect(waterOptimisationIndex.getIndexMean().toFixed(1)).toBe('69.5');
25-
26+
});
27+
28+
test('filterIndicators index-core', ()=>{
29+
const waterOptimisationIndex = indexCore(waterIndicators, waterEntities);
30+
const excluder = (indicator) => (String(indicator.id).indexOf('b')>0); // if the indicator includes "b" in it's id ignore it
31+
waterOptimisationIndex.filterIndicators(excluder)
32+
expect(waterOptimisationIndex.getIndexMean('1').toFixed(1)).toBe('71.2');
33+
expect(waterOptimisationIndex.getIndexMean('1.2').toFixed(1)).toBe('87.9');
34+
expect(waterOptimisationIndex.getIndexMean('2.2').toFixed(1)).toBe('74.9');
35+
expect(waterOptimisationIndex.getIndexMean('2.1.1').toFixed(1)).toBe('89.8');
36+
expect(waterOptimisationIndex.getIndexMean('3.1.1').toFixed(1)).toBe('25.0');
37+
expect(waterOptimisationIndex.getIndexMean('2').toFixed(1)).toBe('74.7');
38+
expect(waterOptimisationIndex.getIndexMean().toFixed(1)).toBe('68.7');
2639
});

0 commit comments

Comments
 (0)