Analysis of OpenStreetMap contributions from automated bots.
Analysis of monthly total, percentage, and accumulated bot edits and contributors.
# Monthly bot statistics with totals, percentages, and accumulated values
df = duckdb.sql("""
WITH monthly_total AS (
SELECT
year,
month,
CONCAT(year, '-', LPAD(CAST(month as VARCHAR), 2, '0')) as months,
COUNT(DISTINCT user_name) as total_contributors,
CAST(SUM(edit_count) as BIGINT) as total_edits
FROM '../changeset_data/year=*/month=*/*.parquet'
GROUP BY year, month
),
monthly_bot AS (
SELECT
year,
month,
CONCAT(year, '-', LPAD(CAST(month as VARCHAR), 2, '0')) as months,
COUNT(DISTINCT user_name) as bot_contributors,
CAST(SUM(edit_count) as BIGINT) as bot_edits
FROM '../changeset_data/year=*/month=*/*.parquet'
WHERE bot = true
GROUP BY year, month
)
SELECT
mt.months,
mt.year,
mt.month,
COALESCE(mb.bot_contributors, 0) as "Bot Contributors",
COALESCE(mb.bot_edits, 0) as "Bot Edits",
ROUND((COALESCE(mb.bot_contributors, 0) * 100.0) / mt.total_contributors, 2) as "Percent Bot Contributors",
ROUND((COALESCE(mb.bot_edits, 0) * 100.0) / mt.total_edits, 2) as "Percent Bot Edits",
FROM monthly_total mt
LEFT JOIN monthly_bot mb ON mt.year = mb.year AND mt.month = mb.month
ORDER BY mt.year, mt.month
""").df()
util.show_figure(
[
util.FigureConfig(
title="Monthly Bot Edits",
label="Total Edits",
x_col="months",
y_col="Bot Edits",
query_or_df=df,
),
util.FigureConfig(
title="Percentage of Bot Edits",
label="Percent Edits",
x_col="months",
y_col="Percent Bot Edits",
y_unit_hover_template="%",
query_or_df=df,
),
util.FigureConfig(
title="Monthly Bot Contributors",
label="Total Contributors",
x_col="months",
y_col="Bot Contributors",
query_or_df=df,
),
util.FigureConfig(
title="Percentage of Bot Contributors",
label="Percent Contributors",
x_col="months",
y_col="Percent Bot Contributors",
y_unit_hover_template="%",
query_or_df=df,
),
]
)
Interactive table showing yearly edits and contributors for each bot editing software.
# Yearly statistics for bot editing software
query = """
WITH user_first_year AS (
SELECT
user_name,
created_by,
MIN(year) as first_year
FROM '../changeset_data/year=*/month=*/*.parquet'
WHERE created_by IS NOT NULL AND bot = true
GROUP BY user_name, created_by
),
software_totals AS (
SELECT
created_by as "Editing Software",
CAST(SUM(edit_count) as BIGINT) as total_edits_all_time,
CAST(SUM(CASE WHEN year >= 2021 THEN edit_count ELSE 0 END) as BIGINT) as total_edits_2021_now,
CAST(COUNT(DISTINCT user_name) as BIGINT) as total_contributors_all_time,
CAST(COUNT(DISTINCT CASE WHEN year >= 2021 THEN user_name END) as BIGINT) as total_contributors_2021_now
FROM '../changeset_data/year=*/month=*/*.parquet'
WHERE created_by IS NOT NULL AND bot = true
GROUP BY created_by
),
yearly_metrics AS (
SELECT
d.year,
d.created_by as "Editing Software",
CAST(SUM(d.edit_count) as BIGINT) as "Edits",
CAST(COUNT(DISTINCT d.user_name) as BIGINT) as "Contributors",
CAST(COUNT(DISTINCT CASE WHEN ufy.first_year = d.year THEN d.user_name END) as BIGINT) as "New Contributors"
FROM '../changeset_data/year=*/month=*/*.parquet' d
LEFT JOIN user_first_year ufy
ON d.user_name = ufy.user_name AND d.created_by = ufy.created_by
WHERE d.created_by IS NOT NULL AND d.bot = true
GROUP BY d.year, d.created_by
)
SELECT
ym.year,
ym."Editing Software",
ym."Edits",
ym."New Contributors",
ym."Contributors",
st.total_edits_all_time as "Total Edits",
st.total_edits_2021_now as "Total Edits (2021 - Now)",
st.total_contributors_all_time as "Total Contributors",
st.total_contributors_2021_now as "Total Contributors (2021 - Now)"
FROM yearly_metrics ym
JOIN software_totals st
ON ym."Editing Software" = st."Editing Software"
ORDER BY year DESC, "Edits" DESC
"""
df = duckdb.sql(query).df()
# Get top editing software by total edits and contributors
top_edits = df.groupby("Editing Software")["Total Edits"].first().nlargest(100)
top_edits_2021_now = df.groupby("Editing Software")["Total Edits (2021 - Now)"].first().nlargest(100)
top_contributors = df.groupby("Editing Software")["Total Contributors"].first().nlargest(100)
top_contributors_2021_now = df.groupby("Editing Software")["Total Contributors (2021 - Now)"].first().nlargest(100)
table_configs = [
util.TableConfig(
title="Top 100 Bot Editing Software by Edits",
query_or_df=df[df["Editing Software"].isin(top_edits.index)],
x_axis_col="year",
y_axis_col="Editing Software",
value_col="Edits",
center_columns=["Rank", "Editing Software"],
sum_col="Total Edits",
),
util.TableConfig(
title="Top 100 Bot Editing Software by Edits 2021 - Now",
query_or_df=df[(df["Editing Software"].isin(top_edits_2021_now.index)) & (df["year"] >= 2021)],
x_axis_col="year",
y_axis_col="Editing Software",
value_col="Edits",
center_columns=["Rank", "Editing Software"],
sum_col="Total Edits (2021 - Now)",
),
util.TableConfig(
title="Top 100 Bot Editing Software by Contributors",
query_or_df=df[df["Editing Software"].isin(top_contributors.index)],
x_axis_col="year",
y_axis_col="Editing Software",
value_col="Contributors",
center_columns=["Rank", "Editing Software"],
sum_col="Total Contributors",
),
util.TableConfig(
title="Top 100 Bot Editing Software by Contributors 2021 - Now",
query_or_df=df[(df["Editing Software"].isin(top_contributors_2021_now.index)) & (df["year"] >= 2021)],
x_axis_col="year",
y_axis_col="Editing Software",
value_col="Contributors",
center_columns=["Rank", "Editing Software"],
sum_col="Total Contributors (2021 - Now)",
),
]
util.show_tables(table_configs)
| Rank | Editing Software | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | Total Edits |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | osmtools | 0 | 0 | 488,405 | 1,199,363 | 1,241,094 | 6,820,108 | 15,029,385 | 2,655,466 | 3,271,936 | 7,558,536 | 6,454,470 | 8,151,944 | 12,399,003 | 29,128,241 | 2,154,313 | 1,546,179 | 98,098,443 |
| 2 | Redaction bot | 0 | 0 | 21,303,266 | 284,119 | 102,887 | 81,142 | 195,753 | 278,968 | 323,820 | 308,288 | 3,375,623 | 33,835 | 8,164 | 521,134 | 1,471 | 0 | 26,818,470 |
| 3 | autoAWS | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2,674,425 | 76,127 | 178,939 | 133,574 | 2,635,334 | 87,343 | 66,735 | 69,521 | 5,921,998 |
| 4 | JOSM | 3,943 | 59,844 | 602,095 | 997,656 | 13,511 | 3,350 | 0 | 134,189 | 120,598 | 218 | 52,828 | 3,267 | 2,158,485 | 285 | 46,430 | 20,103 | 4,216,802 |
| 5 | Roy | 0 | 0 | 1,216,065 | 2,940,171 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4,156,236 |
| 6 | FindvejBot | 3,600,197 | 358,760 | 104,606 | 175 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4,063,738 |
| 7 | osmapi | 0 | 0 | 0 | 0 | 71,233 | 0 | 556,109 | 3 | 15,143 | 53,062 | 14,674 | 281,782 | 107,001 | 501,978 | 789,476 | 187,369 | 2,577,830 |
| 8 | upload.py | 2,045,740 | 0 | 267 | 75,897 | 3,793 | 2,717 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2,128,414 |
| 9 | osmapis | 0 | 0 | 127,237 | 343,781 | 327,188 | 137,592 | 307,640 | 95,443 | 64,731 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,403,612 |
| 10 | bash script | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 124 | 7,548 | 1,237,987 | 1 | 0 | 80 | 0 | 1,245,740 |
| 11 | https_all_the_things | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 443,396 | 11,218 | 10,430 | 62,062 | 365,444 | 76,678 | 28,265 | 997,493 |
| 12 | bulk_upload.py | 0 | 0 | 0 | 0 | 0 | 0 | 785,031 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 785,031 |
| 13 | FixKarlsruheSchema | 261,188 | 426,449 | 3,941 | 0 | 0 | 0 | 132 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 691,710 |
| 14 | OsmPipeline | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,566 | 650,018 | 0 | 0 | 0 | 0 | 0 | 651,584 |
| 15 | osmupload.py | 0 | 0 | 0 | 433,989 | 0 | 16,731 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 450,720 |
| 16 | FixTypo7 | 0 | 207,292 | 47,914 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 255,206 |
| 17 | FixDoubleNodes | 165,239 | 76,666 | 11,259 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 253,164 |
| 18 | PeoriaJanitor | 195,442 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 195,442 |
| 19 | TrimTags | 182,601 | 10,483 | 266 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 193,350 |
| 20 | David Nicholson | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 190,054 | 0 | 0 | 0 | 0 | 0 | 0 | 190,054 |
| 21 | Osmaxil | 0 | 0 | 0 | 0 | 0 | 49,004 | 0 | 118,268 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 167,272 |
| 22 | gtfs2osm v1.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 74,878 | 74,878 |
| 23 | FixStraßeDeAt | 20,251 | 5,988 | 17,563 | 0 | 0 | 0 | 5,727 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 49,529 |
| 24 | GNU Emacs | 0 | 0 | 367 | 26,581 | 14,424 | 5,969 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 47,341 |
| 25 | OSMFixCanada | 46,307 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 46,307 |
| 26 | PythonOsmApi | 0 | 35,843 | 5,295 | 5,139 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 46,277 |
| 27 | gtfs2osm v0.9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 31,176 | 475 | 31,651 |
| 28 | FixRussianAddress | 0 | 20,634 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 20,634 |
| 29 | railwayz.info photolines engine | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 14,923 | 0 | 0 | 0 | 0 | 0 | 0 | 14,923 |
| 30 | FixTMC | 7,591 | 2,646 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 10,237 |
| 31 | AWSbot | 0 | 0 | 0 | 0 | 0 | 8,750 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8,750 |
| 32 | bulk_upload_sax.py | 7,761 | 0 | 920 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8,681 |
| 33 | MoveSourceKeyToChangeset | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8,359 | 0 | 8,359 |
| 34 | FixTypo6 | 7,972 | 175 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8,147 |
| 35 | FixRomanianDiacritics | 1,482 | 4,729 | 1,019 | 0 | 0 | 0 | 699 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 7,929 |
| 36 | bash scripts | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 7,586 | 0 | 0 | 0 | 0 | 0 | 0 | 7,586 |
| 37 | rocketdata.io | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 5,083 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 5,083 |
| 38 | ValidatePhoneNumbers 11432ff0dc | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4,153 | 0 | 4,153 |
| 39 | Codingi.be osmapi/4.0.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4,014 | 0 | 4,014 |
| 40 | treemap/0.1.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3,526 | 3,526 |
| 41 | osmbot 0.1.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2,485 | 0 | 0 | 0 | 0 | 0 | 2,485 |
| 42 | FixStraßeHyphen | 2,423 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2,423 |
| 43 | 0 | 1,819 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,819 | |
| 44 | Python/3.9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,812 | 1,812 |
| 45 | FixStrasseCh | 535 | 868 | 380 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,783 |
| 46 | OSM-Wikipedia bot | 152 | 0 | 1,536 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,688 |
| 47 | script mairies5962 | 0 | 0 | 0 | 1,528 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,528 |
| 48 | FixHighwayRefs | 816 | 593 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,409 |
| 49 | R 0.6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,118 | 0 | 0 | 1,118 |
| 50 | osmuser63783_script 0.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1,050 | 0 | 0 | 1,050 |
| 51 | IsraelHiking.osm.org.il | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 763 | 0 | 763 |
| 52 | Custom Script | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 588 | 588 |
| 53 | ValidatePhoneNumbers 1f5a1d42a6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 572 | 0 | 572 |
| 54 | osmfetch | 0 | 393 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 393 |
| 55 | FixEscapes | 0 | 242 | 14 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 256 |
| 56 | Everydoorbot | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 185 | 54 | 239 |
| 57 | myPyOSM-alpha | 0 | 0 | 0 | 221 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 221 |
| 58 | 小璋流同步機器人 (osmapi/0.6.0) | 0 | 0 | 0 | 0 | 0 | 212 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 212 |
| 59 | business.zoon.ru | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 24 | 170 | 0 | 0 | 0 | 0 | 0 | 194 |
| 60 | machina_reparanda | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 22 | 126 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 148 |
| 61 | tiger-expansion-bot | 0 | 0 | 111 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 111 |
| 62 | https://github.com/OsmSharp/osm-api-client/ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 9 | 0 | 0 | 0 | 0 | 55 | 64 |
| 63 | Fixing railways stations data | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 52 | 0 | 0 | 0 | 0 | 0 | 0 | 52 |
| 64 | SearchAroundBot | 0 | 0 | 0 | 0 | 0 | 0 | 9 | 17 | 5 | 0 | 2 | 6 | 2 | 1 | 2 | 0 | 44 |
| 65 | Kort (osmapi/0.2.23) | 0 | 0 | 0 | 0 | 36 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 36 |
| 66 | FFMChecker | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 35 | 0 | 35 |
| 67 | scosmol | 0 | 0 | 0 | 32 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 32 |
| 68 | BagBot | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 11 | 0 | 0 | 11 |
| 69 | Kikimora | 0 | 0 | 0 | 0 | 0 | 4 | 6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 10 |
| 70 | osm_gobot/0.1.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 9 | 0 | 0 | 0 | 0 | 0 | 9 |
| 71 | update-osm-fips.py | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 5 | 5 |
| 72 | NeisEditSupport v.0.4.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 73 | manchesterclimate.com 1.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 74 | pyxbot | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
World map showing the total number of edits made by bots across different geographical locations.
# Geographic distribution of bot edits
df = duckdb.sql("""
SELECT
mid_pos_x as x,
mid_pos_y as y,
SUM(edit_count) as z
FROM '../changeset_data/year=*/month=*/*.parquet'
WHERE mid_pos_x IS NOT NULL AND mid_pos_y IS NOT NULL AND bot = true
GROUP BY mid_pos_x, mid_pos_y
""").df()
util.show_figure(
[
util.FigureConfig(
title="Geographic Distribution of Bot Edits",
x_col="x",
y_col="y",
z_col="z",
query_or_df=df,
plot_type="map",
)
]
)