# Branch Coverage Drift Audit by Region and Category (`practical_ai_lab_jp/pal-maps-region-category-coverage-drift-audit`) Actor

Compare two dated branch-directory snapshots by region and category to find coverage gaps, missing branches and sampling drift.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-maps-region-category-coverage-drift-audit.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 $20.00 / 1,000 audit completeds

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

## Branch Coverage Drift Audit by Region and Category

Compare two dated snapshots of an authorized branch directory and see, per region and business category, where coverage fell, which branches disappeared, and whether the category mix drifted between snapshots.

The Actor works only on data you supply. It does not scrape Google Maps or any other site, makes no network requests to third-party sites, and uses no proxies.

### What it reports

Dataset rows have a `recordType`:

- `run_summary`: totals, counts of flagged cells and regions, warnings and limitations.
- `region_summary`: per-region counts and a category-mix shift index (total variation distance, 0 to 1).
- `region_category_cell`: per region/category counts, missing branches, new branches, branches reassigned to or from other cells, share shift in percentage points, and a status.
- `missing_branch`: branches present in the baseline but absent from the current snapshot (capped by `maxMissingBranchRows`).

Cell statuses: `coverage_gap`, `share_drift`, `low_sample`, `new_cell`, `stable`. Region statuses: `sampling_drift`, `low_sample`, `new_region`, `stable`.

A branch that appears in both snapshots under a different region or category is counted as reassigned, not missing, so relabeling does not look like a coverage loss.

### Sample input

```json
{
  "authorizationConfirmed": true,
  "baselineDate": "2026-01-31",
  "currentDate": "2026-04-30",
  "baselineRecords": [
    {"branchId": "b1", "name": "Harbor Cafe", "address": "1 Pier Rd", "region": "North", "category": "Cafe"},
    {"branchId": "b2", "name": "Pine Cafe", "address": "2 Pine St", "region": "North", "category": "Cafe"},
    {"branchId": "b3", "name": "Oak Cafe", "address": "3 Oak St", "region": "North", "category": "Cafe"},
    {"branchId": "b4", "name": "Elm Cafe", "address": "4 Elm St", "region": "North", "category": "Cafe"}
  ],
  "currentRecords": [
    {"branchId": "b1", "name": "Harbor Cafe", "address": "1 Pier Rd", "region": "North", "category": "Cafe"},
    {"branchId": "b2", "name": "Pine Cafe", "address": "2 Pine St", "region": "North", "category": "Cafe"}
  ],
  "minBaselineSample": 3,
  "gapPercentThreshold": 30,
  "minMissingBranches": 2
}
```

Here the North/Cafe cell loses 2 of 4 branches (50%) and is flagged `coverage_gap`.

Instead of inline records you can pass `baselineDatasetId` and `currentDatasetId` for existing Apify datasets you own. Field names are configurable (`regionField`, `categoryField`, `idField`, `nameField`, `addressField`).

### Input notes

- `authorizationConfirmed` must be `true`. `currentDate` must be later than `baselineDate`.
- Defaults in the input form (`minBaselineSample` 3, `minMissingBranches` 2, `gapPercentThreshold` 30) suit the tiny sample. For real data, start with a minimum sample of about 10 and a minimum of 3 missing branches, then tune.
- At most 50,000 records per snapshot are read (default 20,000) to keep memory low.

### Limitations

- It measures differences between your two snapshots, not completeness of any public map or real-world place list.
- "Missing" means absent from the current snapshot; the branch may be closed, renamed or just not collected.
- Without a stable ID, matching uses normalized name plus address, so renames or address edits show as one missing and one new branch.
- Small cells are unreliable. Cells under the minimum baseline sample are marked `low_sample` and never flagged as gaps; shifts are only computed for regions that meet the minimum in both snapshots.
- Thresholds are practical heuristics, not statistical significance tests. Results are not legal, financial or operational guarantees.

### Billing and errors

The code defines one pricing event, `audit-completed`, charged once after results are stored. Invalid input or any error ends the run without a charge. No payable plan is configured in this repository.

### Local tests

Tests run offline with the standard library: `python -m unittest discover -s tests -t .`

# Actor input Schema

## `authorizationConfirmed` (type: `boolean`):

Confirm that both snapshots are your own or otherwise authorized business directory data. Nothing is scraped by this Actor.

## `baselineDate` (type: `string`):

ISO date (YYYY-MM-DD) of the older snapshot.

## `currentDate` (type: `string`):

ISO date (YYYY-MM-DD) of the newer snapshot. Must be later than the baseline date.

## `baselineRecords` (type: `array`):

Array of branch objects from the older snapshot. Use this or Baseline dataset ID.

## `currentRecords` (type: `array`):

Array of branch objects from the newer snapshot. Use this or Current dataset ID.

## `baselineDatasetId` (type: `string`):

ID of an existing Apify dataset you own, used instead of inline baseline records.

## `currentDatasetId` (type: `string`):

ID of an existing Apify dataset you own, used instead of inline current records.

## `maxRecordsPerSnapshot` (type: `integer`):

Upper bound on records read per snapshot (hard cap 50000) to keep memory low.

## `regionField` (type: `string`):

Record key holding the region.

## `categoryField` (type: `string`):

Record key holding the business category.

## `idField` (type: `string`):

Record key with a stable branch ID. If empty in a record, name plus address is used.

## `nameField` (type: `string`):

Record key holding the branch name.

## `addressField` (type: `string`):

Record key holding the branch address.

## `minBaselineSample` (type: `integer`):

Cells or regions with fewer baseline branches are labelled low_sample and not flagged. The default of 10 is a heuristic; the sample input lowers it to 3 for demonstration.

## `gapPercentThreshold` (type: `number`):

A cell is a coverage gap when at least this share of its baseline branches is missing.

## `minMissingBranches` (type: `integer`):

Absolute number of missing branches required in addition to the percentage threshold.

## `shareShiftThresholdPoints` (type: `number`):

Flags share_drift when a category's share within a region moves by at least this many percentage points.

## `regionDriftThreshold` (type: `number`):

Flags sampling_drift when the total variation distance between category mixes reaches this value (0.01 to 1).

## `maxMissingBranchRows` (type: `integer`):

Caps the number of individual missing-branch rows written to the dataset.

## Actor input object example

```json
{
  "authorizationConfirmed": true,
  "baselineDate": "2026-01-31",
  "currentDate": "2026-04-30",
  "baselineRecords": [
    {
      "branchId": "b1",
      "name": "Harbor Cafe",
      "address": "1 Pier Rd",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b2",
      "name": "Pine Cafe",
      "address": "2 Pine St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b3",
      "name": "Oak Cafe",
      "address": "3 Oak St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b4",
      "name": "Elm Cafe",
      "address": "4 Elm St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b5",
      "name": "North Pharmacy A",
      "address": "5 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b6",
      "name": "North Pharmacy B",
      "address": "6 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b7",
      "name": "North Pharmacy C",
      "address": "7 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b8",
      "name": "Bay Cafe",
      "address": "8 Bay Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b9",
      "name": "Sun Cafe",
      "address": "9 Sun Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b10",
      "name": "Dune Cafe",
      "address": "10 Dune Ave",
      "region": "South",
      "category": "Cafe"
    }
  ],
  "currentRecords": [
    {
      "branchId": "b1",
      "name": "Harbor Cafe",
      "address": "1 Pier Rd",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b2",
      "name": "Pine Cafe",
      "address": "2 Pine St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b5",
      "name": "North Pharmacy A",
      "address": "5 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b6",
      "name": "North Pharmacy B",
      "address": "6 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b7",
      "name": "North Pharmacy C",
      "address": "7 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b8",
      "name": "Bay Cafe",
      "address": "8 Bay Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b9",
      "name": "Sun Cafe",
      "address": "9 Sun Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b11",
      "name": "Reef Cafe",
      "address": "11 Reef Ave",
      "region": "South",
      "category": "Cafe"
    }
  ],
  "maxRecordsPerSnapshot": 20000,
  "regionField": "region",
  "categoryField": "category",
  "idField": "branchId",
  "nameField": "name",
  "addressField": "address",
  "minBaselineSample": 3,
  "gapPercentThreshold": 30,
  "minMissingBranches": 2,
  "shareShiftThresholdPoints": 10,
  "regionDriftThreshold": 0.15,
  "maxMissingBranchRows": 500
}
```

# 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 = {
    "baselineRecords": [
        {
            "branchId": "b1",
            "name": "Harbor Cafe",
            "address": "1 Pier Rd",
            "region": "North",
            "category": "Cafe"
        },
        {
            "branchId": "b2",
            "name": "Pine Cafe",
            "address": "2 Pine St",
            "region": "North",
            "category": "Cafe"
        },
        {
            "branchId": "b3",
            "name": "Oak Cafe",
            "address": "3 Oak St",
            "region": "North",
            "category": "Cafe"
        },
        {
            "branchId": "b4",
            "name": "Elm Cafe",
            "address": "4 Elm St",
            "region": "North",
            "category": "Cafe"
        },
        {
            "branchId": "b5",
            "name": "North Pharmacy A",
            "address": "5 Main St",
            "region": "North",
            "category": "Pharmacy"
        },
        {
            "branchId": "b6",
            "name": "North Pharmacy B",
            "address": "6 Main St",
            "region": "North",
            "category": "Pharmacy"
        },
        {
            "branchId": "b7",
            "name": "North Pharmacy C",
            "address": "7 Main St",
            "region": "North",
            "category": "Pharmacy"
        },
        {
            "branchId": "b8",
            "name": "Bay Cafe",
            "address": "8 Bay Ave",
            "region": "South",
            "category": "Cafe"
        },
        {
            "branchId": "b9",
            "name": "Sun Cafe",
            "address": "9 Sun Ave",
            "region": "South",
            "category": "Cafe"
        },
        {
            "branchId": "b10",
            "name": "Dune Cafe",
            "address": "10 Dune Ave",
            "region": "South",
            "category": "Cafe"
        }
    ],
    "currentRecords": [
        {
            "branchId": "b1",
            "name": "Harbor Cafe",
            "address": "1 Pier Rd",
            "region": "North",
            "category": "Cafe"
        },
        {
            "branchId": "b2",
            "name": "Pine Cafe",
            "address": "2 Pine St",
            "region": "North",
            "category": "Cafe"
        },
        {
            "branchId": "b5",
            "name": "North Pharmacy A",
            "address": "5 Main St",
            "region": "North",
            "category": "Pharmacy"
        },
        {
            "branchId": "b6",
            "name": "North Pharmacy B",
            "address": "6 Main St",
            "region": "North",
            "category": "Pharmacy"
        },
        {
            "branchId": "b7",
            "name": "North Pharmacy C",
            "address": "7 Main St",
            "region": "North",
            "category": "Pharmacy"
        },
        {
            "branchId": "b8",
            "name": "Bay Cafe",
            "address": "8 Bay Ave",
            "region": "South",
            "category": "Cafe"
        },
        {
            "branchId": "b9",
            "name": "Sun Cafe",
            "address": "9 Sun Ave",
            "region": "South",
            "category": "Cafe"
        },
        {
            "branchId": "b11",
            "name": "Reef Cafe",
            "address": "11 Reef Ave",
            "region": "South",
            "category": "Cafe"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-maps-region-category-coverage-drift-audit").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 = {
    "baselineRecords": [
        {
            "branchId": "b1",
            "name": "Harbor Cafe",
            "address": "1 Pier Rd",
            "region": "North",
            "category": "Cafe",
        },
        {
            "branchId": "b2",
            "name": "Pine Cafe",
            "address": "2 Pine St",
            "region": "North",
            "category": "Cafe",
        },
        {
            "branchId": "b3",
            "name": "Oak Cafe",
            "address": "3 Oak St",
            "region": "North",
            "category": "Cafe",
        },
        {
            "branchId": "b4",
            "name": "Elm Cafe",
            "address": "4 Elm St",
            "region": "North",
            "category": "Cafe",
        },
        {
            "branchId": "b5",
            "name": "North Pharmacy A",
            "address": "5 Main St",
            "region": "North",
            "category": "Pharmacy",
        },
        {
            "branchId": "b6",
            "name": "North Pharmacy B",
            "address": "6 Main St",
            "region": "North",
            "category": "Pharmacy",
        },
        {
            "branchId": "b7",
            "name": "North Pharmacy C",
            "address": "7 Main St",
            "region": "North",
            "category": "Pharmacy",
        },
        {
            "branchId": "b8",
            "name": "Bay Cafe",
            "address": "8 Bay Ave",
            "region": "South",
            "category": "Cafe",
        },
        {
            "branchId": "b9",
            "name": "Sun Cafe",
            "address": "9 Sun Ave",
            "region": "South",
            "category": "Cafe",
        },
        {
            "branchId": "b10",
            "name": "Dune Cafe",
            "address": "10 Dune Ave",
            "region": "South",
            "category": "Cafe",
        },
    ],
    "currentRecords": [
        {
            "branchId": "b1",
            "name": "Harbor Cafe",
            "address": "1 Pier Rd",
            "region": "North",
            "category": "Cafe",
        },
        {
            "branchId": "b2",
            "name": "Pine Cafe",
            "address": "2 Pine St",
            "region": "North",
            "category": "Cafe",
        },
        {
            "branchId": "b5",
            "name": "North Pharmacy A",
            "address": "5 Main St",
            "region": "North",
            "category": "Pharmacy",
        },
        {
            "branchId": "b6",
            "name": "North Pharmacy B",
            "address": "6 Main St",
            "region": "North",
            "category": "Pharmacy",
        },
        {
            "branchId": "b7",
            "name": "North Pharmacy C",
            "address": "7 Main St",
            "region": "North",
            "category": "Pharmacy",
        },
        {
            "branchId": "b8",
            "name": "Bay Cafe",
            "address": "8 Bay Ave",
            "region": "South",
            "category": "Cafe",
        },
        {
            "branchId": "b9",
            "name": "Sun Cafe",
            "address": "9 Sun Ave",
            "region": "South",
            "category": "Cafe",
        },
        {
            "branchId": "b11",
            "name": "Reef Cafe",
            "address": "11 Reef Ave",
            "region": "South",
            "category": "Cafe",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-maps-region-category-coverage-drift-audit").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 '{
  "baselineRecords": [
    {
      "branchId": "b1",
      "name": "Harbor Cafe",
      "address": "1 Pier Rd",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b2",
      "name": "Pine Cafe",
      "address": "2 Pine St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b3",
      "name": "Oak Cafe",
      "address": "3 Oak St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b4",
      "name": "Elm Cafe",
      "address": "4 Elm St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b5",
      "name": "North Pharmacy A",
      "address": "5 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b6",
      "name": "North Pharmacy B",
      "address": "6 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b7",
      "name": "North Pharmacy C",
      "address": "7 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b8",
      "name": "Bay Cafe",
      "address": "8 Bay Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b9",
      "name": "Sun Cafe",
      "address": "9 Sun Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b10",
      "name": "Dune Cafe",
      "address": "10 Dune Ave",
      "region": "South",
      "category": "Cafe"
    }
  ],
  "currentRecords": [
    {
      "branchId": "b1",
      "name": "Harbor Cafe",
      "address": "1 Pier Rd",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b2",
      "name": "Pine Cafe",
      "address": "2 Pine St",
      "region": "North",
      "category": "Cafe"
    },
    {
      "branchId": "b5",
      "name": "North Pharmacy A",
      "address": "5 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b6",
      "name": "North Pharmacy B",
      "address": "6 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b7",
      "name": "North Pharmacy C",
      "address": "7 Main St",
      "region": "North",
      "category": "Pharmacy"
    },
    {
      "branchId": "b8",
      "name": "Bay Cafe",
      "address": "8 Bay Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b9",
      "name": "Sun Cafe",
      "address": "9 Sun Ave",
      "region": "South",
      "category": "Cafe"
    },
    {
      "branchId": "b11",
      "name": "Reef Cafe",
      "address": "11 Reef Ave",
      "region": "South",
      "category": "Cafe"
    }
  ]
}' |
apify call practical_ai_lab_jp/pal-maps-region-category-coverage-drift-audit --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-region-category-coverage-drift-audit"
        }
    }
}
```

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/Qy4l1S1JAQvyMqpRx/builds/uUn0OFcqqZknNguis/openapi.json
