Source code for corerec.explanation.feature_based

"""
Feature-based Explanations

Generate explanations based on matching features between
user preferences and item attributes.
"""

from typing import Any, Callable, Dict, List, Optional

from .base import BaseExplainer, Explanation


[docs] class FeatureExplainer(BaseExplainer): """ Generates explanations based on feature matches. Looks at which features of the item match user preferences and generates explanations like: - "Because you liked action movies" - "Similar to items in your history" - "Matches your preferred price range" Example:: explainer = FeatureExplainer( item_features=item_feature_dict, user_preferences=user_pref_dict, templates={ 'category': "Because you like {value}", 'brand': "From {value}, a brand you've purchased before", } ) explanation = explainer.explain(item_id=123, context={'user_id': 456}) """
[docs] def __init__( self, item_features: Optional[Dict[Any, Dict[str, Any]]] = None, user_preferences: Optional[Dict[Any, Dict[str, Any]]] = None, templates: Optional[Dict[str, str]] = None, feature_extractor: Optional[Callable[[Any], Dict[str, Any]]] = None, preference_extractor: Optional[Callable[[Any], Dict[str, Any]]] = None, name: str = "feature_explainer", ): """ Args: item_features: dict mapping item_id -> feature_dict user_preferences: dict mapping user_id -> preference_dict templates: templates for each feature type feature_extractor: function(item_id) -> features (alternative to dict) preference_extractor: function(user_id) -> preferences name: identifier """ super().__init__(name=name) self.item_features = item_features or {} self.user_preferences = user_preferences or {} self.feature_extractor = feature_extractor self.preference_extractor = preference_extractor self.templates = templates or { 'category': "Because you like {value}", 'genre': "Matches your interest in {value}", 'brand': "From {value}, a brand you trust", 'price_range': "Within your preferred budget", 'default': "Based on your preferences", }
[docs] def explain( self, item_id: Any, context: Dict[str, Any], **kwargs ) -> Explanation: """Generate feature-based explanation.""" user_id = context.get('user_id') # get item features if self.feature_extractor: item_feats = self.feature_extractor(item_id) else: item_feats = self.item_features.get(item_id, {}) # get user preferences if self.preference_extractor and user_id: user_prefs = self.preference_extractor(user_id) elif user_id: user_prefs = self.user_preferences.get(user_id, {}) else: user_prefs = {} # find matching features matches = [] for feat_name, feat_value in item_feats.items(): if feat_name in user_prefs: user_pref = user_prefs[feat_name] # check if they match (simple equality or containment) if self._features_match(feat_value, user_pref): matches.append((feat_name, feat_value)) # generate explanation text if matches: # use the most specific match feat_name, feat_value = matches[0] template = self.templates.get(feat_name, self.templates['default']) text = template.format(value=feat_value, feature=feat_name) exp_type = f"feature_{feat_name}" else: text = "Recommended for you" exp_type = "generic" return Explanation( item_id=item_id, text=text, explanation_type=exp_type, features=item_feats, )
def _features_match(self, item_value: Any, user_pref: Any) -> bool: """Check if item feature matches user preference.""" # exact match if item_value == user_pref: return True # item value in user preference list if isinstance(user_pref, (list, set)): return item_value in user_pref # user preference in item value (e.g., category hierarchy) if isinstance(item_value, str) and isinstance(user_pref, str): return user_pref.lower() in item_value.lower() return False
[docs] class HistoryExplainer(BaseExplainer): """ Explains based on similar items in user history. Generates explanations like: - "Because you watched Movie X" - "Similar to items you purchased recently" """
[docs] def __init__( self, user_history: Optional[Dict[Any, List[Any]]] = None, item_similarity: Optional[Callable[[Any, Any], float]] = None, item_names: Optional[Dict[Any, str]] = None, name: str = "history_explainer", ): """ Args: user_history: dict mapping user_id -> list of interacted item_ids item_similarity: function(item_a, item_b) -> similarity score item_names: dict mapping item_id -> display name name: identifier """ super().__init__(name=name) self.user_history = user_history or {} self.item_similarity = item_similarity self.item_names = item_names or {}
[docs] def explain( self, item_id: Any, context: Dict[str, Any], **kwargs ) -> Explanation: """Generate history-based explanation.""" user_id = context.get('user_id') if not user_id or user_id not in self.user_history: return Explanation( item_id=item_id, text="Recommended for you", explanation_type="generic", ) history = self.user_history[user_id] if not history: return Explanation( item_id=item_id, text="Recommended for you", explanation_type="generic", ) # find most similar item in history best_match = None best_sim = 0.0 if self.item_similarity: for hist_item in history[-10:]: # look at recent history sim = self.item_similarity(item_id, hist_item) if sim > best_sim: best_sim = sim best_match = hist_item else: # no similarity function, just use most recent best_match = history[-1] if best_match: match_name = self.item_names.get(best_match, str(best_match)) text = f"Because you liked {match_name}" supporting = [best_match] else: text = "Based on your history" supporting = [] return Explanation( item_id=item_id, text=text, explanation_type="history", confidence=best_sim if self.item_similarity else 0.5, supporting_items=supporting, )