Skip to content

Commit

Permalink
Merge branch 'userArgumentPriority'
Browse files Browse the repository at this point in the history
  • Loading branch information
sofvanh committed Dec 18, 2024
2 parents 5d033c9 + e236f3c commit 4db3f4e
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 123 deletions.
68 changes: 68 additions & 0 deletions backend/src/analysis/argumentPriorityHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Score } from "../.shared/types";
import { analyzeVotes } from "./voteAnalyzer";
import { getArgumentScores } from "./argumentScoreHandler";
import { ReactionForGraph, getReactionsForGraph} from "../db/operations/reactionOperations";
import { getArgumentIdsByGraphId } from "../db/operations/argumentOperations";

interface ArgumentPriority {
argumentId: string;
priority: number;
}

async function getUniquenessScores(userId: string, graphId: string): Promise<Map<string, number>> {
const {
userIndexMap,
argumentIndexMap,
uniquenessMatrix
} = await analyzeVotes(graphId);

const userIndex = userIndexMap.get(userId);
if (!userIndex) {
return new Map<string, number>();
}
else {
const uniquenessScores = new Map<string, number>();
argumentIndexMap.forEach((argumentIndex, argumentId) => {
const uniquenessScore = uniquenessMatrix[userIndex][argumentIndex];
uniquenessScores.set(argumentId, uniquenessScore);
});
return uniquenessScores;
}
}

export async function getArgumentPriorities(graphId: string, userId: string): Promise<ArgumentPriority[]> {
const argumentIds = await getArgumentIdsByGraphId(graphId);
const argumentPriorityMap = new Map<string, number>();

// Record which arguments the user has reacted to
const reactions: ReactionForGraph[] = await getReactionsForGraph(graphId);
const userReactedMap = new Map<string, boolean>(
reactions.filter(reaction => reaction.userId === userId).map(reaction => [reaction.argumentId, true])
);

// Get priority for arguments with scores (consensus, fragmentation, clarity)
const argumentScores: Map<string, Score> = await getArgumentScores(graphId);
const uniquenessScores: Map<string, number> = await getUniquenessScores(userId, graphId);

for (const [argumentId, score] of argumentScores) {
const uniquenessScore = uniquenessScores.get(argumentId) ?? 1;
const priority = (1 + 50 * score.consensus + 50 * score.fragmentation) * (score.clarity ** 2) * (uniquenessScore ** 2);
argumentPriorityMap.set(argumentId, priority);
}

// Add default priority for arguments without scores
argumentIds.forEach(argumentId => {
if (!argumentPriorityMap.has(argumentId)) {
argumentPriorityMap.set(argumentId, 1);
}
});

// Set priority to 0 for arguments the user has already reacted to
userReactedMap.forEach((_, argumentId) => {
argumentPriorityMap.set(argumentId, 0);
});

// Sort arguments by priority and return
return Array.from(argumentPriorityMap, ([argumentId, priority]) => ({ argumentId, priority }))
.sort((a, b) => b.priority - a.priority);
}
177 changes: 56 additions & 121 deletions backend/src/analysis/argumentScoreHandler.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,51 @@
import { Score } from "../.shared/types";
import { ReactionForGraph, getReactionsForGraph } from "../db/operations/reactionOperations";
import { cosineSimilarityMatrix } from "../utils/math";


export async function getArgumentScores(graphId: string): Promise<Map<string, Score>> {
const reactionArray: ReactionForGraph[] = await getReactionsForGraph(graphId);
const minimumVotesUser = 3;
const minimumVotesArgument = 2;

// Count votes for each user and each argument
const userVoteCounts = new Map<string, number>();
const argumentVoteCounts = new Map<string, number>();

for (const reaction of reactionArray) {
if (reaction.type === 'agree' || reaction.type === 'disagree') {
userVoteCounts.set(reaction.userId, (userVoteCounts.get(reaction.userId) || 0) + 1);
argumentVoteCounts.set(reaction.argumentId, (argumentVoteCounts.get(reaction.argumentId) || 0) + 1);
import { analyzeVotes } from "./voteAnalyzer";

function getArgumentClarityScore(argumentIndex: number,
votingMatrix: number[][],
unclearMatrix: number[][],
uniquenessMatrix: number[][]) {

//Identify all users who reacted on this argument
let usersWhoReacted: number[] = [];
for (let i = 0; i < votingMatrix.length; i++) {
if (votingMatrix[i][argumentIndex] !== 0 || unclearMatrix[i][argumentIndex] !== 0) {
usersWhoReacted.push(i);
}
}

// Filter reactions
const filteredReactions = reactionArray.filter(reaction => {
const userVoteCount = userVoteCounts.get(reaction.userId) || 0;
const argumentVoteCount = argumentVoteCounts.get(reaction.argumentId) || 0;
return userVoteCount >= minimumVotesUser && argumentVoteCount >= minimumVotesArgument;
});
let unclearSum = 0;
let uniquenessSum = 0;
for (let i = 0; i < usersWhoReacted.length; i++) {
let uniqueness = uniquenessMatrix[usersWhoReacted[i]][argumentIndex];

// Create maps for user and argument indices
const userIndexMap = new Map<string, number>();
const argumentIndexMap = new Map<string, number>();

// Fill the maps
let userIndex = 0;
let argumentIndex = 0;

for (const reaction of filteredReactions) {
if (!userIndexMap.has(reaction.userId)) {
userIndexMap.set(reaction.userId, userIndex);
userIndex++;
}
if (!argumentIndexMap.has(reaction.argumentId)) {
argumentIndexMap.set(reaction.argumentId, argumentIndex);
argumentIndex++;
}
unclearSum += unclearMatrix[usersWhoReacted[i]][argumentIndex] * uniqueness;
uniquenessSum += uniqueness;
}

// Initialize the voting matrix and unclear matrix
const userCount = userIndexMap.size;
const argumentCount = argumentIndexMap.size;

const votingMatrix = new Array(userCount).fill(0).map(() => new Array(argumentCount).fill(0));
const unclearMatrix = new Array(userCount).fill(0).map(() => new Array(argumentCount).fill(0));

// Fill the matrices
for (const reaction of filteredReactions) {
const argumentIdx = argumentIndexMap.get(reaction.argumentId)!;
const userIdx = userIndexMap.get(reaction.userId)!;
if (reaction.type === 'agree') {
votingMatrix[userIdx][argumentIdx] = 1;
}
else if (reaction.type === 'disagree') {
votingMatrix[userIdx][argumentIdx] = -1;
}
else if (reaction.type === 'unclear') {
unclearMatrix[userIdx][argumentIdx] = 1;
}
else {
throw new Error('Invalid reaction type');
}
}
return 1 - (unclearSum / uniquenessSum);
}

// Calculate the user similarity matrix
const userSimilarityMatrix: number[][] = cosineSimilarityMatrix(votingMatrix);
export async function getArgumentScores(graphId: string): Promise<Map<string, Score>> {
const {
userIndexMap,
argumentIndexMap,
votingMatrix,
unclearMatrix,
uniquenessMatrix,
sum_pos_pos,
sum_pos_neg,
sum_neg_pos,
sum_neg_neg,
} = await analyzeVotes(graphId);

// Calculate the argument scores for each argument
const argumentScores: Map<string, Score> = new Map();

argumentIndexMap.forEach((argumentIndex, argumentId) => {
//Identify users who voted on this argument
const usersWhoVoted: number[] = [];
for (let i = 0; i < userCount; i++) {
for (let i = 0; i < userIndexMap.size; i++) {
if (votingMatrix[i][argumentIndex] !== 0) {
usersWhoVoted.push(i);
}
Expand All @@ -88,65 +54,32 @@ export async function getArgumentScores(graphId: string): Promise<Map<string, Sc

if (usersWhoVoted.length >= 2) {

// Get smaller matrices for just the users who voted
const userSimilarities = usersWhoVoted.map(i =>
usersWhoVoted.map(j => userSimilarityMatrix[i][j])
);
const userAgreementMatrix = votes.map(voteI =>
votes.map(voteJ => (voteI === voteJ ? 1 : 0))
);
const userDisagreementMatrix = votes.map(voteI =>
votes.map(voteJ => (voteI === -voteJ ? 1 : 0))
);

// Compute individual user scores (to be aggregated as the final argument score later)
const userConsensusScores = new Array(usersWhoVoted.length).fill(0);
const userFragmentationScores = new Array(usersWhoVoted.length).fill(0);
const userUniquenessScores = new Array(usersWhoVoted.length).fill(0);
const userUnclearScores = new Array(usersWhoVoted.length).fill(0);

for (let i = 0; i < usersWhoVoted.length; i++) {
// Partition users into in-group and out-group
const inGroup: number[] = [];
const outGroup: number[] = [];
for (let j = 0; j < usersWhoVoted.length; j++) {
const similarity = userSimilarities[i][j];
if (similarity > 0) {
inGroup.push(j);
} else if (similarity < 0) {
outGroup.push(j);
}
}

// Get in-group and out-group users
const sumIngroupAgree = sum_pos_pos[usersWhoVoted[i]][argumentIndex];
const sumIngroupDisagree = sum_pos_neg[usersWhoVoted[i]][argumentIndex];
const sumOutgroupAgree = sum_neg_pos[usersWhoVoted[i]][argumentIndex];
const sumOutgroupDisagree = sum_neg_neg[usersWhoVoted[i]][argumentIndex];

// Calculate user consensus score
if (outGroup.length > 0) {
let consensusSum = 0
let outgroupWeightedSize = 0;
for (const j of outGroup) {
consensusSum += userAgreementMatrix[i][j] * userSimilarities[i][j];
outgroupWeightedSize += userSimilarities[i][j];
}
userConsensusScores[i] = consensusSum / outgroupWeightedSize;
const sumAlignedOutgroup = votes[i] === 1 ? sumOutgroupAgree : sumOutgroupDisagree;
const sumOutgroup = sumOutgroupAgree + sumOutgroupDisagree;
if (sumOutgroup > 0) {
userConsensusScores[i] = sumAlignedOutgroup / sumOutgroup;
}
else {
userConsensusScores[i] = 0;
}

// Calculate user fragmentation score
let fragmentationSum = 0;
let ingroupWeightedSize = 0;
for (const j of inGroup) { // inGroup.length is always > 0 (includes self)
fragmentationSum += userDisagreementMatrix[i][j] * userSimilarities[i][j];
ingroupWeightedSize += userSimilarities[i][j];
}
userFragmentationScores[i] = fragmentationSum / ingroupWeightedSize;


// Calculate user uniqueness score
userUniquenessScores[i] = 1 / ingroupWeightedSize

// Get user unclear score
userUnclearScores[i] = unclearMatrix[usersWhoVoted[i]][argumentIndex];
const sumDisalignedIngroup = votes[i] === -1 ? sumIngroupAgree : sumIngroupDisagree;
const sumIngroup = sumIngroupAgree + sumIngroupDisagree;
userFragmentationScores[i] = sumDisalignedIngroup / sumIngroup;
}

// Aggregate user scores to get argument scores
Expand All @@ -156,32 +89,34 @@ export async function getArgumentScores(graphId: string): Promise<Map<string, Sc
let weightedConsensusSum = 0;
let uniquenessSum = 0;
for (let i = 0; i < usersWhoVoted.length; i++) {
weightedConsensusSum += userConsensusScores[i] * userUniquenessScores[i];
uniquenessSum += userUniquenessScores[i];
weightedConsensusSum += userConsensusScores[i] * uniquenessMatrix[usersWhoVoted[i]][argumentIndex];
uniquenessSum += uniquenessMatrix[usersWhoVoted[i]][argumentIndex];
}
const argumentConsensusScore = weightedConsensusSum / uniquenessSum;

// Calculate argument fragmentation score
let weightedFragmentationSum = 0;
for (let i = 0; i < usersWhoVoted.length; i++) {
weightedFragmentationSum += userFragmentationScores[i] * userUniquenessScores[i];
weightedFragmentationSum += userFragmentationScores[i] * uniquenessMatrix[usersWhoVoted[i]][argumentIndex];
}
// Score is multiplied by 2 to scale it to the range [0, 1]
const argumentFragmentationScore = (weightedFragmentationSum / uniquenessSum) * 2;

// Calculate argument clarity score
let unclearSum = 0;
for (let i = 0; i < usersWhoVoted.length; i++) {
unclearSum += userUnclearScores[i] * userUniquenessScores[i];
}
const argumentClarityScore = 1 - (unclearSum / uniquenessSum);
const argumentClarityScore = getArgumentClarityScore(argumentIndex,
votingMatrix,
unclearMatrix,
uniquenessMatrix);

argumentScores.set(argumentId, {
consensus: argumentConsensusScore,
fragmentation: argumentFragmentationScore,
clarity: argumentClarityScore
});
}
else {
throw new Error('Argument has less than 2 votes');
}
});

return argumentScores;
Expand Down
Loading

0 comments on commit 4db3f4e

Please sign in to comment.