{"id":57,"date":"2025-08-31T16:20:00","date_gmt":"2025-08-31T13:20:00","guid":{"rendered":"https:\/\/fsaeed.blog\/?p=57"},"modified":"2026-10-31T00:18:52","modified_gmt":"2026-10-30T21:18:52","slug":"ga4-data-into-python","status":"publish","type":"post","link":"https:\/\/fsaeed.blog\/index.php\/2025\/08\/31\/ga4-data-into-python\/","title":{"rendered":"Pulling Google Analytics 4 Data into Python"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Hello, tech enthusiasts! \ud83d\udc4b<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every Monday, many office teams do the same thing. They open Google Analytics, click through a few reports, copy the visitor numbers, and paste them into a spreadsheet. It takes 20 minutes, and it is easy to copy the wrong cell.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">What if Python could do this for you? Google Analytics 4 (GA4) has an <strong>API<\/strong> for this. An API is like a waiter in a restaurant: you ask for something, and it brings the data back to your table. The GA4 API is called the <strong>Google Analytics Data API<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Today we will set it up step by step and get the data into a pandas table.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What you need<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A GA4 property where you are an Administrator (so you can add a new user)<\/li>\n\n\n<li>A Google account that can create a Google Cloud project (<a href=\"https:\/\/console.cloud.google.com\">https:\/\/console.cloud.google.com<\/a>)<\/li>\n\n\n<li>Python 3.9 or newer (<a href=\"https:\/\/www.python.org\">https:\/\/www.python.org<\/a>)<\/li>\n\n\n<li>These packages: <code>pip install google-analytics-data pandas openpyxl<\/code><\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Create a Google Cloud project<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A Google Cloud project is a folder that holds your API settings.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open the Google Cloud Console.<\/li>\n\n\n<li>At the top, click the project list, then <strong>New Project<\/strong>.<\/li>\n\n\n<li>Give it a name like &#8220;analytics-reports&#8221; and click <strong>Create<\/strong>.<\/li>\n\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Turn on the Analytics Data API<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Google keeps APIs switched off until you need them.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In your new project, open <strong>APIs &amp; Services<\/strong> \u2192 <strong>Library<\/strong>.<\/li>\n\n\n<li>Search for <strong>Google Analytics Data API<\/strong>.<\/li>\n\n\n<li>Click it, then click <strong>Enable<\/strong>.<\/li>\n\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Create a service account and a key<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A <strong>service account<\/strong> is a robot user. It is not a person, so your script can log in without your own password.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to <strong>IAM &amp; Admin<\/strong> \u2192 <strong>Service Accounts<\/strong> \u2192 <strong>Create service account<\/strong>.<\/li>\n\n\n<li>Name it &#8220;ga4-reader&#8221; and finish the steps. It does not need any Cloud roles for this task.<\/li>\n\n\n<li>Open the new service account, go to the <strong>Keys<\/strong> tab, and choose <strong>Add key<\/strong> \u2192 <strong>Create new key<\/strong> \u2192 <strong>JSON<\/strong>.<\/li>\n\n\n<li>A file downloads. Rename it to <code>ga4-key.json<\/code> and keep it in your project folder.<\/li>\n\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This JSON file is like a house key. Do not email it, do not put it in shared folders, and do not upload it to GitHub. If you use Git, add it to your <code>.gitignore<\/code> file.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Give the robot access to GA4<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The robot user has an email address. It looks like <code>ga4-reader@your-project-id.iam.gserviceaccount.com<\/code>. You can find it on the service account page, or inside the JSON file as <code>client_email<\/code>.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open Google Analytics and click <strong>Admin<\/strong> (the gear icon).<\/li>\n\n\n<li>Under your property, open <strong>Property access management<\/strong>.<\/li>\n\n\n<li>Click <strong>+<\/strong> \u2192 <strong>Add users<\/strong>, paste the robot&#8217;s email, and choose the <strong>Viewer<\/strong> role.<\/li>\n\n\n<li>Click <strong>Add<\/strong>.<\/li>\n\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Viewer is enough, because the script only reads data. Always give the smallest role that does the job.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While you are in Admin, open <strong>Property details<\/strong> and copy the <strong>Property ID<\/strong>. It is a number like <code>123456789<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Ask for a report<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Now the fun part. We ask GA4 for daily active users and sessions for the last 28 days, split by country.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Dimensions<\/strong> are the &#8220;by what&#8221; part, like date or country. <strong>Metrics<\/strong> are the numbers, like users or sessions.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from google.analytics.data_v1beta import BetaAnalyticsDataClient\nfrom google.analytics.data_v1beta.types import (\n    DateRange,\n    Dimension,\n    Metric,\n    RunReportRequest,\n)\n\nPROPERTY_ID = \"123456789\"  # your GA4 property ID\nKEY_FILE = \"ga4-key.json\"  # the service account key\n\n# Log in with the robot user's key\nclient = BetaAnalyticsDataClient.from_service_account_file(KEY_FILE)\n\nrequest = RunReportRequest(\n    property=f\"properties\/{PROPERTY_ID}\",\n    date_ranges=[DateRange(start_date=\"28daysAgo\", end_date=\"yesterday\")],\n    dimensions=[Dimension(name=\"date\"), Dimension(name=\"country\")],\n    metrics=[Metric(name=\"activeUsers\"), Metric(name=\"sessions\")],\n    limit=10000,\n)\n\nresponse = client.run_report(request)\nprint(\"Rows received:\", response.row_count)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can also use real dates like <code>\"2025-08-01\"<\/code>. Words like <code>\"today\"<\/code>, <code>\"yesterday\"<\/code> and <code>\"7daysAgo\"<\/code> work too.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 6: Turn the answer into a pandas table<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The answer comes back as rows. Each row has dimension values and metric values, in the same order you asked for them. We put them into a DataFrame (a pandas table).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import pandas as pd\n\ndim_names = [d.name for d in response.dimension_headers]\nmet_names = [m.name for m in response.metric_headers]\n\nrecords = []\nfor row in response.rows:\n    record = {}\n    for name, value in zip(dim_names, row.dimension_values):\n        record[name] = value.value\n    for name, value in zip(met_names, row.metric_values):\n        record[name] = value.value\n    records.append(record)\n\ndf = pd.DataFrame(records)\n\n# GA4 sends everything as text, so fix the types\ndf[\"date\"] = pd.to_datetime(df[\"date\"], format=\"%Y%m%d\")\ndf[met_names] = df[met_names].astype(int)\n\ndf = df.sort_values([\"date\", \"activeUsers\"], ascending=[True, False])\nprint(df.head())\n\ndf.to_excel(\"ga4_report.xlsx\", index=False)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">GA4 sends dates as text like <code>20250830<\/code>, so we convert them with a format. Now you have a clean table you can chart, filter, or send by email. \ud83c\udf89<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Common problems<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>403 User does not have sufficient permissions<\/code><\/strong> \u2192 The robot email was not added in GA4, or you added it to a different property. Check Step 4.<\/li>\n\n\n<li><strong><code>Google Analytics Data API has not been used in project<\/code><\/strong> \u2192 The API is not enabled in the same Cloud project as your key. Check Step 2.<\/li>\n\n\n<li><strong><code>Field xyz is not a valid dimension<\/code><\/strong> \u2192 Use the API names, not the names you see on screen. For example, <code>activeUsers<\/code>, not &#8220;Active users&#8221;. Google lists them on the &#8220;API Dimensions &amp; Metrics&#8221; page.<\/li>\n\n\n<li><strong>Numbers are a little different from the website<\/strong> \u2192 GA4 may use sampling or thresholds, and today&#8217;s data is still being processed. Use <code>yesterday<\/code> as the end date for stable numbers.<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Wrap-up<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You created a robot user, gave it read-only access, and pulled real GA4 numbers into pandas. Your Monday copy-paste job is now one script.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next idea: schedule the script to run every Monday morning with Windows Task Scheduler or cron, and save each report with the date in the file name.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to read your website visitor numbers from Google Analytics 4 with Python and turn them into a pandas table.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[47,46,45,13,12],"class_list":["post-57","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-api","tag-ga4","tag-google-analytics","tag-pandas","tag-python"],"_links":{"self":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/57","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/comments?post=57"}],"version-history":[{"count":1,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/57\/revisions"}],"predecessor-version":[{"id":76,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/57\/revisions\/76"}],"wp:attachment":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/media?parent=57"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/categories?post=57"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/tags?post=57"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}