Skip to content

Index

production ¤

Production Events and Tracking

This module contains both event detection and daily tracking tools for production:

Event Detection Classes: - MachineStateEvents: Run/idle intervals and transition points from a boolean state signal. - detect_run_idle: Intervalize run/idle with optional min duration. - transition_events: Point events on idle→run and run→idle changes. - detect_rapid_transitions: Identify suspicious rapid state changes. - state_quality_metrics: Return quality metrics for the state data.

  • LongDowntimeEvents: Detect idle/stopped intervals exceeding a minimum duration, and count production events between consecutive long-downtime boundaries.
  • detect_long_downtime: Find downtime segments >= min_gap (default 3 h).
  • count_events_between_gaps: Count/sum/transitions in each inter-gap production window.

  • LineThroughputEvents: Throughput metrics and takt adherence.

  • count_parts: Parts per fixed window from a counter uuid.
  • takt_adherence: Cycle time violations vs. a takt time.
  • cycle_quality_check: Enhanced cycle detection with quality validation.
  • throughput_oee: Calculate Overall Equipment Effectiveness (OEE) metrics.
  • throughput_trends: Throughput trend with moving averages and degradation flags.

  • ChangeoverEvents: Product/recipe changes and end-of-changeover derivation.

  • detect_changeover: Point events at product value changes.
  • changeover_window: End via fixed window or stable band metrics.
  • changeover_quality_metrics: Compute quality metrics for changeovers.

  • FlowConstraintEvents: Blocked/starved intervals between upstream/downstream run signals.

  • blocked_events: Upstream running while downstream not consuming.
  • starved_events: Downstream running while upstream not supplying.
  • flow_constraint_analytics: Blocked and starved events with severity and summary statistics.

Daily Production Tracking Classes: - PartProductionEvents: Track production quantities by part number. - production_by_part: Production quantity per time window. - daily_production_summary: Daily totals by part. - production_totals: Totals over date ranges. - detect_resets: Find the points in time where the counter was reset.

  • CycleTimeEvents: Analyze cycle times by part number.
  • cycle_time_by_part: Calculate cycle times.
  • cycle_time_statistics: Statistical analysis (min/avg/max/std).
  • detect_slow_cycles: Anomaly detection.
  • cycle_time_trend: Trend analysis.
  • hourly_cycle_time_summary: Hourly summary of cycle times by part.

  • ShiftReportEvents: Shift-based performance analysis.

  • shift_production: Production per shift.
  • shift_comparison: Compare shift performance.
  • shift_targets: Target vs actual analysis.
  • best_and_worst_shifts: Performance ranking.

  • DowntimeEvents: Machine availability and downtime analysis.

  • downtime_by_shift: Downtime and availability per shift.
  • downtime_by_reason: Root cause analysis.
  • top_downtime_reasons: Pareto analysis (80/20 rule).
  • availability_trend: Track availability over time.

  • QualityEvents: NOK (defective parts) and quality metrics.

  • nok_by_shift: NOK parts and First Pass Yield per shift.
  • quality_by_part: Quality metrics by part number.
  • nok_by_reason: Defect type analysis.
  • daily_quality_summary: Daily quality rollup.

OEE and Advanced Analytics: - OEEEvents: Overall Equipment Effectiveness (Availability x Performance x Quality). - availability: Availability % from run/idle intervals. - performance: Actual vs ideal throughput. - quality: Good parts / total parts. - oee: Combined daily OEE metric.

  • AlarmManagementEvents: ISA-18.2 style alarm analysis.
  • alarm_frequency: Alarm activations per time window.
  • alarm_duration_statistics: Min/avg/max/total duration of alarm ON states.
  • chattering_detection: Detect nuisance chattering alarms.
  • standing_alarms: Identify alarms that stay active too long.

  • BatchTrackingEvents: Batch/recipe production tracking.

  • detect_batches: Detect batch start/end from value changes.
  • batch_duration_statistics: Duration statistics per batch type.
  • batch_yield: Production quantity per batch.
  • batch_transition_matrix: Batch-to-batch transition frequencies.

  • BottleneckDetectionEvents: Identify production line bottlenecks.

  • station_utilization: Per-station uptime percentage per window.
  • detect_bottleneck: Identify bottleneck station per window.
  • shifting_bottleneck: Track when the bottleneck moves.
  • throughput_constraint_summary: Summary statistics.

  • MicroStopEvents: Detect brief idle intervals that accumulate into losses.

  • detect_micro_stops: Find idle intervals shorter than max_duration.
  • micro_stop_frequency: Count micro-stops per window.
  • micro_stop_impact: Time lost to micro-stops per window.
  • micro_stop_patterns: Group micro-stops by hour-of-day.

  • DutyCycleEvents: Analyze on/off patterns from boolean signals.

  • duty_cycle_by_window: On-time percentage per window.
  • on_off_intervals: List every on/off interval with duration.
  • cycle_count: Transition counts per window.
  • excessive_cycling: Flag windows with too many transitions.

Traceability: - ValueTraceabilityEvents: Trace any shared identifier across multiple stations. - build_timeline: Full timeline of every identifier at every station. - lead_time: End-to-end lead time per identifier. - current_status: Last-known station for each identifier. - station_dwell_statistics: Dwell-time stats per station. (OrderTraceabilityEvents is a deprecated alias.)

  • RoutingTraceabilityEvents: Trace item routing using ID + state/routing signal. The state_map argument maps signal values (PLC steps, station codes) to station names.
  • build_routing_timeline: Correlate ID signal with state signal.
  • lead_time: End-to-end lead time per item.
  • station_statistics: Dwell-time stats per station/step.
  • routing_paths: Most common routing path sequences.

  • ContinuousProcessAlignmentEvents: Align multi-station readings on a continuous line to a common material reference time using speed-based transport lag compensation. Methods: align_to_reference, segment_by_cut, lag_profile, alignment_quality.

Flow and Line Balancing: - FlowMetricsEvents: WIP, throughput, lead time and Little's Law from entry/exit signals. - wip_over_time: Time-weighted WIP per window. - throughput: Units out per window. - lead_time: FIFO-matched entry -> exit lead time. - flow_summary: Little's Law check and Process Cycle Efficiency.

  • LineBalancingEvents: Line balancing and takt analysis from per-station cycle signals.
  • station_cycle_times: Cycle time statistics per station and window.
  • balance_metrics: Balance efficiency, balance delay and smoothness index.
  • yamazumi: Station-loading table against takt.

  • RuntimeAccountingEvents: Operating-hours accounting from a boolean run signal.

  • runtime_summary: Run and idle time, utilization, starts, longest run.
  • runtime_by_window: Run time per window.
  • operating_hours_meter: Cumulative operating hours.
  • run_uuid: Deprecated read-only alias of run_state_uuid.

  • MultiProcessTraceabilityEvents: Multi-station topology with parallel paths and handovers.

  • build_timeline: Full timeline with parallel-station awareness.
  • lead_time: End-to-end lead time with parallel flag.
  • parallel_activity: Detect overlapping station intervals per item.
  • handover_log: Extract and correlate handover events with item IDs.
  • station_statistics: Per-station/cell dwell-time stats.
  • routing_paths: Path frequency analysis with parallel flag.

Setup, Rework, and Operator Tracking: - SetupTimeEvents: Analyze changeover/setup durations for SMED improvement. - setup_durations: List every setup interval with duration. - setup_by_product: Setup time by product transition (from → to). - setup_statistics: Overall setup time stats (count, avg, median, std, % of available time). - setup_trend: Track setup time improvement over time.

  • OperatorPerformanceEvents: Compare production output and quality across operators.
  • production_by_operator: Parts produced per operator.
  • operator_efficiency: Operator efficiency vs per-shift target.
  • quality_by_operator: Quality metrics (FPY) per operator.
  • operator_comparison: Ranked operator performance comparison.

  • ReworkEvents: Track parts requiring rework (re-processing).

  • rework_by_shift: Rework count per shift.
  • rework_by_reason: Rework by reason code.
  • rework_rate: Rework rate as % of total production.
  • rework_cost: Convert rework counts to monetary cost.
  • rework_trend: Rework trend over time.

Performance and Target Tracking: - PerformanceLossEvents: Track speed losses against target cycle times. - performance_by_shift: Performance % per shift. - slow_periods: Identify windows below target performance. - performance_trend: Performance trend over time.

  • ScrapEvents: Track material scrap and waste.
  • scrap_by_shift: Scrap quantity per shift.
  • scrap_by_reason: Scrap by reason code.
  • scrap_cost: Convert scrap to monetary cost.
  • scrap_trend: Scrap trend over time.

  • TargetEvents: Compare any metric to targets.

  • compare_to_target: Actual vs per-shift targets.
  • target_achievement_summary: Daily target achievement.
  • target_hit_rate: Percentage of days meeting targets.

  • ProductionScheduleAdherenceEvents: Planned schedule vs actual production.

  • adherence_by_period: Planned vs actual and recovery rate per period.
  • deviation_events: Events for periods breaching the deviation bands.
  • adherence_summary: Roll-up across the scheduled horizon.

  • ShiftHandoverEvents: Automated shift handover report generation.

  • generate_report: Full shift report with production, quality, downtime.
  • highlight_issues: Auto-identify metrics below thresholds.
  • from_shift_data: Build a handover report from pre-computed shift-level DataFrames.

  • PeriodSummaryEvents: Weekly/monthly summary aggregation.

  • weekly_summary: Roll up daily metrics to weekly.
  • monthly_summary: Roll up daily metrics to monthly.
  • compare_periods: Period-over-period comparison.
  • from_daily_data: Roll up a pre-computed daily DataFrame to weekly or monthly.

  • OperatingStateEvents: Infer operating states from continuous signals.

  • infer_states: Cluster the signal into N operating states.
  • state_duration_statistics: Duration statistics per inferred state.
  • unusual_state_sequence: Detect unexpected state transitions.
  • dwell_time_violation: Find intervals exceeding max allowed duration in a state.

AlarmManagementEvents ¤

AlarmManagementEvents(
    dataframe: DataFrame,
    alarm_uuid: str,
    *,
    event_uuid: str = "prod:alarm",
    value_column: str = "value_bool",
    time_column: str = "systime"
)

Bases: Base

Analyze alarm signals following ISA-18.2 alarm management principles.

Works with boolean alarm signals where True = alarm active, False = alarm cleared. Provides metrics for alarm rationalization and nuisance alarm detection.

Example usage

alarms = AlarmManagementEvents(df, alarm_uuid='temp_high_alarm')

freq = alarms.alarm_frequency(window='1h') stats = alarms.alarm_duration_statistics() chatter = alarms.chattering_detection(min_transitions=5, window='10min') standing = alarms.standing_alarms(min_duration='1h')

Example

import pandas as pd from ts_shape import AlarmManagementEvents df = pd.DataFrame({ ... "systime": pd.to_datetime([ ... "2025-01-01 08:00", "2025-01-01 08:05", "2025-01-01 08:20", ... "2025-01-01 09:30", "2025-01-01 09:40", "2025-01-01 09:41"]), ... "uuid": "temp_high", ... "value_bool": [True, False, True, False, True, False], ... }) alarms = AlarmManagementEvents(df, "temp_high") alarms.alarm_duration_statistics()[ ... ["alarm_count", "max_duration_seconds", "total_duration_seconds"]] alarm_count max_duration_seconds total_duration_seconds 0 3 4200.0 4560.0 alarms.standing_alarms(min_duration="1h")[["start", "end", "duration_seconds"]] start end duration_seconds 0 2025-01-01 08:20:00 2025-01-01 09:30:00 4200.0

Initialize alarm management analyzer.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
alarm_uuid str

UUID of the alarm signal.

required
event_uuid str

UUID to tag derived events with.

'prod:alarm'
value_column str

Column holding the boolean alarm state.

'value_bool'
time_column str

Name of timestamp column.

'systime'

alarm_frequency ¤

alarm_frequency(
    window: str = "1h", *, include_initial: bool = True
) -> pd.DataFrame

Count alarm activations per time window.

An activation is a transition from False to True.

Parameters:

Name Type Description Default
window str

Pandas offset alias for the aggregation window.

'1h'
include_initial bool

When True (default) an alarm that is already active in the first sample is counted as an activation, because shift(fill_value=False) treats the leading True as a rising edge. Pass False to ignore an alarm that was raised before the data window began (no real activation occurred inside it).

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start: window start
DataFrame
  • end: window end (start + window)
DataFrame
  • duration_seconds: window length in seconds
DataFrame
  • alarm_count
DataFrame
  • uuid
DataFrame
  • source_uuid

alarm_duration_statistics ¤

alarm_duration_statistics() -> pd.DataFrame

Compute min / avg / max / total duration of alarm-ON states.

Returns:

Type Description
DataFrame

DataFrame with a single row containing:

DataFrame
  • source_uuid
DataFrame
  • alarm_count
DataFrame
  • min_duration_seconds
DataFrame
  • avg_duration_seconds
DataFrame
  • max_duration_seconds
DataFrame
  • total_duration_seconds

chattering_detection ¤

chattering_detection(
    min_transitions: int = 5, window: str = "10min"
) -> pd.DataFrame

Detect chattering alarms (too many transitions in a short window).

A chattering alarm is one that toggles on/off rapidly, which is a nuisance and masks real alarms.

Parameters:

Name Type Description Default
min_transitions int

Minimum state changes within window to flag.

5
window str

Rolling window size (Pandas offset alias).

'10min'

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start
DataFrame
  • end
DataFrame
  • duration_seconds: float seconds (end - start)
DataFrame
  • transition_count
DataFrame
  • uuid
DataFrame
  • source_uuid

standing_alarms ¤

standing_alarms(min_duration: str = '1h') -> pd.DataFrame

Identify alarms that stay active longer than min_duration.

Standing (stale) alarms reduce operator trust and should be investigated for shelving or re-engineering.

Parameters:

Name Type Description Default
min_duration str

Minimum ON duration to flag (Pandas offset alias).

'1h'

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start
DataFrame
  • end
DataFrame
  • duration_seconds
DataFrame
  • uuid
DataFrame
  • source_uuid

BatchTrackingEvents ¤

BatchTrackingEvents(
    dataframe: DataFrame,
    batch_uuid: str,
    *,
    event_uuid: str = "prod:batch",
    value_column: str = "value_string",
    time_column: str = "systime"
)

Bases: Base

Track batch/recipe production from a batch-ID string signal.

A "batch" is defined as a contiguous period where the batch-ID value remains constant. A new batch begins when the value changes.

Example usage

batches = BatchTrackingEvents(df, batch_uuid='batch_id_signal')

detected = batches.detect_batches() stats = batches.batch_duration_statistics() yields = batches.batch_yield('part_counter') transitions = batches.batch_transition_matrix()

Initialize batch tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
batch_uuid str

UUID of the batch-ID signal.

required
event_uuid str

UUID to tag derived events with.

'prod:batch'
value_column str

Column holding the batch ID string.

'value_string'
time_column str

Name of timestamp column.

'systime'

detect_batches ¤

detect_batches() -> pd.DataFrame

Detect batch start/end from value changes in the batch-ID signal.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • batch_id: The batch identifier string.
DataFrame
  • start: Timestamp of the first sample in the batch.
DataFrame
  • end: Timestamp of the last sample in the batch.
DataFrame
  • duration_seconds: Duration of the batch.
DataFrame
  • sample_count: Number of data points in the batch.
DataFrame
  • uuid: Event UUID.
DataFrame
  • source_uuid: Source signal UUID.

batch_duration_statistics ¤

batch_duration_statistics() -> pd.DataFrame

Compute duration statistics grouped by batch type (batch_id).

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • batch_id
DataFrame
  • count: Number of occurrences of this batch type.
DataFrame
  • min_duration_seconds
DataFrame
  • avg_duration_seconds
DataFrame
  • max_duration_seconds
DataFrame
  • total_duration_seconds

batch_yield ¤

batch_yield(
    counter_uuid: str,
    *,
    value_column_counter: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Compute production quantity for each detected batch.

Uses a monotonic counter signal. With handle_resets=True the reset-aware increments are computed once over the whole counter and each is booked to the batch that was running when it was produced: a reading reports the production since the previous reading, so it belongs to the batch whose ID was last seen strictly before it. A batch therefore owns the counter readings after its first sample up to and including the next batch-ID change, the production between the last sample of one batch and the first sample of the next counts for the earlier batch, and the batch quantities add up to the counter total (readings before the first batch-ID sample, after the last one, or while the batch ID is empty belong to no batch).

Parameters:

Name Type Description Default
counter_uuid str

UUID of the monotonic part counter.

required
value_column_counter str

Column holding counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it, so quantity is the reset-aware total for the batch. The result gains a resets column with the number of resets observed in each batch. When False, quantity is simply max(0, last - first) of the readings between the batch's first and last sample (0 for fewer than two readings): a batch containing a reset reports too little and production between batches is lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • batch_id
DataFrame
  • start
DataFrame
  • end
DataFrame
  • duration_seconds
DataFrame
  • quantity: Parts produced during the batch (see above).
DataFrame
  • uuid
DataFrame
  • source_uuid
DataFrame
  • resets (only when handle_resets=True)

batch_transition_matrix ¤

batch_transition_matrix(
    *, layout: Literal["long", "wide"] = "long"
) -> pd.DataFrame

Count which batch follows which.

Parameters:

Name Type Description Default
layout Literal['long', 'wide']

"long" (default) returns one row per observed transition. "wide" returns the pivoted matrix.

'long'

Returns:

Type Description
DataFrame

With layout="long", a DataFrame with columns from_state,

DataFrame

to_state and transition_count (int), one row per observed

DataFrame

(from_state, to_state) pair, sorted by from_state and

DataFrame

to_state. With fewer than two batches the result is empty and

DataFrame

keeps these columns.

DataFrame

With layout="wide", a pivot table where index = from_batch,

DataFrame

columns = to_batch, values = transition count, plus a total

DataFrame

column. The batch columns depend on the data, so with fewer than

DataFrame

two batches the result is an empty frame with only the total

DataFrame

column (index named from_batch).

Raises:

Type Description
ValueError

If layout is not "long" or "wide".

BottleneckDetectionEvents ¤

BottleneckDetectionEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    event_uuid: str = "prod:bottleneck",
    value_column: str = "value_bool"
)

Bases: Base

Production: Bottleneck Detection

Identify which station constrains a production line by analyzing utilization of multiple boolean run-state signals.

Methods: - station_utilization: Per-station uptime percentage per window. - detect_bottleneck: Identify the bottleneck station per window. - shifting_bottleneck: Track when the bottleneck moves between stations. - throughput_constraint_summary: Summary statistics across all stations.

station_utilization ¤

station_utilization(
    station_uuids: list[str],
    window: str = "1h",
    *,
    time_weighted: bool = True
) -> pd.DataFrame

Per-station uptime percentage per time window.

Parameters:

Name Type Description Default
station_uuids list[str]

List of UUID strings identifying station run-state signals.

required
window str

Resample window (e.g. '1h', '30m').

'1h'
time_weighted bool

When True (default), utilization is time-weighted: each sample is weighted by the gap to the next sample (as in RuntimeAccountingEvents), so it reflects wall-clock time in the running state on change-encoded and irregularly sampled signals. The trailing sample of a station has no successor and carries no weight. When False, utilization is the mean of the boolean samples in the window, which equals the time share only under uniform sampling.

True

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid (the station uuid), source_uuid (the

DataFrame

station uuid), utilization_pct.

detect_bottleneck ¤

detect_bottleneck(
    station_uuids: list[str],
    window: str = "1h",
    *,
    time_weighted: bool = True
) -> pd.DataFrame

Identify the bottleneck station per window.

The bottleneck is the station with the highest utilization — it is always running because it is the constraint.

Parameters:

Name Type Description Default
station_uuids list[str]

List of station run-state UUIDs.

required
window str

Resample window.

'1h'
time_weighted bool

Forwarded to :meth:station_utilization (default True).

True

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, duration_seconds, uuid (the

DataFrame

detector's event uuid), source_uuid (the bottleneck station uuid),

DataFrame

bottleneck_uuid, utilization_pct.

shifting_bottleneck ¤

shifting_bottleneck(
    station_uuids: list[str],
    window: str = "1h",
    *,
    time_weighted: bool = True
) -> pd.DataFrame

Track when the bottleneck identity changes between stations.

Parameters:

Name Type Description Default
station_uuids list[str]

List of station run-state UUIDs.

required
window str

Resample window.

'1h'
time_weighted bool

Forwarded to :meth:station_utilization.

True

Returns:

Type Description
DataFrame

DataFrame with columns: systime, uuid (the detector's event uuid),

DataFrame

source_uuid ("<from_uuid>,<to_uuid>"), from_uuid, to_uuid,

DataFrame

previous_utilization, new_utilization.

throughput_constraint_summary ¤

throughput_constraint_summary(
    station_uuids: list[str],
    window: str = ...,
    *,
    time_weighted: bool = ...,
    as_frame: Literal[True] = ...
) -> pd.DataFrame
throughput_constraint_summary(
    station_uuids: list[str],
    window: str = ...,
    *,
    time_weighted: bool = ...,
    as_frame: Literal[False]
) -> dict[str, Any]
throughput_constraint_summary(
    station_uuids: list[str],
    window: str = "1h",
    *,
    time_weighted: bool = True,
    as_frame: bool = True
) -> pd.DataFrame | dict[str, Any]

Summary statistics for bottleneck analysis.

Parameters:

Name Type Description Default
station_uuids list[str]

List of station run-state UUIDs.

required
window str

Resample window.

'1h'
time_weighted bool

Forwarded to :meth:station_utilization.

True
as_frame bool

When True (default), return one row per station (see below). as_frame=False returns the dictionary form; it is deprecated, emits a DeprecationWarning and will be removed in the next major release.

True

Returns:

Type Description
DataFrame | dict[str, Any]

With as_frame=True (default), a summary DataFrame with one row

DataFrame | dict[str, Any]

per station that has data, in station_uuids order, columns:

DataFrame | dict[str, Any]
  • start, end, duration_seconds: the analysed span (first window start to last window end), the same on every row
DataFrame | dict[str, Any]
  • uuid, source_uuid: the station uuid
DataFrame | dict[str, Any]
  • bottleneck_count: windows in which the station was the bottleneck (bottleneck_counts of the dict form)
DataFrame | dict[str, Any]
  • bottleneck_pct: share of windows, 0-100 (bottleneck_percentages)
DataFrame | dict[str, Any]
  • avg_utilization_pct: mean utilization over the windows (avg_utilization_per_station)
DataFrame | dict[str, Any]
  • most_frequent_bottleneck: True on the station that was the bottleneck most often
DataFrame | dict[str, Any]

With as_frame=False (deprecated), a dict with:

DataFrame | dict[str, Any]

bottleneck_counts, bottleneck_percentages,

DataFrame | dict[str, Any]

most_frequent_bottleneck, avg_utilization_per_station.

ChangeoverEvents ¤

ChangeoverEvents(
    dataframe: DataFrame,
    *,
    event_uuid: str = "prod:changeover",
    time_column: str = "systime"
)

Bases: Base

Production: Changeover

Detect product/recipe changes and compute changeover windows without requiring a dedicated 'first good' signal.

Methods: - detect_changeover: point events when product/recipe changes. - changeover_window: derive an end time via fixed window or 'stable_band' metrics. - changeover_quality_metrics: per-product changeover counts and time between changeovers.

detect_changeover ¤

detect_changeover(
    product_uuid: str,
    *,
    value_column: str = "value_string",
    min_hold: str = "0s",
    include_initial: bool = True
) -> pd.DataFrame

Emit point events when the product/recipe changes value.

Uses a hold check: the new product must persist for at least min_hold until the next change.

Parameters:

Name Type Description Default
product_uuid str

UUID of the product/recipe signal.

required
value_column str

Column containing the product value.

'value_string'
min_hold str

Minimum time the new product must persist to count.

'0s'
include_initial bool

When True (default) the first reading is emitted as a changeover event, marking the product active at the start of the data. A changeover strictly requires a prior, different value, so pass include_initial=False to suppress this synthetic leading event and report only real value changes.

True

changeover_window ¤

changeover_window(
    product_uuid: str,
    *,
    value_column: str = "value_string",
    start_time: Timestamp | None = None,
    until: str = "fixed_window",
    config: dict[str, Any] | None = None,
    fallback: dict[str, Any] | None = None
) -> pd.DataFrame

Compute changeover windows per product change with enhanced configurability.

Parameters:

Name Type Description Default
product_uuid str

UUID of the product/recipe signal.

required
value_column str

Column containing the product value.

'value_string'
start_time Timestamp | None

Only changes at or after this timestamp are used. None uses all changes.

None
until str

How each window's end is found:

  • "fixed_window": end = start + config['duration'] (default '10min').
  • "stable_band": end when every metric in config['metrics'] stays within its band of a reference for its hold; the window ends at the latest metric's stable start.

Any other value, or a stable band that is never reached, uses fallback.

'fixed_window'
config dict[str, Any] | None

Method settings. Keys: 'min_hold' (minimum hold of a new product, passed to :meth:detect_changeover, default '0s'), 'duration' (for fixed_window), and for stable_band::

{
  'metrics': [
    {'uuid': 'm1', 'value_column': 'value_double',
     'band': 0.2, 'hold': '2min'},
    ...
  ],
  'reference_method': 'expanding_median' | 'rolling_mean'
                      | 'ewma' | 'target_value',
  'rolling_window': 5,  # for rolling_mean (number of points)
  'ewma_span': 10,  # for ewma
  'target_values': {'m1': 100.0, ...}  # for target_value
}

None means an empty dict.

None
fallback dict[str, Any] | None

Used when the window end cannot be determined: {'default_duration': '10min', 'completed': False} (also the default when None). completed is copied to the output.

None

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, duration_seconds (float

DataFrame

seconds, end - start), uuid, source_uuid, is_delta, method,

DataFrame

completed.

changeover_quality_metrics ¤

changeover_quality_metrics(
    product_uuid: str, *, value_column: str = "value_string"
) -> pd.DataFrame

Per-product changeover frequency: how often, and how far apart.

For each product, counts the changeovers to that product and summarizes the time between consecutive ones. Changeover durations are not measured here (a product-signal change is a point, not an interval); use :meth:changeover_window for when the process is stable again after a changeover.

Parameters:

Name Type Description Default
product_uuid str

UUID of the product/recipe signal.

required
value_column str

Column containing the product value.

'value_string'

Returns:

Type Description
DataFrame

DataFrame with one row per product: product, changeover_count,

DataFrame

avg_time_between_seconds, min_time_between_seconds,

DataFrame

max_time_between_seconds, std_time_between_seconds (time between

DataFrame

consecutive changeovers to that product; None with fewer than

DataFrame

two). Empty when there are fewer than two changeovers.

ContinuousProcessAlignmentEvents ¤

ContinuousProcessAlignmentEvents(
    dataframe: DataFrame,
    speed_uuid: str,
    line_config: list[dict],
    *,
    ref_uuid: str | None = None,
    cut_uuid: str | None = None,
    time_column: str = "systime",
    uuid_column: str = "uuid",
    value_column: str = "value_double",
    speed_unit: str = "m/min",
    min_speed: float = 0.01
)

Bases: Base

Align multi-station readings on a continuous production line.

Readings are shifted to a common material reference time via speed-based transport lag compensation.

Parameters:

Name Type Description Default
dataframe DataFrame

Long-format DataFrame with time_column, uuid_column, and signal value columns. All UUIDs (speed, stations, cut signals) share the same DataFrame.

required
speed_uuid str

UUID of the line-speed signal.

required
line_config list[dict]

Physical layout description — list of dicts with keys name (str), offset (float, meters from reference), and uuids (list[str]).

required
ref_uuid str | None

Deprecated; has no effect and will be removed in the next major release (passing it emits a DeprecationWarning). Put a reference-point signal into line_config with offset 0 instead.

None
cut_uuid str | None

Optional UUID whose values carry the cut piece length in meters. Used by segment_by_cut as the default cut_length_uuid.

None
time_column str

Name of the timestamp column (default "systime").

'systime'
uuid_column str

Name of the UUID/identifier column (default "uuid").

'uuid'
value_column str

Value column of the speed signal, and the default station value column when no override is supplied to a method (default "value_double").

'value_double'
speed_unit str

Unit of the speed signal. One of "m/min", "m/s", "mm/s".

'm/min'
min_speed float

Minimum speed in m/s used to clamp the speed before computing lag, preventing infinite lag at standstill (default 0.01).

0.01

Raises:

Type Description
ValueError

If speed_unit is not one of the supported units.

align_to_reference ¤

align_to_reference(
    station_uuids: list[str] | None = None,
    *,
    value_column: str | None = None
) -> pd.DataFrame

Shift each station reading backward by its transport lag.

This produces a common material_ref_time for all stations: systime - offset / speed, with the most recent valid speed reading at each station sample (missing speed values are skipped; samples before the first speed reading use the first one), clamped to at least min_speed.

Parameters:

Name Type Description Default
station_uuids list[str] | None

Subset of UUIDs to process. Defaults to all UUIDs in line_config.

None
value_column str | None

Value column to carry through. Defaults to the constructor value_column.

None

Returns:

Type Description
DataFrame

DataFrame with columns material_ref_time, systime, uuid, component, position_offset_m, lag_seconds, and <value_column>.

segment_by_cut ¤

segment_by_cut(
    aligned_df: DataFrame,
    *,
    cut_length_uuid: str | None = None,
    part_counter_uuid: str | None = None,
    value_column_cut: str = "value_double",
    handle_resets: bool = True
) -> pd.DataFrame

Add piece columns to the output of align_to_reference.

Adds piece_id, piece_length_m, and piece_cut_ref_time to aligned_df.

Cut-event detection strategies (in priority order):

  1. part_counter_uuid (boolean): each True row = 1 cut.
  2. part_counter_uuid (integer/float): each step where the counter increases by delta counts as delta cuts, all attributed to the same timestamp when the signal resolution is coarser than the cut rate. The first reading is the baseline; missing readings are gaps.
  3. cut_length_uuid only: each row of the length signal = 1 cut.

Each cut time is shifted back to the reference point by the cutter's own transport lag (when the cutter's uuid is in line_config), the same way align_to_reference shifts station readings.

Piece assignment: a cut finishes the piece whose material passed the reference point since the previous cut. Rows with material_ref_time in (previous cut, this cut] get this cut's piece_id; rows up to and including the first cut belong to piece 1; rows after the last cut are not cut yet and get piece_id , piece_length_m NaN and piece_cut_ref_time NaT. Pieces cut at the same instant (a counter step > 1) share one boundary, so the material goes to the first of them and the others have no rows.

Parameters:

Name Type Description Default
aligned_df DataFrame

Output of align_to_reference.

required
cut_length_uuid str | None

UUID carrying the cut piece length value. Defaults to the constructor cut_uuid when None.

None
part_counter_uuid str | None

UUID of a part-counter signal (boolean or monotonically increasing integer).

None
value_column_cut str

Column holding the length value in cut_length_uuid rows. With a counter, each cut takes the latest length reported at or before it.

'value_double'
handle_resets bool

When True (default) and a numeric part_counter_uuid is used, a decreasing counter (a reset at a shift/part change or PLC restart) is treated as new production: the post-reset counter value is emitted as that many cut events instead of being skipped. When False, a reset's negative delta is ignored and its post-reset pieces are not counted.

True

Returns:

Type Description
DataFrame

aligned_df augmented with piece_id (nullable Int64, 1-based, numbered in cut order), piece_length_m (float or NaN), and piece_cut_ref_time (the piece's cut at the reference point, in the data's time zone).

Raises:

Type Description
ValueError

If neither cut_length_uuid (nor the constructor cut_uuid) nor part_counter_uuid is given.

lag_profile ¤

lag_profile(
    station_uuids: list[str] | None = None,
    *,
    window: str = "1min"
) -> pd.DataFrame

Compute per-window mean lag for each station.

Parameters:

Name Type Description Default
station_uuids list[str] | None

Subset of UUIDs to process. Defaults to all UUIDs in line_config.

None
window str

Pandas resample frequency for the speed averaging window (default "1min").

'1min'

Returns:

Type Description
DataFrame

DataFrame with columns start, uuid, component, position_offset_m, mean_speed_m_s, and lag_seconds. Windows without speed readings have NaN speed and lag.

alignment_quality ¤

alignment_quality(
    station_uuids: list[str] | None = None,
    *,
    window: str = "1h"
) -> pd.DataFrame

Coverage check: sample counts per window for speed and each station.

Parameters:

Name Type Description Default
station_uuids list[str] | None

Subset of UUIDs to check. Defaults to all UUIDs in line_config.

None
window str

Pandas resample frequency for the counting window (default "1h").

'1h'

Returns:

Type Description
DataFrame

DataFrame with columns start, speed_sample_count, has_speed_data, has_full_coverage, and per_uuid_counts.

CycleTimeEvents ¤

CycleTimeEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    event_uuid: str = "prod:cycle_time"
)

Bases: Base

Track cycle times by part number.

Each UUID represents one signal: - part_id_uuid: string signal with current part number - cycle_trigger_uuid: boolean/integer signal for cycle completion

Example

import pandas as pd, ts_shape df = pd.concat([ts_shape.make_id_signal("part", ["A", "B"], hold=30), ... ts_shape.make_timeseries(["cycle"], n_points=60, value_column="value_bool")]) stats = ts_shape.CycleTimeEvents(df).cycle_time_statistics("part", "cycle") stats[["part_number", "count", "avg_seconds"]].round(1) part_number count avg_seconds 0 A 7 111.4 1 B 5 198.0

Initialize cycle time tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data

required
time_column str

Name of timestamp column (default: 'systime')

'systime'
event_uuid str

UUID tagged on derived cycle events (the uuid column of point outputs; default: 'prod:cycle_time')

'prod:cycle_time'

cycle_time_by_part ¤

cycle_time_by_part(
    part_id_uuid: str,
    cycle_trigger_uuid: str,
    *,
    value_column_part: str = "value_string",
    value_column_trigger: str = "value_bool",
    max_cycle_gap: str | None = None
) -> pd.DataFrame

Calculate cycle time for each part number.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
cycle_trigger_uuid str

UUID for cycle completion trigger

required
value_column_part str

Column containing part numbers

'value_string'
value_column_trigger str

Column containing cycle triggers

'value_bool'
max_cycle_gap str | None

Optional cap (e.g. "5min"). Intervals between consecutive completions longer than this are dropped as inter-cycle downtime rather than reported as a cycle. Without it, a stoppage between completions (a break, a jam) becomes one giant "cycle" that inflates max/avg and is falsely flagged as a slow cycle.

None

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • systime: Cycle completion time
DataFrame
  • uuid: Event UUID of this tracker
DataFrame
  • source_uuid: The cycle trigger UUID
DataFrame
  • part_number: Part number/ID
DataFrame
  • cycle_time_seconds: Cycle time in seconds
Example

import pandas as pd, ts_shape df = pd.concat([ts_shape.make_id_signal("part", ["A", "B"], hold=30), ... ts_shape.make_timeseries(["cycle"], n_points=60, value_column="value_bool")]) out = ts_shape.CycleTimeEvents(df).cycle_time_by_part("part", "cycle") out[["part_number", "cycle_time_seconds"]].head(3) part_number cycle_time_seconds 0 A 60.0 1 A 120.0 2 A 120.0

cycle_time_statistics ¤

cycle_time_statistics(
    part_id_uuid: str,
    cycle_trigger_uuid: str,
    *,
    value_column_part: str = "value_string",
    value_column_trigger: str = "value_bool",
    max_cycle_gap: str | None = None
) -> pd.DataFrame

Calculate statistics: min, avg, max, std cycle time by part.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
cycle_trigger_uuid str

UUID for cycle completion trigger

required
value_column_part str

Column containing part numbers

'value_string'
value_column_trigger str

Column containing cycle triggers

'value_bool'
max_cycle_gap str | None

Optional cap (e.g. "5min") forwarded to :meth:cycle_time_by_part: intervals between consecutive completions longer than this are dropped as inter-cycle downtime. Default None keeps every interval.

None

Returns:

Type Description
DataFrame

DataFrame with statistics per part:

DataFrame
  • part_number: Part number/ID
DataFrame
  • count: Number of cycles
DataFrame
  • min_seconds: Minimum cycle time
DataFrame
  • avg_seconds: Average cycle time
DataFrame
  • max_seconds: Maximum cycle time
DataFrame
  • std_seconds: Standard deviation
DataFrame
  • median_seconds: Median cycle time (robust target)
Example

import pandas as pd, ts_shape df = pd.concat([ts_shape.make_id_signal("part", ["A", "B"], hold=30), ... ts_shape.make_timeseries(["cycle"], n_points=60, value_column="value_bool")]) stats = ts_shape.CycleTimeEvents(df).cycle_time_statistics("part", "cycle") stats[["part_number", "count", "median_seconds", "max_seconds"]] part_number count median_seconds max_seconds 0 A 7 120.0 180.0 1 B 5 150.0 420.0

detect_slow_cycles ¤

detect_slow_cycles(
    part_id_uuid: str,
    cycle_trigger_uuid: str,
    *,
    threshold_factor: float = 1.5,
    value_column_part: str = "value_string",
    value_column_trigger: str = "value_bool",
    max_cycle_gap: str | None = None
) -> pd.DataFrame

Identify cycles that exceed normal time by threshold factor.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
cycle_trigger_uuid str

UUID for cycle completion trigger

required
threshold_factor float

Cycles slower than median * factor are flagged (default: 1.5)

1.5
value_column_part str

Column containing part numbers

'value_string'
value_column_trigger str

Column containing cycle triggers

'value_bool'
max_cycle_gap str | None

Optional cap (e.g. "5min") forwarded to :meth:cycle_time_by_part: intervals between consecutive completions longer than this are dropped as inter-cycle downtime. Default None keeps every interval.

None

Returns:

Type Description
DataFrame

DataFrame with slow cycles:

DataFrame
  • systime: Cycle completion time
DataFrame
  • uuid: Event UUID of this tracker
DataFrame
  • source_uuid: The cycle trigger UUID
DataFrame
  • part_number: Part number/ID
DataFrame
  • cycle_time_seconds: Actual cycle time
DataFrame
  • median_seconds: Expected (median) cycle time
DataFrame
  • deviation_factor: How many times slower than expected
DataFrame
  • is_slow: Flag (always True in returned data)
Example

import pandas as pd, ts_shape df = pd.concat([ts_shape.make_id_signal("part", ["A", "B"], hold=30), ... ts_shape.make_timeseries(["cycle"], n_points=60, value_column="value_bool")]) slow = ts_shape.CycleTimeEvents(df).detect_slow_cycles("part", "cycle") slow[["part_number", "cycle_time_seconds", "deviation_factor"]] part_number cycle_time_seconds deviation_factor 0 A 180.0 1.5 1 B 240.0 1.6 2 B 420.0 2.8

cycle_time_trend ¤

cycle_time_trend(
    part_id_uuid: str,
    cycle_trigger_uuid: str,
    part_number: str,
    *,
    window_size: int = 20,
    value_column_part: str = "value_string",
    value_column_trigger: str = "value_bool",
    max_cycle_gap: str | None = None
) -> pd.DataFrame

Analyze cycle time trends for a specific part.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
cycle_trigger_uuid str

UUID for cycle completion trigger

required
part_number str

Specific part number to analyze

required
window_size int

Number of cycles for moving average (default: 20)

20
value_column_part str

Column containing part numbers

'value_string'
value_column_trigger str

Column containing cycle triggers

'value_bool'
max_cycle_gap str | None

Optional cap (e.g. "5min") forwarded to :meth:cycle_time_by_part: intervals between consecutive completions longer than this are dropped as inter-cycle downtime. Default None keeps every interval.

None

Returns:

Type Description
DataFrame

DataFrame with trend data:

DataFrame
  • systime: Cycle completion time
DataFrame
  • cycle_time_seconds: Actual cycle time
DataFrame
  • moving_avg: Moving average cycle time
DataFrame
  • trend: 'improving', 'stable', or 'degrading'
Example

import pandas as pd, ts_shape df = pd.concat([ts_shape.make_id_signal("part", ["A", "B"], hold=30), ... ts_shape.make_timeseries(["cycle"], n_points=60, value_column="value_bool")]) trend = ts_shape.CycleTimeEvents(df).cycle_time_trend( ... "part", "cycle", "A", window_size=3) trend[["cycle_time_seconds", "moving_avg", "trend"]].tail(3) cycle_time_seconds moving_avg trend 4 60.0 120.0 improving 5 120.0 120.0 stable 6 120.0 100.0 improving

hourly_cycle_time_summary ¤

hourly_cycle_time_summary(
    part_id_uuid: str,
    cycle_trigger_uuid: str,
    *,
    value_column_part: str = "value_string",
    value_column_trigger: str = "value_bool",
    max_cycle_gap: str | None = None
) -> pd.DataFrame

Hourly summary of cycle times by part.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
cycle_trigger_uuid str

UUID for cycle completion trigger

required
value_column_part str

Column containing part numbers

'value_string'
value_column_trigger str

Column containing cycle triggers

'value_bool'
max_cycle_gap str | None

Optional cap (e.g. "5min") forwarded to :meth:cycle_time_by_part: intervals between consecutive completions longer than this are dropped as inter-cycle downtime. Default None keeps every interval.

None

Returns:

Type Description
DataFrame

DataFrame with hourly statistics:

DataFrame
  • hour: Hour timestamp
DataFrame
  • part_number: Part number/ID
DataFrame
  • cycles_completed: Number of cycles
DataFrame
  • avg_cycle_time: Average cycle time
DataFrame
  • min_cycle_time: Fastest cycle
DataFrame
  • max_cycle_time: Slowest cycle
Example

import pandas as pd, ts_shape df = pd.concat([ts_shape.make_id_signal("part", ["A", "B"], hold=30), ... ts_shape.make_timeseries(["cycle"], n_points=60, value_column="value_bool")]) hourly = ts_shape.CycleTimeEvents(df).hourly_cycle_time_summary("part", "cycle") hourly[["part_number", "cycles_completed", "avg_cycle_time"]] part_number cycles_completed avg_cycle_time 0 A 7 111.428571 1 B 5 198.000000

DowntimeEvents ¤

DowntimeEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None,
    specs: Mapping[str, SignalSpec] | None = None
)

Bases: Base

Track machine downtimes by shift and reason.

Each UUID represents one signal: - run_state_uuid: machine state (running/stopped/idle) - reason_uuid: downtime reason code (optional)

Merge keys: [date, shift] for shift-level, [period] for trend data.

Run state comes in one of two forms, and both give the same result:

  • a string state signal in value_string (running_value="Running", stopped_value="Stopped"), or
  • a boolean run signal in value_bool (True = running). It is used when the state rows carry value_bool but no value_string, or when running_value / stopped_value is a bool (then matched as given).

Durations are reported in seconds (*_seconds); the *_minutes columns are kept as a convenience.

Pipeline example::

downtime = DowntimeEvents(df)
shift_dt = downtime.downtime_by_shift('machine_state')
# → merge with QualityEvents.nok_by_shift() on [date, shift]
# → merge with ShiftReportEvents.shift_production() on [date, shift]
# → feed into ShiftHandoverEvents.from_shift_data()
Example

import ts_shape df = ts_shape.make_id_signal("state", ["Running", "Stopped", "Running"], ... hold=20, freq="1min", start="2025-01-01 08:00") out = ts_shape.DowntimeEvents(df).downtime_by_shift("state") out[["date", "shift", "downtime_minutes", "availability_pct"]].round(1) date shift downtime_minutes availability_pct 0 2025-01-01 shift_1 20.0 66.1

Initialize downtime tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data

required
time_column str

Name of timestamp column (default: 'systime')

'systime'
shift_definitions dict[str, tuple[str, str]] | None

Dictionary mapping shift names to (start, end) times Default: 3-shift operation (06:00-14:00, 14:00-22:00, 22:00-06:00)

None
specs Mapping[str, SignalSpec] | None

Optional signal semantics (see :mod:ts_shape.signals). A state signal's max_gap then caps durations by default.

None

downtime_by_shift ¤

downtime_by_shift(
    run_state_uuid: str,
    *,
    running_value: str | bool = "Running",
    value_column: str = "value_string",
    max_gap: str | None = None,
    production_day: bool = True
) -> pd.DataFrame

Calculate downtime duration per shift.

Each reading's state holds until the next reading, and that interval is split at the shift boundaries (the ISO 22400-2 time model): a stop from 07:00 to 15:00 is 7 h of downtime in the 06:00-14:00 shift and 1 h in the 14:00-22:00 shift. A shift is reported when any observed time falls in it.

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal

required
running_value str | bool

Value that indicates machine is running. A bool matches a boolean run signal (value_bool by default); a boolean run signal is also used, with True as running, when the state rows carry no value_column value.

'Running'
value_column str

Column containing state values

'value_string'
max_gap str | None

Optional cap (e.g. "15min"). A duration between two consecutive readings longer than this is treated as a logging gap ("no data") and excluded, instead of being attributed to the preceding state. Without it, a logger outage between a Running sample and the next reading is booked as uptime, inflating availability toward 100 % over a period with no data. None (default) uses the state signal's spec max_gap when specs were given and applies no cap otherwise, so every interval between readings, however long, counts toward the preceding state.

None
production_day bool

Attribute an overnight shift's post-midnight readings to the shift's start date, so a night shift is one (date, shift) row (default). False groups by raw calendar date, which splits a night shift across two rows.

True

Returns:

Type Description
DataFrame

DataFrame with downtime by shift:

DataFrame
  • start: Start of the shift window (tz-aware in the data's tz)
DataFrame
  • end: End of the shift window (next day for overnight shifts)
DataFrame
  • duration_seconds: Shift window length in seconds
DataFrame
  • date: Production date
DataFrame
  • shift: Shift name
DataFrame
  • total_minutes / total_seconds: Observed shift time (uptime + downtime)
DataFrame
  • downtime_minutes / downtime_seconds: Downtime duration
DataFrame
  • uptime_minutes / uptime_seconds: Running time
DataFrame
  • availability_pct: Uptime percentage
DataFrame
  • excluded_seconds: Time removed by max_gap as "no data" (0 without a cap), split at the shift boundaries like the rest; it is in neither uptime nor downtime
DataFrame

The *_minutes columns are a convenience (*_seconds / 60).

Example

import ts_shape df = ts_shape.make_id_signal( ... "state", ["Running", "Stopped", "Running"], hold=20, freq="1min", ... start="2025-01-01 08:00") out = ts_shape.DowntimeEvents(df).downtime_by_shift("state") out[["shift", "downtime_minutes", "uptime_minutes", "availability_pct"]] shift downtime_minutes uptime_minutes availability_pct 0 shift_1 20.0 39.0 66.101695

downtime_by_reason ¤

downtime_by_reason(
    run_state_uuid: str,
    reason_uuid: str,
    *,
    stopped_value: str | bool = "Stopped",
    value_column_state: str = "value_string",
    value_column_reason: str = "value_string"
) -> pd.DataFrame

Analyze downtime by reason code.

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal

required
reason_uuid str

UUID for downtime reason signal

required
stopped_value str | bool

Value indicating machine is stopped. A bool matches a boolean run signal (value_bool by default); a boolean run signal is also used, with False as stopped, when the state rows carry no value_column_state value.

'Stopped'
value_column_state str

Column containing state values

'value_string'
value_column_reason str

Column containing reason codes

'value_string'

Returns:

Type Description
DataFrame

DataFrame with downtime by reason:

DataFrame
  • reason: Reason code
DataFrame
  • occurrences: Number of downtime events
DataFrame
  • total_minutes / total_seconds: Total downtime for this reason
DataFrame
  • avg_minutes / avg_seconds: Average downtime per occurrence
DataFrame
  • pct_of_total: Percentage of total downtime
DataFrame

The *_minutes columns are a convenience (*_seconds / 60).

DataFrame

Canonical twin columns: share_of_total_pct (after pct_of_total); each

DataFrame

holds the same values as the deprecated column right before it, which is

DataFrame

removed in the next major release.

Example

import pandas as pd, ts_shape states = ["Running", "Stopped", "Running", "Stopped", "Running"] df = pd.concat([ts_shape.make_id_signal("state", states, hold=10, freq="1min"), ... ts_shape.make_id_signal("reason", ["-", "Jam", "-", "Tool", "-"], hold=10, ... freq="1min")]) out = ts_shape.DowntimeEvents(df).downtime_by_reason("state", "reason") out[["reason", "occurrences", "total_minutes", "share_of_total_pct"]] reason occurrences total_minutes share_of_total_pct 0 Jam 10 10.0 50.0 1 Tool 10 10.0 50.0

top_downtime_reasons ¤

top_downtime_reasons(
    run_state_uuid: str,
    reason_uuid: str,
    *,
    top_n: int = 5,
    stopped_value: str | bool = "Stopped",
    value_column_state: str = "value_string",
    value_column_reason: str = "value_string"
) -> pd.DataFrame

Get top N downtime reasons (Pareto analysis).

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal

required
reason_uuid str

UUID for downtime reason signal

required
top_n int

Number of top reasons to return

5
stopped_value str | bool

Value indicating machine is stopped (a string, or a bool for a boolean run signal; see :meth:downtime_by_reason)

'Stopped'
value_column_state str

Column containing state values

'value_string'
value_column_reason str

Column containing reason codes

'value_string'

Returns:

Type Description
DataFrame

DataFrame with top N reasons and cumulative percentage: reason,

DataFrame

total_minutes, total_seconds, pct_of_total, cumulative_pct

DataFrame

Canonical twin columns: share_of_total_pct (after pct_of_total); each

DataFrame

holds the same values as the deprecated column right before it, which is

DataFrame

removed in the next major release.

Example

import pandas as pd, ts_shape states = ["Running", "Stopped", "Running", "Stopped", "Running"] df = pd.concat([ts_shape.make_id_signal("state", states, hold=10, freq="1min"), ... ts_shape.make_id_signal("reason", ["-", "Jam", "-", "Tool", "-"], hold=10, ... freq="1min")]) top = ts_shape.DowntimeEvents(df).top_downtime_reasons("state", "reason", top_n=1) top[["reason", "total_minutes", "cumulative_pct"]] reason total_minutes cumulative_pct 0 Jam 10.0 50.0

availability_trend ¤

availability_trend(
    run_state_uuid: str,
    *,
    running_value: str | bool = "Running",
    value_column: str = "value_string",
    window: str = "1D",
    max_gap: str | None = None
) -> pd.DataFrame

Calculate availability trend over time.

Each reading's state holds until the next reading, and that interval is split at the window boundaries (the ISO 22400-2 time model), so every window holds exactly the up / down time that fell inside it. A window is reported when any observed time falls in it.

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal

required
running_value str | bool

Value that indicates machine is running (a string, or a bool for a boolean run signal; see :meth:downtime_by_shift)

'Running'
value_column str

Column containing state values

'value_string'
window str

Time window for aggregation (e.g., '1D', '1W')

'1D'
max_gap str | None

Optional cap (e.g. "15min"). A gap between consecutive readings longer than this is treated as "no data" and excluded, instead of being attributed to the preceding state (which would inflate availability across a logging outage). None (default) applies no cap: every interval between readings, however long, counts toward the preceding state.

None

Returns:

Type Description
DataFrame

DataFrame with availability trend:

DataFrame
  • start: Period start
DataFrame
  • end: Period end (start + window)
DataFrame
  • duration_seconds: Period length in seconds
DataFrame
  • period: Time period
DataFrame
  • availability_pct: Availability percentage
DataFrame
  • uptime_minutes / uptime_seconds: Total uptime
DataFrame
  • downtime_minutes / downtime_seconds: Total downtime
DataFrame
  • excluded_seconds: Time removed by max_gap as "no data" (0 without a cap), split at the window boundaries
DataFrame

The *_minutes columns are a convenience (*_seconds / 60).

Example

import ts_shape df = ts_shape.make_id_signal( ... "state", ["Running", "Stopped", "Running"], hold=20, freq="1min") out = ts_shape.DowntimeEvents(df).availability_trend("state", window="30min") out[["period", "availability_pct", "downtime_minutes"]] period availability_pct downtime_minutes 0 2025-01-01 00:00:00 66.666667 10.0 1 2025-01-01 00:30:00 65.517241 10.0

DutyCycleEvents ¤

DutyCycleEvents(
    dataframe: DataFrame,
    signal_uuid: str,
    *,
    event_uuid: str = "prod:duty_cycle",
    value_column: str = "value_bool",
    time_column: str = "systime",
    interval_end: str = "next_change",
    data_end: Timestamp | None = None
)

Bases: Base

Production: Duty Cycle Analysis

Analyze on/off patterns from a boolean signal: duty cycle percentage, interval listing, transition counts, and excessive cycling detection.

Methods: - duty_cycle_by_window: On-time percentage per time window. - on_off_intervals: List every on and off interval with duration. - cycle_count: Number of on/off transitions per window. - excessive_cycling: Flag windows with too many transitions.

Initialize the duty-cycle analyzer.

Parameters:

Name Type Description Default
dataframe DataFrame

Long-format input data.

required
signal_uuid str

UUID of the boolean on/off signal (True = on).

required
event_uuid str

UUID assigned to generated events.

'prod:duty_cycle'
value_column str

Column holding the on/off state. Missing values count as off.

'value_bool'
time_column str

Timestamp column.

'systime'
interval_end str

How on/off intervals are closed. "next_change" (default) closes each interval at the next transition, which measures both dense and change-encoded (is_delta) signals. "last_sample" ends each interval at its last sample; on a change-encoded signal (one row per transition) every on/off interval then has zero duration.

'next_change'
data_end Timestamp | None

Optional bound for the final, still-open interval when interval_end="next_change".

None

on_off_intervals ¤

on_off_intervals(
    *, max_gap: str | Timedelta | None = None
) -> pd.DataFrame

List every on and off interval with duration.

Parameters:

Name Type Description Default
max_gap str | Timedelta | None

Longest time between two samples an interval may span (e.g. "15min"). A longer gap is "no data": the interval before it ends at its last sample and a new interval starts after the gap, even if the state is unchanged. None (default): no limit.

None

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, duration_seconds (float),

DataFrame

uuid (event uuid), source_uuid (signal uuid), state (on/off),

DataFrame

duration (Timedelta).

duty_cycle_by_window ¤

duty_cycle_by_window(
    window: str = "1h", *, time_weighted: bool = True
) -> pd.DataFrame

Percentage of time the signal is True per window.

Parameters:

Name Type Description Default
window str

Resample window.

'1h'
time_weighted bool

When True (default), each sample is weighted by the gap to the next sample, so the duty cycle reflects wall-clock on-time on change-encoded and irregularly sampled signals; on_time and off_time are the covered seconds in the window. When False, the duty cycle is the mean of the boolean samples in the window, which equals the time share only under uniform sampling (On@09:00, Off@09:59 reads 50 %).

True

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, on_time, off_time,

DataFrame

duty_cycle_pct.

cycle_count ¤

cycle_count(window: str = '1h') -> pd.DataFrame

Number of on/off transitions per window.

Parameters:

Name Type Description Default
window str

Resample window.

'1h'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, on_count, off_count,

DataFrame

total_transitions.

excessive_cycling ¤

excessive_cycling(
    max_transitions: int = 20, window: str = "1h"
) -> pd.DataFrame

Flag windows where transition count exceeds threshold.

Excessive cycling indicates hunting, instability, or wear risk.

Parameters:

Name Type Description Default
max_transitions int

Threshold for flagging.

20
window str

Resample window.

'1h'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, transition_count,

DataFrame

avg_on_duration, avg_off_duration.

FlowConstraintEvents ¤

FlowConstraintEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    event_uuid: str = "prod:flow"
)

Bases: Base

Production: Flow Constraints

Both detectors work on two boolean run signals only (no buffer level or part-presence signal), so they flag the run-state pattern, not its cause:

  • blocked_events: upstream running while downstream is stopped -- the upstream station keeps producing into a stopped consumer and its output buffer fills.
  • starved_events: downstream running while upstream is stopped -- the downstream station keeps consuming with no upstream supply and drains its input buffer. This is the onset of starvation, not the common "station idle for lack of input" state: once the downstream station actually runs dry and stops, both are stopped and the interval is no longer reported.

blocked_events ¤

blocked_events(
    *,
    roles: dict[str, str],
    tolerance: str = "200ms",
    tolerance_before: str | None = None,
    tolerance_after: str | None = None,
    min_duration: str = "0s",
    interval_end: str = "next_change"
) -> pd.DataFrame

Blocked: upstream_run=True while downstream_run=False.

Flags intervals where the upstream station runs while its downstream consumer is stopped, so the buffer between them fills.

Parameters:

Name Type Description Default
roles dict[str, str]

Dictionary mapping role names to UUIDs. Expected keys: 'upstream_run', 'downstream_run'

required
tolerance str

Default tolerance for time alignment (used if directional tolerances not provided)

'200ms'
tolerance_before str | None

Tolerance for looking backward in time during alignment

None
tolerance_after str | None

Tolerance for looking forward in time during alignment

None
min_duration str

Minimum duration for an event to be included

'0s'
interval_end str

"next_change" (default) ends an interval at the first aligned sample where the condition no longer holds (the transition), so a condition seen at a single sample still lasts until the next sample; a condition that holds until the end of the data ends at the last sample. "last_sample" ends it at the last sample where the condition holds (the behaviour before this option; a single-sample event then has zero duration).

'next_change'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, uuid, source_uuid, is_delta, type,

DataFrame

time_alignment_quality, duration, severity

Raises:

Type Description
ValueError

If interval_end is not "next_change" or "last_sample".

Example

roles = {'upstream_run': 'uuid1', 'downstream_run': 'uuid2'}

starved_events ¤

starved_events(
    *,
    roles: dict[str, str],
    tolerance: str = "200ms",
    tolerance_before: str | None = None,
    tolerance_after: str | None = None,
    min_duration: str = "0s",
    interval_end: str = "next_change"
) -> pd.DataFrame

Starved: downstream_run=True while upstream_run=False.

Flags intervals where the downstream station runs while its upstream supplier is stopped, i.e. downstream is consuming its input buffer with no resupply. A downstream station that has already stopped for lack of input (both stations stopped) is not reported; with run signals alone that state cannot be told apart from any other joint stop.

Parameters:

Name Type Description Default
roles dict[str, str]

Dictionary mapping role names to UUIDs. Expected keys: 'upstream_run', 'downstream_run'

required
tolerance str

Default tolerance for time alignment (used if directional tolerances not provided)

'200ms'
tolerance_before str | None

Tolerance for looking backward in time during alignment

None
tolerance_after str | None

Tolerance for looking forward in time during alignment

None
min_duration str

Minimum duration for an event to be included

'0s'
interval_end str

"next_change" (default) ends an interval at the first aligned sample where the condition no longer holds (the transition), so a condition seen at a single sample still lasts until the next sample; a condition that holds until the end of the data ends at the last sample. "last_sample" ends it at the last sample where the condition holds (the behaviour before this option; a single-sample event then has zero duration).

'next_change'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, uuid, source_uuid, is_delta, type,

DataFrame

time_alignment_quality, duration, severity

Raises:

Type Description
ValueError

If interval_end is not "next_change" or "last_sample".

Example

roles = {'upstream_run': 'uuid1', 'downstream_run': 'uuid2'}

flow_constraint_analytics ¤

flow_constraint_analytics(
    *,
    roles: dict[str, str],
    tolerance: str = ...,
    tolerance_before: str | None = ...,
    tolerance_after: str | None = ...,
    min_duration: str = ...,
    warning_threshold: str = ...,
    critical_threshold: str = ...,
    interval_end: str = ...,
    as_frame: Literal[True] = ...
) -> pd.DataFrame
flow_constraint_analytics(
    *,
    roles: dict[str, str],
    tolerance: str = ...,
    tolerance_before: str | None = ...,
    tolerance_after: str | None = ...,
    min_duration: str = ...,
    warning_threshold: str = ...,
    critical_threshold: str = ...,
    interval_end: str = ...,
    as_frame: Literal[False]
) -> dict[str, Any]
flow_constraint_analytics(
    *,
    roles: dict[str, str],
    tolerance: str = "200ms",
    tolerance_before: str | None = None,
    tolerance_after: str | None = None,
    min_duration: str = "0s",
    warning_threshold: str = "5s",
    critical_threshold: str = "30s",
    interval_end: str = "next_change",
    as_frame: bool = True
) -> pd.DataFrame | dict[str, Any]

Generate comprehensive analytics for flow constraints (blockages and starvations).

Parameters:

Name Type Description Default
roles dict[str, str]

Dictionary mapping role names to UUIDs. Expected keys: 'upstream_run', 'downstream_run'

required
tolerance str

Default tolerance for time alignment (used if directional tolerances not provided)

'200ms'
tolerance_before str | None

Tolerance for looking backward in time during alignment

None
tolerance_after str | None

Tolerance for looking forward in time during alignment

None
min_duration str

Minimum duration for an event to be included

'0s'
warning_threshold str

Duration below which an event is 'minor'.

'5s'
critical_threshold str

Duration below which an event is 'moderate' (at or above it, 'severe').

'30s'
interval_end str

Forwarded to :meth:blocked_events and :meth:starved_events: "next_change" (default) ends an event at the transition, "last_sample" at the last sample where the condition holds.

'next_change'
as_frame bool

When True (default), return a summary DataFrame (see below). as_frame=False returns the dictionary form; it is deprecated, emits a DeprecationWarning and will be removed in the next major release.

True

Returns:

Name Type Description
DataFrame | dict[str, Any]

With as_frame=True (default), a summary DataFrame with two

DataFrame | dict[str, Any]

rows, constraint_type "blocked" then "starved", and

columns DataFrame | dict[str, Any]
DataFrame | dict[str, Any]
  • start, end, duration_seconds: first event start to last event end of that type (NaT / 0 when there is none)
DataFrame | dict[str, Any]
  • uuid: the event uuid; source_uuid: the upstream (blocked) or downstream (starved) run signal
DataFrame | dict[str, Any]
  • constraint_type
DataFrame | dict[str, Any]
  • event_count, total_duration_seconds, avg_duration_seconds (0 when there is no event)
DataFrame | dict[str, Any]
  • minor_count, moderate_count, severe_count: the severity breakdown
DataFrame | dict[str, Any]
  • time_alignment_quality: share of samples aligned between the two signals (0 when there is no event)
DataFrame | dict[str, Any]

The totals of the dict form are the column sums:

DataFrame | dict[str, Any]

total_constraint_events is event_count.sum() and

DataFrame | dict[str, Any]

overall_alignment_quality is time_alignment_quality.mean().

DataFrame | dict[str, Any]

The events themselves come from :meth:blocked_events and

DataFrame | dict[str, Any]

meth:starved_events.

DataFrame | dict[str, Any]

With as_frame=False (deprecated), a dictionary containing

DataFrame | dict[str, Any]

analytics for both blocked and starved events:

DataFrame | dict[str, Any]
  • blocked_events: DataFrame of blocked events
DataFrame | dict[str, Any]
  • starved_events: DataFrame of starved events
DataFrame | dict[str, Any]
  • summary: Dictionary with statistics including:
  • blocked_count: Total number of blocked events
  • starved_count: Total number of starved events
  • blocked_total_duration: Total duration of blocked events
  • starved_total_duration: Total duration of starved events
  • blocked_avg_duration: Average duration of blocked events
  • starved_avg_duration: Average duration of starved events
  • blocked_severity_breakdown: Count by severity level
  • starved_severity_breakdown: Count by severity level
  • overall_alignment_quality: Average alignment quality across both types
Example

import pandas as pd, ts_shape t = pd.date_range("2025-01-01", periods=6, freq="10s") up = pd.DataFrame({"systime": t, "uuid": "up", "value_bool": True}) down = up.assign(uuid="down", value_bool=[True, False, False, True, True, True]) flow = ts_shape.FlowConstraintEvents(pd.concat([up, down])) out = flow.flow_constraint_analytics(roles={"upstream_run": "up", "downstream_run": "down"}) out[["constraint_type", "event_count", "total_duration_seconds"]] constraint_type event_count total_duration_seconds 0 blocked 1 20.0 1 starved 0 0.0

FlowMetricsEvents ¤

FlowMetricsEvents(
    dataframe: DataFrame,
    entry_uuid: str,
    exit_uuid: str,
    *,
    event_uuid: str = "prod:flow_metrics",
    value_column: str = "value_bool",
    time_column: str = "systime"
)

Bases: Base

WIP, throughput, lead time and Little's Law metrics for a process.

Example usage::

flow = FlowMetricsEvents(df, entry_uuid="u_in", exit_uuid="u_out")
flow.wip_over_time(window="1h")
flow.throughput(window="1h")
flow.lead_time()
flow.flow_summary(value_add_seconds=120, window="1h")

Initialize the flow-metrics analyzer.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
entry_uuid str

UUID of the boolean unit-entry signal.

required
exit_uuid str

UUID of the boolean unit-exit signal.

required
event_uuid str

UUID to tag derived events with.

'prod:flow_metrics'
value_column str

Column holding the boolean trigger.

'value_bool'
time_column str

Name of the timestamp column.

'systime'

wip_over_time ¤

wip_over_time(window: str = '1h') -> pd.DataFrame

Time-weighted work-in-process per window.

Parameters:

Name Type Description Default
window str

Resample window (e.g. "1h").

'1h'

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns: start, end, duration_seconds,

DataFrame

wip_mean, wip_max, wip_min.

throughput ¤

throughput(window: str = '1h') -> pd.DataFrame

Units completed (exit rising edges) per window.

Parameters:

Name Type Description Default
window str

Resample window.

'1h'

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns: start, end, duration_seconds,

DataFrame

units_out, throughput_per_hour.

lead_time ¤

lead_time() -> pd.DataFrame

FIFO-matched lead time per unit (entry -> exit).

The nth entry is matched to the nth exit (first-in-first-out).

Returns:

Type Description
DataFrame

Point-shape DataFrame with columns: systime (exit time), uuid,

DataFrame

source_uuid, lead_time_seconds, unit_index.

flow_summary ¤

flow_summary(
    *,
    value_add_seconds: Number | None = None,
    window: str = "1h"
) -> pd.DataFrame

Combined flow metrics with a Little's Law consistency check.

Little's Law lead time = WIP / throughput. consistency_ratio is the measured FIFO lead time divided by that prediction (≈ 1 for a stable, FIFO process). Process Cycle Efficiency = value-add time / lead time.

Parameters:

Name Type Description Default
value_add_seconds Number | None

Value-add (touch) time per unit, for PCE. Seconds number or offset string; omit to skip the PCE column.

None
window str

Resample window.

'1h'

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns: start, end, duration_seconds,

DataFrame

wip_mean, throughput_per_hour, lead_time_mean_seconds,

DataFrame

littles_law_lead_time_seconds, consistency_ratio, and

DataFrame

process_cycle_efficiency_pct when value_add_seconds is given.

LineBalancingEvents ¤

LineBalancingEvents(
    dataframe: DataFrame,
    station_uuids: dict[str, str],
    *,
    event_uuid: str = "prod:line_balance",
    value_column: str = "value_bool",
    time_column: str = "systime"
)

Bases: Base

Line balancing and takt analysis from per-station cycle signals.

Example usage::

lb = LineBalancingEvents(
    df,
    station_uuids={
        "uuid_s1": "Station 1",
        "uuid_s2": "Station 2",
        "uuid_s3": "Station 3",
    },
)
lb.station_cycle_times(window="1h")
lb.balance_metrics(takt_time="55s", window="1h")
lb.yamazumi(demand=480, available_time="8h")

Initialize the line-balancing analyzer.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
station_uuids dict[str, str]

Mapping of cycle-completion-trigger UUID -> station name, in line order, e.g. {"u1": "Station 1"}.

required
event_uuid str

UUID to tag derived events with.

'prod:line_balance'
value_column str

Column holding the boolean cycle trigger.

'value_bool'
time_column str

Name of the timestamp column.

'systime'

station_cycle_times ¤

station_cycle_times(window: str = '1h') -> pd.DataFrame

Per-station cycle-time statistics per time window.

Parameters:

Name Type Description Default
window str

Resample window (e.g. "1h", "30min").

'1h'

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns: start, end, duration_seconds,

DataFrame

uuid, station_name, cycle_time_mean, cycle_time_median,

DataFrame

cycle_time_std, cycle_count.

balance_metrics ¤

balance_metrics(
    *,
    takt_time: Number | None = None,
    demand: float | None = None,
    available_time: Number | None = None,
    window: str = "1h",
    require_all_stations: bool = False
) -> pd.DataFrame

Line-level balance metrics per time window.

Balance efficiency = sum(station times) / (n_stations * bottleneck). Takt is resolved from takt_time or from demand plus available_time; when neither is given, theoretical_min_stations is NaN.

Parameters:

Name Type Description Default
takt_time Number | None

Takt time as seconds or an offset string (e.g. "55s").

None
demand float | None

Units required over available_time.

None
available_time Number | None

Available production time (seconds or offset string).

None
window str

Resample window.

'1h'
require_all_stations bool

When False (default), n_stations counts only stations that completed at least one cycle in the window. A completely stopped or starved station -- the worst imbalance -- then drops out of the denominator and the line reads as well balanced. When True, the constructor's full set of station_uuids is used as the denominator, so a missing station drags efficiency down, and a stations_missing column reports how many known stations produced no cycle.

False

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns: start, end, duration_seconds,

DataFrame

n_stations, bottleneck_uuid, bottleneck_cycle_time, takt_seconds,

DataFrame

balance_efficiency_pct, balance_delay_pct, smoothness_index,

DataFrame

theoretical_min_stations (plus stations_missing when

DataFrame

require_all_stations=True).

yamazumi ¤

yamazumi(
    *,
    takt_time: Number | None = None,
    demand: float | None = None,
    available_time: Number | None = None
) -> pd.DataFrame

Yamazumi (station-loading) table over the whole dataset.

Parameters:

Name Type Description Default
takt_time Number | None

Takt time as seconds or an offset string.

None
demand float | None

Units required over available_time.

None
available_time Number | None

Available production time (seconds or offset string).

None

Returns:

Type Description
DataFrame

DataFrame with columns: uuid, station_name, cycle_time_mean,

DataFrame

takt_seconds, loading_pct, idle_to_takt_seconds, is_bottleneck.

DataFrame

Stations are returned in the order given to the constructor.

LineThroughputEvents ¤

LineThroughputEvents(
    dataframe: DataFrame,
    *,
    event_uuid: str = "prod:throughput",
    time_column: str = "systime"
)

Bases: Base

Production: Line Throughput

Methods: - count_parts: Part counts per fixed window from a monotonically increasing counter. - takt_adherence: Cycle time violations against a takt time from step/boolean triggers. - cycle_quality_check: Cycles from a trigger signal, flagged when outside the expected range. - throughput_oee: Availability, performance and OEE ratios (0-1) per window. - throughput_trends: Throughput trend with moving averages and degradation flags.

count_parts ¤

count_parts(
    counter_uuid: str,
    *,
    value_column: str = "value_integer",
    window: str = "1min",
    handle_resets: bool = True
) -> pd.DataFrame

Compute parts per window for a counter uuid.

When handle_resets is True (default), per-reading increments are computed on the raw counter (a decreasing counter is treated as a reset and the post-reset value counted as new production) and summed into each window, and a resets column reports the number of resets per window. When False, the count per window is the window-over-window difference of the counter maximum, clipped at zero, so a window in which the counter resets to a lower value loses that production.

Parameters:

Name Type Description Default
counter_uuid str

UUID of the part counter signal.

required
value_column str

Column holding the counter value.

'value_integer'
window str

Count window (pandas offset string, e.g. '1min', '1h').

'1min'
handle_resets bool

Count production across counter resets (see above) and add a resets column. Default True.

True

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, is_delta, count (plus

DataFrame

resets when handle_resets=True).

takt_adherence ¤

takt_adherence(
    cycle_uuid: str,
    *,
    value_column: str = "value_bool",
    takt_time: str = "60s",
    min_violation: str = "0s"
) -> pd.DataFrame

Flag cycles whose durations exceed the takt_time.

For boolean triggers: detect True rising edges as cycle boundaries. For numeric counters/steps: detect increments as cycle boundaries; a decrease (a counter reset) is not a cycle boundary.

Parameters:

Name Type Description Default
cycle_uuid str

UUID of the cycle trigger signal.

required
value_column str

Column holding the trigger. "value_bool" uses rising edges as cycle boundaries; a numeric column uses every increase of the value (a reset or other decrease is not a boundary, so the cycle spanning it runs to the next increase); a non-numeric column (e.g. a string step name) uses every change of value.

'value_bool'
takt_time str

Takt time (pandas offset string, e.g. '60s').

'60s'
min_violation str

How far a cycle must exceed takt_time to be a violation: a cycle is a violation when it is strictly longer than takt_time + min_violation. Default "0s": any cycle longer than the takt time counts; a cycle exactly equal to the takt time does not.

'0s'

Returns:

Type Description
DataFrame

DataFrame with columns: systime (at the boundary that closes the

DataFrame

cycle), uuid, source_uuid, is_delta, cycle_time_seconds,

DataFrame

violation.

throughput_oee ¤

throughput_oee(
    counter_uuid: str,
    *,
    value_column: str = "value_integer",
    window: str = "1h",
    target_rate: float | None = None,
    availability_threshold: float = 0.0,
    handle_resets: bool = True
) -> pd.DataFrame

Calculate Overall Equipment Effectiveness (OEE) metrics.

OEE = Availability × Performance × Quality

This counter-only estimate has no run-state signal, so availability is a production proxy: a window counts as available when its part count is at least availability_threshold × target_rate. Quality is assumed to be 1.0.

Parameters:

Name Type Description Default
counter_uuid str

UUID for the part counter signal

required
value_column str

Column containing counter values

'value_integer'
window str

Time window for aggregation

'1h'
target_rate float | None

Target production rate (parts per window). If None, uses the 95th percentile of observed counts

None
availability_threshold float

Fraction of target_rate a window must reach to count as available (0.0-1.0). The default 0.0 keeps the historical behavior -- any window with at least one part is available. (Previously this argument was accepted but ignored.)

0.0
handle_resets bool

When True (default), the part counter is treated as reset-aware (see :meth:count_parts), so a window where the counter resets is not dropped. Default True.

True

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, is_delta, actual_count,

DataFrame

target_count, availability, performance, oee_score, followed by

DataFrame

the basis of the ratios:

DataFrame
  • run_seconds: the window length when the window counts as available, else 0 (the counter-only availability proxy)
DataFrame
  • planned_seconds: the window length
DataFrame
  • resets (only when handle_resets=True): counter resets in the window (see :meth:count_parts)
DataFrame

Canonical twin columns: availability_ratio (after availability),

DataFrame

performance_ratio (after performance), oee_ratio (after

DataFrame

oee_score); each holds the same values as the deprecated column right

DataFrame

before it, which is removed in the next major release.

throughput_trends(
    counter_uuid: str,
    *,
    value_column: str = "value_integer",
    window: str = "1h",
    trend_window: int = 24
) -> pd.DataFrame

Analyze throughput trends with moving averages and degradation detection.

Parameters:

Name Type Description Default
counter_uuid str

UUID for the part counter signal

required
value_column str

Column containing counter values

'value_integer'
window str

Time window for counting parts

'1h'
trend_window int

Number of windows for trend calculation

24

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, is_delta, count, moving_avg,

DataFrame

trend_direction, degradation_detected

cycle_quality_check ¤

cycle_quality_check(
    cycle_uuid: str,
    *,
    value_column: str = "value_bool",
    expected_cycle_time: float | None = None,
    tolerance_pct: float = 0.1
) -> pd.DataFrame

Enhanced cycle detection with quality validation.

Parameters:

Name Type Description Default
cycle_uuid str

UUID for the cycle trigger signal

required
value_column str

Column containing cycle trigger (bool/integer)

'value_bool'
expected_cycle_time float | None

Expected cycle time in seconds. If None, uses median

None
tolerance_pct float

Tolerance percentage for cycle time validation

0.1

Returns:

Type Description
DataFrame

DataFrame with cycle times, validation status, and quality flags

LongDowntimeEvents ¤

LongDowntimeEvents(
    dataframe: DataFrame,
    run_state_uuid: str,
    *,
    event_uuid: str = "prod:long_downtime",
    value_column: str = "value_bool",
    time_column: str = "systime",
    value_range: (
        tuple[float | None, float | None] | None
    ) = None
)

Bases: Base

Production: Long Downtime Detection

Detect downtime intervals exceeding a minimum duration threshold from a boolean or integer state signal, then count production events that occur between consecutive long-downtime boundaries.

  • LongDowntimeEvents: Long idle intervals and inter-gap production counts.
  • detect_long_downtime: Find idle/stopped intervals >= min_gap (default 3 h).
  • count_events_between_gaps: Count production events between consecutive long-downtime boundaries.

detect_long_downtime ¤

detect_long_downtime(
    min_gap: str = "3h",
    *,
    interval_end: str = "next_change",
    data_end: Timestamp | None = None
) -> pd.DataFrame

Return idle/stopped intervals whose duration is >= min_gap.

Parameters:

Name Type Description Default
min_gap str

Minimum downtime duration to report (pandas offset string). Default "3h".

'3h'
interval_end str

How each downtime interval is closed. "next_change" (default) closes the interval at the following transition, so stops on change-encoded (is_delta) state signals are measured. The final, still-open downtime is bounded by data_end when given (otherwise it stays zero-length and is filtered out). "last_sample" ends it at its last sample; on a change-encoded signal (one row per transition) a multi-hour stop is then a single zero-duration row and is missed.

'next_change'
data_end Timestamp | None

Optional bound for a downtime still open at the end of the data (e.g. "now").

None

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, duration_seconds,

DataFrame

downtime_index, uuid, source_uuid, is_delta.

count_events_between_gaps ¤

count_events_between_gaps(
    production_df: DataFrame,
    production_uuid: str,
    *,
    min_gap: str = "3h",
    value_column: str = "value_integer",
    aggregation: str = "count",
    interval_end: str = "next_change",
    data_end: Timestamp | None = None
) -> pd.DataFrame

Count production events in each window between consecutive long downtimes.

Parameters:

Name Type Description Default
production_df DataFrame

ts-shape DataFrame with the production signal.

required
production_uuid str

UUID of the production signal in production_df.

required
min_gap str

Minimum downtime duration to qualify as a long-downtime boundary.

'3h'
value_column str

Column used for 'sum' and 'transitions' aggregations.

'value_integer'
aggregation str

How to count events in each window: - 'count': number of rows in the window - 'sum': sum of value_column in the window - 'transitions': number of value changes (state changes)

'count'
interval_end str

How each downtime interval is closed; forwarded to :meth:detect_long_downtime. "next_change" (default) closes it at the next transition; "last_sample" ends it at its last sample, which makes every stop on a change-encoded (is_delta) state signal zero-length.

'next_change'
data_end Timestamp | None

Optional bound for a downtime still open at the end of the data; forwarded to :meth:detect_long_downtime.

None

Returns:

Name Type Description
DataFrame

DataFrame with one row per inter-gap window.

Columns DataFrame

start, end, window_duration_seconds, event_count, downtime_index, uuid, source_uuid, is_delta

MachineStateEvents ¤

MachineStateEvents(
    dataframe: DataFrame,
    run_state_uuid: str,
    *,
    event_uuid: str = "prod:run_idle",
    value_column: str = "value_bool",
    time_column: str = "systime",
    value_range: (
        tuple[float | None, float | None] | None
    ) = None,
    specs: Mapping[str, SignalSpec] | None = None
)

Bases: Base

Production: Machine State

Detect run/idle transitions and intervals from a boolean state signal.

  • MachineStateEvents: Run/idle state intervals and transitions.
  • detect_run_idle: Intervalize run/idle states with optional min duration filter.
  • transition_events: Point events on state changes (idle->run, run->idle).
Example

import pandas as pd, ts_shape t = pd.date_range("2025-01-01 08:00", periods=6, freq="10min") df = pd.DataFrame({"systime": t, "uuid": "run", ... "value_bool": [True, True, False, True, True, False]}) out = ts_shape.MachineStateEvents(df, "run").detect_run_idle() out[["start", "state", "duration_seconds"]] start state duration_seconds 0 2025-01-01 08:00:00 run 1200.0 1 2025-01-01 08:20:00 idle 600.0 2 2025-01-01 08:30:00 run 1200.0 3 2025-01-01 08:50:00 idle 0.0

detect_run_idle ¤

detect_run_idle(
    min_duration: str = "0s",
    *,
    interval_end: str | None = None,
    data_end: Timestamp | None = None
) -> pd.DataFrame

Return intervals labeled as 'run' or 'idle'.

  • min_duration: discard intervals shorter than this duration.
  • interval_end: how to close each interval. "next_change" closes each interval at the next transition instant, which measures both dense and change-encoded (is_delta) signals. "last_sample" ends an interval at its last sample; on a change-encoded signal (one row per transition) every interval then has zero duration. Default (None): follow the signal's spec when specs= was given (held states close at the next change, and gaps longer than the spec's max_gap are "no data"), otherwise "next_change".
  • data_end: optional bound for the final, still-open interval when intervals close at the next change (e.g. the end of the data window). Columns: start, end, uuid, source_uuid, is_delta, state, duration_seconds

transition_events ¤

transition_events() -> pd.DataFrame

Return point events at state transitions.

Columns: systime, uuid, source_uuid, is_delta, transition ('idle_to_run'|'run_to_idle'), time_since_last_transition_seconds

detect_rapid_transitions ¤

detect_rapid_transitions(
    threshold: str = "5s", min_count: int = 3
) -> pd.DataFrame

Identify suspicious rapid state changes.

  • threshold: time window to look for rapid transitions
  • min_count: minimum number of transitions within threshold to be considered rapid Returns: DataFrame with systime (= start, the first transition of the burst), uuid (event uuid), source_uuid (run-state uuid), start, end, transition_count, duration_seconds

state_quality_metrics ¤

state_quality_metrics(
    *,
    interval_end: str | None = ...,
    data_end: Timestamp | None = ...,
    as_frame: Literal[True] = ...
) -> pd.DataFrame
state_quality_metrics(
    *,
    interval_end: str | None = ...,
    data_end: Timestamp | None = ...,
    as_frame: Literal[False]
) -> dict[str, Any]
state_quality_metrics(
    *,
    interval_end: str | None = None,
    data_end: Timestamp | None = None,
    as_frame: bool = True
) -> pd.DataFrame | dict[str, Any]

Return quality metrics for the state data.

Parameters:

Name Type Description Default
interval_end str | None

Forwarded to :meth:detect_run_idle. Default None resolves exactly as in :meth:detect_run_idle: follow the signal's spec when specs= was given, otherwise "next_change" (run/idle durations are measured to the next transition).

None
data_end Timestamp | None

Forwarded to :meth:detect_run_idle; optional bound for the final, still-open interval.

None
as_frame bool

When True (default), return a one-row DataFrame (see below). as_frame=False returns the dictionary form; it is deprecated, emits a DeprecationWarning and will be removed in the next major release.

True

Returns:

Type Description
DataFrame | dict[str, Any]

With as_frame=True (default), a one-row summary DataFrame with

DataFrame | dict[str, Any]

columns start, end, duration_seconds (the span of the state

DataFrame | dict[str, Any]

signal; NaT / 0 without data), uuid (event uuid), source_uuid

DataFrame | dict[str, Any]

(run-state uuid), followed by the metrics below.

DataFrame | dict[str, Any]

With as_frame=False (deprecated), a dictionary with:

DataFrame | dict[str, Any]
  • total_transitions: total number of state transitions
DataFrame | dict[str, Any]
  • avg_run_duration: average duration of run states in seconds
DataFrame | dict[str, Any]
  • avg_idle_duration: average duration of idle states in seconds
DataFrame | dict[str, Any]
  • run_idle_ratio: ratio of total run time to total idle time; NaN when there is no idle time (including no intervals at all), since the ratio is undefined
DataFrame | dict[str, Any]
  • data_gaps_detected: number of data gaps found
DataFrame | dict[str, Any]
  • rapid_transitions_detected: number of rapid transition events

MicroStopEvents ¤

MicroStopEvents(
    dataframe: DataFrame,
    run_state_uuid: str,
    *,
    event_uuid: str = "prod:micro_stop",
    value_column: str = "value_bool",
    time_column: str = "systime",
    interval_end: str = "next_change",
    data_end: Timestamp | None = None
)

Bases: Base

Production: Micro-Stop Detection

Detect brief idle intervals that individually seem harmless but accumulate into significant availability losses.

Methods: - detect_micro_stops: Find idle intervals shorter than max_duration. - micro_stop_frequency: Count micro-stops per time window. - micro_stop_impact: Time lost to micro-stops per window. - micro_stop_patterns: Group micro-stops by hour-of-day.

Example

import pandas as pd from ts_shape import MicroStopEvents df = pd.DataFrame({ ... "systime": pd.to_datetime([ ... "2025-01-01 08:00:00", "2025-01-01 08:10:00", "2025-01-01 08:10:20", ... "2025-01-01 08:30:00", "2025-01-01 08:30:45", "2025-01-01 09:00:00"]), ... "uuid": "run", ... "value_bool": [True, False, True, False, True, True], ... }) stops = MicroStopEvents(df, "run").detect_micro_stops(max_duration="30s") stops[["start", "end", "duration_seconds"]] start end duration_seconds 0 2025-01-01 08:10:00 2025-01-01 08:10:20 20.0

The 45 s stop at 08:30 is longer than max_duration and is not listed.

Initialize the micro-stop detector.

Parameters:

Name Type Description Default
dataframe DataFrame

Long-format input data.

required
run_state_uuid str

UUID of the boolean run-state signal (True = running, False = idle).

required
event_uuid str

UUID assigned to generated events.

'prod:micro_stop'
value_column str

Column holding the run state. Missing values count as idle.

'value_bool'
time_column str

Timestamp column.

'systime'
interval_end str

How idle/run intervals are closed. "next_change" (default) closes each interval at the next transition, which measures both dense and change-encoded (is_delta) run-state signals. "last_sample" ends each interval at its last sample; on a change-encoded signal (one row per transition) every interval then has zero duration and no lost time is reported.

'next_change'
data_end Timestamp | None

Optional bound for the final, still-open interval when interval_end="next_change".

None

detect_micro_stops ¤

detect_micro_stops(
    max_duration: str = "30s", min_duration: str = "0s"
) -> pd.DataFrame

Find idle intervals shorter than max_duration.

Parameters:

Name Type Description Default
max_duration str

Maximum duration to qualify as a micro-stop.

'30s'
min_duration str

Minimum duration to include (filter very short glitches).

'0s'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, duration_seconds (float),

DataFrame

uuid (event uuid), source_uuid (run-state uuid), duration

DataFrame

(Timedelta), preceding_run_duration.

micro_stop_frequency ¤

micro_stop_frequency(
    window: str = "1h", max_duration: str = "30s"
) -> pd.DataFrame

Count micro-stops per time window.

Parameters:

Name Type Description Default
window str

Resample window.

'1h'
max_duration str

Maximum idle duration to qualify as micro-stop.

'30s'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, count, total_lost_time,

DataFrame

pct_of_window.

DataFrame

Canonical twin columns: share_of_window_pct (after pct_of_window); each

DataFrame

holds the same values as the deprecated column right before it, which is

DataFrame

removed in the next major release.

micro_stop_impact ¤

micro_stop_impact(
    window: str = "1h", max_duration: str = "30s"
) -> pd.DataFrame

Time lost to micro-stops vs total available time per window.

A stop is classified as a micro-stop by its full duration; its time and the run time are then split at the window boundaries (the ISO 22400-2 time model), so a stop across the hour counts in both hours. Every window from the first interval to the end of the last one is reported (0 when it holds no run or micro-stop time).

Parameters:

Name Type Description Default
window str

Resample window.

'1h'
max_duration str

Maximum idle duration to qualify as micro-stop.

'30s'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end (start + window),

DataFrame

duration_seconds, uuid, source_uuid, total_run_time,

DataFrame

total_micro_stop_time, performance_loss_pct,

DataFrame

availability_loss_pct.

DataFrame

performance_loss_pct is the micro-stop time as a percentage of

DataFrame

run + micro-stop time. Minor stops are a performance loss in

DataFrame

Nakajima's six big losses (idling and minor stoppages), not an

DataFrame

availability loss. availability_loss_pct holds the same value

DataFrame

under its original, misleading name and is kept for

DataFrame

compatibility.

micro_stop_patterns ¤

micro_stop_patterns(
    hour_grouping: bool = True, max_duration: str = "30s"
) -> pd.DataFrame

Group micro-stops by hour-of-day to find clustering patterns.

Parameters:

Name Type Description Default
hour_grouping bool

If True, group by hour. If False, group by shift (8h blocks).

True
max_duration str

Maximum idle duration to qualify as micro-stop.

'30s'

Returns:

Type Description
DataFrame

DataFrame with columns: hour (or shift), avg_count, avg_lost_time.

MultiProcessTraceabilityEvents ¤

MultiProcessTraceabilityEvents(
    dataframe: DataFrame,
    processes: list[dict[str, str]],
    *,
    handovers: list[dict[str, str]] | None = None,
    event_uuid: str = "prod:multi_process_trace",
    value_column_id: str = "value_string",
    value_column_handover: str = "value_integer",
    time_column: str = "systime"
)

Bases: Base

Trace items across a multi-station topology with parallel paths.

Example usage::

trace = MultiProcessTraceabilityEvents(
    df,
    processes=[
        {"id_uuid": "serial_weld1",  "station": "Welding Cell 1"},
        {"id_uuid": "serial_weld2",  "station": "Welding Cell 2"},
        {"id_uuid": "serial_paint",  "station": "Painting"},
        {"id_uuid": "serial_assy",   "station": "Assembly"},
    ],
    handovers=[
        {"uuid": "ho_w1_paint", "from_station": "Welding Cell 1", "to_station": "Painting"},
        {"uuid": "ho_w2_paint", "from_station": "Welding Cell 2", "to_station": "Painting"},
        {"uuid": "ho_paint_assy", "from_station": "Painting", "to_station": "Assembly"},
    ],
)

timeline = trace.build_timeline()
lead = trace.lead_time()
parallel = trace.parallel_activity()
handover_log = trace.handover_log()
paths = trace.routing_paths()
station_stats = trace.station_statistics()

Initialize multi-process traceability.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
processes list[dict[str, str]]

List of process definitions, each a dict with: - id_uuid: UUID of the signal carrying item IDs at this station. - station: Human-readable station name.

required
handovers list[dict[str, str]] | None

Optional list of handover signal definitions, each a dict: - uuid: UUID of the handover signal. - from_station: Station name the item leaves. - to_station: Station name the item arrives at. Handover signal value > 0 (or True) indicates a transfer event.

None
event_uuid str

UUID to tag derived events with.

'prod:multi_process_trace'
value_column_id str

Column holding item ID strings in process signals.

'value_string'
value_column_handover str

Column holding handover trigger values.

'value_integer'
time_column str

Name of timestamp column.

'systime'

build_timeline ¤

build_timeline(
    *, infer_dwell_from_next: bool = False
) -> pd.DataFrame

Build a full timeline of every item at every station.

Handles parallel stations: the same item may appear at overlapping time intervals at different stations (concurrent processing), or different items at parallel cells of the same station type.

Parameters:

Name Type Description Default
infer_dwell_from_next bool

When False (default), an interval's end is the last sample of the item at that station, so a station that logs the item once on arrival reports a dwell of 0. When True, each sequential (non-parallel) visit's end is set to the item's next station arrival, giving the honest arrival-here → arrival-at-next-station dwell. Parallel / overlapping visits keep their own span, since a "next arrival" is not meaningful while the item is at two stations at once.

False

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • item_id: Order / serial number.
DataFrame
  • station: Human-readable station name.
DataFrame
  • id_uuid: Source UUID for this station's ID signal.
DataFrame
  • start: Interval start.
DataFrame
  • end: Interval end.
DataFrame
  • duration_seconds: Time at this station.
DataFrame
  • sample_count: Number of data points.
DataFrame
  • step_sequence: Visit order per item (1-based, by start time).
DataFrame
  • is_parallel: True if this interval overlaps with another station for the same item.
DataFrame
  • uuid: Event UUID.

lead_time ¤

lead_time() -> pd.DataFrame

Compute end-to-end lead time per item across all processes.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • item_id
DataFrame
  • first_station: First station visited.
DataFrame
  • last_station: Last station visited.
DataFrame
  • first_seen: Earliest timestamp.
DataFrame
  • last_seen: Latest timestamp.
DataFrame
  • lead_time_seconds: Total elapsed time.
DataFrame
  • stations_visited: Number of distinct stations.
DataFrame
  • station_path: Ordered station names (" -> ").
DataFrame
  • has_parallel: Whether item had parallel processing.
DataFrame
  • uuid: Event UUID.

parallel_activity ¤

parallel_activity() -> pd.DataFrame

Find items that were processed at multiple stations simultaneously.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • item_id
DataFrame
  • station_a: First overlapping station.
DataFrame
  • station_b: Second overlapping station.
DataFrame
  • overlap_start: Start of overlap period.
DataFrame
  • overlap_end: End of overlap period.
DataFrame
  • overlap_seconds: Duration of overlap.
DataFrame
  • uuid: Event UUID.

handover_log ¤

handover_log() -> pd.DataFrame

Extract handover events and correlate with item IDs.

For each handover signal, detects trigger points (value > 0 or value changes to a truthy state) and resolves which item was being transferred based on the from-station's ID signal at that time.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • timestamp: When the handover fired.
DataFrame
  • item_id: Item being transferred (from from_station's ID signal).
DataFrame
  • from_station: Source station.
DataFrame
  • to_station: Destination station.
DataFrame
  • handover_uuid: UUID of the handover signal.
DataFrame
  • handover_value: Raw value of the handover signal.
DataFrame
  • uuid: Event UUID.

station_statistics ¤

station_statistics(
    *, infer_dwell_from_next: bool = False
) -> pd.DataFrame

Compute dwell-time statistics per station across all items.

Distinguishes parallel cells of the same station type.

Parameters:

Name Type Description Default
infer_dwell_from_next bool

Forwarded to :meth:build_timeline; pass True when stations log an item once on arrival so dwell is measured to the item's next station arrival instead of collapsing to zero.

False

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • station: Station name.
DataFrame
  • id_uuid: Specific cell UUID (for parallel cells).
DataFrame
  • item_count: Distinct items processed.
DataFrame
  • min_dwell_seconds
DataFrame
  • avg_dwell_seconds
DataFrame
  • max_dwell_seconds
DataFrame
  • total_dwell_seconds

routing_paths ¤

routing_paths() -> pd.DataFrame

Analyze routing path frequencies across all items.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • station_path: Ordered station names (" -> ").
DataFrame
  • item_count: Items that followed this path.
DataFrame
  • avg_lead_time_seconds
DataFrame
  • min_lead_time_seconds
DataFrame
  • max_lead_time_seconds
DataFrame
  • has_parallel_steps: Whether path includes parallel processing.

OEEEvents ¤

OEEEvents(
    dataframe: DataFrame, *, time_column: str = "systime"
)

Bases: Base

Calculate Overall Equipment Effectiveness from timeseries signals.

Combines availability (from run/idle state), performance (from part counters and ideal cycle time), and quality (from total/reject counters) into a single OEE metric per day. Each daily row uses the canonical summary schema: start (midnight) / end (next midnight) / duration_seconds (the real day length: 86400, or 23 h / 25 h on DST days with tz-aware data).

Example

import pandas as pd, ts_shape kw = dict(n_points=60, freq="1min") run = ts_shape.make_timeseries(["run"], value_column="value_bool", **kw) parts = ts_shape.make_timeseries(["parts"], noise=0, drift=59, ... value_column="value_integer", **kw) daily = ts_shape.OEEEvents(pd.concat([run, parts])).oee( ... "run", "parts", ideal_cycle_time=30.0) daily[["availability_pct", "performance_pct", "quality_pct", "oee_pct"]].round(1) availability_pct performance_pct quality_pct oee_pct 0 54.2 92.2 100.0 50.0

Initialize OEE calculator.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
time_column str

Name of timestamp column (default: 'systime').

'systime'

availability ¤

availability(
    run_state_uuid: str,
    planned_time_hours: float | None = None,
    *,
    value_column: str = "value_bool",
    max_gap: str | None = None
) -> pd.DataFrame

Calculate availability percentage from run/idle intervals.

Availability = run_time / planned_time (ISO 22400-2: APT / PBT). Each reading's state holds until the next reading; the interval is split at midnight, so a run across midnight counts in both days. When planned_time_hours is None the planned time is the time the state signal covers that day. A day the state signal does not cover is not reported. A value above 100 % (run time longer than a fixed planned time) is kept and reported with a :class:~ts_shape.errors.DataQualityWarning naming the day.

Parameters:

Name Type Description Default
run_state_uuid str

UUID of the boolean run-state signal (True = running).

required
planned_time_hours float | None

Fixed planned production hours per day. If None, the time the state signal covers each day is used.

None
value_column str

Column holding the boolean state.

'value_bool'
max_gap str | None

Optional cap (e.g. "15min"). A sample's duration is the gap to the next reading; a gap longer than this is treated as a logging outage ("no data") and excluded from both run time and planned time. Without it, an overnight or logger gap while the last state was running is booked as run time, inflating availability. Only meaningful when planned_time_hours is None (the planned time is derived from the data). None (default) applies no cap: every interval between readings, however long, counts toward the preceding state.

None

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns:

DataFrame
  • start, end, duration_seconds (canonical day boundaries; real day length)
DataFrame
  • run_seconds
DataFrame
  • planned_seconds
DataFrame
  • availability_pct
DataFrame
  • excluded_seconds: time removed by max_gap as "no data" (split at midnight like the rest; 0 without max_gap)

performance ¤

performance(
    counter_uuid: str,
    ideal_cycle_time: float,
    run_state_uuid: str | None = None,
    *,
    value_column: str = "value_integer",
    value_column_state: str = "value_bool",
    handle_resets: bool = True,
    max_gap: str | None = None
) -> pd.DataFrame

Calculate performance percentage (actual vs ideal throughput).

Performance = (actual_parts * ideal_cycle_time) / run_time (ISO 22400-2: PQ x pICT / APT). If run_state_uuid is None, the time the counter readings cover each day is used as run time. With a state signal, a day it does not cover has unknown run time: run_seconds, ideal_parts and performance_pct are NaN. A value above 100 % means the inputs disagree; it is kept (not clipped) and reported with a :class:~ts_shape.errors.DataQualityWarning naming the day.

Parameters:

Name Type Description Default
counter_uuid str

UUID of the monotonic part counter.

required
ideal_cycle_time float

Ideal cycle time in seconds per part.

required
run_state_uuid str | None

Optional UUID for run-state to compute actual run time.

None
value_column str

Column holding counter values.

'value_integer'
value_column_state str

Column holding boolean run state.

'value_bool'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it, so actual_parts is the reset-aware total. The increments are computed once over the whole signal; the increment between two readings is spread over the time between them and split at midnight in proportion to time (ISO 22400-2: a part belongs to the period it was produced in), so the days add up to the signal total and actual_parts can be fractional. The result gains a resets column with the number of resets observed each day. When False, actual_parts is simply max(0, last - first) and a day containing a reset reports too little.

True
max_gap str | None

Optional cap forwarded to :meth:availability when run_state_uuid is given, so a logging gap does not inflate the run time used as the performance denominator. None (default) applies no cap.

None

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns:

DataFrame
  • start, end, duration_seconds
DataFrame
  • actual_parts
DataFrame
  • ideal_parts
DataFrame
  • run_seconds
DataFrame
  • performance_pct
DataFrame
  • resets (only when handle_resets=True)

quality ¤

quality(
    total_uuid: str,
    reject_uuid: str,
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Calculate quality percentage (good parts / total parts).

Parameters:

Name Type Description Default
total_uuid str

UUID of the total-parts counter.

required
reject_uuid str

UUID of the reject-parts counter.

required
value_column str

Column holding counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset for both the total and the reject counters, so a day containing a reset is not under-reported. Increments are spread over the time between two readings and split at midnight in proportion to time (see :meth:performance), so the days add up to the signal totals and the part counts can be fractional. The result gains a resets column with the combined number of resets (total + reject) observed each day. When False, both quantities are max(0, last - first).

True

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns:

DataFrame
  • start, end, duration_seconds
DataFrame
  • total_parts
DataFrame
  • reject_parts
DataFrame
  • good_parts
DataFrame
  • quality_pct
DataFrame
  • resets (only when handle_resets=True)

oee ¤

oee(
    run_state_uuid: str,
    counter_uuid: str,
    ideal_cycle_time: float,
    total_uuid: str | None = None,
    reject_uuid: str | None = None,
    *,
    planned_time_hours: float | None = None,
    handle_resets: bool = True,
    max_gap: str | None = None
) -> pd.DataFrame

Calculate daily OEE = Availability * Performance * Quality.

When total_uuid / reject_uuid are not provided, quality is assumed to be 100 %.

Parameters:

Name Type Description Default
run_state_uuid str

UUID of the boolean run-state signal.

required
counter_uuid str

UUID of the monotonic part counter.

required
ideal_cycle_time float

Ideal cycle time in seconds per part.

required
total_uuid str | None

Optional UUID of total-parts counter (for quality).

None
reject_uuid str | None

Optional UUID of reject-parts counter (for quality).

None
planned_time_hours float | None

Fixed planned production hours per day.

None
handle_resets bool

When True (default), the part / total / reject counters are treated as reset-aware (see :meth:performance and :meth:quality), so a day where a counter resets does not silently drag OEE toward zero. Forwarded to the performance and quality components. Default True.

True
max_gap str | None

Optional cap (e.g. "15min") forwarded to the availability and performance components so a logging gap is not booked as run time (which would inflate availability and understate performance). None (default) applies no cap: every interval between readings, however long, counts toward the preceding state.

None

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns:

DataFrame
  • start, end, duration_seconds
DataFrame
  • availability_pct
DataFrame
  • performance_pct
DataFrame
  • quality_pct
DataFrame
  • oee_pct
DataFrame

followed by the basis each percentage was computed from, taken

DataFrame

from the components (a day missing from a component reports 0, except

DataFrame

that a day the state signal does not cover has NaN

DataFrame

availability_pct, performance_pct, oee_pct,

DataFrame

run_seconds, planned_seconds and ideal_parts: its run

DataFrame

time is unknown):

DataFrame
  • run_seconds, planned_seconds, excluded_seconds: from :meth:availability (excluded_seconds is the time max_gap removed as "no data")
DataFrame
  • actual_parts, ideal_parts: from :meth:performance
DataFrame
  • total_parts, reject_parts, good_parts: from :meth:quality; NaN when total_uuid / reject_uuid are not given (quality is then assumed to be 100 %)
DataFrame
  • resets (only when handle_resets=True): counter resets of the part counter plus the total / reject counters

OperatingStateEvents ¤

OperatingStateEvents(
    dataframe: DataFrame,
    signal_uuid: str,
    *,
    event_uuid: str = "prod:operating_state",
    value_column: str = "value_double",
    time_column: str = "systime"
)

Bases: Base

Production: Operating State Inference

Infer discrete operating states from continuous signals (vibration, current, power, temperature) when no explicit boolean state signal is available. Answers the question "what state is the machine in?"

Unlike :class:MachineStateEvents which requires a boolean run/idle signal, this class clusters continuous values to discover states automatically.

Methods: - infer_states: Cluster the signal into N operating states. - state_duration_statistics: How long does each state typically last? - unusual_state_sequence: Did states occur in an unexpected order? - dwell_time_violation: Did the machine stay in a state too long?

infer_states ¤

infer_states(
    n_states: int = 2, min_duration: str = "0s"
) -> pd.DataFrame

Cluster the signal into N operating states.

Uses quantile-based binning to assign each sample to a state, then intervalizes consecutive same-state runs.

Parameters:

Name Type Description Default
n_states int

Number of states to infer (2 = low/high, 3 = low/mid/high, etc.).

2
min_duration str

Discard intervals shorter than this.

'0s'

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, state, state_label,

DataFrame

mean_value, duration_seconds, uuid (event uuid), source_uuid

DataFrame

(signal uuid). Samples without a value are ignored; with fewer

DataFrame

valued samples than n_states the result is empty.

state_duration_statistics ¤

state_duration_statistics(
    n_states: int = 2,
) -> pd.DataFrame

How long does each inferred state typically last?

Parameters:

Name Type Description Default
n_states int

Number of states to infer.

2

Returns:

Type Description
DataFrame

DataFrame with columns: state, state_label, count,

DataFrame

mean_duration, std_duration, min_duration, max_duration,

DataFrame

total_duration.

unusual_state_sequence ¤

unusual_state_sequence(
    n_states: int = 2,
    expected_order: list[int] | None = None,
) -> pd.DataFrame

Detect state transitions that violate an expected order.

By default, the expected order is the most common transition sequence observed in the data. Supply expected_order to override (e.g. [0, 1, 2, 1, 0] for a warm-up/run/cool-down cycle).

Parameters:

Name Type Description Default
n_states int

Number of states to infer.

2
expected_order list[int] | None

List of state IDs defining the expected cycle. If None, the most frequent bigram transitions are treated as "normal".

None

Returns:

Type Description
DataFrame

DataFrame with columns: transition_time, from_state,

DataFrame

to_state, from_label, to_label, is_unusual.

dwell_time_violation ¤

dwell_time_violation(
    state: int, max_duration: str, n_states: int = 2
) -> pd.DataFrame

Find intervals where the machine stayed in a state too long.

Parameters:

Name Type Description Default
state int

State ID to check (0-based).

required
max_duration str

Maximum allowed duration (e.g. '5min').

required
n_states int

Number of states to infer.

2

Returns:

Type Description
DataFrame

DataFrame with columns: start, end, state_label,

DataFrame

duration_seconds, excess_seconds, uuid (event uuid), source_uuid

DataFrame

(signal uuid).

OperatorPerformanceEvents ¤

OperatorPerformanceEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None
)

Bases: Base

Track and compare operator performance.

Each UUID represents one signal: - operator_uuid: operator or team identifier signal - counter_uuid: production counter signal - ok_uuid / nok_uuid: OK/NOK part counters (optional, for quality)

Merge keys: [operator] for operator-level, [date, shift] for shift-level.

Pipeline example::

ops = OperatorPerformanceEvents(df)
by_op = ops.production_by_operator('operator_id', 'part_counter')
# → standalone KPI report
quality = ops.quality_by_operator('operator_id', 'good_parts', 'bad_parts')
# → merge with QualityEvents outputs on [date, shift]
Example usage

tracker = OperatorPerformanceEvents(df)

Parts per operator¤

prod = tracker.production_by_operator( operator_uuid='operator_id', counter_uuid='part_counter', )

Efficiency vs target¤

eff = tracker.operator_efficiency( operator_uuid='operator_id', counter_uuid='part_counter', target_per_shift=500, )

production_by_operator ¤

production_by_operator(
    operator_uuid: str,
    counter_uuid: str,
    *,
    value_column_operator: str = "value_string",
    value_column_counter: str = "value_integer",
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Parts produced per operator.

Parameters:

Name Type Description Default
operator_uuid str

UUID for operator/team identifier signal.

required
counter_uuid str

UUID for production counter signal.

required
value_column_operator str

Column containing operator names.

'value_string'
value_column_counter str

Column containing counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production accumulated after the reset instead of losing it. The increments are computed once over the whole signal and booked to the (operator, date, shift) of the reading that reports them, so the operators add up to the signal total and nothing is lost at a shift or operator change. The result gains a resets column with the number of resets observed across the operator's shifts. When False, each shift uses max(0, last - first) and a shift containing a reset reports too little (often 0).

True
production_day bool

When True, an overnight shift's post-midnight readings are attributed to the shift's start date, so a night shift counts as one worked shift. Default True; False groups by raw calendar date. Only affects overnight schedules.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • operator, total_produced, shifts_worked, avg_per_shift
DataFrame
  • resets: Number of counter resets across the operator's shifts (only when handle_resets=True)

operator_efficiency ¤

operator_efficiency(
    operator_uuid: str,
    counter_uuid: str,
    target_per_shift: int,
    *,
    value_column_operator: str = "value_string",
    value_column_counter: str = "value_integer"
) -> pd.DataFrame

Operator efficiency vs a per-shift target.

Parameters:

Name Type Description Default
operator_uuid str

UUID for operator/team identifier signal.

required
counter_uuid str

UUID for production counter signal.

required
target_per_shift int

Target production quantity per shift.

required
value_column_operator str

Column containing operator names.

'value_string'
value_column_counter str

Column containing counter values.

'value_integer'

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • operator, total_produced, target, efficiency_pct

quality_by_operator ¤

quality_by_operator(
    operator_uuid: str,
    ok_uuid: str,
    nok_uuid: str,
    *,
    value_column_operator: str = "value_string",
    value_column_counter: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Quality metrics (First Pass Yield) per operator.

Parameters:

Name Type Description Default
operator_uuid str

UUID for operator/team identifier signal.

required
ok_uuid str

UUID for good parts counter.

required
nok_uuid str

UUID of the NOK part counter.

required
value_column_operator str

Column containing operator names.

'value_string'
value_column_counter str

Column containing counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the OK/NOK parts accumulated after the reset, for both the OK and NOK counters. Because the counts are taken over the whole range, a single reset otherwise wipes out an operator's entire tally. The result gains a resets column with the number of resets observed across the operator's OK and NOK counters. The increments are computed once over each whole signal and booked to the operator of the reading that reports them, so the operators add up to the signal totals (also when operators alternate). When False, both counters use max(0, last - first) per operator and a reset is lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • operator, ok_count, nok_count, first_pass_yield_pct
DataFrame
  • resets: Number of counter resets across the operator's OK and NOK counters (only when handle_resets=True)

operator_comparison ¤

operator_comparison(
    operator_uuid: str,
    counter_uuid: str,
    *,
    value_column_operator: str = "value_string",
    value_column_counter: str = "value_integer"
) -> pd.DataFrame

Ranked operator performance comparison.

Parameters:

Name Type Description Default
operator_uuid str

UUID for operator/team identifier signal.

required
counter_uuid str

UUID for production counter signal.

required
value_column_operator str

Column containing operator names.

'value_string'
value_column_counter str

Column containing counter values.

'value_integer'

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • operator, total_produced, rank, pct_of_best
DataFrame

Canonical twin columns: share_of_best_pct (after pct_of_best); each holds

DataFrame

the same values as the deprecated column right before it, which is removed

DataFrame

in the next major release.

ValueTraceabilityEvents ¤

ValueTraceabilityEvents(
    dataframe: DataFrame,
    station_uuids: dict[str, str],
    *,
    event_uuid: str = "prod:value_trace",
    value_column: str = "value_string",
    time_column: str = "systime"
)

Bases: Base

Trace a shared identifier across multiple stations.

Each station has its own UUID that carries a string value (the identifier currently being processed — serial number, order ID, batch code, etc.). This module detects when a given identifier appears at each station and builds a timeline.

Example usage

trace = ValueTraceabilityEvents( df, station_uuids={ 'station_a_uuid': 'Station A', 'station_b_uuid': 'Station B', 'station_c_uuid': 'Station C', }, )

Full timeline of every identifier across all stations¤

timeline = trace.build_timeline()

Lead time from first to last station¤

lead = trace.lead_time()

Where is each identifier right now?¤

status = trace.current_status()

Station dwell-time statistics¤

dwell = trace.station_dwell_statistics()

Initialize value traceability.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
station_uuids dict[str, str]

Mapping of UUID -> human-readable station name. e.g. {'uuid_abc': 'Station A', 'uuid_def': 'Station B'}

required
event_uuid str

UUID to tag derived events with.

'prod:value_trace'
value_column str

Column holding the identifier string value.

'value_string'
time_column str

Name of timestamp column.

'systime'

build_timeline ¤

build_timeline(
    *, infer_dwell_from_next: bool = False
) -> pd.DataFrame

Build a full timeline of every identifier at every station.

Parameters:

Name Type Description Default
infer_dwell_from_next bool

When False (default), an interval's end is the last sample of the identifier at that station, so a station that logs the identifier only once on arrival (the common event/change-encoded case) reports a dwell of 0. When True, each visit's end is set to the same identifier's next station arrival, so the dwell is the honest arrival-here → arrival-at-next-station time. The last visit per identifier stays open (keeps its own last sample).

False

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • identifier: The shared identifier string.
DataFrame
  • station_uuid: UUID of the station signal.
DataFrame
  • station_name: Human-readable station name.
DataFrame
  • start: Timestamp when the identifier appeared at the station.
DataFrame
  • end: Timestamp of the last sample at the station.
DataFrame
  • duration_seconds: Time spent at the station.
DataFrame
  • sample_count: Number of data points.
DataFrame
  • station_sequence: Order of visit (1-based) per identifier.
DataFrame
  • uuid: Event UUID.
DataFrame
  • source_uuid: UUID of the station signal (same as station_uuid).

lead_time ¤

lead_time() -> pd.DataFrame

Compute end-to-end lead time per identifier.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • identifier
DataFrame
  • first_station: Name of first station visited.
DataFrame
  • last_station: Name of last station visited.
DataFrame
  • first_seen: Earliest timestamp across all stations.
DataFrame
  • last_seen: Latest timestamp across all stations.
DataFrame
  • lead_time_seconds: Total time from first seen to last seen.
DataFrame
  • stations_visited: Number of distinct stations visited.
DataFrame
  • station_path: Arrow-separated ordered station names.
DataFrame
  • uuid: Event UUID.
DataFrame
  • start: Same as first_seen.
DataFrame
  • end: Same as last_seen.
DataFrame
  • duration_seconds: Same as lead_time_seconds (float seconds).
DataFrame
  • source_uuid: Comma-joined station UUIDs in visit order (each listed once).

current_status ¤

current_status(
    *, infer_dwell_from_next: bool = False
) -> pd.DataFrame

Determine the last-known station for each identifier.

Parameters:

Name Type Description Default
infer_dwell_from_next bool

Forwarded to :meth:build_timeline. Note the current (last) station's dwell is still open, so this mainly affects identifiers whose latest visit has a later arrival.

False

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • identifier
DataFrame
  • current_station: Name of the last station where seen.
DataFrame
  • current_station_uuid: UUID of that station.
DataFrame
  • arrived_at: When the identifier arrived at the current station.
DataFrame
  • time_at_station_seconds: Dwell time at the current station so far.
DataFrame
  • uuid: Event UUID.

station_dwell_statistics ¤

station_dwell_statistics(
    *, infer_dwell_from_next: bool = False
) -> pd.DataFrame

Compute dwell-time statistics per station (across all identifiers).

Parameters:

Name Type Description Default
infer_dwell_from_next bool

Forwarded to :meth:build_timeline. Pass True when stations log an identifier once on arrival, so dwell is measured arrival-here → arrival-at-next-station instead of collapsing to zero.

False

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • station_name
DataFrame
  • station_uuid
DataFrame
  • identifier_count: Number of distinct identifiers seen.
DataFrame
  • min_dwell_seconds
DataFrame
  • avg_dwell_seconds
DataFrame
  • max_dwell_seconds
DataFrame
  • total_dwell_seconds

PartProductionEvents ¤

PartProductionEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    event_uuid: str = "prod:part_tracking"
)

Bases: Base

Track production quantities by part number.

Each UUID represents one signal: - part_id_uuid: string signal with current part number - counter_uuid: monotonic counter for production count

The counter is expected to increase as parts are produced. If the counter is reset during operation (back to zero or a lower value), enable handle_resets so the reset is treated as new production instead of being discarded.

Example

import pandas as pd, ts_shape parts = ts_shape.make_id_signal("part", ["A", "B"], hold=60, freq="1min") counter = ts_shape.make_timeseries(["counter"], n_points=120, freq="1min", ... noise=0, drift=119, value_column="value_integer") pp = ts_shape.PartProductionEvents(pd.concat([parts, counter])) pp.production_by_part("part", "counter")[["start", "part_number", "quantity"]] start part_number quantity 0 2025-01-01 00:00:00 A 59 1 2025-01-01 01:00:00 B 60

Initialize part production tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data

required
time_column str

Name of timestamp column (default: 'systime')

'systime'
event_uuid str

UUID tagged on derived point events (the uuid column of :meth:detect_resets; default: 'prod:part_tracking')

'prod:part_tracking'

production_by_part ¤

production_by_part(
    part_id_uuid: str,
    counter_uuid: str,
    *,
    window: str = "1h",
    value_column_part: str = "value_string",
    value_column_counter: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Calculate production quantity per part number.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
counter_uuid str

UUID for production counter

required
window str

Time window for aggregation (e.g., '1h', '8h', '1d')

'1h'
value_column_part str

Column containing part numbers

'value_string'
value_column_counter str

Column containing counter values

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it. The result gains a resets column with the number of resets observed in each window. When False, the quantity is simply max(0, last_count - first_count) and a window containing a reset reports 0.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start: Start of time window
DataFrame
  • end: End of time window (start + window)
DataFrame
  • duration_seconds: Window length in seconds
DataFrame
  • part_number: Part number/ID
DataFrame
  • quantity: Parts produced in window
DataFrame
  • first_count: Counter value at window start
DataFrame
  • last_count: Counter value at window end
DataFrame
  • resets: Number of counter resets in window (only when handle_resets=True)
Example

import pandas as pd, ts_shape parts = ts_shape.make_id_signal("part", ["A", "B"], hold=60, freq="1min") counter = ts_shape.make_timeseries(["counter"], n_points=120, freq="1min", ... noise=0, drift=119, value_column="value_integer") pp = ts_shape.PartProductionEvents(pd.concat([parts, counter])) pp.production_by_part("part", "counter")[["part_number", "quantity"]] part_number quantity 0 A 59 1 B 60

With handle_resets=True (the default), a window where the counter drops from 432 to 61 reports a quantity of 61 (and resets=1) instead of 0.

detect_resets ¤

detect_resets(
    part_id_uuid: str,
    counter_uuid: str,
    *,
    value_column_part: str = "value_string",
    value_column_counter: str = "value_integer"
) -> pd.DataFrame

Find the points in time where the counter was reset.

A reset is any reading where the counter value is lower than the previous reading.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
counter_uuid str

UUID for production counter

required
value_column_part str

Column containing part numbers

'value_string'
value_column_counter str

Column containing counter values

'value_integer'

Returns:

Type Description
DataFrame

DataFrame with one row per reset and columns:

DataFrame
  • : Timestamp of the reading after the reset
DataFrame
  • uuid: Event UUID of this tracker
DataFrame
  • source_uuid: The counter UUID
DataFrame
  • part_number: Active part number at the reset
DataFrame
  • count_before: Counter value just before the reset
DataFrame
  • count_after: Counter value just after the reset
DataFrame
  • drop: How far the counter fell (count_before - count_after)
Example

import pandas as pd, ts_shape parts = ts_shape.make_id_signal("part", ["A"], hold=6, freq="1min") counter = parts.assign(uuid="counter", value_string=pd.NA, ... value_integer=[430, 431, 432, 61, 62, 63]) pp = ts_shape.PartProductionEvents(pd.concat([parts, counter])) pp.detect_resets("part", "counter")[["part_number", "count_before", "count_after"]] part_number count_before count_after 0 A 432.0 61.0

daily_production_summary ¤

daily_production_summary(
    part_id_uuid: str,
    counter_uuid: str,
    *,
    value_column_part: str = "value_string",
    value_column_counter: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Daily production summary by part number.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
counter_uuid str

UUID for production counter

required
value_column_part str

Column containing part numbers

'value_string'
value_column_counter str

Column containing counter values

'value_integer'
handle_resets bool

Account for counter resets (see :meth:production_by_part). Adds a resets column with the number of resets observed that day.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start: Midnight of date (tz-aware in the data's tz)
DataFrame
  • end: Midnight of the following day
DataFrame
  • duration_seconds: Day length in seconds
DataFrame
  • date: Production date
DataFrame
  • part_number: Part number/ID
DataFrame
  • total_quantity: Total parts produced that day
DataFrame
  • hours_active: Number of hours with production
DataFrame
  • resets: Number of counter resets that day (only when handle_resets=True)
Example

import pandas as pd, ts_shape parts = ts_shape.make_id_signal("part", ["A", "B"], hold=60, freq="1min") counter = ts_shape.make_timeseries(["counter"], n_points=120, freq="1min", ... noise=0, drift=119, value_column="value_integer") pp = ts_shape.PartProductionEvents(pd.concat([parts, counter])) daily = pp.daily_production_summary("part", "counter") daily[["date", "part_number", "total_quantity", "hours_active"]] date part_number total_quantity hours_active 0 2025-01-01 A 59 1 1 2025-01-01 B 60 1

production_totals ¤

production_totals(
    part_id_uuid: str,
    counter_uuid: str,
    *,
    start_date: str | None = None,
    end_date: str | None = None,
    value_column_part: str = "value_string",
    value_column_counter: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Total production by part number for a date range.

Parameters:

Name Type Description Default
part_id_uuid str

UUID for part number signal

required
counter_uuid str

UUID for production counter

required
start_date str | None

Start date 'YYYY-MM-DD' (optional)

None
end_date str | None

End date 'YYYY-MM-DD' (optional)

None
value_column_part str

Column containing part numbers

'value_string'
value_column_counter str

Column containing counter values

'value_integer'
handle_resets bool

Account for counter resets (see :meth:production_by_part). Adds a resets column with the total number of resets in the range.

True

Returns:

Type Description
DataFrame

DataFrame with total production per part

Example

import pandas as pd, ts_shape parts = ts_shape.make_id_signal("part", ["A", "B"], hold=60, freq="1min") counter = ts_shape.make_timeseries(["counter"], n_points=120, freq="1min", ... noise=0, drift=119, value_column="value_integer") pp = ts_shape.PartProductionEvents(pd.concat([parts, counter])) pp.production_totals("part", "counter", start_date="2025-01-01") part_number total_quantity days_produced resets 0 A 59 1 0 1 B 60 1 0

PerformanceLossEvents ¤

PerformanceLossEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None,
    event_uuid: str = "prod:performance_loss"
)

Bases: Base

Track performance and speed losses against target cycle times.

Identifies hidden losses where the machine is running but slower than it should be. Typically 10-20% of production time is lost to speed losses.

Each method takes one signal: - cycle_uuid: cycle trigger or production counter

All methods compare against a single target_cycle_time; there are no per-part targets.

Definitions follow ISO 22400-2: actual_parts is the produced quantity (the sum of reset-aware counter increments, so a counter that steps by 2 counts 2 parts; for a trigger signal one part per row), run_seconds is the running time the parts were made in (the summed intervals between producing readings, minus any dropped by max_cycle_gap), avg_cycle_time_seconds = run_seconds / actual_parts and performance_pct = target_cycle_time x actual_parts / run_seconds x 100. The speed loss is run_seconds - actual_parts x target_cycle_time. Durations are in seconds (*_seconds); loss_minutes and the *_s cycle time columns are kept as a convenience. For a line that runs several part numbers, filter the data to one part (or one product run) per call.

Merge keys: [date, shift] for shift-level, [period] for trend data.

Pipeline example::

perf = PerformanceLossEvents(df)
shift_perf = perf.performance_by_shift('cycle', target_cycle_time=45)
# → merge with DowntimeEvents.downtime_by_shift() on [date, shift]
# → merge with QualityEvents.nok_by_shift() on [date, shift]
# → feed into ShiftHandoverEvents.from_shift_data()
Example usage

tracker = PerformanceLossEvents(df)

Performance by shift¤

perf = tracker.performance_by_shift( cycle_uuid='cycle_trigger', target_cycle_time=45.0, )

Identify slow periods¤

slow = tracker.slow_periods( cycle_uuid='cycle_trigger', target_cycle_time=45.0, threshold_pct=90, )

Initialize the performance-loss tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
time_column str

Name of timestamp column (default: 'systime').

'systime'
shift_definitions dict[str, tuple[str, str]] | None

Mapping of shift name to (start, end) times. Default: 3-shift operation (06:00-14:00, 14:00-22:00, 22:00-06:00).

None
event_uuid str

UUID tagged on derived interval events (the uuid column of :meth:slow_periods; default: 'prod:performance_loss').

'prod:performance_loss'

performance_by_shift ¤

performance_by_shift(
    cycle_uuid: str,
    target_cycle_time: float,
    *,
    value_column: str = "value_integer",
    max_cycle_gap: str | None = None,
    production_day: bool = True
) -> pd.DataFrame

Calculate performance percentage per shift.

Performance = (target_cycle_time * actual_parts) / run_seconds, with actual_parts the produced quantity (counter increments).

Parameters:

Name Type Description Default
cycle_uuid str

UUID of cycle trigger or counter signal.

required
target_cycle_time float

Target (ideal) cycle time in seconds.

required
value_column str

Column with counter values.

'value_integer'
max_cycle_gap str | None

Optional cap (e.g. "5min"). Intervals longer than this are treated as stoppages and excluded, so a break or downtime is not counted as slow-running time (which would otherwise report a large, spurious speed loss).

None
production_day bool

Attribute an overnight shift's post-midnight readings to the shift's start date, so a night shift is one (date, shift) row (default). False groups by raw calendar date, which splits a night shift across two rows.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds: the shift window from shift_definitions (tz-aware in the data's tz; an overnight shift ends on the next day)
DataFrame
  • date, shift, actual_parts, avg_cycle_time_s, avg_cycle_time_seconds, target_cycle_time_s, target_cycle_time_seconds, performance_pct, loss_minutes, loss_seconds
DataFrame
  • run_seconds: running time of the shift (the basis of performance_pct)
DataFrame

avg_cycle_time_s / target_cycle_time_s / loss_minutes

DataFrame

are convenience duplicates of the *_seconds columns.

slow_periods ¤

slow_periods(
    cycle_uuid: str,
    target_cycle_time: float,
    *,
    threshold_pct: float = 90.0,
    window: str = "1h",
    value_column: str = "value_integer",
    max_cycle_gap: str | None = None
) -> pd.DataFrame

Identify time windows where performance is below threshold.

Parameters:

Name Type Description Default
cycle_uuid str

UUID of cycle trigger or counter signal.

required
target_cycle_time float

Target cycle time in seconds.

required
threshold_pct float

Performance must be below this to flag (default 90%).

90.0
window str

Fixed time window per row (default '1h').

'1h'
value_column str

Column with counter values.

'value_integer'
max_cycle_gap str | None

Optional cap (e.g. "5min"). Cycle intervals longer than it are dropped as stops instead of counted as slow cycles. None keeps every interval.

None

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds (float seconds, end - start), uuid (event uuid), source_uuid (cycle_uuid), actual_parts, avg_cycle_time_s, avg_cycle_time_seconds, performance_pct, loss_minutes, loss_seconds
DataFrame
  • run_seconds: running time in the window (the basis of performance_pct)

performance_trend ¤

performance_trend(
    cycle_uuid: str,
    target_cycle_time: float,
    *,
    window: str = "1D",
    value_column: str = "value_integer",
    max_cycle_gap: str | None = None
) -> pd.DataFrame

Track performance trend over time.

Parameters:

Name Type Description Default
cycle_uuid str

UUID of cycle trigger or counter signal.

required
target_cycle_time float

Target cycle time in seconds.

required
window str

Time window for aggregation (default '1D').

'1D'
value_column str

Column with counter values.

'value_integer'
max_cycle_gap str | None

Optional cap (e.g. "5min"). Cycle intervals longer than it are dropped as stops instead of counted as slow cycles. None keeps every interval.

None

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end (start + window), duration_seconds
DataFrame
  • period, actual_parts, avg_cycle_time_s, avg_cycle_time_seconds, performance_pct, loss_minutes, loss_seconds
DataFrame
  • run_seconds: running time in the period (the basis of performance_pct)

PeriodSummaryEvents ¤

PeriodSummaryEvents(
    dataframe: DataFrame, *, time_column: str = "systime"
)

Bases: Base

Aggregate daily metrics into weekly/monthly summaries.

Merge keys: [week_start, week_end] for weekly, [year, month] for monthly.

Two usage patterns:

Pattern A — from raw signals:

summary = PeriodSummaryEvents(df)
weekly = summary.weekly_summary(counter_uuid='prod')

Pattern B — pipeline (compose from upstream modules):

# Build a daily DataFrame from any combination of upstream modules
quality = QualityEvents(df).daily_quality_summary('ok', 'nok')
downtime = DowntimeEvents(df).availability_trend('state', window='1D')

# Merge on date
daily = quality.merge(downtime.rename(columns={'period': 'date'}), on='date')

# Roll up to weekly/monthly
weekly = PeriodSummaryEvents.from_daily_data(daily, window='W')
monthly = PeriodSummaryEvents.from_daily_data(daily, window='MS')
Example

A part counter that rises by 100 a day:

import ts_shape df = ts_shape.make_timeseries(["counter"], n_points=14, freq="1D", ... start="2025-01-06", baseline=0, noise=0, drift=1300, ... value_column="value_integer") weekly = ts_shape.PeriodSummaryEvents(df).weekly_summary("counter") weekly[["start", "end", "total_production", "daily_avg"]] start end total_production daily_avg 0 2025-01-06 2025-01-13 600 85.714286 1 2025-01-13 2025-01-20 700 100.000000

from_daily_data staticmethod ¤

from_daily_data(
    daily_df: DataFrame,
    *,
    window: str = "W",
    date_column: str = "date",
    aggregations: dict[str, str] | None = None
) -> pd.DataFrame

Roll up a pre-computed daily DataFrame to weekly or monthly.

This is the pipeline-friendly entry-point. It accepts any daily DataFrame (e.g. merged output of QualityEvents, DowntimeEvents, OEEEvents, ReliabilityEvents, the energy EnPI / SEC tables) and aggregates its numeric columns by a declared rule per column. A ratio KPI of a period is recomputed from the period's summed basis columns (ISO 22400-2), never summed and, when its basis is present, not averaged either:

======================== ========================================== column period value ======================== ========================================== counts, durations, sum (names containing parts, energy quantity, production, count, failures, resets, seconds, minutes, hours, kwh, energy, units, output, ...) quality_pct, 100 x sum(good) / sum(total), good = first_pass_yield_pct good_parts or ok_parts, total = total_parts or ok + nok / good + reject nok_rate_pct 100 x sum(nok) / sum(total) availability_pct 100 x sum(run) / sum(planned): run / planned = run_seconds / planned_seconds (OEE), uptime_* / (uptime + downtime) or total_seconds (downtime), operating_hours / (operating + repair) (reliability) performance_pct 100 x sum(actual_parts) / sum(ideal_parts) oee_pct availability x performance x quality of the period (each from its summed bases) mttr_hours sum(repair_hours) / sum(failures) mtbf_hours sum(operating_hours) / sum(failures) mtbf_iso22400_hours sum(operating) / (sum(failures) + 1) failure_rate_per_hour sum(failures) / sum(operating_hours) enpi sum(energy_kwh) / sum(units_produced) (ISO 50006) sec sum(total_energy) / sum(total_output) efficiency sum(units) / sum(energy) ======================== ==========================================

When a rule's basis columns are missing the ratio is the mean of the daily values weighted by the ratio's denominator if that column is present (total_parts for quality, planned_seconds / total_seconds for availability and OEE, ideal_parts / run_seconds for performance, failures for MTTR/MTBF, units_produced for EnPI, ...), which is exact. Availability, OEE and other *_pct columns fall back to the mean weighted by the daily rows' duration_seconds (time-weighted). With no weight either, the plain mean is used and a :class:~ts_shape.errors.DataQualityWarning names the columns whose basis is missing. Other intensive columns (avg, mean, rate, ratio, cycle times) are averaged the same way. Numeric columns that match no rule are dropped with a DataQualityWarning naming them, instead of being summed; pass aggregations to keep them.

The output always includes start, end, and days columns.

Parameters:

Name Type Description Default
daily_df DataFrame

DataFrame with a date column and numeric metric columns. Expected merge key: [date].

required
window str

Pandas frequency string. 'W' (pandas' weeks ending Sunday, same as 'W-SUN') gives Monday-to-Sunday weeks: start is the Monday, end the following Sunday (inclusive), even when the data covers only part of the week. An anchored 'W-<DAY>' gives weeks ending on <DAY> (e.g. 'W-SAT' = Sunday-to-Saturday). 'MS' = calendar months (end = last day of the month); any other frequency is passed to :class:pandas.Grouper and end is the last date present in the group.

'W'
date_column str

Name of the date column.

'date'
aggregations dict[str, str] | None

Optional per-column override, {column: how} with how one of "sum", "mean", "min", "max", "first", "last" or "drop". It wins over the built-in rules.

None

Returns:

Type Description
DataFrame

DataFrame with start, end, days, and aggregated

DataFrame

metric columns.

Example::

quality = QualityEvents(df).daily_quality_summary('ok', 'nok')
weekly = PeriodSummaryEvents.from_daily_data(quality, window='W')
# → [start, end, days, ok_parts, nok_parts,
#    total_parts, quality_pct, ...]

weekly_summary ¤

weekly_summary(
    counter_uuid: str,
    ok_counter_uuid: str | None = None,
    nok_counter_uuid: str | None = None,
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Roll up daily production to weekly summaries from raw signals.

For pipeline usage with pre-computed DataFrames, use :meth:from_daily_data instead.

Parameters:

Name Type Description Default
counter_uuid str

UUID of production counter.

required
ok_counter_uuid str | None

UUID of good parts counter (optional).

None
nok_counter_uuid str | None

UUID of the NOK part counter (optional).

None
value_column str

Column containing counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it (a daily boundary is a common reset point). Increments are booked to the day of the reading that reports them, so no production is lost between days. The result gains a resets column with the number of resets observed each week.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds: the week window (Monday midnight to the following Monday midnight, tz-aware in the data's tz)
DataFrame
  • week_start, week_end, total_production, daily_avg, production_days, [ok_parts, nok_parts, quality_pct]
DataFrame
  • resets: Number of counter resets in the week (only when handle_resets=True)

monthly_summary ¤

monthly_summary(
    counter_uuid: str,
    ok_counter_uuid: str | None = None,
    nok_counter_uuid: str | None = None,
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Roll up daily production to monthly summaries from raw signals.

For pipeline usage with pre-computed DataFrames, use :meth:from_daily_data instead.

Parameters:

Name Type Description Default
counter_uuid str

UUID of production counter.

required
ok_counter_uuid str | None

UUID of good parts counter (optional).

None
nok_counter_uuid str | None

UUID of the NOK part counter (optional).

None
value_column str

Column containing counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it. Increments are booked to the day of the reading that reports them, so no production is lost between days. The result gains a resets column with the number of resets observed each month.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds: the calendar-month window (first of the month midnight to first of the next month midnight, tz-aware in the data's tz)
DataFrame
  • year, month, total_production, daily_avg, production_days, [ok_parts, nok_parts, quality_pct]
DataFrame
  • resets: Number of counter resets in the month (only when handle_resets=True)

compare_periods ¤

compare_periods(
    counter_uuid: str,
    period_a: tuple[str, str],
    period_b: tuple[str, str],
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Compare production between two time periods.

Parameters:

Name Type Description Default
counter_uuid str

UUID of production counter.

required
period_a tuple[str, str]

Tuple of (start_date, end_date) for first period.

required
period_b tuple[str, str]

Tuple of (start_date, end_date) for second period.

required
value_column str

Column containing counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it, so the compared metrics are not understated for days containing a reset.

True

Returns:

Type Description
DataFrame

DataFrame with columns: metric, period_a_value, period_b_value,

DataFrame

change, change_pct. period1_value / period2_value are

DataFrame

deprecated twins of period_a_value / period_b_value (each

DataFrame

sits right before its replacement); they are removed in the next

DataFrame

major release.

QualityEvents ¤

QualityEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None
)

Bases: Base

Track NOK (defective) parts and quality metrics.

Each UUID represents one signal: - ok_counter_uuid: counter for good parts - nok_counter_uuid: counter for NOK parts - part_id_uuid: part number signal (optional) - defect_reason_uuid: defect reason code (optional)

Merge keys: [date, shift] for shift-level, [date] for daily, [part_number] for part-level.

Pipeline outputs include quality_pct (alias for first_pass_yield_pct) so downstream modules (ShiftHandoverEvents, PeriodSummaryEvents, OEEEvents) can join on a consistent column name.

Example

import pandas as pd, ts_shape kw = dict(n_points=20, freq="1min", noise=0, value_column="value_integer") df = pd.concat([ts_shape.make_timeseries(["ok"], drift=190, **kw), ... ts_shape.make_timeseries(["nok"], drift=19, **kw), ... ts_shape.make_id_signal("part", ["A", "B"], freq="1min")]) out = ts_shape.QualityEvents(df).quality_by_part("ok", "nok", "part") out[["part_number", "ok_parts", "nok_parts", "first_pass_yield_pct"]].round(1) part_number ok_parts nok_parts first_pass_yield_pct 0 A 90.0 9.0 90.9 1 B 100.0 10.0 90.9

Initialize quality tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data

required
time_column str

Name of timestamp column (default: 'systime')

'systime'
shift_definitions dict[str, tuple[str, str]] | None

Dictionary mapping shift names to (start, end) times Default: 3-shift operation (06:00-14:00, 14:00-22:00, 22:00-06:00)

None

nok_by_shift ¤

nok_by_shift(
    ok_counter_uuid: str,
    nok_counter_uuid: str,
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Calculate NOK (defective) parts per shift.

Parameters:

Name Type Description Default
ok_counter_uuid str

UUID for good parts counter

required
nok_counter_uuid str

UUID of the NOK part counter

required
value_column str

Column containing counter values

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the parts accumulated after the reset, for both the OK and NOK counters. The increments are computed once over each whole counter signal; the increment between two readings is spread over the time between them and split at the shift boundaries in proportion to time (ISO 22400-2: a part belongs to the period it was produced in), so the shifts add up to the signal totals and the parts made in the last hour of a night shift stay in that shift even though the reading that reports them falls in the morning shift. Quantities can therefore be fractional. A reset counts in the shift of the reading that shows it. The result gains a resets column with the number of resets observed in each shift (across both counters). When False, each counter uses max(0, last - first) of the shift's own readings; a shift containing a reset reports too little (often 0) and the production between two shifts' readings is lost.

True
production_day bool

Attribute an overnight shift's post-midnight readings to the shift's start date, so a night shift is one (date, shift) row (default). False groups by raw calendar date, which splits a night shift across two rows.

True

Returns:

Type Description
DataFrame

DataFrame with quality metrics by shift:

DataFrame
  • start: Start of the shift window (date at the shift start time from shift_definitions, tz-aware in the data's tz)
DataFrame
  • end: End of the shift window (next day for overnight shifts)
DataFrame
  • duration_seconds: Shift window length in seconds
DataFrame
  • date: Production date
DataFrame
  • shift: Shift name
DataFrame
  • ok_parts: Good parts produced
DataFrame
  • nok_parts: NOK parts
DataFrame
  • total_parts: Total parts produced
DataFrame
  • nok_rate_pct: Percentage of NOK parts
DataFrame
  • first_pass_yield_pct: Percentage of good parts
DataFrame
  • quality_pct: Same as first_pass_yield_pct
DataFrame
  • resets: Number of counter resets in the shift (only when handle_resets=True)
Example

import pandas as pd, ts_shape kw = dict(n_points=16, freq="30min", start="2025-01-01 06:00", noise=0, ... value_column="value_integer") df = pd.concat([ts_shape.make_timeseries(["ok"], drift=150, **kw), ... ts_shape.make_timeseries(["nok"], drift=15, **kw)]) out = ts_shape.QualityEvents(df).nok_by_shift("ok", "nok") out[["shift", "ok_parts", "nok_parts", "first_pass_yield_pct"]].round(1) shift ok_parts nok_parts first_pass_yield_pct 0 shift_1 150.0 15.0 90.9

quality_by_part ¤

quality_by_part(
    ok_counter_uuid: str,
    nok_counter_uuid: str,
    part_id_uuid: str,
    *,
    value_column_counter: str = "value_integer",
    value_column_part: str = "value_string",
    handle_resets: bool = True
) -> pd.DataFrame

Calculate quality metrics by part number.

Parameters:

Name Type Description Default
ok_counter_uuid str

UUID for good parts counter

required
nok_counter_uuid str

UUID of the NOK part counter

required
part_id_uuid str

UUID for part number signal

required
value_column_counter str

Column containing counter values

'value_integer'
value_column_part str

Column containing part numbers

'value_string'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the parts accumulated after the reset. The part number is attached to a single global counter, so the reset-aware quantity is the sum of per-reading increments over the full time-sorted series, grouped by part -- this also corrects the over-count that last - first produces when parts alternate over a non-contiguous group. The result gains a resets column with the number of resets attributed to each part (across the OK and NOK counters). When False, the quantity is max(0, last - first) per part group and a reset is lost.

True

Returns:

Type Description
DataFrame

DataFrame with quality by part:

DataFrame
  • part_number: Part number/ID
DataFrame
  • ok_parts: Good parts produced
DataFrame
  • nok_parts: NOK parts
DataFrame
  • total_parts: Total parts produced
DataFrame
  • nok_rate_pct: Percentage of NOK parts
DataFrame
  • first_pass_yield_pct: Percentage of good parts
DataFrame
  • quality_pct: Same as first_pass_yield_pct
DataFrame
  • resets: Number of counter resets for the part (only when handle_resets=True)
Example

import pandas as pd, ts_shape kw = dict(n_points=20, freq="1min", noise=0, value_column="value_integer") df = pd.concat([ts_shape.make_timeseries(["ok"], drift=190, **kw), ... ts_shape.make_timeseries(["nok"], drift=19, **kw), ... ts_shape.make_id_signal("part", ["A", "B"], freq="1min")]) out = ts_shape.QualityEvents(df).quality_by_part("ok", "nok", "part") out[["part_number", "ok_parts", "nok_parts", "nok_rate_pct"]] part_number ok_parts nok_parts nok_rate_pct 0 A 90.0 9.0 9.090909 1 B 100.0 10.0 9.090909

nok_by_reason ¤

nok_by_reason(
    nok_counter_uuid: str,
    defect_reason_uuid: str,
    *,
    value_column_counter: str = "value_integer",
    value_column_reason: str = "value_string",
    handle_resets: bool = True
) -> pd.DataFrame

Analyze NOK parts by defect reason.

Parameters:

Name Type Description Default
nok_counter_uuid str

UUID of the NOK part counter

required
defect_reason_uuid str

UUID for defect reason signal

required
value_column_counter str

Column containing counter values

'value_integer'
value_column_reason str

Column containing reason codes

'value_string'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the parts accumulated after the reset. The reason is attached to a single global counter, so the reset-aware quantity is the sum of per-reading increments over the full time-sorted series, grouped by reason -- this also corrects the over-count that last - first produces when reasons alternate over a non-contiguous group. The result gains a resets column with the number of resets attributed to each reason. When False, the quantity is max(0, last - first) per reason group and a reset is lost.

True

Returns:

Type Description
DataFrame

DataFrame with NOK by reason:

DataFrame
  • reason: Defect reason code
DataFrame
  • nok_parts: Number of NOK parts
DataFrame
  • pct_of_total: Percentage of total NOK
DataFrame
  • resets: Number of counter resets for the reason (only when handle_resets=True)
DataFrame

Canonical twin columns: share_of_total_pct (after pct_of_total); each

DataFrame

holds the same values as the deprecated column right before it, which is

DataFrame

removed in the next major release.

Example

import pandas as pd, ts_shape kw = dict(n_points=20, freq="1min", noise=0, value_column="value_integer") df = pd.concat([ts_shape.make_timeseries(["nok"], drift=19, **kw), ... ts_shape.make_id_signal("reason", ["Scratch", "Dent"], freq="1min")]) out = ts_shape.QualityEvents(df).nok_by_reason("nok", "reason") out[["reason", "nok_parts", "share_of_total_pct", "resets"]] reason nok_parts share_of_total_pct resets 0 Dent 10.0 52.631579 0 1 Scratch 9.0 47.368421 0

daily_quality_summary ¤

daily_quality_summary(
    ok_counter_uuid: str,
    nok_counter_uuid: str,
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Daily quality summary.

Parameters:

Name Type Description Default
ok_counter_uuid str

UUID for good parts counter

required
nok_counter_uuid str

UUID of the NOK part counter

required
value_column str

Column containing counter values

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the parts accumulated after the reset (see :meth:nok_by_shift; the day totals add up to the signal totals). The result gains a resets column with the number of resets observed that day. When False, a shift containing a reset reports too little.

True
production_day bool

Forwarded to :meth:nok_by_shift; attributes an overnight shift's post-midnight readings to the shift's start date. Default True; False groups by raw calendar date, which splits a night shift across two rows.

True

Returns:

Type Description
DataFrame

DataFrame with daily quality:

DataFrame
  • start: Midnight of date (tz-aware in the data's tz)
DataFrame
  • end: Midnight of the following day
DataFrame
  • duration_seconds: Day length in seconds
DataFrame
  • date: Production date
DataFrame
  • ok_parts: Good parts produced
DataFrame
  • nok_parts: NOK parts
DataFrame
  • total_parts: Total parts produced
DataFrame
  • nok_rate_pct: Percentage of NOK parts
DataFrame
  • first_pass_yield_pct: Percentage of good parts
DataFrame
  • quality_pct: Same as first_pass_yield_pct
DataFrame
  • resets: Number of counter resets that day (only when handle_resets=True)
Example

import pandas as pd, ts_shape kw = dict(n_points=16, freq="30min", start="2025-01-01 06:00", noise=0, ... value_column="value_integer") df = pd.concat([ts_shape.make_timeseries(["ok"], drift=150, **kw), ... ts_shape.make_timeseries(["nok"], drift=15, **kw)]) out = ts_shape.QualityEvents(df).daily_quality_summary("ok", "nok") out[["date", "ok_parts", "nok_parts", "nok_rate_pct"]].round(1) date ok_parts nok_parts nok_rate_pct 0 2025-01-01 150.0 15.0 9.1

ReworkEvents ¤

ReworkEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None
)

Bases: Base

Track parts that require rework (re-processing through a station).

Each UUID represents one signal: - rework_uuid: rework event counter (monotonic integer) - reason_uuid: rework reason code signal (optional) - total_production_uuid: total production counter (optional) - part_id_uuid: part number / product type signal (optional)

Merge keys: [date, shift] for shift-level, [period] for trend, [reason] for reason-level, [part_number] for part-level.

Pipeline example::

rework = ReworkEvents(df)
shift_rework = rework.rework_by_shift('rework_counter')
# → merge with ShiftReportEvents.shift_production() on [date, shift]
# → merge with QualityEvents.nok_by_shift() on [date, shift]
rate = rework.rework_rate('rework_counter', 'total_counter')
# → merge with ShiftReportEvents.shift_production() on [date, shift]
Example usage

tracker = ReworkEvents(df)

Rework per shift¤

shift_rework = tracker.rework_by_shift(rework_uuid='rework_counter')

Rework rate¤

rate = tracker.rework_rate( rework_uuid='rework_counter', total_production_uuid='total_counter', )

rework_by_shift ¤

rework_by_shift(
    rework_uuid: str,
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Rework count per shift.

Parameters:

Name Type Description Default
rework_uuid str

UUID for rework event counter signal.

required
value_column str

Column containing counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the rework accumulated after the reset instead of losing it. The increments are computed once over the whole signal and booked to the shift of the reading that reports them, so the shifts add up to the signal total. The result gains a resets column with the number of resets observed in each shift. When False, the quantity is max(0, last - first) of the shift's own readings: a shift containing a reset reports too little (often 0) and the production between two shifts' readings is lost.

True
production_day bool

When True, an overnight shift's post-midnight readings are attributed to the shift's start date, so a night shift is one contiguous (date, shift) group. Default True; False groups by raw calendar date, which splits a night shift across two rows. Only affects overnight schedules.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds: the shift window from shift_definitions (date at the shift start time, tz-aware in the data's tz; an overnight shift ends on the next day)
DataFrame
  • date, shift, rework_count
DataFrame
  • resets: Number of counter resets in the shift (only when handle_resets=True)

rework_by_reason ¤

rework_by_reason(
    rework_uuid: str,
    reason_uuid: str,
    *,
    value_column_rework: str = "value_integer",
    value_column_reason: str = "value_string",
    handle_resets: bool = True
) -> pd.DataFrame

Rework quantity by reason code.

Parameters:

Name Type Description Default
rework_uuid str

UUID for rework counter signal.

required
reason_uuid str

UUID for rework reason code signal.

required
value_column_rework str

Column containing rework counter values.

'value_integer'
value_column_reason str

Column containing reason codes.

'value_string'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the rework accumulated after the reset. The reason is attached to a single global counter, so the reset-aware quantity is the sum of per-reading increments over the full time-sorted series, grouped by reason -- this also corrects the over-count that last - first produces when reasons alternate over a non-contiguous group. The result gains a resets column with the number of resets attributed to each reason. When False, the quantity is max(0, last - first) per reason group and a reset is lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • reason, rework_count, pct_of_total
DataFrame
  • resets: Number of counter resets for the reason (only when handle_resets=True)
DataFrame

Canonical twin columns: share_of_total_pct (after pct_of_total); each

DataFrame

holds the same values as the deprecated column right before it, which is

DataFrame

removed in the next major release.

rework_rate ¤

rework_rate(
    rework_uuid: str,
    total_production_uuid: str,
    *,
    value_column_rework: str = "value_integer",
    value_column_production: str = "value_integer",
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Rework rate as percentage of total production per shift.

Parameters:

Name Type Description Default
rework_uuid str

UUID for rework counter signal.

required
total_production_uuid str

UUID for total production counter signal.

required
value_column_rework str

Column containing rework counter values.

'value_integer'
value_column_production str

Column containing production counter values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production/rework accumulated after the reset, for both the numerator (rework) and denominator (total produced). Both are computed once over the whole signal and booked to the shift of the reading that reports them, so the shifts add up to the signal totals. The result gains rework_resets, production_resets and resets (their sum) columns with the number of resets observed in each shift. When False, both use max(0, last - first) per shift and a reset is lost.

True
production_day bool

When True, an overnight shift's post-midnight readings are attributed to the shift's start date. Default True; False groups by raw calendar date, which splits a night shift across two rows. Only affects overnight schedules.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds: the shift window from shift_definitions (date at the shift start time, tz-aware in the data's tz; an overnight shift ends on the next day)
DataFrame
  • date, shift, total_produced, rework_count, rework_rate_pct
DataFrame
  • rework_resets, production_resets, resets: Number of counter resets of the rework counter, the production counter, and both together in the shift (only when handle_resets=True)

rework_cost ¤

rework_cost(
    rework_uuid: str,
    part_id_uuid: str,
    rework_costs: dict[str, float],
    *,
    value_column_rework: str = "value_integer",
    value_column_part: str = "value_string",
    handle_resets: bool = True
) -> pd.DataFrame

Convert rework counts to monetary cost by part number.

Parameters:

Name Type Description Default
rework_uuid str

UUID for rework counter signal.

required
part_id_uuid str

UUID for part number / product type signal.

required
rework_costs dict[str, float]

Dict mapping part numbers to cost per rework.

required
value_column_rework str

Column containing rework counter values.

'value_integer'
value_column_part str

Column containing part number values.

'value_string'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the rework accumulated after the reset. The increments are computed once over the whole signal and booked to the part of the reading that reports them, so the parts add up to the signal total (also when parts alternate). The result gains a resets column with the number of resets observed for each part. When False, the quantity is max(0, last - first) per part and a reset is lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • part_number, rework_count, cost_per_rework, total_cost
DataFrame
  • resets: Number of counter resets for the part (only when handle_resets=True)

rework_trend ¤

rework_trend(
    rework_uuid: str,
    *,
    value_column: str = "value_integer",
    window: str = "1D",
    handle_resets: bool = True
) -> pd.DataFrame

Track rework count trend over time.

Parameters:

Name Type Description Default
rework_uuid str

UUID for rework counter signal.

required
value_column str

Column containing counter values.

'value_integer'
window str

Time window for aggregation (default '1D').

'1D'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the rework accumulated after the reset. The increments are computed once over the whole signal and booked to the period of the reading that reports them, so the periods add up to the signal total. The result gains a resets column with the number of resets observed in each period. When False, the quantity is max(0, last - first) of the period's own readings: a reset and the production between two periods' readings are lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end (start + window), duration_seconds
DataFrame
  • period, rework_count
DataFrame
  • resets: Number of counter resets in the period (only when handle_resets=True)

RoutingTraceabilityEvents ¤

RoutingTraceabilityEvents(
    dataframe: DataFrame,
    id_uuid: str,
    routing_uuid: str,
    *,
    state_map: dict[int | float | str, str] | None = None,
    station_map: dict[int | float | str, str] | None = None,
    event_uuid: str = "prod:routing_trace",
    value_column_id: str = "value_string",
    value_column_routing: str = "value_integer",
    time_column: str = "systime"
)

Bases: Base

Trace item routing using an ID signal paired with a state/routing signal.

Two UUIDs work together: - id_uuid: string signal carrying the current identifier (serial number, order ID, batch code, etc.). - routing_uuid: signal whose value encodes the current process step or station.

A state_map translates signal values to human-readable station or step names. Without it, raw values are used as labels.

Example usage::

# PLC step numbers mapped to process steps
trace = RoutingTraceabilityEvents(
    df,
    id_uuid='serial_code_signal',
    routing_uuid='step_chain_signal',
    state_map={
        10: 'Heating',
        20: 'Holding',
        30: 'Cooling',
        40: 'Discharge',
    },
)

# Station handover signal mapped to stations
trace = RoutingTraceabilityEvents(
    df,
    id_uuid='serial_code_signal',
    routing_uuid='handover_signal',
    state_map={1: 'Welding', 2: 'Painting', 3: 'Assembly'},
)

timeline = trace.build_routing_timeline()
lead = trace.lead_time()
stats = trace.station_statistics()
paths = trace.routing_paths()

Initialize routing traceability.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
id_uuid str

UUID of the signal carrying item identifiers.

required
routing_uuid str

UUID of the state/routing signal.

required
state_map dict[int | float | str, str] | None

Mapping from signal value to station/step name. e.g. {10: 'Heating', 20: 'Holding', 30: 'Cooling'} or {1: 'Welding', 2: 'Painting', 3: 'Assembly'} Values can be int, float, or str keys. If not provided, raw signal values are used as labels.

None
station_map dict[int | float | str, str] | None

Deprecated alias for state_map; passing it emits a :class:DeprecationWarning and it will be removed in the next major release. When both are passed, state_map wins and a UserWarning is emitted.

None
event_uuid str

UUID to tag derived events with.

'prod:routing_trace'
value_column_id str

Column holding the item ID string.

'value_string'
value_column_routing str

Column holding the state/routing value.

'value_integer'
time_column str

Name of timestamp column.

'systime'

build_routing_timeline ¤

build_routing_timeline(
    *, infer_dwell_from_next: bool = False
) -> pd.DataFrame

Correlate the ID signal with the state/routing signal to build a timeline.

For each sample of the routing signal, the current item ID is determined via backward-fill merge (most recent ID at that timestamp). Contiguous intervals where the same (item_id, state) pair holds are grouped into single events.

Parameters:

Name Type Description Default
infer_dwell_from_next bool

When False (default), an interval's end is the last routing sample of the (item, state) segment, so a change-encoded routing signal (one row per change) reports a dwell of 0. When True, each visit's end is set to the item's next routing arrival, giving the honest arrival-here → arrival-at-next-state dwell. The last visit per item stays open.

False

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • item_id: Identifier string.
DataFrame
  • routing_value: Raw signal value.
DataFrame
  • station_name: Resolved station/step name from state_map.
DataFrame
  • start: First timestamp of interval.
DataFrame
  • end: Last timestamp of interval.
DataFrame
  • duration_seconds: Time at this state.
DataFrame
  • sample_count: Number of routing samples in interval.
DataFrame
  • station_sequence: Visit order per item (1-based).
DataFrame
  • uuid: Event UUID.
DataFrame
  • source_uuid: "<id_uuid>,<routing_uuid>" (the two correlated input signals).

lead_time ¤

lead_time() -> pd.DataFrame

Compute end-to-end lead time per item.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • item_id
DataFrame
  • first_station: Name of first station/step visited.
DataFrame
  • last_station: Name of last station/step visited.
DataFrame
  • first_seen: Earliest timestamp.
DataFrame
  • last_seen: Latest timestamp.
DataFrame
  • lead_time_seconds: Total elapsed time.
DataFrame
  • stations_visited: Number of distinct stations/steps.
DataFrame
  • routing_path: Ordered station names joined by " -> ".
DataFrame
  • uuid: Event UUID.
DataFrame
  • start: Same as first_seen.
DataFrame
  • end: Same as last_seen.
DataFrame
  • duration_seconds: Same as lead_time_seconds (float seconds).
DataFrame
  • source_uuid: "<id_uuid>,<routing_uuid>".

station_statistics ¤

station_statistics(
    *, infer_dwell_from_next: bool = False
) -> pd.DataFrame

Compute dwell-time statistics per station/step across all items.

Parameters:

Name Type Description Default
infer_dwell_from_next bool

Forwarded to :meth:build_routing_timeline; pass True for change-encoded routing signals so dwell is measured to the item's next routing arrival instead of collapsing to zero.

False

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • station_name
DataFrame
  • routing_value
DataFrame
  • item_count: Number of distinct items seen.
DataFrame
  • min_dwell_seconds
DataFrame
  • avg_dwell_seconds
DataFrame
  • max_dwell_seconds
DataFrame
  • total_dwell_seconds

routing_paths ¤

routing_paths() -> pd.DataFrame

Analyze routing path frequencies -- which station sequences are most common.

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • routing_path: Ordered station names joined by " -> ".
DataFrame
  • item_count: Number of items that followed this path.
DataFrame
  • avg_lead_time_seconds: Average lead time for items on this path.
DataFrame
  • min_lead_time_seconds
DataFrame
  • max_lead_time_seconds

RuntimeAccountingEvents ¤

RuntimeAccountingEvents(
    dataframe: DataFrame,
    run_state_uuid: str,
    *,
    event_uuid: str = "prod:runtime",
    value_column: str = "value_bool",
    time_column: str = "systime"
)

Bases: Base

Operating-hours accounting from one boolean run signal.

Example usage::

rt = RuntimeAccountingEvents(df, run_state_uuid="machine:running")
rt.runtime_summary()
rt.runtime_by_window(window="1D")
rt.operating_hours_meter(window="1h")

Initialize the runtime-accounting analyzer.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
run_state_uuid str

UUID of the boolean run-state signal (True = running).

required
event_uuid str

UUID to tag derived events with.

'prod:runtime'
value_column str

Column holding the boolean run state.

'value_bool'
time_column str

Name of the timestamp column.

'systime'

run_uuid property ¤

run_uuid: str

Deprecated read-only alias of :attr:run_state_uuid.

runtime_summary ¤

runtime_summary(
    *, max_gap: str | None = None
) -> pd.DataFrame

Overall operating-time summary across the whole dataset.

Parameters:

Name Type Description Default
max_gap str | None

Optional cap (e.g. "15min"). A gap to the next sample longer than this is treated as a logging outage and excluded, so it does not inflate run/idle time or utilization. Without it, a gap while the last state was running is booked as run time. None (default) applies no cap: every interval between readings, however long, counts toward the preceding state.

None

Returns:

Type Description
DataFrame

Summary-shape DataFrame (one row) with columns: start, end,

DataFrame

duration_seconds, run_seconds, run_hours, idle_seconds,

DataFrame

start_count, longest_run_seconds, mean_run_seconds,

DataFrame

utilization_pct.

runtime_by_window ¤

runtime_by_window(
    window: str = "1D", *, max_gap: str | None = None
) -> pd.DataFrame

Run time per calendar window.

Each sample's state holds until the next sample; the interval is split at the window boundaries, so each window gets the run and idle time that fell inside it. A start counts in the window of the sample that begins the run. Windows holding a sample or any observed time are reported.

Parameters:

Name Type Description Default
window str

Resample window (e.g. "1D", "8h").

'1D'
max_gap str | None

Optional cap (e.g. "15min") excluding a logging gap from run time and utilization (see :meth:runtime_summary). None (default) applies no cap.

None

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns: start, end, duration_seconds,

DataFrame

run_seconds, run_hours, start_count, utilization_pct.

operating_hours_meter ¤

operating_hours_meter(
    window: str = "1h", *, max_gap: str | None = None
) -> pd.DataFrame

Cumulative operating-hours meter, sampled per window.

Mirrors a physical equipment hour meter: a monotonically increasing total of run hours. Run time is split at the window boundaries (see :meth:runtime_by_window); every window holding a sample or any observed time is reported, so the meter has no holes.

Parameters:

Name Type Description Default
window str

Resample window for the meter readings.

'1h'
max_gap str | None

Optional cap (e.g. "15min") excluding a logging gap from run time so the meter does not advance across an outage. None (default) applies no cap.

None

Returns:

Type Description
DataFrame

Summary-shape DataFrame with columns: start, end, duration_seconds,

DataFrame

run_seconds, cumulative_run_hours.

ProductionScheduleAdherenceEvents ¤

ProductionScheduleAdherenceEvents(
    dataframe: DataFrame,
    schedule: DataFrame,
    *,
    event_uuid: str = "prod:schedule_adherence",
    time_column: str = "systime",
    uuid_column: str = "uuid",
    value_column: str = "value_double",
    schedule_start_column: str = "start",
    schedule_end_column: str = "end",
    planned_qty_column: str = "planned_qty",
    order_id_column: str = "order_id"
)

Bases: Base

Production: schedule vs. actual adherence.

The schedule is a plain DataFrame — one row per planned period::

schedule = pd.DataFrame({
    "start":       pd.to_datetime(["2024-05-02 06:00", "2024-05-02 14:00"]),
    "end":         pd.to_datetime(["2024-05-02 14:00", "2024-05-02 22:00"]),
    "planned_qty": [400, 400],
    "order_id":    ["WO-1001", "WO-1002"],
})

adherence = ProductionScheduleAdherenceEvents(df, schedule)
adherence.adherence_by_period("counter:line-1")

Column names are configurable via the constructor, so an MES export can be passed through without renaming.

Methods: - adherence_by_period: Planned vs. actual and recovery rate per period. - deviation_events: Point events for periods breaching the deviation bands. - adherence_summary: Roll-up across the scheduled horizon.

Initialize the adherence tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Timeseries frame holding the production counter.

required
schedule DataFrame

Planned schedule, one row per period.

required
event_uuid str

UUID stamped on emitted events.

'prod:schedule_adherence'
time_column str

Timestamp column of dataframe.

'systime'
uuid_column str

Signal-identifier column of dataframe.

'uuid'
value_column str

Counter-value column of dataframe.

'value_double'
schedule_start_column str

Period-start column of schedule.

'start'
schedule_end_column str

Period-end column of schedule.

'end'
planned_qty_column str

Planned-quantity column of schedule.

'planned_qty'
order_id_column str

Optional order-identifier column of schedule. Missing is fine — the emitted order_id is then None.

'order_id'

Raises:

Type Description
TypeError

If schedule is not a DataFrame.

ValueError

If schedule lacks a required column, or a period ends before it starts.

adherence_by_period ¤

adherence_by_period(
    counter_uuid: str,
    *,
    handle_resets: bool = True,
    as_of: Timestamp | str | None = None,
    ahead_pct: float = 105.0,
    behind_pct: float = 95.0,
    critical_pct: float = 80.0
) -> pd.DataFrame

Compare planned against actual production for every scheduled period.

recovery_required_rate is the units per hour the line must hold over the time still left in the period to finish on plan, evaluated at as_of. It is 0 for a period already at or above plan and NaN once the period is over — a finished period cannot be recovered, and reporting a rate for it would invite planning against a number that means nothing.

Parameters:

Name Type Description Default
counter_uuid str

Identifier of the monotonic production counter.

required
handle_resets bool

Treat a decreasing counter as a reset and count the post-reset value as new production. Default True. The increments are computed once over the whole counter signal; a reading reports the production since the previous reading, so a period [start, end] gets the increments of the readings in (start, end]. The production between the last reading before start and the first reading in the period is counted, and a reading on a boundary shared by two periods is booked once, to the period ending there. When False, actual_qty is max(0, last - first) of the readings in [start, end].

True
as_of Timestamp | str | None

Evaluation time for the recovery rate. Defaults to the last timestamp present in the data.

None
ahead_pct float

Adherence above this is ahead.

105.0
behind_pct float

Adherence below this is behind.

95.0
critical_pct float

Adherence below this is critical_behind.

80.0

Returns:

Type Description
DataFrame

Summary-shape DataFrame: start, end, duration_seconds,

DataFrame

uuid, source_uuid, order_id, planned_qty,

DataFrame

actual_qty, schedule_adherence_pct, deviation_units,

DataFrame

deviation_type, recovery_required_rate,

DataFrame

remaining_seconds, sample_count, resets.

Raises:

Type Description
ValueError

If the band thresholds are not ordered critical_pct <= behind_pct <= ahead_pct.

deviation_events ¤

deviation_events(
    counter_uuid: str,
    *,
    handle_resets: bool = True,
    as_of: Timestamp | str | None = None,
    ahead_pct: float = 105.0,
    behind_pct: float = 95.0,
    critical_pct: float = 80.0,
    include_ahead: bool = False
) -> pd.DataFrame

Emit one event per period that deviates from plan.

Parameters:

Name Type Description Default
counter_uuid str

Identifier of the monotonic production counter.

required
handle_resets bool

See :meth:adherence_by_period.

True
as_of Timestamp | str | None

See :meth:adherence_by_period.

None
ahead_pct float

Adherence above this is ahead.

105.0
behind_pct float

Adherence below this is behind.

95.0
critical_pct float

Adherence below this is critical_behind.

80.0
include_ahead bool

Also emit rows for periods running ahead of plan. Off by default — running ahead is rarely an alert, but it is worth reporting when the plan feeds a downstream commitment.

False

Returns:

Type Description
DataFrame

Point-shape DataFrame: systime, uuid, source_uuid,

DataFrame

order_id, planned_qty, actual_qty,

DataFrame

schedule_adherence_pct, deviation_units,

DataFrame

deviation_type, recovery_required_rate, severity,

DataFrame

direction, threshold.

adherence_summary ¤

adherence_summary(
    counter_uuid: str,
    *,
    handle_resets: bool = True,
    as_of: Timestamp | str | None = None,
    ahead_pct: float = 105.0,
    behind_pct: float = 95.0,
    critical_pct: float = 80.0
) -> pd.DataFrame

Roll the per-period adherence up across the scheduled horizon.

overall_adherence_pct is computed from the summed quantities, not as the mean of the per-period percentages: a period with a planned quantity of 10 should not weigh as much as one planning 1000.

Parameters:

Name Type Description Default
counter_uuid str

Identifier of the monotonic production counter.

required
handle_resets bool

See :meth:adherence_by_period.

True
as_of Timestamp | str | None

See :meth:adherence_by_period.

None
ahead_pct float

Adherence above this is ahead.

105.0
behind_pct float

Adherence below this is behind.

95.0
critical_pct float

Adherence below this is critical_behind.

80.0

Returns:

Type Description
DataFrame

Summary-shape DataFrame with a single row: start, end,

DataFrame

duration_seconds, uuid, source_uuid,

DataFrame

total_planned_qty, total_actual_qty,

DataFrame

overall_adherence_pct, deviation_units, periods,

DataFrame

periods_on_track, periods_behind,

DataFrame

periods_critical_behind, periods_ahead,

DataFrame

on_plan_rate_pct, worst_order_id,

DataFrame

worst_adherence_pct, sample_count.

ScrapEvents ¤

ScrapEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None
)

Bases: Base

Track material scrap and waste.

Each UUID represents one signal: - scrap_uuid: scrap weight or count signal - reason_uuid: scrap reason code (optional) - part_id_uuid: part number / material type (optional)

Merge keys: [date, shift] for shift-level, [period] for trend, [reason] for reason-level, [part_number] for part-level.

Pipeline example::

scrap = ScrapEvents(df)
shift_scrap = scrap.scrap_by_shift('scrap_weight')
# → merge with ShiftReportEvents.shift_production() on [date, shift]
# → merge with QualityEvents.nok_by_shift() on [date, shift]
cost = scrap.scrap_cost('scrap_weight', 'part_id', {'A': 12.5})
# → merge with QualityEvents.quality_by_part() on [part_number]
Example

import pandas as pd, ts_shape scrap = ts_shape.make_timeseries(["scrap_kg"], n_points=20, freq="1min", ... baseline=0.0, noise=0, drift=9.5) reason = ts_shape.make_id_signal("reason", ["Burr", "Crack"], freq="1min") out = ts_shape.ScrapEvents(pd.concat([scrap, reason])).scrap_by_reason( ... "scrap_kg", "reason") out[["reason", "scrap_quantity", "share_of_total_pct"]].round(1) reason scrap_quantity share_of_total_pct 0 Crack 5.0 52.6 1 Burr 4.5 47.4

scrap_by_shift ¤

scrap_by_shift(
    scrap_uuid: str,
    *,
    value_column: str = "value_double",
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Scrap quantity per shift.

Parameters:

Name Type Description Default
scrap_uuid str

UUID for scrap weight/count signal.

required
value_column str

Column containing scrap values.

'value_double'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the scrap accumulated after the reset instead of losing it. The increments are computed once over the whole signal and booked to the shift of the reading that reports them, so the shifts add up to the signal total. The result gains a resets column with the number of resets observed in each shift. When False, the quantity is max(0, last - first) of the shift's own readings: a shift containing a reset reports too little (often 0) and the production between two shifts' readings is lost.

True
production_day bool

When True, an overnight shift's post-midnight readings are attributed to the shift's start date, so a night shift is one contiguous (date, shift) group. Default True; False groups by raw calendar date, which splits a night shift across two rows. Only affects overnight schedules.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds: the shift window from shift_definitions (date at the shift start time, tz-aware in the data's tz; an overnight shift ends on the next day)
DataFrame
  • date, shift, scrap_quantity
DataFrame
  • resets: Number of counter resets in the shift (only when handle_resets=True)

scrap_by_reason ¤

scrap_by_reason(
    scrap_uuid: str,
    reason_uuid: str,
    *,
    value_column_scrap: str = "value_double",
    value_column_reason: str = "value_string",
    handle_resets: bool = True
) -> pd.DataFrame

Scrap quantity by reason code.

Parameters:

Name Type Description Default
scrap_uuid str

UUID for scrap weight/count signal.

required
reason_uuid str

UUID for scrap reason code signal.

required
value_column_scrap str

Column containing scrap values.

'value_double'
value_column_reason str

Column containing reason codes.

'value_string'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the scrap accumulated after the reset. The reason is attached to a single global counter, so the reset-aware quantity is the sum of per-reading increments over the full time-sorted series, grouped by reason -- this also corrects the over-count that last - first produces when reasons alternate over a non-contiguous group. The result gains a resets column with the number of resets attributed to each reason. When False, the quantity is max(0, last - first) per reason group and a reset is lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • reason, scrap_quantity, pct_of_total
DataFrame
  • resets: Number of counter resets for the reason (only when handle_resets=True)
DataFrame

Canonical twin columns: share_of_total_pct (after pct_of_total); each

DataFrame

holds the same values as the deprecated column right before it, which is

DataFrame

removed in the next major release.

scrap_cost ¤

scrap_cost(
    scrap_uuid: str,
    part_id_uuid: str,
    material_costs: dict[str, float],
    *,
    value_column_scrap: str = "value_double",
    value_column_part: str = "value_string",
    handle_resets: bool = True
) -> pd.DataFrame

Convert scrap quantities to monetary cost.

Parameters:

Name Type Description Default
scrap_uuid str

UUID for scrap weight/count signal.

required
part_id_uuid str

UUID for part number / material type signal.

required
material_costs dict[str, float]

Dict mapping part numbers to cost per unit scrap.

required
value_column_scrap str

Column containing scrap values.

'value_double'
value_column_part str

Column containing part numbers.

'value_string'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the scrap accumulated after the reset. The increments are computed once over the whole signal and booked to the part of the reading that reports them, so the parts add up to the signal total (also when parts alternate). The result gains a resets column with the number of resets observed for each part. When False, the quantity is max(0, last - first) per part and a reset is lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • part_number, scrap_quantity, cost_per_unit, total_cost
DataFrame
  • resets: Number of counter resets for the part (only when handle_resets=True)

scrap_trend ¤

scrap_trend(
    scrap_uuid: str,
    *,
    value_column: str = "value_double",
    window: str = "1D",
    handle_resets: bool = True
) -> pd.DataFrame

Track scrap quantity trend over time.

Parameters:

Name Type Description Default
scrap_uuid str

UUID for scrap weight/count signal.

required
value_column str

Column containing scrap values.

'value_double'
window str

Time window for aggregation (default '1D').

'1D'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the scrap accumulated after the reset. The increments are computed once over the whole signal and booked to the period of the reading that reports them, so the periods add up to the signal total. The result gains a resets column with the number of resets observed in each period. When False, the quantity is max(0, last - first) of the period's own readings: a reset and the production between two periods' readings are lost.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end (start + window), duration_seconds
DataFrame
  • period, scrap_quantity
DataFrame
  • resets: Number of counter resets in the period (only when handle_resets=True)

SetupTimeEvents ¤

SetupTimeEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None,
    event_uuid: str = "prod:setup_time"
)

Bases: Base

Track and analyze setup/changeover durations.

Each UUID represents one signal: - run_state_uuid: machine state signal containing a setup indicator value - part_id_uuid: part number / product type signal (optional)

Merge keys: [date, shift] for shift-level, [period] for trend data, [from_product, to_product] for product transition analysis.

Durations are reported in seconds (*_seconds); the *_minutes columns are kept as a convenience.

Pipeline example::

setup = SetupTimeEvents(df)
durations = setup.setup_durations('machine_state', setup_value='Setup')
# → merge with ShiftReportEvents.shift_production() on [date, shift]
stats = setup.setup_statistics('machine_state')
# → standalone KPI reporting
Example usage

tracker = SetupTimeEvents(df)

List all setup events¤

events = tracker.setup_durations(run_state_uuid='machine_state')

Setup time by product transition¤

by_product = tracker.setup_by_product( run_state_uuid='machine_state', part_id_uuid='part_number', )

Initialize the setup-time tracker.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data.

required
time_column str

Name of timestamp column (default: 'systime').

'systime'
shift_definitions dict[str, tuple[str, str]] | None

Mapping of shift name to (start, end) times. Default: 3-shift operation (06:00-14:00, 14:00-22:00, 22:00-06:00).

None
event_uuid str

UUID tagged on derived interval events (the uuid column of :meth:setup_durations; default: 'prod:setup_time').

'prod:setup_time'

setup_durations ¤

setup_durations(
    run_state_uuid: str,
    *,
    setup_value: str = "Setup",
    value_column: str = "value_string",
    production_day: bool = True
) -> pd.DataFrame

List every setup event with duration.

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal.

required
setup_value str

Value that indicates a setup state.

'Setup'
value_column str

Column containing state values.

'value_string'
production_day bool

Attribute an overnight shift's post-midnight setups to the shift's start date, so a night shift is one (date, shift) row (default). False groups by raw calendar date, which splits a night shift across two rows.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds (float seconds, end - start), uuid (event uuid), source_uuid (run_state_uuid), duration_minutes (convenience: duration_seconds / 60), date, shift

setup_by_product ¤

setup_by_product(
    run_state_uuid: str,
    part_id_uuid: str,
    *,
    setup_value: str = "Setup",
    value_column_state: str = "value_string",
    value_column_part: str = "value_string"
) -> pd.DataFrame

Setup time statistics by product transition (from → to).

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal.

required
part_id_uuid str

UUID for part number / product type signal.

required
setup_value str

Value indicating setup state.

'Setup'
value_column_state str

Column containing state values.

'value_string'
value_column_part str

Column containing part number values.

'value_string'

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • from_product, to_product, avg_minutes, avg_seconds, min_minutes, min_seconds, max_minutes, max_seconds, count
DataFrame

The *_minutes columns are a convenience (*_seconds / 60).

setup_statistics ¤

setup_statistics(
    run_state_uuid: str,
    *,
    setup_value: str = "Setup",
    value_column: str = "value_string"
) -> pd.DataFrame

Overall setup time statistics.

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal.

required
setup_value str

Value indicating setup state.

'Setup'
value_column str

Column containing state values.

'value_string'

Returns:

Type Description
DataFrame

DataFrame (single row) with columns:

DataFrame
  • total_setups, total_minutes, total_seconds, avg_minutes, avg_seconds, median_minutes, median_seconds, std_minutes, std_seconds, pct_of_available_time
DataFrame

The *_minutes columns are a convenience (*_seconds / 60).

DataFrame

Canonical twin columns: share_of_available_time_pct (after

DataFrame

pct_of_available_time); each holds the same values as the deprecated

DataFrame

column right before it, which is removed in the next major release.

setup_trend ¤

setup_trend(
    run_state_uuid: str,
    *,
    setup_value: str = "Setup",
    value_column: str = "value_string",
    window: str = "1W"
) -> pd.DataFrame

Track setup time trend over time.

Parameters:

Name Type Description Default
run_state_uuid str

UUID for machine state signal.

required
setup_value str

Value indicating setup state.

'Setup'
value_column str

Column containing state values.

'value_string'
window str

Time window for aggregation (default '1W').

'1W'

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • start, end, duration_seconds: the period's bin bounds (for the default '1W' the Monday-to-Monday week; period is the pandas bin label, i.e. the week's Sunday)
DataFrame
  • period, avg_setup_minutes, avg_setup_seconds, setup_count, total_setup_minutes, total_setup_seconds (the *_minutes columns are a convenience)

ShiftHandoverEvents ¤

ShiftHandoverEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None
)

Bases: Base

Generate automated shift handover reports.

Combines production, quality, and downtime data into a single summary suitable for shift handover meetings.

Merge keys: [date, shift] — the report output is keyed on these columns.

Two usage patterns:

Pattern A — from raw signals (one-step):

report = ShiftHandoverEvents(df)
result = report.generate_report(
    counter_uuid='prod', ok_counter_uuid='ok',
    nok_counter_uuid='nok', run_state_uuid='state',
    targets={'shift_1': 450, 'shift_2': 450},
)

Pattern B — pipeline (compose from upstream modules):

# Step 1: compute upstream DataFrames
prod   = ShiftReportEvents(df).shift_production('counter')
qual   = QualityEvents(df).nok_by_shift('ok', 'nok')
downtime = DowntimeEvents(df).downtime_by_shift('state')

# Step 2: assemble into handover report
result = ShiftHandoverEvents.from_shift_data(
    production_df=prod,
    quality_df=qual,
    downtime_df=downtime,
    targets={'shift_1': 450, 'shift_2': 450},
)

Both patterns return the same output schema.

Run state may be a string state signal (running_value="Running" in value_string) or a boolean run signal (value_bool, True = running); see :meth:generate_report. downtime_seconds is the downtime in seconds; downtime_minutes is kept as a convenience.

from_shift_data staticmethod ¤

from_shift_data(
    production_df: DataFrame,
    quality_df: DataFrame | None = None,
    downtime_df: DataFrame | None = None,
    *,
    targets: dict[str, float] | None = None,
    report_date: str | None = None
) -> pd.DataFrame

Build a handover report from pre-computed shift-level DataFrames.

This is the pipeline-friendly entry-point. Instead of reading raw signals, it accepts DataFrames that were already computed by upstream modules (ShiftReportEvents, QualityEvents, DowntimeEvents, etc.).

Parameters:

Name Type Description Default
production_df DataFrame

DataFrame with [date, shift, quantity] — e.g. from ShiftReportEvents.shift_production().

required
quality_df DataFrame | None

DataFrame with [date, shift, ok_parts, nok_parts, quality_pct] — e.g. from QualityEvents.nok_by_shift(). Optional.

None
downtime_df DataFrame | None

DataFrame with [date, shift, availability_pct, downtime_seconds] — e.g. from DowntimeEvents.downtime_by_shift(). Optional. A frame with only the legacy downtime_minutes still works, with a DeprecationWarning, until the next major release.

None
targets dict[str, float] | None

Per-shift production targets (dict).

None
report_date str | None

Specific date (YYYY-MM-DD). If None, uses latest.

None

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame

date, shift, production, production_target, production_achievement_pct,

DataFrame

ok_parts, nok_parts, quality_pct, availability_pct, downtime_minutes,

DataFrame

downtime_seconds. When downtime_df carries only one of

DataFrame

downtime_seconds / downtime_minutes, the other is derived

DataFrame

from it.

generate_report ¤

generate_report(
    counter_uuid: str,
    ok_counter_uuid: str,
    nok_counter_uuid: str,
    run_state_uuid: str,
    *,
    targets: dict[str, float] | None = None,
    quality_target_pct: float = 98.0,
    availability_target_pct: float = 90.0,
    running_value: str | bool = "Running",
    value_column_counter: str = "value_integer",
    value_column_state: str = "value_string",
    report_date: str | None = None,
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Generate a shift handover report from raw timeseries signals.

For pipeline usage with pre-computed DataFrames, use :meth:from_shift_data instead.

Parameters:

Name Type Description Default
counter_uuid str

UUID of production counter.

required
ok_counter_uuid str

UUID of good parts counter.

required
nok_counter_uuid str

UUID of the NOK part counter.

required
run_state_uuid str

UUID of machine state signal.

required
targets dict[str, float] | None

Per-shift production targets.

None
quality_target_pct float

Quality target percentage.

98.0
availability_target_pct float

Availability target percentage.

90.0
running_value str | bool

Value indicating machine is running. A bool matches a boolean run signal (value_bool by default); a boolean run signal is also used, with True as running, when the state rows carry no value_column_state value.

'Running'
value_column_counter str

Column for counter values.

'value_integer'
value_column_state str

Column for state values.

'value_string'
report_date str | None

Specific date (YYYY-MM-DD). If None, uses latest date.

None
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset for production, ok_parts and nok_parts. A shift boundary is exactly where counters commonly reset, so max(0, last - first) (handle_resets=False) under-reports any shift containing a reset. The increments are computed once over each whole signal (before report_date is selected); the increment between two readings is spread over the time between them and split at the shift boundaries in proportion to time, so the shifts add up to the signal totals and production / ok_parts / nok_parts can be fractional; with handle_resets=False the production between two shifts' readings is lost. The result gains a resets column with the number of production-counter resets in each shift.

True
production_day bool

When True, an overnight shift's post-midnight readings are attributed to the shift's start date, so a night shift is one contiguous (date, shift) group instead of being split across two calendar dates. Default True. Only affects schedules with an overnight shift.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame

start, end, duration_seconds (the shift window from

DataFrame

shift_definitions, tz-aware in the data's tz; an overnight

DataFrame

shift ends on the next day), date, shift, production,

DataFrame

production_target, production_achievement_pct, ok_parts,

DataFrame

nok_parts, quality_pct, availability_pct, downtime_minutes

DataFrame

(convenience), downtime_seconds, and resets (only when

DataFrame

handle_resets=True).

highlight_issues ¤

highlight_issues(
    counter_uuid: str | None = ...,
    ok_counter_uuid: str | None = ...,
    nok_counter_uuid: str | None = ...,
    run_state_uuid: str | None = ...,
    *,
    report_df: DataFrame | None = ...,
    thresholds: dict[str, float] | None = ...,
    targets: dict[str, float] | None = ...,
    running_value: str | bool = ...,
    value_column_counter: str = ...,
    value_column_state: str = ...,
    report_date: str | None = ...,
    as_frame: Literal[True] = ...
) -> pd.DataFrame
highlight_issues(
    counter_uuid: str | None = ...,
    ok_counter_uuid: str | None = ...,
    nok_counter_uuid: str | None = ...,
    run_state_uuid: str | None = ...,
    *,
    report_df: DataFrame | None = ...,
    thresholds: dict[str, float] | None = ...,
    targets: dict[str, float] | None = ...,
    running_value: str | bool = ...,
    value_column_counter: str = ...,
    value_column_state: str = ...,
    report_date: str | None = ...,
    as_frame: Literal[False]
) -> list[dict[str, Any]]
highlight_issues(
    counter_uuid: str | None = None,
    ok_counter_uuid: str | None = None,
    nok_counter_uuid: str | None = None,
    run_state_uuid: str | None = None,
    *,
    report_df: DataFrame | None = None,
    thresholds: dict[str, float] | None = None,
    targets: dict[str, float] | None = None,
    running_value: str | bool = "Running",
    value_column_counter: str = "value_integer",
    value_column_state: str = "value_string",
    report_date: str | None = None,
    as_frame: bool = True
) -> pd.DataFrame | list[dict[str, Any]]

Identify issues that need attention.

Can be called in two ways:

  1. From raw signals (provide UUIDs): highlight_issues('prod', 'ok', 'nok', 'state', thresholds={...})

  2. From a pre-built report DataFrame: highlight_issues(report_df=my_report, thresholds={...})

Parameters:

Name Type Description Default
counter_uuid str | None

UUID of production counter (raw-signal mode).

None
ok_counter_uuid str | None

UUID of good parts counter (raw-signal mode).

None
nok_counter_uuid str | None

UUID of the NOK part counter (raw-signal mode).

None
run_state_uuid str | None

UUID of machine state signal (raw-signal mode).

None
report_df DataFrame | None

Pre-computed report DataFrame (pipeline mode). If provided, UUID arguments are ignored.

None
thresholds dict[str, float] | None

Minimum acceptable values for each metric. Defaults to production_achievement_pct=95, quality_pct=98, availability_pct=90.

None
targets dict[str, float] | None

Per-shift production targets.

None
running_value str | bool

Value indicating machine is running.

'Running'
value_column_counter str

Column for counter values.

'value_integer'
value_column_state str

Column for state values.

'value_string'
report_date str | None

Specific date (YYYY-MM-DD).

None
as_frame bool

When True (default), return a DataFrame (see below). as_frame=False returns the list of dicts; it is deprecated, emits a DeprecationWarning and will be removed in the next major release.

True

Returns:

Type Description
DataFrame | list[dict[str, Any]]

With as_frame=True (default), one row per issue with columns

:attr:`ISSUE_COLUMNS`

start, end, duration_seconds (the shift

DataFrame | list[dict[str, Any]]

window of the report row; NaT / NaN when the report has none,

DataFrame | list[dict[str, Any]]

e.g. one built by :meth:from_shift_data), date, shift, metric,

DataFrame | list[dict[str, Any]]

value, threshold, severity, severity_level (warn / critical).

DataFrame | list[dict[str, Any]]

With as_frame=False (deprecated), a list of dicts with keys

DataFrame | list[dict[str, Any]]

shift, metric, value, threshold, severity.

DataFrame | list[dict[str, Any]]

severity is 'warning' (within 5% of threshold) or 'critical'.

Raises:

Type Description
ValueError

If report_df is None and any of the four signal UUIDs is missing, so no report can be built.

ShiftReportEvents ¤

ShiftReportEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None
)

Bases: Base

Simple shift-based production reporting.

Each UUID represents one signal: - counter_uuid: production counter - part_id_uuid: part number (optional)

Merge keys: [date, shift] for shift-level outputs.

Pipeline example::

reporter = ShiftReportEvents(df)
prod = reporter.shift_production('counter')
# → merge with QualityEvents.nok_by_shift() on [date, shift]
# → merge with DowntimeEvents.downtime_by_shift() on [date, shift]
# → feed combined DataFrame into ShiftHandoverEvents.from_shift_data()
Example usage

reporter = ShiftReportEvents(df, shift_definitions={ "day": ("06:00", "14:00"), "afternoon": ("14:00", "22:00"), "night": ("22:00", "06:00"), })

Production per shift¤

shift_prod = reporter.shift_production( counter_uuid='counter_signal', part_id_uuid='part_number_signal' )

Compare shifts¤

comparison = reporter.shift_comparison(counter_uuid='counter_signal')

Initialize shift reporter.

Parameters:

Name Type Description Default
dataframe DataFrame

Input DataFrame with timeseries data

required
time_column str

Name of timestamp column (default: 'systime')

'systime'
shift_definitions dict[str, tuple[str, str]] | None

Dictionary mapping shift names to (start, end) times Default: 3-shift operation (06:00-14:00, 14:00-22:00, 22:00-06:00)

None
Example shift_definitions

{ "shift_1": ("06:00", "14:00"), "shift_2": ("14:00", "22:00"), "shift_3": ("22:00", "06:00"), }

shift_production ¤

shift_production(
    counter_uuid: str,
    part_id_uuid: str | None = None,
    *,
    value_column_counter: str = "value_integer",
    value_column_part: str = "value_string",
    date: str | None = None,
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Production quantity per shift.

The reset-aware increments are computed once over the whole counter signal and each one is booked to the shift of the reading that reports it, so the shift quantities add up to the signal total: the parts made between the last reading of one shift and the first reading of the next are counted in the next shift (in the example, 310 parts over 06:00-21:30 are 150 + 160, not 150 + 150). A shift boundary is also where counters commonly reset (shift change, part change, PLC restart); a decreasing counter is treated as a reset and the post-reset production is counted.

Parameters:

Name Type Description Default
counter_uuid str

Production counter UUID

required
part_id_uuid str | None

Part number UUID (optional, for part-specific production)

None
value_column_counter str

Column containing counter values

'value_integer'
value_column_part str

Column containing part numbers

'value_string'
date str | None

Specific date in 'YYYY-MM-DD' format (optional). Selects the readings of that calendar date; their increments are still computed on the full signal, so the first reading of the day carries the production since the reading before it.

None
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it. The result gains a resets column with the number of resets observed in each shift (a drop at the shift's first reading counts in that shift). When False, the quantity is max(0, last_count - first_count) of the shift's own readings: a shift containing a reset reports too little and the production between two shifts' readings is lost.

True
production_day bool

When True, the post-midnight portion of an overnight shift (e.g. 22:00 -> 06:00) is attributed to the shift's start date, so a night shift is one contiguous (date, shift) group instead of being split across two calendar dates (the default). When False, grouping is by raw calendar date. Only affects schedules with an overnight shift.

True

Returns:

Type Description
DataFrame

DataFrame with production by shift:

DataFrame
  • start: Start of the shift window (date at the shift start time from shift_definitions, tz-aware in the data's tz)
DataFrame
  • end: End of the shift window (next day for overnight shifts)
DataFrame
  • duration_seconds: Shift window length in seconds
DataFrame
  • date: Production date
DataFrame
  • shift: Shift name
DataFrame
  • part_number: Part number (if part_id_uuid provided)
DataFrame
  • quantity: Parts produced: the reset-aware increments reported by the shift's readings (each since the previous reading)
DataFrame
  • resets: Number of counter resets in the shift (only when handle_resets=True)
Example

import ts_shape df = ts_shape.make_timeseries(["counter"], n_points=32, freq="30min", ... start="2025-01-01 06:00", noise=0, drift=310, value_column="value_integer") out = ts_shape.ShiftReportEvents(df).shift_production("counter") out[["date", "shift", "quantity"]] date shift quantity 0 2025-01-01 shift_1 150 1 2025-01-01 shift_2 160

shift_comparison ¤

shift_comparison(
    counter_uuid: str,
    *,
    value_column_counter: str = "value_integer",
    days: int = 7
) -> pd.DataFrame

Compare shift performance over recent days.

Parameters:

Name Type Description Default
counter_uuid str

Production counter UUID

required
value_column_counter str

Column containing counter values

'value_integer'
days int

Number of recent days to analyze (default: 7)

7

Returns:

Type Description
DataFrame

DataFrame with shift comparison:

DataFrame
  • shift: Shift name
DataFrame
  • avg_quantity: Average production per shift
DataFrame
  • min_quantity: Minimum production
DataFrame
  • max_quantity: Maximum production
DataFrame
  • std_quantity: Standard deviation
DataFrame
  • days_count: Number of days included
Example

import ts_shape df = ts_shape.make_timeseries(["counter"], n_points=96, freq="30min", ... start="2025-01-01 06:00", baseline=10, noise=3, value_column="value_integer") df["value_integer"] = df["value_integer"].cumsum() # running part counter out = ts_shape.ShiftReportEvents(df).shift_comparison("counter") out[["shift", "avg_quantity", "min_quantity", "max_quantity"]] shift avg_quantity min_quantity max_quantity 0 shift_1 150.0 144 156 1 shift_2 163.5 157 170 2 shift_3 156.5 147 166

shift_targets ¤

shift_targets(
    counter_uuid: str,
    targets: dict[str, float],
    *,
    value_column_counter: str = "value_integer",
    date: str | None = None
) -> pd.DataFrame

Compare actual production to shift targets.

Parameters:

Name Type Description Default
counter_uuid str

Production counter UUID

required
targets dict[str, float]

Dictionary mapping shift names to target quantities

required
value_column_counter str

Column containing counter values

'value_integer'
date str | None

Specific date in 'YYYY-MM-DD' format (optional)

None

Returns:

Type Description
DataFrame

DataFrame with target comparison:

DataFrame
  • date: Production date
DataFrame
  • shift: Shift name
DataFrame
  • actual: Actual production
DataFrame
  • target: Target production
DataFrame
  • variance: Difference (actual - target)
DataFrame
  • achievement_pct: Percentage of target achieved
Example

import ts_shape df = ts_shape.make_timeseries(["counter"], n_points=32, freq="30min", ... start="2025-01-01 06:00", noise=0, drift=310, value_column="value_integer") rep = ts_shape.ShiftReportEvents(df) out = rep.shift_targets("counter", targets={"shift_1": 150, "shift_2": 200}) out[["shift", "actual", "target", "variance", "achievement_pct"]] shift actual target variance achievement_pct 0 shift_1 150 150 0 100.0 1 shift_2 160 200 -40 80.0

best_and_worst_shifts ¤

best_and_worst_shifts(
    counter_uuid: str,
    *,
    value_column_counter: str = ...,
    days: int = ...,
    as_frame: Literal[True] = ...
) -> pd.DataFrame
best_and_worst_shifts(
    counter_uuid: str,
    *,
    value_column_counter: str = ...,
    days: int = ...,
    as_frame: Literal[False]
) -> dict[str, pd.DataFrame]
best_and_worst_shifts(
    counter_uuid: str,
    *,
    value_column_counter: str = "value_integer",
    days: int = 30,
    as_frame: bool = True
) -> dict[str, pd.DataFrame] | pd.DataFrame

Identify best and worst performing shifts.

Parameters:

Name Type Description Default
counter_uuid str

Production counter UUID

required
value_column_counter str

Column containing counter values

'value_integer'
days int

Number of recent days to analyze (default: 30)

30
as_frame bool

When True (default), return a single DataFrame (see below). as_frame=False returns the dictionary form; it is deprecated, emits a DeprecationWarning and will be removed in the next major release.

True

Returns:

Type Description
dict[str, DataFrame] | DataFrame

With as_frame=True (default), one DataFrame with columns:

dict[str, DataFrame] | DataFrame
  • start, end, duration_seconds: the shift window (date at the shift start time, tz-aware in the data's tz; an overnight shift ends on the next day)
dict[str, DataFrame] | DataFrame
  • rank_type: 'best' or 'worst'
dict[str, DataFrame] | DataFrame
  • date, shift, quantity
dict[str, DataFrame] | DataFrame

The best rows come first (highest quantity first), then the worst

dict[str, DataFrame] | DataFrame

rows (lowest quantity first). A shift can appear in both groups

dict[str, DataFrame] | DataFrame

when there are fewer than ten shifts.

dict[str, DataFrame] | DataFrame

With as_frame=False (deprecated), a dictionary with:

dict[str, DataFrame] | DataFrame
  • 'best': Top 5 best shifts (columns date, shift, quantity)
dict[str, DataFrame] | DataFrame
  • 'worst': Top 5 worst shifts (columns date, shift, quantity)
Example

import ts_shape df = ts_shape.make_timeseries(["counter"], n_points=96, freq="30min", ... start="2025-01-01 06:00", baseline=10, noise=3, value_column="value_integer") df["value_integer"] = df["value_integer"].cumsum() # running part counter ranked = ts_shape.ShiftReportEvents(df).best_and_worst_shifts("counter") best = ranked[ranked["rank_type"] == "best"] best[["date", "shift", "quantity"]].head(3) date shift quantity 0 2025-01-01 shift_2 170 1 2025-01-01 shift_3 166 2 2025-01-02 shift_2 157

TargetEvents ¤

TargetEvents(
    dataframe: DataFrame,
    *,
    time_column: str = "systime",
    shift_definitions: (
        dict[str, tuple[str, str]] | None
    ) = None
)

Bases: Base

Compare any production metric to targets.

Every plant has daily/shift targets. This module provides a generic way to compare actual performance against those targets.

Merge keys: [date, shift] for shift-level, [date] for daily.

Pipeline example::

target = TargetEvents(df)
result = target.compare_to_target('counter', {'shift_1': 450})
# → merge with PerformanceLossEvents.performance_by_shift() on [date, shift]
# → merge with QualityEvents.nok_by_shift() on [date, shift]
# → result['status'] column enables filtering/alerting
Example usage

tracker = TargetEvents(df)

Compare counter to fixed target¤

result = tracker.compare_to_target( metric_uuid='production_counter', targets={'shift_1': 450, 'shift_2': 450, 'shift_3': 400}, )

Achievement summary over time¤

summary = tracker.target_achievement_summary( metric_uuid='production_counter', daily_target=1300, )

compare_to_target ¤

compare_to_target(
    metric_uuid: str,
    targets: dict[str, float],
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True,
    production_day: bool = True
) -> pd.DataFrame

Compare actual metric to per-shift targets.

Parameters:

Name Type Description Default
metric_uuid str

UUID of the metric signal (counter).

required
targets dict[str, float]

Dict mapping shift names to target values.

required
value_column str

Column containing metric values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it, so actual is the reset-aware total. The increments are computed once over the whole signal and booked to the shift of the reading that reports them, so the shifts add up to the signal total. The result gains a resets column with the number of resets observed in each shift. When False, actual is simply max(0, last - first) and a shift containing a reset reports too little.

True
production_day bool

When True, the post-midnight portion of an overnight shift (e.g. 22:00 -> 06:00) is attributed to the shift's start date, so each (date, shift) group is one contiguous interval (the default). When False, readings are grouped by their raw calendar date; a night shift is then split across two dates and, with handle_resets=False, its group spans the whole day so last - first sweeps in other shifts' production. Only affects schedules with an overnight shift.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • date, shift, actual, target, variance, achievement_pct, status
DataFrame
  • resets (only when handle_resets=True)

target_achievement_summary ¤

target_achievement_summary(
    metric_uuid: str,
    daily_target: float,
    *,
    value_column: str = "value_integer",
    handle_resets: bool = True
) -> pd.DataFrame

Summarize target achievement over time.

Parameters:

Name Type Description Default
metric_uuid str

UUID of the metric signal (counter).

required
daily_target float

Daily production target.

required
value_column str

Column containing metric values.

'value_integer'
handle_resets bool

When True (default), treat a decreasing counter as a reset and count the production after the reset instead of losing it, so actual is the reset-aware daily total, with increments booked to the day of the reading that reports them so the days add up to the signal total. The result gains a resets column with the number of resets observed each day. When False, actual is simply max(0, last - first) and a day containing a reset reports too little.

True

Returns:

Type Description
DataFrame

DataFrame with columns:

DataFrame
  • date, actual, target, variance, achievement_pct, status
DataFrame
  • resets (only when handle_resets=True)

target_hit_rate ¤

target_hit_rate(
    metric_uuid: str,
    daily_target: float,
    *,
    value_column: str = ...,
    as_frame: Literal[True] = ...
) -> pd.DataFrame
target_hit_rate(
    metric_uuid: str,
    daily_target: float,
    *,
    value_column: str = ...,
    as_frame: Literal[False]
) -> dict[str, float | int]
target_hit_rate(
    metric_uuid: str,
    daily_target: float,
    *,
    value_column: str = "value_integer",
    as_frame: bool = True
) -> pd.DataFrame | dict[str, float | int]

How often are targets met?

Parameters:

Name Type Description Default
metric_uuid str

UUID of the metric signal (counter).

required
daily_target float

Daily production target.

required
value_column str

Column containing metric values.

'value_integer'
as_frame bool

When True (default), return a one-row DataFrame (see below). as_frame=False returns the dictionary form; it is deprecated, emits a DeprecationWarning and will be removed in the next major release.

True

Returns:

Name Type Description
DataFrame | dict[str, float | int]

With as_frame=True (default), a one-row summary DataFrame with

columns DataFrame | dict[str, float | int]
DataFrame | dict[str, float | int]
  • start, end, duration_seconds: first analysed day's midnight to the midnight after the last day (tz-aware in the data's tz; NaT / 0 without data)
DataFrame | dict[str, float | int]
  • total_days: Number of days analyzed.
DataFrame | dict[str, float | int]
  • days_on_target: Days where target was met.
DataFrame | dict[str, float | int]
  • hit_rate_pct: Percentage of days meeting target.
DataFrame | dict[str, float | int]
  • avg_achievement_pct: Average achievement across days.
DataFrame | dict[str, float | int]

With as_frame=False (deprecated), a dict with the keys

DataFrame | dict[str, float | int]

total_days, days_on_target, hit_rate_pct, avg_achievement_pct.