# Google Maps Popular Times & Traffic Analytics (`fanndev/google-maps-popular-times-analytics`) Actor

Collect Google Maps Popular Times foot-traffic curves in bulk: hour-by-hour busyness for every day of the week, plus live occupancy where available, for whole categories of venue in an area (every gym or cafe in a city). Wide or long format, JSON/CSV/Excel/NDJSON. No API key.

- **URL**: https://apify.com/fanndev/google-maps-popular-times-analytics.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Stats:** 5 total users, 4 monthly users, 68.4% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Maps Popular Times & Traffic Analytics

Collect Google Maps **Popular Times** foot-traffic curves in bulk — hour-by-hour
busyness for every day of the week, plus **live occupancy** where Google has it —
for whole categories of venue across a city.

Built for urban researchers, retail site-selection and investment analysts, and
competitive operations research: the data you can read one venue at a time in the
Google Maps UI, as a dataset you can actually analyse.

No login, no Google API key, no browser.

***

### What you get per venue

| Field | Notes |
| --- | --- |
| `popularTimes` | Seven days, each with an hourly curve: `{hour, occupancyPercent, label, displayHour}` |
| `busiestDay`, `busiestHour`, `peakOccupancyPercent` | Weekly peak, precomputed |
| `averageOccupancyPercent` | Mean across every reported hour |
| `liveOccupancyPercent`, `liveStatusText`, `hasLiveData` | Current busyness, when Google has a live reading |
| `currentDay`, `currentHour` | Scrape time in the venue's own timezone |
| `name`, `primaryCategory`, `address` | Venue identity |
| `latitude`, `longitude`, `timezone` | Real GPS coordinates; timezone is needed to read the curve locally |
| `rating`, `reviewsCount` | Rough proxy for venue size |
| `placeId`, `featureId`, `googleMapsUrl` | Stable identifiers |

Occupancy is Google's own 0–100 scale, where 100 is that venue's weekly peak —
**not** a headcount, and not comparable in absolute terms between venues.

***

### Wide vs long output

This matters more than it sounds:

- **`wide`** — one row per venue, the whole weekly curve nested inside it. Good
  for JSON consumers and for scanning venues.
- **`long`** — one row per **venue × day × hour**. This is the analysis-ready
  shape: open the CSV in a spreadsheet or `pandas` and you can group by hour
  across every gym in a city without unpacking anything.

A venue with a full week produces ~100–126 long rows.

```json
{
  "searchQueries": ["gym"],
  "areaName": "jakarta",
  "maxPlacesPerQuery": 100,
  "outputFormat": "long",
  "exportFormats": ["csv"]
}
```

Locations work the same way as the sibling Maps actors: `areaName`, or a centre
point with `radiusKm`, or an exact `boundingBox` (tiled for coverage, then
filtered using each venue's real coordinates). You can also pass `placeUrls` /
`placeIds` to track specific venues instead of searching.

***

### Why some runs return little data

Two separate effects, both measured rather than assumed. Read this before
concluding the actor is broken.

#### 1. Residential proxy is required (on by default)

Google serves two richnesses of place record and picks by client. The
popular-times block is in the full one only:

| Egress | Coverage |
| --- | --- |
| Apify platform, no proxy | **0%** |
| Apify platform, `RESIDENTIAL` *(the default)* | **62.5%**, with live readings |

A datacenter IP gets the block stripped out entirely. **Leave the residential
proxy on**; the run warns you if you turn it off. `GOOGLE_SERP` cannot be used —
it does not tunnel arbitrary HTTPS.

This is deliberately the opposite of the sibling Google Maps reviews and
lead-generation actors, which run best with **no** proxy. Those only need a page
to load; this one needs the full record.

#### 2. Coverage is genuinely partial anyway

Roughly a third of venues have no foot-traffic curve at all — Google only
publishes one where it has enough location history. Small independent venues
often have none; chains and busy venues usually do. `hasPopularTimes` tells you
which, and `onlyPlacesWithPopularTimes` decides whether empty ones are recorded.

Live occupancy is rarer still and only appears while a venue is open.

#### 3. Heavy runs degrade their own IP

An IP that has made many recent requests starts receiving reduced records too.
The actor paces itself (`requestDelaySeconds`, default 1.5 s) and warns when many
venues in a row come back empty, rather than handing you an empty dataset that
looks like "no venue has data". For large runs raise the delay; if a run comes
back mostly empty, wait and re-run.

***

### Live occupancy: read the flag, not the text

Google shows two things that both look live:

- `"Now: Usually as busy as it gets"` — a **projection** from the weekly curve.
  No live measurement exists.
- `"Busier than usual"` / `"Less busy than usual"` — a **real** live reading.

Only the second sets `hasLiveData: true` and populates `liveOccupancyPercent`.
Filter on `hasLiveData`, never on the presence of `liveStatusText`.

***

### Cost

Popular times is **not** in search results — every venue needs its own request.
A 200-venue run is roughly 210 requests plus pacing. Budget accordingly, and use
`maxPlacesPerQuery` to keep runs bounded.

***

### Output shape

Records follow the portfolio envelope — `_input`, `_source`, `_scrapedAt`,
`recordType` — with `_error` / `_errorDetail` on failures. `recordType` is
`PLACE` in wide format, `POPULAR_TIMES_HOUR` in long format, or `ERROR`.

Three Console views ship with the actor: **Overview**, **Weekly curves (wide)**
and **Hourly rows (long)**.

***

### Development

```bash
pip install -r requirements.txt
python test_errors.py     # offline: parsing, live detection, long format, exporters
python test_local.py      # live end-to-end run
```

See [CRAWLING\_METHOD.md](CRAWLING_METHOD.md) for the endpoint reconnaissance, the
day-numbering proof, and why this block must never be read by slot index.

# Actor input Schema

## `searchQueries` (type: `array`):

Venue types to collect, e.g. 'gym', 'cafe', 'shopping mall'. Each runs across the location below. A location is required with queries.

## `areaName` (type: `string`):

Built-in city or country, e.g. 'jakarta', 'bali', 'london'. For anywhere else use a centre point with a radius, or a bounding box.

## `centerLatitude` (type: `string`):

Latitude of the centre point. Use with centre longitude and radius.

## `centerLongitude` (type: `string`):

Longitude of the centre point.

## `radiusKm` (type: `string`):

Collect venues within this many kilometres of the centre point.

## `boundingBox` (type: `array`):

Most precise option. Tiled into several search points, then filtered to the box using each venue's real coordinates.

## `tileSizeKm` (type: `integer`):

Spacing of search points inside your area. Smaller = more thorough, more requests. Leave empty to choose automatically.

## `maxPlacesPerQuery` (type: `integer`):

Stop each query after this many venues. Each venue costs one extra request to fetch its popular-times curve.

## `placeUrls` (type: `array`):

Optional. Collect named venues directly instead of searching. Short links are followed automatically.

## `placeIds` (type: `array`):

Optional. Accepts a Place ID (ChIJ...), a numeric CID, or a raw feature ID (0x...:0x...).

## `outputFormat` (type: `string`):

Wide keeps the whole weekly curve nested inside one row per venue. Long emits one row per venue/day/hour, which is what you want for CSV, spreadsheets and dataframes - group by hour across venues without unpacking anything.

## `onlyPlacesWithPopularTimes` (type: `boolean`):

Most small venues have no foot-traffic data at all. Leave on to keep the dataset to venues that do; switch off to also record which venues were checked and had none.

## `requestDelaySeconds` (type: `string`):

Pause between requests. Google returns less data to an IP that has made many recent requests, so slowing down can improve how many venues come back with a curve. Raise it for large runs.

## `exportFormats` (type: `array`):

Also write the results to the key-value store in these formats. The Apify dataset is always produced regardless.

## `language` (type: `string`):

Google interface language. Affects busyness wording such as 'Usually a little busy'.

## `region` (type: `string`):

Two-letter country code biasing Google's results, e.g. US, ID, GB.

## `proxyConfiguration` (type: `object`):

Residential proxy is ON by default and this actor genuinely needs it. Google sends a reduced place record to datacenter IPs with the popular-times block stripped out entirely: measured on the Apify platform, the same run returned 0% coverage with no proxy and 62.5% coverage through RESIDENTIAL. Turning the proxy off will usually return no foot-traffic data at all. Note GOOGLE\_SERP cannot be used here - it does not tunnel arbitrary HTTPS.

## Actor input object example

```json
{
  "searchQueries": [
    "gym"
  ],
  "areaName": "jakarta",
  "maxPlacesPerQuery": 50,
  "outputFormat": "wide",
  "onlyPlacesWithPopularTimes": true,
  "requestDelaySeconds": "1.5",
  "exportFormats": [],
  "language": "en",
  "region": "US",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Every place record or hourly occupancy row produced by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchQueries": [
        "gym"
    ],
    "areaName": "jakarta"
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/google-maps-popular-times-analytics").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "searchQueries": ["gym"],
    "areaName": "jakarta",
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/google-maps-popular-times-analytics").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "searchQueries": [
    "gym"
  ],
  "areaName": "jakarta"
}' |
apify call fanndev/google-maps-popular-times-analytics --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fanndev/google-maps-popular-times-analytics"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/hMeYUlpd0nT9LYDOE/builds/tL8SfOMZWKVPgiTUo/openapi.json
