Lookerでは同じデータを dimension と measure に分けて実装しよう

データ分析において、データはディメンション（分析軸）としてもファクト（指標）としても使われることがあります。
そのため、データソースが同じ1列だから、Looker でもそのまま dimension を1つ定義すれば終わり、とはなりません。

この記事では、まずディメンションとファクトの使われ方の違いを説明し、次に Looker でどのようにそれらを分けて表現すればいいのかを紹介します。

# 使用データ
今回の記事では、「サブスクリプションサービスの契約プラン」データを用いて解説します。無料プランが free で、有料プランは安い順に light, standard ... と続きます。
このデータは下図のように集計されています。粒度は「月×ユーザーID」で、値は「その月にユーザーが契約したプラン」を表します。

| month | user_id | plan |
| -------- | -------- | -------- |
| 2024-04  | 0001 | free |
| 2024-04 | 0002 | standard |
| 2024-05  | 0001 | standard |
| 2024-05 | 0002 | standard |
| ... | ... | ... |

Looker の view は以下のように定義されます。

```lookml
view: user_subscription_monthly {
  label: "プラン契約サマリ"
  sql_table_name: `xxx.yyy.user_subsciprion_monthly`
  
  dimension: month {...}
  dimension: user_id {...}
  ...
}
```

# 分析におけるディメンションとファクトの違い
「契約プラン」をディメンションとして扱う場合、それは契約プラン「**別**」に何か他の指標を集計する操作を指します。例えば、契約プラン別にユーザー数を数えたりします。SQLでは `GROUP BY`  や `WHERE` で使います。

「契約プラン」をファクトとして扱う場合、それは契約プランそのものを**指標**として集計する操作を指します。例えば、有料プランのユーザー数を計算したり、ユーザー属性別に有料プラン加入率を計算する、などの操作が挙げられます。SQL では `SELECT` で使います。

このように、同じデータ「契約プラン」でも、分析の要件によって使い方は大きく異なります。
そのため Looker では、plan 列をディメンションとして使いたいのか、ファクトとして使いたいのかによって、dimension と measure に分けて定義する必要があります。

# 列をディメンションとして使用する
まずは「契約プラン」をディメンションとして使用するパターンを考えます。一番シンプルな使い方です。
以下のように dimension を定義します。

```
dimension: plan {
  label: "契約プラン"
  type: string
  sql: ${TABLE}.plan ;;
}
```

このようにすることで、フィルターとして`WHERE`句の絞り込みに使用したり、`GROUP BY`による集約に使用したり、ピボットしてクロス集計を行なったりできます。

ここで注意したいのは、具体例「`GROUP BY`による集約」で求めた指標は、契約プラン毎の「アカウント数」（`COUNT(*)`）であり、「プラン契約者数」（例えばフリープランだと`COUNT(IF(plan="free",1,0))`）とは異なるという点です。
単純にプラン別の契約者数を計算するだけなら「契約プラン」をディメンションとして使うだけでも表現できます。しかし、複数のプラン契約者を合算したり、分析軸が複雑になったり、複数の指標を同時に計算したりしようとすると、「契約プラン」をディメンションとして使うだけでは表現が難しくなってしまいます。

SQLに慣れている人は、plan 列を `GROUP BY`で使う場合と`SELECT` で使う場合の違いをイメージするとよいでしょう。

# 列をファクトとして使用する
次に、「契約プラン」をファクトとして使用する例を紹介します。
色々な指標が考えられますが、今回はよくある例として「プラン契約者数」と「有料プラン加入率」を定義してみましょう。

## 指標「プラン契約者数」を定義する
`type:count`を使うやり方と、`type:count_distinct`を使うやり方があります。

### `type:count` を使う
`type:count` を使って「プラン契約者数」を分析するには、以下のように記述します。
```lookml
dimension: user_subscription_monthly_id {
  primary_key: yes # 主キーを指定しておくことで、type:count の measure が正しく集計を行えるようになる
  type: string
  sql: CONCAT(${TABLE}.month, ${TABLE}.user_id) ;;
}

dimension: plan {
  label: "契約プラン"
  type: string
  sql: ${TABLE}.plan ;;
}

measure: count_user_free {
  label: "フリープランユーザー数"
  description: "複数月に跨って計算する場合は延べ人数となる"
  type: count # primary_key に対して集計を行う
  filters: [plan: "free"] # フリープランの行のみを集計する
}
...
```

このように定義することで、フリープランのユーザー数を集計できます。

measure の`filters`属性を調整すれば、他のプランのユーザー数も同じように計算できます。

### `type:count_distinct` を使う
`type:count_distinct` とSQLを組み合わせて書くこともできます。
```lookml
measure: count_user_free {
  label: "フリープランユーザー数"
  description: "複数月に跨って計算する場合は延べ人数となる"
  type: count_distinct
  sql: IF(${TABLE}.plan = "free", CONCAT(${TABLE}.month, ${TABLE}.user_id), NULL) ;;
}
```

`sql`の設定を調整すれば、他のプランのユーザー数も同じように計算できます。

`type:count`はSQLの`COUNT`に対応し、`type:count_distinct`はSQLの`COUNT(DISTINCT column)`に対応します。

## 「有料プラン契約率」を定義する
次に、「有料プラン契約率」を定義します。

```
measure: avg_paid_plan_user {
  label: "有料プラン契約割合"
  description: "ユーザー全体に占める有料プラン契約者の割合"
  type: average
  value_format_name: percent_2 # 計算結果を「0.00%」という形式で表示する
  sql: IF(${TABLE}.plan != "free", 1, 0) # フリープラン以外は有料プランとみなす。列は非NULLとみなす
}
```

このように定義することで、「どのようなユーザーは有料プランを契約してくれる可能性が高そう？」といった分析を行えるようになります。例えば、ユーザー属性別に、有料プラン契約率に差が出るのかを分析できます。

ディメンションに比べ、ファクトの定義はより多様です。分析者の発想次第で、さまざまなファクトを作ることができます。例えば、「有料プラン」と一括りにするのではなく、プランの金額に応じて重み付け平均をとってもいいでしょう。

# まとめ
データ分析では、同じデータをディメンションとしても、ファクトとしても扱うことがあります。
これらの違いを理解した上で、要件に応じて Looker の dimension と measure に分けて実装しましょう。