Data Platform/Data Lake/Edits/Editor counts per page
The
wmf_contributors.editor_counts_per_page
dataset holds distinct
visible
editor counts for every
(wiki_id, page_id)
across all Wikimedia wikis — broken down into total, permanent (registered human), bot, logged-out (anonymous), and temporary-account editors. It is built from the
Incremental MediaWiki History
table by a daily pipeline, stored as an Iceberg table in the Data Lake, and served per page from a Cassandra-backed
Data Gateway
endpoint.
- Ticket: T426316
- Design Document
-
dbt selector:
+editor_counts_per_page( dbt-repo ) - Airflow DAG: editor_counts_per_page_daily_dag.py .
User documentation
For each page on each wiki, the dataset stores the number of distinct editors who have made at least one publicly visible revision to that page, split by editor type:
- Total — all distinct visible editors (humans and bots).
- Permanent — distinct visible registered (permanent-account) human editors.
- Bot — distinct visible editors classified as bots.
- Logged-out — distinct visible anonymous (IP-based) human editors.
- Temporary — distinct visible temporary-account human editors.
A "distinct editor" is counted once per page regardless of how many revisions they made. Editors are identified by a stable key (central user id when available, otherwise a local user id, otherwise an anonymous identity) so the same registered user is not double-counted across renames.
Schema
The Data Lake table (
wmf_contributors.editor_counts_per_page
) and the per-page API expose the same columns. One row per
(wiki_id, page_id)
.
| column | data type | description |
|---|---|---|
wiki_id
|
string |
Wiki database code (e.g.
enwiki
,
cawiki
). Part of the primary key.
|
page_id
|
bigint | MediaWiki page identifier within the wiki. Part of the primary key. |
page_is_deleted
|
boolean |
true
when the page is deleted in MediaWiki History.
false
when page is not deleted.
null
when no deletion information is available.
|
editor_total_count
|
int | Distinct visible editors on this page (humans and bots), excluding cross-wiki editors. |
editor_permanent_count
|
int | Distinct visible permanent (registered) human editors, excluding cross-wiki editors. |
editor_bot_count
|
int | Distinct visible bot editors, excluding cross-wiki editors. |
editor_logged_out_count
|
int | Distinct visible anonymous (IP-based) human editors, excluding cross-wiki editors. |
editor_temporary_count
|
int | Distinct visible temporary-account human editors, excluding cross-wiki editors. |
loaded_at
|
timestamp |
Load watermark — when this row was written by the pipeline (the pipeline run's
data_interval_end
on incremental runs, current UTC on a full refresh).
Not
the underlying MediaWiki History update time.
|
Access
-
Attribution API
: The
Attribution API
is the preferred access method for MediaWiki clients, allowing lookups by
wiki_idandpage_id. -
Data Lake (Iceberg):
WMF staff can also use the
wmf_contributors.editor_counts_per_pagetable in dbt models, Jupyter notebooks and Superset dashboards. Also queryable directly from Presto / Spark for ad-hoc analysis. See examples below.
For other use cases, such as accessing this data from within a MediaWiki extension or exposing it through a different API, please contact the MediaWiki Interfaces and/or Data Engineering teams.
Examples
Attribution API
See more usage in mw:Attribution_API documentation. This API is in development and will incorporate editor counts soon. Learn more in the diff post .
Example API call with curl:
curl 'https://en.wikipedia.org/w/rest.php/attribution/v0-beta/pages/Earth/signals?expand=trust_and_relevance' | jq
The output looks like the following as of beta v0. We are interested in the
trust_and_relevance.contributor_counts
. This will be updated to include the unique editor counts by permanent, bot, temp, and anonymous accounts.
{
"essential": {
"link": "https://en.wikipedia.org/w/index.php?title=Earth&wprov=afsw1",
"default_brand_marks": [
{
"name": "Default logo",
"url": "https://en.wikipedia.org/static/images/project-logos/enwiki-25.png",
"type": "logo"
}...
],
"source_wiki": {
"site_name": "English Wikipedia",
"project_family": "wikipedia",
"site_id": "enwiki",
"site_language": "en",
"page_language": "en"
},
"title": "Earth",
"license": {
"title": "CC BY-SA 4.0",
"url": "https://creativecommons.org/licenses/by-sa/4.0/deed.en"
}
},
"trust_and_relevance": {
"last_updated": "2026-07-02T17:36:24Z",
"contributor_counts": null,
"page_views": 244510,
"reference_count": 310,
"trending": {
"top": {
"read": false,
"edited": false,
"read_and_edited": false
},
"relative": {
"read": false,
"edited": false,
"read_and_edited": false
}
}
}
}
TBD: API response will be updated after Attribution API incorporates the editor counts.
dbt models
Sample dbt model that lists all pages with more than 100 unique editors. The editor counts table is accessed by
ref('editor_counts_per_page')
.
{{ config(materialized='table') }}
SELECT
wiki_id,
page_id,
editor_total_count,
editor_permanent_count,
editor_bot_count,
editor_logged_out_count,
editor_temporary_count
FROM {{ ref('editor_counts_per_page') }}
WHERE wiki_id = 'enwiki'
AND editor_total_count > 100
SQL
We can use PySpark/Spark or Presto from the terminal in a stat machine; execute python or scala code in stat machines; run SQL directly in Superset ; or code in Jupyter Notebooks to run SQL or equivalent code to access this dataset.
Counts for a single page
SELECT
editor_total_count,
editor_permanent_count,
editor_bot_count,
editor_logged_out_count,
editor_temporary_count
FROM wmf_contributors.editor_counts_per_page
WHERE wiki_id = 'cawiki'
AND page_id = 148945;
Top 20 pages on a wiki by distinct editors
SELECT
page_id,
editor_total_count
FROM wmf_contributors.editor_counts_per_page
WHERE wiki_id = 'enwiki'
ORDER BY editor_total_count DESC
LIMIT 20;
Details
- Visible revisions only. Counts include only editors with at least one publicly visible (non-suppressed, non-deleted) revision. A visibility change on an old revision can therefore change a page's counts.
- Cross-wiki editors are excluded from all counts, until MediaWiki History exposes reliable user-status for them ( phab:T428018 ).
-
loaded_atis a load timestamp, not a data-freshness timestamp. It marks when the pipeline wrote the row, not when the underlying edit happened or when MediaWiki History last changed. Data freshness can be determined from when the latest airflow job ran. - New-wiki completeness gap. A wiki created mid-month can miss events until the next monthly namespace snapshot.
-
~1 day producer latency.
Data of a day is collected and processed at the start of the next day. So we get updated data of day
Dat the start of dayD+1. - ~2-3 days latency on first of month. The editor counts pipeline depends on incremental MWH, which in turn depends on the monthly MWH. The monthly MWH take 2-3 days to land. The 1st of the month of incremental MWH waits on this load before loading the data for the 1st of the month. Hence editor counts pipeline also waits ~2-3 days, after which the data from 1st of the month is loaded, followed by subsequent days.
- Editor is bot. An editor is considered a bot if the editor belongs to a user group bot (bot by group) or has the term "bot" in the name (bot by name).
-
Bot as of event-time.
We use
event_user_is_bot_by_historical(bot status as of event time), as current bot status is not available in incremental MWH. This means if someone was deemed bot later on (or vice versa), we won't know that until that user makes a new change in that page. If we receive an update for that user, we update the bot field. We are able to take on the latest value of bot-ness (if available). More details here: phab:T426316#11979835 . -
page_is_deletedis not comprehensive and may contain NULL values. MWH could not identify the status of those pages, hence they are NULL.
Technical documentation
The dataset is produced by a chain of three dbt models and one Airflow DAG, then loaded into Cassandra for per-page serving. A Data Gateway API acts as an interface for the Cassandra table to be used for serving into applications.
incremental MediaWiki History (wmf_mediawiki.mediawiki_history_incremental_v1)
│
▼
stg_mwh__revision_editor (view) one row per visible revision, with editor identity/type
│
▼
int_editors_per_page (Iceberg) one row per (wiki_id, page_id, editor_key)
│
▼
editor_counts_per_page (Iceberg) one row per (wiki_id, page_id) with the count columns
│
▼ (post-hook)
Cassandra linked_artifacts.editor_counts_per_page ──► Data Gateway API
The original design for this pipeline can be found in a publicly-viewable Google document , kept for historical purposes.
Source: incremental MediaWiki History
The chain reads the incremental MediaWiki History table
wmf_mediawiki.mediawiki_history_incremental_v1
rather than the monthly
wmf.mediawiki_history
snapshot. This gives
daily freshness
from a single table. Every touched row carries a
row_update_dt
watermark — re-stamped on inserts and on revert / visibility / page-deletion back-patches — so a single column captures every change the model cares about, including a visibility flip on an old revision.
dbt Models
-
wmf_contributors.stg_mwh__revision_editor(view) -
Selects visible revisions from incremental MWH and derives each revision's editor identity (
editor_key) and type flags (anonymous / temporary / permanent / bot / cross-wiki / visible). Incremental runs filter on a closed-rightrow_update_dtwindow(data_interval_start, data_interval_end], with a 1st-of-month−1sreach-back to fold in the monthly reconcile. See phab:T428503#12003010 . -
wmf_contributors.int_editors_per_page(Iceberg incremental table) -
Aggregates staging to one row per
(wiki_id, page_id, editor_key). Lists all unique editors of a page with the latest-revision identity/type flags and a page-leveleditor_is_visible/page_is_deleted. Cross-wiki editors are retained here. -
wmf_contributors.editor_counts_per_page(Iceberg incremental table) -
Computes the distinct editor counts over visible, non-cross-wiki editors, one row per
(wiki_id, page_id). Incremental runs recount only pages touched in the currentint_editors_per_pageload, thenMERGEinto Iceberg. A post-hook loads the run's rows to Cassandra.
Incremental design and read window
The watermark is the Airflow
logical_date
interval itself — there is no stored stateful watermark, so retries,
catchup
, and backfills are reproducible because the read set is a pure function of the interval.
-
Normal day:
row_update_dt ∈ (data_interval_start, data_interval_end]→ reads the run's own day (fresh). -
1st of month:
the lower bound reaches back one second (
>= data_interval_start − 1s) to also capture the monthly reconcile, which is stamped at the last second of the prior month. -
Touched-key re-aggregation:
the model finds
(wiki_id, page_id, editor_key)touched in the window, then re-aggregates all revisions for those keys (not an append), so a changed revision correctly re-derives the page's counts.
Orchestration
One Airflow DAG, editor_counts_per_page_daily_dag.py :
-
Schedule:
@daily,max_active_runs=1(the incremental-merge model must run one at a time). -
Dependency:
an
ExternalTaskSensoron the incremental MWH daily writer (dataseticeberg_wmf_mediawiki_mediawiki_history_incremental_v1) at the same logical date. The 1st-of-month run is transitively gated on the prior month's reconcile through that writer, so no separate monthly job and no manual full refresh are needed. -
SLA:
4 days — the 1st-of-month run is full-refresh-sized and can wait ~3 days on the monthly reconcile; with
max_active_runs=1the next runs queue behind it. The job lives in its own DAG (not the shareddbt_daily) so this wait does not block other teams. -
Load step:
on success the dbt post-hook writes the run's rows to Cassandra (
aqs.linked_artifacts.editor_counts_per_page). The Cassandra data is accessible through a Data Gateway endpoint. See usage below. -
A manual
--full-refreshrebuilds the whole table and re-pushes all rows.
Usage
See more usage examples in Data_Platform/Data_Lake/Edits/Editor_counts_per_page#Examples .
Data Gateway
Data Gateway
is a wrapper on the Cassandra table and allows single-page lookups by
(wiki_id, page_id)
, backed by
Cassandra
. See the
Data Gateway documentation
for the endpoint contract. This is only to be used by internal APIs and not for direct access by external applications or APIs.
$ curl https://data-gateway.discovery.wmnet:30443/public/linked_artifacts/editor_counts_per_page/cawiki/148945 | jq
{
"rows": [
{
"wiki_id": "cawiki",
"page_id": 148945,
"editor_bot_count": 0,
"editor_logged_out_count": 0,
"editor_permanent_count": 2,
"editor_temporary_count": 0,
"editor_total_count": 2,
"loaded_at": "2026-06-25 23:29:52.000Z",
"page_is_deleted": false
}
]
}
Operational notes
- 1st-of-month run is full-refresh-sized — it re-aggregates the entire re-inserted snapshot and re-pushes the whole table to Cassandra.
-
Computes all touched pages
—
row_update_dtis re-stamped on any field change with back-patches. All such pages get re-aggregated irrespective of what changed.
Changes and known problems
| Date from | Task | Details |
|---|---|---|
| 2026-06-25 | task T426316 | Initial data set. |