Filling In Missing Values From Date Requests

Nick Mihailovski, equipe da API Google Analytics – outubro de 2009

Este artigo discute como detectar e preencher valores de série temporal ausentes nos dados retornados da API de exportação de dados do Google Analytics.


Antes de começar

O artigo pressupõe que você conhece o funcionamento da API de exportação de dados do Google Analytics. O código de exemplo está em Java, mas você pode usar os conceitos na linguagem de sua escolha. O código deste artigo é fornecido como código aberto e pode ser obtido por download a partir da hospedagem do projeto.

Após ler este artigo, você terá aprendido:

  • Como a API de exportação de dados do Google Analytics trata as dimensões de data.
  • Como estruturar suas consultas para agrupar resultados e detectar datas ausentes.
  • Como preencher os valores ausentes usando Java.

Introdução

A comparação de dados ao longo de um período fornece contexto. Por exemplo, constatar que um website gerou uma receita de US$ 1 milhões não significa muita coisa. Porém, constatar que um website aumentou sua receita em 10 vezes de um trimestre a outro ou de um ano a outro é realmente impressionante. Com a API Google Analytics, é fácil exibir dados ao longo do tempo usando as dimensões ga:date, ga:day e ga:month.

Se sua consulta usar somente uma dimensão de dados e se em algum dia do período não forem coletados dados, a Google Analytics API preencherá as datas e os valores 0 para as métricas.

ga:datega:sessions
2010-03-01101
2010-03-020
2010-03-0369

No entanto, pode ser complicado se você consultar uma data juntamente com outras dimensões. Se uma das datas não tiver dados, a API NÃO retornará uma entrada para essa data. Ela passará para a próxima data disponível que contenha dados.

ga:keywordga:datega:sessions
cadeira2010-03-0155
cadeira2010-03-0348

Idealmente, os analistas gostariam que as datas ausentes de uma determinada palavra-chave fossem preenchidas como no exemplo acima.

Este artigo descreve algumas das práticas recomendadas para preencher os dados de maneira pragmática.

Contexto

Primeiro, vamos analisar a causa do problema. Há duas razões.

  1. O Google Analytics processa somente os dados coletados. Se ninguém visitar o site em um determinado dia, não haverá dados a serem processados, logo, nenhum dado será retornado.
  2. É muito difícil determinar quantas dimensões adicionais e quais valores devem ser usados para datas que não possuem dados.

Assim, em vez de tentar definir um processo para regularizar tudo, a Google Analytics API deixa para o desenvolvedor o trabalho de preenchimento dos dados das consultas que têm várias dimensões. Sorte sua :)

Informações gerais do programa

Siga estas etapas para preencher os dados no gráfico acima.

  1. Modifique a consulta para garantir que as dimensões sejam classificadas de maneira adequada.
  2. Determine as datas esperadas do período.
  3. Repita e preencha os dados ausentes.
  4. Preencha qualquer valor ausente restante.

Modificação da consulta

Para preencher dados, é necessário verificar se os dados retornados pela API estão em um formato que facilita a detecção de datas ausentes. Veja um exemplo de consulta para recuperar ga:keyword e ga:date nos primeiros cinco dias de março:

DataQuery dataQuery = new DataQuery(new URL(BASE_URL));
dataQuery.setIds(TABLE_ID);
dataQuery.setStartDate("2010-03-01");
dataQuery.setEndDate("2010-03-05");
dataQuery.setDimensions("ga:keyword,ga:date");
dataQuery.setMetrics("ga:entrances");

Depois que a consulta é enviada para a API, os resultados contêm uma lista de objetos DataEntry. Cada objeto de entrada representa uma linha de dados e inclui nomes e valores de dimensões/métricas. Como nenhum parâmetro de classificação foi usado, os resultados são retornados em ordem arbitrária.

ga:keywordga:datega:entrances
cadeira2010-03-0414
cadeira2010-03-0123
table2010-03-0418
table2010-03-0224
cadeira2010-03-0313

Para identificar de maneira fácil as datas ausentes, primeiro precisamos agrupar todas as dimensões. Isso pode ser feito configurando o parâmetro de classificação da consulta como as dimensões usadas na consulta original.

dataQuery.setSort("ga:keyword,ga:date");

A adição do parâmetro de classificação faz com que a API retorne os resultados na ordem desejada.

ga:keywordga:datega:entrances
cadeira2010-03-0123
cadeira2010-03-0313
cadeira2010-03-0414
table2010-03-0224
table2010-03-0418

A segunda etapa consiste em verificar se todas as datas são retornadas em ordem crescente para cada dimensão. Embora a API Google Analytics forneça várias dimensões de data, somente ga:date pode ser classificado com precisão ao longo dos limites da data (ou seja, dias, meses, anos). Portanto, se você quiser preencher datas, verifique se a consulta usa a dimensão ga:date nas dimensões e nos parâmetros de classificação da consulta.

Quando a consulta classificada for executada, todas as páginas de destino semelhantes serão retornadas próximas umas das outras, e as datas estarão em ordem sequencial. A lista das datas para uma única página de destino pode ser definida como uma série temporal e, como as datas ficam em ordem, é mais fácil identificar as datas ausentes.

Determinação das datas esperadas

Para determinar as datas ausentes, é necessário comparar as datas atuais retornadas pela API com as datas esperadas em cada série temporal. Podemos descobrir o que é esperado por meio de:

  1. Determinação das datas de início esperadas a partir da consulta de API.
  2. Contagem do número de dias esperados no período da consulta.

Os dois valores podem ser usados juntos para determinar cada data esperada aumentando a data de início em 1 unidade para cada dia no período.

Determinação da data de início esperada

Podemos usar o parâmetro de consulta start-date como a data de início esperada da série. Como o formato de data retornado na resposta da API yyyyMMdd é diferente do formato do parâmetro de consulta yyyy-MM-dd, é necessário primeiro converter o formato de data antes de usá-lo.

O método setExpectedStartDate converte os formatos das datas.

  private static SimpleDateFormat queryDateFormat = new SimpleDateFormat("yyyy-MM-dd");
  private static SimpleDateFormat resultDateFormat = new SimpleDateFormat("yyyyMMdd");

  public void setExpectedStartDate(String startDate) {
    try {
      calendar.setTime(queryDateFormat.parse(startDate));
      expectedStartDate = resultDateFormat.format(calendar.getTime());
    } catch (ParseException e) {
      handleException(e);
    }
  }

Contagem do número de dias esperados

Para conferir o número de dias no período, o programa analisa as datas de início e término em objetos Date do Java. Em seguida, usa um objeto Calendar para descobrir o tempo entre as duas datas. Um dia é adicionado à diferença entre as datas para que a contagem seja inclusiva.

  private static final long millisInDay = 24 * 60 * 60 * 1000;

  public void setNumberOfDays(DataQuery dataQuery) {
    long startDay = 0;
    long endDay = 0;

    try {
      calendar.setTime(queryDateFormat.parse(dataQuery.getStartDate()));
      startDay = calendar.getTimeInMillis() / millisInDay;

      calendar.setTime(queryDateFormat.parse(dataQuery.getEndDate()));
      endDay = calendar.getTimeInMillis() / millisInDay;
    } catch (ParseException e) {
      handleException(e);
    }

    numberOfDays = (int) (endDay - startDay + 1);
  }

Agora temos todos os dados necessários para descobrir quais datas estão ausentes.

Identificação de cada série temporal nos resultados

Quando a consulta é executada, o programa passa por cada objeto DataEntry na resposta da API. Como a consulta foi inicialmente classificada, a resposta terá uma série temporal parcial para cada palavra-chave. Assim, é necessário encontrar o início de cada série temporal e, em seguida, percorrer cada data e preencher os dados ausentes que a API não retornou.

Esse programa usa as variáveis dimensionValue e tmpDimensionValue para detectar o início de cada série.

Veja a seguir um código inteiro para lidar com a resposta. A maneira de preencher os dados ausentes é discutida abaixo.

public void printBackfilledResults(DataFeed dataFeed) {
  String expectedDate = "";
  String dimensionValue = "";
  List<Integer> row = null;

  for (DataEntry entry : dataFeed.getEntries()) {
    String tmpDimValue = entry.getDimensions().get(0).getValue();

    // Detect beginning of a series.
    if (!tmpDimValue.equals(dimensionValue)) {
      if (row != null) {
        forwardFillRow(row);
        printRow(dimensionValue, row);
      }

      // Create a new row.
      row = new ArrayList<Integer>(numberOfDays);
      dimensionValue = tmpDimValue;
      expectedDate = expectedStartDate;
    }

    // Backfill row.
    String foundDate = entry.getDimension("ga:date").getValue();
    if (!foundDate.equals(expectedDate)) {
      backFillRow(expectedDate, foundDate, row);
    }

    // Handle the data.
    Metric metric = entry.getMetrics().get(0);
    row.add(new Integer(metric.getValue()));
    expectedDate = getNextDate(foundDate);
  }

  // Handle the last row.
  if (row != null) {
    forwardFillRow(row);
    printRow(dimensionValue, row);
  }
}

Preenchimento de qualquer data ausente

Para cada entrada de uma série, o programa armazena os valores de métrica (entradas) em um ArrayList chamado row. Quando uma nova série temporal é detectada, uma nova linha é criada, e a data esperada é definida como a data de início esperada.

Em seguida, para cada entrada, o programa verifica se o valor da data na entrada é equivalente à data esperada. Se forem equivalentes, a métrica na entrada será adicionada à linha. Do contrário, o programa terá detectado datas ausentes que precisam ser preenchidas.

O método backfillRow processa o preenchimento de dados. Ele aceita como parâmetros as datas encontradas e esperadas, assim como a linha atual. Em seguida ele determina o número de dias entre as duas datas (não inclusivos) e adiciona esse número de zeros à linha.

  public void backFillRow(String startDate, String endDate, List<Integer> row) {
    long d1 = 0;
    long d2 = 0;

    try {
      calendar.setTime(resultDateFormat.parse(startDate));
      d1 = calendar.getTimeInMillis() / millisInDay;

      calendar.setTime(resultDateFormat.parse(endDate));
      d2 = calendar.getTimeInMillis() / millisInDay;

    } catch (ParseException e) {
      handleException(e);
    }

    long differenceInDays = d2 - d1;
    if (differenceInDays > 0) {
      for (int i = 0; i < differenceInDays; i++) {
        row.add(0);
      }
    }
  }

Quando o método termina, a linha terá sido preenchida com dados, e os dados atuais poderão ser adicionados. A data esperada é incrementada para um dia após a data encontrada usando o método getNextDate.

public String getNextDate(String initialDate) {
  try {
    calendar.setTime(resultDateFormat.parse(initialDate));
    calendar.add(Calendar.DATE, 1);
    return resultDateFormat.format(calendar.getTime());

  } catch (ParseException e) {
    handleException(e);
  }
  return "";
}

Preenchimento de qualquer valor restante

Depois que os dados da série forem processados em um row, será necessário verificar se não há mais datas ausentes no final da série.

O método forwardFillRow simplesmente calcula a diferença entre o número de dias na consulta original e o tamanho atual da linha e adiciona esses zeros ao final da linha.

public void forwardFillRow(List<Integer> row) {
  int remainingElements = numberOfDays - row.size();
  if (remainingElements > 0) {
    for (int i = 0; i < remainingElements; i++) {
      row.add(0);
    }
  }
}

Nesse momento, o programa terá preenchido qualquer valor ausente na série temporal. Agora que temos todos os dados, o programa salva os valores de dimensão e métrica como uma lista separada por vírgulas.

Conclusão

Usando essa amostra, você pode preencher facilmente dados em datas não retornadas pela API. Como mencionado acima, essa solução pode ser adaptada a qualquer linguagem de programação. Os desenvolvedores podem até mesmo adaptar essas técnicas e aplicá-las para lidar com várias dimensões e métricas. Agora ficou mais fácil do que nunca realizar análises avançadas em séries temporais retornadas pela Google Analytics API.