Build an HR Attrition Risk Baseline

Train and save a reproducible HR attrition model with synthetic data.

Introduction

30 Second Summary

Employee turnover can leave a team scrambling to understand what changed. Learning from those patterns safely requires data that cannot expose a real employee.

In this project, you will build a reproducible HR attrition learning system in Python with privacy-safe synthetic data. A scikit-learn classification pipeline lets you compare accuracy with recall on held-out data.

What You'll Build

Run the finished project for a side-by-side demo where the naive baseline misses every attrition case while the trained pipeline produces a risk probability for human review.

By the end of this project, you'll have:

  • Run train.py to generate 240 repeatable synthetic employee records without real personally identifiable information.
  • See a majority-class baseline report zero attrition recall on held-out data. Watch a scaled logistic-regression pipeline improve the result.
  • Use predict.py to reload the saved pipeline for a sample risk probability. Share the code through a public GitHub repository with responsible-use limits.
  • Secret Mission: lower the decision threshold to measure the recall versus false-positive trade-off.

Are there any prerequisites?

You need a Windows computer plus a GitHub account. The first step checks Python, Visual Studio Code with the Microsoft Python extension, plus Git before any project files are created.

Before We Start

This step gives you a moment to commit to what you are building before the hands-on work begins. You are creating a synthetic HR attrition baseline to practise the data, training, evaluation, and persistence stages of the MLOps lifecycle without using it for real employment decisions.

Verify Your Windows Toolchain

The Python interpreter runs every data script in this educational baseline. Visual Studio Code needs its Python extension to work with that interpreter.

Git must also be available from the same Windows terminal. Verifying all three now keeps toolchain problems out of your later model results.

In this step, get ready to:
  • Prepare VS Code with the Microsoft Python extension.
  • Verify that Python 3.11 or newer is available.
  • Verify that Git for Windows is available.
Prepare VS Code

The Python extension adds language support to VS Code. The integrated terminal keeps each version check inside the editor.

  • Press the Windows key to open Windows Search.
  • Type Visual Studio Code into the search bar.
  • Press Enter to open VS Code.
  • Select the Extensions icon in the Activity Bar on the left.
  • Search for Python in the Extensions view.
  • Select the extension named Python that is published by Microsoft.
  • Select Install if that option is available.

The extension page shows a Manage gear after installation. If that gear was already present, the extension was already installed.

  • Select Terminal from the VS Code menu bar.
  • Select New Terminal.

That is the editor side ready. You should see a terminal panel along the bottom of VS Code.

How do these pieces work together?

VS Code is the editor. The Python extension connects its editing features to a separately installed interpreter.

The terminal runs the same commands you use throughout the project. Keeping the checks here confirms that VS Code can reach each tool.

Verify the Python version

The Python extension does not include an interpreter. This project needs Python 3.11 or newer because scikit-learn version 1.9.1 requires that minimum.

  • Check the installed Python 3 version by running this command:
py -3 --version

What does this command check?

The py launcher finds Python installations on Windows. The version option prints the Python 3 release it selects.

✔️ I see a supported Python version

Good. Your interpreter meets the project requirement when the reported version is 3.11 or newer.

ⓧ I see an older Python version

Your current interpreter is below the version required by this project. Install the current 64-bit Python release before continuing.

  • Visit the official Python Windows downloads page.
  • Download the current 64-bit Python installer.
  • Close VS Code before starting the installer.
  • Run the downloaded Python installer.
  • Keep the recommended installation options selected.

A fresh VS Code process reads the updated Windows configuration. Restarting it allows the terminal to find the new interpreter.

  • Press the Windows key after the installation completes.
  • Type Visual Studio Code into Windows Search.
  • Press Enter to start VS Code again.
  • Select Terminal from the menu bar.
  • Select New Terminal.
  • Confirm the updated Python version by running:
py -3 --version

What should this confirm?

The command checks the Python 3 interpreter visible to the fresh terminal. The reported version should now be 3.11 or newer.

ⓧ The Python command fails

Windows cannot currently find a Python 3 interpreter through its launcher. Installing the current 64-bit release provides the missing interpreter.

  • Visit the official Python Windows downloads page.
  • Download the current 64-bit Python installer.
  • Close VS Code before starting the installer.
  • Run the downloaded Python installer.
  • Keep the recommended installation options selected.

The existing terminal does not automatically reload the new Windows configuration. A fresh VS Code session gives the launcher a clean check.

  • Press the Windows key after the installation completes.
  • Type Visual Studio Code into Windows Search.
  • Press Enter to start VS Code again.
  • Select Terminal from the menu bar.
  • Select New Terminal.
  • Confirm that Windows can now find Python by running:
py -3 --version

What should this confirm?

The command asks the Windows Python launcher for its Python 3 interpreter. Success appears as an installed version of 3.11 or newer.

Still seeing the wrong Python version?

  • Close every open VS Code window before starting it again.
  • Create a fresh terminal because an older terminal keeps the environment settings from when it started.
  • Confirm that you downloaded the current 64-bit release from the official Windows page.

Help me fix my Windows Python version check.

Verify Git from the same terminal

Git records the source history that makes this project reproducible. The maintained Git for Windows build for this guide is 2.56.0(2).

  • Check whether Git is available by running this command:
git --version

What does this command check?

The command asks the Git client to print its installed version. A version number confirms that the VS Code terminal can find Git.

✔️ I see the current Git release

That clears the version-control check. Git for Windows is installed and available from VS Code.

ⓧ I see an older Git release

The terminal can find Git, but the installed release predates the build used for this guide. Update it with the official x64 Setup build.

  • Visit the official Git for Windows download page.
  • Download the x64 Setup build.
  • Close VS Code before starting the installer.
  • Run the downloaded Git installer.
  • Keep the default option selected on each setup screen.

The setup wizard includes several configuration screens. Its defaults fit this project.

  • Press the Windows key after the installation completes.
  • Type Visual Studio Code into Windows Search.
  • Press Enter to start VS Code again.
  • Select Terminal from the menu bar.
  • Select New Terminal.
  • Confirm the updated Git installation by running:
git --version

What should this confirm?

The command checks the Git installation visible to the fresh terminal. You should see a Git version number from the updated installation.

ⓧ The Git command fails

The VS Code terminal cannot currently find Git. Install the official Git for Windows x64 Setup build with its default options.

  • Visit the official Git for Windows download page.
  • Download the x64 Setup build.
  • Close VS Code before starting the installer.
  • Run the downloaded Git installer.
  • Keep the default option selected on each setup screen.

Restarting VS Code refreshes the terminal environment after the installation. This allows Windows to expose Git to the new terminal.

  • Press the Windows key after the installation completes.
  • Type Visual Studio Code into Windows Search.
  • Press Enter to start VS Code again.
  • Select Terminal from the menu bar.
  • Select New Terminal.
  • Confirm that the terminal can now find Git by running:
git --version

What should this confirm?

The command asks the newly installed Git client for its version. A printed version number confirms that the installation is available from VS Code.

Before the final check, do you expect both tools to report their versions from this one terminal?

  • Verify the complete Windows toolchain by running both commands:
py -3 --version
git --version

What does this final check prove?

  • The first command proves that Windows can find a supported Python 3 interpreter.
  • The second command proves that Windows can find Git from the same VS Code terminal.

You should see Python 3.11 or newer on the first line. You should see a Git version number on the second line.

Does one check still fail?

  • Close the current terminal with the trash can icon in the terminal panel.
  • Select New Terminal from the Terminal menu.
  • Restart VS Code if the failing tool was installed while the editor was open.

Help me diagnose my Windows toolchain check.

Your Windows toolchain is ready to support a reproducible machine-learning project. Next, you will create an isolated workspace with pinned dependencies.

Set Up the Reproducible Workspace

Your Windows toolchain is ready inside VS Code. A project-specific Python environment keeps this model's dependencies separate from other projects.

This step puts the workspace under Git version control. It pins scikit-learn inside a virtual environment so another computer can rebuild the same setup.

In this step, get ready to:
  • Create the hr-attrition-baseline workspace under local Git version control.
  • Build the isolated sklearn-env virtual environment.
  • Install the pinned scikit-learn dependency.
Create the reproducible workspace

The workspace gives every project file one predictable home. A local repository records future changes from that starting point.

  • In VS Code, click File in the top menu.
  • Click Open Folder.

You'll see the Windows folder picker. This is where you choose the permanent location for the project.

  • Select Desktop in the left sidebar.
  • Click New folder.

A folder name field is ready for the project name.

  • Enter hr-attrition-baseline as the folder name.
  • Click Select Folder.

Your hr-attrition-baseline workspace now fills the VS Code window. Every file you add next lands in the correct folder.

  • Click the Source Control icon in the left Activity Bar.
  • Click Initialize Repository.

That is the first reproducibility layer in place. Git now recognizes hr-attrition-baseline as a local repository.

The requirements.txt file records the exact dependency release. This prevents a future rebuild from silently installing a different version.

  • Click the Explorer icon in the left Activity Bar.
  • Click the New File icon beside the workspace name.
  • Enter requirements.txt in the file name field.
  • Press Enter.

The empty requirements.txt file is now open in the editor.

  • Pin the project dependency by replacing the empty file with this line:
scikit-learn==1.9.1

Why pin the version?

The scikit-learn==1.9.1 requirement tells pip to install one exact release. That fixed release protects the project from dependency drift.

  • Save requirements.txt by pressing Ctrl+S.
  • Confirm the saved file appears in the Explorer sidebar.

The editor now shows one dependency line with the pinned 1.9.1 version.

Can't save requirements.txt?

  • Confirm the Explorer sidebar shows hr-attrition-baseline as the open folder.
  • Check that the file name is exactly requirements.txt.

Ask for help with a requirements.txt saving problem.

✔️ Awesome, I've got everything!

Great. Your saved requirements.txt file contains the exact dependency pin.

ⓧ I'd like to double check the full code

Compare your complete requirements.txt file with this reference.

scikit-learn==1.9.1

The .gitignore file keeps generated folders out of version control. Your repository stays focused on the files another person needs to rebuild the project.

  • Click the New File icon beside the workspace name.
  • Enter .gitignore in the file name field.
  • Press Enter.
  • Add the generated project paths by pasting this content:
sklearn-env/
__pycache__/
artifacts/

What does .gitignore exclude?

  • The sklearn-env/ entry excludes the machine-specific virtual environment.
  • The __pycache__/ entry excludes generated Python cache files.
  • The artifacts/ entry excludes the trained model file created later.
  • Save .gitignore by pressing Ctrl+S.
  • Return to the Source Control view.

You should see requirements.txt plus .gitignore listed as project changes.

Seeing generated files in Source Control?

  • Confirm the file name starts with a period as .gitignore.
  • Check that each ignored path appears on its own line.
  • Save .gitignore before checking Source Control again.

Ask for help with files that Git is not ignoring.

✔️ Awesome, I've got everything!

Your saved .gitignore file protects the repository from generated project files.

ⓧ I'd like to double check the full code

Compare your complete .gitignore file with this reference.

sklearn-env/
__pycache__/
artifacts/
Build the isolated Python environment

The sklearn-env folder stores this project's Python packages. Activating it directs installation commands into that isolated environment.

  • Switch back to the VS Code terminal from earlier.
  • Create the virtual environment inside hr-attrition-baseline by running this command:
python -m venv sklearn-env

What does this command do?

The command asks Python to create an isolated environment named sklearn-env. Its installed packages stay inside the project workspace.

  • Confirm the sklearn-env folder appears in the Explorer sidebar.

The new folder proves that the project-specific environment exists on your computer.

Virtual environment folder missing?

  • Confirm the terminal path ends with hr-attrition-baseline.
  • Check that the earlier Python version test reported 3.11 or newer.

Ask for help creating the virtual environment.

  • Activate the new environment in the current terminal by running this command:
sklearn-env\Scripts\activate

What does activation change?

Activation directs Python package commands to sklearn-env for the current terminal session. This keeps the upcoming installation inside the project.

You should see the environment name at the start of the terminal prompt. That prompt confirms the terminal is using sklearn-env.

Activation blocked on Windows?

PowerShell settings can block environment activation on some Windows computers. Use a Command Prompt terminal profile in VS Code if your current terminal refuses to run the activation script.

Ask for help with a Windows activation problem.

Interpreter menus can be fiddly because they show every Python installation on your computer. The sklearn-env name is the reliable clue for this project.

  • Click View in the VS Code top menu.
  • Click Command Palette.

The Command Palette gives you a searchable list of VS Code actions.

  • Enter Python: Select Interpreter in the Command Palette.
  • Select the interpreter identified by sklearn-env.

The VS Code status bar should now identify sklearn-env as the selected interpreter.

Install and verify scikit-learn

The saved requirements file now becomes an installation recipe. The active environment keeps that recipe scoped to this project.

Package installation can take a minute while pip downloads the dependency. A short pause in the terminal is normal.

  • Return to the activated VS Code terminal from earlier.
  • Install the pinned dependency by running this command:
pip install -r requirements.txt

What does this command install?

The command reads requirements.txt from the current workspace. It installs the pinned scikit-learn release inside the active sklearn-env environment.

When the terminal returns to its prompt without an installation error, the dependency is ready for verification.

Package installation failed?

  • Confirm the terminal prompt shows the active environment name.
  • Check that requirements.txt contains exactly one dependency line.
  • Confirm your internet connection is available for the package download.

Ask for help with the scikit-learn installation error.

Before you run the final check, which two details should prove that the correct package is inside the correct environment?

  • Verify the installed package by running this command:
python -m pip show scikit-learn

What should you see?

  • The package details should include Version: 1.9.1.
  • The reported installation location should sit inside sklearn-env.

That's the workspace locked down. Your local repository now has an isolated dependency setup that another computer can reproduce.

Seeing the wrong version or location?

  • Confirm the terminal prompt still shows the active environment name.
  • Select the sklearn-env interpreter again in VS Code.
  • Run the requirements installation again from the hr-attrition-baseline workspace.

Ask for help with a package version or environment mismatch.

Your reproducible workspace is ready. Next, you will generate the privacy-safe synthetic employee records that power the attrition baseline.

Generate Safe Synthetic HR Data

Your reproducible workspace is ready in VS Code. Now you need employee-like records that are safe to use.

Real employee records carry privacy and consent risks. This step uses deterministic synthetic data so you can learn the modeling workflow without exposing anyone's personal information.

In this step, get ready to:
  • Configure a fixed random seed for repeatable data.
  • Generate synthetic employee rows with binary attrition labels.
  • Confirm identical dataset summaries across two runs.
Create the data generator

A fixed random seed makes generated data repeatable. Each run follows the same sequence of synthetic values.

  • In the Explorer sidebar, use the file-creation icon to create a file inside hr-attrition-baseline.
  • Name the file train.py.
  • Add the import plus the dataset configuration by pasting this code:
from random import Random

RANDOM_SEED = 42
FEATURE_NAMES = [
    "overtime_hours",
    "satisfaction_score",
    "years_at_company",
    "recent_promotion",
]

What Does This Setup Control?

  • The RANDOM_SEED value anchors the pseudo-random sequence. This gives the project reproducibility.
  • The FEATURE_NAMES list records the meaning and order of the four values in each employee row.
  • The feature names describe synthetic work signals. They contain no names or contact details.
  • Save train.py.
  • Confirm train.py appears under hr-attrition-baseline in the Explorer sidebar.

Cannot Find train.py?

Check that train.py sits beside requirements.txt and .gitignore inside hr-attrition-baseline.

Move the file into that folder if it was created elsewhere. Help me place train.py in the correct VS Code workspace folder.

The generator draws every feature from a small range of plausible values. A transparent points rule turns those values into an educational attrition label.

  • In train.py, place your cursor below the closing bracket of FEATURE_NAMES.
  • Add the synthetic dataset generator by pasting this code:
def build_synthetic_dataset(n_samples=240):
    rng = Random(RANDOM_SEED)
    features = []
    labels = []

    for _ in range(n_samples):
        overtime_hours = rng.choice([0, 5, 10, 15])
        satisfaction_score = rng.randint(1, 5)
        years_at_company = rng.randint(0, 12)
        recent_promotion = int(rng.random() < 0.30)

        risk_points = (
            2 * int(overtime_hours >= 10)
            + 2 * int(satisfaction_score <= 2)
            + int(years_at_company <= 1)
            + int(recent_promotion == 0)
        )
        attrition = int(risk_points >= 4)

        features.append(
            [
                overtime_hours,
                satisfaction_score,
                years_at_company,
                recent_promotion,
            ]
        )
        labels.append(attrition)

    return features, labels

How Does the Generator Work?

  • The rng object starts from RANDOM_SEED. It produces the same sequence on every run.
  • The features list stores four numeric values for each synthetic employee. The labels list stores the matching binary outcome.
  • The risk_points rule combines overtime, satisfaction, tenure, and promotion signals into a transparent score.
  • The attrition value becomes 1 when the score reaches the threshold. Every other row receives 0.

The generator now returns feature rows plus their matching labels. The final construct runs that generator and summarizes its output.

  • Place your cursor below return features, labels in train.py.
  • Add the runnable entry point by pasting this code:
def main():
    X, y = build_synthetic_dataset()
    positive_count = sum(y)

    print(f"Generated {len(X)} synthetic employee records.")
    print(
        "Attrition labels: "
        f"{positive_count} yes, {len(y) - positive_count} no"
    )


if __name__ == "__main__":
    main()

What Makes the File Runnable?

  • The main() function generates the dataset. It counts the positive labels.
  • The first print statement reports the number of feature rows. The second reports the two label counts.
  • The final condition calls main() when you run train.py directly.

✔️ Awesome, I've got everything!

Your train.py now contains the configuration, generator, and runnable summary.

ⓧ I'd like to double check the full code

from random import Random

RANDOM_SEED = 42
FEATURE_NAMES = [
    "overtime_hours",
    "satisfaction_score",
    "years_at_company",
    "recent_promotion",
]


def build_synthetic_dataset(n_samples=240):
    rng = Random(RANDOM_SEED)
    features = []
    labels = []

    for _ in range(n_samples):
        overtime_hours = rng.choice([0, 5, 10, 15])
        satisfaction_score = rng.randint(1, 5)
        years_at_company = rng.randint(0, 12)
        recent_promotion = int(rng.random() < 0.30)

        risk_points = (
            2 * int(overtime_hours >= 10)
            + 2 * int(satisfaction_score <= 2)
            + int(years_at_company <= 1)
            + int(recent_promotion == 0)
        )
        attrition = int(risk_points >= 4)

        features.append(
            [
                overtime_hours,
                satisfaction_score,
                years_at_company,
                recent_promotion,
            ]
        )
        labels.append(attrition)

    return features, labels


def main():
    X, y = build_synthetic_dataset()
    positive_count = sum(y)

    print(f"Generated {len(X)} synthetic employee records.")
    print(
        "Attrition labels: "
        f"{positive_count} yes, {len(y) - positive_count} no"
    )


if __name__ == "__main__":
    main()

How to Use This Reference

This reference shows the complete cumulative train.py for this step. Compare each section with your file before running it.

Run the repeatability check

A useful binary classification dataset needs examples from both labels. This first run checks whether the risk rule produced both classes.

Before you run the file, do you think the transparent risk rule produces both attrition labels?

  • Save train.py.
  • Click Run Python File in the editor toolbar.

You'll see Generated 240 synthetic employee records. in the terminal. The next line shows a nonzero yes count.

It also shows a nonzero no count. Your first ML lifecycle stage now produces privacy-safe records on demand.

Before you rerun the file, do you expect either label count to change?

  • Click Run Python File again.

You'll see the same record total and label counts as the first run. The fixed seed makes the dataset deterministic.

You've locked in repeatability. Every later model comparison now starts from the same synthetic records.

Seeing an Error or Different Counts?

  • Compare train.py with the full-code reference. A missing bracket or indentation level can stop the script.
  • Check that RANDOM_SEED = 42 appears exactly once near the top of the file.
  • Check that the generator creates rng with Random(RANDOM_SEED) on every run.

If the issue continues, help me troubleshoot my deterministic synthetic data generator.

Your project now has a stable source of privacy-safe training data. Next up, you'll test a naive baseline and expose why accuracy alone can hide a useless model.

Watch the Naive Baseline Fail

Your synthetic data generator now produces the same 240 privacy-safe records on every run. That repeatability gives you stable material for a fair model check.

A majority-class baseline model can score nonzero accuracy while positive cases remain undetected. In this step, you will use a held-out test set plus recall to expose that gap.

In this step, get ready to:
  • Create reproducible stratified training and test partitions.
  • Fit a majority-class baseline using only the training partition.
  • Compare held-out accuracy with attrition recall.
Create held-out partitions

Train/test separation reserves some records for an honest evaluation. Stratification keeps the attrition label mix represented in both partitions.

  • In the VS Code Explorer sidebar, select train.py.
  • Find the single from random import Random import line.
  • Replace it with the scikit-learn import group below:
from random import Random

from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, recall_score
from sklearn.model_selection import train_test_split

What do these imports add?

  • DummyClassifier provides the simple majority-class comparison.
  • accuracy_score measures the fraction of correct predictions.
  • recall_score measures how many positive attrition cases the predictions identify.
  • train_test_split creates separate training and test partitions.
  • Save train.py.
  • Click Run Python File in the top-right corner of the editor.

The terminal prints the 240-record summary. The command returns to the prompt without an import failure.

Seeing an import failure?

  • Confirm that sklearn-env remains selected as the workspace interpreter.
  • Use Python: Select Interpreter from the Command Palette if another interpreter is active.

Help me fix the scikit-learn import failure in this workspace.

  • Find the summary print block inside main().
  • Place the partitioning block in the blank line after that summary:
    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size=0.25,
        random_state=RANDOM_SEED,
        stratify=y,
    )

How does the split stay trustworthy?

  • test_size=0.25 reserves one quarter of the records for testing.
  • random_state=RANDOM_SEED fixes the partition assignment across reruns.
  • stratify=y preserves the label proportions across both partitions.
  • X_train plus y_train hold the training records.
  • X_test plus y_test hold the untouched evaluation records.
  • Save train.py.
  • Click Run Python File again.

The run prints the same 240-record summary. It completes without a partitioning error.

Does the split fail?

  • Check that the first argument is X.
  • Check that the second argument is y.
  • Confirm that stratify=y uses the same labels created by build_synthetic_dataset().

Help me troubleshoot the train and test split in train.py.

Fit the majority-class baseline

The baseline establishes the easiest score to beat. Its strategy predicts whichever label appears most often in the training partition.

  • Find the closing parenthesis of the train_test_split block.
  • Place the baseline block directly below it:
    baseline = DummyClassifier(strategy="most_frequent")
    baseline.fit(X_train, y_train)
    baseline_predictions = baseline.predict(X_test)

What does the baseline test?

  • DummyClassifier(strategy="most_frequent") identifies the most common training label.
  • Fitting uses only X_train plus y_train.
  • baseline_predictions stores one prediction for every record in X_test.
  • Save train.py.
  • Click Run Python File.

The terminal reaches the dataset summary. It returns to the prompt after fitting the baseline and creating its predictions.

Does the baseline stop the run?

  • Confirm that baseline.fit receives X_train plus y_train.
  • Confirm that baseline.predict receives X_test.
  • Check that the strategy value is exactly most_frequent.

Help me debug the majority-class baseline in train.py.

Measure the baseline

Accuracy counts every correct label. Attrition recall focuses on the positive cases that this HR risk example needs to identify.

  • Find the line that creates baseline_predictions.
  • Place the metric output directly below it:
    print("\nNaive baseline")
    print(f"Accuracy: {accuracy_score(y_test, baseline_predictions):.2f}")
    print(
        "Attrition recall: "
        f"{recall_score(y_test, baseline_predictions, zero_division=0):.2f}"
    )

Why show both metrics?

  • accuracy_score rewards every correct prediction across both labels.
  • recall_score checks how many true attrition labels the baseline catches.
  • Two decimal places make the comparison easy to read in the terminal.
  • Save train.py.

Before you run it, do you think nonzero accuracy guarantees that the baseline catches any attrition cases?

  • Click Run Python File to test your prediction.

You should see Naive baseline followed by a nonzero Accuracy: value. You should also see Attrition recall: 0.00.

That zero recall is the intended shortfall. The majority-class strategy misses every attrition case in the held-out test set.

You caught the trap. Your baseline has measurable accuracy while failing the positive class completely.

Seeing a different baseline result?

  • Confirm that the baseline strategy is strategy="most_frequent".
  • Confirm that the split uses random_state=RANDOM_SEED.
  • Confirm that the split uses stratify=y.

Help me explain why my baseline metrics differ from the expected result.

✔️ Awesome, I've got everything!

Your train.py now creates a reproducible split. It also exposes the majority-class baseline through accuracy and attrition recall.

ⓧ I'd like to double check the full code

from random import Random

from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, recall_score
from sklearn.model_selection import train_test_split

RANDOM_SEED = 42
FEATURE_NAMES = [
    "overtime_hours",
    "satisfaction_score",
    "years_at_company",
    "recent_promotion",
]


def build_synthetic_dataset(n_samples=240):
    rng = Random(RANDOM_SEED)
    features = []
    labels = []

    for _ in range(n_samples):
        overtime_hours = rng.choice([0, 5, 10, 15])
        satisfaction_score = rng.randint(1, 5)
        years_at_company = rng.randint(0, 12)
        recent_promotion = int(rng.random() < 0.30)

        risk_points = (
            2 * int(overtime_hours >= 10)
            + 2 * int(satisfaction_score <= 2)
            + int(years_at_company <= 1)
            + int(recent_promotion == 0)
        )
        attrition = int(risk_points >= 4)

        features.append(
            [
                overtime_hours,
                satisfaction_score,
                years_at_company,
                recent_promotion,
            ]
        )
        labels.append(attrition)

    return features, labels


def main():
    X, y = build_synthetic_dataset()
    positive_count = sum(y)

    print(f"Generated {len(X)} synthetic employee records.")
    print(
        "Attrition labels: "
        f"{positive_count} yes, {len(y) - positive_count} no"
    )

    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size=0.25,
        random_state=RANDOM_SEED,
        stratify=y,
    )

    baseline = DummyClassifier(strategy="most_frequent")
    baseline.fit(X_train, y_train)
    baseline_predictions = baseline.predict(X_test)

    print("\nNaive baseline")
    print(f"Accuracy: {accuracy_score(y_test, baseline_predictions):.2f}")
    print(
        "Attrition recall: "
        f"{recall_score(y_test, baseline_predictions, zero_division=0):.2f}"
    )


if __name__ == "__main__":
    main()

Your held-out evaluation has exposed why one reassuring metric cannot carry the whole decision. Next up, you will train a pipeline that can identify attrition cases and save the fitted artifact for reuse.

Train, Save, Demo, and Publish the Pipeline

Your VS Code workspace now proves why the majority-class baseline falls short. It misses every attrition case in the held-out data.

A scikit-learn pipeline can keep preprocessing tied to the classifier. This gives your MLOps project one fitted artifact that you can evaluate honestly before saving it for reuse.

In this step, get ready to:
  • Train the class-balanced pipeline on the training partition.
  • Save the fitted pipeline before reloading it in a separate prediction program.
  • Publish the source files with Git and GitHub.
Train and persist the pipeline

The model needs consistent feature scaling during training and prediction. A preprocessing pipeline keeps that transformation connected to the logistic-regression classifier.

  • In train.py, find the current import block shown below:
from random import Random

from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, recall_score
from sklearn.model_selection import train_test_split

What are you replacing?

This import block supports the synthetic dataset and the naive baseline. The next version adds the tools required to build the pipeline and save its fitted state.

  • Replace the current import block with this expanded import block:
import pickle
from pathlib import Path
from random import Random

from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

What do these imports add?

  • Path handles the artifact location with a platform-correct file path.
  • StandardScaler learns the feature scaling from the training partition.
  • LogisticRegression provides the binary classifier.
  • make_pipeline connects preprocessing to prediction so the same fitted steps are reused.
  • pickle serializes the fitted pipeline for local reuse.
  • In main(), add this model block below the naive baseline metric output:
    model = make_pipeline(
        StandardScaler(),
        LogisticRegression(class_weight="balanced", max_iter=1000),
    )
    model.fit(X_train, y_train)
    model_predictions = model.predict(X_test)

    print("\nTrained pipeline")
    print(f"Accuracy: {accuracy_score(y_test, model_predictions):.2f}")
    print(
        "Attrition recall: "
        f"{recall_score(y_test, model_predictions, zero_division=0):.2f}"
    )

What does this model block do?

  • make_pipeline applies StandardScaler before LogisticRegression for every fit or prediction call.
  • class_weight="balanced" gives more weight to the less frequent class during training.
  • model.fit() learns only from X_train and y_train.
  • model.predict() evaluates the fitted pipeline against the untouched X_test partition.
  • The printed accuracy and recall make the comparison with the naive baseline visible.
  • Save train.py.

Before you run the file, do you expect the trained pipeline to detect any positive attrition cases?

  • Select Run Python File in the top-right corner of the train.py editor.

You should see the existing Naive baseline section first. The new Trained pipeline section appears below it with held-out accuracy and nonzero attrition recall.

Missing the trained pipeline output?

  • Confirm the new model block remains inside main() with four spaces of indentation.
  • Check that the expanded import block includes StandardScaler and make_pipeline.
  • Confirm the selected interpreter is still sklearn-env.

Help me fix the trained pipeline section in my attrition project.

The fitted model exists only in memory after this run. Persisting the complete pipeline preserves both its learned scaling and its classifier parameters.

  • In train.py, find the current feature-name block shown below:
FEATURE_NAMES = [
    "overtime_hours",
    "satisfaction_score",
    "years_at_company",
    "recent_promotion",
]

Why locate this block?

The artifact path belongs with the other project constants. Placing it after FEATURE_NAMES keeps the reusable configuration together.

  • Replace that block with this version:
FEATURE_NAMES = [
    "overtime_hours",
    "satisfaction_score",
    "years_at_company",
    "recent_promotion",
]
ARTIFACT_PATH = Path("artifacts") / "attrition_pipeline.pkl"

What changed?

ARTIFACT_PATH points to the generated model file inside the artifacts folder. Path builds the location in a form that works on Windows.

  • In main(), add this persistence block below the trained pipeline metric output:
    ARTIFACT_PATH.parent.mkdir(parents=True, exist_ok=True)
    artifact = {
        "model": model,
        "feature_names": FEATURE_NAMES,
    }
    with ARTIFACT_PATH.open("wb") as artifact_file:
        pickle.dump(artifact, artifact_file, pickle.HIGHEST_PROTOCOL)

    print(f"\nSaved trusted local artifact to {ARTIFACT_PATH}")

What does the persistence block do?

  • mkdir creates the artifacts folder when it does not already exist.
  • The artifact dictionary keeps the fitted model beside the exact feature order it expects.
  • pickle.dump writes that dictionary to attrition_pipeline.pkl.
  • The final message confirms which local path received the artifact.

Pickle files can contain executable Python objects. Load only the artifact generated by this trusted local script.

  • Save train.py.

Before you run the file again, which new folder do you expect the training script to create?

  • Select Run Python File in the top-right corner of the train.py editor.

You should see a line beginning with Saved trusted local artifact. The artifacts/attrition_pipeline.pkl file now exists in your workspace.

Artifact file missing?

  • Confirm ARTIFACT_PATH appears below the closing bracket of FEATURE_NAMES.
  • Check that the persistence block remains inside main().
  • Look for a Python error above the baseline output because an earlier failure stops the save block from running.

Help me diagnose why train.py does not create the local pickle artifact.

✔️ Awesome, I've got everything!

  • Confirm train.py is saved.
  • Confirm artifacts/attrition_pipeline.pkl appears in the Explorer sidebar.

ⓧ I'd like to double check the full code

import pickle
from pathlib import Path
from random import Random

from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

RANDOM_SEED = 42
FEATURE_NAMES = [
    "overtime_hours",
    "satisfaction_score",
    "years_at_company",
    "recent_promotion",
]
ARTIFACT_PATH = Path("artifacts") / "attrition_pipeline.pkl"


def build_synthetic_dataset(n_samples=240):
    rng = Random(RANDOM_SEED)
    features = []
    labels = []

    for _ in range(n_samples):
        overtime_hours = rng.choice([0, 5, 10, 15])
        satisfaction_score = rng.randint(1, 5)
        years_at_company = rng.randint(0, 12)
        recent_promotion = int(rng.random() < 0.30)

        risk_points = (
            2 * int(overtime_hours >= 10)
            + 2 * int(satisfaction_score <= 2)
            + int(years_at_company <= 1)
            + int(recent_promotion == 0)
        )
        attrition = int(risk_points >= 4)

        features.append(
            [
                overtime_hours,
                satisfaction_score,
                years_at_company,
                recent_promotion,
            ]
        )
        labels.append(attrition)

    return features, labels


def main():
    X, y = build_synthetic_dataset()
    positive_count = sum(y)

    print(f"Generated {len(X)} synthetic employee records.")
    print(
        "Attrition labels: "
        f"{positive_count} yes, {len(y) - positive_count} no"
    )

    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size=0.25,
        random_state=RANDOM_SEED,
        stratify=y,
    )

    baseline = DummyClassifier(strategy="most_frequent")
    baseline.fit(X_train, y_train)
    baseline_predictions = baseline.predict(X_test)

    print("\nNaive baseline")
    print(f"Accuracy: {accuracy_score(y_test, baseline_predictions):.2f}")
    print(
        "Attrition recall: "
        f"{recall_score(y_test, baseline_predictions, zero_division=0):.2f}"
    )

    model = make_pipeline(
        StandardScaler(),
        LogisticRegression(class_weight="balanced", max_iter=1000),
    )
    model.fit(X_train, y_train)
    model_predictions = model.predict(X_test)

    print("\nTrained pipeline")
    print(f"Accuracy: {accuracy_score(y_test, model_predictions):.2f}")
    print(
        "Attrition recall: "
        f"{recall_score(y_test, model_predictions, zero_division=0):.2f}"
    )

    ARTIFACT_PATH.parent.mkdir(parents=True, exist_ok=True)
    artifact = {
        "model": model,
        "feature_names": FEATURE_NAMES,
    }
    with ARTIFACT_PATH.open("wb") as artifact_file:
        pickle.dump(artifact, artifact_file, pickle.HIGHEST_PROTOCOL)

    print(f"\nSaved trusted local artifact to {ARTIFACT_PATH}")


if __name__ == "__main__":
    main()

What should match?

Compare your imports, constants, model block, and persistence block with this complete file. The baseline logic from the previous step remains unchanged.

Reload and demo the saved model

A persisted model becomes useful when another program can load it without retraining. This separate prediction program proves that the saved preprocessing and classifier work together.

  • In the Explorer sidebar, select the new-file control beside hr-attrition-baseline.
  • Name the new file predict.py.

The Explorer sidebar should now list predict.py beside train.py.

  • Add the complete prediction program to predict.py by pasting this code:
import pickle
from pathlib import Path

ARTIFACT_PATH = Path("artifacts") / "attrition_pipeline.pkl"


def main():
    with ARTIFACT_PATH.open("rb") as artifact_file:
        artifact = pickle.load(artifact_file)

    model = artifact["model"]
    feature_names = artifact["feature_names"]

    synthetic_employee = [15, 1, 1, 0]
    probability = float(model.predict_proba([synthetic_employee])[0][1])
    prediction = int(model.predict([synthetic_employee])[0])

    print("Synthetic input")
    for name, value in zip(feature_names, synthetic_employee):
        print(f"{name}: {value}")

    print(f"\nAttrition probability: {probability:.2%}")
    print(f"Flagged for human review: {'yes' if prediction == 1 else 'no'}")
    print("Education only. Never use this model for real employment decisions.")


if __name__ == "__main__":
    main()

How does the demo work?

  • pickle.load restores the trusted artifact created by train.py.
  • model retrieves the fitted scaler and classifier pipeline.
  • feature_names retrieves the feature order stored during training.
  • synthetic_employee contains a high-risk synthetic example with heavy overtime and low satisfaction.
  • predict_proba returns the estimated attrition probability for the positive class.
  • predict produces the default class decision used for the human-review flag.

The warning keeps the demonstration within its educational purpose. Real employment decisions require validated data and human oversight.

  • Save predict.py.

Before you run the demo, do you expect this synthetic high-risk example to be flagged for review?

  • Select Run Python File in the top-right corner of the predict.py editor.

You should see the four synthetic input values and an attrition probability. The output ends with Flagged for human review: yes and the education-only warning.

Prediction program cannot load the artifact?

  • Run train.py again if artifacts/attrition_pipeline.pkl is missing.
  • Confirm predict.py remains inside the hr-attrition-baseline folder.
  • Load only the artifact generated by your own train.py file.

Help me troubleshoot why predict.py cannot load my locally generated pipeline artifact.

✔️ Awesome, I've got everything!

  • Confirm predict.py is saved.
  • Confirm the terminal prints Flagged for human review: yes.

ⓧ I'd like to double check the full code

import pickle
from pathlib import Path

ARTIFACT_PATH = Path("artifacts") / "attrition_pipeline.pkl"


def main():
    with ARTIFACT_PATH.open("rb") as artifact_file:
        artifact = pickle.load(artifact_file)

    model = artifact["model"]
    feature_names = artifact["feature_names"]

    synthetic_employee = [15, 1, 1, 0]
    probability = float(model.predict_proba([synthetic_employee])[0][1])
    prediction = int(model.predict([synthetic_employee])[0])

    print("Synthetic input")
    for name, value in zip(feature_names, synthetic_employee):
        print(f"{name}: {value}")

    print(f"\nAttrition probability: {probability:.2%}")
    print(f"Flagged for human review: {'yes' if prediction == 1 else 'no'}")
    print("Education only. Never use this model for real employment decisions.")


if __name__ == "__main__":
    main()

What should match?

Compare the artifact path and synthetic input with your file. The final three print statements should report the probability and review flag before showing the responsible-use warning.

Document and publish the project

A reusable model needs documentation that explains how to run it and where its limits begin. The README also tells other developers why the generated artifact stays outside version control.

  • In the Explorer sidebar, select the new-file control beside hr-attrition-baseline.
  • Name the new file README.md.

The Explorer sidebar should now list README.md with your Python files.

  • Add the project documentation to README.md by pasting this content:
# HR Attrition Risk Baseline

A beginner MLOps foundation project that compares a naive majority-class baseline with a reproducible scikit-learn classification pipeline.

## What it demonstrates

- Deterministic synthetic HR data with no real employee PII
- A held-out, stratified train/test split
- Accuracy compared with positive-class recall
- A preprocessing and logistic-regression pipeline
- A locally generated, persisted model artifact
- A separate prediction program that reloads the artifact

## Run the project

1. Select the `sklearn-env` interpreter in VS Code.
2. Open `train.py` and use Run Python File.
3. Open `predict.py` and use Run Python File.

The generated `artifacts` folder is intentionally excluded from Git. Recreate the artifact by running `train.py`.

## Responsible-use limitations

This is a synthetic educational demonstration. It is not validated for real employees and must not be used for hiring, discipline, termination, promotion, compensation, or retention decisions. A real HR system would require representative data, consent and privacy controls, bias and fairness testing, explainability, legal review, human oversight, access controls, monitoring, and an appeal process.

## Next project

Serve the saved pipeline through a tested API and package it with Docker.

What does the README capture?

  • What it demonstrates summarizes reproducibility and held-out evaluation.
  • Run the project gives another developer the execution order.
  • The artifact note explains how to regenerate the excluded model file.
  • Responsible-use limitations prohibit real employment use and name the safeguards a real system would need.
  • Save README.md.
  • Confirm the Explorer sidebar lists README.md beside train.py and predict.py.

README formatting looks incomplete?

  • Confirm the file name ends with .md.
  • Check that every heading begins with the same number of hash characters shown above.
  • Confirm the responsible-use paragraph appears under its own heading.

Help me compare my README with the required project documentation.

✔️ Awesome, I've got everything!

  • Confirm README.md is saved.
  • Confirm the responsible-use limitations prohibit real employment decisions.

ⓧ I'd like to double check the full code

# HR Attrition Risk Baseline

A beginner MLOps foundation project that compares a naive majority-class baseline with a reproducible scikit-learn classification pipeline.

## What it demonstrates

- Deterministic synthetic HR data with no real employee PII
- A held-out, stratified train/test split
- Accuracy compared with positive-class recall
- A preprocessing and logistic-regression pipeline
- A locally generated, persisted model artifact
- A separate prediction program that reloads the artifact

## Run the project

1. Select the `sklearn-env` interpreter in VS Code.
2. Open `train.py` and use Run Python File.
3. Open `predict.py` and use Run Python File.

The generated `artifacts` folder is intentionally excluded from Git. Recreate the artifact by running `train.py`.

## Responsible-use limitations

This is a synthetic educational demonstration. It is not validated for real employees and must not be used for hiring, discipline, termination, promotion, compensation, or retention decisions. A real HR system would require representative data, consent and privacy controls, bias and fairness testing, explainability, legal review, human oversight, access controls, monitoring, and an appeal process.

## Next project

Serve the saved pipeline through a tested API and package it with Docker.

What should match?

Compare every heading and list item with your README. The run instructions and responsible-use limitations should appear exactly once.

Your .gitignore already excludes sklearn-env and artifacts. The Source Control view should therefore show only the source and documentation files you want to publish.

  • Select Source Control in the left activity bar.

Under Changes, you should see .gitignore, requirements.txt, train.py, predict.py, and README.md. The sklearn-env and artifacts folders should be absent from this list.

  • Select the plus control beside .gitignore to stage it.
  • Select the plus control beside requirements.txt to stage it.
  • Select the plus control beside train.py to stage it.
  • Select the plus control beside predict.py to stage it.
  • Select the plus control beside README.md to stage it.
  • Enter Build reproducible attrition baseline in the commit message input box.
  • Select Commit above the staged files.
  • Expand Source Control Graph in the Source Control view.

You should see the Build reproducible attrition baseline commit in local history. That commit now records the five publishable project files.

Commit did not complete?

  • Confirm all five files appear under Staged Changes before committing.
  • Use the identity prompt from VS Code if Git needs an author name or email.
  • Check that the commit message input box contains text.

Help me troubleshoot my first Git commit in VS Code.

Publishing makes the repository visible to everyone on the internet. Your generated artifact and virtual environment remain local because the existing ignore rules exclude them.

What happens during publication?

VS Code may open a browser so you can sign in to GitHub. Return to VS Code after the authorization finishes.

  • Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
  • Type Publish to GitHub into the Command Palette.
  • Select Publish to GitHub from the results.
  • Sign in to GitHub when prompted.
  • Return to VS Code after the authorization finishes.
  • Enter hr-attrition-baseline as the repository name.
  • Choose the public repository option.

VS Code uploads the local commit to a new public GitHub repository. The publication confirmation gives you a route to open that repository.

  • Use the publication confirmation to open the repository on GitHub.

The repository should show .gitignore, requirements.txt, train.py, predict.py, and README.md. It should not show sklearn-env or artifacts.

Publication did not finish?

  • Confirm your GitHub sign-in completed in the browser.
  • Choose another repository name if hr-attrition-baseline is already in use in your account.
  • Confirm the local commit exists in Source Control Graph before publishing.

Help me troubleshoot publishing this committed VS Code repository to GitHub.

Before the final check, what outputs should prove that training, persistence, and artifact reuse all work?

  • Switch back to the train.py editor tab.
  • Select Run Python File.
  • Switch back to the predict.py editor tab.
  • Select Run Python File.

The training run should print the trained-model metrics and Saved trusted local artifact. The prediction run should print a probability and Flagged for human review: yes.

You have completed the full model lifecycle from synthetic data to a reusable local artifact. Your public repository now preserves the code and responsible-use context needed to reproduce that work.

Secret mission

Tune the Decision Threshold

Your trained pipeline turns each risk probability into one default class decision. In this mission, you will lower that decision threshold to 0.35. You will compare the change in attrition recall with the change in false-positive alerts.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your resources available, pause your work for later, or delete everything. Every resource in this project is free, so there are no cloud compute charges or paid API costs to stop.

Resources you used:

  • The local hr-attrition-baseline folder containing your source files, documentation, and local Git repository.
  • The Python virtual environment stored in sklearn-env with scikit-learn 1.9.1 installed.
  • The generated model artifact stored at artifacts/attrition_pipeline.pkl.
  • The public GitHub repository containing your committed source files and documentation.

Keep everything running

No action is needed. Choose this if you want to keep improving the model or use this repository as the foundation for a later API project.

  • Retain the local hr-attrition-baseline folder with its committed source files.
  • Keep the sklearn-env virtual environment ready for future runs.
  • Keep artifacts/attrition_pipeline.pkl available for demonstrations with predict.py.
  • Leave the public GitHub repository available for sharing.

Pause - I'll come back to this later

Pausing ends your current editing session while preserving every local file. Your public repository also remains available on GitHub.

  • Close Visual Studio Code.
  • Leave the hr-attrition-baseline folder in place.
  • Leave the published GitHub repository in place.
  • Return to the hr-attrition-baseline workspace in VS Code when you are ready to continue.
  • Reactivate the existing virtual environment by running this command in the VS Code terminal:
sklearn-env\Scripts\activate

What Does This Command Do?

The activation command restores the isolated Python environment containing scikit-learn. Your project can run again without reinstalling its dependency.

Delete - I don't want to use this again

Deleting these resources is permanent, so copy anything you want to keep before continuing. There are no ongoing costs if you choose to keep them instead.

  • Copy any source files or results you want to preserve outside the hr-attrition-baseline folder.
  • Return to your published hr-attrition-baseline repository on GitHub.
  • Select Settings.
  • Open the General page.
  • Scroll to Danger Zone.
  • Select Delete this repository.
  • Complete the confirmation prompts to delete the public repository.
  • Return to your GitHub profile repository list.

You should no longer see the published repository in that list. This confirms that the online copy has been removed.

The local workspace contains the virtual environment and generated artifact. Removing the workspace deletes those resources with the tracked project files.

  • Return to the existing VS Code terminal for hr-attrition-baseline.
  • Remove the local workspace and display the remaining folders by running these commands:
deactivate
cd ..
rmdir /s /q hr-attrition-baseline
dir

What Do These Commands Remove?

  • The deactivate command exits the active virtual environment.
  • The cd command moves the terminal to the folder above the workspace.
  • The removal command permanently deletes hr-attrition-baseline with sklearn-env and artifacts/attrition_pipeline.pkl.
  • The dir command lists the folders that remain.

You should no longer see hr-attrition-baseline in the directory listing. Your local project resources are now deleted.

Is the Folder Still Listed?

Close any other terminal or File Explorer window using the project folder. Run the deletion commands again from the folder above hr-attrition-baseline.

If the folder still remains, help me safely remove my local Windows project folder.

Nice Work!

Nice Work!

You made it! You built a reproducible HR attrition baseline that moves from deterministic data generation to a reusable local model artifact. Your public repository now captures an MLOps lifecycle with responsible-use limits for sensitive employment decisions.

What you learned:

  • Generated a deterministic 240-record synthetic HR dataset without real employee PII. Kept every run repeatable through RANDOM_SEED = 42.
  • Exposed a baseline with zero attrition recall on held-out data to show why accuracy can hide the positive class. Trained a class-balanced scikit-learn pipeline for leakage-resistant held-out evaluation.
  • Reloaded the saved model artifact through predict.py for a separate human-review demo. Published the reproducible source with responsible-use limitations in a public GitHub repository.
  • Secret Mission: Measured the threshold trade-off between attrition recall and false-positive alerts using 0.35.

Ready to quiz yourself?