日付リクエストで欠落している値を入力する

コレクションでコンテンツを整理 必要に応じて、コンテンツの保存と分類を行います。

Nick Mihailovski、Google Analytics API チーム – 2009 年 10 月

この記事では、Google アナリティクスの Data Export API から返されたデータで欠落している時系列値を検出し、埋め戻す方法について説明します。


始める前に

この記事では、Google アナリティクス Data Export API の機能について知識があることを前提とします。サンプルコードは Java ですが、さまざまな言語でこの概念を利用できます。この記事のコードはオープンソースとして提供されており、プロジェクト ホスティングからダウンロードすることができます。

この記事では次のことを学びます。

  • Google アナリティクス Data Export API で日付ディメンションを処理する方法
  • 結果をグループ分けして欠落した日付を検出するクエリの作成方法
  • Java を使用して欠落した値を埋める方法

はじめに

一定期間のデータを比較することで、状況を把握できます。たとえば、あるウェブサイトが 100 万ドルの収益を上げたという場合は、それほど意味がありません。しかし、前四半期比や前年比でウェブサイトの収益が 10 倍に増えていたとしたら、それはすばらしい成果です。Google Analytics API では、ga:datega:dayga:month ディメンションを使って、長期にわたってデータを簡単にプロットできます。

クエリで日付ディメンションのみを使用する場合、日付範囲内に収集データがゼロの日があると、Google アナリティクス API は日付と指標の 0 値を埋め戻します。

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

しかし、他のディメンションとともに日付をクエリすると、注意が必要です。いずれかの日付にデータがない場合、API はその日付のエントリを返しません。その日付をとばして、データを含む次の使用可能な日付に進んでしまいます。

ga:keywordga:datega:sessions
椅子2010-03-0155
椅子2010-03-0348

アナリストがデータを分析する際、最初の例のように特定のキーワードの欠落日も入力されている方が便利です。

この記事では、実践的にデータを埋め戻す方法についての実用的なヒントをいくつか紹介します。

背景

まずこの問題が起きた原因について考えてみます。ここでは、2 つの原因が考えられます。

  1. Google アナリティクスは収集されたデータのみを処理します。特定の日にサイト訪問者がいなかった場合、処理するデータがないためデータは返されません。
  2. データがない日付に使用する追加のディメンション数と使用する値を決定するのは非常に困難です。

このため Google アナリティクス API では、すべてを規制する 1 つのプロセスを定義するのではなく、複数のディメンションを持つクエリのデータを埋める作業を開発者の方にお任せしています。よろしくお願いいたします。

プログラムの概要

上記の表にデータを埋め戻すステップは次のとおりです。

  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 オブジェクトのリストが含まれます。各エントリ オブジェクトはデータ行を表し、ディメンションや指標の名前と値を含んでいます。sort パラメータを使用していないので、任意の順序で結果が返されます。

ga:keywordga:datega:entrances
椅子2010-03-0414
椅子2010-03-0123
テーブル2010-03-0418
テーブル2010-03-0224
椅子2010-03-0313

まず、欠落している日付を特定しやすくするため、最初にすべてのディメンションをグループ分けします。それには、クエリの sort パラメータを元のクエリで使用されているディメンションに設定します。

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

sort パラメータを追加すると、API は必要な順序で結果を返します。

ga:keywordga:datega:entrances
椅子2010-03-0123
椅子2010-03-0313
椅子2010-03-0414
テーブル2010-03-0224
テーブル2010-03-0418

次のステップではあらゆるディメンションですべての日付が昇順に返されるようにします。Google Analytics API には多数の日付ディメンションが用意されていますが、日付の境界(日、月、年など)を正確に並べ替えられる ga:date だけです。日付をバックフィルする場合は、クエリでディメンションと並べ替えクエリ パラメータの両方で ga:date ディメンションを使用してください。

並べ替えたクエリを実行すると、同じリンク先ページはすべて並んで日付順に返されます。1 つのリンク先ページの日付リストは時系列とみなすことができ、順番に並んでいるため欠落している日付を簡単に見つけることができます。

予定日の決定

欠落した日付を検出するには、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 オブジェクトを使用して、両方の日付の間の時間を確認します。カウントに開始日を含めるには、日付の差に 1 日を追加します。

  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 で返されていない欠落データを埋める必要があります。

このプログラムは、dimensionValue 変数と tmpDimensionValue 変数を使用して各シリーズの始まりを検出します。

レスポンスを処理するコード全体を次に示します。 欠落データの埋め込みについては以下で説明します。

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

欠落している日付の埋め戻し

一連のエントリごとに、row という ArrayList に指標値(入口)が保存されます。新しい時系列が検出されると新しい行が作成され、予定日が予定開始日に設定されます。

次にエントリごとに、エントリの日付値が予定日と等しいかどうかが確認されます。等しい場合、エントリの指標が行に追加されます。 等しくない場合は欠落した日付が検出されたことになるため、これを埋め戻す必要があります。

backfillRow メソッドは、データのバックフィルを処理します。現在の日付だけでなく、予想される日付や見つかった日付をパラメータとして受け取ります。 次に、2 つの日付の間の日数(その日を含まない値)を判断し、その数だけ 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 アナリティクス API によって返された時系列で高度な解析を開始するのがさらに簡単になります。