У меня есть фрейм данных из миллиона строк
Фрейм данных включает идентификатор столбца, 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")
Я искал код, который делал бы то же самое, но работал бы быстрее.
Как вы видите ниже, я даю вам гораздо более быструю альтернативу вместе с исходным временем и временем вычислений:
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 миллион строк.
Справедливо; Я добавил немного пояснений. Попробуйте выполнить те же шаги с немного большим (и, возможно, неупорядоченным) примером, чтобы лучше ознакомиться с тем, что происходит в этом коде.
Пьер, я очень заинтересован в вашей функции. Боюсь, это немного опережает мой уровень знаний. Не могли бы вы дать описание того, что делает каждая строка. Спасибо