You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
590 lines
19 KiB
590 lines
19 KiB
import numpy as np
|
|
import pytest
|
|
|
|
from pandas.errors import SettingWithCopyError
|
|
import pandas.util._test_decorators as td
|
|
|
|
import pandas as pd
|
|
from pandas import (
|
|
DataFrame,
|
|
MultiIndex,
|
|
Series,
|
|
date_range,
|
|
isna,
|
|
notna,
|
|
)
|
|
import pandas._testing as tm
|
|
|
|
|
|
def assert_equal(a, b):
|
|
assert a == b
|
|
|
|
|
|
class TestMultiIndexSetItem:
|
|
def check(self, target, indexers, value, compare_fn=assert_equal, expected=None):
|
|
target.loc[indexers] = value
|
|
result = target.loc[indexers]
|
|
if expected is None:
|
|
expected = value
|
|
compare_fn(result, expected)
|
|
|
|
def test_setitem_multiindex(self):
|
|
# GH#7190
|
|
cols = ["A", "w", "l", "a", "x", "X", "d", "profit"]
|
|
index = MultiIndex.from_product(
|
|
[np.arange(0, 100), np.arange(0, 80)], names=["time", "firm"]
|
|
)
|
|
t, n = 0, 2
|
|
|
|
df = DataFrame(
|
|
np.nan,
|
|
columns=cols,
|
|
index=index,
|
|
)
|
|
self.check(target=df, indexers=((t, n), "X"), value=0)
|
|
|
|
df = DataFrame(-999, columns=cols, index=index)
|
|
self.check(target=df, indexers=((t, n), "X"), value=1)
|
|
|
|
df = DataFrame(columns=cols, index=index)
|
|
self.check(target=df, indexers=((t, n), "X"), value=2)
|
|
|
|
# gh-7218: assigning with 0-dim arrays
|
|
df = DataFrame(-999, columns=cols, index=index)
|
|
self.check(
|
|
target=df,
|
|
indexers=((t, n), "X"),
|
|
value=np.array(3),
|
|
expected=3,
|
|
)
|
|
|
|
def test_setitem_multiindex2(self):
|
|
# GH#5206
|
|
df = DataFrame(
|
|
np.arange(25).reshape(5, 5), columns="A,B,C,D,E".split(","), dtype=float
|
|
)
|
|
df["F"] = 99
|
|
row_selection = df["A"] % 2 == 0
|
|
col_selection = ["B", "C"]
|
|
df.loc[row_selection, col_selection] = df["F"]
|
|
output = DataFrame(99.0, index=[0, 2, 4], columns=["B", "C"])
|
|
tm.assert_frame_equal(df.loc[row_selection, col_selection], output)
|
|
self.check(
|
|
target=df,
|
|
indexers=(row_selection, col_selection),
|
|
value=df["F"],
|
|
compare_fn=tm.assert_frame_equal,
|
|
expected=output,
|
|
)
|
|
|
|
def test_setitem_multiindex3(self):
|
|
# GH#11372
|
|
idx = MultiIndex.from_product(
|
|
[["A", "B", "C"], date_range("2015-01-01", "2015-04-01", freq="MS")]
|
|
)
|
|
cols = MultiIndex.from_product(
|
|
[["foo", "bar"], date_range("2016-01-01", "2016-02-01", freq="MS")]
|
|
)
|
|
|
|
df = DataFrame(
|
|
np.random.default_rng(2).random((12, 4)), index=idx, columns=cols
|
|
)
|
|
|
|
subidx = MultiIndex.from_arrays(
|
|
[["A", "A"], date_range("2015-01-01", "2015-02-01", freq="MS")]
|
|
)
|
|
subcols = MultiIndex.from_arrays(
|
|
[["foo", "foo"], date_range("2016-01-01", "2016-02-01", freq="MS")]
|
|
)
|
|
|
|
vals = DataFrame(
|
|
np.random.default_rng(2).random((2, 2)), index=subidx, columns=subcols
|
|
)
|
|
self.check(
|
|
target=df,
|
|
indexers=(subidx, subcols),
|
|
value=vals,
|
|
compare_fn=tm.assert_frame_equal,
|
|
)
|
|
# set all columns
|
|
vals = DataFrame(
|
|
np.random.default_rng(2).random((2, 4)), index=subidx, columns=cols
|
|
)
|
|
self.check(
|
|
target=df,
|
|
indexers=(subidx, slice(None, None, None)),
|
|
value=vals,
|
|
compare_fn=tm.assert_frame_equal,
|
|
)
|
|
# identity
|
|
copy = df.copy()
|
|
self.check(
|
|
target=df,
|
|
indexers=(df.index, df.columns),
|
|
value=df,
|
|
compare_fn=tm.assert_frame_equal,
|
|
expected=copy,
|
|
)
|
|
|
|
# TODO(ArrayManager) df.loc["bar"] *= 2 doesn't raise an error but results in
|
|
# all NaNs -> doesn't work in the "split" path (also for BlockManager actually)
|
|
@td.skip_array_manager_not_yet_implemented
|
|
def test_multiindex_setitem(self):
|
|
# GH 3738
|
|
# setting with a multi-index right hand side
|
|
arrays = [
|
|
np.array(["bar", "bar", "baz", "qux", "qux", "bar"]),
|
|
np.array(["one", "two", "one", "one", "two", "one"]),
|
|
np.arange(0, 6, 1),
|
|
]
|
|
|
|
df_orig = DataFrame(
|
|
np.random.default_rng(2).standard_normal((6, 3)),
|
|
index=arrays,
|
|
columns=["A", "B", "C"],
|
|
).sort_index()
|
|
|
|
expected = df_orig.loc[["bar"]] * 2
|
|
df = df_orig.copy()
|
|
df.loc[["bar"]] *= 2
|
|
tm.assert_frame_equal(df.loc[["bar"]], expected)
|
|
|
|
# raise because these have differing levels
|
|
msg = "cannot align on a multi-index with out specifying the join levels"
|
|
with pytest.raises(TypeError, match=msg):
|
|
df.loc["bar"] *= 2
|
|
|
|
def test_multiindex_setitem2(self):
|
|
# from SO
|
|
# https://stackoverflow.com/questions/24572040/pandas-access-the-level-of-multiindex-for-inplace-operation
|
|
df_orig = DataFrame.from_dict(
|
|
{
|
|
"price": {
|
|
("DE", "Coal", "Stock"): 2,
|
|
("DE", "Gas", "Stock"): 4,
|
|
("DE", "Elec", "Demand"): 1,
|
|
("FR", "Gas", "Stock"): 5,
|
|
("FR", "Solar", "SupIm"): 0,
|
|
("FR", "Wind", "SupIm"): 0,
|
|
}
|
|
}
|
|
)
|
|
df_orig.index = MultiIndex.from_tuples(
|
|
df_orig.index, names=["Sit", "Com", "Type"]
|
|
)
|
|
|
|
expected = df_orig.copy()
|
|
expected.iloc[[0, 1, 3]] *= 2
|
|
|
|
idx = pd.IndexSlice
|
|
df = df_orig.copy()
|
|
df.loc[idx[:, :, "Stock"], :] *= 2
|
|
tm.assert_frame_equal(df, expected)
|
|
|
|
df = df_orig.copy()
|
|
df.loc[idx[:, :, "Stock"], "price"] *= 2
|
|
tm.assert_frame_equal(df, expected)
|
|
|
|
def test_multiindex_assignment(self):
|
|
# GH3777 part 2
|
|
|
|
# mixed dtype
|
|
df = DataFrame(
|
|
np.random.default_rng(2).integers(5, 10, size=9).reshape(3, 3),
|
|
columns=list("abc"),
|
|
index=[[4, 4, 8], [8, 10, 12]],
|
|
)
|
|
df["d"] = np.nan
|
|
arr = np.array([0.0, 1.0])
|
|
|
|
df.loc[4, "d"] = arr
|
|
tm.assert_series_equal(df.loc[4, "d"], Series(arr, index=[8, 10], name="d"))
|
|
|
|
def test_multiindex_assignment_single_dtype(
|
|
self, using_copy_on_write, warn_copy_on_write
|
|
):
|
|
# GH3777 part 2b
|
|
# single dtype
|
|
arr = np.array([0.0, 1.0])
|
|
|
|
df = DataFrame(
|
|
np.random.default_rng(2).integers(5, 10, size=9).reshape(3, 3),
|
|
columns=list("abc"),
|
|
index=[[4, 4, 8], [8, 10, 12]],
|
|
dtype=np.int64,
|
|
)
|
|
view = df["c"].iloc[:2].values
|
|
|
|
# arr can be losslessly cast to int, so this setitem is inplace
|
|
# INFO(CoW-warn) this does not warn because we directly took .values
|
|
# above, so no reference to a pandas object is alive for `view`
|
|
df.loc[4, "c"] = arr
|
|
exp = Series(arr, index=[8, 10], name="c", dtype="int64")
|
|
result = df.loc[4, "c"]
|
|
tm.assert_series_equal(result, exp)
|
|
|
|
# extra check for inplace-ness
|
|
if not using_copy_on_write:
|
|
tm.assert_numpy_array_equal(view, exp.values)
|
|
|
|
# arr + 0.5 cannot be cast losslessly to int, so we upcast
|
|
with tm.assert_produces_warning(
|
|
FutureWarning, match="item of incompatible dtype"
|
|
):
|
|
df.loc[4, "c"] = arr + 0.5
|
|
result = df.loc[4, "c"]
|
|
exp = exp + 0.5
|
|
tm.assert_series_equal(result, exp)
|
|
|
|
# scalar ok
|
|
with tm.assert_cow_warning(warn_copy_on_write):
|
|
df.loc[4, "c"] = 10
|
|
exp = Series(10, index=[8, 10], name="c", dtype="float64")
|
|
tm.assert_series_equal(df.loc[4, "c"], exp)
|
|
|
|
# invalid assignments
|
|
msg = "Must have equal len keys and value when setting with an iterable"
|
|
with pytest.raises(ValueError, match=msg):
|
|
df.loc[4, "c"] = [0, 1, 2, 3]
|
|
|
|
with pytest.raises(ValueError, match=msg):
|
|
df.loc[4, "c"] = [0]
|
|
|
|
# But with a length-1 listlike column indexer this behaves like
|
|
# `df.loc[4, "c"] = 0
|
|
with tm.assert_cow_warning(warn_copy_on_write):
|
|
df.loc[4, ["c"]] = [0]
|
|
assert (df.loc[4, "c"] == 0).all()
|
|
|
|
def test_groupby_example(self):
|
|
# groupby example
|
|
NUM_ROWS = 100
|
|
NUM_COLS = 10
|
|
col_names = ["A" + num for num in map(str, np.arange(NUM_COLS).tolist())]
|
|
index_cols = col_names[:5]
|
|
|
|
df = DataFrame(
|
|
np.random.default_rng(2).integers(5, size=(NUM_ROWS, NUM_COLS)),
|
|
dtype=np.int64,
|
|
columns=col_names,
|
|
)
|
|
df = df.set_index(index_cols).sort_index()
|
|
grp = df.groupby(level=index_cols[:4])
|
|
df["new_col"] = np.nan
|
|
|
|
# we are actually operating on a copy here
|
|
# but in this case, that's ok
|
|
for name, df2 in grp:
|
|
new_vals = np.arange(df2.shape[0])
|
|
df.loc[name, "new_col"] = new_vals
|
|
|
|
def test_series_setitem(
|
|
self, multiindex_year_month_day_dataframe_random_data, warn_copy_on_write
|
|
):
|
|
ymd = multiindex_year_month_day_dataframe_random_data
|
|
s = ymd["A"]
|
|
|
|
with tm.assert_cow_warning(warn_copy_on_write):
|
|
s[2000, 3] = np.nan
|
|
assert isna(s.values[42:65]).all()
|
|
assert notna(s.values[:42]).all()
|
|
assert notna(s.values[65:]).all()
|
|
|
|
with tm.assert_cow_warning(warn_copy_on_write):
|
|
s[2000, 3, 10] = np.nan
|
|
assert isna(s.iloc[49])
|
|
|
|
with pytest.raises(KeyError, match="49"):
|
|
# GH#33355 dont fall-back to positional when leading level is int
|
|
s[49]
|
|
|
|
def test_frame_getitem_setitem_boolean(self, multiindex_dataframe_random_data):
|
|
frame = multiindex_dataframe_random_data
|
|
df = frame.T.copy()
|
|
values = df.values.copy()
|
|
|
|
result = df[df > 0]
|
|
expected = df.where(df > 0)
|
|
tm.assert_frame_equal(result, expected)
|
|
|
|
df[df > 0] = 5
|
|
values[values > 0] = 5
|
|
tm.assert_almost_equal(df.values, values)
|
|
|
|
df[df == 5] = 0
|
|
values[values == 5] = 0
|
|
tm.assert_almost_equal(df.values, values)
|
|
|
|
# a df that needs alignment first
|
|
df[df[:-1] < 0] = 2
|
|
np.putmask(values[:-1], values[:-1] < 0, 2)
|
|
tm.assert_almost_equal(df.values, values)
|
|
|
|
with pytest.raises(TypeError, match="boolean values only"):
|
|
df[df * 0] = 2
|
|
|
|
def test_frame_getitem_setitem_multislice(self):
|
|
levels = [["t1", "t2"], ["a", "b", "c"]]
|
|
codes = [[0, 0, 0, 1, 1], [0, 1, 2, 0, 1]]
|
|
midx = MultiIndex(codes=codes, levels=levels, names=[None, "id"])
|
|
df = DataFrame({"value": [1, 2, 3, 7, 8]}, index=midx)
|
|
|
|
result = df.loc[:, "value"]
|
|
tm.assert_series_equal(df["value"], result)
|
|
|
|
result = df.loc[df.index[1:3], "value"]
|
|
tm.assert_series_equal(df["value"][1:3], result)
|
|
|
|
result = df.loc[:, :]
|
|
tm.assert_frame_equal(df, result)
|
|
|
|
result = df
|
|
df.loc[:, "value"] = 10
|
|
result["value"] = 10
|
|
tm.assert_frame_equal(df, result)
|
|
|
|
df.loc[:, :] = 10
|
|
tm.assert_frame_equal(df, result)
|
|
|
|
def test_frame_setitem_multi_column(self):
|
|
df = DataFrame(
|
|
np.random.default_rng(2).standard_normal((10, 4)),
|
|
columns=[["a", "a", "b", "b"], [0, 1, 0, 1]],
|
|
)
|
|
|
|
cp = df.copy()
|
|
cp["a"] = cp["b"]
|
|
tm.assert_frame_equal(cp["a"], cp["b"])
|
|
|
|
# set with ndarray
|
|
cp = df.copy()
|
|
cp["a"] = cp["b"].values
|
|
tm.assert_frame_equal(cp["a"], cp["b"])
|
|
|
|
def test_frame_setitem_multi_column2(self):
|
|
# ---------------------------------------
|
|
# GH#1803
|
|
columns = MultiIndex.from_tuples([("A", "1"), ("A", "2"), ("B", "1")])
|
|
df = DataFrame(index=[1, 3, 5], columns=columns)
|
|
|
|
# Works, but adds a column instead of updating the two existing ones
|
|
df["A"] = 0.0 # Doesn't work
|
|
assert (df["A"].values == 0).all()
|
|
|
|
# it broadcasts
|
|
df["B", "1"] = [1, 2, 3]
|
|
df["A"] = df["B", "1"]
|
|
|
|
sliced_a1 = df["A", "1"]
|
|
sliced_a2 = df["A", "2"]
|
|
sliced_b1 = df["B", "1"]
|
|
tm.assert_series_equal(sliced_a1, sliced_b1, check_names=False)
|
|
tm.assert_series_equal(sliced_a2, sliced_b1, check_names=False)
|
|
assert sliced_a1.name == ("A", "1")
|
|
assert sliced_a2.name == ("A", "2")
|
|
assert sliced_b1.name == ("B", "1")
|
|
|
|
def test_loc_getitem_tuple_plus_columns(
|
|
self, multiindex_year_month_day_dataframe_random_data
|
|
):
|
|
# GH #1013
|
|
ymd = multiindex_year_month_day_dataframe_random_data
|
|
df = ymd[:5]
|
|
|
|
result = df.loc[(2000, 1, 6), ["A", "B", "C"]]
|
|
expected = df.loc[2000, 1, 6][["A", "B", "C"]]
|
|
tm.assert_series_equal(result, expected)
|
|
|
|
@pytest.mark.filterwarnings("ignore:Setting a value on a view:FutureWarning")
|
|
def test_loc_getitem_setitem_slice_integers(self, frame_or_series):
|
|
index = MultiIndex(
|
|
levels=[[0, 1, 2], [0, 2]], codes=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]]
|
|
)
|
|
|
|
obj = DataFrame(
|
|
np.random.default_rng(2).standard_normal((len(index), 4)),
|
|
index=index,
|
|
columns=["a", "b", "c", "d"],
|
|
)
|
|
obj = tm.get_obj(obj, frame_or_series)
|
|
|
|
res = obj.loc[1:2]
|
|
exp = obj.reindex(obj.index[2:])
|
|
tm.assert_equal(res, exp)
|
|
|
|
obj.loc[1:2] = 7
|
|
assert (obj.loc[1:2] == 7).values.all()
|
|
|
|
def test_setitem_change_dtype(self, multiindex_dataframe_random_data):
|
|
frame = multiindex_dataframe_random_data
|
|
dft = frame.T
|
|
s = dft["foo", "two"]
|
|
dft["foo", "two"] = s > s.median()
|
|
tm.assert_series_equal(dft["foo", "two"], s > s.median())
|
|
# assert isinstance(dft._data.blocks[1].items, MultiIndex)
|
|
|
|
reindexed = dft.reindex(columns=[("foo", "two")])
|
|
tm.assert_series_equal(reindexed["foo", "two"], s > s.median())
|
|
|
|
def test_set_column_scalar_with_loc(
|
|
self, multiindex_dataframe_random_data, using_copy_on_write, warn_copy_on_write
|
|
):
|
|
frame = multiindex_dataframe_random_data
|
|
subset = frame.index[[1, 4, 5]]
|
|
|
|
frame.loc[subset] = 99
|
|
assert (frame.loc[subset].values == 99).all()
|
|
|
|
frame_original = frame.copy()
|
|
col = frame["B"]
|
|
with tm.assert_cow_warning(warn_copy_on_write):
|
|
col[subset] = 97
|
|
if using_copy_on_write:
|
|
# chained setitem doesn't work with CoW
|
|
tm.assert_frame_equal(frame, frame_original)
|
|
else:
|
|
assert (frame.loc[subset, "B"] == 97).all()
|
|
|
|
def test_nonunique_assignment_1750(self):
|
|
df = DataFrame(
|
|
[[1, 1, "x", "X"], [1, 1, "y", "Y"], [1, 2, "z", "Z"]], columns=list("ABCD")
|
|
)
|
|
|
|
df = df.set_index(["A", "B"])
|
|
mi = MultiIndex.from_tuples([(1, 1)])
|
|
|
|
df.loc[mi, "C"] = "_"
|
|
|
|
assert (df.xs((1, 1))["C"] == "_").all()
|
|
|
|
def test_astype_assignment_with_dups(self):
|
|
# GH 4686
|
|
# assignment with dups that has a dtype change
|
|
cols = MultiIndex.from_tuples([("A", "1"), ("B", "1"), ("A", "2")])
|
|
df = DataFrame(np.arange(3).reshape((1, 3)), columns=cols, dtype=object)
|
|
index = df.index.copy()
|
|
|
|
df["A"] = df["A"].astype(np.float64)
|
|
tm.assert_index_equal(df.index, index)
|
|
|
|
def test_setitem_nonmonotonic(self):
|
|
# https://github.com/pandas-dev/pandas/issues/31449
|
|
index = MultiIndex.from_tuples(
|
|
[("a", "c"), ("b", "x"), ("a", "d")], names=["l1", "l2"]
|
|
)
|
|
df = DataFrame(data=[0, 1, 2], index=index, columns=["e"])
|
|
df.loc["a", "e"] = np.arange(99, 101, dtype="int64")
|
|
expected = DataFrame({"e": [99, 1, 100]}, index=index)
|
|
tm.assert_frame_equal(df, expected)
|
|
|
|
|
|
class TestSetitemWithExpansionMultiIndex:
|
|
def test_setitem_new_column_mixed_depth(self):
|
|
arrays = [
|
|
["a", "top", "top", "routine1", "routine1", "routine2"],
|
|
["", "OD", "OD", "result1", "result2", "result1"],
|
|
["", "wx", "wy", "", "", ""],
|
|
]
|
|
|
|
tuples = sorted(zip(*arrays))
|
|
index = MultiIndex.from_tuples(tuples)
|
|
df = DataFrame(np.random.default_rng(2).standard_normal((4, 6)), columns=index)
|
|
|
|
result = df.copy()
|
|
expected = df.copy()
|
|
result["b"] = [1, 2, 3, 4]
|
|
expected["b", "", ""] = [1, 2, 3, 4]
|
|
tm.assert_frame_equal(result, expected)
|
|
|
|
def test_setitem_new_column_all_na(self):
|
|
# GH#1534
|
|
mix = MultiIndex.from_tuples([("1a", "2a"), ("1a", "2b"), ("1a", "2c")])
|
|
df = DataFrame([[1, 2], [3, 4], [5, 6]], index=mix)
|
|
s = Series({(1, 1): 1, (1, 2): 2})
|
|
df["new"] = s
|
|
assert df["new"].isna().all()
|
|
|
|
def test_setitem_enlargement_keep_index_names(self):
|
|
# GH#53053
|
|
mi = MultiIndex.from_tuples([(1, 2, 3)], names=["i1", "i2", "i3"])
|
|
df = DataFrame(data=[[10, 20, 30]], index=mi, columns=["A", "B", "C"])
|
|
df.loc[(0, 0, 0)] = df.loc[(1, 2, 3)]
|
|
mi_expected = MultiIndex.from_tuples(
|
|
[(1, 2, 3), (0, 0, 0)], names=["i1", "i2", "i3"]
|
|
)
|
|
expected = DataFrame(
|
|
data=[[10, 20, 30], [10, 20, 30]],
|
|
index=mi_expected,
|
|
columns=["A", "B", "C"],
|
|
)
|
|
tm.assert_frame_equal(df, expected)
|
|
|
|
|
|
@td.skip_array_manager_invalid_test # df["foo"] select multiple columns -> .values
|
|
# is not a view
|
|
def test_frame_setitem_view_direct(
|
|
multiindex_dataframe_random_data, using_copy_on_write
|
|
):
|
|
# this works because we are modifying the underlying array
|
|
# really a no-no
|
|
df = multiindex_dataframe_random_data.T
|
|
if using_copy_on_write:
|
|
with pytest.raises(ValueError, match="read-only"):
|
|
df["foo"].values[:] = 0
|
|
assert (df["foo"].values != 0).all()
|
|
else:
|
|
df["foo"].values[:] = 0
|
|
assert (df["foo"].values == 0).all()
|
|
|
|
|
|
def test_frame_setitem_copy_raises(
|
|
multiindex_dataframe_random_data, using_copy_on_write, warn_copy_on_write
|
|
):
|
|
# will raise/warn as its chained assignment
|
|
df = multiindex_dataframe_random_data.T
|
|
if using_copy_on_write or warn_copy_on_write:
|
|
with tm.raises_chained_assignment_error():
|
|
df["foo"]["one"] = 2
|
|
else:
|
|
msg = "A value is trying to be set on a copy of a slice from a DataFrame"
|
|
with pytest.raises(SettingWithCopyError, match=msg):
|
|
with tm.raises_chained_assignment_error():
|
|
df["foo"]["one"] = 2
|
|
|
|
|
|
def test_frame_setitem_copy_no_write(
|
|
multiindex_dataframe_random_data, using_copy_on_write, warn_copy_on_write
|
|
):
|
|
frame = multiindex_dataframe_random_data.T
|
|
expected = frame
|
|
df = frame.copy()
|
|
if using_copy_on_write or warn_copy_on_write:
|
|
with tm.raises_chained_assignment_error():
|
|
df["foo"]["one"] = 2
|
|
else:
|
|
msg = "A value is trying to be set on a copy of a slice from a DataFrame"
|
|
with pytest.raises(SettingWithCopyError, match=msg):
|
|
with tm.raises_chained_assignment_error():
|
|
df["foo"]["one"] = 2
|
|
|
|
result = df
|
|
tm.assert_frame_equal(result, expected)
|
|
|
|
|
|
def test_frame_setitem_partial_multiindex():
|
|
# GH 54875
|
|
df = DataFrame(
|
|
{
|
|
"a": [1, 2, 3],
|
|
"b": [3, 4, 5],
|
|
"c": 6,
|
|
"d": 7,
|
|
}
|
|
).set_index(["a", "b", "c"])
|
|
ser = Series(8, index=df.index.droplevel("c"))
|
|
result = df.copy()
|
|
result["d"] = ser
|
|
expected = df.copy()
|
|
expected["d"] = 8
|
|
tm.assert_frame_equal(result, expected)
|