learn-product-analytics-with-phoebe / Analyst session 7 of 8
Learn Product Analytics with Phoebe · Analyst track · Session 7 of 8

AI feature metrics

An AI feature makes an offer every few seconds, and the offer can be taken, kept, changed or thrown away. Each of those moments is a different metric, and they do not have to agree. Tonight we build a code-assistant event stream in pandas, compute acceptance rate, persistence and characters kept for two versions of the same assistant, and watch the winner change with the metric. Then we look under the average at the developers it is made of.

🟠 Hands-on Product and data analysts Python + pandas + numpy 45 min
0-3 · Welcome 3-18 · Schema, three metrics, the spread 18-40 · Build-along: the constructed stream 40-45 · Q&A
Part 0

Where we start

Six sessions ran on real GitHub rows. This one cannot: no AI-feature event stream is public, so tonight's data is constructed. A short numpy script, seed 2022, generates one working week for two versions of a code assistant, 200 developers each: "short" suggests a line, "long" suggests a whole block. Every rate inside it is an assumption written in the script, and every number on this page is what that script prints. The definitions, and the findings about what the metrics mean, come from a real study read at source: Ziegler et al. (2022), "Productivity Assessment of Neural Code Completion".

🎫 Clear all three quiz questions to stamp your passport Live = presented in the 45 minutes Self-study = the full depth, after class ★ Build-along = run the code yourself

★ What you walk out with tonight. An event schema for an AI suggestion, a pandas table that computes three competing metrics per version, the habit of reporting the per-user spread next to any average, and a sentence you can say in a product review: "which of these metrics matches the decision we are making?"

Part 1 · covers the event schema for an AI feature

One suggestion, followed for ten minutes

6 min live

A normal product event records something that happened once. An AI suggestion is a small story: it is shown, then accepted or not, then, if accepted, it survives, gets changed, or disappears. The schema has to carry the whole story on one row, or the metrics that read its later chapters cannot be computed. Tonight's stream is constructed, and its row is the minimum that makes three metrics possible.

ONE SUGGESTION, FOLLOWED FOR TEN MINUTES t = 0 t = 30 s t = 600 s 1 · shown a row is written 2 · accepted? Tab pressed 3 · kept at 30 s? still in the file 4 · kept at 600 s changed, not gone deleted by 600 s persistence partly rewritten the offer is gone acceptance rate kept 30 s ONE ROW PER SUGGESTION SHOWN arm · dev · accepted · kept_30s · kept_600s · deleted_600s · chars booleans, except chars, an integer Constructed stream, seed 2022. Ziegler et al. 2022 checked persistence at 30, 120, 300 and 600 seconds.
🔍 Click to zoom - acceptance reads station 2, persistence reads the gold box; one row has to carry both
LiveThe row, field by field3 min▶

The event is suggestion_shown, one row per offer. arm says which version of the assistant made it, dev says who saw it. accepted is the moment the developer took it. kept_30s and kept_600s say whether the accepted code was still there, unchanged, 30 seconds and 10 minutes later; deleted_600s says it was gone by 10 minutes. chars is the size of the offer. Every later flag is only meaningful when accepted is true, which is why the metrics divide by accepted rows, not by shown ones.

In a real product the later flags arrive later, as their own events joined back to the suggestion by an id. Storing them as columns on one row, as tonight's stream does, is the shape you end up with after that join. Plan the suggestion id before the feature ships: without it, "kept at 600 s" cannot be tied to the offer it describes.

Self-studyZiegler's definitions, read at source2 min▶

Ziegler et al. define acceptance rate as "the fraction of completions shown to the developer that are subsequently accepted for inclusion in the source file". In code that is accepted.sum() / shown, or in pandas the mean of a boolean column. For what happens after acceptance they measure persistence at four checkpoints, 30, 120, 300 and 600 seconds. Tonight's stream keeps two of them, 30 and 600, which is enough to see the difference between a quick check and a lasting one.

Their productivity outcome came from a survey built on the SPACE dimensions (they used S, P, C and E), matched to each respondent's usage: 17,420 users were emailed and 2,047 responses were matched to usage data, between 10 February and 6 March 2022.

Real world

Same name, different fraction. In the Ziegler study Copilot's acceptance rate was 27 percent, and the paper mentions a click-through rate of 10 percent for IntelliCode Compose. Those two numbers look comparable and are not: different products, different ways of showing a suggestion, different denominators. Before putting two AI features' "acceptance" side by side, read what each one divides by. The definition is part of the number.

Part 2 · covers three metrics that disagree

Three metrics, two answers

5 min live

Run the constructed week and ask which version of the assistant is better. Acceptance says "long": 0.309 against 0.262. Characters kept per suggestion shown says "long" by a wide margin: 20.183 against 6.486. Persistence says "short": 0.618 of its accepted suggestions are still there, unchanged, at 600 seconds, against 0.408 for "long". None of the three is wrong. They answer different questions.

THREE METRICS, TWO ANSWERS constructed stream · 200 developers per arm · one week ACCEPTANCE RATE accepted of shown 0.309 0.262 long short long wins KEPT AT 600 S of accepted, unchanged 0.408 0.618 long short short wins CHARS KEPT PER SHOWN characters still there at 600 s 20.183 6.486 long short long wins Constructed, seed 2022: your numbers will differ if you change it. Each panel has its own scale.
🔍 Click to zoom - the gold bar is the only one that reverses the verdict, and it is the one that asks whether the code survived
LiveWhat each metric is a proxy for3 min▶

Acceptance rate measures the moment of the offer: did the developer think it was worth taking. Persistence measures what the code turned out to be worth: of the accepted suggestions, how many were still there, unchanged, ten minutes later. "Long" is accepted more (0.309) and survives less (0.408 kept, 0.190 deleted by 600 seconds); "short" is accepted less (0.262) and survives more (0.618 kept, 0.078 deleted). Characters kept per shown multiplies survival by size, and a block that survives less often still leaves more text behind than a line that survives more often: 20.183 against 6.486.

So the question is never "which metric is right". It is "which decision are we making". Choosing a default for developers who hate cleaning up after the tool points at persistence. Estimating how much typing the tool saves points at characters kept. Neither table proves that switching versions would change anything for real users: that needs an experiment (see Learn Experimentation with Phoebe).

Self-studyThe 30-second check is kinder than the 600-second one2 min▶

At 30 seconds "long" keeps 0.693 of its accepted suggestions and "short" keeps 0.800. At 600 seconds the figures are 0.408 and 0.618. The early check flatters both arms, and in this constructed stream it flatters the block more, because the script assumes a big suggestion is more often rewritten after the first glance. Pick the checkpoint before you look at the results, and report it with the number: "kept" without a time is not a definition.

Real world

The metric that tracks perception. Ziegler et al. found that "the rate with which shown suggestions are accepted, rather than more specific metrics regarding the persistence of completions in the code over time, drives developers' perception of productivity". Acceptance rate correlated best with aggregate perceived productivity, rho = 0.24, P < 0.0001, and every persistence measure correlated less well. The authors add that "there is still notable unexplained variance". Read it carefully: acceptance predicts how productive developers feel, modestly, in one study. It is a correlation with a survey answer, not proof of value, and it does not make persistence useless for the decisions persistence is built for.

Part 3 · covers the per-developer spread

The average is not a developer

4 min live

An arm's acceptance rate is one number made of 200 developers. In the constructed stream, the middle 80 percent of "long" developers accept between 0.177 and 0.467 of what they are shown; for "short" the range is 0.127 to 0.390. The two ranges overlap over most of their width. Many individual developers on the "worse" arm accept more than many on the "better" one, and a single average says none of that.

WHERE THE 200 DEVELOPERS OF EACH ARM SIT long (block) 200 developers short (line) 200 developers 0.309 0.177 0.467 arm average 0.262 0.127 0.390 both arms' middle developers sit here 0 0.1 0.2 0.3 0.4 0.5 per-developer acceptance rate, 10th to 90th percentile ZIEGLER ET AL. 2022, ACCEPTANCE BY TIME, READ AT SOURCE 23.5% weekend 23% weekday, outside working hours 21.2% working hours Top: constructed, 10th to 90th percentile per arm. Bottom: the real study's acceptance by time.
🔍 Click to zoom - the two averages sit inside one shared band; the difference between arms is smaller than the difference between developers
LiveReport the spread with the average2 min▶

The fix is one extra line of pandas: group by developer first, take each developer's acceptance rate, then describe that distribution with its 10th and 90th percentiles. Put the band next to the arm average in every review. It changes the conversation from "long is better" to "long shifts the whole band up a little, and the band is wide", which is the honest reading.

Two cautions. First, 200 developers per arm is a small group: percentiles from groups that size move noticeably with the seed, so treat the band's edges as rough. Second, the average of per-developer rates and the pooled rate are different numbers when developers see different amounts; a heavy user counts once in the first and many times in the second. Say which one you report.

Self-studyThe same metric moves with the clock2 min▶

Ziegler et al. report that acceptance also varies with time: 23.5 percent at weekends, 23 percent on weekdays outside working hours and 21.2 percent in working hours. So a change in the mix of when people use the tool can move the headline rate with no change in the tool at all. A launch that happens to land near a holiday, or a team that shifts its hours, is enough. Break acceptance down by time of use before reading a week-on-week movement as the feature getting better or worse.

Real world

The vendor says it too. GitHub's own documentation describes acceptance rate as a measure of "how often developers accept Copilot's suggestions", and advises readers to "look for patterns across these signals rather than focusing on any single number". That is the spread rule and the three-metrics rule in one sentence, from the people who ship the tool. When a dashboard shows one acceptance number for a whole organisation, ask for the distribution and the other signals before anyone draws a conclusion.

Build-along

Build the constructed stream and compute all three metrics

★ 22 min · everyone builds

numpy and pandas, no downloads. The script below generates the constructed week from seed 2022 and prints every number on this page. Run it exactly as written and you get the outputs in the comments; change the seed or any assumed rate and your numbers will differ, which is the fastest way to feel how much a metric depends on what you assumed.

FIVE STEPS, ONE SEED 1 · assumptions rates per arm 2 · generate 200 devs per arm 3 · one table a row per shown 4 · metrics grouped by arm 5 · the spread per developer seed 2022 119,905 events 0.309 vs 0.262 0.177 to 0.467 Constructed data: change the seed or any rate and every number moves. Watching them move is the lesson.
🔍 Click to zoom - step 1 is where every number on this page is really decided, so it is written down first

Write the assumptions down. Two arms, and for each the rates the generator will use. On constructed data this block is the whole truth; everything later only reveals it.

★ Step 1 · constructed: seed and assumed ratesimport numpy as np import pandas as pd rng = np.random.default_rng(2022) # Two versions of one assistant, 200 developers each, one working week. # "short" suggests a line; "long" suggests a whole block. Every rate below is an assumption. ARMS = { # accept kept 30s kept 600s deleted by 600s chars "short": (0.26, 0.80, 0.62, 0.08, 40), "long": (0.31, 0.70, 0.41, 0.19, 160), }

Generate a week per developer. Each developer gets their own acceptance probability from a beta distribution, so developers differ, and a Poisson number of suggestions. One uniform draw per suggestion decides what happens after acceptance, so "kept at 600 s" implies "kept at 30 s".

★ Step 2 · constructed: the generation looprows = [] for arm, (p_acc, k30, k600, p_del, chars) in ARMS.items(): for dev in range(200): p_dev = rng.beta(p_acc * 20, (1 - p_acc) * 20) # developers differ shown = rng.poisson(60 * 5) # a week of suggestions acc = rng.random(shown) < p_dev u = rng.random(shown) kept30 = acc & (u < k30) kept600 = acc & (u < k600) # kept at 600s implies kept at 30s deleted = acc & (u > 1 - p_del) n_chars = rng.poisson(chars, shown) rows.append(pd.DataFrame({"arm": arm, "dev": f"{arm}-{dev:03d}", "accepted": acc, "kept_30s": kept30, "kept_600s": kept600, "deleted_600s": deleted, "chars": n_chars}))

One table. Concatenate and count the rows: one per suggestion shown.

★ Step 3 · constructed: the event tableev = pd.concat(rows, ignore_index=True) print("suggestion_shown events:", len(ev)) # suggestion_shown events: 119905

Three metrics, grouped by arm. Acceptance divides by shown; the kept and deleted rates divide by accepted; the character metrics divide by shown, so they combine how often, how much and how long.

★ Step 4 · constructed: metrics by armg = ev.groupby("arm") out = pd.DataFrame({ "shown": g.size(), "acceptance_rate": g["accepted"].mean(), "kept_30s_of_accepted": g.apply(lambda d: d.kept_30s.sum() / d.accepted.sum()), "kept_600s_of_accepted": g.apply(lambda d: d.kept_600s.sum() / d.accepted.sum()), "deleted_of_accepted": g.apply(lambda d: d.deleted_600s.sum() / d.accepted.sum()), "chars_accepted_per_shown": g.apply(lambda d: d.chars[d.accepted].sum() / len(d)), "chars_kept_600s_per_shown": g.apply(lambda d: d.chars[d.kept_600s].sum() / len(d)), }) print(out.round(3).to_string()) # arm shown acceptance kept_30s kept_600s deleted chars_acc/shown chars_kept_600s/shown # long 60023 0.309 0.693 0.408 0.190 49.455 20.183 # short 59882 0.262 0.800 0.618 0.078 10.487 6.486

Look under the average. One rate per developer, then the 10th and 90th percentiles per arm.

★ Step 5 · constructed: the per-developer spreadper_dev = ev.groupby(["arm", "dev"])["accepted"].mean() print("\nper-developer acceptance rate, 10th to 90th percentile:") print(per_dev.groupby("arm").quantile([0.1, 0.9]).unstack().round(3).to_string()) # per-developer acceptance rate, 10th to 90th percentile: # arm 0.1 0.9 # long 0.177 0.467 # short 0.127 0.390

The comments are shortened column names. pandas prints the full names from the dictionary and its own spacing; the values are the ones above. If your run disagrees in the third decimal, check your numpy version before your code: numpy does not promise identical distribution draws across releases, even from the same seed.

Homework

Before session 8

30 min
  1. Add the missing checkpoints. Ziegler et al. measured persistence at 30, 120, 300 and 600 seconds. Add kept_120s and kept_300s to the generator with rates you choose, write them down as assumptions, and plot kept-of-accepted against time for both arms.
  2. Flip the verdict on purpose. Change one assumed rate in ARMS until characters kept per shown favours "short". Write one sentence on what that rate would have to mean about real developers for the flip to be believable.
  3. Pooled versus per-developer. Compute each arm's mean of per-developer acceptance rates and compare it with the pooled acceptance rate from step 4. Explain in one line why they can differ.
  4. Draft the review line. For a product review, write the three metrics, the per-developer band and the word "constructed" on one slide, and one sentence saying which metric you would pick for a default-setting decision, and why.
Check yourself

Three questions

Clear all three and analyst session 7 gets stamped in your passport.

1 · On the constructed stream, "long" has the higher acceptance rate (0.309) and "short" the higher kept-at-600-seconds rate (0.618). What is the right conclusion?

Acceptance reads the moment of the offer, persistence reads what survived. Disagreement between them is information, and on constructed data it only shows how the metrics behave, never which product is better.

2 · What did Ziegler et al. find about acceptance rate and perceived productivity?

A modest correlation with a survey answer, in one study. It says acceptance tracks how productive developers feel, not that it measures the value of the code.

3 · Why report the per-developer 10th to 90th percentile next to an arm's acceptance rate?

The band shows whether a difference between arms is large next to the difference between people. With groups this small, treat the band's edges as rough too.

Sources covered

What this session teaches, and where it came from

✓Acceptance rate definition and persistence checkpointsZiegler, A., Kalliamvakou, E., Simister, S., Sittampalam, G., Li, A., Rice, A., Rifkin, D., and Aftandilian, E. (2022). "Productivity Assessment of Neural Code Completion." MAPS '22. arXiv:2205.06537. Sections 2 and 3, read at source
✓Acceptance and perceived productivity, rho = 0.24; acceptance by time of useSame paper, abstract and sections 4 and 5, read at source
✓"Look for patterns across these signals"GitHub Docs, "Copilot metrics" concepts page, read at source
◐The code-assistant event stream and every number in the build-alongConstructed from seed 2022 by the code on this page; the rates are assumptions, not observations of any real product
◐A real AI-feature event streamNone is public; the gap is why the stream above is constructed

Every fact on this page, and its verification tier, is recorded in the course's source map.

Analyst session 7 cheat sheet · pin this

One row per suggestion shownaccepted, kept_30s, kept_600s, deleted_600s, chars, joined by a suggestion id.
Acceptance divides by shownZiegler: the fraction of shown completions accepted into the file.
Persistence divides by acceptedAlways say the checkpoint: 30, 120, 300 or 600 seconds.
Metrics can disagreeConstructed: long wins acceptance and chars kept, short wins kept at 600 s.
Perception, not valueAcceptance tracked perceived productivity at rho = 0.24, with unexplained variance.
Band beside the averagePer-developer 10th to 90th percentile, and a note when groups are small.