Разделите столбец списков Pandas на несколько столбцов

Вопрос или проблема

У меня есть DataFrame Pandas с одним столбцом:

import pandas as pd

df = pd.DataFrame({"teams": [["SF", "NYG"] for _ in range(7)]})

       teams
0  [SF, NYG]
1  [SF, NYG]
2  [SF, NYG]
3  [SF, NYG]
4  [SF, NYG]
5  [SF, NYG]
6  [SF, NYG]

Как можно разделить этот столбец со списками на два столбца?

Желаемый результат:

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

Вы можете использовать конструктор DataFrame с lists, созданными с помощью to_list:

import pandas as pd

d1 = {'teams': [['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],
                ['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG']]}
df2 = pd.DataFrame(d1)
print (df2)
       teams
0  [SF, NYG]
1  [SF, NYG]
2  [SF, NYG]
3  [SF, NYG]
4  [SF, NYG]
5  [SF, NYG]
6  [SF, NYG]

df2[['team1','team2']] = pd.DataFrame(df2.teams.tolist(), index= df2.index)
print (df2)
       teams team1 team2
0  [SF, NYG]    SF   NYG
1  [SF, NYG]    SF   NYG
2  [SF, NYG]    SF   NYG
3  [SF, NYG]    SF   NYG
4  [SF, NYG]    SF   NYG
5  [SF, NYG]    SF   NYG
6  [SF, NYG]    SF   NYG

И для нового DataFrame:

df3 = pd.DataFrame(df2['teams'].to_list(), columns=['team1','team2'])
print (df3)
  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

Решение с apply(pd.Series) очень медленное:

#7k строк
df2 = pd.concat([df2]*1000).reset_index(drop=True)

In [121]: %timeit df2['teams'].apply(pd.Series)
1.79 s ± 52.5 ms per loop (среднее ± стандартное отклонение за 7 запусков, 1 цикл)

In [122]: %timeit pd.DataFrame(df2['teams'].to_list(), columns=['team1','team2'])
1.63 ms ± 54.3 µs per loop (среднее ± стандартное отклонение за 7 запусков, 1000 циклов)

Гораздо более простое решение:

pd.DataFrame(df2["teams"].to_list(), columns=['team1', 'team2'])

Получается,

  team1 team2
-------------
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG
7    SF   NYG

Если вы хотите разделить столбец разделенных строк, а не списков, вы также можете сделать:

pd.DataFrame(df["teams"].str.split('<delim>', expand=True).values,
             columns=['team1', 'team2'])

Это решение сохраняет индекс DataFrame df2, в отличие от любого решения, использующего tolist():

df3 = df2.teams.apply(pd.Series)
df3.columns = ['team1', 'team2']

Вот результат:

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

Существует более синтаксически простой способ, и, следовательно, более легкий для запоминания, по сравнению с предложенными решениями. Я предполагаю, что столбец называется ‘meta’ в DataFrame df:

df2 = pd.DataFrame(df['meta'].str.split().values.tolist())

Я хотел бы порекомендовать более эффективный и питонический способ.

Сначала определите DataFrame как в оригинальном посте:

df = pd.DataFrame({"teams": [["SF", "NYG"] for _ in range(7)]})

Мое решение:

%%timeit
df['team1'], df['team2'] = zip(*list(df['teams'].values))
>>> 761 µs ± 8.35 µs per loop

На сравнение, самое популярное решение:

%%timeit
df[['team1','team2']] = pd.DataFrame(df.teams.tolist(), index=df.index)
df = pd.DataFrame(df['teams'].to_list(), columns=['team1','team2'])
>>> 1.31 ms ± 11.2 µs per loop

Мое решение сохраняет 40% времени и намного короче. Единственное, что вам нужно запомнить — это как распаковать и изменить форму двумерного списка, используя zip(*list).

Генерация списков

Простая реализация с помощью генерации списков (моя любимая)

df = pd.DataFrame([pd.Series(x) for x in df.teams])
df.columns = ['team_{}'.format(x+1) for x in df.columns]

Время выполнения:

CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 2.71 ms

Выход:

team_1    team_2
0    SF    NYG
1    SF    NYG
2    SF    NYG
3    SF    NYG
4    SF    NYG
5    SF    NYG
6    SF    NYG

Предыдущие решения не сработали для меня, так как у меня есть nan наблюдения в моем dataframe. В моем случае df2[['team1','team2']] = pd.DataFrame(df2.teams.values.tolist(), index= df2.index) дает:

object of type 'float' has no len()

Я решаю это с помощью генерации списков. Вот воспроизводимый пример:

import pandas as pd
import numpy as np
d1 = {'teams': [['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],
            ['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG']]}
df2 = pd.DataFrame(d1)
df2.loc[2,'teams'] = np.nan
df2.loc[4,'teams'] = np.nan
df2

Выход:

        teams
0   [SF, NYG]
1   [SF, NYG]
2   NaN
3   [SF, NYG]
4   NaN
5   [SF, NYG]
6   [SF, NYG]

df2['team1']=np.nan
df2['team2']=np.nan

Решение с помощью генерации списков,

for i in [0,1]:
    df2['team{}'.format(str(i+1))]=[k[i] if isinstance(k,list) else k for k in df2['teams']]

df2

даст:

    teams   team1   team2
0   [SF, NYG]   SF  NYG
1   [SF, NYG]   SF  NYG
2   NaN        NaN  NaN
3   [SF, NYG]   SF  NYG
4   NaN        NaN  NaN
5   [SF, NYG]   SF  NYG
6   [SF, NYG]   SF  NYG

Вот еще одно решение с использованием df.transform и df.set_index:

>>> from operator import itemgetter
>>> df['teams'].transform({'item1': itemgetter(0), 'item2': itemgetter(1)})

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

Который, конечно, можно обобщить как:

>>> indices = range(len(df['teams'][0]))

>>> df['teams'].transform({f'team{i+1}': itemgetter(i) for i in indices})

  team1 team2
0    SF   NYG
1    SF   NYG
2    SF   NYG
3    SF   NYG
4    SF   NYG
5    SF   NYG
6    SF   NYG

Этот подход имеет дополнительное преимущество извлечения нужных индексов:

>>> df
                 teams
0  [SF, NYG, XYZ, ABC]
1  [SF, NYG, XYZ, ABC]
2  [SF, NYG, XYZ, ABC]
3  [SF, NYG, XYZ, ABC]
4  [SF, NYG, XYZ, ABC]
5  [SF, NYG, XYZ, ABC]
6  [SF, NYG, XYZ, ABC]

>>> indices = [0, 2]
>>> df['teams'].transform({f'team{i+1}': itemgetter(i) for i in indices})

  team1 team3
0    SF   XYZ
1    SF   XYZ
2    SF   XYZ
3    SF   XYZ
4    SF   XYZ
5    SF   XYZ
6    SF   XYZ

На основе предыдущих ответов, вот еще одно решение, которое возвращает тот же результат, что и df2.teams.apply(pd.Series) с гораздо более быстрым временем выполнения:

pd.DataFrame([{x: y for x, y in enumerate(item)} for item in df2['teams'].values.tolist()], index=df2.index)

Времена выполнения:

In [1]:
import pandas as pd
d1 = {'teams': [['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],
                ['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG'],['SF', 'NYG']]}
df2 = pd.DataFrame(d1)
df2 = pd.concat([df2]*1000).reset_index(drop=True)

In [2]: %timeit df2['teams'].apply(pd.Series)

8.27 s ± 2.73 s per loop (среднее ± стандартное отклонение за 7 запусков, 1 цикл)

In [3]: %timeit pd.DataFrame([{x: y for x, y in enumerate(item)} for item in df2['teams'].values.tolist()], index=df2.index)

35.4 ms ± 5.22 ms per loop (среднее ± стандартное отклонение за 7 запусков, 10 циклов)

Если кто-то пришел сюда, чтобы найти готовую функцию, я написал одну.

  • она находит все столбцы со списками и разворачивает их, если columns не указаны;
  • добавленные столбцы называются как column_name_0, column_name_1 и т. д.;
  • порядок столбцов сохраняется в окончательном DataFrame;
  • если strict=True, он проверяет, одинакового ли размера списки в указанном столбце.

Улучшающие комментарии приветствуются.

def unfold_columns(df, columns=[], strict=False):
    assert isinstance(columns, list), "Columns should be a list of column names"
    if len(columns) == 0:
        columns = [
            column for column in df.columns 
            if df.applymap(lambda x: isinstance(x, list)).all()[column]
        ]
    else:
        assert(all([(column in df.columns) for column in columns])), \
            "Not all given columns are found in df"
    columns_order = df.columns
    for column_name in columns:
        if df[column_name].apply(lambda x: isinstance(x, list)).all():
            if strict:
                assert len(set(df[column_name].apply(lambda x: len(x)))) == 1, \
                    f"Lists in df['{column_name}'] are not of equal length"
            unfolded = pd.DataFrame(df[column_name].tolist())
            unfolded.columns = [f'{column_name}_{x}' for x in unfolded.columns]
            columns_order = [
                *columns_order[:list(columns_order).index(column_name)], 
                *unfolded.columns, 
                *columns_order[list(columns_order).index(column_name)+1:]
            ]
            df = df.join(unfolded).drop([column_name], axis=1)
    return df[columns_order]

Чтобы добавить два новых столбца в существующий DataFrame:

df[['team1', 'team2']] = df["teams"].to_list()

Подводя итоги всех ответов.
Если вам нужно просто создать новый DataFrame с 2 столбцами

pd.DataFrame(df['teams'].tolist(), columns=['team1', 'team2'], index=df.index)

Если вы хотите назначить это в тот же df, у вас есть несколько вариантов.

  1. Самый короткий

    df[[‘team1’, ‘team2’]] = df[‘teams’].tolist()

  2. Самый медленный (не рекомендуется, это может занять в 10 раз больше времени без каких-либо преимуществ)

    df[[‘team1’, ‘team2’]] = df[‘teams’].apply(pd.Series)

  3. И самый быстрый по каким-то причинам (почти в 2 раза быстрее первого).

    df[‘team1’], df[‘team2’] = zip(*df[‘teams’].tolist())

Поэтому я рекомендую 1. Если вам действительно нужна скорость, вы можете попробовать 3, но это выглядит странно, и преимущество может исчезнуть в будущих версиях.

вы можете попробовать использовать два раза apply, чтобы создать новые столбцы ‘team1’ и ‘team2’ в вашем df

df = pd.DataFrame({"teams": [["SF", "NYG"] for _ in range(7)]})
df["team1"]=df['teams'].apply(lambda x: x[0]  )
df["team2"]=df['teams'].apply(lambda x: x[1]  )
df

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

Ответ или решение

Для решения задачи разделения столбца списков в DataFrame Pandas на несколько отдельных столбцов можно использовать несколько различных методов. В данном руководстве мы рассмотрим наиболее эффективные и удобные способы, а также обсудим их плюсы и минусы.

1. Исходные данные

Предположим, мы имеем следующий DataFrame, содержащий один столбец списков:

import pandas as pd

df = pd.DataFrame({"teams": [["SF", "NYG"] for _ in range(7)]})

Это создаст следующий DataFrame:

       teams
0  [SF, NYG]
1  [SF, NYG]
2  [SF, NYG]
3  [SF, NYG]
4  [SF, NYG]
5  [SF, NYG]
6  [SF, NYG]

2. Метод разделения с помощью to_list()

Наиболее распространенным и простым способом разделения списков на несколько столбцов является использование метода to_list(), который конвертирует списки в обычный DataFrame:

df[['team1', 'team2']] = pd.DataFrame(df['teams'].tolist(), index=df.index)

Этот метод эффективно создаст новые столбцы team1 и team2 и обеспечит правильное совпадение индексов. Итоговый DataFrame будет выглядеть так:

       teams team1 team2
0  [SF, NYG]    SF   NYG
1  [SF, NYG]    SF   NYG
2  [SF, NYG]    SF   NYG
3  [SF, NYG]    SF   NYG
4  [SF, NYG]    SF   NYG
5  [SF, NYG]    SF   NYG
6  [SF, NYG]    SF   NYG

3. Метод с использованием zip()

Второй способ, который находит широкий отклик у пользователей благодаря своей простоте и высокому времени обработки, заключается в использовании функции zip():

df['team1'], df['team2'] = zip(*df['teams'])

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

4. Использование метода apply()

Метод apply() также можно использовать для создания новых столбцов, однако он менее эффективен:

df['team1'] = df['teams'].apply(lambda x: x[0])
df['team2'] = df['teams'].apply(lambda x: x[1])

Хотя этот метод вполне работоспособен, его производительность может значительно уступать вышеуказанным методам, особенно при работе с большими объемами данных.

5. Обработка данных с пропусками

Если ваши данные содержат пропуски (например, NaN), использование методов tolist() или apply() может привести к ошибкам. В таком случае рекомендуется использовать обработку через list comprehension:

for i in [0, 1]:
    df['team{}'.format(i + 1)] = [k[i] if isinstance(k, list) else None for k in df['teams']]

Это сохранит корректный вывод и предотвратит ошибки, связанные с NaN.

Заключение

Для эффективного разделения столбца списков в Pandas вы можете использовать несколько подходов:

  1. pd.DataFrame(df['teams'].tolist()) — наиболее простой и быстрый способ, если вы хотите просто создать новый DataFrame.
  2. zip() — компактный и производительный метод для добавления новых столбцов в существующий DataFrame.
  3. Обход через list comprehension для случаев с NaN — надежный способ обработки, когда в данных могут быть пропуски.

Выбор конкретного метода будет зависеть от ваших требований и структуры данных. Непременно учитывайте дебаты о производительности, чтобы выбрать наилучший вариант для вашей задачи.

Оцените материал
Добавить комментарий

Капча загружается...