lassence.net

Field notes from the AI frontier

Analyze Google Analytics 360 raw logs

--- date: · theme: data · words: 1,632 ---

With Google Analytics 360, you can access raw logs stored on BigQuery, with detail at hit level. This allows for much more thorough and finer analyses. Here are some examples of audience insights that can be generated with SQL queries only.

Setup and connection

# Import libraries
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from google.cloud import bigquery

sns.set()
# Launch BigQuery client from the JSON credentials file
bq_client = bigquery.Client.from_service_account_json('../../bigquery_creds.json')

# Helper function for queries to be returned as a Dataframe
def bquery(sql):
    return bq_client.query(sql).to_dataframe()

Users, sessions and pageviews

# Sessions and page views for 10 days
sql = """
SELECT
    PARSE_DATE("%Y%m%d", date) AS date,
    COUNT(DISTINCT fullVisitorId) AS visitors,
    SUM(totals.visits) AS sessions,
    SUM(totals.pageviews) AS pageviews,
FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
WHERE
    _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
GROUP BY 1
ORDER BY 1
"""
bquery(sql)
datevisitorssessionspageviews
02017-07-01194620486562
12017-07-02179118955637
22017-07-03190420466492
32017-07-04179919385740
42017-07-05268328859927
52017-07-06247826588924
62017-07-07221324509266
72017-07-08173218596087
82017-07-09176119216523
92017-07-102512276910183

Breakdown by devices and channels

# Traffic KPIs broken down by device and acquisition channel
sql = """
SELECT
    device.deviceCategory AS device,
    channelGrouping AS channel_grouping,
    SUM(totals.visits) AS sessions,
    SUM(totals.pageviews) AS pageviews
FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
WHERE
    _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
GROUP BY 1,2
ORDER BY 1,2
"""
bquery(sql)
devicechannel_groupingsessionspageviews
0desktopAffiliates5571543
1desktopDirect18006611
2desktopDisplay74317
3desktopOrganic Search650422545
4desktopPaid Search3331630
5desktopReferral217212220
6desktopSocial18934186
7mobileAffiliates119451
8mobileDirect20165235
9mobileDisplay32134
10mobileOrganic Search422412096
11mobilePaid Search3381361
12mobileReferral89281
13mobileSocial12193100
14tabletAffiliates1785
15tabletDirect223777
16tabletDisplay48
17tabletOrganic Search6132071
18tabletPaid Search100257
19tabletReferral1334
20tabletSocial129399

Traffic stickiness indicators

# Pages per session, average sessions duration (in seconds) and bounce rate
sql = """
SELECT
    PARSE_DATE("%Y%m%d", date) AS date,
    SUM(totals.visits) AS sessions,
    SUM(totals.pageviews)/SUM(totals.visits) AS page_per_session,
    SUM(totals.timeOnSite)/SUM(totals.visits) AS avg_session_duration,
    SUM(totals.bounces)/SUM(totals.visits) AS bounce_rate
FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
WHERE
    _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
GROUP BY 1
ORDER BY 1
"""
bquery(sql)
datesessionspage_per_sessionavg_session_durationbounce_rate
02017-07-0120483.204102103.7641600.581055
12017-07-0218952.974670101.5683380.568865
22017-07-0320463.173021119.0630500.558162
32017-07-0419382.961816107.8204330.567079
42017-07-0528853.440901119.3140380.515425
52017-07-0626583.357412115.7599700.504515
62017-07-0724503.782041146.9040820.489796
72017-07-0818593.274341114.3302850.536310
82017-07-0919213.395627114.9411760.546590
92017-07-1027693.677501142.3853380.514265

Transactions

# Transactions and revenue
sql = """
SELECT
    PARSE_DATE("%Y%m%d", date) AS date,
    SUM(totals.visits) AS sessions,
    SUM(totals.transactions) AS transactions,
    SUM(totals.transactions)/SUM(totals.visits) AS conversion_rate,
    SUM(totals.transactionRevenue)/1e6 AS revenue,
    SUM(totals.transactionRevenue)/1e6/SUM(totals.transactions) AS avg_purchase_value
FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
WHERE
    _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
GROUP BY 1
ORDER BY 1
"""
bquery(sql)
datesessionstransactionsconversion_raterevenueavg_purchase_value
02017-07-01204830.00146584.5428.180000
12017-07-02189580.004222634.9979.373750
22017-07-032046150.0073311225.8181.720667
32017-07-04193870.003612379.9854.282857
42017-07-052885420.0145588029.36191.175238
52017-07-062658310.0116633883.85125.285484
62017-07-072450400.0163274339.02108.475500
72017-07-081859140.007531454.9632.497143
82017-07-091921190.009891751.1039.531579
92017-07-102769470.0169744718.07100.384468

Goals

Several types of Goals can be defined in Google Analytics:

  • Destination page: e.g. /registersuccess.html
  • Duration: e.g. mininum 5 minutes per session
  • Pages per session: e.g. >10 pages per session
  • Event: e.g. click on a video

Goals are not computed in BigQuery raw logs, they have to be extracted with a query. Unlike queries above, that made use of the pre-aggregated metrics in the totals field, we now need to unnest the hits nested field to extract pages paths, so we compute sessions differently, by counting the number of distinct sessions IDs.

# Compute if a goal has been reached in each session
sql = """
WITH sub1 AS (
    SELECT
        PARSE_DATE("%Y%m%d", date) AS date,
        CONCAT(fullVisitorId, CAST(visitId AS STRING)) AS session_id,
        -- Goal: browsed to page /registersuccess.html
        MAX(IF(hits.page.pagePath = '/registersuccess.html', 1, 0)) AS goal_destination,
        -- Goal: session duration >= 5 min
        MAX(IF(totals.timeOnSite >= 300, 1, 0)) AS goal_duration,
        -- Goal: pages per session > 10
        MAX(IF(totals.pageviews > 10, 1, 0)) AS goal_pageviews
    FROM
        `bigquery-public-data.google_analytics_sample.ga_sessions_*`,
        UNNEST(hits) AS hits
    WHERE
        _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
        AND totals.visits > 0
    GROUP BY date, session_id
)

SELECT
    date,
    COUNT(session_id) AS sessions,
    SUM(goal_destination) AS goals_destination,
    SUM(goal_duration) AS goals_duration,
    SUM(goal_pageviews) AS goals_pageviews
FROM sub1
GROUP BY date
ORDER BY date
"""
bquery(sql)
datesessionsgoals_destinationgoals_durationgoals_pageviews
02017-07-01204830181114
12017-07-02189532168104
22017-07-03204639214116
32017-07-0419383119494
42017-07-05288553312202
52017-07-06265843280156
62017-07-07245055298195
72017-07-08185934208114
82017-07-09192139186129
92017-07-10276972338220

Path length: how many sessions before converting

# Get number of session before a transaction, and conversion rates by session position
sql = """
WITH sub1 AS (
    SELECT
        fullVisitorId,
        CONCAT(fullVisitorId, CAST(visitId AS STRING)) AS session_id,
        visitStartTime,
        MAX(IF(totals.transactions > 0, 1, 0)) AS transaction_session,
        RANK() OVER (PARTITION BY fullVisitorId ORDER BY visitStartTime) AS session_number
    FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
    WHERE
        _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
    GROUP BY fullVisitorId, session_id, visitStartTime
)

SELECT
    session_number,
    COUNT(session_id) AS sessions,
    SUM(transaction_session) AS transactions,
    SUM(transaction_session)/COUNT(session_id) AS conversion_rate
FROM sub1
GROUP BY session_number
HAVING transactions > 0
ORDER BY session_number
"""
bquery(sql)
session_numbersessionstransactionsconversion_rate
01196711410.007168
121859430.023131
23520170.032692
3419570.035897
459960.060606
564120.048780
6101010.100000

Time lag: how much time before converting

# Get time lag (in days) between first session of a user and a converting session
sql = """
WITH sub1 AS (
    SELECT
        fullVisitorId,
        MIN(visitStartTime) OVER (PARTITION BY fullVisitorId ORDER BY visitStartTime) AS first_session_time,
        MAX(IF(totals.transactions > 0, visitStartTime, 0)) AS transaction_session_time
    FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
    WHERE
        _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
    GROUP BY fullVisitorId, visitStartTime
)

SELECT
    FLOOR((transaction_session_time - first_session_time)/3600/24) AS lag_days,
    COUNT(*) AS transactions
FROM sub1
WHERE transaction_session_time > 0
GROUP BY lag_days
ORDER BY lag_days
"""
bquery(sql)
lag_daystransactions
00.0187
11.012
22.05
33.04
44.05
55.02
66.01
77.01

Differentiate brand vs non-brand keywords

# Differentiate between SEA keywords containing 'google' vs others
sql = """
SELECT
    CASE
        WHEN REGEXP_CONTAINS(LOWER(trafficSource.keyword), r"google") AND channelGrouping = 'Paid Search' THEN 'Paid Search Brand'
        WHEN channelGrouping = 'Paid Search' THEN 'Paid Search Non Brand'
        ELSE 'Other channels'
    END AS channel,
    SUM(totals.visits) AS sessions,
    SUM(totals.transactions) AS transactions,
    SUM(totals.transactions)/SUM(totals.visits) AS conversion_rate,
FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
WHERE
    _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
GROUP BY 1
ORDER BY 1
"""
bquery(sql)
channelsessionstransactionsconversion_rate
0Other channels216982170.010001
1Paid Search Brand37610.002660
2Paid Search Non Brand39580.020253

True Direct vs Last Non Direct Click

Definition of “Direct” channel in Google Analytics is rather narrow. A user will only be considered coming through “Direct” if he/she did not previously came via any other channel. Hence, acquisition channels in GA are attributed to the Last Non-Direct click.

With the BigQuery raw data, you can know if a user actually came directly to your site, even if he/she previously arrived via another channel, with the trafficSource.isTrueDirect field. Note how conversion rates are much higher when considering this parameter.

# Differentiate True Direct in channels
sql = """
SELECT
    channelGrouping AS channel,
    trafficSource.isTrueDirect IS NOT NULL AS true_direct,
    SUM(totals.visits) AS sessions,
    SUM(totals.transactions) AS transactions,
    ROUND(100*SUM(totals.transactions)/SUM(totals.visits), 2) AS conversion_rate,
FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
WHERE
    _TABLE_SUFFIX BETWEEN '20170701' AND '20170731'
GROUP BY 1,2
ORDER BY 1,2
"""
bquery(sql)
channeltrue_directsessionstransactionsconversion_rate
0(Other)False1NaNNaN
1AffiliatesFalse14602.00.14
2AffiliatesTrue3282.00.61
3DirectTrue12306131.01.06
4DisplayFalse54814.02.55
5DisplayTrue1427.04.93
6Organic SearchFalse31468168.00.53
7Organic SearchTrue6187145.02.34
8Paid SearchFalse168437.02.20
9Paid SearchTrue42116.03.80
10ReferralFalse5508206.03.74
11ReferralTrue4010342.08.53
12SocialFalse73191.00.01
13SocialTrue4301.00.23

Conversion rate by weekday and hour

# Conversion rate by weekday and hour
sql = """
SELECT
    EXTRACT(DAYOFWEEK FROM PARSE_DATE("%Y%m%d", date)) AS weekday,
    EXTRACT(HOUR FROM PARSE_TIMESTAMP("%s", CAST(visitStartTime AS STRING))) AS hour,
    100*SUM(totals.transactions)/SUM(totals.visits) AS conv_rate
FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`
WHERE
    _TABLE_SUFFIX BETWEEN '20170701' AND '20170731'
GROUP BY 1,2
"""
slots = bquery(sql)

# Plot as a heatmap
fig, ax = plt.subplots(1, 1, figsize=(8,8))
sns.heatmap(
    slots.pivot(index='hour', columns='weekday', values='conv_rate').fillna(0),
    cmap="YlGnBu", annot=True, fmt='.2f', cbar=False);

How many pageviews before reaching a Destination Goal

# Number of pages viewed in a session before reaching a Goal (destination page)
sql = """
WITH sub1 AS (
    SELECT
        CONCAT(fullVisitorId, CAST(visitId AS STRING)) AS session_id,
        -- Window function to compute rank of each Page viewed in a session
        RANK() OVER (PARTITION BY fullVisitorId, visitId ORDER BY hits.hitNumber) AS page_number,
        hits.page.pagePath AS page
    FROM `bigquery-public-data.google_analytics_sample.ga_sessions_*`,
    UNNEST(hits) AS hits
    WHERE
        _TABLE_SUFFIX BETWEEN '20170701' AND '20170710'
        -- Filter on Pages only, we don't want all Hits types
        AND hits.type = 'PAGE'
    GROUP BY fullVisitorId, visitId, hits.hitNumber, page
)

SELECT
    page_number,
    COUNT(*) AS goals
FROM sub1
-- Filter on the destination page associated with a Goal
WHERE page = '/registersuccess.html'
GROUP BY 1
ORDER BY 1
"""
goals_pages = bquery(sql)

# Plot distribution
(
    goals_pages
    .set_index('page_number')
    .reindex(list(range(0, max(goals_pages['page_number'])+1)))
    .plot(kind='bar', figsize=(10,6))
);