Your country label may describe the request, not the videos. If you're about to publish a Spotlight chart headed “Trending in the UK”, that's an uncomfortable distinction to discover after the graphic leaves your notebook.
Snapchat's anonymous web feed has no working country control. Passing country, countryCode, geo or region, changing locale, or adding country headers doesn't select a national feed.
The order can change between requests, and there's no working cursor to continue through it. The anonymous Spotlight feed provides no national sampling frame, so country-labelled requests cannot establish national trends.
The reusable problem is simple: an annotation about collection can quietly become a claim about the population collected.
Can this feed support a country label?
You need to preserve the label you're auditing without promoting it into a returned video attribute. A column heading can acquire remarkable authority after a few joins.
Dataspryva's feed Actor gives us a concrete way to examine that boundary.
Example: this collection plan assigns a country claim outside the Actor input; replace that annotation with the claim attached to your chart.
import os
import json
import pandas as pd
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"mode": "spotlightFeed",
"maxItems": 100,
"enrichVideos": True,
}
claimed_countries = ["GB", "GB"]
claimed_countries belongs to your analysis. It isn't sent to the Actor, and nothing in this setup makes it an observed location.
Keep that separation through the export, too:
Collection annotation Returned observation
claimed_country snapId
detectedLanguage
scrapedAt
\ /
\---- coverage audit -/
|
no automatic geography
A cap limits storage, not coverage
The selected mode reads the current anonymous public feed. It doesn't apply a topic restriction, and it doesn't establish complete coverage across content categories.
maxItems limits rows saved across the run. A cap can truncate what you retain; raising it doesn't expand the feed available to collect.
These are the distinctions that matter when you inspect the resulting rows:
| Evidence available to inspect | What it supports | What it doesn't establish |
|---|---|---|
snapId |
Identity matching across observations | A video's probability of entering the feed |
topicSource, rank
|
Returned surface provenance and position | A national popularity ranking |
detectedLanguage |
A description of content language | Creator residence or audience location |
uploadedAt, scrapedAt
|
Content and collection timestamps | Historical exposure during a reporting period |
relatedTags |
Content classification where supplied | Complete coverage of content categories |
Fields unavailable on a surface are omitted. Their absence isn't a negative finding about the video.
With enrichVideos enabled, detail requests can fill missing language and other metadata. Failed detail requests retain the original row, so this setting doesn't guarantee language completeness.
Empty snapshots belong in the collection record
A collection manifest should survive even when its dataset has no rows. Otherwise an empty observation disappears from your audit, leaving only the successful-looking intervals.
This cell records request boundaries separately from returned timestamps and retrieves the dataset through the streaming interface.
observations = []
manifest_rows = []
for snapshot_id, claimed_country in enumerate(claimed_countries):
started_at = pd.Timestamp.now(tz="UTC")
run = client.actor(
"dataspryva/snapchat-topic-spotlight-feed-and-content-tag"
).call(run_input=run_input)
finished_at = pd.Timestamp.now(tz="UTC")
if run["status"] != "SUCCEEDED":
raise RuntimeError(f"Collection stopped: {run['status']}")
saved_rows = 0
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
observations.append({**item, "_snapshot": snapshot_id})
saved_rows += 1
manifest_rows.append({
"_snapshot": snapshot_id,
"claimed_country": claimed_country,
"started_at": started_at,
"finished_at": finished_at,
"saved_rows": saved_rows,
})
manifest = pd.DataFrame(manifest_rows)
_snapshot is a local analysis column.
Request boundaries tell you when collection happened. They don't turn the interval between requests into continuous monitoring. For a reporting window, you can describe your collection schedule, but you can't reconstruct exposure between observations.
Surface provenance isn't geographic evidence
A coverage audit should preserve the evidence you actually have: video identities, content language, collection timestamps and the surface associated with each observation.
The normaliser below treats omitted values and blank strings as missing, keeps structured values inspectable as JSON, and parses timestamps without inventing replacements.
def normalise_value(value):
if isinstance(value, (dict, list)):
text = json.dumps(value, ensure_ascii=False, sort_keys=True)
elif pd.isna(value):
return pd.NA
else:
text = str(value)
return text.strip() or pd.NA
observed = pd.DataFrame(observations).reindex(columns=[
"_snapshot", "snapId", "detectedLanguage", "topicSource", "scrapedAt"
])
observed["_snapshot"] = pd.to_numeric(
observed["_snapshot"], errors="raise"
).astype("Int64")
for field in ["snapId", "detectedLanguage", "topicSource"]:
observed[field] = observed[field].map(normalise_value)
observed["scrapedAt"] = pd.to_datetime(
observed["scrapedAt"], utc=True, errors="coerce"
)
Now count saved appearances by returned surface, retaining missing provenance as a group rather than silently dropping it.
surface_audit = (
observed.groupby(["_snapshot", "topicSource"], dropna=False)
.size()
.rename("saved_observations")
.reset_index()
.merge(
manifest[
["_snapshot", "claimed_country", "started_at", "finished_at"]
],
on="_snapshot",
how="left",
validate="many_to_one",
)
)
surface_audit describes where observations came from. The country claim remains visibly attached to collection.
That distinction matters when you combine exports later. A manifest label can travel alongside every video without becoming evidence about any video's location.
Even when another dataset supplies a location variable, the referent matters: creator location, content location, viewer location and distribution market are different variables. We'd rather leave the geographic claim unresolved than let a familiar column name settle it.
Language shares need named denominators
A language chart can be defensible if its title names content language and its denominator is explicit.
Calculate both the availability of detectedLanguage among saved observations and the distribution among observations where that field is available. The count columns below are derived locally, rather than read from video records.
snapshot_counts = observed.groupby("_snapshot").agg(
language_available=("detectedLanguage", "count"),
id_available=("snapId", "count"),
distinct_identified_videos=("snapId", "nunique"),
valid_scraped_at=("scrapedAt", "count"),
)
language_audit = manifest.merge(
snapshot_counts, on="_snapshot", how="left"
)
count_columns = list(snapshot_counts.columns)
language_audit[count_columns] = (
language_audit[count_columns].fillna(0).astype("int64")
)
language_audit["language_availability"] = (
language_audit["language_available"]
/ language_audit["saved_rows"].replace(0, float("nan"))
)
language_rows = (
observed.dropna(subset=["detectedLanguage"])
.groupby(["_snapshot", "detectedLanguage"])
.size()
)
language_denominators = language_audit.set_index("_snapshot")
language_counts = pd.DataFrame({
"language_rows": language_rows,
"share_of_saved_observations": language_rows.div(
language_denominators["saved_rows"], level="_snapshot"
),
"share_with_language_available": language_rows.div(
language_denominators["language_available"], level="_snapshot"
),
}).reset_index()
Those shares answer different questions. One retains missing language in the denominator; the other describes only the labelled subset.
Neither becomes an audience-location estimate.
These are also observation counts, so a video encountered in repeated snapshots contributes repeated appearances. A distinct-video language analysis needs a separate rule for resolving inconsistent labels attached to the same identity.
That choice belongs in your method, not inside an unnoticed drop_duplicates().
Repeated videos measure recurrence
Repeated collection can reveal how much your observed feed repeats. It can't supply unknown inclusion probabilities or turn accumulated videos into a representative national sample.
The worked example uses Snapchat Topic Spotlight Feed & Content Tag Extractor.
This cell compares identity sets in collection order and tracks cumulative distinct identified videos.
identified = observed.dropna(subset=["snapId"]).drop_duplicates(
["_snapshot", "snapId"]
)
sets_by_snapshot = (
identified.groupby("_snapshot")["snapId"].agg(set).to_dict()
)
seen = set()
previous = None
overlap_rows = []
for snapshot_id in manifest["_snapshot"]:
current = sets_by_snapshot.get(snapshot_id, set())
comparable = bool(previous) and bool(current)
overlap_rows.append({
"_snapshot": snapshot_id,
"identified_videos": len(current),
"shared_with_previous": (
len(current & previous) if comparable else pd.NA
),
"jaccard_with_previous": (
len(current & previous) / len(current | previous)
if comparable else pd.NA
),
"new_identified_videos": len(current - seen),
"cumulative_distinct_identified_videos": len(seen | current),
})
seen |= current
previous = current
overlap_audit = manifest.merge(
pd.DataFrame(overlap_rows), on="_snapshot", how="left"
)
Jaccard overlap compares shared identities with the union of identities. Here it remains unreported when either snapshot lacks usable IDs.
The manifest keeps empty snapshots visible, while language_audit exposes identity availability. Read those alongside overlap: missing identifiers are a measurement limitation, not evidence of feed turnover.
A growing cumulative total means you've encountered additional identifiable videos. It doesn't tell you what fraction of any country's feed you've covered.
The caption carries the sampling claim
“Detected language among anonymous Spotlight feed observations” names a measurable subject.
“Trending videos in the UK” requires evidence this collection doesn't supply.
For a chart or report, state the collection interval, whether the unit is a saved appearance or a distinct identified video, and the denominator used for language shares. Include language availability alongside the distribution so readers can see how much content remains unclassified.
Keep analyst-assigned country labels in the collection method. If they appear as chart groups, readers can reasonably mistake them for observed geography.
The audience remains outside the frame
You can describe public-feed observations, content metadata and recurrence across snapshots. These measurements don't locate the people watching, establish where creators live or identify the market where a video is popular.
Engagement counts don't repair that gap. A count attached to a video doesn't allocate its audience to a country.
A country name is a claim about a population. It isn't decoration.
Top comments (0)