Build a Defensible Wine Classifier
Build and evaluate a scaled KNN wine classifier with reproducible evidence.
Introduction
30 Second Summary
A model can produce an impressive score without proving that its predictions will hold up. Internship interviews often reveal whether you understand the evidence behind your results.
In this project, you will build a local wine classifier in Python that exposes a weak multiclass classification baseline before feature scaling strengthens it. The finished experiment becomes a reproducible GitHub project whose evidence you can defend in an internship interview.
What You'll Build
Your finished train.py run turns a weak baseline into a defensible comparison whose mistakes are visible in a confusion matrix.
By the end of this project, you'll have:
- A runnable scikit-learn comparison that shows how feature scaling changes the accuracy of a K-nearest neighbors model.
- A defensible evaluation story built from holdout accuracy plus five-fold cross-validation. Per-class precision, recall, F1 scores, plus a confusion matrix reveal where predictions succeed.
- A public GitHub repository that another person can reproduce. Meaningful Git history preserves the journey from weak baseline to stronger model.
- Secret Mission: Tune the number of neighbors with training-only cross-validation before evaluating the chosen model once on the untouched test set.
Are there any prerequisites?
Basic Python knowledge plus a Windows computer is enough to begin. Step 1 covers every local tool before you publish through your GitHub account.
Before We Start
Before the hands-on work begins, this step anchors your wine classifier to a clear purpose. You are committing to a project that demonstrates preprocessing, evaluation, and reproducibility for machine learning internship applications.
Set Up the Windows ML Workspace
A machine learning result is hard to trust when package versions drift between experiments. An isolated workspace keeps your wine classifier reproducible.
You will verify Python, Visual Studio Code, and Git on Windows. You will then create a virtual environment with pinned versions of scikit-learn, pandas, and Matplotlib.
In this step, get ready to:
- Install or confirm the Windows tools used by the project.
- Create the wine-classifier workspace with its dependency files.
- Build an isolated environment with the pinned machine learning packages.
Install or confirm the Windows tools
Python runs the classifier. Visual Studio Code gives you one place to edit files and use the project environment.
Git records each stage of the experiment. Verifying these tools now prevents environment problems from interrupting the modeling workflow.
- Press the Windows key to open Windows Search.
- Type PowerShell into Windows Search.
- Press Enter to open PowerShell.
- Check the installed Python version by running this command:
python --version
What does this check prove?
This command prints the Python version available to your terminal. The project uses Python 3.14.8 so every pinned package runs in the intended environment.
✔️ I see Python 3.14.8
Python 3.14.8 is installed and available in PowerShell.
ⓧ I see an older version
Your current Python version is below the project version. Install Python 3.14.8 before creating the environment.
- Open the official Python release page in your browser.
- Choose Windows installer (64-bit) unless Windows reports an ARM64 device.
- Choose the ARM64 installer if Windows reports an ARM64 device.
- Run the downloaded installer.
- Approve the Windows permission request if it appears.
- Complete the installation with the recommended options.
- Close PowerShell after installation.
- Open a new PowerShell window through Windows Search.
- Verify the new installation by running:
python --version
What should this confirm?
The new PowerShell session reloads the available commands. The result should identify Python 3.14.8.
ⓧ Command not found
PowerShell cannot find a Python installation. Install the project version before continuing.
- Open the official Python release page in your browser.
- Choose Windows installer (64-bit) unless Windows reports an ARM64 device.
- Choose the ARM64 installer if Windows reports an ARM64 device.
- Run the downloaded installer.
- Approve the Windows permission request if it appears.
- Complete the installation with the recommended options.
- Close PowerShell after installation.
- Open a new PowerShell window through Windows Search.
- Confirm that Python is now available by running:
python --version
What should this confirm?
The result should identify Python 3.14.8. This confirms PowerShell can access the installation.
- Press the Windows key to return to Windows Search.
- Type Visual Studio Code into Windows Search.
✔️ Visual Studio Code opens
Visual Studio Code is installed and ready for the project.
ⓧ Visual Studio Code is not installed
Install Visual Studio Code before adding its Python extension.
- Open the official Visual Studio Code download page in your browser.
- Download the Windows installer.
- Run the downloaded installer.
- Approve the Windows permission request if it appears.
- Complete the installation with the recommended options.
- Press the Windows key after installation.
- Type Visual Studio Code into Windows Search.
- Press Enter to open Visual Studio Code.
- Click View in the Visual Studio Code menu bar.
- Click Extensions.
- Type python into the Extensions search field.
- Select the Python extension published by Microsoft.
✔️ The Python extension is installed
The Python extension is ready to create and select the project environment.
ⓧ I see Install
- Click Install on the Python extension page.
- Wait for the installation control to change into extension management controls.
The Python extension is now available in Visual Studio Code.
- Switch back to PowerShell.
- Check the installed Git version by running this command:
git --version
What does this check prove?
This command prints the Git release available to PowerShell. The project uses Git for Windows 2.56.0(2) to record the baseline and later improvements.
✔️ Git for Windows 2.56.0(2) is installed
Git for Windows 2.56.0(2) is installed and available in PowerShell.
ⓧ I see an older version
Update Git for Windows so the workspace uses the maintained 2.56.0(2) build.
- Open the official Git for Windows install page in your browser.
- Download the maintained x64 installer for Git for Windows 2.56.0(2).
- Run the downloaded installer.
- Complete the installation with the recommended options.
- Close PowerShell after installation.
- Open a new PowerShell window through Windows Search.
- Verify the updated installation by running:
git --version
What should this confirm?
The result should identify the updated Git release. PowerShell is now using Git for Windows 2.56.0(2).
ⓧ Command not found
PowerShell cannot find Git. Install Git for Windows before continuing.
- Open the official Git for Windows install page in your browser.
- Download the maintained x64 installer for Git for Windows 2.56.0(2).
- Run the downloaded installer.
- Complete the installation with the recommended options.
- Close PowerShell after installation.
- Open a new PowerShell window through Windows Search.
- Confirm that Git is now available by running:
git --version
What should this confirm?
The result should identify Git for Windows 2.56.0(2). This confirms PowerShell can access the installation.
Still having trouble with the Windows tools?
- Restart PowerShell after installing Python or Git so the new commands become available.
- Restart Visual Studio Code if the Python extension does not appear after installation.
- Check that you selected the Python extension published by Microsoft.
Ask for help with the specific tool that is failing: Help me troubleshoot my Windows machine learning tool setup.
Create the workspace files
The workspace needs a dependency manifest and an ignore file before the environment is created. These files make the setup repeatable while keeping generated files out of version control.
- Return to Visual Studio Code.
- Click File in the top menu bar.
- Click Open Folder....
- Select your Desktop in the folder dialog.
- Create a folder named wine-classifier with the folder-creation control.
- Select the wine-classifier folder.
- Click Select Folder.
- Confirm that you trust the folder if Visual Studio Code asks.
You should see wine-classifier at the top of the Explorer sidebar. Your project now has a clear home on the Desktop.
- Click New File... in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Press Enter to create the file.
- Pin the project dependencies by copying these lines into requirements.txt:
scikit-learn==1.9.1
pandas==3.0.6
matplotlib==3.11.2
What does this file control?
- Each line pairs one package with an exact version.
- The pins keep future installations aligned with the environment used to build the classifier.
- Visual Studio Code reads this file when it creates the virtual environment.
- Press Ctrl+S to save requirements.txt.
- Confirm that requirements.txt appears under wine-classifier in the Explorer sidebar.
Missing requirements.txt?
- Check that the Explorer sidebar shows wine-classifier as the open folder.
- Check the file name for an extra extension such as requirements.txt.txt.
- Save the file before checking the Explorer sidebar again.
Get help checking the dependency file: Why is requirements.txt missing from my VS Code workspace?
- Click New File... in the Explorer sidebar.
- Enter .gitignore as the file name.
- Press Enter to create the file.
- Exclude generated environment files by copying these lines into .gitignore:
.venv/
__pycache__/
What does this file protect?
- The .venv/ entry excludes the local virtual environment.
- The __pycache__/ entry excludes Python's generated cache files.
- Both entries keep future Git commits focused on files another person needs.
- Press Ctrl+S to save .gitignore.
- Confirm that .gitignore appears beside requirements.txt in the Explorer sidebar.
Missing .gitignore?
- Check that the file name begins with a period.
- Remove any extra .txt extension from the file name.
- Save the file before checking the Explorer sidebar again.
Get help checking the ignore file: Why is my .gitignore file missing or named incorrectly?
Use these tabs to compare both workspace files before creating the environment.
✔️ Awesome, I've got everything!
- Save requirements.txt.
- Save .gitignore.
- Confirm that both files appear in the Explorer sidebar.
ⓧ I'd like to double check the full code
Compare your dependency file with this exact version.
scikit-learn==1.9.1
pandas==3.0.6
matplotlib==3.11.2
What should match?
Your requirements.txt file should contain these three lines in this order. Each version should match exactly.
Compare your ignore file with this exact version.
.venv/
__pycache__/
What should match?
Your .gitignore file should contain both directory patterns. Each pattern should end with a forward slash.
Create and verify the virtual environment
A virtual environment gives this project its own Python installation context. Package changes inside .venv stay separate from other projects on your computer.
Visual Studio Code can create the environment from requirements.txt. It also selects the environment for future Python commands in this workspace.
- Press Ctrl+Shift+P to open the Command Palette.
- Type Python: Create Environment into the Command Palette.
- Press Enter to run the command.
- Choose venv as the environment type.
- Select Python 3.14.8 as the interpreter.
- Choose requirements.txt when Visual Studio Code asks which dependency file to install.
The package installation may take a few minutes while Python downloads the pinned dependencies. A short wait here is normal.
- Wait for the environment creation process to finish.
- Confirm that .venv appears in the Explorer sidebar.
- Click Terminal in the Visual Studio Code menu bar.
- Click New Terminal.
You should see (.venv) at the start of the terminal prompt. This confirms that the workspace terminal is using the selected environment.
Before you run the final checks, which package versions do you expect this environment to report?
- Verify the installed packages and scikit-learn system information by running these commands:
python -m pip freeze
python -c "import sklearn; sklearn.show_versions()"
What do these checks prove?
The first command lists every installed package. This exposes the exact versions inside .venv.
The second command imports scikit-learn and prints its system information. A completed report confirms that the library loads successfully.
You should see scikit-learn==1.9.1, pandas==3.0.6, and matplotlib==3.11.2 in the package list. Scikit-learn system information should follow.
Seeing the wrong versions or an import problem?
- Check that (.venv) appears at the start of the terminal prompt.
- Check that the selected interpreter points to the .venv folder inside wine-classifier.
- Compare every line in requirements.txt with the full-file reference above.
Ask for help with the environment output: Help me diagnose my .venv package verification results.
Your Windows workspace now has verified tools, isolated dependencies, and the two files needed to reproduce the setup. Next, you will load the Wine dataset and run the first visible baseline.
Build the Unscaled KNN Baseline
Your isolated Windows workspace is ready. You can now produce the first piece of model evidence.
A defensible experiment needs a baseline that preserves the original feature scales. A K-nearest neighbors classifier makes the scaling problem visible because large-magnitude features can dominate its distance calculations.
In this step, get ready to:
- Inspect the pandas-backed Wine dataset.
- Fit an unscaled KNN classifier on a stratified split.
- Commit the reproducible baseline to Git.
Load and inspect the Wine dataset
Scikit-learn can return the Wine dataset as pandas-backed tables. Using load_wine(as_frame=True) keeps the feature names visible while you inspect the first five samples.
- Select the new-file icon in the VS Code Explorer sidebar.
- Enter train.py as the file name.
- Press Enter to create the file inside wine-classifier.
- Build the dataset loader by pasting this code into the blank train.py file:
from sklearn.datasets import load_wine
def load_dataset():
wine = load_wine(as_frame=True)
X = wine.data
y = wine.target
print("Dataset preview:")
print(X.head())
print(f"\nFeature shape: {X.shape}")
print("\nClass counts:")
print(y.value_counts(sort=False))
return wine, X, y
def main():
wine, X, y = load_dataset()
if __name__ == "__main__":
main()
What does this code do?
- The load_dataset() function loads the bundled Wine dataset into a reusable workflow.
- The X variable holds the 13 numeric features used for prediction.
- The y variable holds the three class labels.
- The X.head() call prints the first five samples for a quick data check.
- The shape output confirms how many samples and features were loaded.
- The class-count output shows how many samples belong to each target class.
- Save train.py in VS Code.
- Reveal the dataset summary by running this command in the activated VS Code terminal:
python train.py
What should you see?
You should see a five-row preview followed by Feature shape: (178, 13). Three class counts should appear underneath.
These outputs prove that the script can access the dataset through your selected virtual environment.
That first run matters: your project can now turn a bundled dataset into evidence you can inspect.
Dataset output missing?
- Confirm that the VS Code terminal still shows the activated .venv environment.
- Check that train.py is saved inside the open wine-classifier folder.
- Compare the indentation inside load_dataset() with the code block above.
Help me troubleshoot my Wine dataset loader.
Train the raw-distance baseline
The model needs fixed experiment settings before it can produce a reproducible score. The constants keep the test split size and neighbor count in one visible place.
- Select the first import line in train.py.
- Replace the selected line with this import and configuration block:
from sklearn.datasets import load_wine
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
RANDOM_STATE = 42
TEST_SIZE = 0.30
N_NEIGHBORS = 5
What does this code add?
- The new imports provide the split helper, the KNN classifier, and the accuracy calculation.
- The RANDOM_STATE constant makes repeated splits reproducible.
- The TEST_SIZE constant reserves 30 percent of the data for final testing.
- The N_NEIGHBORS constant tells KNN to classify each sample using five nearby training samples.
- Save train.py.
- Confirm that the new imports execute by running the script again:
python train.py
What should you see now?
You should see the same five-row preview, feature shape, and class counts. A complete run confirms that the new imports are available in .venv.
Import failing after the update?
- Check each import against the block above because module names are case-sensitive.
- Confirm that VS Code still uses the .venv interpreter from the previous step.
- Make sure the original load_dataset() function remains below the constants.
Help me fix the imports in my baseline script.
A stratified split keeps all three classes represented across the training and test sets. The raw KNN model then measures distances using the original feature values.
- Select the code from def main(): through the final main() call in train.py.
- Replace the selected code with this split and baseline workflow:
def split_dataset(X, y):
return train_test_split(
X,
y,
test_size=TEST_SIZE,
random_state=RANDOM_STATE,
stratify=y,
)
def main():
wine, X, y = load_dataset()
X_train, X_test, y_train, y_test = split_dataset(X, y)
baseline_model = KNeighborsClassifier(n_neighbors=N_NEIGHBORS)
baseline_model.fit(X_train, y_train)
baseline_predictions = baseline_model.predict(X_test)
baseline_accuracy = accuracy_score(y_test, baseline_predictions)
print(f"\nUnscaled KNN test accuracy: {baseline_accuracy:.3f}")
if __name__ == "__main__":
main()
How does the baseline work?
- The split_dataset() function reserves 30 percent of the samples for testing.
- The stratify=y setting preserves the class distribution during the split.
- The baseline_model learns from the unscaled training features.
- The baseline_predictions variable stores one predicted class for each test sample.
- The baseline_accuracy value measures the fraction of test samples classified correctly.
- Save train.py.
Before you run this version, do you expect the unscaled model to complete even though the feature ranges remain untouched?
- Expose the unscaled baseline score by running:
python train.py
What did the baseline reveal?
You should see the dataset summary followed by a decimal value labelled Unscaled KNN test accuracy.
That score is your intentionally weak baseline. Its raw distance calculations give large-magnitude features more influence.
You now have a real comparison point. The next model must earn its place by improving on this measured result.
Baseline score missing?
- Confirm that split_dataset() appears above main().
- Check that every line inside main() uses the same four-space indentation.
- Verify that baseline_model.fit(X_train, y_train) appears before the prediction line.
Help me debug my unscaled KNN baseline.
Your script now contains the complete baseline workflow. Use these tabs to confirm that every line is in the right place.
✔️ My file matches
Your baseline file is complete. It loads the data, creates the split, fits the model, and reports its accuracy.
ⓧ I want to check the full code
from sklearn.datasets import load_wine
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
RANDOM_STATE = 42
TEST_SIZE = 0.30
N_NEIGHBORS = 5
def load_dataset():
wine = load_wine(as_frame=True)
X = wine.data
y = wine.target
print("Dataset preview:")
print(X.head())
print(f"\nFeature shape: {X.shape}")
print("\nClass counts:")
print(y.value_counts(sort=False))
return wine, X, y
def split_dataset(X, y):
return train_test_split(
X,
y,
test_size=TEST_SIZE,
random_state=RANDOM_STATE,
stratify=y,
)
def main():
wine, X, y = load_dataset()
X_train, X_test, y_train, y_test = split_dataset(X, y)
baseline_model = KNeighborsClassifier(n_neighbors=N_NEIGHBORS)
baseline_model.fit(X_train, y_train)
baseline_predictions = baseline_model.predict(X_test)
baseline_accuracy = accuracy_score(y_test, baseline_predictions)
print(f"\nUnscaled KNN test accuracy: {baseline_accuracy:.3f}")
if __name__ == "__main__":
main()
How to use this reference
This is the complete cumulative train.py file for this step. Compare its imports, spacing, function order, and indentation with your file.
Preserve the baseline in Git
Git history preserves the weak model before you improve it. This checkpoint makes your experiment sequence visible to anyone reviewing the repository.
- Create the local repository on branch main by running these commands in the activated VS Code terminal:
git init -b main
git add .
git commit -m "Build unscaled KNN baseline"
What do these commands preserve?
- The first command initializes the local repository with main as its first branch.
- The second command stages the project files for the checkpoint.
- The third command records the staged baseline with the message Build unscaled KNN baseline.
That is your first model checkpoint saved: the repository now preserves the exact baseline you measured.
Commit did not complete?
- Follow the identity configuration guidance printed by Git if this is your first local commit.
- Rerun the commit command after completing the requested identity configuration.
- Confirm that the terminal is still working inside the open wine-classifier folder.
Help me complete my first Git commit.
Before your final run, do you expect the committed script to reproduce the same dataset summary and baseline accuracy?
- Verify the committed baseline by running the script once more:
python train.py
What confirms success?
You should see the five-row preview, the (178, 13) feature shape, three class counts, and the unscaled KNN test accuracy.
The repeated score confirms that the fixed split produces a reproducible baseline.
Your unscaled baseline is runnable, reproducible, and preserved in Git. Next, you will attach feature scaling to the model and measure whether it improves the result.
Add a Leakage-Safe Scaling Pipeline
Your unscaled baseline now runs from the activated terminal in Visual Studio Code. That committed result gives you a reproducible comparison point.
K-nearest neighbors classifies samples through distance. Large-range Wine features can dominate that calculation.
Feature scaling puts the features on comparable scales. A scikit-learn pipeline learns that scaling only from the training data used for each fit.
In this step, get ready to:
- Import the scaling and pipeline tools.
- Compare the unscaled baseline with a scaled KNN pipeline.
- Save the working comparison in Git history.
Import the scaling tools
The comparison needs one tool to standardize each feature. It also needs one tool to keep preprocessing attached to the classifier.
- In train.py, find the import for KNeighborsClassifier.
- Place your cursor on the line below that import.
- Add the scaling imports by pasting this code:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
What do these imports provide?
The make_pipeline helper chains preprocessing with a model. StandardScaler learns a mean and variance for each feature during fitting.
- Save train.py.
- Confirm the imports leave the baseline runnable by running this command:
python train.py
What should you see?
You should see the dataset preview and class counts. The terminal should finish with Unscaled KNN test accuracy followed by the baseline score.
Does the import fail?
- Check that both new import lines sit below the existing scikit-learn imports.
- Check the spelling and capitalization of make_pipeline and StandardScaler.
- Confirm the terminal still shows the activated .venv environment.
Help me diagnose the scaling import problem.
Build the scaled comparison function
The evaluator keeps the weak baseline for evidence. It also fits a scaled model using only X_train and y_train.
- In train.py, find def main():.
- Place your cursor on the blank line above def main():.
- Add the evaluation function by pasting this code:
def evaluate_baseline_and_pipeline(X_train, X_test, y_train, y_test):
baseline_model = KNeighborsClassifier(n_neighbors=N_NEIGHBORS)
baseline_model.fit(X_train, y_train)
baseline_predictions = baseline_model.predict(X_test)
baseline_accuracy = accuracy_score(y_test, baseline_predictions)
scaled_model = make_pipeline(
StandardScaler(),
KNeighborsClassifier(n_neighbors=N_NEIGHBORS),
)
scaled_model.fit(X_train, y_train)
scaled_predictions = scaled_model.predict(X_test)
scaled_accuracy = accuracy_score(y_test, scaled_predictions)
print(f"\nUnscaled KNN test accuracy: {baseline_accuracy:.3f}")
print(f"Scaled pipeline test accuracy: {scaled_accuracy:.3f}")
print(
"Accuracy improvement from scaling: "
f"{scaled_accuracy - baseline_accuracy:+.3f}"
)
return scaled_model, scaled_predictions
What does this function do?
- The baseline section fits KNN directly on the original feature values.
- The scaled section places StandardScaler before an identical KNN classifier.
- Both models predict the same held-out X_test samples for a fair comparison.
- The return statement preserves the scaled model and its predictions for later evaluation.
Why keep scaling in a pipeline?
The pipeline fits StandardScaler whenever it fits the classifier. Because the fit receives only the training split, the held-out test data cannot influence the learned scaling values.
This blocks data leakage from the test set. The final accuracy stays a defensible measure of unseen-data performance.
- Save train.py.
- Confirm the expanded file remains runnable by running this command:
python train.py
What should you see?
You should still see the dataset preview and unscaled test accuracy. A clean run confirms that the new evaluation function is valid Python.
Seeing a syntax problem?
- Check that evaluate_baseline_and_pipeline begins at the left edge of the file.
- Check that every line inside the function uses the same four-space indentation.
- Compare the closing parentheses around make_pipeline and print.
Help me find the syntax problem in this function.
Connect the pipeline and save the result
The evaluator now contains both models. The main function can delegate the comparison to it after loading and splitting the data.
- In train.py, find def main():.
- Select the entire current main function through the unscaled accuracy print line.
- Replace the selection by pasting this function:
def main():
wine, X, y = load_dataset()
X_train, X_test, y_train, y_test = split_dataset(X, y)
evaluate_baseline_and_pipeline(X_train, X_test, y_train, y_test)
What does main do now?
The main function still loads the Wine data and creates the stratified split. It now passes those four split objects to the comparison function.
- Save train.py.
✔️ Awesome, I've got everything!
Your saved file now contains the baseline and the leakage-safe scaled comparison.
ⓧ I'd like to double check the full code
from sklearn.datasets import load_wine
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
RANDOM_STATE = 42
TEST_SIZE = 0.30
N_NEIGHBORS = 5
def load_dataset():
wine = load_wine(as_frame=True)
X = wine.data
y = wine.target
print("Dataset preview:")
print(X.head())
print(f"\nFeature shape: {X.shape}")
print("\nClass counts:")
print(y.value_counts(sort=False))
return wine, X, y
def split_dataset(X, y):
return train_test_split(
X,
y,
test_size=TEST_SIZE,
random_state=RANDOM_STATE,
stratify=y,
)
def evaluate_baseline_and_pipeline(X_train, X_test, y_train, y_test):
baseline_model = KNeighborsClassifier(n_neighbors=N_NEIGHBORS)
baseline_model.fit(X_train, y_train)
baseline_predictions = baseline_model.predict(X_test)
baseline_accuracy = accuracy_score(y_test, baseline_predictions)
scaled_model = make_pipeline(
StandardScaler(),
KNeighborsClassifier(n_neighbors=N_NEIGHBORS),
)
scaled_model.fit(X_train, y_train)
scaled_predictions = scaled_model.predict(X_test)
scaled_accuracy = accuracy_score(y_test, scaled_predictions)
print(f"\nUnscaled KNN test accuracy: {baseline_accuracy:.3f}")
print(f"Scaled pipeline test accuracy: {scaled_accuracy:.3f}")
print(
"Accuracy improvement from scaling: "
f"{scaled_accuracy - baseline_accuracy:+.3f}"
)
return scaled_model, scaled_predictions
def main():
wine, X, y = load_dataset()
X_train, X_test, y_train, y_test = split_dataset(X, y)
evaluate_baseline_and_pipeline(X_train, X_test, y_train, y_test)
if __name__ == "__main__":
main()
Before you run it, ask yourself whether putting the features on comparable scales will produce a positive accuracy improvement.
- Run the completed comparison with this command:
python train.py
What should you see?
You should see the dataset preview and class counts first. The terminal should then show Unscaled KNN test accuracy and Scaled pipeline test accuracy.
The final line should show Accuracy improvement from scaling with a positive value. That visible gain exposes how strongly the original feature scales distorted KNN distances.
Missing the scaled result?
- Check that main calls evaluate_baseline_and_pipeline with all four split objects.
- Check that scaled_model.fit receives X_train and y_train.
- Check that scaled_accuracy compares y_test with scaled_predictions.
Help me troubleshoot the missing scaling comparison.
- Stage and commit the working scaling comparison by running these commands:
git add .
git commit -m "Add leakage-safe scaling pipeline"
What do these commands save?
The first command stages the updated project contents. The second records those staged changes with a message that identifies the pipeline improvement.
That locks in the fix. Your history now preserves the weak baseline before the scaled pipeline.
Did the commit stop?
- Follow the identity configuration guidance printed by Git.
- Rerun the commit commands after Git accepts your identity.
Help me complete this Git commit.
Your classifier now proves why preprocessing matters. Next, you will add broader evaluation evidence and package the project for GitHub.
Evaluate, Document, and Publish the Model
Your scaled pipeline now beats the unscaled baseline because preprocessing stays attached to model fitting.
A single holdout score only describes one data split. This step adds cross-validation plus per-class diagnostics to build stronger evidence.
You will also document the experiment for other people. The finished project then goes to GitHub as a public repository.
In this step, get ready to:
- Add repeatable evaluation plus a visual error analysis.
- Document the modeling decisions in README.md.
- Commit the finished project before publishing it on GitHub.
Add repeatable evaluation
Cross-validation tests the scaled model across several training folds. A classification report shows how its predictions perform for each wine class.
- In train.py, select the import block plus the constants at the top of the file.
- Replace the selected code with the code below:
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.metrics import ConfusionMatrixDisplay, accuracy_score, classification_report
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
RANDOM_STATE = 42
TEST_SIZE = 0.30
N_NEIGHBORS = 5
CV_FOLDS = 5
What Does This Setup Add?
- Matplotlib renders the confusion matrix in a separate window.
- The new scikit-learn imports calculate cross-validation scores plus per-class metrics.
- CV_FOLDS keeps the number of evaluation folds in one reusable constant.
- Save train.py.
- Confirm the updated imports still support the existing model comparison by running:
python train.py
What Does This Check Prove?
The command executes the script with the new imports. A successful run confirms that the evaluation dependencies load inside your activated environment.
You should see the dataset inspection plus both holdout accuracies in the terminal.
Does the Script Stop at an Import?
- Check that the terminal prompt still shows the activated .venv environment.
- Check that every import matches the code block exactly.
Help me diagnose the failed evaluation import.
- In train.py, place the following function above main():
def report_model(scaled_model, X_train, y_train, y_test, predictions, target_names):
cv_scores = cross_val_score(
scaled_model,
X_train,
y_train,
cv=CV_FOLDS,
)
mean_cv_accuracy = sum(cv_scores) / len(cv_scores)
print(f"\n{CV_FOLDS}-fold cross-validation scores: {cv_scores}")
print(f"Mean cross-validation accuracy: {mean_cv_accuracy:.3f}")
print("\nClassification report:")
print(
classification_report(
y_test,
predictions,
target_names=target_names,
zero_division=0,
)
)
ConfusionMatrixDisplay.from_predictions(
y_test,
predictions,
display_labels=target_names,
)
plt.show()
How Does the Evaluation Work?
- cross_val_score() evaluates the scaled pipeline across five folds of the training data.
- mean_cv_accuracy summarizes those five scores with one average.
- classification_report() reports precision, recall, plus F1 score for each class.
- ConfusionMatrixDisplay.from_predictions() turns the held-out predictions into a visual error map.
- Save train.py.
- Select the existing main() function.
- Replace the selected function with the code below:
def main():
wine, X, y = load_dataset()
X_train, X_test, y_train, y_test = split_dataset(X, y)
scaled_model, scaled_predictions = evaluate_baseline_and_pipeline(
X_train,
X_test,
y_train,
y_test,
)
report_model(
scaled_model,
X_train,
y_train,
y_test,
scaled_predictions,
wine.target_names,
)
What Does the Updated Main Function Do?
The updated function keeps the fitted scaled model plus its test predictions. It passes both into report_model() with the class names needed for readable output.
✔️ Awesome, I've got everything!
Great. Save train.py before running the finished evaluation.
ⓧ I'd like to double check the full code
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.metrics import ConfusionMatrixDisplay, accuracy_score, classification_report
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
RANDOM_STATE = 42
TEST_SIZE = 0.30
N_NEIGHBORS = 5
CV_FOLDS = 5
def load_dataset():
wine = load_wine(as_frame=True)
X = wine.data
y = wine.target
print("Dataset preview:")
print(X.head())
print(f"\nFeature shape: {X.shape}")
print("\nClass counts:")
print(y.value_counts(sort=False))
return wine, X, y
def split_dataset(X, y):
return train_test_split(
X,
y,
test_size=TEST_SIZE,
random_state=RANDOM_STATE,
stratify=y,
)
def evaluate_baseline_and_pipeline(X_train, X_test, y_train, y_test):
baseline_model = KNeighborsClassifier(n_neighbors=N_NEIGHBORS)
baseline_model.fit(X_train, y_train)
baseline_predictions = baseline_model.predict(X_test)
baseline_accuracy = accuracy_score(y_test, baseline_predictions)
scaled_model = make_pipeline(
StandardScaler(),
KNeighborsClassifier(n_neighbors=N_NEIGHBORS),
)
scaled_model.fit(X_train, y_train)
scaled_predictions = scaled_model.predict(X_test)
scaled_accuracy = accuracy_score(y_test, scaled_predictions)
print(f"\nUnscaled KNN test accuracy: {baseline_accuracy:.3f}")
print(f"Scaled pipeline test accuracy: {scaled_accuracy:.3f}")
print(
"Accuracy improvement from scaling: "
f"{scaled_accuracy - baseline_accuracy:+.3f}"
)
return scaled_model, scaled_predictions
def report_model(scaled_model, X_train, y_train, y_test, predictions, target_names):
cv_scores = cross_val_score(
scaled_model,
X_train,
y_train,
cv=CV_FOLDS,
)
mean_cv_accuracy = sum(cv_scores) / len(cv_scores)
print(f"\n{CV_FOLDS}-fold cross-validation scores: {cv_scores}")
print(f"Mean cross-validation accuracy: {mean_cv_accuracy:.3f}")
print("\nClassification report:")
print(
classification_report(
y_test,
predictions,
target_names=target_names,
zero_division=0,
)
)
ConfusionMatrixDisplay.from_predictions(
y_test,
predictions,
display_labels=target_names,
)
plt.show()
def main():
wine, X, y = load_dataset()
X_train, X_test, y_train, y_test = split_dataset(X, y)
scaled_model, scaled_predictions = evaluate_baseline_and_pipeline(
X_train,
X_test,
y_train,
y_test,
)
report_model(
scaled_model,
X_train,
y_train,
y_test,
scaled_predictions,
wine.target_names,
)
if __name__ == "__main__":
main()
The script opens a Matplotlib window after printing its terminal output. The process remains active until you close that window.
Before you run the finished script, which evidence do you expect to reveal more than the original holdout accuracy?
- Run the complete evaluation with this command:
python train.py
What Should the Finished Run Produce?
The terminal prints five cross-validation scores plus their mean. It then prints precision, recall, F1 score, plus support for each wine class.
The confusion matrix shows where the scaled pipeline predicts each class correctly. Any off-diagonal cells reveal its mistakes.
You should see the full report in the terminal. You should also see a confusion matrix labeled with the three wine classes.
Missing the Report or Matrix?
- Check that main() assigns both values returned by evaluate_baseline_and_pipeline().
- Check that report_model() appears above main().
- Check the taskbar for the Matplotlib window if the terminal appears to pause after the report.
Help me debug the missing evaluation output.
- Close the confusion matrix window.
That is the evaluation layer complete. Your model now produces evidence that you can inspect class by class.
Document the experiment
A strong README turns the terminal output into an argument someone else can follow. It records the problem plus the reasoning behind the scaled pipeline.
- In the VS Code Explorer sidebar, select the wine-classifier folder.
- Select the new-file control at the top of the Explorer sidebar.
- Enter README.md as the file name.
You should see README.md in the Explorer sidebar plus a new editor tab.
- Add the project overview plus the modeling approach by pasting this first section into README.md.
# Wine Classifier: From Weak Baseline to Defensible Model
A compact machine learning portfolio project that shows why preprocessing choices matter for distance-based models.
## Problem
Classify samples in scikit-learn's copy of the UCI Wine dataset into one of three classes. The project deliberately starts with an unscaled K-nearest neighbors baseline, then fixes its scale sensitivity with a pipeline.
## What the project demonstrates
- Data inspection with pandas-backed scikit-learn data
- A stratified train/test split
- Baseline-first experimentation
- Feature standardization with `StandardScaler`
- A leakage-safe `make_pipeline` workflow
- Holdout accuracy and five-fold cross-validation
- Per-class precision, recall, and F1 scores
- Confusion-matrix error analysis
- Reproducible dependencies and Git history
## Approach
1. Load 178 samples with 13 numeric features and three target classes.
2. Reserve 30 percent of the data as an untouched stratified test set.
3. Fit a five-neighbor KNN baseline on the raw feature values.
4. Fit a second five-neighbor KNN model behind `StandardScaler` in one pipeline.
5. Compare holdout accuracy, inspect five-fold cross-validation, and review per-class errors.
What Does This Section Explain?
The opening states the classification problem. The remaining sections connect the weak baseline to the preprocessing fix plus the evaluation evidence.
- Save README.md.
- Confirm that the editor shows the Problem plus Approach headings.
Does the README Look Like Plain Text?
Check that the file name ends with .md. Markdown-aware editors use that extension to recognize headings plus lists.
Help me check my README formatting.
- Add the run instructions plus interview talking points below the existing Approach list:
## Run locally on Windows
Create and select the `.venv` environment described by the project guide, then run:
```powershell
python train.py
```
The script prints the observed metrics and opens a confusion matrix. The scores are computed at runtime rather than hard-coded into this README.
## Why the scaled pipeline is the defensible model
KNN uses distances between samples. When feature ranges differ substantially, large-magnitude features can dominate those distances. `StandardScaler` puts features on comparable scales, while the pipeline keeps scaling attached to each model fit instead of preprocessing the entire dataset in advance.
## Interview talking points
- Why a baseline should come before optimization
- Why the test set stays untouched until final evaluation
- Why preprocessing belongs inside a pipeline
- What accuracy hides and what the classification report reveals
- Why cross-validation is more persuasive than one split alone
## Next experiment
Tune the neighbor count with cross-validation on the training split, then evaluate the selected pipeline once on the untouched test set.
Why Include These Details?
The run instructions make the project reproducible. The talking points give you a concise path through the decisions during an interview.
The final experiment points to a responsible next step. It keeps the untouched test set outside hyperparameter selection.
- Save README.md.
- Confirm that the final heading reads Next experiment.
Is Part of the README Missing?
- Check that the second section begins directly below the fifth Approach item.
- Check that both code-fence lines contain three backticks.
Help me compare my README with the finished documentation.
✔️ Awesome, I've got everything!
Your saved README now explains the model from problem to next experiment.
ⓧ I'd like to double check the full code
# Wine Classifier: From Weak Baseline to Defensible Model
A compact machine learning portfolio project that shows why preprocessing choices matter for distance-based models.
## Problem
Classify samples in scikit-learn's copy of the UCI Wine dataset into one of three classes. The project deliberately starts with an unscaled K-nearest neighbors baseline, then fixes its scale sensitivity with a pipeline.
## What the project demonstrates
- Data inspection with pandas-backed scikit-learn data
- A stratified train/test split
- Baseline-first experimentation
- Feature standardization with `StandardScaler`
- A leakage-safe `make_pipeline` workflow
- Holdout accuracy and five-fold cross-validation
- Per-class precision, recall, and F1 scores
- Confusion-matrix error analysis
- Reproducible dependencies and Git history
## Approach
1. Load 178 samples with 13 numeric features and three target classes.
2. Reserve 30 percent of the data as an untouched stratified test set.
3. Fit a five-neighbor KNN baseline on the raw feature values.
4. Fit a second five-neighbor KNN model behind `StandardScaler` in one pipeline.
5. Compare holdout accuracy, inspect five-fold cross-validation, and review per-class errors.
## Run locally on Windows
Create and select the `.venv` environment described by the project guide, then run:
```powershell
python train.py
```
The script prints the observed metrics and opens a confusion matrix. The scores are computed at runtime rather than hard-coded into this README.
## Why the scaled pipeline is the defensible model
KNN uses distances between samples. When feature ranges differ substantially, large-magnitude features can dominate those distances. `StandardScaler` puts features on comparable scales, while the pipeline keeps scaling attached to each model fit instead of preprocessing the entire dataset in advance.
## Interview talking points
- Why a baseline should come before optimization
- Why the test set stays untouched until final evaluation
- Why preprocessing belongs inside a pipeline
- What accuracy hides and what the classification report reveals
- Why cross-validation is more persuasive than one split alone
## Next experiment
Tune the neighbor count with cross-validation on the training split, then evaluate the selected pipeline once on the untouched test set.
Commit and publish the project
A meaningful Git commit preserves the finished evaluation plus its documentation as one project milestone. Publishing that history makes the work reproducible from a fresh clone.
- Stage the finished project before creating its documentation commit by running:
git add .
git commit -m "Add evaluation and portfolio documentation"
What Does This Commit Capture?
The first command stages the updated script plus the new README. The second records both changes with the project milestone message.
Your local history now preserves the baseline, the scaling fix, plus the complete evaluation. That sequence shows how the model developed.
Did the Commit Stop?
- Follow the identity configuration guidance printed by Git if this is your first local commit.
- Rerun the commit command after completing that configuration.
Help me complete the Git commit.
- Return to GitHub in your browser.
- Select New repository.
- Enter wine-classifier as the repository name.
- Choose the public visibility option.
- Leave every optional initialization choice unselected.
- Select Create repository.
You should see the empty repository instructions for wine-classifier. This confirms GitHub created the destination without extra starter files.
- Replace OWNER in the command below with your GitHub username.
- Replace REPOSITORY with wine-classifier.
- Connect the local repository to the edited GitHub URL by running:
git remote add origin https://github.com/OWNER/REPOSITORY.git
What Does the Remote Command Do?
The command names your GitHub repository origin inside the local repository. Future push commands can use that short remote name.
- Verify the saved remote URL by running:
git remote -v
What Should the Remote Check Show?
You should see the wine-classifier GitHub URL listed for fetch plus push operations.
The first push can trigger GitHub authentication. Follow the prompt so Git can send the local commit to your account.
Before you push, do you expect the public repository to contain only the latest files or the full three-commit history?
- Publish the local main branch by running:
git push origin main
What Does the Push Publish?
The push sends the current main branch plus its commit history to origin. GitHub can now display every stage of the experiment.
You should see terminal output confirming that the main branch reached the remote repository.
Did the Push Fail?
- Check the URL from the remote verification for a typo in the owner or repository name.
- Complete the GitHub authentication flow if the push is waiting for authorization.
- Confirm that the GitHub repository was created without starter files.
Help me troubleshoot the GitHub push.
- Return to the wine-classifier repository page from earlier.
- Refresh the repository page.
You should see train.py, requirements.txt, .gitignore, plus README.md. The README should render below the file list.
You made the model defensible from three angles: repeatable evaluation, clear documentation, plus reproducible history. Your public repository now shows the complete path from weak baseline to evidence-backed pipeline.
Secret mission
Tune the Neighbor Count Without Touching the Test Set
Your scaled classifier currently uses five neighbors because that value was chosen in advance. This mission turns that fixed choice into a training-only search before evaluating the selected model once on the untouched test set.
Clean Up Your Resources
Clean Up Your Resources
Your classifier uses local tools plus a public GitHub repository. GitHub Free supports the repository, so nothing here incurs an ongoing cost.
Choose whether to retain these resources, pause your local session, or delete everything entirely.
Resources you used:
- The wine-classifier folder containing your script, documentation, dependencies, and Git configuration.
- The .venv virtual environment stored inside wine-classifier.
- The local Git repository on branch main containing your complete commit history.
- The public GitHub repository named wine-classifier containing your published portfolio project.
Keep everything running
No action is needed. Choose this if you want to keep improving the classifier or sharing it with internship reviewers.
- Keep the public GitHub repository available as evidence of your machine learning workflow.
- Retain the local wine-classifier folder for future experiments.
- Leave the .venv environment intact so the pinned packages remain ready to use.
- Preserve the local Git history so each stage of the modeling process remains visible.
Pause - I'll come back to this later
End the active local session while keeping every project file and repository. You can return later without rebuilding the environment.
- Exit the activated .venv session by running this command:
deactivate
This exits the virtual environment. Your terminal prompt should stop showing .venv.
- Close Visual Studio Code when you finish reviewing the project.
- Keep the public GitHub repository unchanged so your published work remains available.
Delete - I don't want to use this again
Deleting both copies is permanent. Choose this only when you no longer need the public repository or local Git history.
- Return to the public GitHub repository named wine-classifier.
- Complete GitHub's repository deletion flow for wine-classifier.
- Refresh the former repository page after confirming the deletion.
You should no longer see the repository files at their previous GitHub location.
- Switch back to the activated terminal in Visual Studio Code.
- Exit the .venv environment by running this command:
deactivate
This releases the active virtual environment before its folder is removed. Your terminal prompt should stop showing .venv.
- Start a Command Prompt terminal from the terminal profile menu in Visual Studio Code.
- Remove the local wine-classifier folder by running these commands:
cd ..
rmdir /s /q wine-classifier
The first command moves to the folder above your project. The second command removes wine-classifier with its .venv environment and local Git history.
- Return to the parent folder in File Explorer to confirm the deletion.
You should no longer see the wine-classifier folder.
Still See the Project Folder?
- Close any open confusion-matrix window before retrying the folder deletion.
- Stop any running Python process before retrying the folder deletion.
- Help me remove the locked project folder.
Nice Work!
Nice Work!
You did it! Your published wine classifier now demonstrates a complete multiclass classification workflow from weak baseline to training-only model selection.
You've learned how to:
- Build an end-to-end classification workflow that keeps an untouched holdout for final evaluation.
- Use an unscaled KNN baseline to expose the feature-scaling problem. Fix that limitation with a leakage-safe pipeline.
- Support your result with five-fold cross-validation. Explain per-class performance through a classification report. Visualize errors with a confusion matrix. Publish the reproducible project to GitHub with meaningful Git history.
- Secret Mission: Tune the neighbor count with training-only cross-validation. Evaluate the selected pipeline once on the untouched test set.
Ready to quiz yourself?