When measuring Copilot adoption, a single snapshot (“what share of
people used Copilot last week?”) can be misleading, because usage is a
habit that builds up (or decays) over many weeks. The
vivainsights package ships a function,
identify_usage_segments(), that classifies every
person-week into an adoption segment (Power User,
Habitual User, Novice User,
Low User or Non-user) using a rolling
window of Copilot actions.
This notebook shows how to:
identify_usage_segments(version = "12w"), andWe use the built-in pq_data sample so the notebook runs
end-to-end with no external files. Before adapting it to an export
loaded with
vivainsights::import_query("your-person-query.csv"), verify
the metric schema, eligibility and measurement coverage described
below.
knitr::opts_chunk$set(warning = FALSE, message = FALSE)
library(dplyr)
library(tidyr)
library(ggplot2)
library(scales)
library(vivainsights)
For clarity in this demonstration we use the explicit
package::function() notation in a few places to show which
package each function comes from.
identify_usage_segments() expects a single metric column
that captures Copilot intensity. A Person Query splits Copilot activity
across several Copilot_actions_taken_in_* columns. This
example sums the six named metrics below (Teams, Copilot chat (work),
Excel, Outlook, PowerPoint and Word) into
Total_Copilot_actions_taken; it is not a total across every
possible Copilot surface. These names match the built-in sample’s
schema.
All six columns must be present and contain numeric, finite,
non-negative counts for every included person-week. An observed
0 is valid no activity; a missing column or NA
is unknown coverage, not evidence of non-use. The
notebook stops on invalid inputs rather than filling them with zeros or
silently excluding them from the denominator.
data("pq_data", package = "vivainsights")
app_cols <- c(
"Copilot_actions_taken_in_Teams",
"Copilot_actions_taken_in_Copilot_chat__work_",
"Copilot_actions_taken_in_Excel",
"Copilot_actions_taken_in_Outlook",
"Copilot_actions_taken_in_Powerpoint",
"Copilot_actions_taken_in_Word"
)
app_cols
## [1] "Copilot_actions_taken_in_Teams"
## [2] "Copilot_actions_taken_in_Copilot_chat__work_"
## [3] "Copilot_actions_taken_in_Excel"
## [4] "Copilot_actions_taken_in_Outlook"
## [5] "Copilot_actions_taken_in_Powerpoint"
## [6] "Copilot_actions_taken_in_Word"
missing_cols <- setdiff(app_cols, names(pq_data))
if (length(missing_cols)) {
stop("Missing required Copilot action columns: ",
paste(missing_cols, collapse = ", "),
". Select these query metrics and verify coverage before classifying use.",
call. = FALSE)
}
invalid_cols <- app_cols[vapply(pq_data[app_cols], function(x) {
!is.numeric(x) || anyNA(x) || any(!is.finite(x)) || any(x < 0)
}, logical(1))]
if (length(invalid_cols)) {
stop("Copilot action counts must be numeric, finite, non-missing and non-negative: ",
paste(invalid_cols, collapse = ", "),
". Check query coverage and correct the input; unknown counts are not zero activity.",
call. = FALSE)
}
pq <- pq_data %>%
mutate(Total_Copilot_actions_taken = rowSums(across(all_of(app_cols))))
if (any(!is.finite(pq$Total_Copilot_actions_taken))) {
stop("Total Copilot actions are not finite. Check the input counts and metric scale.",
call. = FALSE)
}
# A quick look at the panel structure
pq %>%
summarise(
persons = dplyr::n_distinct(PersonId),
weeks = dplyr::n_distinct(MetricDate),
from = min(MetricDate),
to = max(MetricDate)
)
## # A tibble: 1 × 4
## persons weeks from to
## <int> <int> <date> <date>
## 1 300 23 2024-04-28 2024-09-29
identify_usage_segments() with
version = "12w" applies the standard 12-week rolling
definition: a person’s segment in a given week depends on their Copilot
actions over that week and the preceding weeks. Returning
return = "data" appends the classification columns to the
input frame.
seg <- vivainsights::identify_usage_segments(
data = pq,
metric = "Total_Copilot_actions_taken",
version = "12w",
return = "data"
)
seg <- seg %>%
mutate(UsageSegments_12w = factor(
UsageSegments_12w,
levels = c("Power User", "Habitual User", "Novice User",
"Low User", "Non-user")
))
# Overall distribution of person-weeks across segments
seg %>%
count(UsageSegments_12w) %>%
mutate(share = scales::percent(n / sum(n), accuracy = 0.1))
## # A tibble: 4 × 3
## UsageSegments_12w n share
## <fct> <int> <chr>
## 1 Power User 7 0.1%
## 2 Habitual User 4479 64.9%
## 3 Novice User 2411 34.9%
## 4 Non-user 3 0.0%
Note on the rolling window. Because the 12-week version looks back up to 12 weeks, the earliest weeks in any export are based on a shorter window and are therefore less stable. When you have a long enough history, it is common to drop the first ~12 weeks before interpreting the trend. With the short sample here we keep all weeks but flag the caveat.
The clearest way to show adoption momentum is the share of the population in each segment, week by week. A stacked-area chart makes the shift from lighter to heavier usage (or vice versa) easy to read.
seg_share <- seg %>%
count(MetricDate, UsageSegments_12w, name = "n") %>%
group_by(MetricDate) %>%
mutate(share = n / sum(n)) %>%
ungroup()
seg_palette <- c(
"Power User" = "#1b4965",
"Habitual User" = "#5fa8d3",
"Novice User" = "#cae9ff",
"Low User" = "#f4a259",
"Non-user" = "#bc4b51"
)
ggplot(seg_share, aes(x = MetricDate, y = share, fill = UsageSegments_12w)) +
geom_area(alpha = 0.9) +
scale_y_continuous(labels = scales::percent) +
scale_fill_manual(values = seg_palette, name = "Usage segment") +
labs(
title = "Copilot usage-segment mix over time",
subtitle = "Share of person-weeks in each 12-week rolling segment",
x = NULL, y = "Share of population"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "top")
Alongside the segment mix, it is useful to plot the trend in mean Copilot actions per person-week. Rising average actions with a growing Power/Habitual share is the signature of healthy adoption.
actions_trend <- seg %>%
group_by(MetricDate) %>%
summarise(mean_actions = mean(Total_Copilot_actions_taken),
.groups = "drop")
ggplot(actions_trend, aes(x = MetricDate, y = mean_actions)) +
geom_line(linewidth = 1.1, colour = "#1b4965") +
geom_point(size = 2, colour = "#1b4965") +
labs(
title = "Average Copilot actions per person-week",
subtitle = "Mean of Total_Copilot_actions_taken across the population",
x = NULL, y = "Mean Copilot actions"
) +
theme_minimal(base_size = 12)
With three short steps, namely summing the Copilot action columns,
calling identify_usage_segments(version = "12w"), and
aggregating by MetricDate, we turned a raw Person Query
into a longitudinal view of Copilot adoption. The same workflow can be
adapted to a real export only after validating its source contract. Map
the six metrics explicitly if names differ; do not omit a metric or
insert zeros just to pass the checks. Numeric completeness alone does
not establish eligibility or telemetry coverage. Confirm which people
and weeks were measured, including missing person-weeks and any
source-provided zeros that may represent unavailable data. If coverage
is unresolved, stop the analysis or design and disclose a separate
unknown-coverage policy before reporting adoption. The notebook does not
infer that policy or perform a real-data privacy review. Treat the
earliest weeks with caution because of the 12-week warm-up window.