web-dev-qa-db-fra.com

Séparer (exploser) l'entrée de chaîne de données de pandas en lignes séparées

J'ai un pandas dataframe dans lequel une colonne de chaînes de texte contient des valeurs séparées par des virgules. Je souhaite fractionner chaque champ CSV et créer une nouvelle ligne par entrée (supposons que les fichiers CSV soient propres et qu'il suffit de les fractionner sur ','). Par exemple, a devrait devenir 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

Jusqu'ici, j'ai essayé diverses fonctions simples, mais la méthode .apply ne semble accepter qu'une seule ligne comme valeur de retour lorsqu'elle est utilisée sur un axe et je ne parviens pas à obtenir .transform au travail. Toutes les suggestions seraient très appréciées!

Exemple de données: 

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}])

Je sais que cela ne fonctionnera pas car nous perdons les métadonnées DataFrame en passant par numpy, mais cela devrait vous donner une idée de ce que j'ai essayé de faire: 

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)
123
Vincent

Que diriez-vous quelque chose comme ça:

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

Ensuite, il vous suffit de renommer les colonnes

51
Chang She

Après des expériences douloureuses pour trouver quelque chose de plus rapide que la réponse acceptée, je réussis à le mettre au travail. Il a fonctionné environ 100 fois plus vite sur l'ensemble de données que j'ai essayé.

Si quelqu'un connaît un moyen de rendre cela plus élégant, veuillez modifier mon code. Je ne pouvais pas trouver une solution qui fonctionne sans définir les autres colonnes que vous souhaitez conserver en tant qu'index, puis réinitialiser l'index et renommer les colonnes, mais je suppose que quelque chose d'autre fonctionne.

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
83
DMulligan

Voici une fonction que j'ai écrite pour cette tâche courante. C'est plus efficace que les méthodes Series/stack. L'ordre des colonnes et les noms sont conservés.

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

Avec cette fonction, la question originale est aussi simple que:

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

Question similaire à: pandas: Comment diviser le texte d’une colonne en plusieurs lignes?

Vous pourriez faire:

>> 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
12
inodb

Pandas> = 0.25

Les méthodes Series et DataFrame définissent une méthode .explode() qui explose en plusieurs rangées. Voir la section docs sur décompression d'une colonne semblable à une liste .

Puisque vous avez une liste de chaînes séparées par des virgules, séparez la chaîne par une virgule pour obtenir une liste d'éléments, puis appelez explode sur cette colonne.

df = pd.DataFrame({'var1': ['a,b,c', 'd,e,f'], 'var2': [1, 2]})
df
    var1  var2
0  a,b,c     1
1  d,e,f     2

df.assign(var1=df['var1'].str.split(',')).explode('var1')

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

Notez que explode ne fonctionne que sur une seule colonne (pour l'instant).


Les NaNs et les listes vides reçoivent le traitement qu’ils méritent sans que vous ayez à passer à travers des cerceaux pour bien faire les choses.

df = pd.DataFrame({'var1': ['d,e,f', '', np.nan], 'var2': [1, 2, 3]})
df
    var1  var2
0  d,e,f     1
1            2
2    NaN     3

df['var1'].str.split(',')

0    [d, e, f]
1           []
2          NaN

df.assign(var1=df['var1'].str.split(',')).explode('var1')

  var1  var2
0    d     1
0    e     1
0    f     1
1          2  # empty list entry becomes empty string after exploding 
2  NaN     3  # NaN left un-touched

Ceci est un avantage sérieux par rapport aux solutions basées sur ravel + repeat (qui ignore complètement les listes vides et étouffe NaN) .

6
cs95

J'ai proposé une solution pour les cadres de données avec un nombre arbitraire de colonnes (tout en ne séparant que les entrées d'une colonne à la fois).

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
5
jlln

Voici un message assez simple qui utilise la méthode split de l'accesseur pandas str puis utilise NumPy pour aplatir chaque ligne dans un seul tableau.

Les valeurs correspondantes sont récupérées en répétant le nombre correct de fois avec np.repeat dans la colonne non fractionnée.

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
4
Ted Petrou

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)})

Manifestation

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

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

Créons un nouveau dataframe d qui a des listes

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

Observations générales

J'utiliserai np.arange avec repeat pour générer des positions d'index de structure de données que je peux utiliser avec iloc.

FAQ

Pourquoi je n'utilise pas loc?

Parce que l'index peut ne pas être unique et que l'utilisation de loc renverra chaque ligne correspondant à un index interrogé.

Pourquoi n'utilisez-vous pas l'attribut values pour le découper?

Lors de l'appel de values, si l'ensemble du cadre de données se trouve dans un "bloc" cohérent, Pandas renverra une vue du tableau qui est le "bloc". Sinon, les Pandas devront bricoler un nouveau tableau. Lors du pavage, ce tableau doit être d'un type uniforme. Cela signifie souvent qu’il faut retourner un tableau contenant le type object. En utilisant iloc au lieu de découper l'attribut values, je me libère du besoin de m'en occuper.

Pourquoi utilisez-vous assign?

Lorsque j'utilise assign en utilisant le même nom de colonne que celui que j'explose, j'écrase la colonne existante et conserve sa position dans le cadre de données.

Pourquoi les valeurs d'index sont-elles répétées?

En utilisant iloc sur des positions répétées, l’indice résultant présente le même motif répété. Une répétition pour chaque élément de la liste ou de la chaîne.
Ceci peut être réinitialisé avec reset_index(drop=True)


Pour cordes

Je ne veux pas avoir à scinder les cordes prématurément. Donc, au lieu de cela, je compte les occurrences de l'argument sep en supposant que si je me scindais, la longueur de la liste résultante serait égale à un de plus que le nombre de séparateurs.

J'utilise ensuite cette sep à join les chaînes puis 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)})

Pour les listes

Semblable comme pour les chaînes sauf que je n'ai pas besoin de compter les occurrences de sep parce que c'est déjà scindé.

J'utilise concatenate de Numpy pour brouiller les listes ensemble.

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)})

3
piRSquared

Basé sur l'excellent solution de @ DMulligan, voici une fonction vectorisée générique (sans boucle) qui divise une colonne d'une trame de données en plusieurs lignes et la fusionne dans la trame de données d'origine. Il utilise également une excellente fonction générique change_column_order de cette réponse .

def change_column_order(df, col_name, index):
    cols = df.columns.tolist()
    cols.remove(col_name)
    cols.insert(index, col_name)
    return df[cols]

def split_df(dataframe, col_name, sep):
    orig_col_index = dataframe.columns.tolist().index(col_name)
    orig_index_name = dataframe.index.name
    orig_columns = dataframe.columns
    dataframe = dataframe.reset_index()  # we need a natural 0-based index for proper merge
    index_col_name = (set(dataframe.columns) - set(orig_columns)).pop()
    df_split = pd.DataFrame(
        pd.DataFrame(dataframe[col_name].str.split(sep).tolist())
        .stack().reset_index(level=1, drop=1), columns=[col_name])
    df = dataframe.drop(col_name, axis=1)
    df = pd.merge(df, df_split, left_index=True, right_index=True, how='inner')
    df = df.set_index(index_col_name)
    df.index.name = orig_index_name
    # merge adds the column to the last place, so we need to move it back
    return change_column_order(df, col_name, orig_col_index)

Exemple:

df = pd.DataFrame([['a:b', 1, 4], ['c:d', 2, 5], ['e:f:g:h', 3, 6]], 
                  columns=['Name', 'A', 'B'], index=[10, 12, 13])
df
        Name    A   B
    10   a:b     1   4
    12   c:d     2   5
    13   e:f:g:h 3   6

split_df(df, 'Name', ':')
    Name    A   B
10   a       1   4
10   b       1   4
12   c       2   5
12   d       2   5
13   e       3   6
13   f       3   6    
13   g       3   6    
13   h       3   6    

Notez qu'il préserve l'index d'origine et l'ordre des colonnes. Il fonctionne également avec les images ayant un index non séquentiel.

2
Dennis Golomazov

La fonction de chaîne scindée peut prendre un argument booléen d'option 'expand'.

Voici une solution utilisant cet argument:

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

mis à jour la réponse de MaxU avec le support MultiIndex

def explode(df, lst_cols, fill_value='', preserve_index=False):
    """
    usage:
        In [134]: df
        Out[134]:
           aaa  myid        num          text
        0   10     1  [1, 2, 3]  [aa, bb, cc]
        1   11     2         []            []
        2   12     3     [1, 2]      [cc, dd]
        3   13     4         []            []

        In [135]: explode(df, ['num','text'], fill_value='')
        Out[135]:
           aaa  myid num text
        0   10     1   1   aa
        1   10     1   2   bb
        2   10     1   3   cc
        3   11     2
        4   12     3   1   cc
        5   12     3   2   dd
        6   13     4
    """
    # make sure `lst_cols` is list-alike
    if (lst_cols is not None
        and len(lst_cols) > 0
        and not isinstance(lst_cols, (list, Tuple, np.ndarray, pd.Series))):
        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()
    # preserve original index values    
    idx = np.repeat(df.index.values, lens)
    res = (pd.DataFrame({
                col:np.repeat(df[col].values, lens)
                for col in idx_cols},
                index=idx)
             .assign(**{col:np.concatenate(df.loc[lens>0, col].values)
                            for col in lst_cols}))
    # append those rows that have empty lists
    if (lens == 0).any():
        # at least one list in cells is empty
        res = (res.append(df.loc[lens==0, idx_cols], sort=False)
                  .fillna(fill_value))
    # revert the original index order
    res = res.sort_index()
    # reset index if requested
    if not preserve_index:        
        res = res.reset_index(drop=True)

    # if original index is MultiIndex build the dataframe from the multiindex
    # create "exploded" DF
    if isinstance(df.index, pd.MultiIndex):
        res = res.reindex(
            index=pd.MultiIndex.from_tuples(
                res.index,
                names=['number', 'color']
            )
    )
    return res
1
Shahar Katz

J'ai trouvé la solution suivante à ce problème:

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"])
1
Pavel

Je viens d'utiliser l'excellente réponse de jiln vue du dessus, mais il a fallu développer pour diviser plusieurs colonnes. Je pensais partager.

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
1

Il est possible de scinder et d'exploser le cadre de données sans modifier la structure du cadre

Contribution:

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



#Get the indexes which are repetative with the split 
df = df.reindex(df.index.repeat(df.var1.str.split(',').apply(len)))
#Assign the split values to dataframe column  
df['var1'] = sum(df.drop_duplicates(keep='first')['var1'].str.split(','),[])

En dehors:

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

Une autre solution qui utilise le paquet de copie python

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)
0
Ankit Maheshwari

Il y a beaucoup de réponses ici mais je suis surpris que personne n'ait mentionné la fonction d'explosion intégrée dans lespandas. Consultez le lien ci-dessous: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.explode.html#pandas.DataFrame.explode

Pour une raison quelconque, j'étais incapable d'accéder à cette fonction, j'ai donc utilisé le code ci-dessous:

import pandas_explode
pandas_explode.patch()
df_zlp_people_cnt3 = df_zlp_people_cnt2.explode('people')

enter image description here

Ci-dessus un échantillon de mes données. Comme vous pouvez le voir, la colonne people comportait une série de personnes et j'essayais de l'exploser. Le code que j'ai donné fonctionne pour les données de type liste. Essayez donc d’obtenir vos données textuelles séparées par des virgules au format liste. De plus, comme mon code utilise des fonctions intégrées, il est beaucoup plus rapide que les fonctions personnalisées/applicables.

Remarque: vous devrez peut-être installer pandas_explode avec pip.

0
Harsha Reddy