BLUF: Two industrial features can both be called a “five-second sum” and still describe different observations. Before comparing offline and online values, freeze interval closure, bucket origin, output labels, duplicate handling and sample availability. Compare contributing sample IDs first, then aggregates. Otherwise a matching number can hide a wrong window and a different number can be entirely correct under a different contract.
This article executes a small synthetic boundary fixture with pandas 2.3.3, NumPy 2.5.4 and Python 3.14.9. It is a reproducible implementation test, not a plant benchmark or proof of Flink behavior. Nineteen named checks passed across rolling membership, resampling configurations and deliberate contract mutations. No production sensor, feature store or control system was exercised.
Start with the feature decision, not the library default
Suppose an advisory maintenance model consumes a sum of dimensionless sensor observations over the preceding five seconds. The sum is chosen to make membership inspectable; it is not a recommended physical feature. A sample mean on irregular observations weights bursts more heavily than quiet periods. A time-weighted mean requires another interpolation and gap contract.
Extend your industrial data contract with asset and sensor identity, timestamp meaning, reference clock, observation ID, event time, available-at time, window duration, closure, origin, label, aggregation, minimum valid count and duplicate policy. Bind these fields to a feature-contract version. A column name such as vibration_5s does not encode them.
The pandas rolling documentation distinguishes duration windows from integer windows and defines closed explicitly. Set min_periods rather than inheriting defaults: offset-based windows default to one observation, whereas integer windows default to the window size. Use center=False for this trailing fixture. A centered label is not evidence of online availability.
An inspectable interval timeline
All observations belong to one synthetic sensor. Times are seconds after 00:00:00 UTC on a fixed fixture date. The two observations at second four are distinct sample IDs, not a retransmitted record. Their original stable order is retained.
| ID | Seconds | Value |
|---|---|---|
| A | 0 | 100 |
| B | 1 | 1 |
| C | 4 | 2 |
| D | 4 | 3 |
| E | 5 | 10 |
| F | 9 | 4 |
| G | 10 | 20 |
At evaluation time five, the trailing right-closed window is (0, 5]: A is outside; B, C, D and E are inside. Moving only A to 0.000001 seconds puts its impulse inside. The sum changes from 16 to 116 without changing the nominal five-second duration. This mutation is deliberately larger in value than the other observations so that a boundary defect is easy to see.
The Python datetime documentation distinguishes timezone-aware from naive timestamps. This fixture performs arithmetic with aware UTC values. Convert to Europe/Berlin only for display; do not strip the timezone and recompute elapsed windows on ambiguous local clock labels. UTC representation does not establish sensor clock accuracy; validate clock uncertainty before joining features separately.
Four closures, four defensible answers
The independent oracle uses ordinary datetime comparisons over the input prefix, not pandas rolling to generate its expected answer. For each evaluation row, compute start = event_time − five seconds. Admit the left endpoint only for left or both; admit the right endpoint only for right or both. Keep only rows already in that prefix. Sum the admitted IDs, or return missing if no observation meets min_periods=1.
| closed | Interval | IDs | Sum |
|---|---|---|---|
| right | (0, 5] | B C D E | 16 |
| left | [0, 5) | A B C D | 106 |
| both | [0, 5] | A B C D E | 116 |
| neither | (0, 5) | B C D | 6 |
The executable pandas path is s.rolling('5s', closed=mode, min_periods=1, center=False).sum(), with a monotonic timezone-aware DatetimeIndex. All seven outputs in each of the four modes were compared against the oracle, including empty windows. The hand-derived sums at second five provide a second, simple check on the oracle itself. A test that merely compares two copies of the same library call would not establish independent expected membership.
At the first row stamped second four, right-closed rolling returns 103; at the second it returns 106. A row-wise rolling operation sees the current prefix, not an atomic batch of all future rows sharing its timestamp. An online service that emits once per timestamp after collecting a group implements a different emission contract. Specify which result is authoritative rather than deleting repeated timestamps indiscriminately. A retransmission with the same observation ID needs a separate deduplication policy; distinct observations at the same time must not automatically collapse.
A bucket label is not a membership rule
The pandas resampling documentation separates closed from label. With five-second buckets, origin fixed at the fixture's UTC epoch, closed='left' and label='right', the output labeled five contains [0, 5) and sums to 106. Changing only label to 'left' moves that same value to label zero. It does not admit sample E at five.
With closed='right' and label='right', the output at five instead contains (0, 5] and sums to 16. Identical output timestamps therefore do not prove identical support. Retain interval_start, interval_end and closure with the represented timestamp, not only the label.
Origin and offset matter independently. Setting offset='2s' with left closure and right labels creates the bucket [2, 7), labeled seven, with sum 15. The fixture checks both closures and both labels for offsets zero and two seconds against an independent integer bucket oracle: floor((t − offset)/5) for left closure, ceil((t − offset)/5) − 1 for right closure. These are fixed-duration ticks. Do not extrapolate the rule to calendar months, daylight-saving calendar boundaries or another stream engine.
Availability and row counts are separate failure modes
The main experiment assumes each observation is available at its event time. A separate mutation delays E's availability until second seven. At decision time five, event-time membership alone still gives 16, but the available subset gives 6. Filter available_at ≤ decision_at before evaluating the as-of feature. Do not rewrite availability time to match event time when reconstructing a historical decision.
At second ten, rolling over the last three rows gives 34, while right-closed rolling over five seconds gives 24. The count window retains E at second five; the duration window excludes it. Irregular sampling makes a row count an unreliable substitute for elapsed support. Requiring min_periods=4 makes that duration result missing, because only two observations remain. It does not become zero or automatically justify imputation.
These checks do not implement watermark progress, allowed lateness or correction retention. Those belong to late-sensor-data finalization. Nor does membership testing prevent spectral aliasing or prove a predictive-maintenance backtest is leakage-free. It addresses one deterministic boundary even when clocks already agree and observations have arrived.
Offline–online contract decision table
| Difference | Check | Release decision |
|---|---|---|
| Same label, different IDs | Closure and support | Block parity claim |
| Same IDs, shifted label | label only | Fix timestamp contract |
| Shifted bucket grid | origin and offset | Freeze common UTC anchor |
| Repeated timestamp | ID and emission prefix | Keep distinct samples |
| Insufficient valid count | Explicit min_periods | Emit missing, not zero |
| Unavailable observation | available_at cutoff | Exclude from as-of result |
| Different numeric sum | IDs then numeric tolerance | Investigate before promotion |
A release checklist you can execute
1. Freeze the contract and a small input manifest with stable sample IDs. Include exact endpoints, just-inside and just-outside values, repeated timestamps, gaps and an impulse. Sort explicitly; reject unsupported timestamp order instead of relying on accidental ingestion order.
2. Generate expected membership independently. Test aggregate values as well, but retain ID sets for diagnosis: equal sums can arise from different records. For floating-point deployment values, define an error budget after proving membership; tolerance must not hide a missing observation.
3. Replay the same availability-filtered input through offline and online implementations. Compare feature version, support, label, count, validity and value. Add restart, partition and duplicate-delivery integration tests before treating this small pandas fixture as a serving release gate.
4. Deliberately mutate closure, origin, label, minimum count and availability. Require the gate to identify the intended contract difference. Store package lock, fixture, oracle and actual results with the release. This run also emitted a NumPy timedelta deprecation warning during the boundary-perturbation setup; assertions passed, but upgrade testing must address that compatibility warning rather than hiding it.
5. Monitor invalid and under-covered windows by asset and operating state. Keep bounded diagnostic IDs or digests rather than an unlimited per-window payload archive. Membership evidence costs storage; grouping duplicate timestamps may add buffering latency; stricter minimum counts reduce feature coverage.
What this proves—and the next useful review
These synthetic checks establish the stated pandas behavior for this fixture, not production throughput, sensor fidelity, model quality or avoided downtime. A sum can be numerically correct and physically meaningless. Large gaps, sensor quality, asynchronous arrival, clock conversion and reset recovery still need deployment-specific tests. Do not transfer pandas defaults to Flink or a feature-store SDK without executing the same contract fixtures there.
For an implementation review, bring one feature definition, a read-only timestamped sensor extract and the offline and serving configurations. I can help turn them into a versioned membership contract and a failing-boundary test suite. The useful first result is a precise explanation of which observations each value represents—not another model trained on subtly different windows.


