creek.tools#
Tools to index, window and segment streams.
Everything here is a plain function or a small callable class that you compose
with map, filter and functools.partial; nothing depends on Creek.
Main entry points:
filter_and_index_stream: index a stream (enumerateby default) and filter on the itemsdynamically_index / DynamicIndexer:enumeratewhere you choose how the index advancesBufferStats: a fixed-size buffer that returns a statistic of its contents each time it is fedSegmenter: feed aBufferStatsand hand its contents to a callback after each value
>>> from creek.tools import BufferStats
>>> list(map(BufferStats(maxlen=3, func=sum), range(5)))
[0, 1, 3, 6, 9]
Functions
|
Alternative to dynamically_index using itertools and partial |
|
Return |
|
Apply function (that returns an Iterable) to an element of a sequence and fanout (broadcast) the resulting items, to produce an iterable of tuples each containing one of these items along with 'a copy' of the other tuple elements |
|
Apply |
|
Index updater that ignores |
|
Doesn't even look at current_idx or obj. |
|
Generalization of |
|
Apply apply_and_fanout to an iterable of sequences. |
|
Apply apply_and_fanout to an iterable of dicts. |
|
Index a stream and filter it (based only on the data items). |
|
Return |
|
Return |
|
Returns True if, and only if, bt_tt_segment overlaps query interval. |
|
Index updater that advances the index by |
Classes
|
A callable, fixed-size (fifo) buffer returning a statistic of its contents on every call. |
|
Turn data items into |
|
Feed values to a |
- class creek.tools.BufferStats(values=(), maxlen=<object object>, func=<built-in function sum>, add_new_val=<method 'append' of 'collections.deque' objects>)[source]#
Bases:
dequeA callable, fixed-size (fifo) buffer returning a statistic of its contents on every call.
Each call adds the input to the buffer (
deque.appendby default, configurable throughadd_new_val) and returnsfuncapplied to the buffer’s contents. Use it withmapto get rolling-window statistics of a stream.- Parameters:
values – Initial contents of the buffer.
maxlen (
int) – Size of the buffer. Required.func (
Callable) – The function computed on the buffer contents and returned on each call;sumby default.add_new_val (
Callable) – The(buffer, new_val)function that adds a value to the buffer: adequemethod (deque.appendby default,deque.extend,deque.appendleft…), its name as a string, or any function with that signature.
- Raises:
TypeError – If
maxlenis not given, or is not anint.
>>> bs = BufferStats(maxlen=4, func=sum) >>> list(map(bs, range(7))) [0, 1, 3, 6, 10, 14, 18]
See what happens when you feed the same sequence again:
>>> list(map(bs, range(7))) [15, 12, 9, 6, 10, 14, 18]
More examples:
>>> list(map(BufferStats(maxlen=4, func=''.join), 'abcdefgh')) ['a', 'ab', 'abc', 'abcd', 'bcde', 'cdef', 'defg', 'efgh']
>>> from math import prod >>> list(map(BufferStats(maxlen=4, func=prod), range(7))) [0, 0, 0, 0, 24, 120, 360]
With a different
add_new_valchoice.>>> bs = BufferStats(maxlen=4, func=''.join, add_new_val=deque.appendleft) >>> list(map(bs, 'abcdefgh')) ['a', 'ba', 'cba', 'dcba', 'edcb', 'fedc', 'gfed', 'hgfe']
With
add_new_val=deque.extend, data can be fed in chunks. In the following, also see how we use iterize to get a function that takes an iterator and returns an iterator>>> from creek.util import iterize >>> window_stats = iterize(BufferStats( ... maxlen=4, func=''.join, add_new_val=deque.extend) ... ) >>> chks = ['a', 'bc', 'def', 'gh'] >>> for x in window_stats(chks): ... print(x) a abc cdef efgh
Note
To those who might think that they can optimize this for special cases: Yes you can. But SHOULD you? Is it worth the increase in complexity and reduction in flexibility? See https://github.com/thorwhalen/umpyre/blob/master/misc/performance_of_rolling_window_stats.md
- class creek.tools.DynamicIndexer(start=0, idx_updater=<function count_increments>)[source]#
Bases:
objectTurn data items into
(index, item)pairs, with a pluggable index update rule.- Parameters:
Let’s take a finite stream of finite iterables (strings here):
>>> stream = ['stream', 'of', 'different', 'sized', 'chunks']The default
DynamicIndexerjust does whatenumeratedoes:>>> counter_index = DynamicIndexer() >>> list(map(counter_index, stream)) [(0, 'stream'), (1, 'of'), (2, 'different'), (3, 'sized'), (4, 'chunks')]
That’s because it uses the default
idx_updaterfunction just increments by one. This function, DynamicIndexer.count_increments, is shown below>>> def count_increments(current_idx, data_item, step=1): ... return current_idx + step
To get the index starting at 10, we can specify
start=10, and to step the index by 3 we can partializecount_increments:>>> from functools import partial >>> step3 = partial(count_increments, step=3) >>> list(map(DynamicIndexer(start=10, idx_updater=step3), stream)) [(10, 'stream'), (13, 'of'), (16, 'different'), (19, 'sized'), (22, 'chunks')]
You can specify any custom
idx_updateryou want: The requirements being that this function should take(current_idx, data_item)as the input, and return the next “current index”, that is, what the index of the next data item will be. Note thatcount_incrementsignored thedata_itemcompletely, but sometimes you want to take the data item into account. For example, your data item may contain several elements, and you want your index to index these elements, therefore you should update your index by incrementing it with the number of elements.We have
DynamicIndexer.size_incrementsfor that, the code is shown below:>>> def size_increments(current_idx, data_item, size_func=len): ... return current_idx + size_func(data_item) >>> size_index = DynamicIndexer(idx_updater=DynamicIndexer.size_increments) >>> list(map(size_index, stream)) [(0, 'stream'), (6, 'of'), (8, 'different'), (17, 'sized'), (22, 'chunks')]
Q: What if I want the index of a data item to be a function of the data item itself?
A: Then you would use that function to make the
(idxof(data_item), data_item)pairs directly.DynamicIndexeris for the use case where the index of an item depends on the (number of, sizes of, etc.) items that came before it.- static count_increments(obj, step=1)#
Index updater that ignores
objand advances the index bystep.>>> count_increments(10, 'whatever') 11 >>> count_increments(10, 'whatever', step=5) 15
- idx_updater(obj, step=1)#
Index updater that ignores
objand advances the index bystep.>>> count_increments(10, 'whatever') 11 >>> count_increments(10, 'whatever', step=5) 15
- static size_increments(obj, size_func=<built-in function len>)#
Index updater that advances the index by
size_func(obj),lenby default.>>> size_increments(10, 'abc') 13
- class creek.tools.Segmenter(buffer, stats_buffer_callback=<function return_buffer_on_stats_condition>)[source]#
Bases:
objectFeed values to a
BufferStatsand hand its contents to a callback after each one.Each call
seg(new_val)addsnew_valtobuffer(which returns its statistic), then returnsstats_buffer_callback(stats, list(buffer)). With the default callback that is the buffer contents when the statistic is notNone, andNoneotherwise.- Parameters:
buffer (
BufferStats) – TheBufferStatsinstance that stores values and computes the statistic.stats_buffer_callback (
Callable[[Any,Iterable],Any]) –(stats, buffer_contents) -> result;return_buffer_on_stats_conditionby default.
>>> gen = iter(range(200)) >>> bs = BufferStats(maxlen=10, func=sum) >>> return_if_stats_is_odd = partial( ... return_buffer_on_stats_condition, ... cond=lambda x: x%2 == 1, else_val='The sum is not odd!' ... ) >>> seg = Segmenter(buffer=bs, stats_buffer_callback=return_if_stats_is_odd)
Since the sum of the values in the buffer [1] is odd, the buffer is returned:
>>> seg(new_val=1) [1]
Adding
1 + 2is still odd so:>>> seg(new_val=2) [1, 2]
Now since
1 + 2 + 5is even, theelse_valofreturn_if_stats_is_oddis returned instead>>> seg(new_val=5) 'The sum is not odd!'
- stats_buffer_callback(buffer, cond=<function is_not_none>, else_val=None)#
Return
bufferifcond(stats)holds, elseelse_val.The default
stats_buffer_callbackofSegmenter; partializecondandelse_valto make your own.>>> return_buffer_on_stats_condition( ... stats=3, buffer=[1,2,3,4], cond=lambda x: x%2 == 1 ... ) [1, 2, 3, 4] >>> return_buffer_on_stats_condition( ... stats=3, buffer=[1,2,3,4], cond=lambda x: x%2 == 0, else_val='3 is not even!' ... ) '3 is not even!'
- creek.tools.alt_dynamically_index(idx_updater=<function count_increments>, start=0)[source]#
Alternative to dynamically_index using itertools and partial
>>> def size_increments(current_idx, data_item, size_func=len): ... return current_idx + size_func(data_item) ... >>> stream = ['stream', 'of', 'different', 'sized', 'chunks'] >>> indexer = alt_dynamically_index(size_increments) >>> t = list(indexer(stream)) >>> assert t == [(0, 'stream'), (6, 'of'), (8, 'different'), (17, 'sized'), ... (22, 'chunks')]
- creek.tools.apply_and_fanout(seq, func, idx)[source]#
Apply function (that returns an Iterable) to an element of a sequence and fanout (broadcast) the resulting items, to produce an iterable of tuples each containing one of these items along with ‘a copy’ of the other tuple elements
>>> list(apply_and_fanout([1, 'abc', 3], iter, 1)) [(1, 'a', 3), (1, 'b', 3), (1, 'c', 3)] >>> list(apply_and_fanout(['bob', 'alice', 2], lambda x: x * ['hi'], 2)) [('bob', 'alice', 'hi'), ('bob', 'alice', 'hi')] >>> list(apply_and_fanout(["bob", "alice", 2], lambda x: x.upper(), 1)) [('bob', 'A', 2), ('bob', 'L', 2), ('bob', 'I', 2), ('bob', 'C', 2), ('bob', 'E', 2)]
See also
fanout_and_flattenandfanout_and_flatten_dicts
- creek.tools.apply_func_to_index(seq, apply_to_idx, func)[source]#
Apply
functo the element atapply_to_idxofseq, returning a tuple.>>> apply_func_to_index([1, 2, 3], 1, lambda x: x * 10) (1, 20, 3)
If you’re going to apply the same function to the same index, you might want to partialize
apply_func_to_indexto be able to reuse it simply:>>> from functools import partial >>> f = partial(apply_func_to_index, apply_to_idx=0, func=str.upper) >>> list(map(f, ['abc', 'defgh'])) [('A', 'b', 'c'), ('D', 'e', 'f', 'g', 'h')]
- creek.tools.count_increments(current_idx, obj, step=1)[source]#
Index updater that ignores
objand advances the index bystep.>>> count_increments(10, 'whatever') 11 >>> count_increments(10, 'whatever', step=5) 15
- creek.tools.current_time(current_idx, obj)[source]#
Doesn’t even look at current_idx or obj. Just gives the current time
- creek.tools.dynamically_index(iterable, start=0, idx_updater=<function count_increments>)[source]#
Generalization of
enumerate(iterable)that allows one to specify how the indices should be updated.The default is the same behavior as
enumerate: Starts with 0 and increments by 1.- Parameters:
iterable (
Iterable) – The items to index.start – The index of the first item.
idx_updater –
(current_idx, item) -> next_idx;count_increments(add 1) by default,size_incrementsaddslen(item).
- Returns:
An iterator of
(index, item)pairs.
See also
DynamicIndexer, the callable doing the work, when you want to apply the same indexing rule to several streams or keep the current index around.>>> stream = ['stream', 'of', 'different', 'sized', 'chunks'] >>> assert (list(dynamically_index(stream, start=2)) ... == list(enumerate(stream, start=2)) ... == [(2, 'stream'), (3, 'of'), (4, 'different'), (5, 'sized'), (6, 'chunks')] ... )
Say we wanted to increment the indices according to the size of the last item instead of just incrementing by 1 at every iteration tick…
>>> def size_increments(current_idx, data_item, size_func=len): ... return current_idx + size_func(data_item) >>> size_index = DynamicIndexer(idx_updater=DynamicIndexer.size_increments) >>> list(map(size_index, stream)) [(0, 'stream'), (6, 'of'), (8, 'different'), (17, 'sized'), (22, 'chunks')]
- creek.tools.fanout_and_flatten(iterable_of_seqs, func, idx, aggregator=<built-in method from_iterable of type object>)[source]#
Apply apply_and_fanout to an iterable of sequences.
>>> seq_iterable = [('abcdef', 'first'), ('ghij', 'second')] >>> func = lambda a: zip(*([iter(a)] * 2)) # func is a chunker >>> assert list(fanout_and_flatten(seq_iterable, func, 0)) == [ ... (('a', 'b'), 'first'), ... (('c', 'd'), 'first'), ... (('e', 'f'), 'first'), ... (('g', 'h'), 'second'), ... (('i', 'j'), 'second') ... ]
- creek.tools.fanout_and_flatten_dicts(iterable_of_dicts, func, fields, idx_field, aggregator=<built-in method from_iterable of type object>)[source]#
Apply apply_and_fanout to an iterable of dicts.
>>> iterable_of_dicts = [ ... {'wf': 'abcdef', 'tag': 'first'}, {'wf': 'ghij', 'tag': 'second'} ... ] >>> func = lambda a: zip(*([iter(a)] * 2)) # func is a chunker >>> fields = ['wf', 'tag'] >>> idx_field = 'wf' >>> assert list( ... fanout_and_flatten_dicts(iterable_of_dicts, func, fields, idx_field)) == [ ... {'wf': ('a', 'b'), 'tag': 'first'}, ... {'wf': ('c', 'd'), 'tag': 'first'}, ... {'wf': ('e', 'f'), 'tag': 'first'}, ... {'wf': ('g', 'h'), 'tag': 'second'}, ... {'wf': ('i', 'j'), 'tag': 'second'} ... ]
- creek.tools.filter_and_index_stream(stream, data_item_filt, timestamper=<class 'enumerate'>)[source]#
Index a stream and filter it (based only on the data items).
- Parameters:
stream (
Iterable) – The items to index and filter.data_item_filt –
A predicate on the data item, or a non-callable value:
items equal to that value are then dropped.timestamper (
Callable[[TypeVar(DataItem)],tuple[Any,TypeVar(DataItem)]]) – Turns the stream into(index, item)pairs;enumerateby default.
- Returns:
An iterator of
(index, item)pairs for the items that pass the filter.
>>> assert ( ... list(filter_and_index_stream('this is a stream', data_item_filt=' ')) == [ ... (0, 't'), ... (1, 'h'), ... (2, 'i'), ... (3, 's'), ... (6, 'i'), ... (7, 's'), ... (11, 'a'), ... (15, 's'), ... (16, 't'), ... (17, 'r'), ... (18, 'e'), ... (19, 'a'), ... (20, 'm') ... ])
>>> list(filter_and_index_stream( ... [1, 2, 3, 4, 5, 6, 7, 8], ... data_item_filt=lambda x: x % 2)) [(0, 1), (2, 3), (4, 5), (6, 7)]
- creek.tools.return_buffer_on_stats_condition(stats, buffer, cond=<function is_not_none>, else_val=None)[source]#
Return
bufferifcond(stats)holds, elseelse_val.The default
stats_buffer_callbackofSegmenter; partializecondandelse_valto make your own.>>> return_buffer_on_stats_condition( ... stats=3, buffer=[1,2,3,4], cond=lambda x: x%2 == 1 ... ) [1, 2, 3, 4] >>> return_buffer_on_stats_condition( ... stats=3, buffer=[1,2,3,4], cond=lambda x: x%2 == 0, else_val='3 is not even!' ... ) '3 is not even!'
- creek.tools.segment_overlaps(bt_tt_segment, query_bt, query_tt)[source]#
Returns True if, and only if, bt_tt_segment overlaps query interval.
A
bt_tt_segmentwill need to be of the(bt, tt, *data)format. That is, an iterable of at least two elements (thebtandtt) followed with more elements (the actual segment data).This function is made to be curried, as shown in the following example:
>>> from functools import partial >>> overlapping_segments_filt = partial(segment_overlaps, query_bt=4, query_tt=8) >>> >>> list(filter(overlapping_segments_filt, [ ... (1, 3, 'completely before'), ... (2, 4, 'still completely before (upper bounds are strict)'), ... (3, 6, 'partially before, but overlaps bottom'), ... (4, 5, 'totally', 'inside'), # <- note this tuple has 4 elements ... (5, 8), # <- note this tuple has only the minimum (2) elements, ... (7, 10, 'partially after, but overlaps top'), ... (8, 11, 'completely after (strict upper bound)'), ... (100, 101, 'completely after (obviously)') ... ])) [(3, 6, 'partially before, but overlaps bottom'), (4, 5, 'totally', 'inside'), (5, 8), (7, 10, 'partially after, but overlaps top')]