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)
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
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
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=',')
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
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) .
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
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
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
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
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
.
loc
?Parce que l'index peut ne pas être unique et que l'utilisation de loc
renverra chaque ligne correspondant à un index interrogé.
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.
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.
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)
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)})
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)})
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.
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"})
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
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"])
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
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
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)
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')
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.