Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fsl/pytreat-practicals-2020
  • mchiew/pytreat-practicals-2020
  • ndcn0236/pytreat-practicals-2020
  • nichols/pytreat-practicals-2020
4 results
Show changes
%% Cell type:markdown id: tags:
# Pandas
Follow along online at: https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020/-/blob/master/talks/pandas/pandas.ipynb
Pandas is a data analysis library focused on the cleaning and exploration of
tabular data.
Some useful links are:
- [main website](https://pandas.pydata.org)
- [documentation](http://pandas.pydata.org/pandas-docs/stable/)<sup>1</sup>
- [Python Data Science Handbook](https://jakevdp.github.io/PythonDataScienceHandbook/)<sup>1</sup> by
Jake van der Plas
- [List of Pandas tutorials](https://pandas.pydata.org/pandas-docs/stable/getting_started/tutorials.html)
<sup>1</sup> This tutorial borrows heavily from the pandas documentation and
the Python Data Science Handbook
%% Cell type:code id: tags:
```
%pylab inline
import pandas as pd # pd is the usual abbreviation for pandas
import matplotlib.pyplot as plt # matplotlib for plotting
import seaborn as sns # seaborn is the main plotting library for Pandas
import statsmodels.api as sm # statsmodels fits linear models to pandas data
import statsmodels.formula.api as smf
from IPython.display import Image
sns.set() # use the prettier seaborn plotting settings rather than the default matplotlib one
```
%% Cell type:markdown id: tags:
> We will mostly be using `seaborn` instead of `matplotlib` for
> visualisation. But `seaborn` is actually an extension to `matplotlib`, so we
> are still using the latter under the hood.
## Loading in data
Pandas supports a wide range of I/O tools to load from text files, binary files,
and SQL databases. You can find a table with all formats
[here](http://pandas.pydata.org/pandas-docs/stable/io.html).
%% Cell type:code id: tags:
```
titanic = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/titanic.csv')
titanic
```
%% Cell type:markdown id: tags:
This loads the data into a
[`DataFrame`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html)
object, which is the main object we will be interacting with in pandas. It
represents a table of data. The other file formats all start with
`pd.read_{format}`. Note that we can provide the URL to the dataset, rather
than download it beforehand.
We can write out the dataset using `dataframe.to_{format}(<filename)`:
We can write out the dataset using `dataframe.to_{format}(<filename>)`:
%% Cell type:code id: tags:
```
titanic.to_csv('titanic_copy.csv', index=False) # we set index to False to prevent pandas from storing the row names
```
%% Cell type:markdown id: tags:
If you can not connect to the internet, you can run the command below to load
this locally stored titanic dataset
%% Cell type:code id: tags:
```
titanic = pd.read_csv('09_pandas/titanic.csv')
titanic = pd.read_csv('titanic.csv')
titanic
```
%% Cell type:markdown id: tags:
Note that the titanic dataset was also available to us as one of the standard
datasets included with seaborn. We could load it from there using
%% Cell type:code id: tags:
```
sns.load_dataset('titanic')
```
%% Cell type:markdown id: tags:
`Dataframes` can also be created from other python objects, using
`pd.DataFrame.from_{other type}`. The most useful of these is `from_dict`,
which converts a mapping of the columns to a pandas `DataFrame` (i.e., table).
%% Cell type:code id: tags:
```
pd.DataFrame.from_dict({
'random numbers': np.random.rand(5),
'sequence (int)': np.arange(5),
'sequence (float)': np.linspace(0, 5, 5),
'letters': list('abcde'),
'constant_value': 'same_value'
})
```
%% Cell type:markdown id: tags:
For many applications (e.g., ICA, machine learning input) you might want to
extract your data as a numpy array. The underlying numpy array can be accessed
using the `values` attribute
using the `to_numpy` method
%% Cell type:code id: tags:
```
titanic.values
titanic.to_numpy()
```
%% Cell type:markdown id: tags:
Note that the type of the returned array is the most common type (in this case
object). If you just want the numeric parts of the table you can use
`select_dtypes`, which selects specific columns based on their dtype:
%% Cell type:code id: tags:
```
titanic.select_dtypes(include=np.number).values
titanic.select_dtypes(include=np.number).to_numpy()
```
%% Cell type:markdown id: tags:
Note that the numpy array has no information on the column names or row indices.
Alternatively, when you want to include the categorical variables in your later
analysis (e.g., for machine learning), you can extract dummy variables using:
%% Cell type:code id: tags:
```
pd.get_dummies(titanic)
```
%% Cell type:markdown id: tags:
## Accessing parts of the data
[Documentation on indexing](http://pandas.pydata.org/pandas-docs/stable/indexing.html)
### Selecting columns by name
Single columns can be selected using the normal python indexing:
%% Cell type:code id: tags:
```
titanic['embark_town']
```
%% Cell type:markdown id: tags:
If the column names are simple strings (not required) we can also access it
directly as an attribute
%% Cell type:code id: tags:
```
titanic.embark_town
```
%% Cell type:markdown id: tags:
Note that this returns a pandas
[`Series`](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.html)
rather than a `DataFrame` object. A `Series` is simply a 1-dimensional array
representing a single column. Multiple columns can be returned by providing a
list of columns names. This will return a `DataFrame`:
%% Cell type:code id: tags:
```
titanic[['class', 'alive']]
```
%% Cell type:markdown id: tags:
Note that you have to provide a list here (square brackets). If you provide a
tuple (round brackets) pandas will think you are trying to access a single
column that has that tuple as a name:
%% Cell type:code id: tags:
```
titanic[('class', 'alive')]
```
%% Cell type:markdown id: tags:
In this case there is no column called `('class', 'alive')` leading to an
error. Later on we will see some uses to having columns named like this.
### Indexing rows by name or integer
Individual rows can be accessed based on their name (i.e., the index) or integer
(i.e., which row it is in). In our current table this will give the same
results. To ensure that these are different, let's sort our titanic dataset
based on the passenger fare:
%% Cell type:code id: tags:
```
titanic_sorted = titanic.sort_values('fare')
titanic_sorted
```
%% Cell type:markdown id: tags:
Note that the re-sorting did not change the values in the index (i.e., left-most
column).
We can select the first row of this newly sorted table using `iloc`
%% Cell type:code id: tags:
```
titanic_sorted.iloc[0]
```
%% Cell type:markdown id: tags:
We can select the row with the index 0 using
%% Cell type:code id: tags:
```
titanic_sorted.loc[0]
```
%% Cell type:markdown id: tags:
Note that this gives the same passenger as the first row of the initial table
before sorting
%% Cell type:code id: tags:
```
titanic.iloc[0]
```
%% Cell type:markdown id: tags:
Another common way to access the first or last N rows of a table is using the
head/tail methods
%% Cell type:code id: tags:
```
titanic_sorted.head(3)
```
%% Cell type:code id: tags:
```
titanic_sorted.tail(3)
```
%% Cell type:markdown id: tags:
Note that nearly all methods in pandas return a new `Dataframe`, which means
that we can easily call another method on them
%% Cell type:code id: tags:
```
titanic_sorted.tail(10).head(5) # select the first 5 of the last 10 passengers in the database
```
%% Cell type:code id: tags:
```
titanic_sorted.iloc[-10:-5] # alternative way to get the same passengers
```
%% Cell type:markdown id: tags:
**Exercise**: use sorting and tail/head or indexing to find the 10 youngest
passengers on the titanic. Try to do this on a single line by chaining calls
to the titanic `DataFrame` object
%% Cell type:code id: tags:
```
titanic.sort_values...
```
%% Cell type:markdown id: tags:
### Indexing rows by value
One final way to select specific columns is by their value
%% Cell type:code id: tags:
```
titanic[titanic.sex == 'female'] # selects all females
```
%% Cell type:code id: tags:
```
# select all passengers older than 60 who departed from Southampton
titanic[(titanic.age > 60) & (titanic['embark_town'] == 'Southampton')]
```
%% Cell type:markdown id: tags:
Note that this required typing `titanic` quite often. A quicker way to get the
same result is using the `query` method, which is described in detail
[here](http://pandas.pydata.org/pandas-docs/stable/indexing.html#the-query-method)
(note that using the `query` method is also faster and uses a lot less
memory).
> You may have trouble using the `query` method with columns which have
a name that cannot be used as a Python identifier.
%% Cell type:code id: tags:
```
titanic.query('(age > 60) & (embark_town == "Southampton")')
```
%% Cell type:markdown id: tags:
When selecting a categorical multiple options from a categorical values you
might want to use `isin`:
%% Cell type:code id: tags:
```
titanic[titanic['class'].isin(['First','Second'])]
```
%% Cell type:markdown id: tags:
Particularly useful when selecting data like this is the `isna` method which
finds all missing data
%% Cell type:code id: tags:
```
titanic[~titanic.age.isna()] # select first few passengers whose age is not N/A
```
%% Cell type:markdown id: tags:
This removing of missing numbers is so common that it has is own method
%% Cell type:code id: tags:
```
titanic.dropna() # drops all passengers that have some datapoint missing
```
%% Cell type:code id: tags:
```
titanic.dropna(subset=['age', 'fare']) # Only drop passengers with missing ages or fares
```
%% Cell type:markdown id: tags:
**Exercise**: use sorting, indexing by value, `dropna` and `tail`/`head` or
indexing to find the 10 oldest female passengers on the titanic. Try to do
this on a single line by chaining calls to the titanic `DataFrame` object
%% Cell type:code id: tags:
```
titanic...
```
%% Cell type:markdown id: tags:
## Plotting the data
Before we start analyzing the data, let's play around with visualizing it.
Pandas does have some basic built-in plotting options:
%% Cell type:code id: tags:
```
titanic.fare.hist(bins=20, log=True)
```
%% Cell type:code id: tags:
```
titanic.age.plot()
```
%% Cell type:markdown id: tags:
Individual columns are essentially 1D arrays, so we can use them as such in
To plot all variables simply call `plot` or `hist` on the full dataframe
rather than a single Series (i.e., column). You might want to set `subplots=True`
to plot each variable in a different subplot.
Individual Series are essentially 1D arrays, so we can use them as such in
`matplotlib`
%% Cell type:code id: tags:
```
plt.scatter(titanic.age, titanic.fare)
```
%% Cell type:markdown id: tags:
However, for most purposes much nicer plots can be obtained using
[Seaborn](https://seaborn.pydata.org). Seaborn has support to produce plots
showing the
[univariate](https://seaborn.pydata.org/tutorial/distributions.html#plotting-univariate-distributions)
or
[bivariate](https://seaborn.pydata.org/tutorial/distributions.html#plotting-bivariate-distributions)
distribution of data in a single or a grid of plots. Most of the seaborn
plotting functions expect to get a pandas `DataFrame` (although they will work
with Numpy arrays as well). So we can plot age vs. fare like:
%% Cell type:code id: tags:
```
sns.jointplot('age', 'fare', data=titanic)
```
%% Cell type:markdown id: tags:
**Exercise**: check the documentation from `sns.jointplot` (hover the mouse
over the text `jointplot` and press shift-tab) to find out how to turn the
scatter plot into a density (kde) map
%% Cell type:code id: tags:
```
sns.jointplot('age', 'fare', data=titanic, ...)
```
%% Cell type:markdown id: tags:
Here is just a brief example of how we can use multiple columns to illustrate
the data in more detail
%% Cell type:code id: tags:
```
sns.relplot(x='age', y='fare', col='class', hue='sex', data=titanic,
col_order=('First', 'Second', 'Third'))
```
%% Cell type:markdown id: tags:
**Exercise**: Split the plot above into two rows with the first row including
the passengers who survived and the second row those who did not (you might
have to check the documentation again by using shift-tab while overing the
mouse over `relplot`)
%% Cell type:code id: tags:
```
sns.relplot(x='age', y='fare', col='class', hue='sex', data=titanic,
col_order=('First', 'Second', 'Third')...)
```
%% Cell type:markdown id: tags:
One of the nice thing of Seaborn is how easy it is to update how these plots
look. You can read more about that
[here](https://seaborn.pydata.org/tutorial/aesthetics.html). For example, to
increase the font size to get a plot more approriate for a talk, you can use:
%% Cell type:code id: tags:
```
sns.set_context('talk')
sns.violinplot(x='class', y='age', hue='sex', data=titanic, split=True,
order=('First', 'Second', 'Third'))
```
%% Cell type:markdown id: tags:
## Summarizing the data (mean, std, etc.)
There are a large number of built-in methods to summarize the observations in
a Pandas `DataFrame`. Most of these will return a `Series` with the columns
names as index:
%% Cell type:code id: tags:
```
titanic.mean()
```
%% Cell type:code id: tags:
```
titanic.quantile(0.75)
```
%% Cell type:markdown id: tags:
One very useful one is `describe`, which gives an overview of many common
summary measures
%% Cell type:code id: tags:
```
titanic.describe()
```
%% Cell type:markdown id: tags:
For a more detailed exploration of the data, you might want to check
[pandas_profiliing](https://pandas-profiling.github.io/pandas-profiling/docs/)
(not installed in fslpython, so the following will not run in fslpython):
%% Cell type:code id: tags:
```
from pandas_profiling import ProfileReport
profile = ProfileReport(titanic, title='Titanic Report', html={'style':{'full_width':True}})
profile.to_widgets()
```
%% Cell type:markdown id: tags:
Note that non-numeric columns are ignored when summarizing data in this way.
We can also define our own functions to apply to the columns (in this case we
have to explicitly set the data types).
%% Cell type:code id: tags:
```
def mad(series):
"""
Computes the median absolute deviatation (MAD)
This is a outlier-resistant measure of the standard deviation
"""
no_nan = series.dropna()
return np.median(abs(no_nan - np.nanmedian(no_nan)))
titanic.select_dtypes(np.number).apply(mad)
```
%% Cell type:markdown id: tags:
We can also provide multiple functions to the `apply` method (note that
functions can be provided as strings)
%% Cell type:code id: tags:
```
titanic.select_dtypes(np.number).apply(['mean', np.median, np.std, mad])
```
%% Cell type:markdown id: tags:
### Grouping by
One of the more powerful features of is `groupby`, which splits the dataset on
a categorical variable. The book contains a clear tutorial on that feature
[here](https://jakevdp.github.io/PythonDataScienceHandbook/03.08-aggregation-and-grouping.html). You
can check the pandas documentation
[here](http://pandas.pydata.org/pandas-docs/stable/groupby.html) for a more
formal introduction. One simple use is just to put it into a loop
%% Cell type:code id: tags:
```
for cls, part_table in titanic.groupby('class'):
print(f'Mean fare in {cls.lower()} class: {part_table.fare.mean()}')
```
%% Cell type:markdown id: tags:
However, it is more often combined with one of the aggregation functions
discussed above as illustrated in this figure from the [Python data science
handbook](https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Split-Apply-Combine)
![group by image](09_pandas/group_by.png)
![group by image](group_by.png)
%% Cell type:code id: tags:
```
titanic.groupby('class').mean()
```
%% Cell type:markdown id: tags:
We can also group by multiple variables at once
%% Cell type:code id: tags:
```
titanic.groupby(['class', 'survived']).mean() # as always in pandas supply multiple column names as lists, not tuples
```
%% Cell type:markdown id: tags:
When grouping it can help to use the `cut` method to split a continuous variable
into a categorical one
%% Cell type:code id: tags:
```
titanic.groupby(['class', pd.cut(titanic.age, bins=(0, 18, 50, np.inf))]).mean()
```
%% Cell type:markdown id: tags:
We can use the `aggregate` method to apply a different function to each series
%% Cell type:code id: tags:
```
titanic.groupby(['class', 'survived']).aggregate((np.median, mad))
```
%% Cell type:markdown id: tags:
Note that both the index (on the left) and the column names (on the top) now
have multiple levels. Such a multi-level index is referred to as `MultiIndex`.
This does complicate selecting specific columns/rows. You can read more of using
`MultiIndex` [here](http://pandas.pydata.org/pandas-docs/stable/advanced.html).
The short version is that columns can be selected using direct indexing (as
discussed above)
%% Cell type:code id: tags:
```
df_full = titanic.groupby(['class', 'survived']).aggregate((np.median, mad))
```
%% Cell type:code id: tags:
```
df_full[('age', 'median')] # selects median age column; note that the round brackets are optional
```
%% Cell type:code id: tags:
```
df_full['age'] # selects both age columns
```
%% Cell type:markdown id: tags:
Remember that indexing based on the index was done through `loc`. The rest is
the same as for the columns above
%% Cell type:code id: tags:
```
df_full.loc[('First', 0)]
```
%% Cell type:code id: tags:
```
df_full.loc['First']
```
%% Cell type:markdown id: tags:
More advanced use of the `MultiIndex` is possible through `xs`:
%% Cell type:code id: tags:
```
df_full.xs(0, level='survived') # selects all the zero's from the survived index
```
%% Cell type:code id: tags:
```
df_full.xs('mad', axis=1, level=1) # selects mad from the second level in the columns (i.e., axis=1)
```
%% Cell type:markdown id: tags:
## Reshaping tables
If we were interested in how the survival rate depends on the class and sex of
the passengers we could simply use a groupby:
%% Cell type:code id: tags:
```
titanic.groupby(['class', 'sex']).survived.mean()
```
%% Cell type:markdown id: tags:
However, this single-column table is difficult to read. The reason for this is
that the indexing is multi-leveled (called `MultiIndex` in pandas), while there
is only a single column. We would like to move one of the levels in the index to
the columns. This can be done using `stack`/`unstack`:
- `unstack`: Moves one levels in the index to the columns
- `stack`: Moves one of levels in the columns to the index
%% Cell type:code id: tags:
```
titanic.groupby(['class', 'sex']).survived.mean().unstack('sex')
```
%% Cell type:markdown id: tags:
The former table, where the different groups are defined in different rows, is
often referred to as long-form. After unstacking the table is often referred to
as wide-form as the different group (sex in this case) is now represented as
different columns. In pandas some operations are easier on long-form tables
(e.g., `groupby`) while others require wide_form tables (e.g., making scatter
plots of two variables). You can go back and forth using `unstack` or `stack` as
illustrated above, but as this is a crucial part of pandas there are many
alternatives, such as `pivot_table`, `melt`, and `wide_to_long`, which we will
discuss below.
We can prettify the table further using seaborn
%% Cell type:code id: tags:
```
ax = sns.heatmap(titanic.groupby(['class', 'sex']).survived.mean().unstack('sex'),
annot=True)
ax.set_title('survival rate')
```
%% Cell type:markdown id: tags:
Note that there are also many ways to produce prettier tables in pandas (e.g.,
color all the negative values). This is documented
[here](http://pandas.pydata.org/pandas-docs/stable/style.html).
Because this stacking/unstacking is fairly common after a groupby operation,
there is a shortcut for it: `pivot_table`
%% Cell type:code id: tags:
```
titanic.pivot_table('survived', 'class', 'sex')
```
%% Cell type:markdown id: tags:
As usual in pandas, where we can also provide multiple column names
%% Cell type:code id: tags:
```
sns.heatmap(titanic.pivot_table('survived', ['class', 'embark_town'], ['sex', pd.cut(titanic.age, (0, 18, np.inf))]), annot=True)
```
%% Cell type:markdown id: tags:
We can also change the function to be used to aggregate the data
%% Cell type:code id: tags:
```
sns.heatmap(titanic.pivot_table('survived', ['class', 'embark_town'], ['sex', pd.cut(titanic.age, (0, 18, np.inf))],
aggfunc='count'), annot=True)
```
%% Cell type:markdown id: tags:
As in `groupby` the aggregation function can be a string of a common aggregation
function, or any function that should be applied.
We can even apply different aggregate functions to different columns
%% Cell type:code id: tags:
```
titanic.pivot_table(index='class', columns='sex',
aggfunc={'survived': 'count', 'fare': np.mean}) # compute number of survivors and mean fare
```
%% Cell type:markdown id: tags:
The opposite of `pivot_table` is `melt`. This can be used to change a wide-form
table into a long-form table. This is not particularly useful on the titanic
dataset, so let's create a new table where this might be useful. Let's say we
have a dataset listing the FA and MD values in various WM tracts:
%% Cell type:code id: tags:
```
tracts = ('Corpus callosum', 'Internal capsule', 'SLF', 'Arcuate fasciculus')
df_wide = pd.DataFrame.from_dict(dict({'subject': list('ABCDEFGHIJ')}, **{
f'FA({tract})': np.random.rand(10) for tract in tracts }, **{
f'MD({tract})': np.random.rand(10) * 1e-3 for tract in tracts
}))
df_wide
```
%% Cell type:markdown id: tags:
This wide-form table (i.e., all the information is in different columns) makes
it hard to select just all the FA values or only the values associated with the
SLF. For this it would be easier to list all the values in a single column.
Most of the tools discussed above (e.g., `group_by` or `seaborn` plotting) work
better with long-form data, which we can obtain from `melt`:
%% Cell type:code id: tags:
```
df_long = df_wide.melt('subject', var_name='measurement', value_name='dti_value')
df_long.head(12)
```
%% Cell type:markdown id: tags:
We can see that `melt` took all the columns (we could also have specified a
specific sub-set) and returned each measurement as a seperate row. We probably
want to seperate the measurement column into the measurement type (FA or MD) and
the tract name. Many string manipulation function are available in the
`DataFrame` object under `DataFrame.str`
([tutorial](http://pandas.pydata.org/pandas-docs/stable/text.html))
%% Cell type:code id: tags:
```
df_long['variable'] = df_long.measurement.str.slice(0, 2) # first two letters correspond to FA or MD
df_long['tract'] = df_long.measurement.str.slice(3, -1) # fourth till the second-to-last letter correspond to the tract
df_long.head(12)
```
%% Cell type:markdown id: tags:
Finally we probably do want the FA and MD variables as different columns.
**Exercise**: Use `pivot_table` or `stack`/`unstack` to create a column for MD
and FA.
%% Cell type:code id: tags:
```
df_unstacked = df_long.
```
%% Cell type:markdown id: tags:
We can now use the tools discussed above to visualize the table (`seaborn`) or
to group the table based on tract (`groupby` or `pivot_table`).
%% Cell type:code id: tags:
```
# feel free to analyze this random data in more detail
```
%% Cell type:markdown id: tags:
In general pandas is better at handling long-form than wide-form data, although
for better visualization of the data an intermediate format is often best. One
exception is calculating a covariance (`DataFrame.cov`) or correlation
(`DataFrame.corr`) matrices which computes the correlation between each column:
%% Cell type:code id: tags:
```
sns.heatmap(df_wide.corr(), cmap=sns.diverging_palette(240, 10, s=99, n=300), )
```
%% Cell type:markdown id: tags:
## Linear fitting (`statsmodels`)
Linear fitting between the different columns is available through the
[`statsmodels`](https://www.statsmodels.org/stable/index.html) library. A nice
way to play around with a wide variety of possible models is to use R-style
functions. The usage of the functions in `statsmodels` is described
[here](https://www.statsmodels.org/dev/example_formulas.html). You can find a
more detailed description of the R-style functions
[here](https://patsy.readthedocs.io/en/latest/formulas.html#the-formula-
language).
In short these functions describe the linear model as a string. For example,
`"y ~ x + a + x * a"` fits the variable `y` as a function of `x`, `a`, and the
interaction between `x` and `a`. The intercept is included by default (you can
add `"+ 0"` to remove it).
%% Cell type:code id: tags:
```
result = smf.logit('survived ~ age + sex + age * sex', data=titanic).fit()
print(result.summary())
```
%% Cell type:markdown id: tags:
Note that `statsmodels` understands categorical variables and automatically
replaces them with dummy variables.
Above we used logistic regression, which is appropriate for the binary
survival rate. A wide variety of linear models are available. Let's try a GLM,
but assume that the fare is drawn from a Gamma distribution:
%% Cell type:code id: tags:
```
age_dmean = titanic.age - titanic.age.mean()
result = smf.glm('fare ~ age_dmean + embark_town', data=titanic).fit()
print(result.summary())
```
%% Cell type:markdown id: tags:
Cherbourg passengers clearly paid a lot more...
Note that we did not actually add the `age_dmean` to the
`DataFrame`. `statsmodels` (or more precisely the underlying
[patsy](https://patsy.readthedocs.io/en/latest/) library) automatically
extracted this from our environment. This can lead to confusing behaviour...
# More reading
Other useful features
- [Concatenating](https://jakevdp.github.io/PythonDataScienceHandbook/03.06-concat-and-append.html)
and
[merging](https://jakevdp.github.io/PythonDataScienceHandbook/03.07-merge-and-join.html)
of tables
- [Lots
of](http://pandas.pydata.org/pandas-docs/stable/basics.html#dt-accessor)
[time](http://pandas.pydata.org/pandas-docs/stable/timeseries.html)
[series](http://pandas.pydata.org/pandas-docs/stable/timedeltas.html)
support
- [Concatenating and merging tables](https://pandas.pydata.org/pandas-docs/stable/getting_started/intro_tutorials/08_combine_dataframes.html)
- [Lots of time series support](https://pandas.pydata.org/pandas-docs/stable/getting_started/intro_tutorials/09_timeseries.html)
- [Rolling Window
functions](http://pandas.pydata.org/pandas-docs/stable/computation.html#window-
functions) for after you have meaningfully sorted your data
- and much, much more
......
# Pandas
Follow along online at: https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020/-/blob/master/talks/pandas/pandas.ipynb
Pandas is a data analysis library focused on the cleaning and exploration of
tabular data.
......@@ -8,6 +10,7 @@ Some useful links are:
- [documentation](http://pandas.pydata.org/pandas-docs/stable/)<sup>1</sup>
- [Python Data Science Handbook](https://jakevdp.github.io/PythonDataScienceHandbook/)<sup>1</sup> by
Jake van der Plas
- [List of Pandas tutorials](https://pandas.pydata.org/pandas-docs/stable/getting_started/tutorials.html)
<sup>1</sup> This tutorial borrows heavily from the pandas documentation and
the Python Data Science Handbook
......@@ -45,7 +48,7 @@ represents a table of data. The other file formats all start with
`pd.read_{format}`. Note that we can provide the URL to the dataset, rather
than download it beforehand.
We can write out the dataset using `dataframe.to_{format}(<filename)`:
We can write out the dataset using `dataframe.to_{format}(<filename>)`:
```
titanic.to_csv('titanic_copy.csv', index=False) # we set index to False to prevent pandas from storing the row names
......@@ -55,7 +58,7 @@ If you can not connect to the internet, you can run the command below to load
this locally stored titanic dataset
```
titanic = pd.read_csv('09_pandas/titanic.csv')
titanic = pd.read_csv('titanic.csv')
titanic
```
......@@ -82,10 +85,10 @@ pd.DataFrame.from_dict({
For many applications (e.g., ICA, machine learning input) you might want to
extract your data as a numpy array. The underlying numpy array can be accessed
using the `values` attribute
using the `to_numpy` method
```
titanic.values
titanic.to_numpy()
```
Note that the type of the returned array is the most common type (in this case
......@@ -93,7 +96,7 @@ object). If you just want the numeric parts of the table you can use
`select_dtypes`, which selects specific columns based on their dtype:
```
titanic.select_dtypes(include=np.number).values
titanic.select_dtypes(include=np.number).to_numpy()
```
Note that the numpy array has no information on the column names or row indices.
......@@ -234,6 +237,12 @@ a name that cannot be used as a Python identifier.
titanic.query('(age > 60) & (embark_town == "Southampton")')
```
When selecting a categorical multiple options from a categorical values you
might want to use `isin`:
```
titanic[titanic['class'].isin(['First','Second'])]
```
Particularly useful when selecting data like this is the `isna` method which
finds all missing data
......@@ -272,7 +281,11 @@ titanic.fare.hist(bins=20, log=True)
titanic.age.plot()
```
Individual columns are essentially 1D arrays, so we can use them as such in
To plot all variables simply call `plot` or `hist` on the full dataframe
rather than a single Series (i.e., column). You might want to set `subplots=True`
to plot each variable in a different subplot.
Individual Series are essentially 1D arrays, so we can use them as such in
`matplotlib`
```
......@@ -351,6 +364,16 @@ summary measures
titanic.describe()
```
For a more detailed exploration of the data, you might want to check
[pandas_profiliing](https://pandas-profiling.github.io/pandas-profiling/docs/)
(not installed in fslpython, so the following will not run in fslpython):
```
from pandas_profiling import ProfileReport
profile = ProfileReport(titanic, title='Titanic Report', html={'style':{'full_width':True}})
profile.to_widgets()
```
Note that non-numeric columns are ignored when summarizing data in this way.
We can also define our own functions to apply to the columns (in this case we
......@@ -394,7 +417,7 @@ However, it is more often combined with one of the aggregation functions
discussed above as illustrated in this figure from the [Python data science
handbook](https://jakevdp.github.io/PythonDataScienceHandbook/06.00-figure-code.html#Split-Apply-Combine)
![group by image](09_pandas/group_by.png)
![group by image](group_by.png)
```
titanic.groupby('class').mean()
......@@ -643,15 +666,8 @@ extracted this from our environment. This can lead to confusing behaviour...
Other useful features
- [Concatenating](https://jakevdp.github.io/PythonDataScienceHandbook/03.06-concat-and-append.html)
and
[merging](https://jakevdp.github.io/PythonDataScienceHandbook/03.07-merge-and-join.html)
of tables
- [Lots
of](http://pandas.pydata.org/pandas-docs/stable/basics.html#dt-accessor)
[time](http://pandas.pydata.org/pandas-docs/stable/timeseries.html)
[series](http://pandas.pydata.org/pandas-docs/stable/timedeltas.html)
support
- [Concatenating and merging tables](https://pandas.pydata.org/pandas-docs/stable/getting_started/intro_tutorials/08_combine_dataframes.html)
- [Lots of time series support](https://pandas.pydata.org/pandas-docs/stable/getting_started/intro_tutorials/09_timeseries.html)
- [Rolling Window
functions](http://pandas.pydata.org/pandas-docs/stable/computation.html#window-
functions) for after you have meaningfully sorted your data
......
% The purpose of this comment is purely
% to make life difficult for you. Sorry
% about that. I'm not really sorry.
128.3 100.8 67.2 120.1 150.2 53.0 64.2 139.3 46.7 118.1 125.8 153.1 83.2 115.9 3.4 126.3 92.2 104.7 131.2 29.3
89.3 118.8 119.2 80.1 121.2 35.0 66.2 153.3 43.7 102.1 147.8 160.1 94.2 140.9 70.4 124.3 124.2 93.7 100.2 32.3
91.3 146.8 137.2 129.1 157.2 -22.0 75.2 90.3 33.7 86.1 74.8 90.1 114.2 81.9 72.4 135.3 85.2 102.7 58.2 55.3
75.3 87.8 84.2 112.1 131.2 57.0 150.2 73.3 41.7 110.1 78.8 102.1 116.2 134.9 34.4 74.3 150.2 52.7 89.2 21.3
79.3 83.8 78.2 103.1 96.2 26.0 151.2 122.3 46.7 113.1 143.8 80.1 61.2 136.9 8.4 94.3 76.2 123.7 150.2 10.3
93.3 157.8 154.2 79.1 155.2 8.0 63.2 120.3 12.7 130.1 105.8 111.1 66.2 99.9 54.4 120.3 113.2 128.7 110.2 46.3
111.3 126.8 65.2 90.1 101.2 36.0 116.2 139.3 36.7 91.1 88.8 94.1 90.2 92.9 -0.6 46.3 80.2 105.7 88.2 -11.7
149.3 119.8 134.2 124.1 89.2 21.0 133.2 113.3 28.7 139.1 136.8 82.1 141.2 90.9 11.4 97.3 109.2 137.7 120.2 32.3
141.3 149.8 115.2 115.1 105.2 51.0 77.2 130.3 -4.3 124.1 141.8 162.1 95.2 130.9 -10.6 114.3 132.2 134.7 79.2 69.3
141.3 77.8 135.2 167.1 103.2 55.0 153.2 68.3 33.7 136.1 125.8 85.1 148.2 114.9 76.4 57.3 147.2 125.7 153.2 45.3
13,7,3,18,15,3,12,11,9,9
13,1,7,17,16,13,18,9,18,6
9,19,16,3,18,3,19,12,9,6
2,11,6,12,2,11,15,9,3,9
2,12,1,7,4,3,6,6,2,4
10,8,14,1,17,19,8,19,2,9
18,14,2,11,17,14,6,16,14,18
6,8,13,16,11,17,16,5,16,15
6,5,4,18,6,14,19,8,4,15
15,17,12,10,17,12,5,9,18,6
Congratulations, you have read your first file from Python!
1 3
14 3
4 7
12 5
15 5
14 4
15 19
7 17
7 15
16 3
15 14
17 9
7 8
4 5
15 4
11 5
17 10
3 19
14 4
18 3
%% Cell type:markdown id: tags:
# Welcome to the WIN Virtual Mini PyTreat 2020!
This notebook is available at:
https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020/-/tree/master/talks%2Fvirtual_intro/intro.ipynb
If you have FSL installed and you'd like to follow along *interactively*,
follow the instructions for attendees in the `README.md` file of the above
repository, and then open the `talks/virtual_intro/intro.ipynb` notebook.
# Contents
* [Introduction](#introduction)
* [Python in a nutshell](#python-in-a-nutshell)
* [Different ways of running Python](#different-ways-of-running-python)
* [Variables and basic types](#variables-and-basic-types)
* [Integer and floating point scalars](#integer-and-floating-point-scalars)
* [Strings](#strings)
* [Lists and tuples](#lists-and-tuples)
* [Dictionaries](#dictionaries)
* [A note on mutablility](#a-note-on-mutablility)
* [Flow control](#flow-control)
* [List comprehensions](#list-comprehensions)
* [Reading and writing text files](#reading-and-writing-text-files)
* [Example: processing lesion counts](#example-processing-lesion-counts)
* [Functions](#functions)
* [Working with `numpy`](#working-with-numpy)
* [The Python list versus the `numpy` array](#the-python-list-versus-the-numpy-array)
* [Creating arrays](#creating-arrays)
* [Example: reading arrays from text files](#example-reading-arrays-from-text-files)
<a class="anchor" id="introduction"></a>
# Introduction
This talk is an attempt to give a whirlwind overview of the Python programming
language. It is assumed that you have experience with another programming
language (e.g. MATLAB).
This talk is presented as an interactive [Jupyter
Notebook](https://jupyter.org/) - you can run all of the code on your own
machine - click on a code block, and press **SHIFT+ENTER**. You can also "run"
the text sections, so you can just move down the document by pressing
**SHIFT+ENTER**.
It is also possible to *change* the contents of each code block (these pages
are completely interactive) so do experiment with the code you see and try
some variations!
You can get help on any Python object, function, or method by putting a `?`
before or after the thing you want help on:
%% Cell type:code id: tags:
```
a = 'hello!'
?a.upper
```
%% Cell type:markdown id: tags:
And you can explore the available methods on a Python object by using the
**TAB** key:
%% Cell type:code id: tags:
```
# Put the cursor after the dot, and press the TAB key...
a.
```
%% Cell type:markdown id: tags:
<a class="anchor" id="python-in-a-nutshell"></a>
## Python in a nutshell
**Pros**
* _Flexible_ Feel free to use functions, classes, objects, modules and
packages. Or don't - it's up to you!
* _Fast_ If you do things right (in other words, if you use `numpy`)
* _Dynamically typed_ No need to declare your variables, or specify their
types.
* _Intuitive syntax_ How do I run some code for each of the elements in my
list?
%% Cell type:code id: tags:
```
mylist = [1, 2, 3, 4, 5]
for element in mylist:
print(element)
```
%% Cell type:markdown id: tags:
**Cons**
* _Dynamically typed_ Easier to make mistakes, harder to catch them
* _No compiler_ See above
* _Slow_ if you don't do things the right way
* _Python 2 is not the same as Python 3_ But there's an easy solution: Forget
that Python 2 exists.
* _Hard to manage different versions of python_ But we have a solution for
you: `fslpython`.
Python is a widely used language, so you can get lots of help through google
and [stackoverflow](https://stackoverflow.com). But make sure that the
information you find is for **Python 3**, and **not** for **Python 2**!
Python 2 is obsolete, but is still used by many organisations, so you will
inevitably come across many Python 2 resources.
The differences between Python 2 and 3 are small, but important. The most
visible difference is in the `print` function: in Python 3, we write
`print('hello!')`, but in Python 2, we would write `print 'hello!'`.
FSL 5.0.10 and newer comes with its own version of Python, bundled with nearly
all of the scientific libraries that you are likely to need.
So if you use `fslpython` for all of your development, you can be sure that it
will work in FSL!
<a class="anchor" id="different-ways-of-running-python"></a>
## Different ways of running Python
Many of the Pytreat talks and practicals are presented as *Jupyter notebooks*,
which is a way of running python code in a web browser.
Jupyter notebooks are good for presentations and practicals, and some people
find them very useful for exploratory data analysis. But they're not the only
way of running Python code.
**Run Python from a file**
This works just like it does in MATLAB:
1. Put your code in a `.py` file (e.g. `mycode.py`).
2. Run `fslpython mycode.py` in a terminal.
3. ??
4. Profit.
**Run python in an interpreter**
Python is an [*interpreted
language*](https://en.wikipedia.org/wiki/Interpreted_language), like MATLAB.
So you can either write your code into a file, and then run that file, or you
can type code directly into a Python interpreter.
Python has a standard interpreter built-in - run `fslpython` in a terminal,
and see what happens (use CTRL+D to exit).
**But** there is another interpreter called [IPython](https://ipython.org/)
which is vastly superior to the standard Python interpreter. Use IPython
instead! It is already installed in `fslpython`, so if you want to do some
interactive work, you can use `fslipython` in a terminal.
<a class="anchor" id="variables-and-basic-types"></a>
# Variables and basic types
There are many different types of values in Python. Python *variables* do not
have a type though - a variable can refer to values of any type, and a
variable can be updated to refer to different values (of different
types). This is just like how things work in MATLAB.
<a class="anchor" id="integer-and-floating-point-scalars"></a>
## Integer and floating point scalars
%% Cell type:code id: tags:
```
a = 7
b = 1 / 3
c = a + b
print('a: ', a)
print('b: ', b)
print('c: ', c)
print('b: {:0.4f}'.format(b))
print('a + b:', a + b)
```
%% Cell type:markdown id: tags:
<a class="anchor" id="strings)"></a>
## Strings
%% Cell type:code id: tags:
```
a = 'Hello'
b = "Kitty"
c = '''
Magic
multi-line
strings!
'''
print(a, b)
print(a + b)
print('{}, {}!'.format(a, b))
print(c)
```
%% Cell type:markdown id: tags:
String objects have a number of useful methods:
%% Cell type:code id: tags:
```
s = 'This is a Test String'
print(s.upper())
print(s.lower())
```
%% Cell type:markdown id: tags:
Another useful method is:
%% Cell type:code id: tags:
```
s = 'This is a Test String'
s2 = s.replace('Test', 'Better')
print(s2)
```
%% Cell type:markdown id: tags:
Two common and convenient string methods are `strip()` and `split()`. The
first will remove any whitespace at the beginning and end of a string:
%% Cell type:code id: tags:
```
s2 = ' A very spacy string '
print('*' + s2 + '*')
print('*' + s2.strip() + '*')
```
%% Cell type:markdown id: tags:
With `split()` we can tokenize a string (to turn it into a list of strings)
like this:
%% Cell type:code id: tags:
```
print(s.split())
print(s2.split())
```
%% Cell type:markdown id: tags:
We can also use the `join` method to re-construct a new string. Imagine that
we need to reformat some data from being comma-separated to being
space-separated:
%% Cell type:code id: tags:
```
data = ' 1,2,3,4,5,6,7 '
```
%% Cell type:markdown id: tags:
`strip`, `split` and `join` makes this job trivial:
%% Cell type:code id: tags:
```
print('Original: {}'.format(data))
print('Strip, split, and join: {}'.format(' '.join(data.strip().split(','))))
```
%% Cell type:markdown id: tags:
<a class="anchor" id="lists-and-tuples"></a>
## Lists and tuples
Both tuples and lists are built-in Python types and are like cell-arrays in
MATLAB. For numerical vectors and arrays it is much better to use *numpy*
arrays, which are covered later.
Tuples are defined using round brackets and lists are defined using square
brackets. For example:
%% Cell type:code id: tags:
```
t = (3, 7.6, 'str')
l = [1, 'mj', -5.4]
print(t)
print(l)
t2 = (t, l)
l2 = [t, l]
print('t2 is: ', t2)
print('l3 is: ', l2)
print(len(t2))
print(len(l2))
```
%% Cell type:markdown id: tags:
The key difference between lists and tuples is that tuples are *immutable*
(once created, they cannot be changed), whereas lists are *mutable*:
%% Cell type:code id: tags:
```
a = [10, 20, 30]
a[2] = 999
print(a)
```
%% Cell type:markdown id: tags:
Square brackets are used to index tuples, lists, strings, dictionaries, etc.
For example:
%% Cell type:code id: tags:
```
d = [10, 20, 30]
print(d[1])
```
%% Cell type:markdown id: tags:
> **MATLAB pitfall:** Python uses zero-based indexing, unlike MATLAB, where
> indices start from 1.
%% Cell type:code id: tags:
```
a = [10, 20, 30, 40, 50, 60]
print(a[0])
print(a[2])
```
%% Cell type:markdown id: tags:
A range of values for the indices can be specified to extract values from a
list or tuple using the `:` character. For example:
%% Cell type:code id: tags:
```
print(a[0:3])
```
%% Cell type:markdown id: tags:
> **MATLAB pitfall:** Note that Python's slicing syntax is different from
> MATLAB in that the second number is *exclusive*, i.e. `a[0:3]` gives us the
> elements of `a` at positions `0`, `1` and `2` , but *not* at position `3`.
When slicing a list or tuple, you can leave the start and end values out -
when you do this, Python will assume that you want to start slicing from the
beginning or the end of the list. For example:
%% Cell type:code id: tags:
```
print(a[:3])
print(a[1:])
print(a[:])
print(a[:-1])
```
%% Cell type:markdown id: tags:
You can also change the step size, which is specified by the third value (not
the second one, as in MATLAB). For example:
%% Cell type:code id: tags:
```
print(a[0:4:2])
print(a[::2])
print(a[::-1])
```
%% Cell type:markdown id: tags:
Some methods are available on `list` objects for adding and removing items:
%% Cell type:code id: tags:
```
print(d)
d.append(40)
print(d)
d.extend([50, 60])
print(d)
d = d + [70, 80]
print(d)
d.remove(20)
print(d)
d.pop(0)
print(d)
```
%% Cell type:markdown id: tags:
What will `d.append([50,60])` do, and how is it different from
`d.extend([50,60])`?
%% Cell type:code id: tags:
```
d.append([50, 60])
print(d)
```
%% Cell type:markdown id: tags:
<a class="anchor" id="dictionaries"></a>
## Dictionaries
Dictionaries (or *dicts*) can be used to store key-value pairs. Almost
anything can used as a key, and anything can be stored as a value; it is
common to use strings as keys:
%% Cell type:code id: tags:
```
e = {'a' : 10, 'b': 20}
print(len(e))
print(e.keys())
print(e.values())
print(e['a'])
```
%% Cell type:markdown id: tags:
Like lists (and unlike tuples), dicts are mutable, and have a number of
methods for manipulating them:
%% Cell type:code id: tags:
```
e['c'] = 30
e.pop('a')
e.update({'a' : 100, 'd' : 400})
print(e)
e.clear()
print(e)
```
%% Cell type:markdown id: tags:
<a class="anchor" id="a-note-on-mutability"></a>
## A note on mutablility
Python variables can refer to values which are either mutable, or
immutable. Examples of immutable values are strings, tuples, and integer and
floating point scalars. Examples of mutable values are lists, dicts, and most
user-defined types.
When you pass an immutable value around (e.g. into a function, or to another
variable), it works the same as if you were to copy the value and pass in the
copy - the original value is not changed:
%% Cell type:code id: tags:
```
a = 'abcde'
b = a
b = b.upper()
print('a:', a)
print('b:', b)
```
%% Cell type:markdown id: tags:
In contrast, when you pass a mutable value around, you are passing a
*reference* to that value - there is only ever one value in existence, but
multiple variables refer to it. You can manipulate the value through any of
the variables that refer to it:
%% Cell type:code id: tags:
```
a = [1, 2, 3, 4, 5]
b = a
a[3] = 999
b.append(6)
print('a', a)
print('b', b)
```
%% Cell type:markdown id: tags:
<a class="anchor" id="flow-control"></a>
# Flow control
Python also has a boolean type which can be either `True` or `False`. Most
Python types can be implicitly converted into booleans when used in a
conditional expression.
Relevant boolean and comparison operators include: `not`, `and`, `or`, `==`
and `!=`
For example:
%% Cell type:code id: tags:
```
a = True
b = False
print('Not a is:', not a)
print('a or b is:', a or b)
print('a and b is:', a and b)
print('Not 1 is:', not 1)
print('Not 0 is:', not 0)
print('Not {} is:', not {})
print('{}==0 is:', {}==0)
```
%% Cell type:markdown id: tags:
There is also the `in` test for strings, lists, etc:
%% Cell type:code id: tags:
```
print('the' in 'a number of words')
print('of' in 'a number of words')
print(3 in [1, 2, 3, 4])
```
%% Cell type:markdown id: tags:
We can use boolean values in `if`-`else` conditional expressions:
%% Cell type:code id: tags:
```
a = [1, 2, 3, 4]
val = 3
if val in a:
print('Found {}!'.format(val))
else:
print('{} not found :('.format(val))
```
%% Cell type:markdown id: tags:
Note that the indentation in the `if`-`else` statement is **crucial**.
**All** python control blocks are delineated purely by indentation. We
recommend using **four spaces** and no tabs, as this is a standard practice
and will help a lot when collaborating with others.
You can use the `for` statement to loop over elements in a list:
%% Cell type:code id: tags:
```
d = [10, 20, 30]
for x in d:
print(x)
```
%% Cell type:markdown id: tags:
You can also loop over the key-value pairs in a dict:
%% Cell type:code id: tags:
```
a = {'a' : 10, 'b' : 20, 'c' : 30}
print('a.items()')
for key, val in a.items():
print(key, val)
print('a.keys()')
for key in a.keys():
print(key, a[key])
print('a.values()')
for val in a.values():
print(val)
```
%% Cell type:markdown id: tags:
> In older versions of Python 3, there was no guarantee of ordering when using dictionaries.
> However, a of Python 3.7, dictionaries will remember the order in which items are inserted,
> and the `keys()`, `values()`, and `items()` methods will return elements in that order.
>
> If you want a dictionary with ordering, *and* you want your code to work with
> Python versions older than 3.7, you can use the
> [`OrderedDict`](https://docs.python.org/3/library/collections.html#collections.OrderedDict)
> class.
There are some handy built-in functions that you can use with `for` loops:
%% Cell type:code id: tags:
```
d = [10, 20, 30]
print('Using the range function')
for i in range(len(d)):
print('element at position {}: {}'.format(i, d[i]))
print('Using the enumerate function')
for i, elem in enumerate(d):
print('element at position {}: {}'.format(i, elem))
```
%% Cell type:markdown id: tags:
<a class="anchor" id=" list-comprehensions"></a>
## List comprehensions
Python has a really neat way to create lists (and dicts), called
*comprehensions*. Let's say we have some strings, and we want to count the
number of characters in each of them:
%% Cell type:code id: tags:
```
strings = ['hello', 'howdy', 'hi', 'hey']
nchars = [len(s) for s in strings]
for s, c in zip(strings, nchars):
print('{}: {}'.format(s, c))
```
%% Cell type:markdown id: tags:
> The `zip` function "zips" two or more sequences, so you can loop over them
> together.
Or we could store the character counts in a dict:
%% Cell type:code id: tags:
```
nchars = { s : len(s) for s in strings }
for s, c in nchars.items():
print('{}: {}'.format(s, c))
```
%% Cell type:markdown id: tags:
<a class="anchor" id="reading-and-writing-text-files"></a>
# Reading and writing text files
The syntax to open a file in python is
`with open(<filename>, <mode>) as <file_object>: <block of code>`, where
* `filename` is a string with the name of the file
* `mode` is one of 'r' (for read-only access), 'w' (for writing a file, this
wipes out any existing content), 'a' (for appending to an existing file).
* `file_object` is a variable name which will be used within the `block of
code` to access the opened file.
For example the following will read all the text in `data/file.txt` and print
it:
%% Cell type:code id: tags:
```
with open('data/file.txt', 'r') as f:
print(f.read())
```
%% Cell type:markdown id: tags:
A very similar syntax is used to write files:
%% Cell type:code id: tags:
```
with open('new_file.txt', 'w') as f:
f.write('This is my first line\n')
f.writelines(['Second line\n', 'and the third\n'])
```
%% Cell type:markdown id: tags:
<a class="anchor" id="example-processing-lesion-counts"></a>
## Example: processing lesion counts
Imagine that we have written an amazing algorithm in Python which
automatically counts the number of lesions in an individual's structural MRI
image.
%% Cell type:code id: tags:
```
subject_ids = ['01', '07', '21', '32']
lesion_counts = [ 4, 9, 13, 2]
```
%% Cell type:markdown id: tags:
We may wish to process this data in another application (e.g. Excel or SPSS).
Let's save the results out to a CSV (comma-separated value) file:
%% Cell type:code id: tags:
```
with open('lesion_counts.csv', 'w') as f:
f.write('Subject ID, Lesion count\n')
for subj_id, count in zip(subject_ids, lesion_counts):
f.write('{}, {}\n'.format(subj_id, count))
```
%% Cell type:markdown id: tags:
We can now load the `lesion_counts.csv` file into our analysis software of
choice. Or we could load it back into another Python session, and store
the data in a dict:
%% Cell type:code id: tags:
```
lesion_counts = {}
with open('lesion_counts.csv', 'r') as f:
# skip the header
f.readline()
for line in f.readlines():
subj_id, count = line.split(',')
lesion_counts[subj_id] = int(count)
print('Loaded lesion counts:')
for subj, count in lesion_counts.items():
print('{}: {}'.format(subj, count))
```
%% Cell type:markdown id: tags:
<a class="anchor" id="functions"></a>
## Functions
You will find functions pretty familiar in python to start with, although they
have a few options which are really handy and different from C++ or matlab (to
be covered in a later practical). To start with we'll look at a simple
function but note a few key points:
* you *must* indent everything inside the function (it is a code block and
indentation is the only way of determining this - just like for the guts of a
loop)
* you can return *whatever you want* from a python function, but only a single
object - it is usual to package up multiple things in a tuple or list, which
is easily unpacked by the calling invocation: e.g., `a, b, c = myfunc(x)`
* parameters are passed by *reference* (more on this below)
%% Cell type:code id: tags:
```
def myfunc(x, y, z=0):
r2 = x*x + y*y + z*z
r = r2**0.5
return r, r2
rad = myfunc(10, 20)
print(rad)
rad, dummy = myfunc(10, 20, 30)
print(rad)
rad, _ = myfunc(10,20,30)
print(rad)
```
%% Cell type:markdown id: tags:
> Note that the `_` is used as shorthand here for a dummy variable
> that you want to throw away.
>
> The return statement implicitly creates a tuple to return and is equivalent
> to `return (r, r2)`
One nice feature of python functions is that you can name the arguments when
you call them, rather than only doing it by position. For example:
%% Cell type:code id: tags:
```
def myfunc(x, y, z=0, flag=''):
if flag=='L1':
r = abs(x) + abs(y) + abs(z)
else:
r = (x*x + y*y + z*z)**0.5
return r
rA = myfunc(10, 20)
rB = myfunc(10, 20, flag='L1')
rC = myfunc(10, 20, flag='L1', z=30)
print(rA, rB, rC)
```
%% Cell type:markdown id: tags:
You will often see python functions called with these named arguments. In
fact, for functions with more than 2 or 3 variables this naming of arguments
is recommended, because it clarifies what each of the arguments does for
anyone reading the code.
Arguments passed into a python function are *passed by reference* - this is
where the difference between *mutable* and *immutable* types becomes
important - if you pass a mutable object into a function, the function
might change it!
%% Cell type:code id: tags:
```
def changelist(l):
l[0] = 'mwahahaha!'
mylist = [1,2,3,4,5]
print('before:', mylist)
changelist(mylist)
print('after:', mylist)
mytup = [1,2,3,4,5]
changelist(mytup)
```
%% Cell type:markdown id: tags:
<a class="anchor" id="working-with-numpy"></a>
# Working with `numpy`
This section introduces you to [`numpy`](http://www.numpy.org/), Python's
numerical computing library. Numpy adds a new data type to the Python
language - the `array` (more specifically, the `ndarray`). A Numpy `array`
is a N-dimensional array of homogeneously-typed numerical data.
Pretty much every scientific computing library in Python is built on top of
Numpy - whenever you want to access some data, you will be accessing it in the
form of a Numpy array. So it is worth getting to know the basics.
<a class="anchor" id="the-python-list-versus-the-numpy-array"></a>
## The Python list versus the `numpy` array
You have already been introduced to the Python `list`, which you can easily
use to store a handful of numbers (or anything else):
%% Cell type:code id: tags:
```
data = [10, 8, 12, 14, 7, 6, 11]
```
%% Cell type:markdown id: tags:
You could also emulate a 2D or ND matrix by using lists of lists, for example:
%% Cell type:code id: tags:
```
xyz_coords = [[-11.4, 1.0, 22.6],
[ 22.7, -32.8, 19.1],
[ 62.8, -18.2, -34.5]]
```
%% Cell type:markdown id: tags:
For simple tasks, you could stick with processing your data using python
lists, and the built-in
[`math`](https://docs.python.org/3.5/library/math.html) library. And this
might be tempting, because it does look quite a lot like what you might type
into Matlab.
But **BEWARE!** A Python list is a terrible data structure for scientific
computing!
This is a major source of confusion for people who are learning Python, and
are trying to write efficient code. It is _crucial_ to be able to distinguish
between a Python list and a Numpy array.
**Python list == Matlab cell array:** A list in Python is akin to a cell
array in Matlab - they can store anything, but are extremely inefficient, and
unwieldy when you have more than a couple of dimensions.
**Numpy array == Matlab matrix:** These are in contrast to the Numpy array
and Matlab matrix, which are both thin wrappers around a contiguous chunk of
memory, and which provide blazing-fast performance (because behind the scenes
in both Numpy and Matlab, it's C, C++ and FORTRAN all the way down).
So you should strongly consider turning those lists into Numpy arrays:
%% Cell type:code id: tags:
```
import numpy as np
data = np.array([10, 8, 12, 14, 7, 6, 11])
xyz_coords = np.array([[-11.4, 1.0, 22.6],
[ 22.7, -32.8, 19.1],
[ 62.8, -18.2, -34.5]])
print('data: ', data)
print('xyz_coords: ', xyz_coords)
print('data.shape: ', data.shape)
print('xyz_coords.shape:', xyz_coords.shape)
```
%% Cell type:markdown id: tags:
> Numpy is not a "built-in" library, so we have to import it. The statement
> `import numpy as np` tells Python to *Import the `numpy` library, and make
> it available as a variable called `np`.*
<a class="anchor" id="creating-arrays"></a>
## Creating arrays
Numpy has quite a few functions which behave similarly to their equivalents in
Matlab:
%% Cell type:code id: tags:
```
print('np.zeros gives us zeros: ', np.zeros(5))
print('np.ones gives us ones: ', np.ones(5))
print('np.arange gives us a range: ', np.arange(5))
print('np.linspace gives us N linearly spaced numbers:', np.linspace(0, 1, 5))
print('np.random.random gives us random numbers [0-1]:', np.random.random(5))
print('np.random.randint gives us random integers: ', np.random.randint(1, 10, 5))
print('np.eye gives us an identity matrix:')
print(np.eye(4))
print('np.diag gives us a diagonal matrix:')
print(np.diag([1, 2, 3, 4]))
```
%% Cell type:markdown id: tags:
The `zeros` and `ones` functions can also be used to generate N-dimensional
arrays:
%% Cell type:code id: tags:
```
z = np.zeros((3, 4))
o = np.ones((2, 10))
print(z)
print(o)
```
%% Cell type:markdown id: tags:
> Note that, in a 2D Numpy array, the first axis corresponds to rows, and the
> second to columns - just like in Matlab.
> **MATLAB pitfall:** Arithmetic operations on arrays in Numpy work on an
> *elementwise* basis. In particular, if you multiply two arrays together,
> you will get the elementwise product. You **won't** get the dot product,
> like you would in MATLAB. You can, however, use the `@` operator to perform
> matrix multiplication on numpy arrays.
<a class="anchor" id="example-reading-arrays-from-text-files"></a>
## Example: reading arrays from text files
The `numpy.loadtxt` function is capable of loading numerical data from
plain-text files. By default it expects space-separated data:
%% Cell type:code id: tags:
```
data = np.loadtxt('data/space_separated.txt')
print('data in data/space_separated.txt:')
print(data)
```
%% Cell type:markdown id: tags:
But you can also specify the delimiter to expect<sup>1</sup>:
%% Cell type:code id: tags:
```
data = np.loadtxt('data/comma_separated.txt', delimiter=',')
print('data in data/comma_separated.txt:')
print(data)
```
%% Cell type:markdown id: tags:
> <sup>1</sup> And many other things such as file headers, footers, comments,
> and newline characters - see the
> [docs](https://docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt.html)
> for more information.
Of course you can also save data out to a text file just as easily, with
[`numpy.savetxt`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html):
%% Cell type:code id: tags:
```
data = np.random.randint(1, 10, (10, 10))
np.savetxt('mydata.txt', data, delimiter=',', fmt='%i')
```
%% Cell type:markdown id: tags:
Jupyter notebooks have a special feature - if you start a line with a `!`
character, you can run a `bash` command. Let's look at the file we just
generated:
%% Cell type:code id: tags:
```
!cat mydata.txt
```
%% Cell type:markdown id: tags:
> The `!` feature won't work in regular Python scripts.
Here's how we can load a 2D array fom a file, and calculate the mean of each
column:
%% Cell type:code id: tags:
```
data = np.loadtxt('data/2d_array.txt', comments='%')
colmeans = data.mean(axis=0)
print('Column means')
print('\n'.join(['{}: {:0.2f}'.format(i, m) for i, m in enumerate(colmeans)]))
```
# Welcome to the WIN Virtual Mini PyTreat 2020!
This notebook is available at:
https://git.fmrib.ox.ac.uk/fsl/pytreat-practicals-2020/-/tree/master/talks%2Fvirtual_intro/intro.ipynb
If you have FSL installed and you'd like to follow along *interactively*,
follow the instructions for attendees in the `README.md` file of the above
repository, and then open the `talks/virtual_intro/intro.ipynb` notebook.
# Contents
* [Introduction](#introduction)
* [Python in a nutshell](#python-in-a-nutshell)
* [Different ways of running Python](#different-ways-of-running-python)
* [Variables and basic types](#variables-and-basic-types)
* [Integer and floating point scalars](#integer-and-floating-point-scalars)
* [Strings](#strings)
* [Lists and tuples](#lists-and-tuples)
* [Dictionaries](#dictionaries)
* [A note on mutablility](#a-note-on-mutablility)
* [Flow control](#flow-control)
* [List comprehensions](#list-comprehensions)
* [Reading and writing text files](#reading-and-writing-text-files)
* [Example: processing lesion counts](#example-processing-lesion-counts)
* [Functions](#functions)
* [Working with `numpy`](#working-with-numpy)
* [The Python list versus the `numpy` array](#the-python-list-versus-the-numpy-array)
* [Creating arrays](#creating-arrays)
* [Example: reading arrays from text files](#example-reading-arrays-from-text-files)
<a class="anchor" id="introduction"></a>
# Introduction
This talk is an attempt to give a whirlwind overview of the Python programming
language. It is assumed that you have experience with another programming
language (e.g. MATLAB).
This talk is presented as an interactive [Jupyter
Notebook](https://jupyter.org/) - you can run all of the code on your own
machine - click on a code block, and press **SHIFT+ENTER**. You can also "run"
the text sections, so you can just move down the document by pressing
**SHIFT+ENTER**.
It is also possible to *change* the contents of each code block (these pages
are completely interactive) so do experiment with the code you see and try
some variations!
You can get help on any Python object, function, or method by putting a `?`
before or after the thing you want help on:
```
a = 'hello!'
?a.upper
```
And you can explore the available methods on a Python object by using the
**TAB** key:
```
# Put the cursor after the dot, and press the TAB key...
a.
```
<a class="anchor" id="python-in-a-nutshell"></a>
## Python in a nutshell
**Pros**
* _Flexible_ Feel free to use functions, classes, objects, modules and
packages. Or don't - it's up to you!
* _Fast_ If you do things right (in other words, if you use `numpy`)
* _Dynamically typed_ No need to declare your variables, or specify their
types.
* _Intuitive syntax_ How do I run some code for each of the elements in my
list?
```
mylist = [1, 2, 3, 4, 5]
for element in mylist:
print(element)
```
**Cons**
* _Dynamically typed_ Easier to make mistakes, harder to catch them
* _No compiler_ See above
* _Slow_ if you don't do things the right way
* _Python 2 is not the same as Python 3_ But there's an easy solution: Forget
that Python 2 exists.
* _Hard to manage different versions of python_ But we have a solution for
you: `fslpython`.
Python is a widely used language, so you can get lots of help through google
and [stackoverflow](https://stackoverflow.com). But make sure that the
information you find is for **Python 3**, and **not** for **Python 2**!
Python 2 is obsolete, but is still used by many organisations, so you will
inevitably come across many Python 2 resources.
The differences between Python 2 and 3 are small, but important. The most
visible difference is in the `print` function: in Python 3, we write
`print('hello!')`, but in Python 2, we would write `print 'hello!'`.
FSL 5.0.10 and newer comes with its own version of Python, bundled with nearly
all of the scientific libraries that you are likely to need.
So if you use `fslpython` for all of your development, you can be sure that it
will work in FSL!
<a class="anchor" id="different-ways-of-running-python"></a>
## Different ways of running Python
Many of the Pytreat talks and practicals are presented as *Jupyter notebooks*,
which is a way of running python code in a web browser.
Jupyter notebooks are good for presentations and practicals, and some people
find them very useful for exploratory data analysis. But they're not the only
way of running Python code.
**Run Python from a file**
This works just like it does in MATLAB:
1. Put your code in a `.py` file (e.g. `mycode.py`).
2. Run `fslpython mycode.py` in a terminal.
3. ??
4. Profit.
**Run python in an interpreter**
Python is an [*interpreted
language*](https://en.wikipedia.org/wiki/Interpreted_language), like MATLAB.
So you can either write your code into a file, and then run that file, or you
can type code directly into a Python interpreter.
Python has a standard interpreter built-in - run `fslpython` in a terminal,
and see what happens (use CTRL+D to exit).
**But** there is another interpreter called [IPython](https://ipython.org/)
which is vastly superior to the standard Python interpreter. Use IPython
instead! It is already installed in `fslpython`, so if you want to do some
interactive work, you can use `fslipython` in a terminal.
<a class="anchor" id="variables-and-basic-types"></a>
# Variables and basic types
There are many different types of values in Python. Python *variables* do not
have a type though - a variable can refer to values of any type, and a
variable can be updated to refer to different values (of different
types). This is just like how things work in MATLAB.
<a class="anchor" id="integer-and-floating-point-scalars"></a>
## Integer and floating point scalars
```
a = 7
b = 1 / 3
c = a + b
print('a: ', a)
print('b: ', b)
print('c: ', c)
print('b: {:0.4f}'.format(b))
print('a + b:', a + b)
```
<a class="anchor" id="strings)"></a>
## Strings
```
a = 'Hello'
b = "Kitty"
c = '''
Magic
multi-line
strings!
'''
print(a, b)
print(a + b)
print('{}, {}!'.format(a, b))
print(c)
```
String objects have a number of useful methods:
```
s = 'This is a Test String'
print(s.upper())
print(s.lower())
```
Another useful method is:
```
s = 'This is a Test String'
s2 = s.replace('Test', 'Better')
print(s2)
```
Two common and convenient string methods are `strip()` and `split()`. The
first will remove any whitespace at the beginning and end of a string:
```
s2 = ' A very spacy string '
print('*' + s2 + '*')
print('*' + s2.strip() + '*')
```
With `split()` we can tokenize a string (to turn it into a list of strings)
like this:
```
print(s.split())
print(s2.split())
```
We can also use the `join` method to re-construct a new string. Imagine that
we need to reformat some data from being comma-separated to being
space-separated:
```
data = ' 1,2,3,4,5,6,7 '
```
`strip`, `split` and `join` makes this job trivial:
```
print('Original: {}'.format(data))
print('Strip, split, and join: {}'.format(' '.join(data.strip().split(','))))
```
<a class="anchor" id="lists-and-tuples"></a>
## Lists and tuples
Both tuples and lists are built-in Python types and are like cell-arrays in
MATLAB. For numerical vectors and arrays it is much better to use *numpy*
arrays, which are covered later.
Tuples are defined using round brackets and lists are defined using square
brackets. For example:
```
t = (3, 7.6, 'str')
l = [1, 'mj', -5.4]
print(t)
print(l)
t2 = (t, l)
l2 = [t, l]
print('t2 is: ', t2)
print('l3 is: ', l2)
print(len(t2))
print(len(l2))
```
The key difference between lists and tuples is that tuples are *immutable*
(once created, they cannot be changed), whereas lists are *mutable*:
```
a = [10, 20, 30]
a[2] = 999
print(a)
```
Square brackets are used to index tuples, lists, strings, dictionaries, etc.
For example:
```
d = [10, 20, 30]
print(d[1])
```
> **MATLAB pitfall:** Python uses zero-based indexing, unlike MATLAB, where
> indices start from 1.
```
a = [10, 20, 30, 40, 50, 60]
print(a[0])
print(a[2])
```
A range of values for the indices can be specified to extract values from a
list or tuple using the `:` character. For example:
```
print(a[0:3])
```
> **MATLAB pitfall:** Note that Python's slicing syntax is different from
> MATLAB in that the second number is *exclusive*, i.e. `a[0:3]` gives us the
> elements of `a` at positions `0`, `1` and `2` , but *not* at position `3`.
When slicing a list or tuple, you can leave the start and end values out -
when you do this, Python will assume that you want to start slicing from the
beginning or the end of the list. For example:
```
print(a[:3])
print(a[1:])
print(a[:])
print(a[:-1])
```
You can also change the step size, which is specified by the third value (not
the second one, as in MATLAB). For example:
```
print(a[0:4:2])
print(a[::2])
print(a[::-1])
```
Some methods are available on `list` objects for adding and removing items:
```
print(d)
d.append(40)
print(d)
d.extend([50, 60])
print(d)
d = d + [70, 80]
print(d)
d.remove(20)
print(d)
d.pop(0)
print(d)
```
What will `d.append([50,60])` do, and how is it different from
`d.extend([50,60])`?
```
d.append([50, 60])
print(d)
```
<a class="anchor" id="dictionaries"></a>
## Dictionaries
Dictionaries (or *dicts*) can be used to store key-value pairs. Almost
anything can used as a key, and anything can be stored as a value; it is
common to use strings as keys:
```
e = {'a' : 10, 'b': 20}
print(len(e))
print(e.keys())
print(e.values())
print(e['a'])
```
Like lists (and unlike tuples), dicts are mutable, and have a number of
methods for manipulating them:
```
e['c'] = 30
e.pop('a')
e.update({'a' : 100, 'd' : 400})
print(e)
e.clear()
print(e)
```
<a class="anchor" id="a-note-on-mutability"></a>
## A note on mutablility
Python variables can refer to values which are either mutable, or
immutable. Examples of immutable values are strings, tuples, and integer and
floating point scalars. Examples of mutable values are lists, dicts, and most
user-defined types.
When you pass an immutable value around (e.g. into a function, or to another
variable), it works the same as if you were to copy the value and pass in the
copy - the original value is not changed:
```
a = 'abcde'
b = a
b = b.upper()
print('a:', a)
print('b:', b)
```
In contrast, when you pass a mutable value around, you are passing a
*reference* to that value - there is only ever one value in existence, but
multiple variables refer to it. You can manipulate the value through any of
the variables that refer to it:
```
a = [1, 2, 3, 4, 5]
b = a
a[3] = 999
b.append(6)
print('a', a)
print('b', b)
```
<a class="anchor" id="flow-control"></a>
# Flow control
Python also has a boolean type which can be either `True` or `False`. Most
Python types can be implicitly converted into booleans when used in a
conditional expression.
Relevant boolean and comparison operators include: `not`, `and`, `or`, `==`
and `!=`
For example:
```
a = True
b = False
print('Not a is:', not a)
print('a or b is:', a or b)
print('a and b is:', a and b)
print('Not 1 is:', not 1)
print('Not 0 is:', not 0)
print('Not {} is:', not {})
print('{}==0 is:', {}==0)
```
There is also the `in` test for strings, lists, etc:
```
print('the' in 'a number of words')
print('of' in 'a number of words')
print(3 in [1, 2, 3, 4])
```
We can use boolean values in `if`-`else` conditional expressions:
```
a = [1, 2, 3, 4]
val = 3
if val in a:
print('Found {}!'.format(val))
else:
print('{} not found :('.format(val))
```
Note that the indentation in the `if`-`else` statement is **crucial**.
**All** python control blocks are delineated purely by indentation. We
recommend using **four spaces** and no tabs, as this is a standard practice
and will help a lot when collaborating with others.
You can use the `for` statement to loop over elements in a list:
```
d = [10, 20, 30]
for x in d:
print(x)
```
You can also loop over the key-value pairs in a dict:
```
a = {'a' : 10, 'b' : 20, 'c' : 30}
print('a.items()')
for key, val in a.items():
print(key, val)
print('a.keys()')
for key in a.keys():
print(key, a[key])
print('a.values()')
for val in a.values():
print(val)
```
> In older versions of Python 3, there was no guarantee of ordering when using dictionaries.
> However, a of Python 3.7, dictionaries will remember the order in which items are inserted,
> and the `keys()`, `values()`, and `items()` methods will return elements in that order.
>
> If you want a dictionary with ordering, *and* you want your code to work with
> Python versions older than 3.7, you can use the
> [`OrderedDict`](https://docs.python.org/3/library/collections.html#collections.OrderedDict)
> class.
There are some handy built-in functions that you can use with `for` loops:
```
d = [10, 20, 30]
print('Using the range function')
for i in range(len(d)):
print('element at position {}: {}'.format(i, d[i]))
print('Using the enumerate function')
for i, elem in enumerate(d):
print('element at position {}: {}'.format(i, elem))
```
<a class="anchor" id=" list-comprehensions"></a>
## List comprehensions
Python has a really neat way to create lists (and dicts), called
*comprehensions*. Let's say we have some strings, and we want to count the
number of characters in each of them:
```
strings = ['hello', 'howdy', 'hi', 'hey']
nchars = [len(s) for s in strings]
for s, c in zip(strings, nchars):
print('{}: {}'.format(s, c))
```
> The `zip` function "zips" two or more sequences, so you can loop over them
> together.
Or we could store the character counts in a dict:
```
nchars = { s : len(s) for s in strings }
for s, c in nchars.items():
print('{}: {}'.format(s, c))
```
<a class="anchor" id="reading-and-writing-text-files"></a>
# Reading and writing text files
The syntax to open a file in python is
`with open(<filename>, <mode>) as <file_object>: <block of code>`, where
* `filename` is a string with the name of the file
* `mode` is one of 'r' (for read-only access), 'w' (for writing a file, this
wipes out any existing content), 'a' (for appending to an existing file).
* `file_object` is a variable name which will be used within the `block of
code` to access the opened file.
For example the following will read all the text in `data/file.txt` and print
it:
```
with open('data/file.txt', 'r') as f:
print(f.read())
```
A very similar syntax is used to write files:
```
with open('new_file.txt', 'w') as f:
f.write('This is my first line\n')
f.writelines(['Second line\n', 'and the third\n'])
```
<a class="anchor" id="example-processing-lesion-counts"></a>
## Example: processing lesion counts
Imagine that we have written an amazing algorithm in Python which
automatically counts the number of lesions in an individual's structural MRI
image.
```
subject_ids = ['01', '07', '21', '32']
lesion_counts = [ 4, 9, 13, 2]
```
We may wish to process this data in another application (e.g. Excel or SPSS).
Let's save the results out to a CSV (comma-separated value) file:
```
with open('lesion_counts.csv', 'w') as f:
f.write('Subject ID, Lesion count\n')
for subj_id, count in zip(subject_ids, lesion_counts):
f.write('{}, {}\n'.format(subj_id, count))
```
We can now load the `lesion_counts.csv` file into our analysis software of
choice. Or we could load it back into another Python session, and store
the data in a dict:
```
lesion_counts = {}
with open('lesion_counts.csv', 'r') as f:
# skip the header
f.readline()
for line in f.readlines():
subj_id, count = line.split(',')
lesion_counts[subj_id] = int(count)
print('Loaded lesion counts:')
for subj, count in lesion_counts.items():
print('{}: {}'.format(subj, count))
```
<a class="anchor" id="functions"></a>
## Functions
You will find functions pretty familiar in python to start with, although they
have a few options which are really handy and different from C++ or matlab (to
be covered in a later practical). To start with we'll look at a simple
function but note a few key points:
* you *must* indent everything inside the function (it is a code block and
indentation is the only way of determining this - just like for the guts of a
loop)
* you can return *whatever you want* from a python function, but only a single
object - it is usual to package up multiple things in a tuple or list, which
is easily unpacked by the calling invocation: e.g., `a, b, c = myfunc(x)`
* parameters are passed by *reference* (more on this below)
```
def myfunc(x, y, z=0):
r2 = x*x + y*y + z*z
r = r2**0.5
return r, r2
rad = myfunc(10, 20)
print(rad)
rad, dummy = myfunc(10, 20, 30)
print(rad)
rad, _ = myfunc(10,20,30)
print(rad)
```
> Note that the `_` is used as shorthand here for a dummy variable
> that you want to throw away.
>
> The return statement implicitly creates a tuple to return and is equivalent
> to `return (r, r2)`
One nice feature of python functions is that you can name the arguments when
you call them, rather than only doing it by position. For example:
```
def myfunc(x, y, z=0, flag=''):
if flag=='L1':
r = abs(x) + abs(y) + abs(z)
else:
r = (x*x + y*y + z*z)**0.5
return r
rA = myfunc(10, 20)
rB = myfunc(10, 20, flag='L1')
rC = myfunc(10, 20, flag='L1', z=30)
print(rA, rB, rC)
```
You will often see python functions called with these named arguments. In
fact, for functions with more than 2 or 3 variables this naming of arguments
is recommended, because it clarifies what each of the arguments does for
anyone reading the code.
Arguments passed into a python function are *passed by reference* - this is
where the difference between *mutable* and *immutable* types becomes
important - if you pass a mutable object into a function, the function
might change it!
```
def changelist(l):
l[0] = 'mwahahaha!'
mylist = [1,2,3,4,5]
print('before:', mylist)
changelist(mylist)
print('after:', mylist)
mytup = [1,2,3,4,5]
changelist(mytup)
```
<a class="anchor" id="working-with-numpy"></a>
# Working with `numpy`
This section introduces you to [`numpy`](http://www.numpy.org/), Python's
numerical computing library. Numpy adds a new data type to the Python
language - the `array` (more specifically, the `ndarray`). A Numpy `array`
is a N-dimensional array of homogeneously-typed numerical data.
Pretty much every scientific computing library in Python is built on top of
Numpy - whenever you want to access some data, you will be accessing it in the
form of a Numpy array. So it is worth getting to know the basics.
<a class="anchor" id="the-python-list-versus-the-numpy-array"></a>
## The Python list versus the `numpy` array
You have already been introduced to the Python `list`, which you can easily
use to store a handful of numbers (or anything else):
```
data = [10, 8, 12, 14, 7, 6, 11]
```
You could also emulate a 2D or ND matrix by using lists of lists, for example:
```
xyz_coords = [[-11.4, 1.0, 22.6],
[ 22.7, -32.8, 19.1],
[ 62.8, -18.2, -34.5]]
```
For simple tasks, you could stick with processing your data using python
lists, and the built-in
[`math`](https://docs.python.org/3.5/library/math.html) library. And this
might be tempting, because it does look quite a lot like what you might type
into Matlab.
But **BEWARE!** A Python list is a terrible data structure for scientific
computing!
This is a major source of confusion for people who are learning Python, and
are trying to write efficient code. It is _crucial_ to be able to distinguish
between a Python list and a Numpy array.
**Python list == Matlab cell array:** A list in Python is akin to a cell
array in Matlab - they can store anything, but are extremely inefficient, and
unwieldy when you have more than a couple of dimensions.
**Numpy array == Matlab matrix:** These are in contrast to the Numpy array
and Matlab matrix, which are both thin wrappers around a contiguous chunk of
memory, and which provide blazing-fast performance (because behind the scenes
in both Numpy and Matlab, it's C, C++ and FORTRAN all the way down).
So you should strongly consider turning those lists into Numpy arrays:
```
import numpy as np
data = np.array([10, 8, 12, 14, 7, 6, 11])
xyz_coords = np.array([[-11.4, 1.0, 22.6],
[ 22.7, -32.8, 19.1],
[ 62.8, -18.2, -34.5]])
print('data: ', data)
print('xyz_coords: ', xyz_coords)
print('data.shape: ', data.shape)
print('xyz_coords.shape:', xyz_coords.shape)
```
> Numpy is not a "built-in" library, so we have to import it. The statement
> `import numpy as np` tells Python to *Import the `numpy` library, and make
> it available as a variable called `np`.*
<a class="anchor" id="creating-arrays"></a>
## Creating arrays
Numpy has quite a few functions which behave similarly to their equivalents in
Matlab:
```
print('np.zeros gives us zeros: ', np.zeros(5))
print('np.ones gives us ones: ', np.ones(5))
print('np.arange gives us a range: ', np.arange(5))
print('np.linspace gives us N linearly spaced numbers:', np.linspace(0, 1, 5))
print('np.random.random gives us random numbers [0-1]:', np.random.random(5))
print('np.random.randint gives us random integers: ', np.random.randint(1, 10, 5))
print('np.eye gives us an identity matrix:')
print(np.eye(4))
print('np.diag gives us a diagonal matrix:')
print(np.diag([1, 2, 3, 4]))
```
The `zeros` and `ones` functions can also be used to generate N-dimensional
arrays:
```
z = np.zeros((3, 4))
o = np.ones((2, 10))
print(z)
print(o)
```
> Note that, in a 2D Numpy array, the first axis corresponds to rows, and the
> second to columns - just like in Matlab.
> **MATLAB pitfall:** Arithmetic operations on arrays in Numpy work on an
> *elementwise* basis. In particular, if you multiply two arrays together,
> you will get the elementwise product. You **won't** get the dot product,
> like you would in MATLAB. You can, however, use the `@` operator to perform
> matrix multiplication on numpy arrays.
<a class="anchor" id="example-reading-arrays-from-text-files"></a>
## Example: reading arrays from text files
The `numpy.loadtxt` function is capable of loading numerical data from
plain-text files. By default it expects space-separated data:
```
data = np.loadtxt('data/space_separated.txt')
print('data in data/space_separated.txt:')
print(data)
```
But you can also specify the delimiter to expect<sup>1</sup>:
```
data = np.loadtxt('data/comma_separated.txt', delimiter=',')
print('data in data/comma_separated.txt:')
print(data)
```
> <sup>1</sup> And many other things such as file headers, footers, comments,
> and newline characters - see the
> [docs](https://docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt.html)
> for more information.
Of course you can also save data out to a text file just as easily, with
[`numpy.savetxt`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html):
```
data = np.random.randint(1, 10, (10, 10))
np.savetxt('mydata.txt', data, delimiter=',', fmt='%i')
```
Jupyter notebooks have a special feature - if you start a line with a `!`
character, you can run a `bash` command. Let's look at the file we just
generated:
```
!cat mydata.txt
```
> The `!` feature won't work in regular Python scripts.
Here's how we can load a 2D array fom a file, and calculate the mean of each
column:
```
data = np.loadtxt('data/2d_array.txt', comments='%')
colmeans = data.mean(axis=0)
print('Column means')
print('\n'.join(['{}: {:0.2f}'.format(i, m) for i, m in enumerate(colmeans)]))
```