-
Notifications
You must be signed in to change notification settings - Fork 433
/
Ch02-statlearn-lab.Rmd
1395 lines (935 loc) · 40.6 KB
/
Ch02-statlearn-lab.Rmd
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
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Introduction to Python
<a target="_blank" href="https://colab.research.google.com/github/intro-stat-learning/ISLP_labs/blob/v2.2/Ch02-statlearn-lab.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
</a>
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/intro-stat-learning/ISLP_labs/v2.2?labpath=Ch02-statlearn-lab.ipynb)
## Getting Started
To run the labs in this book, you will need two things:
* An installation of `Python3`, which is the specific version of `Python` used in the labs.
* Access to `Jupyter`, a very popular `Python` interface that runs code through a file called a *notebook*.
You can download and install `Python3` by following the instructions available at [anaconda.com](http://anaconda.com).
There are a number of ways to get access to `Jupyter`. Here are just a few:
* Using Google's `Colaboratory` service: [colab.research.google.com/](https://colab.research.google.com/).
* Using `JupyterHub`, available at [jupyter.org/hub](https://jupyter.org/hub).
* Using your own `jupyter` installation. Installation instructions are available at [jupyter.org/install](https://jupyter.org/install).
Please see the `Python` resources page on the book website [statlearning.com](https://www.statlearning.com) for up-to-date information about getting `Python` and `Jupyter` working on your computer.
You will need to install the `ISLP` package, which provides access to the datasets and custom-built functions that we provide.
Inside a macOS or Linux terminal type `pip install ISLP`; this also installs most other packages needed in the labs. The `Python` resources page has a link to the `ISLP` documentation website.
To run this lab, download the file `Ch2-statlearn-lab.ipynb` from the `Python` resources page.
Now run the following code at the command line: `jupyter lab Ch2-statlearn-lab.ipynb`.
If you're using Windows, you can use the `start menu` to access `anaconda`, and follow the links. For example, to install `ISLP` and run this lab, you can run the same code above in an `anaconda` shell.
## Basic Commands
In this lab, we will introduce some simple `Python` commands.
For more resources about `Python` in general, readers may want to consult the tutorial at [docs.python.org/3/tutorial/](https://docs.python.org/3/tutorial/).
Like most programming languages, `Python` uses *functions*
to perform operations. To run a
function called `fun`, we type
`fun(input1,input2)`, where the inputs (or *arguments*)
`input1` and `input2` tell
`Python` how to run the function. A function can have any number of
inputs. For example, the
`print()` function outputs a text representation of all of its arguments to the console.
```{python}
print('fit a model with', 11, 'variables')
```
The following command will provide information about the `print()` function.
```{python}
print?
```
Adding two integers in `Python` is pretty intuitive.
```{python}
3 + 5
```
In `Python`, textual data is handled using
*strings*. For instance, `"hello"` and
`'hello'`
are strings.
We can concatenate them using the addition `+` symbol.
```{python}
"hello" + " " + "world"
```
A string is actually a type of *sequence*: this is a generic term for an ordered list.
The three most important types of sequences are lists, tuples, and strings.
We introduce lists now.
The following command instructs `Python` to join together
the numbers 3, 4, and 5, and to save them as a
*list* named `x`. When we
type `x`, it gives us back the list.
```{python}
x = [3, 4, 5]
x
```
Note that we used the brackets
`[]` to construct this list.
We will often want to add two sets of numbers together. It is reasonable to try the following code,
though it will not produce the desired results.
```{python}
y = [4, 9, 7]
x + y
```
The result may appear slightly counterintuitive: why did `Python` not add the entries of the lists
element-by-element?
In `Python`, lists hold *arbitrary* objects, and are added using *concatenation*.
In fact, concatenation is the behavior that we saw earlier when we entered `"hello" + " " + "world"`.
This example reflects the fact that
`Python` is a general-purpose programming language. Much of `Python`'s data-specific
functionality comes from other packages, notably `numpy`
and `pandas`.
In the next section, we will introduce the `numpy` package.
See [docs.scipy.org/doc/numpy/user/quickstart.html](https://docs.scipy.org/doc/numpy/user/quickstart.html) for more information about `numpy`.
## Introduction to Numerical Python
As mentioned earlier, this book makes use of functionality that is contained in the `numpy`
*library*, or *package*. A package is a collection of modules that are not necessarily included in
the base `Python` distribution. The name `numpy` is an abbreviation for *numerical Python*.
To access `numpy`, we must first `import` it.
```{python}
import numpy as np
```
In the previous line, we named the `numpy` *module* `np`; an abbreviation for easier referencing.
In `numpy`, an *array* is a generic term for a multidimensional
set of numbers.
We use the `np.array()` function to define `x` and `y`, which are one-dimensional arrays, i.e. vectors.
```{python}
x = np.array([3, 4, 5])
y = np.array([4, 9, 7])
```
Note that if you forgot to run the `import numpy as np` command earlier, then
you will encounter an error in calling the `np.array()` function in the previous line.
The syntax `np.array()` indicates that the function being called
is part of the `numpy` package, which we have abbreviated as `np`.
Since `x` and `y` have been defined using `np.array()`, we get a sensible result when we add them together. Compare this to our results in the previous section,
when we tried to add two lists without using `numpy`.
```{python}
x + y
```
In `numpy`, matrices are typically represented as two-dimensional arrays, and vectors as one-dimensional arrays. {While it is also possible to create matrices using `np.matrix()`, we will use `np.array()` throughout the labs in this book.}
We can create a two-dimensional array as follows.
```{python}
x = np.array([[1, 2], [3, 4]])
x
```
The object `x` has several
*attributes*, or associated objects. To access an attribute of `x`, we type `x.attribute`, where we replace `attribute`
with the name of the attribute.
For instance, we can access the `ndim` attribute of `x` as follows.
```{python}
x.ndim
```
The output indicates that `x` is a two-dimensional array.
Similarly, `x.dtype` is the *data type* attribute of the object `x`. This indicates that `x` is
comprised of 64-bit integers:
```{python}
x.dtype
```
Why is `x` comprised of integers? This is because we created `x` by passing in exclusively integers to the `np.array()` function.
If
we had passed in any decimals, then we would have obtained an array of
*floating point numbers* (i.e. real-valued numbers).
```{python}
np.array([[1, 2], [3.0, 4]]).dtype
```
Typing `fun?` will cause `Python` to display
documentation associated with the function `fun`, if it exists.
We can try this for `np.array()`.
```{python}
np.array?
```
This documentation indicates that we could create a floating point array by passing a `dtype` argument into `np.array()`.
```{python}
np.array([[1, 2], [3, 4]], float).dtype
```
The array `x` is two-dimensional. We can find out the number of rows and columns by looking
at its `shape` attribute.
```{python}
x.shape
```
A *method* is a function that is associated with an
object.
For instance, given an array `x`, the expression
`x.sum()` sums all of its elements, using the `sum()`
method for arrays.
The call `x.sum()` automatically provides `x` as the
first argument to its `sum()` method.
```{python}
x = np.array([1, 2, 3, 4])
x.sum()
```
We could also sum the elements of `x` by passing in `x` as an argument to the `np.sum()` function.
```{python}
x = np.array([1, 2, 3, 4])
np.sum(x)
```
As another example, the
`reshape()` method returns a new array with the same elements as
`x`, but a different shape.
We do this by passing in a `tuple` in our call to
`reshape()`, in this case `(2, 3)`. This tuple specifies that we would like to create a two-dimensional array with
$2$ rows and $3$ columns. {Like lists, tuples represent a sequence of objects. Why do we need more than one way to create a sequence? There are a few differences between tuples and lists, but perhaps the most important is that elements of a tuple cannot be modified, whereas elements of a list can be.}
In what follows, the
`\n` character creates a *new line*.
```{python}
x = np.array([1, 2, 3, 4, 5, 6])
print('beginning x:\n', x)
x_reshape = x.reshape((2, 3))
print('reshaped x:\n', x_reshape)
```
The previous output reveals that `numpy` arrays are specified as a sequence
of *rows*. This is called *row-major ordering*, as opposed to *column-major ordering*.
`Python` (and hence `numpy`) uses 0-based
indexing. This means that to access the top left element of `x_reshape`,
we type in `x_reshape[0,0]`.
```{python}
x_reshape[0, 0]
```
Similarly, `x_reshape[1,2]` yields the element in the second row and the third column
of `x_reshape`.
```{python}
x_reshape[1, 2]
```
Similarly, `x[2]` yields the
third entry of `x`.
Now, let's modify the top left element of `x_reshape`. To our surprise, we discover that the first element of `x` has been modified as well!
```{python}
print('x before we modify x_reshape:\n', x)
print('x_reshape before we modify x_reshape:\n', x_reshape)
x_reshape[0, 0] = 5
print('x_reshape after we modify its top left element:\n', x_reshape)
print('x after we modify top left element of x_reshape:\n', x)
```
Modifying `x_reshape` also modified `x` because the two objects occupy the same space in memory.
We just saw that we can modify an element of an array. Can we also modify a tuple? It turns out that we cannot --- and trying to do so introduces
an *exception*, or error.
```{python}
my_tuple = (3, 4, 5)
my_tuple[0] = 2
```
We now briefly mention some attributes of arrays that will come in handy. An array's `shape` attribute contains its dimension; this is always a tuple.
The `ndim` attribute yields the number of dimensions, and `T` provides its transpose.
```{python}
x_reshape.shape, x_reshape.ndim, x_reshape.T
```
Notice that the three individual outputs `(2,3)`, `2`, and `array([[5, 4],[2, 5], [3,6]])` are themselves output as a tuple.
We will often want to apply functions to arrays.
For instance, we can compute the
square root of the entries using the `np.sqrt()` function:
```{python}
np.sqrt(x)
```
We can also square the elements:
```{python}
x**2
```
We can compute the square roots using the same notation, raising to the power of $1/2$ instead of 2.
```{python}
x**0.5
```
Throughout this book, we will often want to generate random data.
The `np.random.normal()` function generates a vector of random
normal variables. We can learn more about this function by looking at the help page, via a call to `np.random.normal?`.
The first line of the help page reads `normal(loc=0.0, scale=1.0, size=None)`.
This *signature* line tells us that the function's arguments are `loc`, `scale`, and `size`. These are *keyword* arguments, which means that when they are passed into
the function, they can be referred to by name (in any order). {`Python` also uses *positional* arguments. Positional arguments do not need to use a keyword. To see an example, type in `np.sum?`. We see that `a` is a positional argument, i.e. this function assumes that the first unnamed argument that it receives is the array to be summed. By contrast, `axis` and `dtype` are keyword arguments: the position in which these arguments are entered into `np.sum()` does not matter.}
By default, this function will generate random normal variable(s) with mean (`loc`) $0$ and standard deviation (`scale`) $1$; furthermore,
a single random variable will be generated unless the argument to `size` is changed.
We now generate 50 independent random variables from a $N(0,1)$ distribution.
```{python}
x = np.random.normal(size=50)
x
```
We create an array `y` by adding an independent $N(50,1)$ random variable to each element of `x`.
```{python}
y = x + np.random.normal(loc=50, scale=1, size=50)
```
The `np.corrcoef()` function computes the correlation matrix between `x` and `y`. The off-diagonal elements give the
correlation between `x` and `y`.
```{python}
np.corrcoef(x, y)
```
If you're following along in your own `Jupyter` notebook, then you probably noticed that you got a different set of results when you ran the past few
commands. In particular,
each
time we call `np.random.normal()`, we will get a different answer, as shown in the following example.
```{python}
print(np.random.normal(scale=5, size=2))
print(np.random.normal(scale=5, size=2))
```
In order to ensure that our code provides exactly the same results
each time it is run, we can set a *random seed*
using the
`np.random.default_rng()` function.
This function takes an arbitrary, user-specified integer argument. If we set a random seed before
generating random data, then re-running our code will yield the same results. The
object `rng` has essentially all the random number generating methods found in `np.random`. Hence, to
generate normal data we use `rng.normal()`.
```{python}
rng = np.random.default_rng(1303)
print(rng.normal(scale=5, size=2))
rng2 = np.random.default_rng(1303)
print(rng2.normal(scale=5, size=2))
```
Throughout the labs in this book, we use `np.random.default_rng()` whenever we
perform calculations involving random quantities within `numpy`. In principle, this
should enable the reader to exactly reproduce the stated results. However, as new versions of `numpy` become available, it is possible
that some small discrepancies may occur between the output
in the labs and the output
from `numpy`.
The `np.mean()`, `np.var()`, and `np.std()` functions can be used
to compute the mean, variance, and standard deviation of arrays. These functions are also
available as methods on the arrays.
```{python}
rng = np.random.default_rng(3)
y = rng.standard_normal(10)
np.mean(y), y.mean()
```
```{python}
np.var(y), y.var(), np.mean((y - y.mean())**2)
```
Notice that by default `np.var()` divides by the sample size $n$ rather
than $n-1$; see the `ddof` argument in `np.var?`.
```{python}
np.sqrt(np.var(y)), np.std(y)
```
The `np.mean()`, `np.var()`, and `np.std()` functions can also be applied to the rows and columns of a matrix.
To see this, we construct a $10 \times 3$ matrix of $N(0,1)$ random variables, and consider computing its row sums.
```{python}
X = rng.standard_normal((10, 3))
X
```
Since arrays are row-major ordered, the first axis, i.e. `axis=0`, refers to its rows. We pass this argument into the `mean()` method for the object `X`.
```{python}
X.mean(axis=0)
```
The following yields the same result.
```{python}
X.mean(0)
```
## Graphics
In `Python`, common practice is to use the library
`matplotlib` for graphics.
However, since `Python` was not written with data analysis in mind,
the notion of plotting is not intrinsic to the language.
We will use the `subplots()` function
from `matplotlib.pyplot` to create a figure and the
axes onto which we plot our data.
For many more examples of how to make plots in `Python`,
readers are encouraged to visit [matplotlib.org/stable/gallery/](https://matplotlib.org/stable/gallery/index.html).
In `matplotlib`, a plot consists of a *figure* and one or more *axes*. You can think of the figure as the blank canvas upon which
one or more plots will be displayed: it is the entire plotting window.
The *axes* contain important information about each plot, such as its $x$- and $y$-axis labels,
title, and more. (Note that in `matplotlib`, the word *axes* is not the plural of *axis*: a plot's *axes* contains much more information
than just the $x$-axis and the $y$-axis.)
We begin by importing the `subplots()` function
from `matplotlib`. We use this function
throughout when creating figures.
The function returns a tuple of length two: a figure
object as well as the relevant axes object. We will typically
pass `figsize` as a keyword argument.
Having created our axes, we attempt our first plot using its `plot()` method.
To learn more about it,
type `ax.plot?`.
```{python}
from matplotlib.pyplot import subplots
fig, ax = subplots(figsize=(8, 8))
x = rng.standard_normal(100)
y = rng.standard_normal(100)
ax.plot(x, y);
```
We pause here to note that we have *unpacked* the tuple of length two returned by `subplots()` into the two distinct
variables `fig` and `ax`. Unpacking
is typically preferred to the following equivalent but slightly more verbose code:
```{python}
output = subplots(figsize=(8, 8))
fig = output[0]
ax = output[1]
```
We see that our earlier cell produced a line plot, which is the default. To create a scatterplot, we provide an additional argument to `ax.plot()`, indicating that circles should be displayed.
```{python}
fig, ax = subplots(figsize=(8, 8))
ax.plot(x, y, 'o');
```
Different values
of this additional argument can be used to produce different colored lines
as well as different linestyles.
As an alternative, we could use the `ax.scatter()` function to create a scatterplot.
```{python}
fig, ax = subplots(figsize=(8, 8))
ax.scatter(x, y, marker='o');
```
Notice that in the code blocks above, we have ended
the last line with a semicolon. This prevents `ax.plot(x, y)` from printing
text to the notebook. However, it does not prevent a plot from being produced.
If we omit the trailing semi-colon, then we obtain the following output:
```{python}
fig, ax = subplots(figsize=(8, 8))
ax.scatter(x, y, marker='o')
```
In what follows, we will use
trailing semicolons whenever the text that would be output is not
germane to the discussion at hand.
To label our plot, we make use of the `set_xlabel()`, `set_ylabel()`, and `set_title()` methods
of `ax`.
```{python}
fig, ax = subplots(figsize=(8, 8))
ax.scatter(x, y, marker='o')
ax.set_xlabel("this is the x-axis")
ax.set_ylabel("this is the y-axis")
ax.set_title("Plot of X vs Y");
```
Having access to the figure object `fig` itself means that we can go in and change some aspects and then redisplay it. Here, we change
the size from `(8, 8)` to `(12, 3)`.
```{python}
fig.set_size_inches(12,3)
fig
```
Occasionally we will want to create several plots within a figure. This can be
achieved by passing additional arguments to `subplots()`.
Below, we create a $2 \times 3$ grid of plots
in a figure of size determined by the `figsize` argument. In such
situations, there is often a relationship between the axes in the plots. For example,
all plots may have a common $x$-axis. The `subplots()` function can automatically handle
this situation when passed the keyword argument `sharex=True`.
The `axes` object below is an array pointing to different plots in the figure.
```{python}
fig, axes = subplots(nrows=2,
ncols=3,
figsize=(15, 5))
```
We now produce a scatter plot with `'o'` in the second column of the first row and
a scatter plot with `'+'` in the third column of the second row.
```{python}
axes[0,1].plot(x, y, 'o')
axes[1,2].scatter(x, y, marker='+')
fig
```
Type `subplots?` to learn more about
`subplots()`.
To save the output of `fig`, we call its `savefig()`
method. The argument `dpi` is the dots per inch, used
to determine how large the figure will be in pixels.
```{python}
fig.savefig("Figure.png", dpi=400)
fig.savefig("Figure.pdf", dpi=200);
```
We can continue to modify `fig` using step-by-step updates; for example, we can modify the range of the $x$-axis, re-save the figure, and even re-display it.
```{python}
axes[0,1].set_xlim([-1,1])
fig.savefig("Figure_updated.jpg")
fig
```
We now create some more sophisticated plots. The
`ax.contour()` method produces a *contour plot*
in order to represent three-dimensional data, similar to a
topographical map. It takes three arguments:
* A vector of `x` values (the first dimension),
* A vector of `y` values (the second dimension), and
* A matrix whose elements correspond to the `z` value (the third
dimension) for each pair of `(x,y)` coordinates.
To create `x` and `y`, we’ll use the command `np.linspace(a, b, n)`,
which returns a vector of `n` numbers starting at `a` and ending at `b`.
```{python}
fig, ax = subplots(figsize=(8, 8))
x = np.linspace(-np.pi, np.pi, 50)
y = x
f = np.multiply.outer(np.cos(y), 1 / (1 + x**2))
ax.contour(x, y, f);
```
We can increase the resolution by adding more levels to the image.
```{python}
fig, ax = subplots(figsize=(8, 8))
ax.contour(x, y, f, levels=45);
```
To fine-tune the output of the
`ax.contour()` function, take a
look at the help file by typing `?plt.contour`.
The `ax.imshow()` method is similar to
`ax.contour()`, except that it produces a color-coded plot
whose colors depend on the `z` value. This is known as a
*heatmap*, and is sometimes used to plot temperature in
weather forecasts.
```{python}
fig, ax = subplots(figsize=(8, 8))
ax.imshow(f);
```
## Sequences and Slice Notation
As seen above, the
function `np.linspace()` can be used to create a sequence
of numbers.
```{python}
seq1 = np.linspace(0, 10, 11)
seq1
```
The function `np.arange()`
returns a sequence of numbers spaced out by `step`. If `step` is not specified, then a default value of $1$ is used. Let's create a sequence
that starts at $0$ and ends at $10$.
```{python}
seq2 = np.arange(0, 10)
seq2
```
Why isn't $10$ output above? This has to do with *slice* notation in `Python`.
Slice notation
is used to index sequences such as lists, tuples and arrays.
Suppose we want to retrieve the fourth through sixth (inclusive) entries
of a string. We obtain a slice of the string using the indexing notation `[3:6]`.
```{python}
"hello world"[3:6]
```
In the code block above, the notation `3:6` is shorthand for `slice(3,6)` when used inside
`[]`.
```{python}
"hello world"[slice(3,6)]
```
You might have expected `slice(3,6)` to output the fourth through seventh characters in the text string (recalling that `Python` begins its indexing at zero), but instead it output the fourth through sixth.
This also explains why the earlier `np.arange(0, 10)` command output only the integers from $0$ to $9$.
See the documentation `slice?` for useful options in creating slices.
## Indexing Data
To begin, we create a two-dimensional `numpy` array.
```{python}
A = np.array(np.arange(16)).reshape((4, 4))
A
```
Typing `A[1,2]` retrieves the element corresponding to the second row and third
column. (As usual, `Python` indexes from $0.$)
```{python}
A[1,2]
```
The first number after the open-bracket symbol `[`
refers to the row, and the second number refers to the column.
### Indexing Rows, Columns, and Submatrices
To select multiple rows at a time, we can pass in a list
specifying our selection. For instance, `[1,3]` will retrieve the second and fourth rows:
```{python}
A[[1,3]]
```
To select the first and third columns, we pass in `[0,2]` as the second argument in the square brackets.
In this case we need to supply the first argument `:`
which selects all rows.
```{python}
A[:,[0,2]]
```
Now, suppose that we want to select the submatrix made up of the second and fourth
rows as well as the first and third columns. This is where
indexing gets slightly tricky. It is natural to try to use lists to retrieve the rows and columns:
```{python}
A[[1,3],[0,2]]
```
Oops --- what happened? We got a one-dimensional array of length two identical to
```{python}
np.array([A[1,0],A[3,2]])
```
Similarly, the following code fails to extract the submatrix comprised of the second and fourth rows and the first, third, and fourth columns:
```{python}
A[[1,3],[0,2,3]]
```
We can see what has gone wrong here. When supplied with two indexing lists, the `numpy` interpretation is that these provide pairs of $i,j$ indices for a series of entries. That is why the pair of lists must have the same length. However, that was not our intent, since we are looking for a submatrix.
One easy way to do this is as follows. We first create a submatrix by subsetting the rows of `A`, and then on the fly we make a further submatrix by subsetting its columns.
```{python}
A[[1,3]][:,[0,2]]
```
There are more efficient ways of achieving the same result.
The *convenience function* `np.ix_()` allows us to extract a submatrix
using lists, by creating an intermediate *mesh* object.
```{python}
idx = np.ix_([1,3],[0,2,3])
A[idx]
```
Alternatively, we can subset matrices efficiently using slices.
The slice
`1:4:2` captures the second and fourth items of a sequence, while the slice `0:3:2` captures
the first and third items (the third element in a slice sequence is the step size).
```{python}
A[1:4:2,0:3:2]
```
Why are we able to retrieve a submatrix directly using slices but not using lists?
Its because they are different `Python` types, and
are treated differently by `numpy`.
Slices can be used to extract objects from arbitrary sequences, such as strings, lists, and tuples, while the use of lists for indexing is more limited.
### Boolean Indexing
In `numpy`, a *Boolean* is a type that equals either `True` or `False` (also represented as $1$ and $0$, respectively).
The next line creates a vector of $0$'s, represented as Booleans, of length equal to the first dimension of `A`.
```{python}
keep_rows = np.zeros(A.shape[0], bool)
keep_rows
```
We now set two of the elements to `True`.
```{python}
keep_rows[[1,3]] = True
keep_rows
```
Note that the elements of `keep_rows`, when viewed as integers, are the same as the
values of `np.array([0,1,0,1])`. Below, we use `==` to verify their equality. When
applied to two arrays, the `==` operation is applied elementwise.
```{python}
np.all(keep_rows == np.array([0,1,0,1]))
```
(Here, the function `np.all()` has checked whether
all entries of an array are `True`. A similar function, `np.any()`, can be used to check whether any entries of an array are `True`.)
However, even though `np.array([0,1,0,1])` and `keep_rows` are equal according to `==`, they index different sets of rows!
The former retrieves the first, second, first, and second rows of `A`.
```{python}
A[np.array([0,1,0,1])]
```
By contrast, `keep_rows` retrieves only the second and fourth rows of `A` --- i.e. the rows for which the Boolean equals `TRUE`.
```{python}
A[keep_rows]
```
This example shows that Booleans and integers are treated differently by `numpy`.
We again make use of the `np.ix_()` function
to create a mesh containing the second and fourth rows, and the first, third, and fourth columns. This time, we apply the function to Booleans,
rather than lists.
```{python}
keep_cols = np.zeros(A.shape[1], bool)
keep_cols[[0, 2, 3]] = True
idx_bool = np.ix_(keep_rows, keep_cols)
A[idx_bool]
```
We can also mix a list with an array of Booleans in the arguments to `np.ix_()`:
```{python}
idx_mixed = np.ix_([1,3], keep_cols)
A[idx_mixed]
```
For more details on indexing in `numpy`, readers are referred
to the `numpy` tutorial mentioned earlier.
## Loading Data
Data sets often contain different types of data, and may have names associated with the rows or columns.
For these reasons, they typically are best accommodated using a
*data frame*.
We can think of a data frame as a sequence
of arrays of identical length; these are the columns. Entries in the
different arrays can be combined to form a row.
The `pandas`
library can be used to create and work with data frame objects.
### Reading in a Data Set
The first step of most analyses involves importing a data set into
`Python`.
Before attempting to load
a data set, we must make sure that `Python` knows where to find the file containing it.
If the
file is in the same location
as this notebook file, then we are all set.
Otherwise,
the command
`os.chdir()` can be used to *change directory*. (You will need to call `import os` before calling `os.chdir()`.)
We will begin by reading in `Auto.csv`, available on the book website. This is a comma-separated file, and can be read in using `pd.read_csv()`:
```{python}
import pandas as pd
Auto = pd.read_csv('Auto.csv')
Auto
```
The book website also has a whitespace-delimited version of this data, called `Auto.data`. This can be read in as follows:
```{python}
Auto = pd.read_csv('Auto.data', delim_whitespace=True)
```
Both `Auto.csv` and `Auto.data` are simply text
files. Before loading data into `Python`, it is a good idea to view it using
a text editor or other software, such as Microsoft Excel.
We now take a look at the column of `Auto` corresponding to the variable `horsepower`:
```{python}
Auto['horsepower']
```
We see that the `dtype` of this column is `object`.
It turns out that all values of the `horsepower` column were interpreted as strings when reading
in the data.
We can find out why by looking at the unique values.
```{python}
np.unique(Auto['horsepower'])
```
We see the culprit is the value `?`, which is being used to encode missing values.
To fix the problem, we must provide `pd.read_csv()` with an argument called `na_values`.
Now, each instance of `?` in the file is replaced with the
value `np.nan`, which means *not a number*: