Pandas Spalte von Listen, erstellen Sie eine Zeile für jedes Listenelement

163

Ich habe einen Datenrahmen, in dem einige Zellen Listen mit mehreren Werten enthalten. Anstatt mehrere Werte in einer Zelle zu speichern, möchte ich den Datenrahmen so erweitern, dass jedes Element in der Liste eine eigene Zeile erhält (mit denselben Werten in allen anderen Spalten). Also wenn ich habe:

import pandas as pd
import numpy as np

df = pd.DataFrame(
    {'trial_num': [1, 2, 3, 1, 2, 3],
     'subject': [1, 1, 1, 2, 2, 2],
     'samples': [list(np.random.randn(3).round(2)) for i in range(6)]
    }
)

df
Out[10]: 
                 samples  subject  trial_num
0    [0.57, -0.83, 1.44]        1          1
1    [-0.01, 1.13, 0.36]        1          2
2   [1.18, -1.46, -0.94]        1          3
3  [-0.08, -4.22, -2.05]        2          1
4     [0.72, 0.79, 0.53]        2          2
5    [0.4, -0.32, -0.13]        2          3

Wie konvertiere ich in Langform, zB:

   subject  trial_num  sample  sample_num
0        1          1    0.57           0
1        1          1   -0.83           1
2        1          1    1.44           2
3        1          2   -0.01           0
4        1          2    1.13           1
5        1          2    0.36           2
6        1          3    1.18           0
# etc.

Der Index ist nicht wichtig, es ist in Ordnung, vorhandene Spalten als Index festzulegen, und die endgültige Reihenfolge ist nicht wichtig.

Marius
quelle
11
Ab Pandas 0.25 können Sie dies auch df.explode('samples')lösen. explodekann vorerst nur das Explodieren einer Spalte unterstützen.
CS95

Antworten:

48
lst_col = 'samples'

r = pd.DataFrame({
      col:np.repeat(df[col].values, df[lst_col].str.len())
      for col in df.columns.drop(lst_col)}
    ).assign(**{lst_col:np.concatenate(df[lst_col].values)})[df.columns]

Ergebnis:

In [103]: r
Out[103]:
    samples  subject  trial_num
0      0.10        1          1
1     -0.20        1          1
2      0.05        1          1
3      0.25        1          2
4      1.32        1          2
5     -0.17        1          2
6      0.64        1          3
7     -0.22        1          3
8     -0.71        1          3
9     -0.03        2          1
10    -0.65        2          1
11     0.76        2          1
12     1.77        2          2
13     0.89        2          2
14     0.65        2          2
15    -0.98        2          3
16     0.65        2          3
17    -0.30        2          3

PS hier finden Sie möglicherweise eine etwas allgemeinere Lösung


UPDATE: einige Erklärungen: IMO Der einfachste Weg, diesen Code zu verstehen, besteht darin, ihn Schritt für Schritt auszuführen:

In der folgenden Zeile wiederholen wir die Werte in einer Spalte N, wobei N- die Länge der entsprechenden Liste ist:

In [10]: np.repeat(df['trial_num'].values, df[lst_col].str.len())
Out[10]: array([1, 1, 1, 2, 2, 2, 3, 3, 3, 1, 1, 1, 2, 2, 2, 3, 3, 3], dtype=int64)

Dies kann für alle Spalten mit Skalarwerten verallgemeinert werden:

In [11]: pd.DataFrame({
    ...:           col:np.repeat(df[col].values, df[lst_col].str.len())
    ...:           for col in df.columns.drop(lst_col)}
    ...:         )
Out[11]:
    trial_num  subject
0           1        1
1           1        1
2           1        1
3           2        1
4           2        1
5           2        1
6           3        1
..        ...      ...
11          1        2
12          2        2
13          2        2
14          2        2
15          3        2
16          3        2
17          3        2

[18 rows x 2 columns]

Mit können np.concatenate()wir alle Werte in der listSpalte ( samples) reduzieren und einen 1D-Vektor erhalten:

In [12]: np.concatenate(df[lst_col].values)
Out[12]: array([-1.04, -0.58, -1.32,  0.82, -0.59, -0.34,  0.25,  2.09,  0.12,  0.83, -0.88,  0.68,  0.55, -0.56,  0.65, -0.04,  0.36, -0.31])

alles zusammen:

In [13]: pd.DataFrame({
    ...:           col:np.repeat(df[col].values, df[lst_col].str.len())
    ...:           for col in df.columns.drop(lst_col)}
    ...:         ).assign(**{lst_col:np.concatenate(df[lst_col].values)})
Out[13]:
    trial_num  subject  samples
0           1        1    -1.04
1           1        1    -0.58
2           1        1    -1.32
3           2        1     0.82
4           2        1    -0.59
5           2        1    -0.34
6           3        1     0.25
..        ...      ...      ...
11          1        2     0.68
12          2        2     0.55
13          2        2    -0.56
14          2        2     0.65
15          3        2    -0.04
16          3        2     0.36
17          3        2    -0.31

[18 rows x 3 columns]

Die Verwendung pd.DataFrame()[df.columns]garantiert, dass wir Spalten in der ursprünglichen Reihenfolge auswählen ...

MaxU
quelle
3
Dies sollte die akzeptierte Antwort sein. Die derzeit akzeptierte Antwort ist im Vergleich dazu viel, viel langsamer.
Irene
1
Ich kann nicht herausfinden, wie dies behoben werden kann: TypeError: Array-Daten können gemäß der Regel 'safe'
Greg
1
Dies ist die einzige Antwort, die für mich funktioniert hat, von den 10+, die während einer vollen Stunde der Suche in den Stapeln gefunden wurden. Vielen Dank MaxU 🙏
Olisteadman
1
Beachten Sie, dass dadurch Zeilen gelöscht werden, in denen eine leere Liste lst_colvollständig enthalten ist. Um diese Zeilen beizubehalten und lst_colmit ihnen zu füllen np.nan, können Sie dies df[lst_col] = df[lst_col].apply(lambda x: x if len(x) > 0 else [np.nan])vor der Verwendung dieser Methode tun . Offensichtlich .maskwerden keine Listen zurückgegeben, daher die .apply.
Charles Davis
Dies ist eine ausgezeichnete Antwort, die akzeptiert werden sollte. Obwohl es sich um eine Antwort auf der Ebene der schwarzen Magie handelt, würde ich mich über eine Erklärung freuen, was diese Schritte tatsächlich bewirken.
ifly6
129

Ein bisschen länger als ich erwartet hatte:

>>> df
                samples  subject  trial_num
0  [-0.07, -2.9, -2.44]        1          1
1   [-1.52, -0.35, 0.1]        1          2
2  [-0.17, 0.57, -0.65]        1          3
3  [-0.82, -1.06, 0.47]        2          1
4   [0.79, 1.35, -0.09]        2          2
5   [1.17, 1.14, -1.79]        2          3
>>>
>>> s = df.apply(lambda x: pd.Series(x['samples']),axis=1).stack().reset_index(level=1, drop=True)
>>> s.name = 'sample'
>>>
>>> df.drop('samples', axis=1).join(s)
   subject  trial_num  sample
0        1          1   -0.07
0        1          1   -2.90
0        1          1   -2.44
1        1          2   -1.52
1        1          2   -0.35
1        1          2    0.10
2        1          3   -0.17
2        1          3    0.57
2        1          3   -0.65
3        2          1   -0.82
3        2          1   -1.06
3        2          1    0.47
4        2          2    0.79
4        2          2    1.35
4        2          2   -0.09
5        2          3    1.17
5        2          3    1.14
5        2          3   -1.79

Wenn Sie einen sequentiellen Index wünschen, können Sie ihn reset_index(drop=True)auf das Ergebnis anwenden .

Update :

>>> res = df.set_index(['subject', 'trial_num'])['samples'].apply(pd.Series).stack()
>>> res = res.reset_index()
>>> res.columns = ['subject','trial_num','sample_num','sample']
>>> res
    subject  trial_num  sample_num  sample
0         1          1           0    1.89
1         1          1           1   -2.92
2         1          1           2    0.34
3         1          2           0    0.85
4         1          2           1    0.24
5         1          2           2    0.72
6         1          3           0   -0.96
7         1          3           1   -2.72
8         1          3           2   -0.11
9         2          1           0   -1.33
10        2          1           1    3.13
11        2          1           2   -0.65
12        2          2           0    0.10
13        2          2           1    0.65
14        2          2           2    0.15
15        2          3           0    0.64
16        2          3           1   -0.10
17        2          3           2   -0.76
Roman Pekar
quelle
Vielen Dank, selbst der erste Schritt der Bewerbung, um jedes Element in eine eigene Spalte zu bekommen, ist eine große Hilfe. Ich konnte mir einen etwas anderen Weg einfallen lassen, aber es sind noch ein paar Schritte erforderlich. Anscheinend ist dies in Pandas nicht einfach!
Marius
1
Gute Antwort. Sie können es ein wenig verkürzen durch Ersetzen df.apply(lambda x: pd.Series(x['samples']),axis=1)mit df.samples.apply(pd.Series).
Dennis Golomazov
1
Hinweis für die Leser: Dies leidet schrecklich unter Leistungsproblemen. Sehen Sie hier für eine viel leistungsfähigere Lösung mit numpy.
CS95
2
Was ist die Lösung, wenn die Anzahl der Stichproben nicht für alle Zeilen gleich ist?
SarahData
@SarahData Verwenden Sie df.explode()wie hier
CS95
62

Pandas> = 0,25

Serien- und DataFrame-Methoden definieren eine .explode()Methode, die Listen in separate Zeilen auflöst . Weitere Informationen finden Sie im Abschnitt "Dokumente" zum Auflösen einer listenartigen Spalte .

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

df.explode('var1')

  var1  var2
0    a     1
0    b     1
0    c     1
1    d     2
1    e     2
2  NaN     3  # empty list converted to NaN
3  NaN     4  # NaN entry preserved as-is

# to reset the index to be monotonically increasing...
df.explode('var1').reset_index(drop=True)

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

Beachten Sie, dass dies auch gemischte Spalten von Listen und Skalaren sowie leere Listen und NaNs entsprechend behandelt (dies ist ein Nachteil von repeatLösungen auf Basis ).

Beachten Sie jedoch, dass dies (vorerst) explodenur für eine einzelne Spalte funktioniert .

PS: Wenn Sie eine Spalte mit Zeichenfolgen auflösen möchten , müssen Sie zuerst ein Trennzeichen aufteilen und dann verwenden explode. Siehe diese (sehr) verwandte Antwort von mir.

cs95
quelle
8
Endlich eine Explosion () für Pandas!
Kai
2
endlich! Verblüfft! Tolle Antwort von @MaxU oben, aber das macht die Dinge viel einfacher.
süchtig
12

Sie können auch verwenden pd.concatund pd.meltdafür:

>>> objs = [df, pd.DataFrame(df['samples'].tolist())]
>>> pd.concat(objs, axis=1).drop('samples', axis=1)
   subject  trial_num     0     1     2
0        1          1 -0.49 -1.00  0.44
1        1          2 -0.28  1.48  2.01
2        1          3 -0.52 -1.84  0.02
3        2          1  1.23 -1.36 -1.06
4        2          2  0.54  0.18  0.51
5        2          3 -2.18 -0.13 -1.35
>>> pd.melt(_, var_name='sample_num', value_name='sample', 
...         value_vars=[0, 1, 2], id_vars=['subject', 'trial_num'])
    subject  trial_num sample_num  sample
0         1          1          0   -0.49
1         1          2          0   -0.28
2         1          3          0   -0.52
3         2          1          0    1.23
4         2          2          0    0.54
5         2          3          0   -2.18
6         1          1          1   -1.00
7         1          2          1    1.48
8         1          3          1   -1.84
9         2          1          1   -1.36
10        2          2          1    0.18
11        2          3          1   -0.13
12        1          1          2    0.44
13        1          2          2    2.01
14        1          3          2    0.02
15        2          1          2   -1.06
16        2          2          2    0.51
17        2          3          2   -1.35

Zuletzt können Sie bei Bedarf die ersten drei Spalten nach der ersten sortieren.

behzad.nouri
quelle
1
Dies funktioniert nur, wenn Sie a priori wissen, wie lang die Listen sein werden und / oder ob sie alle die gleiche Länge haben?
Chill2Macht
9

Als ich versuchte, die Lösung von Roman Pekar Schritt für Schritt durchzuarbeiten, um sie besser zu verstehen, entwickelte ich eine eigene Lösung, mit der melteinige verwirrende Stapel- und Indexrücksetzungen vermieden werden. Ich kann nicht sagen, dass es offensichtlich eine klarere Lösung ist:

items_as_cols = df.apply(lambda x: pd.Series(x['samples']), axis=1)
# Keep original df index as a column so it's retained after melt
items_as_cols['orig_index'] = items_as_cols.index

melted_items = pd.melt(items_as_cols, id_vars='orig_index', 
                       var_name='sample_num', value_name='sample')
melted_items.set_index('orig_index', inplace=True)

df.merge(melted_items, left_index=True, right_index=True)

Ausgabe (natürlich können wir jetzt die ursprüngliche Beispielspalte löschen):

                 samples  subject  trial_num sample_num  sample
0    [1.84, 1.05, -0.66]        1          1          0    1.84
0    [1.84, 1.05, -0.66]        1          1          1    1.05
0    [1.84, 1.05, -0.66]        1          1          2   -0.66
1    [-0.24, -0.9, 0.65]        1          2          0   -0.24
1    [-0.24, -0.9, 0.65]        1          2          1   -0.90
1    [-0.24, -0.9, 0.65]        1          2          2    0.65
2    [1.15, -0.87, -1.1]        1          3          0    1.15
2    [1.15, -0.87, -1.1]        1          3          1   -0.87
2    [1.15, -0.87, -1.1]        1          3          2   -1.10
3   [-0.8, -0.62, -0.68]        2          1          0   -0.80
3   [-0.8, -0.62, -0.68]        2          1          1   -0.62
3   [-0.8, -0.62, -0.68]        2          1          2   -0.68
4    [0.91, -0.47, 1.43]        2          2          0    0.91
4    [0.91, -0.47, 1.43]        2          2          1   -0.47
4    [0.91, -0.47, 1.43]        2          2          2    1.43
5  [-1.14, -0.24, -0.91]        2          3          0   -1.14
5  [-1.14, -0.24, -0.91]        2          3          1   -0.24
5  [-1.14, -0.24, -0.91]        2          3          2   -0.91
Marius
quelle
6

Für diejenigen, die nach einer Version der Antwort von Roman Pekar suchen, die die manuelle Benennung von Spalten vermeidet:

column_to_explode = 'samples'
res = (df
       .set_index([x for x in df.columns if x != column_to_explode])[column_to_explode]
       .apply(pd.Series)
       .stack()
       .reset_index())
res = res.rename(columns={
          res.columns[-2]:'exploded_{}_index'.format(column_to_explode),
          res.columns[-1]: '{}_exploded'.format(column_to_explode)})
Charles Davis
quelle
4

Ich fand, der einfachste Weg war:

  1. Konvertieren Sie die samplesSpalte in einen DataFrame
  2. Verbinden mit dem Original df
  3. Schmelzen

Hier gezeigt:

    df.samples.apply(lambda x: pd.Series(x)).join(df).\
melt(['subject','trial_num'],[0,1,2],var_name='sample')

        subject  trial_num sample  value
    0         1          1      0  -0.24
    1         1          2      0   0.14
    2         1          3      0  -0.67
    3         2          1      0  -1.52
    4         2          2      0  -0.00
    5         2          3      0  -1.73
    6         1          1      1  -0.70
    7         1          2      1  -0.70
    8         1          3      1  -0.29
    9         2          1      1  -0.70
    10        2          2      1  -0.72
    11        2          3      1   1.30
    12        1          1      2  -0.55
    13        1          2      2   0.10
    14        1          3      2  -0.44
    15        2          1      2   0.13
    16        2          2      2  -1.44
    17        2          3      2   0.73

Es ist erwähnenswert, dass dies möglicherweise nur funktioniert hat, weil jeder Versuch die gleiche Anzahl von Proben hat (3). Für Versuche mit verschiedenen Stichprobengrößen kann etwas Klügeres erforderlich sein.

Michael Silverstein
quelle
2

Sehr späte Antwort, aber ich möchte Folgendes hinzufügen:

Eine schnelle Lösung mit Vanilla Python, die sich auch um die sample_numSpalte im Beispiel von OP kümmert . Bei meinem eigenen großen Datensatz mit über 10 Millionen Zeilen und einem Ergebnis mit 28 Millionen Zeilen dauert dies nur etwa 38 Sekunden. Die akzeptierte Lösung bricht mit dieser Datenmenge vollständig zusammen und führt memory errorauf meinem System zu einer Lösung mit 128 GB RAM.

df = df.reset_index(drop=True)
lstcol = df.lstcol.values
lstcollist = []
indexlist = []
countlist = []
for ii in range(len(lstcol)):
    lstcollist.extend(lstcol[ii])
    indexlist.extend([ii]*len(lstcol[ii]))
    countlist.extend([jj for jj in range(len(lstcol[ii]))])
df = pd.merge(df.drop("lstcol",axis=1),pd.DataFrame({"lstcol":lstcollist,"lstcol_num":countlist},
index=indexlist),left_index=True,right_index=True).reset_index(drop=True)
Khris
quelle
2

Auch sehr spät, aber hier ist eine Antwort von Karvy1, die für mich gut funktioniert hat, wenn Sie keine Pandas> = 0.25 Version haben: https://stackoverflow.com/a/52511166/10740287

Für das obige Beispiel können Sie schreiben:

data = [(row.subject, row.trial_num, sample) for row in df.itertuples() for sample in row.samples]
data = pd.DataFrame(data, columns=['subject', 'trial_num', 'samples'])

Geschwindigkeitstest:

%timeit data = pd.DataFrame([(row.subject, row.trial_num, sample) for row in df.itertuples() for sample in row.samples], columns=['subject', 'trial_num', 'samples'])

1,33 ms ± 74,8 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Läufen, jeweils 1000 Schleifen)

%timeit data = df.set_index(['subject', 'trial_num'])['samples'].apply(pd.Series).stack().reset_index()

4,9 ms ± 189 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Läufen, jeweils 100 Schleifen)

%timeit data = pd.DataFrame({col:np.repeat(df[col].values, df['samples'].str.len())for col in df.columns.drop('samples')}).assign(**{'samples':np.concatenate(df['samples'].values)})

1,38 ms ± 25 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Läufen, jeweils 1000 Schleifen)

Rémy Pétremand
quelle
1
import pandas as pd
df = pd.DataFrame([{'Product': 'Coke', 'Prices': [100,123,101,105,99,94,98]},{'Product': 'Pepsi', 'Prices': [101,104,104,101,99,99,99]}])
print(df)
df = df.assign(Prices=df.Prices.str.split(',')).explode('Prices')
print(df)

Versuchen Sie dies in der Version pandas> = 0.25

Tapas
quelle
1
Keine Notwendigkeit für .str.split(',')weil Pricesist schon eine Liste.
Oren