Added Pandas Pivot node.
This commit is contained in:
@@ -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",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
@@ -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. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
<HR>
|
||||
Category: Transformation
|
||||
|
||||
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
|
||||
@@ -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}
|
||||
@@ -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,)
|
||||
+1
-1
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user