rcmisk
DISTRIBUTION

what are you not asking your own data?

INVALID DATE · 4 MIN READ · ON RCMISK.COM

my own analytics told me the post format i'd used all week was my worst performer.

twelve posts. every single one in that format.

the data had been sitting there the whole time. i just never asked it anything.

the setup: every post lands in a table

before any of this makes sense, here's the plumbing.

every x post i publish gets recorded as a row the moment it goes out. an agent tags it with its format, the day and slot it went out, my follower count at the time, and how many other times i posted that day. i learned to record the confounders too, because a post's reach is never just about the format.

then, on a schedule, a job pulls the live metrics off the x api and writes a snapshot of each post's numbers at a fixed age. not "impressions right now" but "impressions at 24 hours old," so i'm always comparing posts at the same stage of their life. a post from last week has had seven extra days to collect impressions. comparing its raw total to a fresh post is meaningless.

it's a plain sqlite file. three tables do the work:

-- what format was this post? (written when the post goes out)
CREATE TABLE assignment (
  content_id  TEXT NOT NULL,   -- the post
  arm_id      INTEGER NOT NULL, -- the format bucket
  posted_at   TEXT,
  day_of_week INTEGER,
  posts_that_day          INTEGER, -- confounder
  follower_count_at_post  INTEGER, -- confounder
  PRIMARY KEY (content_id)
);

-- the format buckets: "statement opener", "question closer", "listicle", ...
CREATE TABLE arm (id INTEGER PRIMARY KEY, label TEXT NOT NULL);

-- metric snapshots, captured at fixed ages by the sync job
CREATE TABLE observation (
  content_id TEXT NOT NULL,
  age_hours  REAL NOT NULL,     -- 24, 48, 72 ...
  numerator  REAL,              -- impressions
  PRIMARY KEY (content_id, age_hours)
);

the hardest part of this wasn't the schema. it was the tagging. my first version asked me to label each post's format by hand. that discipline rotted in about four days. so i moved the tagging onto the same scheduled job that syncs the metrics. the rule i keep relearning: a step that depends on me remembering is a step that doesn't happen.

the question i finally asked

one night i actually queried it. group posts by format, average the impressions at 24 hours, sort worst to best.

SELECT arm.label            AS format,
       COUNT(*)             AS posts,
       ROUND(AVG(o.numerator)) AS avg_impressions
FROM observation o
JOIN assignment a ON a.content_id = o.content_id
JOIN arm        ON arm.id = a.arm_id
WHERE o.age_hours = 24
GROUP BY arm.label
ORDER BY avg_impressions ASC;

the format i'd leaned on all week came dead last.

i almost posted the confession right there. "i wasted a week on my worst format." good hook, honest, a little self-deprecating, the kind of post that tends to do well. i had the draft open.

then i re-ran it with one change.

the part where i fooled myself

AVG(). i had compared averages. means.

the "typical post" bucket i was measuring against had four posts from april sitting in it that went semi-viral. four lucky outliers, quietly hauling the whole average up. i wasn't comparing my new format to a normal post. i was comparing it to a normal post plus a handful of once-a-quarter spikes.

the fix is one word, median, but sqlite has no MEDIAN(). that's the tell. the wrong query is a single built-in function. the right one makes you work for it:

WITH ranked AS (
  SELECT arm.label AS format,
         o.numerator AS impressions,
         ROW_NUMBER() OVER (PARTITION BY arm.label ORDER BY o.numerator) AS rn,
         COUNT(*)     OVER (PARTITION BY arm.label)                      AS n
  FROM observation o
  JOIN assignment a ON a.content_id = o.content_id
  JOIN arm        ON arm.id = a.arm_id
  WHERE o.age_hours = 24
)
SELECT format,
       ROUND(AVG(impressions)) AS median_impressions
FROM ranked
WHERE rn IN ((n + 1) / 2, (n + 2) / 2)  -- middle row(s)
GROUP BY format
ORDER BY median_impressions DESC;

swap mean for median and the picture flips. the format i'd just called my worst performer actually beats my typical post. those twelve posts were fine. better than fine.

median, not mean. opposite conclusion.

the real lesson isn't median

it would be easy to stop at "use median." true, but small.

the bigger thing is that the tool had been there the entire time. i spent a week making a call by vibe, then nearly made a second call off a query i never sanity-checked. the data didn't fail me twice. i failed it twice. once by not asking, once by asking sloppily.

that's the pattern i keep hitting as a solo founder. i have more instrumentation than i use. analytics on the site, stripe on the revenue, every post i've ever written sitting in a table i can query in seconds. the constraint was never access. it was the discipline to ask, and the half-second of doubt to ask again.

what the data says this week

with the tagging automated and the median query wired into the scheduled job, the honest read as of now: statement-style openers edge out question-closers, and both beat listicles. posts with no link outperform posts with a link. the samples are still small, so i hold all of it loosely and let the job keep counting.

but the meta-question is the one worth passing on.

what are you not asking your own data?

you almost certainly have the answer to something that's been nagging you, sitting in an export you've never opened. go ask it. then check whether you asked it right.

the next one, while it's happening
one letter a week. real numbers, win or loss.