Pandas-Funktion auf Spalte anwenden, um mehrere neue Spalten zu erstellen?

215

Wie man das bei Pandas macht:

Ich habe eine Funktion extract_text_features für eine einzelne Textspalte, die mehrere Ausgabespalten zurückgibt. Insbesondere gibt die Funktion 6 Werte zurück.

Die Funktion funktioniert, es scheint jedoch keinen geeigneten Rückgabetyp (Pandas DataFrame / Numpy Array / Python-Liste) zu geben, sodass die Ausgabe korrekt zugewiesen werden kann df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Also ich glaube , ich mit zu iterieren fallen müssen zurück df.iterrows(), wie pro diese ?

UPDATE: Das Iterieren mit df.iterrows()ist mindestens 20x langsamer, daher habe ich die Funktion aufgegeben und in sechs verschiedene .map(lambda ...)Aufrufe aufgeteilt.

UPDATE 2: Diese Frage wurde um v0.11.0 zurück gestellt . Daher sind viele Fragen und Antworten nicht allzu relevant.

smci
quelle
1
Ich glaube nicht, dass Sie mehrere Aufgaben so ausführen können, wie Sie es geschrieben haben : df.ix[: ,10:16]. Ich denke, Sie müssen mergeIhre Funktionen in den Datensatz aufnehmen.
Zelazny7
1
Für diejenigen, die eine viel leistungsfähigere Lösung apply
Ted Petrou
Die meisten numerischen Operationen mit Pandas können vektorisiert werden - dies bedeutet, dass sie viel schneller sind als herkömmliche Iterationen. OTOH, einige Operationen (wie String und Regex) sind von Natur aus schwer zu vektorisieren. In diesem Fall ist es wichtig zu verstehen, wie Sie Ihre Daten durchlaufen. Weitere Informationen darüber, wann und wie eine Schleife über Ihre Daten durchgeführt werden soll, finden Sie unter For-Schleifen mit Pandas - Wann sollte es mich interessieren? .
CS95
@coldspeed: Das Hauptproblem war nicht die Auswahl der leistungsstärkeren unter mehreren Optionen, sondern der Kampf gegen die Pandas-Syntax, damit dies überhaupt funktioniert, und zwar um Version 0.11.0 .
smci
In der Tat ist der Kommentar für zukünftige Leser gedacht, die nach iterativen Lösungen suchen, die es entweder nicht besser wissen oder wissen, was sie tun.
CS95

Antworten:

109

Aufbauend auf der Antwort von user1827356 können Sie die Zuweisung in einem Durchgang ausführen, indem Sie df.merge:

df.merge(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})), 
    left_index=True, right_index=True)

    textcol  feature1  feature2
0  0.772692  1.772692 -0.227308
1  0.857210  1.857210 -0.142790
2  0.065639  1.065639 -0.934361
3  0.819160  1.819160 -0.180840
4  0.088212  1.088212 -0.911788

BEARBEITEN: Bitte beachten Sie den enormen Speicherverbrauch und die niedrige Geschwindigkeit: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !

Zelazny7
quelle
2
Wird aus Neugier erwartet, dass dadurch viel Speicherplatz verbraucht wird? Ich mache dies auf einem Datenrahmen, der 2,5-mil-Zeilen enthält, und ich bin fast auf Speicherprobleme gestoßen (außerdem ist es viel langsamer als nur 1 Spalte zurückzugeben).
Jeffrey04
2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1}))' wäre meiner Meinung nach eine bessere Option.
Shivam K. Thakkar
@ShivamKThakkar warum denkst du, wäre dein Vorschlag eine bessere Option? Wäre es Ihrer Meinung nach effizienter oder hätten Sie weniger Speicherkosten?
Tsando
1
Bitte beachten Sie die Geschwindigkeit und den erforderlichen Speicher: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42
189

Normalerweise mache ich das mit zip:

>>> df = pd.DataFrame([[i] for i in range(10)], columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9

>>> def powers(x):
>>>     return x, x**2, x**3, x**4, x**5, x**6

>>> df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
>>>     zip(*df['num'].map(powers))

>>> df
        num     p1      p2      p3      p4      p5      p6
0       0       0       0       0       0       0       0
1       1       1       1       1       1       1       1
2       2       2       4       8       16      32      64
3       3       3       9       27      81      243     729
4       4       4       16      64      256     1024    4096
5       5       5       25      125     625     3125    15625
6       6       6       36      216     1296    7776    46656
7       7       7       49      343     2401    16807   117649
8       8       8       64      512     4096    32768   262144
9       9       9       81      729     6561    59049   531441
ostrokach
quelle
8
Aber was machen Sie, wenn Sie 50 statt 6 Spalten hinzugefügt haben?
Max
14
@ Maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
Ostrokach
8
@ostrokach Ich denke du meintest for i, c in enumerate(columns): df[c] = temp[i]. Dank dessen habe ich wirklich den Zweck bekommen enumerate: D
rocarvaj
4
Dies ist bei weitem die eleganteste und lesbarste Lösung, die mir dafür begegnet ist. Wenn Sie keine Leistungsprobleme haben, ist die Redewendung zip(*df['col'].map(function))wahrscheinlich der richtige Weg.
François Leblanc
84

Das habe ich in der Vergangenheit getan

df = pd.DataFrame({'textcol' : np.random.rand(5)})

df
    textcol
0  0.626524
1  0.119967
2  0.803650
3  0.100880
4  0.017859

df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))
   feature1  feature2
0  1.626524 -0.373476
1  1.119967 -0.880033
2  1.803650 -0.196350
3  1.100880 -0.899120
4  1.017859 -0.982141

Der Vollständigkeit halber bearbeiten

pd.concat([df, df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))], axis=1)
    textcol feature1  feature2
0  0.626524 1.626524 -0.373476
1  0.119967 1.119967 -0.880033
2  0.803650 1.803650 -0.196350
3  0.100880 1.100880 -0.899120
4  0.017859 1.017859 -0.982141
user1827356
quelle
concat () sieht einfacher aus als merge (), um die neuen Spalten mit dem ursprünglichen Datenrahmen zu verbinden.
Kreuzkümmel
2
Gute Antwort, Sie müssen weder ein Diktat noch eine Zusammenführung verwenden, wenn Sie die Spalten außerhalb der Anwendung angebendf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt
66

Dies ist in 95% der Anwendungsfälle der richtige und einfachste Weg, dies zu erreichen:

>>> df = pd.DataFrame(zip(*[range(10)]), columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5

>>> def example(x):
...     x['p1'] = x['num']**2
...     x['p2'] = x['num']**3
...     x['p3'] = x['num']**4
...     return x

>>> df = df.apply(example, axis=1)
>>> df
    num  p1  p2  p3
0    0   0   0    0
1    1   1   1    1
2    2   4   8   16
3    3   9  27   81
4    4  16  64  256
Michael David Watson
quelle
sollten Sie nicht schreiben: df = df.apply (Beispiel (df), axis = 1) korrigieren Sie mich, wenn ich falsch
liege
1
@ user299791, Nein, in diesem Fall behandeln Sie das Beispiel als erstklassiges Objekt, sodass Sie die Funktion selbst übergeben. Diese Funktion wird auf jede Zeile angewendet.
Michael David Watson
Hallo Michael, deine Antwort hat mir bei meinem Problem geholfen. Auf jeden Fall ist Ihre Lösung besser als die df.assign () -Methode der ursprünglichen Pandas, da dies einmal pro Spalte ist. Wenn Sie mit assign () zwei neue Spalten erstellen möchten, müssen Sie df1 verwenden, um an df zu arbeiten, um eine neue Spalte1 zu erhalten, und dann df2 verwenden, um an df1 zu arbeiten, um die zweite neue Spalte zu erstellen. Dies ist ziemlich eintönig. Aber deine Methode hat mir das Leben gerettet !!! Vielen Dank!!!
commentallez-vous
1
Wird der Spaltenzuweisungscode nicht einmal pro Zeile ausgeführt? Wäre es nicht besser, a zurückzugeben pd.Series({k:v})und die Spaltenzuweisung wie in Ewans Antwort zu serialisieren?
Denis de Bernardy
29

Im Jahr 2018 verwende ich apply()mit Argumentresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')
Ben
quelle
6
So machst du es heutzutage!
Make42
1
Dies funktionierte im Jahr 2020 sofort, während viele andere Fragen dies nicht taten. Auch wird es nicht verwendet, pd.Series was in Bezug auf Leistungsprobleme immer schön ist
Théo Rubenach
1
Dies ist eine gute Lösung. Das einzige Problem ist, dass Sie den Namen für die 2 neu hinzugefügten Spalten nicht auswählen können. Sie müssen später df.rename (Spalten = {0: 'col1', 1: 'col2'})
ausführen
2
@pedrambashiri Wenn die Funktion, an die Sie übergeben, a df.applyzurückgibt dict, werden die Spalten entsprechend den Schlüsseln benannt.
Seb
24

Benutz einfach result_type="expand"

df = pd.DataFrame(np.random.randint(0,10,(10,2)), columns=["random", "a"])
df[["sq_a","cube_a"]] = df.apply(lambda x: [x.a**2, x.a**3], axis=1, result_type="expand")
Abhishek
quelle
4
Es ist hilfreich darauf hinzuweisen, dass die Option in 0.23 neu ist . Die Frage wurde am 0.11
smci
Schön, das ist einfach und funktioniert immer noch ordentlich. Dies ist der, den ich gesucht habe. Danke
Isaac Sim
Dupliziert eine frühere Antwort: stackoverflow.com/a/52363890/823470
tar
22

Zusammenfassung: Wenn Sie nur einige Spalten erstellen möchten, verwenden Siedf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

Bei dieser Lösung muss die Anzahl der neuen Spalten, die Sie erstellen, der Anzahl der Spalten entsprechen, die Sie als Eingabe für die Funktion .apply () verwenden. Wenn Sie etwas anderes tun möchten, schauen Sie sich die anderen Antworten an.

Einzelheiten Angenommen, Sie haben einen zweispaltigen Datenrahmen. Die erste Spalte gibt die Größe einer Person an, wenn sie 10 Jahre alt ist. Die zweite ist die Größe der Person, wenn sie 20 Jahre alt ist.

Angenommen, Sie müssen sowohl den Mittelwert der Körpergröße jeder Person als auch die Summe der Körpergröße jeder Person berechnen. Das sind zwei Werte pro Zeile.

Sie können dies über die folgende Funktion tun, die in Kürze angewendet wird:

def mean_and_sum(x):
    """
    Calculates the mean and sum of two heights.
    Parameters:
    :x -- the values in the row this function is applied to. Could also work on a list or a tuple.
    """

    sum=x[0]+x[1]
    mean=sum/2
    return [mean,sum]

Sie können diese Funktion folgendermaßen verwenden:

 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

(Um es klar auszudrücken: Diese Apply-Funktion nimmt die Werte aus jeder Zeile im untergeordneten Datenrahmen auf und gibt eine Liste zurück.)

Wenn Sie dies jedoch tun:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

Sie erstellen eine neue Spalte mit den Listen [Mittelwert, Summe], die Sie vermutlich vermeiden möchten, da hierfür ein weiteres Lambda / Apply erforderlich wäre.

Stattdessen möchten Sie jeden Wert in eine eigene Spalte aufteilen. Dazu können Sie zwei Spalten gleichzeitig erstellen:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)
Evan W.
quelle
4
Für Pandas 0.23 müssen Sie die Syntax verwenden:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla
Diese Funktion kann zu Fehlern führen. Die Rückgabefunktion muss return pd.Series([mean,sum])
Kanishk Mair
22

Bei mir hat das geklappt:

Eingabe df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

Funktion

def f(x):
    return pd.Series([x*x, x*x*x])

Erstellen Sie 2 neue Spalten:

df[['square x', 'cube x']] = df['col x'].apply(f)

Ausgabe:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27
Joe
quelle
13

Ich habe nach verschiedenen Möglichkeiten gesucht, und die hier gezeigte Methode (Rückgabe einer Pandas-Serie) scheint nicht am effizientesten zu sein.

Wenn wir mit einem größeren Datenrahmen aus zufälligen Daten beginnen:

# Setup a dataframe of random numbers and create a 
df = pd.DataFrame(np.random.randn(10000,3),columns=list('ABC'))
df['D'] = df.apply(lambda r: ':'.join(map(str, (r.A, r.B, r.C))), axis=1)
columns = 'new_a', 'new_b', 'new_c'

Das hier gezeigte Beispiel:

# Create the dataframe by returning a series
def method_b(v):
    return pd.Series({k: v for k, v in zip(columns, v.split(':'))})
%timeit -n10 -r3 df.D.apply(method_b)

10 Schleifen, am besten 3: 2,77 s pro Schleife

Eine alternative Methode:

# Create a dataframe from a series of tuples
def method_a(v):
    return v.split(':')
%timeit -n10 -r3 pd.DataFrame(df.D.apply(method_a).tolist(), columns=columns)

10 Schleifen, am besten 3: 8,85 ms pro Schleife

Nach meiner Einschätzung ist es weitaus effizienter, eine Reihe von Tupeln zu nehmen und diese dann in einen DataFrame zu konvertieren. Es würde mich jedoch interessieren, wenn die Leute denken, wenn es einen Fehler in meiner Arbeit gibt.

RFox
quelle
Das ist wirklich nützlich! Ich habe eine 30-fache Beschleunigung im Vergleich zu Methoden, die Serien zurückgeben.
Pushkar Nimkar
9

Die akzeptierte Lösung wird für viele Daten extrem langsam sein. Die Lösung mit der größten Anzahl von Upvotes ist etwas schwer zu lesen und auch mit numerischen Daten langsam. Wenn jede neue Spalte unabhängig von den anderen berechnet werden kann, würde ich sie einfach direkt zuweisen, ohne sie zu verwendenapply .

Beispiel mit gefälschten Zeichendaten

Erstellen Sie 100.000 Zeichenfolgen in einem DataFrame

df = pd.DataFrame(np.random.choice(['he jumped', 'she ran', 'they hiked'],
                                   size=100000, replace=True),
                  columns=['words'])
df.head()
        words
0     she ran
1     she ran
2  they hiked
3  they hiked
4  they hiked

Angenommen, wir wollten einige Textfunktionen extrahieren, wie in der ursprünglichen Frage beschrieben. Lassen Sie uns zum Beispiel das erste Zeichen extrahieren, das Vorkommen des Buchstabens 'e' zählen und die Phrase groß schreiben.

df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
df.head()
        words first  count_e         cap
0     she ran     s        1     She ran
1     she ran     s        1     She ran
2  they hiked     t        2  They hiked
3  they hiked     t        2  They hiked
4  they hiked     t        2  They hiked

Timings

%%timeit
df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
127 ms ± 585 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

def extract_text_features(x):
    return x[0], x.count('e'), x.capitalize()

%timeit df['first'], df['count_e'], df['cap'] = zip(*df['words'].apply(extract_text_features))
101 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Überraschenderweise können Sie eine bessere Leistung erzielen, indem Sie jeden Wert durchlaufen

%%timeit
a,b,c = [], [], []
for s in df['words']:
    a.append(s[0]), b.append(s.count('e')), c.append(s.capitalize())

df['first'] = a
df['count_e'] = b
df['cap'] = c
79.1 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Ein weiteres Beispiel mit gefälschten numerischen Daten

Erstellen Sie 1 Million Zufallszahlen und testen Sie die powersFunktion von oben.

df = pd.DataFrame(np.random.rand(1000000), columns=['num'])


def powers(x):
    return x, x**2, x**3, x**4, x**5, x**6

%%timeit
df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
       zip(*df['num'].map(powers))
1.35 s ± 83.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Das Zuweisen jeder Spalte ist 25x schneller und gut lesbar:

%%timeit 
df['p1'] = df['num'] ** 1
df['p2'] = df['num'] ** 2
df['p3'] = df['num'] ** 3
df['p4'] = df['num'] ** 4
df['p5'] = df['num'] ** 5
df['p6'] = df['num'] ** 6
51.6 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Ich habe hier eine ähnliche Antwort mit weiteren Details gegeben, warum dies applynormalerweise nicht der richtige Weg ist.

Ted Petrou
quelle
8

Habe die gleiche Antwort in zwei anderen ähnlichen Fragen gepostet. Ich bevorzuge dies, indem ich die Rückgabewerte der Funktion in einer Reihe zusammenfasse:

def f(x):
    return pd.Series([x**2, x**3])

Verwenden Sie anschließend "Anwenden" wie folgt, um separate Spalten zu erstellen:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)
Dmytro Bugayev
quelle
1

Sie können die gesamte Zeile anstelle von Werten zurückgeben:

df = df.apply(extract_text_features,axis = 1)

Dabei gibt die Funktion die Zeile zurück

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row
Saket Bajaj
quelle
Nein, ich möchte nicht extract_text_featuresauf jede Spalte des df anwenden , sondern nur auf die df.textcol
Textspalte
-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

Das hat bei mir funktioniert. Neue Spalte wird mit verarbeiteten alten Spaltendaten erstellt.

user2902302
quelle
2
Dies gibt nicht "mehrere neue Spalten" zurück
pedram bashiri
Dies gibt nicht "mehrere neue Spalten" zurück, daher wird die Frage nicht beantwortet. Könnten Sie es bitte löschen?
smci