graphtimer v2 - Utility to plot timings












3












$begingroup$


GitHub repo (MIT)

Clone the repo and replace the contents of example.py with the one at the bottom and you'll have everything setup.



Explanation of the code



A long while ago I posted the question "Abstract graphing-and-timing functions".
It was a god class, wasn't extendable/configurable, you also had to use strings to interact with timeit.
And it just wasn't clear what was what.



A little while ago I posted an answer where I used a couple ikky timeit.timeit calls rather than build a graph. And Graipher showed me a nice looking graph. I've continued to see these nice graphs, with no easy way to make them myself. And so I decided I needed to way-too-early-spring clean my code.





  1. (timer.py) A Timer object should be in charge of building and calling timeit.

    This object was based of timeit.Timer, however I thought it needed some additional functionality:




    1. It should be able to time multiple functions.

    2. It should be able to test against multiple values.

    3. It should only perform timings.



  2. (graph.py) A graph class should be introduced that plots the data in the correct way. If you want to plot in something that isn't matplotlib then you just need to change the class.


  3. (plotter.py) Statistical analysis should not be in either the Timer or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion.

  4. (plotter.py) Performing all the above should be simple with good defaults. And so Plotter handles interacting with everything. Whilst it's a small wrapper it means that I, and other, don't have to write that many lines to get the wanted graph.


What I'd like out of a review.





  1. The thing I find most important right now is, is the design good?



    Is Plotter a poor design pattern? Is splitting the code out like I have done a bad idea?



  2. Is the usage for a user clean and readable?


  3. Is there a way to make my code more readable? I find MatPlotLib to be a bit on the not so clean side.

  4. Any and all critiques are welcome.


Code



(I have left out some code, and the .pyi files. These are available on GitHub.)



graph.py



from .graphtimer import CATEGORY10


class MatPlotLib:
def _graph_times(self, graph, data, domain, colors, error, fmt):
for results, color in zip(data, colors):
values = [v.value for v in results]
if error:
errors = zip(*[v.errors or for v in results])
for error in errors:
lower, upper = zip(*error)
graph.fill_between(domain, upper, lower, facecolor=color, edgecolor=None, alpha=0.1)
yield graph.plot(domain, values, fmt, color=color)[0]

def graph(self, graph, data, domain, *, functions=None, colors=CATEGORY10, title=None, legend=True, error=True,
x_label='Input', y_label='Time [s]', fmt='-'):
lines = list(self._graph_times(graph, data, domain, colors, error, fmt))
if x_label is not None:
graph.set_xlabel(x_label)
if y_label is not None:
graph.set_ylabel(y_label)
if title is not None and hasattr(graph, 'set_title'):
graph.set_title(title)
if legend and functions is not None and hasattr(graph, 'legend'):
graph.legend(lines, [fn.__name__ for fn in functions], loc=0)
return lines


timers.py



import timeit

SENTINAL = object()


class MultiTimer:
"""Interface to timeit.Timer to ease timing over multiple functions."""
def __init__(self, functions, timer=timeit.Timer):
self.timer = timer
self.functions = functions

def build_timer(self, fn, domain, stmt='fn(*args)', setup='pass', timer=SENTINAL, globals=SENTINAL,
args_conv=SENTINAL):
"""Build a timeit.Timer"""
if not isinstance(domain, tuple):
domain = domain,
if args_conv is not SENTINAL:
domain = args_conv(*domain)
if not isinstance(domain, tuple):
domain = domain,

if globals is SENTINAL:
globals = {}
else:
globals = globals.copy()
globals.update({'fn': fn, 'args': domain})

# print(f'{self.timer}({stmt!r}, {setup!r}, {timer!r}, {globals!r})')

if timer is SENTINAL:
timer = timeit.default_timer

return self.timer(stmt, setup, timer, globals=globals)

def build_timers(self, domain, *args, **kwargs):
"""Build multiple timers from various inputs and functions"""
return [
[
self.build_timer(fn, dom, *args, **kwargs)
for fn in self.functions
]
for dom in domain
]

def _call(self, domain, repeat, call, *args, **kwargs):
"""Helper function to generate timing data."""
if len(domain) == 0:
raise ValueError('domain must have at least one argument.')

functions = self.build_timers(domain, *args, **kwargs)
output = [[ for _ in domain] for _ in functions[0]]
for _ in range(repeat):
for j, fns in enumerate(functions):
for i, fn in enumerate(fns):
output[i][j].append(call(fn))
return output

def repeat(self, domain, repeat, number, *args, **kwargs):
"""Interface to timeit.Timer.repeat. `domain` is the values to pass to the functions."""
return self._call(domain, repeat, lambda f: f.timeit(number), *args, **kwargs)

def timeit(self, domain, number, *args, **kwargs):
"""Interface to timeit.Timer.timeit. `domain` is the values to pass to the functions."""
return [
[value[0] for value in values]
for values in self.repeat(domain, 1, number, *args, **kwargs)
]

def autorange(self, domain, *args, **kwargs):
"""Interface to timeit.Timer.autorange. `domain` is the values to pass to the functions."""
return [
[value[0] for value in values]
for values in self._call(domain, 1, lambda f: f.autorange(), *args, **kwargs)
]


class TimerNamespaceMeta(type):
"""Convenience class to ease creation of a MultiTimer."""
def __new__(mcs, name, bases, attrs):
if 'functions' in attrs:
raise TypeError('FunctionTimers cannot define `functions`')
if 'multi_timer' in attrs:
raise TypeError('FunctionTimers cannot define `multi_timer`')

ret: TimerNamespace = super().__new__(mcs, name, bases, attrs)
functions = [v for k, v in attrs.items() if k.startswith('test')]
ret.functions = functions
ret.multi_timer = ret.MULTI_TIMER(functions, ret.TIMER)
return ret


class TimerNamespace(metaclass=TimerNamespaceMeta):
"""Convenience class to ease creation of a MultiTimer."""
TIMER = timeit.Timer
MULTI_TIMER = MultiTimer


plotter.py



from .graph import MatPlotLib


class Plotter:
"""Interface to the timer object. Returns objects made to ease usage."""
def __init__(self, timer):
self.timer = getattr(timer, 'multi_timer', timer)

def timeit(self, number, domain, *args, **kwargs):
"""Interface to self.timer.timeit. Returns a PlotValues."""
return self.repeat(1, number, domain, *args, **kwargs).min(errors=None)

def repeat(self, repeat, number, domain, *args, **kwargs):
"""Interface to self.timer.repeat. Returns a PlotTimings."""
return PlotTimings(
self.timer.repeat(domain, repeat, number, *args, **kwargs),
{
'functions': self.timer.functions,
'domain': domain
}
)


class _DataSet:
"""Holds timeit values and defines statistical methods around them."""
def __init__(self, values):
self.values = sorted(values)

def quartile_indexes(self, outlier):
"""Generates the quartile indexes. Uses tukey's fences to remove outliers."""
delta = (len(self.values) - 1) / 4
quartiles = [int(round(delta * i)) for i in range(5)]
if outlier is not None:
if outlier < 0:
raise ValueError("outlier should be non-negative.")
iqr = outlier * (self.values[quartiles[3]] - self.values[quartiles[1]])
low = self.values[quartiles[1]] - iqr
high = self.values[quartiles[3]] + iqr

for i, v in enumerate(self.values):
if v >= low:
quartiles[0] = i
break

for i, v in reversed(list(enumerate(self.values))):
if v <= high:
quartiles[4] = i
break
return tuple(quartiles)

def errors(self, errors, outlier):
"""Returns tuples containing the quartiles wanted."""
if errors is None:
return None
quartiles = self.quartile_indexes(outlier)
# Allow out of quartile error bars using -1 and 5.
quartiles += (-1, 0)
return [
(
self.values[quartiles[start]],
self.values[quartiles[stop]]
)
for start, stop in errors
]

def quartile(self, quartile, outlier):
"""Return the value of the quartile provided."""
quartiles = self.quartile_indexes(outlier)
return self.values[quartiles[quartile]]

def mean(self, start, end, outlier):
"""Return the mean of the values over the quartiles specified."""
quartiles = self.quartile_indexes(outlier)
start = quartiles[start]
end = quartiles[end]
return sum(self.values[start:end + 1]) / (1 + end - start)


class PlotTimings:
"""Thin interface over _DataSet"""
def __init__(self, data, kwargs):
self.data = [
[_DataSet(results) for results in function_values]
for function_values in data
]
self.kwargs = kwargs

def quartile(self, quartile, *, errors=None, outlier=1.5):
"""Interface to _DataSet.quartile and errors. Returns a PlotValues."""
return PlotValues(
[
[
_DataValues(
ds.quartile(quartile, outlier),
ds.errors(errors, outlier)
)
for ds in function_values
]
for function_values in self.data
],
self.kwargs
)

def min(self, *, errors=((-1, 3),), outlier=1.5):
"""Return the Q1 value and show the error from Q-1 Q3."""
return self.quartile(0, errors=errors, outlier=outlier)

def max(self, *, errors=((1, 5),), outlier=1.5):
"""Return the Q4 value and show the error from Q1 Q5."""
return self.quartile(4, errors=errors, outlier=outlier)

def mean(self, start=0, end=4, *, errors=((1, 3),), outlier=1.5):
"""Interface to _DataSet.mean and errors. Returns a PlotValues."""
return PlotValues(
[
[
_DataValues(
ds.mean(start, end, outlier),
ds.errors(errors, outlier)
)
for ds in function_values
]
for function_values in self.data
],
self.kwargs
)


class _DataValues:
"""Holds the wanted statistical data from the timings."""
def __init__(self, value, errors):
self.value = value
self.errors = errors


class PlotValues:
"""Thin interface to Graph.graph."""
def __init__(self, data, kwargs):
self.data = data
self.kwargs = kwargs

def plot(self, graph, graph_lib=MatPlotLib, **kwargs):
g = graph_lib()
return g.graph(
graph,
self.data,
self.kwargs.pop('domain'),
functions=self.kwargs.pop('functions'),
**kwargs
)


Example usage



I've included the same graph as I did on my old code. And two of Graipher's graphs.




  1. Abstract graphing-and-timing functions - This is to ensure usage is simple in abnormal usage.

    It also shows that you can plot multiple error areas, highlighted in the unoptimised graph.


  2. Plot timings for a range of inputs - This is to make sure standard usage is simple.



  3. String reversal in Python - This is so I know logerithmic graphs display correctly.



    I'm running on Windows and don't have a C compiler, and so I can't include the additional two functions. However I think it nicely shows why the Python docs say to use min.




import time
import math

import matplotlib.pyplot as plt
import numpy as np

from graphtimer import flat, Plotter, TimerNamespace


class UnoptimisedRange(object):
def __init__(self, size):
self.size = size

def __getitem__(self, i):
if i >= self.size:
raise IndexError()
return i


class Peilonrayz(TimerNamespace):
def test_comprehension(iterable):
return [i for i in iterable]

def test_append(iterable):
a =
append = a.append
for i in iterable:
append(i)
return a


SCALE = 10.


class Graipher(TimerNamespace):
def test_o_n(n):
time.sleep(n / SCALE)

def test_o_n2(n):
time.sleep(n ** 2 / SCALE)

def test_o_log(n):
time.sleep(math.log(n + 1) / SCALE)

def test_o_exp(n):
time.sleep((math.exp(n) - 1) / SCALE)

def test_o_nlog(n):
time.sleep(n * math.log(n + 1) / SCALE)


class Reverse(TimerNamespace):
def test_orig(stri):
output = ''
length = len(stri)
while length > 0:
output += stri[-1]
stri, length = (stri[0:length - 1], length - 1)
return output

def test_g(s):
return s[::-1]

def test_s(s):
return ''.join(reversed(s))


def main():
# Reverse
fig, axs = plt.subplots()
axs.set_yscale('log')
axs.set_xscale('log')
(
Plotter(Reverse)
.repeat(10, 10, np.logspace(0, 5), args_conv=lambda i: ' '*int(i))
.min()
.plot(axs, title='Reverse', fmt='-o')
)
fig.show()

# Graipher
fig, axs = plt.subplots()
(
Plotter(Graipher)
.repeat(2, 1, [i / 10 for i in range(10)])
.min()
.plot(axs, title='Graipher', fmt='-o')
)
fig.show()

# Peilonrayz
fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
p = Plotter(Peilonrayz)
axis = [
('Range', {'args_conv': range}),
('List', {'args_conv': lambda i: list(range(i))}),
('Unoptimised', {'args_conv': UnoptimisedRange}),
]
for graph, (title, kwargs) in zip(iter(flat(axs)), axis):
(
p.repeat(100, 5, list(range(0, 10001, 1000)), **kwargs)
.min(errors=((-1, 3), (-1, 4)))
.plot(graph, title=title)
)
fig.show()


if __name__ == '__main__':
main()


My graphsGraipher's graphsReverse graphs










share|improve this question









$endgroup$

















    3












    $begingroup$


    GitHub repo (MIT)

    Clone the repo and replace the contents of example.py with the one at the bottom and you'll have everything setup.



    Explanation of the code



    A long while ago I posted the question "Abstract graphing-and-timing functions".
    It was a god class, wasn't extendable/configurable, you also had to use strings to interact with timeit.
    And it just wasn't clear what was what.



    A little while ago I posted an answer where I used a couple ikky timeit.timeit calls rather than build a graph. And Graipher showed me a nice looking graph. I've continued to see these nice graphs, with no easy way to make them myself. And so I decided I needed to way-too-early-spring clean my code.





    1. (timer.py) A Timer object should be in charge of building and calling timeit.

      This object was based of timeit.Timer, however I thought it needed some additional functionality:




      1. It should be able to time multiple functions.

      2. It should be able to test against multiple values.

      3. It should only perform timings.



    2. (graph.py) A graph class should be introduced that plots the data in the correct way. If you want to plot in something that isn't matplotlib then you just need to change the class.


    3. (plotter.py) Statistical analysis should not be in either the Timer or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion.

    4. (plotter.py) Performing all the above should be simple with good defaults. And so Plotter handles interacting with everything. Whilst it's a small wrapper it means that I, and other, don't have to write that many lines to get the wanted graph.


    What I'd like out of a review.





    1. The thing I find most important right now is, is the design good?



      Is Plotter a poor design pattern? Is splitting the code out like I have done a bad idea?



    2. Is the usage for a user clean and readable?


    3. Is there a way to make my code more readable? I find MatPlotLib to be a bit on the not so clean side.

    4. Any and all critiques are welcome.


    Code



    (I have left out some code, and the .pyi files. These are available on GitHub.)



    graph.py



    from .graphtimer import CATEGORY10


    class MatPlotLib:
    def _graph_times(self, graph, data, domain, colors, error, fmt):
    for results, color in zip(data, colors):
    values = [v.value for v in results]
    if error:
    errors = zip(*[v.errors or for v in results])
    for error in errors:
    lower, upper = zip(*error)
    graph.fill_between(domain, upper, lower, facecolor=color, edgecolor=None, alpha=0.1)
    yield graph.plot(domain, values, fmt, color=color)[0]

    def graph(self, graph, data, domain, *, functions=None, colors=CATEGORY10, title=None, legend=True, error=True,
    x_label='Input', y_label='Time [s]', fmt='-'):
    lines = list(self._graph_times(graph, data, domain, colors, error, fmt))
    if x_label is not None:
    graph.set_xlabel(x_label)
    if y_label is not None:
    graph.set_ylabel(y_label)
    if title is not None and hasattr(graph, 'set_title'):
    graph.set_title(title)
    if legend and functions is not None and hasattr(graph, 'legend'):
    graph.legend(lines, [fn.__name__ for fn in functions], loc=0)
    return lines


    timers.py



    import timeit

    SENTINAL = object()


    class MultiTimer:
    """Interface to timeit.Timer to ease timing over multiple functions."""
    def __init__(self, functions, timer=timeit.Timer):
    self.timer = timer
    self.functions = functions

    def build_timer(self, fn, domain, stmt='fn(*args)', setup='pass', timer=SENTINAL, globals=SENTINAL,
    args_conv=SENTINAL):
    """Build a timeit.Timer"""
    if not isinstance(domain, tuple):
    domain = domain,
    if args_conv is not SENTINAL:
    domain = args_conv(*domain)
    if not isinstance(domain, tuple):
    domain = domain,

    if globals is SENTINAL:
    globals = {}
    else:
    globals = globals.copy()
    globals.update({'fn': fn, 'args': domain})

    # print(f'{self.timer}({stmt!r}, {setup!r}, {timer!r}, {globals!r})')

    if timer is SENTINAL:
    timer = timeit.default_timer

    return self.timer(stmt, setup, timer, globals=globals)

    def build_timers(self, domain, *args, **kwargs):
    """Build multiple timers from various inputs and functions"""
    return [
    [
    self.build_timer(fn, dom, *args, **kwargs)
    for fn in self.functions
    ]
    for dom in domain
    ]

    def _call(self, domain, repeat, call, *args, **kwargs):
    """Helper function to generate timing data."""
    if len(domain) == 0:
    raise ValueError('domain must have at least one argument.')

    functions = self.build_timers(domain, *args, **kwargs)
    output = [[ for _ in domain] for _ in functions[0]]
    for _ in range(repeat):
    for j, fns in enumerate(functions):
    for i, fn in enumerate(fns):
    output[i][j].append(call(fn))
    return output

    def repeat(self, domain, repeat, number, *args, **kwargs):
    """Interface to timeit.Timer.repeat. `domain` is the values to pass to the functions."""
    return self._call(domain, repeat, lambda f: f.timeit(number), *args, **kwargs)

    def timeit(self, domain, number, *args, **kwargs):
    """Interface to timeit.Timer.timeit. `domain` is the values to pass to the functions."""
    return [
    [value[0] for value in values]
    for values in self.repeat(domain, 1, number, *args, **kwargs)
    ]

    def autorange(self, domain, *args, **kwargs):
    """Interface to timeit.Timer.autorange. `domain` is the values to pass to the functions."""
    return [
    [value[0] for value in values]
    for values in self._call(domain, 1, lambda f: f.autorange(), *args, **kwargs)
    ]


    class TimerNamespaceMeta(type):
    """Convenience class to ease creation of a MultiTimer."""
    def __new__(mcs, name, bases, attrs):
    if 'functions' in attrs:
    raise TypeError('FunctionTimers cannot define `functions`')
    if 'multi_timer' in attrs:
    raise TypeError('FunctionTimers cannot define `multi_timer`')

    ret: TimerNamespace = super().__new__(mcs, name, bases, attrs)
    functions = [v for k, v in attrs.items() if k.startswith('test')]
    ret.functions = functions
    ret.multi_timer = ret.MULTI_TIMER(functions, ret.TIMER)
    return ret


    class TimerNamespace(metaclass=TimerNamespaceMeta):
    """Convenience class to ease creation of a MultiTimer."""
    TIMER = timeit.Timer
    MULTI_TIMER = MultiTimer


    plotter.py



    from .graph import MatPlotLib


    class Plotter:
    """Interface to the timer object. Returns objects made to ease usage."""
    def __init__(self, timer):
    self.timer = getattr(timer, 'multi_timer', timer)

    def timeit(self, number, domain, *args, **kwargs):
    """Interface to self.timer.timeit. Returns a PlotValues."""
    return self.repeat(1, number, domain, *args, **kwargs).min(errors=None)

    def repeat(self, repeat, number, domain, *args, **kwargs):
    """Interface to self.timer.repeat. Returns a PlotTimings."""
    return PlotTimings(
    self.timer.repeat(domain, repeat, number, *args, **kwargs),
    {
    'functions': self.timer.functions,
    'domain': domain
    }
    )


    class _DataSet:
    """Holds timeit values and defines statistical methods around them."""
    def __init__(self, values):
    self.values = sorted(values)

    def quartile_indexes(self, outlier):
    """Generates the quartile indexes. Uses tukey's fences to remove outliers."""
    delta = (len(self.values) - 1) / 4
    quartiles = [int(round(delta * i)) for i in range(5)]
    if outlier is not None:
    if outlier < 0:
    raise ValueError("outlier should be non-negative.")
    iqr = outlier * (self.values[quartiles[3]] - self.values[quartiles[1]])
    low = self.values[quartiles[1]] - iqr
    high = self.values[quartiles[3]] + iqr

    for i, v in enumerate(self.values):
    if v >= low:
    quartiles[0] = i
    break

    for i, v in reversed(list(enumerate(self.values))):
    if v <= high:
    quartiles[4] = i
    break
    return tuple(quartiles)

    def errors(self, errors, outlier):
    """Returns tuples containing the quartiles wanted."""
    if errors is None:
    return None
    quartiles = self.quartile_indexes(outlier)
    # Allow out of quartile error bars using -1 and 5.
    quartiles += (-1, 0)
    return [
    (
    self.values[quartiles[start]],
    self.values[quartiles[stop]]
    )
    for start, stop in errors
    ]

    def quartile(self, quartile, outlier):
    """Return the value of the quartile provided."""
    quartiles = self.quartile_indexes(outlier)
    return self.values[quartiles[quartile]]

    def mean(self, start, end, outlier):
    """Return the mean of the values over the quartiles specified."""
    quartiles = self.quartile_indexes(outlier)
    start = quartiles[start]
    end = quartiles[end]
    return sum(self.values[start:end + 1]) / (1 + end - start)


    class PlotTimings:
    """Thin interface over _DataSet"""
    def __init__(self, data, kwargs):
    self.data = [
    [_DataSet(results) for results in function_values]
    for function_values in data
    ]
    self.kwargs = kwargs

    def quartile(self, quartile, *, errors=None, outlier=1.5):
    """Interface to _DataSet.quartile and errors. Returns a PlotValues."""
    return PlotValues(
    [
    [
    _DataValues(
    ds.quartile(quartile, outlier),
    ds.errors(errors, outlier)
    )
    for ds in function_values
    ]
    for function_values in self.data
    ],
    self.kwargs
    )

    def min(self, *, errors=((-1, 3),), outlier=1.5):
    """Return the Q1 value and show the error from Q-1 Q3."""
    return self.quartile(0, errors=errors, outlier=outlier)

    def max(self, *, errors=((1, 5),), outlier=1.5):
    """Return the Q4 value and show the error from Q1 Q5."""
    return self.quartile(4, errors=errors, outlier=outlier)

    def mean(self, start=0, end=4, *, errors=((1, 3),), outlier=1.5):
    """Interface to _DataSet.mean and errors. Returns a PlotValues."""
    return PlotValues(
    [
    [
    _DataValues(
    ds.mean(start, end, outlier),
    ds.errors(errors, outlier)
    )
    for ds in function_values
    ]
    for function_values in self.data
    ],
    self.kwargs
    )


    class _DataValues:
    """Holds the wanted statistical data from the timings."""
    def __init__(self, value, errors):
    self.value = value
    self.errors = errors


    class PlotValues:
    """Thin interface to Graph.graph."""
    def __init__(self, data, kwargs):
    self.data = data
    self.kwargs = kwargs

    def plot(self, graph, graph_lib=MatPlotLib, **kwargs):
    g = graph_lib()
    return g.graph(
    graph,
    self.data,
    self.kwargs.pop('domain'),
    functions=self.kwargs.pop('functions'),
    **kwargs
    )


    Example usage



    I've included the same graph as I did on my old code. And two of Graipher's graphs.




    1. Abstract graphing-and-timing functions - This is to ensure usage is simple in abnormal usage.

      It also shows that you can plot multiple error areas, highlighted in the unoptimised graph.


    2. Plot timings for a range of inputs - This is to make sure standard usage is simple.



    3. String reversal in Python - This is so I know logerithmic graphs display correctly.



      I'm running on Windows and don't have a C compiler, and so I can't include the additional two functions. However I think it nicely shows why the Python docs say to use min.




    import time
    import math

    import matplotlib.pyplot as plt
    import numpy as np

    from graphtimer import flat, Plotter, TimerNamespace


    class UnoptimisedRange(object):
    def __init__(self, size):
    self.size = size

    def __getitem__(self, i):
    if i >= self.size:
    raise IndexError()
    return i


    class Peilonrayz(TimerNamespace):
    def test_comprehension(iterable):
    return [i for i in iterable]

    def test_append(iterable):
    a =
    append = a.append
    for i in iterable:
    append(i)
    return a


    SCALE = 10.


    class Graipher(TimerNamespace):
    def test_o_n(n):
    time.sleep(n / SCALE)

    def test_o_n2(n):
    time.sleep(n ** 2 / SCALE)

    def test_o_log(n):
    time.sleep(math.log(n + 1) / SCALE)

    def test_o_exp(n):
    time.sleep((math.exp(n) - 1) / SCALE)

    def test_o_nlog(n):
    time.sleep(n * math.log(n + 1) / SCALE)


    class Reverse(TimerNamespace):
    def test_orig(stri):
    output = ''
    length = len(stri)
    while length > 0:
    output += stri[-1]
    stri, length = (stri[0:length - 1], length - 1)
    return output

    def test_g(s):
    return s[::-1]

    def test_s(s):
    return ''.join(reversed(s))


    def main():
    # Reverse
    fig, axs = plt.subplots()
    axs.set_yscale('log')
    axs.set_xscale('log')
    (
    Plotter(Reverse)
    .repeat(10, 10, np.logspace(0, 5), args_conv=lambda i: ' '*int(i))
    .min()
    .plot(axs, title='Reverse', fmt='-o')
    )
    fig.show()

    # Graipher
    fig, axs = plt.subplots()
    (
    Plotter(Graipher)
    .repeat(2, 1, [i / 10 for i in range(10)])
    .min()
    .plot(axs, title='Graipher', fmt='-o')
    )
    fig.show()

    # Peilonrayz
    fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
    p = Plotter(Peilonrayz)
    axis = [
    ('Range', {'args_conv': range}),
    ('List', {'args_conv': lambda i: list(range(i))}),
    ('Unoptimised', {'args_conv': UnoptimisedRange}),
    ]
    for graph, (title, kwargs) in zip(iter(flat(axs)), axis):
    (
    p.repeat(100, 5, list(range(0, 10001, 1000)), **kwargs)
    .min(errors=((-1, 3), (-1, 4)))
    .plot(graph, title=title)
    )
    fig.show()


    if __name__ == '__main__':
    main()


    My graphsGraipher's graphsReverse graphs










    share|improve this question









    $endgroup$















      3












      3








      3





      $begingroup$


      GitHub repo (MIT)

      Clone the repo and replace the contents of example.py with the one at the bottom and you'll have everything setup.



      Explanation of the code



      A long while ago I posted the question "Abstract graphing-and-timing functions".
      It was a god class, wasn't extendable/configurable, you also had to use strings to interact with timeit.
      And it just wasn't clear what was what.



      A little while ago I posted an answer where I used a couple ikky timeit.timeit calls rather than build a graph. And Graipher showed me a nice looking graph. I've continued to see these nice graphs, with no easy way to make them myself. And so I decided I needed to way-too-early-spring clean my code.





      1. (timer.py) A Timer object should be in charge of building and calling timeit.

        This object was based of timeit.Timer, however I thought it needed some additional functionality:




        1. It should be able to time multiple functions.

        2. It should be able to test against multiple values.

        3. It should only perform timings.



      2. (graph.py) A graph class should be introduced that plots the data in the correct way. If you want to plot in something that isn't matplotlib then you just need to change the class.


      3. (plotter.py) Statistical analysis should not be in either the Timer or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion.

      4. (plotter.py) Performing all the above should be simple with good defaults. And so Plotter handles interacting with everything. Whilst it's a small wrapper it means that I, and other, don't have to write that many lines to get the wanted graph.


      What I'd like out of a review.





      1. The thing I find most important right now is, is the design good?



        Is Plotter a poor design pattern? Is splitting the code out like I have done a bad idea?



      2. Is the usage for a user clean and readable?


      3. Is there a way to make my code more readable? I find MatPlotLib to be a bit on the not so clean side.

      4. Any and all critiques are welcome.


      Code



      (I have left out some code, and the .pyi files. These are available on GitHub.)



      graph.py



      from .graphtimer import CATEGORY10


      class MatPlotLib:
      def _graph_times(self, graph, data, domain, colors, error, fmt):
      for results, color in zip(data, colors):
      values = [v.value for v in results]
      if error:
      errors = zip(*[v.errors or for v in results])
      for error in errors:
      lower, upper = zip(*error)
      graph.fill_between(domain, upper, lower, facecolor=color, edgecolor=None, alpha=0.1)
      yield graph.plot(domain, values, fmt, color=color)[0]

      def graph(self, graph, data, domain, *, functions=None, colors=CATEGORY10, title=None, legend=True, error=True,
      x_label='Input', y_label='Time [s]', fmt='-'):
      lines = list(self._graph_times(graph, data, domain, colors, error, fmt))
      if x_label is not None:
      graph.set_xlabel(x_label)
      if y_label is not None:
      graph.set_ylabel(y_label)
      if title is not None and hasattr(graph, 'set_title'):
      graph.set_title(title)
      if legend and functions is not None and hasattr(graph, 'legend'):
      graph.legend(lines, [fn.__name__ for fn in functions], loc=0)
      return lines


      timers.py



      import timeit

      SENTINAL = object()


      class MultiTimer:
      """Interface to timeit.Timer to ease timing over multiple functions."""
      def __init__(self, functions, timer=timeit.Timer):
      self.timer = timer
      self.functions = functions

      def build_timer(self, fn, domain, stmt='fn(*args)', setup='pass', timer=SENTINAL, globals=SENTINAL,
      args_conv=SENTINAL):
      """Build a timeit.Timer"""
      if not isinstance(domain, tuple):
      domain = domain,
      if args_conv is not SENTINAL:
      domain = args_conv(*domain)
      if not isinstance(domain, tuple):
      domain = domain,

      if globals is SENTINAL:
      globals = {}
      else:
      globals = globals.copy()
      globals.update({'fn': fn, 'args': domain})

      # print(f'{self.timer}({stmt!r}, {setup!r}, {timer!r}, {globals!r})')

      if timer is SENTINAL:
      timer = timeit.default_timer

      return self.timer(stmt, setup, timer, globals=globals)

      def build_timers(self, domain, *args, **kwargs):
      """Build multiple timers from various inputs and functions"""
      return [
      [
      self.build_timer(fn, dom, *args, **kwargs)
      for fn in self.functions
      ]
      for dom in domain
      ]

      def _call(self, domain, repeat, call, *args, **kwargs):
      """Helper function to generate timing data."""
      if len(domain) == 0:
      raise ValueError('domain must have at least one argument.')

      functions = self.build_timers(domain, *args, **kwargs)
      output = [[ for _ in domain] for _ in functions[0]]
      for _ in range(repeat):
      for j, fns in enumerate(functions):
      for i, fn in enumerate(fns):
      output[i][j].append(call(fn))
      return output

      def repeat(self, domain, repeat, number, *args, **kwargs):
      """Interface to timeit.Timer.repeat. `domain` is the values to pass to the functions."""
      return self._call(domain, repeat, lambda f: f.timeit(number), *args, **kwargs)

      def timeit(self, domain, number, *args, **kwargs):
      """Interface to timeit.Timer.timeit. `domain` is the values to pass to the functions."""
      return [
      [value[0] for value in values]
      for values in self.repeat(domain, 1, number, *args, **kwargs)
      ]

      def autorange(self, domain, *args, **kwargs):
      """Interface to timeit.Timer.autorange. `domain` is the values to pass to the functions."""
      return [
      [value[0] for value in values]
      for values in self._call(domain, 1, lambda f: f.autorange(), *args, **kwargs)
      ]


      class TimerNamespaceMeta(type):
      """Convenience class to ease creation of a MultiTimer."""
      def __new__(mcs, name, bases, attrs):
      if 'functions' in attrs:
      raise TypeError('FunctionTimers cannot define `functions`')
      if 'multi_timer' in attrs:
      raise TypeError('FunctionTimers cannot define `multi_timer`')

      ret: TimerNamespace = super().__new__(mcs, name, bases, attrs)
      functions = [v for k, v in attrs.items() if k.startswith('test')]
      ret.functions = functions
      ret.multi_timer = ret.MULTI_TIMER(functions, ret.TIMER)
      return ret


      class TimerNamespace(metaclass=TimerNamespaceMeta):
      """Convenience class to ease creation of a MultiTimer."""
      TIMER = timeit.Timer
      MULTI_TIMER = MultiTimer


      plotter.py



      from .graph import MatPlotLib


      class Plotter:
      """Interface to the timer object. Returns objects made to ease usage."""
      def __init__(self, timer):
      self.timer = getattr(timer, 'multi_timer', timer)

      def timeit(self, number, domain, *args, **kwargs):
      """Interface to self.timer.timeit. Returns a PlotValues."""
      return self.repeat(1, number, domain, *args, **kwargs).min(errors=None)

      def repeat(self, repeat, number, domain, *args, **kwargs):
      """Interface to self.timer.repeat. Returns a PlotTimings."""
      return PlotTimings(
      self.timer.repeat(domain, repeat, number, *args, **kwargs),
      {
      'functions': self.timer.functions,
      'domain': domain
      }
      )


      class _DataSet:
      """Holds timeit values and defines statistical methods around them."""
      def __init__(self, values):
      self.values = sorted(values)

      def quartile_indexes(self, outlier):
      """Generates the quartile indexes. Uses tukey's fences to remove outliers."""
      delta = (len(self.values) - 1) / 4
      quartiles = [int(round(delta * i)) for i in range(5)]
      if outlier is not None:
      if outlier < 0:
      raise ValueError("outlier should be non-negative.")
      iqr = outlier * (self.values[quartiles[3]] - self.values[quartiles[1]])
      low = self.values[quartiles[1]] - iqr
      high = self.values[quartiles[3]] + iqr

      for i, v in enumerate(self.values):
      if v >= low:
      quartiles[0] = i
      break

      for i, v in reversed(list(enumerate(self.values))):
      if v <= high:
      quartiles[4] = i
      break
      return tuple(quartiles)

      def errors(self, errors, outlier):
      """Returns tuples containing the quartiles wanted."""
      if errors is None:
      return None
      quartiles = self.quartile_indexes(outlier)
      # Allow out of quartile error bars using -1 and 5.
      quartiles += (-1, 0)
      return [
      (
      self.values[quartiles[start]],
      self.values[quartiles[stop]]
      )
      for start, stop in errors
      ]

      def quartile(self, quartile, outlier):
      """Return the value of the quartile provided."""
      quartiles = self.quartile_indexes(outlier)
      return self.values[quartiles[quartile]]

      def mean(self, start, end, outlier):
      """Return the mean of the values over the quartiles specified."""
      quartiles = self.quartile_indexes(outlier)
      start = quartiles[start]
      end = quartiles[end]
      return sum(self.values[start:end + 1]) / (1 + end - start)


      class PlotTimings:
      """Thin interface over _DataSet"""
      def __init__(self, data, kwargs):
      self.data = [
      [_DataSet(results) for results in function_values]
      for function_values in data
      ]
      self.kwargs = kwargs

      def quartile(self, quartile, *, errors=None, outlier=1.5):
      """Interface to _DataSet.quartile and errors. Returns a PlotValues."""
      return PlotValues(
      [
      [
      _DataValues(
      ds.quartile(quartile, outlier),
      ds.errors(errors, outlier)
      )
      for ds in function_values
      ]
      for function_values in self.data
      ],
      self.kwargs
      )

      def min(self, *, errors=((-1, 3),), outlier=1.5):
      """Return the Q1 value and show the error from Q-1 Q3."""
      return self.quartile(0, errors=errors, outlier=outlier)

      def max(self, *, errors=((1, 5),), outlier=1.5):
      """Return the Q4 value and show the error from Q1 Q5."""
      return self.quartile(4, errors=errors, outlier=outlier)

      def mean(self, start=0, end=4, *, errors=((1, 3),), outlier=1.5):
      """Interface to _DataSet.mean and errors. Returns a PlotValues."""
      return PlotValues(
      [
      [
      _DataValues(
      ds.mean(start, end, outlier),
      ds.errors(errors, outlier)
      )
      for ds in function_values
      ]
      for function_values in self.data
      ],
      self.kwargs
      )


      class _DataValues:
      """Holds the wanted statistical data from the timings."""
      def __init__(self, value, errors):
      self.value = value
      self.errors = errors


      class PlotValues:
      """Thin interface to Graph.graph."""
      def __init__(self, data, kwargs):
      self.data = data
      self.kwargs = kwargs

      def plot(self, graph, graph_lib=MatPlotLib, **kwargs):
      g = graph_lib()
      return g.graph(
      graph,
      self.data,
      self.kwargs.pop('domain'),
      functions=self.kwargs.pop('functions'),
      **kwargs
      )


      Example usage



      I've included the same graph as I did on my old code. And two of Graipher's graphs.




      1. Abstract graphing-and-timing functions - This is to ensure usage is simple in abnormal usage.

        It also shows that you can plot multiple error areas, highlighted in the unoptimised graph.


      2. Plot timings for a range of inputs - This is to make sure standard usage is simple.



      3. String reversal in Python - This is so I know logerithmic graphs display correctly.



        I'm running on Windows and don't have a C compiler, and so I can't include the additional two functions. However I think it nicely shows why the Python docs say to use min.




      import time
      import math

      import matplotlib.pyplot as plt
      import numpy as np

      from graphtimer import flat, Plotter, TimerNamespace


      class UnoptimisedRange(object):
      def __init__(self, size):
      self.size = size

      def __getitem__(self, i):
      if i >= self.size:
      raise IndexError()
      return i


      class Peilonrayz(TimerNamespace):
      def test_comprehension(iterable):
      return [i for i in iterable]

      def test_append(iterable):
      a =
      append = a.append
      for i in iterable:
      append(i)
      return a


      SCALE = 10.


      class Graipher(TimerNamespace):
      def test_o_n(n):
      time.sleep(n / SCALE)

      def test_o_n2(n):
      time.sleep(n ** 2 / SCALE)

      def test_o_log(n):
      time.sleep(math.log(n + 1) / SCALE)

      def test_o_exp(n):
      time.sleep((math.exp(n) - 1) / SCALE)

      def test_o_nlog(n):
      time.sleep(n * math.log(n + 1) / SCALE)


      class Reverse(TimerNamespace):
      def test_orig(stri):
      output = ''
      length = len(stri)
      while length > 0:
      output += stri[-1]
      stri, length = (stri[0:length - 1], length - 1)
      return output

      def test_g(s):
      return s[::-1]

      def test_s(s):
      return ''.join(reversed(s))


      def main():
      # Reverse
      fig, axs = plt.subplots()
      axs.set_yscale('log')
      axs.set_xscale('log')
      (
      Plotter(Reverse)
      .repeat(10, 10, np.logspace(0, 5), args_conv=lambda i: ' '*int(i))
      .min()
      .plot(axs, title='Reverse', fmt='-o')
      )
      fig.show()

      # Graipher
      fig, axs = plt.subplots()
      (
      Plotter(Graipher)
      .repeat(2, 1, [i / 10 for i in range(10)])
      .min()
      .plot(axs, title='Graipher', fmt='-o')
      )
      fig.show()

      # Peilonrayz
      fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
      p = Plotter(Peilonrayz)
      axis = [
      ('Range', {'args_conv': range}),
      ('List', {'args_conv': lambda i: list(range(i))}),
      ('Unoptimised', {'args_conv': UnoptimisedRange}),
      ]
      for graph, (title, kwargs) in zip(iter(flat(axs)), axis):
      (
      p.repeat(100, 5, list(range(0, 10001, 1000)), **kwargs)
      .min(errors=((-1, 3), (-1, 4)))
      .plot(graph, title=title)
      )
      fig.show()


      if __name__ == '__main__':
      main()


      My graphsGraipher's graphsReverse graphs










      share|improve this question









      $endgroup$




      GitHub repo (MIT)

      Clone the repo and replace the contents of example.py with the one at the bottom and you'll have everything setup.



      Explanation of the code



      A long while ago I posted the question "Abstract graphing-and-timing functions".
      It was a god class, wasn't extendable/configurable, you also had to use strings to interact with timeit.
      And it just wasn't clear what was what.



      A little while ago I posted an answer where I used a couple ikky timeit.timeit calls rather than build a graph. And Graipher showed me a nice looking graph. I've continued to see these nice graphs, with no easy way to make them myself. And so I decided I needed to way-too-early-spring clean my code.





      1. (timer.py) A Timer object should be in charge of building and calling timeit.

        This object was based of timeit.Timer, however I thought it needed some additional functionality:




        1. It should be able to time multiple functions.

        2. It should be able to test against multiple values.

        3. It should only perform timings.



      2. (graph.py) A graph class should be introduced that plots the data in the correct way. If you want to plot in something that isn't matplotlib then you just need to change the class.


      3. (plotter.py) Statistical analysis should not be in either the Timer or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion.

      4. (plotter.py) Performing all the above should be simple with good defaults. And so Plotter handles interacting with everything. Whilst it's a small wrapper it means that I, and other, don't have to write that many lines to get the wanted graph.


      What I'd like out of a review.





      1. The thing I find most important right now is, is the design good?



        Is Plotter a poor design pattern? Is splitting the code out like I have done a bad idea?



      2. Is the usage for a user clean and readable?


      3. Is there a way to make my code more readable? I find MatPlotLib to be a bit on the not so clean side.

      4. Any and all critiques are welcome.


      Code



      (I have left out some code, and the .pyi files. These are available on GitHub.)



      graph.py



      from .graphtimer import CATEGORY10


      class MatPlotLib:
      def _graph_times(self, graph, data, domain, colors, error, fmt):
      for results, color in zip(data, colors):
      values = [v.value for v in results]
      if error:
      errors = zip(*[v.errors or for v in results])
      for error in errors:
      lower, upper = zip(*error)
      graph.fill_between(domain, upper, lower, facecolor=color, edgecolor=None, alpha=0.1)
      yield graph.plot(domain, values, fmt, color=color)[0]

      def graph(self, graph, data, domain, *, functions=None, colors=CATEGORY10, title=None, legend=True, error=True,
      x_label='Input', y_label='Time [s]', fmt='-'):
      lines = list(self._graph_times(graph, data, domain, colors, error, fmt))
      if x_label is not None:
      graph.set_xlabel(x_label)
      if y_label is not None:
      graph.set_ylabel(y_label)
      if title is not None and hasattr(graph, 'set_title'):
      graph.set_title(title)
      if legend and functions is not None and hasattr(graph, 'legend'):
      graph.legend(lines, [fn.__name__ for fn in functions], loc=0)
      return lines


      timers.py



      import timeit

      SENTINAL = object()


      class MultiTimer:
      """Interface to timeit.Timer to ease timing over multiple functions."""
      def __init__(self, functions, timer=timeit.Timer):
      self.timer = timer
      self.functions = functions

      def build_timer(self, fn, domain, stmt='fn(*args)', setup='pass', timer=SENTINAL, globals=SENTINAL,
      args_conv=SENTINAL):
      """Build a timeit.Timer"""
      if not isinstance(domain, tuple):
      domain = domain,
      if args_conv is not SENTINAL:
      domain = args_conv(*domain)
      if not isinstance(domain, tuple):
      domain = domain,

      if globals is SENTINAL:
      globals = {}
      else:
      globals = globals.copy()
      globals.update({'fn': fn, 'args': domain})

      # print(f'{self.timer}({stmt!r}, {setup!r}, {timer!r}, {globals!r})')

      if timer is SENTINAL:
      timer = timeit.default_timer

      return self.timer(stmt, setup, timer, globals=globals)

      def build_timers(self, domain, *args, **kwargs):
      """Build multiple timers from various inputs and functions"""
      return [
      [
      self.build_timer(fn, dom, *args, **kwargs)
      for fn in self.functions
      ]
      for dom in domain
      ]

      def _call(self, domain, repeat, call, *args, **kwargs):
      """Helper function to generate timing data."""
      if len(domain) == 0:
      raise ValueError('domain must have at least one argument.')

      functions = self.build_timers(domain, *args, **kwargs)
      output = [[ for _ in domain] for _ in functions[0]]
      for _ in range(repeat):
      for j, fns in enumerate(functions):
      for i, fn in enumerate(fns):
      output[i][j].append(call(fn))
      return output

      def repeat(self, domain, repeat, number, *args, **kwargs):
      """Interface to timeit.Timer.repeat. `domain` is the values to pass to the functions."""
      return self._call(domain, repeat, lambda f: f.timeit(number), *args, **kwargs)

      def timeit(self, domain, number, *args, **kwargs):
      """Interface to timeit.Timer.timeit. `domain` is the values to pass to the functions."""
      return [
      [value[0] for value in values]
      for values in self.repeat(domain, 1, number, *args, **kwargs)
      ]

      def autorange(self, domain, *args, **kwargs):
      """Interface to timeit.Timer.autorange. `domain` is the values to pass to the functions."""
      return [
      [value[0] for value in values]
      for values in self._call(domain, 1, lambda f: f.autorange(), *args, **kwargs)
      ]


      class TimerNamespaceMeta(type):
      """Convenience class to ease creation of a MultiTimer."""
      def __new__(mcs, name, bases, attrs):
      if 'functions' in attrs:
      raise TypeError('FunctionTimers cannot define `functions`')
      if 'multi_timer' in attrs:
      raise TypeError('FunctionTimers cannot define `multi_timer`')

      ret: TimerNamespace = super().__new__(mcs, name, bases, attrs)
      functions = [v for k, v in attrs.items() if k.startswith('test')]
      ret.functions = functions
      ret.multi_timer = ret.MULTI_TIMER(functions, ret.TIMER)
      return ret


      class TimerNamespace(metaclass=TimerNamespaceMeta):
      """Convenience class to ease creation of a MultiTimer."""
      TIMER = timeit.Timer
      MULTI_TIMER = MultiTimer


      plotter.py



      from .graph import MatPlotLib


      class Plotter:
      """Interface to the timer object. Returns objects made to ease usage."""
      def __init__(self, timer):
      self.timer = getattr(timer, 'multi_timer', timer)

      def timeit(self, number, domain, *args, **kwargs):
      """Interface to self.timer.timeit. Returns a PlotValues."""
      return self.repeat(1, number, domain, *args, **kwargs).min(errors=None)

      def repeat(self, repeat, number, domain, *args, **kwargs):
      """Interface to self.timer.repeat. Returns a PlotTimings."""
      return PlotTimings(
      self.timer.repeat(domain, repeat, number, *args, **kwargs),
      {
      'functions': self.timer.functions,
      'domain': domain
      }
      )


      class _DataSet:
      """Holds timeit values and defines statistical methods around them."""
      def __init__(self, values):
      self.values = sorted(values)

      def quartile_indexes(self, outlier):
      """Generates the quartile indexes. Uses tukey's fences to remove outliers."""
      delta = (len(self.values) - 1) / 4
      quartiles = [int(round(delta * i)) for i in range(5)]
      if outlier is not None:
      if outlier < 0:
      raise ValueError("outlier should be non-negative.")
      iqr = outlier * (self.values[quartiles[3]] - self.values[quartiles[1]])
      low = self.values[quartiles[1]] - iqr
      high = self.values[quartiles[3]] + iqr

      for i, v in enumerate(self.values):
      if v >= low:
      quartiles[0] = i
      break

      for i, v in reversed(list(enumerate(self.values))):
      if v <= high:
      quartiles[4] = i
      break
      return tuple(quartiles)

      def errors(self, errors, outlier):
      """Returns tuples containing the quartiles wanted."""
      if errors is None:
      return None
      quartiles = self.quartile_indexes(outlier)
      # Allow out of quartile error bars using -1 and 5.
      quartiles += (-1, 0)
      return [
      (
      self.values[quartiles[start]],
      self.values[quartiles[stop]]
      )
      for start, stop in errors
      ]

      def quartile(self, quartile, outlier):
      """Return the value of the quartile provided."""
      quartiles = self.quartile_indexes(outlier)
      return self.values[quartiles[quartile]]

      def mean(self, start, end, outlier):
      """Return the mean of the values over the quartiles specified."""
      quartiles = self.quartile_indexes(outlier)
      start = quartiles[start]
      end = quartiles[end]
      return sum(self.values[start:end + 1]) / (1 + end - start)


      class PlotTimings:
      """Thin interface over _DataSet"""
      def __init__(self, data, kwargs):
      self.data = [
      [_DataSet(results) for results in function_values]
      for function_values in data
      ]
      self.kwargs = kwargs

      def quartile(self, quartile, *, errors=None, outlier=1.5):
      """Interface to _DataSet.quartile and errors. Returns a PlotValues."""
      return PlotValues(
      [
      [
      _DataValues(
      ds.quartile(quartile, outlier),
      ds.errors(errors, outlier)
      )
      for ds in function_values
      ]
      for function_values in self.data
      ],
      self.kwargs
      )

      def min(self, *, errors=((-1, 3),), outlier=1.5):
      """Return the Q1 value and show the error from Q-1 Q3."""
      return self.quartile(0, errors=errors, outlier=outlier)

      def max(self, *, errors=((1, 5),), outlier=1.5):
      """Return the Q4 value and show the error from Q1 Q5."""
      return self.quartile(4, errors=errors, outlier=outlier)

      def mean(self, start=0, end=4, *, errors=((1, 3),), outlier=1.5):
      """Interface to _DataSet.mean and errors. Returns a PlotValues."""
      return PlotValues(
      [
      [
      _DataValues(
      ds.mean(start, end, outlier),
      ds.errors(errors, outlier)
      )
      for ds in function_values
      ]
      for function_values in self.data
      ],
      self.kwargs
      )


      class _DataValues:
      """Holds the wanted statistical data from the timings."""
      def __init__(self, value, errors):
      self.value = value
      self.errors = errors


      class PlotValues:
      """Thin interface to Graph.graph."""
      def __init__(self, data, kwargs):
      self.data = data
      self.kwargs = kwargs

      def plot(self, graph, graph_lib=MatPlotLib, **kwargs):
      g = graph_lib()
      return g.graph(
      graph,
      self.data,
      self.kwargs.pop('domain'),
      functions=self.kwargs.pop('functions'),
      **kwargs
      )


      Example usage



      I've included the same graph as I did on my old code. And two of Graipher's graphs.




      1. Abstract graphing-and-timing functions - This is to ensure usage is simple in abnormal usage.

        It also shows that you can plot multiple error areas, highlighted in the unoptimised graph.


      2. Plot timings for a range of inputs - This is to make sure standard usage is simple.



      3. String reversal in Python - This is so I know logerithmic graphs display correctly.



        I'm running on Windows and don't have a C compiler, and so I can't include the additional two functions. However I think it nicely shows why the Python docs say to use min.




      import time
      import math

      import matplotlib.pyplot as plt
      import numpy as np

      from graphtimer import flat, Plotter, TimerNamespace


      class UnoptimisedRange(object):
      def __init__(self, size):
      self.size = size

      def __getitem__(self, i):
      if i >= self.size:
      raise IndexError()
      return i


      class Peilonrayz(TimerNamespace):
      def test_comprehension(iterable):
      return [i for i in iterable]

      def test_append(iterable):
      a =
      append = a.append
      for i in iterable:
      append(i)
      return a


      SCALE = 10.


      class Graipher(TimerNamespace):
      def test_o_n(n):
      time.sleep(n / SCALE)

      def test_o_n2(n):
      time.sleep(n ** 2 / SCALE)

      def test_o_log(n):
      time.sleep(math.log(n + 1) / SCALE)

      def test_o_exp(n):
      time.sleep((math.exp(n) - 1) / SCALE)

      def test_o_nlog(n):
      time.sleep(n * math.log(n + 1) / SCALE)


      class Reverse(TimerNamespace):
      def test_orig(stri):
      output = ''
      length = len(stri)
      while length > 0:
      output += stri[-1]
      stri, length = (stri[0:length - 1], length - 1)
      return output

      def test_g(s):
      return s[::-1]

      def test_s(s):
      return ''.join(reversed(s))


      def main():
      # Reverse
      fig, axs = plt.subplots()
      axs.set_yscale('log')
      axs.set_xscale('log')
      (
      Plotter(Reverse)
      .repeat(10, 10, np.logspace(0, 5), args_conv=lambda i: ' '*int(i))
      .min()
      .plot(axs, title='Reverse', fmt='-o')
      )
      fig.show()

      # Graipher
      fig, axs = plt.subplots()
      (
      Plotter(Graipher)
      .repeat(2, 1, [i / 10 for i in range(10)])
      .min()
      .plot(axs, title='Graipher', fmt='-o')
      )
      fig.show()

      # Peilonrayz
      fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
      p = Plotter(Peilonrayz)
      axis = [
      ('Range', {'args_conv': range}),
      ('List', {'args_conv': lambda i: list(range(i))}),
      ('Unoptimised', {'args_conv': UnoptimisedRange}),
      ]
      for graph, (title, kwargs) in zip(iter(flat(axs)), axis):
      (
      p.repeat(100, 5, list(range(0, 10001, 1000)), **kwargs)
      .min(errors=((-1, 3), (-1, 4)))
      .plot(graph, title=title)
      )
      fig.show()


      if __name__ == '__main__':
      main()


      My graphsGraipher's graphsReverse graphs







      python python-3.x graph matplotlib






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked 2 hours ago









      PeilonrayzPeilonrayz

      26k338110




      26k338110






















          0






          active

          oldest

          votes











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f215846%2fgraphtimer-v2-utility-to-plot-timings%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          0






          active

          oldest

          votes








          0






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes
















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f215846%2fgraphtimer-v2-utility-to-plot-timings%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          How to reconfigure Docker Trusted Registry 2.x.x to use CEPH FS mount instead of NFS and other traditional...

          is 'sed' thread safe

          How to make a Squid Proxy server?