Эффективный способ заполнить пропущенные значения с помощью groupby

У меня есть фрейм данных из миллиона строк

Фрейм данных включает идентификатор столбца, FDAT, LACT

Для каждого ID может быть несколько FDAT и LACT. FDAT должен быть одинаковым для каждого LACT для этого идентификатора. Иногда отсутствует FDAT, который я хочу заполнить соответствующим FDAT из этого идентификатора для этого LACT.

пример данных

ID  FDAT      LACT
1   1/1/2020    1
1   1/1/2020    1
1   1/1/2021    2
1   NA          2
1   1/1/2021    2
1   1/1/2022    3

В этом примере NA должен быть 01.01.2021.

Для этого я использую следующий код. К сожалению, очень медленно. Я только хочу заполнить недостающие значения. Я не хочу изменять какие-либо ненулевые записи FDAT.

df.sort_values(["ID",'DATE'], inplace=True)

df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method = "ffill")
df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method = "bfill")

Я искал код, который делал бы то же самое, но работал бы быстрее.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
0
63
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

import pandas as pd

data = {'ID': [1, 1, 1, 1, 1, 1],
        'FDAT': ['1/1/2020', '1/1/2020', '1/1/2021', None, '1/1/2021', '1/1/2022'],
        'LACT': [1, 1, 2, 2, 2, 3]}

df = pd.DataFrame(data)
import time

start_time = time.time()

df.sort_values(["ID", "FDAT", "LACT"], inplace=True)
df["FDAT"] = df.groupby(["ID", "LACT"])["FDAT"].transform(lambda x: x.fillna(method = "ffill"))

print(df)

end_time = time.time()
print("Execution time:", end_time - start_time, "seconds")


возвращение:

   ID      FDAT  LACT
0   1  1/1/2020     1
1   1  1/1/2020     1
2   1  1/1/2021     2
4   1  1/1/2021     2
5   1  1/1/2022     3
3   1  1/1/2021     2
Execution time: 0.008013486862182617 seconds

пока вы решаете:

import pandas as pd

data = {'ID': [1, 1, 1, 1, 1, 1],
        'FDAT': ['1/1/2020', '1/1/2020', '1/1/2021', None, '1/1/2021', '1/1/2022'],
        'LACT': [1, 1, 2, 2, 2, 3]}

df = pd.DataFrame(data)
import time

start_time = time.time()

df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method = "ffill")
df.loc[:, 'FDAT'] = df.groupby(['ID','LACT']).fillna(method = "bfill")
print(df)

end_time = time.time()
print("Execution time:", end_time - start_time, "seconds")

возвращает:

   ID      FDAT  LACT
0   1  1/1/2020     1
1   1  1/1/2020     1
2   1  1/1/2021     2
3   1  1/1/2021     2
4   1  1/1/2021     2
5   1  1/1/2022     3
Execution time: 0.011833429336547852 seconds

Таким образом, использование transform вместе с fffill примерно в 1,5 раза быстрее. Обратите внимание, что sort_values() исключено из времени в вашем примере кода. Итак, я считаю, что использование предложенного мной метода должно быть в 2,5 раза быстрее.

Ответ принят как подходящий

Вот некоторый векторизованный код для обработки этого. Он обрабатывает 1 миллион строк менее чем за секунду.

def fillna_fdat(df):
    a = df.set_index(['ID', 'LACT'])['FDAT']
    b = a.dropna()
    return df.assign(
        FDAT=a.fillna(b[~b.index.duplicated(keep='first')]).to_numpy()
    )

Применительно к вашему примеру входных данных:

df = pd.DataFrame({
    'ID': [1, 1, 1, 1, 1, 1],
    'FDAT': [
        '1/1/2020', '1/1/2020', '1/1/2021', float('nan'),
        '1/1/2021', '1/1/2022'],
    'LACT': [1, 1, 2, 2, 2, 3],
})

>>> fillna_fdat(df)
   ID      FDAT  LACT
0   1  1/1/2020     1
1   1  1/1/2020     1
2   1  1/1/2021     2
3   1  1/1/2021     2
4   1  1/1/2021     2
5   1  1/1/2022     3

Объяснение

Основная идея состоит в том, чтобы сделать чистое отображение (ID, LACT): FDAT. Чтобы сделать это эффективно, мы используем версию df, в которой индекс состоит из [ID, LACT]:

a = df.set_index(['ID', 'LACT'])['FDAT']
>>> a
ID  LACT
1   1       1/1/2020
    1       1/1/2020
    2       1/1/2021
    2            NaN
    2       1/1/2021
    3       1/1/2022

Мы удаляем значения NaN и повторяющиеся индексы:

b = a.dropna()
c = b[~b.index.duplicated(keep='first')]
>>> c
ID  LACT
1   1       1/1/2020
    2       1/1/2021
    3       1/1/2022

Теперь мы можем заменить все NaN в a значениями из c для того же индекса ['ID', 'LACT']:

d = a.fillna(b[~b.index.duplicated(keep='first')])
>>> d
ID  LACT
1   1       1/1/2020
    1       1/1/2020
    2       1/1/2021
    2       1/1/2021  <-- this was filled from d.loc[(1,2)]
    2       1/1/2021
    3       1/1/2022

На данный момент мы просто хотим получить те значения, которые находятся в том же порядке, что и в исходном df, и игнорировать индекс, поскольку мы заменяем df['FDAT'] на них (отсюда часть .to_numpy()). Чтобы оставить исходный df немодифицированным (я сильно возмущен любым кодом, который изменяет мои входные данные, если это явно не указано), мы получаем новый df, используя идиому df.assign(FDAT=...), и возвращаем его. Собрав все это вместе, это дает функцию, описанную выше.

Другие наблюдения

Обратите внимание, что другие столбцы, если они есть, сохраняются. Чтобы показать это и измерить производительность, давайте напишем генератор случайных чисел df:

def gen(n, k=None):
    nhalf = n // 2
    k = n // 3 if k is None else k
    df = pd.DataFrame({
        'ID': np.random.randint(0, k, nhalf),
        'FDAT': [f'1/1/{y}' for y in np.random.randint(2010, 2012+k, nhalf)],
        'LACT': np.random.randint(0, k, nhalf),
    })
    df = pd.concat([
        df,
        df.assign(FDAT=np.nan),
    ]).sample(frac=1).reset_index(drop=True).assign(
        other=np.random.uniform(size=2*nhalf)
    )
    return df

Небольшой пример:

np.random.seed(0)  # reproducible example
df = gen(10)

>>> df
   ID      FDAT  LACT     other
0   0  1/1/2010     2  0.957155
1   1  1/1/2014     0  0.140351
2   1  1/1/2010     2  0.870087
3   1       NaN     1  0.473608
4   0       NaN     2  0.800911
5   0  1/1/2012     2  0.520477
6   1       NaN     2  0.678880
7   1       NaN     0  0.720633
8   0       NaN     2  0.582020
9   1  1/1/2014     1  0.537373

>>> fillna_fdat(df)
   ID      FDAT  LACT     other
0   0  1/1/2010     2  0.957155
1   1  1/1/2014     0  0.140351
2   1  1/1/2010     2  0.870087
3   1  1/1/2014     1  0.473608
4   0  1/1/2010     2  0.800911
5   0  1/1/2012     2  0.520477
6   1  1/1/2010     2  0.678880
7   1  1/1/2014     0  0.720633
8   0  1/1/2010     2  0.582020
9   1  1/1/2014     1  0.537373

Скорость

np.random.seed(0)
df = gen(1_000_000)

%timeit fillna_fdat(df)
# 806 ms ± 13.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Меньше секунды за 1 миллион строк.

Пьер, я очень заинтересован в вашей функции. Боюсь, это немного опережает мой уровень знаний. Не могли бы вы дать описание того, что делает каждая строка. Спасибо

JohnH 20.02.2023 04:51

Справедливо; Я добавил немного пояснений. Попробуйте выполнить те же шаги с немного большим (и, возможно, неупорядоченным) примером, чтобы лучше ознакомиться с тем, что происходит в этом коде.

Pierre D 20.02.2023 06:58

Другие вопросы по теме