AI Vital Signs Monitor with Python

Build an ML anomaly detector that flags abnormal patient vital signs.

Introduction

30 Second Summary

Hospital monitors track a patient's heart rate, blood pressure, and oxygen levels around the clock. When something goes wrong, catching it early can be the difference between a quick intervention and a medical emergency.

In this project, you will build a Python-based vital signs monitoring system that uses machine learning to automatically detect abnormal patient readings. You'll train an AI model, compare it against a traditional statistical method, and visualize how each approach catches dangerous patterns in patient data.

What You'll Build

An anomaly detection pipeline that generates synthetic patient vital signs, trains an Isolation Forest model to flag abnormal readings, and compares its performance against a statistical Z-score baseline.

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

  • A synthetic vital signs dataset generator that produces realistic patient data with known anomalies injected for testing.
  • Two anomaly detection approaches (statistical Z-score and Isolation Forest ML model) with a side-by-side performance comparison.
  • Time-series visualizations showing detected anomalies overlaid on vital sign data, plus a bar chart comparing precision, recall, and F1-score.
  • Secret Mission: a real-time monitoring simulator that streams vital signs and triggers alerts when anomalies are detected.

Are there any prerequisites?

You only need basic Python knowledge (variables, functions, loops). No machine learning experience is required.

Before We Start

You are about to build an AI-powered system that monitors patient vital signs and flags dangerous readings automatically.

Before you can train a machine learning model, you need a working Python environment with the right libraries installed. In this step, you will set up everything from scratch so the rest of the project runs smoothly.

In this step, get ready to:
  • Install Python 3.11+ and verify it works.
  • Create a project folder with a virtual environment and install the required packages.
  • Open the project in VS Code and confirm your imports work.
Install Python and check your version

This project requires Python 3.11 or higher because the libraries you will install (scikit-learn 1.8.0 and pandas 3.0.3) both need it as a minimum.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then typing Terminal (macOS) or PowerShell (Windows) and pressing Enter.
  • Check your Python version by running the command below:

macOS

python3 --version

Windows

python --version

✔️ I see version 3.11 or higher

You are all set. Move on to the next substep.

ⓧ I see an older version

Your system has an older Python that will not work with the packages this project uses.

  • Visit python.org/downloads and download the latest Python 3 installer for your operating system.
  • Run the installer. On Windows, check the Add python.exe to PATH checkbox before clicking Install Now.
  • Close and reopen your terminal, then run the version check command again to confirm you see 3.11 or higher.

macOS path shadowing

macOS may still resolve python3 to an older system version after you install a new one. If the version check still shows an old number, try running python3.11 --version or python3.12 --version directly. Use whichever command returns 3.11+ for the rest of this project.

ⓧ Command not found

Python is not installed on your machine yet.

  • Visit python.org/downloads and download the latest Python 3 installer for your operating system.
  • Run the installer. On Windows, check the Add python.exe to PATH checkbox before clicking Install Now.
  • Close and reopen your terminal, then run the version check command again to confirm you see a version number starting with 3.11 or higher.
Create your project folder and install packages

A virtual environment keeps this project's packages separate from the rest of your system. This prevents version conflicts with other Python projects on your machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir vitals-monitor
cd vitals-monitor
  • Create a virtual environment by running the command below:

macOS

python3 -m venv venv

Windows

python -m venv venv
  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

  • Install the required packages by running:
pip install scikit-learn==1.8.0 pandas==3.0.3 matplotlib==3.10.9 numpy

What are these packages?

  • scikit-learn provides machine learning algorithms including the Isolation Forest model you will train.
  • pandas handles tabular data in DataFrames, perfect for working with rows of vital sign readings.
  • matplotlib creates charts and visualizations so you can see detected anomalies on a graph.
  • numpy powers fast numerical operations and random data generation.

The install may take a minute or two. You should see a Successfully installed message at the end listing all four packages.

Install failing or timing out?

  • Make sure you see (venv) in your prompt. If you do not, re-run the activate command above.
  • If you get a permissions error, ensure you are using the virtual environment's pip and not a system-level one.
  • Check your internet connection. pip needs to download each package from the Python Package Index.

help me troubleshoot my pip install error

Open the project in VS Code and verify your imports
  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the vitals-monitor folder, and click Open.

What is VS Code?

VS Code is a free code editor from Microsoft. If you do not have it installed, download it from code.visualstudio.com. You can also use Cursor or any other code editor you prefer.

  • Create a new file by clicking File then New File.
  • Save it immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it vitals_monitor.py.
  • Add the following import lines to vitals_monitor.py:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, precision_recall_fscore_support

What does this code do?

  • numpy and pandas handle data generation and manipulation.
  • matplotlib.pyplot provides plotting functions for your anomaly visualizations.
  • IsolationForest is the machine learning model you will train to detect abnormal vital signs.
  • StandardScaler normalizes your data so the model treats all vitals equally.
  • classification_report and precision_recall_fscore_support measure how well your model performs.
  • Save vitals_monitor.py.
  • Verify the imports work by running the script in your terminal:

macOS

python3 vitals_monitor.py

Windows

python vitals_monitor.py

The script should finish with no output and no errors. A clean return to the terminal prompt means all packages imported successfully.

Seeing a ModuleNotFoundError?

  • Confirm you still see (venv) in your prompt. If not, re-activate with source venv/bin/activate (macOS) or venv\Scripts\activate (Windows).
  • Make sure your terminal is inside the vitals-monitor folder. Run ls and confirm you see vitals_monitor.py in the output.
  • Re-run the pip install command from the previous substep and check for error messages.

help me fix my ModuleNotFoundError when running vitals_monitor.py

✔️ Awesome, I've got everything!

Great. Double check you have saved vitals_monitor.py and that your virtual environment is still active.

ⓧ I'd like to double check the full code

Here is the complete vitals_monitor.py file at this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, precision_recall_fscore_support

Your environment is ready and every library imported without errors. Next up, you will generate realistic synthetic vital signs data that your AI model will learn from.

Generate Synthetic Vital Signs Data

Your Python environment is ready with all the packages you need. Now it's time to create the patient data that your anomaly detection system will analyze.

Real vital signs datasets require credentialed access and complex processing pipelines. Instead, you'll generate synthetic data that mimics real patient readings. This gives you full control over the anomalies, so you can measure how well your model performs later.

In this step, get ready to:
  • Write a function that generates 950 rows of normal vital sign readings.
  • Inject 50 anomalous readings with dangerously high or low values.
  • Save the dataset to CSV and visualize all vitals on a time-series chart.
Write the normal vitals generator function

Each vital sign has a clinically normal range. Your generator uses NumPy random distributions to produce values within those ranges, then clips any outliers to stay within bounds.

  • In vitals_monitor.py, add the following code below your import statements:


# --- Step 2: Generate Synthetic Vital Signs Data ---

def generate_normal_vitals(n_samples=950):
    """Generate normal vital sign readings."""
    np.random.seed(42)
    data = {
        'timestamp': pd.date_range(start='2024-01-01', periods=n_samples, freq='5min'),
        'heart_rate': np.random.normal(75, 8, n_samples).clip(60, 100),
        'systolic_bp': np.random.normal(120, 10, n_samples).clip(90, 140),
        'temperature': np.random.normal(36.6, 0.3, n_samples).clip(36.1, 37.2),
        'spo2': np.random.normal(97.5, 1.0, n_samples).clip(95, 100),
        'respiratory_rate': np.random.normal(16, 2, n_samples).clip(12, 20),
    }
    return pd.DataFrame(data)

What does this code do?

This function creates a DataFrame with 950 rows of patient vital signs, each representing a 5-minute reading interval starting from January 1, 2024.

  • np.random.seed(42) ensures everyone gets the same "random" data for reproducible results.
  • np.random.normal(mean, std, count) generates values that cluster around a realistic center (e.g., 75 bpm for heart rate).
  • .clip(min, max) caps any values that drift outside normal clinical ranges.

You won't see any output yet. This function is defined but not called until a later chunk.

Inject anomalous readings into the dataset

A dataset of only normal readings wouldn't teach your model anything. You need labeled anomalies that simulate real medical emergencies like tachycardia, hypoxia, and fever.

  • Add this function directly below generate_normal_vitals:


def inject_anomalies(df, n_anomalies=50):
    """Inject anomalous vital sign readings."""
    np.random.seed(123)
    anomaly_indices = np.random.choice(len(df), n_anomalies, replace=False)

    for idx in anomaly_indices:
        anomaly_type = np.random.choice(['tachycardia', 'bradycardia', 'hypoxia', 'fever', 'hypotension'])

        if anomaly_type == 'tachycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(150, 200)
        elif anomaly_type == 'bradycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(30, 45)
        elif anomaly_type == 'hypoxia':
            df.loc[idx, 'spo2'] = np.random.uniform(70, 88)
        elif anomaly_type == 'fever':
            df.loc[idx, 'temperature'] = np.random.uniform(39.0, 41.0)
        elif anomaly_type == 'hypotension':
            df.loc[idx, 'systolic_bp'] = np.random.uniform(50, 75)

    df['is_anomaly'] = 0
    df.loc[anomaly_indices, 'is_anomaly'] = 1
    return df

What does this code do?

This function picks 50 random rows and overwrites one vital sign with a dangerously abnormal value. Each anomaly simulates a real medical condition:

  • Tachycardia: heart rate spikes to 150-200 bpm.
  • Bradycardia: heart rate drops to 30-45 bpm.
  • Hypoxia: SpO2 falls to 70-88% (dangerously low oxygen).
  • Fever: temperature rises to 39-41°C.
  • Hypotension: systolic BP drops to 50-75 mmHg.

The is_anomaly column labels each row as 0 (normal) or 1 (anomalous). You'll use these labels later to measure how accurately your model detects problems.

You won't see any output yet. The next chunk calls both functions and produces visible results.

Generate the dataset and visualize it

Now you'll call both functions to create your 1000-row dataset, save it to a CSV file, and plot every vital sign as a time-series chart. The chart helps confirm your data looks realistic before you train a model on it.

  • Add this code below the inject_anomalies function to generate and save the dataset:


# Generate the dataset
print("Generating synthetic vital signs data...")
vitals_df = generate_normal_vitals(950)
vitals_df = inject_anomalies(vitals_df, 50)
vitals_df.to_csv('vital_signs_data.csv', index=False)
print(f"Dataset created: {len(vitals_df)} readings, {vitals_df['is_anomaly'].sum()} anomalies")

What does this code do?

  • generate_normal_vitals(950) creates 950 rows of healthy patient readings.
  • inject_anomalies(vitals_df, 50) corrupts 50 of those rows with dangerous values (5% anomaly rate).
  • .to_csv('vital_signs_data.csv') saves the full dataset for later use or sharing.
  • Save vitals_monitor.py.
  • Run the script in your terminal by running:
python vitals_monitor.py
  • You should see output confirming that 1000 readings were created with 50 anomalies.

Seeing a NameError or ModuleNotFoundError?

  • Make sure your virtual environment is still activated. You should see the environment name in your terminal prompt.
  • Check that your import statements at the top of vitals_monitor.py include import numpy as np and import pandas as pd.

Help me fix this import or module error in my vitals_monitor.py script.

Now add the visualization code to plot all five vital signs as time-series charts with anomalies highlighted in red.

  • Add this code directly below the dataset generation block:

# Visualize raw data
fig, axes = plt.subplots(3, 2, figsize=(14, 10))
vital_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']
titles = ['Heart Rate (bpm)', 'Systolic BP (mmHg)', 'Temperature (\u00b0C)', 'SpO2 (%)', 'Respiratory Rate (breaths/min)']

for i, (col, title) in enumerate(zip(vital_cols, titles)):
    ax = axes[i // 2, i % 2]
    ax.plot(vitals_df.index, vitals_df[col], alpha=0.7, linewidth=0.5)
    anomaly_mask = vitals_df['is_anomaly'] == 1
    ax.scatter(vitals_df.index[anomaly_mask], vitals_df[col][anomaly_mask],
               color='red', s=20, zorder=5, label='Anomaly')
    ax.set_title(title)
    ax.legend()

axes[2, 1].set_visible(False)
plt.tight_layout()
plt.savefig('raw_vitals.png', dpi=100)
plt.close()
print("Raw data visualization saved to raw_vitals.png")

What does this code do?

  • plt.subplots(3, 2) creates a 3x2 grid of charts (one for each vital sign, with the 6th cell hidden).
  • The loop plots each vital as a blue time-series line, then overlays red dots on any row where is_anomaly == 1.
  • plt.savefig('raw_vitals.png') writes the chart to disk so you can review it without re-running the script.
  • Save vitals_monitor.py and run the script again:
python vitals_monitor.py
  • You should see both confirmation messages in your terminal, plus two new files in your project folder: vital_signs_data.csv and raw_vitals.png.
  • Open raw_vitals.png from the file explorer in VS Code. You should see five time-series charts with scattered red dots representing the injected anomalies.

Don't see raw_vitals.png in your folder?

  • Make sure you have the plt.savefig('raw_vitals.png', dpi=100) line before plt.close(). If you close the figure first, the save produces an empty file.
  • Check your terminal for any error messages. A typo in a column name like a missing underscore will cause a KeyError.

Help me figure out why my matplotlib chart isn't saving correctly.

✔️ Awesome, I've got everything!

Great. Double check that you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete vitals_monitor.py file at this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, precision_recall_fscore_support


# --- Step 2: Generate Synthetic Vital Signs Data ---

def generate_normal_vitals(n_samples=950):
    """Generate normal vital sign readings."""
    np.random.seed(42)
    data = {
        'timestamp': pd.date_range(start='2024-01-01', periods=n_samples, freq='5min'),
        'heart_rate': np.random.normal(75, 8, n_samples).clip(60, 100),
        'systolic_bp': np.random.normal(120, 10, n_samples).clip(90, 140),
        'temperature': np.random.normal(36.6, 0.3, n_samples).clip(36.1, 37.2),
        'spo2': np.random.normal(97.5, 1.0, n_samples).clip(95, 100),
        'respiratory_rate': np.random.normal(16, 2, n_samples).clip(12, 20),
    }
    return pd.DataFrame(data)


def inject_anomalies(df, n_anomalies=50):
    """Inject anomalous vital sign readings."""
    np.random.seed(123)
    anomaly_indices = np.random.choice(len(df), n_anomalies, replace=False)

    for idx in anomaly_indices:
        anomaly_type = np.random.choice(['tachycardia', 'bradycardia', 'hypoxia', 'fever', 'hypotension'])

        if anomaly_type == 'tachycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(150, 200)
        elif anomaly_type == 'bradycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(30, 45)
        elif anomaly_type == 'hypoxia':
            df.loc[idx, 'spo2'] = np.random.uniform(70, 88)
        elif anomaly_type == 'fever':
            df.loc[idx, 'temperature'] = np.random.uniform(39.0, 41.0)
        elif anomaly_type == 'hypotension':
            df.loc[idx, 'systolic_bp'] = np.random.uniform(50, 75)

    df['is_anomaly'] = 0
    df.loc[anomaly_indices, 'is_anomaly'] = 1
    return df


# Generate the dataset
print("Generating synthetic vital signs data...")
vitals_df = generate_normal_vitals(950)
vitals_df = inject_anomalies(vitals_df, 50)
vitals_df.to_csv('vital_signs_data.csv', index=False)
print(f"Dataset created: {len(vitals_df)} readings, {vitals_df['is_anomaly'].sum()} anomalies")

# Visualize raw data
fig, axes = plt.subplots(3, 2, figsize=(14, 10))
vital_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']
titles = ['Heart Rate (bpm)', 'Systolic BP (mmHg)', 'Temperature (\u00b0C)', 'SpO2 (%)', 'Respiratory Rate (breaths/min)']

for i, (col, title) in enumerate(zip(vital_cols, titles)):
    ax = axes[i // 2, i % 2]
    ax.plot(vitals_df.index, vitals_df[col], alpha=0.7, linewidth=0.5)
    anomaly_mask = vitals_df['is_anomaly'] == 1
    ax.scatter(vitals_df.index[anomaly_mask], vitals_df[col][anomaly_mask],
               color='red', s=20, zorder=5, label='Anomaly')
    ax.set_title(title)
    ax.legend()

axes[2, 1].set_visible(False)
plt.tight_layout()
plt.savefig('raw_vitals.png', dpi=100)
plt.close()
print("Raw data visualization saved to raw_vitals.png")

Your synthetic dataset is ready with clearly labeled anomalies. Next up, you'll build a statistical baseline that tries to detect those anomalies using Z-scores.

Build a Statistical Baseline with Z-Scores

You have a dataset with 1000 vital sign readings and 50 known anomalies. The next challenge is detecting those anomalies automatically.

Before jumping to machine learning, you'll build a simple statistical baseline using Z-scores. This gives you a benchmark to compare against later when you train the Isolation Forest model.

In this step, get ready to:
  • Calculate Z-scores for each vital sign and flag anomalous readings.
  • Evaluate your Z-score predictions with precision, recall, and F1-score.
  • Run the script and interpret how well the statistical method performs.
Calculate Z-scores for each vital sign

A Z-score measures how many standard deviations a value is from the mean. If a heart rate reading is 3+ standard deviations away from average, something is likely wrong.

  • In vitals_monitor.py, scroll to the bottom of the file (after the plt.close() line) and add the Z-score calculation code:
# --- Step 3: Z-Score Statistical Baseline ---

print("\n--- Z-Score Anomaly Detection ---")
feature_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']

# Calculate Z-scores
z_scores = vitals_df[feature_cols].apply(lambda x: (x - x.mean()) / x.std())
z_score_predictions = (z_scores.abs() > 3).any(axis=1).astype(int)

What does this code do?

feature_cols defines the five vital sign columns you want to analyze. These are the same columns you generated in Step 2.

The .apply(lambda x: (x - x.mean()) / x.std()) line computes a Z-score for every value in each column. For example, a heart rate of 180 bpm might have a Z-score of 13, meaning it is 13 standard deviations above the average.

The final line flags a reading as anomalous if ANY of its five vital signs has an absolute Z-score greater than 3. This threshold is a common statistical convention for identifying outliers.

  • Save vitals_monitor.py.

You won't see a change yet. The next chunk adds evaluation code that prints results to the terminal.

Evaluate your Z-score predictions

You have predictions, but how good are they? Since you know which readings are truly anomalous (the is_anomaly column), you can measure detection accuracy with precision, recall, and F1-score.

  • Add the evaluation code directly below the Z-score calculation you just wrote:
# Evaluate Z-score method
print("\nZ-Score Method Results:")
print(classification_report(vitals_df['is_anomaly'], z_score_predictions,
                            target_names=['Normal', 'Anomaly']))

z_precision, z_recall, z_f1, _ = precision_recall_fscore_support(
    vitals_df['is_anomaly'], z_score_predictions, average='binary')

What does this code do?

classification_report prints a formatted table showing how well your predictions match reality. It compares vitals_df['is_anomaly'] (the truth) against z_score_predictions (your guesses).

precision_recall_fscore_support extracts the same metrics as individual numbers. You store them in z_precision, z_recall, and z_f1 so you can compare them to the Isolation Forest results later.

What are precision, recall, and F1-score?

  • Precision: Of all readings flagged as anomalous, what percentage were actually anomalous? High precision means few false alarms.
  • Recall: Of all truly anomalous readings, what percentage did you catch? High recall means few missed anomalies.
  • F1-score: The harmonic mean of precision and recall. A single number that balances both concerns.
Run the script and review your results
  • Save vitals_monitor.py.
  • Run your script in the terminal:
python vitals_monitor.py

You should see the Z-Score Method Results table printed in your terminal, showing precision, recall, and f1-score columns for both Normal and Anomaly classes.

How do I read the classification report?

The output is a table with columns for precision, recall, f1-score, and support. Look at the Anomaly row specifically. A low recall means the Z-score method missed many real anomalies. A low precision means it flagged too many normal readings as anomalous.

The Z-score method only catches anomalies that are extreme in a single vital sign. In the next step, you'll train an Isolation Forest that can catch patterns across multiple vitals simultaneously.

Not seeing any output?

  • Make sure you saved vitals_monitor.py before running the script.
  • Check that your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
  • Verify the new code is placed after the print("Raw data visualization saved to raw_vitals.png") line, not inside any function.

Help me debug my Z-score anomaly detection output.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete vitals_monitor.py file up to this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, precision_recall_fscore_support


# --- Step 2: Generate Synthetic Vital Signs Data ---

def generate_normal_vitals(n_samples=950):
    """Generate normal vital sign readings."""
    np.random.seed(42)
    data = {
        'timestamp': pd.date_range(start='2024-01-01', periods=n_samples, freq='5min'),
        'heart_rate': np.random.normal(75, 8, n_samples).clip(60, 100),
        'systolic_bp': np.random.normal(120, 10, n_samples).clip(90, 140),
        'temperature': np.random.normal(36.6, 0.3, n_samples).clip(36.1, 37.2),
        'spo2': np.random.normal(97.5, 1.0, n_samples).clip(95, 100),
        'respiratory_rate': np.random.normal(16, 2, n_samples).clip(12, 20),
    }
    return pd.DataFrame(data)


def inject_anomalies(df, n_anomalies=50):
    """Inject anomalous vital sign readings."""
    np.random.seed(123)
    anomaly_indices = np.random.choice(len(df), n_anomalies, replace=False)

    for idx in anomaly_indices:
        anomaly_type = np.random.choice(['tachycardia', 'bradycardia', 'hypoxia', 'fever', 'hypotension'])

        if anomaly_type == 'tachycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(150, 200)
        elif anomaly_type == 'bradycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(30, 45)
        elif anomaly_type == 'hypoxia':
            df.loc[idx, 'spo2'] = np.random.uniform(70, 88)
        elif anomaly_type == 'fever':
            df.loc[idx, 'temperature'] = np.random.uniform(39.0, 41.0)
        elif anomaly_type == 'hypotension':
            df.loc[idx, 'systolic_bp'] = np.random.uniform(50, 75)

    df['is_anomaly'] = 0
    df.loc[anomaly_indices, 'is_anomaly'] = 1
    return df


# Generate the dataset
print("Generating synthetic vital signs data...")
vitals_df = generate_normal_vitals(950)
vitals_df = inject_anomalies(vitals_df, 50)
vitals_df.to_csv('vital_signs_data.csv', index=False)
print(f"Dataset created: {len(vitals_df)} readings, {vitals_df['is_anomaly'].sum()} anomalies")

# Visualize raw data
fig, axes = plt.subplots(3, 2, figsize=(14, 10))
vital_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']
titles = ['Heart Rate (bpm)', 'Systolic BP (mmHg)', 'Temperature (\u00b0C)', 'SpO2 (%)', 'Respiratory Rate (breaths/min)']

for i, (col, title) in enumerate(zip(vital_cols, titles)):
    ax = axes[i // 2, i % 2]
    ax.plot(vitals_df.index, vitals_df[col], alpha=0.7, linewidth=0.5)
    anomaly_mask = vitals_df['is_anomaly'] == 1
    ax.scatter(vitals_df.index[anomaly_mask], vitals_df[col][anomaly_mask],
               color='red', s=20, zorder=5, label='Anomaly')
    ax.set_title(title)
    ax.legend()

axes[2, 1].set_visible(False)
plt.tight_layout()
plt.savefig('raw_vitals.png', dpi=100)
plt.close()
print("Raw data visualization saved to raw_vitals.png")


# --- Step 3: Z-Score Statistical Baseline ---

print("\n--- Z-Score Anomaly Detection ---")
feature_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']

# Calculate Z-scores
z_scores = vitals_df[feature_cols].apply(lambda x: (x - x.mean()) / x.std())
z_score_predictions = (z_scores.abs() > 3).any(axis=1).astype(int)

# Evaluate Z-score method
print("\nZ-Score Method Results:")
print(classification_report(vitals_df['is_anomaly'], z_score_predictions,
                            target_names=['Normal', 'Anomaly']))

z_precision, z_recall, z_f1, _ = precision_recall_fscore_support(
    vitals_df['is_anomaly'], z_score_predictions, average='binary')

You now have a statistical baseline that detects anomalies one vital sign at a time. Next up, you'll train an Isolation Forest model that can detect patterns across multiple vitals simultaneously.

Train an Isolation Forest Model

Your Z-score detector from the previous step works by checking each vital sign independently. If a single reading deviates far enough from the average, it gets flagged.

The problem is that real patient deterioration often shows up as subtle shifts across multiple vitals at once. A patient with a slightly elevated heart rate AND slightly low SpO2 might be in trouble, even though neither value alone triggers a Z-score alarm. Isolation Forest is a machine learning model that considers all vital signs together, catching multivariate patterns that single-variable methods miss.

In this step, get ready to:
  • Scale vital signs data using StandardScaler for consistent model input.
  • Train an Isolation Forest model to detect multivariate anomalies.
  • Evaluate the model's predictions and compare its performance to the Z-score baseline.
Scale the vital signs features

Machine learning models work best when all input features are on the same scale. Heart rate ranges from 60-200, while temperature ranges from 36-41. Without scaling, the model would treat heart rate as far more important simply because its numbers are bigger.

  • In vitals_monitor.py, find the line that says z_precision, z_recall, z_f1, _ = precision_recall_fscore_support( (the last line of your Z-score section).
  • After that block, add the following code to prepare your features for the Isolation Forest:


# --- Step 4: Isolation Forest Model ---

print("\n--- Isolation Forest Anomaly Detection ---")

# Prepare features
scaler = StandardScaler()
X = vitals_df[feature_cols].values
X_scaled = scaler.fit_transform(X)

What does this code do?

  • StandardScaler() creates a scaler that transforms each column to have a mean of 0 and a standard deviation of 1.
  • vitals_df[feature_cols].values extracts just the five vital sign columns as a NumPy array, dropping the timestamp and label columns.
  • fit_transform(X) learns the mean and standard deviation of each column, then applies the transformation in one step.

You won't see a change yet. The next chunk uses this scaled data to train the model.

Train the Isolation Forest and generate predictions

Isolation Forest works by randomly partitioning data points. Anomalies are isolated in fewer partitions because they sit far from normal patterns. The model learns what "normal" looks like, then flags anything that is too easy to isolate.

  • Directly below the scaling code you just added, paste the following to train the model and generate predictions:

# Train Isolation Forest
iso_forest = IsolationForest(
    n_estimators=100,
    contamination=0.05,
    random_state=42
)
iso_forest.fit(X_scaled)

# Predict: IsolationForest returns -1 for anomalies, 1 for normal
iso_predictions_raw = iso_forest.predict(X_scaled)
iso_predictions = (iso_predictions_raw == -1).astype(int)

What does this code do?

  • n_estimators=100 builds 100 random trees. More trees give more stable predictions.
  • contamination=0.05 tells the model to expect roughly 5% of data points to be anomalies. This matches the 50 anomalies injected into 1000 total readings.
  • random_state=42 makes results reproducible. You'll get the same predictions every time you run the script.
  • The predict() method returns -1 for anomalies and 1 for normal readings. The last line converts this to 0/1 format so it matches your is_anomaly labels.

Why Isolation Forest instead of deep learning?

Deep learning approaches (like LSTM Autoencoders) require GPUs, more data, and longer training times. Isolation Forest is part of scikit-learn, trains in seconds on a CPU, and produces interpretable results. It teaches the same core anomaly detection concepts without infrastructure overhead.

You won't see visible output yet. The next chunk evaluates the predictions and prints a report.

Evaluate the model and compare to Z-score
  • Directly below the prediction code, add the evaluation logic that prints a classification report and stores the performance metrics:

# Evaluate Isolation Forest
print("\nIsolation Forest Results:")
print(classification_report(vitals_df['is_anomaly'], iso_predictions,
                            target_names=['Normal', 'Anomaly']))

iso_precision, iso_recall, iso_f1, _ = precision_recall_fscore_support(
    vitals_df['is_anomaly'], iso_predictions, average='binary')

What does this code do?

  • classification_report prints precision, recall, and F1-score for both the "Normal" and "Anomaly" classes. This shows how well the model distinguishes between them.
  • precision_recall_fscore_support stores those same metrics as individual variables so you can use them for the comparison chart in the next step.
  • Save vitals_monitor.py.
  • Run the script in your terminal by running:
python vitals_monitor.py
  • In the terminal output, you should see an "Isolation Forest Results:" section with a classification report table showing precision, recall, and F1-score for both Normal and Anomaly classes.

How do these metrics compare?

Look at the recall for the Anomaly class in both reports. Recall measures what percentage of true anomalies the method caught. The Isolation Forest should catch more anomalies than Z-scores because it considers relationships between all five vital signs simultaneously.

Z-scores only flag a reading when a single vital sign is extreme. Isolation Forest can flag a reading where multiple vitals are slightly off together.

Seeing an import error or NameError?

  • If you see a NameError for StandardScaler or IsolationForest, confirm the imports at the top of your file include from sklearn.ensemble import IsolationForest and from sklearn.preprocessing import StandardScaler.
  • Make sure your virtual environment is activated. You should see the environment name in your terminal prompt.

Help me debug my Isolation Forest training code

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete vitals_monitor.py file up to this point:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, precision_recall_fscore_support


# --- Step 2: Generate Synthetic Vital Signs Data ---

def generate_normal_vitals(n_samples=950):
    """Generate normal vital sign readings."""
    np.random.seed(42)
    data = {
        'timestamp': pd.date_range(start='2024-01-01', periods=n_samples, freq='5min'),
        'heart_rate': np.random.normal(75, 8, n_samples).clip(60, 100),
        'systolic_bp': np.random.normal(120, 10, n_samples).clip(90, 140),
        'temperature': np.random.normal(36.6, 0.3, n_samples).clip(36.1, 37.2),
        'spo2': np.random.normal(97.5, 1.0, n_samples).clip(95, 100),
        'respiratory_rate': np.random.normal(16, 2, n_samples).clip(12, 20),
    }
    return pd.DataFrame(data)


def inject_anomalies(df, n_anomalies=50):
    """Inject anomalous vital sign readings."""
    np.random.seed(123)
    anomaly_indices = np.random.choice(len(df), n_anomalies, replace=False)

    for idx in anomaly_indices:
        anomaly_type = np.random.choice(['tachycardia', 'bradycardia', 'hypoxia', 'fever', 'hypotension'])

        if anomaly_type == 'tachycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(150, 200)
        elif anomaly_type == 'bradycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(30, 45)
        elif anomaly_type == 'hypoxia':
            df.loc[idx, 'spo2'] = np.random.uniform(70, 88)
        elif anomaly_type == 'fever':
            df.loc[idx, 'temperature'] = np.random.uniform(39.0, 41.0)
        elif anomaly_type == 'hypotension':
            df.loc[idx, 'systolic_bp'] = np.random.uniform(50, 75)

    df['is_anomaly'] = 0
    df.loc[anomaly_indices, 'is_anomaly'] = 1
    return df


# Generate the dataset
print("Generating synthetic vital signs data...")
vitals_df = generate_normal_vitals(950)
vitals_df = inject_anomalies(vitals_df, 50)
vitals_df.to_csv('vital_signs_data.csv', index=False)
print(f"Dataset created: {len(vitals_df)} readings, {vitals_df['is_anomaly'].sum()} anomalies")

# Visualize raw data
fig, axes = plt.subplots(3, 2, figsize=(14, 10))
vital_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']
titles = ['Heart Rate (bpm)', 'Systolic BP (mmHg)', 'Temperature (°C)', 'SpO2 (%)', 'Respiratory Rate (breaths/min)']

for i, (col, title) in enumerate(zip(vital_cols, titles)):
    ax = axes[i // 2, i % 2]
    ax.plot(vitals_df.index, vitals_df[col], alpha=0.7, linewidth=0.5)
    anomaly_mask = vitals_df['is_anomaly'] == 1
    ax.scatter(vitals_df.index[anomaly_mask], vitals_df[col][anomaly_mask],
               color='red', s=20, zorder=5, label='Anomaly')
    ax.set_title(title)
    ax.legend()

axes[2, 1].set_visible(False)
plt.tight_layout()
plt.savefig('raw_vitals.png', dpi=100)
plt.close()
print("Raw data visualization saved to raw_vitals.png")


# --- Step 3: Z-Score Statistical Baseline ---

print("\n--- Z-Score Anomaly Detection ---")
feature_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']

# Calculate Z-scores
z_scores = vitals_df[feature_cols].apply(lambda x: (x - x.mean()) / x.std())
z_score_predictions = (z_scores.abs() > 3).any(axis=1).astype(int)

# Evaluate Z-score method
print("\nZ-Score Method Results:")
print(classification_report(vitals_df['is_anomaly'], z_score_predictions,
                            target_names=['Normal', 'Anomaly']))

z_precision, z_recall, z_f1, _ = precision_recall_fscore_support(
    vitals_df['is_anomaly'], z_score_predictions, average='binary')


# --- Step 4: Isolation Forest Model ---

print("\n--- Isolation Forest Anomaly Detection ---")

# Prepare features
scaler = StandardScaler()
X = vitals_df[feature_cols].values
X_scaled = scaler.fit_transform(X)

# Train Isolation Forest
iso_forest = IsolationForest(
    n_estimators=100,
    contamination=0.05,
    random_state=42
)
iso_forest.fit(X_scaled)

# Predict: IsolationForest returns -1 for anomalies, 1 for normal
iso_predictions_raw = iso_forest.predict(X_scaled)
iso_predictions = (iso_predictions_raw == -1).astype(int)

# Evaluate Isolation Forest
print("\nIsolation Forest Results:")
print(classification_report(vitals_df['is_anomaly'], iso_predictions,
                            target_names=['Normal', 'Anomaly']))

iso_precision, iso_recall, iso_f1, _ = precision_recall_fscore_support(
    vitals_df['is_anomaly'], iso_predictions, average='binary')

You now have two anomaly detection methods with measurable results. Next up, you'll visualize both methods side by side to make the performance difference clear at a glance.

Visualize and Compare Results

You now have precision, recall, and F1 scores for both the Z-score baseline and the Isolation Forest model. Numbers in a terminal are useful, but visualizations make the performance differences immediately obvious.

In this step, you will create side-by-side charts showing where each method flags anomalies, build a bar chart comparing their metrics, and print a final summary explaining which approach wins and why.

In this step, get ready to:
  • Create a 2x2 subplot comparing Z-score and Isolation Forest anomaly detections.
  • Build a bar chart comparing precision, recall, and F1-score for both methods.
  • Print a final summary explaining which method performed better.
Create the anomaly comparison plot

Seeing where each method places its red flags on the same data tells you more than any single number. You will plot heart rate and SpO2 side by side for both detection methods so the differences jump out visually.

  • In vitals_monitor.py, scroll to the bottom of your file (after the Isolation Forest evaluation section).
  • Add the 2x2 comparison visualization by copying this code at the end of the file:
# --- Step 5: Visualize and Compare Results ---

# 2x2 subplot: Z-score vs Isolation Forest on Heart Rate and SpO2
fig, axes = plt.subplots(2, 2, figsize=(14, 8))

# Z-score detections on Heart Rate
ax = axes[0, 0]
ax.plot(vitals_df.index, vitals_df['heart_rate'], alpha=0.7, linewidth=0.5, color='steelblue')
z_anomaly_mask = z_score_predictions == 1
ax.scatter(vitals_df.index[z_anomaly_mask], vitals_df['heart_rate'][z_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Z-Score: Heart Rate')
ax.set_ylabel('BPM')
ax.legend()

# Z-score detections on SpO2
ax = axes[0, 1]
ax.plot(vitals_df.index, vitals_df['spo2'], alpha=0.7, linewidth=0.5, color='steelblue')
ax.scatter(vitals_df.index[z_anomaly_mask], vitals_df['spo2'][z_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Z-Score: SpO2')
ax.set_ylabel('%')
ax.legend()

# Isolation Forest detections on Heart Rate
ax = axes[1, 0]
ax.plot(vitals_df.index, vitals_df['heart_rate'], alpha=0.7, linewidth=0.5, color='steelblue')
iso_anomaly_mask = iso_predictions == 1
ax.scatter(vitals_df.index[iso_anomaly_mask], vitals_df['heart_rate'][iso_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Isolation Forest: Heart Rate')
ax.set_ylabel('BPM')
ax.legend()

# Isolation Forest detections on SpO2
ax = axes[1, 1]
ax.plot(vitals_df.index, vitals_df['spo2'], alpha=0.7, linewidth=0.5, color='steelblue')
ax.scatter(vitals_df.index[iso_anomaly_mask], vitals_df['spo2'][iso_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Isolation Forest: SpO2')
ax.set_ylabel('%')
ax.legend()

plt.tight_layout()
plt.savefig('anomaly_comparison.png', dpi=100)
plt.close()
print("\nComparison visualization saved to anomaly_comparison.png")

What does this code do?

This creates a 2x2 grid of time-series charts. The top row shows anomalies flagged by the Z-score method. The bottom row shows anomalies flagged by the Isolation Forest.

Both rows plot the same two vital signs (heart rate and SpO2) so you can directly compare which red dots each method catches. The Isolation Forest typically flags more subtle patterns that Z-scores miss.

  • Save vitals_monitor.py.
  • Run the script in your terminal by running:
python vitals_monitor.py
  • Check your project folder. You should see a new file called anomaly_comparison.png. Open it to confirm you see a 2x2 grid of charts with red dots marking detected anomalies.

Not seeing anomaly_comparison.png?

Make sure your new code starts after the last line of the Step 4 section (the precision_recall_fscore_support call). If it is placed inside a function or indented block, it will not run at the top level.

Also check that your variable names match exactly. z_score_predictions and iso_predictions must be the same names you defined in Steps 3 and 4.

Still stuck? Help me debug why anomaly_comparison.png is not being generated.

Build the metrics comparison bar chart

A bar chart puts precision, recall, and F1-score side by side for both methods. This makes it immediately clear which model is stronger on each metric.

  • In vitals_monitor.py, add the following code directly below the comparison visualization you just wrote:
# Comparison bar chart
fig, ax = plt.subplots(figsize=(8, 5))
metrics = ['Precision', 'Recall', 'F1-Score']
z_scores_metrics = [z_precision, z_recall, z_f1]
iso_metrics = [iso_precision, iso_recall, iso_f1]

x = np.arange(len(metrics))
width = 0.35

bars1 = ax.bar(x - width/2, z_scores_metrics, width, label='Z-Score', color='skyblue')
bars2 = ax.bar(x + width/2, iso_metrics, width, label='Isolation Forest', color='salmon')

ax.set_ylabel('Score')
ax.set_title('Anomaly Detection: Z-Score vs Isolation Forest')
ax.set_xticks(x)
ax.set_xticklabels(metrics)
ax.legend()
ax.set_ylim(0, 1.1)

for bar in bars1:
    ax.annotate(f'{bar.get_height():.2f}', xy=(bar.get_x() + bar.get_width() / 2, bar.get_height()),
                xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9)
for bar in bars2:
    ax.annotate(f'{bar.get_height():.2f}', xy=(bar.get_x() + bar.get_width() / 2, bar.get_height()),
                xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9)

plt.tight_layout()
plt.savefig('metrics_comparison.png', dpi=100)
plt.close()
print("Metrics comparison saved to metrics_comparison.png")

What does this code do?

This creates a grouped bar chart with three clusters (Precision, Recall, F1-Score). Each cluster has two bars: skyblue for Z-Score and salmon for Isolation Forest.

The annotate loop places the exact numeric score above each bar so you can read precise values without squinting at the axis.

  • Save vitals_monitor.py and run the script again:
python vitals_monitor.py
  • Open metrics_comparison.png in your project folder. You should see a bar chart with three clusters and two colored bars in each cluster.

Seeing a blank or distorted chart?

Check that z_precision, z_recall, z_f1, iso_precision, iso_recall, and iso_f1 are all defined earlier in your script. These variables come from the precision_recall_fscore_support calls in Steps 3 and 4.

Need help? Why is my metrics bar chart not rendering correctly?

Print the final summary

Charts are great for presentations, but a quick console summary lets you see the headline result every time you run the script. This final block prints both models' scores and explains the key takeaway.

  • Add the final summary code at the very end of vitals_monitor.py:
# Final summary
print("\n" + "="*50)
print("FINAL SUMMARY")
print("="*50)
print(f"\nZ-Score Method:      Precision={z_precision:.3f}  Recall={z_recall:.3f}  F1={z_f1:.3f}")
print(f"Isolation Forest:    Precision={iso_precision:.3f}  Recall={iso_recall:.3f}  F1={iso_f1:.3f}")
print("\nThe Isolation Forest model captures multivariate anomaly patterns")
print("that single-variable Z-score thresholds can miss. It considers the")
print("relationships between vitals simultaneously, making it more effective")
print("at detecting complex deterioration patterns in patient data.")

Why does Isolation Forest outperform Z-scores?

Z-scores look at each vital sign in isolation. A heart rate of 105 bpm might be fine on its own. But if SpO2 is also 93% at the same time, that combination is concerning.

Isolation Forest considers all five vitals simultaneously. It learns the shape of "normal" in multivariate space and flags readings that are hard to reach from the normal cluster. This is why it catches subtle multi-variable deterioration patterns.

  • Save vitals_monitor.py and run it one final time:
python vitals_monitor.py
  • Scroll to the bottom of your terminal output. You should see a FINAL SUMMARY banner with precision, recall, and F1 scores for both methods printed in a neat table.

Not seeing the summary?

If the script errors before reaching the summary, look at the traceback. The most common cause is a typo in a variable name from a previous step. Make sure z_precision and iso_precision (and their recall/f1 counterparts) are defined without typos.

Still stuck? Help me figure out why my final summary is not printing.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file and that all three output images exist in your project folder.

ⓧ I'd like to double check the full code

Here is the complete vitals_monitor.py file as it should look at the end of this step:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, precision_recall_fscore_support


# --- Step 2: Generate Synthetic Vital Signs Data ---

def generate_normal_vitals(n_samples=950):
    """Generate normal vital sign readings."""
    np.random.seed(42)
    data = {
        'timestamp': pd.date_range(start='2024-01-01', periods=n_samples, freq='5min'),
        'heart_rate': np.random.normal(75, 8, n_samples).clip(60, 100),
        'systolic_bp': np.random.normal(120, 10, n_samples).clip(90, 140),
        'temperature': np.random.normal(36.6, 0.3, n_samples).clip(36.1, 37.2),
        'spo2': np.random.normal(97.5, 1.0, n_samples).clip(95, 100),
        'respiratory_rate': np.random.normal(16, 2, n_samples).clip(12, 20),
    }
    return pd.DataFrame(data)


def inject_anomalies(df, n_anomalies=50):
    """Inject anomalous vital sign readings."""
    np.random.seed(123)
    anomaly_indices = np.random.choice(len(df), n_anomalies, replace=False)

    for idx in anomaly_indices:
        anomaly_type = np.random.choice(['tachycardia', 'bradycardia', 'hypoxia', 'fever', 'hypotension'])

        if anomaly_type == 'tachycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(150, 200)
        elif anomaly_type == 'bradycardia':
            df.loc[idx, 'heart_rate'] = np.random.uniform(30, 45)
        elif anomaly_type == 'hypoxia':
            df.loc[idx, 'spo2'] = np.random.uniform(70, 88)
        elif anomaly_type == 'fever':
            df.loc[idx, 'temperature'] = np.random.uniform(39.0, 41.0)
        elif anomaly_type == 'hypotension':
            df.loc[idx, 'systolic_bp'] = np.random.uniform(50, 75)

    df['is_anomaly'] = 0
    df.loc[anomaly_indices, 'is_anomaly'] = 1
    return df


# Generate the dataset
print("Generating synthetic vital signs data...")
vitals_df = generate_normal_vitals(950)
vitals_df = inject_anomalies(vitals_df, 50)
vitals_df.to_csv('vital_signs_data.csv', index=False)
print(f"Dataset created: {len(vitals_df)} readings, {vitals_df['is_anomaly'].sum()} anomalies")

# Visualize raw data
fig, axes = plt.subplots(3, 2, figsize=(14, 10))
vital_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']
titles = ['Heart Rate (bpm)', 'Systolic BP (mmHg)', 'Temperature (\u00b0C)', 'SpO2 (%)', 'Respiratory Rate (breaths/min)']

for i, (col, title) in enumerate(zip(vital_cols, titles)):
    ax = axes[i // 2, i % 2]
    ax.plot(vitals_df.index, vitals_df[col], alpha=0.7, linewidth=0.5)
    anomaly_mask = vitals_df['is_anomaly'] == 1
    ax.scatter(vitals_df.index[anomaly_mask], vitals_df[col][anomaly_mask],
               color='red', s=20, zorder=5, label='Anomaly')
    ax.set_title(title)
    ax.legend()

axes[2, 1].set_visible(False)
plt.tight_layout()
plt.savefig('raw_vitals.png', dpi=100)
plt.close()
print("Raw data visualization saved to raw_vitals.png")


# --- Step 3: Z-Score Statistical Baseline ---

print("\n--- Z-Score Anomaly Detection ---")
feature_cols = ['heart_rate', 'systolic_bp', 'temperature', 'spo2', 'respiratory_rate']

# Calculate Z-scores
z_scores = vitals_df[feature_cols].apply(lambda x: (x - x.mean()) / x.std())
z_score_predictions = (z_scores.abs() > 3).any(axis=1).astype(int)

# Evaluate Z-score method
print("\nZ-Score Method Results:")
print(classification_report(vitals_df['is_anomaly'], z_score_predictions,
                            target_names=['Normal', 'Anomaly']))

z_precision, z_recall, z_f1, _ = precision_recall_fscore_support(
    vitals_df['is_anomaly'], z_score_predictions, average='binary')


# --- Step 4: Isolation Forest Model ---

print("\n--- Isolation Forest Anomaly Detection ---")

# Prepare features
scaler = StandardScaler()
X = vitals_df[feature_cols].values
X_scaled = scaler.fit_transform(X)

# Train Isolation Forest
iso_forest = IsolationForest(
    n_estimators=100,
    contamination=0.05,
    random_state=42
)
iso_forest.fit(X_scaled)

# Predict: IsolationForest returns -1 for anomalies, 1 for normal
iso_predictions_raw = iso_forest.predict(X_scaled)
iso_predictions = (iso_predictions_raw == -1).astype(int)

# Evaluate Isolation Forest
print("\nIsolation Forest Results:")
print(classification_report(vitals_df['is_anomaly'], iso_predictions,
                            target_names=['Normal', 'Anomaly']))

iso_precision, iso_recall, iso_f1, _ = precision_recall_fscore_support(
    vitals_df['is_anomaly'], iso_predictions, average='binary')


# --- Step 5: Visualize and Compare Results ---

# 2x2 subplot: Z-score vs Isolation Forest on Heart Rate and SpO2
fig, axes = plt.subplots(2, 2, figsize=(14, 8))

# Z-score detections on Heart Rate
ax = axes[0, 0]
ax.plot(vitals_df.index, vitals_df['heart_rate'], alpha=0.7, linewidth=0.5, color='steelblue')
z_anomaly_mask = z_score_predictions == 1
ax.scatter(vitals_df.index[z_anomaly_mask], vitals_df['heart_rate'][z_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Z-Score: Heart Rate')
ax.set_ylabel('BPM')
ax.legend()

# Z-score detections on SpO2
ax = axes[0, 1]
ax.plot(vitals_df.index, vitals_df['spo2'], alpha=0.7, linewidth=0.5, color='steelblue')
ax.scatter(vitals_df.index[z_anomaly_mask], vitals_df['spo2'][z_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Z-Score: SpO2')
ax.set_ylabel('%')
ax.legend()

# Isolation Forest detections on Heart Rate
ax = axes[1, 0]
ax.plot(vitals_df.index, vitals_df['heart_rate'], alpha=0.7, linewidth=0.5, color='steelblue')
iso_anomaly_mask = iso_predictions == 1
ax.scatter(vitals_df.index[iso_anomaly_mask], vitals_df['heart_rate'][iso_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Isolation Forest: Heart Rate')
ax.set_ylabel('BPM')
ax.legend()

# Isolation Forest detections on SpO2
ax = axes[1, 1]
ax.plot(vitals_df.index, vitals_df['spo2'], alpha=0.7, linewidth=0.5, color='steelblue')
ax.scatter(vitals_df.index[iso_anomaly_mask], vitals_df['spo2'][iso_anomaly_mask],
           color='red', s=30, zorder=5, label='Detected Anomaly')
ax.set_title('Isolation Forest: SpO2')
ax.set_ylabel('%')
ax.legend()

plt.tight_layout()
plt.savefig('anomaly_comparison.png', dpi=100)
plt.close()
print("\nComparison visualization saved to anomaly_comparison.png")

# Comparison bar chart
fig, ax = plt.subplots(figsize=(8, 5))
metrics = ['Precision', 'Recall', 'F1-Score']
z_scores_metrics = [z_precision, z_recall, z_f1]
iso_metrics = [iso_precision, iso_recall, iso_f1]

x = np.arange(len(metrics))
width = 0.35

bars1 = ax.bar(x - width/2, z_scores_metrics, width, label='Z-Score', color='skyblue')
bars2 = ax.bar(x + width/2, iso_metrics, width, label='Isolation Forest', color='salmon')

ax.set_ylabel('Score')
ax.set_title('Anomaly Detection: Z-Score vs Isolation Forest')
ax.set_xticks(x)
ax.set_xticklabels(metrics)
ax.legend()
ax.set_ylim(0, 1.1)

for bar in bars1:
    ax.annotate(f'{bar.get_height():.2f}', xy=(bar.get_x() + bar.get_width() / 2, bar.get_height()),
                xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9)
for bar in bars2:
    ax.annotate(f'{bar.get_height():.2f}', xy=(bar.get_x() + bar.get_width() / 2, bar.get_height()),
                xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9)

plt.tight_layout()
plt.savefig('metrics_comparison.png', dpi=100)
plt.close()
print("Metrics comparison saved to metrics_comparison.png")

# Final summary
print("\n" + "="*50)
print("FINAL SUMMARY")
print("="*50)
print(f"\nZ-Score Method:      Precision={z_precision:.3f}  Recall={z_recall:.3f}  F1={z_f1:.3f}")
print(f"Isolation Forest:    Precision={iso_precision:.3f}  Recall={iso_recall:.3f}  F1={iso_f1:.3f}")
print("\nThe Isolation Forest model captures multivariate anomaly patterns")
print("that single-variable Z-score thresholds can miss. It considers the")
print("relationships between vitals simultaneously, making it more effective")
print("at detecting complex deterioration patterns in patient data.")

Your vital signs monitoring system is complete. You have built two anomaly detection methods, measured their performance with real metrics, and produced visualizations that clearly show the Isolation Forest's advantage. Next up, tackle the Secret Mission to turn this into a real-time streaming monitor.

Secret mission

Real-Time Monitoring Simulator

Your anomaly detection model works on historical data. But real patient monitoring happens in real time. Can you build a streaming simulator that processes one reading at a time and triggers escalating alerts when danger patterns emerge?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files for future reference or delete them to free up disk space.

Resources you used:

  • Project folder on your Desktop containing vitals_monitor.py, generated CSV data, and PNG visualizations.
  • Python virtual environment with installed packages (scikit-learn, pandas, matplotlib, numpy).

Keep everything running

No action needed. Choose this if you want to revisit the anomaly detection code, tweak the model parameters, or experiment with new vital sign patterns.

  • Your project folder stays on your Desktop with all files intact.
  • The virtual environment and installed packages remain ready to use.
  • To run the script again later, activate your virtual environment first, then run python vitals_monitor.py.

Pause - I'll come back to this later

Since this project is entirely local with no running processes, there is nothing to pause. Your files sit idle on disk until you use them again.

  • Deactivate the virtual environment by running:
deactivate
  • Your files remain on disk but consume no CPU or memory while idle.
  • To resume later, navigate to your project folder, activate the virtual environment, and run the script.

Delete - I don't want to use this again

Remove all project files, the virtual environment, and installed packages from your machine.

  • Deactivate the virtual environment if it is still active:
deactivate
  • Delete the entire project folder from your Desktop by running:

macOS

cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your project folder name"]]

Windows

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your project folder name"]]
  • This removes vitals_monitor.py, vital_signs_data.csv, all PNG visualizations, and the virtual environment with its installed packages.
  • Python itself remains installed on your system. It is a general-purpose tool you will likely use again.

Nice Work!

Nice Work!

You just built an AI-powered vital signs monitoring system that detects abnormal patient readings using machine learning.

You've learned how to:

  • Generated synthetic patient vital sign data with injected anomalies for training and evaluating anomaly detection models.
  • Trained an Isolation Forest model for multivariate anomaly detection and compared it against a Z-score statistical baseline.
  • Visualized detected anomalies on time-series charts and evaluated model performance with precision, recall, and F1-score metrics.
  • Built a real-time monitoring simulator that streams vital signs and triggers escalating alerts when anomalies cluster.

Ready to quiz yourself?