From 62e4f6efb848a572b4e979d2c66b27c2c1abfea1 Mon Sep 17 00:00:00 2001 From: HowToSD Date: Sat, 1 Mar 2025 16:45:41 -0800 Subject: [PATCH] Added Pandas Create from Tensor, Pandas Rename Advanced nodes. Added an option to reindex the column in Pandas Horizontal Concat node. --- __init__.py | 6 + docs/reference/node_reference.md | 2 + docs/reference/pandas_create_from_tensor.md | 17 +++ docs/reference/pandas_horizontal_concat.md | 3 + docs/reference/pandas_rename_advanced.md | 28 +++++ modules/matplotlib/mpl_bar.py | 3 + modules/matplotlib/mpl_line.py | 3 + modules/matplotlib/mpl_pie_chart.py | 3 + modules/matplotlib/mpl_scatter.py | 3 + .../pandas_create_from_tensor.py | 48 ++++++++ .../pandas_horizontal_concat.py | 14 ++- .../pandas_wrapper/pandas_rename_advanced.py | 72 ++++++++++++ modules/pandas_wrapper/utils.py | 46 +++++++- modules/seaborn_wrapper/sns_bar.py | 3 +- modules/seaborn_wrapper/sns_boxplot.py | 4 +- modules/seaborn_wrapper/sns_line.py | 3 +- modules/seaborn_wrapper/sns_pairplot.py | 2 + modules/seaborn_wrapper/sns_scatter.py | 3 +- pyproject.toml | 2 +- .../pandas_create_from_tensor_test.py | 54 +++++++++ .../pandas_horizontal_concat_test.py | 48 ++++++++ .../pandas_rename_advanced_test.py | 107 ++++++++++++++++++ ...column_label_string_to_target_type_test.py | 61 ++++++++++ 23 files changed, 526 insertions(+), 9 deletions(-) create mode 100644 docs/reference/pandas_create_from_tensor.md create mode 100644 docs/reference/pandas_rename_advanced.md create mode 100644 modules/pandas_wrapper/pandas_create_from_tensor.py create mode 100644 modules/pandas_wrapper/pandas_rename_advanced.py create mode 100644 tests/pandas_wrapper/pandas_create_from_tensor_test.py create mode 100644 tests/pandas_wrapper/pandas_horizontal_concat_test.py create mode 100644 tests/pandas_wrapper/pandas_rename_advanced_test.py create mode 100644 tests/pandas_wrapper/utils_column_label_string_to_target_type_test.py diff --git a/__init__.py b/__init__.py index 460d1af..3a596ea 100644 --- a/__init__.py +++ b/__init__.py @@ -34,6 +34,7 @@ from .modules.pandas_wrapper.pandas_create import PandasCreate from .modules.pandas_wrapper.pandas_create_from_dict import PandasCreateFromDict from .modules.pandas_wrapper.pandas_create_from_multiple_dict import PandasCreateFromMultipleDict from .modules.pandas_wrapper.pandas_create_from_numpy import PandasCreateFromNumpy +from .modules.pandas_wrapper.pandas_create_from_tensor import PandasCreateFromTensor from .modules.pandas_wrapper.pandas_create_series_from_dict import PandasCreateSeriesFromDict from .modules.pandas_wrapper.pandas_create_with_index import PandasCreateWithIndex from .modules.pandas_wrapper.pandas_crosstab import PandasCrosstab @@ -88,6 +89,7 @@ from .modules.pandas_wrapper.pandas_pow_scalar_float import PandasPowScalarFloat from .modules.pandas_wrapper.pandas_pow_scalar_int import PandasPowScalarInt from .modules.pandas_wrapper.pandas_pow_series import PandasPowSeries from .modules.pandas_wrapper.pandas_rename import PandasRename +from .modules.pandas_wrapper.pandas_rename_advanced import PandasRenameAdvanced from .modules.pandas_wrapper.pandas_replace import PandasReplace from .modules.pandas_wrapper.pandas_save_csv import PandasSaveCSV from .modules.pandas_wrapper.pandas_save_json import PandasSaveJSON @@ -254,6 +256,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasCreateFromDict": PandasCreateFromDict, "PandasCreateFromMultipleDict": PandasCreateFromMultipleDict, "PandasCreateFromNumpy": PandasCreateFromNumpy, + "PandasCreateFromTensor": PandasCreateFromTensor, "PandasCreateSeriesFromDict": PandasCreateSeriesFromDict, "PandasCreateWithIndex": PandasCreateWithIndex, "PandasCrosstab": PandasCrosstab, @@ -308,6 +311,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasPowScalarInt": PandasPowScalarInt, "PandasPowSeries": PandasPowSeries, "PandasRename": PandasRename, + "PandasRenameAdvanced": PandasRenameAdvanced, "PandasReplace": PandasReplace, "PandasSaveCSV": PandasSaveCSV, "PandasSaveJSON": PandasSaveJSON, @@ -477,6 +481,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasCreateFromDict": "Pandas Create From Dict", "PandasCreateFromMultipleDict": "Pandas Create From Multiple Dict", "PandasCreateFromNumpy": "Pandas Create From Numpy", + "PandasCreateFromTensor": "Pandas Create From Tensor", "PandasCreateSeriesFromDict": "Pandas Create Series From Dict", "PandasCreateWithIndex": "Pandas Create With Index", "PandasCrosstab": "Pandas Crosstab", @@ -531,6 +536,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasPowScalarInt": "Pandas Pow Scalar Int", "PandasPowSeries": "Pandas Pow Series", "PandasRename": "Pandas Rename", + "PandasRenameAdvanced": "Pandas Rename Advanced", "PandasReplace": "Pandas Replace", "PandasSaveCSV": "Pandas Save CSV", "PandasSaveJSON": "Pandas Save JSON", diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index 81f60c7..285b3fe 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -103,6 +103,7 @@ | [Pandas Create From Dict](pandas_create_from_dict.md) | Creates a pandas DataFrame from a Python dictionary. | | [Pandas Create From Multiple Dict](pandas_create_from_multiple_dict.md) | Creates a pandas DataFrame from multiple Python dictionaries. | | [Pandas Create From Numpy](pandas_create_from_numpy.md) | Creates a pandas DataFrame from a NumPy ndarray. | +| [Pandas Create From Tensor](pandas_create_from_tensor.md) | Creates a pandas DataFrame from a PyTorch tensor. | | [Pandas Create Series From Dict](pandas_create_series_from_dict.md) | Creates a Pandas Series from a Python dictionary. | | [Pandas Create With Index](pandas_create_with_index.md) | Creates a pandas DataFrame using values entered in the text field. Input data is assumed to have an index. | | [Pandas Load CSV](pandas_load_csv.md) | Loads CSV files into a pandas DataFrame. | @@ -297,6 +298,7 @@ | [Pandas Melt](pandas_melt.md) | Performs an unpivot operation (melt) on a pandas DataFrame. | | [Pandas Pivot](pandas_pivot.md) | Computes a pivot table from a pandas DataFrame. | | [Pandas Rename](pandas_rename.md) | Renames an index (row label) or a column label. | +| [Pandas Rename Advanced](pandas_rename_advanced.md) | Renames one or more indices (row labels) or column labels to new labels. | | [Pandas Series To Data Frame](pandas_series_to_dataframe.md) | Converts a Pandas Series to a DataFrame. | | [Pandas Set Index](pandas_set_index.md) | Create index (row label) from existing column or columns. | | [Pandas Sort](pandas_sort.md) | Sorts a pandas DataFrame by a specified column. | diff --git a/docs/reference/pandas_create_from_tensor.md b/docs/reference/pandas_create_from_tensor.md new file mode 100644 index 0000000..866928f --- /dev/null +++ b/docs/reference/pandas_create_from_tensor.md @@ -0,0 +1,17 @@ +# Pandas Create From Tensor +Creates a pandas DataFrame from a PyTorch tensor. + +## Input +| Name | Data type | +|---|---| +| tens | Tensor | + +## Output +| Data type | +|---| +| DataFrame | + +
+Category: IO + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pandas_horizontal_concat.md b/docs/reference/pandas_horizontal_concat.md index 1cb4276..064c1bb 100644 --- a/docs/reference/pandas_horizontal_concat.md +++ b/docs/reference/pandas_horizontal_concat.md @@ -1,11 +1,14 @@ # Pandas Horizontal Concat Horizontally concatenates two pandas DataFrames. +If reset labels is set to True, column labels are reset to a sequential integer index. + ## Input | Name | Data type | |---|---| | a_dataframe | DataFrame | | b_dataframe | DataFrame | +| reset_labels | Boolean | ## Output | Data type | diff --git a/docs/reference/pandas_rename_advanced.md b/docs/reference/pandas_rename_advanced.md new file mode 100644 index 0000000..75a297e --- /dev/null +++ b/docs/reference/pandas_rename_advanced.md @@ -0,0 +1,28 @@ +# Pandas Rename Advanced +Renames one or more indices (row labels) or column labels to new labels. + +Specify the replacement in a Python dictionary format: +``` +{ + "old_foo": "new_bar", + 0: 5 +} +``` +A string label has to be double-quoted. + +## Input +| Name | Data type | +|---|---| +| dataframe | DataFrame | +| axis | | +| replacement_pairs | String | + +## Output +| Data type | +|---| +| DataFrame | + +
+Category: Transformation + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/modules/matplotlib/mpl_bar.py b/modules/matplotlib/mpl_bar.py index 5e2a591..6815dea 100644 --- a/modules/matplotlib/mpl_bar.py +++ b/modules/matplotlib/mpl_bar.py @@ -4,6 +4,7 @@ from io import StringIO import pandas as pd from .common import common_input_types from .util import plot_post_steps +from ..pandas_wrapper.utils import column_label_string_to_target_type class MPLBar: """ @@ -56,6 +57,8 @@ class MPLBar: # Create the plot fig, ax = plt.subplots() + x_column_name = column_label_string_to_target_type(df, x_column_name) + y_column_name = column_label_string_to_target_type(df, y_column_name) ax.bar(df[x_column_name], df[y_column_name]) return plot_post_steps( diff --git a/modules/matplotlib/mpl_line.py b/modules/matplotlib/mpl_line.py index 24077e8..1eebc7b 100644 --- a/modules/matplotlib/mpl_line.py +++ b/modules/matplotlib/mpl_line.py @@ -4,6 +4,7 @@ from io import StringIO import pandas as pd from .common import common_input_types from .util import plot_post_steps +from ..pandas_wrapper.utils import column_label_string_to_target_type class MPLLine: """ @@ -53,6 +54,8 @@ class MPLLine: """ df = dataframe + x_column_name = column_label_string_to_target_type(df, x_column_name) + y_column_name = column_label_string_to_target_type(df, y_column_name) # Create the plot fig, ax = plt.subplots() diff --git a/modules/matplotlib/mpl_pie_chart.py b/modules/matplotlib/mpl_pie_chart.py index 6a3f965..48e66b5 100644 --- a/modules/matplotlib/mpl_pie_chart.py +++ b/modules/matplotlib/mpl_pie_chart.py @@ -2,6 +2,7 @@ from typing import Any, Dict from matplotlib import pyplot as plt import pandas as pd from .util import plot_post_steps +from ..pandas_wrapper.utils import column_label_string_to_target_type class MPLPieChart: """ @@ -54,6 +55,8 @@ class MPLPieChart: """ df = dataframe + label_column_name = column_label_string_to_target_type(df, label_column_name) + data_column_name = column_label_string_to_target_type(df, data_column_name) # Create the plot fig, ax = plt.subplots() diff --git a/modules/matplotlib/mpl_scatter.py b/modules/matplotlib/mpl_scatter.py index 635b604..59b5448 100644 --- a/modules/matplotlib/mpl_scatter.py +++ b/modules/matplotlib/mpl_scatter.py @@ -4,6 +4,7 @@ from io import StringIO import pandas as pd from .common import common_input_types from .util import plot_post_steps +from ..pandas_wrapper.utils import column_label_string_to_target_type class MPLScatter: """ @@ -53,6 +54,8 @@ class MPLScatter: """ df = dataframe + x_column_name = column_label_string_to_target_type(df, x_column_name) + y_column_name = column_label_string_to_target_type(df, y_column_name) # Create the plot fig, ax = plt.subplots() diff --git a/modules/pandas_wrapper/pandas_create_from_tensor.py b/modules/pandas_wrapper/pandas_create_from_tensor.py new file mode 100644 index 0000000..6818c4e --- /dev/null +++ b/modules/pandas_wrapper/pandas_create_from_tensor.py @@ -0,0 +1,48 @@ +from typing import Any, Dict +import pandas as pd +import torch + + +class PandasCreateFromTensor: + """ + Creates a pandas DataFrame from a PyTorch tensor. + + category: IO + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the `create` function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "tens": ("TENSOR", {}), + } + } + + RETURN_TYPES: tuple = ("DATAFRAME",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, tens: torch.Tensor) -> tuple: + """ + Creates a pandas DataFrame from a PyTorch tensor. + + Args: + tens (torch.Tensor): A PyTorch tensor. + + Returns: + tuple: A tuple containing the DataFrame. + + Raises: + ValueError: If the tensor is not rank 1 or rank 2. + """ + if tens.dim() not in [1, 2]: + raise ValueError("Only rank 1 and rank 2 tensors are supported.") + + return (pd.DataFrame(tens.numpy()),) + diff --git a/modules/pandas_wrapper/pandas_horizontal_concat.py b/modules/pandas_wrapper/pandas_horizontal_concat.py index cca6cce..a4cebec 100644 --- a/modules/pandas_wrapper/pandas_horizontal_concat.py +++ b/modules/pandas_wrapper/pandas_horizontal_concat.py @@ -5,6 +5,8 @@ import pandas as pd class PandasHorizontalConcat: """ Horizontally concatenates two pandas DataFrames. + + If reset labels is set to True, column labels are reset to a sequential integer index. category: Transformation """ @@ -20,7 +22,8 @@ class PandasHorizontalConcat: return { "required": { "a_dataframe": ("DATAFRAME", {}), - "b_dataframe": ("DATAFRAME", {}) + "b_dataframe": ("DATAFRAME", {}), + "reset_labels": ("BOOLEAN", {"default": False}) } } @@ -28,16 +31,23 @@ class PandasHorizontalConcat: FUNCTION: str = "f" CATEGORY: str = "Data Analysis" - def f(self, a_dataframe: pd.DataFrame, b_dataframe: pd.DataFrame) -> tuple: + def f(self, + a_dataframe: pd.DataFrame, + b_dataframe: pd.DataFrame, + reset_labels: bool) -> tuple: """ Horizontally concatenate two pandas DataFrames. Args: a_dataframe (DataFrame): The first DataFrame. b_dataframe (DataFrame): The second DataFrame. + reset_labels (bool): If reset labels is set to True, column labels are reset to a sequential integer index. Returns: tuple: A tuple containing the concatenated DataFrame. """ df_out = pd.concat([a_dataframe, b_dataframe], axis=1) + if reset_labels: + df_out.columns = range(df_out.shape[1]) + return (df_out,) diff --git a/modules/pandas_wrapper/pandas_rename_advanced.py b/modules/pandas_wrapper/pandas_rename_advanced.py new file mode 100644 index 0000000..af0fee1 --- /dev/null +++ b/modules/pandas_wrapper/pandas_rename_advanced.py @@ -0,0 +1,72 @@ +from typing import Any, Dict +import pandas as pd +import ast + +class PandasRenameAdvanced: + """ + Renames one or more indices (row labels) or column labels to new labels. + + Specify the replacement in a Python dictionary format: + ``` + { + "old_foo": "new_bar", + 0: 5 + } + ``` + A string label has to be double-quoted. + + 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", {}), + "axis": (("columns", "index"),), + "replacement_pairs": ("STRING", {"default":"{}", "multiline": True}) + } + } + + RETURN_TYPES: tuple = ("DATAFRAME",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, dataframe: pd.DataFrame, axis: str, replacement_pairs: str) -> tuple: + """ + Renames one or more indices (row labels) or column labels to new labels. + + Args: + dataframe (DataFrame): The DataFrame. + axis (str): Index (row label) or column label. + replacement_pairs (str): Replacement pairs specified in the Python dictionary format. + + Returns: + tuple: A tuple containing the DataFrame. + """ + try: + d = ast.literal_eval(replacement_pairs) + if not isinstance(d, dict): + raise ValueError("replacement_pairs must be a dictionary.") + + # Check that values in the dictionary are not lists, tuples, or dictionaries + if any(isinstance(value, (list, tuple, dict)) for value in d.values()): + raise ValueError("replacement_pairs values must not be lists, tuples, or dictionaries.") + + except (SyntaxError, ValueError) as e: + raise ValueError(f"Invalid replacement_pairs format: {e}") + + if axis == "index": + df_out = dataframe.rename(index=d) + elif axis == "columns": + df_out = dataframe.rename(columns=d) + else: + raise ValueError("Invalid axis. Only 'index' or 'columns' are supported.") + + return (df_out,) diff --git a/modules/pandas_wrapper/utils.py b/modules/pandas_wrapper/utils.py index 1113be3..b1f27a8 100644 --- a/modules/pandas_wrapper/utils.py +++ b/modules/pandas_wrapper/utils.py @@ -1,4 +1,4 @@ -from typing import Any, List, Tuple, Union, Set +from typing import Any, List, Tuple, Union, Set, Optional import json import numpy as np import pandas as pd @@ -54,7 +54,7 @@ def column_labels_string_to_list(dataframe:pd.DataFrame, s:str, no_check:bool=Fa Args: dataframe (pd.DataFrame): The DataFrame to validate column names against. s (str): A comma-separated string representing column labels. - no_check (str): If True, input string is processed as a string without checking the column labels + no_check (bool): If True, input string is processed as a string without checking the column labels of the DataFrame. Returns: @@ -84,6 +84,48 @@ def column_labels_string_to_list(dataframe:pd.DataFrame, s:str, no_check:bool=Fa return output_columns +def column_label_string_to_target_type(dataframe: pd.DataFrame, label: str, return_none: bool=False) -> Optional[Union[str, int, float]]: + """ + Converts a string column label into the data type used for the corresponding label in the DataFrame. + + If a column name does not exist in the DataFrame, a ValueError is raised. + + Args: + dataframe (pd.DataFrame): The DataFrame to validate column names against. + label (str): A string representing a column label. + return_none (bool): If True, if the label is not found, None is returned instead of raising an exception. + Returns: + Union[str, int, float]: The column label in the correct type used in the DataFrame. + + Raises: + ValueError: If `s` does not match any existing column name. + """ + valid_columns = flatten_list_tuple_unique(dataframe.columns) + s = label.strip() + + if s in valid_columns: + return s + + try: + numeric_c = int(s) + if numeric_c in valid_columns: + return numeric_c + except ValueError: + pass + + try: + numeric_c = float(s) + if numeric_c in valid_columns: + return numeric_c + except ValueError: + pass + + if return_none: + return None + + raise ValueError(f"Column '{s}' not found in the DataFrame.") + + def series_to_jsons(series: pd.Series) -> str: """ Converts a pandas Series to a JSON string. diff --git a/modules/seaborn_wrapper/sns_bar.py b/modules/seaborn_wrapper/sns_bar.py index 7997192..70f1664 100644 --- a/modules/seaborn_wrapper/sns_bar.py +++ b/modules/seaborn_wrapper/sns_bar.py @@ -6,7 +6,7 @@ from .common import common_input_types from .util import plot_post_steps from .util import set_up_sns_style_palette from .util import process_y_columns_and_labels - +from ..pandas_wrapper.utils import column_label_string_to_target_type class SNSBar: """ @@ -63,6 +63,7 @@ class SNSBar: """ df = dataframe set_up_sns_style_palette(style, palette) + x_column_name = column_label_string_to_target_type(df, x_column_name) y_columns, labels = process_y_columns_and_labels(df, y_column_name, legend_label) # Create the plot diff --git a/modules/seaborn_wrapper/sns_boxplot.py b/modules/seaborn_wrapper/sns_boxplot.py index 0dac6df..e4571d0 100644 --- a/modules/seaborn_wrapper/sns_boxplot.py +++ b/modules/seaborn_wrapper/sns_boxplot.py @@ -4,7 +4,7 @@ import pandas as pd import seaborn as sns from .util import plot_post_steps from .util import set_up_sns_style_palette - +from ..pandas_wrapper.utils import column_label_string_to_target_type class SNSBoxplot: """ @@ -81,6 +81,8 @@ class SNSBoxplot: """ df = dataframe set_up_sns_style_palette(style, palette) + category_column_name = column_label_string_to_target_type(df, category_column_name, return_none=True) + value_column_name = column_label_string_to_target_type(df, value_column_name) # Create the plot fig, ax = plt.subplots() diff --git a/modules/seaborn_wrapper/sns_line.py b/modules/seaborn_wrapper/sns_line.py index d4c85fa..ef27049 100644 --- a/modules/seaborn_wrapper/sns_line.py +++ b/modules/seaborn_wrapper/sns_line.py @@ -6,7 +6,7 @@ from .common import common_input_types from .util import plot_post_steps from .util import set_up_sns_style_palette from .util import process_y_columns_and_labels - +from ..pandas_wrapper.utils import column_label_string_to_target_type class SNSLine: """ @@ -83,6 +83,7 @@ class SNSLine: """ df = dataframe set_up_sns_style_palette(style, palette) + x_column_name = column_label_string_to_target_type(df, x_column_name) y_columns, labels = process_y_columns_and_labels(df, y_column_name, legend_label) fig, ax = plt.subplots() diff --git a/modules/seaborn_wrapper/sns_pairplot.py b/modules/seaborn_wrapper/sns_pairplot.py index f22ae13..11ecbab 100644 --- a/modules/seaborn_wrapper/sns_pairplot.py +++ b/modules/seaborn_wrapper/sns_pairplot.py @@ -4,6 +4,7 @@ import pandas as pd import seaborn as sns from .util import plot_post_steps from .util import set_up_sns_style_palette +from ..pandas_wrapper.utils import column_label_string_to_target_type class SNSPairplot: @@ -60,6 +61,7 @@ class SNSPairplot: set_up_sns_style_palette(style, palette) # Create the plot + hue_column_name = column_label_string_to_target_type(df, hue_column_name, return_none=True) if hue_column_name: pp = sns.pairplot(df, hue=hue_column_name) else: diff --git a/modules/seaborn_wrapper/sns_scatter.py b/modules/seaborn_wrapper/sns_scatter.py index b069ec3..8e398c6 100644 --- a/modules/seaborn_wrapper/sns_scatter.py +++ b/modules/seaborn_wrapper/sns_scatter.py @@ -6,7 +6,7 @@ from .common import common_input_types from .util import plot_post_steps from .util import set_up_sns_style_palette from .util import process_y_columns_and_labels - +from ..pandas_wrapper.utils import column_label_string_to_target_type class SNSScatter: """ @@ -63,6 +63,7 @@ class SNSScatter: """ df = dataframe set_up_sns_style_palette(style, palette) + x_column_name = column_label_string_to_target_type(df, x_column_name) y_columns, labels = process_y_columns_and_labels(df, y_column_name, legend_label) # Create the plot diff --git a/pyproject.toml b/pyproject.toml index f100b04..01503b6 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.5" +version = "1.4.6" license = {file = "LICENSE"} [project.urls] diff --git a/tests/pandas_wrapper/pandas_create_from_tensor_test.py b/tests/pandas_wrapper/pandas_create_from_tensor_test.py new file mode 100644 index 0000000..64fdac9 --- /dev/null +++ b/tests/pandas_wrapper/pandas_create_from_tensor_test.py @@ -0,0 +1,54 @@ +import os +import sys +import unittest +import torch +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_create_from_tensor import PandasCreateFromTensor + +class TestPandasCreateFromTensor(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PandasCreateFromTensor() + + self.test_cases = [ + # Rank 0 (Invalid) + (torch.tensor(42), ValueError), + (torch.tensor(3.14), ValueError), + (torch.tensor(True), ValueError), + + # Rank 1 (Valid) + (torch.tensor([1, 2, 3], dtype=torch.float32), pd.DataFrame([1, 2, 3], dtype="float32")), + (torch.tensor([1.1, 2.2, 3.3], dtype=torch.float32), pd.DataFrame([1.1, 2.2, 3.3], dtype="float32")), + (torch.tensor([True, False, True], dtype=torch.float32), pd.DataFrame([1.0, 0.0, 1.0], dtype="float32")), + + # Rank 2 (Valid) + (torch.tensor([[1, 2], [3, 4]], dtype=torch.float32), pd.DataFrame([[1, 2], [3, 4]], dtype="float32")), + (torch.tensor([[1.1, 2.2], [3.3, 4.4]], dtype=torch.float32), pd.DataFrame([[1.1, 2.2], [3.3, 4.4]], dtype="float32")), + + # Rank 3+ (Invalid) + (torch.rand(2, 2, 2), ValueError), + (torch.rand(2, 2, 2, 2), ValueError), + ] + + def test_create_dataframe(self): + """Test DataFrame creation from various PyTorch tensors.""" + for data, expected in self.test_cases: + with self.subTest(data=data): + if isinstance(expected, type) and issubclass(expected, Exception): + with self.assertRaises(expected): + self.node.f(data) + else: + retval = self.node.f(data)[0] + + # Test if DataFrame structure matches + self.assertTrue(isinstance(retval, pd.DataFrame)) + + # Test if data matches + pd.testing.assert_frame_equal(retval, expected) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pandas_wrapper/pandas_horizontal_concat_test.py b/tests/pandas_wrapper/pandas_horizontal_concat_test.py new file mode 100644 index 0000000..a235871 --- /dev/null +++ b/tests/pandas_wrapper/pandas_horizontal_concat_test.py @@ -0,0 +1,48 @@ +import os +import sys +import unittest +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_horizontal_concat import PandasHorizontalConcat + + +class TestPandasHorizontalConcat(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PandasHorizontalConcat() + + # Sample DataFrames + self.df1 = pd.DataFrame({"A": [1, 2], "B": [3, 4]}) + self.df2 = pd.DataFrame({"C": [5, 6], "D": [7, 8]}) + + def test_concat_without_reset_labels(self): + """Test horizontal concatenation without resetting column labels.""" + expected_df = pd.DataFrame({ + "A": [1, 2], + "B": [3, 4], + "C": [5, 6], + "D": [7, 8] + }) + + retval = self.node.f(self.df1, self.df2, reset_labels=False)[0] + pd.testing.assert_frame_equal(retval, expected_df) + + def test_concat_with_reset_labels(self): + """Test horizontal concatenation with resetting column labels.""" + expected_df = pd.DataFrame({ + 0: [1, 2], + 1: [3, 4], + 2: [5, 6], + 3: [7, 8] + }) + + retval = self.node.f(self.df1, self.df2, reset_labels=True)[0] + pd.testing.assert_frame_equal(retval, expected_df) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pandas_wrapper/pandas_rename_advanced_test.py b/tests/pandas_wrapper/pandas_rename_advanced_test.py new file mode 100644 index 0000000..773a80c --- /dev/null +++ b/tests/pandas_wrapper/pandas_rename_advanced_test.py @@ -0,0 +1,107 @@ +import os +import sys +import unittest +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_rename_advanced import PandasRenameAdvanced + +class TestPandasRenameAdvanced(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PandasRenameAdvanced() + + # Sample DataFrame + self.df = pd.DataFrame( + { + "A": [1, 2, 3], + "B": [4, 5, 6], + 100: [7, 8, 9], + }, + index=["row1", "row2", 10] + ) + + def test_rename_columns_string(self): + """Test renaming string column labels.""" + replacement_pairs = '{"A": "X", "B": "Y"}' + expected_df = pd.DataFrame( + { + "X": [1, 2, 3], + "Y": [4, 5, 6], + 100: [7, 8, 9], + }, + index=["row1", "row2", 10] + ) + result_df = self.node.f(self.df, "columns", replacement_pairs)[0] + pd.testing.assert_frame_equal(result_df, expected_df) + + def test_rename_columns_integer(self): + """Test renaming integer column labels.""" + replacement_pairs = '{100: 200}' + expected_df = pd.DataFrame( + { + "A": [1, 2, 3], + "B": [4, 5, 6], + 200: [7, 8, 9], + }, + index=["row1", "row2", 10] + ) + result_df = self.node.f(self.df, "columns", replacement_pairs)[0] + pd.testing.assert_frame_equal(result_df, expected_df) + + def test_rename_index_string(self): + """Test renaming string row labels.""" + replacement_pairs = '{"row1": "new_row1", "row2": "new_row2"}' + expected_df = pd.DataFrame( + { + "A": [1, 2, 3], + "B": [4, 5, 6], + 100: [7, 8, 9], + }, + index=["new_row1", "new_row2", 10] + ) + result_df = self.node.f(self.df, "index", replacement_pairs)[0] + pd.testing.assert_frame_equal(result_df, expected_df) + + def test_rename_index_integer(self): + """Test renaming integer row labels.""" + replacement_pairs = '{10: 20}' + expected_df = pd.DataFrame( + { + "A": [1, 2, 3], + "B": [4, 5, 6], + 100: [7, 8, 9], + }, + index=["row1", "row2", 20] + ) + result_df = self.node.f(self.df, "index", replacement_pairs)[0] + pd.testing.assert_frame_equal(result_df, expected_df) + + def test_invalid_replacement_pairs_list(self): + """Test invalid replacement_pairs format.""" + invalid_pairs = '{"A": ["X", "Y"]}' # Not a valid mapping. List is not allowed. + with self.assertRaises(ValueError): + self.node.f(self.df, "columns", invalid_pairs) + + def test_invalid_replacement_pairs_dict(self): + """Test invalid replacement_pairs format.""" + invalid_pairs = '{"A": {"X": "Y"}}' + with self.assertRaises(ValueError): + self.node.f(self.df, "columns", invalid_pairs) + + def test_invalid_replacement_pairs_tuple(self): + """Test invalid replacement_pairs format.""" + invalid_pairs = '{"A": ()"X", "Y")}' + with self.assertRaises(ValueError): + self.node.f(self.df, "columns", invalid_pairs) + + def test_invalid_axis(self): + """Test invalid axis value.""" + replacement_pairs = '{"A": "X"}' + with self.assertRaises(ValueError): + self.node.f(self.df, "invalid_axis", replacement_pairs) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pandas_wrapper/utils_column_label_string_to_target_type_test.py b/tests/pandas_wrapper/utils_column_label_string_to_target_type_test.py new file mode 100644 index 0000000..0754d06 --- /dev/null +++ b/tests/pandas_wrapper/utils_column_label_string_to_target_type_test.py @@ -0,0 +1,61 @@ +import os +import sys +import unittest +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.utils import column_label_string_to_target_type + +class TestColumnLabelStringToTargetType(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.df = pd.DataFrame({ + "A": [1, 2, 3], + "B": [4, 5, 6], + "C": [7, 8, 9], + 10: [10, 11, 12], # Integer column name + 20: [13, 14, 15], # Another integer column name + 3.5: [16, 17, 18] # Float column name + }) + + def test_valid_string_column(self): + """Test valid string column names remain unchanged.""" + self.assertEqual(column_label_string_to_target_type(self.df, "A"), "A") + self.assertEqual(column_label_string_to_target_type(self.df, "B"), "B") + + def test_integer_column_labels(self): + """Test conversion when integer column names are referenced as strings.""" + self.assertEqual(column_label_string_to_target_type(self.df, "10"), 10) + self.assertEqual(column_label_string_to_target_type(self.df, "20"), 20) + + def test_float_column_label(self): + """Test conversion when float column names are referenced as strings.""" + self.assertEqual(column_label_string_to_target_type(self.df, "3.5"), 3.5) + + def test_whitespace_handling(self): + """Test that spaces around column names are stripped before conversion.""" + self.assertEqual(column_label_string_to_target_type(self.df, " A "), "A") + self.assertEqual(column_label_string_to_target_type(self.df, " 10 "), 10) + self.assertEqual(column_label_string_to_target_type(self.df, " 3.5 "), 3.5) + + def test_non_existent_column(self): + """Test behavior when a non-existent column is provided.""" + with self.assertRaises(ValueError) as context: + column_label_string_to_target_type(self.df, "D") + self.assertIn("Column 'D' not found in the DataFrame.", str(context.exception)) + + def test_non_existent_column_with_return_none(self): + """Test behavior when a non-existent column is provided.""" + retval = column_label_string_to_target_type(self.df, "D", return_none=True) + self.assertTrue(retval is None) + + def test_invalid_integer_conversion(self): + """Test that a string that can't be converted to an integer or float raises ValueError.""" + with self.assertRaises(ValueError) as context: + column_label_string_to_target_type(self.df, "xyz") + self.assertIn("Column 'xyz' not found in the DataFrame.", str(context.exception)) + +if __name__ == "__main__": + unittest.main()