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 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 184,748 1,635,362
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 90 358,306
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 2,677 321,210
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 15,707 152,566
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 28,052 87,886
6 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 6,932 39,346
7 Organic Maps 0 0 0 0 0 0 0 0 0 0 0 0 3,017 6,258 10,557 12,765 16,987 37,401
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,007 25,218
9 Rapid 0 0 0 0 0 0 0 0 0 0 1,105 3,709 2,124 2,183 9,121 3,662 3,807 20,513
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 3,901 19,724
11 rosemary 0 0 0 762 1,532 1,723 2,761 2,487 2,345 1,990 72 33 13 7 3 0 0 10,646
12 Every Door 0 0 0 0 0 0 0 0 0 0 0 0 0 1,992 3,675 4,324 4,656 9,867
13 Map builder 0 0 0 0 0 0 0 0 0 0 0 0 1 1,506 1,722 6,333 0 9,399
14 Merkaartor 2,262 2,784 2,193 1,687 1,090 597 456 409 332 256 241 264 219 185 148 114 57 8,335
15 OsmHydrant 0 0 0 0 1 344 665 751 866 934 1,166 1,409 1,386 1,456 1,589 1,100 469 8,092
16 MapComplete 0 0 0 0 0 0 0 0 0 0 0 353 718 1,250 1,410 1,545 2,301 5,790
17 StreetComplete_ee 0 0 0 0 0 0 0 0 0 0 0 0 0 414 1,977 2,606 2,441 4,779
18 gnome-maps 0 0 0 0 0 0 2 247 421 472 556 766 588 475 424 575 665 4,723
19 CoMaps 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3,804 3,804
20 Mapzen 403 2,026 942 96 0 0 0 0 0 0 0 0 0 0 0 0 0 3,161
21 Mapzen POI Collector 466 1,656 1,346 138 102 67 9 4 3 2 0 0 0 0 0 0 0 3,140
22 Pushpin 0 0 0 389 998 1,113 780 461 230 15 0 0 0 0 0 0 0 2,928
23 OpenMaps for iOS 0 164 678 977 386 232 182 99 28 0 0 0 0 0 0 0 0 2,433
24 Osm Go! 0 0 0 0 0 0 0 2 2 46 194 346 695 580 539 587 441 2,336
25 Pic4Review 0 0 0 0 0 0 0 0 0 250 517 456 443 480 310 210 108 2,232
26 Osmose Editor 0 0 0 0 0 157 224 249 284 275 377 444 377 293 333 313 290 1,947
27 openaedmap.org 0 0 0 0 0 0 0 0 0 0 0 0 0 88 213 578 1,071 1,788
28 wheelmap.org 0 266 1,137 354 0 0 0 0 0 0 0 0 0 0 0 0 0 1,629
29 osm-revert 0 1 0 0 0 0 0 0 0 0 0 0 0 0 462 825 850 1,583
30 IsraelHiking.osm.org.il 0 0 0 0 0 0 0 0 88 131 153 303 359 301 243 235 150 1,477
31 MapContrib 0 0 0 0 0 0 3 97 243 243 314 401 143 155 124 19 0 1,454
32 Level0 0 0 0 0 0 109 174 205 222 233 252 290 266 320 338 342 322 1,429
33 RevertUI 0 0 0 0 0 0 0 23 58 76 83 189 254 322 382 302 272 1,371
34 OSM Contributor 0 0 0 0 0 0 0 0 198 424 443 289 177 56 23 6 0 1,326
35 POI+ 0 0 0 0 394 622 377 70 25 2 0 0 0 0 0 0 0 1,268
36 OsmInEdit 0 0 0 0 0 0 0 0 0 0 77 172 172 170 186 169 196 982
37 iLOE 131 327 332 343 141 51 4 1 0 0 0 0 0 0 0 0 0 976
38 MapRoulette 0 0 0 0 0 0 0 0 0 0 41 77 131 323 251 180 126 908
39 FireYak 0 0 0 0 0 0 0 21 131 158 221 251 214 172 109 43 0 865
40 OpenStop 0 0 0 0 0 0 0 0 0 0 0 0 0 45 337 424 315 856
41 osmapi 0 0 0 0 0 42 48 72 69 67 76 235 116 75 72 60 59 831
42 ArcGIS Editor for OpenStreetMap 0 45 107 101 61 67 67 156 87 41 40 39 24 23 17 6 0 725
43 https://osm.wikidata.link/ 0 0 0 0 0 0 0 0 66 82 160 113 108 107 189 229 189 718
44 RawEdit 0 45 100 181 147 129 122 161 148 63 0 0 0 0 0 0 0 704
45 OSMyBiz 0 0 0 0 0 0 0 0 0 6 109 109 97 70 83 102 51 618
46 https://aed.openstreetmap.org.pl 0 0 0 0 0 0 0 0 0 0 0 0 0 318 283 38 0 604
47 QGIS OSM 31 157 121 157 112 15 0 0 1 0 0 0 0 0 0 0 0 542
48 AED Map for Android/iOS (aedmap v1.0.50) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 541 541
49 BigTinCan Upload Script 170 187 101 86 12 0 0 0 0 0 0 0 0 0 0 0 0 519
50 OSMPOIEditor 0 0 0 336 214 1 1 0 1 0 0 0 0 0 0 0 0 504
51 OSMapTuner 0 0 0 242 178 66 41 0 0 0 0 0 0 0 0 0 0 472
52 OpenMaps iPhone 0 381 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 385
53 YAPIS 0 0 0 231 47 39 32 27 25 13 0 0 0 0 0 0 0 362
54 OpenSeaMap-Editor 18 114 100 124 20 0 0 0 0 0 0 0 0 0 0 0 0 353
55 iD-indoor 0 0 0 0 0 0 0 71 102 67 82 30 12 13 14 3 0 342
56 osm for ruby 0 0 0 335 1 0 0 0 0 0 0 0 0 0 0 0 0 336
57 Zaczero/osm-relatify 0 0 0 0 0 0 0 0 0 0 0 0 0 0 118 156 168 326
58 osmtools 61 108 53 42 32 30 28 34 36 25 23 17 17 20 20 14 9 312
59 ProjetDuMois.fr 0 0 0 0 0 0 0 0 0 0 0 74 163 93 57 18 9 305
60 Mundi App 0 0 0 0 0 0 0 0 0 0 0 0 56 229 7 5 0 270
61 PLanes 0 0 0 0 0 0 0 0 0 66 69 43 39 61 43 34 15 268
62 rocketdata.io 0 0 0 0 0 0 0 0 0 234 0 0 0 0 0 0 0 234
63 My Opening Hours 0 0 0 0 89 89 70 37 20 7 0 0 0 0 0 0 0 234
64 Grass_and_Green 0 0 0 0 0 0 188 43 0 0 0 0 0 0 0 0 0 227
65 openaedmap-backend 2.14.0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 27 213 225
66 meta 0 30 61 84 41 15 1 0 0 0 0 0 0 0 0 0 0 223
67 AED Map for Android/iOS (aedmap v1.0.63) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 218 218
68 Jungle Bus 0 0 0 0 0 0 0 0 100 79 44 35 5 3 1 1 0 216
69 peundemerg.ro 0 0 0 0 0 0 7 27 34 42 48 52 43 39 37 28 0 209
70 OsmAPP 0 0 0 0 0 0 0 0 0 0 0 0 13 18 16 57 126 194
71 Osm.Org Tags Editor 0 0 0 0 0 0 0 0 0 0 0 0 0 68 75 120 123 191
72 AED Map for Android/iOS 0 0 0 0 0 0 0 0 0 0 0 0 0 0 129 42 1 170
73 AED Map for Android/iOS (aedmap v1.0.62) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 160 160
74 Deriviste 0 0 0 0 0 0 0 0 0 45 99 27 12 0 4 0 0 159
75 SketchOSM 0 0 0 0 0 0 0 0 0 60 87 16 3 1 0 0 0 158
76 CityZen 0 0 0 0 0 0 0 0 34 65 50 11 4 1 5 2 0 156
77 Geocropping 0 0 0 0 0 0 0 0 50 37 29 18 18 17 12 1 0 146
78 OpenMaps iOS 0 141 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 141
79 Refill Südtirol / Alto Adige 0 0 0 0 0 0 0 0 0 0 0 72 15 16 55 14 8 139
80 upload.py 16 15 15 25 16 12 7 4 17 8 4 8 7 9 4 6 4 138
81 AED Map for Android/iOS (aedmap v1.0.28) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 137 0 137
82 AED Map for Android/iOS (aedmap v1.0.31) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 130 9 134
83 bulk_upload.py 65 38 17 6 5 3 6 3 1 3 1 2 1 0 0 1 0 132
84 AED Map for Android/iOS (aedmap v1.0.54) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 123 123
85 PythonOsmApi 19 36 22 41 25 6 2 3 1 0 0 0 0 0 0 0 0 122
86 https://israelhiking.osm.org.il 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 121 121
87 AED Map for Android/iOS (aedmap v1.0.38) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 52 71 118
88 Osmose Raw Editor 93 54 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 109
89 Services_OpenStreetMap 0 0 0 24 17 41 34 21 12 8 8 4 1 2 1 1 0 104
90 osm2go 99 0 0 0 0 0 0 0 2 2 1 2 2 1 1 1 0 102
91 AED Map for Android/iOS (aedmap v1.0.26) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 101 1 102
92 OpenRecycleMap 0 0 0 0 0 0 0 0 0 0 30 30 14 19 24 2 0 101
93 Tracks Editor 0 0 0 38 13 0 37 8 6 4 0 0 0 0 0 0 0 99
94 TrashApp 0 0 0 0 0 0 0 0 0 0 66 13 13 6 5 1 0 97
95 ALARMiator Mobile 0 0 0 0 0 0 0 0 0 0 0 0 0 15 49 44 23 94
96 Centaur Mapper 0 0 0 0 0 0 0 0 0 0 0 51 49 9 0 0 0 94
97 COFFEEDEX 2002 0 0 0 0 0 52 37 7 1 0 3 0 0 0 0 0 0 93
98 andnav.org 33 55 6 2 1 0 0 0 0 0 0 0 0 0 0 0 0 92
99 OpenStreetMap Nomino 0 0 0 32 22 15 19 12 0 0 0 0 0 0 0 0 0 85
100 OsmoTagger 1.1(2) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 42 49 83

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_software AS (
	SELECT 
		user_name,
		created_by,
		year,
		month,
		ROW_NUMBER() OVER (PARTITION BY user_name ORDER BY year, month) as rn
	FROM '../changeset_data/year=*/month=*/*.parquet'
	WHERE created_by IS NOT NULL
),
first_software_only AS (
	SELECT 
		user_name,
		created_by,
		year,
		month,
		CONCAT(year, '-', LPAD(CAST(month as VARCHAR), 2, '0')) as months
	FROM user_first_software
	WHERE rn = 1 AND 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,
        ),
    ],
)