#!/usr/bin/env python
# coding: utf-8

#

#
ENGR 1330 Computational Thinking with Data Science

#

#
# Copyright © 2021 Farhang Forghanparast
#
# Last GitHub Commit Date:
#
# # 17: Descriptive Statistics
# - Central Tendency
# - Dispersion
# - Skewness
# - Kurtosis (Pointyness)

# ## Descriptive Statistics with Python
# ![20x20](https://blog-c7ff.kxcdn.com/blog/wp-content/uploads/2016/12/Cover_descriptive-statistics_BlogImage_Final.png)
#

# A fundamental part of working with data is _**describing**_ it. Descriptive statistics help simplify and summarize large amounts of data in a sensible manner. In this lecture, we will discuss __descriptive statistics__ and cover a variety of methods for __summarizing, describing, and representing__ datasets in Python. The contents of this notebook are inspired by various online resources including the following links:

#
# - "Descriptive statistics with Python-NumPy"
# by __*Rashmi Jain*__, available @ https://www.hackerearth.com/blog/developers/descriptive-statistics-python-numpy/.
#
# - "Python Statistics Fundamentals: How to Describe Your Data"
# by __*Mirko Stojiljković *__, available @ https://realpython.com/python-statistics/.
#
# - "A Quick Guide on Descriptive Statistics using Pandas and Seaborn"
# by __*Bee Guan Teo*__, available @ https://towardsdatascience.com/a-quick-guide-on-descriptive-statistics-using-pandas-and-seaborn-2aadc7395f32.
#
# - "Tutorial: Basic Statistics in Python — Descriptive Statistics"
# , available @ https://www.dataquest.io/blog/basic-statistics-with-python-descriptive-statistics/.
#
#
#
# We will use the [http://54.243.252.9/engr-1330-webroot/4-Databases/HighestGrossingMovies.csv](http://54.243.252.9/engr-1330-webroot/4-Databases/HighestGrossingMovies.csv) dataset as an illustrative example. Let's have a look at it first.

# In[1]:

#Import the necessary external packages
import numpy as np
import pandas as pd
import requests

# In[2]:

# put script here to download file from server
remote_url="http://54.243.252.9/engr-1330-webroot/4-Databases/HighestGrossingMovies.csv"  # set the url
rget = requests.get(remote_url, allow_redirects=True)  # get the remote resource, follow imbedded links
open('HighestGrossingMovies.csv','wb').write(rget.content) # extract from the remote the contents, assign to a local file same name
#

Movies = pd.read_csv("HighestGrossingMovies.csv")         #Dataset of the Top10 highest-grossing films as of 2019 (adjusted for inflation)
                                                          #5 columns (Movie, Director, Year, Budget, Gross) and 10 rows
Movies

# Here is an overall look at some but not all of  measures we will be discussing today:
# ![](https://slideplayer.com/slide/2346765/8/images/2/Summary+Measures+Describing+Data+Numerically+Central+Tendency.jpg)
#

# ### Measures of Central Tendency

# Centrality measures give us an estimate of the center of a distribution and a sense of a typical value we would expect to see.

# The three major measures of center include __*the mean, median, and mode*__.

# #### Mean

# Mean aka arithmetic mean aka average is the sum of all the values, divided by the number of values. Mean represents the typical value that acts as a yardstick for all observations.
# ![](https://x-engineer.org/wp-content/uploads/2016/10/Arithmetic-mean-formula-244x142.jpg?a43cfa&a43cfa)
#
# Let's calculate the average budget of the Top10 highest-grossing films.

# In[3]:

Budget = Movies['Budget_million$']
Budget

# We can use primitive python to calculate the mean of set of numbers:

# In[4]:

# Create a list of all the numbers:
budget = [3.9,237,200,11,356,8.2,10.5,13,11,306]
mean1 = sum(budget) / len(budget)
print("The average budget of the Top10 highest-grossing films is ",mean1,"million USD")

# We can also utilize a variety of external libraries. (You may find some of them familiar!)

# In[5]:

# The usual suspects!
import numpy as np
import pandas as pd
# Also, these two libraries offer useful functions for descriptive statistics
import statistics
import scipy.stats

# In[6]:

# Read the column of interest from the Movies dataframe
Budget = Movies['Budget_million$']
# Use the mean function from the Pandas library
mean2 = Budget.mean()
print("The average budget of the Top10 highest-grossing films is ",mean2,"million USD")

# In[7]:

# Read the column of interest from the Movies dataframe
Budget = Movies['Budget_million$']
# Use the mean function from the Numpy library
mean3 = np.mean(Budget)
print("The average budget of the Top10 highest-grossing films is ",mean3,"million USD")

# In[8]:

# Read the column of interest from the Movies dataframe
Budget = Movies['Budget_million$']
# Use the mean function from the statistics library
mean4 = statistics.mean(Budget)
print("The average budget of the Top10 highest-grossing films is ",mean4,"million USD")

# #### Harmonic Mean

# The harmonic mean is the reciprocal of the mean of the reciprocals of all items in the dataset.
#
# ![](https://www.onlinemath4all.com/images/harmonicmean1.png)
#
# Let's calculate the harmonic mean for the same set of numbers:

# In[9]:

# Primitive Python:
budget = [3.9,237,200,11,356,8.2,10.5,13,11,306]
hmean1 = len(budget) / sum(1 / item for item in budget)
hmean1 = round(hmean1,2)
print("The harmonic mean of the budget of the Top10 highest-grossing films is ",hmean1,"million USD")

# via the Statistics library:
Budget = Movies['Budget_million$']
hmean2 = statistics.harmonic_mean(Budget)
hmean2 = round(hmean2,2)
print("The harmonic mean of the budget of the Top10 highest-grossing films is ",hmean2,"million USD")

# via the scipy.stats library:
Budget = Movies['Budget_million$']
hmean3 = scipy.stats.hmean(Budget)
hmean3 = round(hmean3,2)
print("The harmonic mean of the budget of the Top10 highest-grossing films is ",hmean3,"million USD")

# #### Geometric Mean

# The geometric mean is the 𝑛-th root of the product of all 𝑛 elements 𝑥ᵢ in a dataset.
#
# ![](https://ncalculators.com/images/formulas/geometric-mean.jpg)
#
# Let's calculate the geometric mean for the same set of numbers:

# In[10]:

# Primitive Python: -it is getting more lengthy and labour-intensive
budget = [3.9,237,200,11,356,8.2,10.5,13,11,306]

gmean1 = 1
for item in budget:
     gmean1 *= item

gmean1 **= 1 / len(budget)
gmean1 = round(gmean1,2)
print("The geometric mean of the budget of the Top10 highest-grossing films is ",gmean1,"million USD")

# via the Statistics library:
Budget = Movies['Budget_million$']
gmean2 = statistics.geometric_mean(Budget)
gmean2 = round(gmean2,2)

print("The geometric mean of the budget of the Top10 highest-grossing films is ",gmean2,"million USD")

# via the scipy.stats library:
Budget = Movies['Budget_million$']
gmean3 = scipy.stats.gmean(Budget)
gmean3 = round(gmean3,2)

print("The geometric mean of the budget of the Top10 highest-grossing films is ",gmean3,"million USD")

# #### Arithmetic or Geometric or Harmonic?- How to be Mean!
# ![](https://pbs.twimg.com/media/Dm2BdG2V4AE36xP.jpg)
#

# - If values have the same units: Use the arithmetic mean.

# - If values have differing units: Use the geometric mean. | Also, commonly used for growth rates, like population growth or interest rates.

# - If values are rates: Use the harmonic mean.

#
# __If you are interested in knowing more about these 3 and their differences, you may find these interesting:__
# - "Arithmetic, Geometric, and Harmonic Means for Machine Learning Arithmetic, Geometric, and Harmonic Means for Machine Learning"
# by __*Jason Brownlee*__, available @ https://machinelearningmastery.com/arithmetic-geometric-and-harmonic-means-for-machine-learning/#:~:text=The%20arithmetic%20mean%20is%20appropriate,with%20different%20measures%2C%20called%20rates.
#
# - "On Average, You’re Using the Wrong Average: Geometric & Harmonic Means in Data Analysis"
# by __*Daniel McNichol*__, available @ https://towardsdatascience.com/on-average-youre-using-the-wrong-average-geometric-harmonic-means-in-data-analysis-2a703e21ea0

# #### Median

# Median is the middle element of a sorted dataset. The value where the upper half of the data lies above it and lower half lies below it. In other words, it is the middle value of a data set. To calculate the median, arrange the data points in the increasing (or decreasing) order and the middle value is the median. If the number of elements 𝑛 of the dataset is odd, then the median is the value at the middle position: 0.5(𝑛 + 1). If 𝑛 is even, then the median is the arithmetic mean of the two values in the middle, that is, the items at the positions 0.5𝑛 and 0.5𝑛 + 1.
# Let's find the median of the gross of the Top10 highest-grossing films:

# In[11]:

Gross = Movies['Gross_million$']
Gross

# We can use primitive python to calculate the median of a set of numbers:

# In[12]:

# Create a list of all the numbers:
gross = [3706,3257,3081,3043,2798,2549,2489,2356,2233,2202]
n = len(gross)
if n % 2:
     median1 = sorted(gross)[round(0.5*(n-1))]
else:
     gross_ord, index = sorted(gross), round(0.5 * n)
     median1 = 0.5 * (gross_ord[index-1] + gross_ord[index])

print("The median of gross of the Top10 highest-grossing films is ",median1,"million USD")

# We can use also use external libraries:

# In[13]:

#via the Pandas library:
Gross = Movies['Gross_million$']
median2 = Gross.median()
print("The median of gross of the Top10 highest-grossing films is ",median2,"million USD")

#via the Numpy library:
Gross = Movies['Gross_million$']
median3 = np.median(Gross)
print("The median of gross of the Top10 highest-grossing films is ",median3,"million USD")

#via the Statistics library:
Gross = Movies['Gross_million$']
median4 = statistics.median(Gross)
print("The median of gross of the Top10 highest-grossing films is ",median4,"million USD")
#2 more functions from the same library- For even number of cases:
print("low median :",statistics.median_low(Gross))
print("high median :",statistics.median_high(Gross))

# The main difference between the behavior of the mean and median is related to dataset outliers or extremes. The mean is heavily affected by outliers, but the median only depends on outliers either slightly or not at all. You can compare the mean and median as one way to detect outliers and asymmetry in your data. Whether the mean value or the median value is more useful to you depends on the context of your particular problem. The mean is a better choice when there are no extreme values that can affect it. It is a better summary because the information from every observation is included rather than median, which is just the middle value. However, in the presence of outliers, median is considered a better alternative. Check this out:

# In[14]:

newgross = [99999,3257,3081,3043,2798,2549,2489,2356,2233,2202] #We have replaced 3706 with 99999- an extremely high number (an outlier)
newmean = np.mean(newgross)
newmedian = np.median(newgross)
print(newmean)   #A huge change from the previous value (115.66) - Mean is very sensitive to outliers and extreme values
print(newmedian) #No Change- the median only depends on outliers either slightly or not at all.

# To read more about the differences of mean and median, check these out:
# - "Stuck in the middle – mean vs. median"
# , available @ https://www.clinfo.eu/mean-median/
#
# - "Mean vs Median: When to Use Which Measure?"
# , available @ https://www.datascienceblog.net/post/basic-statistics/mean_vs_median/
#
# - "Mean vs. Median"
# by __*AnswerMiner*__, available @ https://www.answerminer.com/blog/mean-vs-median

# #### Mode

# The value that occurs the most number of times in our data set. Closely tied to the concept of frequency, mode provides information on the most recurrent elements in a dataset. When the mode is not unique, we say that the data set is bimodal, while a data set with more than two modes is multimodal. Let's find the mode in the gross of the Top10 highest-grossing films:

# In[15]:

# In primitive Python:
# Create a list of all the numbers:
gross = [3706,3257,3081,3043,2798,2549,2489,2356,2233,2202]
mode1 = max((gross.count(item), item) for item in gross)[1]
print(mode1) #Since each item is repeated only once, only the first element is printed- This is a multimodal set.

#via the Pandas library:
Gross = Movies['Gross_million$']
mode2 = Gross.mode()
print(mode2) #Returns all modal values- This is a multimodal set.

#via the Statistics library:
Gross = Movies['Gross_million$']
mode3 = statistics.mode(Gross)
print(mode3) #Return a single value
mode4 = statistics.multimode(Gross)
print(mode4) #Returns a list of all modes

#via the scipy.stats library:
Gross = Movies['Gross_million$']
mode5 = scipy.stats.mode(Gross)
print(mode5) #Returns the object with the modal value and the number of times it occurs- If multimodal: only the smallest value

# Mode is not useful when our distribution is flat; i.e., the frequencies of all groups are similar. Mode makes sense when we do not have a numeric-valued data set which is required in case of the mean and the median. For instance:

# In[16]:

Director = Movies['Director']
# via statistics:
mode6 = statistics.mode(Director)
print(mode6) #"James Cameron" with two films (x2 repeats) is the mode
# via pandas:
mode7 = Director.mode()
print(mode7) #"James Cameron" with two films (x2 repeats) is the mode

#

# ### Measures of Dispersion

# Measures of dispersion are values that describe how the data varies. It gives us a sense of how much the data tends to diverge from the typical value. Aka measures of variability, they quantify the spread of data points.The major measures of dispersion include __*range, percentiles, inter-quentile range, variance, standard deviation, skeness and kurtosis*__.

# #### Range

# The range gives a quick sense of the spread of the distribution to those who require only a rough indication of the data. There are some disadvantages of using the range as a measure of spread. One being it does not give any information of the data in between maximum and minimum. Also, the range is very sensitive to extreme values. Let's calculate the range for the budget of the Top10 highest-grossing films:

# In[17]:

# Primitive Python:
budget = [3.9,237,200,11,356,8.2,10.5,13,11,306]
range1 = max(budget)-min(budget)
print("The range of the budget of the Top10 highest-grossing films is ",range1,"million USD")

# via the Statistics library:
Budget = Movies['Budget_million$']
range2 = np.ptp(Budget) #ptp stands for Peak To Peak
print("The range of the budget of the Top10 highest-grossing films is ",range2,"million USD")

# #### Percentiles and Quartiles

# A measure which indicates the value below which a given percentage of points in a dataset fall. The sample 𝑝 percentile is the element in the dataset such that 𝑝% of the elements in the dataset are less than or equal to that value. Also, (100 − 𝑝)% of the elements are greater than or equal to that value. For example, median represents the 50th percentile. Similarly, we can have 0th percentile representing the minimum and 100th percentile representing the maximum of all data points. Percentile gives the relative position of a particular value within the dataset. It also helps in comparing the data sets which have different means and deviations.
# Each dataset has three quartiles, which are the percentiles that divide the dataset into four parts:
#
# - The first quartile (Q1) is the sample 25th percentile. It divides roughly 25% of the smallest items from the rest of the dataset.
# - The second quartile Q2) is the sample 50th percentile or the median. Approximately 25% of the items lie between the first and second quartiles and another 25% between the second and third quartiles.
# - The third quartile (Q3) is the sample 75th percentile. It divides roughly 25% of the largest items from the rest of the dataset.

# In[18]:

Budget = Movies['Budget_million$']
#via Numpy:
p10 = np.percentile(Budget, 10) #returns the 10th percentile
print("The 10th percentile of the budget of the Top10 highest-grossing films is ",p10)
p4070 = np.percentile(Budget, [40,70]) #returns the 40th and 70th percentile
print("The 40th and 70th percentile of the budget of the Top10 highest-grossing films are ",p4070)
#via Pandas:
p10n = Budget.quantile(0.10) #returns the 10th percentile - notice the difference from Numpy
print("The 10th percentile of the budget of the Top10 highest-grossing films is ",p10n)
#via Statistics:
Qs = statistics.quantiles(Budget, n=4, method='inclusive')     #The parameter n defines the number of resulting equal-probability percentiles:
                                                               #n=4 returns the quartiles | n=2 returns the median
print("The quartiles of the budget of the Top10 highest-grossing films is ",Qs)

# #### InterQuartile Range (IQR)

# IQR is the difference between the third quartile and the first quartile (Q3-Q1). The interquartile range is a better option than range because it is not affected by outliers. It removes the outliers by just focusing on the distance within the middle 50% of the data.

# In[19]:

Budget = Movies['Budget_million$']
#via Numpy:
IQR1 = np.percentile(Budget, 75) -np.percentile(Budget, 25)    #returns the IQR = Q3-Q1 = P75-P25
print("The IQR of the budget of the Top10 highest-grossing films is ",IQR1)
#via scipy.stats:
IQR2 = scipy.stats.iqr(Budget)    #returns the IQR- Can be used for other percentile differences as well >> iqr(object, rng=(p1, p2))
print("The IQR of the budget of the Top10 highest-grossing films is ",IQR2)

# #### The Five-number Summary

# A five-number summary is especially useful in descriptive analyses or during the preliminary investigation of a large data set. A summary consists of five values: the most extreme values in the data set (the maximum and minimum values), the lower and upper quartiles, and the median. Five-number summary can be used to describe any data distribution. Boxplots are extremely useful graphical representation of the 5-number summary that we will discuss later.

# In[20]:

Budget = Movies['Budget_million$']
Budget.describe()     #Remember this jewel from Pandas? -It directly return the 5-number summary AND MORE!

# Boxplots are extremely useful graphical representation of the 5-number summary. It can show the range, interquartile range, median, mode, outliers, and all quartiles.

# In[21]:

import matplotlib.pyplot as plt  #Required for the plot

gross = [3706,3257,3081,3043,2798,2549,2489,2356,2233,2202,5000] #same data + an outlier: 5000

fig = plt.figure(figsize =(7, 5))
plt.boxplot(gross,medianprops={'linewidth': 1, 'color': 'purple'})

plt.show()

#

# #### Variance

# The sample variance quantifies the spread of the data. It shows numerically how far the data points are from the mean. The observations may or may not be meaningful if observations in data sets are highly spread. Let's calculate the variance for budget of the Top10 highest-grossing films.
#
# ![](https://miro.medium.com/max/666/0*ovSFlxj9RJMgtQoX.png)
#
# Note that if we are working with the entire population (and not the sample), the denominator should be "n" instead of "n-1".

# Note that if we are working with the entire population (and not the sample), the denominator should be "n" instead of "n-1".

# In[22]:

# Primitive Python:
budget = [3.9,237,200,11,356,8.2,10.5,13,11,306]
n = len(budget)
mean = sum(budget) / n
var1 = sum((item - mean)**2 for item in budget) / (n - 1)
print("The variance of the budget of the Top10 highest-grossing films is ",var1)

# via the Statistics library:
Budget = Movies['Budget_million$']
var2 = statistics.variance(Budget)
print("The variance of the budget of the Top10 highest-grossing films is ",var2)

# #### Standard Deviation

# The sample standard deviation is another measure of data spread. It’s connected to the sample variance, as standard deviation, 𝑠, is the positive square root of the sample variance. The standard deviation is often more convenient than the variance because it has the same unit as the data points.
#
# ![](https://cdn.kastatic.org/googleusercontent/N8xzWFc6eo0XBHEXZjz1SwvLSnPezvhTRF1P17kdjUG_tnJivGKkyCxbwVe4MZ0-USOxIZBohgcjdi8e7Z4Hswcqfw)
#

# In[23]:

# Primitive Python:
budget = [3.9,237,200,11,356,8.2,10.5,13,11,306]
n = len(budget)
mean = sum(budget) / n
var = sum((item - mean)**2 for item in budget) / (n - 1)
sd1 = var**0.5
print("The standard deviation of the budget of the Top10 highest-grossing films is ",sd1,"million USD")

# via the Statistics library:
Budget = Movies['Budget_million$']
sd2 = statistics.stdev(Budget)
print("The standard deviation of the budget of the Top10 highest-grossing films is ",sd2,"million USD")

# ### Skewness

# The sample skewness measures the asymmetry of a data sample. There are several mathematical definitions of skewness. The Fisher-Pearson standardized moment coefficient is calculated by using mean, median and standard deviation of the data.
#
# ![](https://miro.medium.com/max/560/1*B-n9Ujb_IPggKO9P52jrDA.png)
#
# Usually, negative skewness values indicate that there’s a dominant tail on the left side. Positive skewness values correspond to a longer or fatter tail on the right side. If the skewness is close to 0 (for example, between −0.5 and 0.5), then the dataset is considered quite symmetrical.

# In[24]:

# Primitive Python:
budget = [3.9,237,200,11,356,8.2,10.5,13,11,306]
n = len(budget)
mean = sum(budget) / n
var = sum((item - mean)**2 for item in budget) / (n - 1)
std = var**0.5
skew1 = (sum((item - mean)**3 for item in budget)
         * n / ((n - 1) * (n - 2) * std**3))
print("The skewness of the budget of the Top10 highest-grossing films is ",skew1)

# via the scipy.stats library:
Budget = Movies['Budget_million$']
skew2 = scipy.stats.skew(Budget, bias=False)
print("The skewness of the budget of the Top10 highest-grossing films is ",skew2)

# via the Pandas library:
Budget = Movies['Budget_million$']
skew3 = Budget.skew()
print("The skewness of the budget of the Top10 highest-grossing films is ",skew3)

# ### Kurtosis

# Kurtosis describes the peakedness of the distribution. In other words, Kurtosis identifies whether the tails of a given distribution contain extreme values. While Skewness essentially measures the symmetry of the distribution, kurtosis determines the heaviness of the distribution tails.
#
# ![](https://miro.medium.com/max/2220/1*-UQjlHtFyW6m5gcyZ6db2A.png)
#
# If the distribution is tall and thin it is called a leptokurtic distribution. Values in a leptokurtic distribution are near the mean or at the extremes. A flat distribution where the values are moderately spread out (i.e., unlike leptokurtic) is called platykurtic distribution. A distribution whose shape is in between a leptokurtic distribution and a platykurtic distribution is called a mesokurtic distribution.

# In[25]:

# via the scipy.stats library:
Budget = Movies['Budget_million$']
Kurt = scipy.stats.kurtosis(Budget)
print("The kurtosis of the budget of the Top10 highest-grossing films is ",Kurt) #a platykurtic distribution | the tails are heavy

# ## References
# To read more about mode, check these out:
# - "Mode: A statistical measure of central tendency"
# , available @ https://corporatefinanceinstitute.com/resources/knowledge/other/mode/
#
# - "When to use each measure of Central Tendency"
# , available @ https://courses.lumenlearning.com/introstats1/chapter/when-to-use-each-measure-of-central-tendency/
#
# - "Mean, Median, Mode: What They Are, How to Find Them"
# , available @ https://www.statisticshowto.com/probability-and-statistics/statistics-definitions/mean-median-mode/
#
# To read more about the 5-number summary, check these out:
# - "Find a Five-Number Summary in Statistics: Easy Steps"
# , available @ https://www.statisticshowto.com/how-to-find-a-five-number-summary-in-statistics/
#
# - "The Five-Number Summary"
# , available @ https://www.purplemath.com/modules/boxwhisk2.htm
#
# - "What Is the 5 Number Summary?"
# by __*Courtney Taylor*__, available @ https://www.statisticshowto.com/probability-and-statistics/statistics-definitions/mean-median-mode/
#
# - "Measures of Skewness and Kurtosis"
# , available @ https://www.itl.nist.gov/div898/handbook/eda/section3/eda35b.htm#:~:text=Skewness%20is%20a%20measure%20of,relative%20to%20a%20normal%20distribution.
#
# - "Are the Skewness and Kurtosis Useful Statistics?"
# , available @ https://www.spcforexcel.com/knowledge/basic-statistics/are-skewness-and-kurtosis-useful-statistics
#
# - "Skew and Kurtosis: 2 Important Statistics terms you need to know in Data Science"
# by __*Diva Dugar*__, available @ https://codeburst.io/2-important-statistics-terms-you-need-to-know-in-data-science-skewness-and-kurtosis-388fef94eeaa
#
# - "Measures of Shape: Skewness and Kurtosis"
# by __*Stan Brown*__, available @ https://brownmath.com/stat/shape.htm

#

#
# ## Laboratory 17
#
# **Examine** (click) Laboratory 17 as a webpage at [Laboratory 17.html](http://54.243.252.9/engr-1330-webroot/8-Labs/Lab17/Lab17.html)
#
# **Download** (right-click, save target as ...) Laboratory 17 as a jupyterlab notebook from [Laboratory 17.ipynb](http://54.243.252.9/engr-1330-webroot/8-Labs/Lab17/Lab17.ipynb)
#

#

#
# ## Exercise Set 17
#
# **Examine** (click) Exercise Set 17 as a webpage at [Exercise 17.html](http://54.243.252.9/engr-1330-webroot/8-Labs/Lab17/Lab17-TH.html)
#
# **Download** (right-click, save target as ...) Exercise Set 17 as a jupyterlab notebook at  [Exercise Set 17.ipynb](http://54.243.252.9/engr-1330-webroot/8-Labs/Lab17/Lab17-TH.ipynb)
#
#

# In[ ]:


"""
This is the toy example problem in the SNOPT documentation.
     min         x1
     s.t   .   x1**2 + 4x2**2 <= 4
           (x1-2)**2 +  x2**2 <= 5
            x1 >=0

We define the function F(x):
   F(x)  = [     x1             ]
           [     x1**2 + 4x2**2 ]
           [ (x1-2)**2 +  x2**2 ]
with ObjRow = 1 to indicate the objective.

The Jacobian is:
  F'(x)  = [   1       0  ]   [ 1 0 ]    [   0       0  ]
           [   2x1    8x2 ] = [ 0 0 ]  + [   2x1    8x2 ]
           [ 2(x1-2)  2x2 ]   [ 0 0 ]    [ 2(x1-2)  2x2 ]
                             linear(A)     nonlinear (G)

"""

import numpy        as np
import scipy.sparse as sp
from  snopt import snopta, SNOPT_options

def sntoya_objF(status,x,needF,F,needG,G):
    F[0] = x[1]         # objective row
    F[1] = x[0]**2        + 4.0*x[1]**2
    F[2] = (x[0] - 2.0)**2 +     x[1]**2

    return status, F, G

def sntoya_objFG(status,x,needF,F,needG,G):
    F[0] = x[1]         # objective row
    F[1] = x[0]**2        + 4.0*x[1]**2
    F[2] = (x[0] - 2.0)**2 +     x[1]**2

    G[0] = 2*x[0]
    G[1] = 8*x[1]
    G[2] = 2*(x[0]-2)
    G[3] = 2*x[1]

    return status, F, G

inf   = 1.0e20
options = SNOPT_options()

options.setOption('Verbose',True)
options.setOption('Solution print',True)
options.setOption('Print filename','sntoya.out')

options.setOption('Summary frequency',1)

# Name arrays have to be dtype='|S1' and also have to be the
# correct length, else they are ignored by SNOPT:
xnames  = np.empty(2,dtype='|S8')
xnames[0] = "      x0"
xnames[1] = "      x1"

Fnames  = np.empty(3,dtype='|S8')
Fnames[0] = "      F0"
Fnames[1] = "      F1"
Fnames[2] = "      F2"

x0      = np.array([ 1.0, 1.0 ])
xlow    = np.array([ 0.0, -inf])
xupp    = np.array([ inf,  inf])

Flow    = np.array([ -inf, -inf, -inf ])
Fupp    = np.array([  inf,  4.0,  5.0 ])

n       = 2
nF      = 3

ObjRow  = 1

# We first solve the problem without providing derivative info
result = snopta(sntoya_objF,n,nF,x0=x0,
                xlow=xlow,xupp=xupp,
                Flow=Flow,Fupp=Fupp,
                ObjRow=ObjRow,
                xnames=xnames,Fnames=Fnames,
                name=' sntoyaF',options=options)

# Now we set up the derivative structures...

# A and G provide the linear and nonlinear components of
# the Jacobian matrix, respectively. Here, G and A are given
# as dense matrices.
#
# For the nonlinear components, enter any nonzero value to
# indicate the location of the nonlinear deriatives (in this case, 2).
# A must be properly defined with the correct derivative values.

A = np.array([ [0, 1],
               [0, 0],
               [0, 0]])

G = np.array([ [0, 0],
               [2, 2],
               [2, 2]])

# Alternatively, A and G can be input in coordinate form via scipy's
# coordinate matrix
#  A = sp.coo_matrix(A)
#  G = sp.coo_matrix(G)
# or explicitly in coordinate form as a tuple
#  iAfun = row indices of A
#  jAvar = col indices of A
#  A     = matrix values of A
#  (A,iAfun,jAvar)
#
#  iGfun = row indices of G
#  jGvar = col indices of G
#  (iGfun,jGvar)
#

result = snopta(sntoya_objFG,n,nF,x0=x0,name='sntoyaFG',xlow=xlow,xupp=xupp,
                Flow=Flow,Fupp=Fupp,ObjRow=ObjRow,A=A,G=G,xnames=xnames,Fnames=Fnames)
print(result)


import numpy as np
import raster_geometry as rg
from random import uniform, seed
import math

def sphere3d(size, radius):
    return rg.sphere(size, radius).astype(np.float32)

def ramp2d(shape,min,max):
    """ create an image of a 2D ramp to simulate uneven background

    Args:
        shape (list of 2 integers): Size of ramp image
        min (number): min of ramp
        max (number): max of ramp

    Returns:
        [2d numpy array]: ramp image
    """

    ramp=np.empty(shape)

    for y in range(shape[0]):
        for x in range(shape[1]):
            ramp[y,x]=(max-min)*(y+x)/(shape[0]+shape[1])+min
    return ramp

def random_circles(im, num, min_r, max_r, min_intensity, max_intensity, seed_val=-1):
    """ draws random circles in an image useful for testing segmentation, background subtraction etc.

    Args:
        im (2d numpy array): input image, will be changed
        num (number): number of circles to draw
        min_r (number): min radius of circles
        max_r (number): max radius of circles
        min_intensity (number): max intensity of circles
        max_intensity (number): min intensity of circles
        seed_val (int, optional): use seed if you need to replicate the same random image defalts to -1 (no seed used).
    """
    if seed_val!=-1:
        seed(seed_val)

    for i in range(num):
        r=round(uniform(min_r, max_r))
        cx=round(uniform(r,im.shape[1]-r))
        cy=round(uniform(r,im.shape[0]-r))
        intensity=round(uniform(min_intensity, max_intensity))
        print(r,cx,cy,intensity)
        temp1=rg.circle([r*2,r*2],r)
        temp2=np.zeros_like(im)
        temp2[cy-r:cy+r,cx-r:cx+r]=temp1
        im[temp2>0]=im[temp2>0]+intensity

def grid_of_circles(im, radius, space,border,intensity):
    """ draws a grid of circles on a background image

    Args:
        im ([type]): background image
        radius ([type]): radius of circles
        space ([type]): space between circles
        border (): border around circles
        intensity ([type]): intensity of circles
    """
    num_circles_x = math.floor(im.shape[1]/(space))
    num_circles_y = math.floor(im.shape[0]/(space))

    for x in range(num_circles_x):
        for y in range(num_circles_y):
            cx=(x)*space+border
            cy=(y)*space+border
            if cx < (im.shape[1]-radius/2-1) and cy < (im.shape[0]-radius/2-1):
                temp1=rg.circle([radius*2,radius*2],radius)
                temp2=np.zeros_like(im)
                #print(cx,cy)
                temp2[cy-radius:cy+radius,cx-radius:cx+radius]=temp1
                im[temp2>0]=intensity


from numpy import *

def compute_error_for_line_given_points(c, m, points):
    # initialize it at 0
    total_error = 0.0
    #for every point
    for i  in range(0, len(points)):#
        # get the x value
        x = points[i, 0]
        # get the y value
        y = points[i, 1]
        # get the difference, square it and add it to the total
        total_error += (y - (m * x + c)) ** 2

    # get the average and return
    return total_error / float(len(points))

def gradient_descent_runner(points, starting_c, starting_m, learning_rate, num_iteration):
    # starting c and m
    c = starting_c
    m = starting_m

    # gradient descent
    for i in range(num_iteration):
        # update c and m with more accurate c and m by performing this gradient step
        c, m = step_gradient(c, m, array(points), learning_rate)

    return [c, m]

def step_gradient(c_current, m_current, points, learning_rate):

    c_gradient = 0
    m_gradient = 0

    N = float(len(points))

    for i in range(len(points)):
        x = points[i, 0]
        y = points[i, 1]
        # direction wrt c and m computing partial derivatives of our error function
        c_gradient += -(2/N) * (y - ((m_current * x) + c_current))
        m_gradient += -(2/N) * x * (y - ((m_current * x) + c_current))

    # update our c and m values using our partial derivatives
    new_c = c_current - (learning_rate * c_gradient)
    new_m = m_current - (learning_rate * m_gradient)

    return [new_c, new_m]

def run():
    #Step 1 - collect data
    points = genfromtxt('data.csv', delimiter=',')

    #Step 2 - define our hyperparameters
    learning_rate = 0.0001
    # y = mx + c (slope formula)
    initial_c = 0
    initial_m = 0
    num_iteration = 1000

    #Step 3 - train our model
    print('starting gradient descent at c = {0}, m = {1}, error = {2}'.format(initial_c, initial_m, compute_error_for_line_given_points(initial_c, initial_m, points)))
    [c, m] = gradient_descent_runner(points, initial_c, initial_m, learning_rate, num_iteration)
    print('After {0} iterations, ending gradient descent at c = {1}, m = {2}, error = {3}'.format(num_iteration, c, m, compute_error_for_line_given_points(c, m, points)))

if __name__ == '__main__':
    run()


# Question 1 Lab 03
# AB Satyaprakash (180123062)

# imports ----------------------------------------------------------------------
import numpy as np
# ------------------------------------------------------------------------------
# functions --------------------------------------------------------------------

def forwardDiff(fArray):
    sz = len(fArray)
    fdArray = [fArray]
    for i in range(1, sz):
        temp = []
        for j in range(sz-i):
            temp.append(fdArray[i-1][j+1]-fdArray[i-1][j])
        fdArray.append(temp)
    return fdArray

def newtonFDPoly(fArray, xArray):
    x0, x1 = xArray[0], xArray[1]
    h = x1-x0
    u = np.array([1/h, -x0/h])
    fdArray = forwardDiff(fArray)
    sz = len(fArray)
    px = np.array([0])

    for i in range(sz):
        term = np.array([1])
        for j in range(i):
            term = np.polymul(term, np.polyadd(u, np.array([-j])))
            term = term/(j+1)
        term = term*fdArray[i][0]
        px = np.polyadd(px, term)
    return px

# ------------------------------------------------------------------------------
# (i) f(0.43) if f(0) = 1, f(0.25) = 1.64872, f(0.5) = 2.71828, f(0.75) = 4.48169
print('Part (i)-----------------------------------------------------------------\n')
X = [0, 0.25, 0.5, 0.75]
F = [1, 1.64872, 2.71828, 4.48169]
val = 0.43  # since we need to approximate f(0.43)
# (A) Degree 1:

xArray = [X[1], X[2]]
fArray = [F[1], F[2]]
px = newtonFDPoly(fArray, xArray)
print("Newton's interpolating polynomial of degree 1 using nodes: {}, {} is {}".format(
    xArray[0], xArray[1], np.poly1d(px)))
print("Approximated value of f({}) using the above is {}".format(val, np.polyval(px, val)))

print('\n')

# (B) Degree 2:

xArray = [X[1], X[2], X[3]]
fArray = [F[1], F[2], F[3]]
px = newtonFDPoly(fArray, xArray)
print("Newton's interpolating polynomial of degree 2 using nodes: {}, {}, {} is \n{}".format(
    xArray[0], xArray[1], xArray[2], np.poly1d(px)))
print("Approximated value of f({}) using the above is {}".format(val, np.polyval(px, val)))

print('\n')

# (C) Degree 3:
xArray = [X[0], X[1], X[2], X[3]]
fArray = [F[0], F[1], F[2], F[3]]
px = newtonFDPoly(fArray, xArray)
print("Newton's interpolating polynomial of degree 3 using nodes: {}, {}, {}, {} is \n{}".format(
    xArray[0], xArray[1], xArray[2], xArray[3], np.poly1d(px)))
print("Approximated value of f({}) using the above is {}".format(val, np.polyval(px, val)))

# (ii) f(0.18) if f(0.1) = −0.29004986, f(0.2) = −0.56079734, f(0.3) = −0.81401972, f(0.4) = −1.0526302
print('\nPart (ii)--------------------------------------------------------------\n')
X = [0.1, 0.2, 0.3, 0.4]
F = [-0.29004986, -0.56079734, -0.81401972, -1.0526302]
val = 0.18  # since we need to approximate f(0.18)
# (A) Degree 1:

xArray = [X[0], X[1]]
fArray = [F[0], F[1]]
px = newtonFDPoly(fArray, xArray)
print("Newton's interpolating polynomial of degree 1 using nodes: {}, {} is {}".format(
    xArray[0], xArray[1], np.poly1d(px)))
print("Approximated value of f({}) using the above is {}".format(val, np.polyval(px, val)))

print('\n')

# (B) Degree 2:
xArray = [X[0], X[1], X[2]]
fArray = [F[0], F[1], F[2]]
px = newtonFDPoly(fArray, xArray)
print("Newton's interpolating polynomial of degree 2 using nodes: {}, {}, {} is \n{}".format(
    xArray[0], xArray[1], xArray[2], np.poly1d(px)))
print("Approximated value of f({}) using the above is {}".format(val, np.polyval(px, val)))

print('\n')

# (C) Degree 3:
xArray = [X[0], X[1], X[2], X[3]]
fArray = [F[0], F[1], F[2], F[3]]
px = newtonFDPoly(fArray, xArray)
print("Newton's interpolating polynomial of degree 3 using nodes: {}, {}, {}, {} is \n{}".format(
    xArray[0], xArray[1], xArray[2], xArray[3], np.poly1d(px)))
print("Approximated value of f({}) using the above is {}".format(val, np.polyval(px, val)))

# Question 1 ends --------------------------------------------------------------

Semester 6/MA 322 (Scientific Computing Theory)/Labs/Lab 3/Code/q1.py


""" Plots of a Binomial distribution using the ployly library
    The Binomial distribution describes a sequence of n trials. Each trial can result in a positive result
    with probability p and a negative result with probability 1 - p. The parameters of this distribution are p and n.

    The probability mass function is:
         Pr[k successes in n trials] = Pr[X = k] = binomial_coeff(n, k)  p^k  (1 - p)^(n-k)

    Example of using this program:
         python binomial.py -p 0.25 -n 40

    Dependencies: Python 3.7, plotly, pandas, scipy
"""

import sys
import argparse
import plotly.io as pio
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
from scipy.stats import binom

def parse_arguments():
    """ Parses the command line and returns an object containing the desired distribution parameters (p and n)"""

    describe = "Computes the probability of k successes in n trials, where p is the probability "
    describe += "of success in any trial. "
    parser = argparse.ArgumentParser(description=describe)

    required = parser.add_argument_group("required arguments")
    required.add_argument('-p', '--probability', help="success probability", type=float, required=True)
    required.add_argument('-n', '--num_trials', help="number of trials", type=int, required=True)

    args = parser.parse_args()

    # Verify the quality of the arguments

    if args.probability > 1.0 or args.probability < 0:
        print("[Error] The probability value must belong to [0.0, ...  1.0]")
        print("        Current value is {}".format(args.probability))
        sys.exit(1)

    if args.num_trials <= 0:
        print("[Error] Parameter n (num trials) must be non-zero positive")
        print("        Current value is {}".format(args.num_trials))

    # Return arguments as a class object

    class Params:
        p = args.probability
        n = args.num_trials

    return Params

def binomial_pmf_experimental(params):
    """ Computes the PMF for a binomial distribution from 10,0000 samples produced by a random number generator
    Args:
        params: An object containing two attributes: n and p. These are the distribution parameters
    Returns:
        A pandas data frame with 2 columns: 'k' and 'prob_exp'. The first column contains integers from 0 to n
        (representing the number of successful trials). The second column contains the probabilities Pr[X=k]. The
        second column name is 'prob_exp'
    """
    num_experiments = 10000  # Defines the number of random samples
    xvals = range(0, params.n + 1)  # Defines all possible values of k (number of successes in n trials)

    # generate samples using a random number generator
    random_samples = binom.rvs(n=params.n, p=params.p, size=num_experiments)

    # create a dictionary to count how many events of X=k happened in the randomly generated samples
    zvals = [0] * len(xvals)            # list of 0's having the same size as xvals
    zipped_vals = zip(xvals, zvals)     # create a list of pairs, with each pair representing (k, 0)
    dict_vals = dict(zipped_vals)       # convert list into a dictionary with elements like  "k": 0

    for rs in random_samples:
        dict_vals[rs] += 1  # Count number of occurrences for randomly generated value

    # convert dictionary of results to a pandas data frame
    res_df = pd.DataFrame(list(dict_vals.items()), columns=['k', 'raw_count'])

    # normalize results to obtain probabilities and delete original raw counts
    res_df['prob_exp'] = res_df['raw_count'] / num_experiments
    del res_df['raw_count']

    return res_df

def binomial_pmf_theoretical(params):
    """ Computes the PMF for a binomial distribution using the explicit formula
        Args:
            params: An object containing two attributes: n and p. These are the distribution parameters
        Returns:
            A pandas data frame with 2 columns called 'k' and 'prob_thr'. The first column contains integers
            from 0 to n representing the number of successful trials. The second column contains the
            probabilities Pr[X=k].
        """
    xvals = range(0, params.n + 1)   # X is the random variable in Pr[X=k]. It represents number of successes
    probs = [binom.pmf(k=v, n=params.n, p=params.p) for v in xvals] # Generate p(k) for each possible value of k

    # Create a pandas dataframe with the results and return
    res_df = pd.DataFrame(data=list(zip(xvals, probs)), columns=["k", "prob_thr"])

    return res_df

def main(params):
    """ Orchestrates the different procedures for data collection and later displays the results
    Args:
        params: An object with two members p and n, which are the distribution parameters
    Returns:
        Nothing to return. This function displays the resulting charts in a local browser
    """
    # compute a theoretical pmf using the formula for a binomial distribution
    thr_df = binomial_pmf_theoretical(params)

    # compute an experimental pmf using a binomial random number generator
    exp_df = binomial_pmf_experimental(params)

    chart_title = "Probability Mass Function (PMF) of a Binomial Distribution with p = {} and n = {}".format(params.n,
                                                                                params.p)

    # merge results and plot the charts
    merged_df = pd.merge(thr_df, exp_df, on='k')
    display_2_charts(df=merged_df, xcol='k', y1col='prob_thr', y2col="prob_exp", xlabel="k",
                     y1label='Pr[X=k]', y2label='Pr[X=k]',
                     y1title="Theoretical PMF", y2title="Experimental PMF (from 10,000 samples)",
                     title=chart_title)

def display_2_charts(df, xcol, y1col, y2col, xlabel="", y1label="", y2label="", y1title="", y2title="", title=""):
    """ Displays two charts using the browser of the machine that runs the program. Uses plotly to generate
    the composition
    """
    pio.renderers.default = "browser"

    fig = make_subplots(rows=2, cols=1, subplot_titles=(y1title, y2title))

    # Top and bottom figures
    fig.add_trace(go.Bar(x=df[xcol], y=df[y1col]), row=1, col=1)
    fig.add_trace(go.Bar(x=df[xcol], y=df[y2col]), row=2, col=1)

    # Add axis information
    fig.update_xaxes(title_text=xlabel, row=1, col=1)
    fig.update_xaxes(title_text=xlabel, row=2, col=1)

    fig.update_yaxes(title_text=y1label, row=1, col=1)
    fig.update_yaxes(title_text=y2label, row=2, col=1)

    # Add main title
    fig.update_layout(title_text=title, width=800, showlegend=False)

    fig.show()

if __name__ == "__main__":
    main(parse_arguments())


import math
import numpy as np

def normalize_by_min_max(data, min_value=0.0, max_value=1.0):
    """ Normalize the data in min_value and max_value

    Parameters
    -----------
    data : array-like
    min_value : float
        min_value of the fixed data, default is 0.0
    max_value : float
        max_value of the fixed data, default is 1.0

    Returns
    ----------
    normalized_data : numpy.ndarray
        the shape of array is same as the input data
    """
    if min_value >= max_value:
        raise ValueError("max angle must be greater than min angle")

    data = np.array(data)
    data_shape = data.shape

    data = data.flatten()

    max_data = np.amax(data)
    min_data = np.amin(data)

    normalized_data = ((data - min_data) / (max_data - min_data)) * (max_value - min_value) + min_value

    return normalized_data.reshape(data_shape)

def standardize(data):
    """Standardize the data. Fixed data's average is 0 and, variance is 1.0

    Parameters
    ------------
    data : array-like

    Returns
    -----------
    standarized data : numpy.ndarray
        the shape of array is same as the input data
    """
    data = np.array(data)
    data_shape = data.shape

    data = data.flatten()

    ave_data = np.average(data)
    var_data = np.sqrt(np.var(data))

    standardized_data = (data - ave_data) / var_data

    return standardized_data.reshape(data_shape)


import numpy as np
from .integrate import Integrate

class Trapezoid(Integrate):

    """Approximate the integral of f(x) on the interval [a,b] by the trapezoid rule.

    The trapezoid rule approximates the integral of f(x) dx from a to b by the sum:
    dx/2 * sum(f(x_{i}) + f(x_{i-1})) from i = 1 to N, where N is the number of periods.
    dx = (b - a)/N.
    x_i = a + i*dx

    Parameters
    ----------
    f : function
    A single variable function f(x)

    a , b : numbers
    Interval of integration [a,b]
    defult to [-1,1]

    N : integer
    Number of subintervals of [a,b]

    Returns
    -------
    float
        Approximation of the integral of f(x) from a to b using the
        trapezoid rule with N subintervals of equal lengths.

    """
    def __init__(self, f):
        Integrate.__init__(self, f)
        self.N = 250
        self.T = None

    def compute_integral(self, a, b, N=250):
        """
        Approximate the value of the integral of f(x) dx from a to b with N sub-intervals

        Parameters
        ----------
        a , b : numbers
        Interval of integration [a,b]\n
        defult to [-1,1]

        N : integer
        Number of subintervals of [a,b]

        Examples
        --------
        >>> compute_integral(0,5,1000), f = lambda x:1 / (1 + x**2)
        approx = 1.3734007823542813
        actual = 1.373400766945016
        """
        self.a = a
        self.b = b
        self.N = N
        x = np.linspace(self.a, self.b, self.N+1) # N+1 points make N subintervals
        y = self.f(x)
        self.dx = 1.0 * (self.b - self.a) / self.N
        self.T = (self.dx / 2) * np.sum(y[1:] + y[:-1]) # T =  x_i + x_i-1 = (x_1 + x_0) + (x_2 + x_1) + (x_3 + x_2) + ... + (x_N + x_N-1)
        return self.T


"""Hadamard matrix."""
import numpy as np

def hadamard(n_param: int = 1) -> np.ndarray:
    r"""
    Produce a 2^{n_param} dimensional Hadamard matrix [WikHad]_.

    The standard Hadamard matrix that is often used in quantum information as a
    two-qubit quantum gate is defined as

    .. math::
        H_1 = \frac{1}{\sqrt{2}} \begin{pmatrix}
                                    1 & 1 \\
                                    1 & -1
                                 \end{pmatrix}

    In general, the Hadamard matrix of dimension :math:2^{n_param} may be
    defined as

    .. math::
        \left( H_n \right)_{i, j} = \frac{1}{2^{\frac{n}{2}}}
        \left(-1\right)^{i \dot j}

    Examples
    ==========

    The standard 2-qubit Hadamard matrix can be generated in `toqito` as

    >>> from toqito.matrices import hadamard
    >>> hadamard(1)
    [[ 0.70710678  0.70710678]
     [ 0.70710678 -0.70710678]]

    References
    ==========
    .. [WikHad] Wikipedia: Hadamard transform
        https://en.wikipedia.org/wiki/Hadamard_transform

    :param n_param: A non-negative integer (default = 1).
    :return: The Hadamard matrix of dimension `2^{n_param}`.
    """
    if n_param == 0:
        return np.array([1])
    if n_param == 1:
        return 1 / np.sqrt(2) * np.array([[1, 1], [1, -1]])
    if n_param == 2:
        return 1 / 2 * np.array([[1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1]])
    if n_param > 1:
        mat_1 = hadamard(1)
        mat_2 = hadamard(1)
        mat = np.kron(mat_1, mat_2)
        for _ in range(2, n_param):
            mat_1 = mat_2
            mat_2 = mat
            mat = np.kron(mat_1, mat_2)
        return mat
    raise ValueError(f"Improper dimension {n_param} provided.")


import numpy as np
# CubbyFlow ENO
def upwind1(d,dx,direction):
    if direction == -1:
        return (d[0] - d[1])/dx
    elif direction == 1:
        return (d[2] - d[0])/dx

def centreDifference(d,dx):
    return (d[2] - d[0])/(2*dx)

def ENO3(d,dx):
    d1 = [d[1] - d[0],d[2] - d[1],d[3] - d[2],
          d[4] - d[3],d[5] - d[4],d[6] - d[5]]/dx
    d2 = [d1[1] - d1[0],d1[2] - d1[1],d1[3] - d1[2],
          d1[4] - d1[3],d1[5] - d1[4]] / dx / 2
    dfx = np.zeros((2))
    for k in range(2):
        d3 = np.zeros((2))
        if abs(d2[k+1]) < abs(d2[k+2]):
            c = d2[k+1]
            kstar = k - 1
            d3[0] = (d2[k+1] - d2[k]) / 3 / dx
            d3[1] = (d2[k+2] - d2[k+1]) / 3 / dx
        else:
            c = d2[k+2]
            kstar = k
            d3[0] = (d2[k+2] - d2[k+1]) / 3 / dx
            d3[1] = (d2[k+3] - d2[k+2]) / 3 / dx

        if abs(d3[0]) < abs(d3[1]):
            cstar = d3[0]
        else:
            cstar = d3[1]

        dq1 = d1[k+2]
        dq2 = c * (2 * (1 - k) - 1) * dx
        dq3 = cstar*(3*np.sqrt(1-k)-6*(1-kstar)+2)*dx*dx
        dfx[k] = dq1 + dq2 + dq3

    return dfx

def WENO5(v):
    vdev = np.zeros((5))
    dfx = np.zeros((2))
    dx = 1
    for k in range(2):
        if k == 0:
            for m in range(5):
                vdev[m] = (v[m+1] - v[m]) / dx
        else:
            for m in range(5):
                vdev[m] = (v[6-m] - v[5-m]) / dx
        phix1 = vdev[0]/3 - vdev[1]*7/6 + vdev[2]*11/6
        phix2 = -vdev[1]/6 + vdev[2]*5/6 + vdev[3]/3
        phix3 = vdev[2]/3 + vdev[3]*5/6 - vdev[4]/6

        s1 = (vdev[0]-2*vdev[1]+vdev[2])**2*13/12 + (vdev[0]-4*vdev[1]+3*vdev[2])**2/4
        s2 = (vdev[1]-2*vdev[2]+vdev[3])**2*13/12 + (vdev[1]-vdev[3])**2/4
        s3 = (vdev[2]-2*vdev[3]+vdev[4])**2*13/12 + (3*vdev[2]-4*vdev[3]+vdev[4])**2/4

        eps = 1e-10
        alpha1 = (s1 + eps)/10
        alpha2 = (s2 + eps)*3/5
        alpha3 = (s3 + eps)*3/10

        dfx[k] = (alpha1 * phix1 + alpha2 * phix2 + alpha3 * phix3) / (alpha1 + alpha2 + alpha3)

    return dfx


import numpy as np

def mse(X, theta, Y):
    return ((np.dot(X, theta) - Y) ** 2).mean()/2

class LinearRegression:
    """ Ordinary least squares Linear Regression Using Normal Equation,
    This is the Closed Form Solution for OLS.

    ----------
    Parameters
    ----------
    fit_intercept: bool, optional, default True
    Whether to calculate the intercept for this model.

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)
    reg = LinearRegression()
    reg = reg.fit(X_train, Y_train)
    print(reg.theta)
    reg.predict(X_test)

    """

    def __init__(self, fit_intercept = True):
        self.fit_intercept = fit_intercept
        self.theta = None

    def fit(self, X, Y):
        if self.fit_intercept: # add intercept: add X0 = 1 to each constance
            X = np.c_[np.ones((len(X),1)), X]
        self.theta = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(Y)
        return self

    def predict(self, X):
        if self.fit_intercept: # add intercept: add X0 = 1 to each constance
            X = np.c_[np.ones((len(X),1)), X]
        return np.dot(X, self.theta)

class Ridge(LinearRegression):
    """ Ridge Regression Using Normal Equation,
    This is the Closed Form Solution for Ridge.

    ----------
    Parameters
    ----------
    fit_intercept: bool, optional, default True
    Whether to calculate the intercept for this model.

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)
    reg = Ridge(1)
    reg = reg.fit(X_train, Y_train)
    print(reg.theta)
    reg.predict(X_test)

    """

    def __init__(self, alpha = 1, fit_intercept = True):
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.theta = None

    def fit(self, X, Y):
        if self.fit_intercept: # add intercept: add X0 = 1 to each constance
            X = np.c_[np.ones((len(X),1)), X]
        self.theta = np.linalg.inv(X.T.dot(X) + self.alpha * np.eye(X.shape[1])).dot(X.T).dot(Y)
        return self

class BGDRegressor:
    """Linear Regression with Batch Gradient Descent

    ----------
    Parameters
    ----------
    learning_rate: float, default=0.01

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Tolerance, the stopping criterion.
        Default:     'cost function'
        Alternative: 'step length'. faster, without cost computation.

    alpha: float, default=0
        Ridge Regression L2 regularization term
        default: no regularization

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta.

    ----------
    Attributes
    ----------
    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # Linear Regression with Batch Gradient Descent
    reg = BGDRegressor(learning_rate = 0.5, max_iter = 1e6, tol = 1e-3, fit_intercept = True, random_state=42)

    # Ridge Regression with Batch Gradient Descent
    reg = BGDRegressor(learning_rate = 0.5, max_iter = 1e6, tol = 1e-3, alpha = 1e-3, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, learning_rate = 0.01, max_iter = 1e3, tol = 1e-3, alpha = 0, fit_intercept = True, random_state = None):
        # Parameters
        self.learning_rate = learning_rate
        self.max_iter = max_iter
        self.tol = tol
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None

    def _gradient_descent(self, X, Y):
        np.random.seed(self.random_state)
        convergence = False
        cost = [np.inf]*2
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
        else:
            X_b = X
            theta = np.random.randn(self.n_features)
        for iteration in range(int(self.max_iter)):
            gradient = 2/self.n_samples * np.dot(X_b.T, np.dot(X_b, theta) - Y) + self.alpha * theta
            theta = theta - self.learning_rate * gradient

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, theta, Y))
            # print(cost[1], end='\r')

            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break

            # convergence criteria of 'step length'
            # if np.linalg.norm(gradient) < self.tol:
                # print("Converge!")
                # break

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

        self.cost = cost[1]
        self.n_iter = iteration
        return theta

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]

        self.theta = self._gradient_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)

class SGDRegressor:
    """Linear Regression with Stochastic Gradient Descent

    ----------
    Parameters
    ----------
    learning_rate: float or string, default=0.01
        Options:

        float: [default]
            eta = eta0,
            constant learning rate

        'annealing':
            simulated annealing with learning schedule, generally faster and more accurate
            please tuning the hyperpameters t0, t1 for better performance

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Convergence Tolerance, using the stopping criterion of cost function.

    alpha: float, default=0
        Ridge Regression L2 regularization term
        default: no regularization

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta and selecting data.

    ----------
    Attributes
    ----------
    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # constant learning rate
    reg = SGDRegressor(learning_rate = 1e-3, max_iter = 1e3, tol = 1e-3, fit_intercept = True, random_state=42)

    # simulated annealing with learning schedule: generally faster and more accurate
    reg = SGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, t0 = 1e3/2, t1 = 1e3, fit_intercept = True, random_state=42)

    # Ridge Regression: L2 Regularization
    reg = SGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, t0 = 1e3/2, t1 = 1e3, alpha = 1e-6, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, learning_rate = 0.01, max_iter = 1e3, tol = 1e-3, t0 = 10, t1 = 1000, alpha = 0, fit_intercept = True, random_state = None):
        # Parameters
        self.learning_rate = learning_rate
        self.max_iter = max_iter
        self.tol = tol
        self.t0 = t0
        self.t1 = t1
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None

    # descent methods
    def _learning_rate(self, i):
        return self.theta - self.learning_rate * self.gradient
    def _learning_schedule(self, t):
        return self.t0 / (t + self.t1)
    def _simulated_annealing(self, i):
        return self.theta - self._learning_schedule(self.n_iter * self.n_samples + i) * self.gradient + self.alpha * self.theta

    def _gradient_descent(self, X, Y):
        # select descent method accoding to the hyperparameter 'learning_rate'
        if type(self.learning_rate) == float:
            self._descent = self._learning_rate
        elif self.learning_rate == 'annealing':
            self._descent = self._simulated_annealing
        else:
            print('learning rate error!')

        # initialization
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            self.theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
        else:
            X_b = X
            self.theta = np.random.randn(self.n_features)
        convergence = False
        np.random.seed(self.random_state)

        cost = [np.inf]*2 # queue recoding N cost values

        for iteration in range(int(self.max_iter)):
            self.n_iter = iteration
            for i in range(self.n_samples):
                random_index = np.random.randint(self.n_samples)
                Xi = X_b[random_index]
                Yi = Y[random_index]
                self.gradient = 2 / 1 * np.dot(Xi.T, np.dot(Xi, self.theta) - Yi)
                self.theta = self._descent(i)

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, self.theta, Y)) # calculate cost each iteration
            print(cost[1], end="\r")
            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break
            self.cost = cost[1]

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]
        self._gradient_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)

class MBGDRegressor:
    """Linear Regression with Mini-Batch Gradient Descent

    ----------
    Parameters
    ----------
    learning_rate: float or string, default=0.01
        Options:

        float: [default]
            eta = eta0,
            constant learning rate

        'annealing':
            simulated annealing with learning schedule, generally faster and more accurate
            please tuning the hyperpameters t0, t1 for better performance

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Convergence Tolerance, using the stopping criterion of cost function.

    batch_size: int, default=2**5
        the number of instances in each batch
        recommended values: 2**5 ~ 2**10

    alpha: float, default=0
        Ridge Regression L2 regularization term
        default: no regularization

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta and selecting data.

    ----------
    Attributes
    ----------
    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # constant learning rate
    reg = MBGDRegressor(learning_rate = 1e-3, max_iter = 1e3, tol = 1e-3, batch_size = 2**5, alpha = 0, fit_intercept = True, random_state=42)

    # simulated annealing with learning schedule: generally faster and more accurate
    reg = MBGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, batch_size = 2**5, t0 = 1e3/2, t1 = 1e3, alpha = 0, fit_intercept = True, random_state=42)

    # Ridge Regression: L2 Regularization
    reg = MBGDRegressor(learning_rate = 'annealing', max_iter = 1e3, tol = 1e-3, batch_size = 2**5, t0 = 1e3/2, t1 = 1e3, alpha = 1, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, learning_rate = 0.01, max_iter = 1e3, tol = 1e-3, batch_size = 2**5, t0 = 10, t1 = 1000, alpha = 0, fit_intercept = True, random_state = None):
        # Parameters
        self.learning_rate = learning_rate
        self.max_iter = max_iter
        self.tol = tol
        self.batch_size = batch_size
        self.t0 = t0
        self.t1 = t1
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None

    # descent methods
    def _learning_rate(self, i):
        return self.theta - self.learning_rate * self.gradient
    def _learning_schedule(self, t):
        return self.t0 / (t + self.t1)
    def _simulated_annealing(self, i):
        return self.theta - self._learning_schedule(self.n_iter * self.n_samples + i) * self.gradient

    def _gradient_descent(self, X, Y):
        # select descent method accoding to the hyperparameter 'learning_rate'
        if type(self.learning_rate) == float:
            self._descent = self._learning_rate
        elif self.learning_rate == 'annealing':
            self._descent = self._simulated_annealing
        else:
            print('learning rate error!')

        # initialization
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            self.theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
        else:
            X_b = X
            self.theta = np.random.randn(self.n_features)
        convergence = False
        np.random.seed(self.random_state)
        cost = [np.inf]*2 # queue recoding N cost values

        for iteration in range(int(self.max_iter)):
            self.n_iter = iteration
            for i in range(0, self.n_samples, self.batch_size):
                random_indexes = np.random.randint(0, self.n_samples, self.batch_size)
                Xi = X_b[random_indexes]
                Yi = Y[random_indexes]
                self.gradient = 2 / self.batch_size * np.dot(Xi.T, np.dot(Xi, self.theta) - Yi) + self.alpha * self.theta
                self.theta = self._descent(i)

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, self.theta, Y)) # calculate cost each iteration
            print(cost[1], end="\r")
            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break
            self.cost = cost[1]

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]
        self._gradient_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)

def soft_threshold(rho, alpha):
    if rho < - alpha:
        return rho + alpha
    if rho > alpha:
        return rho - alpha
    else:
        return 0

class Lasso(CDRegressor):
class CDRegressor:
    """Lasso Regression with Coordinate Descent

    ----------
    Parameters
    ----------

    max_iter: int, default=1e3
        maximum iterations

    tol: float, default=1e-3
        Convergence Tolerance, using the stopping criterion of cost function.

    alpha: float, default=0.01
        Lasso Regression L1 regularization term

    fit_intercept: bool, default=True
        Whether the intercept should be estimated or not. If False, the data is assumed to be already centered.

    random_state: int, default=None
        The seed of the pseudo random number generator to use when initializing theta.

    ----------
    Attributes
    ----------

    theta: ndarray of shape (n_features,)
        Weights assigned to the features.

    n_iter_ : int
        The actual number of iterations to reach the stopping criterion.

    cost: float
        MSE cost of traning data

    coef_: array
        parameter vector

    intercept_: float
        independent term in decision function

    --------
    Examples
    --------
    from sklearn.datasets import load_boston
    from sklearn.preprocessing import normalize
    from sklearn.model_selection import train_test_split
    boston = load_boston()
    X, Y = boston.data, boston.target
    X = normalize(X)
    X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=42)

    # Lasso Regression with Coordinate Descent
    reg = CDRegressor(max_iter = 1e3, tol = 1e-3, alpha = 0.01, fit_intercept = True, random_state=42)

    reg = reg.fit(X_train, Y_train)
    print('theta:', reg.theta)
    print('iterations:', reg.n_iter)
    print('cost:', reg.cost)
    reg.predict(X_test)

    """

    def __init__(self, max_iter = 1e3, tol = 1e-3, alpha = 0.01, fit_intercept = True, random_state = None):
        # Parameters
        self.max_iter = max_iter
        self.tol = tol
        self.alpha = alpha
        self.fit_intercept = fit_intercept
        self.random_state = None
        # Attributes
        self.theta = None
        self.n_iter = None
        self.n_samples = None
        self.n_features = None
        self.cost = None
        self.coef_ = None
        self.intercept_ = None

    def _coordinate_descent(self, X, Y):
        np.random.seed(self.random_state)
        convergence = False
        cost = [np.inf]*2

        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
            theta = np.random.randn(self.n_features + 1) # random initialization for theta values, from Gaussian distribution
            theta[0] = 0
        else:
            X_b = X
            theta = np.random.randn(self.n_features)

        for iteration in range(int(self.max_iter)):
            for i in range(self.n_features+1):
                if self.fit_intercept and i == 0: # Lasso does not regularize the intercept
                    theta[0] = np.sum(Y - np.dot(X_b[:, 1:], theta[1:]))/self.n_samples
                else:
                    Xi = X_b[:, i]
                    theta[i] = 0
                    rho = np.dot(Xi.T, Y - np.dot(X_b, theta))
                    theta[i] = soft_threshold(rho, self.alpha*self.n_samples)/np.square(X_b[:, i]).sum()

            # convergence criteria of 'cost function'
            cost.pop(0)
            cost.append(mse(X_b, theta, Y))
            print(cost[1], end="\r")
            # print(theta)
            if np.abs(cost[0] - cost[1]) < self.tol:
                convergence = True
                break

        if convergence:
            print("Achieve Convergence!")
        else:
            print("Reached Max Iteration! Not Achieve Convergence.")

        self.cost = cost[1]
        self.n_iter = iteration
        return theta

    def fit(self, X, Y):
        self.n_samples = X.shape[0]
        self.n_features = X.shape[1]
        self.theta = self._coordinate_descent(X, Y)
        return self

    def predict(self, X):
        if self.fit_intercept:
            X_b = np.c_[np.ones((len(X),1)), X]
        else:
            X_b = X
        return np.dot(X_b, self.theta)


import numpy as np
from scipy import signal

# 1 sample delay: y[n] = x[n] + A * y[n-1]
b = [1]
a = [1,0]
a1 = [1,0.25]
a2 = [1,0.5]
a3 = [1,0.75]
a4 = [1,1]

# Sampling frequency = 2
w, h = signal.freqz(b, a, fs=2)
w1, h1 = signal.freqz(b, a1, fs=2)
w2, h2 = signal.freqz(b, a2, fs=2)
w3, h3 = signal.freqz(b, a3, fs=2)
w4, h4 = signal.freqz(b, a4, fs=2)

import matplotlib.pyplot as plt

fig, ax1 = plt.subplots(2)
ax1[0].set_title('Digital filter frequency response')
ax1[0].plot(w, 20 * np.log10(abs(h)), 'black', label = r"$\alpha$ = 0")
ax1[0].plot(w, 20 * np.log10(abs(h1)), 'g', label = r"$\alpha$ = 0.25")
ax1[0].plot(w, 20 * np.log10(abs(h2)), 'orange', label = r"$\alpha$ = 0.5")
ax1[0].plot(w, 20 * np.log10(abs(h3)), 'blue', label = r"$\alpha$ = 0.75")
ax1[0].plot(w, 20 * np.log10(abs(h4)), 'red', label = r"$\alpha$ = 1")
ax1[0].set_ylabel('Magnitude [dB]', color='black')
ax1[0].legend(prop = {'size': 8})

# Wrap angles to be from -pi to pi
# If angle is greater than pi it wraps around to
# the negative side
angles = np.unwrap(np.angle(h))
angles1 = np.unwrap(np.angle(h1))
angles2 = np.unwrap(np.angle(h2))
angles3 = np.unwrap(np.angle(h3))
angles4 = np.unwrap(np.angle(h4))

# Convert from radians to degrees:
angles = ( angles * 180 ) / np.pi
angles1 = ( angles1 * 180 ) / np.pi
angles2 = ( angles2 * 180 ) / np.pi
angles3 = ( angles3 * 180 ) / np.pi
angles4 = ( angles4 * 180 ) / np.pi

ax1[1].plot(w, angles, 'black', label = r"$\alpha$ = 0")
ax1[1].plot(w, angles1, 'g', label = r"$\alpha$ = 0.25")
ax1[1].plot(w, angles2, 'orange', label = r"$\alpha$ = 0.5")
ax1[1].plot(w, angles3, 'blue', label = r"$\alpha$ = 0.75")
ax1[1].plot(w, angles4, 'red', label = r"$\alpha$ = 1")

ax1[1].set_ylabel('Phase (degrees)', color='black')
ax1[1].set_xlabel('Normalized Frequency [$\pi$*rad/sample]')
ax1[1].grid()
ax1[1].axis('tight')
#ax1[1].legend(prop = {'size': 6})

plt.show()


#zadanie 4 Paweł Gałka

import numpy
from scipy.optimize import linprog

#f(x) = 0*x1 + 3*x2 + 1*x3 + 1*x4 + 4*x5 + 2*x6 + 0*x7 + 3*x8 + 4*x9 -> min
#2*x1 + 1*x2 + 1*x3 + 0*x4 + 0*x5 + 0*x6 + 0*x7 + 2*x8 + 1*x9= 12000
#1*x1 + 2*x2 + 1*x3 + 3*x4 + 2*x5 + 1*x6 + 0*x7 + 0*x8 + 0*x9= 24000
#0*x1 + 0*x2 + 2*x3 + 1*x4 + 2*x5 + 4*x6 + 6*x7 + 1*x8 + 3*x9= 27000

A = numpy.array([[2, 1, 1, 0, 0, 0, 0, 2, 1],
[-2, -1, -1, 0, 0, 0, 0, 0, 0],
[1, 2, 1, 3, 2, 1, 0, 1, 3],
[-1, -2, -1, -3, -2, -1, 0, -2, -1],
[0, 0, 2, 1, 2, 4, 6, 0, 0],
[ 0, 0, -2, -1, -2, -4, -6, -1, 3]])

b = [12000, -12000, 24000, -24000, 27000, -27000]

c = numpy.array([ 0, -3, -1, -1, -4, -2, 0, -3, -4])

A_dual = numpy.transpose(A)

b_dual = numpy.transpose(c)

c_dual = numpy.transpose(b)

result = linprog(c_dual,-A_dual,-b_dual)
x = list(result.x)

#sprawdz ograniczenia problemu dualnego

res_x = {}
for i in range(len(A_dual)):
    RHS = round(b_dual[i],3)
    LHS = round(sum([A_dual[i][j] * x[j] for j in range(len(x))]),3)
    if (LHS > RHS): #zerowanie tej zmiennej
        res_x[i] = 0

A_indexes = list(filter(lambda x : x not in res_x.keys(), numpy.arange(0,len(A[0]))))
A_primal = []
for i in range(len(A)):
    A_primal.append([A[i][j] for j in A_indexes])

primal_solution = numpy.linalg.lstsq(A_primal, b,rcond=1)[0]

print('f_min',abs(result.fun),'m')

print('[11cm, 8cm, 5cm]')
for i in range(len(primal_solution)):
    print(A[::2,A_indexes[i]], round(primal_solution[i],2),'m')


# -*- coding: utf-8 -*-
import numpy as np

def hypothesis(x, theta):
    return np.dot(x, theta)

def loss(x, y, theta):
    return hypothesis(x, theta) - y

def grad(x_t, diffs, m):
    return np.dot(x_t, diffs) / m

def J(theta, x, y, m):
    diffs = loss(x, y, theta)
    return np.dot(diffs.T, diffs) / m

def bgd(x, y, alpha=0.01, epsilon=1e-8):
    m, sse2 = len(x), 0
    x_0 = np.ones(m)
    xx = np.column_stack((x_0, x))
    m, n = np.shape(xx)
    x_t, theta = xx.T, np.ones(n)
    while True:
        diffs = loss(xx, y, theta)

        sse = J(theta, xx, y, m)
        if abs(sse - sse2) < epsilon:
            break
        else:
            sse2 = sse

        gradient = grad(x_t, diffs, m)
        theta -= alpha * gradient
    return theta

if __name__ == '__main__':
    import matplotlib.pyplot as plt

    year = [2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013]
    price = [2.000, 2.500, 2.900, 3.147, 4.515, 4.903, 5.365, 5.704, 6.853, 7.971, 8.561, 10.000, 11.280, 12.900]

    b, a = bgd(np.subtract(year, 2000), price, 0.01, 1e-8)

    x_axis = np.linspace(1999, 2015, 10000)

    for i in range(len(year)):
        plt.scatter(year[i], price[i])

    def linear_equation(x, a, b):
        return b + a * (x - 2000)

    print('y = {0} * (x - 2000) + {1}'.format(a, b))
    plt.plot(x_axis, [linear_equation(x, a, b) for x in x_axis], 'r:')
    plt.grid(True)
    plt.show()


import pandas as pd
import numpy as np
import scipy.stats as ss

def cramers_v(confusion_matrix: pd.DataFrame) -> int:
    """
    Calculate Cramers V statistic for categorial-categorial association.
    uses correction from Bergsma and Wicher,
    Journal of the Korean Statistical Society 42 (2013): 323-328

    :param confusion_matrix: Input table of values.
    :return: Correlation of values in input confusion_matrix. Close to 1 - strong association,
     close to 0 - weak association.
    """
    chi2 = ss.chi2_contingency(confusion_matrix)[0]
    n = confusion_matrix.sum()
    phi2 = chi2 / n
    r, k = confusion_matrix.shape
    phi2corr = max(0, phi2 - ((k-1)*(r-1))/(n-1))
    rcorr = r - ((r-1)**2)/(n-1)
    kcorr = k - ((k-1)**2)/(n-1)
    return np.sqrt(phi2corr / min((kcorr-1), (rcorr-1)))

def execute_cramers(input_df: pd.DataFrame, column: str, column_to_compare_with: str) -> int:
    """
    Function to execute Cramers V and check input variables.

    :param input_df: Dataframe, which function gets columns from.
    :param column: Name of the input column.
    :param column_to_compare_with: Name of the input column.
    :return: Calls cramers_v function and returns its return value.
    """
    if (isinstance(column_to_compare_with, str)) and (isinstance(column, str)):
        if (input_df.get(column) is not None) and (input_df.get(column_to_compare_with) is not None):
            confusion_matrix = pd.crosstab(input_df[column], input_df[column_to_compare_with]).as_matrix()
        else:
            raise Exception('Cannot execute Cramers V, because at least one of input columns does not exist.')
    else:
        raise Exception('Cannot execute Cramers V, because at least one input column has wrong variable type.')

    return cramers_v(confusion_matrix)


'''European Pricing Class'''
import numpy as np
import scipy.stats as stats

import pricing
from .pricing import Pricing
from utils import ui

_logger = ui.get_logger()

class BlackScholes(Pricing):
    '''
    This class uses the classic Black-Scholes method to calculate prices for European Call and Put options

    '''

    def __init__(self, ticker, expiry, strike, dividend=0.0):
        super().__init__(ticker, expiry, strike, dividend=dividend)

        self.name = pricing.PRICING_METHODS[0]

    def calculate_price(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option prices based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

            CallOptionPrice = SpotPrice*N(d1) − Strike*exp(−r(T−t))*N(d2))
            PutOptionPrice  = Strike*exp(−r(T−t)) *N(−d2) − SpotPrice*N(−d1)
        :return: <float>, <float> Calculated price of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)
        d2 = self._calculate_d2(spot_price, time_to_maturity, volatility)

        self.price_call = ((spot_price * np.exp(-1 * self.dividend * time_to_maturity)) * stats.norm.cdf(d1, 0.0, 1.0) -
                           (self.strike_price * np.exp(-1 * self.risk_free_rate * time_to_maturity) * stats.norm.cdf(d2, 0.0, 1.0)))

        self.price_put = (self.strike_price * np.exp(-1 * self.risk_free_rate * time_to_maturity) * stats.norm.cdf(-1 * d2, 0.0, 1.0) -
                          (spot_price * np.exp(-1 * self.dividend * time_to_maturity)) * stats.norm.cdf(-1 * d1, 0.0, 1.0))

        return self.price_call, self.price_put

    def calculate_delta(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option delta based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        delta call =  np.exp(-q * T) * si.norm.cdf(d1, 0.0, 1.0)
        delta put  = -np.exp(-q * T) * si.norm.cdf(-d1, 0.0, 1.0)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)

        self.delta_call = np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(d1, 0.0, 1.0)
        self.delta_put = -np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(-d1, 0.0, 1.0)

        return self.delta_call, self.delta_put

    def calculate_gamma(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option gamma based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        gamma call & put = np.exp(-q * T) * si.norm.cdf(d1, 0.0, 1.0) / S * sigma * np.sqrt(T)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)
        gamma = np.exp(-self.dividend * time_to_maturity) * stats.norm.pdf(d1, 0.0, 1.0) / spot_price * volatility * np.sqrt(time_to_maturity)

        self.gamma_call = self.gamma_put = gamma * 365.0

        return self.gamma_call, self.gamma_put

    def calculate_theta(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option theta based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        theta call = -np.exp(-q * T) *
                     (S * si.norm.cdf(d1, 0.0, 1.0) * sigma) /
                     (2 * np.sqrt(T)) - r * K * np.exp(-r * T) *
                     si.norm.cdf(d2, 0.0, 1.0) + q * S *
                     np.exp(-q * T) * si.norm.cdf(d1, 0.0, 1.0)

        theta put = -np.exp(-q * T) *
                     (S * si.norm.cdf(d1, 0.0, 1.0) * sigma) /
                     (2 * np.sqrt(T)) + r * K * np.exp(-r * T) *
                     si.norm.cdf(-d2, 0.0, 1.0) - q * S *
                     np.exp(-q * T) * si.norm.cdf(-d1, 0.0, 1.0)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)
        d2 = self._calculate_d2(spot_price, time_to_maturity, volatility)

        theta_call = (-np.exp(-self.dividend * time_to_maturity) * spot_price * stats.norm.cdf(d1, 0.0, 1.0) * volatility / (2 * np.sqrt(time_to_maturity))) - \
                     (self.risk_free_rate * self.strike_price * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(d2, 0.0, 1.0)) + \
                     (self.dividend * spot_price * np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(d1, 0.0, 1.0))

        theta_put = (-np.exp(-self.dividend * time_to_maturity) * spot_price * stats.norm.cdf(d1, 0.0, 1.0) * volatility / (2 * np.sqrt(time_to_maturity))) + \
            (self.risk_free_rate * self.strike_price * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(-d2, 0.0, 1.0)) - \
            (self.dividend * spot_price * np.exp(-self.dividend * time_to_maturity) * stats.norm.cdf(-d1, 0.0, 1.0))

        self.theta_call = theta_call / 365.0
        self.theta_put = theta_put / 365.0

        return self.theta_call, self.theta_put

    def calculate_vega(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option vega based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        vega call & put = 1 / np.sqrt(2 * np.pi) * S * np.exp(-q * T) * np.exp(-d1 ** 2 * 0.5) * np.sqrt(T)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''
        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = self._calculate_d1(spot_price, time_to_maturity, volatility)

        vega = 1 / np.sqrt(2 * np.pi) * spot_price * np.exp(-self.dividend * time_to_maturity) * np.exp(-d1 ** 2 * 0.5) * np.sqrt(time_to_maturity)

        self.vega_call = self.vega_put = vega / 100.0

        return self.vega_call, self.vega_put

    def calculate_rho(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> tuple[float, float]:
        ''' Calculate Call and Put option rho based on the below equations from Black-Scholes.
        If dividend is not zero, then it is subtracted from the risk free rate in the below calculations.

        vega call & put = 1 / np.sqrt(2 * np.pi) * S * np.exp(-q * T) * np.exp(-d1 ** 2 * 0.5) * np.sqrt(T)

        :return: <float>, <float> Calculated delta of Call & Put options
        '''
        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d2 = self._calculate_d2(spot_price, time_to_maturity, volatility)

        rho_call = self.strike_price * time_to_maturity * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(d2, 0.0, 1.0)
        rho_put = -self.strike_price * time_to_maturity * np.exp(-self.risk_free_rate * time_to_maturity) * stats.norm.cdf(-d2, 0.0, 1.0)

        self.rho_call = rho_call / 100.0
        self.rho_put = rho_put / 100.0

        return self.rho_call, self.rho_put

    def _calculate_d1(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> float:
        ''' Famous d1 variable from Black-Scholes model calculated as shown in:
                https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model

            d1 = (np.log(S / K) + (r - q + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        :return: <float>
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d1 = (np.log(spot_price / self.strike_price) +
              (self.risk_free_rate - self.dividend + 0.5 * volatility ** 2) * time_to_maturity) / (volatility * np.sqrt(time_to_maturity))

        return d1

    def _calculate_d2(self, spot_price: float = -1.0, time_to_maturity: float = -1.0, volatility: float = -1.0) -> float:
        ''' Famous d2 variable from Black-Scholes model calculated as shown in:
                https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model

            d2 = (np.log(S / K) + (r - q - 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        :return: <float>
        '''

        if spot_price <= 0.0:
            spot_price = self.spot_price

        if time_to_maturity <= 0.0:
            time_to_maturity = self.time_to_maturity

        if volatility <= 0.0:
            volatility = self.volatility

        d2 = (np.log(spot_price / self.strike_price) +
              (self.risk_free_rate - self.dividend - 0.5 * volatility ** 2) * time_to_maturity) / (volatility * np.sqrt(time_to_maturity))

        return d2


"""
Matrix LU Solver Methods
"""
import numpy as np

def Lsolve(L, b, tol=1.0e-12):
    """
    Lower triangle matrix solution
    """
    n = len(b)
    for k in range(n):
        if abs(L[k, k]) < tol:
            raise RuntimeError('Matrix L was singular.')

    # solve [L]{x} = {b} for {x} via forward substitution
    x = np.copy(b)
    x[0] = x[0] / L[0, 0]
    for k in range(1, n):
        x[k] = (x[k] - np.dot(L[k, 0:k], x[0:k])) / L[k, k]

    return x

def Usolve(U, b, tol=1.0e-12):
    """
    Upper triangle matrix solution
    """
    n = len(b)
    for k in range(n):
        if abs(U[k, k]) < tol:
            raise RuntimeError('Matrix U was singular.')

    # solve [U]{x} = {b} for {x} via backward substitution
    x = np.copy(b)
    x[n-1] = x[n-1] / U[n-1, n-1]
    for k in range(n-2, -1, -1):
        x[k] = (x[k] - np.dot(U[k, k+1:n], x[k+1:n])) / U[k, k]

    return x

def LUSolve(L, U, b, tol=1.0e-12):
    """
    Implements both Lsolve and Usolve to solve system of equations
    """
    # solve [L]{y} = {b} for {y}
    y = Lsolve(L, b, tol)
    # solve [U]{x} = {y} for {x}
    x = Usolve(U, y, tol)

    return x


# Reference Book: Python Data Science Handbook (page:(59-))
# Date(13 April, 2019) Day-3

# We’ll start with the standard NumPy import, under the alias np :
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn
seaborn.set()  # set plot style

# Often when you are faced with a large amount of data, a first step is to compute sum‐
# mary statistics for the data in question. Perhaps the most common summary statistics
# are the mean and standard deviation, which allow you to summarize the “typical” val‐
# ues in a dataset, but other aggregates are useful as well(the sum, product, median,
# minimum and maximum, quantiles, etc.).

npArray = np.random.random(100)
print('\nSumming the Values in an Array:')
print(round(sum(npArray), 2))  # 52.2
print(np.sum(npArray))  # 52.20359645164018

# big_array = np.random.rand(1000000)
# %timeit sum(big_array) # 10 loops, best of 3: 104 ms per loop
# %timeit np.sum(big_array) # 1000 loops, best of 3: 442 μs per loop

print('\nMinimum and Maximum:')
big_array = np.random.rand(1000000)
print(min(big_array))  # 2.1833396079973255e-07
print(max(big_array))  # 0.9999951829160936

# same function in numpy. numpy version of  the operation is more quickly
print(np.min(big_array))  # 2.1833396079973255e-07
print(np.max(big_array))  # 0.9999951829160936

# For min , max , sum , and several other NumPy aggregates, a shorter syntax is to use
# methods of the array object itself:
print(big_array.min(), big_array.max(), big_array.sum())
# 2.2839487603398823e-06 0.9999994729271785 499748.6111329345

print('\nMultidimensional aggregates:')
matrix_a = np.random.random((3, 4))
print(matrix_a)
'''
[[0.21434649 0.41680862 0.23136847 0.52651149]
 [0.95156885 0.79710065 0.39806083 0.23508013]
 [0.71166791 0.87808004 0.8898395  0.28141989]]
'''
print(matrix_a.sum())  # 4.589765666870639

# Aggregation functions take an additional argument specifying the axis along which
# the aggregate is computed. For example, we can find the minimum value within each
# column by specifying axis = 0:
print(matrix_a.min(axis=0))  # [0.17388545 0.48946081 0.01005003 0.28836908]
print(matrix_a.min(axis=1))  # [0.42009431 0.09302049 0.08362998]

print(matrix_a.max(axis=0))  # [0.74965632 0.93045538 0.84355002 0.81612704]
print(matrix_a.max(axis=1))  # [0.93045538 0.84355002 0.74965632]

'''
Table 2-3. Aggregation functions available in NumPy
Function Name  Nan-safe Version   Description
np.sum         np.nansum          Compute sum of elements
np.prod        np.nanprod         Compute product of elements
np.mean        np.nanmean         Compute median of elements
np.std         np.nanstd          Compute standard deviation
np.var         np.nanvar          Compute variance
np.min         np.nanmin          Find minimum value
np.max         np.nanmax          Find maximum value
np.argmin      np.nanargmin       Find index of minimum value
np.argmax      np.nanargmax       Find index of maximum value
np.median      np.nanmedian       Compute median of elements
np.percentile  np.nanpercentile   Compute rank-based statistics of elements
np.any         N/A                Evaluate whether any elements are true
np.all         N/A Evaluate       whether all elements are true
'''

print('\nExample: What Is the Average Height of US Presidents?:')
data = pd.read_csv(
    '/media/nahid/New Volume/GitHub/Numpy/president_heights.csv')
heights = np.array(data['height(cm)'])
print(pd.DataFrame(data))
'''
    order                    name  height(cm)
0       1       George Washington         189
1       2              John Adams         170
2       3        Thomas Jefferson         189
3       4           James Madison         163
4       5            James Monroe         183
5       6       John Quincy Adams         171
6       7          Andrew Jackson         185
7       8        Martin Van Buren         168
8       9  William Henry Harrison         173
9      10              John Tyler         183
10     11           James K. Polk         173
11     12          Zachary Taylor         173
12     13        Millard Fillmore         175
13     14         Franklin Pierce         178
14     15          James Buchanan         183
15     16         Abraham Lincoln         193
16     17          Andrew Johnson         178
17     18        Ulysses S. Grant         173
18     19     Rutherford B. Hayes         174
19     20       James A. Garfield         183
20     21       Chester A. Arthur         183
21     23       Benjamin Harrison         168
22     25        William McKinley         170
23     26      Theodore Roosevelt         178
24     27     William Howard Taft         182
25     28          Woodrow Wilson         180
26     29       Warren G. Harding         183
27     30         Calvin Coolidge         178
28     31          Herbert Hoover         182
29     32   Franklin D. Roosevelt         188
30     33         Harry S. Truman         175
31     34    Dwight D. Eisenhower         179
32     35         John F. Kennedy         183
33     36       Lyndon B. Johnson         193
34     37           Richard Nixon         182
35     38             Gerald Ford         183
36     39            Jimmy Carter         177
37     40           Ronald Reagan         185
38     41       George H. W. Bush         188
39     42            Bill Clinton         188
40     43          George W. Bush         182
41     44            Barack Obama         185
'''
print(heights)
'''
[189 170 189 163 183 171 185 168 173 183 173 173 175 178 183 193 178 173
 174 183 183 168 170 178 182 180 183 178 182 188 175 179 183 193 182 183
 177 185 188 188 182 185]
'''
print('Mean Heigt:         ', heights.mean())
print('Standerd Deviation: ', heights.std())
print('Maximum Height:     ', heights.min())
print('Minimum Height:     ', heights.max())
'''
Mean Heigt:          179.73809523809524
Standerd Deviation:  6.931843442745892
Maximum Height:      163
Minimum Height:      193
'''
print("25th Percentile:   ", np.percentile(heights, 25))
print("Median:            ", np.median(heights))
print("75th Percentile:   ", np.percentile(heights, 75))
'''
25th Percentile:    174.25
Median:             182.0
75th Percentile:    183.0
'''
plt.hist(heights)
plt.title('Height Distribution of US Presidents')
plt.xlabel('Height(cm)')
plt.ylabel('Number')

print(plt.show())

'''
These aggregates are some of the fundamental pieces of exploratory data analysis that
we’ll explore in more depth in later chapters of the book.
'''

# https://matplotlib.org/users/pyplot_tutorial.html
# https://github.com/jakevdp/PythonDataScienceHandbook/blob/master/notebooks/data/president_heights.csv