通过日期请求填充缺少的值

Nick Mihailovski,Google Analytics(分析)API 小组 - 2009 年 10 月

本文讨论了如何检测和回填 Google Analytics(分析)Data Export API 返回的数据中缺少的时间序列值。


准备工作

本文假定您了解 Google Analytics(分析)Data Export API 的工作原理。示例代码使用 Java 编写,但您可以采用自己选择的语言使用这些概念。本文中的代码作为开放源代码提供,可以通过 Project Hosting 下载

阅读完本文后,您将了解以下内容:

  • Google Analytics(分析)Data Export API 如何处理日期维度。
  • 如何构建查询以便对结果分组并检测缺少的日期。
  • 如何使用 Java 填充缺少的值。

简介

跨时段比较数据可提供上下文。例如,声明一个网站收入 100 万美元没有多大意义。但声明一个网站的季度或年度收入增长 10 倍则意义重大。借助 Google Analytics(分析)API,您可以轻松使用 ga:datega:dayga:month 维度绘制随时间变化的数据图表。

如果您的查询仅使用日期维度,并且该日期范围内的某些天未收集任何数据,那么 Google Analytics(分析)API 将回填日期和 0 值以便获得相关指标。

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

不过,如果您查询日期以及其他维度,则情况会变得非常复杂。如果某个日期没有任何数据,则 API 不会为该日期返回条目,而只会跳转到包含数据的下一可用日期。

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

理想情况下,分析人员希望像上述第一个示例那样填充特定关键字缺少的日期

本文介绍了以编程方式回填数据的一些最佳做法。

背景

我们首先看一下为什么会产生此问题。原因有两个。

  1. Google Analytics(分析)仅处理收集的数据。如果在某个特定日期无人访问网站,则没有要处理的数据,因此不会返回任何数据。
  2. 确定有多少其他维度以及对没有数据的日期使用什么值非常困难。

因此,Google Analytics(分析)API 并不尝试定义一个过程来规范一切,而是将为具有多个维度的查询填充数据的任务留给开发者完成。您真幸运 :)

程序概览

下面是回填上图数据的一些步骤。

  1. 修改查询以确保随机对维度排序。
  2. 根据日期范围确定预计日期。
  3. 迭代和回填任何缺少的日期。
  4. 填充任何其他缺少的值。

修改查询

如需回填日期,我们需要确保从 API 返回的数据的格式能够在缺少日期时轻松检测。以下示例查询同时检索 3 月份前 5 天的 ga:keywordga:date

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");

查询发送到 API 后,结果将包含 DataEntry 对象的列表。每个条目对象代表一行数据,并包括维度/指标的名称和值。由于没有使用排序参数,因此返回的结果是随机排序的。

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

为了便于确定缺少了哪些日期,我们首先需要组合所有维度。这可以通过将查询的排序参数设置为原始查询中使用的维度来完成。

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

添加排序参数可让 API 按期望的顺序返回结果。

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

第二步是确保按升序返回每个维度的所有日期。尽管 Google Analytics(分析)API 提供了许多日期维度,但只有 ga:date 可以跨日期边界(即日、月、年)准确地排序。因此,如果您要回填日期,请确保您的查询在维度和排序查询参数中都使用了 ga:date 维度。

在执行排序的查询之后,所有相同的着陆页都将相互紧挨着返回,并且日期按先后顺序显示。可以将单一着陆页的日期列表视为一个时间序列,因为它们是按顺序排列的,因此可以很容易找到缺少的日期。

确定预计日期

要检测缺少的日期,我们需要比较 API 返回的实际日期和每个时间序列中的预计日期。我们可以通过以下方式找出预计日期:

  1. 通过 API 查询确定预计开始日期。
  2. 计算查询日期范围中的预计天数。

可以结合使用这两个值确定每个预计日期,即用开始日期依次加 1 得到日期范围中每一天。

确定预计开始日期

我们可以将 start-date 查询参数用作系列的预计开始日期。由于 API 响应 yyyyMMdd 中返回的日期格式与查询参数 yyyy-MM-dd 的格式不同,因此我们需要先转换日期格式,然后才能使用它。

setExpectedStartDate 方法可转换日期格式。

  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);
    }
  }

计算预计天数

为了获取日期范围内的天数,该程序会将开始日期和结束日期解析为 Java Date 对象。然后,使用 Calendar 对象找出两个日期之间的时间。在两个日期之间的差上加一天,以便将两头的日期包括在内。

  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);
  }

现在,我们已经得到推算缺少的日期所需的全部数据。

确定结果中的每个时间序列

在执行查询后,该程序会仔细检查 API 响应中的每个 DataEntry 对象。由于该查询最初经过排序,因此该响应对每个关键字都有一个时间序列部分。因此,我们需要找到每个时间序列的开始日期,然后仔细检查每个日期并填充 API 没有返回的缺失数据。

此程序使用 dimensionValuetmpDimensionValue 变量来检测每个系列的开头。

以下是处理响应的完整代码。 填充缺少的数据将在下面讨论。

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);
  }
}

回填任何缺少的日期

对于系列中的每个条目,该程序会将指标值(进入次数)存储在名为 rowArrayList 中。在检测到新的时间序列时,该程序将创建一个新行,并将预计日期设置为预计开始日期。

然后,该程序会检查每个条目中的日期值是否等于预计日期。如果相等,则会向该行添加条目中的指标。如果不相等,该程序就是检测到了需要回填的缺少日期。

backfillRow 方法可处理回填数据。它接受预计日期和发现的日期以及当前行作为参数。 然后确定这两个日期(不含这两个日期)之间的天数,并向该行添加相应数量的 0。

  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);
      }
    }
  }

在完成此方法之后,该行已经回填数据并可以添加当前数据。然后,预期日期会使用 getNextDate 方法加上发现的日期之后的 1 天。

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 "";
}

填充任何其他值

系列数据被处理到 row 后,我们必须检查序列的末尾是否不再缺少日期。

forwardFillRow 方法仅计算原始查询中的天数与行当前大小之间的差值,并在行末尾添加这么多的 0。

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

此时,该程序已经填充了时间序列中任何缺少的值。现在,我们已经获得了所有数据,该程序会将维度和指标值输出为以英文逗号分隔的列表。

总结

使用此示例,您可以轻松回填 API 未返回的日期上的数据。如上所述,此解决方案适用于任何编程语言。开发者甚至可以采用这些技术,并将其用于处理多个维度和多个指标。现在,您能够比以往更方便地着手对 Google Analytics(分析)API 返回的时间序列执行高级分析。