From 56c44da54d474d180d38002de408b014c3e59ec1 Mon Sep 17 00:00:00 2001 From: HowToSD Date: Fri, 21 Feb 2025 19:04:11 -0800 Subject: [PATCH] Added Pandas XS node. --- __init__.py | 3 + docs/reference/node_reference.md | 1 + docs/reference/pandas_xs.md | 44 +++++++ .../pandas_loc_row_multiindex_dataframe.py | 18 +-- modules/pandas_wrapper/pandas_xs.py | 112 ++++++++++++++++++ pyproject.toml | 2 +- 6 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 docs/reference/pandas_xs.md create mode 100644 modules/pandas_wrapper/pandas_xs.py diff --git a/__init__.py b/__init__.py index 97f1ba4..2c2583b 100644 --- a/__init__.py +++ b/__init__.py @@ -95,6 +95,7 @@ from .modules.pandas_wrapper.pandas_transpose import PandasTranspose from .modules.pandas_wrapper.pandas_var import PandasVar from .modules.pandas_wrapper.pandas_vertical_concat import PandasVerticalConcat from .modules.pandas_wrapper.pandas_vertical_split import PandasVerticalSplit +from .modules.pandas_wrapper.pandas_xs import PandasXs T = TypeVar("T") @@ -199,6 +200,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasVar": PandasVar, "PandasVerticalConcat": PandasVerticalConcat, "PandasVerticalSplit": PandasVerticalSplit, + "PandasXs": PandasXs, } @@ -303,6 +305,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasVar": "Pandas Var", "PandasVerticalConcat": "Pandas Vertical Concat", "PandasVerticalSplit": "Pandas Vertical Split", + "PandasXs": "Pandas Xs", } diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index ba3e527..e60bcea 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -53,6 +53,7 @@ | [Pandas Loc Row Series](pandas_loc_row_series.md) | Selects a row from a pandas DataFrame and output as a Series. | | [Pandas Select Columns](pandas_select_columns.md) | Selects specific columns from a pandas DataFrame. | | [Pandas Select Rows](pandas_select_rows.md) | Selects specific rows from a pandas DataFrame based on a condition. | +| [Pandas Xs](pandas_xs.md) | Selects a subset of a Pandas DataFrame using specified index labels or positions. | ## Data type conversion | Node | Description | | --- | --- | diff --git a/docs/reference/pandas_xs.md b/docs/reference/pandas_xs.md new file mode 100644 index 0000000..4b3512f --- /dev/null +++ b/docs/reference/pandas_xs.md @@ -0,0 +1,44 @@ +# Pandas Xs +Selects a subset of a Pandas DataFrame using specified index labels or positions. + +This function extracts a portion of a DataFrame based on MultiIndex labels (row indices or column labels). + +Users must specify: +* The labels to select (comma-separated for multiple labels). +* The level(s) within the MultiIndex (integer positions). +* The axis (whether selecting from rows or columns). + +**Understanding "Level":** +In a MultiIndex DataFrame, a level refers to the hierarchical position of an index within the MultiIndex, which can be referenced using a 0-based integer position or an assigned level name. +Selecting the level using an integer position is similar to integer-based selection in `iloc`. + +When specifying a label, Pandas needs to know which level of the MultiIndex to search. +For example, if selecting "dog" as a row label and the DataFrame has multiple index levels, +you must indicate whether to look in the first index (level 0) or the second index (level 1). + +If selecting multiple labels, their corresponding levels should be provided as a comma-separated list. + +Example: + Given a DataFrame with a 3-level MultiIndex, the first index is at level 0, the second at level 1, and the third at level 2. Selecting "dog" and "brown" at levels 1 and 2 requires specifying 1, 2 in the Level field of the node. These values are internally converted into a list and passed to Pandas. + +The `axis` field specifies whether selection applies to row indices (`axis=0`) or column labels (`axis=1`). + +## Input +| Name | Data type | +|---|---| +| dataframe | DataFrame | +| row_or_column_label | String | +| label_type | | +| level | String | +| level_type | | +| axis | | + +## Output +| Data type | +|---| +| DataFrame | + +
+Category: Data subset selection + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/modules/pandas_wrapper/pandas_loc_row_multiindex_dataframe.py b/modules/pandas_wrapper/pandas_loc_row_multiindex_dataframe.py index 4fdd5a5..e34c177 100644 --- a/modules/pandas_wrapper/pandas_loc_row_multiindex_dataframe.py +++ b/modules/pandas_wrapper/pandas_loc_row_multiindex_dataframe.py @@ -60,12 +60,14 @@ class PandasLocRowMultiIndexDataFrame: ind_list = [int(ind) for ind in ind_list] ind_tuple = tuple(ind_list) # Convert the list to a tuple - if len(ind_tuple) == 1: # Do not enclose in tuple if only 1 index is specified - ind = ind_tuple[0] - else: - ind = ind_tuple + # Ensure partial indexing is supported by adding slice(None) for missing levels + if len(ind_tuple) < dataframe.index.nlevels: + filler_count = dataframe.index.nlevels - len(ind_tuple) + for _ in range(filler_count): + ind_tuple += (slice(None),) - # We need to wrap it to get the DataFrame. Otherwise, we will get a Series. - ind_list = [ind] - value = dataframe.loc[ind_list] - return (value,) + result = dataframe.loc[ind_tuple] + if isinstance(result, pd.Series): # loc can return Series so convert to DataFrame + result = result.to_frame().T + + return (result,) diff --git a/modules/pandas_wrapper/pandas_xs.py b/modules/pandas_wrapper/pandas_xs.py new file mode 100644 index 0000000..bc4655d --- /dev/null +++ b/modules/pandas_wrapper/pandas_xs.py @@ -0,0 +1,112 @@ +from typing import Any, Dict +import pandas as pd + +class PandasXs: + """ + Selects a subset of a Pandas DataFrame using specified index labels or positions. + + This function extracts a portion of a DataFrame based on MultiIndex labels (row indices or column labels). + + Users must specify: + * The labels to select (comma-separated for multiple labels). + * The level(s) within the MultiIndex (integer positions). + * The axis (whether selecting from rows or columns). + + **Understanding "Level":** + In a MultiIndex DataFrame, a level refers to the hierarchical position of an index within the MultiIndex, which can be referenced using a 0-based integer position or an assigned level name. + Selecting the level using an integer position is similar to integer-based selection in `iloc`. + + When specifying a label, Pandas needs to know which level of the MultiIndex to search. + For example, if selecting "dog" as a row label and the DataFrame has multiple index levels, + you must indicate whether to look in the first index (level 0) or the second index (level 1). + + If selecting multiple labels, their corresponding levels should be provided as a comma-separated list. + + Example: + Given a DataFrame with a 3-level MultiIndex, the first index is at level 0, the second at level 1, and the third at level 2. Selecting "dog" and "brown" at levels 1 and 2 requires specifying 1, 2 in the Level field of the node. These values are internally converted into a list and passed to Pandas. + + The `axis` field specifies whether selection applies to row indices (`axis=0`) or column labels (`axis=1`). + + category: Data subset selection + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "dataframe": ("DATAFRAME", {}), + "row_or_column_label": ("STRING", {"default": ""}), + "label_type":(("string", "int"),), + "level": ("STRING", {"default": ""}), + "level_type":(("int", "string"),), + "axis": (("index", "columns"),) + } + } + + RETURN_TYPES: tuple = ("DATAFRAME",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, dataframe: pd.DataFrame, + row_or_column_label: str, + label_type: str, + level: str, + level_type: str, + axis: str) -> tuple: + """ + Selects a subset of a Pandas DataFrame using specified index labels or positions. + + Args: + dataframe (DataFrame): The DataFrame. + row_or_column (str): A comma-separated MultiIndex label for the row or label. + label_type (str): The data type of the row index or column label. + level (str): A comma-separated level list for the MultiIndex to select. + level_type (str): The data type of the level. + axis (str): Specifies whether labels are for a row or column. + + Returns: + tuple: A tuple containing the Pandas DataFrame. + """ + # Extract index list + # We need to pass a tuple of MultiIndex. + # e.g. if the first index is A, second is B, we need to pass: + # ("A", "B") + ind_list = [e.strip() for e in row_or_column_label.split(",")] + + # This will throw an exception if the MultiIndex is not an int. + if label_type == "int": + ind_list = [int(e) for e in ind_list] + + ind_tuple = tuple(ind_list) # Convert the list to a tuple + if len(ind_tuple) == 1: # Do not enclose in tuple if only 1 index is specified + ind = ind_tuple[0] + else: + ind = ind_tuple + + # Extract level list + if len(level) == 0: + level = None + else: + level_list = [e.strip() for e in level.split(",")] + + # This will throw an exception if the MultiIndex is not an int. + if level_type == "int": + level_list = [int(e) for e in level_list] + + if len(level_list) == 1: + level = level_list[0] + else: + level = level_list + + df_out = dataframe.sort_index().xs( + ind, + level=level, + axis=axis + ) + return (df_out,) diff --git a/pyproject.toml b/pyproject.toml index 5df23da..4cd4f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "data-analysis" description = "Data analysis custom modules for ComfyUI - Use Pandas & Matplotlib within ComfyUI" -version = "1.1.17" +version = "1.1.18" license = {file = "LICENSE"} [project.urls]