Added Pandas Crosstab and Pandas Value Counts nodes.

This commit is contained in:
HowToSD
2025-02-25 11:50:45 -08:00
parent c17a68cad2
commit 47965bc771
11 changed files with 304 additions and 1 deletions
+6
View File
@@ -33,6 +33,7 @@ from .modules.pandas_wrapper.pandas_create_from_multiple_dict import PandasCreat
from .modules.pandas_wrapper.pandas_create_from_numpy import PandasCreateFromNumpy
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
from .modules.pandas_wrapper.pandas_cummax import PandasCummax
from .modules.pandas_wrapper.pandas_cummin import PandasCummin
from .modules.pandas_wrapper.pandas_cumprod import PandasCumprod
@@ -111,6 +112,7 @@ from .modules.pandas_wrapper.pandas_to_datetime import PandasToDatetime
from .modules.pandas_wrapper.pandas_to_numpy import PandasToNumpy
from .modules.pandas_wrapper.pandas_to_string import PandasToString
from .modules.pandas_wrapper.pandas_transpose import PandasTranspose
from .modules.pandas_wrapper.pandas_value_counts import PandasValueCounts
from .modules.pandas_wrapper.pandas_var import PandasVar
from .modules.pandas_wrapper.pandas_vertical_concat import PandasVerticalConcat
from .modules.pandas_wrapper.pandas_vertical_split import PandasVerticalSplit
@@ -172,6 +174,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = {
"PandasCreateFromNumpy": PandasCreateFromNumpy,
"PandasCreateSeriesFromDict": PandasCreateSeriesFromDict,
"PandasCreateWithIndex": PandasCreateWithIndex,
"PandasCrosstab": PandasCrosstab,
"PandasCummax": PandasCummax,
"PandasCummin": PandasCummin,
"PandasCumprod": PandasCumprod,
@@ -250,6 +253,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = {
"PandasToNumpy": PandasToNumpy,
"PandasToString": PandasToString,
"PandasTranspose": PandasTranspose,
"PandasValueCounts": PandasValueCounts,
"PandasVar": PandasVar,
"PandasVerticalConcat": PandasVerticalConcat,
"PandasVerticalSplit": PandasVerticalSplit,
@@ -314,6 +318,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {
"PandasCreateFromNumpy": "Pandas Create From Numpy",
"PandasCreateSeriesFromDict": "Pandas Create Series From Dict",
"PandasCreateWithIndex": "Pandas Create With Index",
"PandasCrosstab": "Pandas Crosstab",
"PandasCummax": "Pandas Cummax",
"PandasCummin": "Pandas Cummin",
"PandasCumprod": "Pandas Cumprod",
@@ -392,6 +397,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = {
"PandasToNumpy": "Pandas To Numpy",
"PandasToString": "Pandas To String",
"PandasTranspose": "Pandas Transpose",
"PandasValueCounts": "Pandas Value Counts",
"PandasVar": "Pandas Var",
"PandasVerticalConcat": "Pandas Vertical Concat",
"PandasVerticalSplit": "Pandas Vertical Split",
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+5
View File
@@ -26,6 +26,11 @@
| [Pandas Sub Scalar Float](pandas_sub_scalar_float.md) | Subtracts a floating-point number from a Pandas DataFrame. |
| [Pandas Sub Scalar Int](pandas_sub_scalar_int.md) | Subtracts an integer from a Pandas DataFrame. |
| [Pandas Sub Series](pandas_sub_series.md) | Subtracts a Pandas Series from a Pandas DataFrame. |
## Categorical summary
| Node | Description |
| --- | --- |
| [Pandas Crosstab](pandas_crosstab.md) | Creates a contingency table (crosstab, or cross-tabulation) from two or more columns in a DataFrame. |
| [Pandas Value Counts](pandas_value_counts.md) | Creates a frequency table from one or more columns in a DataFrame. |
## Cumulative calculations
| Node | Description |
| --- | --- |
+19
View File
@@ -0,0 +1,19 @@
# Pandas Crosstab
Creates a contingency table (crosstab, or cross-tabulation) from two or more columns in a DataFrame.
## Input
| Name | Data type |
|---|---|
| dataframe | DataFrame |
| index | String |
| column_names | String |
## Output
| Data type |
|---|
| DataFrame |
<HR>
Category: Categorical summary
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
+20
View File
@@ -0,0 +1,20 @@
# Pandas Value Counts
Creates a frequency table from one or more columns in a DataFrame.
![Frequency table](../images/frequency_table.png)
## Input
| Name | Data type |
|---|---|
| dataframe | DataFrame |
| column_names | String |
## Output
| Data type |
|---|
| DataFrame |
<HR>
Category: Categorical summary
ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved.
+1
View File
@@ -0,0 +1 @@
{"last_node_id":6,"last_link_id":4,"nodes":[{"id":2,"type":"PandasCreate","pos":[2785.739990234375,687.7178344726562],"size":[210,155.83331298828125],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[1],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreate"},"widgets_values":["Fruits\napple\napple\napple\nbanana\nbanana\ncoconut"]},{"id":1,"type":"PandasValueCounts","pos":[3036.5732421875,688.5511474609375],"size":[315,58],"flags":{},"order":2,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":1}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[2],"slot_index":0}],"properties":{"Node name for S&R":"PandasValueCounts"},"widgets_values":["Fruits"]},{"id":4,"type":"PandasCreate","pos":[2783.60302734375,927.6964111328125],"size":[210,156.66668701171875],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[3],"slot_index":0}],"properties":{"Node name for S&R":"PandasCreate"},"widgets_values":["Fruits,Color\napple,red\napple,red\napple,yellow\nbanana,yellow\nbanana,green\ncoconut,brown"]},{"id":3,"type":"PandasShowDataFrame","pos":[3390.740234375,689.3844604492188],"size":[210,116.83334350585938],"flags":{},"order":4,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":2}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[" Fruits count\n0 apple 3\n1 banana 2\n2 coconut 1"]},{"id":5,"type":"PandasValueCounts","pos":[3034.4365234375,930.1962280273438],"size":[315,58],"flags":{},"order":3,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":3}],"outputs":[{"name":"DATAFRAME","type":"DATAFRAME","links":[4],"slot_index":0}],"properties":{"Node name for S&R":"PandasValueCounts"},"widgets_values":["Fruits,Color"]},{"id":6,"type":"PandasShowDataFrame","pos":[3391.93603515625,930.1962890625],"size":[237.0999755859375,134.33334350585938],"flags":{},"order":5,"mode":0,"inputs":[{"name":"dataframe","type":"DATAFRAME","link":4}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[" Fruits Color count\n0 apple red 2\n1 apple yellow 1\n2 banana green 1\n3 banana yellow 1\n4 coconut brown 1"]}],"links":[[1,2,0,1,0,"DATAFRAME"],[2,1,0,3,0,"DATAFRAME"],[3,4,0,5,0,"DATAFRAME"],[4,5,0,6,0,"DATAFRAME"]],"groups":[],"config":{},"extra":{"ds":{"scale":1,"offset":[-2487.817049646748,-548.6010443750537]},"node_versions":{"data-analysis":"e6beae73ef53e21d561aaf8ca714e350de59ec7d"},"ue_links":[]},"version":0.4}
+57
View File
@@ -0,0 +1,57 @@
from typing import Any, Dict
import pandas as pd
from .utils import column_labels_string_to_list
class PandasCrosstab:
"""
Creates a contingency table (crosstab, or cross-tabulation) from two or more columns in a DataFrame.
category: Categorical summary
"""
@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", {}),
"index": ("STRING", {"default": ""}),
"column_names": ("STRING", {"default": ""})
}
}
RETURN_TYPES: tuple = ("DATAFRAME",)
FUNCTION: str = "f"
CATEGORY: str = "Data Analysis"
def f(self, dataframe: pd.DataFrame, index: str, column_names: str) -> tuple:
"""
Creates a contingency table (crosstab, or cross-tabulation) from two or more columns in a DataFrame.
Args:
dataframe (DataFrame): The DataFrame.
index (str): A comma-separated list of index (row labels) containing categorical data.
column_names (str): A comma-separated list of column names containing categorical data.
Returns:
tuple: A tuple containing the DataFrame.
"""
rows = column_labels_string_to_list(dataframe, index)
cols = column_labels_string_to_list(dataframe, column_names)
if len(rows) == 1:
rows = dataframe[rows[0]]
else:
rows = [dataframe[e] for e in rows]
if len(cols) == 1:
cols = dataframe[cols[0]]
else:
cols = [dataframe[e] for e in cols]
df2 = pd.crosstab(index=rows, columns=cols)
return (df2,)
@@ -0,0 +1,80 @@
from typing import Any, Dict
import pandas as pd
from .utils import column_labels_string_to_list
class PandasValueCounts:
"""
Creates a frequency table from one or more columns in a DataFrame.
![Frequency table](../images/frequency_table.png)
category: Categorical summary
"""
@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", {}),
"column_names": ("STRING", {"default": ""})
}
}
RETURN_TYPES: tuple = ("DATAFRAME",)
FUNCTION: str = "f"
CATEGORY: str = "Data Analysis"
def f(self, dataframe: pd.DataFrame, column_names: str) -> tuple:
"""
Creates a frequency tableConverts one or more columns in a DataFrame.
Args:
dataframe (DataFrame): The DataFrame.
column_names (str): A comma-separated list of column names containing categorical data.
Returns:
tuple: A tuple containing the DataFrame with selected columns.
"""
# Note
# If the original DataFrame contains a single column with categorical variables:
#
# Category
# --------
# A
# A
# A
# B
# B
# C
# C
#
# and you want to compute the count of each variable and call value_counts().
# After value_counts(), the column entries are became the index in a generated Series:
# df["Category"].value_counts().to_frame()
# Category
# A 3
# B 2
# C 2
#
# After you call reset_index, A, B, C are converted back to a column, but the column label
# is set to "index".
# Therefore, you need to do the following:
# * Convert a Series to a DataFrame (to_frame)
# * Rename the count column to Count (rename)
# * Convert the original column from index to a column (reset_index)
# * Restore the original column label (rename)
cols = column_labels_string_to_list(dataframe, column_names)
if len(cols) == 1:
col_label = cols[0]
df2 = dataframe[col_label].value_counts().to_frame().rename(columns={col_label:"Count"}).reset_index().rename(columns={"index":col_label})
else:
df2 = dataframe[cols].value_counts().to_frame().rename(columns={0:"Count"}).reset_index()
return (df2,)
+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.3.3"
version = "1.3.4"
license = {file = "LICENSE"}
[project.urls]
@@ -0,0 +1,59 @@
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_crosstab import PandasCrosstab
class TestPandasCrosstab(unittest.TestCase):
def setUp(self):
"""Set up test data"""
self.node = PandasCrosstab()
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]
})
def test_crosstab_single_index_single_column(self):
"""Test crosstab with a single index and a single column"""
result = self.node.f(self.df, index="A", column_names="B")[0]
expected = pd.crosstab(index=self.df["A"], columns=self.df["B"])
pd.testing.assert_frame_equal(result, expected)
def test_crosstab_multi_index_single_column(self):
"""Test crosstab with multiple indices and a single column"""
result = self.node.f(self.df, index="A,B", column_names="C")[0]
expected = pd.crosstab(index=[self.df["A"], self.df["B"]], columns=self.df["C"])
pd.testing.assert_frame_equal(result, expected)
def test_crosstab_single_index_multi_column(self):
"""Test crosstab with a single index and multiple columns"""
result = self.node.f(self.df, index="A", column_names="B,C")[0]
expected = pd.crosstab(index=self.df["A"], columns=[self.df["B"], self.df["C"]])
pd.testing.assert_frame_equal(result, expected)
def test_crosstab_multi_index_multi_column(self):
"""Test crosstab with multiple indices and multiple columns"""
result = self.node.f(self.df, index="A,B", column_names="C,D")[0]
expected = pd.crosstab(index=[self.df["A"], self.df["B"]], columns=[self.df["C"], self.df["D"]])
pd.testing.assert_frame_equal(result, expected)
def test_crosstab_index_not_in_dataframe(self):
"""Test crosstab when the given index is not in the DataFrame"""
with self.assertRaises(ValueError):
self.node.f(self.df, index="Z", column_names="B")[0]
def test_crosstab_columns_not_in_dataframe(self):
"""Test crosstab when the given column is not in the DataFrame"""
with self.assertRaises(ValueError):
self.node.f(self.df, index="A", column_names="Z")[0]
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,56 @@
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_value_counts import PandasValueCounts
class TestPandasValueCounts(unittest.TestCase):
def setUp(self):
"""Set up test data"""
self.node = PandasValueCounts()
self.df = pd.DataFrame({
"Category": ["A", "B", "A", "C", "B", "A", "C"],
"Numbers": [1, 2, 1, 3, 2, 1, 3],
"Letter": ["X", "Y", "X", "Z", "Y", "X", "Z"],
100: ["M", "N", "M", "O", "N", "M", "O"], # Integer-labeled column
200: [10, 20, 10, 30, 20, 10, 30] # Another integer-labeled column
})
def test_single_string_column_label(self):
"""Test value_counts on a single string column label"""
result = self.node.f(self.df, "Category")
expected = self.df["Category"].value_counts().to_frame().rename(columns={"Category": "Count"}).reset_index().rename(columns={"index": "Category"})
pd.testing.assert_frame_equal(result[0], expected)
def test_single_integer_column_label(self):
"""Test value_counts on a single integer column label"""
result = self.node.f(self.df, "100")
expected = self.df[100].value_counts().to_frame().rename(columns={100: "Count"}).reset_index().rename(columns={"index": 100})
pd.testing.assert_frame_equal(result[0], expected)
def test_multiple_string_column_labels(self):
"""Test value_counts on multiple string column labels"""
result = self.node.f(self.df, "Category,Letter")
expected = self.df[["Category", "Letter"]].value_counts().to_frame().rename(columns={0:"Count"}).reset_index()
pd.testing.assert_frame_equal(result[0], expected)
def test_multiple_integer_column_labels(self):
"""Test value_counts on multiple integer column labels"""
result = self.node.f(self.df, "100,200")
expected = self.df[[100, 200]].value_counts().to_frame().rename(columns={0:"Count"}).reset_index()
pd.testing.assert_frame_equal(result[0], expected)
def test_mixed_string_integer_column_labels(self):
"""Test value_counts on a mix of string and integer column labels"""
result = self.node.f(self.df, "Category,100")
expected = self.df[["Category", 100]].value_counts().to_frame().rename(columns={0:"Count"}).reset_index()
pd.testing.assert_frame_equal(result[0], expected)
if __name__ == "__main__":
unittest.main()