Skip to content

Commit 567afe0

Browse files
authored
Adding to_dataframe method for multidim measurements. (#702)
* Adding to_dataframe method to Measurement and DimensionedMeasuredValue to conver to a pandas.DataFrame' * adding pandas as a dependency for unit tests * added an example for multidim measurements including conversion to dataframes * cleaning up formatting in example/measurements.py * making minor changes suggested by code review * adding nice printout to examples/measurement.py * adding test case of pandas not present * patch measurements.pandas for existing test cases
1 parent bce5b3b commit 567afe0

File tree

4 files changed

+122
-2
lines changed

4 files changed

+122
-2
lines changed

examples/measurements.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
# Import openhtf with an abbreviated name, as we'll be using a bunch of stuff
4848
# from it throughout our test scripts. See __all__ at the top of
4949
# openhtf/__init__.py for details on what's in top-of-module namespace.
50+
import random
51+
5052
import openhtf as htf
5153

5254
# Import this output mechanism as it's the specific one we want to use.
@@ -118,10 +120,46 @@ def inline_phase(test):
118120
test.logger.info('Set inline_kwargs to a failing value, test should FAIL!')
119121

120122

123+
# A multidim measurement including how to convert to a pandas dataframe and
124+
# a numpy array.
125+
@htf.measures(htf.Measurement('power_time_series')
126+
.with_dimensions('ms', 'V', 'A'))
127+
@htf.measures(htf.Measurement('average_voltage').with_units('V'))
128+
@htf.measures(htf.Measurement('average_current').with_units('A'))
129+
@htf.measures(htf.Measurement('resistance').with_units('ohm').in_range(9, 11))
130+
def multdim_measurements(test):
131+
# Create some fake current and voltage over time data
132+
for t in range(10):
133+
resistance = 10
134+
voltage = 10 + 10.0*t
135+
current = voltage/resistance + .01*random.random()
136+
dimensions = (t, voltage, current)
137+
test.measurements['power_time_series'][dimensions] = 0
138+
139+
# When accessing your multi-dim measurement a DimensionedMeasuredValue
140+
# is returned.
141+
dim_measured_value = test.measurements['power_time_series']
142+
143+
# Let's convert that to a pandas dataframe
144+
power_df = dim_measured_value.to_dataframe(columns=['ms', 'V', 'A', 'n/a'])
145+
test.logger.info('This is what a dataframe looks like:\n%s', power_df)
146+
test.measurements['average_voltage'] = power_df['V'].mean()
147+
148+
# We can convert the dataframe to a numpy array as well
149+
power_array = power_df.as_matrix()
150+
test.logger.info('This is the same data in a numpy array:\n%s', power_array)
151+
test.measurements['average_current'] = power_array.mean(axis=0)[2]
152+
153+
# Finally, let's estimate the resistance
154+
test.measurements['resistance'] = (
155+
test.measurements['average_voltage'] /
156+
test.measurements['average_current'])
157+
158+
121159
if __name__ == '__main__':
122160
# We instantiate our OpenHTF test with the phases we want to run as args.
123161
test = htf.Test(hello_phase, again_phase, lots_of_measurements,
124-
measure_seconds, inline_phase)
162+
measure_seconds, inline_phase, multdim_measurements)
125163

126164
# In order to view the result of the test, we have to output it somewhere,
127165
# and a local JSON file is a convenient way to do this. Custom output

openhtf/core/measurements.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ def WidgetTestPhase(test):
7373
from openhtf.util import validators
7474
from openhtf.util import units
7575

76+
try:
77+
import pandas
78+
except ImportError:
79+
pandas = None
80+
7681
_LOG = logging.getLogger(__name__)
7782

7883

@@ -194,7 +199,7 @@ def _maybe_make_dimension(self, dimension):
194199
if isinstance(dimension, units.UnitDescriptor):
195200
return Dimension.from_unit_descriptor(dimension)
196201
if isinstance(dimension, str):
197-
return Dimension.from_string(string)
202+
return Dimension.from_string(dimension)
198203

199204
raise TypeError('Cannot convert %s to a dimension', dimension)
200205

@@ -267,6 +272,21 @@ def _asdict(self):
267272
retval[attr] = getattr(self, attr)
268273
return retval
269274

275+
def to_dataframe(self, columns=None):
276+
"""Convert a multi-dim to a pandas dataframe."""
277+
if not isinstance(self.measured_value, DimensionedMeasuredValue):
278+
raise TypeError(
279+
'Only a dimensioned measurement can be converted to a DataFrame')
280+
281+
282+
if columns is None:
283+
columns = [d.name for d in self.dimensions]
284+
columns += [self.units.name if self.units else 'value']
285+
286+
dataframe = self.measured_value.to_dataframe(columns)
287+
288+
return dataframe
289+
270290

271291
class MeasuredValue(
272292
mutablerecords.Record('MeasuredValue', ['name'],
@@ -430,6 +450,15 @@ def value(self):
430450
return [dimensions + (value,) for dimensions, value in
431451
self.value_dict.items()]
432452

453+
def to_dataframe(self, columns=None):
454+
"""Converts to a `pandas.DataFrame`"""
455+
if not self.is_value_set:
456+
raise ValueError('Value must be set before converting to a DataFrame.')
457+
if not pandas:
458+
raise RuntimeError('Install pandas to convert to pandas.DataFrame')
459+
return pandas.DataFrame.from_records(self.value, columns=columns)
460+
461+
433462

434463
class Collection(mutablerecords.Record('Collection', ['_measurements'])):
435464
"""Encapsulates a collection of measurements.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def run_tests(self):
208208
],
209209
tests_require=[
210210
'mock>=2.0.0',
211+
'pandas>=0.22.0',
211212
'pytest>=2.9.2',
212213
'pytest-cov>=2.2.1',
213214
],

test/core/measurements_test.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# coding: utf-8
12
# Copyright 2016 Google Inc. All Rights Reserved.
23

34
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,8 +19,13 @@
1819
actually care about.
1920
"""
2021

22+
from openhtf.core import measurements
23+
24+
import mock
25+
2126
from examples import all_the_things
2227
import openhtf as htf
28+
from openhtf.core.measurements import Outcome
2329
from openhtf.util import test as htf_test
2430

2531

@@ -30,6 +36,12 @@
3036

3137
class TestMeasurements(htf_test.TestCase):
3238

39+
def setUp(self):
40+
# Ensure most measurements features work without pandas.
41+
pandas_patch = mock.patch.object(measurements, 'pandas', None)
42+
pandas_patch.start()
43+
self.addCleanup(pandas_patch.stop)
44+
3345
def test_unit_enforcement(self):
3446
"""Creating a measurement with invalid units should raise."""
3547
self.assertRaises(TypeError, htf.Measurement('bad_units').with_units, 1701)
@@ -78,3 +90,43 @@ def test_measurement_order(self):
7890
self.assertEqual(list(record.measurements.keys()),
7991
['replaced_min_only', 'replaced_max_only',
8092
'replaced_min_max'])
93+
94+
95+
class TestMeasurement(htf_test.TestCase):
96+
97+
@mock.patch.object(measurements, 'pandas', None)
98+
def test_to_dataframe__no_pandas(self):
99+
with self.assertRaises(RuntimeError):
100+
self.test_to_dataframe(units=True)
101+
102+
def test_to_dataframe(self, units=True):
103+
measurement = htf.Measurement('test_multidim')
104+
measurement.with_dimensions('ms', 'assembly',
105+
htf.Dimension('my_zone', 'zone'))
106+
107+
if units:
108+
measurement.with_units('°C')
109+
measure_column_name = 'degree Celsius'
110+
else:
111+
measure_column_name = 'value'
112+
113+
for t in range(5):
114+
for assembly in ['A', 'B', 'C']:
115+
for zone in range(3):
116+
temp = zone + t
117+
dims = (t, assembly, zone)
118+
measurement.measured_value[dims] = temp
119+
120+
measurement.outcome = Outcome.PASS
121+
122+
df = measurement.to_dataframe()
123+
coordinates = (1, 'A', 2)
124+
query = '(ms == %s) & (assembly == "%s") & (my_zone == %s)' % (
125+
coordinates)
126+
127+
self.assertEqual(
128+
measurement.measured_value[coordinates],
129+
df.query(query)[measure_column_name].values[0])
130+
131+
def test_to_dataframe__no_units(self):
132+
self.test_to_dataframe(units=False)

0 commit comments

Comments
 (0)