wuenlp_tools.models.sentiment.harrymotions_aggregation
1from __future__ import annotations 2 3from collections import Counter 4from dataclasses import dataclass, field 5from enum import Enum 6 7from loguru import logger 8from wuenlp.impl.UIMANLPStructs import UIMADocument 9from wuenlp.impl.uima import UIMAInteraction, UIMASpan, UIMASystemScene 10 11from wuenlp_tools.pipeline import PipelineProcessor, PipelineStep 12 13 14class HarryMotionsCategoricalAggregator(str, Enum): 15 MODE = "mode" 16 DISTRIBUTION = "distribution" 17 18 19class HarryMotionsNumericReducer(str, Enum): 20 MEAN = "mean" 21 ABS_MEAN = "abs_mean" 22 MIN = "min" 23 MAX = "max" 24 NEG_RATIO = "neg_ratio" 25 COUNT = "count" 26 27 28class HarryMotionsEmptyPolicy(str, Enum): 29 ZERO = "zero" 30 NONE = "none" 31 32 33@dataclass(frozen=True) 34class HarryMotionsAggregationConfig: 35 reducers: tuple[HarryMotionsNumericReducer, ...] = (HarryMotionsNumericReducer.MEAN,) 36 categorical_aggregator: HarryMotionsCategoricalAggregator = HarryMotionsCategoricalAggregator.MODE 37 empty_policy: HarryMotionsEmptyPolicy = HarryMotionsEmptyPolicy.ZERO 38 39 40def _hm_empty_value(policy: HarryMotionsEmptyPolicy) -> float | None: 41 return 0.0 if policy == HarryMotionsEmptyPolicy.ZERO else None 42 43 44def _hm_setting_reducer_key(setting: str, reducer: str) -> str: 45 return f"harrymotions_{setting}_agg_{reducer}" 46 47 48def _hm_setting_categorical_key(setting: str, suffix: str) -> str: 49 return f"harrymotions_{setting}_agg_{suffix}" 50 51 52@dataclass 53class HarryMotionsAggregationProcessor(PipelineProcessor): 54 config: HarryMotionsAggregationConfig = field(default_factory=HarryMotionsAggregationConfig) 55 56 @staticmethod 57 def _parse_harrymotions_features(interactions: list[UIMAInteraction]) -> dict[str, dict[str, str]]: 58 settings: dict[str, dict[str, str]] = {} 59 suffixes = ("sentiment_polarity", "sentiment_bin", "label") 60 for inter in interactions: 61 for key in inter.additional_features.keys(): 62 for suffix in suffixes: 63 marker = f"_{suffix}" 64 if key.startswith("harrymotions_") and key.endswith(marker): 65 setting = key[len("harrymotions_"):-len(marker)] 66 setting_map = settings.setdefault(setting, {}) 67 setting_map[suffix] = key 68 break 69 return settings 70 71 @staticmethod 72 def _reduce_numeric(values: list[float], reducer: HarryMotionsNumericReducer) -> float: 73 if reducer == HarryMotionsNumericReducer.MEAN: 74 return sum(values) / len(values) 75 if reducer == HarryMotionsNumericReducer.ABS_MEAN: 76 return sum(abs(v) for v in values) / len(values) 77 if reducer == HarryMotionsNumericReducer.MIN: 78 return min(values) 79 if reducer == HarryMotionsNumericReducer.MAX: 80 return max(values) 81 if reducer == HarryMotionsNumericReducer.NEG_RATIO: 82 return sum(1 for v in values if v < 0) / len(values) 83 if reducer == HarryMotionsNumericReducer.COUNT: 84 return float(len(values)) 85 raise ValueError( 86 "Unsupported reducer. Use one of: mean, abs_mean, min, max, neg_ratio, count" 87 ) 88 89 def __call__(self, doc: UIMADocument, unit_type: type[UIMASpan] | None, overwrite: bool = False, **kwargs) -> UIMADocument: 90 if unit_type is None: 91 raise ValueError("HarryMotionsAggregationProcessor requires a non-None unit_type.") 92 interactions = list(doc.interactions) 93 if not interactions: 94 logger.warning("No interactions found; skipping HarryMotions aggregation.") 95 return doc 96 units = list(doc.get_annos_of_type(unit_type)) 97 if not units: 98 logger.warning(f"No units found for {unit_type.__name__}; skipping HarryMotions aggregation.") 99 return doc 100 101 settings = self._parse_harrymotions_features(interactions) 102 if not settings: 103 logger.warning("No HarryMotions interaction features found; skipping aggregation.") 104 return doc 105 106 for unit in units: 107 unit_interactions = list(unit.overlapping(UIMAInteraction)) 108 for setting, feature_map in settings.items(): 109 numeric_key = feature_map.get("sentiment_polarity") or feature_map.get("sentiment_bin") 110 label_key = feature_map.get("label") 111 112 if numeric_key: 113 numeric_values: list[float] = [] 114 for inter in unit_interactions: 115 value = inter.additional_features.get(numeric_key) 116 try: 117 if value is not None: 118 numeric_values.append(float(value)) 119 except (TypeError, ValueError): 120 continue 121 for reducer in self.config.reducers: 122 feature_name = _hm_setting_reducer_key(setting, reducer.value) 123 if not overwrite and feature_name in unit.additional_features: 124 continue 125 if numeric_values: 126 unit.additional_features[feature_name] = self._reduce_numeric(numeric_values, reducer) 127 else: 128 unit.additional_features[feature_name] = _hm_empty_value(self.config.empty_policy) 129 130 if label_key: 131 labels = [ 132 str(inter.additional_features.get(label_key)) 133 for inter in unit_interactions 134 if inter.additional_features.get(label_key) 135 ] 136 if self.config.categorical_aggregator == HarryMotionsCategoricalAggregator.MODE: 137 mode_key = _hm_setting_categorical_key(setting, "label_mode") 138 if overwrite or mode_key not in unit.additional_features: 139 if labels: 140 counts = Counter(labels) 141 mode_label = sorted(counts.items(), key=lambda item: (-item[1], item[0]))[0][0] 142 unit.additional_features[mode_key] = mode_label 143 else: 144 unit.additional_features[mode_key] = None 145 elif self.config.categorical_aggregator == HarryMotionsCategoricalAggregator.DISTRIBUTION: 146 base_key = _hm_setting_categorical_key(setting, "label_prob") 147 counts = Counter(labels) 148 total = sum(counts.values()) 149 for label, count in counts.items(): 150 feature_name = f"{base_key}_{label}" 151 if overwrite or feature_name not in unit.additional_features: 152 unit.additional_features[feature_name] = count / total if total > 0 else 0.0 153 else: 154 raise ValueError(f"Unknown categorical aggregator: {self.config.categorical_aggregator}") 155 156 return doc 157 158 159def HarryMotionsAggregationStep( 160 unit_type: type[UIMASpan] = UIMASystemScene, 161 config: HarryMotionsAggregationConfig | None = None, 162 name: str = "HarryMotions Aggregation", 163) -> PipelineStep[HarryMotionsAggregationProcessor]: 164 processor = HarryMotionsAggregationProcessor(config=config or HarryMotionsAggregationConfig()) 165 return PipelineStep( 166 name=name, 167 processor=processor, 168 unit_type=unit_type, 169 modified_types=[unit_type], 170 added_additional_features=[ 171 "harrymotions_*_agg_*", 172 ], 173 )
15class HarryMotionsCategoricalAggregator(str, Enum): 16 MODE = "mode" 17 DISTRIBUTION = "distribution"
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
20class HarryMotionsNumericReducer(str, Enum): 21 MEAN = "mean" 22 ABS_MEAN = "abs_mean" 23 MIN = "min" 24 MAX = "max" 25 NEG_RATIO = "neg_ratio" 26 COUNT = "count"
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
34@dataclass(frozen=True) 35class HarryMotionsAggregationConfig: 36 reducers: tuple[HarryMotionsNumericReducer, ...] = (HarryMotionsNumericReducer.MEAN,) 37 categorical_aggregator: HarryMotionsCategoricalAggregator = HarryMotionsCategoricalAggregator.MODE 38 empty_policy: HarryMotionsEmptyPolicy = HarryMotionsEmptyPolicy.ZERO
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
53@dataclass 54class HarryMotionsAggregationProcessor(PipelineProcessor): 55 config: HarryMotionsAggregationConfig = field(default_factory=HarryMotionsAggregationConfig) 56 57 @staticmethod 58 def _parse_harrymotions_features(interactions: list[UIMAInteraction]) -> dict[str, dict[str, str]]: 59 settings: dict[str, dict[str, str]] = {} 60 suffixes = ("sentiment_polarity", "sentiment_bin", "label") 61 for inter in interactions: 62 for key in inter.additional_features.keys(): 63 for suffix in suffixes: 64 marker = f"_{suffix}" 65 if key.startswith("harrymotions_") and key.endswith(marker): 66 setting = key[len("harrymotions_"):-len(marker)] 67 setting_map = settings.setdefault(setting, {}) 68 setting_map[suffix] = key 69 break 70 return settings 71 72 @staticmethod 73 def _reduce_numeric(values: list[float], reducer: HarryMotionsNumericReducer) -> float: 74 if reducer == HarryMotionsNumericReducer.MEAN: 75 return sum(values) / len(values) 76 if reducer == HarryMotionsNumericReducer.ABS_MEAN: 77 return sum(abs(v) for v in values) / len(values) 78 if reducer == HarryMotionsNumericReducer.MIN: 79 return min(values) 80 if reducer == HarryMotionsNumericReducer.MAX: 81 return max(values) 82 if reducer == HarryMotionsNumericReducer.NEG_RATIO: 83 return sum(1 for v in values if v < 0) / len(values) 84 if reducer == HarryMotionsNumericReducer.COUNT: 85 return float(len(values)) 86 raise ValueError( 87 "Unsupported reducer. Use one of: mean, abs_mean, min, max, neg_ratio, count" 88 ) 89 90 def __call__(self, doc: UIMADocument, unit_type: type[UIMASpan] | None, overwrite: bool = False, **kwargs) -> UIMADocument: 91 if unit_type is None: 92 raise ValueError("HarryMotionsAggregationProcessor requires a non-None unit_type.") 93 interactions = list(doc.interactions) 94 if not interactions: 95 logger.warning("No interactions found; skipping HarryMotions aggregation.") 96 return doc 97 units = list(doc.get_annos_of_type(unit_type)) 98 if not units: 99 logger.warning(f"No units found for {unit_type.__name__}; skipping HarryMotions aggregation.") 100 return doc 101 102 settings = self._parse_harrymotions_features(interactions) 103 if not settings: 104 logger.warning("No HarryMotions interaction features found; skipping aggregation.") 105 return doc 106 107 for unit in units: 108 unit_interactions = list(unit.overlapping(UIMAInteraction)) 109 for setting, feature_map in settings.items(): 110 numeric_key = feature_map.get("sentiment_polarity") or feature_map.get("sentiment_bin") 111 label_key = feature_map.get("label") 112 113 if numeric_key: 114 numeric_values: list[float] = [] 115 for inter in unit_interactions: 116 value = inter.additional_features.get(numeric_key) 117 try: 118 if value is not None: 119 numeric_values.append(float(value)) 120 except (TypeError, ValueError): 121 continue 122 for reducer in self.config.reducers: 123 feature_name = _hm_setting_reducer_key(setting, reducer.value) 124 if not overwrite and feature_name in unit.additional_features: 125 continue 126 if numeric_values: 127 unit.additional_features[feature_name] = self._reduce_numeric(numeric_values, reducer) 128 else: 129 unit.additional_features[feature_name] = _hm_empty_value(self.config.empty_policy) 130 131 if label_key: 132 labels = [ 133 str(inter.additional_features.get(label_key)) 134 for inter in unit_interactions 135 if inter.additional_features.get(label_key) 136 ] 137 if self.config.categorical_aggregator == HarryMotionsCategoricalAggregator.MODE: 138 mode_key = _hm_setting_categorical_key(setting, "label_mode") 139 if overwrite or mode_key not in unit.additional_features: 140 if labels: 141 counts = Counter(labels) 142 mode_label = sorted(counts.items(), key=lambda item: (-item[1], item[0]))[0][0] 143 unit.additional_features[mode_key] = mode_label 144 else: 145 unit.additional_features[mode_key] = None 146 elif self.config.categorical_aggregator == HarryMotionsCategoricalAggregator.DISTRIBUTION: 147 base_key = _hm_setting_categorical_key(setting, "label_prob") 148 counts = Counter(labels) 149 total = sum(counts.values()) 150 for label, count in counts.items(): 151 feature_name = f"{base_key}_{label}" 152 if overwrite or feature_name not in unit.additional_features: 153 unit.additional_features[feature_name] = count / total if total > 0 else 0.0 154 else: 155 raise ValueError(f"Unknown categorical aggregator: {self.config.categorical_aggregator}") 156 157 return doc
160def HarryMotionsAggregationStep( 161 unit_type: type[UIMASpan] = UIMASystemScene, 162 config: HarryMotionsAggregationConfig | None = None, 163 name: str = "HarryMotions Aggregation", 164) -> PipelineStep[HarryMotionsAggregationProcessor]: 165 processor = HarryMotionsAggregationProcessor(config=config or HarryMotionsAggregationConfig()) 166 return PipelineStep( 167 name=name, 168 processor=processor, 169 unit_type=unit_type, 170 modified_types=[unit_type], 171 added_additional_features=[ 172 "harrymotions_*_agg_*", 173 ], 174 )