graphtimer v2 - Utility to plot timings
$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.
(
timer.py
) ATimer
object should be in charge of building and callingtimeit
.
This object was based oftimeit.Timer
, however I thought it needed some additional functionality:
- It should be able to time multiple functions.
- It should be able to test against multiple values.
- It should only perform timings.
(
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'tmatplotlib
then you just need to change the class.- (
plotter.py
) Statistical analysis should not be in either theTimer
or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion. - (
plotter.py
) Performing all the above should be simple with good defaults. And soPlotter
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.
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?
Is the usage for a user clean and readable?
- Is there a way to make my code more readable? I find
MatPlotLib
to be a bit on the not so clean side. - 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.
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.Plot timings for a range of inputs - This is to make sure standard usage is simple.
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()
python python-3.x graph matplotlib
$endgroup$
add a comment |
$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.
(
timer.py
) ATimer
object should be in charge of building and callingtimeit
.
This object was based oftimeit.Timer
, however I thought it needed some additional functionality:
- It should be able to time multiple functions.
- It should be able to test against multiple values.
- It should only perform timings.
(
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'tmatplotlib
then you just need to change the class.- (
plotter.py
) Statistical analysis should not be in either theTimer
or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion. - (
plotter.py
) Performing all the above should be simple with good defaults. And soPlotter
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.
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?
Is the usage for a user clean and readable?
- Is there a way to make my code more readable? I find
MatPlotLib
to be a bit on the not so clean side. - 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.
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.Plot timings for a range of inputs - This is to make sure standard usage is simple.
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()
python python-3.x graph matplotlib
$endgroup$
add a comment |
$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.
(
timer.py
) ATimer
object should be in charge of building and callingtimeit
.
This object was based oftimeit.Timer
, however I thought it needed some additional functionality:
- It should be able to time multiple functions.
- It should be able to test against multiple values.
- It should only perform timings.
(
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'tmatplotlib
then you just need to change the class.- (
plotter.py
) Statistical analysis should not be in either theTimer
or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion. - (
plotter.py
) Performing all the above should be simple with good defaults. And soPlotter
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.
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?
Is the usage for a user clean and readable?
- Is there a way to make my code more readable? I find
MatPlotLib
to be a bit on the not so clean side. - 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.
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.Plot timings for a range of inputs - This is to make sure standard usage is simple.
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()
python python-3.x graph matplotlib
$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.
(
timer.py
) ATimer
object should be in charge of building and callingtimeit
.
This object was based oftimeit.Timer
, however I thought it needed some additional functionality:
- It should be able to time multiple functions.
- It should be able to test against multiple values.
- It should only perform timings.
(
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'tmatplotlib
then you just need to change the class.- (
plotter.py
) Statistical analysis should not be in either theTimer
or the graph class. I also kept the analysis to be pretty basic as per the Python docs suggestion. - (
plotter.py
) Performing all the above should be simple with good defaults. And soPlotter
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.
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?
Is the usage for a user clean and readable?
- Is there a way to make my code more readable? I find
MatPlotLib
to be a bit on the not so clean side. - 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.
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.Plot timings for a range of inputs - This is to make sure standard usage is simple.
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()
python python-3.x graph matplotlib
python python-3.x graph matplotlib
asked 2 hours ago
PeilonrayzPeilonrayz
26k338110
26k338110
add a comment |
add a comment |
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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