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_mapargument 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
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
With |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
keeps these columns. |
DataFrame
|
With |
DataFrame
|
columns = to_batch, values = transition count, plus a |
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 |
DataFrame
|
column (index named |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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: |
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: |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: systime, uuid (the detector's event uuid), |
DataFrame
|
source_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: |
True
|
as_frame
|
bool
|
When True (default), return one row per station (see
below). |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame | dict[str, Any]
|
With |
DataFrame | dict[str, Any]
|
per station that has data, in |
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
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 |
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:
Any other value, or a stable band that is never reached, uses
|
'fixed_window'
|
config
|
dict[str, Any] | None
|
Method settings. Keys: None means an empty dict. |
None
|
fallback
|
dict[str, Any] | None
|
Used when the window end cannot be determined:
|
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 |
required |
speed_uuid
|
str
|
UUID of the line-speed signal. |
required |
line_config
|
list[dict]
|
Physical layout description — list of dicts with keys
|
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 |
None
|
cut_uuid
|
str | None
|
Optional UUID whose values carry the cut piece length in
meters. Used by |
None
|
time_column
|
str
|
Name of the timestamp column (default |
'systime'
|
uuid_column
|
str
|
Name of the UUID/identifier column (default |
'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'
|
speed_unit
|
str
|
Unit of the speed signal. One of |
'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
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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
|
None
|
value_column
|
str | None
|
Value column to carry through. Defaults to the
constructor |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns |
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):
part_counter_uuid(boolean): each True row = 1 cut.part_counter_uuid(integer/float): each step where the counter increases bydeltacounts asdeltacuts, 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.cut_length_uuidonly: 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 |
required |
cut_length_uuid
|
str | None
|
UUID carrying the cut piece length value.
Defaults to the constructor |
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
|
'value_double'
|
handle_resets
|
bool
|
When True (default) and a numeric |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither |
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
|
None
|
window
|
str
|
Pandas resample frequency for the speed averaging window
(default |
'1min'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns |
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
|
None
|
window
|
str
|
Pandas resample frequency for the counting window (default
|
'1h'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns |
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 |
'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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with statistics per part: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with slow cycles: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with trend data: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with hourly statistics: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 carryvalue_boolbut novalue_string, or whenrunning_value/stopped_valueis 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: |
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 ( |
'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
|
None
|
production_day
|
bool
|
Attribute an overnight shift's post-midnight
readings to the shift's start date, so a night shift is one
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with downtime by shift: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
The |
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 ( |
'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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
The |
DataFrame
|
Canonical twin columns: |
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: |
'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: |
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: |
'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
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with availability trend: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
The |
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'
|
data_end
|
Timestamp | None
|
Optional bound for the final, still-open interval when
|
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. |
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; |
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'
|
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 |
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'
|
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 |
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: |
'next_change'
|
as_frame
|
bool
|
When True (default), return a summary DataFrame (see
below). |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
DataFrame | dict[str, Any]
|
With |
|
DataFrame | dict[str, Any]
|
rows, |
|
columns |
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
The totals of the dict form are the column sums: |
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
The events themselves come from :meth: |
|
DataFrame | dict[str, Any]
|
meth: |
|
DataFrame | dict[str, Any]
|
With |
|
DataFrame | dict[str, Any]
|
analytics for both blocked and starved events: |
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
|
DataFrame | dict[str, Any]
|
|
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'
|
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 |
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. |
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'
|
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 |
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), |
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 |
DataFrame
|
|
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 |
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: start, end (start + window), |
DataFrame
|
duration_seconds, uuid, source_uuid, is_delta, count (plus |
DataFrame
|
|
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'
|
takt_time
|
str
|
Takt time (pandas offset string, e.g. '60s'). |
'60s'
|
min_violation
|
str
|
How far a cycle must exceed |
'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 |
0.0
|
handle_resets
|
bool
|
When True (default), the part counter is treated as reset-aware
(see :meth: |
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
Canonical twin columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
before it, which is removed in the next major release. |
throughput_trends ¤
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'
|
interval_end
|
str
|
How each downtime interval is closed.
|
'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: |
'next_change'
|
data_end
|
Timestamp | None
|
Optional bound for a downtime still open at the end of
the data; forwarded to :meth: |
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 whenspecs=was given (held states close at the next change, and gaps longer than the spec'smax_gapare "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: |
None
|
data_end
|
Timestamp | None
|
Forwarded to :meth: |
None
|
as_frame
|
bool
|
When True (default), return a one-row DataFrame (see
below). |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame | dict[str, Any]
|
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 |
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
DataFrame | dict[str, Any]
|
|
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'
|
data_end
|
Timestamp | None
|
Optional bound for the final, still-open interval when
|
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: |
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
|
|
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. |
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 |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
parallel_activity ¤
parallel_activity() -> pd.DataFrame
Find items that were processed at multiple stations simultaneously.
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
routing_paths ¤
routing_paths() -> pd.DataFrame
Analyze routing path frequencies across all items.
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Summary-shape DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
True
|
max_gap
|
str | None
|
Optional cap forwarded to :meth: |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Summary-shape DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Summary-shape DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
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
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Summary-shape DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
time is unknown): |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
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. |
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 |
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
|
|
DataFrame
|
|
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
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
Canonical twin columns: |
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 |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
lead_time ¤
lead_time() -> pd.DataFrame
Compute end-to-end lead time per identifier.
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
'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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
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 |
'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
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
are convenience duplicates of the |
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
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
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'
|
date_column
|
str
|
Name of the date column. |
'date'
|
aggregations
|
dict[str, str] | None
|
Optional per-column override, |
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
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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. |
DataFrame
|
deprecated twins of |
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 |
True
|
production_day
|
bool
|
Attribute an overnight shift's post-midnight
readings to the shift's start date, so a night shift is one
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with quality metrics by shift: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with quality by part: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with NOK by reason: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
Canonical twin columns: |
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: |
True
|
production_day
|
bool
|
Forwarded to :meth: |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with daily quality: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
Canonical twin columns: |
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 |
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
|
|
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
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 |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
lead_time ¤
lead_time() -> pd.DataFrame
Compute end-to-end lead time per item.
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
routing_paths ¤
routing_paths() -> pd.DataFrame
Analyze routing path frequencies -- which station sequences are most common.
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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'
|
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 |
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'
|
max_gap
|
str | None
|
Optional cap (e.g. "15min") excluding a logging gap from
run time and utilization (see :meth: |
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
|
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 |
'systime'
|
uuid_column
|
str
|
Signal-identifier column of |
'uuid'
|
value_column
|
str
|
Counter-value column of |
'value_double'
|
schedule_start_column
|
str
|
Period-start column of |
'start'
|
schedule_end_column
|
str
|
Period-end column of |
'end'
|
planned_qty_column
|
str
|
Planned-quantity column of |
'planned_qty'
|
order_id_column
|
str
|
Optional order-identifier column of |
'order_id'
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
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
|
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 |
105.0
|
behind_pct
|
float
|
Adherence below this is |
95.0
|
critical_pct
|
float
|
Adherence below this is |
80.0
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Summary-shape DataFrame: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the band thresholds are not ordered
|
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: |
True
|
as_of
|
Timestamp | str | None
|
See :meth: |
None
|
ahead_pct
|
float
|
Adherence above this is |
105.0
|
behind_pct
|
float
|
Adherence below this is |
95.0
|
critical_pct
|
float
|
Adherence below this is |
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: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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: |
True
|
as_of
|
Timestamp | str | None
|
See :meth: |
None
|
ahead_pct
|
float
|
Adherence above this is |
105.0
|
behind_pct
|
float
|
Adherence below this is |
95.0
|
critical_pct
|
float
|
Adherence below this is |
80.0
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Summary-shape DataFrame with a single row: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
Canonical twin columns: |
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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 |
'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
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
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
|
|
DataFrame
|
The |
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
|
|
DataFrame
|
The |
DataFrame
|
Canonical twin columns: |
DataFrame
|
|
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
|
|
DataFrame
|
|
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
|
required |
quality_df
|
DataFrame | None
|
DataFrame with [date, shift, ok_parts, nok_parts, quality_pct]
— e.g. from |
None
|
downtime_df
|
DataFrame | None
|
DataFrame with [date, shift, availability_pct,
downtime_seconds] — e.g. from
|
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 |
DataFrame
|
|
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 ( |
'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 |
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 |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
start, end, duration_seconds (the shift window from |
DataFrame
|
|
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 |
DataFrame
|
|
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:
-
From raw signals (provide UUIDs):
highlight_issues('prod', 'ok', 'nok', 'state', thresholds={...}) -
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).
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame | list[dict[str, Any]]
|
With |
: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: |
DataFrame | list[dict[str, Any]]
|
value, threshold, severity, severity_level (warn / critical). |
DataFrame | list[dict[str, Any]]
|
With |
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 |
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 |
True
|
production_day
|
bool
|
When True, the post-midnight portion of an overnight
shift (e.g. |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with production by shift: |
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
DataFrame
|
|
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). |
True
|
Returns:
| Type | Description |
|---|---|
dict[str, DataFrame] | DataFrame
|
With |
dict[str, DataFrame] | DataFrame
|
|
dict[str, DataFrame] | DataFrame
|
|
dict[str, DataFrame] | DataFrame
|
|
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 |
dict[str, DataFrame] | DataFrame
|
|
dict[str, DataFrame] | DataFrame
|
|
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
|
True
|
production_day
|
bool
|
When True, the post-midnight portion of an overnight
shift (e.g. |
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
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
|
True
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with columns: |
DataFrame
|
|
DataFrame
|
|
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). |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
DataFrame | dict[str, float | int]
|
With |
|
columns |
DataFrame | dict[str, float | int]
|
|
DataFrame | dict[str, float | int]
|
|
|
DataFrame | dict[str, float | int]
|
|
|
DataFrame | dict[str, float | int]
|
|
|
DataFrame | dict[str, float | int]
|
|
|
DataFrame | dict[str, float | int]
|
|
|
DataFrame | dict[str, float | int]
|
With |
|
DataFrame | dict[str, float | int]
|
total_days, days_on_target, hit_rate_pct, avg_achievement_pct. |