diff --git a/__init__.py b/__init__.py index 12577cc..732f849 100644 --- a/__init__.py +++ b/__init__.py @@ -87,6 +87,7 @@ from .modules.pandas_wrapper.pandas_sin import PandasSin from .modules.pandas_wrapper.pandas_skew import PandasSkew from .modules.pandas_wrapper.pandas_sort import PandasSort from .modules.pandas_wrapper.pandas_std import PandasStd +from .modules.pandas_wrapper.pandas_strftime import PandasStrftime from .modules.pandas_wrapper.pandas_sub import PandasSub from .modules.pandas_wrapper.pandas_sub_scalar_float import PandasSubScalarFloat from .modules.pandas_wrapper.pandas_sub_scalar_int import PandasSubScalarInt @@ -94,6 +95,7 @@ from .modules.pandas_wrapper.pandas_sub_series import PandasSubSeries from .modules.pandas_wrapper.pandas_sum import PandasSum from .modules.pandas_wrapper.pandas_summary import PandasSummary from .modules.pandas_wrapper.pandas_tan import PandasTan +from .modules.pandas_wrapper.pandas_to_datetime import PandasToDatetime from .modules.pandas_wrapper.pandas_to_string import PandasToString from .modules.pandas_wrapper.pandas_transpose import PandasTranspose from .modules.pandas_wrapper.pandas_var import PandasVar @@ -196,6 +198,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasSkew": PandasSkew, "PandasSort": PandasSort, "PandasStd": PandasStd, + "PandasStrftime": PandasStrftime, "PandasSub": PandasSub, "PandasSubScalarFloat": PandasSubScalarFloat, "PandasSubScalarInt": PandasSubScalarInt, @@ -203,6 +206,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasSum": PandasSum, "PandasSummary": PandasSummary, "PandasTan": PandasTan, + "PandasToDatetime": PandasToDatetime, "PandasToString": PandasToString, "PandasTranspose": PandasTranspose, "PandasVar": PandasVar, @@ -305,6 +309,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasSkew": "Pandas Skew", "PandasSort": "Pandas Sort", "PandasStd": "Pandas Std", + "PandasStrftime": "Pandas Strftime", "PandasSub": "Pandas Sub", "PandasSubScalarFloat": "Pandas Sub Scalar Float", "PandasSubScalarInt": "Pandas Sub Scalar Int", @@ -312,6 +317,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasSum": "Pandas Sum", "PandasSummary": "Pandas Summary", "PandasTan": "Pandas Tan", + "PandasToDatetime": "Pandas To Datetime", "PandasToString": "Pandas To String", "PandasTranspose": "Pandas Transpose", "PandasVar": "Pandas Var", diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index fc72717..9fac6c9 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -65,6 +65,11 @@ | --- | --- | | [Pandas Columns](pandas_columns.md) | Retrieves the column labels of a pandas DataFrame. | | [Pandas Index](pandas_index.md) | Retrieves the row labels (index) of a pandas DataFrame. | +## Date and time processing +| Node | Description | +| --- | --- | +| [Pandas Strftime](pandas_strftime.md) | Converts one or more datetime columns in a DataFrame to string columns. | +| [Pandas To Datetime](pandas_to_datetime.md) | Converts one or more string columns in a DataFrame to datetime columns. | ## Display data | Node | Description | | --- | --- | diff --git a/docs/reference/pandas_strftime.md b/docs/reference/pandas_strftime.md new file mode 100644 index 0000000..65779c9 --- /dev/null +++ b/docs/reference/pandas_strftime.md @@ -0,0 +1,21 @@ +# Pandas Strftime +Converts one or more datetime columns in a DataFrame to string columns. + +Refer to [https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior](Python document) for date/time format strings. + +## Input +| Name | Data type | +|---|---| +| dataframe | DataFrame | +| column_names | String | +| date_format | String | + +## Output +| Data type | +|---| +| DataFrame | + +
+Category: Date and time processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/pandas_to_datetime.md b/docs/reference/pandas_to_datetime.md new file mode 100644 index 0000000..6b08282 --- /dev/null +++ b/docs/reference/pandas_to_datetime.md @@ -0,0 +1,22 @@ +# Pandas To Datetime +Converts one or more string columns in a DataFrame to datetime columns. + +Note: When only the time portion is provided, the date defaults to 1900-01-01. +Refer to [https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior](the Python documentation) for valid date/time format strings. + +## Input +| Name | Data type | +|---|---| +| dataframe | DataFrame | +| column_names | String | +| date_format | String | + +## Output +| Data type | +|---| +| DataFrame | + +
+Category: Date and time processing + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/modules/pandas_wrapper/pandas_strftime.py b/modules/pandas_wrapper/pandas_strftime.py new file mode 100644 index 0000000..3042d75 --- /dev/null +++ b/modules/pandas_wrapper/pandas_strftime.py @@ -0,0 +1,55 @@ +from typing import Any, Dict +import pandas as pd +import pandas.api.types as ptypes + + +class PandasStrftime: + """ + Converts one or more datetime columns in a DataFrame to string columns. + + Refer to [https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior](Python document) for date/time format strings. + + category: Date and time processing + """ + + @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": ""}), + "date_format": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES: tuple = ("DATAFRAME",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, dataframe: pd.DataFrame, column_names: str, date_format: str) -> tuple: + """ + Converts one or more datetime columns in a DataFrame to string columns. + + Args: + dataframe (DataFrame): The DataFrame. + column_names (str): A comma-separated string of column names containing datetime values. + date_format (str): A date format string for strftime. + + Returns: + tuple: A tuple containing the DataFrame with formatted date columns. + """ + date_columns = [col.strip() for col in column_names.split(",")] + + df2 = dataframe.copy() + df2[date_columns] = df2[date_columns].apply( + # Keep the datetime column check in case the user specifies the non-date column + lambda col: col.dt.strftime(date_format) if ptypes.is_datetime64_any_dtype(col) else col + ) + + return (df2,) diff --git a/modules/pandas_wrapper/pandas_to_datetime.py b/modules/pandas_wrapper/pandas_to_datetime.py new file mode 100644 index 0000000..d18a284 --- /dev/null +++ b/modules/pandas_wrapper/pandas_to_datetime.py @@ -0,0 +1,50 @@ +from typing import Any, Dict +import pandas as pd +import pandas.api.types as ptypes + + +class PandasToDatetime: + """ + Converts one or more string columns in a DataFrame to datetime columns. + + Note: When only the time portion is provided, the date defaults to 1900-01-01. + Refer to [https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior](the Python documentation) for valid date/time format strings. + + category: Date and time processing + """ + + @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": ""}), + "date_format": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES: tuple = ("DATAFRAME",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, dataframe: pd.DataFrame, column_names: str, date_format: str) -> tuple: + """ + Converts one or more string columns to datetime columns. + + Args: + dataframe (DataFrame): The DataFrame. + column_names (str): A comma-separated string of column names containing date time strings. + date_format (str): A date format. + + Returns: + tuple: A tuple containing the DataFrame with selected columns. + """ + date_columns = [col.strip() for col in column_names.split(",")] + df2 = dataframe.apply(lambda col: pd.to_datetime(col, format=date_format) if col.name in date_columns else col) + return (df2,) diff --git a/pyproject.toml b/pyproject.toml index 0034e40..76678a8 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.1.20" +version = "1.1.21" license = {file = "LICENSE"} [project.urls] diff --git a/tests/pandas_wrapper/pandas_to_datetime_test.py b/tests/pandas_wrapper/pandas_to_datetime_test.py new file mode 100644 index 0000000..a007a75 --- /dev/null +++ b/tests/pandas_wrapper/pandas_to_datetime_test.py @@ -0,0 +1,57 @@ +import os +import sys +import unittest +import pandas as pd +import pandas.api.types as ptypes + +PROJECT_ROOT = os.path.realpath(os.path.join(__file__, "..", "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +print(MODULE_ROOT) + +from pandas_wrapper.pandas_to_datetime import PandasToDatetime + +class Test(unittest.TestCase): + def setUp(self): + """Set up test data""" + self.node = PandasToDatetime() + self.pd_date = pd.DataFrame({ + "A": ["2015-01-23", "2013-06-30"], + "B": ["2005-02-24", "2015-09-06"], + "C": [123.45, 987.65], + }) + + self.pd_date_time = pd.DataFrame({ + "A": ["2015-01-23 00:12:34", "2013-06-30 12:50:59"], + "B": [123.45, 987.65], + }) + + self.pd_time = pd.DataFrame({ + "A": ["00:12:34", "12:50:59"], + "B": [123.45, 987.65], + }) + + def test_date(self): + """Test""" + df2 = self.node.f(self.pd_date, column_names="A,B", date_format="%Y-%m-%d")[0] + self.assertEqual(df2.at[0, "A"], pd.Timestamp("2015-01-23")) + self.assertEqual(df2.at[0, "C"], 123.45) + + def test_date_time(self): + """Test""" + df3 = self.node.f(self.pd_date_time, column_names="A", date_format="%Y-%m-%d %H:%M:%S")[0] + self.assertTrue(ptypes.is_datetime64_any_dtype(df3["A"])) + self.assertTrue(ptypes.is_float_dtype(df3["B"])) + self.assertEqual(df3.at[0, "A"], pd.Timestamp("2015-01-23 00:12:34")) + self.assertEqual(df3.at[0, "B"], 123.45) + + def test_time(self): + """Test""" + df3 = self.node.f(self.pd_time, column_names="A", date_format="%H:%M:%S")[0] + self.assertTrue(ptypes.is_datetime64_any_dtype(df3["A"])) + self.assertEqual(df3.at[0, "A"], pd.Timestamp("1900-01-01 00:12:34")) + self.assertEqual(df3.at[0, "B"], 123.45) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pandas_wrapper/pandas_to_strftime_test.py b/tests/pandas_wrapper/pandas_to_strftime_test.py new file mode 100644 index 0000000..8c0a0de --- /dev/null +++ b/tests/pandas_wrapper/pandas_to_strftime_test.py @@ -0,0 +1,63 @@ +import os +import sys +import unittest +import pandas as pd +import pandas.api.types as ptypes + +PROJECT_ROOT = os.path.realpath(os.path.join(__file__, "..", "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +print(MODULE_ROOT) + +from pandas_wrapper.pandas_strftime import PandasStrftime + +class Test(unittest.TestCase): + def setUp(self): + """Set up test data""" + self.node = PandasStrftime() + + self.pd_date = pd.DataFrame({ + "A": [pd.Timestamp("2015-01-23"), pd.Timestamp("2013-06-30")], + "B": [pd.Timestamp("2005-02-24"), pd.Timestamp("2015-09-06")], + "C": [123.45, 987.65], + }) + + self.pd_date_time = pd.DataFrame({ + "A": [pd.Timestamp("2015-01-23 00:12:34"), pd.Timestamp("2013-06-30 12:50:59")], + "B": [123.45, 987.65], + }) + + self.pd_time = pd.DataFrame({ + "A": [pd.Timestamp("1900-01-01 00:12:34"), pd.Timestamp("1900-01-01 12:50:59")], + "B": [123.45, 987.65], + }) + + def test_date(self): + """Test date formatting""" + df2 = self.node.f(self.pd_date, column_names="A,B", date_format="%Y-%m-%d")[0] + self.assertTrue(ptypes.is_string_dtype(df2["A"])) + self.assertTrue(ptypes.is_string_dtype(df2["B"])) + self.assertEqual(df2.at[0, "A"], "2015-01-23") + self.assertEqual(df2.at[1, "B"], "2015-09-06") + self.assertEqual(df2.at[0, "C"], 123.45) # Ensure non-date columns are unchanged + + def test_date_time(self): + """Test datetime formatting""" + df3 = self.node.f(self.pd_date_time, column_names="A", date_format="%Y-%m-%d %H:%M:%S")[0] + self.assertTrue(ptypes.is_string_dtype(df3["A"])) + self.assertTrue(ptypes.is_float_dtype(df3["B"])) + self.assertEqual(df3.at[0, "A"], "2015-01-23 00:12:34") + self.assertEqual(df3.at[1, "A"], "2013-06-30 12:50:59") + self.assertEqual(df3.at[0, "B"], 123.45) + + def test_time(self): + """Test time formatting""" + df4 = self.node.f(self.pd_time, column_names="A", date_format="%H:%M:%S")[0] + self.assertTrue(ptypes.is_string_dtype(df4["A"])) + self.assertEqual(df4.at[0, "A"], "00:12:34") + self.assertEqual(df4.at[1, "A"], "12:50:59") + self.assertEqual(df4.at[0, "B"], 123.45) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pandas/utils_index_json_test.py b/tests/pandas_wrapper/utils_index_json_test.py similarity index 100% rename from tests/pandas/utils_index_json_test.py rename to tests/pandas_wrapper/utils_index_json_test.py diff --git a/tests/pandas/utils_series_json_test.py b/tests/pandas_wrapper/utils_series_json_test.py similarity index 100% rename from tests/pandas/utils_series_json_test.py rename to tests/pandas_wrapper/utils_series_json_test.py