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

Я пытаюсь обнаружить непрерывные промежутки, в которых соответствующая переменная изменяется линейно в пределах определенных данных в DataFrame. В данных может быть много интервалов, которые удовлетворяют этому. Я начал свой подход с ransac на основе надежной оценки линейной модели с использованием RANSAC. Однако у меня возникают проблемы с использованием примера для моих данных.

Цель

Обнаружение смежных диапазонов, в которых релевантная переменная изменяется линейно в пределах данных. Диапазоны, которые необходимо обнаружить, состоят из более чем 20 последовательных точек данных. Желаемым результатом будут даты диапазона, в котором расположены смежные промежутки.

Пример игрушки

В приведенном ниже коде игрушечного примера я генерирую случайные данные, а затем устанавливаю две части данных для создания смежных интервалов, которые изменяются линейно. Затем я пытаюсь подогнать к данным модель линейной регрессии. Остальной код, который я использовал (который здесь не показан), представляет собой остальную часть кода в Надежная оценка линейной модели с использованием страницы RANSAC. Однако я знаю, что мне нужно будет изменить этот оставшийся код, чтобы достичь цели.

import pandas as pd
import matplotlib.pyplot as plt
from sklearn import linear_model, datasets
import numpy as np

## 1. Generate random data for toy sample
times = pd.date_range('2016-08-10', periods=100, freq='15min')
df = pd.DataFrame(np.random.randint(0,100,size=(100, 1)), index=times, columns=["data"])

## 2. Set line1 within random data
date_range1_start = "2016-08-10 08:15"
date_range1_end = "2016-08-10 15:00"
line1 = df.data[date_range1_start:date_range1_end]
value_start1 = 10
values1 = range(value_start1,value_start1+len(line1))
df.data[date_range1_start:date_range1_end] = values1

## 3. Set line2 within random data
date_range2_start = "2016-08-10 17:00"
date_range2_end = "2016-08-10 22:30"
value_start2 = 90
line2 = df.data[date_range2_start:date_range2_end]
values2 = range(value_start2,value_start2-len(line2),-1)
df.data[date_range2_start:date_range2_end] = values2

## 4. Plot data
df.plot()
plt.show()

## 5. Create arrays
X = np.asarray(df.index)
y = np.asarray(df.data.tolist())

## 6. Fit line using all data
lr = linear_model.LinearRegression()
lr.fit(X, y)

Для этого игрушечного примера кода желаемый результат (который я еще не смог закодировать) будет таким DataFrame:

>>> out
              start               end
0  2016-08-10 08:15  2016-08-10 15:00
1  2016-08-10 17:00  2016-08-10 22:30

Сгенерированный график выглядит так: Сгенерированные данные

Код ошибки

Однако, когда выполняется шаг 6, я получаю сообщение об ошибке ниже:

ValueError: Ожидаемый 2D-массив, вместо этого получен 1D-массив: ... Измените форму данных либо с помощью array.reshape(-1, 1), если ваши данные имеют одну функцию, либо с помощью array.reshape(1, -1), если она содержит одну образец.

Я хотел бы иметь возможность обнаруживать в этом примере оба смежных диапазона, в которых соответствующая переменная изменяется линейно (line1 и line2). Но я не могу реализовать пример, указанный в примере кода ransac.

Вопрос

Что нужно изменить в коде, чтобы продолжить? И может ли существовать лучший подход для обнаружения смежных интервалов, в которых соответствующая переменная изменяется линейно?


person Cedric Zoppolo    schedule 22.11.2018    source источник
comment
Нам нужны образцы данных   -  person Setop    schedule 26.11.2018
comment
Я создал образцы данных в игрушечном примере. Я могу предоставить реальные данные, но не уверен, как я могу это сделать. У меня есть некоторые данные в файле рассола.   -  person Cedric Zoppolo    schedule 26.11.2018
comment
Извини. Тогда я не понимаю, что вы называете графиком. На мой взгляд, граф — это набор узлов и ребер.   -  person Setop    schedule 26.11.2018
comment
Я полагаю, что под линейными графиками в данных @CedricZoppolo подразумевает непрерывные интервалы, в которых соответствующая переменная изменяется линейно. Он имеет в виду график как график, а не график как узлы и ребра.   -  person Peter Leimbigler    schedule 26.11.2018
comment
@PeterLeimbigler прав. Возможно, я использовал неправильный термин. Я постараюсь перефразировать свой вопрос, чтобы все поняли мой вопрос.   -  person Cedric Zoppolo    schedule 26.11.2018


Ответы (2)


Чтобы просто продолжить и подогнать линейную регрессию, вам нужно будет сделать следующее:

lr.fit(X.reshape(-1,1), y)

Это потому, что sklearn ожидает двумерный массив значений, где каждая строка представляет собой строку функций.

Итак, после этого вы хотели бы подобрать модели для многих различных диапазонов и посмотреть, найдете ли вы интервалы линейного изменения?

Если вы ищете точно линейные диапазоны (что возможно обнаружить, например, в случае целых чисел, но не для чисел с плавающей запятой), то я бы сделал что-то вроде:

dff = df.diff()
dff['block'] = (dff.data.shift(1) != dff.data).astype(int).cumsum()
out = pd.DataFrame(list(dff.reset_index().groupby('block')['index'].apply(lambda x: \
    [x.min(), x.max()] if len(x) > 20 else None).dropna()))

Выход будет:

>>> out
                    0                   1
0 2016-08-10 08:30:00 2016-08-10 15:00:00
1 2016-08-10 17:15:00 2016-08-10 22:30:00

Если вы пытаетесь сделать что-то подобное, но для данных с плавающей запятой, я бы сделал что-то, используя diff таким же образом, но затем указав какую-то допустимую ошибку или что-то подобное. Пожалуйста, дайте мне знать, если это то, чего вы хотели бы достичь. Или здесь вы также можете точно использовать RANSAC в разных диапазонах (но это просто отбросит термины, которые плохо выровнены, поэтому, если будет какой-то элемент, нарушающий диапазон, вы все равно обнаружите его как диапазон). Все зависит от того, что именно вас интересует.

person zsomko    schedule 26.11.2018
comment
Я использовал (abs(dff.data.shift(1)-dff.data) >= 1e-6) вместо (dff.data.shift(1) != dff.data), так как работал с поплавками. - person Cedric Zoppolo; 11.12.2018

ValueError

Чтобы ответить на вопрос о ValueError: причина, по которой вы получаете сообщение об ошибке, а пример — нет, заключается в том, что, хотя вы изначально создаете массив с формой (100,1) (как в примере), линейная модель подходит для df.data.tolist(), которая имеет форму (100,). Это можно исправить, изменив форму X на 2D с помощью X = X.reshape(-1,1). Следующая ошибка будет заключаться в том, что значения X не могут быть в формате datetime64. Затем это можно исправить, переведя время в секунды. Например, стандартная эпоха для использования — 1970-01-01T00:00Z, а затем все точки данных представляют собой секунды с этой даты и времени. Это преобразование может быть выполнено:

X = (X - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's')

Вот полный код, показывающий линейную подгонку на графике ниже:

import pandas as pd
import matplotlib.pyplot as plt
from sklearn import linear_model, datasets
import numpy as np

## 1. Generate random data for toy sample
times = pd.date_range('2016-08-10', periods=100, freq='15min')
df = pd.DataFrame(np.random.randint(0,100,size=(100, 1)), index=times, columns=["data"])

## 2. Set line1 within random data
date_range1_start = "2016-08-10 08:15"
date_range1_end = "2016-08-10 15:00"
line1 = df.data[date_range1_start:date_range1_end]
value_start1 = 10
values1 = range(value_start1,value_start1+len(line1))
df.data[date_range1_start:date_range1_end] = values1

## 3. Set line2 within random data
date_range2_start = "2016-08-10 17:00"
date_range2_end = "2016-08-10 22:30"
value_start2 = 90
line2 = df.data[date_range2_start:date_range2_end]
values2 = range(value_start2,value_start2-len(line2),-1)
df.data[date_range2_start:date_range2_end] = values2


## 4. Create arrays
X = np.asarray(df.index)
X = ( X - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's')
X = X.reshape(-1,1)
y = np.asarray(df.data.tolist())

## 5. Fit line using all data
lr = linear_model.LinearRegression()
lr.fit(X, y)

## 6. Predict values
z = lr.predict(X)
df['linear fit'] = z

## 7. Plot
df.plot()
plt.show()

введите здесь описание изображения

Обнаружение смежных отрезков

Как вы сказали, для обнаружения диапазонов линейных данных RANSAC является хорошим методом. Для этого линейная модель будет изменена на lr = linear_model.RANSACRegressor(). Однако это вернет только один диапазон, тогда как вам нужно обнаружить все диапазоны. Это означает, что вам нужно повторять обнаружение промежутков, удаляя промежутки после каждого обнаружения, чтобы они не обнаруживались снова. Это следует повторять до тех пор, пока количество точек в обнаруженном диапазоне не станет меньше 20.

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

Ложные вкладыши

Поскольку RANSAC не проверяет, являются ли точки в диапазоне последовательными, выбросы могут быть ложно включены в диапазон. Во избежание этого точки, помеченные как находящиеся в диапазоне, следует изменить на выбросы, если они окружены выбросами. Самый быстрый способ сделать это — свернуть lr.inlier_mask_ с [1,1,1]. Любые одиночные «выбросы» будут иметь значение 1 после свертки (и, таким образом, действительно являются выбросами), в то время как точки как часть прогона диапазона будут равны 2 или 3. Таким образом, следующее исправит ложные выбросы:

lr.inlier_mask_ = np.convolve(lr.inlier_mask_.astype(int), [1,1,1], mode='same') > 1

Код

import pandas as pd
import matplotlib.pyplot as plt
from sklearn import linear_model, datasets
import numpy as np

## 1. Generate random data for toy sample
times = pd.date_range('2016-08-10', periods=100, freq='15min')
df = pd.DataFrame(np.random.randint(0,100,size=(100, 1)), index=times, columns=["data"])

## 2. Set line1 within random data
date_range1_start = "2016-08-10 08:15"
date_range1_end = "2016-08-10 15:00"
line1 = df.data[date_range1_start:date_range1_end]
value_start1 = 10
values1 = range(value_start1,value_start1+len(line1))
df.data[date_range1_start:date_range1_end] = values1

## 3. Set line2 within random data
date_range2_start = "2016-08-10 17:00"
date_range2_end = "2016-08-10 22:30"
value_start2 = 90
line2 = df.data[date_range2_start:date_range2_end]
values2 = range(value_start2,value_start2-len(line2),-1)
df.data[date_range2_start:date_range2_end] = values2

## 4. Create arrays
X = np.asarray(df.index)
X = ( X - np.datetime64('1970-01-01T00:00:00Z')) / np.timedelta64(1, 's')
X = X.reshape(-1,1)
y = np.asarray(df.data.tolist())

## 5. Fit line using all data
lr = linear_model.RANSACRegressor(residual_threshold=0.001)
lr.fit(X, y)

# Placeholders for start/end times
start_times = []
end_times = []

# Repeat fit and check if number of span inliers is greater than 20
while np.sum(lr.inlier_mask_) > 20:

    # Remove false inliers
    lr.inlier_mask_ = np.convolve(lr.inlier_mask_.astype(int), [1,1,1], mode='same') > 1

    # Store start/end times
    in_span = np.squeeze(np.where(lr.inlier_mask_))
    start_times.append(str(times[in_span[0]]))
    end_times.append(str(times[in_span[-1]]))

    # Get outlier and check for another span
    outliers = np.logical_not(lr.inlier_mask_)
    X = X[outliers]
    y = y[outliers]
    times = times[outliers]

    # Fit to remaining points
    lr.fit(X, y)

out = pd.DataFrame({'start':start_times, 'end':end_times}, columns=['start','end'])
out.sort_values('start')

Вот кадр данных out:

введите здесь описание изображения

Вы также можете построить пролеты для проверки.

plt.plot(df['data'],c='b')

for idx,row in out.iterrows():
    x0 = np.datetime64(row['start'])
    y0 = df.loc[x0]['data']
    x1 = np.datetime64(row['end'])
    y1 = df.loc[x1]['data']
    plt.plot([x0,x1],[y0,y1],c='r')

введите здесь описание изображения

person A Kruger    schedule 26.11.2018
comment
Этот ответ очень подробный и работает так, как я ожидаю. Следовательно, я установлю награду за этот ответ. Однако я приму другое решение, использующее простой, но эффективный метод. - person Cedric Zoppolo; 29.11.2018