def distance(point1, point2):
    """The distance between two arrays of numbers."""
    return np.sqrt(np.sum((point1 - point2)**2))

def all_distances(training, point):
    """The distance between p (an array of numbers) and the numbers in row i of attribute_table."""
    attributes = training.drop(columns=['Class'])
    def distance_from_point(row):
        return distance(point, np.array(row))
    return attributes.apply(distance_from_point, axis=1)

def table_with_distances(training, point):
    """A copy of the training table with the distance from each row to array p."""
    training1 = training.copy()
    training1['Distance'] = all_distances(training1, point)
    return training1


def closest(training, point, k):
    """A table containing the k closest rows in the training table to array p."""
    with_dists = table_with_distances(training, point)
    sorted_by_distance = with_dists.sort_values(by=['Distance'])
    topk = sorted_by_distance.take(np.arange(k))
    return topk
def classify_grid(training, test, k):
    ckd_new1 = pd.DataFrame(columns=['Hemoglobin', 'Glucose', 'Class', 'Distance'])
    empty = np.array([])
    for i in range(len(test)):
        ckd_new2 = closest(training, np.array([test.iloc[i]]), k)
        ones = len(ckd_new2[ckd_new2['Class'] == '1'])
        zeros = len(ckd_new2[ckd_new2['Class'] == '0'])
        if ones > zeros:
            empty = np.append(empty, 1)
        else:
            empty = np.append(empty, 0)
    return empty
ckd = pd.read_csv(path_data + 'ckd.csv')
ckd.rename(columns={'Blood Glucose Random':'Glucose'}, inplace=True)


ckd_Standard_Unit = pd.DataFrame({'Hemoglobin':standard_units(ckd['Hemoglobin']), 
                       'Glucose':standard_units(ckd['Glucose']), 
                       'White Blood Cell Count':standard_units(ckd['White Blood Cell Count']), 
                       'Class':ckd['Class'].astype(str)})

color_table = pd.DataFrame(
    {'Class':np.array([1, 0]),
    'Color':np.array(['darkblue', 'gold'])}, index=np.array([1,0]))
     
color_table['Class'] = color_table['Class'].astype(str)

ckd_combined = pd.merge(ckd_Standard_Unit, color_table, on='Class')

ckd_combined
Hemoglobin Glucose White Blood Cell Count Class Color
0 -0.865744 -0.221549 -0.569768 1 darkblue
1 -1.457446 -0.947597 1.162684 1 darkblue
2 -1.004968 3.841231 -1.275582 1 darkblue
3 -2.814879 0.396364 0.809777 1 darkblue
4 -2.083954 0.643529 0.232293 1 darkblue
... ... ... ... ... ...
153 0.700526 0.133751 -0.569768 0 gold
154 0.978974 -0.870358 -0.216861 0 gold
155 0.735332 -0.484162 -0.601850 0 gold
156 0.178436 -0.267893 -0.409356 0 gold
157 0.735332 -0.005280 -0.537686 0 gold

158 rows × 5 columns

2. Training and Testing

How good is our nearest neighbor classifier? To answer this we’ll need to find out how frequently our classifications are correct. If a patient has chronic kidney disease, how likely is our classifier to pick that up?

If the patient is in our training set, we can find out immediately. We already know what class the patient is in. So we can just compare our prediction and the patient’s true class.

But the point of the classifier is to make predictions for new patients not in our training set. We don’t know what class these patients are in but we can make a prediction based on our classifier. How to find out whether the prediction is correct?

One way is to wait for further medical tests on the patient and then check whether or not our prediction agrees with the test results. With that approach, by the time we can say how likely our prediction is to be accurate, it is no longer useful for helping the patient.

Instead, we will try our classifier on some patients whose true classes are known. Then, we will compute the proportion of the time our classifier was correct. This proportion will serve as an estimate of the proportion of all new patients whose class our classifier will accurately predict. This is called testing.

2.1. Overly Optimistic “Testing”

The training set offers a very tempting set of patients on whom to test out our classifier, because we know the class of each patient in the training set.

But let’s be careful … there will be pitfalls ahead if we take this path. An example will show us why.

Suppose we use a 1-nearest neighbor classifier to predict whether a patient has chronic kidney disease, based on glucose and white blood cell count.

glucose_color_darkblue = ckd_combined[ckd_combined['Color'] == 'darkblue']
glucose_color_gold = ckd_combined[ckd_combined['Color'] == 'gold']

fig, ax = plt.subplots(figsize=(7,6))

ax.scatter(glucose_color_darkblue['White Blood Cell Count'], 
           glucose_color_darkblue['Glucose'],  
           label='Color=darkblue', 
           color='darkblue')

ax.scatter(glucose_color_gold['White Blood Cell Count'], 
           glucose_color_gold['Glucose'],  
           label='Color=gold', 
           color='gold')

x_label = 'White Blood Cell Count'

y_label = 'Glucose'

y_vals = ax.get_yticks()

plt.ylabel(y_label)

ax.legend(bbox_to_anchor=(1.04,1), loc="upper left")

plt.xlabel(x_label)

plt.show()
../../_images/Training_and_Testing_7_0.png

Earlier, we said that we expect to get some classifications wrong, because there’s some intermingling of blue and gold points in the lower-left.

But what about the points in the training set, that is, the points already on the scatter? Will we ever mis-classify them?

The answer is no. Remember that 1-nearest neighbor classification looks for the point in the training set that is nearest to the point being classified. Well, if the point being classified is already in the training set, then its nearest neighbor in the training set is itself! And therefore it will be classified as its own color, which will be correct because each point in the training set is already correctly colored.

In other words, if we use our training set to “test” our 1-nearest neighbor classifier, the classifier will pass the test 100% of the time.

Mission accomplished. What a great classifier!

No, not so much. A new point in the lower-left might easily be mis-classified, as we noted earlier. “100% accuracy” was a nice dream while it lasted.

The lesson of this example is not to use the training set to test a classifier that is based on it.

2.1.1. Generating a Test Set

In earlier chapters, we saw that random sampling could be used to estimate the proportion of individuals in a population that met some criterion. Unfortunately, we have just seen that the training set is not like a random sample from the population of all patients, in one important respect: Our classifier guesses correctly for a higher proportion of individuals in the training set than it does for individuals in the population.

When we computed confidence intervals for numerical parameters, we wanted to have many new random samples from a population, but we only had access to a single sample. We solved that problem by taking bootstrap resamples from our sample.

We will use an analogous idea to test our classifier. We will create two samples out of the original training set, use one of the samples as our training set, and the other one for testing.

So we will have three groups of individuals:

  • a training set on which we can do any amount of exploration to build our classifier;

  • a separate testing set on which to try out our classifier and see what fraction of times it classifies correctly;

  • the underlying population of individuals for whom we don’t know the true classes; the hope is that our classifier will succeed about as well for these individuals as it did for our testing set.

How to generate the training and testing sets? You’ve guessed it – we’ll select at random.

There are 158 individuals in ckd. Let’s use a random half of them for training and the other half for testing. To do this, we’ll shuffle all the rows, take the first 79 as the training set, and the remaining 79 for testing.

shuffled_ckd = ckd_combined.sample(len(ckd_combined), replace=False)

shuffled_ckd

training = shuffled_ckd.take(np.arange(79))

testing = shuffled_ckd.take(np.arange(79, 158))

Now let’s construct our classifier based on the points in the training sample:

training_darkblue = training[training['Color'] == 'darkblue']
training_gold = training[training['Color'] == 'gold']

fig, ax = plt.subplots(figsize=(7,6))

ax.scatter(training_darkblue['White Blood Cell Count'], 
           training_darkblue['Glucose'],  
           label='Color=darkblue', 
           color='darkblue')

ax.scatter(training_gold['White Blood Cell Count'], 
           training_gold['Glucose'],  
           label='Color=gold', 
           color='gold')

x_label = 'White Blood Cell Count'

y_label = 'Glucose'

y_vals = ax.get_yticks()

plt.ylabel(y_label)

ax.legend(bbox_to_anchor=(1.04,1), loc="upper left")

plt.xlabel(x_label)

plt.xlim(-2, 6)
plt.ylim(-2, 6);

plt.show()
../../_images/Training_and_Testing_13_0.png

We get the following classification regions and decision boundary:

len(test_grid)
1089
array([1., 1., 1., 0., 0.])
len(c_training)
1089
test_grid['Class'] = c_training.astype(int)
test_grid['Class'] = test_grid['Class'].astype(str)
test_grid.head(5)
Glucose White Blood Cell Count Class
0 -2.0 -2.00 1
1 -2.0 -1.75 1
2 -2.0 -1.50 1
3 -2.0 -1.25 0
4 -2.0 -1.00 0

2.2. Join df’s

Glucose White Blood Cell Count Class Color
0 -2.0 -2.00 1 darkblue
1 -2.0 -1.75 1 darkblue
2 -2.0 -1.50 1 darkblue
3 -2.0 1.75 1 darkblue
4 -2.0 2.00 1 darkblue
... ... ... ... ...
1084 0.5 -0.50 0 gold
1085 0.5 -0.25 0 gold
1086 0.5 0.75 0 gold
1087 0.5 1.00 0 gold
1088 0.5 1.25 0 gold

1089 rows × 4 columns

training_darkblue = test_grid_combined[test_grid_combined['Color'] == 'darkblue']
training_gold = test_grid_combined[test_grid_combined['Color'] == 'gold']

fig, ax = plt.subplots(figsize=(7,6))

ax.scatter(training_darkblue['White Blood Cell Count'], 
           training_darkblue['Glucose'],  
           label='Color=darkblue', 
           alpha=0.4, 
           color='darkblue', 
           s=30)

ax.scatter(training_gold['White Blood Cell Count'], 
           training_gold['Glucose'],  
           label='Color=gold', 
           alpha=0.4, 
           s=30, 
           color='gold')

x_label = 'White Blood Cell Count'

y_label = 'Glucose'

y_vals = ax.get_yticks()

plt.ylabel(y_label)

ax.legend(bbox_to_anchor=(1.04,1), loc="upper left")

plt.xlabel(x_label)

plt.xlim(-2, 6)
plt.ylim(-2, 6);

plt.show()
../../_images/Training_and_Testing_22_0.png

Place the test data on this graph and you can see at once that while the classifier got almost all the points right, there are some mistakes. For example, some blue points of the test set fall in the gold region of the classifier.

../../_images/Training_and_Testing_24_0.png

Some errors notwithstanding, it looks like the classifier does fairly well on the test set. Assuming that the original sample was drawn randomly from the underlying population, the hope is that the classifier will perform with similar accuracy on the overall population, since the test set was chosen randomly from the original sample.