{
  "nbformat": 4,
  "nbformat_minor": 5,
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.13.0"
    },
    "blog_metadata": {
      "topic": "How to Measure Copilot Cowork Adoption and Impact in Microsoft 365",
      "slug": "how-to-measure-copilot-cowork-adoption-and-impact-in-microso",
      "generated_by": "LinkedIn Post Generator + Azure OpenAI",
      "generated_at": "2026-07-01T22:11:26.036Z"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# How to Measure Copilot Cowork Adoption and Impact in Microsoft 365\n",
        "\n",
        "This notebook turns the blog post into a hands-on validation workflow for measuring Microsoft 365 Copilot adoption and impact. The focus is on separating access from adoption, building layered scorecards, segmenting by persona, and connecting usage patterns to workflow change, governance, and cost readiness."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "%pip install -q pandas requests matplotlib seaborn"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import io\n",
        "import os\n",
        "import json\n",
        "import math\n",
        "from textwrap import dedent\n",
        "\n",
        "import pandas as pd\n",
        "import requests\n",
        "import matplotlib.pyplot as plt\n",
        "import seaborn as sns\n",
        "\n",
        "pd.set_option('display.max_columns', None)\n",
        "pd.set_option('display.width', 120)\n",
        "sns.set_theme(style='whitegrid')"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Measurement model: from raw usage to executive scorecard\n",
        "\n",
        "The blog argues that better Copilot dashboards make bad measurement more dangerous, not less. A practical scorecard should separate access metrics, activation metrics, workflow behavior, and business plus governance outcomes.\n",
        "\n",
        "Below is a simple Python representation of the architecture described in the post."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "measurement_layers = {\n",
        "    'Layer 1: Access and readiness': [\n",
        "        'eligible users',\n",
        "        'licenses assigned',\n",
        "        'app and channel availability',\n",
        "        'security and compliance policy coverage',\n",
        "        'knowledge and data access hygiene',\n",
        "        'support readiness and training completion',\n",
        "    ],\n",
        "    'Layer 2: Activation': [\n",
        "        'weekly active Copilot users',\n",
        "        'repeat usage over multiple weeks',\n",
        "        'feature breadth across Microsoft 365 surfaces',\n",
        "        'manager participation',\n",
        "        'completion of onboarding and enablement steps',\n",
        "    ],\n",
        "    'Layer 3: Workflow behavior': [\n",
        "        'meeting follow-up completion patterns',\n",
        "        'email summarization habits',\n",
        "        'document drafting and revision behavior',\n",
        "        'recurring scenario usage by role',\n",
        "        'time reallocation signals',\n",
        "        'handoff quality in routine knowledge work',\n",
        "    ],\n",
        "    'Layer 4: Business and governance outcomes': [\n",
        "        'employee sentiment',\n",
        "        'support burden',\n",
        "        'cycle-time changes for targeted workflows',\n",
        "        'quality improvements in repeatable tasks',\n",
        "        'compliance posture',\n",
        "        'cost per meaningful use case',\n",
        "        'spend against usage-based AI consumption where applicable',\n",
        "    ],\n",
        "}\n",
        "\n",
        "for layer, metrics in measurement_layers.items():\n",
        "    print(f'\\n{layer}')\n",
        "    for item in metrics:\n",
        "        print(f'  - {item}')"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for Microsoft Graph authentication\n",
        "\n",
        "The next example acquires a Microsoft Graph access token using client credentials. Set these environment variables before running it:\n",
        "\n",
        "- `TENANT_ID`\n",
        "- `CLIENT_ID`\n",
        "- `CLIENT_SECRET`\n",
        "\n",
        "This pattern is useful for automating repeatable scorecards instead of relying on screenshots."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Acquire a Microsoft Graph access token\n",
        "\n",
        "This example requests an app-only token for Microsoft Graph. In practice, the app registration must have the appropriate Microsoft Graph application permissions and admin consent for reports and directory data."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "tenant_id = os.environ.get('TENANT_ID')\n",
        "client_id = os.environ.get('CLIENT_ID')\n",
        "client_secret = os.environ.get('CLIENT_SECRET')\n",
        "\n",
        "if not all([tenant_id, client_id, client_secret]):\n",
        "    print('Missing one or more required environment variables: TENANT_ID, CLIENT_ID, CLIENT_SECRET')\n",
        "else:\n",
        "    token_url = f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token'\n",
        "    payload = {\n",
        "        'client_id': client_id,\n",
        "        'client_secret': client_secret,\n",
        "        'scope': 'https://graph.microsoft.com/.default',\n",
        "        'grant_type': 'client_credentials',\n",
        "    }\n",
        "\n",
        "    response = requests.post(token_url, data=payload, timeout=30)\n",
        "    response.raise_for_status()\n",
        "    access_token = response.json()['access_token']\n",
        "    print(access_token[:40] + '...')"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for usage report extraction\n",
        "\n",
        "The next example pulls a Microsoft 365 Copilot usage detail report from Microsoft Graph. Set this environment variable before running it:\n",
        "\n",
        "- `GRAPH_TOKEN`\n",
        "\n",
        "The token should be valid for Microsoft Graph and authorized for the relevant reporting endpoint."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Pull a Microsoft 365 Copilot usage report CSV\n",
        "\n",
        "This example follows the pattern described in the blog: call the Graph reports endpoint, capture the redirect or download location, and load the CSV into pandas. It is illustrative and report fields may vary over time."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "graph_token = os.environ.get('GRAPH_TOKEN')\n",
        "url = \"https://graph.microsoft.com/v1.0/reports/getMicrosoft365CopilotUsageUserDetail(period='D30')\"\n",
        "\n",
        "if not graph_token:\n",
        "    print('Missing GRAPH_TOKEN environment variable')\n",
        "else:\n",
        "    headers = {'Authorization': f'Bearer {graph_token}'}\n",
        "    response = requests.get(url, headers=headers, timeout=60, allow_redirects=False)\n",
        "    response.raise_for_status()\n",
        "\n",
        "    download_url = response.headers.get('Location', response.text.strip())\n",
        "    csv_response = requests.get(download_url, timeout=60)\n",
        "    csv_response.raise_for_status()\n",
        "\n",
        "    usage_df = pd.read_csv(io.StringIO(csv_response.text))\n",
        "    print(usage_df.head(3).to_string(index=False))\n",
        "    print('\\nColumns:', list(usage_df.columns))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Compare assigned access with active usage cohorts\n",
        "\n",
        "The blog emphasizes that license assignment is not the same thing as active value realization. This example creates a simple cohort model to distinguish assigned users, activated users, and habitual users."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "licenses_df = pd.DataFrame([\n",
        "    {'userPrincipalName': 'alex@contoso.com', 'copilotAssigned': True},\n",
        "    {'userPrincipalName': 'maya@contoso.com', 'copilotAssigned': True},\n",
        "    {'userPrincipalName': 'li@contoso.com', 'copilotAssigned': True},\n",
        "    {'userPrincipalName': 'sam@contoso.com', 'copilotAssigned': True},\n",
        "])\n",
        "\n",
        "usage_df = pd.DataFrame([\n",
        "    {'userPrincipalName': 'alex@contoso.com', 'activeDays': 12},\n",
        "    {'userPrincipalName': 'maya@contoso.com', 'activeDays': 4},\n",
        "    {'userPrincipalName': 'li@contoso.com', 'activeDays': 9},\n",
        "])\n",
        "\n",
        "cohorts = licenses_df.merge(usage_df, on='userPrincipalName', how='left').fillna({'activeDays': 0})\n",
        "summary = {\n",
        "    'assigned': int(cohorts['copilotAssigned'].sum()),\n",
        "    'activated_once': int(cohorts['activeDays'].gt(0).sum()),\n",
        "    'habitual_users': int(cohorts['activeDays'].ge(8).sum()),\n",
        "}\n",
        "summary['activation_rate'] = round(summary['activated_once'] / summary['assigned'], 2)\n",
        "summary['habit_rate'] = round(summary['habitual_users'] / summary['assigned'], 2)\n",
        "\n",
        "print('Cohort detail:')\n",
        "print(cohorts.to_string(index=False))\n",
        "print('\\nSummary:')\n",
        "print(summary)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Visualize access versus activation versus habit\n",
        "\n",
        "A single adoption number hides important distinctions. This quick chart makes it easier to see why activation rate and habit rate should be reviewed separately."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "plot_df = pd.DataFrame({\n",
        "    'metric': ['assigned', 'activated_once', 'habitual_users'],\n",
        "    'count': [summary['assigned'], summary['activated_once'], summary['habitual_users']]\n",
        "})\n",
        "\n",
        "ax = sns.barplot(data=plot_df, x='metric', y='count', palette='Blues_d')\n",
        "ax.set_title('Copilot access and adoption cohorts')\n",
        "ax.set_xlabel('')\n",
        "ax.set_ylabel('Users')\n",
        "for i, row in plot_df.iterrows():\n",
        "    ax.text(i, row['count'] + 0.03, str(row['count']), ha='center')\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Reshape usage into an executive scorecard by persona\n",
        "\n",
        "The blog argues that tenant-wide averages can mislead leaders. This example groups usage by persona and period, then calculates leading indicators and a simple productivity proxy for scorecarding."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "usage_df = pd.DataFrame([\n",
        "    {'userPrincipalName': 'alex@contoso.com', 'persona': 'Sales', 'period': 'Last30Days', 'activeDays': 12, 'promptActions': 88, 'assistedMeetings': 6},\n",
        "    {'userPrincipalName': 'maya@contoso.com', 'persona': 'HR', 'period': 'Last30Days', 'activeDays': 4, 'promptActions': 15, 'assistedMeetings': 1},\n",
        "    {'userPrincipalName': 'li@contoso.com', 'persona': 'Sales', 'period': 'Last30Days', 'activeDays': 9, 'promptActions': 52, 'assistedMeetings': 4},\n",
        "])\n",
        "\n",
        "scorecard = (\n",
        "    usage_df.assign(\n",
        "        leading_active_users=usage_df['activeDays'].gt(0).astype(int),\n",
        "        leading_power_users=usage_df['activeDays'].ge(8).astype(int),\n",
        "        lagging_productivity_proxy=usage_df['assistedMeetings'] + usage_df['promptActions'] / 20.0,\n",
        "    )\n",
        "    .groupby(['persona', 'period'], as_index=False)\n",
        "    .agg(\n",
        "        assigned_users=('userPrincipalName', 'nunique'),\n",
        "        active_users=('leading_active_users', 'sum'),\n",
        "        power_users=('leading_power_users', 'sum'),\n",
        "        avg_prompt_actions=('promptActions', 'mean'),\n",
        "        productivity_proxy=('lagging_productivity_proxy', 'mean'),\n",
        "    )\n",
        ")\n",
        "\n",
        "print(scorecard.to_string(index=False))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Visualize persona-level scorecard indicators\n",
        "\n",
        "This chart highlights why role segmentation matters. Different personas can show very different patterns of repeat use and workflow assistance even when overall tenant averages look healthy."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n",
        "\n",
        "sns.barplot(data=scorecard, x='persona', y='power_users', ax=axes[0], palette='Greens_d')\n",
        "axes[0].set_title('Power users by persona')\n",
        "axes[0].set_xlabel('Persona')\n",
        "axes[0].set_ylabel('Users')\n",
        "\n",
        "sns.barplot(data=scorecard, x='persona', y='productivity_proxy', ax=axes[1], palette='Purples_d')\n",
        "axes[1].set_title('Productivity proxy by persona')\n",
        "axes[1].set_xlabel('Persona')\n",
        "axes[1].set_ylabel('Average proxy score')\n",
        "\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Build a layered executive scorecard with readiness, activation, workflow, and governance\n",
        "\n",
        "The blog recommends reviewing multiple measurement layers together every month. The next example creates a compact scorecard that combines access, activation, workflow, and governance indicators in one table."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "executive_scorecard = pd.DataFrame([\n",
        "    {\n",
        "        'month': '2026-06',\n",
        "        'eligible_users': 500,\n",
        "        'licenses_assigned': 420,\n",
        "        'policy_coverage_pct': 96,\n",
        "        'training_completion_pct': 78,\n",
        "        'weekly_active_users': 260,\n",
        "        'repeat_users_4w': 210,\n",
        "        'manager_participation_pct': 64,\n",
        "        'meeting_followup_completion_pct': 71,\n",
        "        'email_summarization_users': 180,\n",
        "        'drafting_workflow_users': 155,\n",
        "        'employee_sentiment_score': 3.9,\n",
        "        'support_tickets': 24,\n",
        "        'compliance_exceptions': 3,\n",
        "        'copilot_spend_usd': 18400,\n",
        "    }\n",
        "])\n",
        "\n",
        "executive_scorecard['assignment_rate'] = (executive_scorecard['licenses_assigned'] / executive_scorecard['eligible_users']).round(2)\n",
        "executive_scorecard['activation_rate'] = (executive_scorecard['weekly_active_users'] / executive_scorecard['licenses_assigned']).round(2)\n",
        "executive_scorecard['habit_rate'] = (executive_scorecard['repeat_users_4w'] / executive_scorecard['licenses_assigned']).round(2)\n",
        "executive_scorecard['cost_per_repeat_user'] = (executive_scorecard['copilot_spend_usd'] / executive_scorecard['repeat_users_4w']).round(2)\n",
        "\n",
        "print(executive_scorecard.to_string(index=False))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Baseline first, then assess impact\n",
        "\n",
        "The blog stresses that impact claims are weak without a before state. This example compares baseline and post-rollout workflow metrics for targeted scenarios such as meeting follow-up and recurring document drafting."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "baseline_vs_after = pd.DataFrame([\n",
        "    {'workflow': 'Meeting follow-up', 'baseline_cycle_time_hours': 6.5, 'after_cycle_time_hours': 4.2, 'baseline_quality_score': 3.6, 'after_quality_score': 4.1},\n",
        "    {'workflow': 'Recurring summary drafting', 'baseline_cycle_time_hours': 4.0, 'after_cycle_time_hours': 2.7, 'baseline_quality_score': 3.8, 'after_quality_score': 4.2},\n",
        "    {'workflow': 'Email summarization', 'baseline_cycle_time_hours': 1.8, 'after_cycle_time_hours': 1.2, 'baseline_quality_score': 3.7, 'after_quality_score': 4.0},\n",
        "])\n",
        "\n",
        "baseline_vs_after['cycle_time_reduction_pct'] = (\n",
        "    (baseline_vs_after['baseline_cycle_time_hours'] - baseline_vs_after['after_cycle_time_hours'])\n",
        "    / baseline_vs_after['baseline_cycle_time_hours'] * 100\n",
        ").round(1)\n",
        "\n",
        "baseline_vs_after['quality_improvement'] = (\n",
        "    baseline_vs_after['after_quality_score'] - baseline_vs_after['baseline_quality_score']\n",
        ").round(2)\n",
        "\n",
        "print(baseline_vs_after.to_string(index=False))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Simulate a scheduled extract-and-enrich flow in Python\n",
        "\n",
        "The blog recommends a simple scheduled process: request usage and licensing data, enrich with persona mappings, classify indicators, and export a scorecard for weekly review. This example simulates that ETL flow with local data."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "usage_extract = pd.DataFrame([\n",
        "    {'userPrincipalName': 'alex@contoso.com', 'activeDays': 12, 'promptActions': 88, 'assistedMeetings': 6},\n",
        "    {'userPrincipalName': 'maya@contoso.com', 'activeDays': 4, 'promptActions': 15, 'assistedMeetings': 1},\n",
        "    {'userPrincipalName': 'li@contoso.com', 'activeDays': 9, 'promptActions': 52, 'assistedMeetings': 4},\n",
        "    {'userPrincipalName': 'sam@contoso.com', 'activeDays': 0, 'promptActions': 0, 'assistedMeetings': 0},\n",
        "])\n",
        "\n",
        "license_extract = pd.DataFrame([\n",
        "    {'userPrincipalName': 'alex@contoso.com', 'copilotAssigned': True},\n",
        "    {'userPrincipalName': 'maya@contoso.com', 'copilotAssigned': True},\n",
        "    {'userPrincipalName': 'li@contoso.com', 'copilotAssigned': True},\n",
        "    {'userPrincipalName': 'sam@contoso.com', 'copilotAssigned': True},\n",
        "])\n",
        "\n",
        "persona_map = pd.DataFrame([\n",
        "    {'userPrincipalName': 'alex@contoso.com', 'persona': 'Sales', 'managerWave': 'Wave1'},\n",
        "    {'userPrincipalName': 'maya@contoso.com', 'persona': 'HR', 'managerWave': 'Wave1'},\n",
        "    {'userPrincipalName': 'li@contoso.com', 'persona': 'Sales', 'managerWave': 'Wave2'},\n",
        "    {'userPrincipalName': 'sam@contoso.com', 'persona': 'Operations', 'managerWave': 'Wave2'},\n",
        "])\n",
        "\n",
        "etl_df = (\n",
        "    license_extract\n",
        "    .merge(usage_extract, on='userPrincipalName', how='left')\n",
        "    .merge(persona_map, on='userPrincipalName', how='left')\n",
        "    .fillna({'activeDays': 0, 'promptActions': 0, 'assistedMeetings': 0})\n",
        ")\n",
        "\n",
        "etl_df['activated_once'] = etl_df['activeDays'].gt(0)\n",
        "etl_df['habitual_user'] = etl_df['activeDays'].ge(8)\n",
        "etl_df['workflow_proxy'] = etl_df['assistedMeetings'] + etl_df['promptActions'] / 20.0\n",
        "\n",
        "weekly_review = (\n",
        "    etl_df.groupby(['persona', 'managerWave'], as_index=False)\n",
        "    .agg(\n",
        "        assigned=('copilotAssigned', 'sum'),\n",
        "        active=('activated_once', 'sum'),\n",
        "        habitual=('habitual_user', 'sum'),\n",
        "        avg_workflow_proxy=('workflow_proxy', 'mean')\n",
        "    )\n",
        ")\n",
        "weekly_review['activation_rate'] = (weekly_review['active'] / weekly_review['assigned']).round(2)\n",
        "weekly_review['habit_rate'] = (weekly_review['habitual'] / weekly_review['assigned']).round(2)\n",
        "\n",
        "print('Enriched ETL dataset:')\n",
        "print(etl_df.to_string(index=False))\n",
        "print('\\nWeekly review scorecard:')\n",
        "print(weekly_review.to_string(index=False))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Governance and cost belong in the same conversation as adoption\n",
        "\n",
        "The blog warns that rising usage with weak governance is not success. This example combines adoption, compliance, and spend indicators so leaders can review scale decisions with a fuller picture."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "governance_cost_df = pd.DataFrame([\n",
        "    {'persona': 'Sales', 'assigned_users': 120, 'active_users': 95, 'compliance_exceptions': 1, 'monthly_spend_usd': 5200, 'meaningful_use_cases': 70},\n",
        "    {'persona': 'HR', 'assigned_users': 60, 'active_users': 28, 'compliance_exceptions': 0, 'monthly_spend_usd': 1800, 'meaningful_use_cases': 20},\n",
        "    {'persona': 'Operations', 'assigned_users': 80, 'active_users': 34, 'compliance_exceptions': 3, 'monthly_spend_usd': 2600, 'meaningful_use_cases': 18},\n",
        "])\n",
        "\n",
        "governance_cost_df['activation_rate'] = (governance_cost_df['active_users'] / governance_cost_df['assigned_users']).round(2)\n",
        "governance_cost_df['cost_per_meaningful_use_case'] = (\n",
        "    governance_cost_df['monthly_spend_usd'] / governance_cost_df['meaningful_use_cases']\n",
        ").round(2)\n",
        "\n",
        "governance_cost_df['scale_readiness_flag'] = governance_cost_df.apply(\n",
        "    lambda row: 'Review' if row['compliance_exceptions'] > 1 or row['activation_rate'] < 0.5 else 'Healthy',\n",
        "    axis=1\n",
        ")\n",
        "\n",
        "print(governance_cost_df.to_string(index=False))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## References used in this notebook\n",
        "\n",
        "- Microsoft 365 Copilot hub: https://learn.microsoft.com/en-us/microsoft-365/copilot/\n",
        "- Copilot Dashboard in Viva Insights: https://learn.microsoft.com/en-us/viva/insights/org-team-insights/copilot-dashboard\n",
        "- Adoption training path: https://learn.microsoft.com/en-us/training/paths/explore-how-drive-adoption-microsoft-copilot-m365/\n",
        "- Microsoft 365 Copilot usage report: https://learn.microsoft.com/en-us/microsoft-365/admin/activity-reports/microsoft-365-copilot-usage\n",
        "- Microsoft 365 Copilot APIs overview: https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/copilot-apis-overview\n",
        "- Usage-based billing and Copilot Credits: https://learn.microsoft.com/en-us/microsoft-365/copilot/usage-based-billing-manage-copilot-credits\n",
        "- Setup and license assignment guidance: https://learn.microsoft.com/en-us/microsoft-365/copilot/microsoft-365-copilot-setup"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Next Steps\n",
        "\n",
        "Use this notebook as a starting point for a real Copilot measurement operating model. Replace the sample data with Microsoft Graph extracts, enrich with Entra ID or HR persona mappings, establish workflow baselines before expansion, and review one combined scorecard that includes access, activation, workflow change, governance, and cost.\n",
        "\n",
        "The key validation question is not whether licensed users are up. It is whether specific roles are showing measurable, governable, repeatable workflow change worth scaling."
      ]
    }
  ]
}