俺、サービス売って家買うんだ

Swift, Vue.js, 統計, GCP / このペースで作ってればいつか2-3億で売れるのがポっと出来るんじゃなかろうか

ExpressでGoogle Cloud Storageに画像をアップロードする

f:id:ie-kau:20170222235628j:plain

やりたきこと

  • Expressで稼働しているWebサービスで画像をGoogle Cloud Storage(以下GCS)にアップロードする
  • サブドメインで画像を閲覧できるようにする

各種バージョン

Node 7.x
Express 4.x

事前準備

以下は完了している

  • ドメインは取得
  • Google Cloud DNSの設定

1. image.[ドメイン].comというサブドメインを設定する

Google Cloud DNSでCNAMEレコードに該当サブドメインを追加します。

f:id:ie-kau:20170222235440p:plain

2. ドメインの所有権の認証をする

ドメイン名を持つバケットを作成するには、GCPにログインしている管理者が対象のドメインの所有者であることを確認しておかなければなりません。

ドメインを持つバケットを作成できるユーザー

ドメインに確認済みのオーナーが複数いる場合、これらのオーナーのみが名前にドメインを使用したバケットを作成することができます。ドメインに確認済のオーナーがいない場合、確認済のウェブサイト オーナーが名前にドメインを使用したバケットを作成することができます。少なくとも 1 人の確認済のオーナーがドメインまたはウェブサイトに含まれていないと、名前にドメインを使用したバケットを作成することはできません。ドメイン所有権はウェブサイト所有権よりも制御レベルが高いため、ドメイン名を持つバケットを作成できるユーザーをサイトで厳しく制御する必要がある場合に役立ちます。

たとえば、「example.com」というサイトの管理を担当する IT スタッフ メンバーが 2 人いるとします。必要な確認(以下を参照)を完了すると、「example.com」、「reports.example.com」、「downloads.example.com」など、ドメイン名を持つバケットを作成できるユーザーはその 2 人のみになります。

確認済のウェブサイトまたはドメイン オーナーは、Search Console を使用してウェブサイトまたはドメイン オーナーを追加することができます。Search Console のダッシュボードで、管理するウェブサイトを検索し、[プロパティの管理] > [ユーザーを追加/削除] を選択します。ドメイン所有者を追加するには、[プロパティ所有者の管理] リンクを選択します。ドメイン所有者は、他のドメイン所有者を追加することができます。

※引用元
Domain-Named Bucket Verification  |  Cloud Storage Documentation  |  Google Cloud Platform

これは引用にあるようにSearch Consoleから対応できます。

3. Google Cloud Platform API に利用する鍵の作成

「IAMと管理」の「サービス アカウント」でGCPのAPIを利用するサービスアカウントと鍵を作成します。

f:id:ie-kau:20170222235332p:plain

ここで作成したjsonの鍵はバケットにファイルをアップロードする時に利用します。

4. GCSのバケットを作る

利用したいサブドメインと同じバケット名でバケットを作ります。 ドメインの所有権が確認されてない場合はここでエラーになります。

f:id:ie-kau:20170222235225p:plain

Expressの設定

利用するモジュール

ソースコード

const express = require('express')
const router = express.Router()
// -- その他色々 -- 
const multer  = require('multer')
const storage = require('@google-cloud/storage')
const gcs = storage({
  projectId: '[projectId]',
  keyFilename: '[/path/to/key.json]' // 事前準備3で作成した鍵
})

const bucket = gcs.bucket('image.[ドメイン].jp')
bucket.acl.default.add({
  entity: "allUsers",  // 外部アクセスを可能にする
  role: storage.acl.READER_ROLE
}, err => {})

const imageUploader = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 10 * 1024 * 1024  // ファイルサイズ上限
  }
}).single('image')

router.post('/upload', (req, res, next) => {
  imageUploader(req, res, err => {
    if (err) {
      var error = null
      switch(err.code) {
        case 'LIMIT_FILE_SIZE':
          error = {code: 1111, message: 'ファイルが大きすぎます'}
          break
        default:
          error = err
          break
      }
      next(error)
      return
    }

    const mimetype = req.file.mimetype
    const type = [
      { ext: 'gif', mime: 'image/gif' },
      { ext: 'jpg', mime: 'image/jpeg' },
      { ext: 'png', mime: 'image/png' }
    ].find(ext => {
      return ext.mime === mimetype
    })

    if (!type) {
      next({code: 1111, message: "不正なファイル形式です"})
      return
    }

    const blob = bucket.file(`uploads/${req.file.originalname}`)
    const opts = { 
      metadata: { 
        contentType: mimetype,
        cacheControl: "public, max-age=300"
      } 
    }
    const blobStream = blob.createWriteStream(opts)

    blobStream.on('error', err => {
      next(err)
      return
    })

    blobStream.on('finish', () => {
      const publicUrl = `http://${bucket.name}/${blob.name}`
      res.json({ok: 1, imageUrl: publicUrl})
    })

    blobStream.end(req.file.buffer)
  })
})

コードは端折りましたがこれで image.[ドメイン].jp のバケットの uploadsというフォルダにファイルをアップロードすることができました。

課題

  • GCSでサーブした場合のSSL対応はどうすればいいのか?

【Swift】関数にオプショナルのブロックを渡す

f:id:ie-kau:20151120074851p:plain:w400


括弧が多くて意味不明になったので一旦メモ。

func execute(completion: (() -> Void)?) {
    if let unwraped = completion {
        unwraped()
    }
}

execute { 
    print("hoge") // hoge
}

これだとコンパイルエラー

func execute(completion: () -> Void?) {
    if let unwraped = completion {
        unwraped()
    }
}

error: initializer for conditional binding must have Optional type, not ‘() -> Void?’ if let unwraped = completion {

Moment.jsを使ってタイムゾーンがEtc/GMTの日付と時刻をAsia/Tokyoに変更する

f:id:ie-kau:20170130023225p:plain:w400


iOSのIn-App Purchase の Auto-Renewing subscription (自動更新型の購読)を利用した開発では購入処理が走った際や、期限切れの際にAppleから得られるレシートの検証をAppleのサーバーにpostして行う必要があります。
当然ながら有効期限を表すexpires_date など時間に関するデータのタイムゾーンは標準時です。Appleの場合Etc/GMT。

現在進めている開発ではDBやサーバーのOSはAisa/Tokyoで動作させているので、データベース追加時や期限切れの確認のときには当然タイムゾーン変更する処理が必要になります。

バックエンドはNodeで作っているので、時間を扱うのに便利なMoment.jsを利用してタイムゾーンを変更していきます。

返ってくるデータの例 : 2017-01-30 16:03:34 Etc/GMT

const datetime = "2017-01-30 16:03:34 Etc/GMT".split(" ")
moment.tz(`${datetime[0]} ${datetime[1]}`, datetime[2]).clone().tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm:ss")
// => 2017-01-31 01:03:34

と言った具合簡単にタイムゾーンの変更ができます。
とはいえ調べる必要があったのでメモメモ。

関係ないけどUTCとGMTの違いがよくわかってなかったので参考。

citizen.jp

  • GMT = 英国のグリニッジ(経度0度)の地方平均時(平均太陽が南中する時を正午とする)のこと
  • UTC = 国際原子時と世界時の誤差が0.9秒以上ならないようにうるう秒で調整をしたのがUTC(協定世界時:Coordinated universal time)

一般的には同義と考えて良い。

Vuexを利用せずに親子関係を持たないコンポーネント同士で通信する

f:id:ie-kau:20150818232251p:plain


しばらくぶりにWebのフロントエンドを書いてるので色々復習を込めて。

ReactやVueなどコンポーネント指向で実装できるフレームワークを利用して開発する際に親子関係を持たないコンポーネント同士で通信をしたい時ってありますよね。

とりわけシングルページアプリケーションを作っている際に自分がよく出会うケースとして

  • 共通ヘッダーのタイトルを書き換える
  • ページによって共通ヘッダーに「戻る」ナビゲーションを出したり消したりする
  • バックグラウンドで通信し続けているソケットからデータを受信して画面上にNotificationのバッヂやポップアップを出す

とかがあります。

こういう時にこそアプリケーション全体で一つのstoreを共有してstateを管理する方針のfluxパターンを実現するVuexやReduxを導入したくなります。

ただただ、概念を理解したり動くようになるまでに準備が結構必要だったりと、ちょっとした開発や、アプリケーション全体で親子関係を持たないコンポーネント間通信が数回しか無かったりするとやることが多すぎると感じることが多々あります。

で、EventEmitterを継承したクラスを作ってエイヤッ!ってやることも多かったのですが、実はVue.jsのドキュメントを読み込んでみると

親子間以外の通信

たびたび、互いに親子関係ではない2つのコンポーネントが互いに通信する必要があるかもしれません。簡単なシナリオとして、空の Vue インスタンスを中 心のイベントバスとして使用することができます。

とのことで、空のVueインスタンスを使えば $on, $emitで通信できるイベントバスとして利用できると公式で明言されていました。 (イベントバスと言う言葉は知らなかったのですが。)

なのでどんな形でもいいのでアプリケーション上で一つのイベントバスを作ってそれを経由させてイベントのやり取りをするのが良いでしょう。

例えば、全てのコンポーネントの上にポップアップを出してモーダル状態にするとなるとこんなイメージです。

Vue.js 2.x系
vue-router 2.x系

EventBus.js

import Vue from 'vue'

const EventBus = new Vue()

export default EventBus

App.js プアリケーション全体管理のJS

import EventBus from '../libs/EventBus.js'

export default {
  name: 'App',

  created() {
    EventBus.$on('show-popup', this.showPopup)
    EventBus.$on('hide-popup',  this.hidePopup)
  },

  data() {
    return {
      isPopupShonw: false,
      isOverlayShown: false
    }
  },

  methods: {
    showPopup() {
      this.isPopupShown = true
      this.isOverlayShown = true
    },
    hidePopup() {
      this.isPopupShown = false
      this.isOverlayShown = false
    }
  }
}

コンポーネント

<template>
  <main>
    <header>ヘッダー</header>

    // vue-routerを利用した際のコンテンツ
    <router-view></router-view>

    <div v-show="isPopupShown" class="popup">ポップアップだよ</div>
    <div @click='hidePopup' v-show="isOverlayShown"></div>
  </main>
</template>

てな具合でしょうか。

まとめ

こんな感じでイベントを利用したコンポーネント間通信が簡単にできます。

注意としては、やりすぎるとイベント地獄が待っているのでアプリケーショ大きくなり始めたところで、規約を持たせるためFluxのフレームワーク導入を検討するのが良いでしょう。

iPhoneゲーム、「荻月のゴマビィ」をリリースしました。

f:id:ie-kau:20170111200551p:plain:w400


あけましておめでとうございます! @hazumu です。

前回 Hayato さんがめっちゃ質の高い記事を書いてたので新年一発目から尻込みしちゃいますね。

さてさてさてさて、一昨年の9月に途中まで作って放置していたゲームを去年の12月に作り込んで12末のAppleの審査がお休みの時期前に申請にだして、この年始に(多分1月2日ぐらい)に、なんとか初回リリースにこぎつけました。

今回はその紹介記事です。

ゲームの名は。

ゲームの名前は「荻月のゴマビィ」と言います。
よくわからんタイトルですが古典的な「コイン落とし + キャラ集めゲーム」です。

最初はキャラクターが黄色くて丸くて仙台名物萩の月に似ていたので「萩の月のゴマビィ」にしていたのですが、流石に商標とかヤバそうでよくわからないから適当に字をもじって意味不明な名前にしました。

ゴマビィというキャラクターは、今回デザインを全部やってくれた @kemkem_ さんが小学校時代にノートの片隅に連載していた、その名も「合体」という漫画の主人公を流用したものです。およそ20年の時を経て色と命を吹き込みました。何を合体させたかはご想像におまかせします!

iOS版ダウンロードはこちら。↓↓

荻月のゴマビィ

荻月のゴマビィ

  • Hazumu Jo
  • Juegos
  • Gratis

何はともあれ概要。

西暦2200年。

エネルギー問題に直面していた人類は月のエネルギーを利用することによりついに永久機関を開発することに成功した。

しかし、月のエネルギーを浴び続けた生物はどうしても他者との異存なしに生きられなくなってしまうという特性を持っていた。そう、融合である。

そして、いよいよその特性を悪用しようとする者が出てくる。 その一人が「ゴマ・パロアルト博士」。彼は、いわばマッドサイエンティストである。

2つの生命体に月のエネルギーを浴びせ、お互いの生物の強依存を利用して2つの生物の融合を促進する機械を開発してしまったのだ。。

最初に創りだされた生物の名前は「ゴマビィ」。 ゴマビィは、「みんなが大好きな仙台銘菓のあのお菓子」と「ゴマフアザラシ」の融合生物(キメラ)である。しかも量産型。

しばらくすると博士の研究所は合成生物で溢れかえっていた。 そんななか偶然生まれた知性をもった一匹の「ゴマビィ」。彼は月にある研究所の頭上に青く光る星を見ながら思った。

「生命として生まれてきたのに、こんな自由のない暮らしをしていて良いのだろうか?僕たちは自由を得るために戦うべきではないのだろうか。」

(後に彼は「民衆を率いる自由のゴマビィ」と呼ばれることになるが、それはもう少し後の話しである。)

そして、あの日、星が降った日。それはまるで、 まるで、夢の景色のように、 美しいながめだった日、知性をもったゴマビィはついに立ち上がった。 数多くのゴマビィやその他の合成生物(キメラ)を率いて博士の研究所内で大暴動を起こしたのだ!

「量産型のゴマビィはどうでもいい!少しでも、少しでもレアな合成生物(キメラ)は捕まえなければ。ワシの資産が!」 合成生物(キメラ)たちのの暴動の中で博士は、UFOに乗り出し逃げ出そうとする合成生物を捕まえる作戦を始めたのだった!!

■追伸 ちなみにこのゲーム内においてあなたの立ち位置は博士であることを述べておこう。

コイン落としです!

説明

ダウンロードしたくなる説明をいざ、転載!!

f:id:ie-kau:20170112191756j:plain

f:id:ie-kau:20170112191809j:plain

f:id:ie-kau:20170112191820j:plain

こだわりポイント

1. 思わず抱きしめたくなる愛くるしいキャラクター

f:id:ie-kau:20170111203459p:plain

f:id:ie-kau:20170111203506p:plain

こんな感じのキャラクターがなんと全32種類!!!

新しいキャラクターを捕まえると、そのキャラクターのなきごえせつめいを見ることができます。 時代にそぐったあんなキャラクターやあんな人のパロディーも出現するかも。

全てコンプリートして、ゴマ博士を目指しましょう。

2. 剽軽な楽曲

無料素材です!
が、、素材が素晴らしいのでどんなに落ち込んだときだってイヤフォンをつけてプレイすれば気持ちも剽軽になります!

クレジット

スタッフ

音楽素材提供

展望

正直言って出すことが目的になってたのでやるかわからないのですが、余力があればこの辺をやってみたいですね。

  • 手ゴマ(体力)回復のインターステシャル広告入れてみたいよね
    • その上でadsを打ってリクープするか見てみたい
  • キャラを増やす
  • キャラクターが弾丸っぽいので弾幕シューティングゲームに作り変える

さてと!

今年も、公私にわたってたくさん作るぞ!!リリースするぞ!!
٩( 'ω' )و オラッオラッ

実店舗の売上データで分析する。(タンタンタイガー)

あけましておめでとうございます! Hayatoです。
去年の暮に出したかったんですが惜しくも間に合わず、年を越した渾身の記事から今年のblogをスタートです!

友人が店主の担々麺屋でタンタンタイガーってのがあります。2016年8月に開店したので、まだ半年しか経ってないのですが、割りといい感じでお客さんが来てくれてます。今回は、そのお店の実際の売上データを、Pythonで分析していきます。

なかなか実店舗のガチデータを公開することはないので、貴重な機会ではないでしょうか。データ分析を練習中の人はもちろん、これから飲食店をやろうとしている人や、いま売上を伸ばすのに苦戦中な飲食店店主さんにも役に立つとうれしいです。

分析ノート(iPython notebook)はGithubにもあげているので、こちら気になる方は参考にしてください。(重いので携帯では見れないかもです)

github.com

Index

  1. データクリーニングと把握
  2. 分析計画
  3. 実際に分析

それでは早速やっていきましょう!

1. データクリーニングと把握

まずはデータをきれいにすることから始めます。今回は気象データが売上にどれくらい影響しているかを調べたかったので、気象庁のデータをこちらから持ってきて、日付で突き合わせました。データ量が少なかったので、スプレッドシートで処理しています。

実際の処理後のCSVデータはこちらにあるので、分析の練習や、お店のシュミレーションに使ってください!

処理が終わったら、どんなデータなのかを見ていきます。分析前に大まかな概要を掴んでおくのはとても大切です。

データの概要を把握

#必要なライブラリを予めすべてインポート
import pandas as pd
import numpy as np
import pylab as pl
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
%matplotlib inline
sns.set_style('whitegrid')
#CSVデータをDF形式で読み込み。 indexにdateを設定
rowdata = pd.read_csv('./data/tantantigerdata.csv', 
                      parse_dates=['日付'],index_col=['日付'], dayfirst=True)
#データ概要1:どういうカラムでどういうデータが入っているか確認
rowdata.sort_values(by='来客数', ascending=True).head(3) 
#データ概要2:平均値などを確認
rowdata.describe().astype(int)                     

結果: f:id:hayato1986:20170105205127p:plain f:id:hayato1986:20170105205135p:plain

なんとなくですが、

  • 前提:サンプル数は77 / 時系列データ。
  • 平均82人の来客数。単価平均は981円。
  • 52 ~ 112 人が95%の来客数範囲。この範囲を超えると何かあったと見て良いかも

くらいまでわかります。

2. 分析計画

データのクリーニングとその把握ができたら、実際の分析計画を立てていきましょう。 店主の東山さんと話して、以下のように計画を立てました。新規とリピーターの割合、天候と売上の相関に関して焦点を当てています。

1. 時系列における売上の推移はどうなっているか
 - 開店直後からの数値推移
 - 新規とリピーターの割合推移
 - リピーターと曜日の相関
2. 天候による売上の相関関係はどうなっているか
 - 気温と売上の相関
 - 天気と売上の相関

3. 実際に分析

それでは計画をもとに、分析をしていきましょう!
ようやく分析できる!ってところまで来ると、もう80%くらいは作業が終わってたりします笑

時系列における売上の推移はどうなっているか

開店直後からの数値推移 (新規顧客の推移 /リピーターの推移)

まずは単純に時系列の売上推移です。リピーター数と新規数も同時に出しています。
一日ごとのばらつきが結構大きく、全体を掴みづらいので14日の移動平均で算出しています。

rowdata_rolling = rowdata.rolling(window=14, min_periods=14).mean() #14日の移動平均

graph_num = 5
fig, axes = plt.subplots(nrows=graph_num, ncols=1, figsize=(16,12),  squeeze=False) #squeezeはsubplotsのおまじない

for i, col in enumerate(rowdata_rolling):
    rowdata_rolling[col].plot(ax=axes[i,0])
    axes[i,0].set_title(col, fontsize=14, fontweight='bold')
    i+=1
    if i == graph_num:
        break
plt.tight_layout() #スペース調整

結果: f:id:hayato1986:20170105205612p:plain

  • 10月がピークだった?
  • 全体数値は落ちてなさそう
  • 新規は落ちてそう
  • リピーターが増えてそう

くらいはわかりますね。実際にエリアチャートを使って視覚化していきましょう。

新規とリピーターの割合推移

rowdata_rolling.loc[:,['リピート数','新規合計']].plot(
    legend=True, figsize=(16, 8), y=['リピート数','新規合計']
          , kind='area', stacked=True, alpha=0.5
)

結果: f:id:hayato1986:20170105205749p:plain

  • リピーター数は伸びている
  • 新規は10月22日のTVCMで大きく増えたが少し右肩下がり
  • リピーター数が伸びているので新規の落ち込みをカバーしている。全体では減ってない

あたりは明確に言えそうですね。次は曜日ごとの数値の関係性を見ていきましょう。

リピーターと曜日の相関

#Pivot Table で分析 / aggfunc='mean' , columns='リピート数 の SUM'
rowdata.pivot_table(index="曜日",aggfunc='mean').astype(int)

#Group by で分析
week_group = rowdata[['リピート数','新規数']].groupby(rowdata['曜日'])
#week_group.mean()
week_group.mean().plot(legend=True, figsize=(16, 4), kind="bar", stacked=True)

結果: f:id:hayato1986:20170105212915p:plain

  • 日曜は新規客が売上を支えている
  • やや木曜日のお客さんの伸びが悪い
  • リピーターはどの曜日でもかわらない

といったことが言えそうです。タンタンタイガーはBlogで集客していたので、日曜日に遠方からのお客さんがちらほら来ていただけるのが大きいですね。

天候による売上の相関関係はどうなっているか

変数が多いので、関係ありそうなところだけを恣意的にピックして、相関関係を見ていこうと思います。

fig, ax = plt.subplots(figsize=(10,10)) #Sizeを定義する
data = rowdata.drop(['最高気温','最低気温','日照時間', '平均風速', '蒸気圧(hPa)'], axis=1)
sns.heatmap(data.corr() ,annot=True, linewidths=.5, ax=ax)

f:id:hayato1986:20170105210538p:plain

ここで注意しなければならないのは、新規のお客さんは時系列で少しづつ減っていて、リピーターが伸びていることです。相関係数だけを見ると、 来客数と気温に相関がありそうですが、開店直後から現在まででリピーター数が比例し、新規客が反比例するのはある種当たり前なので、一概に比例関係にあるとは言いづらくなっています。

その中でも言えそうなのは

  • リピート数と単価平均に相関が見られそう
  • 雨と来店数はあんまり関係なさそう
  • 風はすべての数値にほとんど関係ない

くらいでしょうか。段々と暖かくなってきたら、ここらへんはまた分析して本当の相関を見ていけると思います。 店長の東山さんが「今日は天気が悪かったからお客さんが少なかった」とよく言うので、おまけで平均気温と来客数の相関を視覚的な面から追っていきましょう。

気温と売上の相関を詳しく見てみる

sns.jointplot('平均気温', '来客数', data=rowdata, kind="scatter")

f:id:hayato1986:20170105211313p:plain 散布図上の見た目では、やはりそこまで相関はなさそうです。念のため、見逃している相関が無いかどうか散布図とカーネルグラフを描画します。

returns_fig = sns.PairGrid(data)
returns_fig.map_upper(plt.scatter, color='purple')
returns_fig.map_lower(sns.kdeplot, cmap='cool_d')
returns_fig.map_diag(plt.hist,bins=10)

f:id:hayato1986:20170105211418p:plain

やはりあんまり特徴的な関係性は見られませんでした。

まとめ

飲食店のデータは分析しがいがありますね笑。本当ならこの分析結果から、TODOを決めてってところまでやるのですが、Blogでは分析までということにさせてください!これからもタンタンタイガーのデータを定期的に分析していこうと思います。

オラッオラオラオラッ(ΦωΦ)

Google Analytics で AMP 対応ページへのアクセスを通常ページへのアクセスとして集約する方法

f:id:ie-kau:20161228154610p:plain:w400

AMPとは

Google社が主導するWEBページのモバイルを高速化するプロジェクトです。
詳しくはここ。

www.ampproject.org

詳細は省きますが実装方法は通常のモバイルページ加えAMP対応したページを用意した上で通常ページのメタタグに

<link rel="amphtml" href="[url]?amp=1" >

上記の様にAMPページへのリンクを記述してクローラに伝ええます。

僕らが使っている「はてなブログ」でもAMP対応は施されているようです。 をパラーメータに?amp=1をつければamp用のページになります。

http://www.ie-kau.net/entry/islands_in_Seto_Inland_Sea?amp=1

Google Analytics で分析する際に発生する問題

クエリパラメータでAMP対応ページを作成た場合にGAでアクセス数を計測する際、集計したい値によっては問題が発生します。
AMP対応ページも、その元となった画面も同一のコンテンツなのにもかかわらず、GAはパラメータを含めたURLを1ページとしてアクセス数を集計するため、同一コンテンツへのアクセスが通常のページとAMP用のパラメータが付いたページで分かれて集計されてしまいます。

こんな感じ。
f:id:ie-kau:20161228153548p:plain

解決方法

Google Analytics ログイン後、「管理」より。 f:id:ie-kau:20161228153638p:plain

ビュー設定」へ。 f:id:ie-kau:20161228153943p:plain

除外するURLクエリパラメータ」にAMPの識別に利用しているパラメータを入力します。 f:id:ie-kau:20161228154148p:plain

まぁ、言ってしまえば他の除外したいパラメータで対応する時と同じなのです。
これだけで、AMPページのアクセスも通常ページ側に集約されます。

デメリット

この方法のデメリットとしてはAMPページにどれだけアクセスがあるのか確認できないことでしょう。
SEO対策の一貫などでAMP対応した場合に、効果測定をする場合は一定期間おいてからの集約を施したほうがいいケースも出ると思います。