-
Notifications
You must be signed in to change notification settings - Fork 1
/
eda_py.qmd
580 lines (388 loc) · 19 KB
/
eda_py.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
# Exploratory Data Analysis {#sec-eda}
{{< include _links.qmd >}}
```{python}
#| label: setup
#| cache: false
#| output: false
#| code-fold: true
#| code-summary: 'Setup Code (Click to Expand)'
# packages needed to run the code in this section
# !pip install pandas numpy matplotlib seaborn skimpy
# import packages
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib import rc
from skimpy import skim
# data path
path = '../data/'
file_name = 'heart_disease.csv'
# import data
df = pd.read_csv(f"{path}{file_name}")
# get categorical columns
cat_cols = ['sex', 'fasting_bs', 'resting_ecg', 'angina', 'heart_disease']
for col in cat_cols:
df[col] = df[col].astype('category')
# set plot style
sns.set_style('whitegrid')
# set plot font
rc('font',**{'family':'sans-serif','sans-serif':['Arial']})
# set plot colour palette
colours = ['#1C355E', '#00A499', '#005EB8']
sns.set_palette(sns.color_palette(colours))
```
Exploratory data analysis (EDA) is the process of inspecting, visualising, and summarising a dataset. It is the first step in any data science project, and the importance of EDA can often be overlooked. Without exploring the data, it is difficult to know how to construct a piece of analysis or a model, and it is difficult to know if the data is suitable for the task at hand. As a critical step in the data science workflow, it is important to spend time on EDA and to be thorough and methodical in the process. While EDA is often the most time-consuming step in an analysis, taking the time to explore the data can save time in the long run.
EDA is an iterative process. In this tutorial, we will use the `pandas` and `seaborn` packages to explore a dataset containing information about heart disease. We will start by inspecting the data itself, to get a sense of the structure and the components of the dataset, and to identify any data quality issues (such as missing values). We will then compute summary statistics to get a better understanding of the distribution and central tendency of the variables that are relevant to the analysis. Finally, we will use data visualisations to explore specific variables in more detail, and to identify any interesting relationships between variables.
## Inspecting the Data
The first step when doing EDA is to inspect the data itself and get an idea of the structure of the dataset, the variable types, and the typical values of each variable. This gives a better understanding of exactly what data is being used and informs decisions both about the next steps in the exploratory process and any modelling choices.
We can use the `head()` and `info()` functions to get a sense of the structure of the data. The `head()` function returns the first five rows of the data, and the `info()` method returns a summary of the data, including the number of rows, the number of columns, the column names, and the data type of each column. The `info()` method is particularly useful for identifying missing values, as it returns the number of non-null values in each column. If the number of non-null values is less than the number of rows, then there are missing values in the column.
In addition to these two methods, we can also use the `nunique()` method to count the number of unique values in each column, which helps identify categorical variables, and we can use the `unique()` method to get a list of the unique values in a column.
```{python}
#| label: df-head
df.head()
```
```{python}
#| label: df-info
df.info()
```
```{python}
#| label: count-unique
# count unique values in each column
df.nunique()
```
```{python}
#| label: heart-disease-unique
# unique values of the outcome variable
df.heart_disease.unique()
```
```{python}
#| label: cholesterol-unique
# unique values of a continuous explanatory variable
df.cholesterol.unique()
```
## Summary Statistics
Summary statistics are a quick and easy way to get a sense of the distribution and central tendency of the variables in the dataset. We can use the `describe()` method to get a quick overview of every column in the dataset, including the row count, the mean and standard deviation, the minimum and maximum value, and quartiles of each variable.
```{python}
#| label: df-describe
# summary of the data
df.describe()
```
While the `describe()` function is pretty effective, the `skimpy` package can provide a more detailed summary of the data, using the `skim()` function. If you are looking for a single function to capture the entire process of inspecting the data and computing summary statistics, `skim()` is the function for the job, giving you a wealth of information about the dataset as a whole and each variable in the data.
Another package that provides a similar function is the [profiling][`ydata-profiling`] package, which can be used to generate a report containing a summary of the data, including the data types, missing values, and summary statistics. The `ydata-profiling` package is particularly useful for generating a report that can be shared with others, as it can be exported as an HTML file, however it's a bit more resource-intensive than `skimpy`, so we will stick with `skimpy` for this tutorial.
```{python}
#| label: df-skim
# more detailed summary of the data
skim(df)
```
If we want to examine a particular variable, the functions `mean()`, `median()`, `quantile()`, `min()`, and `max()` will return the same information as the `describe()` function. We can also get a sense of dispersion by computing the standard deviation or variance of a variable. The `std()` function returns the standard deviation of a variable, and the `var()` function returns the variance.
```{python}
#| label: avg-age
# mean & median age
df.age.mean(), df.age.median()
```
```{python}
#| label: min-max-age
# min and max age
df.age.min(), df.age.max()
```
```{python}
#| label: dispersion-age
# dispersion of age
df.age.std(), df.age.var()
```
Finally, we can use the `value_counts()` function to get a count of the number of observations in each category of a discrete variable.
```{python}
#| label: heart-disease-count
# heart disease count
df['heart_disease'].value_counts()
```
```{python}
#| label: resting-ecg-count
# resting ecg count
df['resting_ecg'].value_counts()
```
```{python}
#| label: angina-count
# angina
df['angina'].value_counts()
```
```{python}
#| label: cholesterol-count
# cholesterol
df['cholesterol'].value_counts()
```
We can also use the `groupby()` method to get the counts of each category in a categorical variable, grouped by another categorical variable.
```{python}
#| label: heart-disease-by-resting-ecg-count
df.groupby(['resting_ecg'])['heart_disease'].value_counts()
```
In addition to the counts, we can also get the proportions of each category using the `normalize=True` argument.
```{python}
#| label: heart-disease-by-resting-ecg-freq
df.groupby(['resting_ecg'])['heart_disease'].value_counts(normalize=True).round(3)
```
## Data Visualisation
While inspecting the data directly and using summary statistics to describe it is a good first step, data visualisation is a more effective way to explore the data. It allows us to quickly identify patterns and relationships in the data, and to identify any data quality issues that might not be immediately obvious without a visual representation of the data.
When using data visualisation for exploratory purposes, the intent is generally to visualise the way data is distributed, both within and between variables. This can be done using a variety of different types of plots, including histograms, bar charts, box plots, scatter plots, and line plots. How variables are distributed can tell us a lot about the variable itself, and how variables are distributed relative to each other can tell us a lot about the potential relationship between the variables.
In this tutorial, we will use the `matplotlib` and `seaborn` packages to create a series of data visualisations to explore the data in more detail. The `seaborn` package is a high-level data visualisation library that is built on top of `matplotlib`. Although data visualisation in Python is not as straightforward as it is in R, `seaborn` makes it much easier to create good quality and informative plots.
### Visualising Data Distributions
The first step in the exploratory process is to visualise the data distributions of key variables in the dataset. This allows us to get a sense of the typical values and central tendency of the variable, as well as identifying any outliers or other data quality issues.
#### Continuous Distributions
For continuous variables, we can use histograms to visualise the distribution of the data. We can use the `histplot()` function to create a histogram of a continuous variable. The `binwidth` argument allows us to specify the width of the bins in the histogram.
```{python}
#| label: age-dist
#| fig-align: center
# age distribution
sns.histplot(data=df, x='age', binwidth=5)
sns.despine()
plt.show()
```
```{python}
#| label: max-hr-dist
#| fig-align: center
# max hr distribution
sns.histplot(data=df, x='max_hr', binwidth=10)
sns.despine()
plt.show()
```
```{python}
#| label: cholesterol-dist
#| fig-align: center
# cholesterol distribution
sns.histplot(data=df, x='cholesterol', binwidth=25)
sns.despine()
plt.show()
```
```{python}
#| label: filter-zeroes-cholesterol-dist
#| fig-align: center
# cholesterol distribution
sns.histplot(data=df.loc[df.cholesterol!=0], x='cholesterol', binwidth=25)
sns.despine()
plt.show()
```
The inflated zero values in the cholesterol distribution suggests that there may be an issue with data quality that needs addressing.
#### Discrete Distributions
We can use bar plots to visualise the distribution of discrete variables. We can use the `countplot()` function to create a bar plot of a discrete variable.
```{python}
#| label: heart-disease-dist
#| fig-align: center
# heart disease distribution
sns.countplot(data=df, x='heart_disease')
sns.despine()
plt.show()
```
```{python}
#| label: sex-dist
#| fig-align: center
# sex distribution
sns.countplot(data=df, x='sex')
sns.despine()
plt.show()
```
```{python}
#| label: angina-dist
#| fig-align: center
# angina distribution
sns.countplot(data=df, x='angina')
sns.despine()
plt.show()
```
### Comparing Distributions
There are a number of ways to compare the distributions of multiple variables. Bar plots can be used to visualise two discrete variables, while histograms and box plots are useful for comparing the distribution of a continuous variable across the groups of a discrete variable, and scatter plots are particularly useful for comparing the distribution of two continuous variables.
#### Visualising Multiple Discrete Variables
Bar plots are an effective way to visualize the observed relationship (or association, at least) between a discrete explanatory variable and a discrete outcome (whether binary, ordinal, or categorical). We can use the `countplot()` function to create bar plots, and the `hue` argument to split the bars by a particular variable and display them in different colours.
```{python}
#| label: heart-disease-by-sex
#| fig-align: center
# heart disease by sex
sns.countplot(data=df, x='heart_disease', hue='sex')
sns.despine()
plt.show()
```
```{python}
#| label: heart-disease-by-resting-ecg
#| fig-align: center
# heart disease by resting ecg
sns.countplot(data=df, x='heart_disease', hue='resting_ecg')
sns.despine()
plt.show()
```
```{python}
#| label: heart-disease-by-angina
#| fig-align: center
# angina
sns.countplot(data=df, x='heart_disease', hue='angina')
sns.despine()
plt.show()
```
```{python}
#| label: heart-disease-by-fasting-bs
#| fig-align: center
# fasting bs
sns.countplot(data=df, x='heart_disease', hue='fasting_bs')
sns.despine()
plt.show()
```
#### Visualising A Continuous Variable Across Discrete Groups
Histograms and box plots are useful for comparing the distribution of a continuous variable across the groups of a discrete variable.
##### Histogram Plots
We can use the `histplot()` function to create a histogram of a continuous variable. The `hue` argument allows us to split the histogram by a particular variable and display them in different colours, while the `multiple` argument allows us to specify how the histograms should be displayed. The `multiple` argument can be set to `stack` to stack the histograms on top of each other, or `dodge` to display the histograms side-by-side.
```{python}
#| label: age-by-heart-disease
#| fig-align: center
# age distribution by heart disease
sns.histplot(data=df, x='age', hue='heart_disease', binwidth=5, multiple='dodge')
sns.despine()
plt.show()
```
```{python}
#| label: cholesterol-by-heart-disease
#| fig-align: center
# cholesterol
sns.histplot(data=df, x='cholesterol', hue='heart_disease', binwidth=25, multiple='dodge')
sns.despine()
plt.show()
```
```{python}
#| label: filtered-zeroes-cholesterol-by-heart-disease
#| fig-align: center
# filter zero values
sns.histplot(
data=df.loc[df.cholesterol!=0],
x='cholesterol',
hue='heart_disease',
binwidth=25,
multiple='dodge')
sns.despine()
plt.show()
```
The fact that there is a significantly larger proportion of positive heart disease cases in the zero cholesterol values further demonstrates the need to address this data quality issue.
##### Box Plots
Box plots visualize the characteristics of a continuous distribution over discrete groups. We can use the `boxplot()` function to create box plots, and the `hue` argument to split the box plots by a particular variable and display them in different colours.
However, while box plots can be very useful, they are not always the most effective way of visualising this information, as explained [boxplots][here] by Cedric Scherer. This guide uses box plots for the sake of simplicity, but it is worth considering other options when visualising distributions.
```{python}
#| label: age-by-heart-disease-box
#| fig-align: center
# age & heart disease
sns.boxplot(data=df, x='heart_disease', y='age')
sns.despine()
plt.show()
```
```{python}
#| label: age-box-split-by-sex
#| fig-align: center
# age & heart disease, split by sex
# fig, ax = plt.subplots(figsize=(10,6))
sns.boxplot(data=df, x='heart_disease', y='age', hue='sex')
sns.despine()
plt.show()
```
```{python}
#| label: max-hr-by-heart-disease-box
#| fig-align: center
# max hr & heart disease
sns.boxplot(data=df, x='heart_disease', y='max_hr')
sns.despine()
plt.show()
```
```{python}
#| label: max-hr-box-split-by-sex
#| fig-align: center
# max hr & heart disease, split by sex
# fig, ax = plt.subplots(figsize=(10,6))
sns.boxplot(data=df, x='heart_disease', y='max_hr', hue='sex')
sns.despine()
plt.show()
```
#### Visualising Multiple Discrete Variables
Scatter plots are an effective way to visualize how two continuous variables vary together.
We can use the `scatterplot()` function to create scatter plots, and the `hue` argument to split the scatter plots by a particular variable and display them in different colours.
```{python}
#| label: age-resting-bp-scatter
#| fig-align: center
# age & resting bp
sns.scatterplot(data=df, x='age', y='resting_bp')
sns.despine()
plt.show()
```
```{python}
#| label: filtered-zeroes-resting-bp-scatter
#| fig-align: center
# age & resting bp
sns.scatterplot(data=df.loc[df.resting_bp!=0], x='age', y='resting_bp')
sns.despine()
plt.show()
```
```{python}
#| label: age-cholesterol-scatter
#| fig-align: center
sns.scatterplot(data=df.loc[df.cholesterol!=0], x='age', y='cholesterol')
sns.despine()
plt.show()
```
```{python}
#| label: age-max-hr-scatter
#| fig-align: center
sns.scatterplot(data=df, x='age', y='max_hr')
sns.despine()
plt.show()
```
The scatter plot visualising age and resting blood pressure highlights another observation that needs to be removed due to data quality issues.
If there appears to be an association between the two continuous variables that you have plotted, as is the case with age and maximum heart rate in the above plot, you can also add a regression line to visualize the strength of that association. The `regplot()` function can be used to add a regression line to a scatter plot. The `ci` argument specifies whether or not to display the confidence interval of the regression line.
```{python}
#| label: max-hr-by-age-regression-plot
#| fig-align: center
# age & max hr
sns.regplot(data=df, x='age', y='max_hr', ci=None)
sns.despine()
plt.show()
```
You can also include discrete variables by assigning the discrete groups different colours in the scatter plot, and if you add regression lines to these plots, separate regression lines will be fit to the discrete groups. This can be useful for visualising how the association between the two continuous variables varies across the discrete groups.
The `lmplot()` function can be used to create scatter plots with regression lines, and the `hue` argument can be used to split the scatter plots by a particular variable and display them in different colours.
```{python}
#| label: resting-bp-heart-disease-scatter
#| fig-align: center
# age & resting bp, split by heart disease
sns.scatterplot(data=df.loc[df.resting_bp!=0], x='age', y='resting_bp', hue='heart_disease')
sns.despine()
plt.show()
```
```{python}
#| label: cholesterol-regression-plot
#| fig-align: center
# age & cholesterol, split by heart disease (with regression line)
sns.lmplot(
data=df.loc[df.cholesterol!=0],
x='age', y='cholesterol',
hue='heart_disease',
ci=None,
height = 7,
aspect=1.3)
plt.show()
```
```{python}
#| label: max-hr-regression-plot
#| fig-align: center
# age & max hr, split by heart disease (with regression line)
sns.lmplot(
data=df,
x='age', y='max_hr',
hue='heart_disease',
ci=None,
height = 7,
aspect=1.3)
plt.show()
```
## Next Steps
There are many more visualisation techniques that you can use to explore your data. You can find plenty of inspiration for different approaches to visualising data in the [seaborn] and [matplotlib] documentation. There are also a number of other Python libraries that can be used to create visualisations, including [plotly], [bokeh], and [altair].
The next step in the data science process is to build a model to either explain or predict the outcome variable, heart disease. The exploratory work done here can help inform decisions about the choice of the model, and the choice of the variables that will be used to build the model. It will also help clean up the data, particularly the zero values in the cholesterol and resting blood pressure variables, to ensure that the model is built on the best possible data.
## Resources
There are a wealth of resources available to help you learn more about data visualisation, and while the resources for producing visualisations in R are more extensive, there are still a number of good resources for producing visualisations in Python.
- Scientific Visualization: Python + Matplotlib [[PDF][Scientific Viz PDF]][[GitHub][Scientific Viz GitHub]]
- [Seaborn Tutorials]
- [Matplotlib Cheatsheets]
While the following resources are R-based, they are still useful for learning about data visualisation principles:
- [Data Visualization: A Practical Introduction]
- [Fundamentals of Data Visualization]