Working With Data
The foundational data-discipline skill. Every other skill in this repo (api-data-fetcher, stata-data-cleaning, the analysis skills, the writing skills) assumes the data was prepared with this discipline. Use this skill before, between, or alongside the others — not after.
Operating Principles
- Unit of analysis first, code second. Before any merge, regression, or figure, state the unit of analysis explicitly (firm-year, household-month, country-quarter, individual). Assert it with
isid / df.duplicated().sum() == 0 / stopifnot(!duplicated(...)). Half of empirical mistakes come from a silent change of unit between two operations.
- IDs are a contract. A unique identifier is unique, non-missing, immutable, type-stable, and documented. If your ID can be missing, it isn't an ID. If it can change between waves, it isn't an ID.
- Merging is cardinality first. Always declare 1:1, m:1, 1:m, or m:m before the merge. Always require
match (or its language equivalent) and inspect anything that doesn't match. There is no such thing as a "safe" m:m merge; if you wrote one, redesign.
- Raw is immutable.
data/raw/ is write-once. Every transformation produces a new file in data/intermediate/ or data/processed/. Re-running a pipeline from scratch reproduces the analysis-ready file byte-for-byte.
- PII never leaves the encrypted folder. Names, GPS, dates of birth, contact info, national IDs, photos: encrypted at rest, env-var keys,
.gitignored, never echoed to logs. The de-identified version is what the analysis sees.
- Drops are logged. Every filter has a reason in a comment and an N before/after.
Decision Policy
This skill follows the repo-wide Agent Policy.
ASK before proceeding (blocking):
- What is the unit of analysis (single ID or composite key)?
- Is the dataset a panel (balanced, unbalanced, rotating) or a cross-section?
- Does the raw data contain PII? If yes, is encryption already set up?
- Before any merge: what is the cardinality (1:1, m:1, 1:m), what is the match key, and what should happen to non-matches (drop, keep with
_merge flag, error)?
- When changing the unit of analysis (collapse, expand, reshape): which observations should aggregate, and how?
DEFAULT + flag (use this default; tell the user how to override):
- DataWork folder layout:
data/{raw, intermediate, processed} + data/encrypted/ for PII (see DIME DataWork Folder). Override with explicit paths.
- Merge non-matches: drop master-only and using-only with a count printed, unless the user specifies
keep or keepall.
- Master dataset per unit of observation, in
data/processed/master/.
- Numeric IDs over string IDs when both are available.
- Long format for analysis-ready panels; wide only when needed for a specific output.
DOCUMENT and proceed (write into the decisions log):
- Inferred unit of analysis when not stated, with the assertion that confirms it.
- Each merge: cardinality, key, N matched / master-only / using-only.
- Each filter: reason and N before/after.
- Each derived variable: source columns and transformation.
- Choice of de-identification thresholds (k-anonymity level, age coarsening width).
PROCEED items follow the cross-cutting rules in AGENT_POLICY.md (raw immutable, no PII in git, dynamic absolute paths, set seed, ieboilstart / pyproject.toml / Project.toml).
Pre-flight Checklist
Before generating code, confirm with the user — and write the answers into the script header:
- Unit of analysis. Single ID (
hhid) or composite (hhid year)? What is "one observation"?
- Panel structure. Cross-section / balanced panel / unbalanced / rotating panel?
- ID provenance. Where do IDs come from? Are they stable across waves? Across data sources?
- PII status. Does the file contain personally identifying information? If yes, is the workspace already configured for encryption?
- Goal of this script. Build master dataset / merge for analysis / reshape / sample construction / harmonize across waves / de-identify for release?
Decision Tree: Combining Datasets
Same unit of observation, complementary variables
├── Cross-section: 1:1 merge on the unit ID
│ Stata: merge 1:1 hhid using "B.dta", assert(match)
│ Python: pd.merge(A, B, on="hhid", how="inner", validate="1:1")
│ R: dplyr::inner_join(A, B, by = "hhid")
│ stopifnot(nrow(A) == nrow(result))
└── Panel: 1:1 on the composite key
Stata: merge 1:1 hhid year using "B.dta", assert(match)
Python: pd.merge(A, B, on=["hhid","year"], validate="1:1")
Multiple obs in one, one in the other (e.g. household + member info)
└── m:1 from the many side, attach the household covariates
Stata: merge m:1 hhid using "household.dta", assert(match master)
Python: pd.merge(members, household, on="hhid", validate="m:1")
Both sides multi (e.g. transactions x people, with same key)
└── DO NOT m:m merge. Either:
1. Aggregate one side first to make it 1:m, OR
2. Use a join that explicitly handles the cross product
(rarely the right answer; reconsider the question).
Different units of observation but you need a panel
└── First decide the unit of the result, then aggregate one side.
"Firm-year × employee-quarter" -> aggregate employees to
firm-year (means, sums, counts) before merging.
Cross-walks / fuzzy keys (names, addresses)
└── Use a deterministic crosswalk file when possible. If not, use
a probabilistic match (R: fuzzyjoin; Python: rapidfuzz; Stata:
matchit / reclink2). Always validate on a labeled subset and
report match rates.
ID Variable Contract
(Following DIME's ID Variable Properties.)
A field is an ID variable iff it satisfies all of:
- Unique within the dataset (or with the rest of the composite key).
- Non-missing for every observation.
- Immutable within and across data sources. Same firm = same
firm_id everywhere.
- Type-stable. Either always numeric or always string. Strings preserve leading zeros and very long codes; numerics are smaller and faster. Choose once.
- Documented in the codebook with: source, format (digits, with/without leading zeros), domain, and any known issues (mergers, reassignments).
Standard assertion (run after import, after merge, before save):
isid hhid year // Stata
duplicates report hhid year // explicit count
assert !missing(hhid)
assert df.duplicated(["hhid", "year"]).sum() == 0 # Python
assert df[["hhid", "year"]].notna().all().all()
stopifnot(!anyDuplicated(df[, c("hhid", "year")])) # R
stopifnot(!any(is.na(df$hhid)))
Merging Without Surprises
Before any merge, declare:
- Cardinality. 1:1, m:1, 1:m. (Reject m:m.)
- Key. Single column or composite, same name on both sides.
- Expected match status. All match? Master-only allowed? Using-only allowed?
- What happens to non-matches. Drop, flag, error.
Validation pattern:
* Stata
use "master.dta", clear
isid hhid
merge 1:1 hhid using "using.dta"
assert _merge == 3 // require all match; or:
* tabulate _merge
* drop if _merge == 2 // drop using-only
drop _merge
isid hhid
# Python (pandas)
master = pd.read_parquet("master.parquet")
using = pd.read_parquet("using.parquet")
assert master["hhid"].is_unique
assert using["hhid"].is_unique
out = master.merge(using, on="hhid", how="left",
validate="1:1", indicator=True)
n_only_master = (out["_merge"] == "left_only").sum()
n_only_using = (out["_merge"] == "right_only").sum()
print(f"only_master={n_only_master},
out = out.drop(columns="_merge")
# R (dplyr / inner_join, with explicit anti-joins to inspect)
library(dplyr)
master <- arrow::read_parquet("master.parquet")
using <- arrow::read_parquet("using.parquet")
stopifnot(!anyDuplicated(master$hhid), !anyDuplicated(using$hhid))
only_master <- anti_join(master, using, by = "hhid")
only_using <- anti_join(using, master, by = "hhid")
message(sprintf("only_master=%d,
nrow(only_master), nrow(only_using)))
out <- inner_join(master, using, by = "hhid")
stopifnot(!anyDuplicated(out$hhid))
If n_only_master or n_only_using is non-zero, stop and ask (or document if the agent has been told what to do).
Master Dataset
A master dataset is the single source of truth for time-invariant information about each unit:
- one master per unit of observation (household, individual, firm, school);
- de-identified version in
data/processed/master/ for analysis;
- encrypted version in
data/encrypted/master/ if PII is involved;
- updated through one reviewed script, never ad-hoc.
A master dataset typically holds:
- The ID and any aliases / crosswalks.
- Sample frame (was this unit in the sampling universe?).
- Sampling status and stratum.
- Treatment assignment (and date, for staggered designs).
- Time-invariant characteristics (region, founding year, baseline values used in matching).
When in doubt: store time-invariant fields in master, time-varying fields in survey-round files, and merge m:1 from the round file to master at analysis time.
Sample Construction Discipline
Every cleaning script logs N at every filter. Pattern:
def log_filter(df, label):
print(f"[{label}] N = {len(df):,}, units = {df['unit'].nunique():,}")
return df
clean = (raw
.pipe(log_filter, "raw")
.query("year >= 2010").pipe(log_filter, "year >= 2010")
.dropna(subset=["outcome"]).pipe(log_filter, "outcome non-missing")
.query("revenue > 0").pipe(log_filter, "revenue > 0"))
log_filter <- function(df, label) {
message(sprintf("[%s] N = %d, units = %d",
label, nrow(df), dplyr::n_distinct(df$unit)))
df
}
clean <- raw |>
log_filter("raw") |>
filter(year >= 2010) |> log_filter("year >= 2010") |>
filter(!is.na(outcome)) |> log_filter("outcome non-missing")
In Stata, prefer iedropone over bare drop:
ieduplicates ... // resolve dups first
iedropone if year < 2010, error // errors if count is unexpected
iedropone if missing(outcome), error
The output of these logs goes into the decisions log block at the top of the script.
PII Discipline
Treat PII like radioactive material:
- Common PII (always): names, GPS coordinates, dates of birth, contact info, national IDs, photos.
- Context-dependent PII: small geographic units, rare conditions, employer x role for high-profile people.
- Never commit PII to git. Add
data/encrypted/, *.dta containing PII, and .env to .gitignore.
- Encrypt at rest (VeraCrypt for Stata-native; age / gpg for plain files).
- Coarsen quasi-identifiers before sharing (age in 5-year bands; collapse small geographies; suppress small cells).
- Use J-PAL
pii_scan to flag candidates the agent might miss.
stata-data-cleaning skill has a full deidentify_for_release.do example.
Common Pitfalls
- Merging without checking
_merge codes (Stata) or validate= (pandas) — silently produces a Cartesian-style result when the cardinality is wrong.
- Reshape that silently drops rows because variable names don't match the stub.
m:m merge that turns 1,000 + 1,000 rows into 50,000.
- Using a string ID like
"01234" and a numeric ID like 1234 interchangeably across files — they look the same, never match.
- Filtering on a derived variable, then later regenerating the variable with a different definition.
- Storing dates as strings in some files and as Stata dates / pandas datetime in others.
- Saving a "cleaned" file that still contains observations the team agreed to drop, because the drop was applied in interactive mode but not in the script.
- Hand-typing N in the paper because the cleaning log is in a notebook.
Additional Resources
reference.md — extended patterns: composite keys, hierarchical data (households / individuals), reshape, survey weights, crosswalks, time-zone discipline, encoding gotchas, codebooks.
examples/ — runnable cross-language patterns:
examples/merge_validate.py — pandas merges with explicit validate= and _merge-equivalent inspection
examples/merge_validate.do — Stata merge 1:1 / m:1 with assert(match) and _merge discipline
examples/merge_validate.R — dplyr joins with anti-join inspection
examples/master_dataset_workflow.do — building a master HH dataset DIME-style
examples/sample_construction_logging.py — the log_filter pattern in pandas
examples/decisions_log.txt — annotated example of a filled-in decisions log
Cross-Skill Routing
- For API data acquisition (FRED, World Bank, BLS, etc.) →
api-data-fetcher.
- For Stata-specific cleaning (
iecodebook, ieduplicates, extended missing values, harmonization across waves) → stata-data-cleaning.
- For panel analysis after data is ready →
r-econometrics / stata-regression / python-panel-data.
References
DIME (the conceptual backbone)
Style
- Wickham (2014), Tidy Data.
- Gentzkow & Shapiro (2014), Code and Data for the Social Sciences.
- IPA, Reproducible Research: Best Practices for Data and Code Management.
- Quartz, Bad Data Guide.
1---2name: working-with-data3description: Foundational data-discipline skill for econometrics work in any language. Enforces the discipline that every other data, analysis, and writing skill depends on: define the unit of analysis before anything else; treat IDs as a contract; merge with explicit cardinality and validation; never silently change the unit of observation; protect PII; document every drop; keep raw immutable; build a master dataset per unit of observation. Cross-language patterns for Stata, R, and Python; aligned with DIME Analytics' [Data Cleaning](https://dimewiki.worldbank.org/Data_Cleaning), [ID Variable Properties](https://dimewiki.worldbank.org/ID_Variable_Properties), and [DataWork Folder](https://dimewiki.worldbank.org/DataWork_Folder) guidance. Use when the user asks to combine datasets, build a panel, validate data structure, set up a project's data folder, handle PII / de-identification, define or change the unit of analysis, debug a merge, or whenever multiple data skills are about to interact (e.g. before running an analysi4---56# Working With Data78The foundational data-discipline skill. Every other skill in this repo (`api-data-fetcher`, `stata-data-cleaning`, the analysis skills, the writing skills) assumes the data was prepared with this discipline. Use this skill before, between, or alongside the others — not after.910## Operating Principles11121. **Unit of analysis first, code second.** Before any merge, regression, or figure, state the unit of analysis explicitly (firm-year, household-month, country-quarter, individual). Assert it with `isid` / `df.duplicated().sum() == 0` / `stopifnot(!duplicated(...))`. Half of empirical mistakes come from a silent change of unit between two operations.132. **IDs are a contract.** A unique identifier is unique, non-missing, immutable, type-stable, and documented. If your ID can be missing, it isn't an ID. If it can change between waves, it isn't an ID.143. **Merging is cardinality first.** Always declare 1:1, m:1, 1:m, or m:m before the merge. Always require `match` (or its language equivalent) and inspect anything that doesn't match. There is no such thing as a "safe" `m:m` merge; if you wrote one, redesign.154. **Raw is immutable.** `data/raw/` is write-once. Every transformation produces a new file in `data/intermediate/` or `data/processed/`. Re-running a pipeline from scratch reproduces the analysis-ready file byte-for-byte.165. **PII never leaves the encrypted folder.** Names, GPS, dates of birth, contact info, national IDs, photos: encrypted at rest, env-var keys, `.gitignore`d, never echoed to logs. The de-identified version is what the analysis sees.176. **Drops are logged.** Every filter has a reason in a comment and an N before/after.1819## Decision Policy2021This skill follows the repo-wide [Agent Policy](../../AGENT_POLICY.md).2223**ASK before proceeding** (blocking):24251. What is the unit of analysis (single ID or composite key)?262. Is the dataset a panel (balanced, unbalanced, rotating) or a cross-section?273. Does the raw data contain PII? If yes, is encryption already set up?284. Before any merge: what is the cardinality (1:1, m:1, 1:m), what is the match key, and what should happen to non-matches (drop, keep with `_merge` flag, error)?295. When changing the unit of analysis (collapse, expand, reshape): which observations should aggregate, and how?3031**DEFAULT + flag** (use this default; tell the user how to override):3233- DataWork folder layout: `data/{raw, intermediate, processed}` + `data/encrypted/` for PII (see [DIME DataWork Folder](https://dimewiki.worldbank.org/DataWork_Folder)). Override with explicit paths.34- Merge non-matches: drop master-only and using-only with a count printed, unless the user specifies `keep` or `keepall`.35- Master dataset per unit of observation, in `data/processed/master/`.36- Numeric IDs over string IDs when both are available.37- Long format for analysis-ready panels; wide only when needed for a specific output.3839**DOCUMENT and proceed** (write into the decisions log):4041- Inferred unit of analysis when not stated, with the assertion that confirms it.42- Each merge: cardinality, key, N matched / master-only / using-only.43- Each filter: reason and N before/after.44- Each derived variable: source columns and transformation.45- Choice of de-identification thresholds (k-anonymity level, age coarsening width).4647`PROCEED` items follow the cross-cutting rules in `AGENT_POLICY.md` (raw immutable, no PII in git, dynamic absolute paths, `set seed`, `ieboilstart` / `pyproject.toml` / `Project.toml`).4849## Pre-flight Checklist5051Before generating code, confirm with the user — and write the answers into the script header:5253- **Unit of analysis.** Single ID (`hhid`) or composite (`hhid year`)? What is "one observation"?54- **Panel structure.** Cross-section / balanced panel / unbalanced / rotating panel?55- **ID provenance.** Where do IDs come from? Are they stable across waves? Across data sources?56- **PII status.** Does the file contain personally identifying information? If yes, is the workspace already configured for encryption?57- **Goal of this script.** Build master dataset / merge for analysis / reshape / sample construction / harmonize across waves / de-identify for release?5859## Decision Tree: Combining Datasets6061```62Same unit of observation, complementary variables63├── Cross-section: 1:1 merge on the unit ID64│ Stata: merge 1:1 hhid using "B.dta", assert(match)65│ Python: pd.merge(A, B, on="hhid", how="inner", validate="1:1")66│ R: dplyr::inner_join(A, B, by = "hhid")67│ stopifnot(nrow(A) == nrow(result))68└── Panel: 1:1 on the composite key69 Stata: merge 1:1 hhid year using "B.dta", assert(match)70 Python: pd.merge(A, B, on=["hhid","year"], validate="1:1")7172Multiple obs in one, one in the other (e.g. household + member info)73└── m:1 from the many side, attach the household covariates74 Stata: merge m:1 hhid using "household.dta", assert(match master)75 Python: pd.merge(members, household, on="hhid", validate="m:1")7677Both sides multi (e.g. transactions x people, with same key)78└── DO NOT m:m merge. Either:79 1. Aggregate one side first to make it 1:m, OR80 2. Use a join that explicitly handles the cross product81 (rarely the right answer; reconsider the question).8283Different units of observation but you need a panel84└── First decide the unit of the result, then aggregate one side.85 "Firm-year × employee-quarter" -> aggregate employees to86 firm-year (means, sums, counts) before merging.8788Cross-walks / fuzzy keys (names, addresses)89└── Use a deterministic crosswalk file when possible. If not, use90 a probabilistic match (R: fuzzyjoin; Python: rapidfuzz; Stata:91 matchit / reclink2). Always validate on a labeled subset and92 report match rates.93```9495## ID Variable Contract9697(Following DIME's [ID Variable Properties](https://dimewiki.worldbank.org/ID_Variable_Properties).)9899A field is an ID variable iff it satisfies *all* of:1001011. **Unique** within the dataset (or with the rest of the composite key).1022. **Non-missing** for every observation.1033. **Immutable** within and across data sources. Same firm = same `firm_id` everywhere.1044. **Type-stable.** Either always numeric or always string. Strings preserve leading zeros and very long codes; numerics are smaller and faster. Choose once.1055. **Documented** in the codebook with: source, format (digits, with/without leading zeros), domain, and any known issues (mergers, reassignments).106107Standard assertion (run after import, after merge, before save):108109```stata110isid hhid year // Stata111duplicates report hhid year // explicit count112assert !missing(hhid)113```114115```python116assert df.duplicated(["hhid", "year"]).sum() == 0 # Python117assert df[["hhid", "year"]].notna().all().all()118```119120```r121stopifnot(!anyDuplicated(df[, c("hhid", "year")])) # R122stopifnot(!any(is.na(df$hhid)))123```124125## Merging Without Surprises126127Before any merge, declare:1281291. **Cardinality.** 1:1, m:1, 1:m. (Reject m:m.)1302. **Key.** Single column or composite, same name on both sides.1313. **Expected match status.** All match? Master-only allowed? Using-only allowed?1324. **What happens to non-matches.** Drop, flag, error.133134Validation pattern:135136```stata137* Stata138use "master.dta", clear139isid hhid140merge 1:1 hhid using "using.dta"141assert _merge == 3 // require all match; or:142* tabulate _merge143* drop if _merge == 2 // drop using-only144drop _merge145isid hhid146```147148```python149# Python (pandas)150master = pd.read_parquet("master.parquet")151using = pd.read_parquet("using.parquet")152assert master["hhid"].is_unique153assert using["hhid"].is_unique154out = master.merge(using, on="hhid", how="left",155 validate="1:1", indicator=True)156n_only_master = (out["_merge"] == "left_only").sum()157n_only_using = (out["_merge"] == "right_only").sum()158print(f"only_master={n_only_master}, only_using={n_only_using}")159out = out.drop(columns="_merge")160```161162```r163# R (dplyr / inner_join, with explicit anti-joins to inspect)164library(dplyr)165master <- arrow::read_parquet("master.parquet")166using <- arrow::read_parquet("using.parquet")167stopifnot(!anyDuplicated(master$hhid), !anyDuplicated(using$hhid))168169only_master <- anti_join(master, using, by = "hhid")170only_using <- anti_join(using, master, by = "hhid")171message(sprintf("only_master=%d, only_using=%d",172 nrow(only_master), nrow(only_using)))173174out <- inner_join(master, using, by = "hhid")175stopifnot(!anyDuplicated(out$hhid))176```177178If `n_only_master` or `n_only_using` is non-zero, **stop and ask** (or document if the agent has been told what to do).179180## Master Dataset181182A master dataset is the single source of truth for time-invariant information about each unit:183184- one master per unit of observation (household, individual, firm, school);185- de-identified version in `data/processed/master/` for analysis;186- encrypted version in `data/encrypted/master/` if PII is involved;187- updated through one reviewed script, never ad-hoc.188189A master dataset typically holds:190191- The ID and any aliases / crosswalks.192- Sample frame (was this unit in the sampling universe?).193- Sampling status and stratum.194- Treatment assignment (and date, for staggered designs).195- Time-invariant characteristics (region, founding year, baseline values used in matching).196197When in doubt: store time-invariant fields in master, time-varying fields in survey-round files, and `merge m:1` from the round file to master at analysis time.198199## Sample Construction Discipline200201Every cleaning script logs N at every filter. Pattern:202203```python204def log_filter(df, label):205 print(f"[{label}] N = {len(df):,}, units = {df['unit'].nunique():,}")206 return df207208clean = (raw209 .pipe(log_filter, "raw")210 .query("year >= 2010").pipe(log_filter, "year >= 2010")211 .dropna(subset=["outcome"]).pipe(log_filter, "outcome non-missing")212 .query("revenue > 0").pipe(log_filter, "revenue > 0"))213```214215```r216log_filter <- function(df, label) {217 message(sprintf("[%s] N = %d, units = %d",218 label, nrow(df), dplyr::n_distinct(df$unit)))219 df220}221222clean <- raw |>223 log_filter("raw") |>224 filter(year >= 2010) |> log_filter("year >= 2010") |>225 filter(!is.na(outcome)) |> log_filter("outcome non-missing")226```227228In Stata, prefer `iedropone` over bare `drop`:229230```stata231ieduplicates ... // resolve dups first232iedropone if year < 2010, error // errors if count is unexpected233iedropone if missing(outcome), error234```235236The output of these logs goes into the decisions log block at the top of the script.237238## PII Discipline239240Treat PII like radioactive material:241242- Common PII (always): names, GPS coordinates, dates of birth, contact info, national IDs, photos.243- Context-dependent PII: small geographic units, rare conditions, employer x role for high-profile people.244- Never commit PII to git. Add `data/encrypted/`, `*.dta` containing PII, and `.env` to `.gitignore`.245- Encrypt at rest (VeraCrypt for Stata-native; age / gpg for plain files).246- Coarsen quasi-identifiers before sharing (age in 5-year bands; collapse small geographies; suppress small cells).247- Use [J-PAL `pii_scan`](https://github.com/J-PAL/stata_PII_scan) to flag candidates the agent might miss.248- `stata-data-cleaning` skill has a full `deidentify_for_release.do` example.249250## Common Pitfalls251252- Merging without checking `_merge` codes (Stata) or `validate=` (pandas) — silently produces a Cartesian-style result when the cardinality is wrong.253- Reshape that silently drops rows because variable names don't match the stub.254- `m:m` merge that turns 1,000 + 1,000 rows into 50,000.255- Using a string ID like `"01234"` and a numeric ID like `1234` interchangeably across files — they look the same, never match.256- Filtering on a derived variable, then later regenerating the variable with a different definition.257- Storing dates as strings in some files and as Stata dates / pandas datetime in others.258- Saving a "cleaned" file that still contains observations the team agreed to drop, because the drop was applied in interactive mode but not in the script.259- Hand-typing N in the paper because the cleaning log is in a notebook.260261## Additional Resources262263- `reference.md` — extended patterns: composite keys, hierarchical data (households / individuals), reshape, survey weights, crosswalks, time-zone discipline, encoding gotchas, codebooks.264- `examples/` — runnable cross-language patterns:265 - `examples/merge_validate.py` — pandas merges with explicit `validate=` and `_merge`-equivalent inspection266 - `examples/merge_validate.do` — Stata `merge 1:1` / `m:1` with `assert(match)` and `_merge` discipline267 - `examples/merge_validate.R` — dplyr joins with anti-join inspection268 - `examples/master_dataset_workflow.do` — building a master HH dataset DIME-style269 - `examples/sample_construction_logging.py` — the `log_filter` pattern in pandas270 - `examples/decisions_log.txt` — annotated example of a filled-in decisions log271272## Cross-Skill Routing273274- For API data acquisition (FRED, World Bank, BLS, etc.) → `api-data-fetcher`.275- For Stata-specific cleaning (`iecodebook`, `ieduplicates`, extended missing values, harmonization across waves) → `stata-data-cleaning`.276- For panel analysis after data is ready → `r-econometrics` / `stata-regression` / `python-panel-data`.277278## References279280### DIME (the conceptual backbone)281282- DIME Analytics, [Data Cleaning](https://dimewiki.worldbank.org/Data_Cleaning).283- DIME Analytics, [ID Variable Properties](https://dimewiki.worldbank.org/ID_Variable_Properties).284- DIME Analytics, [DataWork Folder](https://dimewiki.worldbank.org/DataWork_Folder).285- DIME Analytics, [Master Do-files](https://dimewiki.worldbank.org/Master_Do-files).286- DIME Analytics, [Personally Identifiable Information (PII)](https://dimewiki.worldbank.org/Personally_Identifiable_Information_(PII)).287- DIME Analytics, [Reproducible Research](https://dimewiki.worldbank.org/Reproducible_Research).288289### Style290291- Wickham (2014), *Tidy Data*.292- Gentzkow & Shapiro (2014), *Code and Data for the Social Sciences*.293- IPA, *Reproducible Research: Best Practices for Data and Code Management*.294- Quartz, [Bad Data Guide](https://github.com/Quartz/bad-data-guide).