データ リクエストで欠落している値を埋める

Nick Mihailovski、Google アナリティクス 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 によって返された時系列で高度な解析を開始するのがさらに簡単になります。