diff --git a/__init__.py b/__init__.py index 8cbf839..5771209 100644 --- a/__init__.py +++ b/__init__.py @@ -6,6 +6,7 @@ from .modules.matplotlib.mpl_scatter import MPLScatter from .modules.pandas_wrapper.pandas_columns import PandasColumns from .modules.pandas_wrapper.pandas_create import PandasCreate from .modules.pandas_wrapper.pandas_create_with_index import PandasCreateWithIndex +from .modules.pandas_wrapper.pandas_group_by import PandasGroupBy from .modules.pandas_wrapper.pandas_head import PandasHead from .modules.pandas_wrapper.pandas_iloc_row_series import PandasIlocRowSeries from .modules.pandas_wrapper.pandas_iloc_rows_dataframe import PandasIlocRowsDataFrame @@ -36,6 +37,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasColumns": PandasColumns, "PandasCreate": PandasCreate, "PandasCreateWithIndex": PandasCreateWithIndex, + "PandasGroupBy": PandasGroupBy, "PandasHead": PandasHead, "PandasIlocRowSeries": PandasIlocRowSeries, "PandasIlocRowsDataFrame": PandasIlocRowsDataFrame, @@ -69,6 +71,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasColumns": "Pandas Columns", "PandasCreate": "Pandas Create", "PandasCreateWithIndex": "Pandas Create With Index", + "PandasGroupBy": "Pandas Group By", "PandasHead": "Pandas Head", "PandasIlocRowSeries": "Pandas Iloc Row Series", "PandasIlocRowsDataFrame": "Pandas Iloc Rows DataFrame", diff --git a/docs/images/group_by1.png b/docs/images/group_by1.png new file mode 100644 index 0000000..d72143e Binary files /dev/null and b/docs/images/group_by1.png differ diff --git a/docs/images/group_by2.png b/docs/images/group_by2.png new file mode 100644 index 0000000..09900c5 Binary files /dev/null and b/docs/images/group_by2.png differ diff --git a/docs/images/group_by3.png b/docs/images/group_by3.png new file mode 100644 index 0000000..10a1e58 Binary files /dev/null and b/docs/images/group_by3.png differ diff --git a/docs/ug.md b/docs/ug.md index e09688f..746624d 100644 --- a/docs/ug.md +++ b/docs/ug.md @@ -36,9 +36,10 @@ You can right-click, select **Add Node**, go to **Data Analysis**, and look for | **Pandas Loc Cell Str** | Select a cell by row label (index) and column label | | **Pandas Head** | Extract the first few rows | -**Manipulating data** +**Transforming data** | Node Name | Functionality | |-------------------------|-------------------------------------| +| **Pandas Group By** | Group rows and apply aggregation functions | | **Pandas Join** | Join two DataFrames | | **Pandas Sort** | Sort DataFrame by a column | @@ -120,6 +121,26 @@ Use **Pandas Loc Cell Str** for selecting a cell by row label (index) and colum This node outputs the cell value as a string irrespective of the actual cell data type. +## Grouping Rows +Use **Pandas Group By** to group rows and apply aggregation functions. +Currently below aggregation functions are supported: +* sum +* mean +* count +* std +* min +* max + +Below shows an example of using sum, mean, count functions: +![Group By](images/group_by1.png) + +Below shows an example of using std, min, max functions: + +![Group By](images/group_by2.png) + +You can also have multiple columns to apply aggregation functions. In the below example, Qty and Price columns are aggregated for the Fruit column: +![Group By](images/group_by3.png) + ## Joining DataFrames Use **Pandas Join**. You can select from inner, left, right, outer joins. First, use two **Pandas Load CSV** nodes to load two CSVs for the left DataFrame and right DataFrame. diff --git a/examples/workflows/group_by.json b/examples/workflows/group_by.json new file mode 100644 index 0000000..59dfed9 --- /dev/null +++ b/examples/workflows/group_by.json @@ -0,0 +1 @@ +{"last_node_id":25,"last_link_id":21,"nodes":[{"id":15,"type":"PandasShowDataFrame","pos":[-163.7736358642578,1620.09814453125],"size":[210,108.7156753540039],"flags":{},"order":8,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":11,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[""," Qty\napple 1110\nbanana 220\ncoconut 330"]},{"id":17,"type":"PandasShowDataFrame","pos":[-161.14952087402344,1778.2269287109375],"size":[210,100.74386596679688],"flags":{},"order":9,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":12,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[""," Qty\napple 370\nbanana 110\ncoconut 165"]},{"id":14,"type":"PandasGroupBy","pos":[-421.0318908691406,1622.4898681640625],"size":[232.6582489013672,94],"flags":{},"order":2,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":10,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[11],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasGroupBy"},"widgets_values":["","Fruit","sum"]},{"id":16,"type":"PandasGroupBy","pos":[-420.16949462890625,1777.597412109375],"size":[232.6582489013672,94],"flags":{},"order":3,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":14,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[12],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasGroupBy"},"widgets_values":["","Fruit","mean"]},{"id":18,"type":"PandasGroupBy","pos":[-423.3183288574219,1935.9014892578125],"size":[232.6582489013672,94],"flags":{},"order":4,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":15,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[13],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasGroupBy"},"widgets_values":["","Fruit","count"]},{"id":19,"type":"PandasShowDataFrame","pos":[-162.36917114257812,1936.36328125],"size":[210,106.08497619628906],"flags":{},"order":10,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":13,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[""," Qty\napple 3\nbanana 2\ncoconut 2"]},{"id":20,"type":"PandasShowDataFrame","pos":[-166.30099487304688,2114.517578125],"size":[210,108.7156753540039],"flags":{},"order":11,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":16,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[""," Qty\napple 547.448628\nbanana 127.279221\ncoconut 190.918831"]},{"id":21,"type":"PandasShowDataFrame","pos":[-163.6768798828125,2272.646240234375],"size":[210,100.74386596679688],"flags":{},"order":12,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":17,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[""," Qty\napple 10\nbanana 20\ncoconut 30"]},{"id":25,"type":"PandasShowDataFrame","pos":[-164.8965301513672,2430.78271484375],"size":[210,106.08497619628906],"flags":{},"order":13,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":18,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[""," Qty\napple 1000\nbanana 200\ncoconut 300"]},{"id":22,"type":"PandasGroupBy","pos":[-423.5592956542969,2116.9091796875],"size":[232.6582489013672,94],"flags":{},"order":5,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":19,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[16],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasGroupBy"},"widgets_values":["","Fruit","std"]},{"id":23,"type":"PandasGroupBy","pos":[-422.6968994140625,2272.016845703125],"size":[232.6582489013672,94],"flags":{},"order":6,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":20,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[17],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasGroupBy"},"widgets_values":["","Fruit","min"]},{"id":24,"type":"PandasGroupBy","pos":[-425.8457336425781,2430.32080078125],"size":[232.6582489013672,94],"flags":{},"order":7,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":21,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[18],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasGroupBy"},"widgets_values":["","Fruit","max"]},{"id":4,"type":"PandasShowDataFrame","pos":[-417.6891784667969,1422.78369140625],"size":[210,137.6671142578125],"flags":{},"order":1,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":9,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowDataFrame"},"widgets_values":[""," Fruit Qty\n0 apple 10\n1 banana 20\n2 coconut 30\n3 apple 100\n4 banana 200\n5 coconut 300\n6 apple 1000"]},{"id":13,"type":"PandasCreate","pos":[-728.1686401367188,2251.291748046875],"size":[210,153.88217163085938],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"STRING","type":"STRING","links":[9,10,14,15,19,20,21],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasCreate"},"widgets_values":["Fruit,Qty\napple,10\nbanana,20\ncoconut,30\napple,100\nbanana,200\ncoconut,300\napple,1000"]}],"links":[[9,13,0,4,0,"STRING"],[10,13,0,14,0,"STRING"],[11,14,0,15,0,"STRING"],[12,16,0,17,0,"STRING"],[13,18,0,19,0,"STRING"],[14,13,0,16,0,"STRING"],[15,13,0,18,0,"STRING"],[16,22,0,20,0,"STRING"],[17,23,0,21,0,"STRING"],[18,24,0,25,0,"STRING"],[19,13,0,22,0,"STRING"],[20,13,0,23,0,"STRING"],[21,13,0,24,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.9503206450765007,"offset":[1005.2604655032138,-1650.2229158553694]},"node_versions":{"data-analysis":"8ba49404664bc59bfd3918cc61f91f891732d48d"},"ue_links":[]},"version":0.4} \ No newline at end of file diff --git a/modules/pandas_wrapper/pandas_group_by.py b/modules/pandas_wrapper/pandas_group_by.py new file mode 100644 index 0000000..2cb4e58 --- /dev/null +++ b/modules/pandas_wrapper/pandas_group_by.py @@ -0,0 +1,65 @@ +from typing import Any, Dict +from io import StringIO +import pandas as pd + + +class PandasGroupBy: + """ + PandasGroupBy: + A class for computing aggregate values for groups in a DataFrame. + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + See https://pandas.pydata.org/docs/reference/groupby.html for aggragate functions. + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "dataframe_json": ("STRING", {"multiline": True}), + "column_name": ("STRING", {"default": ""}), + "aggragate_function":(("sum", "mean", "count", "std", "min", "max"),) + } + } + + RETURN_TYPES: tuple = ("STRING",) + FUNCTION: str = "groupby_dataframes" + CATEGORY: str = "Data Analysis" + + def groupby_dataframes(self, + dataframe_json: str, + column_name: str, + aggragate_function: str) -> tuple: + """ + Compute aggregate values for groups in a DataFrame + + Args: + dataframe_json (str): A JSON string representation of the DataFrame. + column_name (str): The name of the column for use as groups. + aggregate_function (str): The type of aggregate function to perform. + + Returns: + tuple: A tuple containing a JSON string of the merged DataFrame. + """ + # Deserialize JSON string to DataFrame + df = pd.read_json(StringIO(dataframe_json)) + + if aggragate_function == "max": + df_out = df.groupby(column_name).max() + elif aggragate_function == "min": + df_out = df.groupby(column_name).min() + elif aggragate_function == "std": + df_out = df.groupby(column_name).std() + elif aggragate_function == "count": + df_out = df.groupby(column_name).count() + elif aggragate_function == "sum": + df_out = df.groupby(column_name).sum() + elif aggragate_function == "mean": + df_out = df.groupby(column_name).mean() + else: + raise ValueError("Unsupported aggregate function.") + return (df_out.to_json(),) diff --git a/pyproject.toml b/pyproject.toml index c2704d9..be890a1 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.0.15" +version = "1.0.16" license = {file = "LICENSE"} [project.urls]