#python #algorithm #numpy #iteration
#python #алгоритм #numpy #итерация
Вопрос:
В настоящее время я работаю с массивом, содержащим категориальные данные. Категории организованы следующим образом: None, zoneA, zoneB Мой массив является мерой датчиков, он сообщает мне, находится ли в любой момент датчик в zoneA, zoneB или нет в зоне.
Моя цель здесь — сгладить эти значения.
Например, датчик может находиться вне зоны a или b в течение периода в 30 измерений, но если это произошло, я хочу, чтобы эти меры были «сглажены».
Пример :
массив [zoneA, zoneA, zoneA, None, None, zoneA, zoneA, None, None, None, zoneA]
должно дать
массив [zoneA, zoneA, zoneA, zoneA, zoneA, zoneA, zoneA, Нет, Нет, Нет, zoneA]
с порогом 2.
В настоящее время я использую итерацию по массивам, но ее вычисление слишком дорого и может занять 1 или 2 минуты вычислений. Существует ли существующий алгоритм для решения этой проблемы?
Мой текущий код :
def smooth(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Args:
df (pd.DataFrame): dataframe with landlot column to smooth.
Returns:dataframe smoothed
"""
df_iter = df
last = "None"
last_index = 0
for num, line in df_iter.iterrows():
if (
(line.landlot != "None")
and (line.landlot == last)
and (num - last_index <= self.delay)
and (
df_iter.iloc[(num - 1), df_iter.columns.get_loc("landlot")]
== "None"
)
):
df_iter.iloc[
last_index: (num 1), # noqa: E203
df_iter.columns.get_loc("landlot"),
] = last
if line.landlot != "None":
last = line.landlot
last_index = num
return df_iter
Комментарии:
1. Что должно
[zoneA, None, zoneB]
стать?2. Привет, @timgeb, он должен оставаться [zoneA, None, zoneB], сглаживание происходит только между двумя равными показателями.
3. Почему
[zoneA, None, None, zoneA]
сглаживается, но[zoneA, None, None, None, zoneA]
нет? редактировать: ах, это то, что вы подразумеваете под порогом 2?4. Можете ли вы показать пример вашего текущего кода?
5. @EdgarH Я просто отредактировал свой пост и получил его. Земельные участки — это «зоны»
Ответ №1:
Реализация Python
Мне нравится начинать такие вещи чисто и просто. Поэтому я просто написал простой класс, который делает именно то, что нужно, не слишком задумываясь об оптимизации. Я называю это Interpolator
, поскольку для меня это выглядит как категориальная интерполяция.
class Interpolator:
def __init__(self, data):
self.data = data
self.current_idx = 0
self.current_nan_region_start = None
self.result = None
self.maxgap = 1
def run(self, maxgap=2):
# Initialization
self.result = [None] * len(self.data)
self.maxgap = maxgap
self.current_nan_region_start = None
prev_isnan = 0
for idx, item in enumerate(self.data):
isnan = item is None
self.current_idx = idx
if isnan:
if prev_isnan:
# Result is already filled with empty data.
# Do nothing.
continue
else:
self.entered_nan_region()
prev_isnan = 1
else: # not nan
if prev_isnan:
self.exited_nan_region()
prev_isnan = 0
else:
self.continuing_in_categorical_region()
def entered_nan_region(self):
self.current_nan_region_start = self.current_idx
def continuing_in_categorical_region(self):
self.result[self.current_idx] = self.data[self.current_idx]
def exited_nan_region(self):
nan_region_end = self.current_idx - 1
nan_region_length = nan_region_end - self.current_nan_region_start 1
# Always copy the empty region endpoint even if gap is not filled
self.result[self.current_idx] = self.data[self.current_idx]
if nan_region_length > self.maxgap:
# Do not interpolate as exceeding maxgap
return
if self.current_nan_region_start == 0:
# Special case. data starts with "None"
# -> Cannot interpolate
return
if self.data[self.current_nan_region_start - 1] != self.data[self.current_idx]:
# Do not fill as both ends of missing data
# region do not have same value
return
# Fill the gap
for idx in range(self.current_nan_region_start, self.current_idx):
self.result[idx] = self.data[self.current_idx]
def interpolate(data, maxgap=2):
"""
Interpolate categorical variables over missing
values (None's).
Parameters
----------
data: list of objects
The data to interpolate. Holds
categorical data, such as 'cat', 'dog'
or 108. None is handled as missing data.
maxgap: int
The maximum gap to interpolate over.
For example, with maxgap=2, ['car', None,
None, 'car', None, None, None, 'car']
would become ['car', 'car', 'car' 'car',
None, None None, 'car'].
Note: Interpolation will only occur on missing
data regions where both ends contain the same value.
For example, [1, None, 2, None, 2] will become
[1, None, 2, 2, 2].
"""
interpolator = Interpolator(data)
interpolator.run(maxgap=maxgap)
return interpolator.result
Вот как можно было бы его использовать (код для get_data()
ниже):
data = get_data(k=100)
interpolated_data = interpolate(data)
Копирование-вставка реализации Cython
Скорее всего, реализация python достаточно быстрая, так как при размере массива 1000 000 время, необходимое для обработки данных, составляет 0,504 секунды на моем ноутбуке. В любом случае, создание версий Cython — это весело и может дать небольшой дополнительный временной бонус.
Необходимые шаги:
- Скопируйте и вставьте реализацию python в новый файл, называемый
fast_categorical_interpolate.pyx
- Создайте
setup.py
в ту же папку со следующим содержимым:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize(
"fast_categorical_interpolate.pyx",
language_level="3",
),
)
- Запустите
python setup.py build_ext --inplace
для создания расширения Cython. Вы увидите что-то вродеfast_categorical_interpolate.cp38-win_amd64.pyd
в той же папке. - Теперь вы можете использовать интерполятор следующим образом:
import fast_categorical_interpolate as fpi
data = get_data(k=100)
interpolated_data = fpi.interpolate(data)
- Конечно, в коде Cython могут быть некоторые оптимизации, которые вы могли бы выполнить, чтобы сделать это еще быстрее, но на моей машине улучшение скорости составило 38% из коробки при N = 1000 000 и 126% при N = 10 000.
Тайминги на моей машине
- При N = 100 (количество элементов в списке) реализация python примерно в 160 раз, а реализация Cython примерно в 250 раз быстрее, чем
smooth
In [8]: timeit smooth(test_df, delay=2)
10.2 ms ± 669 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [9]: timeit interpolate(data)
64.8 µs ± 7.39 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [10]: timeit fpi.interpolate(data)
41.3 µs ± 4.64 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
- При N = 10.000 разница во времени составляет примерно от 190x (Python) до 302x (Cython).
In [5]: timeit smooth(test_df, delay=2)
1.08 s ± 166 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [6]: timeit interpolate(data)
5.69 ms ± 852 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [7]: timeit fpi.interpolate(data)
3.57 ms ± 377 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
- При N = 1000 000 реализация python примерно в 210 раз быстрее, а реализация Cython примерно в 287 раз быстрее.
In [9]: timeit smooth(test_df, delay=2)
1min 45s ± 24.2 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [10]: timeit interpolate(data)
504 ms ± 67.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [11]: timeit fpi.interpolate(data)
365 ms ± 38 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Приложение
Создатель тестовых данных get_data()
import random
random.seed(0)
def get_data(k=100):
return random.choices(population=[None, "ZoneA", "ZoneB"], weights=[4, 3, 2], k=k)
Функциональные и тестовые данные для тестирования smooth()
import pandas as pd
data = get_data(k=1000)
test_df = pd.DataFrame(dict(landlot=data)).fillna("None")
def smooth(df: pd.DataFrame, delay=2) -> pd.DataFrame:
"""
Args:
df (pd.DataFrame): dataframe with landlot column to smooth.
Returns:dataframe smoothed
"""
df_iter = df
last = "None"
last_index = 0
for num, line in df_iter.iterrows():
if (
(line.landlot != "None")
and (line.landlot == last)
and (num - last_index <= delay)
and (df_iter.iloc[(num - 1), df_iter.columns.get_loc("landlot")] == "None")
):
df_iter.iloc[
last_index : (num 1), # noqa: E203
df_iter.columns.get_loc("landlot"),
] = last
if line.landlot != "None":
last = line.landlot
last_index = num
return df_iter
Обратите внимание на «текущий код»
Я думаю, что где-то должна быть какая-то ошибка копирования-вставки, поскольку «текущий код» работает не так, как все. Я заменил self.delay
на delay=2
аргумент ключевого слова, чтобы указать максимальный разрыв. Я предполагаю, что так и должно было быть. Даже при этом логика не работала корректно с предоставленными вами данными простого примера.