diff --git a/__init__.py b/__init__.py index 9efacbc..493a97e 100644 --- a/__init__.py +++ b/__init__.py @@ -11,9 +11,11 @@ from .modules.pandas_wrapper.pandas_join import PandasJoin from .modules.pandas_wrapper.pandas_load_csv import PandasLoadCSV from .modules.pandas_wrapper.pandas_load_csv_with_index import PandasLoadCSVWithIndex from .modules.pandas_wrapper.pandas_loc_cell_str import PandasLocCellStr +from .modules.pandas_wrapper.pandas_loc_row_series import PandasLocRowSeries from .modules.pandas_wrapper.pandas_save_csv import PandasSaveCSV from .modules.pandas_wrapper.pandas_select_columns import PandasSelectColumns from .modules.pandas_wrapper.pandas_select_rows import PandasSelectRows +from .modules.pandas_wrapper.pandas_series_to_string import PandasSeriesToString from .modules.pandas_wrapper.pandas_show_dataframe import PandasShowDataFrame from .modules.pandas_wrapper.pandas_show_text import PandasShowText from .modules.pandas_wrapper.pandas_sort import PandasSort @@ -35,9 +37,11 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasLoadCSV": PandasLoadCSV, "PandasLoadCSVWithIndex": PandasLoadCSVWithIndex, "PandasLocCellStr": PandasLocCellStr, + "PandasLocRowSeries": PandasLocRowSeries, "PandasSaveCSV": PandasSaveCSV, "PandasSelectColumns": PandasSelectColumns, "PandasSelectRows": PandasSelectRows, + "PandasSeriesToString": PandasSeriesToString, "PandasShowDataFrame": PandasShowDataFrame, "PandasShowText": PandasShowText, "PandasSort": PandasSort, @@ -62,9 +66,11 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasLoadCSV": "Pandas Load CSV", "PandasLoadCSVWithIndex": "Pandas Load CSV with Index", "PandasLocCellStr": "Pandas Loc Cell Str", + "PandasLocRowSeries": "Pandas Loc Row Series", "PandasSaveCSV": "Pandas Save CSV", "PandasSelectColumns": "Pandas Select Columns", "PandasSelectRows": "Pandas Select Rows", + "PandasSeriesToString": "Pandas Series To String", "PandasShowDataFrame": "Pandas Show DataFrame", "PandasShowText": "Pandas Show Text", "PandasSort": "Pandas Sort", diff --git a/docs/images/loc_row_series.png b/docs/images/loc_row_series.png new file mode 100644 index 0000000..ff4b1a2 Binary files /dev/null and b/docs/images/loc_row_series.png differ diff --git a/docs/ug.md b/docs/ug.md index b1b2000..11cf079 100644 --- a/docs/ug.md +++ b/docs/ug.md @@ -22,6 +22,7 @@ You can right-click, select **Add Node**, go to **Data Analysis**, and look for | **Pandas Index** | Extract row labels (index) | | **Pandas Select Columns** | Select specific columns from a DataFrame | | **Pandas Select Rows** | Filter rows based on conditions | +| **Pandas Loc Row Series** | Select a row by row label (index) | | **Pandas Loc Cell Str** | Select a cell by row label (index) and column label (index) | | **Pandas Join** | Join two DataFrames | | **Pandas Head** | Extract the first few rows | @@ -29,7 +30,8 @@ You can right-click, select **Add Node**, go to **Data Analysis**, and look for | **Pandas Show Text** | Display Text | | **Pandas Summary** | Extract DataFrame statistics | | **Pandas Sort** | Sort DataFrame by a column | -| **Pandas To String** | Convert DataFrame to a string | +| **Pandas Series To String** | Convert Series to a displayable string | +| **Pandas To String** | Convert DataFrame to a displayable string | | **MPL Bar Chart** | Generate a bar chart | | **MPL Line Plot** | Generate a line plot | | **MPL Scatter Plot** | Generate a scatter plot | @@ -64,6 +66,10 @@ Use **Pandas Select Columns** for selecting columns, **Pandas Select Rows** to s ![Pandas select subset](images/subset.jpg) +You can use **Pandas Loc Row Series** to select a row by row label (index). This node returns a JSON-serialized Series object. Data type of the labels needs to be specified as shown below: + +![Pandas Loc Row Series](images/loc_row_series.png) + ## Selecting a cell by row label (index) and column label (index) Use **Pandas Loc Cell Str** for selecting a cell by row label (index) and column label (index). Data type of the labels needs to be specified as shown below: @@ -120,6 +126,9 @@ Use **Pandas Sort**. ## Convert DataFrame to a string Use **Pandas To String**. This node is to convert the DataFrame to a displayable string, and this truncates the data when the data is large. This can be used to display the data content in a text display node (e.g. Show Text node). However, you can use Show DataFrame nodes to display the DataFrame content, so you do not normally need to use this. +## Convert Series to a string +Use **Pandas Series To String**. This node converts a Pandas Series to a displayable string. You can use this as an input to the **Pandas Show Text** node. + ## Displaying charts or plots Use one of the below: | Node | Purpose | diff --git a/examples/workflows/loc_row_series.json b/examples/workflows/loc_row_series.json new file mode 100644 index 0000000..8535167 --- /dev/null +++ b/examples/workflows/loc_row_series.json @@ -0,0 +1 @@ +{"last_node_id":18,"last_link_id":16,"nodes":[{"id":14,"type":"PandasSeriesToString","pos":[-75.9743423461914,1615.0836181640625],"size":[210,39.39631271362305],"flags":{},"order":6,"mode":0,"inputs":[{"name":"series_json","type":"STRING","link":12,"widget":{"name":"series_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[13],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasSeriesToString"},"widgets_values":[""]},{"id":10,"type":"PandasShowDataFrame","pos":[-332.0985412597656,1870.44482421875],"size":[210,116.62158966064453],"flags":{},"order":4,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":7,"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":[""," A B\n0 1 10\n1 2 2345678\n2 3 30"]},{"id":8,"type":"PandasShowText","pos":[175.51785278320312,1615.8155517578125],"size":[210,101.80709075927734],"flags":{},"order":8,"mode":0,"inputs":[{"name":"text","type":"STRING","link":13,"widget":{"name":"text"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowText"},"widgets_values":["","A 2\nB 123456\nName: France, dtype: int64"]},{"id":16,"type":"PandasSeriesToString","pos":[-49.06276321411133,2046.5435791015625],"size":[210,39.39631271362305],"flags":{},"order":7,"mode":0,"inputs":[{"name":"series_json","type":"STRING","link":14,"widget":{"name":"series_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[16],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasSeriesToString"},"widgets_values":[""]},{"id":17,"type":"PandasShowText","pos":[201.8267822265625,2044.7362060546875],"size":[210,101.80709075927734],"flags":{},"order":9,"mode":0,"inputs":[{"name":"text","type":"STRING","link":16,"widget":{"name":"text"}}],"outputs":[{"name":"STRING","type":"STRING","links":null,"shape":6,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasShowText"},"widgets_values":["","A 2\nB 2345678\nName: 1, dtype: int64"]},{"id":4,"type":"PandasShowDataFrame","pos":[-331.5325012207031,1436.6846923828125],"size":[210,116.62158966064453],"flags":{},"order":2,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":2,"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":[""," A B\nUS 1 10\nFrance 2 123456\nGermany 3 30"]},{"id":3,"type":"PandasCreateWithIndex","pos":[-594.9833374023438,1436.66015625],"size":[210,136.70285034179688],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"STRING","type":"STRING","links":[2,11],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasCreateWithIndex"},"widgets_values":["index,A,B\nUS,1,10\nFrance,2,123456\nGermany,3,30",0]},{"id":9,"type":"PandasCreate","pos":[-609.8991088867188,1870.20849609375],"size":[210,113.97640228271484],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"STRING","type":"STRING","links":[7,15],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasCreate"},"widgets_values":["A,B\n1,10\n2,2345678\n3,30"]},{"id":13,"type":"PandasLocRowSeries","pos":[-337.280517578125,1615.083251953125],"size":[221.5128936767578,82.3223648071289],"flags":{},"order":3,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":11,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[12],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasLocRowSeries"},"widgets_values":["","France","string"]},{"id":15,"type":"PandasLocRowSeries","pos":[-323.39080810546875,2047.4111328125],"size":[219.60301208496094,89.9618911743164],"flags":{},"order":5,"mode":0,"inputs":[{"name":"dataframe_json","type":"STRING","link":15,"widget":{"name":"dataframe_json"}}],"outputs":[{"name":"STRING","type":"STRING","links":[14],"slot_index":0,"localized_name":"STRING"}],"properties":{"Node name for S&R":"PandasLocRowSeries"},"widgets_values":["","1","int"]}],"links":[[2,3,0,4,0,"STRING"],[7,9,0,10,0,"STRING"],[11,3,0,13,0,"STRING"],[12,13,0,14,0,"STRING"],[13,14,0,8,0,"STRING"],[14,15,0,16,0,"STRING"],[15,9,0,15,0,"STRING"],[16,16,0,17,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.7853889628731393,"offset":[968.8282590918203,-1332.2955501507915]},"node_versions":{"data-analysis":"29af129ba3913df6cabbb3b7536c61317426759e"},"ue_links":[]},"version":0.4} \ No newline at end of file diff --git a/modules/pandas_wrapper/pandas_loc_row_series.py b/modules/pandas_wrapper/pandas_loc_row_series.py new file mode 100644 index 0000000..d9d4fb2 --- /dev/null +++ b/modules/pandas_wrapper/pandas_loc_row_series.py @@ -0,0 +1,53 @@ +from typing import Any, Dict +from io import StringIO +import pandas as pd +from .utils import series_to_jsons + +class PandasLocRowSeries: + """ + PandasLocRowSeries: + A class for selecting a row from a pandas DataFrame and output as a Series. + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the `select_row` function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "dataframe_json": ("STRING", {"multiline": True}), + "row_index": ("STRING", {"default": ""}), + "row_index_type":(("string", "int"),) + } + } + + RETURN_TYPES: tuple = ("STRING",) + FUNCTION: str = "select_row" + CATEGORY: str = "Data Analysis" + + def select_row(self, dataframe_json: str, + row_index: str, + row_index_type: str) -> tuple: + """ + Selects a specific row using a row label (row index) from a pandas DataFrame, + and returns the JSON serialized Series object. + + Args: + dataframe_json (str): A JSON string representation of the DataFrame. + row_index (str): The row index (row label) for the row. + row_index_type (str): The data type of the row index. + + Returns: + tuple: A tuple containing the value of the row in string. The string is + a JSON string containing the serialized Pandas Series object. + """ + # Deserialize JSON string to DataFrame + df = pd.read_json(StringIO(dataframe_json)) + row_index = int(row_index) if row_index_type == "int" else row_index + + value = df.loc[row_index] + return (series_to_jsons(value),) diff --git a/modules/pandas_wrapper/pandas_series_to_string.py b/modules/pandas_wrapper/pandas_series_to_string.py new file mode 100644 index 0000000..b5b6257 --- /dev/null +++ b/modules/pandas_wrapper/pandas_series_to_string.py @@ -0,0 +1,42 @@ +from typing import Any, Dict +from io import StringIO +import pandas as pd +from .utils import jsons_to_series + +class PandasSeriesToString: + """ + PandasSeriesToString: + A class for converting a pandas Series to a string representation. + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the `to_string` function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "series_json": ("STRING", {"multiline": True}), + } + } + + RETURN_TYPES: tuple = ("STRING",) + FUNCTION: str = "series_to_string" + CATEGORY: str = "Data Analysis" + + def series_to_string(self, series_json: str) -> tuple: + """ + Converts a Pandas Series to a string representation. + + Args: + series_json (str): A JSON string representation of the Series. + + Returns: + tuple: A tuple containing the string representation of the Series. + """ + # Deserialize JSON string to DataFrame + series = jsons_to_series(series_json) + return (str(series),) diff --git a/modules/pandas_wrapper/utils.py b/modules/pandas_wrapper/utils.py index 7e95a27..77aff4d 100644 --- a/modules/pandas_wrapper/utils.py +++ b/modules/pandas_wrapper/utils.py @@ -1,4 +1,5 @@ import json +import numpy as np import pandas as pd def series_to_jsons(series: pd.Series) -> str: @@ -11,13 +12,13 @@ def series_to_jsons(series: pd.Series) -> str: Returns: str: A JSON string representing the Series, including its data, index, and name. """ - array = [x.item() if hasattr(x, "item") else x for x in series.tolist()] # Convert NumPy types to Python types + array = [x.item() if hasattr(x, "item") else x for x in series.array.tolist()] # Convert NumPy types to Python types index = [x.item() if hasattr(x, "item") else x for x in series.index.tolist()] # Convert index to Python types name = series.name d = {"array": array, "index": index, "name": name} - return json.dumps(d) + return json.dumps(d, cls=NpEncoder) # See license for NpEncoder(json.JSONEncoder) below def jsons_to_series(s: str) -> pd.Series: """ @@ -61,3 +62,23 @@ def jsons_to_index(s: str) -> pd.Index: """ d = json.loads(s) return pd.Index(d["index"], name=d.get("name")) # Restore index with name + + +class NpEncoder(json.JSONEncoder): + """ + Source: https://stackoverflow.com/questions/50916422/python-typeerror-object-of-type-int64-is-not-json-serializable + Author: Jie Yang, Tommy + Licensed under CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/) + """ + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + if isinstance(obj, np.floating): + return float(obj) + if isinstance(obj, np.bool_): + return bool(obj) + if isinstance(obj, np.ndarray): + return obj.tolist() + return super(NpEncoder, self).default(obj) + + diff --git a/pyproject.toml b/pyproject.toml index b283524..196a872 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.9" +version = "1.0.10" license = {file = "LICENSE"} [project.urls] diff --git a/tests/pandas/utils_index_json_test.py b/tests/pandas/utils_index_json_test.py index 7397364..ad81d68 100644 --- a/tests/pandas/utils_index_json_test.py +++ b/tests/pandas/utils_index_json_test.py @@ -2,6 +2,7 @@ import os import sys import unittest import json +import numpy as np import pandas as pd PROJECT_ROOT = os.path.realpath(os.path.join(__file__, "..", "..", "..")) diff --git a/tests/pandas/utils_series_json_test.py b/tests/pandas/utils_series_json_test.py index be5d8c1..08faf87 100644 --- a/tests/pandas/utils_series_json_test.py +++ b/tests/pandas/utils_series_json_test.py @@ -2,6 +2,7 @@ import os import sys import unittest import json +import numpy as np import pandas as pd PROJECT_ROOT = os.path.realpath(os.path.join(__file__, "..", "..", "..")) @@ -15,13 +16,20 @@ class TestSeriesJsonConversion(unittest.TestCase): def setUp(self): """Set up test data""" self.series_int = pd.Series([10, 20, 30], index=["a", "b", "c"], name="test_series_int") + self.series_np_int64 = pd.Series(np.array([10, 20, 30], dtype=np.int64), + index=["a", "b", "c"], + name="test_series_np_int64") + self.series_np_int64_2 = pd.Series(np.array([10, 20, 30], dtype=np.int64), + index=np.array([1, 2, 3], dtype=np.int64), + name="test_series_np_int64_2") self.series_float = pd.Series([1.1, 2.2, 3.3], index=["x", "y", "z"], name="test_series_float") self.series_str = pd.Series(["apple", "banana", "cherry"], index=[0, 1, 2], name="test_series_str") self.series_mixed = pd.Series([1, "text", 3.5], index=["one", "two", "three"], name="test_series_mixed") def test_series_to_jsons_and_back(self): """Test conversion from Series to JSON and back""" - for series in [self.series_int, self.series_float, self.series_str, self.series_mixed]: + for series in [self.series_int, self.series_np_int64, self.series_np_int64_2, + self.series_float, self.series_str, self.series_mixed]: with self.subTest(series=series): json_str = series_to_jsons(series) reconstructed_series = jsons_to_series(json_str)