from uvisbox.Core.BandDepths.functional_banddepth import *
import numpy as np
from uvisbox.Core.CommonInterface import BoxplotStyleConfig
# available functions:
# - calculate_band(sorted_curves, percentile)
# - functional_banddepth(data, dtype=np.float64):
# - modified_functional_banddepth(data, dtype=np.float64):
# - fbd = functional_banddepth
# - mfbd = modified_functional_banddepth
#
[docs]
def functional_boxplot_band_depths(data, method='fbd'):
"""
Compute band depths for a set of functional curves.
Parameters:
-----------
curves : np.ndarray
2D array of shape (N, D) where N is the number of curves and D is the number of points per curve.
method : str, optional
Method for computing band depth. Options are:
- 'fbd': Functional band depth (default)
- 'mfbd': Modified functional band depth
"""
if method == 'fbd':
return functional_banddepth(data)
elif method == 'mfbd':
return modified_functional_banddepth(data)
else:
raise ValueError(f"Unknown method '{method}'. Choose 'fbd' or 'mfbd'.")
[docs]
def functional_boxplot_get_band(data, percentile, method='fbd'):
"""
Compute the band envelope for a given percentile using functional band depth.
Parameters:
-----------
data : np.ndarray
2D array of shape (N, D) where N is the number of curves and D is the number of points per curve.
percentile : float
Percentile value (0-100) for the band envelope.
method : str, optional
Method for computing band depth. Options are:
- 'fbd': functional band depth (default)
- 'mfbd': modified functional band depth
Returns:
--------
bottom_curve : np.ndarray
1D array representing the bottom envelope of the band.
top_curve : np.ndarray
1D array representing the top envelope of the band.
Examples:
---------
>>> data = np.random.randn(100, 50) # 100 curves, 50 points each
>>> bottom, top = functional_boxplot_get_band(data, 50, method='fbd') # 50th percentile band
"""
# Validate input
if not isinstance(data, np.ndarray):
data = np.array(data)
if data.ndim != 2:
raise ValueError("Input data must be a 2D array of shape (N, D).")
if not 0 <= percentile <= 100:
raise ValueError("Percentile must be between 0 and 100.")
# Compute band depths based on method
if method == 'fbd':
depths = functional_banddepth(data)
elif method == 'mfbd':
depths = modified_functional_banddepth(data)
else:
raise ValueError(f"Unknown method '{method}'. Choose 'fbd' or 'mfbd'.")
# Sort curves by depth (descending order - highest depth first)
sorted_indices = np.argsort(depths)[::-1]
sorted_curves = data[sorted_indices]
# Calculate the number of curves to include
n_curves = sorted_curves.shape[0]
index = int(np.ceil(n_curves * (percentile / 100)))
# Get the top `index` curves with highest depth
selected_curves = sorted_curves[:index]
# Compute envelope
top_curve = np.max(selected_curves, axis=0)
bottom_curve = np.min(selected_curves, axis=0)
return bottom_curve, top_curve
[docs]
def functional_boxplot_summary_statistics(data, method='fbd', boxplot_style=None):
"""
Compute functional boxplot summary statistics without visualization.
This function computes the same statistics as functional_boxplot but returns
them as numpy arrays in a dictionary instead of creating a plot.
Parameters:
-----------
data : np.ndarray
2D array of shape (N, D) where N is the number of curves and D is the number
of points per curve.
method : str, optional
Method for computing band depth. Options are:
- 'fbd': functional band depth (default)
- 'mfbd': modified functional band depth
boxplot_style : BoxplotStyleConfig, optional
Configuration for the boxplot including percentiles and outlier settings.
If None, uses default configuration.
Returns:
--------
stats : dict
Dictionary containing the following keys:
- 'depths': np.ndarray of shape (N,) - band depths for each curve
- 'median': np.ndarray of shape (D,) - median curve (highest depth)
- 'percentile_bands': dict - percentile bands with keys like '25_percentile_band'
containing tuples (bottom_curve, top_curve) of shape (D,) each
- 'outliers': np.ndarray of shape (n_outliers, D) - outlier curves beyond largest percentile
- 'sorted_curves': np.ndarray of shape (N, D) - curves sorted by depth (descending)
- 'sorted_indices': np.ndarray of shape (N,) - original indices sorted by depth
Raises:
-------
ValueError
If data is not 2D or if method is invalid.
Examples:
---------
>>> import numpy as np
>>> from uvisbox.Modules.FunctionalBoxplot.functional_boxplot_stats import functional_boxplot_summary_statistics
>>> from uvisbox.Core.CommonInterface import BoxplotStyleConfig
>>>
>>> # Generate synthetic functional data
>>> t = np.linspace(0, 1, 100)
>>> data = np.array([np.sin(2*np.pi*t) + 0.2*np.random.randn(100) for _ in range(50)])
>>>
>>> # Basic usage with default settings
>>> stats = functional_boxplot_summary_statistics(data)
>>> print(f"Median curve shape: {stats['median'].shape}")
>>> print(f"Number of outliers: {stats['outliers'].shape[0]}")
>>> print(f"Available percentile bands: {list(stats['percentile_bands'].keys())}")
>>>
>>> # Access specific percentile band
>>> bottom_25, top_25 = stats['percentile_bands']['25_percentile_band']
>>> print(f"25th percentile band shapes: {bottom_25.shape}, {top_25.shape}")
>>>
>>> # Custom percentiles
>>> style = BoxplotStyleConfig(percentiles=[10, 50, 90], show_outliers=True)
>>> stats = functional_boxplot_summary_statistics(data, boxplot_style=style)
"""
# Use default config if none provided
if boxplot_style is None:
boxplot_style = BoxplotStyleConfig()
# Validate and copy input data
if not isinstance(data, np.ndarray):
data = np.array(data)
if data.ndim != 2:
raise ValueError(f"Input data must be a 2D array of shape (N, D). Got {data.ndim}D array.")
# Work on a copy to avoid modifying input data
data_copy = data.copy()
# Compute band depths
if method == 'fbd' or method == 'mfbd':
depths = functional_boxplot_band_depths(data_copy, method=method)
else:
raise ValueError(f"Unknown method '{method}'. Choose 'fbd' or 'mfbd'.")
# Sort curves by depth (descending order - highest depth first)
sorted_indices = np.argsort(depths)[::-1]
sorted_data = data_copy[sorted_indices]
sorted_depths = depths[sorted_indices]
# Initialize results dictionary
stats = {
'depths': depths,
'sorted_indices': sorted_indices,
'sorted_curves': sorted_data,
'percentile_bands': {}
}
# Compute median curve (curve with maximum depth)
stats['median'] = sorted_data[0]
# Compute percentile bands
for percentile in boxplot_style.percentiles:
bottom, top = functional_boxplot_get_band(data_copy, percentile, method=method)
band_key = f'{int(percentile)}_percentile_band'
stats['percentile_bands'][band_key] = (bottom, top)
# Compute outliers if requested
if boxplot_style.show_outliers and len(boxplot_style.percentiles) > 0:
largest_percentile = max(boxplot_style.percentiles)
outlier_start_idx = int(np.ceil(len(sorted_data) * largest_percentile / 100))
stats['outliers'] = sorted_data[outlier_start_idx:]
else:
stats['outliers'] = np.array([]).reshape(0, data_copy.shape[1])
return stats