Added Pandas Create from Tensor, Pandas Rename Advanced nodes. Added an option to reindex the column in Pandas Horizontal Concat node.

This commit is contained in:
HowToSD
2025-03-01 16:45:41 -08:00
parent ce52e12a5d
commit 62e4f6efb8
23 changed files with 526 additions and 9 deletions
+6
View File
@@ -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",
+2
View File
@@ -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. |
@@ -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 |
<HR>
Category: IO
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
@@ -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 |
+28
View File
@@ -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 |
<HR>
Category: Transformation
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
+3
View File
@@ -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(
+3
View File
@@ -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()
+3
View File
@@ -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()
+3
View File
@@ -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()
@@ -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()),)
@@ -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,)
@@ -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,)
+44 -2
View File
@@ -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.
+2 -1
View File
@@ -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
+3 -1
View File
@@ -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()
+2 -1
View File
@@ -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()
+2
View File
@@ -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:
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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]
@@ -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()
@@ -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()
@@ -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()
@@ -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()