--- title: "Copilot Consumption and Ways of Working" subtitle: "The art of the possible with consumption and Viva Insights data (fully synthetic)" date: "Synthetic window: 4 Jan–28 Jun 2026" output: flexdashboard::flex_dashboard: orientation: rows vertical_layout: scroll theme: lumen self_contained: true --- ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE, fig.width = 8.5, fig.height = 4.5) library(dplyr) library(tidyr) library(ggplot2) library(scales) library(knitr) library(vivainsights) library(ggrepel) MIN_GROUP_N <- 10L PAL <- c("#1F4E79", "#2E75B6", "#7FA8D0", "#BDD3E8", "#C9C9C9", "#B03A2E", "#5B8C5A", "#7D5BA6") theme_t <- function() { theme_minimal(base_size = 11) + theme( panel.grid.minor = element_blank(), legend.position = "bottom", plot.title.position = "plot", plot.title = element_text(face = "bold", colour = PAL[1]), plot.subtitle = element_text(colour = "grey30", margin = margin(t = 2, b = 9)), plot.caption = element_text(colour = "grey45", size = 8, hjust = 0, margin = margin(t = 7)), plot.margin = margin(t = 10, r = 16, b = 4, l = 4) ) } read_demo <- function(...) { read.csv(file.path("_data", "consumption", ...), check.names = FALSE, stringsAsFactors = FALSE) } people <- read_demo("reference", "people-snapshot.csv") consumption <- read_demo("consumption-query", "consumption-weekly.csv") |> mutate(MetricDate = as.Date(MetricDate)) credit_tasks <- read_demo("consumption-query", "consumption-task-types.csv") pq <- read_demo("person-query", "person-query-weekly.csv") |> mutate(MetricDate = as.Date(MetricDate)) net_monthly <- read_demo("person-query", "network-monthly.csv") SEGMENT_LEVELS <- c( "Power User", "Habitual User", "Novice User", "Low User", "Non-user" ) seg <- identify_usage_segments( data = consumption, metric = "Total_Copilot_actions_taken", version = "12w", return = "data" ) |> select(PersonId, MetricDate, UsageSegments_12w) LATEST_OBS_WEEK <- max(seg$MetricDate, na.rm = TRUE) OBS_WINDOW_START <- min(consumption$MetricDate, na.rm = TRUE) OBS_WINDOW_END <- max(consumption$MetricDate, na.rm = TRUE) CHART_CAPTION <- sprintf( "Synthetic data | n = %s people | %s to %s", comma(nrow(people)), format(OBS_WINDOW_START, "%d %b %Y"), format(OBS_WINDOW_END, "%d %b %Y") ) person_segment <- seg |> filter(MetricDate == LATEST_OBS_WEEK) |> transmute( PersonId, Segment = factor( as.character(UsageSegments_12w), levels = SEGMENT_LEVELS, ordered = TRUE ) ) segment_snapshot <- consumption |> filter(MetricDate == LATEST_OBS_WEEK) |> transmute( PersonId, weekly_actions = Total_Copilot_actions_taken, weekly_credits = Copilot_credits_consumed, weekly_tokens = Input_tokens + Output_tokens + Reasoning_tokens, credit_intensity = if_else( weekly_tokens > 0, weekly_credits / (weekly_tokens / 1000), NA_real_ ) ) |> left_join(person_segment, by = "PersonId") |> mutate( positive_consumer = weekly_credits > 0, positive_quartile = if_else( positive_consumer, ntile(if_else(positive_consumer, weekly_credits, NA_real_), 4L), NA_integer_ ), CreditQuartile = factor( case_when( !positive_consumer ~ "No consumption", positive_quartile == 1L ~ "Q1 lowest", positive_quartile == 2L ~ "Q2", positive_quartile == 3L ~ "Q3", TRUE ~ "Q4 highest" ), levels = c("No consumption", "Q1 lowest", "Q2", "Q3", "Q4 highest") ) ) person_consumption <- consumption |> group_by(PersonId) |> summarise( weekly_credits = mean(Copilot_credits_consumed), weekly_actions = mean(Total_Copilot_actions_taken), weekly_tokens = mean(Input_tokens + Output_tokens + Reasoning_tokens), total_tokens = sum(Input_tokens + Output_tokens + Reasoning_tokens), reasoning_tokens = sum(Reasoning_tokens), reasoning_share = if_else(total_tokens > 0, reasoning_tokens / total_tokens, NA_real_), credit_intensity = if_else( total_tokens > 0, sum(Copilot_credits_consumed) / (total_tokens / 1000), NA_real_ ), .groups = "drop" ) |> mutate( positive_consumer = weekly_credits > 0, positive_quartile = if_else( positive_consumer, ntile(if_else(positive_consumer, weekly_credits, NA_real_), 4L), NA_integer_ ), CreditQuartile = case_when( !positive_consumer ~ "No consumption", positive_quartile == 1L ~ "Q1 lowest", positive_quartile == 2L ~ "Q2", positive_quartile == 3L ~ "Q3", TRUE ~ "Q4 highest" ) ) reasoning_cut <- quantile( person_consumption$reasoning_share[ person_consumption$positive_consumer & person_consumption$total_tokens >= 10000 ], 0.75, na.rm = TRUE ) person_base <- people |> left_join(person_consumption, by = "PersonId") |> left_join(person_segment, by = "PersonId") |> mutate( CreditQuartile = factor( CreditQuartile, levels = c("No consumption", "Q1 lowest", "Q2", "Q3", "Q4 highest") ), CreditBand = case_when( CreditQuartile %in% c("Q3", "Q4 highest") ~ "High credit", CreditQuartile %in% c("Q1 lowest", "Q2") ~ "Lower credit", TRUE ~ "No consumption" ), ReasoningProfile = case_when( !positive_consumer ~ "No consumption", total_tokens < 10000 ~ "Insufficient token volume", reasoning_share >= reasoning_cut ~ "Reasoning-intensive", TRUE ~ "Standard-cost mix" ), ConsumptionProfile = case_when( CreditBand == "High credit" & ReasoningProfile == "Reasoning-intensive" ~ "High credit, reasoning-intensive", CreditBand == "High credit" ~ "High credit, standard mix", CreditBand == "Lower credit" & ReasoningProfile == "Reasoning-intensive" ~ "Lower credit, reasoning-intensive", CreditBand == "Lower credit" ~ "Lower credit, standard mix", TRUE ~ "No consumption" ) ) WOW_METRICS <- c( "Collaboration_hours", "Active_connected_hours", "Email_hours", "Chat_hours", "Meeting_hours", "Unscheduled_call_hours", "After_hours_collaboration_hours", "Collaboration_span", "Weekend_collaboration_hours", "Internal_network_size", "External_network_size" ) person_wow <- pq |> group_by(PersonId) |> summarise(across(all_of(WOW_METRICS), \(x) mean(x, na.rm = TRUE)), .groups = "drop") |> left_join(person_base, by = "PersonId") function_summary <- person_wow |> filter(weekly_credits > 0) |> group_by(FunctionType) |> summarise( People = n(), `Weekly credits` = mean(weekly_credits), `Weekly tokens` = mean(weekly_tokens), `Weekly actions` = mean(weekly_actions), `Credit intensity` = weighted.mean( credit_intensity, total_tokens, na.rm = TRUE), `Reasoning-token share` = weighted.mean( reasoning_share, total_tokens, na.rm = TRUE), `Collaboration hours` = mean(Collaboration_hours), `After-hours hours` = mean(After_hours_collaboration_hours), `Collaboration span` = mean(Collaboration_span), `Internal network size` = mean(Internal_network_size), .groups = "drop" ) |> filter(People >= MIN_GROUP_N) FUNCTION_ORDER <- function_summary |> arrange(`Weekly credits`) |> pull(FunctionType) overall <- person_wow |> summarise( People = n(), Consumers = sum(weekly_credits > 0), `Weekly credits` = mean(weekly_credits), `Weekly tokens` = mean(weekly_tokens), `Credit intensity` = weighted.mean( credit_intensity, total_tokens, na.rm = TRUE), `Reasoning-token share` = weighted.mean( reasoning_share, total_tokens, na.rm = TRUE) ) std_diff <- function(x, grp) { a <- x[grp] b <- x[!grp] if (length(a) < MIN_GROUP_N || length(b) < MIN_GROUP_N) return(NA_real_) pooled <- sqrt(((length(a) - 1) * var(a) + (length(b) - 1) * var(b)) / (length(a) + length(b) - 2)) if (is.na(pooled) || pooled == 0) return(NA_real_) (mean(a) - mean(b)) / pooled } ``` Overview {data-icon="fa-dashboard"} =====================================
Row {data-height=120} ------------------------------------- ### People represented ```{r} flexdashboard::valueBox( comma(overall$People), "People in the analysis", icon = "fa-users", color = "primary" ) ``` ### Positive users ```{r} flexdashboard::valueBox( percent(overall$Consumers / overall$People, accuracy = 0.1), paste0(comma(overall$Consumers), " of ", comma(overall$People), " people"), icon = "fa-bolt", color = "info" ) ``` ### Credits per person per week ```{r} flexdashboard::valueBox( format(round(overall$`Weekly credits`, 1), big.mark = ",", nsmall = 1), "Credits / person / week (all people)", icon = "fa-calculator", color = "success" ) ``` ### Credits per 1,000 tokens ```{r} flexdashboard::valueBox( format(round(overall$`Credit intensity`, 2), nsmall = 2), "Credits / 1,000 tokens (positive users)", icon = "fa-tachometer", color = "warning" ) ``` Row {data-height=510} ------------------------------------- ### Consumption baselines by function ```{r function-baseline, fig.height=4.4, fig.alt="Horizontal bars comparing average weekly Copilot tokens and credits per positive user across eight functions."} baseline_long <- function_summary |> select(FunctionType, People, `Weekly credits`, `Weekly tokens`) |> pivot_longer(c(`Weekly credits`, `Weekly tokens`), names_to = "Measure", values_to = "Value") |> mutate(Measure = recode( Measure, `Weekly credits` = "Credits / user / week", `Weekly tokens` = "Tokens / user / week" ), Label = if_else( Measure == "Tokens / user / week", format(round(Value), big.mark = ","), format(round(Value, 1), nsmall = 1) ), FunctionType = factor(FunctionType, levels = FUNCTION_ORDER)) ggplot(baseline_long, aes(FunctionType, Value, fill = Measure)) + geom_col(position = position_dodge(width = 0.72), width = 0.66) + geom_text(aes(label = Label), position = position_dodge(width = 0.72), hjust = -0.10, size = 3) + coord_flip() + facet_wrap(~Measure, scales = "free_x") + scale_fill_manual(values = PAL[c(1, 3)], guide = "none") + scale_y_continuous(expand = expansion(mult = c(0, 0.18))) + theme_t() + labs( subtitle = "Positive users only (token volume is the primary usage baseline)", x = NULL, y = NULL ) ``` ### Baseline table ```{r} function_summary |> arrange(desc(`Weekly credits`)) |> transmute( Function = FunctionType, People, `Credits / user / week` = round(`Weekly credits`, 1), `Actions / user / week` = round(`Weekly actions`, 1), `Tokens / user / week` = format(round(`Weekly tokens`), big.mark = ","), `Credits / 1,000 tokens` = round(`Credit intensity`, 2), `Reasoning share` = percent(`Reasoning-token share`, accuracy = 0.1) ) |> kable( caption = "Function baseline (credit intensity is credits per 1,000 tokens; lower means a less expensive token mix)." ) ``` Row {data-height=335} ------------------------------------- ### Population-level highlights **Consumption volume and consumption mix are separate.** The highest-credit people are not automatically the most reasoning-intensive, and the standard Power User definition is based on sustained actions rather than credit cost. **The clearest Ways of Working difference is channel mix.** Higher-credit users lean more towards chat and less towards email. Total collaboration volume is more sensitive to function and seniority than to consumption itself. **Reasoning-intensive use carries a weak workload signal in this simulation.** Within the same broad credit band, reasoning-intensive users have slightly more after-hours collaboration and a longer collaboration span. This is an association to investigate rather than evidence of harm. ### How to read credit intensity **Credit intensity** is the number of credits consumed per 1,000 tokens. It is a cost-mix measure rather than a productivity or value measure. Lower intensity means the observed token mix consumes fewer credits. It does not establish that the work was more efficient, because the report does not observe output quality, task difficulty or business value. Token distribution {data-icon="fa-chart-area"} ===================================== Row {data-height=470} ------------------------------------- ### Concentration of token consumption across users ```{r token-concentration, fig.height=4.4, fig.alt="Concentration curve showing the cumulative share of tokens accounted for by the highest-volume share of users."} token_rank <- person_consumption |> filter(positive_consumer) |> arrange(desc(weekly_tokens)) |> mutate( n = n(), `Share of users` = row_number() / n, `Share of tokens` = cumsum(weekly_tokens) / sum(weekly_tokens) ) marker_x <- c(0.10, 0.25) marker_y <- vapply(marker_x, function(p) { token_rank$`Share of tokens`[which.min(abs(token_rank$`Share of users` - p))] }, numeric(1)) markers <- tibble(x = marker_x, y = marker_y, lbl = percent(marker_y, accuracy = 1)) ggplot(token_rank, aes(`Share of users`, `Share of tokens`)) + geom_area(fill = PAL[3], alpha = 0.35) + geom_line(colour = PAL[1], linewidth = 1) + geom_abline(slope = 1, intercept = 0, linetype = "dashed", colour = "grey55") + geom_point(data = markers, aes(x, y), colour = PAL[6], size = 2.6, inherit.aes = FALSE) + geom_text(data = markers, aes(x, y, label = lbl), inherit.aes = FALSE, vjust = -1.0, hjust = -0.15, size = 3.3, colour = PAL[6]) + scale_x_continuous(labels = percent_format(accuracy = 1), expand = expansion(mult = c(0.01, 0.06))) + scale_y_continuous(labels = percent_format(accuracy = 1), expand = expansion(mult = c(0.01, 0.10))) + theme_t() + labs( title = "Cumulative share of tokens by user rank", subtitle = paste0( "Users ranked from the highest to the lowest weekly token volume; the ", "dashed line marks equal consumption. A curve further above the line ", "indicates more concentrated consumption" ), x = "Share of positive users (ranked highest to lowest)", y = "Cumulative share of tokens", caption = CHART_CAPTION ) ``` ### Concentration bands ```{r} band_levels <- c("Top 1%", "Next 4% (top 1-5%)", "Next 5% (top 5-10%)", "Next 15% (top 10-25%)", "Next 25% (top 25-50%)", "Bottom 50%") band_table <- token_rank |> mutate( Band = case_when( `Share of users` <= 0.01 ~ band_levels[1], `Share of users` <= 0.05 ~ band_levels[2], `Share of users` <= 0.10 ~ band_levels[3], `Share of users` <= 0.25 ~ band_levels[4], `Share of users` <= 0.50 ~ band_levels[5], TRUE ~ band_levels[6] ), Band = factor(Band, levels = band_levels) ) |> group_by(Band) |> summarise( People = n(), `Weekly tokens (band total)` = sum(weekly_tokens), .groups = "drop" ) |> arrange(Band) |> mutate( `Share of users` = percent(People / sum(People), accuracy = 1), `Share of tokens` = percent( `Weekly tokens (band total)` / sum(`Weekly tokens (band total)`), accuracy = 1 ) ) |> select(Band, People, `Share of users`, `Share of tokens`) band_table |> kable(caption = paste0( "Users ranked by average weekly tokens, positive users only (n = ", comma(sum(band_table$People)), ")." )) ``` Row {data-height=440} ------------------------------------- ### Shape of the token-volume distribution ```{r token-histogram, fig.height=4.1, fig.alt="Histogram of average weekly tokens per positive user on a logarithmic scale, with median and mean reference lines."} token_stats <- person_consumption |> filter(positive_consumer) med_tokens <- median(token_stats$weekly_tokens) mean_tokens <- mean(token_stats$weekly_tokens) ref_lines <- tibble( stat = c("Median", "Mean"), value = c(med_tokens, mean_tokens), colour = c(PAL[1], PAL[6]) ) ggplot(token_stats, aes(weekly_tokens)) + geom_histogram(bins = 40, fill = PAL[2], colour = "white", linewidth = 0.2) + geom_vline(data = ref_lines, aes(xintercept = value, colour = stat), linetype = "dashed", linewidth = 0.7, show.legend = TRUE) + scale_colour_manual(values = c(Median = PAL[1], Mean = PAL[6])) + scale_x_log10(labels = label_number(scale = 1 / 1000, suffix = "K")) + scale_y_continuous(expand = expansion(mult = c(0, 0.10))) + theme_t() + labs( title = "Distribution of weekly tokens per user", subtitle = paste0( "Logarithmic x-axis; positive users only. A long right tail indicates a ", "small group of high-volume users; a more symmetric shape indicates ", "more uniform usage" ), x = "Tokens / user / week (log scale)", y = "Number of users", colour = NULL, caption = CHART_CAPTION ) ``` ### Why a concentration curve and a log histogram? Token and credit consumption in Copilot data is typically **right-skewed**: most people use a modest, fairly similar amount, while a small group of heavy users account for a disproportionate share of total volume. A boxplot or violin plot would compress this shape into a small number of summary points and understate how concentrated the top tail is. The concentration curve above answers the practical question directly: **what share of tokens does the top share of users account for?** The histogram (log scale) shows the shape that produces that concentration, and the gap between the median and mean lines is itself evidence of the right skew. This has a practical implication for interpreting averages elsewhere in this report. A mean such as "credits per person per week" can be pulled upward by a small number of heavy users, so organisational and segment comparisons should be read alongside this distribution rather than in isolation. Credit relationships ===================================== Row {data-height=470} ------------------------------------- ### Ways of Working metrics by credit-consumption group ```{r credit-heatmap, fig.height=4.4, fig.alt="Heatmap of standardised Ways of Working metric differences for five credit-consumption groups."} credit_levels <- c("No consumption", "Q1 lowest", "Q2", "Q3", "Q4 highest") scan <- expand_grid( Metric = WOW_METRICS, CreditQuartile = credit_levels ) |> rowwise() |> mutate(d = std_diff( person_wow[[Metric]], person_wow$CreditQuartile == CreditQuartile )) |> ungroup() |> filter(!is.na(d)) |> mutate( Metric = recode( Metric, Collaboration_hours = "Collaboration hours / person / week", Active_connected_hours = "Active connected hours / person / week", Email_hours = "Email hours / person / week", Chat_hours = "Chat hours / person / week", Meeting_hours = "Meeting hours / person / week", Unscheduled_call_hours = "Unscheduled call hours / person / week", After_hours_collaboration_hours = "After-hours collaboration hours / person / week", Collaboration_span = "Collaboration span (hours)", Weekend_collaboration_hours = "Weekend collaboration hours / person / week", Internal_network_size = "Internal network size (people)", External_network_size = "External network size (people)" ), CreditQuartile = factor(CreditQuartile, levels = rev(credit_levels)) ) ggplot(scan, aes(CreditQuartile, reorder(Metric, d), fill = d)) + geom_tile(colour = "white", linewidth = 0.6) + geom_text(aes(label = sprintf("%+.2f", d)), size = 3) + scale_fill_gradient2( low = PAL[6], mid = "#F5F2EA", high = PAL[1], midpoint = 0, limits = c(-1.3, 1.3), oob = squish ) + theme_t() + theme(legend.position = "right") + labs( title = "Standardised difference against everyone outside the group", subtitle = "Positive values indicate the group scores above the rest of the population on that measure", x = NULL, y = NULL, fill = "Std. difference" ) ``` ### Credit groups ```{r} person_base |> count(CreditQuartile, name = "People") |> group_by(CreditQuartile) |> mutate( `Credits / person / week` = mean( person_base$weekly_credits[ person_base$CreditQuartile == first(CreditQuartile) ] ) ) |> ungroup() |> mutate( `Share of population` = percent(People / sum(People), accuracy = 0.1), `Credits / person / week` = round(`Credits / person / week`, 1) ) |> kable(caption = "Credit-volume groups (quartiles are calculated among positive users).") ``` Row {data-height=455} ------------------------------------- ### Collaboration mode mix by credit-consumption group ```{r mode-mix, fig.height=4.1, fig.alt="One hundred percent stacked bars comparing email, chat, meeting and unscheduled-call shares across credit groups."} modes <- c("Email_hours", "Chat_hours", "Meeting_hours", "Unscheduled_call_hours") mode_share <- person_wow |> group_by(CreditQuartile) |> summarise(People = n(), across(all_of(modes), mean), .groups = "drop") |> pivot_longer(all_of(modes), names_to = "Mode", values_to = "Hours") |> group_by(CreditQuartile) |> mutate(Share = Hours / sum(Hours)) |> ungroup() |> mutate(Mode = recode( Mode, Email_hours = "Email", Chat_hours = "Chat", Meeting_hours = "Meetings", Unscheduled_call_hours = "Unscheduled calls" )) ggplot(mode_share, aes(CreditQuartile, Share, fill = Mode)) + geom_col(width = 0.68) + geom_text( aes(label = ifelse(Share >= 0.06, percent(Share, accuracy = 1), "")), position = position_stack(vjust = 0.5), colour = ifelse(mode_share$Mode %in% c("Meetings", "Unscheduled calls"), "#1F2937", "white"), size = 3 ) + coord_flip() + scale_y_continuous(labels = percent_format(accuracy = 1)) + scale_fill_manual(values = PAL[1:4]) + theme_t() + labs( title = "Share of collaboration hours by mode", subtitle = "Mode hours are shown before deduplication, so shares may not sum exactly across modes", x = NULL, y = NULL, fill = "Collaboration mode" ) ``` ### Workload and network ```{r credit-outcomes, fig.height=4.1, fig.alt="Four small bar charts comparing workload and network metrics across credit groups."} person_wow |> group_by(CreditQuartile) |> summarise( People = n(), `After-hours collaboration hours / person / week` = mean(After_hours_collaboration_hours), `Collaboration span (hours)` = mean(Collaboration_span), `Internal network size (people)` = mean(Internal_network_size), `External network size (people)` = mean(External_network_size), .groups = "drop" ) |> pivot_longer(-c(CreditQuartile, People), names_to = "Metric", values_to = "Value") |> ggplot(aes(CreditQuartile, Value)) + geom_col(fill = PAL[2], width = 0.66) + geom_text(aes(label = round(Value, 1)), vjust = -0.35, size = 2.8) + facet_wrap(~Metric, scales = "free_y", ncol = 2) + scale_y_continuous(expand = expansion(mult = c(0, 0.16))) + theme_t() + theme(axis.text.x = element_text(angle = 25, hjust = 1)) + labs( title = "Workload and network by credit-volume group", subtitle = "Average values by credit-volume group; compare with the function drill-down for organisational context", x = NULL, y = NULL ) ``` Function drill-down {data-icon="fa-sitemap"} ===================================== Row {data-height=505} ------------------------------------- ### Consumption and collaboration volume by function ```{r bubble-collaboration, fig.height=4.5, fig.alt="Bubble plot of tokens and collaboration hours by function. Bubble size represents users and colour represents credits per one thousand tokens."} ggplot( function_summary, aes(`Weekly tokens`, `Collaboration hours`, size = People, colour = `Credit intensity`, label = FunctionType) ) + geom_point(alpha = 0.85) + geom_text_repel(size = 3.2, colour = "grey20", seed = 2026, min.segment.length = 0, max.overlaps = Inf, box.padding = 0.6, point.padding = 0.3) + scale_size_area(max_size = 14) + scale_colour_gradient(low = PAL[3], high = PAL[6]) + scale_x_continuous(labels = label_number(scale = 1 / 1000, suffix = "K"), expand = expansion(mult = 0.18)) + scale_y_continuous(expand = expansion(mult = 0.18)) + theme_t() + labs( title = "Token consumption against collaboration hours, by function", subtitle = "Bubble size shows population; colour shows credit intensity (credits per 1,000 tokens)", x = "Tokens / user / week", y = "Collaboration hours / user / week", colour = "Credits / 1,000 tokens", size = "Positive users", caption = CHART_CAPTION ) ``` ### Reading the chart The upper-right functions combine high token volume with high collaboration volume. That does not imply that one causes the other. Their role mix, seniority and working practices may raise both. The colour adds a third dimension. A darker point consumes more credits for the same number of tokens. This can indicate a more expensive reasoning mix, but it may also reflect harder work. The appropriate follow-up is to examine task mix and value, rather than treating lower intensity as automatically better. Row {data-height=490} ------------------------------------- ### Workload and credit intensity by function ```{r bubble-workload, fig.height=4.4, fig.alt="Bubble plot of credits and after-hours collaboration by function. Bubble size represents users and colour represents credits per one thousand tokens."} ggplot( function_summary, aes(`Weekly credits`, `After-hours hours`, size = People, colour = `Credit intensity`, label = FunctionType) ) + geom_point(alpha = 0.85) + geom_text_repel(size = 3.2, colour = "grey20", seed = 2026, min.segment.length = 0, max.overlaps = Inf, box.padding = 0.6, point.padding = 0.3) + scale_size_area(max_size = 14) + scale_colour_gradient(low = PAL[3], high = PAL[6]) + scale_x_continuous(expand = expansion(mult = 0.18)) + scale_y_continuous(expand = expansion(mult = 0.18)) + theme_t() + labs( title = "Credit consumption against after-hours workload, by function", subtitle = "Same bubble-size and colour encoding as the chart above, for comparison across functions", x = "Credits / user / week", y = "After-hours hours / user / week", colour = "Credits / 1,000 tokens", size = "Positive users", caption = CHART_CAPTION ) ``` ### Function comparison matrix ```{r function-heatmap, fig.height=4.4, fig.alt="Indexed heatmap comparing six consumption and Ways of Working measures across eight functions."} heat_metrics <- c( "Weekly credits", "Credit intensity", "Collaboration hours", "After-hours hours", "Collaboration span", "Internal network size" ) function_summary |> mutate(FunctionType = factor(FunctionType, levels = FUNCTION_ORDER)) |> select(FunctionType, all_of(heat_metrics)) |> pivot_longer(-FunctionType, names_to = "Metric", values_to = "Value") |> mutate(Metric = recode( Metric, `Weekly credits` = "Credits / user / week", `Credit intensity` = "Credits / 1,000 tokens", `Collaboration hours` = "Collaboration hours / person / week", `After-hours hours` = "After-hours hours / person / week", `Collaboration span` = "Collaboration span (hours)", `Internal network size` = "Internal network size (people)" )) |> group_by(Metric) |> mutate(Index = 100 * Value / mean(Value)) |> ungroup() |> ggplot(aes(FunctionType, Metric, fill = Index)) + geom_tile(colour = "white", linewidth = 0.7) + geom_text(aes(label = round(Index)), size = 3) + scale_fill_gradient2( low = "#E8EEF5", mid = "#F5F2EA", high = PAL[1], midpoint = 100 ) + theme_t() + theme(axis.text.x = element_text(angle = 30, hjust = 1), legend.position = "right") + labs( title = "Each metric indexed to the function average (100)", subtitle = "A compact view of six dimensions across functions", x = NULL, y = NULL, fill = "Index" ) ``` Consumption mix {data-icon="fa-adjust"} ===================================== Row {data-height=480} ------------------------------------- ### Credit volume by credit band and reasoning-token profile ```{r reasoning-profile, fig.height=4.3, fig.alt="Grouped bars comparing weekly credit volume for reasoning-intensive and standard-cost profiles within each credit band."} profile_summary <- person_wow |> filter(ConsumptionProfile != "No consumption") |> group_by(ConsumptionProfile) |> summarise( People = n(), `Credits / person / week` = mean(weekly_credits), `Weekly actions` = mean(weekly_actions), `Reasoning share` = mean(reasoning_share, na.rm = TRUE), `After-hours hours` = mean(After_hours_collaboration_hours), `Collaboration span` = mean(Collaboration_span), .groups = "drop" ) profile_grid <- profile_summary |> mutate( CreditBand = if_else(grepl("^High credit", ConsumptionProfile), "High credit", "Lower credit"), `Reasoning-token profile` = if_else( grepl("reasoning-intensive", ConsumptionProfile), "Reasoning-intensive", "Standard-cost mix" ) ) ggplot( profile_grid, aes(CreditBand, `Credits / person / week`, fill = `Reasoning-token profile`) ) + geom_col(position = position_dodge(width = 0.7), width = 0.6) + geom_text( aes(label = round(`Credits / person / week`, 1)), position = position_dodge(width = 0.7), vjust = -0.4, size = 3.2 ) + scale_fill_manual(values = c( "Reasoning-intensive" = PAL[6], "Standard-cost mix" = PAL[3] )) + scale_y_continuous(expand = expansion(mult = c(0, 0.18))) + theme_t() + labs( title = "Mix of credit volume and reasoning intensity", subtitle = paste0( "Similar bar heights within a credit band suggest the two dimensions are ", "largely independent; a consistent gap suggests they move together" ), x = "Credit-volume band", y = "Credits / person / week", fill = "Reasoning-token profile" ) ``` ### Profile definitions | Profile component | Definition | Interpretation | |---|---|---| | High credit | Q3 or Q4 among positive users | Higher total credit volume | | Lower credit | Q1 or Q2 among positive users | Lower total credit volume | | Reasoning-intensive | Upper quartile of reasoning-token share among people with at least 10,000 tokens | More expensive token mix | | Standard-cost mix | Below the reasoning-intensive threshold | Less expensive observed token mix | The label **reasoning-intensive** describes the measurable consumption pattern without assuming recklessness. Business value and task difficulty are needed before judging whether the additional expense was warranted. Row {data-height=455} ------------------------------------- ### Workload by reasoning-token profile within credit bands ```{r reasoning-outcomes, fig.height=4.1, fig.alt="Bars comparing after-hours collaboration and collaboration span across standard-cost and reasoning-intensive profiles."} profile_summary |> select(ConsumptionProfile, People, `After-hours hours`, `Collaboration span`) |> pivot_longer(-c(ConsumptionProfile, People), names_to = "Metric", values_to = "Value") |> ggplot( aes(reorder(ConsumptionProfile, Value), Value, fill = grepl("reasoning-intensive", ConsumptionProfile)) ) + geom_col(width = 0.65) + geom_text(aes(label = round(Value, 2)), hjust = -0.12, size = 3) + coord_flip() + facet_wrap(~Metric, scales = "free_x") + scale_fill_manual( values = c(`TRUE` = PAL[6], `FALSE` = PAL[2]), labels = c(`TRUE` = "Reasoning-intensive", `FALSE` = "Standard-cost mix") ) + scale_y_continuous(expand = expansion(mult = c(0, 0.18))) + theme_t() + labs( title = "Workload by reasoning-token profile, within credit bands", subtitle = "Compared within the same broad credit band", x = NULL, y = NULL, fill = "Reasoning-token profile" ) ``` ### What work consumes the credits? ```{r task-mix, fig.height=4.1, fig.alt="Stacked bars comparing the share of credits used by delegated task type for high-credit and lower-credit users."} credit_tasks |> left_join(select(person_base, PersonId, CreditBand), by = "PersonId") |> filter(CreditBand != "No consumption") |> group_by(CreditBand, TaskType) |> summarise(Credits = sum(credits), People = n_distinct(PersonId), .groups = "drop") |> filter(People >= MIN_GROUP_N) |> group_by(CreditBand) |> mutate(Share = Credits / sum(Credits)) |> ungroup() |> ggplot(aes(CreditBand, Share, fill = reorder(TaskType, Share))) + geom_col(width = 0.68) + geom_text( aes(label = ifelse(Share >= 0.06, percent(Share, accuracy = 1), "")), position = position_stack(vjust = 0.5), colour = "grey15", size = 2.9 ) + coord_flip() + scale_y_continuous(labels = percent_format(accuracy = 1)) + scale_fill_brewer(palette = "Blues", direction = -1) + theme_t() + labs( title = "Credit mix by delegated task type", subtitle = "Share of credits by delegated task type, for high-credit and lower-credit groups", x = NULL, y = "Share of credits", fill = "Delegated task type" ) ``` Usage segments {data-icon="fa-users"} ===================================== Row {data-height=480} ------------------------------------- ### Credit-volume distribution within usage segments ```{r segment-credit, fig.height=4.3, fig.alt="Stacked bars showing credit-consumption quartiles within Power, Habitual, Novice, Low and Non-user segments."} segment_credit <- segment_snapshot |> count(Segment, CreditQuartile, name = "People") |> group_by(Segment) |> mutate(Share = People / sum(People)) |> ungroup() ggplot(segment_credit, aes(Segment, Share, fill = CreditQuartile)) + geom_col(width = 0.68) + coord_flip() + scale_x_discrete(limits = rev(SEGMENT_LEVELS)) + scale_y_continuous(labels = percent_format(accuracy = 1)) + scale_fill_manual(values = PAL[c(5, 4, 3, 2, 1)]) + theme_t() + labs( title = "Credit-volume distribution within standard usage segments", subtitle = paste0( "Latest observation week (", format(LATEST_OBS_WEEK, "%d %b %Y"), "); segments use the trailing 12 weeks" ), x = NULL, y = NULL, fill = "Credit-volume group" ) ``` ### Why keep both? The standard usage ladder answers an adoption question: is activity frequent and sustained? Credit volume answers a resource-consumption question: how much credit is used? A Power User can have a standard-cost token mix and moderate credit volume, while someone with fewer actions can consume more credit through longer or more reasoning-intensive requests. The report therefore keeps usage segments as a useful supporting view without letting them substitute for the unique information in the consumption query. Row {data-height=400} ------------------------------------- ### Segment baseline ```{r} segment_snapshot |> group_by(Segment) |> summarise( People = n(), `Weekly actions` = mean(weekly_actions), `Credits / person / week` = mean(weekly_credits), `Tokens / person / week` = mean(weekly_tokens), `Credit intensity` = weighted.mean( credit_intensity, weekly_tokens, na.rm = TRUE), .groups = "drop" ) |> arrange(Segment) |> mutate( across(c(`Weekly actions`, `Credits / person / week`), \(x) round(x, 2)), `Credit intensity` = if_else( is.na(`Credit intensity`) | is.nan(`Credit intensity`), "N/A", format(round(`Credit intensity`, 2), nsmall = 2) ), `Tokens / person / week` = format(round(`Tokens / person / week`), big.mark = ",") ) |> kable(caption = paste0( "Latest-week baseline (", format(LATEST_OBS_WEEK, "%d %b %Y"), "); segment classification uses the trailing 12 weeks." )) ``` ### Interpretation guardrail Neither segment nor credit group is a performance category. They describe observed use. The table and chart align activity to the latest observation week, while the segment reflects sustained behaviour over its trailing 12-week window. Results should be reported for privacy-safe cohorts and interpreted alongside role, task mix, entitlement and organisational context. Methods and appendix {data-icon="fa-book"} ===================================== Row {data-height=410} ------------------------------------- ### Join and aggregation workflow ```{r workflow, fig.height=3.4} steps <- tibble( x = c(1, 3, 5, 7, 9), label = c( "Consumption query\nPerson x week", "Token and credit\nexposures", "Person Query\nPerson x week", "Join on PersonId\nand week", "Population and\nfunction views" ) ) ggplot(steps, aes(x, 1)) + geom_segment( aes(xend = x + 1.35, yend = 1), arrow = grid::arrow(length = grid::unit(0.14, "in")), colour = PAL[2], linewidth = 0.8 ) + geom_label( aes(label = label), fill = "#EAF2F8", colour = PAL[1], label.size = 0.25, size = 3.1, lineheight = 0.95 ) + coord_cartesian(xlim = c(0, 10.6), ylim = c(0.6, 1.4), clip = "off") + theme_void() + labs(title = "Time-aligned person-week panel") ``` ### Core rules | Question | Rule | |---|---| | Organisational attributes | Use a disclosed Person Query snapshot when exact date matching is unnecessarily restrictive | | Weekly relationships | Aggregate consumption to person-week, then join on `PersonId` and weekly `MetricDate` | | Credit quartiles | Calculate among positive users and retain non-users separately | | Reasoning profile | Require sufficient token volume before classifying the token mix | | Network measures | Treat internal and external network size as stock measures; never sum them across weeks | | Privacy | Suppress organisational groups below the approved minimum population | Row {data-height=390} ------------------------------------- ### Simulation design This dashboard is a demonstration of analytical possibilities. Every person, token, credit and Ways of Working value is synthetic. The simulation creates 1,200 people across eight functions and 26 weeks. Function and level affect both AI appetite and some Ways of Working measures so that raw associations contain realistic compositional confounding. Credit consumption is derived from input, output and reasoning tokens using synthetic relative weights. Reasoning tokens have the highest weight, which makes credit intensity responsive to token mix rather than merely to volume. The following effects are deliberately modest: - Higher consumption is associated with more chat and less email as a share of collaboration modes. - Total collaboration volume is raised mainly by organisational composition. - Internal network size has a small residual association after composition is considered. - Reasoning-intensive use has a weak positive association with after-hours collaboration and collaboration span. - External network size is a null. ### Synthetic source assets The uncompressed source-shaped CSVs sit under: ```text examples/utility-r/_data/consumption/ consumption-query/ person-query/ reference/ ``` They are intended for demonstrations, workshops and adaptation of the template. They contain no customer data or real identifiers. Row {data-height=430} ------------------------------------- ### Definitions and limitations **Credit intensity** means credits consumed per 1,000 tokens. It is preferred over “token cost-efficiency” because the report observes resource consumption rather than the quality or value of the output. **Reasoning-intensive** means the upper quartile of reasoning-token share among people with at least 10,000 observed tokens. It is preferred over “reckless” because expensive reasoning can be appropriate for a difficult task. The example Consumption export that motivated this template did not expose input, output and reasoning-token fields. A real implementation can only reproduce the reasoning profile when those governed token fields and applicable credit weights are available. If they are absent, omit the profile rather than inferring it from total credits. The dashboard shows associations. It does not establish that AI consumption caused a working pattern, and it does not observe output quality, task difficulty or business value. ### Replacing the synthetic data 1. Replace the CSVs in `_data/consumption` with schema-aligned governed exports. 2. Validate the identity crosswalk and disclose the match rate. 3. Confirm whether missing consumption rows mean zero activity, suppression or ineligibility. 4. Recompute credit weights from the applicable governed rate card. 5. Keep person-level averaging so each person carries equal analytical weight. 6. Retain the privacy threshold and the organisational composition checks.