diff --git a/__init__.py b/__init__.py
index 732f849..fb125a8 100644
--- a/__init__.py
+++ b/__init__.py
@@ -66,6 +66,7 @@ from .modules.pandas_wrapper.pandas_mul import PandasMul
from .modules.pandas_wrapper.pandas_mul_scalar_float import PandasMulScalarFloat
from .modules.pandas_wrapper.pandas_mul_scalar_int import PandasMulScalarInt
from .modules.pandas_wrapper.pandas_mul_series import PandasMulSeries
+from .modules.pandas_wrapper.pandas_pivot import PandasPivot
from .modules.pandas_wrapper.pandas_pow import PandasPow
from .modules.pandas_wrapper.pandas_pow_scalar_float import PandasPowScalarFloat
from .modules.pandas_wrapper.pandas_pow_scalar_int import PandasPowScalarInt
@@ -177,6 +178,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = {
"PandasMulScalarFloat": PandasMulScalarFloat,
"PandasMulScalarInt": PandasMulScalarInt,
"PandasMulSeries": PandasMulSeries,
+ "PandasPivot": PandasPivot,
"PandasPow": PandasPow,
"PandasPowScalarFloat": PandasPowScalarFloat,
"PandasPowScalarInt": PandasPowScalarInt,
@@ -288,6 +290,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {
"PandasMulScalarFloat": "Pandas Mul Scalar Float",
"PandasMulScalarInt": "Pandas Mul Scalar Int",
"PandasMulSeries": "Pandas Mul Series",
+ "PandasPivot": "Pandas Pivot",
"PandasPow": "Pandas Pow",
"PandasPowScalarFloat": "Pandas Pow Scalar Float",
"PandasPowScalarInt": "Pandas Pow Scalar Int",
diff --git a/docs/images/pivot.png b/docs/images/pivot.png
new file mode 100644
index 0000000..32106be
Binary files /dev/null and b/docs/images/pivot.png differ
diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md
index 9fac6c9..bc9182b 100644
--- a/docs/reference/node_reference.md
+++ b/docs/reference/node_reference.md
@@ -136,6 +136,7 @@
| [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. |
+| [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 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. |
diff --git a/docs/reference/pandas_pivot.md b/docs/reference/pandas_pivot.md
new file mode 100644
index 0000000..a3a57bd
--- /dev/null
+++ b/docs/reference/pandas_pivot.md
@@ -0,0 +1,100 @@
+# Pandas Pivot
+Computes a pivot table from a pandas DataFrame.
+
+## Note on a pivot table
+A pivot table is easier to understand if we start off from the table itself:
+In the below DataFrame:
+
+```
+ Column labels
+ X Y Z
+Index
+A 1 2 3
+B 4 5 6
+C 7 8 9
+```
+
+We have index `["A", "B", "C"]` and column labels `["X", "Y", "Z"]`.
+Values in this DataFrame can be accessed by specifying the index and the column label.
+For example:
+
+```python
+df.loc["A", "X"] # 1
+df.loc["A", "Y"] # 2
+```
+
+Now, let's restructure the table by converting column labels into row values:
+
+```
+A X 1
+A Y 2
+A Z 3
+B X 4
+B Y 5
+B Z 6
+C X 7
+C Y 8
+C Z 9
+```
+
+Let's add a column label to each of these columns:
+
+```
+R C V
+--------
+A X 1
+A Y 2
+A Z 3
+B X 4
+B Y 5
+B Z 6
+C X 7
+C Y 8
+C Z 9
+```
+
+Now, the first two columns uniquely identify the value in the third column.
+
+**Pivoting** is the reverse of this operation.
+- You start with the table above and produce the original one.
+- In pandas, this means telling it to use:
+ - R column: `A`, `B`, `C` as the index (row labels).
+ - C column: `X`, `Y`, `Z` as the column labels.
+ - V column: The numbers (`1, 2, 3, ...`) as the values for each cell indexed by `[A, B, or C]` and `[X, Y, or Z]`.
+
+This can be done using:
+
+```python
+df.pivot(index="R", columns="C", values="V")
+```
+
+The result:
+
+```
+ X Y Z
+A 1 2 3
+B 4 5 6
+C 7 8 9
+```
+
+Now the data is back to its original, structured form.
+
+
+
+## Input
+| Name | Data type |
+|---|---|
+| dataframe | DataFrame |
+| index | String |
+| columns | String |
+| values | String |
+
+## Output
+| Data type |
+|---|
+| DataFrame |
+
+
+Category: Transformation
+
+ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
diff --git a/examples/workflows/pivot.json b/examples/workflows/pivot.json
new file mode 100644
index 0000000..e53e72a
--- /dev/null
+++ b/examples/workflows/pivot.json
@@ -0,0 +1 @@
+{"last_node_id":5,"last_link_id":3,"nodes":[{"id":3,"type":"PandasShowDataFrame","pos":[-1654.97607421875,6915.33837890625],"size":[210,191.09849548339844],"flags":{},"order":1,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":1,"localized_name":"dataframe"}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[" R C V\n0 A X 1\n1 A Y 2\n2 A Z 3\n3 B X 4\n4 B Y 5\n5 B Z 6\n6 C X 7\n7 C Y 8\n8 C Z 9"]},{"id":4,"type":"PandasPivot","pos":[-1655.634765625,7191.73486328125],"size":[210,106],"flags":{},"order":2,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":3,"localized_name":"dataframe"}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[2],"slot_index":0,"localized_name":"DATAFRAME"}],"properties":{"Node name for S&R":"PandasPivot"},"widgets_values":["R","C","V"]},{"id":5,"type":"PandasShowDataFrame","pos":[-1402.3653564453125,7191.20849609375],"size":[225.09877014160156,123.6739273071289],"flags":{},"order":3,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":2,"localized_name":"dataframe"}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":["C X Y Z\nR \nA 1 2 3\nB 4 5 6\nC 7 8 9"]},{"id":2,"type":"PandasCreate","pos":[-1912.679931640625,6913.57373046875],"size":[210,188.82322692871094],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[1,3],"slot_index":0,"localized_name":"DATAFRAME"}],"properties":{"Node name for S&R":"PandasCreate"},"widgets_values":["R,C,V\nA,X,1\nA,Y,2\nA,Z,3\nB,X,4\nB,Y,5\nB,Z,6\nC,X,7\nC,Y,8\nC,Z,9"]}],"links":[[1,2,0,3,0,"DATAFRAME"],[2,4,0,5,0,"DATAFRAME"],[3,2,0,4,0,"DATAFRAME"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.4103714517271915,"offset":[4188.132094541789,-5896.532448779008]},"node_versions":{"data-analysis":"8c154c826fa5684b581ad37159b2f85935da19b5"},"ue_links":[]},"version":0.4}
\ No newline at end of file
diff --git a/modules/pandas_wrapper/pandas_pivot.py b/modules/pandas_wrapper/pandas_pivot.py
new file mode 100644
index 0000000..4c54db1
--- /dev/null
+++ b/modules/pandas_wrapper/pandas_pivot.py
@@ -0,0 +1,140 @@
+from typing import Any, Dict
+import pandas as pd
+from .utils import column_labels_string_to_list
+
+class PandasPivot:
+ """
+ Computes a pivot table from a pandas DataFrame.
+
+ ## Note on a pivot table
+ A pivot table is easier to understand if we start off from the table itself:
+ In the below DataFrame:
+
+ ```
+ Column labels
+ X Y Z
+ Index
+ A 1 2 3
+ B 4 5 6
+ C 7 8 9
+ ```
+
+ We have index `["A", "B", "C"]` and column labels `["X", "Y", "Z"]`.
+ Values in this DataFrame can be accessed by specifying the index and the column label.
+ For example:
+
+ ```python
+ df.loc["A", "X"] # 1
+ df.loc["A", "Y"] # 2
+ ```
+
+ Now, let's restructure the table by converting column labels into row values:
+
+ ```
+ A X 1
+ A Y 2
+ A Z 3
+ B X 4
+ B Y 5
+ B Z 6
+ C X 7
+ C Y 8
+ C Z 9
+ ```
+
+ Let's add a column label to each of these columns:
+
+ ```
+ R C V
+ --------
+ A X 1
+ A Y 2
+ A Z 3
+ B X 4
+ B Y 5
+ B Z 6
+ C X 7
+ C Y 8
+ C Z 9
+ ```
+
+ Now, the first two columns uniquely identify the value in the third column.
+
+ **Pivoting** is the reverse of this operation.
+ - You start with the table above and produce the original one.
+ - In pandas, this means telling it to use:
+ - R column: `A`, `B`, `C` as the index (row labels).
+ - C column: `X`, `Y`, `Z` as the column labels.
+ - V column: The numbers (`1, 2, 3, ...`) as the values for each cell indexed by `[A, B, or C]` and `[X, Y, or Z]`.
+
+ This can be done using:
+
+ ```python
+ df.pivot(index="R", columns="C", values="V")
+ ```
+
+ The result:
+
+ ```
+ X Y Z
+ A 1 2 3
+ B 4 5 6
+ C 7 8 9
+ ```
+
+ Now the data is back to its original, structured form.
+
+ 
+ category: Transformation
+ """
+
+ @classmethod
+ def INPUT_TYPES(cls) -> Dict[str, Any]:
+ """
+ Defines the input types for the `select_columns` function.
+
+ Returns:
+ Dict[str, Any]: A dictionary specifying required input types.
+ """
+ return {
+ "required": {
+ "dataframe": ("DATAFRAME", {}),
+ "index": ("STRING", {"default": ""}),
+ "columns": ("STRING", {"default": ""}),
+ "values": ("STRING", {"default": ""}),
+ }
+ }
+
+ RETURN_TYPES: tuple = ("DATAFRAME",)
+ FUNCTION: str = "f"
+ CATEGORY: str = "Data Analysis"
+
+ def f(self, dataframe: pd.DataFrame,
+ index: str,
+ columns: str,
+ values: str) -> tuple:
+ """
+ Selects specific columns from a pandas DataFrame.
+
+ Args:
+ dataframe (DataFrame): The DataFrame.
+ index (str): A comma-separated string of column names to use as index in the pivot table.
+ columns (str): A comma-separated string of column names to use as columns in the pivot table.
+ values (str): A comma-separated string of column names to use as values in the pivot table.
+
+ Returns:
+ tuple: A tuple containing the DataFrame.
+ """
+ ind = column_labels_string_to_list(dataframe, index)
+ col = column_labels_string_to_list(dataframe, columns)
+ val = column_labels_string_to_list(dataframe, values)
+
+ if len(ind) == 1:
+ ind = ind[0]
+ if len(col) == 1:
+ col = col
+ if len(val) == 1:
+ val = val[0]
+
+ df2 = dataframe.pivot(index=ind, columns=col, values=val)
+ return (df2,)
diff --git a/pyproject.toml b/pyproject.toml
index f387b11..44c966c 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.22"
+version = "1.1.23"
license = {file = "LICENSE"}
[project.urls]
diff --git a/tests/pandas_wrapper/pandas_pivot_test.py b/tests/pandas_wrapper/pandas_pivot_test.py
new file mode 100644
index 0000000..9cda5d4
--- /dev/null
+++ b/tests/pandas_wrapper/pandas_pivot_test.py
@@ -0,0 +1,107 @@
+import os
+import sys
+import unittest
+import pandas as pd
+
+PROJECT_ROOT = os.path.realpath(os.path.join(__file__, "..", "..", ".."))
+MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules")
+sys.path.append(MODULE_ROOT)
+
+from pandas_wrapper.pandas_pivot import PandasPivot
+
+class TestPandasPivot(unittest.TestCase):
+ def setUp(self):
+ """Set up test data"""
+ self.node = PandasPivot()
+
+ self.df = pd.DataFrame({
+ "A": ["X", "X", "Y", "Y", "Z", "Z"],
+ "B": ["M", "N", "M", "N", "M", "N"],
+ "C": [1, 2, 3, 4, 5, 6],
+ "D": [10, 20, 30, 40, 50, 60],
+ "E": [100, 200, 300, 400, 500, 600]
+ })
+
+ def test_pivot_single_index_single_column(self):
+ """Test pivot with a single index and a single column"""
+ result = self.node.f(self.df, index="A", columns="B", values="C")[0]
+ expected = self.df.pivot(index="A", columns="B", values="C")
+ pd.testing.assert_frame_equal(result, expected)
+
+ def test_pivot_multi_index_single_column(self):
+ """Test pivot with multiple indices and a single column"""
+ result = self.node.f(self.df, index="A,B", columns="C", values="D")[0]
+ expected = self.df.pivot(index=["A", "B"], columns="C", values="D")
+ pd.testing.assert_frame_equal(result, expected)
+
+ def test_pivot_single_index_multi_column(self):
+ """Test pivot with a single index and multiple columns"""
+ result = self.node.f(self.df, index="A", columns="B,C", values="D")[0]
+ expected = self.df.pivot(index="A", columns=["B", "C"], values="D")
+ pd.testing.assert_frame_equal(result, expected)
+
+ def test_pivot_multi_index_multi_column(self):
+ """Test pivot with multiple indices and multiple columns"""
+ result = self.node.f(self.df, index="A,B", columns="C,E", values="D")[0]
+ expected = self.df.pivot(index=["A", "B"], columns=["C", "E"], values="D")
+ pd.testing.assert_frame_equal(result, expected)
+
+ def test_pivot_single_index_multi_values(self):
+ """Test pivot with a single index and multiple value columns"""
+ result = self.node.f(self.df, index="A", columns="B", values="C,D")[0]
+ expected = self.df.pivot(index="A", columns="B", values=["C", "D"])
+ pd.testing.assert_frame_equal(result, expected)
+
+ def test_pivot_multi_index_multi_values(self):
+ """Test pivot with multiple indices and multiple value columns"""
+ result = self.node.f(self.df, index="A,B", columns="C", values="D,E")[0]
+ expected = self.df.pivot(index=["A", "B"], columns="C", values=["D", "E"])
+ pd.testing.assert_frame_equal(result, expected)
+
+ def test_pivot_index_not_in_dataframe(self):
+ """Test pivot when the given index is not in the DataFrame"""
+ with self.assertRaises(ValueError):
+ self.node.f(self.df, index="Z", columns="B", values="C")[0]
+
+ def test_pivot_columns_not_in_dataframe(self):
+ """Test pivot when the given column is not in the DataFrame"""
+ with self.assertRaises(ValueError):
+ self.node.f(self.df, index="A", columns="Z", values="C")[0]
+
+ def test_pivot_values_not_in_dataframe(self):
+ """Test pivot when the given value column is not in the DataFrame"""
+ with self.assertRaises(ValueError):
+ self.node.f(self.df, index="A", columns="B", values="Z")[0]
+
+ def test_pivot_duplicate_index_column_pairs(self):
+ """Test pivot when index-column pairs are not unique (should raise an error)"""
+ df_dup = self.df.copy()
+ df_dup.loc[1, "B"] = "M" # Introduce a duplicate index-column pair
+ with self.assertRaises(ValueError):
+ self.node.f(df_dup, index="A", columns="B", values="C")[0]
+
+ def test_pivot_from_long_to_wide(self):
+ """Test pivot where column labels become row values before pivoting.
+ This is the test case referenced in the docstring."""
+ df_long = pd.DataFrame({
+ "R": ["A", "A", "A", "B", "B", "B", "C", "C", "C"],
+ "C": ["X", "Y", "Z", "X", "Y", "Z", "X", "Y", "Z"],
+ "V": [1, 2, 3, 4, 5, 6, 7, 8, 9]
+ })
+
+ result = self.node.f(df_long, index="R", columns="C", values="V")[0]
+ expected = pd.DataFrame({
+ "X": [1, 4, 7],
+ "Y": [2, 5, 8],
+ "Z": [3, 6, 9]
+ }, index=["A", "B", "C"])
+
+ # Explicitly set index and column names to match pivot output
+ expected.index.name = "R"
+ expected.columns.name = "C"
+
+ pd.testing.assert_frame_equal(result, expected)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/pandas_wrapper/utils_column_labels_string_to_list.py b/tests/pandas_wrapper/utils_column_labels_string_to_list_test.py
similarity index 100%
rename from tests/pandas_wrapper/utils_column_labels_string_to_list.py
rename to tests/pandas_wrapper/utils_column_labels_string_to_list_test.py