Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deply_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
- name: Build documentation
run: |
cd docs && make clean && make html



# 5️⃣ Desplegar a GitHub Pages
- name: Deploy to GitHub Pages
Expand Down
9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
src/pywib/__pycache__
src/pywib/tests/__pycache__
src/pywib/utils/__pycache__
src/pywib/core/__pycache__
test/__pycache__
__pycache__
dist
.vscode/
docs/build
venv
venv
.DS_Store
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Pywib (Python Web Interaction Behaviour) is a library desgined for analysing and
## How to
```python
from pywib import velocity, velocity_metrics

v = velocity(df_all_sessions)
v_metrics = velocity_metrics(None, v)
```
Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
project = 'pywib'
copyright = '2025, Guillermo Dylan Carvajal Aza, Alejandro Álvarez Varela'
author = 'Guillermo Dylan Carvajal Aza, Alejandro Álvarez Varela'
release = '0.1.5'
release = '1.0.0'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
10 changes: 8 additions & 2 deletions docs/source/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,14 @@ Small Example

from pywib import velocity, velocity_metrics

v = velocity(df_all_sessions)
v_metrics = velocity_metrics(None, v)
# From an already read DataFrame from a csv
df_all_sessions = process_csv(input_file)
df_all_sessions.rename(columns={'moveX': 'x', 'moveY': 'y', 'id_usuario': 'sessionId'}, inplace=True)

vel = velocity(df_all_sessions)
vel_metrics = velocity_metrics(vel)

acc = acceleration(vel, None, True)

References
=============
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "pywib"
version = "0.1.5"
version = "1.0.0"
authors = [
{ name="Guillermo Dylan Carvajal Aza", email="carvajalguillermo@uniovi.es" },
{ name="Alejandro Álvarez Varela", email="avarela@uniovi.es"}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ numpy>=1.26.4
matplotlib>=3.4.0
opencv-python>=4.12.0.88
seaborn>=0.13.2
joblib>=1.5.3
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
setup(
name='pywib',
packages=find_packages(),
version='0.1.5',
version='1.0.0',
description='HCI Web Interaction Analyzer - A library for analyzing web user interactions',
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
11 changes: 6 additions & 5 deletions src/pywib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
"""
"""

__version__ = "0.1.5"
__version__ = "1.0.0"
__author__ = "Guillermo Dylan Carvajal Aza"
__email__ = "carvajalguillermo@uniovi.es"

from .constants import *
from .utils import (validate_dataframe, validate_dataframe_keyboard,
extract_traces_by_session, visualize_trace, compute_space_time_diff,
video_from_trace, validate_duplicate_timestamps, keyboard_heatmap)
from .core import (velocity, acceleration, jerkiness, path, auc_ratio,
from .core import (velocity, acceleration, jerkiness, path, auc,
execution_time, movement_time, pauses_metrics, velocity_metrics,
acceleration_metrics, jerkiness_metrics, number_of_clicks,
click_slip, num_pauses, deviations, auc_ratio_metrics, typing_speed_metrics, typing_speed, backspace_usage)
click_slip, num_pauses, deviations,
typing_speed_metrics, typing_speed, backspace_usage, typing_durations)

__all__ = [
# Version info
Expand All @@ -38,14 +39,13 @@
# Movement functions
"velocity",
"acceleration",
"auc_ratio",
"jerkiness",
"path",
"velocity_metrics",
"acceleration_metrics",
"jerkiness_metrics",
"deviations",
"auc_ratio_metrics",
"auc"

# Mouse functions
"number_of_clicks",
Expand All @@ -55,6 +55,7 @@
"typing_speed",
"typing_speed_metrics",
"backspace_usage",
"typing_durations"

# Timing
"pauses_metrics",
Expand Down
11 changes: 10 additions & 1 deletion src/pywib/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Version and library information
LIBRARY_NAME = "pywib"
LIBRARY_VERSION = "0.1.5"
LIBRARY_VERSION = "1.0.0"

# Event types for interaction tracking
class EventTypes:
Expand Down Expand Up @@ -73,19 +73,28 @@ class ColumnNames:
DT = 'dt'
DX = 'dx'
DY = 'dy'
DISTANCE = 'distance'
VELOCITY= 'velocity'
ACCELERATION = 'acceleration'
JERKINESS = 'jerkiness'
AUC_RATIO = 'auc_ratio'
NUMBER_OF_PAUSES = 'num_pauses'
MEAN_PAUSE_PER_TRACE = 'mean_pauses_per_trace'
BACKSPACE_USAGE="backspace_usage"
TOTAL_CHARS="total_characters"
CLICK_SLIPS = 'click_slips'
MAX_CLICK_SLIP = 'max_click_slip'
MIN_CLICK_SLIP = 'min_click_slip'
MEAN_CLICK_SLIP = 'mean_click_slip'
MAX_CLICK_DURATION = 'max_click_duration'
MIN_CLICK_DURATION = 'min_click_duration'
MEAN_CLICK_DURATION = 'mean_click_duration'
AUC_GEOM = 'auc_geometric'
AUC_EXEC = 'auc_execution'
AAD = 'aad'
MAD_MAX = 'mad_mad'
MIN_MAD = 'mean_mad'
MEAN_MAD = 'min_mad'

class KeyCodeEvents:
""" Key code event constants for keyboard interactions."""
Expand Down
14 changes: 6 additions & 8 deletions src/pywib/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"""
Utility functions for PyWib
Core metrics functions from PyWib
"""
from .timing import execution_time, movement_time, num_pauses, pauses_metrics
from .movement import (velocity, acceleration, jerkiness,
path, auc_optimal, auc_ratio, auc, auc_ratio_metrics,
from .movement import (velocity, acceleration, jerkiness, auc,
velocity_metrics, acceleration_metrics, jerkiness_metrics,
deviations)
deviations, path)
from .mouse import click_slip, number_of_clicks
from .keyboard import (typing_speed, typing_speed_metrics, backspace_usage)
from .keyboard import (typing_speed, typing_speed_metrics, backspace_usage, typing_durations)

__all__ = [
"execution_time",
Expand All @@ -18,9 +17,6 @@
"acceleration",
"jerkiness",
"path",
"auc_optimal",
"auc_ratio",
"auc_ratio_metrics",
"auc",
"velocity_metrics",
"acceleration_metrics",
Expand All @@ -31,4 +27,6 @@
"typing_speed",
"typing_speed_metrics",
"backspace_usage",
"typing_durations",
"typing_speed"
]
11 changes: 11 additions & 0 deletions src/pywib/core/keyboard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Keyboard functions from PyWib
"""
from .keyboard import (typing_speed, typing_speed_metrics, backspace_usage, typing_durations)

__all__ = [
"typing_speed",
"typing_speed_metrics",
"backspace_usage",
"typing_durations"
]
31 changes: 24 additions & 7 deletions src/pywib/core/keyboard.py → src/pywib/core/keyboard/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,36 @@
import pandas as pd
import numpy as np
from pywib.utils.segmentation import extract_keystroke_traces_by_session
from pywib.utils.validation import validate_dataframe_keyboard
from pywib.utils import validate_dataframe_keyboard
from pywib.constants import EventTypes, ColumnNames
from pywib.utils.keyboard import (backspace_usage_df, backspace_usage_traces, typing_durations_df, typing_durations_traces, typing_speed_df, typing_speed_traces)
from pywib.utils.validation import validate_any_not_none

def typing_durations(df: pd.DataFrame = None, traces: dict[str, list[pd.DataFrame]] = None, per_traces: bool = True) -> list:
def typing_durations(df: pd.DataFrame = None, traces: dict[str, list[pd.DataFrame]] = None, per_traces: bool = True, single: bool = False) -> list:
"""
Calculate the durations of individual keystrokes.

Parameters:
df (pd.DataFrame): DataFrame containing interaction data with 'event_type', 'timestamp', and 'key' columns.
traces (dict[str, list[pd.DataFrame]]): optional Pre-extracted keystroke traces by session.
per_traces (bool): optional Whether to calculate durations per trace. Default is True.
per_traces (bool): optional whether to calculate durations per trace. Default is True.
single (bool): optional, wether to compute durations fully (all the time the user is typing) or for every single keystroke (value for every single key press and release combination)
Returns:
list: List of keystroke durations in milliseconds.
list[float]: List of keystroke durations in milliseconds.
"""

validate_any_not_none(df, traces)

if traces is None and per_traces:
validate_dataframe_keyboard(df)
traces = extract_keystroke_traces_by_session(df)
return typing_durations_traces(traces, False)
return typing_durations_traces(traces, False, single=single)

if not per_traces:
validate_dataframe_keyboard(df)
return typing_durations_df(df)

return typing_durations_traces(traces)
return typing_durations_traces(traces, single=single)

def typing_speed(df: pd.DataFrame = None, traces: dict[str, list[pd.DataFrame]] = None, per_traces : bool = True) -> dict[list[float]] | float:
"""
Expand All @@ -44,11 +51,15 @@ def typing_speed(df: pd.DataFrame = None, traces: dict[str, list[pd.DataFrame]]
dict (dict[list[float]] | float) : A dictionary with session IDs as keys and lists of typing speeds (CPM) per trace as values, or a float representing the typing speed if per_traces is False.
"""

validate_any_not_none(df, traces)

if per_traces and traces is None:
validate_dataframe_keyboard(df)
traces = extract_keystroke_traces_by_session(df)
return typing_speed_traces(traces, False)

elif not per_traces:
validate_dataframe_keyboard(df)
return typing_speed_df(df)

return typing_speed_traces(traces)
Expand All @@ -68,6 +79,9 @@ def typing_speed_metrics(df: pd.DataFrame = None, traces: dict[str, list[pd.Data
Returns:
dict: A dictionary with session IDs as keys and their corresponding typing speed metrics as values.
"""

validate_any_not_none(df, traces)

if traces is None:
traces = extract_keystroke_traces_by_session(df)

Expand All @@ -85,7 +99,7 @@ def typing_speed_metrics(df: pd.DataFrame = None, traces: dict[str, list[pd.Data
])
metrics_by_session[session_id] = {
"average_typing_speed": avg_speed,
"total_characters": total_chars,
ColumnNames.TOTAL_CHARS: total_chars,
"total_time_seconds": total_time,
"avg_keydown_to_keyup_duration": avg_keydown_to_keyup_duration # TODO Review
}
Expand All @@ -103,6 +117,9 @@ def backspace_usage(df: pd.DataFrame = None, traces: dict[str, list[pd.DataFrame
Returns:
dict: A dictionary with session IDs as keys and their corresponding backspace counts as values.
"""

validate_any_not_none(df, traces)

if traces is None and per_trace:
validate_dataframe_keyboard(df)
traces = extract_keystroke_traces_by_session(df)
Expand Down
9 changes: 9 additions & 0 deletions src/pywib/core/mouse/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Core metrics functions from PyWib
"""
from .mouse import click_slip, number_of_clicks

__all__ = [
"click_slip",
"number_of_clicks",
]
23 changes: 9 additions & 14 deletions src/pywib/core/mouse.py → src/pywib/core/mouse/mouse.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pandas as pd
import numpy as np
from ..utils.validation import validate_dataframe
from ..utils.utils import compute_space_time_diff
from ..utils.segmentation import extract_traces_by_session
from ..constants import ColumnNames, EventTypes

from pywib.utils import validate_dataframe
from pywib.constants import ColumnNames, EventTypes
from pywib.utils.validation import validate_any_not_none

def number_of_clicks(df: pd.DataFrame) -> dict:
"""
Expand All @@ -13,6 +13,7 @@ def number_of_clicks(df: pd.DataFrame) -> dict:
Returns:
dict: A dictionary with session IDs as keys and number of clicks as values.
"""
validate_any_not_none(df)
validate_dataframe(df)

clicks_per_session = {}
Expand All @@ -27,13 +28,17 @@ def click_slip(df: pd.DataFrame, threshold: float = 5.0) -> dict:
A click slip is defined as a click event that occurs within a certain distance
from the previous mouse position (indicating an unintended click).

We define a click slip as: the total path distance of all EVENT_ON_MOUSE_MOVE events that occur between an
EVENT_ON_MOUSE_DOWN and the subsequent EVENT_ON_MOUSE_UP. If that total move-distance is less than `threshold` we count it as a slip.

Parameters:
df (pd.DataFrame): DataFrame containing mouse event data.
threshold (float): Distance threshold to consider a click as a slip.

Returns:
dict: A dictionary with session IDs as keys and the metrics (click slips, max, min, average) as values.
"""
validate_any_not_none(df)
validate_dataframe(df)

click_slips_per_session = {}
Expand All @@ -42,11 +47,6 @@ def click_slip(df: pd.DataFrame, threshold: float = 5.0) -> dict:
for session_id, group in df:
group = group.sort_values(by=ColumnNames.TIME_STAMP)
slips = 0
# We define a click slip as: the total path distance of all
# EVENT_ON_MOUSE_MOVE events that occur between an
# EVENT_ON_MOUSE_DOWN and the subsequent EVENT_ON_MOUSE_UP.
# If that total move-distance is less than `threshold` we count
# it as a slip.
in_down = False
last_move_x = None
last_move_y = None
Expand Down Expand Up @@ -91,11 +91,6 @@ def click_slip(df: pd.DataFrame, threshold: float = 5.0) -> dict:
last_move_y = None
mouse_down_time = None
accumulated_move_distance = 0.0
# TODO this variable is never used??
click_slips_per_session[session_id] = {
"slips": slips,
"distances": distances,
}
metrics = {
ColumnNames.CLICK_SLIPS: slips,
ColumnNames.MAX_CLICK_SLIP: max(distances) if distances else 0,
Expand Down
Loading
Loading