팬더에서 여러 열을 반환 apply ()


103

나는 DataFrame 팬더 있습니다 df_test. 크기를 바이트 단위로 나타내는 'size'열을 포함합니다. 다음 코드를 사용하여 KB, MB 및 GB를 계산했습니다.

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]

나는 이것을 120,000 행 이상 실행했으며 % timeit에 따라 열당 약 2.97 초 * 3 = ~ 9 초가 걸립니다.

어쨌든 내가 이것을 더 빨리 만들 수 있습니까? 예를 들어 적용 후 한 번에 하나의 열을 반환하고 세 번 실행하는 대신 한 번에 세 열을 모두 반환하여 원래 데이터 프레임에 다시 삽입 할 수 있습니까?

내가 찾은 다른 질문은 모두 여러 값취하고 단일 값을 반환 하기 원합니다 . 단일 값취하고 여러 열을 반환 하고 싶습니다 .

답변:


116

이것은 오래된 질문이지만 완전성을 위해 새 데이터를 포함하는 적용된 함수에서 Series를 반환 할 수 있으므로 세 번 반복 할 필요가 없습니다. axis=1apply 함수에 전달 sizes하면 데이터 프레임의 각 행에 함수가 적용되고 새 데이터 프레임에 추가 할 시리즈가 반환됩니다. 이 계열 s에는 새 값과 원래 데이터가 포함됩니다.

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)

11
정답없이 거의 2 년을 보냈다는 것에 놀랐습니다. 나는 다른 것을 찾고 있었고 이것을 우연히 발견했습니다. 유용하기에는 너무 늦지 않았기를 바랍니다!
Nelz11 2016

10
rows_list이 대답 은 무엇입니까 ?
David Stansby

Dataframe을 구축하기위한 시리즈 목록 일뿐입니다.
Nelz11

1
pd.Series에 색인이 필요한 경우 pd.Series(data, index=...). 그렇지 않으면 결과를 상위 데이터 프레임에 다시 할당하려고 할 때 알 수없는 오류가 발생합니다.
smci

96

적용하고 zip을 사용하면 Series 방식보다 3 배 빠릅니다.

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

테스트 결과는 다음과 같습니다.

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

나는 이것이 더 많은 찬성표를받지 못했다는 것에 놀랐습니다. 추가 변형 및 타이밍 데이터를 공유해 주셔서 감사합니다.
gumption

튜플을 어떻게 반환했는지 설명해 주시겠습니까? 가장 빠른 옵션 인 것 같습니다
Camilo

내 샘플 코드를 참조하십시오. 튜플 방식입니다.
Jesse

너무 빠르고 쉬운 것 같습니다. 내가 직접 찾을 수 없다는 것에 놀랐습니다.
Shahir Ansari

59

현재 답글 중 일부는 잘 작동하지만, 더 "확실한"옵션을 제공하고 싶습니다. 이것은 현재 pandas 0.23 에서 작동합니다 (이전 버전에서 작동하는지 확실하지 않음).

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

트릭은의 result_type매개 변수에 있으며 apply, 이는 결과 DataFrame를 새 / 이전 열에 직접 할당 할 수있는 로 확장합니다 .


1
맞아요 ... 죄송합니다 ... 몇 가지 확인 후 일부 인스턴스에서 0.22로 작동하지만 가상 환경에 있었고 실제로 시도했을 때 0.23을 실행 중입니다 ... : /
jaumebonet

5
이것이 가장 최적의 대답입니다. 감사합니다
AdR 19

17

또 다른 읽기 가능한 방법입니다. 이 코드는 세 개의 새 열과 해당 값을 추가하여 적용 함수에서 매개 변수를 사용하지 않고 계열을 반환합니다.

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)

일반적인 예 : 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

9

정말 멋진 답변입니다! Jesse와 jaumebonet에게 감사드립니다! 다음에 관한 몇 가지 관찰 :

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

확장이 좀 더 우아하지만 ( pandifyed ), zip은 적어도 ** 2 배 빠릅니다 . 이 간단한 예제에서는 4 배 더 빨라졌습니다 .

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

8

상위 답변 간의 성능은 상당히 다양하며 Jesse & famaral42는 이미 이에 대해 논의했지만 상위 답변 간의 공정한 비교를 공유하고 Jesse의 답변에 대한 미묘하지만 중요한 세부 사항에 대해 자세히 설명 할 가치 가 있습니다. 기능도 성능에 영향을 미칩니다 .

(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

결과는 다음과 같습니다.

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

튜플을 반환하는 것이 가장 빠른 방법이지만 전달 되는 것은 인수로 것이 성능에도 영향을 미치는지 확인하십시오. 코드의 차이는 미묘하지만 성능 향상은 상당합니다.

수행 된 작업이 표면적으로 동일하더라도 테스트 # 4 (단일 값 통과)는 테스트 # 3 (연속 통과)보다 두 배 빠릅니다.

하지만 더 ...

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

경우에 따라 (# 1a 및 # 4a) 출력 열이 이미 존재하는 DataFrame에 함수를 적용하는 것이 함수에서 생성하는 것보다 빠릅니다.

다음은 테스트를 실행하기위한 코드입니다.

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

성능 특성도 분석 해주셔서 감사합니다!
PaulMest

3

나는 1.1 버전이 여기 최고의 답변에서 제안한 동작을 깨뜨린다고 생각합니다.

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)

pandas 1.1.0에서 실행 된 위 코드는 다음을 반환합니다.

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

pandas 1.0.5에서는 다음을 반환했습니다.

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

나는 당신이 기대하는 것이라고 생각합니다.

릴리스 노트 에서이 동작을 어떻게 설명 하는지 확실하지 않지만 여기에 설명 된대로 원본 행을 복사하여 변형을 방지하면 이전 동작이 부활합니다. 즉 :

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

코드 샘플에 복사 / 붙여 넣기 오류가있을 수 있습니다. 그것을 확인하고 그것이 당신이 제출하려는 의도인지 확인할 수 있습니까?
PaulMest

1
@PaulMest에게 감사드립니다. 두 가지 오타를 수정하고 질문에 대한 답변이있는 새 링크 / 참조를 추가했습니다.
moo

1
Stack Overflow에 오신 것을 환영합니다! @moo
PaulMest

1

일반적으로 여러 값을 반환하려면 이것이 내가하는 일입니다.

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)

데이터 프레임을 반환하는 것은 확실히 장점이 있지만 때로는 필요하지 않습니다. apply()반환 되는 내용을보고 함수로 약간 재생할 수 있습니다 .)


이 샘플에 감사드립니다. 그러나 모든 결과에 대해 단일 데이터 프레임을 출력하지는 않습니다. 원래 데이터 프레임에 다시 추가하려고하면 "ValueError : array is not broadcastable to correct shape"가 표시됩니다.
PaulMest 2014 년

작은 데이터 샘플을 생성하는 코드를 제공 할 수 있습니까?
FooBar

확실한 것. 샘플 데이터와 출력을 포함하도록 원래 게시물의 코드를 업데이트했습니다.
PaulMest 2014 년

0

원래 열의 두 열이있는 새 데이터 프레임을 제공합니다.

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)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.