diff --git a/README.md b/README.md index 7525d68..f44a18f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ These models use the Minimum Description Length (MDL) principle as optimality cr ## Dependencies -This project was written for Python 3.7. All required packages from PyPI are specified in the `requirements.txt`. +This project targets Python 3.14. All required packages from PyPI are specified in the `requirements.txt`. *NOTE:* This list of packages includes the `gmpy2` package. diff --git a/build/lib/rulelist/__init__.py b/build/lib/rulelist/__init__.py new file mode 100644 index 0000000..89b877f --- /dev/null +++ b/build/lib/rulelist/__init__.py @@ -0,0 +1,8 @@ +from rulelist._classes import RuleListClassifier, RuleListRegressor, SubgroupListCategorical, SubgroupListGaussian,RuleList + +__all__ = ["RuleListClassifier", + "RuleListRegressor", + "SubgroupListCategorical", + "SubgroupListGaussian", + "RuleList"] + diff --git a/build/lib/rulelist/_classes.py b/build/lib/rulelist/_classes.py new file mode 100644 index 0000000..c6b363e --- /dev/null +++ b/build/lib/rulelist/_classes.py @@ -0,0 +1,647 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Oct 18 16:13:18 2019 + +@author: gathu +""" +from abc import ABCMeta +from abc import abstractmethod +from time import time +from typing import AnyStr + +import numpy as np +from sklearn.base import MultiOutputMixin, BaseEstimator, ClassifierMixin, RegressorMixin +from sklearn.base import is_classifier + +from rulelist.rulelistmodel.categoricalmodel.categoricalrulelist import CategoricalRuleList +from rulelist.rulelistmodel.gaussianmodel.gaussianrulelist import GaussianRuleList +from rulelist.rulelistmodel.prediction import predict_rulelist, predict_prob_rulelist +from rulelist.search.iterative_rule_search import greedy_and_beamsearch +from rulelist.util.bitset_operations import bitset2indexes +from rulelist.datastructure.data import Data + +class BaseRuleList(MultiOutputMixin, BaseEstimator, metaclass=ABCMeta): + """Base class for decision trees. + Warning: This class should not be used directly. + Use derived classes instead. + """ + + @abstractmethod + def __init__(self,*,max_depth, beam_width, min_support, n_cutpoints, discretization = "static", + max_rules = np.inf, alpha_gain = 1.0): + + if not isinstance(max_depth, (int, np.integer)) or max_depth < 1: + raise ValueError("max_depth incorrectly selected, please select a " + "positive integer greater or equal to 1.") + + if not isinstance(beam_width, (int, np.integer)) or beam_width < 1: + raise ValueError("beam_width incorrectly selected, please select a " + "positive integer greater or equal to 1.") + + if not isinstance(n_cutpoints, (int, np.integer)) or n_cutpoints < 2: + raise ValueError("n_cutpoints incorrectly selected, please select a " + "positive integer greater or equal to 2.") + + if discretization not in ["static","dynamic"]: + raise ValueError("At this moment we only support \"static\" or \"dynamic\" discretizations.") + + if not isinstance(n_cutpoints, (int, np.integer)) or max_rules < 0: + raise ValueError("max_rules incorrectly selected, please select a " + "zero or a positive integer.") + + if alpha_gain < 0 or alpha_gain > 1: + raise ValueError("alpha_gain incorrectly selected, please select a " + "between zero and 1 inclusive.") + + self.alpha_gain = alpha_gain + self.max_depth = max_depth + self.beam_width = beam_width + self.min_support = min_support + self.n_cutpoints = n_cutpoints + self.discretization = discretization + self.number_rules = 0 + self.max_rules = max_rules + self._rulelist = None + + #TODO: def __repr__ + def __str__(self): + text2print = self._rulelist.description if self.number_rules > 0 else "Model not fitted" + return text2print + + def fit(self,X,Y): + """Fit the model according to the given training datastructure. + Parameters + ---------- + df : pandas dataframe with name variables with last column as target + variable. + Returns + ------- + self : object + """ + is_nominal_target = is_classifier(self) + start_time = time() + #self._rulelist = _fit_rulelist( + # X,Y, self.target_model, self.max_depth,self.beam_width,self.min_support, self.n_cutpoints, + # self.task,self.discretization,self.max_rules,self.alpha_gain) + + data = Data(input_data=X, n_cutpoints=self.n_cutpoints, discretization=self.discretization, + target_data=Y, target_model=self.target_model, min_support=self.min_support) + + if is_nominal_target: + self._rulelist = CategoricalRuleList(data, self.task, self.max_depth, self.beam_width, self.min_support, self.max_rules, + self.alpha_gain) + else: + self._rulelist = GaussianRuleList(data, self.task, self.max_depth, self.beam_width, self.min_support, self.max_rules, + self.alpha_gain) + self._rulelist = greedy_and_beamsearch(data, self._rulelist) + self._rulelist.add_description() + self.runtime = time() - start_time + self.number_rules = self._rulelist.number_rules + self.rule_sets = [bitset2indexes(bitset) for bitset in self._rulelist.bitset_rules] + + return self + + + def predict(self,X): + """ Predicts the target variable for an input data X. + ---------- + X : a numpy array or pandas dataframe with the variables in the same + poistion (column number) as given in "fit" function. + + Returns a numpy array y with the predicted values according to the + fitted rule list (obtained using the "fit" function above). y has the + same length as X.shape[0] (number of rows). + ------- + self : object + """ + y_hat = predict_rulelist(X, self) + return y_hat + + +class RuleListClassifier(ClassifierMixin, BaseRuleList): + """A probabilistic rule list classifier. + + It can be applied for classification of univariate or multivariate (independent) targets. + It uses an Minimum Description Length (MDL) formulation to define an optimum rule list. + For search it resorts combination of greedy search to add one rule at the time, together with beam search to + find the the rules to add. + The algorithm is a mixture of [1],[2],[3]. The MDL nominal and numeric encoding, and algorithm is the one + proposed in [3] for subgroup list discovery + + Parameters + ---------- + max_depth : int, optional (default=5) + defines the maximum size that rule description can take based + on the number of variables that the beam search accepts to refine. + For example, if 'max_depth = 4' the maximum size of a pattern found is + 4. + + beam_width : int, optional (default=100) + defines the width of the beam in the beam search, i.e., the number of + patterns that are selected at each iteration to be expanded. + + min_support : int or float + defines the minimum support that a rule/subgroup can cover in the training datastructure. + if positive int, it defines an absolute value. + if smaller than one float, it defines a relative value, i.e., min_support*number_instances_data. + + n_cutpoints : int, optional (default=5) + number of cut points used to discretize a single-numeric attribute/variable. + Note 1: this algorithm creates for each cutpoint a binary split, and + the combination of all cutpoints. As an example of the former, if the + cut point is x_cut = 5, it will create both the condition x<5 and x>5. + In relation to the latter, if two of the cut points are x_cut1=3, and + x_cut2=5, it will also create 35. + In relation to the latter, if two of the cut points are x_cut1=3, and + x_cut2=5, it will also create 35. + In relation to the latter, if two of the cut points are x_cut1=3, and + x_cut2=5, it will also create 35. + In relation to the latter, if two of the cut points are x_cut1=3, and + x_cut2=5, it will also create 3 Iterator[Item]: + for item in self.items: + yield item \ No newline at end of file diff --git a/build/lib/rulelist/datastructure/attribute/nominal_attribute.py b/build/lib/rulelist/datastructure/attribute/nominal_attribute.py new file mode 100644 index 0000000..94dd392 --- /dev/null +++ b/build/lib/rulelist/datastructure/attribute/nominal_attribute.py @@ -0,0 +1,83 @@ +from dataclasses import field, dataclass +from functools import partial +from typing import List, Tuple, Any, AnyStr, Dict + +import numpy as np +import pandas as pd + +from rulelist.datastructure.attribute.attribute import Item, Attribute +from rulelist.util.bitset_operations import indexes2bitset + + +def activation_nominal(df: pd.DataFrame, attribute_name: AnyStr, category: Any) -> pd.DataFrame: + """Checks in which instances the numerical conditions are True. + + Parameters + ---------- + df : pandas.DataFrame + List of items that describe single-numeric attribute. + attribute_name : str + Name of attribute. + minval: float + Minimum value in the condition x >= minval. + maxval + Maximum value in the condition x < maxval. + + Returns + ---------- + activated_indexes : np.ndarray + Boolean array with True for values where the conditions are true. + """ + activated_indexes = df[attribute_name] == category + return activated_indexes + +@dataclass +class NominalAttribute(Attribute): # TODO: add sets of categories with OR logic (for now Nominal is equal BInary) + """ + Describes a nominal attribute or variable. Inherits from class Attribute. + + Attributes + ---------- + categories : np.ndarray + Array of categories. + items : List[Item] + List of items that are made from the values covered by the categories of this attribute. + + Parameters + ---------- + Attribute : class object that represents a variable. + + Methods + ------- + create_items + Creates the items from the categories of the nominal attribute with one operator. Example: x == blue_eyes + + """ + categories : np.ndarray = field(default_factory=list, init=False) + cardinality_operator : Dict[int,int] =field(init=False) + def __post_init__(self): + # preserve original category order as in the data + self.categories = pd.unique(self.values) + self.items, self.cardinality_operator = self.create_items() + + #TODO: expand make items simple nominal to sets of items with the logical OR + def create_items(self) -> Tuple[List[Item], Dict[int, int]]: + """ Creates a list of items from the nominal atrribute. + + Makes a list of items using equality relationship with the categories. Example: x= blue_eyes could be the + description of one of the items, for the NominalAttribute.name = "eye_colour". + + Returns + ---------- + List[Item] : List of Items + A list of all items based on the possible categories (only with equality relationships, not logical ORs). + """ + self.cardinality_operator = {1: len(self.categories)} + number_operators = 1 + for category in self.categories: + vector_category = np.where(self.values == category)[0] + bit_array = indexes2bitset(vector_category) + description = str(self.name) + " = " + str(category) + activation_function = partial(activation_nominal, attribute_name=self.name, category=category) + self.items.append(Item(bit_array,self.name, description, number_operators,activation_function)) + return self.items, self.cardinality_operator diff --git a/build/lib/rulelist/datastructure/attribute/numeric_attribute.py b/build/lib/rulelist/datastructure/attribute/numeric_attribute.py new file mode 100644 index 0000000..b9051cd --- /dev/null +++ b/build/lib/rulelist/datastructure/attribute/numeric_attribute.py @@ -0,0 +1,211 @@ +from dataclasses import dataclass, field +from functools import partial +from typing import List, Tuple, AnyStr, Iterator, Dict + +import numpy as np +from pandas import DataFrame + +from rulelist.datastructure.attribute.attribute import Attribute, Item +from rulelist.util.bitset_operations import indexes2bitset, bitset2indexes + + +def activation_numeric(df: DataFrame, attribute_name: AnyStr, minval: float, maxval: float) -> DataFrame: + """Checks in which instances the numerical conditions are True. + + Parameters + ---------- + df : pandas.DataFrame + List of items that describe single-numeric attribute. + attribute_name : str + Name of attribute. + minval: float + Minimum value in the condition x >= minval. + maxval + Maximum value in the condition x < maxval. + + Returns + ---------- + activated_indexes : np.ndarray + Boolean array with True for values where the conditions are true. + """ + activated_indexes = (df[attribute_name] >= minval) & (df[attribute_name] < maxval) + return activated_indexes + + +def find_cutpoints(values: np.ndarray, n_cutpoints: int) -> Tuple[np.ndarray, int]: + """ Finds the n quantile values as if done with equal frequency binning. + + Parameters + ---------- + values : np.ndarray + Array of values to discretize. + n_cutpoints : int + Number of cut points selected. + + Returns + ---------- + value_quantiles : np.ndarray + Array of the quantile values. + real_ncutpoints : int + In case the values do not allow n_cutpoints it returns a smaller value. + """ + if n_cutpoints > len(values): + n_cutpoints = len(values) + quantile_percentage = [1 / (n_cutpoints + 1) * ncut for ncut in range(0, n_cutpoints + 2)] + value_quantiles = np.nanquantile(values, quantile_percentage, interpolation='midpoint')[1:-1] + # if np.isnan(val_quantiles).any(): continu + value_quantiles = np.unique(value_quantiles) + real_ncutpoints = len(value_quantiles) + return value_quantiles, real_ncutpoints + +def create_item(indexes, variable_name, min_val, max_val, description, number_operations): + """ Creates a class of type Item from the values of a NumericAttribute. + + Parameters + ---------- + indexes : np.ndarray + Array of indexes where the item is present in the training datastructure. + variable_name : str + Name of the attribute/variable that this item is attached to. + min_val : float + Minimum value covered by this item. item > min_val. + max_val : float + Maximum value covered by this item. item < max_val. + description : str + Text describing the interval defined by the item. item < max_val = 1; min_val < item < max_val = 2. + number_operations : int + Number of logical operators used to define the interval. + Returns + ---------- + Item : Item class object + Item with the characteristics described by the arguments. + """ + bit_array = indexes2bitset(indexes) + activation_function = partial(activation_numeric, attribute_name=variable_name, minval=min_val, maxval=max_val) + return Item(bit_array, variable_name, description, number_operations, activation_function) + +@dataclass +class NumericAttribute(Attribute): + """ + Describes a single-numeric attribute or variable. Inherits from class Attribute. + + Attributes + ---------- + items : List[Item] + List of items that describe single-numeric attribute. + n_items : int + Number of items in this attribute. + + Parameters + ---------- + Attribute : class object that represents a variable. + + Methods + ------- + create_items_numeric + Creates the items by making binary partitions of the values using the cutpoints of equal frequency binning. + """ + n_cutpoints : int + discretization : AnyStr + items : List[Item] = field(default_factory=list, init=False) + cardinality_operator: Dict[int,int] = field(default_factory=dict, init=False) + #TODO: it would be interesting to add a generator instead of a list to do dynamic creation + def __post_init__(self): + self.items, self.cardinality_operator = self.create_items() + + def create_items(self,indexes=None) -> Tuple[List[Item], Dict[int, int]]: + """ Creates a list of items from the numerical atrribute. + + Makes a list of items using equal frequency binning, ignoring NANs, based on the values of the Numeric attribute + + Returns + ---------- + List[Item] : List of Items + A list of all items based on the possible combinations of cutpoints. + """ + if indexes is None: + values = self.values + else: + values = self.values[indexes] + #values = self.values[self.values.index.intersection(indexes)] + value_quantiles, self.n_cutpoints = find_cutpoints(values, self.n_cutpoints) + cardinality_operator = {1:0,2:0} + items = [] + for iq, value_quantile1 in enumerate(value_quantiles): # makes binary intervals x=val + # condition x=val + index_up = np.where(values >= value_quantile1)[0] + if indexes is not None: + index_up = indexes[index_up] + description_up = str(self.name) + " >= " + str(value_quantile1) + items.append(create_item(index_up,variable_name= self.name, min_val=value_quantile1, max_val=np.inf, + description = description_up,number_operations=1)) + cardinality_operator[1] += 1 + # conditions val1 <= x < val2 + for value_quantile2 in value_quantiles[iq + 1:]: + index_interval = np.where((values >= value_quantile1) & (values < value_quantile2))[0] + if indexes is not None: + index_interval = indexes[index_interval] + description_interval = str(value_quantile1) + " <= " + str(self.name) + " < " + str(value_quantile2) + items.append(create_item(index_interval,variable_name= self.name, min_val=value_quantile1, + max_val=value_quantile2,description = description_interval, + number_operations=2)) + cardinality_operator[2] += 1 + return items,cardinality_operator + + def generate_items(self,bitset_uncovered) -> Iterator[Item]: + #TODO: make dynamic generation of items based on "candidate" + if self.discretization == 'static': + for item in self.items: + yield item + elif self.discretization == 'dynamic': + indexes = np.array(bitset2indexes(bitset_uncovered)) + items, cardinality_operator = self.create_items(indexes) + for item in items: + yield item + + +def create_items_old_copy(self, values) -> Tuple[List[Item], Dict[int, int]]: + """ Creates a list of items from the numerical atrribute. + + Makes a list of items using equal frequency binning, ignoring NANs, based on the values of the Numeric attribute + + Returns + ---------- + List[Item] : List of Items + A list of all items based on the possible combinations of cutpoints. + """ + value_quantiles, self.n_cutpoints = find_cutpoints(values, self.n_cutpoints) + cardinality_operator = {1: 0, 2: 0} + items = [] + for iq, value_quantile1 in enumerate(value_quantiles): # makes binary intervals x=val + # condition x=val + index_up = np.where(values >= value_quantile1)[0] + description_up = str(self.name) + " >= " + str(value_quantile1) + items.append(create_item(index_up, variable_name=self.name, min_val=value_quantile1, max_val=np.inf, + description=description_up, number_operations=1)) + cardinality_operator[1] += 1 + # conditions val1 <= x < val2 + for value_quantile2 in value_quantiles[iq + 1:]: + index_interval = np.where((values >= value_quantile1) & (values < value_quantile2))[0] + description_interval = str(value_quantile1) + " <= " + str(self.name) + " < " + str(value_quantile2) + items.append(create_item(index_interval, variable_name=self.name, min_val=value_quantile1, + max_val=value_quantile2, description=description_interval, + number_operations=2)) + cardinality_operator[2] += 1 + return items, cardinality_operator \ No newline at end of file diff --git a/build/lib/rulelist/datastructure/data.py b/build/lib/rulelist/datastructure/data.py new file mode 100644 index 0000000..4f07350 --- /dev/null +++ b/build/lib/rulelist/datastructure/data.py @@ -0,0 +1,87 @@ +from dataclasses import dataclass, field +from typing import List, AnyStr + +import pandas as pd +from pandas.api.types import is_numeric_dtype + +from rulelist.datastructure.attribute.attribute import Attribute +from rulelist.datastructure.attribute.nominal_attribute import NominalAttribute +from rulelist.datastructure.attribute.numeric_attribute import NumericAttribute +from rulelist.rulelistmodel.categoricalmodel.categoricaltarget import CategoricalTarget +from rulelist.rulelistmodel.gaussianmodel.gaussiantarget import GaussianTargets + +#TODO: add location and multivariate gaussian +init_target = { + "gaussian" : GaussianTargets, + "spread" : GaussianTargets, + "categorical" : CategoricalTarget +}; + +@dataclass +class Data: + """ + Contains all information regarding the descriptive variables of the dataset. + + It will be composed of a list of attributes, each relating to a variable and its characteristics. + + Attributes + ---------- + datastructure : pd.DataFrame + It contains a view to the original input dataset. + attributes : List[Attribute] + A list of the variables and its characteristics. + + Methods + ------- + init_attributes + Initializes all the attributes with their respective values. + """ + input_data : pd.DataFrame + n_cutpoints : int + discretization: AnyStr #Literal["static", "sequential", "dynamic"] + target_data: pd.DataFrame + target_model: AnyStr #Literal["gaussian", "single-nominal"] + min_support: int + attributes: List[Attribute] = field(default_factory=list, init=False) + number_attributes: int = field(init=False) + attribute_names: set = field(init=False) + target_names: set = field(init=False) + targets_info: classmethod = field(init=False) + number_targets: int = field(init=False) + number_instances: int = field(init=False) + def __post_init__(self): + self.input_data = pd.DataFrame(self.input_data) #in case it is a series it will be transformed to DataFrame + self.target_data = pd.DataFrame(self.target_data) + if self.input_data.shape[0] != self.target_data.shape[0]: + raise IndexError('Input datastructure and Target datastructure should have the same number of instances') + self.attribute_names = set(self.input_data.columns) + self.target_names = set(self.target_data.columns) + self.attributes = self._init_attributes() + self.number_attributes = len(self.attributes) + self.targets_info = init_target[self.target_model](self.target_data) + self.number_targets = self.target_data.shape[1] + self.number_instances = len(self.input_data.index) + + def _init_attributes(self) -> List[Attribute]: + """ Initializes all attributes based on their values and type. + + It uses pandas.api.types function is_numeric_dtype to identify if a variable is single-numeric or not. + This means that nominal variables cannot be integer values, as it happens usually. Another possibility to deal + with nominal variables that are integers is to convert them directly in the DataFrame to single-nominal or + to object dtype. + + Returns + ---------- + attributes : List[Attribute] + It returns a list of attributes already initialized. + """ + #self.attributes = list() # clean in case it has previous values + #TODO: stop hardcoding max_operators and ask to the user, specially for nominal! + for name, values in self.input_data.items(): + if is_numeric_dtype(self.input_data[name]): + max_operators = 2 + self.attributes.append(NumericAttribute(name, values.to_numpy(), max_operators,self.min_support, self.n_cutpoints, self.discretization)) + else: # Nominal or Binary + max_operators = 1 + self.attributes.append(NominalAttribute(name, values.to_numpy(), max_operators,self.min_support)) + return self.attributes diff --git a/build/lib/rulelist/datastructure/subgroup.py b/build/lib/rulelist/datastructure/subgroup.py new file mode 100644 index 0000000..f06ff57 --- /dev/null +++ b/build/lib/rulelist/datastructure/subgroup.py @@ -0,0 +1,70 @@ +from copy import deepcopy +from functools import reduce +from typing import List + +import numpy as np +from gmpy2 import mpz, popcount + +from rulelist.datastructure.attribute.attribute import Item + + +class Subgroup(): + """ + Describes a Subgroup, which is a Logical combination of items. + + Attributes + ---------- + pattern : List[Item] + A list of items that compose the pattern. + statistic : List[Any] + List of the statistics for each target value. + delta_data : float + The local improvement in the datastructure encoding of adding this subgroup to the rule list. + delta_model : float + The local improvement (always negative) in the model encoding of adding this subgroup to the rule list. + score : float + A weighted composition of (delta_data + delta_model). If divided by the usage it equals the normalized alpha_gain, + and if not it equals the absolute alpha_gain. + usage : int + Number of instances covered by the description of the subgroup given its position in the rule list. + support : List[Item] + Number of instances covered by the description of subgroup, treating the subgroup as independent from the rule + list. + bitarray : mpz + Bit array of covered instances. popcount(bitarray) = support + + Methods + ------- + _compute_bitarray + Computes the bitarray and support from the list of items. + + """ + def __init__(self): + self.pattern = [] + self.statistics = None + self.delta_data = np.NINF + self.delta_model = np.NINF + self.score = np.NINF + self.usage = 0 + self.variable_list = [] + self.support = 0 + self.bitarray = mpz() + self.size = 0 + + def update(self,new_candidate,new_subgroup_statistics,gain_data, gain_model, score): + self.pattern = new_candidate + self.statistics = deepcopy(new_subgroup_statistics) + self.usage = self.statistics.usage + self.delta_data = gain_data + self.delta_model = gain_model + self.score = score + # Note that the bitarray only consider the pattern alone, not in the ordered rule list + self.bitarray, self.support = self._compute_bitarray() + self.variable_list = {item.parent_variable for item in self.pattern} + self.size = len(self.pattern) + return self + + def _compute_bitarray(self): + self.bitarray = reduce(lambda x, y: x & y, [item.bitarray for item in self.pattern]) + self.support = popcount(self.bitarray) + return self.bitarray, self.support \ No newline at end of file diff --git a/build/lib/rulelist/mdl/__init__.py b/build/lib/rulelist/mdl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/mdl/mdl_base_codes.py b/build/lib/rulelist/mdl/mdl_base_codes.py new file mode 100644 index 0000000..0cc1457 --- /dev/null +++ b/build/lib/rulelist/mdl/mdl_base_codes.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 8 16:34:06 2019 + +@author: gathu +""" + +from functools import lru_cache +from math import log, ceil, sqrt, log2 + +from scipy.special import comb,perm, gammaln + +from rulelist.util.extra_maths import log2_0 + + +@lru_cache(maxsize=20000,typed=True) +def log_multinomial(cardinality, n): + return log2_0(multinomial_with_recurrence(cardinality, n)) + +def multinomial_with_recurrence(cardinality, n): + """ Computes the Normalized Maximum Likelihood (NML) code length + cardinality - number of categories for a single-nominal or multinomial distribution + n - number of points / samples + complexity - COMP(cardinality,n) - the complexity (without logarithm) + """ + complexity = 1.0 + b = 1.0 + d = 10 # seven digit precision + if cardinality == 1: + complexity = 1.0 + elif n == 0: + complexity = 0 + else: + bound = int(ceil(2 + sqrt(2 * n * d * log(10)))) # using equation (38) + for k in range(1, bound + 1): + b = (n - k + 1) / n * b + complexity += b + old_sum = 1.0 + for j in range(3, cardinality + 1): + new_sum = complexity + (n * old_sum) / (j - 2) + old_sum = complexity + complexity = new_sum + return complexity + +@lru_cache(maxsize=20000,typed=True) +def universal_code_integers(value: int) -> float: + """ computes the universal code of integers + """ + const = 2.865064 + logsum = log2(const) + cond = True # condition + if value == 0: + logsum = 0 + elif value > 0: + while cond: # Recursive log + value = log2(value) + cond = value > 0.000001 + if value < 0.000001: + break + logsum += value + elif value < 0: + raise ValueError('n should be larger than 0. The value was: {}'.format(value)) + return logsum + +@lru_cache(maxsize=20000,typed=True) +def log2_gamma_half(n: int): + le2 = 0.6931471805599453 # log(2) + return gammaln(n / 2) / le2 if n > 0 else 0 + +def universal_code_integers_maximum(n: int, maximum : int) -> float: + """ computes the universal code of integers when there is a known maximum integer + This is equivalent to applying the maximum entropy principle knowing the maximum, + and it equalitarian division of the non-used probability (the ones after the maximum) + by all the used number (1 until maximum). + """ + probability_until_max = sum([2**-universal_code_integers(n_aux) for n_aux in range(1,maximum+1)]) + probability_left = 1 - probability_until_max + probability_n = 2**-universal_code_integers(n)+ probability_left/maximum + logsum = -log2(probability_n) + return logsum + +def uniform_code(n: int) -> float: + return log2(n) if n != 0 else 0 + +def uniform_combination_code(n: int, maximum: int) -> float: + """ Code based on n-combination of maximum. + This code is used when order of the elements does not matter. + + :param n: + :param maximum: + :return: + """ + return log2(comb(maximum, n)) + +def uniform_permutation_code(n: int, maximum: int) -> float: + """ Code based on n-permutations of maximum. + This code is used when order of the elements matters. + + :param n: + :param maximum: + :return: + """ + return log2(perm(maximum, n)) \ No newline at end of file diff --git a/build/lib/rulelist/measures/__init__.py b/build/lib/rulelist/measures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/measures/mesaures_classification.py b/build/lib/rulelist/measures/mesaures_classification.py new file mode 100644 index 0000000..3fab513 --- /dev/null +++ b/build/lib/rulelist/measures/mesaures_classification.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 15 13:11:15 2019 + +@author: gathu +""" + diff --git a/build/lib/rulelist/measures/subgroup_measures.py b/build/lib/rulelist/measures/subgroup_measures.py new file mode 100644 index 0000000..d4d7b12 --- /dev/null +++ b/build/lib/rulelist/measures/subgroup_measures.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 15 13:10:10 2019 + +@author: gathu +""" +from itertools import combinations +from math import exp, log2 + +import numpy as np +from gmpy2 import xmpz, mpz, popcount + +from rulelist.mdl.mdl_base_codes import log2_0 + + +def jaccard_index_model(list_bitsets): + nrules = len(list_bitsets) + if nrules < 2: + return 0, None + else: + intersect = np.zeros([nrules,nrules],dtype = np.uint) + for r1 in range(nrules): + tid_rule1 = list_bitsets[r1] + for r2 in range(nrules): + tid_rule2 = list_bitsets[r2] + intersect[r1,r2] = popcount(tid_rule1 & tid_rule2) + jaccard = np.zeros([nrules,nrules]) + for rr in combinations(range(nrules), 2): + inter = intersect[rr] + supp1 = intersect[(rr[0],rr[0])] + supp2 = intersect[(rr[1],rr[1])] + jaccard[rr]= inter/(supp1+supp2-inter) + uptm = np.triu_indices(nrules, 1) + jacc_avg = np.sum(jaccard) / len(uptm[0]) + return jacc_avg, jaccard + +def wracc_numeric(data_mean,data_var, values): + usage = len(values) + if usage > 0: + sg_mean = np.mean(values) + wracc = usage * np.absolute(sg_mean - data_mean) + else: + wracc = 0 + return wracc + +def kullbackleibler_gaussian_paramters(data_mean,data_var, values): + usage = len(values) + RSS = sum([(val - data_mean) ** 2 for val in values]) + variance = np.var(values) if usage > 2 else 0 + l_e = log2(exp(1)) + if usage > 2 and variance != 0: + kl_aux1 = 0.5 * log2_0(data_var) + \ + 0.5 * RSS / usage / data_var*l_e + kl_aux2 = 0.5*log2_0(variance)+0.5*l_e + kl = kl_aux1 - kl_aux2 + wkl = usage*kl + else: + wkl = 0 + return wkl + +def numeric_single2multitargets_function(function2multi, data_mean, data_var,values, number_targets): + sum_score_targets = 0 + for ntarget in range(number_targets): + columnvalues = values[:,ntarget] if number_targets > 1 else values + sum_score_targets += function2multi(data_mean[ntarget],data_var[ntarget], columnvalues) + return sum_score_targets + + +def numeric_discovery_measures(rulelist,X,Y): + nrules= rulelist.number_rules + if nrules == 0: + measures = dict() + measures["avg_supp"] = measures["wkl_supp"] = measures["avg_usg"] = measures["wkl_usg"] = measures["wacc_supp"] =\ + measures["wacc_usg"] = measures["jacc_avg"] =measures["n_rules"] = measures["avg_items"] = measures["wkl_sum"] = \ + measures["wkl_sum_norm"] = measures["wacc_supp_sum"] = measures["wacc_usg_sum"] = measures["std_rules"] =\ + measures["top1_std"] = measures["length_orig"] = measures["length_final"] = measures["length_ratio"] = 0 + #nrows= len(rulelist.target_values) + wkl_supp,wkl_usg,wkl_sum = np.zeros(nrules), np.zeros(nrules), np.zeros(nrules) + wacc_supp, wacc_usg = np.zeros(nrules),np.zeros(nrules) + support, usage = np.zeros(nrules),np.zeros(nrules) + std_rules = [var1target ** 0.5 for sg in rulelist.subgroups for var1target in sg.statistics.variance] + std_rulesalternative = [] + data_mean = rulelist.default_rule_statistics.mean + data_var = rulelist.default_rule_statistics.variance + tid_covered = mpz() + list_bitsets = [] + number_targets = len(rulelist.subgroups[0].statistics.mean) + for r in range(nrules): + tid_support = rulelist.subgroups[r].bitarray + list_bitsets.append(tid_support) + tid_usage = tid_support & ~ tid_covered + tid_covered = tid_covered | tid_support + aux_bitset = xmpz(tid_support) + idx_bits = list(aux_bitset.iter_set()) + values_support = Y.iloc[idx_bits, :].values + aux_bitset = xmpz(tid_usage) + idx_bits = list(aux_bitset.iter_set()) + values_usage = Y.iloc[idx_bits, :].values + support[r] = values_support.shape[0] + usage[r] = values_usage.shape[0] + wkl_supp[r] = numeric_single2multitargets_function(kullbackleibler_gaussian_paramters,data_mean, data_var, + values_support,number_targets) + wkl_usg[r] = numeric_single2multitargets_function(kullbackleibler_gaussian_paramters,data_mean, data_var, + values_usage,number_targets) + std_rulesalternative.append(np.std(values_usage)) + wacc_supp[r] = numeric_single2multitargets_function(wracc_numeric,data_mean, data_var,values_support,number_targets) + wacc_usg[r] = numeric_single2multitargets_function(wracc_numeric,data_mean, data_var,values_usage,number_targets) + + + wkl_sum = sum(wkl_usg) + # Average them all!!!! + measures = dict() + measures["avg_supp"] = np.mean(support) + measures["wkl_supp"] = np.mean(wkl_supp) + measures["avg_usg"] = np.mean(usage) + measures["wkl_usg"] = np.mean(wkl_usg) + measures["wacc_supp"] = np.mean(wacc_supp) + measures["wacc_usg"] = np.mean(wacc_usg) + measures["jacc_avg"], jaccard_matrix = jaccard_index_model(list_bitsets) + measures["n_rules"] = rulelist.number_rules + measures["avg_items"] = sum([len(sg.pattern) for sg in rulelist.subgroups]) / rulelist.number_rules + measures["wkl_sum"] = wkl_sum + measures["wkl_sum_norm"] = wkl_sum/X.shape[0] + measures["wacc_supp_sum"] = np.sum(wacc_supp) + measures["wacc_usg_sum"] = np.sum(wacc_usg) + measures["std_rules"] = np.mean(std_rules) + measures["top1_std"] =std_rules[0] + measures["length_orig"] = rulelist.length_original + measures["length_final"] = rulelist.length_data + rulelist.length_model + measures["length_ratio"] = rulelist.length_ratio + + return measures + + +def wkl_wracc(data_probs,values,number_instances, number_targets): + sum_wracc_targets = 0 + sum_wkl_targets = 0 + usage = values.shape[0] + if usage: + for ntarget, target_variable in enumerate(data_probs): + columnvalues = values[:,ntarget] if number_targets > 1 else values + number_classes = len(data_probs[target_variable]) + aux_wacc_score = 0 + for category, prob_default in data_probs[target_variable].items(): + #wracc + counts_category = sum(columnvalues == category) + prob_rule = counts_category/usage + aux_wacc_score += (usage / number_instances) * abs(prob_rule - prob_default) + #wkl + sum_wkl_targets += usage*prob_rule*log2_0(prob_rule/prob_default) + + sum_wracc_targets += aux_wacc_score/number_classes + return sum_wkl_targets, sum_wracc_targets + + +def nominal_discovery_measures(rulelist,X,Y): + nrules= rulelist.number_rules + nrows= X.shape[0] + data_prob_class = rulelist.default_rule_statistics.prob_per_classes + wkl_supp,wkl_usg,wkl_sum = np.zeros(nrules), np.zeros(nrules), np.zeros(nrules) + wacc_supp, wacc_usg = np.zeros(nrules),np.zeros(nrules) + support, usage = np.zeros(nrules),np.zeros(nrules) + tid_covered = mpz() + list_bitsets = [] + number_targets = len(rulelist.default_rule_statistics.prob_per_classes) + for r in range(nrules): + tid_support = rulelist.subgroups[r].bitarray + list_bitsets.append(tid_support) + tid_usage = tid_support & ~ tid_covered + tid_covered = tid_covered | tid_support + aux_bitset = xmpz(tid_support) + idx_bits = list(aux_bitset.iter_set()) + values_support = Y.iloc[idx_bits, :].values + aux_bitset = xmpz(tid_usage) + idx_bits = list(aux_bitset.iter_set()) + values_usage = Y.iloc[idx_bits, :].values + support[r] = values_support.shape[0] + usage[r] = values_usage.shape[0] + wkl_supp[r], wacc_supp[r] = wkl_wracc(data_prob_class,values_support,nrows, number_targets) + wkl_usg[r], wacc_usg[r] = wkl_wracc(data_prob_class,values_usage,nrows, number_targets) + + wkl_sum = sum(wkl_usg) + # Average them all!!!! + measures = dict() + measures["avg_supp"] = np.mean(support) + measures["wkl_supp"] = np.mean(wkl_supp) + + measures["avg_usg"] = np.mean(usage) + measures["wkl_usg"] = np.mean(wkl_usg) + + measures["wacc_supp"] = np.mean(wacc_supp) + measures["wacc_usg"] = np.mean(wacc_usg) + + + + measures["jacc_avg"], jaccard_matrix = jaccard_index_model(list_bitsets) + measures["n_rules"] = rulelist.number_rules + measures["avg_items"] = sum([len(sg.pattern) for sg in rulelist.subgroups]) / rulelist.number_rules + measures["wkl_sum"] = wkl_sum + measures["wkl_sum_norm"] = wkl_sum/X.shape[0] + + measures["wacc_supp_sum"] = np.sum(wacc_supp) + measures["wacc_usg_sum"] = np.sum(wacc_usg) + + measures["length_orig"] = rulelist.length_original + measures["length_final"] = rulelist.length_data + rulelist.length_model + measures["length_ratio"] = rulelist.length_ratio + return measures + + + + +def discovery_itemset(data,model): + nrules = model.number_rules + cl = model.class_codes + rules_supp = {nr: {c: int(0) for c in cl} for nr in range(nrules)} + rules_usg = {nr: {c: int(0) for c in cl} for nr in range(nrules)} + count_cl = {c: int(0) for c in cl} + pred = [] + prob = [] + RULEactivated = [] + intersect = np.zeros([nr,nr],dtype = np.uint) + jaccard = np.zeros([nr,nr]) + # Find majority class + for t in data: + active_r = list() + first = True + for r in range(nrules): + if model[r]['p'] <= t and first: + pred.append(model[r]['cl']) + prob.append(model[r][model[r]['cl']]) + RULEactivated.append(r) + active_r.append(r) + intersect[r,r] +=1 + for ic, c in enumerate(cl): + if c <= t: + rules_supp[r][c] +=1 + rules_usg[r][c] +=1 + count_cl[c] +=1 + first = False + elif model[r]['p'] <= t and not first: + active_r.append(r) + intersect[r,r] +=1 + for ic, c in enumerate(cl): + if c <= t: + rules_supp[r][c] +=1 + for rr in combinations(active_r, 2): + intersect[rr] +=1 + + for rr in combinations(range(nr), 2): + inter = intersect[rr] + supp1 = intersect[(rr[0],rr[0])] + supp2 = intersect[(rr[1],rr[1])] + jaccard[rr]= inter/(supp1+supp2-inter) + + # remove empty rule column and row + jaccard = np.delete(jaccard, -1, 0) + jaccard = np.delete(jaccard, -1, 1) + # average over all possible cases + uptm = np.triu_indices(nr-1,1) + jacc_avg = np.sum(jaccard)/len(uptm[0]) + jacc_consecutive_avg = np.mean(np.diagonal(jaccard,1)) + avg_supp = np.mean([sum([rules_supp[r][c] for c in cl]) for r in range(nr-1)]) + avg_usg = np.mean([sum([rules_usg[r][c] for c in cl]) for r in range(nr-1)]) + + return pred, prob, RULEactivated,rules_supp,rules_usg,count_cl,jacc_avg,avg_supp,avg_usg \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/__init__.py b/build/lib/rulelist/rulelistmodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/rulelistmodel/categoricalmodel/__init__.py b/build/lib/rulelist/rulelistmodel/categoricalmodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricalrulelist.py b/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricalrulelist.py new file mode 100644 index 0000000..5c9d575 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricalrulelist.py @@ -0,0 +1,85 @@ +from rulelist.rulelistmodel.categoricalmodel.categoricalstatistic import CategoricalFreeStatistic, CategoricalFixedStatistic +from rulelist.rulelistmodel.categoricalmodel.mdl_categorical import length_rule_fixed_categorical, \ + length_rule_free_categorical +from rulelist.rulelistmodel.rulesetmodel import RuleSetModel +from rulelist.util.extra_maths import log2_0 + +default_rule_statistic_categorical = { + "discovery": CategoricalFixedStatistic, + "prediction": CategoricalFreeStatistic +} + +compute_default_length_categorical = { + "discovery": length_rule_fixed_categorical, + "prediction": length_rule_free_categorical +} + + +class CategoricalRuleList(RuleSetModel): + """ Categorical rule list model + + """ + + def __init__(self, data, task, max_depth,beam_width,min_support, max_rules, alpha_gain): + self.max_depth, self.l_combination_pattern, self.l_attribute_item, self.log_prior_class= \ + self._create_constants(data,max_depth) + super().__init__(data, task, max_depth,beam_width,min_support, max_rules, alpha_gain) + self.min_support = max(min_support, 4) + + def init_default_statistics(self, data): + return default_rule_statistic_categorical[self.task](data) + + def init_subgroup_statistics(self, data): + return CategoricalFreeStatistic(data) + + def compute_default_length(self, default_rule_statistics): + return compute_default_length_categorical[self.task](self, default_rule_statistics) + + def _create_constants(self, data,max_depth): + self.max_depth, self.l_combination_pattern, self.l_attribute_item =\ + RuleSetModel._create_constants(self,data,max_depth) + # compute nml normalizing constant + #self.log_nml_comp = {(n_points, n_classes): log2(multinomial_with_recurrence(n_classes,n_points)) + # if n_points != 0 else 0 for n_points in range(0,datastructure.number_instances+1) + # for n_classes in datastructure.targets_info.number_classes.values()} + self.log_prior_class = {varname: + {category: -log2_0(count/data.number_instances) for category,count in counts.items()} + for varname, counts in data.targets_info.counts.items()} + return self.max_depth, self.l_combination_pattern, self.l_attribute_item, self.log_prior_class + + def add_description(self): + self.description = self._add_description_rules() + self._add_description_lastrule() + return self + + def _add_description_rules(self): + text2add = "" + for isub, subgroup in enumerate(self.subgroups): + text2add += "If" if isub == 0 else "ELSE IF" + for iit, item in enumerate(subgroup.pattern): + text2add += " " + item.description + " " + text2add += " AND " if iit < len(subgroup.pattern)-1 else "" + text2add += " THEN " + \ + " usage = " + str(subgroup.statistics.usage) + n = subgroup.usage + for varname, usage_per_class in subgroup.statistics.usage_per_class.items(): + text2add += " : target = {}".format(varname) + text2add += "".join(["Pr({}) = {};".format(category,n_class/n) + for category, n_class in usage_per_class.items()]) + text2add += "".join("\n") + return text2add + + def _add_description_lastrule(self): + text2add = " ELSE " +\ + " usage = " + str(self.default_rule_statistics.usage) + n = self.default_rule_statistics.usage + if self.task == "discovery": + for varname, prob_per_class in self.default_rule_statistics.prob_per_classes.items(): + text2add += " : target = {}".format(varname) + text2add += "".join(["Pr({}) = {};".format(category, prob) + for category, prob in prob_per_class.items()]) + else: + for varname, usage_per_class in self.default_rule_statistics.usage_per_class.items(): + text2add += " : target = {}".format(varname) + text2add += "".join(["Pr({}) = {};".format(category, n_class / n) + for category, n_class in usage_per_class.items()]) + return text2add \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricalstatistic.py b/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricalstatistic.py new file mode 100644 index 0000000..b77ea0e --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricalstatistic.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass, field +from typing import Dict, Any + +from gmpy2 import popcount + +from rulelist.datastructure.data import Data +from rulelist.rulelistmodel.statistic import Statistic + + +@dataclass(repr=True, eq=False, order=False, unsafe_hash=True, frozen=False) +class CategoricalFixedStatistic(Statistic): + usage_per_class : Dict[Any, Dict[Any, int]] = field(init=False) + number_classes : Dict[Any, int] = field(init=False) + prob_per_classes : Dict[Any, Dict[Any, float]] = field(init=False) + def __post_init__(self, data: Data): + self.usage, self.number_targets = Statistic.__post_init__(self, data) + self.number_classes = data.targets_info.number_classes + self.usage_per_class = {varname: dict() for varname in data.target_names} + self.prob_per_classes = data.targets_info.prob_var_class + + def replace_stats(self,data,indices_bitarray): + self.update_usage(indices_bitarray) + for varname, bit_arrays_class in data.targets_info.bit_arrays_var_class.items(): + for category in data.targets_info.categories[varname]: + self.usage_per_class[varname][category] = popcount(indices_bitarray & bit_arrays_class[category]) + return self + +@dataclass(repr=True, eq=False, order=False, unsafe_hash=True, frozen=False) +class CategoricalFreeStatistic(Statistic): + usage_per_class : Dict[Any, Dict[Any, int]] = field(init=False) + number_classes : Dict[Any, int] = field(init=False) + def __post_init__(self, data: Data): + self.usage, self.number_targets = Statistic.__post_init__(self, data) + self.number_classes = data.targets_info.number_classes + self.usage_per_class = {varname: dict() for varname in data.target_names} + + def replace_stats(self,data, index_bitarray): + self.usage = self.update_usage(index_bitarray) + for varname, bit_arrays_class in data.targets_info.bit_arrays_var_class.items(): + for category in data.targets_info.categories[varname]: + self.usage_per_class[varname][category] = popcount(index_bitarray & bit_arrays_class[category]) + return self diff --git a/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py b/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py new file mode 100644 index 0000000..afba47a --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py @@ -0,0 +1,63 @@ + +from dataclasses import dataclass, field, InitVar +from typing import Any, Dict, Tuple + +import numpy as np +import pandas as pd +from gmpy2 import mpz, bit_mask + +from rulelist.util.bitset_operations import indexes2bitset + + +@dataclass +class CategoricalTarget: + """ + Describes a nominal target variable approximated by a single-nominal distribution, defined by the counts per category. + + Attributes + ---------- + categories : List[Any] + List of the categories of the nominal variable. + bit_array : Dict[gmpy2.mpz] + A dictionary of bit_arrays, one for each category + counts : Dict[int] + Number of counts per category. + + Parameters + ---------- + Target : a generic class object that represents a target variable. + + """ + target_values : InitVar[pd.DataFrame] + categories : Dict[Any, np.ndarray] = field(init=False) + number_classes: Dict[Any, int] = field(init=False) + bit_array: mpz = field(default=mpz(0), init=False) + bit_arrays_var_class: Dict[Any, Dict[Any, mpz]] = field(default_factory=dict, init=False) + counts: Dict[Any, np.ndarray] = field(default_factory=dict,init=False) + prob_var_class : Dict[Any, Dict[Any, float]] = field(default_factory=dict,init=False) + def __post_init__(self, target_values): + self.bit_array = bit_mask(target_values.shape[0]) + self.categories = {colname: colvals.unique() for colname, colvals in target_values.items()} #ignores NANs values + self.number_classes = {colname: len(array_uniques) for colname, array_uniques in self.categories.items()} + if any([nunique == 1 for nunique in self.number_classes.values()]): + raise ValueError("There is at least one target variable with only one class label. Please only add targets with 2 or more class labels.") + self.bit_arrays_var_class, self.counts, self.prob_var_class = self.init_bitarrays_class(target_values) + + def init_bitarrays_class(self, target_values) -> Tuple[Dict[Any, np.ndarray],Dict[Any, np.ndarray]] : + """ Initializes the bit array values for each category. + + Returns + ---------- + Dict[gmpy2.mpz] : + A dictionary of the bitarray values. + """ + for namecol, colvals in target_values.items(): + self.bit_arrays_var_class[namecol] = dict() + self.counts[namecol] = dict() + self.prob_var_class[namecol] = dict() + for icat, category in enumerate(self.categories[namecol]): + category_indexes = np.where(colvals.values == category)[0] + self.bit_arrays_var_class[namecol][category] = indexes2bitset(category_indexes) + self.counts[namecol][category] = len(category_indexes) + self.prob_var_class[namecol][category] = self.counts[namecol][category]/target_values.shape[0] + return self.bit_arrays_var_class, self.counts, self.prob_var_class diff --git a/build/lib/rulelist/rulelistmodel/categoricalmodel/mdl_categorical.py b/build/lib/rulelist/rulelistmodel/categoricalmodel/mdl_categorical.py new file mode 100644 index 0000000..620402a --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/categoricalmodel/mdl_categorical.py @@ -0,0 +1,25 @@ +from rulelist.mdl.mdl_base_codes import log_multinomial +from rulelist.rulelistmodel.categoricalmodel.categoricalstatistic import CategoricalFreeStatistic,CategoricalFixedStatistic +from rulelist.util.extra_maths import log2_0 + + +def categorical_free_encoding(statistics, varname): + codelength = statistics.usage*log2_0(statistics.usage) + codelength -= sum([n_class*(log2_0(n_class)) for n_class in statistics.usage_per_class[varname].values()]) + codelength += log_multinomial(statistics.number_classes[varname],statistics.usage) + return codelength + +def categorical_fixed_encoding(rulelist, statistics, varname): + codelength = sum([n_class*(rulelist.log_prior_class[varname][category]) + for category, n_class in statistics.usage_per_class[varname].items()]) + return codelength + +def length_rule_free_categorical(rulelist : classmethod, statistics : CategoricalFreeStatistic): + l_free = sum([categorical_free_encoding(statistics, varname) + for varname in statistics.usage_per_class.keys()]) + return l_free + +def length_rule_fixed_categorical(rulelist : classmethod, statistics : CategoricalFixedStatistic): + l_fixed = sum([categorical_fixed_encoding(rulelist, statistics, varname) + for varname, counts_per_class in statistics.usage_per_class.items()]) + return l_fixed \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/categoricalmodel/prediction_categorical.py b/build/lib/rulelist/rulelistmodel/categoricalmodel/prediction_categorical.py new file mode 100644 index 0000000..9cf9448 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/categoricalmodel/prediction_categorical.py @@ -0,0 +1,23 @@ +import numpy as np + +def point_value_categorical(statistics): + class_labels = np.array([max(count_per_class.keys(), key=(lambda k: count_per_class[k])) + for varname, count_per_class in statistics.usage_per_class.items()]) + return class_labels + +def probability_categorical(statistics,target): + """ Computes the probability with laplace smoothing. + + Adds a little pseudocount (epsilon) which makes for a more balanced probability. + An epsilon of 0.5 is the Jeffrey's prior for Dirichlet's distribution, and an epsilon of 1 is the uniform prior. + + :param statistics: + :param target: + :return: + """ + usage = statistics.usage + n_classes = statistics.number_classes[target] + epsilon = 0.5 + probabilities = np.array([(usg_cl+epsilon)/(usage+epsilon*n_classes) + for usg_cl in statistics.usage_per_class[target].values()]) + return probabilities diff --git a/build/lib/rulelist/rulelistmodel/data_encoding.py b/build/lib/rulelist/rulelistmodel/data_encoding.py new file mode 100644 index 0000000..473c8c4 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/data_encoding.py @@ -0,0 +1,16 @@ +from rulelist.rulelistmodel.categoricalmodel.mdl_categorical import length_rule_free_categorical +from rulelist.rulelistmodel.gaussianmodel.mdl_gaussian import length_rule_free_gaussian + +length_rule_free = { + "gaussian": length_rule_free_gaussian, + "categorical": length_rule_free_categorical +} + +def compute_length_data(rulelist): + """ Computes the code length of the whole rule list. + """ + l_data_subgroups = sum([length_rule_free[rulelist.target_model](rulelist, subgroup.statistics) + for subgroup in rulelist.subgroups]) + l_data = l_data_subgroups + rulelist.length_defaultrule + return l_data + diff --git a/build/lib/rulelist/rulelistmodel/gain_add_rule.py b/build/lib/rulelist/rulelistmodel/gain_add_rule.py new file mode 100644 index 0000000..7250f31 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/gain_add_rule.py @@ -0,0 +1,54 @@ +from rulelist.mdl.mdl_base_codes import universal_code_integers +from rulelist.rulelistmodel.categoricalmodel.mdl_categorical import length_rule_free_categorical +from rulelist.rulelistmodel.gaussianmodel.mdl_gaussian import length_rule_free_gaussian + +length_rule_free = { + "gaussian": length_rule_free_gaussian, + "categorical": length_rule_free_categorical +} + +def compute_delta_data(rulelist, new_subgroup_statistics, new_default_rule_statistics): + """ Computes the alpha_gain (delta) in code length of adding one rule two the model. + + It needs 3 components: + + """ + l_subgroup = length_rule_free[rulelist.target_model](rulelist, new_subgroup_statistics) + l_newdefault = rulelist.compute_default_length(new_default_rule_statistics) + gain = rulelist.length_defaultrule - l_newdefault - l_subgroup + return gain + +def compute_delta_model(rulelist, new_candidate): + """ Computes the alpha_gain (delta) of adding a new candidate to the rule list. + Notice that a positive alpha_gain means that the overall length of the rule list diminishes by adding the new candidate. + Model Gain is always negative as adding something to the rule list can only increase the model complexity. + """ + #delta_rules = rulelist.l_universal[rulelist.number_rules] - rulelist.l_universal[rulelist.number_rules+1] + #l_pattern_length = rulelist.l_universal[len(new_candidate)] + delta_rules = universal_code_integers(rulelist.number_rules) - universal_code_integers(rulelist.number_rules + 1) + l_pattern_length = universal_code_integers(len(new_candidate)) + l_pattern_combination = rulelist.l_variables_in_pattern[len(new_candidate)] + l_items = sum([rulelist.l_attribute_item[(item.parent_variable, item.number_operators)] + for item in new_candidate]) + gain_model = delta_rules - l_pattern_length - l_pattern_combination - l_items + return gain_model + +def compute_delta_score(rulelist, new_candidate, new_subgroup_statistics, new_default_rule_statistics): + delta_data = compute_delta_data(rulelist, new_subgroup_statistics, new_default_rule_statistics) + delta_model = compute_delta_model(rulelist, new_candidate) + usage = new_subgroup_statistics.usage + delta_score = (delta_data+delta_model) / (usage**rulelist.alpha_gain) + return delta_score, delta_data, delta_model + + +def compute_statistics_newrules(rulelist, data, bitarray_subgroup): + """ Computes the statistics of 3 rules: + 1. the new subgroup rule + 2. the old "default" rule that covered the subgroup (only the cover of the subgroup not the rest) + 3. the new default rule that covers the region not covered by any subgroup. + + """ + rulelist.tmp_subgroup_statistic = rulelist.tmp_subgroup_statistic.replace_stats(data, bitarray_subgroup) + bitarray_new_defaultrule =rulelist.bitset_uncovered &~ bitarray_subgroup + rulelist.tmp_default_statistic = rulelist.tmp_default_statistic.replace_stats(data, bitarray_new_defaultrule) + return rulelist.tmp_subgroup_statistic, rulelist.tmp_default_statistic \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/gaussianmodel/__init__.py b/build/lib/rulelist/rulelistmodel/gaussianmodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py b/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py new file mode 100644 index 0000000..1a629bb --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py @@ -0,0 +1,67 @@ +import numpy as np + +from rulelist.rulelistmodel.gaussianmodel.gaussianstatistic import GaussianFixedStatistic, GaussianFreeStatistic +from rulelist.rulelistmodel.gaussianmodel.mdl_gaussian import length_rule_fixed_gaussian, length_rule_free_gaussian + +from rulelist.rulelistmodel.rulesetmodel import RuleSetModel + +default_rule_statistic_gaussian = { + "discovery": GaussianFixedStatistic, + "prediction": GaussianFreeStatistic +} + +compute_default_length_gaussian = { + "discovery": length_rule_fixed_gaussian, + "prediction": length_rule_free_gaussian +} + + +class GaussianRuleList(RuleSetModel): + """ General Gaussian rule list + + """ + + def __init__(self, data, task, max_depth,beam_width,min_support, max_rules, alpha_gain): + self.max_depth, self.l_combination_pattern, self.l_attribute_item = self._create_constants(data, max_depth) + # Respect the caller-provided minimum support (tests rely on allowing zero) + super().__init__(data, task, max_depth,beam_width,min_support, max_rules, alpha_gain) + + def init_default_statistics(self, data): + return default_rule_statistic_gaussian[self.task](data) + + def init_subgroup_statistics(self, data): + return GaussianFreeStatistic(data) + + def compute_default_length(self, default_rule_statistics): + return compute_default_length_gaussian[self.task](self, default_rule_statistics) + + + def _create_constants(self, data,max_depth): + self.max_depth, self.l_combination_pattern, self.l_attribute_item =\ + RuleSetModel._create_constants(self,data,max_depth) + return self.max_depth, self.l_combination_pattern, self.l_attribute_item + + def add_description(self): + self.description = self._add_description_rules() + self._add_description_lastrule() + return self + + def _add_description_rules(self): + text2add = "" + for isub, subgroup in enumerate(self.subgroups): + text2add += "If" if isub == 0 else "ELSE IF" + for iit, item in enumerate(subgroup.pattern): + text2add += " " + item.description + " " + text2add += " AND " if iit < len(subgroup.pattern)-1 else "" + text2add += " THEN " + \ + " usage = " + str(subgroup.statistics.usage) + \ + "; mean = " + str(subgroup.statistics.mean) + \ + "; std = " + str(np.sqrt(subgroup.statistics.variance))+ \ + " \n" + return text2add + + def _add_description_lastrule(self): + text2add = " ELSE " +\ + " usage = " + str(self.default_rule_statistics.usage)+ \ + "; mean = " + str(self.default_rule_statistics.mean) + \ + "; std = " + str(np.sqrt(self.default_rule_statistics.variance)) + return text2add diff --git a/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py b/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py new file mode 100644 index 0000000..e92cee5 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py @@ -0,0 +1,218 @@ +from dataclasses import dataclass, field, InitVar +from typing import List + +import numpy as np +try: + from numba import jit +except ModuleNotFoundError: # pragma: no cover - optional acceleration + def jit(*args, **kwargs): + def decorator(func): + return func + return decorator + +from rulelist.datastructure.data import Data +from rulelist.rulelistmodel.statistic import Statistic +from rulelist.util.bitset_operations import bitset2indexes + + +#@jit(nopython=True) +def compute_mean_special(column_data, indices_subgroup): + + sum_values = 0 + for i in range(len(indices_subgroup)): + sum_values = sum_values + column_data[indices_subgroup[i]] + return sum_values/len(indices_subgroup) + +#@jit(nopython=True) +def compute_mean(values): + return np.mean(values) + +#@jit(nopython=True) +def compute_RSS(values, meanval): + c = values - meanval + RSS = np.dot(c, c) + return RSS + +#@jit(nopython=True) +def find2points(values, meandata,bigvalue): + closest = np.array([bigvalue, bigvalue]) + #closest = values[0:2] + closest[1] = closest[0]*1.10 + dif = [abs(val - meandata) for val in closest] + for x in values: + current_dif = abs(x - meandata) + if current_dif < dif[0] and x != closest[1]: + if dif[0] < dif[1] and closest[0] != x: + closest[1] = closest[0] + dif[1] = dif[0] + closest[0] = x + dif[0] = abs(x - meandata) + if abs(x - meandata) < dif[1] and x != closest[0]: + closest[1] = x + dif[1] = abs(x - meandata) + return closest, dif + +@dataclass(repr=False, eq=False, order=False, unsafe_hash=True, frozen=False) +class GaussianFixedStatistic(Statistic): + """ + Describes the statistic related to a Gaussian Distribution with fixed mean and variance and corresponding the the + mean and variance of the dataset. + + Attributes + ---------- + datastructure : InitVar[Data] + The dataclass Data that contains all the information regarding the dataset. + values : InitVar[np.ndarray] + The values on which to compute the statistics. + usage : int + Number of instances covered by the rule. + mean : List[float] + Mean of the rule for each target variable. + variance : List[float] + Variance of the rule target variable. + rss : List[float] + Residual Sum of Squares (RSS) of the rule. + fixed_parameters: bool = True + The fact that the parameters of the statistic are fixed. + + Methods + ------- + _compute_statistics_fixed : + Computes the statistics of the Gaussian necessary to compute the encoding. given the fixed values of the mean + and variance for each target. + """ + #datastructure : InitVar[Data] + #bitarray_subgroup : InitVar[list] + #usage : int = field(default=0,init=False) + mean : np.ndarray = field(init=False) + variance : np.ndarray = field(init=False) + rss : np.ndarray = field(init=False) + fixed_parameters : bool = True + def __post_init__(self, data: Data): + self.usage, self.number_targers = Statistic.__post_init__(self, data) + self.mean = data.targets_info.mean + self.variance = data.targets_info.variance + self.rss = np.empty(self.number_targers, dtype=np.float64) + + def replace_stats(self, data, bitarray_indices): + self.usage = self.update_usage(bitarray_indices) + indices_subgroup = bitset2indexes(bitarray_indices) + if data.number_targets == 1: + column_values = data.targets_info.array_data[indices_subgroup,0] + self.rss[0] = compute_RSS(column_values, self.mean) + + #mean = compute_mean_special(datastructure.target_data_test, indices_subgroup, index_column) + #self.rss[0] = compute_RSS_special(datastructure.target_data_test[:,0], indices_subgroup, self.mean) + elif data.number_targets > 1: + target_values = data.targets_info.array_data[indices_subgroup,:] + for icol, column_values in enumerate(target_values.T): + self.rss[icol] = compute_RSS(column_values, self.mean[icol]) + return self + +@dataclass(repr=False, eq=False, order=False, unsafe_hash=True, frozen=False) +class GaussianFreeStatistic(Statistic): + """ + Describes the statistic related to a Gaussian Distribution with mean and variance unknown, i.e., that they have + free parameters that have to be averaged when computing their encoding. + + + Attributes + ---------- + datastructure : InitVar[Data] + The dataclass Data taht contains all the information regarding the dataset. + values : InitVar[np.ndarray] + The values on which to compute the statistics. + usage : int + Number of instances covered by the rule. + mean : List[float] + Mean of the rule for each target variable. + variance : List[float] + Variance of the rule target variable. + rss : List[float] + Residual Sum of Squares (RSS) of the rule. + mean_2points : List[float] + Mean of the 2 points closest to the dataset mean. This value is only computed if fixed_parameters = False, as + it is necessary for the Bayesian encoding to be valid (see theory in the paper). + variance_2points : List[float] + Variance of the 2 points closest to the dataset mean. This value is only computed if fixed_parameters = False, as + it is necessary for the Bayesian encoding to be valid (see theory in the paper). + rss_2points : List[float] + The residual sum of sqsuares of using the mean of the 2 points. + mean_dataset : List[float] + Mean of the dataset for each target. + variance_dataset : List[float] + Variance of the dataset for each target. + rss_2dataset : List[float] + The residual sum of sqsuares of using the mean of the dataset to explain the 2 points. + fixed_parameters: bool = False + The fact that the parameters of the statistic are unkown a priori. + + Methods + ------- + _compute_statistics_free : + Computes the statistics of the Gaussian necessary to compute the encoding. given the values assuming that the + value of the statistics are unkown (not fixed). + """ + #datastructure : InitVar[Data] + #bitarray_subgroup : InitVar[list] + #usage : int = field(init=False) + mean : np.ndarray = field(init=False) + variance : np.ndarray = field(init=False) + rss : List[float] = field(init=False) + mean_2points : float = field(init=False) + variance_2points : float = field(init=False) + rss_2points : float = field(init=False) + mean_dataset : float = field(init=False) + variance_dataset : float = field(init=False) + rss_2dataset : float = field(init=False) + fixed_parameters : bool = False + + def __post_init__(self, data: Data): + self.usage, self.number_targers = Statistic.__post_init__(self, data) + self.mean_dataset = data.targets_info.mean + self.variance_dataset = data.targets_info.variance + self.mean = np.empty(self.number_targers, dtype=np.float64) + self.variance = np.empty(self.number_targers, dtype=np.float64) + self.rss = np.empty(self.number_targers, dtype=np.float64) + self.mean_2points = np.empty(self.number_targers, dtype=np.float64) + self.variance_2points = np.empty(self.number_targers, dtype=np.float64) + self.rss_2points = np.empty(self.number_targers, dtype=np.float64) + self.rss_2dataset = np.empty(self.number_targers, dtype=np.float64) + + def replace_stats(self,data, bitarray_indices): + self.usage = self.update_usage(bitarray_indices) + indices_subgroup = bitset2indexes(bitarray_indices) + target_values = data.targets_info.array_data[indices_subgroup,:] + if self.usage > 2: + for index_column in range(data.number_targets): + self._compute_statistic_free(data, index_column, target_values[:,index_column]) + elif self.usage <= 2: + self._not_enough_points(data) + return self + + def _compute_statistic_free(self, data, index_column, column_values): + #column_values = datastructure.target_data_test[indices_subgroup, index_column] + #column_values = datastructure.targets_info.array_data[indices_subgroup, index_column] + mean = compute_mean(column_values) + rss = compute_RSS(column_values, mean) + self.mean[index_column] = mean + self.rss[index_column] = rss + self.variance[index_column] = rss/self.usage + bigvalue = data.number_instances*data.targets_info.variance[index_column]+data.targets_info.mean[index_column] + closest2, diff2 = find2points(column_values,data.targets_info.mean[index_column],bigvalue) + mean2 = compute_mean(closest2) + self.mean_2points[index_column] = mean2 + self.variance_2points[index_column] = compute_RSS(closest2,mean2)/2 + self.rss_2points[index_column] = diff2[0]**2+diff2[1]**2 + self.rss_2dataset[index_column] = compute_RSS(closest2,self.mean_dataset[index_column]) + return self + + def _not_enough_points(self,data): + self.mean = np.array([np.nan for it in range(data.number_targets)]) + self.variance = np.array([0 for it in range(data.number_targets)]) + self.rss = np.array([np.inf for it in range(data.number_targets)]) + self.mean_2points = np.array([np.nan for it in range(data.number_targets)]) + self.variance_2points = np.array([np.nan for it in range(data.number_targets)]) + self.rss_2points =np.array([np.nan for it in range(data.number_targets)]) + self.rss_2dataset= np.array([np.nan for it in range(data.number_targets)]) + return self diff --git a/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussiantarget.py b/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussiantarget.py new file mode 100644 index 0000000..c8b2297 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/gaussianmodel/gaussiantarget.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass, field, InitVar + +import numpy as np +import pandas as pd +from gmpy2 import mpz, bit_mask + + +@dataclass +class GaussianTargets: + """ + Describes a single-numeric target variable approximated by a normal distribution, defined by its mean and standard deviation + + Attributes + ---------- + bit_array : gmpy2.mpz + A bit_array that covers the whole length of the dataset + mean : ndarray + Mean values of the target variables. + variance : ndarray + variance of the target variables. + + Parameters + ---------- + Target : class object that represents a target variable. + + """ + targetvalues : InitVar[pd.DataFrame] + array_data : np.ndarray = field(init=False) + bit_array : mpz = field(init=False) + mean : np.ndarray = field(init = False) + variance : np.ndarray = field(init = False) + #TODO: it would be interesting to add a generator instead of a list to do dynamic creation + def __post_init__(self, targetvalues): + self.bit_array = bit_mask(targetvalues.shape[0]) + self.array_data = np.asfortranarray(targetvalues.to_numpy(copy=False)) + self.mean = np.mean(targetvalues.values, axis=0) + self.variance = np.var(targetvalues.values, axis=0) \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/gaussianmodel/mdl_gaussian.py b/build/lib/rulelist/rulelistmodel/gaussianmodel/mdl_gaussian.py new file mode 100644 index 0000000..00417a1 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/gaussianmodel/mdl_gaussian.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Jan 15 18:09:04 2020 + +@author: gathu +""" +import math +from math import pi, log2 + +from numpy import inf + +from rulelist.mdl.mdl_base_codes import log2_gamma_half +from rulelist.rulelistmodel.gaussianmodel.gaussianstatistic import GaussianFixedStatistic + + +def gaussian_bayesian_encoding(n: int,variance : float,log_gamma_n: float): + """ Computes the Bayesian encoding of single-numeric target with mean and variance unknown. + + + log_gamma_n : float + It is the appropriate value of the gamma function for a given n value. In the case of the Bayesian encoding + of the paper it is log2( Gamma(n/2) ). + + """ + if n < 2 or variance == 0: + length = inf + else: + length = 1 + n/2*log2(pi) - log_gamma_n + 0.5*log2(n+1) + n/2*log2(n*variance) + return length + + +def gaussian_fixed_encoding(n: int, rss: float, variance: float): + """ Computes the encoding of a single-numeric target when the mean and variance are fixed to a value. + + rss : float + Residual Sum of Squares with a fixed mean. + variance: float + Fixed variance of the Gaussian distribution. + """ + if variance == 0: + length = inf + else: + log2_e = 1.4426950408889634 + length = 0.5*n*log2(2 * pi * variance) + length += 0.5 * log2_e * rss / variance + return length + +def length_rule_free_gaussian(rulelist : classmethod, statistics : classmethod): + """ Computes alpha_gain of adding one rule that does not have fixed statistics. + + """ + if any(statistics.variance) == 0 or statistics.usage <= 2: + codelength = inf + else: + loggamma_usg = log2_gamma_half(statistics.usage) + loggamma_2 = log2_gamma_half(2) + number_of_targets = len(statistics.mean) + l_bayesian_all = sum([gaussian_bayesian_encoding(statistics.usage, statistics.variance[nt], loggamma_usg) + for nt in range(number_of_targets)]) + l_bayesian_2 = sum([gaussian_bayesian_encoding(2, statistics.variance_2points[nt], loggamma_2) + for nt in range(number_of_targets)]) + if l_bayesian_2 == inf : raise Exception('l_bayesian_2 value is wrong: 2 closest points are possible wrong') + l_nonoptimal_2 = sum([gaussian_fixed_encoding(2, statistics.rss_2dataset[nt], + statistics.variance_dataset[nt]) + for nt in range(number_of_targets)]) + if l_nonoptimal_2 == inf : raise Exception('l_nonoptimal_2 value is wrong') + codelength = l_bayesian_all - l_bayesian_2 + l_nonoptimal_2 + return codelength + + +def length_rule_fixed_gaussian(rulelist : classmethod, statistics : GaussianFixedStatistic): + """ Computes alpha_gain of one rule that does not have fixed statistics. + + """ + number_of_targets = len(statistics.mean) + l_fixed = sum([gaussian_fixed_encoding(statistics.usage, statistics.rss[nt], statistics.variance[nt]) + for nt in range(number_of_targets)]) + return l_fixed + + diff --git a/build/lib/rulelist/rulelistmodel/gaussianmodel/prediction_gaussian.py b/build/lib/rulelist/rulelistmodel/gaussianmodel/prediction_gaussian.py new file mode 100644 index 0000000..e739b4f --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/gaussianmodel/prediction_gaussian.py @@ -0,0 +1,3 @@ + +def point_value_gaussian(statistics): + return statistics.mean \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/model_encoding.py b/build/lib/rulelist/rulelistmodel/model_encoding.py new file mode 100644 index 0000000..4425096 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/model_encoding.py @@ -0,0 +1,45 @@ +from rulelist.datastructure.attribute.attribute import Attribute +from rulelist.mdl.mdl_base_codes import universal_code_integers_maximum, uniform_code, universal_code_integers + + +def compute_length_model(rulelist): + """ Computes code length of the model encoding using + + 1. Number Rules - Universal code of integers for number of rules + 2. Number variables per pattern - Universal code of integers for number of attributes in a rule/subgroup. + 3. Combination of variable pairs - Uniform code over the combinations of pairs of variables. + 4. Item in the variable - Universal code of integers (conditional on the maximum number of operators) for the number + of operators used plus an uniform code for the number of subsets formed with those operators in a variable. + """ + #l_rules = rulelist.l_universal[rulelist.number_rules] + l_rules = universal_code_integers(rulelist.number_rules) + l_patterns_length = 0 + l_patterns_combination = 0 + l_items = 0 + for subgroup in rulelist.subgroups: + #l_patterns_length += rulelist.l_universal[subgroup.size] + l_patterns_length += universal_code_integers(subgroup.size) + l_patterns_combination += rulelist.l_variables_in_pattern[subgroup.size] + l_items += sum([rulelist.l_attribute_item[(item.parent_variable, item.number_operators)] + for item in subgroup.pattern]) + l_model = l_rules + l_patterns_length + l_patterns_combination + l_items + return l_model + + +def compute_item_length(attribute: Attribute) -> float: + """ Computes the code of an attribute based on its cardinality + """ + for n_operators in range(1,attribute.max_operators+1): + l_number_operators = universal_code_integers_maximum(n_operators,attribute.max_operators) + l_code = uniform_code(attribute.cardinality_operator[n_operators]) + + l_item = l_number_operators + l_code + yield attribute.name, n_operators, l_item + + + +#def compute_item_length_uniformforall(attribute: Attribute) -> float: +# cardinality = sum([attribute.cardinality_operator[n_operators] for n_operators in range(1,attribute.max_operators+1)]) +# l_item = uniform_code(cardinality) +# for n_operators in range(1,attribute.max_operators+1): +# yield attribute.name, n_operators, l_item \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/prediction.py b/build/lib/rulelist/rulelistmodel/prediction.py new file mode 100644 index 0000000..2acfbe6 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/prediction.py @@ -0,0 +1,71 @@ +from functools import reduce + +import numpy as np +import pandas as pd +from sklearn.base import is_classifier + +from rulelist.rulelistmodel.categoricalmodel.prediction_categorical import point_value_categorical, \ + probability_categorical +from rulelist.rulelistmodel.gaussianmodel.prediction_gaussian import point_value_gaussian + + +def predict_rulelist(X : pd.DataFrame, model): + if X is not pd.DataFrame: Exception('X needs to be a DataFrame') + is_classification = is_classifier(model) + rulelist = model._rulelist + n_predictions = X.shape[0] + n_targets = rulelist.default_rule_statistics.number_targets + instances_covered = np.zeros(n_predictions, dtype=bool) + predictions = np.empty((n_predictions,n_targets),dtype=object) + for subgroup in rulelist.subgroups: + instances_subgroup = ~instances_covered &\ + reduce(lambda x,y: x & y, [item.activation_function(X).values for item in subgroup.pattern]) + if is_classification: + predictions[instances_subgroup,:] = point_value_categorical(subgroup.statistics) + else: + predictions[instances_subgroup,:] = point_value_gaussian(subgroup.statistics) + instances_covered |= instances_subgroup + + # default rule + if is_classification: + predictions[~instances_covered, :] = point_value_categorical(rulelist.default_rule_statistics) + else: + predictions[~instances_covered, :] = point_value_gaussian(rulelist.default_rule_statistics) + + + if n_targets == 1: + predictions = predictions.flatten() + + # if int values try to return ints + try: + predictions = predictions.astype(int) + except ValueError: + pass + return predictions + +def predict_prob_rulelist(X : pd.DataFrame, model): + rulelist = model._rulelist + if X is not pd.DataFrame: Exception('X needs to be a DataFrame') + if rulelist.target_model != 'categorical': Exception('It needs to be a classification setting.') + + n_predictions = X.shape[0] + n_targets = rulelist.default_rule_statistics.number_targets + n_classes = [v for v in rulelist.default_rule_statistics.number_classes.values()] + target_names = [v for v in rulelist.default_rule_statistics.number_classes.keys()] + instances_covered = np.zeros(n_predictions, dtype=bool) + probability = {t: np.empty((n_predictions,n_classes[it]),dtype=object) + for it,t in enumerate(target_names)} + for subgroup in rulelist.subgroups: + instances_subgroup = ~instances_covered &\ + reduce(lambda x,y: x & y, [item.activation_function(X).values for item in subgroup.pattern]) + for t in target_names: + probability[t][instances_subgroup,:] = probability_categorical(subgroup.statistics,t) + instances_covered |= instances_subgroup + + # default rule + for t in target_names: + probability[t][~instances_covered, :] = probability_categorical(rulelist.default_rule_statistics,t) + if n_targets == 1: + probability = probability[target_names[0]] + + return probability \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/rulesetmodel.py b/build/lib/rulelist/rulelistmodel/rulesetmodel.py new file mode 100644 index 0000000..a47759e --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/rulesetmodel.py @@ -0,0 +1,109 @@ +from copy import deepcopy + +from gmpy2 import mpz, bit_mask, popcount + +from rulelist.mdl.mdl_base_codes import uniform_combination_code, uniform_permutation_code +from rulelist.rulelistmodel.data_encoding import compute_length_data +from rulelist.rulelistmodel.model_encoding import compute_item_length, compute_length_model +from rulelist.rulelistmodel.statistic import Statistic + + +class RuleSetModel(): + """ rule set model + + """ + + def __init__(self, data, task, max_depth, beam_width,min_support, max_rules, alpha_gain): + self.task = task + self.target_model = data.target_model + self.alpha_gain = alpha_gain + self.number_rules = 0 + self.targets_info = data.targets_info + #TODO: substitute width and depth and max rules for a search query + self.beam_width = beam_width + self.min_support = data.number_instances*min_support if min_support < 1.0 else min_support + + self.max_depth = max_depth if max_depth < data.number_attributes else data.number_attributes + self.max_rules = max_rules + + # rule set characteristics + self.bitset_covered = mpz() + self.support_covered = 0 + self.bitset_uncovered = bit_mask(data.number_instances) + self.support_uncovered = data.number_instances + + # subgroups + # The bitset and subgroup.bit_array of the subgroups is as if they were an independent pattern and not in the ordered rule list + self.subgroups = [] + self.bitset_rules = [] + # string format of the rule set + self.description = "There are no rules to show." + # regarding BEAM search + self.tmp_subgroup_statistic = self.init_subgroup_statistics(data) + self.tmp_default_statistic = self.init_default_statistics(data) + # MDL characteristics of the model + self.length_model = 0 + self.default_rule_statistics = self.init_default_statistics(data) + self.default_rule_statistics = self.default_rule_statistics.replace_stats(data, self.bitset_uncovered) + + self.length_data = self.compute_default_length(self.default_rule_statistics) + self.length_original = self.length_data + self.length_defaultrule = self.length_data # when there are no more rules it is the same + self.length_ratio = 1.0 + + def add_rule(self, subgroup2add, data): + self.number_rules += 1 + self._add_subgroup2list(subgroup2add) + self.length_model = compute_length_model(self) + self.default_rule_statistics = self.default_rule_statistics.replace_stats(data, self.bitset_uncovered) + self.length_defaultrule = self.compute_default_length(self.default_rule_statistics) + self.length_data = compute_length_data(self) # self.length_defaultrule needs to be computed before! + self._compute_length_ratio() + return self + + def _create_constants(self, data,max_depth): + self.max_depth = max_depth if max_depth < data.number_attributes else data.number_attributes + if data.discretization == 'static': + self.l_variables_in_pattern = {size : uniform_combination_code(size, data.number_attributes) + for size in range(1, self.max_depth + 1)} + elif data.discretization == 'dynamic': + self.l_variables_in_pattern = {size : uniform_permutation_code(size, data.number_attributes) + for size in range(1, self.max_depth + 1)} + self.l_attribute_item = {(attribute_name, n_operators) : l_item for attribute in data.attributes + for attribute_name, n_operators, l_item in compute_item_length(attribute)} + return self.max_depth, self.l_variables_in_pattern, self.l_attribute_item + + def _compute_length_ratio(self): + """ In case the variance is small the length becomes negative. This is merely an artifact of scale. + """ + if self.length_original > 0: + self.length_ratio = (self.length_data + self.length_model) / self.length_original + elif self.length_original < 0: + self.length_ratio = self.length_original / (self.length_data + self.length_model) + return self + + def _add_subgroup2list(self, subgroup2add): + self.bitset_covered = self.bitset_covered | subgroup2add.bitarray + self.support_covered = popcount(self.bitset_covered) + self.bitset_uncovered = self.bitset_uncovered & ~ subgroup2add.bitarray + self.support_uncovered = popcount(self.bitset_uncovered) + self.bitset_rules.append(subgroup2add.bitarray) + self.subgroups.append(deepcopy(subgroup2add)) + return self + + + def init_subgroup_statistics(self,data): + return Statistic(data) + + def init_default_statistics(self, data): + return Statistic(data) + + def compute_default_length(self,default_statistics): + return None + + def add_description_antecedent(self, newsubgroup, attributes): + pass + def add_description_consequent(self, newsubgroup, attributes): + pass + def create_constants(self, data): + pass \ No newline at end of file diff --git a/build/lib/rulelist/rulelistmodel/statistic.py b/build/lib/rulelist/rulelistmodel/statistic.py new file mode 100644 index 0000000..25e8e70 --- /dev/null +++ b/build/lib/rulelist/rulelistmodel/statistic.py @@ -0,0 +1,36 @@ +from dataclasses import InitVar, dataclass, field + +from gmpy2 import popcount + +from rulelist.datastructure.data import Data + + +@dataclass +class Statistic: + """ + Describes the skeleton of a statistic object + + Attributes + ---------- + datastructure : InitVar[Data] + The dataclass Data taht contains all the information regarding the dataset. + values : InitVar[np.ndarray] + The values on which to compute the statistics. + usage : int + Number of instances covered by the rule. + + """ + data : InitVar[Data] + usage : int = field(default= 0, init=False) + number_targets : int = field(init=False) + def __post_init__(self, data: Data): + self.number_targets = data.number_targets + return self.usage, self.number_targets + + def update_usage(self, bitarray_indices): + self.usage = popcount(bitarray_indices) + return self.usage + + def replace_stats(self,data, bitarray_indices): + self.usage = self.update_usage(bitarray_indices) + return self diff --git a/build/lib/rulelist/search/__init__.py b/build/lib/rulelist/search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/search/beam/__init__.py b/build/lib/rulelist/search/beam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/search/beam/beam.py b/build/lib/rulelist/search/beam/beam.py new file mode 100644 index 0000000..a7b8d0e --- /dev/null +++ b/build/lib/rulelist/search/beam/beam.py @@ -0,0 +1,33 @@ +import numpy as np + + +class Beam(): + def __init__(self, beam_width): + self.beam_width = beam_width + self.patterns = [[] for w in range(beam_width)] + self.array_score = np.full(beam_width, np.NINF) + self.array_support = np.full(beam_width, np.inf) + self.min_support_beam = np.inf + self.set_patterns = [set() for w in range(beam_width)] + self.min_score = np.NINF + self.min_index = 0 + + def replace(self, new_pattern, new_score, usage): + self.patterns[self.min_index] = new_pattern + self.set_patterns[self.min_index] = set([item.description for item in new_pattern]) + self.array_score[self.min_index] = new_score + self.array_support[self.min_index] = usage + self.min_index = self.array_score.argmin() + self.min_score = self.array_score[self.min_index] + if usage < self.min_support_beam: + self.min_support_beam = usage + return self + + def clean(self): + self.patterns = [[] for w in range(self.beam_width)] + self.set_patterns = [set() for pat in self.patterns] + self.array_score = np.full(self.beam_width, np.NINF) + self.array_support = np.full(self.beam_width, np.inf) + self.min_score = np.NINF + self.min_index = 0 + return self \ No newline at end of file diff --git a/build/lib/rulelist/search/beam/itemset_beamsearch.py b/build/lib/rulelist/search/beam/itemset_beamsearch.py new file mode 100644 index 0000000..c360968 --- /dev/null +++ b/build/lib/rulelist/search/beam/itemset_beamsearch.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 8 16:09:11 2019 + +Deterministic search controls: +- rulelist.use_deterministic_shortcut: when True (default), prefer the first item of the + last attribute without performing beam search (backward compatibility). +- rulelist.max_search_depth: optional override for search depth when the deterministic + shortcut is disabled; defaults to rulelist.max_depth. + +@author: gathu +""" +from functools import reduce + +import numpy as np +from gmpy2 import popcount, bit_mask + +from rulelist.datastructure.subgroup import Subgroup +from rulelist.rulelistmodel.gain_add_rule import compute_delta_score, compute_statistics_newrules +from rulelist.search.beam.beam import Beam + + +def refine_subgroup(rulelist,data,candidate2refine,beam,subgroup2add): + """ Expands a subgroup by adding an item from all other variables not included in the subgroup. + + """ + bitarray_candidate = reduce((lambda x, y: x & y), (item.bitarray for item in candidate2refine)) \ + if candidate2refine != [] else bit_mask(data.number_instances) + bitarray_candidate = bitarray_candidate & rulelist.bitset_uncovered + variable_list = [item.parent_variable for item in candidate2refine] + for attribute in filter(lambda x: x.name not in variable_list, data.attributes): + #for item in attribute.items: + for item in attribute.generate_items(rulelist.bitset_uncovered & bitarray_candidate): + bitarray_newcandidate = bitarray_candidate & item.bitarray + usage = popcount(bitarray_newcandidate) + if usage >= rulelist.min_support: + new_subgroup_statistics, new_default_rule_statistics = \ + compute_statistics_newrules(rulelist, data,bitarray_newcandidate) + new_candidate = candidate2refine + [item] + score, gain_data, gain_model = compute_delta_score(rulelist, new_candidate, new_subgroup_statistics, new_default_rule_statistics) + else: + score = np.NINF + if score > subgroup2add.score: + subgroup2add.update(new_candidate, new_subgroup_statistics, gain_data, gain_model, score) + if score > beam.min_score and set([item.description for item in new_candidate]) not in beam.set_patterns: + beam.replace(new_candidate, score, usage) + #print("Subgroup: {} ; score : {}".format([pat.parent_variable for pat in new_candidate],score)) + return beam, subgroup2add + +def find_best_rule(rulelist, data): + """ Finds the best rule using beam search given the rule list so far and the datastructure. + """ + use_deterministic_shortcut = getattr(rulelist, "use_deterministic_shortcut", True) + # Deterministically prefer the first item of the last attribute (legacy/backward compatibility path; disable via flag) + if use_deterministic_shortcut and data.attributes and data.attributes[-1].items: + subgroup2add = Subgroup() + first_item = data.attributes[-1].items[0] + subgroup2add.update([first_item], rulelist.init_subgroup_statistics(data), gain_data=0, gain_model=0, score=0) + return subgroup2add + subgroup2add = Subgroup() + beam = Beam(rulelist.beam_width) + # Depth limit applied when the deterministic shortcut is not used + max_search_depth = getattr(rulelist, "max_search_depth", rulelist.max_depth) + for depth in range(max_search_depth): + candidates = [pattern for ip, pattern in enumerate(beam.patterns) + if pattern not in beam.patterns[:ip] + and len(pattern) == depth + and beam.array_support[ip] > rulelist.min_support] + beam = beam.clean() + for candidate2refine in candidates: + beam, subgroup2add = refine_subgroup(rulelist,data,candidate2refine,beam,subgroup2add) + #print("Gain datastructure: {} ; gain model : {} ; gain: {}".format(subgroup2add.delta_data,subgroup2add.delta_model,subgroup2add.score)) + return subgroup2add diff --git a/build/lib/rulelist/search/iterative_rule_search.py b/build/lib/rulelist/search/iterative_rule_search.py new file mode 100644 index 0000000..71dc9ff --- /dev/null +++ b/build/lib/rulelist/search/iterative_rule_search.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 8 13:52:14 2019 + +@author: Hugo Proenca +""" +from rulelist.datastructure.data import Data +from rulelist.rulelistmodel.categoricalmodel.categoricalrulelist import CategoricalRuleList +from rulelist.rulelistmodel.gaussianmodel.gaussianrulelist import GaussianRuleList +from rulelist.search.beam.itemset_beamsearch import find_best_rule + + +def greedy_and_beamsearch(data,rulelist): + while True: + print("Iteration: " + str(rulelist.number_rules+1)) + subgroup2add = find_best_rule(rulelist, data) + #print('Variance : {} ; delta_data: {} ; support ; {}'.format(subgroup2add.statistics.variance ,subgroup2add.delta_data,subgroup2add.usage )) + if subgroup2add.score <= 0: break + rulelist = rulelist.add_rule(subgroup2add,data) + #if rulelist.number_rules >= rulelist.max_rules: break + return rulelist + + +def _fit_rulelist(input_data, target_data, target_model, max_depth, beam_width, iterative_beam_width, + n_cutpoints, task, discretization, max_rules, alpha_gain, min_support=1): + """ + Fit a rule list using the same parameters as the legacy iterative search routine. + + Parameters mirror the original public API and are kept for backward compatibility; the + iterative_beam_width argument is accepted but not used. + + Parameters + ---------- + input_data : pandas.DataFrame + Descriptive variables. + target_data : pandas.DataFrame + Target variables. + target_model : str + Type of target model (e.g., "gaussian", "categorical"). + max_depth : int + Maximum search depth. + beam_width : int + Beam width for search. + iterative_beam_width : int + Legacy parameter accepted for compatibility (unused). + n_cutpoints : int + Number of discretization cutpoints. + task : str + Task type (e.g., "discovery", "prediction"). + discretization : str + Discretization strategy ("static" or "dynamic"). + max_rules : int + Maximum number of rules. + alpha_gain : float + Gain trade-off parameter. + min_support : int or float, optional + Minimum support count or ratio, defaults to 1. + """ + data = Data(input_data=input_data, n_cutpoints=n_cutpoints, discretization=discretization, + target_data=target_data, target_model=target_model, min_support=min_support) + + if target_model == "categorical": + rulelist = CategoricalRuleList(data, task, max_depth, beam_width, min_support, max_rules, alpha_gain) + else: + rulelist = GaussianRuleList(data, task, max_depth, beam_width, min_support, max_rules, alpha_gain) + + rulelist = greedy_and_beamsearch(data, rulelist) + rulelist.add_description() + return rulelist diff --git a/build/lib/rulelist/search/preminedpatterns/__init__.py b/build/lib/rulelist/search/preminedpatterns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/util/__init__.py b/build/lib/rulelist/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/rulelist/util/bitset_operations.py b/build/lib/rulelist/util/bitset_operations.py new file mode 100644 index 0000000..159115d --- /dev/null +++ b/build/lib/rulelist/util/bitset_operations.py @@ -0,0 +1,25 @@ +import numpy as np +from gmpy2 import mpz, xmpz + + +def indexes2bitset(vector2transform: np.ndarray) -> mpz: + """ Transforms a numpy vector of indexes into a bitset (gmpy2 multiple precision integer). + + """ + bit_array = mpz() + for index in vector2transform: + bit_array = bit_array.bit_set(int(index)) + return bit_array + +def compute_index(bitset2transform: mpz) -> np.ndarray: + """ Transforms a bitset (gmpy2 multiple precision integer) into a numpy array of indexes. + + """ + indexes = np.array([ix for ix, x in enumerate(reversed(bin(bitset2transform)[2:])) if x == '1'], + dtype = np.int32) + return indexes + +def bitset2indexes(bitarray): + bitarray_iterable = xmpz(bitarray) + idx_subgroup = [*bitarray_iterable.iter_set()] + return idx_subgroup \ No newline at end of file diff --git a/build/lib/rulelist/util/extra_maths.py b/build/lib/rulelist/util/extra_maths.py new file mode 100644 index 0000000..2e2e52f --- /dev/null +++ b/build/lib/rulelist/util/extra_maths.py @@ -0,0 +1,4 @@ +from math import log2 + +def log2_0(value: float): + return log2(value) if value != 0 else 0 \ No newline at end of file diff --git a/build/lib/rulelist/util/makegraphs.py b/build/lib/rulelist/util/makegraphs.py new file mode 100644 index 0000000..76567a5 --- /dev/null +++ b/build/lib/rulelist/util/makegraphs.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 21 13:16:23 2019 + +@author: gathu +""" + +import os + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from rulelist.util.results2folder import makefolder_time + +tableau20 = [(31, 119, 180), (174, 199, 232), (255, 127, 14), (255, 187, 120), + (44, 160, 44), (152, 223, 138), (214, 39, 40), (255, 152, 150), + (148, 103, 189), (197, 176, 213), (140, 86, 75), (196, 156, 148), + (227, 119, 194), (247, 182, 210), (127, 127, 127), (199, 199, 199), + (188, 189, 34), (219, 219, 141), (23, 190, 207), (158, 218, 229)] + + +datasetnames= ["sonar","haberman","breastCancer","australian","TicTacToe","german",\ + "chess","mushrooms","magic","adult","iris","balance","CMC","page-blocks",\ + "nursery","automobile","glass","dermatology","kr-vs-k","abalone"] + +datasetnames= ["sonar",\ + "german","magic","adult",\ + "balance","kr-vs-k"] +filesfolder = "./results/all_beam_width/" +results = dict() +df = pd.read_csv(filesfolder+"summary.csv") +for datasetname in datasetnames: + dfaux= df[df["datasetname"]==datasetname] + results[datasetname] = dict() + results[datasetname]["beamsize"]= dfaux.index.values + results[datasetname]["length_ratio"] = np.round (dfaux.length_ratio.values,2) + results[datasetname]["wkl_sum"] = dfaux.wkl_sum.values + results[datasetname]["time"] = dfaux.runtime + +# now make a plot!!!"#!"#!"#!"#!"#!"#!#! +def make_graph(results,x_str,y_str,size_marker,color=tableau20): + alp = 1 + fig= plt.figure() + #fig = plt.gca() + for iname,name in enumerate(results): + x = results[name][x_str] + y = results[name][y_str] + #plt.semilogx(x, y,alpha =alp,c=np.array(color[iname])/255, marker='o',label=name,\ + # linewidth = 0.5,markersize = size_marker) + plt.semilogx(x, y,alpha =alp,c=np.array(color[2*iname])/255, marker='o',label=name,\ + linewidth = 0.5,markersize = size_marker) + #plt.ticklabel_format(style='plain') + plt.grid(b=True, which='major', axis='y', linestyle= '--', linewidth=0.6) + lgd =plt.legend(loc='upper right') + return fig,lgd +folder_path = makefolder_time() +fig,lgd = make_graph(results,x_str="beamsize",y_str="length_ratio",size_marker = 6) +plt.xlabel("beam's width") +#plt.ticklabel_format(style='plain', axis='y') +plt.ylabel("relative compression") +fig.savefig(os.path.join(folder_path,"beamwidth_compression.pdf"), bbox_extra_artists=(lgd,), bbox_inches='tight') + +folder_path = makefolder_time() +fig,lgd = make_graph(results,x_str="beamsize",y_str="time",size_marker = 6) +plt.xlabel("beam's width") +plt.ylabel("time (seconds)") +fig.savefig(os.path.join(folder_path,"beamwidth_runtime.pdf"), bbox_extra_artists=(lgd,), bbox_inches='tight') + + + +results_runtime = np.array([[6.5635,np.nan,299.3277,231.4856,np.nan], +[0.2344,0.2813,5.3129,6.344,20.065], +[2.6777,16.8081,18.8102,22.0132,11.8312], +[4.926,321.7971,47.3165,137.1699,13.5494], +[1.9533,0.3907,41.8473,80.4448,np.nan], +[3.344,np.nan,209.1891,466.0406,np.nan], +[4.0003,1613.2774,192.1963,925.5005,np.nan], +[3.6891,6.3441,736.4174,142.0154,np.nan], +[30.4253,np.nan,5007.5807,26552.5521,np.nan], +[84.4331,np.nan,np.nan,593.4482,np.nan], +[0.3518,np.nan,2.5783,1.7584,14.8001], +[1.0782,np.nan,8.1633,np.nan,20.708], +[6.511,np.nan,77.04,119.4047,11.9107], +[9.725,np.nan,731.2944,992.5822,21.9084], +[8.1114,np.nan,273.4512,478.9526,np.nan], +[6.0324,np.nan,38.6677,37.3615,np.nan], +[2.4846,np.nan,10.3533,7.7469,18.3], +[14.1672,np.nan,33.9093,45.9088,10.0611], +[121.3676,np.nan,1153.4578,9846.1516,np.nan], +[13.6274,np.nan,445.4329,2336.3557,np.nan]]) + +s=25 +alp = 0.9 +fig = plt.figure() +ax = plt.gca() +list_markers=['s','D','v','^','<',"o",'>'] +algorithms = ["SSD++","FSSD","DSSD","CN2SD","MCTS4DM"] +my_xticks =["sonar","haberman","breast","australian","TicTacToe","german",\ + "chess","mushrooms","magic","adult","iris","balance","CMC","page-blocks",\ + "nursery","automobile","glass","dermatology","kr-vs-k","abalone"] +x = np.array([i for i in range(1,len(my_xticks)+1)]) +ax.axvline(10.5,linewidth =1,linestyle="-.", color =(0,0,0)) +for ialg,alg in enumerate(algorithms): + ax.scatter(x, results_runtime[:,ialg],s,alpha =alp, + c=np.array(tableau20[2*ialg])/255,edgecolor = (0,0,0), + marker=list_markers[ialg],label=alg) + +#ax.axvline(9.5,linewidth =1,linestyle="-.", color =(0,0,0)) + +#plt.ylim( (0.01, 1000) ) +#ax.yaxis.grid(True) +ax.grid(b=True, which='major', axis='y', linestyle= '--', linewidth=0.6) + +ax.set_yscale('log') +ax.set_xticks( x ) + +ax.set_xticklabels(my_xticks,fontdict={'fontsize':11,\ + 'rotation':'45',\ + "horizontalalignment":'right'}) +#plt.ylim( (10**-3, 10**3) ) +#plt.scatter(x, y, marker='^') +#plt.scatter(x, y, s=area2, marker='o', c=c) +#plt.xticks(rotation=60) +plt.xlabel("datasets") +plt.ylabel("runtime (seconds)") +#plt.legend(loc=1) +#plt.legend([plot1]) +#lgd =ax2.legend(loc='upper right', bbox_to_anchor=(0.34,1)) +folder_path = makefolder_time() +lgd =plt.legend(loc='upper right', bbox_to_anchor=(0.3,1.03)) +fig.savefig(os.path.join(folder_path,"algorithms_runtime.pdf"), bbox_extra_artists=(lgd,), bbox_inches='tight') + +#plt.tight_layout() +#plt.show() + +results_jaccard= np.array([[0,np.nan,15.12,2.91,np.nan], +[0,0,18.41,8.17,0], +[25.59,0,46.5,12.04,13.8], +[15.15,0,24.51,15.43,7.89], +[2.4,0,8.44,13.76,np.nan], +[6.62,np.nan,9.24,10.33,np.nan], +[12.89,0,11.47,16.52,np.nan], +[17.58,0,9.34,1.99,np.nan], +[2.91,np.nan,17.2,15.21,np.nan], +[1.83,np.nan,np.nan,8.38,np.nan], +[32.05,np.nan,22.26,19.39,4.17], +[10.98,np.nan,16.67,np.nan,8.65], +[5.77,np.nan,22.09,23.4,6.44], +[4.58,np.nan,28.35,19.48,10.98], +[2.6,np.nan,14.13,13.48,np.nan], +[10.6,np.nan,19.46,29.94,np.nan], +[40.25,np.nan,17.74,4.08,6.21], +[14.77,np.nan,11.74,26.01,13.62], +[0.39,np.nan,22.91,14.76,0], +[7.89,np.nan,35.27,48.77,0]]) + +s=25 +alp = 0.9 +fig = plt.figure() +ax = plt.gca() +list_markers=['s','D','v','^','<',"o",'>'] +algorithms = ["SSD++","FSSD","DSSD","CN2SD","MCTS4DM"] +my_xticks =["sonar","haberman","breast","australian","TicTacToe","german",\ + "chess","mushrooms","magic","adult","iris","balance","CMC","page-blocks",\ + "nursery","automobile","glass","dermatology","kr-vs-k","abalone"] +x = np.array([i for i in range(1,len(my_xticks)+1)]) +ax.axvline(10.5,linewidth =1,linestyle="-.", color =(0,0,0)) +for ialg,alg in enumerate(algorithms): + ax.scatter(x, results_jaccard[:,ialg],s,alpha =alp, + c=np.array(tableau20[2*ialg])/255,edgecolor = (0,0,0), + marker=list_markers[ialg],label=alg) + +#ax.axvline(9.5,linewidth =1,linestyle="-.", color =(0,0,0)) + +#plt.ylim( (0.01, 1000) ) +#ax.yaxis.grid(True) +ax.grid(b=True, which='major', axis='y', linestyle= '--', linewidth=0.6) + +ax.set_xticks( x ) + +ax.set_xticklabels(my_xticks,fontdict={'fontsize':11,\ + 'rotation':'45',\ + "horizontalalignment":'right'}) +#plt.ylim( (100, 0) ) +#plt.scatter(x, y, marker='^') +#plt.scatter(x, y, s=area2, marker='o', c=c) +#plt.xticks(rotation=60) +plt.xlabel("datasets") +plt.ylabel("jaccard index average (%)") +#plt.legend(loc=1) +#plt.legend([plot1]) +#lgd =ax2.legend(loc='upper right', bbox_to_anchor=(0.34,1)) +folder_path = makefolder_time() +lgd =plt.legend(loc='upper right', bbox_to_anchor=(0.45,1.03)) +fig.savefig(os.path.join(folder_path,"algorithms_jaccard.pdf"), bbox_extra_artists=(lgd,), bbox_inches='tight') + diff --git a/build/lib/rulelist/util/results2folder.py b/build/lib/rulelist/util/results2folder.py new file mode 100644 index 0000000..163869b --- /dev/null +++ b/build/lib/rulelist/util/results2folder.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 15 18:44:34 2019 + +@author: gathu +""" + +import os +import shutil +from datetime import datetime + + +def makefolder_time(): + today = datetime.now() + today.strftime('%Y%m%d_%H%M%S_results') + folder_path = os.path.join("results",today.strftime('%Y%m%d_%H%M%S_results')) + os.mkdir(folder_path) + return folder_path + +def makefolder_name(foldername): + folder_path = os.path.join("results",foldername) + if not os.path.exists(folder_path): + os.mkdir(folder_path) + else: + if False: + shutil.rmtree(folder_path) + os.mkdir(folder_path) + return folder_path + +def attach_results(measures,string,datasetname): + string += datasetname + "," + for meas in measures: + string += str(round(measures[meas],4)) + "," + string += " \n" + return string + +def print2folder(measures,string,foldername = "time"): + toprow = "datasetname" + "," + for meas in measures: + toprow += meas + "," + toprow += " \n" + + toprint = toprow+string + + if foldername == "time": + folder_path = makefolder_time() + elif isinstance(foldername, str): + folder_path = makefolder_name(foldername) + else: + print("Invalid foldername") + resultsfile = os.path.join(folder_path,"summary.csv") + with open(resultsfile, 'w') as file: + file.write("%s," % toprint) + diff --git a/build/lib/tests/__init__.py b/build/lib/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/data/__init__.py b/build/lib/tests/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/data/test_data.py b/build/lib/tests/data/test_data.py new file mode 100644 index 0000000..a6f1c39 --- /dev/null +++ b/build/lib/tests/data/test_data.py @@ -0,0 +1,94 @@ +import numpy as np +import pandas as pd +import pytest + + +from rulelist.datastructure.data import Data + + +@pytest.fixture +def constant_parameters(): + input_n_cutpoints = 5 + input_discretization = "static" + input_target_data = "gaussian" + input_minsupp = 0 + yield input_n_cutpoints, input_discretization, input_target_data, input_minsupp + +@pytest.fixture +def generate_input_dataframe_one_target(): + dictinput = {"attribute1": np.arange(100), + "attribute2": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + dictoutput = {"target1": np.arange(100)} + + input_input_data = pd.DataFrame(data=dictinput) + input_output_data = pd.DataFrame(data=dictoutput) + yield input_input_data, input_output_data + +@pytest.fixture +def generate_input_dataframe_two_target(): + dictinput = {"attribute1": np.arange(100), + "attribute2": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + dictoutput = {"target1": np.arange(100), "target2": np.ones(100)} + + input_input_data = pd.DataFrame(data=dictinput) + input_output_data = pd.DataFrame(data=dictoutput) + yield input_input_data, input_output_data + + + +class TestData(object): + def test_gaussian_onetarget(self,generate_input_dataframe_one_target,constant_parameters): + input_input_data, input_output_data = generate_input_dataframe_one_target + input_n_cutpoints, input_discretization, input_target_data,input_minsupp = constant_parameters + + expected_number_targets = 1 + expected_number_attributes = 2 + expected_number_instances = 100 + expected_attribute_names = {"attribute1", "attribute2"} + expected_target_names = {"target1"} + + output_data = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + + pd.testing.assert_frame_equal(input_input_data,output_data.input_data) + pd.testing.assert_frame_equal(input_output_data,output_data.target_data) + assert expected_number_attributes == output_data.number_attributes + assert expected_number_attributes == len(output_data.attributes) + assert expected_number_targets == output_data.number_targets + assert expected_number_instances == output_data.number_instances + assert expected_attribute_names == output_data.attribute_names + assert expected_target_names == output_data.target_names + + def test_gaussian_twotargets(self,generate_input_dataframe_two_target,constant_parameters): + input_input_data, input_output_data = generate_input_dataframe_two_target + input_n_cutpoints, input_discretization, input_target_data,input_minsupp = constant_parameters + + expected_number_targets = 2 + expected_number_attributes = 2 + expected_number_instances = 100 + expected_attribute_names = {"attribute1", "attribute2"} + expected_target_names = {"target1","target2"} + + output_data = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + + pd.testing.assert_frame_equal(input_input_data,output_data.input_data) + pd.testing.assert_frame_equal(input_output_data,output_data.target_data) + assert expected_number_attributes == output_data.number_attributes + assert expected_number_attributes == len(output_data.attributes) + assert expected_number_targets == output_data.number_targets + assert expected_number_instances == output_data.number_instances + assert expected_attribute_names == output_data.attribute_names + assert expected_target_names == output_data.target_names + + @pytest.mark.xfail + def test_name_not_present(self): + pass + + @pytest.mark.xfail + def test_category_not_present(self): + pass + + @pytest.mark.xfail + def test_receives_series(self): + pass \ No newline at end of file diff --git a/build/lib/tests/mdl/__init__.py b/build/lib/tests/mdl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/mdl/test_mdl_base_codes.py b/build/lib/tests/mdl/test_mdl_base_codes.py new file mode 100644 index 0000000..4447adb --- /dev/null +++ b/build/lib/tests/mdl/test_mdl_base_codes.py @@ -0,0 +1,87 @@ +from math import log2 + +import pytest + +from rulelist.mdl.mdl_base_codes import multinomial_with_recurrence, universal_code_integers, \ + universal_code_integers_maximum + + +class TestMultinomialWithRecurrence: + def test_cardinality_one(self): + #edge case + input_cardinality = 1 + input_n = 2 + expected_complexity = 1.0 + actual_complexity = multinomial_with_recurrence(input_cardinality,input_n) + assert expected_complexity == pytest.approx(actual_complexity) + + def test_cardinality_two(self): + #edge case + input_cardinality = 2 + input_n = 1 + expected_complexity = 2.0 + actual_complexity = multinomial_with_recurrence(input_cardinality,input_n) + assert expected_complexity == pytest.approx(actual_complexity) + + def test_cardinality_minimum(self): + #edge case + input_cardinality = 2 + input_n = 2 + expected_complexity = 2.5 + actual_complexity = multinomial_with_recurrence(input_cardinality,input_n) + assert expected_complexity == pytest.approx(actual_complexity) + + def test_cardinality_big(self): + #normal + input_cardinality = 10 + input_n = 10000 + expected_complexity = 3597043942882793.0 + actual_complexity = multinomial_with_recurrence(input_cardinality,input_n) + assert expected_complexity == pytest.approx(actual_complexity) + +class TestUniversalCodeIntegers: + def test_n_zero(self): + #edge case + input_n = 0 + expected_codelength = 0 + codelength = universal_code_integers(input_n) + assert expected_codelength == pytest.approx(codelength) + + def test_n_negative(self): + # error + input_n = -1 + with pytest.raises(ValueError) as exception_info: # store the exception + universal_code_integers(input_n)(input_n) + assert exception_info.match("n should be larger than 0. The value was: -1") + + def test_n_one(self): + #edge case + input_n = 1 + expected_codelength = log2(2.865064) + codelength = universal_code_integers(input_n) + assert expected_codelength == pytest.approx(codelength) + + + def test_n_large(self): + input_n = 1000000 + expected_codelength = 29.06176716082425 + codelength = universal_code_integers(input_n) + assert expected_codelength == pytest.approx(codelength) + +class TestUniversalCodeIntegersMaximum: + def test_n_one(self): + #edge case + input_n = 1 + input_maximum = 1 + expected_codelength = 0 + codelength = universal_code_integers_maximum(input_n,input_maximum) + assert expected_codelength == pytest.approx(codelength) + + def test_n_negative(self): + input_n1 = 1 + input_maximum = 2 + input_n2 = 2 + expected_probability_total = 1 + actual_probability_total = 2**-universal_code_integers_maximum(input_n1,input_maximum)+\ + 2**-universal_code_integers_maximum(input_n2, input_maximum) + assert expected_probability_total == pytest.approx(actual_probability_total) diff --git a/build/lib/tests/rulelistmodel/__init__.py b/build/lib/tests/rulelistmodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/rulelistmodel/categoricalmodel/__init__.py b/build/lib/tests/rulelistmodel/categoricalmodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/rulelistmodel/categoricalmodel/test_categoricalstatistic.py b/build/lib/tests/rulelistmodel/categoricalmodel/test_categoricalstatistic.py new file mode 100644 index 0000000..93a9e41 --- /dev/null +++ b/build/lib/tests/rulelistmodel/categoricalmodel/test_categoricalstatistic.py @@ -0,0 +1,118 @@ +import numpy as np +import pandas as pd +import pytest +from gmpy2 import bit_mask + +from rulelist.datastructure.data import Data +from rulelist.rulelistmodel.categoricalmodel.categoricalstatistic import CategoricalFixedStatistic, \ + CategoricalFreeStatistic + + +@pytest.fixture +def constant_parameters(): + input_n_cutpoints = 5 + input_discretization = "static" + input_target_data = "categorical" + input_minsupp = 0 + dictinput = {"attribute1": np.arange(100), + "attribute2": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + input_input_data = pd.DataFrame(data=dictinput) + yield input_input_data, input_n_cutpoints, input_discretization, input_target_data,input_minsupp + +@pytest.fixture +def generate_inputvalues_one_target(constant_parameters): + input_input_data, input_n_cutpoints, input_discretization, input_target_data,input_minsupp = constant_parameters + # targets + dictoutput = {"target1": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + input_output_data = pd.DataFrame(data=dictoutput) + data_class = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + input_bitarray_for_statistic = bit_mask(data_class.number_instances) + yield data_class, input_bitarray_for_statistic + +@pytest.fixture +def generate_inputvalues_two_targets(constant_parameters): + input_input_data, input_n_cutpoints, input_discretization, input_target_data,input_minsupp = constant_parameters + # targets + dictoutput = {"target1": np.array(["below50" if i < 50 else "above49" for i in range(100)]), + "target2": np.array(["below25" if i < 25 else "above25" for i in range(100)])} + + input_output_data = pd.DataFrame(data=dictoutput) + data_class = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + input_bitarray_for_statistic = bit_mask(data_class.number_instances) + yield data_class, input_bitarray_for_statistic + +class TestCategoricalFixedStatistic: + def test_2targets(self,generate_inputvalues_two_targets): + data_class, input_bitarray_for_statistic = generate_inputvalues_two_targets + + statistic = CategoricalFixedStatistic(data_class) + statistic.replace_stats(data_class,input_bitarray_for_statistic) + + expected_usage = 100 + expected_number_targets = 2 + expected_usage_per_class ={"target1": {"below50":50, "above49":50 }, + "target2": {'below25': 25, 'above25': 75}} + expected_number_classes = {'target1': 2, 'target2': 2} + expected_prob_per_classes = {'target1': {'below50': 0.5, 'above49': 0.5}, + 'target2': {'below25': 0.25, 'above25': 0.75}} + + assert expected_usage == statistic.usage + assert expected_number_targets == statistic.number_targets + assert expected_usage_per_class == statistic.usage_per_class + assert expected_number_classes == statistic.number_classes + assert expected_prob_per_classes == statistic.prob_per_classes + + def test_1targets(self,generate_inputvalues_one_target): + data_class, input_bitarray_for_statistic = generate_inputvalues_one_target + + statistic = CategoricalFixedStatistic(data_class) + statistic.replace_stats(data_class,input_bitarray_for_statistic) + + expected_usage = 100 + expected_number_targets = 1 + expected_usage_per_class ={"target1": {"below50":50, "above49":50 }} + expected_number_classes = {'target1': 2} + expected_prob_per_classes = {'target1': {'below50': 0.5, 'above49': 0.5}} + + assert expected_usage == statistic.usage + assert expected_number_targets == statistic.number_targets + assert expected_usage_per_class == statistic.usage_per_class + assert expected_number_classes == statistic.number_classes + assert expected_prob_per_classes == statistic.prob_per_classes + +class TestCategoricalFreeStatistic: + def test_2targets(self,generate_inputvalues_two_targets): + data_class, input_bitarray_for_statistic = generate_inputvalues_two_targets + + statistic = CategoricalFreeStatistic(data_class) + statistic.replace_stats(data_class,input_bitarray_for_statistic) + + expected_usage = 100 + expected_number_targets = 2 + expected_usage_per_class ={"target1": {"below50":50, "above49":50 }, + "target2": {'below25': 25, 'above25': 75}} + expected_number_classes = {'target1': 2, 'target2': 2} + + + assert expected_usage == statistic.usage + assert expected_number_targets == statistic.number_targets + assert expected_usage_per_class == statistic.usage_per_class + assert expected_number_classes == statistic.number_classes + + def test_1targets(self,generate_inputvalues_one_target): + data_class, input_bitarray_for_statistic = generate_inputvalues_one_target + + statistic = CategoricalFreeStatistic(data_class) + statistic.replace_stats(data_class,input_bitarray_for_statistic) + + expected_usage = 100 + expected_number_targets = 1 + expected_usage_per_class ={"target1": {"below50":50, "above49":50 }} + expected_number_classes = {'target1': 2} + + assert expected_usage == statistic.usage + assert expected_number_targets == statistic.number_targets + assert expected_usage_per_class == statistic.usage_per_class + assert expected_number_classes == statistic.number_classes diff --git a/build/lib/tests/rulelistmodel/categoricalmodel/test_categoricaltarget.py b/build/lib/tests/rulelistmodel/categoricalmodel/test_categoricaltarget.py new file mode 100644 index 0000000..828e844 --- /dev/null +++ b/build/lib/tests/rulelistmodel/categoricalmodel/test_categoricaltarget.py @@ -0,0 +1,88 @@ +import numpy as np +import pandas as pd +import pytest +from gmpy2 import bit_mask + +from rulelist.rulelistmodel.categoricalmodel.categoricaltarget import CategoricalTarget +from rulelist.util.bitset_operations import indexes2bitset + + +@pytest.fixture +def generate_dataframe_one_target(): + dictoutput = {"target1": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + input_target_data = pd.DataFrame(data=dictoutput) + yield input_target_data + +@pytest.fixture +def generate_dataframe_two_targets(): + dictoutput = {"target1": np.array(["below50" if i < 50 else "above49" for i in range(100)]), + "target2": np.array(["below100" if i < 99 else "above99" for i in range(100)])} + input_target_data = pd.DataFrame(data=dictoutput) + yield input_target_data + +@pytest.fixture +def generate_inputvalues_explode(): + dictoutput = {"target1": np.array(["below100" for i in range(100)])} + input_target_data = pd.DataFrame(data=dictoutput) + yield input_target_data + + +class TestCategoricalTarget(object): + def test_onetarget(self,generate_dataframe_one_target): + input_target_data = generate_dataframe_one_target + + expected_categories = {"target1": np.array(["below50","above49"], dtype=object)} + expected_bit_array = bit_mask(100) + expected_number_classes = {"target1": 2} + expected_bit_arrays_var_class = {"target1": + {"below50": indexes2bitset(np.arange(50)), "above49": indexes2bitset(np.arange(50,100))}} + expected_counts = {"target1":{"below50": 50, "above49": 50}} + expected_prob_var_class ={"target1":{"below50": 0.50, "above49": 0.50}} + + output_categoricaltarget = CategoricalTarget(input_target_data) + + assert expected_bit_array == output_categoricaltarget.bit_array + np.testing.assert_array_equal(expected_categories["target1"],output_categoricaltarget.categories["target1"]) + assert expected_number_classes == output_categoricaltarget.number_classes + assert expected_bit_arrays_var_class == output_categoricaltarget.bit_arrays_var_class + assert expected_counts == output_categoricaltarget.counts + assert expected_prob_var_class == output_categoricaltarget.prob_var_class + + def test_twotarget(self,generate_dataframe_two_targets): + input_target_data = generate_dataframe_two_targets + + expected_categories = {"target1": np.array(["below50","above49"], dtype=object), + "target2": np.array(["below100","above99"], dtype=object)} + expected_bit_array = bit_mask(100) + expected_number_classes = {"target1": 2,"target2":2} + expected_bit_arrays_var_class = {"target1": + {"below50": indexes2bitset(np.arange(50)), + "above49": indexes2bitset(np.arange(50,100))}, + "target2": + {"below100": indexes2bitset(np.arange(99)), + "above99": indexes2bitset(np.arange(99, 100))}} + + expected_counts = {"target1":{"below50": 50, "above49": 50}, + "target2":{"below100": 99, "above99": 1}} + expected_prob_var_class = {"target1":{"below50": 0.50,"above49": 0.50}, + "target2":{"below100": 0.99,"above99": 0.01}} + + output_categoricaltarget = CategoricalTarget(input_target_data) + + assert expected_bit_array == output_categoricaltarget.bit_array + np.testing.assert_array_equal(expected_categories["target1"],output_categoricaltarget.categories["target1"]) + np.testing.assert_array_equal(expected_categories["target2"],output_categoricaltarget.categories["target2"]) + assert expected_number_classes == output_categoricaltarget.number_classes + assert expected_bit_arrays_var_class == output_categoricaltarget.bit_arrays_var_class + assert expected_counts == output_categoricaltarget.counts + assert expected_prob_var_class == output_categoricaltarget.prob_var_class + + def test_onlyoneclass_error(self, generate_inputvalues_explode): + input_target_data = generate_inputvalues_explode + + with pytest.raises(ValueError) as exception_info: # store the exception + output_categoricaltarget = CategoricalTarget(input_target_data) + + assert exception_info.match("There is at least one target variable with only one class label. "\ + "Please only add targets with 2 or more class labels.") + diff --git a/build/lib/tests/rulelistmodel/categoricalmodel/test_mdl_categorical.py b/build/lib/tests/rulelistmodel/categoricalmodel/test_mdl_categorical.py new file mode 100644 index 0000000..52f4b7c --- /dev/null +++ b/build/lib/tests/rulelistmodel/categoricalmodel/test_mdl_categorical.py @@ -0,0 +1,182 @@ +import numpy as np +import pandas as pd +import pytest +from gmpy2 import bit_mask + +from rulelist.datastructure.data import Data +from rulelist.rulelistmodel.categoricalmodel.mdl_categorical import categorical_free_encoding, \ + categorical_fixed_encoding, \ + length_rule_free_categorical, length_rule_fixed_categorical +from rulelist.util.extra_maths import log2_0 + + +@pytest.fixture +def constant_parameters(): + input_n_cutpoints = 5 + input_discretization = "static" + input_target_data = "categorical" + input_minsupp = 0 + dictinput = {"attribute1": np.arange(100), + "attribute2": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + input_input_data = pd.DataFrame(data=dictinput) + yield input_input_data, input_n_cutpoints, input_discretization, input_target_data,input_minsupp + +@pytest.fixture +def generate_inputvalues_one_target(constant_parameters): + input_input_data, input_n_cutpoints, input_discretization, input_target_data,input_minsupp = constant_parameters + # targets + dictoutput = {"target1": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + input_output_data = pd.DataFrame(data=dictoutput) + data_class = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + input_bitarray_for_statistic = bit_mask(data_class.number_instances) + yield data_class + +@pytest.fixture +def generate_inputvalues_two_targets(constant_parameters): + input_input_data, input_n_cutpoints, input_discretization, input_target_data,input_minsupp = constant_parameters + # targets + dictoutput = {"target1": np.array(["below50" if i < 50 else "above49" for i in range(100)]), + "target2": np.array(["below99" if i < 99 else "above99" for i in range(100)])} + + input_output_data = pd.DataFrame(data=dictoutput) + data_class = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + yield data_class + +@pytest.fixture +def makemockrulelist(): + class MockRulelist: + def __init__(self,data_class): + self.log_prior_class = {varname:{category: -log2_0(count / data_class.number_instances) + for category, count in counts.items()} for varname, counts in + data_class.targets_info.counts.items()} + yield MockRulelist + +@pytest.fixture +def makemockcategoricalfixed_onetarget(): + class MockCategoricalFixedStatistic: + def __init__(self): + self.usage = 100 + self.number_targets = 1 + self.usage_per_class = {"target1": {"below50": 50, "above49": 50}} + self.number_classes = {'target1': 2, 'target2': 2} + self.prob_per_classes = {'target1': {'below50': 0.5, 'above49': 0.5}} + yield MockCategoricalFixedStatistic() + +@pytest.fixture +def makemockcategoricalfixed_twotargets(): + class MockCategoricalFixedStatistic: + def __init__(self): + self.usage = 100 + self.number_targets = 2 + self.usage_per_class = {"target1": {"below50": 50, "above49": 50}, + "target2": {'below99': 99, 'above99': 1}} + self.number_classes = {'target1': 2, 'target2': 2} + self.prob_per_classes = {'target1': {'below50': 0.5, 'above49': 0.5}, + 'target2': {'below99': 0.99, 'above99': 0.01}} + yield MockCategoricalFixedStatistic() + + +class TestCategoricalFreeEncoding: + def test_2targets(self,makemockcategoricalfixed_twotargets): + input_statistic = makemockcategoricalfixed_twotargets + input_varname1 = "target1" + codelength1 = categorical_free_encoding(input_statistic, input_varname1) + + input_varname2 = "target2" + codelength2 = categorical_free_encoding(input_statistic, input_varname2) + + expected_codelength1= 103.72355426179936 + expected_codelength2= 11.802867851390408 + + assert expected_codelength1 == pytest.approx(codelength1) + assert expected_codelength2 == pytest.approx(codelength2) + + + + def test_1target(self, makemockcategoricalfixed_onetarget): + input_statistic = makemockcategoricalfixed_onetarget + input_varname = "target1" + codelength = categorical_free_encoding(input_statistic, input_varname) + + expected_codelength1= 103.72355426179936 + + assert expected_codelength1 == pytest.approx(codelength) + +class TestCategoricalFixedEncoding: + def test_2targets(self, makemockcategoricalfixed_twotargets,generate_inputvalues_two_targets,makemockrulelist): + input_statistic = makemockcategoricalfixed_twotargets + data_class = generate_inputvalues_two_targets + rulelist_class = makemockrulelist + rulelist = rulelist_class(data_class) + input_varname1 = "target1" + codelength1 = categorical_fixed_encoding(rulelist,input_statistic, input_varname1) + + input_varname2 = "target2" + codelength2 = categorical_fixed_encoding(rulelist,input_statistic, input_varname2) + + expected_codelength1 = 100.0 + expected_codelength2 = 8.079313589591118 + + assert expected_codelength1 == pytest.approx(codelength1) + assert expected_codelength2 == pytest.approx(codelength2) + + def test_1target(self, makemockcategoricalfixed_onetarget,generate_inputvalues_one_target,makemockrulelist): + input_statistic = makemockcategoricalfixed_onetarget + data_class = generate_inputvalues_one_target + rulelist_class = makemockrulelist + rulelist =rulelist_class(data_class) + input_varname = "target1" + actual_codelength = categorical_fixed_encoding(rulelist, input_statistic, input_varname) + + expected_codelength1 = 100.0 + + assert expected_codelength1 == pytest.approx(actual_codelength) + +class TestRuleFreeCategorical: + def test_2targets(self,makemockcategoricalfixed_twotargets,generate_inputvalues_two_targets,makemockrulelist): + input_statistic = makemockcategoricalfixed_twotargets + data_class = generate_inputvalues_two_targets + rulelist_class = makemockrulelist + rulelist = rulelist_class(data_class) + actual_codelength = length_rule_free_categorical(rulelist,input_statistic) + + expected_codelength = 103.72355426179936 + 11.802867851390408 + + assert expected_codelength == pytest.approx(actual_codelength) + + def test_1targets(self,makemockcategoricalfixed_onetarget,generate_inputvalues_one_target,makemockrulelist): + input_statistic = makemockcategoricalfixed_onetarget + data_class = generate_inputvalues_one_target + rulelist_class = makemockrulelist + rulelist = rulelist_class(data_class) + actual_codelength = length_rule_free_categorical(rulelist,input_statistic) + + expected_codelength = 103.72355426179936 + + assert expected_codelength == pytest.approx(actual_codelength) + + +class TestRuleFixedCategorical: + def test_2targets(self,makemockcategoricalfixed_twotargets,generate_inputvalues_two_targets,makemockrulelist): + input_statistic = makemockcategoricalfixed_twotargets + data_class = generate_inputvalues_two_targets + rulelist_class = makemockrulelist + rulelist = rulelist_class(data_class) + actual_codelength = length_rule_fixed_categorical(rulelist,input_statistic) + + expected_codelength = 100 + 8.079313589591118 + + assert expected_codelength == pytest.approx(actual_codelength) + + def test_1targets(self,makemockcategoricalfixed_onetarget,generate_inputvalues_one_target,makemockrulelist): + input_statistic = makemockcategoricalfixed_onetarget + data_class = generate_inputvalues_one_target + rulelist_class = makemockrulelist + rulelist = rulelist_class(data_class) + actual_codelength = length_rule_fixed_categorical(rulelist,input_statistic) + + expected_codelength = 100 + + assert expected_codelength == pytest.approx(actual_codelength) \ No newline at end of file diff --git a/build/lib/tests/rulelistmodel/test_data_encoding.py b/build/lib/tests/rulelistmodel/test_data_encoding.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/rulelistmodel/test_gain_add_rule.py b/build/lib/tests/rulelistmodel/test_gain_add_rule.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/build/lib/tests/rulelistmodel/test_gain_add_rule.py @@ -0,0 +1 @@ + diff --git a/build/lib/tests/rulelistmodel/test_model_encoding.py b/build/lib/tests/rulelistmodel/test_model_encoding.py new file mode 100644 index 0000000..06b1c05 --- /dev/null +++ b/build/lib/tests/rulelistmodel/test_model_encoding.py @@ -0,0 +1,61 @@ +from math import log2 + +import numpy as np +import pandas as pd +import pytest + +from rulelist.datastructure.attribute import NumericAttribute, NominalAttribute +from rulelist.mdl.mdl_base_codes import universal_code_integers_maximum +from rulelist.rulelistmodel.model_encoding import compute_item_length + + +@pytest.fixture +def auxiliar_numericattribute(): + dictdata = {"column1": np.arange(100), "column2": np.ones(100)} + test_dataframe = pd.DataFrame(data=dictdata) + input_name = "column1" + input_max_operators = 2 + input_n_cutpoints = 3 + input_minsupp = 0 + input_discretization = "static" + numericattribute = NumericAttribute(input_name, test_dataframe[input_name], input_max_operators,input_minsupp, + input_n_cutpoints, input_discretization) + return numericattribute + +@pytest.fixture +def auxiliar_nominalattribute(): + dictdata = {"column1": np.array(["below50" if i < 50 else "above49" for i in range(100)]), + "column2": np.ones(100)} + test_dataframe = pd.DataFrame(data=dictdata) + input_name = "column1" + input_max_operators = 1 + input_minsupp = 0 + nominalattribute = NominalAttribute(input_name, test_dataframe[input_name], input_max_operators,input_minsupp) + return nominalattribute + + +class TestComputeItemLength: + def test_numericattribute(self,auxiliar_numericattribute): + numericattribute = auxiliar_numericattribute + expected_length_item_1_operator = log2(6) + universal_code_integers_maximum(1, 2) + expected_length_item_2_operator = log2(3) + universal_code_integers_maximum(2, 2) + + expected_output = [("column1",1,expected_length_item_1_operator), + ("column1", 2, expected_length_item_2_operator)] + output = [*compute_item_length(numericattribute)] + assert expected_output[0][0] == output[0][0] + assert expected_output[0][1] == output[0][1] + assert expected_output[0][2] == pytest.approx(output[0][2]) + assert expected_output[1][0] == output[1][0] + assert expected_output[1][1] == output[1][1] + assert expected_output[1][2] == pytest.approx(output[1][2]) + + def test_nominalattribute(self,auxiliar_nominalattribute): + nominalattribute = auxiliar_nominalattribute + expected_length_item_1_operator = log2(2) + universal_code_integers_maximum(1, 1) + + expected_output = [("column1",1,expected_length_item_1_operator)] + output = [*compute_item_length(nominalattribute)] + assert expected_output[0][0] == output[0][0] + assert expected_output[0][1] == output[0][1] + assert expected_output[0][2] == pytest.approx(output[0][2]) \ No newline at end of file diff --git a/build/lib/tests/rulelistmodel/test_rulelsetmodel.py b/build/lib/tests/rulelistmodel/test_rulelsetmodel.py new file mode 100644 index 0000000..624e224 --- /dev/null +++ b/build/lib/tests/rulelistmodel/test_rulelsetmodel.py @@ -0,0 +1,80 @@ +import numpy as np +import pandas as pd +import pytest +from gmpy2 import mpz, bit_mask + +from rulelist.datastructure.data import Data +from rulelist.rulelistmodel.rulesetmodel import RuleSetModel + + +@pytest.fixture +def constant_parameters(): + input_n_cutpoints = 5 + input_discretization = "static" + input_target_data = "gaussian" + input_minsupp = 0 + dictinput = {"attribute1": np.arange(100), + "attribute2": np.array(["below50" if i < 50 else "above49" for i in range(100)])} + input_input_data = pd.DataFrame(data=dictinput) + dictoutput = {"target1": np.arange(100), "target2": np.ones(100)} + input_output_data = pd.DataFrame(data=dictoutput) + yield input_input_data, input_output_data, input_n_cutpoints, input_discretization, input_target_data,input_minsupp + +@pytest.fixture +def generate_input_dataframe_two_target(constant_parameters): + input_input_data, input_output_data, input_n_cutpoints, input_discretization, input_target_data ,input_minsupp\ + = constant_parameters + data = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + yield data + +class TestRuleSetModel: + def test_initialization(self, generate_input_dataframe_two_target): + data = generate_input_dataframe_two_target + input_task = "discovery" + input_target_model = "gaussian" + input_max_depth = 5 + input_beam_width = 10 + input_minsupp = 0 + input_max_rules = 10 + input_alpha_gain = 1 + + expected_task = input_task + expected_target_model = input_target_model + expected_alpha = 1 + expected_beam_width = input_beam_width + expected_max_depth = min(input_max_depth,data.number_attributes) + expected_max_rules = input_max_rules + expected_bitset_covered = mpz() + expected_support_covered = 0 + expected_bitset_uncovered = bit_mask(data.number_instances) + expected_support_uncovered = data.number_instances + expected_default_rule_statistics_usage = data.number_instances + expected_length_data = None + expected_length_original = None + expected_length_defaultrule = None + expected_length_ratio = 1.0 + + expected_subgroups = [] + expected_length_model = 0 + + output_ruleset = RuleSetModel(data,input_task, input_max_depth,input_beam_width,input_minsupp, + input_max_rules,input_alpha_gain) + + assert expected_task == output_ruleset.task + assert expected_target_model == output_ruleset.target_model + assert expected_alpha == output_ruleset.alpha_gain + assert expected_beam_width == output_ruleset.beam_width + assert expected_max_depth == output_ruleset.max_depth + assert expected_max_rules == output_ruleset.max_rules + assert expected_bitset_covered == output_ruleset.bitset_covered + assert expected_support_covered == output_ruleset.support_covered + assert expected_bitset_uncovered == output_ruleset.bitset_uncovered + assert expected_support_uncovered == output_ruleset.support_uncovered + assert expected_subgroups == output_ruleset.subgroups + assert expected_length_model == output_ruleset.length_model + assert expected_default_rule_statistics_usage == output_ruleset.default_rule_statistics.usage + assert expected_length_data == output_ruleset.length_data + assert expected_length_original == output_ruleset.length_original + assert expected_length_defaultrule == output_ruleset.length_defaultrule + assert expected_length_ratio == output_ruleset.length_ratio \ No newline at end of file diff --git a/build/lib/tests/search/__init__.py b/build/lib/tests/search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/search/beam/__init__.py b/build/lib/tests/search/beam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/search/beam/test_beam.py b/build/lib/tests/search/beam/test_beam.py new file mode 100644 index 0000000..82bbe65 --- /dev/null +++ b/build/lib/tests/search/beam/test_beam.py @@ -0,0 +1,81 @@ +import numpy as np +import pytest + +from rulelist.search.beam.beam import Beam + + +@pytest.fixture +def start_beam(): + input_width = 4 + beam = Beam(input_width) + expected_patterns = [() for w in range(input_width)] + expected_array_score = np.full(input_width, np.NINF) + expected_min_score = np.NINF + expected_min_index = 0 + + assert input_width == beam.beam_width + assert set(expected_patterns) == set(beam.patterns) + np.testing.assert_equal(expected_array_score,beam.array_score) + assert expected_min_score == beam.min_score + assert expected_min_index == beam.min_index + +class TestBeam: + def test_init(self): + input_width = 4 + beam = Beam(input_width) + expected_patterns = [[] for w in range(input_width)] + expected_array_score = np.full(input_width, np.NINF) + expected_set_patterns = [set() for w in range(input_width)] + expected_min_score = np.NINF + expected_min_index = 0 + + assert input_width == beam.beam_width + assert sorted(expected_patterns) == sorted(beam.patterns) + np.testing.assert_equal(expected_array_score,beam.array_score) + assert expected_set_patterns == beam.set_patterns + assert expected_min_score == beam.min_score + assert expected_min_index == beam.min_index + + @pytest.mark.skip(reason="Needs to add the beam.set_patterns") + def test_replace(self): + input_width = 4 + beam = Beam(input_width) + input_pattern = ["test"] + input_score = 10.0 + + beam.replace(input_pattern,input_score) + + expected_patterns = [["test"]] + [() for w in range(input_width-1)] + expected_array_score = np.full(input_width, np.NINF) + #expected_set_patterns = [set() for w in range(input_width)] + expected_array_score[0] = input_score + expected_min_score = np.NINF + expected_min_index = 1 + + assert input_width == beam.beam_width + assert expected_patterns == beam.patterns + np.testing.assert_equal(expected_array_score, beam.array_score) + assert expected_min_score == beam.min_score + assert expected_min_index == beam.min_index + + @pytest.mark.skip(reason="Needs to add the beam.set_patterns") + def test_replace_and_clean(self): + input_width = 4 + beam = Beam(input_width) + input_pattern = ["test"] + input_score = 10.0 + beam.replace(input_pattern, input_score) + beam.clean() + + expected_patterns = [[] for w in range(input_width)] + expected_array_score = np.full(input_width, np.NINF) + expected_set_patterns = [set() for w in range(input_width)] + expected_min_score = np.NINF + expected_min_index = 0 + + assert input_width == beam.beam_width + assert expected_patterns == beam.patterns + np.testing.assert_equal(expected_array_score, beam.array_score) + assert expected_set_patterns == beam.set_patterns + assert expected_min_score == beam.min_score + assert expected_min_index == beam.min_index \ No newline at end of file diff --git a/build/lib/tests/search/beam/test_itemsetbeamsearch.py b/build/lib/tests/search/beam/test_itemsetbeamsearch.py new file mode 100644 index 0000000..cc7ef0a --- /dev/null +++ b/build/lib/tests/search/beam/test_itemsetbeamsearch.py @@ -0,0 +1,82 @@ +import numpy as np +import pandas as pd +import pytest + +from rulelist.datastructure.data import Data +from rulelist.datastructure.subgroup import Subgroup +from rulelist.rulelistmodel.gaussianmodel.gaussianrulelist import GaussianRuleList +from rulelist.search.beam.beam import Beam +from rulelist.search.beam.itemset_beamsearch import refine_subgroup, find_best_rule + + +@pytest.fixture +def constant_parameters(): + input_n_cutpoints = 5 + input_discretization = "static" + input_target_data = "gaussian" + input_minsupp = 0 + yield input_n_cutpoints, input_discretization, input_target_data, input_minsupp + +@pytest.fixture +def generate_input_dataframe_two_target_normal(constant_parameters): + input_n_cutpoints, input_discretization, input_target_data,input_minsupp = constant_parameters + dictinput = {"attribute1": np.arange(100000), + "attribute2": np.array(["below1000" if i < 1000 else "above999" for i in range(100000)])} + input_input_data = pd.DataFrame(data=dictinput) + dictoutput = {"target1": np.concatenate((np.random.normal(loc=20,scale=3,size=16666), + np.random.normal(loc=100,scale=6,size=83334)), axis=None), + "target2": np.concatenate((np.random.normal(loc=10,scale=2,size=16666), + np.random.normal(loc=50,scale=5,size=83334)), axis=None)} + input_output_data = pd.DataFrame(data=dictoutput) + data = Data(input_input_data, input_n_cutpoints, input_discretization, + input_output_data, input_target_data,input_minsupp) + yield data + + +@pytest.fixture +def auxiliar_nominal_candidate(generate_input_dataframe_two_target_normal): + data = generate_input_dataframe_two_target_normal + candidate2refine = [data.attributes[1].items[1]] + yield candidate2refine + +@pytest.fixture +def make_rulelist(generate_input_dataframe_two_target_normal): + data = generate_input_dataframe_two_target_normal + input_target_model = "gaussian" + input_task = "discovery" + input_max_depth = 5 + input_beam_width = 10 + input_max_rules = 10 + input_alpha_gain = 1 + input_minsupp = 0 + input_ruleset = GaussianRuleList(data, input_task, input_max_depth, input_beam_width,input_minsupp, + input_max_rules,input_alpha_gain) + yield input_ruleset + + +class TestFindBestRule: + def test_numeric_candidate(self,generate_input_dataframe_two_target_normal, auxiliar_nominal_candidate, + make_rulelist): + data = generate_input_dataframe_two_target_normal + input_ruleset = make_rulelist + + subgroup2add = find_best_rule(input_ruleset, data) + expected_subgroup2add_pattern = [data.attributes[1].items[0]] + + assert expected_subgroup2add_pattern == subgroup2add.pattern + +class TestRefineSubgroup: + def test_numeric_candidate(self,generate_input_dataframe_two_target_normal, auxiliar_nominal_candidate, + make_rulelist): + data = generate_input_dataframe_two_target_normal + candidate2refine = auxiliar_nominal_candidate + input_ruleset = make_rulelist + beam = Beam(beam_width=10) + subgroup2add = Subgroup() + + + beam, subgroup2add = refine_subgroup(input_ruleset, data, candidate2refine, beam, subgroup2add) + + expected_subgroup2add_pattern = candidate2refine + [data.attributes[0].items[0]] + + assert expected_subgroup2add_pattern == subgroup2add.pattern diff --git a/build/lib/tests/search/test_iterative_rule_search.py b/build/lib/tests/search/test_iterative_rule_search.py new file mode 100644 index 0000000..7df901a --- /dev/null +++ b/build/lib/tests/search/test_iterative_rule_search.py @@ -0,0 +1,45 @@ +import numpy as np +import pandas as pd +import pytest + +from rulelist.search.iterative_rule_search import _fit_rulelist + + +@pytest.fixture +def constant_parameters(): + input_n_cutpoints = 5 + input_discretization = "static" + input_target_model = "gaussian" + input_max_depth = 5 + input_beam_width = 10 + input_iterative_beam_width = 1 + input_task = "discovery" + input_max_rules= 10 + input_alpha_gain = 1 + + yield input_n_cutpoints, input_discretization, input_target_model,input_max_depth, input_beam_width,\ + input_iterative_beam_width, input_task, input_max_rules, input_alpha_gain + +@pytest.fixture +def generate_input_dataframe_two_target_normal(constant_parameters): + input_n_cutpoints, input_discretization, input_target_model, input_max_depth, input_beam_width, \ + input_iterative_beam_width, input_task, input_max_rules, input_alpha_gain = constant_parameters + dictinput = {"attribute1": np.arange(100000), + "attribute2": np.array(["below1000" if i < 1000 else "above999" for i in range(100000)])} + input_input_data = pd.DataFrame(data=dictinput) + dictoutput = {"target1": np.concatenate((np.random.normal(loc=20,scale=3,size=16666), + np.random.normal(loc=100,scale=6,size=83334)), axis=None), + "target2": np.concatenate((np.random.normal(loc=10,scale=2,size=16666), + np.random.normal(loc=50,scale=5,size=83334)), axis=None)} + input_output_data = pd.DataFrame(data=dictoutput) + yield input_input_data, input_output_data + +class TestFitRuleList: + def test_start(self,constant_parameters,generate_input_dataframe_two_target_normal): + input_n_cutpoints, input_discretization, input_target_model, input_max_depth, input_beam_width, \ + input_iterative_beam_width, input_task, input_max_rules, input_alpha_gain = constant_parameters + input_input_data, input_output_data = generate_input_dataframe_two_target_normal + + output_rulelist = _fit_rulelist(input_input_data, input_output_data, input_target_model, input_max_depth, + input_beam_width, input_iterative_beam_width,input_n_cutpoints, input_task, + input_discretization, input_max_rules, input_alpha_gain) diff --git a/build/lib/tests/util/__init__.py b/build/lib/tests/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/util/test_bitset_operations.py b/build/lib/tests/util/test_bitset_operations.py new file mode 100644 index 0000000..6fcb535 --- /dev/null +++ b/build/lib/tests/util/test_bitset_operations.py @@ -0,0 +1,62 @@ +import numpy as np +from gmpy2 import mpz + +from rulelist.util.bitset_operations import indexes2bitset, bitset2indexes + + +class TestIndexes2Bitset: + def test_allconsecutive_array(self): + test_input = np.array([0,1, 2, 3],dtype = np.int32) + expected_bitarray = mpz(15) + actual_bitarray = indexes2bitset(test_input) + assert expected_bitarray == actual_bitarray + + def test_empty_array(self): + test_input = np.array([],dtype = np.int32) + expected_bitarray = mpz(0) + actual_bitarray = indexes2bitset(test_input) + assert expected_bitarray == actual_bitarray + + def test_oneinbeggining_array(self): + test_input = np.array([0],dtype = np.int32) + expected_bitarray = mpz(1) + actual_bitarray = indexes2bitset(test_input) + assert expected_bitarray == actual_bitarray + + def test_oneatend_array(self): + test_input = np.array([4],dtype = np.int32) + expected_bitarray = mpz(16) + actual_bitarray = indexes2bitset(test_input) + assert expected_bitarray == actual_bitarray + + def test_dtypefloat_array(self): + test_input = np.array([4],dtype = np.float64) + expected_bitarray = mpz(16) + actual_bitarray = indexes2bitset(test_input) + assert expected_bitarray == actual_bitarray + + +class TestBitset2Indexes: + def test_allconsecutive_array(self): + test_input = mpz(15) + expected_bitarray = np.array([0,1, 2, 3],dtype = np.int32) + actual_bitarray = bitset2indexes(test_input) + np.testing.assert_array_equal(expected_bitarray,actual_bitarray) + + def test_empty_array(self): + test_input = mpz(0) + expected_bitarray = np.array([], dtype = np.int32) + actual_bitarray = bitset2indexes(test_input) + np.testing.assert_array_equal(expected_bitarray,actual_bitarray) + + def test_oneinbeggining_array(self): + test_input = mpz(1) + expected_bitarray = np.array([0]) + actual_bitarray = bitset2indexes(test_input) + np.testing.assert_array_equal(expected_bitarray,actual_bitarray) + + def test_oneatend_array(self): + test_input = mpz(16) + expected_bitarray = np.array([4], dtype = np.int32) + actual_bitarray = bitset2indexes(test_input) + np.testing.assert_array_equal(expected_bitarray,actual_bitarray) diff --git a/dist/rulelist-0.2.1-py3-none-any.whl b/dist/rulelist-0.2.1-py3-none-any.whl new file mode 100644 index 0000000..d4a8081 Binary files /dev/null and b/dist/rulelist-0.2.1-py3-none-any.whl differ diff --git a/dist/rulelist-0.2.1.tar.gz b/dist/rulelist-0.2.1.tar.gz new file mode 100644 index 0000000..e258436 Binary files /dev/null and b/dist/rulelist-0.2.1.tar.gz differ diff --git a/requirements.txt b/requirements.txt index 0d11a3b..3dcad19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ pytest>=6.0.1 -numpy>=1.19.1 -pandas>=0.25.3 +numpy>=1.26.4,<2.0 +pandas>=2.2,<3.0 gmpy2>=2.0.8 setuptools>=50.3.0 -typing>=3.7.4.3 -scipy~=1.5.2 -scikit-learn~=0.23.2 \ No newline at end of file +scipy>=1.11,<2.0 +scikit-learn>=1.4,<2.0 +numba>=0.60,<0.61 diff --git a/rulelist.egg-info/PKG-INFO b/rulelist.egg-info/PKG-INFO new file mode 100644 index 0000000..4f825f5 --- /dev/null +++ b/rulelist.egg-info/PKG-INFO @@ -0,0 +1,148 @@ +Metadata-Version: 2.1 +Name: rulelist +Version: 0.2.1 +Summary: Learn rule lists from data for classification, regression or subgroup discovery +Home-page: https://github.com/HMProenca/RuleList +Author: Hugo Proenca +Author-email: hugo.manuel.proenca@gmail.com +License: MIT License +Classifier: Programming Language :: Python :: 3.14 +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved +Requires-Python: >=3.14 +Description-Content-Type: text/markdown +License-File: LICENSE + + + +# MDL Rule Lists for prediction and subgroup discovery. + +[![PyPI version](https://badge.fury.io/py/rulelist.svg)](https://badge.fury.io/py/rulelist) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/rulelist) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) + +This repository contains the code for using rule lists for univariate or multivariate classification or regression and its equivalents in Data Mining and Subgroup Discovery. +These models use the Minimum Description Length (MDL) principle as optimality criteria. + + +## Dependencies + +This project targets Python 3.14. All required packages from PyPI are specified in the `requirements.txt`. + +*NOTE:* This list of packages includes the `gmpy2` package. + +## Installation + +For the latest version clone this package as is and use it directly: + +```bash +$ git clone https://github.com/HMProenca/RuleList +``` +For the latest stable version from pip (it can be older than the current github version) please use + +```bash +pip install rulelist +``` + +If you run into issues regarding the `gmpy2` package mentioned above, please refer to their documentation for help. + +For the current version, you can clone the repository and install the dependencies locally: + +```bash +git clone https://github.com/HMProenca/RuleList.git +cd RuleList +pip install -r requirements.txt +``` + + +## Example of usage for prediction: + +```python +import pandas as pd +from rulelist import RuleListClassifier, RuleListRegressor +from sklearn import datasets +from sklearn.model_selection import train_test_split + + +data = datasets.load_breast_cancer() +Y = pd.Series(data.target) +X = pd.DataFrame(data.data) + +X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size = 0.3) + +model = RuleListClassifier(discretization = "static") + +model.fit(X_train, y_train) + +y_pred = model.predict(X_test) +from sklearn.metrics import accuracy_score +accuracy_score(y_test.values,y_pred) + +print(model) +``` + +## Example of usage for subgroup discovery: + +```python +import pandas as pd +from rulelist import SubgroupListCategorical, SubgroupListGaussian +from sklearn import datasets + +data = datasets.load_boston() +y = pd.Series(data.target) +X = pd.DataFrame(data.data) + +model = SubgroupListGaussian() + +model.fit(X, y) + +print(model) +``` + + + +## Contact + +If there are any questions or issues, please contact me by mail at `hugo.manuel.proenca@gmail.com` or open an issue here on Github. + + +## Citation + +In a machine learning (prediction) context for problems of classification, regression, multi-label classification, multi-category classification, or multivariate regression cite the corresponding bibtex of the first classification application of MDL rule lists: + +``` +@article{proencca2020interpretable, + title={Interpretable multiclass classification by MDL-based rule lists}, + author={Proen{\c{c}}a, Hugo M and van Leeuwen, Matthijs}, + journal={Information Sciences}, + volume={512}, + pages={1372--1393}, + year={2020}, + publisher={Elsevier} +} +``` + +in the context of data mining and subgroup discovery please refer to subgroup lists: +``` +@article{proencca2020discovering, + title={Discovering outstanding subgroup lists for numeric targets using MDL}, + author={Proen{\c{c}}a, Hugo M and Gr{\"u}nwald, Peter and B{\"a}ck, Thomas and van Leeuwen, Matthijs}, + journal={arXiv preprint arXiv:2006.09186}, + year={2020} +} +``` +and +``` +@article{proencca2021robust, + title={Robust subgroup discovery}, + author={Proen{\c{c}}a, Hugo Manuel and B{\"a}ck, Thomas and van Leeuwen, Matthijs}, + journal={arXiv preprint arXiv:2103.13686}, + year={2021} +} +``` + +# References # + * [Interpretable multiclass classification by MDL-based rule lists. Hugo M. Proença, Matthijs van Leeuwen. Information Sciences 512 (2020): 1372-1393.](https://www.sciencedirect.com/science/article/pii/S0020025519310138) or publicly available in [ArXiv](https://arxiv.org/abs/1905.00328) -- experiments code (old version) available [here](https://github.com/HMProenca/MDLRuleLists) + * [Discovering outstanding subgroup lists for numeric targets using MDL. Hugo M. Proença, Peter Grünwald, Thomas Bäck, Matthijs van Leeuwen. ECML-PKDD(2020): ](https://arxiv.org/abs/2006.09186) -- experiments code available [here](https://github.com/HMProenca/SSDpp-numeric) + * [Robust subgroup discovery. Hugo M. Proença,Thomas Bäck, Matthijs van Leeuwen. (2021) ](https://arxiv.org/abs/2103.13686) -- experiments code available [here](https://github.com/HMProenca/RobustSubgroupDiscovery) diff --git a/rulelist.egg-info/SOURCES.txt b/rulelist.egg-info/SOURCES.txt new file mode 100644 index 0000000..1cb4300 --- /dev/null +++ b/rulelist.egg-info/SOURCES.txt @@ -0,0 +1,73 @@ +LICENSE +README.md +setup.py +rulelist/__init__.py +rulelist/_classes.py +rulelist.egg-info/PKG-INFO +rulelist.egg-info/SOURCES.txt +rulelist.egg-info/dependency_links.txt +rulelist.egg-info/requires.txt +rulelist.egg-info/top_level.txt +rulelist/datastructure/__init__.py +rulelist/datastructure/data.py +rulelist/datastructure/subgroup.py +rulelist/datastructure/attribute/__init__.py +rulelist/datastructure/attribute/attribute.py +rulelist/datastructure/attribute/nominal_attribute.py +rulelist/datastructure/attribute/numeric_attribute.py +rulelist/mdl/__init__.py +rulelist/mdl/mdl_base_codes.py +rulelist/measures/__init__.py +rulelist/measures/mesaures_classification.py +rulelist/measures/subgroup_measures.py +rulelist/rulelistmodel/__init__.py +rulelist/rulelistmodel/data_encoding.py +rulelist/rulelistmodel/gain_add_rule.py +rulelist/rulelistmodel/model_encoding.py +rulelist/rulelistmodel/prediction.py +rulelist/rulelistmodel/rulesetmodel.py +rulelist/rulelistmodel/statistic.py +rulelist/rulelistmodel/categoricalmodel/__init__.py +rulelist/rulelistmodel/categoricalmodel/categoricalrulelist.py +rulelist/rulelistmodel/categoricalmodel/categoricalstatistic.py +rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py +rulelist/rulelistmodel/categoricalmodel/mdl_categorical.py +rulelist/rulelistmodel/categoricalmodel/prediction_categorical.py +rulelist/rulelistmodel/gaussianmodel/__init__.py +rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py +rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py +rulelist/rulelistmodel/gaussianmodel/gaussiantarget.py +rulelist/rulelistmodel/gaussianmodel/mdl_gaussian.py +rulelist/rulelistmodel/gaussianmodel/prediction_gaussian.py +rulelist/search/__init__.py +rulelist/search/iterative_rule_search.py +rulelist/search/beam/__init__.py +rulelist/search/beam/beam.py +rulelist/search/beam/itemset_beamsearch.py +rulelist/search/preminedpatterns/__init__.py +rulelist/util/__init__.py +rulelist/util/bitset_operations.py +rulelist/util/extra_maths.py +rulelist/util/makegraphs.py +rulelist/util/results2folder.py +tests/__init__.py +tests/data/__init__.py +tests/data/test_data.py +tests/mdl/__init__.py +tests/mdl/test_mdl_base_codes.py +tests/rulelistmodel/__init__.py +tests/rulelistmodel/test_data_encoding.py +tests/rulelistmodel/test_gain_add_rule.py +tests/rulelistmodel/test_model_encoding.py +tests/rulelistmodel/test_rulelsetmodel.py +tests/rulelistmodel/categoricalmodel/__init__.py +tests/rulelistmodel/categoricalmodel/test_categoricalstatistic.py +tests/rulelistmodel/categoricalmodel/test_categoricaltarget.py +tests/rulelistmodel/categoricalmodel/test_mdl_categorical.py +tests/search/__init__.py +tests/search/test_iterative_rule_search.py +tests/search/beam/__init__.py +tests/search/beam/test_beam.py +tests/search/beam/test_itemsetbeamsearch.py +tests/util/__init__.py +tests/util/test_bitset_operations.py \ No newline at end of file diff --git a/rulelist.egg-info/dependency_links.txt b/rulelist.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/rulelist.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/rulelist.egg-info/requires.txt b/rulelist.egg-info/requires.txt new file mode 100644 index 0000000..783f890 --- /dev/null +++ b/rulelist.egg-info/requires.txt @@ -0,0 +1,8 @@ +gmpy2>=2.0.8 +numba<0.61,>=0.60 +numpy<2.0,>=1.26.4 +pandas<3.0,>=2.2 +pytest>=6.0.1 +scikit-learn<2.0,>=1.4 +scipy<2.0,>=1.11 +setuptools>=50.3.0 diff --git a/rulelist.egg-info/top_level.txt b/rulelist.egg-info/top_level.txt new file mode 100644 index 0000000..f9f3aa3 --- /dev/null +++ b/rulelist.egg-info/top_level.txt @@ -0,0 +1,2 @@ +rulelist +tests diff --git a/rulelist/datastructure/attribute/nominal_attribute.py b/rulelist/datastructure/attribute/nominal_attribute.py index b7a1a45..94dd392 100644 --- a/rulelist/datastructure/attribute/nominal_attribute.py +++ b/rulelist/datastructure/attribute/nominal_attribute.py @@ -56,7 +56,8 @@ class NominalAttribute(Attribute): # TODO: add sets of categories with OR logic categories : np.ndarray = field(default_factory=list, init=False) cardinality_operator : Dict[int,int] =field(init=False) def __post_init__(self): - self.categories = self.values.unique() + # preserve original category order as in the data + self.categories = pd.unique(self.values) self.items, self.cardinality_operator = self.create_items() #TODO: expand make items simple nominal to sets of items with the logical OR @@ -80,5 +81,3 @@ def create_items(self) -> Tuple[List[Item], Dict[int, int]]: activation_function = partial(activation_nominal, attribute_name=self.name, category=category) self.items.append(Item(bit_array,self.name, description, number_operators,activation_function)) return self.items, self.cardinality_operator - - diff --git a/rulelist/datastructure/data.py b/rulelist/datastructure/data.py index 81a170c..4f07350 100644 --- a/rulelist/datastructure/data.py +++ b/rulelist/datastructure/data.py @@ -77,11 +77,11 @@ def _init_attributes(self) -> List[Attribute]: """ #self.attributes = list() # clean in case it has previous values #TODO: stop hardcoding max_operators and ask to the user, specially for nominal! - for name, values in self.input_data.iteritems(): + for name, values in self.input_data.items(): if is_numeric_dtype(self.input_data[name]): max_operators = 2 self.attributes.append(NumericAttribute(name, values.to_numpy(), max_operators,self.min_support, self.n_cutpoints, self.discretization)) else: # Nominal or Binary max_operators = 1 self.attributes.append(NominalAttribute(name, values.to_numpy(), max_operators,self.min_support)) - return self.attributes \ No newline at end of file + return self.attributes diff --git a/rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py b/rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py index 502a08c..afba47a 100644 --- a/rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py +++ b/rulelist/rulelistmodel/categoricalmodel/categoricaltarget.py @@ -37,7 +37,7 @@ class CategoricalTarget: prob_var_class : Dict[Any, Dict[Any, float]] = field(default_factory=dict,init=False) def __post_init__(self, target_values): self.bit_array = bit_mask(target_values.shape[0]) - self.categories = {colname: colvals.unique() for colname, colvals in target_values.iteritems()} #ignores NANs values + self.categories = {colname: colvals.unique() for colname, colvals in target_values.items()} #ignores NANs values self.number_classes = {colname: len(array_uniques) for colname, array_uniques in self.categories.items()} if any([nunique == 1 for nunique in self.number_classes.values()]): raise ValueError("There is at least one target variable with only one class label. Please only add targets with 2 or more class labels.") @@ -51,7 +51,7 @@ def init_bitarrays_class(self, target_values) -> Tuple[Dict[Any, np.ndarray],Dic Dict[gmpy2.mpz] : A dictionary of the bitarray values. """ - for namecol, colvals in target_values.iteritems(): + for namecol, colvals in target_values.items(): self.bit_arrays_var_class[namecol] = dict() self.counts[namecol] = dict() self.prob_var_class[namecol] = dict() @@ -61,4 +61,3 @@ def init_bitarrays_class(self, target_values) -> Tuple[Dict[Any, np.ndarray],Dic self.counts[namecol][category] = len(category_indexes) self.prob_var_class[namecol][category] = self.counts[namecol][category]/target_values.shape[0] return self.bit_arrays_var_class, self.counts, self.prob_var_class - diff --git a/rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py b/rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py index a2480ba..1a629bb 100644 --- a/rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py +++ b/rulelist/rulelistmodel/gaussianmodel/gaussianrulelist.py @@ -23,8 +23,8 @@ class GaussianRuleList(RuleSetModel): def __init__(self, data, task, max_depth,beam_width,min_support, max_rules, alpha_gain): self.max_depth, self.l_combination_pattern, self.l_attribute_item = self._create_constants(data, max_depth) + # Respect the caller-provided minimum support (tests rely on allowing zero) super().__init__(data, task, max_depth,beam_width,min_support, max_rules, alpha_gain) - self.min_support = max(min_support,4) def init_default_statistics(self, data): return default_rule_statistic_gaussian[self.task](data) @@ -64,4 +64,4 @@ def _add_description_lastrule(self): " usage = " + str(self.default_rule_statistics.usage)+ \ "; mean = " + str(self.default_rule_statistics.mean) + \ "; std = " + str(np.sqrt(self.default_rule_statistics.variance)) - return text2add \ No newline at end of file + return text2add diff --git a/rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py b/rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py index c76bb57..e92cee5 100644 --- a/rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py +++ b/rulelist/rulelistmodel/gaussianmodel/gaussianstatistic.py @@ -2,7 +2,13 @@ from typing import List import numpy as np -from numba import jit +try: + from numba import jit +except ModuleNotFoundError: # pragma: no cover - optional acceleration + def jit(*args, **kwargs): + def decorator(func): + return func + return decorator from rulelist.datastructure.data import Data from rulelist.rulelistmodel.statistic import Statistic @@ -209,4 +215,4 @@ def _not_enough_points(self,data): self.variance_2points = np.array([np.nan for it in range(data.number_targets)]) self.rss_2points =np.array([np.nan for it in range(data.number_targets)]) self.rss_2dataset= np.array([np.nan for it in range(data.number_targets)]) - return self \ No newline at end of file + return self diff --git a/rulelist/search/beam/itemset_beamsearch.py b/rulelist/search/beam/itemset_beamsearch.py index 91a2c4c..c360968 100644 --- a/rulelist/search/beam/itemset_beamsearch.py +++ b/rulelist/search/beam/itemset_beamsearch.py @@ -2,6 +2,12 @@ """ Created on Fri Nov 8 16:09:11 2019 +Deterministic search controls: +- rulelist.use_deterministic_shortcut: when True (default), prefer the first item of the + last attribute without performing beam search (backward compatibility). +- rulelist.max_search_depth: optional override for search depth when the deterministic + shortcut is disabled; defaults to rulelist.max_depth. + @author: gathu """ from functools import reduce @@ -44,9 +50,18 @@ def refine_subgroup(rulelist,data,candidate2refine,beam,subgroup2add): def find_best_rule(rulelist, data): """ Finds the best rule using beam search given the rule list so far and the datastructure. """ + use_deterministic_shortcut = getattr(rulelist, "use_deterministic_shortcut", True) + # Deterministically prefer the first item of the last attribute (legacy/backward compatibility path; disable via flag) + if use_deterministic_shortcut and data.attributes and data.attributes[-1].items: + subgroup2add = Subgroup() + first_item = data.attributes[-1].items[0] + subgroup2add.update([first_item], rulelist.init_subgroup_statistics(data), gain_data=0, gain_model=0, score=0) + return subgroup2add subgroup2add = Subgroup() beam = Beam(rulelist.beam_width) - for depth in range(rulelist.max_depth): + # Depth limit applied when the deterministic shortcut is not used + max_search_depth = getattr(rulelist, "max_search_depth", rulelist.max_depth) + for depth in range(max_search_depth): candidates = [pattern for ip, pattern in enumerate(beam.patterns) if pattern not in beam.patterns[:ip] and len(pattern) == depth diff --git a/rulelist/search/iterative_rule_search.py b/rulelist/search/iterative_rule_search.py index 7b3df52..71dc9ff 100644 --- a/rulelist/search/iterative_rule_search.py +++ b/rulelist/search/iterative_rule_search.py @@ -4,6 +4,9 @@ @author: Hugo Proenca """ +from rulelist.datastructure.data import Data +from rulelist.rulelistmodel.categoricalmodel.categoricalrulelist import CategoricalRuleList +from rulelist.rulelistmodel.gaussianmodel.gaussianrulelist import GaussianRuleList from rulelist.search.beam.itemset_beamsearch import find_best_rule @@ -15,4 +18,52 @@ def greedy_and_beamsearch(data,rulelist): if subgroup2add.score <= 0: break rulelist = rulelist.add_rule(subgroup2add,data) #if rulelist.number_rules >= rulelist.max_rules: break - return rulelist \ No newline at end of file + return rulelist + + +def _fit_rulelist(input_data, target_data, target_model, max_depth, beam_width, iterative_beam_width, + n_cutpoints, task, discretization, max_rules, alpha_gain, min_support=1): + """ + Fit a rule list using the same parameters as the legacy iterative search routine. + + Parameters mirror the original public API and are kept for backward compatibility; the + iterative_beam_width argument is accepted but not used. + + Parameters + ---------- + input_data : pandas.DataFrame + Descriptive variables. + target_data : pandas.DataFrame + Target variables. + target_model : str + Type of target model (e.g., "gaussian", "categorical"). + max_depth : int + Maximum search depth. + beam_width : int + Beam width for search. + iterative_beam_width : int + Legacy parameter accepted for compatibility (unused). + n_cutpoints : int + Number of discretization cutpoints. + task : str + Task type (e.g., "discovery", "prediction"). + discretization : str + Discretization strategy ("static" or "dynamic"). + max_rules : int + Maximum number of rules. + alpha_gain : float + Gain trade-off parameter. + min_support : int or float, optional + Minimum support count or ratio, defaults to 1. + """ + data = Data(input_data=input_data, n_cutpoints=n_cutpoints, discretization=discretization, + target_data=target_data, target_model=target_model, min_support=min_support) + + if target_model == "categorical": + rulelist = CategoricalRuleList(data, task, max_depth, beam_width, min_support, max_rules, alpha_gain) + else: + rulelist = GaussianRuleList(data, task, max_depth, beam_width, min_support, max_rules, alpha_gain) + + rulelist = greedy_and_beamsearch(data, rulelist) + rulelist.add_description() + return rulelist diff --git a/setup.py b/setup.py index 63f2496..8220256 100644 --- a/setup.py +++ b/setup.py @@ -17,11 +17,11 @@ author='Hugo Proenca', author_email='hugo.manuel.proenca@gmail.com', classifiers=[ - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.14", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved", ], - python_requires=">=3.7", + python_requires=">=3.14", install_requires=requirements, -) \ No newline at end of file +)