Session 2: Classification and Clustering¶

Getting started¶

First, we will load the packages we need for these exercises.

In [1]:
# This line is needed to force matplotlib to display inline in the notebook
%matplotlib inline

# The 3 lines below here suppress ConvergenceWarnings -- this is not necessarily something
# I would recommend doing in practice, but avoids spammed warnings in a particular cell
# towards the end of this file
from warnings import simplefilter
from sklearn.exceptions import ConvergenceWarning
simplefilter("ignore", category=ConvergenceWarning)

import os
import pickle
import tempfile

import polars as pl                        # Work with data frames
import numpy as np                         # Simple mathematics function and linear algebra
import matplotlib.pyplot as plt            # Charting functions
plt.rcParams['figure.figsize'] = [12, 8]   # Make plots larger by default
from sklearn import cluster                # For using kmeans
from sklearn import linear_model           # GLM-type models -- both econometric and ML
from sklearn import model_selection        # Training and testing splits
from sklearn import metrics                # Model performance evaluation
from sklearn import neighbors              # For KNN
from sklearn import preprocessing          # For standardizing data for LASSO and Elastic net
from sklearn import svm                    # For SVM-type models
import xgboost as xgb                      # For xgboost models

# Numba cannot cache beside packages installed on some mapped/network drives.
# Set a writable cache location before importing UMAP.
os.environ.setdefault('NUMBA_CACHE_DIR', os.path.join(tempfile.gettempdir(), 'numba_cache'))
import umap.umap_ as umap                  # Needed for the custom function `umap_compare_svm()`

Next, we need to import the dataset for the sessions exercises.

We will also split the data file like we did for Session 1 and apply the needed transformations to it using sklearn.

In [3]:
# Load data
# Scan the full file when inferring types because some numeric columns begin with integers
# but contain floating-point values later in the file.
df = pl.read_parquet('../../Data/S1_data.parquet')

# Define variable sets
vars_financial = ['logtotasset', 'rsst_acc', 'chg_recv', 'chg_inv', 'soft_assets', 'pct_chg_cashsales', 'chg_roa',
                  'issuance', 'oplease_dum', 'book_mkt', 'lag_sdvol', 'merger', 'bigNaudit', 'midNaudit', 'cffin',
                  'exfin', 'restruct']
vars_style = ['bullets', 'headerlen', 'newlines', 'alltags', 'processedsize', 'sentlen_u', 'wordlen_s', 'paralen_s',
              'repetitious_p', 'sentlen_s', 'typetoken', 'clindex', 'fog', 'active_p', 'passive_p', 'lm_negative_p',
              'lm_positive_p', 'allcaps', 'exclamationpoints', 'questionmarks']
vars_topic = ['Topic_' + str(i+1) + '_n_oI' for i in range(0,31)]

# Subset the final year to be the testing year
train = df.filter(pl.col('year') < 2004)
test = df.filter(pl.col('year') == 2004)

# Set up the data for the linear problem
vars_linear = vars_topic
scaler_X = preprocessing.StandardScaler()
scaler_X.fit(train.select(vars_linear).to_numpy())
train_X_linear = scaler_X.transform(train.select(vars_linear).to_numpy())
test_X_linear = scaler_X.transform(test.select(vars_linear).to_numpy())

scaler_Y = preprocessing.StandardScaler()
train_sdvol = train.get_column('sdvol1').to_numpy().reshape(-1, 1)
test_sdvol = test.get_column('sdvol1').to_numpy().reshape(-1, 1)
scaler_Y.fit(train_sdvol)
train_Y_linear = scaler_Y.transform(train_sdvol)
test_Y_linear = scaler_Y.transform(test_sdvol)

# Set up the data for the binary classification problem
vars_logistic = vars_topic + vars_financial + vars_style
scaler_X = preprocessing.StandardScaler()
scaler_X.fit(train.select(vars_logistic).to_numpy())
train_X_logistic = scaler_X.transform(train.select(vars_logistic).to_numpy())
test_X_logistic = scaler_X.transform(test.select(vars_logistic).to_numpy())

train_Y_logistic = train.get_column('Restate_Int').to_numpy()
test_Y_logistic = test.get_column('Restate_Int').to_numpy()

Below I have also defined some custom functions. Don't worry about these until we get to using them.

In [4]:
# From umap.plot source code on Github
def _get_embedding(umap_object):
    if hasattr(umap_object, "embedding_"):
        return umap_object.embedding_
    elif hasattr(umap_object, "embedding"):
        return umap_object.embedding
    else:
        raise ValueError("Could not find embedding attribute of umap_object")


def umap_color(data_map, data_color, cmap='viridis', subset=None, title=None):
    """Plot a UMAP embedding colored by numeric or categorical labels."""
    data_map = np.asarray(data_map)
    data_color = np.asarray(data_color)
    embed = _get_embedding(umap.UMAP(random_state=42, n_jobs=1).fit(data_map))

    if subset is not None:
        embed = embed[subset]
        data_color = data_color[subset]

    fig, ax = plt.subplots(figsize=(12, 8))
    point_size = 100.0 / np.sqrt(len(embed))
    if np.issubdtype(data_color.dtype, np.number):
        points = ax.scatter(embed[:, 0], embed[:, 1], s=point_size, c=data_color, cmap=cmap)
        fig.colorbar(points, ax=ax)
    else:
        categories = np.unique(data_color)
        colors = plt.get_cmap(cmap)(np.linspace(0, 1, len(categories)))
        for category, color in zip(categories, colors):
            mask = data_color == category
            ax.scatter(embed[mask, 0], embed[mask, 1], s=point_size, color=color, label=category)
        ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')

    ax.set(xticks=[], yticks=[], title=title)
    return ax


# Cut down version of umap.plot.points to remove dependencies on  datashader, bokeh, holoviews, scikit-image, and colorcet
def umap_compare_svm(X, Yhat, Y, clip = None, cmap='viridis', subset=None, binary=False, title=None):
    reducer = umap.UMAP()
    umap_object = reducer.fit(X)
    embed = _get_embedding(umap_object)
    if clip is not None:
        Yhat = np.clip(Yhat, clip[0][0], clip[0][1])
        Y = np.clip(Y, clip[1][0], clip[1][1])

    fig, (ax1, ax2) = plt.subplots(1, 2)
    if subset is not None:
        embed_X = embed[subset,0]
        embed_Y = embed[subset,1]
        Y = np.array(Y[subset])
        X = np.array(X[subset])
        Yhat = np.array(Yhat[subset])
    else:
        embed_X = embed[:, 0]
        embed_Y = embed[:, 1]
    
    point_size = 100.0 / np.sqrt(len(embed_X))
    
    if binary:
        point_size = point_size * (1 + Y * binary)
    
    # color by values for Yhat
    points = ax1.scatter(embed_X, embed_Y, s=point_size, c=Yhat, cmap=cmap)
    fig.colorbar(points, ax=ax1, orientation='horizontal')

    ax1.set(xticks=[], yticks=[])
    ax1.set_title("Predicted values")
    
    # color by values for Y
    points = ax2.scatter(embed_X, embed_Y, s=point_size, c=Y, cmap=cmap)
    
    fig.colorbar(points, ax=ax2, orientation='horizontal')

    ax2.set(xticks=[], yticks=[])
    ax2.set_title("Actual values")
    
    if title is not None:
        fig.suptitle(title)
    
    if clip is not None:
        foot = 'Predicted values winsorized to [{}, {}]; Actual values winsorized to [{}, {}]'.format(clip[0][0], clip[0][1], clip[1][0], clip[1][1])
        plt.figtext(0.2, 0.3, foot, horizontalalignment='left')
    
    return (ax1, ax2)


def logistic(x):
    return 1 / (1 + np.exp(-1 * x))


def coefplot(names, coef, title=None):
    # Make sure coef is list, cast to list if needed.
    if isinstance(coef, np.ndarray):
        if len(coef.shape) > 1:
            coef = list(coef[0])
        else:
            coef = list(coef)
    
    # Drop unneeded vars
    data = []
    for i in range(0, len(coef)):
        if coef[i] != 0:
            data.append([names[i], coef[i]])
    data.sort(key=lambda x: x[1])
    
    # Add in a key for the plot axis
    data = [data[i] + [i+1] for i in range(0,len(data))]
    
    fig, ax = plt.subplots(figsize=(4,0.25*len(data)))

    ax.scatter([i[1] for i in data], [i[2] for i in data])
    
    ax.grid(axis='y')
    ax.set(xlabel="Fitted value", ylabel="Residual", title=(title if title is not None else "Coefficient Plot"))
    
    ax.axvline(x=0, linestyle='dotted')
    ax.set_yticks([i[2] for i in data])
    ax.set_yticklabels([i[0] for i in data])
    
    return ax

SVM and SVR¶

In computer science and many other discplines, a simple type of classifier used in training models is SVM. This is particularly common for algorithms that are being used as supervised methods for predicting some variable in a model.

SVM: Support Vector Machine for Classification¶

This is the most common approach for these algorithms. The algorithm will allow us to approach a classification problem, and it can either report the most likely class for each observation, or, like with logistic regression, it can report the probability of belonging to a given class.

Multiple implementations are available in sklearn for this algorithm:

  • sklearn.svm.LinearSVC(): Fast and memory efficient; assumes a linear kernel. Does not output probabilities!
  • sklearn.svm.SVC(): More flexible as it allows other kernel functions, but less efficient when using a linear kernel
  • sklearn.linear.SGDClassifier(): Can emulate sklearn.svm.LinearSVC() depending on specified parameters, allows some additional flexibility in parameters, is more memory efficient, and can do online (batch) training (good for very large data sets)

Since we are using a linear kernel in this example, we can also make sense of the coefficients assigned to each input in our model. We can actually use the exact same coefplot() as in Session 1 to visualize the algorithm. However, if we change the kernel, such as using the radial basis function kernel, then we cannot use coefplot() meaningfully. There are other ways to visualize this, however, as we will see later.

In [5]:
model_svc = svm.LinearSVC(C=1, dual=False)
model_svc.fit(train_X_logistic, train_Y_logistic)
Out[5]:
LinearSVC(C=1, dual=False)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
dual dual: "auto" or bool, default="auto"

Select the algorithm to either solve the dual or primal
optimization problem. Prefer dual=False when n_samples > n_features.
`dual="auto"` will choose the value of the parameter automatically,
based on the values of `n_samples`, `n_features`, `loss`, `multi_class`
and `penalty`. If `n_samples` < `n_features` and optimizer supports
chosen `loss`, `multi_class` and `penalty`, then dual will be set to True,
otherwise it will be set to False.

.. versionchanged:: 1.3
The `"auto"` option is added in version 1.3 and will be the default
in version 1.5.
False
penalty penalty: {'l1', 'l2'}, default='l2'

Specifies the norm used in the penalization. The 'l2'
penalty is the standard used in SVC. The 'l1' leads to ``coef_``
vectors that are sparse.
'l2'
loss loss: {'hinge', 'squared_hinge'}, default='squared_hinge'

Specifies the loss function. 'hinge' is the standard SVM loss
(used e.g. by the SVC class) while 'squared_hinge' is the
square of the hinge loss. The combination of ``penalty='l1'``
and ``loss='hinge'`` is not supported.
'squared_hinge'
tol tol: float, default=1e-4

Tolerance for stopping criteria.
0.0001
C C: float, default=1.0

Regularization parameter. The strength of the regularization is
inversely proportional to C. Must be strictly positive.
For an intuitive visualization of the effects of scaling
the regularization parameter C, see
:ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py`.
1
multi_class multi_class: {'ovr', 'crammer_singer'}, default='ovr'

Determines the multi-class strategy if `y` contains more than
two classes.
``"ovr"`` trains n_classes one-vs-rest classifiers, while
``"crammer_singer"`` optimizes a joint objective over all classes.
While `crammer_singer` is interesting from a theoretical perspective
as it is consistent, it is seldom used in practice as it rarely leads
to better accuracy and is more expensive to compute.
If ``"crammer_singer"`` is chosen, the options loss, penalty and dual
will be ignored.
'ovr'
fit_intercept fit_intercept: bool, default=True

Whether or not to fit an intercept. If set to True, the feature vector
is extended to include an intercept term: `[x_1, ..., x_n, 1]`, where
1 corresponds to the intercept. If set to False, no intercept will be
used in calculations (i.e. data is expected to be already centered).
True
intercept_scaling intercept_scaling: float, default=1.0

When `fit_intercept` is True, the instance vector x becomes ``[x_1,
..., x_n, intercept_scaling]``, i.e. a "synthetic" feature with a
constant value equal to `intercept_scaling` is appended to the instance
vector. The intercept becomes intercept_scaling * synthetic feature
weight. Note that liblinear internally penalizes the intercept,
treating it like any other term in the feature vector. To reduce the
impact of the regularization on the intercept, the `intercept_scaling`
parameter can be set to a value greater than 1; the higher the value of
`intercept_scaling`, the lower the impact of regularization on it.
Then, the weights become `[w_x_1, ..., w_x_n,
w_intercept*intercept_scaling]`, where `w_x_1, ..., w_x_n` represent
the feature weights and the intercept weight is scaled by
`intercept_scaling`. This scaling allows the intercept term to have a
different regularization behavior compared to the other features.
1
class_weight class_weight: dict or 'balanced', default=None

Set the parameter C of class i to ``class_weight[i]*C`` for
SVC. If not given, all classes are supposed to have
weight one.
The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.
None
verbose verbose: int, default=0

Enable verbose output. Note that this setting takes advantage of a
per-process runtime setting in liblinear that, if enabled, may not work
properly in a multithreaded context.
0
random_state random_state: int, RandomState instance or None, default=None

Controls the pseudo random number generation for shuffling the data for
the dual coordinate descent (if ``dual=True``). When ``dual=False`` the
underlying implementation of :class:`LinearSVC` is not random and
``random_state`` has no effect on the results.
Pass an int for reproducible output across multiple function calls.
See :term:`Glossary <random_state>`.
None
max_iter max_iter: int, default=1000

The maximum number of iterations to be run.
1000
Fitted attributes
Name Type Value
classes_ classes_: ndarray of shape (n_classes,)

The unique classes labels.
ndarray[int64](2,) [0,1]
coef_ coef_: ndarray of shape (1, n_features) if n_classes == 2 else (n_classes, n_features)

Weights assigned to the features (coefficients in the primal
problem).

``coef_`` is a readonly property derived from ``raw_coef_`` that
follows the internal memory layout of liblinear.
ndarray[float64](1, 68) [[ 0.01,-0. ,-0. ,...,-0. ,-0.01,-0.2 ]]
intercept_ intercept_: ndarray of shape (1,) if n_classes == 2 else (n_classes,)

Constants in decision function.
ndarray[float64](1,) [-1.]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 68
n_iter_ n_iter_: int

Maximum number of iterations run across all classes.
int 10
In [6]:
display = metrics.RocCurveDisplay.from_estimator(model_svc, test_X_logistic, test_Y_logistic)
display.plot()
Out[6]:
<sklearn.metrics._plot.roc_curve.RocCurveDisplay at 0x18893b823c0>
In [7]:
coefplot(vars_logistic, model_svc.coef_)
Out[7]:
<Axes: title={'center': 'Coefficient Plot'}, xlabel='Fitted value', ylabel='Residual'>

Optimizing C¶

The above classifier just took the default of C=1. However, like with LASSO and related models, C is a hyperparameter. Unlike LASSO and related models, sklearn does not provide us with a built-in CV function. Instead, we will need to use sklearn's built-in cross validation constructors to customize a cross validation for this. This is built into the same sklearn.model_selection library that we used for splitting data in Session 1.

Note that the model_selection.GridSearchCV() function allows for parallel processing -- you can specify the number of processes to use using the n_jobs= parameter.

Also note that model_selection.StratifiedShuffleSplit() is a bit more complex than the standard cross validation we did in session 1, in that it is making stratified random samples across classes. This is rather important in our case, since there is pretty severe class imbalance (less than 3% of data is an irregularity). If you want to directly replicate the method from before, model_selection.KFold() will do the trick

In [8]:
C_range = np.logspace(-2, 6, 9)
param_grid = dict(C=C_range)
cv = model_selection.StratifiedShuffleSplit(n_splits=5, test_size=0.2, random_state=1)
grid_svc = model_selection.GridSearchCV(svm.LinearSVC(dual=False), param_grid=param_grid, cv=cv, n_jobs=20)
grid_svc.fit(train_X_logistic, train_Y_logistic)
print("The best parameters are %s with a score of %0.2f"
      % (grid_svc.best_params_, grid_svc.best_score_))
The best parameters are {'C': np.float64(0.01)} with a score of 0.99
In [9]:
display = metrics.RocCurveDisplay.from_estimator(grid_svc, test_X_logistic, test_Y_logistic)
display.plot()
Out[9]:
<sklearn.metrics._plot.roc_curve.RocCurveDisplay at 0x188939c9bd0>
In [10]:
coefplot(vars_logistic, grid_svc.best_estimator_.coef_)
Out[10]:
<Axes: title={'center': 'Coefficient Plot'}, xlabel='Fitted value', ylabel='Residual'>

Visualizing SVC¶

Given the high dimensionality of the SVC algorithm, it is difficult to visualize. To aid in this, the below visualizations implement a dimensionality reduction technique (UMAP) and show the resulting probabilities embedded into the 2-dimensional projection from UMAP. This will allow us to see if there are any clear patterns in our data as it relates to our dependent variable.

Note that LinearSVC does not provide probabilities. However, it reports a score that roughly approximates the output of a logistic regression via .decision_function(). Thus, applying a logistic (i.e., sigmoid) function of $f(x)=\frac{1}{1+e^{-x}}$ to the output will convert to probabilities. Note that there is some weighting internally applied within LinearSVC which we are unable to recover, introducing some potential error in the below charts.

Probabilities are fully recoverable under SVC(), however, SVC() uses a "hinge" loss function whereas LinearSVC() uses a "squared hinge" loss function. As the problem we are focused on is quite sensative to the choice of loss function (i.e., performs poorly under hinge loss), we will use the output from our linearSVC() above.

In [11]:
train_Yhat_logistic = logistic(grid_svc.decision_function(train_X_logistic))
In [12]:
umap_compare_svm(train_X_logistic, train_Yhat_logistic, train_Y_logistic,
                 clip=[[0.25, 0.3], [0, 1]], binary=5,
                title="Full sample")
Out[12]:
(<Axes: title={'center': 'Predicted values'}>,
 <Axes: title={'center': 'Actual values'}>)
In [13]:
umap_compare_svm(train_X_logistic, train_Yhat_logistic, train_Y_logistic, clip=[[0.25, 0.3], [0, 1]], cmap='coolwarm', binary=5,
                 subset=((train_Y_logistic==1) | (np.random.rand(len(train_Y_logistic))<0.05)),
                title="Performance on actual irregularities (Large) and random sample of non-irregularities")
Out[13]:
(<Axes: title={'center': 'Predicted values'}>,
 <Axes: title={'center': 'Actual values'}>)
In [14]:
umap_compare_svm(train_X_logistic, train_Yhat_logistic, train_Y_logistic, clip=[[0.25, 0.3], [0, 1]], cmap='coolwarm', binary=5,
                 subset=((train_Y_logistic==0) & (np.random.rand(len(train_Y_logistic))<0.05)),
                title="Performance on a random sample of non-irregularities")
Out[14]:
(<Axes: title={'center': 'Predicted values'}>,
 <Axes: title={'center': 'Actual values'}>)

SVR: Support Vector Regression¶

Notes:

  • I am specifying dual=False here because we have more observations than regressors. If you have more regressors than datapoints, set dual=True.
In [15]:
model_svr = svm.LinearSVR(C=1, dual=False, loss='squared_epsilon_insensitive')
model_svr.fit(train_X_linear, np.ravel(train_Y_linear))
Out[15]:
LinearSVR(C=1, dual=False, loss='squared_epsilon_insensitive')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
loss loss: {'epsilon_insensitive', 'squared_epsilon_insensitive'}, default='epsilon_insensitive'

Specifies the loss function. The epsilon-insensitive loss
(standard SVR) is the L1 loss, while the squared epsilon-insensitive
loss ('squared_epsilon_insensitive') is the L2 loss.
'squared_epsilon_insensitive'
dual dual: "auto" or bool, default="auto"

Select the algorithm to either solve the dual or primal
optimization problem. Prefer dual=False when n_samples > n_features.
`dual="auto"` will choose the value of the parameter automatically,
based on the values of `n_samples`, `n_features` and `loss`. If
`n_samples` < `n_features` and optimizer supports chosen `loss`,
then dual will be set to True, otherwise it will be set to False.

.. versionchanged:: 1.3
The `"auto"` option is added in version 1.3 and will be the default
in version 1.5.
False
epsilon epsilon: float, default=0.0

Epsilon parameter in the epsilon-insensitive loss function. Note
that the value of this parameter depends on the scale of the target
variable y. If unsure, set ``epsilon=0``.
0.0
tol tol: float, default=1e-4

Tolerance for stopping criteria.
0.0001
C C: float, default=1.0

Regularization parameter. The strength of the regularization is
inversely proportional to C. Must be strictly positive.
1
fit_intercept fit_intercept: bool, default=True

Whether or not to fit an intercept. If set to True, the feature vector
is extended to include an intercept term: `[x_1, ..., x_n, 1]`, where
1 corresponds to the intercept. If set to False, no intercept will be
used in calculations (i.e. data is expected to be already centered).
True
intercept_scaling intercept_scaling: float, default=1.0

When `fit_intercept` is True, the instance vector x becomes `[x_1, ...,
x_n, intercept_scaling]`, i.e. a "synthetic" feature with a constant
value equal to `intercept_scaling` is appended to the instance vector.
The intercept becomes intercept_scaling * synthetic feature weight.
Note that liblinear internally penalizes the intercept, treating it
like any other term in the feature vector. To reduce the impact of the
regularization on the intercept, the `intercept_scaling` parameter can
be set to a value greater than 1; the higher the value of
`intercept_scaling`, the lower the impact of regularization on it.
Then, the weights become `[w_x_1, ..., w_x_n,
w_intercept*intercept_scaling]`, where `w_x_1, ..., w_x_n` represent
the feature weights and the intercept weight is scaled by
`intercept_scaling`. This scaling allows the intercept term to have a
different regularization behavior compared to the other features.
1.0
verbose verbose: int, default=0

Enable verbose output. Note that this setting takes advantage of a
per-process runtime setting in liblinear that, if enabled, may not work
properly in a multithreaded context.
0
random_state random_state: int, RandomState instance or None, default=None

Controls the pseudo random number generation for shuffling the data.
Pass an int for reproducible output across multiple function calls.
See :term:`Glossary <random_state>`.
None
max_iter max_iter: int, default=1000

The maximum number of iterations to be run.
1000
Fitted attributes
Name Type Value
coef_ coef_: ndarray of shape (n_features) if n_classes == 2 else (n_classes, n_features)

Weights assigned to the features (coefficients in the primal
problem).

`coef_` is a readonly property derived from `raw_coef_` that
follows the internal memory layout of liblinear.
ndarray[float64](31,) [ 0.05,-0.01,-0.01,...,-0.01,-0.02,-0.05]
intercept_ intercept_: ndarray of shape (1) if n_classes == 2 else (n_classes)

Constants in decision function.
ndarray[float64](1,) [0.]
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 31
n_iter_ n_iter_: int

Maximum number of iterations run across all classes.
int 4
In [16]:
coefplot(vars_linear, model_svr.coef_)
Out[16]:
<Axes: title={'center': 'Coefficient Plot'}, xlabel='Fitted value', ylabel='Residual'>
In [17]:
C_range = np.logspace(-4, 6, 11)
param_grid = dict(C=C_range)
cv = model_selection.KFold(n_splits=5)
grid_svr = model_selection.GridSearchCV(svm.LinearSVR(dual=False, loss="squared_epsilon_insensitive"), param_grid=param_grid, cv=cv, n_jobs=8)
grid_svr.fit(train_X_linear, np.ravel(train_Y_linear))
print("The best parameters are %s with a score of %0.2f"
      % (grid_svr.best_params_, grid_svr.best_score_))
The best parameters are {'C': np.float64(0.0001)} with a score of 0.06
In [18]:
coefplot(vars_linear, grid_svr.best_estimator_.coef_)
Out[18]:
<Axes: title={'center': 'Coefficient Plot'}, xlabel='Fitted value', ylabel='Residual'>
In [19]:
train_Yhat_linear = model_svr.predict(train_X_linear)
In [20]:
umap_compare_svm(train_X_linear, train_Yhat_linear, train_Y_linear, clip=[[0, 2], [0, 2]])
Out[20]:
(<Axes: title={'center': 'Predicted values'}>,
 <Axes: title={'center': 'Actual values'}>)

XGBoost¶

This algorithm differs from the previous algorithms we have looked at primarily by being non-linear (unless you request it to be linear). Furthermore, it is tree-based and leverages a technique called "boosting" -- a type of ensembling.

XGBoost is a flexible algorithm supporting manyt functional forms (both regression structures and loss functions). It supports a variety of linear, binary, count, survival, multiclass, ranking, gamma, and tweedie forms.

Implementing XGBoost in python¶

Python has a very good library for XGBoost, aptly called xgboost. We will use this library to both fit and visualize our models.

Note: The below does not exhaust all possible parameters. There are also $L_2$ and $L_1$ regularization options as well as sampling parameters, among others. See a full list here.

The algorithm requires that data is passed to it in its own format. It can take our current datasets as input, and will create its desired format using the xgb.DMatrix() function.

In [21]:
dtrain = xgb.DMatrix(train_X_logistic, label=train_Y_logistic, feature_names=vars_logistic)
dtest = xgb.DMatrix(test_X_logistic, label=test_Y_logistic, feature_names=vars_logistic)

There are many parameters to set for the model. Every parameter has a default value, so you can focus on just those that are needed for your usage. Below is a sample starting point.

In [22]:
param = {
    'booster': 'gbtree',             # default -- tree based
    'nthread': 8,                    # number of threads to use for parallel processing
    'objective': 'binary:logistic',  # binary, output probabilities
    'eval_metric': 'auc',            # maximize ROC AUC
    'eta': 0.3,                      # shrinkage; [0, 1], default 0.3
    'max_depth': 6,                  # maximum depth of each tree; default 6
    'gamma': 0.1,                    # set above 0 to prune trees, [0, inf], default 0
    'min_child_weight': 1,           # higher leads to more pruning of tress, [0, inf], default 1
    'subsample': 0.8,                # Randomly subsample rows if in (0, 1), default 1
    'colsample_bytree': 0.8,         # Randomly subsample variables if in (0, 1), default 1
    'random_state': 70
}
num_round = 30

Next we can train the model using xgb.train().

In [23]:
model_xgb_logistic = xgb.train(param, dtrain, num_round)

We can check the ROC AUC just like we did with prior models.

In [24]:
test_Yhat_xgb_logistic = model_xgb_logistic.predict(dtest)
auc = metrics.roc_auc_score(test_Y_logistic, test_Yhat_xgb_logistic)
auc
Out[24]:
0.5940013491775207
In [25]:
fpr, tpr, thresholds = metrics.roc_curve(test_Y_logistic, test_Yhat_xgb_logistic)
display = metrics.RocCurveDisplay(fpr=fpr, tpr=tpr, roc_auc=auc)
display.plot()
Out[25]:
<sklearn.metrics._plot.roc_curve.RocCurveDisplay at 0x188adc32490>

To aid with interpretation, XGBoost also can output a chart showing the relative importance of each variable in the model. This can help us to see which data is important for the model. Note that since the model is non-linear, there are no signed coefficients. The chart simply tells us which variables have the greatest impact on its classification.

In [26]:
fig, ax = plt.subplots(figsize=(8,16))
xgb.plot_importance(model_xgb_logistic, ax=ax)
Out[26]:
<Axes: title={'center': 'Feature importance'}, xlabel='Importance score', ylabel='Features'>

Seeing the trees¶

There are two ways to see the tress in the model:

  1. Dump the model to a text file
  2. Use graphviz to draw the trees (which you can output to the notebook or save to a file.

Both of these methods are shown below.

In [27]:
# If you want to see the final tree model, this will dump it into a file in the same folder as this notebook.
model_xgb_logistic.dump_model('dump.raw.txt')
In [28]:
# To run this, you need to:
#     1. Install the python graphviz package
#     2. Install graphviz from https://graphviz.org/download/
#         - MAKE SURE TO SELECT one of the "add to path" options during installation if on Windows!

xgb.to_graphviz(model_xgb_logistic, num_trees=0)
M:\Python_environments\miniconda\envs\MLSS2\Lib\site-packages\xgboost\plotting.py:268: FutureWarning: The `num_trees` parameter is deprecated, use `tree_idx` instead. 
  warnings.warn(
Out[28]:
0 processedsize<1.30414915 1 logtotasset<-1.39944577 0->1 yes 2 pct_chg_cashsales<-0.00804154109 0->2 no, missing 3 lm_negative_p<0.145240888 1->3 yes 4 lm_negative_p<1.37679541 1->4 no, missing 5 Topic_21_n_oI<0.615400076 2->5 yes 6 chg_recv<0.341733605 2->6 no, missing 7 Topic_31_n_oI<-0.276235491 3->7 yes 8 logtotasset<-1.59205961 3->8 no, missing 9 logtotasset<1.30269408 4->9 yes 10 Topic_21_n_oI<-0.143419459 4->10 no, missing 15 leaf=0.298255324 7->15 yes 16 leaf=-0.212395415 7->16 no, missing 17 leaf=0.147831559 8->17 yes 18 leaf=1.22113252 8->18 no, missing 19 paralen_s<1.00319266 9->19 yes 20 Topic_11_n_oI<-0.295223117 9->20 no, missing 21 Topic_3_n_oI<-0.331051439 10->21 yes 22 Topic_31_n_oI<0.343781799 10->22 no, missing 25 lm_negative_p<0.950329721 19->25 yes 26 Topic_4_n_oI<-0.127335146 19->26 no, missing 27 Topic_7_n_oI<-0.0221904907 20->27 yes 28 Topic_2_n_oI<-0.474801213 20->28 no, missing 33 leaf=-0.225886837 25->33 yes 34 leaf=0.0339902081 25->34 no, missing 35 leaf=-0.127058014 26->35 yes 36 leaf=0.498672456 26->36 no, missing 37 leaf=-0.0496588349 27->37 yes 38 leaf=1.10433984 27->38 no, missing 39 leaf=-0.00172489556 28->39 yes 40 leaf=-0.238708422 28->40 no, missing 29 leaf=-0.00172489556 21->29 yes 30 leaf=-0.177581459 21->30 no, missing 31 leaf=1.29331517 22->31 yes 32 Topic_4_n_oI<-0.244352773 22->32 no, missing 41 leaf=0.148265213 32->41 yes 42 leaf=-0.172764793 32->42 no, missing 11 lm_negative_p<0.400546253 5->11 yes 12 leaf=-0.00987907033 5->12 no, missing 13 leaf=-0.223103389 6->13 yes 14 leaf=0.736668468 6->14 no, missing 23 leaf=0.555297375 11->23 yes 24 leaf=2.35661221 11->24 no, missing
In [29]:
xgb.to_graphviz(model_xgb_logistic, num_trees=1)
M:\Python_environments\miniconda\envs\MLSS2\Lib\site-packages\xgboost\plotting.py:268: FutureWarning: The `num_trees` parameter is deprecated, use `tree_idx` instead. 
  warnings.warn(
Out[29]:
0 paralen_s<3.26210833 1 lm_negative_p<1.86245811 0->1 yes 2 Topic_22_n_oI<-0.266438395 0->2 no, missing 3 typetoken<-1.11252701 1->3 yes 4 sentlen_s<-0.355484098 1->4 no, missing 5 leaf=0.0815696418 2->5 yes 6 leaf=1.03576505 2->6 no, missing 7 Topic_10_n_oI<-0.325805396 3->7 yes 8 cffin<1.00708044 3->8 no, missing 9 Topic_23_n_oI<-0.734149456 4->9 yes 10 Topic_13_n_oI<0.07167916 4->10 no, missing 11 leaf=-0.23318924 7->11 yes 12 Topic_8_n_oI<-0.180090398 7->12 no, missing 13 Topic_10_n_oI<0.0253142789 8->13 yes 14 processedsize<-0.125081301 8->14 no, missing 19 leaf=0.861382723 12->19 yes 20 leaf=-0.0159360357 12->20 no, missing 21 lag_sdvol<-0.815557241 13->21 yes 22 alltags<-0.441028595 13->22 no, missing 23 Topic_13_n_oI<-0.150196046 14->23 yes 24 lag_sdvol<0.0178490281 14->24 no, missing 27 leaf=0.157308459 21->27 yes 28 leaf=-0.205622897 21->28 no, missing 29 leaf=0.54270196 22->29 yes 30 leaf=-0.0553485379 22->30 no, missing 31 leaf=0.731927276 23->31 yes 32 leaf=-0.0224806406 23->32 no, missing 33 leaf=-0.247068942 24->33 yes 34 leaf=0.191757128 24->34 no, missing 15 leaf=0.282686889 9->15 yes 16 leaf=-0.24280262 9->16 no, missing 17 chg_roa<0.00529342098 10->17 yes 18 leaf=-0.0693112984 10->18 no, missing 25 leaf=0.0938539654 17->25 yes 26 leaf=1.11476541 17->26 no, missing
In [30]:
xgb.to_graphviz(model_xgb_logistic, num_trees=2)
M:\Python_environments\miniconda\envs\MLSS2\Lib\site-packages\xgboost\plotting.py:268: FutureWarning: The `num_trees` parameter is deprecated, use `tree_idx` instead. 
  warnings.warn(
Out[30]:
0 cffin<1.64797819 1 Topic_21_n_oI<-0.0809354559 0->1 yes 2 chg_recv<0.258733362 0->2 no, missing 3 Topic_28_n_oI<-0.634821177 1->3 yes 4 logtotasset<1.02941751 1->4 no, missing 5 soft_assets<-1.66982877 2->5 yes 6 paralen_s<-0.283015311 2->6 no, missing 7 Topic_20_n_oI<0.163843215 3->7 yes 8 chg_recv<0.884817123 3->8 no, missing 9 Topic_4_n_oI<1.19269776 4->9 yes 10 Topic_11_n_oI<0.520932615 4->10 no, missing 15 clindex<0.0267500114 7->15 yes 16 leaf=0.536578476 7->16 no, missing 17 chg_inv<1.06777072 8->17 yes 18 sentlen_s<-0.405534893 8->18 no, missing 25 leaf=-0.198193908 15->25 yes 26 leaf=-0.00197529607 15->26 no, missing 27 headerlen<0.527863503 17->27 yes 28 leaf=0.282614231 17->28 no, missing 29 leaf=0.485874057 18->29 yes 30 leaf=-0.194260001 18->30 no, missing 37 leaf=-0.270925879 27->37 yes 38 leaf=0.144144759 27->38 no, missing 19 chg_inv<-0.578680873 9->19 yes 20 leaf=0.547501087 9->20 no, missing 21 Topic_31_n_oI<-0.252071947 10->21 yes 22 Topic_19_n_oI<-0.322309375 10->22 no, missing 31 fog<-0.0757915825 19->31 yes 32 Topic_26_n_oI<-0.343289018 19->32 no, missing 39 leaf=0.486091822 31->39 yes 40 leaf=-0.130837128 31->40 no, missing 41 leaf=-0.0150453858 32->41 yes 42 leaf=-0.233591497 32->42 no, missing 33 leaf=-0.176518902 21->33 yes 34 Topic_31_n_oI<0.437063128 21->34 no, missing 35 leaf=-0.229341164 22->35 yes 36 leaf=-0.00930202566 22->36 no, missing 43 leaf=0.870557547 34->43 yes 44 leaf=-0.033154998 34->44 no, missing 11 leaf=-0.0915408283 5->11 yes 12 sentlen_u<0.487671524 5->12 no, missing 13 leaf=-0.00257144985 6->13 yes 14 leaf=-0.196685106 6->14 no, missing 23 leaf=1.00377512 12->23 yes 24 leaf=0.122014523 12->24 no, missing
In [31]:
# If you want to view every tree contained in your final model, the below code will dump a PNG file of each tree
# into a "trees/" directory in the same folder as this file.
num_trees = len(model_xgb_logistic.get_dump())
for tree_index in range(0, num_trees):
    dot = xgb.to_graphviz(model_xgb_logistic, num_trees=tree_index)
    dot.format = 'png'
    dot.render("xgb_trees/tree{}".format(tree_index))
M:\Python_environments\miniconda\envs\MLSS2\Lib\site-packages\xgboost\plotting.py:268: FutureWarning: The `num_trees` parameter is deprecated, use `tree_idx` instead. 
  warnings.warn(

Optimizing parameters¶

Here we will start with the same parameters as above. We will then train a model based on iteratively optimizing various parameters:

  1. max_depth and min_child_weight
  2. eta
  3. gamma
  4. subsample and colsample_bytree
  5. Number of rounds

The below example is reasonably rigorous for a properly trained model, but with some intuition applied to keep the search space smaller and to to keep execution time reasonable.

To make it easier to follow, we will use the same cross-validation methods as before, leaning on Scikit-learn. The XGBoost package comes with an interface to Scikit-learn, which is accessed via the xgb.XGBClassifier() function rather than the xgb.train() function.

In [75]:
param = {
    'booster': 'gbtree',             # default -- tree based
    'nthread': 8,                    # number of threads to use for parallel processing
    'objective': 'binary:logistic',  # binary, output probabilities
    'eval_metric': 'auc',            # maximize ROC AUC
    'eta': 0.3,                      # shrinkage; [0, 1], default 0.3
    'max_depth': 6,                  # maximum depth of each tree; default 6
    'gamma': 0.1,                    # set above 0 to prune trees, [0, inf], default 0
    'min_child_weight': 1,           # higher leads to more pruning of tress, [0, inf], default 1
    'subsample': 0.8,                # Randomly subsample rows if in (0, 1), default 1
    'colsample_bytree': 0.8,         # Randomly subsample variables if in (0, 1), default 1
    'random_state': 70
}
n_rounds = 20
random_states= [231534]
In [76]:
param_test = {
 'max_depth': [3, 4, 5],
 'min_child_weight': [1, 5, 10],
 'subsample': [0.7, 0.8, 0.9],
 'colsample_bytree': [0.8, 1.0],
 'eta': [0.05, 0.08, 0.1],
 'gamma': [0, 0.1, 0.3],
 }

del param['max_depth']
del param['min_child_weight']
del param['subsample']
del param['colsample_bytree']
del param['eta']
del param['gamma']

cv = model_selection.StratifiedShuffleSplit(n_splits=5, test_size=0.2, random_state=random_states[0])
search1 = model_selection.GridSearchCV(xgb.XGBClassifier(**param, n_estimators=n_rounds), 
             param_grid = param_test, scoring='roc_auc', n_jobs=12, cv=cv)
search1.fit(train_X_logistic,train_Y_logistic)
print(search1.best_params_, search1.best_score_)

param.update(search1.best_params_)
{'colsample_bytree': 1.0, 'eta': 0.05, 'gamma': 0, 'max_depth': 4, 'min_child_weight': 1, 'subsample': 0.8} 0.6834784818705524
In [84]:
num_round = 200
cv_results = xgb.cv(
    param, dtrain, num_round, nfold=10, stratified=False,
    early_stopping_rounds=50, as_pandas=False,
)
n_rounds = len(next(iter(cv_results.values())))
print('Stopping after {} rounds'.format(n_rounds))
Stopping after 28 rounds
In [85]:
final = xgb.XGBClassifier(**param, n_estimators=n_rounds)
final.fit(train_X_logistic,train_Y_logistic)
Out[85]:
XGBClassifier(base_score=None, booster='gbtree', callbacks=None,
              colsample_bylevel=None, colsample_bynode=None,
              colsample_bytree=1.0, device=None, early_stopping_rounds=None,
              enable_categorical=True, eta=0.05, eval_metric='auc',
              feature_types=None, feature_weights=None, gamma=0,
              grow_policy=None, importance_type=None,
              interaction_constraints=None, learning_rate=None, max_bin=None,
              max_cat_threshold=None, max_cat_to_onehot=None,
              max_delta_step=None, max_depth=4, max_leaves=None,
              min_child_weight=1, missing=nan, monotone_constraints=None,
              multi_strategy=None, n_estimators=28, n_jobs=None, ...)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
base_score base_score: typing.Union[float, typing.List[float], NoneType]

The initial prediction score of all instances, global bias.
None
booster 'gbtree'
callbacks callbacks: typing.Optional[typing.List[xgboost.callback.TrainingCallback]]

List of callback functions that are applied at end of each iteration.
It is possible to use predefined callbacks by using
:ref:`Callback API <callback_api>`.

.. note::

States in callback are not preserved during training, which means callback
objects can not be reused for multiple training sessions without
reinitialization or deepcopy.

.. code-block:: python

for params in parameters_grid:
# be sure to (re)initialize the callbacks before each run
callbacks = [xgb.callback.LearningRateScheduler(custom_rates)]
reg = xgboost.XGBRegressor(**params, callbacks=callbacks)
reg.fit(X, y)
None
colsample_bylevel colsample_bylevel: typing.Optional[float]

Subsample ratio of columns for each level.
None
colsample_bynode colsample_bynode: typing.Optional[float]

Subsample ratio of columns for each split.
None
colsample_bytree colsample_bytree: typing.Optional[float]

Subsample ratio of columns when constructing each tree.
1.0
device device: typing.Optional[str]

.. versionadded:: 2.0.0

Device ordinal, available options are `cpu`, `cuda`, and `gpu`.
None
early_stopping_rounds early_stopping_rounds: typing.Optional[int]

.. versionadded:: 1.6.0

- Activates early stopping. Validation metric needs to improve at least once in
every **early_stopping_rounds** round(s) to continue training. Requires at
least one item in **eval_set** in :py:meth:`fit`.

- If early stopping occurs, the model will have two additional attributes:
:py:attr:`best_score` and :py:attr:`best_iteration`. These are used by the
:py:meth:`predict` and :py:meth:`apply` methods to determine the optimal
number of trees during inference. If users want to access the full model
(including trees built after early stopping), they can specify the
`iteration_range` in these inference methods. In addition, other utilities
like model plotting can also use the entire model.

- If you prefer to discard the trees after `best_iteration`, consider using the
callback function :py:class:`xgboost.callback.EarlyStopping`.

- If there's more than one item in **eval_set**, the last entry will be used for
early stopping. If there's more than one metric in **eval_metric**, the last
metric will be used for early stopping.
None
enable_categorical enable_categorical: bool

See the same parameter of :py:class:`DMatrix` for details.
True
eval_metric eval_metric: typing.Union[str, typing.List[typing.Union[str, typing.Callable]], typing.Callable, NoneType]

.. versionadded:: 1.6.0

Metric used for monitoring the training result and early stopping. It can be a
string or list of strings as names of predefined metric in XGBoost (See
:doc:`/parameter`), one of the metrics in :py:mod:`sklearn.metrics`, or any
other user defined metric that looks like `sklearn.metrics`.

If custom objective is also provided, then custom metric should implement the
corresponding reverse link function.

Unlike the `scoring` parameter commonly used in scikit-learn, when a callable
object is provided, it's assumed to be a cost function and by default XGBoost
will minimize the result during early stopping.

For advanced usage on Early stopping like directly choosing to maximize instead
of minimize, see :py:obj:`xgboost.callback.EarlyStopping`.

See :doc:`/tutorials/custom_metric_obj` and :ref:`custom-obj-metric` for more
information.

.. code-block:: python

from sklearn.datasets import load_diabetes
from sklearn.metrics import mean_absolute_error
X, y = load_diabetes(return_X_y=True)
reg = xgb.XGBRegressor(
tree_method="hist",
eval_metric=mean_absolute_error,
)
reg.fit(X, y, eval_set=[(X, y)])
'auc'
feature_types feature_types: typing.Optional[typing.Sequence[str]]

.. versionadded:: 1.7.0

Used for specifying feature types without constructing a dataframe. See
the :py:class:`DMatrix` for details.
None
feature_weights feature_weights: Optional[ArrayLike]

Weight for each feature, defines the probability of each feature being selected
when colsample is being used. All values must be greater than 0, otherwise a
`ValueError` is thrown.
None
gamma gamma: typing.Optional[float]

(min_split_loss) Minimum loss reduction required to make a further partition on
a leaf node of the tree.
0
grow_policy grow_policy: typing.Optional[str]

Tree growing policy.

- depthwise: Favors splitting at nodes closest to the node,
- lossguide: Favors splitting at nodes with highest loss change.
None
importance_type None
interaction_constraints interaction_constraints: typing.Union[str, typing.List[typing.Tuple[str]], NoneType]

Constraints for interaction representing permitted interactions. The
constraints must be specified in the form of a nested list, e.g. ``[[0, 1], [2,
3, 4]]``, where each inner list is a group of indices of features that are
allowed to interact with each other. See :doc:`tutorial
</tutorials/feature_interaction_constraint>` for more information
None
learning_rate learning_rate: typing.Optional[float]

Boosting learning rate (xgb's "eta")
None
max_bin max_bin: typing.Optional[int]

If using histogram-based algorithm, maximum number of bins per feature
None
max_cat_threshold max_cat_threshold: typing.Optional[int]

.. versionadded:: 1.7.0

.. note:: This parameter is experimental

Maximum number of categories considered for each split. Used only by
partition-based splits for preventing over-fitting. Also, `enable_categorical`
needs to be set to have categorical feature support. See :doc:`Categorical Data
</tutorials/categorical>` and :ref:`cat-param` for details.
None
max_cat_to_onehot max_cat_to_onehot: Optional[int]

.. versionadded:: 1.6.0

.. note:: This parameter is experimental

A threshold for deciding whether XGBoost should use one-hot encoding based split
for categorical data. When number of categories is lesser than the threshold
then one-hot encoding is chosen, otherwise the categories will be partitioned
into children nodes. Also, `enable_categorical` needs to be set to have
categorical feature support. See :doc:`Categorical Data
</tutorials/categorical>` and :ref:`cat-param` for details.
None
max_delta_step max_delta_step: typing.Optional[float]

Maximum delta step we allow each tree's weight estimation to be.
None
max_depth max_depth: typing.Optional[int]

Maximum tree depth for base learners.
4
max_leaves max_leaves: typing.Optional[int]

Maximum number of leaves; 0 indicates no limit.
None
min_child_weight min_child_weight: typing.Optional[float]

Minimum sum of instance weight(hessian) needed in a child.
1
missing missing: float

Value in the data which needs to be present as a missing value. Default to
:py:data:`numpy.nan`.
nan
monotone_constraints monotone_constraints: typing.Union[typing.Dict[str, int], str, NoneType]

Constraint of variable monotonicity. See :doc:`tutorial </tutorials/monotonic>`
for more information.
None
multi_strategy multi_strategy: typing.Optional[str]

.. versionadded:: 2.0.0

.. note:: This parameter is working-in-progress.

The strategy used for training multi-target models, including multi-target
regression and multi-class classification. See :doc:`/tutorials/multioutput` for
more information.

- ``one_output_per_tree``: One model for each target.
- ``multi_output_tree``: Use multi-target trees.
None
n_estimators n_estimators: Optional[int]

Number of boosting rounds.
28
n_jobs n_jobs: typing.Optional[int]

Number of parallel threads used to run xgboost. When used with other
Scikit-Learn algorithms like grid search, you may choose which algorithm to
parallelize and balance the threads. Creating thread contention will
significantly slow down both algorithms.
None
num_parallel_tree None
random_state random_state: typing.Union[numpy.random.mtrand.RandomState, numpy.random._generator.Generator, int, NoneType]

Random number seed.

.. note::

Using gblinear booster with shotgun updater is nondeterministic as
it uses Hogwild algorithm.
70
reg_alpha reg_alpha: typing.Optional[float]

L1 regularization term on weights (xgb's alpha).
None
reg_lambda reg_lambda: typing.Optional[float]

L2 regularization term on weights (xgb's lambda).
None
sampling_method sampling_method: typing.Optional[str]

Sampling method. Used only by the GPU version of ``hist`` tree method.

- ``uniform``: Select random training instances uniformly.
- ``gradient_based``: Select random training instances with higher probability
when the gradient and hessian are larger. (cf. CatBoost)
None
scale_pos_weight scale_pos_weight: typing.Optional[float]

Balancing of positive and negative weights.
None
subsample subsample: typing.Optional[float]

Subsample ratio of the training instance.
0.8
tree_method tree_method: typing.Optional[str]

Specify which tree method to use. Default to auto. If this parameter is set to
default, XGBoost will choose the most conservative option available. It's
recommended to study this option from the parameters document :doc:`tree method
</treemethod>`
None
validate_parameters validate_parameters: typing.Optional[bool]

Give warnings for unknown parameter.
None
verbosity verbosity: typing.Optional[int]

The degree of verbosity. Valid values are 0 (silent) - 3 (debug).
None
nthread 8
eta 0.05
objective objective: typing.Union[str, xgboost.objective.Objective, xgboost.sklearn._SklObjWProto, typing.Callable[[typing.Any, typing.Any], typing.Tuple[numpy.ndarray, numpy.ndarray]], NoneType]

Specify the learning task and the corresponding learning objective or a custom
objective to be used.

For custom objective, see :doc:`/tutorials/custom_metric_obj` and
:ref:`custom-obj-metric` for more information, along with the end note for
function signatures.
'binary:logistic'
Fitted attributes
Name Type Value
classes_ ndarray[int64](2,) [0,1]
feature_importances_ ndarray[float32](68,) [0.02,0.02,0.01,...,0.02,0. ,0. ]
intercept_ ndarray[float32](1,) [0.01]
n_classes_ int 2
n_features_in_ int 68
In [86]:
metrics.RocCurveDisplay.from_estimator(final, test_X_logistic, test_Y_logistic)
Out[86]:
<sklearn.metrics._plot.roc_curve.RocCurveDisplay at 0x188ccedae90>
In [87]:
fig, ax = plt.subplots(figsize=(8,16))
xgb.plot_importance(final, ax=ax)
locs, labels = plt.yticks()
new_labels = []
for i in locs:
    new_labels.append(vars_logistic[int(labels[i].get_text()[1:])])
_ = plt.yticks(locs, new_labels)  # The `_ = ` here captures the excessive matplotlib output and deletes it
In [88]:
with open('../../Data/S2_models.pkl', 'wb') as f:
    pickle.dump({'SVC': grid_svc, 'XGBoost': final}, f)

Clustering¶

Kmeans¶

In [89]:
topic_names = ['Topic_' + str(i) + '_n_oI' for i in range(1, 32)]
industry_from_sic = (
    pl.when(pl.col('sic').is_between(0, 999)).then(pl.lit('Agriculture'))
    .when(pl.col('sic').is_between(1000, 1499)).then(pl.lit('Mining'))
    .when(pl.col('sic').is_between(1500, 1799)).then(pl.lit('Construction'))
    .when(pl.col('sic').is_between(2000, 3999)).then(pl.lit('Manufacturing'))
    .when(pl.col('sic').is_between(4000, 4999)).then(pl.lit('Utilities'))
    .when(pl.col('sic').is_between(5000, 5199)).then(pl.lit('Wholesale Trade'))
    .when(pl.col('sic').is_between(5200, 5999)).then(pl.lit('Retail Trade'))
    .when(pl.col('sic').is_between(6000, 6799)).then(pl.lit('Finance'))
    .when(pl.col('sic').is_between(7000, 8999)).then(pl.lit('Services'))
    .when(pl.col('sic').is_between(9100, 9999)).then(pl.lit('Public Admin'))
    .otherwise(pl.lit('Unknown'))
    .alias('industry')
)
train = train.with_columns(industry_from_sic)

train.select('industry', 'sic')
Out[89]:
shape: (11_478, 2)
industrysic
stri64
"Wholesale Trade"5080
"Wholesale Trade"5080
"Manufacturing"3661
"Manufacturing"3661
"Manufacturing"2834
……
"Retail Trade"5531
"Wholesale Trade"5040
"Manufacturing"2835
"Services"8051
"Services"7370
In [90]:
umap_color(
    train.select(topic_names).to_numpy(),
    train.get_column('industry').to_numpy(),
    title='Industries',
)
Out[90]:
<Axes: title={'center': 'Industries'}>
In [91]:
model = cluster.KMeans(n_clusters=9, n_init='auto')
kmeans = model.fit(train.select(topic_names).to_numpy())
train = train.with_columns(pl.Series('cluster', kmeans.labels_))
In [92]:
umap_color(
    train.select(topic_names).to_numpy(),
    train.get_column('cluster').cast(pl.String).to_numpy(),
    title='K-means clusters',
)
Out[92]:
<Axes: title={'center': 'K-means clusters'}>

Optimizing kmeans¶

The Gap statistic provides us a way to optimize the number of clusters for kmeans in a way that is unsupervised and statistically grounded. It is simulation based though (bootstrap-ish), which means it will take a while to run.

In [93]:
iterations = 10
ks = []
gaps = []
sks = []
inertias = []

k = 2
init_k = k
optimizing = True
while optimizing:
    model = cluster.KMeans(n_clusters=k, n_init='auto')
    kmeans = model.fit(train.select(topic_names).to_numpy())
    inertia_at_k = kmeans.inertia_
    # run 50 iterations to determine s.d.
    sim_inertias = []
    for i in range(0,iterations):
        model = cluster.KMeans(n_clusters=k, n_init='auto')
        # Simulate on random data
        kmeans = model.fit(np.random.rand(train.height, len(topic_names)))
        sim_inertias.append(kmeans.inertia_)
    l = np.mean(np.log(sim_inertias))
    gap = l - np.log(inertia_at_k)
    sk = (sum((1/iterations) * (np.log(sim_inertias) - l)**2)**(1/2)) * (1+1/iterations)**(1/2)
    ks.append(k)
    sks.append(sk)
    gaps.append(gap)
    inertias.append(inertia_at_k)
    if k > init_k:
        thresh = gap - gaps[-2] - sks[-2]
        if thresh < 0:
            print('Optimal k found: ' + str(k-1))
            print(thresh)
            break
        else:
            print('k={} Not optimal.  To optimize: {:.2f}'.format(k-1, thresh))
            k += 1
    else:
        k += 1
k=2 Not optimal.  To optimize: 0.13
k=3 Not optimal.  To optimize: 0.09
k=4 Not optimal.  To optimize: 0.09
k=5 Not optimal.  To optimize: 0.08
k=6 Not optimal.  To optimize: 0.04
k=7 Not optimal.  To optimize: 0.07
k=8 Not optimal.  To optimize: 0.07
k=9 Not optimal.  To optimize: 0.05
k=10 Not optimal.  To optimize: 0.05
k=11 Not optimal.  To optimize: 0.04
k=12 Not optimal.  To optimize: 0.03
k=13 Not optimal.  To optimize: 0.04
k=14 Not optimal.  To optimize: 0.00
k=15 Not optimal.  To optimize: 0.06
k=16 Not optimal.  To optimize: 0.03
Optimal k found: 17
-0.011332538640951958
In [94]:
print('Using optimal k = ' + str(k-1))
model = cluster.KMeans(n_clusters=k-1, n_init='auto')
kmeans = model.fit(train.select(topic_names).to_numpy())
train = train.with_columns(pl.Series('cluster_opt', kmeans.labels_))
Using optimal k = 17
In [107]:
tables = [
    train.filter(pl.col('cluster_opt') == cluster_id).select('industry').sample(n=10, seed=42)
    for cluster_id in (1, 2, 3)
]
with open('../Results/S2_cluster_tables.pkl', 'wb') as f:
    pickle.dump(tables, f, pickle.HIGHEST_PROTOCOL)
In [96]:
umap_color(
    train.select(topic_names).to_numpy(),
    train.get_column('cluster_opt').cast(pl.String).to_numpy(),
    title='Optimized K-means clusters',
)
Out[96]:
<Axes: title={'center': 'Optimized K-means clusters'}>

KNN for multiclass prediction¶

To set up this problem, we need to apply our industry classification to the testing data as well.

In [97]:
test = test.with_columns(industry_from_sic)

To set up a KNN model, we will use the KNeighborsClassifier() function from Scikit Learn. The most important parameter is n_neighbors, which corresponds to the k in KNN -- how many neighbors to use for classification. There are other parameters for the Scikit Learn implementation as well, which can be seen in the documentation.

In [98]:
knn = neighbors.KNeighborsClassifier(n_neighbors=5)
knn.fit(
    train.select(topic_names).to_numpy(),
    train.get_column('industry').to_numpy(),
)
Out[98]:
KNeighborsClassifier()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
n_neighbors n_neighbors: int, default=5

Number of neighbors to use by default for :meth:`kneighbors` queries.
5
weights weights: {'uniform', 'distance'}, callable or None, default='uniform'

Weight function used in prediction. Possible values:

- 'uniform' : uniform weights. All points in each neighborhood
are weighted equally.
- 'distance' : weight points by the inverse of their distance.
in this case, closer neighbors of a query point will have a
greater influence than neighbors which are further away.
- [callable] : a user-defined function which accepts an
array of distances, and returns an array of the same shape
containing the weights.

Refer to the example entitled
:ref:`sphx_glr_auto_examples_neighbors_plot_classification.py`
showing the impact of the `weights` parameter on the decision
boundary.
'uniform'
algorithm algorithm: {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'

Algorithm used to compute the nearest neighbors:

- 'ball_tree' will use :class:`BallTree`
- 'kd_tree' will use :class:`KDTree`
- 'brute' will use a brute-force search.
- 'auto' will attempt to decide the most appropriate algorithm
based on the values passed to :meth:`fit` method.

Note: fitting on sparse input will override the setting of
this parameter, using brute force.
'auto'
leaf_size leaf_size: int, default=30

Leaf size passed to BallTree or KDTree. This can affect the
speed of the construction and query, as well as the memory
required to store the tree. The optimal value depends on the
nature of the problem.
30
p p: float, default=2

Power parameter for the Minkowski metric. When p = 1, this is equivalent
to using manhattan_distance (l1), and euclidean_distance (l2) for p = 2.
For arbitrary p, minkowski_distance (l_p) is used. This parameter is expected
to be positive.
2
metric metric: str or callable, default='minkowski'

Metric to use for distance computation. Default is "minkowski", which
results in the standard Euclidean distance when p = 2. See the
documentation of `scipy.spatial.distance
<https://docs.scipy.org/doc/scipy/reference/spatial.distance.html>`_ and
the metrics listed in
:class:`~sklearn.metrics.pairwise.distance_metrics` for valid metric
values.

If metric is "precomputed", X is assumed to be a distance matrix and
must be square during fit. X may be a :term:`sparse graph`, in which
case only "nonzero" elements may be considered neighbors.

If metric is a callable function, it takes two arrays representing 1D
vectors as inputs and must return one value indicating the distance
between those vectors. This works for Scipy's metrics, but is less
efficient than passing the metric name as a string.
'minkowski'
metric_params metric_params: dict, default=None

Additional keyword arguments for the metric function.
None
n_jobs n_jobs: int, default=None

The number of parallel jobs to run for neighbors search.
``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
``-1`` means using all processors. See :term:`Glossary <n_jobs>`
for more details.
Doesn't affect :meth:`fit` method.
None
Fitted attributes
Name Type Value
classes_ classes_: array of shape (n_classes,)

Class labels known to the classifier
ndarray[object](9,) ['Agriculture','Construction','Manufacturing',...,'Services','Utilities', 'Wholesale Trade']
effective_metric_ effective_metric_: str or callble

The distance metric used. It will be same as the `metric` parameter
or a synonym of it, e.g. 'euclidean' if the `metric` parameter set to
'minkowski' and `p` parameter set to 2.
str 'eu...an'
effective_metric_params_ effective_metric_params_: dict

Additional keyword arguments for the metric function. For most metrics
will be same with `metric_params` parameter, but may also contain the
`p` parameter value if the `effective_metric_` attribute is set to
'minkowski'.
dict {}
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 31
n_samples_fit_ n_samples_fit_: int

Number of samples in the fitted data.
int 11478
outputs_2d_ outputs_2d_: bool

False when `y`'s shape is (n_samples, ) or (n_samples, 1) during fit
otherwise True.
bool False

To see how well the algorithm works, we can make multiclass predictions using .predict(). We can then pass the predictions to sklearn.metrics.accuracy_score() to get accuracy as a percentage.

In [99]:
in_pred = knn.predict(train.select(topic_names).to_numpy())
out_pred = knn.predict(test.select(topic_names).to_numpy())
In [100]:
print('In sample: {},\nOut of sample: {}'.format(
    metrics.accuracy_score(train.get_column('industry').to_numpy(), in_pred),
    metrics.accuracy_score(test.get_column('industry').to_numpy(), out_pred),
))
In sample: 0.9123540686530754,
Out of sample: 0.8597236981934112

To optimize parameters for KNN, we can use a Grid Search. It is fairly efficient to run, so we can do a full grid search.

In [101]:
knn_opt = neighbors.KNeighborsClassifier(algorithm='auto')
knn_param = {
    'n_neighbors': [1,2,3,4,5,6,7,8,9,10],
    'leaf_size': [10, 20, 30, 40],
    'p': [1, 2],
    'weights': ['uniform', 'distance'],
}
                   
# with GridSearch
grid_knn = model_selection.GridSearchCV(
    estimator=knn_opt,
    param_grid=knn_param,
    scoring = 'accuracy',
    n_jobs = -1,
    cv = 5
)
In [102]:
grid_knn.fit(
    train.select(topic_names).to_numpy(),
    train.get_column('industry').to_numpy(),
)
Out[102]:
GridSearchCV(cv=5, estimator=KNeighborsClassifier(), n_jobs=-1,
             param_grid={'leaf_size': [10, 20, 30, 40],
                         'n_neighbors': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
                         'p': [1, 2], 'weights': ['uniform', 'distance']},
             scoring='accuracy')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
estimator estimator: estimator object

This is assumed to implement the scikit-learn estimator interface.
Either estimator needs to provide a ``score`` function,
or ``scoring`` must be passed.
KNeighborsClassifier()
param_grid param_grid: dict or list of dictionaries

Dictionary with parameters names (`str`) as keys and lists of
parameter settings to try as values, or a list of such
dictionaries, in which case the grids spanned by each dictionary
in the list are explored. This enables searching over any sequence
of parameter settings.
{'leaf_size': [10, 20, ...], 'n_neighbors': [1, 2, ...], 'p': [1, 2], 'weights': ['uniform', 'distance']}
scoring scoring: str, callable, list, tuple or dict, default=None

Strategy to evaluate the performance of the cross-validated model on
the test set.

If `scoring` represents a single score, one can use:

- a single string (see :ref:`scoring_string_names`);
- a callable (see :ref:`scoring_callable`) that returns a single value;
- `None`, the `estimator`'s
:ref:`default evaluation criterion <scoring_api_overview>` is used.

If `scoring` represents multiple scores, one can use:

- a list or tuple of unique strings;
- a callable returning a dictionary where the keys are the metric
names and the values are the metric scores;
- a dictionary with metric names as keys and callables as values.

See :ref:`multimetric_grid_search` for an example.
'accuracy'
n_jobs n_jobs: int, default=None

Number of jobs to run in parallel.
``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
``-1`` means using all processors. See :term:`Glossary <n_jobs>`
for more details.

.. versionchanged:: v0.20
`n_jobs` default changed from 1 to None
-1
cv cv: int, cross-validation generator or an iterable, default=None

Determines the cross-validation splitting strategy.
Possible inputs for cv are:

- None, to use the default 5-fold cross validation,
- integer, to specify the number of folds in a `(Stratified)KFold`,
- :term:`CV splitter`,
- an iterable yielding (train, test) splits as arrays of indices.

For integer/None inputs, if the estimator is a classifier and ``y`` is
either binary or multiclass, :class:`StratifiedKFold` is used. In all
other cases, :class:`KFold` is used. These splitters are instantiated
with `shuffle=False` so the splits will be the same across calls.

Refer :ref:`User Guide <cross_validation>` for the various
cross-validation strategies that can be used here.

.. versionchanged:: 0.22
``cv`` default value if None changed from 3-fold to 5-fold.
5
refit refit: bool, str, or callable, default=True

Refit an estimator using the best found parameters on the whole
dataset.

For multiple metric evaluation, this needs to be a `str` denoting the
scorer that would be used to find the best parameters for refitting
the estimator at the end.

Where there are considerations other than maximum score in
choosing a best estimator, ``refit`` can be set to a function which
returns the selected ``best_index_`` given ``cv_results_``. In that
case, the ``best_estimator_`` and ``best_params_`` will be set
according to the returned ``best_index_`` while the ``best_score_``
attribute will not be available.

The refitted estimator is made available at the ``best_estimator_``
attribute and permits using ``predict`` directly on this
``GridSearchCV`` instance.

Also for multiple metric evaluation, the attributes ``best_index_``,
``best_score_`` and ``best_params_`` will only be available if
``refit`` is set and all of them will be determined w.r.t this specific
scorer.

See ``scoring`` parameter to know more about multiple metric
evaluation.

See :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_digits.py`
to see how to design a custom selection strategy using a callable
via `refit`.

See :ref:`this example
<sphx_glr_auto_examples_model_selection_plot_grid_search_refit_callable.py>`
for an example of how to use ``refit=callable`` to balance model
complexity and cross-validated score.

.. versionchanged:: 0.20
Support for callable added.
True
verbose verbose: int, default=0

Controls the verbosity of information printed during fitting, with higher
values yielding more detailed logging.

- 0 : no messages are printed;
- >=1 : summary of the total number of fits;
- >=2 : computation time for each fold and parameter candidate;
- >=3 : fold indices and scores;
- >=10 : parameter candidate indices and START messages before each fit.
0
pre_dispatch pre_dispatch: int, or str, default='2*n_jobs'

Controls the number of jobs that get dispatched during parallel
execution. Reducing this number can be useful to avoid an
explosion of memory consumption when more jobs get dispatched
than CPUs can process. This parameter can be:

- None, in which case all the jobs are immediately created and spawned. Use
this for lightweight and fast-running jobs, to avoid delays due to on-demand
spawning of the jobs
- An int, giving the exact number of total jobs that are spawned
- A str, giving an expression as a function of n_jobs, as in '2*n_jobs'
'2*n_jobs'
error_score error_score: 'raise' or numeric, default=np.nan

Value to assign to the score if an error occurs in estimator fitting.
If set to 'raise', the error is raised. If a numeric value is given,
FitFailedWarning is raised. This parameter does not affect the refit
step, which will always raise the error.
nan
return_train_score return_train_score: bool, default=False

If ``False``, the ``cv_results_`` attribute will not include training
scores.
Computing training scores is used to get insights on how different
parameter settings impact the overfitting/underfitting trade-off.
However computing the scores on the training set can be computationally
expensive and is not strictly required to select the parameters that
yield the best generalization performance.

.. versionadded:: 0.19

.. versionchanged:: 0.21
Default value was changed from ``True`` to ``False``
False
Fitted attributes
Name Type Value
best_estimator_ best_estimator_: estimator

Estimator that was chosen by the search, i.e. estimator
which gave highest score (or smallest loss if specified)
on the left out data. Not available if ``refit=False``.

See ``refit`` parameter for more information on allowed values.
KNeighborsClassifier KNeighborsCla...ts='distance')
best_index_ best_index_: int

The index (of the ``cv_results_`` arrays) which corresponds to the best
candidate parameter setting.

The dict at ``search.cv_results_['params'][search.best_index_]`` gives
the parameter setting for the best model, that gives the highest
mean score (``search.best_score_``).

For multi-metric evaluation, this is present only if ``refit`` is
specified.
int64 np.int64(37)
best_params_ best_params_: dict

Parameter setting that gave the best results on the hold out data.

For multi-metric evaluation, this is present only if ``refit`` is
specified.
dict {'le...ze': 10, 'n_...rs': 10, 'p': 1, 'weights': 'di...ce'}
best_score_ best_score_: float

Mean cross-validated score of the best_estimator

For multi-metric evaluation, this is present only if ``refit`` is
specified.

This attribute is not available if ``refit`` is a function.
float64 0.7808
classes_ classes_: ndarray of shape (n_classes,)

The classes labels. This is present only if ``refit`` is specified and
the underlying estimator is a classifier.
ndarray[object](9,) ['Agriculture','Construction','Manufacturing',...,'Services','Utilities', 'Wholesale Trade']
cv_results_ cv_results_: dict of numpy (masked) ndarrays

A dict with keys as column headers and values as columns, that can be
imported into a pandas ``DataFrame``.

For instance the below given table

+------------+-----------+------------+-----------------+---+---------+
|param_kernel|param_gamma|param_degree|split0_test_score|...|rank_t...|
+============+===========+============+=================+===+=========+
| 'poly' | -- | 2 | 0.80 |...| 2 |
+------------+-----------+------------+-----------------+---+---------+
| 'poly' | -- | 3 | 0.70 |...| 4 |
+------------+-----------+------------+-----------------+---+---------+
| 'rbf' | 0.1 | -- | 0.80 |...| 3 |
+------------+-----------+------------+-----------------+---+---------+
| 'rbf' | 0.2 | -- | 0.93 |...| 1 |
+------------+-----------+------------+-----------------+---+---------+

will be represented by a ``cv_results_`` dict of::

{
'param_kernel': masked_array(data = ['poly', 'poly', 'rbf', 'rbf'],
mask = [False False False False]...)
'param_gamma': masked_array(data = [-- -- 0.1 0.2],
mask = [ True True False False]...),
'param_degree': masked_array(data = [2.0 3.0 -- --],
mask = [False False True True]...),
'split0_test_score' : [0.80, 0.70, 0.80, 0.93],
'split1_test_score' : [0.82, 0.50, 0.70, 0.78],
'mean_test_score' : [0.81, 0.60, 0.75, 0.85],
'std_test_score' : [0.01, 0.10, 0.05, 0.08],
'rank_test_score' : [2, 4, 3, 1],
'split0_train_score' : [0.80, 0.92, 0.70, 0.93],
'split1_train_score' : [0.82, 0.55, 0.70, 0.87],
'mean_train_score' : [0.81, 0.74, 0.70, 0.90],
'std_train_score' : [0.01, 0.19, 0.00, 0.03],
'mean_fit_time' : [0.73, 0.63, 0.43, 0.49],
'std_fit_time' : [0.01, 0.02, 0.01, 0.01],
'mean_score_time' : [0.01, 0.06, 0.04, 0.04],
'std_score_time' : [0.00, 0.00, 0.00, 0.01],
'params' : [{'kernel': 'poly', 'degree': 2}, ...],
}

For an example of visualization and interpretation of GridSearch results,
see :ref:`sphx_glr_auto_examples_model_selection_plot_grid_search_stats.py`.

NOTE

The key ``'params'`` is used to store a list of parameter
settings dicts for all the parameter candidates.

The ``mean_fit_time``, ``std_fit_time``, ``mean_score_time`` and
``std_score_time`` are all in seconds.

For multi-metric evaluation, the scores for all the scorers are
available in the ``cv_results_`` dict at the keys ending with that
scorer's name (``'_<scorer_name>'``) instead of ``'_score'`` shown
above. ('split0_test_precision', 'mean_train_precision' etc.)
dict {'me...me': array([0.6320..., 0.02922502]), 'me...me': array([0.9570..., 0.16166697]), 'me...re': array([0.7264..., 0.7586689 ]), 'pa...ze': masked_array(..._value=999999), ...}
multimetric_ multimetric_: bool

Whether or not the scorers compute several metrics.
bool False
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`. Only defined if
`best_estimator_` is defined (see the documentation for the `refit`
parameter for more details) and that `best_estimator_` exposes
`n_features_in_` when fit.

.. versionadded:: 0.24
int 31
n_splits_ n_splits_: int

The number of cross-validation splits (folds/iterations).
int 5
refit_time_ refit_time_: float

Seconds used for refitting the best model on the whole dataset.

This is present only if ``refit`` is not False.

.. versionadded:: 0.20
float 0.01489
scorer_ scorer_: function or a dict

Scorer function used on the held out data to choose the best
parameters for the model.

For multi-metric evaluation, this attribute holds the validated
``scoring`` dict which maps the scorer key to the scorer callable.
_Scorer make_scorer(a...hod='predict')
KNeighborsClassifier(leaf_size=10, n_neighbors=10, p=1, weights='distance')
Parameters
n_neighbors n_neighbors: int, default=5

Number of neighbors to use by default for :meth:`kneighbors` queries.
10
weights weights: {'uniform', 'distance'}, callable or None, default='uniform'

Weight function used in prediction. Possible values:

- 'uniform' : uniform weights. All points in each neighborhood
are weighted equally.
- 'distance' : weight points by the inverse of their distance.
in this case, closer neighbors of a query point will have a
greater influence than neighbors which are further away.
- [callable] : a user-defined function which accepts an
array of distances, and returns an array of the same shape
containing the weights.

Refer to the example entitled
:ref:`sphx_glr_auto_examples_neighbors_plot_classification.py`
showing the impact of the `weights` parameter on the decision
boundary.
'distance'
leaf_size leaf_size: int, default=30

Leaf size passed to BallTree or KDTree. This can affect the
speed of the construction and query, as well as the memory
required to store the tree. The optimal value depends on the
nature of the problem.
10
p p: float, default=2

Power parameter for the Minkowski metric. When p = 1, this is equivalent
to using manhattan_distance (l1), and euclidean_distance (l2) for p = 2.
For arbitrary p, minkowski_distance (l_p) is used. This parameter is expected
to be positive.
1
algorithm algorithm: {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'

Algorithm used to compute the nearest neighbors:

- 'ball_tree' will use :class:`BallTree`
- 'kd_tree' will use :class:`KDTree`
- 'brute' will use a brute-force search.
- 'auto' will attempt to decide the most appropriate algorithm
based on the values passed to :meth:`fit` method.

Note: fitting on sparse input will override the setting of
this parameter, using brute force.
'auto'
metric metric: str or callable, default='minkowski'

Metric to use for distance computation. Default is "minkowski", which
results in the standard Euclidean distance when p = 2. See the
documentation of `scipy.spatial.distance
<https://docs.scipy.org/doc/scipy/reference/spatial.distance.html>`_ and
the metrics listed in
:class:`~sklearn.metrics.pairwise.distance_metrics` for valid metric
values.

If metric is "precomputed", X is assumed to be a distance matrix and
must be square during fit. X may be a :term:`sparse graph`, in which
case only "nonzero" elements may be considered neighbors.

If metric is a callable function, it takes two arrays representing 1D
vectors as inputs and must return one value indicating the distance
between those vectors. This works for Scipy's metrics, but is less
efficient than passing the metric name as a string.
'minkowski'
metric_params metric_params: dict, default=None

Additional keyword arguments for the metric function.
None
n_jobs n_jobs: int, default=None

The number of parallel jobs to run for neighbors search.
``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
``-1`` means using all processors. See :term:`Glossary <n_jobs>`
for more details.
Doesn't affect :meth:`fit` method.
None
Fitted attributes
Name Type Value
classes_ classes_: array of shape (n_classes,)

Class labels known to the classifier
ndarray[object](9,) ['Agriculture','Construction','Manufacturing',...,'Services','Utilities', 'Wholesale Trade']
effective_metric_ effective_metric_: str or callble

The distance metric used. It will be same as the `metric` parameter
or a synonym of it, e.g. 'euclidean' if the `metric` parameter set to
'minkowski' and `p` parameter set to 2.
str 'ma...an'
effective_metric_params_ effective_metric_params_: dict

Additional keyword arguments for the metric function. For most metrics
will be same with `metric_params` parameter, but may also contain the
`p` parameter value if the `effective_metric_` attribute is set to
'minkowski'.
dict {}
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 31
n_samples_fit_ n_samples_fit_: int

Number of samples in the fitted data.
int 11478
outputs_2d_ outputs_2d_: bool

False when `y`'s shape is (n_samples, ) or (n_samples, 1) during fit
otherwise True.
bool False
In [103]:
params = grid_knn.best_params_
print(params) 
{'leaf_size': 10, 'n_neighbors': 10, 'p': 1, 'weights': 'distance'}
In [104]:
knn2 = neighbors.KNeighborsClassifier(**params)
knn2.fit(
    train.select(topic_names).to_numpy(),
    train.get_column('industry').to_numpy(),
)
Out[104]:
KNeighborsClassifier(leaf_size=10, n_neighbors=10, p=1, weights='distance')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
n_neighbors n_neighbors: int, default=5

Number of neighbors to use by default for :meth:`kneighbors` queries.
10
weights weights: {'uniform', 'distance'}, callable or None, default='uniform'

Weight function used in prediction. Possible values:

- 'uniform' : uniform weights. All points in each neighborhood
are weighted equally.
- 'distance' : weight points by the inverse of their distance.
in this case, closer neighbors of a query point will have a
greater influence than neighbors which are further away.
- [callable] : a user-defined function which accepts an
array of distances, and returns an array of the same shape
containing the weights.

Refer to the example entitled
:ref:`sphx_glr_auto_examples_neighbors_plot_classification.py`
showing the impact of the `weights` parameter on the decision
boundary.
'distance'
leaf_size leaf_size: int, default=30

Leaf size passed to BallTree or KDTree. This can affect the
speed of the construction and query, as well as the memory
required to store the tree. The optimal value depends on the
nature of the problem.
10
p p: float, default=2

Power parameter for the Minkowski metric. When p = 1, this is equivalent
to using manhattan_distance (l1), and euclidean_distance (l2) for p = 2.
For arbitrary p, minkowski_distance (l_p) is used. This parameter is expected
to be positive.
1
algorithm algorithm: {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'

Algorithm used to compute the nearest neighbors:

- 'ball_tree' will use :class:`BallTree`
- 'kd_tree' will use :class:`KDTree`
- 'brute' will use a brute-force search.
- 'auto' will attempt to decide the most appropriate algorithm
based on the values passed to :meth:`fit` method.

Note: fitting on sparse input will override the setting of
this parameter, using brute force.
'auto'
metric metric: str or callable, default='minkowski'

Metric to use for distance computation. Default is "minkowski", which
results in the standard Euclidean distance when p = 2. See the
documentation of `scipy.spatial.distance
<https://docs.scipy.org/doc/scipy/reference/spatial.distance.html>`_ and
the metrics listed in
:class:`~sklearn.metrics.pairwise.distance_metrics` for valid metric
values.

If metric is "precomputed", X is assumed to be a distance matrix and
must be square during fit. X may be a :term:`sparse graph`, in which
case only "nonzero" elements may be considered neighbors.

If metric is a callable function, it takes two arrays representing 1D
vectors as inputs and must return one value indicating the distance
between those vectors. This works for Scipy's metrics, but is less
efficient than passing the metric name as a string.
'minkowski'
metric_params metric_params: dict, default=None

Additional keyword arguments for the metric function.
None
n_jobs n_jobs: int, default=None

The number of parallel jobs to run for neighbors search.
``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
``-1`` means using all processors. See :term:`Glossary <n_jobs>`
for more details.
Doesn't affect :meth:`fit` method.
None
Fitted attributes
Name Type Value
classes_ classes_: array of shape (n_classes,)

Class labels known to the classifier
ndarray[object](9,) ['Agriculture','Construction','Manufacturing',...,'Services','Utilities', 'Wholesale Trade']
effective_metric_ effective_metric_: str or callble

The distance metric used. It will be same as the `metric` parameter
or a synonym of it, e.g. 'euclidean' if the `metric` parameter set to
'minkowski' and `p` parameter set to 2.
str 'ma...an'
effective_metric_params_ effective_metric_params_: dict

Additional keyword arguments for the metric function. For most metrics
will be same with `metric_params` parameter, but may also contain the
`p` parameter value if the `effective_metric_` attribute is set to
'minkowski'.
dict {}
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 31
n_samples_fit_ n_samples_fit_: int

Number of samples in the fitted data.
int 11478
outputs_2d_ outputs_2d_: bool

False when `y`'s shape is (n_samples, ) or (n_samples, 1) during fit
otherwise True.
bool False
In [105]:
in_pred = knn2.predict(train.select(topic_names).to_numpy())
out_pred = knn2.predict(test.select(topic_names).to_numpy())
In [106]:
print('In sample: {},\nOut of sample: {}'.format(
    metrics.accuracy_score(train.get_column('industry').to_numpy(), in_pred),
    metrics.accuracy_score(test.get_column('industry').to_numpy(), out_pred),
))
In sample: 1.0,
Out of sample: 0.8852284803400637