19 Putting It All Together

A thesis is an argument, and its results chapter is the evidence. Each claim in it rests on a chain of reasoning that runs from a research question, through a hypothesis, a design, measured variables, and an analysis, to a result, a conclusion, and the limits of that conclusion. A thesis convinces when every link in that chain can be seen and checked, and every number in the text can be traced back to the raw data. The methods of this book are the links; this chapter joins them.

In the story, it is the final semester. Elaf’s analyses are spread over two years of scripts, written as she learned each method, and her supervisor’s advice for the last stretch is simple: “Before you write the results chapter, rebuild the whole thing, from the raw export to the last table, as one project that runs from start to finish. Then you will know that every number in your thesis is right, and you can answer any examiner’s question by pointing to the code.” This chapter does exactly that. It sets up a complete research project, runs the analysis from the messy survey export to the tables, figures, and sentences of a results chapter, and traces the chain of reasoning behind three of the research questions. It then steps back: how to report results to the standards journals expect, how to choose a method for a new question, the problems that every researcher meets, and where to go next.

TipBy the end of this chapter you will be able to
  • Organise a research project so that it runs from raw data to finished results.
  • Combine cleaning, description, modelling, and visualisation in one reproducible pipeline.
  • Trace the chain of reasoning from research question to conclusion and limitation.
  • Report results in the style expected in a thesis, following published reporting standards, with numbers taken directly from the code.
  • Choose an appropriate method for a research question, using the map of this book.
  • Recognise common challenges in real research projects, and know where to learn more.

19.1 The shape of a research project

Every quantitative project follows roughly the same path, and this book has followed it too (Figure 19.1).

flowchart TB
  A[Question and<br/>design<br/>Ch 5] --> B[Import<br/>Ch 1-2]
  B --> C[Clean and<br/>reshape<br/>Ch 3]
  C --> D[Explore and<br/>describe<br/>Ch 4, 6]
  D --> E[Test and<br/>model<br/>Ch 7-10]
  D --> F[Predict and<br/>discover<br/>Ch 11-16]
  E --> G[Report and<br/>share<br/>Ch 17-18]
  F --> G
Figure 19.1: The path of a research project, with the chapters of this book that cover each step.

The path is not a straight line in practice. Exploring the data sends you back to cleaning when you find a problem; a model’s diagnostics send you back to exploring. What matters is that each step is written in code, so that going back and rerunning everything is easy.

19.1.1 Organising the project

A project that others (and your future self) can follow has a predictable structure. The final thesis project looks like this:

wellbeing-thesis/
├── README.txt                  what the project is and how to run it
├── data-raw/
│   └── wellbeing_raw.xlsx      the survey export, never edited by hand
├── data/                       clean data, created by the scripts
├── scripts/
│   ├── 01_clean_data.py        raw export -> clean tables (Chapter 3)
│   └── 02_analysis.py          models and figures
├── output/                     figures and tables, created by the scripts
├── results.qmd                 the results chapter (Chapter 17)
├── references.bib
└── requirements.txt            package versions (Chapter 17)

Four principles lie behind it. Raw data is read-only: everything in data/ and output/ can be deleted and recreated by running the scripts. Scripts are numbered in the order they run, and each does one job. Paths are relative to the project, using pathlib.Path (Chapter 1), so the project runs on any computer. And the README says in a few lines what the project is and how to run it.

The downloadable project for this chapter has the same structure, with working scripts (its data/ and output/ folders are created when the scripts run, and it provides a requirements.txt for virtual environments). The rest of the chapter walks through what the scripts do.

19.2 From raw export to clean data

The cleaning of Chapter 3, gathered into one script, runs in a few seconds. Here is its core, reading the raw export and producing the clean questionnaire and semester tables:

import pandas as pd
import numpy as np

raw = pd.read_excel(data2thesis_example("wellbeing_raw.xlsx"))

item_names = [f"stress_{i}" for i in range(1, 7)] + \
             [f"burnout_{i}" for i in range(1, 7)] + \
             [f"support_{i}" for i in range(1, 7)] + \
             [f"satisfaction_{i}" for i in range(1, 5)]

# 1. Drop test responses and duplicates
responses = raw[~raw["Q1_Student ID"].str.upper().str.startswith("TEST")].copy()
responses = responses.drop(columns=["Response ID"]).drop_duplicates()
responses = responses.rename(columns={
    "Q1_Student ID": "student_id",
    "Workshop group": "workshop",
    "Y1_Considered leaving?": "considering_dropout"
})

# Rename item columns Q11_1:Q11_22
q11_cols = [f"Q11_{i}" for i in range(1, 23)]
responses = responses.rename(columns=dict(zip(q11_cols, item_names)))

for col in item_names:
    responses[col] = pd.to_numeric(responses[col].replace([99, "99"], np.nan), errors="coerce")

questionnaire_clean = responses[["student_id"] + item_names].copy()

# 2. Reshape semesters from wide to long
def parse_number(s):
    return pd.to_numeric(s.astype(str).str.replace(",", ".").str.extract(r"(\d+(?:\.\d+)?)")[0], errors="coerce")

long_records = []
for sem in [1, 2, 3, 4]:
    sub = pd.DataFrame({
        "student_id": responses["student_id"],
        "semester": sem,
        "gpa": parse_number(responses[f"GPA_S{sem}"]),
        "sleep_hours": parse_number(responses[f"Sleep_S{sem}"]),
        "study_hours": parse_number(responses[f"Study_S{sem}"]),
        "exercise_days": parse_number(responses[f"Exercise_S{sem}"]),
        "caffeine_mg": parse_number(responses[f"Caffeine_S{sem}"]),
        "supervisor_meetings": parse_number(responses[f"Meetings_S{sem}"]),
        "wellbeing": parse_number(responses[f"Wellbeing_S{sem}"])
    })
    long_records.append(sub)

semesters_clean = pd.concat(long_records, ignore_index=True)
semesters_clean.loc[semesters_clean["sleep_hours"] > 24, "sleep_hours"] = np.nan
semesters_clean.loc[semesters_clean["study_hours"] > 168, "study_hours"] = np.nan
semesters_clean.loc[semesters_clean["gpa"] > 4, "gpa"] = np.nan

metric_cols = ["gpa", "sleep_hours", "study_hours", "exercise_days", "caffeine_mg", "supervisor_meetings", "wellbeing"]
semesters_clean = semesters_clean.dropna(subset=metric_cols, how="all")
semesters_clean["supervisor_meetings"] = semesters_clean["supervisor_meetings"].astype(int)
semesters_clean["wellbeing"] = semesters_clean["wellbeing"].astype(int)

Each step is explained in Chapter 3; the full script in the downloadable project also cleans the background variables. The most important line of any cleaning script is the check at the end, here comparing with the clean reference data:

def check_match(mine, theirs, key):
    m = mine.sort_values(key).reset_index(drop=True)
    t = theirs[mine.columns].sort_values(key).reset_index(drop=True)
    return m.equals(t)

print("Questionnaire matches:", check_match(questionnaire_clean, questionnaire, "student_id"))
print("Semesters matches:    ", check_match(semesters_clean, semesters, ["student_id", "semester"]))
Questionnaire matches: True
Semesters matches:     True

Both are True. In a real project there is no package to compare with, so the checks are the ones from Chapters 3 and 6: counts of categories, ranges of values, numbers of missing values, and a look at a few rows. From here on, the chapter uses the clean tables, together with the scale scores:

scores = questionnaire.copy()
scores["stress_4"] = 6 - scores["stress_4"]
scores["stress"] = scores.filter(like="stress_").mean(axis=1)
scores["burnout"] = scores.filter(like="burnout_").mean(axis=1)
scores["support"] = scores.filter(like="support_").mean(axis=1)
scores = scores[["student_id", "stress", "burnout", "support"]]

study = students.merge(scores, on="student_id").merge(semesters.query("semester == 1"), on="student_id")
print(f"Total students in study dataset: {len(study)}")
Total students in study dataset: 600

The table study has one row per student, with background, questionnaire scores, and first-semester records, as in Chapter 8.

19.3 Describing the sample

Every results chapter begins by describing the participants, often in a table known as “Table 1”. Here the sample is described by programme:

Table 19.1: Characteristics of the sample, by programme.
def mean_sd(x):
    return f"{x.mean():.1f} ({x.std(ddof=1):.1f})"

def percent(x):
    return f"{100 * x.mean():.0f}%"

def describe(d):
    return pd.Series({
        "Students": str(len(d)),
        "Age, mean (SD)": mean_sd(d["age"]),
        "Women": percent(d["gender"] == "Female"),
        "Part-time": percent(d["study_mode"] == "Part-time"),
        "Stress (1-5), mean (SD)": mean_sd(d["stress"]),
        "Support (1-5), mean (SD)": mean_sd(d["support"]),
        "Wellbeing in semester 1, mean (SD)": mean_sd(d["wellbeing"]),
        "Considered dropping out": percent(d["considering_dropout"] == "Yes")
    })

sample_desc = pd.DataFrame({
    "Characteristic": describe(study).index,
    "Master's": describe(study[study["programme"] == "Master's"]).values,
    "PhD": describe(study[study["programme"] == "PhD"]).values,
    "All": describe(study).values
})
print(sample_desc.to_markdown(index=False))
| Characteristic                     | Master's    | PhD         | All         |
|:-----------------------------------|:------------|:------------|:------------|
| Students                           | 426         | 174         | 600         |
| Age, mean (SD)                     | 27.9 (3.3)  | 34.2 (5.1)  | 29.7 (4.8)  |
| Women                              | 52%         | 53%         | 52%         |
| Part-time                          | 26%         | 39%         | 30%         |
| Stress (1-5), mean (SD)            | 3.2 (0.7)   | 3.2 (0.7)   | 3.2 (0.7)   |
| Support (1-5), mean (SD)           | 3.2 (0.8)   | 3.2 (0.7)   | 3.2 (0.8)   |
| Wellbeing in semester 1, mean (SD) | 60.9 (12.1) | 59.3 (11.9) | 60.5 (12.0) |
| Considered dropping out            | 15%         | 14%         | 15%         |

Two small helper functions, mean_sd() and percent(), format the numbers the way theses report them, and describe() builds the column for any group of students, so the same code makes all three columns. Writing a small function whenever you would otherwise copy and paste code is one of the best habits to take from this book.

19.4 Answering the research questions

Three of the research questions are answered below with the methods of Parts 2 and 3, each with a sentence written the way it will appear in the thesis. A small function formats p-values in the usual style:

def format_p(p):
    return "p < .001" if p < 0.001 else f"p = {p:.3f}".replace(" 0.", " .")

19.4.1 The workshop and its persistence (RQ3)

Students were randomly invited to the wellbeing workshop after the first semester. The mixed-effects model of Chapter 10 uses all four semesters and every student:

panel = semesters.merge(students, on="student_id").assign(time=lambda d: d["semester"] - 1)

workshop_model = smf.mixedlm(
    "wellbeing ~ C(semester) * workshop",
    panel,
    groups=panel["student_id"],
    re_formula="~time"
).fit(method=["lbfgs", "powell"])

pred_grid = pd.MultiIndex.from_product(
    [[1, 2, 3, 4], ["Invited", "Not invited"]],
    names=["semester", "workshop"]
).to_frame().reset_index(drop=True)
pred_grid["time"] = pred_grid["semester"] - 1
pred_grid["predicted_wellbeing"] = workshop_model.predict(pred_grid)

piv = pred_grid.pivot(index="semester", columns="workshop", values="predicted_wellbeing")
gaps = piv["Invited"] - piv["Not invited"]
print("Wellbeing gaps across semesters:")
print(gaps.round(1))
Wellbeing gaps across semesters:
semester
1    0.4
2    5.3
3    4.1
4    2.6
dtype: float64

Wellbeing was similar in the two groups before the workshop (difference 0.4 points). After the workshop, invited students’ wellbeing was 5.3 points higher in semester 2, but the difference narrowed to 4.1 points in semester 3 and 2.6 in semester 4.

19.4.2 Explaining GPA (RQ5)

The multiple regression of Chapter 8, with coefficients and confidence intervals organized as a clean summary:

gpa_model = smf.ols("gpa ~ sleep_hours + study_hours + stress + support", data=study).fit()

gpa_table = pd.DataFrame({
    "estimate": gpa_model.params,
    "std_err": gpa_model.bse,
    "t_value": gpa_model.tvalues,
    "p_value": gpa_model.pvalues,
    "conf_low": gpa_model.conf_int()[0],
    "conf_high": gpa_model.conf_int()[1]
})
print(gpa_table.round(3))
             estimate  std_err  t_value  p_value  conf_low  conf_high
Intercept       2.137    0.145   14.712      0.0     1.852      2.423
sleep_hours     0.106    0.014    7.487      0.0     0.078      0.134
study_hours     0.007    0.001    6.270      0.0     0.005      0.009
stress         -0.084    0.018   -4.685      0.0    -0.119     -0.049
support         0.111    0.017    6.632      0.0     0.078      0.144

Each additional hour of sleep was associated with a GPA 0.11 points higher (95% CI 0.08 to 0.13, p < .001), holding study hours, stress, and support constant. Together, the four predictors explained 25% of the variation in first-semester GPA (n = 587).

19.4.3 Considering dropout (RQ9)

The logistic regression of Chapter 8, with odds ratios:

study["dropout_bool"] = (study["considering_dropout"] == "Yes").astype(int)

dropout_model = smf.logit(
    "dropout_bool ~ stress + support + financial_worry + C(employment) + C(study_mode)",
    data=study
).fit(disp=False)

dropout_table = pd.DataFrame({
    "odds_ratio": np.exp(dropout_model.params),
    "conf_low": np.exp(dropout_model.conf_int()[0]),
    "conf_high": np.exp(dropout_model.conf_int()[1]),
    "p_value": dropout_model.pvalues
})
print(dropout_table.round(2))
                                odds_ratio  conf_low  conf_high  p_value
Intercept                             0.02      0.00       0.20     0.00
C(employment)[T.None]                 0.55      0.24       1.31     0.18
C(employment)[T.Part-time job]        0.58      0.24       1.42     0.23
C(study_mode)[T.Part-time]            1.32      0.64       2.71     0.45
stress                                3.60      2.25       5.77     0.00
support                               0.33      0.22       0.49     0.00
financial_worry                       1.57      1.24       2.00     0.00

np.exp() turns the log-odds coefficients into odds ratios and their confidence intervals.

Each one-point increase in stress (on the 1 to 5 scale) was associated with 3.6 times the odds of considering dropping out (95% CI 2.3 to 5.8), and each one-point increase in supervisor support with 0.33 times the odds (95% CI 0.22 to 0.49).

Chapters 11 to 15 went further with this question, asking how well dropout can be predicted; the thesis reports that logistic regression predicted as well as machine learning models (a test AUC of about 0.84 in Chapter 11), which is itself a finding worth stating.

19.5 One figure for the thesis

A thesis figure often combines panels. Using matplotlib.pyplot.subplots(), we can join figures side by side with consistent styling:

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4))

# Panel A: Workshop Trajectory
for w_label, color in [("Invited", navy), ("Not invited", copper)]:
    sub = pred_grid[pred_grid["workshop"] == w_label]
    ax1.plot(sub["semester"], sub["predicted_wellbeing"], marker="o", color=color, linewidth=2, label=w_label)

ax1.set_xlabel("Semester")
ax1.set_ylabel("Predicted wellbeing (0-100)")
ax1.set_xticks([1, 2, 3, 4])
ax1.legend(title="Workshop")
ax1.set_title("A", loc="left", fontweight="bold", fontsize=13)

# Panel B: Sleep and GPA
sns.regplot(
    data=study, x="sleep_hours", y="gpa",
    ax=ax2,
    scatter_kws={"alpha": 0.3, "color": "black", "s": 20},
    line_kws={"color": navy, "linewidth": 2}
)
ax2.set_xlabel("Sleep (hours a night)")
ax2.set_ylabel("GPA (semester 1)")
ax2.set_title("B", loc="left", fontweight="bold", fontsize=13)

for ax in (ax1, ax2):
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)

plt.tight_layout()
plt.show()
Two panels. Panel A: two lines over semesters 1 to 4; the invited group rises above the not-invited group in semester 2, and the gap narrows afterwards. Panel B: a scatter plot of GPA against sleep hours with an upward-sloping regression line.
Figure 19.2: (A) Predicted wellbeing by semester and workshop group, from the mixed-effects model. (B) First-semester GPA against hours of sleep, with a regression line.

plt.savefig() saves the figure at the dimensions and resolution a thesis or journal requires (Chapter 4):

fig.savefig("output/figure-1.png", dpi=300, bbox_inches="tight")

19.6 The chain of reasoning

A result on its own is not yet a conclusion. Between the two lie the design that produced the data, the way the variables were measured, and the limits of both. Table 19.2 traces the whole chain for the three research questions answered above, from the hypotheses stated in Chapter 5 to the limitations that a thesis discussion must acknowledge. Every result in it is filled in by the code of this chapter.

Table 19.2: The chain of reasoning for three research questions, from question to limitation
Step Workshop (RQ3) GPA (RQ5) Considering dropout (RQ9)
Question Does the workshop improve wellbeing? What explains students’ GPA? Who considers dropping out?
Hypothesis (Chapter 5) Invited students have higher wellbeing in semester 2 More sleep goes with a higher GPA, allowing for study hours, stress, and support Higher stress raises the odds of considering dropout
Design Randomised invitation within a longitudinal study Observational, first semester Observational, baseline and end of year 1
Variables Wellbeing (0 to 100), invitation, semester GPA, sleep, study hours, stress, support Considering dropout (yes/no), stress, support, financial worry, employment, study mode
Analysis Mixed-effects model (Chapter 10) Multiple regression (Chapter 8) Logistic regression (Chapter 8)
Result 5.3 points higher in semester 2, narrowing to 2.6 by semester 4 0.11 GPA points per hour of sleep (95% CI 0.08 to 0.13) Odds ratio 3.6 per point of stress (95% CI 2.2 to 5.8)
Conclusion The workshop raised wellbeing, and the effect faded; because the invitation was random, the effect is causal Sleep is associated with GPA, independently of the other predictors; the null hypothesis is rejected, but the design does not show cause Stress is associated with higher odds of considering dropout; the null hypothesis is rejected
Limitation One university; the invitation, not attendance, was randomised Sleep is self-reported, and unmeasured confounders may remain The outcome is considering dropout, not leaving; stress and the outcome were measured close together

Reading the table by columns shows how different the three conclusions are, although all three rest on “significant” results. Only the workshop conclusion is causal, because only the workshop was assigned at random (Chapter 5). The GPA and dropout conclusions are associations, and the limitations say what could still explain them. A discussion chapter that keeps each result attached to its design, in this way, claims exactly what the evidence supports.

19.7 The results chapter

The final step is the one Chapter 17 prepared: the tables, figures, and sentences above go into a Quarto document, results.qmd, in which every number is written with inline code. The downloadable project contains such a document, built from this chapter.

What a results chapter must contain is not left to taste. Reporting standards list the information that readers need in order to judge a study. For quantitative research in psychology and neighbouring fields, the American Psychological Association’s Journal Article Reporting Standards (JARS) set out what to report about the participants, the measures, the analysis, and the results, including effect sizes and confidence intervals (Appelbaum et al. 2018). Randomised trials in health research follow CONSORT, and observational studies follow STROBE (von Elm et al. 2007). Checking a draft against the relevant standard is one of the most useful things a student can do before submission, and many journals require it. When the data changes, or an examiner asks for a different model, the code is changed and rendered again, and the whole chapter is updated.

19.8 Choosing a method

The choice of method for a new research question depends on the goal, on the kind of outcome, and on how the observations are related. Figure 19.3 summarises the methods of this book as a guide.

%%{init: {"flowchart": {"nodeSpacing": 18, "rankSpacing": 45}}}%%
flowchart TD
  Q{What is the goal?} -->|Describe| D[Summaries and plots<br/>Ch 4, 6]
  Q -->|Explain or compare| O{Outcome type?}
  Q -->|Predict new cases| P[Machine learning<br/>Ch 11-15]
  Q -->|Find groups or<br/>structure| U[PCA, factor analysis,<br/>clustering<br/>Ch 9, 14]
  Q -->|Forecast over time| T[Time series<br/>Ch 16]
  O -->|Numeric| N{Repeated or<br/>nested data?}
  O -->|Yes / No| L[Chi-square, logistic<br/>regression<br/>Ch 7-8]
  N -->|No| R[t-test, ANOVA,<br/>regression<br/>Ch 7-8]
  N -->|Yes| M[Mixed-effects<br/>models<br/>Ch 10]
Figure 19.3: A guide to choosing a method, with the chapters that cover each.

Table 19.3 shows how each of the study’s research questions was answered.

Table 19.3: The study’s research questions and the methods used to answer them
Research question Method Chapters
RQ1 What does graduate life look like? Plots, descriptive statistics 4, 6
RQ2 Do students sleep less than 7 hours? One-sample t-test, confidence interval 7
RQ3 Does the workshop improve wellbeing? Two-sample t-test; mixed-effects model 7, 10
RQ4 Do faculties and study modes differ? ANOVA, post-hoc tests 7, 8
RQ5 What explains GPA? Multiple regression 8, 13
RQ6 Does the questionnaire measure what it should? Factor analysis, Cronbach’s alpha 9
RQ7 Are there student profiles? Cluster analysis, mixture models 9, 14
RQ8 How do wellbeing and GPA change? Mixed-effects models 10
RQ9 Who considers dropping out? Logistic regression; classification 8, 11, 12, 15
RQ10 Can final GPA be predicted? Regularised regression, gradient boosting 13
RQ11 What challenges do students describe? Text coding with a language model 18
RQ12 How many counselling visits next year? Time series forecasting 16

19.9 Challenges every researcher meets

Real projects rarely go as planned, and some problems are almost universal. Every real dataset is messy and needs cleaning, which takes longer than the analysis; it is done in code, checked after every step, and never applied to the raw file itself (Chapter 3). Missing data raises the question of why it is missing before anything is done about it, because dropping incomplete cases can bias results when the missing data is not random, as with the students who left the programme (Chapter 6). Small samples give wide confidence intervals and low power (Chapter 7) and make complex models overfit (Chapter 13); the remedies are to report effect sizes with intervals, keep models simple, and plan the sample size before collecting data with a power analysis (Chapter 5).

Analysis brings its own problems. Many tests produce many false alarms, so the main analyses are decided in advance, multiple comparisons are corrected where needed, and exploratory results are reported as exploratory (Chapters 7, 8, and 17). Assumptions are checked with plots rather than with tests alone, with robust tests, non-parametric tests, transformations, and mixed models as the alternatives (Chapters 7, 8, and 10). Correlation is not causation: observational data rarely proves causes, so confounders must be thought through, as sleep was behind the caffeine effect, and randomised designs used where possible, as in the workshop study (Chapters 5 and 8). Prediction and explanation are different aims: a model that predicts well may explain little, and the reverse (Chapter 11). Finally, results must be communicated to supervisors, co-authors, and examiners who may use other software; a clear results chapter with tables, figures, and an appendix of code serves them all, and data can be exported with to_csv() or pyreadstat when they need it (Chapter 2).

19.10 Where to go next

This book is a beginning. Depending on your field, the next methods to learn may be:

Table 19.4: Topics for further study
Topic What it is for Python Packages
Structural equation modelling Confirmatory factor analysis, path models, latent variables semopy
Bayesian statistics Models with prior knowledge and full uncertainty PyMC, bambi
Survival analysis Time until an event, such as dropping out lifelines
Meta-analysis Combining results from several studies statsmodels
Natural language processing Words, sentiment, and embeddings in text spacy, transformers
Spatial data Maps and geographic data geopandas
Publication tables Formatted summary tables of results great_tables, tabulate
Interpreting models Feature importances, SHAP values, and partial effects shap, scikit-learn

Some of the best resources are free online: Python for Data Analysis by Wes McKinney (McKinney 2022), Regression and Other Stories (Gelman et al. 2020) for regression modeling, An Introduction to Statistical Learning with Applications in Python (James et al. 2023) for machine learning, and the official documentation of Scikit-Learn, Statsmodels, and PyTorch. Appendix E lists more.

You do not have to learn alone. The Python data science community is large and welcoming: Stack Overflow, Python Discord servers, local PyData meetups, and open-source contributors around the world. Asking a clear question, with a small reproducible example, is itself an invaluable research skill, and the same skill that makes an AI assistant useful (Chapter 18).

NoteIn your field: nutrition science

The same path works for any dataset, in a few lines. The classic ToothGrowth dataset comes from an experiment on the growth of teeth in 60 guinea pigs given vitamin C at three doses, administered via orange juice (OJ) or ascorbic acid (VC). A complete mini-project, from data to a reported result:

tooth = sm.datasets.get_rdataset("ToothGrowth", "datasets").data
tooth["dose"] = tooth["dose"].astype(str)

tooth_summary = tooth.groupby(["supp", "dose"])["len"].agg(["mean", "std", "count"]).round(2)
print(tooth_summary)

tooth_model = smf.ols("len ~ C(supp) * C(dose)", data=tooth).fit()
anova_res = anova_lm(tooth_model, typ=2)
print(anova_res.round(3))
            mean   std  count
supp dose                    
OJ   0.5   13.23  4.46     10
     1.0   22.70  3.91     10
     2.0   26.06  2.66     10
VC   0.5    7.98  2.75     10
     1.0   16.77  2.52     10
     2.0   26.14  4.80     10
                   sum_sq    df       F  PR(>F)
C(supp)           205.350   1.0  15.572   0.000
C(dose)          2426.434   2.0  92.000   0.000
C(supp):C(dose)   108.319   2.0   4.107   0.022
Residual          712.106  54.0     NaN     NaN

The two-way ANOVA of Chapter 8 shows that tooth length depends on the dose, the delivery method of vitamin C, and their interaction: orange juice is more effective than the supplement at low and medium doses, but both produce comparable growth at the highest dose (2.0 mg). The same steps (prepare, describe, model, report) would take a few more lines with a plot and a Quarto report. Whatever your field, the path from data to thesis is the one this book has followed.

19.11 Common misconceptions

A few misunderstandings are common at the stage of writing up.

  • “A significant result answers the research question.” It answers one link of the chain; the design and the measures decide what the result means.
  • “The discussion should defend the results.” It should also state their limits; examiners trust a thesis more when it says clearly what it cannot show.
  • “Formatting numbers by hand is quicker.” It is quicker once, and wrong the next time the data or the model changes.
  • “Choosing a method means choosing the most advanced one.” The right method is the simplest one that matches the goal, the outcome, and the structure of the data.

19.12 Chapter review

19.12.1 Summary

  • A research project follows a path from question and design through import, cleaning, exploration, and modelling to reporting and sharing, often going back to earlier steps. Writing every step in code makes going back easy.
  • Organise projects with read-only raw data, numbered scripts, relative paths, a README, and a Quarto results document; everything else can be recreated.
  • Check the cleaned data at the end of the cleaning script, then describe the sample, answer each research question with the method suited to it, and report results with numbers taken directly from the code.
  • Every result rests on a chain of reasoning, from question and hypothesis through design, variables, and analysis to result, conclusion, and limitation. Reporting standards such as the APA’s JARS, CONSORT, and STROBE list what a report must contain.
  • Small helper functions (for formatting means, percentages, and p-values) avoid copying and pasting code; pandas and statsmodels turn model results into tidy summary tables; matplotlib subplots combine figures.
  • Choose methods by the goal (describe, explain, predict, find structure, forecast), the type of outcome, and how the observations are related.
  • Messy and missing data, small samples, many tests, assumptions, and causation are challenges in every project; the chapters of this book give tools for each.

19.12.2 Key terms

Research workflow, project structure, raw data, pipeline, sample description (“Table 1”), helper function, tidy model summary, multi-panel figure, reporting sentence, chain of reasoning, reporting standard, method choice.

19.13 Exercises

The playground has these and more, and the downloadable project contains the complete thesis project.

  1. Download the Chapter 19 project, run scripts/01_clean_data.py and scripts/02_analysis.py, and render results.qmd. Then change one cleaning rule (for example, treat ages above 70 as impossible), render again, and note which numbers change.
  2. Add a row to the sample table (Table 19.1) for the share of students with children.
  3. Write a reporting sentence, with inline numbers, for the effect of support on GPA in the regression model.
  4. Add a third panel to the thesis figure: wellbeing by faculty as a box plot.
  5. Choose a research question from your own field. Using Figure 19.3, decide which method you would use, and which chapter of this book you would reread first.
  6. Complete the chain of reasoning (Table 19.2) for RQ2, whether students sleep less than 7 hours, using the results of Chapter 7.

19.14 Further reading

  • Python for Data Analysis (McKinney 2022) covers data manipulation, reshaping, and exploratory analysis in Python.
  • Regression and Other Stories (Gelman et al. 2020) is an excellent guide to building, checking, and reporting regression models in real research.
  • An Introduction to Statistical Learning with Applications in Python (James et al. 2023) provides hands-on machine learning modeling.

References

Appelbaum, Mark, Harris Cooper, Rex B. Kline, Evan Mayo-Wilson, Arthur M. Nezu, and Stephen M. Rao. 2018. “Journal Article Reporting Standards for Quantitative Research in Psychology: The APA Publications and Communications Board Task Force Report.” American Psychologist 73 (1): 3–25. https://doi.org/10.1037/amp0000191.
Gelman, Andrew, Jennifer Hill, and Aki Vehtari. 2020. Regression and Other Stories. Cambridge University Press. https://doi.org/10.1017/9781139161879.
James, Gareth, Daniela Witten, Trevor Hastie, Robert Tibshirani, and Jonathan Taylor. 2023. An Introduction to Statistical Learning: With Applications in Python. Springer. https://doi.org/10.1007/978-3-031-38747-0.
McKinney, Wes. 2022. Python for Data Analysis: Data Wrangling with Pandas, NumPy, and Jupyter. 3rd ed. O’Reilly Media. https://wesmckinney.com/book/.
von Elm, Erik, Douglas G. Altman, Matthias Egger, Stuart J. Pocock, Peter C. Gøtzsche, and Jan P. Vandenbroucke. 2007. “The Strengthening the Reporting of Observational Studies in Epidemiology (STROBE) Statement: Guidelines for Reporting Observational Studies.” The Lancet 370 (9596): 1453–57. https://doi.org/10.1016/S0140-6736(07)61602-X.