Skip to content

Commit b3339a6

Browse files
dweindlclaude
andcommitted
Fix pandas 3.1 deprecations, require pandas>=2.3
* Don't pass `float_precision` to `read_csv` for pandas>=3.1, where round-trip float parsing is the default * Replace deprecated `inplace=True` by reassignment. As a consequence, `get_{condition,observable,parameter,mapping}_df` no longer modify DataFrames passed by the caller * Avoid `fillna` with incompatible fill values * Use a full-length key when adding the sum row to a MultiIndex DataFrame in `get_data_per_observable` * Treat pandas deprecation warnings as errors in tests. They derive from `DeprecationWarning`, which is ignored in `pytest.ini` * Require pandas>=2.3 in accordance with NEP 29 Closes #570 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 73b680c commit b3339a6

24 files changed

Lines changed: 118 additions & 84 deletions

‎conftest.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""pytest configuration."""
2+
3+
import pandas as pd
4+
5+
6+
def pytest_configure(config):
7+
# pandas deprecation warnings are subclasses of DeprecationWarning,
8+
# which is ignored in pytest.ini. Treat them as errors.
9+
# PandasChangeWarning is only available in pandas>=3.0.
10+
if hasattr(pd.errors, "PandasChangeWarning"):
11+
config.addinivalue_line(
12+
"filterwarnings", "error::pandas.errors.PandasChangeWarning"
13+
)

‎petab/_utils.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22

33
from pathlib import Path
44

5+
import pandas as pd
56
from pydantic import AnyUrl, TypeAdapter
67

78
PathOrUrlAdapter = TypeAdapter(AnyUrl | Path)
89

10+
#: Extra keyword arguments for :func:`pandas.read_csv` to ensure lossless
11+
#: float parsing. ``float_precision`` is deprecated in pandas>=3.1,
12+
#: where round-trip parsing is the default.
13+
READ_CSV_FLOAT_KWARGS = (
14+
{"float_precision": "round_trip"}
15+
if tuple(map(int, pd.__version__.split(".")[:2])) < (3, 1)
16+
else {}
17+
)
18+
919

1020
def _generate_path(
1121
file_path: str | Path | AnyUrl,

‎petab/v1/conditions.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import numpy as np
77
import pandas as pd
88

9+
from .._utils import READ_CSV_FLOAT_KWARGS
910
from . import core, lint
1011
from .C import *
1112

@@ -32,21 +33,20 @@ def get_condition_df(
3233

3334
if isinstance(condition_file, str | Path):
3435
condition_file = pd.read_csv(
35-
condition_file, sep="\t", float_precision="round_trip"
36+
condition_file, sep="\t", **READ_CSV_FLOAT_KWARGS
3637
)
3738

3839
lint.assert_no_leading_trailing_whitespace(
3940
condition_file.columns.values, "condition"
4041
)
4142

4243
if not isinstance(condition_file.index, pd.RangeIndex):
43-
condition_file.reset_index(
44+
condition_file = condition_file.reset_index(
4445
drop=condition_file.index.name != CONDITION_ID,
45-
inplace=True,
4646
)
4747

4848
try:
49-
condition_file.set_index([CONDITION_ID], inplace=True)
49+
condition_file = condition_file.set_index([CONDITION_ID])
5050
except KeyError:
5151
raise KeyError(
5252
f"Condition table missing mandatory field {CONDITION_ID}."
@@ -90,7 +90,7 @@ def create_condition_df(
9090
raise ValueError("Invalid parameter ID: " + p)
9191
df[p] = np.nan
9292

93-
df.set_index(CONDITION_ID, inplace=True)
93+
df = df.set_index(CONDITION_ID)
9494

9595
return df
9696

‎petab/v1/core.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import pandas as pd
1515
from pandas.api.types import is_string_dtype
1616

17+
from .._utils import READ_CSV_FLOAT_KWARGS
1718
from . import yaml
1819
from .C import * # noqa: F403
1920

@@ -52,7 +53,7 @@ def get_simulation_df(simulation_file: str | Path) -> pd.DataFrame:
5253
Simulation DataFrame
5354
"""
5455
return pd.read_csv(
55-
simulation_file, sep="\t", index_col=None, float_precision="round_trip"
56+
simulation_file, sep="\t", index_col=None, **READ_CSV_FLOAT_KWARGS
5657
)
5758

5859

@@ -94,7 +95,7 @@ def get_visualization_df(
9495
sep="\t",
9596
index_col=None,
9697
converters=types,
97-
float_precision="round_trip",
98+
**READ_CSV_FLOAT_KWARGS,
9899
)
99100
except pd.errors.EmptyDataError:
100101
warn(

‎petab/v1/mapping.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pandas as pd
77

8+
from .._utils import READ_CSV_FLOAT_KWARGS
89
from . import lint
910
from .C import * # noqa: F403
1011
from .models import Model
@@ -34,13 +35,12 @@ def get_mapping_df(
3435

3536
if isinstance(mapping_file, str | Path):
3637
mapping_file = pd.read_csv(
37-
mapping_file, sep="\t", float_precision="round_trip"
38+
mapping_file, sep="\t", **READ_CSV_FLOAT_KWARGS
3839
)
3940

4041
if not isinstance(mapping_file.index, pd.RangeIndex):
41-
mapping_file.reset_index(
42+
mapping_file = mapping_file.reset_index(
4243
drop=mapping_file.index.name != PETAB_ENTITY_ID,
43-
inplace=True,
4444
)
4545

4646
for col in MAPPING_DF_REQUIRED_COLS:
@@ -51,7 +51,7 @@ def get_mapping_df(
5151
mapping_file.reset_index()[col].values, col
5252
)
5353

54-
mapping_file.set_index([PETAB_ENTITY_ID], inplace=True)
54+
mapping_file = mapping_file.set_index([PETAB_ENTITY_ID])
5555

5656
return mapping_file
5757

‎petab/v1/measurements.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import numpy as np
1010
import pandas as pd
1111

12+
from .._utils import READ_CSV_FLOAT_KWARGS
1213
from . import core, lint, observables
1314
from .C import * # noqa: F403
1415

@@ -43,7 +44,7 @@ def get_measurement_df(
4344

4445
if isinstance(measurement_file, str | Path):
4546
measurement_file = pd.read_csv(
46-
measurement_file, sep="\t", float_precision="round_trip"
47+
measurement_file, sep="\t", **READ_CSV_FLOAT_KWARGS
4748
)
4849

4950
lint.assert_no_leading_trailing_whitespace(
@@ -95,7 +96,8 @@ def get_simulation_conditions(measurement_df: pd.DataFrame) -> pd.DataFrame:
9596
# We require NaN-containing rows, but they are ignored by `groupby`,
9697
# therefore replace them before
9798
simulation_conditions = (
98-
measurement_df.fillna("")
99+
measurement_df[grouping_cols]
100+
.fillna("")
99101
.groupby(grouping_cols)
100102
.size()
101103
.reset_index()[grouping_cols]
@@ -130,7 +132,9 @@ def get_rows_for_condition(
130132
# check for equality in all grouping cols
131133
if PREEQUILIBRATION_CONDITION_ID in condition:
132134
row_filter = (
133-
measurement_df[PREEQUILIBRATION_CONDITION_ID].fillna("")
135+
measurement_df[PREEQUILIBRATION_CONDITION_ID]
136+
.astype("object")
137+
.fillna("")
134138
== condition[PREEQUILIBRATION_CONDITION_ID]
135139
) & row_filter
136140
if SIMULATION_CONDITION_ID in condition:
@@ -252,7 +256,7 @@ def measurements_have_replicates(measurement_df: pd.DataFrame) -> bool:
252256
],
253257
)
254258
return np.any(
255-
measurement_df.fillna("").groupby(grouping_cols).size().values - 1
259+
measurement_df.groupby(grouping_cols, dropna=False).size().values - 1
256260
)
257261

258262

‎petab/v1/observables.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import pandas as pd
99

10+
from .._utils import READ_CSV_FLOAT_KWARGS
1011
from . import core, lint
1112
from .C import * # noqa: F403
1213
from .math import sympify_petab
@@ -39,21 +40,20 @@ def get_observable_df(
3940

4041
if isinstance(observable_file, str | Path):
4142
observable_file = pd.read_csv(
42-
observable_file, sep="\t", float_precision="round_trip"
43+
observable_file, sep="\t", **READ_CSV_FLOAT_KWARGS
4344
)
4445

4546
lint.assert_no_leading_trailing_whitespace(
4647
observable_file.columns.values, "observable"
4748
)
4849

4950
if not isinstance(observable_file.index, pd.RangeIndex):
50-
observable_file.reset_index(
51+
observable_file = observable_file.reset_index(
5152
drop=observable_file.index.name != OBSERVABLE_ID,
52-
inplace=True,
5353
)
5454

5555
try:
56-
observable_file.set_index([OBSERVABLE_ID], inplace=True)
56+
observable_file = observable_file.set_index([OBSERVABLE_ID])
5757
except KeyError:
5858
raise KeyError(
5959
f"Observable table missing mandatory field {OBSERVABLE_ID}."

‎petab/v1/parameters.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import numpy as np
1414
import pandas as pd
1515

16+
from .._utils import READ_CSV_FLOAT_KWARGS
1617
from . import conditions, core, lint, measurements, observables
1718
from .C import * # noqa: F403
1819
from .models import Model
@@ -58,7 +59,7 @@ def get_parameter_df(
5859
parameter_df = parameter_file
5960
elif isinstance(parameter_file, str | Path):
6061
parameter_df = pd.read_csv(
61-
parameter_file, sep="\t", float_precision="round_trip"
62+
parameter_file, sep="\t", **READ_CSV_FLOAT_KWARGS
6263
)
6364
elif isinstance(parameter_file, Iterable):
6465
dfs = [get_parameter_df(x) for x in parameter_file if x]
@@ -77,13 +78,12 @@ def get_parameter_df(
7778
)
7879

7980
if not isinstance(parameter_df.index, pd.RangeIndex):
80-
parameter_df.reset_index(
81+
parameter_df = parameter_df.reset_index(
8182
drop=parameter_file.index.name != PARAMETER_ID,
82-
inplace=True,
8383
)
8484

8585
try:
86-
parameter_df.set_index([PARAMETER_ID], inplace=True)
86+
parameter_df = parameter_df.set_index([PARAMETER_ID])
8787
except KeyError as e:
8888
raise KeyError(
8989
f"Parameter table missing mandatory field {PARAMETER_ID}."
@@ -239,7 +239,7 @@ def create_parameter_df(
239239
OBJECTIVE_PRIOR_PARAMETERS: "",
240240
}
241241
)
242-
df.set_index([PARAMETER_ID], inplace=True)
242+
df = df.set_index([PARAMETER_ID])
243243

244244
# For model parameters, set nominal values as defined in the model
245245
for parameter_id in df.index:

‎petab/v1/simplify.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ def remove_nan_measurements(problem: Problem):
2323
"""Drop any measurements that are NaN"""
2424
problem.measurement_df = problem.measurement_df[
2525
~problem.measurement_df[MEASUREMENT].isna()
26-
]
27-
problem.measurement_df.reset_index(inplace=True, drop=True)
26+
].reset_index(drop=True)
2827

2928

3029
def remove_unused_observables(problem: Problem):
@@ -103,8 +102,7 @@ def condition_parameters_to_parameter_table(problem: Problem):
103102
}
104103
for parameter_id, value in replacements.items()
105104
]
106-
rows = pd.DataFrame(rows)
107-
rows.set_index(PARAMETER_ID, inplace=True)
105+
rows = pd.DataFrame(rows).set_index(PARAMETER_ID)
108106

109107
if problem.parameter_df is None:
110108
problem.parameter_df = rows

‎petab/v1/visualize/data_overview.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ def get_data_per_observable(measurement_df: pd.DataFrame) -> pd.DataFrame:
8383
)
8484

8585
# Add row and column sums
86-
data_per_observable.loc["SUM", :] = data_per_observable.sum(axis=0).values
86+
sum_row = "SUM" if len(index) == 1 else ("SUM", "")
87+
data_per_observable.loc[sum_row, :] = data_per_observable.sum(
88+
axis=0
89+
).values
8790
data_per_observable["SUM"] = data_per_observable.sum(axis=1).values
8891

8992
data_per_observable = data_per_observable.astype(int)

0 commit comments

Comments
 (0)