Architecture¶
Understanding how trend_classifier is designed.
Overview¶
trend_classifier uses a Strategy Pattern to support multiple detection algorithms. This allows:
- Swapping algorithms at runtime
- Adding new algorithms without changing core code
- Consistent API across all detectors
┌─────────────────────────────────────────────────────────────┐
│ Segmenter │
│ (Facade Class) │
├─────────────────────────────────────────────────────────────┤
│ - Handles data input (arrays, DataFrames) │
│ - Delegates to selected detector │
│ - Provides visualization methods │
│ - Maintains backward compatibility │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ BaseDetector │
│ (Abstract Base Class) │
├─────────────────────────────────────────────────────────────┤
│ + fit(x, y) → self │
│ + detect() → DetectionResult │
│ + fit_detect(x, y) → DetectionResult │
│ + name: str │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│SlidingWindow │ │ BottomUp │ │ PELT │
│ Detector │ │ Detector │ │ Detector │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ Window-based │ │ Merge-based │ │ ruptures lib │
│ linear fit │ │ segmentation │ │ optimal CPD │
└───────────────┘ └───────────────┘ └───────────────┘
Key Components¶
Segmenter (Facade)¶
The main entry point that users interact with:
from trend_classifier import Segmenter
# Legacy API (backward compatible)
seg = Segmenter(x=x, y=y, n=40)
# New API with detector selection
seg = Segmenter(x=x, y=y, detector="pelt", detector_params={"penalty": 5})
Responsibilities:
- Data preprocessing and validation
- Detector instantiation and delegation
- Visualization methods
- Legacy API support
BaseDetector (Abstract)¶
Defines the interface all detectors must implement:
from abc import ABC, abstractmethod
class BaseDetector(ABC):
@property
@abstractmethod
def name(self) -> str:
"""Algorithm identifier."""
pass
@abstractmethod
def fit(self, x: np.ndarray, y: np.ndarray) -> "BaseDetector":
"""Fit to data."""
pass
@abstractmethod
def detect(self) -> DetectionResult:
"""Detect segments."""
pass
DetectionResult¶
Structured output from detectors:
@dataclass
class DetectionResult:
segments: SegmentList # Detected segments
breakpoints: list[int] # Change point indices
metadata: dict # Algorithm-specific info
Segment¶
Represents a single detected trend segment:
class Segment(BaseModel):
start: int # Start index
stop: int # End index
slope: float # Trend slope
offset: float # Y-intercept
std: float # Detrended std dev
span: float # Normalized range
# ... more attributes
Adding a New Detector¶
To add a custom detector:
from trend_classifier.detectors import BaseDetector, DetectionResult
class MyDetector(BaseDetector):
name = "my_detector"
def __init__(self, param1=10):
self.param1 = param1
self._x = None
self._y = None
def fit(self, x, y):
self._x = x
self._y = y
return self
def detect(self):
self._validate_fitted()
# Your detection logic here
segments = self._find_segments()
return DetectionResult(
segments=segments,
breakpoints=[s.start for s in segments[1:]],
metadata={"algorithm": self.name}
)
Register it:
from trend_classifier.detectors import DETECTOR_REGISTRY
DETECTOR_REGISTRY["my_detector"] = MyDetector
# Now usable
seg = Segmenter(x=x, y=y, detector="my_detector")
Data Flow¶
User Input Processing Output
─────────────────────────────────────────────────────────────
DataFrame ─┐ ┌─ SegmentList
├─► Segmenter ─► Detector ─► Result ├─ DataFrame
x, y arrays┘ │ │ └─ Plots
│ │
▼ ▼
Validate fit_detect()
Convert segments
to numpy breakpoints
Module Structure¶
trend_classifier/
├── __init__.py # Public API exports
├── segmentation.py # Segmenter facade
├── segment.py # Segment, SegmentList
├── configuration.py # Config class, presets
├── metrics.py # Error calculation
├── visuals.py # Plotting functions
└── detectors/
├── __init__.py # Registry, get_detector()
├── base.py # BaseDetector, DetectionResult
├── sliding_window.py
├── bottom_up.py
└── pelt.py
Design Decisions¶
Why Strategy Pattern?¶
- Extensibility - Add algorithms without touching Segmenter
- Testing - Each detector is independently testable
- User choice - Users pick the best algorithm for their data
Why Facade Pattern for Segmenter?¶
- Simplicity - Single entry point for users
- Backward compatibility - Legacy API preserved
- Convenience - Handles data conversion, visualization
Why Pydantic for Segment?¶
- Validation - Automatic type checking
- Serialization - Easy JSON/dict conversion
- Documentation - Field descriptions as docs