| """ |
| Partially taken and adapted from: https://github.com/jwcarr/eyekit/blob/1db1913411327b108b87e097a00278b6e50d0751/eyekit/measure.py |
| Functions for calculating common reading measures, such as gaze duration or |
| initial landing position. |
| """ |
|
|
| import pandas as pd |
|
|
|
|
| def fix_in_ia(fix_x, fix_y, ia_x_min, ia_x_max, ia_y_min, ia_y_max): |
| in_x = ia_x_min <= fix_x <= ia_x_max |
| in_y = ia_y_min <= fix_y <= ia_y_max |
| if in_x and in_y: |
| return True |
| else: |
| return False |
|
|
|
|
| def fix_in_ia_default(fixation, ia_row, prefix): |
| return fix_in_ia( |
| fixation.x, |
| fixation.y, |
| ia_row[f"{prefix}_xmin"], |
| ia_row[f"{prefix}_xmax"], |
| ia_row[f"{prefix}_ymin"], |
| ia_row[f"{prefix}_ymax"], |
| ) |
|
|
|
|
| def number_of_fixations_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the number of |
| fixations on that interest area. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| counts = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| count = 0 |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia( |
| fixation.x, |
| fixation.y, |
| ia_row[f"{prefix}_xmin"], |
| ia_row[f"{prefix}_xmax"], |
| ia_row[f"{prefix}_ymin"], |
| ia_row[f"{prefix}_ymax"], |
| ): |
| count += 1 |
| counts.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "number_of_fixations": count, |
| } |
| ) |
| return pd.DataFrame(counts) |
|
|
|
|
| def initial_fixation_duration_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the duration of the |
| initial fixation on that interest area for each word. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| durations = [] |
|
|
| for cidx, ia_row in ia_df.iterrows(): |
| initial_duration = 0 |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| initial_duration = fixation.duration |
| break |
| durations.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "initial_fixation_duration": initial_duration, |
| } |
| ) |
|
|
| return pd.DataFrame(durations) |
|
|
|
|
| def first_of_many_duration_own(trial, dffix, prefix="word"): |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| durations = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| fixation_durations = [] |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| fixation_durations.append(fixation.duration) |
| if len(fixation_durations) > 1: |
| durations.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "first_of_many_duration": fixation_durations[0], |
| } |
| ) |
| if durations: |
| return pd.DataFrame(durations) |
| else: |
| return pd.DataFrame() |
|
|
|
|
| def total_fixation_duration_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the sum duration of |
| all fixations on that interest area. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| durations = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| total_duration = 0 |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| total_duration += fixation.duration |
| durations.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "total_fixation_duration": total_duration, |
| } |
| ) |
| return pd.DataFrame(durations) |
|
|
|
|
| def gaze_duration_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the gaze duration on |
| that interest area. Gaze duration is the sum duration of all fixations |
| inside an interest area until the area is exited for the first time. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| durations = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| duration = 0 |
| in_ia = False |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| duration += fixation.duration |
| in_ia = True |
| elif in_ia: |
| break |
| durations.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "gaze_duration": duration, |
| } |
| ) |
| return pd.DataFrame(durations) |
|
|
|
|
| def go_past_duration_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the go-past time on |
| that interest area. Go-past time is the sum duration of all fixations from |
| when the interest area is first entered until when it is first exited to |
| the right, including any regressions to the left that occur during that |
| time period (and vice versa in the case of right-to-left text). |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| results = [] |
|
|
| for cidx, ia_row in ia_df.iterrows(): |
| entered = False |
| go_past_time = 0 |
|
|
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| if not entered: |
| entered = True |
| go_past_time += fixation.duration |
| elif entered: |
| if ia_row[f"{prefix}_xmax"] < fixation.x: |
| break |
| go_past_time += fixation.duration |
|
|
| results.append({f"{prefix}_index": cidx, prefix: ia_row[f"{prefix}"], "go_past_duration": go_past_time}) |
|
|
| return pd.DataFrame(results) |
|
|
|
|
| def second_pass_duration_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the second pass |
| duration on that interest area for each word. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| durations = [] |
|
|
| for cidx, ia_row in ia_df.iterrows(): |
| current_pass = None |
| next_pass = 1 |
| pass_duration = 0 |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| if current_pass is None: |
| current_pass = next_pass |
| if current_pass == 2: |
| pass_duration += fixation.duration |
| elif current_pass == 1: |
| current_pass = None |
| next_pass += 1 |
| elif current_pass == 2: |
| break |
| durations.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "second_pass_duration": pass_duration, |
| } |
| ) |
|
|
| return pd.DataFrame(durations) |
|
|
|
|
| def initial_landing_position_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the initial landing |
| position (expressed in character positions) on that interest area. |
| Counting is from 1. If the interest area represents right-to-left text, |
| the first character is the rightmost one. Returns `None` if no fixation |
| landed on the interest area. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| if prefix == "word": |
| chars_df = pd.DataFrame(trial[f"chars_list"]) |
| else: |
| chars_df = None |
| results = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| landing_position = None |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| if prefix == "char": |
| landing_position = 1 |
| else: |
| prefix_temp = "char" |
| matched_chars_df = chars_df.loc[ |
| (chars_df.char_xmin >= ia_row[f"{prefix}_xmin"]) |
| & (chars_df.char_xmax <= ia_row[f"{prefix}_xmax"]) |
| & (chars_df.char_ymin >= ia_row[f"{prefix}_ymin"]) |
| & (chars_df.char_ymax <= ia_row[f"{prefix}_ymax"]), |
| :, |
| ] |
| for char_idx, (rowidx, char_row) in enumerate(matched_chars_df.iterrows()): |
| if fix_in_ia_default(fixation, char_row, prefix_temp): |
| landing_position = char_idx + 1 |
| break |
| break |
| results.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "initial_landing_position": landing_position, |
| } |
| ) |
| return pd.DataFrame(results) |
|
|
|
|
| def initial_landing_distance_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the initial landing |
| distance on that interest area. The initial landing distance is the pixel |
| distance between the first fixation to land in an interest area and the |
| left edge of that interest area (or, in the case of right-to-left text, |
| the right edge). Technically, the distance is measured from the text onset |
| without including any padding. Returns `None` if no fixation landed on the |
| interest area. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| distances = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| initial_distance = None |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| distance = abs(ia_row[f"{prefix}_xmin"] - fixation.x) |
| if initial_distance is None: |
| initial_distance = distance |
| break |
| distances.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "initial_landing_distance": initial_distance, |
| } |
| ) |
| return pd.DataFrame(distances) |
|
|
|
|
| def landing_distances_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return a dataframe with |
| landing distances for each word in the interest area. |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| distances = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| landing_distances = [] |
| for idx, fixation in dffix.iterrows(): |
| if fix_in_ia_default(fixation, ia_row, prefix): |
| landing_distance = abs(ia_row[f"{prefix}_xmin"] - fixation.x) |
| landing_distances.append(round(landing_distance, ndigits=2)) |
| distances.append({f"{prefix}_index": cidx, prefix: ia_row[f"{prefix}"], "landing_distances": landing_distances}) |
| return pd.DataFrame(distances) |
|
|
|
|
| def number_of_regressions_in_own(trial, dffix, prefix="word"): |
| """ |
| Given an interest area and fixation sequence, return the number of |
| regressions back to that interest area after the interest area was read |
| for the first time. In other words, find the first fixation to exit the |
| interest area and then count how many times the reader returns to the |
| interest area from the right (or from the left in the case of |
| right-to-left text). |
| """ |
| ia_df = pd.DataFrame(trial[f"{prefix}s_list"]) |
| counts = [] |
| for cidx, ia_row in ia_df.iterrows(): |
| entered_interest_area = False |
| first_exit_index = None |
| count = 0 |
| prev_fixation = None |
| regression_counted = False |
|
|
| for fixidx, (rowidx, fixation) in enumerate(dffix.iterrows()): |
| if ( |
| entered_interest_area |
| and first_exit_index is not None |
| and fix_in_ia_default(fixation, ia_row, prefix) |
| and not regression_counted |
| ): |
| if prev_fixation.x > fixation.x: |
| count += 1 |
| regression_counted = True |
|
|
| if fix_in_ia_default(fixation, ia_row, prefix): |
| entered_interest_area = True |
| elif entered_interest_area and first_exit_index is None: |
| first_exit_index = fixidx |
| else: |
| regression_counted = False |
| prev_fixation = fixation |
|
|
| counts.append( |
| { |
| f"{prefix}_index": cidx, |
| prefix: ia_row[f"{prefix}"], |
| "number_of_regressions_in": count, |
| } |
| ) |
|
|
| return pd.DataFrame(counts) |
|
|