Repository: pjaselin/Cubist (language: python)

Issue:
Fix/examples



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index a5030df..41571d6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -26,15 +26,15 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
     runs-on: ${{ matrix.os }}
 
     steps:
     - name: Checkout repo
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     
@@ -57,7 +57,7 @@ jobs:
         pip3 install --upgrade pip
         pip3 install setuptools wheel build cython
         pip3 install flake8 pytest pytest-cov numpy
-        pip install -e .
+        pip install .
 
     - name: Lint with flake8
       run: |
@@ -80,11 +80,11 @@ jobs:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
     env:
-      CIBW_SKIP: "pp* *-win32 cp310-*"
+      CIBW_SKIP: "pp* *-win32"
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
-      - uses: actions/setup-python@v2
+      - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.9'
@@ -106,7 +106,7 @@ jobs:
       - name: Build wheels
         uses: pypa/cibuildwheel@v2.2.2
       
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v3
         with:
           path: ./wheelhouse/*.whl
 
@@ -116,10 +116,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Checkout repo
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: "3.9"
 
@@ -127,7 +127,7 @@ jobs:
       run: |
         python -m pip install setuptools wheel build cython numpy
         python setup.py sdist
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@v3
       with:
         path: dist/*.tar.gz
   
@@ -144,7 +144,7 @@ jobs:
         mkdir dist
 
     - name: download artifacts
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         name: artifact
         path: dist
diff --git a/.gitignore b/.gitignore
index 0eefe09..95416dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@ __pycache__/
 # C extensions
 *.so
 
+# PyCharm
+.idea
 
 # MacOS
 .DS_Store
diff --git a/README.md b/README.md
index 9c0b191..02226f7 100644
--- a/README.md
+++ b/README.md
@@ -43,9 +43,9 @@ The above image is a sample of the verbose output produced by Cubist. It first r
 The following parameters can be passed as arguments to the ```Cubist()``` class instantiation:
 - n_rules (int, default=500): Limit of the number of rules Cubist will build. Recommended value is 500.
 - n_committees (int, default=0): Number of committees to construct. Each committee is a rule based model and beyond the first tries to correct the prediction errors of the prior constructed model. Recommended value is 5.
-- neighbors (int, default=None): Number between 1 and 9 for how many instances should be used to correct the rule-based prediction.
+- neighbors (int, default=None): Number between 1 and 9 for how many instances should be used to correct the rule-based prediction. If no value is given, Cubist will build a rule-based model only. If this value is set, Cubist will create a composite model with the given number of neighbors. Regardless of the value set, if auto=True, Cubist may override this input and choose a different number of neighbors. Please assess the model for the selected value for the number of neighbors used.
 - unbiased (bool, default=False): Should unbiased rules be used? Since Cubist minimizes the MAE of the predicted values, the rules may be biased and the mean predicted value may differ from the actual mean. This is recommended when there are frequent occurrences of the same value in a training dataset. Note that MAE may be slightly higher.
-- composite (True, False, or 'auto', default=False): A composite model is a combination of Cubist's rule-based model and instance-based or nearest-neighbor models to improve the predictive performance of the returned model. A value of True requires Cubist to include the nearest-neighbor model, False will ensure Cubist only generates a rule-based model, and 'auto' allows the algorithm to choose whether to use nearest-neighbor corrections.
+- auto (bool, default=False): A value of True allows the algorithm to choose whether to use nearest-neighbor corrections and how many neighbors to use. False will leave the choice of whether to use a composite model to value passed to the `neighbors` parameter.
 - extrapolation (float, default=0.05): Adjusts how much rule predictions are adjusted to be consistent with the training dataset. Recommended value is 5% as a decimal (0.05)
 - sample (float, default=None): Percentage of the data set to be randomly selected for model building (0.0 or greater but less than 1.0).
 - cv (int, default=None): Whether to carry out cross-validation (recommended value is 10)
@@ -55,7 +55,7 @@ The following parameters can be passed as arguments to the ```Cubist()``` class
 
 ## Considerations
 - For small datasets, using the `sample` parameter is probably inadvisable because Cubist won't have enough samples to produce a representative model.
-- If you are looking for fast inferencing and can spare accuracy, skip using a composite model with `composite=False`.
+- If you are looking for fast inferencing and can spare accuracy, skip using a composite model by not setting a value for `neighbors`.
 
 ## Model Attributes
 The following attributes are exposed to understand the Cubist model results:
diff --git a/cubist/__init__.py b/cubist/__init__.py
index 5575562..013659b 100644
--- a/cubist/__init__.py
+++ b/cubist/__init__.py
@@ -16,6 +16,6 @@
 from .cubist import Cubist
 from .exceptions import CubistError
 
-from ._version import __version__
+from .__version__ import __version__
 
 __all__ = ['Cubist', 'CubistError', '__version__']
diff --git a/cubist/__version__.py b/cubist/__version__.py
new file mode 100644
index 0000000..3dc1f76
--- /dev/null
+++ b/cubist/__version__.py
@@ -0,0 +1,1 @@
+__version__ = "0.1.0"
diff --git a/cubist/_export_files.py b/cubist/_export_files.py
deleted file mode 100644
index e69de29..0000000
diff --git a/cubist/_make_data_string.py b/cubist/_make_data_string.py
index 0cd9909..cf6d442 100644
--- a/cubist/_make_data_string.py
+++ b/cubist/_make_data_string.py
@@ -2,17 +2,17 @@
 from pandas.api.types import is_string_dtype, is_numeric_dtype
 import numpy as np
 
-from ._make_names_string import escapes
+from ._make_names_string import _escapes
 
 
-def r_format(x: float, digits: int = 15) -> str:
+def _r_format(x: float, digits: int = 15) -> str:
     """Python version of the R format function to return a number formatted as a 
     string rounded to `digits` number of digits from the left."""
     # if x is NA return NA
     if pd.isna(x):
         return x
     if np.iscomplex(x):
-        raise ValueError("Complex data not supported")
+        raise ValueError("Complex numbers not supported")
 
     # get the count of whole number digits, i.e. the number of digits to the left of the decimal place
     whole_nums_count = len(str(int(x)))
@@ -25,7 +25,7 @@ def r_format(x: float, digits: int = 15) -> str:
         return str(x)
 
 
-def make_data_string(x, y=None, w=None):
+def _make_data_string(x, y=None, w=None):
     """
     Converts input dataset array X into a string.
 
@@ -50,7 +50,7 @@ def make_data_string(x, y=None, w=None):
     # apply the escapes function to all string columns
     for col in x:
         if is_string_dtype(x[col]):
-            x[col] = escapes(x[col].astype(str))
+            x[col] = _escapes(x[col].astype(str))
 
     # if y is None for model predictions, set y as a column of NaN values, which will become ?'s later
     if y is None:
@@ -60,7 +60,7 @@ def make_data_string(x, y=None, w=None):
         y = y.copy(deep=True)
 
     # format the y column for special charactesrs
-    y = pd.Series(escapes(y.astype(str)))
+    y = pd.Series(_escapes(y.astype(str)))
 
     # insert the y column as the first column of x
     x.insert(0, "y", y)
@@ -74,7 +74,7 @@ def make_data_string(x, y=None, w=None):
     # convert all columns to strings
     for col in x:
         if is_numeric_dtype(x[col]):
-            x[col] = x[col].apply(r_format)
+            x[col] = x[col].apply(_r_format)
             x[col] = x[col].astype(str)
         else:
             x[col] = x[col].astype(str)
diff --git a/cubist/_make_names_string.py b/cubist/_make_names_string.py
index 56e87f8..2bffaaa 100644
--- a/cubist/_make_names_string.py
+++ b/cubist/_make_names_string.py
@@ -5,10 +5,10 @@
 import pandas as pd
 import numpy as np
 
-from ._quinlan_attributes import quinlan_attributes
+from ._quinlan_attributes import _quinlan_attributes
 
 
-def make_names_string(x, w=None, label="outcome"):
+def _make_names_string(x, w=None, label="outcome"):
     """
     Create the names string to pass to Cubist. This string contains information about Python and the time of run along
     with the column names and their data types.
@@ -50,14 +50,14 @@ def make_names_string(x, w=None, label="outcome"):
     out = f'{out}\n{label}.\n{label}{outcome_type}'
 
     # get dictionary of feature names as keys and data types as values
-    var_data = quinlan_attributes(x)
+    var_data = _quinlan_attributes(x)
 
     # if weights are present add this to var_data
     if w is not None:
         var_data["case weight"] = "continuous."
 
     # join the column names and data types into a single string
-    var_data = [f'{escapes([key])[0]}: {value}' for key, value in var_data.items()]
+    var_data = [f'{_escapes([key])[0]}: {value}' for key, value in var_data.items()]
     var_data = '\n'.join(var_data)
 
     # merge the out and var_data strings
@@ -65,7 +65,7 @@ def make_names_string(x, w=None, label="outcome"):
     return out
 
 
-def escapes(x, chars=None):
+def _escapes(x, chars=None):
     """Double escape reserved and special characters in x."""
     # set custom reserved characters list
     if chars is None:
@@ -76,21 +76,22 @@ def escapes(x, chars=None):
         for i in chars:
             x = [c.replace(i, f'\\{i}') for c in x]
         # apply second escaping
-        x = [re_escape(c) for c in x]
+        x = [_re_escape(c) for c in x]
     else:
         # apply first escaping
         for i in chars:
             x = x.replace(i, f'\\{i}')
         # apply second escaping
-        x = re_escape(x)
+        x = _re_escape(x)
     return x
 
 
 _special_chars_map = {i: '\\' + chr(i) for i in b'()[]{}?*+-|:;^$\\.&~#\t\n\r\v\f'}
 
 
-def re_escape(pattern):
-    """Escape special characters in a string. Sourced from 're' Python package."""
+def _re_escape(pattern):
+    """Escape special characters in a string. 
+    Sourced from 're' Python package."""
     if isinstance(pattern, str):
         return pattern.translate(_special_chars_map)
     else:
diff --git a/cubist/_parse_model.py b/cubist/_parse_model.py
index f520841..f8ffd4f 100644
--- a/cubist/_parse_model.py
+++ b/cubist/_parse_model.py
@@ -15,7 +15,7 @@
 }
 
 
-def split_to_groups(x, f):
+def _split_to_groups(x, f):
     """Function to convert two lists into a dictionary where the keys are unique values in f and 
     the values are lists of the corresponding values in x. Analogous to the split function in R."""
     if len(x) != len(f):
@@ -29,7 +29,7 @@ def split_to_groups(x, f):
     return groups
 
 
-def parse_model(model, x):
+def _parse_model(model, x):
     # split on newline
     model = model.split("\n")
     # remove empty strings
@@ -46,7 +46,7 @@ def parse_model(model, x):
     # loop through model and indicate
     for i, row in enumerate(model):
         # break each row of x into dicts for each key/value pair
-        tt = parser(row)
+        tt = _parser(row)
         # get the first key in the first entry of tt
         first_key = list(tt[0])[0]
 
@@ -85,7 +85,7 @@ def parse_model(model, x):
     for i in is_type2:
         # set the type of split
         split_type[i] = "continuous"
-        continuous_split = type2(model[i])
+        continuous_split = _type2(model[i])
         # set split variable name
         split_var[i] = continuous_split["var"].replace('\"', "")
         # set split direction (comparison operator)
@@ -96,7 +96,7 @@ def parse_model(model, x):
     # handle categorical (type 3) rules
     is_type3 = [i for i, c in enumerate(model) if re.search("^type=\"3\"", c)]
     for i in is_type3:
-        categorical_split = type3(model[i])
+        categorical_split = _type3(model[i])
         # set the type of split
         split_type[i] = "categorical"
         # set the split variable
@@ -138,7 +138,7 @@ def parse_model(model, x):
     # get the indices of rows in model that contain model coefficients
     is_eqn = [i for i, c in enumerate(model) if "coeff=" in c]
     # extract the model coefficients from the row
-    coefs = [eqn(model[i], var_names=list(x.columns)) for i in is_eqn]
+    coefs = [_eqn(model[i], var_names=list(x.columns)) for i in is_eqn]
     out = pd.DataFrame(coefs)
     # get the committee number
     out["committee"] = [com_num[i] for i in is_eqn]
@@ -148,7 +148,7 @@ def parse_model(model, x):
     return split_data, out
 
 
-def type2(x, dig=3):
+def _type2(x, dig=3):
     x = x.replace("\"", "")
 
     # get the indices where these keywords start
@@ -173,7 +173,7 @@ def type2(x, dig=3):
             "text": f"{var} {result} {val}"}
 
 
-def type3(x):
+def _type3(x):
     # get the indices where these keywords start
     att_ind = x.find("att=")
     elts_ind = x.find("elts=")
@@ -194,7 +194,7 @@ def type3(x):
     return {"var": var, "val": val, "text": txt}
 
 
-def eqn(x, var_names=None):
+def _eqn(x, var_names=None):
     x = x.replace("\"", "")
     starts = [m.start(0) for m in re.finditer("(coeff=)|(att=)", x)]
     tmp = [""] * len(starts)
@@ -220,7 +220,7 @@ def eqn(x, var_names=None):
     return vals
 
 
-def make_parsed_dict(x):
+def _make_parsed_dict(x):
     x = x.split("=")
     if len(x) > 1:
         return {x[0]: x[1]}
@@ -228,7 +228,7 @@ def make_parsed_dict(x):
         return None
 
 
-def parser(x):
+def _parser(x):
     x = x.split(" ")
-    x = [make_parsed_dict(c) for c in x]
+    x = [_make_parsed_dict(c) for c in x]
     return x
diff --git a/cubist/_quinlan_attributes.py b/cubist/_quinlan_attributes.py
index 2688587..5aaf23e 100644
--- a/cubist/_quinlan_attributes.py
+++ b/cubist/_quinlan_attributes.py
@@ -49,7 +49,7 @@ def _get_data_format(x: pd.Series):
         raise ValueError(f"Dtype {x.dtype} is not supported")
 
 
-def quinlan_attributes(df: pd.DataFrame) -> dict:
+def _quinlan_attributes(df: pd.DataFrame) -> dict:
     """
     Function to collect the data formatting information for each column in a Pandas DataFrame.
 
diff --git a/cubist/_variable_usage.py b/cubist/_variable_usage.py
index 0fece02..7841eea 100644
--- a/cubist/_variable_usage.py
+++ b/cubist/_variable_usage.py
@@ -1,7 +1,7 @@
 import pandas as pd
 
 
-def get_variable_usage(output, x):
+def _get_variable_usage(output, x):
     output = output.split("\n")
     # get the attribute usage section of the model output
     start_vars = [i for i, c in enumerate(output) if '\tAttribute usage' in c]
@@ -14,9 +14,9 @@ def get_variable_usage(output, x):
     if len(has_pct) < 1:
         return None
     output = [output[i] for i in has_pct]
-    values = [get_values(c) for c in output]
+    values = [_get_values(c) for c in output]
     values = pd.DataFrame(values, columns=["Conditions", "Model"])
-    values["Variable"] = [get_variable(c) for c in output]
+    values["Variable"] = [_get_variable(c) for c in output]
 
     if values.shape[0] < x.shape[1]:
         x_names = set(x.columns)
@@ -32,7 +32,7 @@ def get_variable_usage(output, x):
     return values
 
 
-def get_values(x):
+def _get_values(x):
     x2 = x.split(" ")
     has_pct = ["%" in c for c in x2]
     if sum(has_pct) == 2:
@@ -54,6 +54,6 @@ def get_values(x):
             return 0
 
 
-def get_variable(x):
+def _get_variable(x):
     x = x.split(" ")
     return x[-1]
diff --git a/cubist/_version.py b/cubist/_version.py
deleted file mode 100644
index d62d967..0000000
--- a/cubist/_version.py
+++ /dev/null
@@ -1,1 +0,0 @@
-__version__ = "0.0.16"
diff --git a/cubist/cubist.py b/cubist/cubist.py
index 5351351..da61765 100644
--- a/cubist/cubist.py
+++ b/cubist/cubist.py
@@ -1,19 +1,18 @@
 import zlib
 from warnings import warn
-from typing import Type, Union
 
 import numpy as np
 import pandas as pd
-
 from sklearn.utils.validation import check_is_fitted, check_random_state, \
     _check_sample_weight
 from sklearn.base import RegressorMixin, BaseEstimator
 
-from ._make_names_string import make_names_string
-from ._make_data_string import make_data_string
-from ._parse_model import parse_model
-from ._variable_usage import get_variable_usage
 from _cubist import _cubist, _predictions
+
+from ._make_names_string import _make_names_string
+from ._make_data_string import _make_data_string
+from ._parse_model import _parse_model
+from ._variable_usage import _get_variable_usage
 from .exceptions import CubistError
 
 
@@ -35,12 +34,15 @@ class Cubist(BaseEstimator, RegressorMixin):
         Number of committees to construct. Each committee is a rule based model 
         and beyond the first tries to correct the prediction errors of the prior 
         constructed model. Recommended value is 5.
-
-    neighbors : int or None, default=None
+    
+    neighbors : int, default=None
         Number between 1 and 9 for how many instances should be used to correct 
-        the rule-based prediction. Only used when composite is True or 'auto'. 
-        If neighbors=0 and composite=False or 'auto', Cubist will choose a value
-        for this parameter.
+        the rule-based prediction. If no value is given, Cubist will build a
+        rule-based model only. If this value is set, Cubist will create a 
+        composite model with the given number of neighbors. Regardless of the 
+        value set, if auto=True, Cubist may override this input and choose a 
+        different number of neighbors. Please assess the model for the selected 
+        value for the number of neighbors used.
 
     unbiased : bool, default=False
         Should unbiased rules be used? Since Cubist minimizes the MAE of the 
@@ -49,13 +51,11 @@ class Cubist(BaseEstimator, RegressorMixin):
         frequent occurrences of the same value in a training dataset. Note that 
         MAE may be slightly higher.
     
-    composite : bool or 'auto', default=False
-        A composite model is a combination of Cubist's rule-based model and 
-        instance-based or nearest-neighbor models to improve the predictive
-        performance of the returned model. A value of True requires Cubist to
-        include the nearest-neighbor model, False will ensure Cubist only
-        generates a rule-based model, and 'auto' allows the algorithm to choose
-        whether to use nearest-neighbor corrections.
+    auto : bool, default=False
+        A value of True allows the algorithm to choose whether to use 
+        nearest-neighbor corrections and how many neighbors to use. False will
+        leave the choice of whether to use a composite model to value passed to
+        the `neighbors` parameter.
 
     extrapolation : float, default=0.05
         Adjusts how much rule predictions are adjusted to be consistent with 
@@ -64,8 +64,9 @@ class Cubist(BaseEstimator, RegressorMixin):
     sample : float, default=None
         Percentage of the data set to be randomly selected for model building.
     
-    cv : int or None, default=None
-        Whether to carry out cross-validation (recommended value is 10)
+    cv : int, default=None
+        Whether to carry out cross-validation and how many folds to use
+        (recommended value is 10 per Quinlan)
 
     random_state : int, default=None
         An integer to set the random seed for the C Cubist code.
@@ -124,12 +125,11 @@ class Cubist(BaseEstimator, RegressorMixin):
     """
 
     def __init__(self,
-                 n_rules: int = 500,
-                 *,
+                 n_rules: int = 500, *,
                  n_committees: int = 1,
                  neighbors: int = None,
                  unbiased: bool = False,
-                 composite: Union[bool, str] = False,
+                 auto: bool = False,
                  extrapolation: float = 0.05,
                  sample: float = None,
                  cv: int = None,
@@ -142,90 +142,118 @@ def __init__(self,
         self.n_committees = n_committees
         self.neighbors = neighbors
         self.unbiased = unbiased
-        self.composite = composite
+        self.auto = auto
         self.extrapolation = extrapolation
         self.sample = sample
         self.cv = cv
         self.random_state = random_state
         self.target_label = target_label
         self.verbose = verbose
-    
+
     def _more_tags(self):
+        """scikit-learn estimator configuration method
+        """
         return {"allow_nan": True,
                 "X_types": ["2darray", "string"]}
-    
-    def _validate_model_parameters(self):
-        """Validate inputs to the Cubist model
 
-        Raises:
-            ValueError for invalid model parameter inputs
-        """
+    def _check_n_rules(self):
+        # validate number of rules
         if not isinstance(self.n_rules, int):
-            raise TypeError("Number of rules must be an integer")
-        elif self.n_rules < 1 or self.n_rules > 1000000:
-            raise ValueError("Number of rules must be between 1 and 1000000")
+            raise TypeError("`n_rules` must be an integer")
+        if self.n_rules < 1 or self.n_rules > 1000000:
+            raise ValueError("`n_rules` must be between 1 and 1000000")
+        return self.n_rules
 
+    def _check_n_committees(self):
+        # validate number of committees
         if not isinstance(self.n_committees, int):
-            raise TypeError("Number of committees must be an integer")
-        elif self.n_committees < 1 or self.n_committees > 100:
-            raise ValueError("Number of committees must be between 1 and 100")
-        
-        if self.neighbors:
-            if self.composite is False:
-                raise ValueError("`neighbors` should not be set when "
-                                 "`composite` is False")
-            elif not isinstance(self.neighbors, int):
-                raise TypeError("Number of neighbors must be an integer")
+            raise TypeError("`n_committees` must be an integer")
+        if self.n_committees < 1 or self.n_committees > 100:
+            raise ValueError("`n_committees` must be between 1 and 100")
+        return self.n_committees
+
+    def _check_neighbors(self):
+        # validate number of neighbors
+        if self.neighbors is not None:
+            if not isinstance(self.neighbors, int):
+                raise TypeError("`neighbors` must be an integer")
             elif self.neighbors < 1 or self.neighbors > 9:
-                raise ValueError("'neighbors' must be between 1 and 9")
+                raise ValueError("`neighbors` must be between 1 and 9")
+            elif self.auto:
+                warn("Cubist will choose an appropriate value for `neighbor`."
+                     "Cubist will receive neighbors = 0 regardless of the set"
+                     "value for `neighbors`.", stacklevel=3)
+                return 0
             else:
-                self.neighbors_ = self.neighbors
+                return self.neighbors
+        # default value must be zero even when not used
+        return 0
+
+    def _check_unbiased(self):
+        # validate unbiased option
+        if not isinstance(self.unbiased, bool):
+            raise ValueError("Wrong input for parameter `unbiased`. Expected "
+                             f"True or False, got {self.unbiased}")
+        return self.unbiased
+
+    def _check_composite(self, neighbors):
+        # validate the auto parameter
+        if not isinstance(self.auto, bool):
+            raise ValueError("Wrong input for parameter `auto`. Expected "
+                             f"True or False, got {self.auto}")
+        # if auto=True, let cubist decide whether to use a composite model and
+        # how many neighbors to use
+        elif self.auto:
+            return 'auto'
+        # if a number of neighbors is given, make a composite model
+        elif neighbors > 0:
+            return 'yes'
         else:
-            if self.composite:
-                warn("Cubist will choose an appropriate value for `neighbor` "
-                     "as this is not set.", stacklevel=3)
-            self.neighbors_ = 0
-        
-        if self.composite not in [True, False, 'auto']:
-            raise ValueError(f"Wrong input for parameter `composite`. Expected "
-                             f"True, False, or 'auto', got {self.composite}")
-        else:
-            if self.composite is True:
-                self.composite_ = 'yes'
-            elif self.composite is False:
-                self.composite_ = 'no'
-            else:
-                self.composite_ = 'auto'
+            return 'no'
 
+    def _check_extrapolation(self):
+        # validate the range of extrapolation
         if not isinstance(self.extrapolation, float):
             raise TypeError("Extrapolation percentage must be a float")
-        elif self.extrapolation < 0.0 or self.extrapolation > 1.0:
+        if self.extrapolation < 0.0 or self.extrapolation > 1.0:
             raise ValueError("Extrapolation percentage must be between "
                              "0.0 and 1.0")
+        return self.extrapolation
 
-        if self.sample:
+    def _check_sample(self, num_samples):
+        # validate the sample percentage
+        if self.sample is not None:
             if not isinstance(self.sample, float):
                 raise TypeError("Sampling percentage must be a float")
-            if self.sample < 0.0 or self.sample >= 1.0:
+            if not (0.0 < self.sample < 1.0):
                 raise ValueError("Sampling percentage must be between "
                                  "0.0 and 1.0")
-            self.sample_ = self.sample
+            # check to see if the sample will create a very small dataset
+            trained_num_samples = int(round(self.sample * num_samples, 0))
+            if trained_num_samples < 10:
+                warn(f"Sampling a dataset with {num_samples} rows and a "
+                     f"sampling percent of {self.sample} means Cubist will "
+                     f"train with {trained_num_samples} rows. This may lead "
+                     f"to incorrect or failing predictions. Please increase "
+                     f"or remove the `sample` parameter.\n", stacklevel=3)
+            return self.sample
         else:
-            self.sample_ = 0.0
-        
-        if not isinstance(self.cv, (int, type(None))):
-            raise TypeError("Number of cross-validation folds must be an \
-                integer or None")
-        if isinstance(self.cv, int):
-            if self.cv <= 1 and self.cv != 0:
+            return 0
+
+    def _check_cv(self):
+        # validate number of cv folds
+        if self.cv is not None:
+            if not isinstance(self.cv, int):
+                raise TypeError("Number of cross-validation folds must be an \
+                                        integer or None")
+            if self.cv <= 1:
                 raise ValueError("Number of cross-validation folds must be \
-                    greater than 1")
-            else:
-                self.cv_ = self.cv
+                                         greater than 1")
+            return self.cv
         else:
-            self.cv_ = 0
+            return 0
 
-    def fit(self, X, y, sample_weight = None):
+    def fit(self, X, y, sample_weight=None):
         """Build a Cubist regression model from training set (X, y).
 
         Parameters
@@ -244,74 +272,60 @@ def fit(self, X, y, sample_weight = None):
         -------
         self : object
         """
-        # get column name from y if it is a Pandas Series
-        if isinstance(y, pd.Series):
-            target_label_ = y.name
-        else:
-            target_label_ = None
-
-        # scikit-learn checks
-        X, y = self._validate_data(X, y, 
+        # scikit-learn data validation
+        X, y = self._validate_data(X, y,
                                    dtype=None,
-                                   force_all_finite='allow-nan', 
+                                   force_all_finite='allow-nan',
                                    y_numeric=True,
                                    ensure_min_samples=2)
-        
+
         # set the feature names if it hasn't already been done
         if not hasattr(self, "feature_names_in_"):
             self.feature_names_in_ = [f'var{i}' for i in range(X.shape[1])]
-        
+
         # check sample weighting
         if sample_weight is not None:
             sample_weight = _check_sample_weight(sample_weight, X)
             self.is_sample_weighted_ = True
         else:
             self.is_sample_weighted_ = False
-        
-        # validate model parameters
-        self._validate_model_parameters()
 
-        # raise warning if sampling a small dataset
-        if self.sample:
-            trained_num_samples = int(round(self.sample * X.shape[0], 0))
-            if trained_num_samples < 10:
-                warn(f"Sampling a dataset with {X.shape[0]} rows and a "
-                     f"sampling percent of {self.sample} means Cubist will "
-                     f"train with {trained_num_samples} rows. This may lead "
-                     f"to incorrect or failing predictions. Please increase "
-                     f"or remove the `sample` parameter.\n", stacklevel=3)
-        
+        n_rules = self._check_n_rules()
+        n_committees = self._check_n_committees()
+        neighbors = self._check_neighbors()
+        unbiased = self._check_unbiased()
+        composite = self._check_composite(neighbors)
+        extrapolation = self._check_extrapolation()
+        sample = self._check_sample(X.shape[0])
+        cv = self._check_cv()
+        random_state = check_random_state(self.random_state)
+
+        # number of input features
         self.n_features_in_ = X.shape[1]
+        # number of outputs is 1 (single output regression)
         self.n_outputs_ = 1
 
         # (re)construct a dataframe from X
         X = pd.DataFrame(X, columns=self.feature_names_in_)
         y = pd.Series(y)
 
-        random_state = check_random_state(self.random_state)
-
-        # if a Pandas series wasn't used or it has no name,
-        # use the passed target_label feature, otherwise use
-        # the name of the Pandas series
-        self.target_label_ = target_label_ or self.target_label
-
         # create the names and data strings required for cubist
-        names_string = make_names_string(X, w=sample_weight,
-                                         label=self.target_label_)
-        data_string = make_data_string(X, y, w=sample_weight)
-        
+        names_string = _make_names_string(X, w=sample_weight,
+                                          label=self.target_label)
+        data_string = _make_data_string(X, y, w=sample_weight)
+
         # call the C implementation of cubist
         model, output = _cubist(namesv_=names_string.encode(),
                                 datav_=data_string.encode(),
-                                unbiased_=self.unbiased,
-                                compositev_=self.composite_.encode(),
-                                neighbors_=self.neighbors_,
-                                committees_=self.n_committees,
-                                sample_=self.sample_,
+                                unbiased_=unbiased,
+                                compositev_=composite.encode(),
+                                neighbors_=neighbors,
+                                committees_=n_committees,
+                                sample_=sample,
                                 seed_=random_state.randint(0, 4095) % 4096,
-                                rules_=self.n_rules,
-                                extrapolation_=self.extrapolation,
-                                cv_=self.cv_,
+                                rules_=n_rules,
+                                extrapolation_=extrapolation,
+                                cv_=cv,
                                 modelv_=b"1",
                                 outputv_=b"1")
 
@@ -320,18 +334,18 @@ def fit(self, X, y, sample_weight = None):
         output = output.decode()
 
         # raise Cubist training errors
-        if "***" in output or "Error" in output:
+        if ("***" in output) or ("Error" in output):
             raise CubistError(output)
-        
+
         # inform user that they may want to use rules only
         if "Recommend using rules only" in output:
             warn("Cubist recommends using rules only "
-                 "(i.e. set composite=False)", stacklevel=3)
+                 "(i.e. set auto=False)", stacklevel=3)
 
         # print model output if using verbose output
         if self.verbose:
             print(output)
-        
+
         # if the model returned nothing, we're doing cross-validation so stop
         if self.model_ == "1":
             return self
@@ -343,22 +357,24 @@ def fit(self, X, y, sample_weight = None):
             # clean model string when using reserved sample name
             self.model_ = self.model_[:self.model_.index("sample")] + \
                           self.model_[self.model_.index("entries"):]
-        
-        # compress and save descriptors
-        self.names_string_ = zlib.compress(names_string.encode())
 
-        # when a composite model has been used compress and save training data
-        if self.composite is True or "nearest neighbors" in output \
-            or self.neighbors_ > 0:
-            self.data_string_ = zlib.compress(data_string.encode())
-        else:
-            self.data_string_ = zlib.compress("1".encode())
+        # when a composite model has not been used, drop the data_string
+        if not (
+                (composite == "yes") or
+                ("nearest neighbors" in output) or
+                (neighbors > 0)
+        ):
+            data_string = "1"
+
+        # compress and save descriptors/data
+        self.names_string_ = zlib.compress(names_string.encode())
+        self.data_string_ = zlib.compress(data_string.encode())
 
         # parse model contents and store useful information
-        self.rules_, self.coeff_ = parse_model(self.model_, X)
+        self.rules_, self.coeff_ = _parse_model(self.model_, X)
 
         # get the input data variable usage
-        self.feature_importances_ = get_variable_usage(output, X)
+        self.feature_importances_ = _get_variable_usage(output, X)
 
         # get the names of columns that have no nan values
         is_na_col = ~self.coeff_.isna().any()
@@ -373,7 +389,7 @@ def fit(self, X, y, sample_weight = None):
             used_variables = set(self.rules_["variable"]).union(
                 set(not_na_cols)
             )
-            self.variables_ = {"all": list(X.columns),
+            self.variables_ = {"all": list(self.feature_names_in_),
                                "used": list(used_variables)}
         return self
 
@@ -391,13 +407,13 @@ def predict(self, X):
             The predicted values.
         """
         # make sure the model has been fitted
-        check_is_fitted(self, attributes=["model_", "rules_", "coeff_", 
+        check_is_fitted(self, attributes=["model_", "rules_", "coeff_",
                                           "feature_importances_"])
 
         # validate input data
-        X = self._validate_data(X, 
-                                dtype=None, 
-                                force_all_finite='allow-nan', 
+        X = self._validate_data(X,
+                                dtype=None,
+                                force_all_finite='allow-nan',
                                 reset=False)
 
         # (re)construct a dataframe from X
@@ -409,7 +425,7 @@ def predict(self, X):
             X["case_weight_pred"] = np.nan
 
         # make data string for predictions
-        data_string = make_data_string(X)
+        data_string = _make_data_string(X)
 
         # get cubist predictions from trained model
         pred, output = _predictions(data_string.encode(),
@@ -418,15 +434,15 @@ def predict(self, X):
                                     self.model_.encode(),
                                     np.zeros(X.shape[0]),
                                     b"1")
-        
+
         # decode output
         output = output.decode()
 
         # raise Cubist prediction errors
         if "***" in output or "Error" in output:
             raise CubistError(output)
-        
+
         if output:
             print(output)
-        
+
         return pred
diff --git a/cubist/exceptions.py b/cubist/exceptions.py
index b887d5c..456b8ef 100644
--- a/cubist/exceptions.py
+++ b/cubist/exceptions.py
@@ -1,8 +1,8 @@
-class Error(Exception):
+class _Error(Exception):
     """Base class for exceptions in this module."""
     pass
 
 
-class CubistError(Error):
+class CubistError(_Error):
     """Raised when the C Cubist library raises errors"""
     pass
diff --git a/examples/california_housing.py b/examples/california_housing.py
index 6e89ef2..6221206 100644
--- a/examples/california_housing.py
+++ b/examples/california_housing.py
@@ -15,7 +15,7 @@
 # model.fit(X, y)
 
 # X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
-model = Cubist(composite=False, verbose=True)
+model = Cubist(auto=False, verbose=True)
 model.fit(X, y)
 print("score", model.score(X, y))
 pred_list = model.predict(X).tolist()
diff --git a/examples/iris.py b/examples/iris.py
index d5084e2..9d5fca1 100644
--- a/examples/iris.py
+++ b/examples/iris.py
@@ -1,3 +1,4 @@
+
 import pandas as pd
 from sklearn.model_selection import train_test_split
 from cubist import Cubist
@@ -24,12 +25,12 @@
 
 
 # X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
-model = Cubist(composite=True, verbose=True, neighbors=1)
+model = Cubist(auto=True, n_committees=1, verbose=True)
 model.fit(X_train, y_train)
 print("score", model.score(X_train, y_train))
 pred_list = model.predict(X_test).tolist()
-print(pred_list)
-print("pearson", pearsonr(pred_list, y_test.tolist())[0])
+# print(pred_list)
+# print("pearson", pearsonr(pred_list, y_test.tolist())[0])
 # print(model.data_string_)
 # print(model.fit(X_train, y_train, sample_weight=np.ones(y_train.shape[0])).predict(X_test))
 # print(model.feature_importances_)
diff --git a/examples/jwgda.py b/examples/jwgda.py
deleted file mode 100644
index 616c773..0000000
--- a/examples/jwgda.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import pandas as pd
-from cubist import Cubist
-
-X = pd.DataFrame([0.803949,0.792776,0.698957,0.889468,0.898589,0.780680,0.763039,0.733734,0.880098,0.749216,0.772409,0.397889,0.569687,0.863047,0.901520,0.903672,0.850031,0.967257,0.924921],columns=['X'])
-y = [9.101007550426122, 10.105041400955075, 9.665428777858954, 11.108833294273804, 10.543686195210283, 10.041306334130462, 10.29213332885503, 10.229124133661102, 10.793787318304165, 9.726502315371054, 10.102863786063018, 7.843001261439629, 9.474707491805574, 10.290439628383432, 11.106171764961289, 11.73360218758805, 11.607825754410422, 11.168213131314303, 11.230738412087772]
-predict = pd.DataFrame([0.795757],columns=['X'])
-
-tree = Cubist(n_committees=5, n_rules=500, neighbors=None, unbiased=False, composite=False, extrapolation=0.05, sample=0.1, cv=None, target_label="outcome", random_state=1, verbose=0)
-tree.fit(X, y)
-tree.predict(predict)
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 18e8c04..852a792 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,3 @@
 [build-system]
-requires = ["setuptools>=42", "wheel", "build", "Cython>=0.29.23", "numpy"]
+requires = ["setuptools", "wheel", "build", "Cython", "numpy"]
 build-backend = "setuptools.build_meta"
diff --git a/requirements.txt b/requirements.txt
index a2d4468..4ec67c1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-numpy>=1.19.2
-pandas>=1.1.3
-scikit-learn>=0.24.2
+numpy
+pandas
+scikit-learn>=0.23.0
diff --git a/setup.py b/setup.py
index ce78469..9151687 100644
--- a/setup.py
+++ b/setup.py
@@ -20,11 +20,13 @@
 with open("requirements.txt", 'r') as f:
     requires = f.read()
 
+
 def read(rel_path):
     here = os.path.abspath(os.path.dirname(__file__))
     with codecs.open(os.path.join(here, rel_path), 'r') as fp:
         return fp.read()
 
+
 def get_version(rel_path):
     for line in read(rel_path).splitlines():
         if line.startswith('__version__'):
@@ -33,9 +35,10 @@ def get_version(rel_path):
     else:
         raise RuntimeError("Unable to find version string.")
 
+
 setup(
     name="cubist",
-    version=get_version("cubist/_version.py"),
+    version=get_version("cubist/__version__.py"),
     author="Patrick Aselin",
     description="A Python package for fitting Quinlan's Cubist regression model.",
     long_description=long_description,
@@ -47,7 +50,7 @@ def get_version(rel_path):
     url="https://github.com/pjaselin/Cubist",
     packages=["cubist"],
     license="GPL v3",
-    classifiers = [
+    classifiers=[
         "Programming Language :: Python",
         "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
         "Topic :: Software Development :: Libraries",


Test patch:
diff --git a/cubist/tests/test_common.py b/cubist/tests/test_common.py
index 065306e..f1def78 100644
--- a/cubist/tests/test_common.py
+++ b/cubist/tests/test_common.py
@@ -1,13 +1,8 @@
-import pytest
-
-from sklearn.utils.estimator_checks import check_estimator
+from sklearn.utils.estimator_checks import parametrize_with_checks
 
 from ..cubist import Cubist
 
 
-@pytest.mark.parametrize(
-    "estimator",
-    [Cubist()]
-)
-def test_all_estimators(estimator):
-    return check_estimator(estimator)
+@parametrize_with_checks([Cubist()])
+def test_sklearn_compatible_estimator(estimator, check):
+    return check(estimator)
diff --git a/cubist/tests/test_cubist.py b/cubist/tests/test_cubist.py
index f1e4eb1..1784258 100644
--- a/cubist/tests/test_cubist.py
+++ b/cubist/tests/test_cubist.py
@@ -1,4 +1,5 @@
-from _pytest.monkeypatch import V
+from contextlib import contextmanager
+
 import pytest
 
 import pandas as pd
@@ -13,122 +14,115 @@
 X = titanic.drop(["fare"], axis=1)
 
 
-@pytest.mark.parametrize(
-    "expected_output",
-    [True]
-)
+@contextmanager
+def no_raise():
+    yield
+
+
+@pytest.mark.parametrize("expected_output", [True])
 def test_model_instance(expected_output):
     model = Cubist()
     assert isinstance(model, Cubist) == expected_output
 
 
-def test_n_rules():
-    model = Cubist(n_rules=500)
-    model.fit(X, y)
-    check_is_fitted(model)
-
-
-def test_n_rules_fail():
-    model = Cubist(n_rules=10000000)
-    with pytest.raises(ValueError):
+@pytest.mark.parametrize("n_rules,raises",
+                         [(1, no_raise()),
+                          (1000000, no_raise()),
+                          (0, pytest.raises(ValueError)),
+                          (1000001, pytest.raises(ValueError)),
+                          ("asdf", pytest.raises(TypeError))])
+def test_n_rules(n_rules, raises):
+    model = Cubist(n_rules=n_rules)
+    with raises:
         model.fit(X, y)
         check_is_fitted(model)
 
 
-def test_n_committees():
-    model = Cubist(n_committees=5)
-    model.fit(X, y)
-    check_is_fitted(model)
-
-
-def test_n_committees_fail1():
-    model = Cubist(n_committees=-1)
-    with pytest.raises(ValueError):
+@pytest.mark.parametrize("n_committees,raises",
+                         [(1, no_raise()),
+                          (100, no_raise()),
+                          (-1, pytest.raises(ValueError)),
+                          (0, pytest.raises(ValueError)),
+                          (500, pytest.raises(ValueError)),
+                          ("asdf", pytest.raises(TypeError))])
+def test_n_committees(n_committees, raises):
+    model = Cubist(n_committees=n_committees)
+    with raises:
         model.fit(X, y)
         check_is_fitted(model)
 
 
-def test_n_committees_fail2():
-    model = Cubist(n_committees="sdfa")
-    with pytest.raises(TypeError):
+@pytest.mark.parametrize("neighbors,expected,raises",
+                         [(0, None, pytest.raises(ValueError)),
+                          (1, 1, no_raise()),
+                          (9, 9, no_raise()),
+                          (10, None, pytest.raises(ValueError))])
+def test_neighbors(neighbors, expected, raises):
+    model = Cubist(neighbors=neighbors)
+    with raises:
+        assert expected == model._check_neighbors()
         model.fit(X, y)
         check_is_fitted(model)
 
 
-def test_neighbors():
-    model = Cubist(neighbors=5, composite='auto')
-    model.fit(X, y)
-    check_is_fitted(model)
-
-
-def test_neighbors_fail():
-    model = Cubist(neighbors=10)
-    with pytest.raises(ValueError):
+@pytest.mark.parametrize("unbiased,raises",
+                         [(True, no_raise()),
+                          (False, no_raise()),
+                          (None, pytest.raises(ValueError)),
+                          ("aasdf", pytest.raises(ValueError))])
+def test_unbiased(unbiased, raises):
+    model = Cubist(unbiased=unbiased)
+    with raises:
         model.fit(X, y)
         check_is_fitted(model)
 
 
-def test_unbiased():
-    model = Cubist(unbiased=True)
-    model.fit(X, y)
-    check_is_fitted(model)
-
-
-def test_extrapolation():
-    model = Cubist(extrapolation=0.1)
-    model.fit(X, y)
-    check_is_fitted(model)
-
-
-def test_extrapolation_fail():
-    model = Cubist(extrapolation=-0.1)
-    with pytest.raises(ValueError):
+@pytest.mark.parametrize("extrapolation,raises",
+                         [(0.0, no_raise()),
+                          (1.0, no_raise()),
+                          (-0.1, pytest.raises(ValueError)),
+                          (1.01, pytest.raises(ValueError))])
+def test_extrapolation(extrapolation, raises):
+    model = Cubist(extrapolation=extrapolation)
+    with raises:
         model.fit(X, y)
         check_is_fitted(model)
 
 
-def test_sample():
-    model = Cubist(sample=0.1)
-    model.fit(X, y)
-    check_is_fitted(model)
-
-
-def test_sample_fail():
-    model = Cubist(sample=-0.1)
-    with pytest.raises(ValueError):
+@pytest.mark.parametrize("sample,raises",
+                         [(0.5, no_raise()),
+                          (0.0, pytest.raises(ValueError)),
+                          (1.0, pytest.raises(ValueError)),
+                          (-0.1, pytest.raises(ValueError)),
+                          (1.01, pytest.raises(ValueError))])
+def test_sample(sample, raises):
+    model = Cubist(sample=sample)
+    with raises:
         model.fit(X, y)
         check_is_fitted(model)
 
-def test_cv():
-    model = Cubist(cv=10)
-    model.fit(X, y)
-    check_is_fitted(model)
-
 
-def test_cv_fail1():
-    model = Cubist(cv=-0.1)
-    with pytest.raises(TypeError):
+@pytest.mark.parametrize("cv,expected,raises",
+                         [(10, 10, no_raise()),
+                          (-0.1, None, pytest.raises(TypeError)),
+                          (1, None, pytest.raises(ValueError)),
+                          (0, None, pytest.raises(ValueError))])
+def test_cv(cv, expected, raises):
+    model = Cubist(cv=cv)
+    with raises:
+        assert expected == model._check_cv()
         model.fit(X, y)
         check_is_fitted(model)
 
-def test_cv_fail2():
-    model = Cubist(cv=-1)
-    with pytest.raises(ValueError):
-        model.fit(X, y)
-        check_is_fitted(model)
 
-@pytest.mark.parametrize("test_input,expected", 
-                        [(True, True), 
-                         (False, True), 
-                         ('auto', True)
-                         ])
-def test_composite(test_input, expected):
-    model = Cubist(composite=test_input)
-    model.fit(X, y)
-    check_is_fitted(model)
-
-def test_composite_fail():
-    model = Cubist(composite="1234")
-    with pytest.raises(ValueError):
+@pytest.mark.parametrize("auto,expected,n,raises",
+                         [(True, "auto", 5, no_raise()),
+                          (False, "yes", 5, no_raise()),
+                          (False, "no", 0, no_raise()),
+                          ("1234", "", 5, pytest.raises(ValueError))])
+def test_auto(auto, expected, n, raises):
+    model = Cubist(auto=auto)
+    with raises:
+        assert expected == model._check_composite(n)
         model.fit(X, y)
         check_is_fitted(model)
diff --git a/cubist/tests/test_quinlan_attributes.py b/cubist/tests/test_quinlan_attributes.py
index 421fd1e..4621ee8 100644
--- a/cubist/tests/test_quinlan_attributes.py
+++ b/cubist/tests/test_quinlan_attributes.py
@@ -5,10 +5,9 @@
 import numpy as np
 import pytest
 
-from .._quinlan_attributes import quinlan_attributes, _get_data_format, \
+from .._quinlan_attributes import _quinlan_attributes, _get_data_format, \
     _is_all_float_dtype, _is_all_int_dtype
 
-
 int_series = pd.Series(random.sample(range(10, 30), 5))
 float_series = pd.Series(np.random.uniform(low=0.5, high=13.3, size=(5,)))
 str_series = pd.Series(["test0", "test1", "test2", "test3", "test4"])
@@ -29,33 +28,32 @@ def no_raise():
     yield
 
 
-@pytest.mark.parametrize("test_input,expected", 
-                        [(int_series, True), 
-                         (float_series, False), 
-                         (str_series, False),
-                         (complex_series, False),
-                         (date_series, False)])
+@pytest.mark.parametrize("test_input,expected",
+                         [(int_series, True),
+                          (float_series, False),
+                          (str_series, False),
+                          (complex_series, False),
+                          (date_series, False)])
 def test_is_all_int_dtype(test_input, expected):
     assert _is_all_int_dtype(test_input) == expected
 
 
-
-@pytest.mark.parametrize("test_input,expected", 
-                        [(int_series, False), 
-                         (float_series, True), 
-                         (str_series, False),
-                         (complex_series, False),
-                         (date_series, False)])
+@pytest.mark.parametrize("test_input,expected",
+                         [(int_series, False),
+                          (float_series, True),
+                          (str_series, False),
+                          (complex_series, False),
+                          (date_series, False)])
 def test_is_all_float_dtype(test_input, expected):
     assert _is_all_float_dtype(test_input) == expected
 
 
-@pytest.mark.parametrize("test_input,expected,raises", 
-                        [(int_series, "continuous.", no_raise()), 
-                         (float_series, "continuous.", no_raise()), 
-                         (str_series, f"{','.join(set(str_series.values))}.", no_raise()),
-                         (complex_series, None, pytest.raises(ValueError)),
-                         (date_series, date_series, no_raise())])
+@pytest.mark.parametrize("test_input,expected,raises",
+                         [(int_series, "continuous.", no_raise()),
+                          (float_series, "continuous.", no_raise()),
+                          (str_series, f"{','.join(set(str_series.values))}.", no_raise()),
+                          (complex_series, None, pytest.raises(ValueError)),
+                          (date_series, date_series, no_raise())])
 def test_get_data_format(test_input, expected, raises):
     with raises:
         try:
@@ -63,12 +61,12 @@ def test_get_data_format(test_input, expected, raises):
         except TypeError:
             assert _get_data_format(test_input) == expected
         except Exception as exc:
-            raise(exc)
+            raise exc
 
 
-@pytest.mark.parametrize("test_input,expected,raises", 
-                        [(good_df, dict, no_raise()), 
-                         (bad_df, dict, pytest.raises(ValueError))])
+@pytest.mark.parametrize("test_input,expected,raises",
+                         [(good_df, dict, no_raise()),
+                          (bad_df, dict, pytest.raises(ValueError))])
 def test_quinlan_attributes(test_input, expected, raises):
     with raises:
-        assert type(quinlan_attributes(test_input)) == expected
+        assert type(_quinlan_attributes(test_input)) == expected