Medical Electronic Laboratory

Module 9: PPG Signal Processing

⏱️ Duration

60-90 minutes

🎯 Level

Intermediate

📊 Skills

Python, Signal Processing, Data Analysis

🛠️ Tools

Google Colab, Python Libraries

🎯 Lab Objective

Learn to process Photoplethysmography (PPG) signals using Python in Google Colab to extract physiological parameters including heart rate and perform signal quality assessment.

Prerequisites

  • Google account
  • PPG data in CSV format (2 columns: red, IR)
  • Basic Python knowledge
  • Internet connection

🚀 Getting Started

1Access Google Colab

Open your web browser and go to:

URL
https://colab.research.google.com/

Sign in with your Google account if prompted.

2Create New Notebook

Click on "New Notebook" or navigate to File → New Notebook

Rename your notebook to: PPG_Signal_Processing_Lab_YourName

💡 Tip: Use meaningful names to organize your work

3Prepare Your Data

Ensure your PPG data meets these requirements:

  • Format: CSV file
  • Columns: Two columns only (red signal, IR signal)
  • Header: No header row - just numerical data
  • File Size: Preferably under 10MB
⚠️ Important: Your CSV should look like this:
value_red_1, value_ir_1
value_red_2, value_ir_2
value_red_3, value_ir_3
...and so on

🔧 Lab Procedure

✅ Checkpoint 1: Environment Setup

Start by installing and importing required libraries in your first code cell:

Code Cell 1 - Library Installation
# Install and import required libraries !pip install scipy matplotlib numpy pandas import numpy as np import pandas as pd import matplotlib.pyplot as plt from scipy import signal from scipy.fft import fft, fftfreq import requests import io print("✅ Libraries imported successfully!")

Action: Run this cell and wait for installation to complete.

✅ Checkpoint 2: Load PPG Data

Create a new code cell and choose ONE of these options to load your data:

Code Cell 2 - Upload from Local File
from google.colab import files uploaded = files.upload() # Get the filename filename = list(uploaded.keys())[0] ppg_data = pd.read_csv(filename, header=None) ppg_data.columns = ['red', 'ir'] print(f"✅ File '{filename}' loaded successfully!") print("Data shape:", ppg_data.shape)

Expected Output: You should see the data shape and first few rows printed.

✅ Checkpoint 3: Generate Time Vector

Create a new code cell and add the following:

Code Cell 3 - Time Vector Generation
# Define sampling rate (adjust according to your data) sampling_rate = 100 # Hz - typical for PPG # Generate time vector time = np.arange(len(ppg_data)) / sampling_rate # Add time to dataframe ppg_data['time'] = time print(f"📊 Data Information:") print(f"Sampling rate: {sampling_rate} Hz") print(f"Total duration: {time[-1]:.2f} seconds") print(f"Number of samples: {len(ppg_data)}") print(f"Time range: {time[0]:.2f} to {time[-1]:.2f} seconds") # ✅ Checkpoint verification if len(time) == len(ppg_data): print("✅ Time vector generated correctly!") else: print("❌ Error in time vector generation")
💡 Note: If you know your actual sampling rate, replace 100 with the correct value.

✅ Checkpoint 4: Visualize Raw Signals

Create a new code cell for signal visualization:

Code Cell 4 - Raw Signal Visualization
# Create subplots plt.figure(figsize=(12, 8)) # Plot Red signal plt.subplot(2, 1, 1) plt.plot(ppg_data['time'], ppg_data['red'], 'r-', alpha=0.7, linewidth=1) plt.title('Raw Red PPG Signal', fontsize=14, fontweight='bold') plt.xlabel('Time (seconds)') plt.ylabel('Amplitude') plt.grid(True, alpha=0.3) # Plot IR signal plt.subplot(2, 1, 2) plt.plot(ppg_data['time'], ppg_data['ir'], color='darkred', alpha=0.7, linewidth=1) plt.title('Raw IR PPG Signal', fontsize=14, fontweight='bold') plt.xlabel('Time (seconds)') plt.ylabel('Amplitude') plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() print("✅ Raw signals plotted successfully!") print("📝 Observation: Note the periodic nature of PPG waveforms")

Expected Output: Two plots showing your raw PPG signals.

✅ Checkpoint 5: Signal Filtering

Create a new code cell for signal preprocessing:

Code Cell 5 - Bandpass Filtering
def bandpass_filter(signal_data, lowcut, highcut, fs, order=4): """ Apply bandpass filter to remove noise from PPG signal """ nyquist = 0.5 * fs low = lowcut / nyquist high = highcut / nyquist b, a = signal.butter(order, [low, high], btype='band') filtered_signal = signal.filtfilt(b, a, signal_data) return filtered_signal # Apply bandpass filter (0.5 Hz to 5 Hz for heart rate) lowcut = 0.5 highcut = 5.0 print("🔄 Applying bandpass filter...") ppg_data['red_filtered'] = bandpass_filter(ppg_data['red'], lowcut, highcut, sampling_rate) ppg_data['ir_filtered'] = bandpass_filter(ppg_data['ir'], lowcut, highcut, sampling_rate) print(f"✅ Bandpass filtering completed!") print(f"Filter range: {lowcut}-{highcut} Hz")

✅ Checkpoint 6: Compare Raw vs Filtered

Create a new code cell to visualize the filtering results:

Code Cell 6 - Filtered Signal Visualization
plt.figure(figsize=(12, 8)) # Plot filtered Red signal plt.subplot(2, 1, 1) plt.plot(ppg_data['time'], ppg_data['red'], 'r-', alpha=0.3, label='Raw', linewidth=1) plt.plot(ppg_data['time'], ppg_data['red_filtered'], 'r-', linewidth=1.5, label='Filtered') plt.title('Red PPG Signal: Raw vs Filtered', fontsize=14, fontweight='bold') plt.xlabel('Time (seconds)') plt.ylabel('Amplitude') plt.legend() plt.grid(True, alpha=0.3) # Plot filtered IR signal plt.subplot(2, 1, 2) plt.plot(ppg_data['time'], ppg_data['ir'], color='darkred', alpha=0.3, label='Raw', linewidth=1) plt.plot(ppg_data['time'], ppg_data['ir_filtered'], color='darkred', linewidth=1.5, label='Filtered') plt.title('IR PPG Signal: Raw vs Filtered', fontsize=14, fontweight='bold') plt.xlabel('Time (seconds)') plt.ylabel('Amplitude') plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() print("✅ Filtered signals visualized!")

✅ Checkpoint 7: Peak Detection

Create a new code cell for peak detection:

Code Cell 7 - Peak Detection Algorithm
def find_ppg_peaks(signal_data, fs, min_distance=0.4): """ Find peaks in PPG signal for heart rate calculation """ min_samples = int(min_distance * fs) # Minimum distance between peaks in samples peaks, properties = signal.find_peaks(signal_data, distance=min_samples, prominence=np.std(signal_data)) return peaks # Find peaks in filtered signals red_peaks = find_ppg_peaks(ppg_data['red_filtered'], sampling_rate) ir_peaks = find_ppg_peaks(ppg_data['ir_filtered'], sampling_rate) print(f"Red signal peaks found: {len(red_peaks)}") print(f"IR signal peaks found: {len(ir_peaks)}")

✅ Checkpoint 8: Visualize Detected Peaks

Create a new code cell to plot peaks:

Code Cell 8 - Peak Visualization
plt.figure(figsize=(12, 8)) # Plot Red signal with peaks plt.subplot(2, 1, 1) plt.plot(ppg_data['time'], ppg_data['red_filtered'], 'r-', label='Filtered Signal') plt.plot(ppg_data['time'][red_peaks], ppg_data['red_filtered'][red_peaks], 'ro', markersize=8, label='Detected Peaks') plt.title('Red PPG Signal with Detected Peaks', fontsize=14, fontweight='bold') plt.xlabel('Time (seconds)') plt.ylabel('Amplitude') plt.legend() plt.grid(True, alpha=0.3) # Plot IR signal with peaks plt.subplot(2, 1, 2) plt.plot(ppg_data['time'], ppg_data['ir_filtered'], color='darkred', label='Filtered Signal') plt.plot(ppg_data['time'][ir_peaks], ppg_data['ir_filtered'][ir_peaks], 'o', color='darkred', markersize=8, label='Detected Peaks') plt.title('IR PPG Signal with Detected Peaks', fontsize=14, fontweight='bold') plt.xlabel('Time (seconds)') plt.ylabel('Amplitude') plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() print("✅ Peaks visualized!")

✅ Checkpoint 9: Heart Rate Calculation

Create a new code cell for heart rate analysis:

Code Cell 9 - Heart Rate Calculation
def calculate_heart_rate(peaks, time_vector): """ Calculate heart rate from peak intervals """ if len(peaks) < 2: return 0, [] # Calculate time differences between consecutive peaks peak_times = time_vector[peaks] rr_intervals = np.diff(peak_times) # R-R intervals in seconds # Calculate heart rate in BPM heart_rates = 60 / rr_intervals # Convert to beats per minute return np.mean(heart_rates), heart_rates # Calculate heart rates for both signals red_hr, red_rr = calculate_heart_rate(red_peaks, ppg_data['time']) ir_hr, ir_rr = calculate_heart_rate(ir_peaks, ppg_data['time']) print("Red Signal Heart Rate Analysis:") print(f" Average HR: {red_hr:.2f} BPM") print(f" Number of beats: {len(red_peaks)}") print("\nIR Signal Heart Rate Analysis:") print(f" Average HR: {ir_hr:.2f} BPM") print(f" Number of beats: {len(ir_peaks)}") print(f"\nHeart Rate Difference: {abs(red_hr - ir_hr):.2f} BPM")

📊 Expected Results

At the end of this lab, you should have:

  • ✅ Raw PPG signals plotted
  • ✅ Filtered signals compared with raw data
  • ✅ Detected peaks marked on your signals
  • ✅ Calculated heart rate values
  • ✅ Understanding of signal quality
🚀 Open Google Colab