# Exploring Data with Python
# Python으로 데이터 탐색

데이터 과학자의 중요한 역할은 데이터를 탐색, 분석 및 시각화하는 것입니다. 이를 위해 사용할 수 있는 툴과 프로그래밍 언어는 매우 다양하며, 가장 인기 있는 접근 방식은 주피터 노트북 같은 툴로 파이썬을 사용하는 것입니다.

Python은 웹 애플리케이션부터 장치 프로그래밍에 이르기까지 광범위한 시나리오에서 사용되는 유연한 프로그래밍 언어입니다. 데이터 분석 및 시각화를 지원하는 패키지가 많기 때문에 데이터사이언스 및 기계학습 커뮤니티에서 매우 인기가 있습니다.

이 노트북에서는 분석 패키지 중 일부를 살펴보고 데이터 분석하는 기본 기술을 적용하고자 합니다. 이것은 포괄적인 Python 프로그래밍 실습이 아니며, 데이터 분석에 대한 심층적인 내용도 아닙니다. 오히려 데이터 사이언티스트가 Python을 사용하여 데이터로 작업할 수 있는 몇 가지 일반적인 방식을 보여주는 과정으로 사용됩니다.

> **참고**: 전에 주피터 노트북 환경을 사용해 본 적이 없다면, 몇 가지 유의해야 할 사항이 있습니다. 노트북은 *셀*로 구성되어 있습니다. 일부 셀(예: *markdown*)은 텍스트를 포함하지만 다른 셀(예: 이 셀 아래의 셀)은 코드를 포함합니다.
> - 노트북은 Python *커널*에 연결되어 있습니다 (페이지 오른쪽 상단에서 노트북을 확인할 수 있습니다. Azure Machine Learning 컴퓨팅 인스턴스에서이 노트북을 실행하는 경우 **Python 3.6 - AzureML** 커널 사용). 커널을 중지하거나 서버와의 연결을 끊는 경우 (예 : 노트북을 닫았다가 다시 열거나 세션을 종료하고 다시 시작) 실행된 셀의 출력이 계속 표시됩니다. 그러나 노트북을 종료했다면 해당 셀에서 정의 했던 모든 변수 또는 함수는 없어집니다. 이에 종속된 후속 셀을 실행하기 전에 셀을 다시 실행해야합니다.

> - 주피터 노트북은 **► Run** 버튼을 사용하여 각 코드 셀을 실행할 수 있습니다. 오른쪽 상단의 커널 이름 옆에 있는 **◯** 기호는 **⚫** 셀이 실행되기 전에 잠시 **◯** 로 전환됩니다.
> - 각 코드 셀의 출력이 셀 바로 아래에 표시됩니다.
> - 코드 셀을 개별적으로 실행할 수 있지만 코드에 사용 된 일부 변수는 노트북에 전역적입니다. 즉, 모든 코드 셀을 **순서대로** 실행해야 합니다. 코드 셀간에 종속성이있을 수 있으므로 셀을 건너뛰면 후속 셀이 제대로 실행되지 않을 수 있습니다.

## NumPy로 데이터 배열(array) 탐색

간단한 데이터부터 살펴보겠습니다.

한 대학에서 데이터사이언스 수업을 위해 학생 성적 샘플을 가져온 것으로 가정해보겠습니다.

**► Run** 클릭하여 아래 셀에서 코드를 실행합니다. 실행 버튼을 눌러 데이터를 확인합니다.

```python
data = [50,50,47,97,49,3,53,42,26,74,82,62,37,15,70,27,36,35,48,52,63,64]
print(data)
```

    [50, 50, 47, 97, 49, 3, 53, 42, 26, 74, 82, 62, 37, 15, 70, 27, 36, 35, 48, 52, 63, 64]

데이터가 Python **list** 구조로 로드되었습니다. 이는 일반적인 데이터 조작에 적합한 데이터 유형이지만 수치 분석에는 최적화되지 않았습니다. 이를 위해 *Py*thon에서 *Num*bers로 작업하기위한 특정 데이터 유형 및 함수를 포함하는 **NumPy** 패키지를 사용할 것입니다.

아래 셀을 실행하여 데이터를 NumPy **array**에 로드합니다.

```python
import numpy as np

grades = np.array(data)
print(grades)
```

    [50 50 47 97 49  3 53 42 26 74 82 62 37 15 70 27 36 35 48 52 63 64]

**list**와 NumPy **array**의 차이점에 대해 궁금한 경우를 대비하여 이러한 데이터 타입에 2를 곱하는 표현식을 사용할 때 어떻게 작동하는지 비교해 보겠습니다.

```python
print (type(data),'x 2:', data * 2)
print('---')
print (type(grades),'x 2:', grades * 2)
```

    <class 'list'> x 2: [50, 50, 47, 97, 49, 3, 53, 42, 26, 74, 82, 62, 37, 15, 70, 27, 36, 35, 48, 52, 63, 64, 50, 50, 47, 97, 49, 3, 53, 42, 26, 74, 82, 62, 37, 15, 70, 27, 36, 35, 48, 52, 63, 64]
    ---
    <class 'numpy.ndarray'> x 2: [100 100  94 194  98   6 106  84  52 148 164 124  74  30 140  54  72  70
      96 104 126 128]

이 List에 2를 곱하면 list 원소에 원래 시퀀스가 반복되어 2배 길이의 새 list가 생성됩니다. 반면에 NumPy 배열에 2를 곱하면 배열이 *vector* 처럼 동작하여 원소 별 연산이 수행되므로 각 원소에 2를 곱한 동일한 크기의 배열이 됩니다.

여기서 중요한 점은 NumPy 배열이 숫자 데이터에 대한 수학적 연산을 지원하도록 특별히 설계되어 일반 list보다 데이터 분석에 더 유용하다는 것입니다.

위의 numpy 배열의 클래스 유형이 **numpy.ndarray** 인 것을 발견했을 수 있습니다. **nd**는 이것이 여러 *dimensions*으로 구성 될 수있는 구조임을 나타냅니다 (*n* 차원을 가질 수 있음). 우리의 특정 인스턴스에는 학생 성적의 단일 차원이 있습니다.

아래 셀을 실행하여 배열의 **shape**를 확인합니다.

```python
grades.shape
```

    (22,)

shape는 array에 22 개의 원소를 포함하는 하나의 차원만 있음을 확인합니다. (원래 list에는 22개의 grades가 있음). 0부터 시작하는 순서 위치로 array의 개별 원소에 액세스 할 수 있습니다. 첫 번째 원소(위치 0에 있는 원소)를 가져옵니다.

```python
grades[0]
```

    50

이제 NumPy array에 대해 알아보았으니 grades 데이터를 분석할 차례입니다.

array의 요소 전체에 집계를 적용할 수 있으므로 간단한 평균 성적(즉, *mean* grade 값)을 알아보겠습니다.

```python
grades.mean()
```

    49.18181818181818

따라서 평균 성적은 0 ~ 100 사이에서 중간 범위인 약 50으로 이 값에서 더 크거나 작습니다.

같은 학생들을 위한 두 번째 데이터 세트를 추가해봅시다. 이번에는 그들이 주당 공부에 집중한 시간을 기록한 데이터입니다.

```python
# Define an array of study hours
study_hours = [10.0,11.5,9.0,16.0,9.25,1.0,11.5,9.0,8.5,14.5,15.5,
               13.75,9.0,8.0,15.5,8.0,9.0,6.0,10.0,12.0,12.5,12.0]

# Create a 2D array (an array of arrays)
student_data = np.array([study_hours, grades])

# display the array
student_data
```

    array([[10.  , 11.5 ,  9.  , 16.  ,  9.25,  1.  , 11.5 ,  9.  ,  8.5 ,
            14.5 , 15.5 , 13.75,  9.  ,  8.  , 15.5 ,  8.  ,  9.  ,  6.  ,
            10.  , 12.  , 12.5 , 12.  ],
           [50.  , 50.  , 47.  , 97.  , 49.  ,  3.  , 53.  , 42.  , 26.  ,
            74.  , 82.  , 62.  , 37.  , 15.  , 70.  , 27.  , 36.  , 35.  ,
            48.  , 52.  , 63.  , 64.  ]])

이제 데이터는 2차원 array로 구성됩니다. 그 shape을 살펴보겠습니다.

```python
# Show shape of 2D array
student_data.shape
```

    (2, 22)

**student_data**는 2차원 두 개 배열에 각각 22개의 요소를 포함하고 있습니다.

이 구조를 탐색하려면 계층에서 각 원소의 위치를 지정해야 합니다. 따라서 첫 번째 배열(study hours 데이터 포함)에서 첫 번째 값을 찾으려면 다음 코드를 사용할 수 있습니다.

```python
# Show the first element of the first element
student_data[0][0]
```

    10.0

이제 학생의 학업 시간과 성적 정보를 모두 포함하는 다차원 배열이 생성되어 데이터를 비교하는 데 사용할 수 있습니다. 예를 들어, 평균 학업 시간과 평균 성적을 비교하면 어떻습니까?

```python
# Get the mean value of each sub-array
avg_study = student_data[0].mean()
avg_grade = student_data[1].mean()

print('Average study hours: {:.2f}\nAverage grade: {:.2f}'.format(avg_study, avg_grade))
```

    Average study hours: 10.52
    Average grade: 49.18

## Pandas를 이용한 표(tabular) 데이터 탐색

NumPy는 숫자, 특히 숫자 값의 어레이로 작업하는 데 필요한 많은 기능을 제공하지만, 2차원 데이터 테이블을 처리하기 시작하면 **Pandas** 패키지가 **DataFrame**과 함께 작업하기에 더 편리한 구조를 제공합니다.

다음 셀을 실행하여 Pandas 라이브러리를 가져오고 3개의 열이 있는 데이터 프레임을 생성합니다. 첫 번째 열은 학생 이름 리스트입니다. 두 번째 열과 세 번째 열은 학업 시간 및 성적 데이터를 포함하는 NumPy 배열입니다.

```python
import pandas as pd

df_students = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie',
                                     'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny',
                                     'Jakeem','Helena','Ismat','Anila','Skye','Daniel','Aisha'],
                            'StudyHours':student_data[0],
                            'Grade':student_data[1]})

df_students
```

      Name
      StudyHours
      Grade

      0
      Dan
      10.00
      50.0

      1
      Joann
      11.50
      50.0

      2
      Pedro
      9.00
      47.0

      3
      Rosie
      16.00
      97.0

      4
      Ethan
      9.25
      49.0

      5
      Vicky
      1.00
      3.0

      6
      Frederic
      11.50
      53.0

      7
      Jimmie
      9.00
      42.0

      8
      Rhonda
      8.50
      26.0

      9
      Giovanni
      14.50
      74.0

      10
      Francesca
      15.50
      82.0

      11
      Rajab
      13.75
      62.0

      12
      Naiyana
      9.00
      37.0

      13
      Kian
      8.00
      15.0

      14
      Jenny
      15.50
      70.0

      15
      Jakeem
      8.00
      27.0

      16
      Helena
      9.00
      36.0

      17
      Ismat
      6.00
      35.0

      18
      Anila
      10.00
      48.0

      19
      Skye
      12.00
      52.0

      20
      Daniel
      12.50
      63.0

      21
      Aisha
      12.00
      64.0

지정한 열 이외에 데이터 프레임에는 *인덱스*가 포함되어 각 행을 고유하게 식별합니다. 인덱스를 명시적으로 지정하고 적절한 값(예: 이메일 주소)을 할당 할 수 있었지만 인덱스를 지정하지 않았기 때문에 각 행에 대해 고유한 정수 값으로 인덱스가 생성되었습니다.

## 데이터 프레임에서 데이터 찾기 및 필터링 하기

DataFrame의 **loc** 메서드를 사용하여 다음과 같은 특정 인덱스 값에 대한 데이터를 검색할 수 있습니다.

```python
# Get the data for index value 5
df_students.loc[5]
```

    Name          Vicky
    StudyHours      1.0
    Grade           3.0
    Name: 5, dtype: object

다음과 같은 인덱스 값 범위에서 데이터를 가져올 수도 있습니다.

```python
# Get the rows with index values from 0 to 5
df_students.loc[0:5]
```

      Name
      StudyHours
      Grade

      0
      Dan
      10.00
      50.0

      1
      Joann
      11.50
      50.0

      2
      Pedro
      9.00
      47.0

      3
      Rosie
      16.00
      97.0

      4
      Ethan
      9.25
      49.0

      5
      Vicky
      1.00
      3.0

**loc** 메서드를 사용하여 인덱스를 기반으로 행을 찾을 수 있을 뿐 아니라 **iloc** 메서드를 사용하여 인덱스에 관계없이 데이터 프레임에서 해당 순서 위치에 따라 행을 찾을 수 있습니다.

```python
# Get data in the first five rows
df_students.iloc[0:5]
```

      Name
      StudyHours
      Grade

      0
      Dan
      10.00
      50.0

      1
      Joann
      11.50
      50.0

      2
      Pedro
      9.00
      47.0

      3
      Rosie
      16.00
      97.0

      4
      Ethan
      9.25
      49.0

`iloc[0:5]` 결과를 주의 깊게 살펴보고 이전에 얻은 `loc[0:5]` 결과와 비교합니다. 차이점을 찾을 수 있습니까?

**loc** 메서드는 *0*, *1*, *2*, *3*, *4* 및 *5*(6개 행)의 값 목록에서 인덱스가 *라벨* 인 행을 반환했습니다. 그러나 **iloc** 메서드는 0 ~ 5 범위에 포함된 *위치* 의 행을 반환하며, 정수 범위에는 상한 값이 포함되지 않으므로 *0*, *1*, *2*, *3*, *4*(5개 행) 위치가 포함됩니다.

**iloc**는 행을 넘어 열까지 확장되는 *위치*를 기준으로 데이터 프레임의 데이터 값을 식별합니다. 예를 들어 다음과 같이 첫번째 행에서 첫번째와 두번째 컬럼 위치의 값을 찾을 수 있습니다.

```python
df_students.iloc[0,[1,2]]
```

    StudyHours    10.0
    Grade         50.0
    Name: 0, dtype: object

**loc** 메서드로 돌아가 열과 함께 작동하는 방법을 살펴보겠습니다. **loc**는 위치가 아닌 인덱스 값을 기준으로 데이터 항목을 찾는 데 사용됩니다. 명시적 인덱스 열이 없는 경우 데이터 프레임의 행은 정수 값으로 인덱싱되지만 열은 이름으로 식별됩니다.

```python
df_students.loc[0,'Grade']
```

    50.0

또 다른 유용한 트릭이 있습니다. **loc** 메서드를 사용하여 다음과 같이 인덱스가 아닌 이름이 지정된 열을 참조하는 필터링 표현식을 기반으로 인덱스 처럼 생성된 행을 찾을 수 있습니다.

```python
df_students.loc[df_students['Name']=='Aisha']
```

      Name
      StudyHours
      Grade

      21
      Aisha
      12.0
      64.0

실제로 **loc** 메서드를 명시적으로 사용할 필요는 없습니다. 다음과 같은 DataFrame에 필터링을 적용하기만 하면 됩니다.

```python
df_students[df_students['Name']=='Aisha']
```

      Name
      StudyHours
      Grade

      21
      Aisha
      12.0
      64.0

또한 다음과 같은 DataFrame의 **query** 방법을 사용하여 동일한 결과를 얻을 수 있습니다.

```python
df_students.query('Name=="Aisha"')
```

      Name
      StudyHours
      Grade

      21
      Aisha
      12.0
      64.0

앞의 세 가지 예는 Pandas 작업에 대한 혼란스러운 사실을 보여줍니다. 작업을 하면서 동일한 결과를 얻을 수있는 여러가지 방법이 있습니다. 또 다른 예시로 DataFrame 열 이름을 참조하는 방법입니다. 열 이름을 명명 된 인덱스 값으로 지정하거나(ex: `df_students['Name']`) 다음과 같이 열을 DataFrame의 속성으로 사용할 수 있습니다.

```python
df_students[df_students.Name == 'Aisha']
```

      Name
      StudyHours
      Grade

      21
      Aisha
      12.0
      64.0

### 파일에서 DataFrame 불러오기

일부 기존 배열에서 DataFrame으로 구성했습니다. 그러나 많은 실제 시나리오에서 데이터는 파일과 같은 소스에서 로드됩니다. 학생 성적 DataFrame을 텍스트 파일의 내용으로 대체 해보겠습니다.

```python
df_students = pd.read_csv('data/grades.csv',delimiter=',',header='infer')
df_students.head()
```

      Name
      StudyHours
      Grade

      0
      Dan
      10.00
      50.0

      1
      Joann
      11.50
      50.0

      2
      Pedro
      9.00
      47.0

      3
      Rosie
      16.00
      97.0

      4
      Ethan
      9.25
      49.0

DataFrame의 **read_csv** 메서드는 텍스트 파일에서 데이터를 로드하는데 사용됩니다. 예제 코드에서 볼 수 있듯이 열 구분 기호 및 열 머리글이 포함 된 행 (있는 경우)과 같은 옵션을 지정할 수 있습니다 (이 경우 구분 기호는 쉼표이고 첫 번째 행에는 열 이름이 포함됩니다. 기본 설정이므로 매개 변수를 생략 할 수 있습니다).

### 결측값 처리

데이터 과학자가 처리해야하는 가장 일반적인 문제 중 하나는 불완전하거나 결측이 있는 데이터입니다. 그렇다면 DataFrame에 누락 된 값이 포함되어 있는지 어떻게 알 수 있습니까? 다음과 같이 **isnull** 메서드를 사용하여 어떤 값들이 null인지 식별 할 수 있습니다.

```python
df_students.isnull()
```

      Name
      StudyHours
      Grade

      0
      False
      False
      False

      1
      False
      False
      False

      2
      False
      False
      False

      3
      False
      False
      False

      4
      False
      False
      False

      5
      False
      False
      False

      6
      False
      False
      False

      7
      False
      False
      False

      8
      False
      False
      False

      9
      False
      False
      False

      10
      False
      False
      False

      11
      False
      False
      False

      12
      False
      False
      False

      13
      False
      False
      False

      14
      False
      False
      False

      15
      False
      False
      False

      16
      False
      False
      False

      17
      False
      False
      False

      18
      False
      False
      False

      19
      False
      False
      False

      20
      False
      False
      False

      21
      False
      False
      False

      22
      False
      False
      True

      23
      False
      True
      True

더 큰 DataFrame을 사용하면 모든 행과 열을 개별적으로 검토하는 것은 비효율적입니다. 따라서 다음과 같이 각 열에 대해 누락 된 값의 합계를 얻습니다.

```python
df_students.isnull().sum()
```

    Name          0
    StudyHours    1
    Grade         2
    dtype: int64

이제 누락 된 **StudyHours** 값이 하나 있고 **Grade** 값이 누락 된 두 개가 있다는 것을 알았습니다.

데이터 프레임의 컨텍스트를 확인하기 위해 열 중 하나(데이터 프레임의 축 1)가 null인 행만 포함하도록 데이터 프레임을 필터링할 수 있습니다.

```python
df_students[df_students.isnull().any(axis=1)]
```

      Name
      StudyHours
      Grade

      22
      Bill
      8.0
      NaN

      23
      Ted
      NaN
      NaN

DataFrame이 검색되면 누락 된 숫자 값이 ** NaN ** (* 숫자가 아님 *)으로 표시됩니다.

이제 null 값을 찾았으므로 어떻게 해야할까요?

한 가지 일반적인 접근 방식은 대표 값으로 *대체* 하는 것입니다. 예를 들어, 학업 시간이 누락된 경우, 학생이 평균 학업 시간 동안 학습했다고 가정하고 누락 된 값을 평균 학업 시간으로 대체할 수 있습니다. 이를 위해 다음과 같이 **fillna** 메서드를 사용할 수 있습니다.

```python
df_students.StudyHours = df_students.StudyHours.fillna(df_students.StudyHours.mean())
df_students
```

      Name
      StudyHours
      Grade

      0
      Dan
      10.000000
      50.0

      1
      Joann
      11.500000
      50.0

      2
      Pedro
      9.000000
      47.0

      3
      Rosie
      16.000000
      97.0

      4
      Ethan
      9.250000
      49.0

      5
      Vicky
      1.000000
      3.0

      6
      Frederic
      11.500000
      53.0

      7
      Jimmie
      9.000000
      42.0

      8
      Rhonda
      8.500000
      26.0

      9
      Giovanni
      14.500000
      74.0

      10
      Francesca
      15.500000
      82.0

      11
      Rajab
      13.750000
      62.0

      12
      Naiyana
      9.000000
      37.0

      13
      Kian
      8.000000
      15.0

      14
      Jenny
      15.500000
      70.0

      15
      Jakeem
      8.000000
      27.0

      16
      Helena
      9.000000
      36.0

      17
      Ismat
      6.000000
      35.0

      18
      Anila
      10.000000
      48.0

      19
      Skye
      12.000000
      52.0

      20
      Daniel
      12.500000
      63.0

      21
      Aisha
      12.000000
      64.0

      22
      Bill
      8.000000
      NaN

      23
      Ted
      10.413043
      NaN

또는 절대적으로 정확한 것으로 알고있는 데이터만 사용하도록 하는 것이 중요할 수 있습니다. 따라서 **dropna** 메서드를 사용하여 null 값이 포함된 행이나 열을 삭제할 수 있습니다. 이 경우 열에 null 값이 포함된 행 (DataFrame의 세로축(0))을 제거합니다.

```python
df_students = df_students.dropna(axis=0, how='any')
df_students
```

      Name
      StudyHours
      Grade

      0
      Dan
      10.00
      50.0

      1
      Joann
      11.50
      50.0

      2
      Pedro
      9.00
      47.0

      3
      Rosie
      16.00
      97.0

      4
      Ethan
      9.25
      49.0

      5
      Vicky
      1.00
      3.0

      6
      Frederic
      11.50
      53.0

      7
      Jimmie
      9.00
      42.0

      8
      Rhonda
      8.50
      26.0

      9
      Giovanni
      14.50
      74.0

      10
      Francesca
      15.50
      82.0

      11
      Rajab
      13.75
      62.0

      12
      Naiyana
      9.00
      37.0

      13
      Kian
      8.00
      15.0

      14
      Jenny
      15.50
      70.0

      15
      Jakeem
      8.00
      27.0

      16
      Helena
      9.00
      36.0

      17
      Ismat
      6.00
      35.0

      18
      Anila
      10.00
      48.0

      19
      Skye
      12.00
      52.0

      20
      Daniel
      12.50
      63.0

      21
      Aisha
      12.00
      64.0

### DataFrame에서 데이터 탐색

이제 누락된 값을 처리했으므로 DataFrame에서 데이터를 탐색할 준비가 되었습니다. 평균 학업 시간과 성적을 비교하여 시작하겠습니다.

```python
# Get the mean study hours using the column name as an index
mean_study = df_students['StudyHours'].mean()

# Get the mean grade using the column name as a property (just to make the point!)
mean_grade = df_students.Grade.mean()

# Print the mean study hours and mean grade
print('Average weekly study hours: {:.2f}\nAverage grade: {:.2f}'.format(mean_study, mean_grade))
```

    Average weekly study hours: 10.52
    Average grade: 49.18

평균 학업 시간 이상 공부한 학생만 찾기 위해 DataFrame을 필터링 해줍니다.

```python
# Get students who studied for the mean or more hours
df_students[df_students.StudyHours > mean_study]
```

      Name
      StudyHours
      Grade

      1
      Joann
      11.50
      50.0

      3
      Rosie
      16.00
      97.0

      6
      Frederic
      11.50
      53.0

      9
      Giovanni
      14.50
      74.0

      10
      Francesca
      15.50
      82.0

      11
      Rajab
      13.75
      62.0

      14
      Jenny
      15.50
      70.0

      19
      Skye
      12.00
      52.0

      20
      Daniel
      12.50
      63.0

      21
      Aisha
      12.00
      64.0

필터링 된 결과 자체가 DataFrame이므로 다른 DataFrame과 마찬가지로 해당 열을 사용할 수 있습니다.

예를 들어, 평균 공부 시간보다 더 많은 시간을 공부한 학생의 평균 성적을 찾아 보겠습니다.

```python
# What was their mean grade?
df_students[df_students.StudyHours > mean_study].Grade.mean()
```

    66.7

이 수업의 합격 점수가 60점이라고 가정해보겠습니다.

이 정보를 사용하여 각 학생이 합격했는지 여부를 나타내는 새로운 열을 DataFrame에 추가할 수 있습니다.

먼저 합격/불합격(True 또는 False)을 포함하는 Pandas **Series**로 만든 다음, 해당 Series를 DataFrame의 새로운 열로 병합합니다.

```python
passes  = pd.Series(df_students['Grade'] >= 60)
df_students = pd.concat([df_students, passes.rename("Pass")], axis=1)

df_students
```

      Name
      StudyHours
      Grade
      Pass

      0
      Dan
      10.00
      50.0
      False

      1
      Joann
      11.50
      50.0
      False

      2
      Pedro
      9.00
      47.0
      False

      3
      Rosie
      16.00
      97.0
      True

      4
      Ethan
      9.25
      49.0
      False

      5
      Vicky
      1.00
      3.0
      False

      6
      Frederic
      11.50
      53.0
      False

      7
      Jimmie
      9.00
      42.0
      False

      8
      Rhonda
      8.50
      26.0
      False

      9
      Giovanni
      14.50
      74.0
      True

      10
      Francesca
      15.50
      82.0
      True

      11
      Rajab
      13.75
      62.0
      True

      12
      Naiyana
      9.00
      37.0
      False

      13
      Kian
      8.00
      15.0
      False

      14
      Jenny
      15.50
      70.0
      True

      15
      Jakeem
      8.00
      27.0
      False

      16
      Helena
      9.00
      36.0
      False

      17
      Ismat
      6.00
      35.0
      False

      18
      Anila
      10.00
      48.0
      False

      19
      Skye
      12.00
      52.0
      False

      20
      Daniel
      12.50
      63.0
      True

      21
      Aisha
      12.00
      64.0
      True

DataFrame은 테이블(tabular) 형식 데이터 용도로 설계되었으며, 이를 사용하여 관계형 데이터베이스에서 수행할 수 있는 다양한 종류의 데이터 그룹화 및 집계와 같은 데이터 분석 작업을 수행할 수 있습니다.

예를 들어 **groupby** 방법을 사용하여 이전에 추가한 **Pass** 열을 기반으로 학생 데이터를 그룹으로 그룹화하고 각 그룹에 속한 학생 수를 계산할 수 있습니다. 즉, 얼마나 많은 학생들이 합격하고 실패했는지 확인할 수 있습니다.

```python
print(df_students.groupby(df_students.Pass).Name.count())
```

    Pass
    False    15
    True      7
    Name: Name, dtype: int64

사용 가능한 집계 함수를 사용하여 그룹의 여러 필드를 집계 할 수 있습니다. 예를 들어, 과정에 합격하고 실패한 학생 그룹의 평균 학습 시간과 성적을 찾을 수 있습니다.

```python
print(df_students.groupby(df_students.Pass)['StudyHours', 'Grade'].mean())
```

           StudyHours      Grade
    Pass
    False    8.783333  38.000000
    True    14.250000  73.142857

    <ipython-input-32-8b15539b8d19>:1: FutureWarning: Indexing with multiple keys (implicitly converted to a tuple of keys) will be deprecated, use a list instead.
      print(df_students.groupby(df_students.Pass)['StudyHours', 'Grade'].mean())

데이터프레임은 놀라울 정도로 다재다능하며 데이터를 쉽게 조작할 수 있습니다. 대부분의 데이터프레임 작업은 데이터프레임의 새 복사본을 반환하므로 기존 변수를 수정하고 기존 변수를 유지하려면 작업 결과를 기존 변수에 할당해야 합니다. 예를 들어, 아래 코드는 학생 데이터를 내림차순으로 정렬하고 결과로 정렬된 DataFrame을 원래 **df_students** 변수에 할당합니다.

```python
# Create a DataFrame with the data sorted by Grade (descending)
df_students = df_students.sort_values('Grade', ascending=False)

# Show the DataFrame
df_students
```

      Name
      StudyHours
      Grade
      Pass

      3
      Rosie
      16.00
      97.0
      True

      10
      Francesca
      15.50
      82.0
      True

      9
      Giovanni
      14.50
      74.0
      True

      14
      Jenny
      15.50
      70.0
      True

      21
      Aisha
      12.00
      64.0
      True

      20
      Daniel
      12.50
      63.0
      True

      11
      Rajab
      13.75
      62.0
      True

      6
      Frederic
      11.50
      53.0
      False

      19
      Skye
      12.00
      52.0
      False

      1
      Joann
      11.50
      50.0
      False

      0
      Dan
      10.00
      50.0
      False

      4
      Ethan
      9.25
      49.0
      False

      18
      Anila
      10.00
      48.0
      False

      2
      Pedro
      9.00
      47.0
      False

      7
      Jimmie
      9.00
      42.0
      False

      12
      Naiyana
      9.00
      37.0
      False

      16
      Helena
      9.00
      36.0
      False

      17
      Ismat
      6.00
      35.0
      False

      15
      Jakeem
      8.00
      27.0
      False

      8
      Rhonda
      8.50
      26.0
      False

      13
      Kian
      8.00
      15.0
      False

      5
      Vicky
      1.00
      3.0
      False

## Matplotlib로 데이터 시각화

DataFrames는 표 형식 데이터를 탐색하고 분석할 수 있는 좋은 방법을 제공하지만 때로는 한 장의 그림이 천 개의 행과 열의 가치를 지닙니다. **Matplotlib** 라이브러리는 데이터 분석 능력을 크게 향상시킬 수있는 데이터 시각화 플로팅을 위한 기반을 제공합니다.

각 학생의 성적을 보여주는 간단한 막대 차트로 시작하겠습니다.

```python
# Ensure plots are displayed inline in the notebook
%matplotlib inline

from matplotlib import pyplot as plt

# Create a bar plot of name vs grade
plt.bar(x=df_students.Name, height=df_students.Grade)

# Display the plot
plt.show()
```

잘 적용됬습니다. 하지만 우리가 보고있는 차트를 더 명확하게 만들어 볼 수 있습니다.

Matplotlib의 **pyplot** 클래스를 사용하여 차트를 플로팅했습니다. 이 클래스는 플롯의 시각적 요소를 개선하는 다양한 방법을 제공합니다. 예를 들어, :

- Specifies the color of the bar chart.(막대 차트의 색상을 지정합니다.)
- Adds a title to the chart(우리가 알 수 있게 대표적인 제목을 추가합니다.)
- Adds labels to the X and Y (따라서 어떤 축에 어떤 데이터가 표시되는지 알 수 있습니다).
- Adds a grid (막대 값을 쉽게 결정할 수 있도록 해줍니다.)
- Rotates the X markers (마커를 제대로 읽을 수 있도록 해줍니다.)

```python
# Create a bar plot of name vs grade
plt.bar(x=df_students.Name, height=df_students.Grade, color='orange')

# Customize the chart
plt.title('Student Grades')
plt.xlabel('Student')
plt.ylabel('Grade')
plt.grid(color='#95a5a6', linestyle='--', linewidth=2, axis='y', alpha=0.7)
plt.xticks(rotation=90)

# Display the plot
plt.show()
```

플롯은 기술적으로 **Figure**과 함께 포함되어 있습니다. 이전 예에서는 그림이 암묵적으로 생성되었지만 명시적으로 생성할 수 있습니다. 예를 들어, 다음 코드는 특정 크기의 플롯을 생성합니다.

```python
# Create a Figure
fig = plt.figure(figsize=(8,3))

# Create a bar plot of name vs grade
plt.bar(x=df_students.Name, height=df_students.Grade, color='orange')

# Customize the chart
plt.title('Student Grades')
plt.xlabel('Student')
plt.ylabel('Grade')
plt.grid(color='#95a5a6', linestyle='--', linewidth=2, axis='y', alpha=0.7)
plt.xticks(rotation=90)

# Show the figure
plt.show()
```

Figure에는 각각 고유 한 *축*에 여러 개의 서브 플롯이 포함될 수 있습니다.

예를 들어, 다음 코드는 두 개의 서브 플롯이있는 그림을 만듭니다. 하나는 학생 성적을 표시하는 막대 차트이고 다른 하나는 합격 점수와 비 합격 점수를 비교하는 원형 차트입니다.

```python
# Create a figure for 2 subplots (1 row, 2 columns)
fig, ax = plt.subplots(1, 2, figsize = (10,4))

# Create a bar plot of name vs grade on the first axis
ax[0].bar(x=df_students.Name, height=df_students.Grade, color='orange')
ax[0].set_title('Grades')
ax[0].set_xticklabels(df_students.Name, rotation=90)

# Create a pie chart of pass counts on the second axis
pass_counts = df_students['Pass'].value_counts()
ax[1].pie(pass_counts, labels=pass_counts)
ax[1].set_title('Passing Grades')
ax[1].legend(pass_counts.keys().tolist())

# Add a title to the Figure
fig.suptitle('Student Data')

# Show the figure
fig.show()
```

지금까지 Matplotlib.pyplot 객체의 메서드를 사용하여 차트를 플로팅했습니다. 그러나 Matplotlib는 Python의 그래픽에 매우 기초가되어 Pandas를 비롯한 많은 패키지가 기본 Matplotlib 함수를 추상화하고 플로팅을 단순화하는 메서드를 제공합니다. 예를 들어, DataFrame은 학습 시간의 막대 차트를 표시하기 위해 다음 예와 같이 데이터를 표시하는 자체 방법을 제공합니다.

```python
df_students.plot.bar(x='Name', y='StudyHours', color='teal', figsize=(6,4))
```

## 통계 분석 시작하기

이제 Python을 사용하여 데이터를 조작하고 시각화하는 방법을 알았으므로 분석을 시작할 수 있습니다.

많은 데이터 과학이 *통계*에 뿌리를 두고 있으므로 몇 가지 기본적인 통계 기법을 살펴보겠습니다.

> **참고** : 이것은 통계를 가르치기위한 것이 아닙니다.이 노트북에는 너무 큰 주제입니다. 그러나 데이터 과학자가 기계 학습 모델링을 준비하기 위해 데이터를 탐색 할 때 사용하는 몇 가지 통계 개념과 기술을 소개합니다.

### 기술 통계 및 데이터 분포

*변수* (예 : 학생 성적 샘플)를 조사할 때 데이터사이언티스트는 특히 *분포* (즉, 샘플에 걸쳐 모든 다른 성적 값이 어떻게 분산되어 있는지)에 관심이 있습니다. 이 탐색의 시작점은 데이터를 히스토그램으로 시각화하고 변수의 각 값이 얼마나 자주 발생하는지 확인하는 것입니다.

```python
# Get the variable to examine
var_data = df_students['Grade']

# Create a Figure
fig = plt.figure(figsize=(10,4))

# Plot a histogram
plt.hist(var_data)

# Add titles and labels
plt.title('Data Distribution')
plt.xlabel('Value')
plt.ylabel('Frequency')

# Show the figure
fig.show()
```

성적에 대한 히스토그램은 대칭 모양으로, 가장 자주 발생하는 성적은 범위의 중간 (약 50개)에있는 경향이 있으며 척도의 가장 끝에있는 성적은 더 적습니다.

#### 중심 경향 측정

분포를 더 잘 이해하기 위해 소위 *중심 경향 측정*을 사용할 수 있습니다. 데이터의 "중간"을 나타내는 통계를 설명하는 멋진 방법입니다. 이것의 목표는 "일반적인" 값을 찾는 것입니다. 데이터 중간을 정의하는 일반적인 방법은 다음과 같습니다.

- *평균* : 샘플 세트의 모든 값을 더한 다음 합계를 샘플 수로 나눈 단순 평균입니다.
- *중간 값* : 모든 샘플 값의 범위 중간에있는 값입니다.
- *모드* : 샘플 세트 \* 에서 가장 일반적으로 발생하는 값입니다.

비교를 위한 최소값 및 최대값과 함께 이 값을 계산하여 히스토그램에 표시해 보겠습니다.

> \* 물론 일부 샘플 세트에서는 가장 일반적인 값에 대한 동점이 있을 수 있습니다.이 경우 데이터 세트는 *bimodal* 또는 *multimodal*로 설명됩니다.

```python
# Get the variable to examine
var = df_students['Grade']

# Get statistics
min_val = var.min()
max_val = var.max()
mean_val = var.mean()
med_val = var.median()
mod_val = var.mode()[0]

print('Minimum:{:.2f}\nMean:{:.2f}\nMedian:{:.2f}\nMode:{:.2f}\nMaximum:{:.2f}\n'.format(min_val,
                                                                                mean_val,
                                                                                med_val,
                                                                                mod_val,
                                                                                max_val))

# Create a Figure
fig = plt.figure(figsize=(10,4))

# Plot a histogram
plt.hist(var)

# Add lines for the statistics
plt.axvline(x=min_val, color = 'gray', linestyle='dashed', linewidth = 2)
plt.axvline(x=mean_val, color = 'cyan', linestyle='dashed', linewidth = 2)
plt.axvline(x=med_val, color = 'red', linestyle='dashed', linewidth = 2)
plt.axvline(x=mod_val, color = 'yellow', linestyle='dashed', linewidth = 2)
plt.axvline(x=max_val, color = 'gray', linestyle='dashed', linewidth = 2)

# Add titles and labels
plt.title('Data Distribution')
plt.xlabel('Value')
plt.ylabel('Frequency')

# Show the figure
fig.show()
```

성적 데이터의 경우 평균, 중앙값 및 최빈값은 모두 최소값과 최대 값의 중간 인 약 50 인 것 같습니다.

변수의 분포를 시각화하는 또 다른 방법은 *box* 플롯 (*box-and-whiskers* 플롯 이라고도 함)을 사용하는 것입니다. 성적 데이터용으로 하나를 만들어 보겠습니다.

```python
# Get the variable to examine
var = df_students['Grade']

# Create a Figure
fig = plt.figure(figsize=(10,4))

# Plot a histogram
plt.boxplot(var)

# Add titles and labels
plt.title('Data Distribution')

# Show the figure
fig.show()
```

상자 수염 그림은 히스토그램과 다른 형식으로 성적의 분포를 보여줍니다. 그림의 *box* 부분은 데이터의 내부 2 개 *사분위수*가있는 위치를 보여줍니다. 따라서이 경우 성적의 절반은 약 36과 63 사이입니다. 상자에서 확장 된 *수염*은 외부 2 사분위수를 표시합니다. 따라서 이 경우의 나머지 절반은 0과 36 또는 63과 100 사이입니다. 상자의 선은 앙의 값을 나타냅니다.

히스토그램과 상자 플롯을 결합하고 상자 플롯의 방향을 변경하여 히스토그램과 정렬하는 것이 유용할 수 있습니다 (어떤 면에서 히스토그램을 분포의 "front elevation" 보기로 생각하는 것이 도움이 될 수 있습니다. 위에서 분포의 "plan" 보기로 플롯합니다.)

```python
# Create a function that we can re-use
def show_distribution(var_data):
    from matplotlib import pyplot as plt

    # Get statistics
    min_val = var_data.min()
    max_val = var_data.max()
    mean_val = var_data.mean()
    med_val = var_data.median()
    mod_val = var_data.mode()[0]

    print('Minimum:{:.2f}\nMean:{:.2f}\nMedian:{:.2f}\nMode:{:.2f}\nMaximum:{:.2f}\n'.format(min_val,
                                                                                mean_val,
                                                                                med_val,
                                                                                mod_val,
                                                                                max_val))

    # Create a figure for 2 subplots (2 rows, 1 column)
    fig, ax = plt.subplots(2, 1, figsize = (10,4))

    # Plot the histogram
    ax[0].hist(var_data)
    ax[0].set_ylabel('Frequency')

    # Add lines for the mean, median, and mode
    ax[0].axvline(x=min_val, color = 'gray', linestyle='dashed', linewidth = 2)
    ax[0].axvline(x=mean_val, color = 'cyan', linestyle='dashed', linewidth = 2)
    ax[0].axvline(x=med_val, color = 'red', linestyle='dashed', linewidth = 2)
    ax[0].axvline(x=mod_val, color = 'yellow', linestyle='dashed', linewidth = 2)
    ax[0].axvline(x=max_val, color = 'gray', linestyle='dashed', linewidth = 2)

    # Plot the boxplot
    ax[1].boxplot(var_data, vert=False)
    ax[1].set_xlabel('Value')

    # Add a title to the Figure
    fig.suptitle('Data Distribution')

    # Show the figure
    fig.show()

# Get the variable to examine
col = df_students['Grade']
# Call the function
show_distribution(col)
```

중심 경향의 모든 측정은 데이터 분포의 중간에 있으며, 값이 중간에서 양방향으로 점진적으로 낮아지는 대칭입니다.

이 분포를 더 자세히 살펴 보려면 통계가 기본적으로 데이터 *샘플*을 취하고 확률 함수를 사용하여 데이터의 전체 *모집*에 대한 정보를 외삽하는 것에 관한 것임을 이해해야합니다. 예를 들어, 학생 데이터는 22 개의 샘플로 구성되며 각 샘플에 대해 성적 값이 있습니다. 각 샘플 성적이 과정에 부여된 모든 성적 집합에서 무작위로 선택된 변수로 생각할 수 있습니다. 이러한 랜덤 변수가 충분히 있으면 전체 모집단에 대한 성적 분포를 추정하는 *확률 밀도 함수*를 계산할 수 있습니다.

Pandas DataFrame 클래스는이 밀도를 표시하는데 유용한 플롯 함수를 제공합니다.

```python
def show_density(var_data):
    from matplotlib import pyplot as plt

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

    # Plot density
    var_data.plot.density()

    # Add titles and labels
    plt.title('Data Density')

    # Show the mean, median, and mode
    plt.axvline(x=var_data.mean(), color = 'cyan', linestyle='dashed', linewidth = 2)
    plt.axvline(x=var_data.median(), color = 'red', linestyle='dashed', linewidth = 2)
    plt.axvline(x=var_data.mode()[0], color = 'yellow', linestyle='dashed', linewidth = 2)

    # Show the figure
    plt.show()

# Get the density of Grade
col = df_students['Grade']
show_density(col)
```

표본의 히스토그램에서 예상한대로 밀도는 평균과 모드가 중심에 있고 대칭 꼬리에있는 *정규* 분포라고하는 통계학의 특징적인 '종 모양의 곡선'을 보여줍니다.

이제 학습 시간 데이터의 분포를 살펴 보겠습니다.

```python
# Get the variable to examine
col = df_students['StudyHours']
# Call the function
show_distribution(col)
```

학습 시간 데이터의 분포는 성적의 분포와 크게 다릅니다.

상자 그림의 수염은 약 6.0까지만 확장되며, 이는 데이터의 1/4 대부분이 이 값보다 크다는 것을 나타냅니다. 최소값은 통계적으로 나머지 분포의 범위를 크게 벗어나는 *이상치*임을 나타내는 **o** 로 표시됩니다.

이상치는 여러가지 이유로 발생할 수 있습니다. 학생이 "10"시간의 학습 시간을 기록하려고 했지만 "1"을 입력하고 "0"을 놓쳤을 수 있습니다. 아니면 그 학생이 공부에 관해서라면 비정상적으로 게으른 것일 수도 있어요! 어느 쪽이든, 이것은 전형적인 학생을 나타내지 않는 통계적인 이례이다. 이상치가 없는 분포는 어떤지 봅시다.

```python
# Get the variable to examine
col = df_students[df_students.StudyHours>1]['StudyHours']
# Call the function
show_distribution(col)
```

이 예에서 데이터 세트는 **1** 값이 **StudyHours** 열에 대한 이상치임을 명확하게 볼 수 있을만큼 충분히 작으므로 명시적으로 제외 할 수 있습니다.
대부분의 실제 사례에서 이상치를 대부분의 데이터가 있는 백분위 수보다 낮거나 높은 값으로 간주하는 것이 더 쉽습니다. 예를 들어, 다음 코드는 Pandas **quantile** 함수를 사용하여 0.01번째 백분위 수 (데이터의 99 %가 상주하는 값) 미만의 관측치를 제외합니다.

```python
q01 = df_students.StudyHours.quantile(0.01)
# Get the variable to examine
col = df_students[df_students.StudyHours>q01]['StudyHours']
# Call the function
show_distribution(col)
```

> **Tip** : 높은 백분위 수 값에서 임계값을 정의하여 분포의 상한에서 이상치를 제거할 수도 있습니다. 예를 들어 **quantile** 함수를 사용하여 99 %보다 낮은 0.99 백분위 수를 찾을 수 있습니다.

이상치를 제거하면 상자 그림에 4 사분위수 내의 모든 데이터가 표시됩니다. 분포는 성적 데이터와 같이 대칭이 아닙니다. 학습 시간이 약 16 시간으로 매우 높은 일부 학생이 있지만 데이터의 대부분은 7 시간에서 13 시간 사이입니다. 극히 높은 몇 가지 값은 평균을 척도의 더 높은 쪽 끝으로 당깁니다.

이 분포의 밀도를 살펴 보겠습니다.

```python
# Get the density of StudyHours
show_density(col)
```

이런 종류의 분포를 *right skewed* 이라고 합니다. 데이터의 질량은 분포의 왼쪽에 있으며, 가장 높은 값의 값으로 인해 오른쪽에 긴 꼬리를 생성합니다. 평균을 오른쪽으로 끌어옵니다.

#### 분산 측정

이제 우리는 중간 성적 및 학습 시간 데이터 분포가 어디에 있는지 좋은 아이디어를 얻었습니다. 그러나 우리가 조사해야 할 분포의 또 다른 측면이 있습니다. 이 데이터에 얼마나 많은 변동성이 있을까요?

데이터의 변동성을 측정하는 일반적인 통계는 다음과 같습니다.

- **범위** : 최대 값과 최소값의 차이. 이를 위한 내장 함수는 없지만 **min** 및 **max** 함수를 사용하여 쉽게 계산할 수 있습니다.
- **분산** : 평균과의 차이 제곱의 평균입니다. **var** 함수를 사용하여 이를 찾을 수 있습니다.
- **표준 편차** : 분산의 제곱근입니다. **std** 함수를 사용하여 이를 찾을 수 있습니다.

```python
for col_name in ['Grade','StudyHours']:
    col = df_students[col_name]
    rng = col.max() - col.min()
    var = col.var()
    std = col.std()
    print('\n{}:\n - Range: {:.2f}\n - Variance: {:.2f}\n - Std.Dev: {:.2f}'.format(col_name, rng, var, std))
```

    Grade:
     - Range: 94.00
     - Variance: 472.54
     - Std.Dev: 21.74

    StudyHours:
     - Range: 15.00
     - Variance: 12.16
     - Std.Dev: 3.49

이러한 통계 중 표준 편차가 일반적으로 가장 유용합니다. 데이터 자체와 동일한 척도로 데이터의 분산 측정 값을 제공합니다.(예 : 성적 분포에 대한 포인트 및 StudyHours 분포에 대한 시간). 표준 편차가 높을수록 분포의 값을 분포 평균과 비교할 때 더 많은 분산이 있습니다. 즉, 데이터가 더 분산되어 있습니다.

정규분포로 작업 할 때 표준 편차는 더 큰 통찰력을 제공하기 위해 정규 분포의 특정 특성과 함께 작동합니다. 아래 셀을 실행하여 표준 편차와 정규 분포의 데이터 간의 관계를 확인 합니다.

```python
import scipy.stats as stats

# Get the Grade column
col = df_students['Grade']

# get the density
density = stats.gaussian_kde(col)

# Plot the density
col.plot.density()

# Get the mean and standard deviation
s = col.std()
m = col.mean()

# Annotate 1 stdev
x1 = [m-s, m+s]
y1 = density(x1)
plt.plot(x1,y1, color='magenta')
plt.annotate('1 std (68.26%)', (x1[1],y1[1]))

# Annotate 2 stdevs
x2 = [m-(s*2), m+(s*2)]
y2 = density(x2)
plt.plot(x2,y2, color='green')
plt.annotate('2 std (95.45%)', (x2[1],y2[1]))

# Annotate 3 stdevs
x3 = [m-(s*3), m+(s*3)]
y3 = density(x3)
plt.plot(x3,y3, color='orange')
plt.annotate('3 std (99.73%)', (x3[1],y3[1]))

# Show the location of the mean
plt.axvline(col.mean(), color='cyan', linestyle='dashed', linewidth=1)

plt.axis('off')

plt.show()
```

수평선은 평균의 1, 2, 3 표준 편차 (+ 또는 -) 내의 데이터 백분율을 표시합니다.

모든 정규 분포에서 :
- 약 68.26 %의 값이 평균에서 1 std(표준편차) 내에 있습니다.
- 값의 약 95.45 %가 평균에서 2 std(표준편차) 내에 있습니다.
- 값의 약 99.73 %가 평균에서 3 std(표준편차) 내에 있습니다.

따라서 평균 성적이 49.18이고 표준 편차가 21.74이고 성적 분포가 거의 정상이라는 것을 알기 때문에 68.26%의 학생이 27.44에서 70.92 사이의 점수를 받아야한다고 계산할 수 있습니다.

학생 데이터 변수의 분포를 이해하는 데 사용한 기술 통계는 통계 분석의 기초입니다. 데이터를 탐색하는 데 매우 중요한 부분이므로 모든 숫자 열에 대한 기본 설명 통계를 반환하는 DataFrame 개체의 기본 제공 **Describe** 메서드가 있습니다.

```python
df_students.describe()
```

      StudyHours
      Grade

      count
      22.000000
      22.000000

      mean
      10.522727
      49.181818

      std
      3.487144
      21.737912

      min
      1.000000
      3.000000

      25%
      9.000000
      36.250000

      50%
      10.000000
      49.500000

      75%
      12.375000
      62.750000

      max
      16.000000
      97.000000

## 데이터 비교

이제 데이터 세트에있는 데이터의 통계적 분포에 대해 알고 있으므로 데이터를 조사하여 변수 간의 명백한 관계를 식별할 준비가되었습니다.

우선, 이상치가 포함된 행을 제거하여 일반적인 학생 클래스를 대표하는 샘플을 얻습니다. StudyHours 열에 매우 낮은 값을 가진 일부 이상치가 포함되어 있음을 확인 했으므로 해당 행을 제거합니다.

```python
df_sample = df_students[df_students['StudyHours']>1]
df_sample
```

      Name
      StudyHours
      Grade
      Pass

      3
      Rosie
      16.00
      97.0
      True

      10
      Francesca
      15.50
      82.0
      True

      9
      Giovanni
      14.50
      74.0
      True

      14
      Jenny
      15.50
      70.0
      True

      21
      Aisha
      12.00
      64.0
      True

      20
      Daniel
      12.50
      63.0
      True

      11
      Rajab
      13.75
      62.0
      True

      6
      Frederic
      11.50
      53.0
      False

      19
      Skye
      12.00
      52.0
      False

      1
      Joann
      11.50
      50.0
      False

      0
      Dan
      10.00
      50.0
      False

      4
      Ethan
      9.25
      49.0
      False

      18
      Anila
      10.00
      48.0
      False

      2
      Pedro
      9.00
      47.0
      False

      7
      Jimmie
      9.00
      42.0
      False

      12
      Naiyana
      9.00
      37.0
      False

      16
      Helena
      9.00
      36.0
      False

      17
      Ismat
      6.00
      35.0
      False

      15
      Jakeem
      8.00
      27.0
      False

      8
      Rhonda
      8.50
      26.0
      False

      13
      Kian
      8.00
      15.0
      False

### 수치형 및 범주형 변수 비교

데이터에는 두 개의 *수치형* 변수 (**StudyHours** 및 **Grade**)와 두 개의 *범주형* 변수 (**Name** 및 **Pass**)가 포함됩니다. 먼저 수치형 **StudyHours** 열을 범주형 **통과** 열과 비교하여 공부한 시간과 합격 점수 사이에 명백한 관계가 있는지 살펴보겠습니다.

이 비교를 위해 각 합격 값(참 및 거짓)에 대한 StudyHours의 분포를 보여주는 상자 그림을 만들어 보겠습니다.

```python
df_sample.boxplot(column='StudyHours', by='Pass', figsize=(8,5))
```

StudyHours 분포를 비교해보면, 과정을 통과한 학생들이 그렇지 않은 학생들보다 더 많은 시간을 공부하는 경향이 있다는 것이 확실 합니다(딱히 놀라운 것은 아니지만..). 따라서 학생이 과정을 통과할 가능성이 있는지 여부를 예측하려는 경우 학습에 소비하는 시간이 좋은 예측 특징변수가 될 수 있습니다.

### 수치형 변수 비교

이제 두 개의 수치형 변수를 비교해 봅시다. 먼저 점수와 학습 시간을 모두 보여주는 막대 차트를 만들 것입니다.

```python
# Create a bar plot of name vs grade and study hours
df_sample.plot(x='Name', y=['Grade','StudyHours'], kind='bar', figsize=(8,5))
```

차트는 각 학생의 점수 및 학습 시간에 대한 막대를 보여줍니다. 그러나 값이 다른 척도에 있기 때문에 비교하기가 쉽지 않습니다. 점수로 측정되며 범위는 3 ~ 97 입니다. 공부 시간은 시간 단위로 측정되며 1에서 16까지입니다.

다른 척도의 숫자 데이터를 다룰 때 일반적인 기술은 값이 비례 분포를 유지하지만 동일한 척도로 측정되도록 데이터를 *정규화* 하는 것입니다. 이를 수행하기 위해 0에서 1까지의 비율로 값을 비례 적으로 분배하는 *MinMax* 스케일링이라는 기술을 사용할 것입니다. 이 변환을 적용하는 코드를 작성할 수 있지만, **Scikit-Learn** 라이브러리에서 스케일러 함수를 제공합니다.

```python
from sklearn.preprocessing import MinMaxScaler

# Get a scaler object
scaler = MinMaxScaler()

# Create a new dataframe for the scaled values
df_normalized = df_sample[['Name', 'Grade', 'StudyHours']].copy()

# Normalize the numeric columns
df_normalized[['Grade','StudyHours']] = scaler.fit_transform(df_normalized[['Grade','StudyHours']])

# Plot the normalized values
df_normalized.plot(x='Name', y=['Grade','StudyHours'], kind='bar', figsize=(8,5))
```

데이터를 정규화하면 성적과 학습 시간 사이의 명백한 관계를 쉽게 확인할 수 있습니다. 정확히 일치하지는 않지만 성적이 높은 학생들이 더 많이 공부하는 경향이있는 것 같습니다.

그래서 공부 시간과 성적 사이에 상관 관계가 있다고 볼 수 있습니다. 사실, 이러한 열 간의 관계를 정량화하는 데 사용할 수 있는 통계적 상관 계수(correlation)가 있습니다.

```python
df_normalized.Grade.corr(df_normalized.StudyHours)
```

    0.9117666413789675

통계에서 correlation는 관계의 강도를 나타내는 -1과 1 사이의 값입니다. 0보다 큰 값은 *양의  상관 관계* 를 나타내며 (한 변수의 높은 값은 다른 변수의 높은 값과 일치하는 경향이 있음), 0 미만의 값은 *음의 상관 관계* 를 나타냅니다 (한 변수의 높은 값은 낮은 값과 일치하는 경향이 있음). 이 경우 상관 관계 값은 1에 가깝습니다. 공부 시간과 성적 사이에 강한 양의 상관 관계가 나타납니다.

> **참고** : 데이터 과학자들은 종종 "correlation is not causation"라는 격언을 인용합니다. 다시 말해, 통계적 상관 관계를 값 중 하나가 높은 *이유*를 설명하는 것으로 해석해서는 안됩니다. 학생 데이터의 경우, 통계에 따르면 성적이 높은 학생도 공부 시간이 많은 경향이 있습니다. 그러나 이것은 그들이 공부를 많이했기 때문에 높은 점수를 받았다는 것을 증명하는 것과는 다릅니다. 통계는 학생들이 성적이 높을 것이기 때문에 *많은 공부를 했다는 무의미한 결론을 뒷받침하는 증거*로도 사용할 수 있습니다.

두 수치형 열 간의 명백한 상관 관계를 시각화하는 또 다른 방법은 *산점도(scatter)* 플롯을 사용하는 것입니다.

```python
# Create a scatter plot
df_sample.plot.scatter(title='Study Time vs Grade', x='StudyHours', y='Grade')
```

다시 말해보면, 가장 많은 시간을 공부한 학생과 가장 높은 성적을받은 학생이라는 식별 가능한 패턴이있는 것 같습니다.

데이터의 일반적인 추세를 보여주는 그림에 *회귀* 선 (또는 *최적 적합 선*)을 추가하여 이를 더 명확하게 볼 수 있습니다. 이를 위해 *최소 제곱 회귀*라는 통계 기법을 사용합니다.

> **경고-수학 앞서서 보기!**
>
> 학교에서 선형 방정식을 푸는 방법을 배웠을 때로 돌아가서 선형 방정식의 *slope-intercept* 형식이 다음과 같다는 것을 기억하십시오.
>
> \begin{equation}y = mx + b\end{equation}
>
>이 방정식에서 *y* 및 *x*는 좌표 변수, *m*은 선의 기울기, *b*는 y 절편 (선이 Y 축을 통과하는 지점)입니다.
>
> 학생 데이터에 대한 산점도의 경우 이미 *x* (* StudyHours *) 및 *y* (*Grade*)에 대한 값이 있으므로 직선의 절편과 기울기만 계산하면 됩니다. 그 지점에 가장 가까운 선. 그런 다음 각 *x* (*StudyHours*) 값에 대해 해당 줄에 새로운 *y* 값을 계산하는 선형 방정식을 만들 수 있습니다. 혼동을 피하기 위해 새로운 *y* 값을 *f(x)* (*x*를 기반으로 한 선형 방정식 ***f*** unction의 출력이기 때문에). 원래 *y* (*Grade*) 값과 *f(x)* 값의 차이는 회귀선과 학생이 달성한 실제 *Grade* 간의 *오차*입니다. 우리의 목표는 전체 오차가 가장 낮은 선에 대한 기울기와 절편을 계산하는 것입니다.
>
> 특히, 각 점에 대한 오차를 취하고 제곱한 다음 모든 제곱 오차를 더하여 전체 오차를 정의합니다. 최적의 선은 제곱 오차의 합에 대해 가장 낮은 값을 제공하는 선입니다. 따라서 이름은 *최소 제곱 회귀*입니다.

다행히도 회귀 계산을 직접 코딩할 필요가 없습니다. **SciPy** 패키지에는 어려운 작업을 수행하는 **linregress** 메서드를 제공하는 **stats** 클래스가 포함되어 있습니다. 이것은 (다른 것들 중에서) 비교하고자 하는 주어진 변수 샘플 쌍을 기반으로 기울기 (*m*) 및 절편 (*b*) 등 기울기 방정식에 필요한 계수를 반환합니다.

```python
from scipy import stats

#
df_regression = df_sample[['Grade', 'StudyHours']].copy()

# Get the regression slope and intercept
m, b, r, p, se = stats.linregress(df_regression['StudyHours'], df_regression['Grade'])
print('slope: {:.4f}\ny-intercept: {:.4f}'.format(m,b))
print('so...\n f(x) = {:.4f}x + {:.4f}'.format(m,b))

# Use the function (mx + b) to calculate f(x) for each x (StudyHours) value
df_regression['fx'] = (m * df_regression['StudyHours']) + b

# Calculate the error between f(x) and the actual y (Grade) value
df_regression['error'] = df_regression['fx'] - df_regression['Grade']

# Create a scatter plot of Grade vs StudyHours
df_regression.plot.scatter(x='StudyHours', y='Grade')

# Plot the regression line
plt.plot(df_regression['StudyHours'],df_regression['fx'], color='cyan')

# Display the plot
plt.show()
```

이번에는 코드가 두 가지 다른 점을 표시했습니다. 샘플 연구 시간과 성적의 산점도가 이전과 같이 표시되고 최소 제곱 회귀 계수를 기반으로 한 최적의 라인이 표시됩니다.

회귀선에 대해 계산된 기울기 및 절편 계수가 플롯 위에 표시됩니다.

선은 각 **StudyHours** 값에 대해 계산된 ***f*(x)** 값을 기반으로합니다. 다음 값이 포함 된 테이블을 보려면 다음 셀을 실행하십시오.

- 각 학생의 **StudyHours**.
- 각 학생이 달성 한 **Grade**.
- 회귀선 계수를 사용하여 계산 된 ***f(x)*** 값.
- 계산 된 ***f(x)*** 값과 실제 **Grade** 값 사이의 *오차*입니다.

일부 오류, 특히 극단의 오류는 상당히 큽니다 (최대 17.5 점 이상). 그러나 일반적으로 선은 실제 성적에 매우 가깝습니다.

```python
# Show the original x,y values, the f(x) value, and the error
df_regression[['StudyHours', 'Grade', 'fx', 'error']]
```

      StudyHours
      Grade
      fx
      error

      3
      16.00
      97.0
      83.098400
      -13.901600

      10
      15.50
      82.0
      79.941687
      -2.058313

      9
      14.50
      74.0
      73.628262
      -0.371738

      14
      15.50
      70.0
      79.941687
      9.941687

      21
      12.00
      64.0
      57.844698
      -6.155302

      20
      12.50
      63.0
      61.001410
      -1.998590

      11
      13.75
      62.0
      68.893193
      6.893193

      6
      11.50
      53.0
      54.687985
      1.687985

      19
      12.00
      52.0
      57.844698
      5.844698

      1
      11.50
      50.0
      54.687985
      4.687985

      0
      10.00
      50.0
      45.217846
      -4.782154

      4
      9.25
      49.0
      40.482777
      -8.517223

      18
      10.00
      48.0
      45.217846
      -2.782154

      2
      9.00
      47.0
      38.904421
      -8.095579

      7
      9.00
      42.0
      38.904421
      -3.095579

      12
      9.00
      37.0
      38.904421
      1.904421

      16
      9.00
      36.0
      38.904421
      2.904421

      17
      6.00
      35.0
      19.964144
      -15.035856

      15
      8.00
      27.0
      32.590995
      5.590995

      8
      8.50
      26.0
      35.747708
      9.747708

      13
      8.00
      15.0
      32.590995
      17.590995

### 예측을 위한 회귀 계수 사용

이제 학습 시간 및 성적 관계에 대한 회귀 계수가 있으므로 함수를 사용하여 주어진 학습량에 대한 예상 성적을 추정 할 수 있습니다.

```python
# Define a function based on our regression coefficients
def f(x):
    m = 6.3134
    b = -17.9164
    return m*x + b

study_time = 14

# Get f(x) for study time
prediction = f(study_time)

# Grade can't be less than 0 or more than 100
expected_grade = max(0,min(100,prediction))

#Print the estimated grade
print ('Studying for {} hours per week may result in a grade of {:.0f}'.format(study_time, expected_grade))
```

    Studying for 14 hours per week may result in a grade of 70

따라서 표본 데이터에 통계를 적용하여 학습 시간과 성적 사이의 관계를 결정했습니다. 주어진 학습 시간 동안 성적을 예측하는 데 사용할 수있는 일반적인 기능에 그 관계를 캡슐화했습니다.

이 실습은 실제로 기계 학습의 기본 전제입니다. 하나 이상의 *특징* (이 경우 학습 한 시간)과 알려진 *라벨* 값 (이 경우 달성 한 성적)을 포함하는 샘플 데이터 세트를 가져 와서 도출하는 데 사용할 수 있습니다. 주어진 기능 집합에 대해 예측 된 레이블 값을 계산하는 함수입니다.

## 더 읽을 거리

이 노트북에서 살펴본 Python 패키지에 대해 자세히 알아 보려면 다음 문서를 참조하세요.

- [NumPy](https://numpy.org/doc/stable/)
- [Pandas](https://pandas.pydata.org/pandas-docs/stable/)
- [Matplotlib](https://matplotlib.org/contents.html)

## 과제 : 비행 데이터 분석

이 노트북이 데이터 탐색에 영감을 주었다면 미국 교통부의 비행 기록이 포함 된 실제 데이터 세트에 도전 해보세요. 노트북에서 도전 과제를 찾을 수 있습니다! [/challenges/01 - Flights Challenge.ipynb](./challenges/01%20-%20Flights%20Challenge.ipynb)

> **참고** : 이 옵션 챌린지를 완료하는 데 걸리는 시간은 연습의 예상 시간에 포함되지 않습니다. 원하는만큼 시간을 적게 또는 많이 사용할 수 있습니다!

```python

```


# hello

```python
import sympy as sp
print(sp.__version__)
a = {'hello':1, 'world':2}
```

    1.4

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

x = np.linspace(0, 100, 100)
y = np.sqrt(x)
plt.plot(x, y)
plt.show()
```


# Find worst cases

\begin{equation}
\begin{array}{rl}
    \mathcal{F}_L =& \dfrac{4 K I H r}{Q_{in}(1+f)}\\
    u_{c} =& \dfrac{-KI\mathcal{F}_L}{\theta \left(\mathcal{F}_L+1\right)}\\
    \tau =& -\dfrac{r}{|u_{c}|}\\
    C_{\tau,{\rm decay}}=& C_0 \exp{\left(-\lambda \tau \right)}\\
    C_{\tau,{\rm filtr}}=& C_0 \exp{\left(-k_{\rm att} \tau \right)}\\
    C_{\tau,{\rm dilut}} =& C_{in} \left( \dfrac{Q_{in}}{u_c \Delta y \Delta z} \right)\\
    C_{\tau,{\rm both}} =& \dfrac{C_{\rm in}Q_{\rm in}}{u_c \Delta y H \theta} \exp{\left(-\lambda\dfrac{r}{|u_c|}\right)}
\end{array}
\end{equation}

```python
%reset -f
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from os import system
from matplotlib.gridspec import GridSpec
from drawStuff import *

''' GLOBAL CONSTANTS '''
PI = 3.141592
THETA = 0.35
```

```python
def flowNumber():
    return (4.0*K*I*H*r) / (Qin*(1+f))

def uChar():
    return -(K*I*flowNumber())/(THETA*(flowNumber() + 1))

def tChar():
    return -r/uChar()

def cDecay():
    return C0 * np.exp(-decayRate * tChar())

def cDilut():
    return (C0 * Qin) / (-uChar() * delY * delZ * THETA)

def cBoth():
    return (C0 * Qin) / (-uChar() * delY * delZ * THETA) * np.exp(-decayRate * tChar())

def findSweet():
    deltaConc = np.abs(cBoth() - np.max(cBoth()))
    return np.argmin(deltaConc)
```

```python
K = 10**-2
Qin = 0.24/86400
f = 10
H = 20
r = 40
I = 0.001
C0 = 1.0
decayRate = 3.5353E-06
delY,delZ = 1.35,H
```

```python
print("Nondim Flow  = {:.2E}".format(flowNumber()))
print("Charac. Vel  = {:.2E} m/s".format(uChar()))
print("Charac. time = {:.2E} s".format(tChar()))
```

    Nondim Flow  = 1.05E+03
    Charac. Vel  = -2.85E-05 m/s
    Charac. time = 1.40E+06 s

```python
print("Rel concenc. due decay = {:.2E}".format(cDecay()))
print("Rel conc. due dilution = {:.2E}".format(cDilut()))
print("Rel conc. due both eff = {:.2E}".format(cBoth()))
```

    Rel concenc. due decay = 7.05E-03
    Rel conc. due dilution = 1.03E-02
    Rel conc. due both eff = 7.26E-05

```python
I = 10**np.linspace(-5,0,num=100)

c1 = cDecay()
c2 = cDilut()
c3 = cBoth()
i = findSweet()

worstC = c3[i]
worstI = I[i]
```

```python
fig, axs = plt.subplots(2,2,sharex=True, sharey=False,\
              figsize=(12,8),gridspec_kw={"height_ratios":[1,3],"hspace":0.04,"wspace":0.35})

bbox = dict(boxstyle='round', facecolor='mintcream', alpha=0.90)

arrowprops = dict(
    arrowstyle="->",
    connectionstyle="angle,angleA=90,angleB=10,rad=5")

annotation = \
    r"$\bf{-\log(C/C_0)} = $" + "{:.1f}".format(-np.log10(worstC)) + \
    "\n@" + r" $\bf{I} = $" + "{:.1E}".format(worstI)

information = \
    r"$\bf{K}$" + " = {:.1E} m/s".format(K) + "\n"\
    r"$\bf{H}$" + " = {:.1f} m".format(H) + "\n"\
    r"$\bf{r}$" + " = {:.1f} m".format(r) + "\n"\
    r"$\bf{Q_{in}}$" + " = {:.2f} m³/d".format(Qin*86400) + "\n"\
    r"$\bf{f}$" + " = {:.1f}".format(f) + "\n"\
    r"$\bf{\lambda}$" + " = {:.2E} 1/s".format(decayRate)

# Ax1 - Relative concentration
ax = axs[1,0]
ax.plot(I,c1,label="Due decay",lw=3,ls="dashed",alpha=0.8)
ax.plot(I,c2,label="Due dilution",lw=3,ls="dashed",alpha=0.8)
#ax.plot(I,np.minimum(c1,c2),label="Overall effect",lw=3,c='k',alpha=0.9)
ax.plot(I,c3,label="Overall effect",lw=3,c='k',alpha=0.9)

ax.set_xscale("log")
ax.set_yscale("log")

ax.set_ylim(1.0E-10,1)
ax.set_xlim(1.0E-4,1.0E-1)

ax.set_xlabel("Water table gradient\n$I$ [m/m]")
ax.set_ylabel("Relative Concentration\n$C/C_0$ [-]")
ax.legend(loc="lower left",shadow=True)

ax.annotate(annotation,(worstI,worstC),
    xytext=(0.05,0.85), textcoords='axes fraction',
    bbox=bbox, arrowprops=arrowprops)

ax.text(0.65,0.05,information,bbox=bbox,transform=ax.transAxes)
ax.axvline(x=I[i], lw = 1, ls = "dashed", c = "red")

####################################
# Ax2 - log-removals
ax = axs[1,1]
ax.plot(I,-np.log10(c1),label="Due decay",lw=3,ls="dashed",alpha=0.8)
ax.plot(I,-np.log10(c2),label="Due dilution",lw=3,ls="dashed",alpha=0.8)
#ax.plot(I,-np.log10(np.minimum(c1,c2)),label="Overall effect",lw=3,c='k',alpha=0.9)
ax.plot(I,-np.log10(c3),label="Overall effect",lw=3,c='k',alpha=0.9)

ax.set_xscale("log")
ax.set_ylim(0,10)
ax.set_xlim(1.0E-4,1.0E-1)
ax.set_xlabel("Water table gradient\n$I$ [m/m]")
ax.set_ylabel("log-reductions\n$-\log(C/C_0)$ [-]")
ax.legend(loc="lower right",shadow=True)

ax.annotate(annotation,(worstI,-np.log10(worstC)),
    xytext=(0.05,0.10), textcoords='axes fraction',
    bbox=bbox, arrowprops=arrowprops)

ax.text(0.65,0.70,information,bbox=bbox,transform=ax.transAxes)
ax.axvline(x=I[i], lw = 1, ls = "dashed", c = "red")

####################################
#Ax3 - Flow number
ax = axs[0,1]
ax.plot(I,flowNumber(),label="flowNumber",lw=3,c="gray")
ax.axhline(y=1.0)
ax.set_xscale("log")
ax.set_yscale("log")
ax.xaxis.set_tick_params(which="both",labeltop='on',top=True,bottom=False)

ax.set_ylabel("Nondim. flow number\n$\mathcal{F}_L$ [-]")
ax.axvline(x=I[i], lw = 1, ls = "dashed", c = "red")

#Ax4 - Flow number
ax = axs[0,0]
ax.plot(I,flowNumber(),label="flowNumber",lw=3,c="gray")
ax.axhline(y=1.0)
ax.set_xscale("log")
ax.set_yscale("log")
ax.xaxis.set_tick_params(which="both",labeltop='on',top=True,bottom=False)

ax.set_ylabel("Nondim. flow number\n$\mathcal{F}_L$ [-]")
ax.axvline(x=I[i], lw = 1, ls = "dashed", c = "red")

plt.show()
```

____
# Find the worst case
## >> Geometric parameters $H$ and $r$

```python
K = 10**-2
Qin = 0.24/86400
f = 10
C0 = 1.0
decayRate = 3.5353E-06

Harray = np.array([2.,5.,10.,20.,50.])
rarray = np.array([5.,10.,40.,100.])
Iarray = 10**np.linspace(-5,0,num=100)

Ci = np.zeros([len(rarray),len(Harray)])
Ii = np.zeros([len(rarray),len(Harray)])
FLi = np.zeros([len(rarray),len(Harray)])

for hi,H in enumerate(Harray):
    for ri,r in enumerate(rarray):
        i = findSweet()

        worstC = -np.log10(cBoth()[i])
        worstGradient = Iarray[i]
        worstFlowNumber = flowNumber()[i]

        Ci[ri,hi]  = worstC
        Ii[ri,hi]  = worstGradient
        FLi[ri,hi] = worstFlowNumber
```

```python
myLabels={"Title": { 0: r"$\bf{-\log (C_{\tau}/C_0)}$",
                     1: r"$\bf{I}$ (%)",
                     2: r"$\log(\mathcal{F}_L)$"},
          "Y": "Aquifer thickness\n$\\bf{H}$ (m)",
          "X": "Setback distance\n$\\bf{r}$ (m)"}

threeHeatplots(data={"I":Ii.T,"C":Ci.T,"FL":FLi.T},\
               xlabel=Harray,ylabel=rarray,myLabels=myLabels);
```

## >>Well parameters

```python
K = 10**-2
H = 20
r = 40
C0 = 1.0
decayRate = 3.5353E-06

Qin_array = np.array([0.24,1.0,10.0,100.])/86400.
f_array = np.array([1,10.,100.,1000.,10000.])
Iarray = 10**np.linspace(-5,0,num=100)

Ci = np.zeros([len(Qin_array),len(f_array)])
Ii = np.zeros([len(Qin_array),len(f_array)])
FLi = np.zeros([len(Qin_array),len(f_array)])

for fi,f in enumerate(f_array):
    for qi,Qin in enumerate(Qin_array):
        i = findSweet()
        worstC = -np.log10(cBoth()[i])
        worstGradient = Iarray[i]
        worstFlowNumber = flowNumber()[i]

        Ci[qi,fi] = worstC
        Ii[qi,fi] = worstGradient
        FLi[qi,fi] = worstFlowNumber

### Plot heatmap
myLabels={"Title": { 0: r"$\bf{-\log (C_{\tau}/C_0)}$",
                     1: r"$\bf{I}$ (%)",
                     2: r"$\log(\mathcal{F}_L)$"},
          "Y": "Extraction to injection ratio\n$\\bf{f}$ (-)",
          "X": "Injection flow rate\n$\\bf{Q_{in}}$ (m³/d)"}

threeHeatplots(data={"I":Ii.T,"C":Ci.T,"FL":FLi.T},\
               xlabel=f_array,ylabel=np.round(Qin_array*86400,decimals=2),myLabels=myLabels);
```

## Hydraulic conductivity

```python
Qin = 0.24/86400
f = 10
C0 = 1.0
decayRate = 3.5353E-06

Karray = 10.**np.array([-1.,-2.,-3.,-4.,-5.])
rarray = np.array([5.,10.,40.,100.])
Iarray = 10**np.linspace(-5,0,num=100)

Ci = np.zeros([len(rarray),len(Karray)])
Ii = np.zeros([len(rarray),len(Karray)])
FLi = np.zeros([len(rarray),len(Karray)])

for ki,K in enumerate(Karray):
    for ri,r in enumerate(rarray):
        i = findSweet()

        worstC = -np.log10(cBoth()[i])
        worstGradient = Iarray[i]
        worstFlowNumber = flowNumber()[i]

        Ci[ri,ki] = worstC
        Ii[ri,ki] = worstGradient
        FLi[ri,ki] = worstFlowNumber

### Plot heatmap
myLabels={"Title": { 0: r"$\bf{-\log (C_{\tau}/C_0)}$",
                     1: r"$\bf{I}$ (%)",
                     2: r"$\log(\mathcal{F}_L)$"},
          "Y": "Hydraulic conductivity\n$\\bf{K}$ (m/s)",
          "X": "Setback distance\n$\\bf{r}$ (m)"}

threeHeatplots(data={"I":Ii.T,"C":Ci.T,"FL":FLi.T},\
               xlabel=Karray,ylabel=rarray,myLabels=myLabels);
```

```python
K = 10**-2
H = 20
f = 10
C0 = 1.0
decayRate = 3.5353E-06

Qin_array = np.array([0.24,1.0,10.0,100.])/86400.
rarray = np.array([5.,10.,40.,100.])
Iarray = 10**np.linspace(-5,0,num=100)

Ci = np.zeros([len(rarray),len(Qin_array)])
Ii = np.zeros([len(rarray),len(Qin_array)])
FLi = np.zeros([len(rarray),len(Qin_array)])

for qi,Qin in enumerate(Qin_array):
    for ri,r in enumerate(rarray):
        i = findSweet()

        worstC = -np.log10(cBoth()[i])
        worstGradient = Iarray[i]
        worstFlowNumber = flowNumber()[i]

        Ci[ri,qi] = worstC
        Ii[ri,qi] = worstGradient
        FLi[ri,qi] = worstFlowNumber

myLabels={"Title": { 0: r"$\bf{-\log (C_{\tau}/C_0)}$",
                     1: r"$\bf{I}$ (%)",
                     2: r"$\log(\mathcal{F}_L)$"},
          "Y": "Injection flow rate\n$\\bf{Q_{in}}$ (m³/d)",
          "X": "Setback distance\n$\\bf{r}$ (m)"}

threeHeatplots(data={"I":Ii.T,"C":Ci.T,"FL":FLi.T},\
               ylabel=rarray,xlabel=np.round(Qin_array*86400,decimals=2),myLabels=myLabels);
```

```python
K = 10**-2
H = 20
f = 10
C0 = 1.0
decayRate = 1.119E-5

Qin_array = np.array([0.24,1.0,10.0,100.])/86400.
rarray = np.array([5.,10.,40.,100.])
Iarray = 10**np.linspace(-5,0,num=100)

Ci = np.zeros([len(rarray),len(Qin_array)])
Ii = np.zeros([len(rarray),len(Qin_array)])
FLi = np.zeros([len(rarray),len(Qin_array)])

for qi,Qin in enumerate(Qin_array):
    for ri,r in enumerate(rarray):
        i = findSweet()

        worstC = -np.log10(cBoth()[i])
        worstGradient = Iarray[i]
        worstFlowNumber = flowNumber()[i]

        Ci[ri,qi] = worstC
        Ii[ri,qi] = worstGradient
        FLi[ri,qi] = worstFlowNumber

myLabels={"Title": { 0: r"$\bf{-\log (C_{\tau}/C_0)}$",
                     1: r"$\bf{I}$ (%)",
                     2: r"$\log(\mathcal{F}_L)$"},
          "Y": "Injection flow rate\n$\\bf{Q_{in}}$ (m³/d)",
          "X": "Setback distance\n$\\bf{r}$ (m)"}

threeHeatplots(data={"I":Ii.T,"C":Ci.T,"FL":FLi.T},\
               ylabel=rarray,xlabel=np.round(Qin_array*86400,decimals=2),myLabels=myLabels);
```

## PLOTRAN SIMULATION RESULTS

```python
minI = np.array([0.00046667, 0.0013    , 0.0048    , 0.012     , 0.00046667,
       0.0013    , 0.0048    , 0.012     , 0.00053333, 0.0013    ,
       0.0048    , 0.012     , 0.00056667, 0.0021    , 0.0053    ,
       0.012     ])

minC = np.array([2.2514572 , 2.62298917, 3.14213329, 3.51421485, 1.64182175,
       2.00913676, 2.52461269, 2.89537637, 0.74130696, 1.0754177 ,
       1.55071976, 1.90646243, 0.18705258, 0.39222131, 0.73428991,
       1.00387133])
```

```python
Qin_array = np.array([0.24,1.0,10.0,100.])/86400.
rarray = np.array([5.,10.,40.,100.])
Iarray = 10**np.linspace(-5,0,num=100)

Ci = np.zeros([len(rarray),len(Qin_array)])
Ii = np.zeros([len(rarray),len(Qin_array)])
FLi = np.zeros([len(rarray),len(Qin_array)])

i = 0
for qi,Qin in enumerate(Qin_array):
    for ri,r in enumerate(rarray):
        worstC = minC[i]
        worstGradient = minI[i]

        Ci[ri,qi] = worstC
        Ii[ri,qi] = worstGradient

        i += 1

myLabels={"Title": { 0: r"$\bf{-\log (C_{\tau}/C_0)}$",
                     1: r"$\bf{I}$ (%)",
                     2: r"$\log(\mathcal{F}_L)$"},
          "Y": "Injection flow rate\n$\\bf{Q_{in}}$ (m³/d)",
          "X": "Setback distance\n$\\bf{r}$ (m)"}

threeHeatplots(data={"I":Ii.T,"C":Ci.T,"FL":Ii.T},\
               ylabel=rarray,xlabel=np.round(Qin_array*86400,decimals=2),myLabels=myLabels);
```

```python

```


```
from tectosaur2.nb_config import setup

setup()
```

# Topography

## Summary

We're going to replicate the calculation from the previous section above except for a free surface with topography. The calculations will be identical except for the construction of the surface itself. As a result, there will be no narration. You can take a look through and cross reference the steps with the corresponding step in the previous section.

To construct the mesh, I'll specify a parametrized topography curve with a Gaussian hill shape near the origin. Remember that in the parametrized description of curves accepted by `refine_surfaces`, the parameter (normally $t$) varies from -1 to 1.

## Solving for displacement from a fault below topography

```
import warnings
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
from tectosaur2 import gauss_rule, refine_surfaces, integrate_term, pts_grid
from tectosaur2.laplace2d import double_layer, hypersingular
```

```

surf_half_L = 1000
t = sp.symbols("t")
x = -surf_half_L * t
y = sp.exp(-(t ** 2) * 500000) * sp.Rational(1.0) - sp.Rational(1.0)
sp.Tuple(x, y)
```

$\displaystyle \left( - 1000 t, \  -1 + e^{- 500000 t^{2}}\right)$

```
fault, topo = refine_surfaces(
    [
        (t, t * 0, 1 * ((t + 1) * -0.5)), # the fault
        (t, x, y) # the free surface
    ],
    gauss_rule(8),
    control_points=np.array([(0, 0, 10, 0.2)]),
)
print(
    f"The free surface mesh has {topo.n_panels} panels with a total of {topo.n_pts} points."
)
print(
    f"The fault mesh has {fault.n_panels} panels with a total of {fault.n_pts} points."
)
```

    The free surface mesh has 220 panels with a total of 1760 points.
    The fault mesh has 8 panels with a total of 64 points.

```
plt.plot(fault.panel_edges[:, 0], fault.panel_edges[:, 1], "r-o")
plt.plot(topo.panel_edges[:, 0], topo.panel_edges[:, 1], "k-o")
plt.xlim([-2.5, 2.5])
plt.ylim([-3.5, 1.5])
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.show()
```

```
%%time
# Specify singularities
singularities = np.array([
    [-surf_half_L,0],
    [surf_half_L,0],
    [0,0],
    [0,-1],
])

# Integrate!
(surf_disp_to_surf_disp, fault_slip_to_surf_disp), reports = integrate_term(
    double_layer,
    topo.pts,
    topo,
    fault,
    limit_direction=1.0,
    singularities=singularities,
    return_report=True
)

# Specify a constant unit slip along the fault.
slip = np.ones(fault.n_pts)

# rhs = B \Delta u
rhs = -fault_slip_to_surf_disp[:,0,:,0].dot(slip)

# lhs = I + A
lhs = np.eye(topo.n_pts) + surf_disp_to_surf_disp[:,0,:,0]

surf_disp = np.linalg.solve(lhs, rhs)
```

    CPU times: user 440 ms, sys: 97.4 ms, total: 537 ms
    Wall time: 207 ms

```
analytical = -np.arctan(-1 / topo.pts[:,0]) / np.pi
%matplotlib inline
plt.figure(figsize=(6, 6))
plt.plot(topo.pts[:, 0], surf_disp, "ro")
plt.plot(topo.pts[:, 0], analytical, "bo")
plt.xlabel("$x$")
plt.ylabel("$u_z$")
plt.title("Displacement")
plt.xlim([-4, 4])
plt.ylim([-0.6, 0.6])
plt.tight_layout()
plt.show()
```

## Interior evaluation of displacement and stress.

```
nobs = 200
zoomx = [-1.5, 1.5]
zoomy = [-2, 0]
xs = np.linspace(*zoomx, nobs)
ys = np.linspace(*zoomy, nobs)
obs_pts = pts_grid(xs, ys)
obsx = obs_pts[:, 0]
obsy = obs_pts[:, 1]
```

```
K = double_layer
(free_to_disp, fault_to_disp), report = integrate_term(
    double_layer,
    obs_pts,
    topo,
    fault,
    singularities=singularities,
    return_report=True,
)
interior_disp = -free_to_disp[:, 0, :, 0].dot(surf_disp) - fault_to_disp[:, 0, :, 0].dot(slip)
```

```
(free_to_stress, fault_to_stress), report = integrate_term(
    hypersingular,
    obs_pts,
    topo,
    fault,
    singularities=singularities,
    return_report=True,
)
interior_stress = -free_to_stress[:,:,:,0].dot(surf_disp) - fault_to_stress[:,:,:,0].dot(slip)
interior_sxz = interior_stress[:, 0]
interior_syz = interior_stress[:, 1]
```

    /Users/tbent/Dropbox/active/eq/tectosaur2/tectosaur2/integrate.py:201: UserWarning: Some integrals failed to converge during adaptive integration. This an indication of a problem in either the integration or the problem formulation.
      warnings.warn(
    /Users/tbent/Dropbox/active/eq/tectosaur2/tectosaur2/integrate.py:208: UserWarning: Some expanded integrals reached maximum expansion order. These integrals may be inaccurate.
      warnings.warn(

```
analytical_disp = (
    1.0 / (2 * np.pi) * (np.arctan((obsy + 1) / obsx) - np.arctan((obsy - 1) / obsx))
)
with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    disp_err = np.log10(np.abs(interior_disp - analytical_disp))

rp = obsx ** 2 + (obsy + 1) ** 2
ri = obsx ** 2 + (obsy - 1) ** 2
analytical_sxz = -(1.0 / (2 * np.pi)) * (((obsy + 1) / rp) - ((obsy - 1) / ri))
analytical_syz = (1.0 / (2 * np.pi)) * ((obsx / rp) - (obsx / ri))
sxz_err = np.log10(np.abs(interior_sxz - analytical_sxz))
syz_err = np.log10(np.abs(interior_syz - analytical_syz))
plt.figure(figsize=(12, 8))

plots = [
    ("interior_disp", "topo: $u_z$"),
    ("interior_sxz", "topo: $\sigma_{xz}$"),
    ("interior_syz", "topo: $\sigma_{yz}$"),
    ("analytical_disp", "flat: $u_z$"),
    ("analytical_sxz", "flat: $\sigma_{xz}$"),
    ("analytical_syz", "flat: $\sigma_{yz}$"),
]
for i, (k, title) in enumerate(plots):
    plt.subplot(2, 3, 1 + i)
    plt.title(title)
    v = locals()[k].reshape((nobs, nobs))
    v2d = v.reshape((nobs, nobs))
    levels = np.linspace(-0.5, 0.5, 11)
    cntf = plt.contourf(xs, ys, v2d, levels=levels, extend="both")
    plt.contour(
        xs,
        ys,
        v2d,
        colors="k",
        linestyles="-",
        linewidths=0.5,
        levels=levels,
        extend="both",
    )
    plt.colorbar(cntf)
    # plt.xlim([-0.01, 0.01])
    # plt.ylim([-0.02, 0.0])

plt.tight_layout()
plt.show()
```


```python
import numpy as np
import scipy as sp
from scipy import linalg
from scipy import optimize
from scipy import interpolate
import sympy as sm

%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

import pickle
import numpy as np

from scipy.stats import norm # normal distribution

%matplotlib inline
import matplotlib.pyplot as plt
import ipywidgets as widgets
```

# Model foundation
Consider a risk neutral company wanting to go public. The company can either choose a regular book building process or do an IPO Auction. If they perform the latter, the company will sell $Q$ perfectly divisible shares at an auction, with the objective to maximise the expected revenue.

There are $N>2$ risk averse (informed) competitive bidders (i.e. institutional investors as pension funds, asset managers, insurance companies, etc.). Risk aversion is modelled through constant absolute risk aversion (CARA) utility preferences with the common Arrow-Pratt risk aversion parameter $\rho \geq 0$, corresponding to an exponential utility function. We assume that bidders' goal is to maximise their utility (or as we will see later; expected utility).
%
\begin{align} \label{eq:CARA}
U_i(\pi_i)=-e^{-\rho \pi_i}
\end{align}

As with all stocks, the common value of the stock is given by the resale price in the secondary market, which theoretically should reflect the present value of all future cash flows. However, the after market price is unknown at the time of the bid, therefore we denote the intrinsic value of the asset by the random variable $v \in V$. The prior distribution of $V$ is common knowledge to the bidders, the corresponding \textit{cdf} is denoted $F(v)$ and the \textit{pdf} $f(v)$.

Each competitive bidder ($i=1, 2, \dots, N$) submits a bid schedule $x_i(p,s_i)$ dependent on the realisation of their private signal $s_i\in S$, where $S$ is a signal space with a possible infinite number of elements, and the price $p$. we will assume that the bidder's conditional expected value, the conditional mean ($E[v]=\bar{v}\geq 0$), and the variance ($Var[v]=\tau_v^{-1}$) are well defined.

# The Parametric Model
We go straight to the parametric model, as it is the interesting part code wise.

The common value of the asset is $v \sim N(\bar{v}, \tau_v^{-1})$. Where the variance of the asset $v$, $\tau_v^{-1}$, can be seen as valuation dispersion, i.e. $\tau_v^{-1}$ measures how precisely the general public can asses the value of the company. In this sense, more complex issuing companies may lead to a higher variance. The total supply of the asset is given by $Q$.

The parametric IPO auction consists of two stages

1 Each competitive bidder $i$ chooses $\eta_i$, independently from, and simultaneously with, his opponents.

2 After observing the realisation of their private signal $s_{\eta i}$, but not their opponents signals nor their choice of $\eta_j$ ($j \neq i$), each competitive bidder $i$ bid for the shares at the IPO auction.

In order to solve this parametric model, we postulate that there exist a unique symmetric equilibrium in linear strategies $x_i = \mu + \beta s_{\eta i} - \gamma p$, for which we determine the coefficients and show the strategy is optimal in equilibrium. This is a unique symmetric equilibrium in linear strategies where all bidders chose the signal precision $\eta$. This equilibrium is characterised by:

\begin{align} \label{eq:bidschedule}
x_i = \mu + \beta s_{\eta i} - \gamma p, \quad i=1, 2, \dots, N
\end{align}

After skipping alot of computation the equilibrium strategy  $\gamma$, $\mu$ and $\beta$, which are given as:

\begin{align} \label{eq:betaroot}
\rho \tau_{\eta \epsilon}^{-1} \beta^3 + \frac{N}{N-1} \beta^2 + \frac{\rho \tau_z^{-1}}{N-1}\beta - \frac{(N-2) \tau_{\eta \epsilon}\tau_z^{-1}}{(N-1)^2}=0
\end{align}

\begin{align}
\gamma &= \frac{\beta \tau_\eta - \phi \tau_{\eta \epsilon}(N-1) \gamma }{ \frac{\tau_{\eta \epsilon}}{ \left( \lambda \tau_\eta + \rho + \frac{\phi \tau_{\eta \epsilon} }{\beta} \right)} \left( \lambda \tau_\eta + \rho + \frac{\phi \tau_{\eta \epsilon} }{\beta} \right)}
\end{align}

\begin{align}
\mu &= \frac{\tau_v \beta \bar{v} + \phi \tau_{\eta \epsilon} (1-(N-1)\mu)}{\frac{\tau_{\eta \epsilon} }{\lambda \tau_\eta + \rho + \frac{\phi \tau_{\eta \epsilon} }{\beta}} \left( \lambda \tau_\eta + \rho + \frac{\phi \tau_{\eta \epsilon} }{\beta} \right)} \nonumber \\
\end{align}

We will isolate $\mu$ and $\gamma$ as a function of $\beta$:

```python
# First we define all needed symbols
N = sm.symbols('N') # Number of bidders
rho = sm.symbols('rho') # Risk averision (CARA)
vbar = sm.symbols('vbar') # Expected Value E[v]
tauv = sm.symbols('tauv') # inverse varianse of value
taue = sm.symbols('taue') # inverse varianse of signal
beta = sm.symbols('beta') # what we call the effect of the signal before we numerically solve the third degree equation
tauz = sm.symbols('tauz') # inverse varianse of z. z is in this project only included for computational reasons
Lambda = sm.symbols('Lambda')
gamma = sm.symbols('gamma')
mu = sm.symbols('mu')
phi = sm.symbols('phi')
x = sm.symbols('x') # Effect of the signal. It is described as beta above, but in the code we use x for convinense.
```

```python
# We begin by isolating gamma
tau = taue + tauv + tauz # Tau is the variance of the expected valuation
cogamma = sm.solve((beta*taue - phi*(N-1)*gamma)/((taue/(Lambda*tau+rho+(phi*taue)/beta))*(Lambda*tau+rho+(phi*taue)/beta))-gamma, gamma)
cogamma
```

    [beta*taue/(N*phi - phi + taue)]

Unfurtunatly we could not get the result of the paper which is

\begin{align}
\gamma = \beta \left(1+ \frac{\tau_v}{\tau_{\eta \epsilon}(1+ \phi (N-1))} \right)
\end{align}

And for some unknown reason our later results did not make sence if we used the above result.

```python
# We then isolate mu
comu = sm.solve((beta*tauv*vbar - phi*taue*(1-(N-1)*mu))/((taue/(Lambda*tau+rho+(phi*taue)/beta))*(Lambda*tau+rho+(phi*taue)/beta))-mu, mu)
comu
```

    [(beta*tauv*vbar - phi*taue)/(taue*(-N*phi + phi + 1))]

Again it is not quite the same as in the article which is:
\begin{align}
\mu &= (\gamma-\beta) \bar{v} + \frac{\phi}{1+(N-1)\phi} \label{eq:mu}
\end{align}
But solving the equations was more to see how the scipy function worked.

The last equation that will be needed but not explained is  $\phi$ which you just need to know is the price efficiency. Hence
\begin{align}
\phi &= \frac{(N-1)\beta^2}{(N-1)\beta^2 + \tau_{\eta \epsilon}\tau_z^{-1}} \in \left( 0, \frac{N-2}{2(N-1)} \right) \label{eq:phi}
\end{align}

Solving for the optimal bidding strategy we can analyse what, and how information and risk-aversion will affect the expected price

\begin{align}
E[p]&= \bar{v} - \frac{1-\phi}{N \gamma (1+(N-1)\phi)}
\end{align}

### The properties of the parametric model
In the optimal bidding strategy, $\gamma$ is the slope of each bidder's bid schedule. Thus, $\gamma$ is a measure for price elasticity in equilibrium. $\beta$ represents the bidders' responsiveness to their private signal. $\beta$ is positive at all times, indicating that bidders respond positively to their private signals. This fact arises from the affiliation between the signals and the true value. A strictly positive $\beta$ implies that the competitive bidder who values the good the highest receives the largest share.

# Solving the model numerically

### Newtons and Halleys method

#### Disclaimer This is taken from the lecture!!!

We consider solving non-linear equations on the form,

$$ f(x) = 0, x \in \mathbb{R} $$

This is called root-finding.

Derivative based methods
Newton methods: Assume you know the function value and derivatives at $x_0$.

A first order approximate value of the function at $x_1$ then is:

$$ f(x_1) \approx f(x_0) + f^{\prime}(x_0)(x_1-x_0)$$

implying

$$ f(x_1) = 0 \Leftrightarrow x_1 = x_0 - \frac{f(x_0)}{f^{\prime}(x_0)} $$

This is called Newtons method.

An alternative is Halleys method, which uses

$$ x_1 = x_0 - \frac{f(x_0)}{f^{\prime}(x_0)} \Big[ 1-\frac{f(x_0)}{f^{\prime}(x_0)}\frac{f^{\prime\prime}(x_0)}{2f^{\prime}(x_0)} \Big]^{-1} $$

making use of information from the second derivative.

Algorithm: find_root()

Choose tolerance $\epsilon > 0$, guess on $x_0$ and set $n = 0$.

Calculate $f(x_n)$, $f^{\prime}(x_n)$, and perhaps $f^{\prime\prime}(x_n)$.
If $|f(x_n)| < \epsilon$ stop.

Calculate $x_{n+1}$ using Newtons or Halleys formula (see above).

Set $n = n + 1$ and return to step 2.

```python
def find_root(x0,f,fp,fpp=None,method='newton',max_iter=1000,tol=1e-8,full_info=False):
    """ find root

    Args:

        x0 (float): initial value
        f (callable): function
        fp (callable): derivative
        fp (callable): second derivative
        method (str): newton or halley
        max_iter (int): maximum number of iterations
        tol (float): tolerance
        full_info (bool): controls information returned

    Returns:

        x (float/ndarray): root (if full_info, all x tried)
        i (int): number of iterations used
        fx (ndarray): function values used (if full_info)
        fpx (ndarray): derivative values used (if full_info)
        fppx (ndarray): second derivative values used (if full_info)

    """

    # initialize
    x = np.zeros(max_iter)
    fx = np.zeros(max_iter)
    fpx = np.zeros(max_iter)
    fppx = np.zeros(max_iter)
    x[0] = x0
    i = 0

    # iterate
    while True:

        # step 2: evaluate function and derivatives
        fx[i] = f(x[i])
        fpx[i] = fp(x[i])
        if method == 'halley':
            fppx[i] = fpp(x[i])

        # step 3: check convergence
        if abs(fx[i]) < tol or i >= max_iter:
            break

        # step 4: update x
        if method == 'newton':
            x[i+1] = x[i] - fx[i]/fpx[i]
        elif method == 'halley':
            a = fx[i]/fpx[i]
            b = a*fppx[i]/(2*fpx[i])
            x[i+1] = x[i] - a/(1-b)

        # step 5: increment counter
        i += 1

    # return
    if full_info:
        return x,i,fx,fpx,fppx
    else:
        return x[i],i
```

## Testing which method is best at approximating beta

```python
# define exogen paramters as a test

N = 50
rho = 50
vbar = 1
tauv = 5
taue = 8
tauz = 100
```

```python
# We first look at the Halley method
```

```python
def simulate_expected_p(tauz,taue,tauv,rho,vbar,N):
    f = lambda x: rho*taue**(-1)*x**3+N/(N-1)*x**2+rho*tauz**(-1)/(N-1)*x-((N-2)*taue*tauz**(-1))/((N-1)**2) # The function (remember we x instead of beta)
    fp = lambda x: 2*rho*taue**(-1)*x**2+N/(N-1)*x+rho*tauz**(-1)/(N-1) # First drivative
    fpp = lambda x: 6*rho*taue**(-1)*x+N/(N-1) # Second derivatie

    x,i = find_root(-5,f,fp,fpp,method='halley') # Using the "find root" algorithm
#    x,i = find_root(-5,f,fp,fpp,method='newton')

    phi = ((N-1)*x**2)/((N-1)*x**2+taue*tauz**(-1)) # When beta has been found we can insert it in phi which was price sensitivity

    gamma = x*(1+tauv*taue**(-1)/(1+(N-1)*phi)) # Phi and beta can be inserted into gamma

    expected_p = vbar-(1-phi)/(N*gamma*(1+(N-1)*phi)) # Which leads to us being able to compute expected price

    return expected_p

#defining loop that lets us look at expected price over N
expected_p_list = []
for N in range(7,50):

    expected_p_list.append(simulate_expected_p(tauz,taue,tauv,rho,vbar,N))
print(expected_p_list)
```

    [0.3952588048834892, 0.5231550828584473, 0.6134203714034683, 0.6796863553962017, 0.7298616333354655, 0.76881623679504, 0.7996946092420647, 0.8246058161319978, 0.8450092814271486, 0.861939567816909, 0.8761528091140007, 0.8882055123366049, 0.8985191064015454, 0.9074181899850148, 0.9151514726711859, 0.9219184550124406, 0.9278745117084513, 0.933146456439869, 0.9378360643302379, 0.9420281881465141, 0.9457914900499584, 0.9491835091182154, 0.9522526218367751, 0.955038575933651, 0.9575760443827032, 0.9598938834890098, 1.0016262488198961, 1.0013084500983698, 1.0011024332454055, 0.9674247700074815, 0.968960112808176, 0.9703832882922616, 1.0006531559851926, 0.972936584507568, 1.0005278323805777, 1.0004781249153234, 1.0004349325830781, 1.0003971304724364, 1.000363845792943, 1.0003343731375203, 0.9795940443628078, 0.9803301463397966, 0.981024815195463]

```python
# plotting figure
fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.plot(expected_p_list[7:50],'r--',label="Expected Price")
plt.legend()
plt.title("Expected price over N")
plt.xlabel('Number of participants')
plt.ylabel('price')
plt.show()
```

As seen in the figure above it is not smooth at all which means the "halleys" method is not a good way to approximate beta. Thus we try with the newton method.

```python
def simulate_expected_p(tauz,taue,tauv,rho,vbar,N):
    f = lambda x: rho*taue**(-1)*x**3+N/(N-1)*x**2+rho*tauz**(-1)/(N-1)*x-((N-2)*taue*tauz**(-1))/((N-1)**2) # The function (remember we x instead of beta)
    fp = lambda x: 2*rho*taue**(-1)*x**2+N/(N-1)*x+rho*tauz**(-1)/(N-1) # First drivative
    fpp = lambda x: 6*rho*taue**(-1)*x+N/(N-1) # Second derivatie

#   x,i = find_root(-5,f,fp,fpp,method='halley') # Using the "find root" algorithm
    x,i = find_root(-5,f,fp,fpp,method='newton')

    phi = ((N-1)*x**2)/((N-1)*x**2+taue*tauz**(-1)) # When beta has been found we can insert it in phi which was price sensitivity

    gamma = x*(1+tauv*taue**(-1)/(1+(N-1)*phi)) # Phi and beta can be inserted into gamma

    expected_p = vbar-(1-phi)/(N*gamma*(1+(N-1)*phi)) # Which leads to us being able to compute expected price

    return expected_p

#defining loop that lets us look at expected price over N
expected_p_list = []
for N in range(7,50):

    expected_p_list.append(simulate_expected_p(tauz,taue,tauv,rho,vbar,N))
print(expected_p_list)
```

    [0.395258674025628, 0.5231549566509033, 0.6134194700748861, 0.6796872216925881, 0.7298624852759854, 0.7688162407355085, 0.7996954365972444, 0.8246064472505937, 0.845008704791858, 0.8619401879792398, 0.876152717861291, 0.8882048843025689, 0.8985191165680196, 0.9074176432892418, 0.9151514616319281, 0.9219184843084098, 0.9278740725931013, 0.9331459578610027, 0.9378360282505352, 0.9420286719114582, 0.9457919326783959, 0.9491835003460157, 0.9522522384006388, 0.9550381525056313, 0.9575756635188839, 0.9598938659740973, 1.0016262514168346, 1.0013084343042948, 1.001102420569103, 1.000949905077947, 1.0008303458854892, 1.000733450237346, 1.0006531524087527, 1.0005855194214102, 1.0005278298570737, 1.0004781230647142, 1.000434930355825, 1.0003971308531865, 1.000363844393682, 1.0003343721766234, 1.000308149868454, 1.0002847185198993, 1.00026369759772]

```python
# plotting figure
fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.plot(expected_p_list[7:50],'r--',label="Expected Price")
plt.legend()
plt.title("Expected price over N")
plt.xlabel('Number of participants')
plt.ylabel('price')
plt.show()
```

This figure is much better and we are now ready to interpret how different parameters affect the expected price. We wil do that with an interactive figure.

```python
def interactive_figure(tauz,taue,tauv,rho,vbar,N):

    # a.
    expected_p_list = []
    for N in range(3,50):
        expected_p_list.append(simulate_expected_p(tauz,taue,tauv,rho,vbar,N))

    # b. figure
    fig = plt.figure()
    ax = fig.add_subplot(1,1,1)

    ax.plot(expected_p_list[3:50],'r--',label="Expected Price")
    plt.legend()
    plt.title("Expected price over N")
    plt.xlabel('N')
    plt.ylabel('price')
    plt.show()

```

# Conclusion
First as a note, the full price of the company is one. Also when you play around with the figure remember it is the inverse of the variance hence low values means high variance and vice versa.

First of you wil notice that the more who participate the higher the expected price will be, no matter the bidders information or riskaversion. Hence, the auctioner or the invetment bankers in this case whishes to promote the issued company as much as possible such that moste people participate. The reason for this mechanism is just basic demand and supply. However there are also other explanasions to this correlation, which is evolvs around bidshading. The fewer people participating them more power over price they have, and thus will not bid their true value.

If you start sliding the riskaversion you will see that the less riskavers people are the fewer is needed to reach an expected price of one. That is due to people not being "afraid" of winners curse meaning paying too much given the others bids.

When sliding the variance it makes the most sense if the riskaverse button is all the way to the right. Here you will see that more accurate information leads to higher expected price, thus the auctioneer would want to provide bidders with as much information as possible.

```python
style = {'description_width': 'initial'} # this is used to make place for the longer descrpitions at the slidebar
widgets.interact(interactive_figure,
    N=widgets.fixed(N),
    tauz=widgets.fixed(tauz),
    vbar=widgets.fixed(vbar),
    rho=widgets.FloatSlider(description="$Risk aversion$", min=30, max=100, step=2, value=1,style=style),
    taue=widgets.FloatSlider(description="$Variance of signal$", min=8, max=25, step=1, value=1, style=style),
    tauv=widgets.FloatSlider(description="$Variance of value$", min=5, max=100, step=1, value=1, style=style),
);
```

    interactive(children=(FloatSlider(value=8.0, description='$Variance of signal$', max=25.0, min=8.0, step=1.0, …


# Berechnung der Durchschnittlichen Spiellänge mittels Markov-Ketten
### Import der benötigten Bibliotheken

## Berechnen der Werte

```python
import matplotlib.pyplot as plt
import numpy as np
from helpers import setup_grid

import sympy as sy
from datetime import datetime

from matplotlib.colors import LogNorm,SymLogNorm
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.style as mplstyle

%config InlineBackend.figure_formats = ['svg']
# mplstyle.use('fast')
# mplstyle.use('default')
# mpl.rcParams['figure.figsize'] = [20, 10]

colors = ["lightgreen", "yellow", "red"]
cmap = LinearSegmentedColormap.from_list("mycmap", colors)
mplstyle.use("fast")
mplstyle.use("default")

```

### Definition der Wahrscheinlichkeiten der einzelnen Würfelwerte

```python
VierfachMünzwurf = {
    "dice":1/16,
    "diceI":1/4,
    "diceII":3/8,
    "diceIII":1/4,
    "diceIV":1/16
}

VierfachMünzwurf_exakt = {
    "dice": sy.nsimplify(1/16, tolerance=0.001, rational=True),
    "diceI": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceII": sy.nsimplify(3/8, tolerance=0.001, rational=True),
    "diceIII": sy.nsimplify(1/4, tolerance=0.001, rational=True),
    "diceIV": sy.nsimplify(1/16, tolerance=0.001, rational=True)
}

```

### Definition der Übergangsmatrizen

```python

def RundenMatrix(dice, diceI, diceII, diceIII, diceIV):
    return np.asarray(
        [[dice,  diceI,  diceII,  diceIII,  diceIV * dice,  diceIV * diceI,  diceIV * diceII,  diceIV * diceIII,  diceIV * diceIV * dice,  diceIV * diceIV * diceI,  diceIV * diceIV * diceII,  diceIV * diceIV * diceIII,  diceIV * diceIV * diceIV, 0, 0, 0],
         [0,  dice,  diceI,  diceII,  diceIII * dice,  diceIV + diceIII * diceI,  diceIII * diceII,  diceIII * diceIII,  diceIII * diceIV *
            dice,  diceIII * diceIV * diceI,  diceIII * diceIV * diceII,  diceIII * diceIV * diceIII,  diceIII * diceIV * diceIV, 0, 0, 0],
            [0, 0,  dice,  diceI,  diceII * dice,  diceIII + diceII * diceI,  diceIV + diceII * diceII,  diceII * diceIII,  diceII * diceIV *
                dice,  diceII * diceIV * diceI,  diceII * diceIV * diceII,  diceII * diceIV * diceIII,  diceII * diceIV * diceIV, 0, 0, 0],
            [0, 0, 0,  dice,  diceI * dice,  diceII + diceI * diceI,  diceIII + diceI * diceII,  diceIV + diceI * diceIII,  diceI *
                diceIV * dice,  diceI * diceIV * diceI,  diceI * diceIV * diceII,  diceI * diceIV * diceIII,  diceI * diceIV * diceIV, 0, 0, 0],
            [0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII,  diceIV * dice,  diceIV *
                diceI,  diceIV * diceII,  diceIV * diceIII,  diceIV * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII * dice,  diceIV + diceIII *
                diceI,  diceIII * diceII,  diceIII * diceIII,  diceIII * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII * dice,  diceIII + diceII * diceI,
                diceIV + diceII * diceII,  diceII * diceIII,  diceII * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0,  dice,  diceI * dice,  diceII + diceI * diceI, diceIII +
                diceI * diceII,  diceIV + diceI * diceIII,  diceI * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,
                diceII,  diceIII,  diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,
                diceII,  diceIII,  diceIV, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII,
                diceIV*(dice + diceII + diceIII + diceIV),  diceIV * diceI],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII *
                (dice + diceII + diceIII + diceIV),  diceIV + (diceIII * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice + diceIV,  diceI,  diceII *
                (dice + diceII + diceIII + diceIV),  diceIII + (diceII * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice + diceIII + diceIV,
                diceI*(dice + diceII + diceIII + diceIV),  diceII + (diceI * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                dice + diceII + diceIII + diceIV,  diceI],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]])

def SchritteMatrix(dice, diceI, diceII, diceIII, diceIV):
    return np.asarray([
        [dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII, diceIV],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice +
            diceIV,  diceI,  diceII,  diceIII],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice+diceIII + diceIV,  diceI,  diceII],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice+diceII + diceIII + diceIV,  diceI],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice + diceI + diceII + diceIII + diceIV],
    ])

def RundenMatrix_exakt(dice, diceI, diceII, diceIII, diceIV):
    return sy.Matrix(
        [[dice,  diceI,  diceII,  diceIII,  diceIV * dice,  diceIV * diceI,  diceIV * diceII,  diceIV * diceIII,  diceIV * diceIV * dice,  diceIV * diceIV * diceI,  diceIV * diceIV * diceII,  diceIV * diceIV * diceIII,  diceIV * diceIV * diceIV, 0, 0, 0],
         [0,  dice,  diceI,  diceII,  diceIII * dice,  diceIV + diceIII * diceI,  diceIII * diceII,  diceIII * diceIII,  diceIII * diceIV *
            dice,  diceIII * diceIV * diceI,  diceIII * diceIV * diceII,  diceIII * diceIV * diceIII,  diceIII * diceIV * diceIV, 0, 0, 0],
            [0, 0,  dice,  diceI,  diceII * dice,  diceIII + diceII * diceI,  diceIV + diceII * diceII,  diceII * diceIII,  diceII * diceIV *
                dice,  diceII * diceIV * diceI,  diceII * diceIV * diceII,  diceII * diceIV * diceIII,  diceII * diceIV * diceIV, 0, 0, 0],
            [0, 0, 0,  dice,  diceI * dice,  diceII + diceI * diceI,  diceIII + diceI * diceII,  diceIV + diceI * diceIII,  diceI *
                diceIV * dice,  diceI * diceIV * diceI,  diceI * diceIV * diceII,  diceI * diceIV * diceIII,  diceI * diceIV * diceIV, 0, 0, 0],
            [0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII,  diceIV * dice,  diceIV *
                diceI,  diceIV * diceII,  diceIV * diceIII,  diceIV * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII * dice,  diceIV + diceIII *
                diceI,  diceIII * diceII,  diceIII * diceIII,  diceIII * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII * dice,  diceIII + diceII * diceI,
                diceIV + diceII * diceII,  diceII * diceIII,  diceII * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0,  dice,  diceI * dice,  diceII + diceI * diceI, diceIII +
                diceI * diceII,  diceIV + diceI * diceIII,  diceI * diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,
                diceII,  diceIII,  diceIV, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,
                diceII,  diceIII,  diceIV, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII,
                diceIV*(dice + diceII + diceIII + diceIV),  diceIV * diceI],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice,  diceI,  diceII,  diceIII *
                (dice + diceII + diceIII + diceIV),  diceIV + (diceIII * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice + diceIV,  diceI,  diceII *
                (dice + diceII + diceIII + diceIV),  diceIII + (diceII * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  dice + diceIII + diceIV,
                diceI*(dice + diceII + diceIII + diceIV),  diceII + (diceI * diceI)],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                dice + diceII + diceIII + diceIV,  diceI],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]])

def SchritteMatrix_exakt(dice, diceI, diceII, diceIII, diceIV):
    return sy.Matrix([
        [dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, dice, diceI,  diceII,  diceIII,  diceIV, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII,  diceIII, diceIV, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII,  diceIV, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice,  diceI,  diceII, diceIII, diceIV],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, dice +
            diceIV,  diceI,  diceII,  diceIII],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice+diceIII + diceIV,  diceI,  diceII],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice+diceII + diceIII + diceIV,  diceI],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            dice + diceI + diceII + diceIII + diceIV],
    ])

```

### Festlegen welcher Würfel verwendet wird

```python
M_S = SchritteMatrix(**VierfachMünzwurf)
M_R = RundenMatrix(**VierfachMünzwurf)
M_S_e = SchritteMatrix_exakt(**VierfachMünzwurf_exakt)
M_R_e = RundenMatrix_exakt(**VierfachMünzwurf_exakt)

```

### Löschen der letzten Spalte und Zeile

```python
M_S_mod = np.delete(np.delete(M_S, -1, 0), -1, 1)
M_R_mod = np.delete(np.delete(M_R, -1, 0), -1, 1)
M_S_e.col_del(-1)
M_R_e.col_del(-1)
M_S_e.row_del(-1)
M_R_e.row_del(-1)

```

### Definition der 15x15 - Einheitsmatrix

```python
I = np.eye(15)
I_e = sy.eye(15)

```

### Berechnung der Ergebnismatrizen

```python
E_S = np.linalg.inv(I-M_S_mod)
E_R = np.linalg.inv(I-M_R_mod)

E_S_e = (I_e-M_S_e)**-1
E_R_e = (I_e-M_R_e)**-1

```

### Berechnng der jeweils ersten Zeilensumme

```python
ZS_S= sum(E_S[0])
ZS_R = sum(E_R[0])

v=sy.Matrix([[1]+[0]*14])
ZS_S_e= sum(v*E_S_e)
ZS_R_e= sum(v*E_R_e)
```

## Vergleich mit den exakt berechneten Werten
es fällt auf, dass einer der beiden Werte Abweicht

```python
print("numpy Berechnung: ",ZS_S)
print("exakte Berechnung: ", ZS_S_e)
print("exakte Berechnung als float: ", float(ZS_S_e))
A_S = sy.nsimplify(ZS_S, tolerance=1e-15, rational=True)-ZS_S_e
print("Abweichung: ", A_S)
print("Abweichung als float: ", float(A_S))

```

    numpy Berechnung:  9.86784676744529
    exakte Berechnung:  1280316980941868/129746337890625
    exakte Berechnung als float:  9.867846767445288
    Abweichung:  58605836370217/36520347436056576000000000000
    Abweichung als float:  1.6047447651704265e-15

```python
print("numpy Berechnung: ", ZS_R)
print("exakte Berechnung: ", ZS_R_e)
print("exakte Berechnung auf 50 Stellen: ", sy.N(ZS_R_e,50))
# print("exakte Berechnung latex: ", sy.latex(ZS_R_e))
A_R = sy.nsimplify(ZS_R,full=True)-ZS_R_e
print("Abweichung: ", A_R)
print("Abweichung auf 50 Stellen: ", sy.N(A_R,50))
# print("Abweichung latex: ", sy.latex(A_R))

```

    numpy Berechnung:  8.391927856222578
    exakte Berechnung:  1088821907187203/129746337890625
    exakte Berechnung auf 50 Stellen:  8.3919278562225787773242937597964778780711311321482
    Abweichung:  32489/26572050000000000000
    Abweichung auf 50 Stellen:  0.0000000000000012226757062402035221219288688678517464779721549523

```python

```

Definieren wir nun zwei verschiedene Methoden die jeweils eine Liste aus Vektoren ausgeben.
Aus dem Vektor an Stelle n kann man ablesen mit welcher Wahrscheinlichkeit der Spielstein sich auf einem bestimmten Spielfeld befindet.

```python
def step_by_step(M, RundenAnzahl):
    v = np.zeros(16, np.longdouble)
    v[0] = 1
    output = [v]
    for i in range(RundenAnzahl):
        output.append(np.matmul(output[i], M))
    return output

def Matrix_Potenz(M, RundenAnzahl):
    v = np.zeros(16, np.longdouble)
    v[0] = 1
    output = []
    for i in range(RundenAnzahl+1):
        output.append(np.matmul(v, np.linalg.matrix_power(M, i)))
    return output

def exakt_step_by_step(M, RundenAnzahl):
    v = sy.Matrix([1]+[0]*15).T
    output = [v]
    for i in range(RundenAnzahl):
        output.append(output[i]*(M))
    return output

def exakt_Matrix_Potenz(M, RundenAnzahl):
    v = sy.Matrix([1]+[0]*15).T
    output = []
    for i in range(RundenAnzahl):
        output.append(v*(M**i))
    return output

```

Lassen wir nun mit diesen Methoden jeweils die ersten 50 Runden berechnen

```python
runden = 50
d0 = datetime.now()
datenreihe_sbs = step_by_step(M_R,runden)
d1 = datetime.now()
datenreihe_mp = Matrix_Potenz(M_R,runden)
d2 = datetime.now()
datenreihe_exakt = exakt_step_by_step(
    RundenMatrix_exakt(**VierfachMünzwurf_exakt), runden)
d3 = datetime.now()
datenreihe_exakt_mp = exakt_Matrix_Potenz(
    RundenMatrix_exakt(**VierfachMünzwurf_exakt), runden)
d4 = datetime.now()

print("Rechenzeit step_by_step: ",d1-d0)
print("Rechenzeit mit_Matrix_Potenz: ",d2-d1)
print("Rechenzeit exakt_step_by_step: ", d3-d2)
print("Rechenzeit exakt_mit_Matrix_Potenz: ", d4-d3)

```

    Rechenzeit step_by_step:  0:00:00.001000
    Rechenzeit mit_Matrix_Potenz:  0:00:00.001000
    Rechenzeit exakt_step_by_step:  0:00:00.021004
    Rechenzeit exakt_mit_Matrix_Potenz:  0:00:01.304295

## Plotten der Werte

Plotten wir die drei berechneten Wertelisten, fällt erst mal nicht all zu viel auf...

```python
fig_heatmap, ax = plt.subplots(3, 1, figsize=[10,8], layout="constrained")
for i, data in enumerate([datenreihe_sbs, datenreihe_mp, np.array(datenreihe_exakt).astype(np.float64).reshape((51, 16))]):
    data = np.array(data).T
    im0 = ax[i].imshow(data, origin="lower", cmap=plt.get_cmap(
        "plasma"), interpolation='none', norm=LogNorm(vmin=1e-35, vmax=1,clip=True))

fig_heatmap.colorbar(im0, ax=ax, pad=.01, extend="min", label="Wahrscheinlichkeit,dass ein Stein auf dem Feld ist")

for i, name in enumerate(["Step by Step", "Matrix Potenz","exakt"]):
    ax[i].set_title(name)
    ax[i].set_yticks([0]+list(range(1, 15))+[15],
                     ["Start"]+list(range(1, 15))+["Ende"])
    ax[i].set_xlabel("Runden")
    ax[i].set_ylabel("Spielfeld")
    ax[i].set_ylim(-.5, 15.5)
    setup_grid(ax[i],gridoff=True)

```

Berechnung der Differenzen

```python
datenreihe_diff_sbs = np.asarray([np.asarray(sy.Matrix(datenreihe_sbs[i]).T - datenreihe_exakt[i])[0]
                                 for i in range(len(datenreihe_exakt))], dtype=np.float64)
datenreihe_diff_mp = np.asarray([np.asarray(sy.Matrix(datenreihe_mp[i]).T - datenreihe_exakt[i])[0]
                                 for i in range(len(datenreihe_exakt))], dtype=np.float64)

```

Plotten wir nun die Differenzen ist deutlich zu sehen wie sich bereits in Runde 13 eine Ungenauigkeit einschleicht

```python
fig_diff, ax = plt.subplots(2, 1, figsize=[10, 6], layout="constrained")

for i, data in enumerate([datenreihe_diff_sbs, datenreihe_diff_mp]):
    data = np.array(data).T
    im1 = ax[i].imshow(data, origin="lower", interpolation='none', cmap=plt.get_cmap(
        "RdBu_r"), norm=SymLogNorm(linthresh=1e-35, linscale=10e-2,
                                   vmin=-1.0, vmax=1.0, base=10))

tick_locations = ([-(10**(-x*5)) for x in range(7)]
                  + [0.0]
                  + [(10**(-x*5)) for x in range(6,-1,-1)])
fig_diff.colorbar(im1, ax=ax, pad=.01, extend="both",
             format="%.1e", label="Berechnungsfehler", ticks=tick_locations)

for i, name in enumerate(["Differenz: Step by Step - exakt", "Differenz: Matrix Potenz - exakt"]):
    ax[i].set_title(name)
    ax[i].set_yticks([0]+list(range(1, 15))+[15],
                     ["Start"]+list(range(1, 15))+["Ende"])
    ax[i].set_xlabel("Runden")
    ax[i].set_ylabel("Spielfeld")
    ax[i].set_ylim(-.5, 15.5)

    setup_grid(ax[i], gridoff=True)

```

```python
runden = 500
runden = 5
d0 = datetime.now()
datenreihe_sbs = step_by_step(M_R, runden)
d1 = datetime.now()
datenreihe_mp = Matrix_Potenz(M_R, runden)
d2 = datetime.now()
datenreihe_exakt = exakt_step_by_step(
    RundenMatrix_exakt(**VierfachMünzwurf_exakt), runden)
d3 = datetime.now()
datenreihe_exakt_mp = exakt_Matrix_Potenz(
    RundenMatrix_exakt(**VierfachMünzwurf_exakt), runden)
d4 = datetime.now()

print("Rechenzeit step by step: ", d1-d0)
print("Rechenzeit Matrix Potenz: ", d2-d1)
print("Rechenzeit exakt: ", d3-d2)
print("Rechenzeit exakt Matrix Potenz: ", d4-d3)

```

    Rechenzeit step by step:  0:00:00
    Rechenzeit Matrix Potenz:  0:00:00
    Rechenzeit exakt:  0:00:00.003000
    Rechenzeit exakt Matrix Potenz:  0:00:00.030008

Die Erklärung für das Problem

```python
np.finfo(float)

```

    finfo(resolution=1e-15, min=-1.7976931348623157e+308, max=1.7976931348623157e+308, dtype=float64)

Speichern der Grafiken

```python
fig_heatmap.savefig("../../tex/game_ur_ba_thesis/img/Grafiken/Wahrscheinlichkeiten50Runden.png", dpi=300, bbox_inches='tight',)
fig_diff.savefig( "../../tex/game_ur_ba_thesis/img/Grafiken/BerechnungsfehlerGraph.png", dpi=300,bbox_inches='tight',)
```