Панды: эффективный способ получить случайное подмножество из каждой строки в пределах ограниченного диапазона столбцов - PullRequest
2 голосов
/ 14 мая 2019

У меня есть несколько числовых временных рядов различной длины, хранящихся в широком кадре данных панд. Каждая строка соответствует одному ряду, а каждый столбец - точке измерения времени. Из-за разной длины эти ряды могут иметь хвосты пропущенных значений (NA), либо левые (первые моменты времени), либо правые (последние моменты времени), либо оба. В каждом ряду всегда есть непрерывная полоса без NA минимальной длины.

Мне нужно получить случайное подмножество фиксированной длины из каждой из этих строк, не включая NA. В идеале я хочу сохранить исходный фрейм данных и сообщить о подмножествах в новом.

Мне удалось получить этот вывод с очень неэффективным циклом for, который проходит по каждой строке один за другим, определяет начало для позиции обрезки, так что NA не будут включены в вывод, и копирует обрезанный результат. Это работает, но это очень медленно на больших наборах данных. Вот код:

import pandas as pd
import numpy as np
from copy import copy

def crop_random(df_in, output_length, ignore_na_tails=True):
    # Initialize new dataframe
    colnames = ['X_' + str(i) for i in range(output_length)]
    df_crop = pd.DataFrame(index=df_in.index, columns=colnames)
    # Go through all rows
    for irow in range(df_in.shape[0]):
        series = copy(df_in.iloc[irow, :])
        series = np.array(series).astype('float')
        length = len(series)
        if ignore_na_tails:
            pos_non_na = np.where(~np.isnan(series))
            # Range where the subset might start
            lo = pos_non_na[0][0]
            hi = pos_non_na[0][-1]
            left = np.random.randint(lo, hi - output_length + 2)  
        else:
            left = np.random.randint(0, length - output_length)
        series = series[left : left + output_length]
        df_crop.iloc[irow, :] = series
    return df_crop

И пример с игрушкой:

df = pd.DataFrame.from_dict({'t0': [np.NaN, 1, np.NaN],
                             't1': [np.NaN, 2, np.NaN],
                             't2': [np.NaN, 3, np.NaN],
                             't3': [1, 4, 1],
                             't4': [2, 5, 2],
                             't5': [3, 6, 3],
                             't6': [4, 7, np.NaN],
                             't7': [5, 8, np.NaN],
                             't8': [6, 9, np.NaN]})
#     t0   t1   t2  t3  t4  t5   t6   t7   t8
# 0  NaN  NaN  NaN   1   2   3    4    5    6
# 1    1    2    3   4   5   6    7    8    9
# 2  NaN  NaN  NaN   1   2   3  NaN  NaN  NaN

crop_random(df, 3)
# One possible output:
#    X_0  X_1  X_2
# 0    2    3    4
# 1    7    8    9
# 2    1    2    3

Как я мог достичь тех же результатов способом, адаптированным к большим фреймам данных?

Редактировать: мое улучшенное решение перенесено в раздел ответов.

1 Ответ

1 голос
/ 16 мая 2019

Мне удалось резко ускорить процесс:

def crop_random(dataset, output_length, ignore_na_tails=True):
    # Get a random range to crop for each row
    def get_range_crop(series, output_length, ignore_na_tails):
        series = np.array(series).astype('float')
        if ignore_na_tails:
            pos_non_na = np.where(~np.isnan(series))
            start = pos_non_na[0][0]
            end = pos_non_na[0][-1]
            left = np.random.randint(start,
                                     end - output_length + 2)  # +1 to include last in randint; +1 for slction span
        else:
            length = len(series)
            left = np.random.randint(0, length - output_length)
        right = left + output_length
        return left, right

    # Crop the rows to random range, reset_index to do concat without recreating new columns
    range_subset = dataset.apply(get_range_crop, args=(output_length,ignore_na_tails, ), axis = 1)
    new_rows = [dataset.iloc[irow, range_subset[irow][0]: range_subset[irow][1]]
                for irow in range(dataset.shape[0])]
    for row in new_rows:
        row.reset_index(drop=True, inplace=True)

    # Concatenate all rows
    dataset_cropped = pd.concat(new_rows, axis=1).T

    return dataset_cropped
...