Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions petab/v2/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ def _convert_nan_to_none(v):
return v


def _reset_index(df: pd.DataFrame) -> pd.DataFrame:
"""Move named index levels to columns and drop unnamed ones.

Avoids spurious ``index`` columns for DataFrames with a default index.
"""
return df.reset_index(drop=all(name is None for name in df.index.names))


def _valid_petab_id(v: str) -> str:
"""Field validator for PEtab IDs."""
if not v:
Expand Down Expand Up @@ -411,10 +419,10 @@ def from_df(cls, df: pd.DataFrame, **kwargs) -> ObservableTable:
if df is None:
return cls(**kwargs)

df = get_observable_df(df)
df = get_observable_df(df.copy())
observables = [
Observable(**row.to_dict())
for _, row in df.reset_index().iterrows()
for _, row in _reset_index(df).iterrows()
]
return cls(observables, **kwargs)

Expand Down Expand Up @@ -786,7 +794,7 @@ def convert_nan_to_none(cls, v, info: ValidationInfo):
)
@classmethod
def _sympify_list(cls, v):
if v is None:
if v is None or v == "":
return []

if isinstance(v, float) and np.isnan(v):
Expand Down Expand Up @@ -814,14 +822,15 @@ def from_df(cls, df: pd.DataFrame, **kwargs) -> MeasurementTable:
if df is None:
return cls(**kwargs)

df = _reset_index(df)
if C.MODEL_ID in df.columns:
df[C.MODEL_ID] = df[C.MODEL_ID].apply(_convert_nan_to_none)

measurements = [
Measurement(
**row.to_dict(),
)
for _, row in df.reset_index().iterrows()
for _, row in df.iterrows()
]

return cls(measurements, **kwargs)
Expand Down Expand Up @@ -893,7 +902,7 @@ def from_df(cls, df: pd.DataFrame, **kwargs) -> MappingTable:
return cls(**kwargs)

mappings = [
Mapping(**row.to_dict()) for _, row in df.reset_index().iterrows()
Mapping(**row.to_dict()) for _, row in _reset_index(df).iterrows()
]
return cls(mappings, **kwargs)

Expand Down Expand Up @@ -1103,7 +1112,7 @@ def from_df(cls, df: pd.DataFrame, **kwargs) -> ParameterTable:

parameters = [
Parameter(**row.to_dict())
for _, row in df.reset_index().iterrows()
for _, row in _reset_index(df).iterrows()
]

return cls(parameters, **kwargs)
Expand Down
97 changes: 97 additions & 0 deletions tests/v2/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,103 @@ def test_condition_table_round_trip():
assert conditions == conditions2


@pytest.mark.parametrize(
"table_cls,df",
[
(
ParameterTable,
pd.DataFrame(
{
PARAMETER_ID: ["k1", "k2"],
LOWER_BOUND: [0.1, 0.1],
UPPER_BOUND: [10.0, 10.0],
NOMINAL_VALUE: [1.0, 2.0],
ESTIMATE: [True, False],
}
),
),
(
ObservableTable,
pd.DataFrame(
{
OBSERVABLE_ID: ["obs1", "obs2"],
OBSERVABLE_FORMULA: ["x", "y"],
NOISE_FORMULA: ["1", "2"],
}
),
),
(
MeasurementTable,
pd.DataFrame(
{
C.OBSERVABLE_ID: ["obs1", "obs2"],
C.TIME: [0.0, 1.0],
C.MEASUREMENT: [1.0, 2.0],
}
),
),
(
MappingTable,
pd.DataFrame(
{
PETAB_ENTITY_ID: ["a", "b"],
MODEL_ENTITY_ID: ["1a", "1b"],
}
),
),
],
)
def test_from_df_ignores_unnamed_index(table_cls, df):
"""Unnamed indices must not end up as extra columns."""
# non-default unnamed index
df.index = [5, 7]
df_orig = df.copy()
table = table_cls.from_df(df)
assert_frame_equal(df, df_orig)
Comment on lines +116 to +119

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this assert do? It looks like this must be equal because one is a copy of the other?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to the df = get_observable_df(df.copy()) line in petab/v2/core.py. Previously, from_df would mutated the input via get_observable_df.

out_df = table.to_df()
cols = set(out_df.columns) | set(filter(None, out_df.index.names))
assert not cols & {"index", "level_0"}
assert table_cls.from_df(out_df) == table

# a named index is kept as a column
id_col = df.columns[0]
table2 = table_cls.from_df(df.set_index(id_col))
assert table2 == table


@pytest.mark.filterwarnings(
"ignore:.*Parameter scales are not supported in PEtab v2.*:UserWarning"
)
def test_problem_to_files_round_trip_columns():
"""Saving and reloading a problem must not add columns."""
with tempfile.TemporaryDirectory() as tmp_dir:
petab1to2(example_dir_fujita / "Fujita.yaml", tmp_dir)
yaml_file = Path(tmp_dir, "Fujita.yaml")
problem = Problem.from_yaml(yaml_file)

def get_columns(p: Problem):
return {
name: set(getattr(p, name).columns)
| set(filter(None, getattr(p, name).index.names))
for name in (
"parameter_df",
"measurement_df",
"observable_df",
"condition_df",
"experiment_df",
)
}

expected = get_columns(problem)
for i in range(2):
out_dir = Path(tmp_dir, f"round{i}")
problem.to_files(out_dir)
problem = Problem.from_yaml(out_dir / yaml_file.name)
assert get_columns(problem) == expected
for cols in expected.values():
assert not cols & {"index", "level_0"}


@pytest.mark.filterwarnings(
"ignore:.*Parameter scales are not supported in PEtab v2.*:UserWarning"
)
Expand Down
Loading