diff --git a/__init__.py b/__init__.py index 68f9466..a4ee5b3 100644 --- a/__init__.py +++ b/__init__.py @@ -16,6 +16,8 @@ from .modules.matplotlib.mpl_pie_chart import MPLPieChart from .modules.matplotlib.mpl_scatter import MPLScatter from .modules.numpy_wrapper.numpy_float_create import NumpyFloatCreate from .modules.numpy_wrapper.numpy_int_create import NumpyIntCreate +from .modules.numpy_wrapper.numpy_show import NumpyShow +from .modules.numpy_wrapper.numpy_squeeze import NumpySqueeze from .modules.pandas_wrapper.pandas_add import PandasAdd from .modules.pandas_wrapper.pandas_add_scalar_float import PandasAddScalarFloat from .modules.pandas_wrapper.pandas_add_scalar_int import PandasAddScalarInt @@ -46,6 +48,7 @@ from .modules.pandas_wrapper.pandas_div_series import PandasDivSeries from .modules.pandas_wrapper.pandas_drop_duplicates import PandasDropDuplicates from .modules.pandas_wrapper.pandas_dropna import PandasDropNA from .modules.pandas_wrapper.pandas_exp import PandasExp +from .modules.pandas_wrapper.pandas_feature_split_to_numpy import PandasFeatureSplitToNumpy from .modules.pandas_wrapper.pandas_fillna_scalar_float import PandasFillNAScalarFloat from .modules.pandas_wrapper.pandas_fillna_scalar_int import PandasFillNAScalarInt from .modules.pandas_wrapper.pandas_group_by import PandasGroupBy @@ -178,6 +181,7 @@ from .modules.pytorch_wrapper.pt_randn import PtRandn from .modules.pytorch_wrapper.pt_remainder import PtRemainder from .modules.pytorch_wrapper.pt_reshape import PtReshape from .modules.pytorch_wrapper.pt_scatter import PtScatter +from .modules.pytorch_wrapper.pt_show_size import PtShowSize from .modules.pytorch_wrapper.pt_show_text import PtShowText from .modules.pytorch_wrapper.pt_sin import PtSin from .modules.pytorch_wrapper.pt_sinh import PtSinh @@ -228,6 +232,8 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "MPLScatter": MPLScatter, "NumpyFloatCreate": NumpyFloatCreate, "NumpyIntCreate": NumpyIntCreate, + "NumpyShow": NumpyShow, + "NumpySqueeze": NumpySqueeze, "PandasAdd": PandasAdd, "PandasAddScalarFloat": PandasAddScalarFloat, "PandasAddScalarInt": PandasAddScalarInt, @@ -258,6 +264,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasDropDuplicates": PandasDropDuplicates, "PandasDropNA": PandasDropNA, "PandasExp": PandasExp, + "PandasFeatureSplitToNumpy": PandasFeatureSplitToNumpy, "PandasFillNAScalarFloat": PandasFillNAScalarFloat, "PandasFillNAScalarInt": PandasFillNAScalarInt, "PandasGroupBy": PandasGroupBy, @@ -390,6 +397,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PtRemainder": PtRemainder, "PtReshape": PtReshape, "PtScatter": PtScatter, + "PtShowSize": PtShowSize, "PtShowText": PtShowText, "PtSin": PtSin, "PtSinh": PtSinh, @@ -443,6 +451,8 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "MPLScatter": "MPL Scatter Plot", "NumpyFloatCreate": "Numpy Float Create", "NumpyIntCreate": "Numpy Int Create", + "NumpyShow": "Numpy Show", + "NumpySqueeze": "Numpy Squeeze", "PandasAdd": "Pandas Add", "PandasAddScalarFloat": "Pandas Add Scalar Float", "PandasAddScalarInt": "Pandas Add Scalar Int", @@ -473,6 +483,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasDropDuplicates": "Pandas Drop Duplicates", "PandasDropNA": "Pandas Drop NA", "PandasExp": "Pandas Exp", + "PandasFeatureSplitToNumpy": "Pandas Feature Split To Numpy", "PandasFillNAScalarFloat": "Pandas Fill NA Scalar Float", "PandasFillNAScalarInt": "Pandas Fill NA Scalar Int", "PandasGroupBy": "Pandas Group By", @@ -605,6 +616,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PtRemainder": "Pt Remainder", "PtReshape": "Pt Reshape", "PtScatter": "Pt Scatter", + "PtShowSize": "Pt Show Size", "PtShowText": "Pt Show Text", "PtSin": "Pt Sin", "PtSinh": "Pt Sinh", diff --git a/docs/images/pandas_feature_split.png b/docs/images/pandas_feature_split.png new file mode 100644 index 0000000..9e6ffbc Binary files /dev/null and b/docs/images/pandas_feature_split.png differ diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index 0941656..38dd84e 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -89,6 +89,7 @@ | [Pandas Show Series](pandas_show_series.md) | Displays a Pandas Series as text. | | [Pandas Show Text](pandas_show_text.md) | Displays a string. | | [Pandas To String](pandas_to_string.md) | Converts a pandas DataFrame to a string representation. | +| [Pt Show Size](pt_show_size.md) | Displays PyTorch Size object as a string. | | [Pt Show Text](pt_show_text.md) | Displays PyTorch tensor as a string. Note that the tensor is partially printed out when | ## IO | Node | Description | @@ -128,6 +129,8 @@ | --- | --- | | [Numpy Float Create](numpy_float_create.md) | Creates a NumPy ndarray with 32-bit floating point precision | | [Numpy Int Create](numpy_int_create.md) | Creates a NumPy ndarray with 32-bit integer | +| [Numpy Show](numpy_show.md) | Displays a Numpy ndarray as text. | +| [Numpy Squeeze](numpy_squeeze.md) | Removes a dimension at the specified position in the input array if it is of size 1. | ## Plot | Node | Description | | --- | --- | @@ -283,6 +286,7 @@ ## Transformation | Node | Description | | --- | --- | +| [Pandas Feature Split To Numpy](pandas_feature_split_to_numpy.md) | Splits a Pandas DataFrame into feature Numpy ndarray and label Numpy ndarray. | | [Pandas Horizontal Concat](pandas_horizontal_concat.md) | Horizontally concatenates two pandas DataFrames. | | [Pandas Horizontal Split](pandas_horizontal_split.md) | Horizontally splits a Pandas DataFrame into two pandas DataFrames. | | [Pandas Join](pandas_join.md) | Merges two pandas DataFrames based on a common column. | diff --git a/docs/reference/numpy_show.md b/docs/reference/numpy_show.md new file mode 100644 index 0000000..4c5086c --- /dev/null +++ b/docs/reference/numpy_show.md @@ -0,0 +1,17 @@ +# Numpy Show +Displays a Numpy ndarray as text. + +## Input +| Name | Data type | +|---|---| +| ndarray | Ndarray | + +## Output +| Data type | +|---| +| String | + +
+Category: Numpy + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/numpy_squeeze.md b/docs/reference/numpy_squeeze.md new file mode 100644 index 0000000..8445e2e --- /dev/null +++ b/docs/reference/numpy_squeeze.md @@ -0,0 +1,20 @@ +# Numpy Squeeze +Removes a dimension at the specified position in the input array if it is of size 1. + +Specifying a tuple of dimensions is not supported. + +## Input +| Name | Data type | +|---|---| +| array | Ndarray | +| dim | Int | + +## Output +| Data type | +|---| +| Ndarray | + +
+Category: Numpy + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pandas_feature_split_to_numpy.md b/docs/reference/pandas_feature_split_to_numpy.md new file mode 100644 index 0000000..e7ce852 --- /dev/null +++ b/docs/reference/pandas_feature_split_to_numpy.md @@ -0,0 +1,24 @@ +# Pandas Feature Split To Numpy +Splits a Pandas DataFrame into feature Numpy ndarray and label Numpy ndarray. +If output_1d_label is set, label will be output as a 1d array instead of a 2d array. + +As shown in the below screenshot, this node can replace multiple nodes that are used to split Pandas DataFrame into two DataFrames, convert to NumPy, then squeeze the label data. +![Pandas Feature Split To Numpy](../images/pandas_feature_split.png) + +## Input +| Name | Data type | +|---|---| +| dataframe | DataFrame | +| label_integer_position | Int | +| output_1d_label | Boolean | + +## Output +| Data type | +|---| +| Ndarray | +| Ndarray | + +
+Category: Transformation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pt_show_size.md b/docs/reference/pt_show_size.md new file mode 100644 index 0000000..8830fb1 --- /dev/null +++ b/docs/reference/pt_show_size.md @@ -0,0 +1,17 @@ +# Pt Show Size +Displays PyTorch Size object as a string. + +## Input +| Name | Data type | +|---|---| +| size | Ptsize | + +## Output +| Data type | +|---| +| String | + +
+Category: Display data + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/modules/numpy_wrapper/numpy_show.py b/modules/numpy_wrapper/numpy_show.py new file mode 100644 index 0000000..b0aeee3 --- /dev/null +++ b/modules/numpy_wrapper/numpy_show.py @@ -0,0 +1,57 @@ +""" +This code is based on +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/py/show_text.py + +See credit/credit.md for the full license. +""" +import numpy as np + +class NumpyShow: + """ + Displays a Numpy ndarray as text. + + category: Numpy + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "ndarray": ("NDARRAY", {}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = ("STRING",) + FUNCTION = "notify" + OUTPUT_NODE = True + OUTPUT_IS_LIST = (True,) + + CATEGORY = "Data Analysis" + + def notify(self, ndarray, unique_id=None, extra_pnginfo=None): + text = None + if unique_id is not None and extra_pnginfo is not None: + if not isinstance(extra_pnginfo, list): + print("Error: extra_pnginfo is not a list") + elif ( + not isinstance(extra_pnginfo[0], dict) + or "workflow" not in extra_pnginfo[0] + ): + print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key") + else: + ind = ndarray[0] + text = [str(ind)] # wrap in list + workflow = extra_pnginfo[0]["workflow"] + node = next( + (x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])), + None, + ) + if node: + node["widgets_values"] = [text] + else: + print("unique_id or extra_pnginfo is None.") + return {"ui": {"text": text}, "result": (text,)} diff --git a/modules/numpy_wrapper/numpy_squeeze.py b/modules/numpy_wrapper/numpy_squeeze.py new file mode 100644 index 0000000..c4cfa3d --- /dev/null +++ b/modules/numpy_wrapper/numpy_squeeze.py @@ -0,0 +1,45 @@ +from typing import Any, Dict +import numpy as np + + +class NumpySqueeze: + """ + Removes a dimension at the specified position in the input array if it is of size 1. + + Specifying a tuple of dimensions is not supported. + + category: Numpy + """ + + @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": { + "array": ("NDARRAY", {}), + "dim": ("INT", {"default": -1, "min": -1, "max": 10}) + } + } + + RETURN_TYPES: tuple = ("NDARRAY",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, array: np.ndarray, dim: int) -> tuple: + """ + Removes a dimension at the specified position in the input array if it is of size 1. + + Args: + array (np.ndarray): Input array. + dim (int): Dimension to remove if it is of size 1. + + Returns: + tuple: A tuple containing the transformed NumPy array. + """ + ar_out = np.squeeze(array, dim) + return (ar_out,) diff --git a/modules/pandas_wrapper/pandas_feature_split_to_numpy.py b/modules/pandas_wrapper/pandas_feature_split_to_numpy.py new file mode 100644 index 0000000..906507f --- /dev/null +++ b/modules/pandas_wrapper/pandas_feature_split_to_numpy.py @@ -0,0 +1,71 @@ +from typing import Any, Dict +import pandas as pd +import numpy as np + +class PandasFeatureSplitToNumpy: + """ + Splits a Pandas DataFrame into feature Numpy ndarray and label Numpy ndarray. + If output_1d_label is set, label will be output as a 1d array instead of a 2d array. + + As shown in the below screenshot, this node can replace multiple nodes that are used to split Pandas DataFrame into two DataFrames, convert to NumPy, then squeeze the label data. + ![Pandas Feature Split To Numpy](../images/pandas_feature_split.png) + + category: Transformation + """ + + @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", {}), + "label_integer_position": ("INT", {"default": -1, "min": -10, "max": 1024}), + "output_1d_label": ("BOOLEAN", {"default": True}) + } + } + + RETURN_TYPES: tuple = ("NDARRAY", "NDARRAY") + RETURN_NAMES: tuple = ("feature", "label") + + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, dataframe: pd.DataFrame, + label_integer_position: int, + output_1d_label: bool) -> tuple: + """ + Splits a Pandas DataFrame into feature Numpy ndarray and label Numpy ndarray. + + Args: + dataframe (pd.DataFrame): The DataFrame to split. + label_integer_position (int): The column integer position of the label. + output_1d_label (bool): If output_1d_label is set, label will be output as a 1d array instead of a 2d array. + + Returns: + tuple: A tuple containing the feature and label arrays. + """ + num_columns = dataframe.shape[1] + + if label_integer_position < 0: + label_integer_position += num_columns + + # Validate label_integer_position + if label_integer_position >= num_columns: + raise ValueError(f"label_integer_position {label_integer_position} is out of range for dataframe with {num_columns} columns.") + + # Extract features + np_left = dataframe.iloc[:, :label_integer_position].to_numpy() + np_right = dataframe.iloc[:, label_integer_position + 1:].to_numpy() if label_integer_position < num_columns - 1 else np.empty((len(dataframe), 0)) + np_features = np.hstack([np_left, np_right]) if np_right.shape[1] > 0 else np_left + + # Extract label + np_label = dataframe.iloc[:, label_integer_position].to_numpy() + if output_1d_label: + np_label = np_label.ravel() + + return (np_features, np_label) diff --git a/modules/pytorch_wrapper/pt_show_size.py b/modules/pytorch_wrapper/pt_show_size.py new file mode 100644 index 0000000..d8b154a --- /dev/null +++ b/modules/pytorch_wrapper/pt_show_size.py @@ -0,0 +1,60 @@ +""" +Almost all of this code was taken from +https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/py/show_text.py + +See credit/credit.md for the full license. +""" +from typing import List +import torch + + +class PtShowSize: + """ + Displays PyTorch Size object as a string. + + category: Display data + """ + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "size": ("PTSIZE", {}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = ("STRING",) + FUNCTION = "notify" + OUTPUT_NODE = True + OUTPUT_IS_LIST = (True,) + + CATEGORY = "Data Analysis" + + MAX_NUM_ELEMENTS_TO_DISPLAY = 100 + + def notify(self, size: torch.Size, unique_id=None, extra_pnginfo=None): + text = str(size[0]) + + if unique_id is not None and extra_pnginfo is not None: + if not isinstance(extra_pnginfo, list): + print("Error: extra_pnginfo is not a list") + elif ( + not isinstance(extra_pnginfo[0], dict) + or "workflow" not in extra_pnginfo[0] + ): + print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key") + else: + workflow = extra_pnginfo[0]["workflow"] + node = next( + (x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])), + None, + ) + if node: + node["widgets_values"] = [text] + else: + print("unique_id or extra_pnginfo is None.") + return {"ui": {"text": [text]}, "result": ([text],)} diff --git a/pyproject.toml b/pyproject.toml index 4a8d377..3dbe4ad 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.4.2" +version = "1.4.3" license = {file = "LICENSE"} [project.urls] diff --git a/tests/numpy_wrapper/numpy_squeeze_test.py b/tests/numpy_wrapper/numpy_squeeze_test.py new file mode 100644 index 0000000..1d7b10f --- /dev/null +++ b/tests/numpy_wrapper/numpy_squeeze_test.py @@ -0,0 +1,58 @@ +import os +import sys +import unittest +import numpy as np + + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from numpy_wrapper.numpy_squeeze import NumpySqueeze # Importing the NumpySqueeze class + + +class TestNumpySqueeze(unittest.TestCase): + def setUp(self): + """Set up test instance.""" + self.node = NumpySqueeze() + + # Define test cases: (input_array, dim, expected_shape) + self.test_cases = [ + # Case 1: Squeezing a singleton dimension + (np.ones((1, 3, 4)), 0, (3, 4)), # (1,3,4) -> (3,4) + (np.ones((2, 1, 4)), 1, (2, 4)), + (np.ones((100, 1)), 1, (100, )), + + # Case 2: Multiple singleton dimensions, squeezing one + (np.ones((1, 2, 1, 4)), 2, (1, 2, 4)), + + # Case 3: Squeezing a negative index + (np.ones((2, 3, 1)), -1, (2, 3)), + + # Case 4: Squeezing all size 1 dimensions + (np.ones((1, 1, 1)), 0, (1, 1)), + (np.ones((1, 1, 1)), 1, (1, 1)), + (np.ones((1, 1, 1)), 2, (1, 1)), + + # Case 5: Squeezing a 1d array with a single element to a scalar + (np.ones((1,)), 0, ()), + ] + + def test_squeeze(self): + """Test array squeeze operation on various dimensions.""" + for input_array, dim, expected_shape in self.test_cases: + with self.subTest(input_shape=input_array.shape, dim=dim): + result, = self.node.f(input_array, dim) + + # Ensure return value is a array + self.assertTrue(isinstance(result, np.ndarray), "Output is not a array") + + # Ensure result has correct shape + self.assertEqual(result.shape, expected_shape, f"Expected shape {expected_shape}, got {result.shape}") + + # Ensure result has the same dtype + self.assertEqual(result.dtype, input_array.dtype, "Dtype mismatch after squeeze operation") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pandas_wrapper/pandas_feture_split_to_numpy_test.py b/tests/pandas_wrapper/pandas_feture_split_to_numpy_test.py new file mode 100644 index 0000000..7b4f5c6 --- /dev/null +++ b/tests/pandas_wrapper/pandas_feture_split_to_numpy_test.py @@ -0,0 +1,55 @@ +import os +import sys +import unittest +import numpy as np +import pandas as pd + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) + +from pandas_wrapper.pandas_feature_split_to_numpy import PandasFeatureSplitToNumpy + +class TestPandasFeatureSplitToNumpy(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PandasFeatureSplitToNumpy() + + # Sample DataFrame + self.df = pd.DataFrame({ + 'A': [1, 2, 3], + 'B': [4, 5, 6], + 'C': [7, 8, 9] + }) + + def test_feature_split_last_column_label_0(self): + """Test splitting with the last column as the label.""" + features, labels = self.node.f(self.df, label_integer_position=0, output_1d_label=True) + + np.testing.assert_array_equal(features, np.array([[4, 7], [5, 8], [6, 9]])) + np.testing.assert_array_equal(labels, np.array([1, 2, 3])) + + def test_feature_split_last_column_label_1(self): + """Test splitting with the last column as the label.""" + features, labels = self.node.f(self.df, label_integer_position=1, output_1d_label=True) + + np.testing.assert_array_equal(features, np.array([[1, 7], [2, 8], [3, 9]])) + np.testing.assert_array_equal(labels, np.array([4, 5, 6])) + + def test_feature_split_last_column_label_2(self): + """Test splitting with the last column as the label.""" + features, labels = self.node.f(self.df, label_integer_position=2, output_1d_label=True) + + np.testing.assert_array_equal(features, np.array([[1, 4], [2, 5], [3, 6]])) + np.testing.assert_array_equal(labels, np.array([7, 8, 9])) + + def test_feature_split_last_column_label_negative_1(self): + """Test splitting with the last column as the label.""" + features, labels = self.node.f(self.df, label_integer_position=-1, output_1d_label=True) + + np.testing.assert_array_equal(features, np.array([[1, 4], [2, 5], [3, 6]])) + np.testing.assert_array_equal(labels, np.array([7, 8, 9])) + + +if __name__ == "__main__": + unittest.main() diff --git a/web/js/ShowSeries.js b/web/js/ShowTextGeneric.js similarity index 90% rename from web/js/ShowSeries.js rename to web/js/ShowTextGeneric.js index 62e3162..1a68f01 100644 --- a/web/js/ShowSeries.js +++ b/web/js/ShowTextGeneric.js @@ -10,9 +10,9 @@ import { ComfyWidgets } from "../../../scripts/widgets.js"; // Displays input text on a node app.registerExtension({ - name: "HowToSD.ShowSeries", + name: "HowToSD.ShowTextGeneric", async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (["PtShowText", "PandasShowSeries"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python + if (["NumpyShow", "PtShowText", "PtShowSize", "PandasShowSeries"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python function populate(text) { if (this.widgets) { // Clear all widgets on the node