[docs]classBaseFeature(BaseModel,ABC):"""Base class for all feature types."""name:str=Field(description="The name of the feature. This is the name that will be used to identify the feature in the scorecard. It is recommended to match the name of the feature to the name of the column in the data.")family:str=Field(description="The family of the feature. Useful for grouping features together and understanding the dominance of feature families.")description:strbuckets:list[NumericBucket]|list[ObjectBucket]weight:float=Field(description="The weight of the feature in the overall scorecard. This is the weight that will be used to calculate the score for the feature i.e bucket points * weight = final score")default_score:float=0.0def_get_bucket_for_value(self,value:float|str)->NumericBucket|ObjectBucket|None:"""Get the bucket for a given value."""forbucketinself.buckets:ifbucket.contains(value):returnbucketreturnNone
[docs]defget_score(self,value:float|str)->float:"""Get the score for a given value."""bucket=self._get_bucket_for_value(value)ifbucketisnotNone:returnbucket.scorelogger.warning(f"No bucket found for value: {value} in feature: {self.name}")returnself.default_score
[docs]defget_score_range(self)->tuple[float,float]:"""Get the range of scores for a given feature."""return(min(bucket.scoreforbucketinself.buckets),max(bucket.scoreforbucketinself.buckets))
[docs]@model_validator(mode="after")deforder_buckets(self)->BaseFeature:# Sort buckets for visual purposes - doesn't change functionalityself.buckets.sort(key=lambdax:x.get_sort_key())returnself
[docs]defhas_overlapping_buckets(self)->bool:"""Check if any buckets in this feature overlap with each other"""fori,bucket1inenumerate(self.buckets):forbucket2inself.buckets[i+1:]:ifbucket1.overlaps_with(bucket2):# type: ignorereturnTruereturnFalse
[docs]defget_overlapping_buckets(self)->list[tuple[Any,Any]]:"""Get all pairs of overlapping buckets in this feature"""overlaps=[]fori,bucket1inenumerate(self.buckets):forbucket2inself.buckets[i+1:]:ifbucket1.overlaps_with(bucket2):# type: ignoreoverlaps.append((bucket1,bucket2))returnoverlaps
[docs]defget_feature_rows(self)->list[FeatureTableRow]:""" Get table rows for visualization purposes. """rows=[]fori,bucketinenumerate(self.buckets):row={"name":self.nameifi==0else"","family":self.familyor""ifi==0else"","description":self.descriptionor""ifi==0else"","weight":self.weightifi==0elseNone,"definition":bucket.display_definition(),# type: ignore"score":bucket.score,# type: ignore"default_score":self.default_scoreifi==0elseNone,}rows.append(FeatureTableRow(**row))returnrows
[docs]classNumericFeature(BaseFeature):"""Feature for numeric values using NumericBucket instances."""buckets:list[NumericBucket]
[docs]classObjectFeature(BaseFeature):"""Feature for categorical/string values using ObjectBucket instances."""buckets:list[ObjectBucket]