Exploratory Data Analysis

Exploratory Data Analysis

In [1]:
import numpy as np
import pandas as pd
from scipy.constants import pound as POUND
In [2]:
from statadict import parse_stata_dict
In [3]:
stata_dict = parse_stata_dict('./data/2002FemPreg.dct')
resp = pd.read_fwf('./data/2002FemPreg.dat', names=stata_dict.names, colspecs=stata_dict.colspecs)
resp
Out[3]:
caseid pregordr howpreg_n howpreg_p moscurrp nowprgdk pregend1 pregend2 nbrnaliv multbrth ... poverty_i laborfor_i religion_i metro_i basewgt adj_mod_basewgt finalwgt secu_p sest cmintvw
0 1 1 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 0 0 3410.389399 3869.349602 6448.271112 2 9 1231
1 1 2 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 0 0 3410.389399 3869.349602 6448.271112 2 9 1231
2 2 1 NaN NaN NaN NaN 5.0 NaN 3.0 5.0 ... 0 0 0 0 7226.301740 8567.549110 12999.542264 2 12 1231
3 2 2 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 0 0 7226.301740 8567.549110 12999.542264 2 12 1231
4 2 3 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 0 0 7226.301740 8567.549110 12999.542264 2 12 1231
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
13588 12571 1 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 0 0 4670.540953 5795.692880 6269.200989 1 78 1227
13589 12571 2 NaN NaN NaN NaN 3.0 NaN NaN NaN ... 0 0 0 0 4670.540953 5795.692880 6269.200989 1 78 1227
13590 12571 3 NaN NaN NaN NaN 3.0 NaN NaN NaN ... 0 0 0 0 4670.540953 5795.692880 6269.200989 1 78 1227
13591 12571 4 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 0 0 4670.540953 5795.692880 6269.200989 1 78 1227
13592 12571 5 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 0 0 4670.540953 5795.692880 6269.200989 1 78 1227

13593 rows × 243 columns

I just want to check if data is corrupted since a lot of rows have NaN for howpreg_n

In [4]:
resp[resp["howpreg_n"].notna()]
Out[4]:
caseid pregordr howpreg_n howpreg_p moscurrp nowprgdk pregend1 pregend2 nbrnaliv multbrth ... poverty_i laborfor_i religion_i metro_i basewgt adj_mod_basewgt finalwgt secu_p sest cmintvw
202 225 1 4.0 1.0 1.0 NaN NaN NaN NaN NaN ... 0 0 0 0 2148.748487 2689.603580 3580.669246 1 44 1235
283 285 2 9.0 1.0 2.0 NaN NaN NaN NaN NaN ... 0 0 0 0 6562.704646 8648.584060 14546.053530 2 65 1236
295 292 4 8.0 1.0 2.0 NaN NaN NaN NaN NaN ... 0 0 0 0 2817.008202 3357.492213 4365.075626 1 54 1234
307 302 2 22.0 1.0 5.0 NaN NaN NaN NaN NaN ... 0 0 0 0 4870.737797 5152.472057 8939.539019 1 74 1228
453 427 2 29.0 1.0 7.0 NaN NaN NaN NaN NaN ... 0 0 0 0 4596.926139 5285.561481 7435.199336 1 30 1228
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
13425 12407 2 27.0 1.0 6.0 NaN NaN NaN NaN NaN ... 0 0 0 0 1833.295397 2078.242296 2766.763968 2 2 1228
13440 12415 3 8.0 1.0 2.0 NaN NaN NaN NaN NaN ... 0 0 0 0 2580.625342 3002.831758 5050.462734 2 82 1234
13441 12416 1 22.0 1.0 5.0 NaN NaN NaN NaN NaN ... 0 0 0 0 3454.927434 3723.198051 6459.748618 2 78 1233
13530 12517 1 25.0 1.0 6.0 NaN NaN NaN NaN NaN ... 0 0 0 0 2734.661252 3244.480863 5629.174298 1 69 1237
13577 12565 2 35.0 1.0 8.0 NaN NaN NaN NaN NaN ... 0 0 0 0 3195.641221 3834.241709 6652.409365 1 78 1236

352 rows × 243 columns

No, data is not corrupted. Checking the codebook may help for better resolution.

Now, let's check the shape of this dataframe.

In [5]:
resp.shape
Out[5]:
(13593, 243)

It has 243 variables about 13593 pregnancies.

Let's check the outcome variable.

In [6]:
resp["outcome"].value_counts().sort_index()
Out[6]:
outcome
1    9148
2    1862
3     120
4    1921
5     190
6     352
Name: count, dtype: int64

value_counts() counts the number of different values, useful for counting elements for different categorical variables.

In [7]:
for i in resp.columns:
    if i.startswith("birth"):
        print(i)
birthwgt_lb
birthwgt_oz
birthwgt_lb2
birthwgt_oz2
birthwgt_lb3
birthwgt_oz3
birthplc
birthord
birthord_i

Also, here are the birth weights.

In [8]:
resp["birthwgt_lb"].value_counts(dropna=False).sort_index()
Out[8]:
birthwgt_lb
0.0        8
1.0       40
2.0       53
3.0       98
4.0      229
5.0      697
6.0     2223
7.0     3049
8.0     1889
9.0      623
10.0     132
11.0      26
12.0      10
13.0       3
14.0       3
15.0       1
51.0       1
97.0       1
98.0       1
99.0      57
NaN     4449
Name: count, dtype: int64

As we can see, there are weights far above what a baby could possibly weigh. We are going to replace those values with NaN.

In [9]:
resp["birthwgt_lb"] = resp["birthwgt_lb"].replace([15,51,97,98,99], np.nan)

Now it is clearer.

In [10]:
resp["birthwgt_lb"].value_counts(dropna=False).sort_index()
Out[10]:
birthwgt_lb
0.0        8
1.0       40
2.0       53
3.0       98
4.0      229
5.0      697
6.0     2223
7.0     3049
8.0     1889
9.0      623
10.0     132
11.0      26
12.0      10
13.0       3
14.0       3
NaN     4510
Name: count, dtype: int64

Now, let's look at the average age of women for the end of birth.

In [11]:
for i in resp.columns:
    if i.startswith("age"):
        print(i)
ageatend
ageqtnur_n
ageqtnur_p
ageqtnur
ageqtnur_n2
ageqtnur_p2
ageqtnur2
ageqtnur_n3
ageqtnur_p3
ageqtnur3
agepreg
agecon
ager
agescrn
agepreg_i
agecon_i
ager_i
In [12]:
resp["agepreg"].mean()
Out[12]:
np.float64(2468.8151197039497)

In this dataset, agepreg is stored as centiyear (which is hundredths of a year), it would be eaiser for us if we transformed this column into years.

In [13]:
resp["agepreg"] /= 100
In [14]:
resp["agepreg"]
Out[14]:
0        33.16
1        39.25
2        14.33
3        17.83
4        18.33
         ...  
13588    17.91
13589    18.50
13590    19.75
13591    21.58
13592    21.58
Name: agepreg, Length: 13593, dtype: float64

Now, let's look at the average of agepreg. Mother's age at the end of the pregnancy.

In [15]:
resp["agepreg"].mean()
Out[15]:
np.float64(24.6881511970395)
In [16]:
resp[['birthwgt_lb', 'birthwgt_oz']]
Out[16]:
birthwgt_lb birthwgt_oz
0 8.0 13.0
1 7.0 14.0
2 9.0 2.0
3 7.0 0.0
4 6.0 3.0
... ... ...
13588 6.0 3.0
13589 NaN NaN
13590 NaN NaN
13591 7.0 8.0
13592 7.0 8.0

13593 rows × 2 columns

Coming back to weights, we see that birth weight is separated to pounds and ounces, we will combine them in totalwgt_lb.

In [17]:
resp["totalwgt_lb"] = resp["birthwgt_lb"] + resp["birthwgt_oz"] / 16.0
resp[['totalwgt_lb','birthwgt_lb', 'birthwgt_oz']]
/tmp/ipykernel_2681/781844691.py:1: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  resp["totalwgt_lb"] = resp["birthwgt_lb"] + resp["birthwgt_oz"] / 16.0
Out[17]:
totalwgt_lb birthwgt_lb birthwgt_oz
0 8.8125 8.0 13.0
1 7.8750 7.0 14.0
2 9.1250 9.0 2.0
3 7.0000 7.0 0.0
4 6.1875 6.0 3.0
... ... ... ...
13588 6.1875 6.0 3.0
13589 NaN NaN NaN
13590 NaN NaN NaN
13591 7.5000 7.0 8.0
13592 7.5000 7.0 8.0

13593 rows × 3 columns

Also, who uses imperal system anyway? Let's convert this to kilograms.

In [18]:
resp["totalwgt_kg"] = resp["totalwgt_lb"] * POUND
resp["totalwgt_kg"]
/tmp/ipykernel_2681/542290660.py:1: PerformanceWarning: DataFrame is highly fragmented.  This is usually the result of calling `frame.insert` many times, which has poor performance.  Consider joining all columns at once using pd.concat(axis=1) instead. To get a de-fragmented frame, use `newframe = frame.copy()`
  resp["totalwgt_kg"] = resp["totalwgt_lb"] * POUND
Out[18]:
0        3.997283
1        3.572040
2        4.139030
3        3.175147
4        2.806603
           ...   
13588    2.806603
13589         NaN
13590         NaN
13591    3.401943
13592    3.401943
Name: totalwgt_kg, Length: 13593, dtype: float64

We are getting "highly fragmented dataframe" warning, let's fix it.

In [19]:
resp = resp.copy()
resp
Out[19]:
caseid pregordr howpreg_n howpreg_p moscurrp nowprgdk pregend1 pregend2 nbrnaliv multbrth ... religion_i metro_i basewgt adj_mod_basewgt finalwgt secu_p sest cmintvw totalwgt_lb totalwgt_kg
0 1 1 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 3410.389399 3869.349602 6448.271112 2 9 1231 8.8125 3.997283
1 1 2 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 3410.389399 3869.349602 6448.271112 2 9 1231 7.8750 3.572040
2 2 1 NaN NaN NaN NaN 5.0 NaN 3.0 5.0 ... 0 0 7226.301740 8567.549110 12999.542264 2 12 1231 9.1250 4.139030
3 2 2 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 7226.301740 8567.549110 12999.542264 2 12 1231 7.0000 3.175147
4 2 3 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 7226.301740 8567.549110 12999.542264 2 12 1231 6.1875 2.806603
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
13588 12571 1 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 4670.540953 5795.692880 6269.200989 1 78 1227 6.1875 2.806603
13589 12571 2 NaN NaN NaN NaN 3.0 NaN NaN NaN ... 0 0 4670.540953 5795.692880 6269.200989 1 78 1227 NaN NaN
13590 12571 3 NaN NaN NaN NaN 3.0 NaN NaN NaN ... 0 0 4670.540953 5795.692880 6269.200989 1 78 1227 NaN NaN
13591 12571 4 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 4670.540953 5795.692880 6269.200989 1 78 1227 7.5000 3.401943
13592 12571 5 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 4670.540953 5795.692880 6269.200989 1 78 1227 7.5000 3.401943

13593 rows × 245 columns

For now, we fixed it. But using dictionaries, creating a data frame with it, then concatenating them in one of those data frames is much more efficient when there are innumerous amount of variables.

Now we can summarize the statistics.

In [20]:
weights = resp["totalwgt_kg"]
n = weights.count()
weights
Out[20]:
0        3.997283
1        3.572040
2        4.139030
3        3.175147
4        2.806603
           ...   
13588    2.806603
13589         NaN
13590         NaN
13591    3.401943
13592    3.401943
Name: totalwgt_kg, Length: 13593, dtype: float64
In [21]:
print(weights.sum() / n)
print(weights.mean())
mean = weights.mean()
3.3079302256167424
3.3079302256167424

$$ SS = \sum (x_i - \hat{x})^2 $$

In [22]:
squared_deviations = (weights - mean) ** 2
squared_deviations
Out[22]:
0        0.475207
1        0.069754
2        0.690727
3        0.017631
4        0.251329
           ...   
13588    0.251329
13589         NaN
13590         NaN
13591    0.008838
13592    0.008838
Name: totalwgt_kg, Length: 13593, dtype: float64

$$ s^2 = \frac{SS}{n-1} $$

In [23]:
print(squared_deviations.sum() / (n - 1))
var = weights.var(ddof=1)
0.43871134313956867

$$ s = \sqrt{\frac{SS}{n-1}} $$

In [24]:
print(np.sqrt(var))
std = weights.std(ddof=1)
print(std)
0.662352884148298
0.662352884148298

Informally, values that are one or two SDs from the mean are common.

There are number of ways to interpret this data. But for this exercise, we are going to analyse caseid 10229

In [25]:
subset = resp[resp["caseid"] == 10229]
subset["outcome"]
Out[25]:
11093    4
11094    4
11095    4
11096    4
11097    4
11098    4
11099    1
Name: outcome, dtype: int64

1 indicates a live birth, 4 indicates a miscarriage. If we consider this data with empathy, it is natural to be moved by the story it tells.

Finally, let's do some practice.

In [26]:
resp["birthord"].value_counts().sort_index()
Out[26]:
birthord
1.0     4413
2.0     2874
3.0     1234
4.0      421
5.0      126
6.0       50
7.0       20
8.0        7
9.0        2
10.0       1
Name: count, dtype: int64
In [27]:
resp[resp["caseid"] == 2298]
Out[27]:
caseid pregordr howpreg_n howpreg_p moscurrp nowprgdk pregend1 pregend2 nbrnaliv multbrth ... religion_i metro_i basewgt adj_mod_basewgt finalwgt secu_p sest cmintvw totalwgt_lb totalwgt_kg
2610 2298 1 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 3247.916977 5123.759559 5556.717241 2 18 1234 6.8750 3.118448
2611 2298 2 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 3247.916977 5123.759559 5556.717241 2 18 1234 5.5000 2.494758
2612 2298 3 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 3247.916977 5123.759559 5556.717241 2 18 1234 4.1875 1.899418
2613 2298 4 NaN NaN NaN NaN 6.0 NaN 1.0 NaN ... 0 0 3247.916977 5123.759559 5556.717241 2 18 1234 6.8750 3.118448

4 rows × 245 columns

In [28]:
resp[(resp["caseid"] == 5013) & (resp["pregordr"] == 1)]["totalwgt_kg"]
Out[28]:
5516    3.345244
Name: totalwgt_kg, dtype: float64
In [29]:
resp.to_csv("./data/2002FemPreg_after_01.csv", index=False)

Glossary from the resource

  • anecdotal evidence: Data collected informally from a small number of individual cases, often without systematic sampling.
  • cross-sectional study: A study that collects data from a representative sample of a population at a single point or interval in time.
  • cycle: One data-collection interval in a study that collects data at multiple intervals in time.
  • population: The entire group of individuals or items that is the subject of a study.
  • sample: A subset of a population, often chosen at random.
  • respondents: People who participate in a survey and respond to questions.
  • representative: A sample is representative if it is similar to the population in ways that are important for the purposes of the study.
  • stratified: A sample is stratified if it deliberately oversamples some groups, usually to make sure that enough members are included to support valid conclusions.
  • oversampled: A group is oversampled if its members have a higher chance of appearing in a sample.
  • variable: In survey data, a variable is a collection of responses to questions or values computed from responses.
  • codebook: A document that describes the variables in a dataset, and provides other information about the data.
  • recode: A variable that is computed based on other variables in a dataset.
  • raw data: Data that has not been processed after collection.
  • data cleaning: A process for identifying and correcting errors in a dataset, dealing with missing values, and computing recodes.
  • statistic: A value that describes or summarizes a property of a sample.
  • standard deviation: A statistic that quantifies the spread of data around the mean.
>