如何检测数据在 DataFrame 中线性变化的连续跨度?

Posted

技术标签:

【中文标题】如何检测数据在 DataFrame 中线性变化的连续跨度?【英文标题】:How to detect contiguous spans in which data changes linearly within a DataFrame? 【发布时间】:2019-04-25 09:50:30 【问题描述】:

我正在尝试检测相关变量在 DataFrame 中的某些数据内线性变化的连续跨度。满足这一点的数据中可能有许多跨度。我基于Robust linear model estimation using RANSAC 使用ransac 开始了我的方法。但是,我在将示例用于我的数据时遇到了问题。

目标

检测相关变量在数据中线性变化的连续跨度。要检测的跨度由20多个连续的数据点组成。所需的输出将是放置连续跨度的范围日期。

玩具示例

在下面的玩具示例代码中,我生成随机数据,然后设置数据的两个部分以创建线性变化的连续跨度。然后我尝试将线性回归模型拟合到数据中。我使用的其余代码(此处未显示)只是Robust linear model estimation using 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: Expected 2D array, got 1D array instead: ... 重塑你的 如果您的数据有单个数据,则使用 array.reshape(-1, 1) 如果包含单个样本,则为 feature 或 array.reshape(1, -1)。

我希望能够在此示例中检测到相关变量线性变化的两个连续跨度(line1line2)。但我无法实现ransac code example 中所述的示例。

问题

我应该在我的代码中修改什么才能继续?而且,是否有更好的方法来检测相关变量线性变化的连续跨度

【问题讨论】:

我们需要样本数据 我在玩具示例中创建了示例数据。我可能会提供真实数据,但不确定我该怎么做。我在 pickle 文件中有一些数据。 对不起。然后我不明白你所谓的图表。在我看来,图是一组节点和边。 通过“数据中的线性图”,我相信@CedricZoppolo 的意思是“相关变量线性变化的连续跨度”。他的意思是图形中的图形,而不是节点和边中的图形。 @PeterLeimbigler 是正确的。我可能使用了错误的术语。我会尝试重新表述我的问题,以确保每个人都能理解我的问题。 【参考方案1】:

值错误

要回答有关 ValueError 的问题:您收到错误而示例没有的原因是,虽然您最初创建了一个形状为 (100,1) 的数组(如示例),但线性模型适合df.data.tolist() 的形状为 (100,)。这可以通过X = X.reshape(-1,1)X 重塑为2D 来解决。下一个错误将是 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')

【讨论】:

这个答案非常详细,可以按我的预期工作。因此,我将为这个答案设置赏金。但是,我会接受使用简单但有效方法的其他解决方案。【参考方案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(但这只会丢弃没有很好对齐的术语,所以如果有一些元素破坏了跨度,你仍然会检测到它是一个跨度)。一切都取决于你到底对什么感兴趣。

【讨论】:

我使用了(abs(dff.data.shift(1)-dff.data) >= 1e-6),而不是(dff.data.shift(1) != dff.data),因为我正在使用浮点数

以上是关于如何检测数据在 DataFrame 中线性变化的连续跨度?的主要内容,如果未能解决你的问题,请参考以下文章

图像处理中关于Blob的阈值的一些概念

Cocoa - 如何检测泛在容器中的变化

Python中使用DataFrame的线性回归预测模型

Vue如何检测数组变化

如何在Visual Studio代码中检测文档中语言的变化?

SQL:检测具有相同键的连续行的连续块