# Local Category & Location Trends from Place Data (`practical_ai_lab_jp/pal-maps-local-category-trends`) Actor

Compare two snapshots of existing place datasets to find where business categories are growing or shrinking by region. Reports count and share changes, with sample-size caveats. No live Google Maps scraping.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-maps-local-category-trends.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** Lead generation, SEO tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 category trend rows

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

## Local Category & Location Trends from Place Data

Turn two or more dated place datasets you already have into a clear picture of how business categories are distributed across locations, and how that distribution changed between snapshots. Every row carries sample-size warnings, so thin data is not mistaken for a trend.

This Actor **analyzes data you supply**. It does not scrape Google Maps or any other website and makes no requests to third-party sites.

### What it does

1. Reads each snapshot (inline rows or an existing Apify dataset you can access).
2. Removes duplicate places inside each snapshot (by place ID, CID or URL, otherwise by name + address).
3. Counts places per category and per location (country, state, city or postal code), plus an all-locations total.
4. Compares consecutive snapshots (or first vs last) and reports count change, category share within each location, and share change in percentage points.
5. Flags each row as `likely_shift`, `no_clear_change` or `insufficient_sample`.

### Typical uses

- Local-market analysts checking whether a category is growing or shrinking in specific cities.
- Agencies and franchise teams tracking category mix in territories they already export data for.
- Anyone who has periodic place exports and needs a repeatable change report instead of manual spreadsheet pivots.

### Input

| Field | Meaning |
| --- | --- |
| `snapshots` | At least two objects with `date` (YYYY-MM-DD or YYYY-MM), optional `label`, and either `items` (rows) or `datasetId`. |
| `geoLevel` | `country`, `state`, `city` (default) or `postalCode`. |
| `comparisonMode` | `consecutive` (default) or `first_vs_last`. |
| `minGeoSample` | Minimum unique places per location in both snapshots (default 30). |
| `minCellCount` | Minimum places in a category in at least one snapshot (default 5). |
| `minShareChangePct` | Minimum share movement, in points, to call a likely shift (default 2). |
| `maxRowsPerComparison` | Output cap per snapshot pair (default 500). |
| `maxItemsPerSnapshot` | Read cap per snapshot (default 50,000). |

Recognised row fields: category from `categoryName`, `category`, `mainCategory`, `primaryCategory`, `type` or the first entry of `categories`; location from `countryCode`/`country`, `state`/`region`/`province`, `city`/`town`/`locality`, `postalCode`/`zip`/`postcode`.

Example input (also the default prefill):

```json
{
  "snapshots": [
    {"date": "2026-01-01", "label": "January export", "items": [
      {"placeId": "a1", "categoryName": "Coffee shop", "city": "Springfield"},
      {"placeId": "a3", "categoryName": "Bakery", "city": "Springfield"}
    ]},
    {"date": "2026-04-01", "label": "April export", "datasetId": "YOUR_DATASET_ID"}
  ],
  "geoLevel": "city",
  "minGeoSample": 30
}
```

The prefilled sample is tiny on purpose and will show `insufficient_sample` warnings; use your real exports for meaningful results.

### Output

The default dataset holds three row types (`recordType`):

- `category_geo_change` – one row per location and category per comparison, with counts, shares, `shareChangePoints`, `direction` (`new`, `dropped`, `up`, `down`, `flat`), `zScore`, `signal` and `warnings`.
- `comparison_summary` – totals per snapshot pair, including likely-shift counts and truncation notes.
- `snapshot_summary` – per snapshot: input rows, unique places, duplicates removed, missing category/location percentages and warnings.

### Warnings explained

- `low_location_sample` – a location has fewer unique places than `minGeoSample` in either snapshot.
- `low_category_count` – the category has fewer than `minCellCount` places in both snapshots.
- `coverage_mismatch` – one snapshot has more than twice the unique places of the other; the snapshots probably do not cover the same area or search, so changes may reflect coverage rather than the market.
- `small_snapshot`, `many_missing_categories`, `many_missing_locations`, `many_duplicates`, `empty_snapshot`, `location_missing_in_one_snapshot`, `rows_truncated`.

### Limitations

- Results describe **your datasets**, not the whole market. If two exports were collected with different searches, areas or limits, differences may be artifacts of collection.
- Category names are compared as lowercase text. Different labels for the same business type are not merged, and no translation is done.
- Rows without a recognised category or location are counted under `(unknown)`, and the percentage is reported in the snapshot summary.
- Locations are read from structured fields only; addresses are not parsed.
- The z-score is a rough indicator. Snapshots overlap (the same places appear in both), which violates the independence assumption, so it is not a formal significance test. Small samples can still produce misleading shares.
- Places without any ID or name + address cannot be de-duplicated.
- At most 24 snapshots and 200,000 rows per snapshot are read.

### Pricing

The code defines one pay-per-event event name, `category-trend-row`, applied to `category_geo_change` rows only. Summary rows are free. Invalid input fails the run before any rows are produced, so failed runs produce no charged events. The actual price is set by the publisher in Apify Console and is not part of this repository.

### Data and compliance

Only use data you are entitled to process. Place datasets can contain business contact details; this Actor reads only category and location fields for counting and does not output individual place records.

# Actor input Schema

## `snapshots` (type: `array`):

At least two snapshots. Each has a 'date' (YYYY-MM-DD or YYYY-MM), an optional 'label', and EITHER inline 'items' (place rows) OR a 'datasetId' of an existing Apify dataset you can access. Rows need a category field (categoryName, category or categories) and a location field (city, state, countryCode or postalCode). Place data is never fetched from any website.

## `geoLevel` (type: `string`):

Which location field to group by.

## `comparisonMode` (type: `string`):

Compare each snapshot with the next one, or only the first with the last.

## `minGeoSample` (type: `integer`):

Rows whose location has fewer unique places than this in either snapshot are flagged as insufficient sample.

## `minCellCount` (type: `integer`):

Rows where the category has fewer places than this in both snapshots are flagged as insufficient sample.

## `minShareChangePct` (type: `number`):

A row is only called a likely shift if its category share moved at least this many points.

## `maxRowsPerComparison` (type: `integer`):

Caps output per snapshot pair (strongest signals first). This also bounds cost.

## `maxItemsPerSnapshot` (type: `integer`):

Safety cap on rows read from each snapshot.

## Actor input object example

```json
{
  "snapshots": [
    {
      "date": "2026-01-01",
      "label": "January export",
      "items": [
        {
          "placeId": "a1",
          "categoryName": "Coffee shop",
          "city": "Springfield"
        },
        {
          "placeId": "a2",
          "categoryName": "Coffee shop",
          "city": "Springfield"
        },
        {
          "placeId": "a3",
          "categoryName": "Bakery",
          "city": "Springfield"
        },
        {
          "placeId": "a4",
          "categoryName": "Gym",
          "city": "Rivertown"
        },
        {
          "placeId": "a5",
          "categoryName": "Coffee shop",
          "city": "Rivertown"
        }
      ]
    },
    {
      "date": "2026-04-01",
      "label": "April export",
      "items": [
        {
          "placeId": "a1",
          "categoryName": "Coffee shop",
          "city": "Springfield"
        },
        {
          "placeId": "a3",
          "categoryName": "Bakery",
          "city": "Springfield"
        },
        {
          "placeId": "b1",
          "categoryName": "Bakery",
          "city": "Springfield"
        },
        {
          "placeId": "a4",
          "categoryName": "Gym",
          "city": "Rivertown"
        },
        {
          "placeId": "b2",
          "categoryName": "Gym",
          "city": "Rivertown"
        },
        {
          "placeId": "b3",
          "categoryName": "Yoga studio",
          "city": "Rivertown"
        }
      ]
    }
  ],
  "geoLevel": "city",
  "comparisonMode": "consecutive",
  "minGeoSample": 30,
  "minCellCount": 5,
  "minShareChangePct": 2,
  "maxRowsPerComparison": 500,
  "maxItemsPerSnapshot": 50000
}
```

# Actor output Schema

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

Download rows from the latest run in JSON/CSV.

# 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 = {
    "snapshots": [
        {
            "date": "2026-01-01",
            "label": "January export",
            "items": [
                {
                    "placeId": "a1",
                    "categoryName": "Coffee shop",
                    "city": "Springfield"
                },
                {
                    "placeId": "a2",
                    "categoryName": "Coffee shop",
                    "city": "Springfield"
                },
                {
                    "placeId": "a3",
                    "categoryName": "Bakery",
                    "city": "Springfield"
                },
                {
                    "placeId": "a4",
                    "categoryName": "Gym",
                    "city": "Rivertown"
                },
                {
                    "placeId": "a5",
                    "categoryName": "Coffee shop",
                    "city": "Rivertown"
                }
            ]
        },
        {
            "date": "2026-04-01",
            "label": "April export",
            "items": [
                {
                    "placeId": "a1",
                    "categoryName": "Coffee shop",
                    "city": "Springfield"
                },
                {
                    "placeId": "a3",
                    "categoryName": "Bakery",
                    "city": "Springfield"
                },
                {
                    "placeId": "b1",
                    "categoryName": "Bakery",
                    "city": "Springfield"
                },
                {
                    "placeId": "a4",
                    "categoryName": "Gym",
                    "city": "Rivertown"
                },
                {
                    "placeId": "b2",
                    "categoryName": "Gym",
                    "city": "Rivertown"
                },
                {
                    "placeId": "b3",
                    "categoryName": "Yoga studio",
                    "city": "Rivertown"
                }
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-maps-local-category-trends").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 = { "snapshots": [
        {
            "date": "2026-01-01",
            "label": "January export",
            "items": [
                {
                    "placeId": "a1",
                    "categoryName": "Coffee shop",
                    "city": "Springfield",
                },
                {
                    "placeId": "a2",
                    "categoryName": "Coffee shop",
                    "city": "Springfield",
                },
                {
                    "placeId": "a3",
                    "categoryName": "Bakery",
                    "city": "Springfield",
                },
                {
                    "placeId": "a4",
                    "categoryName": "Gym",
                    "city": "Rivertown",
                },
                {
                    "placeId": "a5",
                    "categoryName": "Coffee shop",
                    "city": "Rivertown",
                },
            ],
        },
        {
            "date": "2026-04-01",
            "label": "April export",
            "items": [
                {
                    "placeId": "a1",
                    "categoryName": "Coffee shop",
                    "city": "Springfield",
                },
                {
                    "placeId": "a3",
                    "categoryName": "Bakery",
                    "city": "Springfield",
                },
                {
                    "placeId": "b1",
                    "categoryName": "Bakery",
                    "city": "Springfield",
                },
                {
                    "placeId": "a4",
                    "categoryName": "Gym",
                    "city": "Rivertown",
                },
                {
                    "placeId": "b2",
                    "categoryName": "Gym",
                    "city": "Rivertown",
                },
                {
                    "placeId": "b3",
                    "categoryName": "Yoga studio",
                    "city": "Rivertown",
                },
            ],
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-maps-local-category-trends").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 '{
  "snapshots": [
    {
      "date": "2026-01-01",
      "label": "January export",
      "items": [
        {
          "placeId": "a1",
          "categoryName": "Coffee shop",
          "city": "Springfield"
        },
        {
          "placeId": "a2",
          "categoryName": "Coffee shop",
          "city": "Springfield"
        },
        {
          "placeId": "a3",
          "categoryName": "Bakery",
          "city": "Springfield"
        },
        {
          "placeId": "a4",
          "categoryName": "Gym",
          "city": "Rivertown"
        },
        {
          "placeId": "a5",
          "categoryName": "Coffee shop",
          "city": "Rivertown"
        }
      ]
    },
    {
      "date": "2026-04-01",
      "label": "April export",
      "items": [
        {
          "placeId": "a1",
          "categoryName": "Coffee shop",
          "city": "Springfield"
        },
        {
          "placeId": "a3",
          "categoryName": "Bakery",
          "city": "Springfield"
        },
        {
          "placeId": "b1",
          "categoryName": "Bakery",
          "city": "Springfield"
        },
        {
          "placeId": "a4",
          "categoryName": "Gym",
          "city": "Rivertown"
        },
        {
          "placeId": "b2",
          "categoryName": "Gym",
          "city": "Rivertown"
        },
        {
          "placeId": "b3",
          "categoryName": "Yoga studio",
          "city": "Rivertown"
        }
      ]
    }
  ]
}' |
apify call practical_ai_lab_jp/pal-maps-local-category-trends --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,practical_ai_lab_jp/pal-maps-local-category-trends"
        }
    }
}
```

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/Bt0HAL9cyhAJFZzfS/builds/PUp6RdTe9hT3hAOEb/openapi.json
