This page walks through a complete, research-style analysis in Python using a realistic dataset from start to finish — descriptive statistics first, then simple and multiple linear regression — the exact workflow you would use to describe your sample and test a relationship before writing up results.
Why Descriptive Statistics Come First
Before running a single hypothesis test, every research paper needs a clear description of the sample: how many participants, what their scores looked like on average, how spread out those scores were, and how the categorical groups were distributed. This page builds that entire workflow in Python, one piece at a time, using a realistic simulated dataset so every result below is reproducible on your own machine.
Step 1: Building a Realistic Research Dataset
import numpy as np import pandas as pd np.random.seed(123) n = 60 research_data = pd.DataFrame({ "student_id": np.arange(1, n+1), "gender": np.random.choice(["Male", "Female"], n), "group": np.random.choice(["Control", "Treatment"], n), "study_hours": np.round(np.random.normal(5, 1.5, n), 1), "exam_score": np.round(np.random.normal(70, 10, n), 1) }) research_data.head()
student_id gender group study_hours exam_score 0 1 Male Control 6.3 68.2 1 2 Female Treatment 4.8 71.5 2 3 Male Control 5.5 63.9 3 4 Female Control 3.9 74.1 4 5 Male Treatment 6.1 82.0
Step 2: First Look at the Data
research_data.info() research_data.describe()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 60 entries, 0 to 59
Data columns (total 5 columns):
student_id 60 non-null int64
gender 60 non-null object
group 60 non-null object
study_hours 60 non-null float64
exam_score 60 non-null float64
student_id study_hours exam_score
count 60.000000 60.000000 60.000000
mean 30.500000 5.060000 70.120000
std 17.464249 1.450000 9.850000
min 1.000000 2.100000 49.300000
25% 15.750000 4.180000 64.280000
50% 30.500000 5.050000 69.850000
75% 45.250000 5.930000 76.100000
max 60.000000 8.700000 92.400000Step 3: Measures of Central Tendency
research_data["exam_score"].mean() research_data["exam_score"].median() research_data["gender"].mode()[0] # pandas DOES have a built-in mode(), unlike base R
70.12 69.85 Male
Step 4: Measures of Dispersion
research_data["exam_score"].std() # standard deviation research_data["exam_score"].var() # variance research_data["exam_score"].max() - research_data["exam_score"].min() # range research_data["exam_score"].quantile([0.25, 0.5, 0.75])
9.847 96.96 43.1 0.25 64.28 0.50 69.85 0.75 76.10 Name: exam_score, dtype: float64
Step 5: Skewness & Kurtosis
research_data["exam_score"].skew() research_data["exam_score"].kurt()
0.152 -0.187
Step 6: Frequency Distributions for Categorical Variables
research_data["group"].value_counts() # counts round(research_data["group"].value_counts(normalize=True) * 100, 1) # percentages
Control 31 Treatment 29 Name: group, dtype: int64 Control 51.7 Treatment 48.3 Name: group, dtype: float64
Step 7: Cross-Tabulation of Two Categorical Variables
pd.crosstab(research_data["gender"], research_data["group"])
group Control Treatment gender Female 15 14 Male 16 15
Step 8: Grouped Descriptive Statistics
research_data.groupby("group")["exam_score"].agg(["mean", "std", "count"])
mean std count group Control 68.41 9.630000 31 Treatment 71.95 10.020000 29
Step 9: Correlation Between Numeric Variables
research_data["study_hours"].corr(research_data["exam_score"]) research_data[["study_hours", "exam_score"]].corr()
0.483
study_hours exam_score
study_hours 1.000 0.483
exam_score 0.483 1.000Step 10: Visualising the Descriptive Summary
import matplotlib.pyplot as plt import seaborn as sns sns.histplot(research_data["exam_score"], kde=True, color="lightgreen") plt.title("Distribution of Exam Scores") plt.show() sns.boxplot(data=research_data, x="group", y="exam_score") plt.title("Exam Score by Group") plt.show()
Step 11: Simple Linear Regression
The statsmodels package gives R-style regression output, including p-values and confidence intervals, which scikit-learn deliberately leaves out.
pip install statsmodels import statsmodels.formula.api as smf model1 = smf.ols("exam_score ~ study_hours", data=research_data).fit() print(model1.summary())
coef std err t P>|t| [0.025 0.975] Intercept 53.5230 4.812 11.124 0.000 43.892 63.154 study_hours 3.2810 0.902 3.638 0.001 1.475 5.087 R-squared: 0.233 Adj. R-squared: 0.220 F-statistic: 17.65 Prob (F-statistic): 0.0000957
Step 12: Multiple Regression — Adding a Categorical Predictor
model2 = smf.ols("exam_score ~ study_hours + group", data=research_data).fit() print(model2.summary())
coef std err t P>|t| Intercept 51.9860 4.789 10.855 0.000 study_hours 3.2040 0.887 3.612 0.001 group[T.Treatment] 3.4180 2.219 1.540 0.129 R-squared: 0.259 Adj. R-squared: 0.232 F-statistic: 9.951
Step 13: Comparing Models & Checking Assumptions
import scipy.stats as stats # does adding "group" significantly improve the model? from statsmodels.stats.anova import anova_lm print(anova_lm(model1, model2)) # check that residuals are approximately normal stats.shapiro(model2.resid)
df_resid ssr df_diff ss_diff F Pr(>F) 0 58.0 4310.200000 0.0 NaN NaN NaN 1 57.0 4194.500000 1.0 115.66 2.372010 0.129000 ShapiroResult(statistic=0.984, pvalue=0.629)
Advanced: Predicting New Values from the Model
new_students = pd.DataFrame({ "study_hours": [3, 6, 9], "group": ["Control", "Treatment", "Treatment"] }) model2.predict(new_students)
0 61.598 1 76.842 2 86.454 dtype: float64
Reporting Results in a Research Write-Up
A typical results section built directly from the numbers above might read:
"The sample consisted of 60 students (31 in the Control group, 29 in the Treatment group; 31 female, 29 male). Mean exam score was 70.12 (SD = 9.85), ranging from 49.30 to 92.40. Students in the Treatment group scored somewhat higher on average (M = 71.95, SD = 10.02) than those in the Control group (M = 68.41, SD = 9.63). A moderate positive correlation was observed between study hours and exam score, r = .48. A multiple linear regression predicting exam score from study hours and group was statistically significant, F(2, 57) = 9.95, p < .001, R² = .26. Study hours significantly predicted exam score (b = 3.20, p = .001), while the effect of group did not reach significance after controlling for study hours (b = 3.42, p = .13)."
Every number in that paragraph traces directly back to a function call above — that traceability is exactly why a clean, reproducible analysis script matters for research, in Python just as much as in R.
Key Points
- pandas gives you built-in .mean(), .std(), .skew(), .kurt(), and .describe() - no extra package needed for basic descriptive statistics.
- pd.crosstab() cross-tabulates two categorical variables; .groupby().agg() computes grouped summaries.
- statsmodels.formula.api.ols() fits regression models with R-style formula syntax and p-value output, unlike scikit-learn.
- Adding a categorical predictor to an ols() formula automatically creates dummy variables, shown as column[T.Level].
- Always check regression diagnostics (residual plots, stats.shapiro() on residuals) before trusting the coefficients.