Guia de criação de consultas

Visão geral

Nesta seção, você verá como configurar corretamente as consultas de detecção de anomalias. Em um nível alto, há quatro tipos de parâmetros que você precisa ajustar:

  • Parâmetros de seleção de dados que especificam quais dados estão incluídos na nossa análise. Por exemplo: QueryDataSetRequest.dimensionNames, QueryDataSetRequest.pinnedDimensions e QueryDataSetRequest.testedInterval;
  • Parâmetros de agregação que especificam como vários eventos são agrupados para produzir um valor usado para comparar frações. Por exemplo: ForecastParams.aggregatedDimension.
  • Parâmetros de previsão que são usados para configurar os algoritmos de previsão usados para calcular os valores esperados. Por exemplo: ForecastParams.holdout, ForecastParams.forecastHistory e ForecastParams.minDensity;
  • Parâmetros de ajuste da sensibilidade que, quando alterados, podem aumentar ou diminuir o número de anomalias informadas, a latência e os recursos computacionais. Por exemplo: ForecastParams.maxPositiveRelativeChange, ForecastParams.maxNegativeRelativeChange e ForecastParams.forecastExtraWeight;

Pré-requisitos

Siga as instruções de configuração no nosso Guia de início rápido para garantir que seja possível executar todos os comandos deste guia.

Mostraremos como a variação de cada parâmetro afeta os resultados retornados consultando o conjunto de dados público de demonstração que já pré-carregadomos com dados do projeto GDELT (em inglês).

Salve a seguinte consulta como query.json no seu diretório de trabalho:

{
  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
  }
}

Conforme mencionado no guia de início rápido, é possível emitir uma consulta com o seguinte comando:

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

Neste guia, alteraremos parâmetros diferentes em query.json para mostrar como isso afeta a saída. Para editar, copie sua cópia e emita as consultas novamente usando o comando gcurl acima, que é identificado como auxiliar no Guia de início rápido.

Seleção de dados

Uma consulta de detecção de anomalias avalia se há anomalias em um determinado conjunto de dados, dentro de um determinado intervalo de tempo, dividindo os dados em algumas dimensões e, opcionalmente, filtrando alguns valores de algumas dimensões. Nesta seção, detalhamos como controlar todas essas etapas de seleção de dados.

Especificação do intervalo de tempo

Para especificar qual intervalo de tempo queremos verificar anomalias, defina QueryDataSetRequest.tested_interval. Em query.json, especificamos que queremos detectar anomalias que ocorreram durante o dia 15 de abril de 2019:

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

Se, por exemplo, quiser detectar anomalias que aconteceram no dia 15 de abril de 2019 entre 13h e 14h, definiremos testedInterval como:

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

DICA: brinque com o valor testedInterval definindo horários de início diferentes e durações diferentes e veja como as anomalias retornadas variam.

Sucos

Ao detectar anomalias dentro de um determinado intervalo de tempo, também é necessário especificar como os eventos são agrupados em frações de dados. Isso é feito ao definir dimensionNames como o conjunto de dimensões que queremos dividir no conjunto de dados.

Em query.json, estamos fragmentando apenas os dados pela dimensão "EntityLOCATION": dimensionNames: ["EntityLOCATION"]. Na lista de resultados, todas as frações são simplesmente valores diferentes para a dimensão "EntityLocation".

Se, em vez disso, você quiser separar os dados em várias dimensões, especificaremos vários nomes de dimensões:

{
  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
  }
}

Ao executar essa consulta, observe que as frações resultantes assumem valores entre as duas dimensões especificadas, "EntityLOCATION" e "EntityORGANIZATION" (mostramos apenas a primeira fração de anomalias sendo retornada):

$ 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": {}
      },
...

Filtro

Se estiver interessado em analisar apenas um subconjunto do conjunto de dados, poderemos filtrar apenas os eventos que têm determinados valores em algumas dimensões definindo pinnedDimensions.

Exemplo de filtragem por EntityORGANIZATION e ingestão de EntityLOCATION (mostrando apenas o primeiro resultado):

$ 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": {}
      },
...

Agregação

O método de agregação padrão é a contagem do número de eventos na fração.

Também é possível especificar eventos para somar uma dimensão numérica. Ela é especificada configurando o ForecastParams.aggregatedDimension para a dimensão numérica que queremos somar.

Configuração da previsão

Para calcular o valor esperado durante o intervalo de tempo testado, usamos algoritmos de previsão, que, com base nos valores históricos da fração, preveja qual será o valor durante o intervalo testado.

OBSERVA O: os valores de uma fatia são fornecidos pelo método de agregação.

Histórico de previsões

A quantidade de dados que nossa série temporal contém é fornecida pelo parâmetro ForecastParams.forecastHistory.

Em nosso exemplo inicial:

  • Definimos forecastHistory: "2592000s" para que seja necessário buscar eventos entre 16 de março de 2019 e 15 de abril de 2019 (30 dias, que é 2592.000 segundos) e formar uma série temporal entre esses dois dias.
  • Cada ponto de dados na série temporal cobrirá um período igual ao comprimento do intervalo testado (por isso, de 86400s a 1 dia no nosso exemplo).
  • A série temporal terá 30 pontos, cada um contendo o valor agregado para os eventos do dia. Supondo que a contagem seja nosso método de agregação, o primeiro ponto da série temporal terá o valor do número total de eventos em 16 de março de 2019, o segundo ponto a partir de 17 de março de 2019.

Veja os valores históricos de séries temporais retornadas no campo ForecastResult.history. Ele só será retornado se QueryDataSetRequest.returnTimeseries estiver definido como true na solicitação.

OBSERVA O: estamos modelando uma série temporal noTimeseries proto como uma coleção de pontos (definidos comoTimeseriesPoint ), classificadas por tempo e com valor (TimeseriesPoint.value ) o valor agregado da fração durante o [TimeseriesPoint.time, TimeseriesPoint.time + testedInterval.length ].

Ao aumentar o ForecastParams.forecastHistory e incluir um histórico mais longo, você pode capturar determinados padrões de periodicidade e, possivelmente, aumentar a precisão da previsão, porque ela conterá mais dados. Por exemplo, se tivermos padrões de periodicidade mensal, definir forecastHistory como 30 dias não permitirá a captura desses padrões. Portanto, nesse cenário, aumentaremos o forecastHistory, então temos vários padrões mensais na nossa série temporal analisada (por exemplo, para que os padrões mensais sejam detectados de 100 a 300 dias devem ser suficientes, mas também depende dos padrões reais).

No nosso exemplo inicial, tente reduzir testedInterval.length para 3600s (1 hora) e aumentar ForecastParams.forecastHistory para 8640000s (100 dias). Isso aumentará o número de pontos na série temporal history para 2.400, tornando-o mais granular e cobrindo um período mais longo.

Intervalo de validação/teste

Para avaliar a qualidade da nossa previsão, treinamos somente os primeiros X% da série temporal. A última parte da série temporal é mantida para fins de avaliação e é controlada pelo parâmetro ForecastParams.holdout.

O parâmetro ForecastParams.holdout representa uma porcentagem (no intervalo de 0 a 100) e informa a quantidade de dados que precisamos manter para fins de teste. Em nosso exemplo, especificamos holdout: 10.0, por isso mantemos os últimos 10% das séries temporais para avaliar o desempenho da previsão com base nos primeiros 90%.

Considerando que temos 30 pontos de série temporal no nosso exemplo inicial, isso significa que os primeiros 27 pontos são mantidos para treinamento e os três últimos para teste/avaliação do modelo.

Os erros medidos durante o período de espera são retornados em ForecastResult.holdoutErrors, e esses erros são usados para calcular os limites inferior e superior da previsão (testedIntervalForecastLowerBound e testedIntervalForecastUpperBound). Erros de retenção mais altos nos proporcionam um limite de previsão mais amplo, enquanto erros mais baixos nos proporcionam limites mais rigorosos (e próximos a ForecastResult.testedIntervalForecast).

O valor de retenção deve estar em pontos de porcentagem baixos (3% a 10%), mas garante que ele contenha pontos de dados suficientes, pois esses erros são necessários para calcular o limite de previsão.

Densidade mínima

A densidade mínima, fornecida pelo parâmetro ForecastParams.minDensity, especifica quantos pontos de série temporal não vazios (contendo pelo menos um evento) que uma série temporal precisa conter para serem considerados como uma possível anomalia.

Semelhante à parada, é um valor percentual no intervalo de 0 a 100.

O número de pontos é comparado com o número de pontos esperados entre testedInterval.startTime - forecastHistory e testedInterlal.startTime.

Por exemplo, se tivermos 30 pontos de série temporal esperados (como no nosso primeiro exemplo que define testedInterval.length como 1 dia e forecastHistory como 30 dias) e definir minDensity 80, aceitaremos somente séries temporais com 24 pontos com pelo menos um evento.

OBSERVA O: no exemplo que definimosminDensity como 0 e sugerimos que, ao detectar picos, ele também seja definido como 0.

Horizon

O horizonte da hora, especificado por ForecastParams.horizonTime simplesmente especifica o quanto no futuro, com base no intervalo testado, precisa prever os valores com base na série temporal histórica.

Se QueryDataSetRequest.returnTimeseries for definido como verdadeiro, a série temporal prevista será retornada em ForecastResult.forecast para cada fração e conterá os valores previstos entre testedInterval.startTime + testedInterval.length e testedInterval.startTime + testedInterval.length + horizonTime.

Ajuste de sensibilidade

Com base nos limites previstos e no valor real durante o intervalo testado de uma fração, podemos classificá-lo ou não como uma anomalia com base nos parâmetros de sensibilidade. Esses parâmetros são:

  • ForecastParameters.maxPositiveRelativeChange especifica o valor real pode aumentar em comparação com o testedIntervalForecastUpperBound.
  • ForecastParameters.maxNegativeRelativeChange especifica quanto o valor real pode diminuir em comparação com o testedIntervalForecastLowerBound.
  • ForecastParameters.forecastExtraWeight é usado como um peso extra ao comparar o limite real e os limites de previsão. Ter um peso extra maior é útil se você estiver interessado em ignorar variações absolutas menores.

OBSERVA O: temos parâmetros diferentes para alterações positivas e negativas, porque queremos permitir que nossos clientes tenham a flexibilidade de diferentes limites para picos e quedas ou desative um ou outro definindo limites realmente altos.

OBSERVA O: o valor prático máximo paramaxNegativeRelativeChange é 1.0 para séries temporais positivas (por exemplo, ao contar o número de eventos como o método de agregação) como um valor não pode cair mais de 100% do valor real.

Como referência, defina todos esses parâmetros de sensibilidade como 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
  }
}

Essa é a consulta mais confidencial que podemos executar e ela marcará como anomalias em todas as frações que têm os testedIntervalActual fora dos limites [testedIntervalForecastLowerBound, testedIntervalForecastUpperBound]. Embora isso possa parecer o que queremos (e pode ser útil em alguns aplicativos), na prática, estaremos interessados em ignorar a maioria dessas anomalias, porque isso resultará em falsos positivos.

A execução desta consulta de referência resultará em cerca de 2.500 anomalias:

$ 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

Se aumentarmos maxPositiveRelativeChange e maxNegativeRelativeChange para 1.0, notaremos que o número de anomalias é reduzido em ~ 1.800, que gera uma queda de fatias com variações menores. Porém, ainda temos que um alto número de fatias classificadas como anomalias.

Supondo que só estejamos interessados nos maiores picos das notícias durante esse dia, também podemos aumentar o forecastExtraWeight, que filtrará as frações de baixo volume. Vamos definir para 200.0, o que nos dará o query.json final:

{
  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
  }
}

A execução da consulta anterior gera apenas três anomalias, todas associadas ao incêndio de Notre Dame, que foi o evento mais mencionado em notícias em 15 de abril de 2019:

$ 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"

Desempenho da consulta e uso de recursos

Além de reduzir o ruído e filtrar frações não interessantes, reduzir a sensibilidade da detecção de anomalias também pode reduzir significativamente a latência da consulta e o uso de recursos.

Como aumentar o forecastExtraWeight geralmente resulta na redução mais perceptível em frações anômalas, também é o parâmetro de sensibilidade principal que precisa ser aumentado com o objetivo de melhorar o desempenho da consulta.

Você pode testar diferentes parâmetros de sensibilidade para ver como eles afetam o desempenho da consulta. Exemplo:

$ 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

Os parâmetros mais confidenciais levam à nossa consulta levando cerca de 26,7 segundos. Reduzir a sensibilidade oferece um aumento de velocidade de até 3,4 s:

$ 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

A seguir