Skip to content

Commit 577b337

Browse files
committed
Merge branch 'master' of https://github.com/pandas-dev/pandas into nat_sub
2 parents c0a39df + 7208610 commit 577b337

File tree

8 files changed

+151
-32
lines changed

8 files changed

+151
-32
lines changed

doc/source/whatsnew/v0.23.0.txt

+32
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,34 @@ If installed, we now require:
225225
| openpyxl | 2.4.0 | |
226226
+-----------------+-----------------+----------+
227227

228+
.. _whatsnew_0230.api_breaking.deprecate_panel:
229+
230+
Deprecate Panel
231+
^^^^^^^^^^^^^^^
232+
233+
``Panel`` was deprecated in the 0.20.x release, showing as a ``DeprecationWarning``. Using ``Panel`` will now show a ``FutureWarning``. The recommended way to represent 3-D data are
234+
with a ``MultiIndex`` on a ``DataFrame`` via the :meth:`~Panel.to_frame` or with the `xarray package <http://xarray.pydata.org/en/stable/>`__. Pandas
235+
provides a :meth:`~Panel.to_xarray` method to automate this conversion. For more details see :ref:`Deprecate Panel <dsintro.deprecate_panel>` documentation. (:issue:`13563`, :issue:`18324`).
236+
237+
.. ipython:: python
238+
:okwarning:
239+
240+
p = tm.makePanel()
241+
p
242+
243+
Convert to a MultiIndex DataFrame
244+
245+
.. ipython:: python
246+
247+
p.to_frame()
248+
249+
Convert to an xarray DataArray
250+
251+
.. ipython:: python
252+
:okwarning:
253+
254+
p.to_xarray()
255+
228256

229257
Build Changes
230258
^^^^^^^^^^^^^
@@ -273,6 +301,7 @@ Other API Changes
273301
- The default ``Timedelta`` constructor now accepts an ``ISO 8601 Duration`` string as an argument (:issue:`19040`)
274302
- ``IntervalDtype`` now returns ``True`` when compared against ``'interval'`` regardless of subtype, and ``IntervalDtype.name`` now returns ``'interval'`` regardless of subtype (:issue:`18980`)
275303
- :func:`Series.to_csv` now accepts a ``compression`` argument that works in the same way as the ``compression`` argument in :func:`DataFrame.to_csv` (:issue:`18958`)
304+
- Addition or subtraction of ``NaT`` from :class:`TimedeltaIndex` will return ``TimedeltaIndex`` instead of ``DatetimeIndex`` (:issue:`19124`)
276305

277306
.. _whatsnew_0230.deprecations:
278307

@@ -290,6 +319,7 @@ Deprecations
290319
- :func:`read_excel` has deprecated the ``skip_footer`` parameter. Use ``skipfooter`` instead (:issue:`18836`)
291320
- The ``is_copy`` attribute is deprecated and will be removed in a future version (:issue:`18801`).
292321

322+
293323
.. _whatsnew_0230.prior_deprecations:
294324

295325
Removal of prior version deprecations/changes
@@ -454,6 +484,8 @@ Reshaping
454484
^^^^^^^^^
455485

456486
- Bug in :func:`DataFrame.stack` which fails trying to sort mixed type levels under Python 3 (:issue:`18310`)
487+
- Bug in :func:`DataFrame.unstack` which casts int to float if ``columns`` is a ``MultiIndex`` with unused levels (:issue:`17845`)
488+
- Bug in :func:`DataFrame.unstack` which raises an error if ``index`` is a ``MultiIndex`` with unused labels on the unstacked level (:issue:`18562`)
457489
- Fixed construction of a :class:`Series` from a ``dict`` containing ``NaN`` as key (:issue:`18480`)
458490
- Bug in :func:`Series.rank` where ``Series`` containing ``NaT`` modifies the ``Series`` inplace (:issue:`18521`)
459491
- Bug in :func:`cut` which fails when using readonly arrays (:issue:`18773`)

pandas/core/indexes/timedeltas.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,8 @@ def _add_datelike(self, other):
403403
# adding a timedeltaindex to a datetimelike
404404
from pandas import Timestamp, DatetimeIndex
405405
if other is NaT:
406-
result = self._nat_new(box=False)
406+
# GH#19124 pd.NaT is treated like a timedelta
407+
return self._nat_new()
407408
else:
408409
other = Timestamp(other)
409410
i8 = self.asi8
@@ -413,12 +414,13 @@ def _add_datelike(self, other):
413414
return DatetimeIndex(result, name=self.name, copy=False)
414415

415416
def _sub_datelike(self, other):
416-
from pandas import DatetimeIndex
417+
# GH#19124 Timedelta - datetime is not in general well-defined.
418+
# We make an exception for pd.NaT, which in this case quacks
419+
# like a timedelta.
417420
if other is NaT:
418-
result = self._nat_new(box=False)
421+
return self._nat_new()
419422
else:
420423
raise TypeError("cannot subtract a datelike from a TimedeltaIndex")
421-
return DatetimeIndex(result, name=self.name, copy=False)
422424

423425
def _add_offset_array(self, other):
424426
# Array/Index of DateOffset objects

pandas/core/panel.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def __init__(self, data=None, items=None, major_axis=None, minor_axis=None,
151151
"http://xarray.pydata.org/en/stable/.\n"
152152
"Pandas provides a `.to_xarray()` method to help "
153153
"automate this conversion.\n",
154-
DeprecationWarning, stacklevel=3)
154+
FutureWarning, stacklevel=3)
155155

156156
self._init_data(data=data, items=items, major_axis=major_axis,
157157
minor_axis=minor_axis, copy=copy, dtype=dtype)

pandas/core/reshape/reshape.py

+19-18
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,19 @@ def __init__(self, values, index, level=-1, value_columns=None,
112112
if value_columns is None and values.shape[1] != 1: # pragma: no cover
113113
raise ValueError('must pass column labels for multi-column data')
114114

115-
self.index = index
115+
self.index = index.remove_unused_levels()
116116

117117
self.level = self.index._get_level_number(level)
118118

119119
# when index includes `nan`, need to lift levels/strides by 1
120120
self.lift = 1 if -1 in self.index.labels[self.level] else 0
121121

122-
self.new_index_levels = list(index.levels)
123-
self.new_index_names = list(index.names)
122+
self.new_index_levels = list(self.index.levels)
123+
self.new_index_names = list(self.index.names)
124124

125125
self.removed_name = self.new_index_names.pop(self.level)
126126
self.removed_level = self.new_index_levels.pop(self.level)
127+
self.removed_level_full = index.levels[self.level]
127128

128129
self._make_sorted_values_labels()
129130
self._make_selectors()
@@ -173,21 +174,10 @@ def _make_selectors(self):
173174
self.compressor = comp_index.searchsorted(np.arange(ngroups))
174175

175176
def get_result(self):
176-
# TODO: find a better way than this masking business
177-
178-
values, value_mask = self.get_new_values()
177+
values, _ = self.get_new_values()
179178
columns = self.get_new_columns()
180179
index = self.get_new_index()
181180

182-
# filter out missing levels
183-
if values.shape[1] > 0:
184-
col_inds, obs_ids = compress_group_index(self.sorted_labels[-1])
185-
# rare case, level values not observed
186-
if len(obs_ids) < self.full_shape[1]:
187-
inds = (value_mask.sum(0) > 0).nonzero()[0]
188-
values = algos.take_nd(values, inds, axis=1)
189-
columns = columns[inds]
190-
191181
# may need to coerce categoricals here
192182
if self.is_categorical is not None:
193183
categories = self.is_categorical.categories
@@ -275,17 +265,28 @@ def get_new_columns(self):
275265
width = len(self.value_columns)
276266
propagator = np.repeat(np.arange(width), stride)
277267
if isinstance(self.value_columns, MultiIndex):
278-
new_levels = self.value_columns.levels + (self.removed_level,)
268+
new_levels = self.value_columns.levels + (self.removed_level_full,)
279269
new_names = self.value_columns.names + (self.removed_name,)
280270

281271
new_labels = [lab.take(propagator)
282272
for lab in self.value_columns.labels]
283273
else:
284-
new_levels = [self.value_columns, self.removed_level]
274+
new_levels = [self.value_columns, self.removed_level_full]
285275
new_names = [self.value_columns.name, self.removed_name]
286276
new_labels = [propagator]
287277

288-
new_labels.append(np.tile(np.arange(stride) - self.lift, width))
278+
# The two indices differ only if the unstacked level had unused items:
279+
if len(self.removed_level_full) != len(self.removed_level):
280+
# In this case, we remap the new labels to the original level:
281+
repeater = self.removed_level_full.get_indexer(self.removed_level)
282+
if self.lift:
283+
repeater = np.insert(repeater, 0, -1)
284+
else:
285+
# Otherwise, we just use each level item exactly once:
286+
repeater = np.arange(stride) - self.lift
287+
288+
# The entire level is then just a repetition of the single chunk:
289+
new_labels.append(np.tile(repeater, width))
289290
return MultiIndex(levels=new_levels, labels=new_labels,
290291
names=new_names, verify_integrity=False)
291292

pandas/core/window.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1286,7 +1286,7 @@ class Expanding(_Rolling_and_Expanding):
12861286
12871287
Parameters
12881288
----------
1289-
min_periods : int, default None
1289+
min_periods : int, default 1
12901290
Minimum number of observations in window required to have a value
12911291
(otherwise result is NA).
12921292
center : boolean, default False

pandas/tests/frame/test_reshape.py

+68
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,74 @@ def test_unstack_dtypes(self):
560560
assert left.shape == (3, 2)
561561
tm.assert_frame_equal(left, right)
562562

563+
def test_unstack_unused_levels(self):
564+
# GH 17845: unused labels in index make unstack() cast int to float
565+
idx = pd.MultiIndex.from_product([['a'], ['A', 'B', 'C', 'D']])[:-1]
566+
df = pd.DataFrame([[1, 0]] * 3, index=idx)
567+
568+
result = df.unstack()
569+
exp_col = pd.MultiIndex.from_product([[0, 1], ['A', 'B', 'C']])
570+
expected = pd.DataFrame([[1, 1, 1, 0, 0, 0]], index=['a'],
571+
columns=exp_col)
572+
tm.assert_frame_equal(result, expected)
573+
assert((result.columns.levels[1] == idx.levels[1]).all())
574+
575+
# Unused items on both levels
576+
levels = [[0, 1, 7], [0, 1, 2, 3]]
577+
labels = [[0, 0, 1, 1], [0, 2, 0, 2]]
578+
idx = pd.MultiIndex(levels, labels)
579+
block = np.arange(4).reshape(2, 2)
580+
df = pd.DataFrame(np.concatenate([block, block + 4]), index=idx)
581+
result = df.unstack()
582+
expected = pd.DataFrame(np.concatenate([block * 2, block * 2 + 1],
583+
axis=1),
584+
columns=idx)
585+
tm.assert_frame_equal(result, expected)
586+
assert((result.columns.levels[1] == idx.levels[1]).all())
587+
588+
# With mixed dtype and NaN
589+
levels = [['a', 2, 'c'], [1, 3, 5, 7]]
590+
labels = [[0, -1, 1, 1], [0, 2, -1, 2]]
591+
idx = pd.MultiIndex(levels, labels)
592+
data = np.arange(8)
593+
df = pd.DataFrame(data.reshape(4, 2), index=idx)
594+
595+
cases = ((0, [13, 16, 6, 9, 2, 5, 8, 11],
596+
[np.nan, 'a', 2], [np.nan, 5, 1]),
597+
(1, [8, 11, 1, 4, 12, 15, 13, 16],
598+
[np.nan, 5, 1], [np.nan, 'a', 2]))
599+
for level, idces, col_level, idx_level in cases:
600+
result = df.unstack(level=level)
601+
exp_data = np.zeros(18) * np.nan
602+
exp_data[idces] = data
603+
cols = pd.MultiIndex.from_product([[0, 1], col_level])
604+
expected = pd.DataFrame(exp_data.reshape(3, 6),
605+
index=idx_level, columns=cols)
606+
# Broken (GH 18455):
607+
# tm.assert_frame_equal(result, expected)
608+
diff = result - expected
609+
assert(diff.sum().sum() == 0)
610+
assert((diff + 1).sum().sum() == 8)
611+
612+
assert((result.columns.levels[1] == idx.levels[level]).all())
613+
614+
@pytest.mark.parametrize("cols", [['A', 'C'], slice(None)])
615+
def test_unstack_unused_level(self, cols):
616+
# GH 18562 : unused labels on the unstacked level
617+
df = pd.DataFrame([[2010, 'a', 'I'],
618+
[2011, 'b', 'II']],
619+
columns=['A', 'B', 'C'])
620+
621+
ind = df.set_index(['A', 'B', 'C'], drop=False)
622+
selection = ind.loc[(slice(None), slice(None), 'I'), cols]
623+
result = selection.unstack()
624+
625+
expected = ind.iloc[[0]][cols]
626+
expected.columns = MultiIndex.from_product([expected.columns, ['I']],
627+
names=[None, 'C'])
628+
expected.index = expected.index.droplevel('C')
629+
tm.assert_frame_equal(result, expected)
630+
563631
def test_unstack_nan_index(self): # GH7466
564632
cast = lambda val: '{0:1}'.format('' if val != val else val)
565633
nan = np.nan

pandas/tests/generic/test_label_or_level_utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def df_duplabels(df):
4646

4747
@pytest.fixture
4848
def panel():
49-
with tm.assert_produces_warning(DeprecationWarning,
49+
with tm.assert_produces_warning(FutureWarning,
5050
check_stacklevel=False):
5151
return pd.Panel()
5252

pandas/tests/scalar/test_nat.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -302,14 +302,30 @@ def test_nat_arithmetic_index():
302302
tm.assert_index_equal(left - right, exp)
303303
tm.assert_index_equal(right - left, exp)
304304

305-
# timedelta
305+
# timedelta # GH#19124
306306
tdi = TimedeltaIndex(['1 day', '2 day'], name='x')
307-
exp = DatetimeIndex([NaT, NaT], name='x')
308-
for (left, right) in [(NaT, tdi)]:
309-
tm.assert_index_equal(left + right, exp)
310-
tm.assert_index_equal(right + left, exp)
311-
tm.assert_index_equal(left - right, exp)
312-
tm.assert_index_equal(right - left, exp)
307+
tdi_nat = TimedeltaIndex([NaT, NaT], name='x')
308+
309+
tm.assert_index_equal(tdi + NaT, tdi_nat)
310+
tm.assert_index_equal(NaT + tdi, tdi_nat)
311+
tm.assert_index_equal(tdi - NaT, tdi_nat)
312+
tm.assert_index_equal(NaT - tdi, tdi_nat)
313+
314+
315+
@pytest.mark.parametrize('box, assert_func', [
316+
(TimedeltaIndex, tm.assert_index_equal),
317+
pytest.param(Series, tm.assert_series_equal,
318+
marks=pytest.mark.xfail(reason='NaT - Series returns NaT'))
319+
])
320+
def test_nat_arithmetic_td64_vector(box, assert_func):
321+
# GH#19124
322+
vec = box(['1 day', '2 day'], dtype='timedelta64[ns]')
323+
box_nat = box([NaT, NaT], dtype='timedelta64[ns]')
324+
325+
assert_func(vec + NaT, box_nat)
326+
assert_func(NaT + vec, box_nat)
327+
assert_func(vec - NaT, box_nat)
328+
assert_func(NaT - vec, box_nat)
313329

314330

315331
def test_nat_pinned_docstrings():

0 commit comments

Comments
 (0)