Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wip entire aoi row instead ia #852

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
15 changes: 7 additions & 8 deletions src/pymovements/events/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import numpy as np
import polars as pl
from tqdm import tqdm

from pymovements.events.properties import duration
from pymovements.stimulus.text import TextStimulus
Expand Down Expand Up @@ -323,14 +324,12 @@ def map_to_aois(self, aoi_dataframe: TextStimulus) -> None:
Text dataframe to map fixation to.
"""
self.unnest()
self.frame = self.frame.with_columns(
area_of_interest=pl.Series(
get_aoi(
aoi_dataframe, row, 'location_x', 'location_y',
)
for row in self.frame.iter_rows(named=True)
),
)
aois = [
get_aoi(aoi_dataframe, row, 'location_x', 'location_y')
for row in tqdm(self.frame.iter_rows(named=True))
]
aoi_df = pl.concat(aois)
self.frame = pl.concat([self.frame, aoi_df], how='horizontal')

def __str__(self: Any) -> str:
"""Return string representation of event dataframe."""
Expand Down
14 changes: 6 additions & 8 deletions src/pymovements/gaze/gaze_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,14 +1023,12 @@
else:
raise ValueError('neither position nor pixel in gaze dataframe, one needed for mapping')

self.frame = self.frame.with_columns(
area_of_interest=pl.Series(
get_aoi(
aoi_dataframe, row, x_eye, y_eye,
)
for row in self.frame.iter_rows(named=True)
),
)
aois = [
get_aoi(aoi_dataframe, row, x_eye, y_eye)
for row in tqdm(self.frame.iter_rows(named=True))
]
aoi_df = pl.concat(aois)
self.frame = pl.concat([self.frame, aoi_df], how='horizontal')

Check warning on line 1031 in src/pymovements/gaze/gaze_dataframe.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/gaze/gaze_dataframe.py#L1030-L1031

Added lines #L1030 - L1031 were not covered by tests

def nest(
self,
Expand Down
186 changes: 184 additions & 2 deletions src/pymovements/reading_measures/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@
"""Module for the Reading Measure DataFrame."""
from __future__ import annotations

import os

import pandas as pd
import polars as pl
from tqdm import tqdm

from pymovements.stimulus.text import from_file


class ReadingMeasures:
Expand All @@ -32,6 +38,182 @@
A reading measure dataframe.
"""

def __init__(self, reading_measure_df: pl.DataFrame) -> None:
def __init__(self, reading_measure_df: pl.DataFrame | None = None) -> None:
self.frame = reading_measure_df
if reading_measure_df is None:
self.frame = []

Check warning on line 44 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L44

Added line #L44 was not covered by tests

def process_dataset(self, dataset, aoi_dict, save_path) -> int:
for event_idx in tqdm(range(len(dataset.events))):
tmp_df = dataset.events[event_idx]

Check warning on line 48 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L48

Added line #L48 was not covered by tests
if tmp_df.frame.is_empty():
print('+ skip due to empty DF')
continue
text_id = tmp_df['text_id'][0]
aoi_text_stimulus = from_file(

Check warning on line 53 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L50-L53

Added lines #L50 - L53 were not covered by tests
aoi_dict[text_id],
aoi_column='character',
start_x_column='start_x',
start_y_column='start_y',
end_x_column='end_x',
end_y_column='end_y',
page_column='page',
custom_read_kwargs={'separator': '\t'},
)

dataset.events[event_idx].map_to_aois(aoi_text_stimulus)

Check warning on line 64 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L64

Added line #L64 was not covered by tests

for _fix_file in dataset.events:
if _fix_file.frame.is_empty():
print('+ skip due to empty DF')
continue
fixations_df = _fix_file.frame.to_pandas()

Check warning on line 70 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L68-L70

Added lines #L68 - L70 were not covered by tests

text_id = fixations_df.iloc[0]['text_id']
subject_id = int(fixations_df.iloc[0]['subject_id'])
aoi_df = pd.read_csv(aoi_dict[text_id], delimiter='\t')

Check warning on line 74 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L72-L74

Added lines #L72 - L74 were not covered by tests

rm_df = self.compute_reading_measures(

Check warning on line 76 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L76

Added line #L76 was not covered by tests
fixations_df=fixations_df,
aoi_df=aoi_df,
)

rm_df['subject_id'] = subject_id
rm_df['text_id'] = text_id

Check warning on line 82 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L81-L82

Added lines #L81 - L82 were not covered by tests

# Append the computed reading measures DataFrame to the list
self.frame.append(rm_df)

Check warning on line 85 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L85

Added line #L85 was not covered by tests

# Save to CSV if save_path is provided
if save_path is not None:
rm_filename = f'{subject_id}-{text_id}-reading_measures.csv'
path_save_rm_file = os.path.join(save_path, rm_filename)
rm_df.to_csv(path_save_rm_file, index=False)

Check warning on line 91 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L89-L91

Added lines #L89 - L91 were not covered by tests

return 0

Check warning on line 93 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L93

Added line #L93 was not covered by tests

def compute_reading_measures(
self, fixations_df: pd.DataFrame,
aoi_df: pd.DataFrame,
) -> pd.DataFrame:
"""
Computes reading measures from fixation sequences.

Parameters
----------
fixations_df : pd.DataFrame
DataFrame with fixation data, containing columns 'index', 'duration', 'aoi', 'word_roi_str'.
aoi_df : pd.DataFrame
DataFrame with AOI data, containing columns 'word_index', 'word', and the AOIs of each word.

Returns
-------
pd.DataFrame
DataFrame with computed reading measures.
"""
# Append an extra dummy fixation to have the next fixation for the actual last fixation.
fixations_df = pd.concat(

Check warning on line 115 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L115

Added line #L115 was not covered by tests
[
fixations_df,
pd.DataFrame(
[[0 for _ in range(len(fixations_df.columns))]],
columns=fixations_df.columns,
),
],
ignore_index=True,
)

# Adjust AOI indices (fix off by one error).
aoi_df['aoi'] = aoi_df['aoi'] - 1

Check warning on line 127 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L127

Added line #L127 was not covered by tests
# Get original words of the text and their indices.
text_aois = aoi_df['aoi'].tolist()
text_strs = aoi_df['character'].tolist()

Check warning on line 130 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L129-L130

Added lines #L129 - L130 were not covered by tests

# Initialize dictionary for reading measures per word.
word_dict = {

Check warning on line 133 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L133

Added line #L133 was not covered by tests
int(word_index): {
'word': word,
'word_index': word_index,
'FFD': 0, 'SFD': 0, 'FD': 0, 'FPRT': 0, 'FRT': 0, 'TFT': 0, 'RRT': 0,
'RPD_inc': 0, 'RPD_exc': 0, 'RBRT': 0, 'Fix': 0, 'FPF': 0, 'RR': 0,
'FPReg': 0, 'TRC_out': 0, 'TRC_in': 0, 'SL_in': 0, 'SL_out': 0, 'TFC': 0,
} for word_index, word in zip(text_aois, text_strs)
}

# Variables to track fixation progress.
right_most_word, cur_fix_word_idx, next_fix_word_idx, next_fix_dur = -1, -1, -1, -1

Check warning on line 144 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L144

Added line #L144 was not covered by tests

# Iterate over fixation data.
for index, fixation in fixations_df.iterrows():
try:
aoi = int(fixation['aoi']) - 1
except ValueError:
continue

Check warning on line 151 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L148-L151

Added lines #L148 - L151 were not covered by tests

# Update variables.
last_fix_word_idx = cur_fix_word_idx
cur_fix_word_idx = next_fix_word_idx
cur_fix_dur = next_fix_dur

Check warning on line 156 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L154-L156

Added lines #L154 - L156 were not covered by tests
if pd.isna(cur_fix_dur):
continue

Check warning on line 158 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L158

Added line #L158 was not covered by tests

next_fix_word_idx = aoi
next_fix_dur = fixation['duration']

Check warning on line 161 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L160-L161

Added lines #L160 - L161 were not covered by tests

if next_fix_dur == 0:
next_fix_word_idx = cur_fix_word_idx

Check warning on line 164 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L164

Added line #L164 was not covered by tests

if right_most_word < cur_fix_word_idx:
right_most_word = cur_fix_word_idx

Check warning on line 167 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L167

Added line #L167 was not covered by tests

if cur_fix_word_idx == -1:
continue

Check warning on line 170 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L170

Added line #L170 was not covered by tests

# Update reading measures for the current word.
word_dict[cur_fix_word_idx]['TFT'] += int(cur_fix_dur)
word_dict[cur_fix_word_idx]['TFC'] += 1

Check warning on line 174 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L173-L174

Added lines #L173 - L174 were not covered by tests
if word_dict[cur_fix_word_idx]['FD'] == 0:
word_dict[cur_fix_word_idx]['FD'] += int(cur_fix_dur)

Check warning on line 176 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L176

Added line #L176 was not covered by tests

if right_most_word == cur_fix_word_idx:
if word_dict[cur_fix_word_idx]['TRC_out'] == 0:
word_dict[cur_fix_word_idx]['FPRT'] += int(cur_fix_dur)

Check warning on line 180 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L180

Added line #L180 was not covered by tests
if last_fix_word_idx < cur_fix_word_idx:
word_dict[cur_fix_word_idx]['FFD'] += int(cur_fix_dur)

Check warning on line 182 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L182

Added line #L182 was not covered by tests
else:
word_dict[right_most_word]['RPD_exc'] += int(cur_fix_dur)

Check warning on line 184 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L184

Added line #L184 was not covered by tests

if cur_fix_word_idx < last_fix_word_idx:
word_dict[cur_fix_word_idx]['TRC_in'] += 1

Check warning on line 187 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L187

Added line #L187 was not covered by tests
if cur_fix_word_idx > next_fix_word_idx:
word_dict[cur_fix_word_idx]['TRC_out'] += 1

Check warning on line 189 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L189

Added line #L189 was not covered by tests
if cur_fix_word_idx == right_most_word:
word_dict[cur_fix_word_idx]['RBRT'] += int(cur_fix_dur)

Check warning on line 191 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L191

Added line #L191 was not covered by tests
if (
word_dict[cur_fix_word_idx]['FRT'] == 0 and
(not next_fix_word_idx == cur_fix_word_idx or next_fix_dur == 0)
):
word_dict[cur_fix_word_idx]['FRT'] = word_dict[cur_fix_word_idx]['TFT']

Check warning on line 196 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L196

Added line #L196 was not covered by tests
if word_dict[cur_fix_word_idx]['SL_in'] == 0:
word_dict[cur_fix_word_idx]['SL_in'] = cur_fix_word_idx - last_fix_word_idx

Check warning on line 198 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L198

Added line #L198 was not covered by tests
if word_dict[cur_fix_word_idx]['SL_out'] == 0:
word_dict[cur_fix_word_idx]['SL_out'] = next_fix_word_idx - cur_fix_word_idx

Check warning on line 200 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L200

Added line #L200 was not covered by tests

# Finalize reading measures.
for word_indices, word_rm in sorted(word_dict.items()):
if word_rm['FFD'] == word_rm['FPRT']:
word_rm['SFD'] = word_rm['FFD']
word_rm['RRT'] = word_rm['TFT'] - word_rm['FPRT']
word_rm['FPF'] = int(word_rm['FFD'] > 0)
word_rm['RR'] = int(word_rm['RRT'] > 0)
word_rm['FPReg'] = int(word_rm['RPD_exc'] > 0)
word_rm['Fix'] = int(word_rm['TFT'] > 0)
word_rm['RPD_inc'] = word_rm['RPD_exc'] + word_rm['RBRT']

Check warning on line 211 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L205-L211

Added lines #L205 - L211 were not covered by tests

# Create or append to DataFrame.
if word_indices == 0:
rm_df = pd.DataFrame([word_rm])

Check warning on line 215 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L215

Added line #L215 was not covered by tests
else:
rm_df = pd.concat([rm_df, pd.DataFrame([word_rm])])

Check warning on line 217 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L217

Added line #L217 was not covered by tests

self.frame = reading_measure_df.clone()
return rm_df

Check warning on line 219 in src/pymovements/reading_measures/frame.py

View check run for this annotation

Codecov / codecov/patch

src/pymovements/reading_measures/frame.py#L219

Added line #L219 was not covered by tests
61 changes: 31 additions & 30 deletions src/pymovements/utils/aois.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,41 +64,42 @@ def get_aoi(
height_column=aoi_dataframe.width_column,
width_column=aoi_dataframe.height_column,
)
try:
aoi = aoi_dataframe.aois.filter(
(aoi_dataframe.aois[aoi_dataframe.start_x_column] <= row[x_eye]) &
(
row[x_eye] <
aoi_dataframe.aois[aoi_dataframe.start_x_column] +
aoi_dataframe.aois[aoi_dataframe.width_column]
) &
(aoi_dataframe.aois[aoi_dataframe.start_y_column] <= row[y_eye]) &
(
row[y_eye] <
aoi_dataframe.aois[aoi_dataframe.start_y_column] +
aoi_dataframe.aois[aoi_dataframe.height_column]
),
)[aoi_dataframe.aoi_column].item()
return aoi
except ValueError:
return ''
aoi = aoi_dataframe.aois.filter(
(aoi_dataframe.aois[aoi_dataframe.start_x_column] <= row[x_eye]) &
(
row[x_eye] <
aoi_dataframe.aois[aoi_dataframe.start_x_column] +
aoi_dataframe.aois[aoi_dataframe.width_column]
) &
(aoi_dataframe.aois[aoi_dataframe.start_y_column] <= row[y_eye]) &
(
row[y_eye] <
aoi_dataframe.aois[aoi_dataframe.start_y_column] +
aoi_dataframe.aois[aoi_dataframe.height_column]
),
)

if aoi.is_empty():
aoi.extend(pl.from_dict({col: None for col in aoi.columns}))
return aoi
elif aoi_dataframe.end_x_column is not None:
checks.check_is_none_is_mutual(
end_x_column=aoi_dataframe.end_x_column,
end_y_column=aoi_dataframe.end_y_column,
)
try:
aoi = aoi_dataframe.aois.filter(
# x-coordinate: within bounding box
(aoi_dataframe.aois[aoi_dataframe.start_x_column] <= row[x_eye]) &
(row[x_eye] < aoi_dataframe.aois[aoi_dataframe.end_x_column]) &
# y-coordinate: within bounding box
(aoi_dataframe.aois[aoi_dataframe.start_y_column] <= row[y_eye]) &
(row[y_eye] < aoi_dataframe.aois[aoi_dataframe.end_y_column]),
)[aoi_dataframe.aoi_column].item()
return aoi
except ValueError:
return ''
aoi = aoi_dataframe.aois.filter(
# x-coordinate: within bounding box
(aoi_dataframe.aois[aoi_dataframe.start_x_column] <= row[x_eye]) &
(row[x_eye] < aoi_dataframe.aois[aoi_dataframe.end_x_column]) &
# y-coordinate: within bounding box
(aoi_dataframe.aois[aoi_dataframe.start_y_column] <= row[y_eye]) &
(row[y_eye] < aoi_dataframe.aois[aoi_dataframe.end_y_column]),
)

if aoi.is_empty():
aoi.extend(pl.from_dict({col: None for col in aoi.columns}))

return aoi
else:
raise ValueError(
'either aoi_dataframe.width or aoi_dataframe.end_x_column have to be not None',
Expand Down
Loading
Loading