Skip to content

POC: Implement pythonic label-based index slicing NDFrame.slice(closed='left') #27060

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
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
61 changes: 61 additions & 0 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2073,6 +2073,67 @@ def __setstate__(self, state):

self._item_cache = {}

def slice(self, *args, closed="left", axis=0, **kwds):
"""A minimalist, pythonic, function for slicing a Pandas-Object

slice(stop)
slice(start, stop[, step]
slice(start, stop[, step], closed="left")
slice(start, stop[, step], axis=1)

Parameters
----------
start : optional, default None
stop : optional, default None
step : integer, optional, default 1
closed : str, default 'left'
'left'/'right'/'both'/'neither'
axis : int, default 0

Returns
-------
Series or DataFrame
"""
assert axis in (0, 1)
step = None
if len(args) < 1:
raise TypeError("TypeError: slice expected at least 1 arguments, got 0")
elif len(args) == 1:
start = None
stop = args[0]
elif len(args) < 4:
values = [None] * 3
values[: len(args)] = args
start, stop, step = values
else:
msg = "TypeError: slice expected 3 arguments, got %d"
raise TypeError(msg % len(args))

obj = self
if axis == 1:
obj = obj.T

if closed == "both":
obj = obj.loc[slice(start, stop, step)]
elif closed == "left":
obj = obj.loc_left[slice(start, stop, step)]
elif closed == "right":
raise NotImplementedError
elif closed == "neither":
raise NotImplementedError
else:
raise ValueError(
(
"closed='%s' is Invalid. "
" Valid values for 'closed' are 'left', "
"'right', 'both', or 'neither'"
)
% closed
)
if axis == 1:
obj = obj.T
return obj

# ----------------------------------------------------------------------
# Rendering Methods

Expand Down
20 changes: 13 additions & 7 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3101,7 +3101,7 @@ def _filter_indexer_tolerance(self, target, indexer, tolerance):

@Appender(_index_shared_docs["_convert_scalar_indexer"])
def _convert_scalar_indexer(self, key, kind=None):
assert kind in ["ix", "loc", "getitem", "iloc", None]
assert kind in ["ix", "loc", "loc_left", "getitem", "iloc", None]

if kind == "iloc":
return self._validate_indexer("positional", key, kind)
Expand Down Expand Up @@ -3131,7 +3131,7 @@ def _convert_scalar_indexer(self, key, kind=None):
]:
return self._invalid_indexer("label", key)

elif kind in ["loc"] and is_integer(key):
elif kind in ["loc", "loc_left"] and is_integer(key):
if not self.holds_integer():
return self._invalid_indexer("label", key)

Expand All @@ -3153,7 +3153,7 @@ def _convert_scalar_indexer(self, key, kind=None):

@Appender(_index_shared_docs["_convert_slice_indexer"])
def _convert_slice_indexer(self, key, kind=None):
assert kind in ["ix", "loc", "getitem", "iloc", None]
assert kind in ["ix", "loc", "loc_left", "getitem", "iloc", None]

# if we are not a slice, then we are done
if not isinstance(key, slice):
Expand Down Expand Up @@ -5094,7 +5094,7 @@ def _validate_indexer(self, form, key, kind):

@Appender(_index_shared_docs["_maybe_cast_slice_bound"])
def _maybe_cast_slice_bound(self, label, side, kind):
assert kind in ["ix", "loc", "getitem", None]
assert kind in ["ix", "loc", "loc_left", "getitem", None]

# We are a plain index here (sub-class override this method if they
# wish to have special treatment for floats/ints, e.g. Float64Index and
Expand Down Expand Up @@ -5143,7 +5143,7 @@ def get_slice_bound(self, label, side, kind):
int
Index of label.
"""
assert kind in ["ix", "loc", "getitem", None]
assert kind in ["ix", "loc", "loc_left", "getitem", None]

if side not in ("left", "right"):
raise ValueError(
Expand Down Expand Up @@ -5184,10 +5184,16 @@ def get_slice_bound(self, label, side, kind):
if side == "left":
return slc.start
else:
return slc.stop
if kind == "loc_left":
return slc.start
else:
return slc.stop
else:
if side == "right":
return slc + 1
if kind == "loc_left":
return slc
else:
return slc + 1
else:
return slc

Expand Down
2 changes: 1 addition & 1 deletion pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ def _convert_scalar_indexer(self, key, kind=None):
kind : {'ix', 'loc', 'getitem', 'iloc'} or None
"""

assert kind in ["ix", "loc", "getitem", "iloc", None]
assert kind in ["ix", "loc", "loc_left", "getitem", "iloc", None]

# we don't allow integer/float indexing for loc
# we don't allow float indexing for ix/getitem
Expand Down
11 changes: 8 additions & 3 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from pandas._libs import Timestamp, index as libindex, lib, tslib as libts
import pandas._libs.join as libjoin
from pandas._libs.tslibs import ccalendar, fields, parsing, timezones
from pandas._libs.tslibs import Timedelta, ccalendar, fields, parsing, timezones
from pandas.util._decorators import Appender, Substitution, cache_readonly

from pandas.core.dtypes.common import (
Expand Down Expand Up @@ -1094,7 +1094,7 @@ def _maybe_cast_slice_bound(self, label, side, kind):
Value of `side` parameter should be validated in caller.

"""
assert kind in ["ix", "loc", "getitem", None]
assert kind in ["ix", "loc", "loc_left", "getitem", None]

if is_float(label) or isinstance(label, time) or is_integer(label):
self._invalid_indexer("slice", label)
Expand All @@ -1111,7 +1111,12 @@ def _maybe_cast_slice_bound(self, label, side, kind):
# and length 1 index)
if self._is_strictly_monotonic_decreasing and len(self) > 1:
return upper if side == "left" else lower
return lower if side == "left" else upper
if side == "left":
return lower
if kind == "loc_left":
return lower - Timedelta(1, "ns")
else:
return upper
else:
return label

Expand Down
8 changes: 4 additions & 4 deletions pandas/core/indexes/numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None, fastpath=None):

@Appender(_index_shared_docs["_maybe_cast_slice_bound"])
def _maybe_cast_slice_bound(self, label, side, kind):
assert kind in ["ix", "loc", "getitem", None]
assert kind in ["ix", "loc", "loc_left", "getitem", None]

# we will try to coerce to integers
return self._maybe_cast_indexer(label)
Expand Down Expand Up @@ -237,7 +237,7 @@ def asi8(self):

@Appender(_index_shared_docs["_convert_scalar_indexer"])
def _convert_scalar_indexer(self, key, kind=None):
assert kind in ["ix", "loc", "getitem", "iloc", None]
assert kind in ["ix", "loc", "loc_left", "getitem", "iloc", None]

# don't coerce ilocs to integers
if kind != "iloc":
Expand Down Expand Up @@ -292,7 +292,7 @@ def asi8(self):

@Appender(_index_shared_docs["_convert_scalar_indexer"])
def _convert_scalar_indexer(self, key, kind=None):
assert kind in ["ix", "loc", "getitem", "iloc", None]
assert kind in ["ix", "loc", "loc_left", "getitem", "iloc", None]

# don't coerce ilocs to integers
if kind != "iloc":
Expand Down Expand Up @@ -377,7 +377,7 @@ def astype(self, dtype, copy=True):

@Appender(_index_shared_docs["_convert_scalar_indexer"])
def _convert_scalar_indexer(self, key, kind=None):
assert kind in ["ix", "loc", "getitem", "iloc", None]
assert kind in ["ix", "loc", "loc_left", "getitem", "iloc", None]

if kind == "iloc":
return self._validate_indexer("positional", key, kind)
Expand Down
7 changes: 5 additions & 2 deletions pandas/core/indexes/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,15 +746,18 @@ def _maybe_cast_slice_bound(self, label, side, kind):
Value of `side` parameter should be validated in caller.

"""
assert kind in ["ix", "loc", "getitem"]
assert kind in ["ix", "loc", "loc_left", "getitem"]

if isinstance(label, datetime):
return Period(label, freq=self.freq)
elif isinstance(label, str):
try:
_, parsed, reso = parse_time_string(label, self.freq)
bounds = self._parsed_string_to_bounds(reso, parsed)
return bounds[0 if side == "left" else 1]
if kind == "loc_left":
return bounds[0]
else:
return bounds[0 if side == "left" else 1]
except Exception:
raise KeyError(label)
elif is_integer(label) or is_float(label):
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/indexes/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def _maybe_cast_slice_bound(self, label, side, kind):
label : object

"""
assert kind in ["ix", "loc", "getitem", None]
assert kind in ["ix", "loc", "loc_left", "getitem", None]

if isinstance(label, str):
parsed = Timedelta(label)
Expand Down
5 changes: 3 additions & 2 deletions pandas/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def get_indexers_list():
("loc", _LocIndexer),
("at", _AtIndexer),
("iat", _iAtIndexer),
("loc_left", _LocIndexer),
]


Expand Down Expand Up @@ -1190,7 +1191,7 @@ def _validate_read_indexer(

# We (temporarily) allow for some missing keys with .loc, except in
# some cases (e.g. setting) in which "raise_missing" will be False
if not (self.name == "loc" and not raise_missing):
if not (self.name in ("loc", "loc_left") and not raise_missing):
not_found = list(set(key) - set(ax))
raise KeyError("{} not in index".format(not_found))

Expand Down Expand Up @@ -1269,7 +1270,7 @@ def _convert_to_indexer(
if is_setter:

# always valid
if self.name == "loc":
if self.name in ("loc", "loc_left"):
return {"key": obj}

# a positional
Expand Down
Loading