-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathtimeit_plot.py
206 lines (180 loc) · 7.76 KB
/
timeit_plot.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import timeit
from collections import defaultdict
from itertools import product
import heapq
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.ticker import LinearLocator, FormatStrFormatter
def timeit_compare(funcs, inputs, setups='pass', **kwargs):
"""Compares speed of functions across input conditions.
'funcs' should be a list of functions expressed as strings.
String substitution is done on each function while iterating
over ranges of values in 'inputs' to compare speed.
'inputs' should be an iterable range of values over which 'funcs'
should be tested.
'setups' can be 'pass' for no setup, 'main' to import each function
from the local environment, or a list of setup strings that maps
to the list 'funcs'.
Other inputs:
'number' - the number of times to run the function (a timeit option)
'print_conditions' - if True, print the combinations of input conditions
>>> functions = ['"-".join(str(n) for n in range({0}))',
'"-".join([str(n) for n in range({0})])',
'"-".join(map(str, range({0})))']
>>> data = timeit_compare(functions, [range(10,101,10)], number=10000)
testing "-".join(str(n) for n in range({0}))...
testing "-".join([str(n) for n in range({0})])...
testing "-".join(map(str, range({0})))...
Returns a defaultdict that has function names as keys, results as values.
"""
number = kwargs.get('number', 100000)
print_conditions = kwargs.get('print_conditions', False)
performance = defaultdict(list)
if isinstance(setups, list):
# user specifies their own list of setups corresponding to funcs
pass
elif setups == 'pass':
# specify no setups for built-in functions like join
setups = ['pass' for f in funcs]
elif setups == 'main':
# uniformly import all setups from the local environment
fnames = [f[:f.find("(")] for f in funcs]
setups = ["from __main__ import " + fname for fname in fnames]
# convert the input ranges to a set of conditions
conditions = get_conditions(inputs)
if print_conditions:
print "conditions: " + conditions
def timer(func, value, setup):
return timeit.Timer(func.format(*value), setup=setup)
for i, f in enumerate(funcs):
print "testing " + f + "..."
for value in conditions:
test = timer(f, value, setups[i])
result = test.timeit(number=number)
performance[f].append(list(value) + [result])
return performance
def get_conditions(inputs):
"""Converts conditions for individual variables into an
exhaustive list of combinations for timeit_compare().
"""
# itertools.product summarizes all combinations of ordered conditions
# at len = 1 it wraps values in tuples (0,) that confuse the timer below
if hasattr(inputs[0], '__iter__'):
return list(product(*inputs))
else:
return [[n] if not isinstance(n,(list,tuple)) else n for n in inputs]
# TODO: function to filter a large dataset
# TODO: filter and options to plot 2 and 3 grams in different colors
# on the same chart
def timeit_plot2D(data, xlabel='xlabel', title='title', **kwargs):
"""Plots the results from a defaultdict returned by timeit_compare.
Each function will be plotted as a different series.
timeit_compare may test many conditions, and the order of the conditions
in the results data can be understood from the string substitutions
noted in the keys of the defaultdict. By default series=0 means
that the first series is plotted, but this can be changed to plot
any of the testing conditions available.
"""
series = kwargs.get('series', 0)
style = kwargs.get('style', 'line')
size = kwargs.get('size', 500)
ylabel = kwargs.get('ylabel', 'time')
cmap = kwargs.get('cmap', 'rainbow')
lloc = kwargs.get('lloc', 2)
dataT = {}
# set color scheme
c = iter(plt.get_cmap(cmap)(np.linspace(0, 1, len(data))))
# transpose the data from [x, y, z]... into ([x...], [y...], [z...])
for k, v in data.items():
dataT[k] = zip(*v)
fig, ax = plt.subplots()
for k, v in dataT.items():
if style == 'scatter':
ax.scatter(v[series], v[-1], s=size, c=next(c), alpha=.75)
elif style == 'bubble':
x, y, z = v[series[0]], v[series[1]], v[-1]
ax.scatter(x, y, s=[size*i for i in z], c=next(c), alpha=.5)
else:
ax.plot(v[series], v[-1], c=next(c), lw=2)
# TODO: BUG: no way to set other parameters manually (README fig2)
ax.legend([substitute_titles(k,series) for k in dataT.keys()], loc=lloc)
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_title(title)
ax.grid(True)
return fig
def timeit_plot3D(data, xlabel='xlabel', ylabel='ylabel', **kwargs):
"""3D plot of timeit data, one chart per function.
"""
dataT = {}
figs = []
series = kwargs.get('series', (0,1))
cmap = kwargs.get('cmap', cm.coolwarm)
for k, v in data.items():
dataT[k] = zip(*v)
fig = plt.figure()
ax = fig.gca(projection='3d')
X, Y, Z = dataT[k][series[0]], dataT[k][series[1]], dataT[k][-1]
wide, tall = (max(X)-min(X)+1), (max(Y)-min(Y)+1)
intervalX = max(X) - min(heapq.nlargest(2,set(X)))
intervalY = max(Y) - min(heapq.nlargest(2,set(Y)))
wide, tall = 1+wide/intervalX, 1+tall/intervalY
X = np.reshape(X, [wide, tall])
Y = np.reshape(Y, [wide, tall])
# TODO: BUG: fix so that Z transposes with x & y reversed
Z = np.reshape(Z, [wide, tall])
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cmap, linewidth=0, antialiased=False)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_title(substitute_titles(k,series))
fig.colorbar(surf, shrink=0.5, aspect=5)
figs.append(fig)
return figs
def timeit_heatmap(data, xlabel='xlabel', ylabel='ylabel', **kwargs):
"""Heatmap plot of timeit data, one chart per function.
"""
dataT = {}
figs = []
series = kwargs.get('series', (0,1))
cmap = kwargs.get('cmap', cm.coolwarm)
for k, v in data.items():
dataT[k] = zip(*v)
X, Y, Z = dataT[k][series[0]], dataT[k][series[1]], dataT[k][-1]
left, right = min(X), max(X)
bottom, top = min(Y), max(Y)
extent = [left, right, bottom, top]
wide, tall = (max(X)-min(X)+1), (max(Y)-min(Y)+1)
intervalX = max(X) - min(heapq.nlargest(2,set(X)))
intervalY = max(Y) - min(heapq.nlargest(2,set(Y)))
if intervalX > 1:
wide = 1 + wide/intervalX
else:
wide = 1
if intervalY > 1:
tall = 1 + tall/intervalY
else:
tall = 1
# TODO: BUG: fix so that Z transposes with x & y series reversed
Z = np.reshape(Z, [wide, tall])
Z = list(zip(*Z)) # Z is transposed
Z = [i for i in Z[::-1]] # Z is upside down
fig, ax = plt.subplots()
hmap = ax.imshow(Z, extent=extent, cmap=cmap, interpolation='nearest')
fig.colorbar(hmap).set_label("time")
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_title(substitute_titles(k,series))
figs.append(fig)
return figs
def substitute_titles(label, series):
ordered_axes=["x", "y", "z"]
try:
for i, v in enumerate(series):
label = label.replace("{"+str(v)+"}", ordered_axes[i])
except:
label = label.replace("{"+str(series)+"}", ordered_axes[0])
return label