Binning Strategies¶
Comprehensive guide to VPC binning methods for optimal time stratification.
Overview¶
Binning groups observations by time for percentile calculation. The choice of binning strategy significantly affects VPC interpretability.
from neopkpd.vpc import (
QuantileBinning,
EqualWidthBinning,
KMeansBinning,
BinDefinition
)
# Default binning
binning = QuantileBinning(10) # 10 bins with equal obs count
Binning Classes¶
QuantileBinning¶
Creates bins with approximately equal numbers of observations:
from neopkpd.vpc import QuantileBinning
# 10 bins with equal observation count
binning = QuantileBinning(n_bins=10)
# Fewer bins for sparse data
binning = QuantileBinning(n_bins=5)
Algorithm: 1. Sort observations by time 2. Divide into n_bins groups with equal count 3. Bin boundaries at group transitions
Best for: - Sparse or uneven sampling designs - Rich sampling at specific times - Most population PK studies
Properties: - Stable percentile estimates (equal n per bin) - May have uneven time coverage - Robust to outliers
EqualWidthBinning¶
Creates bins with equal time ranges:
from neopkpd.vpc import EqualWidthBinning
# 10 bins with equal time width
binning = EqualWidthBinning(n_bins=10)
# For 24-hour study: each bin covers 2.4 hours
# Bin 1: 0.0 - 2.4 hr
# Bin 2: 2.4 - 4.8 hr
# ...
Algorithm: 1. Determine time range [t_min, t_max] 2. Divide into n_bins equal intervals 3. Width = (t_max - t_min) / n_bins
Best for: - Dense, uniformly sampled data - Steady-state trough sampling - When time coverage matters
Properties: - Even time coverage - Variable observations per bin - May have empty bins if sampling sparse
KMeansBinning¶
Uses k-means clustering to find natural time groupings:
from neopkpd.vpc import KMeansBinning
# K-means with 8 clusters
binning = KMeansBinning(n_bins=8)
# With custom max iterations
binning = KMeansBinning(n_bins=8, max_iter=200)
Algorithm: 1. Initialize k cluster centers 2. Assign times to nearest center 3. Update centers as cluster means 4. Repeat until convergence or max_iter
Best for: - Data with natural time clusters - Sparse PK sampling designs - When observation times cluster naturally
Properties: - Adaptive to data structure - May find unintuitive boundaries - Results can vary with initialization
BinDefinition¶
Structure representing a computed bin:
from dataclasses import dataclass
@dataclass
class BinDefinition:
"""Definition of a time bin."""
bin_id: int # Bin identifier (1-indexed)
time_min: float # Lower bound (inclusive)
time_max: float # Upper bound (exclusive)
time_midpoint: float # Midpoint for plotting
Accessing Bin Definitions¶
from neopkpd.vpc import compute_bins
# Compute bins from times
times = [0.5, 1.0, 2.0, 4.0, 8.0, 12.0, 24.0]
binning = QuantileBinning(5)
bins = compute_bins(times, binning)
for bin_def in bins:
print(f"Bin {bin_def.bin_id}: [{bin_def.time_min:.1f}, {bin_def.time_max:.1f})")
print(f" Midpoint: {bin_def.time_midpoint:.1f}")
Choosing Binning Strategy¶
Decision Guide¶
| Data Characteristic | Recommended Binning | Reason |
|---|---|---|
| Sparse sampling | QuantileBinning(5-8) | Ensures sufficient obs per bin |
| Dense sampling | EqualWidthBinning(8-12) | Even time coverage |
| Natural clusters | KMeansBinning | Adaptive to structure |
| Mixed (sparse + dense) | QuantileBinning | More robust |
| Steady-state only | EqualWidthBinning | Regular intervals |
Sample Size Considerations¶
| Subjects | Obs/Subject | Recommended n_bins |
|---|---|---|
| < 20 | 5-7 | 3-5 bins |
| 20-50 | 5-7 | 5-7 bins |
| 50-100 | 5-10 | 7-10 bins |
| > 100 | 7-15 | 8-12 bins |
Rule of thumb: Minimum 10-20 observations per bin for stable percentiles.
Comparison Example¶
from neopkpd.vpc import QuantileBinning, EqualWidthBinning, KMeansBinning
import numpy as np
# Sparse PK sampling times (typical clinical design)
times = np.array([
# Pre-dose and absorption phase (dense)
0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0,
# Distribution/elimination phase (sparse)
6.0, 8.0, 12.0, 24.0
] * 50) # 50 subjects
print("=== Binning Strategy Comparison ===\n")
# QuantileBinning
print("QuantileBinning(7):")
from neopkpd.vpc import compute_bins
bins_q = compute_bins(times, QuantileBinning(7))
for b in bins_q:
n_in_bin = sum(1 for t in times if b.time_min <= t < b.time_max)
print(f" [{b.time_min:5.1f}, {b.time_max:5.1f}): n={n_in_bin}")
# EqualWidthBinning
print("\nEqualWidthBinning(7):")
bins_e = compute_bins(times, EqualWidthBinning(7))
for b in bins_e:
n_in_bin = sum(1 for t in times if b.time_min <= t < b.time_max)
print(f" [{b.time_min:5.1f}, {b.time_max:5.1f}): n={n_in_bin}")
# KMeansBinning
print("\nKMeansBinning(7):")
bins_k = compute_bins(times, KMeansBinning(7))
for b in bins_k:
n_in_bin = sum(1 for t in times if b.time_min <= t < b.time_max)
print(f" [{b.time_min:5.1f}, {b.time_max:5.1f}): n={n_in_bin}")
Expected output patterns: - QuantileBinning: Even n per bin, uneven time widths - EqualWidthBinning: Even time widths, highly variable n - KMeansBinning: Adapts to dense early sampling
Advanced: Custom Binning¶
For special requirements, you can create custom bin definitions:
from neopkpd.vpc import BinDefinition
# Manual bin boundaries matching sampling design
manual_bins = [
BinDefinition(1, 0.0, 0.75, 0.375), # Pre-Cmax
BinDefinition(2, 0.75, 2.5, 1.625), # Around Cmax
BinDefinition(3, 2.5, 5.0, 3.75), # Early elimination
BinDefinition(4, 5.0, 10.0, 7.5), # Mid elimination
BinDefinition(5, 10.0, 25.0, 17.5), # Terminal phase
]
# Use in VPC computation
# (Implementation depends on VPC function accepting custom bins)
Bin Boundary Recommendations¶
| Sampling Design | Suggested Boundaries |
|---|---|
| Rich PK (0-24h) | 0, 0.5, 1, 2, 4, 8, 12, 24 |
| Sparse PK | 0, 2, 8, 24 |
| Steady-state QD | 0, 4, 8, 12, 16, 20, 24 |
| Steady-state BID | 0, 2, 4, 6, 8, 10, 12 |
Binning Effect on VPC¶
Too Few Bins¶
Problem: Wide time ranges, mixed populations
Visual: Smooth but may miss local misfit
Risk: Masks time-varying model errors
Too Many Bins¶
Problem: Few observations per bin
Visual: Noisy percentiles, wide CIs
Risk: False positive model misfit
Optimal Binning¶
Goal: Balance precision and resolution
Target: 15-30 observations per bin (minimum 10)
Visual: Clear percentile trends, reasonable CI width
Visualization¶
from neopkpd import viz
from neopkpd.vpc import VPCConfig, QuantileBinning, EqualWidthBinning
import matplotlib.pyplot as plt
# Compare VPCs with different binning
configs = [
("Quantile (5 bins)", VPCConfig(binning=QuantileBinning(5))),
("Quantile (10 bins)", VPCConfig(binning=QuantileBinning(10))),
("Equal Width (10 bins)", VPCConfig(binning=EqualWidthBinning(10))),
]
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for ax, (name, config) in zip(axes, configs):
vpc_result = neopkpd.compute_vpc(
observed_data=observed_data,
population_spec=pop_spec,
grid=grid,
config=config
)
plt.sca(ax)
viz.plot_vpc(vpc_result, title=name)
plt.tight_layout()
plt.savefig("binning_comparison.png", dpi=300)
Best Practices¶
- Start with QuantileBinning - Most robust choice
- Match bins to sampling - Consider nominal times
- Check observations per bin - Minimum 10-20
- Avoid empty bins - Reduce n_bins if needed
- Use VPC CI plot - Visualize bin boundaries
# Recommended workflow
from neopkpd.vpc import VPCConfig, QuantileBinning
# 1. Estimate optimal bins
n_obs = len(observed_data["times"])
n_bins = max(5, min(12, n_obs // 50)) # ~50 obs per bin
# 2. Configure VPC
config = VPCConfig(
binning=QuantileBinning(n_bins),
n_simulations=500
)
# 3. Compute and visualize
vpc_result = neopkpd.compute_vpc(...)
fig = viz.plot_vpc_ci(vpc_result, show_bin_boundaries=True)
See Also¶
- VPC Index - VPC overview
- BLQ Handling - Below quantification data
- VPC Visualization - Plotting options