diff --git a/src/app/credExplorer/PagerankTable.js b/src/app/credExplorer/PagerankTable.js index 55a2786..fbea0c7 100644 --- a/src/app/credExplorer/PagerankTable.js +++ b/src/app/credExplorer/PagerankTable.js @@ -11,9 +11,9 @@ import { } from "../../core/graph"; import type { PagerankNodeDecomposition, - ScoredContribution, + ScoredConnection, } from "../../core/attribution/pagerankNodeDecomposition"; -import type {Contribution} from "../../core/attribution/graphToMarkovChain"; +import type {Connection} from "../../core/attribution/graphToMarkovChain"; import type {DynamicPluginAdapter} from "../pluginAdapter"; import * as NullUtil from "../../util/null"; @@ -164,7 +164,7 @@ export class PagerankTable extends React.PureComponent< Description - Contribution + Connection Score @@ -241,7 +241,7 @@ export class NodeRow extends React.PureComponent { {scoreDisplay(score)} {expanded && ( - { } } -type ContributionRowListProps = {| +type ConnectionRowListProps = {| +depth: number, +node: NodeAddressT, +sharedProps: SharedProps, |}; -export class ContributionRowList extends React.PureComponent< - ContributionRowListProps +export class ConnectionRowList extends React.PureComponent< + ConnectionRowListProps > { render() { const {depth, node, sharedProps} = this.props; const {pnd, maxEntriesPerList} = sharedProps; - const {scoredContributions} = NullUtil.get(pnd.get(node)); + const {scoredConnections} = NullUtil.get(pnd.get(node)); return ( - {scoredContributions + {scoredConnections .slice(0, maxEntriesPerList) .map((sc) => ( - ))} @@ -284,15 +284,15 @@ export class ContributionRowList extends React.PureComponent< } } -type ContributionRowProps = {| +type ConnectionRowProps = {| +depth: number, +target: NodeAddressT, - +scoredContribution: ScoredContribution, + +scoredConnection: ScoredConnection, +sharedProps: SharedProps, |}; -export class ContributionRow extends React.PureComponent< - ContributionRowProps, +export class ConnectionRow extends React.PureComponent< + ConnectionRowProps, RowState > { constructor() { @@ -304,18 +304,13 @@ export class ContributionRow extends React.PureComponent< sharedProps, target, depth, - scoredContribution: { - contribution, - source, - sourceScore, - contributionScore, - }, + scoredConnection: {connection, source, sourceScore, connectionScore}, } = this.props; const {pnd, adapters} = sharedProps; const {expanded} = this.state; const {score: targetScore} = NullUtil.get(pnd.get(target)); - const contributionProportion = contributionScore / targetScore; - const contributionPercent = (contributionProportion * 100).toFixed(2); + const connectionProportion = connectionScore / targetScore; + const connectionPercent = (connectionProportion * 100).toFixed(2); return ( @@ -337,13 +332,13 @@ export class ContributionRow extends React.PureComponent< > {expanded ? "\u2212" : "+"} - + - {contributionPercent}% + {connectionPercent}% {scoreDisplay(sourceScore)} {expanded && ( - , |}> { render() { - const {contribution, adapters} = this.props; + const {connection, adapters} = this.props; function Badge({children}) { return ( // The outer acts as a strut to ensure that the badge @@ -378,30 +373,30 @@ export class ContributionView extends React.PureComponent<{| ); } - const {contributor} = contribution; - switch (contributor.type) { + const {adjacency} = connection; + switch (adjacency.type) { case "SYNTHETIC_LOOP": return synthetic loop; case "IN_EDGE": return ( - {edgeVerb(contributor.edge.address, "BACKWARD", adapters)} + {edgeVerb(adjacency.edge.address, "BACKWARD", adapters)} {" "} - {nodeDescription(contributor.edge.src, adapters)} + {nodeDescription(adjacency.edge.src, adapters)} ); case "OUT_EDGE": return ( - {edgeVerb(contributor.edge.address, "FORWARD", adapters)} + {edgeVerb(adjacency.edge.address, "FORWARD", adapters)} {" "} - {nodeDescription(contributor.edge.dst, adapters)} + {nodeDescription(adjacency.edge.dst, adapters)} ); default: - throw new Error((contributor.type: empty)); + throw new Error((adjacency.type: empty)); } } } diff --git a/src/app/credExplorer/PagerankTable.test.js b/src/app/credExplorer/PagerankTable.test.js index a27cbf2..beeec09 100644 --- a/src/app/credExplorer/PagerankTable.test.js +++ b/src/app/credExplorer/PagerankTable.test.js @@ -8,13 +8,13 @@ import { PagerankTable, NodeRowList, NodeRow, - ContributionRowList, - ContributionRow, - ContributionView, + ConnectionRowList, + ConnectionRow, + ConnectionView, } from "./PagerankTable"; import {pagerank} from "../../core/attribution/pagerank"; 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 { @@ -26,7 +26,7 @@ import { require("../testUtil").configureEnzyme(); -const COLUMNS = () => ["Description", "Contribution", "Score"]; +const COLUMNS = () => ["Description", "Connection", "Score"]; async function example() { const graph = new Graph(); @@ -337,14 +337,14 @@ describe("app/credExplorer/PagerankTable", () => { .text() ).toEqual(expectedDescription); }); - it("renders an empty contribution column", async () => { + it("renders an empty connection column", async () => { const {element} = await setup(); - const contributionColumn = COLUMNS().indexOf("Contribution"); - expect(contributionColumn).not.toEqual(-1); + const connectionColumn = COLUMNS().indexOf("Connection"); + expect(connectionColumn).not.toEqual(-1); expect( element .find("td") - .at(contributionColumn) + .at(connectionColumn) .text() ).toEqual("—"); }); @@ -352,18 +352,18 @@ describe("app/credExplorer/PagerankTable", () => { const {element, sharedProps, node} = await setup(); const {score: rawScore} = NullUtil.get(sharedProps.pnd.get(node)); const expectedScore = (-Math.log(rawScore)).toFixed(2); - const contributionColumn = COLUMNS().indexOf("Score"); - expect(contributionColumn).not.toEqual(-1); + const connectionColumn = COLUMNS().indexOf("Score"); + expect(connectionColumn).not.toEqual(-1); expect( element .find("td") - .at(contributionColumn) + .at(connectionColumn) .text() ).toEqual(expectedScore); }); it("does not render children by default", async () => { const {element} = await setup(); - expect(element.find("ContributionRowList")).toHaveLength(0); + expect(element.find("ConnectionRowList")).toHaveLength(0); }); it('has a working "expand" button', async () => { const {element, sharedProps, node} = await setup(); @@ -371,7 +371,7 @@ describe("app/credExplorer/PagerankTable", () => { element.find("button").simulate("click"); expect(element.find("button").text()).toEqual("\u2212"); - const crl = element.find("ContributionRowList"); + const crl = element.find("ConnectionRowList"); expect(crl).toHaveLength(1); expect(crl.prop("sharedProps")).toEqual(sharedProps); expect(crl.prop("depth")).toBe(1); @@ -379,18 +379,18 @@ describe("app/credExplorer/PagerankTable", () => { element.find("button").simulate("click"); 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) { const {adapters, pnd, nodes} = await example(); const depth = 2; const node = nodes.bar1; const sharedProps = {adapters, pnd, maxEntriesPerList}; const component = ( - { const element = shallow(component); 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 contributions = NullUtil.get(sharedProps.pnd.get(node)) - .scoredContributions; - const rows = element.find("ContributionRow"); - expect(rows).toHaveLength(contributions.length); + const connections = NullUtil.get(sharedProps.pnd.get(node)) + .scoredConnections; + const rows = element.find("ConnectionRow"); + expect(rows).toHaveLength(connections.length); const rowPropses = rows.map((row) => row.props()); // Order should be the same as the order in the decomposition. expect(rowPropses).toEqual( - contributions.map((sc) => ({ + connections.map((sc) => ({ depth, sharedProps, target: node, - scoredContribution: sc, + scoredConnection: sc, })) ); }); it("limits the number of rows by `maxEntriesPerList`", async () => { const maxEntriesPerList = 1; const {element, node, sharedProps} = await setup(maxEntriesPerList); - const contributions = NullUtil.get(sharedProps.pnd.get(node)) - .scoredContributions; - expect(contributions.length).toBeGreaterThan(maxEntriesPerList); - const rows = element.find("ContributionRow"); + const connections = NullUtil.get(sharedProps.pnd.get(node)) + .scoredConnections; + expect(connections.length).toBeGreaterThan(maxEntriesPerList); + const rows = element.find("ConnectionRow"); expect(rows).toHaveLength(maxEntriesPerList); - const rowContributions = rows.map((row) => - row.prop("scoredContribution") - ); + const rowConnections = rows.map((row) => row.prop("scoredConnection")); // Should have selected the right nodes. - expect(rowContributions).toEqual( - contributions.slice(0, maxEntriesPerList) - ); + expect(rowConnections).toEqual(connections.slice(0, maxEntriesPerList)); }); }); - describe("ContributionRow", () => { + describe("ConnectionRow", () => { async function setup() { const {pnd, adapters, nodes} = await example(); const sharedProps = {adapters, pnd, maxEntriesPerList: 123}; const target = nodes.bar1; - const {scoredContributions} = NullUtil.get(pnd.get(target)); - const alphaContributions = scoredContributions.filter( + const {scoredConnections} = NullUtil.get(pnd.get(target)); + const alphaConnections = scoredConnections.filter( (sc) => sc.source === nodes.fooAlpha ); - expect(alphaContributions).toHaveLength(1); - const contribution = alphaContributions[0]; - const {source} = contribution; + expect(alphaConnections).toHaveLength(1); + const connection = alphaConnections[0]; + const {source} = connection; const depth = 2; const 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 () => { expect((await setup()).element.find("td")).toHaveLength(COLUMNS().length); @@ -469,25 +465,25 @@ describe("app/credExplorer/PagerankTable", () => { }).toMatchSnapshot(); }); it("renders the source view", async () => { - const {element, sharedProps, contribution} = await setup(); + const {element, sharedProps, connection} = await setup(); const descriptionColumn = COLUMNS().indexOf("Description"); expect(descriptionColumn).not.toEqual(-1); const view = element .find("td") .at(descriptionColumn) - .find("ContributionView"); + .find("ConnectionView"); expect(view).toHaveLength(1); expect(view.props()).toEqual({ adapters: sharedProps.adapters, - contribution: contribution.contribution, + connection: connection.connection, }); }); - it("renders the contribution percentage", async () => { - const {element, contribution, sharedProps, target} = await setup(); - const contributionColumn = COLUMNS().indexOf("Contribution"); - expect(contributionColumn).not.toEqual(-1); + it("renders the connection percentage", async () => { + const {element, connection, sharedProps, target} = await setup(); + const connectionColumn = COLUMNS().indexOf("Connection"); + expect(connectionColumn).not.toEqual(-1); const proportion = - contribution.contributionScore / + connection.connectionScore / NullUtil.get(sharedProps.pnd.get(target)).score; expect(proportion).toBeGreaterThan(0.0); expect(proportion).toBeLessThan(1.0); @@ -495,25 +491,25 @@ describe("app/credExplorer/PagerankTable", () => { expect( element .find("td") - .at(contributionColumn) + .at(connectionColumn) .text() ).toEqual(expectedText); }); it("renders a score column with the source's log-score", async () => { - const {element, contribution} = await setup(); - const expectedScore = (-Math.log(contribution.sourceScore)).toFixed(2); - const contributionColumn = COLUMNS().indexOf("Score"); - expect(contributionColumn).not.toEqual(-1); + const {element, connection} = await setup(); + const expectedScore = (-Math.log(connection.sourceScore)).toFixed(2); + const connectionColumn = COLUMNS().indexOf("Score"); + expect(connectionColumn).not.toEqual(-1); expect( element .find("td") - .at(contributionColumn) + .at(connectionColumn) .text() ).toEqual(expectedScore); }); it("does not render children by default", async () => { const {element} = await setup(); - expect(element.find("ContributionRowList")).toHaveLength(0); + expect(element.find("ConnectionRowList")).toHaveLength(0); }); it('has a working "expand" button', async () => { const {element, depth, sharedProps, source} = await setup(); @@ -521,7 +517,7 @@ describe("app/credExplorer/PagerankTable", () => { element.find("button").simulate("click"); expect(element.find("button").text()).toEqual("\u2212"); - const crl = element.find("ContributionRowList"); + const crl = element.find("ConnectionRowList"); expect(crl).toHaveLength(1); expect(crl).not.toHaveLength(0); expect(crl.prop("sharedProps")).toEqual(sharedProps); @@ -530,75 +526,75 @@ describe("app/credExplorer/PagerankTable", () => { element.find("button").simulate("click"); 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() { const {pnd, adapters, nodes} = await example(); - const {scoredContributions} = NullUtil.get(pnd.get(nodes.bar1)); - const contributions = scoredContributions.map((sc) => sc.contribution); - function contributionByType(t) { + const {scoredConnections} = NullUtil.get(pnd.get(nodes.bar1)); + const connections = scoredConnections.map((sc) => sc.connection); + function connectionByType(t) { return NullUtil.get( - contributions.filter((c) => c.contributor.type === t)[0], - `Couldn't find contribution for type ${t}` + connections.filter((c) => c.adjacency.type === t)[0], + `Couldn't find connection for type ${t}` ); } - const inContribution = contributionByType("IN_EDGE"); - const outContribution = contributionByType("OUT_EDGE"); - const syntheticContribution = contributionByType("SYNTHETIC_LOOP"); - function cvForContribution(contribution: Contribution) { + const inConnection = connectionByType("IN_EDGE"); + const outConnection = connectionByType("OUT_EDGE"); + const syntheticConnection = connectionByType("SYNTHETIC_LOOP"); + function cvForConnection(connection: Connection) { return shallow( - + ); } return { adapters, - contributions, + connections, pnd, - cvForContribution, - inContribution, - outContribution, - syntheticContribution, + cvForConnection, + inConnection, + outConnection, + syntheticConnection, }; } it("always renders exactly one `Badge`", async () => { const { - cvForContribution, - inContribution, - outContribution, - syntheticContribution, + cvForConnection, + inConnection, + outConnection, + syntheticConnection, } = await setup(); - for (const contribution of [ - syntheticContribution, - inContribution, - outContribution, + for (const connection of [ + syntheticConnection, + inConnection, + 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 () => { - const {cvForContribution, inContribution} = await setup(); - const view = cvForContribution(inContribution); + it("for inward connections, renders a `Badge` and description", async () => { + const {cvForConnection, inConnection} = await setup(); + const view = cvForConnection(inConnection); const outerSpan = view.find("span").first(); const badge = outerSpan.find("Badge"); const description = outerSpan.children().find("span"); expect(badge.children().text()).toEqual("is barred by"); expect(description.text()).toEqual('bar: NodeAddress["bar","a","1"]'); }); - it("for outward contributions, renders a `Badge` and description", async () => { - const {cvForContribution, outContribution} = await setup(); - const view = cvForContribution(outContribution); + it("for outward connections, renders a `Badge` and description", async () => { + const {cvForConnection, outConnection} = await setup(); + const view = cvForConnection(outConnection); const outerSpan = view.find("span").first(); const badge = outerSpan.find("Badge"); const description = outerSpan.children().find("span"); expect(badge.children().text()).toEqual("bars"); expect(description.text()).toEqual("xox node!"); }); - it("for synthetic contributions, renders only a `Badge`", async () => { - const {cvForContribution, syntheticContribution} = await setup(); - const view = cvForContribution(syntheticContribution); + it("for synthetic connections, renders only a `Badge`", async () => { + const {cvForConnection, syntheticConnection} = await setup(); + const view = cvForConnection(syntheticConnection); expect(view.find("span")).toHaveLength(0); expect( view diff --git a/src/app/credExplorer/__snapshots__/PagerankTable.test.js.snap b/src/app/credExplorer/__snapshots__/PagerankTable.test.js.snap index 5f59191..38bcd59 100644 --- a/src/app/credExplorer/__snapshots__/PagerankTable.test.js.snap +++ b/src/app/credExplorer/__snapshots__/PagerankTable.test.js.snap @@ -1,6 +1,6 @@ // 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 { "buttonStyle": Object { "marginLeft": 30, diff --git a/src/core/attribution/__snapshots__/pagerankNodeDecomposition.test.js.snap b/src/core/attribution/__snapshots__/pagerankNodeDecomposition.test.js.snap index 8e1b848..be85fb5 100644 --- a/src/core/attribution/__snapshots__/pagerankNodeDecomposition.test.js.snap +++ b/src/core/attribution/__snapshots__/pagerankNodeDecomposition.test.js.snap @@ -1,13 +1,13 @@ // 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 { "NodeAddress[\\"n1\\"]" => Object { "score": 0.19117656878499834, - "scoredContributions": Array [ + "scoredConnections": Array [ Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e3\\"]", "dst": "NodeAddress[\\"sink\\"]", @@ -17,13 +17,13 @@ Map { }, "weight": 0.1875, }, - "contributionScore": 0.1102941261444197, + "connectionScore": 0.1102941261444197, "source": "NodeAddress[\\"sink\\"]", "sourceScore": 0.5882353394369051, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e1\\"]", "dst": "NodeAddress[\\"n2\\"]", @@ -33,18 +33,18 @@ Map { }, "weight": 0.3, }, - "contributionScore": 0.066176427533429, + "connectionScore": 0.066176427533429, "source": "NodeAddress[\\"n2\\"]", "sourceScore": 0.22058809177809668, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "type": "SYNTHETIC_LOOP", }, "weight": 0.07692307692307693, }, - "contributionScore": 0.014705889906538334, + "connectionScore": 0.014705889906538334, "source": "NodeAddress[\\"n1\\"]", "sourceScore": 0.19117656878499834, }, @@ -52,10 +52,10 @@ Map { }, "NodeAddress[\\"n2\\"]" => Object { "score": 0.22058809177809668, - "scoredContributions": Array [ + "scoredConnections": Array [ Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e2\\"]", "dst": "NodeAddress[\\"sink\\"]", @@ -65,13 +65,13 @@ Map { }, "weight": 0.1875, }, - "contributionScore": 0.1102941261444197, + "connectionScore": 0.1102941261444197, "source": "NodeAddress[\\"sink\\"]", "sourceScore": 0.5882353394369051, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e1\\"]", "dst": "NodeAddress[\\"n2\\"]", @@ -81,18 +81,18 @@ Map { }, "weight": 0.46153846153846156, }, - "contributionScore": 0.08823533943923001, + "connectionScore": 0.08823533943923001, "source": "NodeAddress[\\"n1\\"]", "sourceScore": 0.19117656878499834, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "type": "SYNTHETIC_LOOP", }, "weight": 0.1, }, - "contributionScore": 0.02205880917780967, + "connectionScore": 0.02205880917780967, "source": "NodeAddress[\\"n2\\"]", "sourceScore": 0.22058809177809668, }, @@ -100,10 +100,10 @@ Map { }, "NodeAddress[\\"sink\\"]" => Object { "score": 0.5882353394369051, - "scoredContributions": Array [ + "scoredConnections": Array [ Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e4\\"]", "dst": "NodeAddress[\\"sink\\"]", @@ -113,13 +113,13 @@ Map { }, "weight": 0.375, }, - "contributionScore": 0.2205882522888394, + "connectionScore": 0.2205882522888394, "source": "NodeAddress[\\"sink\\"]", "sourceScore": 0.5882353394369051, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e2\\"]", "dst": "NodeAddress[\\"sink\\"]", @@ -129,13 +129,13 @@ Map { }, "weight": 0.6, }, - "contributionScore": 0.132352855066858, + "connectionScore": 0.132352855066858, "source": "NodeAddress[\\"n2\\"]", "sourceScore": 0.22058809177809668, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e4\\"]", "dst": "NodeAddress[\\"sink\\"]", @@ -145,13 +145,13 @@ Map { }, "weight": 0.1875, }, - "contributionScore": 0.1102941261444197, + "connectionScore": 0.1102941261444197, "source": "NodeAddress[\\"sink\\"]", "sourceScore": 0.5882353394369051, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "edge": Object { "address": "EdgeAddress[\\"e3\\"]", "dst": "NodeAddress[\\"sink\\"]", @@ -161,18 +161,18 @@ Map { }, "weight": 0.46153846153846156, }, - "contributionScore": 0.08823533943923001, + "connectionScore": 0.08823533943923001, "source": "NodeAddress[\\"n1\\"]", "sourceScore": 0.19117656878499834, }, Object { - "contribution": Object { - "contributor": Object { + "connection": Object { + "adjacency": Object { "type": "SYNTHETIC_LOOP", }, "weight": 0.0625, }, - "contributionScore": 0.03676470871480657, + "connectionScore": 0.03676470871480657, "source": "NodeAddress[\\"sink\\"]", "sourceScore": 0.5882353394369051, }, diff --git a/src/core/attribution/graphToMarkovChain.js b/src/core/attribution/graphToMarkovChain.js index 8756ec2..3788adc 100644 --- a/src/core/attribution/graphToMarkovChain.js +++ b/src/core/attribution/graphToMarkovChain.js @@ -6,40 +6,34 @@ import * as MapUtil from "../../util/map"; import * as NullUtil from "../../util/null"; export type Probability = number; -export type Contributor = +export type Adjacency = | {|+type: "SYNTHETIC_LOOP"|} | {|+type: "IN_EDGE", +edge: Edge|} | {|+type: "OUT_EDGE", +edge: Edge|}; -export type Contribution = {| - +contributor: Contributor, +export type Connection = {| + +adjacency: Adjacency, // This `weight` is a conditional probability: given that you're at - // the source of this contribution's contributor, what's the - // probability that you travel along this contribution to the target? + // the source of this connection's adjacency, what's the + // probability that you travel along this connection to the target? +weight: Probability, |}; -export function contributorSource( - target: NodeAddressT, - contributor: Contributor -) { - switch (contributor.type) { +export function adjacencySource(target: NodeAddressT, adjacency: Adjacency) { + switch (adjacency.type) { case "SYNTHETIC_LOOP": return target; case "IN_EDGE": - return contributor.edge.src; + return adjacency.edge.src; case "OUT_EDGE": - return contributor.edge.dst; + return adjacency.edge.dst; default: - throw new Error((contributor.type: empty)); + throw new Error((adjacency.type: empty)); } } export type NodeDistribution = Map; -export type NodeToContributions = Map< - NodeAddressT, - $ReadOnlyArray ->; +export type NodeToConnections = Map>; type NodeAddressMarkovChain = Map< NodeAddressT, @@ -56,11 +50,11 @@ export type EdgeWeight = {| +froWeight: number, // weight from dst to src |}; -export function createContributions( +export function createConnections( graph: Graph, edgeWeight: (Edge) => EdgeWeight, syntheticLoopWeight: number -): NodeToContributions { +): NodeToConnections { const result = new Map(); const totalOutWeight: Map = new Map(); for (const node of graph.nodes()) { @@ -68,23 +62,20 @@ export function createContributions( totalOutWeight.set(node, 0); } - function processContribution( - target: NodeAddressT, - contribution: Contribution - ) { - const contributions = NullUtil.get(result.get(target)); - (((contributions: $ReadOnlyArray): any): Contribution[]).push( - contribution + function processConnection(target: NodeAddressT, connection: Connection) { + const connections = NullUtil.get(result.get(target)); + (((connections: $ReadOnlyArray): any): Connection[]).push( + connection ); - const source = contributorSource(target, contribution.contributor); + const source = adjacencySource(target, connection.adjacency); const priorOutWeight = NullUtil.get(totalOutWeight.get(source)); - totalOutWeight.set(source, priorOutWeight + contribution.weight); + totalOutWeight.set(source, priorOutWeight + connection.weight); } // Add self-loops. for (const node of graph.nodes()) { - processContribution(node, { - contributor: {type: "SYNTHETIC_LOOP"}, + processConnection(node, { + adjacency: {type: "SYNTHETIC_LOOP"}, weight: syntheticLoopWeight, }); } @@ -93,25 +84,25 @@ export function createContributions( for (const edge of graph.edges()) { const {toWeight, froWeight} = edgeWeight(edge); const {src, dst} = edge; - processContribution(dst, { - contributor: {type: "IN_EDGE", edge}, + processConnection(dst, { + adjacency: {type: "IN_EDGE", edge}, weight: toWeight, }); - processContribution(src, { - contributor: {type: "OUT_EDGE", edge}, + processConnection(src, { + adjacency: {type: "OUT_EDGE", edge}, weight: froWeight, }); } // Normalize in-weights. - for (const [target, contributions] of result.entries()) { - for (const contribution of contributions) { - const source = contributorSource(target, contribution.contributor); + for (const [target, connections] of result.entries()) { + for (const connection of connections) { + const source = adjacencySource(target, connection.adjacency); const normalization = NullUtil.get(totalOutWeight.get(source)); - const newWeight: typeof contribution.weight = - contribution.weight / normalization; + const newWeight: typeof connection.weight = + connection.weight / normalization; // (any-cast because property is not writable) - (contribution: any).weight = newWeight; + (connection: any).weight = newWeight; } } @@ -119,15 +110,15 @@ export function createContributions( } function createNodeAddressMarkovChain( - ntc: NodeToContributions + ntc: NodeToConnections ): NodeAddressMarkovChain { - return MapUtil.mapValues(ntc, (target, contributions) => { + return MapUtil.mapValues(ntc, (target, connections) => { const inNeighbors = new Map(); - for (const contribution of contributions) { - const source = contributorSource(target, contribution.contributor); + for (const connection of connections) { + const source = adjacencySource(target, connection.adjacency); inNeighbors.set( source, - contribution.weight + NullUtil.orElse(inNeighbors.get(source), 0) + connection.weight + NullUtil.orElse(inNeighbors.get(source), 0) ); } return inNeighbors; @@ -163,9 +154,9 @@ function nodeAddressMarkovChainToOrderedSparseMarkovChain( } export function createOrderedSparseMarkovChain( - contributions: NodeToContributions + connections: NodeToConnections ): OrderedSparseMarkovChain { - const chain = createNodeAddressMarkovChain(contributions); + const chain = createNodeAddressMarkovChain(connections); return nodeAddressMarkovChainToOrderedSparseMarkovChain(chain); } diff --git a/src/core/attribution/graphToMarkovChain.test.js b/src/core/attribution/graphToMarkovChain.test.js index 59f122f..d5ffc03 100644 --- a/src/core/attribution/graphToMarkovChain.test.js +++ b/src/core/attribution/graphToMarkovChain.test.js @@ -5,7 +5,7 @@ import sortBy from "lodash.sortby"; import {EdgeAddress, Graph, NodeAddress} from "../graph"; import { distributionToNodeDistribution, - createContributions, + createConnections, createOrderedSparseMarkovChain, normalize, normalizeNeighbors, @@ -81,9 +81,9 @@ describe("core/attribution/graphToMarkovChain", () => { expect(actual).toEqual(expected); }); - describe("createContributions", () => { + describe("createConnections", () => { // 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", () => { const n1 = NodeAddress.fromParts(["n1"]); const n2 = NodeAddress.fromParts(["n2"]); @@ -101,28 +101,28 @@ describe("core/attribution/graphToMarkovChain", () => { .addEdge(e3) .addEdge(e4); 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): // - 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 `n3`: 1 out, 3 in, 1 synthetic: 6 + 9 + 1 = 16 const expected = new Map() .set(n1, [ - {contributor: {type: "SYNTHETIC_LOOP"}, weight: 1 / 13}, - {contributor: {type: "OUT_EDGE", edge: e1}, weight: 3 / 10}, - {contributor: {type: "OUT_EDGE", edge: e3}, weight: 3 / 16}, + {adjacency: {type: "SYNTHETIC_LOOP"}, weight: 1 / 13}, + {adjacency: {type: "OUT_EDGE", edge: e1}, weight: 3 / 10}, + {adjacency: {type: "OUT_EDGE", edge: e3}, weight: 3 / 16}, ]) .set(n2, [ - {contributor: {type: "SYNTHETIC_LOOP"}, weight: 1 / 10}, - {contributor: {type: "IN_EDGE", edge: e1}, weight: 6 / 13}, - {contributor: {type: "OUT_EDGE", edge: e2}, weight: 3 / 16}, + {adjacency: {type: "SYNTHETIC_LOOP"}, weight: 1 / 10}, + {adjacency: {type: "IN_EDGE", edge: e1}, weight: 6 / 13}, + {adjacency: {type: "OUT_EDGE", edge: e2}, weight: 3 / 16}, ]) .set(n3, [ - {contributor: {type: "SYNTHETIC_LOOP"}, weight: 1 / 16}, - {contributor: {type: "IN_EDGE", edge: e2}, weight: 6 / 10}, - {contributor: {type: "IN_EDGE", edge: e3}, weight: 6 / 13}, - {contributor: {type: "IN_EDGE", edge: e4}, weight: 6 / 16}, - {contributor: {type: "OUT_EDGE", edge: e4}, weight: 3 / 16}, + {adjacency: {type: "SYNTHETIC_LOOP"}, weight: 1 / 16}, + {adjacency: {type: "IN_EDGE", edge: e2}, weight: 6 / 10}, + {adjacency: {type: "IN_EDGE", edge: e3}, weight: 6 / 13}, + {adjacency: {type: "IN_EDGE", edge: e4}, weight: 6 / 16}, + {adjacency: {type: "OUT_EDGE", edge: e4}, weight: 3 / 16}, ]); const canonicalize = (map) => 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"); }; const osmc = createOrderedSparseMarkovChain( - createContributions(g, edgeWeight, 1e-3) + createConnections(g, edgeWeight, 1e-3) ); const expected = { nodeOrder: [n], @@ -167,7 +167,7 @@ describe("core/attribution/graphToMarkovChain", () => { .addEdge(e4); const edgeWeight = () => ({toWeight: 1, froWeight: 0}); const osmc = createOrderedSparseMarkovChain( - createContributions(g, edgeWeight, 0.0) + createConnections(g, edgeWeight, 0.0) ); const expected = { nodeOrder: [n1, n2, n3], @@ -205,7 +205,7 @@ describe("core/attribution/graphToMarkovChain", () => { .addEdge(e3); const edgeWeight = () => ({toWeight: 1, froWeight: 1}); const osmc = createOrderedSparseMarkovChain( - createContributions(g, edgeWeight, 0.0) + createConnections(g, edgeWeight, 0.0) ); const expected = { nodeOrder: [n1, n2, n3], @@ -237,7 +237,7 @@ describe("core/attribution/graphToMarkovChain", () => { return {toWeight: 4 - epsilon / 2, froWeight: 1 - epsilon / 2}; } const osmc = createOrderedSparseMarkovChain( - createContributions(g, edgeWeight, epsilon) + createConnections(g, edgeWeight, epsilon) ); // Edges from `src`: // - to `src` with weight `epsilon` diff --git a/src/core/attribution/pagerank.js b/src/core/attribution/pagerank.js index 3cdd797..278584e 100644 --- a/src/core/attribution/pagerank.js +++ b/src/core/attribution/pagerank.js @@ -3,7 +3,7 @@ import {type Edge, Graph} from "../graph"; import { distributionToNodeDistribution, - createContributions, + createConnections, createOrderedSparseMarkovChain, type EdgeWeight, } from "./graphToMarkovChain"; @@ -44,12 +44,12 @@ export async function pagerank( ...defaultOptions(), ...(options || {}), }; - const contributions = createContributions( + const connections = createConnections( graph, edgeWeight, fullOptions.selfLoopWeight ); - const osmc = createOrderedSparseMarkovChain(contributions); + const osmc = createOrderedSparseMarkovChain(connections); const distribution = await findStationaryDistribution(osmc.chain, { verbose: fullOptions.verbose, convergenceThreshold: fullOptions.convergenceThreshold, @@ -57,5 +57,5 @@ export async function pagerank( yieldAfterMs: 30, }); const pi = distributionToNodeDistribution(osmc.nodeOrder, distribution); - return decompose(pi, contributions); + return decompose(pi, connections); } diff --git a/src/core/attribution/pagerankNodeDecomposition.js b/src/core/attribution/pagerankNodeDecomposition.js index d37883f..03f67b1 100644 --- a/src/core/attribution/pagerankNodeDecomposition.js +++ b/src/core/attribution/pagerankNodeDecomposition.js @@ -4,50 +4,50 @@ import sortBy from "lodash.sortby"; import type {NodeAddressT} from "../graph"; import { - type Contribution, - type NodeToContributions, - contributorSource, + type Connection, + type NodeToConnections, + adjacencySource, } from "./graphToMarkovChain"; import type {NodeDistribution} from "./pagerank"; import * as MapUtil from "../../util/map"; import * as NullUtil from "../../util/null"; -export type ScoredContribution = {| - +contribution: Contribution, +export type ScoredConnection = {| + +connection: Connection, +source: NodeAddressT, +sourceScore: number, - +contributionScore: number, + +connectionScore: number, |}; export type PagerankNodeDecomposition = Map< NodeAddressT, {| +score: number, - // Contributions are sorted by `contributorScore` descending, + // Connections are sorted by `adjacencyScore` descending, // breaking ties in a deterministic (but unspecified) order. - +scoredContributions: $ReadOnlyArray, + +scoredConnections: $ReadOnlyArray, |} >; export function decompose( pr: NodeDistribution, - contributions: NodeToContributions + connections: NodeToConnections ): PagerankNodeDecomposition { - return MapUtil.mapValues(contributions, (target, contributions) => { + return MapUtil.mapValues(connections, (target, connections) => { const score = NullUtil.get(pr.get(target)); - const scoredContributions = sortBy( - contributions.map( - (contribution): ScoredContribution => { - const source = contributorSource(target, contribution.contributor); + const scoredConnections = sortBy( + connections.map( + (connection): ScoredConnection => { + const source = adjacencySource(target, connection.adjacency); const sourceScore = NullUtil.get(pr.get(source)); - const contributionScore = contribution.weight * sourceScore; - return {contribution, source, sourceScore, contributionScore}; + const connectionScore = connection.weight * sourceScore; + return {connection, source, sourceScore, connectionScore}; } ), - (x) => -x.contributionScore, + (x) => -x.connectionScore, // 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}; }); } diff --git a/src/core/attribution/pagerankNodeDecomposition.test.js b/src/core/attribution/pagerankNodeDecomposition.test.js index 71e7bb2..cab0d1a 100644 --- a/src/core/attribution/pagerankNodeDecomposition.test.js +++ b/src/core/attribution/pagerankNodeDecomposition.test.js @@ -3,7 +3,7 @@ import {EdgeAddress, Graph, NodeAddress, edgeToStrings} from "../graph"; import { distributionToNodeDistribution, - createContributions, + createConnections, createOrderedSparseMarkovChain, } from "./graphToMarkovChain"; import {findStationaryDistribution} from "./markovChain"; @@ -17,57 +17,57 @@ import {advancedGraph} from "../graphTestUtil"; * addresses and edges to strings to avoid NUL characters. */ function formatDecomposition(d) { - return MapUtil.mapEntries(d, (key, {score, scoredContributions}) => [ + return MapUtil.mapEntries(d, (key, {score, scoredConnections}) => [ NodeAddress.toString(key), { score, - scoredContributions: scoredContributions.map( - ({contribution, source, sourceScore, contributionScore}) => ({ - contribution: { - contributor: formatContributor(contribution.contributor), - weight: contribution.weight, + scoredConnections: scoredConnections.map( + ({connection, source, sourceScore, connectionScore}) => ({ + connection: { + adjacency: formatAdjacency(connection.adjacency), + weight: connection.weight, }, source: NodeAddress.toString(source), sourceScore, - contributionScore, + connectionScore, }) ), }, ]); - function formatContributor(contributor) { - switch (contributor.type) { + function formatAdjacency(adjacency) { + switch (adjacency.type) { case "SYNTHETIC_LOOP": return {type: "SYNTHETIC_LOOP"}; case "IN_EDGE": - return {type: "IN_EDGE", edge: edgeToStrings(contributor.edge)}; + return {type: "IN_EDGE", edge: edgeToStrings(adjacency.edge)}; case "OUT_EDGE": - return {type: "OUT_EDGE", edge: edgeToStrings(contributor.edge)}; + return {type: "OUT_EDGE", edge: edgeToStrings(adjacency.edge)}; default: - throw new Error((contributor.type: empty)); + throw new Error((adjacency.type: empty)); } } } /** * 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 - * contributions are listed in non-increasing order of score. + * connections are listed in non-increasing order of score. */ function validateDecomposition(decomposition) { const epsilon = 1e-6; // Check that each node's score is the sum of its subscores. - for (const [key, {score, scoredContributions}] of decomposition.entries()) { - const totalSubscore = scoredContributions - .map((sc) => sc.contributionScore) + for (const [key, {score, scoredConnections}] of decomposition.entries()) { + const totalSubscore = scoredConnections + .map((sc) => sc.connectionScore) .reduce((a, b) => a + b, 0); const delta = totalSubscore - score; if (Math.abs(delta) > epsilon) { const message = [ `for node ${NodeAddress.toString(key)}: `, `expected total score (${score}) to equal `, - `sum of contribution scores (${totalSubscore}) `, + `sum of connection scores (${totalSubscore}) `, `within ${epsilon}, but the difference is ${delta}`, ].join(""); throw new Error(message); @@ -89,18 +89,18 @@ function validateDecomposition(decomposition) { } } - // Check that each node's contributions are in score-descending order. - for (const {scoredContributions} of decomposition.values()) { - scoredContributions.forEach((current, index) => { + // Check that each node's connections are in score-descending order. + for (const {scoredConnections} of decomposition.values()) { + scoredConnections.forEach((current, index) => { if (index === 0) { return; } - const previous = scoredContributions[index - 1]; - if (current.contributionScore > previous.contributionScore) { + const previous = scoredConnections[index - 1]; + if (current.connectionScore > previous.connectionScore) { const message = [ - `expected contribution score to be non-increasing, but `, - `element at index ${index} has score ${current.contributionScore}, `, - `higher than that of its predecessor (${previous.contributionScore})`, + `expected connection score to be non-increasing, but `, + `element at index ${index} has score ${current.connectionScore}, `, + `higher than that of its predecessor (${previous.connectionScore})`, ].join(""); throw new Error(message); } @@ -108,7 +108,7 @@ function validateDecomposition(decomposition) { } } -describe("core/attribution/contributions", () => { +describe("core/attribution/connections", () => { describe("decompose", () => { it("has the expected output on a simple asymmetric chain", async () => { const n1 = NodeAddress.fromParts(["n1"]); @@ -127,8 +127,8 @@ describe("core/attribution/contributions", () => { .addEdge(e3) .addEdge(e4); const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0}); - const contributions = createContributions(g, edgeWeight, 1.0); - const osmc = createOrderedSparseMarkovChain(contributions); + const connections = createConnections(g, edgeWeight, 1.0); + const osmc = createOrderedSparseMarkovChain(connections); const pi = await findStationaryDistribution(osmc.chain, { verbose: false, convergenceThreshold: 1e-6, @@ -136,7 +136,7 @@ describe("core/attribution/contributions", () => { yieldAfterMs: 1, }); const pr = distributionToNodeDistribution(osmc.nodeOrder, pi); - const result = decompose(pr, contributions); + const result = decompose(pr, connections); expect(formatDecomposition(result)).toMatchSnapshot(); validateDecomposition(result); }); @@ -144,8 +144,8 @@ describe("core/attribution/contributions", () => { it("is valid on the example graph", async () => { const g = advancedGraph().graph1(); const edgeWeight = () => ({toWeight: 6.0, froWeight: 3.0}); - const contributions = createContributions(g, edgeWeight, 1.0); - const osmc = createOrderedSparseMarkovChain(contributions); + const connections = createConnections(g, edgeWeight, 1.0); + const osmc = createOrderedSparseMarkovChain(connections); const pi = await findStationaryDistribution(osmc.chain, { verbose: false, convergenceThreshold: 1e-6, @@ -153,7 +153,7 @@ describe("core/attribution/contributions", () => { yieldAfterMs: 1, }); const pr = distributionToNodeDistribution(osmc.nodeOrder, pi); - const result = decompose(pr, contributions); + const result = decompose(pr, connections); validateDecomposition(result); }); });