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
1 change: 1 addition & 0 deletions changelog/39.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Renamed `df` to `pobj` in [pandas_openscm.index_manipulation.update_index_levels_func] and [pandas_openscm.index_manipulation.update_index_levels_from_other_func]
1 change: 1 addition & 0 deletions changelog/39.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
More methods for [pandas_openscm.accessors.series.PandasSeriesOpenSCMAccessor] and [pandas_openscm.accessors.index.PandasIndexOpenSCMAccessor], leading to feature completeness ([#25](https://github.com/openscm/pandas-openscm/issues/25)), specifically [pandas_openscm.accessors.series.PandasSeriesOpenSCMAccessor.update_index_levels], [pandas_openscm.accessors.series.PandasSeriesOpenSCMAccessor.update_index_levels_from_other], [pandas_openscm.accessors.index.PandasIndexOpenSCMAccessor.update_levels] and [pandas_openscm.accessors.index.PandasIndexOpenSCMAccessor.update_levels_from_other]
123 changes: 121 additions & 2 deletions src/pandas_openscm/accessors/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Generic, TypeVar
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar

import pandas as pd

from pandas_openscm.index_manipulation import ensure_is_multiindex
from pandas_openscm.index_manipulation import (
ensure_is_multiindex,
update_levels,
update_levels_from_other,
)

if TYPE_CHECKING:
# Hmm this is somehow not correct.
Expand Down Expand Up @@ -72,3 +76,118 @@ def eim(self) -> pd.MultiIndex:
this is a no-op (although the value of copy is respected).
"""
return self.ensure_is_multiindex()

def update_levels(
self,
updates: dict[Any, Callable[[Any], Any]],
remove_unused_levels: bool = True,
) -> pd.MultiIndex:
"""
Update the levels

Parameters
----------
updates
Updates to apply

Each key is the level to which the updates will be applied.
Each value is a function which updates the level to its new values.

remove_unused_levels
Remove unused levels before applying the update

Specifically, call
[pd.MultiIndex.remove_unused_levels][pandas.MultiIndex.remove_unused_levels].

This avoids trying to update levels that aren't being used.

Returns
-------
:
`index` with updates applied
"""
if not isinstance(self._index, pd.MultiIndex):
msg = (
"This method is only intended to be used "
"when index is an instance of `MultiIndex`. "
f"Received {type(self._index)}"
)
raise TypeError(msg)

return update_levels(
self._index,
updates=updates,
remove_unused_levels=remove_unused_levels,
)

def update_levels_from_other(
self,
update_sources: dict[
Any,
tuple[
Any,
Callable[[Any], Any] | dict[Any, Any] | pd.Series[Any],
]
| tuple[
tuple[Any, ...],
Callable[[tuple[Any, ...]], Any]
| dict[tuple[Any, ...], Any]
| pd.Series[Any],
],
],
remove_unused_levels: bool = True,
) -> pd.MultiIndex:
"""
Update levels based on other levels

If the level to be updated doesn't exist, it is created.

Parameters
----------
update_sources
Updates to apply and their source levels

Each key is the level to which the updates will be applied
(or the level that will be created if it doesn't already exist).

There are two options for the values.

The first is used when only one level is used to update the 'target level'.
In this case, each value is a tuple of which the first element
is the level to use to generate the values (the 'source level')
and the second is mapper of the form used by
[pd.Index.map][pandas.Index.map]
which will be applied to the source level
to update/create the level of interest.

Each value is a tuple of which the first element
is the level or levels (if a tuple)
to use to generate the values (the 'source level')
and the second is mapper of the form used by
[pd.Index.map][pandas.Index.map]
which will be applied to the source level
to update/create the level of interest.

remove_unused_levels
Call `ini.remove_unused_levels` before updating the levels

This avoids trying to update based on levels that aren't being used.

Returns
-------
:
`index` with updates applied
"""
if not isinstance(self._index, pd.MultiIndex):
msg = (
"This method is only intended to be used "
"when index is an instance of `MultiIndex`. "
f"Received {type(self._index)}"
)
raise TypeError(msg)

return update_levels_from_other(
self._index,
update_sources=update_sources,
remove_unused_levels=remove_unused_levels,
)
190 changes: 101 additions & 89 deletions src/pandas_openscm/accessors/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from __future__ import annotations

from collections.abc import Collection, Mapping
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar

import pandas as pd

Expand All @@ -17,6 +17,8 @@
convert_index_to_category_index,
ensure_index_is_multiindex,
set_index_levels_func,
update_index_levels_from_other_func,
update_index_levels_func,
)
from pandas_openscm.indexing import mi_loc
from pandas_openscm.unit_conversion import convert_unit, convert_unit_like
Expand Down Expand Up @@ -387,91 +389,101 @@ def to_category_index(self) -> S:
# Figuring this out is a job for another day
return res # type: ignore

# def update_index_levels(
# self,
# updates: dict[Any, Callable[[Any], Any]],
# copy: bool = True,
# remove_unused_levels: bool = True,
# ) -> pd.DataFrame:
# """
# Update the index levels
#
# Parameters
# ----------
# updates
# Updates to apply to the index levels
#
# Each key is the index level to which the updates will be applied.
# Each value is a function which updates the levels to their new values.
#
# copy
# Should the [pd.DataFrame][pandas.DataFrame] be copied before returning?
#
# remove_unused_levels
# Remove unused levels before applying the update
#
# Specifically, call
# [pd.MultiIndex.remove_unused_levels][pandas.MultiIndex.remove_unused_levels]. # noqa: E501
#
# This avoids trying to update levels that aren't being used.
#
# Returns
# -------
# :
# [pd.DataFrame][pandas.DataFrame] with updates applied to its index
# """
# return update_index_levels_func(
# self._df,
# updates=updates,
# copy=copy,
# remove_unused_levels=remove_unused_levels,
# )
#
# def update_index_levels_from_other(
# self,
# update_sources: dict[
# Any, tuple[Any, Callable[[Any], Any] | dict[Any, Any] | pd.Series[Any]]
# ],
# copy: bool = True,
# remove_unused_levels: bool = True,
# ) -> pd.DataFrame:
# """
# Update the index levels based on other index levels
#
# Parameters
# ----------
# update_sources
# Updates to apply to `df`'s index
#
# Each key is the level to which the updates will be applied
# (or the level that will be created if it doesn't already exist).
#
# Each value is a tuple of which the first element
# is the level to use to generate the values (the 'source level')
# and the second is mapper of the form used by
# [pd.Index.map][pandas.Index.map]
# which will be applied to the source level
# to update/create the level of interest.
#
# copy
# Should the [pd.DataFrame][pandas.DataFrame] be copied before returning?
#
# remove_unused_levels
# Remove unused levels before applying the update
#
# Specifically, call
# [pd.MultiIndex.remove_unused_levels][pandas.MultiIndex.remove_unused_levels]. # noqa: E501
#
# This avoids trying to update levels that aren't being used.
#
# Returns
# -------
# :
# [pd.DataFrame][pandas.DataFrame] with updates applied to its index
# """
# return update_index_levels_from_other_func(
# self._df,
# update_sources=update_sources,
# copy=copy,
# remove_unused_levels=remove_unused_levels,
# )
def update_index_levels(
self,
updates: dict[Any, Callable[[Any], Any]],
copy: bool = True,
remove_unused_levels: bool = True,
) -> S:
"""
Update the index levels

Parameters
----------
updates
Updates to apply to the index levels

Each key is the index level to which the updates will be applied.
Each value is a function which updates the levels to their new values.

copy
Should the [pd.Series][pandas.Series] be copied before returning?

remove_unused_levels
Remove unused levels before applying the update

Specifically, call
[pd.MultiIndex.remove_unused_levels][pandas.MultiIndex.remove_unused_levels].

This avoids trying to update levels that aren't being used.

Returns
-------
:
[pd.Series][pandas.Series] with updates applied to its index
"""
res = update_index_levels_func(
self._series,
updates=updates,
copy=copy,
remove_unused_levels=remove_unused_levels,
)

# Ignore return type
# because I've done something wrong with how I've set this up.
# Figuring this out is a job for another day
return res # type: ignore

def update_index_levels_from_other(
self,
update_sources: dict[
Any, tuple[Any, Callable[[Any], Any] | dict[Any, Any] | pd.Series[Any]]
],
copy: bool = True,
remove_unused_levels: bool = True,
) -> S:
"""
Update the index levels based on other index levels

Parameters
----------
update_sources
Updates to apply to the index levels

Each key is the level to which the updates will be applied
(or the level that will be created if it doesn't already exist).

Each value is a tuple of which the first element
is the level to use to generate the values (the 'source level')
and the second is mapper of the form used by
[pd.Index.map][pandas.Index.map]
which will be applied to the source level
to update/create the level of interest.

copy
Should the [pd.Series][pandas.Series] be copied before returning?

remove_unused_levels
Remove unused levels before applying the update

Specifically, call
[pd.MultiIndex.remove_unused_levels][pandas.MultiIndex.remove_unused_levels].

This avoids trying to update levels that aren't being used.

Returns
-------
:
[pd.Series][pandas.Series] with updates applied to its index
"""
res = update_index_levels_from_other_func(
self._series,
update_sources=update_sources,
copy=copy,
remove_unused_levels=remove_unused_levels,
)

# Ignore return type
# because I've done something wrong with how I've set this up.
# Figuring this out is a job for another day
return res # type: ignore
Loading