Rename Contributor & Contribution types (#607)
Consider the following types: ``` // Used to be called "Contributor" export type Adjacency = | {|+type: "SYNTHETIC_LOOP"|} | {|+type: "IN_EDGE", +edge: Edge|} | {|+type: "OUT_EDGE", +edge: Edge|}; // Used to be called "Contribution" export type Connection = {| +adjacency: Adjacency, // This `weight` is a conditional probability: given that you're at // the source of this connection's adjacency, what's the // probability that you travel along this connection to the target? +weight: Probability, |}; // Used to be called "ScoredContribution" export type ScoredConnection = {| +connection: Connection, +source: NodeAddressT, +sourceScore: number, +connectionScore: number, |}; ``` These types represent how a node's PagerankScore is influenced by its connections in the markov chain. The previous names, "Contributor", "Contribution" and "ScoredContribution", were quite confusing as elsewhere in the project "contributon" means something that added value to the project, and "contributor" means the author of contributions. While these new names aren't necessarily much better a priori, in the context of the project's vernacular they are much less confusing. Test plan: It's just a rename, and `yarn test` passes.
This commit is contained in:
parent
59ac10b612
commit
00da630bb2
|
@ -11,9 +11,9 @@ import {
|
||||||
} from "../../core/graph";
|
} from "../../core/graph";
|
||||||
import type {
|
import type {
|
||||||
PagerankNodeDecomposition,
|
PagerankNodeDecomposition,
|
||||||
ScoredContribution,
|
ScoredConnection,
|
||||||
} from "../../core/attribution/pagerankNodeDecomposition";
|
} from "../../core/attribution/pagerankNodeDecomposition";
|
||||||
import type {Contribution} from "../../core/attribution/graphToMarkovChain";
|
import type {Connection} from "../../core/attribution/graphToMarkovChain";
|
||||||
import type {DynamicPluginAdapter} from "../pluginAdapter";
|
import type {DynamicPluginAdapter} from "../pluginAdapter";
|
||||||
import * as NullUtil from "../../util/null";
|
import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ export class PagerankTable extends React.PureComponent<
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{textAlign: "left"}}>Description</th>
|
<th style={{textAlign: "left"}}>Description</th>
|
||||||
<th style={{textAlign: "right"}}>Contribution</th>
|
<th style={{textAlign: "right"}}>Connection</th>
|
||||||
<th style={{textAlign: "right"}}>Score</th>
|
<th style={{textAlign: "right"}}>Score</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -241,7 +241,7 @@ export class NodeRow extends React.PureComponent<NodeRowProps, RowState> {
|
||||||
<td style={{textAlign: "right"}}>{scoreDisplay(score)}</td>
|
<td style={{textAlign: "right"}}>{scoreDisplay(score)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<ContributionRowList
|
<ConnectionRowList
|
||||||
key="children"
|
key="children"
|
||||||
depth={1}
|
depth={1}
|
||||||
node={node}
|
node={node}
|
||||||
|
@ -253,29 +253,29 @@ export class NodeRow extends React.PureComponent<NodeRowProps, RowState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContributionRowListProps = {|
|
type ConnectionRowListProps = {|
|
||||||
+depth: number,
|
+depth: number,
|
||||||
+node: NodeAddressT,
|
+node: NodeAddressT,
|
||||||
+sharedProps: SharedProps,
|
+sharedProps: SharedProps,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export class ContributionRowList extends React.PureComponent<
|
export class ConnectionRowList extends React.PureComponent<
|
||||||
ContributionRowListProps
|
ConnectionRowListProps
|
||||||
> {
|
> {
|
||||||
render() {
|
render() {
|
||||||
const {depth, node, sharedProps} = this.props;
|
const {depth, node, sharedProps} = this.props;
|
||||||
const {pnd, maxEntriesPerList} = sharedProps;
|
const {pnd, maxEntriesPerList} = sharedProps;
|
||||||
const {scoredContributions} = NullUtil.get(pnd.get(node));
|
const {scoredConnections} = NullUtil.get(pnd.get(node));
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{scoredContributions
|
{scoredConnections
|
||||||
.slice(0, maxEntriesPerList)
|
.slice(0, maxEntriesPerList)
|
||||||
.map((sc) => (
|
.map((sc) => (
|
||||||
<ContributionRow
|
<ConnectionRow
|
||||||
key={JSON.stringify(sc.contribution.contributor)}
|
key={JSON.stringify(sc.connection.adjacency)}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
target={node}
|
target={node}
|
||||||
scoredContribution={sc}
|
scoredConnection={sc}
|
||||||
sharedProps={sharedProps}
|
sharedProps={sharedProps}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -284,15 +284,15 @@ export class ContributionRowList extends React.PureComponent<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContributionRowProps = {|
|
type ConnectionRowProps = {|
|
||||||
+depth: number,
|
+depth: number,
|
||||||
+target: NodeAddressT,
|
+target: NodeAddressT,
|
||||||
+scoredContribution: ScoredContribution,
|
+scoredConnection: ScoredConnection,
|
||||||
+sharedProps: SharedProps,
|
+sharedProps: SharedProps,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export class ContributionRow extends React.PureComponent<
|
export class ConnectionRow extends React.PureComponent<
|
||||||
ContributionRowProps,
|
ConnectionRowProps,
|
||||||
RowState
|
RowState
|
||||||
> {
|
> {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -304,18 +304,13 @@ export class ContributionRow extends React.PureComponent<
|
||||||
sharedProps,
|
sharedProps,
|
||||||
target,
|
target,
|
||||||
depth,
|
depth,
|
||||||
scoredContribution: {
|
scoredConnection: {connection, source, sourceScore, connectionScore},
|
||||||
contribution,
|
|
||||||
source,
|
|
||||||
sourceScore,
|
|
||||||
contributionScore,
|
|
||||||
},
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {pnd, adapters} = sharedProps;
|
const {pnd, adapters} = sharedProps;
|
||||||
const {expanded} = this.state;
|
const {expanded} = this.state;
|
||||||
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
const {score: targetScore} = NullUtil.get(pnd.get(target));
|
||||||
const contributionProportion = contributionScore / targetScore;
|
const connectionProportion = connectionScore / targetScore;
|
||||||
const contributionPercent = (contributionProportion * 100).toFixed(2);
|
const connectionPercent = (connectionProportion * 100).toFixed(2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -337,13 +332,13 @@ export class ContributionRow extends React.PureComponent<
|
||||||
>
|
>
|
||||||
{expanded ? "\u2212" : "+"}
|
{expanded ? "\u2212" : "+"}
|
||||||
</button>
|
</button>
|
||||||
<ContributionView contribution={contribution} adapters={adapters} />
|
<ConnectionView connection={connection} adapters={adapters} />
|
||||||
</td>
|
</td>
|
||||||
<td style={{textAlign: "right"}}>{contributionPercent}%</td>
|
<td style={{textAlign: "right"}}>{connectionPercent}%</td>
|
||||||
<td style={{textAlign: "right"}}>{scoreDisplay(sourceScore)}</td>
|
<td style={{textAlign: "right"}}>{scoreDisplay(sourceScore)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<ContributionRowList
|
<ConnectionRowList
|
||||||
key="children"
|
key="children"
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
node={source}
|
node={source}
|
||||||
|
@ -355,12 +350,12 @@ export class ContributionRow extends React.PureComponent<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContributionView extends React.PureComponent<{|
|
export class ConnectionView extends React.PureComponent<{|
|
||||||
+contribution: Contribution,
|
+connection: Connection,
|
||||||
+adapters: $ReadOnlyArray<DynamicPluginAdapter>,
|
+adapters: $ReadOnlyArray<DynamicPluginAdapter>,
|
||||||
|}> {
|
|}> {
|
||||||
render() {
|
render() {
|
||||||
const {contribution, adapters} = this.props;
|
const {connection, adapters} = this.props;
|
||||||
function Badge({children}) {
|
function Badge({children}) {
|
||||||
return (
|
return (
|
||||||
// The outer <span> acts as a strut to ensure that the badge
|
// The outer <span> acts as a strut to ensure that the badge
|
||||||
|
@ -378,30 +373,30 @@ export class ContributionView extends React.PureComponent<{|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const {contributor} = contribution;
|
const {adjacency} = connection;
|
||||||
switch (contributor.type) {
|
switch (adjacency.type) {
|
||||||
case "SYNTHETIC_LOOP":
|
case "SYNTHETIC_LOOP":
|
||||||
return <Badge>synthetic loop</Badge>;
|
return <Badge>synthetic loop</Badge>;
|
||||||
case "IN_EDGE":
|
case "IN_EDGE":
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<Badge>
|
<Badge>
|
||||||
{edgeVerb(contributor.edge.address, "BACKWARD", adapters)}
|
{edgeVerb(adjacency.edge.address, "BACKWARD", adapters)}
|
||||||
</Badge>{" "}
|
</Badge>{" "}
|
||||||
<span>{nodeDescription(contributor.edge.src, adapters)}</span>
|
<span>{nodeDescription(adjacency.edge.src, adapters)}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case "OUT_EDGE":
|
case "OUT_EDGE":
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<Badge>
|
<Badge>
|
||||||
{edgeVerb(contributor.edge.address, "FORWARD", adapters)}
|
{edgeVerb(adjacency.edge.address, "FORWARD", adapters)}
|
||||||
</Badge>{" "}
|
</Badge>{" "}
|
||||||
<span>{nodeDescription(contributor.edge.dst, adapters)}</span>
|
<span>{nodeDescription(adjacency.edge.dst, adapters)}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error((contributor.type: empty));
|
throw new Error((adjacency.type: empty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,13 @@ import {
|
||||||
PagerankTable,
|
PagerankTable,
|
||||||
NodeRowList,
|
NodeRowList,
|
||||||
NodeRow,
|
NodeRow,
|
||||||
ContributionRowList,
|
ConnectionRowList,
|
||||||
ContributionRow,
|
ConnectionRow,
|
||||||
ContributionView,
|
ConnectionView,
|
||||||
} from "./PagerankTable";
|
} from "./PagerankTable";
|
||||||
import {pagerank} from "../../core/attribution/pagerank";
|
import {pagerank} from "../../core/attribution/pagerank";
|
||||||
import sortBy from "lodash.sortby";
|
import sortBy from "lodash.sortby";
|
||||||
import {type Contribution} from "../../core/attribution/graphToMarkovChain";
|
import {type Connection} from "../../core/attribution/graphToMarkovChain";
|
||||||
import * as NullUtil from "../../util/null";
|
import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -26,7 +26,7 @@ import {
|
||||||
|
|
||||||
require("../testUtil").configureEnzyme();
|
require("../testUtil").configureEnzyme();
|
||||||
|
|
||||||
const COLUMNS = () => ["Description", "Contribution", "Score"];
|
const COLUMNS = () => ["Description", "Connection", "Score"];
|
||||||
|
|
||||||
async function example() {
|
async function example() {
|
||||||
const graph = new Graph();
|
const graph = new Graph();
|
||||||
|
@ -337,14 +337,14 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
.text()
|
.text()
|
||||||
).toEqual(expectedDescription);
|
).toEqual(expectedDescription);
|
||||||
});
|
});
|
||||||
it("renders an empty contribution column", async () => {
|
it("renders an empty connection column", async () => {
|
||||||
const {element} = await setup();
|
const {element} = await setup();
|
||||||
const contributionColumn = COLUMNS().indexOf("Contribution");
|
const connectionColumn = COLUMNS().indexOf("Connection");
|
||||||
expect(contributionColumn).not.toEqual(-1);
|
expect(connectionColumn).not.toEqual(-1);
|
||||||
expect(
|
expect(
|
||||||
element
|
element
|
||||||
.find("td")
|
.find("td")
|
||||||
.at(contributionColumn)
|
.at(connectionColumn)
|
||||||
.text()
|
.text()
|
||||||
).toEqual("—");
|
).toEqual("—");
|
||||||
});
|
});
|
||||||
|
@ -352,18 +352,18 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
const {element, sharedProps, node} = await setup();
|
const {element, sharedProps, node} = await setup();
|
||||||
const {score: rawScore} = NullUtil.get(sharedProps.pnd.get(node));
|
const {score: rawScore} = NullUtil.get(sharedProps.pnd.get(node));
|
||||||
const expectedScore = (-Math.log(rawScore)).toFixed(2);
|
const expectedScore = (-Math.log(rawScore)).toFixed(2);
|
||||||
const contributionColumn = COLUMNS().indexOf("Score");
|
const connectionColumn = COLUMNS().indexOf("Score");
|
||||||
expect(contributionColumn).not.toEqual(-1);
|
expect(connectionColumn).not.toEqual(-1);
|
||||||
expect(
|
expect(
|
||||||
element
|
element
|
||||||
.find("td")
|
.find("td")
|
||||||
.at(contributionColumn)
|
.at(connectionColumn)
|
||||||
.text()
|
.text()
|
||||||
).toEqual(expectedScore);
|
).toEqual(expectedScore);
|
||||||
});
|
});
|
||||||
it("does not render children by default", async () => {
|
it("does not render children by default", async () => {
|
||||||
const {element} = await setup();
|
const {element} = await setup();
|
||||||
expect(element.find("ContributionRowList")).toHaveLength(0);
|
expect(element.find("ConnectionRowList")).toHaveLength(0);
|
||||||
});
|
});
|
||||||
it('has a working "expand" button', async () => {
|
it('has a working "expand" button', async () => {
|
||||||
const {element, sharedProps, node} = await setup();
|
const {element, sharedProps, node} = await setup();
|
||||||
|
@ -371,7 +371,7 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
|
|
||||||
element.find("button").simulate("click");
|
element.find("button").simulate("click");
|
||||||
expect(element.find("button").text()).toEqual("\u2212");
|
expect(element.find("button").text()).toEqual("\u2212");
|
||||||
const crl = element.find("ContributionRowList");
|
const crl = element.find("ConnectionRowList");
|
||||||
expect(crl).toHaveLength(1);
|
expect(crl).toHaveLength(1);
|
||||||
expect(crl.prop("sharedProps")).toEqual(sharedProps);
|
expect(crl.prop("sharedProps")).toEqual(sharedProps);
|
||||||
expect(crl.prop("depth")).toBe(1);
|
expect(crl.prop("depth")).toBe(1);
|
||||||
|
@ -379,18 +379,18 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
|
|
||||||
element.find("button").simulate("click");
|
element.find("button").simulate("click");
|
||||||
expect(element.find("button").text()).toEqual("+");
|
expect(element.find("button").text()).toEqual("+");
|
||||||
expect(element.find("ContributionRowList")).toHaveLength(0);
|
expect(element.find("ConnectionRowList")).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ContributionRowList", () => {
|
describe("ConnectionRowList", () => {
|
||||||
async function setup(maxEntriesPerList: number = 100000) {
|
async function setup(maxEntriesPerList: number = 100000) {
|
||||||
const {adapters, pnd, nodes} = await example();
|
const {adapters, pnd, nodes} = await example();
|
||||||
const depth = 2;
|
const depth = 2;
|
||||||
const node = nodes.bar1;
|
const node = nodes.bar1;
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
const sharedProps = {adapters, pnd, maxEntriesPerList};
|
||||||
const component = (
|
const component = (
|
||||||
<ContributionRowList
|
<ConnectionRowList
|
||||||
depth={depth}
|
depth={depth}
|
||||||
node={node}
|
node={node}
|
||||||
sharedProps={sharedProps}
|
sharedProps={sharedProps}
|
||||||
|
@ -399,64 +399,60 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
const element = shallow(component);
|
const element = shallow(component);
|
||||||
return {element, depth, node, sharedProps};
|
return {element, depth, node, sharedProps};
|
||||||
}
|
}
|
||||||
it("creates `ContributionRow`s with the right props", async () => {
|
it("creates `ConnectionRow`s with the right props", async () => {
|
||||||
const {element, depth, node, sharedProps} = await setup();
|
const {element, depth, node, sharedProps} = await setup();
|
||||||
const contributions = NullUtil.get(sharedProps.pnd.get(node))
|
const connections = NullUtil.get(sharedProps.pnd.get(node))
|
||||||
.scoredContributions;
|
.scoredConnections;
|
||||||
const rows = element.find("ContributionRow");
|
const rows = element.find("ConnectionRow");
|
||||||
expect(rows).toHaveLength(contributions.length);
|
expect(rows).toHaveLength(connections.length);
|
||||||
const rowPropses = rows.map((row) => row.props());
|
const rowPropses = rows.map((row) => row.props());
|
||||||
// Order should be the same as the order in the decomposition.
|
// Order should be the same as the order in the decomposition.
|
||||||
expect(rowPropses).toEqual(
|
expect(rowPropses).toEqual(
|
||||||
contributions.map((sc) => ({
|
connections.map((sc) => ({
|
||||||
depth,
|
depth,
|
||||||
sharedProps,
|
sharedProps,
|
||||||
target: node,
|
target: node,
|
||||||
scoredContribution: sc,
|
scoredConnection: sc,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it("limits the number of rows by `maxEntriesPerList`", async () => {
|
it("limits the number of rows by `maxEntriesPerList`", async () => {
|
||||||
const maxEntriesPerList = 1;
|
const maxEntriesPerList = 1;
|
||||||
const {element, node, sharedProps} = await setup(maxEntriesPerList);
|
const {element, node, sharedProps} = await setup(maxEntriesPerList);
|
||||||
const contributions = NullUtil.get(sharedProps.pnd.get(node))
|
const connections = NullUtil.get(sharedProps.pnd.get(node))
|
||||||
.scoredContributions;
|
.scoredConnections;
|
||||||
expect(contributions.length).toBeGreaterThan(maxEntriesPerList);
|
expect(connections.length).toBeGreaterThan(maxEntriesPerList);
|
||||||
const rows = element.find("ContributionRow");
|
const rows = element.find("ConnectionRow");
|
||||||
expect(rows).toHaveLength(maxEntriesPerList);
|
expect(rows).toHaveLength(maxEntriesPerList);
|
||||||
const rowContributions = rows.map((row) =>
|
const rowConnections = rows.map((row) => row.prop("scoredConnection"));
|
||||||
row.prop("scoredContribution")
|
|
||||||
);
|
|
||||||
// Should have selected the right nodes.
|
// Should have selected the right nodes.
|
||||||
expect(rowContributions).toEqual(
|
expect(rowConnections).toEqual(connections.slice(0, maxEntriesPerList));
|
||||||
contributions.slice(0, maxEntriesPerList)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ContributionRow", () => {
|
describe("ConnectionRow", () => {
|
||||||
async function setup() {
|
async function setup() {
|
||||||
const {pnd, adapters, nodes} = await example();
|
const {pnd, adapters, nodes} = await example();
|
||||||
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
|
||||||
const target = nodes.bar1;
|
const target = nodes.bar1;
|
||||||
const {scoredContributions} = NullUtil.get(pnd.get(target));
|
const {scoredConnections} = NullUtil.get(pnd.get(target));
|
||||||
const alphaContributions = scoredContributions.filter(
|
const alphaConnections = scoredConnections.filter(
|
||||||
(sc) => sc.source === nodes.fooAlpha
|
(sc) => sc.source === nodes.fooAlpha
|
||||||
);
|
);
|
||||||
expect(alphaContributions).toHaveLength(1);
|
expect(alphaConnections).toHaveLength(1);
|
||||||
const contribution = alphaContributions[0];
|
const connection = alphaConnections[0];
|
||||||
const {source} = contribution;
|
const {source} = connection;
|
||||||
const depth = 2;
|
const depth = 2;
|
||||||
const component = (
|
const component = (
|
||||||
<ContributionRow
|
<ConnectionRow
|
||||||
depth={depth}
|
depth={depth}
|
||||||
target={target}
|
target={target}
|
||||||
scoredContribution={contribution}
|
scoredConnection={connection}
|
||||||
sharedProps={sharedProps}
|
sharedProps={sharedProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const element = shallow(component);
|
const element = shallow(component);
|
||||||
return {element, depth, target, source, contribution, sharedProps};
|
return {element, depth, target, source, connection, sharedProps};
|
||||||
}
|
}
|
||||||
it("renders the right number of columns", async () => {
|
it("renders the right number of columns", async () => {
|
||||||
expect((await setup()).element.find("td")).toHaveLength(COLUMNS().length);
|
expect((await setup()).element.find("td")).toHaveLength(COLUMNS().length);
|
||||||
|
@ -469,25 +465,25 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
}).toMatchSnapshot();
|
}).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it("renders the source view", async () => {
|
it("renders the source view", async () => {
|
||||||
const {element, sharedProps, contribution} = await setup();
|
const {element, sharedProps, connection} = await setup();
|
||||||
const descriptionColumn = COLUMNS().indexOf("Description");
|
const descriptionColumn = COLUMNS().indexOf("Description");
|
||||||
expect(descriptionColumn).not.toEqual(-1);
|
expect(descriptionColumn).not.toEqual(-1);
|
||||||
const view = element
|
const view = element
|
||||||
.find("td")
|
.find("td")
|
||||||
.at(descriptionColumn)
|
.at(descriptionColumn)
|
||||||
.find("ContributionView");
|
.find("ConnectionView");
|
||||||
expect(view).toHaveLength(1);
|
expect(view).toHaveLength(1);
|
||||||
expect(view.props()).toEqual({
|
expect(view.props()).toEqual({
|
||||||
adapters: sharedProps.adapters,
|
adapters: sharedProps.adapters,
|
||||||
contribution: contribution.contribution,
|
connection: connection.connection,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("renders the contribution percentage", async () => {
|
it("renders the connection percentage", async () => {
|
||||||
const {element, contribution, sharedProps, target} = await setup();
|
const {element, connection, sharedProps, target} = await setup();
|
||||||
const contributionColumn = COLUMNS().indexOf("Contribution");
|
const connectionColumn = COLUMNS().indexOf("Connection");
|
||||||
expect(contributionColumn).not.toEqual(-1);
|
expect(connectionColumn).not.toEqual(-1);
|
||||||
const proportion =
|
const proportion =
|
||||||
contribution.contributionScore /
|
connection.connectionScore /
|
||||||
NullUtil.get(sharedProps.pnd.get(target)).score;
|
NullUtil.get(sharedProps.pnd.get(target)).score;
|
||||||
expect(proportion).toBeGreaterThan(0.0);
|
expect(proportion).toBeGreaterThan(0.0);
|
||||||
expect(proportion).toBeLessThan(1.0);
|
expect(proportion).toBeLessThan(1.0);
|
||||||
|
@ -495,25 +491,25 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
expect(
|
expect(
|
||||||
element
|
element
|
||||||
.find("td")
|
.find("td")
|
||||||
.at(contributionColumn)
|
.at(connectionColumn)
|
||||||
.text()
|
.text()
|
||||||
).toEqual(expectedText);
|
).toEqual(expectedText);
|
||||||
});
|
});
|
||||||
it("renders a score column with the source's log-score", async () => {
|
it("renders a score column with the source's log-score", async () => {
|
||||||
const {element, contribution} = await setup();
|
const {element, connection} = await setup();
|
||||||
const expectedScore = (-Math.log(contribution.sourceScore)).toFixed(2);
|
const expectedScore = (-Math.log(connection.sourceScore)).toFixed(2);
|
||||||
const contributionColumn = COLUMNS().indexOf("Score");
|
const connectionColumn = COLUMNS().indexOf("Score");
|
||||||
expect(contributionColumn).not.toEqual(-1);
|
expect(connectionColumn).not.toEqual(-1);
|
||||||
expect(
|
expect(
|
||||||
element
|
element
|
||||||
.find("td")
|
.find("td")
|
||||||
.at(contributionColumn)
|
.at(connectionColumn)
|
||||||
.text()
|
.text()
|
||||||
).toEqual(expectedScore);
|
).toEqual(expectedScore);
|
||||||
});
|
});
|
||||||
it("does not render children by default", async () => {
|
it("does not render children by default", async () => {
|
||||||
const {element} = await setup();
|
const {element} = await setup();
|
||||||
expect(element.find("ContributionRowList")).toHaveLength(0);
|
expect(element.find("ConnectionRowList")).toHaveLength(0);
|
||||||
});
|
});
|
||||||
it('has a working "expand" button', async () => {
|
it('has a working "expand" button', async () => {
|
||||||
const {element, depth, sharedProps, source} = await setup();
|
const {element, depth, sharedProps, source} = await setup();
|
||||||
|
@ -521,7 +517,7 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
|
|
||||||
element.find("button").simulate("click");
|
element.find("button").simulate("click");
|
||||||
expect(element.find("button").text()).toEqual("\u2212");
|
expect(element.find("button").text()).toEqual("\u2212");
|
||||||
const crl = element.find("ContributionRowList");
|
const crl = element.find("ConnectionRowList");
|
||||||
expect(crl).toHaveLength(1);
|
expect(crl).toHaveLength(1);
|
||||||
expect(crl).not.toHaveLength(0);
|
expect(crl).not.toHaveLength(0);
|
||||||
expect(crl.prop("sharedProps")).toEqual(sharedProps);
|
expect(crl.prop("sharedProps")).toEqual(sharedProps);
|
||||||
|
@ -530,75 +526,75 @@ describe("app/credExplorer/PagerankTable", () => {
|
||||||
|
|
||||||
element.find("button").simulate("click");
|
element.find("button").simulate("click");
|
||||||
expect(element.find("button").text()).toEqual("+");
|
expect(element.find("button").text()).toEqual("+");
|
||||||
expect(element.find("ContributionRowList")).toHaveLength(0);
|
expect(element.find("ConnectionRowList")).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ContributionView", () => {
|
describe("ConnectionView", () => {
|
||||||
async function setup() {
|
async function setup() {
|
||||||
const {pnd, adapters, nodes} = await example();
|
const {pnd, adapters, nodes} = await example();
|
||||||
const {scoredContributions} = NullUtil.get(pnd.get(nodes.bar1));
|
const {scoredConnections} = NullUtil.get(pnd.get(nodes.bar1));
|
||||||
const contributions = scoredContributions.map((sc) => sc.contribution);
|
const connections = scoredConnections.map((sc) => sc.connection);
|
||||||
function contributionByType(t) {
|
function connectionByType(t) {
|
||||||
return NullUtil.get(
|
return NullUtil.get(
|
||||||
contributions.filter((c) => c.contributor.type === t)[0],
|
connections.filter((c) => c.adjacency.type === t)[0],
|
||||||
`Couldn't find contribution for type ${t}`
|
`Couldn't find connection for type ${t}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const inContribution = contributionByType("IN_EDGE");
|
const inConnection = connectionByType("IN_EDGE");
|
||||||
const outContribution = contributionByType("OUT_EDGE");
|
const outConnection = connectionByType("OUT_EDGE");
|
||||||
const syntheticContribution = contributionByType("SYNTHETIC_LOOP");
|
const syntheticConnection = connectionByType("SYNTHETIC_LOOP");
|
||||||
function cvForContribution(contribution: Contribution) {
|
function cvForConnection(connection: Connection) {
|
||||||
return shallow(
|
return shallow(
|
||||||
<ContributionView adapters={adapters} contribution={contribution} />
|
<ConnectionView adapters={adapters} connection={connection} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
adapters,
|
adapters,
|
||||||
contributions,
|
connections,
|
||||||
pnd,
|
pnd,
|
||||||
cvForContribution,
|
cvForConnection,
|
||||||
inContribution,
|
inConnection,
|
||||||
outContribution,
|
outConnection,
|
||||||
syntheticContribution,
|
syntheticConnection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
it("always renders exactly one `Badge`", async () => {
|
it("always renders exactly one `Badge`", async () => {
|
||||||
const {
|
const {
|
||||||
cvForContribution,
|
cvForConnection,
|
||||||
inContribution,
|
inConnection,
|
||||||
outContribution,
|
outConnection,
|
||||||
syntheticContribution,
|
syntheticConnection,
|
||||||
} = await setup();
|
} = await setup();
|
||||||
for (const contribution of [
|
for (const connection of [
|
||||||
syntheticContribution,
|
syntheticConnection,
|
||||||
inContribution,
|
inConnection,
|
||||||
outContribution,
|
outConnection,
|
||||||
]) {
|
]) {
|
||||||
expect(cvForContribution(contribution).find("Badge")).toHaveLength(1);
|
expect(cvForConnection(connection).find("Badge")).toHaveLength(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it("for inward contributions, renders a `Badge` and description", async () => {
|
it("for inward connections, renders a `Badge` and description", async () => {
|
||||||
const {cvForContribution, inContribution} = await setup();
|
const {cvForConnection, inConnection} = await setup();
|
||||||
const view = cvForContribution(inContribution);
|
const view = cvForConnection(inConnection);
|
||||||
const outerSpan = view.find("span").first();
|
const outerSpan = view.find("span").first();
|
||||||
const badge = outerSpan.find("Badge");
|
const badge = outerSpan.find("Badge");
|
||||||
const description = outerSpan.children().find("span");
|
const description = outerSpan.children().find("span");
|
||||||
expect(badge.children().text()).toEqual("is barred by");
|
expect(badge.children().text()).toEqual("is barred by");
|
||||||
expect(description.text()).toEqual('bar: NodeAddress["bar","a","1"]');
|
expect(description.text()).toEqual('bar: NodeAddress["bar","a","1"]');
|
||||||
});
|
});
|
||||||
it("for outward contributions, renders a `Badge` and description", async () => {
|
it("for outward connections, renders a `Badge` and description", async () => {
|
||||||
const {cvForContribution, outContribution} = await setup();
|
const {cvForConnection, outConnection} = await setup();
|
||||||
const view = cvForContribution(outContribution);
|
const view = cvForConnection(outConnection);
|
||||||
const outerSpan = view.find("span").first();
|
const outerSpan = view.find("span").first();
|
||||||
const badge = outerSpan.find("Badge");
|
const badge = outerSpan.find("Badge");
|
||||||
const description = outerSpan.children().find("span");
|
const description = outerSpan.children().find("span");
|
||||||
expect(badge.children().text()).toEqual("bars");
|
expect(badge.children().text()).toEqual("bars");
|
||||||
expect(description.text()).toEqual("xox node!");
|
expect(description.text()).toEqual("xox node!");
|
||||||
});
|
});
|
||||||
it("for synthetic contributions, renders only a `Badge`", async () => {
|
it("for synthetic connections, renders only a `Badge`", async () => {
|
||||||
const {cvForContribution, syntheticContribution} = await setup();
|
const {cvForConnection, syntheticConnection} = await setup();
|
||||||
const view = cvForContribution(syntheticContribution);
|
const view = cvForConnection(syntheticConnection);
|
||||||
expect(view.find("span")).toHaveLength(0);
|
expect(view.find("span")).toHaveLength(0);
|
||||||
expect(
|
expect(
|
||||||
view
|
view
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`app/credExplorer/PagerankTable ContributionRow has proper depth-based styling 1`] = `
|
exports[`app/credExplorer/PagerankTable ConnectionRow has proper depth-based styling 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"buttonStyle": Object {
|
"buttonStyle": Object {
|
||||||
"marginLeft": 30,
|
"marginLeft": 30,
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`core/attribution/contributions decompose has the expected output on a simple asymmetric chain 1`] = `
|
exports[`core/attribution/connections decompose has the expected output on a simple asymmetric chain 1`] = `
|
||||||
Map {
|
Map {
|
||||||
"NodeAddress[\\"n1\\"]" => Object {
|
"NodeAddress[\\"n1\\"]" => Object {
|
||||||
"score": 0.19117656878499834,
|
"score": 0.19117656878499834,
|
||||||
"scoredContributions": Array [
|
"scoredConnections": Array [
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e3\\"]",
|
"address": "EdgeAddress[\\"e3\\"]",
|
||||||
"dst": "NodeAddress[\\"sink\\"]",
|
"dst": "NodeAddress[\\"sink\\"]",
|
||||||
|
@ -17,13 +17,13 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.1875,
|
"weight": 0.1875,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.1102941261444197,
|
"connectionScore": 0.1102941261444197,
|
||||||
"source": "NodeAddress[\\"sink\\"]",
|
"source": "NodeAddress[\\"sink\\"]",
|
||||||
"sourceScore": 0.5882353394369051,
|
"sourceScore": 0.5882353394369051,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e1\\"]",
|
"address": "EdgeAddress[\\"e1\\"]",
|
||||||
"dst": "NodeAddress[\\"n2\\"]",
|
"dst": "NodeAddress[\\"n2\\"]",
|
||||||
|
@ -33,18 +33,18 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.3,
|
"weight": 0.3,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.066176427533429,
|
"connectionScore": 0.066176427533429,
|
||||||
"source": "NodeAddress[\\"n2\\"]",
|
"source": "NodeAddress[\\"n2\\"]",
|
||||||
"sourceScore": 0.22058809177809668,
|
"sourceScore": 0.22058809177809668,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"type": "SYNTHETIC_LOOP",
|
"type": "SYNTHETIC_LOOP",
|
||||||
},
|
},
|
||||||
"weight": 0.07692307692307693,
|
"weight": 0.07692307692307693,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.014705889906538334,
|
"connectionScore": 0.014705889906538334,
|
||||||
"source": "NodeAddress[\\"n1\\"]",
|
"source": "NodeAddress[\\"n1\\"]",
|
||||||
"sourceScore": 0.19117656878499834,
|
"sourceScore": 0.19117656878499834,
|
||||||
},
|
},
|
||||||
|
@ -52,10 +52,10 @@ Map {
|
||||||
},
|
},
|
||||||
"NodeAddress[\\"n2\\"]" => Object {
|
"NodeAddress[\\"n2\\"]" => Object {
|
||||||
"score": 0.22058809177809668,
|
"score": 0.22058809177809668,
|
||||||
"scoredContributions": Array [
|
"scoredConnections": Array [
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e2\\"]",
|
"address": "EdgeAddress[\\"e2\\"]",
|
||||||
"dst": "NodeAddress[\\"sink\\"]",
|
"dst": "NodeAddress[\\"sink\\"]",
|
||||||
|
@ -65,13 +65,13 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.1875,
|
"weight": 0.1875,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.1102941261444197,
|
"connectionScore": 0.1102941261444197,
|
||||||
"source": "NodeAddress[\\"sink\\"]",
|
"source": "NodeAddress[\\"sink\\"]",
|
||||||
"sourceScore": 0.5882353394369051,
|
"sourceScore": 0.5882353394369051,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e1\\"]",
|
"address": "EdgeAddress[\\"e1\\"]",
|
||||||
"dst": "NodeAddress[\\"n2\\"]",
|
"dst": "NodeAddress[\\"n2\\"]",
|
||||||
|
@ -81,18 +81,18 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.46153846153846156,
|
"weight": 0.46153846153846156,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.08823533943923001,
|
"connectionScore": 0.08823533943923001,
|
||||||
"source": "NodeAddress[\\"n1\\"]",
|
"source": "NodeAddress[\\"n1\\"]",
|
||||||
"sourceScore": 0.19117656878499834,
|
"sourceScore": 0.19117656878499834,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"type": "SYNTHETIC_LOOP",
|
"type": "SYNTHETIC_LOOP",
|
||||||
},
|
},
|
||||||
"weight": 0.1,
|
"weight": 0.1,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.02205880917780967,
|
"connectionScore": 0.02205880917780967,
|
||||||
"source": "NodeAddress[\\"n2\\"]",
|
"source": "NodeAddress[\\"n2\\"]",
|
||||||
"sourceScore": 0.22058809177809668,
|
"sourceScore": 0.22058809177809668,
|
||||||
},
|
},
|
||||||
|
@ -100,10 +100,10 @@ Map {
|
||||||
},
|
},
|
||||||
"NodeAddress[\\"sink\\"]" => Object {
|
"NodeAddress[\\"sink\\"]" => Object {
|
||||||
"score": 0.5882353394369051,
|
"score": 0.5882353394369051,
|
||||||
"scoredContributions": Array [
|
"scoredConnections": Array [
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e4\\"]",
|
"address": "EdgeAddress[\\"e4\\"]",
|
||||||
"dst": "NodeAddress[\\"sink\\"]",
|
"dst": "NodeAddress[\\"sink\\"]",
|
||||||
|
@ -113,13 +113,13 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.375,
|
"weight": 0.375,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.2205882522888394,
|
"connectionScore": 0.2205882522888394,
|
||||||
"source": "NodeAddress[\\"sink\\"]",
|
"source": "NodeAddress[\\"sink\\"]",
|
||||||
"sourceScore": 0.5882353394369051,
|
"sourceScore": 0.5882353394369051,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e2\\"]",
|
"address": "EdgeAddress[\\"e2\\"]",
|
||||||
"dst": "NodeAddress[\\"sink\\"]",
|
"dst": "NodeAddress[\\"sink\\"]",
|
||||||
|
@ -129,13 +129,13 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.6,
|
"weight": 0.6,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.132352855066858,
|
"connectionScore": 0.132352855066858,
|
||||||
"source": "NodeAddress[\\"n2\\"]",
|
"source": "NodeAddress[\\"n2\\"]",
|
||||||
"sourceScore": 0.22058809177809668,
|
"sourceScore": 0.22058809177809668,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e4\\"]",
|
"address": "EdgeAddress[\\"e4\\"]",
|
||||||
"dst": "NodeAddress[\\"sink\\"]",
|
"dst": "NodeAddress[\\"sink\\"]",
|
||||||
|
@ -145,13 +145,13 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.1875,
|
"weight": 0.1875,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.1102941261444197,
|
"connectionScore": 0.1102941261444197,
|
||||||
"source": "NodeAddress[\\"sink\\"]",
|
"source": "NodeAddress[\\"sink\\"]",
|
||||||
"sourceScore": 0.5882353394369051,
|
"sourceScore": 0.5882353394369051,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"edge": Object {
|
"edge": Object {
|
||||||
"address": "EdgeAddress[\\"e3\\"]",
|
"address": "EdgeAddress[\\"e3\\"]",
|
||||||
"dst": "NodeAddress[\\"sink\\"]",
|
"dst": "NodeAddress[\\"sink\\"]",
|
||||||
|
@ -161,18 +161,18 @@ Map {
|
||||||
},
|
},
|
||||||
"weight": 0.46153846153846156,
|
"weight": 0.46153846153846156,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.08823533943923001,
|
"connectionScore": 0.08823533943923001,
|
||||||
"source": "NodeAddress[\\"n1\\"]",
|
"source": "NodeAddress[\\"n1\\"]",
|
||||||
"sourceScore": 0.19117656878499834,
|
"sourceScore": 0.19117656878499834,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"contribution": Object {
|
"connection": Object {
|
||||||
"contributor": Object {
|
"adjacency": Object {
|
||||||
"type": "SYNTHETIC_LOOP",
|
"type": "SYNTHETIC_LOOP",
|
||||||
},
|
},
|
||||||
"weight": 0.0625,
|
"weight": 0.0625,
|
||||||
},
|
},
|
||||||
"contributionScore": 0.03676470871480657,
|
"connectionScore": 0.03676470871480657,
|
||||||
"source": "NodeAddress[\\"sink\\"]",
|
"source": "NodeAddress[\\"sink\\"]",
|
||||||
"sourceScore": 0.5882353394369051,
|
"sourceScore": 0.5882353394369051,
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,40 +6,34 @@ import * as MapUtil from "../../util/map";
|
||||||
import * as NullUtil from "../../util/null";
|
import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
export type Probability = number;
|
export type Probability = number;
|
||||||
export type Contributor =
|
export type Adjacency =
|
||||||
| {|+type: "SYNTHETIC_LOOP"|}
|
| {|+type: "SYNTHETIC_LOOP"|}
|
||||||
| {|+type: "IN_EDGE", +edge: Edge|}
|
| {|+type: "IN_EDGE", +edge: Edge|}
|
||||||
| {|+type: "OUT_EDGE", +edge: Edge|};
|
| {|+type: "OUT_EDGE", +edge: Edge|};
|
||||||
export type Contribution = {|
|
export type Connection = {|
|
||||||
+contributor: Contributor,
|
+adjacency: Adjacency,
|
||||||
// This `weight` is a conditional probability: given that you're at
|
// This `weight` is a conditional probability: given that you're at
|
||||||
// the source of this contribution's contributor, what's the
|
// the source of this connection's adjacency, what's the
|
||||||
// probability that you travel along this contribution to the target?
|
// probability that you travel along this connection to the target?
|
||||||
+weight: Probability,
|
+weight: Probability,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export function contributorSource(
|
export function adjacencySource(target: NodeAddressT, adjacency: Adjacency) {
|
||||||
target: NodeAddressT,
|
switch (adjacency.type) {
|
||||||
contributor: Contributor
|
|
||||||
) {
|
|
||||||
switch (contributor.type) {
|
|
||||||
case "SYNTHETIC_LOOP":
|
case "SYNTHETIC_LOOP":
|
||||||
return target;
|
return target;
|
||||||
case "IN_EDGE":
|
case "IN_EDGE":
|
||||||
return contributor.edge.src;
|
return adjacency.edge.src;
|
||||||
case "OUT_EDGE":
|
case "OUT_EDGE":
|
||||||
return contributor.edge.dst;
|
return adjacency.edge.dst;
|
||||||
default:
|
default:
|
||||||
throw new Error((contributor.type: empty));
|
throw new Error((adjacency.type: empty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NodeDistribution = Map<NodeAddressT, Probability>;
|
export type NodeDistribution = Map<NodeAddressT, Probability>;
|
||||||
|
|
||||||
export type NodeToContributions = Map<
|
export type NodeToConnections = Map<NodeAddressT, $ReadOnlyArray<Connection>>;
|
||||||
NodeAddressT,
|
|
||||||
$ReadOnlyArray<Contribution>
|
|
||||||
>;
|
|
||||||
|
|
||||||
type NodeAddressMarkovChain = Map<
|
type NodeAddressMarkovChain = Map<
|
||||||
NodeAddressT,
|
NodeAddressT,
|
||||||
|
@ -56,11 +50,11 @@ export type EdgeWeight = {|
|
||||||
+froWeight: number, // weight from dst to src
|
+froWeight: number, // weight from dst to src
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export function createContributions(
|
export function createConnections(
|
||||||
graph: Graph,
|
graph: Graph,
|
||||||
edgeWeight: (Edge) => EdgeWeight,
|
edgeWeight: (Edge) => EdgeWeight,
|
||||||
syntheticLoopWeight: number
|
syntheticLoopWeight: number
|
||||||
): NodeToContributions {
|
): NodeToConnections {
|
||||||
const result = new Map();
|
const result = new Map();
|
||||||
const totalOutWeight: Map<NodeAddressT, number> = new Map();
|
const totalOutWeight: Map<NodeAddressT, number> = new Map();
|
||||||
for (const node of graph.nodes()) {
|
for (const node of graph.nodes()) {
|
||||||
|
@ -68,23 +62,20 @@ export function createContributions(
|
||||||
totalOutWeight.set(node, 0);
|
totalOutWeight.set(node, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function processContribution(
|
function processConnection(target: NodeAddressT, connection: Connection) {
|
||||||
target: NodeAddressT,
|
const connections = NullUtil.get(result.get(target));
|
||||||
contribution: Contribution
|
(((connections: $ReadOnlyArray<Connection>): any): Connection[]).push(
|
||||||
) {
|
connection
|
||||||
const contributions = NullUtil.get(result.get(target));
|
|
||||||
(((contributions: $ReadOnlyArray<Contribution>): any): Contribution[]).push(
|
|
||||||
contribution
|
|
||||||
);
|
);
|
||||||
const source = contributorSource(target, contribution.contributor);
|
const source = adjacencySource(target, connection.adjacency);
|
||||||
const priorOutWeight = NullUtil.get(totalOutWeight.get(source));
|
const priorOutWeight = NullUtil.get(totalOutWeight.get(source));
|
||||||
totalOutWeight.set(source, priorOutWeight + contribution.weight);
|
totalOutWeight.set(source, priorOutWeight + connection.weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add self-loops.
|
// Add self-loops.
|
||||||
for (const node of graph.nodes()) {
|
for (const node of graph.nodes()) {
|
||||||
processContribution(node, {
|
processConnection(node, {
|
||||||
contributor: {type: "SYNTHETIC_LOOP"},
|
adjacency: {type: "SYNTHETIC_LOOP"},
|
||||||
weight: syntheticLoopWeight,
|
weight: syntheticLoopWeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -93,25 +84,25 @@ export function createContributions(
|
||||||
for (const edge of graph.edges()) {
|
for (const edge of graph.edges()) {
|
||||||
const {toWeight, froWeight} = edgeWeight(edge);
|
const {toWeight, froWeight} = edgeWeight(edge);
|
||||||
const {src, dst} = edge;
|
const {src, dst} = edge;
|
||||||
processContribution(dst, {
|
processConnection(dst, {
|
||||||
contributor: {type: "IN_EDGE", edge},
|
adjacency: {type: "IN_EDGE", edge},
|
||||||
weight: toWeight,
|
weight: toWeight,
|
||||||
});
|
});
|
||||||
processContribution(src, {
|
processConnection(src, {
|
||||||
contributor: {type: "OUT_EDGE", edge},
|
adjacency: {type: "OUT_EDGE", edge},
|
||||||
weight: froWeight,
|
weight: froWeight,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize in-weights.
|
// Normalize in-weights.
|
||||||
for (const [target, contributions] of result.entries()) {
|
for (const [target, connections] of result.entries()) {
|
||||||
for (const contribution of contributions) {
|
for (const connection of connections) {
|
||||||
const source = contributorSource(target, contribution.contributor);
|
const source = adjacencySource(target, connection.adjacency);
|
||||||
const normalization = NullUtil.get(totalOutWeight.get(source));
|
const normalization = NullUtil.get(totalOutWeight.get(source));
|
||||||
const newWeight: typeof contribution.weight =
|
const newWeight: typeof connection.weight =
|
||||||
contribution.weight / normalization;
|
connection.weight / normalization;
|
||||||
// (any-cast because property is not writable)
|
// (any-cast because property is not writable)
|
||||||
(contribution: any).weight = newWeight;
|
(connection: any).weight = newWeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,15 +110,15 @@ export function createContributions(
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNodeAddressMarkovChain(
|
function createNodeAddressMarkovChain(
|
||||||
ntc: NodeToContributions
|
ntc: NodeToConnections
|
||||||
): NodeAddressMarkovChain {
|
): NodeAddressMarkovChain {
|
||||||
return MapUtil.mapValues(ntc, (target, contributions) => {
|
return MapUtil.mapValues(ntc, (target, connections) => {
|
||||||
const inNeighbors = new Map();
|
const inNeighbors = new Map();
|
||||||
for (const contribution of contributions) {
|
for (const connection of connections) {
|
||||||
const source = contributorSource(target, contribution.contributor);
|
const source = adjacencySource(target, connection.adjacency);
|
||||||
inNeighbors.set(
|
inNeighbors.set(
|
||||||
source,
|
source,
|
||||||
contribution.weight + NullUtil.orElse(inNeighbors.get(source), 0)
|
connection.weight + NullUtil.orElse(inNeighbors.get(source), 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return inNeighbors;
|
return inNeighbors;
|
||||||
|
@ -163,9 +154,9 @@ function nodeAddressMarkovChainToOrderedSparseMarkovChain(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOrderedSparseMarkovChain(
|
export function createOrderedSparseMarkovChain(
|
||||||
contributions: NodeToContributions
|
connections: NodeToConnections
|
||||||
): OrderedSparseMarkovChain {
|
): OrderedSparseMarkovChain {
|
||||||
const chain = createNodeAddressMarkovChain(contributions);
|
const chain = createNodeAddressMarkovChain(connections);
|
||||||
return nodeAddressMarkovChainToOrderedSparseMarkovChain(chain);
|
return nodeAddressMarkovChainToOrderedSparseMarkovChain(chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import sortBy from "lodash.sortby";
|
||||||
import {EdgeAddress, Graph, NodeAddress} from "../graph";
|
import {EdgeAddress, Graph, NodeAddress} from "../graph";
|
||||||
import {
|
import {
|
||||||
distributionToNodeDistribution,
|
distributionToNodeDistribution,
|
||||||
createContributions,
|
createConnections,
|
||||||
createOrderedSparseMarkovChain,
|
createOrderedSparseMarkovChain,
|
||||||
normalize,
|
normalize,
|
||||||
normalizeNeighbors,
|
normalizeNeighbors,
|
||||||
|
@ -81,9 +81,9 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
expect(actual).toEqual(expected);
|
expect(actual).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createContributions", () => {
|
describe("createConnections", () => {
|
||||||
// The tests for `createOrderedSparseMarkovChain` also must invoke
|
// The tests for `createOrderedSparseMarkovChain` also must invoke
|
||||||
// `createContributions`, so we add only light testing separately.
|
// `createConnections`, so we add only light testing separately.
|
||||||
it("works on a simple asymmetric chain", () => {
|
it("works on a simple asymmetric chain", () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
const n1 = NodeAddress.fromParts(["n1"]);
|
||||||
const n2 = NodeAddress.fromParts(["n2"]);
|
const n2 = NodeAddress.fromParts(["n2"]);
|
||||||
|
@ -101,28 +101,28 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
.addEdge(e3)
|
.addEdge(e3)
|
||||||
.addEdge(e4);
|
.addEdge(e4);
|
||||||
const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0});
|
const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0});
|
||||||
const actual = createContributions(g, edgeWeight, 1.0);
|
const actual = createConnections(g, edgeWeight, 1.0);
|
||||||
// Total out-weights (for normalization factors):
|
// Total out-weights (for normalization factors):
|
||||||
// - for `n1`: 2 out, 0 in, 1 synthetic: 12 + 0 + 1 = 13
|
// - for `n1`: 2 out, 0 in, 1 synthetic: 12 + 0 + 1 = 13
|
||||||
// - for `n2`: 1 out, 1 in, 1 synthetic: 6 + 3 + 1 = 10
|
// - for `n2`: 1 out, 1 in, 1 synthetic: 6 + 3 + 1 = 10
|
||||||
// - for `n3`: 1 out, 3 in, 1 synthetic: 6 + 9 + 1 = 16
|
// - for `n3`: 1 out, 3 in, 1 synthetic: 6 + 9 + 1 = 16
|
||||||
const expected = new Map()
|
const expected = new Map()
|
||||||
.set(n1, [
|
.set(n1, [
|
||||||
{contributor: {type: "SYNTHETIC_LOOP"}, weight: 1 / 13},
|
{adjacency: {type: "SYNTHETIC_LOOP"}, weight: 1 / 13},
|
||||||
{contributor: {type: "OUT_EDGE", edge: e1}, weight: 3 / 10},
|
{adjacency: {type: "OUT_EDGE", edge: e1}, weight: 3 / 10},
|
||||||
{contributor: {type: "OUT_EDGE", edge: e3}, weight: 3 / 16},
|
{adjacency: {type: "OUT_EDGE", edge: e3}, weight: 3 / 16},
|
||||||
])
|
])
|
||||||
.set(n2, [
|
.set(n2, [
|
||||||
{contributor: {type: "SYNTHETIC_LOOP"}, weight: 1 / 10},
|
{adjacency: {type: "SYNTHETIC_LOOP"}, weight: 1 / 10},
|
||||||
{contributor: {type: "IN_EDGE", edge: e1}, weight: 6 / 13},
|
{adjacency: {type: "IN_EDGE", edge: e1}, weight: 6 / 13},
|
||||||
{contributor: {type: "OUT_EDGE", edge: e2}, weight: 3 / 16},
|
{adjacency: {type: "OUT_EDGE", edge: e2}, weight: 3 / 16},
|
||||||
])
|
])
|
||||||
.set(n3, [
|
.set(n3, [
|
||||||
{contributor: {type: "SYNTHETIC_LOOP"}, weight: 1 / 16},
|
{adjacency: {type: "SYNTHETIC_LOOP"}, weight: 1 / 16},
|
||||||
{contributor: {type: "IN_EDGE", edge: e2}, weight: 6 / 10},
|
{adjacency: {type: "IN_EDGE", edge: e2}, weight: 6 / 10},
|
||||||
{contributor: {type: "IN_EDGE", edge: e3}, weight: 6 / 13},
|
{adjacency: {type: "IN_EDGE", edge: e3}, weight: 6 / 13},
|
||||||
{contributor: {type: "IN_EDGE", edge: e4}, weight: 6 / 16},
|
{adjacency: {type: "IN_EDGE", edge: e4}, weight: 6 / 16},
|
||||||
{contributor: {type: "OUT_EDGE", edge: e4}, weight: 3 / 16},
|
{adjacency: {type: "OUT_EDGE", edge: e4}, weight: 3 / 16},
|
||||||
]);
|
]);
|
||||||
const canonicalize = (map) =>
|
const canonicalize = (map) =>
|
||||||
MapUtil.mapValues(map, (_, v) => sortBy(v, (x) => JSON.stringify(x)));
|
MapUtil.mapValues(map, (_, v) => sortBy(v, (x) => JSON.stringify(x)));
|
||||||
|
@ -138,7 +138,7 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
throw new Error("Don't even look at me");
|
throw new Error("Don't even look at me");
|
||||||
};
|
};
|
||||||
const osmc = createOrderedSparseMarkovChain(
|
const osmc = createOrderedSparseMarkovChain(
|
||||||
createContributions(g, edgeWeight, 1e-3)
|
createConnections(g, edgeWeight, 1e-3)
|
||||||
);
|
);
|
||||||
const expected = {
|
const expected = {
|
||||||
nodeOrder: [n],
|
nodeOrder: [n],
|
||||||
|
@ -167,7 +167,7 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
.addEdge(e4);
|
.addEdge(e4);
|
||||||
const edgeWeight = () => ({toWeight: 1, froWeight: 0});
|
const edgeWeight = () => ({toWeight: 1, froWeight: 0});
|
||||||
const osmc = createOrderedSparseMarkovChain(
|
const osmc = createOrderedSparseMarkovChain(
|
||||||
createContributions(g, edgeWeight, 0.0)
|
createConnections(g, edgeWeight, 0.0)
|
||||||
);
|
);
|
||||||
const expected = {
|
const expected = {
|
||||||
nodeOrder: [n1, n2, n3],
|
nodeOrder: [n1, n2, n3],
|
||||||
|
@ -205,7 +205,7 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
.addEdge(e3);
|
.addEdge(e3);
|
||||||
const edgeWeight = () => ({toWeight: 1, froWeight: 1});
|
const edgeWeight = () => ({toWeight: 1, froWeight: 1});
|
||||||
const osmc = createOrderedSparseMarkovChain(
|
const osmc = createOrderedSparseMarkovChain(
|
||||||
createContributions(g, edgeWeight, 0.0)
|
createConnections(g, edgeWeight, 0.0)
|
||||||
);
|
);
|
||||||
const expected = {
|
const expected = {
|
||||||
nodeOrder: [n1, n2, n3],
|
nodeOrder: [n1, n2, n3],
|
||||||
|
@ -237,7 +237,7 @@ describe("core/attribution/graphToMarkovChain", () => {
|
||||||
return {toWeight: 4 - epsilon / 2, froWeight: 1 - epsilon / 2};
|
return {toWeight: 4 - epsilon / 2, froWeight: 1 - epsilon / 2};
|
||||||
}
|
}
|
||||||
const osmc = createOrderedSparseMarkovChain(
|
const osmc = createOrderedSparseMarkovChain(
|
||||||
createContributions(g, edgeWeight, epsilon)
|
createConnections(g, edgeWeight, epsilon)
|
||||||
);
|
);
|
||||||
// Edges from `src`:
|
// Edges from `src`:
|
||||||
// - to `src` with weight `epsilon`
|
// - to `src` with weight `epsilon`
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import {type Edge, Graph} from "../graph";
|
import {type Edge, Graph} from "../graph";
|
||||||
import {
|
import {
|
||||||
distributionToNodeDistribution,
|
distributionToNodeDistribution,
|
||||||
createContributions,
|
createConnections,
|
||||||
createOrderedSparseMarkovChain,
|
createOrderedSparseMarkovChain,
|
||||||
type EdgeWeight,
|
type EdgeWeight,
|
||||||
} from "./graphToMarkovChain";
|
} from "./graphToMarkovChain";
|
||||||
|
@ -44,12 +44,12 @@ export async function pagerank(
|
||||||
...defaultOptions(),
|
...defaultOptions(),
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
};
|
};
|
||||||
const contributions = createContributions(
|
const connections = createConnections(
|
||||||
graph,
|
graph,
|
||||||
edgeWeight,
|
edgeWeight,
|
||||||
fullOptions.selfLoopWeight
|
fullOptions.selfLoopWeight
|
||||||
);
|
);
|
||||||
const osmc = createOrderedSparseMarkovChain(contributions);
|
const osmc = createOrderedSparseMarkovChain(connections);
|
||||||
const distribution = await findStationaryDistribution(osmc.chain, {
|
const distribution = await findStationaryDistribution(osmc.chain, {
|
||||||
verbose: fullOptions.verbose,
|
verbose: fullOptions.verbose,
|
||||||
convergenceThreshold: fullOptions.convergenceThreshold,
|
convergenceThreshold: fullOptions.convergenceThreshold,
|
||||||
|
@ -57,5 +57,5 @@ export async function pagerank(
|
||||||
yieldAfterMs: 30,
|
yieldAfterMs: 30,
|
||||||
});
|
});
|
||||||
const pi = distributionToNodeDistribution(osmc.nodeOrder, distribution);
|
const pi = distributionToNodeDistribution(osmc.nodeOrder, distribution);
|
||||||
return decompose(pi, contributions);
|
return decompose(pi, connections);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,50 +4,50 @@ import sortBy from "lodash.sortby";
|
||||||
|
|
||||||
import type {NodeAddressT} from "../graph";
|
import type {NodeAddressT} from "../graph";
|
||||||
import {
|
import {
|
||||||
type Contribution,
|
type Connection,
|
||||||
type NodeToContributions,
|
type NodeToConnections,
|
||||||
contributorSource,
|
adjacencySource,
|
||||||
} from "./graphToMarkovChain";
|
} from "./graphToMarkovChain";
|
||||||
import type {NodeDistribution} from "./pagerank";
|
import type {NodeDistribution} from "./pagerank";
|
||||||
import * as MapUtil from "../../util/map";
|
import * as MapUtil from "../../util/map";
|
||||||
import * as NullUtil from "../../util/null";
|
import * as NullUtil from "../../util/null";
|
||||||
|
|
||||||
export type ScoredContribution = {|
|
export type ScoredConnection = {|
|
||||||
+contribution: Contribution,
|
+connection: Connection,
|
||||||
+source: NodeAddressT,
|
+source: NodeAddressT,
|
||||||
+sourceScore: number,
|
+sourceScore: number,
|
||||||
+contributionScore: number,
|
+connectionScore: number,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export type PagerankNodeDecomposition = Map<
|
export type PagerankNodeDecomposition = Map<
|
||||||
NodeAddressT,
|
NodeAddressT,
|
||||||
{|
|
{|
|
||||||
+score: number,
|
+score: number,
|
||||||
// Contributions are sorted by `contributorScore` descending,
|
// Connections are sorted by `adjacencyScore` descending,
|
||||||
// breaking ties in a deterministic (but unspecified) order.
|
// breaking ties in a deterministic (but unspecified) order.
|
||||||
+scoredContributions: $ReadOnlyArray<ScoredContribution>,
|
+scoredConnections: $ReadOnlyArray<ScoredConnection>,
|
||||||
|}
|
|}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export function decompose(
|
export function decompose(
|
||||||
pr: NodeDistribution,
|
pr: NodeDistribution,
|
||||||
contributions: NodeToContributions
|
connections: NodeToConnections
|
||||||
): PagerankNodeDecomposition {
|
): PagerankNodeDecomposition {
|
||||||
return MapUtil.mapValues(contributions, (target, contributions) => {
|
return MapUtil.mapValues(connections, (target, connections) => {
|
||||||
const score = NullUtil.get(pr.get(target));
|
const score = NullUtil.get(pr.get(target));
|
||||||
const scoredContributions = sortBy(
|
const scoredConnections = sortBy(
|
||||||
contributions.map(
|
connections.map(
|
||||||
(contribution): ScoredContribution => {
|
(connection): ScoredConnection => {
|
||||||
const source = contributorSource(target, contribution.contributor);
|
const source = adjacencySource(target, connection.adjacency);
|
||||||
const sourceScore = NullUtil.get(pr.get(source));
|
const sourceScore = NullUtil.get(pr.get(source));
|
||||||
const contributionScore = contribution.weight * sourceScore;
|
const connectionScore = connection.weight * sourceScore;
|
||||||
return {contribution, source, sourceScore, contributionScore};
|
return {connection, source, sourceScore, connectionScore};
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
(x) => -x.contributionScore,
|
(x) => -x.connectionScore,
|
||||||
// The following should be called rarely and on small objects.
|
// The following should be called rarely and on small objects.
|
||||||
(x) => JSON.stringify(x.contribution.contributor)
|
(x) => JSON.stringify(x.connection.adjacency)
|
||||||
);
|
);
|
||||||
return {score, scoredContributions};
|
return {score, scoredConnections};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import {EdgeAddress, Graph, NodeAddress, edgeToStrings} from "../graph";
|
import {EdgeAddress, Graph, NodeAddress, edgeToStrings} from "../graph";
|
||||||
import {
|
import {
|
||||||
distributionToNodeDistribution,
|
distributionToNodeDistribution,
|
||||||
createContributions,
|
createConnections,
|
||||||
createOrderedSparseMarkovChain,
|
createOrderedSparseMarkovChain,
|
||||||
} from "./graphToMarkovChain";
|
} from "./graphToMarkovChain";
|
||||||
import {findStationaryDistribution} from "./markovChain";
|
import {findStationaryDistribution} from "./markovChain";
|
||||||
|
@ -17,57 +17,57 @@ import {advancedGraph} from "../graphTestUtil";
|
||||||
* addresses and edges to strings to avoid NUL characters.
|
* addresses and edges to strings to avoid NUL characters.
|
||||||
*/
|
*/
|
||||||
function formatDecomposition(d) {
|
function formatDecomposition(d) {
|
||||||
return MapUtil.mapEntries(d, (key, {score, scoredContributions}) => [
|
return MapUtil.mapEntries(d, (key, {score, scoredConnections}) => [
|
||||||
NodeAddress.toString(key),
|
NodeAddress.toString(key),
|
||||||
{
|
{
|
||||||
score,
|
score,
|
||||||
scoredContributions: scoredContributions.map(
|
scoredConnections: scoredConnections.map(
|
||||||
({contribution, source, sourceScore, contributionScore}) => ({
|
({connection, source, sourceScore, connectionScore}) => ({
|
||||||
contribution: {
|
connection: {
|
||||||
contributor: formatContributor(contribution.contributor),
|
adjacency: formatAdjacency(connection.adjacency),
|
||||||
weight: contribution.weight,
|
weight: connection.weight,
|
||||||
},
|
},
|
||||||
source: NodeAddress.toString(source),
|
source: NodeAddress.toString(source),
|
||||||
sourceScore,
|
sourceScore,
|
||||||
contributionScore,
|
connectionScore,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
function formatContributor(contributor) {
|
function formatAdjacency(adjacency) {
|
||||||
switch (contributor.type) {
|
switch (adjacency.type) {
|
||||||
case "SYNTHETIC_LOOP":
|
case "SYNTHETIC_LOOP":
|
||||||
return {type: "SYNTHETIC_LOOP"};
|
return {type: "SYNTHETIC_LOOP"};
|
||||||
case "IN_EDGE":
|
case "IN_EDGE":
|
||||||
return {type: "IN_EDGE", edge: edgeToStrings(contributor.edge)};
|
return {type: "IN_EDGE", edge: edgeToStrings(adjacency.edge)};
|
||||||
case "OUT_EDGE":
|
case "OUT_EDGE":
|
||||||
return {type: "OUT_EDGE", edge: edgeToStrings(contributor.edge)};
|
return {type: "OUT_EDGE", edge: edgeToStrings(adjacency.edge)};
|
||||||
default:
|
default:
|
||||||
throw new Error((contributor.type: empty));
|
throw new Error((adjacency.type: empty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform basic sanity checks on a decomposition. This ensures that
|
* Perform basic sanity checks on a decomposition. This ensures that
|
||||||
* every node's score is the sum of its contributions' scores, that the
|
* every node's score is the sum of its connections' scores, that the
|
||||||
* scores of the decomposition sum to 1, and that each node's
|
* scores of the decomposition sum to 1, and that each node's
|
||||||
* contributions are listed in non-increasing order of score.
|
* connections are listed in non-increasing order of score.
|
||||||
*/
|
*/
|
||||||
function validateDecomposition(decomposition) {
|
function validateDecomposition(decomposition) {
|
||||||
const epsilon = 1e-6;
|
const epsilon = 1e-6;
|
||||||
|
|
||||||
// Check that each node's score is the sum of its subscores.
|
// Check that each node's score is the sum of its subscores.
|
||||||
for (const [key, {score, scoredContributions}] of decomposition.entries()) {
|
for (const [key, {score, scoredConnections}] of decomposition.entries()) {
|
||||||
const totalSubscore = scoredContributions
|
const totalSubscore = scoredConnections
|
||||||
.map((sc) => sc.contributionScore)
|
.map((sc) => sc.connectionScore)
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
const delta = totalSubscore - score;
|
const delta = totalSubscore - score;
|
||||||
if (Math.abs(delta) > epsilon) {
|
if (Math.abs(delta) > epsilon) {
|
||||||
const message = [
|
const message = [
|
||||||
`for node ${NodeAddress.toString(key)}: `,
|
`for node ${NodeAddress.toString(key)}: `,
|
||||||
`expected total score (${score}) to equal `,
|
`expected total score (${score}) to equal `,
|
||||||
`sum of contribution scores (${totalSubscore}) `,
|
`sum of connection scores (${totalSubscore}) `,
|
||||||
`within ${epsilon}, but the difference is ${delta}`,
|
`within ${epsilon}, but the difference is ${delta}`,
|
||||||
].join("");
|
].join("");
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
|
@ -89,18 +89,18 @@ function validateDecomposition(decomposition) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that each node's contributions are in score-descending order.
|
// Check that each node's connections are in score-descending order.
|
||||||
for (const {scoredContributions} of decomposition.values()) {
|
for (const {scoredConnections} of decomposition.values()) {
|
||||||
scoredContributions.forEach((current, index) => {
|
scoredConnections.forEach((current, index) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const previous = scoredContributions[index - 1];
|
const previous = scoredConnections[index - 1];
|
||||||
if (current.contributionScore > previous.contributionScore) {
|
if (current.connectionScore > previous.connectionScore) {
|
||||||
const message = [
|
const message = [
|
||||||
`expected contribution score to be non-increasing, but `,
|
`expected connection score to be non-increasing, but `,
|
||||||
`element at index ${index} has score ${current.contributionScore}, `,
|
`element at index ${index} has score ${current.connectionScore}, `,
|
||||||
`higher than that of its predecessor (${previous.contributionScore})`,
|
`higher than that of its predecessor (${previous.connectionScore})`,
|
||||||
].join("");
|
].join("");
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ function validateDecomposition(decomposition) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("core/attribution/contributions", () => {
|
describe("core/attribution/connections", () => {
|
||||||
describe("decompose", () => {
|
describe("decompose", () => {
|
||||||
it("has the expected output on a simple asymmetric chain", async () => {
|
it("has the expected output on a simple asymmetric chain", async () => {
|
||||||
const n1 = NodeAddress.fromParts(["n1"]);
|
const n1 = NodeAddress.fromParts(["n1"]);
|
||||||
|
@ -127,8 +127,8 @@ describe("core/attribution/contributions", () => {
|
||||||
.addEdge(e3)
|
.addEdge(e3)
|
||||||
.addEdge(e4);
|
.addEdge(e4);
|
||||||
const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0});
|
const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0});
|
||||||
const contributions = createContributions(g, edgeWeight, 1.0);
|
const connections = createConnections(g, edgeWeight, 1.0);
|
||||||
const osmc = createOrderedSparseMarkovChain(contributions);
|
const osmc = createOrderedSparseMarkovChain(connections);
|
||||||
const pi = await findStationaryDistribution(osmc.chain, {
|
const pi = await findStationaryDistribution(osmc.chain, {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
convergenceThreshold: 1e-6,
|
convergenceThreshold: 1e-6,
|
||||||
|
@ -136,7 +136,7 @@ describe("core/attribution/contributions", () => {
|
||||||
yieldAfterMs: 1,
|
yieldAfterMs: 1,
|
||||||
});
|
});
|
||||||
const pr = distributionToNodeDistribution(osmc.nodeOrder, pi);
|
const pr = distributionToNodeDistribution(osmc.nodeOrder, pi);
|
||||||
const result = decompose(pr, contributions);
|
const result = decompose(pr, connections);
|
||||||
expect(formatDecomposition(result)).toMatchSnapshot();
|
expect(formatDecomposition(result)).toMatchSnapshot();
|
||||||
validateDecomposition(result);
|
validateDecomposition(result);
|
||||||
});
|
});
|
||||||
|
@ -144,8 +144,8 @@ describe("core/attribution/contributions", () => {
|
||||||
it("is valid on the example graph", async () => {
|
it("is valid on the example graph", async () => {
|
||||||
const g = advancedGraph().graph1();
|
const g = advancedGraph().graph1();
|
||||||
const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0});
|
const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0});
|
||||||
const contributions = createContributions(g, edgeWeight, 1.0);
|
const connections = createConnections(g, edgeWeight, 1.0);
|
||||||
const osmc = createOrderedSparseMarkovChain(contributions);
|
const osmc = createOrderedSparseMarkovChain(connections);
|
||||||
const pi = await findStationaryDistribution(osmc.chain, {
|
const pi = await findStationaryDistribution(osmc.chain, {
|
||||||
verbose: false,
|
verbose: false,
|
||||||
convergenceThreshold: 1e-6,
|
convergenceThreshold: 1e-6,
|
||||||
|
@ -153,7 +153,7 @@ describe("core/attribution/contributions", () => {
|
||||||
yieldAfterMs: 1,
|
yieldAfterMs: 1,
|
||||||
});
|
});
|
||||||
const pr = distributionToNodeDistribution(osmc.nodeOrder, pi);
|
const pr = distributionToNodeDistribution(osmc.nodeOrder, pi);
|
||||||
const result = decompose(pr, contributions);
|
const result = decompose(pr, connections);
|
||||||
validateDecomposition(result);
|
validateDecomposition(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue