查询构建指南

概览

在本部分中,我们将介绍如何正确配置异常值检测查询。概括来讲,您需要调整四种类型的参数:

  • 数据选择参数,用于指定我们的分析中包含哪些数据。 示例:QueryDataSetRequest.dimensionNamesQueryDataSetRequest.pinnedDimensionsQueryDataSetRequest.testedInterval
  • Aggregation 参数,用于指定多个事件如何分组以生成用于比较切片的值。示例:ForecastParams.aggregatedDimension
  • 预测参数,用于配置用于计算预期值的预测算法。示例:ForecastParams.holdoutForecastParams.forecastHistoryForecastParams.minDensity
  • 灵敏度调节参数,如果发生变化,则可以增加或减少所报告的异常次数、延迟时间和计算资源。示例:ForecastParams.maxPositiveRelativeChangeForecastParams.maxNegativeRelativeChangeForecastParams.forecastExtraWeight

前提条件

请按照快速入门指南中的设置说明进行操作,以确保您可以运行本指南中的所有命令。

我们会通过查询我们已经使用 GDELT 项目中的数据预加载的公开演示数据集,来展示每个参数对返回的结果有何影响。

将以下查询保存为工作目录中的 query.json

{
  dimensionNames: ["EntityLOCATION"],
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
  forecastParams: {
    forecastHistory: "2592000s",
    holdout: 10.0,
    minDensity: 0.0,
    maxPositiveRelativeChange: 1.0,
    maxNegativeRelativeChange: 1.0,
    forecastExtraWeight: 200.0
  }
}

如快速入门指南中所述,您可以使用以下命令发出查询:

$ gcurl -X POST -d @query.json https://timeseriesinsights.googleapis.com/v1/projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104:query

在本指南中,我们将更改 query.json 中的不同参数,以说明它对输出有何影响。您可以修改您的副本,并使用上面的 gcurl 命令重发查询,该命令在快速入门指南中被当作实例。

数据选择

异常值检测查询用于评估特定数据集内在特定时间范围内是否有任何异常,分离部分维度的数据,并视需要过滤某些维度的值。在本部分中,我们将详细介绍如何控制所有这些数据选择步骤。

时间间隔规范

我们可以通过设置 QueryDataSetRequest.tested_interval,指定我们想要探测异常的时间间隔。在 query.json 中,我们指定检测在 2019 年 4 月 15 日当天发生的异常:

...
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
...

例如,如果我们想检测在 2019 年 4 月 15 日下午 1 点到 2 点之间发生的异常,我们将 testedInterval 设置为:

...
  testedInterval: {
    startTime: "2019-04-15T13:00:00Z",
    length: "3600s"
  },
...

提示:通过设置不同的开始时间和不同的长度来使用 testedInterval 值,并查看返回的异常值有何不同。

切片

在检测特定时间间隔内的异常值时,我们还必须指定事件的分组方式。将 dimensionNames 设置为我们希望对整个数据集进行切片的维度集。

query.json 中,我们只按“EntityLOCATION”维度细分数据:dimensionNames: ["EntityLOCATION"]。您可以在结果列表中发现,所有切片都只是“EntityLocation”维度的值有所不同。

如果我们想要将数据拆分为多个维度,我们可以指定多个维度名称:

{
  dimensionNames: ["EntityLOCATION", "EntityORGANIZATION"],
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
  forecastParams: {
    forecastHistory: "2592000s",
    holdout: 10.0,
    minDensity: 0.0,
    maxPositiveRelativeChange: 1.0,
    maxNegativeRelativeChange: 1.0,
    forecastExtraWeight: 50.0
  }
}

运行此查询时,您会注意到生成的切片会获取两个指定维度(“EntityLOCATION”和“EntityORGANIZATION”)的值(我们仅显示所返回的第一个异常切片):

$ gcurl -X POST -d @query.json https://timeseriesinsights.googleapis.com/v1/projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104:query

{
  "name": "projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104",
  "anomalyDetectionResult": {
    "anomalies": [
      {
        "dimensions": [
          {
            "name": "EntityLOCATION",
            "stringVal": "Seine"
          },
          {
            "name": "EntityORGANIZATION",
            "stringVal": "AP"
          }
        ],
        "result": {
          "holdoutErrors": {},
          "trainingErrors": {},
          "forecastStats": {
            "numAnomalies": 1
          },
          "testedIntervalActual": 344
        },
        "status": {}
      },
...

过滤

如果您只想分析数据集的部分子集,则可以通过设置 pinnedDimensions,仅过滤出某些维度中包含特定值的事件。

EntityORGANIZATION 过滤并按 EntityLOCATION 排序的说明(仅显示第一个结果):

$ cat query.json

{
  dimensionNames: ["EntityLOCATION"],
  pinnedDimensions: [
  {
    name: "EntityORGANIZATION",
    stringVal: "AP"
  }
  ],
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
  forecastParams: {
    forecastHistory: "2592000s",
    holdout: 10.0,
    minDensity: 0.0,
    maxPositiveRelativeChange: 1.0,
    maxNegativeRelativeChange: 1.0,
    forecastExtraWeight: 250.0
  }
}

$ gcurl -X POST -d @query.json https://timeseriesinsights.googleapis.com/v1/projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104:query

{
  "name": "projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104",
  "anomalyDetectionResult": {
    "anomalies": [
      {
        "dimensions": [
          {
            "name": "EntityLOCATION",
            "stringVal": "Seine"
          }
        ],
        "result": {
          "holdoutErrors": {},
          "trainingErrors": {},
          "forecastStats": {
            "numAnomalies": 1
          },
          "testedIntervalActual": 344
        },
        "status": {}
      },
      {
        "dimensions": [
          {
            "name": "EntityLOCATION",
            "stringVal": "Ile de la Cite"
          }
        ],
        "result": {
          "holdoutErrors": {},
          "trainingErrors": {},
          "forecastStats": {
            "numAnomalies": 1
          },
          "testedIntervalActual": 282
        },
        "status": {}
      },
...

聚合

默认汇总方法是统计切片中的事件数量。

我们还可以指定总计数值来汇总事件。可以通过将 ForecastParams.aggregatedDimension 设置为我们求和得出的数值维度来指定。

预测配置

为了在测试时间间隔内计算出预期值,我们采用预测算法,该算法会根据切片的历史值预测预测在测试时间间隔应该达到的值。

注意:切片的值是通过聚合方法提供的。

预测历史记录

我们的时间序列包含多少数据由 ForecastParams.forecastHistory 参数提供。

在我们的初始示例中:

  • 我们设置了 forecastHistory: "2592000s",以便我们确定应在 2019 年 3 月 16 日到 2019 年 4 月 15 日(30 天,2592000 秒)内提取事件,并形成两天之间的时间序列。
  • 时间序列中的每个数据点将覆盖与测试间隔时长相等的时间段(因此,86400s - 1 天)。
  • 时间序列将具有 30 个点,每个点保存该日历日事件的汇总值。假设按照我们的聚合方法来计算时间序列,则第一个时间序列的值将是 2019 年 3 月 16 日的总事件值,也就是 2019 年 3 月 17 日的第二点,依此类推。

您可以查看 ForecastResult.history 字段中返回的时间序列的历史值(仅当请求中的 QueryDataSetRequest.returnTimeseries 设置为 true 时才会返回此值)。

注意:我们正在将 Timeseries proto 中的时间序列作为点的集合(定义为 TimeseriesPoint)按时间排序,并使用值 ( TimeseriesPoint.value) 切片在 [TimeseriesPoint.time, TimeseriesPoint.time + testedInterval.length] 时间间隔内的聚合值。

通过增加 ForecastParams.forecastHistory 并包含更长时间的历史记录,您可以捕获某些季节性模式,并有可能提高预测的准确性,因为它将包含更多数据。例如,如果我们具有每月季节性模式,将 forecastHistory 设置为 30 天,那么系统将无法捕获这些模式,因此在这种情况下,我们应提高 forecastHistory,因此我们分析的时间序列中存在多个每月模式(例如,系统可以检测到每月模式 100-300 天,但实际模式也取决于实际模式)。

在我们的初始示例中,尝试将 testedInterval.length 减少到 3600s(1 小时),并将 ForecastParams.forecastHistory 增加到 8640000s(100 天)。这会将 history 时间序列中的点数增加至 2400,以使其更加精细,并且涵盖的时间段较长。

暂停/测试时间间隔

为了评估预测的质量,我们只针对时间序列的前 X% 进行训练。时间序列的最后一部分用于评估目的,由 ForecastParams.holdout 参数控制。

ForecastParams.holdout 参数表示一个百分比(介于 0 到 100 之间),并告诉了出于测试目的应保留多少数据。在我们的示例中,我们指定了 holdout: 10.0,因此我们保留时间序列的最后 10%,以便根据前 90% 的表现评估预测结果。

考虑到最初示例中有 30 个时间序列点,这意味着前 27 个数据点会保留用于训练,而后 3 个用于模型测试/评估。

我们在预留期间测量的错误会在 ForecastResult.holdoutErrors 中返回,并且这些错误用于计算预测的下限和上限(testedIntervalForecastLowerBoundtestedIntervalForecastUpperBound)。较高的保全错误有助于我们将预测范围扩大,而较低的错误则会导致更紧密的边界(更接近于 ForecastResult.testedIntervalForecast)。

保全值应在低百分比点 (3%-10%) 中,但应确保其包含足够多的数据点,因为预测边界需要这些错误。

密度下限

最小密度由 ForecastParams.minDensity 参数指定,用于指定时间序列必须有多少非空(包含至少一个事件)时间序列才能被视为潜在异常。

与留出值类似,是 0-100 范围内的百分比值。

系统会将点数与 testedInterval.startTime - forecastHistorytestedInterlal.startTime 之间的预期点数进行比较。

例如,如果预计有 30 个时间序列点(如我们的第一个示例将 testedInterval.length 设置为 1 天,将 forecastHistory 设置为 30 天),则设置为 minDensity那么,我们只接受包含至少 24 个点的时间序列,且其中至少要包含一个事件。

注意:在我们的示例中,我们将 minDensity 设置为 0,建议您在检测峰值时也将设置为 0。

地平线

ForecastParams.horizonTime 指定的时间间隔仅指定从经过测试的时间间隔到未来的未来时长,我们应根据历史时间序列预测这些值。

如果 QueryDataSetRequest.returnTimeseries 设置为 true,则每个切片的预测时间序列都将在 ForecastResult.forecast 中返回,并将包含 testedInterval.startTime + testedInterval.lengthtestedInterval.startTime + testedInterval.length + horizonTime 之间的预测值。

灵敏度调节

根据对切片的测试时间间隔和预测时间间隔之间的实际值,我们可能会根据敏感度参数,将它视为异常值。这些参数是:

  • ForecastParameters.maxPositiveRelativeChange 指定与 testedIntervalForecastUpperBound 相比,实际值可以增加多少。
  • ForecastParameters.maxNegativeRelativeChange 指定相对于 testedIntervalForecastLowerBound 的实际值可以减少多少
  • 在比较实际和预测范围时,ForecastParameters.forecastExtraWeight 会用作额外权重。如果您希望忽略较小的绝对方差,则较高的权重十分有用。

注意:我们希望客户端和肯定更改具有不同的参数,因为我们希望客户通过设置较高的上限。

注意:正时间序列(例如,将汇总事件作为聚合方法计算)时,maxNegativeRelativeChange 的最大实际值为 1.0,因为该值不能超过一个 100% 的实际值。

作为基准,请尝试将所有敏感参数设置为 0:

{
  dimensionNames: ["EntityLOCATION"],
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
  forecastParams: {
    forecastHistory: "2592000s",
    holdout: 10.0,
    minDensity: 0.0,
    maxPositiveRelativeChange: 0.0,
    maxNegativeRelativeChange: 0.0,
    forecastExtraWeight: 0.0
  }
}

这是我们可以运行的最敏感查询,它会将 [testedIntervalForecastLowerBound, testedIntervalForecastUpperBound] 边界之外的 testedIntervalActual 的所有切片标记为异常值。虽然这看起来或许我们需要什么(或许在某些应用中可能有用),但在实际实践中,我们喜欢忽略大多数异常,因为它大部分是误报。

运行此基准查询会产生约 2500 个异常:

$ gcurl -X POST -d @query.json https://timeseriesinsights.googleapis.com/v1/projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104:query \
    | grep "testedIntervalActual" | wc -l

2520

如果我们将 maxPositiveRelativeChangemaxNegativeRelativeChange 都增加到 1.0,那么我们会注意到,异常次数会减少至 $1800,这会消除具有较大方差的切片,但仍有大量归类为异常的切片

假设我们只想在这个日关注该新闻领域的最高峰值,我们还可以增加 forecastExtraWeight,这会过滤掉低流量切片。我们将其设置为 200.0,这将得到我们最终的 query.json

{
  dimensionNames: ["EntityLOCATION"],
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
  forecastParams: {
    forecastHistory: "2592000s",
    holdout: 10.0,
    minDensity: 0.0,
    maxPositiveRelativeChange: 1.0,
    maxNegativeRelativeChange: 1.0,
    forecastExtraWeight: 200.0
  }
}

运行上一个查询只会产生 3 个异常,所有这些异常都与巴黎圣母院触发,这些事件是 2019 年 4 月 15 日新闻中提到的提及事件最多:

$ gcurl -X POST -d @query.json https://timeseriesinsights.googleapis.com/v1/projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104:query \
    | grep stringVal

            "stringVal": "Ile de la Cite"
            "stringVal": "Notre Dame"
            "stringVal": "Seine"

查询性能和资源用量

除了降低噪声和过滤掉非有趣切片之外,减少异常检测的灵敏度还可大幅减少查询延迟和资源使用量。

随着 forecastExtraWeight 的增加通常会使异常切片中出现显著的降低,它也应该是应该提高查询性能的主要敏感参数,而应该提高查询性能。

您可以试验各种敏感参数,以了解它们对查询性能有何影响。例如:

$ cat query.json

{
  dimensionNames: ["EntityLOCATION"],
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
  forecastParams: {
    forecastHistory: "2592000s",
    holdout: 10.0,
    minDensity: 0.0,
    maxPositiveRelativeChange: 0.0,
    maxNegativeRelativeChange: 0.0,
    forecastExtraWeight: 0.0
  }
}

$ time gcurl -X POST -d @query.json https://timeseriesinsights.googleapis.com/v1/projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104:query \
    | grep stringVal | wc -l

2580

real 0m26.765s
user 0m0.618s
sys  0m0.132s

我们可以看到,最敏感的参数导致的查询占用了 26.7 美元。降低灵敏度将可加快大约 3.4 秒:

$ cat query.json

{
  dimensionNames: ["EntityLOCATION"],
  testedInterval: {
    startTime: "2019-04-15T00:00:00Z",
    length: "86400s"
  },
  forecastParams: {
    forecastHistory: "2592000s",
    holdout: 10.0,
    minDensity: 0.0,
    maxPositiveRelativeChange: 1.0,
    maxNegativeRelativeChange: 1.0,
    forecastExtraWeight: 200.0
  }
}

$ time gcurl -X POST -d @query.json https://timeseriesinsights.googleapis.com/v1/projects/timeseries-insights-api-demo/datasets/webnlp-201901-202104:query \
    | grep stringVal | wc -l

3

real 0m3.412s
user 0m0.681s
sys  0m0.047s

后续步骤