Repository: pandas-dev/pandas

Issue:
`sort=False` option to stack/unstack/pivot
It would be really nice if there was a `sort=False` option on stack/unstack and pivot. (Preferably the default)

It is reasonably common to have data in non-standard order that actually provides information (in my case, I have model names, and the order of the names denotes  complexity of the models). Stacking or unstacking currently loses all of this information, with no way to retrieve it. That does not seem like a sensible default to me.

It would be relatively easy to work around a non-sorted stack/unstack method (using .sort_index). To go the other way is less trivial, requiring the user to store a list of the values in the necessary order.

I actually find it hard to think of a situation where a sort on unstack would be more useful...


Discussion:
can you show an example. These are ordered by the index and not sorted. 
Indeed, as @jreback points out we don't sort when stacking or unstacking. Rather, levels are sorted internally in a MultiIndex when a MultiIndex is constructed (e.g., with `from_arrays` or `from_product`).

This is a confusing implementation detail that leaks into the public API. The `levels` of a MultiIndex only look sorted if you look at the low-level MultiIndex repr, not the values in a series/dataframe:
```
In [4]: index = pd.MultiIndex.from_arrays([['b', 'a'], [1, 0]])

In [5]: index
Out[5]:
MultiIndex(levels=[['a', 'b'], [0, 1]],
           labels=[[1, 0], [1, 0]])

In [6]: index.to_series()
Out[6]:
b  1    (b, 1)
a  0    (a, 0)
dtype: object
```

See https://github.com/pandas-dev/pandas/issues/14903 and https://github.com/pandas-dev/pandas/issues/14672 for related discussion.

I see a few alternatives for cleaning this up:

1. Add a `sort_levels=False` argument to `from_arrays` and `from_product`, to allow not sorting levels at MultiIndex construction time. The downside is that this means that some MultiIndex indexing operations will be slow, which defeats part of the purpose of a MultiIndex, but it could still be nice to have the option.
2. Add a `sort=False` or `reorder=False` option (maybe make this the default?) to stack/unstack/pivot, which would ensure that unstacked columns appear in order of appearance in the MultiIndex rather than sorted order (as you suggest here).
3. Alternatively, we could do nothing and encourage using `Categorical` dtype to preserve level order, e.g., use `pd.Categorical(['b', 'a'], categories=['b', 'a'])` rather than just `['b', 'a']`. But this is pretty cumbersome and doesn't make the default behavior any more intuitive.
2 - that would basically imply that the stack/unstack/pivot operation would record the order of index elements of the input, and re-order the output based on that, is that correct? That would be fine, I think.
> 2 - that would basically imply that the stack/unstack/pivot operation would record the order of index elements of the input, and re-order the output based on that, is that correct? That would be fine, I think.

Correct, yes. One downside of this approach is that it is slightly slower to construct the new labels. It requires a pass over the full index using `pd.unique`, rather than just using the unique set of labels.
There's a good example of this here, I think: https://stackoverflow.com/questions/28686053/pandas-pivot-table-reoganize-order-of-multi-index
So this is quite straightforward to provide categorical orderings.
This currently has a small bug, see #15058, but does work (even though indexing generally requires lexsorting, this is compatible). So This is a reasonable soln.

```
In [31]: index = pd.MultiIndex.from_tuples(list(zip(['a', 'a', 'a', 'b', 'b', 'b'],
    ...:                                       [0, 0, 0, 1, 1, 1],
    ...:                                       ['x', 'xx', 'xxx', 'x', 'xx', 'xxx'])),
    ...:                                       names=['A', 'B', 'C'])
    ...: df = pd.DataFrame(np.random.rand(6, 3), index = index)
    ...: 
    ...: 

In [32]: df
Out[32]: 
                0         1         2
A B C                                
a 0 x    0.179126  0.320740  0.048524
    xx   0.448321  0.755726  0.685202
    xxx  0.058184  0.984778  0.432928
b 1 x    0.279140  0.876715  0.249856
    xx   0.185775  0.403420  0.729933
    xxx  0.133858  0.652050  0.960392

In [33]: df = df.reset_index()

In [34]: df = df.assign(C=df.C.astype('category', categories=['xxx', 'xx', 'x']))

In [35]: df
Out[35]: 
   A  B    C         0         1         2
0  a  0    x  0.179126  0.320740  0.048524
1  a  0   xx  0.448321  0.755726  0.685202
2  a  0  xxx  0.058184  0.984778  0.432928
3  b  1    x  0.279140  0.876715  0.249856
4  b  1   xx  0.185775  0.403420  0.729933
5  b  1  xxx  0.133858  0.652050  0.960392

In [36]: df.dtypes
Out[36]: 
A      object
B       int64
C    category
0     float64
1     float64
2     float64
dtype: object

In [37]: df.sort_values(['A', 'B', 'C'])
Out[37]: 
   A  B    C         0         1         2
2  a  0  xxx  0.058184  0.984778  0.432928
1  a  0   xx  0.448321  0.755726  0.685202
0  a  0    x  0.179126  0.320740  0.048524
5  b  1  xxx  0.133858  0.652050  0.960392
4  b  1   xx  0.185775  0.403420  0.729933
3  b  1    x  0.279140  0.876715  0.249856
```
The point is that `stack`/`unstack` really sorts the the index. Let's continue with the session by @jreback .
```ipython
In [7]: df = df.sort_values(['A', 'B', 'C'], ascending=False) 

In [8]: df    
Out[8]: 
   A  B    C         0         1         2
3  b  1    x  0.674635  0.607875  0.356187
4  b  1   xx  0.062986  0.675858  0.895372
5  b  1  xxx  0.974950  0.448647  0.178389
0  a  0    x  0.683399  0.652522  0.549847
1  a  0   xx  0.168813  0.891502  0.385490
2  a  0  xxx  0.881862  0.143344  0.867280

In [9]: df = df.set_index(['A', 'B', 'C'])  

In [10]: df  
Out[10]: 
                0         1         2
A B C                                
b 1 x    0.674635  0.607875  0.356187
    xx   0.062986  0.675858  0.895372
    xxx  0.974950  0.448647  0.178389
a 0 x    0.683399  0.652522  0.549847
    xx   0.168813  0.891502  0.385490
    xxx  0.881862  0.143344  0.867280

In [11]: df.index     
Out[11]: 
MultiIndex(levels=[['a', 'b'], [0, 1], ['xxx', 'xx', 'x']],
           codes=[[1, 1, 1, 0, 0, 0], [1, 1, 1, 0, 0, 0], [2, 1, 0, 2, 1, 0]],
           names=['A', 'B', 'C'])
```
Now, this is the bug mentioned above. Never mind this, let's continue.
```
In [12]: df = df.unstack()  

In [13]: df    
Out[13]: 
            0                             1                             2                    
C         xxx        xx         x       xxx        xx         x       xxx        xx         x
A B                                                                                          
a 0  0.881862  0.168813  0.683399  0.143344  0.891502  0.652522  0.867280  0.385490  0.549847
b 1  0.974950  0.062986  0.674635  0.448647  0.675858  0.607875  0.178389  0.895372  0.356187

In [14]: df.index  
Out[14]: 
MultiIndex(levels=[['a', 'b'], [0, 1]],
           codes=[[0, 1], [0, 1]],
           names=['A', 'B'])
```
Now the data frame is sorted by the remaining levels in the index!

This is undocumented behavior. The documentation says only: "The level involved will automatically get sorted." 

Also requested [in](https://stackoverflow.com/questions/33604760/how-to-maintain-pandas-dataframe-index-order-when-using-stack-unstack) [multiple](https://stackoverflow.com/questions/45434630/pandas-unstack-should-not-sort-remaining-indexes) [SO](https://stackoverflow.com/questions/48636363/how-can-i-unstack-without-sorting-in-pandas) [questions](https://stackoverflow.com/questions/28686053/pandas-pivot-table-reoganize-order-of-multi-index).
This is quite aggravating with column multiindex. It doesn't seem like there is a work around.

Firstly,there doesn't seem to be an easy way to convert an existing multindex into a categorical one. The best I could come up with is to select a df row, reset_index on the resultant series, and manually create a categorical multi-index index out of all the relevant columns, and then re-assign that to the original. This works, but it would be nice if there was an easy way to do something like `df.columns['blah'] = pd.CategoricalIndex(df.columns['blah'], ordered=True)` or something.

Secondly, even with a categorical column multi index, the categories are completely ignored. For example, I would like to stack the 'year' level of the columns, and retain the order of the other levels, but the Categoricalness gets lost in transit:

```python
In [1]: final_df.columns                                                                                  
Out[1]: 
MultiIndex([('value',       'sum_risk_cost', 2020, 'national'),
            ('value',       'sum_risk_cost', 2100, 'national'),
            ('value',   'avg_risk_fraction', 2020, 'national'),
            ('value',   'avg_risk_fraction', 2100, 'national'),
            ('value',   'count_uninsurable', 2020, 'national'),
            ('value',   'count_uninsurable', 2100, 'national'),
            ('value', 'percent_uninsurable', 2020, 'national'),
            ('value', 'percent_uninsurable', 2100, 'national'),
            ( 'rank',       'sum_risk_cost', 2020, 'national'),
            ( 'rank',       'sum_risk_cost', 2020,  'nat_10k'),
            ( 'rank',       'sum_risk_cost', 2020,    'state'),
            ( 'rank',       'sum_risk_cost', 2100, 'national'),
            ( 'rank',       'sum_risk_cost', 2100,  'nat_10k'),
            ( 'rank',       'sum_risk_cost', 2100,    'state'),
            ( 'rank',   'avg_risk_fraction', 2020, 'national'),
            ( 'rank',   'avg_risk_fraction', 2020,  'nat_10k'),
            ( 'rank',   'avg_risk_fraction', 2020,    'state'),
            ( 'rank',   'avg_risk_fraction', 2100, 'national'),
            ( 'rank',   'avg_risk_fraction', 2100,  'nat_10k'),
            ( 'rank',   'avg_risk_fraction', 2100,    'state'),
            ( 'rank',   'count_uninsurable', 2020, 'national'),
            ( 'rank',   'count_uninsurable', 2020,  'nat_10k'),
            ( 'rank',   'count_uninsurable', 2020,    'state'),
            ( 'rank',   'count_uninsurable', 2100, 'national'),
            ( 'rank',   'count_uninsurable', 2100,  'nat_10k'),
            ( 'rank',   'count_uninsurable', 2100,    'state'),
            ( 'rank', 'percent_uninsurable', 2020, 'national'),
            ( 'rank', 'percent_uninsurable', 2020,  'nat_10k'),
            ( 'rank', 'percent_uninsurable', 2020,    'state'),
            ( 'rank', 'percent_uninsurable', 2100, 'national'),
            ( 'rank', 'percent_uninsurable', 2100,  'nat_10k'),
            ( 'rank', 'percent_uninsurable', 2100,    'state')],
           names=['type', 'stat', 'year', 'subset'])

In [2]: final_df.columns.get_level_values('type')                                                         
Out[2]: 
CategoricalIndex(['value', 'value', 'value', 'value', 'value', 'value',
                  'value', 'value', 'rank', 'rank', 'rank', 'rank', 'rank',
                  'rank', 'rank', 'rank', 'rank', 'rank', 'rank', 'rank',
                  'rank', 'rank', 'rank', 'rank', 'rank', 'rank', 'rank',
                  'rank', 'rank', 'rank', 'rank', 'rank'],
                 categories=['rank', 'value'], ordered=True, name='type', dtype='category')

In [3]: final_df = final_df.stack('year').sort_index()                                                    

In [4]: final_df.columns.get_level_values('type')                                                         
Out[4]: 
Index(['rank', 'rank', 'rank', 'rank', 'rank', 'rank', 'rank', 'rank', 'rank',
       'rank', 'rank', 'rank', 'value', 'value', 'value', 'value'],
      dtype='object', name='type')
```

It also doesn't seem easy to manually store the column orders and re-use them afterwards, die to the missing 'year' level.

Anyone have a suggested work-around for this?
Is anyone going to fix this?
@cemanughian pandas is all volunteer and there are quite a number of open issues

you are welcome to do a pill request - core devs can provide review
 @jreback I am wondering if we need to add a new argument for this? I think we can advice using `droplevel` and `reindex`?
 If we do want to add this, I can have a look.
```python3
import pandas as pd

>>> tuples = list(zip(['zzz', 'xxx', 'ddd', 'zzz', 'aaa', 'zzz', 'aaa'], ['z', 'z', 'z', 'a', 'z', 'x', 'a']))
>>> index = pd.MultiIndex.from_tuples(tuples, names=['first', 'second'])
>>> df = pd.DataFrame({'A': [2, 5, 3, 1, 1, 6, 7], 'B': [4, 3, 5, 10, 5, 3, 8]}, index=index)
>>> df
              A   B
first second
zzz   z       2   4
xxx   z       5   3
ddd   z       3   5
zzz   a       1  10
aaa   z       1   5
zzz   x       6   3
aaa   a       7   8
>>> unstacked = df.unstack()
>>> unstacked
          A               B
second    a    x    z     a    x    z
first
aaa     7.0  NaN  1.0   8.0  NaN  5.0
ddd     NaN  NaN  3.0   NaN  NaN  5.0
xxx     NaN  NaN  5.0   NaN  NaN  3.0
zzz     1.0  6.0  2.0  10.0  3.0  4.0
>>> new_index = index.droplevel(-1).unique()
>>> unstacked.reindex(new_index)
          A               B
second    a    x    z     a    x    z
first
zzz     1.0  6.0  2.0  10.0  3.0  4.0
xxx     NaN  NaN  5.0   NaN  NaN  3.0
ddd     NaN  NaN  3.0   NaN  NaN  5.0
aaa     7.0  NaN  1.0   8.0  NaN  5.0
```

Patch:
diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index 7cc2e19f477dd..bb51e64fe682d 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -98,6 +98,7 @@ Other enhancements
 - Performance improvement in :func:`read_csv` (:issue:`52632`) with ``engine="c"``
 - :meth:`Categorical.from_codes` has gotten a ``validate`` parameter (:issue:`50975`)
 - :meth:`DataFrame.stack` gained the ``sort`` keyword to dictate whether the resulting :class:`MultiIndex` levels are sorted (:issue:`15105`)
+- :meth:`DataFrame.unstack` gained the ``sort`` keyword to dictate whether the resulting :class:`MultiIndex` levels are sorted (:issue:`15105`)
 - Added ``engine_kwargs`` parameter to :meth:`DataFrame.to_excel` (:issue:`53220`)
 - Performance improvement in :func:`concat` with homogeneous ``np.float64`` or ``np.float32`` dtypes (:issue:`52685`)
 - Performance improvement in :meth:`DataFrame.filter` when ``items`` is given (:issue:`52941`)
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index d3d0e32eefdfc..d4c2124182ea5 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -9296,7 +9296,7 @@ def explode(
 
         return result.__finalize__(self, method="explode")
 
-    def unstack(self, level: Level = -1, fill_value=None):
+    def unstack(self, level: Level = -1, fill_value=None, sort: bool = True):
         """
         Pivot a level of the (necessarily hierarchical) index labels.
 
@@ -9312,6 +9312,8 @@ def unstack(self, level: Level = -1, fill_value=None):
             Level(s) of index to unstack, can pass level name.
         fill_value : int, str or dict
             Replace NaN with this value if the unstack produces missing values.
+        sort : bool, default True
+            Sort the level(s) in the resulting MultiIndex columns.
 
         Returns
         -------
@@ -9359,7 +9361,7 @@ def unstack(self, level: Level = -1, fill_value=None):
         """
         from pandas.core.reshape.reshape import unstack
 
-        result = unstack(self, level, fill_value)
+        result = unstack(self, level, fill_value, sort)
 
         return result.__finalize__(self, method="unstack")
 
diff --git a/pandas/core/reshape/reshape.py b/pandas/core/reshape/reshape.py
index b61cdf4ad6992..3866d30e9c757 100644
--- a/pandas/core/reshape/reshape.py
+++ b/pandas/core/reshape/reshape.py
@@ -102,8 +102,11 @@ class _Unstacker:
     unstacked : DataFrame
     """
 
-    def __init__(self, index: MultiIndex, level: Level, constructor) -> None:
+    def __init__(
+        self, index: MultiIndex, level: Level, constructor, sort: bool = True
+    ) -> None:
         self.constructor = constructor
+        self.sort = sort
 
         self.index = index.remove_unused_levels()
 
@@ -119,11 +122,15 @@ def __init__(self, index: MultiIndex, level: Level, constructor) -> None:
         self.removed_name = self.new_index_names.pop(self.level)
         self.removed_level = self.new_index_levels.pop(self.level)
         self.removed_level_full = index.levels[self.level]
+        if not self.sort:
+            unique_codes = unique(self.index.codes[self.level])
+            self.removed_level = self.removed_level.take(unique_codes)
+            self.removed_level_full = self.removed_level_full.take(unique_codes)
 
         # Bug fix GH 20601
         # If the data frame is too big, the number of unique index combination
         # will cause int32 overflow on windows environments.
-        # We want to check and raise an error before this happens
+        # We want to check and raise an warning before this happens
         num_rows = np.max([index_level.size for index_level in self.new_index_levels])
         num_columns = self.removed_level.size
 
@@ -164,13 +171,17 @@ def _indexer_and_to_sort(
     @cache_readonly
     def sorted_labels(self) -> list[np.ndarray]:
         indexer, to_sort = self._indexer_and_to_sort
-        return [line.take(indexer) for line in to_sort]
+        if self.sort:
+            return [line.take(indexer) for line in to_sort]
+        return to_sort
 
     def _make_sorted_values(self, values: np.ndarray) -> np.ndarray:
-        indexer, _ = self._indexer_and_to_sort
+        if self.sort:
+            indexer, _ = self._indexer_and_to_sort
 
-        sorted_values = algos.take_nd(values, indexer, axis=0)
-        return sorted_values
+            sorted_values = algos.take_nd(values, indexer, axis=0)
+            return sorted_values
+        return values
 
     def _make_selectors(self):
         new_levels = self.new_index_levels
@@ -195,7 +206,10 @@ def _make_selectors(self):
 
         self.group_index = comp_index
         self.mask = mask
-        self.compressor = comp_index.searchsorted(np.arange(ngroups))
+        if self.sort:
+            self.compressor = comp_index.searchsorted(np.arange(ngroups))
+        else:
+            self.compressor = np.sort(np.unique(comp_index, return_index=True)[1])
 
     @cache_readonly
     def mask_all(self) -> bool:
@@ -376,7 +390,9 @@ def new_index(self) -> MultiIndex:
         )
 
 
-def _unstack_multiple(data: Series | DataFrame, clocs, fill_value=None):
+def _unstack_multiple(
+    data: Series | DataFrame, clocs, fill_value=None, sort: bool = True
+):
     if len(clocs) == 0:
         return data
 
@@ -421,7 +437,7 @@ def _unstack_multiple(data: Series | DataFrame, clocs, fill_value=None):
         dummy = data.copy()
         dummy.index = dummy_index
 
-        unstacked = dummy.unstack("__placeholder__", fill_value=fill_value)
+        unstacked = dummy.unstack("__placeholder__", fill_value=fill_value, sort=sort)
         new_levels = clevels
         new_names = cnames
         new_codes = recons_codes
@@ -430,7 +446,7 @@ def _unstack_multiple(data: Series | DataFrame, clocs, fill_value=None):
             result = data
             while clocs:
                 val = clocs.pop(0)
-                result = result.unstack(val, fill_value=fill_value)
+                result = result.unstack(val, fill_value=fill_value, sort=sort)
                 clocs = [v if v < val else v - 1 for v in clocs]
 
             return result
@@ -439,7 +455,9 @@ def _unstack_multiple(data: Series | DataFrame, clocs, fill_value=None):
         dummy_df = data.copy(deep=False)
         dummy_df.index = dummy_index
 
-        unstacked = dummy_df.unstack("__placeholder__", fill_value=fill_value)
+        unstacked = dummy_df.unstack(
+            "__placeholder__", fill_value=fill_value, sort=sort
+        )
         if isinstance(unstacked, Series):
             unstcols = unstacked.index
         else:
@@ -464,12 +482,12 @@ def _unstack_multiple(data: Series | DataFrame, clocs, fill_value=None):
     return unstacked
 
 
-def unstack(obj: Series | DataFrame, level, fill_value=None):
+def unstack(obj: Series | DataFrame, level, fill_value=None, sort: bool = True):
     if isinstance(level, (tuple, list)):
         if len(level) != 1:
             # _unstack_multiple only handles MultiIndexes,
             # and isn't needed for a single level
-            return _unstack_multiple(obj, level, fill_value=fill_value)
+            return _unstack_multiple(obj, level, fill_value=fill_value, sort=sort)
         else:
             level = level[0]
 
@@ -479,9 +497,9 @@ def unstack(obj: Series | DataFrame, level, fill_value=None):
 
     if isinstance(obj, DataFrame):
         if isinstance(obj.index, MultiIndex):
-            return _unstack_frame(obj, level, fill_value=fill_value)
+            return _unstack_frame(obj, level, fill_value=fill_value, sort=sort)
         else:
-            return obj.T.stack(dropna=False)
+            return obj.T.stack(dropna=False, sort=sort)
     elif not isinstance(obj.index, MultiIndex):
         # GH 36113
         # Give nicer error messages when unstack a Series whose
@@ -491,18 +509,22 @@ def unstack(obj: Series | DataFrame, level, fill_value=None):
         )
     else:
         if is_1d_only_ea_dtype(obj.dtype):
-            return _unstack_extension_series(obj, level, fill_value)
+            return _unstack_extension_series(obj, level, fill_value, sort=sort)
         unstacker = _Unstacker(
-            obj.index, level=level, constructor=obj._constructor_expanddim
+            obj.index, level=level, constructor=obj._constructor_expanddim, sort=sort
         )
         return unstacker.get_result(
             obj._values, value_columns=None, fill_value=fill_value
         )
 
 
-def _unstack_frame(obj: DataFrame, level, fill_value=None) -> DataFrame:
+def _unstack_frame(
+    obj: DataFrame, level, fill_value=None, sort: bool = True
+) -> DataFrame:
     assert isinstance(obj.index, MultiIndex)  # checked by caller
-    unstacker = _Unstacker(obj.index, level=level, constructor=obj._constructor)
+    unstacker = _Unstacker(
+        obj.index, level=level, constructor=obj._constructor, sort=sort
+    )
 
     if not obj._can_fast_transpose:
         mgr = obj._mgr.unstack(unstacker, fill_value=fill_value)
@@ -513,7 +535,9 @@ def _unstack_frame(obj: DataFrame, level, fill_value=None) -> DataFrame:
         )
 
 
-def _unstack_extension_series(series: Series, level, fill_value) -> DataFrame:
+def _unstack_extension_series(
+    series: Series, level, fill_value, sort: bool
+) -> DataFrame:
     """
     Unstack an ExtensionArray-backed Series.
 
@@ -529,6 +553,8 @@ def _unstack_extension_series(series: Series, level, fill_value) -> DataFrame:
         The user-level (not physical storage) fill value to use for
         missing values introduced by the reshape. Passed to
         ``series.values.take``.
+    sort : bool
+        Whether to sort the resulting MuliIndex levels
 
     Returns
     -------
@@ -538,7 +564,7 @@ def _unstack_extension_series(series: Series, level, fill_value) -> DataFrame:
     """
     # Defer to the logic in ExtensionBlock._unstack
     df = series.to_frame()
-    result = df.unstack(level=level, fill_value=fill_value)
+    result = df.unstack(level=level, fill_value=fill_value, sort=sort)
 
     # equiv: result.droplevel(level=0, axis=1)
     #  but this avoids an extra copy
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 12924a167fc46..cb3edbfd6cfc3 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -4274,7 +4274,9 @@ def explode(self, ignore_index: bool = False) -> Series:
 
         return self._constructor(values, index=index, name=self.name, copy=False)
 
-    def unstack(self, level: IndexLabel = -1, fill_value: Hashable = None) -> DataFrame:
+    def unstack(
+        self, level: IndexLabel = -1, fill_value: Hashable = None, sort: bool = True
+    ) -> DataFrame:
         """
         Unstack, also known as pivot, Series with MultiIndex to produce DataFrame.
 
@@ -4284,6 +4286,8 @@ def unstack(self, level: IndexLabel = -1, fill_value: Hashable = None) -> DataFr
             Level(s) to unstack, can pass level name.
         fill_value : scalar value, default None
             Value to use when replacing NaN values.
+        sort : bool, default True
+            Sort the level(s) in the resulting MultiIndex columns.
 
         Returns
         -------
@@ -4318,7 +4322,7 @@ def unstack(self, level: IndexLabel = -1, fill_value: Hashable = None) -> DataFr
         """
         from pandas.core.reshape.reshape import unstack
 
-        return unstack(self, level, fill_value)
+        return unstack(self, level, fill_value, sort)
 
     # ----------------------------------------------------------------------
     # function application


Test patch:
diff --git a/pandas/tests/frame/test_stack_unstack.py b/pandas/tests/frame/test_stack_unstack.py
index aa4abd627066b..2818df721db34 100644
--- a/pandas/tests/frame/test_stack_unstack.py
+++ b/pandas/tests/frame/test_stack_unstack.py
@@ -1210,6 +1210,44 @@ def test_unstack_swaplevel_sortlevel(self, level):
         tm.assert_frame_equal(result, expected)
 
 
+@pytest.mark.parametrize("dtype", ["float64", "Float64"])
+def test_unstack_sort_false(frame_or_series, dtype):
+    # GH 15105
+    index = MultiIndex.from_tuples(
+        [("two", "z", "b"), ("two", "y", "a"), ("one", "z", "b"), ("one", "y", "a")]
+    )
+    obj = frame_or_series(np.arange(1.0, 5.0), index=index, dtype=dtype)
+    result = obj.unstack(level=-1, sort=False)
+
+    if frame_or_series is DataFrame:
+        expected_columns = MultiIndex.from_tuples([(0, "b"), (0, "a")])
+    else:
+        expected_columns = ["b", "a"]
+    expected = DataFrame(
+        [[1.0, np.nan], [np.nan, 2.0], [3.0, np.nan], [np.nan, 4.0]],
+        columns=expected_columns,
+        index=MultiIndex.from_tuples(
+            [("two", "z"), ("two", "y"), ("one", "z"), ("one", "y")]
+        ),
+        dtype=dtype,
+    )
+    tm.assert_frame_equal(result, expected)
+
+    result = obj.unstack(level=[1, 2], sort=False)
+
+    if frame_or_series is DataFrame:
+        expected_columns = MultiIndex.from_tuples([(0, "z", "b"), (0, "y", "a")])
+    else:
+        expected_columns = MultiIndex.from_tuples([("z", "b"), ("y", "a")])
+    expected = DataFrame(
+        [[1.0, 2.0], [3.0, 4.0]],
+        index=["two", "one"],
+        columns=expected_columns,
+        dtype=dtype,
+    )
+    tm.assert_frame_equal(result, expected)
+
+
 def test_unstack_fill_frame_object():
     # GH12815 Test unstacking with object.
     data = Series(["a", "b", "c", "a"], dtype="object")