Merging runs¶
A sequence is merged when no two neighbouring runs hold the same value.
Operations merge their results, so 1 1 2 is stored as two runs, not three.
This page covers when values count as the same, sequences that aren't
merged, and merging by your own rule.
When are two values the same?¶
Neighbouring runs merge when their values are identical, which differs from
== in two places:
NaNis identical toNaN, so a stretch of NaN is one run. (With==, NaN never equals anything, and long NaN stretches would stay in pieces.)-0.0is not identical to0.0, because they are different values.
Nulls are identical to each other, and never to NaN.
>>> import runlength as rl
>>> rl.Rle.from_dense([float("nan"), float("nan"), 0.0, -0.0, None, None])
Rle(length=6, runs=4, dtype=Float64)
runs: [2, 1, 1, 2]
values: [NaN, 0.0, -0.0, null]
Unmerged sequences¶
The constructor keeps runs as you give them, without merging:
>>> r = rl.Rle([1, 1, 2], [5, 5, 7])
>>> r
Rle(length=4, runs=3, dtype=Int64)
runs: [1, 1, 2]
values: [5, 5, 7]
>>> r.is_merged()
False
>>> r.merge()
Rle(length=4, runs=2, dtype=Int64)
runs: [2, 2]
values: [5, 7]
Unmerged sequences behave the same in every operation; they just use more
runs than needed. equals compares the positions, not the runs, so it doesn't
matter how a sequence is split:
>>> r.equals(r.merge())
True
Plans and zip_with(..., merge=False) also produce unmerged sequences, with
one run per segment.
Merging by your own rule¶
merge also takes a mask, with one boolean per pair of neighbouring runs:
mask[i] says whether run i + 1 merges into run i. A group of merged runs
keeps the value of its "first" or "last" run.
For example, merge neighbouring runs whose values differ by less than 0.5:
>>> import numpy as np
>>> signal = rl.Rle([2, 2, 2, 2], [1.0, 1.25, 5.0, 5.25])
>>> mask = np.abs(np.diff(np.asarray(signal.values))) < 0.5
>>> mask
array([ True, False, True])
>>> signal.merge(mask, keep="first")
Rle(length=8, runs=2, dtype=Float64)
runs: [4, 4]
values: [1.0, 5.0]
To give each group a value of your choosing, such as its mean, merge_plan
returns the groups without touching the values: group g ends at ends[g]
and holds runs offsets[g] up to offsets[g + 1].
>>> ends, offsets = signal.merge_plan(mask)
>>> ends.to_pylist(), offsets.to_pylist()
([4, 8], [0, 2, 4])
>>> runs = np.asarray(signal.runs)
>>> values = np.asarray(signal.values)
>>> starts = np.asarray(offsets)[:-1]
>>> sums = np.add.reduceat(runs * values, starts)
>>> lengths = np.add.reduceat(runs, starts)
>>> rl.Rle.from_ends(ends, sums / lengths)
Rle(length=8, runs=2, dtype=Float64)
runs: [4, 4]
values: [1.125, 5.125]
The result holds the mean of each group, counting every position.