Repository: pjaselin/Cubist (language: python)

Issue:
Error Installing with Python 3.11
Hi,

I received the following error when attempting to pip install cubist on python 3.11.5: 
cubist/src\utility.c(33): fatal error C1083: Cannot open include file: 'sys/time.h': No such file or directory

Would anyone possibly know how to address this error? Or has anyone had success with pip installing cubist with python 3.11? I performed the C++ compiler installation via https://wiki.python.org/moin/WindowsCompilers#Microsoft_Visual_C.2B-.2B-_14.2_standalone:_Build_Tools_for_Visual_Studio_2019_.28x86.2C_x64.2C_ARM.2C_ARM64.29

Thanks!
Rpkg catchup
Catching this python package up to the R package's relevant code


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/release.yml
similarity index 53%
rename from .github/workflows/main.yml
rename to .github/workflows/release.yml
index 41571d6..7457094 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/release.yml
@@ -1,4 +1,4 @@
-name: build and release cubist
+name: Build and Release Cubist
 
 on:
   workflow_dispatch:
@@ -9,85 +9,35 @@ on:
         default: 'warning'
   push:
     branches:
-      - master
       - main
-      - release
-      - test
       - dev
   release:
+    types:
+      - published
+  workflow_run:
+    workflows: ["Test and Lint Cubist"]
     branches:
-      - master
+      - main
+      - dev
     types:
-      - created
+      - completed
 
 jobs:
-  test:
-    name: Test and lint on Python ${{ matrix.python-version }} and ${{ matrix.os }}
-    strategy:
-      matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
-    runs-on: ${{ matrix.os }}
-
-    steps:
-    - name: Checkout repo
-      uses: actions/checkout@v3
-
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
-      with:
-        python-version: ${{ matrix.python-version }}
-    
-    - name: Enable Developer Command Prompt for Windows
-      if: matrix.os == 'windows-latest'
-      uses: ilammy/msvc-dev-cmd@v1
-    
-    - name: Add sys/time.h for Windows
-      if: matrix.os == 'windows-latest'
-      run: |
-        mv winsys/ cubist/sys
-      
-    - name: Remove winsys folder for non-Windows OS
-      if: matrix.os != 'windows-latest'
-      run: |
-        rm -r winsys/
-    
-    - name: Install dependencies
-      run: |
-        pip3 install --upgrade pip
-        pip3 install setuptools wheel build cython
-        pip3 install flake8 pytest pytest-cov numpy
-        pip install .
-
-    - name: Lint with flake8
-      run: |
-        pip install flake8
-        # stop the build if there are Python syntax errors or undefined names
-        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
-        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
-        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
-
-    - name: Test with pytest
-      run: |
-        pip install pytest
-        pytest --cov=com --cov-report=xml --cov-report=html
-
   build_wheels:
-    needs: test
+    if: ${{ github.event.workflow_run.conclusion == 'success' }}
     name: Build wheels on ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-    env:
-      CIBW_SKIP: "pp* *-win32"
+    
     steps:
       - uses: actions/checkout@v3
 
       - uses: actions/setup-python@v4
         name: Install Python
         with:
-          python-version: '3.9'
+          python-version: '3.10'
 
       - name: Enable Developer Command Prompt for Windows
         if: matrix.os == 'windows-latest'
@@ -103,8 +53,11 @@ jobs:
         run: |
           rm -r winsys/
 
+      - name: Install cibuildwheel
+        run: python -m pip install cibuildwheel==2.16.2
+      
       - name: Build wheels
-        uses: pypa/cibuildwheel@v2.2.2
+        run: python -m cibuildwheel --output-dir wheelhouse
       
       - uses: actions/upload-artifact@v3
         with:
@@ -112,7 +65,6 @@ jobs:
 
   build_sdist:
     name: Build source distribution
-    needs: test
     runs-on: ubuntu-latest
     steps:
     - name: Checkout repo
@@ -121,7 +73,7 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v4
       with:
-        python-version: "3.9"
+        python-version: "3.10"
 
     - name: Build sdist
       run: |
@@ -134,8 +86,8 @@ jobs:
   release:
     # release only tagged commits to pypi / github
     name: release distribution
+    if: startsWith(github.ref, 'refs/tags')
     needs: [build_wheels, build_sdist]
-    if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-latest
 
     steps:
@@ -151,10 +103,16 @@ jobs:
 
     # test publish to pypi
     - name: Publish distribution 📦 to Test PyPI
-      uses: pypa/gh-action-pypi-publish@master
+      uses: pypa/gh-action-pypi-publish@release/v1
       with:
         password: ${{ secrets.TEST_PYPI_API_TOKEN }}
-        repository_url: https://test.pypi.org/legacy/
+        repository-url: https://test.pypi.org/legacy/
+
+    # publish only tagged commits to pypi
+    - name: Publish distribution 📦 to PyPI
+      uses: pypa/gh-action-pypi-publish@release/v1
+      with:
+        password: ${{ secrets.PYPI_API_TOKEN }}
 
     # github upload release asset
     - name: Upload Release Asset
@@ -167,9 +125,3 @@ jobs:
           dist/*
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-    # publish only tagged commits to pypi
-    - name: Publish distribution 📦 to PyPI
-      uses: pypa/gh-action-pypi-publish@master
-      with:
-        password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/README.md b/README.md
index 02226f7..cd25947 100644
--- a/README.md
+++ b/README.md
@@ -4,22 +4,18 @@ A Python package for fitting Quinlan's [Cubist](https://www.rulequest.com/cubist
 
 ## Installation 
 ```bash
-pip install cubist
-```
-or
-```bash
 pip install --upgrade cubist
 ```
 
 ## Background
-Cubist is a regression algorithm develped by John Ross Quinlan for generating rule-based predictive models. This has been available in the R world thanks to the work of Max Kuhn and his colleagues. With this package it is introduced to Python and made scikit-learn compatible for easy use with existing data and model pipelines. Additionally, cross-validation and control over whether Cubist creates a composite model is added here.
+Cubist is a regression algorithm develped by John Ross Quinlan for generating rule-based predictive models. This has been available in the R world thanks to the work of Max Kuhn and his colleagues. It is introduced to Python with this package and made scikit-learn compatible for easy use with existing model pipelines. Cross-validation and control over whether Cubist creates a composite model is also enabled here.
 
 ## Advantages
 Unlike other ensemble models such as RandomForest and XGBoost, Cubist generates a set of rules, making it easy to understand precisely how the model makes it's predictive decisions. Thus tools such as SHAP and LIME are unnecessary as Cubist doesn't exhibit black box behavior. 
 
-Like XGBoost, Cubist can perform boosting by the addition of more models (here called committees) that correct for the error of prior models (i.e. the second model created corrects for the prediction error of the first, the third for the error of the second, etc.). 
+Like XGBoost, Cubist can perform boosting by the addition of more models (called committees) that correct for the error of prior models (i.e. the second model created corrects for the prediction error of the first, the third for the error of the second, etc.). 
 
-In addition to boosting, the model can perform instance-based (nearest-neighbor) corrections to create composite models, thus combining the advantages of these two methods. Note that with instance-based correction, model accuracy may be improved at the expense of computing time (this extra step takes longer) and some interpretability as the linear regression rules are no longer completely followed. It should also be noted that a composite model might be quite large as the full training dataset must be stored in order to perform instance-based corrections for inferencing. A composite model will be used when `composite=True` or Cubist can be allowed to decide whether to take advantage of this feature with `composite='auto'`.
+In addition to boosting, the model can perform instance-based (nearest-neighbor) corrections to create composite models, thus combining the advantages of these two methods. Note that with instance-based correction, model accuracy may be improved at the expense of computing time (this extra step takes longer) and some interpretability as the linear regression rules are no longer completely followed. It should also be noted that a composite model might be quite large as the full training dataset must be stored in order to perform instance-based corrections for inferencing. A composite model will be used when `auto=False` with `neighbors` set to an integer between 1 and 9. Cubist can be allowed to decide whether to take advantage of composite models with `auto=True`.
 
 ## Use
 ```python
@@ -37,15 +33,15 @@ model.score(X, y)
     <img src="www/iris_cubist_output.png" alt="[Sample Cubist output for Iris dataset" width="400"/>
 </p>
 
-The above image is a sample of the verbose output produced by Cubist. It first reports the total number of cases (rows) and attributes (columns) in the training dataset. Below that it summarizes the model by committee (if used but not in this sample) and rule where each rule is definined by an if..then statement along with metrics for this rule in the training data and the linear regression equation used for each rule. The 'if' section of each rule identifies the training input columns and feature value ranges for which this rule holds true. The 'then' statement shows the linear regressor for this rule. The model performance is then summarized by the average and relative absolute errors as well as with the Pearson correlation coefficient r. Finally, the output reports the usage of training features in the model and rules as well as the time taken to complete training.
+The above image is a sample of the verbose output produced by Cubist. It first reports the total number of _cases_ (rows) and _attributes_ (columns) in the training dataset. Below that it summarizes the model by _committee_ (if used but not in this sample) and _rule_ where each rule is definined by an if..then statement along with metrics for this rule in the training data and the linear regression equation used for each rule. The _if_ section of each rule identifies the training input columns and feature value ranges for which this rule holds true. The _then_ statement shows the linear regressor for this rule. The model performance is then summarized by the average and relative absolute errors as well as with the Pearson correlation coefficient r. Finally, the output reports the usage of training features in the model and rules as well as the time taken to complete training.
 
 ## Model Parameters
 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. 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.
+- neighbors (int, default=None): Integer 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.
-- 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.
+- 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 the value passed `neighbors`.
 - 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)
@@ -63,6 +59,7 @@ The following attributes are exposed to understand the Cubist model results:
 - rules_ (pd.DataFrame): Table of the rules built by the Cubist model and the percentage of data for which each rule condition applies.
 - coeff_ (pd.DataFrame): Table of the regression coefficients found by the Cubist model.
 - variables_ (dict): Information about all the variables passed to the model and those that were actually used.
+- feature_names_in_ (list): List of features used to train Cubist.
 
 ## Benchmarks
 There are many literature examples demonstrating the power of Cubist and comparing it to Random Forest as well as other bootstrapped/boosted models. Some of these are compiled here: https://www.rulequest.com/cubist-pubs.html. To demonstrate this, some benchmark scripts are provided in the respectively named folder.
diff --git a/cubist/__version__.py b/cubist/__version__.py
index 3dc1f76..5becc17 100644
--- a/cubist/__version__.py
+++ b/cubist/__version__.py
@@ -1,1 +1,1 @@
-__version__ = "0.1.0"
+__version__ = "1.0.0"
diff --git a/cubist/_cubist.pyx b/cubist/_cubist.pyx
index f4259ba..4a0db9e 100644
--- a/cubist/_cubist.pyx
+++ b/cubist/_cubist.pyx
@@ -14,7 +14,7 @@ cdef extern from "src/top.c":
 def _cubist(namesv_, datav_, unbiased_, compositev_, neighbors_, committees_, 
             sample_, seed_, rules_, extrapolation_, cv_, modelv_, outputv_):
     """
-    Train and retun Cubist model and output from C code
+    Train and return Cubist model and output from C code
     """
     cdef char *namesv = namesv_;
     cdef char *datav = datav_;
diff --git a/cubist/_make_data_string.py b/cubist/_make_data_string.py
index cf6d442..0f1c6fa 100644
--- a/cubist/_make_data_string.py
+++ b/cubist/_make_data_string.py
@@ -14,15 +14,16 @@ def _r_format(x: float, digits: int = 15) -> str:
     if np.iscomplex(x):
         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
+    # 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)))
-    # Where there are decimal places that need to be rounded, round to digits - whole_nums_count decimal places
+    # Where there are decimal places that need to be rounded, round to digits 
+    # - whole_nums_count decimal places
     if whole_nums_count < digits:
         remaining_decimals = digits - whole_nums_count
         return str(round(x, remaining_decimals))
     # Where there are no decimals that need to/can be rounded
-    else:
-        return str(x)
+    return str(x)
 
 
 def _make_data_string(x, y=None, w=None):
@@ -43,7 +44,8 @@ def _make_data_string(x, y=None, w=None):
     Returns
     -------
     x : str
-        Input dataset converted to a string and formatted per Cubist's requirements.
+        Input dataset converted to a string and formatted per Cubist's 
+        requirements.
     """
     x = x.copy(deep=True)
     
@@ -52,7 +54,8 @@ def _make_data_string(x, y=None, w=None):
         if is_string_dtype(x[col]):
             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 for model predictions, set y as a column of NaN values, 
+    # # which will become ?'s later
     if y is None:
         y = [np.nan] * x.shape[0]
         y = pd.Series(y)
@@ -67,7 +70,8 @@ def _make_data_string(x, y=None, w=None):
 
     # handle weights matrix (?) TODO: validate
     if w is not None:
-        column_names = list(x.columns) + ["w"] #[f"w{i}" for i in range(x.shape[1])]
+        # [f"w{i}" for i in range(x.shape[1])]
+        column_names = list(x.columns) + ["w"]
         x = x.assign(w=w)
         x.columns = column_names
 
diff --git a/cubist/_make_names_string.py b/cubist/_make_names_string.py
index 2bffaaa..3224552 100644
--- a/cubist/_make_names_string.py
+++ b/cubist/_make_names_string.py
@@ -86,7 +86,8 @@ def _escapes(x, chars=None):
     return x
 
 
-_special_chars_map = {i: '\\' + chr(i) for i in b'()[]{}?*+-|:;^$\\.&~#\t\n\r\v\f'}
+_special_chars_map = \
+    {i: '\\' + chr(i) for i in b'()[]{}?*+-|:;^$\\.&~#\t\n\r\v\f'}
 
 
 def _re_escape(pattern):
@@ -94,6 +95,5 @@ def _re_escape(pattern):
     Sourced from 're' Python package."""
     if isinstance(pattern, str):
         return pattern.translate(_special_chars_map)
-    else:
-        pattern = str(pattern, 'latin1')
-        return pattern.translate(_special_chars_map).encode('latin1')
+    pattern = str(pattern, 'latin1')
+    return pattern.translate(_special_chars_map).encode('latin1')
diff --git a/cubist/_parse_model.py b/cubist/_parse_model.py
index f8ffd4f..4334266 100644
--- a/cubist/_parse_model.py
+++ b/cubist/_parse_model.py
@@ -16,8 +16,9 @@
 
 
 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."""
+    """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):
         raise ValueError("lists x and f must be of the same length")
     groups = {}
@@ -187,7 +188,7 @@ def _type3(x):
     # TODO: enter all vals in dataframe but limit the column width when printing
     multiple_vals = "," in val
     if multiple_vals:
-        val = f"{{multiple_vals}}"
+        val = "{" + str(multiple_vals) + "}"
         txt = f"{var} in {val}"
     else:
         txt = f"{var} = {val}"
@@ -198,22 +199,22 @@ def _eqn(x, var_names=None):
     x = x.replace("\"", "")
     starts = [m.start(0) for m in re.finditer("(coeff=)|(att=)", x)]
     tmp = [""] * len(starts)
-    for i in range(len(starts)):
+    for i, val in enumerate(starts):
         if i < len(starts) - 1:
-            txt = x[starts[i]:starts[i + 1] - 1]
+            txt = x[val:starts[i + 1] - 1]
         else:
-            txt = x[starts[i]:]
+            txt = x[val:]
         tmp[i] = txt.replace("coeff=", "").replace("att=", "")
 
     vals = tmp[::2]
     vals = [float(c) for c in vals]
     nms = tmp[1::2]
     nms = ["(Intercept)"] + nms
-    vals = {nm: val for nm, val in zip(nms, vals)}
+    vals = dict(zip(nms, vals))
     if var_names:
         vars2 = [var for var in var_names if var not in nms]
         vals2 = [np.nan] * len(vars2)
-        vals2 = {nm: val for nm, val in zip(vars2, vals2)}
+        vals2 = dict(zip(vars2, vals2))
         vals = {**vals, **vals2}
         new_names = ["(Intercept)"] + var_names
         vals = {nm: vals[nm] for nm in new_names}
@@ -224,8 +225,7 @@ def _make_parsed_dict(x):
     x = x.split("=")
     if len(x) > 1:
         return {x[0]: x[1]}
-    else:
-        return None
+    return None
 
 
 def _parser(x):
diff --git a/cubist/_quinlan_attributes.py b/cubist/_quinlan_attributes.py
index 5aaf23e..8f558d1 100644
--- a/cubist/_quinlan_attributes.py
+++ b/cubist/_quinlan_attributes.py
@@ -6,12 +6,12 @@
 
 def _is_all_float_dtype(x: pd.Series):
     """check whether all values are of float dtype"""
-    return all([j == float or np.issubdtype(j, np.floating) for j in [type(i) for i in x.values]])
+    return all(j == float or np.issubdtype(j, np.floating) for j in [type(i) for i in x.values])
 
 
 def _is_all_int_dtype(x: pd.Series):
     """check whether all values are of float dtype"""
-    return all([j == int or np.issubdtype(j, np.integer) for j in [type(i) for i in x.values]])
+    return all(j == int or np.issubdtype(j, np.integer) for j in [type(i) for i in x.values])
 
 
 def _get_data_format(x: pd.Series):
@@ -36,17 +36,16 @@ def _get_data_format(x: pd.Series):
     if is_complex_dtype(x):
         raise ValueError("Complex data not supported")
     # for numeric columns
-    elif is_numeric_dtype(x) or _is_all_float_dtype(x) or _is_all_int_dtype(x):
+    if is_numeric_dtype(x) or _is_all_float_dtype(x) or _is_all_int_dtype(x):
         return "continuous."
     # for string columns
-    elif is_string_dtype(x):
+    if is_string_dtype(x):
         x = x.astype(str)
         return f"{','.join(set(x))}."
     # for datetime columns
-    elif is_datetime64_any_dtype(x):
+    if is_datetime64_any_dtype(x):
         return x
-    else:
-        raise ValueError(f"Dtype {x.dtype} is not supported")
+    raise ValueError(f"Dtype {x.dtype} is not supported")
 
 
 def _quinlan_attributes(df: pd.DataFrame) -> dict:
@@ -64,4 +63,4 @@ def _quinlan_attributes(df: pd.DataFrame) -> dict:
         Dictionary with keys as column names and values as the description of
         the data type.
     """
-    return {col_name: _get_data_format(col_data) for col_name, col_data in df.iteritems()}
+    return {col_name: _get_data_format(df[col_name]) for col_name in df.columns}
diff --git a/cubist/_variable_usage.py b/cubist/_variable_usage.py
index 7841eea..bfc59d9 100644
--- a/cubist/_variable_usage.py
+++ b/cubist/_variable_usage.py
@@ -39,21 +39,17 @@ def _get_values(x):
         x2 = [x2[i] for i, c in enumerate(has_pct) if c]
         x2 = [float(c.replace("%", "")) for c in x2]
         return x2
-    else:
-        if sum(has_pct) == 1:
-            pct_ind = [i for i, c in enumerate(x2) if "%" in c][0]
-            if x2[1:pct_ind+1].count("") < x2[pct_ind+1:].count(""):
-                x2 = [c for c in x2 if "%" in c][0]
-                x2 = float(x2.replace("%", ""))
-                return [x2, 0]
-            else:
-                x2 = [c for c in x2 if "%" in c][0]
-                x2 = float(x2.replace("%", ""))
-                return [0, x2]
-        else:
-            return 0
+    if sum(has_pct) == 1:
+        pct_ind = [i for i, c in enumerate(x2) if "%" in c][0]
+        if x2[1:pct_ind+1].count("") < x2[pct_ind+1:].count(""):
+            x2 = [c for c in x2 if "%" in c][0]
+            x2 = float(x2.replace("%", ""))
+            return [x2, 0]
+        x2 = [c for c in x2 if "%" in c][0]
+        x2 = float(x2.replace("%", ""))
+        return [0, x2]
+    return 0
 
 
 def _get_variable(x):
-    x = x.split(" ")
-    return x[-1]
+    return x.split(" ")[-1]
diff --git a/cubist/cubist.py b/cubist/cubist.py
index da61765..308ca55 100644
--- a/cubist/cubist.py
+++ b/cubist/cubist.py
@@ -177,15 +177,14 @@ def _check_neighbors(self):
         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:
+            if self.neighbors < 1 or self.neighbors > 9:
                 raise ValueError("`neighbors` must be between 1 and 9")
-            elif self.auto:
+            if 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:
-                return self.neighbors
+            return self.neighbors
         # default value must be zero even when not used
         return 0
 
@@ -203,13 +202,12 @@ def _check_composite(self, neighbors):
                              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:
+        if self.auto:
             return 'auto'
         # if a number of neighbors is given, make a composite model
-        elif neighbors > 0:
+        if neighbors > 0:
             return 'yes'
-        else:
-            return 'no'
+        return 'no'
 
     def _check_extrapolation(self):
         # validate the range of extrapolation
@@ -237,8 +235,7 @@ def _check_sample(self, num_samples):
                      f"to incorrect or failing predictions. Please increase "
                      f"or remove the `sample` parameter.\n", stacklevel=3)
             return self.sample
-        else:
-            return 0
+        return 0
 
     def _check_cv(self):
         # validate number of cv folds
@@ -250,8 +247,7 @@ def _check_cv(self):
                 raise ValueError("Number of cross-validation folds must be \
                                          greater than 1")
             return self.cv
-        else:
-            return 0
+        return 0
 
     def fit(self, X, y, sample_weight=None):
         """Build a Cubist regression model from training set (X, y).
@@ -259,7 +255,8 @@ def fit(self, X, y, sample_weight=None):
         Parameters
         ----------
         X : {array-like} of shape (n_samples, n_features)
-            The training input samples.
+            The training input samples. Must have complete column names or none
+            provided at all (NumPy arrays will be given names by column index).
 
         y : array-like of shape (n_samples,)
             The target values (Real numbers in regression).
@@ -282,6 +279,11 @@ def fit(self, X, y, sample_weight=None):
         # 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 to see if any of the feature names are empty
+        if any(n == "" or pd.isnull(n) for n in self.feature_names_in_):
+            raise ValueError("At least one column is missing unnamed and is NA \
+                             or an empty string.")
 
         # check sample weighting
         if sample_weight is not None:
@@ -399,7 +401,8 @@ def predict(self, X):
         Parameters
         ----------
         X : {array-like} of shape (n_samples, n_features)
-            The input samples.
+            The input samples. Must have complete column names or none
+            provided at all (NumPy arrays will be given names by column index).
 
         Returns
         -------
diff --git a/cubist/exceptions.py b/cubist/exceptions.py
index 456b8ef..b156d1b 100644
--- a/cubist/exceptions.py
+++ b/cubist/exceptions.py
@@ -1,8 +1,6 @@
 class _Error(Exception):
     """Base class for exceptions in this module."""
-    pass
 
 
 class CubistError(_Error):
     """Raised when the C Cubist library raises errors"""
-    pass
diff --git a/cubist/src/construct.c b/cubist/src/construct.c
index 336807e..aae0a21 100644
--- a/cubist/src/construct.c
+++ b/cubist/src/construct.c
@@ -472,4 +472,4 @@ void NoteUsed(Attribute Att)
 
     ForEach(i, 1, AttDefUses[Att][0]) { NoteUsed(AttDefUses[Att][i]); }
   }
-}
+}
\ No newline at end of file
diff --git a/cubist/src/defns.h b/cubist/src/defns.h
index 9e887e7..9337ba4 100644
--- a/cubist/src/defns.h
+++ b/cubist/src/defns.h
@@ -706,4 +706,4 @@ void FreeInstances(void);
 void CrossVal(void);
 void Prepare(void);
 void Shuffle(int *);
-void Summary(void);
+void Summary(void);
\ No newline at end of file
diff --git a/cubist/src/formrules.c b/cubist/src/formrules.c
index 0e28160..c91caef 100644
--- a/cubist/src/formrules.c
+++ b/cubist/src/formrules.c
@@ -207,7 +207,7 @@ void Scan(Tree T)
 /*            */
 /*************************************************************************/
 
-void PushCondition()
+void PushCondition(void)
 /*   -------------  */
 {
   int i;
@@ -219,7 +219,7 @@ void PushCondition()
   }
 }
 
-void PopCondition()
+void PopCondition(void)
 /*   -------------  */
 {
   int i;
@@ -398,7 +398,7 @@ void UpdateCount(int d, CaseNo i, double *Total, double *PredErr)
 /*            */
 /*************************************************************************/
 
-void ProcessLists()
+void ProcessLists(void)
 /*   ------------  */
 {
   CaseNo i, iNext, *Prev;
diff --git a/cubist/src/getdata.c b/cubist/src/getdata.c
index efc50fc..686d987 100644
--- a/cubist/src/getdata.c
+++ b/cubist/src/getdata.c
@@ -48,7 +48,7 @@ float runif( float min, float max )
 /*  Alternative random number generator  */
 
 #define AltRandom my_rand()
-static double my_rand() {
+static double my_rand(void) {
   double dval;
   // GetRNGstate();
   dval = runif(0, 1);
diff --git a/cubist/src/hash.h b/cubist/src/hash.h
index f7dd429..98fc8a2 100644
--- a/cubist/src/hash.h
+++ b/cubist/src/hash.h
@@ -12,7 +12,7 @@ extern unsigned int ht_hashcode(const char *key);
 
 /* Constructor and destructor */
 extern void *ht_new(int size);
-extern void ht_destroy();
+extern void ht_destroy(void *ht);
 
 /* Hash table iteration functions */
 extern void *ht_next(void *ht);
diff --git a/cubist/src/implicitatt.c b/cubist/src/implicitatt.c
index 06f03dc..973689f 100644
--- a/cubist/src/implicitatt.c
+++ b/cubist/src/implicitatt.c
@@ -654,7 +654,7 @@ void DefSemanticsError(int Fi, String Msg, int OpCode)
       sprintf(Op, "%s", "%");
       break;
     case OP_POW:
-      sprintf(Op, "%s", "^");
+      snprintf(Op, "%s", "^");
       break;
     case OP_SIN:
       sprintf(Op, "%s", "sin");
@@ -672,7 +672,7 @@ void DefSemanticsError(int Fi, String Msg, int OpCode)
       sprintf(Op, "%s", "exp");
       break;
     case OP_INT:
-      sprintf(Op, "%s", "int");
+      snprintf(Op, "%s", "int");
     }
 
     sprintf(XMsg, "%s with '%s'", Msg, Op);
diff --git a/cubist/src/modelfiles.c b/cubist/src/modelfiles.c
index 1415279..81fdae2 100644
--- a/cubist/src/modelfiles.c
+++ b/cubist/src/modelfiles.c
@@ -181,7 +181,6 @@ void WriteFilePrefix(String Extension)
 void ReadFilePrefix(String Extension)
 /*   --------------  */
 {
-  int Head;
 
   if (!(Mf = GetFile(Extension, "r")))
     Error(NOFILE, Fn, "");
diff --git a/cubist/src/rcubist.c b/cubist/src/rcubist.c
index bb3f05d..15adc78 100644
--- a/cubist/src/rcubist.c
+++ b/cubist/src/rcubist.c
@@ -37,7 +37,7 @@
 #include "redefine.h"
 #include "transform.h"
 
-int cubistmain()
+int cubistmain(void)
 /*  ----------  */
 {
   double StartTime, SumCWt = 0;
diff --git a/cubist/src/redefine.c b/cubist/src/redefine.c
index ef58d24..325303a 100644
--- a/cubist/src/redefine.c
+++ b/cubist/src/redefine.c
@@ -43,7 +43,7 @@ static void *strbufv;
  * XXX Is this called anywhere in Cubist?  It looks like it's
  * XXX been made obsolete by rbm_removeall.
  */
-int rbm_init() {
+int rbm_init(void) {
   if (strbufv == NULL) {
     strbufv = ht_new(HASH_LEN);
   }
@@ -188,7 +188,7 @@ int rbm_remove(const char *path) {
  * all "files" generated on the previous run and to prepare it
  * for the next run.
  */
-void rbm_removeall() {
+void rbm_removeall(void) {
   /* Check if there actually is anything to remove */
   if (strbufv != NULL) {
     /*
diff --git a/cubist/src/redefine.h b/cubist/src/redefine.h
index 8dea49b..ab44925 100644
--- a/cubist/src/redefine.h
+++ b/cubist/src/redefine.h
@@ -6,7 +6,7 @@
 
 #include "strbuf.h"
 
-extern int rbm_init();
+extern int rbm_init(void);
 extern int rbm_register(STRBUF *sb, const char *filename, int force);
 extern int rbm_deregister(const char *filename);
 extern STRBUF *rbm_lookup(const char *filename);
@@ -24,7 +24,7 @@ extern int rbm_fputs(const char *s, FILE *stream);
 extern size_t rbm_fwrite(const void *ptr, size_t size, size_t nitems,
                          FILE *stream);
 extern int rbm_remove(const char *fname);
-extern void rbm_removeall();
+extern void rbm_removeall(void);
 extern void rbm_exit(int status);
 
 #endif
diff --git a/cubist/src/rulebasedmodels.c b/cubist/src/rulebasedmodels.c
index 6f92556..2bfe4e5 100644
--- a/cubist/src/rulebasedmodels.c
+++ b/cubist/src/rulebasedmodels.c
@@ -198,12 +198,12 @@ void setglobals(int unbiased, char *composite, int neighbors, int committees,
   }
 }
 
-void setOf() {
+void setOf(void) {
   // XXX Experimental
   Of = rbm_fopen("rulebasedmodels.stdout", "w");
 }
 
-char *closeOf() {
+char *closeOf(void) {
   if (Of) {
 
     rbm_fclose(Of);
diff --git a/cubist/src/rules.c b/cubist/src/rules.c
index 3d6d999..6d4dc7f 100644
--- a/cubist/src/rules.c
+++ b/cubist/src/rules.c
@@ -264,8 +264,8 @@ void PrintRules(RRuleSet RS, String Msg)
 void PrintRule(CRule R)
 /*   ---------  */
 {
-  int c, d, dd, id, LineLen, EntryLen, Indent, NCoeff = 0;
   Attribute Att;
+  int c, d, dd, id, LineLen, EntryLen, Indent, NCoeff = 0;
   char Entry[1000];
   double *Model;
   float *Importance;
diff --git a/cubist/src/strbuf.c b/cubist/src/strbuf.c
index 187e76c..77abcba 100644
--- a/cubist/src/strbuf.c
+++ b/cubist/src/strbuf.c
@@ -31,7 +31,7 @@ STRBUF *strbuf_create_empty(unsigned int len) {
     return NULL;
 
   /* Allocate memory for the buffer */
-  sb->buf = (unsigned char *)malloc(len);
+  sb->buf = (char *)malloc(len);
   if (sb->buf == NULL) {
     free(sb);
     return NULL;
@@ -53,7 +53,7 @@ STRBUF *strbuf_create_empty(unsigned int len) {
  * it doesn't assume that the memory has been dynamically allocated.
  * The data buffer is neither reallocated or freed for any reason.
  */
-STRBUF *strbuf_create_full(unsigned char *data, unsigned int len) {
+STRBUF *strbuf_create_full(char *data, unsigned int len) {
   STRBUF *sb;
 
   /* Allocate memory for the STRBUF */
@@ -82,7 +82,7 @@ STRBUF *strbuf_copy(STRBUF *sb) {
     return NULL;
 
   /* Allocate memory for the buffer */
-  nsb->buf = (unsigned char *)malloc(sb->len);
+  nsb->buf = (char *)malloc(sb->len);
   if (nsb->buf == NULL) {
     free(nsb);
     return NULL;
@@ -155,7 +155,7 @@ void strbuf_destroy(STRBUF *sb) {
 /*
  * Write the specified amount of data to the STRBUF.
  */
-int strbuf_write(STRBUF *sb, const unsigned char *data, unsigned int n) {
+int strbuf_write(STRBUF *sb, const char *data, unsigned int n) {
   unsigned int nlen = sb->i + n; /* Minimum length needed */
 
   /* If the new string won't fit, extend sb */
@@ -174,7 +174,7 @@ int strbuf_write(STRBUF *sb, const unsigned char *data, unsigned int n) {
   return 0;
 }
 
-int strbuf_printf(STRBUF *sb, const unsigned char *format, ...) {
+int strbuf_printf(STRBUF *sb, const char *format, ...) {
   va_list ap;
   int status;
 
@@ -185,7 +185,7 @@ int strbuf_printf(STRBUF *sb, const unsigned char *format, ...) {
   return status;
 }
 
-int strbuf_vprintf(STRBUF *sb, const unsigned char *format, va_list ap) {
+int strbuf_vprintf(STRBUF *sb, const char *format, va_list ap) {
   int s;
   unsigned int nlen;
   int size = sb->len - sb->i; /* Remaining space left */
@@ -264,7 +264,7 @@ int strbuf_vprintf(STRBUF *sb, const unsigned char *format, va_list ap) {
 /*
  * Write a null-terminated string to the STRBUF.
  */
-int strbuf_puts(STRBUF *sb, const unsigned char *s) {
+int strbuf_puts(STRBUF *sb, const char *s) {
   return strbuf_write(sb, s, strlen(s));
 }
 
@@ -280,7 +280,7 @@ int strbuf_putc(STRBUF *sb, int c) {
       return -1;
 
   /* Put the character into the buffer, and update the position and size */
-  sb->buf[sb->i++] = (unsigned char)c;
+  sb->buf[sb->i++] = (char)c;
   if (sb->i > sb->n)
     sb->n = sb->i;
 
@@ -292,7 +292,7 @@ int strbuf_putc(STRBUF *sb, int c) {
 /*
  * Read a string from the STRBUF.
  */
-unsigned char *strbuf_gets(STRBUF *sb, unsigned char *s, unsigned int n) {
+char *strbuf_gets(STRBUF *sb, char *s, unsigned int n) {
   int c = -1;
   int i, j;
 
@@ -327,7 +327,7 @@ int strbuf_getc(STRBUF *sb) {
  * Return the entire buffer.
  * Be careful, because it's not null-terminated!
  */
-unsigned char *strbuf_getall(STRBUF *sb) {
+char *strbuf_getall(STRBUF *sb) {
   /* Make sure there is enough memory to null-terminate the data */
   if (sb->n >= sb->len) {
     /* sb->n should never actually be larger than sb->len */
@@ -349,7 +349,7 @@ unsigned char *strbuf_getall(STRBUF *sb) {
  * Increase the allocated size of the buffer to the specified size.
  */
 static int extend(STRBUF *sb, unsigned int nlen) {
-  unsigned char *buf;
+  char *buf;
 
   /* Do nothing if nlen isn't larger than the current size */
   if (nlen <= sb->len)
@@ -360,7 +360,7 @@ static int extend(STRBUF *sb, unsigned int nlen) {
     return -1;
 
   /* Try to reallocate the buffer */
-  buf = (unsigned char *)realloc(sb->buf, nlen);
+  buf = (char *)realloc(sb->buf, nlen);
   if (buf == NULL)
     return -1; /* Note that sb is untouched */
 
diff --git a/cubist/src/strbuf.h b/cubist/src/strbuf.h
index 1e00ecc..d0b84fe 100644
--- a/cubist/src/strbuf.h
+++ b/cubist/src/strbuf.h
@@ -4,7 +4,7 @@
 #include <stdarg.h>
 
 typedef struct _STRBUF {
-  unsigned char *buf; // The buffer itself
+  char *buf; // The buffer itself
   unsigned int i;     // Current position in buffer
   unsigned int n;     // Current size of the data in the buffer
   unsigned int len;   // Current length of buffer
@@ -13,20 +13,20 @@ typedef struct _STRBUF {
 } STRBUF;
 
 extern STRBUF *strbuf_create_empty(unsigned int len);
-extern STRBUF *strbuf_create_full(unsigned char *data, unsigned int len);
+extern STRBUF *strbuf_create_full(char *data, unsigned int len);
 extern STRBUF *strbuf_copy(STRBUF *sb);
 extern int strbuf_open(STRBUF *sb);
 extern int strbuf_close(STRBUF *sb);
 extern int strbuf_rewind(STRBUF *sb);
 extern int strbuf_truncate(STRBUF *sb);
 extern void strbuf_destroy(STRBUF *sb);
-extern int strbuf_vprintf(STRBUF *sb, const unsigned char *format, va_list ap);
-extern int strbuf_printf(STRBUF *sb, const unsigned char *format, ...);
-extern int strbuf_puts(STRBUF *sb, const unsigned char *s);
+extern int strbuf_vprintf(STRBUF *sb, const char *format, va_list ap);
+extern int strbuf_printf(STRBUF *sb, const char *format, ...);
+extern int strbuf_puts(STRBUF *sb, const char *s);
 extern int strbuf_putc(STRBUF *sb, int c);
-extern int strbuf_write(STRBUF *sb, const unsigned char *data, unsigned int n);
-extern unsigned char *strbuf_gets(STRBUF *sb, unsigned char *s, unsigned int n);
+extern int strbuf_write(STRBUF *sb, const char *data, unsigned int n);
+extern char *strbuf_gets(STRBUF *sb, char *s, unsigned int n);
 extern int strbuf_getc(STRBUF *sb);
-extern unsigned char *strbuf_getall(STRBUF *sb);
+extern char *strbuf_getall(STRBUF *sb);
 
 #endif
diff --git a/cubist/src/top.c b/cubist/src/top.c
index d09b761..9e5a5e0 100644
--- a/cubist/src/top.c
+++ b/cubist/src/top.c
@@ -5,7 +5,7 @@
 #include "rulebasedmodels.h"
 #include "strbuf.h"
 
-extern void cubistmain();
+extern void cubistmain(void);
 extern void samplemain(double *outputv);
 extern void FreeCases(void);
 
@@ -65,9 +65,7 @@ static void cubist(char **namesv, char **datav, int *unbiased,
       // I think the previous value of *modelv will be garbage collected
       *modelv = model;
     }
-  } //else {
-    // printf("training code called exit with value %d\n", val - JMP_OFFSET);
-  //}
+  }
 
   // Close file object "Of", and return its contents via argument outputv
   char *outputString = closeOf();
diff --git a/cubist/src/utility.c b/cubist/src/utility.c
index e866ede..bf444f5 100644
--- a/cubist/src/utility.c
+++ b/cubist/src/utility.c
@@ -281,7 +281,8 @@ void Error(int ErrNo, String S1, String S2)
 /*   -----  */
 {
   Boolean Quit = false, WarningOnly = false;
-  char Buffer[10000], *Msg = Buffer;
+  char Buffer[10000];
+  char *Msg = Buffer;
 
   if (!ErrMsgs)
     fprintf(Of, "\n");
@@ -627,8 +628,9 @@ void DayToDate(int Day, String Date)
     Year++;
   }
 
-  sprintf(Date, "%d/%d%d/%d%d", Year, Month / 10, Month % 10, Day / 10,
-          Day % 10);
+  sprintf(Date, 
+          "%d/%d%d/%d%d", 
+          Year, Month / 10, Month % 10, Day / 10, Day % 10);
 }
 
 /*************************************************************************/
@@ -665,8 +667,9 @@ void SecsToTime(int Secs, String Time)
   Mins = (Secs % 3600) / 60;
   Secs = Secs % 60;
 
-  sprintf(Time, "%d%d:%d%d:%d%d", Hour / 10, Hour % 10, Mins / 10, Mins % 10,
-          Secs / 10, Secs % 10);
+  sprintf(Time,
+          "%d%d:%d%d:%d%d", 
+          Hour / 10, Hour % 10, Mins / 10, Mins % 10, Secs / 10, Secs % 10);
 }
 
 void SetTSBase(int y)
diff --git a/setup.py b/setup.py
index 9151687..13b3404 100644
--- a/setup.py
+++ b/setup.py
@@ -10,14 +10,15 @@
     Extension(
         name='_cubist',
         sources=["cubist/_cubist.pyx"] + glob.glob("cubist/src/*.c"),
-        include_dirs=["cubist/src", np.get_include()]
+        include_dirs=["cubist/src", np.get_include()],
+        define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')],
     )
 ]
 
-with open("README.md", 'r') as f:
+with open("README.md", 'r', encoding="utf-8") as f:
     long_description = f.read()
 
-with open("requirements.txt", 'r') as f:
+with open("requirements.txt", 'r', encoding="utf-8") as f:
     requires = f.read()
 
 
@@ -32,8 +33,7 @@ def get_version(rel_path):
         if line.startswith('__version__'):
             delim = '"' if '"' in line else "'"
             return line.split(delim)[1]
-    else:
-        raise RuntimeError("Unable to find version string.")
+    raise RuntimeError("Unable to find version string.")
 
 
 setup(
@@ -43,7 +43,9 @@ def get_version(rel_path):
     description="A Python package for fitting Quinlan's Cubist regression model.",
     long_description=long_description,
     long_description_content_type="text/markdown",
-    ext_modules=cythonize(extensions),
+    ext_modules=cythonize(extensions, 
+                          include_path = [np.get_include()],
+                          compiler_directives={'language_level' : "3"}),
     zip_safe=False,
     include_package_data=True,
     install_requires=requires,


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..eab060a
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,69 @@
+name: Test and Lint Cubist
+
+on:
+  workflow_dispatch:
+    inputs:
+      logLevel:
+        description: 'Log level'
+        required: true
+        default: 'warning'
+  pull_request:
+    types:
+      - synchronize
+    branches:
+      - dev
+      - main
+  workflow_call:
+
+jobs:
+  test:
+    name: Test and lint on Python 3.11 and ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+    runs-on: ${{ matrix.os }}
+    defaults:
+      run:
+        shell: bash
+
+    steps:
+    - name: Checkout repo
+      uses: actions/checkout@v3
+
+    - name: Set up Python 3.11
+      uses: actions/setup-python@v4
+      with:
+        python-version: "3.11"
+    
+    - name: Enable Developer Command Prompt for Windows
+      if: matrix.os == 'windows-latest'
+      uses: ilammy/msvc-dev-cmd@v1
+    
+    - name: Add sys/time.h for Windows
+      if: matrix.os == 'windows-latest'
+      run: |
+        mv winsys/ cubist/sys
+      
+    - name: Remove winsys folder for non-Windows OS
+      if: matrix.os != 'windows-latest'
+      run: |
+        rm -r winsys/
+
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install setuptools wheel build cython
+        pip install flake8 pytest pytest-cov numpy
+        pip install -r requirements.txt
+        python setup.py build_ext --inplace
+
+    - name: Lint with flake8
+      run: |
+        # stop the build if there are Python syntax errors or undefined names
+        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+
+    - name: Test with pytest
+      run: |
+        pytest --cov=com --cov-report=xml --cov-report=html
diff --git a/cubist/tests/test_cubist.py b/cubist/tests/test_cubist.py
index 1784258..8c471d4 100644
--- a/cubist/tests/test_cubist.py
+++ b/cubist/tests/test_cubist.py
@@ -126,3 +126,17 @@ def test_auto(auto, expected, n, raises):
         assert expected == model._check_composite(n)
         model.fit(X, y)
         check_is_fitted(model)
+
+
+@pytest.mark.parametrize("raises",
+                         [(pytest.raises(ValueError))])
+def test_missing_column_name(raises):
+    model = Cubist()
+    # copy X so we can change the column names without editing the main object
+    X_changed_cols = X.copy(deep=True)
+    # change the age column to an empty string
+    X_changed_cols = X_changed_cols.rename(columns={"age": ""})
+    # make sure we get a ValueError for this
+    with raises:
+        model.fit(X_changed_cols, y)
+        check_is_fitted(model)