diff options
-rw-r--r-- | src/argaze.test/GazeFeatures.py | 47 | ||||
-rw-r--r-- | src/argaze/GazeFeatures.py | 54 |
2 files changed, 94 insertions, 7 deletions
diff --git a/src/argaze.test/GazeFeatures.py b/src/argaze.test/GazeFeatures.py index f73eefe..8cd2e56 100644 --- a/src/argaze.test/GazeFeatures.py +++ b/src/argaze.test/GazeFeatures.py @@ -13,6 +13,7 @@ from dataclasses import dataclass from argaze import GazeFeatures import numpy +import pandas def random_gaze_positions(size, screen_dimension: tuple[float, float] = (1, 1)): """ Generate random TimeStampedGazePsoitions for testing purpose. @@ -209,6 +210,52 @@ class TestTimeStampedGazePositionsClass(unittest.TestCase): self.assertEqual(repr(ts_gaze_positions), "{\"0\": {\"message\": null, \"value\": [null, null], \"precision\": null}}") + def test_from_dataframe(self): + """Test from_dataframe classmethod.""" + + data = {'Specific timestamp label': [0, 1, 2, 3, 4], + 'Specific gaze position x label': [0, 10, numpy.nan, 30, 40], + 'Specific gaze position y label': [0, 100, numpy.nan, 300, 400] + } + dataframe = pandas.DataFrame().from_dict(data) + + ts_gaze_positions = GazeFeatures.TimeStampedGazePositions.from_dataframe(dataframe, timestamp='Specific timestamp label', x='Specific gaze position x label', y='Specific gaze position y label') + + # Check buffer length + self.assertEqual(len(ts_gaze_positions), 5) + + # Check first gaze position is correctly stored and accessible as a GazePosition + self.assertIsInstance(ts_gaze_positions[0], GazeFeatures.GazePosition) + self.assertEqual(ts_gaze_positions[0].precision, 0) + self.assertEqual(ts_gaze_positions[0].valid, True) + + # Check third gaze position is correctly stored and accessible as a UnvalidGazePosition + self.assertIsInstance(ts_gaze_positions[2], GazeFeatures.UnvalidGazePosition) + self.assertEqual(ts_gaze_positions[2].precision, None) + self.assertEqual(ts_gaze_positions[2].valid, False) + + data = {'Specific timestamp label': [0, 1, 2, 3, 4], + 'Specific gaze position x label': [0, 10, numpy.nan, 30, 40], + 'Specific gaze position y label': [0, 100, numpy.nan, 300, 400], + 'Specific precisison label': [15, 15, numpy.nan, 15, 15] + } + dataframe = pandas.DataFrame().from_dict(data) + + ts_gaze_positions = GazeFeatures.TimeStampedGazePositions.from_dataframe(dataframe, timestamp='Specific timestamp label', x='Specific gaze position x label', y='Specific gaze position y label', precision='Specific precisison label') + + # Check buffer length + self.assertEqual(len(ts_gaze_positions), 5) + + # Check first gaze position is correctly stored and accessible as a GazePosition + self.assertIsInstance(ts_gaze_positions[0], GazeFeatures.GazePosition) + self.assertEqual(ts_gaze_positions[0].precision, 15) + self.assertEqual(ts_gaze_positions[0].valid, True) + + # Check third gaze position is correctly stored and accessible as a UnvalidGazePosition + self.assertIsInstance(ts_gaze_positions[2], GazeFeatures.UnvalidGazePosition) + self.assertEqual(ts_gaze_positions[2].precision, None) + self.assertEqual(ts_gaze_positions[2].valid, False) + def test_as_dataframe(self): """Test inherited as_dataframe method.""" diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index 53bec25..870cd3c 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -125,9 +125,15 @@ class TimeStampedGazePositions(DataStructures.TimeStampedBuffer): assert(set(['value', 'precision']).issubset(value.keys())) - if 'message' in value.keys(): + if math.isnan(value['precision']): - value = UnvalidGazePosition(value['message']) + if 'message' in value.keys(): + + value = UnvalidGazePosition(value['message']) + + else : + + value = UnvalidGazePosition() else: @@ -148,14 +154,48 @@ class TimeStampedGazePositions(DataStructures.TimeStampedBuffer): return TimeStampedGazePositions({ast.literal_eval(ts_str): json_buffer[ts_str] for ts_str in json_buffer}) @classmethod - def from_dataframe(self, dataframe: pandas.DataFrame, exclude=[]) -> TimeStampedGazePositionsType: - """Create a TimeStampedGazePositions from [Pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html).""" + def from_dataframe(self, dataframe: pandas.DataFrame, timestamp: str, x: str, y: str, precision: str = None) -> TimeStampedGazePositionsType: + """Create a TimeStampedGazePositions from [Pandas DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html). + + Parameters: + timestamp: specific timestamp column label. + x: specific x column label. + y: specific y column label. + precision: specific precision column label if exist. + """ + + # Select columns + if precision: + + df = dataframe[[timestamp, x, y, precision]] + + else: - dataframe.drop(exclude, inplace=True, axis=True) + df = dataframe[[timestamp, x, y]] - assert(dataframe.index.name == 'timestamp') + # Merge x and y columns into one 'value' column + df['value'] = tuple(zip(df[x], df[y])) + df.drop(columns= [x, y], inplace=True, axis=1) + + # Handle precision data + if precision: + + # Rename precision column into 'precision' column + df.rename(columns={precision: 'precision'}, inplace=True) + + else: + + # Append a precision column where precision is NaN if value is a tuple of NaN else 0 + df['precision'] = df.apply(lambda row: numpy.nan if math.isnan(row.value[0]) or math.isnan(row.value[1]) else 0, axis=True) + + # Rename timestamp column into 'timestamp' column then use it as index + df.rename(columns={timestamp: 'timestamp'}, inplace=True) + df.set_index('timestamp', inplace=True) + + # Filter duplicate timestamps + df = df[df.index.duplicated() == False] - return TimeStampedGazePositions(dataframe.to_dict('index')) + return TimeStampedGazePositions(df.to_dict('index')) GazeMovementType = TypeVar('GazeMovement', bound="GazeMovement") # Type definition for type annotation convenience |