Users have 1000 cred in aggregate (#709)

This commit changes the cred normalization algorithm so that the total
cred of all GitHub user nodes always sums to 1000. For rationale on the
change, see #705.

Fixes #705.

Note that this introduces a new way for PageRank to fail: if the
graph has no GitHub userlike nodes, then PageRank will throw an error
when it attempts to normalize. This will result in a message being
displayed to the user, and a more helpful error being printed to
console. If we need the cred explorer to display graphs that have no
userlike nodes, then we can modify the codepath so that it falls back to
normalizing based on all nodes instead of on the GitHub userlike nodes
specifically.

Test plan: There is an included unit test which verifies that the
new argument gets threaded through the state properly. But this is
mostly a config change, so it's best tested by actually inspecting
the cred explorer. I have done so, and can verify that the behavior is
as expected: the sum of users' cred now sums to 1000, and e.g. modifying
the weight on the repository node doesn't produce drastic changes to
cred scores.
This commit is contained in:
Dandelion Mané 2018-08-29 12:20:57 -07:00 committed by GitHub
parent 5b47c504b9
commit a5c909689a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 36 additions and 16 deletions

View File

@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
- Normalize scores so that 1000 cred is split amongst users (#709)
- Stop persisting weights in local store (#706)
- Execute GraphQL queries with exponential backoff (#699)
- Introduce a simplified Git plugin that only tracks commits (#685)

View File

@ -105,7 +105,9 @@ export function createApp(
loadingState !== "LOADING"
)
}
onClick={() => this.stateTransitionMachine.runPagerank()}
onClick={() =>
this.stateTransitionMachine.runPagerank(GithubPrefix.userlike)
}
>
Run PageRank
</button>

View File

@ -3,7 +3,7 @@
import deepEqual from "lodash.isequal";
import * as NullUtil from "../../util/null";
import {Graph} from "../../core/graph";
import {Graph, type NodeAddressT} from "../../core/graph";
import type {Assets} from "../../app/assets";
import type {Repo} from "../../core/repo";
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
@ -80,7 +80,7 @@ export interface StateTransitionMachineInterface {
+setRepo: (Repo) => void;
+setEdgeEvaluator: (EdgeEvaluator) => void;
+loadGraph: (Assets) => Promise<void>;
+runPagerank: () => Promise<void>;
+runPagerank: (NodeAddressT) => Promise<void>;
}
/* In production, instantiate via createStateTransitionMachine; the constructor
* implementation allows specification of the loadGraphWithAdapters and
@ -201,7 +201,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
}
}
async runPagerank() {
async runPagerank(totalScoreNodePrefix: NodeAddressT) {
const state = this.getState();
if (
state.type !== "INITIALIZED" ||
@ -228,6 +228,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
edgeEvaluator,
{
verbose: true,
totalScoreNodePrefix: totalScoreNodePrefix,
}
);
const newSubstate = {

View File

@ -7,7 +7,7 @@ import {
type GraphWithAdapters,
} from "./state";
import {Graph} from "../../core/graph";
import {Graph, NodeAddress} from "../../core/graph";
import {Assets} from "../assets";
import {makeRepo, type Repo} from "../../core/repo";
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
@ -274,7 +274,9 @@ describe("app/credExplorer/state", () => {
const badStates = [initialState(), readyToLoadGraph()];
for (const b of badStates) {
const {stm} = example(b);
await expect(stm.runPagerank()).rejects.toThrow("incorrect state");
await expect(stm.runPagerank(NodeAddress.empty)).rejects.toThrow(
"incorrect state"
);
}
});
it("can be run when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
@ -283,7 +285,7 @@ describe("app/credExplorer/state", () => {
const {stm, getState, pagerankMock} = example(g);
const pnd = pagerankNodeDecomposition();
pagerankMock.mockResolvedValue(pnd);
await stm.runPagerank();
await stm.runPagerank(NodeAddress.empty);
const state = getState();
const substate = getSubstate(state);
if (substate.type !== "PAGERANK_EVALUATED") {
@ -296,9 +298,16 @@ describe("app/credExplorer/state", () => {
it("immediately sets loading status", () => {
const {getState, stm} = example(readyToRunPagerank());
expect(loading(getState())).toBe("NOT_LOADING");
stm.runPagerank();
stm.runPagerank(NodeAddress.empty);
expect(loading(getState())).toBe("LOADING");
});
it("calls pagerank with the totalScoreNodePrefix option", async () => {
const {pagerankMock, stm} = example(readyToRunPagerank());
const foo = NodeAddress.fromParts(["foo"]);
await stm.runPagerank(foo);
const args = pagerankMock.mock.calls[0];
expect(args[2].totalScoreNodePrefix).toBe(foo);
});
it("does not transition if another transition happens first", async () => {
const {getState, stm, pagerankMock} = example(readyToRunPagerank());
const swappedRepo = makeRepo("too", "fast");
@ -309,7 +318,7 @@ describe("app/credExplorer/state", () => {
resolve(graphWithAdapters());
})
);
await stm.runPagerank();
await stm.runPagerank(NodeAddress.empty);
const state = getState();
const substate = getSubstate(state);
expect(loading(state)).toBe("NOT_LOADING");
@ -322,7 +331,7 @@ describe("app/credExplorer/state", () => {
// $ExpectFlowError
console.error = jest.fn();
pagerankMock.mockRejectedValue(error);
await stm.runPagerank();
await stm.runPagerank(NodeAddress.empty);
const state = getState();
const substate = getSubstate(state);
expect(loading(state)).toBe("FAILED");

View File

@ -1,6 +1,6 @@
// @flow
import {type Edge, Graph} from "../graph";
import {type Edge, Graph, NodeAddress, type NodeAddressT} from "../graph";
import {
distributionToNodeDistribution,
createConnections,
@ -12,7 +12,7 @@ import {
type PagerankNodeDecomposition,
} from "./pagerankNodeDecomposition";
import {scoreByMaximumProbability} from "./nodeScore";
import {scoreByConstantTotal} from "./nodeScore";
import {findStationaryDistribution} from "./markovChain";
@ -23,8 +23,10 @@ export type PagerankOptions = {|
+verbose?: boolean,
+convergenceThreshold?: number,
+maxIterations?: number,
// Scores will be normalized so that `maxScore` is the highest score
+maxScore?: number,
// Scores will be normalized so that scores sum to totalScore
+totalScore?: number,
// Only nodes matching this prefix will count for normalization
+totalScoreNodePrefix?: NodeAddressT,
|};
export type {EdgeWeight} from "./graphToMarkovChain";
@ -36,7 +38,8 @@ function defaultOptions(): PagerankOptions {
selfLoopWeight: 1e-3,
convergenceThreshold: 1e-7,
maxIterations: 255,
maxScore: 1000,
totalScore: 1000,
totalScoreNodePrefix: NodeAddress.empty,
};
}
@ -62,6 +65,10 @@ export async function pagerank(
yieldAfterMs: 30,
});
const pi = distributionToNodeDistribution(osmc.nodeOrder, distribution);
const scores = scoreByMaximumProbability(pi, fullOptions.maxScore);
const scores = scoreByConstantTotal(
pi,
fullOptions.totalScore,
fullOptions.totalScoreNodePrefix
);
return decompose(scores, connections);
}