In [1]:
import pandas as pd
import nibabel as nib
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

%matplotlib inline
%load_ext autoreload
%autoreload 2
import sys
sys.path.append('..')
import sfp 
import math
from scipy import stats
from scipy import optimize as opt
import torch
import glob
import os
import warnings
import itertools
import hessian
from torch.utils import data as torchdata

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
sns.set_style('whitegrid')
#df_path = "/mnt/winawerlab/Projects/spatial_frequency_preferences/BIDS/derivatives/first_level_analysis/stim_class/posterior/sub-wlsubj045/ses-04/sub-wlsubj045_ses-04_task-sfprescaled_v1_e1-12_summary.csv"
df_path = "/mnt/prince_scratch/spatial_frequency_preferences/derivatives/first_level_analysis/stim_class/bayesian_posterior/sub-wlsubj045/ses-04/sub-wlsubj045_ses-04_task-sfprescaled_v1_e1-12_summary.csv"


Failed to import duecredit due to No module named 'duecredit'

In [2]:
sns.set_style('white', {'axes.spines.right': False, 'axes.spines.top': False})

Model motivation

To explain the motivation behind this model, let's step through some reasoning.


In [5]:
# Load in data
df = pd.read_csv(df_path)

Let's look at some voxels that have good GLM $R^2$ values and pick one


In [11]:
df[(df.varea==1)&(df.GLM_R2>40)].drop_duplicates('voxel').sort_values('GLM_R2', ascending=False).head(20)[['voxel', 'GLM_R2', 'varea', 'hemi', 'angle' ,'eccen', 'precision', 'prf_vexpl']]


Out[11]:
voxel GLM_R2 varea hemi angle eccen precision prf_vexpl
3269 3269 76.047745 1 rh 2.760368 8.217803 4.869241 0.734004
3268 3268 74.536285 1 rh 2.805402 8.353544 3.192470 0.752097
3270 3270 73.499702 1 rh 2.742864 8.085112 6.539636 0.725643
1962 1962 73.478546 1 lh 4.730360 2.853651 2.646652 0.719022
3277 3277 72.855141 1 rh 2.814290 8.130795 4.520051 0.805642
3498 3498 72.825729 1 rh 2.736296 8.276639 4.162940 0.656153
3261 3261 72.681000 1 rh 2.746991 8.333627 3.457480 0.667239
3356 3356 72.513184 1 rh 2.949604 3.009490 3.906445 0.833681
3350 3350 72.401497 1 rh 2.951332 3.168415 4.643666 0.837977
3349 3349 72.398254 1 rh 2.998524 3.205108 2.952097 0.836020
2972 2972 72.286964 1 rh 2.498455 2.088567 4.652300 0.625065
3059 3059 71.974144 1 rh 2.578800 2.256740 2.152391 0.696033
3355 3355 71.906837 1 rh 3.009597 3.089526 3.087009 0.822670
3040 3040 71.858459 1 rh 2.835505 2.601278 5.075848 0.808184
2956 2956 71.613571 1 rh 2.778346 2.350393 1.562762 0.793931
3048 3048 71.595070 1 rh 2.850946 2.452312 2.951960 0.798309
3764 3764 71.579018 1 rh 2.701077 8.680505 11.398286 0.205937
3171 3171 71.555038 1 rh 2.961612 2.925480 3.754540 0.815415
3260 3260 71.456688 1 rh 2.766411 8.418975 2.544967 0.687042
2928 2928 71.370651 1 rh 2.615927 1.807594 2.190861 0.743359

In [12]:
# Pick a V1 voxel with a good R2
voxel_df = df[(df.voxel.isin([3269]))]#, 1421]))]
voxel_df.head()


Out[12]:
varea voxel stimulus_superclass w_r w_a eccen angle stimulus_class amplitude_estimate_std_error hemi ... local_w_r local_w_a local_sf_magnitude local_sf_xy_direction local_sf_ra_direction baseline precision amplitude_estimate_norm amplitude_estimate_median_normed amplitude_estimate_std_error_normed
3269 1 3269 angular 0.0 6.0 8.217803 2.760368 0 0.593104 rh ... 5.871965e-17 0.116203 0.116203 4.331164 1.570796 0 4.869241 45.727131 0.057103 0.012971
8947 1 3269 angular 0.0 8.0 8.217803 2.760368 1 0.485870 rh ... 7.829287e-17 0.154937 0.154937 4.331164 1.570796 0 4.869241 45.727131 0.073693 0.010625
14625 1 3269 angular 0.0 11.0 8.217803 2.760368 2 0.382195 rh ... 1.076527e-16 0.213038 0.213038 4.331164 1.570796 0 4.869241 45.727131 0.090514 0.008358
20303 1 3269 angular 0.0 16.0 8.217803 2.760368 3 0.482488 rh ... 1.565857e-16 0.309873 0.309873 4.331164 1.570796 0 4.869241 45.727131 0.145640 0.010551
25981 1 3269 angular 0.0 23.0 8.217803 2.760368 4 0.716420 rh ... 2.250920e-16 0.445443 0.445443 4.331164 1.570796 0 4.869241 45.727131 0.162595 0.015667

5 rows × 32 columns

Let's examine the response of this voxel as a function of spatial frequency. In the plot below, we plot the normed amplitude estimate as a function of the local spatial frequency. We see that the response looks roughly log-Normal, but there appears to be some difference between the different stimulus classes.


In [232]:
pal = sfp.plotting.stimulus_type_palette('relative')
hue_order = sfp.plotting.stimulus_type_order('relative')

In [15]:
g = sns.FacetGrid(voxel_df[~voxel_df.stimulus_superclass.isin(['mixtures'])], hue='stimulus_superclass',palette=pal, size=8, aspect=1.5, hue_order=hue_order)
g.map(plt.scatter, 'local_sf_magnitude', 'amplitude_estimate_median', linewidth=6)
g.map(plt.plot, 'local_sf_magnitude', 'amplitude_estimate_median', linewidth=6)
# g.ax.set_xscale('log', basex=2)
g.add_legend()
g.ax.tick_params(size=0)
g.ax.set_xlim((0, 3))
g.set_xlabels('Local spatial frequency (cpd)')
g.set_ylabels('Amplitude Estimate')


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/seaborn/axisgrid.py:230: UserWarning: The `size` paramter has been renamed to `height`; please update your code.
  warnings.warn(msg, UserWarning)
Out[15]:
<seaborn.axisgrid.FacetGrid at 0x7f3c3bae2b00>

These classes differ in their local orientation, so we can look at a plot of the response as a function of the local spatial frequency with respect to x and y (size represents the response). This plot is difficult to parse, but the main point is that these different stimulus classes are not arbitrary and discrete: they lie on a continuum, related by the stimulus orientation, and so we can fit the response of the voxel as a 2d tuning curve.


In [16]:
def scatter_sizes(x, y, s, plot_color=False, cmap=None, size_scale=1, **kwargs):
    if plot_color:
        kwargs.pop('color')
        if cmap is None:
            cmap = 'Blues'
        plt.scatter(x, y, s=s*80*size_scale, c=s, cmap=cmap, **kwargs)
    else:
        plt.scatter(x, y, s=s*80*size_scale, **kwargs)

with sns.axes_style('whitegrid'):
    voxel_df['normalized_resp'] = voxel_df['amplitude_estimate_median'].copy()
    voxel_df['normalized_resp'] = (voxel_df['normalized_resp'] - voxel_df['normalized_resp'].min()) / (voxel_df['normalized_resp'].max() - voxel_df['normalized_resp'].min())
    g=sns.FacetGrid(voxel_df, size=8, aspect=1, hue='stimulus_superclass', palette=pal, hue_order=hue_order)
    g.map(scatter_sizes, 'local_w_x', 'local_w_y', 'normalized_resp', plot_color=False, size_scale=2)
    g.add_legend()
    scatter_ax = plt.gca()
    scatter_ax.set_aspect('equal')
    g.ax.tick_params(size=0)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/ipykernel_launcher.py:11: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  # This is added back by InteractiveShellApp.init_path()
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/ipykernel_launcher.py:12: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  if sys.path[0] == '':
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/seaborn/axisgrid.py:230: UserWarning: The `size` paramter has been renamed to `height`; please update your code.
  warnings.warn(msg, UserWarning)

This is just that same data, but rotated and plotted on a semi-log plots.


In [17]:
with sns.axes_style('whitegrid'), sns.plotting_context('notebook'):
    g=sns.FacetGrid(voxel_df, hue='stimulus_superclass', size=5, aspect=1, palette=pal, hue_order=hue_order)
    g.map(scatter_sizes, 'local_w_r', 'local_w_a', 'normalized_resp')
    g.add_legend()
    scatter_ax = plt.gca()
    scatter_ax.set_xscale('symlog', basex=2, linthreshx=2**(-4))
    scatter_ax.set_yscale('symlog', basey=2, linthreshy=2**(-4))


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/seaborn/axisgrid.py:230: UserWarning: The `size` paramter has been renamed to `height`; please update your code.
  warnings.warn(msg, UserWarning)

But then the question is: how does the tuning change with orientation? Two possibilities are:

  1. The preferred frequency of the tuning curve / mode of the log-Gaussian distribution changes with orientation.
  2. The amplitude of the tuning curve changes with orientation.

Then there's the question of how either the mode or the amplitude changes with orientation. Let's assume it changes smoothly and periodically, symmetrically about 180 degrees (because 2d orientation is runs from 0 to 180 degrees). We'll examine three possibilities:

  1. all orientation are equally important (mode/amplitude does not depend on orientation; constant)
  2. horizontal or vertical is preferred, but the other is anti-preferred (sinusoid with frequency $2\theta$)
  3. the cardinals are preferred, the obliques are anti-preferred (or vice-versa, sinusoid with frequency $4\theta$)

I'll build up the complexity of the model, adding terms (and plots) to show additional possibilities that we're modeling. However, note that there are several things we are not modeling:

  • Categorical differences between upper and lower; left and right; foveal, parafoveal, and peripheral visual field (some elements of these are caught by our parameterization, but not in any complete sense; we are modeling them separately as well to see if there's a difference in the parameters found)
  • Voxel-to-voxel differences in the max response amplitude; there's too much confounding with blood vessel location and other non-neural factors, so we only model the variation in max amplitude within voxels

There are lots of ways to show this. For our purposes, we're going to show three types of plots:

  1. Preferred period of vs. eccentricity, with separate plots for different retinotopic angles, and color used to show different stimulus types (which contain different orientation information)
  2. Preferred period contour plots, polar plots that shows the eccentricity where we have a specific preferred period at different retinotopic angles. As above, color shows different stimulus types.
  3. Max amplitude plots, polar plots that show the maximum amplitude of the response at each polar angle, where (as above) color shows different stimulus types (note that there's no interaction between this and eccentricity or preferred period).

We'll build up the complexity of these examples, starting with the simplest and getting more complex as we go.

As we mentioned, we noticed the response looked roughly log-normal, so we start with that:

$\hat{\beta} = A \exp\left(\frac{-(\log_2\omega+\log_2p)^2}{2\sigma^2} \right)$

Here:

  • $\hat{\beta}$: our prediction of the voxel's response, $\beta$ (defined as the estimate give by GLMdenoise) of a given voxel to a given stimulus
  • $\omega$: the spatial frequency content of the stimulus at the center of the voxel's population receptive field
  • $p$: the preferred period of this voxel
  • $\sigma$: the bandwidth of this voxel's response (in octaves)
  • $A$: the voxel's max response amplitude

This here is what we were fitting in our first pass analysis, as seen in notebook 04-MRI-first-level; for each eccentricity band and stimulus class, we fit all three parameters ($p$, $\sigma$, and $A$) separately, and then plotted them out as a function of eccentricity to see their relationship.

Now, we're interested in parameterizing the relationship between these parameters and the voxel's eccentricity, retinotopic angle, and the stimulus's local orientation information. The simplest way we'll do this, corresponding to the largest effect we saw, is to say that retinotopic angle and stimulus local orientation don't matter and that the preferred period is some linear function of eccentricity, $e$:

$p=ae+b$

where we fit $a$ and $b$ across all voxels and stimuli. Note that we now fit the same $\sigma$ to all voxels (but since it's in octaves, there's an implicit scaling here). We don't actually fit $A$ here, since we normalize each voxel's response in our loss function, so any constant value for $A$ will be equivalent.


In [35]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', abs_mode_cardinals=0)
tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

For this simple model, there's on effect of retinotopic angle or stimulus orientation, so we won't look at any other plots.

However, we can look at the effect of changing these two parameters, $a$ and $b$, which do what you expect:


In [34]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.5, sf_ecc_intercept=.2, orientation_type='full', abs_mode_cardinals=0)
tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Increased $a$', ylim=(-.1, 2.5))

m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.05, sf_ecc_intercept=.2, orientation_type='full', abs_mode_cardinals=0)
tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Decreased $a$', ylim=(-.1, 2.5))


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

In [36]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.4, orientation_type='full', abs_mode_cardinals=0)
tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Increased $b$', ylim=(-.1, 2.5))

m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=0, orientation_type='full', abs_mode_cardinals=0)
tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Decreased $b$', ylim=(-.1, 2.5))


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

Alright, now we're going to make things more complicated by parameterizing an effect of stimulus orientation and voxel retinotopic angle on the preferred period:

$\hat{\beta} = A \exp\left(\frac{-(\log_2\omega+\log_2p)^2}{2\sigma^2} \right)$

$p=(ae+b)(1+p_1\cos 2\theta + p_2\cos 4\theta + p_3\cos 2(\theta-\phi) + p_4\cos 4(\theta-\phi))$

where all parameters are as before and we now have two new variables:

  • $\theta$: the stimulus local orientation information at the center of the voxel's population receptive field
  • $\phi$: the retinotopic angle of the voxel's population receptive field

and the new $p_i$ parameters control the size of the effects on the preferred period:

  • $p_1$: absolute cardinal effect, horizontal vs. vertical
  • $p_2$: absolute cardinals vs. obliques effect, horizontal/vertical vs. diagonals
  • $p_3$: relative cardinal effect, radial vs. angular
  • $p_4$: relative cardinals vs. obliques effect, radial/angular vs. spirals

Note that we're saying the effect of eccentricity and retinotopic angle / orientation are multiplied together, that they have separable effects on the preferred period.

Let's first consider a model that has $p_3=p_4=0$ and $p_1>p_2>0$, that is, where there's only an effect of absolute orientation and the effect of vertical vs. horizontal is larger than that of cardinals vs. obliques.


In [562]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', abs_mode_cardinals=.2, abs_mode_obliques=.1)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

Now we'll look at these plots again, to see what happens when we increase $p_1$


In [69]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', abs_mode_cardinals=.4, abs_mode_obliques=.1)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

... when we make $p_1=p_2$


In [70]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', abs_mode_cardinals=.2, abs_mode_obliques=.2)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

... and we make $p_1<p_2$


In [71]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', abs_mode_cardinals=.2, abs_mode_obliques=.4)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

And now let's set $p_1=p_2=0$ and look at the effects of $p_3$ and $p_4$, with $p_3 > p_4$


In [72]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', rel_mode_cardinals=.2, rel_mode_obliques=.1)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

... when we increase $p_3$


In [73]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', rel_mode_cardinals=.4, rel_mode_obliques=.1)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

... when $p_3 = p_4$


In [74]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', rel_mode_cardinals=.2, rel_mode_obliques=.2)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

... when $p_3 < p_4$


In [75]:
m = sfp.model.LogGaussianDonut(vary_amplitude=False, sf_ecc_slope=.2, sf_ecc_intercept=.2, orientation_type='full', rel_mode_cardinals=.2, rel_mode_obliques=.4)

tmp = sfp.analyze_model.create_preferred_period_df(m, reference_frame='relative',)
g = sfp.plotting.feature_df_plot(tmp, col=None, title='Preferred period averaged across all retinotopic angles', ylim=(-.1, 2.5))

tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='relative', orientation=np.linspace(0, np.pi, 2, endpoint=False),)
g = sfp.plotting.feature_df_polar_plot(tmp, )
tmp = sfp.analyze_model.create_preferred_period_contour_df(m, reference_frame='absolute', orientation=np.linspace(0, np.pi, 2, endpoint=False))
g = sfp.plotting.feature_df_polar_plot(tmp)


/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
/home/billbrod/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

And we do the same thing with amplitude, parameterizing it as:

$\hat{\beta} = A \exp\left(\frac{-(\log_2\omega+\log_2p)^2}{2\sigma^2} \right)$

$p=(ae+b)(1+p_1\cos 2\theta + p_2\cos 4\theta + p_3\cos 2(\theta-\phi) + p_4\cos 4(\theta-\phi))$

$A=(1+A_1\cos 2\theta + A_2\cos 4\theta + A_3\cos 2(\theta-\phi) + A_4\cos 4(\theta-\phi))$

Model variants

That seems like a lot of potential parameters (11 total); are they all necessary? We'll fit different variants of the model, each fitting different subsets of the parameters, and use cross-validation to see which fits best


In [2]:
model_names = ['constant iso', 'scaling iso', 'full iso', 'full absolute', 'full relative', 'full full', 'full absolute amps', 'full relative amps', 'full full amps']
parameters = [r'$\sigma$', r'$a$', r'$b$', r'$p_1$', r'$p_2$', r'$p_3$', r'$p_4$', r'$A_1$', r'$A_2$', r'$A_3$', r'$A_4$']
model_variants = np.zeros((len(model_names), len(parameters))).astype(bool)
# everyone fits sigma
model_variants[:, 0] = True
model_variants[1:, 1] = True
model_variants[0, 2] = True
model_variants[2:, 2] = True
model_variants[3, [3, 4]] = True
model_variants[4, [5, 6]] = True
model_variants[5, [3, 4, 5, 6]] = True
model_variants[6, [3, 4, 7, 8]] = True
model_variants[7, [5, 6, 9, 10]] = True
model_variants[8, 3:] = True

In [5]:
model_variants = pd.DataFrame(model_variants, model_names, parameters)
green, red = sns.color_palette('deep', 4)[2:]
pal = sns.blend_palette([red, green])

In [6]:
with sns.plotting_context('poster', font_scale=1.5):
    fig = plt.figure(figsize=(12,10))
    ax = sns.heatmap(model_variants, cmap=pal, cbar=False, )
    ax.set_yticklabels(model_names, rotation=0)


Voxel precision

One of the things we care about is the noisiness of our voxels. We'll use this in our loss function, so we want to know whether we can just use the noise we've calculated on a voxel-by-voxel basis or whether we have to worry about differing levels of noise for different stimulus classes.


In [7]:
df = pd.read_csv(df_path)

What's unclear from the following plot is whether the apparent relationship between the median amplitude estimate and the error is because of differences between voxels or within them. If it's between voxels, we're fine, but it's bad if it's within them (this would be like for a Poisson-process neuron, whose variance increases with the mean firing rate)


In [8]:
sns.relplot(x='amplitude_estimate_median', y='amplitude_estimate_std_error', data=df)


Out[8]:
<seaborn.axisgrid.FacetGrid at 0x7fb719e93a58>

So to answer this question, we do a simple linear regression between the median amplitude estimate and the standard error.


In [9]:
def linear_regression(data, x='amplitude_estimate_median', y='amplitude_estimate_std_error'):
    s,i,r,p,e = stats.linregress(data[x], data[y])
    return pd.Series({'slope': s, 'intercept': i, 'r_squared': r**2, 'p_value': p, 'stderr': e})

linregress = df.groupby('voxel').apply(linear_regression)

And what we see here is that there doesn't appear to be a consistent relationship between the median amplitude estimate and the error of those estimates: the slope looks like it's distributed around zero, as does the relationship between slope and intercept. This is good, so we can treat precision as a voxel-by-voxel number


In [10]:
sns.pairplot(linregress, vars=['slope', 'intercept'], height=5)


Out[10]:
<seaborn.axisgrid.PairGrid at 0x7fb719e79ac8>

Since we're happy with that, we'll use the following loss function, evaluated for each voxel $v$ (NOTE: probably should add $_v$ to all these values):

$L(\beta,\hat{\beta})=\frac{1}{\sigma_v^2}\sum_{i=1}^{n}\frac{1}{n}\left(\frac{\beta_i}{||\beta||_2} - \frac{\hat{\beta}_i}{||\hat{\beta}||_2} \right)^2$

where $i$ indexes the $n$ different stimulus classes, $\beta_i$ is the response (estimated using GLMdenoise) to stimulus class $i$, $\hat{\beta}_i$ is the response to stimulus class $i$ predicted by our model, $||\beta||_2$ is the L2-norm of $\beta$ (for this voxel, across all stimulus classes), and $\sigma_v^2$ is the variance of the voxel's response (that is, $\sigma_v^2 = \frac{1}{n}\sum_{i=1}^n\sigma^2_{vi}$, where $\sigma_{vi}$ is half of the 68 percentile range of the response of voxel $v$ to stimulus class $i$, as estimated by GLMdenoise).

If we didn't have that $\frac{1}{\sigma_v}$ in the beginning, this would be $\frac{2}{n}(1-\cos(\beta,\hat{\beta}))$, where $\cos(A,B)$ is the cosine similarity between two vectors $A$ and $B$. It's similar to the Pearson correlation, except that it's not de-meaned (see this StackOverflow post for a bit more information)

To see how:

$\sum_i\frac{1}{n}\left(\frac{\beta_i}{||\beta||} - \frac{\hat{\beta}_i}{||\hat{\beta}||} \right)^2$

$\frac{1}{n}\sum_i\left(\frac{\beta_i^2}{||\beta||^2} - \frac{2\beta_i\hat{\beta}_i}{||\hat{\beta}|| ||\beta||} + \frac{\hat{\beta}_i^2}{||\hat{\beta}||^2}\right)$

$\frac{1}{n}\left(\sum_i\frac{\beta_i^2}{||\beta||^2} + \sum_i\frac{\hat{\beta}_i^2}{||\hat{\beta}||^2} - \sum_i\frac{2\beta_i\hat{\beta}_i}{||\hat{\beta}|| ||\beta||} \right)$

$\frac{1}{n}\left(\frac{\sum_i\beta_i^2}{||\beta||^2} + \frac{\sum_i\hat{\beta}_i^2}{||\hat{\beta}||^2} - \frac{2\sum_i\beta_i\hat{\beta}_i}{||\hat{\beta}|| ||\beta||} \right)$

$\frac{1}{n}\left(\frac{||\beta||^2}{||\beta||^2} + \frac{ ||\hat{\beta}||^2}{||\hat{\beta}||^2} - \frac{2\sum_i\beta_i\hat{\beta}_i}{||\hat{\beta}|| ||\beta||} \right)$

$\frac{1}{n}\left(2 - 2\frac{\sum_i\beta_i\hat{\beta}_i}{||\hat{\beta}|| ||\beta||} \right)$

$\frac{2}{n}\left(1 - \cos(\beta,\hat{\beta}) \right)$

For cross-validated loss, we fit model to 44 of the 48 classes using standard procedure (with above loss), then get predictions for the 4 held out classes. We do this for each of the 12 subsets, which gets us a complete $\hat{\beta}_v$ that we can compare against the actual $\beta_v$ using the above loss. Therefore, interpretation should be the same.

Analyzing outputs

In the 05a-Simulations notebook, we look at some simulated data to show how we picked the learning hyperparameters (learning rate and batch size), as well as some simulations that reassure us about model recovery (finding the correct model and the correct parameter values). Thus reassured, we examine the actual data.

Looking at subjects' cross-validated model fits

Across subjects and sessions

Now we look at all the cross-validated fits to subjects and sessions, using the pre-shrunk data computed by the snakemake rule summarize_model_cv (because the files are too big to hold all in memory).


In [151]:
base_str = '/users/broderick/mnt/winawerlab/Projects/spatial_frequency_preferences/BIDS/derivatives/tuning_2d_model/stim_class/bayesian_posterior/initial_cv/v1_e1-12_summary_b10_r0.001_g0_s0_all_'
models = pd.read_csv(base_str + 'models.csv')
timing_df = pd.read_csv(base_str + 'timing.csv')
avg_loss = pd.read_csv(base_str + 'loss.csv')
cv_loss = pd.read_csv(base_str + 'cv_loss.csv')

In [111]:
model_order = ['constant_donut_iso_amps-constant', 'scaling_donut_iso_amps-constant', 'full_donut_iso_amps-constant',
               'full_donut_absolute_amps-constant', 'full_donut_relative_amps-constant', 'full_donut_full_amps-constant',
               'full_donut_absolute_amps-vary', 'full_donut_relative_amps-vary', 'full_donut_full_amps-vary']

In [112]:
avg_loss['indicator'] = avg_loss.apply(lambda x: str((x.subject, x.session)), 1)
models['indicator'] = models.apply(lambda x: str((x.subject, x.session)), 1)
cv_loss['indicator'] = cv_loss.apply(lambda x: str((x.subject, x.session)), 1)
timing_df['indicator'] = timing_df.apply(lambda x: str((x.subject, x.session)), 1)
indicator_order = cv_loss.indicator.unique()

In [113]:
pd.pivot_table(cv_loss, 'cv_loss', ['fit_model_type'], 'indicator')


Out[113]:
indicator ('sub-wlsubj001', 'ses-01') ('sub-wlsubj001', 'ses-02') ('sub-wlsubj001', 'ses-pilot01') ('sub-wlsubj004', 'ses-03') ('sub-wlsubj007', 'ses-04') ('sub-wlsubj014', 'ses-03') ('sub-wlsubj042', 'ses-01') ('sub-wlsubj042', 'ses-02') ('sub-wlsubj042', 'ses-pilot00') ('sub-wlsubj042', 'ses-pilot01') ('sub-wlsubj045', 'ses-01') ('sub-wlsubj045', 'ses-02') ('sub-wlsubj045', 'ses-03') ('sub-wlsubj045', 'ses-04') ('sub-wlsubj045', 'ses-pilot01') ('sub-wlsubj062', 'ses-04') ('sub-wlsubj064', 'ses-04') ('sub-wlsubj081', 'ses-04') ('sub-wlsubj095', 'ses-04')
fit_model_type
constant_donut_iso_amps-constant 0.068717 0.123048 0.104922 0.128973 0.116044 0.059773 0.132354 0.137453 0.084002 0.261898 0.174417 0.158018 0.110281 0.092728 0.085027 0.060568 0.038914 0.118617 0.048931
full_donut_absolute_amps-constant 0.067821 0.120581 0.104491 0.128210 0.111762 0.053492 0.130170 0.139262 0.083288 0.261189 0.168515 0.150318 0.104903 0.086734 0.080862 0.057337 0.036133 0.116753 0.045934
full_donut_absolute_amps-vary 0.067751 0.118788 0.104337 0.127948 0.111597 0.053285 0.126189 0.137200 0.082998 0.260226 0.151799 0.149414 0.103767 0.085820 0.080193 0.056711 0.036061 0.116731 0.045920
full_donut_full_amps-constant 0.067308 0.120594 0.104564 0.128318 0.111001 0.053587 0.130323 0.139492 0.083382 0.261265 0.169824 0.150430 0.104832 0.087181 0.080661 0.056821 0.036530 0.116242 0.045671
full_donut_full_amps-vary 0.067058 0.117534 0.103935 0.126982 0.110353 0.053437 0.125693 0.136740 0.083225 0.259347 0.151373 0.151627 0.103636 0.086298 0.079960 0.056543 0.036266 0.116201 0.045052
full_donut_iso_amps-constant 0.067805 0.120670 0.104507 0.128061 0.111991 0.053553 0.132959 0.138892 0.083419 0.260497 0.162216 0.150437 0.104938 0.087039 0.080890 0.057294 0.036249 0.116840 0.046324
full_donut_relative_amps-constant 0.067311 0.120621 0.104531 0.128158 0.111238 0.053676 0.133560 0.138910 0.083584 0.261065 0.163137 0.150495 0.104909 0.087387 0.080711 0.056800 0.036634 0.116340 0.046077
full_donut_relative_amps-vary 0.067213 0.119807 0.104188 0.127352 0.110583 0.053832 0.134256 0.139077 0.083944 0.261400 0.163403 0.152036 0.105069 0.087751 0.080694 0.057376 0.036686 0.116301 0.045426
scaling_donut_iso_amps-constant 0.069321 0.121727 0.105561 0.130528 0.115787 0.054584 0.132168 0.141417 0.083597 0.262282 0.166418 0.154931 0.106215 0.087742 0.081665 0.060271 0.037229 0.119202 0.048553

First thing we note is that the cross-validated loss varies a lot across subjects / scanning sessions and is generally pretty tightly packed within them. In order to examine which model fit the data best, we will demean them -- average the cross-validated loss for a given subject / scanning session across model types, and subtract that off.


In [116]:
g = sns.catplot('indicator', 'cv_loss', 'fit_model_type', data=cv_loss, hue_order=model_order,
                legend='full', height=8, aspect=1.5, order=indicator_order, palette=sns.color_palette('deep', 9), )
for ax in g.axes.flatten():
    labels = ax.get_xticklabels()
    if labels:
        ax.set_xticklabels(labels, rotation=25)
#g.set(ylim=(0, .15))
g.fig.subplots_adjust(bottom=.25)


Once we do that, we can start to see some more obvious patterns, but we want to view these as a function of model type, not of subjects / scanning sessions.


In [117]:
demeaned_loss = cv_loss.set_index(['indicator'])
demeaned_loss['loss_mean'] = cv_loss.groupby(['indicator']).cv_loss.mean()
demeaned_loss['demeaned_loss'] = (demeaned_loss.cv_loss - demeaned_loss.loss_mean)# / demeaned_all_loss.loss_mean
demeaned_loss = demeaned_loss.reset_index()

g = sns.catplot('indicator', 'demeaned_loss', 'fit_model_type', data=demeaned_loss, hue_order=model_order,
                legend='full', height=6, aspect=1.5, order=indicator_order, palette=sns.color_palette('deep', 9),)
for ax in g.axes.flatten():
    labels = ax.get_xticklabels()
    if labels:
        ax.set_xticklabels(labels, rotation=25)
    ax.axhline(color='grey', linestyle='dashed')
#g.set(ylim=(-.005, .01))
g.fig.subplots_adjust(bottom=.25)


Here the patterns become more clear...


In [118]:
with sns.plotting_context('poster'), sns.color_palette('husl', demeaned_loss.indicator.nunique()):
    g = sns.catplot('fit_model_type', 'demeaned_loss', 'indicator', data=demeaned_loss, s=15,
                    legend='full', height=17, aspect=.75, kind='strip', ci=95, estimator=np.median, order=model_order)
    for ax in g.axes.flatten():
        labels = ax.get_xticklabels()
        if labels:
            ax.set_xticklabels(labels, rotation=25)
        ax.axhline(color='grey', linestyle='dashed')
    # g.set(ylim=(-.002, .006))
    g.fig.subplots_adjust(bottom=.25)
    g.set(xlabel='Model type', ylabel='Cross-validated Loss (demeaned by subject)')
    #g.fig.savefig('cv_loss_by_type.svg')


And by plotting the median with 95% bootstrapped confidence intervals, we can get a clearer picture: we get a very big gain by fitting both parameters $a$ and $b$ (instead of one or the other), which we expected from the 1d results seen in the 04-MRI-first-level notebook, and fitting all parameters does seem to be the best, though it appears that the absolute amplitudes are doing the heavy lifting.


In [119]:
with sns.plotting_context('poster'):
    g = sns.catplot('fit_model_type', 'demeaned_loss', 'fit_model_type', data=demeaned_loss, s=15,
                    legend='full', height=17, aspect=.75, kind='point', ci=95, estimator=np.median, order=model_order, hue_order=model_order)
    for ax in g.axes.flatten():
        labels = ax.get_xticklabels()
        if labels:
            ax.set_xticklabels(labels, rotation=25)
        ax.axhline(color='grey', linestyle='dashed')
    # g.set(ylim=(-.002, .006))
    g.fig.subplots_adjust(bottom=.25)
    g.set(xlabel='Model type', ylabel='Cross-validated Loss (demeaned by subject)')
    #g.fig.savefig('cv_loss_by_type.svg')


Full model fits (and bootstraps)

For these, the model has been fit to all data for a given subject (no cross-validation). We'll only look at the best one, full_donut_full_amps-vary.

We also fit models to the 100 bootstraps we have, in order to get a sense for the error bars on the model parameters.


In [5]:
models = pd.read_csv('/users/broderick/mnt/winawerlab/Projects/spatial_frequency_preferences/BIDS/derivatives/tuning_2d_model/stim_class/bayesian_posterior/initial/v1_e1-12_summary_b10_r0.001_g0_full_full_vary_all_models.csv')
bootstrap_models = pd.read_csv('/users/broderick/mnt/winawerlab/Projects/spatial_frequency_preferences/BIDS/derivatives/tuning_2d_model/stim_class/bayesian_posterior/bootstrap/v1_e1-12_full_b10_r0.001_g0_full_full_vary_all_models.csv')

In [6]:
indicator_order = models.indicator.unique()

In [7]:
parameters = [r'$\sigma$', r'$a$', r'$b$', r'$p_1$', r'$p_2$', r'$p_3$', r'$p_4$', r'$A_1$', r'$A_2$', r'$A_3$', r'$A_4$']
original_param_names = ['sigma', 'sf_ecc_slope', 'sf_ecc_intercept', 'abs_mode_cardinals', 'abs_mode_obliques', 'rel_mode_cardinals', 'rel_mode_obliques',
                        'abs_amplitude_cardinals', 'abs_amplitude_obliques', 'rel_amplitude_cardinals', 'rel_amplitude_obliques']
rename_params = dict((k, v) for k, v in zip(original_param_names, parameters))
models_plot = models.copy()#.query('fit_model_type==@fit_model_type')
models_plot = models_plot.set_index('model_parameter')
models_plot.loc['sigma', 'param_category'] = 'sigma'
models_plot.loc[['sf_ecc_slope', 'sf_ecc_intercept'], 'param_category'] = 'eccen'
models_plot.loc[['abs_mode_cardinals', 'abs_mode_obliques', 'rel_mode_cardinals', 'rel_mode_obliques',
                 'abs_amplitude_cardinals', 'abs_amplitude_obliques', 'rel_amplitude_cardinals', 'rel_amplitude_obliques'], 'param_category'] = 'orientation'
models_plot = models_plot.reset_index()
models_plot['model_parameter'] = models_plot.model_parameter.map(rename_params)
pal = dict(zip(models.indicator.unique(), sns.color_palette('husl', models.indicator.nunique())))

Model fit to the median responses (across bootstraps) for each subject/session, then this data shows the median across subject/sessions, with confidence intervals giving the 95% bootstrapped (across subject/sessions) confidence intervals


In [164]:
with sns.plotting_context('poster', font_scale=1):
    fig, axes = plt.subplots(1, 3, figsize=(20, 10), gridspec_kw={'width_ratios': [.15, .3, .6]})
    for i, ax in enumerate(axes):
        cat = ['sigma', 'eccen', 'orientation'][i]
        models_plot_order = [i for i in parameters if i in models_plot.query("param_category==@cat").model_parameter.unique()]
        sns.pointplot('model_parameter', 'fit_value', 'model_parameter', data=models_plot.query("param_category==@cat"), estimator=np.median, s=15, ax=ax, order=models_plot_order)
        if ax.legend_:
            ax.legend_.remove()
        #if i==2:
        #    ax.set(ylim=(-.25, .25))
        ax.axhline(color='grey', linestyle='dashed')
        ax.set(ylabel='Fit value', xlabel='Parameter')
    #fig.savefig('param_vals.svg')


Same as above, except now we're showing each subject/session as a different point, also indicated by hue


In [124]:
# models_plot = models_plot.query("session in ['ses-03', 'ses-04']")
with sns.plotting_context('poster', font_scale=1):
    fig, axes = plt.subplots(1, 3, figsize=(20, 10), gridspec_kw={'width_ratios': [.15, .3, .6]})
    for i, ax in enumerate(axes):
        cat = ['sigma', 'eccen', 'orientation'][i]
        models_plot_order = [i for i in parameters if i in models_plot.query("param_category==@cat").model_parameter.unique()]
        sns.stripplot('model_parameter', 'fit_value', 'indicator', data=models_plot.query("param_category==@cat"), s=15, ax=ax, order=models_plot_order, palette=pal)
        if ax.legend_:
            ax.legend_.remove()
        #if i==2:
        #    ax.set(ylim=(-.25, .25))
        ax.axhline(color='grey', linestyle='dashed')
        ax.set(ylabel='Fit value', xlabel='Parameter')
    #fig.savefig('param_vals.svg')



In [18]:
parameters = [r'$\sigma$', r'$a$', r'$b$', r'$p_1$', r'$p_2$', r'$p_3$', r'$p_4$', r'$A_1$', r'$A_2$', r'$A_3$', r'$A_4$']
original_param_names = ['sigma', 'sf_ecc_slope', 'sf_ecc_intercept', 'abs_mode_cardinals', 'abs_mode_obliques', 'rel_mode_cardinals', 'rel_mode_obliques',
                        'abs_amplitude_cardinals', 'abs_amplitude_obliques', 'rel_amplitude_cardinals', 'rel_amplitude_obliques']
rename_params = dict((k, v) for k, v in zip(original_param_names, parameters))
models_plot = bootstrap_models.copy()#.query('fit_model_type==@fit_model_type')
models_plot = models_plot.set_index('model_parameter')
models_plot.loc['sigma', 'param_category'] = 'sigma'
models_plot.loc[['sf_ecc_slope', 'sf_ecc_intercept'], 'param_category'] = 'eccen'
models_plot.loc[['abs_mode_cardinals', 'abs_mode_obliques', 'rel_mode_cardinals', 'rel_mode_obliques',
                 'abs_amplitude_cardinals', 'abs_amplitude_obliques', 'rel_amplitude_cardinals', 'rel_amplitude_obliques'], 'param_category'] = 'orientation'
models_plot = models_plot.reset_index()
models_plot['model_parameter'] = models_plot.model_parameter.map(rename_params)
pal = dict(zip(models.indicator.unique(), sns.color_palette('husl', models.indicator.nunique())))
#for k, v in zip(models.indicator.unique(), sns.color_palette('husl', models.indicator.nunique())):
#    pal[k] = np.expand_dims(list(v)+[1], 0)

Same as above, except now we're showing the values from when we fit the model to each bootstrap separately (above was just fit to the median across bootstraps), with error bars showing the 95% confidence intervals across bootstraps.

You might look at this, compare it to the previous plot, and think -- these two don't look the same


In [126]:
with sns.plotting_context('poster', font_scale=1):
    fig, axes = plt.subplots(1, 3, figsize=(20, 10), gridspec_kw={'width_ratios': [.15, .3, .6]})
    for i, ax in enumerate(axes):
        cat = ['sigma', 'eccen', 'orientation'][i]
        models_plot_order = [i for i in parameters if i in models_plot.query("param_category==@cat").model_parameter.unique()]
        for n, g in models_plot.query("param_category==@cat & session not in ['ses-pilot00', 'ses-pilot01'] & task!='task-sfpconstant'").groupby('indicator'):
            sfp.plotting.scatter_ci_dist('model_parameter', 'fit_value', data=g, label=n, ax=ax, x_jitter=.2, color=pal[n], x_order=models_plot_order)
        if ax.legend_:
            ax.legend_.remove()
        #if i==2:
        #    ax.set(ylim=(-.25, .25))
        ax.axhline(color='grey', linestyle='dashed')
        ax.set(ylabel='Fit value', xlabel='Parameter')
        ax.set_xlim((-.5, models_plot.query("param_category==@cat").model_parameter.nunique()-.5))
    #fig.savefig('param_vals.svg')



In [79]:
compare_df = models[['model_parameter', 'indicator', 'fit_value', 'session', 'subject', 'task']]
tmp = bootstrap_models[['model_parameter', 'indicator', 'fit_value', 'session', 'subject', 'task']].rename(columns={'fit_value': 'fit_value_bs'})
compare_df = pd.merge(tmp, compare_df, on=['model_parameter', 'indicator', 'session', 'subject', 'task'])

And you'd be correct, the median parameter value across bootstraps and the parameter value when fit to the median across bootstraps are not always the same. The following has each parameter on a separate subplot, with different subject/sessions on the x-axis, and there are two points for each: the one with error bars is the median parameter value across bootstraps (with 90% CI), while the one without is the value fit to the median across bootstraps. Hue shows the different tasks: regular in blue, constant stimuli in orange, and rescaled stimuli in green.

It looks like a handful of subjects (notably sub-wlsubj042) that have the most issues.


In [128]:
g = sns.FacetGrid(compare_df.query("session not in ['ses-pilot00', 'ses-pilot01']"), col='model_parameter', hue='task', col_wrap=4, sharey=False, aspect=2.5, height=3, col_order=original_param_names)
g.map_dataframe(sfp.plotting.scatter_ci_dist, 'indicator', 'fit_value_bs', ci_vals=[5, 95])
for ax in g.axes:
    ax.set_xticklabels(ax.get_xticklabels(), rotation=90)
g.map_dataframe(plt.scatter, 'indicator', 'fit_value')


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:211: UserWarning: Tight layout not applied. tight_layout cannot make axes height small enough to accommodate all axes decorations
  warnings.warn('Tight layout not applied. '
Out[128]:
<seaborn.axisgrid.FacetGrid at 0x7f150674bdd8>

In the above, it's easier to see which subject/sessions differ the most between these two, the following is an attempt to see for which parameters it differs the most.


In [130]:
id_vars = ['model_parameter', 'indicator', 'session', 'subject', 'task']
tmp = pd.melt(compare_df, id_vars,)

In [131]:
g = sns.FacetGrid(tmp.query("session not in ['ses-pilot00', 'ses-pilot01']"), row='subject', col='session', hue='model_parameter', sharey=False, aspect=2.5, height=3, )
g.map_dataframe(sfp.plotting.scatter_ci_dist, 'variable', 'value', ci_vals=[5, 95], x_jitter=True, join=True)
g.add_legend()
g.set(xlim=(-1, 2))


Out[131]:
<seaborn.axisgrid.FacetGrid at 0x7f1505655438>

In [132]:
g = sns.FacetGrid(tmp.query("session not in ['ses-pilot00', 'ses-pilot01']"), col='model_parameter', hue='indicator', col_wrap=4, sharey=False, aspect=2.5, height=3, col_order=original_param_names)
g.map_dataframe(sfp.plotting.scatter_ci_dist, 'variable', 'value', ci_vals=[5, 95], x_jitter=True, join=True)
g.add_legend()
g.set(xlim=(-1, 2))


Out[132]:
<seaborn.axisgrid.FacetGrid at 0x7f1504d92828>

Looking at parameter values, it's hard to interpret what they mean. So we'll make some plots showing the variability in preferred period (as function of eccentricity, angle, and orientation) and amplitude (as function of angle and orientation), across and within subjects.

First, bootstrapping across subjects (using their median)


In [248]:
abs_period = sfp.analyze_model.create_feature_df(models.query("task=='task-sfprescaled'"), )
rel_period = sfp.analyze_model.create_feature_df(models.query("task=='task-sfprescaled'"), reference_frame='relative')

In [250]:
sfp.plotting.feature_df_plot(rel_period)


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[250]:
<seaborn.axisgrid.FacetGrid at 0x7f8ace16af98>

In [252]:
sfp.plotting.feature_df_plot(rel_period, col='subject', pre_boot_gb_func='mean', col_wrap=8, )


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[252]:
<seaborn.axisgrid.FacetGrid at 0x7f8b045c5400>

In [251]:
sfp.plotting.feature_df_plot(rel_period, col=None, pre_boot_gb_func=np.mean)


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[251]:
<seaborn.axisgrid.FacetGrid at 0x7f8ace261898>

In [249]:
sfp.plotting.feature_df_plot(abs_period)


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[249]:
<seaborn.axisgrid.FacetGrid at 0x7f8b1aeadd30>

In [253]:
sfp.plotting.feature_df_plot(abs_period, col='subject', pre_boot_gb_func='mean', col_wrap=8, )


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[253]:
<seaborn.axisgrid.FacetGrid at 0x7f8acdced860>

In [246]:
sfp.plotting.feature_df_plot(abs_period, col=None, row=None, pre_boot_gb_func='mean', )


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[246]:
<seaborn.axisgrid.FacetGrid at 0x7f8ace54d5c0>

In [320]:
rel_period = sfp.analyze_model.create_feature_df(bootstrap_models.query("task=='task-sfprescaled'"), reference_frame='relative', )

In [366]:
g = sfp.plotting.feature_df_plot(rel_period, col='subject', pre_boot_gb_func='mean', plot_func=sfp.plotting.scatter_ci_dist, draw_ctr_pts=False, ci_mode='fill', join=True)


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

In [384]:
period = sfp.analyze_model.create_feature_df(bootstrap_models.query("task=='task-sfprescaled'"), eccentricity=[2, 4, 10], retinotopic_angle=np.linspace(0, 2*np.pi, 49),)

In [385]:
period.head()


Out[385]:
Retinotopic angle (rad) Orientation (rad) Eccentricity (deg) Preferred period (dpc) reference_frame Stimulus type subject bootstrap_num
0 0.000000 0.0 2 0.733216 absolute vertical sub-wlsubj007 0.0
1 0.130900 0.0 2 0.725946 absolute vertical sub-wlsubj007 0.0
2 0.261799 0.0 2 0.705299 absolute vertical sub-wlsubj007 0.0
3 0.392699 0.0 2 0.674504 absolute vertical sub-wlsubj007 0.0
4 0.523599 0.0 2 0.638151 absolute vertical sub-wlsubj007 0.0

In [435]:
g = sfp.plotting.feature_df_polar_plot(period, row='Eccentricity (deg)' , col='subject', plot_func=sfp.plotting.scatter_ci_dist, draw_ctr_pts=False, ci_mode='fill', join=True, r='Preferred period (deg)', 
                                       top=.9, hspace=.3, wspace=.1)


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

In [400]:
period.columns


Out[400]:
Index(['Retinotopic angle (rad)', 'Orientation (rad)', 'Eccentricity (deg)',
       'Preferred period (dpc)', 'reference_frame', 'Stimulus type', 'subject',
       'bootstrap_num'],
      dtype='object')

In [408]:
a = {'a': 1}

In [409]:
a.update({'a': 2})

In [433]:
g = sfp.plotting.feature_df_polar_plot(period[period['Eccentricity (deg)']==2], row='Eccentricity (deg)' , col='subject', plot_func=sfp.plotting.scatter_ci_dist, draw_ctr_pts=False, ci_mode='fill', join=True, r='Preferred period (deg)', 
                                       all_tick_labels=['r', 'theta'], top=.9)


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")

In [390]:
abs_period_contour = sfp.analyze_model.create_feature_df(models, 'preferred_period_contour', )
rel_period_contour = sfp.analyze_model.create_feature_df(models, 'preferred_period_contour', reference_frame='relative')


---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-390-e42dc703ae45> in <module>
----> 1 abs_period_contour = sfp.analyze_model.create_feature_df(models, 'preferred_period_contour', )
      2 rel_period_contour = sfp.analyze_model.create_feature_df(models, 'preferred_period_contour', reference_frame='relative')

/e/3.2/p1/broderick/Documents/spatial-frequency-preferences/sfp/analyze_model.py in create_feature_df(models, feature_type, reference_frame, gb_cols, **kwargs)
    545     df = []
    546     for n, g in models.groupby(gb_cols):
--> 547         m = sfp_model.LogGaussianDonut.init_from_df(g)
    548         if feature_type == 'preferred_period':
    549             df.append(create_preferred_period_df(m, reference_frame, **kwargs))

/e/3.2/p1/broderick/Documents/spatial-frequency-preferences/sfp/model.py in init_from_df(cls, df)
    355         fit_model_type = df.fit_model_type.unique()
    356         if len(fit_model_type) > 1 or len(df) != 11:
--> 357             raise Exception("df must contain exactly one model!")
    358         params = {}
    359         for i, row in df.iterrows():

Exception: df must contain exactly one model!

In [5]:
sfp.plotting.feature_df_polar_plot(abs_period_contour)


Out[5]:
<seaborn.axisgrid.FacetGrid at 0x7f5428308cf8>

In [6]:
sfp.plotting.feature_df_polar_plot(abs_period_contour.query("`Stimulus type` in ['vertical', 'horizontal']"))


Out[6]:
<seaborn.axisgrid.FacetGrid at 0x7f53f818b9e8>

In [7]:
sfp.plotting.feature_df_polar_plot(rel_period_contour)


Out[7]:
<seaborn.axisgrid.FacetGrid at 0x7f53f8a27240>

In [8]:
sfp.plotting.feature_df_polar_plot(rel_period_contour.query("`Stimulus type` in ['angular', 'radial']"))


Out[8]:
<seaborn.axisgrid.FacetGrid at 0x7f5343e13208>

In [17]:
abs_ampl = sfp.analyze_model.create_feature_df(models, 'max_amplitude', )
rel_ampl = sfp.analyze_model.create_feature_df(models, 'max_amplitude', reference_frame='relative')

In [18]:
sfp.plotting.feature_df_polar_plot(abs_ampl, col=None, r='Max amplitude')


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[18]:
<seaborn.axisgrid.FacetGrid at 0x7f53f30dcf28>

In [19]:
sfp.plotting.feature_df_polar_plot(abs_ampl.query("`Stimulus type` in ['vertical', 'horizontal']"), col=None, r='Max amplitude')


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[19]:
<seaborn.axisgrid.FacetGrid at 0x7f53266c65f8>

In [20]:
sfp.plotting.feature_df_polar_plot(rel_ampl, col=None, r='Max amplitude')


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[20]:
<seaborn.axisgrid.FacetGrid at 0x7f53269cd668>

In [21]:
sfp.plotting.feature_df_polar_plot(rel_ampl.query("`Stimulus type` in ['angular', 'radial']"), col=None, r='Max amplitude')


/users-lcv/broderick/miniconda3/envs/sfp/lib/python3.6/site-packages/matplotlib/tight_layout.py:231: UserWarning: tight_layout : falling back to Agg renderer
  warnings.warn("tight_layout : falling back to Agg renderer")
Out[21]:
<seaborn.axisgrid.FacetGrid at 0x7f5325ebde48>

In [96]:
b = sfp.analyze_model.bootstrap_features(feat, 100)

In [391]:
fit_model_type = 'full_donut_full_amps-vary'
angles = np.linspace(0, 2*np.pi, 49)
oris = np.linspace(0, np.pi, 4, endpoint=False)
eccens = np.linspace(0, 11, 11)
period_target = [.5, 1, 1.5]
ori_labels = ['radial', 'forward spiral', 'angular', 'reverse spiral']
abs_ori_labels = ['vertical', 'forward diagonal', 'horizontal', 'reverse diagonal']
angle_labels = ['0', r'$\frac{\pi}{4}$', r'$\frac{\pi}{2}$', r'$\frac{3\pi}{4}$']
pal = sns.color_palette('deep', len(ori_labels))
abs_pal = sns.color_palette('cubehelix', len(abs_ori_labels))

indicators = models.indicator.unique()
all_contours = np.empty((len(indicators), len(period_target), len(angles), len(oris)))
all_rel_contours = np.empty_like(all_contours)
all_periods = np.empty((len(indicators), len(angles), len(oris), len(eccens)))
all_rel_periods = np.empty_like(all_periods)
all_amps = np.empty((len(indicators), len(angles), len(oris)))
all_rel_amps = np.empty_like(all_amps)

for j, ind in enumerate(indicators):
    single_model_df = models.query('indicator==@ind & fit_model_type==@fit_model_type')
    model = sfp.model.LogGaussianDonut.init_from_df(single_model_df)
    for k, p in enumerate(period_target):
        all_contours[j, k, :, :] = model.preferred_period_contour(p, angles, oris).detach().numpy()
        all_rel_contours[j, k, :, :] = model.preferred_period_contour(p, angles, rel_sf_angle=oris).detach().numpy()
    all_amps[j, :, :] = model.max_amplitude(angles, oris).detach().numpy()
    all_rel_amps[j, :, :] = model.max_amplitude(angles, rel_sf_angle=oris).detach().numpy()
    for k, a in enumerate(oris):
        all_rel_periods[j, :, k, :] = model.preferred_period(eccens, angles, rel_sf_angle=a).detach().numpy()
        all_periods[j, :, k, :] = model.preferred_period(eccens, angles, a).detach().numpy()

In [392]:
n_bootstraps = 10000
bootstraps = np.random.randint(0, len(indicators), size=(n_bootstraps, len(indicators)))

contours = np.empty((n_bootstraps, len(period_target), len(angles), len(oris)))
rel_contours = np.empty_like(contours)
periods = np.empty((n_bootstraps, len(angles), len(oris), len(eccens)))
rel_periods = np.empty_like(periods)
amps = np.empty((n_bootstraps, len(angles), len(oris)))
rel_amps = np.empty_like(amps)

for i, b in enumerate(bootstraps):
    periods[i, :, :, :] = np.mean(all_periods[b], 0)
    rel_periods[i, :, :, :] = np.mean(all_rel_periods[b], 0)
    contours[i, :, :, :] = np.mean(all_contours[b], 0)
    rel_contours[i, :, :, :] = np.mean(all_rel_contours[b], 0)
    amps[i, :, :] = np.mean(all_amps[b], 0)
    rel_amps[i, :, :] = np.mean(all_rel_amps[b], 0)

In [208]:
periods_ci = np.percentile(periods, [16, 84], 0)
periods_mn = np.median(periods, 0)
rel_periods_ci = np.percentile(rel_periods, [16, 84], 0)
rel_periods_mn = np.median(rel_periods, 0)
angle_labels_tmp = angle_labels[::2]

fig, axes = plt.subplots(2, 2, figsize=(15, 10), sharey=True, sharex=True,  subplot_kw={'ylim': (0, 2.25)})
for j_idx, j in enumerate(np.where(np.in1d(angles, [0, np.pi/2]))[0]):
    for i in range(4):
        axes[0, j_idx].plot(eccens, periods_mn[j,i,:], label=abs_ori_labels[i], c=abs_pal[i])
        axes[0, j_idx].fill_between(eccens, *periods_ci[:,j,i,:], alpha=.2, color=abs_pal[i])
        axes[1, j_idx].plot(eccens, rel_periods_mn[j,i,:], label=ori_labels[i], c=pal[i])
        axes[1, j_idx].fill_between(eccens, *rel_periods_ci[:,j,i,:], alpha=.2, color=pal[i])
    axes[0,j_idx].set_title('retinotopic angle %s' % angle_labels_tmp[j_idx])
    axes[0,j_idx].legend()
    axes[1,j_idx].legend()
    axes[1, j_idx].set_xlabel("Eccentricity (deg)")
axes[0, 0].set_ylabel("Preferred Period (deg)")
axes[1, 0].set_ylabel("Preferred Period (deg)")
plt.tight_layout()
fig.savefig('all_pref_period.svg')



In [105]:
periods_ci = np.percentile(periods.mean(1), [16, 84], 0)
periods_mn = np.median(periods.mean(1), 0)
rel_periods_ci = np.percentile(rel_periods.mean(1), [16, 84], 0)
rel_periods_mn = np.median(rel_periods.mean(1), 0)

fig, axes = plt.subplots(1, 2, figsize=(15, 5), sharex=True, sharey=True, subplot_kw={'ylim': (0, 2.25)})
for i in range(4):
    axes[0].plot(eccens, periods_mn[i,:], label=abs_ori_labels[i], c=abs_pal[i])
    axes[0].fill_between(eccens, *periods_ci[:,i,:], alpha=.2, color=abs_pal[i])
    axes[1].plot(eccens, rel_periods_mn[i,:], label=ori_labels[i], c=pal[i])
    axes[1].fill_between(eccens, *rel_periods_ci[:,i,:], alpha=.2, color=pal[i])
axes[0].set_title('Averaged across retinotopic angles')
axes[0].legend()
axes[1].set_title('Averaged across retinotopic angles')
axes[1].legend()
axes[0].set_ylabel("Preferred Period (deg)")
axes[0].set_xlabel("Eccentricity (deg)")
axes[1].set_xlabel("Eccentricity (deg)")
plt.tight_layout()
fig.savefig('pref_period.svg')



In [359]:
periods_ci = np.percentile(periods, [16, 84], 0)
periods_mn = np.median(periods, 0)
rel_periods_ci = np.percentile(rel_periods, [16, 84], 0)
rel_periods_mn = np.median(rel_periods, 0)
angle_labels_tmp = angle_labels[::2]

with sns.plotting_context('poster'):
    fig, axes = plt.subplots(1, 2, figsize=(14, 6), sharey=True, sharex=True,  )#subplot_kw={'ylim': (0, 2.25)})
    for j_idx, j in enumerate(np.where(np.in1d(angles, [0, np.pi/2]))[0]):
        for i in range(4):
            axes[j_idx].plot(eccens, rel_periods_mn[j,i,:], label=ori_labels[i], c=pal[i])
            axes[j_idx].fill_between(eccens, *rel_periods_ci[:,j,i,:], alpha=.2, color=pal[i])
        axes[j_idx].set_title('retinotopic angle %s' % angle_labels_tmp[j_idx])
        axes[j_idx].set_xlabel("Eccentricity (deg)")
        axes[j_idx].axhline(color='k', linestyle='--')
        axes[j_idx].axvline(color='k', linestyle='--')
    axes[0].set_ylabel("Preferred Period (deg)")
    plt.tight_layout()
    fig.savefig('all_pref_period.svg')



In [365]:
periods_ci = np.percentile(periods.mean(1), [16, 84], 0)
periods_mn = np.median(periods.mean(1), 0)
rel_periods_ci = np.percentile(rel_periods.mean(1), [16, 84], 0)
rel_periods_mn = np.median(rel_periods.mean(1), 0)

with sns.plotting_context('poster'):
    fig, ax = plt.subplots(1, 1, figsize=(7, 6), sharex=True, sharey=True,)#subplot_kw={'ylim': (0, 2.5)})
    for i in range(4):
        ax.plot(eccens, rel_periods_mn[i,:], label=ori_labels[i], c=pal[i])
        ax.fill_between(eccens, *rel_periods_ci[:,i,:], alpha=.2, color=pal[i])
    ax.set_title('Averaged across retinotopic angles')
    ax.legend()
    ax.axhline(color='k', linestyle='--')
    ax.axvline(color='k', linestyle='--')
    ax.set_ylabel("Preferred Period (deg)")
    ax.set_xlabel("Eccentricity (deg)")
    plt.tight_layout()
    fig.savefig('pref_period.svg')



In [366]:
contours_ci = np.percentile(contours, [16, 84], 0)
contours_mn = contours.mean(0)

# with sns.plotting_context('poster'):
#     fig, axes = plt.subplots(1, 3, figsize=(24, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
#     for j, ax in enumerate(axes.flatten()):
#         for i in [0, 2, ]:
#             ax.plot(angles, contours_mn[j,:,i], label=abs_ori_labels[i], color=abs_pal[i])
#             ax.fill_between(angles, contours_ci[0,j,:,i], contours_ci[1,j,:,i], alpha=.2, color=abs_pal[i])
#         ax.set_title('Preferred period %s' % period_target[j])
#         ax.legend()
#     fig.savefig('abs_contour.svg')
    
with sns.plotting_context('poster'):
    fig, ax = plt.subplots(1, 1, figsize=(7, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
    for i in [0, 2, ]:
        ax.plot(angles, contours_mn[1,:,i], label=abs_ori_labels[i], color=abs_pal[i])
        ax.fill_between(angles, *contours_ci[:,1,:,i], alpha=.2, color=abs_pal[i])
    ax.set_title('Preferred period=%s contour' % period_target[1])
    ax.legend()
    fig.savefig('abs_contour.svg')



In [130]:
contours_ci = np.percentile(rel_contours, [16, 84], 0)
contours_mn = rel_contours.mean(0)

# with sns.plotting_context('poster'):
#     fig, axes = plt.subplots(1, 3, figsize=(24, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
#     for j, ax in enumerate(axes.flatten()):
#         for i in [0, 2]:
#             ax.plot(angles, contours_mn[j,:,i], label=ori_labels[i], color=pal[i])
#             ax.fill_between(angles, contours_ci[0,j,:,i], contours_ci[1,j,:,i], alpha=.2, color=pal[i])
#         ax.set_title('Preferred period %s' % period_target[j])
#         ax.legend()
#     fig.savefig('rel_contour.svg')

with sns.plotting_context('poster'):
    fig, ax = plt.subplots(1, 1, figsize=(7, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
    for i in [0, 2]:
        ax.plot(angles, contours_mn[1,:,i], label=ori_labels[i], color=pal[i])
        ax.fill_between(angles, *contours_ci[:,1,:,i], alpha=.2, color=pal[i])
    ax.set_title('Preferred period %s' % period_target[1])
    ax.legend()
    fig.savefig('rel_contour.svg')



In [219]:
contours_ci = np.percentile(rel_periods, [16, 84], 0)
contours_mn = rel_periods.mean(0)

# with sns.plotting_context('poster'):
#     fig, axes = plt.subplots(1, 3, figsize=(24, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
#     for j, ax in enumerate(axes.flatten()):
#         for i in [0, 2]:
#             ax.plot(angles, contours_mn[j,:,i], label=ori_labels[i], color=pal[i])
#             ax.fill_between(angles, contours_ci[0,j,:,i], contours_ci[1,j,:,i], alpha=.2, color=pal[i])
#         ax.set_title('Preferred period %s' % period_target[j])
#         ax.legend()
#     fig.savefig('rel_contour.svg')

with sns.plotting_context('poster'):
    fig, ax = plt.subplots(1, 1, figsize=(7, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
    for i in [0, 2]:
        ax.plot(angles, contours_mn[:,i,4], label=ori_labels[i], color=pal[i])
        ax.fill_between(angles, *contours_ci[:,:,i,4], alpha=.2, color=pal[i])
    ax.set_title('Preferred period at eccentricity %.01f' % eccens[4])
    ax.legend()
    fig.savefig('rel_pref_period.svg')



In [225]:
contours_ci = np.percentile(periods, [16, 84], 0)
contours_mn = periods.mean(0)

# with sns.plotting_context('poster'):
#     fig, axes = plt.subplots(1, 3, figsize=(24, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
#     for j, ax in enumerate(axes.flatten()):
#         for i in [0, 2]:
#             ax.plot(angles, contours_mn[j,:,i], label=ori_labels[i], color=pal[i])
#             ax.fill_between(angles, contours_ci[0,j,:,i], contours_ci[1,j,:,i], alpha=.2, color=pal[i])
#         ax.set_title('Preferred period %s' % period_target[j])
#         ax.legend()
#     fig.savefig('rel_contour.svg')

with sns.plotting_context('poster'):
    fig, ax = plt.subplots(1, 1, figsize=(7, 7), subplot_kw={'projection': 'polar'}, sharex=True, sharey=True)
    for i in [0, 2]:
        ax.plot(angles, contours_mn[:,i,4], label=abs_ori_labels[i], color=abs_pal[i])
        ax.fill_between(angles, *contours_ci[:,:,i,4], alpha=.2, color=abs_pal[i])
    ax.set_title('Preferred period at eccentricity %.01f' % eccens[4])
    ax.legend()
    fig.savefig('abs_pref_period.svg')



In [449]:
amps_ci = np.percentile(amps, [16, 84], 0)
amps_mn = amps.mean(0)

with sns.plotting_context('poster'):
    fig, ax = plt.subplots(1, 1, figsize=(7, 7), subplot_kw={'projection': 'polar'})
    for i in [0, 2,1,3]:
        ax.plot(angles, amps_mn[:,i], label=abs_ori_labels[i], color=abs_pal[i])
        ax.fill_between(angles, *amps_ci[:,:,i], alpha=.2, color=abs_pal[i])
    ax.set_title('Max amplitude')
    ax.legend()
    ax.set_ylim((0, 1.2))
    ax.set(yticklabels=['', .4, '', '', 1])
    fig.savefig('abs_amps.svg')



In [450]:
amps_ci = np.percentile(rel_amps, [16, 84], 0)
amps_mn = rel_amps.mean(0)

with sns.plotting_context('poster'):
    fig, ax = plt.subplots(1, 1, figsize=(7,7), subplot_kw={'projection': 'polar'})
    for i in [0, 2,1,3]:
        ax.plot(angles, amps_mn[:,i], label=ori_labels[i], color=pal[i])
        ax.fill_between(angles, *amps_ci[:,:,i], alpha=.2, color=pal[i])
    ax.set_title('Max amplitude')
    ax.legend()
    ax.set_ylim((0, 1.2))
    ax.set(yticklabels=['', .4, '', '', 1])
    fig.savefig('rel_amps.svg')



In [245]:
with sns.plotting_context('poster'):
    fig, axes = plt.subplots(1, 7, figsize=(49, 7), subplot_kw={'projection': 'polar'})
    for j, ax in enumerate(axes.flatten()):
        for i in [0, 2]:
            ax.plot(angles, all_amps[j,:,i], label=abs_ori_labels[i], color=abs_pal[i])
        ax.set_title('Max amplitude')
        ax.legend()
        ax.set_ylim((0, 1.2))
        ax.set(yticklabels=['', .4, '', '', 1])



In [394]:
with sns.plotting_context('poster'):
    fig, axes = plt.subplots(1, 7, figsize=(49, 7), subplot_kw={'projection': 'polar'})
    for j, ax in enumerate(axes.flatten()):
        for i in [0, 2]:
            ax.plot(angles, all_rel_amps[j,:,i], label=ori_labels[i], color=pal[i])
        ax.set_title('Max amplitude')
        ax.legend()
        ax.set_ylim((0, 1.2))
        ax.set(yticklabels=['', .4, '', '', 1])



In [246]:
with sns.plotting_context('poster'):
    fig, axes = plt.subplots(1, 7, figsize=(49, 7), subplot_kw={'projection': 'polar'})
    for j, ax in enumerate(axes.flatten()):
        for i in [0, 2]:
            ax.plot(angles, all_contours[j,1,:,i], label=abs_ori_labels[i], color=abs_pal[i])



In [393]:
with sns.plotting_context('poster'):
    fig, axes = plt.subplots(1, 7, figsize=(49, 7), subplot_kw={'projection': 'polar'})
    for j, ax in enumerate(axes.flatten()):
        for i in [0, 2]:
            ax.plot(angles, all_rel_contours[j,1,:,i], label=ori_labels[i], color=pal[i])



In [383]:
original_param_names = ['sigma', 'sf_ecc_slope', 'sf_ecc_intercept', 'abs_mode_cardinals', 'abs_mode_obliques', 'rel_mode_cardinals', 'rel_mode_obliques',
                        'abs_amplitude_cardinals', 'abs_amplitude_obliques', 'rel_amplitude_cardinals', 'rel_amplitude_obliques']
rename_params = dict((k, v) for k, v in zip(original_param_names, parameters))
tmp = models.query('fit_model_type==@fit_model_type')
tmp = tmp.set_index('model_parameter')
tmp.loc['sigma', 'param_category'] = 'sigma'
tmp.loc[['sf_ecc_slope', 'sf_ecc_intercept'], 'param_category'] = 'eccen'
tmp.loc[['abs_mode_cardinals', 'abs_mode_obliques', 'rel_mode_cardinals', 'rel_mode_obliques',
         'abs_amplitude_cardinals', 'abs_amplitude_obliques', 'rel_amplitude_cardinals', 'rel_amplitude_obliques'], 'param_category'] = 'orientation'
tmp = tmp.reset_index()
tmp['model_parameter'] = tmp.model_parameter.map(rename_params)

In [448]:
with sns.plotting_context('poster', font_scale=1):
    fig, axes = plt.subplots(1, 3, figsize=(20, 10), gridspec_kw={'width_ratios': [.15, .3, .6]})
    for i, ax in enumerate(axes):
        cat = ['sigma', 'eccen', 'orientation'][i]
        tmp_order = [i for i in parameters if i in tmp.query("param_category==@cat").model_parameter.unique()]
        sns.stripplot('model_parameter', 'fit_value', 'indicator', data=tmp.query("param_category==@cat"), s=15, ax=ax, order=tmp_order)
        if ax.legend_:
            ax.legend_.remove()
        if i==2:
            ax.set(ylim=(-.25, .25))
        ax.axhline(color='grey', linestyle='dashed')
        ax.set(ylabel='Fit value', xlabel='Parameter')
    #fig.savefig('param_vals.svg')