Разделить (взорвать) запись строки фрейма данных pandas на отдельные строки



у меня есть pandas dataframe в котором один столбец текстовых строк содержит значения, разделенные запятыми. Я хочу разделить каждое поле CSV и создать новую строку для каждой записи (предположим, что CSV чист и должен быть разделен только на','). Например, a должны стать b:



In [7]: a
Out[7]:
var1 var2
0 a,b,c 1
1 d,e,f 2

In [8]: b
Out[8]:
var1 var2
0 a 1
1 b 1
2 c 1
3 d 2
4 e 2
5 f 2


до сих пор я пробовал различные простые функции, но .apply метод, кажется, принимает только одну строку в качестве возвращаемого значения, когда он используется на оси, и я не могу получить .transform на работу. Любые предложения будут очень ценится!



пример:



from pandas import DataFrame
import numpy as np
a = DataFrame([{'var1': 'a,b,c', 'var2': 1},
{'var1': 'd,e,f', 'var2': 2}])
b = DataFrame([{'var1': 'a', 'var2': 1},
{'var1': 'b', 'var2': 1},
{'var1': 'c', 'var2': 1},
{'var1': 'd', 'var2': 2},
{'var1': 'e', 'var2': 2},
{'var1': 'f', 'var2': 2}])


Я знаю, что это не сработает, потому что мы теряем метаданные DataFrame, проходя через numpy, но это должно дать вам представление о том, что я пытался сделать:



def fun(row):
letters = row['var1']
letters = letters.split(',')
out = np.array([row] * len(letters))
out['var1'] = letters
a['idx'] = range(a.shape[0])
z = a.groupby('idx')
z.transform(fun)
2488   13  

13 ответов:

Как насчет чего-то вроде этого:

In [55]: pd.concat([Series(row['var2'], row['var1'].split(','))              
                    for _, row in a.iterrows()]).reset_index()
Out[55]: 
  index  0
0     a  1
1     b  1
2     c  1
3     d  2
4     e  2
5     f  2

тогда вам просто нужно переименовать столбцы

после болезненных экспериментов, чтобы найти что-то быстрее, чем принятый ответ, я получил эту работу. Он работал примерно в 100 раз быстрее на наборе данных, который я пробовал.

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

b = DataFrame(a.var1.str.split(',').tolist(), index=a.var2).stack()
b = b.reset_index()[[0, 'var2']] # var1 variable is currently labeled 0
b.columns = ['var1', 'var2'] # renaming var1

обновление 2: более общей векторной функции, которая будет работать для нескольких normal и несколько list колонки

def explode(df, lst_cols, fill_value=''):
    # make sure `lst_cols` is a list
    if lst_cols and not isinstance(lst_cols, list):
        lst_cols = [lst_cols]
    # all columns except `lst_cols`
    idx_cols = df.columns.difference(lst_cols)

    # calculate lengths of lists
    lens = df[lst_cols[0]].str.len()

    if (lens > 0).all():
        # ALL lists in cells aren't empty
        return pd.DataFrame({
            col:np.repeat(df[col].values, lens)
            for col in idx_cols
        }).assign(**{col:np.concatenate(df[col].values) for col in lst_cols}) \
          .loc[:, df.columns]
    else:
        # at least one list in cells is empty
        return pd.DataFrame({
            col:np.repeat(df[col].values, lens)
            for col in idx_cols
        }).assign(**{col:np.concatenate(df[col].values) for col in lst_cols}) \
          .append(df.loc[lens==0, idx_cols]).fillna(fill_value) \
          .loc[:, df.columns]

Demo:

несколько list столбцы - все list столбцы должны иметь одинаковое количество элементов в каждой строке:

In [36]: df
Out[36]:
   aaa  myid        num          text
0   10     1  [1, 2, 3]  [aa, bb, cc]
1   11     2     [1, 2]      [cc, dd]
2   12     3         []            []
3   13     4         []            []

In [37]: explode(df, ['num','text'], fill_value='')
Out[37]:
   aaa  myid num text
0   10     1   1   aa
1   10     1   2   bb
2   10     1   3   cc
3   11     2   1   cc
4   11     2   2   dd
2   12     3
3   13     4

настройка:

df = pd.DataFrame({
 'aaa': {0: 10, 1: 11, 2: 12, 3: 13},
 'myid': {0: 1, 1: 2, 2: 3, 3: 4},
 'num': {0: [1, 2, 3], 1: [1, 2], 2: [], 3: []},
 'text': {0: ['aa', 'bb', 'cc'], 1: ['cc', 'dd'], 2: [], 3: []}
})

CSV столбец:

In [46]: df
Out[46]:
        var1  var2 var3
0      a,b,c     1   XX
1  d,e,f,x,y     2   ZZ

In [47]: explode(df.assign(var1=df.var1.str.split(',')), 'var1')
Out[47]:
  var1  var2 var3
0    a     1   XX
1    b     1   XX
2    c     1   XX
3    d     2   ZZ
4    e     2   ZZ
5    f     2   ZZ
6    x     2   ZZ
7    y     2   ZZ

используя этот маленький трюк, мы можем конвертировать CSV-подобный столбец в list колонка:

In [48]: df.assign(var1=df.var1.str.split(','))
Out[48]:
              var1  var2 var3
0        [a, b, c]     1   XX
1  [d, e, f, x, y]     2   ZZ

обновление:общий векторизованный подход (будет работать также для нескольких столбцов):

Оригинал DF:

In [177]: df
Out[177]:
        var1  var2 var3
0      a,b,c     1   XX
1  d,e,f,x,y     2   ZZ

устранение:

сначала давайте преобразуем строки CSV в списки:

In [178]: lst_col = 'var1' 

In [179]: x = df.assign(**{lst_col:df[lst_col].str.split(',')})

In [180]: x
Out[180]:
              var1  var2 var3
0        [a, b, c]     1   XX
1  [d, e, f, x, y]     2   ZZ

теперь мы можем сделать это:

In [181]: pd.DataFrame({
     ...:     col:np.repeat(x[col].values, x[lst_col].str.len())
     ...:     for col in x.columns.difference([lst_col])
     ...: }).assign(**{lst_col:np.concatenate(x[lst_col].values)})[x.columns.tolist()]
     ...:
Out[181]:
  var1  var2 var3
0    a     1   XX
1    b     1   XX
2    c     1   XX
3    d     2   ZZ
4    e     2   ZZ
5    f     2   ZZ
6    x     2   ZZ
7    y     2   ZZ

ответ:

вдохновленный @ AFinkelstein solution, я хотел сделать его немного более обобщенным, который можно было бы применить к DF с более чем двумя столбцами и так же быстро, ну почти так же быстро, как решение Афинкельштейна):

In [2]: df = pd.DataFrame(
   ...:    [{'var1': 'a,b,c', 'var2': 1, 'var3': 'XX'},
   ...:     {'var1': 'd,e,f,x,y', 'var2': 2, 'var3': 'ZZ'}]
   ...: )

In [3]: df
Out[3]:
        var1  var2 var3
0      a,b,c     1   XX
1  d,e,f,x,y     2   ZZ

In [4]: (df.set_index(df.columns.drop('var1',1).tolist())
   ...:    .var1.str.split(',', expand=True)
   ...:    .stack()
   ...:    .reset_index()
   ...:    .rename(columns={0:'var1'})
   ...:    .loc[:, df.columns]
   ...: )
Out[4]:
  var1  var2 var3
0    a     1   XX
1    b     1   XX
2    c     1   XX
3    d     2   ZZ
4    e     2   ZZ
5    f     2   ZZ
6    x     2   ZZ
7    y     2   ZZ

здесь функция, которую я написал для этой общей задачи. Это более эффективно, чем Series/stack методы. Порядок столбцов и имена сохраняются.

def tidy_split(df, column, sep='|', keep=False):
    """
    Split the values of a column and expand so the new DataFrame has one split
    value per row. Filters rows where the column is missing.

    Params
    ------
    df : pandas.DataFrame
        dataframe with the column to split and expand
    column : str
        the column to split and expand
    sep : str
        the string used to split the column's values
    keep : bool
        whether to retain the presplit value as it's own row

    Returns
    -------
    pandas.DataFrame
        Returns a dataframe with the same columns as `df`.
    """
    indexes = list()
    new_values = list()
    df = df.dropna(subset=[column])
    for i, presplit in enumerate(df[column].astype(str)):
        values = presplit.split(sep)
        if keep and len(values) > 1:
            indexes.append(i)
            new_values.append(presplit)
        for value in values:
            indexes.append(i)
            new_values.append(value)
    new_df = df.iloc[indexes, :].copy()
    new_df[column] = new_values
    return new_df

С этой функцией,исходный вопрос так же просто, как:

tidy_split(a, 'var1', sep=',')

подобный вопрос так: панды: как разделить текст в столбце на несколько строк?

вы могли бы сделать:

>> a=pd.DataFrame({"var1":"a,b,c d,e,f".split(),"var2":[1,2]})
>> s = a.var1.str.split(",").apply(pd.Series, 1).stack()
>> s.index = s.index.droplevel(-1)
>> del a['var1']
>> a.join(s)
   var2 var1
0     1    a
0     1    b
0     1    c
1     2    d
1     2    e
1     2    f

Я придумал решение для фреймов данных с произвольным числом столбцов (в то же время разделяя только записи одного столбца за раз).

def splitDataFrameList(df,target_column,separator):
    ''' df = dataframe to split,
    target_column = the column containing the values to split
    separator = the symbol used to perform the split

    returns: a dataframe with each entry for the target column separated, with each element moved into a new row. 
    The values in the other columns are duplicated across the newly divided rows.
    '''
    def splitListToRows(row,row_accumulator,target_column,separator):
        split_row = row[target_column].split(separator)
        for s in split_row:
            new_row = row.to_dict()
            new_row[target_column] = s
            row_accumulator.append(new_row)
    new_rows = []
    df.apply(splitListToRows,axis=1,args = (new_rows,target_column,separator))
    new_df = pandas.DataFrame(new_rows)
    return new_df

вот довольно простое сообщение, которое использует split метод от панд str accessor, а затем использует NumPy для сглаживания каждой строки в один массив.

соответствующие значения извлекаются путем повторения столбца без разделения правильное количество раз с np.repeat.

var1 = df.var1.str.split(',', expand=True).values.ravel()
var2 = np.repeat(df.var2.values, len(var1) / len(df))

pd.DataFrame({'var1': var1,
              'var2': var2})

  var1  var2
0    a     1
1    b     1
2    c     1
3    d     2
4    e     2
5    f     2

TL; DR

import pandas as pd
import numpy as np

def explode_str(df, col, sep):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
    return df.iloc[i].assign(**{col: sep.join(s).split(sep)})

def explode_list(df, col):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.len())
    return df.iloc[i].assign(**{col: np.concatenate(s)})

демонстрация

explode_str(a, 'var1', ',')

  var1  var2
0    a     1
0    b     1
0    c     1
1    d     2
1    e     2
1    f     2

давайте создадим новый фрейм данных d что есть списки

d = a.assign(var1=lambda d: d.var1.str.split(','))

explode_list(d, 'var1')

  var1  var2
0    a     1
0    b     1
0    c     1
1    d     2
1    e     2
1    f     2

Общие Замечания

я буду использовать np.arange С repeat для создания позиций индекса фрейма данных, которые я могу использовать с iloc.

часто задаваемые вопросы

почему бы мне не использовать loc?

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

почему бы вам не использовать values атрибут и разрезать?

при вызове values, если весь фрейм данных находится в одном Связном "блоке", Pandas вернет представление массива, который является "блоком". В противном случае пандам придется сколотить новый массив. При cobbling этот массив должен иметь единый тип dtype. Часто это означает возврат массива с dtype, который является object. С помощью iloc вместо нарезки values атрибут, я облегчаю себя от того, чтобы иметь дело с этим.

почему вы используете assign?

когда я использую assign используя то же имя столбца, которое я взрываю, я перезаписываю существующий столбец и сохраняю его позицию в фрейме данных.

почему значения Индекса повторить?

в силу использования iloc на повторяющихся позициях результирующий индекс показывает тот же повторяющийся шаблон. Один повтор для каждого элемента списка или строка.
Это можно сбросить с помощью reset_index(drop=True)


Для Строк

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

я sep до join строки split.
def explode_str(df, col, sep):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.count(sep) + 1)
    return df.iloc[i].assign(**{col: sep.join(s).split(sep)})

Списки

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

я использую библиотеки numpy это concatenate чтобы сжать списки вместе.

import pandas as pd
import numpy as np

def explode_list(df, col):
    s = df[col]
    i = np.arange(len(s)).repeat(s.str.len())
    return df.iloc[i].assign(**{col: np.concatenate(s)})

на основе превосходного @Dmulligan's решение, вот общая векторизованная функция (без циклов), которая разбивает столбец фрейма данных на несколько строк и объединяет его обратно в исходный фрейм данных. Он также использует большой общий

просто использовал отличный ответ jiln сверху, но нужно было расширить, чтобы разделить несколько столбцов. Думал, что поделюсь.

def splitDataFrameList(df,target_column,separator):
''' df = dataframe to split,
target_column = the column containing the values to split
separator = the symbol used to perform the split

returns: a dataframe with each entry for the target column separated, with each element moved into a new row. 
The values in the other columns are duplicated across the newly divided rows.
'''
def splitListToRows(row, row_accumulator, target_columns, separator):
    split_rows = []
    for target_column in target_columns:
        split_rows.append(row[target_column].split(separator))
    # Seperate for multiple columns
    for i in range(len(split_rows[0])):
        new_row = row.to_dict()
        for j in range(len(split_rows)):
            new_row[target_columns[j]] = split_rows[j][i]
        row_accumulator.append(new_row)
new_rows = []
df.apply(splitListToRows,axis=1,args = (new_rows,target_column,separator))
new_df = pd.DataFrame(new_rows)
return new_df

строковая функция split может принимать параметр логический аргумент 'expand'.

вот решение с использованием этого аргумента:

a.var1.str.split(",",expand=True).set_index(a.var2).stack().reset_index(level=1, drop=True).reset_index().rename(columns={0:"var1"})

Я придумал следующее решение этой проблемы:

def iter_var1(d):
    for _, row in d.iterrows():
        for v in row["var1"].split(","):
            yield (v, row["var2"])

new_a = DataFrame.from_records([i for i in iter_var1(a)],
        columns=["var1", "var2"])

другое решение, которое использует python copy package

import copy
new_observations = list()
def pandas_explode(df, column_to_explode):
    new_observations = list()
    for row in df.to_dict(orient='records'):
        explode_values = row[column_to_explode]
        del row[column_to_explode]
        if type(explode_values) is list or type(explode_values) is tuple:
            for explode_value in explode_values:
                new_observation = copy.deepcopy(row)
                new_observation[column_to_explode] = explode_value
                new_observations.append(new_observation) 
        else:
            new_observation = copy.deepcopy(row)
            new_observation[column_to_explode] = explode_values
            new_observations.append(new_observation) 
    return_df = pd.DataFrame(new_observations)
    return return_df

df = pandas_explode(df, column_name)

Comments

    Ничего не найдено.