programing

panda apply()에서 여러 열을 반환합니다.

bestcode 2022. 9. 18. 12:52
반응형

panda apply()에서 여러 열을 반환합니다.

팬더 데이터 프레임이 있는데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초입니다.

좀 더 빨리 할 수 있는 방법이 없을까요?예를 들어, 적용에서 한 번에 한 열을 반환하고 세 번 실행하는 대신 세 열을 모두 한 번에 반환하여 원래 데이터 프레임에 다시 삽입할 수 있습니까?

제가 발견한 다른 질문들은 모두 여러 개의 값을 취하여 하나의 값을 반환하고자 합니다.단일 값을 사용하여 여러 열을 반환하고 싶습니다.

새 데이터가 포함된 적용된 함수에서 영상 시리즈를 반환하여 세 번 반복할 필요가 없습니다.패스axis=1적용 함수에 적용함수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)

apply를 사용하면 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

현재 답변 중 일부는 정상적으로 작동하지만, 저는 더 많은 "pandify" 옵션을 제안하고 싶습니다.이것은 현재 팬더 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_string("%.1f", s['size'] / 1024.0, grouping=True) + ' KB'
  b = locale.format_string("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB'
  c = locale.format_string("%.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새 열 또는 이전 열에 직접 할당할 수 있습니다.

그냥 읽을 수 있는 또 다른 방법일 뿐이야.이 코드는 세 개의 새 열과 값을 추가하여 적용 함수에 매개 변수를 사용하지 않고 영상 시리즈를 반환합니다.

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

정말 멋진 대답이에요!제시와 쥬메보넷에게 고마워!다음 사항에 대해 몇 가지 관찰을 해 보겠습니다.

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

확장이 좀 더 우아한(팬디파이)이지만 **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))

상위 답변 간의 퍼포먼스는 크게 달라 Jesse와 famaral42는 이미 이것에 대해 논의했지만, 상위 답변 간의 공정한 비교를 통해 Jesse의 답변에 대한 미묘하지만 중요한 세부사항을 상세히 기술할 필요가 있습니다. 즉, 함수에 전달된 인수도 퍼포먼스에 영향을 미칩니다.

(피톤 3.7.4, 판다 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(일련으로 통과)보다 2배 더 빠릅니다.

하지만 더 많은...

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

apply와 lamda를 사용하여 이 작업을 매우 빠르게 수행할 수 있습니다.여러 값을 목록으로 반환하고 to_list()를 사용합니다.

import pandas as pd

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

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

def add_and_div(x):
    add = x + 3
    div = x / 3
    return [add, div]

start = time.time()
df[['c','d']] = df['a'].apply(lambda x: add_and_div(x)).to_list()
end = time.time()

print(end-start) # output: 0.27606

심플하고 간단하게:

def func(item_df):
  return [1,'Label 1'] if item_df['col_0'] > 0 else [0,'Label 0']
 
my_df[['col_1','col2']] = my_df.apply(func, axis=1,result_type='expand')

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)

위 코드는 팬더 1.1.0 반품 시 실행되었습니다.

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

팬더 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

일반적으로 여러 값을 반환하려면 다음과 같이 합니다.

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(), 함수로 놀다

대신 수치로 계산하면 여기 있는 상위 답보다 40배 이상 빨리 갈 수 있습니다.@Rocky K의 상위 2개의 답변을 채택합니다.주요 차이점은 실제 df 120k 행에서 실행된다는 것입니다.함수를 값별로 적용하는 대신 함수를 배열별로 적용하는 것이 수학에서 Numpy가 훨씬 빠릅니다.수학에 numpy를 사용하기 때문에 가장 좋은 답은 단연 세 번째 답이다.또한 각 행에 대해 한 번씩이 아니라 1024**2와 1024**3을 각각 한 번만 계산하므로 240k 계산이 절약됩니다.내 기계의 타이밍은 다음과 같습니다.

Tuples (pass value, return tuple then zip, new columns dont exist):
Runtime: 10.935037851333618 

Tuples (pass value, return tuple then zip, new columns exist):
Runtime: 11.120025157928467 

Use numpy for math portions:
Runtime: 0.24799370765686035

다음은 Rocky K에서 채택한 스크립트로 다음 시간을 계산하기 위해 사용합니다.

import numpy as np
import pandas as pd
import locale
import time

size = np.random.random(120000) * 1000000000
data = pd.DataFrame({'Size': size})

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

print('\nTuples (pass value, return tuple then zip, new columns dont exist):')
df1 = data.copy()
start = time.time()
df1['size_kb'],  df1['size_mb'], df1['size_gb'] = zip(*df1['Size'].apply(sizes_pass_value_return_tuple))
end = time.time()
print('Runtime:', end - start, '\n')

print('Tuples (pass value, return tuple then zip, new columns exist):')
df2 = data.copy()
start = time.time()
df2 = pd.concat([df2, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])])
df2['size_kb'],  df2['size_mb'], df2['size_gb'] = zip(*df2['Size'].apply(sizes_pass_value_return_tuple))
end = time.time()
print('Runtime:', end - start, '\n')

print('Use numpy for math portions:')
df3 = data.copy()
start = time.time()
df3['size_kb'] = (df3.Size.values / 1024).round(1)
df3['size_kb'] = df3.size_kb.astype(str) + ' KB'
df3['size_mb'] = (df3.Size.values / 1024 ** 2).round(1)
df3['size_mb'] = df3.size_mb.astype(str) + ' MB'
df3['size_gb'] = (df3.Size.values / 1024 ** 3).round(1)
df3['size_gb'] = df3.size_gb.astype(str) + ' GB'
end = time.time()
print('Runtime:', end - start, '\n')

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

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)

언급URL : https://stackoverflow.com/questions/23586510/return-multiple-columns-from-pandas-apply

반응형