Skip to content

Pipelines¤

ts_shape.Pipeline chains transforms and detectors into one definition that you can run on many DataFrames, print, and debug step by step. Use it when the same sequence of steps runs on several datasets or on a schedule.

import pandas as pd
import ts_shape
from ts_shape import Pipeline
from ts_shape.transform.calculator.numeric_calc import IntegerCalc
from ts_shape.events.quality.outlier_detection import OutlierDetectionEvents

df = ts_shape.make_timeseries(["sensor:temp"], n_points=1000, n_outliers=4)

pipe = (
    Pipeline(name="sensor-quality")
    .transform(IntegerCalc, "scale_column", value_column="value_double", factor=0.1)
    .detect(OutlierDetectionEvents, "detect_outliers_zscore",
            name="outliers", value_column="value_double", threshold=3.0)
)

result = pipe.run(df)
result.data                 # the signal after all transforms
result.events["outliers"]   # the detector output
result.to_event_log()       # all detector outputs as one OCEL event log

Step kinds¤

The pipeline is linear. You declare the role of each step:

Method Effect
.source(...) optional, first step only: loads the first DataFrame
.transform(...) the output replaces the working DataFrame
.detect(...) the output is stored in result.events[name]; the working DataFrame is unchanged

Step names are unique: a second step with an explicit name that is taken raises ValueError, while a transform without name= gets a _2, _3 suffix. "input" is reserved for the input frame in run_steps(). Each transform works on a copy of the working DataFrame, so a transform that changes its input in place never changes the DataFrame passed to run().

Step targets¤

A step is either a plain callable df -> df:

pipe = Pipeline(name="select").transform(
    lambda d: d[d["uuid"].isin(["sensor:temp"])], name="select_signals"
)

or a (class, "method") pair. A classmethod or staticmethod (the filters, IntegerCalc) is called on the class; for an instance method (detectors, DataHarmonizer) the class is instantiated first. Keyword arguments are passed flat and routed to the constructor or the method by parameter name. An unknown keyword raises a ValueError listing what both accept.

Loading inside the pipeline¤

With a .source(...) step the pipeline loads its own data and is run as run(), with no argument. The source can be a loader classmethod, a (class, "method") pair or a plain callable:

pipe = (
    Pipeline(name="quality-from-source")
    .source(ts_shape.make_timeseries, uuids=["sensor:temp"], n_points=500, n_outliers=3)
    .detect(OutlierDetectionEvents, "detect_outliers_zscore",
            name="outliers", value_column="value_double", threshold=3.0)
)
result = pipe.run()

For files, use for example .source(ParquetLoader, "load_by_time_range", base_path=..., start=..., end=...). There is at most one source step and it must come first. Passing a DataFrame to run() of a pipeline with a source raises TypeError; a loader error is raised as a RuntimeError naming step 0.

Steps that need two DataFrames¤

Any keyword value can be a sentinel: "$input" is the DataFrame passed to run(), "$prev" the current working DataFrame. If the keyword names the step's own DataFrame parameter (such as dataframe), the sentinel replaces the automatically passed working frame. Only strings are compared with the sentinels, so arrays, Series and DataFrames can be passed as keyword values; write "$$input" for the literal string "$input".

The recipe below cuts process signals into order segments and computes a feature table with one row per order and 15-minute window:

from ts_shape.features.segment_analysis.segment_extractor import SegmentExtractor
from ts_shape.features.segment_analysis.segment_processor import SegmentProcessor
from ts_shape.features.segment_analysis.time_windowed_features import TimeWindowedFeatureTable

process = ts_shape.make_timeseries(["temperature", "pressure"], n_points=180, freq="1min")
orders = pd.DataFrame({
    "systime": pd.to_datetime(["2025-01-01 00:00", "2025-01-01 01:00", "2025-01-01 02:00"]),
    "uuid": "order_number",
    "value_string": ["Order-A", "Order-B", "Order-C"],
})
data = pd.concat([process, orders], ignore_index=True)

features = (
    Pipeline(name="feature-table")
    .transform(SegmentExtractor, "extract_time_ranges", segment_uuid="order_number")
    .transform(SegmentProcessor, "apply_ranges",
               dataframe="$input", time_ranges="$prev", target_uuids=["temperature", "pressure"])
    .transform(TimeWindowedFeatureTable, "compute", window="15min", metrics=["mean", "std", "max"])
)
table = features.run(data).data   # time_window, segment_value, temperature__mean, ...

Cycles and segments explains the segment classes.

Inspecting and debugging¤

print(features.describe())      # every step with its arguments, without running

steps = features.run_steps(data)
steps["input"]                  # the original frame
steps["extract_time_ranges"]    # the output of each step, by step name

An exception inside a step names the failing step (index and name). A step that does not return a DataFrame raises TypeError.

Event-log export¤

PipelineResult.to_event_log() normalizes the output of every .detect step into one canonical event log (see Event log). Pass concat=False for a dict with one log per step.