Repository: lincc-frameworks/nested-pandas

Issue:
Support multiindex
**Bug report**

Currently, `packer` module doesn't support multiindex frames and series. It should be fixed.

```python
from nested_pandas.series.packer import pack_flat

df = pd.DataFrame({
    "survey": ["ztf"] * 100 + ["lsst"] * 100,
    "id": np.r_[np.repeat(np.arange(10), 10), np.repeat(np.arange(10), 10)],
    "time": np.linspace(0, 2000, 200),
    "flux": np.random.normal(size=200)})
df = df.set_index(["survey", "id"])
# Raises
pack_flat(df)
```

**Before submitting**
Please check the following:

- [x] I have described the situation in which the bug arose, including what code was executed, information about my environment, and any applicable data others will need to reproduce the problem.
- [ ] I have included available evidence of the unexpected behavior (including error messages, screenshots, and/or plots) as well as a descriprion of what I expected instead.
- [ ] If I have a solution in mind, I have provided an explanation and/or pseudocode and/or task list.

Discussion:
github-actions[bot]: | Before [92b422df]    | After [0f68001d]    |   Ratio | Benchmark (Parameter)                               |
|----------------------|---------------------|---------|-----------------------------------------------------|
| 8.22±0.07ms          | 8.80±0.1ms          |    1.07 | benchmarks.NestedFrameAddNested.time_run            |
| 268M                 | 280M                |    1.05 | benchmarks.ReassignHalfOfNestedSeries.peakmem_run   |
| 88.2M                | 90.9M               |    1.03 | benchmarks.NestedFrameQuery.peakmem_run             |
| 6.13±0.04ms          | 6.33±0.09ms         |    1.03 | benchmarks.NestedFrameQuery.time_run                |
| 257M                 | 263M                |    1.02 | benchmarks.AssignSingleDfToNestedSeries.peakmem_run |
| 88.2M                | 89.3M               |    1.01 | benchmarks.NestedFrameReduce.peakmem_run            |
| 32.6±1ms             | 32.5±3ms            |    1    | benchmarks.AssignSingleDfToNestedSeries.time_run    |
| 86M                  | 86.1M               |    1    | benchmarks.NestedFrameAddNested.peakmem_run         |
| 5.09±0.01ms          | 5.10±0.02ms         |    1    | benchmarks.NestedFrameReduce.time_run               |
| 56.7±1ms             | 56.8±2ms            |    1    | benchmarks.ReassignHalfOfNestedSeries.time_run      |


Click [here](https://github.com/lincc-frameworks/nested-pandas/actions/runs/9037650619/job/24837120313#step:11:1) to view all benchmarks.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/lincc-frameworks/nested-pandas/pull/79?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lincc-frameworks) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.65%. Comparing base [(`631787f`)](https://app.codecov.io/gh/lincc-frameworks/nested-pandas/commit/631787f7aaa3b60b47ac29cee7713cf05236922e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lincc-frameworks) to head [(`1a270d7`)](https://app.codecov.io/gh/lincc-frameworks/nested-pandas/pull/79?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lincc-frameworks).
> Report is 6 commits behind head on main.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #79      +/-   ##
==========================================
+ Coverage   98.52%   98.65%   +0.12%     
==========================================
  Files          15       15              
  Lines         816      818       +2     
==========================================
+ Hits          804      807       +3     
+ Misses         12       11       -1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/lincc-frameworks/nested-pandas/pull/79?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lincc-frameworks).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lincc-frameworks).


Patch:
diff --git a/src/nested_pandas/series/accessor.py b/src/nested_pandas/series/accessor.py
index 8a075a7..3bd967b 100644
--- a/src/nested_pandas/series/accessor.py
+++ b/src/nested_pandas/series/accessor.py
@@ -182,10 +182,10 @@ class NestSeriesAccessor(MutableMapping):
 
     def get_flat_index(self) -> pd.Index:
         """Index of the flat arrays"""
-        return pd.Index(
-            np.repeat(self._series.index.values, np.diff(self._series.array.list_offsets)),
-            name=self._series.index.name,
-        )
+        flat_index = np.repeat(self._series.index, np.diff(self._series.array.list_offsets))
+        # pd.Index supports np.repeat, so flat_index is the same type as self._series.index
+        flat_index = cast(pd.Index, flat_index)
+        return flat_index
 
     def get_flat_series(self, field: str) -> pd.Series:
         """Get the flat-array field as a Series


Test patch:
diff --git a/tests/nested_pandas/series/test_accessor.py b/tests/nested_pandas/series/test_accessor.py
index e461ac6..5ee617d 100644
--- a/tests/nested_pandas/series/test_accessor.py
+++ b/tests/nested_pandas/series/test_accessor.py
@@ -6,7 +6,7 @@ from nested_pandas import NestedDtype
 from nested_pandas.series.ext_array import NestedExtensionArray
 from nested_pandas.series.packer import pack_flat
 from numpy.testing import assert_array_equal
-from pandas.testing import assert_frame_equal, assert_series_equal
+from pandas.testing import assert_frame_equal, assert_index_equal, assert_series_equal
 
 
 def test_registered():
@@ -345,6 +345,26 @@ def test_query_flat_empty_rows():
     assert_series_equal(filtered, desired)
 
 
+@pytest.mark.parametrize(
+    "df",
+    [
+        pd.DataFrame({"a": [1] * 10}, index=[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]),
+        pd.DataFrame(
+            {"a": [1] * 10},
+            index=pd.MultiIndex.from_arrays(([1, 1, 1, 1, 1, 1, 2, 2, 2, 2], [0, 1, 1, 2, 2, 2, 1, 1, 0, 0])),
+        ),
+        pd.DataFrame({"a": [1] * 10}, index=[1, 0, 0, 3, 3, 3, 0, 0, 0, 0]),
+        pd.DataFrame(
+            {"a": [1] * 6}, index=pd.MultiIndex.from_arrays(([0, 1, 0, 1, 0, 1], [1, 0, 0, 1, 0, 2]))
+        ),
+    ],
+)
+def test_get_flat_index(df):
+    """Test .nest.get_flat_index() returns the index of the original flat df"""
+    series = pack_flat(df)
+    assert_index_equal(series.nest.get_flat_index(), df.index.sort_values())
+
+
 def test_get_list_series():
     """Test that the .nest.get_list_series() method works."""
     struct_array = pa.StructArray.from_arrays(