Editing Software Statistics

Analysis of OpenStreetMap contributors and edits by editing software.

Top 10 Editing Software

code
df = duckdb.sql("""
WITH top_software AS (
    SELECT created_by
    FROM (
        SELECT
            created_by,
            COUNT(DISTINCT user_name) as total_contributors
        FROM '../changeset_data/year=*/month=*/*.parquet'
        WHERE created_by IS NOT NULL
        GROUP BY created_by
        ORDER BY total_contributors DESC
        LIMIT 10
    )
),
user_first_appearance AS (
    SELECT 
        user_name,
        year,
        month,
        created_by,
        ROW_NUMBER() OVER (PARTITION BY user_name ORDER BY year, month) as rn
    FROM (
        SELECT DISTINCT user_name, year, month, created_by
        FROM '../changeset_data/year=*/month=*/*.parquet'
        WHERE created_by IN (SELECT created_by FROM top_software)
    )
),
first_appearances AS (
    SELECT user_name, year, month, created_by
    FROM user_first_appearance 
    WHERE rn = 1
),
monthly_contributors AS (
    SELECT 
        year,
        month,
        CONCAT(year, '-', LPAD(CAST(month as VARCHAR), 2, '0')) as months,
        created_by,
        COUNT(DISTINCT user_name) as "Contributors",
        SUM(edit_count) as "Edit Count"
    FROM '../changeset_data/year=*/month=*/*.parquet'
    WHERE created_by IN (SELECT created_by FROM top_software)
    GROUP BY year, month, created_by
),
monthly_new_contributors AS (
    SELECT
        year,
        month,
        created_by,
        COUNT(DISTINCT user_name) as "New Contributors"
    FROM first_appearances
    GROUP BY year, month, created_by
),
base_data AS (
    SELECT
        m.year,
        m.month,
        m.months,
        m.created_by,
        m."Contributors",
        COALESCE(n."New Contributors", 0) as "New Contributors",
        m."Edit Count"
    FROM monthly_contributors m
    LEFT JOIN monthly_new_contributors n ON m.year = n.year AND m.month = n.month AND m.created_by = n.created_by
)
SELECT
    months,
    created_by,
    "Contributors",
    "New Contributors",
    "Edit Count",
    SUM("New Contributors") OVER (
        PARTITION BY created_by 
        ORDER BY year, month 
        ROWS UNBOUNDED PRECEDING
    ) as "Accumulated Contributors",
    SUM("Edit Count") OVER (
        PARTITION BY created_by 
        ORDER BY year, month 
        ROWS UNBOUNDED PRECEDING
    ) as "Accumulated Edits"
FROM base_data
ORDER BY year, month, created_by
""").df()

util.show_figure(
    [
        util.FigureConfig(
            title="Monthly Contributors by Top 10 Editing Software",
            label="Contributors",
            x_col="months",
            y_col="Contributors",
            group_col="created_by",
            query_or_df=df,
        ),
        util.FigureConfig(
            title="Monthly New Contributors by Top 10 Editing Software",
            label="New Contributors",
            x_col="months",
            y_col="New Contributors",
            group_col="created_by",
            query_or_df=df,
        ),
        util.FigureConfig(
            title="Monthly Edit Count by Top 10 Editing Software",
            label="Edit Count",
            x_col="months",
            y_col="Edit Count",
            group_col="created_by",
            query_or_df=df,
        ),
        util.FigureConfig(
            title="Accumulated Contributors by Top 10 Editing Software",
            label="Accumulated Contributors",
            x_col="months",
            y_col="Accumulated Contributors",
            group_col="created_by",
            query_or_df=df,
        ),
        util.FigureConfig(
            title="Accumulated Edits by Top 10 Editing Software",
            label="Accumulated Edits",
            x_col="months",
            y_col="Accumulated Edits",
            group_col="created_by",
            query_or_df=df,
        ),
    ],
)

Top 100 Editing Software Yearly

code
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
	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
	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
	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()

with open("../config/replace_rules_created_by.json") as f:
    editing_software_name_to_html_link = {
        name: f'<a href="{item["link"]}">{name}</a>' for name, item in json.load(f).items() if "link" in item
    }

df["Editing Software"] = df["Editing Software"].apply(
    lambda name: editing_software_name_to_html_link[name] if name in editing_software_name_to_html_link else name
)

top_100_edits = df.groupby("Editing Software")["Total Edits"].first().nlargest(100)
top_100_contributors = df.groupby("Editing Software")["Total Contributors"].first().nlargest(100)
top_100_edits_2021_now = df.groupby("Editing Software")["Total Edits (2021 - Now)"].first().nlargest(100)
top_100_contributors_2021_now = df.groupby("Editing Software")["Total Contributors (2021 - Now)"].first().nlargest(100)

table_configs = [
    util.TableConfig(
        title="Top 100 Contributors",
        query_or_df=df[df["Editing Software"].isin(top_100_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 Contributors 2021 - Now",
        query_or_df=df[(df["Editing Software"].isin(top_100_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.TableConfig(
        title="Top 100 Edits All Time",
        query_or_df=df[df["Editing Software"].isin(top_100_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 Edits 2021 - Now",
        query_or_df=df[(df["Editing Software"].isin(top_100_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.show_tables(table_configs)
Rank Editing Software 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 Total Contributors
1 iD 0 0 0 0 56,176 125,149 133,826 148,447 194,537 214,057 203,438 241,197 239,537 204,579 210,136 205,427 212,959 193,974 1,776,032
2 Potlatch 59,540 69,047 81,896 107,156 72,954 29,604 24,283 14,901 10,184 6,598 5,043 3,531 608 350 234 147 100 62 358,320
3 MAPS.ME 0 0 0 0 0 0 0 96,584 102,375 71,199 55,719 35,483 19,974 15,515 16,231 7,729 3,279 2,889 323,638
4 JOSM 13,755 18,797 20,232 23,441 23,140 21,862 22,796 22,315 23,191 22,762 22,801 22,109 21,512 20,537 19,741 18,127 17,121 15,496 158,081
5 StreetComplete 0 0 0 0 0 0 0 7 9,572 9,764 8,967 11,695 21,935 23,525 26,809 27,111 31,412 33,227 104,748
6 Organic Maps 0 0 0 0 0 0 0 0 0 0 0 0 3,017 6,258 10,557 12,765 19,307 25,674 59,409
7 OsmAnd 0 192 645 1,129 1,653 1,927 2,354 3,534 4,862 5,982 7,063 6,977 7,959 8,623 8,544 7,875 7,513 6,534 42,175
8 Vespucci 58 237 460 957 1,622 1,801 2,075 2,379 2,920 3,331 3,632 4,314 4,539 5,025 5,371 5,322 5,469 5,216 27,538
9 Rapid 0 0 0 0 0 0 0 0 0 0 1,105 3,709 2,124 2,183 9,121 3,662 4,233 2,625 22,420
10 Go Map!! 0 0 0 0 2,205 2,046 1,335 1,508 1,834 2,685 3,074 3,152 3,748 3,720 3,830 4,162 4,231 3,910 21,407
11 DeFlock 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 17,617 17,617
12 CoMaps 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4,952 11,786 14,676
13 Every Door 0 0 0 0 0 0 0 0 0 0 0 0 0 1,992 3,675 4,324 5,089 4,781 12,668
14 rosemary 0 0 0 762 1,532 1,723 2,761 2,487 2,345 1,990 72 33 13 7 3 0 0 0 10,646
15 Map builder 0 0 0 0 0 0 0 0 0 0 0 0 1 1,506 1,722 6,333 0 0 9,399
16 MapComplete 0 0 0 0 0 0 0 0 0 0 0 353 718 1,250 1,410 1,545 2,715 3,212 8,587
17 Merkaartor 2,262 2,784 2,193 1,687 1,090 597 456 409 332 256 241 264 219 185 148 114 65 44 8,349
18 OsmHydrant 0 0 0 0 1 344 665 751 866 934 1,166 1,409 1,386 1,456 1,589 1,100 470 44 8,095
19 StreetComplete_ee 0 0 0 0 0 0 0 0 0 0 0 0 0 414 1,977 2,606 2,573 2,458 5,655
20 gnome-maps 0 0 0 0 0 0 2 247 421 472 556 766 588 475 424 575 797 613 5,396
21 DeFlock 2.10.1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4,399 4,399
22 AED Map 0 0 0 0 0 0 0 0 0 0 0 0 0 0 129 696 1,642 893 3,206
23 Mapzen 403 2,026 942 96 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3,161
24 Mapzen POI Collector 466 1,656 1,346 138 102 67 9 4 3 2 0 0 0 0 0 0 0 0 3,140
25 Pushpin 0 0 0 389 998 1,113 780 461 230 15 0 0 0 0 0 0 0 0 2,928
26 DeFlock 2.3.1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 429 2,683 2,863
27 OpenMaps 0 610 682 977 386 232 182 99 28 0 0 0 0 0 0 0 0 0 2,833
28 openaedmap.org 0 0 0 0 0 0 0 0 0 0 0 0 0 88 213 578 1,309 905 2,777
29 Osm Go! 0 0 0 0 0 0 0 2 2 46 194 346 695 580 539 587 480 266 2,475
30 Pic4Review 0 0 0 0 0 0 0 0 0 250 517 456 443 480 310 210 120 59 2,286
31 Osmose Editor 0 0 0 0 0 157 224 249 284 275 377 444 377 293 333 313 330 240 2,073
32 osm-revert 0 1 0 0 0 0 0 0 0 0 0 0 0 0 462 825 959 702 2,031
33 wheelmap.org 0 266 1,137 354 0 0 0 0 0 0 0 0 0 0 2 3 3 2 1,634
34 RevertUI 0 0 0 0 0 0 0 23 58 76 83 189 254 322 382 302 320 302 1,624
35 Level0 0 0 0 0 0 109 174 205 222 233 252 290 266 320 338 342 356 316 1,578
36 IsraelHiking.osm.org.il 0 0 0 0 0 0 0 0 88 131 153 303 359 301 243 235 150 0 1,477
37 MapContrib 0 0 0 0 0 0 3 97 243 243 314 401 143 155 124 19 0 0 1,454
38 OSM Contributor 0 0 0 0 0 0 0 0 198 424 443 289 177 56 23 6 0 0 1,326
39 POI+ 0 0 0 0 394 622 377 70 25 2 0 0 0 0 0 0 0 0 1,268
40 OsmInEdit 0 0 0 0 0 0 0 0 0 0 77 172 172 170 186 169 226 144 1,117
41 DeFlock 1.5.1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1,086 28 1,094
42 OpenStop 0 0 0 0 0 0 0 0 0 0 0 0 0 45 337 424 357 282 1,059
43 MapRoulette 0 0 0 0 0 0 0 0 0 0 41 77 131 323 251 180 148 152 1,043
44 DeFlock 2.9.0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1,013 1,013
45 iLOE 131 327 332 343 141 51 4 1 0 0 0 0 0 0 0 0 0 0 976
46 OpenAEDMap Website 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 932 932
47 FireYak 0 0 0 0 0 0 0 21 131 158 221 251 214 172 109 43 0 0 865
48 DeFlock 2.1.3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 833 43 861
49 osmapi 0 0 0 0 0 42 48 72 69 67 76 235 116 75 72 60 65 29 848
50 DeFlock 2.7.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 836 836
51 DeFlock 1.3.2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 821 24 832
52 DeFlock 2.7.1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 813 813
53 https://osm.wikidata.link/ 0 0 0 0 0 0 0 0 66 82 160 113 108 107 189 229 209 110 782
54 AED Map App 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 766 766
55 ArcGIS Editor for OpenStreetMap 0 45 107 101 61 67 67 156 87 41 40 39 24 23 17 6 0 0 725
56 RawEdit 0 45 100 181 147 129 122 161 148 63 0 0 0 0 0 0 0 0 704
57 Cartes.app 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 82 647 698
58 OSMyBiz 0 0 0 0 0 0 0 0 0 6 109 109 97 70 83 102 51 2 619
59 https://aed.openstreetmap.org.pl 0 0 0 0 0 0 0 0 0 0 0 0 0 318 283 38 0 0 604
60 QGIS OSM 31 157 121 157 112 15 0 0 1 0 0 0 0 0 0 0 0 1 543
61 BigTinCan Upload Script 170 187 101 86 12 0 0 0 0 0 0 0 0 0 0 0 0 0 519
62 openaedmap-backend 0 0 0 0 0 0 0 0 0 0 0 0 0 0 21 100 256 216 510
63 OSMPOIEditor 0 0 0 336 214 1 1 0 1 0 0 0 0 0 0 0 0 0 504
64 OSMapTuner 0 0 0 242 178 66 41 0 0 0 0 0 0 0 0 0 0 0 472
65 DeFlock 2.0.0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 443 8 448
66 Zaczero/osm-relatify 0 0 0 0 0 0 0 0 0 0 0 0 0 0 118 156 190 135 393
67 DeFlock 1.2.8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 370 10 371
68 YAPIS 0 0 0 231 47 39 32 27 25 13 0 0 0 0 0 0 0 0 362
69 OpenSeaMap-Editor 18 114 100 124 20 0 0 0 0 0 0 0 0 0 0 0 0 0 353
70 iD-indoor 0 0 0 0 0 0 0 71 102 67 82 30 12 13 14 3 0 0 342
71 osm for ruby 0 0 0 335 1 0 0 0 0 0 0 0 0 0 0 0 0 0 336
72 osmtools 61 108 53 42 32 30 28 34 36 25 23 17 17 20 20 14 10 14 316
73 ProjetDuMois.fr 0 0 0 0 0 0 0 0 0 0 0 74 163 93 57 18 9 0 305
74 OsmAPP 0 0 0 0 0 0 0 0 0 0 0 0 13 18 16 57 141 96 281
75 Mundi App 0 0 0 0 0 0 0 0 0 0 0 0 56 229 7 5 0 0 270
76 PLanes 0 0 0 0 0 0 0 0 0 66 69 43 39 61 43 34 15 3 269
77 https://israelhiking.osm.org.il 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 155 110 242
78 rocketdata.io 0 0 0 0 0 0 0 0 0 234 0 0 0 0 0 0 0 0 234
79 My Opening Hours 0 0 0 0 89 89 70 37 20 7 0 0 0 0 0 0 0 0 234
80 Osm.Org Tags Editor 0 0 0 0 0 0 0 0 0 0 0 0 0 68 75 120 130 130 231
81 Grass_and_Green 0 0 0 0 0 0 188 43 0 0 0 0 0 0 0 0 0 0 227
82 meta 0 30 61 84 41 15 1 0 0 0 0 0 0 0 0 0 0 0 223
83 Jungle Bus 0 0 0 0 0 0 0 0 100 79 44 35 5 3 1 1 0 0 216
84 peundemerg.ro 0 0 0 0 0 0 7 27 34 42 48 52 43 39 37 28 0 0 209
85 FairwayMapper.com 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 197 197
86 Deriviste 0 0 0 0 0 0 0 0 0 45 99 27 12 0 4 0 0 0 159
87 SketchOSM 0 0 0 0 0 0 0 0 0 60 87 16 3 1 0 0 0 0 158
88 CityZen 0 0 0 0 0 0 0 0 34 65 50 11 4 1 5 2 0 0 156
89 Geocropping 0 0 0 0 0 0 0 0 50 37 29 18 18 17 12 1 0 0 146
90 better osm.org 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 17 61 117 145
91 upload.py 16 15 15 25 16 12 7 4 17 8 4 8 7 9 4 6 4 3 139
92 Refill Südtirol / Alto Adige 0 0 0 0 0 0 0 0 0 0 0 72 15 16 55 14 8 0 139
93 OsmoTagger 0 0 0 0 0 0 0 0 0 0 0 0 0 0 17 48 54 47 139
94 bulk_upload.py 65 38 17 6 5 3 6 3 1 3 1 2 1 0 0 1 0 0 132
95 osm-api-js 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 120 122
96 OnWheels 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 42 58 34 122
97 PythonOsmApi 19 36 22 41 25 6 2 3 1 0 0 0 0 0 0 0 0 0 122
98 Phone, Names and Opening Hours Report 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 111 111
99 ALARMiator Mobile 0 0 0 0 0 0 0 0 0 0 0 0 0 15 49 45 27 26 111
100 Osmose Raw Editor 93 54 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 109

Monthly Percentage of Contributors by Top 10 Editing Software

code
df = duckdb.sql("""
WITH top_software AS (
	SELECT created_by
	FROM (
		SELECT
			created_by,
			COUNT(DISTINCT user_name) as total_contributors
		FROM '../changeset_data/year=*/month=*/*.parquet'
		WHERE created_by IS NOT NULL
		GROUP BY created_by
		ORDER BY total_contributors DESC
		LIMIT 10
	)
),
monthly_software_contributors AS (
	SELECT 
		CONCAT(year, '-', LPAD(CAST(month as VARCHAR), 2, '0')) as months,
		created_by,
		COUNT(DISTINCT user_name) as contributors
	FROM '../changeset_data/year=*/month=*/*.parquet'
	WHERE created_by IN (SELECT created_by FROM top_software)
	GROUP BY year, month, created_by
),
monthly_total_contributors AS (
	SELECT 
		CONCAT(year, '-', LPAD(CAST(month as VARCHAR), 2, '0')) as months,
		COUNT(DISTINCT user_name) as total_contributors
	FROM '../changeset_data/year=*/month=*/*.parquet'
	WHERE created_by IS NOT NULL
	GROUP BY year, month
)
SELECT 
	msc.months,
	msc.created_by,
	ROUND((msc.contributors * 100.0) / mtc.total_contributors, 2) as 'Percentage of Contributors'
FROM monthly_software_contributors msc
JOIN monthly_total_contributors mtc ON msc.months = mtc.months
ORDER BY msc.months, msc.created_by""").df()

util.show_figure(
    [
        util.FigureConfig(
            title="Monthly Percentage of Contributors by Top 10 Editing Software",
            x_col="months",
            y_col="Percentage of Contributors",
            y_unit_hover_template="%",
            group_col="created_by",
            query_or_df=df,
        ),
    ],
)

Monthly Contributors by Device Type

code
df_device_metrics = duckdb.sql("""
SELECT
    CONCAT(year, '-', LPAD(CAST(month as VARCHAR), 2, '0')) as months,
    device_type,
    COUNT(DISTINCT user_name) as Contributors,
    CAST(SUM(edit_count) as BIGINT) as "Edit Count"
FROM '../changeset_data/year=*/month=*/*.parquet'
WHERE device_type IS NOT NULL
GROUP BY year, month, device_type
ORDER BY year, month, device_type
""").df()

util.show_figure(
    [
        util.FigureConfig(
            title="Monthly Contributors by Device Type",
            label="Contributors",
            x_col="months",
            y_col="Contributors",
            group_col="device_type",
            query_or_df=df_device_metrics,
        ),
        util.FigureConfig(
            title="Monthly Edit Count by Device Type",
            label="Edit Count",
            x_col="months",
            y_col="Edit Count",
            group_col="device_type",
            query_or_df=df_device_metrics,
        ),
    ],
)
code
df = duckdb.sql("""
WITH top_software AS (
	SELECT created_by
	FROM (
		SELECT
			created_by,
			COUNT(DISTINCT user_name) as total_contributors
		FROM '../changeset_data/year=*/month=*/*.parquet'
		WHERE created_by IS NOT NULL
		GROUP BY created_by
		ORDER BY total_contributors DESC
		LIMIT 10
	)
),
user_first_edit AS (
	SELECT 
		user_name,
		ARG_MIN(created_by, changeset_id) as first_created_by,
		ARG_MIN(year * 100 + month, changeset_id) as year_month
	FROM '../changeset_data/year=*/month=*/*.parquet'
	WHERE created_by IS NOT NULL
	GROUP BY user_name
),
first_software_only AS (
	SELECT 
		user_name,
		first_created_by as created_by,
		CONCAT(CAST(year_month // 100 as VARCHAR), '-', LPAD(CAST(year_month % 100 as VARCHAR), 2, '0')) as months
	FROM user_first_edit
	WHERE first_created_by IN (SELECT created_by FROM top_software)
),
monthly_first_software_counts AS (
	SELECT 
		months,
		created_by,
		COUNT(DISTINCT user_name) as first_time_users
	FROM first_software_only
	GROUP BY months, created_by
)
SELECT 
	months,
	created_by,
	first_time_users as 'First Time Contributors'
FROM monthly_first_software_counts
ORDER BY months""").df()
util.show_figure(
    [
        util.FigureConfig(
            title="Top 10 First Editing Software Per Month",
            x_col="months",
            y_col="First Time Contributors",
            group_col="created_by",
            query_or_df=df,
        ),
    ],
)