Data Cleaning Log Template (Statistics, Missing Data, R and Python)
Reproducible data cleaning with an audit log: duplicates, range and consistency checks, pre-set outlier policy, missing data mechanisms (MCAR, MAR, MNAR), imputation choices, R and Python code
How to use: Use when preparing a raw data set for analysis: fix ranges, consistency rules and the outlier policy before looking at results, then clean only by script and log every step with the rows it affected. Describe missingness by mechanism before choosing complete-case analysis or multiple imputation. Main pitfall: editing raw data by hand or deleting outliers because they weaken the result.
Preview
Data Cleaning Log: [Your data set and project]
A working cleaning log for your own data set: it sets up a reproducible workflow in which the raw file is never edited, every change is made by a script and recorded in a log table, outliers are handled by a rule fixed in advance, and missing data are described by mechanism before any imputation. It is for students handling their first real data set, thesis writers who must defend every excluded row to an examiner, and analysts who inherit messy exports. It speeds up learning by turning cleaning from a series of ad hoc fixes into a fixed sequence of checks you can repeat, explain and test yourself on. The worked examples use fictional data; replace them with your own. Source basis: Rubin (1976) and Little and Rubin on missing data, Tukey (1977) on fences, and the NIST/SEMATECH e-Handbook, written in original words.
Document control
| Field | Value |
|---|---|
| Course or project | [Course code and title, or thesis project name] |
| Learner or analyst | [Your name] |
| Data set | [Raw file name, export date, number of rows and columns, checksum if available] |
| Software and versions | R 4.5.1 (base and stats); Python 3.14 with pandas 3.1.0 and numpy 2.5.4 |
| Date | YYYY-MM-DD |
| Version | [0.1] |
Principles of a reproducible cleaning workflow
Data cleaning changes your data, and therefore your results. A reproducible workflow means that anyone with the raw file and your script can recreate the cleaned file exactly, and can see every decision and its reason. Five rules make that possible.
- Raw data are never edited. Store the original export in a
data/raw/folder and make it read-only. Never "fix a typo" in the spreadsheet: the fix is invisible and cannot be undone or reviewed. - All changes are made by a script. The script reads the raw file and writes a new file to
data/clean/. Running it again from scratch gives the same result. - Every step is logged. Each rule gets one row in the cleaning log: what the rule was, how many rows it touched, why, and where in the script it lives. Rows touched are counted by the script, not by hand.
- Rules are fixed before results are seen. Valid ranges, outlier policy and missing-data handling come from the codebook (academic/stats-variable-codebook) and the analysis plan, not from what makes the result significant.
- Flag before you delete. Prefer a flag column (
flag_credits = TRUE) over deleting rows, so the analysis can be run with and without the flagged cases.
Rules fixed in advance
Fill this table before opening the data. Each rule later becomes a script step and a log row.
| Variable | Valid range or codes | Consistency rule | Action if broken |
|---|---|---|---|
| Example: age | 16 to 80 years | none | Set to NA, log the ID |
| Example: credits | 0 to 360 | Year 1 students cannot have more than 60 credits | Flag, check with registry |
| Example: visits | 0 to 100 per term | none | Outlier rule: flag if above |
| [variable] | [ ] | [ ] | [ ] |
| [variable] | [ ] | [ ] | [ ] |
The cleaning log
| Step | Rule | Rows affected | Reason | Script line |
|---|---|---|---|---|
| Example: 1 | Map faculty labels to codebook codes (Arts to ARTS, Science to SCI, Business to BUS) | 8 | Free-text entry used different spellings of the same faculty | clean.R line 17 |
| Example: 2 | Drop exact duplicate rows | 1 | Respondent 103 submitted twice (identical answers) | clean.R line 20 |
| Example: 3 | Age outside 16 to 80 set to NA | 1 | Age 210 for ID 104, a likely typing error; true value unknown | clean.R line 23 |
| Example: 4 | Flag year 1 with credits above 60 | 1 | ID 109 has 300 credits in year 1; sent to registry | clean.R line 26 |
| [5] | [rule] | [n] | [reason] | [file and line] |
| [6] | [rule] | [n] | [reason] | [file and line] |
Rows at start: [ ] · Rows at end: [ ] · Difference explained by steps: [ ]
Checks in order
| Order | Check | Question it answers | Typical code (R / Python) |
|---|---|---|---|
| 1 | Structure | Right number of rows and columns? Right types (numbers not stored as text)? | str(d) / d.info() |
| 2 | Labels | Do category values match the codebook exactly (case, spelling, spaces)? | table(d$faculty) / d["faculty"].value_counts() |
| 3 | Duplicates | Is any record present twice (exact duplicate, or same ID with different values)? | duplicated(d) / d.duplicated() |
| 4 | Ranges | Is each value possible under the codebook? | logical filter / between() |
| 5 | Consistency | Do values agree across columns (year and credits, start and end dates, age and year of birth)? | logical filter on two columns |
| 6 | Outliers | Is a possible value unusual enough to need a pre-set rule? | fences, see below |
| 7 | Missing data | How much is missing, where, and why? | colSums(is.na(d)) / d.isna().sum() |
Two kinds of duplicate need different actions. An exact duplicate (every column identical) is usually a double submission and can be dropped with a log row. A key duplicate (same ID, different answers) is a real conflict: two people sharing an ID, or one person answering twice with changed answers. Do not drop either copy silently; decide by a rule (first submission, most complete submission) and log it.
An impossible value is different from an unusual one. An age of 210 is impossible, so it is an error: set it to missing and log it. A student with 40 library visits per term is unusual but possible, so it is a candidate outlier: apply the pre-set rule, which usually means flag, keep, and check the effect in a sensitivity analysis.
Outlier handling policy
An outlier is a value far from the rest of the data. It may be an error that the range check missed, a real but rare case, or a sign that the variable has a skewed distribution. Deleting outliers because they weaken a result is one of the most common forms of research bias, so the policy is fixed before the analysis and applied blindly.
A widely used rule, from Tukey (1977), uses the quartiles and and the interquartile range :
| Symbol | Meaning |
|---|---|
| , | First and third quartiles (25th and 75th percentiles) |
| IQR | Interquartile range, the spread of the middle half of the data |
| inner fence | Points beyond it are "possible outliers" (the dots on a box plot) |
| outer fence | Points beyond it are "far out" values |
Fences are robust: one extreme value barely moves them. A rule based on the mean and standard deviation ("more than 3 SD from the mean") is weaker, because the extreme value itself inflates the SD and can hide itself. Note also that software computes quartiles in slightly different ways; R and pandas both default to the same linear interpolation method, but other tools may give slightly different fences.
| Policy item | My decision |
|---|---|
| Variables the rule applies to | [ ] |
| Rule (fence type, or a domain limit) | [e.g. flag values above the outer fence] |
| Action | [Flag and keep / winsorise at the fence / transform / remove with reason] |
| Sensitivity analysis | [Main analysis repeated without flagged cases; both reported] |
| Example: Rule for visits | Flag values above ; keep them; report results with and without |
For skewed variables, such as counts or incomes, many "outliers" disappear after a log transformation; see academic/stats-data-transformations. For regression, unusual points are judged by their influence on the fitted model (leverage, Cook's distance) rather than by fences on single variables; see academic/stats-assumption-checklist.
Missing data: mechanisms and what they mean
Plain language first. Missing values are not just holes; the reason they are missing matters. Rubin (1976) defined three mechanisms, by asking what the probability of a value being missing depends on.
| Mechanism | Meaning | Example | What it implies |
|---|---|---|---|
| MCAR (missing completely at random) | Missingness is unrelated to any variable, observed or not | A page of paper questionnaires was lost in the post | Complete cases are a random subset: estimates are unbiased, only less precise |
| MAR (missing at random) | Missingness depends only on observed variables | First-year students skip the credits question more often, and year is recorded | Complete-case analysis can be biased; methods that use the observed variables (multiple imputation, likelihood methods) can correct it |
| MNAR (missing not at random) | Missingness depends on the unobserved value itself, even after accounting for observed variables | Students with very high stress are less likely to answer the stress items | No standard method fixes it from the data alone; use sensitivity analyses and say so |
The names are confusing: "missing at random" does not mean "missing for no reason". It means the reason is captured by variables you have.
What complete-case analysis assumes. Complete-case (listwise) analysis drops every row with any missing value in
the variables used. It is the default in R (na.omit, model functions) and in statsmodels formulas. It is unbiased
when data are MCAR, and in some regression settings also when missingness depends only on the predictors, but it
always loses cases: in worked example A, three variables with only five missing values between them reduce 11 rows
to 6. The loss of power is often larger than people expect, because each row is dropped for any one missing value.
Simple imputation. Replacing missing values with one guess (the mean, the median, the last observation) is quick but treats the guess as if it were observed. Mean imputation shrinks the standard deviation and weakens correlations, and every simple method understates uncertainty, so standard errors come out too small and p-values too small. Worked example A shows the shrinkage: the SD of visits drops from 11.74 to 10.51 after mean imputation.
Multiple imputation (MI). MI creates several (often 20 or more) completed data sets, each filling the gaps with
plausible values drawn from a model that uses the other variables, analyses each one, and combines the results with
Rubin's rules so that the final standard errors include the uncertainty about the missing values. It is valid under
MAR if the imputation model includes the analysis variables (including the outcome) and the variables that predict
missingness. In R the standard tool is the mice package (van Buuren and Groothuis-Oudshoorn 2011,
install.packages("mice")); in Python, statsmodels provides statsmodels.imputation.mice (classes MICEData and
MICE). Both use chained equations: each incomplete variable is imputed from the others in turn.
Little's MCAR test, with caveats. Little (1988) proposed a test of the null hypothesis that data are MCAR (in R,
for example, naniar::mcar_test()). Use it with care:
- It can only reject MCAR; a non-significant result does not show MCAR, especially in small samples with low power.
- It cannot distinguish MAR from MNAR. No test on the observed data can, because MNAR depends on values you do not have.
- It assumes approximately multivariate normal data, which is doubtful for Likert items, counts and categories.
- With large samples it rejects for trivial departures.
A more useful habit is to compare rows with and without missing values on the observed variables (for example, the year of study for students with and without a credits value). Differences point away from MCAR and tell you which variables to include in the imputation model.
The chart shows missing values per variable in worked example A after the range check. Seeing the counts side by side helps decide where to focus: here, age and visits each lose two values.
Missing-data plan for my project
| Item | My entry |
|---|---|
| Variables with missing values and counts | [ ] |
| Missing codes recoded to NA (from codebook) | [ ] |
| Proposed mechanism per variable, with reason | [ ] |
| Comparison of complete and incomplete rows | [ ] |
| Main handling (complete case, MI, model-based) | [ ] |
| Sensitivity analysis (for example, MNAR scenarios) | [ ] |
Worked example A: cleaning a messy survey export
Setting. A Lakemoor University (fictional) library-use survey export has 12 rows: student ID, faculty (typed by students), age, year of study, credits earned and library visits this term. The rules fixed in advance are the example rows in the "Rules fixed in advance" table.
# Raw export from the Lakemoor University library-use survey (fictional). Never edit raw.
raw <- data.frame(
id = c(101, 102, 103, 103, 104, 105, 106, 107, 108, 109, 110, 111),
faculty = c("Arts", "SCI", "sci", "sci", "Business", "Arts", "Science", "ENG", "Arts", "BUS", "Eng", "SCI"),
age = c(19, 21, 20, 20, 210, 22, 18, NA, 23, 19, 20, 24),
year = c(1, 3, 2, 2, 3, 4, 1, 2, 4, 1, 2, 1),
credits = c(30, 150, 80, 80, 140, 220, 20, 95, 230, 300, 70, NA),
visits = c(4, 12, 7, 7, 9, NA, 2, 5, NA, 3, 6, 40),
stringsAsFactors = FALSE
)
d <- raw
log <- data.frame(step = integer(), rule = character(), rows = integer())
add <- function(rule, rows) log[nrow(log) + 1, ] <<- list(nrow(log) + 1, rule, rows)
# 1. Standardise faculty labels
map <- c(ARTS = "ARTS", SCI = "SCI", SCIENCE = "SCI", BUSINESS = "BUS", BUS = "BUS", ENG = "ENG")
new <- unname(map[toupper(d$faculty)])
add("faculty labels to codebook codes", sum(new != d$faculty)); d$faculty <- new
# 2. Exact duplicates
dup <- duplicated(d)
add("drop exact duplicate rows", sum(dup)); d <- d[!dup, ]
# 3. Range check: age 16-80, else NA
bad_age <- !is.na(d$age) & (d$age < 16 | d$age > 80)
add("age outside 16-80 set to NA", sum(bad_age)); d$age[bad_age] <- NA
# 4. Consistency: year 1 cannot have more than 60 credits
bad_cr <- !is.na(d$credits) & d$year == 1 & d$credits > 60
add("year 1 with credits > 60 flagged", sum(bad_cr)); d$flag_credits <- bad_cr
print(log, row.names = FALSE)
cat("rows:", nrow(raw), "->", nrow(d), "\n")
print(colSums(is.na(d[, c("age", "credits", "visits")])))
cc <- complete.cases(d[, c("age", "credits", "visits")])
cat("complete cases:", sum(cc), "of", nrow(d), "\n")
# Outlier rule fixed in advance: visits > Q3 + 3*IQR flagged, not deleted
q <- quantile(d$visits, c(.25, .75), na.rm = TRUE)
fence <- q[2] + 3 * diff(q)
cat("visits fence:", fence, " flagged ids:", d$id[which(d$visits > fence)], "\n")
# Mean imputation shrinks the spread
v_imp <- ifelse(is.na(d$visits), mean(d$visits, na.rm = TRUE), d$visits)
cat(sprintf("SD visits observed %.2f, after mean imputation %.2f\n",
sd(d$visits, na.rm = TRUE), sd(v_imp)))
step rule rows
1 faculty labels to codebook codes 8
2 drop exact duplicate rows 1
3 age outside 16-80 set to NA 1
4 year 1 with credits > 60 flagged 1
rows: 12 -> 11
age credits visits
2 1 2
complete cases: 6 of 11
visits fence: 24 flagged ids: 111
SD visits observed 11.74, after mean imputation 10.51
import numpy as np, pandas as pd
raw = pd.DataFrame({
"id": [101, 102, 103, 103, 104, 105, 106, 107, 108, 109, 110, 111],
"faculty": ["Arts", "SCI", "sci", "sci", "Business", "Arts", "Science", "ENG", "Arts", "BUS", "Eng", "SCI"],
"age": [19, 21, 20, 20, 210, 22, 18, np.nan, 23, 19, 20, 24],
"year": [1, 3, 2, 2, 3, 4, 1, 2, 4, 1, 2, 1],
"credits": [30, 150, 80, 80, 140, 220, 20, 95, 230, 300, 70, np.nan],
"visits": [4, 12, 7, 7, 9, np.nan, 2, 5, np.nan, 3, 6, 40],
})
d = raw.copy()
log = []
add = lambda rule, rows: log.append({"step": len(log) + 1, "rule": rule, "rows": int(rows)})
fmap = {"ARTS": "ARTS", "SCI": "SCI", "SCIENCE": "SCI", "BUSINESS": "BUS", "BUS": "BUS", "ENG": "ENG"}
new = d["faculty"].str.upper().map(fmap)
add("faculty labels to codebook codes", (new != d["faculty"]).sum()); d["faculty"] = new
dup = d.duplicated()
add("drop exact duplicate rows", dup.sum()); d = d[~dup].copy()
bad_age = d["age"].notna() & ~d["age"].between(16, 80)
add("age outside 16-80 set to NA", bad_age.sum()); d.loc[bad_age, "age"] = np.nan
bad_cr = d["credits"].notna() & (d["year"] == 1) & (d["credits"] > 60)
add("year 1 with credits > 60 flagged", bad_cr.sum()); d["flag_credits"] = bad_cr
print(pd.DataFrame(log).to_string(index=False))
print("rows:", len(raw), "->", len(d))
print(d[["age", "credits", "visits"]].isna().sum().to_dict())
cc = d[["age", "credits", "visits"]].notna().all(axis=1)
print("complete cases:", cc.sum(), "of", len(d))
q1, q3 = d["visits"].quantile([.25, .75])
fence = q3 + 3 * (q3 - q1)
print("visits fence:", fence, " flagged ids:", d.loc[d["visits"] > fence, "id"].tolist())
v_imp = d["visits"].fillna(d["visits"].mean())
print(f"SD visits observed {d['visits'].std():.2f}, after mean imputation {v_imp.std():.2f}")
step rule rows
1 faculty labels to codebook codes 8
2 drop exact duplicate rows 1
3 age outside 16-80 set to NA 1
4 year 1 with credits > 60 flagged 1
rows: 12 -> 11
{'age': 2, 'credits': 1, 'visits': 2}
complete cases: 6 of 11
visits fence: 24.0 flagged ids: [111]
SD visits observed 11.74, after mean imputation 10.51
Reading the output, step by step.
- Labels (8 rows). Arts (3 rows), sci (2), Business, Science and Eng were mapped to the codebook codes. Without this step a table of faculty would show eight different spellings instead of four codes.
- Duplicates (1 row). ID 103 appears twice with identical values. Note the order: the two copies were both typed "sci", so they were duplicates before step 1 too, but if one had been "sci" and the other "SCI", only the label fix would reveal it. Rows go from 12 to 11.
- Range (1 row). Age 210 (ID 104) is impossible. It is set to NA rather than "corrected" to 21, because the true value is unknown; guessing would be inventing data. Age now has 2 missing values (ID 104 and ID 107).
- Consistency (1 row). ID 109 is in year 1 with 300 credits. Both values are within range on their own, but together they are impossible. The row is flagged and sent to the registry, not deleted.
- Missing data. Age 2, credits 1, visits 2: five missing values in total, spread over five different rows, so complete-case analysis keeps only 6 of 11 rows (55%). That is a large loss from a small amount of missingness.
- Outlier rule. The non-missing visits are 2, 3, 4, 5, 6, 7, 9, 12, 40. By hand: , , , outer fence . ID 111 (40 visits) is flagged and kept; the analysis will be reported with and without it.
- Imputation warning. Filling the two missing visits with the mean (9.78) lowers the SD from 11.74 to 10.51, because two values are placed exactly at the centre. Any test using the imputed column would look more certain than the data justify.
R and Python agree on every count. One design difference to know: duplicated() in R and DataFrame.duplicated()
in pandas both mark the second and later copies, keeping the first; pandas lets you change this with keep=. And
the log counts come from the code (sum(...)), so if the raw file changes, the log updates itself.
Report sentence. "The raw export contained 12 records. One exact duplicate was removed (n = 11). One impossible age was set to missing, and one record with inconsistent year and credits was flagged for registry review. Five values were missing (age 2, credits 1, visits 2), leaving 6 complete cases; [the planned main analysis uses multiple imputation under a MAR assumption, with complete-case results reported as a sensitivity analysis]. One extreme visits value (40) exceeded the pre-specified outer fence and was retained; results without it are reported in the appendix."
Worked example B: Brightwell Bakery loaf weights (half solved)
Setting. Brightwell Bakery (fictional) records the weight in grams of 10 sampled loaves from one batch: 798, 802,
805, 810, 795, 800, 8000, 803, 799, 650. The codebook range is 300 to 1,200 g. The outlier policy, fixed in advance,
is "flag values beyond the outer fences; keep unless shown to be errors". Following the order of checks, the range
check runs first, so the fences are computed on the nine weights left after it: software gives and
(R quantile() default; pandas gives the same).
| Item | Your answer |
|---|---|
| Range check: which value breaks the 300 to 1,200 g rule? | [ ] |
| Likely explanation and action | Probably a slipped decimal (800.0 typed as 8000). Action: [ ] |
| IQR | [ ] |
| Lower outer fence | [ ] |
| Is 650 g an error or an outlier? | It is within range, so it is [ ]. It may be a real underweight loaf, which is exactly what quality control is meant to find. |
| Action for 650 g | [ ] |
| Log rows | Step 1: [ ] · Step 2: [ ] |
Exercise C: Harbour City Library daily visits
Harbour City Library (fictional) has 60 days of door-counter visits for its Central branch. Eight days are missing: five because the counter battery failed (the failures happened on random days according to the maintenance log), and three because the branch closed early on stormy days, when staff did not record the partial count. On two other days the counter recorded 0 visits while the branch was open.
Tasks: (a) propose a mechanism for each group of missing days, with a reason; (b) how should the two zero days be treated, and in which check do they appear? (c) the analyst plans to compare average visits in wet and dry weeks using complete cases only. What is the risk? (d) write two cleaning log rows for this data set.
Cleaning checklist
- Raw file stored read-only; cleaned file written to a separate folder by script.
- Rules (ranges, consistency, outliers, missing data) written before looking at results.
- Labels standardised to codebook codes before duplicate checks.
- Exact and key duplicates checked; every removal logged with a reason.
- Impossible values set to NA (not guessed); unusual values handled by the pre-set outlier rule.
- Missing data counted per variable; complete-case count reported; mechanism argued per variable.
- Imputation method chosen to match the mechanism; simple imputation not used for inference.
- Row count at start and end reconciled with the log.
- Sensitivity analyses listed for outliers and missing data.
- Script, log and both data versions saved together (see academic/replication-package-readme).
Common mistakes and misconceptions
| Mistake or misconception | Why it is wrong | Correct practice |
|---|---|---|
| Fixing values directly in the spreadsheet | No record, no review, not reproducible; the raw data are lost. | Change data only in a script; keep raw read-only. |
| Deleting outliers because they weaken the result | Outcome-driven exclusion biases the estimate and inflates false positives. | Fix the rule before analysis; flag and keep; report with and without. |
| "Missing at random means missing for no reason." | MAR means missingness depends on observed variables. | MCAR is "no reason"; MAR is "reason captured by observed data". |
| "Little's test was not significant, so the data are MCAR." | Failing to reject is not evidence of MCAR, and the test cannot detect MNAR. | Argue the mechanism from the data collection process; compare complete and incomplete rows. |
| Mean imputation, then normal tests | Shrinks variance and correlations; standard errors too small. | Multiple imputation or likelihood-based methods; simple imputation only for description. |
| Guessing a "corrected" value (age 210 to 21) | Invents data; the guess may be wrong. | Set to NA unless a source document confirms the true value. |
| Counting rows affected by eye | Counts drift from the real script output. | Let the script compute and print every count. |
| Treating 0 or 99 as real values | Missing codes enter means and models. | Recode missing codes to NA first, using the codebook. |
Self-test questions
- Why should label standardisation come before the duplicate check? Use worked example A.
- A health survey finds that people with low income skip the income question more often, and education (recorded) predicts both. Missing income is fully explained by education. Which mechanism is this?
- Students who failed an exam are less likely to report their exam score, even among students with the same attendance and year. Which mechanism, and can multiple imputation from the observed variables fix it?
- Data on four variables each have 5% missing values in different rows of 100 rows. Roughly how many complete cases might remain in the worst case, and what does that say about complete-case analysis?
- Compute the inner fences for data with and . Is a value of 31 beyond the inner or the outer upper fence?
- Name two reasons why a non-significant Little's test does not justify complete-case analysis.
My notes and next steps
| Item | My entry |
|---|---|
| Confidence with the cleaning workflow (1 to 5) | [ ] |
| Confidence with missing-data mechanisms (1 to 5) | [ ] |
| Confidence with outlier rules (1 to 5) | [ ] |
| Questions still open | [ ] |
| Mistakes to copy into my error log (academic/stats-error-log) | [ ] |
| Next step | [Describe the cleaned data with academic/stats-eda-report] |
| Review again on | YYYY-MM-DD |
Sources and further reading
Based on the missing-data framework of Rubin and Little and on common reproducible-research practice, written in original words; the fence rule follows Tukey's exploratory data analysis.
- Rubin, D. B. (1976). Inference and missing data. Biometrika, 63(3), 581–592.
- Rubin, D. B. (1987). Multiple Imputation for Nonresponse in Surveys. Wiley.
- Little, R. J. A. (1988). A test of missing completely at random for multivariate data with missing values. Journal of the American Statistical Association, 83(404), 1198–1202.
- Little, R. J. A., and Rubin, D. B. (2019). Statistical Analysis with Missing Data (3rd ed.). Wiley.
- van Buuren, S., and Groothuis-Oudshoorn, K. (2011). mice: Multivariate imputation by chained equations in R. Journal of Statistical Software, 45(3), 1–67.
- van Buuren, S. (2018). Flexible Imputation of Missing Data (2nd ed.). CRC Press.
- Sterne, J. A. C., White, I. R., Carlin, J. B., Spratt, M., Royston, P., Kenward, M. G., et al. (2009). Multiple imputation for missing data in epidemiological and clinical research: potential and pitfalls. BMJ, 338, b2393.
- Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley.
- Wickham, H. (2014). Tidy data. Journal of Statistical Software, 59(10), 1–23.
- NIST/SEMATECH e-Handbook of Statistical Methods, chapter 1 Exploratory Data Analysis, and section 1.3.5.17 Detection of Outliers, https://www.itl.nist.gov/div898/handbook/ (accessed 2026-10-25). Public domain.
- Further reading (by name only): de Smith, M. J. (2024). Statistical Analysis Handbook (2024 edition, StatsRef); Diez, D. M., Çetinkaya-Rundel, M., and Barr, C. D., OpenIntro Statistics (CC BY-SA).
Answer key
Worked example B. 8000 g breaks the range. Action: set to NA and log it; restore 800 only if the original batch sheet confirms the value. IQR . Lower outer fence (upper outer fence ). If you had kept 8000 in the quartile calculation you would get , : this is why range errors are removed before fences are computed. 650 g is within range, so it is a possible value, and it is below the lower outer fence, so it is an outlier, not an error. Action: flag and keep; it is a real quality signal to investigate, not noise to remove. Log rows: Step 1, "weight outside 300 to 1,200 g set to NA", 1 row, likely decimal slip, script line [n]. Step 2, "flag weights beyond outer fences", 1 row (650 g), pre-set policy, script line [n].
Exercise C.
(a) Battery failures on random days: plausibly MCAR (the maintenance log shows no link to the day or the visits).
Storm closures: MAR if weather is recorded and included (missingness depends on observed rainfall), and arguably
MNAR if the partial counts were unusually low for reasons not captured. (b) Zero visits on an open day is
impossible for a busy branch, so it fails a consistency check (open day with zero visits) and should be set to NA as
a probable counter fault, with a log row. (c) Missing days are concentrated in wet weather, so complete cases
under-represent the stormiest days; the wet-week average is biased (probably upward if storm days have fewer
visits). Include rainfall in an imputation model or analyse with a model that uses it. (d) For example: "Counter
value 0 on open days set to NA, 2 rows, probable counter fault"; "Days with counter battery failure left NA and
marked miss_reason = battery, 5 rows".
Self-test.
- Because different spellings hide duplicates: "sci" and "SCI" would look like different rows. Standardise first, then compare.
- MAR: missingness depends on an observed variable (education), not on income itself once education is known.
- MNAR: missingness depends on the unobserved score itself. Imputation from observed variables does not fix it; use sensitivity analyses (for example, assume missing scores are lower by a set amount) and report the assumption.
- Worst case: missing values never overlap, so rows are incomplete and only 80 complete cases remain. Even small per-variable missingness can remove many rows, wasting information and possibly adding bias.
- IQR ; inner fences and ; outer upper fence . A value of 31 is beyond the inner upper fence but within the outer fence: a possible outlier, not a far-out value.
- The test has low power in small samples (not rejecting is not proof), it cannot detect MNAR, and it relies on multivariate normality; also, even under MCAR, complete-case analysis loses precision.
Revision history
| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [Your name] | Rules fixed in advance, before opening the data |
| 0.2 | YYYY-MM-DD | [Your name] | Example: Registry confirmed ID 109 credits are 30; consistency flag cleared by script step 5 |
| 1.0 | YYYY-MM-DD | [Your name] | Cleaning frozen before main analysis |
Related templates
Data Transformations Method Card Template (Statistics, R and Python)
Data transformations method card – log, sqrt, Box-Cox, Yeo-Johnson, logit, z-scores, centring and ranks, percent-change interpretation and smearing back-transformation, with R and Python
AcademicStatistical Analysis Plan Template (Statistics, Thesis or Project)
Statistical analysis plan for a thesis or project: estimands, hypotheses, sample size, primary model, pre-set assumption fallbacks, missing data, multiplicity, sensitivity, deviations log
AcademicStatistical Analysis Report Template (Statistics, Thesis or Client)
Complete statistical analysis report for a thesis results chapter or client: plain-language summary, methods, descriptives, main analyses, assumption checks, sensitivity, limitations, reproducibility appendix, reviewer checklist