Как разбить список внутри ячейки Dataframe на отдельные строки

95

Я хочу превратить ячейку pandas, содержащую список, в строки для каждого из этих значений.

Итак, возьмите это:

введите описание изображения здесь

Если я хочу распаковать и сложить значения в nearest_neighborsстолбце, чтобы каждое значение было строкой в ​​каждом opponentиндексе, как мне лучше всего это сделать? Существуют ли методы pandas, предназначенные для подобных операций?

SpicyClubСоус
источник
Не могли бы вы привести пример желаемого результата и того, что вы пробовали до сих пор? Другим проще всего помочь вам, если вы предоставите образцы данных, которые также можно вырезать и вставить.
dagrha 08
Вы можете pd.DataFrame(df.nearest_neighbors.values.tolist())распаковать эту колонку, а затем pd.mergeсклеить ее с другими.
hellpanderr
@helpanderr я не думаю values.tolist(), что здесь что-нибудь делает; столбец уже является списком
maxymoo 09
2
@maxymoo i.imgur.com/YGQAYOY.png
hellpanderr
1
Связано, но содержит более подробную информацию stackoverflow.com/questions/53218931/…
BEN_YO 01

Ответы:

56

В приведенном ниже коде я сначала сбрасываю индекс, чтобы упростить итерацию строки.

Я создаю список списков, в котором каждый элемент внешнего списка является строкой целевого DataFrame, а каждый элемент внутреннего списка является одним из столбцов. Этот вложенный список в конечном итоге будет объединен для создания желаемого DataFrame.

Я использую lambdaфункцию вместе с итерацией списка, чтобы создать строку для каждого элемента в nearest_neighborsпаре с соответствующими nameи opponent.

Наконец, я создаю новый DataFrame из этого списка (используя исходные имена столбцов и устанавливая индекс обратно на nameи opponent).

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

>>> df
                                                    nearest_neighbors
name       opponent                                                  
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]

df.reset_index(inplace=True)
rows = []
_ = df.apply(lambda row: [rows.append([row['name'], row['opponent'], nn]) 
                         for nn in row.nearest_neighbors], axis=1)
df_new = pd.DataFrame(rows, columns=df.columns).set_index(['name', 'opponent'])

>>> df_new
                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

ИЗМЕНИТЬ ИЮНЬ 2017

Альтернативный метод выглядит следующим образом:

>>> (pd.melt(df.nearest_neighbors.apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name='nearest_neighbors')
     .set_index(['name', 'opponent'])
     .drop('variable', axis=1)
     .dropna()
     .sort_index()
     )
Александр
источник
apply(pd.Series)подходит для самых маленьких рамок, но для рамок любого разумного размера вам следует пересмотреть более производительное решение. См. Когда я должен использовать pandas apply () в своем коде? (Лучшее решение - сначала прослушать столбец.)
cs95
2
Разнесение столбца в виде списка было значительно упрощено в pandas 0.25 с добавлением explode()метода. Я добавил ответ с примером, используя ту же настройку df, что и здесь.
joelostblom
@joelostblom Приятно слышать. Спасибо за добавление примера с текущим использованием.
Александр
37
df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))

df.explode('nearest_neighbors')

Вне:

                    nearest_neighbors
name       opponent                  
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia
Joelostblom
источник
2
Обратите внимание, что это работает только для одного столбца (начиная с 0,25). См. Здесь и здесь для получения более общих решений.
cs95
это самое простое и быстрое решение (действительно, если у вас есть только один столбец со списком, который нужно взорвать или «раскрутить», как это будет называться в mongodb)
annakeuchenius
Самое быстрое решение от pandas document. Но будьте осторожны: .explode не на месте! Скорее сделаюdf = df.explode(...)
губная гармоника141
34

Используйте apply(pd.Series)и stack, затем reset_indexиto_frame

In [1803]: (df.nearest_neighbors.apply(pd.Series)
              .stack()
              .reset_index(level=2, drop=True)
              .to_frame('nearest_neighbors'))
Out[1803]:
                    nearest_neighbors
name       opponent
A.J. Price 76ers          Zach LaVine
           76ers           Jeremy Lin
           76ers        Nate Robinson
           76ers                Isaia
           blazers        Zach LaVine
           blazers         Jeremy Lin
           blazers      Nate Robinson
           blazers              Isaia
           bobcats        Zach LaVine
           bobcats         Jeremy Lin
           bobcats      Nate Robinson
           bobcats              Isaia

Детали

In [1804]: df
Out[1804]:
                                                   nearest_neighbors
name       opponent
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
Нуль
источник
1
Любите элегантность вашего решения! Вы случайно не сравнивали его с другими подходами?
rpyzh
1
Результат df.nearest_neighbors.apply(pd.Series)меня очень удивил;
Calum You
1
@rpyzh Да, довольно изящно, но жалко медлительно.
cs95
16

Я думаю, что это действительно хороший вопрос, в Hive вы могли бы использовать EXPLODE, я думаю, что есть основания полагать, что Pandas должен включать эту функцию по умолчанию. Я бы, вероятно, взорвал столбец списка с помощью понимания вложенного генератора следующим образом:

pd.DataFrame({
    "name": i[0],
    "opponent": i[1],
    "nearest_neighbor": neighbour
    }
    for i, row in df.iterrows() for neighbour in row.nearest_neighbors
    ).set_index(["name", "opponent"])
Maxymoo
источник
Мне нравится, как это решение позволяет различать количество элементов списка для каждой строки.
user1718097 07
Есть ли способ сохранить исходный индекс с помощью этого метода?
SummerEla
2
@SummerEla lol, это был действительно старый ответ, я обновил, чтобы показать, как я буду это делать сейчас
maxymoo
1
@maxymoo Но это все еще отличный вопрос. Спасибо за обновление!
SummerEla
Я нашел это полезным и превратил в пакет
Орен
11

Самый быстрый метод, который я нашел до сих пор, - это расширение DataFrame с .ilocпомощью сглаженного целевого столбца и его возвращение .

Учитывая обычный ввод (немного воспроизведенный):

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                    'opponent': ['76ers', 'blazers', 'bobcats'], 
                    'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
      .set_index(['name', 'opponent']))
df = pd.concat([df]*10)

df
Out[3]: 
                                                   nearest_neighbors
name       opponent                                                 
A.J. Price 76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           bobcats   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           76ers     [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
           blazers   [Zach LaVine, Jeremy Lin, Nate Robinson, Isaia]
...

Учитывая следующие предлагаемые альтернативы:

col_target = 'nearest_neighbors'

def extend_iloc():
    # Flatten columns of lists
    col_flat = [item for sublist in df[col_target] for item in sublist] 
    # Row numbers to repeat 
    lens = df[col_target].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    cols = [i for i,c in enumerate(df.columns) if c != col_target]
    new_df = df.iloc[ilocations, cols].copy()
    new_df[col_target] = col_flat
    return new_df

def melt():
    return (pd.melt(df[col_target].apply(pd.Series).reset_index(), 
             id_vars=['name', 'opponent'],
             value_name=col_target)
            .set_index(['name', 'opponent'])
            .drop('variable', axis=1)
            .dropna()
            .sort_index())

def stack_unstack():
    return (df[col_target].apply(pd.Series)
            .stack()
            .reset_index(level=2, drop=True)
            .to_frame(col_target))

Я считаю, что extend_iloc()это самый быстрый :

%timeit extend_iloc()
3.11 ms ± 544 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit melt()
22.5 ms ± 1.25 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit stack_unstack()
11.5 ms ± 410 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Олег
источник
хорошая оценка
StephenBoesch
2
Спасибо за это, мне это очень помогло. Я использовал решение extend_iloc и обнаружил , что cols = [c for c in df.columns if c != col_target] должно быть: cols = [i for i,c in enumerate(df.columns) if c != col_target] The df.iloc[ilocations, cols].copy()ошибка , если не представлен с индексом столбца.
jdungan 03
Еще раз спасибо за предложение iloc. Я написал подробное объяснение того, как это работает, здесь: medium.com/@johnadungan/… . Надеюсь, это поможет кому-нибудь с подобной проблемой.
jdungan
7

Более приятное альтернативное решение с помощью apply (pd.Series):

df = pd.DataFrame({'listcol':[[1,2,3],[4,5,6]]})

# expand df.listcol into its own dataframe
tags = df['listcol'].apply(pd.Series)

# rename each variable is listcol
tags = tags.rename(columns = lambda x : 'listcol_' + str(x))

# join the tags dataframe back to the original dataframe
df = pd.concat([df[:], tags[:]], axis=1)
Филипп Шварц
источник
Этот расширяет столбцы, а не строки.
Олег
@ Олег прав, но вы всегда можете транспонировать DataFrame, а затем применить pd.Series - намного проще, чем большинство других предложений,
Филипп Шварц,
7

Подобно функциональности EXPLODE Hive:

import copy

def pandas_explode(df, column_to_explode):
    """
    Similar to Hive's EXPLODE function, take a column with iterable elements, and flatten the iterable to one element 
    per observation in the output table

    :param df: A dataframe to explod
    :type df: pandas.DataFrame
    :param column_to_explode: 
    :type column_to_explode: str
    :return: An exploded data frame
    :rtype: pandas.DataFrame
    """

    # Create a list of new observations
    new_observations = list()

    # Iterate through existing observations
    for row in df.to_dict(orient='records'):

        # Take out the exploding iterable
        explode_values = row[column_to_explode]
        del row[column_to_explode]

        # Create a new observation for every entry in the exploding iterable & add all of the other columns
        for explode_value in explode_values:

            # Deep copy existing observation
            new_observation = copy.deepcopy(row)

            # Add one (newly flattened) value from exploding iterable
            new_observation[column_to_explode] = explode_value

            # Add to the list of new observations
            new_observations.append(new_observation)

    # Create a DataFrame
    return_df = pandas.DataFrame(new_observations)

    # Return
    return return_df
13Гергер
источник
1
Когда я запускаю это, я получаю следующую ошибку:NameError: global name 'copy' is not defined
frmsaul
4

Итак, все эти ответы хороши, но я хотел чего-то действительно простого ^ так что вот мой вклад:

def explode(series):
    return pd.Series([x for _list in series for x in _list])                               

Вот и все ... просто используйте это, когда вам нужна новая серия, в которой списки "разнесены". Вот пример, в котором мы делаем value_counts () для выбора тако :)

In [1]: my_df = pd.DataFrame(pd.Series([['a','b','c'],['b','c'],['c']]), columns=['tacos'])      
In [2]: my_df.head()                                                                               
Out[2]: 
   tacos
0  [a, b, c]
1     [b, c]
2        [c]

In [3]: explode(my_df['tacos']).value_counts()                                                     
Out[3]: 
c    3
b    2
a    1
Брифорд Уайли
источник
2

Вот потенциальная оптимизация для больших фреймов данных. Это происходит быстрее, когда в поле «взрывающееся» несколько одинаковых значений. (Чем больше фрейм данных по сравнению с количеством уникальных значений в поле, тем лучше будет работать этот код.)

def lateral_explode(dataframe, fieldname): 
    temp_fieldname = fieldname + '_made_tuple_' 
    dataframe[temp_fieldname] = dataframe[fieldname].apply(tuple)       
    list_of_dataframes = []
    for values in dataframe[temp_fieldname].unique().tolist(): 
        list_of_dataframes.append(pd.DataFrame({
            temp_fieldname: [values] * len(values), 
            fieldname: list(values), 
        }))
    dataframe = dataframe[list(set(dataframe.columns) - set([fieldname]))]\ 
        .merge(pd.concat(list_of_dataframes), how='left', on=temp_fieldname) 
    del dataframe[temp_fieldname]

    return dataframe
Синан Озель
источник
1

Расширение .ilocответа Олега для автоматического выравнивания всех столбцов списка:

def extend_iloc(df):
    cols_to_flatten = [colname for colname in df.columns if 
    isinstance(df.iloc[0][colname], list)]
    # Row numbers to repeat 
    lens = df[cols_to_flatten[0]].apply(len)
    vals = range(df.shape[0])
    ilocations = np.repeat(vals, lens)
    # Replicate rows and add flattened column of lists
    with_idxs = [(i, c) for (i, c) in enumerate(df.columns) if c not in cols_to_flatten]
    col_idxs = list(zip(*with_idxs)[0])
    new_df = df.iloc[ilocations, col_idxs].copy()

    # Flatten columns of lists
    for col_target in cols_to_flatten:
        col_flat = [item for sublist in df[col_target] for item in sublist]
        new_df[col_target] = col_flat

    return new_df

Это предполагает, что каждый столбец списка имеет одинаковую длину.

Брайан Этвуд
источник
1

Вместо использования apply (pd.Series) вы можете сгладить столбец. Это улучшает производительность.

df = (pd.DataFrame({'name': ['A.J. Price'] * 3, 
                'opponent': ['76ers', 'blazers', 'bobcats'], 
                'nearest_neighbors': [['Zach LaVine', 'Jeremy Lin', 'Nate Robinson', 'Isaia']] * 3})
  .set_index(['name', 'opponent']))



%timeit (pd.DataFrame(df['nearest_neighbors'].values.tolist(), index = df.index)
           .stack()
           .reset_index(level = 2, drop=True).to_frame('nearest_neighbors'))

1.87 ms ± 9.74 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)


%timeit (df.nearest_neighbors.apply(pd.Series)
          .stack()
          .reset_index(level=2, drop=True)
          .to_frame('nearest_neighbors'))

2.73 ms ± 16.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Сон Кумар
источник
IndexError: слишком много уровней: индекс имеет только 2 уровня, а не 3, когда я пробую свой пример
vinsent paramanantham
1
Вы должны изменить «уровень» в reset_index согласно вашему примеру
suleep kumar