Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactoring scores #263

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions app/feed/get_feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from collections import Counter, OrderedDict
import pickle
import random
import sys

from flask import current_app
import networkx as nx
import numpy as np

from ..scoring.build_localised_acyclic_graph import build_localised_acyclic_graph


class Feed:

"""
After users have completed their survey, they want to receive personalized suggestions
for issues that affect them and actions they can take that will directly impact those
issues.

This class handles the feed functions. Currently the feed works by associating
a user's values with relevant issues (nodes).
"""

def __init__(self, user_scores):
self.G = current_app.config["T"].copy() # Using test ontology
self.user_scores = user_scores
self.climate_effects = None

def get_scores_vector(self):
"""
Extracts scores from a dictionary and returns the scores as a vector (alphabetical order).
Used in simple_scoring to compute a dot product.
"""
return [
self.user_scores["achievement"],
self.user_scores["benevolence"],
self.user_scores["conformity"],
self.user_scores["hedonism"],
self.user_scores["power"],
self.user_scores["security"],
self.user_scores["self_direction"],
self.user_scores["stimulation"],
self.user_scores["tradition"],
self.user_scores["universalism"],
]

def score_nodes(self, session_uuid, nx_utils, myth_processor, solution_processor):
"""
Each climate effects node will have personal values associated with it. These
values are stored as a vector within the node. This vector is run through the
dot product with the users scores to determine the overall relevance of the node
to a user's values.

:param session_uuid: uuid4 as str
:param nx_utils: Class to extract data from nodes
:param myth_processor: Class to extract data from myths
:param solution_processor: Class to extract data from solutions
"""
climate_effects = []
user_scores_vector = np.array(self.get_scores_vector())

alpha = 2 # transforms user questionnaire scores to exponential distribution
modified_user_scores_vector = np.power(user_scores_vector, alpha)
localised_acyclic_graph = build_localised_acyclic_graph(
self.G, session_uuid
) # TODO: Fix

for node in self.G.nodes:
current_node = self.G.nodes[node]
if "personal_values_10" in current_node and any(
current_node["personal_values_10"]
):
node_values_associations_10 = current_node["personal_values_10"]

myth_processor.set_current_node(current_node)
nx_utils.set_current_node(current_node)
d = {
"effectId": nx_utils.get_node_id(),
"effectTitle": current_node["label"],
"effectDescription": nx_utils.get_description(),
"effectShortDescription": nx_utils.get_short_description(),
"imageUrl": nx_utils.get_image_url(),
"effectSources": nx_utils.get_causal_sources(),
"isPossiblyLocal": nx_utils.get_is_possibly_local(
localised_acyclic_graph.nodes[node]
),
"effectSpecificMythIRIs": myth_processor.get_effect_specific_myths(),
}

if any(v is None for v in node_values_associations_10):
score = None

else:
node_values_associations_10 = np.array(node_values_associations_10)
# double the magnitude of the backfire-effect representation:
modified_node_values_associations_10 = np.where(
node_values_associations_10 < 0,
2 * node_values_associations_10,
node_values_associations_10,
)
score = np.dot(
modified_user_scores_vector,
modified_node_values_associations_10,
)
d["effectSolutions"] = solution_processor.get_user_actions(
current_node["label"]
)

d["effectScore"] = score
climate_effects.append(d)

self.climate_effects = climate_effects

def get_best_nodes(self, num_feed_cards):
"""
Sorts nodes by their relevance to a user.
Nodes with no score are assigned -inf as it could be risky to show
a user an unreviewed climate effect (may have a backfire effect).

:returns best_nodes: top nodes for a user
"""
best_nodes = sorted(
self.climate_effects,
key=lambda k: k["effectScore"] or float("-inf"),
reverse=True,
)[:num_feed_cards]

return best_nodes
110 changes: 65 additions & 45 deletions app/feed/routes.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import uuid

from flask import jsonify, request
from app.scoring import bp
from app.scoring.score_nodes import score_nodes
from app.errors.errors import InvalidUsageError, DatabaseError, CustomError
from app.auth.utils import check_uuid_in_db, uuidType, validate_uuid
from app.models import Scores, Sessions
from flask_cors import cross_origin
from sqlalchemy.exc import SQLAlchemyError

from app import db, auto, cache
import uuid
from app import auto, cache, db
from . import bp
from .get_feed import Feed
from .store_climate_feed_data import store_climate_feed_data
from ..auth.utils import check_uuid_in_db, uuidType, validate_uuid
from ..errors.errors import CustomError, DatabaseError, InvalidUsageError
from ..models import Sessions, Scores
from ..myths.process_myths import process_myths
from ..network_x_tools.network_x_utils import network_x_utils
from ..solutions.process_solutions import process_solutions


@bp.route("/feed", methods=["GET"])
@cross_origin()
@auto.doc()
def get_feed():
"""
The front-end needs to request personalized climate change effects that are most
relevant to a user to display in the user's feed.
PARAMETER (as GET)
------------------
session-id : uuid4 as string
Provides the user with climate change effects that are most
personalized to display in the user's feed.

:param quizId: uuid4 as string (passed via url arg)

:returns: Feed data as JSON & 200 on success
"""
N_FEED_CARDS = 21
num_feed_cards = 21 # Planned for more use, do not move
quiz_uuid = request.args.get("quizId")
quiz_uuid = validate_uuid(quiz_uuid, uuidType.QUIZ)
check_uuid_in_db(quiz_uuid, uuidType.QUIZ)
Expand All @@ -34,48 +41,61 @@ def get_feed():
session_uuid = validate_uuid(session_uuid, uuidType.SESSION)
check_uuid_in_db(session_uuid, uuidType.SESSION)

feed_entries = get_feed_results(quiz_uuid, N_FEED_CARDS, session_uuid)
feed_entries = get_feed_results(quiz_uuid, session_uuid, num_feed_cards)

return feed_entries
return jsonify(feed_entries), 200


@cache.memoize(timeout=1200) # 20 minutes
def get_feed_results(quiz_uuid, N_FEED_CARDS, session_uuid):
def get_feed_results(quiz_uuid, session_uuid, num_feed_cards):
"""
Mitigation solutions are served randomly based on a user's highest scoring climate
impacts. The order of these should not change when a page is refreshed. This method
looks for an existing cache based on a user's session ID, or creates a new feed if
one is not found.

:param quiz_uuid: uuid4 as string
:param session_uuid: uuid4 as string
:param num_feed_cards: int

:returns feed_entries: dictionary of feed data
"""
scores = db.session.query(Scores).filter_by(quiz_uuid=quiz_uuid).first()

if scores:

personal_values_categories = [
"security",
"conformity",
"benevolence",
"tradition",
"universalism",
"self_direction",
"stimulation",
"hedonism",
"achievement",
"power",
]

scores = scores.__dict__
scores = {key: scores[key] for key in personal_values_categories}

try:
SCORE_NODES = score_nodes(scores, N_FEED_CARDS, quiz_uuid, session_uuid)
recommended_nodes = SCORE_NODES.get_user_nodes()
feed_entries = {"climateEffects": recommended_nodes}
return jsonify(feed_entries), 200
except:
raise CustomError(
message="Cannot get feed results. Something went wrong while processing the user's recommended nodes."
)

else:
if not scores:
raise DatabaseError(message="Cannot get feed results. Quiz ID not in database.")

personal_values_categories = [
"security",
"conformity",
"benevolence",
"tradition",
"universalism",
"self_direction",
"stimulation",
"hedonism",
"achievement",
"power",
]

scores = scores.__dict__
scores = {key: scores[key] for key in personal_values_categories}

node_scoring = Feed(scores)

nx_utils = network_x_utils()
myth_processor = process_myths()
solution_processor = process_solutions()
node_scoring.score_nodes(session_uuid, nx_utils, myth_processor, solution_processor)

recommended_nodes = node_scoring.get_best_nodes(num_feed_cards)

try:
store_climate_feed_data(session_uuid, recommended_nodes)

except SQLAlchemyError:
raise DatabaseError(message="Cannot store the results to the DB.")

feed_entries = {"climateEffects": recommended_nodes}

return feed_entries
16 changes: 16 additions & 0 deletions app/network_x_tools/network_x_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import networkx as nx

from ..scoring.scoring_utils import (
get_test_ontology,
get_valid_test_ont,
get_non_test_ont,
)


class network_x_processor:
def __init__(self):
Expand All @@ -27,3 +33,13 @@ def get_graph(self):
return self.G
except ValueError:
raise

def get_test_graph(self):
"""
Returns the test ontology for use in the user feed algorithm.
"""
T = self.G.copy()
valid_test_ont = get_valid_test_ont()
not_test_ont = get_non_test_ont()
get_test_ontology(T, valid_test_ont, not_test_ont)
return T
Loading