graphomotor.plot.feature_plots

Feature visualization functions for Graphomotor.

This module provides plotting functions for visualizing extracted features from spiral drawing data. The plotting functions expect CSV files with the first 5 columns reserved for metadata (source_file, participant_id, task, hand, start_time), and treat all subsequent columns as numerical features.

Available Features

The graphomotor toolkit extracts 25 features from spiral drawing data. For a complete list of all available features, see the features module documentation.

Custom Features

Users can add custom feature columns to their CSV files alongside the standard graphomotor features. Any additional columns after the first 5 metadata columns will be automatically detected and available for plotting.

Plot Types

  • Distribution plots: Kernel density estimation plots showing feature distributions grouped by task type and hand
  • Trend plots: Line plots displaying feature progression across task sequences
  • Box plots: Box-and-whisker plots comparing distributions across conditions
  • Cluster heatmaps: Hierarchically clustered heatmaps of standardized features
  1"""Feature visualization functions for Graphomotor.
  2
  3This module provides plotting functions for visualizing extracted features from spiral
  4drawing data. The plotting functions expect CSV files with the first 5 columns reserved
  5for metadata (`source_file`, `participant_id`, `task`, `hand`, `start_time`), and treat
  6all subsequent columns as numerical features.
  7
  8Available Features
  9------------------
 10The graphomotor toolkit extracts 25 features from spiral drawing data.
 11For a complete list of all available features, see the
 12[features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
 13
 14Custom Features
 15---------------
 16Users can add custom feature columns to their CSV files alongside the standard
 17graphomotor features. Any additional columns after the first 5 metadata columns
 18will be automatically detected and available for plotting.
 19
 20Plot Types
 21----------
 22- **Distribution plots**: Kernel density estimation plots showing feature distributions
 23  grouped by task type and hand
 24- **Trend plots**: Line plots displaying feature progression across task sequences
 25- **Box plots**: Box-and-whisker plots comparing distributions across conditions
 26- **Cluster heatmaps**: Hierarchically clustered heatmaps of standardized features
 27"""
 28
 29import pathlib
 30import warnings
 31
 32import matplotlib
 33import pandas as pd
 34import seaborn as sns
 35from matplotlib import figure
 36from matplotlib import pyplot as plt
 37
 38from graphomotor.core import config
 39from graphomotor.utils import plotting
 40
 41matplotlib.use("agg")  # prevent interactive matplotlib
 42logger = config.get_logger()
 43
 44
 45def plot_feature_distributions(
 46    data: str | pathlib.Path | pd.DataFrame,
 47    output_path: str | pathlib.Path | None = None,
 48    features: list[str] | None = None,
 49) -> figure.Figure:
 50    """Plot histograms for each feature grouped by task type and hand.
 51
 52    This function creates kernel density estimation plots showing feature distributions
 53    grouped by task type (trace/recall) and hand (Dom/NonDom). The input data should
 54    be a CSV file with the first 5 columns reserved for metadata (`source_file`,
 55    `participant_id`, `task`, `hand`, `start_time`), with all subsequent columns treated
 56    as numerical features.
 57
 58    Both standard graphomotor features and custom feature columns added by users
 59    are supported. For a complete list of the 25 standard features available from
 60    the graphomotor extraction pipeline, see the
 61    [features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
 62
 63    Args:
 64        data: Path to CSV file containing features or pandas DataFrame. Input data
 65            should have the first 5 columns as metadata (`source_file`,
 66            `participant_id`, `task`, `hand`, `start_time`) followed by numerical
 67            feature columns.
 68        output_path: Optional directory where the figure will be saved. If None,
 69            the function only returns the figure without saving.
 70        features: List of specific features to plot, if None plots all features.
 71            Can include any of the 25 standard graphomotor features (see module
 72            docstring) or custom feature columns added to the CSV file.
 73
 74    Returns:
 75        The matplotlib Figure.
 76    """
 77    logger.debug("Starting feature distributions plot generation")
 78
 79    plot_data, features, _ = plotting.prepare_feature_plot_data(data, features)
 80
 81    hands = plot_data["hand"].unique()
 82    task_types = plot_data["task_type"].unique()
 83
 84    colors = {
 85        (hand, task_type): plt.get_cmap("tab20")(i)
 86        for i, (hand, task_type) in enumerate(
 87            [(h, t) for h in hands for t in task_types]
 88        )
 89    }
 90
 91    fig, axes = plotting.init_feature_subplots(len(features))
 92    for i, feature in enumerate(features):
 93        ax = axes[i]
 94
 95        for hand in hands:
 96            for task_type in task_types:
 97                subset = plot_data[
 98                    (plot_data["hand"] == hand) & (plot_data["task_type"] == task_type)
 99                ]
100                sns.kdeplot(
101                    data=subset,
102                    x=feature,
103                    fill=True,
104                    cut=0,
105                    alpha=0.6,
106                    color=colors[(hand, task_type)],
107                    label=f"{hand} - {task_type.capitalize()}",
108                    ax=ax,
109                )
110
111        display_name = plotting.format_feature_name(feature)
112        ax.set_title(display_name)
113        ax.set_xlabel(display_name)
114        ax.set_ylabel("Density")
115        ax.legend(title="Hand - Task Type")
116        ax.grid(alpha=0.3)
117
118    plotting.hide_extra_axes(axes, len(features))
119
120    plt.tight_layout()
121    plt.suptitle(
122        "Feature Distributions across Task Types and Hands",
123        y=1.01,
124        fontsize=10 + len(axes) // 2,
125    )
126
127    if output_path:
128        plotting.save_figure(output_path, "feature_distributions")
129    else:
130        logger.debug("Feature distributions plot generated but not saved")
131
132    return fig
133
134
135def plot_feature_trends(
136    data: str | pathlib.Path | pd.DataFrame,
137    output_path: str | pathlib.Path | None = None,
138    features: list[str] | None = None,
139) -> figure.Figure:
140    """Plot lineplots to compare feature values across conditions per participant.
141
142    This function creates line plots displaying feature progression across task
143    sequences with individual participant trajectories and group means. The input
144    data should be a CSV file with the first 5 columns reserved for metadata
145    (`source_file`, `participant_id`, `task`, `hand`, `start_time`), with all subsequent
146    columns treated as numerical features.
147
148    Both standard graphomotor features and custom feature columns added by users
149    are supported. For a complete list of the 25 standard features available from
150    the graphomotor extraction pipeline, see the
151    [features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
152
153    Args:
154        data: Path to CSV file containing features or pandas DataFrame. Input data
155            should have the first 5 columns as metadata (`source_file`,
156            `participant_id`, `task`, `hand`, `start_time`) followed by numerical
157            feature columns.
158        output_path: Optional directory where the figure will be saved. If None,
159            the function only returns the figure without saving.
160        features: List of specific features to plot, if None plots all features.
161            Can include any of the 25 standard graphomotor features (see module
162            docstring) or custom feature columns added to the CSV file.
163
164    Returns:
165        The matplotlib Figure.
166    """
167    logger.debug("Starting feature trends plot generation")
168
169    plot_data, features, tasks = plotting.prepare_feature_plot_data(data, features)
170    logger.debug(f"Plotting trends across {len(tasks)} tasks")
171
172    fig, axes = plotting.init_feature_subplots(len(features))
173    for i, feature in enumerate(features):
174        ax = axes[i]
175        sns.lineplot(
176            data=plot_data,
177            x="task_order",
178            y=feature,
179            hue="hand",
180            units="participant_id",
181            estimator=None,
182            alpha=0.2,
183            linewidth=0.5,
184            legend=False,
185            ax=ax,
186        )
187        sns.lineplot(
188            data=plot_data,
189            x="task_order",
190            y=feature,
191            hue="hand",
192            estimator="mean",
193            errorbar=None,
194            linewidth=2,
195            marker="o",
196            markersize=4,
197            ax=ax,
198        )
199        display_name = plotting.format_feature_name(feature)
200        ax.set_title(display_name)
201        ax.set_ylabel(display_name)
202        ax.set_xlabel("Task")
203        ax.set_xticks(list(range(1, len(tasks) + 1)))
204        ax.set_xticklabels(tasks, rotation=45, ha="right")
205        ax.legend(title="Hand")
206        ax.grid(alpha=0.3)
207
208    plotting.hide_extra_axes(axes, len(features))
209
210    plt.tight_layout()
211    plt.suptitle(
212        "Feature Trends across Tasks and Hands", y=1.01, fontsize=10 + len(axes) // 2
213    )
214
215    if output_path:
216        plotting.save_figure(output_path, "feature_trends")
217    else:
218        logger.debug("Feature trends plot generated but not saved")
219
220    return fig
221
222
223def plot_feature_boxplots(
224    data: str | pathlib.Path | pd.DataFrame,
225    output_path: str | pathlib.Path | None = None,
226    features: list[str] | None = None,
227) -> figure.Figure:
228    """Plot boxplots to compare feature distributions across conditions.
229
230    This function creates box-and-whisker plots comparing feature distributions
231    across different tasks and hand conditions. The input data should be a CSV
232    file with the first 5 columns reserved for metadata (`source_file`,
233    `participant_id`, `task`, `hand`, `start_time`), with all subsequent columns treated
234    as numerical features.
235
236    Both standard graphomotor features and custom feature columns added by users
237    are supported. For a complete list of the 25 standard features available from
238    the graphomotor extraction pipeline, see the
239    [features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
240
241    Args:
242        data: Path to CSV file containing features or pandas DataFrame. Input data
243            should have the first 5 columns as metadata (`source_file`,
244            `participant_id`, `task`, `hand`, `start_time`) followed by numerical
245            feature columns.
246        output_path: Optional directory where the figure will be saved. If None,
247            the function only returns the figure without saving.
248        features: List of specific features to plot, if None plots all features.
249            Can include any of the 25 standard graphomotor features (see module
250            docstring) or custom feature columns added to the CSV file.
251
252    Returns:
253        The matplotlib Figure.
254    """
255    logger.debug("Starting feature boxplots generation")
256
257    plot_data, features, tasks = plotting.prepare_feature_plot_data(data, features)
258    logger.debug(f"Creating boxplots across {len(tasks)} tasks")
259
260    fig, axes = plotting.init_feature_subplots(len(features))
261    for i, feature in enumerate(features):
262        ax = axes[i]
263
264        # Suppress seaborn's internal deprecation warning about 'vert' parameter
265        with warnings.catch_warnings():
266            warnings.filterwarnings(
267                "ignore",
268                category=PendingDeprecationWarning,
269                message="vert: bool will be deprecated.*",
270            )
271            sns.boxplot(
272                data=plot_data,
273                x="task",
274                y=feature,
275                hue="hand",
276                order=tasks,
277                ax=ax,
278            )
279
280        display_name = plotting.format_feature_name(feature)
281        ax.set_title(display_name)
282        ax.set_ylabel(display_name)
283        ax.set_xlabel("Task")
284        ax.set_xticks(list(range(len(tasks))))
285        ax.set_xticklabels(tasks, rotation=45, ha="right")
286        ax.legend(title="Hand")
287        ax.grid(alpha=0.3)
288
289    plotting.hide_extra_axes(axes, len(features))
290
291    plt.tight_layout()
292    plt.suptitle(
293        "Feature Boxplots across Tasks and Hands", y=1.01, fontsize=10 + len(axes) // 2
294    )
295
296    if output_path:
297        plotting.save_figure(output_path, "feature_boxplots")
298    else:
299        logger.debug("Feature boxplots generated but not saved")
300
301    return fig
302
303
304def plot_feature_clusters(
305    data: str | pathlib.Path | pd.DataFrame,
306    output_path: str | pathlib.Path | None = None,
307    features: list[str] | None = None,
308) -> figure.Figure:
309    """Plot clustered heatmap of standardized feature values across conditions.
310
311    This function creates a hierarchically clustered heatmap that visualizes the median
312    feature values across conditions. Values are z-score standardized across features to
313    allow comparison when features are on different scales. Both features and
314    conditions are hierarchically clustered to highlight groups of similar feature
315    response patterns and conditions that elicit similar profiles.
316
317    The input data should be a CSV file with the first 5 columns reserved for metadata
318    (`source_file`, `participant_id`, `task`, `hand`, `start_time`), with all subsequent
319    columns treated as numerical features. Both standard graphomotor features and custom
320    feature columns added by users are supported. For a complete list of the 25
321    standard features available from the graphomotor extraction pipeline, see the
322    [features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
323
324    Args:
325        data: Path to CSV file containing features or pandas DataFrame. Input data
326            should have the first 5 columns as metadata (`source_file`,
327            `participant_id`, `task`, `hand`, `start_time`) followed by numerical
328            feature columns.
329        output_path: Optional directory where the figure will be saved. If None,
330            the function only returns the figure without saving.
331        features: List of specific features to plot, if None plots all features.
332            Can include any of the 25 standard graphomotor features (see module
333            docstring) or custom feature columns added to the CSV file.
334
335    Returns:
336        The matplotlib Figure.
337
338    Raises:
339        ValueError: If less than 2 features are provided.
340    """
341    logger.debug("Starting feature clusters heatmap generation")
342
343    plot_data, features, _ = plotting.prepare_feature_plot_data(data, features)
344
345    if len(features) < 2:
346        error_msg = (
347            f"At least 2 features required for clustered heatmap, got {len(features)}"
348        )
349        logger.error(error_msg)
350        raise ValueError(error_msg)
351
352    plot_data["condition"] = plot_data["task"] + "_" + plot_data["hand"]
353
354    condition_medians = plot_data.groupby("condition")[features].median()
355
356    heatmap_data = condition_medians.T
357    logger.debug(f"Heatmap data shape: {heatmap_data.shape} for (features, conditions)")
358
359    width = max(10, len(heatmap_data.columns) * 0.8)
360    height = max(6, len(heatmap_data.index) * 0.3)
361
362    grid = sns.clustermap(
363        heatmap_data,
364        z_score=0,
365        figsize=(width, height),
366        cbar_kws={
367            "label": "z-score",
368            "location": "bottom",
369            "orientation": "horizontal",
370        },
371        cbar_pos=(0.025, 0.93, 0.1 + 0.001 * width, 0.02 + 0.001 * height),
372        center=0,
373        cmap="coolwarm",
374        linewidths=0.1,
375        linecolor="black",
376    )
377
378    grid.figure.suptitle(
379        "Feature Clusters Across Conditions",
380        fontsize=14,
381        y=1.01,
382    )
383    grid.ax_heatmap.set_xlabel("Condition")
384    grid.ax_heatmap.set_ylabel("Feature")
385    grid.ax_heatmap.set_yticklabels(grid.ax_heatmap.get_yticklabels(), rotation=0)
386    grid.ax_heatmap.set_xticklabels(
387        grid.ax_heatmap.get_xticklabels(), rotation=45, ha="right"
388    )
389
390    if output_path:
391        plotting.save_figure(output_path, "feature_clusters")
392    else:
393        logger.debug("Feature clusters heatmap generated but not saved")
394
395    return grid.figure
logger = <Logger graphomotor (WARNING)>
def plot_feature_distributions( data: str | pathlib._local.Path | pandas.core.frame.DataFrame, output_path: str | pathlib._local.Path | None = None, features: list[str] | None = None) -> matplotlib.figure.Figure:
 46def plot_feature_distributions(
 47    data: str | pathlib.Path | pd.DataFrame,
 48    output_path: str | pathlib.Path | None = None,
 49    features: list[str] | None = None,
 50) -> figure.Figure:
 51    """Plot histograms for each feature grouped by task type and hand.
 52
 53    This function creates kernel density estimation plots showing feature distributions
 54    grouped by task type (trace/recall) and hand (Dom/NonDom). The input data should
 55    be a CSV file with the first 5 columns reserved for metadata (`source_file`,
 56    `participant_id`, `task`, `hand`, `start_time`), with all subsequent columns treated
 57    as numerical features.
 58
 59    Both standard graphomotor features and custom feature columns added by users
 60    are supported. For a complete list of the 25 standard features available from
 61    the graphomotor extraction pipeline, see the
 62    [features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
 63
 64    Args:
 65        data: Path to CSV file containing features or pandas DataFrame. Input data
 66            should have the first 5 columns as metadata (`source_file`,
 67            `participant_id`, `task`, `hand`, `start_time`) followed by numerical
 68            feature columns.
 69        output_path: Optional directory where the figure will be saved. If None,
 70            the function only returns the figure without saving.
 71        features: List of specific features to plot, if None plots all features.
 72            Can include any of the 25 standard graphomotor features (see module
 73            docstring) or custom feature columns added to the CSV file.
 74
 75    Returns:
 76        The matplotlib Figure.
 77    """
 78    logger.debug("Starting feature distributions plot generation")
 79
 80    plot_data, features, _ = plotting.prepare_feature_plot_data(data, features)
 81
 82    hands = plot_data["hand"].unique()
 83    task_types = plot_data["task_type"].unique()
 84
 85    colors = {
 86        (hand, task_type): plt.get_cmap("tab20")(i)
 87        for i, (hand, task_type) in enumerate(
 88            [(h, t) for h in hands for t in task_types]
 89        )
 90    }
 91
 92    fig, axes = plotting.init_feature_subplots(len(features))
 93    for i, feature in enumerate(features):
 94        ax = axes[i]
 95
 96        for hand in hands:
 97            for task_type in task_types:
 98                subset = plot_data[
 99                    (plot_data["hand"] == hand) & (plot_data["task_type"] == task_type)
100                ]
101                sns.kdeplot(
102                    data=subset,
103                    x=feature,
104                    fill=True,
105                    cut=0,
106                    alpha=0.6,
107                    color=colors[(hand, task_type)],
108                    label=f"{hand} - {task_type.capitalize()}",
109                    ax=ax,
110                )
111
112        display_name = plotting.format_feature_name(feature)
113        ax.set_title(display_name)
114        ax.set_xlabel(display_name)
115        ax.set_ylabel("Density")
116        ax.legend(title="Hand - Task Type")
117        ax.grid(alpha=0.3)
118
119    plotting.hide_extra_axes(axes, len(features))
120
121    plt.tight_layout()
122    plt.suptitle(
123        "Feature Distributions across Task Types and Hands",
124        y=1.01,
125        fontsize=10 + len(axes) // 2,
126    )
127
128    if output_path:
129        plotting.save_figure(output_path, "feature_distributions")
130    else:
131        logger.debug("Feature distributions plot generated but not saved")
132
133    return fig

Plot histograms for each feature grouped by task type and hand.

This function creates kernel density estimation plots showing feature distributions grouped by task type (trace/recall) and hand (Dom/NonDom). The input data should be a CSV file with the first 5 columns reserved for metadata (source_file, participant_id, task, hand, start_time), with all subsequent columns treated as numerical features.

Both standard graphomotor features and custom feature columns added by users are supported. For a complete list of the 25 standard features available from the graphomotor extraction pipeline, see the features module documentation.

Arguments:
  • data: Path to CSV file containing features or pandas DataFrame. Input data should have the first 5 columns as metadata (source_file, participant_id, task, hand, start_time) followed by numerical feature columns.
  • output_path: Optional directory where the figure will be saved. If None, the function only returns the figure without saving.
  • features: List of specific features to plot, if None plots all features. Can include any of the 25 standard graphomotor features (see module docstring) or custom feature columns added to the CSV file.
Returns:

The matplotlib Figure.

def plot_feature_boxplots( data: str | pathlib._local.Path | pandas.core.frame.DataFrame, output_path: str | pathlib._local.Path | None = None, features: list[str] | None = None) -> matplotlib.figure.Figure:
224def plot_feature_boxplots(
225    data: str | pathlib.Path | pd.DataFrame,
226    output_path: str | pathlib.Path | None = None,
227    features: list[str] | None = None,
228) -> figure.Figure:
229    """Plot boxplots to compare feature distributions across conditions.
230
231    This function creates box-and-whisker plots comparing feature distributions
232    across different tasks and hand conditions. The input data should be a CSV
233    file with the first 5 columns reserved for metadata (`source_file`,
234    `participant_id`, `task`, `hand`, `start_time`), with all subsequent columns treated
235    as numerical features.
236
237    Both standard graphomotor features and custom feature columns added by users
238    are supported. For a complete list of the 25 standard features available from
239    the graphomotor extraction pipeline, see the
240    [features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
241
242    Args:
243        data: Path to CSV file containing features or pandas DataFrame. Input data
244            should have the first 5 columns as metadata (`source_file`,
245            `participant_id`, `task`, `hand`, `start_time`) followed by numerical
246            feature columns.
247        output_path: Optional directory where the figure will be saved. If None,
248            the function only returns the figure without saving.
249        features: List of specific features to plot, if None plots all features.
250            Can include any of the 25 standard graphomotor features (see module
251            docstring) or custom feature columns added to the CSV file.
252
253    Returns:
254        The matplotlib Figure.
255    """
256    logger.debug("Starting feature boxplots generation")
257
258    plot_data, features, tasks = plotting.prepare_feature_plot_data(data, features)
259    logger.debug(f"Creating boxplots across {len(tasks)} tasks")
260
261    fig, axes = plotting.init_feature_subplots(len(features))
262    for i, feature in enumerate(features):
263        ax = axes[i]
264
265        # Suppress seaborn's internal deprecation warning about 'vert' parameter
266        with warnings.catch_warnings():
267            warnings.filterwarnings(
268                "ignore",
269                category=PendingDeprecationWarning,
270                message="vert: bool will be deprecated.*",
271            )
272            sns.boxplot(
273                data=plot_data,
274                x="task",
275                y=feature,
276                hue="hand",
277                order=tasks,
278                ax=ax,
279            )
280
281        display_name = plotting.format_feature_name(feature)
282        ax.set_title(display_name)
283        ax.set_ylabel(display_name)
284        ax.set_xlabel("Task")
285        ax.set_xticks(list(range(len(tasks))))
286        ax.set_xticklabels(tasks, rotation=45, ha="right")
287        ax.legend(title="Hand")
288        ax.grid(alpha=0.3)
289
290    plotting.hide_extra_axes(axes, len(features))
291
292    plt.tight_layout()
293    plt.suptitle(
294        "Feature Boxplots across Tasks and Hands", y=1.01, fontsize=10 + len(axes) // 2
295    )
296
297    if output_path:
298        plotting.save_figure(output_path, "feature_boxplots")
299    else:
300        logger.debug("Feature boxplots generated but not saved")
301
302    return fig

Plot boxplots to compare feature distributions across conditions.

This function creates box-and-whisker plots comparing feature distributions across different tasks and hand conditions. The input data should be a CSV file with the first 5 columns reserved for metadata (source_file, participant_id, task, hand, start_time), with all subsequent columns treated as numerical features.

Both standard graphomotor features and custom feature columns added by users are supported. For a complete list of the 25 standard features available from the graphomotor extraction pipeline, see the features module documentation.

Arguments:
  • data: Path to CSV file containing features or pandas DataFrame. Input data should have the first 5 columns as metadata (source_file, participant_id, task, hand, start_time) followed by numerical feature columns.
  • output_path: Optional directory where the figure will be saved. If None, the function only returns the figure without saving.
  • features: List of specific features to plot, if None plots all features. Can include any of the 25 standard graphomotor features (see module docstring) or custom feature columns added to the CSV file.
Returns:

The matplotlib Figure.

def plot_feature_clusters( data: str | pathlib._local.Path | pandas.core.frame.DataFrame, output_path: str | pathlib._local.Path | None = None, features: list[str] | None = None) -> matplotlib.figure.Figure:
305def plot_feature_clusters(
306    data: str | pathlib.Path | pd.DataFrame,
307    output_path: str | pathlib.Path | None = None,
308    features: list[str] | None = None,
309) -> figure.Figure:
310    """Plot clustered heatmap of standardized feature values across conditions.
311
312    This function creates a hierarchically clustered heatmap that visualizes the median
313    feature values across conditions. Values are z-score standardized across features to
314    allow comparison when features are on different scales. Both features and
315    conditions are hierarchically clustered to highlight groups of similar feature
316    response patterns and conditions that elicit similar profiles.
317
318    The input data should be a CSV file with the first 5 columns reserved for metadata
319    (`source_file`, `participant_id`, `task`, `hand`, `start_time`), with all subsequent
320    columns treated as numerical features. Both standard graphomotor features and custom
321    feature columns added by users are supported. For a complete list of the 25
322    standard features available from the graphomotor extraction pipeline, see the
323    [features module documentation](https://childmindresearch.github.io/graphomotor/graphomotor/features.html).
324
325    Args:
326        data: Path to CSV file containing features or pandas DataFrame. Input data
327            should have the first 5 columns as metadata (`source_file`,
328            `participant_id`, `task`, `hand`, `start_time`) followed by numerical
329            feature columns.
330        output_path: Optional directory where the figure will be saved. If None,
331            the function only returns the figure without saving.
332        features: List of specific features to plot, if None plots all features.
333            Can include any of the 25 standard graphomotor features (see module
334            docstring) or custom feature columns added to the CSV file.
335
336    Returns:
337        The matplotlib Figure.
338
339    Raises:
340        ValueError: If less than 2 features are provided.
341    """
342    logger.debug("Starting feature clusters heatmap generation")
343
344    plot_data, features, _ = plotting.prepare_feature_plot_data(data, features)
345
346    if len(features) < 2:
347        error_msg = (
348            f"At least 2 features required for clustered heatmap, got {len(features)}"
349        )
350        logger.error(error_msg)
351        raise ValueError(error_msg)
352
353    plot_data["condition"] = plot_data["task"] + "_" + plot_data["hand"]
354
355    condition_medians = plot_data.groupby("condition")[features].median()
356
357    heatmap_data = condition_medians.T
358    logger.debug(f"Heatmap data shape: {heatmap_data.shape} for (features, conditions)")
359
360    width = max(10, len(heatmap_data.columns) * 0.8)
361    height = max(6, len(heatmap_data.index) * 0.3)
362
363    grid = sns.clustermap(
364        heatmap_data,
365        z_score=0,
366        figsize=(width, height),
367        cbar_kws={
368            "label": "z-score",
369            "location": "bottom",
370            "orientation": "horizontal",
371        },
372        cbar_pos=(0.025, 0.93, 0.1 + 0.001 * width, 0.02 + 0.001 * height),
373        center=0,
374        cmap="coolwarm",
375        linewidths=0.1,
376        linecolor="black",
377    )
378
379    grid.figure.suptitle(
380        "Feature Clusters Across Conditions",
381        fontsize=14,
382        y=1.01,
383    )
384    grid.ax_heatmap.set_xlabel("Condition")
385    grid.ax_heatmap.set_ylabel("Feature")
386    grid.ax_heatmap.set_yticklabels(grid.ax_heatmap.get_yticklabels(), rotation=0)
387    grid.ax_heatmap.set_xticklabels(
388        grid.ax_heatmap.get_xticklabels(), rotation=45, ha="right"
389    )
390
391    if output_path:
392        plotting.save_figure(output_path, "feature_clusters")
393    else:
394        logger.debug("Feature clusters heatmap generated but not saved")
395
396    return grid.figure

Plot clustered heatmap of standardized feature values across conditions.

This function creates a hierarchically clustered heatmap that visualizes the median feature values across conditions. Values are z-score standardized across features to allow comparison when features are on different scales. Both features and conditions are hierarchically clustered to highlight groups of similar feature response patterns and conditions that elicit similar profiles.

The input data should be a CSV file with the first 5 columns reserved for metadata (source_file, participant_id, task, hand, start_time), with all subsequent columns treated as numerical features. Both standard graphomotor features and custom feature columns added by users are supported. For a complete list of the 25 standard features available from the graphomotor extraction pipeline, see the features module documentation.

Arguments:
  • data: Path to CSV file containing features or pandas DataFrame. Input data should have the first 5 columns as metadata (source_file, participant_id, task, hand, start_time) followed by numerical feature columns.
  • output_path: Optional directory where the figure will be saved. If None, the function only returns the figure without saving.
  • features: List of specific features to plot, if None plots all features. Can include any of the 25 standard graphomotor features (see module docstring) or custom feature columns added to the CSV file.
Returns:

The matplotlib Figure.

Raises:
  • ValueError: If less than 2 features are provided.