Skip to content

Conversation

@UnravelSports
Copy link
Owner

@UnravelSports UnravelSports commented May 8, 2025

This PR adds:

Refactor Feature Calculations

A refactor for compute_edge_features_pl, compute_node_features_pl and compute_adjacency_matrix_pl. They are renamed to compute_edge_features, compute_node_features and compute_adjacency_matrix.

They now look like something like this under the hood:

node_features = compute_node_features(
            funcs=self.node_feature_funcs,
            opts=self.feature_opts,
            settings=self.settings,
            **d,
        )

Edge / Node Feature Functions

self.node_feature_funcs and self.edge_feature_funcs are now both a list of functions that should take in **kwargs. For example:

@graph_feature(is_custom=False, feature_type="node")
def x_normed(**kwargs):
    return normalize_between(
        value=kwargs["x"],
        max_value=kwargs["settings"].pitch_dimensions.x_dim.max,
        min_value=kwargs["settings"].pitch_dimensions.x_dim.min,
    )

Passing this x_normed function in SoccerGraphConverterPolars(..., node_feature_funcs=[x_normed]) now computes the normalized x value as defined in the above function.

The default functions that were initially the edge and node features in this package have now been declared as the default features properties (see below).

@property
def default_node_feature_funcs(self) -> list:
    return [
        x_normed,
        y_normed,
        speeds_normed,
        velocity_components_2d_normed,
        distance_to_goal_normed,
        distance_to_ball_normed,
        is_possession_team,
        is_gk,
        is_ball,
        angle_to_goal_components_2d_normed,
        angle_to_ball_components_2d_normed,
        is_ball_carrier,
    ]

@property
def default_edge_feature_funcs(self) -> list:
    return [
        distances_between_players_normed,
        speed_difference_normed,
        angle_between_players_normed,
        velocity_difference_normed,
    ]

Custom Functions

We can also pass custom edge and node feature functions. Any of these functions needs to be decorated with @graph_feature decorator. This decorator takes 2 parameters is_custom and feature_type ("node" or "edge").
This will simply make sure the user consciously adding the correct functions to the correct list (node or edge features).

Additionally we introduce additional_feature_cols to SoccerGraphConverterPolars which allows us to pass columns we've added to our KloppyPolarsData.data dataframe which we can subsequently access through kwargs in our custom functions.

For example:

kloppy_polars_dataset.data = (
    kloppy_polars_dataset.data
    # note, normally you'd join these columns on a frame level
    .with_columns(
        [
            pl.lit(0.45).alias("fake_additional_feature_a"),
            pl.lit(1).alias("fake_graph_feature_a"),
            pl.lit(0.12).alias("fake_graph_feature_b"),
        ]
    )
)

@graph_feature(is_custom=True, feature_type="edge")
def custom_edge_feature(**kwargs):
    return (
        kwargs["fake_additional_feature_a"][None, :]
        + kwargs["fake_additional_feature_a"][:, None]
    )

SoccerGraphConverterPolars(
      dataset=kloppy_polars_dataset,
      additional_feature_cols=["fake_additional_feature_a"],
      edge_feature_funcs=[
          distances_between_players_normed,
          speed_difference_normed,
          angle_between_players_normed,
          velocity_difference_normed,
          custom_edge_feature,
      ],
      chunk_size=2_0000,
      non_potential_receiver_node_value=0.1,
      self_loop_ball=True,
      adjacency_matrix_connect_type="ball",
      adjacency_matrix_type="split_by_team",
      label_type="binary",
      defending_team_node_value=0.0,
      random_seed=False,
      pad=False,
      verbose=False,
  )

This will now add an extra edge feature to our edge feature matrix with value "0.90" for all players.

Custom node feature functions need to return a (N, ) or a (N, k) numpy array. Custom edge features need to return a (N, N) or tuple of multiple (N, N) numpy arrays.


Gobal Features

We also show above how to add global_feature_cols which. These values are joined to the node_features to the "ball" or to "all" by setting global_feature_type (defaults to "ball").


Feature Options

We have also added feature_opts to DefaultGraphConverter. This is a dictionary with additional information we might want to use inside our custom functions. For example, if we want to normalize height in centimeters between 0 and 1, we can add feature_opts={"max_height": 200} and pass it to the SoccerGraphConverterPolars. Subsequently we can access this maximum height in our custom node feature function via kwargs:

@graph_feature(is_custom=True, feature_type="node")
def normalized_height(**kwargs):
    return kwargs['height_cm'] / kwargs['max_height']

For this to work we also need to pass additional_feature_cols=["height_cm"] to SoccerGraphConverterPolars and we would need to join a "height_cm" column to our KloppyPolarsDataset.data otherwise "height_cm" would not be in kwargs.

@UnravelSports UnravelSports merged commit de4511c into main May 8, 2025
3 checks passed
@UnravelSports UnravelSports deleted the feat/feature-select branch June 20, 2025 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants