import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.io as pio
pio.renderers.default = 'notebook_connected'Introduction to Data Science
University of Redlands - DATA 101
Prof: Joanna Bieri joanna_bieri@redlands.edu
Class Website: data101.joannabieri.com
| # Day 4 - In Class Challenge - CDC Health Data - SOLUTIONS |
These are one set of answers, not the only set. Groups will pick different plot types and that is fine as long as the plot actually answers the question. The written answers below are what I would hope to hear when a group presents.
Numbers quoted below come from the 60 person sample. If you swap in a different sample they will change.
file_location = 'https://joannabieri.com/introdatascience/data/cdc.samp.csv'
DF = pd.read_csv(file_location)
display(DF)| genhlth | exerany | hlthplan | smoke100 | height | weight | wtdesire | age | gender | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | very good | 1 | 1 | 1 | 75 | 265 | 225 | 45 | m |
| 1 | excellent | 1 | 1 | 0 | 72 | 150 | 150 | 24 | m |
| 2 | excellent | 1 | 1 | 1 | 69 | 137 | 150 | 47 | m |
| 3 | good | 1 | 1 | 1 | 66 | 159 | 125 | 26 | f |
| 4 | very good | 1 | 1 | 0 | 63 | 145 | 125 | 33 | f |
| 5 | very good | 1 | 1 | 1 | 64 | 125 | 120 | 28 | f |
| 6 | excellent | 1 | 1 | 1 | 72 | 175 | 175 | 34 | m |
| 7 | very good | 1 | 1 | 1 | 60 | 104 | 104 | 42 | f |
| 8 | good | 1 | 1 | 1 | 65 | 140 | 140 | 50 | f |
| 9 | very good | 1 | 1 | 1 | 70 | 188 | 170 | 57 | m |
| 10 | very good | 1 | 1 | 0 | 67 | 170 | 155 | 57 | m |
| 11 | good | 0 | 0 | 1 | 65 | 120 | 130 | 19 | f |
| 12 | good | 1 | 1 | 0 | 73 | 290 | 220 | 42 | m |
| 13 | fair | 1 | 0 | 1 | 66 | 170 | 160 | 43 | m |
| 14 | good | 1 | 1 | 0 | 65 | 120 | 120 | 60 | f |
| 15 | very good | 0 | 1 | 0 | 64 | 140 | 110 | 34 | f |
| 16 | fair | 1 | 1 | 1 | 63 | 164 | 144 | 48 | f |
| 17 | good | 1 | 1 | 1 | 69 | 170 | 170 | 37 | m |
| 18 | good | 0 | 1 | 0 | 62 | 170 | 150 | 74 | f |
| 19 | good | 1 | 1 | 0 | 73 | 198 | 190 | 52 | m |
| 20 | fair | 1 | 1 | 0 | 62 | 130 | 130 | 61 | f |
| 21 | good | 0 | 1 | 1 | 66 | 300 | 150 | 68 | f |
| 22 | very good | 1 | 1 | 1 | 74 | 192 | 185 | 24 | m |
| 23 | good | 1 | 0 | 0 | 72 | 175 | 175 | 24 | m |
| 24 | good | 0 | 1 | 1 | 68 | 195 | 170 | 59 | f |
| 25 | excellent | 1 | 1 | 0 | 70 | 155 | 155 | 49 | m |
| 26 | good | 0 | 0 | 0 | 67 | 400 | 200 | 37 | f |
| 27 | excellent | 0 | 1 | 0 | 68 | 145 | 140 | 47 | m |
| 28 | very good | 1 | 1 | 1 | 62 | 145 | 120 | 56 | f |
| 29 | good | 0 | 1 | 1 | 63 | 165 | 140 | 74 | f |
| 30 | excellent | 1 | 1 | 1 | 68 | 140 | 140 | 28 | m |
| 31 | very good | 1 | 1 | 0 | 73 | 220 | 200 | 24 | m |
| 32 | excellent | 1 | 1 | 1 | 71 | 200 | 185 | 68 | m |
| 33 | excellent | 1 | 0 | 1 | 64 | 120 | 110 | 33 | f |
| 34 | very good | 1 | 1 | 1 | 65 | 180 | 140 | 43 | f |
| 35 | fair | 0 | 0 | 1 | 64 | 135 | 135 | 49 | f |
| 36 | excellent | 1 | 1 | 0 | 69 | 170 | 150 | 37 | f |
| 37 | excellent | 1 | 1 | 0 | 63 | 151 | 135 | 51 | f |
| 38 | good | 1 | 1 | 1 | 67 | 180 | 140 | 52 | f |
| 39 | very good | 1 | 1 | 1 | 71 | 195 | 180 | 50 | m |
| 40 | fair | 1 | 0 | 0 | 63 | 120 | 120 | 47 | f |
| 41 | good | 1 | 1 | 0 | 68 | 175 | 175 | 38 | m |
| 42 | excellent | 0 | 1 | 0 | 62 | 115 | 115 | 43 | f |
| 43 | very good | 1 | 1 | 1 | 66 | 160 | 140 | 35 | f |
| 44 | excellent | 1 | 1 | 1 | 72 | 200 | 188 | 50 | m |
| 45 | good | 1 | 0 | 1 | 70 | 154 | 154 | 31 | m |
| 46 | very good | 0 | 1 | 0 | 62 | 210 | 180 | 77 | f |
| 47 | very good | 0 | 1 | 0 | 62 | 135 | 135 | 69 | f |
| 48 | very good | 1 | 1 | 0 | 66 | 148 | 140 | 23 | f |
| 49 | excellent | 1 | 0 | 0 | 72 | 230 | 225 | 25 | m |
| 50 | fair | 1 | 1 | 1 | 60 | 192 | 147 | 48 | f |
| 51 | excellent | 1 | 1 | 1 | 74 | 200 | 200 | 51 | m |
| 52 | poor | 0 | 1 | 1 | 72 | 210 | 190 | 64 | m |
| 53 | excellent | 1 | 1 | 1 | 70 | 199 | 180 | 35 | m |
| 54 | excellent | 0 | 1 | 0 | 65 | 160 | 140 | 30 | m |
| 55 | good | 1 | 1 | 0 | 61 | 132 | 108 | 55 | f |
| 56 | very good | 1 | 1 | 0 | 72 | 165 | 160 | 19 | m |
| 57 | fair | 0 | 0 | 0 | 62 | 160 | 140 | 32 | f |
| 58 | excellent | 1 | 1 | 0 | 73 | 175 | 190 | 28 | m |
| 59 | very good | 1 | 1 | 0 | 69 | 165 | 165 | 40 | m |
| ## Warm Up |
Q1 Observations and variables.
print(DF.shape)
print('Observations (rows):', len(DF))
print('Variables (columns):', len(DF.columns))(60, 9)
Observations (rows): 60
Variables (columns): 9
60 observations (people) and 9 variables.
Q2 Numerical vs categorical.
Numerical: * height (inches), weight (pounds), wtdesire (pounds), age (years). Height, weight and age are really continuous quantities, but they were recorded as whole numbers so they look discrete in the data. Either answer is fine as long as the group can say why.
Categorical: * genhlth is ordinal: poor, fair, good, very good, excellent has a natural order. * gender is nominal. * exerany, hlthplan, smoke100 are the tricky ones. They are stored as 0 and 1 but they are yes/no questions, so they are categorical (nominal). The 1 does not mean “one of something”. If you average them you get a proportion, which is useful, but you should not treat them like weight.
A quick way to see this is that describe() happily computes a mean of smoke100 (about 0.52) which just means 52% of the sample has smoked 100 cigarettes.
DF.describe()| exerany | hlthplan | smoke100 | height | weight | wtdesire | age | |
|---|---|---|---|---|---|---|---|
| count | 60.000000 | 60.000000 | 60.000000 | 60.000000 | 60.000000 | 60.000000 | 60.000000 |
| mean | 0.750000 | 0.833333 | 0.516667 | 67.183333 | 173.300000 | 155.083333 | 43.766667 |
| std | 0.436667 | 0.375823 | 0.503939 | 4.151747 | 49.034854 | 29.896597 | 14.813378 |
| min | 0.000000 | 0.000000 | 0.000000 | 60.000000 | 104.000000 | 104.000000 | 19.000000 |
| 25% | 0.750000 | 1.000000 | 0.000000 | 63.750000 | 143.750000 | 135.000000 | 32.750000 |
| 50% | 1.000000 | 1.000000 | 1.000000 | 67.000000 | 165.000000 | 150.000000 | 43.000000 |
| 75% | 1.000000 | 1.000000 | 1.000000 | 71.000000 | 192.750000 | 176.250000 | 52.000000 |
| max | 1.000000 | 1.000000 | 1.000000 | 75.000000 | 400.000000 | 225.000000 | 77.000000 |
Q3 Counts of genhlth, in order.
counts = DF['genhlth'].value_counts()
display(counts)
health_order = ['poor', 'fair', 'good', 'very good', 'excellent']
fig = px.bar(DF,
x='genhlth',
category_orders={'genhlth': health_order},
color_discrete_sequence=['lightseagreen'])
fig.update_traces(dict(marker_line_width=0))
fig.update_layout(title='How people rate their own health',
title_x=0.5,
xaxis_title='Self rated general health',
yaxis_title='Number of people')
fig.show()genhlth
very good 18
excellent 17
good 17
fair 7
poor 1
Name: count, dtype: int64
Very good 18, excellent 17, good 17, fair 7, poor 1. Once the bars are in order the shape is lopsided toward the good end. Almost everyone says they are at least “good”, only 8 of 60 say fair or poor. People tend to rate their own health well (or people in poor health are less likely to answer a phone survey, which is worth mentioning).
| ## Challenge 1 - How much weight do people want to lose? |
Q4 Scatter plot of weight vs desired weight.
fig = px.scatter(DF,
x='weight',
y='wtdesire',
color='gender',
hover_data='age',
title='Desired weight vs current weight')
fig.update_traces(marker=dict(size=10))
fig.update_layout(title_x=0.5,
xaxis_title='Current weight (pounds)',
yaxis_title='Desired weight (pounds)')
fig.show()Q5 A point on the line wtdesire = weight is a person who is happy with their weight. A point above the line wants to gain weight. A point below the line wants to lose weight. Almost everyone is below the line, most people want to lose weight.
Q6 Find the people who want to gain weight with a filter.
gainers = DF[DF['wtdesire'] > DF['weight']]
display(gainers)
print('Number who want to gain weight:', len(gainers))
print(gainers['gender'].value_counts())| genhlth | exerany | hlthplan | smoke100 | height | weight | wtdesire | age | gender | |
|---|---|---|---|---|---|---|---|---|---|
| 2 | excellent | 1 | 1 | 1 | 69 | 137 | 150 | 47 | m |
| 11 | good | 0 | 0 | 1 | 65 | 120 | 130 | 19 | f |
| 58 | excellent | 1 | 1 | 0 | 73 | 175 | 190 | 28 | m |
Number who want to gain weight: 3
gender
m 2
f 1
Name: count, dtype: int64
Only 3 people out of 60 want to gain weight, 2 men and 1 woman. All three only want to add 10 to 15 pounds, and none of them is heavy to start with.
Q7 The outlier and the mean.
# The point way off by itself
display(DF[DF['weight'] > 300])
mean_all = DF['weight'].mean()
mean_no_outlier = DF[DF['weight'] < 300]['weight'].mean()
median_all = DF['weight'].median()
print(f'Mean weight, everyone: {mean_all:.1f}')
print(f'Mean weight, without outlier: {mean_no_outlier:.1f}')
print(f'Median weight, everyone: {median_all:.1f}')| genhlth | exerany | hlthplan | smoke100 | height | weight | wtdesire | age | gender | |
|---|---|---|---|---|---|---|---|---|---|
| 26 | good | 0 | 0 | 0 | 67 | 400 | 200 | 37 | f |
Mean weight, everyone: 173.3
Mean weight, without outlier: 167.2
Median weight, everyone: 165.0
The outlier is a 37 year old woman, 67 inches tall, 400 pounds, who wants to weigh 200. One person out of 60 pulls the mean from about 167 up to about 173 pounds. The median (165) barely notices her. For a “typical” person the median is the better description here, because the mean is being dragged around by a single observation.
Q8 New column and compare by gender.
DF['want_to_lose'] = DF['weight'] - DF['wtdesire']
fig = px.box(DF,
x='want_to_lose',
y='gender',
color='gender',
title='Pounds people want to lose, by gender')
fig.update_layout(title_x=0.5,
xaxis_title='Current weight minus desired weight (pounds)',
yaxis_title='Gender')
fig.show()print('Women')
display(DF[DF['gender'] == 'f']['want_to_lose'].describe())
print('Men')
display(DF[DF['gender'] == 'm']['want_to_lose'].describe())Women
Men
count 31.000000
mean 26.354839
std 42.640003
min -10.000000
25% 0.000000
50% 20.000000
75% 27.500000
max 200.000000
Name: want_to_lose, dtype: float64
count 29.000000
mean 9.517241
std 16.101510
min -15.000000
25% 0.000000
50% 5.000000
75% 15.000000
max 70.000000
Name: want_to_lose, dtype: float64
Women in this sample want to lose more: median 20 pounds vs 5 pounds for men, mean about 26 vs about 10. The women’s mean is pulled up by the 400 pound outlier (she alone wants to lose 200), so the medians are the fairer comparison. Even so, the difference between the groups (about 15 pounds at the median) is smaller than the spread inside each group (the women’s box runs from 0 to about 28), so there is a lot of overlap.
| ## Challenge 2 - Does how people feel match the numbers? |
Q9 BMI column.
DF['bmi'] = 703 * DF['weight'] / DF['height']**2
display(DF[['gender', 'height', 'weight', 'bmi', 'genhlth']])
DF['bmi'].describe()| gender | height | weight | bmi | genhlth | |
|---|---|---|---|---|---|
| 0 | m | 75 | 265 | 33.119111 | very good |
| 1 | m | 72 | 150 | 20.341435 | excellent |
| 2 | m | 69 | 137 | 20.229154 | excellent |
| 3 | f | 66 | 159 | 25.660468 | good |
| 4 | f | 63 | 145 | 25.682792 | very good |
| 5 | f | 64 | 125 | 21.453857 | very good |
| 6 | m | 72 | 175 | 23.731674 | excellent |
| 7 | f | 60 | 104 | 20.308889 | very good |
| 8 | f | 65 | 140 | 23.294675 | good |
| 9 | m | 70 | 188 | 26.972245 | very good |
| 10 | m | 67 | 170 | 26.622856 | very good |
| 11 | f | 65 | 120 | 19.966864 | good |
| 12 | m | 73 | 290 | 38.256709 | good |
| 13 | m | 66 | 170 | 27.435721 | fair |
| 14 | f | 65 | 120 | 19.966864 | good |
| 15 | f | 64 | 140 | 24.028320 | very good |
| 16 | f | 63 | 164 | 29.048123 | fair |
| 17 | m | 69 | 170 | 25.101869 | good |
| 18 | f | 62 | 170 | 31.090010 | good |
| 19 | m | 73 | 198 | 26.120098 | good |
| 20 | f | 62 | 130 | 23.774714 | fair |
| 21 | f | 66 | 300 | 48.415978 | good |
| 22 | m | 74 | 192 | 24.648649 | very good |
| 23 | m | 72 | 175 | 23.731674 | good |
| 24 | f | 68 | 195 | 29.646410 | good |
| 25 | m | 70 | 155 | 22.237755 | excellent |
| 26 | f | 67 | 400 | 62.642014 | good |
| 27 | m | 68 | 145 | 22.044766 | excellent |
| 28 | f | 62 | 145 | 26.517950 | very good |
| 29 | f | 63 | 165 | 29.225246 | good |
| 30 | m | 68 | 140 | 21.284602 | excellent |
| 31 | m | 73 | 220 | 29.022331 | very good |
| 32 | m | 71 | 200 | 27.891291 | excellent |
| 33 | f | 64 | 120 | 20.595703 | excellent |
| 34 | f | 65 | 180 | 29.950296 | very good |
| 35 | f | 64 | 135 | 23.170166 | fair |
| 36 | f | 69 | 170 | 25.101869 | excellent |
| 37 | f | 63 | 151 | 26.745528 | excellent |
| 38 | f | 67 | 180 | 28.188906 | good |
| 39 | m | 71 | 195 | 27.194009 | very good |
| 40 | f | 63 | 120 | 21.254724 | fair |
| 41 | m | 68 | 175 | 26.605753 | good |
| 42 | f | 62 | 115 | 21.031478 | excellent |
| 43 | f | 66 | 160 | 25.821855 | very good |
| 44 | m | 72 | 200 | 27.121914 | excellent |
| 45 | m | 70 | 154 | 22.094286 | good |
| 46 | f | 62 | 210 | 38.405307 | very good |
| 47 | f | 62 | 135 | 24.689126 | very good |
| 48 | f | 66 | 148 | 23.885216 | very good |
| 49 | m | 72 | 230 | 31.190201 | excellent |
| 50 | f | 60 | 192 | 37.493333 | fair |
| 51 | m | 74 | 200 | 25.675676 | excellent |
| 52 | m | 72 | 210 | 28.478009 | poor |
| 53 | m | 70 | 199 | 28.550408 | excellent |
| 54 | m | 65 | 160 | 26.622485 | excellent |
| 55 | f | 61 | 132 | 24.938457 | good |
| 56 | m | 72 | 165 | 22.375579 | very good |
| 57 | f | 62 | 160 | 29.261186 | fair |
| 58 | m | 73 | 175 | 23.085945 | excellent |
| 59 | m | 69 | 165 | 24.363579 | very good |
count 60.000000
mean 26.890102
std 6.936902
min 19.966864
25% 23.149111
50% 25.679234
75% 28.496109
max 62.642014
Name: bmi, dtype: float64
Q10 BMI by self rated health, in order.
fig = px.box(DF,
x='genhlth',
y='bmi',
color='genhlth',
category_orders={'genhlth': health_order},
title='BMI by self rated health')
fig.update_layout(title_x=0.5,
xaxis_title='Self rated general health',
yaxis_title='BMI',
showlegend=False)
fig.show()
# Check the medians with a table
DF.groupby('genhlth')['bmi'].median().reindex(health_order)genhlth
poor 28.478009
fair 27.435721
good 26.120098
very good 25.752323
excellent 23.731674
Name: bmi, dtype: float64
Yes, mostly. The median BMI goes down steadily as the rating goes up: poor about 28.5, fair about 27.4, good about 26.1, very good about 25.8, excellent about 23.7. Where it does not line up: the “poor” group is a single person so that box is not really a box, and “very good” contains the 400 pound outlier with a BMI over 60, which shows a high BMI does not stop someone rating their own health well. The boxes overlap a lot, so BMI does not determine how people feel, it just trends with it.
Q11 BMI by exercise.
# First try - the legend says 0 and 1
fig = px.histogram(DF, x='bmi', color='exerany', nbins=15, opacity=0.7)
fig.show()DF['exercise'] = DF['exerany'].replace({0: 'no', 1: 'yes'})
fig = px.histogram(DF,
x='bmi',
color='exercise',
nbins=15,
opacity=0.7,
barmode='overlay',
title='BMI for people who did and did not exercise last month')
fig.update_layout(title_x=0.5,
xaxis_title='BMI',
yaxis_title='Number of people',
legend_title='Exercised last month')
fig.show()
DF.groupby('exercise')['bmi'].median()exercise
no 28.478009
yes 25.660468
Name: bmi, dtype: float64
The annoying part is that the legend reads 0 and 1 and the reader has to go look up which is which. After the replace the legend says yes and no.
The exercisers sit a little lower, median BMI about 25.7 vs about 28.5 for the non exercisers, but there are only 15 non exercisers so their histogram is very bumpy. The two groups overlap a lot. barmode='overlay' is optional, the default stacked histogram also works but is harder to compare.
| ## Challenge 3 - Smoking and health, and being a skeptic |
Q12 Bar plot of health rating colored by smoking.
DF['smoker'] = DF['smoke100'].replace({0: 'no', 1: 'yes'})
fig = px.bar(DF,
x='genhlth',
color='smoker',
category_orders={'genhlth': health_order},
barmode='group',
title='Self rated health for smokers and non smokers')
fig.update_traces(dict(marker_line_width=0))
fig.update_layout(title_x=0.5,
xaxis_title='Self rated general health',
yaxis_title='Number of people',
legend_title='Smoked 100+ cigarettes')
fig.show()It is hard to say from the bars. The only “poor” is a smoker and the smokers have one fewer “excellent”, so a group might say smokers rate their health a little worse. The stacked version (the default) is even harder to read.
Q13 Compare within each group.
print('Smokers')
display(DF[DF['smoker'] == 'yes']['genhlth'].value_counts())
print('Non smokers')
display(DF[DF['smoker'] == 'no']['genhlth'].value_counts())Smokers
Non smokers
genhlth
very good 9
good 9
excellent 8
fair 4
poor 1
Name: count, dtype: int64
genhlth
excellent 9
very good 9
good 8
fair 3
Name: count, dtype: int64
# Optional - as a fraction of each group, which is the fair comparison
DF.groupby('smoker')['genhlth'].value_counts(normalize=True).unstack().reindex(columns=health_order).round(2)| genhlth | poor | fair | good | very good | excellent |
|---|---|---|---|---|---|
| smoker | |||||
| no | NaN | 0.10 | 0.28 | 0.31 | 0.31 |
| yes | 0.03 | 0.13 | 0.29 | 0.29 | 0.26 |
There are 31 smokers and 29 non smokers, so the groups are close in size here and the bar plot was not lying much. As fractions: about 26% of smokers say excellent vs 31% of non smokers, and 13% vs 10% say fair. The one “poor” is a smoker. So smokers rate their health slightly worse, but the difference is one or two people in each category. A group that said “no real difference” is right, and a group that said “smokers a little worse” is also right, as long as they say how small it is.
The habit to take away: whenever you color a bar plot by a group, check whether the groups are the same size before you compare the heights.
Q14 Being a skeptic. Any two of these (or others) are good:
- Tiny sample. 60 people, and the difference is one or two people per category. If we picked a different 60 people the answer could easily flip.
- Self reported. Both smoking and health are what people said on the phone, not measured. People tend to say they weigh a little less and feel a little better than they really do.
- Other differences between the groups. Smokers in this sample might be older, exercise less, or have different jobs. Any of those could explain a health difference without smoking causing it. (We will call these confounding variables later in the semester.)
- Who answers the phone. People in poor health are less likely to be surveyed at all, which is one reason “poor” has only one person.
- Direction. Even if there is a link, this data cannot say smoking caused the lower rating. That takes a very different kind of study.
| ## Challenge (optional) - Age |
All three questions have a real signal. Here is one plot for each.
fig = px.box(DF,
x='age',
y='genhlth',
color='genhlth',
category_orders={'genhlth': health_order},
title='Age by self rated health')
fig.update_layout(title_x=0.5, xaxis_title='Age (years)', yaxis_title='Self rated health', showlegend=False)
fig.show()
DF.groupby('genhlth')['age'].median().reindex(health_order)genhlth
poor 64.0
fair 48.0
good 50.0
very good 41.0
excellent 37.0
Name: age, dtype: float64
Older people rate their health lower. Median age is about 37 for excellent, 41 very good, 50 good, 48 fair, and the one poor is 64.
fig = px.scatter(DF,
x='age',
y='want_to_lose',
color='gender',
title='Pounds people want to lose vs age')
fig.update_layout(title_x=0.5, xaxis_title='Age (years)', yaxis_title='Pounds to lose')
fig.show()No clear trend with age once you ignore the 400 pound outlier. The gender difference from Q8 is a lot easier to see than any age pattern.
DF['health_plan'] = DF['hlthplan'].replace({0: 'no', 1: 'yes'})
fig = px.box(DF,
x='age',
y='health_plan',
color='health_plan',
title='Age of people with and without a health plan')
fig.update_layout(title_x=0.5, xaxis_title='Age (years)', yaxis_title='Has a health plan', showlegend=False)
fig.show()
DF.groupby('health_plan')['age'].median()health_plan
no 32.5
yes 47.0
Name: age, dtype: float64
The 10 people without a health plan are younger, median age about 32 vs about 47 for people with one. Only 10 people though!
| ## FREE FORM |
No solution here, this is whatever the group came up with. When they present, listen for: a real title and labeled axes with units, one sentence saying what the plot shows, and one honest reason they might be wrong (sample size and self reporting are the easy ones).