Rückgabe mehrerer Spalten von pandas apply ()

99

Ich habe einen Pandas DataFrame , df_test. Es enthält eine Spalte 'Größe', die die Größe in Bytes darstellt. Ich habe KB, MB und GB mit dem folgenden Code berechnet:

df_test = pd.DataFrame([
    {'dir': '/Users/uname1', 'size': 994933},
    {'dir': '/Users/uname2', 'size': 109338711},
])

df_test['size_kb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0, grouping=True) + ' KB')
df_test['size_mb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 2, grouping=True) + ' MB')
df_test['size_gb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 3, grouping=True) + ' GB')

df_test


             dir       size       size_kb   size_mb size_gb
0  /Users/uname1     994933      971.6 KB    0.9 MB  0.0 GB
1  /Users/uname2  109338711  106,776.1 KB  104.3 MB  0.1 GB

[2 rows x 5 columns]

Ich habe dies über 120.000 Zeilen ausgeführt und es dauert ungefähr 2,97 Sekunden pro Spalte * 3 = ~ 9 Sekunden gemäß% timeit.

Kann ich das trotzdem schneller machen? Kann ich beispielsweise, anstatt jeweils eine Spalte nach dem Anwenden zurückzugeben und dreimal auszuführen, alle drei Spalten in einem Durchgang zurückgeben, um sie wieder in den ursprünglichen Datenrahmen einzufügen?

Die anderen Fragen, die ich gefunden habe, möchten alle mehrere Werte annehmen und einen einzelnen Wert zurückgeben . Ich möchte einen einzelnen Wert annehmen und mehrere Spalten zurückgeben .

PaulMest
quelle

Antworten:

114

Dies ist eine alte Frage, aber der Vollständigkeit halber können Sie eine Reihe aus der angewendeten Funktion zurückgeben, die die neuen Daten enthält, sodass nicht dreimal wiederholt werden muss. Wenn Sie die Apply- axis=1Funktion übergeben, wird die Funktion sizesauf jede Zeile des Datenrahmens angewendet, und es wird eine Reihe zurückgegeben, die einem neuen Datenrahmen hinzugefügt werden soll. Diese Reihe s enthält die neuen Werte sowie die Originaldaten.

def sizes(s):
    s['size_kb'] = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
    s['size_mb'] = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    s['size_gb'] = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return s

df_test = df_test.append(rows_list)
df_test = df_test.apply(sizes, axis=1)
Nelz11
quelle
11
Ich bin überrascht, dass es fast 2 Jahre ohne die richtige Antwort verbracht hat. Ich suchte etwas anderes und stolperte darüber. Hoffe, es ist nicht zu spät, um nützlich zu sein!
Nelz11
10
Was ist rows_listin dieser Antwort?
David Stansby
Es ist nur eine Liste von Serien zum Erstellen des Datenrahmens.
Nelz11
1
Wenn die pd.Series einen Index benötigt, müssen Sie diesen bereitstellen pd.Series(data, index=...). Andernfalls erhalten Sie kryptische Fehler, wenn Sie versuchen, das Ergebnis wieder dem übergeordneten Datenrahmen zuzuweisen.
smci
91

Verwenden Sie anwenden und Reißverschluss wird 3-mal schneller als Serie Weg.

def sizes(s):    
    return locale.format("%.1f", s / 1024.0, grouping=True) + ' KB', \
        locale.format("%.1f", s / 1024.0 ** 2, grouping=True) + ' MB', \
        locale.format("%.1f", s / 1024.0 ** 3, grouping=True) + ' GB'
df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes))

Testergebnisse sind:

Separate df.apply(): 

    100 loops, best of 3: 1.43 ms per loop

Return Series: 

    100 loops, best of 3: 2.61 ms per loop

Return tuple:

    1000 loops, best of 3: 819 µs per loop
Jesse
quelle
Ich bin überrascht, dass dies keine weiteren Stimmen erhalten hat. Vielen Dank für die Freigabe der zusätzlichen Varianten- und Zeitdaten.
Kaugummi
Könnten Sie bitte erklären, wie Sie Tupel zurückgegeben haben? Es scheint die schnellste Option zu sein
Camilo
Bitte beziehen Sie sich auf meinen Beispielcode, das ist der Tupelweg.
Jesse
scheint auch am schnellsten und einfachsten zu sein. überrascht, dass ich es selbst nicht gefunden habe.
Shahir Ansari
52

Einige der aktuellen Antworten funktionieren einwandfrei, aber ich möchte eine andere, möglicherweise "pandifiziertere" Option anbieten. Dies funktioniert bei mir mit den aktuellen Pandas 0.23 (nicht sicher, ob es in früheren Versionen funktioniert):

import pandas as pd

df_test = pd.DataFrame([
  {'dir': '/Users/uname1', 'size': 994933},
  {'dir': '/Users/uname2', 'size': 109338711},
])

def sizes(s):
  a = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
  b = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
  c = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
  return a, b, c

df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes, axis=1, result_type="expand")

Beachten Sie, dass der Trick auf dem result_typeParameter von liegt apply, der das Ergebnis in einen erweitert DataFrame, der direkt neuen / alten Spalten zugewiesen werden kann.

Jaumebonet
quelle
1
Das ist richtig ... Entschuldigung ... nach einigen Überprüfungen funktioniert es in einigen Fällen mit 0,22, aber ich war in einer virtuellen Umgebung und habe tatsächlich 0,23 ausgeführt, als ich das versuchte ...: /
jaumebonet
4
Dies ist die optimalste Antwort. Vielen Dank
AdR
16

Nur ein weiterer lesbarer Weg. Dieser Code fügt drei neue Spalten und deren Werte hinzu und gibt Reihen ohne Verwendung von Parametern in der Apply-Funktion zurück.

def sizes(s):

    val_kb = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
    val_mb = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    val_gb = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return pd.Series([val_kb,val_mb,val_gb],index=['size_kb','size_mb','size_gb'])

df[['size_kb','size_mb','size_gb']] = df.apply(lambda x: sizes(x) , axis=1)

Ein allgemeines Beispiel aus: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.apply.html

df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1)

#foo  bar
#0    1    2
#1    1    2
#2    1    2
Alvaro Nortes
quelle
9

Wirklich coole Antworten! Danke Jesse und Jaumebonet! Nur einige Bemerkungen zu:

  • zip(* ...
  • ... result_type="expand")

Obwohl expand etwas eleganter ( pandifiziert ) ist, ist der Reißverschluss mindestens ** 2x schneller . In diesem einfachen Beispiel unten bin ich 4x schneller geworden .

import pandas as pd

dat = [ [i, 10*i] for i in range(1000)]

df = pd.DataFrame(dat, columns = ["a","b"])

def add_and_sub(row):
    add = row["a"] + row["b"]
    sub = row["a"] - row["b"]
    return add, sub

df[["add", "sub"]] = df.apply(add_and_sub, axis=1, result_type="expand")
# versus
df["add"], df["sub"] = zip(*df.apply(add_and_sub, axis=1))
famaral42
quelle
8

Die Leistung zwischen den Top-Antworten ist sehr unterschiedlich, und Jesse & famaral42 haben dies bereits diskutiert, aber es lohnt sich, einen fairen Vergleich zwischen den Top-Antworten zu teilen und auf ein subtiles, aber wichtiges Detail von Jesses Antwort einzugehen: das Argument, das an die Funktion, wirkt sich auch auf die Leistung aus .

(Python 3.7.4, Pandas 1.0.3)

import pandas as pd
import locale
import timeit


def create_new_df_test():
    df_test = pd.DataFrame([
      {'dir': '/Users/uname1', 'size': 994933},
      {'dir': '/Users/uname2', 'size': 109338711},
    ])
    return df_test


def sizes_pass_series_return_series(series):
    series['size_kb'] = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
    series['size_mb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    series['size_gb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return series


def sizes_pass_series_return_tuple(series):
    a = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB'
    b = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB'
    c = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB'
    return a, b, c


def sizes_pass_value_return_tuple(value):
    a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB'
    b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB'
    c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB'
    return a, b, c

Hier sind die Ergebnisse:

# 1 - Accepted (Nels11 Answer) - (pass series, return series):
9.82 ms ± 377 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 2 - Pandafied (jaumebonet Answer) - (pass series, return tuple):
2.34 ms ± 48.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 3 - Tuples (pass series, return tuple then zip):
1.36 ms ± 62.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# 4 - Tuples (Jesse Answer) - (pass value, return tuple then zip):
752 µs ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Beachten Sie, wie Tupel Rückkehr ist die schnellste Methode, aber was passiert ist in als Argument, wirkt sich auch auf die Leistung. Der Unterschied im Code ist geringfügig, aber die Leistungsverbesserung ist erheblich.

Test Nr. 4 (Bestehen eines einzelnen Werts) ist doppelt so schnell wie Test Nr. 3 (Bestehen einer Reihe), obwohl die durchgeführte Operation angeblich identisch ist.

Aber es gibt noch mehr ...

# 1a - Accepted (Nels11 Answer) - (pass series, return series, new columns exist):
3.23 ms ± 141 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 2a - Pandafied (jaumebonet Answer) - (pass series, return tuple, new columns exist):
2.31 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# 3a - Tuples (pass series, return tuple then zip, new columns exist):
1.36 ms ± 58.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# 4a - Tuples (Jesse Answer) - (pass value, return tuple then zip, new columns exist):
694 µs ± 3.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In einigen Fällen (Nr. 1a und Nr. 4a) ist das Anwenden der Funktion auf einen DataFrame, in dem die Ausgabespalten bereits vorhanden sind, schneller als das Erstellen aus der Funktion.

Hier ist der Code zum Ausführen der Tests:

# Paste and run the following in ipython console. It will not work if you run it from a .py file.
print('\nAccepted Answer (pass series, return series, new columns dont exist):')
df_test = create_new_df_test()
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)
print('Accepted Answer (pass series, return series, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit result = df_test.apply(sizes_pass_series_return_series, axis=1)

print('\nPandafied (pass series, return tuple, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")
print('Pandafied (pass series, return tuple, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand")

print('\nTuples (pass series, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))
print('Tuples (pass series, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1))

print('\nTuples (pass value, return tuple then zip, new columns dont exist):')
df_test = create_new_df_test()
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
print('Tuples (pass value, return tuple then zip, new columns exist):')
df_test = create_new_df_test()
df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
%timeit df_test['size_kb'],  df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
Rocky K.
quelle
Vielen Dank, dass Sie auch die Leistungsmerkmale aufgeschlüsselt haben!
PaulMest
2

Ich glaube, dass die Version 1.1 das in der oberen Antwort hier vorgeschlagene Verhalten verletzt.

import pandas as pd
def test_func(row):
    row['c'] = str(row['a']) + str(row['b'])
    row['d'] = row['a'] + 1
    return row

df = pd.DataFrame({'a': [1, 2, 3], 'b': ['i', 'j', 'k']})
df.apply(test_func, axis=1)

Der obige Code lief auf Pandas 1.1.0 und gibt Folgendes zurück:

   a  b   c  d
0  1  i  1i  2
1  1  i  1i  2
2  1  i  1i  2

In Pandas 1.0.5 kehrte es zurück:

   a   b    c  d
0  1   i   1i  2
1  2   j   2j  3
2  3   k   3k  4

Was ich denke, ist das, was Sie erwarten würden.

Sie sind sich nicht sicher, wie die Versionshinweise dieses Verhalten erklären. Wie hier erläutert, wird jedoch das alte Verhalten wiederbelebt, indem eine Mutation der ursprünglichen Zeilen durch Kopieren vermieden wird. dh:

def test_func(row):
    row = row.copy()   #  <---- Avoid mutating the original reference
    row['c'] = str(row['a']) + str(row['b'])
    row['d'] = row['a'] + 1
    return row
muhen
quelle
Ich denke, Ihr Codebeispiel hatte möglicherweise einen Fehler beim Kopieren / Einfügen. Könnten Sie es überprüfen und sehen, ob Sie dies einreichen wollten?
PaulMest
1
Danke @PaulMest du hattest recht. Ich habe die beiden Tippfehler behoben und einen neuen Link / eine neue Referenz hinzugefügt, unter der die Frage beantwortet wird.
Moo
1
Willkommen bei Stack Overflow! @moo
PaulMest
1

Um mehrere Werte zurückzugeben, gehe ich im Allgemeinen folgendermaßen vor

def gimmeMultiple(group):
    x1 = 1
    x2 = 2
    return array([[1, 2]])
def gimmeMultipleDf(group):
    x1 = 1
    x2 = 2
    return pd.DataFrame(array([[1,2]]), columns=['x1', 'x2'])
df['size'].astype(int).apply(gimmeMultiple)
df['size'].astype(int).apply(gimmeMultipleDf)

Das endgültige Zurückgeben eines Datenrahmens hat seine Vorteile, ist jedoch manchmal nicht erforderlich. Sie können sich die apply()Ergebnisse ansehen und ein wenig mit den Funktionen spielen;)

FooBar
quelle
Danke für dieses Beispiel. Dies gibt jedoch nicht für alle Ergebnisse einen einzigen Datenrahmen aus. Wenn ich versuche, es wieder zum ursprünglichen Datenrahmen hinzuzufügen, wird "ValueError: Array kann nicht zur korrekten Form gesendet werden" angezeigt.
PaulMest
Können Sie Code bereitstellen, um ein kleines Datenbeispiel zu erstellen?
FooBar
Sichere Sache. Ich habe gerade den Code in meinem ursprünglichen Beitrag aktualisiert, um Beispieldaten und Ausgabe einzuschließen.
PaulMest
0

Es gibt einen neuen Datenrahmen mit zwei Spalten aus der ursprünglichen.

import pandas as pd
df = ...
df_with_two_columns = df.apply(lambda row:pd.Series([row['column_1'], row['column_2']], index=['column_1', 'column_2']),axis = 1)
Waldeyr Mendes da Silva
quelle