Added Pandas Crosstab and Pandas Value Counts nodes.
This commit is contained in:
@@ -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 |
@@ -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 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Pandas Value Counts
|
||||
Creates a frequency table from one or more columns in a DataFrame.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -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}
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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
@@ -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()
|
||||
Reference in New Issue
Block a user