Update distributionToCred contract (#1560)

Resolves #1317

Updates timeline cred to handle the case where the scoring nodes' total
cred sums to zero in an interval. In practice, we've encountered this
circumstance when a github.io repository contains timestamps that
predates any User's contributions by several weeks, such as
sfosc.github.io.

Test Plan:

- Added a test case to handle this circumstance
- Updated a test case per discussion on #1317 to return a cred score
of 0 for all nodes in all intervals when there are no scoring nodes
passed to the function, so that we handle these cases consistently.

Also loaded sfosc.github.io and observed that the cred output appeared
to match expectations and didn't contain any `NaN` or `Infinity` values
as it did before.
This commit is contained in:
Brian Litwin 2020-01-19 18:09:51 -05:00 committed by Dandelion Mané
parent 74acd1fa80
commit dea45fb8c1
3 changed files with 40 additions and 12 deletions

View File

@ -30,6 +30,13 @@ export type FullTimelineCred = $ReadOnlyArray<{|
* This implementation normalizes the scores so that in each interval, the
* total score of every node matching scoringNodePrefix is equal to the
* interval's weight.
*
* Edge cases:
* - If in an interval the sum of the scoring nodes' distribution is 0,
* return a total cred score of 0 for all nodes in the interval.
* - If none of the nodes match a scoring node prefix, return
* a total cred score of 0 for all nodes in all intervals.
*
*/
export function distributionToCred(
ds: TimelineDistributions,
@ -49,15 +56,14 @@ export function distributionToCred(
}
cred[i] = new Array(intervals.length);
}
if (scoringNodeIndices.length === 0) {
throw new Error("no nodes matched scoringNodePrefix");
}
return ds.map(({interval, distribution, intervalWeight}) => {
const intervalTotalScore = sum(
scoringNodeIndices.map((x) => distribution[x])
);
const intervalNormalizer = intervalWeight / intervalTotalScore;
const intervalNormalizer =
intervalTotalScore == 0 ? 0 : intervalWeight / intervalTotalScore;
const cred = distribution.map((x) => x * intervalNormalizer);
return {interval, cred};
});

View File

@ -87,7 +87,7 @@ describe("src/analysis/timeline/distributionToCred", () => {
];
expect(expected).toEqual(actual);
});
it("errors when no nodes are scoring", () => {
it("handles the case where no nodes are scoring", () => {
const ds = [
{
interval: {startTimeMs: 0, endTimeMs: 10},
@ -96,9 +96,35 @@ describe("src/analysis/timeline/distributionToCred", () => {
},
];
const nodeOrder = [na("foo"), na("bar")];
const fail = () => distributionToCred(ds, nodeOrder, []);
expect(fail).toThrowError("no nodes matched scoringNodePrefix");
const actual = distributionToCred(ds, nodeOrder, []);
const expected = [
{
interval: {startTimeMs: 0, endTimeMs: 10},
cred: new Float64Array([0, 0]),
},
];
expect(actual).toEqual(expected);
});
it("handles the case where all nodes' cred sums to zero", () => {
const ds = [
{
interval: {startTimeMs: 0, endTimeMs: 10},
intervalWeight: 2,
distribution: new Float64Array([1, 0]),
},
];
const nodeOrder = [na("foo"), na("bar")];
const actual = distributionToCred(ds, nodeOrder, [na("bar")]);
const expected = [
{
interval: {startTimeMs: 0, endTimeMs: 10},
cred: new Float64Array([0, 0]),
},
];
expect(actual).toEqual(expected);
});
it("returns empty array if no intervals are present", () => {
expect(distributionToCred([], [], [])).toEqual([]);
});

View File

@ -242,11 +242,7 @@ export class TimelineCred {
fullParams.intervalDecay,
fullParams.alpha
);
const cred = distributionToCred(
distribution,
nodeOrder,
userTypes.map((x) => x.prefix)
);
const cred = distributionToCred(distribution, nodeOrder, scorePrefixes);
const addressToCred = new Map();
for (let i = 0; i < nodeOrder.length; i++) {
const addr = nodeOrder[i];