【Universal Analytics (UA)→GA4】ディメンジョンの互換性エラーをどうにか回避する【Python API】

# やりたいこと
Google Analytics (Universal Analytics: UA) が2023年7月から使えなくなるので、Google Analytics 4 (GA4) への移行を余儀なくされています。
UA時代からAPIを使ったデータ取得を行っていた都合で、こちらもGA4への移行が必要になりました。
APIではディメンジョンとメトリクスの指定をしますが、GA4では廃止されたものや何故か従来どおりに使えないものもありました。
本記事は、ひとまずエラーを起こさない範囲での対策を備忘録的に記載しています。

# GA4 Python APIでデータを取得する
本題に入る前に、APIを使うまでの参考ページを紹介しておきます。

【Google から出ているクイックスタートガイド】
クライアント用のJSONファイルのダウンロードから必要なライブラリのインストール、サンプルコードまでを紹介しています。
まずはこちらを実施して、ひとまずデータが取得できるところまで確認してみてください。

https://developers.google.com/analytics/devguides/reporting/data/v1/quickstart-client-libraries

上記のガイドをより噛み砕いて解説している記事もありましたので、こちらもどうぞ。（大変参考になりました）

https://www.lifewithpython.com/2023/01/python-migrate-to-google-analytics-data-api-v1.html


## UAからGA4へのディメンジョン・メトリクスの対応
Google公式から「UAからGA4へのディメンジョン・メトリクスの対応」のドキュメントが出ています。こちらを参考にすれば移行はできる！・・・と思っていた時期が私にもありました。

https://developers.google.com/analytics/devguides/migration/api/reporting-ua-to-ga4-dims-mets

私の場合、合計13個のディメンジョン・メトリクスを使っていました。対応表を参考にどういった変換になるのかを記載しておきます。
※ [D]：ディメンジョン、[M]：メトリクス
1. [D] date -> date
1. [D] dateHourMinute -> dateHourMinute
1. [D] pagePath -> pagePathPlusQueryString
1. [D] source -> sessionSource
1. [D] deviceCategory -> deviceCategory
1. [D] operatingSystem -> operatingSystem
1. [D] pageTitle -> pageTitle
1. [D] pagePathLevel1 -> 無し
1. [M] users -> totalUsers
1. [M] sessions -> sessions
1. [M] pageviews -> screenPageViews
1. [M] timeOnPage -> userEngagementDuration
1. [M] avgTimeOnPage -> 無し

# 互換性エラー
No. 8「pagePathLevel1」とNo. 13「avgTimeOnPage」は変換先がありません。
どうにかして、作る必要がありそうです。

そして問題がもう一つ、クイックスタートガイドのソースコードをベースに下記のようにディメンジョンを指定してみました。（# ***で囲んでいる部分）

```Python
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    DateRange,
    Dimension,
    Metric,
    RunReportRequest,
)
import os

# ************************
DIMENSIONS = [
    "date", 
    "dateHourMinute", 
    "pagePathPlusQueryString", 
    "sessionSource",
    "deviceCategory", 
    "operatingSystem", 
    "pageTitle"
    ]
# ************************

METRICS = [
    
]
PROPERTY_ID = 123456789
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = "hogehoge.json"


def sample_run_report(startdate = "2023-03-01", enddate = "2023-03-07"):
    """Runs a simple report on a Google Analytics 4 property."""
    # TODO(developer): Uncomment this variable and replace with your
    #  Google Analytics 4 property ID before running the sample.
    # property_id = "YOUR-GA4-PROPERTY-ID"

    # Using a default constructor instructs the client to use the credentials
    # specified in GOOGLE_APPLICATION_CREDENTIALS environment variable.
    client = BetaAnalyticsDataClient()

    request = RunReportRequest(
        property=f"properties/{PROPERTY_ID}",
        dimensions=[ Dimension(name = dim) for dim in DIMENSIONS],
        metrics=[ Metric(name = met) for met in METRICS],
        date_ranges=[DateRange(start_date=startdate, end_date=enddate)],
    )
    response = client.run_report(request)
    
sample_run_report()
```

こちらを実行すると下記のようなエラーが発生します。このディメンジョンには互換性がないということで「dateHourMinute」と「pagePathPlusQueryString」を削除しましょうとのことです。

```
google.api_core.exceptions.InvalidArgument: 400 Please remove dateHourMinute and pagePathPlusQueryString to make the request compatible. 
The request's dimensions & metrics are incompatible. To learn more, see https://ga-dev-tools.web.app/ga4/dimensions-metrics-explorer/
```

ただ私の方で色々調査したところ、これらの互換性がないと言われるディメンジョンも、単体であれば問題なく動作しました。
```Python
# 問題なく実行できるパターン
DIMENSIONS = [
    "pagePathPlusQueryString"
    ]
```
```Python
# 問題なく実行できるパターン
DIMENSIONS = [
    "dateHourMinute"
    ]
```

**特定のディメンジョンと一緒にリクエストすると、互換性がない旨のエラーが発生するようです。**
```Python
# 互換性エラーが発生するパターン
DIMENSIONS = [
    "dateHourMinute",
    "pagePathPlusQueryString"
    ]
```
```Python
# 互換性エラーが発生するパターン
DIMENSIONS = [
    "pagePathPlusQueryString",
    "sessionSource"
    ]
```

こちらのエラーについて色々調べてみたものの、どの組み合わせでエラーが出るのか、エラーが出たときにはどんな対応を行えばよいのか分からずじまいでした。

# 対応方法
原因の特定が難しく根本的な解決方法が見つからないので、ひとまずGoogle公式にあるディメンジョン・メトリクス一覧から使えそうなものを指定し、Python上で変換を行うことにしました。

https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema

現状の問題を整理すると、下記の4つのディメンジョン・メトリクスについて対応が必要です。
1. dateHourMinute
1. pagePath
1. pagePathLevel1
1. avgTimeOnPage

### dateHourMinute（Hourまでで妥協）
「date」や「hour」、「dateHour」はディメンジョンに指定しても問題なく実行できますが、なぜか「dateHourMinute」と「Minute」など分を含めたものを指定すると互換性エラーが発生します。
なので、「dateHourMinute」については妥協して「dateHour」で代用することにしました。。。

### pagePath, pagePathLevel1
UA->GA4の対応表をみると「pagePath」は「pagePathPlusQueryString」に変更とありますが、ディメンジョン・メトリクス一覧から同じようなデータが取れる「PagePath」を指定してみました。
「pagePath」が取れれば、「pagePathLevel1」は下記のように簡単な加工で作成できます。
```
[pagePath -> pagePathLevel1]
/ -> /
/aaa -> /aaa
/aaa/bbb -> /aaa/
/aaa/bbb/ccc -> /aaa/
```

### avgTimeOnPage
幸い「pageviews」（GA4ではscreenPageViews）と「timeOnPage」（GA4ではuserEngagementDuration）のメトリクスは同時に指定してリクエストしてもエラーは出ないようです。
そのため、avgTimeOnPageはこれらの割り算（= userEngagementDuration / screenPageViews）で求めることができます。

# スクリプト

加工部分は「convert_GA4response_to_UAdataframe」関数で行っています。

```Python
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    DateRange,
    Dimension,
    Metric,
    RunReportRequest,
)
from googleapiclient.discovery import build
import logging
import os
import datetime
import configparser
import pandas as pd
import traceback

config_ini = configparser.ConfigParser()

DIMENSIONS = [
    "date", 
    "dateHour", # "dateHourMinute", "minute"は残念ながら使えなかった
    "pagePath", # "pagePathPlusQueryString" から変更
    "sessionSource",
    "deviceCategory", 
    "operatingSystem", 
    "pageTitle"
    ]
METRICS = [
    "totalUsers",
    "sessions",
    "screenPageViews",
    "userEngagementDuration"
]

class get_GA4_data():
    def __init__(self):
        self.PROPERTY_ID = 123456789
        # 省略

    def sample_run_report(self, startdate = "2023-03-01", enddate = "2023-03-07"):
        """Runs a simple report on a Google Analytics 4 property."""
        # TODO(developer): Uncomment this variable and replace with your
        #  Google Analytics 4 property ID before running the sample.
        # property_id = "YOUR-GA4-PROPERTY-ID"

        # Using a default constructor instructs the client to use the credentials
        # specified in GOOGLE_APPLICATION_CREDENTIALS environment variable.
        client = BetaAnalyticsDataClient()

        request = RunReportRequest(
            property=f"properties/{self.PROPERTY_ID}",
            dimensions=[ Dimension(name = dim) for dim in DIMENSIONS],
            metrics=[ Metric(name = met) for met in METRICS],
            date_ranges=[DateRange(start_date=startdate, end_date=enddate)],
        )
        self.response = client.run_report(request)
    
    def convert_GA4response_to_UAdataframe(self):
        ### dimensions:   ['date', 'dateHour', 'pagePath', 'sessionSource', 'deviceCategory', 'operatingSystem', 'pageTitle']
        ### metrics:      ['totalUsers', 'sessions', 'screenPageViews', 'userEngagementDuration']
        ### -> ['date', 'hour', 'pagePath', 'sessionSource', 'deviceCategory', 'operatingSystem', 'pageTitle', 'totalUsers', 'sessions', 'screenPageViews', 'userEngagementDuration'] 
        output_header = []
        output_header.extend([header.name for header in self.response.dimension_headers])
        output_header.extend([header.name for header in self.response.metric_headers])

        ### dataframeにappendするより、辞書にしてDataframe化したほうが早そう。
        output_dic = {}
        for header in output_header:
            output_dic[header] = []

        for row in self.response.rows:
            value_list = []
            value_list.extend([elem.value for elem in row.dimension_values])
            value_list.extend([elem.value for elem in row.metric_values])

            for k,v in zip(output_header, value_list): 
                output_dic[k].append(v)

        ### [追加] pagePathLevel1 : pagePathの/*/までを抽出 ( / -> /, /hogehoge -> /hogehoge, /aaa/bbb/ccc -> /aaa/)        
        pagePathLevel1 = []
        for pagepath in output_dic["pagePath"]:
            if pagepath.count("/") <= 1: pagePathLevel1.append(pagepath)
            else: pagePathLevel1.append(pagepath[0 : pagepath.find("/", 1) + 1])
        output_dic["pagePathLevel1"] = pagePathLevel1

        ### [追加] avgTimeOnPage : 計算して列追加。userEngagementDuration / screenPageView
        avgTimeOnPage = []
        for pageviews, duration in zip(output_dic["screenPageViews"], output_dic["userEngagementDuration"]):
            if int(pageviews) == 0: avgTimeOnPage.append(0)
            else: avgTimeOnPage.append(int(duration) / int(pageviews))
        output_dic["avgTimeOnPage"] = avgTimeOnPage

        self.df = pd.DataFrame(output_dic)

        ### 列の順を変更する場合。本来、CSVを吐き出すフェーズでは意識すべきではない。
        ### ハードコードになり自由度が下がるためPythonスクリプト実行側で吸収した方が良い。
        # self.df = self.df.iloc[:, [0,1,2,3,4,5,6,11,7,8,9,10,12]]

    def write_csv(self, filename):
        self.df.to_csv(filename, index=False)
```

# さいごに
今回、UA → GA4への移行にあたりハマった部分を紹介しました。
互換性エラーの根本原因はわからないのですが、エラーが発生しない範囲での対応策についても紹介しました。
原因について、なにか分かり次第また投稿しようと思います。