90-Minute Main Lesson: Distance → Neighbors → Out-of-Sample Choice
Objectives: Calculate and interpret distances; explain training-only scaling; choose \(k\) with time-based validation using Euclidean distance and uniform voting; diagnose distance concentration.
learning path:
prerequisites and distance 15 min → scaling and \(k\) 20 min → bias–variance and high-dimensional distance checks 20 min → Fuyao Glass main case 25 min → transfer and feedback 10 min.
Weighted KNN, regression, and KD-trees are extensions.
Answer first: What are the Euclidean and Manhattan distances from \((0,0)\) to \((3,4)\)?
Feedback: They are \(5\) and \(7\). Choose a metric from the task’s meaning of similarity, not by habit.
Today’s Agenda: Credit Risk Assessment
Imagine you are a credit manager, and a new customer applies for a loan.
Your core task is:
Assess Risk: How likely is this customer to default in the future?
Make a Decision: Should you approve or reject the loan?
This is a classic business classification problem.
The Data-Driven Solution
You are not guessing blindly. Your bank possesses valuable historical data.
Existing Data: Complete records of thousands of past customers.
Features: Income, debt, age, occupation…
Outcome: Paid on time ✅ or Defaulted ❌
Our Approach: Use this data to profile the new customer’s risk. This is where the K-Nearest Neighbors (KNN) algorithm comes into play.
KNN’s Core Idea: ‘Birds of a feather flock together’
The philosophy of the KNN algorithm is very simple: To determine the class of a new sample, just look at which class its nearest ‘neighbors’ belong to.
It’s like trying to figure out a stranger’s hobbies; the easiest way is to see what kind of people their closest friends are.
The Business Analogy for KNN
Machine Learning Concept
Business Analogy (Credit Assessment)
New Sample (Unknown Class)
A new loan applicant
Neighbors (Known Class)
Historical customers in the database who are most similar to the new applicant
Class
‘Will Default’ or ‘Will Not Default’
Measure of ‘Closeness’
Similarity of customer features (e.g., income, age, debt)
The value ‘K’
How many similar customers do we consult to make a decision?
Our Learning Roadmap Today
Core | Define the neighbor rule: how distance, \(k\), and voting jointly produce a prediction.
Core | Scale before searching: fit preprocessing on training data only and explain why scale changes the neighbors.
Core | Select the model chronologically: choose in advance Euclidean distance and uniform voting, then choose \(k\) with training-only ordered folds.
Core | evaluate on the final test period: after choosing the model on validation data, report prevalence, ROC-AUC, AP, and a confusion matrix once and state its limits.
Extension | Mechanisms and scale: weighted KNN, regression, KD/Ball Trees, and high-dimensional probability derivations; Core retains one normalized-distance intuition and its algorithmic implication.
4.1 Formal Definition of the k-NN Rule
Given a sample \(x_n\) to be classified, the KNN decision process involves two steps:
Find Neighbors: In the entire dataset, identify the \(k\) samples that are closest in distance to \(x_n\).
Majority Vote: Among these \(k\) neighbors, use a majority vote to assign the most frequent class as the predicted class for \(x_n\).
Here, \(I(\cdot)\) is an indicator function. It is 1 if the class \(y_i\) of neighbor \(i\) is equal to class \(c\), and 0 otherwise. This formula is essentially counting votes.
Understanding the Three Key Elements of KNN
To successfully apply the KNN algorithm, we must clearly define three core components. They are the building blocks of any KNN model.
Distance Metric
How do we quantify the ‘similarity’ between samples?
The Choice of \(k\)
How many neighbors should we look at? One or ten?
Decision Rule
How do we make the final judgment based on the neighbors? (e.g., simple majority vote)
Key Element 1: The Distance Metric
How do we define ‘close’? This depends on the distance metric we choose.
Key Element 2: The Choice of k
How many neighbors should we consult? This is the critical dial for model complexity.
Key Element 3: The Decision Rule
How do we aggregate the ‘opinions’ of the neighbors? The most common method is ‘majority rules’.
Deep Dive: Distance Metrics
The similarity between samples is measured by a distance function. For two \(d\)-dimensional samples \(x_i\) and \(x_j\), common distance metrics include:
Euclidean Distance: The familiar straight-line distance, and the most commonly used in KNN.
Euclidean distance is ‘as the crow flies’, while Manhattan distance is ‘walking the blocks’.
Critical Prerequisite: Data Standardization
Important Note: Distance metrics are highly sensitive to the scale of features.
The Problem: If you directly calculate distance using ‘Annual Income’ (in thousands) and ‘Age’ (in years), the large numerical values of income will completely dominate the calculation, making the effect of age negligible.
The Solution: Data Standardization. Transform all features to a similar scale (e.g., a mean of 0 and a standard deviation of 1).
Before applying KNN, data standardization is almost always a mandatory step.
Why Standardization Is Crucial
Deep Dive: The Choice of k
The choice of \(k\) directly impacts the model’s performance and represents a Bias-Variance Tradeoff.
Small \(k\) values:
The model is more complex, with a more irregular decision boundary.
It is susceptible to noise, leading to Overfitting.
Low Bias, High Variance.
Large \(k\) values:
The model is simpler, with a smoother decision boundary.
It may ignore local, subtle structures in the data, leading to Underfitting.
High Bias, Low Variance.
Small k: High Flexibility, Prone to Overfitting
When k is small (e.g., k=1), the model’s decision boundary becomes highly convoluted, trying to perfectly match every training point, including noise.
Large k: High Stability, Prone to Underfitting
When k is large (e.g., k=N), the model ignores local data structures, and the decision boundary becomes overly simple, failing to capture class differences.
Finding the Optimal k Value
So, how do we find that ‘just right’ value of \(k\)?
It’s not a guessing game: We can’t choose based on intuition.
A systematic approach:
We typically use Cross-Validation.
We split the data into several folds, iteratively use one part as a ‘mini test set’ to evaluate the performance of different \(k\) values, and finally select the \(k\) that performs the best on average.
We will demonstrate this process in the upcoming Python practical session.
Let’s Visualize KNN with an Example
Suppose we have a dataset of tumors, classified as ‘Benign’ or ‘Malignant’. A new patient arrives (represented by ?), and we need to determine if their tumor is benign or malignant.
1-NN is the simplest form of KNN. We only look at the single closest neighbor.
In this example, the sample closest to the new patient belongs to the ‘Malignant’ class, so 1-NN would classify it as Malignant. This could be a flawed judgment influenced by noise.
Closer neighbors have a ‘louder voice’, while distant neighbors have a ‘quieter voice’.
Implementing Weighted KNN in scikit-learn
In scikit-learn, weights='distance' gives closer neighbors more voting power. This slide shows an executable Extension configuration; the Core Practice chooses in advance uniform voting and never uses test results to choose weighting.
Code
from sklearn.neighbors import KNeighborsClassifier # Importing a Weighted Nearest Neighbor Classifierweighted_knn_example = KNeighborsClassifier(n_neighbors=5, weights='distance') # Set up the weighted-neighbor model before fitting itweighted_knn_example.get_params()["weights"] # Check the after-class Extension configuration
'distance'
Any after-class weighting comparison requires a new training-only validation and a separate untouched test period; the current main lesson does not compare that rule.
4.3 KNN for More Than Just Classification: Regression
The core idea of KNN also applies to regression problems, where the goal is to predict a continuous value (like a stock price or house value).
Classification
Goal: Predict a discrete class
Method: Neighbors vote
Example: Is an email ‘Spam’ or ‘Not Spam’?
Regression
Goal: Predict a continuous value
Method: Neighbors are averaged
Example: Predict the sale price of a house
The Mathematics of KNN Regression
For a new sample, the predicted value from a KNN regression model is the average (or weighted average) of the target values of its \(k\) nearest neighbors.
Here, \(y_i\) is the actual numerical value of the \(i\)-th neighbor.
4.4 Real-World Challenges and Solutions
So far, we’ve dealt with idealized numerical data. But in the business world, data is often more complex.
Challenge 1: How do we handle non-numerical features (like ‘City’ or ‘Product Category’)?
Challenge 2: What if the dataset is massive (millions of records)? KNN predictions become extremely slow.
Challenge 1: Handling Categorical Features
The Problem: How do you calculate the ‘distance’ between samples that include text features like ‘Beijing’ and ‘Shanghai’?
Solutions:
One-Hot Encoding: Convert the categorical feature into multiple binary (0/1) features. This is the most common approach.
Use a specific distance metric: For example, the Hamming Distance, which counts the number of positions at which two strings of equal length are different.
Visualizing One-Hot Encoding
Challenge 2: Data Scale and Prediction Speed
A major drawback of KNN is its high computational cost.
Training Phase: Extremely fast. There’s almost no computation, just storing the data.
Prediction Phase: Extremely slow. For every new sample to be predicted, it must calculate its distance to every single training sample. If the training set has millions of records, this is unacceptable.
The Question: How can we speed up the process of finding nearest neighbors without sacrificing too much accuracy?
The Core Idea: Avoid ‘Brute-Force Search’
Directly calculating the distance from a new sample to all training points is known as a brute-force search.
The core idea behind acceleration strategies is: build intelligent data structures to quickly eliminate large numbers of samples that cannot possibly be the nearest neighbors.
Figure 4: How a KD-Tree recursively partitions a 2D space
Core Diagnosis: Vanishing Neighbor Contrast
Core idea: as \(d\) grows, distances concentrate; neighbors blur and KD-Tree pruning approaches brute force.
Check: nearest/farthest distance \(\to 1\) signals less stable voting or weighting—a risk, not a universal KNN failure theorem.
learning path: Core \(\to\)Practice; the optional probability After the optional topics to that same Practice.
Extension: The Curse of Dimensionality
After the optional topic: after this branch, continue to the main case without replaying the completed Core diagnosis.
The Core Question: What is ‘Distance’ in High-Dimensional Space?
Optional topic: before entering this branch, the 90-minute Core has completed the minimal relative-distance diagnosis. The boundary-layer, fourth-moment, and sub-gaussian derivations below are after-class work and return only forward to the still-pending main case; probability theory is not assumed in Core.
In high-dimensional spaces, the geometric intuition we’ve built from our low-dimensional (2D or 3D) experience often fails.
Our Intuition: Points are either near or far; you can always find a ‘nearest neighbor’.
High-Dimensional Reality: As dimensions increase, the Euclidean distances between pairs of points tend to become very similar to each other.
This lecture aims to reveal this counter-intuitive phenomenon—the failure of distance metrics—through intuitive examples and mathematical derivation.
A Simplified Model: A Corner Cell and a Boundary Layer
Let’s begin with a simple thought experiment: slicing a unit hypercube \([0, 1]^d\) at the midpoint \(0.5\) along each dimension.
This partitions the entire space into \(2^d\) identical ‘small hypercubes’.
The cell \([0,1/2]^d\) is one corner cell, not the entire interior of the cube. This partition illustrates shrinking corner volume; a genuine boundary layer needs a separate definition.
Low-Dimensional Intuition: Corner Partitions for d=1 and d=2
In low dimensions we can see the origin corner cell and its complement directly. This compares one corner with the rest of the cube, not an interior core with a boundary shell.
Figure 5: In \(d=2\), out of the \(2^2=4\) small squares, only one, \([0, 1/2]\times[0, 1/2]\), is near the origin.
High Dimensions: Corner Cells and Boundary Layers
As \(d\) increases, the origin corner cell \([0,1/2]^d\) has volume \(2^{-d}\to0\); its complement is not the same object as a boundary layer.
For fixed \(0<\varepsilon<1/2\), \(P(X\in B_\varepsilon)=1-(1-2\varepsilon)^d\to1\): uniform mass lies near at least one face.
From Discrete to Continuous: Distance Concentration Under a Uniform Distribution
The slicing model was a simplification. Now consider a more general case: \(n\) data points \(X_1, \ldots, X_n\) are independently and identically distributed in the unit hypercube \([0, 1]^d\).
Conditional finding
if sample size \(n\) is fixed and coordinates are i.i.d.
\(U(0,1)\), the finite set of Euclidean pair distances shares the same first-order scale, so its max–min relative gap converges to zero in probability.
Thus relative, not absolute, variation shrinks, and \(\lVert X_i-X_j\rVert_2/\sqrt d\xrightarrow{p}1/\sqrt6\).
Absolute variance does not vanish; fixed \(n\) extends the result to the finite set of pairs.
Conceptual Visualization: Concentration of the Distance Distribution
This phenomenon can be understood visually through the changing distribution of distances.
Figure 6: Interpret the sketch as the normalized distribution of \(D/\sqrt d\) or \(S/d\): its relative width shrinks. It does not claim that the unnormalized distance has vanishing absolute width.
Generalization: Concentration of the Norm
Related concentration results extend beyond the uniform cube, but they require explicit distribution and dependence assumptions.
Intuitive theorem: a centered isotropic sub-gaussian vector \(X\in\mathbb R^d\) has \(\lVert X\rVert_2\) concentrated on the \(\sqrt d\) scale.
Required qualification: the constants and tail bounds are controlled by the explicitly stated sub-gaussian parameters.
A stronger result (Vershynin, 2018) shows that for sub-gaussian vectors, the probability of deviating from \(\sqrt{d}\) decays exponentially with the dimension \(d\).
This means that in high-dimensional space, the length of a random vector is almost a deterministic, not random, quantity.
The failure of distance metrics poses a severe challenge to many algorithms that rely on distance calculations, especially k-Nearest Neighbors (k-NN).
Goal of k-NN: To find the k neighbors in the feature space that are ‘most similar’ to a query point.
High-Dimensional Dilemma: If the distances from a query point to all its neighbors are nearly equal, there’s no meaningful difference between the ‘nearest’ and ‘farthest’ neighbors.
Result: Without low-dimensional structure or useful signal, neighbor ranks can lose contrast and predictions can become unstable; this is not a theorem that k-NN is meaningless in every high-dimensional task.
Summary
High-Dimensional Geometry is Counter-intuitive: We cannot directly extend our low-dimensional intuitions to high-dimensional spaces.
Define the Boundary Layer: In a uniform cube, a fixed-width boundary layer has volume share approaching one; the complement of one corner is not that layer.
Relative Distance Concentration: Under stated distributions and fixed sample size, normalized distances have shrinking relative variation.
Diagnose Algorithms: Concentration can weaken contrast for k-NN or clustering, but the effect depends on scaling, structure, metric, and sample growth.
After the optional topic
continue to the main case; do not skip training-only selection and the held-out test check.
Objective Retrieval Before the Main Case
Retrieve the opening objectives: calculate Euclidean/Manhattan distance, explain why scaling changes neighbors, choose \(k\) with time-respecting validation, and diagnose distance concentration.
Retrieval prompt: What are the Euclidean and Manhattan distances from \((0,0)\) to \((3,4)\)?
Answer
Euclidean is \(\sqrt{3^2+4^2}=5\); Manhattan is \(|3|+|4|=7\). The task’s meaning of similarity determines the metric.
Formative Check 1: Scale Before or After Splitting?
Should the scaler be fit on all observations or the training period only?
Answer
Fit on training only, then apply unchanged to validation/test. Full-sample moments contain future information and leak through preprocessing.
Main Case: Fuyao Glass KNN Decline Classification
Data used in this example:data/stock/stock_price_pre_adjusted.h5; key=data; Fuyao Glass 600660.XSHG; 2018–2024; predict next-day decline from day return, five-day return, and five-day volatility; chronological 80/20 split.
from pathlib import Path # Locate the downloaded data fileimport pandas as pd # Read local quotes into a tablefrom sklearn.preprocessing import StandardScaler # Define Comparable Distance with Training Period Scalefrom sklearn.neighbors import KNeighborsClassifier # Use nearest neighbor majority voting to complete classificationfrom sklearn.metrics import average_precision_score, balanced_accuracy_score, confusion_matrix, roc_auc_score # Prepare ranking and operating-point evidence for the fixed check# Public download: https://assets.qiufei.site/data/stock/stock_price_pre_adjusted.h5# After downloading, change the next line to the file's actual location on your device.# Course-relative option: Path("data/stock/stock_price_pre_adjusted.h5")# Windows: Path(r"C:\qiufei\data\stock\stock_price_pre_adjusted.h5")# macOS: Path("/Users/your_name/data/stock/stock_price_pre_adjusted.h5")# Linux: Path("/home/your_name/data/stock/stock_price_pre_adjusted.h5")price_path = Path("/home/ubuntu/r2_data_mount/data/stock/stock_price_pre_adjusted.h5")price_rows = pd.read_hdf(price_path, key='data', where=['order_book_id=="600660.XSHG"', 'date>=Timestamp("2018-01-01")', 'date<=Timestamp("2024-12-31")'], columns=['close']) # Only Load Target Companies and Closing Pricesknn_frame = price_rows.reset_index().sort_values('date') # Build Forecast Order by Trading Dayknn_frame['return_t'] = knn_frame['close'].pct_change() # Compute one-day returnsknn_frame['return_5d_t'] = knn_frame['close'].pct_change(5) # Compute cumulative five-day returnsknn_frame['volatility_5d_t'] = knn_frame['return_t'].rolling(5).std() # Compute rolling five-day volatilityknn_frame['future_return_t1'] = knn_frame['return_t'].shift(-1) # Retain continuous future returns before label construction to identify an unknown terminal labelknn_frame['target_date_t1'] = knn_frame['date'].shift(-1) # Preserve the label-realization date for boundary purgingknn_frame = knn_frame.dropna() # Drop rows with rolling-window missing values or unknown future returns before label construction
Code
knn_frame['down_t1'] = (knn_frame['future_return_t1'] <0).astype(int) # Create classes only for observed future returnsassert knn_frame['future_return_t1'].notna().all() and knn_frame['date'].max() < price_rows.reset_index()['date'].max() # Verify that the last feature date is earlier than the last label realization datesplit_row =int(len(knn_frame) *0.8) # Fixed First Eighty Percent as Training Periodtest_start_date = knn_frame.iloc[split_row]['date']train_rows = knn_frame[(knn_frame['date'] < test_start_date) & (knn_frame['target_date_t1'] < test_start_date)] # Purge training labels realized in testtest_rows = knn_frame[knn_frame['date'] >= test_start_date]assert train_rows['target_date_t1'].max() < test_rows['date'].min() # Verify the label-realization boundaryfeature_names = ['return_t', 'return_5d_t', 'volatility_5d_t'] # Define the three semantic dimensions of the distance spacepd.Series({'train_end': train_rows['date'].max(), 'test_start': test_rows['date'].min()}) # Verify only the time boundary before selection; do not read test-label summaries
As \(k\) grows from 1 to 51, what usually happens to the boundary, training error, and variance?
Answer
The boundary smooths, training error usually rises, variance falls, and bias rises. Select \(k\) inside training validation, never from final-test results.
Step-by-Step Exercise: Choose \(k\)
Task:
Choose in advance Euclidean distance and uniform voting.
With five expanding training windows, compare \(k\in\{5,15,31\}\) by balanced accuracy.
After choosing \(k\), report test prevalence, ROC-AUC, AP, and the confusion matrix once.
Complete solution:
Fit a fresh scaler and uniform-vote Euclidean KNN inside each window, record validation scores, and select the highest mean.
Refit on the full training period and open the fixed test once.
Distance weighting and alternative metrics are Extension; this exercise selects neither them nor a separate probability threshold.
Code
from sklearn.model_selection import TimeSeriesSplit # Keep training earlier than validation with an extended windowvalidation_rows = [] # Collect Equilibrium Accuracy Rate for Each Window and Candidate Kfor fold_id, (fit_index, validation_index) inenumerate(TimeSeriesSplit(n_splits=5).split(train_rows), start=1): # Generate Five Time Ordered Windows fold_train = train_rows.iloc[fit_index] # Obtain Historical Training Segments for Current Window fold_validation = train_rows.iloc[validation_index] # Obtain Validation Segments Immediately Following fold_train = fold_train[fold_train['target_date_t1'] < fold_validation['date'].min()] # Purge labels realized in the validation foldassert fold_train['target_date_t1'].max() < fold_validation['date'].min() # Verify each fold's label boundary fold_scaler = StandardScaler().fit(fold_train[feature_names]) # estimate scale only on current training segmentfor neighbor_count in [5, 15, 31]: # Compare three pre-declared candidate values fold_model = KNeighborsClassifier(n_neighbors=neighbor_count, weights='uniform', metric='euclidean').fit(fold_scaler.transform(fold_train[feature_names]), fold_train['down_t1']) # Compare candidate k values with the same Euclidean distance and uniform voting fold_prediction = fold_model.predict(fold_scaler.transform(fold_validation[feature_names])) # Generate Forecast in Future Validation Segment validation_rows.append({'fold': fold_id, 'k': neighbor_count, 'balanced_accuracy': balanced_accuracy_score(fold_validation['down_t1'], fold_prediction)}) # Save Reviewable Breakdown Resultsvalidation_table = pd.DataFrame(validation_rows) # Organize step-by-step validation recordsvalidation_summary = validation_table.groupby('k', as_index=False)['balanced_accuracy'].mean().sort_values('balanced_accuracy', ascending=False) # Summarize average validation evidenceselected_k =int(validation_summary.iloc[0]['k']) # Select Number of Neighbors by Training Period Onlyfinal_scaler = StandardScaler().fit(train_rows[feature_names]) # Re-estimate the scale with the full training period after selectionfinal_knn = KNeighborsClassifier(n_neighbors=selected_k, weights='uniform', metric='euclidean').fit(final_scaler.transform(train_rows[feature_names]), train_rows['down_t1']) # Refit the fixed specification on the full training periodfinal_test_prediction = final_knn.predict(final_scaler.transform(test_rows[feature_names]))final_test_probability = final_knn.predict_proba(final_scaler.transform(test_rows[feature_names]))[:, 1] # Generate ranking scores from the same fixed modelfinal_test_confusion = confusion_matrix(test_rows['down_t1'], final_test_prediction) # Record majority-vote operating-point countstest_summary = pd.Series({'selected_k': selected_k, 'prevalence': test_rows['down_t1'].mean(), 'roc_auc': roc_auc_score(test_rows['down_t1'], final_test_probability), 'AP': average_precision_score(test_rows['down_t1'], final_test_probability), 'balanced_accuracy': balanced_accuracy_score(test_rows['down_t1'], final_test_prediction), 'TN': final_test_confusion[0, 0], 'FP': final_test_confusion[0, 1], 'FN': final_test_confusion[1, 0], 'TP': final_test_confusion[1, 1]}) # Summarize the one held-out test checkdisplay(validation_summary.round(6), test_summary.to_frame().T.round(6)) # Compact training selection and one-row held-out test check
Table 1
k
balanced_accuracy
0
5
0.530118
1
15
0.509807
2
31
0.508497
selected_k
prevalence
roc_auc
AP
balanced_accuracy
TN
FP
FN
TP
0
5.0
0.469027
0.4645
0.452156
0.474161
79.0
101.0
78.0
81.0
Apply It to a New Case
Switch to Hengrui Pharmaceuticals 600276.XSHG;
choose in advance distance weighting, compare Euclidean/Manhattan distance and \(k\) on the same training-only chronological folds, choose one combination, and only then report test class balance, balanced accuracy, and query time once.
Complete-answer reminder
keep the chronological analysis clear, estimate scaling from training data only, compare distance measures on the same validation period, report both speed and predictive performance, and state the limitations.
Cite actual output and do not interpret predictive proximity causally.
Complete Solution for the New Case: Hengrui Distance Comparison
Table 2
Code
from time import perf_counter # Measuring the query time of the fixed test setfrom pathlib import Path # Locate the downloaded data filefrom sklearn.model_selection import GridSearchCV, TimeSeriesSplit # Extending the window only during the training periodfrom sklearn.pipeline import Pipeline # Fit scaling and KNN on the same training rows# Public download: https://assets.qiufei.site/data/stock/stock_price_pre_adjusted.h5# After downloading, change the next line to the file's actual location on your device.# Course-relative option: Path("data/stock/stock_price_pre_adjusted.h5")# Windows: Path(r"C:\qiufei\data\stock\stock_price_pre_adjusted.h5")# macOS: Path("/Users/your_name/data/stock/stock_price_pre_adjusted.h5")# Linux: Path("/home/your_name/data/stock/stock_price_pre_adjusted.h5")price_path = Path("/home/ubuntu/r2_data_mount/data/stock/stock_price_pre_adjusted.h5")transfer_rows = pd.read_hdf(price_path, key='data', where=['order_book_id=="600276.XSHG"', 'date>=Timestamp("2018-01-01")', 'date<=Timestamp("2024-12-31")'], columns=['close']) # Select Hengrui Medicine Close Pricetransfer_frame = transfer_rows.reset_index().sort_values('date') # Sort by trading days in order of forecast availabilitytransfer_frame['return_t'] = transfer_frame['close'].pct_change() # Compute one-day returnstransfer_frame['return_5d_t'] = transfer_frame['close'].pct_change(5) # Compute five-day returnstransfer_frame['volatility_5d_t'] = transfer_frame['return_t'].rolling(5).std() # Compute rolling five-day volatilitytransfer_frame['future_return_t1'] = transfer_frame['return_t'].shift(-1) # Retain continuous future returns before label constructiontransfer_frame['target_date_t1'] = transfer_frame['date'].shift(-1) # Preserve the label-realization datetransfer_frame = transfer_frame.dropna() # Drop rows with unknown future outcomes or rolling-window missing values before label constructiontransfer_frame['down_t1'] = (transfer_frame['future_return_t1'] <0).astype(int) # Create labels only for observed future returnstransfer_train = transfer_frame[(transfer_frame['date'] <='2022-12-31') & (transfer_frame['target_date_t1'] <'2023-01-01')] # Purge training labels realized in testtransfer_test = transfer_frame[transfer_frame['date'] >='2023-01-01']assert transfer_train['target_date_t1'].max() < transfer_test['date'].min() # Verify the label-realization boundarytransfer_features = ['return_t', 'return_5d_t', 'volatility_5d_t'] # Three Dimensions of Fixed Distance Space
Table 3
Code
transfer_pipeline = Pipeline([('scale', StandardScaler()), ('knn', KNeighborsClassifier(weights='distance'))]) # Keep scaling inside each training foldtransfer_cv = TimeSeriesSplit(5, gap=1) # Purge one row for the one-day forecast horizon in every foldassertall(transfer_train.iloc[fit_idx]['target_date_t1'].max() < transfer_train.iloc[val_idx]['date'].min() for fit_idx, val_idx in transfer_cv.split(transfer_train)) # Verify every fold's label boundarytransfer_grid = {'knn__metric': ['euclidean', 'manhattan'], 'knn__n_neighbors': [5, 15, 31]} # Declare the complete candidate set on common validation evidencetransfer_search = GridSearchCV(transfer_pipeline, transfer_grid, cv=transfer_cv, scoring='balanced_accuracy', return_train_score=False).fit(transfer_train[transfer_features], transfer_train['down_t1']) # Select one metric and k using training-period folds onlyvalidation_comparison = pd.DataFrame(transfer_search.cv_results_)[['param_knn__metric', 'param_knn__n_neighbors', 'mean_test_score']].sort_values('mean_test_score', ascending=False) # Publish comparable validation summariesselected_metric = transfer_search.best_params_['knn__metric']selected_k =int(transfer_search.best_params_['knn__n_neighbors'])query_start = perf_counter() # Record a monotone clock before the unique test predictiontransfer_prediction = transfer_search.predict(transfer_test[transfer_features]) # Open the test once with the fixed combinationquery_ms = (perf_counter() - query_start) *1000# Convert the unique test query to millisecondstransfer_test_summary = pd.Series({'selected_metric': selected_metric, 'selected_k': selected_k, 'test_prevalence': transfer_test['down_t1'].mean(), 'balanced_accuracy': balanced_accuracy_score(transfer_test['down_t1'], transfer_prediction), 'query_ms': query_ms}) # Record one held-out test check for one specification
Transfer Validation and held-out-Test check
Code
display(validation_comparison.round(6), transfer_test_summary.to_frame().T.round(6)) # Separate validation selection from test evidence
param_knn__metric
param_knn__n_neighbors
mean_test_score
0
euclidean
5
0.519945
4
manhattan
15
0.517000
5
manhattan
31
0.512933
1
euclidean
15
0.511542
2
euclidean
31
0.510255
3
manhattan
5
0.508568
selected_metric
selected_k
test_prevalence
balanced_accuracy
query_ms
0
euclidean
5
0.52588
0.541321
2.9185
Expanding-window validation locks Euclidean distance with \(k=5\) from the six candidates; only then is the test opened once.
The test has 483 observations, decline prevalence 0.525880, and fixed-specification balanced accuracy 0.541321.
The last feature date is 2024-12-30, and unknown future labels are removed before integer conversion.
Timing varies by hardware; predictive proximity is not causal similarity.
Formative Check 3: The Curse of Dimensionality
If nearest and farthest distances become almost equal in high dimensions, what happens to KNN?
Answer
Relative neighbor contrast collapses, making local voting and distance weighting unstable. Use feature selection/reduction, a domain metric, or a model more robust to high dimension.
Make units comparable, not predictive importance equal
Fit on training data before computing distance
Final Thoughts: The Keys to Success with KNN
To apply KNN successfully, economists and data scientists need to focus on:
Feature Engineering: Selecting the most relevant features for the problem is crucial. Garbage in, garbage out.
Distance Metric: Choose a distance metric appropriate for your specific problem. Euclidean distance is not always the best choice.
Parameter Tuning: Choose in advance uniform Euclidean majority voting and select only \(k\) in training-only chronological folds, refitting preprocessing inside every fold; finish that choice before evaluating the test period once.
Efficiency Considerations: For large datasets, it’s essential to consider acceleration algorithms like KD-Trees or Ball Trees, or to use approximate nearest neighbor search methods.
KNN is an excellent gateway into the world of machine learning—it’s simple, intuitive, and powerful.