Панды: как использовать нарезку для мультииндексов смешанного типа в python3? - PullRequest
0 голосов
/ 30 апреля 2018

Как я уже отмечал в этом частично связанном вопросе , невозможно сортировать последовательности смешанного типа:

# Python3.6
sorted(['foo', 'bar', 10, 200, 3])
# => TypeError: '<' not supported between instances of 'str' and 'int'

Это влияет на запросы нарезки в пандах. Следующий пример иллюстрирует мою проблему.

import pandas as pd
import numpy as np
index = [(10,3),(10,1),(2,2),('foo',4),('bar',5)]
index = pd.MultiIndex.from_tuples(index)
data = np.random.randn(len(index),2)
table = pd.DataFrame(data=data, index=index)

idx=pd.IndexSlice
table.loc[idx[:10,:],:]
# The last line will raise an UnsortedIndexError because 
# 'foo' and 'bar' appear in the wrong order.

Сообщение об исключении читается следующим образом:

UnsortedIndexError: 'MultiIndex slicing requires the index to be lexsorted: slicing on levels [0], lexsort depth 0'

В python2.x я восстановился после этого исключения путем лексической сортировки индекса:

# Python2.x:
table = table.sort_index()

#               0         1
# 2   2  0.020841  0.717178
# 10  1  1.608883  0.807834
#     3  0.566967  1.978718
# bar 5 -0.683814 -0.382024
# foo 4  0.150284 -0.750709

table.loc[idx[:10,:],:]
#              0         1
# 2  2  0.020841  0.717178
# 10 1  1.608883  0.807834
#    3  0.566967  1.978718

Однако в python3 я получаю исключение, о котором упоминал в начале:

TypeError: '<' not supported between instances of 'str' and 'int'

Как оправиться от этого? Преобразование индекса в строки перед сортировкой не вариант, потому что это нарушает правильное упорядочение индекса:

# Python2/3
index = [(10,3),(10,1),(2,2),('foo',4),('bar',5)]
index = list(map(lambda x: tuple(map(str,x)), index))
index = pd.MultiIndex.from_tuples(index)
data = np.random.randn(len(index),2)
table = pd.DataFrame(data=data, index=index)
table = table.sort_index()
#               0         1
# 10  1  0.020841  0.717178
#     3  1.608883  0.807834
# 2   2  0.566967  1.978718
# bar 5 -0.683814 -0.382024
# foo 4  0.150284 -0.750709

При таком порядке цензура на основе значений будет нарушена.

table.loc[idx[:10,:],:]     # Raises a TypeError
table.loc[idx[:'10',:],:]   # Misses to return the indices [2,:]

Как мне восстановиться после этого?

Ответы [ 2 ]

0 голосов
/ 01 мая 2018

Это второе решение, которое я придумал. Это лучше, чем мое предыдущее предложение, поскольку оно не изменяет значения индекса отсортированной по лекс-таблице. Здесь я временно преобразую нестроковые индексы перед сортировкой таблицы, но я разыскиваю эти индексы после сортировки.

Решение работает, потому что панды, естественно, могут работать с индексами смешанного типа. Похоже, что нужно отсортировать по лексам только подмножество индексов на основе строк. (Pandas внутренне использует так называемый объект Categorical, который, по-видимому, самостоятельно различает строки и другие типы.)

import numpy as np
import pandas as pd

def stringifiedSortIndex(table):
    # 1) Stringify the index.
    _stringifyIdx = _StringifyIdx()
    table.index = table.index.map(_stringifyIdx)
    # 2) Sort the index.
    table = table.sort_index()
    # 3) Destringify the sorted table.
    _stringifyIdx.revert = True
    table.index = table.index.map(_stringifyIdx)
    # Return table and IndexSlice together.
    return table

class _StringifyIdx(object):
    def __init__(self):
        self._destringifyMap = dict()
        self.revert = False
    def __call__(self, idx):
        if not self.revert:
            return self._stringifyIdx(idx)
        else:
            return self._destringifyIdx(idx)

    # Stringify whatever needs to be converted.
    # In this example: only ints are stringified.
    @staticmethod
    def _stringify(x):
        if isinstance(x,int):
            x = '%03d' % x
            destringify = int
        else:
            destringify = lambda x: x
        return x, destringify

    def _stringifyIdx(self, idx):
        if isinstance(idx, tuple):
            idx = list(idx)
            destr = [None]*len(idx)
            for i,x in enumerate(idx):
                idx[i], destr[i] = self._stringify(x)
            idx = tuple(idx)
            destr = tuple(destr)
        else:
            idx, destr = self._stringify(idx)
        if self._destringifyMap is not None:
            self._destringifyMap[idx] = destr
        return idx

    def _destringifyIdx(self, idx):
        if idx not in self._destringifyMap:
            raise ValueError(("Index to destringify has not been stringified "
                              "this class instance. Index must not change "
                              "between stringification and destringification."))
        destr = self._destringifyMap[idx]
        if isinstance(idx, tuple):
            assert(len(destr)==len(idx))
            idx = tuple(d(i) for d,i in zip(destr, idx))
        else:
            idx = destr(idx)
        return idx


# Build the table.
index = [(10,3),(10,1),(2,2),('foo',4),('bar',5)]
index = pd.MultiIndex.from_tuples(index)
data = np.random.randn(len(index),2)
table = pd.DataFrame(data=data, index=index)
idx = pd.IndexSlice

table = stringifiedSortIndex(table)
print(table)

# Now, the table rows can be accessed as usual.
table.loc[idx[10],:]
table.loc[idx[:10],:]
table.loc[idx[:'bar',:],:]
table.loc[idx[:,:2],:]

# This works also for simply indexed table.
table = pd.DataFrame(data=data, index=[4,1,'foo',3,'bar'])
table = stringifiedSortIndex(table)
table[:'bar']
0 голосов
/ 01 мая 2018

Это лучшее, что я смог придумать. Решение в три этапа:

  • Stringify multi-index таким образом, чтобы сортировка lex сохраняла старую сортировку смешанного типа из python2. Например, к int с может быть добавлено достаточно 0.
  • Сортировка таблицы.
  • Используйте ту же строку при доступе к таблице с ломтиками.

В коде это выглядит следующим образом (полный пример):

import numpy as np
import pandas as pd 

# Stringify whatever needs to be converted.
# In this example: only ints are stringified.
def toString(x):
    if isinstance(x,int):
        x = '%03d' % x
    return x
# Stringify an index tuple.
def idxToString(idx):
    if isinstance(idx, tuple):
        idx = list(idx)
        for i,x in enumerate(idx):
            idx[i] = toString(x)
        return tuple(idx)
    else:
        return toString(idx)
# Replacement for pd.IndexSlice
class IndexSlice(object):
    @staticmethod
    def _toString(arg):
        if isinstance(arg, slice):
            arg = slice(toString(arg.start),
                        toString(arg.stop),
                        toString(arg.step))
        else:
            arg = toString(arg)
        return arg

    def __getitem__(self, arg):
        if isinstance(arg, tuple):
            return tuple(map(self._toString, arg))
        else:
            return self._toString(arg)

# Build the table.
index = [(10,3),(10,1),(2,2),('foo',4),('bar',5)]
index = pd.MultiIndex.from_tuples(index)
data = np.random.randn(len(index),2)
table = pd.DataFrame(data=data, index=index)
# 1) Stringify the index.
table.index = table.index.map(idxToString)
# 2) Sort the index.
table = table.sort_index()
# 3) Create an IndexSlice that applies the same
#    stringification rules. (Replaces pd.IndexSlice)
idx = IndexSlice()
# Now, the table rows can be accessed as usual.
table.loc[idx[10],:]
table.loc[idx[:10],:]
table.loc[idx[:'bar',:],:]
table.loc[idx[:,:2],:]

Это не очень красиво, но исправляет доступ к данным таблицы на основе срезов, который был прерван после обновления до python3. Я рад прочитать лучшие предложения, если у вас есть какие-либо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...