# Bayesci Negatif Olmayan Matris Ayrışımı için Aktif Eleman Seçimi
# Active Selection of Elements for Bayesian Nonnegative Matrix Factorization

  Burak Suyunu, Gönül Aycı, A.Taylan Cemgil
 * Bilgisayar Mühendisliği Bölümü, Boğaziçi Üniversitesi
 * {burak.suyunu, gonul.ayci, taylan.cemgil}@boun.edu.tr

## Özetçe

Klasik matris tamamlama problemlerinde elimizdeki matris, gözlemlenen ve bilinmeyen elemanlar olarak iki gruba ayrılabilir. Bu çalışmadaki yaklaşımda ise matrisler üç farklı gruptan oluşmaktadır: bilinen ve masrafsız olarak her zaman erişebildigimiz gözlemlenmiş olan veri, tahmin etmeye çalıştığımız bilinmeyen veri ve şu an bilinmeyen ancak istenildigi zaman sorgulanabilen veri. Son gruptaki veriler ilk kez sorgulandığında bir maliyet ortaya çıkmaktadır. Bu gözlemden yola çıkarak, mümkün olduğu kadar az sorgu yaparak ikinci gruptaki bilinmeyen verideki değerleri en az hata ile tahminlemek istiyoruz. Amacımız, sorgulamaya çalıştığımız gözlemleri akıllıca seçebilmek. Bu çalışmamızda, gözlem sırası seçme stratejileri tanımlanarak MovieLens veri setinde karşılaştırılmıştır.

## Abstract
In classical matrix completion problems, you can divide the matrix into two groups as observed and unknown elements. In this study, the matrices are composed of three different groups: observed data that is known and accessible at any time without any expense, unknown data that we are trying to predict, and data that is currently unknown but can be queried when desired. When the data in the last group is queried for the first time, a cost arises. From this observation, we want to estimate the unknown data in the second group with the least error by making as few queries as possible. Our goal is to choose the observations we are trying to question wisely. In this study, observation sequence selections were defined and compared in
the MovieLens data set.

## Giriş
Negatif olmayan matris ayrıştırma yöntemi (NOMA), ilk olarak Lee ve Seung [1] tarafından önerilmiştir. Negatif olmayan matris ayrıştırması, verilen negatif olmayan bir *X* matrisinin negatif olamayan değerler içeren *T* ve *V* çarpanlarına ayırma yöntemidir. Elde edilen iki matrisin çarpımlarının değeri ayrıştırılan matrisin değerine yaklaşık olarak eşittir.

Öneri sistemlerinde ayrışım tabanlı yapılar sıklıkla kullanılmaktadır. Sistemin mantığı şu şekilde açıklanabilir. Bir *X* matrisi ile belirli bir süre aralıgında toplanmış olan kullanıcı-film ilişkisine dair verimizi gösterelim. Bu matrisin satırları filmleri, sütunları kullanıcıları, elemanları ise kullanıcıların filmlere vermiş oldugu puanlamaları temsil etmektedir. Eğer *X* matrisinin herhangi bir elemanının değeri yok ise, bu kullanıcı ve film arasında henüz bir ilişkinin olmadığını göstermektedir yani kullanıcı bu filme henüz herhangi bir oy vermemiştir. Kullanıcılara etkileşimli olarak film hakkındaki puanlamaları sorulabilir. Ancak herkesin her zaman her film hakkında puanlama yapması mümkün degildir. Dolayısıyla bir kişiye bir film hakkındaki puanını sorarken akıllıca bir yol izlemek gerekmektedir. Bu şekilde en az kişiden bilgi talep ederek, elde edilen bilgilerle kullanıcı-film arasındaki örüntüye ulaşmak ve bilmedigimiz veriler hakkındaki tahminlerimizi iyileştirmek istenmektedir.

Şekil 1: **Senaryonun işlenişi:** Burada mavi hücreler maskelenmiş, kırmızılar test ve beyazlar ise gözlemlenmiş veriyi göstermektedir. Kırmızının koyuluğu hatanın fazlalığına işaret etmektedir. Bir veri gözlemlendiginde en çok bulunduğu satır ve sütun hakkında bilgi vermesi beklenmektedir. *Senaryo B*’nin test hücreleri hakkında verdiği bilgi *Senaryo A*’dan fazla olduğu görülmektedir.

**Senaryo:** Elimizde bir *X* matrisimiz olsun. Verilen *X* matrisine Gibbs örnekleyicisi ile negatif olmayan matris ayrıştırması yöntemi kullanarak yaklaşım yapıyoruz. Matrisimizi
bildiğimiz, bilmediğimiz (tahmin etmeye çalıştığımız) ve zaman içinde açarak gözlemleyeceğimiz (maskelenmiş) üç çeşit veriden oluşturuyoruz. Üçüncü kategorideki veriden belli yığınlarda ve minimum sayıda veri açarak maksimum bilgi edinmeyi ve en iyi tahminleme yapmayı hedefliyoruz. Bu gruptaki veriyi nasıl seçecegimiz konusunda tanımladığımız çeşitli gözlem sırası seçme stratejilerimizi karşılaştırıyoruz.

```python
import numpy as np
import scipy as sp
import math
import time
from scipy import special
from scipy.stats import gamma
from scipy.stats import entropy
from scipy.integrate import simps
import matplotlib.pyplot as plt
import matplotlib
from sklearn import preprocessing
import random
import pandas as pd
from IPython.html.widgets import *
import operator
```

    C:\Users\Burki\Anaconda3\lib\site-packages\IPython\html.py:14: ShimWarning: The `IPython.html` package has been deprecated. You should import from `notebook` instead. `IPython.html.widgets` has moved to `ipywidgets`.
      "`IPython.html.widgets` has moved to `ipywidgets`.", ShimWarning)

## MovieLens Veri Seti

[MovieLens][1] veri setinde 700 kullanıcının 9000 filme vermiş olduğu 0,5 ile 5 puan arasında degişen toplam 100.000 oy bulunmaktadır. Modelimizdeki çok terimli dagılıma uygun bir girdi oluşturmak için oy puan aralıgını 0,5-5 ten 1-10 aralığına eşledik.

[1]:https://grouplens.org/datasets/movielens/latest/

```python
df_MovieLens = pd.read_csv('ratings.csv')
df_MovieLens
```

      userId
      movieId
      rating
      timestamp

      0
      1
      31
      2.5
      1260759144

      1
      1
      1029
      3.0
      1260759179

      2
      1
      1061
      3.0
      1260759182

      3
      1
      1129
      2.0
      1260759185

      4
      1
      1172
      4.0
      1260759205

      5
      1
      1263
      2.0
      1260759151

      6
      1
      1287
      2.0
      1260759187

      7
      1
      1293
      2.0
      1260759148

      8
      1
      1339
      3.5
      1260759125

      9
      1
      1343
      2.0
      1260759131

      10
      1
      1371
      2.5
      1260759135

      11
      1
      1405
      1.0
      1260759203

      12
      1
      1953
      4.0
      1260759191

      13
      1
      2105
      4.0
      1260759139

      14
      1
      2150
      3.0
      1260759194

      15
      1
      2193
      2.0
      1260759198

      16
      1
      2294
      2.0
      1260759108

      17
      1
      2455
      2.5
      1260759113

      18
      1
      2968
      1.0
      1260759200

      19
      1
      3671
      3.0
      1260759117

      20
      2
      10
      4.0
      835355493

      21
      2
      17
      5.0
      835355681

      22
      2
      39
      5.0
      835355604

      23
      2
      47
      4.0
      835355552

      24
      2
      50
      4.0
      835355586

      25
      2
      52
      3.0
      835356031

      26
      2
      62
      3.0
      835355749

      27
      2
      110
      4.0
      835355532

      28
      2
      144
      3.0
      835356016

      29
      2
      150
      5.0
      835355395

      ...
      ...
      ...
      ...
      ...

      99974
      671
      4034
      4.5
      1064245493

      99975
      671
      4306
      5.0
      1064245548

      99976
      671
      4308
      3.5
      1065111985

      99977
      671
      4880
      4.0
      1065111973

      99978
      671
      4886
      5.0
      1064245488

      99979
      671
      4896
      5.0
      1065111996

      99980
      671
      4963
      4.5
      1065111855

      99981
      671
      4973
      4.5
      1064245471

      99982
      671
      4993
      5.0
      1064245483

      99983
      671
      4995
      4.0
      1064891537

      99984
      671
      5010
      2.0
      1066793004

      99985
      671
      5218
      2.0
      1065111990

      99986
      671
      5299
      3.0
      1065112004

      99987
      671
      5349
      4.0
      1065111863

      99988
      671
      5377
      4.0
      1064245557

      99989
      671
      5445
      4.5
      1064891627

      99990
      671
      5464
      3.0
      1064891549

      99991
      671
      5669
      4.0
      1063502711

      99992
      671
      5816
      4.0
      1065111963

      99993
      671
      5902
      3.5
      1064245507

      99994
      671
      5952
      5.0
      1063502716

      99995
      671
      5989
      4.0
      1064890625

      99996
      671
      5991
      4.5
      1064245387

      99997
      671
      5995
      4.0
      1066793014

      99998
      671
      6212
      2.5
      1065149436

      99999
      671
      6268
      2.5
      1065579370

      100000
      671
      6269
      4.0
      1065149201

      100001
      671
      6365
      4.0
      1070940363

      100002
      671
      6385
      2.5
      1070979663

      100003
      671
      6565
      3.5
      1074784724

100004 rows × 4 columns

```python
df_MovieLens.describe()
```

      userId
      movieId
      rating
      timestamp

      count
      100004.000000
      100004.000000
      100004.000000
      1.000040e+05

      mean
      347.011310
      12548.664363
      3.543608
      1.129639e+09

      std
      195.163838
      26369.198969
      1.058064
      1.916858e+08

      min
      1.000000
      1.000000
      0.500000
      7.896520e+08

      25%
      182.000000
      1028.000000
      3.000000
      9.658478e+08

      50%
      367.000000
      2406.500000
      4.000000
      1.110422e+09

      75%
      520.000000
      5418.000000
      4.000000
      1.296192e+09

      max
      671.000000
      163949.000000
      5.000000
      1.476641e+09

```python
movieWatchedCount = {}
userWatchCount = {}
```

```python
for m in df_MovieLens.movieId.unique():
    movieWatchedCount[m] = len(df_MovieLens[df_MovieLens.movieId == m])
for u in df_MovieLens.userId.unique():
    userWatchCount[u] = len(df_MovieLens[df_MovieLens.userId == u])
```

```python
sorted_movieWatchedCount = sorted(movieWatchedCount.items(), key=operator.itemgetter(1), reverse=True)
sorted_userWatchCount = sorted(userWatchCount.items(), key=operator.itemgetter(1), reverse=True)
```

### Veri Matrisinin Oluşturulması

Yoğun veya seyrek matris oluşturma isteğinize göre alttaki hücrelerden sadece birini çalıştırın.

Oluşturduğumuz matrisin satırları filmleri, sütunları kullanıcıları, elemanları ise kullanıcıların filmlere vermiş olduğu puanlamaları temsil etmektedir.

#### Yoğun Matrisin Üretilmesi

Yoğun veri seti için MovieLens’ten en çok film izlemiş 20 kullanıcı ve en çok izlenen 50 filmi kullandık.

```python
# Number of Rows
# nu: 1 -> W
movieCount = 50

# Number of Columns
# tao: 1 -> K
userCount = 20

topMovies = np.asarray(sorted_movieWatchedCount)[:movieCount,0]
topUsers = np.asarray(sorted_userWatchCount)[:userCount,0]

userMovie = np.zeros((movieCount, userCount))

for i, m in enumerate(topMovies):
    for j, u in enumerate(topUsers):
        if len(df_MovieLens[(df_MovieLens.userId  == u) & (df_MovieLens.movieId == m)]) != 0:
            userMovie[i][j] = 2*float(df_MovieLens[(df_MovieLens.userId  == u) & (df_MovieLens.movieId == m)]['rating'])
```

#### Seyrek Matrisin Üretilmesi

Seyrek veri için ise her seferinde rastgele en çok izlenen 1000 filmden 50, en çok film izlemiş 200 kullanıcıdan 20 tanesini kullandık.

```python
# Number of Rows
# nu: 1 -> W
movieCount = 50

# Number of Columns
# tao: 1 -> K
userCount = 20

sparsityParameter = 13

movieIndex = np.random.permutation(movieCount*sparsityParameter)[:movieCount]
userIndex = np.random.permutation(userCount*sparsityParameter)[:userCount]

topMovies = []
topUsers = []

for mI in movieIndex:
    topMovies.append(sorted_movieWatchedCount[mI][0])
for uI in userIndex:
    topUsers.append(sorted_userWatchCount[uI][0])

topMovies = np.asarray(topMovies)
topUsers = np.asarray(topUsers)

userMovie = np.zeros((movieCount, userCount))

for i, m in enumerate(topMovies):
    for j, u in enumerate(topUsers):
        if len(df_MovieLens[(df_MovieLens.userId  == u) & (df_MovieLens.movieId == m)]) != 0:
            userMovie[i][j] = 2*float(df_MovieLens[(df_MovieLens.userId  == u) & (df_MovieLens.movieId == m)]['rating'])
```

### Maskeleme

Eksik veriyi yani $x_{\nu, \tau}$ gözlemlenmemiş değerlerini modellerken *X* matrisi ile aynı boyuta sahip olan $\textit{M} = \left \{ m_{\nu, \tau} \right \}$  *maske matrisi* aşağıdaki gibi tanımlanır:

$m_{\nu, \tau} = \left\{\begin{matrix}
 0,& x_{\nu, \tau} \text{ gözlemlenmemişse},\\
 1,& x_{\nu, \tau} \text{ gözlemlenmişse}.
\end{matrix}\right.$

Bu maskeyi kullanarak olabilirlik fonksiyonunu şu şekilde yazılır:

\begin{align*}
p(X,S\mid T,V) = \prod_{\nu, \tau}\left ( p\left (x_{\nu, \tau}\mid s_{\nu, 1:I, \tau}\right ) p\left (s_{\nu, 1:I, \tau}\mid t_{\nu, 1:I}, v_{1:I, \tau}  \right )\right )^{m_{\nu, \tau}}
\end{align*}

### Maskeleme Metodları

Bu çalışmamızda veriye *kısmi* ve *tam* olarak iki farklı açıdan yaklaşarak KL ıraksayı ve KOKH metrikleri (ileride açıklanacak) ile karşılaştırmalar yaptık.

Kısmi yaklaşımda verimizi %30 test, %69 maskelenmiş, %1 başlangıçta bilinen olarak ayırdık. Maskelenmiş veriyi açtıkça değişen test üzerindeki hatayı ölçtük.

Tam yaklaşımda ise %99 maskelenmiş, %1 başlangıçta bilinen olarak ayırdık ve hatayı bütün veri üzerinden ölçtük.

```python
def randomMasking(W, K, dataExistance):
    dataIndices = np.argwhere(dataExistance>0)

    #test, mask, known
    mask = np.zeros([W,K])
    test = np.copy(dataExistance)

    np.random.shuffle(dataIndices)

    for i in range(30*len(dataIndices)//100):
        test[dataIndices[i][0], dataIndices[i][1]] = 0
    for i in range(30*len(dataIndices)//100, 31*len(dataIndices)//100):
        mask[dataIndices[i][0], dataIndices[i][1]] = 1

    return mask, test
```

### GÖZLEM SIRASI SEÇME STRATEJİLERİ

Bu çalışmada, maskelenmiş olan veriyi gözlemlemek için beş farklı gözlem sırası seçme stratejisi tanımladık.

Amacımız, en hızlı şekilde bütün veriyi ögrenebileceğimiz (yakınsayabilecegimiz) bilgi elde etme stratejisini bulmak ve ögrenme sonucunda test verisi hakkında doğru tahminlemede bulunmak.

#### Rastgele Strateji

**Tanım**: Rastgele bir pozisyonda yer alan maskelenmiş veri gözlemlenir.

Gözlemlenmiş verideki bilgiyi kullanmadan maskelenmiş veriyi açar. Tanımlanan diğer stratejilerin işlevselliğini değerlendirmek için bir alt sınır oluşturur.

```python
def openRandom(W, K, mask, test):
    openOrder = np.arange(W*K)
    random.shuffle(openOrder)

    for i in openOrder:
        if mask[i//K][i%K] == 0 and test[i//K][i%K] == 1:
            return i//K, i%K

    return -1, -1
```

#### Satır-Sütun Stratejileri

**Tanım**: Satır ve sütunlar en az veri gözlemlenenden en çok veri gözlemlenene dogru sıralanır. Satıra veya sütuna öncelik vererek kesişimlerindeki ilk maskelenmiş veri açılır.

Bir veriyi tahmin ederken o veri hakkında en çok bilgi edinebilecegimiz yerler o verinin bulunduğu satır ve sütunundaki diger verilerdir. Düzenli olarak satır ve sütunlardaki gözlemlenmiş veri sayısını artırarak bütün veri hakkındaki tahminimizi iyileştirebiliriz.

```python
def openMaxColMask(W, K, mask, test):
    colSum = mask.sum(0)
    rowSum = mask.sum(1)

    colMins = colSum.argsort()
    rowMins = rowSum.argsort()

    for c in colMins:
        for r in rowMins:
            if mask[r][c] == 0 and test[r][c] == 1:
                return r, c

    return openRandom(W, K, mask, test)

```

```python
def openMaxRowMask(W, K, mask, test):
    colSum = mask.sum(0)
    rowSum = mask.sum(1)

    colMins = colSum.argsort()
    rowMins = rowSum.argsort()

    for r in rowMins:
        for c in colMins:
            if mask[r][c] == 0 and test[r][c] == 1:
                return r, c

    return openRandom(W, K, mask, test)
```

#### Maskelenmiş Verilerin Varyansı Stratejileri

**Tanım**: Varyansın en küçük veya en büyük oldugu pozisyondaki veri gözlemlenir.

Gibbs örnekleyicisi sonucunda *T* ve *V* matrislerinin yanında bunları oluşturan *Gamma* dağılımı parametreleri de elde ediliyor. Bu parametreleri kullanarak belirlenen sayıda T ve V örnekleyerek tahminler üretilir. Bu tahminlerle, maskelenmiş olan kısımdaki her bir veri tahmininin varyansı hesaplanır. Bir pozisyondaki varyansın büyük olması, o pozisyon için üretilen tahmin değerinin belirsizliğinin fazla oldugunu göstermektedir. Küçük varyans ise belirsizliğin az olduğu yerdir.

```python
def openMinVar(W, K, mask, test, xCandidates):
    var_candidate = np.var(xCandidates, axis=0)

    ind = var_candidate.flatten().argsort()

    for i in ind:
        if mask[i//K][i%K] == 0 and test[i//K][i%K] == 1:
            return i//K, i%K

    return -1, -1
```

```python
def openMaxVar(W, K, mask, test, xCandidates):
    var_candidate = np.var(xCandidates, axis=0)

    ind = var_candidate.flatten().argsort()[::-1]

    for i in ind:
        if mask[i//K][i%K] == 0 and test[i//K][i%K] == 1:
            return i//K, i%K

    return -1, -1
```

#### Gözlemlenmiş Satır Varyans Stratejisi

**Tanım**: Her satır için o satırdaki açılmış olan veriler üzerinden o satırın varyansı hesaplanır. Satırlar bu varyans hesabına göre büyükten küçüge sıralanır. Sütunlar en az veri gözlemlenenden en çok veri gözlemlenene dogru sıralanır. Satıra öncelik vererek sütunlarla olan kesişimindeki ilk maskelenmiş veri açılır.

Veri setimizde satırlar filmlere karşılık gelmektedir. Bu stratejimizde her filme verilen puanın varyansı hesaplanır. Genellikle bir filme benzer puanlar verilmesi beklenir. Burada,
varyansı büyük olan filmlerin puan aralığı daha geniştir. Bu filmlere verilen puanların tahmin edilmesi daha zordur.

```python
def openRowVarColMask(W, K, mask, test, xCandidate):
    mean_candidate = mask*xCandidate
    rowIndVarSorted = np.argsort(np.nan_to_num(np.nanvar(np.where(mean_candidate!=0,mean_candidate,np.nan), axis=1)))[::-1]

    colSum = mask.sum(0)
    colMins = colSum.argsort()

    for r in rowIndVarSorted:
        for c in colMins:
            if mask[r][c] == 0 and test[r][c] == 1:
                return r, c

    return openRandom(W, K, mask, test)

```

## Başlatma

### Model seçimi

Önerdigimiz yöntemi MovieLens verisi üzerinde deniyoruz. Burada $W = 50$ (satır/film sayısı), $K = 20$ (sütun/kullanıcı sayısı) ve kaynakların sayısı $I = 4$ olarak belirledik. Gerçek modelin hiperparametrelerini ise $a^{t} = b^{t} = 1$ ve $a^{v} = b^{v} = 1$ olarak aldık.

```python
# Number of Rows
# nu: 1 -> W
W = movieCount;

# Number of Columns
# tao: 1 -> K
K = userCount

# Number of templates
I = 4;

# Set prior parameters
A_t = np.ones([W,I]) # Shape
B_t = np.ones([W,I]) # Scale
A_v = np.ones([I,K])
B_v = np.ones([I,K])

# Generate a random template and excitation
orgT = T = np.random.gamma(A_t,B_t)
orgV = V = np.random.gamma(A_v,B_v)

x = userMovie.copy()
```

```python
#strategyLabels = ["Random", "Row Mask Eq", "Min Var", "Max Var", "Row Var Col Mask"]
strategyLabels = ["Rastgele", "Satır-Sütun", "Min Varyans", "Maks Varyans", "Satır Varyans"]
strategyColors = ["b","r","y","k","m"]

#errorMetricLabels = ["RMSE_Partial", "RMSE_Full", "KL_Partial", "KL_Full"]
errorMetricLabels = ["KOKH Kısmi", "KOKH Tam", "KL Iraksayı Kısmi", "KL Iraksayı Tam"]

# dataExistance: True if data exist, False othwerwise
# mask: True if mask opened, False if masked
# test: True if not test data and data  exist, False if test data or no data exist
#       For testing we use dataExistance and test together
#       For cell opening we use test with mask
dataExistance = userMovie > 0
mask, test = randomMasking(W,K,dataExistance)

allLikelihood = []
allOpenedCells = []

allDiffRMSEPartial = []
allDiffRMSEFull = []
allDiffKLPartial = []
allDiffKLFull = []

allErrorDiffs = []

allRMSEPartial = []
allRMSEFull = []
allKLPartial = []
allKLFull = []

allError = []

allEstimationVariance = []

allStrategyLabels = []
allStrategyColors = []

KLsampleSize = 1000
```

#### Olabilirlik hesabı

```python
def calculateLikelihood(x, xPredicted, test, dataExistance, W, K):
    lh = 0
    for w in range(W):
        for k in range(K):
            lh += (dataExistance[w,k]^test[w,k]) * (x[w,k] * np.log(xPredicted[w,k]) - xPredicted[w,k] - special.gammaln(x[w,k]+1))
    return lh
```

#### Adayların örneklenmesi

```python
def sampleCandidates(a_t, b_t, a_v, b_v, sampleSize=100):
    xCandidates = []
    for i in range(sampleSize):
        T = np.random.gamma(a_t,b_t)
        V = np.random.gamma(a_v,b_v)

        xCandidates.append(np.dot(T, V))

    return np.asarray(xCandidates)
```

#### Varyansın hesaplanması ve örneklenen X adaylarından gelen hata

#### Değerlendirme Metrikleri

Yaklaşımımızın performansını değerlendirmek için çeşitli yöntemler bulunmaktadır. Bu çalışmamızda, popüler olan Kullback-Leibler (KL) ıraksayı ve Kök Ortalama Kare Hata (KOKH) olmak üzere iki ayrı metrik kullandık. Bu metrikler sırasıyla aşağıdaki gibi tanımlanır:

\begin{align*}
D_{KL}\left ( X \parallel \widehat{X} \right ) = \sum_{i,j} X\left ( i,j \right ) \log \frac{X\left ( i,j \right )}{\widehat{X}\left ( i,j \right )}
\end{align*}

\begin{align*}
KOKH = \sqrt{\frac{1}{X_{test}}\sum_{i,j}\left ( X(i,j) -  \widehat{X}(i,j)\right )^{2}}
\end{align*}
Burada, $\widehat{X}(i,j)$ önerilen metod tarafından *i* filmi hakkında *j* kullanıcısının verdiği tahmin edilen oylama değerini ve $X_{test}$ ise, toplam test edilen oylama sayısını göstermektedir.

```python
def calculateVarianceAndError(x, test, dataExistance, xCandidates):
    mean_candidate = np.mean(xCandidates, axis=0)
    varEst = np.var(xCandidates, axis=0)
    varEst = 1.0 * (varEst - varEst.min()) / (varEst.max() - varEst.min())

    diffMeanEst = abs((dataExistance^test)*mean_candidate - (dataExistance^test)*x)

    return varEst, diffMeanEst
```

#### Adayların dağılıma dönüştürülmesi

```python
def transformCandidatesToDistributions(candidates, sampleSize, test, dataExistance):
    candidates = np.round(candidates)
    candidates = np.minimum(candidates,10*np.ones(candidates.shape)).astype(int)
    candidates = np.maximum(candidates,np.ones(candidates.shape)).astype(int)
    candidates = (dataExistance^test) * candidates

    candidateDistributions = []

    for i in range(W):
        for j in range(K):
            if candidates[0,i,j] != 0:
                y = np.bincount(candidates[:,i,j])
                c = np.zeros(11)
                c[:len(y)] += y
                c = np.maximum(c,0.00000001*np.ones(c.shape))
                c /= sampleSize
                candidateDistributions.append(c[1:])
            else:
                candidateDistributions.append(np.ones(10))

    return candidateDistributions
```

#### KL ıraksayı hesabı

```python
def calculateKLdivergence(xCandidates, bestCandidateDistributions, sampleSize, test, dataExistance):
    xCandidateDistributions = transformCandidatesToDistributions(xCandidates, sampleSize, test, dataExistance)
    return entropy(np.asarray(xCandidateDistributions).T, np.asarray(bestCandidateDistributions).T).reshape((W,K))

```

## Gibbs Örnekleyicisi

*Monte Carlo* metodları [4, 5] beklentileri tahmin etmek için güçlü hesaplama teknikleridir.
*Markov Zinciri Monte Carlo* teknikleri, geçiş çekirdeği $\mathcal{T}$ tarafından tanımlanan bir Markov zincirinden sonraki örnekleri üretir yani $x^{i}$'e şartlanmış $x^{i+1}$ şu şekilde üretilir:

\begin{equation}
x^{i+1} \sim \mathcal{T}(x\mid x^{i})
\end{equation}

İstenen dağılım durgun dağılım olacak şekilde bir $\mathcal{T}$ geçiş çekirdeği tasarlamak. Özellikle kullanışlı ve basit bir prosedür olan Gibbs örnekleyicisi'nde her değişken *tam koşullu dağılımlardan* örneklenir. NOMA modeli için Gibbs örnekleyicisi,

\begin{eqnarray}
S^{n+1} &\sim & p(S\mid T^{n}, V^{n}, X, \theta), \nonumber \\
T^{n+1} &\sim & p(T\mid V^{n}, S^{n+1}, X, \theta), \\
V^{n+1} &\sim & p(V\mid S^{n+1}, T^{n+1}, X, \theta). \nonumber
\end{eqnarray}

Sabit nokta döngüsü saklı kaynaklar olarak adlandırdığımız $S_{i} = \left \{ s_{\nu,i,\tau} \right \}$ için $(m_{\nu, \tau}=1)$  aşağıdaki gibi bulunur:

\begin{eqnarray}
q(s_{\nu,1:I,\tau}) &=& \mathcal{M} (s_{\nu,1:I, \tau}; x_{\nu,\tau}, p_{\nu, 1:I, \tau}) \\
p_{\nu, i, \tau} &=& \frac{exp(\left \langle \log t_{\nu,i} \right \rangle + \left \langle \log v_{i,\tau} \right \rangle)}{\sum_{i} exp(\left \langle \log t_{\nu,i} \right \rangle + \left \langle \log v_{i, \tau} \right \rangle)} \\
\end{eqnarray}

Şablon $T$ ve katsayı $V$ matrislerinin dağılımları ve onların yeterli istatistikleri Gamma dağılımının özelliklerini takip eder:

\begin{eqnarray}
q(t_{\nu,i}) &=& \mathcal{G} (t_{\nu,i}; \alpha_{\nu,i}^{t}, \beta_{\nu,i}^{t}) \\
\alpha_{\nu, i}^{t} &=& a^{t} + \sum_{\tau} m_{\nu, \tau} \left \langle s_{\nu, i, \tau} \right \rangle \\
\beta_{\nu, i}^{t} &=& \left ( \frac{a^{t}}{b^{t}} + \sum_{\tau } m_{\nu, \tau} \left \langle v_{i, \tau} \right \rangle \right )^{-1} \\
q(v_{i,\tau}) &=& \mathcal{G} (v_{i,\tau}; \alpha_{i,\tau}^{v}, \beta_{i,\tau}^{v}) \\
\alpha_{i, \tau}^{v} &=& a^{v} + \sum_{\nu } m_{\nu, \tau} \left \langle s_{\nu, i, \tau} \right \rangle \\
\beta_{i, \tau}^{v} &=& \left ( \frac{a^{v}}{b^{v}} + \sum_{\nu } m_{\nu, \tau} \left \langle t_{\nu, i} \right \rangle \right )^{-1}
\end{eqnarray}

```python
def gibbsSampler(x, T, V, maskX, MAXITER, likelihood = None, test = None, dataExistance = None):
    W = T.shape[0]
    K = V.shape[1]
    I = T.shape[1]
    tt = 0
    t00 = time.time()

    for n in range(MAXITER):
        Tprev = T.copy()
        Vprev = V.copy()

        S = np.ones([I,W,K])

        t0 = time.time()

        # Sample Sources
        TdotV = np.dot(Tprev, Vprev)
        p = np.einsum('i...,k...',Tprev, np.transpose(Vprev))/np.array([TdotV]*I)
        for nu in range(W):
            for tao in range(K):
                if maskX[nu,tao] == 0:
                    S[:,nu,tao] = 0
                else:
                    S[:,nu,tao] = np.random.multinomial(x[nu,tao], p[:,nu,tao], size=1)

        sigmaT = np.transpose(np.sum(maskX*S, axis=2))
        sigmaV = np.sum(maskX*S, axis=1)

        # Sample Templates
        a_t = A_t + sigmaT
        b_t = 1 / ( np.divide(A_t, B_t) + np.dot(maskX, np.transpose(Vprev)) )

        T = np.random.gamma(a_t,b_t)

        # Sample Excitations
        a_v = A_v + sigmaV
        b_v = 1 / ( np.divide(A_v, B_v) + np.dot(np.transpose(Tprev),maskX) )

        V = np.random.gamma(a_v,b_v)

        if likelihood != None:
            likelihood.append(calculateLikelihood(x, np.dot(T, V), test, dataExistance, W, K))

    if likelihood == None:
        return T, V, a_t, b_t, a_v, b_v
    else:
        return T, V, a_t, b_t, a_v, b_v, likelihood
```

## En iyi ayrıştırmanın hesaplanması

```python
t0 = time.time()

T = orgT.copy()
V = orgV.copy()
maskX = dataExistance.copy()
MAXITER = 10000

T, V, a_t, b_t, a_v, b_v = gibbsSampler(x, T, V, maskX, MAXITER)
bestCandidates = sampleCandidates(a_t, b_t, a_v, b_v, sampleSize=1000)
bestCandidatesMean = np.mean(bestCandidates, axis=0)
bestCandidateDistributionsFull = transformCandidatesToDistributions(bestCandidates, KLsampleSize, test&False, dataExistance|True)
bestCandidateDistributionsPartial = transformCandidatesToDistributions(bestCandidates, KLsampleSize, test, dataExistance)

```

### İlk-ısınma (Burn-in) periyodu

1000 adımlık bir ilk-ısınma devresi uyguladık.

```python
t0 = time.time()
T = orgT.copy()
V = orgV.copy()
maskX = mask.copy()

T, V, _, _, _, _ = gibbsSampler(x, T, V, maskX, MAXITER = 1000)

modT = T.copy()
modV = V.copy()
print(time.time()-t0)
```

    1.1921601295471191

### Gibbs örnekleyicisi ile eğitim (daha çok veri açma)

```python
for cellOpenStrategy in range(5):
    t0 = time.time()

    likelihood = []
    openedCells = [0]

    diffErrorPartial = []
    diffErrorFull = []
    diffKLPartial = []
    diffKLFull = []
    varEst = []

    T = modT.copy()
    V = modV.copy()

    maskX = mask.copy()

    cellOpenCount = 10
    extraIter = 20
    EPOCH = int((dataExistance.sum()-(dataExistance^test).sum()-mask.sum())//cellOpenCount + extraIter)
    MAXITER = 20

    for nn in range(EPOCH):
        # Apply gibbs sampler
        if nn >= (dataExistance.sum()-(dataExistance^test).sum()-mask.sum())//cellOpenCount:
            MAXITER = 50
        T, V, a_t, b_t, a_v, b_v, likelihood = gibbsSampler(x, T, V, maskX, MAXITER, likelihood, test, dataExistance)

        # Take Mean estimate and calculate diff from X
        xCandidates = sampleCandidates(a_t, b_t, a_v, b_v, sampleSize=100)

        ve, de = calculateVarianceAndError(bestCandidatesMean, test, dataExistance, xCandidates)
        varEst.append(ve)
        diffErrorPartial.append(de)

        _, de = calculateVarianceAndError(bestCandidatesMean, test&False, dataExistance|True, xCandidates)
        diffErrorFull.append(de)

        de = calculateKLdivergence(xCandidates, bestCandidateDistributionsPartial, KLsampleSize, test, dataExistance)
        diffKLPartial.append(de)

        de = calculateKLdivergence(xCandidates, bestCandidateDistributionsFull, KLsampleSize, test&False, dataExistance|True)
        diffKLFull.append(de)

        # Apply Cell Opening Strategy
        for co in range(cellOpenCount):
            if cellOpenStrategy == 0:
                row, col = openRandom(W, K, maskX, test)
            elif cellOpenStrategy == 1:
                if nn % 2 == 0:
                    row, col = openMaxColMask(W, K, maskX, test)
                else:
                    row, col = openMaxRowMask(W, K, maskX, test)
            elif cellOpenStrategy == 2:
                row, col = openMinVar(W, K, maskX, test, xCandidates)
            elif cellOpenStrategy == 3:
                row, col = openMaxVar(W, K, maskX, test, xCandidates)
            elif cellOpenStrategy == 4:
                row, col = openRowVarColMask(W, K, maskX, test, np.dot(T,V))
            else:
                row, col = (-1, -1)

            # Remove mask from (row, col)
            if not (row == -1 and col == -1):
                maskX[row][col] = 1
            openedCells.append((row,col))

    allStrategyLabels.append(cellOpenStrategy)
    allStrategyColors.append(cellOpenStrategy)

    allLikelihood.append(likelihood)
    allOpenedCells.append(openedCells)

    allEstimationVariance.append(varEst)

    allDiffRMSEPartial.append(diffErrorPartial)
    allDiffRMSEFull.append(diffErrorFull)
    allDiffKLPartial.append(diffKLPartial)
    allDiffKLFull.append(diffKLFull)

    rmse = np.zeros(len(diffErrorPartial))
    for i in range(len(diffErrorPartial)):
        # RMSE
        de2 = diffErrorPartial[i]*diffErrorPartial[i]
        rmse[i] = np.sqrt(np.nanmean(np.where(de2!=0,de2,np.nan)))
    allRMSEPartial.append(rmse)

    rmse = np.zeros(len(diffErrorFull))
    for i in range(len(diffErrorFull)):
        # RMSE
        de2 = diffErrorFull[i]*diffErrorFull[i]
        rmse[i] = np.sqrt(np.nanmean(np.where(de2!=0,de2,np.nan)))
    allRMSEFull.append(rmse)

    kl = np.zeros(len(diffKLPartial))
    for i in range(len(diffKLPartial)):
        kl[i] = np.mean(diffKLPartial[i])
    allKLPartial.append(kl)

    kl = np.zeros(len(diffKLFull))
    for i in range(len(diffKLFull)):
        kl[i] = np.mean(diffKLFull[i])
    allKLFull.append(kl)

    print(strategyLabels[cellOpenStrategy] + " %0.3fs. de tamamlandı." % (time.time() - t0))

allErrorDiffs.append(allDiffRMSEPartial)
allErrorDiffs.append(allDiffRMSEFull)
allErrorDiffs.append(allDiffKLPartial)
allErrorDiffs.append(allDiffKLFull)

allError.append(allRMSEPartial)
allError.append(allRMSEFull)
allError.append(allKLPartial)
allError.append(allKLFull)
```

    Rastgele compeleted in 25.083s.
    Satır-Sütun compeleted in 25.324s.
    Min Varyans compeleted in 25.153s.
    Maks Varyans compeleted in 25.120s.

    C:\Users\Burki\Anaconda3\lib\site-packages\ipykernel\__main__.py:3: RuntimeWarning: Degrees of freedom <= 0 for slice.
      app.launch_new_instance()

    Satır Varyans compeleted in 24.291s.

## Etkileşimli Hata ve Varyans Isı haritası

Satır-sütun stratejisi ve KL ıraksayı hata metriği yoğun matris üzerinde kullanılmış bir deney süreci gösterilmektedir. Çıktıda hata ve varyans ısı haritasını görmekteyiz. Bu grafikte, kırmızı ile hata, mavi ile maskelenmiş verinin varyansı ve beyaz ile ise gözlemlenmiş veri gösterilmektedir. Kırmızı ve mavinin tonları hata ve varyansın şiddetini yansıtmaktadır. Yinelemeler süresince veriler gözlemlendikçe maskelenmiş (mavilikler) verinin kaybolduğunu (beyaza dönmesini), hatanın (kırmızılıkların) ise azaldığını grafikten görmekteyiz. Sağdaki grafikte KL ıraksayı kullanılarak elde edilen hata grafiğini ve ona oturtulan polinomu görmekteyiz.

```python
cmap = matplotlib.colors.LinearSegmentedColormap.from_list('my_colormap',
                                           ['blue','white', 'red'],
                                           256)

chosenStrategy = 1
chosenErrorMetric = 2

rmseHM = allError[chosenErrorMetric][chosenStrategy]
matrixHM = allErrorDiffs[chosenErrorMetric][chosenStrategy]
openedCellsHM = allOpenedCells[chosenStrategy]
varEstHM = allEstimationVariance[chosenStrategy]

xAxis = list(range(len(rmseHM)))
xp = np.linspace(0, EPOCH-1, 1000)
polDeg = 10
p30 = np.poly1d(np.polyfit(xAxis, rmseHM, polDeg))

vmax = rmseHM.max()

#matrixHM = diffErrorHM.copy()
for i in range(EPOCH-extraIter):
    #if openedCellsHM[i] == (-1,-1):
    #    break
    for oc in openedCellsHM[1+(i*cellOpenCount):]:
        if oc[0] == -1:
            break
        matrixHM[i][oc[0],oc[1]] = -varEstHM[i][oc[0],oc[1]]*vmax

def pltErrorVarianceHM(rc):
    fig = plt.figure(figsize=(8,8))
    plt.subplot(1, 2, 1)
    if rc == 0:
        plt.title("İlk Durum")
    elif rc <= (EPOCH-extraIter):
        plt.title("Yineleme Sayısı: " + str(rc))
    else:
        plt.title("Yineleme Sayısı: " + str(rc))

    img = plt.imshow(matrixHM[rc],interpolation='nearest',
                    cmap = cmap,
                    origin='lower',
                    vmax = vmax,
                    vmin = -vmax)
    plt.colorbar(img,cmap=cmap)

    plt.subplot(1, 2, 2)
    plt.plot( range(rc+1), rmseHM[:rc+1])

    try:
        xpRC = list(xp).index(xp[xp>=(rc+1)][0])
    except:
        xpRC = list(xp).index(xp[xp>=(rc)][0])
    plt.plot( xp[:xpRC], p30(xp)[:xpRC], "-", color='r', linewidth=2)

    plt.xlim(0,EPOCH)
    #plt.ylim(0,rmseHM.max()+1)
    font = {'size'   : 15}
    #plt.title("KL : " + str(p30(xp)[xpRC-1]))
    plt.xlabel("Yineleme Sayısı", **font)
    plt.ylabel(errorMetricLabels[chosenErrorMetric], **font)
    plt.tight_layout()
    plt.show()

interact(pltErrorVarianceHM, rc = (0, EPOCH-1, 1))
```

## Yoğun ve seyrek veri üzerinde, tanımlanmış beş strateji için 10’lu açarak kısmi KL ıraksayı değişim grafiği

Stratejilerin performanslarını, eşik değerine ne kadar hızlı ulaştıklarını karşılaştırarak ölçtük. Eşik değerine ulaşma metriği olarak *eğri altında kalan alanı* kullandık. En iyi strateji, eğri altında kalan alanı en az olandır.

Hata fonksiyonlarının asıl davranışını gözlemleyebilmek için fonksiyonlara polinom oturttuk. Bu polinomların salınımdan daha az etkilenmesi için ise bütün veriler gözlemlendikten
sonra 1000 yinelemeli Gibbs örnekleyicisi çalıştırdık.

```python
xAxis = list(range(len(allKLPartial[0])))
xp = np.linspace(0, EPOCH-1, 1000)
polDeg = 15

chosenErrorMetric = 2

errorFunction = allError[chosenErrorMetric].copy()
thr = 0
for rmse in errorFunction:
    thr += np.mean(rmse[-15:-5])
thr /= len(errorFunction)

fig = plt.figure(figsize=(10,10))

plt.plot(range(EPOCH - extraIter+15), (EPOCH - extraIter+15)*[thr], "--", color='c', label="Eşik: "+str(thr)[:5], linewidth=2)

aucTrapz = []
aucSimps = []

xpRC = list(xp).index(xp[xp>=(EPOCH - extraIter+10)][0])

for i, rmse in enumerate(errorFunction):
    p30 = np.poly1d(np.polyfit(xAxis, rmse, polDeg))
    aucTrapz.append(np.trapz(p30(xp)[:xpRC]-thr, x=xp[:xpRC]))
    aucSimps.append(np.trapz(p30(xp)[:xpRC], x=xp[:xpRC]))
    print(np.trapz(p30(xp)[:xpRC]-thr, x=xp[:xpRC]))
    zz = i
    if i == 1:
        zz = 10
    plt.plot( xp[:xpRC], p30(xp)[:xpRC], "-", label=strategyLabels[allStrategyLabels[i]], color=strategyColors[allStrategyColors[i]], linewidth=2, zorder=zz)

plt.xlim(0,)
#plt.ylim(0,)

font = {'size'   : 18}
plt.xlabel("Yineleme Sayısı (" + str(cellOpenCount) + "'lu açma)", **font)
plt.ylabel(errorMetricLabels[chosenErrorMetric], **font)

plt.legend()
plt.show()

```

## Vargılar

Bu çalışmamızda negatif olmayan matris ayrışımı için eşlenik Gamma önselleri ile hiyerarşik bir model inceledik ve çıkarımlar için Gibbs örnekleyicisi kullandık. Buradan yola çıkarak aktif eleman seçimi [7] problemine çözüm önerdik. Tanımladığımız beş stratejiyi KL ıraksayı ve KOKH metrikleri üzerinden karşılaştırdık. Yaptığımız deneyler ile satır-sütun stratejisinin etkili bir aktif ögrenme tekniği olduğunu gösterdik. Satır-sütun stratejisinin başarısının gözlemlenmemiş veriyi dengeli bir biçimde açıyor olmasına bağlıyoruz. Ayrıca bu stratejinin verinin içeriğinden bağımsız olarak tanımlanmış olması bu stratejiyi diğer alanlara da uygulanabilir kılıyor.

## Kaynaklar

[1] D. D. Lee and H. S. Seung, "Learning the parts of objects
with nonnegative matrix factorization.", Nature, 401:788–791, 1999.

[2]  A. T. Cemgil, "Bayesian inference in non-negative matrix
factorisation models.", Technical Report CUED/FINFENG/TR.609, University of Cambridge, July 2008. Submitted for publication to Computational Intelligence and Neuroscience

[3]  D. D. Lee and H. S. Seung, "Algorithms for non-negative matrix factorization.", Advances in neural information processing systems. 2001.

[4] J. S. Liu, "Monte Carlo Strategies in Scientific Computing", Springer, New York, NY, USA, 2004.

[5] W. R. Gilks, S. Richardson, and D. J. Spiegelhalter, Eds., "Markov Chain Monte Carlo in Practice", CRC Press, London, UK, 1996.

[6] Harper, F. Maxwell, and Joseph A. Konstan, "The movielens datasets: History and context.", ACM Transactions on Interactive Intelligent Systems (TiiS) 5.4 (2016): 19.

[7] Silva, Jorge, and Lawrence Carin. "Active learning for online bayesian matrix factorization.", Proceedings of the 18th ACM SIGKDD international conference on Knowledge discovery and data mining. ACM, 2012.


### This notebook computes the joint likelihood in 5 dimensions for the parameters of interest (see below). See inference_demo for more details about basic usage

```python
from lenslikelihood.measurements import flux_measurements, flux_measurement_uncertainties, all_lens_names, all_param_ranges

# Note that the syntax for the uncertainties is
# {'lens_name': (1-sigma-uncertainty, reference_index, uncertainty_in_ratio)}
# where reference_index is the reference image with which to compute the flux ratio, and uncertainty_in_ratio specifies
# whether the measurement uncertainty refers to the flux or the flux ratio

for name in all_lens_names:
    print(name)
    print('fluxes/flux ratios measured: ', flux_measurements[name])
    print('uncertainties: ', flux_measurement_uncertainties[name])
    print('\n')
```

    WGDJ0405
    fluxes/flux ratios measured:  [0.8  0.52 1.   0.94]
    uncertainties:  ([0.04, 0.061538461538461535, 0.024, 0.03418803418803419], 0, False)

    HE0435
    fluxes/flux ratios measured:  [0.96  0.976 1.    0.65 ]
    uncertainties:  ([0.05, 0.049, 0.048, 0.056], 0, False)

    WGD2038
    fluxes/flux ratios measured:  [0.86 1.   0.79 0.4 ]
    uncertainties:  ([0.01, 0.01724137931034483, 0.021739130434782608, 0.021739130434782608], 0, False)

    B1422
    fluxes/flux ratios measured:  [0.88  1.    0.474 0.025]
    uncertainties:  ([0.011363636363636364, 0.01, 0.012765957446808512, None], 0, False)

    WFI2033
    fluxes/flux ratios measured:  [1.   0.65 0.5  0.53]
    uncertainties:  ([0.03, 0.046875, 0.04, 0.03773584905660377], 0, False)

    PSJ1606
    fluxes/flux ratios measured:  [1.   1.   0.59 0.79]
    uncertainties:  ([0.03, 0.03, 0.03333333333333333, 0.02564102564102564], 0, False)

    WFI2026
    fluxes/flux ratios measured:  [1.   0.75 0.31 0.28]
    uncertainties:  ([0.02, 0.02666666666666667, 0.06451612903225806, 0.03571428571428571], 0, False)

    RXJ0911
    fluxes/flux ratios measured:  [0.56 1.   0.53 0.24]
    uncertainties:  ([0.07142857142857142, 0.05, 0.07547169811320754, 0.16666666666666669], 0, False)

    RXJ1131
    fluxes/flux ratios measured:  [1.   0.61 0.73 0.12]
    uncertainties:  ([[0.012345679012345678, 0.024691358024691357], [0.10084033613445378, 0.025210084033613446], None], 1, True)

    MG0414
    fluxes/flux ratios measured:  [1.   0.83 0.36 0.16]
    uncertainties:  ([0.06024096385542169, 0.11111111111111112, 0.11764705882352941], 0, True)

    PG1115
    fluxes/flux ratios measured:  [1.   0.93 0.16 0.21]
    uncertainties:  ([0.06451612903225806, 0.43750000000000006, 0.1904761904761905], 0, True)

### Models implemented for the halo mass function and concentration-mass relation

The full set of hyper-parameters we're interested in constraining are defined by the parameterizations of the halo mass function and concentration-mass relation. They are $\Sigma_{\rm{sub}}$, $\delta_{\rm{LOS}}$, $\Delta \alpha$, $q$, $c_8$, and $\beta$. The first four define to the subhalo and field halo mass functions, and the last two define the concentration-mass relation.

The field halo mass function is parameterized as
\begin{equation}
\frac{dN_{\rm{LOS}}}{dm dV} = \delta_{\rm{LOS}} \left(1+\xi_{\rm{2halo}}\right) \left(\frac{m}{10^8}\right)^{\Delta \alpha} \ \frac{dN_{\rm{ShethTormen}}}{dm dV}
\end{equation}
where $\delta_{\rm{LOS}}$ scales the overall normalization, and $\Delta \alpha$ parameterizes deviations from the logarithmic slope predicted by CDM around $10^8 M_{\odot}$.

The subhalo mass function is parameterized as
\begin{equation}
\frac{dN_{\rm{sub}}}{dm dA} \sim \Sigma_{\rm{sub}} \ \left(\frac{m}{10^8}\right)^{\alpha + q \Delta \alpha}
\end{equation}
where $\Sigma_{\rm{sub}}$ is the normalization, $\alpha$ is the logarithmic slope predicted by CDM, $\Delta \alpha$ parameterizes deviations from the value predicted by CDM, and $q$ controls the coupling between the line of sight halo mass function slope and the subhalo mass function slope. When $q=1$ the slopes change in the same way, and when $q=0$ the slopes of the subhalo and field halo mass functions are completely decoupled.

The concentration-mass relation is parameterized as

\begin{equation}
c\left(M, z\right) = c_8 \left(1+z\right)^{\zeta} \left(\frac{\nu\left(M, z\right)}{\nu\left(10^8, z\right)}\right)^{-\beta}
\end{equation}
i.e. it is a power-law in the peak height $\nu$ with normalization $c_8$ at $10^8$ and a logarithmic slope $\beta$. The parameter $\zeta$ modifies the redshift evolution and is marginalized over in the sampling.

The parameter names used in the python code have the following correspondence:

1) sigma_sub = $\Sigma_{\rm{sub}}$

2) delta_power_law_index = $\Delta \alpha$

3) c0 = $c_8$

4) beta = $\beta$

5) delta_power_law_index_coupling = $q$

6) LOS_normalization = $\delta_{\rm{LOS}}$

### Example inference on three parameters with a subset of lenses

First load the model samples, define what parameters we want to look at

```python
import pickle
import matplotlib.pyplot as plt
# specify the parameter names
param_names = ['LOS_normalization', 'beta', 'c0', 'delta_power_law_index', 'sigma_sub']
param_ranges = [all_param_ranges[name] for name in param_names]
print(param_ranges)
# specify the lenses to use
lenses = all_lens_names

raw_samples_dict = {}
# load the forward model samples
for lens in lenses:
    print(lens)
    f = open('./../raw_samples/'+lens, 'rb')
    raw_samples = pickle.load(f)
    f.close()
    raw_samples_dict[lens] = raw_samples

print(raw_samples)
```

    [[0.0, 2.0], [0.0, 6.0], [1, 200], [-0.6, 0.9], [0, 0.1]]
    WGDJ0405
    HE0435
    WGD2038
    B1422
    WFI2033
    PSJ1606
    WFI2026
    RXJ0911
    RXJ1131
    MG0414
    PG1115
    <lenslikelihood.extract_raw_samples.RawLensSamples object at 0x7ff7fb8a2978>

The next cell computes the joint likelihood without any importance sampling weights.

```python
# compute the summary statistics, retaining the 1500 sets of model parameters with lowest summary statistic
n_samples_keep = 1000
samples_dict = {}
fig = plt.figure(1)
fig.set_size_inches(8,8)
for lens in lenses:
    print('working on lens '+str(lens) + '... ')
    importance_sampling_weights = None
    measured_fluxes = flux_measurements[lens]
    measurement_uncertainties = flux_measurement_uncertainties[lens][0]
    reference_index = flux_measurement_uncertainties[lens][1]
    uncertaintiy_in_flux_ratios = flux_measurement_uncertainties[lens][2]

    samples, full_samples, statistic = raw_samples_dict[lens].sample_with_abc(measured_fluxes,
                                                                   param_names,
                                                                   measurement_uncertainties,
                                                                   reference_index,
                                                                   n_samples_keep,
                                                                   uncertaintiy_in_ratios=uncertaintiy_in_flux_ratios,
                                                                   importance_sampling_weights=importance_sampling_weights)
    plt.hist(statistic, alpha=0.5, label=lens)
    samples_dict[lens] = samples

# the histogram shows the distribution of the retained summary statistics
plt.legend(fontsize=12)
plt.xlim(0., 0.15)
plt.xlabel('summary statistic', fontsize=14)
plt.show()
```

The next cell computes the joint likelihood with importance sampling weights, selecting only samples with delta_power_law_index_coupling (or 'q') close to 1. This makes the subhalo mass function track the slope of the field halo mass function, hence the name "coupled"

```python
# compute the summary statistics, retaining the 1500 sets of model parameters with lowest summary statistic
samples_dict_coupled = {}
fig = plt.figure(1)
fig.set_size_inches(8,8)
for lens in lenses:
    print('working on lens '+str(lens) + '... ')
    importance_sampling_weights = {'delta_power_law_index_coupling': [1., 0.25]}
    measured_fluxes = flux_measurements[lens]
    measurement_uncertainties = flux_measurement_uncertainties[lens][0]
    reference_index = flux_measurement_uncertainties[lens][1]
    uncertaintiy_in_flux_ratios = flux_measurement_uncertainties[lens][2]
    samples, full_samples, statistic = raw_samples_dict[lens].sample_with_abc(flux_measurements[lens],
                                                                   param_names,
                                                                   measurement_uncertainties,
                                                                   reference_index,
                                                                   n_samples_keep,
                                                                   uncertaintiy_in_ratios=uncertaintiy_in_flux_ratios,
                                                                   importance_sampling_weights=importance_sampling_weights)
    plt.hist(statistic, alpha=0.5, label=lens)
    samples_dict_coupled[lens] = samples

# the histogram shows the distribution of the retained summary statistics
plt.legend(fontsize=12)
plt.xlim(0., 0.2)
plt.xlabel('summary statistic', fontsize=14)
plt.show()
```

Now we compute the likelihood using the package trikde https://github.com/dangilman/trikde

```python
from trikde.pdfs import DensitySamples, IndepdendentLikelihoods
import os

nbins = 20
likelihoods, likelihoods_coupled = [], []

load_from_pickle = False # if True, will look for a pre-computed DensitySamples class
save_to_pickle = True # if True, will pickle each class for accelerated later use;
# save_to_pickle=True will do nothing if load_from_pickle=True

filename_extension = '_joint'
filename_extension_coupled = '_joint_coupled'
base_path = './../lenslikelihood/precomputed_likelihoods/'

for lens in lenses:

    fname = base_path + lens + filename_extension
    if load_from_pickle and os.path.exists(fname):
        print('loading joint likelihoods for lens '+lens+' ...')
        f = open(fname, 'rb')
        single_lens_likelihood = pickle.load(f)
        f.close()
    else:
        print('computing joint likelihoods for lens '+lens+' ...')
        lens_samples = samples_dict[lens]
        weights = None
        single_lens_likelihood = DensitySamples(lens_samples, param_names, weights,
                                 param_ranges, nbins=nbins, use_kde=True, bandwidth_scale=1.)
        if save_to_pickle:
            f = open(fname, 'wb')
            pickle.dump(single_lens_likelihood, f)
            f.close()
    likelihoods.append(single_lens_likelihood)

    fname = base_path + lens + filename_extension_coupled
    if load_from_pickle and os.path.exists(fname):
        f = open(fname, 'rb')
        single_lens_likelihood = pickle.load(f)
        f.close()
    else:
        lens_samples = samples_dict_coupled[lens]
        weights = None
        single_lens_likelihood = DensitySamples(lens_samples, param_names, weights,
                                 param_ranges, nbins=nbins, use_kde=True, bandwidth_scale=1.)
        if save_to_pickle:
            f = open(fname, 'wb')
            pickle.dump(single_lens_likelihood, f)
            f.close()
    likelihoods_coupled.append(single_lens_likelihood)

likelihood = IndepdendentLikelihoods(likelihoods)
likelihood_coupled = IndepdendentLikelihoods(likelihoods_coupled)
```

    computing joint likelihoods for lens WGDJ0405 ...
    computing joint likelihoods for lens HE0435 ...
    computing joint likelihoods for lens WGD2038 ...
    computing joint likelihoods for lens B1422 ...
    computing joint likelihoods for lens WFI2033 ...
    computing joint likelihoods for lens PSJ1606 ...

### The joint likelihood/posterior with no modeling assumptions (indepedent, uniform priors on all model parameters).

This likelihood includes a marginalization over a term that couples the subhalo mass function slope to the field halo mass function slope

```python
from trikde.triangleplot import TrianglePlot

triangle_plot = TrianglePlot([likelihood])
triangle_plot.set_cmap('magma')
axes = triangle_plot.make_triplot(filled_contours=False, show_intervals=False, show_contours=False
                                 )
# can change axis labels
```

### The joint likelihood/posterior assuming the slope of the subhalo mass function tracks the slope of the field halo mass function.

This likelihood has a Gaussian prior on q (see equation for subhalo mass function at the top of this notebook) with a mean of 1 and a variance of 0.25.

```python
from trikde.triangleplot import TrianglePlot

triangle_plot = TrianglePlot([likelihood_coupled])
triangle_plot.cmap = 'magma'
axes = triangle_plot.make_triplot(filled_contours=False, show_intervals=False, show_contours=False)
# can change axis labels
```

```python

```


```python
%%capture
## compile PyRoss for this notebook
import os
owd = os.getcwd()
os.chdir('../../')
%run setup.py install
os.chdir(owd)
%matplotlib inline
```

```python
import numpy as np
import matplotlib.pyplot as plt
import pyross
```

In this notebook we consider a control protocol consisting of an initial lockdown, which is then partly released. For our numerical study we generate synthetic data using the stochastic SIIR model.

While we use the Denmark age structure and contact matrix, we emphasise that **the model parameters considered in this notebook have NOT been obtained from real data, but rather are chosen ad-hoc**.

**Outline of this notebook:**

1. We briefly summarize the SEAI5R model.
2. We load the age structure and contact matrix for Denmark. The contact matrix is generally given as
\begin{equation}
    C = C_{H} + C_{W} + C_{S} + C_{O},
\end{equation}
where the four terms denote the number of contacts at home, work, school, and all other remaining contacts.
3. We define the other model parameters of the SEAI5R model **(again, these are not fitted to any real data, but rather chosen ad-hoc)**.
4. We define a "lockdown-protocol":
    1. After a fixed time, a lockdown is imposed. The contact matrix is reduced to
    \begin{equation}
        C = C_{H} + 0.1\cdot C_W + 0.4 \cdot C_O,
        \end{equation}
        i.e. the "home" contacts $C_H$, and 10% of the "work" as well as 40% of the "other" contacts are reainted. The latter two model that even in lockdown, people go to work, and that initially people might be lenient in following the lockdown advice.
    2. 5 Days after the initial lockdown, the 40% "social interaction" part of the contact matrix is removed, and the contact matrix becomes
        \begin{equation}
        C = C_{H} + 0.1\cdot C_W.
        \end{equation}
        This models that people take the lockdown seriously and seize to interact socially.
    3. 50 Days after the initial lockdown, 50% of the "school" contacts are added to the contact matrix,
        \begin{equation}
        C = C_{H} + 0.1\cdot C_W + 0.5 \cdot C_S.
        \end{equation}
        This models an opening of schools, with a social distancing protocol in place such that the number of contacts at school is halved.
5. We run 50 stochastic simulations of this lockdown protocol, and find that in Stage 2 (only home and 10% of work contacts), the total number of known active infectives (the sum of the stages symptomatic infective, hospitalised, and in ICU) shows a maximum and then decreases. After schools are opened, the total number of known active infectives shows a minimum and starts to decrease again. We plot the distribution of waiting time between change in the contact matrix and the extrema in the stochastic time series.
6. Finally, we consider the protocol from point 4 again, but this time with a prefactor 0.2 in front of $C_S$, signifying a more strict social distancing protocol. Running 50 stochastic simulations with this protocol, we find that this time the total number of active infectives keeps decreases even after schools have been opened.

## 1. Brief summary of the model

The SEAI5R model contains the following stages:

If a susceptible individual is infected, it enters the "exposed" class, where it does not show symptoms and is not yet infective. In the subsequent class, called "activated", the individual still does not show symptoms, but is infective. After the "activated" class, an individual is either a symptomatic or an asymptomatic infective. Those who are asymptomatic recover, those who show symptoms either recover, or need to be hospitalised. The condition of a hospitalised individual might improve, so that the individual recovers, or worsens, at which point the individual enters the "ICU" class. From this class the individual can again recover, or enter a final "mortality" class.

## 2. Load Denmark age structure and contact matrix

```python
my_data = np.genfromtxt('../data/age_structures/Denmark-2019.csv', delimiter=',', skip_header=1)
aM, aF = my_data[:, 1], my_data[:, 2]

Ni0=aM+aF;

M=16  ## number of age classes

Ni = Ni0[:M]
N=np.sum(Ni)

print("Age groups are in brackets of 5 (i.e. 0-4, 5-9, 10-14, .. , 75-79).")
print("Number of individuals in each bracket:")
print(Ni.astype('int'))
print("Total number of individuals: {0}".format(np.sum(Ni.astype('int'))))
```

    Age groups are in brackets of 5 (i.e. 0-4, 5-9, 10-14, .. , 75-79).
    Number of individuals in each bracket:
    [302353 305513 338779 341219 379522 395469 342443 320132 366147 385944
     422585 381360 338039 319145 346572 220374]
    Total number of individuals: 5505596

```python
# Get individual contact matrices
CH, CW, CS, CO = pyross.contactMatrix.Denmark()

# By default, home, work, school, and others contribute to the contact matrix
C = CH + CW + CS + CO

# Illustrate the individual contact matrices:
fig,aCF =  plt.subplots(2,2);
aCF[0][0].pcolor(CH, cmap=plt.cm.get_cmap('GnBu', 10));
aCF[0][1].pcolor(CW, cmap=plt.cm.get_cmap('GnBu', 10));
aCF[1][0].pcolor(CS, cmap=plt.cm.get_cmap('GnBu', 10));
aCF[1][1].pcolor(CO, cmap=plt.cm.get_cmap('GnBu', 10));
```

The above contact matrices illustrate the interactions at home (upper left), work (upper right), school (lower left), and the other remaining contacts (lower right). x- and y- axes denote the age groups, a darker color indicates more interaction.

## 3. Define model parameters

**Note: These have not been fitted to real data.**

```python
beta  = 0.036692            # infection rate

gE    = 1/5.
gA    = 1/4.
gIa   = 1./3.                # recovery rate of asymptomatic infectives
gIs   = 1./7                # recovery rate of symptomatic infectives
alpha = 0.4                 # fraction of asymptomatic infectives
fsa   = 0.2                 # the self-isolation parameter
fh    = 0
gIh   = 1/7
gIc   = 1/14

sa    = 0*np.ones(M)      #  rate of additional/removal of population by birth etc
sa[0]     = 0            # birth
sa[3] = 0          # mortality

hh   = 0.1*np.ones(M)       # fraction which goes from Is to hospital
hh[:M] = 0.01
hh[M:2*M] = 0.025
cc   = 0.05*np.ones(M)      # fraction which goes from  hospital to ICU
mm   = 0.4*np.ones(M)       # mortality from IC

# initial conditions
Is_0 = np.zeros((M));

Ia_0 = 10*np.ones((M)) # start with 10 asymptomatic infectives
R_0  = np.zeros((M))
E_0  = np.zeros((M))
A_0  = np.zeros((M))
Ih_0  = np.zeros((M))
Ic_0  = np.zeros((M))
Im_0  = np.zeros((M))

S_0  = Ni - (E_0 + A_0 + Ia_0 + Is_0 + Ih_0 + Ic_0 +Im_0 + R_0)
```

## 4. Define events for protocol

```python
# Dummy event for initial (standard) contact matrix
events = [lambda t: 1]
contactMatrices = [C]

# After 20 days, start lockdown
lockdown_threshold_0 = 20
def event0(t,rp):
    return t - lockdown_threshold_0
events.append(event0)
contactMatrices.append( CH + 0.1*CW + 0.4*CS )

# After 25 days, decrease contacts even further
lockdown_threshold_1 = 25
def event1(t,rp):
    return t- lockdown_threshold_1
events.append(event1)
contactMatrices.append( CH + 0.1*CW)

# After 70 days, add 50% of school contacts to contact matrix
lockdown_threshold_2 = 70
def event2(t,rp):
    return t - lockdown_threshold_2
events.append(event2)
contactMatrices.append( CH + 0.1*CW + 0.5*CS ) # everybody in lockdown
```

## 5. Simulate protocol and analyse results

#### Initialise pyross.control, run and plot a single test simulation

```python
# duration of simulation
Tf=150;  Nf=Tf+1;

# intantiate model
parameters = {'alpha':alpha,'beta':beta, 'gIa':gIa,'gIs':gIs,
              'gIh':gIh,'gIc':gIc,  'gE':gE, 'gA':gA,
              'fsa':fsa, 'fh':fh,
              'sa':sa,
              'hh':hh, 'cc':cc, 'mm':mm}
model = pyross.control.SEAI5R(parameters, M, Ni.copy())

# run model once
method='gillespie'
data=model.simulate(S_0, E_0, A_0, Ia_0, Is_0, Ih_0, Ic_0, Im_0,
                    events,contactMatrices, Tf, Nf,
                                method=method)
```

```python
# Plot result

t = data['t'];

# get total population in classes
# - symptomatic infective,
# - hospitalised,
# - in ICU
y_plot = np.sum ( data['X'][:,4*M:7*M], axis = -1)

lw=2
fig,ax = plt.subplots(1,1,figsize=(8,5))
ax.plot(t,y_plot,lw=lw,)
ax.axvline(data['events_occured'][0][0],
           color='crimson',lw=lw,
          label='Beginning of lockdown',ls='--')
ax.axvline(data['events_occured'][-1][0],
 color='limegreen',lw=lw,
          label='Schools re-opened',ls='--')
ax.set_xlim(0,Tf)
fs=20
ax.legend(loc='best',fontsize=fs,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Number of known active cases',fontsize=fs)
plt.show()
plt.close()
```

#### Run 50 simulations with protocol, then

* plot trajectories,
* plot mean trajectory + standard deviation,
* plot distribution of delay between interventions (lockdown, re-opening of schools) until subsequent maximum/minimum.

```python
# Run 50 simulations with this protocol
N_simulations = 50

data_results = np.zeros([N_simulations,Nf,M*9],
                        dtype=int)
for i in range(N_simulations):
    print('Running simulation {0} of {1}'.format(i+1,N_simulations),end='\r')
    data=model.simulate(S_0, E_0, A_0, Ia_0, Is_0, Ih_0, Ic_0, Im_0,
                    events,contactMatrices, Tf, Nf,
                                method=method)
    data_results[i] = data['X']
```

    Running simulation 50 of 50

```python
# Plot result

t = data['t'];
fig,ax = plt.subplots(1,1,figsize=(8,5))
for i,e in enumerate(data_results):
    # get total population in classes
    # - symptomatic infective,
    # - hospitalised,
    # - in ICU
    y_plot = np.sum ( e[:,4*M:7*M], axis = -1)
    ax.plot(t,y_plot,lw=0.5,)
ax.axvline(data['events_occured'][0][0],
               color='crimson',lw=lw,
              label='Beginning of lockdown',ls='--')
ax.axvline(data['events_occured'][-1][0],
 color='limegreen',lw=lw,
          label='Schools re-opened',ls='--')
ax.set_xlim(0,Tf)
fs=20
ax.legend(loc='best',fontsize=fs,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Number of known active cases',fontsize=fs)
plt.show()
plt.close()
```

#### Mean and standard deviation

```python
# For all simulations, plot total population in classes
# - symptomatic infective,
# - hospitalised,
# - in ICU

# mean and standard deviation of trajectories at every time
y_mean = np.mean( ( np.sum(data_results[:,:,4*M:7*M], axis = -1) ), axis=0)
y_std = np.std ( np.sum( data_results[:,:,4*M:7*M], axis = -1), axis =0)

t = data['t'];
fig,ax = plt.subplots(1,1,figsize=(8,5))
ax.fill_between(t,y_mean - y_std,y_mean+y_std,
                label='Standard deviation',
           color='grey',alpha=0.4)
ax.plot(t,y_mean,
        label='Mean',
           color='black',alpha=1)
ax.axvline(data['events_occured'][0][0],
               color='crimson',lw=lw,
              label='Beginning of lockdown',ls='--')
ax.axvline(data['events_occured'][-1][0],
 color='limegreen',lw=lw,
          label='Schools re-opened',ls='--')
ax.set_xlim(0,Tf)
fs=20
ax.legend(loc='upper left',fontsize=17,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Number of known active cases',fontsize=fs)
plt.show()
plt.close()
```

#### Plot distributions for both

* time from beginning of lockdown to subsequent peak, and
* time from opening of schools to subsequent minimum.

```python
# calculate time from lockdown to local maximum for each trajectory
mask = (t > lockdown_threshold_0)*(t < lockdown_threshold_2)
t_mask = t[(mask)]
durations_from_lockdown_to_local_maximum = np.zeros(N_simulations,
                                                   dtype=float)
for i,e in enumerate(data_results):
    y_plot = np.sum ( e[:,4*M:7*M], axis = -1)
    index = np.argmax( y_plot[(mask)])
    durations_from_lockdown_to_local_maximum[i] = t_mask[index] - lockdown_threshold_0
print('Mean time from beginning of lockdown to subsequent peak = {0:3.1f} days'.format(np.mean(durations_from_lockdown_to_local_maximum)))

hist, bin_edges = np.histogram(durations_from_lockdown_to_local_maximum,10 ,density=True)
bin_centers = (bin_edges[1:] + bin_edges[:-1])/2.
bin_widths = bin_edges[1]-bin_edges[0]
fig, ax =plt.subplots(1,1,figsize=(10,6))
ax.axvline(np.mean(durations_from_lockdown_to_local_maximum),ls='--',color='black',
          label='Mean')
ax.bar(bin_centers,hist,bin_widths,color='dodgerblue',
       label='Distribution',alpha=0.7)
ax.set_xlabel('Time from beginning of lockdown until subsequent peak [days]',fontsize=fs)
ax.set_ylabel('Probability',fontsize=fs)
ax.legend(loc='best',fontsize=fs)
plt.show()
plt.close()

# calculate time from opening of schools to local minimum for each trajectory
# (recall lockdown_threshold_2 = time at which schools are opened)
mask = (t > lockdown_threshold_2)*(t < Tf)
t_mask = t[(mask)]
durations_from_school_opening_to_local_minimum = np.zeros(N_simulations,
                                                   dtype=float)
for i,e in enumerate(data_results):
    y_plot = np.sum ( e[:,4*M:7*M], axis = -1)
    index = np.argmin( y_plot[(mask)])
    durations_from_school_opening_to_local_minimum[i] = t_mask[index] - lockdown_threshold_2
print('Mean time from opening of schools to subsequent minimum = {0:3.1f} days'.format(np.mean(durations_from_school_opening_to_local_minimum)))

hist, bin_edges = np.histogram(durations_from_school_opening_to_local_minimum,10 ,density=True)
bin_centers = (bin_edges[1:] + bin_edges[:-1])/2.
bin_widths = bin_edges[1]-bin_edges[0]
fig, ax =plt.subplots(1,1,figsize=(10,6))
ax.axvline(np.mean(durations_from_school_opening_to_local_minimum),ls='--',color='black',
          label='Mean')
ax.bar(bin_centers,hist,bin_widths,color='dodgerblue',
       label='Distribution',alpha=0.7)
ax.set_xlabel('Time from school re-opening until subsequent minimum [days]',fontsize=fs)
ax.set_ylabel('Probability',fontsize=fs)
ax.legend(loc='best',fontsize=fs)
plt.show()
plt.close()
```

## 6. Redo simulations and analysis of point 5, but with more social distancing at school

```python
# we modify only the last contact matrix of the previous protocol:
contactMatrices[-1] =  CH + 0.1*CW + 0.2*CS
```

```python
# Run 50 simulations with this protocol
N_simulations = 50

data_results2 = np.zeros([N_simulations,Nf,M*9],
                        dtype=int)
for i in range(N_simulations):
    print('Running simulation {0} of {1}'.format(i+1,N_simulations),end='\r')
    data=model.simulate(S_0, E_0, A_0, Ia_0, Is_0, Ih_0, Ic_0, Im_0,
                    events,contactMatrices, Tf, Nf,
                                method=method)
    data_results2[i] = data['X']
```

    Running simulation 50 of 50

```python
# Plot results

print("Simulated trajectories:")
t = data['t'];
fig,ax = plt.subplots(1,1,figsize=(8,5))
for i,e in enumerate(data_results2):
    # get total population in classes
    # - symptomatic infective,
    # - hospitalised,
    # - in ICU
    y_plot = np.sum ( e[:,4*M:7*M], axis = -1)
    ax.plot(t,y_plot,lw=0.5,)
ax.axvline(data['events_occured'][0][0],
               color='crimson',lw=lw,
              label='Beginning of lockdown',ls='--')
ax.axvline(data['events_occured'][-1][0],
 color='limegreen',lw=lw,
          label='Schools re-opened',ls='--')
ax.set_xlim(0,Tf)
fs=20
ax.legend(loc='best',fontsize=15,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Number of known active cases',fontsize=fs)
plt.show()
plt.close()

print("Mean trajectory and standard deviation:")
# mean and standard deviation of trajectories at every time
y_mean = np.mean( ( np.sum(data_results2[:,:,4*M:7*M], axis = -1) ), axis=0)
y_std = np.std ( np.sum( data_results2[:,:,4*M:7*M], axis = -1), axis =0)
t = data['t'];
fig,ax = plt.subplots(1,1,figsize=(8,5))
ax.fill_between(t,y_mean - y_std,y_mean+y_std,
                label='Standard deviation',
           color='grey',alpha=0.4)
ax.plot(t,y_mean,
        label='Mean',
           color='black',alpha=1)
ax.axvline(data['events_occured'][0][0],
               color='crimson',lw=lw,
              label='Beginning of lockdown',ls='--')
ax.axvline(data['events_occured'][-1][0],
 color='limegreen',lw=lw,
          label='Schools re-opened',ls='--')
ax.set_xlim(0,Tf)
fs=20
ax.legend(loc='upper right',fontsize=15,framealpha=1)
ax.set_xlabel('time since first infection [days]',fontsize=fs)
ax.set_ylabel('Number of known active cases',fontsize=fs)
plt.show()
plt.close()

print("Time from lockdown to local maximum:")
# calculate time from lockdown to local maximum for each trajectory
mask = (t > lockdown_threshold_0)*(t < lockdown_threshold_2)
t_mask = t[(mask)]
durations_from_lockdown_to_local_maximum2 = np.zeros(N_simulations,
                                                   dtype=float)
for i,e in enumerate(data_results2):
    y_plot = np.sum ( e[:,4*M:7*M], axis = -1)
    index = np.argmax( y_plot[(mask)])
    durations_from_lockdown_to_local_maximum2[i] = t_mask[index] - lockdown_threshold_0
print('Mean time from beginning of lockdown to subsequent peak = {0:3.1f} days'.format(np.mean(durations_from_lockdown_to_local_maximum2)))

hist, bin_edges = np.histogram(durations_from_lockdown_to_local_maximum2,10 ,density=True)
bin_centers = (bin_edges[1:] + bin_edges[:-1])/2.
bin_widths = bin_edges[1]-bin_edges[0]
fig, ax =plt.subplots(1,1,figsize=(10,6))
ax.axvline(np.mean(durations_from_lockdown_to_local_maximum2),ls='--',color='black',
          label='Mean')
ax.bar(bin_centers,hist,bin_widths,color='dodgerblue',
       label='Distribution',alpha=0.7)
ax.set_xlabel('Time from beginning of lockdown until subsequent peak [days]',fontsize=fs)
ax.set_ylabel('Probability',fontsize=fs)
ax.legend(loc='best',fontsize=fs)
plt.show()
plt.close()
```

```python

```

```python

```

examples/control/ex05 - SEAI5R - Denmark.ipynb