Once you have access to clean location/presence data from a manufacturing shop floor then what? Though displays that depict the current number of attendees are convenient. A step further, by predicting impending congestion, allows the supervisor adequate time to effectively manage the situation and take necessary steps.
Here’s a step-by-step, honest breakdown of such an example - although it isn’t cutting edge, it’s effective, easily understandable (which matters on a production floor more than complexity) and a robust starting point.
Setup
Let’s assume that you already possess normalised presence information as demonstrated below based on your raw sensor data (you can look at my previous blog post on the Normalisation Layer to have an idea of how this normalisation works) :
# one row per presence interval
{
"zone_id": "station_7",
"entity_type": "wip_cart",
"entry_time": "2026-07-29T09:14:02",
"exit_time": "2026-07-29T09:22:41",
"confidence": 0.91
}
The Goal - Given the recent input data, we want to establish if a particular station (for example, station_7) is likely to go above its regular capacity in the next thirty minutes.
1. Create a Regular Time Series of the Zone’s Occupancy
This step takes your existing range (interval) data and generates a standardized time series for occupancy that regularly samples data over specified periods (1-minute here). This is crucial, and the accuracy of all subsequent data depends on this step.
import pandas as pd
def build_occupancy_series(events_df, zone_id, freq="1min"):
events_df = events_df[events_df["zone_id"] == zone_id].copy()
events_df["entry_time"] = pd.to_datetime(events_df["entry_time"])
events_df["exit_time"] = pd.to_datetime(events_df["exit_time"])
start = events_df["entry_time"].min()
end = events_df["exit_time"].max()
timeline = pd.date_range(start, end, freq=freq)
occupancy = pd.Series(0, index=timeline)
for _, row in events_df.iterrows():
mask = (timeline >= row["entry_time"]) & (timeline < row["exit_time"])
occupancy[mask] += 1
return occupancy
Please note, though this code gets the job done, for very large datasets, you’ll want to consider more efficient data structures and algorithms (e.g., interval trees or sweep-line algorithms). However, this straightforward approach is perfectly adequate for getting a baseline model working.
2. Calculate Per-Zone Normal Operating Ranges (Baselines)
One key advantage of modelling congestion is recognising that “normal” operating conditions vary dramatically from zone to zone. A kitting station might typically accommodate only a few units, whereas a staging area before a production Bottleneck might regularly handle fifteen or more. This approach uses each zone’s historical data to establish its own realistic normal operating boundaries, a far more effective strategy than relying on plant-wide, fixed thresholds.
def compute_baseline(occupancy_series, quantile=0.90):
# baseline = the occupancy level this zone rarely exceeds under normal conditions
return occupancy_series.quantile(quantile)
When working with such a system, the most significant improvements in predictive accuracy are usually achieved by considering unique station baselines rather than global ones, often superseding the impact of algorithm complexity.
3. Develop a Simple Flow-Rate Predictor
Instead of instantly attempting a highly sophisticated time series forecasting model, begin with a more intuitive approach: projecting the current occupancy level and flow rate forward linearly. Although not highly advanced, this method offers a strong baseline and is easily traceable and debuggable within a factory environment.
def predict_congestion(occupancy_series, baseline, horizon_minutes=30, lookback_minutes=15):
recent = occupancy_series.last(f"{lookback_minutes}min")
if len(recent) < 2:
return {"will_exceed": False, "confidence": "low"}
inflow_rate = (recent.iloc[-1] - recent.iloc[0]) / lookback_minutes # per minute
projected = recent.iloc[-1] + (inflow_rate * horizon_minutes)
return {
"will_exceed": projected > baseline,
"projected_occupancy": round(projected, 1),
"baseline": baseline,
"confidence": "high" if len(recent) >= lookback_minutes * 0.7 else "low"
}
Remember the importance of assigning a confidence metric based on the volume of data. The prediction must indicate low certainty in situations with significant gaps in readings (network disruption or sensor malfunction) as falsely asserting high confidence under such circumstances could be far more problematic than saying the model lacks information.
4. Backtest Your Predictions
Before you can trust a model near operational systems, test its historical performance.
def backtest(occupancy_series, baseline, horizon_minutes=30):
correct = 0
total = 0
for i in range(len(occupancy_series) - horizon_minutes):
window = occupancy_series.iloc[:i+1]
prediction = predict_congestion(window, baseline, horizon_minutes)
actual_future = occupancy_series.iloc[i:i+horizon_minutes]
actually_exceeded = (actual_future > baseline).any()
if prediction["will_exceed"] == actually_exceeded:
correct += 1
total += 1
return correct / total if total else None
When applied to steady manufacturing environments, this linear-inflow approach typically yields around 70-80% accuracy for 30-minute forecasts, making it sufficiently reliable for decisions about staffing and material sequencing, even when dealing with rapid, non-linear events like unexpected machine downtime or production interruptions.
Next Steps…
For even better predictive accuracy, especially with abundant historical data, you could incorporate: scheduled production throughput, real-time occupancy at upstream stages (as congestion can cascade), and cyclical shift pattern information using approaches like Gradient-Boosting Models or even a basic ARIMA.
For further reference on production-grade implementations dealing with complex cascading effects between zones and ERP integrations for schedules, PlantLog AI has detailed descriptions of its in-plant logistics prediction system here. It’s my belief there’s room for creating a smarter, yet fundamentally simpler baseline prediction method; if so, share!
Top comments (0)