From 7e4354dfd8dfd27282bfe1a992bb54e1c1277a28 Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Wed, 4 Mar 2026 16:14:28 +0100 Subject: [PATCH 1/3] Close #229 Problem was created from creating np.nan values in all columns and then converting with pandas. --- src/petab_gui/commands.py | 45 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/petab_gui/commands.py b/src/petab_gui/commands.py index 60a3e01..8516756 100644 --- a/src/petab_gui/commands.py +++ b/src/petab_gui/commands.py @@ -155,7 +155,38 @@ def redo(self): if np.any(dtypes != df.dtypes): for col, dtype in dtypes.items(): if dtype != df.dtypes[col]: - df[col] = df[col].astype(dtype) + is_pandas_nullable_int = isinstance( + dtype, + ( + pd.Int64Dtype, + pd.Int32Dtype, + pd.Int16Dtype, + pd.Int8Dtype, + ), + ) + + if is_pandas_nullable_int: + # Keep pandas nullable integer types as is + df[col] = df[col].astype(dtype) + # If column has NaN and dtype is integer, use nullable Int type + elif ( + np.issubdtype(dtype, np.integer) + and df[col].isna().any() + ): + # Convert numpy int types to pandas nullable Int types + if dtype == np.int64: + df[col] = df[col].astype("Int64") + elif dtype == np.int32: + df[col] = df[col].astype("Int32") + elif dtype == np.int16: + df[col] = df[col].astype("Int16") + elif dtype == np.int8: + df[col] = df[col].astype("Int8") + else: + # Fallback for other integer types + df[col] = df[col].astype("Int64") + else: + df[col] = df[col].astype(dtype) self.model.endInsertRows() else: self.model.beginRemoveRows( @@ -261,7 +292,17 @@ def _apply_changes(self, use_new: bool): for col, dtype in original_dtypes.items(): if col not in update_df.columns: continue - if np.issubdtype(dtype, np.number): + # Check if it's a pandas extension dtype (like Int64) + is_pandas_nullable_int = isinstance( + dtype, + (pd.Int64Dtype, pd.Int32Dtype, pd.Int16Dtype, pd.Int8Dtype), + ) + + if is_pandas_nullable_int: + # Keep pandas nullable integer types as is + df[col] = pd.to_numeric(df[col], errors="coerce") + df[col] = df[col].astype(dtype) + elif np.issubdtype(dtype, np.number): df[col] = pd.to_numeric(df[col], errors="coerce") else: df[col] = df[col].astype(dtype) From 50fdf42c233d1f7c9a9eee4df18f58368c760d57 Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Wed, 4 Mar 2026 16:34:51 +0100 Subject: [PATCH 2/3] additional commit --- src/petab_gui/commands.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/petab_gui/commands.py b/src/petab_gui/commands.py index 8516756..90d8397 100644 --- a/src/petab_gui/commands.py +++ b/src/petab_gui/commands.py @@ -304,6 +304,18 @@ def _apply_changes(self, use_new: bool): df[col] = df[col].astype(dtype) elif np.issubdtype(dtype, np.number): df[col] = pd.to_numeric(df[col], errors="coerce") + # If original dtype was integer and column has NaN, use nullable Int type + if np.issubdtype(dtype, np.integer) and df[col].isna().any(): + if dtype == np.int64: + df[col] = df[col].astype("Int64") + elif dtype == np.int32: + df[col] = df[col].astype("Int32") + elif dtype == np.int16: + df[col] = df[col].astype("Int16") + elif dtype == np.int8: + df[col] = df[col].astype("Int8") + else: + df[col] = df[col].astype("Int64") else: df[col] = df[col].astype(dtype) From 152827aa373f74a85c000e9b2fe63390ad39b8be Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Mon, 16 Mar 2026 12:39:52 +0100 Subject: [PATCH 3/3] Put into function --- src/petab_gui/commands.py | 99 +++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/src/petab_gui/commands.py b/src/petab_gui/commands.py index 90d8397..85a6646 100644 --- a/src/petab_gui/commands.py +++ b/src/petab_gui/commands.py @@ -8,6 +8,45 @@ pd.set_option("future.no_silent_downcasting", True) +def _convert_dtype_with_nullable_int(series, dtype): + """Convert a series to the specified dtype, handling nullable integers. + + When converting to integer types and the series contains NaN values, + this function automatically uses pandas nullable integer types (Int64, Int32, etc.) + instead of numpy integer types which don't support NaN. + + Args: + series: The pandas Series to convert + dtype: The target dtype + + Returns: + The series with the appropriate dtype applied + """ + # Check if it's already a pandas nullable int type + is_pandas_nullable_int = isinstance( + dtype, + (pd.Int64Dtype, pd.Int32Dtype, pd.Int16Dtype, pd.Int8Dtype), + ) + + if is_pandas_nullable_int: + # Keep pandas nullable integer types as is + return series.astype(dtype) + # If column has NaN and dtype is integer, use nullable Int type + if np.issubdtype(dtype, np.integer) and series.isna().any(): + # Convert numpy int types to pandas nullable Int types + if dtype == np.int64: + return series.astype("Int64") + if dtype == np.int32: + return series.astype("Int32") + if dtype == np.int16: + return series.astype("Int16") + if dtype == np.int8: + return series.astype("Int8") + # Fallback for other integer types + return series.astype("Int64") + return series.astype(dtype) + + class ModifyColumnCommand(QUndoCommand): """Command to add or remove a column in the table. @@ -155,38 +194,9 @@ def redo(self): if np.any(dtypes != df.dtypes): for col, dtype in dtypes.items(): if dtype != df.dtypes[col]: - is_pandas_nullable_int = isinstance( - dtype, - ( - pd.Int64Dtype, - pd.Int32Dtype, - pd.Int16Dtype, - pd.Int8Dtype, - ), + df[col] = _convert_dtype_with_nullable_int( + df[col], dtype ) - - if is_pandas_nullable_int: - # Keep pandas nullable integer types as is - df[col] = df[col].astype(dtype) - # If column has NaN and dtype is integer, use nullable Int type - elif ( - np.issubdtype(dtype, np.integer) - and df[col].isna().any() - ): - # Convert numpy int types to pandas nullable Int types - if dtype == np.int64: - df[col] = df[col].astype("Int64") - elif dtype == np.int32: - df[col] = df[col].astype("Int32") - elif dtype == np.int16: - df[col] = df[col].astype("Int16") - elif dtype == np.int8: - df[col] = df[col].astype("Int8") - else: - # Fallback for other integer types - df[col] = df[col].astype("Int64") - else: - df[col] = df[col].astype(dtype) self.model.endInsertRows() else: self.model.beginRemoveRows( @@ -292,32 +302,17 @@ def _apply_changes(self, use_new: bool): for col, dtype in original_dtypes.items(): if col not in update_df.columns: continue - # Check if it's a pandas extension dtype (like Int64) + + # For numeric types, convert string inputs to numbers first is_pandas_nullable_int = isinstance( dtype, (pd.Int64Dtype, pd.Int32Dtype, pd.Int16Dtype, pd.Int8Dtype), ) - - if is_pandas_nullable_int: - # Keep pandas nullable integer types as is + if is_pandas_nullable_int or np.issubdtype(dtype, np.number): df[col] = pd.to_numeric(df[col], errors="coerce") - df[col] = df[col].astype(dtype) - elif np.issubdtype(dtype, np.number): - df[col] = pd.to_numeric(df[col], errors="coerce") - # If original dtype was integer and column has NaN, use nullable Int type - if np.issubdtype(dtype, np.integer) and df[col].isna().any(): - if dtype == np.int64: - df[col] = df[col].astype("Int64") - elif dtype == np.int32: - df[col] = df[col].astype("Int32") - elif dtype == np.int16: - df[col] = df[col].astype("Int16") - elif dtype == np.int8: - df[col] = df[col].astype("Int8") - else: - df[col] = df[col].astype("Int64") - else: - df[col] = df[col].astype(dtype) + + # Convert to appropriate dtype, handling nullable integers + df[col] = _convert_dtype_with_nullable_int(df[col], dtype) rows = [df.index.get_loc(row_key) for (row_key, _) in self.changes] cols = [