0Pricing
Pandas & NumPy Academy · Lesson

Descriptive Stats and Normality Testing

Compute skewness and kurtosis with scipy.stats, run a Shapiro-Wilk test for normality, and interpret the p-value.

Descriptive Stats and Normality Testing is a free Pandas & NumPy Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Pandas & NumPy Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Descriptive vs Inferential Statistics

Descriptive statistics summarise what is in your data: mean, median, standard deviation, skewness. Inferential statistics make claims about a population based on a sample: hypothesis tests, confidence intervals, p-values. SciPy's scipy.stats module bridges these worlds — it provides both descriptive measures that go beyond Pandas' describe() and the full suite of classical hypothesis tests. Combining Pandas for data manipulation and SciPy for statistical testing is the standard Python data science workflow.

Extended Descriptive Statistics

Pandas describe() gives count, mean, std, min/max, and quartiles. scipy.stats adds skewness (asymmetry of the distribution) and kurtosis (tail heaviness). Skewness of 0 means symmetric; positive skewness means a longer right tail (many small values, few very large). Kurtosis of 3 (or 0 in excess form) is normal; higher values indicate heavier tails with more extreme outliers than a normal distribution would produce.

import pandas as pd
from scipy import stats
import numpy as np

np.random.seed(0)
data = np.concatenate([
    np.random.exponential(scale=2, size=500),  # right-skewed
    np.random.normal(loc=5, scale=1, size=500)
])

print('Mean:', round(data.mean(), 3))
print('Std:', round(data.std(), 3))
print('Skewness:', round(stats.skew(data), 3))
print('Kurtosis (excess):', round(stats.kurtosis(data), 3))

Understanding Skewness

Skewness matters for choosing statistical tests and transformations. A right-skewed (positive skew) distribution has its mean larger than its median, typical of income data, reaction times, and sales amounts. A left-skewed (negative skew) distribution has its mean smaller than its median. Rule of thumb: |skewness| < 0.5 is approximately symmetric; 0.5–1.0 is moderately skewed; > 1.0 is highly skewed and may benefit from a log or square-root transformation before applying tests that assume normality.

import numpy as np
from scipy import stats

# Right-skewed: income-like data
right_skewed = np.random.lognormal(mean=1, sigma=1, size=1000)
print('Right skew:', round(stats.skew(right_skewed), 3))

# After log transform
print('After log transform:', round(stats.skew(np.log(right_skewed)), 3))

# Symmetric normal
normal_data = np.random.normal(0, 1, 1000)
print('Normal skew:', round(stats.skew(normal_data), 3))

Why Normality Testing Matters

Many statistical tests — t-tests, ANOVA, Pearson correlation — assume that the data (or the residuals) follow a normal (Gaussian) distribution. If this assumption is violated with small samples, the test's p-values may be inaccurate. Normality testing checks whether the assumption holds. For large samples (n > 100), normality tests become very sensitive and will almost always reject normality for trivial deviations — in that case, rely on the Central Limit Theorem (sample means are approximately normal) rather than testing the raw data.

The Shapiro-Wilk Test

The Shapiro-Wilk test (scipy.stats.shapiro(data)) is the most powerful normality test for sample sizes up to about 5,000. It returns a W-statistic and a p-value. If p > 0.05, you fail to reject normality — the data is consistent with a normal distribution. If p ≤ 0.05, you reject normality. Remember: failing to reject normality does not prove the data is normal; it just means you do not have enough evidence to say it is not.

import numpy as np
from scipy import stats

np.random.seed(42)

# Normal data
normal = np.random.normal(0, 1, 100)
stat, p = stats.shapiro(normal)
print(f'Normal data: W={stat:.4f}, p={p:.4f}')
print('Conclusion:', 'Looks normal' if p > 0.05 else 'Not normal')

# Skewed data
skewed = np.random.exponential(1, 100)
stat2, p2 = stats.shapiro(skewed)
print(f'Skewed data: W={stat2:.4f}, p={p2:.4f}')
print('Conclusion:', 'Looks normal' if p2 > 0.05 else 'Not normal')

The Kolmogorov-Smirnov Test

The Kolmogorov-Smirnov (K-S) test (scipy.stats.kstest(data, 'norm', args)) tests whether a sample follows a specified distribution. Unlike Shapiro-Wilk, it works for any distribution (not just normal) and for large samples. It computes the maximum difference between the empirical CDF of your data and the theoretical CDF. A variation, the two-sample K-S test (stats.ks_2samp(a, b)), tests whether two samples come from the same distribution — useful for comparing before/after distributions.

import numpy as np
from scipy import stats

np.random.seed(0)
data = np.random.normal(loc=5, scale=2, size=200)

# One-sample K-S test against N(5, 2) distribution
stat, p = stats.kstest(data, 'norm', args=(5, 2))
print(f'K-S test: stat={stat:.4f}, p={p:.4f}')
print('Follows N(5,2)?', 'Yes' if p > 0.05 else 'No')

# Two-sample K-S test
data2 = np.random.normal(loc=5.5, scale=2, size=200)
stat2, p2 = stats.ks_2samp(data, data2)
print(f'Two-sample K-S: stat={stat2:.4f}, p={p2:.4f}')

Visual Normality Check: QQ Plot

The Q-Q (quantile-quantile) plot is a visual normality check: it plots the quantiles of your data against the quantiles of a normal distribution. If the data is normal, the points fall on a straight diagonal line. Deviations from the line indicate departure from normality — S-curves indicate kurtosis, bends indicate skewness. Statistical tests tell you if deviations are significant; Q-Q plots show you the nature and location of deviations. Use scipy.stats.probplot() to generate Q-Q plot data.

import numpy as np
from scipy import stats
import matplotlib.pyplot as plt

np.random.seed(1)
data = np.random.exponential(2, 200)

# Q-Q plot
(osm, osr), (slope, intercept, r) = stats.probplot(data, dist='norm')
print(f'R-squared of Q-Q fit: {r**2:.4f}')  # Close to 1 = normal
# Visual inspection: if plotting, points on the line = normal
# plt.figure()
# stats.probplot(data, dist='norm', plot=plt)
# plt.show()

The Central Limit Theorem in Practice

The Central Limit Theorem (CLT) states that the distribution of sample means approaches normality as sample size grows, regardless of the underlying distribution. For n ≥ 30, the sample mean is approximately normal even if individual observations are skewed. This is why t-tests are robust for large samples: you are testing whether the mean differs, and the mean is approximately normal even when the raw data is not. For small samples from non-normal populations, use non-parametric tests instead.

import numpy as np
from scipy import stats

np.random.seed(0)
# Population: heavily right-skewed (exponential)
population = np.random.exponential(scale=1, size=10000)
print('Population skewness:', round(stats.skew(population), 3))

# Sample means are approximately normal (CLT)
sample_means = [np.random.choice(population, 50).mean()
                for _ in range(1000)]
print('Sample means skewness:', round(stats.skew(sample_means), 3))
_, p = stats.shapiro(sample_means)
print('Shapiro p-value for means:', round(p, 4))

Normality by Group

In group comparison tests (t-test, ANOVA), normality is required within each group, not in the overall data. When testing whether sales differ by region, test the normality of sales within each region separately. A distribution that is not normal overall can still satisfy normality within subgroups. Apply stats.shapiro() to each group using Pandas groupby() and iterate over groups to check the assumption systematically.

import pandas as pd
import numpy as np
from scipy import stats

np.random.seed(0)
df = pd.DataFrame({
    'region': ['N']*50 + ['S']*50 + ['E']*50,
    'sales': np.concatenate([
        np.random.normal(100, 20, 50),
        np.random.normal(110, 25, 50),
        np.random.normal(95, 15, 50)
    ])
})

for region, group in df.groupby('region'):
    stat, p = stats.shapiro(group['sales'])
    print(f'Region {region}: W={stat:.4f}, p={p:.4f} -> '
          f'{"Normal" if p>0.05 else "Not normal"}')

Non-Parametric Alternatives

When normality is violated, use non-parametric tests that make no distributional assumptions. The Mann-Whitney U test (stats.mannwhitneyu) replaces the independent t-test; the Wilcoxon signed-rank test (stats.wilcoxon) replaces the paired t-test; the Kruskal-Wallis test (stats.kruskal) replaces one-way ANOVA. These tests use ranks instead of raw values and are robust to outliers, but they have less statistical power than their parametric counterparts when normality actually holds.

import numpy as np
from scipy import stats

np.random.seed(0)
# Non-normal data
group_a = np.random.exponential(2, 40)
group_b = np.random.exponential(2.5, 40)

# Parametric would be wrong here; use non-parametric
stat, p = stats.mannwhitneyu(group_a, group_b, alternative='two-sided')
print(f'Mann-Whitney U test: U={stat:.1f}, p={p:.4f}')
print('Groups differ?' , 'Yes' if p < 0.05 else 'No')

Summarising Stats for Multiple Columns

In EDA, you often need to check normality and skewness for all numeric columns at once. Combine Pandas select_dtypes with a loop over columns calling stats.shapiro() and stats.skew(). Build a summary DataFrame with columns for skewness, kurtosis, and Shapiro p-value to get a birds-eye view of which columns are non-normal and need transformation before modelling. This is part of a systematic data quality checklist.

import pandas as pd
import numpy as np
from scipy import stats

np.random.seed(0)
df = pd.DataFrame({
    'age': np.random.normal(35, 10, 200),
    'income': np.random.lognormal(10, 1, 200),
    'score': np.random.uniform(0, 100, 200)
})

rows = []
for col in df.select_dtypes(include='number').columns:
    s = stats.skew(df[col])
    _, p = stats.shapiro(df[col][:200])
    rows.append({'column': col, 'skewness': round(s, 3),
                 'shapiro_p': round(p, 4), 'normal': p > 0.05})

print(pd.DataFrame(rows).to_string(index=False))

Quick Check

Test your understanding of Data Analysis concepts from this lesson.

Lesson Recap

In this lesson you learned: scipy.stats.skew() and kurtosis() describe the shape of a distribution beyond what describe() provides, scipy.stats.shapiro() tests normality with p > 0.05 meaning no evidence against normality, and the Central Limit Theorem makes t-tests robust for large samples even with non-normal data. Next up we apply hypothesis testing with t-tests to compare group means.

Frequently asked questions

Is the “Descriptive Stats and Normality Testing” lesson free?

Yes — the full text of “Descriptive Stats and Normality Testing” is free to read here on the web, and the Pandas & NumPy Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Pandas & NumPy Academy course, upgrade to CoddyKit PRO.

What will I learn in “Descriptive Stats and Normality Testing”?

Compute skewness and kurtosis with scipy.stats, run a Shapiro-Wilk test for normality, and interpret the p-value. You practise Pandas & NumPy Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Pandas & NumPy Academy?

No prior experience is required. Pandas & NumPy Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Descriptive Stats and Normality Testing” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Pandas & NumPy Academy lesson?

Yes. Every Pandas & NumPy Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Descriptive Stats and Normality Testing
  2. T-Tests for Comparing Means
  3. Chi-Squared Test for Independence
  4. ANOVA and Post-Hoc Tests
← Back to Pandas & NumPy Academy