learn-web3-with-phoebe / Practitioner session 1 of 10
Learn Web3 with Phoebe · Practitioner session 1 of 10

Reading a chain as a dataset

Put the ledger down for a moment and start where you always start: what is a row, what are the columns, and what is the grain. The answers are unusual in a specific and learnable way. The natural grain is the transaction, the natural key is an address, the schema belongs to code somebody else deployed, and there is no upstream team to raise a ticket with. Tonight is those four facts, measured on Larkfield's generated month, plus the honest admission that the raw form barely supports analysis at all.

🔧 Practitioner track Data engineers who would have to build this Analysts who query what it produces 45 min
0-3 · Welcome 3-17 · Rows, columns, grain 17-30 · A schema you do not own 30-45 · Build-along and Q&A
Part 0

Larkfield hands you a connection string and walks away

Larkfield ships a product that issues rewards on a public ledger, and somebody has decided the data team should be able to report on it. How the ledger is built, agreed and kept tamper evident is settled and is not tonight's subject; that belongs to learn-blockchain-with-phoebe, which is the prerequisite here. Tonight starts at the point an engineer usually starts, with a source that nobody has profiled yet, and the first honest hour spent working out what is actually in it.

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

★ What you walk out with tonight. A correct answer to the three questions you ask of any new source, for this one: the row, the columns, the grain. The reason the natural key looks exactly like a customer id and behaves exactly unlike one. A clear-eyed comparison against a normal ingestion, with the losses and the gains kept apart rather than blended. And a measured look at the raw form on Larkfield's month, which will make the case for session 2 better than any argument could.

LiveThe three questions, before anything else4 min▶

Every engineer has the same opening move on an unfamiliar source and it is not reading the documentation. It is: show me one row, tell me what the columns mean, and tell me what one row is one of. Grain first, because every aggregation you will ever write is wrong if the grain is wrong, and a wrong grain does not raise an error. It quietly doubles a count.

The move works here too, which is the useful news. What changes is the answers. One row is a transaction. The columns are thin and one of them is opaque until you decode it. The grain has two defensible readings rather than one, and which of them you pick determines whether a whole category of analysis is available to you later. None of that requires new engineering instincts. It requires the ordinary instincts applied to a source that will punish skipping the step.

Real world

Larkfield's first analyst skipped the grain question because the source looked simple. They wrote a count over a table that had been exploded to one row per transaction input, reported it as transactions, and were about eleven percent high. Nothing errored, nothing looked odd, and the number was wrong for two months. That is the specific failure the grain question exists to prevent, and it is not a chain problem. It is the oldest problem there is, arriving through a new door.

Self-studyThe words that keep coming back4 min▶
  • Address. The only identifier a row carries. Persistent, and not a person. Part 1 tonight, taken apart in session 3.
  • Entity. The real party behind one or more addresses. Never present in the data.
  • Transaction. One record. It names input addresses, output addresses and a payload, and it sits at a known position in a known block.
  • Payload. The part of the record that is meaningless until something decodes it against the definition the deploying code used. Session 2.
  • Grain. What one row is one of. Here there are two answers, one per transaction and one per transaction input, and both are used.
  • Co-spend. A single transaction spending from two addresses at once. Visible only at the input grain. Session 3.
  • Retraction. The upstream withdrawing history you already loaded and reported. Sessions 6 and 7.
  • Idempotency, late-arriving data, append-only loads. General pipeline craft, and it all applies. It lives in learn-data-engineering-with-phoebe and is not repeated here.
Self-studyWhere the ten sessions go3 min▶

Two sessions on getting the data into a usable shape, three on counting things that resist being counted, two on the upstream taking history back, and three on what all of that does to a model, a bench and a review.

  • 1 and 2. The dataset as it arrives, then the pipeline that turns it into tables anybody can query.
  • 3, 4 and 5. The co-spend heuristic and union-find, how to measure what it got right, and what happens when some of the parties are being paid to be hard to count.
  • 6 and 7. A chain reorganisation landing in a warehouse that already published, and the two separate defences against it.
  • 8, 9 and 10. Training on data where your features and your model are both visible to the people generating the rows, a bench that fooled its own author, and the whole Larkfield estate driven live.
Part 1 · covers the shape of the data

One row, thin columns, and a grain with two answers

14 min live

Start with the record itself, because everything downstream is a consequence of its shape. A transaction names some input addresses, some output addresses and a payload, and it sits at a position in a block. That is close to all of it. The interesting part is not what is there; it is that the one relationship the record does state is the one relationship everybody ignores, and the column that looks most like a key is the one that is not.

Show me one row, tell me the columns, tell me what one row is one of QUESTION 1 · WHAT IS ONE ROW INPUTS p12-0 p12-3 ONE TRANSACTION written into one block OUTPUT s2-14 THE COLUMNS THE ROW CARRIES block height, position in the block, input addresses, output addresses, and a payload that means nothing until something decodes it. Two addresses authorised one spend, so one party controls both. That is the only relationship any row here states. QUESTION 2 · WHAT IS THE GRAIN, AND WHY THERE ARE TWO ANSWERS Grain: one row per transaction TX INPUT ADDRESSES OUTPUT t1 [p12-0, p12-3] s2-14 t2 [p44-1] p7-0 t3 [p88-0, p88-2] s0-31 The co-spend is real but it is trapped inside an array cell. Counting rows here counts transactions, which is usually what you want. Grain: one row per transaction input TX INPUT ADDRESS ROLE t1 p12-0 input 1 t1 p12-3 input 2 t2 p44-1 input 1 Two rows now share a transaction id, which is the join session 3 needs. Counting rows here counts inputs, and reporting that as transactions is high. Question 3, the key: every row names an address, and an address passes for a key in every mechanical sense. It is stable, it is unique, it never goes null, and it will satisfy any constraint you put on it. It simply is not one value per party. One person routinely holds several and one party can hold sixty on purpose, and no external table exists that would tell them apart. Session 3 runs the standard method for closing that gap and session 4 measures how far it gets.
🔍 Click to zoom - the grain choice is not cosmetic. One of the two makes the co-spend queryable and the other buries it in an array.
LiveWhat one row is, and the relationship it does state4 min▶

The record is thinner than most sources you will have ingested. There is no event name you did not have to derive, no user column, no timestamp you can trust to mean what a product timestamp means, and no free-text field to profile. In exchange there is total completeness: nothing was sampled, nothing was dropped by a collector, and no agent failed to fire. That combination is rare and it changes where the risk sits. The risk is not missing rows. It is misreading the rows you have.

The one relationship a record does state is the one worth noticing on day one. When a transaction draws on two input addresses, whoever built that transaction had to be able to authorise both. That is a fact about control, stated by the structure of the record rather than inferred from behaviour, and it is the only such fact in the whole dataset. Most teams skate past it because it looks like plumbing. It is the entire basis of every entity count anyone will ever ask you for.

Everything else you might want about a party - who they are, where they are, what they bought, whether they came back - is not thin, it is absent. There is no column with nulls in it. There is no column.

LiveThe grain question has two correct answers4 min▶

One row per transaction is the natural grain and it is what most people mean when they say a chain is a list of transactions. It is the right grain for counting activity, for time series, for anything where the unit of interest is an event. It has one property worth writing on a wall: the input addresses are a collection inside a single row, so the co-spend relationship exists in your table but is not joinable.

One row per transaction input is the exploded grain. The same transaction id now appears on several rows, one per input address, and suddenly the co-spend is a self join rather than an array operation. That is the shape session 3 needs. It is also the shape that produces a wrong transaction count the first time somebody writes count(*) against it, because the row count is now the input count.

Keep both. Neither is a compromise and neither replaces the other. The failure is not choosing wrongly, it is choosing silently, so that six months later nobody in the room can say which grain the table downstream of them is at. Write it into the table name if you have to.

LiveThe key that behaves like a key and is not one3 min▶

An address is the most key-like thing in the dataset. It is stable over time, it is unique as a string, it never arrives null, it joins cleanly, and every profiling tool you point at it will report a well behaved high-cardinality identifier. A uniqueness test on it passes. A referential integrity test on it passes. Nothing you can automate will tell you anything is wrong.

What is wrong is the cardinality of the thing behind it. On Larkfield's generated month there are about 1,000 addresses and 304 real parties, and no column, table, vendor or request will move you from the first number to the second. That is a 3.2 times error sitting inside a column that passes every check you own, which is a genuinely unusual failure mode and the reason it survives so long in real reporting.

Sessions 3, 4 and 5 are the response to this. Tonight the only obligation is to stop reading the column as an identity and start reading it as what it is: a handle on a spending capability, one party may hold many.

Self-studyProfiling a source with no owner to correct you4 min▶

Standard profiling still works and you should still do it: cardinality per column, null rates, value distributions, the widest and narrowest records, the tails. Point ydata-profiling or whatever you normally reach for at a month of it and read the report the way you always would. The craft does not change.

What changes is the follow-up. Normally a surprise in a profile becomes a question to the owning team, and the answer is either "yes that is a bug" or "no, here is the business reason". Here the surprise is the answer. If the widest transaction has two inputs and you expected more, that is simply what the source does, and there is nobody to confirm it and nobody to change it. Your profile is not a draft to be validated. It is the specification, and you wrote it by looking.

The practical consequence is to profile more often and to keep the profiles. A distributional shift here is not a data quality incident that somebody will explain to you; it is the only warning you will get that the thing producing the rows has changed behaviour.

Part 2 · covers the schema and the ingestion

A schema you do not own, and nobody to raise a ticket with

13 min live

Now the part that feels strangest to an engineer who has spent a career negotiating with upstreams. There is no upstream to negotiate with. Not an unresponsive one, not a slow one, not one behind three layers of ticketing. There is nobody. Four things you normally rely on are missing, and it is worth being precise about which of the four is a loss, because two of them are not.

The same four parts, drawn twice. Only the upstream owner was removed. A PIPELINE YOU NEGOTIATED SOURCE SYSTEM Owned by a team with a name. CONTRACT + SLA Agreed columns, agreed lateness. YOUR LOADER Decodes nothing. It arrives typed. WAREHOUSE Reported from, and restated. You can ask. Fix a row, explain a change, send me the backfill, widen my access. THE SAME FOUR PARTS, ON A PUBLIC LEDGER THE LEDGER No owner, and no one to email. NO CONTRACT The schema is whatever was deployed. YOUR INDEXER Decodes it itself. That is session 2. WAREHOUSE Reported from, and not restated. Nothing to ask, and nobody to ask it of. No SLA, no lateness guarantee, no change notice, no backfill request. And no request to make. No gatekeeper, no grant, no review, and no scope that anyone can narrow later. Two of these absences are a loss and two are a gain, and every one of them is the same missing thing. The owner who would have promised you a schema is the owner who would have decided whether you were allowed to read it. You cannot delete the second without deleting the first. Anyone selling you the gain without the cost is selling half a mechanism. What replaces the contract is your own decode, held as code, versioned by you. Session 2 is entirely about that step.
🔍 Click to zoom - the struck return path is the whole session in one mark. Every recovery habit an engineer has runs along that line.
Live Larkfield's month, profiled in this page by the same generator every session uses. Change the month and watch which quantities are stable.

...

...
LiveNo SLA, and no gatekeeper either4 min▶

Take the four absences one at a time and price them honestly, because a session that only lists the costs will be dismissed by anyone who has actually enjoyed working on this data, and they are not wrong to enjoy it.

No SLA is a real loss. Nothing promises you that rows arrive within a window, that the shape stays constant, or that anything at all continues. Your alerting cannot be a breach notification because there is no promise to breach. It has to be a distributional check you wrote against a baseline you measured.

No contract negotiation is a real loss and a small saving. You never spend six weeks agreeing a payload with another team. You also never get to ask for a field, and the field you need is frequently not there.

No backfill request is a gain wearing the clothes of a loss. You cannot ask anyone for history, and you do not need to. The whole history is sitting there and you can re-read it whenever you like, as many times as you like, without anybody's sprint capacity. Re-deriving five years of a metric after finding a bug is a week of compute rather than a quarter of politics. Almost nothing in a normal warehouse works this way.

No gatekeeper is the biggest gain and the one that gets understated. There is no access request, no approval, no scoped credential, no quarterly review, and no possibility of your access being narrowed because a team reorganised. The dependency that most often kills an analysis is the one that does not exist here.

LiveThe schema is code somebody else deployed3 min▶

The payload in a transaction is bytes. What those bytes mean is decided by the definition that the deploying code published, and different deployments publish different definitions for what a business would call the same event. You do not get a schema registry handed to you; you get a decision to make about how to interpret each family of records, and that decision is yours, which means it is also your bug when it is wrong.

Then it moves. A new version is deployed with a different definition, and your decode keeps running against the old one. Nothing fails. The rows still parse, or they stop matching and silently fall out of your filter, and either way the first symptom is a metric bending in a direction nobody ordered. There was no deprecation notice because there is no relationship in which a notice could be sent.

This is the single most important operational fact in the whole track and it is why the next session is about the indexer rather than about the chain. The decode is not a technical detail sitting under the pipeline. The decode is the pipeline's opinion about what happened, and it is where every disagreement between two teams' numbers turns out to live.

Everything the rows hold, everything they support, and everything that was asked WHAT THE ROWS GIVE YOU About 1,000 addresses. 4,000 transaction records. Input and output addresses. A block position on each. A payload nothing decoded. Complete. Nothing sampled, nothing dropped by a collector. WHAT YOU CAN ACTUALLY RUN count(*) RUNS count(distinct address) RUNS group by user NO COLUMN group by country NO COLUMN join to plan NO TABLE WHAT WAS ACTUALLY ASKED How many people used it? Did any of them come back? Which country are they in? Which reward actually worked? Not one of these is answerable from the panel on the left, and two of them never will be. The two queries that run answer a question nobody asked. This distance is not a defect in the source. It is the normal condition of raw data anywhere, and closing it is what a pipeline is for. The difference here is that no producing team has closed the first two thirds of it already, so the whole distance is yours to cross.
🔍 Click to zoom - the amber column is the honest inventory. Two queries run, three cannot be written, and two of those three never will be.
LiveWhy the raw form is close to useless3 min▶

Look back at the live box. The counts are correct, they were computed rather than asserted, and almost none of them answers a question anybody would ask. You can say how many transactions happened and how many addresses appear. You cannot say how many people, which activity was a reward and which was something else, whether anybody came back, or what any of it means in the product's own vocabulary.

That is not a criticism of the source and it is not a gap to be complained about. It is the normal condition of raw data everywhere. The difference is the size of the distance and who is expected to close it. On a normal source the producing team has already done the first two thirds of that work before you see a row. Here nobody has done any of it, and the whole of it is yours.

Which is the honest case for session 2, so it may as well be said plainly: the thing you are going to build is not a connector. It is an interpretation layer, it will be most of the work, and the numbers it produces will be your numbers rather than the chain's.

Self-studyWhere serious people disagree5 min▶

One camp says land the raw records first and keep them forever. Their argument is that your decode is an interpretation and interpretations are wrong, so the only reproducible artefact is the undecoded record, byte for byte, exactly as it was read. Keep that and you can re-derive any table from any point with a corrected decoder, and prove to an auditor that the derivation, not the data, was what changed. Throw it away and every historical figure is hostage to a decoder version you no longer have.

The other camp says that is a comforting story that does not survive contact with a real restatement. The raw bytes are only meaningful against the definitions in force at the time they were written, so preserving them without also preserving a complete, versioned map of those definitions preserves nothing but the illusion of reproducibility. And the cost is not theoretical: raw records are large, they are queried by nobody, and the reprocessing run that was supposed to justify them is quietly never scheduled. Better, on this view, to store the decoded rows plus the decoder version that produced them, and to treat a decode change as a schema migration with a proper backfill.

Both camps have shipped this. They are not really arguing about storage, they are arguing about which artefact carries the meaning: the bytes or the interpretation. Notice that the second camp's position depends on actually versioning the decoders, which most teams claim to do and few do. Ask the person in front of you which of the two they hold, and then ask them where their decoder versions are stored. The gap between those two answers is the real risk.

Part 3 · build-along

Answer the three questions on a source you own

15 min hands on

No code tonight. Six steps on a pipeline your own team runs, said out loud in the room. The aim is to find out how much of your current practice is standing on an upstream owner, because that is the part that will not survive the move.

State the grain of a table you own

Pick a table your team publishes. Say out loud what one row is one of, without looking at the code. If two people in the room give different answers, you have found tonight's most useful result and it has nothing to do with chains.

Find your exploded twin

Every warehouse has a pair of tables at two grains where one is the other exploded. Name yours. Then say how a new joiner would know which is which, and whether the answer is anything other than asking someone.

Name your upstream owner out loud

Who do you message when a row is wrong? Write the name of an actual person. Then delete the name and say what your last three data incidents would have looked like without them.

Count your ticket-shaped dependencies

How many things in your last month needed somebody else to agree: a backfill, a new field, wider access, a schema explanation. Mark which of those would be unnecessary on a source that is fully readable by everyone, and which would simply be impossible.

Look for a key that is not one entity

Device id, cookie, account, email, phone, session. Find one your reporting treats as a person. Say what its real cardinality per person is, and whether anything in your test suite would notice if it doubled.

Write the profile you would keep

For the Larkfield month in the live box, write down the three quantities you would baseline and alert on if nobody was ever going to tell you the source had changed. Bring them to session 2 and check them against the pipeline you would build.

The habit to keep. When a source has no owner, your profile stops being a draft and becomes the specification. So keep it, date it, and diff it. The question is never "is this value surprising", because there is nobody to be surprised at. The question is "is this value different from the one I measured last month, and did anything in my code change to explain it".

Check yourself

Three questions

An engineer builds one table at the transaction-input grain and reports a monthly transaction count from it. What happens?

The exploded grain has one row per input, so any transaction that spends from more than one address is counted more than once. There is no error, no null and no failed test. This is the ordinary grain mistake, and it is worth rehearsing precisely because the source looks too simple for it to happen.

A team runs a full profiling suite over the address column. Uniqueness passes, nulls are zero, cardinality is high and stable. What has the suite established?

Every mechanical property of a key holds. The property that fails is the one no profiler measures: values per real party. On Larkfield's month that is about 1,000 addresses against 304 parties, a 3.2 times error living inside a column that passes every test you own.

Which of the four missing pieces of a normal ingestion is best described as a gain rather than a loss?

No SLA and no contract are straightforward losses. The absent gatekeeper and the absent backfill queue are gains, and they come from exactly the same missing upstream owner as the losses do. An unowned schema drifts more easily than an owned one, not less, because nothing is obliged to announce the change.

Before session 2

Homework

Twenty minutes, on a pipeline you run and on the profile you wrote in step 6.

Trace one field back to its decision

Pick one column in a table you publish and find who decided what it means. Not who populates it, who decided. Write down how you would answer the same question if that person did not exist and never had.

Bring three baselines and one decode question

Your three quantities from step 6, and one thing in the Larkfield rows you would have to interpret rather than read. Session 2 builds the interpretation layer and puts a number on how much two teams can disagree while both being right.

Covered tonight

What this session covers

✓What one row isA transaction: input addresses, output addresses, a position in a block, and a payload nothing has decoded yet.
✓The grain, and its two correct answersOne row per transaction, one row per transaction input, and which analysis each one makes possible.
✓The key that is not a keyAn address passes every mechanical test for an identifier and is not one value per party.
✓A schema you do not ownDefined by deployed code, changeable without notice, and interpreted by your decode rather than handed to you.
✓Ingestion without an upstream ownerNo SLA and no contract, and equally no backfill queue and no gatekeeper. The same absence produces all four.
✓The raw form profiled honestlyComputed counts for a Larkfield month, and the list of business questions none of them answers.
◐Decode, normalise, enrich, aggregateNamed tonight as the work the raw form forces on you. Session 2 builds it and says where two teams diverge.
◐Getting from addresses to partiesSet up tonight as the gap the key leaves. Session 3 runs the co-spend heuristic and union-find in code.
◐An upstream that retracts historyNamed tonight in the vocabulary. Sessions 6 and 7 measure it and design against it.
○Hashes, blocks, consensus and chain mechanicsThe prerequisite is learn-blockchain-with-phoebe and it is assumed rather than repeated.
○Idempotency, late-arriving data and append-only loadsGeneral pipeline craft, and it lives in learn-data-engineering-with-phoebe.
○Probabilistic record linkageFellegi-Sunter, blocking and string comparison live in learn-entity-resolution-with-phoebe.
○Anything to do with price, markets or holdingsOut of scope by design, and nothing here is advice of any kind.
Take this with you

Cheat sheet

The row. A transaction: input addresses, output addresses, a block position, and a payload that means nothing yet.
The grain. Two answers. Per transaction for activity, per transaction input for anything about common control.
The key. An address passes every uniqueness and null test you own, and is not one value per party.
The one stated relationship. Two addresses in one transaction means one party could authorise both. Nothing else in the data says anything about who.
The schema. Owned by deployed code, changed without notice, and interpreted by your decode. Your decode, your bug.
The four absences. No SLA, no contract, no backfill queue, no gatekeeper. Two losses, two gains, one missing owner.
The profile is the spec. Nobody will validate your reading of the source. Measure it, date it, diff it.
The honest admission. Raw, this answers almost nothing a business asks. The interpretation layer is the job, and it is session 2.