LIVE Commerce Analytics / Builder session 6 of 8
Learn LIVE Commerce Analytics with Phoebe · Builder session 6 of 8

From traits to a playbook

Someone senior will ask you what the top streams share, and the honest answer is that the question has two answers. There is a traffic answer, which is about what the platform sent to the room, and there is a selling answer, which is about what the host did with the room once it was there. A regression run across streams can only ever give you the first. Tonight you run that regression, then run the one that gives the second, watch them disagree about the giveaway by a mile, and understand why both are right. Then you train a model that sides with the second, and write a playbook where every line carries the number that earned it.

🟠 Advanced Data scientists Anyone who has been asked what the top streams share 45 min
0-3 · Welcome 3-20 · Pooled, within, and a model 20-40 · Build-along: the two regressions 40-45 · Q&A
Part 0

Where session 6 sits

Session 5 scored every minute of the 60 Lumen Glow streams and produced the segment table: CTA minutes sell at 11.57 orders per thousand viewer-minutes, dead air at 0.71. That table is descriptive. It says which minutes sold, not what made them sell, and a CTA minute in a room of 1,500 and a CTA minute in a room of 300 are in it side by side. Tonight separates the room from the pitch, with two regressions and a tree model, and turns what survives into the playbook the leader track hands to hosts.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. Two regressions that disagree about the giveaway and are both right: pooled across streams the giveaway opener is worth plus 5,576 dollars of GMV, within a stream a giveaway minute costs 2.31 points of click-to-order rate. A LightGBM 4.7.0 model with SHAP 0.52.0, held out by stream, whose ranking sides with the second regression. And a playbook, written to data/derived/playbook.csv, whose every line carries its within-stream effect and its p-value.

LiveTwo answers to one question3 min▶

"What do the top streams share?" sounds like one question. Run it against the summary table and you get the traffic answer: the top ten by GMV are all human hosts, nine of the ten opened with a giveaway, and giveaway streams average 10,364 views against 7,546. That is real. The platform's boost rewards a giveaway opener, x1.93 against x1.25, and the room fills.

Run it against the minute table, comparing every stream with itself, and you get the selling answer: inside a stream, the minutes where the giveaway is live convert 2.31 points worse than the same stream's other minutes. Also real. The room is bigger and browsing for a free serum, and it clicks less on the card that costs 29 dollars.

The pooled regression sees streams, so it sees traffic. The within-stream regression sees minutes of one stream, so the traffic is held fixed and only the pitch is left to vary. Neither can answer the other's question, and the mistake is not picking the wrong one, it is not knowing there are two.

Real world

Lumen Glow's brand lead came to the data team with the top-ten list and a plan to make every stream open with a giveaway. The pooled number backed her: plus 5,576 dollars a stream. The within-stream number said the free serum was pulling a bigger, colder room. Both went on the same slide, on separate lines: a giveaway is a traffic decision, priced in views, and it is paid for in conversion. She kept the giveaway on the two weekend slots where the boost mattered most and dropped it elsewhere.

Self-studyThe vocabulary tonight leans on4 min▶
  • Pooled regression - one OLS over all rows as if they were exchangeable. Across streams it compares stream A to stream B; across minutes it compares minute 43 of stream A to minute 12 of stream B.
  • Fixed effects - a separate intercept per stream, so each stream is compared only with itself. Everything constant inside a stream (host, hour, weekday, the traffic draw, the giveaway opener) is absorbed and cannot confound.
  • Demeaning - the arithmetic that implements fixed effects: subtract each stream's mean from every column, then run OLS without an intercept. The stream dummies never have to be built.
  • Clustered standard errors - minutes of one stream are not independent draws; they share a room, a host, a mood. Clustering by stream tells the standard error to expect that, so the p-values are honest about 60 streams rather than pretending to 7,200 free observations.
  • Confound - a variable that moves both the treatment and the outcome. Here, the platform's boost moves both "giveaway" and "GMV", which is why the pooled coefficient is a traffic number wearing a giveaway costume.
  • SHAP - a per-row, per-feature share of a model's prediction, summing to the prediction. Mean absolute SHAP over rows is a feature ranking.
  • Held-out stream - a stream the model never trained on. Holding out rows instead lets the model see minute 42 and be asked about minute 43 of the same stream, which is not a test.
Part 1 · covers the confound, the pooled OLS and the within-stream fixed-effects regression

Pooled versus within

9 min live

Same data, same giveaway column, two lenses. The first lens looks across the 60 streams and finds a large positive coefficient with a p-value below one in ten million. The second looks inside each stream and finds a modest negative one. The diagram puts them side by side so you never again read one without asking for the other.

One giveaway column, two lenses left: streams compared with each other · right: minutes of one stream compared with the same stream Pooled: 60 streams, GMV per stream $30k $16k $4k no giveaway (28) mean $15,265 · boost x1.25 giveaway opener (32) mean $16,904 · boost x1.93 pooled OLS: +$5,576 · p below 1e-7 Within: one stream, minutes demeaned +8 pts 0 -8 pts minute 1 to 120, same stream giveaway live giveaway minutes: -2.31 pts CTOR (p .01) CTA minutes: +6.50 · dead air: -4.72 bars are each minute minus the stream mean within-stream OLS, SE clustered by stream Same column, opposite signs, both right. Left prices the traffic a giveaway buys; right prices what it does to the pitch.
🔍 Click to zoom - the pooled lens sees streams and therefore traffic; the within lens sees minutes of one stream and therefore the pitch.
LiveThe naive read and the pooled OLS3 min▶

Start where everyone starts, with the summary table sorted by GMV. Nine of the top ten opened with a giveaway, against 32 of 60 overall. Giveaway streams average 16,904 dollars of GMV against 15,265, and 10,364 views against 7,546. The eye says "giveaway wins", and a regression that treats streams as the unit says the same thing with a p-value attached.

The pooled model is gmv ~ giveawayOpen + C(hostType) + C(hour) on the 60-row summary. Controlling host type and hour is the responsible-looking move; it removes the AI-host penalty and the evening effect. The giveaway coefficient survives it: plus 5,576 dollars, p below 1e-7. That is not a fluke and it is not wrong. It is the platform's boost, x1.93 against x1.25, showing up as GMV, because GMV is traffic times selling and the giveaway bought traffic.

Look at the same summary table's other columns and the seam is already visible. Giveaway streams have a lower CTOR, 11.16 against 13.10, a lower GPM, 1,547 against 1,983, and a shorter watch, 8.67 against 9.38 minutes. More people, converting less. The pooled GMV regression cannot see that because GMV adds the two together.

LiveWithin-stream demeaning by hand3 min▶

Fixed effects sounds like a technique; it is a subtraction. For every column in the model, subtract that stream's mean from every one of its minutes. Then run OLS on the demeaned columns with no intercept. Whatever was constant within a stream is now zero everywhere, so the host, the hour, the weekday, the traffic draw and the giveaway opener all vanish from the comparison. What is left is minute 43 against minute 12 of the same stream. Standard errors are clustered by stream, because 120 minutes of one room are not 120 independent draws.

The outcome is CTOR in points, orders over clicks, and the regressors are the segment indicators, the three visual columns, and whether the giveaway is live in that minute.

RegressorCTOR, pointspRead
CTA minute+6.50below .001The ask moves the order
Price minute+3.78below .001Saying the number sells
Short urgency (run 1-2)+4.93below .001Two minutes of scarcity work
Stretched urgency (run 3+)-1.84.0001Minute three of "last chance" costs
Q&A minute+0.96.012Small, positive, real
Hook minute-3.18below .001Openers do not sell; they are not meant to
Filler minute-2.64below .001Nothing to buy into
Dead air-4.72.0013The worst minute you can have
Giveaway live-2.31.010A bigger, colder room
Product in hand+0.04.81No effect on the order
Close-up ratio+0.46.65No effect on the order
Price overlay-0.13.58No effect on the order

Three rows say nothing, and that is the finding of the night, not a disappointment. Product in hand, close-up ratio and a price on screen do not move CTOR. Run the identical regression with CTR as the outcome and the same three rows come alive.

RegressorCTR, pointsp
Product in hand+1.38below .001
Close-up ratio+1.01below .001
Price overlay+0.97below .001
CTA minute+3.28below .001
Demo minute+2.54below .001
Price minute+1.55below .001
Q&A minute-0.97below .001
Giveaway live-0.05.71
LiveReading the two tables together3 min▶

Put the CTR table over the CTOR table and a clean split appears. What you show moves the click. Product in hand, a close-up, a price card on screen: each adds about a point of CTR and none of them touches CTOR. The viewer taps the card because they saw the thing. What you say moves the order. CTA, price, a short burst of urgency: each adds points of CTOR, and the CTA adds points of both. The viewer buys because they were asked, told the number, and given a reason to do it now.

The Q&A row is the one worth pausing on. It lowers CTR by a point (the host is looking at comments, not the product) and raises CTOR by a point (the person whose question got answered converts). A per-viewer read from session 5 says Q&A minutes sell 3.68 orders per thousand viewer-minutes, below the mean of 5.80, so on its own it is a weak selling minute. Session 8's bench is where "answer the room every ten minutes" gets priced properly, and it comes out at plus 4.4 percent GMV with the longest watch of any single cue.

You do not have to demean by hand. linearmodels 7.0 does the same arithmetic in one line: PanelOLS(y, X, entity_effects=True) on a MultiIndex of (stream_id, t), then fit(cov_type="clustered", cluster_entity=True). Its coefficients match the hand version to the third decimal. Tonight's build-along does it by hand once, so you know exactly what the one line is doing and can explain it to the person who asks.

Real world

Maya's stream 17, Wednesday 21:00 with a giveaway opener, took 29,799 dollars on 16,404 views at a CTOR of 11.58. Priya's stream 16, Monday 18:00 with no giveaway, took 11,217 dollars on 4,858 views at 14.01. Ranked by GMV, Maya's stream is the one to copy. Ranked by GPM, 1,817 against 2,309, Priya's is. The pooled regression is ranking Maya's traffic. The within regression is measuring what each host did with each minute, and by that measure the Monday stream is the better pitch.

Self-studyWhere DoWhy and EconML pick up4 min▶

The within-stream regression answers "on average, inside a stream, what does a CTA minute do to CTOR". It does not answer "for which hosts, in which rooms, does it do the most". That is a heterogeneous-effects question, and two libraries in the panel are built for it.

  • DoWhy 0.14 makes the causal claim explicit before estimating it: CausalModel(data, treatment, outcome, graph), then identify_effect(), estimate_effect(), and a refute_estimate() step that adds a random common cause or a placebo treatment and checks the estimate survives. The graph is where you write down that the boost moves both the giveaway and the room, which is exactly what the pooled regression could not say.
  • EconML 0.17.0 estimates conditional effects: LinearDML or CausalForestDML with the minute's context (room size, minute of show, host) as the effect modifiers. The output is a per-minute estimate of what a CTA is worth, which is what a coach needs at minute 43 and a pooled average cannot give.

Both need the identification argument that fixed effects gave you tonight: within a stream, what varies is the pitch. The method ladder for that argument, from DAGs through backdoor adjustment to doubly robust estimators, is a course of its own: learn causal inference with phoebe.

Part 2 · covers LightGBM held out by stream, SHAP, and the playbook the leader track hands to hosts

A model that agrees

8 min live

A regression with hand-chosen columns is one witness. A gradient-boosted model that was never told which columns mattered is a second, and if its ranking lands on the same columns the within-stream table found, the playbook rests on two legs. If it lands somewhere else, you have learned something before a host did.

What the model leaned on: mean absolute SHAP, 12 held-out streams LightGBM 4.7.0 on per-minute orders · SHAP 0.52.0 TreeExplainer · bar length is the mean absolute SHAP value impressions 1.058 is_cta 0.684 close_up_ratio 0.463 product_in_hand 0.417 room 0.414 fomo_run 0.310 is_hook 0.261 price_overlay 0.193 is_price 0.192 is_ai 0.153 traffic: the room and its exposures content: what was said or shown who the host is Held-out error, orders/min model MAE 1.43 mean-only baseline 2.56 · the model halves it Read the shape, not the size Traffic dominates: impressions and room are 2 of the top 5. After them, the same columns the within table found: CTA, close-up, in hand, urgency. A SHAP value is a share of a prediction, not a causal effect. The within table is the evidence; the model confirms it.
🔍 Click to zoom - the model was never told which columns mattered, and it leaned on the traffic first and then on the same content columns the regression found.
LiveLightGBM, split by stream and not by row3 min▶

The model is LightGBM 4.7.0's LGBMRegressor on per-minute orders, with 24 features: the traffic columns (room, impressions), the content columns (the three visual scores, scene cuts, words per minute, the urgency run counter, the eight segment indicators), the giveaway state, the clock (hour, minute of show), the engagement counts, and whether the host is an AI avatar. The settings are modest on purpose: n_estimators=400, learning_rate=0.03, num_leaves=15, min_child_samples=40, subsample=0.8, subsample_freq=1, colsample_bytree=0.8, random_state=20260924. Fifteen leaves and forty minimum samples per leaf keep it from memorising individual minutes.

The split is the part that matters. Twelve streams, chosen by a seeded shuffle, are held out whole, every minute of them unseen by the model. Splitting by row would let the model train on minute 42 of stream 9 and be tested on minute 43 of the same stream, and adjacent minutes of one room are near copies of each other. That test scores brilliantly and tells you nothing about a stream you have not seen. On the 12 held-out streams the model's MAE is 1.43 orders per minute against 2.56 for predicting the mean; the model's error is a little over half the baseline's.

Say the baseline every time you say the score. "MAE 1.43" is a number. "MAE 1.43 against 2.56 for the mean" is a claim. The same rule returns in session 7 with the nowcast, where a naive baseline is the only thing standing between a forecast and a guess.

LiveSHAP and what the ranking says3 min▶

SHAP 0.52.0's TreeExplainer is exact for tree ensembles: for every held-out minute and every feature it returns the share of that minute's prediction the feature is responsible for, and the shares sum to the prediction. Take the absolute value, average over the held-out minutes, sort, and you have the ranking in the diagram.

Impressions is first at 1.058 and room is fifth at 0.414. That is the traffic, and it should dominate a model of order counts: a minute in a room of 1,500 has more orders than a minute in a room of 300 whatever the host does. The model is not being clever there. The interesting part is everything between and after: is_cta at 0.684, close_up_ratio at 0.463, product_in_hand at 0.417, fomo_run at 0.310, is_hook at 0.261, then price_overlay and is_price at 0.19. Those are, almost column for column, the rows of the two within-stream tables that had a p-value below .05. The model found them without being told, from a different objective (orders, not a rate) with a different method (trees, not demeaned OLS).

Note what is not in the top ten: giveaway_live and giveaway_open. In a model that already has the room and impressions as features, the giveaway has nothing left to explain. Its whole effect on orders was the traffic it bought, which the model reads directly from the room. That is the pooled-versus-within lesson again, from a third angle.

is_ai is tenth at 0.153. The host being an avatar matters, and it matters less than what the host is doing in the minute. On TikTok Shop the point is moot: the Requirements for High-Quality Videos and LIVEs, effective 2026-09-17, require real-time verbal interaction and do not permit AI-generated voices on a promotional LIVE, so the AI-host streams in this data describe a trade that is not on offer there.

LiveThe playbook, with a number on every line2 min▶

A playbook line a host will follow needs a verb, a moment and a number. The number comes from the within-stream table, because that is the one that compares a host with herself; the model is the corroborating witness, not the source. Here is the table the leader track's sessions 3 and 4 hand out, as the build-along writes it.

LeverMovesWithin-stream effectp
Say: the ask (CTA)CTOR+6.50 ptsbelow .001
Say: the priceCTOR+3.78 ptsbelow .001
Say: two minutes of urgency, then stopCTOR+4.93 pts (run 1-2)below .001
Never: stretch the urgency past twoCTOR-1.84 pts (run 3+).0001
Show: product in handCTR+1.38 ptsbelow .001
Show: a close-upCTR+1.01 ptsbelow .001
Show: the price on screenCTR+0.97 ptsbelow .001
Answer the roomCTOR+0.96 pts.012
Giveaway: a traffic decisionCTOR while live-2.31 pts.010

Short urgency on this data is minutes one and two of a run, with CTOR at 15.12 and 15.44 for those two minutes and 8.30 from the third. The playbook says two minutes because that is where the number turns, and because the TikTok Shop Content Policy (effective 2026-09-17) and the UK DMCC Act both treat a limited-time claim that is not genuine as a violation; a countdown that runs six minutes is not a countdown. The giveaway line is not a "never". It is worth 5,576 dollars of GMV through traffic on the pooled read and costs 2.31 points of CTOR while it is live, and whoever owns the traffic budget makes that call with both numbers in front of them.

Self-studyWhat the model cannot tell you3 min▶

A SHAP value is a share of a prediction. If the model had learned that CTA minutes tend to come after price minutes, and price minutes tend to be high-order minutes, the CTA feature would carry a large SHAP value for reasons that have nothing to do with the CTA. SHAP faithfully reports what the model used; it makes no promise that what the model used is what caused the orders. The within-stream regression, with the segment indicators entered together and the stream fixed, is the design that lets you read a coefficient as "holding the rest of the minute's content and the stream fixed, this segment does this". That is why the playbook cites the regression and merely mentions the model.

Two more things the model cannot say. It cannot price a cue the host has never done, because a tree has no leaf for it; the bench in session 8 is where a policy is actually applied. And it cannot separate "the host did a CTA" from "the host did a CTA at the right moment", because the moment is exactly the thing session 7's nowcast exists to find. The model tells you the CTA column matters. It does not tell you when.

Build-along

The two regressions, the model, and the playbook file

★ 20 min · everyone builds

Five steps. The pooled OLS in one statsmodels formula, the demeaning function you write yourself, the same fit with CTR as the outcome, LightGBM with the stream-level split and SHAP, and a playbook CSV with the evidence beside every line. statsmodels 0.15.0, LightGBM 4.7.0, SHAP 0.52.0 (Python 3.12 or later).

The pooled OLS on the summary table. Sixty rows, one per stream. Control host type and hour so the giveaway coefficient is not carrying the AI penalty or the evening effect, and watch it stay large anyway.

★ Pooled: what the giveaway opener is worth across streams import pandas as pd import numpy as np import statsmodels.formula.api as smf m = pd.read_csv("data/streams.csv") # 7,200 minute rows s = pd.read_csv("data/summary.csv") # 60 stream rows pooled = smf.ols("gmv ~ giveawayOpen + C(hostType) + C(hour)", data=s).fit() print(round(pooled.params["giveawayOpen"]), pooled.pvalues["giveawayOpen"]) # 5576 3.5e-08 -> a real number, about traffic print(s.groupby("giveawayOpen")[["gmv", "views", "cvr", "gpm", "avgWatchMin"]].mean().round(2)) # the same table shows cvr, gpm and watch all lower with the giveaway

Demean by stream, then OLS with no intercept and clustered errors. The function is the whole of fixed effects. The outcome is CTOR in points; minutes with zero clicks have no CTOR and are dropped, as analyze.py does.

★ Within: minutes compared with their own stream m["ctr"] = np.where(m.impressions > 0, m.clicks / m.impressions, np.nan) m["cvr"] = np.where(m.clicks > 0, m.orders / m.clicks, np.nan) mm = m.dropna(subset=["cvr"]).copy() mm["cvr100"] = 100 * mm["cvr"] mm["ctr100"] = 100 * mm["ctr"] for c in ["cta", "demo", "price", "fomo", "qna", "hook", "filler", "dead"]: mm[f"is_{c}"] = (mm.segment == c).astype(int) mm["fomo_short"] = ((mm.segment == "fomo") & (mm.fomo_run < 3)).astype(int) mm["fomo_stretched"] = ((mm.segment == "fomo") & (mm.fomo_run >= 3)).astype(int) X = ["is_cta", "is_demo", "is_price", "fomo_short", "fomo_stretched", "is_qna", "is_hook", "is_filler", "is_dead", "product_in_hand", "close_up_ratio", "price_overlay", "giveaway_live"] def within(df_, cols, y): """Subtract each stream's mean from every column: the stream fixed effect, by hand.""" g = df_.groupby("stream_id") d = df_[cols + [y]].copy() for c in cols + [y]: d[c] = df_[c] - g[c].transform("mean") return d def fe_fit(y): d = within(mm, X, y) fit = smf.ols(f"{y} ~ " + " + ".join(X) + " - 1", data=d).fit( cov_type="cluster", cov_kwds={"groups": mm["stream_id"]}) return pd.DataFrame({"coef": fit.params.round(2), "p": fit.pvalues.round(4)}) fe_cvr = fe_fit("cvr100") print(fe_cvr) # is_cta 6.50, is_price 3.78, fomo_short 4.93, fomo_stretched -1.84, giveaway_live -2.31, # product_in_hand 0.04 (p .81), close_up_ratio 0.46 (p .65), price_overlay -0.13 (p .58)

The CTR version. Same function, same regressors, the click rate as the outcome. The three visual rows that were flat on CTOR come alive here.

★ What you show moves the click fe_ctr = fe_fit("ctr100") print(fe_ctr.loc[["product_in_hand", "close_up_ratio", "price_overlay", "is_cta", "is_demo", "is_price", "is_qna"]]) # product_in_hand 1.38, close_up_ratio 1.01, price_overlay 0.97, is_cta 3.28, is_demo 2.54, # is_price 1.55, is_qna -0.97 : all p below .001 # the one-line equivalent with linearmodels 7.0, same numbers to the third decimal # from linearmodels.panel import PanelOLS # p = mm.set_index(["stream_id", "t"]) # res = PanelOLS(p["cvr100"], p[X], entity_effects=True).fit(cov_type="clustered", cluster_entity=True)

LightGBM with a stream-level split, then SHAP. Shuffle the stream ids with the seeded generator, hold twelve out whole, fit, score against the mean-only baseline, and rank features by mean absolute SHAP over the held-out minutes.

★ A second witness that was never told the answer import lightgbm as lgb import shap SEED = 20260924 rng = np.random.default_rng(SEED) feat = ["room", "impressions", "product_in_hand", "close_up_ratio", "price_overlay", "scene_cuts", "wpm", "fomo_run", "giveaway_live", "giveaway_open", "hour", "t", "replies", "comments", "likes"] \ + [f"is_{c}" for c in ["cta", "demo", "price", "fomo", "qna", "hook", "filler", "dead"]] mm["is_ai"] = (mm.host_type == "ai").astype(int) feat.append("is_ai") streams = mm.stream_id.unique() rng.shuffle(streams) test_ids = set(streams[:12]) # 12 whole streams, never seen in training tr, te = mm[~mm.stream_id.isin(test_ids)], mm[mm.stream_id.isin(test_ids)] model = lgb.LGBMRegressor(n_estimators=400, learning_rate=0.03, num_leaves=15, min_child_samples=40, subsample=0.8, subsample_freq=1, colsample_bytree=0.8, random_state=SEED, verbose=-1) model.fit(tr[feat], tr["orders"]) pred = model.predict(te[feat]) mae = np.mean(np.abs(pred - te["orders"])) base_mae = np.mean(np.abs(te["orders"].mean() - te["orders"])) print(round(mae, 2), round(base_mae, 2)) # 1.43 vs 2.56 expl = shap.TreeExplainer(model) sv = expl.shap_values(te[feat]) # one row per held-out minute; rows sum to the prediction imp = pd.Series(np.abs(sv).mean(axis=0), index=feat).sort_values(ascending=False) print(imp.head(10).round(3)) # impressions 1.058, is_cta 0.684, close_up_ratio 0.463, product_in_hand 0.417, room 0.414, ...

Write the playbook with its evidence. One row per lever, which metric it moves, the within-stream coefficient and its p-value. The model is not in the file; it is the reason you trust the file.

★ data/derived/playbook.csv rows = [ ("Say: the ask (CTA)", "CTOR", fe_cvr, "is_cta"), ("Say: the price", "CTOR", fe_cvr, "is_price"), ("Say: two minutes of urgency, then stop", "CTOR", fe_cvr, "fomo_short"), ("Never: stretch the urgency past two", "CTOR", fe_cvr, "fomo_stretched"), ("Show: product in hand", "CTR", fe_ctr, "product_in_hand"), ("Show: a close-up", "CTR", fe_ctr, "close_up_ratio"), ("Show: the price on screen", "CTR", fe_ctr, "price_overlay"), ("Answer the room", "CTOR", fe_cvr, "is_qna"), ("Giveaway: a traffic decision", "CTOR", fe_cvr, "giveaway_live"), ] playbook = pd.DataFrame( [{"lever": lever, "moves": moves, "within_effect": tbl.loc[k, "coef"], "p": tbl.loc[k, "p"]} for lever, moves, tbl, k in rows]) playbook.to_csv("data/derived/playbook.csv", index=False) print(playbook)

If your hand-demeaned coefficients and PanelOLS disagree, the dropped rows are the reason. The zero-click minutes with no CTOR have to be dropped before the demeaning, not after, or the stream means are computed on rows the regression never sees. analyze.py drops them first; so should you.

Homework

Before session 7

30 min
  1. Run the pooled minute-level regression, cvr100 on the same X with no demeaning, clustered by stream. Put its coefficients beside the within-stream ones and write one sentence per row where the two differ by more than a point. Every one of those rows is a confound with a name.
  2. Add time_effects=True to the PanelOLS call, so minute-of-show is absorbed as well as stream. Which coefficients move? The hook and the CTA both happen at predictable minutes, so this is a real test of whether the segment or the clock is doing the work.
  3. Re-run the LightGBM split with a different seed three times. Record the MAE and the top five SHAP features each time. Report the ranking as "in the top five on every draw" and "in the top five on some draws"; a ranking quoted from one draw is a single sample.
  4. Take stream 17 (Maya, giveaway) and stream 16 (Priya, no giveaway). For each, compute the mean of every X column, and write the two rows of the playbook each host follows least. That is the coaching note the leader track's session 5 would hand them.
Check yourself

Three questions

The pooled regression says a giveaway opener is worth plus 5,576 dollars of GMV. The within-stream regression says a giveaway minute costs 2.31 points of CTOR. Which is wrong?

Both are right about different questions. GMV is traffic times selling. The giveaway buys traffic (boost x1.93 against x1.25, views 10,364 against 7,546) and costs selling (CTOR 11.16 against 13.10). The pooled model sees the product; the within model sees the second factor alone.

Why is the LightGBM test set twelve whole streams rather than a random 20 percent of minute rows?

The unit the model will be used on is a stream nobody has seen. The test has to be that unit. Held out by stream, MAE is 1.43 against a mean-only 2.56; held out by row it would look far better and mean far less.

The mean absolute SHAP value for is_cta is 0.684, second only to impressions. What does that number tell you?

SHAP attributes a prediction to features. It is faithful to the model, not to the world. The playbook cites the within-stream coefficient (+6.50 points of CTOR, p below .001) as the evidence and the SHAP ranking as the confirmation that a second method found the same columns.

Sources covered

What tonight maps to

✓ Pooled versus within-stream regression, with the canon numbers Pooled OLS +5,576 dollars (p below 1e-7); within-stream CTOR and CTR tables from the demeaned, cluster-robust fit; the split between what you show and what you say.
✓ LightGBM 4.7.0 and SHAP 0.52.0, held out by stream LGBMRegressor with the seeded stream split, MAE 1.43 against 2.56, TreeExplainer ranking with impressions, is_cta, close_up_ratio, product_in_hand, room at the top.
✓ The playbook with per-line evidence Nine levers, each with the metric it moves, its within-stream coefficient and its p-value, written to data/derived/playbook.csv.
◐ linearmodels 7.0 PanelOLS Named as the one-line equivalent of the hand demeaning (entity_effects=True, clustered by entity). Homework adds time effects; the fit itself is not walked through.
◐ DoWhy 0.14 and EconML 0.17.0 Self-study only: where an explicit causal graph and conditional effects pick up from fixed effects. The method ladder lives in the causal inference course.
· Heterogeneous treatment effects Not covered. Tonight's coefficients are averages within streams; which hosts and rooms benefit most from each lever is an EconML question left to the causal inference course.

Builder session 6 cheat sheet

Two questions"What do top streams share" has a traffic answer and a selling answer. Pooled across streams gives only the first.
The giveaway, twicePooled: +5,576 dollars GMV, p below 1e-7. Within: -2.31 points CTOR while live, p .01. Both right.
Fixed effects is a subtractionSubtract each stream's mean from every column, OLS with no intercept, cluster SE by stream. PanelOLS(entity_effects=True) is the same line.
Say moves the orderCTOR: CTA +6.50, price +3.78, short urgency +4.93, stretched -1.84, Q&A +0.96, dead air -4.72.
Show moves the clickCTR: product in hand +1.38, close-up +1.01, price overlay +0.97. All three flat on CTOR.
Split by stream12 whole streams held out. MAE 1.43 vs mean-only 2.56. A row split tests memory, not generalisation.
SHAP rankingimpressions, is_cta, close_up_ratio, product_in_hand, room, fomo_run, is_hook, price_overlay, is_price, is_ai. Traffic first, then the same content columns.
A SHAP value is not an effectIt is a share of a prediction. The within table is the evidence; the model is the confirmation. Cite the regression.